Feature plan: Step 1 · Step 2 · Step 3 · Step 4

Debug mode: enabled

Effort study: railspike-effort / v3/calendar; host ledger. Standing P2 enrollment.

Extended estimates: enabled

Status: Step 3 approved 2026-11-04 with the schema version correction below; Step 4 on feat/calendar. Step 2 v3 approved. Repository update after the merge of main on 2026-11-04: work tracking was removed upstream; Stage 4 now follows the title directive precedent, Stage 2 replays feedback item PF-20261104-B10, and no Work: trailer applies. main reached read-model schema 48 earlier the same day (8651cbcb, delete-record label indexing), so Stage 3 bumps 48 → 49 and seeds its rebuild test at 48; the maintainer approved Step 3 with that correction. Stage scopes and estimates are otherwise unchanged. Steps 1–3, the debug log and the ledger were first committed to main on 2026-11-04 at the maintainer's explicit request; the mockup file stays uncommitted.

Cycle estimate: 0.90

Mockup: calendar_mockups.html (source unavailable). Conversation notes for the implementer: addendum. Precedent: the event time cycle added one event header end to end; Stages 1–3 follow its shape.

Completion Contract

  • Normal entry: a member opens /calendar or /calendar/<tag> (the couch calendar is /calendar/couches), moves between months, opens a day, and posts through the shared composer with the date fixed, an optional last day, and the tag preset on a tag calendar. An operator replies event: confirmed or event: declined, or posts the event with the line.

  • End-to-end outcome: the day pages and the grid list the event on every day of its span under the author's username; confirmed days are marked; declined events leave the grid but stay in their thread; the thread page shows the state under the title.

  • Required recovery: a refused post keeps the draft with the reason, as the composer does today; event support off shows operators the flag link and nothing else; a read model from the prior schema version is rebuilt on startup before the calendar query runs; records without the new header parse and render exactly as before.

  • Deployment/external verification: deployed to the maintainer's instance by the usual pull; the invite-link path is replayed with an unapproved then approved host; recorded as done or explicitly not done.

  • Release condition: Step 2 success criteria met locally with JavaScript on and off, phone and desktop widths, text and icon themes, plus the production replay above.

Key Risks

  • High risk: format and schema change. Impact: a wrong header breaks parsing of every post; a missed read site shows no span. Early validation: Stage 1 confirms how the current parser treats an unknown root header (older releases must keep reading new records as single-day events) and Stage 3 greps every event_time site to mirror it. Mitigation: optional header, additive column, prior-version rebuild test, byte-identical rendering of old records asserted.

  • High risk: directive authority. Impact: a member confirming their own offer corrupts the grid. Early validation: Stage 4 test that a non-operator's and a quoted line change nothing, before any page uses the state. Mitigation: operator check as TitleDirectiveAuthority makes it (RetractionAuthority::currentOperator) plus approved, unretired and verified attribution of the line's author.

  • Day-state correctness across spans and month boundaries. Impact: wrong empty/open/confirmed marks. Early validation: Stage 5 fixtures with overlapping spans, a span crossing a month edge, and a confirmed plus declined pair. Mitigation: one derivation function with table-driven tests.

  • Grid density and overflow. Impact: unreadable cells on phones. Early validation: Stage 8 layout checks with a five-event day. Mitigation: cap entries per cell with a link to the day page; table scrolls in its container.

  • Composer placed outside /compose/thread. Impact: signing, drafts or Outbox scripts assume the compose page. Early validation: Stage 6 posts from a day page with JavaScript on and off. Mitigation: render the day page with the same scripts and hidden fields the compose page uses.

Stage 1

Estimate: 0.08

  • Goal: a thread root may carry Event-End-Date; replies may not; old records are unchanged.

  • Dependencies: none.

  • Expected changes: PostRecord gains a trailing nullable eventEndDate; PostRecordParser adds the header to EVENT_HEADERS, parses it with the existing date check, and rejects an end before the start; docs/specs/canonical_post_record_v1.md gains the missing event header section covering all five headers.

  • Verification approach: parser cases for absent, valid span, end before start, end on a reply; confirm by test how an unknown root header is treated today so the compatibility statement in the spec is true.

  • Estimate details: driver: mirroring the event-time stage; uncertainty: unknown-header behaviour of older parsers.

  • Risks or open questions: see Key Risks (format).

  • Canonical components/API contracts touched: canonical post record V1, PostRecordParser, PostRecord.

Stage 2

Estimate: 0.12

  • Goal: the composer and both write paths accept an optional last day.

  • Dependencies: Stage 1.

  • Expected changes: LocalWriteService reads, normalizes and gates event_end_date in the plain and prepared thread flows, refuses end before start, and emits the header after Event-Date; the composer's Event disclosure gains an Until date field and the disclosure summary includes it; browser_signing.js sends the field; ComposeAndAccountKeyController prefills it from the query and on error re-render; agent API field documentation.

  • Verification approach: write tests for span, single day, end before start, flag off; fetch /compose/thread?event_date=…&event_end_date=… and confirm both inputs are prefilled; signed prepare path carries the field; replay feedback item PF-20261104-B10 (the Event disclosure cannot be closed again) in the browser before and after adding the field, and record the result in the item; fix it only if the cause is in the lines this stage changes, otherwise leave the item as replayed.

  • Estimate details: driver: four touch points per field, as event time needed; uncertainty: low.

  • Risks or open questions: none beyond Stage 1.

  • Canonical components/API contracts touched: /compose/thread, /api/prepare_thread, /api/create_thread input fields, shared composer partial.

Stage 3

Estimate: 0.12

  • Goal: the read model stores the span and the event block shows it.

  • Dependencies: Stage 1.

  • Expected changes: threads.event_end_date nullable column; ReadModelMetadata::SCHEMA_VERSION 48 → 49; builder, incremental updater, ThreadRepository and Application::fetchThread select lists; docs/specs/read_model_schema_v1.md; event_block.php renders "date to end date" when present; a regression test seeded at version 48 proving startup rebuilds before the new column is read.

  • Verification approach: incremental and full rebuild agree for span and single-day threads; old fixture thread renders byte-identical; ./v3 rebuild.

  • Estimate details: driver: contributing-rule obligations for a column; uncertainty: low.

  • Risks or open questions: see Key Risks (schema).

  • Canonical components/API contracts touched: read-model schema V1, ReadModelSchema, ReadModelBuilder, IncrementalReadModelUpdater, event_block.php.

Stage 4

Estimate: 0.12

  • Goal: an event thread has one derived state set by operator event: lines, shown on the thread page.

  • Dependencies: none (Stage 3 for the span, not for the state).

  • Expected changes: EventStatusDirective::extract(string $body): ?array for event: confirmed|declined with the quoted-line rule; EventStatus::forThreads(PDO, list<string>): array reading root and replies, latest valid line from an active operator with verified attribution, approved and unretired; ThreadAndPostPageController passes the state for threads with an event date; thread.php shows an Event: line under the title.

  • Verification approach: tests for latest wins, root line, non-operator ignored, quoted ignored, retracted post ignored, thread page markup.

  • Estimate details: driver: sibling of TitleDirective with read-time derivation and a stricter author check; uncertainty: operator check needs the repository root alongside the PDO.

  • Risks or open questions: see Key Risks (authority).

  • Canonical components/API contracts touched: thread page template, TitleDirective scanning rules, RetractionAuthority::currentOperator.

Stage 5

Estimate: 0.20

  • Goal: /calendar and /calendar/<tag> show month grids of events with navigation.

  • Dependencies: Stages 3 and 4.

  • Expected changes: CalendarPageController with grid(?string $tag, ?string $month): string; CalendarQuery::eventsBetween(PDO, string $from, string $to, ?string $tag): array over the threads table, tag matched as TagPage does; CalendarDays::derive(events, states): array giving each day its entries and empty/open/confirmed mark; calendar.php rendering two months as tables with previous and next links (?month=YYYY-MM), a link between the tag view and all events, and entries capped per cell with a link to the day; routes registered as application routes; event support off shows operators the flag link; navigation entry explicitly not added.

  • Verification approach: route tests with fixtures for single day, span, span across a month edge, overlapping confirmed and declined, declined hidden, tag filter by board tag and by label; members-only gate sends an unapproved viewer to the Lobby; identical markup for two approved viewers.

  • Estimate details: driver: new page, query and derivation; uncertainty: month arithmetic and timezone (use the instance's date, UTC, consistently).

  • Risks or open questions: see Key Risks (day state, density).

  • Canonical components/API contracts touched: TagPage membership query, membersOnlyRequestAllowed route list, page template registry.

Stage 6

Estimate: 0.14

  • Goal: a day page lists that day's events and posts a new one with the date fixed and the tag preset.

  • Dependencies: Stages 2 and 5.

  • Expected changes: CalendarPageController::day(?string $tag, string $date): string at /calendar/<date> and /calendar/<tag>/<date>; calendar_day.php listing entries with author, span, place and state, then the shared composer rendered as on /compose/thread with eventDate fixed, eventEndDate optional and boardTags preset to the tag; submission uses the existing plain and signed routes; a validation error re-renders through the existing compose error path with the draft kept.

  • Verification approach: JavaScript-off form post creates the thread with the right headers and redirects to it; signed post through the browser test harness; the day page shows the new event; error re-render keeps body and date.

  • Estimate details: driver: composer placement and script set; uncertainty: Outbox and signing script assumptions about the page.

  • Risks or open questions: see Key Risks (composer placement).

  • Canonical components/API contracts touched: shared composer partial and its hidden fields, compose scripts, /compose/thread.

Stage 7

Estimate: 0.08

  • Goal: people and agents can find and use the calendar without reading code.

  • Dependencies: Stages 5 and 6.

  • Expected changes: docs/reference/calendar.md (pages, tag calendars, span, directive, flag, access); concept entries docs/concepts/calendar.md and docs/concepts/post_event_metadata.md; README docs index; docs/reference/agent_api.md mentions event_end_date; /llms.txt or the API index lists /calendar if other pages are listed there.

  • Verification approach: documentation review against the shipped behaviour; links resolve.

  • Estimate details: driver: wording; uncertainty: low.

  • Risks or open questions: none.

  • Canonical components/API contracts touched: concept inventory, agent API reference.

Stage 8

Estimate: 0.12

  • Goal: release verification and production replay.

  • Dependencies: Stages 1–7.

  • Expected changes: no product changes expected; browser layout checks for the grid and day page at phone and desktop sizes across text and icon themes, with a five-event day; JavaScript-off pass; ./v3 rebuild from the prior schema version on a copy of real data; deploy to the maintainer's instance; replay the invite-link entry with an unapproved then approved host; record results in the Step 4 summary and the UI verification notes; no feedback item matches the calendar itself, so commits carry a Feedback: trailer only for PF-20261104-B10 if Stage 2 fixed it.

  • Verification approach: Step 2 success criteria checked one by one; smoke set plus the tests added in Stages 1–6; the full suite is not run by the agent.

  • Estimate details: driver: theme and device matrix; uncertainty: production timing.

  • Risks or open questions: see Key Risks (density).

  • Canonical components/API contracts touched: none new.

Concepts Touched

Shared composer used unchanged (Stage 6 placement); thread title directive pattern reused (Stage 4); tag browsing membership query reused (Stage 5); read model rebuild changed (Stage 3); viewer session and invitation lifecycle used unchanged (Stage 8 replay). Created in Stage 7: calendar and post event metadata.

Study Checkpoint

2026-11-04T18:00:00Z; scope v3/A, original baseline f41f656936e740809242a5d8e1c646068afddc28, R1/P2, hashes verified earlier this session. User explicitly approved Step 2 v3. Whole-cycle 0.90 reaffirmed after inspecting the event-time precedent, both thread write flows, TagPage membership, WorkItems, the compose controller and the rebuild test pattern. Eight stage-work originals 0.08/0.12/0.12/0.12/0.20/0.14/0.08/0.12, non-additive; shared planning and ledger bookkeeping owned by the cycle, not charged to stages. Claude, single exposed rater. No implementation, measured labor or accepted outcome. Await Approved Step 3; planning uncommitted.

— Claude · 2026-11-04 UTC · Model: claude-fable-5-1

— Revised by Claude · 2026-11-04 UTC · Model: claude-opus-5-5 (status and Stage 3 schema version only)