feat(types): ObjectKanbanSchema.objectName becomes a presence rule on both faces (bind | data | objectName) - #8412
Conversation
…th faces `ObjectKanban` resolves rows through the pre-fetched `data` prop, then `bind`, then the inline row array on `schema.data`, and only then a fetch keyed by `schema.objectName` — every `objectName` read guarded. Both published faces nevertheless REQUIRED `objectName`, so a `bind`-only or `data`-only board that renders correctly was refused by the shipped validator and could not be annotated with its own type. `objectName` is now optional on the interface and on the mirror, with the requirement the renderer really has expressed as a refinement: `requireKanbanRecordSource` — at least one of `bind`, `data`, `objectName`. Deliberately NOT the map / gantt / calendar predicate: that ladder is `data` (a ViewData provider block) -> `staticData` -> `objectName` through the shared `resolveRecordSourceConfig`. This board has no `staticData` rung, reads `data` as a raw row array, has a `bind` rung the other three never walk, and calls `getDataConfig` zero times. objectui#7651 (ruled B, closed not_planned) refuses building the shared ladder here; nothing here adds a rung, and `data` / `bind` stay inherited from `BaseSchema` rather than re-declared. `groupBy` stays REQUIRED (objectui#7322, PR #7774): a record source and a lane key are different questions, and the two readings that PR excluded from counting as a lane-less mode are pinned still-refused. Refs #7780 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
|
PM 落地 — 契约复核 PASS,已翻 ready 并入队
三条落地判据,全部在 ready 那一刻重测 —— ⛔ 不沿用复核时的读数。 这一点不是形式:复核当时我钉的 main 是
main 漂移的语义面也读了一次,不只读了冲突面。 merge-base 闭合关键词两读 —— 用 GitHub 自己的解析器读,不是肉眼扫首行
⇒ 本 PR 的正文关闭不了任何卡,包括它自己那张。这是安全拼法,不是遗漏。 合并后待办(记进落地待办,⛔ 不留给下次巡检)
正文里两句现已被本次动作取代dev 报告写的「⛔ 本 PR 保持 draft 与未入队,等待复核」与「⛔ 未翻 ready、未入队」,是交报那一刻对自身停放状态的如实记述,当时正确。ACCEPT 之后的路径分叉是 PM 的,不是 dev 的 —— 现由本评论记录状态迁移,正文不改写,以免在一份 500 行正文上引入誊抄缺陷。 ⛔ 未自审批。本会话的 GitHub 身份 Generated by Claude Code |
Refs #7780
ObjectKanbanSchema.objectNamebecomes a presence rule on both published faces — at least one ofbind,data,objectName— so the declared face matches the record-source ladderObjectKanbanactually walks.Dispatched by the
domain:spec@ objectui PM seat.Clause-②: yes(a published accept set widens), soneeds:contract-reviewis hung on this PR and on the card. ⛔ This PR stays draft and unqueued awaiting that review — that is its sanctioned resting state, not a stall.The two faces
packages/types/src/objectql.ts—objectName: stringbecomesobjectName?: string, with a docblock naming the four rungs the renderer reads and why this is not the map/gantt/calendar ladder.packages/types/src/zod/objectql.zod.ts—z.string()becomesz.string().optional(), and the member ends in.superRefine(requireKanbanRecordSource). That predicate is new and local, notrequireRecordSource:It is a
function, not anexport const, for the reasonrequireRecordSourcegives right above it: the parity census reads^export constout of that directory and would demand a registered TS counterpart.Also in this PR, as the dispatch requires: the
@ts-expect-errorinobject-calendar-record-source-7313.test.tsthat asserted "objectName is still required on ObjectKanbanSchema" is deleted (leaving it would beTS2578undertsconfig.test.json), and the literal it guarded is renamed and re-documented to record that the class boundary moved.Accept set, before to after — from source, both entry paths
Measured with a probe against a rebuilt
dist, atorigin/mainfff250ffand at this branch's HEADdeed7878. Every document carriesgroupBy, soobjectNameis the one member under test.ObjectKanbanSchema.safeParsebeforesafeValidateSchemabeforebindonlyobjectName:invalid_typedataonly (raw rows)objectName:invalid_typeobjectNameonlybind+objectNamedata+objectNameNegative controls — refused before, still refused after:
objectName:invalid_type(root):RECORD_SOURCE_REQUIREDobjectName(nogroupBy)groupBygroupBybindobjectName+groupBygroupBydataobjectName+groupBygroupByobjectName+groupBygroupBygroupFieldpresentgroupFieldgroupFieldobjectName: 42objectNameobjectNamegroupBy: 7groupBygroupBylimit: 0limit:too_smalllimit:too_smallobjectName: ''!== undefined, not truthiness)No row moved from ACCEPT to REFUSE. Two rows moved REFUSE to ACCEPT, and one refusal moved from a key-level failure to the named refinement at the root path.
One reading worth naming: with
groupByabsent the refinement does not fire, because zod 4 skips refinements when the shape parse already failed. That is why the pin's "no record source" document carriesgroupBy— otherwise the case would go green for the lane key's reasons and stop saying anything about the record source.groupByrequiredness (PR #7774) is not overturnedPR #7774's
groupBymeasurement explicitly excluded two readings from counting as a lane-less mode. Both are still refused, and the new pin asserts it:content/docs/utilities/data-objectstack.mdx— thedataSourcejson fragment.dataSourceis not a rung of this ladder (ElementDataSourceGatemaps itsobjectontoobjectNameupstream of the node), so the fragment has no record source and no lane key and is refused for both.packages/plugin-list/src/ListView.tsx— the runtime-generated node, in the shape it emits when the view declared no lane field (groupBy: laneFieldwithlaneFieldundefined). Still refused atgroupBy.The retired
groupFieldtombstone is likewise still refused by name.objectui#7651 holds — and a reading on its radius
objectui#7651 ("Should
ObjectKanbanget a real record-source ladder?") was ruled B and closednot_plannedon 2026-09-05; its epitaph is in the tree onKanbanSchema.data. It refuses a sixthgetDataConfigproducer, aViewDataretype of the board'sdata, and astaticDatarung. Nothing here adds a rung, and the pin asserts that by IDENTITY rather than membership —shape.data === BaseSchema.shape.dataandshape.bind === BaseSchema.shape.bind— becausedataandbindreach this member by inheritance throughBaseSchema.extend, so membership alone cannot tell inherited from re-declared. AViewDataretype or astaticDataaddition replaces the entry and reddens it.KanbanSchema.data— thetype: 'kanban'interface inpackages/types/src/complex.ts— and its epitaph sits on that interface'sdatamember. This card movesObjectKanbanSchema(type: 'object-kanban',packages/types/src/objectql.ts), a different interface in a different file. So #7651 does not literally cover the member this PR edits. What it does cover, and what this PR obeys, is the substance: it refuses building a record-source ladder for the board, and no rung is built here — the only thing that moved is the requiredness of one member. The refusal list is respected on both interfaces regardless of which one it literally names. ⛔ I did not treat the literal gap as permission for anything; flagged for the contract reviewer.In-repo census of authored
object-kanbannodesInstrument: every git-tracked file under
apps/·examples/·content/·packages/whose bytes containobject-kanban(93 files), scanned for occurrences where the token is the value of atypekey, then walked out to the enclosing brace-balanced literal and read at depth 1. 49 occurrences recognised.objectName-onlycontent/docs/plugins/plugin-kanban.mdx(the documented object-driven board)data-onlyplugin-kanban/src/ObjectKanban.overlayTitleI18n.test.tsx,…overlayTitleNoProviderFallback.test.tsxdata+objectNamebind-onlypackages/types/src/base.ts— the JSON example inBaseSchema.bind's docblockControl — the instrument fires. A known-positive
bind-only node was planted as a tracked file, the census re-run, and the plant reported by path and line (bind-only 1 to 2); removing it restored output byte-identical to the baseline run, with a cleangit status. The positives above are the other half of that control: the walker finds the well-formed nodes too, not only the planted one.Radius, stated rather than implied. It does not see nodes assembled at runtime by spread or variable, nodes whose
typeis a variable, or nodes assembled across files — and ⛔ authorship outside this repository is not measurable from this checkout at all: customer apps, published examples and stored page documents are simply not in the walk. No claim is made about them.bind-only node in this repo — theBaseSchema.binddocblock example — also carries nogroupBy, so this widening alone does not make it validate; it stays refused, atgroupBy. The twodata-only fixtures are the nodes this change actually unblocks: both carry anas neverescape whose comment names this issue and says "when #7780 lands, this one follows". Those files are inpackages/plugin-kanban/**, which this card does not hold, so they are not converted here.Corrections to the inherited premises — both measured
① The plugin's own
KanbanSchemaatplugin-kanban/src/types.ts:89does not exist — CONFIRMED false, as the dispatch stated.wc -lon that file is 59, so:89cannot exist;grep -rnE '(interface|type|const) +KanbanSchema\b' packages/plugin-kanban/returns 0 hits (exit 1) while the same pattern overpackages/types/src/fires oncomplex.tsandzod/complex.zod.ts(exit 0). TheKanbanSchemain that file is a pure re-export, collapsed to one source by objectui#7664.② The props type is a union — CONFIRMED, and this card needs no
domain:uileg.packages/plugin-kanban/src/ObjectKanban.tsxdeclaresschema: KanbanSchema | ObjectKanbanSchema;, not the bareKanbanSchemathat triage 5557326104 §4 measured. It already carries both arms, so there is nothing for a ui leg to change. Stronger than that:KanbanSchema.objectNamewas already optional, so on the union the property already resolved tostring | undefined— makingObjectKanbanSchema.objectNameoptional does not move the union's property type at all. Measured, not inferred:pnpm exec vitest run --maxWorkers=2 packages/plugin-kanban/is 30 files / 204 tests green on this branch, unchanged.Parity — measured, not asserted
The dispatch declined to assert whether this widening moves a
zod-mirror-parity.test.tsledger row or an objectui#7279 header figure, and asked for a measurement. It does not, and here is the two-leg evidence rather than a claim:packages/types/run.The mechanism: no key is added or removed on either face, so both key sets are unchanged; optionality moved on both faces in the same stroke, so no required-vs-optional drift row is created; and zod 4 attaches
.superRefinein place, leaving.shapeexposed (measured directly, and the sibling membersObjectMapSchema/ObjectGanttSchema/ObjectCalendarSchemaalready carry refinements and sit in the same census). ⇒zod-mirror-parity.test.tsneeds no edit, and PR #8354 is not touched.Reverse verification
Both faces reverted in one mutation, restored by
git checkout HEAD -- (paths)under anEXIT INT TERMtrap on absolute paths.objectql.ts5d76024dtof7c01902;objectql.zod.ts4ddc342cto03b7ab9b. Anchored counts moved with it:superRefine(requireKanbanRecordSource)1 to 0, the injectedvoid requireKanbanRecordSource;1, the optionalobjectNamedescribe string 1 to 0,objectName?: string;7 to 6.1 failed | 141 passed (142)files,7 failed | 2705 passed (2712)tests. The one red file isobject-kanban-record-source-7780.test.ts, and only it. All seven red assertions are this card's own subject (the four documents, the published entry point,.shapeoptionality, the no-rung identity pin, the sibling-ladder contrast). ThegroupBy-requiredness block and the off-disk renderer-read block stayed green, correctly — the mutation does not touch those facts.git diff HEADis 0 bytes,git status --porcelainempty.142 passed (142)files,2712 passed (2712)tests.No dist rebuild is needed between legs here, and that is a property of the resolution path rather than an assumption: the pin imports
'../zod/objectql.zod', a relative source specifier inside the same package, so nodist/is in the path — which the red leg demonstrates by going red with no rebuild at all.Gates (exit codes captured before any pipe)
pnpm exec vitest run --maxWorkers=2 packages/types/Test Files 142 passed (142)·Tests 2712 passed (2712)pnpm exec vitest run --maxWorkers=2 packages/plugin-kanban/Test Files 30 passed (30)·Tests 204 passed (204)pnpm --filter @object-ui/types builddist completeness: 1 package(s) complete (124 emitted files verified)pnpm --filter @object-ui/types type-checktsc --noEmit && tsc -p tsconfig.examples.json && tsc -p tsconfig.test.jsonnode scripts/check-changeset-presence.mjs3 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)pnpm check:spec-symbolsnothing cites a key its spec symbol does not declarepnpm check:control-bytesOK (scanned 6659 tracked text file(s); skipped 85 binary)pnpm check:unreferenced-sourcesEvery shipped source file in every covered package is reachablenode scripts/check-governed-queue-guard.mjs --test (5 paths)NOT GOVERNED — 5 path(s) checked against 5 governed surface(s); none matchedpnpm exec eslint . --format jsontsconfig.test.jsondemonstrably covers the new pin rather than excluding it:tsc -p tsconfig.test.json --listFilesputs it in a 605-file program (the 7313 pin is there as the control).pnpm check:readme-exportsexits 1 in this worktree, and 514 of its 517 findings are literallytype entry ./dist/index.d.ts is not on disk -- run pnpm build first; the other three are ratchet floors that read the same unbuilt packages (packagesRead: found 2, floor is 25). It needs a full monorepo build, which CI does. It is unrelated to this diff on the evidence: no README is touched, no export is added or removed from any shipped entry point (the only renamedexport constis inside a test file), and zero of its findings name@object-ui/types— the one package that is built here.验收备注
Out-of-scope observations, recorded rather than filed and not acted on:
packages/plugin-kanban/**carry anas neverescape whose comment says "when finding(types):ObjectKanbanSchemarequiresobjectNameon both faces while the renderer reads an undeclared inlinedataahead of the fetch — the record-source ladder class (#7313) on a fourth view schema #7780 lands, this one follows" —ObjectKanban.overlayTitleI18n.test.tsxandObjectKanban.overlayTitleNoProviderFallback.test.tsx. They can now become a realsatisfies ObjectKanbanSchema. That package belongs todomain:ui's finding(types/plugin-kanban): an authoredkanbandocument can PASSsafeValidateSchemaand still render empty — the validator and the renderer honour different faces #7664, so ⛔ not converted here. Follow-up for that seat.BaseSchema.binddocblock teaches{ "type": "object-kanban", "bind": "app.settings.users" }— a node that is still refused after this change, on the missinggroupBy. Whether the teaching example should author a lane key is a docs judgement about a doc this card does not hold, and it is not a defect in either face.dataSourceis authored onobject-kanbannodes incontent/docs/utilities/data-objectstack.mdxand is undeclared onObjectKanbanSchema, ridingBaseSchema's passthrough. It is not a rung of this ladder —ElementDataSourceGatemaps itsobjectontoobjectNameupstream — so it is deliberately out of the presence rule, and PR feat(types): declare ObjectKanbanSchema.groupBy and .limit, retire groupField on both faces (objectui#7322 item 1) #7774 already ruled that fragment out of the lane-less census. Noted only.None of the three is a reproducible defect, a declared-contract violation, or a trap producing runtime-rejected metadata, so none was filed as a card.
⛔ Not flipped ready, not enqueued, no auto-merge, not self-approved. This session's GitHub identity is
os-zhuang, which is both aGOVERNED_APPROVERand this PR's author.Generated by Claude Code