Skip to content

Commit 25c9a83

Browse files
os-billclaude
andauthored
feat(spec): the ten bare wall-clock instants adopt EpochMs (tranche 1 of ruling C on #18080) (#18631)
Fixes #18114 `Clause-②: no` Derived by the dispatching seat and confirmed by the measurements below: adopting `EpochMs` **narrows** the accepted set on ten already-published keys (seven of them), adds no exported symbol, no new key, no registry row and no closed-set member; clause ② polices the widening direction. `check-widening-tells --declaration no` agrees (below). Nothing measured here contradicts it. Tranche 1 of ruling C on #18080 (decision batch #133 item 4). Durations are tranche 2 and are untouched here. ## What changed Each of the ten wall-clock instants named by census #18078 now declares its unit through the shared `EpochMs` schema (`packages/spec/src/shared/epoch.zod.ts`) instead of a bare `z.number()`. No key is renamed: `timestamp`, `tokenExpiry`, `deadline` and `validUntil` keep their names. The gate script is untouched — this tranche is data-only. Where a JSDoc already said "(Unix milliseconds)" it stays and the describe is left alone; where nothing named the unit, the describe gains the unit the type now carries. ## The population, re-derived on today's `main` — three rows are NOT what the card describes The card calls all ten "bare `z.number()`". Re-derived by key + file at the branch point (`559041d39d`), **three are not**: | file | key | shape BEFORE (re-derived) | shape AFTER | unit was stated | |:--|:--|:--|:--|:--| | `data/document.zod.ts` | `createdAt` | `z.number()` | `EpochMs` | JSDoc | | `data/document.zod.ts` | `expiresAt` | `z.number().optional()` | `EpochMs.optional()` | JSDoc | | `system/supplier-security.zod.ts` | `assessedAt` | `z.number()` | `EpochMs` | JSDoc | | `system/supplier-security.zod.ts` | `validUntil` | `z.number()` | `EpochMs` | JSDoc | | `system/supplier-security.zod.ts` | `deadline` | `z.number()` | `EpochMs` | nowhere — describe gains it | | `identity/identity.zod.ts` | `expiresAt` | `z.number().optional()` | `EpochMs.optional()` | describe named the epoch only — describe gains the unit | | `kernel/plugin-loading.zod.ts` | `timestamp` | ⚠️ `z.number().int().min(0)` | `EpochMs.min(0)` | nowhere — describe gains it | | `kernel/plugin-loading.zod.ts` | `startedAt` | ⚠️ `z.number().int().min(0).optional()` | `EpochMs.min(0).optional()` | nowhere — describe gains it | | `kernel/plugin-loading.zod.ts` | `completedAt` | ⚠️ `z.number().int().min(0).optional()` | `EpochMs.min(0).optional()` | nowhere — describe gains it | | `shared/connector-auth.zod.ts` | `tokenExpiry` | `z.number().optional()` | `EpochMs.optional()` | nowhere — describe gains it | The seat's spot-check caught `startedAt`; `timestamp` and `completedAt` carry the same `.int().min(0)` and were unread by it. This does not refute the row's identity ("an instant not declared `EpochMs`"), but it decides how the three are written. ### Why those three keep `.min(0)` — a bare `EpochMs` there would have WIDENED three published keys `EpochMs` is `z.number().int()` and deliberately carries **no** `.min()` (its docblock: "a pre-1970 instant is negative and legitimate, and inventing a floor here would refuse data this schema has no business judging"). The three `plugin-loading` keys already declared their own floor. Writing them as a bare `EpochMs` would have **dropped `.min(0)`**, i.e. newly accepted negative instants on three published keys — a widening, the exact direction `Clause-②: no` denies. They are therefore written `EpochMs.min(0)`, so their accepted set is unchanged and only their description is new. Evidence, from the regenerated JSON Schema: ``` PluginLoadingEvent.timestamp {"type":"integer","minimum":0,"maximum":9007199254740991,"description":"Event timestamp — Unix milliseconds"} PluginLoadingState.startedAt {"description":"Loading start time — Unix milliseconds","type":"integer","minimum":0,"maximum":9007199254740991} DocumentVersion.createdAt {"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"Creation timestamp"} ``` `EpochMs`'s docblock precedent supports this: "the four that already declared `.int()` keep exactly what they had" — adoption preserves the site's accepted set rather than resetting it. The gate recognises the chain: its root walk bottoms out at the `EpochMs` identifier whatever methods sit above it, and all three are reported `[instant: EpochMs]` in the census below. The seven genuinely bare rows move from `{"type":"number"}` to `{"type":"integer"}` with Zod's safe-integer bounds. That is the ruled narrowing. ## Measurement 1 — does any first-party producer write a NON-INTEGER into any of the ten keys? **Zero.** So this is alignment to a declared convention: no `**BREAKING**` banner, no ADR-0087 disposition, and `packages/spec/src/migrations/registry.ts` is **not touched** (the branch the card told me to stop and report on did not fire). Method, not just the verdict: 1. **Who can produce these shapes at all.** Grepped the eight enclosing schema names and their distinctive sibling keys (`versionNumber`, `assessedBy`, `overallCompliant`, `requirementId`, `tokenExpiry`, `sharedWith`) across the repo. Outside `packages/spec` the only hits are generated reference pages and the surface snapshots; the sole first-party producers are `packages/spec`'s own tests. 2. **Every value those producers write**, read by hand: `document.test.ts` (`1704067200000`, `Date.now()`, `1735689600000`, `1767225600000`), `supplier-security.test.ts` (`Date.now()`, `Date.now() + 365*24*60*60*1000`, `1704067200000`), `connector-auth.test.ts` (`1700000000`), `plugin-loading.test.ts` (`Date.now()`), `identity.test.ts` (`Date.now() + 3600000`). All integers; `Date.now()` is one by definition. 3. **Repo-wide fractional sweep with a positive control.** A regex for a fractional literal on any of the nine key names across `*.ts,tsx,js,mjs,json,yaml,mdx` returns exactly one hit — `packages/spec/src/kernel/context.test.ts:90 startedAt: 1.5` — which is the **refusal pin** on `KernelContext.startedAt`, a key that is ALREADY `EpochMs` (`it('should reject non-integer startedAt')`). That is the control: the instrument does find a fractional write when one exists, and it found no producer. A second sweep for `performance.now()` / `Math.random()` into those keys returns nothing. 4. **The suites**, post-merge: `pnpm --filter @objectstack/spec test` — 485 files / 13837 tests, all passing. No fixture needed changing. ## Measurement 2 — the gate's own summary line ``` BEFORE (at 559041d): 5 declared `EpochMs` instant(s) / 185 unit-declaring numeric key(s) AFTER (at 597bb75): 15 declared `EpochMs` instant(s) / 195 unit-declaring numeric key(s) ``` ⚠️ **The card's acceptance reading is off by one at the base.** It predicts 6 before and 16 after; the instrument reads **5 before and 15 after**. The delta is exactly +10, as ruled — only the base was wrong. The five pre-existing adopters are `api/websocket.zod.ts` `occurredAt` and `lastSeenAt`, `kernel/context.zod.ts` `startedAt`, `kernel/service-registry.zod.ts` `registeredAt` and `createdAt`; there is no sixth in the tree, before or after this change. The "6" is a census-side count, not this gate's. ## Measurement 3 — `tokenExpiry` and the A-class row shape Reported as the card asks, and one half of it is **NOT MEASURED**, deliberately, rather than reported clean: - **NOT MEASURED — "`tokenExpiry` stops being a `[name: -] [prose: -]` row" is not a statement this instrument can make.** `tsx scripts/check-duration-unit-keys.ts --list` printed **zero** `[name: -] [prose: -]` rows before the change, so `tokenExpiry` was not one of them and could not leave. The A-class 23 belongs to census #18078, a different instrument with its own classes; this gate only lists keys it ADMITS, and admission needs a unit in the key name, in a closed type, or in the prose. `tokenExpiry` had none of the three, so it was absent from the census entirely. - **Measured instead, the real transition:** `tokenExpiry` **enters** this census, admitted by TYPE. ``` BEFORE: (no row — the key is not in --list at all) AFTER: packages/spec/src/shared/connector-auth.zod.ts:39 tokenExpiry [name: -] [prose: ms] [instant: EpochMs] "Token expiry timestamp (Unix milliseconds)" ``` - And the row shape the card named does move, in the other direction: `[name: -] [prose: -]` rows go **0 → 4** — `document.createdAt`, `document.access.expiresAt`, `supplier-security.assessedAt`, `.validUntil`. Those are the four whose describe deliberately stays unit-free because their JSDoc already says "(Unix milliseconds)", exactly as the card instructs; they are admitted by type and carry `[jsdoc: ms]`. ## A gate red before green **`check:docs` — observed red, then green.** `check:generated` at the implementation commit: `✗ 1 of 15 artifact(s) stale: content/docs/references/**`, exit 1. After `gen:docs`: `✓ All 15 generated artifacts are up to date.`, exit 0. **`check:duration-unit-keys` — ablated, because it is green both before and after and would otherwise never have been observed failing for this change.** One describe on one of the new rows was mutated to name a different unit, proving the gate actually reaches the ten new sites rather than passing them by: ``` anchor counts BEFORE mutation: orig=1 mut=0 on-disk blob 747bb85… (== HEAD blob) anchor counts AFTER mutation: orig=0 mut=1 on-disk blob a1723e069fea… (≠ HEAD blob) RUN A (mutated) EXIT=1 ✗ check:duration-unit-keys — 1 offender(s) among 195 unit-declaring numeric key(s) [instant-unit-contradicts-schema] packages/spec/src/shared/connector-auth.zod.ts:39 `tokenExpiry` — typed `EpochMs` (epoch MILLISECONDS) but the describe says seconds. restore: git checkout HEAD -- PATH; on-disk blob 747bb85… (== HEAD blob); git diff HEAD empty anchor counts AFTER restore: orig=1 mut=0 RUN B (restored) EXIT=0 — 15 declared `EpochMs` instant(s); zero offenders, no baseline. ``` Expected direction was "turns red" and that is what was observed. The mutation ran under a `trap ... EXIT INT TERM` restore with absolute paths, the restore is proven by blob-hash identity with the `HEAD` blob (not by an exit code), and no test file was left behind. No `dist` preflight applies: this gate is a TypeScript **source** AST scan, so the on-disk byte proof above IS the reachability proof. ## Gates run 95 commands derived by `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` (it derives the change set itself from the merge base) were run. **94 exit 0.** Named explicitly by the card, all green: ``` pnpm --filter @objectstack/spec check:generated ✓ All 15 generated artifacts are up to date. pnpm --filter @objectstack/spec check:authorable-surface ✓ 1538 schemas generated pnpm --filter @objectstack/spec check:api-surface ✓ public API surface + factory signatures unchanged pnpm --filter @objectstack/spec check:docs ✓ 224 generated files in sync with packages/spec pnpm --filter @objectstack/spec typecheck ✓ (incl. check:scripts-typecheck, check:test-typecheck) pnpm --filter @objectstack/spec test ✓ 485 files / 13837 tests node scripts/pm/check-widening-tells.mjs --declaration no ✓ 10 file(s) — 5 judged against a declared surface (no widening tell), 5 NOT MEASURED (generated docs, no declared surface covers them) pnpm lint (eslint . --no-inline-config, whole repo) ✓ 6809 files judged, 0 errors, 0 warnings, at 597bb75 ``` `check:api-surface` reporting the surface **unchanged** is the load-bearing one for consumers: `z.input` of `EpochMs` is `number`, so no TypeScript type on any published entry moves and no importing package owes a re-measure. **NOT MEASURED (1):** `pnpm check:dual-build-cjs-loads` — exit 3, `PREREQUISITE NOT MET`, needs a full repo build (26+ connector packages have no `dist/` in this worktree). Neither a pass nor a failure. CI builds everything and owns it. Five siblings that first reported the same code (`check:doc-formula-expressions`, `check:doc-security-posture`, `check:skill-examples`, `check:docs-transcript-drift`, `check:lean-entry-closure`) were converted into real readings by building `@objectstack/lint`, `@objectstack/formula` and `@objectstack/client-react`, and all five are green. **One red that this diff does not cause:** `pnpm check:cross-package-test-inputs` exits 1 in this worktree, flagging `@objectstack/cli`'s directory walk from `packages/spec/dist/`. Proven unrelated by a two-legged control — see acceptance notes. ## File surface Ten paths, and every one of them is either a card row or a member of the pre-declared OPEN set: ``` packages/spec/src/data/document.zod.ts 2 rows + the EpochMs import packages/spec/src/system/supplier-security.zod.ts 3 rows + the import packages/spec/src/identity/identity.zod.ts 1 row + the import packages/spec/src/kernel/plugin-loading.zod.ts 3 rows + the import packages/spec/src/shared/connector-auth.zod.ts 1 row + the import content/docs/references/data/document.mdx OPEN set — gen:docs, gate-reverse-required content/docs/references/identity/identity.mdx OPEN set — gen:docs content/docs/references/integration/connector.mdx OPEN set — gen:docs content/docs/references/kernel/plugin-loading.mdx OPEN set — gen:docs content/docs/references/system/supplier-security.mdx OPEN set — gen:docs .changeset/18114-epochms-instants-tranche1.md OPEN set — pre-declared in the claim ``` Nothing under `packages/spec/src/migrations/**`, `packages/spec/src/ui/view.zod.ts`, `packages/spec/scripts/**`, `packages/spec/liveness/**` or `content/docs/releases/**` is touched. `packages/spec/scripts/check-duration-unit-keys.ts` is untouched: this tranche is data-only. ## Acceptance notes (out of scope, not filed as part of this PR) 1. **To file — `pnpm check:cross-package-test-inputs` decides one of its verdicts from a GITIGNORED directory's presence on disk, not from tracked source.** In a worktree where `packages/spec/dist/` exists it exits 1, flagging `@objectstack/cli`'s walk from `packages/spec/dist/` as reached by no declared glob; in a checkout of the identical source where it does not exist, it exits 0. Two-legged control, each an unbuilt worktree running `node scripts/check-cross-package-test-inputs.mjs` directly: the branch point `559041d39d` exits 0, and this branch's own tree exits 0 — then `mkdir -p packages/spec/dist/probe-subdir && echo x > .../a.d.ts` in that same clean tree flips it to exit 1 with the byte-identical finding. So the verdict is a property of the developer's build state; a contributor who builds spec before running the gate sees a red that a fresh checkout never shows, and the reverse hides it. Dedupe terms: `check-cross-package-test-inputs`, `walked root`, `packages/spec/dist`, `init-created-files-summary.e2e.test.ts`, `ACCEPTED_WALK_RADII`, `build-state dependent gate verdict`. 2. **Noted, not filed — the only first-party fixture for `tokenExpiry` carries a value that reads as epoch SECONDS.** `packages/spec/src/shared/connector-auth.test.ts:32` writes `tokenExpiry: 1700000000` — November 2023 read as seconds, 20 January 1970 read as milliseconds. It is an integer, so it is not a measurement-1 hit and the schema cannot tell the two apart; it is simply evidence that the ×1000 ambiguity this card closes was live in the tree rather than theoretical. The describe now says milliseconds, so the trap is shut at the contract. Successor: whoever next edits that fixture — this PR deliberately leaves the file untouched because it is outside the claim's declared file surface and changing it would measure nothing. 3. **Noted, not filed — `EpochMs`'s docblock recommends spelling an instant `*At`, and four of the ten keys do not** (`timestamp`, `tokenExpiry`, `deadline`, `validUntil`). The card rules a rename out: it is a retirement with its own ADR-0087 entry. Successor: named and already scoped by the ruling on #18080, not a loose end. --- _Generated by [Claude Code](https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 681ebe4 commit 25c9a83

11 files changed

Lines changed: 48 additions & 24 deletions

File tree

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
---
2+
"@objectstack/spec": minor
3+
---
4+
5+
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.
6+
7+
`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.
8+
9+
The keys, by schema:
10+
11+
- `Data.DocumentVersion.createdAt`, `Data.Document.access.expiresAt`
12+
- `System.SupplierSecurityAssessment.assessedAt`, `.validUntil`, `.remediationItems[].deadline`
13+
- `Identity.Account.expiresAt`
14+
- `Kernel.PluginLoadingEvent.timestamp`, `Kernel.PluginLoadingState.startedAt`, `.completedAt`
15+
- the shared connector OAuth2 auth shape's `tokenExpiry`
16+
17+
**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.
18+
19+
`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.

‎content/docs/references/data/document.mdx‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,15 +37,15 @@ const result = DocumentSchema.parse(data);
3737
| **versioning** | `{ enabled: boolean; versions: object[]; majorVersion: number; minorVersion: number }` | optional | Version control |
3838
| **template** | `{ id: string; name: string; description?: string; fileUrl: string; … }` | optional | Document template |
3939
| **eSignature** | `{ provider: Enum<'docusign' \| 'adobe-sign' \| 'hellosign' \| 'custom'>; enabled: boolean; signers: object[] }` | optional | E-signature config |
40-
| **access** | `{ isPublic: boolean; sharedWith?: string[]; expiresAt?: number }` | optional | Access control |
40+
| **access** | `{ isPublic: boolean; sharedWith?: string[]; expiresAt?: integer }` | optional | Access control |
4141
| **metadata** | `Record<string, any>` | optional | Custom metadata |
4242

4343
### Nested Shape: `Document.versioning`
4444

4545
| Property | Type | Required | Description |
4646
| :--- | :--- | :--- | :--- |
4747
| **enabled** | `boolean` | ✅ | Versioning enabled |
48-
| **versions** | `{ versionNumber: number; createdAt: number; createdBy: string; size: number; … }[]` | ✅ | Version history |
48+
| **versions** | `{ versionNumber: number; createdAt: integer; createdBy: string; size: number; … }[]` | ✅ | Version history |
4949
| **majorVersion** | `number` | ✅ | Major version |
5050
| **minorVersion** | `number` | ✅ | Minor version |
5151

@@ -76,7 +76,7 @@ const result = DocumentSchema.parse(data);
7676
| :--- | :--- | :--- | :--- |
7777
| **isPublic** | `boolean` | optional (default: `false`) | Public access |
7878
| **sharedWith** | `string[]` | optional | Shared with |
79-
| **expiresAt** | `number` | optional | Access expiration |
79+
| **expiresAt** | `integer` | optional | Access expiration |
8080

8181

8282
---
@@ -113,7 +113,7 @@ const result = DocumentSchema.parse(data);
113113
| Property | Type | Required | Description |
114114
| :--- | :--- | :--- | :--- |
115115
| **versionNumber** | `number` | ✅ | Version number |
116-
| **createdAt** | `number` | ✅ | Creation timestamp |
116+
| **createdAt** | `integer` | ✅ | Creation timestamp |
117117
| **createdBy** | `string` | ✅ | Creator user ID |
118118
| **size** | `number` | ✅ | File size in bytes |
119119
| **checksum** | `string` | ✅ | File checksum |

‎content/docs/references/identity/identity.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ const result = AccountSchema.parse(data);
4242
| **providerAccountId** | `string` | ✅ | Provider account ID |
4343
| **refreshToken** | `string` | optional | OAuth refresh token |
4444
| **accessToken** | `string` | optional | OAuth access token |
45-
| **expiresAt** | `number` | optional | Token expiry timestamp (Unix) |
45+
| **expiresAt** | `integer` | optional | Token expiry timestamp (Unix milliseconds) |
4646
| **tokenType** | `string` | optional | OAuth token type |
4747
| **scope** | `string` | optional | OAuth scope |
4848
| **idToken** | `string` | optional | OAuth ID token |

‎content/docs/references/integration/connector.mdx‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,7 @@ Circuit breaker configuration
204204
| **scopes** | `string[]` | optional | Requested OAuth2 scopes |
205205
| **redirectUri** | `string` | optional | OAuth2 redirect URI |
206206
| **refreshToken** | `string` | optional | Refresh token for token renewal |
207-
| **tokenExpiry** | `number` | optional | Token expiry timestamp |
207+
| **tokenExpiry** | `integer` | optional | Token expiry timestamp (Unix milliseconds) |
208208

209209
### Nested Shape: `Connector.authentication[type='api-key']`
210210

@@ -688,7 +688,7 @@ Connector type
688688
| **scopes** | `string[]` | optional | Requested OAuth2 scopes |
689689
| **redirectUri** | `string` | optional | OAuth2 redirect URI |
690690
| **refreshToken** | `string` | optional | Refresh token for token renewal |
691-
| **tokenExpiry** | `number` | optional | Token expiry timestamp |
691+
| **tokenExpiry** | `integer` | optional | Token expiry timestamp (Unix milliseconds) |
692692

693693
### Nested Shape: `DeclarativeConnectorEntry.authentication[type='api-key']`
694694

‎content/docs/references/kernel/plugin-loading.mdx‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ Plugin loading lifecycle event
4242
| :--- | :--- | :--- | :--- |
4343
| **type** | `Enum<'load-started' \| 'load-completed' \| 'load-failed' \| 'init-started' \| 'init-completed' \| 'init-failed' \| 'preload-started' \| 'preload-completed' \| 'cache-hit' \| … +5 more>` | ✅ | |
4444
| **pluginId** | `string` | ✅ | |
45-
| **timestamp** | `integer` | ✅ | |
45+
| **timestamp** | `integer` | ✅ | Event timestamp — Unix milliseconds |
4646
| **durationMs** | `integer` | optional | Duration in milliseconds |
4747
| **metadata** | `Record<string, any>` | optional | |
4848
| **error** | `{ message: string; code?: string; stack?: string }` | optional | |
@@ -78,8 +78,8 @@ Plugin loading state
7878
| **pluginId** | `string` | ✅ | |
7979
| **state** | `Enum<'pending' \| 'loading' \| 'loaded' \| 'initializing' \| 'ready' \| 'failed' \| 'reloading' \| 'unloading' \| 'unloaded'>` | ✅ | |
8080
| **progress** | `number` | optional (default: `0`) | |
81-
| **startedAt** | `integer` | optional | |
82-
| **completedAt** | `integer` | optional | |
81+
| **startedAt** | `integer` | optional | Loading start time — Unix milliseconds |
82+
| **completedAt** | `integer` | optional | Loading completion time — Unix milliseconds |
8383
| **lastError** | `string` | optional | |
8484
| **retryCount** | `integer` | optional (default: `0`) | |
8585

‎content/docs/references/system/supplier-security.mdx‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -66,14 +66,14 @@ Supplier security assessment record per ISO 27001:2022 A.5.19–A.5.21
6666
| **riskLevel** | `Enum<'critical' \| 'high' \| 'medium' \| 'low'>` | ✅ | Supplier risk classification |
6767
| **status** | `Enum<'pending' \| 'in_progress' \| 'completed' \| 'expired' \| 'failed'>` | ✅ | Assessment status |
6868
| **assessedBy** | `string` | ✅ | Assessor user ID or team |
69-
| **assessedAt** | `number` | ✅ | Assessment timestamp |
70-
| **validUntil** | `number` | ✅ | Assessment validity expiry timestamp |
69+
| **assessedAt** | `integer` | ✅ | Assessment timestamp |
70+
| **validUntil** | `integer` | ✅ | Assessment validity expiry timestamp |
7171
| **requirements** | `{ id: string; description: string; controlReference?: string; mandatory: boolean; … }[]` | ✅ | Security requirements and their compliance status |
7272
| **overallCompliant** | `boolean` | ✅ | Whether supplier meets all mandatory requirements |
7373
| **dataClassificationsShared** | `Enum<'pii' \| 'phi' \| 'pci' \| 'financial' \| 'confidential' \| 'internal' \| 'public'>[]` | optional | Data classifications shared with supplier |
7474
| **servicesProvided** | `string[]` | optional | Services provided by this supplier |
7575
| **certifications** | `string[]` | optional | Supplier certifications (e.g., ISO 27001, SOC 2) |
76-
| **remediationItems** | `{ requirementId: string; action: string; deadline: number; status: Enum<'pending' \| 'in_progress' \| 'completed'> }[]` | optional | Remediation items for non-compliant requirements |
76+
| **remediationItems** | `{ requirementId: string; action: string; deadline: integer; status: Enum<'pending' \| 'in_progress' \| 'completed'> }[]` | optional | Remediation items for non-compliant requirements |
7777
| **metadata** | `Record<string, any>` | optional | Custom metadata key-value pairs |
7878

7979
### Nested Shape: `SupplierSecurityAssessment.requirements[number]`
@@ -95,7 +95,7 @@ Individual supplier security requirement
9595
| :--- | :--- | :--- | :--- |
9696
| **requirementId** | `string` | ✅ | Non-compliant requirement ID |
9797
| **action** | `string` | ✅ | Required remediation action |
98-
| **deadline** | `number` | ✅ | Remediation deadline timestamp |
98+
| **deadline** | `integer` | ✅ | Remediation deadline timestamp (Unix milliseconds) |
9999
| **status** | `Enum<'pending' \| 'in_progress' \| 'completed'>` | optional (default: `"pending"`) | Remediation status |
100100

101101

‎packages/spec/src/data/document.zod.ts‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import { z } from 'zod';
44
import { retiredKey } from '../shared/retired-key';
5+
import { EpochMs } from '../shared/epoch.zod';
56

67
/**
78
* Document Version Schema
@@ -32,7 +33,7 @@ export const DocumentVersionSchema = lazySchema(() => z.object({
3233
/**
3334
* Timestamp when this version was created (Unix milliseconds)
3435
*/
35-
createdAt: z.number().describe('Creation timestamp'),
36+
createdAt: EpochMs.describe('Creation timestamp'),
3637

3738
/**
3839
* User ID who created this version
@@ -399,7 +400,7 @@ export const DocumentSchema = lazySchema(() => z.object({
399400
/**
400401
* Timestamp when access expires (Unix milliseconds)
401402
*/
402-
expiresAt: z.number().optional().describe('Access expiration'),
403+
expiresAt: EpochMs.optional().describe('Access expiration'),
403404
}).optional().describe('Access control'),
404405

