Skip to content

docs(spec): teach DatasetMeasureSchema.format what a date measure can say - #16515

Merged
huangyiirene merged 1 commit into
mainfrom
claude/issue-14933-dataset-measure-format-date-styles
Sep 7, 2026
Merged

docs(spec): teach DatasetMeasureSchema.format what a date measure can say#16515
huangyiirene merged 1 commit into
mainfrom
claude/issue-14933-dataset-measure-format-date-styles

Conversation

@huangyiirene

Copy link
Copy Markdown
Collaborator

Fixes #14933

Documentation over a published schema. DatasetMeasureSchema.format was silent about date-valued measures while its docblock advertised e.g. "$0,0.00", "0.0%" — exactly the 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. 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" on origin/main at 21c5dcbb3d073716c7d15354a6f9b312cf2205ff:

191:  /** Display format, e.g. "$0,0.00", "0.0%". */
192:  format: z.string().optional(),

The card's :191-192 still held at this base. After the edit the field sits at packages/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 (its date-display.ts header names objectui#7178 and formatMeasure by name).

Method, so the readings are re-checkable: packages/core/src/utils/date-display.ts and number-display.ts were used byte-identical to the pin — git hash-object on each equals the pin blob (b924b028… and f9ec3339…). dataset-format.ts differs from the pin blob in exactly two lines, its relative import specifiers rewritten .js to .ts so Node's type stripping resolves them (diff against the pin blob shows those two lines and nothing else). Executed with node --experimental-strip-types, locale en-US.

formatMeasure(value, format, undefined, undefined, 'en-US'):

measure value format reachable renders
'2024-07-04' (date-only) 'short' yes Jul 4, '24
'2024-07-04' (date-only) 'relative' yes, style read Jul 4, 2024 — outside the ±7-day window
date-only, 3 days ago 'relative' yes 3 days ago
'2024-07-04' (date-only) 'YYYY-MM-DD' no Jul 4, 2024 (locale default)
'2024-07-04' (date-only) absent n/a Jul 4, 2024
'2024-07-04T07:00:00.000Z' (datetime) 'short' NO Jul 4, 2024, 07:00 AM
'2024-07-04T07:00:00.000Z' (datetime) 'relative' NO Jul 4, 2024, 07:00 AM
'2024-07-04T07:00:00.000Z' (datetime) 'YYYY-MM-DD' no Jul 4, 2024, 07:00 AM

Controls run in the same harness, so a "no" above is not a harness that simply never formats anything:

  • Positive, numeric arm still reads numeral patterns: formatMeasure(1234.5, '0,0.00') gives "1,234.50", formatMeasure(0.125, '0.0%') gives "12.5%".
  • Positive, the style parameter really is live: formatDate('2024-07-04', 'short', { locale: 'en-US' }) gives "Jul 4, '24" while the same call with 'YYYY-MM-DD' gives "Jul 4, 2024".
  • Negative, a non-date string is untouched: formatMeasure('hello', 'short') gives "hello".

Where my readings differ from the card's — mine win, and here is why

