From 7d9607b7f7a7055fa64bab78445d9d6badd03dc1 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 29 Aug 2026 20:37:00 +0000 Subject: [PATCH] docs(types): declare the gantt dependencyField deprecation on both faces MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `ObjectGanttSchema` declares two spellings of one thing and reads them with a `||`. `dependenciesField` is the spec's key; the singular `dependencyField` has no spec counterpart, and until #6051 it was the only one declared — so the published type taught the non-spec spelling and hid the canonical one. Mark the singular `@deprecated` on both published faces (the TS interface and its zod mirror), naming the plural as canonical, following the idiom `KanbanConfig`'s pre-#2231 aliases already carry. Nothing is removed: the `||` read stays, both spellings still parse, and two pins hold that in place. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01CRJge11jso9TpXRWFt1Z49 --- .../6470-gantt-dependency-field-deprecated.md | 49 ++++ packages/plugin-gantt/README.md | 7 +- .../src/ObjectGantt.dependencyAlias.test.tsx | 176 ++++++++++++++ ...-dependency-field-deprecated-alias.test.ts | 226 ++++++++++++++++++ packages/types/src/objectql.ts | 23 +- packages/types/src/zod/objectql.zod.ts | 9 +- 6 files changed, 486 insertions(+), 4 deletions(-) create mode 100644 .changeset/6470-gantt-dependency-field-deprecated.md create mode 100644 packages/plugin-gantt/src/ObjectGantt.dependencyAlias.test.tsx create mode 100644 packages/types/src/__tests__/gantt-dependency-field-deprecated-alias.test.ts diff --git a/.changeset/6470-gantt-dependency-field-deprecated.md b/.changeset/6470-gantt-dependency-field-deprecated.md new file mode 100644 index 0000000000..74346edef4 --- /dev/null +++ b/.changeset/6470-gantt-dependency-field-deprecated.md @@ -0,0 +1,49 @@ +--- +'@object-ui/types': patch +'@object-ui/plugin-gantt': patch +--- + +`ObjectGanttSchema.dependencyField` is now marked `@deprecated` on both published +declaration faces, naming `dependenciesField` as the canonical spelling +(objectui#6470). Nothing is removed and nothing is rejected that was accepted +before. + +**What the two spellings were.** `ObjectGanttSchema` declares both, and +`getGanttConfig`'s flat branch reads them with a `||`: +`dependenciesField: schema.dependenciesField || schema.dependencyField`. +`dependenciesField` is the spec's key (`@objectstack/spec` +`GanttConfigSchema.dependenciesField`); the singular `dependencyField` has NO +spec counterpart — zero occurrences across `packages/spec/src`, measured against +a live positive control on the plural. Until objectui#6051 declared the plural, +the singular was the ONLY dependencies spelling this interface carried, so for +the whole time the alias existed the published type taught the non-spec key and +hid the canonical one. + +**What was missing was the ranking, not the behaviour.** The two were declared as +equals: nothing on either face said which one to author, so a reader — including +an AI writing metadata, which is the reader this project optimises for — had a +coin flip between a spec key and pre-spec vocabulary. The marker turns that coin +flip into a fact the type itself carries, and the zod mirror's description makes +it readable at runtime as well as in an editor. + +This adopts the idiom already ruled for this exact shape rather than inventing a +second one: `KanbanConfig`'s pre-#2231 aliases (`groupField`, `cardFields`) carry +`/** @deprecated legacy alias for the spec's X */` plus +`.describe('Deprecated alias for X')`, and `dependencyField` now reads the same +way. + +**⛔ Not a removal, deliberately.** Deleting the alias — or narrowing the +renderer's `||` — would break every author who wrote the singular and narrow the +accept set of a published surface. That is a maintainer decision on a future +enforce-or-remove card once the deprecation has sat a release, and it is +explicitly excluded here. Two pins hold the line in both directions: +`packages/types/src/__tests__/gantt-dependency-field-deprecated-alias.test.ts` +fails if the marker goes missing AND if the alias stops being declared or +accepted, and `packages/plugin-gantt/src/ObjectGantt.dependencyAlias.test.tsx` +fails if the `||` limb is dropped — the two spellings must keep resolving to the +same config, with the canonical one winning when both carry a value. + +`packages/plugin-gantt/README.md`'s `ObjectGanttSchema` example authored the +singular; it was the only in-repo site that did, and it now authors the plural +with the alias named as legacy. No runtime code, fixture, example app or catalog +schema authored it. diff --git a/packages/plugin-gantt/README.md b/packages/plugin-gantt/README.md index 6c651d3376..0dd9191aba 100644 --- a/packages/plugin-gantt/README.md +++ b/packages/plugin-gantt/README.md @@ -670,10 +670,15 @@ const gantt: ObjectGanttSchema = { startDateField: 'start_date', endDateField: 'end_date', progressField: 'completion_percentage', - dependencyField: 'dependent_task_ids', + dependenciesField: 'dependent_task_ids', }; ``` +`dependenciesField` (plural) is the spec's spelling and the one to author. The +singular `dependencyField` is a `@deprecated` legacy alias: `ObjectGantt` still +reads it (`dependenciesField || dependencyField`), so existing metadata keeps +working, but new metadata should not use it. + For a list view served under the `gantt` view type, the same configuration is a `gantt` block on `ListViewSchema` (typed by `GanttConfig`, also from `@object-ui/types`) rather than top-level keys — `ObjectGantt` reads either diff --git a/packages/plugin-gantt/src/ObjectGantt.dependencyAlias.test.tsx b/packages/plugin-gantt/src/ObjectGantt.dependencyAlias.test.tsx new file mode 100644 index 0000000000..fd9ab6cccf --- /dev/null +++ b/packages/plugin-gantt/src/ObjectGantt.dependencyAlias.test.tsx @@ -0,0 +1,176 @@ +/** + * 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. + */ + +/** + * Behaviour pin — deprecating `dependencyField` changed NOTHING at the read site + * (objectui#6470). + * + * ## What this file is for + * + * objectui#6470 marks the singular `dependencyField` `@deprecated` on both + * published declaration faces (`packages/types/src/objectql.ts` and its zod + * mirror), naming the spec's `dependenciesField` as canonical. The marker is + * documentation; `getGanttConfig`'s flat branch still reads + * `dependenciesField || dependencyField` and every author who wrote the singular + * must keep rendering exactly as before. + * + * ⛔ Removal was explicitly excluded from that card and deferred to a future + * enforce-or-remove decision. This suite is what makes the exclusion mechanical: + * drop the `|| schema.dependencyField` limb and the first two cases below go red + * naming the break, rather than a published surface silently losing an accepted + * spelling. + * + * ## Why the two spellings name DIFFERENT record fields here + * + * A fixture where both spellings point at the same field can only prove that + * SOMETHING resolved; it cannot show WHICH limb of the `||` produced it, so it + * would stay green with either limb deleted. Pointing them at different fields + * (`legacy_preds` vs `canonical_preds`, with values that cannot be confused) + * makes the resolved config visible in the rendered tasks — the same technique + * `ObjectGantt.blockPrecedence.test.tsx` uses for the block/flat flip. + * + * The "same config" half the card asks for is then pinned directly, by the + * third case: the two spellings aimed at ONE field produce byte-identical task + * dependencies. + * + * ## The observable + * + * `dependenciesField` is not passed to `GanttView` as a config value — it is + * consumed on the way in (`dependencies: normalizeDependencies(record[dep])`) + * and again as the `autoSchedule` / `onDependencyCreate` switches. Both are + * observed: the per-task dependency ids say WHICH field was read, and + * `autoSchedule` says the config resolved to a truthy field name at all. + */ +import React from 'react'; +import { render, waitFor } from '@testing-library/react'; +import { describe, it, expect, vi } from 'vitest'; +import { ObjectGantt } from './ObjectGantt'; + +vi.mock('./GanttView', () => ({ + GanttView: ({ tasks, autoSchedule }: any) => ( +
t.dependencies.join('+')).join('|')} + data-autoschedule={String(!!autoSchedule)} + /> + ), +})); + +vi.mock('./ResourceWorkload', () => ({ + ResourceWorkload: ({ tasks }: any) =>
, +})); + +/** + * One record carrying BOTH candidate fields, with values that cannot be + * confused: the legacy field's ids start `L`, the canonical field's start `C`. + */ +const INLINE = [ + { + id: '1', + name: 'Alpha', + start: '2024-01-01', + end: '2024-01-05', + legacy_preds: 'L1,L2', + canonical_preds: 'C1,C2', + shared_preds: 'S1,S2', + }, +]; + +const DATES = { startDateField: 'start', endDateField: 'end', titleField: 'name' }; + +async function rendered(extra: Record, objectName: string) { + const { container } = render( + , + ); + const el = () => container.querySelector('[data-testid="gantt-view"]') as HTMLElement; + await waitFor(() => expect(el()?.getAttribute('data-count')).toBe('1')); + return { + deps: el().getAttribute('data-deps'), + autoSchedule: el().getAttribute('data-autoschedule'), + }; +} + +describe('both dependency spellings still resolve to the same config (objectui#6470)', () => { + it('the DEPRECATED singular is still read', async () => { + // ⛔ The removal guard. Deleting `|| schema.dependencyField` from + // `getGanttConfig` turns this red — every author who wrote the singular + // loses their dependency links, and `autoSchedule` silently switches off + // with them. + const { deps, autoSchedule } = await rendered( + { dependencyField: 'legacy_preds' }, + 'alias_singular', + ); + expect(deps).toBe('L1+L2'); + expect(autoSchedule).toBe('true'); + }); + + it('the CANONICAL plural is read', async () => { + // The counter-probe: the assertion above must be about the singular limb, + // not about dependencies working at all. + const { deps, autoSchedule } = await rendered( + { dependenciesField: 'canonical_preds' }, + 'alias_plural', + ); + expect(deps).toBe('C1+C2'); + expect(autoSchedule).toBe('true'); + }); + + it('aimed at ONE field, the two spellings produce an identical result', async () => { + // The card's acceptance criterion, stated directly: the deprecation is a + // marker, so the singular and the plural are interchangeable at the read + // site. Compared value-to-value rather than each against a literal, so this + // stays true however `normalizeDependencies` evolves. + const viaLegacy = await rendered({ dependencyField: 'shared_preds' }, 'alias_same_a'); + const viaCanonical = await rendered({ dependenciesField: 'shared_preds' }, 'alias_same_b'); + expect(viaLegacy).toEqual(viaCanonical); + expect(viaLegacy.deps).toBe('S1+S2'); + }); + + it('the canonical spelling WINS when a node carries both', async () => { + // Precedence, and the reason the deprecation marker is truthful: an author + // migrating key-by-key gets the canonical value, not a merge and not the + // legacy one. + const { deps } = await rendered( + { dependenciesField: 'canonical_preds', dependencyField: 'legacy_preds' }, + 'alias_both', + ); + expect(deps).toBe('C1+C2'); + }); + + it('an EMPTY canonical value falls through to the alias — a property of `||`', async () => { + // Recorded, not designed. `dependenciesField: ''` is falsy, so the `||` + // reaches the singular; `??` would not. Pinned so that a future rewrite of + // that expression is a decision rather than an accident — and so the + // enforce-or-remove card inherits the real accept semantics instead of + // re-deriving them. + const { deps } = await rendered( + { dependenciesField: '', dependencyField: 'legacy_preds' }, + 'alias_empty_canonical', + ); + expect(deps).toBe('L1+L2'); + }); + + it('neither spelling authored: no dependencies, and auto-scheduling stays OFF', async () => { + // Non-vacuity for every case above — without this, a harness that always + // reported the same string would pass them all. + const { deps, autoSchedule } = await rendered({}, 'alias_none'); + expect(deps).toBe(''); + expect(autoSchedule).toBe('false'); + }); +}); diff --git a/packages/types/src/__tests__/gantt-dependency-field-deprecated-alias.test.ts b/packages/types/src/__tests__/gantt-dependency-field-deprecated-alias.test.ts new file mode 100644 index 0000000000..865ad3df40 --- /dev/null +++ b/packages/types/src/__tests__/gantt-dependency-field-deprecated-alias.test.ts @@ -0,0 +1,226 @@ +/** + * 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. + */ + +/** + * Deprecation pin — `ObjectGanttSchema.dependencyField` is a DECLARED legacy + * alias, and both published faces now say so (objectui#6470). + * + * ## What was wrong, and what it was NOT + * + * `ObjectGanttSchema` declares two spellings of one thing. `dependenciesField` + * is the spec's (`@objectstack/spec` `GanttConfigSchema.dependenciesField`); + * `dependencyField`, the singular, has no spec counterpart at all — zero + * occurrences across `packages/spec/src`, against a live positive control on the + * plural. Until objectui#6051 declared the plural, the singular was the ONLY + * dependencies spelling on this interface, so for the whole time the alias + * existed the published type taught the non-spec key and hid the canonical one. + * + * The card was originally filed as commandment #0.1 — a lenient renderer + * fallback to an UNDECLARED key. Triage's dissent measured that and it is not + * this shape: the singular is declared outright, documented, and deliberately + * retained, with the rationale written next to the canonical key. The honest + * description is a types-vs-spec divergence, and the remedy re-scoped to match + * (triage ruling 2026-08-26): mark it, keep it. + * + * ## Why the marker is the whole deliverable, and removal is not + * + * Two spellings declared as EQUALS is what a reader could not resolve. Nothing + * on either face said which one to write, so an author — or an AI writing + * metadata, which is the reader this project optimises for — had a coin flip + * between a spec key and a pre-spec alias. A machine-readable marker turns the + * coin flip into a fact the type itself carries. + * + * ⛔ Deleting the alias is deliberately NOT in this card. It would narrow the + * accept set of a published surface — a breaking removal of a published + * capability, which is a maintainer call on a future enforce-or-remove card once + * the deprecation has sat a release. That is why the pins below assert the alias + * is still THERE and still ACCEPTED as loudly as they assert it is tagged. + * + * ## The two failure modes, which are opposite + * + * 1. the tag is missing or nameless — the deprecation is cosmetic, no reader + * learns which spelling is canonical, and the card bought nothing; + * 2. the alias is gone — the deprecation quietly became the removal that was + * explicitly excluded, breaking every author who wrote the singular. + * + * A test asserting only (1) passes on a tree where the alias was deleted; one + * asserting only (2) passes on a tree where the tag never shipped. Both halves + * are pinned. + * + * ## Why the TS face is read as SOURCE TEXT and the zod face at RUNTIME + * + * They carry the statement in different places, and each is checked where it + * actually lives. A TS `@deprecated` exists only in the JSDoc — it reaches + * consumers through `dist/*.d.ts` and their editor, and there is no runtime + * value to interrogate, so the source is read (the idiom + * `plugin-component-input-deprecation.test.ts` established, for the same reason + * and with the same anchoring discipline). The zod mirror's `.describe()` IS a + * runtime string on the parsed schema, so it is read off the schema object — + * which is what makes the mirror half of this deprecation machine-readable + * rather than merely human-readable. + */ + +import { readFileSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; +import { describe, it, expect } from 'vitest'; +import { ObjectGanttSchema } from '../zod/objectql.zod.js'; +import type { ObjectGanttSchema as ObjectGanttSchemaTS } from '../objectql.js'; + +const OBJECTQL_SRC = readFileSync( + fileURLToPath(new URL('../objectql.ts', import.meta.url)), + 'utf8', +); + +/** The alias member, exactly as `objectql.ts` spells it. */ +const ALIAS = 'dependencyField?: string;'; +/** The canonical member, exactly as `objectql.ts` spells it. */ +const CANONICAL = "dependenciesField?: GanttConfig['dependenciesField'];"; + +/** + * The JSDoc block immediately preceding a member, or `null` when the member is + * not preceded by one. + * + * Anchored to the member rather than searching the file for `@deprecated`: + * `objectql.ts` carries fifteen unrelated deprecations, so a file-wide search + * would go green on any of them and say nothing about this key. + */ +function docBlockBefore(member: string): string | null { + const at = OBJECTQL_SRC.indexOf(member); + if (at === -1) return null; + const before = OBJECTQL_SRC.slice(0, at); + const close = before.lastIndexOf('*/'); + // Only whitespace may sit between the block and the member, otherwise the + // block belongs to some earlier member and says nothing about this one. + if (close === -1 || before.slice(close + 2).trim() !== '') return null; + const open = before.lastIndexOf('/**', close); + if (open === -1) return null; + return before.slice(open, close + 2); +} + +describe('ObjectGanttSchema.dependencyField — the TS face declares the deprecation', () => { + it('the member is unique in the file, so the anchor cannot drift onto a twin', () => { + // Non-vacuity for every `docBlockBefore(ALIAS)` below: `indexOf` takes the + // FIRST hit, so a second declaration of the same member would let this suite + // report on the wrong one. + expect(OBJECTQL_SRC.split(ALIAS)).toHaveLength(2); + }); + + it('is still declared — deprecating is not deleting', () => { + expect(OBJECTQL_SRC).toContain(ALIAS); + }); + + it('carries a JSDoc block attached to the member itself', () => { + expect(docBlockBefore(ALIAS)).not.toBeNull(); + }); + + it('tags that block `@deprecated`', () => { + expect(docBlockBefore(ALIAS)).toContain('@deprecated'); + }); + + it('names the canonical spelling, so the marker is actionable', () => { + // A bare `@deprecated` tells an author to stop, not what to write instead — + // and "what to write instead" is the entire content of this card. + expect(docBlockBefore(ALIAS)).toContain('dependenciesField'); + }); + + it('says the alias is still READ, so nobody reads the tag as a removal notice', () => { + expect(docBlockBefore(ALIAS)).toContain('dependenciesField || dependencyField'); + }); + + it('does NOT deprecate the canonical key — the control', () => { + // The tag must be attached to ONE member. If a future edit widened the + // block, or moved it, the spec's own key would read as deprecated and this + // card would have inverted its own result. + const canonicalDoc = docBlockBefore(CANONICAL); + expect(canonicalDoc).not.toBeNull(); + expect(canonicalDoc).not.toContain('@deprecated '); + expect(canonicalDoc).toContain('CANONICAL'); + }); +}); + +describe('ObjectGanttSchema.dependencyField — the zod mirror declares it too', () => { + it('is still declared on the mirror', () => { + expect(Object.keys(ObjectGanttSchema.shape)).toContain('dependencyField'); + }); + + it('its description states the deprecation and names the replacement', () => { + // Runtime-readable, which is the half a JSDoc cannot give: this string is on + // the schema object, so a gate or a generated form can ask. + const description = ObjectGanttSchema.shape.dependencyField.description; + expect(description).toBeTruthy(); + expect(description).toMatch(/deprecated/i); + expect(description).toContain('dependenciesField'); + }); + + it('follows the ruled idiom rather than a third spelling of "deprecated"', () => { + // `KanbanConfig`'s pre-#2231 aliases are the shape this adopts. If that + // convention is ever restated differently, this pin makes the divergence a + // decision instead of a drift. + expect(ObjectGanttSchema.shape.dependencyField.description).toMatch( + /^Deprecated alias for /, + ); + }); + + it('the canonical key is NOT described as deprecated — the control', () => { + const canonical = ObjectGanttSchema.shape.dependenciesField.description; + expect(canonical ?? '').not.toMatch(/deprecated/i); + }); +}); + +describe('ObjectGanttSchema.dependencyField — still ACCEPTED (removal was excluded)', () => { + const MINIMAL = { + type: 'object-gantt', + objectName: 'task', + startDateField: 'start', + endDateField: 'end', + } as const; + + it('parses green when the singular is authored alone', () => { + const result = ObjectGanttSchema.safeParse({ ...MINIMAL, dependencyField: 'preds' }); + expect(result.success ? null : result.error.issues).toBe(null); + }); + + it('parses green when both spellings are authored together', () => { + const result = ObjectGanttSchema.safeParse({ + ...MINIMAL, + dependencyField: 'preds', + dependenciesField: 'predecessors', + }); + expect(result.success ? null : result.error.issues).toBe(null); + }); + + it('still TYPE-validates the alias — a deprecated key is not an unchecked one', () => { + // Counter-probe for the two greens above: it must be the KEY that is + // accepted, not everything. `dependencyField: 5` was refused before this + // card and is refused after it. + const result = ObjectGanttSchema.safeParse({ ...MINIMAL, dependencyField: 5 }); + expect(result.success).toBe(false); + if (result.success) return; + expect(result.error.issues.find((i) => i.path[0] === 'dependencyField')).toBeTruthy(); + }); +}); + +describe('ObjectGanttSchema.dependencyField — the TS member is unchanged', () => { + it('is still declared `string | undefined`', () => { + // Real enforcement: `tsconfig.test.json` compiles this file. If the member + // were deleted, it would resolve to `any` through `BaseSchema`'s + // `[key: string]: any` index signature, the assignment below would start + // succeeding, and the directive would fail the build as an unused + // `@ts-expect-error` (TS2578) — NAMING the removal. That is the pin against + // the deprecation quietly becoming the excluded removal. + + // @ts-expect-error — `dependencyField` is declared `string | undefined`. + const bad: ObjectGanttSchemaTS['dependencyField'] = 5; + + // Counter-probe: without this, a declaration narrowed to `never` would + // satisfy the directive above while breaking every existing author. + const good: ObjectGanttSchemaTS['dependencyField'] = 'dependent_task_ids'; + + expect([bad, good]).toHaveLength(2); + }); +}); diff --git a/packages/types/src/objectql.ts b/packages/types/src/objectql.ts index 0c39c3eef6..7ab4ca9363 100644 --- a/packages/types/src/objectql.ts +++ b/packages/types/src/objectql.ts @@ -2285,7 +2285,25 @@ export interface ObjectGanttSchema extends BaseSchema { endDateField?: string; /** Field for task title/name */ titleField?: string; - /** Field for task dependencies */ + /** + * Field for task dependencies. + * + * @deprecated Legacy alias — author {@link ObjectGanttSchema.dependenciesField} + * instead. The plural is the spec's spelling (`@objectstack/spec` + * `GanttConfigSchema.dependenciesField`); this singular has NO spec + * counterpart. It is pre-spec objectui vocabulary, and until objectui#6051 it + * was the ONLY dependencies spelling this interface declared — so for the whole + * time the alias existed the published type taught the non-spec key and hid the + * canonical one. That is what this tag exists to correct: the two were declared + * as equals, and a reader had no way to learn which one to write. + * + * Still accepted, deliberately. `getGanttConfig`'s flat branch reads + * `dependenciesField || dependencyField`, so metadata already written against + * this key renders unchanged and the canonical key wins wherever both carry a + * value. Deprecating is NOT removing: dropping the alias would narrow the + * accept set of a published surface, which is a separate enforce-or-remove + * decision (objectui#6470) and not something this marker takes. + */ dependencyField?: string; /** Field for progress (0-100) */ progressField?: string; @@ -2440,7 +2458,8 @@ export interface ObjectGanttSchema extends BaseSchema { * Record field holding this task's predecessors. The CANONICAL spelling — the * flat branch reads `dependenciesField || dependencyField`, so the singular * {@link ObjectGanttSchema.dependencyField} above stays accepted as the legacy - * alias and this one wins. + * alias and this one wins. That alias is `@deprecated` (objectui#6470): still + * read, no longer taught. */ dependenciesField?: GanttConfig['dependenciesField']; /** diff --git a/packages/types/src/zod/objectql.zod.ts b/packages/types/src/zod/objectql.zod.ts index a69a6db28a..c683993f19 100644 --- a/packages/types/src/zod/objectql.zod.ts +++ b/packages/types/src/zod/objectql.zod.ts @@ -666,7 +666,14 @@ export const ObjectGanttSchema = BaseSchema.extend({ startDateField: z.string().optional().describe('Start date field'), endDateField: z.string().optional().describe('End date field'), titleField: z.string().optional().describe('Title field'), - dependencyField: z.string().optional().describe('Dependency field'), + // The legacy singular alias. Kept accepted — `getGanttConfig`'s flat branch + // reads `dependenciesField || dependencyField` — but no longer declared as an + // equal of the canonical key: same treatment `KanbanConfig` above gives + // `groupField`/`cardFields`, so this adopts the ruled idiom rather than a + // second spelling of "deprecated". The canonical `dependenciesField` is + // declared below, BY REFERENCE to the spec (objectui#6470). + /** @deprecated legacy alias for the spec's `dependenciesField` */ + dependencyField: z.string().optional().describe('Deprecated alias for dependenciesField'), progressField: z.string().optional().describe('Progress field'), // DERIVED from the spec's `GanttConfigSchema.shape.viewMode` (an optional // enum, deliberately WITHOUT a default) so the member list cannot drift