Skip to content

spec(ui): declare titleField on KanbanConfigSchema — the optional key its five item-titled siblings already carry - #18561

Merged
os-bill merged 2 commits into
mainfrom
claude/issue-16894-kanban-titlefield
Sep 17, 2026
Merged

os-bill merged 2 commits into
mainfrom
claude/issue-16894-kanban-titlefield

Conversation

@os-bill

@os-bill os-bill commented Sep 17, 2026

Copy link
Copy Markdown
Collaborator

Fixes #16894

Clause-②: yes (widening)

KanbanConfigSchema now declares titleField as optional z.string(), executing the director seat's decision batch #87 (recorded at objectstack-ai/objectui#8367 comment 5582071618, confirmed by the maintainer verbatim 「批 #87 同意」). The direction was settled there; this PR is the implementation only.

What moved

file change
packages/spec/src/ui/view.zod.ts titleField: z.string().optional() on KanbanConfigSchema, plus the docblock that records why it is optional
packages/spec/src/ui/view.test.ts the card's four-leg probe (both controls firing on the same call shape) and the optionality leg
packages/spec/authorable-surface/ui.json regenerated — one added row, ui/KanbanConfig:titleField
content/docs/references/ui/view.mdx, content/docs/references/api/protocol.mdx, content/docs/references/data/object.mdx regenerated by gen:docs
.changeset/16894-kanban-config-titlefield.md @objectstack/spec: minor