1. A DATETIME value ignores format entirely. The card's table marks 'short' and 'relative' reachable without qualification. formatMeasureDate (objectui packages/core/src/utils/dataset-format.ts:184-197) has two arms: the date-only arm at :191 threads format into formatDate's style parameter, and the datetime arm at :194 calls formatDateTime(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. formatRelativeDate falls back to the absolute form beyond that (date-display.ts:90), so the card's flat 3 days ago is 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 preferred

There 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.mdx is generated by packages/spec/scripts/build-docs.ts and its Description column is fed by .describe(), nothing else. On origin/main the row read:

| **format** | `string` | optional |  |

The silence was being rendered as a literal blank cell in the published reference, twice (the measure shape appears under both Dataset.measures and 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:

  • Baseline at 67e92e559f: source marker count 1, generated mdx description cells 2.
  • Mutation: strip only the .describe() back to a bare z.string().optional(), keep the new JSDoc. On-disk proof — source marker count 0, bare-optional anchor 1, JSDoc anchor still 1, git hash-object moved 57e5e857… to e855905f….
  • Regenerate gen:schema + gen:docs without rebuilding dist/. Predicted direction: the description cells go back to blank. Observed: description cells 0, blank format rows 2. The output moving with dist/ untouched is also the proof that this generator reads src, so the built-artifact hazard does not apply to this ablation.
  • Restore git checkout HEAD -- ...; git diff HEAD empty 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.json and DatasetMeasure.json now carry the description (they ship — json-schema is in the package's files), and content/docs/references/ui/dataset.mdx is regenerated in this PR with pnpm --filter @objectstack/spec gen:schema && pnpm --filter @objectstack/spec gen:docs. No generated file was hand-edited. The strictness-batch14 pin 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:

  • It advertised pattern grammar as the whole story, which is the original card's complaint. The examples are now explicitly scoped to a numeric measure.
  • Measured, a $ in the pattern IS honoured (formatMeasure(1234.5, '$0,0.00') gives "$1,234.50"legacyDollar in formatMeasure), but the currency field's own docblock immediately below already says an amount must take its symbol from currency and never from "a $ baked into format". The example was teaching the thing its neighbour forbids. The docblock now says the $ is a legacy literal and points at currency; the .describe() says the short version.

Clause 2 reads NO, confirmed mechanically

format stays z.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. The baseRev lag line it prints is informational and pre-existing, not a delta from this diff.
  • Corroborating: pnpm --filter @objectstack/spec gen:api-surface runs as part of the package build and left the tracked api-surface/ tree byte-identical (git status clean after the build), and check:generated reports all 15 generated artifacts up to date.

Changeset: owed, and here is the reasoning

Included, @objectstack/spec: patch. This is not a skip-changeset case: the package publishes the change three ways. json-schema and src/**/*.zod.ts are both in the package's files array, so both the generated JSON Schema description and 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 at 67e92e559f against merge base 21c5dcbb3d, three paths: 91 commands. 89 run, all exit 0.

Named highlights:

  • check:objectui-pin-citations9 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:docs228 generated files in sync with packages/spec (exit 0).
  • check:generatedAll 15 generated artifacts are up to date. (exit 0).
  • check:nul-bytesOK (scanned 8093 text file(s) ... no raw ASCII control bytes) (exit 0).
  • pnpm --filter @objectstack/spec typecheck — exit 0, including check:test-typecheck: OK over the test layer, so the assertion that typecheck covers the tests is measured and not assumed.
  • pnpm --filter @objectstack/spec testTest Files 482 passed (482) · Tests 13102 passed (13102), exit 0.
  • Repo-wide pnpm exec eslint . --no-inline-config --format json — exit 0, 6252 files in eslint's own population, 0 errors, 0 warnings, run at 67e92e559f. No narrowing was needed, so no narrowing is claimed. For the record the config never enables type-aware linting for any file (no parserOptions.project, no typed rules — stated and positively controlled in eslint.config.mjs itself), and only the .ts file 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/dist absent). Those are NOT MEASURED, not red, and none is reported as a pass: pnpm --filter @objectstack/spec build, then the lint and formula closures, then the client and client-react closures were run under scripts/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)), plus check:doc-formula-expressions, check:doc-security-posture and check:docs-transcript-drift.

Declared narrowing — two families left to CI

pnpm check:dts-closure and pnpm check:dual-build-cjs-loads both refused with exit 3, PREREQUISITE NOT MET: each reads the tree a whole-repo closure build leaves behind (check:dual-build-cjs-loads named 103 packages with no dist/). 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 --ran names 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#8263formatMeasureDate's Date.parse guard does not reject an impossible calendar date. Its comment states that 2026-02-30 "keeps falling through to String(v)"; measured, Date.parse('2026-02-30') is not NaN (ECMAScript rolls the surplus day over), so formatMeasure('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

… 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
@github-actions github-actions Bot added the size/s label Sep 7, 2026
@github-actions

github-actions Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec, touching 1 documentable anchor(s).

1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/data-modeling/analytics.mdx (via DatasetMeasureSchema (symbol, a top-level const))
What this run could not see
  • 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 — 130 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 a5eccf92577490da8a4ee82285fcdcb5b876c0e1packageMentionDocs.

Which tree this was computed on

This run read content/docs from 1105260128cc1ec092eb35069fc1523cd9647f55 — the merge of head 67e92e559f9cedc94c43960f6ac491c890e0a241 into base a5eccf92577490da8a4ee82285fcdcb5b876c0e1, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main 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 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

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

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs a5eccf92577490da8a4ee82285fcdcb5b876c0e1 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@github-actions github-actions Bot added documentation Improvements or additions to documentation protocol:ui tooling labels Sep 7, 2026
@huangyiirene
huangyiirene marked this pull request as ready for review September 7, 2026 06:57
@huangyiirene
huangyiirene added this pull request to the merge queue Sep 7, 2026
Merged via the queue into main with commit 8976ea1 Sep 7, 2026
36 checks passed
@huangyiirene
huangyiirene deleted the claude/issue-14933-dataset-measure-format-date-styles branch September 7, 2026 07:30
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 protocol:ui size/s tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

DatasetMeasureSchema.format is silent about date measures — the reachability is now measured, so the ruled one-line documentation can land

2 participants