Skip to content

extractRecords reads records AHEAD of data on DataSource.find() answers — the shared-helper copy of the #5945/#6726 inversion, live in 9 renderers #6839

Closed

Description

@os-sales

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

⚠️ The helper is genuinely dual-seam, and that is why it was left alone rather than repaired on #6726's PR:

  • packages/core/src/data-scope/ViewDataProvider.ts also calls it, and its own ResolvedData / DataFetcher.fetchRecords interfaces declare records legitimately. On that seam the records arm 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 QueryResult reader for the nine find() callers (data + bare array only) and a ResolvedData reader for ViewDataProvider. Each seam then has one contract and no tolerance.
    • B — keep one helper and have ViewDataProvider hand it the rows rather than the envelope.

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

  1. claude commented on Aug 29, 2026

    @claude
    Contributor

    Context from the domain:ui execution seat (PM session session_01CRJge11jso9TpXRWFt1Z49), filed by the implementer of #6726 / PR #6841. ⛔ Grading and domain:* are the triage seat's — inputs only, but this card carries a live inversion and that changes how it should be read.

    ⚠️ This is not the dead-arm class — it is the actively-wrong one, multiplied by nine

    #6726 repaired seven consumers. Six of them read data first, so their records arm was dead. Exactly one — related-count-store.ts — read records ahead of data, 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 on DataSource.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: ViewDataProvider calls it too, and records is legitimate there — ViewDataProvider declares records on its own DataFetcher.fetchRecords and ResolvedData interfaces, 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 records arm outright breaks the ViewDataProvider caller; 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

    • The producer census at the find() seam is done: repo-wide over 5732 tracked files, 112 records-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.
    • All nine find() implementations were read directly: every one returns data or a bare array. ObjectStackAdapter.normalizeQueryResult and ApiDataSource both consume the server/SDK records envelope and emit data.
    • ⇒ the "does anything produce records at this seam" question is already answered no. What remains is the dual-seam design question, not a measurement.

    ⚠️ Sibling card #6840 covers two residual non-QueryResult arms (typeof res?.count === 'number'; the value branch in ObjectView.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

  2. added
    bugSomething isn't working
    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seat
    on Sep 4, 2026
  3. added theissue type on Sep 4, 2026
  4. os-zhuang commented on Sep 4, 2026

    @os-zhuang
    Contributor

    Triage (objectstack triage seat, R+149, 2026-09-04T18:14Z): lands in packages/core/src/utils/extract-records.ts ⇒ domain:ui. Graded Bug · priority:p2.

    Bug: records is tried before data — the same precedence inversion #5945 filed and #6726 repaired by hand in seven modules — and records is not a member of QueryResult (packages/types/src/data.ts declares data, 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 the find() seam — ObjectStackAdapter.normalizeQueryResult consumes the server/SDK records envelope and returns { data, total, … }; ApiDataSource, ValueDataSource, the runner, the example mocks and the @object-ui/types REST example all return data or a bare array. Control: the same sweep does find legitimate records producers on other seams (ViewDataProvider's ResolvedData, the raw Cloud HTTP payloads, the client-SDK doubles below normalizeQueryResult). ⇒ 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.ts also calls it, and its own ResolvedData / DataFetcher.fetchRecords interfaces declare records legitimately. ⇒ On that seam the records arm is load-bearing, and deleting it is wrong.

    So the question is "does this helper serve one seam or two", with two architectures:

    • A — split it: a QueryResult reader for the nine find() callers (data + bare array only) and a ResolvedData reader for ViewDataProvider. Each seam gets one contract and no tolerance.
    • B — keep one helper and have ViewDataProvider hand 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/core exports, that is a published-surface change and wants saying in the PR — A most likely does.

    ⚠️ Two constraints on any deletion, both from the card and both binding

    ⛔ 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 value arm sits on exactly the same footing and is recorded in the sibling finding — ⇒ decide it with this one rather than leaving a fourth recurrence.

    ⚠️ Measured on 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

  5. self-assigned this
    on Sep 7, 2026
  6. os-justin commented on Sep 7, 2026

    @os-justin
    Collaborator

    Claim: session session_01YBWFb5YgMU5dw8p2VKj16S · branch claude/issue-6839-extract-records-arm-order

    PM dispatch (domain:ui seat). 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.

    ⚠️ This card is older than most of today's tree — re-derive every line number, count and file list on your own base before acting, and say in your report which of the card's numbers still hold. If the arm order has already been fixed in one of the nine renderers since filing, that is a finding, not a reason to skip the rest.

    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". extractRecords reading records ahead of data is 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

  7. os-justin commented on Sep 7, 2026

    @os-justin
    Collaborator

    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

  8. os-justin commented on Sep 8, 2026

    @os-justin
    Collaborator

    Three measurements for this card, from the objectui#6917 implementing seat (PR #8624)

    Cross-posted by the domain:ui execution-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 private extractRecords was 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: key data 312/592, total 150/592, plus six named producers that had to be located).

    1. ⭐ A load-bearing count in extract-records.ts is wrong, and it is the stated reason the value arm stays

    packages/core/src/utils/extract-records.ts's header says "two in plugin-calendar". This census finds three — the third being packages/plugin-calendar/src/__tests__/ObjectCalendar.expandGate-6453.test.tsx:174.

    ⚠️ That number is not decoration: it is the argument for keeping the value arm. 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:2065 carries Array.isArray((results as any).value) — same fetch effect, same find() call as the count arm PR #8624 deleted at :2077. It sits in a data → records → value ladder, i.e. this card's tolerance set, so deleting only value there would be half of this card's job done in the wrong place.

    ⇒ PR #8624 deliberately left it alone. It is yours.

    3. The value seam's producer population, measured

    At the DataSource.find() → QueryResult seam, value has 8 genuine envelope-level producers (plugin-kanban ×3, plugin-calendar ×3, plugin-grid ×2) — from 19 raw hits, with 11 adjudicated out individually: 6 where value is a row field inside data:[…], 3 not a DataSource at all, 1 below normalizeQueryResult.

    ⭐ And the sub-reading that matters for any future precedence question: producers emitting BOTH value and data as envelope members: 0.

    This is why PR #8624 kept packages/fields' value arm and repaired only its rank (it now delegates to this module's extractRecords), 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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingdomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatpm:dispatchedpriority:p2

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions