Skip to content

feat(types): declare fitContent on TableColumn (#6424) - #6673

Merged
os-sales merged 1 commit into
mainfrom
claude/issue-6424-declare-fitcontent
Aug 28, 2026
Merged

feat(types): declare fitContent on TableColumn (#6424)#6673
os-sales merged 1 commit into
mainfrom
claude/issue-6424-declare-fitcontent

Conversation

@claude

@claudeclaudeBot commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

Part of #6424

Declares fitContent on TableColumn — the second of this card's two keys, in
the exact shape PR #6615 used for headerIcon. Maintainer ruling 2026-08-28
(live director session, summon #2, batch #4 item 5), presented with
recommendation A; ruling verbatim: 「同意」.

Part of, not Fixes: the card's second item (the
ObjectGridColumnHolds.headerIcon hold) is measured and reported below, not
acted on — removing a hold is an adjudication and the seat rules it. A Fixes
would take that unsettled half into closure.

The declaration, in all three places (#6615's shape)

placeheaderIcon (#6615)fitContent (here)
TableColumn interface?: React.ReactNode?: boolean
TableColumnSchema mirrorz.any().optional()z.boolean().optional()
StaticTableColumn tombstone?: never?: never
StaticTableColumnSchema tombstonez.never().optional()z.never().optional()

One deliberate divergence from the mirrored shape: headerIcon is a rendered
React node, so its mirror is z.any() and its only job is to pass the node
through. fitContent is serializable metadata, so the mirror types it —
z.boolean(). Declaring it as z.any() would have bought acceptance without
validation, the lenient face #5853 removed for type. A non-boolean is now a
loud refusal naming the key, pinned below.

The static side tombstones it under #5474's lockstep rule. Measured, not
assumed: the static renderer's every col.KEY read is header,
accessorKey, className, cellClassName, widthfitContent scores 0
there against a positive control of accessorKey 5 in the same query shape. It
has no auto-width pass to opt out of and no per-cell overflow clamp to lift.

Lockstep counts move 5 live + 13 tombstones -> 5 live + 14.

The authoring surface — what actually changes

The renderer already honoured the key; the published declaration refused it. A
typed author writing { accessorKey: '_actions', fitContent: true } got a
compile error for a key data-table implements, and TableColumnSchema.parse
silently stripped it — while an untyped producer's identical key worked.

Both halves are pinned positively:

  • TS (static-table-narrow-surface.test.ts) — rich accepts, static refuses.
  • zod (data-table-declared-column-keys.test.tsx) — the flag survives
    parse
    (result.data.fitContent === true), not merely "parse succeeded".
    Acceptance cannot pin this: a non-strict z.object() accepted this exact
    input before the declaration, while the flag vanished and the row-actions
    column fell back to the 80px floor that clipped its buttons.

The freshness caveat, handled

At a fresh object literal an undeclared key and a ?: never tombstone are
indistinguishable — both are tsc errors, by excess-property checking. So a
fresh-literal pin measures freshness and declaration together. Each side
therefore also carries a non-fresh route:

  • static: the same value routed through a variable. Extra properties on a
    non-fresh value are structurally fine, so an undeclared key would be
    accepted and the directive would go unused. Only a real tombstone refuses it.
  • rich: the pin is a read (column.fitContent), which compiles only if
    TableColumn declares the key, whatever the value's freshness.
  • plus two type-level pins that touch no object at all:
    Equal< TableColumn['fitContent'], boolean | undefined > and
    Equal< StaticTableColumn['fitContent'], undefined >.

Ablation A — delete the static tombstone only, rebuild, tsc -p tsconfig.test.json (mutation proven on disk: anchored count 1 -> 0, blob hash
d8c5f4db -> a141c9d4):

static-table-narrow-surface.test.ts(381,5): error TS2578: Unused '@ts-expect-error' directive.

Line 381 is the non-fresh directive. Line 360 — the fresh-literal directive
— does not appear: it stayed "used", i.e. a fresh literal cannot tell a
tombstone from mere absence. That is the caveat, measured.

Ablation B — delete the rich declaration only (hash d8c5f4db ->
911c554e): the freshness-free read fires,
static-table-narrow-surface.test.ts(391,45): error TS2339: Property 'fitContent' does not exist on type 'TableColumn',
alongside the key-list exhaustiveness pins at 142/147.

Both ablations restored under a trap … EXIT INT TERM with absolute paths and
an explicit git checkout HEAD -- ABSOLUTE_PATH; restoration proven by
observation — blob hash back to the HEAD blob and git diff HEAD empty.

The two (col as any).fitContent casts — bookkeeping, NOT the fix

Dropped, per the ruling. But this PR does not claim them as the change:

Ablation C — remove the declaration from src and dist (dist proof:
fitContent?: boolean; in packages/types/dist/data-display.d.ts = 0) while
keeping the casts removed, then type-check @object-ui/components:
TC_EXIT=0, green. So col is already any at both sites — widened by the
file's own col: any normalization — and the cast removal is a no-op at
compile time today, independent of the declaration. It becomes load-bearing
only once those render callbacks are typed, which is the standing instrument
gap and is not closed here.

Visible only at the lint layer: as any in data-table.tsx 6 -> 4.

⚠️ Corollary worth carrying: cast count is not a reliable instrument for this
family — the normalization is.

Serial constraint: FENCED (not folded)

The emit-side cast travels with this verdict and needs no new ruling, but it
needs the file free. PR #6670 (#6458) is still open — checked at
implementation time: state: open, merged: false. So
packages/plugin-grid/src/ObjectGrid.tsx is untouched here; this PR's diff
contains no plugin-grid file.

⚠️ Line number correction for whoever picks up the remainder: the card says
ObjectGrid.tsx:3418. Measured on this ref it is :3587
if ((col as any).fitContent) continue;. The card's number is stale, as its
sibling's was.

Second item: the ObjectGridColumnHolds.headerIcon hold — MEASURED, NOT ACTED ON

Reading only; the hold is left exactly as it is.

The hold is redundant, not load-bearing. Three measured facts:

  1. headerIcon IS a member of keyof TableColumn (landed by Declare headerIcon on TableColumn (interface + zod mirror + static tombstone); measure the fitContent author surface #6615), so both
    ObjectGridColumnDraft (via Omit< TableColumn, 'type' >) and
    ObjectGridColumn (via TableColumn) already contribute
    headerIcon?: React.ReactNode.
  2. headerIcon is not a ListColumn member — ListColumn declares
    field label width align hidden sortable resizable wrap type pinned summary
    (0 hits for headerIcon, positive control width 1). So it was never in
    the derived RetiredListColumnKey band, and the holds member was never
    rescuing it from a tombstone.
  3. ObjectGridColumnHolds.headerIcon declares the identical type,
    React.ReactNode.

⇒ The intersection is idempotent (A & A = A): removing the member changes
neither emit type.

No second road. The distinguishing check #5453 demonstrated, applied here:
ObjectGrid writes headerIcon at three sites (all under
schema.showColumnTypeIcons, getTypeIcon(...)) and data-table reads it at
one ({col.headerIcon && …} into the header cell). Forwarded verbatim, never
re-expressed. Contrast pinned, which stays load-bearing on both counts —
it is undeclared by TableColumn (so the holds member is the only thing
declaring it on the emit type) andObjectGrid's own reorder pass consumes
it first and re-expresses it as the sticky className.

⚠️ Nothing goes red if the member is removed — which is exactly the
silent-expiry shape #6425 recorded for options and this seam's own docblock
warns about ("a pin enforced by a key's non-membership silently stops enforcing
the moment the key becomes a member").

Caveat, stated rather than papered over: this reading is a type-algebra
derivation from three mechanically measured facts, not a tsc ablation — the
ablation would require editing the fenced ObjectGrid.tsx. The one-line probe
that would settle it mechanically, for the round that owns the file:
Equal< ObjectGridColumn['headerIcon'], TableColumn['headerIcon'] >.

Also false as written today, and untouched here: the docblock's general claim
that the held keys are "undeclared by TableColumn". #6651 already corrected
the one sentence it was editing; the rest is the seat's to rule.

Gate verdicts (exit code captured before any pipe; each gate's own verdict line)

Heavy work serialized through the shared verify lock; verdicts read from the
lock's own VERDICT line, never a bare $?.

gateexitverdict line
union vitest @ d431b5dd0Test Files 90 passed (90) / Tests 887 passed (887)VERDICT command-exit 0
@object-ui/types build0VERDICT command-exit 0
all-packages build0VERDICT command-exit 0 (needed for the two population gates below)
@object-ui/types type-check0tsc --noEmit && tsc -p tsconfig.examples.json && tsc -p tsconfig.test.jsonVERDICT command-exit 0
components + plugin-grid + plugin-dashboard type-check0Scope: 3 of 47 workspace projects, each DoneVERDICT command-exit 0
lint (@object-ui/types + @object-ui/components)0927 problems (0 errors, 927 warnings)VERDICT command-exit 0
changeset:check0
check:control-bytes0
check:spec-symbols0
check:phantom-deps0
check:self-import0
check:esm-specifiers0
check:shell-escape-residue0
check:doc-types0
check:published-dist0✅ No published package's build output carries tooling material.
check:readme-exports0✅ check-readme-exports: OK (… 0 unbuilt …)

check:readme-exports first returned exit 1 with
❌ the population COLLAPSED -- this run proves nothing / packagesRead: found 14, floor is 25.
That is NOT MEASURED, neither green nor red — so the tree was built as the
gate names and it was re-run; the green above is from the real population (37
of 40 packages, 0 unbuilt, 378 self-imports judged vs 87 in the collapsed run).

@ts-expect-error directives are real enforcement herepackages/types
type-checks its tests via tsconfig.test.json, proven with --listFiles: the
edited test file appears in that compilation (1 hit, among 65 test files), so a
re-widening fails the build on an unused directive.

Declared narrowing

Repo-wide turbo run lint and the full vitest farm were not run locally;
CI runs the farm exactly once regardless. The lint narrowing is a measurement,
not a skip, and all three pieces of evidence are present:

  1. Population from eslint's own config, not a guess: lint is
    turbo run lint, i.e. per-package eslint . units. Only the two edited
    packages' units can move.
  2. File counts from --format json: packages/types 127 files,
    packages/components 415 files — both units run in full, 542 files.
  3. Invariance for untouched files: type-aware linting is not enabled
    (0 matches for projectService/parserOptions in eslint.config.js), so
    this diff cannot move the verdict on any file it did not touch.

Gates skipped with reason: check:vi-mock-specifiers (no vi.mock in diff),
i18n / skills / designer / action / icon / doc-snippets / doc-fences gates (no
such surface in the diff — no skills/** file is touched, so no skills-budget
readings are owed).

Scope

Changeset: @object-ui/typesminor (published-type widening, Clause-2 yes).

Out of scope and untouched: #6004's other held keys
(options / pinned / wrap / essential / size); the name alias, kept
deliberately per #5120; @objectstack/specTableColumn is objectui's own
type, ListColumn is the spec's, and this PR touches only the former.


Generated by Claude Code

`data-table` skips `fitContent` columns in the auto-width pass and renders
them as a `width:1%` + nowrap cell with no overflow clamp, but the published
declaration refused the key: a typed author got a compile error, and
`TableColumnSchema.parse` silently stripped the flag, while an untyped
producer's identical key worked.
Declare it — interface + `TableColumnSchema` mirror (`z.boolean()`, typed
rather than waved through) + the `StaticTableColumn` tombstone under #5474's
lockstep rule — in the shape #6615 used for `headerIcon`. Retire was excluded
by measurement: shipped source authors the key on the row-actions column.
The two `(col as any).fitContent` casts drop with the declaration; they were
redundant at compile time already (`col` is `any` via the file's own
normalization), so that removal is bookkeeping, not the fix.
Part of #6424
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CRJge11jso9TpXRWFt1Z49
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 52 chunks)3237.0 KB3266.6 KB
Main entry chunk (gzip)157.3 KB350 KB
Entry fileindex-JlP1yM48.js
StatusPASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)11.89KB4.50KB
app-shell (runtime-config.js)20.61KB7.35KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)509.24KB115.61KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)173.10KB47.96KB
fields (index.js)239.05KB60.06KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)9.53KB3.38KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)4.64KB1.50KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)1.93KB0.88KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.85KB12.89KB
plugin-charts (index.js)64.66KB18.32KB
plugin-chatbot (index.js)190.33KB45.10KB
plugin-dashboard (index.js)133.43KB34.48KB
plugin-designer (index.js)212.80KB43.15KB
plugin-detail (index.js)245.29KB62.39KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.01KB32.23KB
plugin-gantt (index.js)165.16KB40.33KB
plugin-grid (index.js)201.62KB54.56KB
plugin-kanban (index.js)53.11KB14.62KB
plugin-list (index.js)112.86KB27.54KB
plugin-map (index.js)20.09KB6.62KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)26.72KB7.71KB
plugin-tree (index.js)9.26KB3.13KB
plugin-view (index.js)85.87KB21.12KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)65.97KB21.98KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)2.44KB1.21KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@os-salesClaude

Copy link
Copy Markdown
Collaborator

✅ ACCEPT — flipped ready, auto-merge on

Reviewer: domain:ui @ objectui execution seat, PM session 8ca04858-ea8e-5b85-9182-de59aa49e00c. Part of #6424 is the correct keyword; the card stays open for the remainder, which is ruled and scheduled.

Gate reading: all 29 check runs read, returned count compared against total_count: 29. 26 success, 3 skipped, 0 failing, 0 still running.

⏱ This PR sat green for ~35 minutes before being flipped: this seat hit the GitHub API rate limit at 13:05Z and backed off rather than retrying. The delay is the reviewer's, not the change's. Measured, since it is now twice: the window is ~33 minutes (12:17Z→12:51Z, 13:05Z→13:38Z).

⭐ The best decision here is a deliberate departure from the precedent it was told to follow

The dispatch said to use PR #6615's shape for headerIcon. This PR follows it structurally and diverges on one point, with a reason: headerIcon is a React node, so its mirror is z.any() passthrough; fitContent is serializable metadata, so its mirror types itz.boolean().

declaring it z.any() would have bought acceptance without validation, the lenient face #5853 removed for type

Copying the precedent wholesale would have re-introduced exactly the leniency this card family exists to remove: accepting the key while declining to say what it is. Precedent followed where it applies, departed from where its reason does not.

The cast claim was proven, not taken on the reviewer's word

The order asserted the two (col as any).fitContent sites were redundant and that their removal must not be reported as the fix. Ablation C measured it instead: the declaration was removed from srcanddist — with the dist mutation proven (fitContent?: boolean; in packages/types/dist/data-display.d.ts0, so it reached the artefact components actually resolves) — while keeping the casts removed. components type-check: EXIT=0, green.

⇒ Compile-time no-op, visible only at the lint layer (as any 6→4). Confirmed rather than assumed, and the reasoning behind it (the file's own col: any normalization, not cast counting) now has a measurement under it.

The freshness caveat, observed rather than recited

Ablation A deletes only the static tombstone and produces TS2578: Unused @ts-expect-error directive at the non-fresh directive, while the fresh-literal directive at line 360 does not appear. That is the trap the order named, seen firing: a fresh object literal is refused regardless of tombstones, so a pin routed only through one measures freshness, not the declaration. Each side also carries a non-fresh route plus two object-free type-level Equal pins. Ablation B fires the freshness-free read at TS2339. Three ablations, none uniform red.

The check I did not think to ask for

A published-type widening can silently move a derived tombstone band. Measured: fitContent is a member of neitherFieldMeta (control: format 1) norListColumn (control: width 1), so ObjectDataTable's two Exclude bands and plugin-grid's RetiredListColumnKey are provably unmoved. Lockstep counts 5 live + 13 tombstones → 5 live + 14.

Serial constraint: fenced, correctly, and the line number was corrected

PR #6670 was verified open, merged: false at implementation time, so packages/plugin-grid/src/ObjectGrid.tsx is untouched and this diff carries no plugin-grid file. ⭐ The ruling says the emit-side cast is at ObjectGrid.tsx:3418; measured on today's ref it is :3587.

The remainder — ruled and scheduled, not forgotten

packages/plugin-grid is now free (#6670 landed 12:51:35Z, queue empty). One round lands three things together, dispatched at the next free slot:

  1. the emit-side fitContent cast at :3587 — already ruled, no new ruling needed;
  2. removal of the ObjectGridColumnHolds.headerIcon member, ruled A on finding(components): data-table reads two column keys TableColumn does not declare — headerIcon and fitContent #6424 (redundant since Declare headerIcon on TableColumn (interface + zod mirror + static tombstone); measure the fitContent author surface #6615 declared the key; never in the derived band; no second road — unlike pinned, which stays load-bearing on both counts);
  3. the docblock's general claim that the held keys are "undeclared by TableColumn", false as written today.

⚠️ That round must run Equal<ObjectGridColumn['headerIcon'], TableColumn['headerIcon']> itself. ⛔ It may not inherit this round's reasoning — the dev declared plainly that the hold analysis is a type-algebra derivation from three measured facts, not a tsc ablation, because the ablation would have required editing the fenced file. That caveat is carried, not dropped.


Generated by Claude Code

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@os-sales@claude