Repository navigation
Commit dd0d78f
fix(components): element:number reads its object from dataSource.object, as the spec binding declares (objectui#10909) (#10944)
Fixes #10909
Clause-②: yes (widening)
Implemented on branch `claude/issue-10909-element-number-datasource` by
the `os-dev` run dispatched from the `domain:ui` seat 2 session
`session_014mXUNuFomfj24w7s1pZzhN` (claim comment 5866532141 on the
card).
## What changed
- **`ElementNumberRenderer`**
(`packages/components/src/renderers/basic/elements.tsx`) now reads the
spec's per-element binding (`PageComponentSchema.dataSource`) through
`useElementDataSource`, the way its element twin `element:record_picker`
does.
- The object resolves ONCE as `dataSource.object ?? properties.object`,
and that one value is the fetch guard, the `adapter.aggregate` /
`adapter.find` target and the `useDataInvalidation` key.
`dataSource.object` wins when both are set.
- While a named view is unresolved or unresolvable, there is no object,
even beside a flat `properties.object`.
- **Filter (seat ruling B):** with a binding present,
`properties.filter` is AND-combined with the binding's filter and with
its view's. The lowering and merge are the ones `ElementDataSourceGate`
uses (`toFilterNodeSafely` + `mergeFilterNodes`), so neither filter is
dropped. A merge the converter refuses shows the configuration-error
panel and aggregates nothing. With no binding, `properties.filter`
reaches the adapter exactly as authored.
- **Registration:** `element:number` is registered through
`elementDataSourceBlock(...)` (from `@object-ui/core`), so
`Registry.register` injects the one shared `dataSource` declaration
(`ELEMENT_DATA_SOURCE_INPUT`, `binding: 'object'`).
- `object` is no longer `required: true`.
- The `object` input's description states its precedence.
- The `filter` input's description is exactly: "Criteria the aggregate
is scoped by. When a node-level `dataSource` binding also supplies a
filter (its own, or the saved view its `view` names), the two are
AND-combined: neither is dropped."
- **Pins:** new
`packages/components/src/renderers/basic/__tests__/elementNumber.dataSourceBinding-10909.test.tsx`
(18 rows). It is registered as the `element:number.dataSource` member
pin in `apps/console/src/__tests__/registry-inputs-spec-parity.test.ts`,
which the parity census requires once the block declares an object-armed
input.
- **Docs:** `content/docs/guide/data-source.md` gains the
`element:number` row in the per-element binding table. A paragraph says
how the two `element:*` rows read the binding, and that they differ on
`filter`: `record_picker` substitutes, `element:number` AND-combines.
- **Changeset:** `.changeset/10909-element-number-datasource.md`,
`'@object-ui/components': minor`, with `Clause-②: yes (widening)`.
## H1: reproduced on `main` (733fd5a)
The probe used the real `SchemaRenderer`, the real registry, and an
`AdapterCtx` adapter with `aggregate` / `find` spies. The probe file was
temporary and not committed.
```text
H1 dataSource-form :: aggregate calls=[] find calls=[] painted="—"
H1 properties-form :: aggregate calls=["contact"] find calls=[] painted="7"
```
The same probe on this branch:
```text
H1 dataSource-form :: aggregate calls=["contact"] find calls=[] painted="7"
H1 properties-form :: aggregate calls=["contact"] find calls=[] painted="7"
```
## `ElementDataSourceSchema` members: which ones this renderer reads
| member | read? | how |
|---|---|---|
| `object` | yes | `dataSource.object ?? properties.object`. There is no
object at all while a named view is unresolved or unresolvable. A
binding naming no object (empty or not a string) supplies nothing, and
the flat object stands. |
| `view` | yes | Through `useElementDataSource`: the saved view's
`filter` scopes the aggregate. While the view resolves, the shared
loading panel shows and nothing is read. If it cannot be resolved, the
shared error panel (`element-number-datasource-error`) shows and nothing
is aggregated. |
| `filter` | yes | `properties.filter` AND (view filter AND binding
filter), lowered and merged as `ElementDataSourceGate` does. Neither is
dropped; a refused merge reports and aggregates nothing. Context tokens
are handled by the existing `useResolvedFilter` on the merged value.
There is no record-context binding (objectui#7297 stays on hold), and
the filter-shape note (objectui#8945) is untouched. |
| `sort` | no | An aggregate has no ordering. It never reaches
`aggregate()` or `find()`, and the whole options bag is asserted. |
| `limit` | no | A capped count would be a wrong number. Neither the
binding's nor a view's `pagination.pageSize` reaches `aggregate()` or
the `find()` fallback. |
| `columns` / `viewType` (view-only composed keys) | no | A metric has
no projection and no view kind. |
No member is declared per member. The only declaration is the injected
`dataSource` input (`type: 'object'`, `binding: 'object'`), whose
description lists the binding's five members generically. Which of them
this block reads is pinned at the read site.
## The registration change: what `elementDataSourceBlock` changes,
exactly
`elementDataSourceBlock` is a marker. It adds the renderer to a
`WeakSet` and returns it unchanged, so it adds no gate, no loading state
and no fetch. The declaration is emitted by `withElementDataSourceInput`
inside `Registry.register`. Measured on the live registry:
- the registered inputs on head are
`object,aggregate!,field,filter,format,prefix,suffix,dataSource` (`!` =
required). On base, `object!` carried the required mark and there was no
`dataSource`;
- `validateTree` on `{ type: 'element:number', dataSource: { object:
'contact' }, aggregate: 'count' }` returns no diagnostic. On base it
returned `is missing required prop "object"` and `has no prop
"dataSource"`.
The render-path changes come from `useElementDataSource` and from the
filter merge, and they reach only a node that carries a binding:
- a named `view` reads saved views and shows the shared loading panel
while it resolves;
- an unresolvable `view`, or a filter refused in either merge, shows the
shared error panel;
- a node with no `dataSource` resolves `absent` synchronously, so the
`properties` form issues no new read, shows no new panel, and sends its
filter unlowered. This is pinned: the control row asserts
`getObjectSchema` is never called.
## Pins: red before, green on head
File: `elementNumber.dataSourceBinding-10909.test.tsx`, 18 rows. The
file was byte-identical in all three runs, and each base run used a
temporary detached worktree that was removed afterwards.
| run | result |
|---|---|
| head 37bc45f | `Tests 18 passed (18)` |
| base 733fd5a | `Tests 16 failed \| 2 passed (18)` — the 2 green
rows are the `properties`-form control and the empty-`object` binding
row |
| previous head 48d755d (rule A) | `Tests 6 failed \| 12 passed (18)`
|
The 6 rows red on the previous head:
- the two AND rows (binding + properties filter, view + properties
filter) and the refused-merge row are red on semantics;
- "the binding filter scopes the aggregate", "a named saved view's
filter scopes the aggregate" and the sort/limit whole-bag row are red on
wire shape only. With a binding present, every filter source is now
lowered to the ObjectQL AST before merging, as the gate does.
Rows:
1. the dataSource form calls aggregate for the bound object and paints
the value;
2. dataSource.object wins over properties.object when both are set;
3. control: the properties form is unchanged;
4. the find() fallback reads the bound object when the adapter cannot
aggregate;
5. a binding that names no object supplies nothing, and the flat object
stands;
6. one bus event re-reads it, keyed on the resolved object;
7. the key is the binding, not the flat object it outranks;
8. filter: the binding filter scopes the aggregate;
9. filter: properties.filter is AND-combined with the binding filter —
both reach the aggregate, neither is dropped;
10. filter: a named view's filter and properties.filter are both
applied, AND-combined;
11. filter: a merge the converter refuses reports on the error panel and
aggregates nothing;
12. view: a named saved view's filter scopes the aggregate;
13. view: one that cannot be resolved reports and aggregates nothing;
14. view: an unresolvable one does not fall back to properties.object —
no object while it is unresolved;
15. sort and limit are not read: the options bag stays whole with a
binding and a view carrying both;
16. limit is not read on the find() fallback either: a count is never
capped;
17. the registration declares dataSource as an object binding, and
object is no longer required;
18. the html tier accepts the dataSource form without a diagnostic
(control in the same row: `element:text` still reports `has no prop
"dataSource"`).
**Ablations** at 37bc45f, through
`objectstack/scripts/ablation-replace.mjs` in wrap mode with its
trap-armed restore. Each was run over the pin file plus the three
pre-existing `element:number` files (`elementNumberFilterMembers-8071`,
`elementReaders.invalidationRefetch-10623`,
`element-number.contractEnvelope-6726`), 33 rows in all. Each restore
printed `ok restored: blob == HEAD (3ab0d9a) and git diff HEAD is
empty`.
| # | ablation | red | green |
|---|---|---|---|
| 1 | resolution line: `(composed?.object ?? props.object)` becomes `(/*
ablation-10909 */ props.object)`; blob `3ab0d9a745a4` to `875d8dfa0e4a`
| `11 failed \| 22 passed (33)`: rows 1, 2, 4, 6–10, 12, 15 and 16, the
dataSource-form rows that reach the resolution line | both controls, the
refused-merge and unresolvable-view rows, the two registration rows, and
all 15 rows of the three pre-existing files |
| 2 | AND merge: `mergeFilterNodes(own.node, bound.node)` becomes `(/*
ablation-10909-and */ bound.node ?? own.node)`, the rule A semantics;
blob `3ab0d9a745a4` to `2a9bf06af87c` | `2 failed \| 31 passed (33)`:
exactly the two AND rows, 9 and 10 | everything else |
| 3 | guard: `unresolved \|\| filterRefusal ? undefined : ` becomes `/*
ablation-10909-guard */ `; blob `3ab0d9a745a4` to `c249c85fa79b` | `2
failed \| 31 passed (33)`: exactly row 11 (refused merge) and row 14
(unresolved view beside a flat object) | everything else |
## Census: element renderers and the `dataSource` binding
Every `element:*` registration is in `@object-ui/components`, and no
other package registers one. The census probe rendered each registered
element with `dataSource: { object: 'contact' }` and no flat object:
| block | object-bound? | reads `dataSource`? | html tier on
`dataSource` |
|---|---|---|---|
| `element:number` | yes (`object`) | **yes** (this PR):
`aggregate(contact)` | accepted |
| `element:record_picker` | yes (`object`) | yes: `find(contact)` |
accepted |
| `element:repeater` | yes (`object`, required) | no: no read | reports
`has no prop "dataSource"` and the missing `object` |
| `element:metadata_viewer` | names a METADATA subject (a state
machine's object), not a record query | no | reports `has no prop
"dataSource"` |
| `element:text`, `element:divider`, `element:image`, `element:button`,
`element:text_input`, `element:definition-list` | no | no | reports `has
no prop "dataSource"` |
`element:form` and `element:filter` have `ComponentPropsMap` rows but no
objectui renderer. `element:repeater` is not a spec block: it has no
`PageComponentType` member and no `ComponentPropsMap` row. Its
declaration and its runtime agree, and the html tier reports the key, so
this is an observation, not a finding.
## Consumer sweep
The file set is the `git grep` hits for `element:number`,
`ElementNumberRenderer`, `elementDataSourceBlock`,
`withElementDataSourceInput` and `ELEMENT_DATA_SOURCE_INPUT` in tests,
plus:
- the html-tier manifest and html-tier `dataSource` declaration suites;
- the registry-inputs parity suite and the
`element-data-source-input-injection` render probe;
- the `record_picker` data-source and malformed-filter pins;
- the suites that render every element renderer generically.
Fix round 1 adds the gate-family filter-merge tests this change reuses:
`ElementDataSourceGate.malformedFilter-10789`,
`ElementDataSourceGate.test`, core `filter-source-merge.test`, and
`ObjectMetric.elementDataSource.test`.
At 37bc45f, 34 files, run under the shared verify lock: `Test Files
34 passed (34)` · `Tests 983 passed (983)`.
The full `@object-ui/components` suite ran at 75f027e (before this
round) in three directory runs: 326 collected files, all green. This
round changes only `elements.tsx` and its pin file inside that package.
Both are covered above by the directories' own `element:number` files
and by the sweep.
## Gates
At 37bc45f. Each verdict is the gate's own line.
- `pnpm --filter @object-ui/components type-check` (`tsc --noEmit && tsc
-p tsconfig.test.json`), after `pnpm --workspace-concurrency=2 --filter
'@object-ui/components^...' build` (8 of 47 projects): exit 0.
- `pnpm check:element-data-source-declaration`: `OK — 14 gate-consuming
file(s) checked, 3 definition file(s) excluded; all reach the seam and
take it from core.`
- `node scripts/check-changeset-presence.mjs`: `3 source file(s) of 2
released package(s) changed, and this change declares 1 changeset(s):
.changeset/10909-element-number-datasource.md.`
- `node scripts/check-changeset-no-major.mjs`: `No changeset declares a
major bump.` The frontmatter is `minor`.
- `pnpm check:changeset-claims`: report-only. The same three pending
changesets name touched files (`6726-find-envelope-records-arms`,
`6783-readprops-degenerate-config-bag`,
`8067-component-input-member-kind`); each paragraph is still true.
- `pnpm check:control-bytes`: `OK`.
- `pnpm check:new-line-citations`: `VERDICT
new-cross-file-line-citations: 0 new citation(s)`.
- `node scripts/check-doc-links.mjs`: `Links are valid across 17 scan
roots.`
- **eslint, a proven narrowing** (`--no-inline-config --format json`
over the three changed source and test files):
1. **Population** from eslint's own config: `ESLint.isPathIgnored`
answers `false` for all three.
2. **Count** from the JSON: 3 files, 0 errors. `elements.tsx` has 24
warnings, the same per-rule split as base; the other two files have 0.
3. **Invariance**: type-aware linting is off (no `parserOptions.project`
or `projectService`), and the one custom rule reads no file from disk,
so untouched files cannot move.
- Carried from round 0, and not affected by this round's diff:
`check:handler-key-reads`, `check:designer-field-key-parity`,
`check:component-surface-parity`, `check:unreferenced-sources`,
`check:test-path-roots`, `check:vi-mock-specifiers`,
`check:registry-bare-names`, `check:i18n-keys`, `check:doc-types`,
`check:doc-fences`, `check:doc-example-ids` and `check:spec-symbols`
were green at 75f027e.
- **NOT MEASURED: `apps/console` type-check.** Reason: its program needs
every plugin's built declarations, and that closure build is beyond the
foreground cap. The CI `Type Check` job owns it. The console edit is one
`MEMBER_PINS` entry with two string fields.
- **NOT MEASURED: `check:sdui-registration-pins`.** Reason:
PREREQUISITE, no console bundle (exit 2). The `performance-budget`
workflow runs it.
- **NOT MEASURED: `check:doc-snippets` and `check:doc-examples`.**
Reason: PREREQUISITE, unbuilt packages (exit 2). The guide edit adds no
fenced snippet.
## Serial
- **Open PRs**, read through REST before opening: 19 PRs, all file lists
(1,867 rows). None touches `packages/components/src/renderers/basic/`,
the parity suite or `content/docs/guide/data-source.md`. Positive
control: PR objectui#10908's `packages/types/` files match the same
grep.
- `packages/core` is unchanged; this change only imports its existing
`toFilterNodeSafely` and `mergeFilterNodes`.
- `git merge-tree --write-tree` against a fresh `origin/main`
5c94589, fetched into a private ref: exit 0, tree `4c51202bf5`. Main
moved 20 commits since the base, and none of them touches the renderer,
the parity suite, the guide, `useElementDataSource`,
`useResolvedFilter`, the gate, the core data-source module,
`filter-converter`, `Registry.ts` or `validate.ts`.
- `git merge-tree --write-tree` against PR objectui#10908's head
fe8f6a6: exit 0, tree `4bb57ac81c`.
## Fix round 1
This responds to the contract review FAIL at 48d755d, on text and one
seat ruling. Object resolution, the unchanged properties form, the
registration, the census and the pins held.
- **Seat ruling B, filter AND-combine.** Implemented in the renderer
with the gate's `toFilterNodeSafely` + `mergeFilterNodes`.
- The precedence row flipped to assert that both filters reach the
aggregate.
- New rows: a view filter plus `properties.filter`; a refused merge
taking the error panel with no aggregate call.
- The registered `filter` description is the ruling's text verbatim. The
guide paragraph and the changeset bullet say AND-combined.
- `record_picker` is untouched.
- **Changeset:**
- the frontmatter becomes `minor`;
- the Clause paragraph is replaced with the review's `Clause-②: yes
(widening)` text verbatim;
- "accepted by both validators" becomes "which the spec lint gate
accepts, as `objectui validate` will once objectui#10908 arms the type";
- the filter bullet now states AND-combine and the loud refusal.
- **PR body:** line 2 is `Clause-②: yes (widening)`.
- **Unresolved-guard pin:** `dataSource: { object: 'contact', view:
'no_such_view' }` beside `properties.object: 'contact'` asserts no
aggregate call and the error panel. The guard ablation reddens exactly
that row and the refused-merge row.
- **`MEMBER_PINS` prose:** rewritten for rule B. It no longer says the
declaration carries "nothing about members"; it says the injected
description lists the five members generically and the pin carries which
of them this block reads.
## Acceptance notes
- **This unblocks PR objectui#10908.** It is the contract review's
option (c) sequencing. With this landed, both validators, the html tier
and the renderer tell one story about `{ dataSource: { object } }`, and
PR objectui#10908's arm (which accepts `properties.filter` beside a
binding filter or view) now matches a renderer that honours both.
- **Clause-②: yes (widening).** The published `element:number` manifest
entry declares `dataSource`, and `object` is no longer `required`. The
html tier, and the objectstack CLI's JSX gate that reads the manifest,
now also accept a node that names neither `object` nor a binding, which
paints the empty dash. That case is still reported on the JSON door by
the spec lint gate (advisory), and after PR objectui#10908 by its arm
when a bag is present. A runtime "no object named" notice, or a one-of
check at the html tier, would be a follow-up card. Precedent:
objectui#10508 made the same trade on calendar and gantt `objectName`.
- **Wire shape with a binding.** When a binding is present, every filter
source is lowered to the ObjectQL AST before it reaches `aggregate()` /
`find()`, as `ElementDataSourceGate` does for `object-metric`. Without a
binding, `properties.filter` is sent as authored, as before.
- **Studio is unchanged.** Studio's block config for `element:number`
stays properties-only and offers no `dataSource` editor.
- **The error-panel title** "could not be resolved" is imprecise for a
refused filter; the panel's message names the refused rule.
`record_picker` shares the wording. This is a review nit, and the text
is unchanged here.
---
_Generated by [Claude
Code](https://claude.ai/code/session_014mXUNuFomfj24w7s1pZzhN)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 5c94589 commit dd0d78f
5 files changed
Lines changed: 517 additions & 14 deletions
File tree
- .changeset
- apps/console/src/__tests__
- content/docs/guide
- packages/components/src/renderers/basic
- __tests__
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
2301 | 2301 | | |
2302 | 2302 | | |
2303 | 2303 | | |
| 2304 | + | |
| 2305 | + | |
| 2306 | + | |
| 2307 | + | |
2304 | 2308 | | |
2305 | 2309 | | |
2306 | 2310 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
253 | 253 | | |
254 | 254 | | |
255 | 255 | | |
| 256 | + | |
256 | 257 | | |
257 | 258 | | |
258 | 259 | | |
| |||
279 | 280 | | |
280 | 281 | | |
281 | 282 | | |
| 283 | + | |
| 284 | + | |
| 285 | + | |
| 286 | + | |
| 287 | + | |
| 288 | + | |
| 289 | + | |
| 290 | + | |
| 291 | + | |
| 292 | + | |
| 293 | + | |
| 294 | + | |
| 295 | + | |
282 | 296 | | |
283 | 297 | | |
284 | 298 | | |
| |||
0 commit comments