Skip to content

feat(types,plugin-gantt): declare the ten gantt keys ObjectGantt reads through a cast - #6053

Merged
yinlianghui merged 2 commits into
mainfrom
claude/issue-5903-objectgantt-declared-keys
Aug 24, 2026
Merged

feat(types,plugin-gantt): declare the ten gantt keys ObjectGantt reads through a cast#6053
yinlianghui merged 2 commits into
mainfrom
claude/issue-5903-objectgantt-declared-keys

Conversation

@yinlianghui

@yinlianghuiyinlianghui commented Aug 24, 2026

Copy link
Copy Markdown
Collaborator

Fixes#5903

Verified at fb876485d (final commit; every gate result below was re-run on that
tree). Merge-base b0de7a85c.

What landed

ObjectGanttSchema declares the ten keys ObjectGantt reads — skipWeekends,
holidays, persistLayout, viewName, navigation, markers, criticalPath,
showBaselines, readOnly, mobileReadOnly — on both its TS declaration
and its zod mirror, at the same requiredness (all optional). All twelve
(schema as any).K read sites are gone.

navigation is taken from the spec's NavigationConfigSchema by reference, the
same way ObjectGridSchema.navigation is, rather than restated. The other nine
have no spec member (GanttConfigSchema models none of them), so they are
declared as objectui's own display extension — the standing timeSegments
already has on GanttConfig.

ObjectGanttProps.schema is retyped ObjectGridSchemaObjectGanttSchema.
This is the load-bearing part of the change, not a tidy-up. The ten keys are not
grid keys, so under the old prop type dropping the casts would have left every
read landing on BaseSchema's index signature — the same invisibility in
different syntax. The grid-style { gantt: { … } } block is unaffected:
getGanttConfig reads it through that same index signature exactly as before,
and the registered renderer (index.tsx) passes schema: any, so no runtime
shape is turned away. There is no other in-repo caller.

Per-key verdict — declare, delete, or already declared

Two outcomes were admissible per key. No read was deleted: all eleven are
live, and PR #5900's README (landed) documents all eleven as genuine working
features. Read sites are cited at the merge-base line numbers.

KeyVerdictRead siteEvidence it is live
skipWeekendsdeclare:891builds workingCalendar; GanttView.workaxis.test.tsx drives it
holidaysdeclare:892same workingCalendar; same test
persistLayoutdeclare:1023gates persistLayoutKey; ObjectGantt.persistfilters.test.tsx, ObjectGantt.test.tsx:530
viewNamedeclare:1025scopes persistLayoutKey
navigationdeclare:1132feeds useNavigationOverlay; drawer-vs-page mode
markersdeclare:1471GanttViewmarkers prop → resolvedMarkers
criticalPathdeclare:1474seeds React.useState(criticalPathDefault) (GanttView:2864)
showBaselinesdeclare:1477read at GanttView:2996, :3848
readOnlydeclare:1478, :1523disables writes; locks drawer (ObjectGantt.drawerlock.test.tsx:84)
mobileReadOnlydeclare:1479effectiveReadOnly (GanttView:752)
labelalready declared — cast dropped only:1494BaseSchema.label: string | I18nLabel

Delta against the card, key by key. The card's count of 11 holds exactly on
merge-base b0de7a85c — same eleven keys, same line numbers, nothing added or
removed by today's packages/types traffic. Neither #6003 (dashboard widget
vocabulary) nor #6012 (IconSchema's glyph key rename) touches any of them. One
correction: the card lists label as undeclared, and it is notBaseSchema
has carried it since #4580. So this card declares ten, not eleven. label is
pinned in the new test anyway, because "already declared" is the claim that would
silently stop being true.

Enumeration method, and its counter-probe

The card's reproduce command is a regex over (schema as X).KEY. That is blind
by construction to (schema as any)['foo'], to a destructure off an any-typed
local, and to a cast spanning lines — three spellings of one defect with no
shared pattern. So the read set was re-derived with an AST walk instead: an
expression counts as schema-rooted after stripping parenthesis / as /
satisfies / non-null wrappers, with local aliases chased to a fixed point;
property access, string-literal element access, and object-binding patterns are
all collected.

Result on merge-base: 47 distinct top-level keys read off schema, of which
11 are cast reads — precisely the card's eleven.

