{"ns":"review","title":"Diffline","version":"1.8","summary":"This product is a pull-request review tool for the halden/billing-service repository. Reviewers sign in, find a pull request in a filterable list, read its commits and unified or split diffs, discuss individual lines in threads, submit reviews and merge once every merge requirement is met.","problem":"Reviewers need to find the right change, read it line by line, track which conversations are still open and know exactly what blocks a merge, without merging something that has failing checks, missing approvals or unresolved discussion.","personas":[{"name":"Reviewer","need":"Read diffs, comment on lines, resolve threads and approve or request changes."},{"name":"Author","need":"See what blocks the pull request and merge it once everything is satisfied."},{"name":"Phone or tablet user","need":"Triage and review from a small screen without sideways scrolling."}],"goals":["The merge gate states every unmet requirement and never allows a merge while one is unmet.","Counts (tabs, files, additions, deletions, resolved conversations) always equal what is on screen.","Diffs read correctly: each deleted line sits beside the line that replaces it in split view."],"features":[{"id":"AUTH","title":"Sign-in and sign-out","summary":"Session access to the pull request list.","reqs":[{"id":"FR-AUTH-01","weight":"Must","text":"A signed-in user lands on the \"Pull requests\" page for halden/billing-service on the Open tab.","accept":["The sign-in page is headed \"Sign in to\" followed by the product name, with Username, Password and a \"Sign in\" button","After sign-in the tab strip reads Open 8, Merged 3, Closed 2, All 13"]},{"id":"FR-AUTH-02","weight":"Should","text":"Failed sign-ins are rejected with a specific message and the form stays.","accept":["Empty username shows \"Enter your username\"","Wrong password shows \"Invalid credentials\" and no list is shown"]},{"id":"FR-AUTH-03","weight":"Could","text":"Sign out ends the session and returns to the sign-in page; Back does not reveal any pull request.","accept":["Sign out is in the left rail, or in the bottom bar on a phone"]}]},{"id":"LST","title":"Pull request list","summary":"Tabs, filters, search and sort over the repository’s pull requests.","reqs":[{"id":"FR-LST-01","weight":"Must","text":"The Open tab lists the 8 open pull requests newest first with correct title, labels, author, date, review and check chips, comment count and +/- totals.","accept":["Order: #421, #420, #418, #417, #415, #414, #412, #409","#421 is \"WIP: audit log CSV export\" with a Draft pill, label feature, \"#421 opened Oct 18, 2026 by Ines Carvalho\", \"1 pending\", 0 comments, \"+5 -0\"","#412 \"Add idempotency keys to POST /charges\": labels feature and security, \"4 passed\", 4 comments, \"+75 -1\"","#409 \"Fix rounding in partial and prorated refunds\": label bug, \"1 failing\", 3 comments, \"+12 -4\"","#415 \"Cache currency rates for 5 minutes\": \"3 passed\", 1 comment, \"+7 -0\""]},{"id":"FR-LST-02","weight":"Should","text":"Merged, Closed and All tabs list only pull requests in that state, each with a state pill.","accept":["Merged (3): #411, #405, #403","Closed (2): #407 and #398","All shows 13, first #421, last #398"]},{"id":"FR-LST-03","weight":"Should","text":"Label and author filters narrow the list and the heading count follows.","accept":["Label \"security\": 2 pull requests, #420 and #412","Author \"Bao Nguyen\": 2 pull requests, #415 and #409"]},{"id":"FR-LST-04","weight":"Should","text":"Search matches title text; an empty result shows a way back.","accept":["\"cache\" returns only #415","No match shows \"No pull requests match these filters\", \"0 pull requests\" and a \"Clear filters\" button that restores the 8 open pull requests"]},{"id":"FR-LST-05","weight":"Could","text":"\"Awaiting my review\" lists open pull requests where the signed-in user is a pending reviewer; the sort control can reverse the order.","accept":["\"Awaiting my review\": #414 and #412","\"Oldest\": #409, #412, #414, #415, #417, #418, #420, #421"]}]},{"id":"PRD","title":"Pull request page","summary":"Conversation, Commits and Files changed tabs with a sidebar of reviewers, checks and conversations.","reqs":[{"id":"FR-PRD-01","weight":"Should","text":"The page header and sidebar show the seeded author, branches, reviewers, checks and conversation summary.","accept":["#412: Open pill, \"Tomasz Kowalski wants to merge tomasz/idempotency-keys into main\", labels feature and security","Tabs: Conversation 4, Commits 4, Files changed 4 with \"+75 -1\"","Reviewers: Amira Hassan \"approved\"; Ines Carvalho and Bao Nguyen \"review requested\"","Checks: build, unit-tests and lint \"Passed · required\"; e2e-smoke \"Passed\"","Conversations: \"1 of 2 resolved\""]},{"id":"FR-PRD-02","weight":"Could","text":"The Commits tab lists the pull request’s commits oldest first with hash, message and author.","accept":["#412: a13f7c2 \"Add idempotency helpers\", 5be09d1 \"Wire idempotency into POST /charges\", f20c6ae \"Add idempotency_keys migration\", 62d8b07 \"Cover replay and conflict in tests\", all by Tomasz Kowalski"]},{"id":"FR-PRD-03","weight":"Could","text":"A merged pull request shows who merged it and how, and offers no merge or close action.","accept":["#411 shows \"Merged commit 4be71c9 into main by ahassan using squash and merge on Oct 12, 2026.\""]}]},{"id":"DIF","title":"Diff reading","summary":"Files changed tab with file tree, unified and split layouts and viewed marks.","reqs":[{"id":"FR-DIF-01","weight":"Must","text":"Files changed lists every changed file with its own +/- counts and starts with nothing viewed.","accept":["#412: \"0 of 4 files viewed\", Unified selected","idempotency.ts +37 -0; charges.ts +15 -1; 0042_idempotency_keys.sql +9 -0; charges.test.ts +14 -0","The first file section reads src/payments/idempotency.ts, \"added\", \"+37 -0\" with an unchecked Viewed box"]},{"id":"FR-DIF-02","weight":"Must","text":"Addition and deletion totals count every added and deleted line, in the list row, the tab and each file.","accept":["#412 shows \"+75 -1\" in the list and tab, and charges.ts \"+15 -1\"","src/payments/refunds.ts on #409 reads \"+3 -3\""]},{"id":"FR-DIF-03","weight":"Must","text":"In split view each deleted line sits on the same row as the added line that replaces it, and the chosen layout persists across reloads.","accept":["refunds.ts: left row 9 \"const unused = (daysInPeriod - daysUsed) / daysInPeriod;\" beside right row 9 \"const unusedDays = daysInPeriod - daysUsed;\"","Left row 10 \"return Math.round(total * unused);\" beside right row 10 \"return Math.floor((total * unusedDays) / daysInPeriod);\"","Unified view lists the deleted old lines 9 and 10, then the added new lines 9 and 10"]},{"id":"FR-DIF-04","weight":"Should","text":"Ticking Viewed folds the file, advances the \"N of M files viewed\" counter and is remembered after reload.","accept":["Folded file reads \"Marked as viewed — the diff is hidden.\" with a \"Show diff\" button; Viewed stays ticked when the diff is shown again","After viewing two files and reloading, \"2 of 4 files viewed\" is kept"]}]},{"id":"THR","title":"Inline conversations","summary":"Comment on a line, then resolve and unresolve the thread.","reqs":[{"id":"FR-THR-01","weight":"Must","text":"Clicking a line number opens a composer; adding a comment creates an unresolved conversation and updates the counts.","accept":["Composer reads \"Comment on line 2\" with Cancel and \"Add comment\"","An empty comment shows \"Comment cannot be empty\" and the composer stays","On success a toast \"Comment added\" appears; the Conversation tab reads 5 and the sidebar reads \"1 of 3 resolved\" listing idempotency.ts:2 as Unresolved"]},{"id":"FR-THR-02","weight":"Must","text":"Resolving and unresolving a thread updates the sidebar \"N of M resolved\" summary and the merge gate line in both directions.","accept":["Resolving charges.ts line 34 on #412 shows toast \"Conversation resolved\", summary \"2 of 2 resolved\" and gate line \"All conversations resolved\"","Unresolving idempotency.ts line 4 shows toast \"Conversation reopened\"; the resolved count drops by one (from \"1 of 2 resolved\" to \"0 of 2 resolved\" with gate \"2 unresolved conversations\")"]},{"id":"FR-THR-03","weight":"Could","text":"A resolved thread is collapsed to a one-line summary that expands on demand and offers \"Unresolve conversation\".","accept":["idempotency.ts line 4 shows a Resolved pill and \"Show resolved conversation (2 comments)\"","Expanded it shows Amira Hassan \"Is 24 hours enough for clients that retry from a queue?\" and Tomasz Kowalski \"Matches the payment provider window. I can make it configurable in a follow-up.\""]}]},{"id":"REV","title":"Reviews","summary":"Approve or request changes from the Review changes popover.","reqs":[{"id":"FR-REV-01","weight":"Must","text":"Approving adds the reviewer as \"approved\", increments the required-approvals count and refreshes the merge gate.","accept":["On #412 approving moves the Merge box from \"1 of 2 required approvals\" to \"2 of 2 required approvals\" and lists Ines Carvalho as \"approved\""]},{"id":"FR-REV-02","weight":"Should","text":"Requesting changes requires a summary; without one nothing changes.","accept":["\"Add a comment explaining the requested changes\" appears and the popover stays open","Reviewer states are unchanged afterwards"]},{"id":"FR-REV-03","weight":"Must","text":"The author of a pull request cannot approve it.","accept":["The popover shows \"You can't approve your own pull request\"","The reviewer list and the approval count do not change"]}]},{"id":"MRG","title":"Merge gate and merge","summary":"A pull request merges only when every requirement is met.","reqs":[{"id":"FR-MRG-01","weight":"Must","text":"The Merge box lists five requirements: ready for review, required checks passed, 2 required approvals, no changes requested, all conversations resolved. The button is \"Merge pull request\" only when all are met, otherwise a disabled \"Merge blocked\".","accept":["#412 initially: \"1 of 2 required approvals\" and \"1 unresolved conversation\" not met, others met, button disabled"]},{"id":"FR-MRG-02","weight":"Must","text":"A failing required check blocks the merge regardless of approvals.","accept":["With unit-tests \"Failed · required\", the requirement reads \"1 required check failing\" as not met and the button is a disabled \"Merge blocked\""]},{"id":"FR-MRG-03","weight":"Should","text":"Every unmet requirement is stated, including draft, pending check and review state.","accept":["#421: \"This pull request is still a draft\", \"1 required check pending\", \"0 of 2 required approvals\" not met; \"No changes requested\" and \"All conversations resolved\" met","#415: 3 checks passed (met); \"0 of 2 required approvals\", \"Changes requested by smoreau\" and \"1 unresolved conversation\" not met; Sophie Moreau is \"requested changes\""]},{"id":"FR-MRG-04","weight":"Must","text":"Merging asks for confirmation, then marks the pull request Merged with the chosen method and moves it between tab counts.","accept":["Methods include \"Create a merge commit\" (default) and \"Squash and merge\"; the squash title reads \"Add idempotency keys to POST /charges (#412)\" with counter \"44/72\"","Dialog \"Merge pull request #412?\" with Confirm merge; toast \"Pull request merged\"","Card reads \"Merged commit <hash> into main by you using squash and merge on Oct 19, 2026.\"","After merging #412 the tabs read Open 7, Merged 4, Closed 2, All 13"]},{"id":"FR-MRG-05","weight":"Must","text":"A pull request with no blockers merges with the default method and title.","accept":["#418: all five requirements met, \"2 of 2 required approvals\", method \"Create a merge commit\", Commit title \"Docs: describe the webhook retry policy (#418)\"","After merging, the card reads \"...using create a merge commit on Oct 19, 2026.\" and the merge button is gone"]},{"id":"FR-MRG-06","weight":"Should","text":"A pull request can be closed (after confirmation) and reopened; a draft must be marked ready before it can merge.","accept":["Dialog \"Close pull request #418?\" with Cancel; toast \"Pull request closed\"; pill Closed; merge box \"This pull request is closed. Reopen it to continue the review.\"; timeline \"closed this pull request\"","Reopen shows toast \"Pull request reopened\", pill Open and an enabled \"Merge pull request\"","\"Ready for review\" replaces the Draft pill with Open and adds timeline \"marked this pull request as ready for review\""]}]},{"id":"KEY","title":"Keyboard shortcuts","summary":"Review the files without a mouse.","reqs":[{"id":"FR-KEY-01","weight":"Should","text":"\"?\" opens a \"Keyboard shortcuts\" dialog listing j / k, v, s, /, ?, Esc and Ctrl+Enter; Esc closes it and \"/\" focuses search."},{"id":"FR-KEY-02","weight":"Should","text":"On Files changed, j and k move the current file, v marks it viewed and s toggles split view.","accept":["j makes charges.ts current; v gives \"1 of 4 files viewed\" and folds it; s switches to Split; k returns to idempotency.ts"]}]}],"nfr":[{"id":"NFR-01","weight":"Should","text":"On a 390 px phone the list stacks, navigation moves to a bottom bar (Pull requests, Requests, Shortcuts, Sign out) and no page scrolls sideways.","accept":["16 px side margins","On the pull request page the sidebar sections (Reviewers, Checks, Conversations, Labels) follow the conversation and the merge form is one full-width column"]},{"id":"NFR-02","weight":"Could","text":"On a 768 px tablet the file list sits above the diffs and wide code scrolls inside its file card, not the page."}],"glossary":[{"term":"Merge gate","meaning":"The set of five requirements that must all be met before a pull request can merge."},{"term":"Required check","meaning":"A CI check marked \"required\"; it must pass for the gate to open."},{"term":"Viewed","meaning":"A per-file mark that folds the diff and counts toward \"N of M files viewed\"."}],"outOfScope":["Real source control","Notifications and email","Branch protection settings"]}