docs(guide): user-state-persistence taught the rejected user_app_state shape (objectui#5950) - #10011
Merged
Merged
Conversation
…e shape
The page documented a `user_app_state` object keyed by `kind` with a
`payload` column. That is the PRE-5.0.0 shape the adapter was migrated
off; `createObjectStackUserStateAdapter` has defaulted to the platform's
`sys_user_preference` store keyed by `key` with a `value` column since
the "Unify per-user UI state storage onto sys_user_preference" release,
whose own migration note records that "no backend ships that schema".
A reader following the page called an object that does not exist.
Corrected the prose and the yaml object declaration to the shipped
shape, and while there, the read/write walkthrough:
- `find` is documented with the `$`-prefixed OData spellings the sealed
`QueryParams` actually declares. The page taught `{ filter, limit }`,
the exact misspelling that used to compile and then get silently
dropped at the conversion layer -- which is how the Favorites adapter
once loaded the Recents row.
- `save` no longer claims to send `updated_at`. The adapter deliberately
never does: that column is server-managed and a non-system write to it
comes back as a dropped field, surfacing a "Some fields were not
saved" toast about a field nobody touched.
- The unique-race recovery and the save chaining are stated, since a
reader writing their own adapter against the same unique constraint
needs them.
- `key` (storage key) and `kind` (registry slot) are distinguished by
name, because the page uses both words and they used to be the same
thing.
- The `resource` override is documented; it is the option a reader needs
to keep the retired table.
Scope held to the prose and the yaml block. All five ts/tsx/typescript
fences on the page are byte-identical to the base commit -- they are the
snippet-compile surface another card already repaired.
Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018HrVaotisyhgmot9o2MLRq
os-tesla
marked this pull request as ready for review
September 19, 2026 11:42
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #5950
The guide taught readers to build a
user_app_stateobject keyed bykind. That object doesnot ship. The shipped adapter writes to
sys_user_preferencekeyed bykey— and its ownmodule header records the parallel table as considered and rejected.
Premise, re-derived at source (the card demanded it, and the stop condition was checked)
Triage left one condition on this card: if the documented design turns out to be an intended
future shape rather than a stale rejected one, stop and report instead of picking. It is
stale, and the record is unambiguous.
packages/data-objectstack/CHANGELOG.md, the 5.0.0 minor "Unify per-user UI state storage ontosys_user_preference", verbatim:@object-ui/data-objectstackis at 17.6.0 today. The documented shape is a releasedbreaking change's before picture, nine majors back — a past, not a plan. ⇒ the stop condition
does not fire; the direction triage already ruled stands.
Tree-wide,
user_app_statesurvived in exactly four places before this branch: this page (4lines), two
CHANGELOGentries recording the migration away from it, and the adapter's owncomment naming it as the road not taken. Nothing else — no ADR, no schema, no test, no
migration — proposes it.
The reading, before and after
Same file, same command, on
origin/mainb234a8497and on this branch's tip:content/docs/guide/user-state-persistence.mduser_app_statesys_user_preferenceEach column carries its own lit control: a zero is a reading only because the other token on
the same file with the same command is non-zero. Before, the page never named the object it
actually writes to; after, it never names the one it does not.
What changed, beyond swapping the wrong word for the right one
Correcting only the object name would have left a walkthrough that still does not work. Four
things on the page were wrong, and all four are the same defect — prose written against a
storage model the adapter left behind:
yamldeclaration now showssys_user_preferencewith(user_id, key, value, updated_at)andunique [user_id, key], matching whatpackages/data-objectstack/README.mdalready documented correctly. The page says the adapterdefines no object of its own — it writes into the per-user KV store
@objectstack/plugin-authships, where a user's
themeandlocalelive.find('user_app_state', { filter: ..., limit: 1 }).QueryParamsis sealed and declares the$-prefixed OData members; barefilter/limitare not members of it. That is not acosmetic difference: the type's own header records that the misspelling "made a typo compile"
and then got dropped at the conversion layer, which answered with any row in the table —
how the Favorites adapter once loaded the Recents row and clicked items leaked into Starred.
The page now teaches
$filter/$topand says why.savesendsupdated_at. The adapter deliberately neverdoes: that column is server-managed, a non-system caller's write to it is stripped and
reported back as a dropped field, and the console surfaces that as a "Some fields were not
saved" toast about a field nobody touched. A reader copying the documented call would have
re-introduced a bug that was fixed in the implementation.
keyversuskind. Both words are live on this page and they are not synonyms:keyis the storage key you choose (ui.favorites,ui.recent,ui.grid.account.state),kindis the registry slot you handattach()/useUserStateAdapter(). They used to be thesame thing, which is exactly why a reader needs telling that they no longer are. The page now
says so in one place, and the surviving
kindreferences — which are correct — are leftalone.
Also added, because the card asked what a reader is meant to do: the
resourceoverride (theoption a reader keeps the retired table with), the unique-race recovery and save chaining (a
reader writing their own adapter against the same constraint needs them), and the
onErrorhook.
Scope proof — mechanical, not a promise
The fence was the prose and the
yamlblock. The page'sts/tsx/typescriptfences areobjectui#5174's surface and were repaired there.
Hashing every fenced block's body on the base commit and on this branch: the single
yamlblock's hash moves, and all five
ts/tsx/typescriptblock hashes are byte-identical.Since
check:doc-snippetscompiles those five fences and reads nothing else on the page, thisbranch cannot change that gate's verdict here — that is a property of the diff, not a claim
about a run.
The reading the claim asked for: does any gate read this page?
The card's body states 「no gate reads them」 and its history puts the page on an
UNGATED_DOCSledger. Both are stale, and the true answer is more specific. Measured, not inferred — one
surface poisoned at a time, each mutation proved on disk before the gate ran and each restore
proved by hash against the
HEADblob:typescriptfence on this pagecheck:doc-typesyamlblockcheck:doc-types(yaml)check:doc-typesdocs:check-linksThe control is lit, so the greens are readings. Three corrections follow:
UNGATED_DOCS. Read programmatically, that ledger holds four entriestoday, all under
docs/adr/anddocs/audits/. objectui#5174's batch 1 took this page offit, which the gate's own header records. The page's
tsfences are gated.yamlblock is not unread either —check:doc-typeswalks it and names the languagein its diagnostic. The card's claim that a
yamlblock "is invisible tocheck-doc-component-types" is false as written.this defect does not answer to:
check:doc-snippetscompilestsfences only;check:doc-typesreads theyamlblock but asks only whether a quotedtype: '…'literal names a registered component — the page's field types were unquoted, and an object
name and column names are not its business at all;
docs:check-linksreads the prose forlinks;
check:doc-fencesreads theyamlfence only to ask whether its body is TypeScript.⇒ the durable reading is not 「no gate reads it」 but 「every gate that reads it is asking
something else」 — and the second is worse, because a page under four green gates reads as
covered. Nothing in this repo compares a documented storage contract against the adapter that
implements it. That is stated here rather than filed, because the fix is a new instrument and
not this card.
Gates run
All on the final commit
b238ea92e, repo root, foreground:pnpm check:doc-types✅ Every documented component type is registered.(188 docs, 1107 blocks, 901typeliterals)pnpm check:doc-fences✅ check:doc-fences — every TypeScript block in 227 document(s) is fenced ts/tsx/typescriptpnpm check:doc-example-ids✅ 414 real reference(s) all resolve in the catalog registry.pnpm docs:check-linksLinks are valid across 17 scan roots.pnpm check:control-bytes✅ check-control-bytes: OK (scanned 8074 tracked text file(s))pnpm check:new-line-citationsVERDICT new-cross-file-line-citations: 0 new citation(s)node scripts/check-changeset-presence.mjs✅ No source or published contract of a released package changed in this range, so no changeset is owed.Each exit code was captured before any pipe.
pnpm check:doc-snippetsis NOT MEASURED here:it needs the built
distclosure, and the fence-hash identity above makes its verdict on thispage independent of this diff. CI runs it.
No changeset: the changeset gate's own script, which is the authority, reports zero published
source and zero moved contract fields in this range — a
content/docs/page is not shipped inany package's
fileslist.No governed surface:
node scripts/check-governed-queue-guard.mjs --teston the changed pathreturns
NOT GOVERNED — 1 path(s) checked against 5 governed surface(s); none matched.Acceptance notes
Found while working, in scope for reporting and not fixed here:
FILED as objectui#10012, added by the claiming seat at 2026-09-19T11:17Z (measured AFTER this body was published, so it was not in it). ⭐ This PR is the probe. It touches⚠️ Second half:
content/docs/**only;Auto Label PRsrun35439380882completed success at 2026-09-19T11:10:35Z; labels afterwards = (none). CONTROL: the same workflow labelled objectui#10009package: react, testsand objectui#10008plugin, tests..github/labeler.ymlhas no rule matchingcontent/**—documentationglobsdocs/**/*(25 tracked files) and missescontent/docs(203)..github/workflows/labeler.ymlsetssync-labels: true, anddocumentationis a config-defined label, so the one applied by hand to this PR is removed on the next synchronize.FILED as objectui#10013, same timestamp — the four-green-gates finding below is now a card: no gate compares a documented storage contract against the implementation it names, which is how a page taught an object
no backend shipsacross a major version boundary while passingcheck:doc-snippets,check:doc-types,check:doc-fencesanddocs:check-links.To file (a) — the card's own body and the R+318 triage note cite this page by line address
(
:93,:95-109,:116,:188,:205and:97,:100,:105,:112,:120,:187).Both sets predate this branch and the first set was already wrong against
origin/mainbeforeit. This is AGENTS.md commandment 11's cross-file class in a card body rather than in source, and
check:new-line-citationsscans source carriers. Dedupe words:line address,cross-file citation,issue body,objectui#7853,card carrier.noted, not filed:
packages/data-objectstack/README.mdandpackages/app-shell/README.mdboth link to this guide and both already document the correct
sys_user_preferenceshape, sothe guide had drifted alone. Nothing to repair. Successor who would hit it: the next
editor of either README — they would have found the guide contradicting them.
noted, not filed: the heading on the corrected section still reads "Required backend
object", which is now slightly off — the object is not something a reader creates, it ships
with
@objectstack/plugin-auth. Left alone deliberately: renaming it moves an anchor, and thefirst sentence resolves the ambiguity immediately. Successor: none — no link in the tree
targets any anchor on this page (checked with a control that fires elsewhere).
Session, in prose so it survives an edit:
https://claude.ai/code/session_018HrVaotisyhgmot9o2MLRqGenerated by Claude Code
Generated by Claude Code