Skip to content

Commit e9fcd6b

Browse files
claude[bot]os-salesclaude
authored
feat(spec)!: duration-shaped number keys carry their unit in the key name — no-baseline gate + seven ADR-0087 renames (timeoutMs, ttlSeconds/ttlMs, *TimeoutSeconds) (#15626)
* wip(spec): duration-unit-keys gate draft (#14478) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4138K1EG7kQ81FNba5Kp4 * wip(spec): rename the seven duration keys, conversions, readers, pins (#14478) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4138K1EG7kQ81FNba5Kp4 * wip(spec): regenerate artifacts, i18n bundles, remaining fixtures (#14478) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4138K1EG7kQ81FNba5Kp4 * wip(spec): strip issue ids from tombstones, declare the gate population, regen docs (#14478) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4138K1EG7kQ81FNba5Kp4 * fix(driver-turso): the DriverOptions door pin writes timeoutMs (#14478) `turso-driver-options-door.test.ts` builds a `Parameters<TursoDriver['update']>[3]` literal — that argument IS `DriverOptions`, so the `timeout` key it wrote is the one renamed to `timeoutMs` here, not a driver-local key. Same magnitude (milliseconds), no value conversion. `TursoDriverConfig.timeout` in `turso-driver.ts` and the `timeout` in `src/spec/turso.zod.ts` are a different key on the driver's own connection schema and stay as they are. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4138K1EG7kQ81FNba5Kp4 * chore(spec): regenerate liveness state counts on the merged tree (#14478) `packages/spec/liveness/state-counts.md` is routed to `merge=os-regen`, so the merge of origin/main took it without a text merge and left it owing a regeneration. Regenerated with `pnpm --filter @objectstack/spec gen:liveness-counts` from the merged tree, which is the union of both sides: main's `field` row (89 live / 3 planned -> 90 live / 2 planned) lands on top of this branch's `hook` and `job` rows, and the totals follow. No hand edits: the file is generator output. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4138K1EG7kQ81FNba5Kp4 * feat(spec)!: declare the duration rule's two structural exemptions on the schema — a shared `EpochMs` instant and a `.meta({ externalVocabulary })` marker (#15814) * feat(spec)!: the twelve api/ duration keys carry their unit in the key name (#15677, stack 2/6) (#15837) * feat(spec)!: the fourteen kernel/ duration keys carry their unit in the key name (#15678, stack 3/6) (#15906) * feat(spec): declare the two duration-rule exemptions on the schema (#15676) Ruling B on #14478 exempts two structural classes from the duration-unit rule, and is explicit that both are declared ON THE SCHEMA, never in a gate ledger. This commit lands the declaration channels themselves: - `EpochMs` (`packages/spec/src/shared/epoch.zod.ts`) — the shared epoch-milliseconds instant. A key whose value IS this schema is an instant, not a duration, and `check:duration-unit-keys` recognises that structurally. - `.meta({ externalVocabulary: '<the standard>' })` — the marker a key carries when it mirrors a name fixed outside this repo. It rides `z.toJSONSchema` verbatim, the same channel `xRef` / `xExpression` already use. Neither exemption is a pass on lying: a marked key still fails `name-unit-contradicts-prose`, and an `EpochMs` key whose describe names a unit other than milliseconds fails the new `instant-unit-contradicts-schema`. Both classes stay visible in the census — `--list` marks them and the verdict line counts them. The gate also now reads `description` out of `.meta()`. Without it, moving a describe into `.meta({ description })` would take a key out of the population silently — an exemption by blindness. Measured: one numeric key declares its description that way today (`data/Field.precision`), naming no time unit, so the reading adds no offender. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4138K1EG7kQ81FNba5Kp4 * feat(spec)!: move the six epoch instants onto EpochMs and mark the external-vocabulary keys (#15676) The two exemption classes ruling B declares, applied to the keys the gate lists. Instants (exemption i) — all six now typed `EpochMs`; the four whose name was bare are renamed to the `*At` instant convention, tombstoned with `retiredKey()` and registered in `RETIRED_KEYS_BY_MAJOR[18]` plus one D3 semantic entry: api/WebSocketEvent.timestamp -> occurredAt api/SimplePresenceState.lastSeen -> lastSeenAt kernel/KernelContext.startTime -> startedAt (+ TenantRuntimeContext) kernel/HealthStatus.timestamp -> checkedAt kernel/ServiceMetadata.registeredAt (already `*At`, schema only) kernel/ScopeInfo.createdAt (already `*At`, schema only) `*At` and not `*Ms`, measured rather than chosen: on this package's own authorable surface all 51 distinct `*Ms` keys are durations and all 51 distinct `*At` keys are instants, so spelling an instant `*Ms` would move it into the family the rule exists to separate it from. Semantic entries rather than D2 conversions because all four are runtime-emitted — wire payloads, a host-constructed kernel context, an emitted health report — so no conversion seam ever sees one. That is the disposition `kernel/KernelContext:previewMode` already carries on one of these defs, and what ruling B prescribes for a runtime-emitted key. External-standard mirrors (exemption ii) — eleven keys marked, not thirteen. Two of the thirteen the card attributed do not survive verification against their own schema and are left for their directory cards; the PR body records the evidence. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4138K1EG7kQ81FNba5Kp4 * feat(spec): publish the externalVocabulary standard on the reference page (#15676) The published half of exemption (ii). A marked key keeps its bare name BECAUSE an external standard fixes it, and that argument only reaches the reference-page reader if the page names the standard — so the description cell now carries "(unit per <the standard>)". Without it the exemption would leave exactly the reader `check:duration-unit-keys` was filed for where the gate found them. Also: `EpochMs` gains its type alias (the docs import-surface ratchet demands one for every documented schema) and its ADR-0122 isomorphism pin. Regenerated: json-schema.manifest/, authorable-surface/, api-surface/, export-origins/, declaration-map/, content/docs/references/**. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4138K1EG7kQ81FNba5Kp4 * docs(changeset): the two duration-rule exemptions (#15676) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4138K1EG7kQ81FNba5Kp4 * chore(spec): regenerate the derived artifacts and fix the consumers the tombstones caught (#15676) - `src/contracts/startup-orchestrator.test.ts` built a `HealthStatus` with the old `timestamp` key. The `retiredKey()` tombstone refused it at compile time (`Type 'number' is not assignable to type 'undefined'`, 7 errors) — the audible-removal property the tombstone exists for, working on the first consumer it met. - `type-alias-convention.pin.test.ts`: the pin count assertion follows the new `Iso868`. - Regenerated: export-origins/, declaration-map/, api-surface/ and the `objectstack-api` skill reference index (one generated line, naming the new `shared/epoch.zod.ts` module). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4138K1EG7kQ81FNba5Kp4 * wip(spec): rename the 12 api/ duration keys, tombstones on the old spellings (#15677) The schema half of stack card 2/6. Gate reads 48 -> 36 with src/api/ at 0. Readers, registry entries and regenerated artifacts follow. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4138K1EG7kQ81FNba5Kp4 * wip(spec): readers, ADR-0087 registrations, ledger row for the api/ renames (#15677) 12 retired-key entries, one D2 conversion (api-endpoint-cache-ttl-to-cache-ttl-seconds — apis: is a stack collection) and five semantic entries for the eleven runtime-emitted / construction-argument keys. Readers moved in runtime, metadata, rest-adjacent tests, dogfood fixtures and the showcase example; liveness/api.json carries the renamed row plus the dead tombstone row. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4138K1EG7kQ81FNba5Kp4 * wip(spec): tombstone refusal tests, alias retarget, regenerated artifacts (#15677) Per-key refusal tests assert the prescription (code + rename text), not a bare throw. Two readers the key-name grep missed and tsc/the tombstones caught: the ApiEndpoint alias table (cacheTTL/ttl/cache retargeted onto cacheTtlSeconds — an alias must point at a key the schema accepts) and the showcase endpoint fixture in metadata-type-api-registration.test.ts. Regenerated: authorable surface + defaults, reference docs, liveness state-counts. skills/objectstack-api/SKILL.md carries the rename (governed; net 0 lines, file and package both). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4138K1EG7kQ81FNba5Kp4 * docs(changeset): the twelve api/ duration renames (#15677) @objectstack/spec minor with the BREAKING banner naming every renamed key, the six adr-0087 ids registered, the retryAfter wire note, and the disposition split (one D2 conversion, five semantic entries). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4138K1EG7kQ81FNba5Kp4 * fix(docs-audit): declare the conversion-replay exclusion kind (b) relied on incidentally (#15677) An ADR-0087 conversion fixture copies a routable metadata kind verbatim, so an `apis:` fixture carries `method:` beside `path:` — because that is what an ApiEndpoint IS. Ruling A named conversions/registry.ts as the guard's target but enforced it with requireMethodSignal, a content proxy that held only while no conversion fixture carried a verb. This card's apis: conversion is the first that does, and the live pin red exactly as designed. The fixture is correct and stays. The exclusion moves to CONVERSION_REPLAY_FILE_RE, which states the structural fact instead of testing a symptom, and three cases pin the new guard as load-bearing rather than incidental in its turn. NOT restricting kind (b) to packages/spec/src/api/**: that is the invariant the live pin asserts, and enforcing it in the walk would make that pin true by construction — a check that cannot fail. Measured tail-neutral: the scan census is byte-identical to the base (17 route sources, 12 call sites, 5 contract declarations, 78 tails, 61 reachable). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4138K1EG7kQ81FNba5Kp4 * docs: move the hand-written pages onto the renamed keys, and strip the issue ids (#15677) Lap 1 regenerated content/docs/references/** but left the HAND-WRITTEN pages teaching the old spellings. Three of them carried `os:check` blocks authoring `cacheTtl`, so check:skill-examples was RED and lap 1 never ran it — it sits in check:generated's "not run here" list and I did not run it separately. cacheTtl -> cacheTtlSeconds: 14 occurrences on 13 lines, all the ApiEndpoint key. retryAfter -> retryAfterSeconds: 14 occurrences, the ADR-0112 envelope field only. Deliberately NOT swept, each verified rather than assumed: - the HTTP `Retry-After` response header (6 locals over 4 sites) — RFC 9110, a separate surface, and the thing the tombstone prose exists to protect; - `retry_after` as a RetryStrategy ENUM VALUE (errors.zod.ts z.enum); - `details.retry_after` on the wire, and the pre-existing `details.retryAfterSeconds` the runtime really emits (endpoint-policy.ts). Also strips `(#14478 ruling B)` from the twelve tombstone prescriptions THIS card wrote: check:doc-authoring forbids an internal issue id in customer-facing spec text (maintainer ruling 2026-08-12), and the campaign's own earlier tombstones already comply. The version and the FROM -> TO mapping stay — those are the durable references AGENTS.md requires. Measured: the gate read 4 findings on the base and 16 on my head; it now reads the base's 4 again, so this PR adds none. Those 4 are card 1/6's (PR #15814) and are not mine to touch. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4138K1EG7kQ81FNba5Kp4 * docs(spec): drop the internal issue id from the four epoch-instant tombstone prescriptions (#15676) `check:doc-authoring` rule 3 forbids an internal issue id in customer-facing spec text (maintainer ruling 2026-08-12). The four `retiredKey()` prescriptions this stack card added each opened with a `(#14478 ruling B)` parenthetical — a citation-shaped token that resolves to nothing for the author who meets it the moment their key is refused, and which the generated reference pages publish verbatim. The parenthetical goes; the sentence keeps everything actionable it carried — the FROM to TO rename, that the value is unchanged, and the `SimplePresenceState.lastSeen` neighbour caveat — matching the shape the campaign's already-compliant tombstones use (`hook.timeout`, `job.timeout`, `DriverOptions.timeout`). The internal anchor is untouched in the adjacent `//` and `/** */` comments, which are not customer-facing and were never findings. `content/docs/references/**` regenerated with `pnpm --filter @objectstack/spec gen:docs` — no generated artifact was hand-edited. check:doc-authoring: 4 findings before, exit 0 after. check:duration-unit-keys: unmoved — 48 offender(s) among 215 duration-shaped numeric key(s), (6 declared `EpochMs` instant(s), 11 declared `externalVocabulary` mirror(s)). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4138K1EG7kQ81FNba5Kp4 * chore(spec): regenerate the reference page the merge deferred (#15677) The merge with card 1/6's advanced tip (9605808) touched api/websocket.zod.ts on both sides. The schema source auto-merged; the generated content/docs/references/api/websocket.mdx is routed to merge=os-regen, so the driver deferred it and the merge kept OUR side — silently dropping card 1/6's half. Regenerating from the merged tree is what repairs it, and it carries both sides: their two stripped prescriptions land (issue-id occurrences 2 -> 0) while my four renamed keys stay (6 -> 6). Not hand-edited and not resolved by taking a side: the bytes come from `pnpm --filter @objectstack/spec check:generated --fix` on the merged tree, and the staged diff was read before committing (`git diff` reads clean over this trap; only the staged diff shows it). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4138K1EG7kQ81FNba5Kp4 * wip(spec): rename the 14 kernel/ duration keys, tombstones on the old spellings (#15678) The schema half of stack card 3/6. Gate reads 36 -> 22 with src/kernel/ at 0. Readers, registrations and regenerated artifacts follow. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4138K1EG7kQ81FNba5Kp4 * wip(spec): readers and ADR-0087 registrations for the kernel/ renames (#15678) 14 retired-key entries and five semantic entries. No D2 conversion on this card: none of the twelve defs is a stack collection member or a stored sys_metadata row (stack.zod.ts declares no eventBus / startup / plugin-security root), so the conversion chain has no seam that would see one. Readers moved in core's health monitor, the kernel and contracts test suites, and the hand-written lifecycle protocol page. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4138K1EG7kQ81FNba5Kp4 * wip(spec): tombstone refusal tests and regenerated reference pages (#15678) Per-key refusal tests assert the prescription (issue code + rename text), not a bare throw, plus acceptance pins at the same magnitudes and defaults. Two deliberate NEGATIVE controls: RuntimeConfig.resourceLimits.timeout names no unit so it stays bare, and EventSourcingConfig.snapshotRetention is a count. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4138K1EG7kQ81FNba5Kp4 * docs(changeset): the fourteen kernel/ duration renames (#15678) @objectstack/spec minor + @objectstack/core patch, BREAKING banner naming every renamed key, adr-0087: registered with the five semantic ids. Documents the two unit collisions the rename removes (responseTime hours vs ms; uptime ms vs the seconds-valued GET /health) and the three keys deliberately left bare. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4138K1EG7kQ81FNba5Kp4 * docs(changeset): correct why `RuntimeConfig.resourceLimits.timeout` is left bare (#15678) The changeset stated the key "names no unit anywhere in its prose". That is false and it would have published into the CHANGELOG: the JSDoc directly above the key (plugin-security-advanced.zod.ts:291-293) says "Execution timeout in milliseconds". Its `.describe('Maximum execution time')` names none. The accurate reason is stronger: `check:duration-unit-keys` reads `.describe()` and `.meta({ description })` and does NOT read JSDoc, so this key's unit sits in a channel the gate cannot see. The gate therefore lists it among the duration-shaped keys but deliberately does not judge it — neither an offender nor an exemption. That channel gap is tracked in #15939. The same false clause appeared in three more places on this branch and is corrected in all of them: the ADR-0087 semantic migration entry (its `reason` publishes into the generated upgrade guide), its generated mirror in registry.ts (via gen:migration-registry, never hand-edited), and the comment above the pin test. Prose only. No key renamed, no schema, `.describe()`, JSDoc, tombstone or pin assertion touched; the key stays bare and stays out of the rename. `check:duration-unit-keys` still reads 22 offender(s) among 215 duration-shaped numeric key(s) in 805 source file(s), `kernel/` at 0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4138K1EG7kQ81FNba5Kp4 * chore(spec): regenerate the os-regen artifacts on the merged tree Discharges the deferral the merge commit recorded. `check:generated` proved exactly one artifact set stale (content/docs/references/**) after the spec build re-derived authorable-surface/kernel.json, so only that one was regenerated. Both files re-derive this card's [RETIRED] rows on top of the trunk's side; the trunk's own renames (checkedAt, startedAt) survive in both. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4138K1EG7kQ81FNba5Kp4 --------- Co-authored-by: Claude <noreply@anthropic.com> * feat(spec)!: the fifteen system/ duration keys carry their unit in the key name [#14478 stack 4/6] (#15938) * feat(spec): widen check:duration-unit-keys to every workspace package's zod schemas, and convert the one turso offender it finds (#16022) * feat(spec): declare the two duration-rule exemptions on the schema (#15676) Ruling B on #14478 exempts two structural classes from the duration-unit rule, and is explicit that both are declared ON THE SCHEMA, never in a gate ledger. This commit lands the declaration channels themselves: - `EpochMs` (`packages/spec/src/shared/epoch.zod.ts`) — the shared epoch-milliseconds instant. A key whose value IS this schema is an instant, not a duration, and `check:duration-unit-keys` recognises that structurally. - `.meta({ externalVocabulary: '<the standard>' })` — the marker a key carries when it mirrors a name fixed outside this repo. It rides `z.toJSONSchema` verbatim, the same channel `xRef` / `xExpression` already use. Neither exemption is a pass on lying: a marked key still fails `name-unit-contradicts-prose`, and an `EpochMs` key whose describe names a unit other than milliseconds fails the new `instant-unit-contradicts-schema`. Both classes stay visible in the census — `--list` marks them and the verdict line counts them. The gate also now reads `description` out of `.meta()`. Without it, moving a describe into `.meta({ description })` would take a key out of the population silently — an exemption by blindness. Measured: one numeric key declares its description that way today (`data/Field.precision`), naming no time unit, so the reading adds no offender. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4138K1EG7kQ81FNba5Kp4 * feat(spec)!: move the six epoch instants onto EpochMs and mark the external-vocabulary keys (#15676) The two exemption classes ruling B declares, applied to the keys the gate lists. Instants (exemption i) — all six now typed `EpochMs`; the four whose name was bare are renamed to the `*At` instant convention, tombstoned with `retiredKey()` and registered in `RETIRED_KEYS_BY_MAJOR[18]` plus one D3 semantic entry: api/WebSocketEvent.timestamp -> occurredAt api/SimplePresenceState.lastSeen -> lastSeenAt kernel/KernelContext.startTime -> startedAt (+ TenantRuntimeContext) kernel/HealthStatus.timestamp -> checkedAt kernel/ServiceMetadata.registeredAt (already `*At`, schema only) kernel/ScopeInfo.createdAt (already `*At`, schema only) `*At` and not `*Ms`, measured rather than chosen: on this package's own authorable surface all 51 distinct `*Ms` keys are durations and all 51 distinct `*At` keys are instants, so spelling an instant `*Ms` would move it into the family the rule exists to separate it from. Semantic entries rather than D2 conversions because all four are runtime-emitted — wire payloads, a host-constructed kernel context, an emitted health report — so no conversion seam ever sees one. That is the disposition `kernel/KernelContext:previewMode` already carries on one of these defs, and what ruling B prescribes for a runtime-emitted key. External-standard mirrors (exemption ii) — eleven keys marked, not thirteen. Two of the thirteen the card attributed do not survive verification against their own schema and are left for their directory cards; the PR body records the evidence. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4138K1EG7kQ81FNba5Kp4 * feat(spec): publish the externalVocabulary standard on the reference page (#15676) The published half of exemption (ii). A marked key keeps its bare name BECAUSE an external standard fixes it, and that argument only reaches the reference-page reader if the page names the standard — so the description cell now carries "(unit per <the standard>)". Without it the exemption would leave exactly the reader `check:duration-unit-keys` was filed for where the gate found them. Also: `EpochMs` gains its type alias (the docs import-surface ratchet demands one for every documented schema) and its ADR-0122 isomorphism pin. Regenerated: json-schema.manifest/, authorable-surface/, api-surface/, export-origins/, declaration-map/, content/docs/references/**. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4138K1EG7kQ81FNba5Kp4 * docs(changeset): the two duration-rule exemptions (#15676) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4138K1EG7kQ81FNba5Kp4 * chore(spec): regenerate the derived artifacts and fix the consumers the tombstones caught (#15676) - `src/contracts/startup-orchestrator.test.ts` built a `HealthStatus` with the old `timestamp` key. The `retiredKey()` tombstone refused it at compile time (`Type 'number' is not assignable to type 'undefined'`, 7 errors) — the audible-removal property the tombstone exists for, working on the first consumer it met. - `type-alias-convention.pin.test.ts`: the pin count assertion follows the new `Iso868`. - Regenerated: export-origins/, declaration-map/, api-surface/ and the `objectstack-api` skill reference index (one generated line, naming the new `shared/epoch.zod.ts` module). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4138K1EG7kQ81FNba5Kp4 * wip(spec): rename the 12 api/ duration keys, tombstones on the old spellings (#15677) The schema half of stack card 2/6. Gate reads 48 -> 36 with src/api/ at 0. Readers, registry entries and regenerated artifacts follow. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4138K1EG7kQ81FNba5Kp4 * wip(spec): readers, ADR-0087 registrations, ledger row for the api/ renames (#15677) 12 retired-key entries, one D2 conversion (api-endpoint-cache-ttl-to-cache-ttl-seconds — apis: is a stack collection) and five semantic entries for the eleven runtime-emitted / construction-argument keys. Readers moved in runtime, metadata, rest-adjacent tests, dogfood fixtures and the showcase example; liveness/api.json carries the renamed row plus the dead tombstone row. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4138K1EG7kQ81FNba5Kp4 * wip(spec): tombstone refusal tests, alias retarget, regenerated artifacts (#15677) Per-key refusal tests assert the prescription (code + rename text), not a bare throw. Two readers the key-name grep missed and tsc/the tombstones caught: the ApiEndpoint alias table (cacheTTL/ttl/cache retargeted onto cacheTtlSeconds — an alias must point at a key the schema accepts) and the showcase endpoint fixture in metadata-type-api-registration.test.ts. Regenerated: authorable surface + defaults, reference docs, liveness state-counts. skills/objectstack-api/SKILL.md carries the rename (governed; net 0 lines, file and package both). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4138K1EG7kQ81FNba5Kp4 * docs(changeset): the twelve api/ duration renames (#15677) @objectstack/spec minor with the BREAKING banner naming every renamed key, the six adr-0087 ids registered, the retryAfter wire note, and the disposition split (one D2 conversion, five semantic entries). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4138K1EG7kQ81FNba5Kp4 * fix(docs-audit): declare the conversion-replay exclusion kind (b) relied on incidentally (#15677) An ADR-0087 conversion fixture copies a routable metadata kind verbatim, so an `apis:` fixture carries `method:` beside `path:` — because that is what an ApiEndpoint IS. Ruling A named conversions/registry.ts as the guard's target but enforced it with requireMethodSignal, a content proxy that held only while no conversion fixture carried a verb. This card's apis: conversion is the first that does, and the live pin red exactly as designed. The fixture is correct and stays. The exclusion moves to CONVERSION_REPLAY_FILE_RE, which states the structural fact instead of testing a symptom, and three cases pin the new guard as load-bearing rather than incidental in its turn. NOT restricting kind (b) to packages/spec/src/api/**: that is the invariant the live pin asserts, and enforcing it in the walk would make that pin true by construction — a check that cannot fail. Measured tail-neutral: the scan census is byte-identical to the base (17 route sources, 12 call sites, 5 contract declarations, 78 tails, 61 reachable). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4138K1EG7kQ81FNba5Kp4 * docs: move the hand-written pages onto the renamed keys, and strip the issue ids (#15677) Lap 1 regenerated content/docs/references/** but left the HAND-WRITTEN pages teaching the old spellings. Three of them carried `os:check` blocks authoring `cacheTtl`, so check:skill-examples was RED and lap 1 never ran it — it sits in check:generated's "not run here" list and I did not run it separately. cacheTtl -> cacheTtlSeconds: 14 occurrences on 13 lines, all the ApiEndpoint key. retryAfter -> retryAfterSeconds: 14 occurrences, the ADR-0112 envelope field only. Deliberately NOT swept, each verified rather than assumed: - the HTTP `Retry-After` response header (6 locals over 4 sites) — RFC 9110, a separate surface, and the thing the tombstone prose exists to protect; - `retry_after` as a RetryStrategy ENUM VALUE (errors.zod.ts z.enum); - `details.retry_after` on the wire, and the pre-existing `details.retryAfterSeconds` the runtime really emits (endpoint-policy.ts). Also strips `(#14478 ruling B)` from the twelve tombstone prescriptions THIS card wrote: check:doc-authoring forbids an internal issue id in customer-facing spec text (maintainer ruling 2026-08-12), and the campaign's own earlier tombstones already comply. The version and the FROM -> TO mapping stay — those are the durable references AGENTS.md requires. Measured: the gate read 4 findings on the base and 16 on my head; it now reads the base's 4 again, so this PR adds none. Those 4 are card 1/6's (PR #15814) and are not mine to touch. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4138K1EG7kQ81FNba5Kp4 * docs(spec): drop the internal issue id from the four epoch-instant tombstone prescriptions (#15676) `check:doc-authoring` rule 3 forbids an internal issue id in customer-facing spec text (maintainer ruling 2026-08-12). The four `retiredKey()` prescriptions this stack card added each opened with a `(#14478 ruling B)` parenthetical — a citation-shaped token that resolves to nothing for the author who meets it the moment their key is refused, and which the generated reference pages publish verbatim. The parenthetical goes; the sentence keeps everything actionable it carried — the FROM to TO rename, that the value is unchanged, and the `SimplePresenceState.lastSeen` neighbour caveat — matching the shape the campaign's already-compliant tombstones use (`hook.timeout`, `job.timeout`, `DriverOptions.timeout`). The internal anchor is untouched in the adjacent `//` and `/** */` comments, which are not customer-facing and were never findings. `content/docs/references/**` regenerated with `pnpm --filter @objectstack/spec gen:docs` — no generated artifact was hand-edited. check:doc-authoring: 4 findings before, exit 0 after. check:duration-unit-keys: unmoved — 48 offender(s) among 215 duration-shaped numeric key(s), (6 declared `EpochMs` instant(s), 11 declared `externalVocabulary` mirror(s)). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4138K1EG7kQ81FNba5Kp4 * chore(spec): regenerate the reference page the merge deferred (#15677) The merge with card 1/6's advanced tip (9605808) touched api/websocket.zod.ts on both sides. The schema source auto-merged; the generated content/docs/references/api/websocket.mdx is routed to merge=os-regen, so the driver deferred it and the merge kept OUR side — silently dropping card 1/6's half. Regenerating from the merged tree is what repairs it, and it carries both sides: their two stripped prescriptions land (issue-id occurrences 2 -> 0) while my four renamed keys stay (6 -> 6). Not hand-edited and not resolved by taking a side: the bytes come from `pnpm --filter @objectstack/spec check:generated --fix` on the merged tree, and the staged diff was read before committing (`git diff` reads clean over this trap; only the staged diff shows it). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4138K1EG7kQ81FNba5Kp4 * wip(spec): rename the 14 kernel/ duration keys, tombstones on the old spellings (#15678) The schema half of stack card 3/6. Gate reads 36 -> 22 with src/kernel/ at 0. Readers, registrations and regenerated artifacts follow. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4138K1EG7kQ81FNba5Kp4 * wip(spec): readers and ADR-0087 registrations for the kernel/ renames (#15678) 14 retired-key entries and five semantic entries. No D2 conversion on this card: none of the twelve defs is a stack collection member or a stored sys_metadata row (stack.zod.ts declares no eventBus / startup / plugin-security root), so the conversion chain has no seam that would see one. Readers moved in core's health monitor, the kernel and contracts test suites, and the hand-written lifecycle protocol page. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4138K1EG7kQ81FNba5Kp4 * wip(spec): tombstone refusal tests and regenerated reference pages (#15678) Per-key refusal tests assert the prescription (issue code + rename text), not a bare throw, plus acceptance pins at the same magnitudes and defaults. Two deliberate NEGATIVE controls: RuntimeConfig.resourceLimits.timeout names no unit so it stays bare, and EventSourcingConfig.snapshotRetention is a count. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4138K1EG7kQ81FNba5Kp4 * docs(changeset): the fourteen kernel/ duration renames (#15678) @objectstack/spec minor + @objectstack/core patch, BREAKING banner naming every renamed key, adr-0087: registered with the five semantic ids. Documents the two unit collisions the rename removes (responseTime hours vs ms; uptime ms vs the seconds-valued GET /health) and the three keys deliberately left bare. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4138K1EG7kQ81FNba5Kp4 * wip(spec): rename the 15 system/ duration keys, tombstones on the old spellings (#15679) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4138K1EG7kQ81FNba5Kp4 * wip(spec): readers, tombstone refusal tests and ADR-0087 registrations for the system/ renames (#15679) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4138K1EG7kQ81FNba5Kp4 * wip(spec): regenerated artifacts and reference pages for the system/ renames (#15679) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4138K1EG7kQ81FNba5Kp4 * test(spec): fix the three new metrics pins — required label, and assert the prescription explains the non-mechanical name (#15679) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4138K1EG7kQ81FNba5Kp4 * docs(changeset): the fifteen system/ duration renames (#15679) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4138K1EG7kQ81FNba5Kp4 * wip(spec): rename the 7 data/ ui/ ai/ integration/ duration keys, tombstones on the old spellings (#15680) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4138K1EG7kQ81FNba5Kp4 * wip(spec): readers, ADR-0087 registrations and regenerated artifacts for the 7 renames (#15680) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4138K1EG7kQ81FNba5Kp4 * test(spec): tombstone refusal pins for the 7 renames; narrow credential derivation so a retirement tombstone is not read as a secret (#15680) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4138K1EG7kQ81FNba5Kp4 * chore(i18n): regenerate the metadata-form bundles for the dashboard key rename, translations carried over (#15680) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4138K1EG7kQ81FNba5Kp4 * docs(changeset): the seven data/ ui/ ai/ integration/ duration renames, plus the auto-arm sibling (#15680) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4138K1EG7kQ81FNba5Kp4 * docs(changeset): declare the ADR-0087 disposition on the driver-memory rename (#15680) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4138K1EG7kQ81FNba5Kp4 * wip(spec): widen check:duration-unit-keys to every workspace package's src (#15682) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4138K1EG7kQ81FNba5Kp4 * feat(driver-turso)!: rename the published config timeout to timeoutMs, tombstone on the old spelling (#15682) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4138K1EG7kQ81FNba5Kp4 * fix(scripts): declare the three workspace-enumerator exports the widened gate imports (#15682) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4138K1EG7kQ81FNba5Kp4 * fix(service-datasource): read the canonical turso `config.timeoutMs` at the shared libSQL seam `buildTursoDriverConfig` still consulted `config.timeout` after that authored key was renamed to `timeoutMs` and tombstoned, so a datasource authored the canonical way had its timeout silently dropped. `TursoConfigSource.config` is an untyped string-keyed bag, so tsc could not see the rename through it, and the covering test authored the retired spelling at all three of its sites and stayed green over the defect. The reader now reads `config.timeoutMs`; the driver key it lands on stays `timeout` (published-but-inert, must not be ratified by a rename). No fallback arm for the retired spelling — the sqlite `filename` and mongo `url` arms in `default-datasource-driver-factory.ts` set that precedent, and both authoring and stored-row rehydration deliver the canonical key already. The covering test moves to the canonical spelling and gains contract-derived cases that read the schema's own tombstones, so they hold for the next rename without being edited. The two sibling pins that author the same spec move with it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4138K1EG7kQ81FNba5Kp4 * docs(spec): correct the exclusion pin's stale comment to the fixture it actually builds (#15682) The behavioural exclusion pin in `check-duration-unit-keys.ts` was described against an earlier, smaller fixture: "asserts the walk finds ONE file. Seven offenders on disk, one in the verdict." The code below it writes 8 excluded shapes plus 2 real source files and asserts `walked.files === 2 && walked.findings.length === 2` — three numbers the prose got wrong. The code is right and the comment was stale, so only the comment moves. The assertion, the `excluded` list and the second `expect` ("an excluded file is not merely unjudged, it is never read") are untouched; the wider fixture is the point of the pin and is deliberately kept. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4138K1EG7kQ81FNba5Kp4 --------- Co-authored-by: Claude <noreply@anthropic.com> * chore(changeset): the turso ADR-0087 disposition reads `registered` against main (#14478) Card 6/6's `already-registered` was honest at its own base, where card 5/6 had already registered `turso-config-timeout-to-timeout-ms`. Measured against `main`, this branch's combined diff is what ADDS that registration, so the honest disposition at this level is `registered` — which is what check-adr-0087-registration.mjs --base origin/main names. The explanation moves to a second comment on its own line: every whitespace-separated token after the id on the marker line itself is parsed as another id. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4138K1EG7kQ81FNba5Kp4 * chore(spec): regenerate the deferred artifacts on the merged tree (#14478) Discharges the os-regen deferral the merge commit recorded. Regenerated in the order the generators require — `gen:migration-registry` writes a source file, so the spec is rebuilt before `gen:api-surface` reads `dist/*.d.ts`: gen:migration-registry, build, gen:api-surface, gen:export-origins, gen:declaration-map, gen:docs `check:generated` then reports all 15 generated artifacts up to date. The ledger keeps every row from both sides and invents none: 356 entry files at the merge base, +27 from main, +80 from this branch, 463 in the result — set equality, not just a matching total. `check:migration-registry` reads the same population back as 186 semantic, 161 retired-key, 116 retired-def. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4138K1EG7kQ81FNba5Kp4 --------- Co-authored-by: os-dev <sales@objectstack.ai> Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
1 parent 932acc3 commit e9fcd6b

325 files changed

Lines changed: 9109 additions & 776 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)