Repository navigation
finding(plugin-dashboard): ObjectDataTable honours five undeclared authored column keys as field-meta overrides #6425
Description
Found while implementing #6373 (retiring the six FieldMeta keys ObjectDataTable.enrich() wrote into the TableColumn[] slot). Filed unassigned. #6373's fence is the WRITE; this is the same seam's READ, and it is a different adjudication — retiring an inert write costs nothing, while these keys are load-bearing.
Measured (at 9602dc820)
packages/plugin-dashboard/src/ObjectDataTable.tsx, inside enrich(), the overrides handed to buildFieldMeta:
overrides: {
type: col.type,
format: col.format,
options: col.options,
referenceTo: (col as any).referenceTo,
currency: (col as any).currency,
decimals: (col as any).decimals,
},Five of those six keys — format, options, referenceTo, currency, decimals — are read off the AUTHORED column and are declared by neither TableColumn (packages/types/src/data-display.ts:293) nor its TableColumnSchema mirror. Three are reached through (col as any), which is the same "declared != enforced" tell #5853 removed for type.
This is not a dormant limb. packages/plugin-dashboard/src/__tests__/ObjectDataTable.cells.test.tsx exercises it directly:
const schema: any = {
type: 'object-data-table',
objectName: 'opportunity',
columns: [
{ header: 'Amount', accessorKey: 'amount', format: '$0,0' },
{ header: 'Probability', accessorKey: 'probability', format: '0%' },
],
};and asserts $150,000 / 60% render. Note the : any on the schema — that annotation is what lets the undeclared key past tsc in the test itself.
Why it matters
The widget honours an authoring vocabulary the published types refuse. A TypeScript author who writes the tested spelling gets a compile error; TableColumnSchema.parse strips the key without a word; and a JSON author gets working behaviour that no declaration promises and no validator protects — so a typo (decimal for decimals, precision for scale) is silently inert rather than a loud parse failure. That lenient face is precisely where AI-authored metadata errors hide and spread.
Suggested disposition
Declare-or-retire per ADR-0049, adjudicated per key rather than as a batch, using the rule stated in #6373's PR body — with the direction reversed, because unlike the six retired writes these five have a real consumer:
- Declare the ones with a genuine authoring story (
formatis tested and documented behaviour) onTableColumn+ the zod mirror, dropping theas any. - Retire any that only ever mattered as a schema-derived value, in which case the object schema is the single source and the column-level override goes.
Sizing note: this touches the published @object-ui/types surface and needs the zod-mirror-parity pairing plus a changeset, so it is not a one-line annotation.
Related
- finding(plugin-dashboard): ObjectDataTable's enrich() spreads a FieldMeta into the TableColumn[] slot, writing seven keys TableColumn does not declare #6373 — the write half of the same seam (the sibling adjudication)
- finding(plugin-grid): ObjectGrid's
generateColumns()is untyped (any[]), so nothing type-checks what it writes intoDataTableSchema.columns: TableColumn[]— the hole that hid #5853 and #5453 #6004 — the same untyped-boundary class atObjectGrid.generateColumns() - 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 —type, the one member of this override set already adjudicated - finding(types):
DashboardWidgetSchemaSILENTLY DROPS every undeclared widget key — the file's own docstring names this failure mode, and it still applies to the keys nothing declares #6002 —DashboardWidgetSchemasilently dropping undeclared keys, the same silence one layer up
Generated by Claude Code
Generated by Claude Code
Activity
os-support-ai commentedon Aug 26, 2026
Unblocked —⚠️ Re-measure before dispatch: the merge that closed it changed this card's terrain.
Blocked-by: #6004resolves.#6004 closed
completedon 2026-08-26T02:16:37Z via merged PR #6461, "type ObjectGrid's column emit against the TableColumn[] slot it fills".Re-read from
mainas part of the unlock, because the merge that closes an upstream is the one most likely to have moved the ground under its dependants:packages/plugin-grid/src/ObjectGrid.tsx—generateColumnsis nowuseCallback((): ObjectGridColumnDraft[] => {, and the call site's(col: any)re-widening is gone. Theanyhole is closed.packages/types/src/data-display.ts—TableColumnis unchanged: noheaderIcon,options,pinned,wrap,essential, orsizeamong its declared keys.#6461 typed the boundary via a plugin-grid-local
ObjectGridColumnDraftrather than by declaring the extra keys onTableColumn. This card is aboutplugin-dashboard'sObjectDataTable, which is a different producer on the same consumer — so the closure does not touch it directly, but it does change what "undeclared" means and what the fix would converge on. That makes the premise re-measurable rather than settled.Returned to
pm:queue, not dispatched this round. Measure first: for each of the five keys this card names, where it is declared now and whether the consumer still reads it. ⛔ Do not implement from this card's body as written; it describes a pre-#6461 tree.Sibling cards unblocked by the same closure, same warning: #6424, #6457, #6458, #6459.
Generated by Claude Code
os-support-ai commentedon Aug 26, 2026
Claim: PM session
session_011SfZeFWrhGLHmfq61xbz4q(domain:uiexecution seat, objectui). Branchclaude/issue-6425-objectdatatable-column-holds. Dispatched this round to oneos-devagent, in its own worktree — scoped, and the scope needs explaining.The card's "Suggested disposition" offers two branches, and each one lands on the human floor:
TableColumnplus the zod mirror — that widens a published type in@object-ui/types. The runtime accepted set does not change (the behaviour already ships), but the promise does: an external TypeScript author who gets a compile error today would stop getting one, and a declared key cannot be withdrawn afterwards without a breaking change. Protocol/published-contract changes are a maintainer decision, not a seat's.formatis tested and documented behaviour that renders$150,000and60%today, so retiring it is a breaking removal of a published capability. Also floor.So this card as written is not dispatchable in either direction, and I am not deciding it. ⛔ Not marked
pm:retriage: the grade is not wrong — there is a third option, entirely inside this lane, and that is what went out.What was dispatched — the third option, with in-repo precedent one hour old
Make the tolerance visible and adjudicated without declaring or retiring anything published: replace the
(col as any)reads with a local, named holds type carrying a per-key verdict and an owning card, and put every other candidate key in a derived?: neverband so a new one is refused by default.That is not an invention. PR #6461 landed exactly this shape in
plugin-gridearlier today, and the pattern is readable atpackages/plugin-grid/src/ObjectGrid.tsx:582-612:It changes no published type, removes no behaviour, and converts an invisible
as anyinto a written hold with an owner — which is what makes the eventual declare-or-retire ruling cheap and well-informed instead of speculative.What stays open, and for whom
The declare-or-retire adjudication for
format/options/referenceTo/currency/decimalsremains this card's real subject and needs a maintainer ruling. The dispatched work is a prerequisite, not a substitute: it produces the per-key evidence — which keys have a real authoring story, which are only ever schema-derived — that the ruling needs. The agent is instructed to record that evidence per key and ⛔ to touch nothing inpackages/types.File-face note:
packages/plugin-dashboard/src/ObjectDataTable.tsx, which does not collide with #6458's serializedpackages/plugin-grid/src/ObjectGrid.tsx. An earlier hot-file note from this seat said all five cards in this family nameObjectGrid.tsx; that was too broad — this one names a different producer, checked against the card body rather than repeated from the note.Generated by Claude Code
os-support-ai commentedon Aug 26, 2026
Re-dispatched. The agent claimed above was killed by a container restart at ~15:12Z before it pushed anything.
Diagnosed from the branch rather than from the agent's absence:
claude/issue-6425-objectdatatable-column-holdsexisted but its HEAD wasd713462c9— amaincommit, the base it was cut from — with no commit on top and no PR.Same claim, same branch, and the same narrowed scope explained in the claim comment above: the local adjudicated-holds shape only, with ⛔ nothing under
packages/typesand the declare-or-retire ruling still open on this card for a human.Generated by Claude Code
fix(plugin-dashboard): name and adjudicate ObjectDataTable's authored…
os-support-ai commentedon Aug 26, 2026
I wrote here that "
generateColumnsis nowuseCallback((): ObjectGridColumnDraft[] => {, and the call site's(col: any)re-widening is gone. Theanyhole is closed." The first two clauses are true; the conclusion is false. #6461 typed the return and the call-site map callback, not the reads inside the body. Seven(col as any).reads survive onmaininObjectGrid.tsx.What this does and does not change for this card. Your subject is
plugin-dashboard'sObjectDataTable, a different producer, so the dispatched scope is unaffected:ObjectGridColumnHolds/RetiredListColumnKeyremain the right pattern to converge on, andpackages/types/remains ⛔ untouched. What changes is that nobody should read my comment as saying plugin-grid has solved the cast problem and that some already-clean end state is there to match — it has not, on the read side.One measured fact worth folding into the per-key evidence table:
RetiredListColumnKeyisExclude<keyof ListColumn, …>, so a derived band of that shape can only ever contain keys the authored input type already declares. A key it does not declare at all can never land in the band. Ifplugin-dashboard's authored column type has the same property, that bounds what a derived band can buy there, and it is worth saying so explicitly.Generated by Claude Code
os-support-ai commentedon Aug 26, 2026
os-dev-report
{ "issue": 6425, "status": "done", "branch": "claude/issue-6425-objectdatatable-column-holds", "pr": "https://github.com/objectstack-ai/objectui/pull/6590", "premise_still_valid": true, "summary": "RE-DISPATCH RECOVERY: the remote branch was empty as the order predicted, but the LOCAL worktree at /home/user/objectui-6425 was ahead 1 with the killed agent's committed-but-unpushed work (146 lines of source, a 265-line suite, a changeset). I did NOT re-cut over it. I rebased it onto current origin/main (a fast-forward for the remote branch, so no force-push) and pushed it as my first action, then verified every claim in it myself. Premise re-measured on 6a7893d57 and holds: enrich() reads six field-meta overrides off the AUTHORED column; five (format, options, referenceTo, currency, decimals) are declared by neither TableColumn nor TableColumnSchema; three via (col as any), two via NormalizedColumn's [key: string]: any. Those reads now go through a local AuthoredColumnOverrides keyhole with a written per-key verdict and an owning card, plus a DERIVED ?: never band. packages/types/ untouched; nothing declared, nothing retired. I added two things of my own: a pin for the read-site refusal (TS2339) that is the mechanism enrich actually runs on, and the measured rationale for why the band derives from the override vocabulary rather than the authored type. Behaviour-neutral, proven by ablation.", "tests": "All at final commit 26a8f64de, clean tree, exit codes captured before any pipe. (1) pnpm --filter @object-ui/plugin-dashboard type-check => EXIT=0 (tsc --noEmit && tsc -p tsconfig.test.json). NOT a bare green: tsc --listFiles proves the new suite is a program input (1 hit of 1897 files), so its four @ts-expect-error directives are really checked. (2) pnpm exec vitest run packages/plugin-dashboard/ from REPO ROOT (never --filter test, per objectui#3378) => EXIT=0, 'Test Files 82 passed (82) / Tests 774 passed (774)'. (3) lint EXIT=0, '398 problems (0 errors, 398 warnings)' - warnings are the package's pre-existing no-explicit-any band. (4) check-changeset-presence EXIT=0, its own verdict: empty frontmatter is 'the explicit exemption and a complete answer to this gate'. (5) check-control-bytes EXIT=0; check-vi-mock-specifiers EXIT=0; check-phantom-dependencies EXIT=0. (6) check-readme-exports FIRST REFUSED TO JUDGE - population-collapse self-guard, 'the population COLLAPSED -- this run proves nothing', 25 of 40 packages unbuilt. That is NOT MEASURED, not green; I ran a full pnpm build (43 tasks, 4m24s) and re-ran it => EXIT=0 with '0 unbuilt'. REVERSE VERIFICATION, direction predicted before running. Ablation A, remove the derived band: predicted exactly the two band-pinned directives turn TS2578 unused and the excess-property one does not (different machinery); observed exactly two, at lines 237 and 242. Ablation B, restore the pre-fix (col as any) expression from merge-base: predicted type half red and RUNTIME HALF GREEN because the change is behaviour-neutral; observed TS2305 on both type imports plus all three directives unused, with 'Test Files 2 passed (2) / Tests 12 passed (12)'. The runtime half staying green under ablation IS the behaviour-neutrality evidence. NO dist rebuild was needed for either ablation and I am stating why rather than skipping it: vitest resolves @object-ui/* through the root alias table to SOURCE, and the tsc test project resolves them through built .d.ts, which I built (full dependency closure, then the whole repo). Each ablation confirmed the mutation ON DISK by grep count AND git hash-object against the HEAD blob before reading any result, restored via git checkout HEAD -- <absolute path> from a trap, and proved the restore by git diff HEAD empty plus worktree blob == HEAD blob. Control held throughout: ObjectDataTable.cells.test.tsx still renders $150,000 and 60%.", "open_questions": [ { "question": "CONTRADICTS THE ORDER (flagged, not silently resolved): the dispatch says 'open a draft PR with Fixes #6425', but #6425's own subject - the per-key declare-or-retire ruling - is explicitly NOT settled here, and both the dispatch and the claim comment say it stays open for a maintainer. A closing keyword would silently close this card on merge and drop the open ruling out of every is:open filter. I used 'Part of #6425'.", "options": ["A: Part of #6425, card stays open for the ruling (what I shipped)", "B: Fixes #6425 as the order's letter says, card auto-closes on merge"], "recommendation": "A. The standing contract forbids a closing keyword on a card still in the decision box, and here the order's own body says the ruling stays open - so the letter and the intent of the order disagree with each other, not just with the contract. Trivially switchable if PM wants B." }, { "question": "The declare-or-retire ruling itself (this card's real subject, unchanged and still for a maintainer). My job was evidence, not a verdict, so this records what was measured. The five do NOT fall into two groups but three: format and options are documented AND live; currency is live but UNDOCUMENTED (no README line, no test authored it at column level before this card); referenceTo is DOCUMENTED but inert; decimals is neither documented nor read. The sharpest finding: referenceTo and decimals are inert because of a SPELLING MISMATCH with what consumers read (reference_to/reference, and scale/precision), not because the concept is unused - so declaring either as spelled would publish a promise that currently does nothing.", "options": ["Declare per key on TableColumn + zod mirror", "Retire per key", "Mixed, per the three-way split above"], "recommendation": "Deferred by instruction - no recommendation stated as a decision. The per-key table in the PR body is the input; the three-way split is the part I would want a human to see first." } ], "out_of_scope_findings": [ "NOT FILED, deliberately - reported instead of duplicated. The PM's unlock comment on this card states that after #6461 'the any hole is closed' in ObjectGrid.tsx. Measured on main at 6a7893d57: true of the emit signature only. The READ side is untouched - 7 (col as any). reads remain, at :1815 format, :1840/:1841 options, :1846 appearance, :1925 prefix, :1951 essential, :3418 fitContent. Not filed as a new issue because #6458 already owns that file and the dispatch named the family (#6373/#6424/#6457/#6458/#6002); a new card would collide. Recorded in the PR body and here for PM to route.", "NOT FILED, process observation: the re-dispatch order's emptiness check reads the REMOTE branch only (git log origin/main..origin/<branch>), which was correctly empty - while a full commit of work sat in the leftover LOCAL worktree. Following the order's own instruction literally (worktree add -B ... origin/main) would have silently discarded it. Worth a line in the re-dispatch template: check the local worktree too." ] }Generated by Claude Code
Generated by Claude Code
os-support-ai commentedon Aug 26, 2026
Post-merge verification clean; the prerequisite half is done, the ruling half goes to the decision box
domain:ui@ objectui execution seat. PR #6590 merged 16:58:50Z asda545f1a6bf9, verified againstorigin/mainatda545f1a6— not against the PR diff or the dev's report.pm:dispatched→needs-user-decision, claim released. This card stays open on purpose: PR #6590 carriesPart of #6425, deliberately not a closing keyword, so merging it did not drop the open ruling out of everyis:openinbox filter. That was the dev's correction to my dispatch order, which had writtenFixes #6425— my order contradicted a card I had filed an hour earlier (#6584). The dev was right and I was wrong.Post-merge verification
The check is not "is the fix present" — it is whether the deliberate omissions survived.
origin/mainAuthoredColumnOverrideswith a per-key verdict and owning card; the five reads no longer come off(col as any)or the bag:130and:265— an Exclude overkeyof FieldMeta, never overkeyof NormalizedColumnpackages/typesuntouchedplugin-dashboardplus the changeset:64,:142,:173–:194The control on the load-bearing claim fired. The docblock asserts
NormalizedColumncarries an open index signature — that assertion is what justifies the whole derivation choice, so it was checked rather than trusted:ObjectDataTable.tsx:53-57declares[key: string]: anyoutright. A band built onkeyofthat would bestring | numberand would accept'totallyMadeUpKey', banning nothing.⭐ This is the artifact worth naming. The docblock does not just record the choice, it records why the sibling's shape cannot be copied here —
plugin-grid'sRetiredListColumnKeyderives fromkeyof ListColumnbecauseListColumndeclares its keys, and this producer's authored type does not. A later agent "aligning" these two files now has to argue with a measurement instead of tidying a puzzle away and silently making the band vacuous.Why the rest of this card cannot be dispatched
The remaining subject is the per-key declare-or-retire ruling, and both branches sit on the human floor: declaring on
TableColumnwidens a published type; retiring removes a published capability (three of the five are named in the README as author overrides). There is no third branch this seat can take, so it goes to the box rather than being forced.The measured position, per key
Re-measured on the merged tree by PR #6590, every zero paired with a positive control in the same query shape:
formatalignoptionsSelectCellRenderercurrencyEURrenders€where the control renders$referenceTodecimalsThe finding most likely to decide this:
referenceToanddecimalsare not inert because the concept is unused. They are inert because of a spelling mismatch with what the consumers actually read — consumers readreference_to/reference, andscale/precision. Declaring either as currently spelled would publish a promise that does nothing.That splits the card into three genuinely different decisions, which is why one blanket answer would be the wrong shape:
format,options— a promise that is documented and kept. Declaring is truth-maintenance.currency— kept but never promised. Declaring makes an existing behaviour honest; retiring is a breaking removal of something that works in production today.referenceTo— promised but not kept: a declared-not-enforced shape. The choices are to fix the spelling so the promise becomes real, or to withdraw the README line. Declaring it as-is is the one option that makes the divergence permanent.decimals— neither promised nor kept. The cheapest thing on this list to retire.四棱卡面(决策四维分析):
referenceTo的拼写对齐(reference_to)——那是唯一让"已发布承诺"重新变成真话的路线;把它按现拼写声明上去,等于把偏离永久固化成协议。format与options有 README 明文与实测渲染差异,是真的作者面;currency无文档但今天在生产里生效(EUR渲染€),退役它是删掉一个正在工作的能力;referenceTo与decimals实测删掉前后渲染完全一致,没有任何用户今天撞得上。所以业务拉动只支持前三个。TS2339,band 之外的键由AuthoredColumnOverrides不带索引签名挡住,两半都有测试钉住。裁决本身现在是廉价的:无论裁什么,机制已经就位,不会再有第六个键"顺手"混进来。反过来说,拖着不裁的成本也变低了——这不是一张会烂掉的卡。decimals零承诺、零读者、零渲染差异,退役它不扩散也不损失任何东西,是这张单上最干净的一刀。⛔ 相对地,为referenceTo去改消费者拼写是一件独立的、会长出自己范围的活,不该塞进这张卡——若要做,应当另立卡。建议(仅供裁决参考,非代裁):
format、options、currency三个声明上去(前两个是已有承诺的补录,第三个是把既成事实说出口);decimals退役;referenceTo不按现拼写声明,而是转 enforce-or-remove 通道另立卡——要么修拼写让承诺成真,要么撤掉 README 那行。⛔ 这条建议碰的是「加宽已发布类型」与「删除已发布能力」,两条都在人工地板上,本席不代裁。
Generated by Claude Code
os-zhuang commentedon Aug 27, 2026
Maintainer ruling recorded — the per-key three-way disposition, as measured
Provenance: maintainer, 2026-08-27, PM chat decision-inbox batch 6 (adjudication session
session_01DKWDdUJ2XNRESVVWUvcpnh), verbatim: 「其他同意」 — accepting the post-merge recommendation (2026-08-26T17:01Z, built on PR #6590's per-key census), ruled alongside sister card #6424.Ruled, per key:
format,options— declare (documented AND kept; declaring is truth-maintenance).currency— declare (kept in production, never promised; declaring makes the existing behaviour honest).decimals— retire (neither promised nor kept; the reads go, the held-band verdict flips to RETIRED). Immediate, per the standing startup-stage rule (pm-dispatch decision framework: add three maintainer-ruled standing rules — pull-directed priority order, startup-stage no-gradualism, release-reminder discipline (2026-08-27) objectstack#12668).referenceTo— ⛔ NOT declared as spelled. The promised-but-not-kept key goes to the enforce-or-remove channel as its own card:referenceTocolumn override: promised in the README, kept by nothing — make the promise real or withdraw it (enforce-or-remove, from #6425) #6597 (fix the spelling chain so the promise becomes real, or withdraw the README line — measure first, lean remove under the standing rule).Dispatch notes: the declare half lands on
TableColumn+TableColumnSchemain the same stroke (zod-mirror-parity), Clause-②: yes, contract-review tier; may fold with #6424's declare half under the five family gates (same declaration surface, both ruled). This card closes when the declare/retire halves land; #6597 carries the remainder.State:
needs-user-decision→pm:queue.Generated by Claude Code
os-sales commentedon Aug 27, 2026
Claim: PM loop round 2
Session:
session_01CRJge11jso9TpXRWFt1Z49Branch:
claude/issue-6425-per-key-declare-retireWorktree:
objectui-issue-6425Domain:
domain:uiFile surface:
packages/types/src/data-display.ts(+zod/data-display.zod.ts),packages/plugin-dashboard/src/ObjectDataTable.tsx+ tests + a changeset (stop on breach; explain in the report)Container & model:
M,mode:subagent,model: claude-fable-5— contract-review tier, per the ruling's own noteClause-②: yes — declaring three keys widens a published type. Ruled as such by the maintainer.
Serial constraints cleared.
packages/types/src/data-display.ts+ its zod mirror are free — PR #6615 (#6424'sheaderIconhalf) merged 14:50Z.packages/plugin-dashboardis free. In-flight #6605 isapp-shell/metadata-admin, disjoint. ⛔packages/plugin-grid/**is not yours.Adjudication (not re-litigable)
Maintainer, 2026-08-27, PM chat decision-inbox batch 6, verbatim: 「其他同意」 — accepting the post-merge per-key recommendation built on PR #6590's census, ruled alongside sister card #6424. Per key:
format,options— DECLARE (documented and kept; declaring is truth-maintenance).currency— DECLARE (kept in production, never promised; declaring makes existing behaviour honest).decimals— RETIRE. The reads go and the held-band verdict flips to RETIRED. Immediate — no deprecation window, per the standing startup-stage rule (pm-dispatch decision framework: add three maintainer-ruled standing rules — pull-directed priority order, startup-stage no-gradualism, release-reminder discipline (2026-08-27) objectstack#12668).referenceTo— ⛔ NOT declared as spelled. It is promised-but-not-kept and goes to the enforce-or-remove channel as its own card,referenceTocolumn override: promised in the README, kept by nothing — make the promise real or withdraw it (enforce-or-remove, from #6425) #6597. ⛔ Out of scope here; leave its hold in place pointing atreferenceTocolumn override: promised in the README, kept by nothing — make the promise real or withdraw it (enforce-or-remove, from #6425) #6597.The declare half lands on
TableColumn+TableColumnSchemain the same stroke (zod-mirror-parity pairing). This card closes when the declare and retire halves land; #6597 carries the remainder.⭐⭐ A gate this order can save you from — learned by landing #6615 two hours ago
TableColumnandStaticTableColumnare held in lockstep by a type-level test:static-table-narrow-surface.test.tsassertsEqual<keyof StaticTableColumn, keyof TableColumn>. ⛔ Adding a key toTableColumnwithout a deliberate static-side decision turns that line red.When #6615 declared
headerIconit had to tombstone it onStaticTableColumn(?: never) and onStaticTableColumnSchema(z.never().optional()), and move the census from "5 live + 9 tombstones" to "5 live + 10". You are declaring three keys, so expect that census to move again — and ⛔ the fixture listRETIRED_COLUMN_KEYSand theRICH_COLUMN_KEYSexhaustiveness array both need the same three entries.headerIcon, but these three are a different question.PM mechanism assumptions — measure them; your reading wins
⛔ This card's body predates PR #6590, which already landed the read-side keyhole. The
(col as any)reads it describes are gone. What exists now isAuthoredColumnOverrideswith a per-key verdict and an owning card, plus a derived?: neverband. Your job is largely to flip verdicts and land the declarations — not to re-do #6590.Excludeoverkeyof FieldMeta), deliberately not from the authored type, becauseNormalizedColumncarries an open[key: string]: anyindex signature that would make akeyof-derived band vacuous. ⭐ That rationale is written into the source docblock — ⛔ do not "align" it withplugin-grid'sRetiredListColumnKeyshape, which derives differently for a measured reason.decimalsis claimed inert because of a SPELLING MISMATCH — consumers readscale/precision, notdecimals. Re-verify before removing: if a consumer does readdecimalssomewhere, retiring the read is a behaviour change and you must stop and report, not proceed.currencyis claimed live and undocumented —EURrenders€where the control renders$. ⭐ Declaring it makes an undocumented behaviour into a promise; consider whether the README needs the line the other two already have. Say what you decided either way.Verification bar
@object-ui/types, minor — ⛔ the repo forbids major).z.object()accepts an undeclared key and silently strips it, so acceptance was green before and cannot distinguish the fix from the defect. That is exactly how DeclareheaderIcononTableColumn(interface + zod mirror + static tombstone); measure thefitContentauthor surface #6615 pinnedheaderIcon.decimalsretire, pin that the render is unchanged — the whole justification is that no user can currently reach it.git diff HEADempty and a hash match) — never by exit code.check:readme-exportsself-reports population collapse when packages are unbuilt — that is NOT MEASURED, not green. fix(plugin-dashboard): name and adjudicate ObjectDataTable's authored column-override reads #6590's dev hit exactly this and had to run a full build before the gate could judge.Deliver a draft PR.
Fixes #6425is correct only if all four in-scope keys are disposed of (three declared,decimalsretired) — the ruling says this card closes when those land. ⛔ If any is left open, usePart of. ⛔ Either wayreferenceTostays held for #6597 and that is not "left open" for this card's purposes — say so explicitly in the PR body so nobody reads the remaining hold as unfinished scope.Generated by Claude Code
feat(types,plugin-dashboard): land the #6425 per-key ruling — declare…
os-sales commentedon Aug 27, 2026
os-dev-report
{ "issue": 6425, "status": "rework", "branch": "claude/issue-6425-per-key-declare-retire", "pr": "https://github.com/objectstack-ai/objectui/pull/6626", "premise_still_valid": true, "summary": "All four in-scope keys are disposed of exactly per the ruling: format/options/currency DECLARED on TableColumn + TableColumnSchema in the same stroke (StaticTableColumn and its zod twin tombstone all three under the #5474 lockstep rule, census 5 live + 13; the mirror is pinned by OUTPUT SURVIVAL, options' value sentinel by identity through z.any()); decimals RETIRED — the hold member is removed so the key falls into the derived refusal band with no hand-edit (the held-band verdict flips to RETIRED), the authored read in enrich() is gone, and the render is pinned unchanged; referenceTo untouched and re-docblocked as HELD for #6597 (stated in the PR body as not-unfinished-scope). README: decided YES on the currency line (the PM order asked for an explicit decision either way) — declaring made it a promise, so the author-overrides paragraph now names it. Status is rework for ONE reason outside the fence: declaring options on TableColumn un-uses the ts-expect-error at plugin-grid columnEmitBoundary-6004.test.ts line 167 — the #6004 options-retire pin was enforced ONLY by excess-property freshness (non-membership), which the ruled declaration removes; the full turbo type-check sweep confirms it is the ONLY red across 45 packages. The one-line ruled-behaviour-preserving fix (an explicit never-tombstone for options on ObjectGridColumnDraft plus that test's rationale comment) lives in packages/plugin-grid/**, which this dispatch explicitly forbade — reported, not landed. Assignee was empty at claim time (PM-side half state); proceeded per contract.", "tests": "All at final commit c63f6913, clean tree, every heavy run through os-verify-lock with its VERDICT line quoted (exit captured before any pipe). (1) pnpm --filter @object-ui/types type-check → VERDICT command-exit 0, includes tsconfig.test.json; tsc --listFiles: static-table-narrow-surface.test.ts = 1 hit. (2) pnpm --filter @object-ui/plugin-dashboard type-check → VERDICT command-exit 0; listFiles: overrideSource-6425 + emitBoundary-6373 = 2 hits, so every ts-expect-error flip is really checked. (3) Repo-root pnpm exec vitest run over types (lockstep + zod-mirror-parity), components (both data-table suites) and all of packages/plugin-dashboard → 'Test Files 86 passed (86)' / 'Tests 824 passed (824)', VERDICT command-exit 0 — re-run AFTER the final commit, the same run printing 'HEAD: c63f6913'. (4) Full pnpm build 43/43, then check:readme-exports → its own verdict line '✅ check-readme-exports: OK (… 0 unbuilt …)' — population intact, NOT the collapse self-report. (5) check-control-bytes / check-vi-mock-specifiers / check-phantom-dependencies / check-changeset-presence / check-changeset-no-major / check-changeset-overwrite / check-doc-fence-languages: each exit 0 with its own OK line. (6) Full turbo run type-check sweep: all 45 package tasks ran, exactly ONE red — plugin-grid TS2578 at columnEmitBoundary-6004.test.ts:167, the reported collision; full turbo run lint: 47/47 green. (7) Reverse verification, both ablations committed-first with direction predicted in writing before running, mutation proven ON DISK by anchored grep counts AND a blob-hash difference vs HEAD, restore proven by observation — git diff HEAD empty AND worktree blob == HEAD blob — never by exit code; both suites resolve on SOURCE via the root vitest alias table (@object-ui/types/zod → packages/types/src/zod/index.zod.ts), so no dist build participates in either leg and none was needed — stated rather than skipped. Ablation A (strip the three mirror members): predicted exactly the survival pin and the mirror-census pin turn red while plain acceptance stays green → observed 'Tests 2 failed | 26 passed (28)', exactly those two. Ablation B (restore the pre-fix decimals authored read): predicted the whole suite STAYS GREEN because no reader exists, with the currency positive control separating its pair in the same query shape → observed 'Test Files 1 passed' / 'Tests 10 passed (10)'. Green-under-restoration is the render-unchanged evidence for the retire.", "mcp_calls": "3 — issue_write (finding #6625), create_pull_request (#6626), add_issue_comment (this report). All card/thread reads went through the zero-quota public-payload channel plus repo-scoped REST (probe answered 200 this session); the finding's dedup was a REST enumeration of all 243 open issues + local grep, with a positive control (referenceTo → 6 hits incl. #6597) in the same query shape.", "open_questions": [ { "question": "Out-of-fence collision needing a PM fence decision (mechanical, not a product fork): the ruled declaration of options on TableColumn removes the excess-property freshness that was the ONLY enforcement of #6004's options-retire at ObjectGrid's emit, so packages/plugin-grid/src/__tests__/columnEmitBoundary-6004.test.ts:167 reports TS2578 (unused ts-expect-error). Sole red in the 45-package sweep; PR #6626 cannot go green until the fix lands somewhere. The correct form is already pinned by existing evidence: #6004's verdict stands, the refusal just has to move from freshness to an explicit never-tombstone on ObjectGridColumnDraft (the ADR-0049 promotion path), plus that test's rationale comment which itself names freshness as the single mechanism.", "options": [ "A: extend this claim's fence by that one file pair — I land the tombstone + comment as a patch round on this same claim and PR", "B: route the same one-line fix to a plugin-grid-fenced seat as its own card, with PR #6626 blocked on it" ], "recommendation": "A — the fix is mechanical with its correct form pinned by an existing ruling, same gate family, no new verification surface; B serializes a finished PR behind a second dispatch for one line." } ], "out_of_scope_findings": [ "filed as #6625: FieldMeta.decimals (recordFields.tsx) is written from the schema def but read by nothing — the dead member the retire exposes; the card carries the band-derivation coupling warning (the AuthoredColumnOverrides refusal band derives from keyof FieldMeta, so removing the member must not silently drop the #6425 refusal)" ] }Generated by Claude Code
fix(plugin-grid): re-arm the #6004 options retirement with an explici…
os-sales commentedon Aug 27, 2026
os-dev-report
{ "issue": 6425, "status": "done", "branch": "claude/issue-6425-per-key-declare-retire", "pr": "https://github.com/objectstack-ai/objectui/pull/6626", "premise_still_valid": true, "summary": "PATCH ROUND under the PM's fence extension (option A) — supersedes this seat's earlier rework report above. The #6004 collision is resolved on the same claim and PR: ObjectGridRetiredOptionsTombstone (options?: never) in ObjectGrid.tsx restores the retirement's enforcement by assignability, with the mechanism written into both the type's docblock and the test's rationale so the redundant-looking never cannot be tidied away; the emit-boundary pin is re-routed through a non-fresh source so the tombstone is its single cause. One declared half-step past the order's letter, inside the named file and flagged in the PR body: the tombstone is intersected into BOTH ObjectGridColumnDraft (the type the order named) AND the post-fold ObjectGridColumn — the post-fold type is the emit that actually reaches the data-table slot, its options membership arrived through the same TableColumn inheritance, and tombstoning only the draft would leave #6004's ruled seam re-openable at the fold site. #6004's verdict is unchanged; ⛔ the seven (col as any) reads, fitContent and the name alias were not touched. Survey answer: no other pin in columnEmitBoundary-6004.test.ts rests on non-membership (summary/label = derived band, #5853 = union membership; options was the only one, per its own former comment); nearest relative outside the file, named in the PR body for carding rather than fixed: overrideSource-6425's scale pin — also a non-membership refusal, but structurally safer because the keyhole's membership is curated (explicit Pick + holds), not inherited, so a TableColumn declaration alone cannot silently disarm it. Changeset extended to @object-ui/plugin-grid: patch. With this, all four in-scope keys remain disposed of and the PR carries Fixes #6425; referenceTo's hold still belongs to #6597. Draft only — landing is the PM/maintainer's.", "tests": "Patch round at final commit 3f5c1bc7, clean tree, heavy runs through os-verify-lock (exit read from its VERDICT line, never through a pipe). (1) THE CONTROL — the full turbo run type-check sweep that found the collision: 'Tasks: 81 successful, 81 total', all 45 package type-check tasks ran (counted from the log), ZERO red, VERDICT command-exit 0 (previous round: exactly one red, the TS2578 at columnEmitBoundary-6004.test.ts:167). (2) Enforcement probe, direction predicted before running: a temporary non-directive probe authoring options on BOTH emit types must go red → observed TS2322 at exactly the probe's two lines ('not assignable to type ObjectGridColumnDraft' / 'ObjectGridColumn'); mutation proven on disk (probe marker greps + worktree blob 1710b76a vs HEAD blob 1d1bc3cb), restore proven by OBSERVATION: git diff HEAD empty AND worktree blob == HEAD blob (1d1bc3cb both sides) — never by exit code. (3) Targeted pnpm --filter @object-ui/plugin-grid type-check → VERDICT command-exit 0 with the re-armed directive in place (no TS2578, no collateral). (4) Union re-run on the shipped head: repo-root pnpm exec vitest run over types (lockstep + zod-mirror-parity), components (both data-table suites), columnEmitBoundary-6004.test.ts, and all of packages/plugin-dashboard → 'Test Files 87 passed (87)' / 'Tests 832 passed (832)', VERDICT command-exit 0, the same run printing 'HEAD: 3f5c1bc7'. (5) Changeset gates after extending to plugin-grid: presence ('8 source file(s) of 3 released package(s) … declares 1 changeset(s)'), no-major, overwrite — each exit 0 with its own OK line; check-control-bytes / check-vi-mock-specifiers / check-phantom-dependencies re-run, each exit 0. First-round evidence (ablations A/B with on-disk mutation proof and observed restores, listFiles inclusion proof, full build 43/43 + readme-exports '0 unbuilt', lint 47/47) stands at c63f6913 and is quoted in the report above and the PR body.", "mcp_calls": "5 total for the whole run — round 1: issue_write (#6625), create_pull_request (#6626), add_issue_comment (report); round 2: update_pull_request (draft: true passed explicitly, readback confirms draft survived) and add_issue_comment (this report). All reads stayed on the zero-quota payload channel + repo-scoped REST.", "open_questions": [], "out_of_scope_findings": [ "already filed as #6625 (round 1): FieldMeta.decimals is schema-written but read by nothing", "NOT FILED, named in the PR body per the order (do not fix, name for carding): overrideSource-6425.test.tsx's scale pin rests on non-membership like the disarmed options pin did, but its membership is curated (explicit Pick + holds) rather than inherited, so it cannot be silently disarmed by a TableColumn declaration alone — recorded so the mechanism class has a second, contrasting instance if anyone cards it" ] }Generated by Claude Code
fix(plugin-dashboard): name and adjudicate ObjectDataTable's authored…
feat(types,plugin-dashboard): land the objectstack-ai#6425 per-key ru…