405406
/**

‎packages/spec/src/identity/identity.zod.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
22

33
import { z } from 'zod';
4+
import { EpochMs } from '../shared/epoch.zod';
45

56
/**
67
* Identity & User Model Specification
@@ -108,7 +109,7 @@ export const AccountSchema = lazySchema(() => z.object({
108109
/**
109110
* Token expiry timestamp
110111
*/
111-
expiresAt: z.number().optional().describe('Token expiry timestamp (Unix)'),
112+
expiresAt: EpochMs.optional().describe('Token expiry timestamp (Unix milliseconds)'),
112113

113114
/**
114115
* OAuth token type

‎packages/spec/src/kernel/plugin-loading.zod.ts‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import { z } from 'zod';
44
import { lazySchema } from '../shared/lazy-schema';
5+
import { EpochMs } from '../shared/epoch.zod';
56

67
/**
78
* # Plugin Loading Protocol
@@ -96,7 +97,7 @@ export const PluginLoadingEventSchema = lazySchema(() => z.object({
9697
/**
9798
* Timestamp
9899
*/
99-
timestamp: z.number().int().min(0),
100+
timestamp: EpochMs.min(0).describe('Event timestamp — Unix milliseconds'),
100101

101102
/**
102103
* Duration in milliseconds
@@ -151,12 +152,12 @@ export const PluginLoadingStateSchema = lazySchema(() => z.object({
151152
/**
152153
* Loading start time
153154
*/
154-
startedAt: z.number().int().min(0).optional(),
155+
startedAt: EpochMs.min(0).optional().describe('Loading start time — Unix milliseconds'),
155156

156157
/**
157158
* Loading completion time
158159
*/
159-
completedAt: z.number().int().min(0).optional(),
160+
completedAt: EpochMs.min(0).optional().describe('Loading completion time — Unix milliseconds'),
160161

161162
/**
162163
* Last error

‎packages/spec/src/shared/connector-auth.zod.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
22

33
import { z } from 'zod';
4+
import { EpochMs } from './epoch.zod';
45

56
/**
67
* SHARED CONNECTOR AUTHENTICATION SCHEMAS
@@ -35,7 +36,7 @@ export const ConnectorOAuth2Schema = lazySchema(() => z.object({
3536
scopes: z.array(z.string()).optional().describe('Requested OAuth2 scopes'),
3637
redirectUri: z.string().url().optional().describe('OAuth2 redirect URI'),
3738
refreshToken: z.string().optional().describe('Refresh token for token renewal'),
38-
tokenExpiry: z.number().optional().describe('Token expiry timestamp'),
39+
tokenExpiry: EpochMs.optional().describe('Token expiry timestamp (Unix milliseconds)'),
3940
}));
4041

4142
/**

0 commit comments

Comments
 (0)