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
| Surface | Location | Reaches the author as |
|---|
retiredKey() tombstone text | packages/spec/src/ui/dashboard.zod.ts:595-603 | a parse error message |
| generated reference page | content/docs/references/ui/dashboard.mdx:113 | published docs |
| protocol upgrade guide | docs/protocol-upgrade-guide.md:197 | published docs |
| migration entry prose | packages/spec/src/migrations/registry.ts:926-929 | os migrate meta --from 16 output |
| conversion doc comment | packages/spec/src/conversions/registry.ts:2511-2512 | source only |
| ledger note | packages/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:
| Symbol | Occurrences | Production call/read sites |
|---|
useResponsiveConfig | 8 | 0 |
ResolvedResponsiveState | 3 | 0 |
resolveResponsiveConfig | 1 | 0 |
getVisibilityClasses | 1 | 0 |
getColumnClasses | 1 | 0 |
getOrderClasses | 1 | 0 |
shouldHideAtBreakpoint | 1 | 0 |
ResolvedResponsiveConfig | 2 | 0 |
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
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/spec17.0.0 retireddashboard.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:Measured this session across objectui and objectstack:
useResponsiveConfighas zero callers, and nothing in objectui reads.responsiveoff 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
retiredKey()tombstone textpackages/spec/src/ui/dashboard.zod.ts:595-603content/docs/references/ui/dashboard.mdx:113docs/protocol-upgrade-guide.md:197packages/spec/src/migrations/registry.ts:926-929os migrate meta --from 16outputpackages/spec/src/conversions/registry.ts:2511-2512packages/spec/liveness/dashboard.json(widgets.responsive)The first four are author-facing.
There is a fifth, subtler dependency:
packages/spec/src/ui/responsive.zod.ts:206-209justifies thehidden -> hiddenOnalias in its author-error curation by citing the return shape ofuseResponsiveConfig. 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 nonode_modules/and nodist/present (verified byfind -type d -name node_modules|distreturning 0 before counting). Occurrences counted withrg -o | wc -l, notgrep -c, and with--hiddenso dot-directories are included.objectui — every occurrence of each symbol, whole-repo:
useResponsiveConfigResolvedResponsiveStateresolveResponsiveConfiggetVisibilityClassesgetColumnClassesgetOrderClassesshouldHideAtBreakpointResolvedResponsiveConfigThe
useResponsiveConfigeight, itemised: 2 in its own definition file, 2 inpackages/mobile/src/index.ts(the re-export), 1 in its own parity test, 1 in apackages/types/src/complex.tscomment, 2 in CHANGELOGs. Nothing else.The second unwired consumer, not named in objectui#4773.
@object-ui/corecarries a whole second implementation of the same contract:packages/core/src/protocols/ResponsiveProtocol.tsconverts aSpecResponsiveConfiginto 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 throughprotocols/index.tsthensrc/index.ts, twoexport *hops, so it is published without ever being named at the entry; a grep-only census cannot see that it is exposed at all. Sopage.components[].responsivehas 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: thecomplex.tstombstone comment, thereport-chart-query-spec-parity.test.tspin, andpackages/core/src/protocols/SharingProtocol.ts— an unrelatedEmbedConfig.responsiveboolean about iframe sizing.The renderer path has no seam for it either.
packages/components/src/renderers/layout/page.tsx:157,173-182flattensregions[].components[]and hands each node straight toSchemaRendererviatoRenderableSchema(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 declareresponsiveat all — a renderer readingschema.responsivetoday would be a type error. Thespec-bridgehas bridges forform-viewandlist-viewonly; no page-component bridge exists.Authored occurrences: zero. No
responsive:key in any.json/.yaml/.ymlin objectui. In objectstack the only two hits are rows inside the liveness ledgers themselves, describing the already-retiredview.list.responsiveanddashboard.widgets[].responsive.Reachability of the exports, measured through the TypeScript checker (not grepped —
export *andexport type { X } fromboth 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, allTS2305, 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, includingComponentRegistry/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'sbuildis a baretscemitting declarations fromsrctodist(packages/{types,core,mobile}/tsconfig.json), nostripInternalanywhere in the repo, and the emit exclusions cover only__tests__/__mocks__/__benchmarks__/*.test.*.@object-ui/core,@object-ui/typesand@object-ui/mobileare all non-private at version 17.6.0, i.e. this is published npm surface.Why the ledger never asked
page.components[].responsivehas never been classified by the liveness ledger.packages/spec/liveness/page.jsonhas 17 top-level props and nocomponentsamong them;componentslives underregions, whose row is a single blanketliveverdict with nochildren, and the walk drills one level only through an explicitchildren.page/regionsis one of the 58 rows inpackages/spec/scripts/liveness/undrilled-containers.baseline.json.That is the same instrument gap the dashboard tombstone names as the reason
widget.responsivesurvived the #3896 sweep — recorded as #4956, withdashboard.widgetsas 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.tsrecords thatui/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
PageComponentSchemadeclares two per-breakpoint channels sixteen lines apart, and the other one is genuinely live:responsiveStyles(ADR-0065,ResponsiveStylesSchema) — read by objectuipackages/react/src/SchemaRenderer.tsxand compiled bypackages/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
livewith a real citation.responsiveStyles).Option B — retire the key (ADR-0049 D2), and repair the four redirects in the same change.
ResponsiveConfigitself would need a separate decision: after this key it has no other authorable carrier.view.responsive(POST /data/sharing/rules绕过SharingRuleSchema:criteria缺失或拼错静默变成"共享该对象全部记录",与 ADR-0049 "never seeded as a permissive match-all" 直接冲突 #3896) anddashboard.widgets[].responsive(spec:DashboardWidgetSchema.responsive按断点建模 —— objectui#3173 维护者已裁决;目标 17.0.0 正式版,带回退条款 #4876) on identical evidence. Leaving it is the outlier, not removing it.responsiveStyles.Option C — classify
deadin the ledger and defer. Drillpage/regions(or at minimum add the row), recorddeadwith this evidence, leave the key.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/objectstackandobjectstack-ai/objectuionly. Triage's disposition was to post the greps rather than claim a clean zero. Run from a cleancloudcheckout:Note for whoever runs it: hits in step 3 need reading, not counting — objectui has an unrelated
EmbedConfig.responsiveboolean that matches the same pattern.Method notes, so the numbers can be falsified
rg -o | wc -l), not lines.node_modules/and nodist/, verified before counting. (A built tree inflates this kind of count badly.)--hiddenwas passed so.changeset/,.github/and friends were in scope.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.Generated by Claude Code