Skip to content

feat(spec)!: the fifteen system/ duration keys carry their unit in the key name [#14478 stack 4/6] - #15938

Open
os-sales wants to merge 27 commits into
claude/issue-14478-duration-unit-in-key-namefrom
claude/issue-15679-system-duration-unit-in-key-name
Open

feat(spec)!: the fifteen system/ duration keys carry their unit in the key name [#14478 stack 4/6]#15938
os-sales wants to merge 27 commits into
claude/issue-14478-duration-unit-in-key-namefrom
claude/issue-15679-system-duration-unit-in-key-name

Conversation

@os-sales

@os-sales os-sales commented Sep 5, 2026

Copy link
Copy Markdown
Collaborator

Part of #14478
Stack card 4/6 — issue #15679, system/: the fifteen remaining duration keys carry their unit in the key name.

Stacked on card 3/6. Base branch is claude/issue-15678-kernel-duration-unit-in-key-name at 8f2b8f389f44862bfa7ca899a3cc4a580d5f0afb, not main. Review the diff against that branch.

Clause-②: yes — this changes contract accept/reject behaviour on the published spec surface.

What the gate says

Re-derived on this branch's own head before any edit, and again on the tip. The gate's verbatim verdict lines:

Before (base 8f2b8f389):

✗ check:duration-unit-keys — 22 offender(s) among 215 duration-shaped numeric key(s) in 805 source file(s) (6 declared `EpochMs` instant(s), 11 declared `externalVocabulary` mirror(s)):

After (tip 45a35896c):

✗ check:duration-unit-keys — 7 offender(s) among 215 duration-shaped numeric key(s) in 828 source file(s) (6 declared `EpochMs` instant(s), 11 declared `externalVocabulary` mirror(s)):

src/system/** goes 15 → 0 and the whole tree falls 22 → 7. The seven that remain are card 5/6's entire population and no src/system/ row is among them: ai/conversation.zod.ts duration, data/driver-nosql.zod.ts timeout, data/driver/memory.zod.ts autoSaveInterval, data/driver/turso.zod.ts timeout, integration/connector.zod.ts monitoringWindow and interval, ui/dashboard.zod.ts refreshInterval. Exit 1 is by design until card 6/6 declares the population empty.

The source-file count rises 805 → 828 because this PR adds exactly 23 files under packages/spec/src/migrations/entries/ (15 retired-key entries + 8 semantic entries). The 215 duration-shaped-key denominator is unchanged, as a pure rename must leave it.

The population was 15, not the 14 the card's table lists

The card's table subtracted card 1/6's intended exemptions and so omitted AccessControlConfig.maxAge. Card 1/6's dev demoted that key rather than marking it, and the gate on this base agrees: it reports 15 src/system/ rows, object-storage.zod.ts:261 maxAge among them.

FROM → TO

key replacement unit
CacheTier.ttl ttlSeconds seconds
CacheAvalanchePrevention.circuitBreaker.resetTimeout resetTimeoutSeconds seconds
CollaborationSessionConfig.idleTimeout idleTimeoutMs milliseconds
CollaborationSessionConfig.snapshot.interval intervalMs milliseconds
FailoverConfig.healthCheckInterval healthCheckIntervalSeconds seconds
MetricAggregationConfig.window.size durationSeconds seconds
ServiceLevelIndicator.window.size durationSeconds seconds
ServiceLevelObjective.period.duration durationSeconds seconds
AccessControlConfig.maxAge maxAgeSeconds seconds
StorageConnection.timeout timeoutMs milliseconds
RegistryUpstream.syncInterval syncIntervalSeconds seconds
RegistryUpstream.timeout timeoutMs milliseconds
RegistryConfig.cache.ttl ttlSeconds seconds
Span.duration durationMs milliseconds
QueueConfig.rateLimit.duration durationMs milliseconds

Every value, default and bound is unchanged. Every old spelling is a retiredKey() tombstone, so it fails tsc at the authoring site and fails the parse with the rename prescription rather than a bare unrecognized-key error. No prescription carries an internal issue id — those live only in // and /** */ comments, per check:doc-authoring.

The two size keys get an honest name, not the mechanical one

The gate prints sizeSeconds for MetricAggregationConfig.window.size and ServiceLevelIndicator.window.size. This PR renames both to durationSeconds instead, and says so in the prescription so the next author does not read the departure as a slip.

Two reasons, in order of weight:

  1. size is byte/row-count vocabulary everywhere else in this spec. CacheTier.maxSize is megabytes, RegistryConfig.cache.maxSize is bytes, and MetricExportConfig.batch.size on the very same file is a record count. sizeSeconds would have kept the misleading half of the name and bolted a unit onto it — a reader still has to decide whether the number is a quantity of data measured per second or a length of time.
  2. The parent key is already window, so the card's suggested windowSeconds would read window.windowSeconds.

durationSeconds names what the number is, and the file supplied its own precedent one schema down: ServiceLevelObjective.period.duration already called a period length a duration. After the rename all three read alike. A pin asserts the prescription contains the sentence explaining the choice.

The maxAge asymmetry is preserved, deliberately

AccessControlConfig.maxAge (bucket CORS) is renamed. Its twin shared/CorsConfig.maxAge (HTTP CORS) keeps its bare name under its externalVocabulary marker, and this PR does not touch it.

The demotion card 1/6 made is right on the schema's own evidence: every bucket-CORS standard the value is forwarded to already spells the unit — S3 MaxAgeSeconds, GCS maxAgeSeconds, Azure MaxAgeInSeconds — so marking that key would have exempted a deviation from the cited standard rather than a mirror of it. The Fetch response header the twin mirrors, Access-Control-Max-Age, genuinely carries no unit token.

⚠️ No gate can catch the twin being renamed along with this one — the marker exempts it either way. A pin test in object-storage.test.ts asserts CorsConfig.maxAge survives, and is the only guard.

Dispositions — eight semantic entries, no D2 conversion

Decided per key from the schema's actual producer rather than defaulted, and this card's answer is uniform. stack.zod.ts declares no cache, collaboration, disasterRecovery, metrics, objectStorage, registry, tracing or worker root — I read its collection list rather than assuming — and none of the twelve declaring defs is a registered metadata kind stored as a sys_metadata row, so the conversion chain has no seam that would see one. They are host configuration, call arguments and runtime-emitted measurements.

All fifteen are registered by exact key in RETIRED_KEYS_BY_MAJOR[18], nested spellings included (system/CacheAvalanchePrevention:circuitBreaker.resetTimeout, system/RegistryConfig:cache.ttl, and so on). The registry diff is 394 insertions, 0 deletions — no other card's tombstone was disturbed.

Readers moved in the same PR

Grepped by TYPE across every workspace package, then each hit read rather than rewritten. No package outside @objectstack/spec imports any of the twelve declaring types, so the readers are: the eight src/system/*.test.ts suites, the regenerated content/docs/references/system/*.mdx, and the authorable-surface/ + authorable-defaults/ ratchets. No liveness ledger, form, i18n bundle or skills/** page names any of these keys — checked, not assumed.

Keys deliberately left alone, each with a pin

FailoverConfig.dns.ttl (declared externalVocabulary mirror, RFC 1035 §4.1.3) · CacheAvalanchePrevention.lockout.lockTimeoutMs (already correct, and milliseconds where its resetTimeoutSeconds sibling is seconds) · MetricExportConfig.batch.size and QueueConfig.rateLimit.max (counts, not durations) · the error-budget burn-rate window and the OpenTelemetry exporter timeout (their describes name no unit, so both are outside the gate's population) · FileMetadata.size (bytes) · MetricDefinition.summary.maxAge and MetricExportConfig.interval (different keys that a name-based sweep would have caught). Pin tests assert each, so a later sweep cannot read this card as "every duration-shaped number on these files".

Verification

Everything below is a measurement; nothing is recalled.

run result
pnpm --filter @objectstack/spec test 473 files / 12815 tests passed, 0 failed
pnpm --filter @objectstack/spec typecheck green (tsc --noEmit, check:scripts-typecheck, check:test-typecheck — 54 files / 261 errors / 145 pinned signatures held, unchanged)
pnpm --filter @objectstack/spec check:generated 15 of 15 artifacts green
pnpm check:doc-authoring exit 0 — 831 pinned sites across 231 files
pnpm check:adr-0087-registration exit 0 — 5 declared-breaking changesets, each carrying a disposition
check:skill-examples exit 0 — 257 prose examples type-check (@objectstack/client-react built first)
check:yaml-examples · check:variant-docs · check:empty-state exit 0
check:exported-any · check:dual-source-exports · check:entry-nameability exit 0
check:error-code-provenance · check:error-code-casing · check:nul-bytes exit 0
check:doc-anchors · check:docs-single-h1 · check:docs-image-tag · check:docs-redirects · check:doc-frontmatter exit 0
check:test-source-alias · check:cross-package-test-inputs · check:type-check-coverage exit 0
check:declared-population-live · check:stack-collection-maps · check:spec-parsed-alias exit 0
check:empty-changeset · check:changeset-no-major · check:published-files exit 0
check:duration-unit-keys exit 1 by design — the 7 rows above belong to card 5/6

Every exit code was captured before any pipe (cmd > log 2>&1; EXIT=$?), and every verdict quoted is the gate's own printed line. The full suite and check:generated were re-run on the final commit 45a35896c; git rev-parse --short HEAD at that run is the sha quoted above.

Reverse verification — two legs, predictions written before running

Both legs mutate packages/spec/src/system/cache.zod.ts, prove the mutation landed by counting the removed and injected markers on disk (never by an exit code), and restore via git checkout HEAD -- path verified by both a blob-hash match against the HEAD blob and an empty git diff HEAD. The script carries a trap ... EXIT INT TERM restore with absolute paths.

No dist rebuild leg is needed here, and the reason is checked rather than assumed: the gate walks packages/spec/src/ as source text (SRC_ROOT = join(pkgRoot, 'src')), and cache.test.ts imports ./cache.zod relatively. Neither subject resolves through the package's exports into dist/.

Leg A — revert the rename entirely (key back to ttl, tombstone removed).
Predicted: gate 7 → 8 with a new row naming src/system/cache.zod.ts ttl; cache.test.ts red on 2 CacheTier cases.
Observed: gate 8 offender(s), the new row src/system/cache.zod.ts:70 ttl — as predicted. Tests: 4 failures, not 2. ⚠️ My prediction under-counted: I forgot the two pre-existing CacheTierSchema fixtures this PR had already moved onto ttlSeconds, which the revert also breaks. Direction and the two named cases were right, the magnitude was wrong, and it is reported as measured rather than as predicted.

Leg B — keep the rename, delete only the tombstone.
Predicted: gate stays at 7 (it reads the key name, not the tombstone); cache.test.ts red on exactly 1 case — REFUSES the retired CacheTier.ttl — because ttl is then silently stripped instead of refused.
Observed: gate 7 offender(s) with no cache.zod.ts row, and exactly 1 failure, that case. Prediction met exactly.

Leg B is the load-bearing one: it shows the refusal pins catch a silent strip that the headline gate is blind to by construction, which is the failure mode the tombstones exist for.

Two inherited reds — not from this PR

check:llms-txt and check:quick-reference-counts are red on this branch with three findings:

packages/spec/llms.txt:94  [count] domain `shared` declares 13 schemas; src/shared/ holds 14
packages/spec/llms.txt:80  [count] heading declares 207 schemas; packages/spec/src/ holds 208
content/docs/getting-started/quick-reference.mdx:209  [total] section "Shared Protocol" declares "of 8 schemas" but content/docs/references/shared/ publishes 9 page(s)

Confirmed inherited by measurement, not by assertion: I ran both gates in a throwaway detached worktree at the base commit 8f2b8f389 and diffed the output against this head's, with repo paths normalised. Both are byte-identical. They trace to card 1/6 and are already fixed on its branch; they clear when the stack integrates in order. Repairing them here would be another card's diff.

Narrowing declared

Heavy CI does not run on a stacked PR (lint.yml and ci.yml both declare pull_request: branches: [main]), so the gate family above was run by hand. scripts/pm/dispatch-gates.mjs derives its change set three-dot against origin/main, which for a card stacked four deep is the whole stack's 268 paths and therefore the whole stack's ~180 families — the other cards' obligations, not this one's. I narrowed to the families that read this card's own diff surface, which is exactly seven directories: content/docs/references/system/, packages/spec/authorable-surface/, packages/spec/authorable-defaults/, packages/spec/src/migrations/ and its two entries/ subdirectories, packages/spec/src/system/, plus one .changeset/ file. The narrowing is a measurement of the diff, not a judgement about which gates matter.

Not touched

api/, kernel/, data/ui/ai/integration (card 5/6), the gate's declared population (card 6/6), any other PR's diff, and content/docs/releases/.

🤖 Generated with Claude Code


Generated by Claude Code

…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
…ternal-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
…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
…he 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
…ellings (#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
…enames (#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
…acts (#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
@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
…ied 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
…e 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
…mbstone 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
…external-vocabulary-exemptions' into claude/issue-15677-api-duration-unit-in-key-name
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
… 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
…#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
…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
@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
… spellings (#15679)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G4138K1EG7kQ81FNba5Kp4
…s for the system/ renames (#15679)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G4138K1EG7kQ81FNba5Kp4
…rt 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
@github-actions

github-actions Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 5 package(s): @objectstack/cli, @objectstack/driver-memory, @objectstack/platform-objects, @objectstack/service-datasource, @objectstack/spec, touching 78 documentable anchor(s). ⚠️ 33 changed file(s) yielded no anchor (packages/spec/authorable-defaults/data.json, packages/spec/authorable-defaults/integration.json, packages/spec/authorable-defaults/system.json, …), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

14 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/automation/connectors.mdx (via billing_api (literal, a string literal in fixture))
  • content/docs/automation/flows.mdx (via durationMs (literal, a string literal on a changed line), timeoutMs (literal, a string literal in apply))
  • content/docs/automation/hook-bodies.mdx (via timeoutMs (literal, a string literal in apply))
  • content/docs/automation/jobs.mdx (via timeoutMs (literal, a string literal in apply))
  • content/docs/automation/webhooks.mdx (via timeoutMs (literal, a string literal in apply))
  • content/docs/deployment/environment-variables.mdx (via timeoutMs (literal, a string literal in apply))
  • content/docs/getting-started/quick-reference.mdx (via DashboardSchema (symbol, a top-level const))
  • content/docs/kernel/contracts/metadata-service.mdx (via sales_overview (literal, a string literal in fixture))
  • content/docs/protocol/kernel/i18n-standard.mdx (via sales_overview (literal, a string literal in fixture))
  • content/docs/protocol/objectui/actions.mdx (via sales_overview (literal, a string literal in fixture))
  • content/docs/protocol/objectui/concept.mdx (via sales_overview (literal, a string literal in fixture))
  • content/docs/protocol/objectui/index.mdx (via sales_overview (literal, a string literal in fixture))
  • content/docs/ui/apps.mdx (via sales_overview (literal, a string literal in fixture))
  • content/docs/ui/dashboards.mdx (via refreshIntervalSeconds (literal, a string literal in DashboardSchema; a string literal in apply; a string literal in dashboard; a string literal in sections; a string literal in summary), sales_overview (literal, a string literal in fixture))

3 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v15.mdx (via DashboardSchema (symbol, a top-level const))
  • content/docs/releases/v16.mdx (via timeoutMs (literal, a string literal in apply), os explain (command, read off packages/cli/src/commands/explain.ts))
  • content/docs/releases/v17.mdx (via ConnectorTriggerSchema (symbol, a top-level const), durationMs (literal, a string literal on a changed line), timeoutMs (literal, a string literal in apply))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 33 changed file(s) yielded no anchor (packages/spec/authorable-defaults/data.json, packages/spec/authorable-defaults/integration.json, packages/spec/authorable-defaults/system.json, …) — pages documenting those are invisible to this run
  • 13 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 61 of 219 client-bound route-ledger rows — the other 158 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 158: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 136 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 74ddc0d6d090903deafdfa4df2abbe50a6f4301dpackageMentionDocs.

Which tree this was computed on

This run read content/docs from e7d832b7312f46f3332466e1576f94976a7d78f2 — the merge of head 9509c6d67b54c85fe545ebf495d2595f60c169c4 into base 74ddc0d6d090903deafdfa4df2abbe50a6f4301d, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older claude/issue-14478-duration-unit-in-key-name holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin e7d832b7312f46f3332466e1576f94976a7d78f2 && git checkout e7d832b7312f46f3332466e1576f94976a7d78f2
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 74ddc0d6d090903deafdfa4df2abbe50a6f4301d 9509c6d67b54c85fe545ebf495d2595f60c169c4 && git checkout -B drift-repro 74ddc0d6d090903deafdfa4df2abbe50a6f4301d && git merge --no-ff 9509c6d67b54c85fe545ebf495d2595f60c169c4

node scripts/docs-audit/affected-docs.mjs --json 74ddc0d6d090903deafdfa4df2abbe50a6f4301d

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 74ddc0d6d090903deafdfa4df2abbe50a6f4301d → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@github-actions github-actions Bot added documentation Improvements or additions to documentation protocol:system tests tooling labels Sep 5, 2026

Copy link
Copy Markdown
Contributor

Contract review (clause ②) PASS at head 45a35896 — director seat, session_01TezFG8ZMrNH6n5VTNpPpdH, 2026-09-05T22:5xZ. Verdict (15 renames incl. maxAge, durationSeconds over the mechanical name, the CORS twin preserved with its pin, eight semantic entries and no D2, readers confined to spec): objectstack#15679 comment 5555234121. needs:contract-review removed from this PR and the card in the same stroke. Not governed on its own (0 of 51), but the stack lands in order under the domain:spec seat with stack 0 human-merged — no ready flip / auto-merge from this seat.

Implemented-by: session_01G4138K1EG7kQ81FNba5Kp4
Reviewed-by: session_01TezFG8ZMrNH6n5VTNpPpdH


Generated by Claude Code

Base automatically changed from claude/issue-15678-kernel-duration-unit-in-key-name to claude/issue-14478-duration-unit-in-key-name September 5, 2026 23:15
claude and others added 4 commits September 5, 2026 23:24
…tem/ card

Card 3/6 landed on the trunk as a squash, which destroyed ancestry and rewound
the merge base to 140e0b2 — before cards 1/6, 2/6 and 3/6. The four textual
conflicts are all one thing: the prose correction card 3/6 made after this
branch forked, about RuntimeConfig.resourceLimits.timeout. The gate reads
.describe() and .meta({ description }) but not the JSDoc above a key, so that
key is listed among the duration-shaped keys without being judged — outside the
verdict, not outside the population. The JSDoc-channel gap is filed separately.

Resolution:
- .changeset/kernel-duration-keys-unit-in-key-name.md,
  packages/spec/src/migrations/entries/semantic/18.kernel-plugin-security-durations-unit-in-key.ts,
  packages/spec/src/kernel/plugin-security-advanced.test.ts — trunk's side taken
  whole. This branch never edited them: each blob is byte-identical to the fork
  point 8f2b8f3, so there was nothing of ours to preserve.
- packages/spec/src/migrations/registry.ts — trunk's side inside the single
  conflict hunk (the same prose correction); everything outside it kept. All 25
  'system/' rows from the trunk and all 40 from this branch survive.

The two content/docs/references/system/*.mdx artifacts are merge=os-regen paths
and are deferred to a regeneration commit on the merged tree.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G4138K1EG7kQ81FNba5Kp4
…carry their unit in the key name — the gate reaches zero offenders (#15680) (#15988)
…p here

The merge deferred two merge=os-regen artifacts:
content/docs/references/system/object-storage.mdx and
content/docs/references/system/disaster-recovery.mdx. Both were regenerated
from the merged tree, in the prescribed order and out of MERGE state:

  pnpm --filter "@objectstack/spec..." build
  pnpm --filter @objectstack/spec gen:schema && gen:docs
  pnpm --filter @objectstack/spec check:generated   -> all 15 artifacts current

The regeneration is provably not a no-op measurement: the trunk's bytes were
restored into the working tree first (worktree only, never the index), and
gen:docs wrote this branch's bytes back over them, byte-identical to what the
merge commit already carried. So the driver kept the correct side for both
paths and there is nothing to correct — the deferral is discharged with an
empty tree change rather than an unexplained silence.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G4138K1EG7kQ81FNba5Kp4
…orking branch

Card 5/6 was stacked on this card, so its PR base was this branch and its squash
landed directly on origin's tip while the trunk merge was in progress here. Its
own branch is deleted, so this branch now carries cards 4/6 and 5/6.

Measured before merging, both readings agree and neither reports a conflict:

  git merge-tree --write-tree bcf0412 HEAD                              -> exit 0
  git -c merge.os-regen.driver=false merge-tree --write-tree bcf0412 HEAD -> exit 0
  both write tree 7b40011

Nothing to resolve by hand. src/migrations/registry.ts auto-merged and every
retired-key row from all three inputs survives: 138 rows on the trunk
74ddc0d, 153 on this branch after the trunk merge, 161 on bcf0412; the
result holds 161, equal to their union, with no row missing and none invented.
The forty 'system/ rows of this card are unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G4138K1EG7kQ81FNba5Kp4
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants