feat(cycle-dates): real lunar dates, an editable cycle window, and visible boundaries #39
No reviewers
Labels
No labels
area/auth
area/backend
area/ci
area/deploy
area/e2e
area/frontend
area/shared
blocked
good-first-issue
needs-info
priority/high
priority/low
priority/medium
type/bug
type/chore
type/docs
type/feature
type/tech-debt
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
celilo/lunacycle!39
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "cycle-dates"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Implements the
cycle-datesOpenSpec change (all 11 tasks inopenspec/changes/cycle-dates/tasks.md).Fixes the 2026-08-02 incident directly: no surface showed when the running cycle ends, so July's mid-flight cycle read as corrupted data.
⚠️ Design revised mid-implementation: the boundary is the FULL moon
The change as proposed defaulted the cycle end to the next new moon. Operator decision on review, now recorded as D2a in
design.md, flipped it to the full moon. The reasoning:MOON_NAMESare the traditional full-moon names, and D5 already named the cycle from its full moon.getLunarPhase().inWindow— the modification windowLoadIndicatorgates on — is true for full moon, waning gibbous and last quarter (lunar age ~15–22). On new-moon boundaries (age 0 → 29.5) that window lands in the middle of the cycle. On full-moon boundaries (14.8 → 14.8) it becomes the cycle's opening week, which is what "you can still change the plan" should mean.proposal.md,design.md(D2, D2a, D5) and the spec scenarios that hardcoded new-moon dates were rewritten to match — the docs were not left diverged from the code.Naming needed a second helper as a consequence: the window is now bounded by two full moons, and
nextFullMoon(start)returns the one that closes it. Cycles get started a day either side of the full moon, so "next" would name half of them for the following lunation — a whole-month error in the displayed name.nearestFullMoon(start)picks the lunation the window opens in, which is also what the spec's requirement text ("the lunation the window predominantly covers") always said.What changed
Shared —
nextNewMoon/nextFullMoon/nearestFullMooninpackages/shared/src/lib/lunarPhase.ts, by lunar-age projection against the mean synodic month (D1). No new dependency, no ephemeris table. Validated against every published 2026 new moon and full moon; worst calendar-day error across the year is 1 day, inside the spec's ±1 tolerance. The ceiling is marked with aponytail:comment.Server — new pure
cycleWindow.ts:planCycleWindow(proposed, now)resolves start/end (defaults: today → next full moon), computes the realnewMoonDate/fullMoonDate, and names the month fromnearestFullMoon(start).parseWindowBoundsis the single validation path — parses and enforcesend > start— used by bothstartCycleandupdateActiveMonth.StartCycleTypegains optionalstartDate/endDate, so existing clients and tests keep working (D2). The window is planned before rollover, so an invalid one rejects without touching the outgoing cycle.createNewMonthstores the planned window; the literal string"TODO"is gone from new rows.newMoonDatestill stores a real new moon — it now falls mid-cycle.updateActiveMonthprocedure: a plain update of the active month row, no task re-parenting (D3).Web — Goals page shows the proposed window with native
<input type="date">(D4) wired into Start Cycle, plus a Current cycle card that corrects the running cycle's boundaries. Home header now readsBuck Moon - 26 · Aug 2 – Aug 28. Date round-tripping stays in local time (lib/cycle-dates.ts) so a picked date reads back as the same date anduseLunarPhase's days-remaining stays consistent.Seed — seeded scenarios now pass an explicit 30-day window. The lunar default would produce a very short cycle whenever a seed runs just before the full moon, and the e2e suite schedules tasks days out against the DatePicker's
endDatebound — that would have been a fail-by-calendar flake.No migration
new_moon_date/full_moon_datecolumns already exist. Past months keep their"TODO"values (explicit non-goal).Verification
bun run lint— 0 errors (12 pre-existing warnings, none from these files)bun run test:unit— 109 passed (20 new intests/unit/cycle-window.test.ts)bun run test:seed— 1 passedbun run test:playwright— 40 passed, 2 skipped (4 new intests/e2e/cycle-dates.spec.ts)tsc --noEmitonapps/lunacycle-server— clean;bun run build:client— cleanOpen: one intermittent e2e failure, filed not waved off
tests/e2e/goals.spec.ts:64failed once in six full-suite runs on this branch —expect(gateRow.schedules).toBe(1)got0, withmonth_idcorrectly stamped. It did not reproduce in 3 further full-suite runs on this branch, 3 isolated runs of the spec, or 3 full-suite runs onorigin/main.0-in-3 vs 1-in-6 does not distinguish "rare pre-existing flake" from "introduced here", so I am claiming neither. Written up in
KNOWN_TEST_FAILURES.mdwith the leading (unconfirmed) hypothesis: a placeholder-month race whereDatePickerderives its bounds fromEMPTY_MONTH's"-"endDate whilecurrentMonthis still loading. If that holds it is a live-app bug, not just a test flake — a user opening the schedule dialog during initial atom load would silently lose the selection. Worth a look before merge.Known limitation, pre-existing, not fixed here
MOON_NAMESis a 12-entry array indexed by calendar month. A year has 13 full moons roughly every 2.7 years, so in a blue-moon year two consecutive cycles get the identical name. This predates the change, but full-moon boundaries make cycles lunation-locked, so it will now recur on schedule rather than by luck.🤖 Generated with Claude Code