You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
examples: the showcase CRM workbench react page passes limit: 200 to adapter.find — same dropped-option class as #10288, a second file that card's fix does not reach #10479
Found while re-measuring the adapter.find claims for objectui#5413 (the react-pages
guide's Live data sample). Filing rather than fixing — examples/** is outside that
card's file surface, which is the objectui guide only.
Same defect class as #10288, but a different file, and #10288's stated fix explicitly
does not reach it ("top: 500 → $top: 500 in both calls. Two characters; no other
change." — both calls being in renewals-pipeline.page.ts).
The call
examples/app-showcase/src/ui/pages/crm-workbench.page.ts, in refreshStats:
constall=awaitadapter.find('showcase_project',{limit: 200});constrows=Array.isArray(all) ? all : (all&&(all.data||all.records))||[];setStats({total: rows.length,active: rows.filter((r)=>r.status==='active').length});
limit is not a query option. It is arguably worse than #10288's top, because there is
no $limit either — the key that means this is $top.
Evidence
QueryParams (objectui packages/types/src/data.ts) declares only $-prefixed keys: $select, $filter, $orderby, $skip, $top, $expand, $search, $searchFields, $count. ObjectStackAdapter.find builds its wire options in convertQueryParams
(objectui packages/data-objectstack/src/index.ts), which reads the prefixed spellings
into a fresh options object:
Nothing copies an unprefixed key across, so limit reaches no branch and is dropped
silently.
Counter-probe — the two other .limit reads in that file are different methods with
their own option types (searchAll(query, options) at :1721 and aggregate(resource, params) at :4030), not the find route. find itself is find → convertQueryParams → client.data.find.
The visible consequence
The KPI cards this effect feeds ("total" / "active") are computed as rows.length over
the returned page. With limit dropped the query returns the backend's default page
size, so both counts are silently capped and read as real numbers — the same failure
shape #10288 describes for the renewals KPI strip.
Note the page's own docstring already records one round of this bug class (reading .records instead of .data), which is why the defensive Array.isArray(all) ? …
fallback is there. The option key was missed in that pass.
Fix
limit: 200 → $top: 200. One call site.
Worth doing in the same sweep as #10288 so the showcase's react pages stop teaching the
unprefixed spelling by example — a copy-paste from either page propagates it.
Triage: lands in examples/app-showcase/src/ui/pages/crm-workbench.page.ts (one call site, limit: 200 → $top: 200) → domain:devx, pm:queue, type Bug — routing mirrors #10288 (same class, same lane, bug + domain:devx), whose fix explicitly does not reach this file. Parent link to #10288 already set by the filer.
Dispatch notes: the evidence chain (QueryParams $-prefixed keys only; convertQueryParams copies nothing unprefixed) is already verified in the card against objectui sources. Counter-probe done by filer: the other two .limit reads in the file are different methods with their own option types — do not "fix" those. KPI consequence (silently capped counts) makes this a real example-teaching defect, not polish.
Size/model suggestion: S mechanical, mode:subagent, sonnet.
⭐ Same defect class as #10288, different file — #10288's own stated fix ("top: 500 → $top: 500 in both calls") is scoped to renewals-pipeline.page.ts and does not reach this one.
⛔ Governed surface out of scope (docs/adr/**, .claude/**, skills/**, AGENTS.md, CLAUDE.md, content/docs/releases/**) and the #9465 epic fence (.changeset/ tooling, cut-rc.yml, release.yml, root package.json). ⚠️ This repo has no formatter of record (no format/prettier script, prettier not in devDeps, no .prettierrc/.editorconfig) — ⛔ do not run prettier --write (#10622).
{
"issue": 10479,
"status": "done",
"branch": "claude/issue-10479-crm-workbench-limit",
"pr": null,
"premise_still_valid": false,
"summary": "No code written: the fix is ALREADY on origin/main and has been since commit 6cca75c3a3 (PR #10481, merged 2026-08-21T01:39:27Z, ~5.5h before this dispatch). crm-workbench.page.ts:48 today reads `adapter.find('showcase_project', { $top: 200 })`, and the landed fix is a SUPERSET of the one this card asks for: it also takes the headline count off the envelope's `total` rather than `rows.length`, because `$top: 200` alone would have CREATED the silently-capped count this card describes as the harm. The card's central scoping claim is false: #10288's fix DOES reach this file. PR #10481 closed #10288 but swept both react pages, and its own census table lists `examples/app-showcase/src/ui/pages/crm-workbench.page.ts:39 limit` as a site it fixed. Verified $top is the live spelling at the pinned objectui SHA (not assumed, not secondhand). Nothing left to implement; recommend closing #10479 as already-fixed by #10481. NOTE for PM: the protocol probe branch `claude/issue-10479-crm-workbench-limit` was pushed and is 0 commits ahead of origin/main; `git push origin --delete` was blocked by the auto-mode classifier, so it needs deleting by hand.",
"tests": "HEAD = origin/main 6b0be02209, zero commits ahead, zero uncommitted files — so there is no diff to gate, ablate, or changeset. (1) Gate union, `node scripts/pm/dispatch-gates.mjs` with NO paths passed: exit 2, its own verdict line `dispatch-gates: this branch changes nothing against 'origin/main' (merge base 6b0be0220) — nothing to derive.` No gate family to run. (2) Dependency closure built first: `pnpm --workspace-concurrency=2 --filter '@objectstack/example-showcase^...' build` via scripts/pm/os-verify-lock.sh -> `os-verify-lock: VERDICT command-exit 0 · held the lock 369s (6m09s) · waited 286s (4m46s)`. (3) Showcase suite: `pnpm --filter @objectstack/example-showcase test -- --maxWorkers=2` via the lock -> `os-verify-lock: VERDICT command-exit 0 · held the lock 26s · waited 265s (4m25s)`, vitest `Test Files 24 passed (24)` / `Tests 364 passed (364)`. That is EXACTLY the 24/364 baseline the brief quoted; it did not move, as expected with no diff. (4) Regression pin executed verbosely: `vitest run test/react-page-adapter-query-contract.test.ts --reporter=verbose` via the lock -> `VERDICT command-exit 0`, `Tests 13 passed (13)`, including `showcase_crm_workbench passes only $-prefixed query options` (green — this card's exact target is pinned) and `the scanners fire on a known-bad source (positive control)`, whose synthetic contains `adapter.find('showcase_invoice', { limit: 200 })` and asserts the scanner reports ['top','limit'] — so the `limit` key IS executed-detected as dropped-class, not merely read about. No ablation was run: there is no change of mine to ablate, and mutating already-merged main to re-demonstrate a fix another PR already ablated (its Legs A and B are recorded in #10481) would be work for its own sake.",
"open_questions": [],
"out_of_scope_findings": []
}
Verification of $top at the pinned objectui version — confirmed, not assumed
The brief asked me not to assume this. objectui is not a typed npm dep here (@objectstack/console ships a prebuilt SPA), and there was no objectui checkout in this container at the pin, so I fetched the pinned commit itself and read it:
packages/types/src/data.ts @ that SHA — QueryParams declares $top?: number; every declared key is $-prefixed. There is no $limit and no limit.
packages/data-objectstack/src/index.ts @ that SHA — private convertQueryParams(params?: QueryParams): ObjectStackQueryOptions starts const options: ObjectStackQueryOptions = {} (a fresh object) and the only assignment reaching options.top is:
No branch reads an unprefixed key, and nothing spreads params into options. So { limit: 200 } reaches no branch — dropped. $top is the live spelling at the pinned version. The card's evidence chain is accurate; only its scoping and its consequence are not.
Read-only: I fetched the pinned commit into the pre-existing clone at /home/user/objectstack-ai/objectui (git fetch --depth 1 origin <sha>), which adds objects and moves no refs. No checkout, no branch, no edit — the worktree rule was not engaged because objectui was never written to.
The reproduction, and which half is executed vs. read
Executed: that limit is recognised as a dropped-class key, and that the current crm-workbench source passes the $-prefix contract — both are live assertions in the verbose run above.
Read, not executed: that the key is dropped on the wire. That claim rests on reading convertQueryParams at the exact pin (quoted above). Executing it would mean building objectui from source purely to re-derive a behaviour PR fix(example-showcase): make the react rollups use the adapter's query contract #10481 already measured end-to-end (its BEFORE/AFTER shows $top seen by the adapter: UNDEFINED vs 500). Stating the split explicitly, as asked.
Pre-fix source for the record — git show 6cca75c3a3^:…/crm-workbench.page.ts line 39: const all = await adapter.find('showcase_project', { limit: 200 });, line 41: setStats({ total: rows.length, … }).
Does anything gate this class? Yes — and the wider question was already ruled on
The brief said to file if nothing gates it. Something does, so filing would have duplicated a card that was deliberately closed:
In-repo regression pin:examples/app-showcase/test/react-page-adapter-query-contract.test.ts, added by fix(example-showcase): make the react rollups use the adapter's query contract #10481, sweeps everykind:'react' page the app ships and asserts every key sent to adapter.find/findOne starts with $. It duplicates no key table — the assertion is the prefix rule itself. It carries extraction, census and positive controls. All 3 react pages in examples/** are covered; there are no kind:'react' pages in app-crm, app-todo or embed-objectql.
Types and lint structurally cannot catch it here: a react page's source is a template-literal string of runtime metadata, so tsc never sees it and ESLint never parses it. Separately, this repo's ESLint is not type-aware at all (measured and documented in the header of scripts/check-query-options-erasure-ratchet.mjs).
check:query-options-erasure is not this class — it ratchets any-erasure of engine query-option types, not key spelling.
The card's stated one-line fix was insufficient.$top: 200 alone would have created the capped-count defect the card names as the harm, because rows.length under a cap is a page length. The landed fix takes total from the envelope.
Closing as already fixed — by PR #10481, merged 2026-08-21T01:39:27Z, ~5.5 hours before this seat dispatched it.
⛔ This dispatch was my error. The card states that #10288's fix "explicitly does not reach it", and I carried that claim into the brief without checking it. It is false. PR #10481 closed #10288 but swept both react pages — its file list is crm-workbench.page.ts, renewals-pipeline.page.ts, a contract test and a changeset — and its own census table names examples/app-showcase/src/ui/pages/crm-workbench.page.ts:39 limit as a site it fixed.
Verified against origin/main directly, not from the report:
examples/app-showcase/src/ui/pages/crm-workbench.page.ts
40: // The cap is $top, not 'limit': QueryParams declares only $-prefixed keys
41: // and the adapter copies only those, so a bare 'limit' is dropped without
48: const all = await adapter.find('showcase_project', { $top: 200 });
That is the precise irony of tonight's work: I have spent the round crediting dev seats for checking a handed-down claim instead of adopting it, and then adopted one myself. A card's scoping sentence is a claim like any other, and "this fix does not reach my file" is exactly the kind that decays the moment a broader PR lands. The cost was a full agent run.
⭐ And the landed fix is a SUPERSET — the two-character fix I briefed would have introduced a defect
The dispatched seat measured why #10481 did more than swap the key:
it also takes the headline count off the envelope's total rather than rows.length, because $top: 200 alone would have CREATED the silently-capped count this card describes as the harm
⇒ Had this been unfixed and had the seat followed my brief literally ("the minimal correct fix", framed after #10288's "two characters; no other change"), the result would have been a page whose stat tile silently reports at most 200 — the same class of quiet wrongness, arriving through the fix. Worth recording beyond this card: a minimal fix is only minimal with respect to a measured harm, and here the harm moved when the key was corrected.
What the seat did instead of writing code
Correctly: nothing. Zero commits, zero uncommitted files, and it said so rather than manufacturing a diff. It also verified $top is the live spelling at the pinned objectui SHA rather than trusting the card's secondhand quote of QueryParams, and ran the existing regression pin verbosely — react-page-adapter-query-contract.test.ts, 13 passed, including showcase_crm_workbench passes only $-prefixed query options and a positive control whose synthetic carries adapter.find('showcase_invoice', { limit: 200 }) and asserts the scanner reports ['top','limit']. ⇒ the limit key is executed-detected as dropped-class, not merely read about. So the class is pinned, not just this instance.
dispatch-gates with no paths returned exit 2 — this branch changes nothing against 'origin/main', which is the honest verdict for an empty diff.
Housekeeping: pm:dispatched stripped and the assignee cleared. The probe branch claude/issue-10479-crm-workbench-limit is ahead=0; deleting it returned 403 from this session, so it needs removing by hand.
Activity
os-zhuang commentedon Aug 21, 2026
Triage: lands in
examples/app-showcase/src/ui/pages/crm-workbench.page.ts(one call site,limit: 200→$top: 200) →domain:devx,pm:queue, type Bug — routing mirrors #10288 (same class, same lane,bug+domain:devx), whose fix explicitly does not reach this file. Parent link to #10288 already set by the filer.Dispatch notes: the evidence chain (QueryParams
$-prefixed keys only;convertQueryParamscopies nothing unprefixed) is already verified in the card against objectui sources. Counter-probe done by filer: the other two.limitreads in the file are different methods with their own option types — do not "fix" those. KPI consequence (silently capped counts) makes this a real example-teaching defect, not polish.Size/model suggestion: S mechanical,
mode:subagent, sonnet.Generated by Claude Code
claude commentedon Aug 21, 2026
Claim:
/pm-dispatchdomain:devx execution seat.Session:
session_01DdCnBGcHeufjrq7drTD3wtBranch:
claude/issue-10479-crm-workbench-limitWorktree:
objectstack-issue-10479Domain:
domain:devxFile surface:
examples/app-showcase/src/ui/pages/crm-workbench.page.ts⭐ Same defect class as #10288, different file — #10288's own stated fix ("
top: 500→$top: 500in both calls") is scoped torenewals-pipeline.page.tsand does not reach this one.⛔ Governed surface out of scope (
⚠️ This repo has no formatter of record (no
docs/adr/**,.claude/**,skills/**,AGENTS.md,CLAUDE.md,content/docs/releases/**) and the #9465 epic fence (.changeset/tooling,cut-rc.yml,release.yml, rootpackage.json).format/prettierscript, prettier not in devDeps, no.prettierrc/.editorconfig) — ⛔ do not runprettier --write(#10622).Generated by Claude Code
os-zhuang commentedon Aug 21, 2026
{ "issue": 10479, "status": "done", "branch": "claude/issue-10479-crm-workbench-limit", "pr": null, "premise_still_valid": false, "summary": "No code written: the fix is ALREADY on origin/main and has been since commit 6cca75c3a3 (PR #10481, merged 2026-08-21T01:39:27Z, ~5.5h before this dispatch). crm-workbench.page.ts:48 today reads `adapter.find('showcase_project', { $top: 200 })`, and the landed fix is a SUPERSET of the one this card asks for: it also takes the headline count off the envelope's `total` rather than `rows.length`, because `$top: 200` alone would have CREATED the silently-capped count this card describes as the harm. The card's central scoping claim is false: #10288's fix DOES reach this file. PR #10481 closed #10288 but swept both react pages, and its own census table lists `examples/app-showcase/src/ui/pages/crm-workbench.page.ts:39 limit` as a site it fixed. Verified $top is the live spelling at the pinned objectui SHA (not assumed, not secondhand). Nothing left to implement; recommend closing #10479 as already-fixed by #10481. NOTE for PM: the protocol probe branch `claude/issue-10479-crm-workbench-limit` was pushed and is 0 commits ahead of origin/main; `git push origin --delete` was blocked by the auto-mode classifier, so it needs deleting by hand.", "tests": "HEAD = origin/main 6b0be02209, zero commits ahead, zero uncommitted files — so there is no diff to gate, ablate, or changeset. (1) Gate union, `node scripts/pm/dispatch-gates.mjs` with NO paths passed: exit 2, its own verdict line `dispatch-gates: this branch changes nothing against 'origin/main' (merge base 6b0be0220) — nothing to derive.` No gate family to run. (2) Dependency closure built first: `pnpm --workspace-concurrency=2 --filter '@objectstack/example-showcase^...' build` via scripts/pm/os-verify-lock.sh -> `os-verify-lock: VERDICT command-exit 0 · held the lock 369s (6m09s) · waited 286s (4m46s)`. (3) Showcase suite: `pnpm --filter @objectstack/example-showcase test -- --maxWorkers=2` via the lock -> `os-verify-lock: VERDICT command-exit 0 · held the lock 26s · waited 265s (4m25s)`, vitest `Test Files 24 passed (24)` / `Tests 364 passed (364)`. That is EXACTLY the 24/364 baseline the brief quoted; it did not move, as expected with no diff. (4) Regression pin executed verbosely: `vitest run test/react-page-adapter-query-contract.test.ts --reporter=verbose` via the lock -> `VERDICT command-exit 0`, `Tests 13 passed (13)`, including `showcase_crm_workbench passes only $-prefixed query options` (green — this card's exact target is pinned) and `the scanners fire on a known-bad source (positive control)`, whose synthetic contains `adapter.find('showcase_invoice', { limit: 200 })` and asserts the scanner reports ['top','limit'] — so the `limit` key IS executed-detected as dropped-class, not merely read about. No ablation was run: there is no change of mine to ablate, and mutating already-merged main to re-demonstrate a fix another PR already ablated (its Legs A and B are recorded in #10481) would be work for its own sake.", "open_questions": [], "out_of_scope_findings": [] }Verification of
$topat the pinned objectui version — confirmed, not assumedThe brief asked me not to assume this. objectui is not a typed npm dep here (
@objectstack/consoleships a prebuilt SPA), and there was no objectui checkout in this container at the pin, so I fetched the pinned commit itself and read it:.objectui-sha=9a3daf8d37ad973a621e5edd276fe32467f90684packages/types/src/data.ts@ that SHA —QueryParamsdeclares$top?: number; every declared key is$-prefixed. There is no$limitand nolimit.packages/data-objectstack/src/index.ts@ that SHA —private convertQueryParams(params?: QueryParams): ObjectStackQueryOptionsstartsconst options: ObjectStackQueryOptions = {}(a fresh object) and the only assignment reachingoptions.topis:No branch reads an unprefixed key, and nothing spreads
paramsintooptions. So{ limit: 200 }reaches no branch — dropped.$topis the live spelling at the pinned version. The card's evidence chain is accurate; only its scoping and its consequence are not.Read-only: I fetched the pinned commit into the pre-existing clone at
/home/user/objectstack-ai/objectui(git fetch --depth 1 origin <sha>), which adds objects and moves no refs. No checkout, no branch, no edit — the worktree rule was not engaged because objectui was never written to.The reproduction, and which half is executed vs. read
limitis recognised as a dropped-class key, and that the currentcrm-workbenchsource passes the$-prefix contract — both are live assertions in the verbose run above.convertQueryParamsat the exact pin (quoted above). Executing it would mean building objectui from source purely to re-derive a behaviour PR fix(example-showcase): make the react rollups use the adapter's query contract #10481 already measured end-to-end (its BEFORE/AFTER shows$top seen by the adapter: UNDEFINEDvs500). Stating the split explicitly, as asked.git show 6cca75c3a3^:…/crm-workbench.page.tsline 39:const all = await adapter.find('showcase_project', { limit: 200 });, line 41:setStats({ total: rows.length, … }).Does anything gate this class? Yes — and the wider question was already ruled on
The brief said to file if nothing gates it. Something does, so filing would have duplicated a card that was deliberately closed:
examples/app-showcase/test/react-page-adapter-query-contract.test.ts, added by fix(example-showcase): make the react rollups use the adapter's query contract #10481, sweeps everykind:'react'page the app ships and asserts every key sent toadapter.find/findOnestarts with$. It duplicates no key table — the assertion is the prefix rule itself. It carries extraction, census and positive controls. All 3 react pages inexamples/**are covered; there are nokind:'react'pages inapp-crm,app-todoorembed-objectql.sourceis a template-literal string of runtime metadata, sotscnever sees it and ESLint never parses it. Separately, this repo's ESLint is not type-aware at all (measured and documented in the header ofscripts/check-query-options-erasure-ratchet.mjs).check:query-options-erasureis not this class — it ratchetsany-erasure of engine query-option types, not key spelling.QueryParamsis objectui's contract, so a checker here would hard-code another repo's key list and become a second source of truth for it (Prime Directive Add comprehensive test suite for Zod schema validation #12). The rule belongs beside objectui's existingeslint-rules/no-query-params-under-options.js, which already gates the sibling half ({ options: { $top } }). That extension was filed as [objectui] four liveadapter.findcalls pass an unprefixed query option — andno-query-params-under-optionsgates only the sibling half of the class #10470 and closed as moved to objectui#5458, where it is queued.What in the card / brief turned out to be false
crm-workbench.page.tspasseslimit: 200" — false atorigin/main; it reads$top: 200, and has since 6cca75c.top: 500toadapter.find, which is not a query option — the rollup silently reads only the first page #10288's fix does not reach this one" (card and brief, both starred) — false. fix(example-showcase): make the react rollups use the adapter's query contract #10481 closed examples: the showcase renewals react page passestop: 500toadapter.find, which is not a query option — the rollup silently reads only the first page #10288 and fixed both files; its census names this file and key.packages/client/src/index.ts:4475— "The GET list route has no default page size, so an absenttopreturns the ENTIRE match set" — pinned inpackages/client/src/client.test.ts([finding][client]data.find's pagination params are emitted on truthiness, so{ limit: 0 }is dropped exactly like thelimitbug #6322 fixed #6485). The real harm of the dropped key was an unbounded read, not a truncated one. The same misstatement is separately tracked in docs(ui): the react-pages live-data sample readsresult.records, which is always undefined — and its Callout misstates what a dropped option does #10469 for the docs Callout.$top: 200alone would have created the capped-count defect the card names as the harm, becauserows.lengthunder a cap is a page length. The landed fix takestotalfrom the envelope.limit: 200toadapter.find— same dropped-option class as #10288, a second file that card's fix does not reach #10479 was filed 00:29:29Z; fix(example-showcase): make the react rollups use the adapter's query contract #10481 was opened 00:30:17Z — 48 seconds later — and merged 01:39:27Z. Triage at 02:25Z and dispatch at 07:12Z both post-date the merge. The card was overtaken by a PR it did not know about; agit log -Son the file at claim time is what would have caught it.24 passed / 364 passedbaseline — accurate, reproduced exactly.Generated by Claude Code
Generated by Claude Code
claude commentedon Aug 21, 2026
Closing as already fixed — by PR #10481, merged 2026-08-21T01:39:27Z, ~5.5 hours before this seat dispatched it.
⛔ This dispatch was my error. The card states that #10288's fix "explicitly does not reach it", and I carried that claim into the brief without checking it. It is false. PR #10481 closed #10288 but swept both react pages — its file list is
crm-workbench.page.ts,renewals-pipeline.page.ts, a contract test and a changeset — and its own census table namesexamples/app-showcase/src/ui/pages/crm-workbench.page.ts:39 limitas a site it fixed.Verified against
origin/maindirectly, not from the report:That is the precise irony of tonight's work: I have spent the round crediting dev seats for checking a handed-down claim instead of adopting it, and then adopted one myself. A card's scoping sentence is a claim like any other, and "this fix does not reach my file" is exactly the kind that decays the moment a broader PR lands. The cost was a full agent run.
⭐ And the landed fix is a SUPERSET — the two-character fix I briefed would have introduced a defect
The dispatched seat measured why #10481 did more than swap the key:
⇒ Had this been unfixed and had the seat followed my brief literally ("the minimal correct fix", framed after #10288's "two characters; no other change"), the result would have been a page whose stat tile silently reports at most 200 — the same class of quiet wrongness, arriving through the fix. Worth recording beyond this card: a minimal fix is only minimal with respect to a measured harm, and here the harm moved when the key was corrected.
What the seat did instead of writing code
Correctly: nothing. Zero commits, zero uncommitted files, and it said so rather than manufacturing a diff. It also verified
$topis the live spelling at the pinned objectui SHA rather than trusting the card's secondhand quote ofQueryParams, and ran the existing regression pin verbosely —react-page-adapter-query-contract.test.ts, 13 passed, includingshowcase_crm_workbench passes only $-prefixed query optionsand a positive control whose synthetic carriesadapter.find('showcase_invoice', { limit: 200 })and asserts the scanner reports['top','limit']. ⇒ thelimitkey is executed-detected as dropped-class, not merely read about. So the class is pinned, not just this instance.dispatch-gateswith no paths returnedexit 2 — this branch changes nothing against 'origin/main', which is the honest verdict for an empty diff.Housekeeping:
pm:dispatchedstripped and the assignee cleared. The probe branchclaude/issue-10479-crm-workbench-limitisahead=0; deleting it returned 403 from this session, so it needs removing by hand.Generated by Claude Code