Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
e22957d
feat(spec): declare the two duration-rule exemptions on the schema (#…
claude Sep 5, 2026
414e515
feat(spec)!: move the six epoch instants onto EpochMs and mark the ex…
claude Sep 5, 2026
3f95444
feat(spec): publish the externalVocabulary standard on the reference …
claude Sep 5, 2026
884646a
docs(changeset): the two duration-rule exemptions (#15676)
claude Sep 5, 2026
c1949b9
chore(spec): regenerate the derived artifacts and fix the consumers t…
claude Sep 5, 2026
796f24f
wip(spec): rename the 12 api/ duration keys, tombstones on the old sp…
claude Sep 5, 2026
7e870b1
wip(spec): readers, ADR-0087 registrations, ledger row for the api/ r…
claude Sep 5, 2026
37fc158
wip(spec): tombstone refusal tests, alias retarget, regenerated artif…
claude Sep 5, 2026
8cd4d8c
docs(changeset): the twelve api/ duration renames (#15677)
claude Sep 5, 2026
d7ebd6c
fix(docs-audit): declare the conversion-replay exclusion kind (b) rel…
claude Sep 5, 2026
dc3b847
docs: move the hand-written pages onto the renamed keys, and strip th…
claude Sep 5, 2026
9605808
docs(spec): drop the internal issue id from the four epoch-instant to…
claude Sep 5, 2026
19de17a
Merge remote-tracking branch 'origin/claude/issue-15676-epoch-ms-and-…
claude Sep 5, 2026
b463390
chore(spec): regenerate the reference page the merge deferred (#15677)
claude Sep 5, 2026
ecbc4be
wip(spec): rename the 14 kernel/ duration keys, tombstones on the old…
claude Sep 5, 2026
cf5c834
wip(spec): readers and ADR-0087 registrations for the kernel/ renames…
claude Sep 5, 2026
6bf1119
wip(spec): tombstone refusal tests and regenerated reference pages (#…
claude Sep 5, 2026
8f2b8f3
docs(changeset): the fourteen kernel/ duration renames (#15678)
claude Sep 5, 2026
adad660
docs(changeset): correct why `RuntimeConfig.resourceLimits.timeout` i…
claude Sep 5, 2026
d48ff25
Merge the #14478 trunk into the kernel duration-unit card
claude Sep 5, 2026
7abb2ff
chore(spec): regenerate the os-regen artifacts on the merged tree
claude Sep 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 114 additions & 0 deletions .changeset/kernel-duration-keys-unit-in-key-name.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
---
"@objectstack/spec": minor
"@objectstack/core": patch
---

feat(spec)!: the fourteen `kernel/` duration keys carry their unit in the key name (#15678, ruling B on #14478)

<!-- adr-0087: registered kernel-event-bus-retention-unit-in-key, kernel-package-lifecycle-durations-unit-in-key, kernel-plugin-health-report-durations-unit-in-key, kernel-plugin-security-durations-unit-in-key, kernel-startup-orchestrator-durations-unit-in-key -->

**BREAKING** — fourteen published `kernel/` duration keys are renamed and
tombstoned. Shipped as `minor` under the repo's launch-window convention for
breaking changes; the hand-migration prescriptions are registered under protocol
major 18. Maintainer ruling B on #14478 (2026-09-02, decision batch #43,
「同意」).

`check:duration-unit-keys` makes a duration-shaped `z.number()` carry its unit
in the key NAME, never only in its `.describe()` prose, and grandfathers no
existing offender. Stack card 1/6 (#15676) landed the rule's two structural
exemptions and card 2/6 (#15677) cleared `api/`; this card clears `kernel/`.
Measured with the gate itself: `src/kernel/**` goes from 14 offenders to **0**,
and the whole-tree count falls **36 → 22**.

## FROM → TO

| key | replacement | unit |
|:--|:--|:--|
| `EventPersistence.retention` | `retentionDays` | days |
| `EventSourcingConfig.retention` | `retentionDays` | days |
| `UpgradePlan.estimatedDuration` | `estimatedDurationSeconds` | seconds |
| `PluginHealthReport.metrics.uptime` | `uptimeMs` | milliseconds |
| `PluginHealthReport.metrics.responseTime` | `responseTimeMs` | milliseconds |
| `SandboxConfig.process.timeout` | `timeoutMs` | milliseconds |
| `KernelSecurityPolicy.authentication.tokenExpiration` | `tokenExpirationSeconds` | seconds |
| `KernelSecurityPolicy.auditLog.retention` | `retentionDays` | days |
| `PluginSecurityManifest.vulnerabilityDisclosure.responseTime` | `responseTimeHours` | hours |
| `PackageDependencyResolutionResult.resolvedIn` | `resolvedInMs` | milliseconds |
| `MultiVersionSupport.rollout.duration` | `durationMs` | milliseconds |
| `StartupOptions.timeout` | `timeoutMs` | milliseconds |
| `PluginStartupResult.duration` | `durationMs` | milliseconds |
| `StartupOrchestrationResult.totalDuration` | `totalDurationMs` | milliseconds |

**Every value is unchanged** — only key names move, and every default moves with
its key (`StartupOptions` still defaults to 30000, `EventSourcingConfig` to
365). Every old spelling is a `retiredKey()` tombstone, so it fails `tsc` at the
authoring site (input type `never`) and fails the parse with the rename
prescription rather than a bare unrecognized-key error.

## ⚠️ Two collisions this rename removes — check these by hand, not by search-and-replace

**`responseTime` meant two different units on two kernel shapes.** On
`PluginSecurityManifest.vulnerabilityDisclosure` it is HOURS (how fast a
publisher promises to answer a vulnerability report); on
`PluginHealthReport.metrics` the identical bare name is MILLISECONDS. So
`responseTime: 24` was a day on one shape and a fortieth of a second on the
other, with nothing at the authoring site to tell them apart. They land on
`responseTimeHours` and `responseTimeMs` respectively — do not let one
find-and-replace rewrite both.

**`uptime` is milliseconds here and SECONDS on `GET /health`.** That collision
was already costing prose: the protocol lifecycle page carried a standing
paragraph whose only job was telling the two apart. `metrics.uptime` becomes
`metrics.uptimeMs`; the seconds-valued `uptime` of the HTTP health body is a
separate, unchanged surface and must not be renamed with it.

A third split worth reading before you migrate: `estimatedDurationSeconds: 120`
is two MINUTES while `durationMs: 3600000` is one HOUR. Three adjacent
measurements of the same package install carried two different units, and no
parse can catch a value moved between them — both bounds accept any
non-negative integer.

## Dispositions — five semantic entries, no D2 conversion

Justified per key rather than defaulted, and this card's answer is uniform:
**none of the fourteen gets an ADR-0087 D2 conversion.** A D2 conversion runs
over a stack document, and `stack.zod.ts` declares no `eventBus`, `startup`,
`upgrade` or plugin-security root — none of these twelve defs is a stack
collection member or a registered metadata kind stored as a `sys_metadata` row,
so the conversion chain has no seam that would see one. They are host
construction arguments (`EventBusConfig`, `StartupOptions`, `SandboxConfig`,
`MultiVersionSupport`), package artifacts (`PluginSecurityManifest`) and
runtime-emitted measurements (`PluginHealthReport`, `PluginStartupResult`,
`StartupOrchestrationResult`, `UpgradePlan`,
`PackageDependencyResolutionResult`). Each therefore carries a **semantic**
entry, which is the disposition `kernel/HealthStatus:timestamp` already holds on
one of these very files (`epoch-instant-keys-renamed`, card 1/6) and what ruling
B prescribes for a key that is not authorable metadata. All fourteen are
registered by exact key in `RETIRED_KEYS_BY_MAJOR`.

## Keys deliberately left alone

`EventSourcingConfig.snapshotRetention` is a COUNT of snapshots and
`MultiVersionSupport.rollout.percentage` is a proportion — neither is a
duration, so neither has a unit to carry and both keep their names.
`RuntimeConfig.resourceLimits.timeout` names its unit only in the JSDoc above
the key ("Execution timeout in milliseconds"), a channel
`check:duration-unit-keys` does not read: it reads `.describe()` and
`.meta({ description })`, and this key's describe ("Maximum execution time")
names none. The gate therefore lists it among the duration-shaped keys but
deliberately does not judge it — neither an offender nor an exemption — so it is
outside this rename; that JSDoc-channel gap is filed as #15939. A pin test
asserts the key still parses bare, so a later sweep cannot read the four
security renames as "every timeout on that file".

## Readers moved in the same PR, at the same magnitude

`@objectstack/core`'s health monitor (`metrics.uptimeMs: Date.now() -
startTime`), the kernel and contracts test suites, and the hand-written
`content/docs/protocol/kernel/lifecycle.mdx`, whose `uptime` paragraph now
states the collision the rename removes.

⚠️ `packages/core/src/plugin-loader.ts` declares its OWN local
`PluginStartupResult` interface — a different type, carrying `startTime` rather
than any duration key. It is not a reader of this schema, it is untouched by
this rename, and the divergence between the two shapes is tracked separately.
9 changes: 5 additions & 4 deletions content/docs/protocol/kernel/lifecycle.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -770,7 +770,7 @@ this shape over HTTP — it is an in-process model, not a wire body.
"status": "healthy",
"timestamp": "2024-01-15T11:00:00.000Z",
"metrics": {
"uptime": 3600000
"uptimeMs": 3600000
},
"checks": [
{ "name": "healthCheck", "status": "passed" }
Expand All @@ -789,12 +789,13 @@ this shape over HTTP — it is an in-process model, not a wire body.
| `"plugin-loaded"` | no `checkMethod` is configured, **or** the configured name does not resolve to a function on the plugin |
| `"health-check"` | the check **threw** — a `timeout` overrun included, since the race surfaces it as a rejection. A fixed name, neither the method's nor the default's, and always `status: "failed"` |

`metrics.uptime` is in **milliseconds** (`Date.now() - startTime`), unlike
the seconds-valued `uptime` of `GET /health` above, and the report carries no
`metrics.uptimeMs` is in **milliseconds** (`Date.now() - startTime`), unlike
the seconds-valued `uptime` of `GET /health` above — which is the very
collision the unit-in-the-key-name rule exists to remove — and the report carries no
`version` field — it identifies its plugin by the key it is stored under. The
optional `message` is set only when a check fails; the schema's remaining
`metrics` fields (`memoryUsage`, `cpuUsage`, `activeConnections`, `errorRate`,
`responseTime`) and its `dependencies` array are declared but left unset by the
`responseTimeMs`) and its `dependencies` array are declared but left unset by the
monitor today.

## Shutdown Sequence
Expand Down
10 changes: 6 additions & 4 deletions content/docs/references/kernel/events-bus.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,9 @@ const result = EventBusConfigSchema.parse(data);

| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **persistence** | `{ enabled: boolean; retention: integer; filter?: any; storage: Enum<'database' \| 'file' \| 's3' \| 'custom'> }` | optional | Event persistence configuration |
| **persistence** | `{ enabled: boolean; retentionDays: integer; filter?: any; storage: Enum<'database' \| 'file' \| 's3' \| 'custom'> }` | optional | Event persistence configuration |
| **queue** | `{ name: string; concurrency: integer; retryPolicy?: object; deadLetterQueue?: string; … }` | optional | Event queue configuration |
| **eventSourcing** | `{ enabled: boolean; snapshotInterval: integer; snapshotRetention: integer; retention: integer; … }` | optional | Event sourcing configuration |
| **eventSourcing** | `{ enabled: boolean; snapshotInterval: integer; snapshotRetention: integer; retentionDays: integer; … }` | optional | Event sourcing configuration |
| **replay** | `{ enabled: boolean }` | optional | Event replay configuration |
| **webhooks** | `{ id?: string; eventPattern: string; url: string; method: Enum<'GET' \| 'POST' \| 'PUT' \| 'PATCH'>; … }[]` | optional | Webhook configurations |
| **messageQueue** | `{ provider: Enum<'kafka' \| 'rabbitmq' \| 'aws-sqs' \| 'redis-pubsub' \| 'google-pubsub' \| 'azure-service-bus'>; topic: string; eventPattern: string; partitionKey?: string; … }` | optional | Message queue integration |
Expand All @@ -42,7 +42,8 @@ const result = EventBusConfigSchema.parse(data);
| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **enabled** | `boolean` | optional (default: `false`) | Enable event persistence |
| **retention** | `integer` | ✅ | Days to retain persisted events |
| **retentionDays** | `integer` | ✅ | Days to retain persisted events |
| **retention** | `never` | optional | [REMOVED] `EventPersistence.retention` was renamed to `retentionDays` in @objectstack/spec 17 — the unit of a duration-shaped number lives in the key name, not only in the describe prose. Rename the key to `retentionDays`; the value (days) is unchanged. |
| **filter** | `any` | optional | Optional filter function to select which events to persist |
| **storage** | `Enum<'database' \| 'file' \| 's3' \| 'custom'>` | optional (default: `"database"`) | Storage backend for persisted events |

Expand All @@ -63,7 +64,8 @@ const result = EventBusConfigSchema.parse(data);
| **enabled** | `boolean` | optional (default: `false`) | Enable event sourcing |
| **snapshotInterval** | `integer` | optional (default: `100`) | Create snapshot every N events |
| **snapshotRetention** | `integer` | optional (default: `10`) | Number of snapshots to retain |
| **retention** | `integer` | optional (default: `365`) | Days to retain events |
| **retentionDays** | `integer` | optional (default: `365`) | Days to retain events |
| **retention** | `never` | optional | [REMOVED] `EventSourcingConfig.retention` was renamed to `retentionDays` in @objectstack/spec 17 — the unit of a duration-shaped number lives in the key name, not only in the describe prose. Rename the key to `retentionDays`; the value (days) is unchanged. The neighbouring `snapshotRetention` is a COUNT of snapshots, not a duration, so it keeps its name. |
| **aggregateTypes** | `string[]` | optional | Aggregate types to enable event sourcing for |
| **storage** | `{ type: Enum<'database' \| 'file' \| 's3' \| 'eventstore'>; options?: Record<string, any> }` | optional | Event store configuration |

Expand Down
3 changes: 2 additions & 1 deletion content/docs/references/kernel/events-handlers.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@ const result = EventHandlerSchema.parse(data);
| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **enabled** | `boolean` | optional (default: `false`) | Enable event persistence |
| **retention** | `integer` | ✅ | Days to retain persisted events |
| **retentionDays** | `integer` | ✅ | Days to retain persisted events |
| **retention** | `never` | optional | [REMOVED] `EventPersistence.retention` was renamed to `retentionDays` in @objectstack/spec 17 — the unit of a duration-shaped number lives in the key name, not only in the describe prose. Rename the key to `retentionDays`; the value (days) is unchanged. |
| **filter** | `any` | optional | Optional filter function to select which events to persist |
| **storage** | `Enum<'database' \| 'file' \| 's3' \| 'custom'>` | optional (default: `"database"`) | Storage backend for persisted events |

Expand Down
3 changes: 2 additions & 1 deletion content/docs/references/kernel/events-queue.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,8 @@ const result = EventQueueConfigSchema.parse(data);
| **enabled** | `boolean` | optional (default: `false`) | Enable event sourcing |
| **snapshotInterval** | `integer` | optional (default: `100`) | Create snapshot every N events |
| **snapshotRetention** | `integer` | optional (default: `10`) | Number of snapshots to retain |
| **retention** | `integer` | optional (default: `365`) | Days to retain events |
| **retentionDays** | `integer` | optional (default: `365`) | Days to retain events |
| **retention** | `never` | optional | [REMOVED] `EventSourcingConfig.retention` was renamed to `retentionDays` in @objectstack/spec 17 — the unit of a duration-shaped number lives in the key name, not only in the describe prose. Rename the key to `retentionDays`; the value (days) is unchanged. The neighbouring `snapshotRetention` is a COUNT of snapshots, not a duration, so it keeps its name. |
| **aggregateTypes** | `string[]` | optional | Aggregate types to enable event sourcing for |
| **storage** | `{ type: Enum<'database' \| 'file' \| 's3' \| 'eventstore'>; options?: Record<string, any> }` | optional | Event store configuration |

Expand Down
6 changes: 4 additions & 2 deletions content/docs/references/kernel/package-upgrade.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,8 @@ Upgrade package response
| **requiresMigration** | `boolean` | optional (default: `false`) | Whether data migration scripts are needed |
| **migrationScripts** | `string[]` | optional | Paths to migration scripts |
| **dependencyUpgrades** | `{ packageId: string; fromVersion: string; toVersion: string }[]` | optional | Dependent packages that also need upgrading |
| **estimatedDuration** | `integer` | optional | Estimated upgrade duration in seconds |
| **estimatedDurationSeconds** | `integer` | optional | Estimated upgrade duration in seconds |
| **estimatedDuration** | `never` | optional | [REMOVED] `UpgradePlan.estimatedDuration` was renamed to `estimatedDurationSeconds` in @objectstack/spec 17 — the unit of a duration-shaped number lives in the key name, not only in the describe prose. Rename the key to `estimatedDurationSeconds`; the value (seconds) is unchanged. |
| **summary** | `string` | optional | Human-readable upgrade summary |


Expand Down Expand Up @@ -241,7 +242,8 @@ Upgrade analysis plan generated before execution
| **requiresMigration** | `boolean` | optional (default: `false`) | Whether data migration scripts are needed |
| **migrationScripts** | `string[]` | optional | Paths to migration scripts |
| **dependencyUpgrades** | `{ packageId: string; fromVersion: string; toVersion: string }[]` | optional | Dependent packages that also need upgrading |
| **estimatedDuration** | `integer` | optional | Estimated upgrade duration in seconds |
| **estimatedDurationSeconds** | `integer` | optional | Estimated upgrade duration in seconds |
| **estimatedDuration** | `never` | optional | [REMOVED] `UpgradePlan.estimatedDuration` was renamed to `estimatedDurationSeconds` in @objectstack/spec 17 — the unit of a duration-shaped number lives in the key name, not only in the describe prose. Rename the key to `estimatedDurationSeconds`; the value (seconds) is unchanged. |
| **summary** | `string` | optional | Human-readable upgrade summary |

### Nested Shape: `UpgradePlan.changes[number]`
Expand Down
8 changes: 5 additions & 3 deletions content/docs/references/kernel/plugin-lifecycle-advanced.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -79,20 +79,22 @@ const result = HotReloadConfigSchema.parse(data);
| **status** | `Enum<'healthy' \| 'degraded' \| 'unhealthy' \| 'failed' \| 'recovering' \| 'unknown'>` | ✅ | Current health status of the plugin |
| **timestamp** | `string` | ✅ | |
| **message** | `string` | optional | |
| **metrics** | `{ uptime?: number; memoryUsage?: number; cpuUsage?: number; activeConnections?: number; … }` | optional | |
| **metrics** | `{ uptimeMs?: number; memoryUsage?: number; cpuUsage?: number; activeConnections?: number; … }` | optional | |
| **checks** | `{ name: string; status: Enum<'passed' \| 'failed' \| 'warning'>; message?: string; data?: Record<string, any> }[]` | optional | |
| **dependencies** | `{ pluginId: string; status: Enum<'healthy' \| 'degraded' \| 'unhealthy' \| 'failed' \| 'recovering' \| 'unknown'>; message?: string }[]` | optional | |

### Nested Shape: `PluginHealthReport.metrics`

| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **uptime** | `number` | optional | Plugin uptime in milliseconds |
| **uptimeMs** | `number` | optional | Plugin uptime in milliseconds |
| **memoryUsage** | `number` | optional | Memory usage in bytes |
| **cpuUsage** | `number` | optional | CPU usage percentage |
| **activeConnections** | `number` | optional | Number of active connections |
| **errorRate** | `number` | optional | Error rate (errors per minute) |
| **responseTime** | `number` | optional | Average response time in ms |
| **responseTimeMs** | `number` | optional | Average response time in ms |
| **uptime** | `never` | optional | [REMOVED] `PluginHealthReport.metrics.uptime` was renamed to `uptimeMs` in @objectstack/spec 17 — the unit of a duration-shaped number lives in the key name, not only in the describe prose, and this platform already spells a SECONDS-valued uptime with the same bare name on GET /health. Rename the key to `uptimeMs`; the value (milliseconds, `Date.now() - startTime`) is unchanged. |
| **responseTime** | `never` | optional | [REMOVED] `PluginHealthReport.metrics.responseTime` was renamed to `responseTimeMs` in @objectstack/spec 17 — the unit of a duration-shaped number lives in the key name, not only in the describe prose. Rename the key to `responseTimeMs`; the value (milliseconds) is unchanged. |

### Nested Shape: `PluginHealthReport.checks[number]`

Expand Down
Loading