fix(board): page the board list #147

Merged
Claude-Run merged 1 commit from fix/YT-91-board-list-pagination into main 2026-08-06 12:56:15 +02:00
Member

yt board list called client.get_json("/api/agiles?fields=id,name") directly instead of going through api::list_boards, so the request carried no $top / $skip and never asked for a second page: the command returned whatever the server's default page held and silently dropped the rest. The MCP already read through api::list_boards, so the two front ends could report different board lists for the same instance. The command now calls list_boards; the --json output and the table rendering are unchanged.

The invariant is "every collection read pages until a short page (never silently truncates)", and fetch_all_pages is its single implementation. Sweeping every get_json call whose path is a collection endpoint across crates/yt-cli and crates/youtrack-client found five more violations, all fixed here.

Site Endpoint Class Reason
yt-cli/src/commands/board.rs:44 /api/agiles violating -> fixed now api::list_boards
api.rs:41 fetch_all_pages any compliant the pager itself
api.rs:113 get_board /api/agiles/<id> N/A single-entity read; its projects / sprints collections already read through their own paged subresources
api.rs:207 fetch_all_sprints /api/agiles/<id>/sprints compliant own $top / $skip loop
api.rs:235 list_issue_activities /api/issues/<id>/activities compliant own $top / $skip loop, bounded by the caller's limit
api.rs:283 list_issue_sprints /api/issues/<id>/sprints violating -> fixed an issue's sprint membership is admin-shaped, not structurally bounded
api.rs:297 inspect_issue (issue) /api/issues/<id> N/A single-entity read; its comments and work items already page
api.rs:330 inspect_issue (links) /api/issues/<id>/links violating -> fixed one slot per link type and direction, and link types are admin-created
api.rs:366 fetch_issues (issues) /api/issues compliant fetch_all_pages
api.rs:366 fetch_issues (sprints) /api/issues/<id>/sprints violating -> fixed same shape as list_issue_sprints
api.rs:647 discover_issue_field_type /api/admin/projects/<id>/customFields violating -> fixed a project's field count is admin-shaped; the canonical reader project_custom_fields_by_id already pages the same path
api.rs:991 fetch_issues_page (issues) /api/issues N/A deliberate single page: $top / $skip are the caller's (yt list --top / --skip), and fetch_all_issues is the loop over it
api.rs:991 fetch_issues_page (sprints) /api/issues/<id>/sprints violating -> fixed same shape as list_issue_sprints
api.rs:1053 get_user_me /api/users/me N/A single-entity read
api.rs:1062 list_users /api/users N/A deliberate bound: $top is always sent and is the CLI's --top (default 50)
api.rs:1076 get_user /api/users/<login> N/A single-entity read
api.rs:1261 get_server_config /api/config N/A single-entity read
api.rs:1268 list_project_states /api/admin/projects/<id>/customFields violating -> fixed same shape as discover_issue_field_type
api.rs:1309 get_issue_custom_fields /api/issues/<id> N/A single-entity read
api.rs:1373 list_project_vcs_processors /api/admin/projects/<id> N/A single-entity read; processors is a nested attribute of that entity, not a collection endpoint, and is bounded by the project's configured VCS integrations
api.rs:1728 project_team_by_id /api/admin/projects/<id>/team N/A single-entity read; its three collections read through their own paged subresources (YT-83)
api.rs:3065 list_articles /api/articles N/A deliberate bound: this arm runs only when the caller passed --top, and the None arm calls fetch_all_pages
api.rs:3074 get_article /api/articles/<id> N/A single-entity read
api.rs:3215 build_timeline /api/issues/<id> N/A single-entity read, once per already-resolved id
api.rs:3369 fetch_link_slots /api/issues/<id>/links violating -> fixed same shape as the inspect_issue links read
api.rs:3476 fetch_issue_identity /api/issues/<id> N/A single-entity read

crates/youtrack-mcp has no get_json call of its own; it reads through youtrack_client::api throughout.

Regression cover: commands::board::tests::list_pages_until_a_short_page mounts a full page plus a short page on /api/agiles and asserts board list requests both and surfaces the union (101 boards), and api::tests gains list_issue_sprints_paginates, fetch_link_slots_paginates, and list_project_states_paginates. crates/yt-cli/tests/no_raw_collection_reads.rs fails the build if any CLI source calls get_json directly again, so the removal is enforced rather than asserted.

#YT-91

