Skip to content

fix(lint): walk kanban.titleField at calendar's level — the one item-titled face with no position row - #18833

Merged
os-bill merged 2 commits into
mainfrom
claude/issue-18565-kanban-titlefield-position
Sep 18, 2026
Merged

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

Conversation

@os-bill

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

Copy link
Copy Markdown
Collaborator

Fixes #18565

POSITIONS in packages/lint/src/validate-list-view-field-refs.ts declares, per view face, which field-reference keys are walked and at what level. kanban was the only item-titled face with no titleField row. Since #16894 the key is authorable on KanbanConfigSchema, so a misspelt field name cleared the schema door, was walked by nothing, and left the board titled from the ADR-0079 display-name chain instead of the field the author named — a wrong value on a board that renders correctly, reported by nothing, while the byte-identical typo one block away on calendar or timeline was reported.

Clause-②: no

Adding a position row to an internal table. No published schema shape moves, nothing previously accepted is refused, and the authorable accept set stays packages/spec's — untouched here.

Where the symbols actually are

Anchored on symbols, not on the card's line numbers, and then compared with them. Read on this branch's base a7bafc29af:

symbol the card's table read on a7bafc29af
const POSITIONS — :310
kanban: block :322 :322 — scalars: { groupByField: 'error', summarizeField: 'warning' }, no titleField
calendar.titleField :332 :332
gantt.titleField :340 :340
timeline.titleField :359 :359
gallery / map titleField one row, :366 two rows: gallery :366, map :375

The card's numbers hold; only its last row merges two faces that sit nine lines apart.

Question 1 — which level, and why it is not name similarity

Every item-titled face's titleField row, side by side with the schema that declares the key:

face POSITIONS row (a7bafc29af) level declaration in packages/spec/src/ui/view.zod.ts the key is
kanban absent (block at :322) none :1467 z.string().optional() optional
calendar :332 warning :1523 z.string().optional() optional
gallery :366 warning :1142 z.string().optional() optional
map :375 warning :1761 z.string().optional() optional
timeline :359 error :1156 z.string() required
gantt :340 error :1606 z.string() required

The siblings do not in fact disagree per face. The split is exactly the schema's own requiredness: the two faces that spell titleField required take error, the three that spell it optional take warning. That is the rule's own two-tier line, applied — error is "the miss changes what data the view returns, or collapses the layout it configures", warning is "the renderer drops one decoration and renders the rest: an optional colour / title / tooltip / cover binding".

kanban's shape is measured isomorphic to the calendar row, on three counts:

  1. The schema. KanbanConfigSchema.titleField is z.string().optional() (:1467), the same as calendar's (:1523), not the bare z.string() of timeline (:1156) and gantt (:1606).
  2. The declaration's own words. spec(ui): declare titleField on KanbanConfigSchema — the one item-titled view config of five that omits the key its four siblings already declare (executes objectui#8367 ruling A, decision batch #87) #16894's docblock on that key says it is "OPTIONAL, deliberately — the shape CalendarConfigSchema already writes down for this exact key", and names TimelineConfigSchema and GanttConfigSchema as "the two siblings this declaration does NOT copy".
  3. The renderer, measured rather than assumed. In objectui at dda8f3815, packages/plugin-kanban/src/ObjectKanban.tsx resolves the card title as: resolveKanbanTitleField(schema) returns the written name, the card reads rec[titleField], gets nothing for a name no record carries, and falls through to getRecordDisplayName — "objectDef.titleFormat, objectDef.displayNameField, type-aware field derivation, Record #id floor" (its own comment, the ADR-0079 chain). The drawer heading takes the same resolver. Every card still renders; one decoration is wrong.

So: titleField: 'warning' on kanban. Compare kanban.groupByField, which is error on the other branch of that same line — a bad group-by collapses every card into one uncolumned lane, which is layout, not decoration.

Question 2 — does the new row red existing metadata? No. BEFORE reading taken.

