feat(spec): widen check:duration-unit-keys to every workspace package's zod schemas, and convert the one turso offender it finds - #16022
Conversation
…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 74df674559f82d7754b8cb21e4c93c697aa94292 && git checkout 74df674559f82d7754b8cb21e4c93c697aa94292
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 808691883023a04d844f5360bfaae7361a01b61f 0ae5a8aa5eafa2073304675918966369824cc23c && git checkout -B drift-repro 808691883023a04d844f5360bfaae7361a01b61f && git merge --no-ff 0ae5a8aa5eafa2073304675918966369824cc23c
node scripts/docs-audit/affected-docs.mjs --json 808691883023a04d844f5360bfaae7361a01b61f |
…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 |
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