Skip to content

feat(spec): widen check:duration-unit-keys to every workspace package's zod schemas, and convert the one turso offender it finds - #16022

Merged
os-sales merged 36 commits into
claude/issue-14478-duration-unit-in-key-namefrom
claude/issue-15682-widen-duration-unit-keys-population
Sep 6, 2026
Merged

feat(spec): widen check:duration-unit-keys to every workspace package's zod schemas, and convert the one turso offender it finds#16022
os-sales merged 36 commits into
claude/issue-14478-duration-unit-in-key-namefrom
claude/issue-15682-widen-duration-unit-keys-population

Conversation

@os-sales

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

Copy link
Copy Markdown
Collaborator

Part of #14478
Fixes #15642

Clause-②: yes — a published driver key renames (@objectstack/driver-turso).

Card 6 of 6, the last of the #14478 stack and the one that closes it. Stacked on
claude/issue-15680-data-ui-ai-integration-duration-unit-in-key-name at
3351a860dbf5db0627f85a0a95c13d074c213743
(card 5/6's head), which is also this PR's base.

What changed

1. The gate's declared population widens from packages/spec/src/** to every workspace
package's src/
(packages/spec/scripts/check-duration-unit-keys.ts). The predicate is
untouched; only the population moves. Members are enumerated through the shared
workspace-enumerator module — the one parse of pnpm-workspace.yaml — rather than a
private copy of it, and ROOT_DIR_WATCH_HINTS becomes eleven literals
(packages/*/src/**examples/*/src/**) held against the live workspace globs in both
directions by the self-test. Wiring in lint.yml is unchanged: still the last step of
Lint & Repo Gates.

src/ is the whole boundary, and that is measured rather than assumed: all 210 tracked
*.zod.ts files in this repo live under some workspace member's src/.

2. The walk excludes node_modules, build output and test files, and the self-test pins
it behaviourally.
This is the trap #15642 measured: pointing --root at a package root
walked that package's installed dependencies and reported "7151 offender(s) … in 150098
source file(s)"
— a reading about this repo's dependencies wearing this gate's verdict
line. The exclusion is applied to the walk, not to the roots, so an explicit --root
cannot route around it. A SKIP_DIRS.has('node_modules') assertion could not catch that
coming back, so the pin builds a fixture tree containing every excluded shape — each file
carrying the same offender the self-test's first case uses — and asserts the walk finds
2 source files of 10, and 2 offenders. Eight offenders on disk that must not be seen.

On this tree the exclusion removes nothing tracked: no tracked file under any member's
src/ sits below a skipped directory name.

3. The one offender the widening finds is converted.
packages/drivers/driver-turso/src/spec/turso.zod.ts timeouttimeoutMs, with a
tombstone on the old spelling and a @objectstack/driver-turso minor + BREAKING
changeset. That package's schema is a different declaration from the spec's
packages/spec/src/data/driver/turso.zod.ts, which card 5/6 renamed; both were read before
either was touched. The ADR-0087 disposition is not-required (already-registered turso-config-timeout-to-timeout-ms) — that protocol-18 conversion, registered by card 5/6,
rewrites datasources[].config.timeout for turso datasources, which is the same authored
key this package mirrors.

The tombstone is not decoration: this schema is a plain z.object, so a bare deletion would
have stripped timeout and parsed successfully. See the ablation below, where exactly
that is demonstrated.

4. Two consequential edits. packages/spec/src/conversions/registry.ts — the turso
conversion's docblock said the two declarations "disagree by design … until the card that
widens that population"; this PR is that card, so the sentence is corrected rather than left
false. And scripts/workspace-enumerator.d.mts — a partial hand-written declaration for the
three exports the widened gate imports, the idiom its four siblings in scripts/ already
use, held by check:declaration-mirrors.

The measurement, before and after

Verbatim, on this branch. Before — widened population, conversion not yet made:

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

  [unit-in-prose-not-in-name] packages/drivers/driver-turso/src/spec/turso.zod.ts:104 `timeout` — describe names ms but the key name carries no unit. Rename it to carry the unit (e.g. `timeoutMs`), with an ADR-0087 conversion if the key is published.

After — head eb55348e2, exit 0:

✓ check:duration-unit-keys — 217 duration-shaped numeric key(s) across 2291 source file(s) all carry their unit in the key name (or in a sibling `unit`, or under a declared exemption: 6 declared `EpochMs` instant(s), 11 declared `externalVocabulary` mirror(s)); zero offenders, no baseline.

The population genuinely grew, which is the claim the file count carries: 838 source
files ⇒ 2291, and 1097 numeric keys ⇒ 1105. The same head restricted to the old population
(--root packages/spec/src) still reads 215 … across 838 source file(s), unchanged from
card 5/6.

Offenders the widening surfaced: exactly one, the whole list:

site rule disposition
packages/drivers/driver-turso/src/spec/turso.zod.ts:104 timeout unit-in-prose-not-in-name renamed to timeoutMs + tombstone, in this PR

No exception list, no baseline, and no key marked exempt — nothing outside packages/spec
needed either exemption class.

Two duration-shaped sites entered the census with the widening, both in that one file; the
other was already compliant and is the positive control that the instrument fired outside
packages/spec at all:

packages/drivers/driver-turso/src/spec/turso.zod.ts:36   intervalSeconds  [name: seconds] [prose: seconds]
packages/drivers/driver-turso/src/spec/turso.zod.ts:129  timeoutMs        [name: ms]      [prose: ms]

That the other 74 enumerated src trees contribute no duration site is a real reading, not
a silent zero: an independent git grep for z.number( / z.int( / z.coerce.number(
across every non-spec src tree returns 14 lines in 6 files, and the three files the gate
reports no numeric key for spell those chains only inside comments and prose.

Reverse verification

Leg 1 — the widening itself. Predicted before running: with the widened population and
the conversion reverted to the base bytes, RED with exactly one offender, counts unchanged
at 217 / 2291. Measured: exit 1, precisely that offender, 217 … 2291. Mutation proven on
disk — target blob 5b7ff632… equals the base commit's blob and differs from HEAD's
c3ddc7a9…, timeoutMs occurrences 3 ⇒ 0. Restore proven by blob hash back to c3ddc7a9…,
empty git diff HEAD and empty git status --porcelain; the gate then reads green again.
No rebuild leg applies: this gate parses source text with the TypeScript parser and resolves
through no package exports, which the mutated run itself demonstrates by reporting the
mutated line.

Leg 2 — the tombstone, card 5/6's model. Keep the rename, delete only its tombstone.
Predicted: the headline gate stays green (a z.never() key is outside its numeric
population, so it cannot see the strip), while the refusal pin goes red. Measured
exactly that:

GATE EXIT=0   ✓ check:duration-unit-keys — 217 … 2291 … zero offenders, no baseline.
TEST EXIT=1   × refuses the retired `timeout` spelling, and the refusal names `timeoutMs`
              AssertionError: expected true to be false

expected true to be false is result.success — the plain z.object accepted
{ url, timeout: 30000 } and stripped the key in silence. That is the failure the tombstone
exists to prevent, and the headline gate is blind to it by construction. Mutation and
restore both proven on disk (z.never 2 ⇒ 0 ⇒ 2, hash back to c3ddc7a9…, empty
git diff HEAD); both ablation scripts carry trap … EXIT INT TERM restores, and the first
attempt at leg 2 aborted on an anchor count mismatch and left the tree untouched, which
is the guard working.

Verification

Green on head eb55348e2: @objectstack/spec test 473 files / 12844 tests, and
typecheck (tsc --noEmit + check:scripts-typecheck, which is what type-checks the edited
gate, + check:test-typecheck); @objectstack/driver-turso test 44 files / 1170 tests
and typecheck. pnpm lint (eslint . --no-inline-config) over the whole repo: 6063
files, 0 errors, 0 warnings, empty stderr — no narrowing was needed.

Gates green on eb55348e2: check:duration-unit-keys (self-test + gate),
check:declared-population-live, check:watch-hint-literal, check:declaration-mirrors,
check:self-test-wired, check:pm-dispatch-gates, check:cross-package-test-inputs,
check:undeclared-dep-imports, check:nul-bytes, check:parse-guard,
check:published-files, check:type-check-coverage, check:pnpm-filter-targets,
check:test-source-alias, check-adr-0087-registration (measured against this PR's own
base
, claude/issue-15680-data-ui-ai-integration-duration-unit-in-key-name — see the
qualifier directly below), check-changeset-fixed, check-empty-changeset,
check-changeset-no-major, check:doc-authoring (15070 customer-facing strings, 0
findings), check:generated (15/15), check:skill-examples (257/257, after building
@objectstack/client-react and its closure).

check-adr-0087-registration's green is base-relative — that qualifier is load-bearing.
The changeset's disposition, not-required (already-registered turso-config-timeout-to-timeout-ms), is the honest one at THIS PR's base, where card 5/6 has
already registered that conversion, and the gate is green there. Run with --base origin/main the identical changeset fires R5 (severity 3 — already-registered naming
an id this very diff adds), because that id does not exist at main yet. ⛔ The disposition
is deliberately NOT changed here: registered would fire R4 against this PR's own base. The
two base levels genuinely demand contradictory dispositions, which is a recorded finding
rather than a defect in this card; the main-level edit is scheduled on PR #15626 (comment
5554574094), for after all six cards reach the base branch.

Two non-readings, labelled as such: check:type-check-debt and
check:published-readme-exports both exit 3 (prerequisite not met — each needs the whole
workspace built). Neither is a pass and neither is a finding. The narrowing: this PR's only
published-package surface change is driver-turso's schema, and that package's README names
none of TursoConfig, TursoConfigSchema or timeoutMs — it documents TursoDriverConfig,
the TypeScript constructor option, which this PR leaves alone. CI runs both against a built
tree.

Inherited, not repaired here: check:llms-txt (2 findings) and
check:quick-reference-counts (1) are red on the base. Both count packages/spec/src/shared/
schemas and its published reference pages — 13 vs 14, 207 vs 208, 8 vs 9 — which is card 1/6's
EpochMs addition flowing forward, and no part of this diff touches them.

Base-branch merge, and re-verification on the merged head

The base branch moved after this PR was cut: card 5/6 pushed 808691883
(service-datasource reads the canonical turso config.timeoutMs at the shared libSQL
seam
). It is absorbed here as an ordinary merge commit,
55cd03d0592dab38e05461a5e55198ef12a9b2a8 — no rebase, no amend, no force-push, because
this branch is on a PR others are reading.

Checked before merging, since a merge=os-regen path deferred behind the driver is the
failure this repo pays for: git merge-tree --write-tree 808691883 eb55348e2 and the same
command under -c merge.os-regen.driver=false both exit 0 and both write tree
11e6d43ce7f83d05e9697da16b889a55e0e82310 — byte-identical, which is the discriminating
signal that no driver-managed path was touched on both sides. The two sides are in fact
file-disjoint and neither names any merge=os-regen entry in .gitattributes. The committed
merge's tree is that same 11e6d43ce…, and no regeneration deferral was recorded.

Also on this head, the exclusion pin's in-source comment in
packages/spec/scripts/check-duration-unit-keys.ts is corrected. It read "asserts the walk
finds ONE file. Seven offenders on disk, one in the verdict"
— prose written against an
earlier, smaller fixture. The code below it builds 8 excluded shapes plus 2 real source files
and asserts 2 source file(s) of 10, 2 offender(s). The comment now describes that fixture;
the assertion, the excluded list and the "an excluded file is not merely unjudged, it is
never read"
pin are untouched — the wider fixture is the point of the pin.

Re-verified on the merged head 0ae5a8aa5, every reading below taken from that commit:

✓ check:duration-unit-keys — 217 duration-shaped numeric key(s) across 2291 source file(s) all carry their unit in the key name (or in a sibling `unit`, or under a declared exemption: 6 declared `EpochMs` instant(s), 11 declared `externalVocabulary` mirror(s)); zero offenders, no baseline.

The census is unmoved — 217 keys, 2291 source files, zero offenders — which is the
reading that matters here: this lap adds no spec source, so a moved census would have been a
finding rather than a pass. Alongside it, on 0ae5a8aa5: @objectstack/spec test 473
files / 12844 tests
and typecheck (tsc --noEmit + check:scripts-typecheck +
check:test-typecheck); @objectstack/driver-turso test 44 files / 1170 tests and
typecheck; @objectstack/service-datasource test 31 files / 647 tests, run because the
merge's incoming change lands on the same turso timeout seam this PR renames. Gates, each
exit 0: check:generated, check:nul-bytes, check:scripts-typecheck,
check:declared-population-live, check:watch-hint-literal, check:declaration-mirrors
(self-test + gate), check:self-test-wired (self-test + gate), and
check-adr-0087-registration against this PR's base.

The two inherited reds are still red on the merged head, and are still not repaired here.
check:llms-txtllms.txt:94 shared declares 13 vs 14 on disk, llms.txt:80 heading
declares 207 vs 208 — and check:quick-reference-countsquick-reference.mdx:209 "5 of 8"
against 9 published pages. Measured across the trees rather than assumed: the corrected side
(14 / 208 / 9) exists only on claude/issue-14478-duration-unit-in-key-name at c09fd1023,
the stack's own trunk and PR #15626's head, and that commit is an ancestor of neither this
PR's base branch tip 808691883 nor origin/main a2051fa4e — both of which read
13 / 207 / 8, identical to this chain. They therefore clear when this chain reaches that
trunk, not at this merge.

Why CI cannot show the gate green here

lint.yml and ci.yml both declare pull_request: branches: [main], and this PR's base is
a feature branch — so none of the 7 required contexts reports on this head, including
Lint & Repo Gates, where the gate is wired. The green above is local measurement on this
head, quoted verbatim. CI's silence here is not a pass. The stack's full required-context run
happens on the main-based PR #15626.

Not in scope, filed instead

Two findings this work surfaced are filed bare for triage, and are out of scope here:

Both were checked against the open backlog with one targeted search, control-verified, before
filing.

Authored by Claude Code, session session_01G4138K1EG7kQ81FNba5Kp4 — recorded in prose
because an edited PR body normalises the session-URL footer down to its bare form.


Generated by 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
…bstones on the old spellings (#15680)

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

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G4138K1EG7kQ81FNba5Kp4
…al 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
…ey rename, translations carried over (#15680)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G4138K1EG7kQ81FNba5Kp4
…s, plus the auto-arm sibling (#15680)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G4138K1EG7kQ81FNba5Kp4
…, tombstone on the old spelling (#15682)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G4138K1EG7kQ81FNba5Kp4
…ned gate imports (#15682)

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

2 anchor(s) derived from 2 changed package(s); no hand-written page names any of them. ⚠️ 1 changed file(s) yielded no anchor (packages/spec/src/conversions/registry.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/spec/src/conversions/registry.ts) — pages documenting those are invisible to this run
  • 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 — 132 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 5197579bd0a056695a8e39d16563562b396a9c1epackageMentionDocs.

Which tree this was computed on

This run read content/docs from bae48ed0ddbaf6f18ac39d59bd2852e10167b5cf — the merge of head c496f56e4b355ea6727c112aa9ba7d622cc400aa into base 5197579bd0a056695a8e39d16563562b396a9c1e, 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 bae48ed0ddbaf6f18ac39d59bd2852e10167b5cf && git checkout bae48ed0ddbaf6f18ac39d59bd2852e10167b5cf
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 5197579bd0a056695a8e39d16563562b396a9c1e c496f56e4b355ea6727c112aa9ba7d622cc400aa && git checkout -B drift-repro 5197579bd0a056695a8e39d16563562b396a9c1e && git merge --no-ff c496f56e4b355ea6727c112aa9ba7d622cc400aa

node scripts/docs-audit/affected-docs.mjs --json 5197579bd0a056695a8e39d16563562b396a9c1e

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

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling labels Sep 5, 2026
…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
…-in-key-name' into claude/issue-15682-widen-duration-unit-keys-population

Absorbs card 5/6's follow-up commit 8086918 (service-datasource reads the
canonical turso `config.timeoutMs` at the shared libSQL seam), which landed on
the base branch after this PR was cut.

The two sides are file-disjoint and neither touches a `merge=os-regen` path in
.gitattributes, so nothing is deferred behind the merge driver. Verified before
merging: `git merge-tree --write-tree 8086918 eb55348` and the same command
under `-c merge.os-regen.driver=false` both exit 0 and both write tree
11e6d43 — byte-identical.

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

Copy link
Copy Markdown
Contributor

Contract review (clause ②) PASS at head 0ae5a8aa — director seat, session_01TezFG8ZMrNH6n5VTNpPpdH, 2026-09-05T23:0xZ. Verdict (population widening with the walk-level exclusion pinned, the one turso offender renamed with a load-bearing inline tombstone, the base-relative ADR-0087 disposition and its scheduled edit on the stack base, the ordinary base-tip merge): objectstack#15682 comment 5555240077. needs:contract-review removed from this PR and the card in the same stroke. No ready flip / auto-merge from this seat: last card of the stack, lands in order under the domain:spec seat, stack 0 human-merged.

Implemented-by: session_01G4138K1EG7kQ81FNba5Kp4
Reviewed-by: session_01TezFG8ZMrNH6n5VTNpPpdH


Generated by Claude Code

Base automatically changed from claude/issue-15680-data-ui-ai-integration-duration-unit-in-key-name to claude/issue-15679-system-duration-unit-in-key-name September 5, 2026 23:28
Base automatically changed from claude/issue-15679-system-duration-unit-in-key-name to claude/issue-14478-duration-unit-in-key-name September 6, 2026 01:12
…de/issue-15682-widen-duration-unit-keys-population

Cards 2/6..5/6 of the #14478 stack landed on the trunk as squashes, which kept
their content and destroyed their ancestry, so this branch's merge base rewound
to 140e0b2 and seven files conflicted. Every one resolves by taking a side
whole; none is hand-merged.

Group A - five files this card never touched (head blob == fork point
3351a86), resolved to the TRUNK's side, which carries cards 3/6 and 4/6's
later prose corrections:

  .changeset/kernel-duration-keys-unit-in-key-name.md
  packages/spec/src/kernel/plugin-security-advanced.test.ts
  packages/spec/src/migrations/entries/semantic/18.kernel-plugin-security-durations-unit-in-key.ts
  packages/spec/src/migrations/entries/semantic/18.system-metrics-window-durations-unit-in-key.ts
  packages/spec/src/migrations/registry.ts

Group B - two files that are this card's own work, resolved to THIS BRANCH's
side. The trunk contributes nothing to either: its blob is byte-identical to
this branch's fork point (check-duration-unit-keys.ts fe98c8f,
conversions/registry.ts 07e13ff), so the conflict is an artifact of the
rewound base alone:

  packages/spec/scripts/check-duration-unit-keys.ts
  packages/spec/src/conversions/registry.ts

No merge=os-regen path is modified on both sides (all seven read
`merge: unspecified`), so nothing is deferred and there is no regeneration
half. The migrations registry takes the trunk's side and loses no row: both
sides carry the same 180 retired-key ids, and the only textual difference is
prose inside two entries' rationale strings.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G4138K1EG7kQ81FNba5Kp4
@os-sales
os-sales merged commit 2434d2e into claude/issue-14478-duration-unit-in-key-name Sep 6, 2026
9 checks passed
@os-sales
os-sales deleted the claude/issue-15682-widen-duration-unit-keys-population branch September 6, 2026 01:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants