Skip to content

ADR-0049 enforce-or-remove: page.components[].responsive is wired into no renderer — and four shipped author-facing texts redirect authors to it as the live alternative (cloud leg unmeasured) #11027

Description

@os-sales

Filed from objectui#4773, which triage scoped as a measurement + proposal card, not a removal card — removal of published spec surface is not a dev's call. Evidence below; the disposition is the spec seat's.

Headline

@objectstack/spec 17.0.0 retired dashboard.widgets[].responsive (#4876, ADR-0049 D2) because no renderer read it. The tombstone does not just remove the key — it redirects the author to another key, and four shipped surfaces repeat that redirect verbatim:

The shared ResponsiveConfig shape is NOT gone — it stays live on page.components[].responsive, which objectui useResponsiveConfig really does read; move the layout there if you need breakpoint behaviour today.

Measured this session across objectui and objectstack: useResponsiveConfig has zero callers, and nothing in objectui reads .responsive off a page component. The destination the tombstone names applies exactly as much breakpoint behaviour as the key it retired — none. An author following the prescribed migration moves an inert key to an inert key and is told it now works.

Cloud is UNMEASURED (this session cannot reach it). Runnable greps for the cloud seat are at the bottom; every total below is "in objectui + objectstack", never "anywhere".

Where the claim is shipped

SurfaceLocationReaches the author as
retiredKey() tombstone textpackages/spec/src/ui/dashboard.zod.ts:595-603a parse error message
generated reference pagecontent/docs/references/ui/dashboard.mdx:113published docs
protocol upgrade guidedocs/protocol-upgrade-guide.md:197published docs
migration entry prosepackages/spec/src/migrations/registry.ts:926-929os migrate meta --from 16 output
conversion doc commentpackages/spec/src/conversions/registry.ts:2511-2512source only
ledger notepackages/spec/liveness/dashboard.json (widgets.responsive)source only

The first four are author-facing.

There is a fifth, subtler dependency: packages/spec/src/ui/responsive.zod.ts:206-209 justifies the hidden -> hiddenOn alias in its author-error curation by citing the return shape of useResponsiveConfig. The spec's own error-message curation is derived from a hook nothing calls.

The measurement

objectui @ 3b147a367ffe1edccd43b82e76c26872f4bb8eba, objectstack @ 8d21f7a76912df531c144f17bc3f5bedd07f79c5. Both measured on source-only trees — freshly created worktrees with no node_modules/ and no dist/ present (verified by find -type d -name node_modules|dist returning 0 before counting). Occurrences counted with rg -o | wc -l, not grep -c, and with --hidden so dot-directories are included.

objectui — every occurrence of each symbol, whole-repo:

SymbolOccurrencesProduction call/read sites
useResponsiveConfig80
ResolvedResponsiveState30
resolveResponsiveConfig10
getVisibilityClasses10
getColumnClasses10
getOrderClasses10
shouldHideAtBreakpoint10
ResolvedResponsiveConfig20

The useResponsiveConfig eight, itemised: 2 in its own definition file, 2 in packages/mobile/src/index.ts (the re-export), 1 in its own parity test, 1 in a packages/types/src/complex.ts comment, 2 in CHANGELOGs. Nothing else.

The second unwired consumer, not named in objectui#4773.@object-ui/core carries a whole second implementation of the same contract: packages/core/src/protocols/ResponsiveProtocol.ts converts a SpecResponsiveConfig into Tailwind visibility / grid-column / order classes and offers a runtime width check. Each of its five exported functions occurs exactly once in the repo — its own definition. It reaches the public entry point only through protocols/index.ts then src/index.ts, two export * hops, so it is published without ever being named at the entry; a grep-only census cannot see that it is exposed at all. So page.components[].responsive has two complete, published, mutually independent consumer implementations and zero wiring to either.

Nothing reads the key. Whole-repo search of objectui for .responsive, ['responsive'], ["responsive"] returns three files, none a renderer: the complex.ts tombstone comment, the report-chart-query-spec-parity.test.ts pin, and packages/core/src/protocols/SharingProtocol.ts — an unrelated EmbedConfig.responsive boolean about iframe sizing.

The renderer path has no seam for it either.packages/components/src/renderers/layout/page.tsx:157,173-182 flattens regions[].components[] and hands each node straight to SchemaRenderer via toRenderableSchema (packages/react/src/schema-input.ts:44), which is a total identity function on objects. objectui's own node interface, BaseSchema (packages/types/src/base.ts:69), does not declare responsive at all — a renderer reading schema.responsive today would be a type error. The spec-bridge has bridges for form-view and list-view only; no page-component bridge exists.

Authored occurrences: zero. No responsive: key in any .json / .yaml / .yml in objectui. In objectstack the only two hits are rows inside the liveness ledgers themselves, describing the already-retired view.list.responsive and dashboard.widgets[].responsive.

Reachability of the exports, measured through the TypeScript checker (not grepped — export * and export type { X } from both propagate a symbol without naming it). A probe program imported every subject from its package entry point, with controls in both directions. Result: exactly four diagnostics, all TS2305, all on the four negative-control lines (a nonsense name from each of @object-ui/types, @object-ui/core, @object-ui/mobile, @objectstack/spec/ui). Every positive control resolved, including ComponentRegistry / compileScopedStyles / hasResponsiveStyles, which reach the entry through the sameexport * mechanism as ResponsiveProtocol. Every subject resolved. PageComponent['responsive'] resolved against the installed published @objectstack/spec17.1.0.

The objectui probe resolved package entries to src/index.ts. That equals the published surface here: each package's build is a bare tsc emitting declarations from src to dist (packages/{types,core,mobile}/tsconfig.json), no stripInternal anywhere in the repo, and the emit exclusions cover only __tests__ / __mocks__ / __benchmarks__ / *.test.*. @object-ui/core, @object-ui/types and @object-ui/mobile are all non-private at version 17.6.0, i.e. this is published npm surface.

Why the ledger never asked

page.components[].responsive has never been classified by the liveness ledger. packages/spec/liveness/page.json has 17 top-level props and no components among them; components lives under regions, whose row is a single blanket live verdict with no children, and the walk drills one level only through an explicit children. page/regions is one of the 58 rows in packages/spec/scripts/liveness/undrilled-containers.baseline.json.

That is the same instrument gap the dashboard tombstone names as the reason widget.responsive survived the #3896 sweep — recorded as #4956, with dashboard.widgets as its worked example. This card is the page-side instance of it: the key was never asked, and survived on that silence.

One nuance worth stating so it is not misread as contradicting the above: packages/spec/src/ui/responsive.zod.ts records that ui/responsive.zod.ts "measured REACHABLE (page.components[].responsive)" in #4001 batch 13, and was tightened rather than retired. That is schema-graph reachability — the BFS from the metadata roots reaches the shape — and it is true. It is a different question from runtime consumption, and that file's own header says a tightening "must not claim reach it does not have". Both readings are consistent: schema-reachable, consumer-dead.

What makes this different from an ordinary dead key

PageComponentSchema declares two per-breakpoint channels sixteen lines apart, and the other one is genuinely live:

  • responsiveStyles (ADR-0065, ResponsiveStylesSchema) — read by objectui packages/react/src/SchemaRenderer.tsx and compiled by packages/core/src/styling/scoped-styles.ts. The spec's own JSDoc calls it "the preferred styling channel for metadata-authored pages".
  • responsive (ResponsiveConfigSchema) — the subject of this card. No reader.

responsive.zod.ts's header already warns that crossing the two vocabularies is "THE mistake this file invites". So an author who wants breakpoint behaviour today has a real destination — it is simply not the one the tombstone names.

The decision (spec seat / maintainer)

Not a dev call; stated as options with the trade-offs, not as a recommendation to execute.

Option A — enforce. Wire one of the two existing consumers into the page renderer (objectui), then the tombstone's promise becomes true and the ledger row goes live with a real citation.

  • Real business need: unevidenced today. Zero authored occurrences in either measured repo; no producer writes the key.
  • Long-term soundness: the contract stays as documented; no author-facing text has to change.
  • AI-authored-metadata safety: good — declared becomes enforced.
  • Startup scope discipline: poor. This is renderer work funded by no pull, on a channel that already has a live sibling (responsiveStyles).

Option B — retire the key (ADR-0049 D2), and repair the four redirects in the same change.ResponsiveConfig itself would need a separate decision: after this key it has no other authorable carrier.

Option C — classify dead in the ledger and defer. Drill page/regions (or at minimum add the row), record dead with this evidence, leave the key.

  • Cheapest, and it shuts the instrument gap even if the key's fate is deferred. But it leaves the four author-facing redirects saying something false, which is the part of this that actively harms authors.

Whatever is chosen, the four redirect texts should not survive unchanged — they are wrong today under every option except A-completed.

The cloud third — UNMEASURED, greps for the cloud seat

This session's repository scope is objectstack-ai/objectstack and objectstack-ai/objectui only. Triage's disposition was to post the greps rather than claim a clean zero. Run from a clean cloud checkout:

# 0. Confirm you are counting SOURCE, not a built tree. Both must print 0,# or add --exclude-dir / -g '!...' to every command below.
find . -type d -name node_modules -not -path '*/node_modules/*'| wc -l
find . -type d -name dist -not -path '*/node_modules/*'| wc -l
# 1. Occurrence counts (rg -o | wc -l counts OCCURRENCES; grep -c counts LINES).# --hidden so dot-directories are not skipped.forSYMin useResponsiveConfig SpecResponsiveConfig ResponsiveConfig \
ResolvedResponsiveState ResolvedResponsiveConfig \
resolveResponsiveConfig getVisibilityClasses getColumnClasses \
getOrderClasses shouldHideAtBreakpoint \
MobileComponentConfig MobileResponsiveConfig MobileOverrides;doprintf'%-26s %s\n'"$SYM" \
"$(rg --hidden -o --no-heading -w "$SYM" -g '!node_modules' -g '!dist'.| wc -l)"done# 2. Per-file breakdown for anything non-zero above.
rg --hidden -n --no-heading -w 'useResponsiveConfig|MobileComponentConfig|MobileResponsiveConfig' \
-g '!node_modules' -g '!dist'.# 3. Does anything READ the key off a node?
rg --hidden -n -g '!node_modules' -g '!dist' \
"\.responsive\b|\['responsive'\]|\[\"responsive\"\]".# 4. Are there AUTHORED occurrences in metadata?
rg --hidden -n -g '!node_modules' -g '!dist' -g '*.json' -g '*.yaml' -g '*.yml' \
'"responsive"\s*:|(^|\s)responsive\s*:'.# 5. Does cloud import from the packages at all?
rg --hidden -n "from '@object-ui/mobile'|from '@object-ui/core'" -g '!node_modules' -g '!dist'.

Note for whoever runs it: hits in step 3 need reading, not counting — objectui has an unrelated EmbedConfig.responsive boolean that matches the same pattern.

Method notes, so the numbers can be falsified

  • Counts are occurrences (rg -o | wc -l), not lines.
  • Trees were pinned worktrees with no node_modules/ and no dist/, verified before counting. (A built tree inflates this kind of count badly.)
  • --hidden was passed so .changeset/, .github/ and friends were in scope.
  • Export reachability was measured with tsc, never grep, with negative controls that had to fail and positive controls that had to pass. Without both, "not found" is indistinguishable from a broken probe.
  • Every total is scoped "objectui + objectstack". Cloud is unmeasured, and these are published packages, so "no consumer in these repos" is not "no consumer".

Generated by Claude Code

Metadata

Metadata

Assignees

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions