Skip to content

Commit ec2ede0

Browse files
os-warrenclaude
andauthored
fix(spec): the calendar binding warning describes the refusal objectui#7029 left, not the fallback it deleted (#19623)
Fixes #17445 Clause-②: no ## What was wrong Two carriers in `packages/spec/src/kernel/functional-completeness.ts` asserted the same renderer behaviour, and objectui deleted it: - `VIEW_BINDING_BLOCKS`' calendar row — `calendar` goes to `startDateField || 'start_date'`, `endDateField || 'end_date'` — stated as **measured**, against "the built console 17.2.0"; - the body `checkViewCompleteness` emits on the `view/layout-without-binding` route: "the renderer falls back to literal default field names ... the view renders empty while authoring reports success". ## The re-verification — mine, at my own ref ⛔ The card's measurement (objectui `3fbdd4a2d`, 2026-09-10) was **not** taken on trust. Re-read on objectui `main` at **`0cf2d6644`** (2026-09-21), and again at the pin this repo builds against, `.objectui-sha` = `87af769e9` (2026-09-20). **Both measured facts hold at both refs; only half ① is byte-identical.** Half ①, `packages/plugin-list/src/ListView.tsx`, is byte-identical across the two refs — `git log 87af769e9..0cf2d6644 -- packages/plugin-list/src/ListView.tsx` is empty, and so is the file diff. Half ②, `packages/plugin-calendar/src/ObjectCalendar.tsx`, is **not**: 43 insertions / 17 deletions, one commit, objectui `afb228418` (objectui#10101, routing the component's copy through the locale packs). What the finding rests on survives that commit exactly: `getCalendarConfig` occupies lines 287–313 at **both** refs with an md5-identical body (`3424cd63d0300a8ad7c3f78c233f742f`), and the refusal sentence is character-identical at both refs — objectui#10101 moved it out of JSX text into the English default of `tt('calendar.configRequired', …)`, so the rendered string is unchanged at the pin this repo ships while a non-English locale on objectui's head renders other words. The in-tree docblock already had the accurate wording ("Both reads are identical at the pin"); this paragraph now matches it rather than over-claiming past it: 1. `packages/plugin-list/src/ListView.tsx`, `case 'calendar':` — the two literal floors are gone. The branch resolves `startDateField` / `endDateField` / `titleField` from the declared block only and spreads each conditionally; its comment records objectui#7029 and the reason ("field names no view had written and most objects do not carry"). 2. `packages/plugin-calendar/src/ObjectCalendar.tsx` — `getCalendarConfig(schema)` returns `null` with neither a `calendar` block nor a flat `startDateField`, and the `if (!calendarConfig)` arm renders the refusal screen. Two deltas against the card, both in the same direction: - The refusal text has **moved since the card**: objectui#8170 corrected it to "Calendar configuration required. Please specify startDateField, the calendar's one required key; the event title resolves without titleField." — `titleField` is **not** required, which matches `CalendarConfigSchema` on this side (one required key: `startDateField`). - The flat `dateField` rung the card's sentence named is **retired** (objectui#8355); `getCalendarConfig` reads the declared spellings only. ⇒ the claim under test is confirmed dead, and confirmed dead in the tree this repo ships, not just in objectui's head. ## What this PR changes - **The calendar row** now states what the two reads show, names both refs, and cites objectui#7029 / objectui#8170 rather than a 17.2.0 reading. - **The docblock's opening premise** no longer generalises one row to the next: what a view without its block does is per type — a literal-name fallback, an inferred binding, or a refusal — and each row is only as true as its last measurement. The 17.2.0 provenance sentence is kept and marked dated, because it was true once and that is why it needed correcting rather than deleting. - **The warning body is now per type.** `VIEW_BINDING_MESSAGE` carries the calendar entry; the other five types receive the unchanged generic body through `unboundBlockMessage`. A type that stops flooring gets an entry, never a reworded universal. - **The prescription is preserved** — the half the old text was right about. The new body names `calendar.startDateField` (the block's one required key) and records that the event title resolves through the ADR-0079 display-name chain when `titleField` is omitted. The `fix` hint is unchanged. - **No severity moves — because the severity is already RULED, not because the question is open.** #16577 ruled **B** on 2026-09-11 (comment `5634033966`, director seat, class-one self-adjudication, summon #22; the card closed `completed`): the `type: 'calendar'` route stays warning-class under ADR-0078 §1, carried at `warning` by `checkViewCompleteness`'s `VIEW_BINDING_BLOCKS`. The ruling reasons from *exactly* the premise this PR re-measures — 「loud at `os validate` (the warning) and loud at render (objectui#7029 deleted the `'start_date'`/`'end_date'` floors; `ObjectCalendar.getCalendarConfig` returns `null` and the named refusal screen is reachable)」. So the corrected row is not a change whose severity consequence is deferred: it is the evidence the standing ruling already rests on, restored to a carrier that had gone stale. The three in-tree carriers that deferred to #16577 as an open question (`functional-completeness.ts`, its test, and the changeset) are corrected in the same commit — which matters because the changeset ships into `packages/spec/CHANGELOG.md`, the exact file where the *previous* stale claim survived. ## Per-row verdict for the rest of `VIEW_BINDING_BLOCKS` Every row was re-read at `0cf2d6644`, because this lane's recurring failure is patching a region and leaving the neighbour: - **`kanban`** — STILL TRUE. `groupByField` then `groupField` then `detectStatusField(objectDef)`; the row's "inferred" is that detector. - **`calendar`** — FALSE. Corrected here. - **`gantt`** — FALSE, recorded and **not** corrected. All four floors are gone (objectui#7070 for the dates, objectui#7499 for `progressField` / `dependenciesField`) and `ObjectGantt` REFUSES an absent date binding rather than drawing the "blank chart" the row describes. - **`timeline`** — HALF FALSE, recorded. The `created_at` floor was deleted by objectui#7070 step three on the ruling 日期轴永不虚构, with a refusal screen in its place; the `titleField || 'name'` half of the row still stands. - **`map`** — FALSE, recorded. `locationField || 'location'` is gone on BOTH faces (objectui#8169) and `ObjectMap` renders "Map configuration required". ⚠️ That literal is also quoted in this rule's map-specific message and **pinned** by `functional-completeness.test.ts`, so correcting that row means correcting message and pin together — which is why it is not a rider here. - **`tree`** — STILL TRUE. `treeCfg.labelField || treeCfg.titleField || 'name'`, with `parentField` auto-detected when omitted. - (`gallery`, the recorded non-row, is still `titleField || 'name'` — unchanged.) The three stale rows are recorded in the table's own re-measurement note so nobody reuses them as current fact, and reported for filing. Correcting them is a separate finding: each changes what its row's severity rests on — the same class of question #16577 settled for `calendar` (ruled B, closed `completed` 2026-09-11), decided one route at a time and on that route's own measurement. #16577 is closed; it owns no open docket for `gantt` / `timeline` / `map`. ## Validation — every command with its exit code Exit codes captured before any pipe (redirect first, `EXIT=$?` second — never across a pipe). Head for the block below: `6f1b457d48`. **Fix round, head `4530d21191`** — prose-only re-run of every derived family that reads what the fix touched, each exit code captured before any pipe: `pnpm --filter @objectstack/spec run check:objectui-pin-citations` — **0** ("29 asserting objectui pin citation(s) match .objectui-sha (87af769e9), 48 historical citation(s) recorded and not checked, across 1511 spec source(s)"; 7 anchor content assertions verified, i.e. `ASSERTED_ANCHOR_FLOOR` untouched) · `pnpm --filter @objectstack/spec exec vitest run src/kernel/functional-completeness.test.ts` — **0** (62 passed) · `pnpm --filter @objectstack/spec typecheck` — **0** · `node scripts/check-spec-docblock-symbol-anchors.mjs` — **0** · `node scripts/check-issue-citations.mjs --base origin/main` — **0** · `pnpm check:nul-bytes` — **0** · `node scripts/check-comment-mask-adoption.mjs` — **0** · `node scripts/check-keyed-text-bounds.mjs` — **0** · `node scripts/check-empty-changeset.mjs --base origin/main` — **0** · `node scripts/check-changeset-no-major.mjs --base origin/main` — **0** · `node scripts/check-adr-0087-registration.mjs --base origin/main` — **0** · `node scripts/check-closing-keyword-parity.mjs` — **0** · `pnpm check:pm-prior-rulings` — **0** · `pnpm exec eslint --no-inline-config --format json` over the two changed sources — **0**, 2 files / 0 errors / 0 warnings. `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --ran` reconciles that record as **82 derived, 11 run, 0 NOT-MEASURED, 71 UNRUN** — the 71 are CI's farm, a declared narrowing, not a silent one. - `pnpm --filter @objectstack/spec build` — **0** - `pnpm --filter @objectstack/spec exec vitest run src/kernel/functional-completeness.test.ts` — **0** (62 passed) - `pnpm --filter "@objectstack/lint^..." build --concurrency=2` then `pnpm --filter @objectstack/lint exec vitest run src/validate-functional-completeness.test.ts` — **0** (25 passed). The first attempt at that suite exited 1 on `Failed to resolve entry for package "@objectstack/formula"` — a PREREQUISITE NOT MET, not a red gate: the dependency closure was unbuilt. Re-run after building it. - `pnpm --filter @objectstack/spec typecheck` — **0** - `pnpm --filter @objectstack/spec check:generated` — **0** (15 generated artifacts up to date) - `pnpm exec eslint --no-inline-config` on both changed files — **0**, 0 errors / 0 warnings over 2 files (counted from `--format json`). The repo-wide `pnpm lint` is CI's run; this narrowing is measurable rather than assumed because `eslint.config.mjs` declares no `parserOptions.project` and no typed rules, so a diff in these two files cannot move the verdict on any file it does not contain. - `pnpm check:nul-bytes` — **0**; plus a direct control-character scan of the three touched files, no hits. - `pnpm --filter @objectstack/spec check:objectui-pin-citations` — **0**; the asserting-citation count rose 28 to 29, i.e. the new pin citation is recognised and checked against `.objectui-sha`, so the next pin bump fails loudly on this record. - `node scripts/check-spec-docblock-symbol-anchors.mjs` — **0** - `pnpm check:issue-citations` — **0**. That alias is the **`--self-test` half** (`package.json:84` spells it `node scripts/check-issue-citations.mjs --self-test`). The **diff-scoped** half is `node scripts/check-issue-citations.mjs --base origin/main`, run on the fix commit — **0**, 13 citations judged across 1 file (3 resolve, 1 resolves-as-pull-request, 9 cross-repo-unjudged). CI runs both halves at `lint.yml:4893` (`pnpm check:issue-citations && node scripts/check-issue-citations.mjs`) inside "Lint & Repo Gates". · `node scripts/check-changeset-no-major.mjs --base origin/main` — **0** · `node scripts/check-adr-0087-registration.mjs --base origin/main` — **0** · `node scripts/check-closing-keyword-parity.mjs` — **0** · `node scripts/check-comment-mask-adoption.mjs` — **0** · `pnpm check:pm-prior-rulings` — **0** · `node scripts/check-changeset-fixed.mjs` — **0** Gate families were derived with `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` (77 commands); the rest of that list is CI's — nothing here is claimed for a command that was not run. ## Acceptance notes - **No test pinned the old warning string.** `functional-completeness.test.ts` and `@objectstack/lint`'s `validate-functional-completeness.test.ts` assert rule ids, severities, paths and `fix` content; the only message pin on this route is the map one quoting `locationField || 'location'`, which this PR leaves alone. A stale prose comment in the spec test's `describe` header did restate the deleted premise for all six types — corrected, and two pins added: the calendar body must not claim a literal fallback and must name `calendar.startDateField`, and the other five types must keep receiving the generic body (an honest pin: it records which body a type gets, not that the body is true of it). - `VIEW_BINDING_FIX.calendar` is left untouched. It suggests `startDateField` plus the optional `titleField`, which `CalendarConfigSchema` accepts and the renderer honours, so it is still correct — the card says so too. - Noted, not filed: the changelog table in released `CHANGELOG.md` files carries the same "renderer falls back to literal default field names" sentence. Released entries are amended only in a dedicated docs-only PR, never as a rider here. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01UDXER3sdqfeVYpEWZs5mZx --- _Generated by [Claude Code](https://claude.ai/code)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent c3a4c74 commit ec2ede0

