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

Open
os-sales wants to merge 12 commits into
claude/issue-15679-system-duration-unit-in-key-namefrom
claude/issue-15682-widen-duration-unit-keys-population
Open

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 wants to merge 12 commits into
claude/issue-15679-system-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

…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 808691883023a04d844f5360bfaae7361a01b61fpackageMentionDocs.

Which tree this was computed on

This run read content/docs from 74df674559f82d7754b8cb21e4c93c697aa94292 — the merge of head 0ae5a8aa5eafa2073304675918966369824cc23c into base 808691883023a04d844f5360bfaae7361a01b61f, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older claude/issue-15680-data-ui-ai-integration-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 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

⚠️ 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

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