<feed xmlns='http://www.w3.org/2005/Atom'>
<title>lectio.git/internal/calendar, branch main</title>
<subtitle>offline Catholic daily readings and liturgical calendar in Go, with CLI, TUI and web clients</subtitle>
<id>https://git.labunix.xyz/lectio.git/atom?h=main</id>
<link rel='self' href='https://git.labunix.xyz/lectio.git/atom?h=main'/>
<link rel='alternate' type='text/html' href='https://git.labunix.xyz/lectio.git/'/>
<updated>2026-08-24T20:02:39Z</updated>
<entry>
<title>perf(calendar): precompute the EF occupancy index alongside the transfer plan</title>
<updated>2026-08-24T20:02:39Z</updated>
<author>
<name>Lukasz Kasprzak</name>
<email>lukas@labunix.xyz</email>
</author>
<published>2026-08-24T20:02:39Z</published>
<link rel='alternate' type='text/html' href='https://git.labunix.xyz/lectio.git/commit/?id=b24702cc9b8d7f2e408bb92032e8f7fa2aaa836b'/>
<id>urn:sha1:b24702cc9b8d7f2e408bb92032e8f7fa2aaa836b</id>
<content type='text'>
The prior commit (ea6e598) memoised efTransferPlan but left the SAME
occupiedByClass1/Or2 O(len(merged)) scan pattern in a second place:
transferIfImpededEF's own fallback path, called from computeEF once per
day for every class-1 candidate the (now cached) plan does not resolve
-- confirmed by CPU profile, not assumed: computeEF.func1 (the old
occupiedByRank closure) was ~62% of BenchmarkDays7EF's total time,
almost all of it inside buildCelebration, called from BOTH efTransferPlan
internally AND this second, uncached site.

Before optimising, established precisely what "occupied" depends on, per
the coordinator's warning that occupancy might genuinely mutate during
planning (efTransferPlan's own `claimed` map suggested as much). It does
not: occupiedByRank asks only "does some OTHER entry's ORIGINAL,
untransferred date (buildCelebration + celebrationDate, which never
considers a transfer) equal d" -- a pure function of (merged content,
year, sel) alone, identical to what the transfer-plan cache already
keys on. `claimed`, by contrast, genuinely mutates during one planning
pass (it tracks which TARGET days a transfer walk has already assigned)
and is NOT part of occupiedByRank's computation at all -- it remains
computed fresh inside efTransferPlan every call, untouched by this
change. These are independent, not the same thing wearing two names:
efTransferPlan's own forward-walk loop already checks both, plus a
third condition (the target day's own temporal class), as separate
disjuncts.

Given that, an occupancy INDEX -- not a second cache, and not a
"we already know the answer" shortcut derived from plan's absence (which
would have been correct for the transferIfImpededEF fallback's own
control flow ONLY by coincidence: it ignores the unconditional All Souls
Sunday-transfer special case that runs before the class-1 check on ANY
rank, so a shortcut skipping straight past it would misfire the moment a
user overlay retagged All Souls class-1, however unlikely on shipped
data) -- is the safe fix: buildEFOccupancyIndex does the same
merged-scan ONCE, into date -&gt; []{slug, rank}, and .occupied does the
exact O(1)-ish lookup + tiny-list filter occupiedByRank always computed,
just precomputed. transferIfImpededEF's own signature, control flow and
All Souls handling are completely unchanged; only what its two closure
parameters read from changed.

The index shares the transfer-plan cache's existing key (year, Selection,
SHA-256 of merged) rather than adding a new one -- both are pure in
exactly those three inputs, built in the same pass, so one key correctly
covers both. Only the plan is copied per call (clonePlan); the index,
which can hold one entry per merged slug (~330 on shipped data), is
returned uncopied and documented immutable-after-construction -- safe
under Go's concurrent-read guarantee since nothing anywhere writes to a
returned index.

New TestEFOccupancyIndexDetectsSameDateClass1Collision covers a path
no existing test reached: two class-1 SANCTORAL entries sharing one
ORIGINAL date (St Joseph/the Annunciation, covered by the prior commit's
tests, collide via HOLY WEEK's temporal precedence on DIFFERENT dates,
never with each other). A synthetic overlay (Compute's own public API,
same style as the existing tests) puts two class-1 entries on the same
otherwise-ordinary date: alone, either is simply observed; both
together, RG 97/98 transfer both forward and the shared date reverts to
its temporal office. Mutation-proved: dropping the index's exceptSlug
self-exclusion (reverted after) made the test fail immediately -- every
class-1 entry saw itself in the index and wrongly self-impeded, even the
single-entry case. TestEFTransferPlanCacheConcurrentUse gained a third,
disjoint key exercising this same collision path from goroutines
alongside the two existing ones; `go test -race` on the whole package is
clean.

Benchmarked (interleaved before/after, same method throughout this
branch): BenchmarkDays7EF drops from ~46-48ms (the prior commit's own
plan-cache-only state) to ~15-18ms/op (allocs 208639 -&gt; 98727, -53%;
bytes 16.1MB -&gt; 4.2MB, -74%) -- roughly a further 3x, ~4.6x cumulative
against the original ~74ms. BenchmarkDaysWeek (OF, which never touches
any of this) is unaffected: ~4.9-5.9ms/op both before and after, with
byte-for-byte identical allocs/bytes in every run -- the ms-level
wobble is machine noise, not a regression.

A fresh CPU profile confirms the new remaining bottleneck precisely:
writeSortedFields/hashMergedForPlan (the cache key's own SHA-256 of
merged, ~330 entries) is now ~35% of total time, because it still runs
on EVERY day (7x/week) to know whether a call is a cache hit, even
though the work it gates now mostly isn't. Not fixed here: hoisting the
key computation itself up to mobile.Days's batch level (mirroring the
first commit on this branch, b6ee8f0) would need Compute's public
signature to accept a precomputed key, a bigger surface change than this
task's scope, reported rather than taken unilaterally.

Output identity re-verified: the same 492-case sweep (both forms, both
UI languages, all four corpora, the leap day/Triduum/Requiem/season-
boundary dates, and the three Joseph/Annunciation years) is
byte-identical (SHA-256-equal) before and after -- the same SHA-256 as
the prior commit's own sweep, confirming zero output drift across the
whole chain. go test ./... and make ci (both build tags,
oracle/differential suite included) are green; go test -race on the
whole internal/calendar package is clean.
</content>
</entry>
<entry>
<title>perf(calendar): memoise the EF transfer plan across a shared calendar</title>
<updated>2026-08-24T19:42:48Z</updated>
<author>
<name>Lukasz Kasprzak</name>
<email>lukas@labunix.xyz</email>
</author>
<published>2026-08-24T19:42:48Z</published>
<link rel='alternate' type='text/html' href='https://git.labunix.xyz/lectio.git/commit/?id=ea6e59862dbdb607b9bcb221212a85ee3e4bd84a'/>
<id>urn:sha1:ea6e59862dbdb607b9bcb221212a85ee3e4bd84a</id>
<content type='text'>
computeEF calls efTransferPlan once per day, but the plan is pure in
(year, the merged sanctoral content, Selection) and identical for every
day sharing those three -- e.g. every date in one mobile.Days week.
Rebuilding it per day was a real cost: efTransferPlan walks every
merged entry looking for class-1 candidates, and for each one it
considers, occupiedByClass1/Or2 (an occurrence check it also uses)
walks merged AGAIN -- confirmed by CPU profile, not just by reading the
code (github.com/lukaszkasprzak/lectio/internal/calendar.computeEF.func1,
the occupiedByRank closure, at ~62% of BenchmarkDays7EF's total time
before this fix, almost all of it inside buildCelebration).

