Skip to content
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
This repository was archived by the owner on Nov 4, 2026. It is now read-only.

finding(plugin-dashboard): ObjectDataTable honours five undeclared authored column keys as field-meta overrides #6425

Closed

Description

@claude

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 (format is tested and documented behaviour) on TableColumn + the zod mirror, dropping the as 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


Generated by Claude Code


Generated by Claude Code

Activity

  1. os-support-ai commented on Aug 26, 2026

    @os-support-ai
    Collaborator

    Unblocked — Blocked-by: #6004 resolves. ⚠️ Re-measure before dispatch: the merge that closed it changed this card's terrain.

    #6004 closed completed on 2026-08-26T02:16:37Z via merged PR #6461, "type ObjectGrid's column emit against the TableColumn[] slot it fills".

    Re-read from main as 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 — generateColumns is now useCallback((): ObjectGridColumnDraft[] => {, and the call site's (col: any) re-widening is gone. The any hole is closed.
    • packages/types/src/data-display.ts — TableColumn is unchanged: no headerIcon, options, pinned, wrap, essential, or size among its declared keys.

    #6461 typed the boundary via a plugin-grid-local ObjectGridColumnDraft rather than by declaring the extra keys on TableColumn. This card is about plugin-dashboard's ObjectDataTable, 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

  2. os-support-ai commented on Aug 26, 2026

    @os-support-ai
    Collaborator

    Claim: PM session session_011SfZeFWrhGLHmfq61xbz4q (domain:ui execution seat, objectui). Branch claude/issue-6425-objectdatatable-column-holds. Dispatched this round to one os-dev agent, in its own worktree — scoped, and the scope needs explaining.

    ⚠️ Both dispositions this card proposes are above this seat

    The card's "Suggested disposition" offers two branches, and each one lands on the human floor:

    • Declare the keys on TableColumn plus 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.
    • Retire any of them — format is tested and documented behaviour that renders $150,000 and 60% 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 ?: never band so a new one is refused by default.

    That is not an invention. PR #6461 landed exactly this shape in plugin-grid earlier today, and the pattern is readable at packages/plugin-grid/src/ObjectGrid.tsx:582-612:

    export type RetiredListColumnKey = Exclude<keyof ListColumn, keyof TableColumn | 'wrap' | 'pinned'>;
    
    export interface ObjectGridColumnHolds {
      /** HELD, objectui#6424 — `data-table` renders it; `TableColumn` does not declare it. */
      headerIcon?: React.ReactNode;
      …
    }

    It changes no published type, removes no behaviour, and converts an invisible as any into 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 / decimals remains 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 in packages/types.

    File-face note: packages/plugin-dashboard/src/ObjectDataTable.tsx, which does not collide with #6458's serialized packages/plugin-grid/src/ObjectGrid.tsx. An earlier hot-file note from this seat said all five cards in this family name ObjectGrid.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

  3. os-support-ai commented on Aug 26, 2026

    @os-support-ai
    Collaborator

    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-holds existed but its HEAD was d713462c9 — a main commit, 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/types and the declare-or-retire ruling still open on this card for a human.


    Generated by Claude Code

  4. os-support-ai commented on Aug 26, 2026

    @os-support-ai
    Collaborator

    ⚠️ Correction: this seat's unlock comment above contains a false claim. Full measurement and proof on #6458 (correction comment). The agent in flight on this card has been messaged directly.

    I wrote here that "generateColumns is now useCallback((): ObjectGridColumnDraft[] => {, and the call site's (col: any) re-widening is gone. The any hole 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 on main in ObjectGrid.tsx.

    What this does and does not change for this card. Your subject is plugin-dashboard's ObjectDataTable, a different producer, so the dispatched scope is unaffected: ObjectGridColumnHolds / RetiredListColumnKey remain the right pattern to converge on, and packages/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: RetiredListColumnKey is Exclude<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. If plugin-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

  5. os-support-ai commented on Aug 26, 2026

    @os-support-ai
    Collaborator

    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

  6. os-support-ai commented on Aug 26, 2026

    @os-support-ai
    Collaborator

    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 as da545f1a6bf9, verified against origin/main at da545f1a6 — 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 carries Part of #6425, deliberately not a closing keyword, so merging it did not drop the open ruling out of every is:open inbox filter. That was the dev's correction to my dispatch order, which had written Fixes #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.

    what had to hold reading on origin/main
    the read-side keyhole is in place AuthoredColumnOverrides with a per-key verdict and owning card; the five reads no longer come off (col as any) or the bag
    ⛔ the band derives from the override vocabulary, not the authored type :130 and :265 — an Exclude over keyof FieldMeta, never over keyof NormalizedColumn
    ⛔ packages/types untouched nothing published declared, nothing retired; 3 files, all in plugin-dashboard plus the changeset
    the anti-vacuity reason is written into the source, not left in the PR :64, :142, :173–:194

    The control on the load-bearing claim fired. The docblock asserts NormalizedColumn carries an open index signature — that assertion is what justifies the whole derivation choice, so it was checked rather than trusted: ObjectDataTable.tsx:53-57 declares [key: string]: any outright. A band built on keyof that would be string | number and 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's RetiredListColumnKey derives from keyof ListColumn because ListColumn declares 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 TableColumn widens 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:

    key published promise? live reader? if the key is absent
    format yes — README, plus three authored example columns yes — currency / percent / date branches, and it decides align render changes
    options yes — README yes — SelectCellRenderer render changes
    currency no — undocumented yes — EUR renders € where the control renders $ render changes
    referenceTo yes — README no identical render
    decimals no no identical render

    The finding most likely to decide this: referenceTo and decimals are not inert because the concept is unused. They are inert because of a spelling mismatch with what the consumers actually read — consumers read reference_to / reference, and scale / 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 实测删掉前后渲染完全一致,没有任何用户今天撞得上。所以业务拉动只支持前三个。
    • ③ 防 AI 犯错:本 PR 已经把读侧关死——未裁决的键在读点就是 TS2339,band 之外的键由 AuthoredColumnOverrides 不带索引签名挡住,两半都有测试钉住。裁决本身现在是廉价的:无论裁什么,机制已经就位,不会再有第六个键"顺手"混进来。反过来说,拖着不裁的成本也变低了——这不是一张会烂掉的卡。
    • ④ 创业阶段不扩散需求:decimals 零承诺、零读者、零渲染差异,退役它不扩散也不损失任何东西,是这张单上最干净的一刀。⛔ 相对地,为 referenceTo 去改消费者拼写是一件独立的、会长出自己范围的活,不该塞进这张卡——若要做,应当另立卡。

    建议(仅供裁决参考,非代裁):format、options、currency 三个声明上去(前两个是已有承诺的补录,第三个是把既成事实说出口);decimals 退役;referenceTo 不按现拼写声明,而是转 enforce-or-remove 通道另立卡——要么修拼写让承诺成真,要么撤掉 README 那行。

    ⛔ 这条建议碰的是「加宽已发布类型」与「删除已发布能力」,两条都在人工地板上,本席不代裁。


    Generated by Claude Code

  7. removed their assignment
    on Aug 26, 2026
  8. os-zhuang commented on Aug 27, 2026

    @os-zhuang
    Contributor

    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:

    Dispatch notes: the declare half lands on TableColumn + TableColumnSchema in 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

  9. self-assigned this
    on Aug 27, 2026
  10. os-sales commented on Aug 27, 2026

    @os-sales
    Collaborator

    Claim: PM loop round 2
    Session: session_01CRJge11jso9TpXRWFt1Z49
    Branch: claude/issue-6425-per-key-declare-retire
    Worktree: objectui-issue-6425
    Domain: domain:ui
    File 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 note
    Clause-②: 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's headerIcon half) merged 14:50Z. packages/plugin-dashboard is free. In-flight #6605 is app-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:

    The declare half lands on TableColumn + TableColumnSchema in 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

    TableColumn and StaticTableColumn are held in lockstep by a type-level test: static-table-narrow-surface.test.ts asserts Equal<keyof StaticTableColumn, keyof TableColumn>. ⛔ Adding a key to TableColumn without a deliberate static-side decision turns that line red.

    When #6615 declared headerIcon it had to tombstone it on StaticTableColumn (?: never) and on StaticTableColumnSchema (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 list RETIRED_COLUMN_KEYS and the RICH_COLUMN_KEYS exhaustiveness array both need the same three entries.

    ⚠️ Verify this yourself on the merged ref rather than taking my word — I am describing a file I have read reports about, not one I have opened. If the static-side decision for any of these three should be live rather than tombstoned, that is your call to argue; the static renderer never read 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 is AuthoredColumnOverrides with a per-key verdict and an owning card, plus a derived ?: never band. Your job is largely to flip verdicts and land the declarations — not to re-do #6590.

    1. The band derives from the override vocabulary (Exclude over keyof FieldMeta), deliberately not from the authored type, because NormalizedColumn carries an open [key: string]: any index signature that would make a keyof-derived band vacuous. ⭐ That rationale is written into the source docblock — ⛔ do not "align" it with plugin-grid's RetiredListColumnKey shape, which derives differently for a measured reason.
    2. decimals is claimed inert because of a SPELLING MISMATCH — consumers read scale / precision, not decimals. Re-verify before removing: if a consumer does read decimals somewhere, retiring the read is a behaviour change and you must stop and report, not proceed.
    3. currency is claimed live and undocumented — EUR renders € 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

    • The declare half needs the zod-mirror-parity pairing and a changeset (@object-ui/types, minor — ⛔ the repo forbids major).
    • ⭐ For the mirror, pin output survival, not parse acceptance: a non-strict 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 Declare headerIcon on TableColumn (interface + zod mirror + static tombstone); measure the fitContent author surface #6615 pinned headerIcon.
    • For the decimals retire, pin that the render is unchanged — the whole justification is that no user can currently reach it.
    • Reverse-verify: commit first, predict the direction, prove the mutation on disk (anchored grep counts plus a blob-hash difference), observe, prove the restore by observation (git diff HEAD empty and a hash match) — never by exit code.
    • A zero needs a positive control in the same query shape.
    • ⚠️ check:readme-exports self-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 #6425 is correct only if all four in-scope keys are disposed of (three declared, decimals retired) — the ruling says this card closes when those land. ⛔ If any is left open, use Part of. ⛔ Either way referenceTo stays 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

  11. added a commit that references this issue on Aug 27, 2026
  12. os-sales commented on Aug 27, 2026

    @os-sales
    Collaborator

    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

  13. added a commit that references this issue on Aug 27, 2026
  14. os-sales commented on Aug 27, 2026

    @os-sales
    Collaborator

    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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatpm:dispatched

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions