Skip to content

Commit a43b9d0

Browse files
os-billclaude
andauthored
fix(lint): walk the four list-view field-naming keys that had no position row (#18860)
Fixes #18835 Clause-②: no (narrowing) `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. Four declared, authorable field-naming keys had no row at all — `calendar.allDayField` (`view.zod.ts:1552`) and the three objectui-lifted `gantt` bindings `borderColorField` (`:1660`), `lockField` (`:1661`) and `objectField` (`:1662`), every one of them `z.string().optional()`. A misspelt field name at any of them cleared the schema door, was walked by nothing, and was dropped by the renderer in silence. Each now has a row. ## The tier is per key, and deliberately not per schema shape All four are `.optional()`. That is the one thing they share, and it is not what tiers them: each is tiered by what its own renderer does with a name no record carries, read off the key's own `.describe()` and measured in objectui `dda8f3815` (the pin this repo builds against is `53ded82b`; the two gantt readings below are unchanged between them — the code is `plugin-gantt/src/ObjectGantt.tsx` in both). | key | tier | what a misspelt name does | |:--|:--|:--| | `calendar.allDayField` | `warning` | `ObjectCalendar` maps every event with `allDay: allDayField ? Boolean(record[allDayField]) : !endDate`. A name no record carries reads `undefined` on every row, so nothing is banded — and the renderer's own no-end-date inference is switched off too, because a DECLARED key is absolute (the spec's own "⛔ No default field name" note). Every event still renders, at its start time: the all-day band is the one decoration dropped. | | `gantt.borderColorField` | `warning` | `borderColorRaw = borderColorField ? record[borderColorField] : undefined` leaves `borderColor` undefined for every task. Every bar keeps its fill and renders without its alert outline — `colorField`'s case, eight rows up. | | `gantt.lockField` | **`error`** | A declared WRITE GUARD that fails OPEN. `locked: lockField ? !!record[lockField] : undefined` reads `undefined` on every row, and the drawer's `recLocked` falls the same way, so every row the author froze becomes draggable, resizable, progress-draggable, link-able, inline-editable and deletable — and the drag persists. | | `gantt.objectField` | **`error`** | `isSyntheticRow` is `!!objectField && !String(rec[objectField] ?? '').trim()`, so a name no record carries answers TRUE for every row. `onTaskClick` then never calls `navigation.handleClick` and `renderRecordOverlay` returns null: no bar in the chart opens a drawer or a detail page. | ## The two `error` rows are a consequence the rule's severity note did not name Its two tiers read "the miss changes what data the view returns, or collapses the layout it configures" against "the renderer drops one decoration and renders the rest". `lockField` and `objectField` are neither: nothing is missing from the picture, which is exactly why they gate. The note now names the third consequence — a binding whose job is to RESTRICT or to ROUTE, where the miss is read as "no restriction" / "no route" on every row. Both are the shape Prime Directive #10 names, a capability advertised in the metadata and not delivered by the runtime, and both are worse DECLARED than omitted, because each renderer guards its behaviour on the key's mere presence. `allDayField` is deliberately NOT put in that third class: no restriction and no route rides on it, and a wrongly-banded event is still an event the user can click. ## Controls — both arms, all four keys separately One instrument, run against the rule before the edit and after it, over one list view carrying every walked position, one mutation at a time. The diff of the two runs is four lines long. ### LIT — each key pointed at a field that does not exist | probe | before | after | |:--|:--|:--| | `calendar.allDayField` | SILENT | `warning` `list-view-field-unknown` at `views[0].list.calendar.allDayField` | | `gantt.borderColorField` | SILENT | `warning` `list-view-field-unknown` at `views[0].list.gantt.borderColorField` | | `gantt.lockField` | SILENT | `error` `list-view-field-unknown` at `views[0].list.gantt.lockField` | | `gantt.objectField` | SILENT | `error` `list-view-field-unknown` at `views[0].list.gantt.objectField` | Four lit for four. No key was proven by a neighbour: each is its own mutation, its own path assertion and its own tier assertion, and the suite-level pin puts all four misses on one stack and asserts four findings rather than one. ### DARK — must read zero, and does | probe | before | after | |:--|:--|:--| | the 53 already-walked position probes | 53 reported, each at its severity | the same 53, each at its same severity — byte-identical lines | | each of the four keys pointed at a REAL field, one at a time | SILENT | SILENT | | the clean fixture, all four bound to real fields | 0 findings | 0 findings | | the four keys carrying a dotted path whose head resolves | no finding | no finding (they join `POSITIONS` and deliberately not `DOTTED_AXIS`; pinned) | ## Does anything in this repository redden? No, and it is a proof rather than a sample The four rows read exactly four keys. `git grep` over every tracked file finds those key names only in the spec declaration, its own schema tests, the generated reference docs, the changesets that landed them, and unrelated identifiers (`objectFieldLabelKey`, `objectFieldTypeRefusal`). `git grep` over `examples/` matches nothing at all. No example app, fixture or seed in this tree authors any of the four, so no existing declaration can start reporting — 0 findings before, 0 after, with no declaration data touched. ## Completeness, since this was a hole in a table After these four rows, every `*Field` / `*Fields` key declared on `ListViewShapeSchema` and on each of the seven view-face config schemas (`Kanban`, `Calendar`, `Gantt`, `Timeline`, `Gallery`, `ListMap`, `Tree`) has either a `POSITIONS` row or a named owner elsewhere (`searchableFields`, `sort`). Enumerated from `view.zod.ts` key by key, not recalled. ## Verification Measured on `496b71de99`, this branch merged with `origin/main`: - `pnpm --filter @objectstack/lint exec vitest run src/validate-list-view-field-refs.test.ts` — 130 passed. - `pnpm --filter @objectstack/lint test` — 104 files, 3902 tests, all passed. - `pnpm --filter @objectstack/lint typecheck` — green (`tsc --noEmit` plus `check:test-typecheck`). - `pnpm --filter '@objectstack/lint^...' build` and `pnpm --filter '@objectstack/lint...' build` — green. - `pnpm exec eslint . --no-inline-config` over the WHOLE repository — exit 0, 6846 files linted, 0 errors, 0 warnings. Not a narrowing: the repo-wide run is the reading. - The gate families derived from this diff by `scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` on the merged tree — 58 commands, **54 green**. - ⊘ NOT MEASURED, 4 of the 58, every one of them `PREREQUISITE NOT MET` (exit 3, which is neither a pass nor a finding) for want of a whole-workspace build this worktree does not carry: `check:dual-build-cjs-loads`, `check:lean-entry-closure`, `check:type-check-debt`, and `check:docs-transcript-drift` — the last was re-run after building `@objectstack/lint` and is **green**, so three remain. CI builds before it runs them. ## Acceptance notes - **`gantt.objectField`'s own headline use case is out of this rule's reach, by construction.** Its `.describe()` frames it for "an api provider composing parent-object rows with child-object rows", and this rule's fourth skip drops any list view whose `data.provider` is not `object` — such a view binds to no object graph, so no field name on it is resolvable here. The row still earns its place: `isSyntheticRow` runs whatever the provider is, so an object-provider list view that declares a misspelt `objectField` loses every drawer in the chart, and that is the case the row reports. Noted, not filed — nothing here is a defect, it is the skip doing what it says. - **The floor assertion's remaining slack is untouched.** It moved 47 → 51, by exactly the four cases added; the table now holds 52. Whether the floor should be tight against the table is the separate card's question and its semantics were not altered here. - **`calendar-view-renderer.tsx` in objectui defaults `allDayField` to the literal `allDay`**, where `ObjectCalendar` has no default and the spec forbids one. Checked and it is not a contradiction: that renderer serves the `calendar-view` node with inline `schema.data`, a different surface from a list view's `calendar` block, and it defaults every one of its five field names the same way. Noted, not filed. --- _Generated by [Claude Code](https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3)_ Co-authored-by: Claude <noreply@anthropic.com>
1 parent e8ba892 commit a43b9d0

3 files changed

Lines changed: 233 additions & 2 deletions

File tree

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
---
2+
"@objectstack/lint": minor
3+
---
4+
5+
fix(lint)!: `list-view-field-unknown` walks the four field-naming keys that had no position row at all
6+
7+
Clause-②: no (narrowing)
8+
9+
**BREAKING** — an accept-set narrowing on the list-view authoring surface. Four declared, authorable field-naming keys had no row in `POSITIONS` in `validate-list-view-field-refs.ts`, so a misspelt field name at any of them cleared the schema door, was walked by nothing, and was dropped by the renderer. From this release each is judged, and two of the four gate `validate` and `build`, so a stack that built yesterday with one of those two misspelt does not build now. Shipped as `minor` under the repo's launch-window convention for accept-set narrowings.
10+
11+
<!-- adr-0087: not-required (no-migration-prescription) nothing is removed and nothing is renamed. Every spelling that parsed before still parses, no stored value has to be rewritten to a different one, and the correct name is whatever the bound object declares -- so there is no mapping for the ledger to carry and `objectstack migrate meta` has nothing to apply. -->
12+
13+
## The keys, and why the tier is not the same for all four
14+
15+
All four are `z.string().optional()` on their config schema, and the schema shape is deliberately not what tiers them — the tier is the consequence, read per key off its own `.describe()` and its renderer (measured in objectui `dda8f3815`).
16+
17+
| key | declared at | tier | what a misspelt name does |
18+
|:--|:--|:--|:--|
19+
| `calendar.allDayField` | `CalendarConfigSchema` | `warning` | `ObjectCalendar` maps each event with `allDay: allDayField ? Boolean(record[allDayField]) : !endDate`, so every row reads `undefined` and no event is banded — and the renderer's own no-end-date inference is switched off by the key's mere presence. Every event still renders, at its start time: one decoration dropped. |
20+
| `gantt.borderColorField` | `GanttConfigSchema` | `warning` | `borderColorRaw = borderColorField ? record[borderColorField] : undefined` leaves `borderColor` undefined for every task. Every bar keeps its fill and renders without its alert outline — `colorField`'s case. |
21+
| `gantt.lockField` | `GanttConfigSchema` | **`error`** | A declared WRITE GUARD that fails OPEN. `locked: lockField ? !!record[lockField] : undefined` reads `undefined` on every row, and the drawer's `recLocked` falls the same way, so every row the author froze becomes draggable, resizable, progress-draggable, link-able, inline-editable and deletable — and the drag persists. |
22+
| `gantt.objectField` | `GanttConfigSchema` | **`error`** | `isSyntheticRow` is `!!objectField && !String(rec[objectField] ?? '').trim()`, so a name no record carries answers TRUE for every row. `onTaskClick` never calls `navigation.handleClick` and `renderRecordOverlay` returns null: no bar in the chart opens a drawer or a detail page. |
23+
24+
The two `error` rows are a consequence the rule's own severity note did not name and now does: a binding whose job is to RESTRICT or to ROUTE, where the miss is read as "no restriction" / "no route" on every row. Nothing is missing from the picture, which is exactly why it gates — it is the shape Prime Directive #10 names, a capability advertised in the metadata and not delivered by the runtime. Both are also worse DECLARED than omitted, because each renderer guards its behaviour on the key's mere presence.
25+
26+
## Measured, one list view carrying every walked position, one mutation at a time
27+
28+
| probe | before | after |
29+
|:--|:--|:--|
30+
| `calendar.allDayField` naming a field that does not exist | silent | `warning` at `views[0].list.calendar.allDayField` |
31+
| `gantt.borderColorField` naming a field that does not exist | silent | `warning` at `views[0].list.gantt.borderColorField` |
32+
| `gantt.lockField` naming a field that does not exist | silent | `error` at `views[0].list.gantt.lockField` |
33+
| `gantt.objectField` naming a field that does not exist | silent | `error` at `views[0].list.gantt.objectField` |
34+
| each of the four naming a REAL field | silent | silent |
35+
| the other 53 walked-position probes | 53 reported, each at its severity | the same 53, each at its same severity |
36+
| the clean fixture carrying all four bound to real fields | 0 findings | 0 findings |
37+
38+
Over this repository's own tree the finding count is **0 before and 0 after**: no example app, fixture or seed authors any of the four keys at all (`git grep` over every tracked file finds the spec declaration, its own schema tests and the generated reference docs, and nothing else), so nothing existing starts reporting.
39+
40+
## What an author does about a report
41+
42+
Nothing is renamed and nothing is removed — every spelling that was valid is still valid, and no stored value has to be rewritten to a different one. What changes is that a name which resolves to no field on the bound object is now reported instead of being dropped in silence.
43+
44+
There is no mapping to apply, and deliberately so: the correct spelling is whatever the bound object declares, which only that object knows. The remedy is always the same — name a field the object actually has, or drop the key — and the finding carries the object's own field list plus a "did you mean" suggestion, so the message itself names the spelling to write.
45+
46+
## Scope — what is deliberately NOT changed
47+
48+
- **No dotted verdict.** The four positions join `POSITIONS` and deliberately not `DOTTED_AXIS`: none of them reaches a query door this rule measured, so a dotted name at one of them is unjudged, exactly as every other renderer binding is. Pinned.
49+
- **No other surface.** `listViews`, `recordTypes` and the other label/field-naming surfaces are untouched; widening to them is a measurement, not a corollary.
50+
- **No new rule id, no message shape change, no severity change for any existing position.** `list-view-field-unknown` gains four more places it can be reported from.

‎packages/lint/src/validate-list-view-field-refs.test.ts‎

Lines changed: 131 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,14 @@ const OBJECTS = [
4646
{ name: 'payload', type: 'json', label: 'Payload' },
4747
{ name: 'score', type: 'formula', label: 'Score' },
4848
{ name: 'tags', type: 'text', multiple: true, label: 'Tags' },
49+
// [#18835] The targets for the four field-naming keys that had no
50+
// position row, each declared at the type its own `.describe()` names —
51+
// `allDayField` / `lockField` say "boolean field", `objectField` carries
52+
// an object api name and `borderColorField` a colour string.
53+
{ name: 'is_all_day', type: 'boolean', label: 'All day' },
54+
{ name: 'is_locked', type: 'boolean', label: 'Locked' },
55+
{ name: 'alert_color', type: 'text', label: 'Alert colour' },
56+
{ name: 'row_object', type: 'text', label: 'Row object' },
4957
],
5058
},
5159
{
@@ -81,6 +89,7 @@ const FULL_LIST_VIEW: AnyRec = {
8189
endDateField: 'visible_from',
8290
titleField: 'title',
8391
colorField: 'status',
92+
allDayField: 'is_all_day',
8493
},
8594
gantt: {
8695
startDateField: 'visible_from',
@@ -98,6 +107,9 @@ const FULL_LIST_VIEW: AnyRec = {
98107
effortField: 'estimate',
99108
tooltipFields: ['status', { field: 'business_unit' }],
100109
quickFilters: [{ field: 'status' }],
110+
borderColorField: 'alert_color',
111+
lockField: 'is_locked',
112+
objectField: 'row_object',
101113
},
102114
timeline: {
103115
startDateField: 'visible_from',
@@ -235,6 +247,10 @@ describe('#14107 — every other walked position', () => {
235247
[{ calendar: { titleField: BAD } }, 'views[0].list.calendar.titleField', 'warning'],
236248
[{ calendar: { colorField: BAD } }, 'views[0].list.calendar.colorField', 'warning'],
237249
[{ calendar: { startDateField: BAD } }, 'views[0].list.calendar.startDateField', 'error'],
250+
// [#18835] A declared `allDayField` is absolute — it switches the
251+
// renderer's own all-day inference off — so a miss un-bands every event.
252+
// They all still render, at their start time: one decoration.
253+
[{ calendar: { allDayField: BAD } }, 'views[0].list.calendar.allDayField', 'warning'],
238254
[{ gantt: { endDateField: BAD } }, 'views[0].list.gantt.endDateField', 'error'],
239255
[{ gantt: { titleField: BAD } }, 'views[0].list.gantt.titleField', 'error'],
240256
[{ gantt: { progressField: BAD } }, 'views[0].list.gantt.progressField', 'warning'],
@@ -247,6 +263,13 @@ describe('#14107 — every other walked position', () => {
247263
[{ gantt: { groupByField: BAD } }, 'views[0].list.gantt.groupByField', 'warning'],
248264
[{ gantt: { assigneeField: BAD } }, 'views[0].list.gantt.assigneeField', 'warning'],
249265
[{ gantt: { effortField: BAD } }, 'views[0].list.gantt.effortField', 'warning'],
266+
// [#18835] The three objectui-lifted field bindings, on three different
267+
// levels — see each row's note in the rule. The stroke is a decoration;
268+
// the lock is a write guard that fails open; `objectField` makes every row
269+
// answer the synthetic-row test, so nothing in the chart opens.
270+
[{ gantt: { borderColorField: BAD } }, 'views[0].list.gantt.borderColorField', 'warning'],
271+
[{ gantt: { lockField: BAD } }, 'views[0].list.gantt.lockField', 'error'],
272+
[{ gantt: { objectField: BAD } }, 'views[0].list.gantt.objectField', 'error'],
250273
[{ gantt: { tooltipFields: [BAD] } }, 'views[0].list.gantt.tooltipFields[0]', 'warning'],
251274
[
252275
{ gantt: { tooltipFields: [{ field: BAD }] } },
@@ -284,8 +307,104 @@ describe('#14107 — every other walked position', () => {
284307

285308
// A floor, so a position quietly dropped from the rule's table cannot pass
286309
// by simply never being asserted.
310+
//
311+
// [#18835] Raised by the four rows this card adds — `calendar.allDayField`
312+
// and the three objectui-lifted `gantt` bindings. The number moves by what
313+
// was added and nothing else; the floor's own semantics are untouched.
287314
it('covers every position the rule walks', () => {
288-
expect(cases.length).toBeGreaterThanOrEqual(47);
315+
expect(cases.length).toBeGreaterThanOrEqual(51);
316+
});
317+
});
318+
319+
/**
320+
* [#18835] The four declared, authorable field-naming keys that had no row in
321+
* `POSITIONS` at all: `calendar.allDayField` and the three objectui-lifted
322+
* `gantt` bindings (`borderColorField`, `lockField`, `objectField`). Each was
323+
* admitted by the schema, walked by nothing, and dropped by the runtime.
324+
*
325+
* All four are `.optional()`, and that is deliberately NOT what tiers them —
326+
* the tier is the consequence, read per key off its own `.describe()` and its
327+
* renderer (objectui `dda8f3815`). Two land in each tier, and the block below
328+
* asserts the tiers where they are felt: `validate` / `build`.
329+
*
330+
* Each key is pinned SEPARATELY, in both directions. One key proven does not
331+
* generalise to the other three: they are four different renderer behaviours
332+
* that happen to share a schema shape.
333+
*/
334+
describe('#18835 — the four keys with no position row', () => {
335+
const KEYS: Array<[string, (bad: string) => AnyRec, string, 'error' | 'warning', string]> = [
336+
[
337+
'calendar.allDayField',
338+
(v) => ({ calendar: { allDayField: v } }),
339+
'views[0].list.calendar.allDayField',
340+
'warning',
341+
'is_all_day',
342+
],
343+
[
344+
'gantt.borderColorField',
345+
(v) => ({ gantt: { borderColorField: v } }),
346+
'views[0].list.gantt.borderColorField',
347+
'warning',
348+
'alert_color',
349+
],
350+
[
351+
'gantt.lockField',
352+
(v) => ({ gantt: { lockField: v } }),
353+
'views[0].list.gantt.lockField',
354+
'error',
355+
'is_locked',
356+
],
357+
[
358+
'gantt.objectField',
359+
(v) => ({ gantt: { objectField: v } }),
360+
'views[0].list.gantt.objectField',
361+
'error',
362+
'row_object',
363+
],
364+
];
365+
366+
for (const [label, patch, path, severity, realField] of KEYS) {
367+
it(`${label} naming a field that does not exist reports at \`${severity}\``, () => {
368+
const findings = validateListViewFieldRefs(stackWith(mutate(patch('zz_no_such_field'))));
369+
expect(idsOf(findings)).toEqual([path]);
370+
expect(findings[0].rule).toBe(LIST_VIEW_FIELD_UNKNOWN);
371+
expect(findings[0].severity).toBe(severity);
372+
expect(findings[0].message).toContain('is not a field on object "duly_task"');
373+
expect(findings[0].hint).toContain('Fields on "duly_task"');
374+
});
375+
376+
// The dark half, per key: a real field is silent, so the rows report the
377+
// MISS and not the key's presence.
378+
it(`${label} naming a real field ("${realField}") stays silent`, () => {
379+
expect(validateListViewFieldRefs(stackWith(mutate(patch(realField))))).toEqual([]);
380+
});
381+
382+
const arm = severity === 'error' ? 'gates' : 'advises';
383+
it(`${label} ${arm} \`build\``, () => {
384+
const normalized = stackWith(mutate(patch('zz_no_such_field')));
385+
const { errors, advisories } = splitBySeverity(runAuthoringRules('build', { normalized }));
386+
const gated = errors.some((f) => f.path === path);
387+
const advised = advisories.some((f) => f.path === path);
388+
expect([gated, advised]).toEqual(severity === 'error' ? [true, false] : [false, true]);
389+
});
390+
}
391+
392+
it('the reference-integrity suite carries all four', () => {
393+
// One stack, all four missed at once: the suite reports four findings and
394+
// not one, so no key rides on a neighbour's row.
395+
const stack = stackWith(
396+
mutate({
397+
calendar: { allDayField: 'zz_a' },
398+
gantt: { borderColorField: 'zz_b', lockField: 'zz_c', objectField: 'zz_d' },
399+
}),
400+
);
401+
const mine = validateReferenceIntegrity(stack).filter((f) => f.rule === LIST_VIEW_FIELD_UNKNOWN);
402+
expect(mine.map((f) => f.path).sort()).toEqual([
403+
'views[0].list.calendar.allDayField',
404+
'views[0].list.gantt.borderColorField',
405+
'views[0].list.gantt.lockField',
406+
'views[0].list.gantt.objectField',
407+
]);
289408
});
290409
});
291410

@@ -535,8 +654,18 @@ describe('#14282 — the measured exclusions: positions read CLIENT-SIDE', () =>
535654
mutate({
536655
rowColor: { field: 'owner.name' },
537656
kanban: { groupByField: 'owner.name' },
538-
calendar: { titleField: 'owner.name' },
657+
calendar: { titleField: 'owner.name', allDayField: 'owner.name' },
539658
gallery: { coverField: 'owner.name' },
659+
// [#18835] The four positions this card adds are in POSITIONS and
660+
// deliberately NOT in DOTTED_AXIS, which is what "a position added to
661+
// the surface map does not silently acquire a dotted verdict nobody
662+
// measured" means in practice. Pinned here so the default is a
663+
// decision rather than an omission.
664+
gantt: {
665+
borderColorField: 'owner.name',
666+
lockField: 'owner.name',
667+
objectField: 'owner.name',
668+
},
540669
tree: { parentField: 'owner.name' },
541670
grouping: { fields: [{ field: 'owner.name' }] },
542671
hiddenFields: ['owner.name'],

‎packages/lint/src/validate-list-view-field-refs.ts‎

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,17 @@
5454
* card into the uncolumned bucket; a `gantt` / `calendar` / `timeline`
5555
* required date field leaves the renderer nothing to place, so the chart is
5656
* blank; a map with no resolvable coordinate field plots no marker.
57+
* [#18835] A third consequence belongs to this tier and the two sentences
58+
* above did not name it: a binding whose job is to RESTRICT or to ROUTE,
59+
* where the miss is read as "no restriction" / "no route" on every row.
60+
* Nothing is missing from the picture — which is exactly why it gates: a
61+
* `gantt.lockField` that resolves to nothing unfreezes every row the author
62+
* declared view-only, and a `gantt.objectField` that resolves to nothing
63+
* makes every row answer the renderer's synthetic-row test, so no bar in
64+
* the chart opens anything. Both are the shape Prime Directive #10 names —
65+
* a capability advertised in the metadata and not delivered by the runtime
66+
* — and both are worse DECLARED than omitted, because each renderer guards
67+
* its behaviour on the key's mere presence.
5768
* - **`warning`** — the renderer drops one decoration and renders the rest:
5869
* an optional colour / title / tooltip / cover binding, a stale
5970
* `hiddenFields` entry that hides nothing, a stale `fieldOrder` entry that
@@ -345,6 +356,19 @@ const POSITIONS: Record<string, BlockPositions> = {
345356
endDateField: 'warning',
346357
titleField: 'warning',
347358
colorField: 'warning',
359+
// [#18835] `allDayField` names a BOOLEAN field, and a declared one is
360+
// ABSOLUTE — the spec's own `⛔ No default field name` note. Measured in
361+
// objectui `dda8f3815`: `ObjectCalendar` maps every event with
362+
// `allDay: allDayField ? Boolean(record[allDayField]) : !endDate`, so a
363+
// name no record carries reads `undefined` on every row, `Boolean()`
364+
// answers false for all of them, AND the `!endDate` inference that would
365+
// have banded a dateless event is switched off by the key's mere
366+
// presence. Every event still renders and is still placed at its start:
367+
// what is lost is the all-day BAND, one decoration, which is this tier
368+
// and not `startDateField`'s (nothing to place at all). Not the error
369+
// tier's third consequence either — no restriction and no route rides on
370+
// it; a wrongly-banded event is still an event the user can click.
371+
allDayField: 'warning',
348372
},
349373
},
350374
gantt: {
@@ -362,6 +386,34 @@ const POSITIONS: Record<string, BlockPositions> = {
362386
groupByField: 'warning',
363387
assigneeField: 'warning',
364388
effortField: 'warning',
389+
// ── [#18835] Three of the ten objectui-lifted members (#15469) are
390+
// field bindings, and they share only their `.optional()`. Each is tiered
391+
// by what its own renderer does with a name no record carries, measured
392+
// in objectui `dda8f3815` (`plugin-gantt/src/ObjectGantt.tsx`).
393+
//
394+
// The stroke is opt-in and exceptional, and the renderer says so:
395+
// `borderColorRaw = borderColorField ? record[borderColorField] :
396+
// undefined` leaves `borderColor` undefined for every task, so every bar
397+
// keeps its fill and renders without an outline. `colorField`'s case,
398+
// eight rows up — one decoration dropped, the chart intact.
399+
borderColorField: 'warning',
400+
// ⛔ NOT a decoration: a declared WRITE GUARD that fails OPEN.
401+
// `locked: lockField ? !!record[lockField] : undefined` reads `undefined`
402+
// on every row and the drawer's `recLocked` falls the same way, so every
403+
// row the author froze becomes draggable, resizable, progress-draggable,
404+
// link-able, inline-editable and deletable — and the drag PERSISTS. The
405+
// chart looks perfect and the restriction the author declared is simply
406+
// not in force, which is the error tier's third consequence above.
407+
lockField: 'error',
408+
// The row's OWN object api name, and a miss takes the whole detail
409+
// surface with it: `isSyntheticRow` is `!!objectField &&
410+
// !String(rec[objectField] ?? '').trim()`, so a name no record carries
411+
// answers TRUE for every row. `onTaskClick` then never calls
412+
// `navigation.handleClick` and `renderRecordOverlay` returns null — no
413+
// bar in the chart opens a drawer or a detail page, uniformly and
414+
// silently. Strictly worse than omitting the key, which is what the
415+
// `!!objectField &&` guard exists to make safe.
416+
objectField: 'error',
365417
},
366418
// A quick filter is a FILTER: a stale one filters on a column that does
367419
// not exist and empties the chart.

0 commit comments

Comments
 (0)