3 files changed

Lines changed: 194 additions & 17 deletions

File tree

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
---
2+
"@objectstack/spec": patch
3+
---
4+
5+
`view/layout-without-binding` — the `calendar` warning no longer tells an author the renderer falls back to `start_date` / `end_date`, because objectui deleted those floors; it names the refusal screen the renderer shows instead, and the key that clears it (#17445).
6+
7+
Two carriers asserted the same deleted behaviour: `VIEW_BINDING_BLOCKS`' calendar row in `src/kernel/functional-completeness.ts` (stated as *measured*, against "the built console 17.2.0") and the body of the warning `checkViewCompleteness` emits on that route. Re-measured on objectui `main` at `0cf2d6644` (2026-09-21), both halves of the path a `type: 'calendar'` list view takes:
8+
9+
- `packages/plugin-list/src/ListView.tsx`, `case 'calendar'` — the two literal floors are gone (objectui#7029); the branch restates only bindings the view declared.
10+
- `packages/plugin-calendar/src/ObjectCalendar.tsx` — `getCalendarConfig` resolves `null` with neither a `calendar` block nor a flat `startDateField`, and the component renders its "Calendar configuration required" refusal screen, which names `startDateField` (objectui#8170 corrected that screen: `titleField` is not required).
11+
12+
So the old body was wrong twice — there is no fallback to literal field names, and the failure is not silent. What it was right about is the remedy, and that is the half the new body keeps: it names `calendar.startDateField`, `CalendarConfigSchema`'s one required key, and records that the event title resolves through the ADR-0079 display-name chain when `titleField` is omitted. The `fix` hint is unchanged.
13+
14+
- **The message is now per type.** `VIEW_BINDING_MESSAGE` carries an entry for a type whose measured outcome is not the generic literal-fallback sentence; the other five types receive the generic body unchanged. A type that stops flooring gets an entry, never a reworded universal — the two carriers drifted apart once, and the map is what makes correcting both one edit.
15+
- **No severity moves — the severity is already ruled.** #16577 ruled **B** on 2026-09-11 (comment `5634033966`, card closed `completed`): the `type: 'calendar'` route stays warning-class under ADR-0078 §1, and it stays there *because* both doors are loud — loud at `os validate` (this warning) and loud at render (objectui#7029 deleted the `start_date` / `end_date` floors; `getCalendarConfig` returns `null` and the named refusal screen is reachable). That is exactly the premise re-measured here, so the corrected row is the evidence the standing ruling rests on, not a change whose severity consequence is pending.
16+
- ⚠️ **The same reading found three sibling rows stale, and they are recorded rather than corrected** — out of this card's scope, and each changes what its row's severity rests on: `gantt`'s four floors are gone (objectui#7070, objectui#7499) and `ObjectGantt` refuses; `timeline`'s `created_at` floor is gone (objectui#7070) while its `titleField || 'name'` stands; `map`'s `locationField || 'location'` is gone on both faces (objectui#8169). The table now carries that re-measurement note so the three are not reused as current fact, and `kanban` / `tree` were re-read at the same ref and still say what they say.
17+
18+
No schema moved, no export moved and no accept set moved: this corrects prose and one warning string. `Clause-②: no`

‎packages/spec/src/kernel/functional-completeness.test.ts‎

Lines changed: 55 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -118,10 +118,17 @@ describe('checkFieldCompleteness — the verified inert shapes go red', () => {
118118
});
119119

120120
describe('checkViewCompleteness — layout bindings', () => {
121-
// Six of the view `type` members carry a binding block. The renderer's
122-
// fallback for every one is a literal field name (measured in objectui's
123-
// ListView adapter — see the table's docblock), so the missing block is the
124-
// same defect on all six, not a lesser one on the last three.
121+
// Six of the view `type` members carry a binding block, and the missing
122+
// block is the same defect on all six: the surface the author asked for is
123+
// not the one that renders, while authoring reports success.
124+
//
125+
// ⛔ What it is NOT is one shared mechanism. This comment used to say the
126+
// renderer's fallback for every one is a literal field name; objectui has
127+
// been deleting those floors one view type at a time, and for `calendar`
128+
// the renderer now REFUSES by name instead (#17445). The rule fires on all
129+
// six either way — that is what this pin holds — and the per-type
130+
// mechanism belongs to the table's docblock, which states each row's own
131+
// measurement and which of them have gone stale.
125132
it.each(['kanban', 'calendar', 'gantt', 'timeline', 'map', 'tree'])('flags a %s view missing its block as a WARNING', (type) => {
126133
const f = only(checkViewCompleteness({ type }) as never);
127134
expect(f.rule).toBe(VIEW_LAYOUT_WITHOUT_BINDING);
@@ -151,6 +158,50 @@ describe('checkViewCompleteness — layout bindings', () => {
151158
expect(checkViewCompleteness({ type: 'tree', tree: { parentField: 'parent' } })).toEqual([]);
152159
});
153160

161+
it('the calendar body describes the REFUSAL, not a deleted literal fallback (#17445)', () => {
162+
// Re-measured on objectui `main` at `0cf2d6644` (2026-09-21), both halves
163+
// of the path: `ListView.tsx`'s `case 'calendar'` restates only declared
164+
// bindings — objectui#7029 deleted the `startDateField || 'start_date'` /
165+
// `endDateField || 'end_date'` floors — and `ObjectCalendar`'s
166+
// `getCalendarConfig` resolves `null`, so the component renders its
167+
// "Calendar configuration required" refusal screen. The body asserted
168+
// those floors as the reason for the warning, which is what this pin
169+
// stops from coming back.
170+
const f = only(checkViewCompleteness({ type: 'calendar' }) as never);
171+
expect(f.message).not.toContain('falls back to literal default field names');
172+
expect(f.message).toContain('Calendar configuration required');
173+
expect(f.message).toContain('ObjectCalendar.tsx');
174+
// ⛔ The half the correction had to PRESERVE: a warning an author meets at
175+
// authoring time earns its place by naming the key to declare, not by
176+
// reporting that something is missing.
177+
expect(f.message).toContain('calendar.startDateField');
178+
expect(f.fix).toContain('startDateField');
179+
// Severity is untouched because it is already RULED, not because the
180+
// question is open: #16577 ruled B (comment `5634033966`, card closed
181+
// `completed` 2026-09-11) — a `calendar` route that refuses BY NAME
182+
// stays warning-class under ADR-0078 §1, on the reasoning that BOTH
183+
// doors are loud, `os validate` and render. That is the reading the
184+
// assertions above re-measure, so this pin carries the ruling's
185+
// evidence rather than standing in for a pending one.
186+
expect(f.severity).toBe('warning');
187+
});
188+
189+
it('the five types with no per-type body keep the generic one — an override, not a rewrite', () => {
190+
// ⚠️ An honest pin: it records WHICH body each type receives, NOT that
191+
// the body is true of each. The table's re-measurement note says `gantt`,
192+
// `timeline` and `map` inherit this sentence pending corrections of their
193+
// own; `kanban` and `tree` were re-read at the same ref and still floor a
194+
// literal. Correcting one of the three means adding an entry beside
195+
// `calendar`'s and moving that type out of this list — a deliberate edit,
196+
// which is the point.
197+
for (const type of ['kanban', 'gantt', 'timeline', 'map', 'tree']) {
198+
const f = only(checkViewCompleteness({ type }) as never);
199+
expect(f.rule).toBe(VIEW_LAYOUT_WITHOUT_BINDING);
200+
expect(f.message).toContain('falls back to literal default field names');
201+
expect(f.message).toContain(`A \`${type}\` view with no \`${type}\` block`);
202+
}
203+
});
204+
154205
it('names the schema-required keys in the timeline prescription', () => {
155206
// `TimelineConfigSchema` requires exactly these two; the hint must not
156207
// send an author to declare a block the parser then refuses.

‎packages/spec/src/kernel/functional-completeness.ts‎

Lines changed: 121 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -216,24 +216,66 @@ export function checkFieldCompleteness(def: unknown): CompletenessFinding[] {
216216
}
217217

218218
/**
219-
* The layout-specific config block each view type is inert without. The
220-
* renderer falls back to LITERAL field names, so a view without its block
221-
* renders — but empty — on any object that does not happen to declare those
222-
* exact fields. Degrades rather than fully dies: WARNING, not error
223-
* (ADR-0078 §1).
224-
*
225-
* Every entry names its measured renderer fallback. The verify-then-enforce
219+
* The layout-specific config block each view type is inert without. What a
220+
* view WITHOUT its block actually does is per type — a fallback to LITERAL
221+
* field names that renders empty on any object not happening to declare
222+
* them, a binding inferred from the object, or a refusal screen — so every
223+
* row below states its own measured outcome and names the reading it came
224+
* from. ⛔ Do not generalise one row to the next, and ⛔ do not restate a row
225+
* as fact without re-reading the renderer: objectui is deleting these floors
226+
* one view type at a time, so a row is only as true as its last measurement.
227+
*
228+
* Either way the author asked for a surface that does not render, while
229+
* authoring reports success: WARNING, not error (ADR-0078 §1). ⚠️ The
230+
* rubric's other half — refuse what renders NOTHING — was put to [#16577]
231+
* for the `type: 'calendar'` route and is SETTLED, not pending: ruled B
232+
* (director seat, comment `5634033966`; the card closed `completed` on
233+
* 2026-09-11), the route STAYS warning-class and is carried at `warning` by
234+
* the table below. The ruling turns on BOTH doors being loud — loud at
235+
* `os validate` (this warning) and loud at render (objectui#7029 deleted the
236+
* `'start_date'` / `'end_date'` floors; `ObjectCalendar.getCalendarConfig`
237+
* returns `null` and the named refusal screen is reachable) — which is
238+
* exactly what the `calendar` row below now measures. So this table moves no
239+
* severity because the severity is already RULED, and the corrected row is
240+
* the evidence that ruling rests on. ⛔ Reopening it takes a new ruling,
241+
* not a re-read; a row going stale is a reason to re-measure the ROW.
242+
*
243+
* Every entry names its measured renderer binding. The verify-then-enforce
226244
* gate this table sits behind (the audit's Tier-A had named only the first
227245
* three) was discharged for `timeline` / `map` / `tree` by two measurements
228246
* on record: the per-type props builder of objectui's ListView adapter
229247
* (`packages/plugin-list/src/ListView.tsx`, read back verbatim from the built
230248
* console 17.2.0), and a both-direction ablation on `os validate` — deleting
231249
* a `timeline` block left `warnings: []` with `valid: true`, deleting the
232250
* sibling `gantt` block warned as designed. The gate was working; the table
233-
* was short.
251+
* was short. ⚠️ That reading is dated — console 17.2.0 — and is no longer
252+
* current for every row; see the re-measurement note under the table.
234253
*
235254
* - `kanban` → `groupBy = groupByField || groupField || <inferred>`
236-
* - `calendar` → `startDateField || 'start_date'`, `endDateField || 'end_date'`
255+
* - `calendar` → NO fallback, and no silence: the view is bound to nothing
256+
* and the renderer says so on screen. Measured on objectui `main` at
257+
* `0cf2d6644` (2026-09-21), both halves of the path a `type: 'calendar'`
258+
* list view takes. ① `ListView.tsx`'s `case 'calendar'` restates only the
259+
* bindings the view DECLARED — the `startDateField || 'start_date'` and
260+
* `endDateField || 'end_date'` floors this row used to name were deleted
261+
* by objectui#7029, whose own comment records that they were "field names
262+
* no view had written and most objects do not carry". ② `ObjectCalendar`'s
263+
* `getCalendarConfig` returns `null` with neither a `calendar` block nor a
264+
* flat `startDateField`, and the `if (!calendarConfig)` arm renders the
265+
* "Calendar configuration required" refusal screen, which names
266+
* `startDateField` as the key to declare (objectui#8170 corrected that
267+
* screen's second clause: `titleField` is NOT required). ⚠️ That literal
268+
* is the text RENDERED at the pin: objectui#10101 landed AFTER the pin and
269+
* moved it into a `tt('calendar.configRequired', …)` default, so on
270+
* objectui's head a non-English locale renders other words for the same
271+
* screen. Harmless for the console this repo ships — and the pin citation
272+
* below is what reds when the pin moves past it. So the loss is
273+
* total rather than partial — every record, on every object — and the
274+
* author is told at render time as well as here. Both reads are identical
275+
* at the pin this repo builds against (`.objectui-sha` = `87af769e9`), so
276+
* this describes the console this repo SHIPS and not only objectui's head.
277+
* This repo already records the same deletion one door over: the #13817
278+
* check in `../ui/view.zod.ts` names objectui#7029 as its runtime half.
237279
* - `gantt` → `startDateField || 'start_date'`, `endDateField || 'end_date'`,
238280
* `progressField || 'progress'`, `dependenciesField || 'dependencies'` —
239281
* fails CLOSED (`null` unless both dates resolve): a blank chart
@@ -255,6 +297,30 @@ export function checkFieldCompleteness(def: unknown): CompletenessFinding[] {
255297
* still renders flat — the parent pointer has its own rule,
256298
* {@link VIEW_TREE_WITHOUT_PARENT_FIELD}, below.
257299
*
300+
* ⚠️ RE-MEASUREMENT [#17445], objectui `main` at `0cf2d6644` (2026-09-21).
301+
* The `calendar` row above is the one that card corrected. The same reading
302+
* found three siblings still naming floors objectui has since deleted, and
303+
* they are RECORDED here rather than corrected, because that correction is a
304+
* separate finding to rule on and not a rider (the card scoped itself to
305+
* `calendar`, and each of the three changes what its row's severity rests
306+
* on):
307+
*
308+
* - `gantt` — all four floors gone (objectui#7070 for the dates, #7499 for
309+
* `progressField` / `dependenciesField`); `ObjectGantt` REFUSES an absent
310+
* date binding rather than drawing the blank chart this row describes.
311+
* - `timeline` — `startDateField || 'created_at'` gone (objectui#7070 step
312+
* ③, on the ruling 日期轴永不虚构), with a refusal screen in its place;
313+
* the `titleField || 'name'` half of the row still stands.
314+
* - `map` — `locationField || 'location'` gone on BOTH faces (objectui#8169):
315+
* `ObjectMap` renders "Map configuration required" instead of an empty map.
316+
* ⛔ That literal is also quoted in this rule's `map`-specific message
317+
* below and pinned by `functional-completeness.test.ts`, so correcting the
318+
* row means correcting the message and the pin together.
319+
*
320+
* `kanban` and `tree` were re-read at the same ref and still say what they
321+
* say. ⛔ Until the three above are corrected, do not cite them — or the
322+
* warning text they feed — as a current measurement.
323+
*
258324
* `gallery` is measured too (`titleField || 'name'`) and is deliberately NOT
259325
* here: `GalleryConfigSchema` requires no key, so "has a `gallery` block"
260326
* would assert nothing an author could act on, and the fallback mis-titles
@@ -296,6 +362,51 @@ const VIEW_BINDING_FIX: Readonly<Record<string, string>> = {
296362
tree: "tree: { parentField: '<self_lookup_field>', labelField: '<text_field>' }",
297363
};
298364

365+
/**
366+
* The body of the `view/layout-without-binding` warning for a type whose
367+
* measured outcome is NOT the generic literal-fallback sentence
368+
* ({@link unboundBlockMessage}). A type with no entry here gets the generic
369+
* body, so this map is the exception list, not a second copy of the table.
370+
*
371+
* ⛔ An entry is written from a reading of the renderer, never from the row
372+
* above it — the two carriers went out of sync once already, which is what
373+
* this map exists to make cheap to fix: {@link VIEW_BINDING_BLOCKS}'s
374+
* `calendar` row and this rule's message BOTH asserted a
375+
* `startDateField || 'start_date'` fallback that objectui#7029 had deleted,
376+
* and correcting one without the other would have left the author reading the
377+
* stale half.
378+
*
379+
* ⚠️ What the correction must PRESERVE is the prescription. The old sentence
380+
* was wrong about the mechanism and still right about the remedy, and the
381+
* remedy is the whole value of a warning an author meets at authoring time:
382+
* it says which key to declare, not merely that something is missing.
383+
*/
384+
const VIEW_BINDING_MESSAGE: Readonly<Record<string, string>> = {
385+
calendar:
386+
'A `calendar` view with no `calendar` block declares no date axis, and the renderer does '
387+
+ 'not invent one: objectui\'s `ListView.tsx` calendar branch forwards only the bindings the '
388+
+ 'view DECLARED, so `getCalendarConfig` (objectui `ObjectCalendar.tsx`) resolves `null` and '
389+
+ 'the view renders its "Calendar configuration required" refusal screen instead of records '
390+
+ '— it parses and publishes clean, then shows no event on any object, not just on one that '
391+
+ 'happens to lack a field. Declare `calendar.startDateField`, the block\'s one required key; '
392+
+ 'the event title resolves through the ADR-0079 record display-name chain when `titleField` '
393+
+ 'is omitted.',
394+
};
395+
396+
/**
397+
* The generic body — a view type whose renderer still floors the binding at a
398+
* literal field name. It is what the five types with no
399+
* {@link VIEW_BINDING_MESSAGE} entry receive, and the re-measurement note on
400+
* {@link VIEW_BINDING_BLOCKS} says which of those five it is still true of
401+
* (`kanban`, `tree`) and which three inherit it pending their own correction.
402+
* ⛔ So it is the DEFAULT, never a universal: a type that stops flooring gets
403+
* an entry above, not a reworded sentence here.
404+
*/
405+
const unboundBlockMessage = (type: string, block: string): string =>
406+
`A \`${type}\` view with no \`${block}\` block is bound to nothing: the renderer falls `
407+
+ 'back to literal default field names, which works only if the object happens to declare '
408+
+ 'them — on any other object the view renders empty while authoring reports success.';
409+
299410
const isNonEmptyString = (v: unknown): v is string => typeof v === 'string' && v.length > 0;
300411

301412
/** A `map` block's coordinate binding — either of the two forms its schema documents. */
@@ -514,10 +625,7 @@ export function checkViewCompleteness(view: unknown, boundObject?: unknown): Com
514625
rule: VIEW_LAYOUT_WITHOUT_BINDING,
515626
severity: 'warning',
516627
path: block,
517-
message:
518-
`A \`${type}\` view with no \`${block}\` block is bound to nothing: the renderer falls `
519-
+ 'back to literal default field names, which works only if the object happens to declare '
520-
+ 'them — on any other object the view renders empty while authoring reports success.',
628+
message: VIEW_BINDING_MESSAGE[type] ?? unboundBlockMessage(type, block),
521629
fix: VIEW_BINDING_FIX[type],
522630
});
523631
} else if (type === 'map' && isRec(view.map) && !hasMapCoordinateBinding(view.map)) {

0 commit comments

Comments
 (0)