Skip to content

Commit 0161c7f

Browse files
feat(spec,lint): 声明 chart segment drill —— ChartDrillDownSchema,挂在真正读它的那一层 (#5022) (#5221)
* wip(spec): #5022 measurement checkpoint — drillDown carrier evidence (no code yet) 断点位置:测量完成,**尚未写任何 spec 代码**。测量结果与裁决前提冲突, 倾向 needs_decision(见下)。分支上目前是空提交,仅承载证据。 ## 已完成的测量(objectui @91757a7 + objectstack @41e605ec70) ### A. ObjectChart 实际读取词汇(反推自 packages/plugin-charts/src/ObjectChart.tsx) 读到的键(:602 起,经 @object-ui/core drill-down.ts): enabled? boolean isDrillEnabled: 无 config=关;{}=开;enabled:false=关 filter? Record<string,unknown> computeDrillFilter,支持 ${event.*} 插值 title? string resolveDrillTitle,支持 ${event.*} 插值 target? 'drawer'|'dialog' :755 — **只分支 'dialog',其余一律 Sheet** columns? string[] :762 → accessorKey/header maxRows? number :761 → pageSize ObjectChart **不读**:mode / view / report / sort,且 **不实现 target:'navigate'** (navigate 只在 DrillDownDrawer.tsx:84 实现,ObjectChart 自绘 Sheet/Dialog,不走它)。 objectui 侧已有 TS 接口 DrillDownConfig(packages/types/src/data-display.ts:897), 是 5 个 widget 共享的更宽形状:{enabled,mode,target(含 navigate),filter,title, view,report,columns,sort,maxRows}。其中 view/sort 无任何渲染器读取。 ### B. 载体测量 —— 与裁决前提("渲染器已在读的 drillDown,objectui 零改动,挂 chart config")冲突 1. widget.chartConfig.drillDown → **无人读**。 DashboardRenderer.tsx 全文 0 处 chartConfig;DatasetWidget.tsx:595-605 只取 showLegend 一个键,注释自陈 "the rest of chartConfig stays unforwarded"; :632 构造的 chart schema 里没有 drillDown。 2. widget.options.drillDown → DashboardRenderer.tsx:494/:570/:531 确实读,但**全部在 isObjectProvider(widgetData) 的 legacy inline 分支内**;而 DashboardWidgetSchema 要求 dataset(dashboard.zod.ts:427,非 optional),datasetBound 恒真, DashboardRenderer.tsx:691/:720 恒走 <DatasetWidget>,componentSchema 被丢弃。 ⇒ 对 v17 合法 widget,options.drillDown 永不被读。 3. ADR-0021 正路 DatasetWidget 的 drill 是**从 dataset 结果派生、不可配置**的: canDrill 由 dimensionFields/drillRanges 决定,DrillDownDrawer 只收 {objectName, filter, title, dataSource} —— 上述词汇一个都不认。 4. 唯一真活的面是 **react tier**:ChartConfigSchema 的键在 react 块上是**扁平 prop** (react-blocks.ts:226-247 dataProps),所以 <ObjectChart drillDown={…}> 确实生效; 但 drillDown 既不在 spec dataProps/interactions,也不在 objectui registry inputs (ObjectChart.tsx:822-830),SDUI save gate 会报 unknown-prop(warning,不拦)。 ⇒ 结论:挂 chart config 在 **react tier 交付、dashboard 元数据面不交付**; 挂 options 在两面都不交付。任一选择都会造出 #5011 "same key, two fates" 的形状。 ## 剩余步骤(若维护者裁定后继续) 1. [待裁决] 定载体:(A) 只挂 react 块契约 /(B) 挂 ChartConfigSchema 并明写 dashboard 面不转发 /(C) 挂 options 并在 objectui DatasetWidget 落地实现(非小改动)。 2. [待裁决] 定形状:chart 诚实子集 {enabled,filter,title,target:'drawer'|'dialog', columns,maxRows} vs objectui 共享 DrillDownConfig 全集。 3. 写 ChartDrillDownSchema(strictObject,surface/history/guidance),JSDoc 与 ReportSchema.drilldown 双向点名消歧。 4. 逐键验收测试 + 调和 chart.test.ts:464 的批 15 钉子。 5. liveness/dashboard.json 分类 + check:strictness-ledger 台账行。 6. changeset(minor,v17 pre 模式);objectui 侧把 (schema as any).drillDown 换成声明类型。 7. 合 origin/main(#5154/#5162 已入队)后重跑 check:strictness-ledger + check:generated。 ## 顺带发现(超范围,待立 issue) - widget.chartConfig 在 dashboard 面只有 showLegend 落地,其余 11 个已声明键全惰性; liveness/dashboard.json 的 chartConfig 行写 "chart-config bag forwarded",高估。 - objectui DrillDownConfig.view / .sort 无渲染器读取;ObjectChart 忽略 target:'navigate'。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ehu85kbvMcrNTUJjwxvLJ9 * feat(spec,lint): declare the chart segment drill as ChartDrillDownSchema, on the tier that reads it (#5022) `drillDown` drove a real capability with no protocol declaration behind it: objectui's ObjectChart read `(schema as any).drillDown` and really did open a filtered record drawer from it, while `grep drillDown packages/spec` found only a comment — the one #3752 wrote prescribing the key as a migration target. That inverted Prime Directive #10: delivered, never declared. Declared now, additively, at the one surface measured to read it. ## The shape is the six keys ObjectChart reads `{ enabled?, filter?, title?, target?: 'drawer' | 'dialog', columns?, maxRows? }`, each reverse-engineered from a read point: `isDrillEnabled` (absent = off, `{}` = on), `computeDrillFilter`, `resolveDrillTitle`, the `'dialog'` branch, the column map, the page size. objectui's own `DrillDownConfig` is wider because it is shared with the table / pivot / metric widgets, and copying that union would have promoted four keys a chart ignores — two of which NO widget reads (objectui#3354) — into protocol-declared capabilities. Each absent key is a `guidance` entry naming where it does apply, not a rename. ## The carrier is the react block, NOT chart config or the widget options bag Both candidates in the issue measured dead on the dashboard metadata path: `DashboardRenderer` never reads `chartConfig`, and `DatasetWidget` forwards exactly one key out of it (`showLegend`); `options.drillDown` is read only in `DashboardRenderer`'s legacy `isObjectProvider` branch, which a spec-legal v17 widget cannot reach, because `dataset` is required and `datasetBound` therefore discards that component schema unrendered. An ADR-0021 dataset-bound widget drills through the semantic layer and honours no drill config at all — which `content/docs/ui/dashboards.mdx` already said. So the declaration rides `react-blocks.ts`'s interaction overlay, deliberately not `ChartConfigSchema`, whose members a dashboard widget's `chartConfig` also parses. A member there would have been authorable, parse-clean and unread. The three places an author reaches for it now answer instead of shrugging: `chartConfig.drillDown` → the react prop; `widget.drillDown`/`widget.drilldown` → dashboard drill is AUTOMATIC, plus both configurable drills named; `report.drillDown` → back to the chart prop. ## Two-way disambiguation with ReportSchema.drilldown Same word, one letter apart, two capabilities. Edit distance actively gets this wrong — the spellings are distance 1, so a bare "did you mean" sends an author to the boolean slot where their config object fails a second time. Both gates name the TYPE difference, not just the spelling. ## Declared = enforced, in the same commit `.strict()` is a property of a parse, and nothing on the react surface called one. `validate-react-page-props.ts` now PARSES the schema against a static `drillDown={{…}}` literal (`react-chart-drilldown-invalid`) instead of re-deriving the rules the way `CHART_FUNCTIONS` does for `aggregate` beside it — the fix the strictness ledger's `chart.zod.ts` row has been waiting on, shown on one key. A value from React state is skipped: unresolvable is not wrong (ADR-0072 D1). ## Instruments - 批 15's pin (`chart.test.ts`) is RECONCILED, not relaxed: the ChartInteraction prescription still must not say a bare `drillDown`, now because that schema is reached from both tiers and cannot tell which, so the advice would be a dead end for half its readers. - 20 new assertions: per-key acceptance, `{}` enables, absent stays optional, unknown key rejected with the surface named, each non-chart key rejected with its reason, `target:'navigate'` prescribed (and a plain wrong value NOT given that text), the carrier verdict pinned in both directions, and the published react type string derived-checked against the schema shape. - 10 lint-gate tests; ledger row and site counts updated (`ui/` 198 → 199). Filed, not fixed here: #5175 (`chartConfig` delivers 1 of 12 keys on the dashboard path; the liveness row's evidence overstates it) and objectui#3354 (`DrillDownConfig.view`/`.sort` read by nothing; `navigate` silently degrades). Fixes #5022 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ehu85kbvMcrNTUJjwxvLJ9 --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent d9971d3 commit 0161c7f

17 files changed

Lines changed: 741 additions & 29 deletions
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
---
2+
"@objectstack/spec": minor
3+
"@objectstack/lint": minor
4+
---
5+
6+
feat(spec,lint): declare the chart segment drill — `ChartDrillDownSchema`, on the react tier where it is actually read (#5022)
7+
8+
`drillDown` has driven a real capability since long before this release: click a
9+
bar or a slice on an `<ObjectChart>` and objectui opens the underlying records,
10+
filtered by the clicked category, in a drawer. The protocol declared it
11+
**nowhere**. objectui read it as `(schema as any).drillDown`, so every key inside
12+
it — right, wrong, or misspelled — reached the renderer unchecked, and a typo was
13+
simply ignored at click time. This is Prime Directive #10 inverted: not declared
14+
without being delivered, but delivered without ever being declared.
15+
16+
It is declared now, as `ChartDrillDownSchema`, and it is **additive** — nothing
17+
that parsed before stops parsing.
18+
19+
## What you can write
20+
21+
`drillDown` is a prop on the react-tier `<ObjectChart>` block:
22+
23+
```jsx
24+
<ObjectChart objectName="opportunity"
25+
aggregate={{ function: 'sum', field: 'amount', groupBy: 'stage' }}
26+
drillDown={{ columns: ['name', 'amount'], maxRows: 50 }} />
27+
```
28+
29+
| key | type | meaning |
30+
|---|---|---|
31+
| `enabled` | `boolean` | Only needed to force the drill OFF — the block being present already means on, so `drillDown={{}}` enables it |
32+
| `filter` | `Record<string, unknown>` | Filter for the drilled list; values support `${event.*}`. Omit it and the filter is derived from `aggregate.groupBy` equal to the clicked category |
33+
| `title` | `string` | Drawer/dialog heading; supports `${event.*}` |
34+
| `target` | `'drawer' \| 'dialog'` | In-place side sheet (default), or a centered modal when the chart is already inside a drawer |
35+
| `columns` | `string[]` | Column whitelist for the drilled list |
36+
| `maxRows` | `number` | Rows per page in the drilled list |
37+
38+
Every one of those six is a key objectui's `ObjectChart` was measured to read.
39+
The renderer's own drill type is wider — it is shared with the table / pivot /
40+
metric widgets — and the extra keys are **deliberately not declared**, because a
41+
chart reads none of them:
42+
43+
- **`mode`** (`'filter'`/`'record'`) is a table/pivot/metric key. A chart segment
44+
is always an aggregate, so there is nothing to discriminate.
45+
- **`report`** (drill into a report instead of a record list) is a metric/pivot
46+
capability.
47+
- **`view`** and **`sort`** are read by *no* renderer at all (objectui#3354).
48+
- **`target: 'navigate'`** is implemented for the other widgets but not for a
49+
chart, which falls back to the drawer.
50+
51+
Writing any of them is now a loud rejection that says which surface owns it,
52+
rather than a value that silently does nothing.
53+
54+
## Where it is NOT declared, and why that is deliberate
55+
56+
**Not on `ChartConfigSchema`, and not a dashboard widget key.** A dashboard
57+
widget has no per-widget drill configuration, by design: an ADR-0021
58+
dataset-bound widget drills through the semantic layer, deriving the target
59+
object and filter from the dataset row that was clicked. That is what
60+
`content/docs/ui/dashboards.mdx` has said all along, and it is what the renderer
61+
does — `DashboardRenderer` never reads `chartConfig`, and `DatasetWidget`
62+
forwards exactly one key out of it (`showLegend`). Declaring the drill there
63+
would have produced authorable metadata that parses clean and never reaches a
64+
renderer — the failure this campaign removes elsewhere.
65+
66+
So the three places an author might reach for it now answer instead of shrugging:
67+
68+
- `widget.chartConfig.drillDown` → rejected, pointing at the react-tier prop.
69+
- `widget.drillDown` / `widget.drilldown` → rejected, explaining that dashboard
70+
drill-through is **automatic**, and naming both configurable drills.
71+
- `report.drillDown` → rejected, pointing back at the chart prop.
72+
73+
## `drillDown` is not `drilldown`
74+
75+
Two capabilities, one letter apart, and they are now disambiguated in both
76+
directions at the schema gate:
77+
78+
| | `drillDown` | `drilldown` |
79+
|---|---|---|
80+
| spelling | camelCase | all lowercase |
81+
| type | configuration object | boolean |
82+
| surface | react `<ObjectChart>` prop | `ReportSchema` key (ADR-0021 D2, on by default) |
83+
84+
Edit distance alone gets this wrong — the two spellings are a distance of 1, so a
85+
plain "did you mean" would happily send an author writing `drillDown` on a report
86+
to `drilldown`, where their config object then fails a second time as a boolean.
87+
Both gates name the **type** difference, not just the spelling.
88+
89+
## Enforced, not just declared
90+
91+
`@objectstack/lint`'s react-page publish gate now **parses** the schema
92+
(`react-chart-drilldown-invalid`) against a static `drillDown={{…}}` literal,
93+
rather than re-deriving the rules. Unknown keys, the wrong `target`, and the
94+
near-key spelling all fail the build with the schema's own prescription. A value
95+
assembled from React state is skipped, unchanged: an unresolvable binding is not
96+
a wrong one (ADR-0072 D1).

‎content/docs/references/ui/chart.mdx‎

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@ Provides a comprehensive set of chart types for data visualization.
1818
## TypeScript Usage
1919

2020
```typescript
21-
import { ChartAggregateSchema, ChartAggregateFunctionSchema, ChartAnnotationSchema, ChartAxisSchema, ChartConfigSchema, ChartGroupBySchema, ChartInteractionSchema, ChartSeriesSchema, ChartTypeSchema } from '@objectstack/spec/ui';
22-
import type { ChartAggregate, ChartAggregateFunction, ChartAnnotation, ChartAxis, ChartConfig, ChartGroupBy, ChartInteraction, ChartSeries, ChartType } from '@objectstack/spec/ui';
21+
import { ChartAggregateSchema, ChartAggregateFunctionSchema, ChartAnnotationSchema, ChartAxisSchema, ChartConfigSchema, ChartDrillDownSchema, ChartGroupBySchema, ChartInteractionSchema, ChartSeriesSchema, ChartTypeSchema } from '@objectstack/spec/ui';
22+
import type { ChartAggregate, ChartAggregateFunction, ChartAnnotation, ChartAxis, ChartConfig, ChartDrillDown, ChartGroupBy, ChartInteraction, ChartSeries, ChartType } from '@objectstack/spec/ui';
2323

2424
// Validate data
2525
const result = ChartAggregateSchema.parse(data);
@@ -113,6 +113,22 @@ Inline aggregation for an object-bound chart
113113
| **aria** | `{ ariaLabel?: string; ariaDescribedBy?: string; role?: string }` | optional | ARIA accessibility attributes |
114114

115115

116+
---
117+
118+
## ChartDrillDown
119+
120+
### Properties
121+
122+
| Property | Type | Required | Description |
123+
| :--- | :--- | :--- | :--- |
124+
| **enabled** | `boolean` | optional | Turn the segment drill on/off; the block being present already means on, so this is only needed to force it off |
125+
| **filter** | `Record<string, any>` | optional | Filter for the drilled list; values support $`{event.*}`. Omit to derive it from the clicked category |
126+
| **title** | `string` | optional | Drill drawer/dialog heading; supports $`{event.*}` interpolation |
127+
| **target** | `Enum<'drawer' \| 'dialog'>` | optional | Where the drilled list opens: 'drawer' (default, side sheet) or 'dialog' (centered modal) |
128+
| **columns** | `string[]` | optional | Field names to show as columns in the drilled list (default: the table's own columns) |
129+
| **maxRows** | `integer` | optional | Rows per page in the drilled list |
130+
131+
116132
---
117133

118134
## ChartGroupBy

‎docs/audits/2026-07-unknown-key-strictness-ledger.counts.md‎

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ regenerate.
2121
| Measure | Value |
2222
|---|---|
2323
| Triaged directories | 5 |
24-
| Object sites in them | 482 |
24+
| Object sites in them | 483 |
2525
| Still-open (strip) sites | 235 |
2626
| Files carrying at least one | 36 |
2727

@@ -43,12 +43,12 @@ The `strict` column is the one the campaign schedules against; it counts both th
4343

4444
| Dir | Sites | strict | passthrough | catchall | strip |
4545
|---|---|---|---|---|---|
46-
| `ui/` | 198 | 117 | 5 | 0 | 76 |
46+
| `ui/` | 199 | 118 | 5 | 0 | 76 |
4747
| `data/` | 162 | 41 | 1 | 0 | 120 |
4848
| `automation/` | 75 | 49 | 0 | 0 | 26 |
4949
| `security/` | 20 | 7 | 0 | 0 | 13 |
5050
| `studio/` | 27 | 27 | 0 | 0 | 0 |
51-
| **total** | **482** | **241** | **6** | **0** | **235** |
51+
| **total** | **483** | **242** | **6** | **0** | **235** |
5252

5353
## File-level triage — site counts
5454

@@ -64,7 +64,7 @@ classify and is not listed (it becomes reportable the day it grows its first sit
6464
| `animation.zod.ts` | 4 |
6565
| `app.zod.ts` | 18 |
6666
| `bulk-action.zod.ts` | 3 |
67-
| `chart.zod.ts` | 7 |
67+
| `chart.zod.ts` | 8 |
6868
| `component.zod.ts` | 29 |
6969
| `dashboard.zod.ts` | 11 |
7070
| `dataset.zod.ts` | 4 |
@@ -81,7 +81,7 @@ classify and is not listed (it becomes reportable the day it grows its first sit
8181
| `touch.zod.ts` | 7 |
8282
| `view.zod.ts` | 50 |
8383
| `widget.zod.ts` | 9 |
84-
| **total** | **198** |
84+
| **total** | **199** |
8585

8686
### `data/` — sites
8787

@@ -161,13 +161,13 @@ over it is here.
161161

162162
### `ui/` — open
163163

164-
**76 strip of 198**, in 13 file(s).
164+
**76 strip of 199**, in 13 file(s).
165165

166166
| File | Strip | Sites |
167167
|---|---|---|
168168
| `animation.zod.ts` | 4 | 4 |
169169
| `app.zod.ts` | 1 | 18 |
170-
| `chart.zod.ts` | 2 | 7 |
170+
| `chart.zod.ts` | 2 | 8 |
171171
| `component.zod.ts` | 29 | 29 |
172172
| `dnd.zod.ts` | 4 | 4 |
173173
| `i18n.zod.ts` | 5 | 6 |
@@ -178,7 +178,7 @@ over it is here.
178178
| `touch.zod.ts` | 7 | 7 |
179179
| `view.zod.ts` | 6 | 50 |
180180
| `widget.zod.ts` | 9 | 9 |
181-
| **total** | **76** | **198** |
181+
| **total** | **76** | **199** |
182182

183183
| Bucket | Sites |
184184
|---|---|

0 commit comments

Comments
 (0)