[{"slug":"ZV-2026-0607","server_name":"almanack.dev","severity":"breaking","title":"almanack.dev: Type of date on add_to_date changed object → unset.","summary":"[safe] Tool calendar_periods was added. [safe] Tool sun_daylight was added. [risky] Description of add_to_date changed (46% word delta). [risky] Optional field on_error was added to add_to_date; may shift model behaviour. [risky] Optional field steps was added to add_to_date; may shift model behaviour. [risky] Optional field ticks was added to add_to_date; may shift model behaviour. [risky] Optional field time was added to add_to_date; may shift model behaviour. [safe] Field date on add_to_date is no longer required. [breaking] Type of date on add_to_date changed object → unset. [risky] Default of date on add_to_date changed unset → null. [risky] Description of calendar_events changed (32% word delta). [risky] Optional field events was added to calendar_events; may shift model behaviour. [safe] Field event on calendar_events is no longer required. [risky] Default of event on calendar_events changed unset → null. [risky] Description of calendar_weekday changed (31% word delta). [risky] Optional field dates was added to calendar_weekday; may shift model behaviour. [risky] Optional field on_error was added to calendar_weekday; may shift model behaviour. [safe] Field date on calendar_weekday is no longer required. [breaking] Type of date on calendar_weekday changed object → unset. [risky] Default of date on calendar_weekday changed unset → null. [risky] Description of convert_date changed (53% word delta). [risky] Optional field dates was added to convert_date; may shift model behaviour. [risky] Optional field day_numbers was added to convert_date; may shift model behaviour. [risky] Optional field gregorians was added to convert_date; may shift model behaviour. [risky] Optional field on_error was added to convert_date; may shift model behaviour. [risky] Optional field time was added to convert_date; may shift model behaviour. [risky] Description of date_interval changed (48% word delta). [risky] Optional field end_time was added to date_interval; may shift model behavio","changes":[{"kind":"tool_added","tool":"calendar_periods","detail":"Tool `calendar_periods` was added.","severity":"safe"},{"kind":"tool_added","tool":"sun_daylight","detail":"Tool `sun_daylight` was added.","severity":"safe"},{"kind":"description_changed","tool":"add_to_date","after":"Move a date by years, months, days and ticks. They are applied in that fixed order, which matters: a month then a day is not always the same landing as a day then a month. Adding days is always exact. policy decides what happens when the day does not exist in the target month: 'clamp' (default) moves to that month's last day, 'reject' refuses with an error, 'spill' carries the excess into the following month. Adding months to an intercalary date is refused, because those days sit between months; add days instead. 'ticks' moves within the day for a calendar that divides its day, carrying whole days as they fall. USE 'steps' FOR MORE THAN ONE MOVE: each step gives its own years, months, days and ticks, and its own date or the shared one, so a base date and a list of offsets is a single call rather than one per offset. Only the date is shared: a step states its own move in full, so a step naming no months moves by no months whatever the call around it says. Answers up to 1000 in one call, in the order given, as {count, ok, results}. Prefer this over one call per item: the spec travels once instead of once each.","before":"Move a date by years, months and days. They are applied in that fixed order, which matters: a month then a day is not always the same landing as a day then a month. Adding days is always exact. policy decides what happens when the day does not exist in the target month: 'clamp' (default) moves to that month's last day, 'reject' refuses with an error, 'spill' carries the excess into the following month. Adding months to an intercalary date is refused, because those days sit between months; add days instead.","detail":"Description of `add_to_date` changed (46% word delta).","severity":"risky","descriptionDelta":0.4649122807017544},{"kind":"input_property_added","path":"inputSchema.properties.on_error","tool":"add_to_date","after":{"type":"string","title":"On Error","default":"fail"},"detail":"Optional field `on_error` was added to `add_to_date`; may shift model behaviour.","severity":"risky"},{"kind":"input_property_added","path":"inputSchema.properties.steps","tool":"add_to_date","after":{"anyOf":[{"type":"array","items":{"type":"object","additionalProperties":true}},{"type":"null"}],"title":"Steps","default":null},"detail":"Optional field `steps` was added to `add_to_date`; may shift model behaviour.","severity":"risky"},{"kind":"input_property_added","path":"inputSchema.properties.ticks","tool":"add_to_date","after":{"type":"integer","title":"Ticks","default":0},"detail":"Optional field `ticks` was added to `add_to_date`; may shift model behaviour.","severity":"risky"},{"kind":"input_property_added","path":"inputSchema.properties.time","tool":"add_to_date","after":{"anyOf":[{"type":"object","additionalProperties":{"type":"integer"}},{"type":"integer"},{"type":"null"}],"title":"Time","default":null},"detail":"Optional field `time` was added to `add_to_date`; may shift model behaviour.","severity":"risky"},{"kind":"input_required_removed","path":"inputSchema.required.date","tool":"add_to_date","detail":"Field `date` on `add_to_date` is no longer required.","severity":"safe"},{"kind":"input_type_changed","path":"inputSchema.properties.date","tool":"add_to_date","after":"unset","before":"object","detail":"Type of `date` on `add_to_date` changed object → unset.","severity":"breaking"},{"kind":"default_changed","path":"inputSchema.properties.date","tool":"add_to_date","after":null,"detail":"Default of `date` on `add_to_date` changed unset → null.","severity":"risky"},{"kind":"description_changed","tool":"calendar_events","after":"When recurring events fall, and whether they land on each other. query 'next' gives the first occurrence after the date in 'after' (or on it when inclusive is true). query 'occurrences' gives every occurrence between start and end inclusive, capped at limit and at most 1000; 'truncated' says whether the list was cut short, so never read a truncated list as complete. query 'collisions' gives the days between start and end where two or more of the given events fall together, each naming what shares it — the question a calendar of festivals is really asking, and one to ask here rather than reason out in prose. An event is either a name the calendar defines or an event given inline: {'name':..,'type':'fixed','month':..,'day':..}, {'name':..,'type':'nth_weekday','month':..,'weekday':..,'n':..} or {'name':..,'type':'interval','anchor':{..},'every':N,'unit':'days'|'years'}. Pass one as 'event', or many as 'events' — which 'collisions' requires and which answers the other two queries for every event in a single call. A year in which an event's date does not exist produces no occurrence: a leap-day feast happens in leap years and not otherwise, and is never moved to a nearby day.","before":"When a recurring event falls. query 'next' gives the first occurrence after the date in 'after' (or on it when inclusive is true). query 'occurrences' gives every occurrence between start and end inclusive, capped at limit and at most 1000; 'truncated' says whether the list was cut short, so never read a truncated list as complete. The event is either a name the calendar defines or an event given inline: {'name':..,'type':'fixed','month':..,'day':..}, {'name':..,'type':'nth_weekday','month':..,'weekday':..,'n':..} or {'name':..,'type':'interval','anchor':{..},'every':N,'unit':'days'|'years'}. A year in which the event's date does not exist produces no occurrence: a leap-day feast happens in leap years and not otherwise, and is never moved to a nearby day.","detail":"Description of `calendar_events` changed (32% word delta).","severity":"risky","descriptionDelta":0.3214285714285714},{"kind":"input_property_added","path":"inputSchema.properties.events","tool":"calendar_events","after":{"anyOf":[{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"object","additionalProperties":true}]}},{"type":"null"}],"title":"Events","default":null},"detail":"Optional field `events` was added to `calendar_events`; may shift model behaviour.","severity":"risky"},{"kind":"input_required_removed","path":"inputSchema.required.event","tool":"calendar_events","detail":"Field `event` on `calendar_events` is no longer required.","severity":"safe"},{"kind":"default_changed","path":"inputSchema.properties.event","tool":"calendar_events","after":null,"detail":"Default of `event` on `calendar_events` changed unset → null.","severity":"risky"},{"kind":"description_changed","tool":"calendar_weekday","after":"Which weekday a date falls on, as a 0-based index into the calendar's own week plus its name. Exact; never estimates. Returns weekday null when the date is an intercalary day the week does not count: those days sit outside the cycle and the week resumes after them exactly where it left off. Fails if the calendar defines no week rather than inventing a seven-day one. Pass 'dates' instead of 'date' for more than one. Answers up to 1000 in one call, in the order given, as {count, ok, results}. Prefer this over one call per item: the spec travels once instead of once each.","before":"Which weekday a date falls on, as a 0-based index into the calendar's own week plus its name. Exact; never estimates. Returns weekday null when the date is an intercalary day the week does not count: those days sit outside the cycle and the week resumes after them exactly where it left off. Fails if the calendar defines no week rather than inventing a seven-day one.","detail":"Description of `calendar_weekday` changed (31% word delta).","severity":"risky","descriptionDelta":0.3125},{"kind":"input_property_added","path":"inputSchema.properties.dates","tool":"calendar_weekday","after":{"anyOf":[{"type":"array","items":{"type":"object","additionalProperties":true}},{"type":"null"}],"title":"Dates","default":null},"detail":"Optional field `dates` was added to `calendar_weekday`; may shift model behaviour.","severity":"risky"},{"kind":"input_property_added","path":"inputSchema.properties.on_error","tool":"calendar_weekday","after":{"type":"string","title":"On Error","default":"fail"},"detail":"Optional field `on_error` was added to `calendar_weekday`; may shift model behaviour.","severity":"risky"},{"kind":"input_required_removed","path":"inputSchema.required.date","tool":"calendar_weekday","detail":"Field `date` on `calendar_weekday` is no longer required.","severity":"safe"},{"kind":"input_type_changed","path":"inputSchema.properties.date","tool":"calendar_weekday","after":"unset","before":"object","detail":"Type of `date` on `calendar_weekday` changed object → unset.","severity":"breaking"},{"kind":"default_changed","path":"inputSchema.properties.date","tool":"calendar_weekday","after":null,"detail":"Default of `date` on `calendar_weekday` changed unset → null.","severity":"risky"},{"kind":"description_changed","tool":"convert_date","after":"Convert between a calendar date, its day number, and its Gregorian date. Give exactly one of date, day_number, gregorian or one of their plurals dates, day_numbers, gregorians; all three forms come back, with whether the year is a leap year, how long it is, and any named period of the year the date falls in. USE THE PLURALS FOR MORE THAN ONE DATE: the calendar spec travels on every call, so fifteen dates in one call cost the spec once instead of fifteen times, and there is no reason to work an answer out by hand to save a round trip. Answers up to 1000 in one call, in the order given, as {count, ok, results}. Prefer this over one call per item: the spec travels once instead of once each. Exact arithmetic; never estimates. A date that does not exist in this calendar is refused with the real range, never rounded to a nearby day.","before":"Convert between a calendar date, its day number, and its Gregorian date. Give exactly one of date, day_number or gregorian; all three come back, with whether the year is a leap year and how long it is. Exact arithmetic; never estimates. A date that does not exist in this calendar is refused with the real range, never rounded to a nearby day.","detail":"Description of `convert_date` changed (53% word delta).","severity":"risky","descriptionDelta":0.53125},{"kind":"input_property_added","path":"inputSchema.properties.dates","tool":"convert_date","after":{"anyOf":[{"type":"array","items":{"type":"object","additionalProperties":true}},{"type":"null"}],"title":"Dates","default":null},"detail":"Optional field `dates` was added to `convert_date`; may shift model behaviour.","severity":"risky"},{"kind":"input_property_added","path":"inputSchema.properties.day_numbers","tool":"convert_date","after":{"anyOf":[{"type":"array","items":{"type":"integer"}},{"type":"null"}],"title":"Day Numbers","default":null},"detail":"Optional field `day_numbers` was added to `convert_date`; may shift model behaviour.","severity":"risky"},{"kind":"input_property_added","path":"inputSchema.properties.gregorians","tool":"convert_date","after":{"anyOf":[{"type":"array","items":{"type":"string"}},{"type":"null"}],"title":"Gregorians","default":null},"detail":"Optional field `gregorians` was added to `convert_date`; may shift model behaviour.","severity":"risky"},{"kind":"input_property_added","path":"inputSchema.properties.on_error","tool":"convert_date","after":{"type":"string","title":"On Error","default":"fail"},"detail":"Optional field `on_error` was added to `convert_date`; may shift model behaviour.","severity":"risky"},{"kind":"input_property_added","path":"inputSchema.properties.time","tool":"convert_date","after":{"anyOf":[{"type":"object","additionalProperties":{"type":"integer"}},{"type":"integer"},{"type":"null"}],"title":"Time","default":null},"detail":"Optional field `time` was added to `convert_date`; may shift model behaviour.","severity":"risky"},{"kind":"description_changed","tool":"date_interval","after":"The distance between two dates. 'days' is the exact signed count (negative when end precedes start) and is always reliable. 'calendar' breaks the same distance into whole years, then whole months, then days, using the clamp policy, so adding that breakdown back to the start date with add_to_date returns the end date exactly. Give start_time and end_time and the answer also carries 'ticks', the signed distance in the calendar's finest unit of the day, and 'time', that distance as whole days plus a count per unit. Give both or neither: one alone is refused rather than measured from the start of the other day, which would be a moment you did not give — pass 0 if that is what you mean. Exact integer arithmetic; never estimates.","before":"The distance between two dates. 'days' is the exact signed count (negative when end precedes start) and is always reliable. 'calendar' breaks the same distance into whole years, then whole months, then days, using the clamp policy, so adding that breakdown back to the start date with add_to_date returns the end date exactly. Exact integer arithmetic; never estimates.","detail":"Description of `date_interval` changed (48% word delta).","severity":"risky","descriptionDelta":0.48235294117647054},{"kind":"input_property_added","path":"inputSchema.properties.end_time","tool":"date_interval","after":{"anyOf":[{"type":"object","additionalProperties":{"type":"integer"}},{"type":"integer"},{"type":"null"}],"title":"End Time","default":null},"detail":"Optional field `end_time` was added to `date_interval`; may shift model behaviour.","severity":"risky"},{"kind":"input_property_added","path":"inputSchema.properties.start_time","tool":"date_interval","after":{"anyOf":[{"type":"object","additionalProperties":{"type":"integer"}},{"type":"integer"},{"type":"null"}],"title":"Start Time","default":null},"detail":"Optional field `start_time` was added to `date_interval`; may shift model behaviour.","severity":"risky"},{"kind":"description_changed","tool":"format_calendar_date","after":"Write a date out to a pattern. pattern is either a name from the calendar's own 'formats' (names win) or a pattern written out. Tokens: {day} {day-name} {month} {month-name} {year} {era} {era-abbr} {era-year} {intercalary} {period}, plus one named for each unit the spec's 'day' block declares ({bell}), with an optional zero-pad width on the numeric ones as {day:2}. Write a literal brace as {{ or }}. The output is exactly reversible by parse_calendar_date with the same pattern, so use these two as a pair. Pass 'dates' instead of 'date' to write many to the same pattern, with 'times' alongside when each carries its own time. Answers up to 1000 in one call, in the order given, as {count, ok, results}. Prefer this over one call per item: the spec travels once instead of once each.","before":"Write a date out to a pattern. pattern is either a name from the calendar's own 'formats' (names win) or a pattern written out. Tokens: {day} {day-name} {month} {month-name} {year} {era} {era-abbr} {era-year} {intercalary}, with an optional zero-pad width on the numeric ones as {day:2}. Write a literal brace as {{ or }}. The output is exactly reversible by parse_calendar_date with the same pattern, so use these two as a pair.","detail":"Description of `format_calendar_date` changed (44% word delta).","severity":"risky","descriptionDelta":0.4382022471910112},{"kind":"input_property_added","path":"inputSchema.properties.dates","tool":"format_calendar_date","after":{"anyOf":[{"type":"array","items":{"type":"object","additionalProperties":true}},{"type":"null"}],"title":"Dates","default":null},"detail":"Optional field `dates` was added to `format_calendar_date`; may shift model behaviour.","severity":"risky"},{"kind":"input_property_added","path":"inputSchema.properties.on_error","tool":"format_calendar_date","after":{"type":"string","title":"On Error","default":"fail"},"detail":"Optional field `on_error` was added to `format_calendar_date`; may shift model behaviour.","severity":"risky"},{"kind":"input_property_added","path":"inputSchema.properties.time","tool":"format_calendar_date","after":{"anyOf":[{"type":"object","additionalProperties":{"type":"integer"}},{"type":"integer"},{"type":"null"}],"title":"Time","default":null},"detail":"Optional field `time` was added to `format_calendar_date`; may shift model behaviour.","severity":"risky"},{"kind":"input_property_added","path":"inputSchema.properties.times","tool":"format_calendar_date","after":{"anyOf":[{"type":"array","items":{"anyOf":[{"type":"object","additionalProperties":{"type":"integer"}},{"type":"integer"}]}},{"type":"null"}],"title":"Times","default":null},"detail":"Optional field `times` was added to `format_calendar_date`; may shift model behaviour.","severity":"risky"},{"kind":"input_required_removed","path":"inputSchema.required.date","tool":"format_calendar_date","detail":"Field `date` on `format_calendar_date` is no longer required.","severity":"safe"},{"kind":"input_type_changed","path":"inputSchema.properties.date","tool":"format_calendar_date","after":"unset","before":"object","detail":"Type of `date` on `format_calendar_date` changed object → unset.","severity":"breaking"},{"kind":"default_changed","path":"inputSchema.properties.date","tool":"format_calendar_date","after":null,"detail":"Default of `date` on `format_calendar_date` changed unset → null.","severity":"risky"},{"kind":"description_changed","tool":"moon_phases","after":"Every moon's phase on a date. 'fraction' is the exact position in the cycle, 0.0 at new and 0.5 at full, and is the number to compute with. 'phase' is only which named bucket that fraction falls in, and 'illumination' is the lit portion of the disc from 0.0 to 1.0. Computed in closed form from the moon's period and offset, not simulated and not astronomical: it is exactly what the spec describes. Measured at the start of the day; pass day_fraction 0.5 for midday, or 'time' to say where in the day exactly for a calendar whose spec divides its day. Give one or the other.","before":"Every moon's phase on a date. 'fraction' is the exact position in the cycle, 0.0 at new and 0.5 at full, and is the number to compute with. 'phase' is only which named bucket that fraction falls in, and 'illumination' is the lit portion of the disc from 0.0 to 1.0. Computed in closed form from the moon's period and offset, not simulated and not astronomical: it is exactly what the spec describes. Measured at the start of the day; pass day_fraction 0.5 for midday.","detail":"Description of `moon_phases` changed (16% word delta).","severity":"safe","descriptionDelta":0.16176470588235292},{"kind":"input_property_added","path":"inputSchema.properties.time","tool":"moon_phases","after":{"anyOf":[{"type":"object","additionalProperties":{"type":"integer"}},{"type":"integer"},{"type":"null"}],"title":"Time","default":null},"detail":"Optional field `time` was added to `moon_phases`; may shift model behaviour.","severity":"risky"},{"kind":"description_changed","tool":"parse_calendar_date","after":"Read a written date back into a date. Strict, with no fuzzy matching of any kind: literal characters must match character for character, month, weekday, era and period names must be names this calendar actually defines, and a weekday or period written into the string must be one that date really falls on. A string that does not fit is an error naming what did not fit; it never returns a best guess. Use the same pattern that wrote the string. A pattern that wrote a time of day gives the time back too.","before":"Read a written date back into a date. Strict, with no fuzzy matching of any kind: literal characters must match character for character, month, weekday and era names must be names this calendar actually defines, and a weekday written into the string must be the weekday that date really falls on. A string that does not fit is an error naming what did not fit; it never returns a best guess. Use the same pattern that wrote the string.","detail":"Description of `parse_calendar_date` changed (11% word delta).","severity":"safe","descriptionDelta":0.11475409836065575},{"kind":"description_changed","tool":"render_calendar_page","after":"Draw one month of a calendar as an SVG grid, or a whole year on one sheet, and return the SVG itself. Columns come from the calendar's own week, so a ten-day week gets ten columns. Intercalary days belong to no month and never take a cell in the grid: the blocks either side of this month are drawn as a strip outside it, dashed when the week does not count them. Moons get a glyph per day, lit to that moon's phase, when the calendar defines any. Events use the same shape as render_timeline and are filtered to the days on this page, with at most 500 given and at most three shown per day before the rest become a count. Colours are assigned per color_key from the whole list before filtering, so a category keeps its colour across months. OMIT 'month' TO DRAW THE WHOLE YEAR on one sheet, every month side by side — for seeing at once whether festivals are evenly spread, which nine separate month pages do not show. A year sheet has no room for labels or moon glyphs, so each day carries its number and a dot per event, every dot holding its label in a <title>. Fully deterministic: the same arguments always produce byte-identical SVG. The output is static SVG and never contains script or style.","before":"Draw one month of a calendar as an SVG grid and return the SVG itself. Columns come from the calendar's own week, so a ten-day week gets ten columns. Intercalary days belong to no month and never take a cell in the grid: the blocks either side of this month are drawn as a strip outside it, dashed when the week does not count them. Moons get a glyph per day, lit to that moon's phase, when the calendar defines any. Events use the same shape as render_timeline and are filtered to the days on this page, with at most 500 given and at most three shown per day before the rest become a count. Colours are assigned per color_key from the whole list before filtering, so a category keeps its colour across months. Fully deterministic: the same arguments always produce byte-identical SVG. The output is static SVG and never contains script or style.","detail":"Description of `render_calendar_page` changed (23% word delta).","severity":"safe","descriptionDelta":0.2272727272727273},{"kind":"input_required_removed","path":"inputSchema.required.month","tool":"render_calendar_page","detail":"Field `month` on `render_calendar_page` is no longer required.","severity":"safe"},{"kind":"input_type_changed","path":"inputSchema.properties.month","tool":"render_calendar_page","after":"unset","before":"integer","detail":"Type of `month` on `render_calendar_page` changed integer → unset.","severity":"breaking"},{"kind":"default_changed","path":"inputSchema.properties.month","tool":"render_calendar_page","after":null,"detail":"Default of `month` on `render_calendar_page` changed unset → null.","severity":"risky"}],"published_at":"2026-09-01T19:53:18.138Z"}]