Skip to content

test(core): pin the OUT-of-window precedence pair formatDate(V, 'relative', { style: 'short' }) - #8353

Merged
os-justin merged 2 commits into
mainfrom
claude/issue-8262-relative-out-of-window-precedence-pin
Sep 7, 2026
Merged

os-justin merged 2 commits into
mainfrom
claude/issue-8262-relative-out-of-window-precedence-pin

Conversation

@os-justin

Copy link
Copy Markdown
Collaborator

Fixes #8262

Test-only. No source file changes, no rendered face moves.

What the pin asserts

One new case in packages/core/src/utils/__tests__/date-display.optionsStyle-7745.test.ts, in the
existing the precedence between the two spellings is pinned: positional wins (#7745) describe:

formatDate(V, 'relative', { style: 'short', locale: 'en-US' }) on a years-old V renders the
default absolute face (Jul 4, 2024) — byte-identical to formatDate(V, undefined, { locale })
and not the 'short' face Jul 4, '24.

Why that is a reading and not a snapshot: #7745's sharpest pair was measured only inside the
±7-day window, where formatDate(v, 'relative', ...) produces the phrase locally and never reaches
the absolute fallback. Outside the window the same call round-trips through formatRelativeDate
and back into formatDate, carrying the shared options bag the whole way — a second site at which
the losing spelling can take effect, and the one the in-window case structurally cannot see.

Assertion shape: the TZ-free equivalence first (=== formatDate(V, undefined, { locale })), then the
literal anchor, then the named loser with its own lit control immediately before it, so the
not.toBe cannot pass against a face that renders nothing.

Ablation — two legs, both restored BY STATE

Every leg mutates the read site in packages/core/src/utils/date-display.ts, never the pin, and
runs from a committed implementation. Restore is git checkout HEAD -- PATH under a
trap ... EXIT INT TERM with absolute paths; proof is git diff HEAD empty and the blob hash
back to git rev-parse HEAD:PATH, never an exit code.

HEAD blob of date-display.ts (both legs): 6cbabb21ccb0eb105722aaaf87b5185b3bfebd2c

Leg C — drop the strip (:152, absoluteFallbackOptions(options) to options)

Mutated blob e46f7f1fce2bdece1be4d9ccdcf493d22fd6970b. On-disk proof: anchor count 1 to 0,
injected count 0 to 1. Verdict Tests 4 failed | 11 passed (15). Red by name:

  • ... > the options spelling IS the positional spelling, for every face (RangeError, recursion)
  • ... > beats it OUT of window too — on the delegated absolute face (#8262) (new)
  • ... > ignores it on the out-of-window ABSOLUTE fallback too
  • ... > does not recurse when the bag carries style: 'relative'

AssertionError: expected 'Jul 4, \'24' to be 'Jul 4, 2024' — the leak, exactly as #7816 recorded it.
... > beats it on the sharpest pair (the in-window case) stays GREEN, which is the point.

Leg P — invert the precedence (:203, style ?? options?.style to options?.style ?? style)

Mutated blob 3ae103e49dd4894f5788eb89e5241090da341a61; same on-disk proof.
Verdict Tests 4 failed | 11 passed (15). Red by name: the four cases of the precedence describe,
including the new one. The entire formatRelativeDate still does not read style describe stays
GREEN — the strip neutralises style before the delegated call, so those rows cannot see this half.

The result the two legs give together

Leg C reds and leg P reds intersect in exactly one case: the new one. No existing row in the file
is red in both. That is the new case's whole claim — it is anchored to the conjunction of the
precedence and the strip, which is what "two-sided" means here, and it is the row that survives as a
trip-wire if #7816 is ever reopened and the strip flipped (the seat doing that legitimately rewrites
the formatRelativeDate rows; this one is not in that blast radius by name).

Correction against the card

#8262's body says leg C makes the call render Jul 4, '24 "and no existing test goes red".
Measured false: leg C turns three existing rows red on current main, and #7816's own body
already recorded that (It turned three cases red). The true and stronger statement is the
intersection above. The card's conclusion survives the correction; its stated evidence does not.

Scope

  • objectui#8194's SCOPE FENCE (packages/fields/src/__tests__/fields-date-widget-convention-8194.test.tsx:285)
    fences the two readonly datetime widget sites whose home is formatDateTime. This PR is
    @object-ui/core's formatDate precedence and touches no packages/fields file and no
    formatDateTime face. Not crossed.
  • objectui#8209 (unruled, same territory) is untouched.
  • No display-convention choice was needed: the face asserted is the one main renders today, measured.

Locale and timezone

locale is fixed explicitly in the fixture (en-US, the file's existing L).

TZ is an input here and the repo pins none — no TZ in vitest.config.mts, in any
vitest.setup.*, in turbo.json, in package.json, or in any workflow (zero hits; lit control on
the same corpus fires). CI and this container both resolve UTC. V is 07:00:00.000Z and every
face in the module is built from local date parts, so the literal reads Jul 4 in UTC and in
every zone from UTC-07:00 eastward, Jul 3 west of it. The comment in the new case says exactly
this, and the two equivalences either side of the literal are TZ-free and carry the property alone.

This exposure is pre-existing and out of scope here — measured, not assumed: at the branch base
411a132c0, TZ=Etc/GMT+8 pnpm exec vitest run <the pin file> already gives
Tests 10 failed | 4 passed (14) without this PR's row. Reported to the PM as an out-of-scope
finding (6 files, 49 literal day-bearing assertions in the family) rather than filed: the session's
REST channel is gated 403 and the MCP pool returned API rate limit already exceeded, so the
mandatory de-duplication search could not be run, and filing unchecked is the worse failure.

Verification (all quoted from the runner's own summary, at final HEAD 4aef6b774)

what verdict line
the pin file Test Files 1 passed (1) / Tests 15 passed (15)
packages/core/ in full Test Files 122 passed (122) / Tests 2574 passed (2574)
packages/core type-check exit 0 (tsc --noEmit && tsc -p tsconfig.test.json)
the new file is really in that program tsc -p tsconfig.test.json --listFiles hits the pin file 1; the main tsc --noEmit program hits it 0 — which is why the script runs both
packages/core eslint . ✖ 517 problems (0 errors, 517 warnings); 0 of them name this file
check-changeset-presence ✅ 1 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s): .changeset/pin-relative-out-of-window-precedence-8262.md. + Every one of them has an EMPTY frontmatter — declared as releasing nothing, which is the explicit exemption and a complete answer to this gate. (its first run, before the changeset, was ❌ ... adds no changeset — a test-only change under a released package's src/ still owes the declaration)
check:changeset-no-major ✅ No changeset declares a major bump.
check:control-bytes ✅ check-control-bytes: OK (scanned 6638 tracked text file(s); skipped 85 binary).
check:vi-mock-specifiers / check:vi-mock-inherit both ✅ ... OK
check-governed-queue-guard --test <the two paths> ✅ NOT GOVERNED — 2 path(s) checked against 5 governed surface(s); none matched. (lit control: --test AGENTS.md fires the governed branch)

Every exit code above was captured with cmd > file 2>&1; EXIT=$?, never through a pipe.

Declared narrowing. turbo ls --affected names 38 packages, because everything depends on
@object-ui/core. The diff is one file under packages/core/src/**/__tests__/ plus a changeset:
git grep optionsStyle-7745 across packages apps examples scripts e2e returns zero references
(lit control absoluteFallbackOptions returns 2), and @object-ui/core's files is
["dist","README.md","CHANGELOG.md","LICENSE"], so no other package can observe it. Everything
outside packages/core/ is declared to CI.

🤖 Generated with Claude Code

https://claude.ai/code/session_01YBWFb5YgMU5dw8p2VKj16S


Generated by Claude Code

…tive', { style: 'short' })

objectui#7745 pinned "positional wins; `options.style` is read only when the
positional slot is `undefined`", but its sharpest pair was measured only
INSIDE the ±7-day relative window, where `formatDate(v, 'relative', …)`
produces the phrase locally and never reaches the absolute fallback.

Outside the window the same call round-trips through `formatRelativeDate` and
back into `formatDate`, carrying the shared options bag — a second site at
which the losing spelling could take effect, and one the in-window case cannot
reach. This adds that half: the call renders the default absolute face
(`Jul 4, 2024`, `en-US`), not the `style: 'short'` face.

Test-only; no behaviour change. objectui#7816 was ruled A (`formatRelativeDate`
stays style-less), so nothing lands on the relative path.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YBWFb5YgMU5dw8p2VKj16S
`scripts/check-changeset-presence.mjs` counts any file under a released
package's `src/` — `__tests__` included — so a test-only change still owes a
declaration. Empty frontmatter is the gate's own explicit exemption.

Verdict line after adding it:
  ✅  1 source file(s) of 1 released package(s) changed, and this change
      declares 1 changeset(s) ... Every one of them has an EMPTY frontmatter

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YBWFb5YgMU5dw8p2VKj16S
@github-actions

github-actions Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 50 chunks) 3472.2 KB 3512.7 KB
Main entry chunk (gzip) 143.9 KB 350 KB
Entry file index-BtFtjBF1.js —
Status PASS —

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 15.67KB 5.75KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 25.05KB 9.16KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.18KB 10.59KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.41KB 1.23KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.46KB 3.43KB
auth (index.js) 3.19KB 1.44KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 5.13KB 2.35KB
collaboration (CommentThread.js) 26.08KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 498.55KB 114.03KB
core (index.js) 7.48KB 2.96KB
create-plugin (index.js) 10.08KB 3.26KB
data-objectstack (index.js) 189.15KB 52.56KB
fields (index.js) 243.15KB 61.40KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 1.22KB 0.64KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 6.57KB 2.76KB
i18n (index.js) 3.65KB 1.47KB
i18n (pickLocalized.js) 7.62KB 3.26KB
i18n (provider.js) 26.89KB 9.04KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 38.84KB 10.94KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 4.39KB 1.66KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 11.71KB 4.29KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 5.12KB 1.74KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 15.16KB 3.68KB
plugin-calendar (index.js) 49.00KB 13.91KB
plugin-charts (index.js) 71.39KB 19.92KB
plugin-chatbot (index.js) 193.79KB 46.09KB
plugin-dashboard (index.js) 131.48KB 34.45KB
plugin-designer (index.js) 213.21KB 43.63KB
plugin-detail (index.js) 248.56KB 63.89KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 131.01KB 32.32KB
plugin-gantt (index.js) 167.16KB 40.99KB
plugin-grid (index.js) 208.58KB 56.63KB
plugin-kanban (index.js) 55.38KB 15.72KB
plugin-list (index.js) 113.38KB 27.73KB
plugin-map (index.js) 20.49KB 6.83KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.42KB 11.92KB
plugin-timeline (index.js) 30.10KB 8.74KB
plugin-tree (index.js) 9.33KB 3.25KB
plugin-view (index.js) 84.46KB 20.80KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.66KB 3.50KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 81.07KB 26.86KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.63KB 2.18KB
react (schema-input.js) 2.32KB 1.24KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 6.58KB 2.74KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 5.55KB 2.45KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 20.57KB 5.88KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 13.64KB 4.59KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 1.00KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.93KB 1.49KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.74KB 2.25KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 4.73KB 2.28KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@os-justin
os-justin marked this pull request as ready for review September 7, 2026 14:39
@os-justin
os-justin added this pull request to the merge queue Sep 7, 2026
Merged via the queue into main with commit 79c2cd7 Sep 7, 2026
34 checks passed
@os-justin
os-justin deleted the claude/issue-8262-relative-out-of-window-precedence-pin branch September 7, 2026 14:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

test(core): pin the OUT-of-window precedence pair formatDate(V, 'relative', { style: 'short' }) — the assertion #7745's pin file lacks

2 participants