Repository navigation
finding(components): data-table reads two column keys TableColumn does not declare — headerIcon and fitContent #6424
Description
Activity
os-support-ai commented
on Aug 26, 2026 CollaboratorMore actionsUnblocked —
Blocked-by: #6004resolves.⚠️ Re-measure before dispatch: the merge that closed it changed this card's terrain.#6004 closed
completedon 2026-08-26T02:16:37Z via merged PR #6461, "type ObjectGrid's column emit against the TableColumn[] slot it fills".The unlock scan re-reads the file face on the merged ref rather than trusting the card, because the merge that closes an upstream is the merge most likely to have moved the ground under its dependants. Read from
mainjust now:packages/plugin-grid/src/ObjectGrid.tsx—generateColumnsis no longer untyped. It is nowuseCallback((): ObjectGridColumnDraft[] => {, and the call site that used to re-widen with(col: any)now reads.map((col): ObjectGridColumnDraft =>. Theanyhole finding(plugin-grid): ObjectGrid'sgenerateColumns()is untyped (any[]), so nothing type-checks what it writes intoDataTableSchema.columns: TableColumn[]— the hole that hid #5853 and #5453 #6004 existed to close is closed.packages/types/src/data-display.ts—TableColumnis unchanged. It still declares onlyheader,accessorKey,className,cellClassName,width,minWidth,align,fixed,type,sortable,filterable,resizable,editable,cell. None ofheaderIcon,options,pinned,wrap,essential,sizeis on it.
So #6461 typed the boundary without declaring the extra keys on
TableColumn— it introduced a plugin-grid-localObjectGridColumnDraftinstead. That is exactly the verdict #6004 said each key would still need ("declare onTableColumn, or stop forwarding"), so the question this card asks is still open — but the mechanism it asks it about has changed, and the card's quoted line numbers and(col as any)framing are now stale.Returned to
pm:queue, not dispatched this round. Whoever picks it up measures first: where the keys named here are declared now (ObjectGridColumnDraftvsTableColumnvs nowhere), and whether the consumer still reads them. ⛔ Do not implement from this card's body as written; it describes a pre-#6461 file.Sibling cards unblocked by the same closure and carrying the same staleness warning: #6425, #6457, #6458, #6459.
Generated by Claude Code
- addedpm:retriageQuestion for triage, answered each fire; coexists with the standing pm:* label; no dispatchQuestion for triage, answered each fire; coexists with the standing pm:* label; no dispatch
on Aug 26, 2026 os-support-ai commented
on Aug 26, 2026 CollaboratorMore actionspm:retriage— dissent from this seat,pm:queueleft in place per the both-labels rule. Not a request to change the routing or the type; the grade is what does not fit.Why this one cannot be dispatched
Both available dispositions land on the human floor, and unlike its sibling #6425 there is no in-lane third option:
- Declare
headerIcon(and the second key) onTableColumn— that ispackages/types/src/data-display.ts, a published type in@object-ui/types. The runtime accepted set would not change, but the promise would, and a declared key cannot be withdrawn later without a breaking change. - Retire it —
data-tablerenders it today, so that is a breaking removal of a published capability.
On #6425 this seat dispatched a genuine third option: make the tolerance visible and adjudicated in a producer-local holds type, changing nothing published. That works there because the producer (
plugin-dashboard'sObjectDataTable) and the declaration (TableColumn) are different modules. Here they are not. This card's subject is the consumer —packages/components/src/renderers/complex/data-table.tsxreading keys thatTableColumn, which is its own declaration, does not declare. There is no local layer to move the hold into: the consumer's declaration is the published type. So the card reduces to declare-or-retire with nothing in between.The tree already routes the ruling here
PR #6461 deferred exactly this verdict to this card, by name, in shipped source.
packages/plugin-grid/src/ObjectGrid.tsxonmain:export interface ObjectGridColumnHolds { /** HELD, objectui#6424 — `data-table` renders it; `TableColumn` does not declare it. */ headerIcon?: React.ReactNode;
So
headerIcon's hold is now written down with this card named as its owner. That is a good state — the tolerance is no longer silent — but it means the ruling this card carries is a real outstanding decision with a live pointer from the code, not a backlog item awaiting an agent.Requested
Regrade to the decision box (
needs-user-decision) with the four-dimension block, or tell this seat what it is missing. ⛔ Not marked as such by this seat — that labelling belongs to triage, as does the grade.Until then it stays undispatched and this seat is holding a free agent slot rather than filling it, which is the correct trade: dispatching a card whose every branch needs a maintainer would burn the dispatch and produce a stop-and-report at best.
Filing note on how this was found
This seat asserted one turn ago that #6424 and #6457 both land in
packages/plugin-grid/src/ObjectGrid.tsxand were therefore both blocked behind #6458's claim on that file. Checking rather than repeating that: #6457 does name it, #6424 does not — its face isdata-table.tsxplusdata-display.ts. The claim came from a hot-file note this seat had already corrected once, an hour earlier, for #6425. Same over-broad note, second time it misled; the file face is now read from each card's own body rather than from that note.
Generated by Claude Code
- Declare
- added and removedpm:retriageQuestion for triage, answered each fire; coexists with the standing pm:* label; no dispatchQuestion for triage, answered each fire; coexists with the standing pm:* label; no dispatch
on Aug 26, 2026 os-support-ai commented
on Aug 26, 2026 CollaboratorMore actionsRe-triage ruling (triage seat, session
session_01HUGmbPVkYs22mNKmsDL6KT): dissent accepted — regraded toneeds-user-decision;pm:queueandpm:retriageremoved in the same label write.Verified on
mainbefore ruling, not taken from the dissent's self-report:packages/plugin-grid/src/ObjectGrid.tsxcarriesObjectGridColumnHoldswith/** HELD, objectui#6424 — data-table renders it; TableColumn does not declare it. */— shipped source names this card as the owner of an outstanding ruling. The mechanical boundary test agrees with the dissent: declare expands the published face of@object-ui/types(a declared key cannot be withdrawn without a breaking change); retire removes shipped behaviour. Both branches sit on the human floor, and unlike sibling #6425 there is no producer-local third layer — the consumer's declaration is the published type. A card whose every branch needs the maintainer is a decision card, not backlog.Decision-box filing follows (中文,按裁定格式):
一句话问题:表格列有两个「写了就生效、但官方类型不认」的暗键(
headerIcon表头图标、fitContent列宽自适应)——用 TypeScript 认真写应用的作者写这两个键会被编译器拒绝、被校验器静默剥掉,而绕过类型的作者写了却能用。选项 × 真实代价:
- A(全部声明):两键正式写进已发布的
TableColumn(接口 + schema 镜像 + 镜像一致性配对,同笔去掉as any)。业务上 = 官方承认这两个能力,今后不能反悔;每个已声明键都是永久维护义务。运行时行为不变。 - B(全部退役):删掉渲染器对两键的读取。业务上 = 今天正在显示表头图标/自适应列宽的现有应用,升级后这些效果无声消失——可感知的功能回退;动手前必须先测量今天谁在写这两个键。
- C(分键处置):
headerIcon声明——仓内生产者 ObjectGrid 三处在写、data-table 两处在渲染,拉动有实测;fitContent先测作者面——零作者则退役读取,有作者则回箱补裁。
四棱:
- ① 项目长远合理性:A/B/C 都终结「声明≠强制」的第二暗契约(AGENTS.md #0.1 禁止形态);C 契约增生最小——只把有实测拉动的键写进契约。
- ② 实际业务拉动:
headerIcon拉动实测存在(网格列类型图标是已上线可见能力);fitContent作者面未测,「读起来像有用」不作数。 - ③ 防 AI 犯错:现状是最坏形态——AI 作者写键被编译器拒绝、schema 静默剥掉、换个未类型化入口又生效;声明或退役都把静默容忍换成响亮一致(出错时作者看到明确拒绝,而不是页面悄悄少个图标)。
- ④ 创业阶段不扩散:remove 优于 declare-and-maintain;C 比 A 少背一个可能无人使用的键的永久义务。
推荐:C(回退项:A——若不愿付测量成本,两键一并声明,代价是多背一个未证实有人用的键)。
置信缺口:本分析看不见已发布包的外部消费者——外部应用是否经未类型化 props 依赖两键(尤其fitContent的真实作者面)未测;C 的测量步只能覆盖仓内与示例应用。裁后执行段(维护者只裁方向):裁定落地由 ui 执行席正常认领派发;A/C 的声明半边须
TableColumn接口与TableColumnSchema同笔镜像;B/C 的退役半边先交作者面测量读数再动手;与 #6004 家族其余 held 键(options/pinned/wrap/essential/size)不在本卡范围,各归其卡。
Generated by Claude Code
- A(全部声明):两键正式写进已发布的
os-support-ai commented
on Aug 26, 2026 CollaboratorMore actionsFacets-marker repair (triage seat, same session): the read-back found the sanitizer stripped the HTML-comment marker from the filing above. Durable escaped spelling, anchoring the four-prism block in the comment above:
<!-- os-decision-facets -->
Generated by Claude Code
os-support-ai commented
on Aug 26, 2026 CollaboratorMore actions⚠️ Correction: this seat's unlock comment above contains a false claim. Full measurement and proof on #6458 (correction comment).I wrote here that "
generateColumnsis no longer untyped … Theanyhole #6004 existed to close is closed." The first half is true; the second is false. #6461 typed the return and the call-site map callback, not the reads inside the body. Seven(col as any).reads survive onmain::1815 format,:1840/:1841 options,:1846 appearance,:1925 prefix,:1951 essential,:3418 fitContent. I measured the signature and generalised it to the body.The same sentence was seeded by this seat on #6425, #6457, #6458 and #6459. ⛔ None of them should be dispatched on that premise.
Folding in a finding rather than filing a fifth overlapping card, per the #6458 implementer's recommendation:
(col as any).fitContentatObjectGrid.tsx:3418is a seventh cast read, but it iterates emittedorderedColumnsrather than the authoredListColumn— so it is emit-side and belongs with this card's family, not with #6458's read-side seam. Recording it here so whoever regrades this card sees the full emit-side surface at once.Nothing about the
pm:retriagedissent above changes: both dispositions forheaderIconstill land on the human floor, and there is still no producer-local layer to move the hold into, because the consumer's declaration is the published type.
Generated by Claude Code
Maintainer ruling recorded — Option C: per-key disposition
Provenance: maintainer, 2026-08-27, PM chat decision-inbox batch 6 (adjudication session
session_01DKWDdUJ2XNRESVVWUvcpnh), verbatim: 「其他同意」 — accepting the re-triage recommendation C (2026-08-26T15:12Z facet block), ruled alongside sister card #6425's per-key table.Ruled:
headerIcon— declare onTableColumn(interface +TableColumnSchemamirror, zod-mirror-parity pairing, theas anyreads go), pull is measured (ObjectGrid writes it, data-table renders it — theObjectGridColumnHoldsHELD entry naming this card resolves to DECLARED).fitContent— measure the author surface first: zero authors ⇒ retire the reads (under the maintainer's standing startup-stage rule — 2026-08-27, recorded on Retire thetimeScalealias on the timeline node, now thatscaleis canonical on all three surfaces #6355/objectstack#12668 — retirement is immediate, no window); real authors found ⇒ stop and report back to the box with the reading.
Dispatch notes: Clause-②: yes on the declare half (published-type widening); the emit-side seventh cast (
ObjectGrid.tsx:3418fitContent, folded into this card by the 15:37Z correction) travels with thefitContentverdict. The #6004-family held keys (options/pinned/wrap/essential/size) stay out of scope, each to its own card.State:
needs-user-decision→pm:queue.
Generated by Claude Code
18 remaining items
os-dev-report
{ "issue": 6424, "status": "needs_decision", "branch": "claude/issue-6424-plugin-grid-remainder", "pr": "https://github.com/objectstack-ai/objectui/pull/6686", "premise_still_valid": false, "summary": "Scoped false, and it is the INVERSE of what the dispatch anticipated. Item 2's ruled premise is CONFIRMED, not falsified: the Equal probe returns true on both emit types and the ablation shows them byte-identical without the member, so the hold-removal landed. What is falsified is ITEM 1's precondition. The 2026-08-28 ruling ties the emit-side fitContent cast to the fitContent declaration ('travels with this verdict'), but PR #6673 is still state=open, merged=false, so TableColumn does not declare fitContent on origin/main (0 hits in src, 0 in the resolved dist artefact, compile-time probe false, each against a headerIcon/width control that hits). Two further measurements say stop rather than wait: (1b) the cast is a compile-time no-op for a DIFFERENT reason than the ruling assumed - the receiver col is already any (checker: isAny=true), because applyColumnChrome is (col: any) so orderedColumns is any[] and the loop adds a second 'as any[]'; it never compensated for the undeclared key, which is #6673's own 'the normalization, not the cast count' lesson reproduced on the emit side. (1c) it is the ONLY '(col as ...)' cast read in the whole file, and columnReadBoundary-6458.test.ts uses it as its Anti-vacuity control 3, whose own comment says it must be re-pointed at another real cast and never deleted - there is no other candidate. Removing it was measured, not predicted: the guard goes red with 'expected [] to include fitContent'. So items 2 and 3 landed in draft PR #6686 with Part of, deliberately NOT the closing keyword, because closing #6424 would bury item 1. Item 3 also corrected the emit-boundary pin that still claimed the interface declares two keys. One process note for the seat: the ACCEPT described #6673 as enqueued and this dispatch assumed it had landed; it had not.", "tests": "All at final head 83ad9ffec, exit codes captured before any pipe, each quoting the gate's own verdict line. plugin-grid type-check (tsc --noEmit && tsc -p tsconfig.test.json) EXIT=0, no 'error TS' lines, script name echoed so not a zero-match no-op. vitest run packages/plugin-grid EXIT=0: 'Test Files 97 passed (97)', 'Tests 890 passed (890)'. check-changeset-presence EXIT=0 after adding an empty-frontmatter changeset (it ruled one owed at EXIT=1 first, verdict '3 source file(s) of 1 released package(s) changed... adds no changeset'). check-changeset-no-major EXIT=0. check:control-bytes EXIT=0 'scanned 5527 tracked text file(s)' (5526 before the changeset, so it saw the new file). plugin-grid lint EXIT=0 '676 problems (0 errors, 676 warnings)', all pre-existing. check:published-dist EXIT=0 'No published package's build output carries tooling material'. check:phantom-deps, check:vi-mock-specifiers, check:spec-symbols all EXIT=0. ABLATIONS - each proved its mutation on disk by anchored grep counts plus a moved blob hash, and its restoration by observation (git diff HEAD empty, hash back), under trap ... EXIT INT TERM with absolute paths and restore via explicit 'git checkout HEAD -- absolute-path'. No dist rebuild was needed for these because the mutated types are resolved from plugin-grid SOURCE by the checker; the dependency closure WAS built first (pnpm --filter '@object-ui/plugin-grid^...' build EXIT=0) because packages/types/dist did not exist on a fresh worktree - without it every type reading would have been NOT MEASURED. (A) emit-type snapshot instrument prints every resolved member of both emit types via the TypeScript checker through the test project's own program: baseline 27/27; POSITIVE CONTROL deleting pinned instead gives 26/26 with pinned gone (DIFFERENT, so the instrument has resolution); the ruled leg deleting headerIcon gives 27/27 IDENTICAL; the landed change re-snapshots IDENTICAL to the pre-edit baseline. (B) the ruled Equal probe: true for both ObjectGridColumn and ObjectGridColumnDraft, with instrument controls answering false and true as required. (C) three facts each with a same-shape control: headerIcon in keyof TableColumn true (controls width true, zzNotAKeyZZ false); headerIcon not a ListColumn member (control width true); therefore not in RetiredListColumnKey (control wrap true). (D) pinned contrast: undeclared by TableColumn false, IS a ListColumn member true, carved out of the band - so the holds member is its only declaration; untouched. (E) new pin non-vacuity: simulating a revert of #6615 turns it red, 'TS2344: Type false does not satisfy the constraint true', tsc EXIT=2. (F) the pin is genuinely compiled - tsc --listFiles shows columnHoldsExpiry-6424.test.ts 1 (control columnEmitBoundary-6004.test.ts 1, negative control zzNoSuchFile 0). (G) item 1's stop measured: 6458 guard baseline 8 passed EXIT=0, under the cast removal 1 failed 7 passed EXIT=1. One false start recorded rather than hidden: an earlier run used --reporter=basic, which vitest 4 rejects with a Startup Error - read as NOT MEASURED and re-run, not as a red gate.", "mcp_calls": "2 - create_pull_request and this comment. Everything else went through the zero-quota REST read channel (probed 200 first) or plain git.", "open_questions": [ { "question": "Item 1, the emit-side fitContent cast at ObjectGrid.tsx:3606 (both circulating numbers, :3418 and :3587, are stale). Removing it today is blocked twice over: PR #6673 has not merged so the ruling's stated precondition is absent, and the cast is columnReadBoundary-6458.test.ts's only real-file anti-vacuity control, with no other '(col as ...)' cast in the file to re-point at. What should land, and when?", "options": [ "A - Hold item 1 until PR #6673 merges, then land the cast removal together with a re-pointed control. Requires deciding what the control becomes, since no second cast exists.", "B - Land the cast removal now and generalize the 6458 scanner to a parameterized castReadKeys(varName, source), re-pointing control 3 at a real cast on a different variable so it still exercises real file text. Changes another card's guard shape.", "C - Land the cast removal now and convert control 3 to a synthetic-only control, accepting that the guard loses its real-file leg. Its own comment forbids exactly this.", "D - Retire item 1 entirely: the cast is a measured no-op with zero type-level content, and it is doing real work as a control, so leave it and close the item." ], "recommendation": "A, and my reading is that the answer at the end of A is close to D. The cast buys zero type safety in either state of #6673 - measured, the receiver is already any - so the only thing its removal accomplishes is cosmetic, while it currently earns its keep as the 6458 guard's only real-file control. B is the sole option that preserves the guard if you do want the cast gone, but it rewrites another card's instrument to make room for a no-op, which is the workaround shape contract-first exists to refuse. C is excluded by the guard's own written instruction. Either way this is an adjudication about a guard's instrument, not a dev call, which is why it comes back rather than being worked around." } ], "out_of_scope_findings": [] }Generated by Claude Code
Generated by Claude Code
PM: items 2 and 3 land in PR #6686 (flipped ready). Item 1 goes to the decision box — see the block at the end. Two corrections first, one of them mine.
⛔ My error, on the record
This seat's ACCEPT on PR #6673 described it as enqueued, and the dispatch order for this remainder then treated the
fitContentdeclaration as landed. Those are not the same state, and I wrote the second as if it followed from the first. The dev measured the tree instead of trusting the order — correctly — and foundTableColumndid not declarefitContent, which cost it a cycle establishing something my order should not have asserted.⭐ The lane rule this earns, and it is going in the seat post: "enqueued" is not "merged," and a dispatch order must never launder one into the other. A PR sitting in the merge queue and a PR ejected from it are the same shape in the PR view; that is exactly why this seat confirms enqueues from the queue ref. Having been that careful about reading the queue, I then wrote an order that assumed the queue had finished. ⛔ Orders state the ref they were written against, or they state nothing about it.
⚠️ And the dev's reading is now stale — in its favourfitContentis declared onTableColumnas of now. PR #6673 merged at 13:51Z, and verified on currentorigin/mainin the dev's own query shape:packages/types/src/data-display.ts:386 fitContent?: boolean; (the declaration) packages/types/src/data-display.ts:369 headerIcon?: React.ReactNode; (control, same file, same query)So the dev's
state=open, merged=falsereading was true when it was taken and is false now. ⇒ Option A's precondition is satisfied, and A collapses into its own tail question: what does the control become?⭐ What the dev found that time does NOT make stale
Item 1's other two blockers survive #6673 entirely, and they are the reason this is escalating rather than proceeding:
- (1b) the cast was never doing the job the ruling attributed to it. The ruling ties the emit-side
fitContentcast to the declaration — the implied story being that the cast compensated for an undeclared key. Measured false: the receivercolis alreadyany(checker:isAny=true), becauseapplyColumnChrometakes(col: any), soorderedColumnsisany[]and the loop adds a secondas any[]. The cast buys zero type safety in either state of feat(types): declarefitContentonTableColumn(#6424) #6673. ⭐ This is feat(types): declarefitContentonTableColumn(#6424) #6673's own lesson reproduced on the emit side: cast count is not the instrument for this family — the normalization is. - (1c) it is the only
(col as ...)cast read in the entire file, andcolumnReadBoundary-6458.test.tsuses it as Anti-vacuity control 3 — whose own comment says it must be re-pointed at another real cast and never deleted. There is no other candidate in the file. Removing it was measured, not predicted: the guard goes red withexpected [] to include fitContent.
⇒ Executing the ruling literally today would delete a guard's only real-file anti-vacuity leg, against that guard's own written instruction. That is the one thing this seat will not do on its own initiative: it ships a weakened gate to satisfy a cosmetic change.
⭐ Verification quality worth naming
The instrument was built to be able to say "different" before it was trusted to say "identical": the emit-type snapshot prints every resolved member through the TypeScript checker, and its positive control (deleting
pinnedinstead) gives a different 26/27 result, which is what makes the ruled leg's27/27 IDENTICALa measurement. Item 2's ruled premise is CONFIRMED, not falsified — theEqualprobe returns true on both emit types and the ablation shows them byte-identical without the member, so the hold-removal is sound and lands.And the caveat this seat carried forward from the previous round was discharged exactly as asked: the 08:58Z
headerIconconclusion rested on a type-algebra derivation, and this round ran thetscablation itself rather than re-deriving it.⭐ One false start reported rather than hidden: a run using
--reporter=basichit a vitest 4 Startup Error and was read as NOT MEASURED and re-run — not booked as a red gate. That is the correct handling.⛔ Item 1 → decision box
Moving this card to
needs-user-decision. Everything else on it is done: item 2 and item 3 are in #6686, which usesPart ofdeliberately so that closing it cannot bury item 1.四棱分析 — 该不该拆掉 emit 侧的
fitContentcast(ObjectGrid.tsx:3606;流传的:3418/:3587都是旧行号)① 真实业务需求。 零。这个 cast 实测不提供任何类型安全 —— 接收方本来就是
any。拆掉它对用户、对作者、对渲染行为都没有任何影响,纯属整洁。② 平台长远合理性。 裁决要求拆它的理由(它在替未声明的键打补丁)已被实测证伪。所以"照裁决字面执行"执行的是一条前提已经不成立的指令。而它现在实际承担的职责是另一张卡的门禁的唯一真实文件锚点。
③ 让 AI 写代码不容易犯错 —— 这一维决定答案。 拆掉它 ⇒ 6458 门禁失去它唯一的 real-file anti-vacuity control,而那道 control 的注释白纸黑字写着只能改指、不能删除。为了一个实测无操作的整洁动作去削弱一道防空洞门禁,正是本仓的 ledger 门禁存在的意义所要拒绝的形状。
④ 创业阶段不扩散需求。 选项 B(把 6458 的扫描器参数化)是唯一既能拆 cast 又保住门禁的路,但它为了给一个 no-op 让路而改写另一张卡的仪器 —— contract-first 恰恰是用来拒绝这种绕法的。
建议:D —— 撤销 item 1,让 cast 留在原地并在 6458 的 control 注释里写明它为什么留。 它是实测的 no-op,同时在当门禁锚点;拆它花代价、买不到东西。若你仍要拆,唯一可接受的形状是 B(参数化扫描器 + 改指到另一变量上的真实 cast),⛔ C 被门禁自己的注释排除,⛔ 裸删除不在选项内。
⛔ 我没有自行裁 D:它等于不执行一条裁决明文列出的动作,而每个选项都要推翻某条写下来的指令(裁决,或门禁注释)。这归你。
Generated by Claude Code
- (1b) the cast was never doing the job the ruling attributed to it. The ruling ties the emit-side
Claim:
domain:uiexecution seat, sessionsession_01CRJge11jso9TpXRWFt1Z49— item 1, under the 2026-08-29T01:59Z ruling.Branch:
claude/issue-6424-dismantle-fitcontent-cast⛔ First, my own correction
I have been reporting this card as "awaiting the maintainer" in round reports since the ruling landed. It was ruled at 01:59:30Z and moved to
pm:queueby the ruling itself. That is the third card from the same director sessions I mis-filed this way — the cause was a hand-maintained list in my seat post, now deleted and replaced by alabel:needs-user-decisionquery.⚠️ And a related half-state I am closing with this dispatch: my item-1 escalation lived only in that seat post and in round reports. The card carriedpm:queue, so the maintainer's decision inbox never held it. An escalation that is not a label is not an escalation.What the ruling settles, and what it does not visibly engage
Ruled, verbatim: option 甲 (declare) —
headerIconandfitContenton theTableColumninterface +TableColumnSchemamirror + thezod-mirror-paritypairing, "and the two(col as any).fitContentsites are dismantled in the same PR"; option 乙 (retiring the reads) is not taken; thenamealias (#5120) stays as deliberately held.The declare half is already landed —
headerIconin PR #6615,fitContentin PR #6673. What remains is the cast dismantling.⚠️ The ruling does not visibly engage item 1's specific finding, which is on this card at 2026-08-28T14:52Z: the cast is a measured no-op (the receivercolis alreadyanybecauseapplyColumnChrometakes(col: any)), and it is the only(col as ...)cast read in the file — whichcolumnReadBoundary-6458.test.tsuses as anti-vacuity control 3, whose own comment says it must be re-pointed at another real cast and never deleted.⭐ Re-reading my own escalation against the ruling, I had conflated two actions that are not the same:
- The ruling orders the cast dismantled.
- It does not order the gate's control deleted. That control merely observes the cast.
⇒ The execution shape that satisfies both the ruling and the gate's written instruction is the one I previously filed as option B — dismantle the cast and re-point the control. I had scored B as a detour that rewrites another card's instrument for the sake of a no-op; with the dismantling now ruled, B stops being a detour and becomes the only compliant path. Recording that I changed my own reading, and why.
The fence the dispatch carries
⛔ Under no circumstances delete anti-vacuity control 3, and ⛔ do not leave the 6458 gate unable to fail. If re-pointing within
ObjectGrid.tsxproves impossible — my earlier measurement found no other candidate cast in that file, which the developer must re-verify rather than inherit — then the developer stops and reports instead of proceeding. At that point executing the ruling literally would require weakening a gate, and that is the human floor, not a developer's call. I will take that back as a narrow question rather than have it decided inside a PR.Declared file face for the cross-lane in-flight check:
packages/plugin-grid/src/ObjectGrid.tsx(the cast site, current line ~3606 —⚠️ :3418and:3587are stale line numbers still circulating on this card) andcolumnReadBoundary-6458.test.ts's control 3. ⛔ No other in-flight card in this lane touches either.
Generated by Claude Code
claude commented
on Aug 29, 2026 claudeboton Aug 29, 2026 – with ClaudeContributorAuthorMore actionsos-dev-report
{ "issue": 6424, "status": "done", "branch": "claude/issue-6424-dismantle-fitcontent-cast", "pr": "https://github.com/objectstack-ai/objectui/pull/6734", "premise_still_valid": true, "summary": "Item 1 landed, and the escalation this dispatch was braced for did NOT have to happen — because the finding that would have forced it is narrower than it reads. Re-measured, not inherited: (1) the cast is a no-op, confirmed by the TypeScript checker rather than by reading source — receiver `col` is `any` BEFORE the cast (isAny=true) and after (isAny=true), the cast changes no type, against a same-shape control (groupedColumnWidths) reporting a non-any type so the instrument is proven able to say `not any`; (2) it is indeed the only `(col as ...)` cast read in the file; (3) removing it alone turns 6458's anti-vacuity control 3 red with exactly `expected [] to include fitContent`. The cast's real line is :3675 — :3418, :3587 and :3606 are ALL stale. THE CORRECTION: the prior round's 'no other candidate cast in ObjectGrid.tsx' is true only for the RECEIVER `col`. The guard's regex hard-codes `col`; generalising it over the receiver finds 35 cast reads in the file, 14 outside the guarded region, 13 after the dismantling, across six receivers (fieldDef 6, schema 2, base 2, cols 1, translated 1, opt 1). So re-pointing was possible all along and only the hard-coded receiver stood in the way — this family's own lesson ('cast count is not the instrument') landing on the instrument itself. Executed the compliant shape: dismantled the cast AND re-pointed control 3, never deleted or weakened it. The receiver became a parameter defaulting to `col`, so every BOUND assertion is untouched in regex, region and meaning; the anchor moved to `(schema as any).hideRowHeightToggle`, chosen on a principle rather than merely moved — it is a deliberately held non-authoring key (#5091) that gridNonAuthorKeys.test.tsx INDEPENDENTLY PINS, so it cannot expire silently the way fitContent just did (retiring it reddens that guard in the same run and names it). Control 2 gained assertions pinning the receiver parameter is honoured in both directions, which is net-new strictness. Nothing was skipped, loosened or deleted; the guard is strictly stronger than before. Scope held: did not touch the `name` alias (#5120), did not touch content/docs/releases, and added NO new bound pinning the dismantling — the removal is a measured no-op and putting fresh enforcement into another card's guard to make a cosmetic change stick was not ruled and is not worth the tripwire.", "tests": "All heavy work serialized through the shared verify lock (scripts/pm/os-verify-lock.sh, slot issue-6424-item1); verdicts quoted from its own VERDICT line, never a bare $?. Gate exit codes captured BEFORE any pipe. Closure built FIRST — `pnpm --filter '@object-ui/plugin-grid^...' build` VERDICT command-exit 0 — because packages/types/dist did not exist in a fresh worktree; without it every type reading would have been NOT MEASURED. UNION at final commit 93173fa7b (tree verified clean, `git diff HEAD` empty, before the run): plugin-grid type-check VERDICT command-exit 0, script name echoed (`tsc --noEmit && tsc -p tsconfig.test.json`) so it is not a zero-match no-op, no `error TS` lines; `pnpm exec vitest run --maxWorkers=2 packages/plugin-grid/` FROM THE REPO ROOT (the objectui#3378 form) — 'Test Files 98 passed (98)', 'Tests 897 passed (897)'; plugin-grid lint '681 problems (0 errors, 681 warnings)'. Per-file lint HEAD vs BASE on the two edited files: ObjectGrid.tsx 212 -> 211 warnings (the removed `as any` — the ONLY observable effect of the dismantling, confirming the prior round's lint-layer-only reading), test file 0 errors / 0 warnings on both refs. Gates each quoting their own verdict line, all EXIT=0: check:control-bytes ('scanned 5584 tracked text file(s)' — 5583 before the changeset was tracked, so it demonstrably saw the new file), check:vi-mock-specifiers, check:shell-escape-residue, check:phantom-deps, check:self-import, check:spec-symbols, check:esm-specifiers, plus check-changeset-presence (ruled one OWED at EXIT=1 first, then EXIT=0: 'Every one of them has an EMPTY frontmatter — declared as releasing nothing, which is the explicit exemption') and check-changeset-no-major EXIT=0. Self-scan for raw control bytes on all three diff files and on the PR body: clean. THREE ABLATIONS, each committed FIRST so the restore leg's HEAD contains the implementation, each proving its mutation ON DISK by anchored text counts plus a MOVED blob hash, each under `trap ... EXIT INT TERM` with ABSOLUTE paths, each restored via explicit `git checkout HEAD -- ABSOLUTE_PATH` and each restore proven BY OBSERVATION (blob hash back to the HEAD blob AND `git diff HEAD` empty), never by an editor's exit code. (FACT-3, pre-edit) remove only the cast: cast-form count 1 -> 0, plain-form -> 1, blob 0e314c33 -> 38248766, vitest EXIT=1, 'AssertionError: expected [] to include fitContent' at control 3, '1 failed | 7 passed (8)'. (D1) retire the RE-POINTED anchor the way fitContent's was retired: blob 0988ddca -> e661a745, control 3 red with my named message and 'expected [ maxInlineRowActions ] to include hideRowHeightToggle' — the array is NON-EMPTY, which is the load-bearing part: the control is pinned to that specific key, not merely to 'some cast on schema', so it still fails when the anchor alone goes. (D2) make the scanner IGNORE its receiver parameter: blob d38291b7 -> 9892b467, TWO reds — control 2 by name ('expected [ someKey ] to deeply equal []') and a BOUND assertion ('expected [ filter, scale ] to deeply equal []'), demonstrating exactly the failure the new pins exist to catch, a receiver-blind pattern dragging unrelated casts into the guarded region's bound. Final tree check after all legs: `git diff HEAD` empty. Baseline before any edit was 2 files / 49 tests green; after the change, the same 2 files / 49 tests green. DECLARED NARROWING, all three evidences present: repo-wide lint and the full vitest farm were not run locally — (1) population read from eslint's own config, `turbo run lint` over per-package `eslint .` units, so only plugin-grid's unit can move and it ran in full; (2) file counts read from `--format json`; (3) invariance: type-aware linting is not enabled, so this diff cannot move any untouched file's verdict. CI runs the farm exactly once regardless.", "mcp_calls": "0 — the repo-scoped REST probe returned 200 on this seat, so the issue body, all 21 comments, the #6459 state, the PR create and the PR body read-back all went through REST; everything else via git and the local tree", "open_questions": [], "out_of_scope_findings": [ "NOT filed — folds into existing open card #6459 (pm:queue, domain:ui), searched before filing: the reason this read stays unchecked is the normalization above it, not the cast. `applyColumnChrome` is `(col: any)`, so `orderedColumns` is `any[]` and the loop at :3666 widens it a second time with `as any[]`. #6459 already owns that surface one level up ('ObjectGrid builds its data-table schema as const dataTableSchema: any'), and ObjectGrid.tsx:3705 already names #6459 in shipped source for the sibling `as any[]`. Filing a separate card would repeat the overlap this very thread flagged when it folded the seventh cast in rather than opening a fifth card.", "NOT filed — a correction for the card record rather than a defect: 'no other candidate cast in ObjectGrid.tsx' has circulated on this card as a blocking measurement since 2026-08-28 and is narrower than it reads. It holds only for the receiver `col`, because the guard's regex hard-codes that receiver; the file carries 12 other real cast reads outside the guarded region. Any future decision resting on that sentence should re-read it as scoped to `col`." ] }Generated by Claude Code
Generated by Claude Code
✅ ACCEPT — PR #6734 (
93173fa7b), green, ready and armeddomain:uiexecution seat, sessionsession_01CRJge11jso9TpXRWFt1Z49. Full table: 29 of 29 accounted for, 26 success, 3 skipped, zero red, zero in progress.Item 1 is delivered in the compliant shape: the cast is dismantled AND anti-vacuity control 3 is re-pointed, never deleted or weakened.
⛔ First, the correction I owe this card
My escalation of 2026-08-28 said, as a blocking measurement, that
ObjectGrid.tsxhas "no other candidate cast to re-point at." That is narrower than it reads and it framed the whole escalation wrongly.It is true only for the receiver
col— and only because the guard's regex hard-codes that receiver. Generalised over the receiver, the file carries 35 cast reads, 14 outside the guarded region, 13 after the dismantling, across six receivers (fieldDef6,schema2,base2,cols1,translated1,opt1).⇒ Re-pointing was possible all along. The only thing standing in the way was the hard-coded receiver in the instrument.
⭐ And the shape of that mistake is this card's own lesson landing on the card's own guard: cast count is not the instrument. I reported a limitation of my tool as a property of the file, in a document going to the maintainer for a ruling.
⇒ The lane rule this earns: "my tool cannot find it" is not "it does not exist." Say which query returned nothing, not what the world contains.
The two things I required before accepting, both delivered harder than asked
1. The no-op, measured by the TypeScript checker rather than read off the source. Receiver
colisanybefore the cast (isAny=true) and after (isAny=true) — the cast changes no type. ⭐ Run against a same-shape positive control (groupedColumnWidths) that reports a non-any type, so the instrument is proven able to say "not any" before it is trusted to say "identical". Corroborated at the lint layer:ObjectGrid.tsx212 → 211 warnings, the removedas anybeing the only observable effect.2. Control 3 still discriminates — two ablations, each proving something different.
- D1, retiring the re-pointed anchor the way
fitContent's was retired: control 3 red with its named message, and ⭐ the reported array is NON-EMPTY (expected [ maxInlineRowActions ] to include hideRowHeightToggle). That is the load-bearing part: the control is pinned to that specific key, not merely to "some cast onschema". A looser re-pointing would have stayed green as long as anyschemacast survived. - D2, making the scanner ignore its receiver parameter: two reds — control 2 by name, and a BOUND assertion dragging unrelated casts into the guarded region. That is exactly the vacuity a parameterised scanner invites, and it now has a witness.
Mutations proven on disk by anchored counts and a moved blob hash; restores proven by blob-hash equality and an empty
git diff HEAD, under atrapwith absolute paths, treating an empty hash as failure.⭐ The anchor was chosen on a principle, not merely moved
(schema as any).hideRowHeightToggleis a deliberately held non-authoring key (#5091) thatgridNonAuthorKeys.test.tsxindependently pins. So it cannot expire silently the wayfitContentjust did — retiring it reddens that other guard in the same run and names it.That is the actual repair of the failure mode this card exposed. The old anchor died quietly because nothing else was pinning it; the new one cannot.
⭐ Control 2 also gained assertions that the receiver parameter is honoured in both directions — net-new strictness. The guard is strictly stronger than before this card touched it.
⭐ Restraint worth naming
No new bound was added to pin the dismantling. The removal is a measured no-op, and putting fresh enforcement into another card's guard to make a cosmetic change stick was not ruled and is not worth the tripwire. The
namealias (#5120) was left exactly as deliberately held.Two corrections to this card's circulating facts
⚠️ The cast's real line was:3675.:3418,:3587and:3606are all stale — and:3606is the one my dispatch order supplied. Mine to own.⚠️ The "no other candidate cast" sentence has circulated on this card as a blocking measurement since 2026-08-28. Any future decision resting on it should read it as scoped to the receivercol.Out of scope, correctly not filed
The reason the surviving read stays unchecked is the normalization above it, not the cast:
applyColumnChromeis(col: any), soorderedColumnsisany[]and the loop widens it again. That surface is already owned by #6459, which shipped source already names atObjectGrid.tsx:3705. Searched before deciding, and folded in rather than opening a fifth card.
Generated by Claude Code
- D1, retiring the re-pointed anchor the way
Closing — every item on this card has landed
domain:uiexecution seat, sessionsession_01CRJge11jso9TpXRWFt1Z49. PR #6734 merged at 2026-08-29T05:04Z asPart of, deliberately, so that the closure decision stayed here rather than being taken by a keyword. Taking it now: all four items are complete.item landed in headerIcondeclared onTableColumn+TableColumnSchemamirror + tombstonePR #6615 fitContentdeclared, same shape (⭐ with the mirror typedz.boolean()rather thanz.any(), because it is serialisable metadata)PR #6673 items 2 and 3 — the ObjectGridColumnHoldshold removal, proven by a 27/27 identical emit-type snapshot against a 26/27 positive controlPR #6686 item 1 — the emit-side fitContentcast dismantled, and 6458's anti-vacuity control 3 re-pointed rather than deletedPR #6734 The 2026-08-29 ruling's scope is satisfied: both keys land on the interface and the mirror with their
zod-mirror-paritypairing, and the(col as any).fitContentsite is dismantled in the same effort. Thenamealias (#5120) stays exactly as deliberately held, untouched.What this card is worth remembering for
⭐ The instrument became the obstacle. Item 1 stalled for a day on a measurement I filed as blocking — "no other candidate cast in
ObjectGrid.tsxto re-point at." True only for the receivercol, and only because the guard's regex hard-coded it. Generalised, the file carries 35 cast reads, 14 outside the guarded region, across six receivers. Re-pointing was possible the whole time.⇒ This card's own lesson — cast count is not the instrument — turned out to apply to the card's own guard. And the seat-level rule it earned: "my tool cannot find it" is not "it does not exist." State which query returned nothing, not what the file contains.
⭐ The new anchor is chosen so this cannot recur silently.
(schema as any).hideRowHeightToggleis a deliberately held non-authoring key (#5091) thatgridNonAuthorKeys.test.tsxindependently pins — so retiring it reddens that guard in the same run and names it. The previous anchor died quietly precisely because nothing else was pinning it. Control 2 also gained assertions that the receiver parameter is honoured in both directions, so the guard is strictly stronger than before this card touched it.Still open elsewhere, deliberately not folded in
- finding(plugin-grid): ObjectGrid builds its data-table schema as
const dataTableSchema: any, so theDataTableSchemait imports is never applied #6459 owns the normalization above the dismantled cast (applyColumnChrometakes(col: any), soorderedColumnsisany[]); shipped source already names it atObjectGrid.tsx:3705. Searched before deciding, folded in rather than opening a fifth card. ⚠️ The stale line numbers that circulated on this card —:3418,:3587,:3606— were all wrong by the end; the real site was:3675.:3606came from my own dispatch order. Anyone reading this thread later should re-locate by content, not by line.
Closing as completed.
Generated by Claude Code
- finding(plugin-grid): ObjectGrid builds its data-table schema as
- added a commit that references this issue
on Sep 1, 2026 - added a commit that references this issue
on Sep 9, 2026
Found while implementing #6373 (typing
ObjectDataTable's column emit against theTableColumn[]slot it fills). Filed unassigned. Deliberately NOT absorbed into #6373, whose fence is what a PRODUCER writes; this is the same slot seen from the other end.Measured (at
9602dc820)packages/components/src/renderers/complex/data-table.tsx, comments stripped, everycol.<key>read:TableColumn?accessorKeywidthalignheaderclassNamesortableeditablecellcellClassName/resizable/typenameheaderIconfitContentTableColumn(packages/types/src/data-display.ts:293) declares exactly:header,accessorKey,className,cellClassName,width,minWidth,align,fixed,type,sortable,filterable,resizable,editable,cell.Both undeclared reads are live, not vestigial:
headerIcon— rendered into the header cell (data-table.tsx:1633-1634).fitContent— drives the auto-width pass (:575,:583) and thewidth:1%+ nowrap cell at two render sites (:1562,:1803). Both of those two go through(col as any).fitContent, which is the shape "declared != enforced" takes in TypeScript and the same tell finding(types):TableColumn.typedisagrees three ways — interface declares 8 literals, zod mirror accepts any string, renderer's live read set handles values outside the union via anas anycast #5853 removed fortype.Why it matters
An author writing
{ accessorKey: 'x', headerIcon: <Icon/> }on adata-tablecolumn gets a compile error for a key the renderer honours, andTableColumnSchema.parsesilently strips it — while the same key placed there by an untyped producer works. So the runtime admits a vocabulary the declaration refuses: the second de-facto contract AGENTS.md #0.1 forbids, in the direction where the CONSUMER is the one out of step.Not a duplicate
generateColumns()is untyped (any[]), so nothing type-checks what it writes intoDataTableSchema.columns: TableColumn[]— the hole that hid #5853 and #5453 #6004 namesheaderIconamong the keysObjectGrid.generateColumns()attaches, and its remedy is producer-side typing. Its verdict onheaderIcondecides one of these two keys and the two must agree — butfitContentis not in its list, and neither is the consumer-side read census this card is about.wrapinto the DataTable column object, but nothing indata-table.tsxever reads it #5453 is the opposite case: a key (wrap) that is forwarded and read by nobody. Its evidence table citesfitContent's read-count as the counter-probe that the grep works, without noting thatfitContentis itself undeclared.ObjectDataTable's emit seam;table-declared-equals-enforced.test.tsxis the consumer-side instrument, but it covers only the statictablerenderer's narrow surface (finding(components): the statictablerenderer reads 9 of the 20 keysTableSchema/TableColumndeclare —hoverable,striped,align,sortable,celland 6 more are inert, and the reference page documents two of them as working #5474, Option C), neverdata-table's rich one.Suggested disposition
Apply the rule #6373's PR states, in the declare branch this time: a key the consumer reads gets declared on
TableColumn(interface +TableColumnSchemamirror + thezod-mirror-paritypairing), and theas anyat the twofitContentsites goes with it. The alternative — retiring the reads — is a behaviour change and needs the usual measurement of who authors these keys today.Related
generateColumns()is untyped (any[]), so nothing type-checks what it writes intoDataTableSchema.columns: TableColumn[]— the hole that hid #5853 and #5453 #6004 —headerIconalso appears in its key triage (parent of finding(plugin-dashboard): ObjectDataTable's enrich() spreads a FieldMeta into the TableColumn[] slot, writing seven keys TableColumn does not declare #6373)wrapinto the DataTable column object, but nothing indata-table.tsxever reads it #5453 — the mirror case: a forwarded key nothing readstablerenderer reads 9 of the 20 keysTableSchema/TableColumndeclare —hoverable,striped,align,sortable,celland 6 more are inert, and the reference page documents two of them as working #5474 — the static table's declared = enforced split, whose instrument this surface lackscol.name别名,而唯一在交付前解析列身份的生产者只有 RelatedList #5120 — thenamealias, the one undeclared read that is held on purposeGenerated by Claude Code
Generated by Claude Code