diff options
Diffstat (limited to 'lib')
| -rw-r--r-- | lib/kernel/calendar.ml | 104 |
1 files changed, 79 insertions, 25 deletions
diff --git a/lib/kernel/calendar.ml b/lib/kernel/calendar.ml index 27e564f..f2a03a7 100644 --- a/lib/kernel/calendar.ml +++ b/lib/kernel/calendar.ml @@ -305,33 +305,86 @@ let build_day (rite : ('s, 'r) Rite.t) (idx : 'r Layer.by_date) double-booking it in both would fail Task 12's "appears exactly once" reading of this day alone. *) (* Whether a transferred/injected candidate is genuinely accounted for at - its assigned target -- true if it won the day outright there (every - [Transfer]-disposed candidate this kernel produced before a rite could - transfer a [Celebration.status = Commemoration_only] one: a losing - FEAST, which RG 96's own "next day not I or II class" guarantees an - unblocked day to win once it arrives -- [occupant_of] alone answered - this), OR if it survives at the target as one of the day's own admitted + its assigned target -- true under any of THREE conditions, not two + (fix round 1, ef-major-litanies task -- the first version of this + function, and this comment, claimed two, missing exactly the same + shape one channel further out than the bug it had just fixed; see + below for how that was found). + + (1) It won the day outright there (every [Transfer]-disposed candidate + this kernel produced before a rite could transfer a + [Celebration.status = Commemoration_only] one: a losing FEAST, which + RG 96's own "next day not I or II class" guarantees an unblocked day + to win once it arrives -- [occupant_of] alone used to answer this, and + still would). + + (2) It survives at the target as one of the day's own admitted COMMEMORATIONS instead (a shape [place_transfers] itself never used to produce, because nothing before could dispose a [Commemoration_only] - candidate as [Transfer] -- {!Precedence.resolve} holds such a candidate - out of the band contest entirely, so it can never win a day outright, - only ever be commemorated on one; a rite is nonetheless free to - [Transfer] one to a named date, e.g. RG 80's Major Litanies, RG 81's - own "nihil fit in Officio" making [observed] structurally impossible - for it anywhere). [occupant_of] alone under-reports this second shape - as unsettled, which previously had no live witness to catch it: a - transferred candidate that only ever becomes a commemoration, never the - day's own office, was mislabelled here with [unconverged_reason] (a - hardcoded string, not a true read of the placement pass's own - convergence -- {!place_transfers} itself had already reached a fixed - point) and double-counted by {!Validate}'s own "duplicated" check - (sighted once in [omitted] here under that wrong label, and correctly - again in [commemorations] at its target) -- found by this kernel's own - exhaustive property sweep once a rite (Rite_ef, RG 80) first produced - this shape, not guessed at in advance. Kept rite-agnostic: nothing here - reads anything EF-specific, only {!Precedence.resolution}'s own - [observed]/[commemorations] fields, the same two channels - {!Liturgical_day.t} already promises never to lose. *) + candidate as [Transfer] -- {!Precedence.resolve} holds such a + candidate out of the band contest entirely, so it can never win a day + outright, only ever be commemorated on one; a rite is nonetheless free + to [Transfer] one to a named date, e.g. RG 80's Major Litanies, RG + 81's own "nihil fit in Officio" making [observed] structurally + impossible for it anywhere). The FIRST version of this function + stopped here, at (1) and (2) -- {!Liturgical_day.t}'s own doc comment + promises [observed] and [commemorations] are never silently lost, and + this read as "the same two channels", which is where the "two, not + three" miscount came from: that promise is about {!Liturgical_day.t}'s + OWN five fields, not an exhaustive account of every way + {!Precedence.resolve} can dispose of a candidate at one date. + + (3) It reaches the target and is disposed there as [Omit] by + {!Precedence.rules.disposition} itself (RG 33's vigil omission, RG + 16(a)'s Sunday suppression, RG 26's IV-class-feria omission, the + Lord-vs-Lord exclusion, ...) OR is offered to + {!Precedence.rules.admit} there but capped out by an admission-count + limit RG 111 itself imposes (e.g. a Class1 day admits only ONE + privileged commemoration; a second one due the same day, or the SAME + Litanies candidate no longer privileged, loses that slot) -- both + land in the target's own [omitted], with their own accurate, + already-diagnostic reason ("omitted: yielded to a higher day" / + "omitted: admission limit reached"), and both are just as genuinely + "delivered and considered" as (1)/(2), not stuck anywhere. Missing + this third channel reproduces the EXACT ORIGINAL BUG this function was + written to fix, one level further out: a candidate settled (via (3)) + at its target was still reported [unresolved] at its ORIGIN, under + the same wrong, hardcoded [unconverged_reason] label, and + double-counted by {!Validate}'s own "duplicated" check the same way. + + Found, not merely reasoned to: fix round 1's review reproduced it two + ways. Constructively, a second privileged [Commemoration_only] entry + placed on the Litanies' own transfer target (Easter+2) that sorts + ahead of it forces the Litanies to lose {!Precedence.rules.admit}'s + own Class1 "one privileged commemoration only" cap there. And, + already present in this branch's own mutation-testing record without + being run to ground at the time: mutation 3 (this task's own report, + `privilege_of`'s RG 109(f) branch forced to [Ordinary]) makes the + transferred Litanies itself lose that SAME Class1 cap at its OWN + target -- no second candidate needed, since an [Ordinary] commemoration + has no standing at all against a [Class1] day's privileged-only + admission rule ({!Precedence_ef.admit}'s own Class1 case). That + mutation's 9th failure, the exhaustive `Validate` property sweep on a + random year, was this bug; the report noted the failure and declined + to diagnose it before reverting the mutation, which is precisely how + it survived one fix round. + + Unreachable on shipped EF data today (every `Commemoration_only` + candidate this rite's own real data carries is at most [Class3] + except the Litanies themselves, and no second privileged + commemoration can ever fall on Easter+2 -- {!Precedence_ef + .privilege_of}'s own (a)-(e) categories are all Sunday/Ember/Advent- + Lent-Passiontide/Nativity-octave shaped, none of which Easter+2 is or + can be), which is exactly why it survived this far: nothing in the + shipped calendar has ever exercised it. Fails LOUDLY (a wrong, + misleading label) rather than silently, and is fixed here rather than + left as a documented residual, since the fix is a one-line + generalisation of the same check, not new machinery. Kept + rite-agnostic: nothing here reads anything EF-specific, only + {!Precedence.resolution}'s own [observed]/[commemorations]/[omitted] + fields -- THREE of {!Precedence.resolution}'s four fields (the fourth, + [deferred], denotes a candidate that has NOT yet settled at this date, + by definition, so it is correctly never consulted here). *) let settled_at target slug = let _, _, target_resolution = resolve_with_injected rite idx injected target in let matches (c : 'r Precedence.candidate) = @@ -339,6 +392,7 @@ let build_day (rite : ('s, 'r) Rite.t) (idx : 'r Layer.by_date) in matches target_resolution.Precedence.observed || List.exists (fun (c, _) -> matches c) target_resolution.Precedence.commemorations + || List.exists (fun (c, _) -> matches c) target_resolution.Precedence.omitted in let unresolved c = let slug = c.Precedence.cel.Celebration.slug in |