Two instruments, each with its own non-zero control.

(a) The rule, run over the repo's own example apps, before and after. Stacks loaded from objectstack.config.ts (app-crm, app-todo, app-multi-package) and from the objects/views barrels (app-showcase):

corpus kanban blocks kanban blocks carrying titleField findings BEFORE findings AFTER
app-crm 2 0 0 0
app-todo 0 0 0 0
app-multi-package 0 0 0 0
app-showcase 3 0 0 0

Control that the walker really reaches these stacks: pointing an existing calendar.titleField at a bogus name reports warning list-view-field-unknown at views[0].listViews.calendar.calendar.titleField (crm) and views[5].list.calendar.titleField (showcase) — both BEFORE and AFTER.

(b) A repo-wide census of authored kanban blocks, brace-matched rather than line-matched, over every ts/tsx/js/mjs/cjs/json/mdx/md/yaml/yml file outside node_modules, dist, .git, .turbo, .cache: 82 kanban blocks, 26 of which mention titleField — and all 26 are generated JSON Schema under packages/spec/json-schema/**, i.e. the schema DECLARATION of the key from #16894, not authored view metadata. Non-zero controls from the same scanner on the same run: calendar 50, gantt 47, timeline 43, gallery 36, map 35 blocks carrying the key, first hits in real example views.

So the row starts walking a key that no authored view in this repository writes today: nothing existing turns red, and no view file needed editing (none was edited).

LIT — the behaviour flip

One list view carrying every walked position, one mutation at a time, rule source imported directly (no dist in the path, so no stale build can fake either reading):

probe BEFORE AFTER
kanban.titleField: 'ttle' (no such field) silent, 0 findings warning list-view-field-unknown at views[0].list.kanban.titleField — "titleField "ttle" is not a field on object "duly_task". Did you mean "title"? …"

The same flip on real corpus metadata: injecting titleField: 'zz_no_such_field' into the boards of app-crm and app-showcase reports 0 findings BEFORE and 1 warning AFTER, at views[1].listViews.pipeline.kanban.titleField and views[4].listViews.by_status.kanban.titleField.

DARK — nothing else moved

  • A kanban view whose titleField names a real field: 0 findings BEFORE, 0 findings AFTER.
  • The full clean surface (every walked position bound to a real field, kanban.titleField included): 0 findings BEFORE and AFTER.
  • All 52 positions probed one at a time: BEFORE 51 reported / 1 silent (that one silent position being kanban.titleField), AFTER 52 reported / 0 silent. Every one of the other 51 keeps its identical severity — the before/after diff of the probe output is three hunks, all three naming kanban.titleField and nothing else. The 51 non-zero readings are this probe's own control: the single zero was a true zero.

Changeset — decided by measuring published bytes

@objectstack/lint's files[] is ["dist", "README.md", "CHANGELOG.md"], so the question is whether this source file reaches dist. Measured, not inferred: summarizeField — a row of this very table — occurs in dist/index.js, dist/index.cjs and dist/runtime.js; positive control list-view-field-unknown occurs twice in each; negative control duly_task (a symbol that exists only in the test file) occurs zero times in each. After the change, dist/index.js:8605 reads scalars: { groupByField: "error", summarizeField: "warning", titleField: "warning" }. The change ships, so it carries a changeset (minor — a rule reports where it was silent, no id and no severity moves elsewhere).

Verification

Everything below at 02c18bfcc2, which is this PR's tip.

  • pnpm --filter @objectstack/lint exec vitest run --maxWorkers=2 src/validate-list-view-field-refs.test.ts — 113 passed.
  • pnpm --filter @objectstack/lint test — 104 files, 3885 tests passed.
  • pnpm --filter @objectstack/lint typecheck — tsc --noEmit plus check:test-typecheck, exit 0.
  • pnpm --filter '@objectstack/lint...' build — the package and its upstream closure, exit 0.
  • pnpm lint (eslint . --no-inline-config, the whole repo) — exit 0.
  • node scripts/pm/dispatch-gates.mjs --commands derived 58 families for this diff; all 58 were run and reconciled with --ran: 55 exit 0, 3 NOT MEASURED, 0 red. The three are check:dual-build-cjs-loads, check:lean-entry-closure and check:type-check-debt, each exiting 3 with PREREQUISITE NOT MET because they read a built dist for the whole workspace, which this worktree does not have (only lint's closure was built). CI builds that closure before those steps.

Acceptance notes

  • The test file was declared read-only by the dispatching claim, and this PR edits it anyway — in its own commit, 02c18bfcc2, so it can be dropped alone. The claim's open surface is the rule file plus derivatives; the executor's own binding rules require the change to carry test coverage, and the suite's own docblock calls its severity table "the readable half of the rule's own POSITIONS table". Measured before touching it: across all 28 open PRs, zero hold either validate-list-view-field-refs.ts or its test (live control: scripts/check-cross-package-test-inputs.mjs reads [18817]), so nothing collides. The rule commit stands alone without it and the suite is green either way. The seat decides.
  • The floor in that test (expect(cases.length).toBeGreaterThanOrEqual(47)) is a floor: it catches a case DROPPED from the test table, and it does not catch a position ADDED to the rule without a row here — which is what the docblock above it claims it does. Noted here and handed back, not folded in.
  • Four more field-naming keys are declared on view-face configs and walked by no POSITIONS row — measured silent with live controls, same class as this card, handed back rather than fixed here: calendar.allDayField, gantt.borderColorField, gantt.lockField, gantt.objectField.
  • The card's "more durable half" — whether POSITIONS should be reconciled against the spec member lists by a gate rather than by hand — is untouched here, as the card asks.

Generated by Claude Code

…iew position table

`POSITIONS` declares, per view face, which field-reference keys are walked and
at what level. `kanban` was the only item-titled face with no `titleField` row,
and since the key became authorable on `KanbanConfigSchema` a misspelt field
name cleared the schema door, was walked by nothing, and left the board titled
from the ADR-0079 display-name chain instead of the field the author named.

The level is `warning`, the one `calendar` takes: the key is OPTIONAL on both
schemas, and objectui's board (`resolveKanbanTitleField` -> `rec[titleField]`
-> `getRecordDisplayName`) renders every card regardless. `timeline` and
`gantt` spell the key required and take `error`; they are not the siblings this
row copies.

Claude-Session: https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3
Co-authored-by: Claude <noreply@anthropic.com>
…n table

The suite's clean fixture is "one list view carrying every position the rule
walks", and its severity table is "the readable half of the rule's own
POSITIONS table"; a position added to the rule without a row here is pinned by
nothing. This adds the fixture binding, the severity row and the floor that
goes with it.

⚠️ This file was declared read-only by the dispatching seat's claim, whose
open surface is the rule file plus derivatives. It is committed separately so
it can be dropped on its own if the seat judges the declaration should win; the
rule commit stands without it and the suite is green either way.

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 tests tooling labels Sep 17, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

Nothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 1 changed package(s)), so this run has no opinion about the docs.

What this run could not see
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • 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 — 4 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 55523fdee7a9a7ec73b7d56e6de973ffa228292a → packageMentionDocs.

@os-bill os-bill added domain:spec priority:p2 Medium: important, M3 labels Sep 17, 2026 — with Claude
@os-bill
os-bill marked this pull request as ready for review September 17, 2026 23:51
@os-bill
os-bill added this pull request to the merge queue Sep 17, 2026
Merged via the queue into main with commit 939f3ea Sep 18, 2026
44 checks passed
@os-bill
os-bill deleted the claude/issue-18565-kanban-titlefield-position branch September 18, 2026 00:13
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 domain:spec priority:p2 Medium: important, M3 size/s tests tooling

Projects

None yet

2 participants