Counter-probe (the method must find keys that are legitimately declared, or it
is blind):
it finds objectName, viewMode, startDateField, endDateField,
titleField, dependencyField, progressField — the seven already on
ObjectGanttSchema — and label, already on BaseSchema. After the change the
same walk reports zero cast reads and the same 47 keys.

The other 36 keys are read bare through the index signature rather than through a
cast; that is a different mechanism and out of this card's fence, filed as #6051.

Reverse verification — the signal this card exists to create

Directions were predicted before running. Both mutations were proved on disk in
both directions (removed text → 0, surviving anchor → 1), run under
trap … EXIT INT TERM, and git diff HEAD --stat is empty afterwards.

Ablation A — restore ObjectGanttSchema's TS declaration to its pre-card state.
packages/types tests import source-relative, but plugin-gantt resolves
@object-ui/types through exportsdist, so the types package was rebuilt
between mutation and reading, and the mutation was confirmed to have reached
dist/objectql.d.ts (skipWeekends count 1 → 0).

  • Predicted RED on @object-ui/types type-check. Measured: exit 2,
    tenerror TS2578: Unused '@ts-expect-error' directive. — one per declared
    key, at the ten pinned lines. Mechanism: with the declaration gone each member
    resolves to any through the index signature, the wrong-typed assignment
    starts succeeding, and the directive becomes unused. Each directive names its
    key, so the failure is self-describing.
  • Predicted GREEN on @object-ui/plugin-gantt type-check, and stated as a
    non-signal up front. Measured: exit 0. Under the index signature a read
    site can never be the detector — schema.readOnly type-checks as any either
    way. This is why the pin lives in packages/types, compiled by
    tsconfig.test.json, and not at the read site.