`yt board list` called `client.get_json("/api/agiles?fields=id,name")` directly instead of going through `api::list_boards`, so the request carried no `$top` / `$skip` and never asked for a second page: the command returned whatever the server's default page held and silently dropped the rest. The MCP already read through `api::list_boards`, so the two front ends could report different board lists for the same instance. The command now calls `list_boards`; the `--json` output and the table rendering are unchanged. The invariant is "every collection read pages until a short page (never silently truncates)", and `fetch_all_pages` is its single implementation. Sweeping every `get_json` call whose path is a collection endpoint across `crates/yt-cli` and `crates/youtrack-client` found five more violations, all fixed here. | Site | Endpoint | Class | Reason | | --- | --- | --- | --- | | `yt-cli/src/commands/board.rs:44` | `/api/agiles` | violating -> fixed | now `api::list_boards` | | `api.rs:41` `fetch_all_pages` | any | compliant | the pager itself | | `api.rs:113` `get_board` | `/api/agiles/<id>` | N/A | single-entity read; its `projects` / `sprints` collections already read through their own paged subresources | | `api.rs:207` `fetch_all_sprints` | `/api/agiles/<id>/sprints` | compliant | own `$top` / `$skip` loop | | `api.rs:235` `list_issue_activities` | `/api/issues/<id>/activities` | compliant | own `$top` / `$skip` loop, bounded by the caller's `limit` | | `api.rs:283` `list_issue_sprints` | `/api/issues/<id>/sprints` | violating -> fixed | an issue's sprint membership is admin-shaped, not structurally bounded | | `api.rs:297` `inspect_issue` (issue) | `/api/issues/<id>` | N/A | single-entity read; its comments and work items already page | | `api.rs:330` `inspect_issue` (links) | `/api/issues/<id>/links` | violating -> fixed | one slot per link type and direction, and link types are admin-created | | `api.rs:366` `fetch_issues` (issues) | `/api/issues` | compliant | `fetch_all_pages` | | `api.rs:366` `fetch_issues` (sprints) | `/api/issues/<id>/sprints` | violating -> fixed | same shape as `list_issue_sprints` | | `api.rs:647` `discover_issue_field_type` | `/api/admin/projects/<id>/customFields` | violating -> fixed | a project's field count is admin-shaped; the canonical reader `project_custom_fields_by_id` already pages the same path | | `api.rs:991` `fetch_issues_page` (issues) | `/api/issues` | N/A | deliberate single page: `$top` / `$skip` are the caller's (`yt list --top` / `--skip`), and `fetch_all_issues` is the loop over it | | `api.rs:991` `fetch_issues_page` (sprints) | `/api/issues/<id>/sprints` | violating -> fixed | same shape as `list_issue_sprints` | | `api.rs:1053` `get_user_me` | `/api/users/me` | N/A | single-entity read | | `api.rs:1062` `list_users` | `/api/users` | N/A | deliberate bound: `$top` is always sent and is the CLI's `--top` (default 50) | | `api.rs:1076` `get_user` | `/api/users/<login>` | N/A | single-entity read | | `api.rs:1261` `get_server_config` | `/api/config` | N/A | single-entity read | | `api.rs:1268` `list_project_states` | `/api/admin/projects/<id>/customFields` | violating -> fixed | same shape as `discover_issue_field_type` | | `api.rs:1309` `get_issue_custom_fields` | `/api/issues/<id>` | N/A | single-entity read | | `api.rs:1373` `list_project_vcs_processors` | `/api/admin/projects/<id>` | N/A | single-entity read; `processors` is a nested attribute of that entity, not a collection endpoint, and is bounded by the project's configured VCS integrations | | `api.rs:1728` `project_team_by_id` | `/api/admin/projects/<id>/team` | N/A | single-entity read; its three collections read through their own paged subresources (YT-83) | | `api.rs:3065` `list_articles` | `/api/articles` | N/A | deliberate bound: this arm runs only when the caller passed `--top`, and the `None` arm calls `fetch_all_pages` | | `api.rs:3074` `get_article` | `/api/articles/<id>` | N/A | single-entity read | | `api.rs:3215` `build_timeline` | `/api/issues/<id>` | N/A | single-entity read, once per already-resolved id | | `api.rs:3369` `fetch_link_slots` | `/api/issues/<id>/links` | violating -> fixed | same shape as the `inspect_issue` links read | | `api.rs:3476` `fetch_issue_identity` | `/api/issues/<id>` | N/A | single-entity read | `crates/youtrack-mcp` has no `get_json` call of its own; it reads through `youtrack_client::api` throughout. Regression cover: `commands::board::tests::list_pages_until_a_short_page` mounts a full page plus a short page on `/api/agiles` and asserts `board list` requests both and surfaces the union (101 boards), and `api::tests` gains `list_issue_sprints_paginates`, `fetch_link_slots_paginates`, and `list_project_states_paginates`. `crates/yt-cli/tests/no_raw_collection_reads.rs` fails the build if any CLI source calls `get_json` directly again, so the removal is enforced rather than asserted. #YT-91
fix(board): page the board list
All checks were successful
Check / fmt + clippy + build + tests (pull_request) Successful in 1m4s
Create release / Create release from merged PR (pull_request) Has been skipped
93a5db9826
`yt board list` called `client.get_json("/api/agiles?fields=id,name")` directly instead of going through `api::list_boards`, so the request carried no `$top` / `$skip` and never asked for a second page: the command returned whatever the server's default page held and silently dropped the rest. The MCP already read through `api::list_boards`, so the two front ends could report different board lists for the same instance. The command now calls `list_boards`; the `--json` output and the table rendering are unchanged.

