…n act on
`ObjectCalendar`'s unconfigured-view screen read "Calendar configuration
required. Please specify startDateField and titleField." The first clause was
right and is unchanged; the second was wrong twice.
It demanded an optional key. `@objectstack/spec`'s `CalendarConfigSchema` is a
strict object whose one required key is `startDateField` — re-measured on the
installed 17.4.0, three legs: `{}` and `{ titleField: 't' }` both fail
`invalid_type` at `startDateField`, `{ startDateField: 'd' }` parses clean. The
spec's own note on that schema names this renderer as the reason: `resolveTitle`
takes an explicit `titleField` when present and otherwise resolves through the
ADR-0079 display-name chain.
And it named keys without saying where they go, which is unactionable on the
interface-page door: that surface's `interfaceConfig` carries no calendar key at
all, so its only lever is `sourceView` — which the screen never mentioned.
The copy is door-COMPLETE rather than door-AWARE, and that is measured rather
than chosen for convenience. Two producers emit an `object-calendar` node (the
calendar branches of `plugin-list`'s `ListView` and `plugin-view`'s
`ObjectView`), reached from both app-shell faces, and every one hands this
component the same `baseProps` bag plus the declared binding keys — nothing on
the node names the door. A door prop would have to be declared on the published
node schema and threaded through four packages; inferring the door from the
object's date fields only correlates with it, because `InterfaceListPage`'s
deriver runs solely when `calendar` is whitelisted.
The first clause is untouched on purpose: five suites pin this screen with
`/Calendar configuration required/i` and `@object-ui/types`' alias tombstones
assert on the same phrase. Citations of the retired second clause inside this
package are clipped to the surviving clause rather than deleted.
Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018HrVaotisyhgmot9o2MLRq
Fixes #8170
ObjectCalendar's unconfigured-view refusal screen used to read:The first clause was right. The second was wrong twice — it demanded an optional key, and it
named keys without saying where they go, which is unactionable on the interface-page door. It
now reads:
The three spec legs, re-measured in this worktree
Installed artifact is
@objectstack/spec17.4.0, not the 17.3.0 the card's comment measured.Run from the worktree root, importing
CalendarConfigSchemafrom@objectstack/spec/ui(thepackage root does not export it):
Identical to the card's 17.3.0 reading, one version later. The declared shape is a strict object
of
startDateField(required) plus optionalendDateField/titleField/colorField, and thespec's own note on that schema names this renderer as the reason
titleFieldis optional:resolveTitletakes an explicit key when present and otherwise resolves through the ADR-0079record display-name chain.
Fact number 1 reproduces — with one number corrected
configRequiredandconfigurationRequiredare zero inpackages/i18n/src/locales/en.ts, andthe sentence sits in the JSX as plain text. Confirmed.
The companion number does not survive. A naive
grep -cforuseObjectTranslationor at(callreturns 15 lines, but that pattern also matches
const t = def.type;and every ordinary callwhose name ends in
t(such ashandleDateClickDefault(. The real i18n census of that file isthree translated strings — two through
useSafeTranslate(errors.unauthorized,table.saveFailed) and one throughuseObjectTranslation(calendar.unscheduled) — plus the twohook imports and the two hook calls. The conclusion still holds: the component is i18n-aware
elsewhere and this one string is not. That is a separate, separable defect and it is
deliberately NOT fixed here; see Acceptance notes.
The design question this card decides
The card offered three shapes. All three were measured and all three are refused, on measurement
rather than on convenience.
Passed as a prop — out of fence, by measurement. Two producers emit an
object-calendarnode:the calendar branch of
plugin-list'sListView, and the one inplugin-view'sObjectView.app-shell reaches the first from BOTH
InterfaceListPage(the interface-page door) and its ownObjectView(the object-view door). A door prop therefore has to be declared on the node'spublished schema and threaded through four packages, three of which this card's fence excludes.
Inferred from the schema — nothing to infer from. Every one of those producers hands this
component the same shared
basePropsbag (objectName, fields, filter, sort, className, showSearch,onRowClick, rowHeight, hideRowHeightToggle, and the two optional column-state keys) plus whichever
of
startDateField/endDateField/titleFieldthe author declared. Nothing on the node namesthe door. Inference is unavailable until the marker exists, which collapses into the prop option.
There is a tempting near-miss: ask whether the object carries a date field, since
InterfaceListPage.defaultCalendarFromObjectderives a binding from the first one it finds, sothis screen is only reachable on that door when the derivation came back empty. That is a
correlation, not an identity — the deriver only runs when
calendaris whitelisted inappearance.allowedVisualizations— and pinning user-facing copy to a predicate that lives inanother package is the "the gate and the seam must answer one question" failure this repo has
already recorded on
map,chartandkanban.Split into two keys — the premise is false. That shape presupposes a locale key. Fact number 1
measures a hard-coded English literal with no key behind it. There are no keys to split.
So the copy is door-COMPLETE rather than door-AWARE: it names the one place every door reads
the binding from — the view's
calendarblock — and then states the page door's indirectionoutright, because that door genuinely has no slot. Measured:
InterfaceListPagereadscolumns,sort,filterBy,userFilters,appearance,addRecord,userActions,showRecordCount,source,sourceView,buttonsandrecordActionoffinterfaceConfig, and no calendar key atall (occurrences of
cfg.calendar= 0; positive controlcfg.columns= 1).This is an implementation shape, not a product-semantics fork, so the card's stop-and-report clause
did not fire. The user-visible outcome is the same on both doors: an author is told the one key to
write and where it lives.
Every quoter of the sentence, and what happened to each
Eight files quote it. The fence admits four of them.
Changed, in fence:
ObjectCalendar.tsx— the literal itself, plus the retirement note ongetCalendarConfigwhosecitation is now clipped to the surviving clause with the reason kept.
ObjectCalendar.unconfiguredRefusal-7029.test.tsx— header citation clipped; its statement thatthe objectui#7029 card did not redesign the screen is kept and annotated.
ObjectCalendar.filterIsNotAConfigSlot-7711.test.tsx— same treatment.packages/plugin-calendar/README.md— its quote was already clipped to the surviving clause, sonothing went stale; a paragraph documenting the new remedy was added.
Reported, not touched — each is outside the declared file surface, and each is listed in the
report so the seat can decide:
packages/types/src/zod/objectql.zod.ts— thedateFieldalias tombstone'sbindingconsequencestring quotes the sentence verbatim in a user-facing authoring diagnostic. Its load-bearing
claim ("a screen that names the canonical keys and never the key you wrote") survives this change;
only the illustrative quote drifts. Its pin asserts on the first clause only, so it stays green.
packages/app-shell/src/views/ObjectView.tsxand itsObjectView.calendarBinding-7029.test.tsx—comment citations only. That test reads
ObjectView.tsxfrom disk, not this file, so it isunaffected.
.changeset/7711-calendar-filter-is-not-a-config-slot.md— a pending release note quoting thesentence verbatim. It publishes into the CHANGELOG at the next release and will then describe a
screen that never shipped that way.
.changeset/8355-...quotes the first clause only and staystrue.
Three more suites pin the screen with the first clause only and are unaffected:
calendarUnionReads-8651.test.tsx,calendar-date-alias-refusal-8355.test.ts, and the two REFUSALmatchers already listed. Not one pin was deleted.
Control reading — the new pin fails against today's wording
The fix was committed first, then the refusal block was reverted to the pre-card wording on disk,
with the mutation proved by occurrence count before the run and the restore proved by blob hash
afterwards. No build or dist is involved: the test imports the component by relative path inside
its own package.
Against the new wording the same file is
Tests 6 passed (6), exit 0. The asymmetry is the point:the three cases that move are exactly the three clauses this card adds, and the three that hold in
both worlds are the unchanged first clause plus the two render controls — so a change that simply
refused everything would not pass this file.
The strongest of those controls is the one that makes the screen's new promise checkable: a
calendar with a date binding and no
titleFieldrenders real record titles through the ADR-0079chain. If that ever regresses, the sentence on the refusal screen becomes a lie and the case says so.
Validation
All commands run from the worktree root, exit codes captured before any pipe.
--filter '@object-ui/plugin-calendar^...'pnpm exec vitest run packages/plugin-calendar/pnpm --filter @object-ui/plugin-calendar type-check--listFilesconfirms the new test is inside the program (1 hit, positive control 1, negative control 0)check:doc-snippets,check:doc-examples@object-ui/plugin-calendaritself, this package produces zero findings; the rest is a whole-tree build and belongs to CIcheck:readme-exportscheck:sdui-registration-pinsapps/consolebundle. This diff does not touchpackages/plugin-calendar/src/index.tsxat all--no-inline-config, on the four changed source filesno-explicit-any/ react-hooks noise of the same kind the sibling tests already carryThe eslint run is a declared narrowing, and here is why it is a measurement rather than a
shortcut. The judged population comes from
eslint.config.jsitself, which declares noparserOptions.projectand noprojectService(grep exit 1, positive control exit 0) — type-awarelinting is off. The file count, 4, is read from the
--format jsonoutput, not estimated. And withno type-aware linting, a file's verdict is a function of that file's own bytes plus the shared
config; this diff moves neither for any untouched file, so it cannot change an untouched file's
verdict. The repo-wide
pnpm lintis CI's run.Acceptance notes
Findings met along the way, none of them fixed here.
literal in a component that translates three other strings. Adding a key would need no locale
pack, because
useSafeTranslatecarries the English fallback — but it is a second, separabledefect and the dispatching seat reserved the call.
.changeset/7711-calendar-filter-is-not-a-config-slot.mdquotes the retired sentence verbatim.dateFieldalias tombstone's
bindingconsequence string inpackages/types/src/zod/objectql.zod.ts.retired clause and neither of which any instrument reads.
Session:
session_018HrVaotisyhgmot9o2MLRq.Generated by Claude Code