Skip to content

Commit 2cf9db7

Browse files
fix(spec): the gantt, timeline and map binding warnings describe the refusals objectui left, not the floors it deleted (#19788)
Fixes #19630 Clause-②: no — author-facing warning text and its premise corrected to what the renderer does; the warning class is held by the #16577 ruling (B), no accept/reject change, no public surface added ## What was wrong Three `VIEW_BINDING_BLOCKS` rows in `packages/spec/src/kernel/functional-completeness.ts` stated renderer fallbacks that objectui has deleted, and the warning `checkViewCompleteness` prints on `os validate` for those three types inherited the generic body built on the same premise ("the renderer falls back to literal default field names ... the view renders empty"). For `map` the stale literal `locationField || 'location'` had three carriers: the row, the block-present `map` message (which quoted it as the renderer's read), and the `functional-completeness.test.ts` pin that held it. ## The re-derivation, at the pin The card's verdicts were measured at objectui `main` `0cf2d6644`. They were **not** inherited: all three were re-derived at the objectui pin this repo ships, `.objectui-sha` = `87af769e9a3e` (read on `origin/main` at `43e17b8905`), from a read-only fetch of that exact commit. Every verdict held at the pin, so there is no fork to report: - **`gantt` — FALSE (confirmed).** `plugin-list/src/ListView.tsx` `case 'gantt'` spreads `startDateField` / `endDateField` / `titleField` only when declared (the date floors deleted by objectui#7070, the `progress` / `dependencies` floors by objectui#7499, omitted rather than refused). `plugin-gantt/src/ObjectGantt.tsx` `getGanttConfig` takes its flat branch only with both dates, otherwise returns `null`, and the `if (!ganttConfig)` arm renders "Gantt configuration required. Please specify startDateField, endDateField, and titleField." A refusal by name, not a blank chart. - **`timeline` — HALF FALSE (confirmed).** `case 'timeline'` resolves the axis via `resolveTimelineDateBinding` and spreads it only when declared; the `'created_at'` floor is gone (objectui#7070 step three, on the ruling 日期轴永不虚构), while `titleField: dateBinding.titleField || 'name'` is kept on purpose. `plugin-timeline/src/ObjectTimeline.tsx` ends its start-date chain with no literal rung and, with no authored items and no start date, renders the `timeline.unconfigured.noDateAxis` refusal ("Timeline date axis required — ...", `data-testid="timeline-missing-date-axis"`). - **`map` — FALSE (confirmed).** `case 'map'` forwards `resolveListMapConfig(schema)` with no `locationField || 'location'` floor (objectui#8169). `plugin-map/src/ObjectMap.tsx` `getMapConfig`'s unconfigured branch returns only `{ style }` (no guessed coordinate names) and `hasCoordinateBinding` — applied to every branch, a declared block included — renders "Map configuration required — declare map.locationField or map.latitudeField + map.longitudeField". Zero `'location'` literals remain in `ObjectMap.tsx` at the pin; the one in `ListView.tsx` is the tombstone comment. - `kanban` (`groupByField || groupField || detectStatusField(objectDef)`) and `tree` (`labelField || titleField || 'name'`) were re-read at the pin and still hold. The `plugin-view` / `app-shell` `ObjectView.tsx` faces were grepped at the pin too: they carry the same deletions as tombstone comments, no live floor. ## What this PR changes - **The three rows** now state what the pin shows, name both halves of each path, and each carries an **asserting pin citation** (`.objectui-sha` = `87af769e9`), so the next pin bump reds on them rather than letting them rot. `check:objectui-pin-citations` lists 4 asserting citations in this file (1 before, 3 new), 46 repo-wide. - **The re-measurement note** that recorded the three as stale ("do not cite them") is replaced by a short history: which card corrected which rows, and at which ref. - **Per-type bodies.** `VIEW_BINDING_MESSAGE` gains `gantt`, `timeline` and `map` entries beside `calendar`, each written from its row's reading. The generic `unboundBlockMessage` body now reaches only `kanban` and `tree`, the two rows it is still true of. - **The block-present `map` message** no longer quotes `locationField || 'location'`; it names the `hasCoordinateBinding` refusal and the keys that clear it. - **The prescription is preserved on every body**: gantt names `gantt.startDateField` / `gantt.endDateField` / `gantt.titleField` (the three `GanttConfigSchema` requires); timeline names `timeline.startDateField` / `timeline.titleField` (the two `TimelineConfigSchema` requires) and says only the title has a renderer default; map names `map.locationField` or the lat/lng pair. `VIEW_BINDING_FIX` is unchanged: all three hints were already correct. - **No severity moves and no firing moves.** Every route stays `warning`; the same six view types warn on the same inputs. That is consistent with #16577's ruling B (comment `5634033966`) for the `calendar` route, whose reasoning is that both doors are loud; these rows now measure the same two-door shape. The docblock says explicitly that this is consistency with the ruling, not an extension of it, and it does not reopen it. ## Tests - The map pin at the old `:221` was re-judged in place, not dropped: it now asserts the literal is **absent** (`not.toContain("|| 'location'")`) and that the body names `Map configuration required`, `hasCoordinateBinding`, `map.locationField` and `map.latitudeField`. - New `it.each` over gantt / timeline / map: no literal-fallback sentence, the renderer's refusal text, the component file, every key the refusal names, severity `warning`. - The generic-body pin shrinks from five types to `kanban` and `tree`. **Reverse verification (one-shot, not kept).** With the fix committed (`c0131a1325`), `functional-completeness.ts` was replaced on disk by its `43e17b8905` blob (landing proven: old-literal count 1, new `Gantt configuration required` count 0) and the test file run against it: **4 failed / 61 passed**, exit 1: the three new per-type pins plus the re-judged map pin. Direction: red, as predicted. Restored with `git checkout HEAD -- PATH` under an EXIT/INT/TERM trap; restoration proven by `git hash-object` equal to the HEAD blob (`57a4b064e8`) and an empty `git diff HEAD`. The test imports `./functional-completeness` from source, so no dist rebuild sat between mutation and measurement. ## Validation — exit codes captured before any pipe, head `c0131a1325` - `pnpm --filter @objectstack/spec build` · `vitest run src/kernel/functional-completeness.test.ts` (**65 passed**) · `pnpm --filter @objectstack/spec typecheck`: one locked run, `VERDICT command-exit 0`. - `pnpm --filter "@objectstack/lint..." --filter "@objectstack/objectql..." build`: **0**; then `@objectstack/lint` `vitest run src/validate-functional-completeness.test.ts`: **0** (25 passed). That suite asserts rule ids and paths, not message text. - `pnpm --filter @objectstack/spec check:generated`: **0**. - `node scripts/check-issue-citations.mjs --base origin/main`: **0** (diff-scoped half; `pnpm check:issue-citations` is also in the derived set, **0**). - `pnpm exec eslint --no-inline-config --format json` over the two changed sources: **0**, 2 files, 0 errors, 0 warnings. That is a proven narrowing, not a skipped lint: `eslint.config.mjs` enables no type-aware linting (no `parserOptions.project`, no typed rules; its own comment at `:327-328` says so), so this diff cannot change the verdict on any file it does not touch. The repo-wide `pnpm lint` is CI's. - `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derived **82** commands; all 82 were run, each with its exit code recorded. `--ran` reconciles: **82 derived, 80 run, 2 NOT MEASURED, 0 UNRUN**. The two NOT MEASURED are `pnpm check:dual-build-cjs-loads` and `pnpm check:type-check-debt`, both exit 3 PREREQUISITE NOT MET because they need a full `./packages/*` build. That is out of reach under the foreground cap, so it is declared for CI. `check:doc-formula-expressions` and `check:lean-entry-closure` first exited 3 (unbuilt closure); both were re-run after the closure build above and returned **0**. `check:objectui-pin-citations`: **0** (46 asserting match the pin, 37 historical). ## Acceptance notes - `packages/lint/src/validate-list-view-field-refs.ts` (its docblock near `:26-31`) paraphrases the old premise of `view/layout-without-binding` ("the renderer then falls back to literal default field names and the view renders empty"). It is a comment in another package, outside this card's file surface. Noted, not filed; carrier: none. - `resolveTimelineDateBinding` at the pin also reads a `calendar` block's `startDateField` as a timeline axis, so a `type: 'timeline'` view that carries a `calendar` block and no `timeline` block renders, and still gets this warning. That is unchanged by this PR (firing did not move), and the row now records it. Observation only. - Released `CHANGELOG.md` entries still carry the old generic sentence. Released entries are amended only in a dedicated docs-only PR, never as a rider. - Labels: the dispatch names none and a changeset is present, so no label was written. --- _Generated by [Claude Code](https://claude.ai/code/session_01VWsFyWDp8Rjb2Ma6a3Cyo8)_ Co-authored-by: Claude <noreply@anthropic.com>
1 parent 628e55d commit 2cf9db7

3 files changed

Lines changed: 171 additions & 58 deletions

File tree

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
"@objectstack/spec": patch
3+
---
4+
5+
`view/layout-without-binding` — the `gantt`, `timeline` and `map` warnings no longer tell an author the renderer falls back to literal default field names, because objectui deleted those floors; each now names the refusal screen the renderer shows instead, and the keys that clear it (#19630).
6+
7+
Re-derived at the objectui pin this repo builds against (`87af769e9`), not at objectui's head, both halves of each path a list view takes:
8+
9+
- **`gantt`** — `ListView.tsx`'s `case 'gantt'` restates only declared bindings (objectui#7070 deleted the `start_date` / `end_date` floors, objectui#7499 the `progress` / `dependencies` ones); `ObjectGantt`'s `getGanttConfig` returns `null` without both dates and the component renders "Gantt configuration required". The body now names `gantt.startDateField`, `gantt.endDateField` and `gantt.titleField`, the three keys `GanttConfigSchema` requires.
10+
- **`timeline`** — the `startDateField || 'created_at'` floor is gone (objectui#7070 step three) and `ObjectTimeline` renders "Timeline date axis required"; the `titleField || 'name'` default still stands, and the body says so. It names `timeline.startDateField` and `timeline.titleField`, the two keys `TimelineConfigSchema` requires.
11+
- **`map`** — `locationField || 'location'` is gone on both faces (objectui#8169): `ObjectMap` no longer guesses coordinate field names and its `hasCoordinateBinding` gate renders "Map configuration required", for an absent `map` block and for a declared block that names neither coordinate form alike. Both `map` messages — the absent-block body and the block-present one, which had quoted `locationField || 'location'` as the renderer's read — now describe that refusal and name `map.locationField` or the `map.latitudeField` + `map.longitudeField` pair.
12+
13+
`kanban` and `tree` were re-read at the same pin and still floor (or infer) a binding, so they keep the generic body. The `VIEW_BINDING_BLOCKS` docblock rows carry asserting pin citations, so the next `.objectui-sha` bump reds on them instead of leaving them to go stale.
14+
15+
No severity moves and no finding appears or disappears: every route stays `warning`, consistent with #16577's ruling B for the `calendar` route (both doors loud — `os validate` and a named refusal at render), which these rows now measure too without extending or reopening it. No schema, export or accept set moved: this corrects prose and three warning strings. `Clause-②: no`

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

Lines changed: 51 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -125,10 +125,10 @@ describe('checkViewCompleteness — layout bindings', () => {
125125
// ⛔ What it is NOT is one shared mechanism. This comment used to say the
126126
// renderer's fallback for every one is a literal field name; objectui has
127127
// 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.
128+
// (#17445), `gantt`, `timeline` and `map` (#19630) the renderer now REFUSES
129+
// by name instead. The rule fires on all six either way — that is what this
130+
// pin holds — and the per-type mechanism belongs to the table's docblock,
131+
// which states each row's own measurement.
132132
it.each(['kanban', 'calendar', 'gantt', 'timeline', 'map', 'tree'])('flags a %s view missing its block as a WARNING', (type) => {
133133
const f = only(checkViewCompleteness({ type }) as never);
134134
expect(f.rule).toBe(VIEW_LAYOUT_WITHOUT_BINDING);
@@ -186,15 +186,43 @@ describe('checkViewCompleteness — layout bindings', () => {
186186
expect(f.severity).toBe('warning');
187187
});
188188

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']) {
189+
it.each([
190+
// [#19630] Re-derived at the pin this repo builds against, both halves of
191+
// each path: `ListView.tsx` restates only DECLARED bindings (objectui#7070
192+
// for the gantt / timeline date axes, objectui#7499 for gantt progress /
193+
// dependencies, objectui#8169 for map), and the component then renders a
194+
// named refusal screen. The generic body asserted literal floors as the
195+
// reason for the warning; this pin stops that from coming back.
196+
{ type: 'gantt', component: 'ObjectGantt.tsx', screen: 'Gantt configuration required',
197+
keys: ['gantt.startDateField', 'gantt.endDateField', 'gantt.titleField'] },
198+
{ type: 'timeline', component: 'ObjectTimeline.tsx', screen: 'Timeline date axis required',
199+
keys: ['timeline.startDateField', 'timeline.titleField'] },
200+
{ type: 'map', component: 'ObjectMap.tsx', screen: 'Map configuration required',
201+
keys: ['map.locationField', 'map.latitudeField', 'map.longitudeField'] },
202+
])('the $type body describes the REFUSAL, not a deleted literal fallback (#19630)', ({ type, component, screen, keys }) => {
203+
const f = only(checkViewCompleteness({ type }) as never);
204+
expect(f.rule).toBe(VIEW_LAYOUT_WITHOUT_BINDING);
205+
expect(f.message).not.toContain('falls back to literal default field names');
206+
expect(f.message).toContain(screen);
207+
expect(f.message).toContain(component);
208+
// ⛔ The half the correction had to PRESERVE: the prescription. Each key
209+
// the renderer's refusal screen names is the key the body tells the
210+
// author to declare.
211+
for (const key of keys) expect(f.message).toContain(key);
212+
// Severity is untouched and CONSISTENT with #16577's ruling B (comment
213+
// `5634033966`): a route that refuses BY NAME stays warning-class under
214+
// ADR-0078 §1 because both doors are loud. These rows now measure that
215+
// shape; they do not extend or reopen the ruling.
216+
expect(f.severity).toBe('warning');
217+
});
218+
219+
it('the two types with no per-type body keep the generic one — an override, not a rewrite', () => {
220+
// ⚠️ An honest pin: it records WHICH body each type receives. `kanban`
221+
// and `tree` are the two rows re-read at the pin that still floor (or
222+
// infer) a binding, so the generic sentence is still true of them. A type
223+
// that stops flooring gets an entry beside `calendar`'s and moves out of
224+
// this list — a deliberate edit, which is the point.
225+
for (const type of ['kanban', 'tree']) {
198226
const f = only(checkViewCompleteness({ type }) as never);
199227
expect(f.rule).toBe(VIEW_LAYOUT_WITHOUT_BINDING);
200228
expect(f.message).toContain('falls back to literal default field names');
@@ -212,13 +240,21 @@ describe('checkViewCompleteness — layout bindings', () => {
212240

213241
it('flags a `map` block that declares neither coordinate form — `map: {}` is the unbound view with braces', () => {
214242
// `ListMapConfigSchema` requires no key, so block presence alone would
215-
// bless `map: { titleField }` on its way to `locationField || 'location'`.
243+
// bless `map: { titleField }` — which objectui's `ObjectMap` refuses by
244+
// its `hasCoordinateBinding` gate exactly as it refuses an absent block.
216245
for (const map of [{}, { titleField: 'title' }, { latitudeField: 'lat' }, { longitudeField: 'lng' }]) {
217246
const f = only(checkViewCompleteness({ type: 'map', map }) as never);
218247
expect(f.rule).toBe(VIEW_LAYOUT_WITHOUT_BINDING);
219248
expect(f.severity).toBe('warning');
220249
expect(f.path).toBe('map.locationField');
221-
expect(f.message).toContain("locationField || 'location'");
250+
// [#19630] The literal `locationField || 'location'` floor this message
251+
// used to quote is gone on both faces (objectui#8169); the body names
252+
// the refusal the renderer shows instead, and the keys that clear it.
253+
expect(f.message).not.toContain("|| 'location'");
254+
expect(f.message).toContain('Map configuration required');
255+
expect(f.message).toContain('hasCoordinateBinding');
256+
expect(f.message).toContain('map.locationField');
257+
expect(f.message).toContain('map.latitudeField');
222258
expect(f.fix).toContain('locationField');
223259
expect(f.fix).toContain('latitudeField');
224260
}

0 commit comments

Comments
 (0)