Skip to content

fix(mobile): SpecResponsiveConfig derives from its claim's source (#4598) - #4604

Merged
yinlianghui merged 1 commit into
mainfrom
claude/issue-4598-mobile-responsive-config
Aug 13, 2026
Merged

fix(mobile): SpecResponsiveConfig derives from its claim's source (#4598)#4604
yinlianghui merged 1 commit into
mainfrom
claude/issue-4598-mobile-responsive-config

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Fixes #4598

packages/mobile/src/useResponsiveConfig.ts:26 hand-declared an interface over the schema's four responsive keys, renamed off the schema's own symbol (ResponsiveConfigSpecResponsiveConfig), introduced by a comment saying it mirrored ResponsiveConfigSchema. There was no import, no z.infer, and no other compile-time tie — the sentence was the entire connection.

It agreed with the schema key-for-key on the day #4592's census measured it, and that is the reason to fix it rather than the reason to leave it: the agreement was maintained by nobody and checked by nothing, while the comment already told the next session the copy was canonical. ViewNavigationConfig (#4588) read exactly like this until it had drifted on mode.

The two measurements

(a) Does @object-ui/mobile directly depend on @objectstack/spec? — No.

packages/mobile/package.json dependencies is { "@object-ui/types": "workspace:*" } and nothing else. @objectstack/spec sits in devDependencies (^17.0.0-rc.6) — usable by a test, not by a published .d.ts. node scripts/check-phantom-dependencies.mjs passes on this branch, which is the arbiter confirming both halves.

(b) Does a package mobile already depends on publish a spec-bound ResponsiveConfig? — Yes.

packages/types/src/index.ts:1223:

export type {
  ResponsiveConfig as SpecResponsiveConfig,
  BreakpointName as SpecBreakpointName,
} from '@objectstack/spec/ui';

Bound by import, not copied — verified at the source rather than inferred from the name. Two supporting measurements:

  • The BreakpointName that useResponsiveConfig.ts already imported from @object-ui/types is itself spec-bound: packages/types/src/mobile.ts:19 imports it from @objectstack/spec/ui and re-exports it at :32. The file was already reaching the spec through this edge for one of its two types.
  • The sibling census hit, @object-ui/core's ResolvedResponsiveConfig (packages/core/src/protocols/ResponsiveProtocol.ts:48), is not spec-bound — it is a local shape over a locally declared BreakpointKey union (:27), so it was not the edge to use. But ResponsiveProtocol.ts:21 consumes SpecResponsiveConfig from @object-ui/types — the sibling package already does exactly what this card asks mobile to do.

Branch taken: 1 of the ruling's hierarchy

An existing dependency edge offers a spec-bound export, so the type is re-exported through it. No new dependency edge; branch 2 not needed, branch 3 (honest comment + CLAIM_ALLOW) not reached, and the open question about adding @objectstack/spec to mobile's dependencies does not arise.

import type { BreakpointName, SpecResponsiveConfig } from '@object-ui/types';
export type { SpecResponsiveConfig };

src/index.ts:24 is unchanged, so the published name does not move.

Red-first: the ratchet, verbatim

Predictions were written before the run (P1–P5). With the source fix applied and the ledger line still present, node scripts/check-spec-symbol-derivation.mjs exited 1:

