**Top Revisions I Recommend** 1. **Fix auth semantics + a real inconsistency in your test plan** Your ACs require graceful handling for `403`, but the test list says the “403” test returns `401`. That hides the exact behavior you care about and can let permission regressions slip through. ```diff @@ AC-1: GraphQL Client (Unit) - [ ] HTTP 401 → `LoreError::GitLabAuthFailed` + [ ] HTTP 401 → `LoreError::GitLabAuthFailed` + [ ] HTTP 403 → `LoreError::GitLabForbidden` @@ AC-3: Status Fetcher (Integration) - [ ] GraphQL 403 → returns `Ok(HashMap::new())` with warning log + [ ] GraphQL 403 (`GitLabForbidden`) → returns `Ok(HashMap::new())` with warning log @@ TDD Plan (RED) - 13. `test_fetch_statuses_403_graceful` — mock returns 401 → `Ok(HashMap::new())` + 13. `test_fetch_statuses_403_graceful` — mock returns 403 → `Ok(HashMap::new())` ``` 2. **Make enrichment atomic and stale-safe** Current plan can leave stale status values forever when a widget disappears or status becomes null. Make writes transactional and clear status fields for fetched scope before upserts. ```diff @@ AC-6: Enrichment in Orchestrator (Integration) + [ ] Enrichment DB writes are transactional per project (all-or-nothing) + [ ] Status fields are cleared for fetched issue scope before applying new statuses + [ ] If enrichment fails mid-project, prior persisted statuses are unchanged (rollback) @@ File 6: `src/ingestion/orchestrator.rs` - fn enrich_issue_statuses(...) + fn enrich_issue_statuses_txn(...) + // BEGIN TRANSACTION + // clear status columns for fetched issue scope + // apply updates + // COMMIT ``` 3. **Add transient retry/backoff (429/5xx/network)** Right now one transient failure loses status enrichment for that sync. Retrying with bounded backoff gives much better reliability at low cost. ```diff @@ AC-1: GraphQL Client (Unit) + [ ] Retries 429/502/503/504/network errors with bounded exponential backoff + jitter (max 3 attempts) + [ ] Honors `Retry-After` on 429 before retrying @@ AC-6: Enrichment in Orchestrator (Integration) + [ ] Cancellation signal is checked before each retry sleep and between paginated calls ``` 4. **Stop full GraphQL scans when nothing changed** Running full pagination on every sync will dominate runtime on large repos. Trigger enrichment only when issue ingestion reports changes, with a manual override. ```diff @@ AC-6: Enrichment in Orchestrator (Integration) - [ ] Runs on every sync (not gated by `--full`) + [ ] Runs when issue ingestion changed at least one issue in the project + [ ] New override flag `--refresh-status` forces enrichment even with zero issue deltas + [ ] Optional periodic full refresh (e.g. every N syncs) to prevent long-tail drift ``` 5. **Do not expose raw token via `client.token()`** Architecturally cleaner and safer: keep token encapsulated and expose a GraphQL-ready client factory from `GitLabClient`. ```diff @@ File 13: `src/gitlab/client.rs` - pub fn token(&self) -> &str + pub fn graphql_client(&self) -> crate::gitlab::graphql::GraphqlClient @@ File 6: `src/ingestion/orchestrator.rs` - let graphql_client = GraphqlClient::new(&config.gitlab.base_url, client.token()); + let graphql_client = client.graphql_client(); ``` 6. **Add indexes for new status filters** `--status` on large tables will otherwise full-scan `issues`. Add compound indexes aligned with project-scoped list queries. ```diff @@ AC-4: Migration 021 (Unit) + [ ] Adds index `idx_issues_project_status_name(project_id, status_name)` + [ ] Adds index `idx_issues_project_status_category(project_id, status_category)` @@ File 14: `migrations/021_work_item_status.sql` ALTER TABLE issues ADD COLUMN status_name TEXT; ALTER TABLE issues ADD COLUMN status_category TEXT; ALTER TABLE issues ADD COLUMN status_color TEXT; ALTER TABLE issues ADD COLUMN status_icon_name TEXT; +CREATE INDEX IF NOT EXISTS idx_issues_project_status_name + ON issues(project_id, status_name); +CREATE INDEX IF NOT EXISTS idx_issues_project_status_category + ON issues(project_id, status_category); ``` 7. **Improve filter UX: add category filter + case-insensitive status** Case-sensitive exact name matches are brittle with custom lifecycle names. Category filter is stable and useful for automation. ```diff @@ AC-9: List Issues Filter (E2E) - [ ] Filter is case-sensitive (matches GitLab's exact status name) + [ ] `--status` uses case-insensitive exact match by default (`COLLATE NOCASE`) + [ ] New filter `--status-category` supports `triage|to_do|in_progress|done|canceled` + [ ] `--status-exact` enables strict case-sensitive behavior when needed ``` 8. **Add capability probe/cache to avoid pointless calls** Free tier / old GitLab versions will never return status widget. Cache that capability per project (with TTL) to reduce noise and wasted requests. ```diff @@ GitLab API Constraints +### Capability Probe +On first sync per project, detect status-widget support and cache result for 24h. +If unsupported, skip enrichment silently (debug log) until TTL expiry. @@ AC-3: Status Fetcher (Integration) + [ ] Unsupported capability state bypasses GraphQL fetch and warning spam ``` 9. **Use a nested robot `status` object instead of 4 top-level fields** This is cleaner schema design and scales better as status metadata grows (IDs, lifecycle, timestamps, etc.). ```diff @@ AC-7: Show Issue Display (Robot) - [ ] JSON includes `status_name`, `status_category`, `status_color`, `status_icon_name` fields - [ ] Fields are `null` (not absent) when status not available + [ ] JSON includes `status` object: + `{ "name": "...", "category": "...", "color": "...", "icon_name": "..." }` or `null` @@ AC-8: List Issues Display (Robot) - [ ] JSON includes `status_name`, `status_category` fields on each issue + [ ] JSON includes `status` object (or `null`) on each issue ``` 10. **Add one compelling feature: status analytics, not just status display** Right now this is mostly a transport/display enhancement. Make it genuinely useful with “stale in-progress” detection and age-in-status filters. ```diff @@ Acceptance Criteria +### AC-11: Status Aging & Triage Value (E2E) +- [ ] `lore list issues --status-category in_progress --stale-days 14` filters to stale work +- [ ] Human table shows `Status Age` (days) when status exists +- [ ] Robot output includes `status_age_days` (nullable integer) ``` 11. **Harden test plan around failure modes you’ll actually hit** The current tests are good, but miss rollback/staleness/retry behavior that drives real reliability. ```diff @@ TDD Plan (RED) additions +21. `test_enrich_clears_removed_status` +22. `test_enrich_transaction_rolls_back_on_failure` +23. `test_graphql_retry_429_then_success` +24. `test_graphql_retry_503_then_success` +25. `test_cancel_during_backoff_aborts_cleanly` +26. `test_status_filter_query_uses_project_status_index` (EXPLAIN smoke test) ``` If you want, I can produce a fully revised v3 plan document end-to-end (frontmatter + reordered ACs + updated file list + updated TDD matrix) so it is ready to implement directly.