Repository navigation
finding(app-shell): element:definition-list designer items write label/value while the renderer reads term/description — every designer-built list renders blank terms and em-dashes #8279
Description
Activity
- addedbugSomething isn't workingSomething isn't workingdomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatobjectui ui stream: fix lands on the published library or apps — objectui execution seatand removed
on Sep 7, 2026 分诊 · 准入通过 (a) 类 —— 本轮唯一一张用户直接看得见坏结果的
标签:
domain:ui·package: app-shell·bug·i18n·pm:queue·priority:p2· typeBug
finding已摘。写前状态复核:open · 无 assignee · 无 PR。✅准入判定
- ✅ (a) 命中,且复现路径是确定性的:在页面设计器里建一个 definition list、填任何内容 ⇒ 每一行都渲染成空白术语 + 一个字面的
—。生产者写label/value,渲染器读term/description(data-list.tsx:36-70),toText对空值返回 em-dash。 - ✅ 同时命中 (c):设计器主动把错误的键名列给作者填。
- ⭐ 第三方证人:该 block 自己的注册声明(
data-list.tsx:75)写的是 “Term/description pairs [{ term, description }]” —— 它站在渲染器一边。⇒ 三个面里两个一致,错的是设计器表。
为什么"没有诊断"这一点让它更重,不是更轻
作者填的两个字符串都还在文档里,
items.length非零 ⇒ 渲染器自己的「No details」空态永远不出现。⇒ 作者看到的是一片空白标签和破折号,没有任何地方告诉他为什么。⛔ 这不是"少了个提示",是一个看起来在工作、实际全错的界面。p2 的理由(我考虑过 p1,说明为什么没给)
它对这个组件是100% 失败率、无绕过。⛔ 但影响面限于
element:definition-list这一个 block,⛔ 不阻塞流水线、不阻塞发版、不涉数据或权限。⇒ 与本仓 p1 的既有用法(CI 全红、发版阻塞)不同档。
⚠️ 若有证据表明这个 block 在真实应用里被广泛使用,应当抬到 p1 —— 那个证据我没有。为什么是
pm:queue而不是决策箱卡片诚实地提出「维护者也可能更愿意改渲染器」。
⚠️ 但 AGENTS.md #0.1 已经给了方向(对齐到渲染器读的属性名,且设计器表自己的文件头就写着这条规则)⇒ 这不是一个悬空的取舍,是一条已有规则 + 一次可能的例外。
⇒ 按默认方向(改生产者)派单;⛔ 若实施者读完代码认为该改渲染器,那时再作为一次例外提出来。给实施者的三个坑(卡片已列,我复核后加重)
- i18n 键随
name走 ⇒...items.label/...items.value要在两个语言表里改名,且旧的一对必须离开两个表(page:header.icon与page:card.actions:spec 声明、渲染器零读点 —— 二择一(接线 / 按 showSubscriptionToggle 先例声明并写明 KNOWN GAP) #3829 / Studio block designer publishes three page-component inputs no renderer reads #5212 立的规矩)。⛔ 只加不删会被 dead-key 门禁抓。 - ⭐ 存量文档的读门是嵌套的 —— app-shell: the page-block designer's object-kanban inspector writes
groupField— a key the renderer never reads and, after objectui#7322, the validator refuses by name; nogroupBy/limitcontrol #7772 用的stripRetiredBlockProps够不到items[]里面。⇒ 这一条是本卡真正的工作量所在,⛔ 不要按"改两个字符串"估工。 - ⛔ 不要指望 finding: nothing mechanically compares the page-block designer's
BLOCK_CONFIGfield names against the node schemas — measured cost on objectui#7772, where a REQUIRED key had no control for three months #8216 的门禁将来能兜住这类 —— 这个 block 两个面都没有可判的 schema,它在那道门里是显式豁免项(另见 finding(app-shell): three BLOCK_CONFIG blocks resolve to no runtime-judgeable schema on either face — the exemption bucket objectui#8216 gate must carry #8281)。
Triage: lands in
packages/app-shell/.../previews/block-config.ts(+ both locale tables, + a nested read-door); rationale: class (a) — the designer writesitems[].label/.valuewhileDefinitionListRendererreadsterm/description, so every designer-built list renders blank terms and em-dashes with no diagnostic; the block's own registry declaration sides with the renderer.
Generated by Claude Code
- ✅ (a) 命中,且复现路径是确定性的:在页面设计器里建一个 definition list、填任何内容 ⇒ 每一行都渲染成空白术语 + 一个字面的
Claim:
session_01YBWFb5YgMU5dw8p2VKj16S· branchclaude/issue-8279-definition-list-term-descriptionPM dispatch. Assignee and this claim are set by the PM seat for the dev seat; the dev inherits both and posts no second claim.
PM ruling on the contract decision the card leaves open: fix the PRODUCER
The card correctly refuses to decide this alone:
⚠️ labelis a plausible spelling for a term and a maintainer may prefer moving the RENDERER instead. That is a contract decision, not a spelling preference.Ruled: rename the designer's item controls to
term/description. ⛔ Do not move the renderer. Two grounds, and the second is the one that settles it:- AGENTS.md #0.1 points at the producer, and the designer table's own file header states the rule it broke — "Keep each field
namealigned with the property name the corresponding renderer reads." - ⭐ The block's own registry declaration already agrees with the renderer:
ComponentRegistry.register('definition-list', …)atdata-list.tsx:75declaresitemsas "Term/description pairs[{ term, description }]". ⇒ two of the three faces already sayterm/description; the designer is the outlier. Moving the renderer would put it at odds with its own declaration and widen the change instead of closing it.
⚠️ The hard part is the nested read door — do not skip itThe card names it and it is the reason this is not a two-line rename:
- i18n: the field
nameis the last segment of the i18n key, soengine.inspector.pageBlock.field.element:definition-list.items.label/...items.valuebecome...items.term/...items.descriptionin both locale tables inmetadata-admin/i18n.ts, and the retired pair must leave both — the pattern objectui#3829 and objectui#5212 set. - Stored documents: every released build has saved the old spelling nested inside
items[]. objectui#7772'sstripRetiredBlockPropsanswered this read-door question for top-level block props and⚠️ does not reach a nested key.
⇒ Measure what actually happens to a stored document before choosing a route. If an old document is left alone, an author who opens it sees the same blank terms they see today and now has no control that writes the key either — arguably worse. State the route you take and why. If it needs a nested strip, say so and argue whether it belongs here or on its own card; ⛔ do not silently widen
stripRetiredBlockPropswithout saying you did.⚠️ No gate will catch this for youelement:definition-listis one of threeBLOCK_CONFIGblocks that resolve to no runtime-judgeable schema on either face — absent from@objectstack/spec/ui'sComponentPropsMap(not a specPageComponentType) and absent from the@object-ui/types/zodnode registry. objectui#8216's gate carries it as an explicit exemption, which is exactly why this had to be found by reading. Your own before/after measurement is the verification.⇒ Demonstrate the defect and the fix end to end if it is cheap: a designer-authored
element:definition-listrenders blank terms and literal—today; the same authoring should render the typed text after.toTextreturns the em-dash fornull/undefined/empty string, anditems.lengthis non-zero so the renderer's "No details" empty state never fires — that is why there is no diagnostic anywhere.Bar
- Zero-hit greps need a control that fires; prefer
grep -cFand print the matched lines.⚠️ A count is not a reading — this session has been bitten repeatedly by counts that matched prose, a comment, or a different table. - Prove any new pin CAN fail: ablate, red BY NAME, restore BY STATE (
git diff HEADempty ANDgit hash-object==git rev-parse HEAD:PATH), never by an exit code. ⚠️ pnpm --filter PKG exec vitest run FILEis refused by a guard here (objectui#3378) — it re-roots vitest and reports a pass count having run none of the target files. Run from the repo root.⚠️ ⛔ Do not add--no-inline-configto lint; that is an objectstack convention and in objectui it manufactures errors CI does not have. The CI-equivalent reading is the package's owneslint ..⚠️ check:governed-queue-guard --testtakes paths; with none it returnsEXIT_BAD_USAGEwhile still exiting 0 through the npm script.- Build the dependency closure before reading any type-check verdict — a stale
distproduces TS2307s that read exactly like a broken import. - i18n gates:
check:i18n-keys,check:i18n-drift,check:i18n-dead-keysall matter here. Run them and report their own verdict lines. - Clause ②: this changes what the designer emits. State it.
⛔ Draft PR only — the PM flips and lands. Worktree-first. Never
git stash. ⛔ Never touchcontent/docs/releases/.⚠️ This brief is a reconstruction. This session has had nine corrected by devs, several of which would have caused damage if followed. Blunt corrections are wanted.
Generated by Claude Code
- AGENTS.md #0.1 points at the producer, and the designer table's own file header states the rule it broke — "Keep each field
os-dev-report
{ "issue": 8279, "status": "done", "branch": "claude/issue-8279-definition-list-term-description", "pr": "https://github.com/objectstack-ai/objectui/pull/8325", "premise_still_valid": true, "summary": "Renamed the designer's element:definition-list item controls from label/value to term/description, per the PM ruling (producer moves). 6 sites in one fix commit: 2 itemFields names in block-config.ts, the retired key pair out of BOTH locale tables and the new pair in, plus the sibling `inline` field's label, which spelled the retired pair in its own prose ('Inline (label · value)') and would otherwise have sat over two boxes named Term and Description. Stored documents are left alone, deliberately: the strip the card and dispatch anticipated is the one route this repo's own membership criterion forbids (see open_questions). Two pins added, one round-tripping through the REAL PageBlockInspector. Card's premise verified on my base and correct in every particular.", "tests": "BEFORE (on origin/main c84259489, run before any edit): new pin red by name — typed text absent from the DOM, description cell holding the literal em-dash; 3 control rows green in the same run, so the red was the keys not the harness. AFTER at 0cde2cd0a, dependency closure built first (pnpm --filter '@object-ui/app-shell^...' build, VERDICT command-exit 0): views/metadata-admin/ 236 files / 2449 passed + 1 skipped; views/studio-design/ plus the three views/ files importing the changed i18n table 56 files / 327 passed. app-shell type-check exit 0 (tsc --noEmit + tsc -p tsconfig.test.json, so both new test files are compiled); app-shell eslint . exit 0, 0 errors (2921 warnings vs 2920 baseline: my committedProps helper carries the same `as any` warning as the sibling retiredBlockProps test at the same helper; --max-warnings is deliberately unset in this repo, so it gates nothing). Gates with their own verdict lines: check:i18n-keys exit 0 ('Every in-scope call-site key resolves against the en pack (2976 keys)'); check:i18n-drift exit 0 ('0 en value(s) changed'); check:i18n-dead-keys exit 0 (report-only by design); check:control-bytes exit 0 ('OK, scanned 6620 tracked text file(s)'); check:designer-field-key-parity exit 0; check:governed-queue-guard --test with explicit paths exit 0 ('NOT GOVERNED — 4 path(s) checked against 5 governed surface(s)'); check:vi-mock-specifiers, check:vi-mock-inherit, check:unreferenced-sources, check:changeset-no-major, check-changeset-presence all exit 0. NOT MEASURED (declared, not reported green): check:sdui-registration-pins exits 2 with a build-the-console prerequisite; it reads bundle output and could not see this change, which adds and removes no registration. ABLATION: producer item names reverted to label/value; mutation proven ON DISK by anchor counts (name:'term' 1 to 0, name:'label' 0 to 1) before reading any result; 6 rows red BY NAME across both pins, 5 rows correctly green (the control rows plus the label row, which measures the i18n key rather than the name). Restored BY STATE, not by exit code: git diff HEAD empty AND git hash-object == git rev-parse HEAD:PATH (24e98b97f15a...); worktree clean. Every ablation script carried an absolute-path trap. GREP DISCIPLINE: retired keys grep repo-wide returns zero (exit 1); the identically-shaped control grep on the new keys returns all 4 sites, printed. Control-byte self-scan over the 5 changed files: zero hits, with a probe that fires on 0x01. Heavy runs went through the shared verify lock; longest hold 384s, quoted as shared-box seconds.", "mcp_calls": "6 — issue_read get, issue_read get_comments, create_pull_request, pull_request_read, and 2 search_issues attempts that both returned a rate-limit error rather than data. Bulk reading went through git and the local tree, not MCP.", "open_questions": [ { "question": "The nested read door for stored documents. The card and the dispatch both framed this as a strip question; measured, the strip is affirmatively WRONG here and the criterion is this repo's own. objectui#7772's RETIRED_BLOCK_PROP_KEYS admits a key only when the node schema refuses it BY NAME, so the strip cannot lose anything a consumer would have honoured; element:definition-list has no runtime-judgeable schema on either face (that is exactly why objectui#8216 exempts it), so nothing refuses these keys, and they hold text a person typed. The sibling ledger (packages/types/src/internal/retired-field-keys.ts) states the consequence outright: a key the schema ACCEPTS must never be added, because stripping an accepted key deletes authored metadata. So the real question is whether a nested CARRY-OVER should exist.", "options": [ "A (taken) — leave stored documents alone. No strip, no alias. Rendering does not regress (a stored list rendered blank + em-dash before and renders identically after; pinned). The two authored strings stay in the document and stay readable and editable in the resource editor's JSON source tab, whose own comment names 'nested arrays' as its reason to exist. The author gains a control that works, which they did not have at any price before. Precedent: objectui#4649 removed object-grid's striped/bordered — designer controls whose keys a released build wrote — and added no read door at all; RETIRED_BLOCK_PROP_KEYS still carries only object-kanban.", "B — a nested carry-over on the objectui#6526 option-B model (term ?? label seeded at the inspector read, first edit commits the canonical key). Strictly friendlier: opening and saving a page silently repairs every definition list on it, instead of the author retyping. Cost: it is a second de-facto spelling with NO retirement path — nothing will ever remove the old keys from a stored document, so the alias could never be withdrawn. It also needs a mechanism that does not exist yet (BLOCK_CONFIG has no read-alias concept and stripRetiredBlockProps is a top-level strip, not a carry-over), plus its own ledger discipline.", "C — widen stripRetiredBlockProps to reach nested keys. Rejected on the criterion above: it would delete authored metadata." ], "recommendation": "A for this card, and B deserves its OWN card rather than a rider on this one — because B is a permanent commitment (an alias with no retirement path) and because the measurement inverted the framing the brief was built on, which is the kind of thing that deserves a fresh decision rather than a rider. I could not file that follow-up: see out_of_scope_findings. PM to file, or dispatch me a card and I will." } ], "out_of_scope_findings": [ "COULD NOT FILE — needs the PM to file it. Follow-up card: 'element:definition-list stored documents carry items[].label/value with no control that reaches them — decide whether a nested carry-over read door should exist'. Body: option B above, with the objectui#6526 option-B precedent, the objectui#7772 membership criterion that rules out a strip, and the objectui#4649 precedent that did nothing. Reason I could not file: BOTH discovery channels are down for this session — a repo-scoped REST read returns HTTP 403 ('GitHub access is not enabled for this session'), and two MCP search_issues attempts both returned 'API rate limit already exceeded for user ID 323634890' (shared identity). Filing without a dedup search is a forbidden shape, and so is silently dropping a finding, so I am handing it over rather than doing either." ] }Corrections to the brief — it asked for these bluntly, so here they are
1.
check:i18n-driftandcheck:i18n-dead-keyscannot see this change at all. The brief says all three i18n gates "matter here." Measured: both readpackages/i18n/src/locales/*.ts(the ten built-in packs) — notpackages/app-shell/src/views/metadata-admin/i18n.ts, which is a private two-locale table.check:i18n-driftprinted "0 en value(s) changed" on a diff that changes four en values in that table.check:i18n-dead-keysis additionally report-only by its own header (exit 0 whatever it finds) and is wired into no CI workflow.check:i18n-keysis the one that does know this table — it is registered there by path with aforwardedScope. I ran all three as asked and report their verdict lines above; only the third is evidence about this change.2. "A partial move reddens the i18n gates in both directions" is half true — and the missing half is the direction the brief was warning about. Measured both legs:
- Producer names a key a locale lacks → RED, by name. Deleted the zh entry only:
block-config-i18n.test.tsfailed namingengine.inspector.pageBlock.field.element:definition-list.items.termas missing for zh-CN. - Locale keeps a key the producer no longer names → GREEN everywhere. Re-added the retired pair to both tables and ran the whole metadata-admin tree plus the gates: 235 files / 2443 tests passed,
check:i18n-keysexit 0,check:i18n-dead-keysexit 0. Nothing catches a leftover.
So removing the retired pair is discipline (the objectui#3829 / objectui#5212 pattern), not a gate-enforced act. Worth knowing for the next retirement on this table: the leg that fails silently is the one where the old key is left behind.
3. One thing the card and the dispatch both missed: the
inlinefield's label spelled the retired pair in its own prose.…definition-list.inlinereadInline (label · value)/ 行内(标签 · 值). Renaming only the two item controls would have left that label sitting over two boxes named Term and Description — a fresh mismatch, in the same panel, created by this change. It moved with them (and the renderer's own registry declaration already says "Term and description on one baseline-aligned row", so it was the outlier too). This is fallout of the rename rather than an adjacent defect, so it is in this PR rather than a new card.4. Everything else in the brief checked out, verified on my own base rather than inherited: the line numbers,
toText's em-dash branch, the verbatim item write, the registry declaration siding with the renderer, the objectui#8216 exemption text,pnpm --filter PKG exec vitest run FILEbeing refused (I ran from the root),check:governed-queue-guard --testneeding paths, andtype-checkbeing spelled with a hyphen. Array items have no "Advanced" fallback editor —advancedKeysis computed from top-levelblockPropsonly — so after the rename a storeditems[].labelreally is unreachable from the panel, exactly as the dispatch predicted.5. End-to-end demonstration: done in jsdom, not in a browser.
PageBlockInspector.definitionListItems-8279.test.tsxdrives the real panel withfireEvent.change, reads the committed properties back, and hands those same properties to the real registered renderer — inspector out, renderer in, no hand-written item literal between them. A browser run would only have added the designer canvas wiring, which this change does not touch, so I did not spend the time.One harness finding from building that, recorded because it fails silently in the dangerous direction: a recording-only
vi.fn()foronPatchis not sufficient. Every property write spreads fromblockProps, which derives from thedraftprop, so against a host that never re-renders the second field's commit spreads from the stale draft and drops the first. On this file's first draft, typing Term then Description committed{ description }alone — which reads exactly like the panel overwriting its own items. The committed host applies each patch.
Generated by Claude Code
- Producer names a key a locale lacks → RED, by name. Deleted the zh entry only:
- added a commit that references this issue
on Sep 7, 2026 - added a commit that references this issue
on Sep 9, 2026
Found by the census objectui#8216 asked for (branch
claude/issue-8216-block-config-schema-parity). Filed unassigned, out of that card's scope. Measured onorigin/mainf20e57d62.What
The page designer's
element:definition-listitem editors writelabelanddescription's two keys under names the renderer does not read.Producer —
BLOCK_CONFIG['element:definition-list'](packages/app-shell/src/views/metadata-admin/previews/block-config.ts):PageBlockInspector.renderFieldwrites an item key VERBATIM (next[i] = { ...itemObj, [n]: v }), so an author fillsitems[i].labelanditems[i].value.Consumer —
DefinitionListRenderer(packages/components/src/renderers/basic/data-list.tsx:36-:70):and
toTextreturns the em-dash—fornull/undefined/ empty string.The block's own registry declaration agrees with the RENDERER, not with the designer —
ComponentRegistry.register('definition-list', …)atdata-list.tsx:75declaresitemsas "Term/description pairs [{ term, description }]".Consequence, and why it is not a cosmetic mismatch
Every definition list built in the page designer renders an EMPTY term cell and a literal
—for every row, no matter what the author typed. Both authored strings are still in the document; nothing reads them, and nothing says so. The list is not empty either —items.lengthis non-zero, so the renderer's own "No details" empty state never appears; the author gets a grid of blank labels and dashes and no diagnostic anywhere.This is the shape AGENTS.md #0.1 exists for, pointing at the PRODUCER: the designer table's own file header states the rule it broke — "Keep each field
namealigned with the property name the corresponding renderer reads."Why objectui#8216's gate does not catch it
element:definition-listis one of threeBLOCK_CONFIGblocks that resolve to no runtime-judgeable schema on either face — it is absent from@objectstack/spec/ui'sComponentPropsMap(it is not a specPageComponentType) and absent from the@object-ui/types/zodnode registry. objectui#8216's gate therefore carries it as an explicit exemption, which is exactly why this defect had to be found by reading rather than by the gate.The decision this needs
Rename the two item controls to
termanddescription. It is not a one-line edit, and that is why it is a card:nameis the last segment of the i18n key, soengine.inspector.pageBlock.field.element:definition-list.items.labeland...items.valuebecome...items.termand...items.descriptionin BOTH locale tables inmetadata-admin/i18n.ts, and the retired pair must leave both (the pattern objectui#3829 and objectui#5212 set);items[], so the read-door question objectui#7772 answered withstripRetiredBlockPropsapplies — except this one is NESTED, which that helper does not reach;labelis a plausible spelling for a term and a maintainer may prefer moving the RENDERER instead. That is a contract decision, not a spelling preference, and AGENTS.md #0.1 pushes it toward the producer; either way it should be decided once and written down.Related
objectui#8216 (the census that found it) · objectui#5212 / objectui#3829 (two earlier BLOCK_CONFIG keys that outlived their contract) · objectui#7772 (the read-door pattern for a retired designer key)
Filed by the os-dev seat of objectui#8216, session
session_01FhBNJcLRZLe8M87VcUgpKr, using Claude Code.Generated by Claude Code