8 Commits
Author SHA1 Message Date
funkpower d0efc56312 The Sundays the calendar was not naming, and a calendar that keeps every day
Nine Sundays went unnamed in every jurisdiction. Three were missing from the
movable cycle outright: the Paralytic, the Samaritan Woman and the Blind Man,
so the fourth, fifth and sixth Sundays of Pascha reported only the saint of the
fixed date. Six more are reckoned from a fixed feast rather than from Pascha —
before and after the Exaltation, the Forefathers, before and after the
Nativity, before and after the Theophany — and were not modelled at all.

The three Pentecostarion Sundays rank with Thomas Sunday and the Myrrh-bearing
Women, so a vigil-rank saint still keeps the day and the Sunday is commemorated
on it. The six fixed-cycle ones never name a day: they are found in
ecclesiastical space, so the old calendar and the new get them on the right
morning, and they join the commemorations. Where a short window holds no Sunday
the Typikon says what to do and this follows it; where the books disagree, the
day goes unmarked rather than carrying an answer this calendar cannot defend.

`week` says where a day falls in the count from Pentecost, which is how the
books head the long ordinary stretch. It runs on across the new year and stops
at the Triodion, whose Sundays have names instead of numbers.

`--every-day` gives ics an event for every day, ferias included. The default
still thins the calendar, which is right for a subscription and wrong for a
reader who wants the saint of each morning; and the description now leads with
the commemorations rather than burying them under the church date.

Checked against 1.1.2 over every jurisdiction from 2015 to 2035: rank, feast
and title move on the three new Sundays and nowhere else. No fast, obligation,
binding or season changes on any day.
2026-09-21 09:28:27 -04:00
funkpower 93b597302a 1.1.2 — one sentence, in all five places a reader meets it
1.1.1 changed the description in Cargo.toml and in the npm package and
missed pyproject.toml, which is the one maturin publishes. PyPI took the
old summary and neither registry lets that be edited afterwards, so the
correction needs a version of its own.

The two READMEs had drifted further than the description fields. A
registry page leads with the README rather than the summary, and npm's
opened by saying the package needs no network without ever saying it
covers the Byzantine Catholic churches — which is most of what the
sentence was changed to convey.

Five files now carry it, and a test reads all five and names the ones
that disagree. It was written against the mistake above and fails on it.
2026-09-21 07:46:58 -04:00
funkpower dde50192c7 1.1.1
The README stops explaining how the project tests and releases itself.
Someone arriving at it wants to know what a day of the Great Fast
permits, not how the wheels get built; the parity suite and the release
workflow are in docs/, where the person who needs them will look.

The published description now says how many churches are covered, on
both registries. That is a number in two files nobody editing the
jurisdiction table will open, so a test pins it and names them.
2026-09-21 07:29:05 -04:00
funkpower 364dd0afdf 1.1.0 2026-09-18 15:03:18 -04:00
funkpower 69907eb186 javascript and typescript, as one wasm package for every runtime
WebAssembly rather than a native Node addon: one artifact serves Node,
Deno, Bun, a browser and anything built on them, where napi would have
meant a prebuilt binary per platform and a source build for whoever the
matrix missed. The engine is pure computation and the menologion is
already compiled in, so there is nothing for it to give up.

A day comes back as a plain object, not a handle. A wasm class would
have meant a .free() that every caller eventually forgets, and this way
JSON.stringify works and the thing spreads into state. Two serializer
defaults had to be overridden to keep the promise that it is the same
document --json emits: maps as objects, and an absent field as null
rather than undefined, which stringify would otherwise drop entirely.

The TypeScript is hand-written, because the generated types for a value
crossing as JsValue are `any`, and a binding that types its whole
return as `any` is a binding TypeScript cannot check. tsc --strict is a release
gate now, the way mypy --strict already is, and the check includes three
lines that must fail to compile.

The one place this differs from every other binding: no remembered
default jurisdiction, because a browser has nowhere to keep one. It is
called out in the README, in the types and in docs/versioning.md.

