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
52 changes: 52 additions & 0 deletions .changeset/6475-gantt-block-face-declared.md
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
*
Expand Down Expand Up @@ -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<readonly [string, unknown]> = [
// — the flattened GanttConfig face —
Expand Down Expand Up @@ -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', () => {
Expand Down
40 changes: 21 additions & 19 deletions packages/types/src/objectql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) ────────────────
//
Expand Down
44 changes: 31 additions & 13 deletions packages/types/src/zod/objectql.zod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
Loading