diff --git a/.changeset/dual-source-contracts-convergence.md b/.changeset/dual-source-contracts-convergence.md new file mode 100644 index 0000000000..8405c24d52 --- /dev/null +++ b/.changeset/dual-source-contracts-convergence.md @@ -0,0 +1,80 @@ +--- +"@objectstack/spec": major +"@objectstack/service-analytics": major +"@objectstack/metadata": patch +--- + +feat(spec)!: converge the 11 contracts-vs-domain dual-source type names (#4538) + +`packages/spec/src/contracts/` hand-wrote parameter/result interfaces whose +names collided with same-named zod-derived types in the domains — the #4411 +trap, tracked as 11 rows of `dual-source-exports.baseline.json`. Each name was +judged individually against a three-repo import-level scan (framework, cloud, +objectui): which declaration actually flows at runtime decides the direction. +All 11 rows are deleted from the baseline; no name below is exported twice +anymore. + +**Converged — `./contracts` now re-exports the domain zod type (same +declaration on both entries, imports keep compiling from either):** + +- `NotificationChannel` → `system/notification.zod`'s + `z.infer` (member sets were identical). +- `ValidationResult` → `kernel/plugin-validator.zod` (shapes were identical). +- `HealthStatus` → `kernel/startup-orchestrator.zod` (`details` narrows + `Record` → `Record`). +- `PluginStartupResult` → `kernel/startup-orchestrator.zod`. FROM `plugin: + Plugin` (live object) and `error?: Error` TO the serializable projection + (`plugin: { name, version? }`-passthrough, `error?: { name, message, + stack?, code? }`). Neither side had any consumer outside spec; the + zod-validatable shape wins. +- `StartupOptions` → `kernel/startup-orchestrator.zod` — the PARSED tier + (defaults applied). `IStartupOrchestrator.orchestrateStartup` now takes + `StartupOptionsInput` (the caller-authored all-optional tier, also + re-exported from `./contracts`). Fix for callers typed to the old + all-optional `StartupOptions`: rename to `StartupOptionsInput`. +- `JobExecution` → `system/job.zod`. The system schema's `duration` field is + RENAMED `durationMs` — that is what every job adapter produces and what the + `sys_job_run.duration_ms` column round-trips; the schema described records + nothing ever wrote. Fix: `duration` → `durationMs` when parsing + `JobExecutionSchema` payloads. +- `AnalyticsQuery` → `data/analytics.zod`. The domain schema aligned to the + contract's semantics first: `timezone` LOST its `.default('UTC')` — absence + is meaningful (the engine resolves org timezone, #1982/#2018; the + `/analytics` entry always refused to apply that default). The schema is now + transform-free, so `AnalyticsQuery` ≡ `AnalyticsQueryInput` (both kept + exported). Fix for code that relied on `.parse()` injecting `timezone: + 'UTC'`: pass the timezone explicitly or resolve it via the engine chain + (`selection.timezone ?? context.timezone ?? 'UTC'`). + +**Renamed — two genuinely different concepts were sharing one name (both +flow at runtime):** + +- `./contracts` `DriverCapabilities` → **`AnalyticsDriverCapabilities`** + (`{ nativeSql, objectqlAggregate, inMemory }`, the analytics strategy-chain + execution-path probe). The `DriverCapabilities` name now belongs solely to + the data domain's driver feature-flag record (`DriverCapabilitiesSchema`, + what `IDataDriver.supports` declares). Fix: importers of the trio from + `@objectstack/spec/contracts` (or `@objectstack/service-analytics`, whose + re-export is renamed in lockstep) rename the import; importers who meant + the driver flags import `DriverCapabilities` from `@objectstack/spec/data`. + +**Removed — the domain-side declaration was dead (zero import-level consumers +in framework/cloud/objectui; the #4411 family's last survivors):** + +- `system` `MetadataExportOptionsSchema` / `MetadataExportOptions` and + `MetadataImportOptionsSchema` / `MetadataImportOptions` (the + `output`/`source`-directory bags). The names now have ONE declaration each: + the `IMetadataService.exportMetadata` / `importMetadata` parameter + interfaces on `./contracts` (`types`/`namespaces`/`format` and + `conflictResolution`/`validate`/`dryRun`), which `MetadataManager` + implements. No tombstone/D2 conversion, deliberately — these are runtime + option-bag types, not authorable metadata (same reasoning as #4458). + `@objectstack/metadata` re-exports the two names from `./contracts` now + (it previously re-exported the dead system-side shapes its own manager + did not accept). +- `system` `JobSchedule` (the `= Schedule` back-compat alias). The name's one + declaration is the `IJobService.schedule` boundary shape on `./contracts` + (plain-string cron `expression`); the authored metadata type keeps its real + name `Schedule`. Fix: `import type { JobSchedule } from + '@objectstack/spec/system'` → `Schedule` (authoring tier) or the + `./contracts` `JobSchedule` (service boundary), whichever you meant. diff --git a/content/docs/references/api/analytics.mdx b/content/docs/references/api/analytics.mdx index 255bfdd664..9ba3f9a605 100644 --- a/content/docs/references/api/analytics.mdx +++ b/content/docs/references/api/analytics.mdx @@ -66,7 +66,7 @@ const result = AnalyticsEndpoint.parse(data); | **order** | `Record>` | optional | | | **limit** | `number` | optional | | | **offset** | `number` | optional | | -| **timezone** | `string` | ✅ | | +| **timezone** | `string` | optional | | | **query** | `any` | optional | [REMOVED] `query` was removed from AnalyticsQueryRequest in @objectstack/spec 17.0.0 (#3878). The `{ cube, query: {...}` } envelope was the dialect of the retired degraded analytics shim (#3891) — the real engine never understood it. Move the query.* fields to the body top level: `{ cube, measures, dimensions?, where?, timeDimensions?, order?, limit?, offset?, timezone? }`. | | **format** | `any` | optional | [REMOVED] `format` was removed from AnalyticsQueryRequest in @objectstack/spec 17.0.0 (#3878). It was never implemented — every response is the JSON envelope. Delete the key; for CSV/XLSX use the export surface instead. | diff --git a/content/docs/references/data/analytics.mdx b/content/docs/references/data/analytics.mdx index eccf3d83f7..5e83ae01bd 100644 --- a/content/docs/references/data/analytics.mdx +++ b/content/docs/references/data/analytics.mdx @@ -62,7 +62,7 @@ const result = AggregationMetricType.parse(data); | **order** | `Record>` | optional | | | **limit** | `number` | optional | | | **offset** | `number` | optional | | -| **timezone** | `string` | ✅ | | +| **timezone** | `string` | optional | | --- diff --git a/content/docs/references/system/job.mdx b/content/docs/references/system/job.mdx index e8d400a4b6..4eff66e034 100644 --- a/content/docs/references/system/job.mdx +++ b/content/docs/references/system/job.mdx @@ -87,7 +87,7 @@ const result = CronSchedule.parse(data); | **completedAt** | `string` | optional | ISO 8601 datetime when execution completed | | **status** | `Enum<'running' \| 'success' \| 'failed' \| 'timeout'>` | ✅ | Execution status | | **error** | `string` | optional | Error message if failed | -| **duration** | `integer` | optional | Execution duration in milliseconds | +| **durationMs** | `integer` | optional | Execution duration in milliseconds | --- diff --git a/content/docs/references/system/metadata-persistence.mdx b/content/docs/references/system/metadata-persistence.mdx index d10d4cd0f2..758c99d7b7 100644 --- a/content/docs/references/system/metadata-persistence.mdx +++ b/content/docs/references/system/metadata-persistence.mdx @@ -16,8 +16,8 @@ Defines the lifecycle and mutability of a metadata item. ## TypeScript Usage ```typescript -import { MetadataCollectionInfo, MetadataDiffResult, MetadataExportOptions, MetadataHistoryQueryOptions, MetadataHistoryQueryResult, MetadataHistoryRecord, MetadataHistoryRetentionPolicy, MetadataImportOptions, MetadataLoadOptions, MetadataLoadResult, MetadataLoaderContract, MetadataRecord, MetadataSaveOptions, MetadataSaveResult, MetadataScope, MetadataSource, MetadataState, MetadataStats, MetadataWatchEvent, PackagePublishResult } from '@objectstack/spec/system'; -import type { MetadataCollectionInfo, MetadataDiffResult, MetadataExportOptions, MetadataHistoryQueryOptions, MetadataHistoryQueryResult, MetadataHistoryRecord, MetadataHistoryRetentionPolicy, MetadataImportOptions, MetadataLoadOptions, MetadataLoadResult, MetadataLoaderContract, MetadataRecord, MetadataSaveOptions, MetadataSaveResult, MetadataScope, MetadataSource, MetadataState, MetadataStats, MetadataWatchEvent, PackagePublishResult } from '@objectstack/spec/system'; +import { MetadataCollectionInfo, MetadataDiffResult, MetadataHistoryQueryOptions, MetadataHistoryQueryResult, MetadataHistoryRecord, MetadataHistoryRetentionPolicy, MetadataLoadOptions, MetadataLoadResult, MetadataLoaderContract, MetadataRecord, MetadataSaveOptions, MetadataSaveResult, MetadataScope, MetadataSource, MetadataState, MetadataStats, MetadataWatchEvent, PackagePublishResult } from '@objectstack/spec/system'; +import type { MetadataCollectionInfo, MetadataDiffResult, MetadataHistoryQueryOptions, MetadataHistoryQueryResult, MetadataHistoryRecord, MetadataHistoryRetentionPolicy, MetadataLoadOptions, MetadataLoadResult, MetadataLoaderContract, MetadataRecord, MetadataSaveOptions, MetadataSaveResult, MetadataScope, MetadataSource, MetadataState, MetadataStats, MetadataWatchEvent, PackagePublishResult } from '@objectstack/spec/system'; // Validate data const result = MetadataCollectionInfo.parse(data); @@ -55,20 +55,6 @@ const result = MetadataCollectionInfo.parse(data); | **summary** | `string` | optional | Human-readable summary of changes | ---- - -## MetadataExportOptions - -### Properties - -| Property | Type | Required | Description | -| :--- | :--- | :--- | :--- | -| **types** | `string[]` | optional | | -| **namespaces** | `string[]` | optional | | -| **output** | `string` | ✅ | Output directory or file | -| **format** | `Enum<'yaml' \| 'json' \| 'typescript' \| 'javascript'>` | ✅ | Metadata file format | - - --- ## MetadataHistoryQueryOptions @@ -135,19 +121,6 @@ const result = MetadataCollectionInfo.parse(data); | **cleanupIntervalHours** | `integer` | ✅ | How often to run cleanup (in hours) | ---- - -## MetadataImportOptions - -### Properties - -| Property | Type | Required | Description | -| :--- | :--- | :--- | :--- | -| **source** | `string` | ✅ | Input directory or file | -| **strategy** | `Enum<'merge' \| 'replace' \| 'skip'>` | ✅ | | -| **validate** | `boolean` | ✅ | | - - --- ## MetadataLoadOptions diff --git a/packages/metadata/src/index.ts b/packages/metadata/src/index.ts index dd5ee69ec8..e976446a26 100644 --- a/packages/metadata/src/index.ts +++ b/packages/metadata/src/index.ts @@ -45,8 +45,6 @@ export type { MetadataStats, MetadataLoadOptions, MetadataSaveOptions, - MetadataExportOptions, - MetadataImportOptions, MetadataLoadResult, MetadataSaveResult, MetadataWatchEvent, @@ -60,11 +58,18 @@ export type { MetadataHistoryRetentionPolicy, } from '@objectstack/spec/system'; -// Re-export IMetadataService contract +// Re-export IMetadataService contract. +// [#4538] `MetadataExportOptions` / `MetadataImportOptions` moved into this +// block: this package used to re-export the same-named system-entry bags +// (`output`/`source`-flavored, removed with #4538) while `MetadataManager` +// implements the contracts shapes — the public re-export was pointing at the +// wrong declaration. export type { IMetadataService, MetadataWatchCallback, MetadataWatchHandle, + MetadataExportOptions, + MetadataImportOptions, MetadataTypeInfo, MetadataImportResult, } from '@objectstack/spec/contracts'; diff --git a/packages/plugins/driver-memory/src/memory-analytics.test.ts b/packages/plugins/driver-memory/src/memory-analytics.test.ts index 1d42b13245..f0cbbe3bce 100644 --- a/packages/plugins/driver-memory/src/memory-analytics.test.ts +++ b/packages/plugins/driver-memory/src/memory-analytics.test.ts @@ -7,15 +7,15 @@ import { AnalyticsQuerySchema, defineCube } from '@objectstack/spec/data'; import type { AnalyticsQuery, AnalyticsQueryInput, Cube } from '@objectstack/spec/data'; /** - * Author-tier literal → the parsed `AnalyticsQuery` the service contract takes. + * Validate a literal through the schema before handing it to the service — + * the same route a real request body takes (the REST layer validates against + * the schema and forwards). * - * `timezone` is `.default('UTC')` on the schema, so it is optional to write and - * required on the parsed type — the two tiers are genuinely different types. A - * real query reaches `query()` through the schema (the REST layer parses the - * request body), so these tests take the same route rather than hand-writing - * the filled-in default: the parse IS the proof that the default lands. Until - * #4311 no tsc read this file, so 19 author-tier literals sat unnoticed in a - * parameter that had required `timezone` all along. + * [#4538] The two tiers collapsed: `AnalyticsQuerySchema` no longer carries + * any `.default()`/`.transform()` (`timezone` is genuinely optional — absence + * means the engine resolves org timezone, #1982/#2018), so `AnalyticsQuery` + * and `AnalyticsQueryInput` are the same shape and the parse is validation + * only. The helper stays so every test query is proven schema-valid. */ const asQuery = (input: AnalyticsQueryInput): AnalyticsQuery => AnalyticsQuerySchema.parse(input); diff --git a/packages/runtime/src/domains/analytics.ts b/packages/runtime/src/domains/analytics.ts index 402c6da84c..e0850db612 100644 --- a/packages/runtime/src/domains/analytics.ts +++ b/packages/runtime/src/domains/analytics.ts @@ -38,8 +38,13 @@ import type { DomainHandlerDeps, DomainRoute } from '../domain-handler-registry. * "unrecognized key" — gets a bespoke hint at the contract field `where`. * * Validation only — the ORIGINAL body is forwarded to the service untouched. - * (Parsing would inject the schema's `timezone: 'UTC'` default and silently - * override the engine's org-timezone resolution, #1982/#2018.) + * (Historically load-bearing: the schema carried a `timezone: 'UTC'` default + * that parsing would have injected, silently overriding the engine's + * org-timezone resolution, #1982/#2018. #4538 removed that default from + * `AnalyticsQuerySchema` itself — the schema is transform-free now, so + * validated body ≡ parsed output by construction — but forwarding the + * original body stays the rule: it keeps this entry immune to any future + * default someone adds to the schema without re-reading this file.) */ function assertAnalyticsQueryBody(body: unknown): void { if (body && typeof body === 'object' && !Array.isArray(body)) { diff --git a/packages/services/service-analytics/src/__tests__/analytics-service.test.ts b/packages/services/service-analytics/src/__tests__/analytics-service.test.ts index 790f4c60d8..73f35e3724 100644 --- a/packages/services/service-analytics/src/__tests__/analytics-service.test.ts +++ b/packages/services/service-analytics/src/__tests__/analytics-service.test.ts @@ -7,7 +7,7 @@ import { AnalyticsService } from '../analytics-service.js'; import { CubeRegistry } from '../cube-registry.js'; import { NativeSQLStrategy } from '../strategies/native-sql-strategy.js'; import { ObjectQLStrategy } from '../strategies/objectql-strategy.js'; -import type { DriverCapabilities } from '../strategies/types.js'; +import type { AnalyticsDriverCapabilities } from '../strategies/types.js'; // ───────────────────────────────────────────────────────────────── // Test fixtures diff --git a/packages/services/service-analytics/src/analytics-service.ts b/packages/services/service-analytics/src/analytics-service.ts index 7f8d1a9c8e..00e67d10c6 100644 --- a/packages/services/service-analytics/src/analytics-service.ts +++ b/packages/services/service-analytics/src/analytics-service.ts @@ -13,7 +13,7 @@ import type { Dataset } from '@objectstack/spec/ui'; import type { Logger } from '@objectstack/spec/contracts'; import { createLogger, bucketKeyToCalendarRange, zonedDateStartToUtcMs } from '@objectstack/core'; import { CubeRegistry } from './cube-registry.js'; -import type { AnalyticsStrategy, DriverCapabilities, StrategyContext } from './strategies/types.js'; +import type { AnalyticsStrategy, AnalyticsDriverCapabilities, StrategyContext } from './strategies/types.js'; import { NativeSQLStrategy } from './strategies/native-sql-strategy.js'; import { ObjectQLStrategy } from './strategies/objectql-strategy.js'; import { compileDataset, type CompiledDataset, type RelationshipResolver } from './dataset-compiler.js'; @@ -112,7 +112,7 @@ export interface AnalyticsServiceConfig { * Probe driver capabilities for the object that backs a cube. * The service calls this function to decide which strategy can handle a query. */ - queryCapabilities?: (cubeName: string) => DriverCapabilities; + queryCapabilities?: (cubeName: string) => AnalyticsDriverCapabilities; /** * Execute raw SQL on the driver for a given object. * Required for NativeSQLStrategy. @@ -287,7 +287,7 @@ export interface AnalyticsServiceConfig { /** * Default capabilities when probing is not configured — assumes in-memory only. */ -const DEFAULT_CAPABILITIES: DriverCapabilities = { +const DEFAULT_CAPABILITIES: AnalyticsDriverCapabilities = { nativeSql: false, objectqlAggregate: false, inMemory: true, diff --git a/packages/services/service-analytics/src/index.ts b/packages/services/service-analytics/src/index.ts index 19573997df..4b72b4d6dc 100644 --- a/packages/services/service-analytics/src/index.ts +++ b/packages/services/service-analytics/src/index.ts @@ -35,6 +35,6 @@ export { compileScopedFilterToSql } from './read-scope-sql.js'; // Strategies export { NativeSQLStrategy } from './strategies/native-sql-strategy.js'; export { ObjectQLStrategy } from './strategies/objectql-strategy.js'; -export type { AnalyticsStrategy, StrategyContext, DriverCapabilities } from './strategies/types.js'; +export type { AnalyticsStrategy, StrategyContext, AnalyticsDriverCapabilities } from './strategies/types.js'; // Note: InMemoryStrategy is exported from @objectstack/driver-memory diff --git a/packages/services/service-analytics/src/plugin.ts b/packages/services/service-analytics/src/plugin.ts index 256b723b25..135d9d4246 100644 --- a/packages/services/service-analytics/src/plugin.ts +++ b/packages/services/service-analytics/src/plugin.ts @@ -6,7 +6,7 @@ import type { ExecutionContext } from '@objectstack/spec/kernel'; import type { IAnalyticsService, IDataDriver } from '@objectstack/spec/contracts'; import { AnalyticsService } from './analytics-service.js'; import type { AnalyticsServiceConfig } from './analytics-service.js'; -import type { DriverCapabilities } from './strategies/types.js'; +import type { AnalyticsDriverCapabilities } from './strategies/types.js'; import { pickDisplayField, type DimensionLabelDeps } from './dimension-labels.js'; /** @@ -77,7 +77,7 @@ export interface AnalyticsServicePluginOptions { * Probe driver capabilities for a given cube. * When omitted, defaults to in-memory only. */ - queryCapabilities?: (cubeName: string) => DriverCapabilities; + queryCapabilities?: (cubeName: string) => AnalyticsDriverCapabilities; /** * Execute raw SQL on a driver. Enables NativeSQLStrategy. */ diff --git a/packages/services/service-analytics/src/strategies/types.ts b/packages/services/service-analytics/src/strategies/types.ts index f585468cb8..6b00d93986 100644 --- a/packages/services/service-analytics/src/strategies/types.ts +++ b/packages/services/service-analytics/src/strategies/types.ts @@ -3,9 +3,14 @@ /** * Strategy pattern types — re-exported from @objectstack/spec/contracts * for convenience. The canonical definitions live in the spec package. + * + * [#4538] `DriverCapabilities` → `AnalyticsDriverCapabilities`: the old name + * belonged to the data domain's driver feature-flag record + * (`DriverCapabilitiesSchema` — every `IDataDriver.supports`); the analytics + * execution-path trio was renamed with its spec declaration. */ export type { AnalyticsStrategy, StrategyContext, - DriverCapabilities, + AnalyticsDriverCapabilities, } from '@objectstack/spec/contracts'; diff --git a/packages/spec/api-surface.json b/packages/spec/api-surface.json index 65c150d4ff..6bce99d899 100644 --- a/packages/spec/api-surface.json +++ b/packages/spec/api-surface.json @@ -985,7 +985,6 @@ "JobExecutionSchema (const)", "JobExecutionStatus (type)", "JobInput (type)", - "JobSchedule (type)", "JobSchema (const)", "KernelServiceMapSchema (const)", "KeyManagementProvider (type)", @@ -1032,8 +1031,6 @@ "MetadataCollectionInfoSchema (const)", "MetadataDiffResult (type)", "MetadataDiffResultSchema (const)", - "MetadataExportOptions (type)", - "MetadataExportOptionsSchema (const)", "MetadataFallbackStrategy (type)", "MetadataFallbackStrategySchema (const)", "MetadataFormType (type)", @@ -1047,8 +1044,6 @@ "MetadataHistoryRecordSchema (const)", "MetadataHistoryRetentionPolicy (type)", "MetadataHistoryRetentionPolicySchema (const)", - "MetadataImportOptions (type)", - "MetadataImportOptionsSchema (const)", "MetadataLoadOptions (type)", "MetadataLoadOptionsSchema (const)", "MetadataLoadResult (type)", @@ -3648,7 +3643,9 @@ "APPROVAL_STATUSES (const)", "AdapterContext (interface)", "AdapterSearchOptions (interface)", - "AnalyticsQuery (interface)", + "AnalyticsDriverCapabilities (interface)", + "AnalyticsQuery (type)", + "AnalyticsQueryInput (type)", "AnalyticsResult (interface)", "AnalyticsStrategy (interface)", "ApprovalActionAttachment (interface)", @@ -3694,7 +3691,6 @@ "DelegableAdminScope (interface)", "DelegableScope (interface)", "DeployExecutionResult (interface)", - "DriverCapabilities (interface)", "EMBEDDER_SERVICE (const)", "EmailAddress (type)", "EmailAttachment (interface)", @@ -3709,7 +3705,7 @@ "GenerateDraftOpts (interface)", "GenerateObjectOptions (interface)", "GrantShareInput (interface)", - "HealthStatus (interface)", + "HealthStatus (type)", "HierarchyScope (type)", "HierarchyScopeContext (interface)", "IAIConversationService (interface)", @@ -3783,7 +3779,7 @@ "IntrospectedIndex (interface)", "IntrospectedSchema (interface)", "IntrospectedTable (interface)", - "JobExecution (interface)", + "JobExecution (type)", "JobHandler (type)", "JobRetryPolicy (interface)", "JobSchedule (interface)", @@ -3823,7 +3819,7 @@ "PendingActionStatus (type)", "PlanUpgradeInput (interface)", "Plugin (interface)", - "PluginStartupResult (interface)", + "PluginStartupResult (type)", "PresignedDownloadDescriptor (interface)", "PresignedDownloadOptions (interface)", "PresignedUploadDescriptor (interface)", @@ -3889,7 +3885,8 @@ "SharingRuleRow (interface)", "SmsDeliveryStatus (type)", "SmsTransportSendResult (interface)", - "StartupOptions (interface)", + "StartupOptions (type)", + "StartupOptionsInput (type)", "StorageFileInfo (interface)", "StorageUploadOptions (interface)", "StrategyContext (interface)", @@ -3906,7 +3903,7 @@ "UploadArtifactInput (interface)", "UploadArtifactResult (interface)", "UserModelMessage (type)", - "ValidationResult (interface)", + "ValidationResult (type)", "WriteObservabilityOptions (interface)" ], "./integration": [ diff --git a/packages/spec/authorable-surface.json b/packages/spec/authorable-surface.json index 3ea00869a6..5272fb7ca2 100644 --- a/packages/spec/authorable-surface.json +++ b/packages/spec/authorable-surface.json @@ -6219,7 +6219,7 @@ "system/Job:schedule", "system/Job:timeout", "system/JobExecution:completedAt", - "system/JobExecution:duration", + "system/JobExecution:durationMs", "system/JobExecution:error", "system/JobExecution:jobId", "system/JobExecution:startedAt", @@ -6326,10 +6326,6 @@ "system/MetadataDiffResult:type", "system/MetadataDiffResult:version1", "system/MetadataDiffResult:version2", - "system/MetadataExportOptions:format", - "system/MetadataExportOptions:namespaces", - "system/MetadataExportOptions:output", - "system/MetadataExportOptions:types", "system/MetadataHistoryQueryOptions:includeMetadata", "system/MetadataHistoryQueryOptions:limit", "system/MetadataHistoryQueryOptions:offset", @@ -6356,9 +6352,6 @@ "system/MetadataHistoryRetentionPolicy:cleanupIntervalHours", "system/MetadataHistoryRetentionPolicy:maxAgeDays", "system/MetadataHistoryRetentionPolicy:maxVersions", - "system/MetadataImportOptions:source", - "system/MetadataImportOptions:strategy", - "system/MetadataImportOptions:validate", "system/MetadataLoadOptions:cache", "system/MetadataLoadOptions:ifNoneMatch", "system/MetadataLoadOptions:limit", diff --git a/packages/spec/dual-source-exports.baseline.json b/packages/spec/dual-source-exports.baseline.json index 3f28016eb5..fd934fa2d7 100644 --- a/packages/spec/dual-source-exports.baseline.json +++ b/packages/spec/dual-source-exports.baseline.json @@ -3,36 +3,27 @@ "entries": [ "ActionLocationSchema — [./studio (const)] ≠ [./ui (const)]", "ActivationEventSchema — [./kernel (const)] ≠ [./studio (const)]", - "AnalyticsQuery — [./contracts (interface)] ≠ [./data (type)]", "ConflictResolution — [./automation (type)] ≠ [./integration (type)] ≠ [./ui (type)]", "ConflictResolutionSchema — [./automation (const)] ≠ [./integration (const)] ≠ [./ui (const)]", "DataSyncConfig — [./automation (type)] ≠ [./integration (type)]", "DataSyncConfigSchema — [./automation (const)] ≠ [./integration (const)]", - "DriverCapabilities — [./contracts (interface)] ≠ [./data (type)]", "EnvironmentArtifact — [./cloud (type)] ≠ [./system (type)]", "EnvironmentArtifactInput — [./cloud (type)] ≠ [./system (type)]", "EnvironmentArtifactSchema — [./cloud (const)] ≠ [./system (const)]", "EventSchema — [./automation (const)] ≠ [./kernel (const)]", "FieldMapping — [./data (type)] ≠ [./integration (type)] ≠ [./shared (type)]", "FieldMappingSchema — [./data (const)] ≠ [./integration (const)] ≠ [./shared (const)]", - "HealthStatus — [./contracts (interface)] ≠ [./kernel (type)]", "HttpMethod — [./api, ./shared (type)] ≠ [./ui (type)]", "HttpRequest — [./shared (type)] ≠ [./ui (type)]", - "JobExecution — [./contracts (interface)] ≠ [./system (type)]", - "JobSchedule — [./contracts (interface)] ≠ [./system (type)]", "MetadataBulkRegisterRequestSchema — [./api (const)] ≠ [./kernel (const)]", "MetadataEvent — [./api (type)] ≠ [./kernel (type)]", "MetadataEventSchema — [./api (const)] ≠ [./kernel (const)]", - "MetadataExportOptions — [./contracts (interface)] ≠ [./system (type)]", - "MetadataImportOptions — [./contracts (interface)] ≠ [./system (type)]", "Notification — [./api (type)] ≠ [./ui (type)]", - "NotificationChannel — [./contracts (type)] ≠ [./system (type)]", "NotificationConfig — [./system (type)] ≠ [./ui (type)]", "NotificationConfigSchema — [./system (const)] ≠ [./ui (const)]", "NotificationSchema — [./api (const)] ≠ [./ui (const)]", "PackageDependency — [./cloud (type)] ≠ [./kernel (type)]", "PackageDependencySchema — [./cloud (const)] ≠ [./kernel (const)]", - "PluginStartupResult — [./contracts (interface)] ≠ [./kernel (type)]", "RateLimitConfig — [./integration (type)] ≠ [./shared (type)]", "RateLimitConfigSchema — [./integration (const)] ≠ [./shared (const)]", "RetryPolicy — [./automation (type)] ≠ [./system (type)]", @@ -40,11 +31,9 @@ "Session — [./api (type)] ≠ [./identity (type)]", "SessionSchema — [./api (const)] ≠ [./identity (const)]", "ShareRecipientType — [./contracts (type)] ≠ [./security (const)]", - "StartupOptions — [./contracts (interface)] ≠ [./kernel (type)]", "TenantPlan — [./cloud (type)] ≠ [./system (type)]", "TenantPlanSchema — [./cloud (const)] ≠ [./system (const)]", "TransformType — [./data (const)] ≠ [./shared (type)]", - "ValidationResult — [./contracts (interface)] ≠ [./kernel (type)]", "WebhookConfig — [./api (type)] ≠ [./integration (type)]", "WebhookConfigSchema — [./api (const)] ≠ [./integration (const)]", "WebhookEvent — [./api (type)] ≠ [./integration (type)]", diff --git a/packages/spec/json-schema.manifest.json b/packages/spec/json-schema.manifest.json index 95041dcfa7..cdc7a9a284 100644 --- a/packages/spec/json-schema.manifest.json +++ b/packages/spec/json-schema.manifest.json @@ -1343,14 +1343,12 @@ "system/MessageQueueProvider", "system/MetadataCollectionInfo", "system/MetadataDiffResult", - "system/MetadataExportOptions", "system/MetadataFallbackStrategy", "system/MetadataFormat", "system/MetadataHistoryQueryOptions", "system/MetadataHistoryQueryResult", "system/MetadataHistoryRecord", "system/MetadataHistoryRetentionPolicy", - "system/MetadataImportOptions", "system/MetadataLoadOptions", "system/MetadataLoadResult", "system/MetadataLoaderContract", diff --git a/packages/spec/src/contracts/analytics-service.ts b/packages/spec/src/contracts/analytics-service.ts index 7d426533c7..d9d3d275e9 100644 --- a/packages/spec/src/contracts/analytics-service.ts +++ b/packages/spec/src/contracts/analytics-service.ts @@ -1,6 +1,6 @@ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. -import type { Cube } from '../data/analytics.zod.js'; +import type { AnalyticsQuery, Cube } from '../data/analytics.zod.js'; import type { FilterCondition } from '../data/filter.zod.js'; import type { PercentScale } from '../data/percent-scale.js'; import type { ExecutionContext } from '../kernel/execution-context.zod.js'; @@ -20,44 +20,19 @@ import type { Dataset } from '../ui/dataset.zod.js'; */ /** - * An analytical query definition + * An analytical query definition. + * + * [#4538] Re-exported from the zod source (`AnalyticsQuerySchema`, + * data/analytics.zod.ts) instead of a hand-written mirror — the mirror had + * drifted (`where` had decayed to `Record` where the schema + * declares the canonical `FilterCondition`; `timeDimensions[].granularity` + * to bare `string`). One shape, both tiers: the schema carries no + * `.default()`/`.transform()` — `timezone` is genuinely optional, because an + * absent timezone means "the engine resolves it" (org-timezone chain, + * #1982/#2018) — so what a caller authors is exactly what an executor + * receives. */ -export interface AnalyticsQuery { - /** Target cube name. Optional when cube is specified at a higher level (e.g. API request wrapper or cube-scoped endpoint). Implementations should validate presence at runtime. */ - cube?: string; - /** Measures to compute (e.g. ['orders.count', 'orders.totalRevenue']) */ - measures: string[]; - /** Dimensions to group by (e.g. ['orders.status', 'orders.createdAt']) */ - dimensions?: string[]; - /** - * WHERE clause — canonical filter shape per the unified Query DSL - * (see `FilterConditionSchema` in `spec/data/filter.zod.ts`). - * MongoDB-style: implicit equality, `$eq/$ne/$gt/$gte/$lt/$lte/ - * $in/$nin/$contains/...` operator wrappers, `$and/$or/$not` - * logical combinators. This is the same filter shape used by - * `find()`, dashboard widget `filter`, RLS, etc. - * - * @example - * ```ts - * { where: { is_active: true, stage: { $nin: ['lost'] } } } - * ``` - */ - where?: Record; - /** Time dimension configuration */ - timeDimensions?: Array<{ - dimension: string; - granularity?: string; - dateRange?: string | string[]; - }>; - /** Sort order for results */ - order?: Record; - /** Result limit */ - limit?: number; - /** Result offset */ - offset?: number; - /** Timezone for date/time calculations */ - timezone?: string; -} +export type { AnalyticsQuery, AnalyticsQueryInput } from '../data/analytics.zod.js'; /** * Analytics query result @@ -253,12 +228,19 @@ export interface IAnalyticsService { // ========================================== /** - * Driver capability descriptor. + * Analytics execution-path capability descriptor. * * Used by the strategy chain to decide at runtime which execution path * is available for a given cube / object. + * + * [#4538] Renamed from `DriverCapabilities`: that name belongs to the data + * domain's driver feature-flag record (`DriverCapabilitiesSchema`, + * data/driver.zod.ts — what every `IDataDriver.supports` declares), a + * genuinely different concept that was squatting behind the same name on + * this entry. This trio answers one narrow question — which analytics + * execution path can serve a cube — and now says so in its name. */ -export interface DriverCapabilities { +export interface AnalyticsDriverCapabilities { /** Driver supports native SQL execution (e.g. Postgres, MySQL, SQLite). */ nativeSql: boolean; /** Driver supports ObjectQL aggregate() operations. */ @@ -274,7 +256,7 @@ export interface StrategyContext { /** Resolve a cube definition by name. */ getCube(name: string): Cube | undefined; /** Probe driver capabilities for the object backing a cube. */ - queryCapabilities(cubeName: string): DriverCapabilities; + queryCapabilities(cubeName: string): AnalyticsDriverCapabilities; /** * Execute a raw SQL string on the driver that owns `objectName`. * Only available when `nativeSql` capability is true. diff --git a/packages/spec/src/contracts/job-service.ts b/packages/spec/src/contracts/job-service.ts index cfd355cdee..10c4f1af87 100644 --- a/packages/spec/src/contracts/job-service.ts +++ b/packages/spec/src/contracts/job-service.ts @@ -13,8 +13,23 @@ * Aligned with CoreServiceName 'job' in core-services.zod.ts. */ +// [#4538] `JobExecution` is the system domain's zod-derived type — one +// declaration, re-exported here for the IJobService surface below. +import type { JobExecution } from '../system/job.zod'; +export type { JobExecution } from '../system/job.zod'; + /** - * Schedule definition for a job + * Schedule definition for a job — the `IJobService.schedule` BOUNDARY shape. + * + * [#4538] The SOLE declaration of this name; the legacy `JobSchedule = + * Schedule` alias on `./system` was consumer-free and removed. This is + * deliberately NOT the authored `Schedule` union from `system/job.zod.ts`: + * that is the authoring/persistence tier (discriminated union, cron + * `expression` parsed into the ADR expression envelope, `timezone` + * defaulted), while this is the plain-runtime-value shape the schedulers + * consume — `trigger-schedule` normalizes authored shorthands into it, and + * the cron adapter hands `expression` (a bare cron string here) straight to + * croner. */ export interface JobSchedule { /** Schedule type */ @@ -64,24 +79,6 @@ export interface JobScheduleOptions { timeout?: number; } -/** - * Status of a job execution - */ -export interface JobExecution { - /** Job identifier */ - jobId: string; - /** Execution status */ - status: 'running' | 'success' | 'failed' | 'timeout'; - /** Start time (ISO 8601) */ - startedAt: string; - /** Completion time (ISO 8601) */ - completedAt?: string; - /** Error message if failed */ - error?: string; - /** Duration in milliseconds */ - durationMs?: number; -} - export interface IJobService { /** * Schedule a recurring or one-time job diff --git a/packages/spec/src/contracts/metadata-service.ts b/packages/spec/src/contracts/metadata-service.ts index ff0b4de561..91087651cd 100644 --- a/packages/spec/src/contracts/metadata-service.ts +++ b/packages/spec/src/contracts/metadata-service.ts @@ -66,7 +66,14 @@ export interface MetadataWatchHandle { } /** - * Metadata export options + * Metadata export options — the parameter type of + * {@link IMetadataService.exportMetadata}. + * + * [#4538] The SOLE declaration of this name. `@objectstack/spec/system` used + * to export a same-named, differently-shaped options bag + * (`output`-directory-flavored, from the #4411 duplicate persistence-envelope + * family); it had no consumer anywhere and was removed. `MetadataManager` + * implements THIS shape (`options.types` drives the export loop). */ export interface MetadataExportOptions { /** Filter by metadata types */ @@ -78,7 +85,13 @@ export interface MetadataExportOptions { } /** - * Metadata import options + * Metadata import options — the parameter type of + * {@link IMetadataService.importMetadata}. + * + * [#4538] The SOLE declaration of this name (the same-named + * `source`/`strategy` bag on `./system` was consumer-free and removed). + * `MetadataManager` implements THIS shape (`conflictResolution` / `validate` / + * `dryRun` are what its import loop destructures). */ export interface MetadataImportOptions { /** Conflict resolution strategy */ diff --git a/packages/spec/src/contracts/notification-service.ts b/packages/spec/src/contracts/notification-service.ts index a8667b40da..47fb62af65 100644 --- a/packages/spec/src/contracts/notification-service.ts +++ b/packages/spec/src/contracts/notification-service.ts @@ -14,14 +14,19 @@ */ /** - * Supported notification delivery channels + * Supported notification delivery channels. * - * ⚠️ PARTIALLY ENFORCED — mirrors `NotificationChannelSchema` - * (system/notification.zod.ts); the delivery channels actually registered by + * [#4538] Re-exported from the zod source (`NotificationChannelSchema`, + * system/notification.zod.ts) instead of a hand-written mirror union — the + * member sets had stayed identical only by discipline, and one declaration + * per name is the rule (#4446). + * + * ⚠️ PARTIALLY ENFORCED — the delivery channels actually registered by * `service-messaging` are `inbox`, `email`, and `sms` only (#3197). Messages * addressed to an unregistered channel are dead-lettered, not delivered. */ -export type NotificationChannel = 'email' | 'sms' | 'push' | 'in-app' | 'slack' | 'teams' | 'webhook'; +import type { NotificationChannel } from '../system/notification.zod'; +export type { NotificationChannel } from '../system/notification.zod'; /** * A notification message to be sent diff --git a/packages/spec/src/contracts/plugin-validator.ts b/packages/spec/src/contracts/plugin-validator.ts index 21dc91805b..2147961ec9 100644 --- a/packages/spec/src/contracts/plugin-validator.ts +++ b/packages/spec/src/contracts/plugin-validator.ts @@ -8,32 +8,16 @@ */ /** - * Validation result for a plugin + * Validation result for a plugin. + * + * [#4538] Re-exported from the zod source (`ValidationResultSchema`, + * kernel/plugin-validator.zod.ts) instead of a hand-written twin — the two + * declarations were field-for-field identical, and one declaration per name + * is the rule (#4446). `ValidationError` / `ValidationWarning` element types + * live there too. */ -export interface ValidationResult { - /** - * Whether the plugin passed validation - */ - valid: boolean; - - /** - * Validation errors (if any) - */ - errors?: Array<{ - field: string; - message: string; - code?: string; - }>; - - /** - * Validation warnings (non-fatal issues) - */ - warnings?: Array<{ - field: string; - message: string; - code?: string; - }>; -} +import type { ValidationResult } from '../kernel/plugin-validator.zod'; +export type { ValidationResult } from '../kernel/plugin-validator.zod'; /** * Plugin metadata for validation diff --git a/packages/spec/src/contracts/startup-orchestrator.test.ts b/packages/spec/src/contracts/startup-orchestrator.test.ts index 64c2aadb63..489667f59e 100644 --- a/packages/spec/src/contracts/startup-orchestrator.test.ts +++ b/packages/spec/src/contracts/startup-orchestrator.test.ts @@ -1,24 +1,37 @@ import { describe, it, expect } from 'vitest'; import type { StartupOptions, + StartupOptionsInput, PluginStartupResult, HealthStatus, IStartupOrchestrator, } from './startup-orchestrator'; +// [#4538] The contract's data shapes ARE the kernel zod types — the same +// declaration, re-exported. This import compiling is part of the pin. +import { StartupOptionsSchema } from '../kernel/startup-orchestrator.zod'; import type { Plugin } from './plugin-validator'; describe('Startup Orchestrator Contract', () => { - describe('StartupOptions interface', () => { - it('should allow an empty options object (all optional)', () => { - const options: StartupOptions = {}; + describe('StartupOptions tiers (re-exported kernel zod types, #4538)', () => { + it('should allow an empty INPUT-tier options object (all optional)', () => { + const options: StartupOptionsInput = {}; expect(options).toBeDefined(); expect(options.timeout).toBeUndefined(); expect(options.rollbackOnFailure).toBeUndefined(); }); + it('parses the input tier into the defaulted StartupOptions tier', () => { + const parsed: StartupOptions = StartupOptionsSchema.parse({}); + + expect(parsed.timeout).toBe(30000); + expect(parsed.rollbackOnFailure).toBe(true); + expect(parsed.healthCheck).toBe(false); + expect(parsed.parallel).toBe(false); + }); + it('should allow full options', () => { - const options: StartupOptions = { + const options: StartupOptionsInput = { timeout: 30000, rollbackOnFailure: true, healthCheck: true, @@ -73,13 +86,15 @@ describe('Startup Orchestrator Contract', () => { expect(result.error).toBeUndefined(); }); - it('should represent a failed startup with error', () => { + it('should represent a failed startup with a SERIALIZABLE error (#4538)', () => { const plugin: Plugin = { name: 'broken-plugin' }; const result: PluginStartupResult = { plugin, success: false, duration: 30000, - error: new Error('Timeout'), + // The kernel schema declares the serializable projection, not a live + // Error instance — what a wire/log consumer of the result can carry. + error: { name: 'Error', message: 'Timeout' }, }; expect(result.success).toBe(false); diff --git a/packages/spec/src/contracts/startup-orchestrator.ts b/packages/spec/src/contracts/startup-orchestrator.ts index aabc55c22f..bae8dd96a8 100644 --- a/packages/spec/src/contracts/startup-orchestrator.ts +++ b/packages/spec/src/contracts/startup-orchestrator.ts @@ -1,108 +1,37 @@ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. import { Plugin } from './plugin-validator.js'; +// [#4538] The startup-orchestration data shapes are the kernel domain's +// zod-derived types — one declaration per name (#4446), re-exported here so +// the contract surface and the schema surface can never drift again. The +// hand-written twins this file used to carry had already drifted twice: +// `error` was a live `Error` where the schema declares the serializable +// projection, and `StartupOptions` conflated the caller-authored (input) +// tier with the parsed tier. +import type { + HealthStatus, + PluginStartupResult, + StartupOptionsInput, +} from '../kernel/startup-orchestrator.zod'; +export type { + HealthStatus, + PluginStartupResult, + StartupOptions, + StartupOptionsInput, +} from '../kernel/startup-orchestrator.zod'; /** * IStartupOrchestrator - Startup Orchestrator Interface - * + * * Abstract interface for orchestrating plugin startup with advanced features: * - Timeout handling * - Rollback on failure * - Health checks * - Startup metrics - * + * * Extracted from PluginLoader to follow Single Responsibility Principle. */ -/** - * Startup options for orchestration - */ -export interface StartupOptions { - /** - * Maximum time (ms) to wait for each plugin to start - * @default 30000 (30 seconds) - */ - timeout?: number; - - /** - * Whether to rollback (destroy) already-started plugins on failure - * @default true - */ - rollbackOnFailure?: boolean; - - /** - * Whether to run health checks after startup - * @default false - */ - healthCheck?: boolean; - - /** - * Whether to run plugins in parallel (if dependencies allow) - * @default false (sequential startup) - */ - parallel?: boolean; - - /** - * Custom context to pass to plugin lifecycle methods - */ - context?: any; -} - -/** - * Plugin startup result - */ -export interface PluginStartupResult { - /** - * Plugin that was started - */ - plugin: Plugin; - - /** - * Whether startup was successful - */ - success: boolean; - - /** - * Time taken to start (milliseconds) - */ - duration: number; - - /** - * Error if startup failed - */ - error?: Error; - - /** - * Health status after startup (if healthCheck enabled) - */ - health?: HealthStatus; -} - -/** - * Health status for a plugin - */ -export interface HealthStatus { - /** - * Whether the plugin is healthy - */ - healthy: boolean; - - /** - * Health check timestamp - */ - timestamp: number; - - /** - * Optional health details - */ - details?: Record; - - /** - * Optional error message if unhealthy - */ - message?: string; -} - /** * IStartupOrchestrator - Plugin startup orchestration interface */ @@ -111,12 +40,13 @@ export interface IStartupOrchestrator { * Orchestrate startup of multiple plugins * Handles timeout, rollback, and health checks * @param plugins - Array of plugins to start (in dependency order) - * @param options - Startup options + * @param options - Startup options — the caller-authored (input) tier; + * implementations apply `StartupOptionsSchema` defaults * @returns Promise resolving to startup results for each plugin */ orchestrateStartup( - plugins: Plugin[], - options: StartupOptions + plugins: Plugin[], + options: StartupOptionsInput ): Promise; /** diff --git a/packages/spec/src/data/analytics.test.ts b/packages/spec/src/data/analytics.test.ts index 5588c47ca7..a9a3065536 100644 --- a/packages/spec/src/data/analytics.test.ts +++ b/packages/spec/src/data/analytics.test.ts @@ -324,7 +324,9 @@ describe('AnalyticsQuerySchema', () => { }); expect(query.measures).toEqual(['orders.count']); - expect(query.timezone).toBe('UTC'); + // [#4538] No `timezone` default: absence is meaningful (the engine + // resolves org timezone -- #1982/#2018), so the parse must preserve it. + expect(query.timezone).toBeUndefined(); }); it('should accept query with all fields', () => { @@ -375,12 +377,13 @@ describe('AnalyticsQuerySchema', () => { } }); - it('should apply default timezone', () => { + it('should NOT default timezone -- absence means the engine resolves it (#4538)', () => { const query = AnalyticsQuerySchema.parse({ measures: ['orders.count'], }); - expect(query.timezone).toBe('UTC'); + expect(query.timezone).toBeUndefined(); + expect('timezone' in query).toBe(false); }); it('should reject query without measures', () => { diff --git a/packages/spec/src/data/analytics.zod.ts b/packages/spec/src/data/analytics.zod.ts index 4729f904a0..914f52a749 100644 --- a/packages/spec/src/data/analytics.zod.ts +++ b/packages/spec/src/data/analytics.zod.ts @@ -167,7 +167,17 @@ export const AnalyticsQuerySchema = lazySchema(() => z.object({ limit: z.number().optional(), offset: z.number().optional(), - timezone: z.string().optional().default('UTC'), + /** + * Reference timezone (IANA name) for date bucketing. OPTIONAL WITH NO + * DEFAULT, deliberately (#4538): an ABSENT timezone is a meaningful state — + * the engine resolves it (`selection.timezone ?? context.timezone ?? 'UTC'`, + * ADR-0053 Phase 2), and the `/analytics` entry forwards bodies + * validation-only precisely so a schema default cannot silently override + * the org-timezone resolution chain (#1982/#2018). The `.default('UTC')` + * this field used to carry declared a boundary the runtime refused to + * enforce. + */ + timezone: z.string().optional(), })); export type Metric = z.infer; @@ -188,14 +198,15 @@ export function defineCube(config: z.input): Cube { export type AnalyticsQuery = z.infer; /** - * Author-tier `AnalyticsQuery` — what a caller writes, before `.parse()` fills - * the defaults in. `timezone` is `.default('UTC')`, so it is optional here and - * REQUIRED on {@link AnalyticsQuery}: the two tiers are genuinely different - * types, and only the parse turns one into the other. + * Input-tier alias of {@link AnalyticsQuery}. * - * Every executor (`IAnalyticsService.query`, the analytics strategies) takes - * the PARSED type, because a request body reaches them through the schema. Use - * this one for the literal handed to `AnalyticsQuerySchema.parse()` — the - * mirror of `QueryInput` / `QueryAST` in `data/query.zod.ts`. + * [#4538] The two tiers COLLAPSED when `timezone` lost its `.default('UTC')` + * (see the field's own note): the schema now carries no `.default()` or + * `.transform()` anywhere — `FilterCondition` is declared transform-free — + * so what a caller writes is exactly what an executor receives, and this + * name survives only for source compatibility. `IAnalyticsService.query`, + * the analytics strategies, and the `/analytics` entry all traffic in the + * single {@link AnalyticsQuery} shape (validated at the entry by + * `AnalyticsQueryRequestSchema`, forwarded unmodified). */ export type AnalyticsQueryInput = z.input; diff --git a/packages/spec/src/system/job.test.ts b/packages/spec/src/system/job.test.ts index 87be304d62..215a1fbd91 100644 --- a/packages/spec/src/system/job.test.ts +++ b/packages/spec/src/system/job.test.ts @@ -401,12 +401,12 @@ describe('JobExecutionSchema', () => { startedAt: '2024-01-15T10:30:00Z', completedAt: '2024-01-15T10:35:00Z', status: 'success', - duration: 300000, + durationMs: 300000, }; const parsed = JobExecutionSchema.parse(execution); expect(parsed.completedAt).toBe('2024-01-15T10:35:00Z'); - expect(parsed.duration).toBe(300000); + expect(parsed.durationMs).toBe(300000); }); it('should accept failed execution', () => { @@ -416,7 +416,7 @@ describe('JobExecutionSchema', () => { completedAt: '2024-01-15T11:05:00Z', status: 'failed', error: 'Database connection timeout', - duration: 300000, + durationMs: 300000, }; const parsed = JobExecutionSchema.parse(execution); @@ -431,7 +431,7 @@ describe('JobExecutionSchema', () => { completedAt: '2024-01-15T12:10:00Z', status: 'timeout', error: 'Job exceeded maximum execution time of 600000ms', - duration: 600000, + durationMs: 600000, }; const parsed = JobExecutionSchema.parse(execution); @@ -548,14 +548,14 @@ describe('Job Scheduling Integration', () => { startedAt: '2024-01-15T02:00:00Z', completedAt: '2024-01-15T02:15:00Z', status: 'success', - duration: 900000, + durationMs: 900000, }, { jobId: 'backup-daily', startedAt: '2024-01-16T02:00:00Z', completedAt: '2024-01-16T02:10:00Z', status: 'success', - duration: 600000, + durationMs: 600000, }, { jobId: 'backup-daily', @@ -563,7 +563,7 @@ describe('Job Scheduling Integration', () => { completedAt: '2024-01-17T02:35:00Z', status: 'failed', error: 'Insufficient disk space', - duration: 2100000, + durationMs: 2100000, }, ]; diff --git a/packages/spec/src/system/job.zod.ts b/packages/spec/src/system/job.zod.ts index 1f2c3d1a11..02dc744828 100644 --- a/packages/spec/src/system/job.zod.ts +++ b/packages/spec/src/system/job.zod.ts @@ -48,7 +48,12 @@ export type Schedule = z.infer; export type CronSchedule = z.infer; export type IntervalSchedule = z.infer; export type OnceSchedule = z.infer; -export type JobSchedule = Schedule; // Alias for backwards compatibility +// NOTE [#4538]: the legacy `export type JobSchedule = Schedule` alias was +// removed. It collided with the differently-shaped `JobSchedule` on +// `@objectstack/spec/contracts` — the IJobService boundary type every runtime +// caller (trigger-schedule, wait-node, the job adapters) imports — while this +// alias itself had zero consumers. The authored metadata type keeps its real +// name: `Schedule`. /** * Retry Policy Schema @@ -142,7 +147,14 @@ export type JobExecutionStatus = z.infer; /** * Job Execution Schema - * Logs for job execution + * Logs for job execution. + * + * [#4538] This is the ONE declaration of `JobExecution` — the contracts entry + * re-exports it for `IJobService.getExecutions`. The duration field is + * `durationMs`: that is what every job adapter produces (cron/interval set it + * from `Date.now()` deltas; the DB adapter round-trips the `duration_ms` + * column). The schema's earlier `duration` spelling described records nothing + * ever wrote and was aligned to the runtime truth. */ export const JobExecutionSchema = lazySchema(() => z.object({ jobId: z.string().describe('Job identifier'), @@ -150,7 +162,7 @@ export const JobExecutionSchema = lazySchema(() => z.object({ completedAt: z.string().datetime().optional().describe('ISO 8601 datetime when execution completed'), status: JobExecutionStatus.describe('Execution status'), error: z.string().optional().describe('Error message if failed'), - duration: z.number().int().optional().describe('Execution duration in milliseconds'), + durationMs: z.number().int().optional().describe('Execution duration in milliseconds'), })); export type JobExecution = z.infer; diff --git a/packages/spec/src/system/metadata-persistence.test.ts b/packages/spec/src/system/metadata-persistence.test.ts index 61caafa790..38a98371fd 100644 --- a/packages/spec/src/system/metadata-persistence.test.ts +++ b/packages/spec/src/system/metadata-persistence.test.ts @@ -12,8 +12,6 @@ import { MetadataSaveResultSchema, MetadataWatchEventSchema, MetadataCollectionInfoSchema, - MetadataExportOptionsSchema, - MetadataImportOptionsSchema, MetadataManagerConfigSchema, MetadataFallbackStrategySchema, MetadataSourceSchema, @@ -451,50 +449,6 @@ describe('MetadataCollectionInfoSchema', () => { }); }); -describe('MetadataExportOptionsSchema', () => { - it('should accept minimal export options with defaults', () => { - const opts = MetadataExportOptionsSchema.parse({ output: '/export' }); - expect(opts.output).toBe('/export'); - expect(opts.format).toBe('json'); - }); - - it('should accept full export options', () => { - const opts = MetadataExportOptionsSchema.parse({ - types: ['view', 'object'], - namespaces: ['crm'], - output: '/export/crm', - format: 'yaml', - }); - - expect(opts.types).toEqual(['view', 'object']); - expect(opts.format).toBe('yaml'); - }); - - it('should reject missing output', () => { - expect(() => MetadataExportOptionsSchema.parse({})).toThrow(); - }); -}); - -describe('MetadataImportOptionsSchema', () => { - it('should accept minimal import options with defaults', () => { - const opts = MetadataImportOptionsSchema.parse({ source: '/import' }); - expect(opts.source).toBe('/import'); - expect(opts.strategy).toBe('merge'); - expect(opts.validate).toBe(true); - }); - - it('should accept all strategies', () => { - const strategies = ['merge', 'replace', 'skip']; - strategies.forEach((strategy) => { - expect(() => MetadataImportOptionsSchema.parse({ source: '/x', strategy })).not.toThrow(); - }); - }); - - it('should reject missing source', () => { - expect(() => MetadataImportOptionsSchema.parse({})).toThrow(); - }); -}); - describe('MetadataManagerConfigSchema', () => { it('should accept empty object with defaults', () => { const config = MetadataManagerConfigSchema.parse({}); diff --git a/packages/spec/src/system/metadata-persistence.zod.ts b/packages/spec/src/system/metadata-persistence.zod.ts index 00dcc871bb..1b9b8ee659 100644 --- a/packages/spec/src/system/metadata-persistence.zod.ts +++ b/packages/spec/src/system/metadata-persistence.zod.ts @@ -313,21 +313,14 @@ export const MetadataCollectionInfoSchema = lazySchema(() => z.object({ namespaces: z.array(z.string()), })); -/** - * Metadata Export/Import Options - */ -export const MetadataExportOptionsSchema = lazySchema(() => z.object({ - types: z.array(z.string()).optional(), - namespaces: z.array(z.string()).optional(), - output: z.string().describe('Output directory or file'), - format: MetadataFormatSchema.default('json'), -})); - -export const MetadataImportOptionsSchema = lazySchema(() => z.object({ - source: z.string().describe('Input directory or file'), - strategy: z.enum(['merge', 'replace', 'skip']).default('merge'), - validate: z.boolean().default(true), -})); +// `MetadataExportOptionsSchema` / `MetadataImportOptionsSchema` (an +// `output`/`source`-directory-flavored options pair) lived here until #4538. +// They were the last survivors of the duplicate persistence-envelope family +// #4411 removed from kernel: no runtime, CLI, or sibling-repo code ever +// consumed them — their only references were their own pin tests — while their +// NAMES collided with the `IMetadataService.exportMetadata`/`importMetadata` +// parameter types in `../contracts/metadata-service.ts`, which `MetadataManager` +// actually implements. The name now has one declaration, on `./contracts`. /** * Metadata Source Origin @@ -368,8 +361,6 @@ export type MetadataSaveOptions = z.infer; export type MetadataSaveResult = z.infer; export type MetadataWatchEvent = z.infer; export type MetadataCollectionInfo = z.infer; -export type MetadataExportOptions = z.infer; -export type MetadataImportOptions = z.infer; export type { MetadataManagerConfig, MetadataFallbackStrategy } from '../kernel/metadata-loader.zod'; export type MetadataSource = z.infer;