Repository navigation
Description
Found while fixing #10288 (the same two defects in examples/app-showcase). Filing
rather than fixing: content/docs/** pulls in a different gate family than that card's
diff (check:doc-anchors, check:docs-audit-scope, check:docs-redirects,
check:published-readme-links, check:role-word, plus the {/* os:check */} verified-
sample convention), and the Callout's second half needs a wording decision, not a
mechanical edit.
1. The sample renders an empty list
content/docs/ui/react-pages.mdx, the "Live data" sample (L143-148):
const result = await adapter.find('showcase_invoice', {
$filter: ['status', '!=', 'paid'],
$top: 200,
});
const records = Array.isArray(result) ? result : (result && result.records) || [];The query options are right. The result read is not. useAdapter() returns
ObjectStackAdapter, whose find() resolves to a normalized QueryResult — objectui
packages/types/src/data.ts declares data / total / page / pageSize /
hasMore / cursor, and normalizeQueryResult (objectui
packages/data-objectstack/src/index.ts) returns exactly that shape. There is no
records key. result.records is undefined on every call, so records falls to []
and the documented sample renders an empty <ul> forever, with no error.
This is not theoretical: the identical read was fixed once already in
examples/app-showcase/src/ui/pages/crm-workbench.page.ts, whose comment records the
symptom verbatim — "Reading .records here always missed, so the KPI cards silently stuck
at 0 even though the ListView beside them showed the same rows" — and was still live in
renewals-pipeline.page.ts until #10288. The doc is the third instance, and it is the
one a customer copies from.
Fix: result.data, with the array arm kept. Every objectui consumer of this contract
spells it res?.data ?? res?.records ?? … (data-list.tsx, record-picker.tsx,
ObjectRefField.tsx, record-history.tsx, …) — canonical first, the legacy spelling as
a fallback, never alone.
2. The Callout names the wrong consequence
Directly below the sample:
An unprefixed
top:or afilters:key is not a query option — it is silently
dropped, and the query comes back unfiltered or with the default page size. There
is no error.
The "unfiltered" half is right. The page-size half is backwards. The GET list route has
no default page size, so an absent top returns the ENTIRE match set — stated in
packages/client/src/index.ts ("The GET list route has no default page size, so an
absent top returns the ENTIRE match set"), pinned in packages/client/src/client.test.ts
(#6485), and measured by packages/objectql/src/protocol-unknown-query-param.test.ts's
baseline — no params returns every row (10 rows seeded, total: 10 with no params).
So the real failure mode of a dropped top is an unbounded read, not a truncated
one — the cap the author wrote never happens, and the page pulls every matching row over
the wire. That is a different thing to warn an author about, and the current wording
sends anyone debugging it looking for pagination that is not there.
Suggested wording: "…it is silently dropped. The query then runs as if you had not
written it: an unprefixed filter: comes back unfiltered, and an unprefixed top:
comes back with every matching row, because the list route has no default page size.
There is no error."
Acceptance
- The sample reads
result.data(array arm preserved) and, run against a real adapter,
renders rows. - The Callout states the unbounded-read consequence, sourced from the client/protocol
behaviour above rather than from the old wording. - The named docs gates stay green.
Activity
os-zhuang commentedon Aug 21, 2026
Triage: lands in
content/docs/ui/react-pages.mdx→domain:devx(content/docs/**), type Bug (the published sample renders an empty list and the Callout states the inverse of the measured consequence). Queued. Both halves are docs-only edits with the correct wording already drafted in the body; the docs gate family named in the body is the verification surface. Size/model suggestion: S, sonnet.Generated by Claude Code
claude commentedon Aug 21, 2026
Claim — PM dispatch (devx seat)
c970724d-303c-5614-9d20-a3f92205cfadclaude/issue-10469-react-pages-live-dataPremise re-verified on
origin/main=9185ff0213(fetched 2026-08-21T10:04:48Z).content/docs/ui/react-pages.mdx:147still reads:Array.isArray(result)armdoes not rescue it.
find()resolves to a normalizedQueryResultobject, so the array armnever fires,
result.recordsisundefinedon every call, andrecordsfalls to[]. Thepublished sample renders an empty
<ul>forever, with no error.This is the third instance of one read. It was fixed in
examples/app-showcase/src/ui/pages/crm-workbench.page.ts— whose comment records the symptomverbatim, "the KPI cards silently stuck at 0 even though the ListView beside them showed the
same rows" — and again in
renewals-pipeline.page.tsunder #10288. The doc is the one acustomer copies from, which is why it is the worst of the three to leave.
⛔
content/docs/ui/react-pages.mdxonly. It is not among the 19 files PR #10714 changed, soit is free — but that PR merged recently, so re-derive at your own merge base.
wording must be verified against the route source, not adopted: the Callout claims a dropped
query option means the query comes back "with the default page size", and the card argues the
GET list route has no default page size at all. Confirm that in the route implementation before
rewriting the sentence — a Callout that states the inverse of the measured behaviour is exactly
the defect being fixed, and shipping a second wrong consequence would repeat it.
Generated by Claude Code
claude commentedon Aug 21, 2026
{ "issue": 10469, "status": "done", "branch": "claude/issue-10469-react-pages-live-data", "pr": "https://github.com/objectstack-ai/objectstack/pull/10748", "premise_still_valid": true, "summary": "Both halves landed in content/docs/ui/react-pages.mdx, one file, no other path touched. Half 1: the Live-data sample now reads `result?.data ?? result?.records ?? (Array.isArray(result) ? result : [])` -- canonical first, legacy key as fallback, array arm preserved, the exact shape data-list.tsx:140, record-picker.tsx:133, ObjectRefField.tsx:59 and record-history.tsx:90 all use. QueryResult contract verified in the objectui sibling checkout at /home/user/objectstack-ai/objectui (it IS present): packages/types/src/data.ts:109 declares data/total/page/pageSize/hasMore/cursor with no `records` key, and normalizeQueryResult (packages/data-objectstack/src/index.ts:2774) returns exactly {data,total,page,pageSize,hasMore} -- it CONSUMES the envelope's `records`/`value` and re-emits under `data`. It also folds the raw-array case into that same object, which is why the old `Array.isArray(result)` arm could never fire either: both arms missed, records fell to [], empty <ul> forever. Half 2: the card's page-size claim is CORRECT and was verified at the route, not adopted -- see the open_questions-free measured answer below. Callout rewritten to name the unbounded read. Docs-only, publishes nothing, so no changeset; skip-changeset label applied additively (POST /issues/10748/labels) and READ BACK after the bots ran: ['documentation','size/xs','skip-changeset'] -- the bot labels survived, mine survived.", "measured_answer_callout_page_size": "CARD CONFIRMED: the GET list route has no default page size, so a dropped `top` is an UNBOUNDED read, not a truncated one. Verified at the route implementation, packages/metadata-protocol/src/protocol.ts:8469 `findData` -- the resolver the REST GET list route runs. Paging resolves as `const pageLimit = typeof options.limit === 'number' && options.limit > 0 ? options.limit : undefined;` with no default assigned on any branch, and its own comment states the consequence: \"Without a limit the full result set is returned, so its length already IS the total.\" Earlier in the same function `limit` is coerced on PRESENCE only (`if (options.limit != null) options.limit = Number(options.limit)`), so an absent top stays absent through to engine.find. Measured, not merely read: packages/objectql/src/protocol-unknown-query-param.test.ts:147 `baseline -- no params returns every row` seeds 10 rows and asserts findData({object:'showcase_task'}) resolves total: 10. Corroborated by packages/client/src/index.ts:4475 and the #6485 pin at packages/client/src/client.test.ts:1776. SECOND MEASUREMENT the card did not make: the Callout's \"There is no error\" half is ALSO correct, and for a non-obvious reason worth keeping in the doc -- the drop happens client-side. The adapter's convertQueryParams (objectui packages/data-objectstack/src/index.ts:2976) reads ONLY the $-prefixed slots and builds a fresh options object, so an unprefixed key never reaches the wire. Had it reached the server it would NOT be silent: protocol-unknown-query-param.test.ts:161 pins ?zzzz / ?pageSize / ?page_size / ?perPage as 400 INVALID_FIELD. So the warning is correctly aimed at the client layer and the wording now names the right consequence there.", "before_after": { "sample_before_L147": " const records = Array.isArray(result) ? result : (result && result.records) || [];", "sample_after_L147": " const records = result?.data ?? result?.records ?? (Array.isArray(result) ? result : []);", "callout_before_L157_160": "The `$` prefixes are load-bearing. An unprefixed `top:` or a `filters:` key is not a\nquery option -- it is silently dropped, and the query comes back unfiltered or with the\ndefault page size. There is no error.", "callout_after_L157_162": "The `$` prefixes are load-bearing. An unprefixed `top:` or a `filters:` key is not a\nquery option -- it is silently dropped, and the query runs as if you had not written\nit: a dropped `filters:` comes back unfiltered, and a dropped `top:` comes back with\n**every matching row**, because the list route has no default page size. The failure\nmode is an unbounded read, not a truncated one. There is no error." }, "anchor_counts_on_disk": { "result.records (bare)": "1 -> 0", "result?.records": "0 -> 1", "result?.data": "0 -> 1", "Array.isArray(result)": "1 -> 1 (arm preserved, moved to last position)", "default page size": "1 -> 1, but now inside `no default page size`: 0 -> 1", "every matching row": "0 -> 1" }, "tests": "Docs-only card: no unit suite applies; the docs gate families ARE the verification surface. Gate list derived mechanically, no paths passed: `node scripts/pm/dispatch-gates.mjs` -> exit 0, \"change set derived from git -- 1 path(s) vs merge base 9185ff021\" (committed 1, working tree 0), 13 families. UNION RE-RUN AT THE FINAL COMMIT, sha from that run: git rev-parse --short HEAD = 882bbf7733 (= PR head sha). All 13 derived families plus check:nul-bytes (unconditional on any edit) = 14 runs, 14 green, RED COUNT: 0. Exit codes captured BEFORE any pipe (`cmd > file 2>&1; e=$?` then tail), never a bare $? after a tail. Verdict lines as each gate printed them: check:nul-bytes 'check-nul-bytes: OK (scanned 6231 text file(s) ... no raw ASCII control bytes).' | check:doc-anchors '272 internal #fragment link(s) across 408 source file(s) all resolve to a real heading' | check:doc-authoring 'doc authoring guard: 389 files clean -- no bare metadata literals.' | check:role-word 'check-role-word: OK, no new occurrences of the reserved word.' | check:docs-audit-scope 'docs-accuracy-audit scope is in sync with content/docs/: 189 hand-written doc(s).' | check:docs-redirects 'check-docs-redirects: OK (apps/docs/redirects.mjs: 92 entries ...)' | check:published-readme-links '152 outbound link(s) across 60 published markdown file(s)' | check:cross-package-test-inputs 'OK: 13 package(s) read outside themselves, all declared' (+ the same script run directly) | lint check:doc-formula-expressions '22 record-scoped formula example(s) across 416 files / 1443 TS blocks judged clean' | spec check:empty-state 'all classified (1 closed, 2 open, 4 output, 9 scope)' | spec check:variant-docs '18 discriminated union(s) -- 8 governed, 10 exempt' | spec check:strictness-ledger 'strictness ledger: 61 file(s) across 5 triaged director(ies)' | spec check:liveness 'every governed-type property ... is classified'. Dependency-closure trap hit and handled: `pnpm --filter @objectstack/lint run check:doc-formula-expressions` first died ERR_MODULE_NOT_FOUND on @objectstack/formula/dist/index.mjs; built the closure under the shared lock (`bash scripts/pm/os-verify-lock.sh -c '... --workspace-concurrency=2 --filter \"@objectstack/lint^...\" build'` -> 'VERDICT command-exit 0 - held the lock 133s - waited 220s'), then the gate passed. Script names were checked to exist in each package.json BEFORE running the filtered gates, so no zero-match --filter could exit 0 silently. No ablation was performed: this is a prose/sample docs edit with no guard to prove failable.", "check_skill_examples": "NOT APPLICABLE, and checked rather than assumed -- the brief asked either way. content/docs/ui/react-pages.mdx carries exactly ONE {/* os:check */} marker, at :346 (was :344 pre-edit), directly above a ```typescript fence -- the RenewalsConsolePage example. The Live-data sample is a ```jsx fence at :135-155. packages/spec/scripts/check-skill-examples.ts refuses a marker that is not directly above a ```ts / ```typescript fence (its error text: 'Found an os:check marker not directly above a ```ts / ```typescript fence'), so a jsx block is not even eligible for the gate. My diff's only hunks are @@ -147 +147 @@ and @@ -159,2 +159,4 @@ -- nowhere near the marked block, and the marker/fence pairing was re-confirmed intact after the edit. dispatch-gates did not name the gate either.", "open_questions": [], "out_of_scope_findings": [ "filed as #10750: packages/client-react/src/data-hooks.tsx TSDoc @examples for useQuery (:82) and usePagination (:405) both read `data?.value`, but useQuery's data is PaginatedResult<T> (packages/client/src/index.ts:310) which declares records/total/object/hasMore and no `value`. Worse than the sibling defect: ?. short-circuits only on a nullish data, so once the query resolves `.map` runs on undefined and THROWS. Labels domain:devx + documentation, unassigned, no pm:queue. NOT fixed here -- different file, different package, out of this card's one-file scope.", "filed as #10751: [finding] the #10288 guard examples/app-showcase/test/react-page-adapter-query-contract.test.ts sweeps only kind:'react' pages from the app-showcase page registry, so content/docs react-page samples are invisible to it -- the mechanism by which this identical read survived two code fixes and reached instance three in the doc a customer copies. Proposes extending that guard's population to the docs corpus to close the whole class rather than adding a second scanner. Labels domain:devx + finding, unassigned, no pm:queue.", "NOT a fourth instance (checked, reporting the negative): content/docs/api/client-sdk.mdx:659 reads `data?.records.map(...)`. That is the CLIENT SDK path, not the adapter -- findData returns {object, records, total, hasMore} and PaginatedResult declares `records`, so that read is correct and was left alone." ] }Generated by Claude Code