feat(types): declare a gantt bar as an object on both published faces - #8405
Conversation
`TimelineSchema.items[].items[]` — a gantt row's bars — was `z.array(z.any())`
in the zod mirror and `any[]` on the TypeScript face, so an authored `null` bar
was green through `validate` and only met the render-time date diagnostic,
which named `items[0].items[0].startDate is undefined` — a key the author never
wrote.
Both faces now declare the shape objectui#7164 declared one level up: the
mirror's row `items` becomes `z.array(z.object({}).passthrough())`, and the TS
face states the row/bar shape its docblock previously carried only in prose.
objectui#7164's deliberate stop at the row level is superseded knowingly, and
the prose recording that stop is removed rather than qualified.
The bar stays `.passthrough()` — its own keys are still undeclared — and the
render-time `malformedRow` copy and its ten language packs are untouched.
Narrows a published accept set. In-repo stock measured on 289d146 across
apps/ examples/ content/ packages/types/examples/: five authored bars, all
well-formed objects, zero null, positive-controlled.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QtGhnU3WnnWyiWeYQhw2aX
…tt-bar-object-declaration
…d on The five-bar, zero-null in-repo reading was taken on 289d146 and re-taken unchanged on c4b3750, the base this branch merges against. Cite both so the figure in the docblocks, the pin header and the changeset matches the tree a reviewer will check it against. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QtGhnU3WnnWyiWeYQhw2aX
✅ Console Performance Budget
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
Size Limits
|
… the TS-face break Contract review returned FAIL on two RECORD defects. Neither is a schema change, and no existing assertion moved. R1 — `timeline-items-row-shape-7164.test.ts` still carried objectui#7164's "What is deliberately NOT narrowed" passage, stating that the bars stay `z.any()` and that narrowing them is "a wider contract than the ruling named". objectui#7365 supersedes that stop knowingly, so the file — the one the new bar-level pin cross-references, and the one the mirror docblock names as the row-level pin — now records the supersession instead of the superseded stop, and points at `timeline-items-bar-shape-7365.test.ts`. The still-true half (element and bar both stay `.passthrough()`; feed variants untouched; refining by `variant` is still wider than either ruling named) is kept. No assertion in the file was touched. R2 — the changeset declared the `safeParse` narrowing in detail but not the TypeScript face, which is the only record a downstream TypeScript consumer reads. It now names that a row's and a bar's own keys type as `unknown` where they were `any` (`TS2322: Type 'unknown' is not assignable to type 'string'` at the read site), and that a `null` or non-object bar literal — and a `null` row literal — no longer compiles. All five verified two-sidedly against a rebuilt-in-place "previous face" control. Also folds in the reviewer's optional, non-blocking suggestions on the 7365 pin, adding assertions only: every bar refusal is asserted to be an `invalid_type` (an `unrecognized_keys` there would mean the wrong rule fired, since the bar stays passthrough), and one case runs through the union door `safeValidateSchema` that `objectui validate` actually applies, with the well-formed half asserted beside it so a green refusal is not just "no arm matched". Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QtGhnU3WnnWyiWeYQhw2aX
Patch round on the contract-review FAIL — two record edits, no schema changeHead R1 — the superseded stop was still standing
Before (on After (same The replacement runs to ⛔ Prose only: 6 comment lines out, 19 comment lines in, zero assertions touched. R2 — the changeset omitted the TypeScript-face breakAdded to (The two code lines sit in a real Verified two-sidedly before the probe file was deleted. A throwaway test file rebuilt the PREVIOUS face in place — the head type with Line 22 is ⭐ Optional suggestions, folded in — assertions ADDED, none changed
Gates on
|
| gate | exit | verdict line |
|---|---|---|
both pins (timeline-items-row-shape-7164 + timeline-items-bar-shape-7365) |
0 | Test Files 2 passed (2) · Tests 54 passed (54) |
the 7365 pin, --reporter=verbose |
0 | Tests 31 passed (31); the three new cases named and green |
pnpm --filter @object-ui/types type-check |
0 | tsc --noEmit && tsc -p tsconfig.examples.json && tsc -p tsconfig.test.json — third leg included |
node scripts/check-changeset-presence.mjs |
0 | ✅ 4 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s) |
eslint on the two edited test files |
0 | clean |
| control-byte self-scan on the three edited files | no hits | grep -naP over the C0/DEL ranges |
origin/main was NOT merged
mergeable_state read clean immediately before the push, and git merge-tree --write-tree origin/main HEAD exited 0 against e5a28b9 (merge-base c4b3750) — no conflict to resolve, so the branch was left un-churned and the gates above sit on a head that is exactly the reviewed diff plus this patch.
⛔ PR stays draft. Not enqueued, no auto-merge, no approval, and needs:contract-review is untouched — the carrier stays until a fresh contract-tier review passes.
Generated by Claude Code
|
The file on disk carries the real spelling and is the authority — Generated by Claude Code |
✅ Console Performance Budget
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
Size Limits
|
Fixes #7365
A gantt BAR is now declared an OBJECT on both published faces. An authored
nullbar is refused atvalidate, by its own name, atitems[i].items[j].What was wrong
TimelineSchema.items[].items[]— a gantt row's bars — wasz.array(z.any())in the zod mirror and
any[]on the TypeScript face. So this ordinary JSON wasgreen through
validate:{ "type": "timeline", "variant": "gantt", "items": [{ "label": "R", "items": [null] }] }and the author's only signal came later, from the render-time date diagnostic:
That names the wrong fault. The bar is
null;startDateis a key the authornever wrote. This is the mis-naming class objectui#7164 repaired for ROWS, one
level down.
The ruling this implements
Director seat, decision batch #71, 2026-09-07; maintainer reply verbatim,
unaltered and untranslated: 「其他同意」. Option B.
objectui#7164 narrowed the row and stopped at the bar level deliberately,
and wrote the stop down in both faces' docblocks. This PR supersedes that stop
knowingly, per the ruling — and removes the prose recording it rather than
qualifying it, so the next reader does not re-derive a gap that is now closed.
That re-derivation is how this card came to exist.
⛔ Option A is refused, not deferred.
timeline.gantt.unusableRange.malformedRow'scopy is untouched, no fourth path level was added to that sentence, and the ten
language packs are untouched. The render-time date diagnostic remains the
defined outcome for anything that still reaches it — the renderer stays only
ever MORE lenient than
validate, never the reverse.⛔
packages/plugin-timeline/src/renderer.tsxwas READ ONLY here; its rowpredicate belongs to objectui#7364 and is not touched.
The change
Re-derived on the merged base
c4b3750(the ruling's line numbers were stale;so were the ones taken at dispatch, because main moved again in between):
c4b3750packages/types/src/zod/data-display.zod.ts:732z.array(z.any())z.array(z.object({}).passthrough())packages/types/src/data-display.ts:1988items?: any[]packages/types/src/zod/data-display.zod.ts:701z.array(z.any())occurs three times in that mirror;:243and:258aredata:on Table schemas and are NOT this target. The edit is anchored on theenclosing
TimelineRowSchema, not on a grep.The TS face is the TypeScript spelling of the mirror at each level —
a
RecordKEYED BYstringWITHunknownVALUES for the mirror'sz.object({}).passthrough(), and a string index signature for the passthroughitself. (That type is spelled in words throughout this body, never in its
angle-bracket form — see the note at the foot.)
Nothing beyond the bar's object-ness is narrowed. A bar's own keys
(
title/startDate/endDate/variant?) stay undeclared and open, soitems: [{}]still parses.TimelineSchemaserves more than gantt, so thiswas checked one variant over: a feed item carries no
itemskey at all and isuntouched — pinned both ways in the new test.
Stock measurement — required, because this narrows a published accept set
Counted on
289d146and re-measured unchanged onc4b3750, acrossapps/·examples/·content/·packages/types/examples/:The zero is a reading, not an empty search. Two firing controls:
paths.
nullbar and a planted numeric bar, added to a scratch copy ofthe gantt fixture, were both found and named:
examples/planted.json :: .items[0].items[2]and.items[1].items[2] (non-object: 7).⇒ Nothing in this repository moves.⚠️
hotcrmis a separate repository and isnot reachable from this checkout: it is unmeasured, and named as such rather
than counted as zero.
The docs page's gantt example is a TS literal, not JSON, so it is covered by a
different instrument — see the ablation below.
Evidence
Reverse verification of the TS face, against the rebuilt
.d.tsa consumeractually resolves (
@object-ui/plugin-timelinehas no src path alias, so itreads
dist/data-display.d.ts):items: [null]TS2322: Type 'null' is not assignable to type 'Record KEYED BY string WITH unknown VALUES'The two legs' full error sets differ by exactly that one line. Restore proven by
an empty
git diff HEAD, not by an exit code.Ablation proving
check:doc-snippetsactually measures the docs ganttexample — a green gate over 632 blocks does not by itself say this block is
one of them. Planting a
nullbar incontent/docs/plugins/plugin-timeline.mdxturned it red at exactly the expected place:
Restored and verified by blob hash
362f6d19ff9aba4656976c6cb870abab69e23f47matching the HEAD blob, plus an empty
git diff HEAD.New pin —
packages/types/src/__tests__/timeline-items-bar-shape-7365.test.ts,modelled on objectui#7164's. It carries the ruling's pin (
{ items: [{ label: 'R', items: [null] }] }fails atitems[0].items[0]; a well-formed barpasses), asserts the refusal never mentions
startDate, re-asserts objectui#7164'srow-level refusals as unchanged, and rebuilds the PREVIOUS declaration in-test
so the "before" column is measured against today's base rather than remembered.
Gates — all on the final head
630f54c, exit code captured before any pipe@object-ui/typesbuilddist completeness: 1 package(s) complete (124 emitted files verified)@object-ui/typestype-check--noEmit+tsconfig.examples.json+tsconfig.test.json)@object-ui/typestestsTest Files 142 passed (142) · Tests 2723 passed (2723)@object-ui/plugin-timelinetype-check@object-ui/plugin-timelinetestsTest Files 25 passed (25) · Tests 303 passed (303)@object-ui/typeslint272 problems (0 errors, 272 warnings)— 208 files, all warnings pre-existingcheck:doc-snippetsSemantic phase: 632 of 632 block(s) judged, 0 failedcheck:doc-typesEvery documented component type is registeredcheck:doc-fencescheck:spec-symbols1353 sources + 184 documentation pages; nothing cites a key its spec symbol does not declarecheck:spec-floorsEvery consumer-facing @objectstack/spec floor carries the symbolscheck:readme-exports43 tracked README(s) … 0 unbuiltcheck:sdui-registration-pinsAll 16 registration(s) … present in the built consolecheck:control-bytesscanned 6656 tracked text file(s)check:element-data-source-declarationcheck:handler-key-readscheck:self-importcheck:phantom-depsRoot
test,lintandtype-checkrun the whole farm and are CI's; thepackage-scoped equivalents above are complete for this diff —
@object-ui/typesis the only package whose source changed, and
@object-ui/plugin-timelineisthe only consumer that reads the narrowed key.
Serial constraint, and the answer it produced
packages/types/src/zod/data-display.zod.tsis shared with PR #8354(objectui#7760, draft, parked at the same gate), which edits
TreeNodeSchemaatabout
:323. This card's region is the timeline rowitemsat about:732.That PR pins seven header figures on
packages/types/src/__tests__/zod-mirror-parity.test.ts, so the stop conditionwas: does this change move any row there?
No. Zero rows added, removed or moved — the file is byte-identical to
origin/main(git diff origin/main -- packages/types/src/__tests__/zod-mirror-parity.test.tsis empty), and it was not edited. Both faces narrowed symmetrically, so
there is no divergence to record:
WiderThanDeclaredKeysstill resolves toneverforTimelineSchema::items, which the parity test's type-level ledgerassertions confirm by passing under
tsc -p tsconfig.test.json.Contract review
Clause-② is yes — this narrows a published accept set: a
nullgantt bar islegal today and will not be after this lands.
needs:contract-reviewis hung onthis PR and on the card. This PR stays draft, is not enqueued, and has no
auto-merge armed; parking green is the intended end state.
Changeset:
@object-ui/typesminor. Breaking semantics,minorper thisrepo's rule that its own breaking changes never declare
major(the fixed groupfollows
@objectstack's cadence); the breaking half is spelled out in thechangeset body.
Notes
objectui#7364 owns the renderer's row predicate, and objectui#7164 is the
row-level card this one sits below. Both stay open; this PR acts on 7365 only.
written in its angle-bracket form — this platform's body sanitizer eats short
tag-shaped fragments, including inside backticks and fenced blocks, so type
arguments are spelled as words (
Record KEYED BY string WITH unknown VALUES)and read literally in the source files, where they carry their real spelling.
🤖 Generated with Claude Code
https://claude.ai/code/session_01QtGhnU3WnnWyiWeYQhw2aX
Generated by Claude Code