Skip to content

Commit 38472ce

Browse files
os-billclaude
andauthored
fix(spec): declare allDayField on CalendarConfigSchema, the key the object-calendar prescription already names (#17877)
Fixes #17054 `CalendarConfigSchema` now declares **`allDayField`**, the fifth field binding on a calendar config. ## The two sentences, and which one was wrong The `object-calendar` door refuses a flat `allDayField` and prescribes, verbatim from its own diagnostic: > Write this as a key of the `calendar` config object instead — `calendar: { startDateField, endDateField, titleField, colorField, allDayField }`. `CalendarConfigSchema` was a `strictObject` of exactly four keys and refused that shape by name. **The round measured which half was wrong rather than picking the convenient one, and the answer is (a) — the schema was missing a key that is honoured.** It is not (b): trimming the prescription would leave a shipped, honoured capability with no protocol carrier. The evidence, read at the objectui pin this repo builds against (`.objectui-sha` = `53ded82bf7a494f54e344e19099dbf00854b8694`, read with `git show PIN:path`, not at that checkout's HEAD): - `packages/plugin-list/src/ListView.tsx` — `collectViewFields` reads `v.allDayField` off `schema.calendar` and `schema.options.calendar` at two sites, feeding the `$select` projection and the `$expand` set. The authored nested key already changes what the server is asked for. - The same file's `case 'calendar':` branch spreads `...(schema.calendar || {})` onto the `object-calendar` node, so the nested key reaches the block. - `packages/plugin-calendar/src/ObjectCalendar.tsx` — `getCalendarConfig` resolves it into the calendar config. - `packages/app-shell/src/views/ObjectView.tsx` — the dev-mode Spec Compliance warning lists `allDayField` among the flat keys an author must move under `viewDef.calendar`: a third face prescribing the nested spelling. - `packages/types/src/zod/objectql.zod.ts` — the mirror keeps `.passthrough()` and names this key as its reason: *"the renderers grow config knobs ahead of the protocol (calendar's `allDayField`, for one), and stripping them here would silently disable a shipped capability."* - Post-pin, on objectui `main`, the renderer makes it load-bearing: `allDay: allDayField ? Boolean(record[allDayField]) : !endDate`. ⭐ A widening is not made acceptable by the diagnostic having promised it. This one is right because the renderer honours the key — the prescription merely happened to be the accurate half. **The countervailing reading, stated plainly.** objectui `main` carries a comment declaring `allDayField` objectui-LOCAL, in the same class as its sanctioned `defaultView`, and concluding "honouring `allDayField` widens no accept set". That is a true statement about what objectui needed in order to honour it, and it does not bind what the protocol may declare. The two keys are not the same class from this side: `defaultView` is the renderer's initial view mode, a UI preference that already has a declared home as an `object-calendar` component prop; `allDayField` is a field binding, the same kind as its four neighbours, and it had no home at all. `defaultView` stays refused on this config, pinned. ## A correction to the card's framing, measured The card reads as though the prescribed shape is refused at the door that printed the prescription. It is not. `ObjectCalendarPropsSchema.calendar` is `z.unknown()`, so the block accepts `calendar: { …, allDayField }` today. The second refusal lands one door over, on **stored view metadata** — `ListViewShapeSchema.calendar` is `CalendarConfigSchema` — which is how calendars are actually authored in this product. The trap is real; the two doors are just not the same door. Both readings are in the before/after table. ## Measured against the BUILT dist, before and after Build first and confirm both passes finished (`check-dts-emitted: 34/34 declared declaration file(s) present`), then parse through the package's own `./ui` export. | input | door | before | after | |---|---|---|---| | `{ objectName, allDayField }` flat | `object-calendar` | REFUSED `unrecognized_keys` keys=`["allDayField"]` | REFUSED, unchanged | | `calendar: { four, allDayField }` | `object-calendar` | ACCEPTED | ACCEPTED | | `{ four, allDayField }` | `CalendarConfigSchema` | REFUSED `unrecognized_keys` keys=`["allDayField"]` | ACCEPTED | | `calendar: { four, allDayField }` | `ListViewSchema` | REFUSED `unrecognized_keys` at `path: ["calendar"]` | ACCEPTED | | `{ four }` | `CalendarConfigSchema` | ACCEPTED (positive control) | ACCEPTED | | `{ four, bogusKeyXy }` | `CalendarConfigSchema` | REFUSED (negative control) | REFUSED | The exact refusal texts, before: - flat, on `object-calendar`: `Unrecognized key(s) on this \`object-calendar\`: \`allDayField\`.` followed by the prescription quoted above. - nested, on the config: `Unrecognized key(s) on this calendar configuration: \`allDayField\`. Until these shapes were closed an unknown key was dropped silently — the view still rendered, without whatever the key was meant to configure.` After, the first is byte-identical and the second is gone — replaced by acceptance. The bogus-key control still produces that second text verbatim with `keys=["bogusKeyXy"]`, which is what proves the message did not change, only the membership. ## Pins, both directions `packages/spec/src/ui/calendar-config-allday-prescription-17054.test.ts`, 9 cases. They import `./view.zod` and `./component.zod` — **`src/`, not `dist/`**, so no rebuild leg is needed for the ablation, and that is measured rather than assumed. **Accepted:** the prescribed shape at the config schema; the same shape through the stored-view door where the second refusal used to land; the same view without the key as a control. ⭐ The lead pin is written on the defect CLASS, not on one key: it reads the key list out of the `calendar: { … }` shape the runtime's own prescription prints and asks the config schema to accept each name, with a floor on the extracted list so an empty extraction cannot make it vacuously true. Any future diagnostic that names a non-member goes red here, including a key nobody has thought of yet. **Still refused — what the widening did NOT cost:** the flat `allDayField` on `object-calendar` (one key per concept, and the refusal still carries the prescription); `defaultView` on the config, so the opening is exactly one key wide; an unknown key, in the same message shape, at the config and at `path: ["calendar"]`; and `startDateField` is still required, so `allDayField` alone is not a calendar binding. ## Ablation Mutation: rename the declaration to `allDayFieldAblated` in `packages/spec/src/ui/view.zod.ts`. Absolute paths, `trap '…' EXIT INT TERM`. On-disk proof read FIRST, before the run: declaration occurrences `1 → 0`, injected text `0 → 1`, and the file's `git hash-object` moving `3ecc02a254265786fc29c146408479ed072ee462 → 560dc1d3299460e582e04d0a727a89600e6d23ba`. The run then aborts itself if the injected text is not present exactly once. Predicted direction: RED. Observed: `3 failed | 6 passed (9)`, exit 1 — exactly the three acceptance pins, with the lead pin failing on its own sentence: *"the prescription names `allDayField`, which CalendarConfigSchema refuses"*. GREEN after restore: `9 passed (9)`, exit 0. Restore proven by hash, not by an exit code: `git checkout HEAD -- ABSOLUTE_PATH` (never the bare form, which reads the index), restored hash `3ecc02a254265786fc29c146408479ed072ee462` equal to the HEAD blob, with an empty-hash guard treating a missing read as FAILURE, and `git diff HEAD` empty. ## Changeset `.changeset/17054-calendar-config-all-day-field.md`, grade **`minor`** — a published accept set widens, and `minor` is the floor for this class. Not `skip-changeset`, measured rather than assumed, with `npm pack --dry-run --json` after a build and controls in both directions over the packed file list (2012 files): - subject `allDayField` → **52** published files. - positive control `startDateField` (a sibling key that must publish) → **55**. - negative control `bogusKeyXy`, which lives only in the new test file → **0**. - the new test file is **absent** from the packed list, and 0 `*.test.ts` files publish at all. - ⚠️ `packages/spec` also ships `src/**/*.zod.ts` **as source** and its tsup does not strip comments, so a source comment is published text: the probe phrase from the new TSDoc block appears in **23** published files. Controls were picked accordingly. Purely additive — nothing that parsed before is refused now, and no key is renamed or removed, so there is no ADR-0087 disposition to declare. ## Verification Head `7803e3d6fd`. `origin/main` merged via `scripts/pm/os-regen-merge.sh` before opening; main brought driver-sql and lint changes only, no `packages/spec`, and no contact with PR #17796's `ui/view.zod.ts` hunks — that PR is not addressed here and remains open. Every number below is from the final head, after the merge. - `pnpm --filter @objectstack/spec test` (`--project local`) :: exit 0 — 473 files / 13443 tests, 0 skipped. - `pnpm --filter @objectstack/spec test:repo` (`--project repo`, the cross-corpus scanners) :: exit 0 — 30 files / 520 tests. ⭐ Run separately on purpose: `test` is not the whole suite. - `pnpm --filter @objectstack/spec typecheck` :: exit 0 — including `check:test-typecheck` (shrink-only ledger held). - `pnpm --filter @objectstack/spec check:generated` :: exit 0 — all 15 generated artifacts up to date; `authorable-surface/ui.json` gained exactly one line, `ui/CalendarConfig:allDayField`, and `authorable-surface.base.json` was not touched. - `pnpm lint` (`eslint . --no-inline-config`, repo-wide) :: exit 0 — no narrowing claimed. - Derived families via `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack`, re-derived on the merged head and identical to the pre-merge derivation: **106 derived, 104 run green, 2 NOT MEASURED, 0 unrun** (`--ran` reconciliation exits 0). Exit codes captured before any pipe. - ⛔ **NOT MEASURED, declared, not green:** `pnpm check:dual-build-cjs-loads` and `pnpm check:type-check-debt`, both exit **3** — PREREQUISITE NOT MET. Each needs every workspace package built (`turbo run build --filter='./packages/*' --filter='./packages/*/*'`), which is the whole-farm run CI owns; nothing about them is answerable from a spec-only closure. This is a declared narrowing, not a skipped gate, and their verdicts are CI's. - Control bytes: `pnpm check:nul-bytes` :: exit 0, plus a direct scan of all seven changed paths for the wider control-byte class — no match, grep exit 1. Clause-②: yes — this widens a published accept set, so the round's measurement agrees with the value declared at dispatch. `needs:contract-review` rides on both carriers and this does not enqueue without an at-tier verdict on the head that lands. ## 验收备注 Out of scope, noted and not filed — each with its carrier named: - `ObjectCalendarPropsSchema.calendar` is `z.unknown()`, so the component door validates nothing about the config it names in its own `.describe()`. Tightening it to `CalendarConfigSchema` would narrow a published accept set and needs its own ruling; it is not a defect, it is an unbuilt door. Carrier: whoever next converges the component-door configs. - objectui's mirror comment and its `ObjectCalendar` docblock both state that `allDayField` is not a spec key. Once this lands, both are stale. objectui#8831 is already the declared follow-up and triage named it, so this is not a new card. Carrier: objectui#8831. - The objectui-side `list-view-spec-parity` pin lists `defaultView` as the only sanctioned local key on the calendar config; the mirror derives from the spec schema, so it picks up this key without an edit. Nothing to do, recorded so the next reader does not go looking. Carrier: objectui#8831. --- _Generated by [Claude Code](https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 6ec467b commit 38472ce

7 files changed

Lines changed: 233 additions & 9 deletions

File tree

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
---
2+
"@objectstack/spec": minor
3+
---
4+
5+
`CalendarConfigSchema` now declares **`allDayField`** — the fifth field binding on a calendar config, and the one key the rest of this package already published as a member while the schema refused it by name.
6+
7+
**The trap this closes.** The `object-calendar` door refuses a flat `allDayField` and prescribes, verbatim: *"Write this as a key of the `calendar` config object instead — `calendar: { startDateField, endDateField, titleField, colorField, allDayField }`."* That block's `calendar` prop `.describe()` publishes the same five-key shape, and it ships to `content/docs/references/ui/component.mdx`. An author who followed the prescription on a stored view was refused a **second** time, by a different schema with a different message — `Unrecognized key(s) on this calendar configuration: allDayField` — and neither message said the key was not a member at all, so the natural next move was to assume a typo and try more spellings.
8+
9+
**Why the schema was the wrong half, measured rather than assumed.** The key is honoured, not inert. At the objectui pin this repo builds against, `ListView`'s `collectViewFields` reads `calendar.allDayField` into the fetch projection and its calendar branch forwards the authored block onto the `object-calendar` node, where `getCalendarConfig` resolves it; objectui then made it load-bearing in the render itself. Trimming the prescription instead would have left a shipped capability with no protocol carrier — and the mirror that carries it today keeps `.passthrough()` explicitly so the key is not stripped, which means a later hardening there would silently drop it.
10+
11+
**What is authorable, and what still is not.**
12+
13+
```ts
14+
// accepted
15+
calendar: { startDateField: 'start_date', endDateField: 'end_date',
16+
titleField: 'subject', colorField: 'status', allDayField: 'is_all_day' }
17+
18+
// still refused — one key per concept, not a second authorable spelling
19+
{ type: 'object-calendar', allDayField: 'is_all_day' }
20+
```
21+
22+
`allDayField` **names a boolean field, not a value**: a record whose flag is true draws as an all-day band rather than at a clock time, and one whose flag is absent or false is not all-day. Omit it and the renderer's existing inference is untouched — an event with no end date draws as all-day — so every calendar that never authored the key renders exactly as before.
23+
24+
**The opening is one key wide.** `defaultView` stays refused on this config: it is the renderer's initial view mode, a UI preference rather than a field binding, and it already has its own declared home as an `object-calendar` component prop. Unknown keys are refused in the same shape as before, and `startDateField` is still required.
25+
26+
Purely additive: nothing that parsed before is refused now, and no key is renamed or removed.

content/docs/references/api/protocol.mdx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1641,7 +1641,7 @@ The published metadata item body, opaque by ruling (1C). Shape is the item's own
16411641
| **navigation** | `{ mode?: Enum<'page' \| 'drawer' \| 'modal' \| 'split' \| 'popover' \| 'new_window' \| 'none'>; view?: string; preventNavigation?: boolean; openNewTab?: boolean; … }` | optional | Configuration for item click navigation (page, drawer, modal, etc.) |
16421642
| **pagination** | `{ pageSize?: integer; pageSizeOptions?: integer[] }` | optional | Pagination configuration |
16431643
| **kanban** | `{ groupByField: string; summarizeField?: string; columns: string[] }` | optional | Kanban-board configuration — applies when the view renders as a kanban layout |
1644-
| **calendar** | `{ startDateField: string; endDateField?: string; titleField?: string; colorField?: string }` | optional | Calendar configuration — applies when the view renders as a calendar layout |
1644+
| **calendar** | `{ startDateField: string; endDateField?: string; titleField?: string; colorField?: string; … }` | optional | Calendar configuration — applies when the view renders as a calendar layout |
16451645
| **gantt** | `{ startDateField: string; endDateField: string; titleField: string; progressField?: string; … }` | optional | Gantt-timeline configuration — applies when the view renders as a gantt layout |
16461646
| **gallery** | `{ coverField?: string; coverFit?: Enum<'cover' \| 'contain'>; cardSize?: Enum<'small' \| 'medium' \| 'large'>; titleField?: string; … }` | optional | Gallery/card view configuration |
16471647
| **timeline** | `{ startDateField: string; endDateField?: string; titleField: string; groupByField?: string; … }` | optional | Timeline view configuration |
@@ -1726,7 +1726,7 @@ The published metadata item body, opaque by ruling (1C). Shape is the item's own
17261726
| **navigation** | `{ mode?: Enum<'page' \| 'drawer' \| 'modal' \| 'split' \| 'popover' \| 'new_window' \| 'none'>; view?: string; preventNavigation?: boolean; openNewTab?: boolean; … }` | optional | Configuration for item click navigation (page, drawer, modal, etc.) |
17271727
| **pagination** | `{ pageSize?: integer; pageSizeOptions?: integer[] }` | optional | Pagination configuration |
17281728
| **kanban** | `{ groupByField: string; summarizeField?: string; columns: string[] }` | optional | Kanban-board configuration — applies when the view renders as a kanban layout |
1729-
| **calendar** | `{ startDateField: string; endDateField?: string; titleField?: string; colorField?: string }` | optional | Calendar configuration — applies when the view renders as a calendar layout |
1729+
| **calendar** | `{ startDateField: string; endDateField?: string; titleField?: string; colorField?: string; … }` | optional | Calendar configuration — applies when the view renders as a calendar layout |
17301730
| **gantt** | `{ startDateField: string; endDateField: string; titleField: string; progressField?: string; … }` | optional | Gantt-timeline configuration — applies when the view renders as a gantt layout |
17311731
| **gallery** | `{ coverField?: string; coverFit?: Enum<'cover' \| 'contain'>; cardSize?: Enum<'small' \| 'medium' \| 'large'>; titleField?: string; … }` | optional | Gallery/card view configuration |
17321732
| **timeline** | `{ startDateField: string; endDateField?: string; titleField: string; groupByField?: string; … }` | optional | Timeline view configuration |

content/docs/references/data/object.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -375,7 +375,7 @@ const result = ApiMethod.parse(data);
375375
| **navigation** | `{ mode?: Enum<'page' \| 'drawer' \| 'modal' \| 'split' \| 'popover' \| 'new_window' \| 'none'>; view?: string; preventNavigation?: boolean; openNewTab?: boolean; … }` | optional | Configuration for item click navigation (page, drawer, modal, etc.) |
376376
| **pagination** | `{ pageSize?: integer; pageSizeOptions?: integer[] }` | optional | Pagination configuration |
377377
| **kanban** | `{ groupByField: string; summarizeField?: string; columns: string[] }` | optional | Kanban-board configuration — applies when the view renders as a kanban layout |
378-
| **calendar** | `{ startDateField: string; endDateField?: string; titleField?: string; colorField?: string }` | optional | Calendar configuration — applies when the view renders as a calendar layout |
378+
| **calendar** | `{ startDateField: string; endDateField?: string; titleField?: string; colorField?: string; … }` | optional | Calendar configuration — applies when the view renders as a calendar layout |
379379
| **gantt** | `{ startDateField: string; endDateField: string; titleField: string; progressField?: string; … }` | optional | Gantt-timeline configuration — applies when the view renders as a gantt layout |
380380
| **gallery** | `{ coverField?: string; coverFit?: Enum<'cover' \| 'contain'>; cardSize?: Enum<'small' \| 'medium' \| 'large'>; titleField?: string; … }` | optional | Gallery/card view configuration |
381381
| **timeline** | `{ startDateField: string; endDateField?: string; titleField: string; groupByField?: string; … }` | optional | Timeline view configuration |

0 commit comments

Comments
 (0)