Ablation B — restore only the zod mirror, TS declaration intact. This tests
the dispatch's mechanism assumption 3 ("declaring on the TS side alone will turn
zod-mirror-parity red").

  • That assumption does not hold for this pair, measured. The parity test:
    5 passed, exit 0. Its compile-time half (the LedgerMismatch assignment,
    which runs under tsc -p tsconfig.test.json, not vitest): exit 0. Both green
    with the mirror missing all ten keys.
  • Cause, read off the construction rather than guessed — the drift type
    NarrowerThanDeclared maps over the intersection of the mirror's mirrored keys
    with keyof the declaration, so a key present in the declaration but absent
    from the mirror's .shape
    drops out of the comparison entirely. The ratchet
    detects a mirror that is narrower on a shared key; it cannot detect a mirror
    that is missing one. The reverse direction is blind too, and for a second
    reason: ObjectGanttSchema's declaration carries BaseSchema's index
    signature, so keyof it is string | number and every indexed lookup resolves
    to any, which is assignable to anything the mirror could say.
  • Consequence: no KnownDrift entry was ever on the table — the ledger is
    unchanged, still 17 entries on this merge-base (the "13" figure describes the
    post-fix(types): widen seven zod mirrors the renderer already implements (#5927 group A) #6032 state, which has not landed). The new pin caught the missing mirror
    instead: 2 failed | 7 passed, exit 1.

Counter-probe on enforcement — measured, not assumed, exactly as the dispatch
asked.
BaseSchema is .passthrough() with an index signature (#5155's
structural ceiling), so the two halves differ and the test pins both:

Both halves also have positive counter-probes so neither can pass vacuously: a
well-typed value is accepted on every key (runtime), and the correctly-typed
literal assigns on every key (compile time) — without which a declaration
narrowed to never would satisfy all ten directives.

Gates — by name, with exit codes

Exit codes captured before any pipe. Every reading below is from fb876485d.

GateExit
pnpm --filter @object-ui/types type-check (runs tsc --noEmit, then the examples and test projects)0
pnpm --filter @object-ui/plugin-gantt type-check (runs tsc --noEmit, then the test project)0
pnpm exec vitest run packages/types/src packages/plugin-gantt/src (root form)0 — 101 files, 1006 tests passed
pnpm --filter '...@object-ui/types' build (downstream)0 — 91 packages
pnpm --filter '...@object-ui/types' type-check (downstream)0 — 42 packages
eslint . in packages/types0 — 0 errors, 244 warnings (all pre-existing no-explicit-any)
eslint . in packages/plugin-gantt0 — 0 errors, 251 warnings
check:control-bytes0
check:spec-symbols0
check:doc-types0
check:self-import, check:phantom-deps, lint:coverage0
check-changeset-presence, check-changeset-no-major0
regenerate-known-schema-types --check0

The script name is echoed in each type-check log, so a zero-match silent pass
cannot read as green. The prefix form '...@object-ui/types' is the
downstream consumer direction — the correct one for a contract tightening.
@object-ui/react-runtime was built separately first; it is not in the prefix
closure but packages/components needs its dist. Per the dispatch that is a
known local gotcha, not a finding.

Serial constraints

packages/types was taken after #5927's PR #6032 was confirmed not merged
into origin/main (checked by commit-log scan at branch time). The predicted
conflict on zod-mirror-parity.test.tsdoes not arise: that file is not in
this diff at all — declaring on both sides at constant requiredness leaves the
ratchet at zero drift for this pair, so there was nothing to edit there. No
sibling-claimed file was touched: packages/test-support and the parity test
files (#5872), RecordDetailView.tsx (#5835), form.tsx (#6010) and
plugin-detail/src/renderers/__tests__/ (#5808) are all untouched.

Out-of-scope findings (filed, not fixed here)

⛔ Class card #4631 is not reopened — triage said doing this per-package
specimen does not reopen the class, and nothing here widens past ObjectGantt.

⛔ Draft on purpose: the PM lands this. Not marked ready, not enqueued, no
auto-merge.


Generated by Claude Code

…s through a cast (#5903)
All ten were read as `(schema as any).K` in ObjectGantt.tsx — real, working,
README-documented features that nothing connected to a declaration: not tsc, not
the zod mirror, not the designer registry inputs.
- `ObjectGanttSchema` (TS) and its zod mirror gain the same ten keys at the same
requiredness (all optional), so `zod-mirror-parity` stays at zero drift for the
pair and no `KnownDrift` entry is added. `navigation` derives from the spec's
`NavigationConfigSchema` by reference.
- `ObjectGanttProps.schema` is retyped `ObjectGridSchema` -> `ObjectGanttSchema`.
Without that, dropping the casts would leave the reads on `BaseSchema`'s index
signature — the same invisibility in different syntax.
- `label`, the eleventh reported key, was already declared on `BaseSchema`; only
its cast is dropped.
- New declaration pin `gantt-declared-keys.test.ts` compiles under
`tsconfig.test.json`, so removing a declaration fails the build naming the key.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CSoz9uGhaaSgiq3hshtN7L
…-describing
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CSoz9uGhaaSgiq3hshtN7L
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 52 chunks)3220.0 KB3990.2 KB
Main entry chunk (gzip)153.6 KB350 KB
Entry fileindex-gOJnnBHd.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)10.38KB3.90KB
app-shell (runtime-config.js)18.10KB6.51KB
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)505.15KB114.53KB
core (index.js)4.92KB1.97KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)165.30KB45.79KB
fields (index.js)238.40KB59.89KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)23.13KB7.63KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)7.77KB3.13KB
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.62KB12.83KB
plugin-charts (index.js)64.66KB18.32KB
plugin-chatbot (index.js)188.21KB44.67KB
plugin-dashboard (index.js)133.35KB34.44KB
plugin-designer (index.js)212.30KB42.80KB
plugin-detail (index.js)244.12KB61.87KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)125.63KB30.64KB
plugin-gantt (index.js)164.15KB39.88KB
plugin-grid (index.js)200.79KB54.26KB
plugin-kanban (index.js)52.93KB14.60KB
plugin-list (index.js)111.86KB27.22KB
plugin-map (index.js)20.11KB6.64KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.49KB11.93KB
plugin-timeline (index.js)26.49KB7.59KB
plugin-tree (index.js)8.50KB2.88KB
plugin-view (index.js)84.57KB20.74KB
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)52.40KB17.45KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)1.35KB0.70KB
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)12.13KB3.65KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)7.54KB2.63KB
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)0.20KB0.18KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.87KB0.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.49KB2.14KB
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

@yinlianghui
yinlianghui marked this pull request as ready for review August 24, 2026 15:09
@yinlianghui
yinlianghui added this pull request to the merge queueAug 24, 2026
Merged via the queue into main with commit 00f3eb5Aug 24, 2026
23 checks passed
@yinlianghui
yinlianghui deleted the claude/issue-5903-objectgantt-declared-keys branch August 24, 2026 15:21
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.

ObjectGantt reads 11 top-level schema keys that ObjectGanttSchema does not declare — all via as any, so nothing connects read to declaration

2 participants

@yinlianghui@claude