Regenerated, never hand-edited: check:authorable-surface wrote the first and pnpm --filter @objectstack/spec gen:docs the rest. check:generated reports 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/main 79a046f8c (the dispatch's own derivation ran one commit behind, at 582d3e5). git grep -n 'titleField' -- packages/spec/src/ui/view.zod.ts returns six carriers; mapping each to the schema whose member list encloses it:

line (pre-change) owning schema arity
:1057 GalleryConfigSchema (declared :1050) optional
:1071 TimelineConfigSchema (declared :1065) required
:1407 CalendarConfigSchema (declared :1401) optional — the ADR-0079 fallback docblock the ruling names
:1490 GanttConfigSchema (declared :1484) required
:1638 ListMapConfigSchema (declared :1631) optional
KanbanConfigSchema (declared :1346) absent — the defect

Two corrections to the card body, neither of which disturbs the ruling:

  • The card's table calls Timeline optional. It is required on the tree.
  • The card's table lists five schemas. There are six carriers: ListMapConfigSchema also declares titleField, optional, and the card does not mention it.

The ruling is unaffected: it prescribes the arity directly (optional z.string()) and names CalendarConfigSchema'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: KanbanConfigSchema is a strictObject and refused titleField by name.

Evidence

leg command verdict
build pnpm --filter @objectstack/spec build VERDICT command-exit 0
② typecheck pnpm --filter @objectstack/spec typecheck TYPECHECK_EXIT=0check:test-typecheck: OK
② tests pnpm --filter @objectstack/spec test TEST_EXIT=0Test Files 483 passed (483) / Tests 13775 passed (13775)
targeted pnpm --filter @objectstack/spec exec vitest run --maxWorkers=2 src/ui/view.test.ts Test Files 1 passed (1) / Tests 376 passed (376)
artifacts pnpm --filter @objectstack/spec check:generated 15 of 15 up to date

① is empty by construction: packages/spec has no workspace dependencies, so --filter '@objectstack/spec^...' build has an empty closure. The package itself was built before any gate that reads dist/.

③ Gate families were derived on this worktree, not inherited: node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands over 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:

dispatch-gates --ran: 108 derived family(ies) accounted for — 105 run, 3 NOT-MEASURED (3 DERIVED from a recorded exit 3)

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-debtexit 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-testexit 1, but a prerequisite refusal in its own words: "cannot read the positive control at 621a487607881c66b2899b7e3477115229a156b4 … Deepen the clone". This container's checkout is shallow. NOT MEASURED. The gate itself (without --self-test) ran green.
  • pnpm check:cross-package-test-inputsexit 1, pre-existing on the base tree, proven by control rather than asserted. With all seven changed paths restored to 79a046f8c (and the changeset removed), the gate fails identically; the finding it prints names packages/cli/test/init-created-files-summary.e2e.test.ts descending packages/spec/dist/, and this diff touches neither that test, nor turbo.json, nor any declaration table. Restore was proven by git diff HEAD empty plus a git hash-object match against every HEAD blob.

Lint, as a proven narrowing rather than a repo-wide sweep. pnpm exec eslint --no-inline-config --format json over 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 via ESLint#isPathIgnored over git ls-files, not estimated. The narrowing excludes nothing, and that is a property of this repository's config rather than a hope: eslint.config.mjs states it in its own comment — this repo "runs one eslint.config.mjs, which never enables type-aware linting (no parserOptions.project, no typed @typescript-eslint rules) 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-wide pnpm lint run remains CI's.

Every reading above was taken at 6d01b4b, this branch's final commit, on a tree whose git status --porcelain is empty. origin/main had not moved from the branch point (79a046f8c) when the PR was opened, so no merge was owed.

Acceptance notes

File surface

Two files beyond the dispatch's declared surface, both generated by the mandated gen:docs run and neither hand-edited: content/docs/references/api/protocol.mdx and content/docs/references/data/object.mdx. The kanban config shape is inlined on those two reference pages as well as on ui/view.mdx, so each picks up titleField?: string in its kanban row. Omitting them would leave check:docs red. Every hunk in all three files is this one key and nothing else.


Generated by Claude Code

`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>
@github-actions github-actions Bot added size/s documentation Improvements or additions to documentation protocol:ui tests tooling labels Sep 17, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

1 anchor(s) derived from 1 changed package(s); no hand-written page names any of them. ⚠️ 1 changed file(s) yielded no anchor (packages/spec/authorable-surface/ui.json), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/spec/authorable-surface/ui.json) — pages documenting those are invisible to this run
  • the SDK route bridge reached 60 of 215 client-bound route-ledger rows — the other 155 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 155: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 100 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 136 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 79a046f8cdf085d95200826ee9bb2fa6584bc3d5packageMentionDocs.

Which tree this was computed on

This run read content/docs from 67d8299911aca37ad3db044fbe890779d35791c7 — the merge of head 6d01b4b63e07b5fbdc0069ce8f5143167b9813ff into base 79a046f8cdf085d95200826ee9bb2fa6584bc3d5, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

os-bill commented Sep 17, 2026

Copy link
Copy Markdown
Collaborator Author

Contract review

Served-tier: 30/30 CONTRACT_REVIEW_TIER
Head-sha: 6d01b4b63e07b5fbdc0069ce8f5143167b9813ff

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 53ded82b fetched for the renderer readings. Live re-read: the PR head is still 6d01b4b6, two commits on base 79a046f8; origin/main has moved 16 commits since that base and none of them touches any of the 7 changed paths (git diff --stat 79a046f8..origin/main over the seven paths is empty), so the head reviewed is the head that would merge.

① Derived judgments

Each accept-set or public-surface change the diff produces, named and judged:

  1. KanbanConfigSchema gains titleField: z.string().optional()packages/spec/src/ui/view.zod.ts:1370 at the head. Arity matches the ruled shape (optional, string, absence falls through). Judged RIGHT. Sibling table re-derived on the head, not quoted: Gallery :1057 optional, Timeline :1071 REQUIRED, Calendar :1426 optional, Gantt :1509 required, ListMap :1657 optional. The card's table (Timeline optional, five carriers) is wrong on both counts and the PR's correction is accurate; the ruling names the arity directly, so nothing in it is disturbed.
  2. Transitive accept sets: ListView.kanban, ObjectListView.kanban and the published metadata item body (api/protocol.mdx) widen by the same one key through schema reuse — one declaration, no second one anywhere. Judged RIGHT.
  3. packages/spec/authorable-surface/ui.json: exactly one added row, ui/KanbanConfig:titleField (file 1218 → 1219 lines; ui/KanbanConfig:* rows 3 → 4). No other shard, no authorable-surface.base.json re-anchor, no api-surface/ or api-surface-signatures.json movement (the latter hashes define* helpers only). Re-derived: pnpm --filter @objectstack/spec build then check:generated on the head worktree → 15 of 15 artifacts up to date, check:authorable-surface, check:api-surface, check:docs, check:liveness all ✓. Judged RIGHT, and regenerated rather than hand-edited.
  4. Generated reference pages: ui/view.mdx (+3 table rows in the KanbanConfig, ListView.kanban, ObjectListView.kanban nested-shape tables; 6 inline-shape lines rewritten), api/protocol.mdx (2 inline lines), data/object.mdx (1 inline line). Every hunk is this one key and nothing else — no smuggled second change. Judged RIGHT.
  5. Mechanical widening control: node scripts/pm/check-widening-tells.mjs --declaration yes --diff pr.diff → exit 0 (a yes is never blocked). The control run with --declaration no → exit 4 with exactly ONE tell, T1 at packages/spec/src/ui/view.zod.ts:1370, and no T2/T3/T4 — the diff carries one widening shape, and it is the declared one.
  6. The .describe() — "Field displayed as the card title. Omit to fall back to the record display name (ADR-0079 resolver chain)" — measured against the pinned renderer rather than assumed. The key IS read: plugin-list/src/ListView.tsx:1464 projects kanban.titleField into the fetch, and plugin-kanban/src/ObjectKanban.tsx:302-341 tries the explicit field first and, when it yields nothing, calls getRecordDisplayName — the ADR-0079 resolver (core/src/utils/record-title.ts:28-40: nameFielddisplayNameFieldtitleFormat → type-aware derivation → Record #id). So a layer delivers the promised fallback. ⚠️ One rung sits ahead of it at the pin: app-shell/src/views/ObjectView.tsx:2388 floors an omitted viewDef.kanban?.titleField at the literal 'name' before the node sees it, pinned by ObjectView.kanbanLane-8193.test.tsx:129 ("floors titleField at name"). objectui#7029 deleted exactly that rung for calendar, which is why the identical Calendar describe is exact and this one is exact only for records with no non-empty name value. The wording is the ruled wording (「exactly as the CalendarConfigSchema docblock already states」) and the residual rung is renderer-side in the sibling repo, so this does not refuse the spec change; it is escalated in ③ (d). Judged RIGHT as ruled, with the rung named.
  7. Tests (view.test.ts:190-223): the four-leg probe — CONTROL-1 bogus key refused on the named surface, CONTROL-2 canonical accepted, PROBE titleField accepted AND survives the parse as a member — and the optionality leg (key absent on output when omitted). Mirrors the card's executable acceptance criterion. Re-run here: vitest run src/ui/view.test.ts src/ui/view-metadata-schema.test.ts → 426 passed. eslint on the two changed TS files → exit 0. Judged RIGHT.
  8. Nothing else widens: no enum/union member, no export, no registry row, no ADR text (the ruling forbids one), no governed surface in the file list.

② Semver level

.changeset/16894-kanban-config-titlefield.md"@objectstack/spec": minor, carrying Clause-②: yes (widening) — the same declaration as the PR body line 3 and the card. Right level: a new optional key on a published strict accept set is a widening, yes takes at least minor, nothing admitted before is refused, nothing is renamed or retired, so no migration text, no tombstone and no ADR-0087 marker is owed. Gates run on the head tree: check-empty-changeset ✓ (1 declaring changeset added, none from the base modified), check-adr-0087-registration ✓ (non-breaking), check-changeset-no-major ✓. The changeset text matches the diff: the six-carrier sentence and the "Timeline and Gantt spell it required" sentence check against the head lines in ① item 1; the "generated projections" sentence names exactly the files that moved; "the key the board already reads" checks against ListView.tsx:1464 / ObjectKanban.tsx:303 at the pin. Judged RIGHT.

③ Boundary flags

  • (a) packages/lint gap — CONFIRMED on the head, and it is a metadata-authoring trap rather than a cosmetic note. validate-list-view-field-refs.ts:317-322 POSITIONS.kanban lists groupByField (error), summarizeField (warning), columns (warning) and no titleField, while calendar/gallery/map warn and gantt/timeline error on the same key. No test pins POSITIONS against the schema (the test table at validate-list-view-field-refs.test.ts:199-280 is hand-maintained), so CI stays green — and from this release a misspelled kanban.titleField is accepted by the spec, reported by nothing, and at the pin silently degrades to the resolver title (ObjectKanban.tsx:334-341). Demonstrated on the head lint sources with sibling controls: kanban: { titleField: "nope_field" } → no finding; the identical typo on kanban.summarizeField → warning, calendar.titleField → warning, gallery.titleField → warning, timeline.titleField → error (the lint suite itself: 112 passed). That is the ADR-0078 silently-inert shape; Prime Directive chore: version packages #10 says file, and the PR only noted it. Needs its own card (one-line fix: titleField: 'warning' in POSITIONS.kanban plus a test row). This seat's proxy refuses the search endpoint, so whether a card already exists is unverified — the adopting seat files or links one. Out of this card's declared scope; not a reason to refuse the diff.
  • (b) objectui#7742 conditional resolves to "no" — verified: component.zod.ts:2846 ObjectKanbanPropsSchema.titleField already exists as an optional legacy alias of cardTitle on a different schema with a different semantic, so one declaration cannot serve both faces and the card is correctly not widened. Agreed.
  • (c) KanbanConfigSchema carries no object-level .describe() where its siblings do — cosmetic, generated-docs only, out of scope. Agreed.
  • (d) NEW — renderer rung ahead of the ruled fallback (① item 6): objectui pin ObjectView.tsx:2388 || 'name'. When the objectui leg of objectui#8367 declares the member and narrows the refused residual (KNOWN_REFUSED_RESIDUAL in ObjectView.kanbanGroupByRetired-8213.test.tsx:102['cardFields']), it should also drop that floor as objectui#7029 did for calendar, so that "omit" reaches the ADR-0079 chain directly and the describe is exact. Sibling-repo work; escalated, not blocking here.
  • (e) NEW, cosmetic — the new TSDoc docblock's chain (titleFormatdisplayNameField → derivation → 'Untitled') is copied verbatim from Calendar's [spec: appearance.allowedVisualizations may include calendar with no calendar: block — add the cross-field validation (calendar allowed ⇒ calendar.startDateField required) #13817] docblock via the ruling's own quotation; ADR-0079 D2/D3/D6 in this repo and objectui record-title.ts:28-40 order it nameField first and floor at Record #id, with 'Untitled' only for an id-less record. Not in the .describe() that ships to the reference page; leave it with the sibling docblock it copies.
  • (f) Downstream unblock is installability, not merge: objectui#8367 stays blocked until a released @objectstack/spec carrying this is pinned in objectui. Nothing for this PR to do.
  • Gate families this seat could not measure here: check:react-declaration-parity (needs the objectui manifest — declared by check:generated as external input), the lint package's own vitest run (measured separately, see (a)). CI on the head: 34 success / 5 skipped / 0 failure, including Spec property liveness, Lint & Repo Gates, Test Core (6/6), TypeScript Type Check, Governed Surface Queue Guard.

Implemented-by: claude/issue-16894-kanban-titlefield
Reviewed-by: session_01JbZnqu8bt6YqfJsr9vaFb3

VERDICT: PASS


Generated by Claude Code

os-bill commented Sep 17, 2026

Copy link
Copy Markdown
Collaborator Author

Clause-② adopted — PASS; both carriers stripped in this act

Review of record: PR-thread comment 5710778226.
Head judged: 6d01b4b63e07b5fbdc0069ce8f5143167b9813ff — re-read live at adoption and unmoved (draft: true, mergeable_state: clean), so the head judged is the head that would merge.

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.

needs:contract-review is stripped from both carriers in this same act: PR #18561 and card #16894.

Independence pair

  • Implemented-by: claude/issue-16894-kanban-titlefield — the branch that produced the diff (mode:subagent).
  • Reviewed-by: session_01JbZnqu8bt6YqfJsr9vaFb3 — the seat that renders/adopts. An isolated review subagent holds no session of its own, so contract-review.md records the adopting seat.
  • Different identities ⇒ no SELF-REVIEW.

Why an at-tier review exists here at all

This seat measures below CONTRACT_REVIEW_TIER (scripts/pm/dispatch-gates.mjs), so it is ⛔ barred from self-reviewing a clause-② card. It ran an isolated at-tier reviewer under the rule in #18511 — 「未达档席 ⛔ 不自审,起隔离达档子代理转录核档采信」 — on the maintainer's explicit instruction for this PR. That rule is not yet merged; this is stated plainly rather than implied.

Tier evidence, in the shape that rule demands: the reviewer parsed every type:"assistant" line of its own transcript for the harness-stamped per-request message.model and compared each against CONTRACT_REVIEW_TIER imported live from dispatch-gates.mjs — 30/30 at tier at posting, 32/32 on the final line, exactly one distinct model value, no fallback line anywhere in the run. ⛔ get_session was not used: inside mode:subagent it measures the dispatching session and is not mutual proof. The Served-tier: line of the record carries that 30/30 stamp control, and check-clause2-carriers --pair 18561 re-reads it at C7.

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

# check reading
in-seat clause-② PASS on record comment 5710778226, at-tier, Head-sha matches
double carrier check-clause2-carriers.mjs --pair 18561exit 0 (both limbs agree, no widening tell, record on head) — re-run after the strip
PR checks 39 check-runs: 34 success · 5 skipped · 0 other; check-expected-skips.mjs --pr 18561exit 0, all 5 skips in the roster (Auto Label, Check PR Size, Console Pin Gate, Packed-tarball smoke (opt-in) ×2)
governed surface check-governed-merges.mjs --pr 18561 → 0 of 7 paths hit the register ⇒ NOT governed; ordinary queue landing applies, ⛔ not the governed four-piece

Next: ready → auto-merge, then followed to MERGED.

[EDIT — added after posting, ⛔ not a re-measurement dressed as the original] The sentence above says the #18511 rule is "not yet merged". That was true when this comment was posted and is now stale: origin/main was re-fetched for unrelated work minutes later and its tip reads ab1d359259 skills(pm-dispatch): off-tier seat discharges clause-② review by spawning an at-tier subagent (ruling C) (#18511) — the rule is MERGED. Nothing about the review or this adoption changes; the rule this seat invoked simply stopped being provisional. Recorded rather than quietly edited away.


Generated by Claude Code

@os-bill
os-bill marked this pull request as ready for review September 17, 2026 07:45
@os-bill
os-bill enabled auto-merge September 17, 2026 07:45
@os-bill
os-bill added this pull request to the merge queue Sep 17, 2026
Merged via the queue into main with commit 0bd7dae Sep 17, 2026
44 checks passed
@os-bill
os-bill deleted the claude/issue-16894-kanban-titlefield branch September 17, 2026 08:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation protocol:ui size/s tests tooling

Projects

None yet

2 participants