efTransferPlanCached (transfer_plan_cache.go) wraps efTransferPlan with
a small, bounded, thread-safe LRU (container/list + sync.Mutex, capped
at 64 entries -- gomobile may call in from multiple goroutines, and an
unbounded map keyed by year would grow as a user scrolls through
decades). The cache key is (year, Selection, a SHA-256 of merged's full
content): merged is a map, so it cannot be a map key field itself, and
Go's randomised map iteration order means two calls with identical
content can visit it differently, so the hash sorts slugs and, within
each entry, its Fields/Variant keys before hashing, and covers every
field of every entry -- not just Rank/Date, the ones efTransferPlan's
own read path happens to touch today, because which entries even
qualify as class-1 is itself computed from that data, and
occupiedByClass1/Or2 scan ALL of merged, not just the class-1 subset.
Selection is included even though EF date resolution ignores it today
(resolveDate never reads its sel parameter) -- keying on it costs
nothing (four small strings) and protects a future change from
silently poisoning a cache that never accounted for it. The returned
map is always a fresh copy (clonePlan), never the cached instance, so
sharing it across goroutines needs no further synchronisation.

Verified the key is complete rather than trusted: with the content hash
temporarily dropped from the key (mutation test, not committed),
TestEFTransferPlanCacheInvalidatesOnOverlay failed immediately -- a
plan warmed for the shipped 2008 calendar was wrongly served back for
the same year with a user overlay applied (the Annunciation suppressed,
which changes where the RG 96(a)/97/98 collision sends St Joseph: 31
March instead of 1 April, empirically confirmed against the pre-cache
code before the test was written). TestEFTransferPlanCacheInvalidatesOnYear
is a lighter companion covering the year field. TestEFTransferPlanCacheConcurrentUse
hammers the cache from 12 goroutines across two different keys and
reasserts correctness afterward; clean under `go test -race`.

Benchmarked (interleaved before/after, same method as the readings.Prepare
commit, to control for machine thermal drift): BenchmarkDays7EF drops
from ~48-51ms to ~32-35ms/op (allocs 253810 -&gt; 208639, -18%; bytes
28.1MB -&gt; 16.1MB, -43%), roughly a third faster. BenchmarkDaysWeek (OF,
which never calls efTransferPlan at all) is unaffected, ~3.5-4.1ms/op
both before and after -- within noise, confirming this change is
EF-only as intended.

EF remains well outside OF's range (~32ms vs ~4ms), and a fresh CPU
profile after this fix places the dominant remaining cost precisely: it
is the SAME occupiedByClass1/Or2 pattern, but living OUTSIDE
efTransferPlan -- transferIfImpededEF's own fallback path, called once
per day for every class-1 candidate NOT already resolved by the (now
cached) plan, i.e. the ordinarily-unimpeded ones (~15-20 of them),
each triggering another O(len(merged)) scan. That call site was not
part of what this task named, and memoising it is a materially
different change (it is keyed per-candidate, not once per day), so it
is reported here rather than folded into this commit.

Output identity re-verified: a 492-case sweep of mobile.Day/mobile.Days
(both forms, both UI languages, all four corpora, a leap day, the
Sacred Triduum, a Requiem day, Christmas/Pentecost/Assumption/All Souls
windows, and the three Joseph/Annunciation transfer-collision years
this fix specifically touches -- 2008, 2035, 2046) produced
byte-identical (SHA-256-equal) JSON before and after. go test ./... and
make ci (both build tags, oracle/differential suite included) are
green.
</content>
</entry>
<entry>
<title>fix(ef): two I-class feasts can no longer transfer onto the same day</title>
<updated>2026-08-18T15:33:20Z</updated>
<author>
<name>Lukasz Kasprzak</name>
<email>lukas@labunix.xyz</email>
</author>
<published>2026-08-18T15:33:20Z</published>
<link rel='alternate' type='text/html' href='https://git.labunix.xyz/lectio.git/commit/?id=e713da2d796eabc55d407ad2f8488861e7598798'/>
<id>urn:sha1:e713da2d796eabc55d407ad2f8488861e7598798</id>
<content type='text'>
RG 97/98 require a year's impeded I-class transfers to be resolved together.
transferIfImpededEF resolved each one independently, so when an early Easter
impedes both St Joseph (19 March) and the Annunciation (25 March), both walked
to the Monday after Low Sunday -- and St Joseph, losing pickEF there, was
observed on NO day of 2008, 2035 or 2046 at all. He appeared only as a
commemoration in the Annunciation's tail.

RG 96(a) settles it outright, and it is not a tie-break: the Annunciation
transferred after Easter has a sedes propria -- "quando est transferendum post
Pascha, transfertur, tamquam in sedem propriam, in feriam II post dominicam in
albis". It claims the Monday by law; the rest queue in RG 98's order (table
position, then the office impeded first) and walk around it.

