.cursor/skills/anchor-based-lunisolar/SKILL.md
Rules and pitfalls for implementing lunisolar (or similar) calendars that are defined by explicit anchor dates and a fixed cycle (e.g. Metonic), not by astronomical events like equinox or "first new moon after X". Use when implementing or fixing such calendars (e.g. Babylonian, or any spec that gives "date X = year Y month 1 day 1").
npx skillsauth add CodapopKSP/LibraryOfTime anchor-based-lunisolarInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Use this skill when:
getMoonPhase in astronomicalData.js) and the calendar’s day-start rule (e.g. if new moon before local sunset, month starts at that sunset; else at the next sunset). Do not use mean synodic month for placing month boundaries when correctness matters. It can be used to estimate the current month, but the specific start of the month should come from the actual moon phase.When the spec says the new moon must be at least N hours before sunset for the month to start at that sunset (e.g. “moon not visible if less than 15 hours old”):
So: first resolve “which sunset are we considering?” then “is the new moon at least N hours before that sunset?” and only then possibly push one more day.
yearStart and nextYearStart for that year. If the target date is before yearStart, decrement the year and recompute; if it is on or after nextYearStart, increment the year and recompute.[yearStart, nextYearStart). Otherwise you can remain in a wrong year (e.g. a year whose start is in the future), which leads to no month interval containing the date and then to a negative day-of-month when you use yearStart as the "current" month start.totalMonths iterations. For dates far from the anchor (e.g. today), totalMonths can be tens of thousands; that will hang the page or block the main thread.approxInstant = epoch + totalMonths * meanSynodicMonth (e.g. ~29.53 days), then get the new moon near that instant with getMoonPhase(approxInstant, 0) and apply the calendar’s day-boundary rule (e.g. sunset). Use that as the candidate year start. The year-resolution loop (section 2) will correct if the approximation is off by a year.When the calendar year is before the anchor year, you are counting months backward from the epoch. Do not reuse the same formula as for years after the anchor.
totalMonths = (year - anchorYear) * 12 + countLeapYears(anchorYear, year). (Count leap years in the range that lies between anchor and year; that's the extra months you add.)totalMonths = (year - anchorYear) * 12 - countLeapYears(year, anchorYear). You must subtract the number of leap years in the range [year, anchorYear) (years from the target year up to but not including the anchor). Each of those years has 12 or 13 months; going backward, the leap years in that interval are the ones that add an extra month you must step back through.Why: A single "count leap years from anchor" helper that only counts forward (e.g. for (y = anchorYear; y < year; y++)) returns 0 when year < anchorYear, so reusing it for past years gives totalMonths = (year - anchorYear) * 12 + 0 and you step back too few months. The year immediately before the anchor (e.g. cycle year 19 in a Metonic calendar) is often a leap year; if you only step back 12 months instead of 13, the "leap month" of that year is wrongly attributed to the next calendar year and appears as a non-leap month 12.
Implementation: Add a separate helper for the backward case, e.g. countLeapYearsInRange(low, high) for the half-open interval [low, high), and use it only when year < anchorYear to compute the count to subtract.
floor(days between date and start of current month) + 1. If the date is before the start of the month you chose (e.g. because no month interval contained the date), this becomes negative.[inputDate, timezone, expectedOutput] where expectedOutput is the exact string the calendar function returns. Use the shared runner (e.g. runSingleParameterTests); do not add a custom loop that checks object fields (e.g. result.year, result.month) or a different expected format.expected: getBabylonianLunisolarCalendar(parseInputDate(...))). That always passes and does not validate correctness.calculateHebrewCalendar, getChineseLunisolarCalendarDate). The caller (e.g. nodeUpdate.js) should pass the return value directly to setTimeValue(..., getCalendar(currentDateTime))..formatted property and then have the caller do result.formatted; that is inconsistent with the rest of the project and adds unnecessary branching.getMetonicIndex(year) and leap-year list (e.g. [0, 3, 6, 8, 11, 14, 17]) must match.When the anchor is the start of year 1 of the cycle (e.g. "year -74 month 1 day 1 = start of Metonic cycle"), the civil year that contains the anchor is cycle year 1, not cycle year 19.
r = (civilYear - anchorYear) % 19 (or equivalent), with r in 0..18 (e.g. 0 for the anchor year, 1 for the next, …, 18 for the 19th year of the cycle).cycleYear1Based = r + 1. So: 0→1, 1→2, …, 18→19. The first year of the cycle is 1; the last is 19.r === 0 ? 19 : r. That maps the first year of the cycle (remainder 0) to 19, so the anchor year is treated as a leap year and gets a 13th month; the next year then shows as "month 1" only after an erroneous "leap month" for the previous year.r + 1, not r === 0 ? 19 : r.createAdjustedDateTime with that timezone and the correct hour (e.g. hour: 'SUNSET') and avoid hardcoding UTC hours for "before sunset" checks if you have a sunset helper.getMoonPhase + day-boundary rule), not a loop of totalMonths iterations from the epoch.totalMonths uses a different formula: subtract leap years in [year, anchorYear), do not reuse the forward "count from anchor" helper (it returns 0 and undercounts months).yearStart <= date < nextYearStart, with a max-iteration guard..formatted or object fields.[input, timezone, expectedOutput] array and the shared runner; expected output is the exact display string.r + 1), not 19 (r === 0 ? 19 : r); first year of cycle must not be leap unless the spec says so.development
Compute weekdays correctly for calendars where some days (intercalary/epagomenal) are excluded from the weekday cycle. Use when implementing or debugging weekday logic with “skipped days”, “epagomenal”, “intercalary”, or “non-weekday” days.
development
Catalogs common implementation patterns used by existing calendars and timekeeping systems in the Library of Time, with examples and guidance on when to use each pattern. Use when choosing how to structure a new calendar’s algorithm.
tools
Use user-provided month names or labels in calendar implementations and output. Use when implementing or updating a calendar and the user has given a list of month names (including for leap months), so the implementation does not default to numeric-only output.
development
Designs and implements complete calendar and timekeeping calculation modules for the Library of Time project, including algorithms, date conversions, and tests. Use when working on calendar logic, adding a new calendar, or one-shotting an entire calendar implementation from a prompt. Treat `CalendarAPI/Calendars/*`, `CalendarAPI/Timekeeping/*`, and `CalendarAPI/Other/*` as a single Calendar API layer.