Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
98 changes: 98 additions & 0 deletions .changeset/17782-logging-duration-units.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
---
"@objectstack/spec": minor
---

feat(spec)!: the four `system/logging.zod.ts` duration keys carry their unit in the key name (#17782, ruling A on #15939)

<!-- adr-0087: registered logging-durations-unit-in-key -->

**BREAKING** — the HTTP log destination's batch flush, retry backoff start and request deadline,
and the logging buffer's flush, now carry `Ms` in the key name.

| def | before | after |
|:--|:--|:--|
| `HttpDestinationConfig` | `batch.flushInterval: 5000` | `batch.flushIntervalMs: 5000` |
| `HttpDestinationConfig` | `retry.initialDelay: 1000` | `retry.initialDelayMs: 1000` |
| `HttpDestinationConfig` | `timeout: 30000` | `timeoutMs: 30000` |
| `LoggingConfig` | `buffer.flushInterval: 1000` | `buffer.flushIntervalMs: 1000` |
| values, defaults, bounds | ms; 5000 / 1000 / 30000 / 1000; positive int | **unchanged** |

## Migration

```diff
const destination = HttpDestinationConfigSchema.parse({
url: 'https://logs.example.com/v1/logs',
- batch: { maxSize: 500, flushInterval: 10000 },
- retry: { maxAttempts: 3, initialDelay: 1000 },
- timeout: 30000,
+ batch: { maxSize: 500, flushIntervalMs: 10000 },
+ retry: { maxAttempts: 3, initialDelayMs: 1000 },
+ timeoutMs: 30000,
});

const logging = LoggingConfigSchema.parse({
name: 'app_logging',
label: 'App logging',
destinations: [],
- buffer: { enabled: true, size: 5000, flushInterval: 2000 },
+ buffer: { enabled: true, size: 5000, flushIntervalMs: 2000 },
});
```

Rename the keys. Every value is the same number of milliseconds it always was, and the
5000 / 1000 / 30000 / 1000 defaults are unchanged; nothing else on either def moves.

## Why

Each key named milliseconds in a source JSDoc — "Flush interval in milliseconds", "Initial retry
delay in milliseconds", "Timeout in milliseconds" — and the JSDoc above a key is not what
`content/docs/references/**` renders; `.describe()` is, and **none of the four carried one at
all**. Measured by the `check:duration-unit-keys` census on this tree before the change, all four
read `[name: -] [prose: -]`: no unit in the key, and no published prose to supply it either. So
`content/docs/references/system/logging.mdx` printed a bare `5000` / `1000` / `30000` / `1000`,
and nothing on the page decided milliseconds from seconds. Under the #14478 rule, moving the unit
into the describe alone would itself be a violation (unit in prose, none in the name), so each key
is renamed and given the describe it never had in the same stroke. Executes director-seat ruling A
on #15939 (2026-09-11, maintainer 「同意」, decision batch #115), the per-file remediation of the
#14478 rule.

⚠️ `flushInterval` was declared **twice** on this file, in two different defs and with two
different defaults — 5000 on the HTTP destination's `batch`, 1000 on the logging `buffer`. They are
two keys, not one; each gets its own tombstone, its own registered row, and a prescription that
names its def, so an author who lands on one is not sent to the other.

The `Ms` suffix is the family's own spelling, counted in key position on this tree: 272 `*Ms:`
declarations in `packages/spec/src` against 75 `*Seconds:`. The only competing unit spellings are
3 `*MS:` and 9 `*Millis:`, and every one of them mirrors a name fixed outside this repo — MongoDB's
`maxCommitTimeMS` and `connectTimeoutMS`, node-postgres's `idleTimeoutMillis` and
`connectionTimeoutMillis` on `PoolConfigSchema` — so unlike the `Ttl`-versus-`TTL` question a
sibling round had to settle, there was no in-repo alternative to choose between. All three target
spellings were already attested as key-position `*.zod.ts` declarations before this change:
`flushIntervalMs` 1 (on `kernel/events/integrations.zod.ts`, at the same 1000 default),
`initialDelayMs` 5, `timeoutMs` 30.

## The kit

- a `retiredKey()` tombstone on each of the four old spellings, so `tsc` types it `never` and a
value reaching the parse raises the rename prescription instead of being silently stripped — none
of the four enclosing objects is `.strict()` (`HttpDestinationConfig` itself and its nested
`batch` and `retry`; `LoggingConfig`'s nested `buffer`)
- the ADR-0087 D3 semantic entry `logging-durations-unit-in-key` and four
`RETIRED_KEYS_BY_MAJOR[18]` rows, one per key. No D2 conversion: `stack.zod.ts` declares no
logging collection and neither `LoggingConfigSchema` nor `HttpDestinationConfigSchema` is
referenced anywhere in `packages/spec/src` outside `system/logging.zod.ts`, so the chain has no
rehydration seam that runs on an authored logging document — the same reading
`tenant-schema-cache-ttl-unit-in-key` recorded for its sibling key
- pin tests per key: the refusal carries the rename prescription and names the def, the suffixed
key parses at the magnitude the retired one carried with the same default, and the describe
publishes the unit
- exactly one authorable-surface row pair moves, and it is the one that should: that ratchet records
top-level keys per def (`build-schemas.ts` reads `schema.properties` one level deep), and
`HttpDestinationConfig.timeout` is the only top-level key of the four —
`system/HttpDestinationConfig:timeout` becomes `[RETIRED]` beside a new
`system/HttpDestinationConfig:timeoutMs`, and the `authorable-defaults/` row is renamed with it.
The three nested keys move neither file, which is correct and not an omission
- the pinned objectui checkout is untouched by this rename: at `.objectui-sha` pin
`53ded82bf7a494f54e344e19099dbf00854b8694` it spells `flushInterval` 0 times, `initialDelay` 0,
`HttpDestinationConfig` 0 and `LoggingConfig` 0 across its 6409 tracked files, against lit
controls `useState` 2304 and `timeout` 702 on the same corpus
43 changes: 36 additions & 7 deletions content/docs/references/system/logging.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -109,9 +109,10 @@ HTTP destination configuration
| **method** | `Enum<'POST' \| 'PUT'>` | optional (default: `"POST"`) | |
| **headers** | `Record<string, string>` | optional | |
| **auth** | `{ type: Enum<'basic' \| 'bearer' \| 'api_key'>; username?: string; password?: string; token?: string; … }` | optional | |
| **batch** | `{ maxSize: integer; flushInterval: integer }` | optional | |
| **retry** | `{ maxAttempts: integer; initialDelay: integer; backoffMultiplier: number }` | optional | |
| **timeout** | `integer` | optional (default: `30000`) | |
| **batch** | `{ maxSize: integer; flushIntervalMs: integer }` | optional | |
| **retry** | `{ maxAttempts: integer; initialDelayMs: integer; backoffMultiplier: number }` | optional | |
| **timeoutMs** | `integer` | optional (default: `30000`) | Timeout in milliseconds |
| **timeout** | `never` | optional | [REMOVED] `HttpDestinationConfig.timeout` was renamed to `timeoutMs` in @objectstack/spec 17 — the unit of a duration-shaped number lives in the key name, not only in the describe prose. Its unit (milliseconds) lived in a source JSDoc only and the key carried no `.describe()` at all, so the reference page published a bare 30000. Rename the key to `timeoutMs`; the value (milliseconds) and the 30000 default are unchanged. |

### Nested Shape: `HttpDestinationConfig.auth`

Expand All @@ -124,6 +125,23 @@ HTTP destination configuration
| **apiKey** | `string` | optional | |
| **apiKeyHeader** | `string` | optional (default: `"X-API-Key"`) | |

### Nested Shape: `HttpDestinationConfig.batch`

| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **maxSize** | `integer` | optional (default: `100`) | |
| **flushIntervalMs** | `integer` | optional (default: `5000`) | Flush interval in milliseconds |
| **flushInterval** | `never` | optional | [REMOVED] `HttpDestinationConfig.batch.flushInterval` was renamed to `flushIntervalMs` in @objectstack/spec 17 — the unit of a duration-shaped number lives in the key name, not only in the describe prose. Its unit (milliseconds) lived in a source JSDoc only and the key carried no `.describe()` at all, so the reference page published a bare 5000. Rename the key to `flushIntervalMs`; the value (milliseconds) and the 5000 default are unchanged. This is the batch flush on an HTTP log destination — `LoggingConfig.buffer.flushInterval` is a different key with its own rename. |

### Nested Shape: `HttpDestinationConfig.retry`

| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **maxAttempts** | `integer` | optional (default: `3`) | |
| **initialDelayMs** | `integer` | optional (default: `1000`) | Initial retry delay in milliseconds |
| **initialDelay** | `never` | optional | [REMOVED] `HttpDestinationConfig.retry.initialDelay` was renamed to `initialDelayMs` in @objectstack/spec 17 — the unit of a duration-shaped number lives in the key name, not only in the describe prose. Its unit (milliseconds) lived in a source JSDoc only and the key carried no `.describe()` at all, so the reference page published a bare 1000. Rename the key to `initialDelayMs`; the value (milliseconds) and the 1000 default are unchanged. |
| **backoffMultiplier** | `number` | optional (default: `2`) | |


---

Expand Down Expand Up @@ -163,9 +181,10 @@ Log destination configuration
| **method** | `Enum<'POST' \| 'PUT'>` | optional (default: `"POST"`) | |
| **headers** | `Record<string, string>` | optional | |
| **auth** | `{ type: Enum<'basic' \| 'bearer' \| 'api_key'>; username?: string; password?: string; token?: string; … }` | optional | |
| **batch** | `{ maxSize: integer; flushInterval: integer }` | optional | |
| **retry** | `{ maxAttempts: integer; initialDelay: integer; backoffMultiplier: number }` | optional | |
| **timeout** | `integer` | optional (default: `30000`) | |
| **batch** | `{ maxSize: integer; flushIntervalMs: integer }` | optional | |
| **retry** | `{ maxAttempts: integer; initialDelayMs: integer; backoffMultiplier: number }` | optional | |
| **timeoutMs** | `integer` | optional (default: `30000`) | Timeout in milliseconds |
| **timeout** | `never` | optional | [REMOVED] `HttpDestinationConfig.timeout` was renamed to `timeoutMs` in @objectstack/spec 17 — the unit of a duration-shaped number lives in the key name, not only in the describe prose. Its unit (milliseconds) lived in a source JSDoc only and the key carried no `.describe()` at all, so the reference page published a bare 30000. Rename the key to `timeoutMs`; the value (milliseconds) and the 30000 default are unchanged. |


---
Expand Down Expand Up @@ -297,7 +316,7 @@ Logging configuration
| **enrichment** | `{ staticFields?: Record<string, any>; dynamicEnrichers?: string[]; addHostname: boolean; addProcessId: boolean; … }` | optional | Log enrichment configuration |
| **redact** | `string[]` | optional (has default) | Fields to redact |
| **sampling** | `{ enabled: boolean; rate: number; rateByLevel?: Record<string, number> }` | optional | |
| **buffer** | `{ enabled: boolean; size: integer; flushInterval: integer; flushOnShutdown: boolean }` | optional | |
| **buffer** | `{ enabled: boolean; size: integer; flushIntervalMs: integer; flushOnShutdown: boolean }` | optional | |
| **performance** | `{ async: boolean; workers: integer }` | optional | |

### Nested Shape: `LoggingConfig.default`
Expand Down Expand Up @@ -354,6 +373,16 @@ Log destination configuration
| **addCaller** | `boolean` | optional (default: `false`) | |
| **addCorrelationIds** | `boolean` | optional (default: `true`) | |

### Nested Shape: `LoggingConfig.buffer`

| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **enabled** | `boolean` | optional (default: `true`) | |
| **size** | `integer` | optional (default: `1000`) | |
| **flushIntervalMs** | `integer` | optional (default: `1000`) | Flush interval in milliseconds |
| **flushInterval** | `never` | optional | [REMOVED] `LoggingConfig.buffer.flushInterval` was renamed to `flushIntervalMs` in @objectstack/spec 17 — the unit of a duration-shaped number lives in the key name, not only in the describe prose. Its unit (milliseconds) lived in a source JSDoc only and the key carried no `.describe()` at all, so the reference page published a bare 1000. Rename the key to `flushIntervalMs`; the value (milliseconds) and the 1000 default are unchanged. This is the in-process log buffer — `HttpDestinationConfig.batch.flushInterval` is a different key with its own rename. |
| **flushOnShutdown** | `boolean` | optional (default: `true`) | |


---

Expand Down
2 changes: 1 addition & 1 deletion packages/spec/authorable-defaults/system.json
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@
"system/FileDestinationConfig:append = true",
"system/FileDestinationConfig:encoding = \"utf8\"",
"system/HttpDestinationConfig:method = \"POST\"",
"system/HttpDestinationConfig:timeout = 30000",
"system/HttpDestinationConfig:timeoutMs = 30000",
"system/Job:enabled = true",
"system/KeyRotationPolicy:autoRotate = true",
"system/KeyRotationPolicy:enabled = false",
Expand Down
3 changes: 2 additions & 1 deletion packages/spec/authorable-surface/system.json
Original file line number Diff line number Diff line change
Expand Up @@ -460,7 +460,8 @@
"system/HttpDestinationConfig:headers",
"system/HttpDestinationConfig:method",
"system/HttpDestinationConfig:retry",
"system/HttpDestinationConfig:timeout",
"system/HttpDestinationConfig:timeout [RETIRED]",
"system/HttpDestinationConfig:timeoutMs",
"system/HttpDestinationConfig:url",
"system/IntervalSchedule:intervalMs",
"system/IntervalSchedule:type",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.

// #15939 ruling A (per-file remediation of #14478). `batch.flushInterval` said
// "Flush interval in milliseconds" in a source JSDoc and carried no
// `.describe()` at all, so the reference page published a bare 5000. Renamed to
// `flushIntervalMs` — the family's own spelling, 272 key-position `*Ms:`
// declarations in `packages/spec/src` and `flushIntervalMs` already declared on
// `kernel/events/integrations.zod.ts`. The value and the 5000 default are
// unchanged. Tombstoned with `retiredKey()`: the nested `batch` object is not
// strict, so a bare deletion would silently strip the key. ⚠️ Not the same key
// as `system/LoggingConfig:buffer.flushInterval`, which defaults to 1000 and
// has its own row. No D2 conversion: no logging collection on `stack.zod.ts`,
// not a stored row. See `logging-durations-unit-in-key`.
export const entry = 'system/HttpDestinationConfig:batch.flushInterval';
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.

// #15939 ruling A (per-file remediation of #14478). `retry.initialDelay` said
// "Initial retry delay in milliseconds" in a source JSDoc and carried no
// `.describe()` at all, so the reference page published a bare 1000. Renamed to
// `initialDelayMs` — already attested as a key-position declaration 5 times on
// this tree. The value and the 1000 default are unchanged. Tombstoned with
// `retiredKey()`: the nested `retry` object is not strict, so a bare deletion
// would silently strip the key. No D2 conversion: no logging collection on
// `stack.zod.ts`, not a stored row. See `logging-durations-unit-in-key`.
export const entry = 'system/HttpDestinationConfig:retry.initialDelay';
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.

// #15939 ruling A (per-file remediation of #14478). `timeout` said "Timeout in
// milliseconds" in a source JSDoc and carried no `.describe()` at all, so the
// reference page published a bare 30000. Renamed to `timeoutMs` — already
// attested as a key-position declaration 30 times on this tree. The value and
// the 30000 default are unchanged. Tombstoned with `retiredKey()`:
// `HttpDestinationConfig` is not strict, so a bare deletion would silently
// strip the key. ⚠️ The one TOP-LEVEL key of this card's four, so this is the
// one whose `authorable-surface/` and `authorable-defaults/` rows move — that
// ratchet records `schema.properties` one level deep. No D2 conversion: no
// logging collection on `stack.zod.ts`, not a stored row. See
// `logging-durations-unit-in-key`.
export const entry = 'system/HttpDestinationConfig:timeout';
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.

// #15939 ruling A (per-file remediation of #14478). `buffer.flushInterval` said
// "Flush interval in milliseconds" in a source JSDoc and carried no
// `.describe()` at all, so the reference page published a bare 1000. Renamed to
// `flushIntervalMs`. The value and the 1000 default are unchanged. Tombstoned
// with `retiredKey()`: the nested `buffer` object is not strict, so a bare
// deletion would silently strip the key. ⚠️ Not the same key as
// `system/HttpDestinationConfig:batch.flushInterval`, which defaults to 5000
// and has its own row — the two spellings were identical and the defaults never
// were. No D2 conversion: no logging collection on `stack.zod.ts`, not a stored
// row. See `logging-durations-unit-in-key`.
export const entry = 'system/LoggingConfig:buffer.flushInterval';
Loading
Loading