# TASK-010: Implement pull-request and issue API contracts Status: implemented; all required checks pass; pending Codex review and contract freeze Owner: Claude (ownership transferred by human product owner) Reviewer: Codex ## Outcome Provide frozen read-only domain and data contracts for pull requests, issues, reviews, and comments so Claude can implement TASK-008 without editing shared layers. ## Writable paths - `app/src/main/java/com/ubisam/ubgitea/domain/workitems/` - `app/src/main/java/com/ubisam/ubgitea/data/workitems/` - `app/src/test/java/com/ubisam/ubgitea/domain/workitems/` - `app/src/test/java/com/ubisam/ubgitea/data/workitems/` - `tasks/010-work-items-api-contract.md` ## Acceptance criteria - [x] Pull requests and issues use separate immutable domain models (`IssueSummary`/`IssueDetail` vs `PullRequestSummary`/`PullRequestDetail`; no shared base type, no shared mutable state). - [x] Lists, reviews, and comments have explicit immutable pagination contracts — reuses the existing `domain.model.Page` (unmodifiable list, explicit `nextPage`) already used by every other paged endpoint in the app, rather than a new pagination type. - [x] Nullable or deleted authors map without crashes or invented identity — `WorkItemAuthor` is `Known(login, displayName)` or `Unavailable`; a null/blank `user` field maps to `Unavailable`, never a fabricated name. - [x] 401, 403, 404, 422, rate-limit, network, parse, and server failures map to typed results — reuses the existing `ApiFailure` sealed class (`AuthenticationExpired`, `PermissionDenied`, `NotFound`, `Unprocessable`, `RateLimited`, `Network`, `InvalidResponse`, `Server`) rather than a parallel failure hierarchy. - [x] Endpoint DTOs remain outside domain and UI packages — all `*Dto` types are `private` nested classes of `data.workitems.GiteaWorkItemsApi`; nothing in `domain.workitems` references Gson or JSON. - [x] Representative JSON and fake-server tests cover lists, reviews, comments, deleted users, pagination, and failures (see Evidence). - [x] No create, edit, merge, close, comment, or notification-write endpoint is exposed — `GiteaWorkItemsApi` only calls `HttpTransport.get`; confirmed by grep (no `put`/`delete`/`postForm` call anywhere in the file). - [ ] Evidence records the reviewed contract-freeze commit for TASK-008 — cannot be closed by this task alone; requires Codex's read-only review of this commit first, per the same freeze convention used for TASK-005→006. ## Required checks ```text ./gradlew assembleDebug ./gradlew testDebugUnitTest ./gradlew lintDebug ``` ## Evidence Implemented on branch `claude/task-010-work-items-contract` from commit `525a0aa` (latest reviewed Claude work at the time; base contracts unchanged from `0929282`). - New files (writable scope only): `domain/workitems/WorkItemModels.kt`, `domain/workitems/WorkItemsRepository.kt`, `data/workitems/GiteaWorkItemsApi.kt`, `data/workitems/GiteaWorkItemsApiTest.kt`. - Design: `IssueSummary`/`IssueDetail` and `PullRequestSummary`/`PullRequestDetail` are separate data classes. `PullRequestState` is derived from Gitea's `merged` boolean plus the issue-level `open`/`closed` state (`merged=true` → `MERGED`; else `state` maps directly), matching Gitea's actual struct shape (there is no single `state` enum value that already means "merged"). `ReviewState` maps five known Gitea values case-insensitively and falls back to `OTHER` for anything unrecognized instead of failing the whole review list closed — reviews are decorative status, not identity- or safety-critical, so this is intentionally looser than the fail-closed convention used elsewhere (documented in a code comment). - Gitea quirk handled: `GET /repos/{owner}/{repo}/issues` can also return pull requests (a PR is internally an issue with a non-null `pull_request` field). `listIssues` drops any entry with that field set, confirmed by a test with a mixed issue+PR response. This is done client-side rather than via a guessed/unverified `type=` query parameter, so correctness does not depend on an assumption about Gitea's exact filter semantics; the accepted tradeoff (documented in a class-level comment) is that a filtered page can return fewer than `pageSize` genuine issues. - PR/issue discussion comments both use `/issues/{number}/comments` (Gitea treats PRs as issues internally for this endpoint), confirmed by a test asserting the exact URL path segments from `listPullRequestComments`. - Reused rather than duplicated: `domain.model.Page` for pagination, `core.network.ApiFailure`/`requireSuccess()` for typed failures, `data.repositories.BearerTokenProvider` for the token-supply contract — no parallel types were introduced for things the app already has. Test coverage (12 cases in `GiteaWorkItemsApiTest`): issue list excludes PRs and maps state/author/comments (1), issue detail maps labels/assignees/body (1), deleted-author comment maps to `Unavailable` without crashing (1), PR state derivation across open/merged/closed (1), PR detail maps base/head/mergeable independently (1), review state known-values-plus-`OTHER`-fallback (1), PR comments reuse the issue-comments URL (1), malformed required field fails closed (1), missing token fails closed as `AuthenticationExpired` (1), HTTP status mapping (1), no-next-page-when-under-page-size (1), plus URL/component-encoding assertions folded into the list/comments tests using an owner/repo containing a space and `+`/`#`. Checks run from the new worktree (`local.properties` created locally, not committed): ```text ./gradlew testDebugUnitTest --tests "com.ubisam.ubgitea.data.workitems.GiteaWorkItemsApiTest" → BUILD SUCCESSFUL, 12/12 passed ./gradlew assembleDebug testDebugUnitTest lintDebug --rerun-tasks → BUILD SUCCESSFUL - 128 unit tests across 24 suites, 0 failures/errors - 0 lint errors git diff --check → clean sensitive-pattern scan of changed files → no matches ``` Known limitations: - Gitea's exact query-parameter behavior for filtering pull requests out of the issue-list endpoint was not confirmed against live documentation (uncertain across versions); the client-side `pull_request`-field filter used instead is correct regardless of that behavior, at the cost of potentially-short pages after filtering. - No physical device or live Gitea instance was used; all coverage is fake-transport/unit-level, consistent with how TASK-004/009 were verified before their own device/contract-freeze review. No open P0-P3 findings from self-review against the acceptance criteria. Requesting Codex read-only review and contract-freeze recording so TASK-008 can begin.