Repository navigation
fix(plugin-dashboard): an object-metric with a structured groupBy and a rule-list filter draws its number (objectui#11526) - #11535
Merged
objectstack-fleet[bot] merged 3 commits intoOct 3, 2026
Conversation
…re the spec-shape aggregate query (objectui#11526) An authored object-metric with a structured aggregate.groupBy and a ViewFilterRule[] filter drew ObjectStackAdapter's UnloweredAggregateWhereError instead of its number: the widget passed the rule list into the spec-shape query's where as written, and the adapter posts that where verbatim and refuses a rule list there (objectui#6825, ruling A). The widget now lowers the rule list for the spec-shape query only, with toFilterNode (@object-ui/core) and parseFilterAST (@objectstack/spec/data), the two stages the adapter applies to the same rule list on the legacy aggregate bag. A non-array filter stays verbatim, an empty list sends no where, and an array the lowering cannot read is handed on so the adapter's refusal still answers it. The legacy bag and the drill drawer are unchanged. Claude-Session: https://claude.ai/code/session_01YLg8XqWGJ785fwQ5v4pH37 Co-authored-by: Claude <noreply@anthropic.com>
…hat the legacy-wire equality was a one-time measurement The specShapeWhere docblock said the pin compares the lowered where with the legacy wire's where for every operator. It does not: it checks that every operator lowers to a FilterCondition. The equality with the legacy wire was measured once against the real adapter, and the comment now says so. The pin's showcase comment now says which tile it extends and that a second rule was added. Claude-Session: https://claude.ai/code/session_01YLg8XqWGJ785fwQ5v4pH37 Co-authored-by: Claude <noreply@anthropic.com>
Contributor
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
…oring faces, and say so The changeset and the pin comment said the card's document passes the strict authoring face, while the pin checked only the block's own arm (ObjectMetricBlockSchema). The pin now also asserts safeValidateSchema (the tolerant AnyComponentSchema, which objectui validate runs) and StrictAnyComponentSchema on the same documents, and both sentences say what is measured. Claude-Session: https://claude.ai/code/session_01YLg8XqWGJ785fwQ5v4pH37 Co-authored-by: Claude <noreply@anthropic.com>
Contributor
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
objectstack-fleet
Bot
deleted the
claude/issue-11526-object-metric-rule-filter-lowering
branch
October 3, 2026 04:14
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 #11526
Clause-②: no
What this changes
An authored
object-metricwhoseaggregate.groupByis the structured node ({ field, dateGranularity }) and whosefilteris the bag's rule list (ViewFilterRule[], asObjectMetricPropsSchemain@objectstack/spec/uideclares it) passes both authoring faces (safeValidateSchema, whichobjectui validateruns, andStrictAnyComponentSchema; the pin asserts both, beside the block's own armObjectMetricBlockSchema), and onObjectStackAdapterit drewUnloweredAggregateWhereErrorinstead of its number.ObjectMetricWidgetnow lowers the rule list for the spec-shape aggregate query only, in a new module functionspecShapeWhereinpackages/plugin-dashboard/src/ObjectMetricWidget.tsx.Files:
packages/plugin-dashboard/src/ObjectMetricWidget.tsx:specShapeWhere, called at the one spec-shape call site incomputeOne.packages/plugin-dashboard/src/__tests__/ObjectMetricWidget.ruleFilterSpecShape-11526.test.tsx: the pins..changeset/11526-object-metric-rule-filter-spec-shape.md:@object-ui/plugin-dashboardpatch.No
@object-ui/coreedit, no adapter edit, no exported type moves (soClause-②: noholds).The lowering, named
groupBy, the control that draws the number):lowerAnalyticsFilterForWireinpackages/data-objectstack/src/index.ts. It runstranslateFilterArray(rule list to filter AST, same file) and thenparseFilterASTfrom@objectstack/spec/data(filter AST toFilterCondition). Both adapter functions are module-private.@object-ui/data-objectstackdepends on@object-ui/core, so a re-export from core would be a dependency cycle.plugin-dashboarddoes not depend on the adapter package and reads anyDataSource(AGENTS.md Implement visual designer for Object UI schemas #1). Moving the translator into core would be a logic move in a package this claim does not cover. The brief's re-export fallback is therefore not available; see Acceptance notes.toFilterNode(packages/core/src/utils/filter-converter.ts, exported from@object-ui/core). It is core's sink for aViewFilterRule[], andobject-gridlowers its own rule-listfilterthrough it (viatoFilterNodeSafely). Stage 2 isparseFilterAST, the call the adapter makes. No conversion is written in the widget. TheisFilterASTgate between them hands an array that is not a filter on unparsed, becauseparseFilterASTanswersundefinedfor it, which would post an unfiltered aggregate. Handed on, the adapter's refusal answers it.@objectstack/spec17.5.0. I comparedparseFilterAST(toFilterNode(rules))with thewherethe legacy wire posts for the same rules. They are byte-equal for each operator inVIEW_FILTER_OPERATORS(20 at this spec version), for a two-rule list ($and), and for the legacy aliaseq. Where a rule is refused, both paths refuse. An array comparand onequalsand an emptyicontainscomparand are refused by both, with different error classes: the adapter'sUnlowerableAnalyticsFilterError/MalformedFilterError, and core'sFilterOperatorError. A scalar oninis refused byparseFilterASTon both paths. Nothing in this repo re-derives this equality, and the docblock says so.Reverse verification and ablation
One-time proofs, run from the committed fix (
b60869195). The probe that drives a realObjectStackAdapterlived in the worktree only during these runs and is not in this PR.Prediction, written before the runs: BASE and the ablation are both red on the pin's four lowering rows (structured groupBy plus rules; multi-rule count; every operator; empty list) and green on the boundary row and the two controls. With the real adapter, the structured groupBy plus rules document draws the refusal, and both controls draw 42.
ObjectMetricWidget.tsxat58d1f5f4b)wherearray that is not a filter ..."; nothing postedwhere{"stage":"won"}where{"stage":"won"}specShapeWhere(filterForRun)replaced byfilterForRun)b60869195)where{"stage":"won"}, the same bytes the legacy wire postsObserved matches predicted. The empty-list row's BASE red is a change in the posted body (
where: []becomes nowhere), not the card's defect. The platform engine readswhere: []as no filter (objectstack'sengine-filter-array-lowering.test.ts, the row 'an empty array is "no filter", exactly as before').Landing and restore evidence:
scripts/ablation-replace.mjs): anchor count went from 1 to 0, and the blob went from041e0f5dto42c7914d. Restore gave blob equal to HEAD041e0f5dwithgit diff HEADempty.git checkout HEAD --on the absolute path. Blob equality with HEAD and an emptygit diff HEADwere checked by the leg script, which carries atraprestore.@object-ui/*resolve tosrcthrough the vitest alias.Tests and gates
The final head is
fea8ccb0e. Fromb60869195tofea8ccb0e, only comment lines changed (a docblock correction and a test comment).fea8ccb0epnpm exec vitest runon the new pin,objectMetricStructuredGroupBy-8613,ObjectMetricWidget.drillRoutedToSharedDrawer-8970,ObjectMetricWidget.compareTo,objectMetricQueryMembers-8071, anddata-objectstack'saggregate-spec-shape-wherefea8ccb0epnpm --filter @object-ui/plugin-dashboard type-check(tsc --noEmitandtsc -p tsconfig.test.json; the new pin is in the test program per--listFiles)fea8ccb0epnpm check:control-bytes/check:new-line-citations/node scripts/check-changeset-presence.mjs/changeset:check/check:changeset-claims/check:pending-changeset-literalsfea8ccb0eb60869195pnpm exec vitest run packages/plugin-dashboard/b60869195plugin-dashboardthat nameObjectMetricWidget,object-metricorobjectAggregateSpecQuery(git grep -l,apps/consoleandpackages/coreincluded), plusaggregate-spec-shape-whereb60869195pnpm turbo run build --filter='./packages/*', thenpnpm --filter @object-ui/console build(the Bundle Analysis type program)b60869195check:phantom-deps,check:spec-symbols,check:test-path-roots,check:vi-mock-specifiers/-inherit/-override-shape,check:unused-deps,check:component-surface-parity,check:handler-key-reads,check:unreferenced-sourcesnode scripts/check-governed-queue-guard.mjs --test(the 3 paths)ESLint narrowing, as a measurement:
eslint.config.jslints**/*.{ts,tsx}withtseslint.configs.recommendedand sets no parserproject, so linting is not type-aware.--format jsonlists 2 files.ObjectMetricWidget.tsxhas 0 errors and 11 warnings, and its BASE blob linted the same way also has 0 errors and 11 warnings, from the same two rules. The new pin has 0 errors and 0 warnings.fsread in anyeslint-rules/*.js(grep returns 0 matches; the same pattern hitsscripts/check-control-bytes.mjs, the positive control), this diff cannot move a verdict on an untouched file. Repo-wide lint is CI's.e37c484be, at the seat's request): both authoring faces measured and pinned. The card's document, the showcase-shaped document and all 20 per-operator documents passObjectMetricBlockSchema,safeValidateSchema(the tolerant faceobjectui validateruns) andStrictAnyComponentSchema. The probe's controls show each face can refuse: an unknownpropertieskey is refused by all three, and an unknown node-level key is refused by the strict face only. The changeset and the pin comment now say "passes both authoring faces" instead of "the strict face". The pin passes 7 of 7,plugin-dashboardtype-check is green, and there is no source change.Acceptance notes
resolvedFilter, and the 8970 pin is green. objectui#11506's drill path is not touched.UnloweredAggregateWhereErrorand its check are untouched. The refusal of a caller that skips the lowering is pinned bydata-objectstack'saggregate-spec-shape-where.test.ts("throws UnloweredAggregateWhereError instead of posting a rule array"). The ablation above shows the refusal coming back end to end once the lowering call is removed. That file also pins the other half of the seam: "a FilterCondition OBJECT is not an array, and this gate leaves it alone".plugin-dashboarddoes not declare@object-ui/data-objectstack, and declaring it (apackage.jsonand lockfile edit) is outside this claim's file surface. The pin therefore asserts the postedwhereis aFilterCondition(FilterConditionSchemafrom@objectstack/spec/data). The adapter's acceptance of that shape is pinned on the adapter's side, as above.translateFilterArray(adapter-private) andtoFilterNode(core). They agreed afterparseFilterASTacross the declared vocabulary in the measurement above, and on refused rules they differ only in error class. No defect was measured. Carrier: none.object-chartmakes the same spec-shape call. Its declaredfilteris aFilterArrayor a record (ObjectChartSchema.filter), not a rule list, so this card's dialect does not reach it.Generated by Claude Code