feat(spec): widen check:duration-unit-keys to every workspace package's zod schemas, and convert the one turso offender it finds - #16022
Conversation
…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
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
…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
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
…y rename (#15680) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4138K1EG7kQ81FNba5Kp4
…s src (#15682) 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
📓 Docs Drift Check2 anchor(s) derived from 2 changed package(s); no hand-written page names any of them. What this run could not see
Coarse fallback — 132 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # 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 |
…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
|
Contract review (clause ②) PASS at head Implemented-by: Generated by Claude Code |
…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
2434d2e
into
claude/issue-14478-duration-unit-in-key-name
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-nameat3351a860dbf5db0627f85a0a95c13d074c213743(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 workspacepackage's
src/(packages/spec/scripts/check-duration-unit-keys.ts). The predicate isuntouched; only the population moves. Members are enumerated through the shared
workspace-enumeratormodule — the one parse ofpnpm-workspace.yaml— rather than aprivate copy of it, and
ROOT_DIR_WATCH_HINTSbecomes eleven literals(
packages/*/src/**…examples/*/src/**) held against the live workspace globs in bothdirections by the self-test. Wiring in
lint.ymlis unchanged: still the last step ofLint & Repo Gates.src/is the whole boundary, and that is measured rather than assumed: all 210 tracked*.zod.tsfiles in this repo live under some workspace member'ssrc/.2. The walk excludes
node_modules, build output and test files, and the self-test pinsit behaviourally. This is the trap #15642 measured: pointing
--rootat a package rootwalked 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
--rootcannot route around it. A
SKIP_DIRS.has('node_modules')assertion could not catch thatcoming 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.tstimeout⇒timeoutMs, with atombstone on the old spelling and a
@objectstack/driver-tursominor + BREAKINGchangeset. 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 beforeeither 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.timeoutfor turso datasources, which is the same authoredkey this package mirrors.
The tombstone is not decoration: this schema is a plain
z.object, so a bare deletion wouldhave stripped
timeoutand parsed successfully. See the ablation below, where exactlythat is demonstrated.
4. Two consequential edits.
packages/spec/src/conversions/registry.ts— the tursoconversion'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 thethree exports the widened gate imports, the idiom its four siblings in
scripts/alreadyuse, held by
check:declaration-mirrors.The measurement, before and after
Verbatim, on this branch. Before — widened population, conversion not yet made:
After — head
eb55348e2, exit 0: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 reads215 … across 838 source file(s), unchanged fromcard 5/6.
Offenders the widening surfaced: exactly one, the whole list:
packages/drivers/driver-turso/src/spec/turso.zod.ts:104timeoutunit-in-prose-not-in-nametimeoutMs+ tombstone, in this PRNo exception list, no baseline, and no key marked exempt — nothing outside
packages/specneeded 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/specat all:That the other 74 enumerated
srctrees contribute no duration site is a real reading, nota silent zero: an independent
git grepforz.number(/z.int(/z.coerce.number(across every non-spec
srctree returns 14 lines in 6 files, and the three files the gatereports 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 ondisk — target blob
5b7ff632…equals the base commit's blob and differs from HEAD'sc3ddc7a9…,timeoutMsoccurrences 3 ⇒ 0. Restore proven by blob hash back toc3ddc7a9…,empty
git diff HEADand emptygit 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 themutated 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 numericpopulation, so it cannot see the strip), while the refusal pin goes red. Measured
exactly that:
expected true to be falseisresult.success— the plainz.objectaccepted{ url, timeout: 30000 }and stripped the key in silence. That is the failure the tombstoneexists to prevent, and the headline gate is blind to it by construction. Mutation and
restore both proven on disk (
z.never2 ⇒ 0 ⇒ 2, hash back toc3ddc7a9…, emptygit diff HEAD); both ablation scripts carrytrap … EXIT INT TERMrestores, and the firstattempt 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/spectest 473 files / 12844 tests, andtypecheck (
tsc --noEmit+check:scripts-typecheck, which is what type-checks the editedgate, +
check:test-typecheck);@objectstack/driver-tursotest 44 files / 1170 testsand typecheck.
pnpm lint(eslint . --no-inline-config) over the whole repo: 6063files, 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 ownbase,
claude/issue-15680-data-ui-ai-integration-duration-unit-in-key-name— see thequalifier directly below),
check-changeset-fixed,check-empty-changeset,check-changeset-no-major,check:doc-authoring(15070 customer-facing strings, 0findings),
check:generated(15/15),check:skill-examples(257/257, after building@objectstack/client-reactand 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 hasalready registered that conversion, and the gate is green there. Run with
--base origin/mainthe identical changeset fires R5 (severity 3 —already-registerednamingan id this very diff adds), because that id does not exist at
mainyet. ⛔ The dispositionis deliberately NOT changed here:
registeredwould fire R4 against this PR's own base. Thetwo 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 (comment5554574094), for after all six cards reach the base branch.Two non-readings, labelled as such:
check:type-check-debtandcheck:published-readme-exportsboth exit 3 (prerequisite not met — each needs the wholeworkspace 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 namesnone of
TursoConfig,TursoConfigSchemaortimeoutMs— it documentsTursoDriverConfig,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) andcheck:quick-reference-counts(1) are red on the base. Both countpackages/spec/src/shared/schemas and its published reference pages — 13 vs 14, 207 vs 208, 8 vs 9 — which is card 1/6's
EpochMsaddition 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.timeoutMsat the shared libSQLseam). It is absorbed here as an ordinary merge commit,
55cd03d0592dab38e05461a5e55198ef12a9b2a8— no rebase, no amend, no force-push, becausethis branch is on a PR others are reading.
Checked before merging, since a
merge=os-regenpath deferred behind the driver is thefailure this repo pays for:
git merge-tree --write-tree 808691883 eb55348e2and the samecommand under
-c merge.os-regen.driver=falseboth exit 0 and both write tree11e6d43ce7f83d05e9697da16b889a55e0e82310— byte-identical, which is the discriminatingsignal that no driver-managed path was touched on both sides. The two sides are in fact
file-disjoint and neither names any
merge=os-regenentry in.gitattributes. The committedmerge'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.tsis corrected. It read "asserts the walkfinds 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
excludedlist and the "an excluded file is not merely unjudged, it isnever 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: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/spectest 473files / 12844 tests and typecheck (
tsc --noEmit+check:scripts-typecheck+check:test-typecheck);@objectstack/driver-tursotest 44 files / 1170 tests andtypecheck;
@objectstack/service-datasourcetest 31 files / 647 tests, run because themerge'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), andcheck-adr-0087-registrationagainst this PR's base.The two inherited reds are still red on the merged head, and are still not repaired here.
check:llms-txt—llms.txt:94shared declares 13 vs 14 on disk,llms.txt:80headingdeclares 207 vs 208 — and
check:quick-reference-counts—quick-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-nameatc09fd1023,the stack's own trunk and PR #15626's head, and that commit is an ancestor of neither this
PR's base branch tip
808691883nororigin/maina2051fa4e— both of which read13 / 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.ymlandci.ymlboth declarepull_request: branches: [main], and this PR's base isa 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 thishead, 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:
config.timeout, so a datasource authored with the canonicaltimeoutMsis dropped at the seam that builds the driver config #16023 —service-datasource's turso reader still consults the retiredconfig.timeoutatturso-driver-config.ts:170, so a datasource authored with thecanonical
timeoutMsis dropped at the seam that builds the driver config. Nothing catchesit: the bag is untyped, the covering test authors the retired spelling, and the ADR-0087
conversion hands that seam the new spelling it does not read.
@objectstack/driver-tursopublishes three config keys nothing reads —timeouton the TS interface,localPathandwasmon the zod schema #16024 — ADR-0049:@objectstack/driver-tursopublishes three config keys nothing reads(
TursoDriverConfig.timeout,TursoConfigSchema.localPath,TursoConfigSchema.wasm).Deliberately not renamed by this PR — renaming an inert key ratifies it as real, which is
the outcome ADR-0049 exists to prevent.
Both were checked against the open backlog with one targeted search, control-verified, before
filing.
Authored by Claude Code, session
session_01G4138K1EG7kQ81FNba5Kp4— recorded in prosebecause an edited PR body normalises the session-URL footer down to its bare form.
Generated by Claude Code
Generated by Claude Code