Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 114 additions & 0 deletions backend/openapi/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -902,6 +902,82 @@
}
}
},
"/api/v1/bills/{id}/diff": {
"get": {
"tags": ["Bills"],
"summary": "Get bill version diff",
"description": "Returns a clause-aware diff between two published versions of a bill. The backend computes the structured diff against the ingested LEGISinfo version text; the iOS surface only renders the result and never parses LEGISinfo wire formats.",
"operationId": "getBillVersionDiff",
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"description": "Bill identifier from the list bills response.",
"schema": { "type": "string", "example": "C-8" }
},
{
"name": "from",
"in": "query",
"required": true,
"description": "Identifier of the \"before\" version, as published in the bill depth response's versions list.",
"schema": { "type": "string", "example": "C-8-v1" }
},
{
"name": "to",
"in": "query",
"required": true,
"description": "Identifier of the \"after\" version, as published in the bill depth response's versions list.",
"schema": { "type": "string", "example": "C-8-v3" }
}
],
"responses": {
"200": {
"description": "Structured clause-level diff between the two versions.",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/BillVersionDiff" },
"examples": {
"diff": {
"value": {
"from": {
"id": "C-8-v1",
"label": "First reading",
"stage": "First Reading",
"chamber": "House of Commons",
"published_on": "2026-04-27"
},
"to": {
"id": "C-8-v3",
"label": "As passed by the House",
"stage": "Third Reading",
"chamber": "House of Commons",
"published_on": "2026-06-04"
},
"clauses": [
{
"id": "clause-3",
"label": "Clause 3",
"change_type": "added",
"from_text": "",
"to_text": "The Minister shall publish quarterly progress reports to Parliament.",
"hansard_anchor_url": "https://www.ourcommons.ca/DocumentViewer/en/45-1/house/sitting-42/hansard#clause-3"
}
]
}
}
}
}
}
},
"204": { "description": "Backend has no diff available for the requested version pair (text missing or diff job not run yet)." },
"404": { "$ref": "#/components/responses/Error" },
"429": { "$ref": "#/components/responses/RateLimit" },
"500": { "$ref": "#/components/responses/Error" },
"503": { "$ref": "#/components/responses/Error" }
}
}
},
"/api/v1/bills/{legisinfo_id}/lobbying-context": {
"get": {
"tags": ["Bills", "Lobbying"],
Expand Down Expand Up @@ -2641,6 +2717,44 @@
"source_url": { "type": "string", "format": "uri" }
}
},
"BillVersionDiff": {
"type": "object",
"required": ["from", "to", "clauses"],
"properties": {
"from": { "$ref": "#/components/schemas/BillVersion" },
"to": { "$ref": "#/components/schemas/BillVersion" },
"clauses": {
"type": "array",
"items": { "$ref": "#/components/schemas/BillClauseDiff" }
}
}
},
"BillClauseDiff": {
"type": "object",
"required": ["change_type"],
"properties": {
"id": { "type": "string" },
"label": { "type": "string" },
"change_type": {
"type": "string",
"enum": ["added", "removed", "modified", "unchanged"],
"description": "Kind of change the clause represents. Synonyms ('inserted', 'deleted', 'replaced', 'context', etc.) are normalised by the iOS adapter."
},
"from_text": {
"type": "string",
"description": "Verbatim clause text from the 'before' version. Empty for purely added clauses."
},
"to_text": {
"type": "string",
"description": "Verbatim clause text from the 'after' version. Empty for purely removed clauses."
},
"hansard_anchor_url": {
"type": "string",
"format": "uri",
"description": "Hansard anchor for the chamber speech that introduced the change, when known."
}
}
},
"BillAmendment": {
"type": "object",
"properties": {
Expand Down
51 changes: 50 additions & 1 deletion docs/architecture/use-case-catalog.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@ For the Clean Architecture shape this catalog assumes, see [`docs/architecture/`
| `Bill` | A Parliament of Canada bill with number, title, stage, sponsor, Royal Assent date when available, and LEGISinfo source URL. |
| `BillCommitteeStage` | A bill's active committee study stage, carrying the committee, study dates, upcoming meetings, and past meetings. |
| `BillCommitteeMeeting` | A committee meeting tied to a bill study, including meeting number, date, optional evidence URL, and witness count. |
| `BillVersion` | Backend-only bill publication/version row with source links for text, PDF, and XML artifacts. |
| `BillVersion` | A published version of a bill (e.g. "First reading", "As passed by the House") with label, stage, chamber, publication date, and source URL. Surfaced on the iOS bill page via `LoadBillVersions` to drive the "Compare versions" picker. |
| `BillVersionDiff` | A clause-level diff between two published bill versions, carrying the `fromVersion`/`toVersion` metadata and an ordered list of `BillClauseDiff` entries. Surfaced on the iOS diff viewer via `LoadBillVersionDiff`. |
| `BillClauseDiff` | One clause within a `BillVersionDiff`: label, change type (added/removed/modified/unchanged), verbatim before/after text, and optional Hansard anchor for the chamber speech that introduced the change. |
| `BillAmendment` | House or committee amendment record associated with a bill and chamber stage, with number, sponsor name, status, stage, verbatim amendment text, and source link. Surfaced on the iOS bill page via `LoadBillAmendments`. |
| `PBOCosting` | Parliamentary Budget Officer independent costing note linked to a bill, with verbatim headline figure (5-year cost in millions), methodology category, publication date, report PDF URL, and optional summary text. Surfaced on the iOS bill page via `LoadPBOCosting`. |
| `ParliamentaryCommittee` | A House or Senate committee reference with acronym, name, chamber code, and authoritative source URL. |
Expand Down Expand Up @@ -102,6 +104,8 @@ to the issue that will build the missing artifact.
| `BillRepository` | backend Go | outbound | Implemented: `backend/bills/internal/usecase/bills.go`; adapter: `backend/bills/internal/adapter/sqlite/repository.go`. | List bills and load bill-depth rows from the verified bills SQLite artifact. |
| `BillCommitteeStageRepository` | iOS Swift | outbound | Implemented: `ios/epac/Domain/Ports/BillCommitteeStageRepository.swift`; adapter: `ios/epac/Data/Repositories/BackendBillCommitteeStageRepository.swift`. | Load the committee currently studying a bill, including study dates and meeting rows. |
| `BillAmendmentsRepository` | iOS Swift | outbound | Implemented: `ios/epac/Domain/Ports/BillAmendmentsRepository.swift`; adapter: `ios/epac/Data/Repositories/BackendBillAmendmentsRepository.swift`. | Load amendments tabled against a bill (number, mover, stage, status, verbatim text) from the backend bill-depth endpoint. |
| `BillVersionsRepository` | iOS Swift | outbound | Implemented: `ios/epac/Domain/Ports/BillVersionsRepository.swift`; adapter: `ios/epac/Data/Repositories/BackendBillVersionsRepository.swift`. | Load the published versions of a bill (label, stage, chamber, publication date, source URL) from the backend bill-depth endpoint. |
| `BillVersionDiffRepository` | iOS Swift | outbound | Implemented: `ios/epac/Domain/Ports/BillVersionDiffRepository.swift`; adapter: `ios/epac/Data/Repositories/BackendBillVersionDiffRepository.swift`. | Load a clause-level diff between two published bill versions from the backend `GET /api/v1/bills/{id}/diff?from=…&to=…` endpoint. |
| `PBOCostingQueryPort` | iOS Swift | outbound | Implemented: `ios/epac/Domain/Ports/PBOCostingQueryPort.swift`; adapter: `ios/epac/Data/Repositories/BackendPBOCostingRepository.swift`. | Load Parliamentary Budget Officer costing notes linked to a bill from the backend `GET /pbo/by-bill/{legisinfo_id}` endpoint; `nil` when no PBO link record exists. |
| `RecentLawQueryPort` | iOS Swift | outbound | Implemented by `ios/epac/Application/TrackRoyalAssent.swift`; adapter input is `BillRepository`. | Query current-session bills that received Royal Assent within the recent-law window. |
| `MemberRepository` | backend Go | outbound | Implemented: `backend/members/internal/usecase/members.go`; adapter: `backend/members/internal/adapter/sqlite/repository.go`. | List members and load member-profile attendance rows from the verified members SQLite artifact. |
Expand Down Expand Up @@ -690,6 +694,51 @@ Current implementation:

---

### LoadBillVersions

```
Actor: User (iOS app, bill detail)
Goal: Pick which two published versions of a bill to compare in the diff viewer (e.g. "First reading" vs. "As passed by the House").
Inputs: LEGISinfo bill ID.
Outputs: Optional [BillVersion]; nil when the backend has no versions record for the bill (hide the "Compare versions" entry point), empty array when the bill is tracked but no version text has been ingested yet (same UI treatment), single-element array when only one version exists (the diff viewer renders an empty state), multi-element array otherwise.
Entities / values: Bill, BillVersion.
Ports: iOS Swift: `BillVersionsRepository`.
Primary adapters: BackendBillVersionsRepository (GET /api/v1/bills/{id}), BillDetailView, BillVersionsDiffView.
Current implementation:
ios/epac/Application/LoadBillVersions.swift
ios/epac/Domain/Entities/BillVersion.swift
ios/epac/Domain/Ports/BillVersionsRepository.swift
ios/epac/Data/Repositories/BackendBillVersionsRepository.swift
ios/epac/Views/Bills/BillVersionsDiffView.swift
ios/epac/Views/Bills/BillDetailView.swift
```

> **Boundary rule:** Version metadata is reproduced verbatim from the backend's typed JSON. The iOS layer never parses LEGISinfo or parl.ca HTML/XML wire formats; published-version ingestion is a backend responsibility. The bill page hides the "Compare versions" entry point when the use case returns `nil` or an empty array, and the diff viewer renders an empty state when only one version has been published.

---

### LoadBillVersionDiff

```
Actor: User (iOS app, bill diff viewer)
Goal: See what changed at the clause level between two published versions of a bill (additions, deletions, modifications) with the verbatim before/after clause text, and follow the change back to the chamber speech that introduced it when known.
Inputs: LEGISinfo bill ID, "before" version ID, "after" version ID.
Outputs: Optional BillVersionDiff; nil when the backend cannot produce a diff for the requested version pair (either version is missing text, or the diff job has not run yet) — the diff viewer renders an unavailable state.
Entities / values: Bill, BillVersion, BillVersionDiff, BillClauseDiff, BillClauseChangeType.
Ports: iOS Swift: `BillVersionDiffRepository`.
Primary adapters: BackendBillVersionDiffRepository (GET /api/v1/bills/{id}/diff?from=…&to=…), BillVersionsDiffView.
Current implementation:
ios/epac/Application/LoadBillVersionDiff.swift
ios/epac/Domain/Entities/BillVersionDiff.swift
ios/epac/Domain/Ports/BillVersionDiffRepository.swift
ios/epac/Data/Repositories/BackendBillVersionDiffRepository.swift
ios/epac/Views/Bills/BillVersionsDiffView.swift
```

> **Boundary rule:** The clause-aware diff algorithm lives in the backend — respecting clause/sub-clause structure is a use-case policy enforced on the backend side; iOS only renders the structured result. Clause text is rendered verbatim; the diff viewer never paraphrases or summarises the change with generated text. Hansard anchors are surfaced when the backend has them and omitted otherwise — the view does not invent them.

---

### TagPrivateMembersBill

```
Expand Down
21 changes: 21 additions & 0 deletions ios/epac/Application/LoadBillVersionDiff.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/// Loads the structured clause-level diff between two published bill versions.
///
/// Returns `nil` when the backend cannot produce a diff for the requested pair
/// — the diff viewer renders an unavailable state in that case. The clause-
/// aware diff algorithm lives in the backend; this use case is the application
/// policy boundary that the iOS view sits behind.
struct LoadBillVersionDiff: Sendable {
let repository: any BillVersionDiffRepository

func execute(
billID: String,
fromVersionID: String,
toVersionID: String
) async throws -> BillVersionDiff? {
try await repository.loadBillVersionDiff(
billID: billID,
fromVersionID: fromVersionID,
toVersionID: toVersionID
)
}
}
13 changes: 13 additions & 0 deletions ios/epac/Application/LoadBillVersions.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/// Loads the published versions of one bill.
///
/// Returns `nil` when the backend has no versions record for the bill — the
/// caller hides the "Compare versions" entry point in that case. An empty
/// array gets the same UI treatment. A single-element array means only one
/// version has been ingested and the diff viewer's empty state applies.
struct LoadBillVersions: Sendable {
let repository: any BillVersionsRepository

func execute(billID: String) async throws -> [BillVersion]? {
try await repository.loadBillVersions(billID: billID)
}
}
Loading
Loading