Repository navigation
fix(lint): walk kanban.titleField at calendar's level — the one item-titled face with no position row - #18833
Merged
Conversation
…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>
Contributor
📓 Docs Drift CheckNothing 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
Coarse fallback — 4 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): |
This was referenced Sep 17, 2026
os-bill
marked this pull request as ready for review
September 17, 2026 23:51
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #18565
POSITIONSinpackages/lint/src/validate-list-view-field-refs.tsdeclares, per view face, which field-reference keys are walked and at what level.kanbanwas the only item-titled face with notitleFieldrow. Since #16894 the key is authorable onKanbanConfigSchema, 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 oncalendarortimelinewas 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:a7bafc29afconst POSITIONS:310kanban:block:322:322—scalars: { groupByField: 'error', summarizeField: 'warning' }, notitleFieldcalendar.titleField:332:332gantt.titleField:340:340timeline.titleField:359:359gallery/maptitleField:366:366, map:375The 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
titleFieldrow, side by side with the schema that declares the key:POSITIONSrow (a7bafc29af)packages/spec/src/ui/view.zod.ts:322):1467z.string().optional():332warning:1523z.string().optional():366warning:1142z.string().optional():375warning:1761z.string().optional():359error:1156z.string():340error:1606z.string()The siblings do not in fact disagree per face. The split is exactly the schema's own requiredness: the two faces that spell
titleFieldrequired takeerror, the three that spell it optional takewarning. That is the rule's own two-tier line, applied —erroris "the miss changes what data the view returns, or collapses the layout it configures",warningis "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:
KanbanConfigSchema.titleFieldisz.string().optional()(:1467), the same as calendar's (:1523), not the barez.string()of timeline (:1156) and gantt (:1606).titleFieldonKanbanConfigSchema— 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 shapeCalendarConfigSchemaalready writes down for this exact key", and namesTimelineConfigSchemaandGanttConfigSchemaas "the two siblings this declaration does NOT copy".dda8f3815,packages/plugin-kanban/src/ObjectKanban.tsxresolves the card title as:resolveKanbanTitleField(schema)returns the written name, the card readsrec[titleField], gets nothing for a name no record carries, and falls through togetRecordDisplayName— "objectDef.titleFormat, objectDef.displayNameField, type-aware field derivation,Record #idfloor" (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'onkanban. Comparekanban.groupByField, which iserroron 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):titleFieldapp-crmapp-todoapp-multi-packageapp-showcaseControl that the walker really reaches these stacks: pointing an existing
calendar.titleFieldat a bogus name reportswarning list-view-field-unknownatviews[0].listViews.calendar.calendar.titleField(crm) andviews[5].list.calendar.titleField(showcase) — both BEFORE and AFTER.(b) A repo-wide census of authored
kanbanblocks, brace-matched rather than line-matched, over everyts/tsx/js/mjs/cjs/json/mdx/md/yaml/ymlfile outsidenode_modules,dist,.git,.turbo,.cache: 82kanbanblocks, 26 of which mentiontitleField— and all 26 are generated JSON Schema underpackages/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
distin the path, so no stale build can fake either reading):kanban.titleField: 'ttle'(no such field)warninglist-view-field-unknownatviews[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 ofapp-crmandapp-showcasereports 0 findings BEFORE and 1warningAFTER, atviews[1].listViews.pipeline.kanban.titleFieldandviews[4].listViews.by_status.kanban.titleField.DARK — nothing else moved
titleFieldnames a real field: 0 findings BEFORE, 0 findings AFTER.kanban.titleFieldincluded): 0 findings BEFORE and AFTER.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 namingkanban.titleFieldand 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'sfiles[]is["dist", "README.md", "CHANGELOG.md"], so the question is whether this source file reachesdist. Measured, not inferred:summarizeField— a row of this very table — occurs indist/index.js,dist/index.cjsanddist/runtime.js; positive controllist-view-field-unknownoccurs twice in each; negative controlduly_task(a symbol that exists only in the test file) occurs zero times in each. After the change,dist/index.js:8605readsscalars: { 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 --noEmitpluscheck: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 --commandsderived 58 families for this diff; all 58 were run and reconciled with--ran: 55 exit 0, 3 NOT MEASURED, 0 red. The three arecheck:dual-build-cjs-loads,check:lean-entry-closureandcheck:type-check-debt, each exiting 3 withPREREQUISITE NOT METbecause they read a builtdistfor the whole workspace, which this worktree does not have (only lint's closure was built). CI builds that closure before those steps.Acceptance notes
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 eithervalidate-list-view-field-refs.tsor its test (live control:scripts/check-cross-package-test-inputs.mjsreads[18817]), so nothing collides. The rule commit stands alone without it and the suite is green either way. The seat decides.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.POSITIONSrow — measured silent with live controls, same class as this card, handed back rather than fixed here:calendar.allDayField,gantt.borderColorField,gantt.lockField,gantt.objectField.POSITIONSshould 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