New efTransferPlan resolves the whole year's transfers as a set and hands each
feast its target; transferIfImpededEF keeps the single-candidate walk and All
Souls. The stale doc comment recording this as a known unfixable limitation is
replaced -- its reasoning argued RG 97/98 tie-breaks, when RG 96(a) decides the
only collision the universal calendar actually produces.

  2008-04-01, 2035-04-03, 2046-04-03  ef-easter-2-tuesday -&gt; St Joseph

Matches colitur on all three. Mutation-tested: bypassing the plan reddens the
new test on all three years.

fix(ef): 13 January's Baptism is omitted when Holy Family falls on it

The Holy Family Mass propers, verbatim in both photographic scans of the 1962
Missal:

  "Si festum S. Familiae occurrerit die 13 ianuarii, Missa dicitur de festo
   S. Familiae, sine commemoratione Baptismatis D.N.I.C., et sine
   commemoratione dominicae."

Sine commemoratione: the Baptism is omitted outright, not merely outranked, so
the candidate is dropped rather than left in Others. Structurally the same
answer RG 91 entry 14 gives (Festa Domini II classis: primum mobilia, deinde
fixa), but the propers' rubric is the direct authority for the omission.

lectio kept the Baptism as the observed office on all seven such years in
2005-2050 -- 2008, 2013, 2019, 2030, 2036, 2041, 2047 -- and never observed the
Holy Family Mass on 13 January at all. It already had the Holy Family Mass on
every other Sunday after Epiphany, so this was the one date it could not reach.

13 rows of the 16801-day differential fixture change; colitur agrees with all
of them.
</content>
</entry>
<entry>
<title>feat(ef): the Most Holy Name of Jesus, and the ferias of 2-5 January</title>
<updated>2026-08-18T15:02:47Z</updated>
<author>
<name>Lukasz Kasprzak</name>
<email>lukas@labunix.xyz</email>
</author>
<published>2026-08-18T15:02:47Z</published>
<link rel='alternate' type='text/html' href='https://git.labunix.xyz/lectio.git/commit/?id=9572942a0db4a89aaddd42cd33182d44a6e1fefb'/>
<id>urn:sha1:9572942a0db4a89aaddd42cd33182d44a6e1fefb</id>
<content type='text'>
RG 17(a): "festum Ss.mi Nominis Iesu, celebrandum dominica quae occurrit a die
2 ad 5 ianuarii, secus die 2 ianuarii." The Most Holy Name is kept on the
Sunday falling 2-5 January and, when no Sunday falls in that window, on 2
January instead. lectio built neither shape, so the day was an ordinary Sunday
within the octave and read Gal 4:1-7.

The FALLBACK matters more than it looks. The window is four days wide, so in
about three years in seven no Sunday lands in it -- in those years there was no
Holy Name office at all, a missing II-class feast rather than a misnamed one.
The test pins both shapes, checks the fallback does NOT also fire in a year
where the Sunday carries the feast, and asserts across 2005-2050 that every
year gets it exactly once by one shape or the other.

Separately, the ferial days of that window get their own Mass. The Missal's
rubric at the feast says it directly: "Diebus ferialibus a 2 ad 5 ianuarii
Missa dicitur ut die 1 ianuarii" -- the Circumcision's Mass, Titus 2:11-15 /
Luke 2:21. They had been falling through to the Sunday within the octave.

Ferial only, and the exclusions are the interesting part: the Sunday carries
the Holy Name and its own Mass, and an unoccupied Saturday carries Our Lady's
office and hers. 3 January 2026 is such a Saturday and correctly reads
Titus 3:4-7, not 2:11-15 -- which works because the BVM check runs first.

