docs(spec): teach DatasetMeasureSchema.format what a date measure can say - #16515
Conversation
… say `format` was silent about date-valued measures while its JSDoc advertised `e.g. "$0,0.00", "0.0%"` — exactly the numeral/date pattern grammar a date measure cannot read. An author with a `min`/`max` over a date field read that line, wrote `format: 'YYYY-MM-DD'`, parsed clean and got the locale default. Carried by a `.describe()` where there was none, so the statement reaches the published JSON Schema and the reference table in `content/docs/references/ui/dataset.mdx`, whose Description cell for `format` was rendering the silence as a blank. The docblock above carries the longer measured record and its objectui read points. Measured at the objectui pin this repo builds against rather than inherited: a date-only value reads `format` as a display STYLE (`short`, `relative`), a datetime value ignores `format` altogether, and no value reads a date PATTERN. Nothing accepts or rejects differently — `format` stays `z.string().optional()`. Generated artifact regenerated with `pnpm --filter @objectstack/spec gen:schema && pnpm --filter @objectstack/spec gen:docs`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T6HeZvT9wdSJD1ZxJb5Eno
📓 Docs Drift CheckThis PR changes 1 package(s): 1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
What this run could not see
Coarse fallback — 130 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 1105260128cc1ec092eb35069fc1523cd9647f55 && git checkout 1105260128cc1ec092eb35069fc1523cd9647f55
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin a5eccf92577490da8a4ee82285fcdcb5b876c0e1 67e92e559f9cedc94c43960f6ac491c890e0a241 && git checkout -B drift-repro a5eccf92577490da8a4ee82285fcdcb5b876c0e1 && git merge --no-ff 67e92e559f9cedc94c43960f6ac491c890e0a241
node scripts/docs-audit/affected-docs.mjs --json a5eccf92577490da8a4ee82285fcdcb5b876c0e1
|
Fixes #14933
Documentation over a published schema.
DatasetMeasureSchema.formatwas silent about date-valued measures while its docblock advertisede.g. "$0,0.00", "0.0%"— exactly the pattern grammar a date measure cannot read. An author with amin/maxover a date field read that line, wroteformat: 'YYYY-MM-DD', parsed clean, and got the locale default. objectui#7178 ruled A and its conditional follow-up assigned this filing by name; the direction is settled and is not reopened here.Located by symbol, not by line
grep -n "Display format"onorigin/mainat21c5dcbb3d073716c7d15354a6f9b312cf2205ff:The card's
:191-192still held at this base. After the edit the field sits atpackages/spec/src/ui/dataset.zod.ts:218.The reachability table, re-taken against THIS repo's pin
The card measured in objectui's own tree. Re-taken against
.objectui-sha=a472b07167a39e55491109e864bb5a54027dcfbd, which is dated 2026-09-04 and already carries objectui PR #7442 (itsdate-display.tsheader names objectui#7178 andformatMeasureby name).Method, so the readings are re-checkable:
packages/core/src/utils/date-display.tsandnumber-display.tswere used byte-identical to the pin —git hash-objecton each equals the pin blob (b924b028…andf9ec3339…).dataset-format.tsdiffers from the pin blob in exactly two lines, its relative import specifiers rewritten.jsto.tsso Node's type stripping resolves them (diffagainst the pin blob shows those two lines and nothing else). Executed withnode --experimental-strip-types, localeen-US.formatMeasure(value, format, undefined, undefined, 'en-US'):format'2024-07-04'(date-only)'short'Jul 4, '24'2024-07-04'(date-only)'relative'Jul 4, 2024— outside the ±7-day window'relative'3 days ago'2024-07-04'(date-only)'YYYY-MM-DD'Jul 4, 2024(locale default)'2024-07-04'(date-only)Jul 4, 2024'2024-07-04T07:00:00.000Z'(datetime)'short'Jul 4, 2024, 07:00 AM'2024-07-04T07:00:00.000Z'(datetime)'relative'Jul 4, 2024, 07:00 AM'2024-07-04T07:00:00.000Z'(datetime)'YYYY-MM-DD'Jul 4, 2024, 07:00 AMControls run in the same harness, so a "no" above is not a harness that simply never formats anything:
formatMeasure(1234.5, '0,0.00')gives"1,234.50",formatMeasure(0.125, '0.0%')gives"12.5%".formatDate('2024-07-04', 'short', { locale: 'en-US' })gives"Jul 4, '24"while the same call with'YYYY-MM-DD'gives"Jul 4, 2024".formatMeasure('hello', 'short')gives"hello".Where my readings differ from the card's — mine win, and here is why
1. A DATETIME value ignores
formatentirely. The card's table marks'short'and'relative'reachable without qualification.formatMeasureDate(objectuipackages/core/src/utils/dataset-format.ts:184-197) has two arms: the date-only arm at:191threadsformatintoformatDate's style parameter, and the datetime arm at:194callsformatDateTime(v, { locale })with no style argument at all. So on a datetime-shaped measure value neither word is read. objectui#7443 is already open on that half; this text does not pre-empt it, it just stops claiming otherwise.2.
'relative'is read, but only produces a relative phrase inside a ±7-day window.formatRelativeDatefalls back to the absolute form beyond that (date-display.ts:90), so the card's flat3 days agois true only for a recent value. The style is genuinely honoured either way, which is why the table above marks it reachable and the prose qualifies the rendering.Neither correction changes the card's conclusion — a date PATTERN is unreadable — and the ruled sentence "date measures render by locale" is still not pasted anywhere: it would be wrong in both directions now.
Carrier:
.describe(), and the choice was measured rather than preferredThere was no
.describe()to extend, so this introduces one. The reason is not style — it is where each carrier lands.content/docs/references/ui/dataset.mdxis generated bypackages/spec/scripts/build-docs.tsand its Description column is fed by.describe(), nothing else. Onorigin/mainthe row read:The silence was being rendered as a literal blank cell in the published reference, twice (the measure shape appears under both
Dataset.measuresand the nested shape). A JSDoc-only fix leaves both blanks exactly as they are.Ablated to prove that rather than assert it, from the committed tree, with the restore leg proven by content and not by an exit code:
67e92e559f: source marker count 1, generated mdx description cells 2..describe()back to a barez.string().optional(), keep the new JSDoc. On-disk proof — source marker count 0, bare-optional anchor 1, JSDoc anchor still 1,git hash-objectmoved57e5e857…toe855905f….gen:schema+gen:docswithout rebuildingdist/. Predicted direction: the description cells go back to blank. Observed: description cells 0, blankformatrows 2. The output moving withdist/untouched is also the proof that this generator readssrc, so the built-artifact hazard does not apply to this ablation.git checkout HEAD -- ...;git diff HEADempty and both blob hashes equal their HEAD blobs (57e5e857…,5dab244d…).So the
.describe()is load-bearing and the JSDoc alone would have closed nothing an author can see. Its generated-artifact consequences are handled rather than left for CI:json-schema/ui/Dataset.jsonandDatasetMeasure.jsonnow carry the description (they ship —json-schemais in the package'sfiles), andcontent/docs/references/ui/dataset.mdxis regenerated in this PR withpnpm --filter @objectstack/spec gen:schema && pnpm --filter @objectstack/spec gen:docs. No generated file was hand-edited. Thestrictness-batch14pin over this file asserts rejection messages and their prescriptions; it neither reads nor forbids.describe(), and it passes.The longer measured record — the two arms, the ±7-day fallback, the objectui read points and their line anchors — lives in the docblock, where it belongs, carrying an asserting pin citation so the next pin bump forces a re-measure of exactly these claims.
The example list moved too
e.g. "$0,0.00", "0.0%"is gone as a bare pair. Two defects in it, not one:$in the pattern IS honoured (formatMeasure(1234.5, '$0,0.00')gives"$1,234.50"—legacyDollarinformatMeasure), but thecurrencyfield's own docblock immediately below already says an amount must take its symbol fromcurrencyand never from "a$baked intoformat". The example was teaching the thing its neighbour forbids. The docblock now says the$is a legacy literal and points atcurrency; the.describe()says the short version.Clause 2 reads NO, confirmed mechanically
formatstaysz.string().optional(). No key added or removed, no export moved, no signature changed, nothing accepts or rejects differently. Not inherited — measured:pnpm --filter @objectstack/spec check:api-surface—@objectstack/spec public API surface + factory signatures unchanged ✓(exit 0).pnpm --filter @objectstack/spec check:authorable-surface— exit 0. ThebaseRevlag line it prints is informational and pre-existing, not a delta from this diff.pnpm --filter @objectstack/spec gen:api-surfaceruns as part of the package build and left the trackedapi-surface/tree byte-identical (git statusclean after the build), andcheck:generatedreports all 15 generated artifacts up to date.Changeset: owed, and here is the reasoning
Included,
@objectstack/spec: patch. This is not askip-changesetcase: the package publishes the change three ways.json-schemaandsrc/**/*.zod.tsare both in the package'sfilesarray, so both the generated JSON Schemadescriptionand the docblock itself ship; and.describe()is runtime metadata a consumer can read off the exported schema. A consumer sees new text where there was none, which is a user-visible change to a published package even though no behaviour moved.Verification
Every verdict below is the gate's own printed line with its exit code captured before any pipe.
Derived gate families for this change set —
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, derived at67e92e559fagainst merge base21c5dcbb3d, three paths: 91 commands. 89 run, all exit 0.Named highlights:
check:objectui-pin-citations—9 asserting objectui pin citation(s) match .objectui-sha (a472b0716), 16 historical citation(s) recorded and not checked, across 1297 spec source(s)(exit 0). The new citation is one of the nine.check:docs—228 generated files in sync with packages/spec(exit 0).check:generated—All 15 generated artifacts are up to date.(exit 0).check:nul-bytes—OK (scanned 8093 text file(s) ... no raw ASCII control bytes)(exit 0).pnpm --filter @objectstack/spec typecheck— exit 0, includingcheck:test-typecheck: OKover the test layer, so the assertion that typecheck covers the tests is measured and not assumed.pnpm --filter @objectstack/spec test—Test Files 482 passed (482) · Tests 13102 passed (13102), exit 0.pnpm exec eslint . --no-inline-config --format json— exit 0, 6252 files in eslint's own population, 0 errors, 0 warnings, run at67e92e559f. No narrowing was needed, so no narrowing is claimed. For the record the config never enables type-aware linting for any file (noparserOptions.project, no typed rules — stated and positively controlled ineslint.config.mjsitself), and only the.tsfile of this diff is in that population at all.Six gates first returned a PREREQUISITE-NOT-MET refusal (
packages/spec/dist/packages/lint/dist/packages/client-react/distabsent). Those are NOT MEASURED, not red, and none is reported as a pass:pnpm --filter @objectstack/spec build, then thelintandformulaclosures, then theclientandclient-reactclosures were run underscripts/pm/os-verify-lock.sh, and all six re-ran to exit 0 —check:api-surface,check:browser-reachable-entries,check:dual-source-exports,check:entry-nameability,check:exported-any,check:skill-examples(257 prose examples type-check across 3 surface(s)), pluscheck:doc-formula-expressions,check:doc-security-postureandcheck:docs-transcript-drift.Declared narrowing — two families left to CI
pnpm check:dts-closureandpnpm check:dual-build-cjs-loadsboth refused with exit 3, PREREQUISITE NOT MET: each reads the tree a whole-repo closure build leaves behind (check:dual-build-cjs-loadsnamed 103 packages with nodist/). Building all 79 workspace packages in a shared container is not a defensible local step for a documentation diff in one package, so they are declared to CI rather than run here.node scripts/pm/dispatch-gates.mjs --rannames exactly these two and nothing else:91 derived, 89 run, 0 NOT-MEASURED, 2 UNRUN. Neither can be moved by this diff's content — both judge build output, and nothing here changes what is emitted.Out of scope — filed, not repaired here
objectstack-ai/objectui#8263 —
formatMeasureDate'sDate.parseguard does not reject an impossible calendar date. Its comment states that2026-02-30"keeps falling through toString(v)"; measured,Date.parse('2026-02-30')is notNaN(ECMAScript rolls the surplus day over), soformatMeasure('2026-02-30', 'short')renders"Mar 2, '26"— a different day from the one stored, silently. Found while taking the table above, lands in objectui, unassigned and ungraded there.Nothing in the rendering path is touched here (that is objectui PR #7442), no accept/reject behaviour moves, and
content/docs/releases/is untouched.Generated by Claude Code