Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .changeset/6424-retire-headericon-hold.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
---
---

Internal type cleanup in `plugin-grid`: `ObjectGridColumnHolds` no longer declares
`headerIcon`, which `TableColumn` has declared since objectui#6615, plus the docblock
corrections and pins that go with it. Nothing published moves — `ObjectGridColumnHolds`
is not part of the package entry's exported surface (measured: 0 occurrences in
`dist/index.d.ts`, control `ObjectGridColumnState` 1), and the two emit types are
byte-identical without the member (27 resolved members before and after, against a
positive control that removing `pinned` instead takes them to 26). No published
behaviour changes.
46 changes: 38 additions & 8 deletions packages/plugin-grid/src/ObjectGrid.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -544,10 +544,17 @@ function normalizeColumns(
*
* Verdicts, each with the read-count behind it:
*
* - `headerIcon` — HELD. Live: `data-table.tsx` renders it into the header
* cell (2 reads). Whether `TableColumn` should DECLARE it is objectui#6424's
* call, not this card's; declared here at the seam meanwhile, so the hold is
* visible instead of anonymous.
* - `headerIcon` — DECLARED by `TableColumn`, so NOT held. Live:
* `data-table.tsx` renders it into the header cell (1 render site, 2
* syntactic reads), forwarded verbatim and never re-expressed. It WAS held
* here, on the "undeclared by `TableColumn`" premise; objectui#6615
* declared it and that premise expired with nothing going red at the moment
* of loss. objectui#6424 then removed the hold, MEASURED rather than
* derived: with the member deleted both emit types below are byte-identical
* (27 members, every member's resolved type unchanged), against a positive
* control that deleting `pinned` instead moves them to 26. Pinned in
* `columnHoldsExpiry-6424.test.ts` — which now also carries the claim the
* hold used to carry implicitly, that `TableColumn` DECLARES the key.
* - `pinned` — HELD. Live, and consumed BEFORE the slot: the reorder pass
* below reads it (5 reads in this file) and re-expresses it as the sticky
* `className` that `data-table` actually reads. `data-table` never reads
Expand DownExpand Up@@ -594,11 +601,34 @@ function normalizeColumns(
*/
export type RetiredListColumnKey = Exclude<keyof ListColumn, keyof TableColumn | 'pinned'>;

/** The undeclared-but-live keys this producer holds. See the docblock above. */
/**
* The undeclared-but-live keys this producer holds. See the docblock above.
*
* ⚠️ "Undeclared by `TableColumn`" is this interface's ENTRY CONDITION, and it
* is a claim about ANOTHER package that can stop being true with nothing going
* red here. So it is re-checked per key when the card owning that key closes,
* never inherited: `headerIcon` sat here on exactly that premise until
* objectui#6615 declared it on `TableColumn`, at which point the hold was
* redundant rather than load-bearing, and objectui#6424 removed it.
*
* ⛔ A key whose ONLY declaration on the emit types is this interface is not in
* that position — deleting it deletes the key from the emit. `pinned` is that
* key today, which is why the two verdicts differ.
*/
export interface ObjectGridColumnHolds {
/** HELD, objectui#6424 — `data-table` renders it; `TableColumn` does not declare it. */
headerIcon?: React.ReactNode;
/** HELD — consumed by this file's own reorder pass before the array reaches the slot. */
/**
* HELD, load-bearing on BOTH counts — the pair that has to hold for a hold to
* be real, and the contrast that made `headerIcon`'s removal safe:
*
* 1. `TableColumn` does NOT declare it, and `RetiredListColumnKey` carves it
* out of the derived tombstone band, so this member is its ONLY
* declaration on both emit types. Deleting it drops the key from them
* (measured: 27 members → 26).
* 2. It has a live consumer BEFORE the slot — this file's own reorder pass
* reads it and re-expresses it as the sticky `className` that
* `data-table` actually reads. That is the "second road" the emit rule
* demands before a key may be retired, and `wrap` failed it.
*/
pinned?: 'left' | 'right';
}

Expand Down
27 changes: 15 additions & 12 deletions packages/plugin-grid/src/__tests__/columnEmitBoundary-6004.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -139,19 +139,22 @@ describe('objectui#6004 — the emit boundary is an instrument, not a decoration
});

/**
* The HELD keys — the two `ObjectGridColumnHolds` still declares, which the
* emit type must therefore ACCEPT: a tombstone set that swallowed either
* Both keys the emit type must ACCEPT: a tombstone set that swallowed either
* would be a behaviour change wearing a type change's clothes.
*
* ⚠️ Their two holds no longer rest on the same footing, and that is
* objectui#6424's to settle rather than this pin's. `pinned` is undeclared by
* `TableColumn` and has a measured live reader — `ObjectGrid`'s own reorder
* pass, which consumes it before the array reaches the slot. `headerIcon` was
* held on the same "undeclared by `TableColumn`" premise, and that premise has
* since expired: `TableColumn` DOES declare it today
* (`packages/types/src/data-display.ts`), so its entry in
* `ObjectGridColumnHolds` is redundant rather than load-bearing. This test
* pins only that both are accepted, which is true either way.
* ⚠️ They no longer arrive by the same route, and objectui#6424 settled
* that. `pinned` is still HELD: undeclared by `TableColumn`, so
* `ObjectGridColumnHolds` is its only declaration on the emit types, AND it
* has a measured live reader — `ObjectGrid`'s own reorder pass, which
* consumes it before the array reaches the slot. `headerIcon` reaches the
* emit types through `TableColumn` itself: it was held on the same
* "undeclared by `TableColumn`" premise, objectui#6615 declared it
* (`packages/types/src/data-display.ts`) and the premise expired with nothing
* going red, so objectui#6424 removed the hold after measuring that both emit
* types are unchanged without it. So `ObjectGridColumnHolds` declares ONE key
* today, not two. This test pins only that both keys are accepted, which is
* true either way; the routes themselves are pinned in
* `columnHoldsExpiry-6424.test.ts`.
*
* ⚠️ There were three until objectui#5453. `wrap` was the third, and it was
* held for a reason that was never "a live reader": the card that owned it
Expand All@@ -161,7 +164,7 @@ describe('objectui#6004 — the emit boundary is an instrument, not a decoration
* open card is a hold with no evidence under it yet, and it should be
* re-checked the moment the card closes rather than aging into a fact.
*/
it('accepts the two held keys — headerIcon, pinned', () => {
it('accepts headerIcon (via TableColumn) and pinned (via the hold)', () => {
const held = { header: 'H', accessorKey: 'a', headerIcon: null, pinned: 'left' as const };
const accepted: ObjectGridColumnDraft = held;
expect(accepted.pinned).toBe('left');
Expand Down
156 changes: 156 additions & 0 deletions packages/plugin-grid/src/__tests__/columnHoldsExpiry-6424.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
/**
* ObjectUI
* Copyright (c) 2024-present ObjectStack Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

/**
* objectui#6424 — `ObjectGridColumnHolds` after the `headerIcon` hold came out.
*
* ## What actually changed, and what this file is for
*
* `ObjectGridColumnHolds` exists for keys `data-table` READS that `TableColumn`
* does NOT declare. `headerIcon` was one until objectui#6615 declared it on
* `TableColumn`; from that moment the hold was redundant rather than
* load-bearing, and NOTHING WENT RED — the same silent expiry objectui#6425
* recorded for `options`. objectui#6424 removed the member.
*
* ⚠️ THE REMOVAL MOVED A LIVENESS CLAIM RATHER THAN DELETING ONE. While the
* hold existed, the emit types declared `headerIcon` on their own account; now
* they get it ONLY from `TableColumn`. So if `TableColumn` ever stops declaring
* it, the key silently vanishes from both emit types and `ObjectGrid` goes on
* writing it at three sites into a slot that no longer admits it. That is a
* NEW exposure created by the removal, and the first assertion below is what
* covers it — it is the guard the deleted member used to provide implicitly.
*
* ## Why these pins are compile-time
*
* The claim is about TYPES, and a rendering test is blind to it: the grid
* renders exactly as correctly with every type in this file deleted. What can
* fail is a compile, and `tsc -p tsconfig.test.json` reads this file (the
* package build's own program excludes `__tests__`). The `expect()` calls are
* there so vitest reports the file at all; the assertions that matter are the
* `Expect<...>` aliases, which are erased at runtime.
*
* ## Every zero here has a positive control in the same query shape
*
* A `false` from a probe that can only answer `false` measures nothing, so each
* negative claim is paired with a sibling that must come back the other way.
*/
import { describe, it, expect } from 'vitest';
import type { ListColumn, TableColumn } from '@object-ui/types';
import type { ObjectGridColumn, ObjectGridColumnDraft, RetiredListColumnKey } from '../ObjectGrid';

/** Compile-time equality, exact in both directions (not mutual assignability). */
type Equal<X, Y> = (<T>() => T extends X ? 1 : 2) extends (<T>() => T extends Y ? 1 : 2)
? true
: false;

type Has<K extends string, T> = K extends keyof T ? true : false;
type Expect<T extends true> = T;

describe('objectui#6424 — the holds interface after the headerIcon expiry', () => {
/**
* ⭐ THE GUARD THE REMOVED MEMBER USED TO PROVIDE.
*
* `ObjectGrid` writes `headerIcon` at three sites (all under
* `schema.showColumnTypeIcons`, via `getTypeIcon`) and `data-table` renders
* it. With the hold gone, `TableColumn` is the ONLY thing declaring the key
* on the emit types. Reverting objectui#6615 would otherwise drop it from
* both types in silence; this is what makes that loud.
*/
it('`TableColumn` declares `headerIcon` — the emit types now depend on it alone', () => {
type _Declared = Expect<Has<'headerIcon', TableColumn>>;
// Controls: the probe can answer both ways in this exact shape.
type _CtrlPresent = Expect<Has<'width', TableColumn>>;
type _CtrlAbsent = Expect<Has<'zzNotAKeyZZ', TableColumn> extends false ? true : false>;
expect(true).toBe(true);
});

/**
* ⭐ THE RULED PROBE (objectui#6424, the seat's Option A).
*
* The removal was ruled on a type-algebra derivation — the holds member
* declared the identical type, so the intersection was idempotent. This runs
* it as a compile-time probe instead of inheriting it: the emit types' member
* and `TableColumn`'s must be THE SAME TYPE, not merely compatible, which is
* why this is `Equal` and not `extends`.
*/
it('`headerIcon` on both emit types is identical to `TableColumn`s', () => {
type _Column = Expect<Equal<ObjectGridColumn['headerIcon'], TableColumn['headerIcon']>>;
type _Draft = Expect<Equal<ObjectGridColumnDraft['headerIcon'], TableColumn['headerIcon']>>;
// Controls on the instrument itself: `Equal` must be able to say false here,
// otherwise the two lines above are green for no reason.
type _CtrlTrue = Expect<Equal<TableColumn['headerIcon'], TableColumn['headerIcon']>>;
type _CtrlFalse = Expect<
Equal<ObjectGridColumn['headerIcon'], TableColumn['width']> extends false ? true : false
>;
expect(true).toBe(true);
});

/**
* The hold was never rescuing `headerIcon` from the DERIVED tombstone band.
* `RetiredListColumnKey` is `Exclude<keyof ListColumn, keyof TableColumn | 'pinned'>`,
* so a key that is not a `ListColumn` member can never enter it — and
* `headerIcon` is not one. Without this, "removing the hold is safe" would
* have to assume the band, and the band is exactly where a removed hold would
* bite (as `?: never`) instead of merely disappearing.
*/
it('`headerIcon` is not a `ListColumn` member, so it was never in the derived band', () => {
type _NotListColumn = Expect<Has<'headerIcon', ListColumn> extends false ? true : false>;
type _NotInBand = Expect<
'headerIcon' extends RetiredListColumnKey ? false : true
>;
// Controls, same shapes: a real `ListColumn` member, and a key that IS in the band.
type _CtrlListColumn = Expect<Has<'width', ListColumn>>;
type _CtrlInBand = Expect<'wrap' extends RetiredListColumnKey ? true : false>;
expect(true).toBe(true);
});

/**
* ⭐ THE CONTRAST THAT MAKES THE TWO VERDICTS DIFFERENT — and the reason
* `pinned` must NOT follow `headerIcon` out.
*
* `pinned` fails the expiry test on both counts: `TableColumn` does not
* declare it, and `RetiredListColumnKey` explicitly carves it out of the
* Exclude, so `ObjectGridColumnHolds` is its ONLY declaration on the emit
* types. (Measured: deleting the member takes both emit types from 27
* resolved members to 26, `pinned` gone. Deleting `headerIcon` left them
* byte-identical — that pair of ablations is what this verdict rests on.)
*
* It also has the second road `wrap` lacked: this file's reorder pass reads
* it and re-expresses it as the sticky `className` `data-table` reads.
*/
it('`pinned` is still load-bearing — undeclared by `TableColumn` and outside the band', () => {
type _Undeclared = Expect<Has<'pinned', TableColumn> extends false ? true : false>;
type _CarvedOut = Expect<'pinned' extends RetiredListColumnKey ? false : true>;
// Control: `pinned` IS a `ListColumn` member, so its absence from the band
// is the carve-out doing work — not non-membership, which is `headerIcon`s
// reason. Two different routes to the same `false`, and they must not be
// conflated.
type _CtrlIsListColumnMember = Expect<Has<'pinned', ListColumn>>;
// …and it is therefore still a member of both emit types.
type _StillOnColumn = Expect<Has<'pinned', ObjectGridColumn>>;
type _StillOnDraft = Expect<Has<'pinned', ObjectGridColumnDraft>>;
expect(true).toBe(true);
});

/**
* The behaviour that must survive the removal: `ObjectGrid` writes
* `headerIcon` into the slot at three sites, so the emit types still have to
* ACCEPT it. Routed through a non-fresh value, like the other emit-boundary
* pins — a fresh literal would be refused or admitted by excess-property
* freshness, which is a different question from membership.
*/
it('the emit types still accept a written `headerIcon`', () => {
const emitted: { header: string; accessorKey: string; headerIcon?: unknown } = {
header: 'Name',
accessorKey: 'name',
headerIcon: null,
};
const draft: ObjectGridColumnDraft = emitted as ObjectGridColumnDraft;
expect(draft.accessorKey).toBe('name');
});
});
Loading