Skip to content

docs(zs): calendar view design memo (T12a) - #15

Merged
wiggdevin merged 7 commits into
zs/mainfrom
docs/calendar-view-design
Sep 5, 2026
Merged

wiggdevin merged 7 commits into
zs/mainfrom
docs/calendar-view-design

Conversation

@wiggdevin

@wiggdevin wiggdevin commented Sep 5, 2026

Copy link
Copy Markdown

Summary

T12a's design memo: docs/plans/2026-09-04-calendar-view-design.md, 90 lines, thirteen numbered Decision: … Reason: … entries plus Open verifications and Risks accepted. Docs only — no code in the diff.

What it decides, under the accepted T11 contract (cited by line):

  • Event model (1-5). A CalendarEvent DTO keyed by Google's opaque id, start/end as { date } or { dateTime, timeZone? }, text fields carrying a per-field { value, truncated } cap; canEdit/canDelete from T11's accessRole, 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.
  • Rendering (6-8, 12). react-day-picker@^10.0.1 stays; one CalendarMonth wrapper overrides the MonthGrid slot, spreading every received prop onto the inner <table> and rendering children inside it, with the segment/"+N more" overlay outside it. The agenda is a flat virtualized list through the existing VirtualizedList, 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.
  • Operation (9-11). Two composite widgets, one tab stop each, roving focus, a named focusable on every navigable row; day naming composes labels.labelDayButton instead of replacing it; the library's two role="status" regions carry month changes and boundary/truncation text. Edits patch changed fields only, are fenced by If-Match/412, use a client-generated insert id for replay, and split Google's 404/410 by T11's terminal-vs-transient probe classes.
  • Truncation (13). A batch carries the interval it proves — [window start, min(window end, the last complete page's maximum start)) under orderBy=startTime — plus complete or truncated(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.

  • Run 1 (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.
  • Run 2 (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.
  • Run 3 (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.1 in node_modules, and every file:line re-resolved with git show origin/zs/main:<file> | sed -n '<n>p'.

Claude critic rounds 4 to 6

  • Round 4 (critic-4.md) — FAIL: one BLOCK (decision 13 closed the proven interval at the last page's maximum end, which orderBy=startTime does not prove, so a cap-stopped walk still painted unread days complete) and one WARN (decision 10 routed announcements to a role="status" region that is neither unique nor mounted when it first announces), plus five NITs.
  • Round 5 (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.
  • Round 6 (this commit) — applies round 5's W1 and N1 to N4 and stops there by driver decision, with no further critic round. Decision 13 now states that a date is authoritative only when it lies wholly inside the proven interval, in decision 2's calendar zone, so the date holding the bound renders "unknown, more…" with the dates past it and is not editable. 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 was tightened only to hold the 90-line limit; just file-size-check exits 0 and git diff --check is clean.

Supersedes

  • The plan's T12a bullet names the memo docs/plans/2026-09-xx-calendar-view-design.md; it lands as docs/plans/2026-09-04-calendar-view-design.md.
  • The plan's T12a eval reads "Sol reviews the memo". Sol reviewed it three times; rounds 4 to 6 ran as Claude critic passes by driver decision, and round 6 applied the remaining WARN and NITs without a further critic. The eval's substance — every checklist item has a decision, nothing above NIT remains — is what the rounds were held to.
  • Decision 13 requires a write on T11's side that the accepted T11 contract does not yet carry: eviction must downgrade every interval it touches to truncated(evicted) in the dropping transaction. T12 inherits that dependency; the memo records the interim gap as a risk.
  • Decision 7 changes a component seven surfaces share, but as opt-in props rather than a new shared default, so no other consumer's semantics move.

Follow-ups

  • T11 amendment for the truncated(evicted) eviction downgrade. Until it lands, an evicted interval can still paint empty inside stale_after.
  • The memo's five open verifications land in T12: the emitted ARIA roles off T12's DOM, labelDayButton composition and announce-once for the Footer line, 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).
  • Widening the 120-day horizon needs T11 amended; until then a date outside it is reachable only in Google's own UI.
  • Flake seen while pushing: 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 from origin/zs/main in one docs file, so the flake is not from this diff.

🤖 Generated with Claude Code

https://claude.ai/code/session_01E51uwemNnQ6wdrBWU9EhPE


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

wiggdevin and others added 7 commits September 4, 2026 20:26
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
wiggdevin marked this pull request as ready for review September 5, 2026 15:17
@wiggdevin
wiggdevin enabled auto-merge September 5, 2026 15:18
@wiggdevin
wiggdevin added this pull request to the merge queue Sep 5, 2026
Merged via the queue into zs/main with commit e213418 Sep 5, 2026
71 of 72 checks passed
@wiggdevin
wiggdevin deleted the docs/calendar-view-design branch September 5, 2026 15:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant