From f3966309d01435c32f7ae7422035ba04bd04b49b Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 17 Sep 2026 09:42:08 +0000 Subject: [PATCH 1/2] feat(spec): the ten bare wall-clock instants adopt EpochMs (tranche 1 of ruling C on #18080) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Each of the ten instants named by census #18078 now declares its unit through the shared `EpochMs` schema (`src/shared/epoch.zod.ts`) instead of a bare `z.number()`. No key is renamed: `timestamp`, `tokenExpiry`, `deadline` and `validUntil` keep their names — a rename is a separate retirement with its own ADR-0087 entry and is not ruled here. Where a JSDoc already said "(Unix milliseconds)" it stays and the describe is untouched; where nothing named the unit, the describe gains the unit the type now carries. Three of the ten (`kernel/plugin-loading.zod.ts` `timestamp`, `startedAt`, `completedAt`) were NOT bare `z.number()` — they already carried `.int().min(0)`. They adopt `EpochMs.min(0)` so the site keeps the floor it already declared: a bare `EpochMs` would have DROPPED `.min(0)` and widened three published keys, which is the opposite of what this tranche does. Claude-Session: https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3 Co-authored-by: Claude --- content/docs/references/data/document.mdx | 8 ++++---- content/docs/references/identity/identity.mdx | 2 +- content/docs/references/integration/connector.mdx | 4 ++-- content/docs/references/kernel/plugin-loading.mdx | 6 +++--- content/docs/references/system/supplier-security.mdx | 8 ++++---- packages/spec/src/data/document.zod.ts | 5 +++-- packages/spec/src/identity/identity.zod.ts | 3 ++- packages/spec/src/kernel/plugin-loading.zod.ts | 7 ++++--- packages/spec/src/shared/connector-auth.zod.ts | 3 ++- packages/spec/src/system/supplier-security.zod.ts | 7 ++++--- 10 files changed, 29 insertions(+), 24 deletions(-) diff --git a/content/docs/references/data/document.mdx b/content/docs/references/data/document.mdx index 91642cef46e..7bad0f313eb 100644 --- a/content/docs/references/data/document.mdx +++ b/content/docs/references/data/document.mdx @@ -37,7 +37,7 @@ const result = DocumentSchema.parse(data); | **versioning** | `{ enabled: boolean; versions: object[]; majorVersion: number; minorVersion: number }` | optional | Version control | | **template** | `{ id: string; name: string; description?: string; fileUrl: string; … }` | optional | Document template | | **eSignature** | `{ provider: Enum<'docusign' \| 'adobe-sign' \| 'hellosign' \| 'custom'>; enabled: boolean; signers: object[] }` | optional | E-signature config | -| **access** | `{ isPublic: boolean; sharedWith?: string[]; expiresAt?: number }` | optional | Access control | +| **access** | `{ isPublic: boolean; sharedWith?: string[]; expiresAt?: integer }` | optional | Access control | | **metadata** | `Record` | optional | Custom metadata | ### Nested Shape: `Document.versioning` @@ -45,7 +45,7 @@ const result = DocumentSchema.parse(data); | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **enabled** | `boolean` | ✅ | Versioning enabled | -| **versions** | `{ versionNumber: number; createdAt: number; createdBy: string; size: number; … }[]` | ✅ | Version history | +| **versions** | `{ versionNumber: number; createdAt: integer; createdBy: string; size: number; … }[]` | ✅ | Version history | | **majorVersion** | `number` | ✅ | Major version | | **minorVersion** | `number` | ✅ | Minor version | @@ -76,7 +76,7 @@ const result = DocumentSchema.parse(data); | :--- | :--- | :--- | :--- | | **isPublic** | `boolean` | optional (default: `false`) | Public access | | **sharedWith** | `string[]` | optional | Shared with | -| **expiresAt** | `number` | optional | Access expiration | +| **expiresAt** | `integer` | optional | Access expiration | --- @@ -113,7 +113,7 @@ const result = DocumentSchema.parse(data); | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **versionNumber** | `number` | ✅ | Version number | -| **createdAt** | `number` | ✅ | Creation timestamp | +| **createdAt** | `integer` | ✅ | Creation timestamp | | **createdBy** | `string` | ✅ | Creator user ID | | **size** | `number` | ✅ | File size in bytes | | **checksum** | `string` | ✅ | File checksum | diff --git a/content/docs/references/identity/identity.mdx b/content/docs/references/identity/identity.mdx index c5f26df7768..f23523096dc 100644 --- a/content/docs/references/identity/identity.mdx +++ b/content/docs/references/identity/identity.mdx @@ -42,7 +42,7 @@ const result = AccountSchema.parse(data); | **providerAccountId** | `string` | ✅ | Provider account ID | | **refreshToken** | `string` | optional | OAuth refresh token | | **accessToken** | `string` | optional | OAuth access token | -| **expiresAt** | `number` | optional | Token expiry timestamp (Unix) | +| **expiresAt** | `integer` | optional | Token expiry timestamp (Unix milliseconds) | | **tokenType** | `string` | optional | OAuth token type | | **scope** | `string` | optional | OAuth scope | | **idToken** | `string` | optional | OAuth ID token | diff --git a/content/docs/references/integration/connector.mdx b/content/docs/references/integration/connector.mdx index c2d4bc239d8..23370d3bdc4 100644 --- a/content/docs/references/integration/connector.mdx +++ b/content/docs/references/integration/connector.mdx @@ -204,7 +204,7 @@ Circuit breaker configuration | **scopes** | `string[]` | optional | Requested OAuth2 scopes | | **redirectUri** | `string` | optional | OAuth2 redirect URI | | **refreshToken** | `string` | optional | Refresh token for token renewal | -| **tokenExpiry** | `number` | optional | Token expiry timestamp | +| **tokenExpiry** | `integer` | optional | Token expiry timestamp (Unix milliseconds) | ### Nested Shape: `Connector.authentication[type='api-key']` @@ -688,7 +688,7 @@ Connector type | **scopes** | `string[]` | optional | Requested OAuth2 scopes | | **redirectUri** | `string` | optional | OAuth2 redirect URI | | **refreshToken** | `string` | optional | Refresh token for token renewal | -| **tokenExpiry** | `number` | optional | Token expiry timestamp | +| **tokenExpiry** | `integer` | optional | Token expiry timestamp (Unix milliseconds) | ### Nested Shape: `DeclarativeConnectorEntry.authentication[type='api-key']` diff --git a/content/docs/references/kernel/plugin-loading.mdx b/content/docs/references/kernel/plugin-loading.mdx index abde1743ee4..f15c1150faf 100644 --- a/content/docs/references/kernel/plugin-loading.mdx +++ b/content/docs/references/kernel/plugin-loading.mdx @@ -42,7 +42,7 @@ Plugin loading lifecycle event | :--- | :--- | :--- | :--- | | **type** | `Enum<'load-started' \| 'load-completed' \| 'load-failed' \| 'init-started' \| 'init-completed' \| 'init-failed' \| 'preload-started' \| 'preload-completed' \| 'cache-hit' \| … +5 more>` | ✅ | | | **pluginId** | `string` | ✅ | | -| **timestamp** | `integer` | ✅ | | +| **timestamp** | `integer` | ✅ | Event timestamp — Unix milliseconds | | **durationMs** | `integer` | optional | Duration in milliseconds | | **metadata** | `Record` | optional | | | **error** | `{ message: string; code?: string; stack?: string }` | optional | | @@ -78,8 +78,8 @@ Plugin loading state | **pluginId** | `string` | ✅ | | | **state** | `Enum<'pending' \| 'loading' \| 'loaded' \| 'initializing' \| 'ready' \| 'failed' \| 'reloading' \| 'unloading' \| 'unloaded'>` | ✅ | | | **progress** | `number` | optional (default: `0`) | | -| **startedAt** | `integer` | optional | | -| **completedAt** | `integer` | optional | | +| **startedAt** | `integer` | optional | Loading start time — Unix milliseconds | +| **completedAt** | `integer` | optional | Loading completion time — Unix milliseconds | | **lastError** | `string` | optional | | | **retryCount** | `integer` | optional (default: `0`) | | diff --git a/content/docs/references/system/supplier-security.mdx b/content/docs/references/system/supplier-security.mdx index c93d800bf18..85a1f72cf87 100644 --- a/content/docs/references/system/supplier-security.mdx +++ b/content/docs/references/system/supplier-security.mdx @@ -66,14 +66,14 @@ Supplier security assessment record per ISO 27001:2022 A.5.19–A.5.21 | **riskLevel** | `Enum<'critical' \| 'high' \| 'medium' \| 'low'>` | ✅ | Supplier risk classification | | **status** | `Enum<'pending' \| 'in_progress' \| 'completed' \| 'expired' \| 'failed'>` | ✅ | Assessment status | | **assessedBy** | `string` | ✅ | Assessor user ID or team | -| **assessedAt** | `number` | ✅ | Assessment timestamp | -| **validUntil** | `number` | ✅ | Assessment validity expiry timestamp | +| **assessedAt** | `integer` | ✅ | Assessment timestamp | +| **validUntil** | `integer` | ✅ | Assessment validity expiry timestamp | | **requirements** | `{ id: string; description: string; controlReference?: string; mandatory: boolean; … }[]` | ✅ | Security requirements and their compliance status | | **overallCompliant** | `boolean` | ✅ | Whether supplier meets all mandatory requirements | | **dataClassificationsShared** | `Enum<'pii' \| 'phi' \| 'pci' \| 'financial' \| 'confidential' \| 'internal' \| 'public'>[]` | optional | Data classifications shared with supplier | | **servicesProvided** | `string[]` | optional | Services provided by this supplier | | **certifications** | `string[]` | optional | Supplier certifications (e.g., ISO 27001, SOC 2) | -| **remediationItems** | `{ requirementId: string; action: string; deadline: number; status: Enum<'pending' \| 'in_progress' \| 'completed'> }[]` | optional | Remediation items for non-compliant requirements | +| **remediationItems** | `{ requirementId: string; action: string; deadline: integer; status: Enum<'pending' \| 'in_progress' \| 'completed'> }[]` | optional | Remediation items for non-compliant requirements | | **metadata** | `Record` | optional | Custom metadata key-value pairs | ### Nested Shape: `SupplierSecurityAssessment.requirements[number]` @@ -95,7 +95,7 @@ Individual supplier security requirement | :--- | :--- | :--- | :--- | | **requirementId** | `string` | ✅ | Non-compliant requirement ID | | **action** | `string` | ✅ | Required remediation action | -| **deadline** | `number` | ✅ | Remediation deadline timestamp | +| **deadline** | `integer` | ✅ | Remediation deadline timestamp (Unix milliseconds) | | **status** | `Enum<'pending' \| 'in_progress' \| 'completed'>` | optional (default: `"pending"`) | Remediation status | diff --git a/packages/spec/src/data/document.zod.ts b/packages/spec/src/data/document.zod.ts index a4906b32619..5f70b8be892 100644 --- a/packages/spec/src/data/document.zod.ts +++ b/packages/spec/src/data/document.zod.ts @@ -2,6 +2,7 @@ import { z } from 'zod'; import { retiredKey } from '../shared/retired-key'; +import { EpochMs } from '../shared/epoch.zod'; /** * Document Version Schema @@ -32,7 +33,7 @@ export const DocumentVersionSchema = lazySchema(() => z.object({ /** * Timestamp when this version was created (Unix milliseconds) */ - createdAt: z.number().describe('Creation timestamp'), + createdAt: EpochMs.describe('Creation timestamp'), /** * User ID who created this version @@ -399,7 +400,7 @@ export const DocumentSchema = lazySchema(() => z.object({ /** * Timestamp when access expires (Unix milliseconds) */ - expiresAt: z.number().optional().describe('Access expiration'), + expiresAt: EpochMs.optional().describe('Access expiration'), }).optional().describe('Access control'), /** diff --git a/packages/spec/src/identity/identity.zod.ts b/packages/spec/src/identity/identity.zod.ts index 0f7f1b97525..5a91d73047b 100644 --- a/packages/spec/src/identity/identity.zod.ts +++ b/packages/spec/src/identity/identity.zod.ts @@ -1,6 +1,7 @@ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. import { z } from 'zod'; +import { EpochMs } from '../shared/epoch.zod'; /** * Identity & User Model Specification @@ -108,7 +109,7 @@ export const AccountSchema = lazySchema(() => z.object({ /** * Token expiry timestamp */ - expiresAt: z.number().optional().describe('Token expiry timestamp (Unix)'), + expiresAt: EpochMs.optional().describe('Token expiry timestamp (Unix milliseconds)'), /** * OAuth token type diff --git a/packages/spec/src/kernel/plugin-loading.zod.ts b/packages/spec/src/kernel/plugin-loading.zod.ts index 4810e976076..70c74ca49d5 100644 --- a/packages/spec/src/kernel/plugin-loading.zod.ts +++ b/packages/spec/src/kernel/plugin-loading.zod.ts @@ -2,6 +2,7 @@ import { z } from 'zod'; import { lazySchema } from '../shared/lazy-schema'; +import { EpochMs } from '../shared/epoch.zod'; /** * # Plugin Loading Protocol @@ -96,7 +97,7 @@ export const PluginLoadingEventSchema = lazySchema(() => z.object({ /** * Timestamp */ - timestamp: z.number().int().min(0), + timestamp: EpochMs.min(0).describe('Event timestamp — Unix milliseconds'), /** * Duration in milliseconds @@ -151,12 +152,12 @@ export const PluginLoadingStateSchema = lazySchema(() => z.object({ /** * Loading start time */ - startedAt: z.number().int().min(0).optional(), + startedAt: EpochMs.min(0).optional().describe('Loading start time — Unix milliseconds'), /** * Loading completion time */ - completedAt: z.number().int().min(0).optional(), + completedAt: EpochMs.min(0).optional().describe('Loading completion time — Unix milliseconds'), /** * Last error diff --git a/packages/spec/src/shared/connector-auth.zod.ts b/packages/spec/src/shared/connector-auth.zod.ts index da6a2fea06e..747bb855f3e 100644 --- a/packages/spec/src/shared/connector-auth.zod.ts +++ b/packages/spec/src/shared/connector-auth.zod.ts @@ -1,6 +1,7 @@ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. import { z } from 'zod'; +import { EpochMs } from './epoch.zod'; /** * SHARED CONNECTOR AUTHENTICATION SCHEMAS @@ -35,7 +36,7 @@ export const ConnectorOAuth2Schema = lazySchema(() => z.object({ scopes: z.array(z.string()).optional().describe('Requested OAuth2 scopes'), redirectUri: z.string().url().optional().describe('OAuth2 redirect URI'), refreshToken: z.string().optional().describe('Refresh token for token renewal'), - tokenExpiry: z.number().optional().describe('Token expiry timestamp'), + tokenExpiry: EpochMs.optional().describe('Token expiry timestamp (Unix milliseconds)'), })); /** diff --git a/packages/spec/src/system/supplier-security.zod.ts b/packages/spec/src/system/supplier-security.zod.ts index 170aa1c0df4..1baeea1aa34 100644 --- a/packages/spec/src/system/supplier-security.zod.ts +++ b/packages/spec/src/system/supplier-security.zod.ts @@ -2,6 +2,7 @@ import { z } from 'zod'; import { DataClassificationSchema } from './security-context.zod'; +import { EpochMs } from '../shared/epoch.zod'; /** * Supplier Security Protocol — ISO 27001:2022 (A.5.19–A.5.22) @@ -143,12 +144,12 @@ export const SupplierSecurityAssessmentSchema = lazySchema(() => z.object({ /** * Assessment completion timestamp (Unix milliseconds) */ - assessedAt: z.number().describe('Assessment timestamp'), + assessedAt: EpochMs.describe('Assessment timestamp'), /** * Assessment validity expiry (Unix milliseconds) */ - validUntil: z.number().describe('Assessment validity expiry timestamp'), + validUntil: EpochMs.describe('Assessment validity expiry timestamp'), /** * Security requirements assessed @@ -185,7 +186,7 @@ export const SupplierSecurityAssessmentSchema = lazySchema(() => z.object({ remediationItems: z.array(z.object({ requirementId: z.string().describe('Non-compliant requirement ID'), action: z.string().describe('Required remediation action'), - deadline: z.number().describe('Remediation deadline timestamp'), + deadline: EpochMs.describe('Remediation deadline timestamp (Unix milliseconds)'), status: z.enum(['pending', 'in_progress', 'completed']).default('pending') .describe('Remediation status'), })).optional().describe('Remediation items for non-compliant requirements'), From bcca31339d3bac805491dff60546bc9add2df3ae Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 17 Sep 2026 09:56:37 +0000 Subject: [PATCH 2/2] =?UTF-8?q?chore(changeset):=20minor=20for=20@objectst?= =?UTF-8?q?ack/spec=20=E2=80=94=20the=20ten=20instants=20adopt=20EpochMs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Claude-Session: https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3 Co-authored-by: Claude --- .changeset/18114-epochms-instants-tranche1.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 .changeset/18114-epochms-instants-tranche1.md diff --git a/.changeset/18114-epochms-instants-tranche1.md b/.changeset/18114-epochms-instants-tranche1.md new file mode 100644 index 00000000000..386bb47d068 --- /dev/null +++ b/.changeset/18114-epochms-instants-tranche1.md @@ -0,0 +1,19 @@ +--- +"@objectstack/spec": minor +--- + +Ten wall-clock instants now declare their unit through the shared `EpochMs` schema (`@objectstack/spec/shared`) instead of a bare `z.number()`. No key is renamed and no key is added or removed. + +`EpochMs` is `z.number().int()` with the describe "Unix timestamp in milliseconds (epoch)". Adopting it moves each key's published JSON Schema from `{"type":"number"}` to `{"type":"integer"}` and puts the millisecond unit on the contract itself, where a reader of the reference page, the JSON Schema or the TypeScript surface all see the same answer. Before this, the unit lived in a JSDoc block (invisible in every published artifact), in prose that named only the epoch and not the unit, or nowhere at all — the ×1000 ambiguity a `timestamp: number` key carries by default. + +The keys, by schema: + +- `Data.DocumentVersion.createdAt`, `Data.Document.access.expiresAt` +- `System.SupplierSecurityAssessment.assessedAt`, `.validUntil`, `.remediationItems[].deadline` +- `Identity.Account.expiresAt` +- `Kernel.PluginLoadingEvent.timestamp`, `Kernel.PluginLoadingState.startedAt`, `.completedAt` +- the shared connector OAuth2 auth shape's `tokenExpiry` + +**What an author must change: nothing, unless they were writing a fractional millisecond.** Seven of the ten previously accepted any `number` and now accept integers only; `Date.now()` — the value every one of these keys is documented to carry — is already an integer. The three `Kernel.PluginLoading*` keys already declared `.int().min(0)`; they keep that floor (`EpochMs.min(0)`), so their accepted set is byte-for-byte what it was and only their description is new. + +`timestamp`, `tokenExpiry`, `deadline` and `validUntil` deliberately keep their names. `EpochMs`'s own docblock recommends spelling an instant `*At`, but a rename of a published key is a retirement with its own ADR-0087 entry and is not part of this change.