diff --git a/.changeset/6475-gantt-block-face-declared.md b/.changeset/6475-gantt-block-face-declared.md new file mode 100644 index 0000000000..20c9f2ec69 --- /dev/null +++ b/.changeset/6475-gantt-block-face-declared.md @@ -0,0 +1,52 @@ +--- +'@object-ui/types': minor +--- + +**`ObjectGanttSchema` now declares the `gantt` BLOCK face — and the spec's +required trio enforces at validate/check time.** + +The `gantt` nested-block spelling of a gantt config (`{ type: 'object-gantt', +gantt: { … } }`) had **no mirror entry at all**: it rode through +`BaseSchema`'s `.passthrough()` entirely unvalidated, and the published +TypeScript never taught the shape either — an author writing `gantt: { … }` +got no completion, no type checking, no error on a misspelt member. It was +the 28th and last of the keys `getGanttConfig` (`plugin-gantt/src/ObjectGantt.tsx`) +reads off the schema that objectui#6051 (PR #6472) did not declare — severed +into its own card because, unlike the other 27, declaring it changes what a +published tool refuses. + +Both faces now declare it: + +```ts +// packages/types/src/objectql.ts +gantt?: GanttConfig; + +// packages/types/src/zod/objectql.zod.ts +gantt: SpecGanttConfigSchema.extend(GanttConfigExtensionFields).optional(), +``` + +`GanttConfig` derives from the spec's `GanttConfigSchema`, which has required +`startDateField`, `endDateField` and `titleField` since rc.6. + +**What the CLI now refuses that it accepted before:** `ObjectGanttSchema` is a +member of `AnyComponentSchema`, so it reaches `safeValidateSchema` and +therefore the CLI's `validate` and `check` commands. A `gantt` block missing +any of the three required fields — previously accepted silently — is now +**refused**, naming the missing field. A block carrying all three, or a +schema with no `gantt` block at all, is accepted exactly as before. + +**This is a `declared = enforced` restoration, not new requiredness.** +`getGanttConfig`'s block branch already fed the block to +`GanttConfigSchema.safeParse` and logged `[ObjectGantt] Invalid gantt +configuration` to the console on failure — a block missing the trio was +already non-functional at runtime, silently. What changes is *when* the +author is told: runtime console warning becomes an authoring-time refusal. + +Maintainer ruling, objectui#6475 (2026-08-27), **Option A** — enforce as-is, +immediately, no warning window (the startup-stage no-gradualism rule, +objectstack#12668: transitions do not get phased windows without named +external-user evidence, and none exists here). A census of every `gantt` +block reachable through `ObjectGanttSchema` in this repository — the +`examples/schema-catalog` fixtures, `content/docs/plugins/plugin-gantt.mdx`, +and the published `skills/objectui/guides/page-builder.md` guide — found +**zero** blocks missing the trio. diff --git a/packages/types/src/__tests__/gantt-flat-config-declared-keys.test.ts b/packages/types/src/__tests__/gantt-flat-config-declared-keys.test.ts index b2efa27e25..6ff9a452b6 100644 --- a/packages/types/src/__tests__/gantt-flat-config-declared-keys.test.ts +++ b/packages/types/src/__tests__/gantt-flat-config-declared-keys.test.ts @@ -40,8 +40,9 @@ * `markers`, `criticalPath`, `showBaselines`, `readOnly`, `mobileReadOnly`) * are disjoint from them. * - * 27 of the 28 are declared here. The 28th, `gantt`, is severed to objectui#6475 - * — see the section below. + * 27 of the 28 are declared here. The 28th, `gantt`, was severed to objectui#6475 + * and is now declared too — pinned separately below, since it is the one entry + * that narrows the accept set rather than merely adding a name. * * The residue is four LARGER than the card's list, and #5903 is why. The card * scored "declared by neither `ObjectGanttSchema` nor `ObjectGridSchema`" over @@ -59,28 +60,30 @@ * keeps that true as either side moves is the type-level pin at the bottom: * every key of `GanttConfig` is declared on the node's flat face. * - * ## Why `gantt` is measured here but NOT declared here + * ## `gantt`, the 28th key, was severed — and is now declared (objectui#6475) * * The 27 declared below are new OPTIONAL members: additive on both sides, nothing - * previously legal loses its slot. `gantt` is the one that would not have been. - * It has no mirror entry at all, so a block rides through `.passthrough()` - * unvalidated; declaring it as `GanttConfig` means it gets parsed, and + * previously legal loses its slot. `gantt` was the one that would not have been — + * it had no mirror entry at all, so a block rode through `.passthrough()` + * unvalidated. Declaring it as `GanttConfig` means it is now parsed, and * `GanttConfig` derives from the spec's `GanttConfigSchema`, which REQUIRES * `startDateField`, `endDateField` and `titleField`. `ObjectGanttSchema` is a * member of `AnyComponentSchema`, so that reaches `safeValidateSchema` and with - * it the CLI's `validate` / `check` — a block missing one of the three would move - * from "accepted, then warned about at runtime" to "refused at authoring time". + * it the CLI's `validate` / `check`: a block missing one of the three moves from + * "accepted, then warned about at runtime" to "refused at authoring time". * * PM ruling (2026-08-26): sever it, so a published CLI's refusal behaviour is - * decided on its own card rather than inside a 27-key declaration PR. - * objectui#6475 carries the full measurement, including the case FOR enforcing — - * `getGanttConfig`'s block branch already feeds the block to - * `GanttConfigSchema.safeParse` and logs `[ObjectGantt] Invalid gantt + * decided on its own card rather than inside a 27-key declaration PR. Maintainer + * ruling on that severed card, objectui#6475 (2026-08-27), Option A: declare it + * as-is, enforce the spec's requiredness immediately, no warning window (the + * startup-stage no-gradualism rule, objectstack#12668 — no named external-user + * evidence). `getGanttConfig`'s block branch already fed the block to + * `GanttConfigSchema.safeParse` and logged `[ObjectGantt] Invalid gantt * configuration`, so declaring it restores declared = enforced rather than * inventing a stricter contract. * - * Today's behaviour is pinned below rather than left implicit, so the omission is - * a measured state and not a silent gap. + * Today's behaviour — the trio enforced, everything else `GanttConfig` allows + * accepted — is pinned below rather than left implicit. * * ## What the pin has teeth against, and what it does not * @@ -108,8 +111,9 @@ const MINIMAL = { * The 27 keys this card declared, each with a value its declared type refuses. * * 24 flattened `GanttConfig` members and the three query keys (`staticData` / - * `filter` / `sort`). The 28th measured key, `gantt`, is severed to objectui#6475 - * and is pinned separately below as an UNDECLARED read. + * `filter` / `sort`). The 28th measured key, `gantt` (objectui#6475), is pinned + * separately below — it is declared too, but as a PARSED block rather than a + * bare optional scalar, so its refusal shape does not fit this table. */ const DECLARED: ReadonlyArray = [ // — the flattened GanttConfig face — @@ -228,17 +232,38 @@ describe('ObjectGanttSchema — the flattened gantt config is declared (objectui expect(result.success ? null : result.error.issues).toBe(null); }); - it('the `gantt` BLOCK face is still undeclared, and today rides through UNVALIDATED', () => { - // The 28th measured key, severed to objectui#6475. Pinned rather than left - // implicit so the omission is a measured state: a block missing the trio the - // spec's `GanttConfigSchema` REQUIRES parses green today, because the mirror - // has no `gantt` entry and `BaseSchema` is `.passthrough()`. - expect(Object.keys(ObjectGanttSchema.shape)).not.toContain('gantt'); - const missingTrio = ObjectGanttSchema.safeParse({ ...MINIMAL, gantt: { lockField: 'locked' } }); - expect(missingTrio.success).toBe(true); - // Not even a wrong-TYPED block is refused — that is what "no entry" means, and - // it is exactly what objectui#6475 proposes to change. - expect(ObjectGanttSchema.safeParse({ ...MINIMAL, gantt: 'flat' }).success).toBe(true); + it('the `gantt` BLOCK face is declared, and the spec trio now enforces at parse time (objectui#6475)', () => { + // The 28th measured key. objectui#6475, Option A: declared as `GanttConfig`, + // so a block is now PARSED against the spec's `GanttConfigSchema` — the + // published CLI's `validate`/`check` refusal this card exists to pin. + expect(Object.keys(ObjectGanttSchema.shape)).toContain('gantt'); + + // A block missing one of the required trio (startDateField / endDateField / + // titleField) is REFUSED — this is the accept-set narrowing itself, and it + // names the missing field rather than failing silently. + const missingTitleField = ObjectGanttSchema.safeParse({ + ...MINIMAL, + gantt: { startDateField: 'start', endDateField: 'end', lockField: 'locked' }, + }); + expect(missingTitleField.success).toBe(false); + if (!missingTitleField.success) { + const issue = missingTitleField.error.issues.find((i) => i.path.join('.') === 'gantt.titleField'); + expect(issue, 'refusal must name the missing titleField').toBeTruthy(); + } + + // A completely empty block is refused too — all three of the trio absent. + expect(ObjectGanttSchema.safeParse({ ...MINIMAL, gantt: {} }).success).toBe(false); + + // A wrong-TYPED block (not even an object) is refused. + expect(ObjectGanttSchema.safeParse({ ...MINIMAL, gantt: 'flat' }).success).toBe(false); + + // A block carrying the complete trio IS accepted — the narrowing is exactly + // the trio, nothing more. + const complete = ObjectGanttSchema.safeParse({ + ...MINIMAL, + gantt: { startDateField: 'start', endDateField: 'end', titleField: 'name', lockField: 'locked' }, + }); + expect(complete.success ? null : complete.error.issues).toBe(null); }); it('does NOT reject an undeclared key — objectui#5155 ceiling, measured not assumed', () => { diff --git a/packages/types/src/objectql.ts b/packages/types/src/objectql.ts index 43e303f4ca..cdad2b2b45 100644 --- a/packages/types/src/objectql.ts +++ b/packages/types/src/objectql.ts @@ -2507,27 +2507,29 @@ export interface ObjectGanttSchema extends BaseSchema { /** Whether the store persists dependency link TYPES (fs/ss/ff/sf). */ dependencyTypes?: GanttConfig['dependencyTypes']; - // ⛔ `gantt` — the BLOCK face — is DELIBERATELY still undeclared here - // (objectui#6475). It is the 28th key of this card's residue and the only one - // objectui#6051 did not declare; the omission is a scoping decision, not an - // oversight, and reading it as "nothing reads `gantt`" would be wrong — - // `getGanttConfig`'s second branch reads it and honours it in full. + // ── The BLOCK face (the `ObjectGridSchema`-style shape, objectui#6475) ────── // - // Declaring it is not additive the way the 27 above are. It has no mirror entry - // at all today, so a block rides through `.passthrough()` UNVALIDATED; declaring - // it as {@link GanttConfig} means it gets parsed, and `GanttConfig` derives from - // the spec's `GanttConfigSchema`, which REQUIRES `startDateField`, - // `endDateField` and `titleField`. Because `ObjectGanttSchema` is a member of - // `AnyComponentSchema`, that reaches `safeValidateSchema` and therefore the - // CLI's `validate` / `check` commands: a block missing one of the three moves - // from "accepted, then warned about at runtime" to "refused at authoring time". + // `getGanttConfig`'s FIRST branch (`plugin-gantt/src/ObjectGantt.tsx`) reads + // this and wins whenever present (objectui#6469 ruled block-over-flat). It was + // the 28th and last undeclared key of the objectui#6051 census — the one key + // whose VALUES get stricter on declaration rather than merely gaining a name: + // it had no mirror entry at all, so a block rode through `.passthrough()` + // entirely unvalidated. Declaring it as {@link GanttConfig} means it is now + // PARSED, and `GanttConfig` derives from the spec's `GanttConfigSchema`, which + // REQUIRES `startDateField`, `endDateField` and `titleField`. Because + // `ObjectGanttSchema` is a member of `AnyComponentSchema`, that reaches + // `safeValidateSchema` and therefore the CLI's `validate` / `check` commands: a + // block missing one of the three moves from "accepted, then warned about at + // runtime" to "refused at authoring time". // - // That is very likely the RIGHT change — the renderer already feeds the block to - // `GanttConfigSchema.safeParse` and warns, so enforcing restores - // declared = enforced rather than inventing a contract — but it is a published - // CLI's refusal behaviour, and an in-repo census cannot see authored metadata - // living outside this tree. objectui#6475 carries the full measurement and the - // decision. + // This is a `declared = enforced` restoration, not new requiredness: the + // renderer already fed the block to `GanttConfigSchema.safeParse` and logged + // `[ObjectGantt] Invalid gantt configuration` on failure — the trio was already + // required for the block to actually work, just silently. Maintainer ruling, + // objectui#6475 (2026-08-27), Option A: declare as-is, spec requiredness + // enforces immediately, no warning window (excluded by the startup-stage + // no-gradualism rule, objectstack#12668 — no named external-user evidence). + gantt?: GanttConfig; // ── The query/data keys the fetch path reads (objectui#6051) ──────────────── // diff --git a/packages/types/src/zod/objectql.zod.ts b/packages/types/src/zod/objectql.zod.ts index e5f1c5ec13..a69a6db28a 100644 --- a/packages/types/src/zod/objectql.zod.ts +++ b/packages/types/src/zod/objectql.zod.ts @@ -602,9 +602,17 @@ export const ObjectTreeSchema = BaseSchema.extend({ * * Held as ONE field map rather than inlined, so the flattened top-level spelling * below is built from a single source — the same way the TS side derives its - * flattened members from `GanttConfig`. It is deliberately the shape the nested - * `gantt` block would ALSO be built from, one line, if objectui#6475 rules that - * block in; today that entry is severed and this map has one consumer. + * flattened members from `GanttConfig`. It is also the shape the nested `gantt` + * block is built from (objectui#6475), one line extending the spec's gantt + * config schema with this map — so both authoring faces share one vocabulary + * and cannot fork from each other. + * + * ⚠️ Keep this docstring free of a literal `Spec` + capital-letter token: it + * sits between the `ObjectTreeSchema` and `ObjectGanttSchema` export + * boundaries, and `zod-mirror-parity.test.ts`'s `SPEC_DERIVED_PAIRS` re-check + * scans raw text between export boundaries for `\bSpec[A-Z]\w*` — a match here + * is misattributed to `ObjectTreeSchema`, which references no spec schema at + * all (measured: this comment alone flipped that test red). * * Not exported: the parity census in `__tests__/zod-mirror-parity.test.ts` reads * `^export const` out of this directory and would require a registered TS @@ -723,16 +731,26 @@ export const ObjectGanttSchema = BaseSchema.extend({ autoZoomToFilter: SpecGanttConfigSchema.shape.autoZoomToFilter, // …and objectui's own ten, from the one field map above. ...GanttConfigExtensionFields, - // ⛔ `gantt` — the BLOCK face `getGanttConfig`'s second branch reads — is - // DELIBERATELY still unmirrored (objectui#6475). It would be one line here, - // `SpecGanttConfigSchema.extend(GanttConfigExtensionFields).optional()`, built - // from the same field map as the flat face above; the reason it is not is that - // it is the one entry that NARROWS. With no entry a block rides through - // `.passthrough()` unvalidated; with one it is parsed against the spec's - // `GanttConfigSchema`, which REQUIRES startDateField/endDateField/titleField — - // and this mirror reaches the CLI's `validate`/`check` through - // `AnyComponentSchema`, so that is a published refusal change. See the TS - // declaration's note in `../objectql.ts` and objectui#6475. + // `gantt` — the BLOCK face `getGanttConfig`'s FIRST branch reads and prefers + // (objectui#6469 ruled block-over-flat) — objectui#6475. Built from the same + // field map as the flat face above, `SpecGanttConfigSchema` extended with + // `GanttConfigExtensionFields`, so the two authoring faces cannot fork. + // + // This is the one entry among the 28 that NARROWS rather than merely names: a + // `gantt` block previously rode through `.passthrough()` entirely unvalidated; + // now it is PARSED against the spec's `GanttConfigSchema`, which REQUIRES + // `startDateField`/`endDateField`/`titleField`. This mirror reaches the CLI's + // `validate`/`check` through `AnyComponentSchema` → `safeValidateSchema`, so a + // block missing the trio moves from "accepted, then warned about at runtime" + // to "refused at authoring time" — a `declared = enforced` restoration, not + // new requiredness: the renderer already fed the block to + // `GanttConfigSchema.safeParse` and logged `[ObjectGantt] Invalid gantt + // configuration` on failure. Maintainer ruling, objectui#6475 (2026-08-27), + // Option A: enforce as-is, no warning window (excluded by the startup-stage + // no-gradualism rule, objectstack#12668 — no named external-user evidence). + gantt: SpecGanttConfigSchema.extend(GanttConfigExtensionFields).optional().describe( + 'Nested gantt config block — the authoring face, and the winner over the flattened top-level keys whenever present' + ), // The query/data keys the fetch path reads. They were declared on // `ObjectGridSchema` — what `ObjectGanttProps.schema` used to be typed as before // objectui#5903 retyped it to `ObjectGanttSchema` — so they need declaring here.