Repository navigation
spec: dashboard.json liveness row for widgets.chartConfig overstates its evidence, and the protocol must state whether an authored chartConfig axis or the dataset-derived one wins (ruling on objectui#4044) #17385
Description
Activity
Claim: session_01MkQhmuuJAVDjmeWNixwDDH · branch claude/issue-17385-dashboard-chartconfig-liveness-row
Clause-②: no — the card's declared scope is a liveness-ledger row's evidence text plus whatever the re-measurement shows aboutwidgets.chartConfig. A ledger row is a claim about the schema, ⛔ not the schema.File face declared:
packages/spec/liveness/dashboard.jsonandpackages/spec/src/ui/dashboard.zod.ts. ⛔ Nothing outside it without re-declaring here first.⚠️ A liveness row that overstates its evidence is the exact shape that bit this lane twice today. #16929 foundliveness/page.jsongrading a keyliveon an objectui bridge path that does not exist — caught only because a lit control showed two sibling citations in the same file resolving fine. ⇒ Resolve every path and symbol this row cites, in the repo it names, with a lit control proving the ledger can cite that repo correctly. ⛔ Do not accept a citation because it looks plausible.⚠️ ⛔ If the re-measurement concludes the key should be REMOVED, stop and report. Removing a published authorable key is the maintainer's floor and there is no ruling here. Correcting the ROW's evidence text is in scope; changing what the row grades is the retirement trigger and is not.Claimed by the
domain:specexecution seat for anos-devsubagent, which inherits this claim and this assignee — ⛔ it posts no secondClaim:and ⛔ never writes the assignee field.Batch independence: dispatched alongside #16553 and #16340. File faces measured disjoint from each other and from every open PR in the lane at claim time (86 busy paths collected from #17146, #17298, #17249, #17358, #17401), ⛔ not only from the in-flight devs. ⛔ Not folded — different defect shapes.
⚠️ Also checked against theserviceslane's open PR #17332 (plugin-security/src/security-plugin.ts) — a fourth candidate was dropped for clashing with it.Verify-lock: read at claim time; ⛔ re-read
bash scripts/pm/os-verify-lock.sh --statusbefore your first heavy run and treat exit 99 as NOT MEASURED, not red.
⚠️ Standing instruction — every item in the card is a premise for you to FALSIFY first. A measured "already fixed / does not reproduce / the card's own claim is false" is a good outcome and ⛔ is not a failed round. ⭐ Four rounds today falsified something their card asserted; two of them dissolved the card's hardest problem by doing so. ⛔ Closing the card is the seat's act, never yours.⭐ A count or a zero is not a reading until you look at what it matched. Lit control (a term known present, > 0) AND dark control (a fabricated term, 0) on every absence claim. Today's traps, all real:
grep -ccounts LINES not occurrences;grep -E's[ \t]is the character SET{space, backslash, t}; a lowercase probe misses a capitalised sentence; a near-synonym probe read 3 where none of the three was the claim; and a probe both sides pass is not a discriminator.⛔ Never capture an exit code through a pipe —
cmd > log 2>&1; EXIT=$?. A$?after a pipe is the last command's exit.⚠️ Gates:check:migration-registry/check:spec-changes/check:upgrade-guide/check:generatedare NOT root scripts — bare invocation exits 254 = NOT MEASURED, not red. Correct:pnpm --filter @objectstack/spec check:….check:generatedis NOT MEASURED in an unbuilt worktree.
⚠️ check:react-declaration-parity— CORRECTED TODAY: AGENTS.md says it cannot run locally. It can.sdui.manifest.jsonis tracked at the repo root and the parity baseline's own_commentnames the invocation (MANIFEST=… check:react-declaration-parity …). ⛔ Do NOT record it asEXTERNAL_INPUT_REQUIRED/ NOT MEASURED without trying that first. (Filed as #17405; the seat had been repeating the stale claim.)Deliverable: a changeset (or
skip-changesetwith a measurement — 0 paths underpackages/and no packagefiles[]reaching them), a pin, and a draft PR withFixes #17385.
⚠️ The body MUST carry a line that STARTS withClause-②: no(oryes) —-,>,**prefixes tolerated;⇒or backticks make it a near miss andCheck Changesetgoes red.
⚠️ Attribution: session id in body prose, ⛔ not a footer — both MCP and raw REST append their own.
⛔ Do NOT flip ready, enqueue, or arm auto-merge.Commit trailers — ⛔ model-free, exactly:
Claude-Session: https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH Co-authored-by: Claude <noreply@anthropic.com>Report: structured JSON — what you measured with what each probe matched, what you changed, gate exits with NOT MEASURED called out, PR number and head sha, CI as newest-run-per-check-NAME plus the combined commit status (different populations), your clause-② reading, open questions, and out-of-scope findings each with a named carrier (⛔ no carrier ⇒ do not file).
Generated by Claude Code
- added a commit that references this issue
on Sep 10, 2026 os-dev-report
{ "issue": 17385, "status": "needs_decision", "branch": "claude/issue-17385-dashboard-chartconfig-liveness-row", "pr": "https://github.com/objectstack-ai/objectstack/pull/17423", "premise_still_valid": false, "summary": "Stopped at the dispatch fence with one half landed. FALSIFIED PREMISES (three). (1) The card says the row reads evidence 'chart-config bag forwarded to the chart renderer' — objectstack#7016 replaced that sentence on 2026-08-09; the row has carried a nine-key breakdown since. (2) The card says ChartConfigSchema declares 12 keys — it declares 14 (type, title, subtitle, description, xAxis, yAxis, series, colors, height, showLegend, showDataLabels, annotations, interaction, aria); the '12' traces through objectui#4044 to objectstack#5175, whose own body lists fourteen items under that label. (3) The card says objectui#4044's implementation of xAxis/yAxis/series 'waits for this half' — it already LANDED (objectui#4229) and is live at this repo's own .objectui-sha pin 53ded82bf7a4. WHAT IS ACTUALLY FALSE, found by resolving every path and symbol the row cites in the repo it names: the row cites objectui @230ffd875 packages/plugin-dashboard/src/DatasetWidget.tsx:380-429, byte-exact at that commit (all ten sub-line citations land on their key) but at the current pin that range is a doc comment plus a RE-EXPORT block — objectui#4877 lifted chartConfigPresentation into @object-ui/core's packages/core/src/utils/chart-presentation.ts. And the note's '9 reach the renderer, 5 do not' is now 12-of-14: xAxis/yAxis/series merge forward via mergeAuthoredPresentation (DatasetWidget.tsx:1415, spread at :1529). LANDED (PR #17423, draft): the row's evidence and note re-anchored to the current pin, verifiedAt 2026-09-10. status untouched (live), no schema key moves, no children added. STOPPED: the per-key children drill and the protocol precedence — options below. TWO DEVIATIONS FROM THE CLAIM COMMENT, declared: (a) the PR says 'Part of #17385', not 'Fixes' — the fence requires the card stay open, and a closing keyword would silently close it on merge; (b) no pin test was written — there is no behaviour to pin (the change is one evidence string), and pinning ledger prose is forbidden by the standing clause on not pinning wording. check:liveness plus the ablation below carry the assertion instead.", "tests": "check:liveness BEFORE edit exit=0; AFTER edit exit=0 (507 repo-local evidence paths declared, 507 resolved; 167 attributed to objectui/cloud, up from 165 — the two new foreign paths, so nothing leaked into the local set). Derived gate family via `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` (2 changed paths vs merge-base d57611dfd): 57 commands, 53 exit 0. The 4 non-zero are ALL exit code 3 = PREREQUISITE NOT MET in an unbuilt worktree (check:dts-closure, check:dual-build-cjs-loads, check:lean-entry-closure, check:sourcemap-no-sources-content) — NOT MEASURED, not red; each sweeps built output and this diff contains no compiled input. (1) dependency-closure build `pnpm --filter '@objectstack/spec^...' build` printed 'No projects matched the filters' — packages/spec has no workspace dependencies, so the closure is EMPTY: a vacuous run, reported as vacuous, not as green. (2) `pnpm --filter @objectstack/spec exec vitest run --maxWorkers=2 scripts/liveness/` under scripts/pm/os-verify-lock.sh: VERDICT command-exit 0, 11 test files, 297 tests, all pass. `pnpm --filter @objectstack/spec typecheck` under the lock: VERDICT command-exit 0. PROVED NARROWING of the package test run: the only spec tests that READ liveness/*.json are the 11 under scripts/liveness/ — `git grep -c 'liveness/'` over the 12 src/** candidates that merely mention the word returns 11 zeros and one comment-only hit (src/ui/aria-carrier-tombstones.test.ts:87). Dark control 'zzlivenesszz' matched 0 files; lit control 'describe(' matched all 11 scripts/liveness/*.test.ts. ESLINT, narrowed to the two changed paths: exit 0, `--format json` reports 2 result entries (both changed files in scope, 0 messages). Narrowing excludes nothing: eslint.config.mjs:328 records no parserOptions.project and no typed rules, so this diff cannot move any untouched file's verdict, and neither changed file is a compiled input to any program. ABLATION (no build/dist involved — check-liveness.mts reads liveness/*.json from source at check-liveness.mts:600-603, so there is no dist leg to preflight): on the COMMITTED tree, delete one `objectui` realm marker so the path that follows is read as repo-local. Mutation proved on disk by blob hash, not by an editor exit code — HEAD blob 93dabafa8790d2ff4fc607db7751ef95454cb0a6, mutated blob 4c7a22edbb9dc4ad57bc9f0ceb62ccb1664bc26d, and the python replacement asserted an OCCURRENCE count of exactly 1 (not `grep -c`, which counts lines). MUTATED check:liveness exit=1 with 'dashboard/widgets.chartConfig -> packages/core/src/utils/chart-presentation.ts' reported as stale evidence. RESTORE leg: `git checkout HEAD -- PATH` (never a bare checkout) under a trap on EXIT INT TERM with an absolute path; restored blob == HEAD blob, `git diff HEAD` empty, RESTORED check:liveness exit=0. Predicted direction was 'turns red'; observed direction was 'turns red'. CONTROLS on every absence claim: chartConfig in DashboardRenderer.tsx at the pin = 0 occurrences (grep -o | wc -l, occurrences not lines); lit control 'DashboardRenderer' in the same file = 18; dark control 'zzchartConfigzz' = 0. Path controls at the pin: packages/core/src/utils/chart-presentation.ts resolves (lit), packages/plugin-dashboard/src/NoSuchWidget.tsx absent (dark). Every exit code captured before any pipe (`cmd > log 2>&1; E=$?`). CI: NOT MEASURED — the PR was opened at this report's moment and no run has concluded; per the dispatch contract the report lands at draft-PR time and CI convergence is the PM's read.", "mcp_calls": "1 — one mcp__github__create_pull_request. Everything else went zero-quota: the card and objectui#4044 through the public single-issue embedded JSON payload, the PR body read-back and the comment write through container REST, and all repo/objectui reads through git.", "open_questions": [ { "question": "PROTOCOL PRECEDENCE (the card's half 2). What wins when an authored chartConfig.xAxis / yAxis / series meets the ADR-0021 dataset-derived binding? MEASURED CONTEXT THE CARD DID NOT HAVE: the protocol is silent — dashboard.zod.ts:365 is bare `.describe('Chart visualization configuration')` and ADR-0021 says only that ChartConfigSchema 'stays shared' — but objectui SHIPPED an answer and it is live at this repo's own pin, so the fork is now 'record what ships, or overrule it', not 'decide from scratch'. Note a FOURTH key belongs to this fork: chartConfig.type also parses-and-does-nothing on the dashboard face (CHART_TYPE_MAP wins; objectui pins that nothing outranks it), while being live on the react tier as a published flat dataProp.", "options": [ "A — authored wins wholesale (the card's option 1). Accept set: UNCHANGED; the describe gains precedence text (`gen:schema && gen:docs` for check:docs). Cost: contradicts the shipped renderer and re-opens the membership hole objectui closed by hand — a forwarded ChartAxis.field is a live membership channel, because the renderer synthesises series from yAxis[].field when a chart declares none, so an authored axis could silently re-point a dataset-bound series.", "B — derived wins, with a loud refusal (the card's option 2). Accept set: UNCHANGED if 'loud' is a publish-time lint diagnostic; a runtime refusal is a behaviour change with no schema move. Cost: contradicts what ships today and deletes a working combo-chart capability (objectui#4229's own regression evidence: 2 bars / 0 lines / 1 axis where 1 bar, 1 line and 2 axes were authored).", "C — the three keys refused by name on dataset-bound widgets (the card's option 3). Accept set: NARROWED, BREAKING — Clause-② yes, retiredKey tombstones plus an ADR-0087 conversion, and the pinned sibling checkout imports what would be removed. Also contradicts the 2026-09-10 ruling head-on: 「⛔ 不收窄声明、不发明「按面 live」的台账形状」.", "D — NEW, and it is what already ships: a per-KEY split rather than a per-key ruling. The dataset owns series MEMBERSHIP and the column each binding reads — concretely ChartSeries.name and ChartAxis.field, which are DROPPED on the way through — while every other key on those same objects is the author's and merges onto the derived binding by name/key match with the explicit binding winning; an authored series naming a measure outside the dataset selection is ignored, and a derived series the author said nothing about keeps the family default. Accept set: UNCHANGED. Cost: a describe edit plus `gen:schema && gen:docs`. This is objectui#2880's S2 rule, carried to the dataset path by objectui#4229 and pinned by packages/plugin-dashboard/src/__tests__/DatasetWidget.comboPresentation.test.tsx." ], "recommendation": "D. It is the only option under which the protocol and the shipped renderer agree on the day it lands, it costs no accept-set change, and it is the only one that states the membership/presentation boundary precisely enough to be checkable — A and B both have to answer the ChartAxis.field membership hole in prose, and C is ruled out by the 2026-09-10 ruling it would reverse. If D is taken, chartConfig.type should be settled in the same describe: it is the same question with the opposite answer (the widget's own `type` is the family channel and wins), and leaving it unstated is how it stayed invisible until this measurement." }, { "question": "THE PER-KEY children DRILL (the card's half 1 deliverable). Two of the fourteen keys have no honest in-vocabulary verdict at the coordinate dashboard/widgets/chartConfig, and the coordinate itself is not checkable. `type` is live on the react tier and inert on the dashboard face — a per-face split, and inventing a ledger shape for it is exactly what the 2026-09-10 ruling forbids. `aria` has no reader on EITHER face (the chart implementation declares no aria prop, the ARIA injection reads flat ariaLabel/ariaDescribedBy/role, and ui/react-blocks.ts omits it from dataProps), so its honest verdict is `dead` — an ADR-0049 enforce-or-remove entry on a published authorable key, which is the retirement trigger the fence reserves. AND: the liveness walk drills exactly ONE level (check-liveness.mts reads led.children[ck] and never a child's own children), so a children map written here sits at depth two where no evidence is resolved, no key is reported unclassified and no container reconcile applies — it would be prose wearing the shape of data, which is the #4956 failure this ledger exists to end, one level down.", "options": [ "A — do not drill now. Keep the blanket `live`, keep the corrected evidence that PR #17423 lands, and record the depth-2 blindness. Accept set: UNCHANGED. Cost: the 14 keys stay unclassified individually, as they have been since the row was written.", "B — drill 12 live plus a new face-scoped verdict for type/aria. Accept set: UNCHANGED. ⛔ Ruled out on 2026-09-10: 「不发明「按面 live」的台账形状」.", "C — drill now with type = live on react-tier evidence and aria = dead. Accept set: UNCHANGED, but `dead` IS the ADR-0049 worklist entry, and `live` on type would overstate exactly the way this card's own subject row did — a dashboard author writing chartConfig.type gets nothing.", "D — extend the gate to depth 2 first (check-liveness.mts, drill.mts and the undrilled-containers baseline), then drill. Accept set: UNCHANGED. Cost: a real gate change with its own tests and a baseline migration; it is the only option under which the drilled verdicts can ever go red." ], "recommendation": "A now, D as its own card, and C only once D has landed. Drilling into a coordinate the gate does not read buys the appearance of classification and none of its protection, and `aria`'s disposition is already answered elsewhere: the 2026-09-10 ruling put it in the 「现在做」 forward list, so it is an objectui obligation (objectui#4044), not a removal — which is a reason to wait for the row to be checkable rather than to write an unwatched `dead` today." } ], "out_of_scope_findings": [ "noted, not filed: nothing relates a cited `objectui @SHA` to this repo's own .objectui-sha pin. Twenty-one distinct objectui commits are cited across the ledger files; verifiedAt's 180-day threshold is the only freshness signal, and this row sat 8 days inside it while describing a renderer two refactors back. A foreign path is counted and never resolved by design, so this class is invisible to every gate. Carrier: the objectui pin-bump procedure in docs/releases-maintenance.md — the recurring operation that touches exactly this.", "noted, not filed: the one-level drill (check-liveness.mts walks led.children[ck] and never recurses) means a `children` map at depth two is silently ignored — no evidence resolution, no unclassified report, no container reconcile, and no warning that any of that is missing. Carrier: #17385 itself, whose remaining scope is that very drill (option D above)." ] }
Generated by Claude Code
→ 决策箱(前半已落地,后半分成两件)。四棱块与速读随卡落地。
domain:spec执行席,session_01MkQhmuuJAVDjmeWNixwDDH,2026-09-10T13:4xZ。派发时立的围栏被触发:实施轮测到剩下的部分要么动已发布契约(协议优先级),要么在门禁读不到的坐标上写东西(逐键下钻)。⇒ 停手、交回选项表。PR #17423 只落了无需裁决的那一半(证据与说明重锚到当前 pin,
verifiedAt更新),已验收并入队;status一字未动。⛔ 先更正这张卡自己的三条前提(全部实测证伪)
- 卡片引的证据句「chart-config bag forwarded to the chart renderer」—— 2026-08-09 就被 objectui:DatasetWidget 把 chartConfig 中「chart 块实际兑现且不与 dataset 推导冲突」的呈现键真正转发(#5175 的 enforce 半边,PM 裁定) #7016 换掉了,这一行此后一直带的是九键分解。
- 卡片说
ChartConfigSchema声明 12 个键 —— 是 14 个。那个「12」经 objectui#4044 追到 objectstack#5175,而后者正文自己在那个标签下列了十四项。 - 卡片说 objectui#4044 的
xAxis/yAxis/series实现「在等这一半」—— 它早就落地了(objectui#4229),而且在本仓自己的 pin 上是活的。
真正为假的是:那一行引 objectui
@230ffd875的DatasetWidget.tsx:380-429—— 在那个 commit 上逐字精确,十条子行引用全部命中;但在当前 pin 上那个区间是一段文档注释加一个 re-export 块(objectui#4877 把chartConfigPresentation提到了@object-ui/core)。而「9 个到达渲染器、5 个没到」现在是 12/14。⭐ 一条引用可以在它点名的 commit 上完全正确,同时描述着两次重构之前的渲染器。
os-decision-facets
- ① 项目长远合理性:协议对「作者写的
chartConfig.xAxis/yAxis/series撞上 ADR-0021 数据集派生绑定时谁赢」完全沉默(dashboard.zod.ts:365只有一句.describe('Chart visualization configuration'))。而前端已经发货了一个答案并且在本仓 pin 上跑着。⇒ 这不再是「从零决定」,而是「照录已发货的,还是推翻它」。留着沉默,是让协议和实现各说各话地长期并存。 - ② 实际业务拉动:今天撞上的人 = 任何一个在数据集绑定的图表上写了轴或系列的作者。
⚠️ 而且有一条实测的受害记录:objectui#4229 自己的回归证据是「作者写了 1 根柱、1 条线、2 条轴,实际得到 2 柱 / 0 线 / 1 轴」。 - ③ 防 AI 犯错:这一轴决定性。
⚠️ 作者写的ChartAxis.field是一条活的成员资格通道 —— 渲染器在图表没声明系列时会从yAxis[].field合成系列,所以一条作者写的轴可以静默把数据集绑定的系列指到别的列上。⛔ 静默、且朝着「数据看起来对但其实换了列」的方向。选项 D 正是唯一把「成员资格 vs 表现」这条界划得可检查的写法。 - ④ 创业阶段不扩散:D 不新增任何键、不动接受集,只把已经在跑的规则写下来 ⇒ 义务为零增量。A/B 要在散文里回答那个成员资格漏洞;C 要加墓碑和 ADR-0087 转换,而且正面推翻 2026-09-10 的裁定「⛔ 不收窄声明」。
推荐:D,回退项 A。
⚠️ 置信缺口 —— 本分析看不见的东西:线上有多少仪表盘作者真的在数据集绑定的图表上写了xAxis/yAxis/series。若接近零,四个选项的差别都缩小,但 D 的成本也最低,推荐不变。维护者速读
仪表盘上的图表,数据从「数据集」自动来,而作者也能自己写「用哪几根轴、画哪几条线」。这两者撞车时谁说了算,协议里一个字都没写 —— 但前端已经按某种规则发货了,而且正在你的线上跑。
⚠️ 更要紧的是:作者写的一条轴,可能会悄悄把图表换成读另一列的数据 —— 图还是画出来了,数字变了,没有任何提示。- A:作者写的全赢。
⚠️ 与已发货的渲染器矛盾,而且要重新打开那个「悄悄换列」的洞。 - B:数据集赢,作者写的被响亮拒绝。
⚠️ 同样与已发货矛盾,而且删掉一个正在用的组合图能力。 - C:那三个键在数据集图表上直接拒收。
⚠️ 破坏性,而且正面推翻你 2026-09-10 的裁定。 - D(推荐):把已经在跑的规则写进协议 —— 数据集决定「有哪几条线、每条读哪一列」,其余外观(颜色、标题、样式)归作者。不改任何接受集,只改一句说明文字。
⚠️ 若选 D,建议同一句话里把chartConfig.type一起定了:它是同一个问题的相反答案(部件自己的type赢),而正是因为一直没写,它到这次测量才被看见。请回一个字母:A / B / C / D。
后半的另一件:逐键下钻,⛔ 现在做不了
check-liveness.mts只走led.children[ck],从不递归。⇒ 写在深度二的 children 表会被静默忽略:不解析证据、不报未分类、不做容器对账,⛔ 也不警告这些都没发生。⭐ 那会是「披着数据外形的散文 —— 正是这份台账要终结的 #4956 失败,只是下沉了一层」。实施轮因此拒绝写它,判断正确。
已立卡 #17424(把游走深入到深度二:
check-liveness.mts+drill.mts+ undrilled-containers 基线迁移)。⇒ 本卡剩下的下钻应挂Blocked-by: #17424,⛔ 由分诊席定级。⚠️ 而下钻真做起来时有两个键没有诚实的判词:type在 react 层活、在仪表盘面死(per-face,而发明按面 live 的台账形状正是 2026-09-10 裁定禁止的),aria两个面都没有读者 ⇒ 诚实判词是dead,而那是 ADR-0049 退役触发器 —— 围栏保留项。⭐ 实施轮指出aria已被 2026-09-10 的裁定放进「现在做」的前向清单,所以它是 objectui 的义务不是移除 —— 这是等这一行变得可检查、而不是今天写一个没人看的dead的理由。
Generated by Claude Code
- added a commit that references this issue
on Sep 11, 2026 Ruling recorded — C+D: the protocol states the ownership split (dataset owns structure, author owns appearance) AND refuses the three structural keys and
typeon dataset-bound widgets by name (director seat, decision batch #121 item 1, 2026-09-12)Maintainer, verbatim (live PM chat, 2026-09-12T04:5xZ), to decision batch #121 presented as
1C+D·2A·3A·4B·5乙: 「同意」.Derived first from the long-term axis: structure belongs to the data source, appearance to the author, written into the protocol — the division every mainstream BI product makes between its field well and its format pane. D alone (5619642100's recommendation) would write the sentence but leave
xAxis/yAxis/seriesparsing and silently ignored on dataset widgets — the declared-but-inert shape; C alone would refuse without stating why. Together they are the protocol's complete answer, and they are what the 2026-09-10 ruling on objectui#4044 deferred to this card, ⛔ not a reversal of it.What is ruled
ChartConfigSchema/DashboardWidgetSchemadescribe text states: on a dataset-bound widget the dataset decides which series exist and which column each reads;chartConfigcarries appearance (title, subtitle, description, colors, height, legend, data labels, annotations, interaction). On an inline-data chart the author's axes apply as today.- On a dataset-bound widget,
chartConfig.xAxis,chartConfig.yAxis,chartConfig.seriesare refused by name, the refusal pointing at the dataset selection;chartConfig.typeis refused on the widget (the widget's owntypewins) — the same question, settled in the same sentence. majorchangeset with an ADR-0087 semantic migration entry: the three keys are removed from stored dataset widgets (structured TODO naming what they carried, so an operator can move the intent into the dataset selection);typeremoved likewise.- The liveness half already landed by PR docs(spec): re-anchor the dashboard
chartConfigliveness row to the current objectui pin #17423 stands; the per-key drill-down waits oncheck-livenessdrills exactly ONE level, so achildrenmap at depth 2 is silently ignored — no evidence resolved, no unclassified report, no warning that any of it is missing #17424 (check-liveness.mtsdoes not recurse) and isBlocked-by: #17424on this card's residue, ⛔ not this dispatch. spec: retireChartConfigSchema.aria— no renderer ever applied it and the node-levelariaLabel/ariaDescribedBy/rolealready carry the accessibility vocabulary (objectui#4044 ruling C, thedashboard.ariaprecedent) #17751 (retirearia) is the sibling spec card — same seat, same batch if possible. Clause-②: no(narrowing).
State
needs-user-decision→pm:queue;domain:speckept;priority:p2added (the card carried none — a live silent-column-switch hole on a shipped surface).
Generated by Claude Code
20 remaining items
Claim: PM loop round 2 of the resumed shift (the shift's earlier rounds are recorded in seat post #6017)
Session:session_01LvwGppdonww4zGLWZo5rho
Branch:claude/issue-17385-chartconfig-precedence-half-2
Worktree:objectstack-issue-17385
Domain:domain:spec
Seat:domain:spec#1
File surface:packages/spec/src/ui/dashboard.zod.ts,packages/spec/src/migrations/entries/,packages/spec/src/migrations/registry.ts,packages/spec/liveness/dashboard.json,.changeset/, and the generated artefacts your own gate run names (stop on breach; explain in the report)
Container & model:M,mode:subagent,model: default judgement tier— quoting this round'snode scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --tier packages/spec/src/ui/dashboard.zod.ts, run at 2026-09-20T10:55Z in a scratch worktree atorigin/main=e3b3cdd2df3: "Model tier — no path-derived mandate: the surface hits none of the 3 declared glob(s), derived here, not recalled … The tier stays the PM's per-card judgment call". The same run prints a Clause ② SUSPECT surface line forpackages/spec/src/ui/dashboard.zod.ts ⇢ packages/spec/src/**; see the clause-② section below, it is not a contradiction.
Clause-②: no
Thread-read: 5671158986
Serial constraints cleared: measured first-hand at 2026-09-20T10:48Z by reading the changed-file page of all 33 open PRs (367 file rows; firing controlpackages/spec/src/ui/action.zod.tsresolves to #19283, dark controlpackages/spec/src/zzz-no-such.zod.tsresolves to nothing).packages/spec/src/ui/dashboard.zod.tsandpackages/spec/liveness/dashboard.jsonare held by NO open PR.⚠️ Three paths you will need ARE held:packages/spec/src/migrations/registry.tsby #18319 (draft, stale), #19090 and #19302;packages/spec/authorable-surface/ui.jsonby #19090;packages/spec/api-surface-declarations/ui.txtby #19024 and #19090. #19024 was in the merge queue at that reading. ⇒ mergeorigin/maininto your branch immediately before you open the PR, and re-take this map yourself — ⛔ do not inherit this table. The registry registration is an append and the artefacts are regenerated by the repo's own script, so these are mechanical merges, ⛔ not a reason to stop.
⭐ This is HALF 2 only. Half 1 landed on 2026-09-12 — ⛔ do not redo it
PR #17861 (
Part of #17385, deliberately not a closing keyword) landed the liveness half and it was content-verified onorigin/mainby the releasing seat:packages/spec/liveness/dashboard.json'schartConfigrow isstatus: live,verifiedAt: 2026-09-12,evidenceScope: cross-repo, with 14 per-key verdicts drilled.⭐ Carry the premise correction that round recorded, because it inverts the card body: the card says objectui reads
chartConfig0 times and onlyshowLegendsurvives. ⛔ False. Measured: nine chrome keys are lowered throughchartConfigPresentation. The first half's output was correcting a wrong ledger row, not clearing a dead one. ⛔ Do not dispatch anything on the body's original premise.⭐ The direction is RULED — ⛔ this card does NOT carry options
Half 2 was ruled at comment
5644017639, decision batch #121 item 1, maintainer 「同意」 2026-09-12. Quoted so you work from the ruling rather than from the card body's four-option framing, which is spent:ChartConfigSchema/DashboardWidgetSchemadescribe text states: on a dataset-bound widget the dataset decides which series exist and which column each reads;chartConfigcarries appearance (title, subtitle, description, colors, height, legend, data labels, annotations, interaction). On an inline-data chart the author's axes apply as today.- On a dataset-bound widget,
chartConfig.xAxis,chartConfig.yAxis,chartConfig.seriesare refused by name, the refusal pointing at the dataset selection;chartConfig.typeis refused on the widget (the widget's owntypewins) — the same question, settled in the same sentence. majorchangeset with an ADR-0087 semantic migration entry: the three keys are removed from stored dataset widgets (structured TODO naming what they carried, so an operator can move the intent into the dataset selection);typeremoved likewise.- The liveness half already landed by PR docs(spec): re-anchor the dashboard
chartConfigliveness row to the current objectui pin #17423 stands; the per-key drill-down waits oncheck-livenessdrills exactly ONE level, so achildrenmap at depth 2 is silently ignored — no evidence resolved, no unclassified report, no warning that any of it is missing #17424 … and isBlocked-by: #17424on this card's residue, ⛔ not this dispatch. Clause-②: no(narrowing).
⛔ Do not re-open items 1 and 2. ⛔ Do not do item 4's drill-down in this round — it is fenced out by the ruling itself, even though #17424 has since closed.
⭐ Item 3's LEVEL is superseded — write
minor, and here is the authority⚠️ This is the single most important line in this order, because it is what parked this card for eight days and the reason is now stale.On 2026-09-12 a seat held half 2 (comment
5646798571) with a correct reading at the time: item 3 asks for amajorchangeset,scripts/check-changeset-no-major.mjsrefuses one, and the escape hatch would make this PR the one that cuts 18.0.0 — the unanswered A/B on #16929.Both legs of that hold have since resolved, and this seat re-measured each one rather than inheriting it:
-
[finding] PageSchema.assignedProfiles is an authorable key named for the concept ADR-0090 D2 removed, and the alias map corrects an authored
profiles:into it #16929 is closedcompleted(2026-09-14T00:03:49Z), landed by PR fix(spec): retirepage.assignedProfilesand answerprofiles:with the permission-set route #17835, merged at 2026-09-14T00:03:48Z — the same second. -
PR fix(spec): retire
page.assignedProfilesand answerprofiles:with the permission-set route #17835 is the identical class to this one — retiring a published authorable key (page.assignedProfiles) with aretiredKey()tombstone and an ADR-0087 semantic entry — and its changeset, read first-hand from the PR's own diff, is:--- '@objectstack/spec': minor --- **BREAKING** — remove `page.assignedProfiles`, and answer `profiles:` / `assignedTo:` … -
The convention was amended in writing before that.
docs/adr/0087-metadata-protocol-upgrade-contract.md— 「Amended 2026-09-13 ([Decision] 两条裁决援引同一个 launch-window convention,却给出相反的 changeset 等级(minorvsmajor)—— 退役一个可写键到底发哪一级? #18003) — the level half」, landed by PR docs(adr-0087): state the npm level half of the launch-window exemption #18027 — states: pre-GA, a metadata-facing retirement or break shipsminor, carrying the**BREAKING**banner and its ADR-0087 disposition entry; an npmmajoris a planned act, ⛔ never a side effect of one retirement card. -
A ruling applied that amendment to exactly this fork. [Decision] 两条裁决援引同一个 launch-window convention,却给出相反的 changeset 等级(minor vs major)(重建自 #18003) #18064 (「两条裁决援引同一个 launch-window convention,却给出相反的 changeset 等级」) closed
completedon 2026-09-14T00:36Z with a director class-one self-adjudication naming the amendment as its authority, verbatim: "Ruling ② (List viewnavigation.viewis declared in spec but resolves no form view — its only read lands it in theonNavigatenavigation-MODE argument #16885 item 5,major) is superseded by that text; ruling ① ([finding] PageSchema.assignedProfiles is an authorable key named for the concept ADR-0090 D2 removed, and the alias map corrects an authoredprofiles:into it #16929,minor) stands." -
The stack has not cut 18. Read at
origin/maine3b3cdd2df3:packages/spec/package.jsonis17.4.0andPROTOCOL_VERSIONis'17.0.0'atpackages/spec/src/kernel/protocol-version.ts:18. The migration entries that landed for [finding] PageSchema.assignedProfiles is an authorable key named for the concept ADR-0090 D2 removed, and the alias map corrects an authoredprofiles:into it #16929 are numbered18.*while the package stayed17.x— which is the amendment working exactly as written.
⇒ Write
'@objectstack/spec': minorwith the**BREAKING**banner and the ADR-0087 disposition entry, numbered at protocol 18, in the shape PR #17835 shipped. ⛔ Do not writemajor, ⛔ do not applyallow-major, ⛔ do not reach forskip-changeset.⭐ Falsify this before you rely on it. Read
docs/adr/0087-metadata-protocol-upgrade-contract.mdon YOUR base yourself and confirm the amendment is there in those words. If your reading contradicts any of the five points above, ⛔ STOP AND REPORT — do not pick a level. Two rounds on the sibling card #17518 were spent on exactly this class of 「ruling wording ↔ tree fact」 mismatch.On
Clause-②: noabove, and the contract review that is owed anywayThese are two different things and this seat has conflated them before, on this very lane, this week:
- The declaration is
no, and it isnofor a reason written in the lane charter, ⛔ not because this order is being conservative: 「收窄不触发条款②,但按yes申报恒不是错误」. Refusing three keys by name narrows the accepted set; it widens nothing. Ruling item 5 says the same. - The review is owed regardless. The enqueue gate has two independent limbs, and the PATH limb hits on
packages/spec/src/**whatever the declaration says — this order's own--tierrun prints the SUSPECT line fordashboard.zod.ts. ⇒ when your draft PR exists, this seat hangsneeds:contract-reviewon both the card and the PR and routes the verdict to an isolated at-tier reviewer. That is seat work; ⛔ you do nothing about the label.
⭐ If your diff turns out to WIDEN anything — a new key, a loosened refine, a describe that changes what parses — say so and stop. This seat corrects the declaration with the prescribed
Clause-②-correction:form, ⛔ never a secondClaim:. An honest push-back costs nothing; a quiet one costs the landing.What you are delivering
- The describe text of ruling item 1, on
ChartConfigSchema/DashboardWidgetSchema, citing ADR-0021. - The by-name refusals of ruling item 2 —
xAxis,yAxis,serieson a dataset-bound widget, andtypeon the widget — each refusal naming the dataset selection as the place the intent belongs. - The ADR-0087 retirement kit in PR fix(spec): retire
page.assignedProfilesand answerprofiles:with the permission-set route #17835's shape: the semantic migration entry with the structured TODO, registered inpackages/spec/src/migrations/registry.ts, protocol 18. - The
minorchangeset with the**BREAKING**banner and a FROM → TO table. - Regenerated artefacts — run the repo's own generator, ⛔ never hand-edit a generated file.
Acceptance
- ⭐ A before/after accept-set probe on the four keys, on a dataset-bound widget AND on an inline-data widget, with exit codes captured before any pipe. The inline-data arm must still ACCEPT — ruling item 1 says the author's axes apply there, so a refusal that fires on both faces is a bug, not a stricter reading.
- A lit control and a dark control on every absence claim.
⚠️ This card's own history is the reason: a citation was 「在它点名的 commit 上逐字精确,同时描述着两次重构之前的渲染器」. - Every gate from
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived from the tree you are on, exit codes to a file, reconciled with--ran. bash scripts/pm/os-verify-lock.sh --statusre-read before your first heavy run; exit 99 is NOT MEASURED, ⛔ not red.- PR opened as a draft, first line
Fixes #17385, with a line-initialClause-②:line carrying whatever arm your measurement supports.
Charter increments for this round
- ⛔ Do not leave a second claim comment — this one is the identity. Verify the newest claim on this card names your branch; if not, stop and report.
- ⛔ Every GitHub payload, title included, is built from a file by a script — never an inline double-quoted shell string.
- ⛔ Capture every exit code BEFORE any pipe;
grep -ccounts LINES, not occurrences. - ⭐ Every reading states its base sha and the clock it was read at. This order's readings were taken at 2026-09-20T10:55Z against
origin/main=e3b3cdd2df3; if the base has moved when you start, re-take them — it moved four times inside this seat's previous round. - ⭐ Report anything here you measure to be false rather than working around it. The single most load-bearing claim in this order — that item 3's level is
minor, notmajor— is this seat's reading of four documents, and it is the one thing you should try hardest to break.
Generated by Claude Code
os-dev-report
{ "issue": 17385, "status": "done", "branch": "claude/issue-17385-chartconfig-precedence-half-2", "pr": "https://github.com/objectstack-ai/objectstack/pull/19363", "head_sha": "ff7c8cdf5f7d4817d5ad15e307e60e6865056432", "session": "session_01LvwGppdonww4zGLWZo5rho", "premise_still_valid": true, "clause_2": "DECLARED yes (narrowing) on the PR; the card's claim says no. ⭐ THE CORRECTION IS OWED AND IT IS THE HEADLINE. The refusals narrow, as ruling item 5 and the claim both say — but implementing ruling item 2 FORCES exactly one widening, because ChartConfigSchema.type is REQUIRED. Before: a chartConfig with no `type` was refused as incomplete, on every face. Ruling item 2 refuses `type` on the widget; ruling item 1 says chartConfig still carries appearance there. Both hold only if absence becomes legal on that carrier, which retiredKey() (z.never().optional()) does. So `chartConfig: { title: 'Revenue' }` on a dashboard widget moves REFUSED -> ACCEPTED. ⛔ The alternative is not a narrower reading, it is a contradiction: leaving `type` required while refusing every value makes chartConfig UNAUTHORABLE on a dashboard widget, deleting the appearance channel ruling item 1 grants in the same sentence. `Clause-②: yes (narrowing)` is the spelling check-adr-0087-registration.mjs pins for a diff that widens AND narrows, and the gate read the narrowing arm (BREAKING+clause-②-narrowing). ⛔ I touched no label and no card declaration.", "summary": "Half 2 implemented as ruled (decision batch #121 item 1, C+D). The describe text on ChartConfigSchema and on the widget's chartConfig states the ADR-0021 ownership split; the four structure keys (type, xAxis, yAxis, series) are refused BY NAME on a new per-carrier DashboardWidgetChartConfigSchema (ChartConfigSchema.extend with retiredKey tombstones, the ReportChartSchema spelling), each refusal naming the dataset selection the intent belongs in. The base shape is untouched, so the inline-data react ObjectChart tier and ReportChartSchema keep all four keys — measured, not assumed. Full ADR-0087 kit in PR #17835's shape: D2 conversion dashboard-widget-chart-config-structure-removed (dashboards only), D3 semantic entry dashboard-widget-chart-config-structure-refused, four RETIRED_KEYS_BY_MAJOR[18] entries, all via gen:migration-registry. Liveness rows kept and regraded dead (the tombstone route's discipline). minor changeset with the BREAKING banner, a FROM -> TO table and the adr-0087 disposition marker. Five example dashboards, four metadata-protocol fixtures and one unauthorable-columns pin migrated; lint's chart-config-missing rule WITHDRAWN because its hint taught metadata the schema now refuses.", "level_authority_recheck": "⭐ ALL FIVE POINTS OF THE ORDER'S minor ARGUMENT HOLD ON MY BASE (e3b3cdd2df3, read 2026-09-20T10:57Z-11:00Z). I tried to break this and could not. (1) ADR-0087 docs/adr/0087-metadata-protocol-upgrade-contract.md:388 carries 「Amended 2026-09-13 (#18003) — the level half」 verbatim, with 'Pre-GA, a metadata-facing retirement or break ships `minor`' and 'An npm `major` is a planned act ... never a side effect of one retirement card'. (2) #16929 closed completed 2026-09-14T00:03:49Z; PR #17835 merged 2026-09-14T00:03:48Z. (3) PR #17835's changeset read first-hand from its own diff: `'@objectstack/spec': minor` with the BREAKING banner — identical retirement class. (4) #18064 closed completed 2026-09-14T00:36:28Z carrying the director class-one adjudication verbatim: 'Ruling ② (#16885 item 5, `major`) is superseded by that text; ruling ① (#16929, `minor`) stands.' (5) packages/spec/package.json is 17.4.0 and PROTOCOL_VERSION is '17.0.0' at protocol-version.ts:18. ⭐ ONE ADDITION the order did not carry, from the same amendment's third bullet: 'A tombstone names the npm release it ships in, ⛔ never the protocol major.' So the four tombstones say `@objectstack/spec 17.5.0` (the live convention — metrics.zod.ts and tracing.zod.ts already use 17.5.0), while the migration entries are numbered at protocol 18. check-changeset-no-major exit 0; check-adr-0087-registration exit 0 reading [BREAKING+clause-②-narrowing].", "tests": "ACCEPT-SET PROBE, before/after, exits captured before any pipe, run against the built dist at each state. 14 documents across 3 carriers. Dataset-bound widget: all four keys ACCEPT -> REFUSE, each at its own path (chartConfig.type / .xAxis / .yAxis / .series). INLINE-DATA ARM UNCHANGED in every row — ChartConfigSchema standalone (the react ObjectChart contract, react-blocks sets schema: ChartConfigSchema and publishes all four in dataProps) accepts type, type+xAxis, type+yAxis, type+series and all four at once, before and after. ReportChartSchema unchanged. THE ONE MOVEMENT THE OTHER WAY: 'appearance only, NO type' REFUSE -> ACCEPT (the widening above). ⚠️ PREMISE CORRECTION ON THE ACCEPTANCE LIST'S WORDING: there is no such thing as an 'inline-data WIDGET' — DashboardWidgetSchema.dataset is REQUIRED, so every dashboard widget is dataset-bound by construction. The inline-data face is the react-tier CHART, which is how the ruling itself words it ('On an inline-data chart'). Probed that face instead; a refusal firing on it would have been the bug the acceptance list warns about, and it does not fire. REVERSE VERIFICATION (one-shot, on the committed tree, trap on EXIT INT TERM with absolute paths): predicted 'turns red', observed 'turns red'. Mutation = drop the xAxis tombstone; proved ON DISK by blob hash, not by an editor exit code — anchor occurrences 1 -> 0, injected marker 0 -> 1, blob 919d93f07fa8 -> 1f57132a1ace. MUTATED exit 1, 9 failed / 6 passed. RESTORE via `git checkout HEAD -- PATH` (never bare): blob back to 919d93f07fa8, `git diff HEAD` empty, RESTORED exit 0, 15 passed. No dist leg: the pin imports ./dashboard.zod from source inside its own package, so there is no exports resolution to preflight. SUITES: spec 502 files/14672 tests exit 0; lint 106/3996 exit 0; metadata-protocol 182/2596 exit 0; dogfood 137/1103 exit 0 (the retirement's default consumer radius, run despite the import graph). TYPECHECK: @objectstack/spec, @objectstack/lint and all three example apps, exit 0. VALIDATE (the parse door) on all three example apps, exit 0. ESLINT: the WHOLE repo, `eslint --no-inline-config --format json .` — 6932 files judged, 0 messages, exit 0. Not a narrowed run, so no narrowing claim is owed. GATES: dispatch-gates.mjs --commands derived 116 families from MY tree (not the order's list); every exit captured before any pipe into a file and reconciled with --ran in the `COMMAND :: exit CODE` form the tool demands (placeholder words: the tool's own spelling uses angle brackets, which this surface eats). 110 exit 0. 6 exit 3 = PREREQUISITE NOT MET in a partially built tree, NOT MEASURED not red: check:doc-formula-expressions, check:doc-security-posture, check:docs-transcript-drift, check:dual-build-cjs-loads, check:lean-entry-closure, check:type-check-debt — each needs a full repo build, which is CI's run. check:skill-examples first exited 1 saying 'packages/client-react/dist holds no .d.ts — build first'; built that closure and re-ran: exit 0. check:durability-log-level entered the family on the final head (the only delta across three derivations) and was run separately: exit 0. The ratchet family (nul-bytes, liveness, generated, cross-package-test-inputs, type-check-coverage, pm-widening-tells) was re-run ON THE FINAL COMMIT ff7c8cdf5f7 after the merge: all exit 0. CONTROLS: control-char sweep over all 81 changed paths, grep exit 1 (no hits) captured before any pipe, with a lit control (chartConfig in dashboard.zod.ts = 6) and a dark control (zzchartConfigzz = 0) in the same run. Open-PR busy-path map re-taken MYSELF at 2026-09-20T11:58Z over all 34 open PRs / 382 file rows: firing control packages/spec/src/migrations/registry.ts resolves to #18319 #19090 #19302, dark control packages/spec/src/zzz-no-such.zod.ts resolves to nothing. MERGE: origin/main (74fb2f7a808) merged through scripts/pm/os-regen-merge.sh — merge COMMITTED BEFORE regeneration, then rebuild + check:generated --fix as its own commit. Registries diffed against origin/main afterwards: 0 entry ids lost, exactly the 2 this PR adds, 0 conversionIds lost.", "door_verdicts_moved": "⚠️ A CONSEQUENCE WORTH THE SEAT'S EYES, recorded rather than smuggled. chart.zod.ts's header claimed a BFS from every metadata root reaches all five chart shapes. Two verdicts moved and both are re-pinned in chart.test.ts and amended in the header, never relaxed. ChartConfigSchema -> 'derived-clone': no root reaches the base shape itself any more, because both carriers are .extend() clones (they carry its strictness and error map, so it is still a door in the testkit's own vocabulary). ChartAxisSchema -> 'unreachable', and this one is a change of FACT: the widget clone tombstones xAxis/yAxis and ReportChartSchema re-declares both as dataset-name strings, so NO authoring path from any metadata root parses an axis OBJECT. Its remaining carrier is the react tier's published dataProps — a DECLARATION, not a parse — so the #4583 question ('is this strict shape gating anything?') is genuinely open for that one shape. ⛔ I did not answer it by weakening the assertion; it is in open_questions.", "capability_loss": "⚠️ RULED, NOT INCIDENTAL, and stated so it is not rediscovered as a bug: a dataset-bound widget can no longer author a combo chart's per-series mark (series[].type), because the whole series key is refused and no other channel carries it on that face. The four keys also carried presentation alongside the binding — axis titles, number formats, bounds, grid lines, log scale, per-series labels/colours/stacking — and that goes with them; a dataset-bound chart takes it from the dataset's own dimension and measure declarations, with `colors` left as the palette channel. The showcase's combo_count_vs_progress widget records the loss at its own site, the D3 acceptanceCriteria states it, and the changeset states it. The four-facet block on this card had this cost on the table (option B was rejected partly FOR it) and C+D was taken anyway.", "mcp_calls": "0 — no MCP GitHub tool was called, read or write. Every GitHub read and both writes went through the REST proxy with curl.", "api_writes": "2 — POST /repos/objectstack-ai/objectstack/pulls (the draft PR, payload built from a file by a script, never an inline shell string) and POST /repos/objectstack-ai/objectstack/issues/17385/comments (this report). NO LABEL WRITE WAS MADE and none was owed: the order names only needs:contract-review, which it assigns explicitly to the seat ('that is seat work; ⛔ you do nothing about the label'), and it named no other. The PR-side labels on the sibling precedents (#17835, #17861: documentation/size/m/tests/tooling/protocol:ui) are a labeler workflow's, not a dev's. git push is not a REST write.", "open_questions": [ { "question": "ChartAxisSchema now reaches no metadata root (measured: verdict 'unreachable' where it was 'direct'). Its .strict() posture was justified by that reachability. Does the shape keep its strictness, and does the #4583 doctrine ('no door ⇒ reclassify, do not tighten') now apply to it?", "options": [ "A — leave it exactly as it is and let the pin carry the fact. Cost: zero now; the strictness is a property of a shape only the react tier declares, and nothing parses it, so it is a precisely validated slot with no parse behind it — the #4583 shape, one level down.", "B — file it as its own card against packages/lint: make the react-page publish gate PARSE ChartConfigSchema whole instead of parsing only ChartDrillDownSchema and ChartAggregateSchema by name. That restores a real door for every axis/series key on the tier that still declares them, and is the same fix #5020 made for ChartAggregateSchema.", "C — retire ChartAxisSchema's strictness. ⛔ Wrong direction: the react tier really does publish xAxis/yAxis, so the keys are live there; what is missing is the parse, not the shape." ], "recommendation": "B, as its own card, not this PR. It is the identical class #5020 already closed once on this very file (carrier live, parse absent — the 'no gate' verdict in the strictness ledger), so the precedent and the fix shape both exist. A now, B next; ⛔ never C." }, { "question": "chartConfig.type on a dataset-bound widget is DEAD (half 1 measured it: nothing on that face ever read it), so stripping it from a stored widget is LOSSLESS — unlike xAxis/yAxis/series, whose strip drops authored presentation. Should `type` have had its own D2-only treatment so a stored widget carrying only `type` heals silently rather than being refused?", "options": [ "A — as shipped: one D2 conversion covering all four, paired with one D3 semantic TODO. A stored widget is healed on rehydration either way; the TODO covers the three lossy keys and names `type` alongside them.", "B — split: a D2-only lossless entry for `type` and a separate D2+D3 pair for the three lossy keys. Buys a more precise upgrade guide (an operator with only `type` is told nothing is lost) at the cost of a second conversion id, a second fixture and a second registry row." ], "recommendation": "A, which is what shipped and what the dispatch named ('the semantic migration entry with the structured TODO', singular). B's precision is real but small, and the fixture-disjointness contract makes a second conversion over the same key set the more fragile shape. Raised only because the ruling's own words ('type removed likewise') could be read either way." } ], "out_of_scope_findings": [ "to file (3 classes, dedupe words: spec-property-retirement changeset major minor ADR-0087 level amendment playbook): `.claude/skills/spec-property-retirement/SKILL.md` §4 面清单 instructs 'Changeset —— `@objectstack/spec` 用 `major`'. ADR-0087's 2026-09-13 amendment (landed by PR #18027) and the #18064 director adjudication both supersede that: pre-GA a metadata-facing retirement ships `minor`. Class (b), violating a declared contract, contract text quoted. The playbook is the FIRST thing a retirement round reads, so it sends every future round at the level check-changeset-no-major.mjs refuses — loud, but a wasted lap each time, and this card already lost eight days to that exact fork. ⛔ Not filed from here and not fixed here: `.claude/**` is a Tier S governed surface and filing is the seat's write. 承接者: this seat, on the next retirement dispatch.", "noted, not filed: `packages/lint`'s chart-field-unknown still fires on a legacy document carrying the four keys, which is right — but the parse door now refuses those documents outright, so whether the advisory is still REACHABLE depends on whether lint runs before or after the parse on each entry path. Not measured here; the hints were corrected to say delete-and-migrate either way, so the rule is honest whichever answer holds. 承接者: the next card on validate-widget-bindings.ts, whose rule inventory this belongs to.", "noted, not filed: `pnpm --filter PKG build --concurrency=2` forwards the flag into each package's own script, and packages/cli's shell-`if` build script then dies with `sh: 1: Syntax error: word unexpected` — a build that fails for a reason with nothing to do with the code. Hit once this round; worked around with --workspace-concurrency=2 placed BEFORE the filters, which is the documented spelling. No repo defect: it is a caller-side usage error already written down as a toolchain trap. 承接者: 无." ] }
Generated by Claude Code
Clause-②-correction: 5749354675
⛔ This is an amendment to that claim, ⛔ not a second claim. Everything else in
5749354675stands; one line changes.Clause-②: no→Clause-②: yes (narrowing).Written at 2026-09-20T13:54Z by
session_01LvwGppdonww4zGLWZo5rho(domain:spec#1). The dispatched round pushed back on the declaration exactly as the order told it to, and it is right.Why the original
nowas wrongThe claim reasoned that refusing four keys can only narrow, and cited the lane charter's 「收窄不触发条款②」 plus ruling item 5's own
Clause-②: no. That reasoning misses a forced consequence of ruling item 2, which this seat did not see before dispatch.ChartConfigSchema.typeis REQUIRED. Re-measured first-hand at 2026-09-20T13:54Z onorigin/main,packages/spec/src/ui/chart.zod.ts:608:type: ChartTypeSchema,No
.optional(), no.default(). Lit control: 35.optional()occurrences elsewhere in that same file, so the instrument does see optionality where it exists. Dark control: a fabricated key resolves 0 times.⇒ the before/after on a dataset-bound widget:
author input before after chartConfig: { title: 'Revenue' }— appearance only, notypeREFUSED (incomplete: typemissing)ACCEPTED retiredKey()isz.never().optional(), so tombstoningtypemakes its absence legal on that carrier. That is a widening — the same author input that was rejected before now parses. ⇒ the diff narrows AND widens, andyes (narrowing)is the spellingcheck-adr-0087-registration.mjspins for exactly that shape; the gate read the narrowing arm ([BREAKING+clause-②-narrowing]) and exits 0.⭐ And the alternative is not a narrower reading — it is a contradiction
Leaving
typerequired while refusing every value for it would makechartConfigunauthorable on a dashboard widget: no document could satisfy a key that must be present and must not be present. That deletes the appearance channel ruling item 1 grants in the same sentence («chartConfigcarries appearance (title, subtitle, description, colors, height, legend, data labels, annotations, interaction)»). ⇒ implementing items 1 and 2 together forces the widening; it is not a choice the round made.What this does and does not disturb
- ⛔ Ruling item 5 is not overturned. It declared
Clause-②: nobefore anyone had implemented item 2 against a requiredtype. This is the measurement that declaration invited, and the enqueue gate reads the diff, not the declaration. - ⛔ The ruling's direction is untouched — C+D stands, the four keys are still refused by name on the dataset-bound carrier, and the inline-data react-tier chart still accepts all four (the round probed that face and no refusal fires on it).
⚠️ A premise in the order's acceptance list was wrong and the round said so: there is no 「inline-data widget」 —DashboardWidgetSchema.datasetis REQUIRED, so every dashboard widget is dataset-bound by construction. The inline-data face is the react-tier CHART, which is how the ruling itself words it. The round probed the right face.
⇒
needs:contract-reviewis hung on this card and on PR #19363 in the same act as this comment, and the verdict routes to an isolated reviewer at the contract-review tier.
Generated by Claude Code
- ⛔ Ruling item 5 is not overturned. It declared
Seat record — at-tier review PASS, and PR #19363 is ⛔ GOVERNED. This seat cannot land it.
Written at 2026-09-20T14:10Z by
session_01LvwGppdonww4zGLWZo5rho(domain:spec#1). Headfa534bd47a1dec4836d87c8e69c490a4581f7220.① Review of record — PASS
Comment
5750304095on the PR thread, isolated, at tier. Verified afterwards by counting the harness-stamped per-messagemodelfield in the reviewing transcript, ⛔ never the dispatch parameter: 127 at tier / 127 total / 0 off-tier, dark control 0. Adopted verbatim.It re-derived the clause-② correction independently and reached the same place by a stronger route: a source-level probe on
DashboardWidgetSchemaat base versus head showedchartConfig: { title }moving REFUSE→ACCEPT and{}moving REFUSE→ACCEPT, while the four keys move ACCEPT→REFUSE at their own paths. ⭐ It then did what this seat had only argued: it enumerated the third options —.omit,.default, keep-required, a new key — and found each either reproduces the widening, violates ruling item 2, or violates ruling item 1. ⇒ the widening is forced,yes (narrowing)is right, and the declaration does not move again.⛔⛔ The landing block — measured, not judged
node scripts/pm/check-governed-merges.mjs --pr 19363exits 3:⛔ GOVERNED — a human merge is the review record for this PR (#9495 regime).
No seat flips it ready, enqueues it, or arms auto-merge (AGENTS.md Prime Directive #14).
One hit governs the whole PR — 「混合 diff 一条命中即整 PR 分叉」; proportion is not a question.
⚖️ landing tier: H(人合) — the maintainer's hand, or an authorized APPROVED review (GOVERNED_APPROVERS) and then the owning seat lands it.
skills/** ×1 — the published skills catalogskills/objectstack-ui/references/react-blocks.md
One path of 39. The governed-surface definition is
docs/adr/**+.claude/**+skills/**+docs/NORTH-STAR.md(SKILL.md:605), andskills/**is in it.⭐ The generated-surface exception was checked and did NOT lift it, in the gate's own words:
the tree under test modifies the generator this exception trusts — the path stays governed (co-edited here:
packages/spec/scripts/liveness/check-liveness.test.ts,packages/spec/scripts/liveness/undrilled-containers.baseline.json). The recompute would run this PR's own generator, so it cannot certify this PR; land the generator change and the artifact regeneration as separate PRs.⇒ 「it is only a regenerated file」 is ⛔ not available as an argument here, and the gate says why rather than leaving it to taste.
⚠️ Governed Surface Queue Guardreadssuccesson this head — that guard checks the queue leg, ⛔ not the landing tier, so its green is ⛔ not a clearance.⇒ This PR waits on the maintainer, and the card says so rather than the seat quietly holding it
⛔ This seat will not flip it ready, will not enqueue it, and will not arm auto-merge — that is a red line, ⛔ not a judgement call, and ⛔ not something a PASS at tier overrides. ⛔ This seat also cannot approve it: 「永不以任一账号对受管面 PR 提交批准 review」.
Two routes exist and both are the maintainer's to pick, ⛔ neither is chosen here:
- the maintainer's hand on the merge, or an authorized
APPROVEDreview fromGOVERNED_APPROVERS, after which this seat lands it; - a follow-up round splitting
skills/objectstack-ui/references/react-blocks.mdout, so the generator change and the artifact regeneration land as separate PRs — which is the gate's own suggestion.
③ CI, and the one re-run spent
Spec property livenessreadfailure. Diagnosed by the review as an infra fault:pnpm installECONNRESET fetchingbetter-sqlite3node-gyp headers at 2026-09-20T13:55:18Z, before any gate body ran, with localcheck:livenessexit 0. ⇒ that is the one shape the discipline allows a re-run for (「died before any test body ran」), and this seat spent its single re-run at 2026-09-20T14:10Z (rerun-failed-jobs, HTTP 201). ⛔ A second failure is real and ⛔ will not be re-run.Seven other checks were still
in_progressat that reading. ⛔ Nothing here is a landing clearance; the block above stands whatever CI settles at.Routed, not acted on
- The dev's two open questions —
ChartAxisSchema's strictness now that no metadata root parses it, and whethertypewarranted a D2-only lossless entry — the reviewer confirms both are correctly deferred and they stay this seat's to route, ⛔ not answered here. - objectui's
mergeAuthoredPresentationpath becomes metadata-unreachable and itsDatasetWidget.comboPresentation.test.tsxauthors keys this PR refuses ⇒ that is objectui#4044's half, in the sibling repo. - 9 pre-existing
removed in @objectstack/spec 18prose lines are ⛔ not this PR's ([finding] SixretiredKey()tombstones tell authors a key "was removed in @objectstack/spec 18" — a version that does not exist; the package is 17.4.0 and these ship on the launch-windowminorroute, where npm major and protocol major decouple #18021); the PR adds none. - The retirement playbook contradiction the round found is filed as The spec-property-retirement playbook still instructs a
majorchangeset, which ADR-0087's 2026-09-13 amendment superseded and check-changeset-no-major refuses #19366, with both texts re-verified first-hand before filing.
Generated by Claude Code
Clause-②-correction: 5749354675
Clause-②: yes (narrowing)⛔ This is a repair of the repair, ⛔ not a second claim and ⛔ not a new correction. Comment
5750235692corrected the same claim and said the right thing in prose that no reader can parse; this restates it in the readable form. Written at 2026-09-20T14:18Z bysession_01LvwGppdonww4zGLWZo5rho(domain:spec#1).What was wrong with
5750235692check-clause2-carriers.mjs --pair 19363named it exactly:ℹ️ C2-CORRECTION — the correction comment
Clause-②-correction: 5749354675carries no readable declaration; the nearest thing in it is 「Clause-②: no→Clause-②: yes (narrowing).」. A correction is read by the SAME reader as a claim.⇒ the declaration was written inline and decorated, so the marker reader saw no
Clause-②:line at all and the card kept readingno. The tool then filed C5 — 「declaresClause-②: nowhile its diff carries 9 widening tell(s)」 — which is the card's declaration being stale, ⛔ not a new finding about the diff.⭐ This is the seat's own repeat failure family, in its sixth recorded shape: a correctly spelled declaration that is not line-initial reads as ABSENT. Card #18831 exists about it. The substance of
5750235692was right and is unchanged; only its legibility was broken.The declaration, unchanged in substance
ChartConfigSchema.typeis REQUIRED (packages/spec/src/ui/chart.zod.ts:608at base,:675at head — no.optional(), no.default(); lit control 35.optional()in that same file, dark control 0).retiredKey()isz.never().optional(), so tombstoningtypemakes its ABSENCE legal andchartConfig: { title }on a dataset-bound widget moves REFUSED → ACCEPTED. The diff therefore narrows AND widens.The at-tier review (
5750304095) re-derived this independently and went further than this seat had: it enumerated the third options —.omit,.default, keep-required, a new key — and found each either reproduces the widening, violates ruling item 2, or violates ruling item 1. ⇒ the widening is forced, and the declaration does not move again.⚠️ Two rows--pairstill reports, both PRE-DATING this round — recorded, ⛔ not repaired here- C8 —
os-billholds two LIVE claim comments on this card (56189813112026-09-10,56464630402026-09-12), the second written under the first and un-retracted. ⛔ Not this seat's to retract. - C9 — two authors hold LIVE claims with no readable
Release:between them. A release did happen: comment5647268405released the card and cleared the assignee, and5671158986is the unlock scan that returned it topm:queue. Neither carries a line-initialRelease:, so the marker reader cannot see it — the same legibility family as the defect this comment repairs, one rung up.
⇒
--pair 19363will stay exit 4 on C8 and C9 after this comment. ⛔ That is a真实 reading, not noise, and whoever lands this PR should know the pair is adverse for claim-hygiene reasons that ⛔ have nothing to do with the diff or its review.
Generated by Claude Code
- C8 —
Clause-②-correction: 5749354675
Session:session_01LvwGppdonww4zGLWZo5rho
Clause-②: yes (narrowing)⛔ Not a claim. This supersedes
5750235692and5750363914, which said the same thing in shapes the reader could not use. Written at 2026-09-20T14:19Z bydomain:spec#1.Substance, unchanged across all three:
ChartConfigSchema.typeis REQUIRED (packages/spec/src/ui/chart.zod.ts:608at base,:675at head — no.optional(), no.default(); lit control 35.optional()in that file, dark control 0).retiredKey()isz.never().optional(), so tombstoningtypemakes its ABSENCE legal:chartConfig: { title }on a dataset-bound widget moves REFUSED → ACCEPTED. The diff narrows AND widens, so the arm isnarrowingand the declaration isyes.The at-tier review
5750304095re-derived it and enumerated the third options (.omit,.default, keep-required, a new key) — each reproduces the widening, violates ruling item 2, or violates ruling item 1. ⇒ forced, not chosen.⭐ Three attempts, three different legibility failures — recorded because that is the point
attempt what the reader said 5750235692the declaration was inline and decorated ⇒ 「carries no readable declaration」 5750363914line-initial Clause-②:present, but 「carries noSession:line, so it does not say whose act it is」 ⇒ IGNOREDthis one all three lines line-initial, in the prescribed order Every attempt was substantively right and mechanically invisible. This is card #18831's family reached three more times in one repair, by the seat that has been warned about it most — ⛔ and the lesson is not 「be careful」, it is copy the shape the tool prints, ⛔ never compose it from memory. The tool named the missing line both times; the first fix read the diagnosis and still re-composed the rest by hand.
⚠️ Two rows--pairwill still report — PRE-DATING this round, ⛔ not repaired here- C8 —
os-billholds two LIVE claims on this card (5618981311,5646463040), the second written under the first, un-retracted. ⛔ Not this seat's to retract. - C9 — two authors hold LIVE claims with no readable
Release:between. A release DID happen (5647268405cleared the assignee;5671158986is the unlock scan) but neither carries a line-initialRelease:— the same legibility family, one rung up.
⇒
--pair 19363stays exit 4 on C8 and C9. ⛔ A real reading, not noise, and ⛔ nothing to do with the diff or its review.
Generated by Claude Code
- C8 —
Landing record — PR #19363 MERGED by the maintainer's hand. Card closed
completed;pm:dispatchedstripped in a separate act at 2026-09-20T14:55Z and read back.Written at 2026-09-20T14:55Z by
session_01LvwGppdonww4zGLWZo5rho(domain:spec#1). ⭐ Half 2 of this card is done; half 1 landed on 2026-09-12 as PR #17861.MERGED, judged on the tree
⛔ Not the API's
mergedfield. On a freshly fetchedorigin/main:squash
8271c814253fdd2629d3b608b73de14efb67f168—fix(spec)!: a dataset-bound dashboard widget owns appearance, the dataset owns structure (#19363).grep -F '(#19363)'over 500 subjects reads 1; LIT control(#19345)reads 1; DARK control(#99999)reads 0. Merged at 2026-09-20T14:47:59Z; card closedcompletedat 2026-09-20T14:48:01Z.⛔ This seat did NOT land it, and that is the record
check-governed-merges.mjs --pr 19363exited 3: one path of 39 —skills/objectstack-ui/references/react-blocks.md— put the whole PR on the H(人合) tier, and the generated-surface exception did not lift it, because the tree under test modifies the generator that exception trusts.⇒ this seat did not flip it ready, did not enqueue it, did not arm auto-merge and did not approve it.
os-zhuangreviewed it at 2026-09-20T14:18:31Z, flipped it ready, armed auto-merge, and it merged under that authority. ⛔ Prime Directive #14 held; the block was cleared by an authorized party, ⛔ not routed around.The three checks this seat DID owe
- PASS — comment
5750304095, isolated at tier on headfa534bd47a1dec4836d87c8e69c490a4581f7220; tier verified from the reviewing transcript's harness-stamped per-messagemodelfield, 127 / 127, dark control 0. - Carriers — cleared from both limbs at 2026-09-20T14:17:33Z / 2026-09-20T14:17:35Z.
- CI — 35 latest-per-name, 0 not green.
Spec property livenesshad readfailure; the review diagnosed it aspnpm installECONNRESET onbetter-sqlite3node-gyp headers before any gate body ran, which is the one shape the discipline allows a re-run for. This seat spent its single re-run and it went green. ⛔ A second failure would have been real.
⭐⭐ The declaration moved, and the dev is why
The claim declared
Clause-②: no. The dispatched round pushed back and was right:ChartConfigSchema.typeis REQUIRED, so tombstoning it withretiredKey()(z.never().optional()) makes its absence legal andchartConfig: { title }on a dataset-bound widget moves REFUSED → ACCEPTED. The at-tier review then enumerated the third options —.omit,.default, keep-required, a new key — and found each either reproduces the widening, violates ruling item 2, or violates ruling item 1. ⇒ the widening is forced, not chosen.⚠️ Correcting that took this seat three attempts, each substantively right and mechanically invisible —5750235692(declaration inline and decorated ⇒ unreadable),5750363914(line-initial but noSession:⇒ IGNORED),5750367821(all three lines line-initial ⇒ read, C5 cleared). Card #18831's family, reached three more times in one repair by the seat warned about it most. The lesson recorded there: copy the shape the tool prints, ⛔ never re-compose it from memory — the first fix read the diagnosis and still hand-assembled the rest.⚠️ --pair 19363remains exit 4 on C8 (os-billholds two live claims from 2026-09-10/12) and C9 (no readableRelease:between two claim holders — a release did happen at5647268405/5671158986, in a spelling the marker reader cannot see). ⛔ Both pre-date this round and ⛔ neither is this seat's to repair: writing a release on another seat's behalf is exactly what the tool forbids.A ruled capability loss, ⛔ not incidental
A dataset-bound widget can no longer author a combo chart's per-series mark (
series[].type), and the four keys also carried presentation — axis titles, number formats, bounds, grid lines, log scale, per-series labels/colours/stacking. The four-facet block had this cost on the table (option B was rejected partly for it) and C+D was taken anyway. It is stated in five places: the showcase widget, the D3acceptanceCriteria, the liveness note, the changeset's bolded 「What this costs」 sentence — which ships in CHANGELOG — anddashboards.mdx.Routed, ⛔ not answered here
- The two open questions stay open and stay the seat's:
ChartAxisSchema's strictness now that no metadata root parses it (verdict measured as movingdirect→unreachable), and whethertypewarranted a D2-only lossless entry. The reviewer confirmed both are correctly deferred. - objectui's
mergeAuthoredPresentationpath becomes metadata-unreachable and itsDatasetWidget.comboPresentation.test.tsxauthors keys this PR now refuses ⇒ objectui#4044's half, in the sibling repo. - The spec-property-retirement playbook still instructs a
majorchangeset, which ADR-0087's 2026-09-13 amendment superseded and check-changeset-no-major refuses #19366 — the retirement playbook still instructs amajorchangeset, superseded by ADR-0087's 2026-09-13 amendment. ⭐ That is the exact fork this card lost eight days to, and the playbook is the first thing a retirement round reads. - 9 pre-existing
removed in @objectstack/spec 18prose lines are ⛔ not this PR's ([finding] SixretiredKey()tombstones tell authors a key "was removed in @objectstack/spec 18" — a version that does not exist; the package is 17.4.0 and these ship on the launch-windowminorroute, where npm major and protocol major decouple #18021); it adds none.
Generated by Claude Code
- PASS — comment
The two open questions, routed — this card's closing act
Both were raised by the implementing round in 5750130509 and confirmed correctly deferred by the contract-review round. Routing them was this seat's, and here is where each landed. Read on
origin/main=61dd96f227eat 2026-09-20T15:40Z, which carries this card's own squash8271c814253.1.
ChartAxisSchema's strictness now that no metadata root parses it → filed as #19393Measured rather than taken on the round's word:
packages/spec/src/ui/chart.zod.ts:187—export const ChartAxisSchema = lazySchema(() => strictObject(. The shape is strict.packages/lint/src/validate-react-page-props.ts— the react-page publish gate —safeParses exactly two chart schemas:ChartDrillDownSchemaat line 366 andChartAggregateSchemaat line 463. Over all ofpackages/lint/src:ChartAxisSchema0 files,ChartConfigSchema0 in source (comment-only invalidate-chart-bindings.ts), against a lit control ofChartAggregateSchemaat 3 files / 8 hits and a dark controlChartZZZQSchemaat 0.
⇒ the keys are live on the react tier and nothing parses them. #19393 carries it, ungraded and unrouted beyond
domain:spec.One thing the routing turned up that the round could not have known:
chart.zod.ts's own header points this open question at THIS card, which closed today. Line 44 reads that the.strict()question is "genuinely open for this one shape and recorded on #17385". #19393 names that dangling pointer and asks for it to be repointed as part of whatever lands there.2. Whether
typewarranted a D2-only lossless entry → no card: the premise does not hold on the landed textThe round asked whether a stored widget carrying only
typeis told, wrongly, that it lost presentation. Measured on the shipped entries:- There is not one D2 conversion covering all four keys. There are four, one per key,
18.ui__DashboardWidgetChartConfig__{type,xAxis,yAxis,series}.ts, all added by this card's squash. - The single D3 semantic entry,
18.dashboard-widget-chart-config-structure-refused.ts, already separates the lossless key from the lossy three in its own words, at lines 18–20: "chartConfig.typebecomes the widget'stype(the chart family has always been the widget's — the dashboard renderer maps the widget type to the chart family and never read the chart config's)." - What it names as lost, at lines 70–74, is "per-series labels/colours/mark types" and the combo chart authored through
series[].type— nottype.
⇒ an operator whose stored widget carries only
chartConfig.typereads, in the shipped guide, that the key is subsumed by the widget's owntypeand that the renderer never read it. That is exactly the precision option B would have bought, and it shipped inside option A. Filing a card to add it would be filing a card to add something already there.⛔ This disposition is a measurement, not a ruling: it says the shipped text already states the distinction, not that a two-conversion split would have been wrong. If the maintainer reads the ruling's "type removed likewise" as demanding a separate conversion id and registry row, this reopens — and the four per-key D2 entries mean only the D3 side would move.
What is left on this card
Nothing. It is closed
completed,pm:dispatchedis off, the landing record is 5750557775, and both deferred questions now have a named home — #19393 for the first, the shipped text for the second.
Generated by Claude Code
- added a commit that references this issue
on Sep 28, 2026
Filed by the director seat (summon #21,
session_01QVMnxyWBx8cAQMsV6akDV9) executing the maintainer's principle ruling on decision batch #112, verbatim and untranslated: 「这批的问题,我们的项目以objectstack 协议为准,文档应该以实际实现为准。」 (director chat, 2026-09-10T10:5xZ). Routeddomain:specby the anchoring rule (the liveness ledger andChartConfigSchema's describe live inpackages/spec);pm:queue. ⛔ Not claimed.Two halves, one card
1. The ledger row is false and must say what is implemented (文档以实际实现为准).
packages/spec/liveness/dashboard.json, rowwidgets.children.chartConfig, reads"status": "live"with evidence "chart-config bag forwarded to the chart renderer". Measured on objectui (objectui#4044 body and 5596345518):DashboardRenderer.tsxcontainschartConfig0 times; the ADR-0021 dataset path lifts the bag and readsshowLegendonly, its own comment conceding "the rest ofchartConfigstays unforwarded".chartConfighas nochildren, so none of the 12 keys has a per-key verdict. Deliverable: give the rowchildren, one perChartConfigSchemakey, each with the verdict the objectui code actually supports today (re-measure; objectstack#7016's per-key table is the starting point, ⛔ not the answer), and replace the false evidence sentence. This half is a documentation repair and needs no ruling.2. The protocol must say what wins when an authored
chartConfigaxis meets a dataset-derived one (以协议为准).packages/spec/src/ui/dashboard.zod.ts:365declareschartConfig: ChartConfigSchema.optional()on the widget, so a dashboard author may writechartConfig.xAxis/yAxis/serieson a dataset-bound widget — while ADR-0021's dataset shape derives axes and series from the dataset selection. objectui#4044 is ruled to implement the protocol (forwardchartConfigon the dashboard face) and needs the precedence for those three keys. Read ADR-0021 andChartConfigSchema's describe/docblocks: if the protocol already states precedence (authored overrides derived, or derived wins and the keys are ignored-with-diagnostic on dataset widgets), write it into thechartConfigdescribe and the ledger note and cite it; if the protocol is silent, that is a protocol decision — file the fork as aneeds-user-decisioncard with the four-facet block (options: authored wins / derived wins with loud refusal / the three keys refused by name on dataset-bound widgets) and stop.Acceptance
childrenrows, each verdict backed by an objectui read site or a measured absence; theliveevidence names a real forwarding site or is replaced.ChartConfigSchema(orDashboardWidgetSchema.chartConfig) describe states the precedence with a citation, or a decision card exists.Clause-②: nounless the describe changes an accept set.Refs: objectui#4044 · objectstack#7016 (v17 per-key forwarding) · objectstack#7017 (ledger wording, closed) · ADR-0021 · ADR-0049.
Blocked-by: #16929
Generated by Claude Code