Split out of objectui#7299 (dev seat, scope discipline — that card's ruling is RelatedList only). Filed as a separate card because the fix is NOT mechanical here: the badge probe has no field metadata in hand at all, so giving it arity is a design change in a different package, not a spelling change.
The measurement
RelatedList composes the parent-relationship condition for the ROWS. The tab badge composes it independently, for the COUNT:
packages/components/src/hooks/related-count-store.ts parentScope[relField] = parentId;
packages/components/src/renderers/layout/containers.tsx the probe that calls it
Both were bare equality. objectui#7299 compiles the ROW condition to match the relationship field's arity (multiple: true becomes { [relField]: { $contains: parentId } }, which is the spelling driver-sql prescribes in the 400 INVALID_FILTER it answers the equality form with). The badge still sends =.
Why it matters, stated no wider than it is
The store's own comment states the invariant this breaks, verbatim:
That shared sink is what makes badge/row parity a property of the code rather than of two implementations agreeing by luck: the badge cannot count a set the list does not show, because both sides send the same $filter.
And the spec text objectui#4664 pins says the same from the contract side: "The related-list tab badge count honors the same composed filter, so counts match the visible rows."
After objectui#7299, for a multiple: true relationship, the two sides send DIFFERENT $filters. The visible symptom today is milder than a wrong number, and that is worth stating precisely so nobody over-reads this card: fetchCount swallows the driver refusal (catch { return 0; }) WITHOUT calling setCount, so the store holds no entry, computeCount returns undefined, and the tab renders with NO badge at all. So the failure is an ABSENT count above a populated list, not a 0 above a populated list.
It is still the parity property failing, and it is still measured rather than predicted: the probe sends a predicate the driver refuses by name.
Why it was not folded into objectui#7299
The row fix reads arity off the child object's schema, which RelatedList already fetches for its own columns. The badge probe has nothing equivalent: containers.tsx walks SCHEMA NODES (rl.properties.objectName / relationshipField) and RelatedCountStore.fetch receives four scalars plus a filter. Getting arity there means a per-child-object metadata fetch inside the tab strip — new async state, new cache identity, new tests, in @object-ui/components rather than @object-ui/plugin-detail. That is a card, not a rider.
Shape of the fix (not a ruling — the seat that takes this decides)
Whatever route is chosen, the parity property is the acceptance criterion, not the spelling: the badge and the rows must send the same $filter for the same list. The cache key already discriminates on the declared scope filter (objectui#4664); arity would have to reach the same place.
⛔ Do not close objectui#7299 on this — that card's rows fix is complete and correct on its own terms.
Generated by Claude Code
Split out of objectui#7299 (dev seat, scope discipline — that card's ruling is
RelatedListonly). Filed as a separate card because the fix is NOT mechanical here: the badge probe has no field metadata in hand at all, so giving it arity is a design change in a different package, not a spelling change.The measurement
RelatedListcomposes the parent-relationship condition for the ROWS. The tab badge composes it independently, for the COUNT:Both were bare equality. objectui#7299 compiles the ROW condition to match the relationship field's arity (
multiple: truebecomes{ [relField]: { $contains: parentId } }, which is the spellingdriver-sqlprescribes in the400 INVALID_FILTERit answers the equality form with). The badge still sends=.Why it matters, stated no wider than it is
The store's own comment states the invariant this breaks, verbatim:
And the spec text objectui#4664 pins says the same from the contract side: "The related-list tab badge count honors the same composed filter, so counts match the visible rows."
After objectui#7299, for a
multiple: truerelationship, the two sides send DIFFERENT$filters. The visible symptom today is milder than a wrong number, and that is worth stating precisely so nobody over-reads this card:fetchCountswallows the driver refusal (catch { return 0; }) WITHOUT callingsetCount, so the store holds no entry,computeCountreturnsundefined, and the tab renders with NO badge at all. So the failure is an ABSENT count above a populated list, not a0above a populated list.It is still the parity property failing, and it is still measured rather than predicted: the probe sends a predicate the driver refuses by name.
Why it was not folded into objectui#7299
The row fix reads arity off the child object's schema, which
RelatedListalready fetches for its own columns. The badge probe has nothing equivalent:containers.tsxwalks SCHEMA NODES (rl.properties.objectName/relationshipField) andRelatedCountStore.fetchreceives four scalars plus a filter. Getting arity there means a per-child-object metadata fetch inside the tab strip — new async state, new cache identity, new tests, in@object-ui/componentsrather than@object-ui/plugin-detail. That is a card, not a rider.Shape of the fix (not a ruling — the seat that takes this decides)
Whatever route is chosen, the parity property is the acceptance criterion, not the spelling: the badge and the rows must send the same
$filterfor the same list. The cache key already discriminates on the declared scope filter (objectui#4664); arity would have to reach the same place.⛔ Do not close objectui#7299 on this — that card's rows fix is complete and correct on its own terms.
Generated by Claude Code