feat: serialise the resolved day to the versioned output contract (#53) #354

Merged
manlycucumber merged 1 commit from feature/53-output-contract-serialiser into develop 2026-07-02 04:40:18 +00:00
manlycucumber commented 2026-07-02 04:39:16 +00:00 (Migrated from github.com)

Closes #53.

Adds the public output contract the Api/Site/Ordo repos build on: a new Introibo\Core\Contract namespace that serialises a resolved LiturgicalDay to a versioned, deterministic, JSON-ready structure. LiturgicalDay stays a pure aggregate; the serialiser is the seam.

What's here

  • Contract\DayContract — frozen SHAPE_VERSION = 1.0.0; from(LiturgicalDay, Provenance)toArray() / toJson() (flags JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR). Offices are serialised via an instanceof branch (temporal vs sanctoral) so the shape is uniform.
  • Contract\Provenance — the three independent version axes: edition (roman:rubricae-1960), corpusVersion (1962-seed), engineVersion (Introibo::VERSION).
  • Introibo\Core\contract(DateTimeImmutable): array — the public entry point beside day(), sharing one memoised year resolver. Introibo::VERSION and ResolvedYear::provenance() added.

Resolver ripple (offices become self-describing)

  • RoledObservance now also carries the OccurrenceOutcome and the transferredTo / transferredFrom dates.
  • LiturgicalDay holds roled offices; the bare-observance accessors (celebration() etc.) are retained, so every existing reader is unaffected.
  • DayResolver stamps outcomes in assemble() and links every transfer end-to-end in a reconciliation pass. The links follow each feast's hop chain across the year rather than a single impeded/landing pair, so a re-transfer cascade or a feast celebrated on its own date link correctly (hardened after an adversarial review of the reconciliation).

Shape (per the frozen contract spec)

Day: contractVersion · corpusVersion · engineVersion · rite · edition · date · season · commemorationLimit · celebration/commemoration/displaced/tempora (office lists) · secondVespers (object) · reserved firstVespers/resolution/fasting/calendar.
Office: id · urn · role · kind · rank/rankOrdinal · season · colour{base,roseAllowed} · names{la} · titulars · outcome · transferredTo/transferredFrom · vigilOf · reserved octaveOf/aliases/citations/text/chant/audio.

Tests & docs

  • Golden snapshot pins the full shape for a sample day; focused snapshots pin the transfer (both ends), omission, and commemoration cases, plus determinism and JSON round-trip.
  • docs/design/output-contract.md documents the shape, the three-version scheme, id-vs-urn, transfer links, and reserved slots (bump-rules and worked examples expand in #58).

Local gate: composer lint (0), composer analyse ([OK]), composer test (479 tests / 30,360 assertions).

Closes #53. Adds the public **output contract** the Api/Site/Ordo repos build on: a new `Introibo\Core\Contract` namespace that serialises a resolved `LiturgicalDay` to a versioned, deterministic, JSON-ready structure. `LiturgicalDay` stays a pure aggregate; the serialiser is the seam. ## What's here - **`Contract\DayContract`** — frozen `SHAPE_VERSION = 1.0.0`; `from(LiturgicalDay, Provenance)` → `toArray()` / `toJson()` (flags `JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR`). Offices are serialised via an `instanceof` branch (temporal vs sanctoral) so the shape is uniform. - **`Contract\Provenance`** — the three independent version axes: `edition` (`roman:rubricae-1960`), `corpusVersion` (`1962-seed`), `engineVersion` (`Introibo::VERSION`). - **`Introibo\Core\contract(DateTimeImmutable): array`** — the public entry point beside `day()`, sharing one memoised year resolver. `Introibo::VERSION` and `ResolvedYear::provenance()` added. ## Resolver ripple (offices become self-describing) - `RoledObservance` now also carries the `OccurrenceOutcome` and the `transferredTo` / `transferredFrom` dates. - `LiturgicalDay` holds roled offices; the bare-observance accessors (`celebration()` etc.) are retained, so **every existing reader is unaffected**. - `DayResolver` stamps outcomes in `assemble()` and links every transfer end-to-end in a reconciliation pass. The links follow each feast's **hop chain** across the year rather than a single impeded/landing pair, so a re-transfer cascade or a feast celebrated on its own date link correctly (hardened after an adversarial review of the reconciliation). ## Shape (per the frozen contract spec) Day: `contractVersion` · `corpusVersion` · `engineVersion` · `rite` · `edition` · `date` · `season` · `commemorationLimit` · `celebration`/`commemoration`/`displaced`/`tempora` (office lists) · `secondVespers` (object) · reserved `firstVespers`/`resolution`/`fasting`/`calendar`. Office: `id` · `urn` · `role` · `kind` · `rank`/`rankOrdinal` · `season` · `colour{base,roseAllowed}` · `names{la}` · `titulars` · `outcome` · `transferredTo`/`transferredFrom` · `vigilOf` · reserved `octaveOf`/`aliases`/`citations`/`text`/`chant`/`audio`. ## Tests & docs - Golden snapshot pins the full shape for a sample day; focused snapshots pin the transfer (both ends), omission, and commemoration cases, plus determinism and JSON round-trip. - `docs/design/output-contract.md` documents the shape, the three-version scheme, id-vs-urn, transfer links, and reserved slots (bump-rules and worked examples expand in #58). Local gate: `composer lint` (0), `composer analyse` ([OK]), `composer test` (479 tests / 30,360 assertions).
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!354
No description provided.