Skip to content

User Stories Per-User View — V5 Build-Out Playbook (T10)

# User Stories Per-User View — V5 Build-Out Playbook (T10)
> **File:** `docs/playbooks/v4-user-stories-playbook.md`
> **Base branch:** `dev` (integration branch — `main` stays protected/production; PRs target `dev`)
> **Branch naming:** `feature/<ado-task-id>-<slug>-v4-pr<n>`
> **Plan date:** 2026-07-31
> **Owner:** TBD
> **Priority:** Independent of `v2-team-velocity-playbook.md`'s T6 (on hold, pending Management
> sign-off) and T3 (on hold, pending IT secrets) — this playbook reuses T6's already-merged
> `ado_boards_connector.py` discovery code (boards/done-states/WIQL patterns) but does not modify
> or depend on T6's velocity computation being finalized.
> **Source:** `work-items/112621-user-stories-per-user-view.md`; conversation 2026-07-31 (Option
> A locked — flat list with real ADO state labels; superseded same-day by Option B — see
> "Kanban vs. flat-list — DECIDED" below).
---
## How to Use This Document
**T10** adds a "User Stories" tab to the contributor detail page: every Azure Boards work item
(User Story / Product Backlog Item / Bug) assigned to that person, grouped by project. It reuses
`ado_boards_connector.py`'s existing WIQL + batch-fetch pattern (built for T6's Team Velocity
KPI) but removes the done-state filter and adds assignee/title fields — the velocity path
(`query_closed_work_items`, `WORK_ITEM_FIELDS`, `boards_work_items` table) is untouched; this is
a fully additive second query path against the same ADO endpoints.
### Kanban vs. flat-list — DECIDED (2026-07-31, revised same day)
| Option | What it looks like | Cost |
|---|---|---|
| A — Flat list, real state labels | Sortable list per project, each item shows its actual `System.State` as a colored pill | Reuses fields already fetched for T6; no new board-layout discovery |
| B — Per-project Kanban columns | Visually matches each project's real ADO board columns | Requires new full column/`stateMappings` discovery (not just done-states) for every project, plus variable-column-count UI |
**Original decision: Option A**, shipped first as PR-10. **Revised same-day to Option B** after
the user explicitly requested a board/column view "similar to what we have in ADO boards."
Implemented as a **client-side-only** change — no new connector/API/schema work was needed:
`UserStoriesPanel.vue`'s `columnsForProject()` derives columns by grouping each project's
already-fetched `stories` array by its real `state` value (no fixed/hardcoded column set — a
project with custom states still gets one column per distinct state actually present, ordered
by a best-effort priority list covering common Agile/Scrum/CMMI state names, with any
unrecognized state appended alphabetically). This preserves Acceptance Criteria #1
("real state labels, no hardcoded columns") even under the board layout — see §"Verification
Samples" for confirmation this did not require new backend discovery work
(`get_board_done_states()`-style per-project column mapping was NOT needed/added).
---
## 1. Executive Summary
| # | Item | Gap | Depends on |
|---|---|---|---|
| T10 | No per-user Boards work item view | `ado_boards_connector.py` only fetches closed items for velocity; no assignee/title fields fetched; no per-user grouped API or UI exists | None (reuses merged T6 connector code, no dependency on its computation) |
### PR Strategy — 1 PR
- **PR-10** carries T10 end-to-end: connector extension, parallel fetch, storage, API, UI.
Branches from `dev`.
### Merge Sequencing
Single PR — merges directly to `dev` once approved.
### Model Guidance
| PR | Task | Recommended Model | Effort | Reason |
|---|---|---|---|---|
| PR-10 | T10 (User Stories view) | Claude Sonnet 4.5 | medium | New WIQL query variant + parallel fetch refactor + new table/endpoint/Vue component; moderate breadth, low algorithmic complexity |
---
## 2. PR Plan
| PR | Branch | Items | Branches from | Est. |
|---|---|---|---|---|
| PR-10 | `feature/112621-user-stories-per-user-view-v4-pr10` | T10 | `dev` | 1 session |
ADO task IDs: T10=112621 (see `work-items/112621-user-stories-per-user-view.md`).
---
## 3. Files Touched
| File / path | PR |
|---|---|
| `connectors/ado_boards_connector.py` | PR-10 |
| `connectors/ado_connector.py` (parallel top-level fetch in `main()`) | PR-10 |
| `store/schema.sql` (new `work_items` table) | PR-10 |
| `store/db.py` (`ingest_work_items`, `query_user_stories`) | PR-10 |
| `api/server.py` (`/api/user-stories-detail`) | PR-10 |
| `public/src/views/ContributorView.vue` (4th tab wiring) | PR-10 |
| `public/src/components/DetailToolTabs.vue` (tabs list, likely no change needed) | PR-10 |
| `public/src/components/UserStoriesPanel.vue` (new) | PR-10 |
| `public/src/api.js` (`fetchUserStoriesDetail`) | PR-10 |
| `tests/test_ado_boards_connector.py` | PR-10 |
| `tests/test_store_db.py` | PR-10 |
| `tests/test_api_server.py` | PR-10 |
---
## 4. Hard Rules
(Inherits Hard Rules from `docs/playbooks/v1-dev-productivity-dashboard-playbook.md` §4 —
never fabricate data, aggregate/normalize at ingest not query time where applicable, zero-data
is a valid state not an error, and **Hard Rule 12: no automatic git actions** (stage/commit/push
only on explicit user approval, no PR opened). The following are additive, specific to this
playbook.)
1. Never modify `query_closed_work_items`, `WORK_ITEM_FIELDS`, `boards_work_items`, or
`query_team_velocity` — the velocity path (T6) is a separate, already-shipped concern that
this playbook must not regress.
2. Story points and assignee fields stay nullable — never coerced to `0`/`""`; unassigned items
are a valid state (render as "Unassigned", not hidden).
3. Real `System.State` strings are displayed as-is (title-cased for display only, never remapped
to a hardcoded vocabulary) — Option A's whole premise is no fixed column assumption.
4. Parallel fetch changes must preserve identical output shape/ordering-independence to the
sequential version — concurrency is a performance change only, not a behavior change.
---
## 5. Out of Scope
- **Option B (real per-project Kanban columns)** — revisit only if management/users explicitly
ask for visual board parity; would require generalizing `get_board_done_states()` into a
full column/state map per project, not just the done-state subset.
- Sprint/iteration-based grouping of stories (matches T6's existing simplified, non-sprint scope).
- Editing/updating work items from the dashboard (read-only view only).
- Filtering user stories by the dashboard's global date range beyond `changed_date`/open-state
presence — exact windowing behavior to be confirmed in PR-10's own design step if ambiguous.
---
## 6. Per-Session Contract
Every session:
1. Branch from `dev` (first and only PR in this playbook).
2. Scoped changes only — no opportunistic cleanup outside this PR's stated files.
3. Add a runnable check for any new non-trivial logic — extend `tests/test_ado_boards_connector.py`,
`tests/test_store_db.py`, `tests/test_api_server.py`; no test-framework scaffolding beyond
what the PR's own code needs.
4. All checks green before any git action.
5. **Once all checks are green, stop and confirm with the user before staging, committing, or
pushing anything** — do not take any git action without explicit approval (Hard Rule 12).
6. On approval: `git add -A`, `commit-msg` skill for the message (no Claude co-author trailer),
`git push -u origin <branch>` (or `git push origin dev` if committing directly to `dev`).
7. No PR is opened as part of this workflow — push is the last step.
---
## PR 10 — User Stories Per-User View (T10)
**Branch:** `feature/112621-user-stories-per-user-view-v4-pr10`
**Branches from:** `dev`
**Model:** Claude Sonnet 4.5
**Effort:** medium
### Problem
`connectors/ado_boards_connector.py`'s `query_closed_work_items()` and `WORK_ITEM_FIELDS` only
ever fetch **closed** leaf work items for the Team Velocity KPI, and never fetch
`System.AssignedTo` or `System.Title`. There is no per-user, per-project view of "what work is
assigned to this person" anywhere in the dashboard — `store/schema.sql`'s `boards_work_items`
table has no assignee column and is intentionally shaped only for `SUM(story_points)`. The
contributor detail page (`public/src/views/ContributorView.vue`) has three tabs (Azure DevOps,
Copilot, Claude Code) and no user-stories tab.
### Required change
1. In `connectors/ado_boards_connector.py`:
- Add `ALL_WORK_ITEM_FIELDS` (new list, extends `WORK_ITEM_FIELDS` with
`System.AssignedTo`, `System.Title`, `System.CreatedDate`) — leave `WORK_ITEM_FIELDS` itself
untouched.
- Add `query_all_leaf_work_items(org, project, since, until, headers)` — same WIQL shape as
`query_closed_work_items` but no `[System.State] IN (...)` clause; scoped to
`LEAF_WORK_ITEM_TYPES` and `[System.ChangedDate]` in `[since, until]`.
- Add `normalise_all_work_items(raw_items, project_id, project_name)` → rows with
`work_item_id, work_item_type, title, state, assignee_email, assignee_name, story_points,
iteration_path, created_date, changed_date`. Parse `System.AssignedTo` (dict with
`uniqueName`/`displayName`, or absent = unassigned → both fields `None`).
- Extend `discover_project_boards_data()` to also call the new query/fetch/normalise
functions and return an `all_work_items` key, additive to the existing return dict.
- Parallelize the project loop in `run()` using a bounded `ThreadPoolExecutor` (new
`BOARDS_FETCH_WORKERS` constant, mirrors `ado_connector.py`'s `COMMIT_FETCH_WORKERS`).
- Update `meta` in the output dict to include `all_work_item_count`.
2. In `connectors/ado_connector.py`'s `main()`: run the commits/PRs `run()` call and the Boards
`boards_run()` call concurrently (e.g. via `ThreadPoolExecutor(max_workers=2)` or
`concurrent.futures`), preserving both output files being written and identical log/summary
output at the end. Keep `--skip-boards` behavior intact.
3. In `store/schema.sql`: add the new `work_items` table (see work-item doc's Design Notes for
exact columns) with an index on `assignee_email`.
4. In `store/db.py`: add `ingest_work_items(connector_json_path, db_path)` (upsert on
`(project_id, work_item_id)`, mirrors `ingest_boards()`) and
`query_user_stories(email, db_path)` returning stories grouped by project. Wire
`ingest_work_items` into `ingest_all()`.
5. In `api/server.py`: add `GET /api/user-stories-detail?email=...` following the
`/api/copilot-detail` handler pattern (404-free — empty list per project is valid, not 404).
6. In `public/src/api.js`: add `fetchUserStoriesDetail(apiBase, email)`.
7. In `public/src/views/ContributorView.vue`: add a 4th tab (`{ id: 'stories', label: 'User
Stories' }`) to `DETAIL_TABS`, lazy-load pattern mirroring `loadCopilotDetail()`/
`copilotLoaded`/`copilotDetail`. **Revised post-ship:** tab order changed to Azure DevOps →
User Stories → GitHub Copilot → Claude Code (User Stories moved to 2nd position, immediately
after Azure DevOps, per explicit user request) — both the `DETAIL_TABS` array and the
matching template `v-if`/`v-else-if` block order were updated to match.
8. New `public/src/components/UserStoriesPanel.vue`: collapsible accordion per project (**all
projects collapsed by default** — revised post-ship; user explicitly did not want any
project pre-expanded), each expanded project renders a **Kanban-style board** (revised from
the originally-shipped flat list — see "Kanban vs. flat-list" above): one column per distinct
`state` value present in that project's stories (dynamic, not hardcoded — ordered by a
best-effort priority list, unrecognized states still get their own column), each story
rendered as a card = ID + title (link to
`https://dev.azure.com/{org}/{project}/_workitems/edit/{id}`), work item type badge, story
points badge (or omitted if null), iteration path, changed date. Empty-state per project
section when its `stories` array is empty.
### Prompt — paste verbatim into Claude Code
```text
You are implementing PR-10 of the User Stories Per-User View playbook
(docs/playbooks/v4-user-stories-playbook.md).
Read the full PR-10 section and the "Kanban vs. flat-list — DECIDED" subsection before writing
any code. Read connectors/ado_boards_connector.py, connectors/ado_connector.py, store/schema.sql,
store/db.py, api/server.py, public/src/views/ContributorView.vue,
public/src/components/DetailToolTabs.vue, public/src/components/CopilotUsagePanel.vue, and
public/src/api.js in full first.
Branch: feature/112621-user-stories-per-user-view-v4-pr10 from dev.
Scope — T10 ONLY: add a per-user, per-project "User Stories" view (Option A: flat list, real ADO
state labels, no Kanban columns). Do NOT modify query_closed_work_items, WORK_ITEM_FIELDS,
boards_work_items, or query_team_velocity — the existing Team Velocity (T6) path must be
completely untouched.
═══════════════════════════════════════
STEP 1 — Connector: fetch all leaf work items with assignee/title
═══════════════════════════════════════
1. In connectors/ado_boards_connector.py add ALL_WORK_ITEM_FIELDS (WORK_ITEM_FIELDS +
System.AssignedTo, System.Title, System.CreatedDate) without touching WORK_ITEM_FIELDS.
2. Add query_all_leaf_work_items() — WIQL query scoped to LEAF_WORK_ITEM_TYPES and
[System.ChangedDate] in [since, until], no state filter.
3. Add normalise_all_work_items() producing rows with work_item_id, work_item_type, title,
state, assignee_email, assignee_name, story_points, iteration_path, created_date,
changed_date. Handle missing/absent AssignedTo as (None, None).
4. Extend discover_project_boards_data() to also return all_work_items (additive key).
5. Add BOARDS_FETCH_WORKERS and parallelize the per-project loop in run() with a bounded
ThreadPoolExecutor, preserving current output shape/ordering-independence.
6. Add all_work_item_count to the output meta dict.
═══════════════════════════════════════
STEP 2 — Parallel top-level ETL in ado_connector.py
═══════════════════════════════════════
1. In main(), run the commits/PRs run() and the Boards boards_run() concurrently (two
futures/threads), keeping --skip-boards working and both output files written with
unchanged log summaries.
═══════════════════════════════════════
STEP 3 — Storage
═══════════════════════════════════════
1. Add work_items table to store/schema.sql (project_id, work_item_id PK, work_item_type,
title, state, assignee_email, assignee_name, story_points, iteration_path, created_date,
changed_date) + index on assignee_email. Do not touch boards_work_items or weekly_velocity.
2. Add ingest_work_items() (upsert on project_id+work_item_id) and query_user_stories(email,
db_path) (grouped by project) to store/db.py. Wire ingest_work_items into ingest_all().
═══════════════════════════════════════
STEP 4 — API
═══════════════════════════════════════
1. Add GET /api/user-stories-detail?email=... to api/server.py using query_user_stories,
following the /api/copilot-detail handler's structure and error handling.
═══════════════════════════════════════
STEP 5 — Frontend
═══════════════════════════════════════
1. Add fetchUserStoriesDetail(apiBase, email) to public/src/api.js.
2. Add a 4th "User Stories" tab to ContributorView.vue's DETAIL_TABS with lazy-load state
(storiesLoading, storiesErrorMsg, storiesDetail, storiesLoaded) mirroring the existing
Copilot tab's pattern exactly.
3. Create public/src/components/UserStoriesPanel.vue: collapsible per-project accordion
(first section expanded by default), each story row showing ID+title (linked to ADO),
a colored status pill from the raw state string, story points badge or "—", iteration path,
changed date. Per-project empty-state when stories is empty. Match existing component
styling conventions (reuse .tool-pill-style CSS class patterns already in the design system).
═══════════════════════════════════════
Verification
═══════════════════════════════════════
1. Extend tests/test_ado_boards_connector.py to cover query_all_leaf_work_items,
normalise_all_work_items (including unassigned-item case), and parallel run() behavior —
no live API calls (mock requests).
2. Extend tests/test_store_db.py to cover ingest_work_items and query_user_stories (grouping,
empty-project case, null story_points).
3. Extend tests/test_api_server.py to cover /api/user-stories-detail (found, empty, unknown
email).
4. Run the full test suite (pytest) and the Vue test suite (public/tests) — no failures.
5. Confirm query_closed_work_items / query_team_velocity / boards_work_items are byte-for-byte
unchanged (git diff shows no modifications to those symbols).
Once implementation is complete and all tests pass, STOP. Do not stage,
commit, or push anything — confirm with the user first.
On user approval only:
1. git add -A (stage all files).
2. Commit via the commit-msg skill.
Suggested title: "feat: add per-user user stories view grouped by project (T10)"
Resolves: #112621
3. git push -u origin feature/112621-user-stories-per-user-view-v4-pr10. No PR to open.
No Co-authored-by: Claude trailer.
```
---
## 7. Verification Samples
**Connector (mocked WIQL response), 2 projects, 1 unassigned item:**
```json
{
"meta": { "all_work_item_count": 3 },
"projects": [
{
"project_id": "p1", "project_name": "Buildfitters",
"all_work_items": [
{"work_item_id": 501, "title": "Add login flow", "work_item_type": "User Story",
"state": "Active", "assignee_email": "fehsan@alphabold.com",
"assignee_name": "Fehsan", "story_points": 3.0, "iteration_path": "Sprint 12",
"created_date": "2026-07-01", "changed_date": "2026-07-20"}
]
}
]
}
```
**`/api/user-stories-detail?email=fehsan@alphabold.com` expected shape:**
```json
{
"projects": [
{ "project_id": "p1", "project_name": "Buildfitters",
"stories": [ { "work_item_id": 501, "title": "Add login flow", "state": "Active",
"story_points": 3.0, "iteration_path": "Sprint 12",
"changed_date": "2026-07-20", "work_item_type": "User Story" } ] }
]
}
```
A contributor with zero assigned stories returns `{"projects": []}` (or projects present with
empty `stories` arrays, per implementation choice) — never a 404 or error.
---
## 7a. Post-Ship Revisions (2026-07-31, same day as initial PR-10 ship)
The following changes were made after PR-10's initial implementation, all client-side only
(`public/src/components/UserStoriesPanel.vue`, `public/src/views/ContributorView.vue`,
`public/dashboard.css`) — no connector/schema/API changes were required for any of them:
1. **Board/Kanban view (supersedes Option A → Option B).** User explicitly requested a
column view "similar to what we have in ADO boards" with columns "dynamic based on total
columns/work types per project." Implemented via `columnsForProject()`, which groups each
project's already-fetched stories by their real `state` value — no new backend discovery
was needed since every story's state was already being fetched. Modernized card-based visual
design added to `dashboard.css` (`.story-board`, `.story-column`, `.story-card`, etc.).
2. **All projects collapsed by default.** Originally the first project was expanded by default;
changed so that clicking into the User Stories tab shows all project sections collapsed,
requiring an explicit click to expand any one of them.
3. **Tab reordered.** `DETAIL_TABS` changed from `[ado, copilot, claude, stories]` to
`[ado, stories, copilot, claude]` — User Stories now appears immediately after Azure DevOps.
4. **Clickable story titles bug fix.** The originally-shipped `adoWorkItemUrl()` read
`window.__ADO_ORG__ || ''`, but that global was never set anywhere in the codebase, so titles
were silently never clickable (always fell through to a plain `<span>`). Fixed by hardcoding
`const ADO_ORG = 'AlphaBOLD'` (matches `.env`'s `ADO_ORG` and the connector's org usage
app-wide) and simplifying the template to always render an `<a target="_blank">` — this is a
single-org-scoped dashboard, so there was no real per-request/per-org variability to plumb
through in the first place.
None of these revisions touched `connectors/ado_boards_connector.py`, `store/schema.sql`,
`store/db.py`, or `api/server.py` — Acceptance Criteria #1, #2, #3, #5, #6, #7 (§ work-item doc)
all still hold exactly as originally verified.
---
## 8. Rollback
Revert the PR-10 commit on `dev` (single commit/branch, no stacked dependents). This drops the
new `work_items` table population and `/api/user-stories-detail` endpoint/UI tab; the existing
Team Velocity KPI, commits, and PR data are entirely unaffected since `boards_work_items` and
`weekly_velocity` were never touched. The `work_items` table itself can remain in the SQLite
schema harmlessly (empty/unused) or be dropped manually if a clean rollback is required.