spec(ui): declare titleField on KanbanConfigSchema — the optional key its five item-titled siblings already carry - #18561
Conversation
`KanbanConfigSchema` was the one item-titled view config of its family that omitted `titleField`, while Gallery, Timeline, Calendar, Gantt and ListMap all declare the key under the same name and the same `z.string()`. The schema is a `strictObject`, so an author writing the key the board actually reads was refused by name. Declared optional, matching the shape `CalendarConfigSchema` already writes down for this exact key: absence resolves through the ADR-0079 record display-name chain, so requiring it would demand more than the renderer reads. Timeline and Gantt spell it required and are the two siblings this declaration deliberately does not copy. Tests carry the four-leg probe with both controls firing on the same call shape, plus the optionality leg. Claude-Session: https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3 Co-authored-by: Claude <noreply@anthropic.com>
…tion `authorable-surface/ui.json` gains `ui/KanbanConfig:titleField`; the three reference pages that inline the kanban config shape pick up `titleField?: string`. Regenerated, not hand-edited: `check:authorable-surface` wrote the first and `gen:docs` the rest. Adds the minor changeset for the widening. Claude-Session: https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3 Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift Check1 anchor(s) derived from 1 changed package(s); no hand-written page names any of them. What this run could not see
Coarse fallback — 136 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 67d8299911aca37ad3db044fbe890779d35791c7 && git checkout 67d8299911aca37ad3db044fbe890779d35791c7
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 79a046f8cdf085d95200826ee9bb2fa6584bc3d5 6d01b4b63e07b5fbdc0069ce8f5143167b9813ff && git checkout -B drift-repro 79a046f8cdf085d95200826ee9bb2fa6584bc3d5 && git merge --no-ff 6d01b4b63e07b5fbdc0069ce8f5143167b9813ff
node scripts/docs-audit/affected-docs.mjs --json 79a046f8cdf085d95200826ee9bb2fa6584bc3d5 |
Contract reviewServed-tier: 30/30 Isolated, at-tier clause-② review of PR #18561 for card #16894, executing the director ruling (decision batch #87, objectstack-ai/objectui#8367 comment 5582071618, maintainer 「批 #87 同意」). The dispatch order and the dispatching seat's acceptance were withheld from this seat; every reading below was taken from the tree and the diff, on a detached worktree of the head, with the objectui pin ① Derived judgmentsEach accept-set or public-surface change the diff produces, named and judged:
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
Clause-② adopted — PASS; both carriers stripped in this actReview of record: PR-thread comment 5710778226. Adopted verbatim. A dispatching seat may adopt an isolated reviewer's record whole or void it whole; it may ⛔ never rewrite or polish one, and this seat did neither. Everything below is disposition of the flags the record raises — ⛔ not a supplement to its judgments.
Independence pair
Why an at-tier review exists here at allThis seat measures below Tier evidence, in the shape that rule demands: the reviewer parsed every The reviewer was fed the card, the ruling it executes, and the PR body/diff — ⛔ not the dispatch order and ⛔ not this seat's own acceptance conclusions. Disposition of the reviewer's boundary flags
Landing pre-checks, read on this head
Next: ready → auto-merge, then followed to MERGED.
Generated by Claude Code |
Fixes #16894
Clause-②: yes (widening)
KanbanConfigSchemanow declarestitleFieldas optionalz.string(), executing the director seat's decision batch #87 (recorded at objectstack-ai/objectui#8367 comment5582071618, confirmed by the maintainer verbatim 「批 #87 同意」). The direction was settled there; this PR is the implementation only.What moved
packages/spec/src/ui/view.zod.tstitleField: z.string().optional()onKanbanConfigSchema, plus the docblock that records why it is optionalpackages/spec/src/ui/view.test.tspackages/spec/authorable-surface/ui.jsonui/KanbanConfig:titleFieldcontent/docs/references/ui/view.mdx,content/docs/references/api/protocol.mdx,content/docs/references/data/object.mdxgen:docs.changeset/16894-kanban-config-titlefield.md@objectstack/spec: minorRegenerated, never hand-edited:
check:authorable-surfacewrote the first andpnpm --filter @objectstack/spec gen:docsthe rest.check:generatedreports all 15 artifacts up to date.Premise re-derivation — the card's sibling table does NOT match the tree
Re-derived on this worktree at
origin/main79a046f8c(the dispatch's own derivation ran one commit behind, at582d3e5).git grep -n 'titleField' -- packages/spec/src/ui/view.zod.tsreturns six carriers; mapping each to the schema whose member list encloses it::1057GalleryConfigSchema(declared:1050):1071TimelineConfigSchema(declared:1065):1407CalendarConfigSchema(declared:1401):1490GanttConfigSchema(declared:1484):1638ListMapConfigSchema(declared:1631)KanbanConfigSchema(declared:1346)Two corrections to the card body, neither of which disturbs the ruling:
ListMapConfigSchemaalso declarestitleField, optional, and the card does not mention it.The ruling is unaffected: it prescribes the arity directly (
optional z.string()) and namesCalendarConfigSchema's docblock as the reference, which is optional at:1407. So the shape landed here is the ruled one, and the card's optional/required split was simply not re-measured when it was written. The defect itself re-derives exactly as filed:KanbanConfigSchemais astrictObjectand refusedtitleFieldby name.Evidence
pnpm --filter @objectstack/spec buildVERDICT command-exit 0pnpm --filter @objectstack/spec typecheckTYPECHECK_EXIT=0—check:test-typecheck: OKpnpm --filter @objectstack/spec testTEST_EXIT=0—Test Files 483 passed (483)/Tests 13775 passed (13775)pnpm --filter @objectstack/spec exec vitest run --maxWorkers=2 src/ui/view.test.tsTest Files 1 passed (1)/Tests 376 passed (376)pnpm --filter @objectstack/spec check:generated① is empty by construction:
packages/spechas no workspace dependencies, so--filter '@objectstack/spec^...' buildhas an empty closure. The package itself was built before any gate that readsdist/.③ Gate families were derived on this worktree, not inherited:
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commandsover the real 7-path change set yields 108 families. All 108 were run with each exit code landed to disk before being read, and reconciled with--ran:103 green. The five non-zero results, none of them a finding against this diff:
pnpm check:dual-build-cjs-loads,pnpm check:lean-entry-closure,pnpm check:type-check-debt— exit 3, PREREQUISITE NOT MET, all three refusing because the whole monorepo is not built on this worktree. NOT MEASURED, declared to CI, which checks out and builds fresh.node scripts/check-plugin-teardown-shape.mjs --self-test— exit 1, but a prerequisite refusal in its own words: "cannot read the positive control at621a487607881c66b2899b7e3477115229a156b4… Deepen the clone". This container's checkout is shallow. NOT MEASURED. The gate itself (without--self-test) ran green.pnpm check:cross-package-test-inputs— exit 1, pre-existing on the base tree, proven by control rather than asserted. With all seven changed paths restored to79a046f8c(and the changeset removed), the gate fails identically; the finding it prints namespackages/cli/test/init-created-files-summary.e2e.test.tsdescendingpackages/spec/dist/, and this diff touches neither that test, norturbo.json, nor any declaration table. Restore was proven bygit diff HEADempty plus agit hash-objectmatch against every HEAD blob.Lint, as a proven narrowing rather than a repo-wide sweep.
pnpm exec eslint --no-inline-config --format jsonover the two changed TypeScript files: exit 0, 2 files, 0 errors, 0 warnings, the file count read from the JSON output's own length. The population it narrows from is 6798 files — computed from ESLint's own resolved config viaESLint#isPathIgnoredovergit ls-files, not estimated. The narrowing excludes nothing, and that is a property of this repository's config rather than a hope:eslint.config.mjsstates it in its own comment — this repo "runs oneeslint.config.mjs, which never enables type-aware linting (noparserOptions.project, no typed@typescript-eslintrules) for ANY file, test or not." With no rule reading types across a file boundary, a diff confined to these two files cannot move the verdict on any of the other 6796. The repo-widepnpm lintrun remains CI's.Every reading above was taken at
6d01b4b, this branch's final commit, on a tree whosegit status --porcelainis empty.origin/mainhad not moved from the branch point (79a046f8c) when the PR was opened, so no merge was owed.Acceptance notes
packages/lintdoes not field-check the key this PR declares.POSITIONS.kanbaninpackages/lint/src/validate-list-view-field-refs.tslistsgroupByField(error),summarizeField(warning) andcolumns(warning) — and notitleField. Every sibling face lists it:calendar.titleFieldwarns,gantt.titleFieldandtimeline.titleFielderror. So from this release a stale or misspelledkanban.titleFieldnames nothing and is reported by nothing, while the identical typo one block away is caught. Read-only on this card by dispatch, so it is reported and not edited; nothing in this PR turns it red, and no gate reconciles that table against the spec's member lists. Worth its own card.KanbanSchemacarries three zero-read members (allowCollapse,cardTemplates,columnWidths) and the board reads an undeclaredtitleField— enforce-or-remove on the shape objectui#7664 declared objectui#7742) has no home here, and the card's conditional resolves to "no".ObjectKanbanPropsSchemainpackages/spec/src/ui/component.zod.tsalready declarestitleField: z.string().optional(), described as "Legacy fallback forcardTitle(the board readscardTitle || titleField). PrefercardTitle". That is a different semantic on a different schema in a different file — a deprecated alias forcardTitle, not a view-config field binding — so one declaration cannot serve both faces and this card is not widened. That file is also held by spec(ui):ComponentPropsMaphas no rows forobject-map,object-ganttandobject-tree— add them from the renderers' read points (#7751 method) so 「以协议为准」 resolves for all five ladder blocks (objectui#8348 Q1, ruled C) #18305 / PR feat(spec): object-map, object-gantt and object-tree get ComponentPropsMap rows, derived from the renderers' read points (objectui#8348 Q1-C) #18403; it was read, never touched.KanbanConfigSchemacarries no object-level.describe(), whereGalleryConfigSchema,TimelineConfigSchema,CalendarConfigSchemaandListMapConfigSchemaall do, so its nested-shape heading in the generated reference renders with no description sentence. Cosmetic, generated-docs only, no accept set involved — left for whichever card next edits this schema.Refs:line.File surface
Two files beyond the dispatch's declared surface, both generated by the mandated
gen:docsrun and neither hand-edited:content/docs/references/api/protocol.mdxandcontent/docs/references/data/object.mdx. The kanban config shape is inlined on those two reference pages as well as onui/view.mdx, so each picks uptitleField?: stringin itskanbanrow. Omitting them would leavecheck:docsred. Every hunk in all three files is this one key and nothing else.Generated by Claude Code