All four days of 2-5 January 2026 now match colitur.
</content>
</entry>
<entry>
<title>fix(ef): Passion week and Holy Week are two weeks, not one</title>
<updated>2026-08-18T14:49:03Z</updated>
<author>
<name>Lukasz Kasprzak</name>
<email>lukas@labunix.xyz</email>
</author>
<published>2026-08-18T14:49:03Z</published>
<link rel='alternate' type='text/html' href='https://git.labunix.xyz/lectio.git/commit/?id=74d557580e747cf309b8217d34ab6b9336939fca'/>
<id>urn:sha1:74d557580e747cf309b8217d34ab6b9336939fca</id>
<content type='text'>
efWeek had no Passiontide case, so both weeks fell through to 0 and every day
of Holy Week took the slug of its Passion-week namesake: ef-passiontide-0-
monday served both Passion Monday and Holy Monday. The two weeks therefore
shared one set of readings and Holy Week could not have its own -- Good Friday
was reading Passion Friday's Mass.

Numbered from Passion Sunday now, so the weeks are 1 and 2 and the slugs
distinguish them.

The six ef-passiontide-0-* sections are replaced by colitur's twelve rather
than renamed, and the reason is worth recording: they were not simply Passion
week's Masses. The Tuesday section carried HOLY Tuesday's (Jer 11:18-20 with
the Passion according to Mark), which was Passion Tuesday's only by the same
collision -- colitur documents that specific mix-up and its own correction of
it. Renaming 0 to 1 would have kept the wrong Mass on Passion Tuesday while
looking like a clean migration.

All twelve are colitur's, verified there against both photographic scans. All
six days of Holy Week now match colitur, and so does Passion Tuesday.
</content>
</entry>
<entry>
<title>feat(ef): Rogation Monday and Tuesday (RG 87)</title>
<updated>2026-08-18T14:35:41Z</updated>
<author>
<name>Lukasz Kasprzak</name>
<email>lukas@labunix.xyz</email>
</author>
<published>2026-08-18T14:35:41Z</published>
<link rel='alternate' type='text/html' href='https://git.labunix.xyz/lectio.git/commit/?id=25aa1fa687ec714e145a998714c29a5f5728a603'/>
<id>urn:sha1:25aa1fa687ec714e145a998714c29a5f5728a603</id>
<content type='text'>
RG 87: "Litaniae minores seu Rogationes, per se, assignantur feriis II, III et
IV ante festum Ascensionis Domini." Easter+36 and +37 are that Monday and
Tuesday. RG 128(d) gives them violet -- "for the procession and Mass of the
Greater and Lesser Litanies" -- a penitential note inside white Paschaltide.
lectio computed no Rogation days at all and showed a plain paschaltide feria.

The WEDNESDAY is deliberately not built, and the test asserts its absence.
Easter+38 is by construction the Vigil of the Ascension, II class, which wins
the day outright; what the rubric asks for there is a COMMEMORATION, and this
function returns one office per day. colitur reaches it through a movable-date
entry in its sanctoral overlay. Extending the Rogation branch to +38 to
"complete" the rubric would silently displace the Vigil, so the test pins the
Vigil rather than leaving the omission to a comment.

Rank is left as the feria's: these are IV-class days and any III-class saint
displaces them, which is what makes the office rare. In 2026 all three are
impeded and none appears; 2027 has the Monday unimpeded, and lectio and
colitur are byte-identical across 2-5 May that year.

One oracle allow-list entry: missalemeum builds no Rogation days either and
shows a white feria, so it is the outlier here rather than lectio.
</content>
</entry>
<entry>
<title>feat(ef): the votive Office of Our Lady on Saturday (RG 78)</title>
<updated>2026-08-18T14:21:02Z</updated>
<author>
<name>Lukasz Kasprzak</name>
<email>lukas@labunix.xyz</email>
</author>
<published>2026-08-18T14:21:02Z</published>
<link rel='alternate' type='text/html' href='https://git.labunix.xyz/lectio.git/commit/?id=c9bdca22b2c4492fcbb6cca696fe3a8c13713867'/>
<id>urn:sha1:c9bdca22b2c4492fcbb6cca696fe3a8c13713867</id>
<content type='text'>
RG 78, Caput IX "De sancta Maria in sabbato": "In sabbatis, in quibus occurrit
Officium de feria IV classis, fit de sancta Maria in sabbato." A Saturday whose
office would otherwise be a IV-class feria keeps Our Lady's office instead, and
it is white -- RG 431(e) classes that Mass as a "Missa votiva IV classis ... de
B. Maria Virg.", RG 121(a) gives a votive Mass the colour of the feast-type it
answers to, and RG 120(b) makes feasts of the BVM white.

