docs(zs): calendar view design memo (T12a) - #15
Merged
Merged
Conversation
Answers every T12a checklist item as a numbered decision with its reason: event DTO and string caps, all-day and multi-day handling, timezone, recurrence expansion window, month and agenda rendering, paging, keyboard operation, screen-reader semantics, create/edit conflict handling, and the component choice (no new calendar dependency). Every decision is bound to the accepted T11 authorization contract and cites the T11 decision that constrains it. Code citations verified against zs/main. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Signed-off-by: wiggdevin <202901685+wiggdevin@users.noreply.github.com>
Sol run 1 raised one promoted BLOCK and ten verified WARNs against the T12a
memo. Every one now changes a decision:
- Truncated coverage is a new decision 13: a cap-exhausted `events.list` is
never the authoritative snapshot for the dates it did not reach, those dates
render as unknown rather than empty, and its events are read-only.
- Decision 1 replaces the single `editable` boolean with `canEdit`/`canDelete`
narrowed by event type and organizer per T11 decision 3, adds a `truncated`
flag per capped string, and normalizes the timed variant to an instant plus
an optional source zone.
- Decision 6 moves the event layer off the `DayButton` seam onto a
`Week`/`MonthGrid` override, so the "+N more" indicator and cross-day bars
are siblings of the day buttons; decisions 3 and 9 agree with it.
- Decisions 7, 9 and 10 reconcile the agenda with virtualization: one tab stop
with roving focus, `role="list"` plus `aria-posinset`/`aria-setsize` from the
wrapper, `labelDayButton` as the naming seam, and DayPicker's own status
region instead of a second announcer.
- Decision 4 states all-day arithmetic as plain calendar-date arithmetic on the
`{ date }` strings, which `date-fns` v4 core can do.
- Decision 8 keeps paging inside T11's fixed 120-day horizon, removing the
contradiction with decision 5.
- Decision 7 defines multi-day and all-day agenda placement and ordering.
- Decision 11 adds a client-supplied `events.insert` id, dirty-field patching,
read-only truncated fields, and 404 disambiguation against T11 decision 8's
terminal class.
Gate: just file-size-check exit 0. Memo is 89 lines, under the 90-line cap.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: wiggdevin <202901685+wiggdevin@users.noreply.github.com>
Sol run 2 left one BLOCK and three WARNs above NIT. Each now changes a decision: - N1: cache eviction painted an authoritative-empty calendar the same way cap exhaustion did. Decision 13 now holds the coverage marker outside the evictable rows and has T11's eviction downgrade each interval it touches to `truncated(evicted)` in the transaction that drops the rows, so a snapshot reads `complete` only while its rows are retained. T11 owns that write; a risk records the dependency. - N2: decision 11's 404 probe no longer purges on a transient or ambiguous probe. A clean probe closes the form, T11 decision 8's terminal class purges, and anything else rejects that one command behind the stale, read-only snapshot until `stale_after`. - N3: decision 6 named `Week` and `MonthGrid` together, which cannot hold a non-cell layer. It now overrides `MonthGrid` alone, forwarding `role="grid"` and the grid label to an inner `<table>` and painting the segment and "+N more" overlay beside it, with `Week`, `Day` and the `DayButton` override untouched. - N4: decision 7 adds `role="listitem"` beside `aria-posinset`/`aria-setsize` on the row div and puts `role="list"` on the spacer that owns them; decision 9 sequences agenda focus as index, `scrollToIndex`, mount, focus. The traversal is verified at T11's 5,000-row bound. - N5: decision 11 fixes the `events.insert` id encoding to a UUID's 32 hex digits, lowercase and unhyphenated, inside Google's base32hex alphabet. Prose across the memo is tightened to stay inside the 90-line limit; the file is 89 lines. Every `file:line` re-verified with sed on this branch. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Signed-off-by: wiggdevin <202901685+wiggdevin@users.noreply.github.com>
…gs (T12a)
Decision 13 now states how a cap-stopped `events.list` walk proves the interval
it reached: pages arrive ordered by start (decision 5's `orderBy=startTime`)
under T11 decision 6's caps (T11:51), a page cut mid-stream is discarded whole,
and the proven interval is [window start, min(window end, the last complete
page's maximum end)]. Everything past that bound renders "unknown, more…" and is
not editable.
Also from the run-3 audit: decision 9 gives every navigable agenda row a named
focusable, so arrowing onto a day header or the boundary row terminates; decision
7 derives the agenda row maximum from the same bound as the cache (T11:49-50) and
verifies against it; decision 10 composes `labels.labelDayButton` rather than
replacing it, keeping the "today" and "selected" announcements. Nits: the DTO
carries the event id, the `MonthGrid` override forwards every prop it receives,
`T11:66-69` and `labels.labelDayButton` replace the two slips, and each capped
text field is `{ value, truncated }`.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: wiggdevin <202901685+wiggdevin@users.noreply.github.com>
…atus region Decision 13 closed the proven interval at the last complete page's maximum end. Under decision 5's orderBy=startTime a complete page proves an ordering on start only, so days between that page's maximum start and its maximum end could hold unfetched events and still render complete and editable. State the invariant with T11 decision 6's caps (T11:51): the interval is half-open, [window start, min(window end, the last complete page's maximum start)); zero complete pages prove nothing. Events already received that run past the bound still render as known. Decision 10 pointed at "DayPicker's own role=status region". Version 10.0.1 has two: the caption, always rendered (dist/esm/DayPicker.js:293, :282 under a dropdown), and Footer, mounted only while props.footer is truthy (:341). Give the caption month changes and Footer the boundary and truncated text, with a non-empty Footer line from first render, so decision 8's end-of-window state cannot announce into an unmounted region. Decision 8 now names that line as the month's end-of-window surface. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Signed-off-by: wiggdevin <202901685+wiggdevin@users.noreply.github.com>
… (T12a) Run 5's WARN: the proven interval ends at an instant while the rendered unit is a date, so decision 13 now says a date is authoritative only when it lies wholly inside the interval, in decision 2's calendar zone; the date holding the bound renders "unknown, more…" with those past it and is not editable. Nits: decision 6 says the MonthGrid override spreads every received prop onto the inner table and renders children inside it, leaving only the overlay outside; decision 7 counts the 121 dates a 120-day window anchored at an arbitrary instant touches (5,122 rows, restated in the open verification) and states the list/listitem roles as opt-in props the calendar passes rather than a new default for VirtualizedList's other consumers; decision 10 gives the agenda/month toggle an accessible name and aria-pressed. Wording elsewhere is tightened only to hold the 90-line limit. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Signed-off-by: wiggdevin <202901685+wiggdevin@users.noreply.github.com>
…-design Signed-off-by: wiggdevin <202901685+wiggdevin@users.noreply.github.com>
wiggdevin
marked this pull request as ready for review
September 5, 2026 15:17
wiggdevin
enabled auto-merge
September 5, 2026 15:18
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
T12a's design memo:
docs/plans/2026-09-04-calendar-view-design.md, 90 lines, thirteen numberedDecision: … Reason: …entries plusOpen verificationsandRisks accepted. Docs only — no code in the diff.What it decides, under the accepted T11 contract (cited by line):
CalendarEventDTO keyed by Google's opaqueid,start/endas{ date }or{ dateTime, timeZone? }, text fields carrying a per-field{ value, truncated }cap;canEdit/canDeletefrom T11'saccessRole, revalidated at call time. All-day values stay a half-open day range in the calendar's zone; display uses one OS zone per render, so no new time-zone dependency is added. Recurrence expands at Google inside T11's 120-day window — no RRULE parser enters the fork.react-day-picker@^10.0.1stays; oneCalendarMonthwrapper overrides theMonthGridslot, spreading every received prop onto the inner<table>and renderingchildreninside it, with the segment/"+N more" overlay outside it. The agenda is a flat virtualized list through the existingVirtualizedList, bounded at 5,122 rows by T11's 5,000-row partition cap. Paging stops at T11's horizon and renders a named end-of-window state, never an empty month. No calendar-widget dependency.labels.labelDayButtoninstead of replacing it; the library's tworole="status"regions carry month changes and boundary/truncation text. Edits patch changed fields only, are fenced byIf-Match/412, use a client-generated insert id for replay, and split Google's 404/410 by T11's terminal-vs-transient probe classes.[window start, min(window end, the last complete page's maximum start))underorderBy=startTime— pluscompleteortruncated(reason). In the calendar zone, only a date wholly inside that interval is authoritative; the date holding the bound and every date past it render "unknown, more…", never empty, and none is editable.GPT-5.6 Sol audit
Three read-only runs (
gpt-5.6-sol,model_reasoning_effort=high,-s read-only, exit 0), each triaged against the plan's rubric before anything was applied.audit.md) — Sol verdict line 1:BLOCK — the memo is not ready to serve as T12a's implementation contract.Verified after triage: 1 BLOCK, 10 WARN, 3 discarded.audit-delta-2.md) — Sol verdict line 1:OPEN. Sol raised 1 BLOCK, 2 WARN, 2 NIT. Verified after triage: 1 BLOCK, 3 WARN, 1 NIT, 2 discarded.audit-delta-3.md) — Sol verdict line 1:OPEN. Sol raised 2 BLOCK, 3 WARN, 3 NIT. Verified after triage: 1 BLOCK, 3 WARN, 4 NIT, 1 discarded.Sol read the vendor docs rather than the installed tree in all three runs; every library claim was re-verified against
react-day-picker@10.0.1innode_modules, and everyfile:linere-resolved withgit show origin/zs/main:<file> | sed -n '<n>p'.Claude critic rounds 4 to 6
critic-4.md) — FAIL: one BLOCK (decision 13 closed the proven interval at the last page's maximum end, whichorderBy=startTimedoes not prove, so a cap-stopped walk still painted unread days complete) and one WARN (decision 10 routed announcements to arole="status"region that is neither unique nor mounted when it first announces), plus five NITs.critic-5.md) — FAIL: both round-4 findings resolved and no regression anywhere else, but one WARN remained — the proven interval ends at an instant while the rendered unit is a date, and the memo never placed the date holding the bound — with four NITs carried.MonthGridoverride spreads every received prop onto the inner<table>and renderschildreninside it, leaving only the overlay outside. Decision 7 counts the 121 dates a 120-day window anchored at an arbitrary instant touches (5,122 rows, restated in the open verification) and states thelist/listitemroles as opt-in props the calendar passes rather than a new default forVirtualizedList's other consumers. Decision 10 gives the agenda/month toggle an accessible name andaria-pressed. Wording elsewhere was tightened only to hold the 90-line limit;just file-size-checkexits 0 andgit diff --checkis clean.Supersedes
docs/plans/2026-09-xx-calendar-view-design.md; it lands asdocs/plans/2026-09-04-calendar-view-design.md.truncated(evicted)in the dropping transaction. T12 inherits that dependency; the memo records the interim gap as a risk.Follow-ups
truncated(evicted)eviction downgrade. Until it lands, an evicted interval can still paint empty insidestale_after.labelDayButtoncomposition and announce-once for theFooterline, lane and "+N more" thresholds on the 250-event fixture, agenda traversal at 5,122 rows,If-Match/412 and insert-id replay against the mock server then the live checklist, and the equal-start page-boundary check (defence in depth only).desktop/src/features/messages/ui/useComposerLinkPreviews.test.mjs, "a stale in-flight upload cannot publish after the URL re-enters and a fresh upload wins", failed once in the pre-push gate and passed on a clean rerun of the full suite (6391 pass, 0 fail). This branch's tree differs fromorigin/zs/mainin one docs file, so the flake is not from this diff.🤖 Generated with Claude Code
https://claude.ai/code/session_01E51uwemNnQ6wdrBWU9EhPE
Need help on this PR? Tag
@codesmith-botwith what you need. Autofix is disabled.