The test resolves typikon through a staged node_modules rather than a
relative path, so what it exercises is the exports map, and it compares
a year of days field by field against the command line for four
jurisdictions.
2026-09-18 14:55:40 -04:00
funkpower 785754295b 1.0.0
Nobody depends on this yet, which makes now the moment to commit to a surface
rather than later when someone does.

docs/versioning.md says what that surface is. A calendar has a wider one than
a library usually does: the JSON shape, the fasting level names and their
order, the jurisdiction keys, the command line, and the two language APIs are
all things a caller can reasonably build on, and only one of them is a
function signature. The text output, the reason strings and the menologion's
wording are explicitly not.

It also names the category semver has no word for. A correction to a fast rule
or a feast rank breaks no interface, so it is a patch; but it changes what the
program tells someone to eat. Those are called out in the changelog under
Calendar whatever the version does, and CHANGELOG.md starts by listing every
one of them against pre-1.0 behaviour.

daysToPascha is documented as signed, counting to the Pascha of that day's own
civil year, which it always was and nothing said.
2026-09-18 13:23:59 -04:00
funkpower 5d7e02cffb answer from Python too
The engine is the same; only the front door is new. pyo3 behind an optional
feature, maturin for the wheel, and the bindings mirror DayJson rather than Day
because DayJson is already the shape front ends read and the one that has to stay
still. to_dict() therefore gives exactly what --json gives, and a test holds the
two to that.

That test earned its place immediately. The bindings were calling
jurisdictions::resolve(None), which answers with a built-in default that knows
nothing about the machine, so Python was reckoning by the New Calendar while the
command line on the same box reckoned by ROCOR — every field downstream of the
calendar differed, including the fast. They now take the same two steps main.rs
takes, and agree field for field on Pascha, the Nativity and Theophany.

The wheel is abi3, so one build serves 3.9 upward rather than one per
interpreter. That costs the datetime C API, which the limited API does not
expose, so a date is read through its attributes instead — which incidentally
makes datetime.datetime work as well as datetime.date. Constructing dates is
unaffected, pyo3 calls Python's own constructor for that.

panic = "abort" comes out of the release profile. An extension module has to be
able to unwind a panic into a Python exception; aborting would take the
interpreter down with it. The CLI keeps the same behaviour it always had, a
panic there just unwinds now.

Mixed layout so the wheel can carry py.typed and a stub file, which a bare
extension module has nowhere to put. mypy --strict is clean against the stubs
and catches a deliberate error, so the Typing :: Typed classifier is not a claim
about nothing.
2026-09-18 11:28:08 -04:00
funkpower d115616eff rebuild the engine in Rust
Python was never the house language here: the mw-* desktop surface it came
from is 39 bash scripts and was exactly one Python script, the one this was
extracted from. What it cost was distribution, a widget dependency that
needed pipx and a Python floor, and 24ms of interpreter startup before a line
of the program ran.

The port is verified rather than asserted. Both implementations were run over
199,446 day-records, 26 jurisdictions across 21 years, with every field of
every record required to match; the JSON, the iCalendar and 480 renderings of
the text output are identical too. examples/dump.rs is the harness.

What the type system actually buys, which is less than the usual claim and
more than nothing:

  - Level is an ordered enum, strictest first, so  reads as 'a is
    stricter' and relaxed_to() is the one place a feast can lift a fast. No
    rule can get the direction backwards.
  - Date is a newtype over the Julian Day Number and JulianDate is a separate
    type, so a civil date and an ecclesiastical one cannot be mixed up. That
    confusion is the single most common way calendar code goes wrong.
  - Octets is a newtype in the iCalendar folder, because the bug it replaced
    counted characters against a limit RFC 5545 states in octets.
  - Movables is a struct, not a map, so a mistyped key is a compile error
    rather than a None at the far end of the program.

Of the six defects an adversarial review found in the Python, exactly one had
a shape a type system would have caught. The rest were logic, and the tests
came with them.

The menologion is compiled in with include_str!, so the binary answers on its
own: 930KB, no runtime, 1.2ms to start against Python's 36ms.
2026-09-18 11:13:15 -04:00