Smaller than it sounds, because the protasis is already computed. "A IV-class
feria" is exactly what rank still being RankClass4 means at that point: every
branch above has promoted Advent, Lent, Passiontide, the Ember days, the
privileged ferias and the Christmas octave out of it.

The SLUG IS DELIBERATELY UNCHANGED. colitur, which has had this office since
its v0.3.0, does the same: a bespoke slug would recur many times a year and
break the one-slug-per-liturgical-year invariant, and the readings are
selected by the Mass formulary rather than by the slug. Only the colour moves.
It is also the temporal office only -- a sanctoral feast that outranks the
Saturday still wins and brings its own colour, exactly as before.

Result: lectio and colitur now agree on the colour of EVERY DAY of 2026. Zero
mismatches, where this class alone was nine days that year and 439 rows across
2005-2050 -- the largest single divergence class between the two engines.

Pinned in both directions, because the tempting wrong fix is to whiten every
Saturday: Lent and Advent Saturdays are III-class violet ferias and must stay
violet, and Holy Saturday must too. Mutation-tested -- dropping the IV-class
guard reddens the test on the Lenten Saturday, not on the per-annum ones.
</content>
</entry>
<entry>
<title>fix(ef): weeks after Epiphany count from the first Sunday, not from Epiphany</title>
<updated>2026-08-18T14:16:07Z</updated>
<author>
<name>Lukasz Kasprzak</name>
<email>lukas@labunix.xyz</email>
</author>
<published>2026-08-18T14:16:07Z</published>
<link rel='alternate' type='text/html' href='https://git.labunix.xyz/lectio.git/commit/?id=7657c4126339606c7c70d4ccd336ed14e78010bc'/>
<id>urn:sha1:7657c4126339606c7c70d4ccd336ed14e78010bc</id>
<content type='text'>
efWeek anchored the Time-after-Epiphany week number at 6 January, which put
every week of the season one too high -- 14 January 2026 came out as week 2
when the first Sunday after Epiphany was 11 January and the week running from
it is the first. The error persisted from Epiphany until Septuagesima cut the
season short, so it was wrong for most of January and February every year.

The Sunday is the anchor because it carries the Mass the week is named for,
"Dominica I post Epiphaniam", and its ferias follow it. Epiphany itself is a
feast inside Christmas Time (RG 72), not the head of a numbered week -- which
is the same distinction the Christmas Time boundary fix just made, showing up
again one function along.

Week numbers now match colitur day for day through the season.

The test pins the ordinary years AND the edge that a naive fix gets wrong:
when Epiphany itself falls on a Sunday, "the first Sunday AFTER Epiphany" is
the following week, so the helper starts its search on 7 January rather than
the 6th. It finds such a year in 2005..2050 rather than hardcoding one.
Mutation-tested: restoring the 6 January anchor reddens it.
</content>
</entry>
<entry>
<title>fix(ef): Christmas Time runs to 13 January (RG 72)</title>
<updated>2026-08-18T14:02:17Z</updated>
<author>
<name>Lukasz Kasprzak</name>
<email>lukas@labunix.xyz</email>
</author>
<published>2026-08-18T14:02:17Z</published>
<link rel='alternate' type='text/html' href='https://git.labunix.xyz/lectio.git/commit/?id=27acee1930dc728b286c655cb66bd5e44804df6f'/>
<id>urn:sha1:27acee1930dc728b286c655cb66bd5e44804df6f</id>
<content type='text'>
RG 72: "Tempus natalicium decurrit a I Vesperis Nativitatis Domini usque ad
diem 13 ianuarii INCLUSIVE." 6-13 January is the Epiphany section OF Christmas
Time, not the start of Time after Epiphany. RG 119(a) backs the same boundary
from the colour side, white "usque ad expletum tempus Epiphaniae". lectio
ended Christmas Time on 5 January, so eight days a year came out green that
should be white, and Epiphany itself reported the wrong season.