The invariant is "every collection read pages until a short page (never silently truncates)", and `fetch_all_pages` is its single implementation. Sweeping every `get_json` call whose path is a collection endpoint across `crates/yt-cli` and `crates/youtrack-client` found five more violations, all fixed here.

| Site | Endpoint | Class | Reason |
| --- | --- | --- | --- |
| `yt-cli/src/commands/board.rs:44` | `/api/agiles` | violating -> fixed | now `api::list_boards` |
| `api.rs:41` `fetch_all_pages` | any | compliant | the pager itself |
| `api.rs:113` `get_board` | `/api/agiles/<id>` | N/A | single-entity read; its `projects` / `sprints` collections already read through their own paged subresources |
| `api.rs:207` `fetch_all_sprints` | `/api/agiles/<id>/sprints` | compliant | own `$top` / `$skip` loop |
| `api.rs:235` `list_issue_activities` | `/api/issues/<id>/activities` | compliant | own `$top` / `$skip` loop, bounded by the caller's `limit` |
| `api.rs:283` `list_issue_sprints` | `/api/issues/<id>/sprints` | violating -> fixed | an issue's sprint membership is admin-shaped, not structurally bounded |
| `api.rs:297` `inspect_issue` (issue) | `/api/issues/<id>` | N/A | single-entity read; its comments and work items already page |
| `api.rs:330` `inspect_issue` (links) | `/api/issues/<id>/links` | violating -> fixed | one slot per link type and direction, and link types are admin-created |
| `api.rs:366` `fetch_issues` (issues) | `/api/issues` | compliant | `fetch_all_pages` |
| `api.rs:366` `fetch_issues` (sprints) | `/api/issues/<id>/sprints` | violating -> fixed | same shape as `list_issue_sprints` |
| `api.rs:647` `discover_issue_field_type` | `/api/admin/projects/<id>/customFields` | violating -> fixed | a project's field count is admin-shaped; the canonical reader `project_custom_fields_by_id` already pages the same path |
| `api.rs:991` `fetch_issues_page` (issues) | `/api/issues` | N/A | deliberate single page: `$top` / `$skip` are the caller's (`yt list --top` / `--skip`), and `fetch_all_issues` is the loop over it |
| `api.rs:991` `fetch_issues_page` (sprints) | `/api/issues/<id>/sprints` | violating -> fixed | same shape as `list_issue_sprints` |
| `api.rs:1053` `get_user_me` | `/api/users/me` | N/A | single-entity read |
| `api.rs:1062` `list_users` | `/api/users` | N/A | deliberate bound: `$top` is always sent and is the CLI's `--top` (default 50) |
| `api.rs:1076` `get_user` | `/api/users/<login>` | N/A | single-entity read |
| `api.rs:1261` `get_server_config` | `/api/config` | N/A | single-entity read |
| `api.rs:1268` `list_project_states` | `/api/admin/projects/<id>/customFields` | violating -> fixed | same shape as `discover_issue_field_type` |
| `api.rs:1309` `get_issue_custom_fields` | `/api/issues/<id>` | N/A | single-entity read |
| `api.rs:1373` `list_project_vcs_processors` | `/api/admin/projects/<id>` | N/A | single-entity read; `processors` is a nested attribute of that entity, not a collection endpoint, and is bounded by the project's configured VCS integrations |
| `api.rs:1728` `project_team_by_id` | `/api/admin/projects/<id>/team` | N/A | single-entity read; its three collections read through their own paged subresources (YT-83) |
| `api.rs:3065` `list_articles` | `/api/articles` | N/A | deliberate bound: this arm runs only when the caller passed `--top`, and the `None` arm calls `fetch_all_pages` |
| `api.rs:3074` `get_article` | `/api/articles/<id>` | N/A | single-entity read |
| `api.rs:3215` `build_timeline` | `/api/issues/<id>` | N/A | single-entity read, once per already-resolved id |
| `api.rs:3369` `fetch_link_slots` | `/api/issues/<id>/links` | violating -> fixed | same shape as the `inspect_issue` links read |
| `api.rs:3476` `fetch_issue_identity` | `/api/issues/<id>` | N/A | single-entity read |

`crates/youtrack-mcp` has no `get_json` call of its own; it reads through `youtrack_client::api` throughout.

Regression cover: `commands::board::tests::list_pages_until_a_short_page` mounts a full page plus a short page on `/api/agiles` and asserts `board list` requests both and surfaces the union (101 boards), and `api::tests` gains `list_issue_sprints_paginates`, `fetch_link_slots_paginates`, and `list_project_states_paginates`. `crates/yt-cli/tests/no_raw_collection_reads.rs` fails the build if any CLI source calls `get_json` directly again, so the removal is enforced rather than asserted.

#YT-91
Claude-Run deleted branch fix/YT-91-board-list-pagination 2026-08-06 12:56:15 +02:00
Sign in to join this conversation.
No reviewers
No labels
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
pandoras-box/youtrack-cli!147
No description provided.