Skip to content

docs(guide): user-state-persistence taught the rejected user_app_state shape (objectui#5950) - #10011

Merged
os-tesla merged 1 commit into
mainfrom
claude/issue-5950-user-state-doc-shape
Sep 19, 2026
Merged

os-tesla merged 1 commit into
mainfrom
claude/issue-5950-user-state-doc-shape

Conversation

@os-tesla

@os-tesla os-tesla commented Sep 19, 2026

Copy link
Copy Markdown
Collaborator

Fixes #5950

The guide taught readers to build a user_app_state object keyed by kind. That object does
not ship. The shipped adapter writes to sys_user_preference keyed by key — and its own
module 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 onto
sys_user_preference", verbatim:

createObjectStackUserStateAdapter previously wrote to a bespoke user_app_state object
using (user_id, kind, payload) columns. […] Migration: callers passing kind: need to
switch to key:. Callers relying on the old user_app_state table can pin
resource: 'user_app_state' to keep the legacy behaviour, but no backend ships that
schema
[…]

@object-ui/data-objectstack is at 17.6.0 today. The documented shape is a released
breaking 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_state survived in exactly four places before this branch: this page (4
lines), two CHANGELOG entries recording the migration away from it, and the adapter's own
comment 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/main b234a8497 and on this branch's tip:

lines matching, content/docs/guide/user-state-persistence.md before after
user_app_state 4 0
sys_user_preference 0 5
CONTROL — the file's own length 245 261

Each 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:

  • The object and its columns. The yaml declaration now shows sys_user_preference with
    (user_id, key, value, updated_at) and unique [user_id, key], matching what
    packages/data-objectstack/README.md already documented correctly. The page says the adapter
    defines no object of its own — it writes into the per-user KV store @objectstack/plugin-auth
    ships, where a user's theme and locale live.
  • The query spelling. The page taught
    find('user_app_state', { filter: ..., limit: 1 }). QueryParams is sealed and declares the
    $-prefixed OData members; bare filter / limit are not members of it. That is not a
    cosmetic 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 / $top and says why.
  • The write. The page claimed save sends updated_at. The adapter deliberately never
    does: 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.
  • key versus kind. Both words are live on this page and they are not synonyms:
    key is the storage key you choose (ui.favorites, ui.recent, ui.grid.account.state),
    kind is the registry slot you hand attach() / useUserStateAdapter(). They used to be the
    same 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 kind references — which are correct — are left
    alone.

Also added, because the card asked what a reader is meant to do: the resource override (the
option 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 onError
hook.

Scope proof — mechanical, not a promise

The fence was the prose and the yaml block. The page's ts / tsx / typescript fences are
objectui#5174's surface and were repaired there.

Hashing every fenced block's body on the base commit and on this branch: the single yaml
block's hash moves, and all five ts / tsx / typescript block hashes are byte-identical.
Since check:doc-snippets compiles those five fences and reads nothing else on the page, this
branch 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_DOCS
ledger. 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 HEAD blob:

probe surface poisoned gate verdict
CONTROL a typescript fence on this page check:doc-types RED — the page is in the scan set
A1 the yaml block check:doc-types RED, reported as (yaml)
A2 prose, outside every fence check:doc-types GREEN
B prose, a broken internal link docs:check-links RED

The control is lit, so the greens are readings. Three corrections follow:

  1. The page is not on UNGATED_DOCS. Read programmatically, that ledger holds four entries
    today, all under docs/adr/ and docs/audits/. objectui#5174's batch 1 took this page off
    it, which the gate's own header records. The page's ts fences are gated.
  2. The yaml block is not unread eithercheck:doc-types walks it and names the language
    in its diagnostic. The card's claim that a yaml block "is invisible to
    check-doc-component-types" is false as written.
  3. And yet no gate could have caught this. Every gate that reads this page asks a question
    this defect does not answer to: check:doc-snippets compiles ts fences only;
    check:doc-types reads the yaml block but asks only whether a quoted type: '…'
    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-links reads the prose for
    links; check:doc-fences reads the yaml fence 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:

command verdict line
pnpm check:doc-types ✅ Every documented component type is registered. (188 docs, 1107 blocks, 901 type literals)
pnpm check:doc-fences ✅ check:doc-fences — every TypeScript block in 227 document(s) is fenced ts/tsx/typescript
pnpm check:doc-example-ids ✅ 414 real reference(s) all resolve in the catalog registry.
pnpm docs:check-links Links are valid across 17 scan roots.
pnpm check:control-bytes ✅ check-control-bytes: OK (scanned 8074 tracked text file(s))
pnpm check:new-line-citations VERDICT 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-snippets is NOT MEASURED here:
it needs the built dist closure, and the fence-hash identity above makes its verdict on this
page 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 in
any package's files list.

No governed surface: node scripts/check-governed-queue-guard.mjs --test on the changed path
returns 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 content/docs/** only; Auto Label PRs run 35439380882 completed success at 2026-09-19T11:10:35Z; labels afterwards = (none). CONTROL: the same workflow labelled objectui#10009 package: react, tests and objectui#10008 plugin, tests. .github/labeler.yml has no rule matching content/**documentation globs docs/**/* (25 tracked files) and misses content/docs (203). ⚠️ Second half: .github/workflows/labeler.yml sets sync-labels: true, and documentation is 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 ships across a major version boundary while passing check:doc-snippets, check:doc-types, check:doc-fences and docs: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, :205 and :97, :100, :105, :112, :120, :187).
    Both sets predate this branch and the first set was already wrong against origin/main before
    it. This is AGENTS.md commandment 11's cross-file class in a card body rather than in source, and
    check:new-line-citations scans source carriers. Dedupe words: line address,
    cross-file citation, issue body, objectui#7853, card carrier.

  • noted, not filed: packages/data-objectstack/README.md and packages/app-shell/README.md
    both link to this guide and both already document the correct sys_user_preference shape, so
    the 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 the
    first 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_018HrVaotisyhgmot9o2MLRq


Generated by Claude Code


Generated by Claude Code

…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 os-tesla added the documentation Improvements or additions to documentation label Sep 19, 2026 — with Claude
@os-tesla
os-tesla marked this pull request as ready for review September 19, 2026 11:42
@os-tesla
os-tesla added this pull request to the merge queue Sep 19, 2026
Merged via the queue into main with commit ec1de92 Sep 19, 2026
34 checks passed
@os-tesla
os-tesla deleted the claude/issue-5950-user-state-doc-shape branch September 19, 2026 12:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation

Projects

None yet

2 participants