Repository navigation
Commit 939f3ea
fix(lint): walk
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.** #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](https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3)_
---------
Co-authored-by: Claude <noreply@anthropic.com>kanban.titleField at calendar's level — the one item-titled face with no position row (#18833)1 parent 42f8df1 commit 939f3ea
3 files changed
Lines changed: 44 additions & 3 deletions
File tree
- .changeset
- packages/lint/src
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
75 | 75 | | |
76 | 76 | | |
77 | 77 | | |
78 | | - | |
| 78 | + | |
79 | 79 | | |
80 | 80 | | |
81 | 81 | | |
| |||
227 | 227 | | |
228 | 228 | | |
229 | 229 | | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
230 | 234 | | |
231 | 235 | | |
232 | 236 | | |
| |||
281 | 285 | | |
282 | 286 | | |
283 | 287 | | |
284 | | - | |
| 288 | + | |
285 | 289 | | |
286 | 290 | | |
287 | 291 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
322 | 322 | | |
323 | 323 | | |
324 | 324 | | |
325 | | - | |
| 325 | + | |
| 326 | + | |
| 327 | + | |
| 328 | + | |
| 329 | + | |
| 330 | + | |
| 331 | + | |
| 332 | + | |
| 333 | + | |
| 334 | + | |
| 335 | + | |
| 336 | + | |
| 337 | + | |
| 338 | + | |
| 339 | + | |
326 | 340 | | |
327 | 341 | | |
328 | 342 | | |
| |||
0 commit comments