NOT a one-line boundary change, which is why an earlier attempt was reverted
rather than shipped. efSeason both named the slugs and reported the season, so
moving the boundary renamed every slug in the window -- ef-time-after-epiphany-*
became ef-christmas-* -- and orphaned the ef-time-after-epiphany-sunday-1
lectionary key. The two are now separate axes:

  efSlugSeason  names slugs and numbers weeks; unchanged behaviour
  efSeason      the liturgical season, which the colour follows; RG 72's

The split is not a new idea in this file. The resumed-Sundays branch already
does exactly this and says so -- "Season stays time-after-pentecost
(calendrical); the Epiphany slug only routes the readings" -- it simply had no
name, so the one place that needed it could not reuse it.

Result, checked against colitur across all 365 days of 2026: SEASON now agrees
on every single day, where it previously differed on eight. Colour differences
drop to nine, every one a Saturday and every one the BVM Saturday Office
(RG 78, colitur C17) which lectio does not build. Slugs and week numbers are
byte-identical to before.

The oracle's season axis now consults the allow-list and excludes excused days
from its threshold. Neither was true before, because no cited divergence had
ever BEEN a season difference -- the January window showed up as a colour one,
since the wrong season produced the wrong colour too. With the season right,
what is left is a season difference against missalemeum, and an axis whose
threshold is a t.Fatalf would have failed regardless of any allow-list.

TestTemporalEFChristmastideBoundary pins the boundary AND the split, mutation-
tested both ways: reverting the boundary reddens it, and re-merging the two
seasons so the slug is built from the liturgical one reddens it differently,
naming the orphaned lectionary key.
</content>
</entry>
<entry>
<title>fix(ef): twelve RG 124 colours -- missalemeum has the rule inverted</title>
<updated>2026-08-18T13:39:48Z</updated>
<author>
<name>Lukasz Kasprzak</name>
<email>lukas@labunix.xyz</email>
</author>
<published>2026-08-18T13:39:48Z</published>
<link rel='alternate' type='text/html' href='https://git.labunix.xyz/lectio.git/commit/?id=967e911728eefb85dc53065c73c5c5d4cdce6a59'/>
<id>urn:sha1:967e911728eefb85dc53065c73c5c5d4cdce6a59</id>
<content type='text'>
RG 124, "De coloribus paramentorum", section C, both photographic scans word
for word: 124(b) gives red to "Sanctorum Apostolorum et Evangelistarum, in
eorum die natalicio, excepto festo S. Ioannis (27 decembris)", and 124(e) to
"Sanctorum Martyrum, quorum colitur aut martyrium". Red is for Apostles,
Evangelists and Martyrs. A Confessor, Doctor or non-martyr Virgin is white.

missalemeum has twelve of these INVERTED IN BOTH DIRECTIONS: ten confessors,
doctors and virgins in red (Ephrem, Julia of Falconieri, John Gualbert,
Camillus de Lellis, Jerome Emiliani, Martha, Alphonsus Liguori, Augustine,
Rose of Lima, John of San Fecundo), and two actual martyrs in white
(Apollinaris and Josaphat, both Bishop and Martyr). Both directions failing is
why this reads as one rule applied backwards rather than twelve separate
slips, and why they are corrected as a block.

Adjudicated by colitur against the scans -- its C18 carries the per-slug
reasoning and its M21 the oracle side, verdict colitur. Two of its fourteen,
conversion-of-st-paul and chair-of-st-peter, already agreed here.

Found by a full-year sweep comparing clectio's output against colitur day by
day, not by the patch tool that produced the earlier corrections. That tool
reads colitur's sanctoral.sexp directly and so misses everything in
adjustments.sexp, the overlay where all twenty of colitur's colour edits
actually live -- its SECOND structural blind spot, after temporal days.

All twelve go into the generator's missalOverrides table, so regeneration
preserves them, and into TestTridentineMissalOverrides, now sixteen pinned
fields. Mutation-tested in BOTH directions: reverting a martyr to white and a
confessor to red each redden the test with the rubric named. Pinning only one
direction would have let a careless "make them all white" fix pass ten of
twelve.

The oracle allow-list gains one entry covering the twelve dates in both years.
</content>
</entry>
</feed>
