Skip to content

Commit b92b550

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-15779-engine-httpstatus
2 parents 77bcc41 + 658262e commit b92b550

348 files changed

Lines changed: 11248 additions & 829 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
---
2+
"@objectstack/spec": minor
3+
"@objectstack/runtime": patch
4+
---
5+
6+
feat(spec)!: the twelve `api/` duration keys carry their unit in the key name (#15677, ruling B on #14478)
7+
8+
<!-- adr-0087: registered api-endpoint-cache-ttl-to-cache-ttl-seconds, api-error-retry-after-unit-in-key, api-runtime-config-durations-unit-in-key, device-request-response-interval-unit-in-key, rest-api-plugin-durations-unit-in-key, websocket-durations-unit-in-key -->
9+
10+
**BREAKING** — twelve published `api/` duration keys are renamed and tombstoned.
11+
Shipped as `minor` under the repo's launch-window convention for breaking
12+
changes; the hand-migration prescriptions are registered under protocol major
13+
18. Maintainer ruling B on #14478 (2026-09-02, decision batch #43, 「同意」).
14+
15+
`check:duration-unit-keys` makes a duration-shaped `z.number()` carry its unit
16+
in the key NAME, never only in its `.describe()` prose, and grandfathers no
17+
existing offender. Stack card 1/6 (#15676) landed the rule's two structural
18+
exemptions; this card clears the `api/` directory against it. Measured with the
19+
gate itself: `src/api/**` goes from 12 offenders to **0**, and the whole-tree
20+
count falls **48 → 36**.
21+
22+
## FROM → TO
23+
24+
| key | replacement | unit |
25+
|:--|:--|:--|
26+
| `ApiEndpoint.cacheTtl` | `cacheTtlSeconds` | seconds |
27+
| `DataLoaderConfig.cacheTtl` | `cacheTtlSeconds` | seconds |
28+
| `DeviceRequestResponse.interval` | `intervalSeconds` | seconds |
29+
| `EnhancedApiError.retryAfter` | `retryAfterSeconds` | seconds |
30+
| `RestApiEndpoint.timeout` | `timeoutMs` | milliseconds |
31+
| `RestApiEndpoint.cacheTtl` | `cacheTtlSeconds` | seconds |
32+
| `RestApiPluginConfig.performance.defaultCacheTtl` | `defaultCacheTtlSeconds` | seconds |
33+
| `RouteDefinition.timeout` | `timeoutMs` | milliseconds |
34+
| `WebSocketConfig.reconnectInterval` | `reconnectIntervalMs` | milliseconds |
35+
| `WebSocketConfig.pingInterval` | `pingIntervalMs` | milliseconds |
36+
| `WebSocketConfig.timeout` | `timeoutMs` | milliseconds |
37+
| `WebSocketServerConfig.heartbeatInterval` | `heartbeatIntervalMs` | milliseconds |
38+
39+
**Every value is unchanged** — only key names move. Every old spelling is a
40+
`retiredKey()` tombstone, so it fails `tsc` at the authoring site (input type
41+
`never`) and fails the parse with the rename prescription rather than a bare
42+
unrecognized-key error.
43+
44+
## ⚠️ `ApiError.retryAfter` — the wire envelope, and what it does NOT touch
45+
46+
Ruling B put this key explicitly in scope with its own BREAKING note: the
47+
runtime-emitted measurements are read by humans and agents even though nobody
48+
authors them. A consumer meets two retry-after values on one 429 — this
49+
ADR-0112 envelope field, always delta-seconds, and the HTTP `Retry-After`
50+
header, which per RFC 9110 §10.2.3 may carry delta-seconds **or** an HTTP-date.
51+
Spelled identically they read as one value in two places.
52+
53+
**The HTTP `Retry-After` response header is a separate, unchanged surface.** Its
54+
name is fixed outside this repo and nothing here touches it. Do not "fix" the
55+
header to match the envelope, and do not read a surviving `retry-after` in
56+
transport code as leftover work.
57+
58+
## Dispositions — one D2 conversion, five semantic entries
59+
60+
Justified per key rather than defaulted. **`ApiEndpoint.cacheTtl` is the only
61+
one of the twelve that gets an ADR-0087 D2 conversion**
62+
(`api-endpoint-cache-ttl-to-cache-ttl-seconds`), because `apis:` is a stack
63+
collection (`apis: z.array(ApiEndpointSchema)`) and `api` is a registered
64+
metadata kind stored as a row, so the conversion chain has a seam that sees it.
65+
`os migrate meta --from 17` lists the mechanical edits.
66+
67+
The other eleven are wire payloads and construction arguments — a device-flow
68+
response body, an error envelope, REST-plugin route registration, a batch-loader
69+
config, a router registration, WebSocket client/server configuration. None is
70+
ever a stack collection member or a `sys_metadata` row, so no conversion seam
71+
runs on them and each carries a **semantic** entry instead: this is the
72+
disposition `api/RestApiEndpoint:handlerStatus` already holds on one of these
73+
very shapes, and what ruling B prescribes for a runtime-emitted key.
74+
75+
## `DeviceRequestResponse.interval` is a rename, not an external-vocabulary mirror
76+
77+
Attributed to RFC 8628 by the campaign card; the attribution fails against the
78+
schema's own evidence. `DeviceRequestResponseSchema` does not mirror RFC 8628 as
79+
a set — `code` is not `device_code`, `verificationUrl` is not
80+
`verification_uri`, `expiresAt` is not `expires_in` (a different name *and* a
81+
different type, an ISO-8601 instant where the RFC carries a relative lifetime).
82+
A schema that already renames every RFC field it carries into house style cannot
83+
claim the standard fixes the one name it left bare. Renamed rather than marked
84+
deliberately: a wrongly marked key is exempted permanently and silently, while a
85+
wrongly renamed one is visible.
86+
87+
## Readers moved in the same PR, at the same magnitude
88+
89+
`@objectstack/runtime`'s policy chain (`computeCacheControl` now reads
90+
`endpoint.cacheTtlSeconds`), the publish gate's issue path
91+
(`apis.N.cacheTtlSeconds`), the built-in REST route tables, the showcase
92+
example, dogfood fixtures, `liveness/api.json` (renamed row plus a `dead`
93+
tombstone row) and the `objectstack-api` skill. The `ApiEndpoint` alias table is
94+
retargeted onto the live key — an alias must point at a key the schema really
95+
accepts, and `cacheTtl` now accepts nothing.
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
"@objectstack/cli": patch
3+
---
4+
5+
fix(cli): `explain` names the renamed `dashboard.refreshIntervalSeconds` (#14478)
6+
7+
The dashboard key catalogue `os explain` prints lists
8+
`refreshIntervalSeconds` instead of `refreshInterval`, following the
9+
`@objectstack/spec` rename of the authored key (the unit now lives in the key
10+
name). Same key, same seconds; no other command output and no public surface of
11+
this package changes.
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
---
2+
"@objectstack/spec": minor
3+
---
4+
5+
feat(spec)!: the last seven `data/` · `ui/` · `ai/` · `integration/` duration keys carry their unit in the key name (#15680, ruling B on #14478)
6+
7+
<!-- adr-0087: registered dashboard-refresh-interval-to-refresh-interval-seconds, connector-health-and-trigger-durations-unit-in-key, memory-persistence-auto-save-interval-to-ms, turso-config-timeout-to-timeout-ms, ai-conversation-analytics-duration-unit-in-key, data-nosql-query-options-timeout-unit-in-key -->
8+
9+
**BREAKING** — eight published duration keys are renamed and tombstoned. Shipped
10+
as `minor` under the repo's launch-window convention for breaking changes; the
11+
hand-migration prescriptions are registered under protocol major 18. Maintainer
12+
ruling B on #14478 (2026-09-02, decision batch #43, 「同意」).
13+
14+
`check:duration-unit-keys` makes a duration-shaped `z.number()` carry its unit in
15+
the key NAME, never only in its `.describe()` prose, and grandfathers no existing
16+
offender. Card 1/6 (#15676) landed the rule's two structural exemptions, card 2/6
17+
(#15677) cleared `api/`, card 3/6 (#15678) cleared `kernel/` and card 4/6
18+
(#15679) cleared `system/`. This card clears the remainder, and is the first
19+
where the gate itself reads **`zero offenders`** and exits `0`.
20+
21+
⚠️ That is green **for the gate's currently declared population**
22+
(`packages/spec/src/**`), not for the epic. Card 6/6 widens the population and has
23+
already measured an offender outside this subtree, so the gate is expected to go
24+
red again by design. This changeset does not claim #14478 is finished.
25+
26+
## FROM → TO
27+
28+
| key | replacement | unit |
29+
|:--|:--|:--|
30+
| `dashboard.refreshInterval` | `refreshIntervalSeconds` | seconds |
31+
| `CircuitBreakerConfig.monitoringWindow` | `monitoringWindowMs` | milliseconds |
32+
| `ConnectorTrigger.interval` | `intervalSeconds` | seconds |
33+
| `FilePersistenceConfig.autoSaveInterval` | `autoSaveIntervalMs` | milliseconds |
34+
| `AutoPersistenceConfig.autoSaveInterval` | `autoSaveIntervalMs` | milliseconds |
35+
| `TursoConfig.timeout` | `timeoutMs` | milliseconds |
36+
| `NoSQLQueryOptions.timeout` | `timeoutMs` | milliseconds |
37+
| `ConversationAnalytics.duration` | `durationSeconds` | seconds |
38+
39+
**Every value is unchanged** — only key names move. The two keys that carried a
40+
default keep it (`CircuitBreakerConfig.monitoringWindowMs` still defaults to
41+
60000, `FilePersistenceConfig.autoSaveIntervalMs` to 2000); the other six declare
42+
none. Bounds move with their keys, so `autoSaveIntervalMs` still refuses anything
43+
under 100 on both persistence arms, `NoSQLQueryOptions.timeoutMs` and
44+
`TursoConfig.timeoutMs` still refuse a zero or negative integer, and
45+
`ConversationAnalytics.durationSeconds` still refuses a negative length. Every old
46+
spelling is a `retiredKey()` tombstone, so it fails `tsc` at the authoring site
47+
(input type `never`) and fails the parse with the rename prescription rather than
48+
a bare unrecognized-key error.
49+
50+
`dashboard`'s three rename-hint aliases — `refresh`, `autoRefresh`, `pollInterval`
51+
— were repointed to `refreshIntervalSeconds` in the same edit. A hint left naming
52+
the tombstone would have prescribed a key the shape refuses, which is the one
53+
failure this rename could have introduced silently; a pin asserts all three.
54+
55+
## ⚠️ `dashboard.refreshInterval` crosses a repository boundary
56+
57+
This is the only rename in the whole stack whose consumer is in **another
58+
repository**, so its reader could not move in this PR the way every other reader
59+
in this card did. objectui's dashboard renderer reads the key, multiplies by
60+
1000 to drive a `setInterval`, and republishes it as an authoring input the
61+
console offers. Those sites move in a follow-up objectui card, sequenced behind
62+
a release that actually ships this rename.
63+
64+
Until that lands the renderer sees an absent key and simply does not start its
65+
refresh timer — a dashboard still renders, and still refreshes when the user
66+
asks. The ADR-0087 conversion in this changeset is what keeps stored dashboards
67+
and `os migrate meta` correct in the meantime.
68+
69+
## ⚠️ An eighth key moves that the gate did not list
70+
71+
`AutoPersistenceConfig.autoSaveInterval` is not a gate offender: its `.describe()`
72+
named no unit at all, and the predicate judges prose against name.
73+
74+
It moves anyway because it is not a second key. `persistence: { type: 'auto' }`
75+
resolves to the same Node.js file adapter as `type: 'file'`, and this value is
76+
forwarded to the same `FileSystemPersistenceAdapter` field, in the same
77+
milliseconds, under the same `min(100)` bound. Renaming one arm and not the other
78+
would have left one value with two spellings across sibling arms of one union,
79+
and the driver reading both — the consumer-side dialect Prime Directive #12
80+
forbids. Its describe now names the unit too, and a pin asserts the refusal on
81+
the arm the gate never listed, so a later reader cannot "restore" the bare
82+
spelling as an over-application of the rule.
83+
84+
## Dispositions — four D2 conversions, two semantic entries
85+
86+
Judged per key from `stack.zod.ts`'s collection roots rather than defaulted, and
87+
unlike card 4/6 this card's answer is split.
88+
89+
**D2 conversions** (six keys). `dashboards:`, `connectors:` and `datasources:`
90+
are each a stack collection whose members are stored whole as `sys_metadata`
91+
rows, so the conversion chain has a seam that sees them:
92+
`dashboard-refresh-interval-to-refresh-interval-seconds`,
93+
`connector-health-and-trigger-durations-unit-in-key` (both connector keys in one
94+
pass, emitting separately),
95+
`memory-persistence-auto-save-interval-to-ms` (both persistence arms) and
96+
`turso-config-timeout-to-timeout-ms`. The two datasource conversions are
97+
driver-aware for the reason `datasource-config-driver-key-aliases` records: a
98+
bare `config.timeout` under another driver is that driver's own key and must not
99+
be touched.
100+
101+
**Semantic entries** (two keys). `ConversationAnalytics` is computed at runtime
102+
and handed to a consumer, and `NoSQLQueryOptions` is a per-call driver argument
103+
reached only through `AggregationPipeline.options`. Neither is a stack collection
104+
member or a stored row, so the chain has no seam — the disposition every
105+
runtime-emitted measurement in this stack has taken.
106+
107+
All eight are registered by exact key in `RETIRED_KEYS_BY_MAJOR`.
108+
109+
## A retirement tombstone is no longer read as a secret
110+
111+
`refusedCredentialKeys` derives a driver's refused inline credentials by finding
112+
`z.never()` keys in its config contract. A `retiredKey()` tombstone is also a
113+
`z.never()`, and until this card no driver contract carried one — so "never ⇒
114+
credential" held by accident of population rather than by construction. The first
115+
tombstone to arrive (`TursoConfig.timeout`) made the derivation answer that a
116+
millisecond budget was a secret: it was redacted off the datasource read path and
117+
dragged a non-credential name into the fallback list every unrecognised driver is
118+
scrubbed by.
119+
120+
The derivation now skips keys carrying the `[REMOVED] ` prefix `retiredKey()`
121+
itself stamps. The exclusion is deliberately **negative** — skip declared
122+
tombstones — rather than positive (keep only keys marked `format: 'password'`),
123+
even though every credential slot in every builtin contract does carry that
124+
marker today: under-redacting is the dangerous direction, so a future credential
125+
key whose author forgets the marker is still scrubbed, and only a key that has
126+
explicitly declared itself retired may drop out. Both directions are pinned.
127+
128+
## Keys deliberately left alone
129+
130+
`TursoConfig.sync.intervalSeconds` and `CircuitBreakerConfig.resetTimeoutMs`
131+
already carried their unit — they are the same-shape neighbours that made the
132+
bare `timeout` and `monitoringWindow` collisions visible, and pins assert they
133+
did not move. `NoSQLQueryOptions.batchSize` is a COUNT of documents and every
134+
number on `ConversationAnalytics` other than the duration is a count of messages,
135+
tokens or events: a count has no unit to carry. The turso schema shipped by
136+
`@objectstack/driver-turso` is a separate declaration outside this gate's
137+
declared population and is not touched here; card 6/6 owns it, so the two
138+
declarations disagree by design until that lands.
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
---
2+
"@objectstack/driver-memory": minor
3+
---
4+
5+
feat(driver-memory)!: the file-persistence auto-save interval names its unit (#15680, ruling B on #14478)
6+
7+
<!-- adr-0087: registered memory-persistence-auto-save-interval-to-ms -->
8+
9+
**BREAKING**`InMemoryDriverOptions.persistence.autoSaveInterval` and
10+
`FileSystemPersistenceAdapter`'s `autoSaveInterval` constructor option are both
11+
renamed to **`autoSaveIntervalMs`**, following the `@objectstack/spec` rename of
12+
the authored keys on both persistence arms.
13+
14+
Same value, same milliseconds, same 2000 default, same `setInterval` cadence. The
15+
option was always milliseconds — it is passed straight to `setInterval` — and the
16+
spec's `min(100)` bound is what made the bare name dangerous rather than untidy:
17+
100 reads as a plausible number of seconds, so an author who guessed the unit
18+
wrong cleared the bound, was refused nowhere, and saved a thousand times more
19+
often than intended.
20+
21+
Both persistence arms move together: `type: 'auto'` resolves to this same file
22+
adapter and forwards the same field, so this package reads exactly one spelling
23+
rather than two.
24+
25+
```diff
26+
- new InMemoryDriver({ persistence: { type: 'file', autoSaveInterval: 5000 } })
27+
+ new InMemoryDriver({ persistence: { type: 'file', autoSaveIntervalMs: 5000 } })
28+
```
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
---
2+
"@objectstack/driver-turso": minor
3+
---
4+
5+
feat(driver-turso)!: the published connection config names its timeout's unit (#15682, ruling B on #14478)
6+
7+
<!-- adr-0087: registered turso-config-timeout-to-timeout-ms -->
8+
<!-- The protocol-18 conversion above is registered by card 5/6 of the #14478 stack; relative to
9+
`main` this combined diff is what adds it, so `registered` is the honest disposition at this
10+
level even though `already-registered` was honest per-card. -->
11+
12+
**BREAKING**`TursoConfigSchema`'s `timeout` is renamed to **`timeoutMs`**. The
13+
value is unchanged: the same milliseconds, the same `min(0)` bound, the same
14+
optionality.
15+
16+
`@objectstack/spec`'s own turso contract renamed the same authored key in
17+
#15680. This package publishes a parallel schema for the same connection config
18+
— the Spec / Studio metadata a host reads to expose Turso configuration UI — so
19+
until now the two declarations of one setting disagreed on its spelling. They
20+
agree again.
21+
22+
The unit was never in the key name, only in the describe prose, while
23+
`sync.intervalSeconds` — the same shape, three keys above — already spelled its
24+
own. One published config carrying both conventions is what made the bare name
25+
dangerous rather than untidy: an author who has just written
26+
`intervalSeconds: 30` has no reason to read `timeout: 30` as milliseconds, and
27+
nothing in the schema, the type or the parse would have told them otherwise.
28+
29+
The old spelling is not dropped in silence. `TursoConfigSchema` is a plain
30+
`z.object`, so a bare deletion would have STRIPPED `timeout` and parsed
31+
successfully. The key stays declared as a tombstone instead: `tsc` refuses it on
32+
anything typed `TursoConfig`, and a value that reaches the parse raises a
33+
message naming `timeoutMs` rather than a generic unrecognised-key error.
34+
35+
```diff
36+
- TursoConfigSchema.parse({ url: 'libsql://app.turso.io', timeout: 30000 })
37+
+ TursoConfigSchema.parse({ url: 'libsql://app.turso.io', timeoutMs: 30000 })
38+
```
39+
40+
`TursoDriverConfig` — this package's TypeScript constructor option, a separate
41+
declaration — keeps its `timeout` spelling and is untouched here.

0 commit comments

Comments
 (0)