Repository navigation
fix(core): the pivot preserves category-key ABSENCE, so the framework#4033 guard can fire (#4507) - #5281
Merged
os-support-ai merged 1 commit intoAug 19, 2026
Conversation
…rd can fire (#4507) `buildChartSeries`' pivot branch wrote `[xKey]` onto every bucket it created, so `key in row` was unconditionally true downstream and `hasNoCategoryKey` (plugin-charts) could never fire for a pivoted chart — the defence was dead exactly where the defect it guards against lands. An emitted bucket now carries the axis key exactly when some source row did, which is the guard's own `rows.some(key in row)` lifted through the pivot's aggregation. A bucket that DOES collect a key-carrying row keeps its `(None)` label and draws, so objectui#4466/#4497's answer is untouched. Measured first, per the card's binding precondition: an unprojected dimension arrives with the key OMITTED (the strategies never null-fill it, JSON cannot carry `undefined`, and the adapter passes rows through by reference), while an explicit `null` means the column WAS projected — a different, drawn case. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RV6yuVCxymHYE16PL9vQkE
This was referenced Aug 19, 2026
Contributor
✅ Console Performance Budget
📦 Bundle Size Report
Size Limits
|
os-support-ai
marked this pull request as ready for review
August 19, 2026 00:51
os-support-ai
deleted the
claude/issue-4507-pivot-preserves-key-absence
branch
August 19, 2026 00:51
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #4507
Verified at
02779e625(the commit this body's gate list was run against).The measurement the route rests on (taken FIRST, before any edit)
The card's own precondition is binding: omitting
[xKey]from a bucket only helps if key-absence is genuinely the shape a dataset query produces for a dimension it grouped by but never projected. So that was measured before anything was written.Producer half — the key is OMITTED, never null-filled.
service-analytics' ObjectQL strategy builds each result row from scratch and writes a dimension key only when the engine actually returned that column —if (shortName in row) mapped[dim] = row[shortName]. A dimension that did not project simply never becomes a property.ctx.executeRawSql) verbatim; a column the statement never selected is not a property of the row object either. Same shape, different path.objectql-timedimension-projection.test.ts, case "keeps a non-granular timeDimension OUT of the projection", assertsexpect(row).not.toHaveProperty('due_date'). And framework#4033 itself was measured on a live showcase app, where the rows read[{ count: 2 }, { count: 8 }]— the bucket key entirely gone while the numbers were right.undefinedat all, so no layer between the server and a chart can turn "no column" into a present-but-undefined key — even the executor paths that spellout[dim] = row[dim]emit a keyJSON.stringifythen drops.ObjectStackAdapter.queryDatasetpassesrowsthrough by reference (no per-row normalization).Consumer half — executed end to end through the real adapter with the server answering the framework#4033 payload for
dimensions: ['status','priority'], measures: ['est_hours'](statusnever projected):So: the row omits the key, the rows are still returned (they are grouped by the dimension that did project), and the pivot is exactly where the signal dies. An explicit
nullmeans the opposite — the column WAS projected and its value is null — which is the(None)bucket's case (objectui#4466 / #4497) and is untouched here. The two shapes are cleanly separated at the source, which is what makes route 1 coherent.The measurement confirms route 1. No fork to report.
The route taken, and why
Route 1 — the pivot preserves key-absence, so the existing guard sees it. One guard, one meaning; nothing of the pivot's internals leaks into the renderer, and
packages/plugin-chartsis not touched at all (route 2 would have widened the file surface, and the dispatch ruled that a fork to report rather than a widening to take).An emitted bucket now carries the axis key exactly when some row of it did:
That is
hasNoCategoryKey's ownrows.some(key in row)lifted through the pivot's aggregation, so the guard reads the same fact before and after the pivot instead of the opposite one.The second clause is not cosmetic.
chartBucketIdencodes an absent value and a storednullidentically ([null], by ADR-0021's normalization), so one bucket can collect both key-absent rows and rows whose category is genuinely null. If any of its rows carried the key, the bucket is "known to be empty": it keeps its(None)label and draws. Refusing there would tell an author their query never projected a dimension that it demonstrably did — the mirror of the false sentence #4497 refused to print by not relabelling a key-absent row(None).Why this survived four cards' worth of pins over the same rows
Both
JSON.stringifyandtoEqualdrop an undefined-valued key, so{ status: undefined, Low: 3 }and{ Low: 3 }read as identical everywhere — except in the one test that decides the placeholder,key in row. The new assertions sayin/Object.keys/toStrictEqualout loud for that reason.The pinned limit keeps its meaning
chart-series.nullCategory.test.ts, case "does NOT bucket a row that lacks the category key ENTIRELY", is not rewritten to match the change and does not fail:(None)-relabel stays refused for key-absent rows, which is what the case asserts. What changed is the stale limit recorded in its comment ("the pivot writes[xKey]onto every bucket … the guard cannot see it") — that limit is what this card exists to lift, so the note now says so, and the expectation is tightened fromtoEqual([{ status: undefined, Low: 3 }])(blind in both directions) totoStrictEqual([{ Low: 3 }])plus an explicit'status' inassertion. Same meaning, now falsifiable.Reverse verification — predicted before running, then run
Ablation: restore the pre-fix
chart-series.tsfromorigin/main, keep the new tests. No rebuild is involved in the resolution path: the tests import./chart-series.js, which vitest resolves topackages/core/src/utils/chart-series.ts(source), andvitest.config.mtsadditionally aliases@object-ui/coretopackages/core/src. There is nodistand no package-exportshop anywhere in the path, so a stale build cannot hold a result — and the ablation going red is itself the proof that the source edit is what the run reads. The restore leg was confirmed bygit status/git diff HEADboth empty (tree byte-identical to the commit) and the suites returning to green.Predicted 5 red, named individually; observed 5 red, exactly those five:
After restore:
Test Files 3 passed (3) · Tests 88 passed (88).Gates run locally (at
02779e625)pnpm --filter '@object-ui/core^...' build— dependency closure, greenpnpm exec vitest run packages/core/ packages/plugin-charts/— 121 files, 2132 tests, greenpnpm exec vitest run packages/plugin-dashboard/ packages/plugin-report/ packages/data-objectstack/— 117 files, 1280 tests, green (everybuildChartSeriesconsumer)pnpm --filter @object-ui/core type-check— greenpnpm exec eslinton the three changed source files — greennode scripts/check-control-bytes.mjs,check-changeset-presence.mjs,check-changeset-no-major.mjs,check-changeset-fixed.mjs,check-type-check-coverage.mjs— greenpnpm check:esm-specifiers,check:self-import,check:phantom-deps— greenThe DOM half is already pinned and untouched:
AdvancedChartImpl.missingCategoryKey.test.tsxrenders the explanatory placeholder for key-absent rows. This PR is what finally lets a pivoted chart reach it.Generated by Claude Code
Generated by Claude Code