❌  a spec-alignment claim has nothing behind it:

    • @object-ui/mobile lists 1 symbol in CLAIM_DEBT whose spec-alignment claim is gone — `SpecResponsiveConfig`.
      Delete them from scripts/check-spec-symbol-derivation.mjs (`--claim-ledger` regenerates
      the block) so the symbol cannot re-acquire an unbacked claim silently (and close #4592 once the ledger is empty).

After deleting "@object-ui/mobile": ["SpecResponsiveConfig"] — the only edit to scripts/**, one line, zero insertions:

✅  spec symbol derivation: 1251 files scanned against 4834 spec export names; 13 declared dialects, 3 untriaged collisions in 1 packages.
✅  spec alignment claims: 2 declared deliberate copies, 26 unbacked claims in 6 packages.

Baseline on origin/main was 27 unbacked claims in 7 packages; the mobile key held exactly one symbol, so the key disappears. No CLAIM_ALLOW entry was added — the claim is structurally backed, which was the ruling's test for branch 1.

How the gate goes quiet, stated honestly

It stops reporting because after the fix there is no declaration to report: scanFileForClaims collects type aliases, interfaces, enums and variables, and a bare export type { … } is none of those. That is the right outcome — the hand copy is gone — but the gate got there by seeing nothing, not by following the chain. It cannot: the binding runs mobile → @object-ui/types@objectstack/spec/ui and the gate reads one package at a time. That is precisely why the test below is the load-bearing pin.

The type pin, and why it is not vacuous

An Equal< … > against @object-ui/types' SpecResponsiveConfig would be vacuous — same import, true by construction. The new packages/mobile/src/__tests__/responsive-config-spec-parity.test.ts pins against @objectstack/spec/ui's ResponsiveConfig directly, which is falsifiable: it reds if @object-ui/types ever replaces its re-export with a hand copy of its own — drift one link up the chain, the only place this fix can still rot, and the one link no gate covers. A satisfies or one-way extends check would be vacuous here for a different reason: it accepts a narrowing, so a copy that silently dropped order would still pass. Hence invariant Equal< A, B >. The spec import is legal because @objectstack/spec is a devDependency and tests are not published.

Reverse verification — re-grew the hand copy with order dropped (git checkout/file-copy, sha256-verified restore; never git stash). tsc -p tsconfig.test.json:

src/__tests__/responsive-config-spec-parity.test.ts(76,40): error TS2344: Type 'false' does not satisfy the constraint 'true'.
src/__tests__/responsive-config-spec-parity.test.ts(85,45): error TS2344: Type 'false' does not satisfy the constraint 'true'.
src/__tests__/responsive-config-spec-parity.test.ts(123,7): error TS2353: Object literal may only specify known properties, and 'order' does not exist in type 'SpecResponsiveConfig'.
src/useResponsiveConfig.ts(95,26): error TS2339: Property 'order' does not exist on type 'SpecResponsiveConfig'.

Both type pins red, plus the runtime half's full-config literal, plus — unpredicted — the hook's own body, which reads config.order. Restore verified: packages/mobile/src/useResponsiveConfig.ts: OK against the recorded sha256, and typecheck green again.

Runtime must-not-change

useResponsiveConfig's body is untouched — this is a types-only change. There is no existing test suite for useResponsiveConfig: packages/mobile/src/__tests__/ held only breakpoints.test.ts, gesture-spec-parity.test.tsx and useBreakpoint.test.ts. Saying that plainly rather than reporting a green suite that does not exist, and not inventing a behavior suite for a types-only change. The new file's runtime half covers the config surface against the schema value, which is what keeps the erased type assertions honest.

.d.ts analysis — patch

Built both ways with dist/ and tsconfig.tsbuildinfo cleared:

  • dist/index.d.tsbyte-identical, diff clean. The published entry surface does not move.
  • dist/useResponsiveConfig.d.tsexport interface SpecResponsiveConfig { … } becomes export type { SpecResponsiveConfig } re-exported from @object-ui/types. Same name, same position in the signature useResponsiveConfig(config?: SpecResponsiveConfig).

Position analysis on the shape itself: a throwaway probe asserted Equal< OldPublishedShape, SpecResponsiveConfig > where OldPublishedShape is origin/main's interface copied verbatim — it compiled, so the two are invariant-equal, not merely mutually assignable. A pure re-binding with an identical published shape ⇒ patch. Not analyze (no surface change), never major.

Verification

  • Build closure: pnpm --workspace-concurrency=2 --filter "@object-ui/mobile^..." --filter "@object-ui/mobile" build — green (packages/types, packages/mobile).
  • pnpm --filter "@object-ui/mobile" type-check (tsc --noEmit && tsc -p tsconfig.test.json) — exit 0.
  • Repo-root pnpm exec vitest run --maxWorkers=2 packages/mobile/src scripts/__tests__/check-spec-symbol-derivation.test.ts5 files, 49 tests passed. The gate's own suite is included because this PR edits its source of truth.
  • Downstream consumer sweep, PREFIX direction (...@object-ui/mobile = dependents): turbo run type-check --filter="...@object-ui/mobile" --concurrency=248 tasks successful, 48 total.
  • Gate battery, all PASS: check-control-bytes, check-phantom-dependencies, check-changeset-presence, check-changeset-no-major, check-changeset-fixed, check-type-check-coverage, check-lint-coverage, check-spec-symbol-derivation, check-doc-links.
  • eslint net-zero: 0 errors, 18 warnings, all pre-existing and none in a file this PR touches. The four type pins would otherwise have added no-unused-vars warnings — the repo config ignores ^_ for arguments only, which is why packages/types' equivalent parity file carries seven of them. Scoped an eslint-disable/enable pair around the pin block with the reason, rather than widening the shared config.
  • Control-byte self-scan (grep -naP over every touched file including untracked) — clean.

Scope

Four files: packages/mobile/src/useResponsiveConfig.ts, the new test, the changeset, and the single ledger-line deletion in scripts/check-spec-symbol-derivation.mjs. Nothing else in scripts/**. #4597's surfaces (i18n spec-formatters, packages/types/src/views.ts, their ledger lines), packages/types base.ts, react, layout, app-shell, and content/docs/releases/** are untouched.


Generated by Claude Code

)

`useResponsiveConfig.ts` hand-declared an interface over the schema's four
responsive keys, renamed off the schema's own symbol (`ResponsiveConfig` ->
`SpecResponsiveConfig`), under a comment saying it mirrored
`ResponsiveConfigSchema`. No import, no `z.infer`, no compile-time tie of any
kind: the sentence was the whole connection.

It agreed key-for-key on the day it was measured, which is the reason to fix it
rather than the reason to leave it: the agreement was maintained by nobody and
checked by nothing, while the comment told every later reader the copy was
canonical. `ViewNavigationConfig` (#4588) read like this until it drifted on
`mode`.

Measured before choosing: `@object-ui/mobile` has no runtime dependency on
`@objectstack/spec` (devDependency only), but `@object-ui/types` -- already its
only runtime dependency -- publishes the schema's own type under this exact
name, imported from `@objectstack/spec/ui`. So the type is re-exported through
that existing edge; no new dependency, published name unchanged, and the
entry `.d.ts` is byte-identical.

A parity test pins the chain to `@objectstack/spec/ui` directly, because
`@object-ui/types` re-growing a hand copy is the one link the re-export cannot
see. The `CLAIM_DEBT` ledger entry is deleted -- ratchet #5 fails on it while
it is stale.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Qqyix2QcnpUC9XeYVDzx3
@vercel

vercel Bot commented Aug 13, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectui Ignored Ignored Aug 13, 2026 5:58pm

Request Review

@github-actions github-actions Bot added the tests label Aug 13, 2026
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Main entry (gzip) 24.7 KB 350 KB
Entry file index-D__aFqth.js
Status PASS

📦 Bundle Size Report

Package Size Gzipped
app-shell (index.js) 9.56KB 3.59KB
app-shell (runtime-config.js) 7.42KB 2.32KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 8.92KB 3.41KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 1.17KB 0.53KB
auth (AuthProvider.js) 25.13KB 5.40KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.13KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.64KB 2.21KB
auth (SocialSignInButtons.js) 9.60KB 3.89KB
auth (UserMenu.js) 3.40KB 1.22KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 38.46KB 10.17KB
auth (createAuthenticatedFetch.js) 6.34KB 2.43KB
auth (index.js) 2.35KB 1.07KB
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.02KB 0.88KB
auth (useIsWorkspaceAdmin.js) 1.61KB 0.85KB
collaboration (CommentThread.js) 26.07KB 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.65KB 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) 489.28KB 108.46KB
core (index.js) 3.79KB 1.52KB
create-plugin (index.js) 10.08KB 3.26KB
data-objectstack (index.js) 163.56KB 44.83KB
fields (index.js) 230.37KB 57.17KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (currency.js) 1.22KB 0.64KB
i18n (i18n.js) 4.32KB 1.77KB
i18n (index.js) 3.35KB 1.38KB
i18n (pickLocalized.js) 3.69KB 1.73KB
i18n (provider.js) 23.12KB 7.62KB
i18n (useDisplayLocale.js) 2.84KB 1.45KB
i18n (useObjectLabel.js) 27.59KB 6.63KB
i18n (useSafeTranslation.js) 7.77KB 3.13KB
layout (index.js) 38.98KB 10.85KB
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.74KB
mobile (index.js) 1.50KB 0.62KB
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.71KB 0.42KB
mobile (useResponsiveConfig.js) 1.36KB 0.63KB
mobile (useSpecGesture.js) 4.32KB 1.64KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 8.75KB 3.06KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 3.67KB 1.12KB
permissions (evaluator.js) 4.41KB 1.44KB
permissions (index.js) 0.91KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.52KB
permissions (usePermissions.js) 1.55KB 0.71KB
plugin-ai (index.js) 15.75KB 3.80KB
plugin-calendar (index.js) 46.86KB 12.91KB
plugin-charts (index.js) 62.10KB 17.67KB
plugin-chatbot (index.js) 181.21KB 43.14KB
plugin-dashboard (index.js) 121.04KB 31.57KB
plugin-designer (index.js) 212.58KB 42.83KB
plugin-detail (index.js) 239.93KB 60.01KB
plugin-editor (index.js) 2.46KB 1.10KB
plugin-form (index.js) 114.72KB 27.70KB
plugin-gantt (index.js) 164.30KB 40.02KB
plugin-grid (index.js) 190.02KB 50.48KB
plugin-kanban (index.js) 52.74KB 14.53KB
plugin-list (index.js) 112.01KB 27.27KB
plugin-map (index.js) 18.16KB 5.81KB
plugin-markdown (index.js) 13.72KB 4.69KB
plugin-report (index.js) 41.38KB 11.09KB
plugin-timeline (index.js) 26.68KB 7.66KB
plugin-tree (index.js) 8.50KB 2.88KB
plugin-view (index.js) 84.09KB 20.56KB
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.71KB 3.53KB
providers (index.js) 0.44KB 0.22KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.67KB 2.37KB
react (LazyPluginLoader.js) 3.77KB 1.33KB
react (SchemaRenderer.js) 27.64KB 9.44KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 1.26KB 0.67KB
react (schema-input.js) 1.45KB 0.83KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 4.09KB 1.74KB
sdui-parser (index.js) 4.47KB 2.03KB
sdui-parser (parse.js) 10.04KB 2.82KB
sdui-parser (types.js) 0.29KB 0.24KB
sdui-parser (validate.js) 4.69KB 1.48KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 0.99KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 0.20KB 0.18KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 0.20KB 0.18KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.87KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-retry.js) 4.32KB 2.02KB
types (index.js) 3.05KB 1.52KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 2.59KB 1.31KB
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 (spec-report.js) 5.05KB 1.93KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 0.20KB 0.18KB
types (ui-action.js) 3.40KB 1.71KB
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

@yinlianghui
yinlianghui marked this pull request as ready for review August 13, 2026 18:11
@yinlianghui
yinlianghui added this pull request to the merge queue Aug 13, 2026
Merged via the queue into main with commit c911544 Aug 13, 2026
21 checks passed
@yinlianghui
yinlianghui deleted the claude/issue-4598-mobile-responsive-config branch August 13, 2026 18:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

finding(mobile): SpecResponsiveConfig is a renamed hand-copy of the spec's ResponsiveConfig — same key set, no compile-time tie

2 participants