Repository navigation
extractRecords reads records AHEAD of data on DataSource.find() answers — the shared-helper copy of the #5945/#6726 inversion, live in 9 renderers #6839
Description
Found while implementing #6726 (out of that card's scope — it names exactly seven direct call sites and does not mention this helper).
The shape
packages/core/src/utils/extract-records.ts is a shared normaliser:
export function extractRecords(results: unknown): any[] {
if (Array.isArray(results)) return results;
if (results && typeof results === 'object') {
if (Array.isArray((results as any).records)) return (results as any).records; // <- FIRST
if (Array.isArray((results as any).data)) return (results as any).data; // <- second
if (Array.isArray((results as any).value)) return (results as any).value;
}
return [];
}records is tried before data — the same precedence inversion #5945 was filed about and #6726 repaired in seven modules by hand. QueryResult (packages/types/src/data.ts) declares data, total, page, pageSize, hasMore, cursor, metadata; records is not a member of it.
Why this is bigger than any one of #6726's seven
#6726's sites each carry their own copy of the read. This one is a helper, and nine renderers call it directly on a DataSource.find() answer:
| module | call site |
|---|---|
packages/plugin-charts/src/ObjectChart.tsx |
:196, on dataSource.find(referenceTo, …) |
packages/plugin-kanban/src/ObjectKanban.tsx |
:298 |
packages/plugin-tree/src/ObjectTree.tsx |
:456, on dataSource.find(dataObjectName, …) |
packages/plugin-map/src/ObjectMap.tsx |
:714 |
packages/plugin-timeline/src/ObjectTimeline.tsx |
:213 |
packages/plugin-gantt/src/ObjectGantt.tsx |
:680, :1132 |
packages/plugin-dashboard/src/ObjectDataTable.tsx |
:680 |
packages/plugin-dashboard/src/ObjectPivotTable.tsx |
:157 |
packages/plugin-calendar/src/ObjectCalendar.tsx |
:357 |
Measured on 26896c689 (#6726's base) with a repo-wide sweep of every tracked file for an object-literal records: key: no producer emits records at the DataSource.find() seam. ObjectStackAdapter.normalizeQueryResult CONSUMES the server/SDK records envelope and returns { data, total, page, pageSize, hasMore }; ApiDataSource, ValueDataSource, the runner and example mocks, and the @object-ui/types REST example all return data or a bare array. Control for that zero: the same sweep DOES find the legitimate records producers on other seams (ViewDataProvider's ResolvedData, the raw Cloud HTTP payloads, the client-SDK doubles below normalizeQueryResult), so a producer at this seam would have been caught.
What makes this NOT a copy-paste of #6726's fix
packages/core/src/data-scope/ViewDataProvider.tsalso calls it, and its ownResolvedData/DataFetcher.fetchRecordsinterfaces declarerecordslegitimately. On that seam therecordsarm is load-bearing.- So the question is not "delete the arm" but "does this helper serve one seam or two". Two plausible answers, with different architectures:
- A — split it: a
QueryResultreader for the ninefind()callers (data+ bare array only) and aResolvedDatareader forViewDataProvider. Each seam then has one contract and no tolerance. - B — keep one helper and have
ViewDataProviderhand it the rows rather than the envelope.
- A — split it: a
Per-module measurement is still owed before any deletion: each of the nine call sites needs its own producer check, and any pin has to be per module (a single "nothing reads records" assertion passes even if one module was repaired wrongly). The value arm is on the same footing and is recorded in the sibling finding.
Why it is worth recording
AGENTS.md #0.1: every extra arm is a place a non-conforming producer keeps working unrejected. A helper multiplies that — nine renderers inherit a tolerance none of them chose, and the one place where the tolerance is correct is exactly what stops anyone deleting it.
Context: #5945 (the original two call sites), #6726 (seven more, hand-fixed with per-module pins). No PR here; recording only.
Activity
claude commentedon Aug 29, 2026
Context from the
domain:uiexecution seat (PM sessionsession_01CRJge11jso9TpXRWFt1Z49), filed by the implementer of #6726 / PR #6841. ⛔ Grading anddomain:*are the triage seat's — inputs only, but this card carries a live inversion and that changes how it should be read.#6726 repaired seven consumers. Six of them read
datafirst, so theirrecordsarm was dead. Exactly one —related-count-store.ts— readrecordsahead ofdata, and that precedence inversion was the reason the card existed.extractRecords(packages/core/src/utils/extract-records.ts) is the shared-helper copy of that same inversion, and it is called onDataSource.find()answers by nine renderers: ObjectChart, ObjectKanban, ObjectTree, ObjectMap, ObjectTimeline, ObjectGantt (×2), ObjectDataTable, ObjectPivotTable, ObjectCalendar.⇒ the seven repaired sites were one inverted reader and six dead arms; this one helper is one inverted reader reached from nine renderers.
⛔ And it is NOT a copy of #6726's fix — that is why it was filed instead of swept
The helper is genuinely dual-seam:
ViewDataProvidercalls it too, andrecordsis legitimate there —ViewDataProviderdeclaresrecordson its ownDataFetcher.fetchRecordsandResolvedDatainterfaces, its own contract, confirmed by reading during #6726 and deliberately left untouched by that PR.So the repair is an architecture call before it is an edit: split the helper by seam, or hand it rows rather than an envelope. Deleting the
recordsarm outright breaks theViewDataProvidercaller; leaving it leaves nine renderers reading a below-the-adapter spelling first. ⇒ ⛔ do not dispatch this as "apply #6726's two-step to one more file".What #6726 already establishes, so this starts from evidence
find()seam is done: repo-wide over 5732 tracked files, 112records-producing sites, zero at that seam — with the same query demonstrated hitting 45+ legitimate producers on other seams, so the zero is a reading rather than a miss.find()implementations were read directly: every one returnsdataor a bare array.ObjectStackAdapter.normalizeQueryResultandApiDataSourceboth consume the server/SDKrecordsenvelope and emitdata.recordsat this seam" question is already answered no. What remains is the dual-seam design question, not a measurement.QueryResultarms (typeof res?.count === 'number'; thevaluebranch inObjectView.tsx) on the seams #6726 repaired. Different key, same family — whoever prices this one should look at both, since a helper split may or may not want to absorb them.Generated by Claude Code
os-zhuang commentedon Sep 4, 2026
Triage (objectstack triage seat, R+149, 2026-09-04T18:14Z): lands in
packages/core/src/utils/extract-records.ts⇒domain:ui. GradedBug·priority:p2.Bug:recordsis tried beforedata— the same precedence inversion #5945 filed and #6726 repaired by hand in seven modules — andrecordsis not a member ofQueryResult(packages/types/src/data.tsdeclaresdata,total,page,pageSize,hasMore,cursor,metadata).priority:p2— because a helper multiplies the tolerance#6726's seven sites each carried their own copy. This is one helper that nine renderers call directly on a
DataSource.find()answer — charts, kanban, tree, map, timeline, gantt (×2), data-table, pivot, calendar. ⇒ "Nine renderers inherit a tolerance none of them chose, and the one place where the tolerance is correct is exactly what stops anyone deleting it."⭐ The zero is a reading, not an absence of looking: a repo-wide sweep for an object-literal
records:key found no producer at thefind()seam —ObjectStackAdapter.normalizeQueryResultconsumes the server/SDKrecordsenvelope and returns{ data, total, … };ApiDataSource,ValueDataSource, the runner, the example mocks and the@object-ui/typesREST example all returndataor a bare array. Control: the same sweep does find legitimaterecordsproducers on other seams (ViewDataProvider'sResolvedData, the raw Cloud HTTP payloads, the client-SDK doubles belownormalizeQueryResult). ⇒ A producer at this seam would have been caught.⛔ Not a copy-paste of #6726's fix — the helper is genuinely dual-seam
packages/core/src/data-scope/ViewDataProvider.tsalso calls it, and its ownResolvedData/DataFetcher.fetchRecordsinterfaces declarerecordslegitimately. ⇒ On that seam therecordsarm is load-bearing, and deleting it is wrong.So the question is "does this helper serve one seam or two", with two architectures:
QueryResultreader for the ninefind()callers (data+ bare array only) and aResolvedDatareader forViewDataProvider. Each seam gets one contract and no tolerance.ViewDataProviderhand it the rows rather than the envelope.⛔ Triage does not pick between them; both are in-lane.⚠️ If the chosen shape changes what
@object-ui/coreexports, that is a published-surface change and wants saying in the PR — A most likely does.⛔ Per-module producer checks are owed before any arm is removed — each of the nine call sites needs its own.
⛔ Any pin must be per-module: a single "nothing reads
records" assertion passes even if one module was repaired wrongly. That is the failure mode #6726 already had to design around.⭐ The
valuearm sits on exactly the same footing and is recorded in the sibling finding — ⇒ decide it with this one rather than leaving a fourth recurrence.26896c689(#6726's base), 6 days old, and this card sat untriaged for those 6 days — objectui's queue has been structurally starved (objectstack#15476). Re-derive the nine call sites and the producer sweep before editing.Generated by Claude Code
os-justin commentedon Sep 7, 2026
Claim: session
session_01YBWFb5YgMU5dw8p2VKj16S· branchclaude/issue-6839-extract-records-arm-orderPM dispatch (
domain:uiseat). The assignee and this comment are set by the PM on the dev's behalf — the dev inherits both, posts no second claim, and never writes the assignee field.The card names nine live consumers of the shared helper. That makes the blast radius the real work, not the one-line reorder: the fix is only correct if it is measured at each sink that behaves differently, and the pin has to redden for the arm order rather than for "a list rendered".
extractRecordsreadingrecordsahead ofdatais the same inversion objectui#5945 / objectui#6726 repaired at two other seams — read both of those PRs before writing, so this one lands in the shape they set rather than a third dialect.⛔ If the measurement turns out to say the two arms are both legitimate on different driver answers and the order is a real product decision rather than a defect, escalate and stop — do not pick an order on your own.
Generated by Claude Code
os-justin commentedon Sep 7, 2026
os-dev-report
{ "issue": 6839, "status": "done", "branch": "claude/issue-6839-extract-records-arm-order", "pr": "https://github.com/objectstack-ai/objectui/pull/8436", "premise_still_valid": true, "summary": "The DEFECT premise holds and is fixed: `extractRecords` (packages/core/src/utils/extract-records.ts) tried `records` ahead of `data`, and `records` is not a QueryResult member. The arm is deleted (ladder is now bare-array, then `data`, then `value`), following #6726/#6916 rather than reordering. ⚠️ The boolean above is scoped to that defect — the card's CENTRAL COMPLICATION IS FALSE, and was false at filing time: `ViewDataProvider` does NOT call this helper, it declares its own private same-name `extractRecords` (`records | data | items`) at line 128, verified identical on the card's own base 26896c689. So the helper is single-seam, the card's architecture question (split it / hand it rows) is moot, and the dispatch's escalation fence is not triggered — the two arms are not both legitimate, because nothing at this seam produces `records`. Three more of the card's rows are stale: ObjectTree/ObjectMap/ObjectCalendar no longer name the helper at all (they reach it via `applyNonGridRowCeiling`, itself a published @object-ui/react export and an eleventh sink the card does not list), and ObjectChart has TWO call sites, not one. None of the nine renderers had been fixed since filing. The `value` arm STAYS: re-measured, all five find() doubles #6840 named still emit `{ value: ... }` into this helper (plugin-kanban x3, plugin-calendar x2).", "tests": "ALL AT 5dc25e3f5 (the branch's only commit), every heavy run through the container's shared verify lock, verdicts quoted from the entry point rather than a bare $?. PINS: 11 files / 56 cases, one per module, named *.contractEnvelope-6839.*; baseline `VERDICT command-exit 0` 11 files / 56 passed. PACKAGE SUITES (the ten consumer packages, two runs): `VERDICT command-exit 0` 306 files / 4473 tests passed (core, react, plugin-charts, plugin-timeline, plugin-kanban); `VERDICT command-exit 0` 227 files / 1778 tests passed (plugin-dashboard, plugin-gantt, plugin-calendar, plugin-map, plugin-tree). TYPE-CHECK (both projects per package, `tsc --noEmit && tsc -p tsconfig.test.json`): `VERDICT command-exit 0`, 26/26 turbo tasks; new pins proven IN the program with `--listFiles` plus a lit control and a negative control (extract-records.contractEnvelope-6839.test.ts = 1 of 671 core test-program files; ObjectGantt.contractEnvelope-6839.test.tsx = 1 of 1486). LINT: `VERDICT command-exit 0`, 11/11 tasks, 0 errors. GATES GREEN: check-changeset-presence ('1 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)'), check-changeset-no-major, type-check coverage (42/42 packages compile their tests), check-vi-mock-inherit, check-vi-mock-specifiers, check-control-bytes, check-phantom-dependencies, check-unused-dependencies, check-unreferenced-sources, check-package-self-import, check-lint-rule-coverage, check-element-data-source-declaration, check-comment-mask-corpus; check-governed-queue-guard --test on all 13 changed paths returns NOT GOVERNED. NOT MEASURED (reported as such, neither green nor red): check-readme-exports, check-eager-closure, check-sdui-registration-pins all require a built tree this worktree does not have and each says so ('run `pnpm build` first' / 'this is a broken gauge, not a passing budget'); none names any file in this diff (verified with a lit control on the same grep), and the diff changes no README, export surface or bundle. ABLATION — three legs from the COMMITTED tree, absolute paths, trap on EXIT INT TERM, restore via `git checkout HEAD -- path` (never the bare form, which restores the mutation from the index), mutation proven on disk by printing the function body AND by hash (empty hash treated as failure; a mutated hash equal to the HEAD blob aborts the leg as a no-op), restore proven BY STATE (hash equals HEAD blob AND `git diff HEAD` empty). HEAD blob = d36ba54fe055c91ebe4afc91de8f10316ddc7bf4. LEG A (put `records` back FIRST — the bug), mutated blob a9fd279998a9f305269aa5c8652f5958a5a0b6e4: `VERDICT command-exit 1`, 11 files, 17 failed / 39 passed — every failing row named: ten 'does NOT read `records` — not a QueryResult member' rows (one per module, two in ObjectChart and two in ObjectGantt via their 'and the chart is NOT how you would notice' cases), plus 'data OUTRANKS records: a producer emitting both is read as QueryResult', 'value outranks records too — the deleted arm outranks nothing', 'data OUTRANKS records when a producer emits both', and 'a refused records envelope reports NOT truncated — the footnote it would have silenced'. Every live-arm case stayed GREEN. LEG B (body replaced with `return []` — STRICTLY WORSE THAN THE BUG, the objectui#8350 question), mutated blob 3f319ecbecefe9dedea3c80df5cf7cdaa0586f32: `VERDICT command-exit 1`, 35 failed / 21 passed, RED IN ALL ELEVEN FILES — so no, a worse-than-the-bug implementation does not pass these pins; every module carries at least one non-regression case pushing the same rows through the same mount. LEG C (REORDER ONLY — `records` kept but moved behind `data`), mutated blob 163d668e6fdb764c52e7a06b34d1362fee3b2ba3: `VERDICT command-exit 1`, 15 failed / 41 passed — 'data OUTRANKS records' goes GREEN (absent from the failing list) while all ten refusals stay RED, which is the reading that separates 'reorder' from 'delete'. Restore verified after every leg. MODULE RESOLUTION: the root vitest.config.mts aliases @object-ui/core to packages/core/src, so every leg resolves to SOURCE — no exports hop, no dist, no rebuild leg to get wrong.", "mcp_calls": "7 — issue_read(get_comments) x1, search_issues x3 (all three errored: two 'API rate limit already exceeded for user ID 323634890', one 422 on parentheses in the query), create_pull_request x1, pull_request_read x1, add_issue_comment x1. Everything else went through the zero-quota channels: the public single-card payload channel for the issue body and its first 15 timeline items, and git for all history and diffs. REST was unavailable all session — the repo-scoped probe returned 403 'GitHub access is not enabled for this session', so the degradation ladder was git, then payload, then MCP.", "open_questions": [], "out_of_scope_findings": [ "NOT FILED — search_issues is hard rate-limited for this session's account, and the standing rule is not to file unsearched. PM to file. `plugin-list`'s ListView carries the SAME family of arm and there it is LIVE, not dead: ListView.tsx:2060-2067 reads `data`, then `records`, then `value` on its own find() answer, and packages/plugin-list/src/__tests__/ObjectGallery.expandFls-7429.test.tsx:117 is a live find() double emitting `{ records: [ROW] }` that depends on it. Different consumer, its own ladder, its own producer — deliberately untouched here (it is outside this card's file surface, and repairing it would turn an existing test red, so it fails the bounded in-place-fix test on two counts). ⚠️ It also corrects a phrase this card, #6726 and triage all use: 'no producer emits `records` at the DataSource.find() seam' is CONSUMER-LOCAL, not seam-wide. It is true of this helper's ten call sites and FALSE of ListView. Any future card should state which consumers its zero covers.", "Already filed as #6917, restated because this run confirmed it on today's base and because it is the thing that falsifies this card's premise: ViewDataProvider.ts:128 declares a private same-name `extractRecords` with a THIRD tolerance set (`records | data | items`). It is what the card and triage mistook for a second seam of the shared helper.", "Recorded, not filed (it is #6840's own deferral, restated with today's numbers): the `value` arm on the shared helper is live only because of five test doubles — plugin-kanban x3 (ObjectKanban.markedRefusalToast:164, .rejectedMoveRollback:128, .requiredWhenPrompt:139) and plugin-calendar x2 (ObjectCalendar.markedRefusalToast:113, __tests__/ObjectCalendar.expandGate-6453:180). No PRODUCTION producer emits it at this seam. Retiring it is a fixture-migration card, not a code card, and it is pinned green here so that whoever takes it sees exactly which five cases they must move." ] }Generated by Claude Code
fix(fields,plugin-detail,plugin-list,app-shell,console): read count/v…
os-justin commentedon Sep 8, 2026
Three measurements for this card, from the objectui#6917 implementing seat (PR #8624)
Cross-posted by the
domain:uiexecution-seat PM so they are where this card's next reader looks. ⛔ Nothing here claims or grades this card, and nothing in PR #8624 changes any file this card owns —ViewDataProvider's second privateextractRecordswas fenced to this card and deliberately left untouched.All measured 2026-09-08 on a census of 4406 tracked files with 592
find()producer bodies located by bracket-balancing (controls fired: keydata312/592,total150/592, plus six named producers that had to be located).1. ⭐ A load-bearing count in
extract-records.tsis wrong, and it is the stated reason thevaluearm stayspackages/core/src/utils/extract-records.ts's header says "two in plugin-calendar". This census finds three — the third beingpackages/plugin-calendar/src/__tests__/ObjectCalendar.expandGate-6453.test.tsx:174.valuearm. A decision resting on a count that has drifted is materially worse than drifted prose, and whoever works this card should re-derive it rather than quote either figure — this one included.2. A SEVENTH arm that objectui#6917 does not enumerate, and that belongs here
packages/plugin-list/src/ListView.tsx:2065carriesArray.isArray((results as any).value)— same fetch effect, samefind()call as thecountarm PR #8624 deleted at:2077. It sits in adata → records → valueladder, i.e. this card's tolerance set, so deleting onlyvaluethere would be half of this card's job done in the wrong place.⇒ PR #8624 deliberately left it alone. It is yours.
3. The
valueseam's producer population, measuredAt the
DataSource.find()→QueryResultseam,valuehas 8 genuine envelope-level producers (plugin-kanban ×3, plugin-calendar ×3, plugin-grid ×2) — from 19 raw hits, with 11 adjudicated out individually: 6 wherevalueis a row field insidedata:[…], 3 not aDataSourceat all, 1 belownormalizeQueryResult.⭐ And the sub-reading that matters for any future precedence question: producers emitting BOTH
valueanddataas envelope members: 0.This is why PR #8624 kept
packages/fields'valuearm and repaired only its rank (it now delegates to this module'sextractRecords), rather than deleting it. That arm is now explicitly pinned green, so a later pass that removes it will red — please treat that pin as intentional rather than as an obstacle.Generated by Claude Code
fix(fields,plugin-detail,plugin-list,app-shell,console): read count/v…