feat: emit a cited resolution trace from day() (#234) #432

Merged
manlycucumber merged 1 commit from feature/234-resolution-trace into develop 2026-07-02 19:03:36 +00:00
manlycucumber commented 2026-07-02 19:02:16 +00:00 (Migrated from github.com)

First of Epic #233 (show-your-work resolution trace). Closes #234.

Fills the output contract's reserved day-level resolution slot with a cited "why-this-won" trace.

Design — docs/design/resolution-trace-model.md

  • Opt-in. day() / contract() are unchanged — resolution stays null, payloads stay lean, and the golden digest is untouched (it hashes the default contract; no re-freeze). explain($date) / contract($date, true) resolve the year with a distinct tracing resolver and populate the slot — the API's ?explain surface.
  • No decision/explanation drift. Each occurrence reason comes from the same private decideOccurrence() that decides the outcome, so occurrenceOutcome() and the new explainOccurrence() are two readers of one decision. The trace records the actual sorted candidates + outcomes from assemble(), not a reconstruction — transfers and displaced offices are reported exactly as decided.
  • Every step cited to rg-1960:<n>, reusing the Codex Rubricarum paragraphs already vetted in the precedence rules.

Shape (a real trace, St Silverius 2024-06-20 — the #424 fix, shown)

"resolution": {
  "winner": { "id": "…:pentecost-time:week-4:feria-5", "line": 28, "rule": "n91-table-of-liturgical-days",
              "summary": "celebrated as the day's highest office, line 28 …", "citation": "rg-1960:91" },
  "candidates": [ {"id":"…:feria-5","rank":4,"kind":"feria","tier":{"ordinal":28,"line":28,"selector":"fourth"}},
                  {"id":"roman:sanctorale:silverius","rank":4,"kind":"commemoration-only","tier":{"ordinal":29,"line":29,"selector":"commemoration"}} ],
  "losers": [ {"id":"roman:sanctorale:silverius","outcome":"commemorate","rule":"commemoration-admitted",
               "summary":"commemorated: an impeded office is kept as a commemoration …","citation":"rg-1960:112"} ],
  "commemorationLimit": { "value": 2, "rule": "n111-commemoration-limit", "summary": "a class 4 day admits 2 commemoration(s)", "citation": "rg-1960:111" }
}

What's here

  • src/Trace/ResolutionReason.php, ResolutionTrace.php — the trace VOs (depend only on Citation, so the Precedence→Trace dependency stays one-way).
  • PrecedenceTier carries its selector + n.91 line (already in the corpus data); the sort still keys only on ordinal/subOrder → resolution unchanged.
  • PrecedenceRules gains explainPrecedence / explainOccurrence / explainCommemorationLimit; Rubrics1962Precedence implements them via the single-source decideOccurrence().
  • DayResolver::explaining() (opt-in tracing mode) records the trace in assemble(); LiturgicalDay carries it; DayContract fills resolution; functions.php adds explain().
  • Tests exercise the real explain()/contract() on contested days (commemoration-grade #424, Annunciation transfer, All Saints, Christmas).

Colour + season derivation (#235), citation-completeness (#236), and golden trace fixtures (#240) follow.

Gate: lint clean · analyse [OK] · test 652 (+11) · golden unchanged · corpus-verify OK.

First of Epic #233 (show-your-work resolution trace). Closes #234. Fills the output contract's **reserved** day-level `resolution` slot with a cited "why-this-won" trace. ## Design — `docs/design/resolution-trace-model.md` - **Opt-in.** `day()` / `contract()` are unchanged — `resolution` stays `null`, payloads stay lean, and **the golden digest is untouched** (it hashes the default contract; no re-freeze). `explain($date)` / `contract($date, true)` resolve the year with a *distinct tracing resolver* and populate the slot — the API's `?explain` surface. - **No decision/explanation drift.** Each occurrence reason comes from the *same* private `decideOccurrence()` that decides the outcome, so `occurrenceOutcome()` and the new `explainOccurrence()` are two readers of one decision. The trace records the **actual** sorted candidates + outcomes from `assemble()`, not a reconstruction — transfers and displaced offices are reported exactly as decided. - **Every step cited** to `rg-1960:<n>`, reusing the Codex Rubricarum paragraphs already vetted in the precedence rules. ## Shape (a real trace, St Silverius 2024-06-20 — the #424 fix, shown) ```jsonc "resolution": { "winner": { "id": "…:pentecost-time:week-4:feria-5", "line": 28, "rule": "n91-table-of-liturgical-days", "summary": "celebrated as the day's highest office, line 28 …", "citation": "rg-1960:91" }, "candidates": [ {"id":"…:feria-5","rank":4,"kind":"feria","tier":{"ordinal":28,"line":28,"selector":"fourth"}}, {"id":"roman:sanctorale:silverius","rank":4,"kind":"commemoration-only","tier":{"ordinal":29,"line":29,"selector":"commemoration"}} ], "losers": [ {"id":"roman:sanctorale:silverius","outcome":"commemorate","rule":"commemoration-admitted", "summary":"commemorated: an impeded office is kept as a commemoration …","citation":"rg-1960:112"} ], "commemorationLimit": { "value": 2, "rule": "n111-commemoration-limit", "summary": "a class 4 day admits 2 commemoration(s)", "citation": "rg-1960:111" } } ``` ## What's here - `src/Trace/ResolutionReason.php`, `ResolutionTrace.php` — the trace VOs (depend only on `Citation`, so the Precedence→Trace dependency stays one-way). - `PrecedenceTier` carries its `selector` + n.91 `line` (already in the corpus data); the sort still keys only on `ordinal`/`subOrder` → resolution unchanged. - `PrecedenceRules` gains `explainPrecedence` / `explainOccurrence` / `explainCommemorationLimit`; `Rubrics1962Precedence` implements them via the single-source `decideOccurrence()`. - `DayResolver::explaining()` (opt-in tracing mode) records the trace in `assemble()`; `LiturgicalDay` carries it; `DayContract` fills `resolution`; `functions.php` adds `explain()`. - Tests exercise the real `explain()`/`contract()` on contested days (commemoration-grade #424, Annunciation transfer, All Saints, Christmas). **Colour + season derivation (#235), citation-completeness (#236), and golden trace fixtures (#240) follow.** Gate: `lint` clean · `analyse` [OK] · `test` 652 (+11) · **golden unchanged** · corpus-verify OK.
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Directorium/Core!432
No description provided.