Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 | 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 3x 3x 1x 1x 2x 2x 2x 2x 2x 2x 2x 2x 4x 1x 1x 4x 4x 4x 2x 2x 1x 1x 1x 1x 1x 1x 1x 1x 1x 2x 2x 2x 2x 2x 1x 1x 1x 1x 2x 1x 1x 1x 1x 1x 1x 2x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x | import type { UserStory } from "./domain/types.js" import type { GraphEdge, GraphModel, GraphNode } from "./domain/graph-model.js" export const SPRINT_NONE = "__none__" export type DeliveryGraphFilters = { versions?: ReadonlySet<string> sprints?: ReadonlySet<string> epics?: ReadonlySet<string> } export function sprintKeyForStory(story: UserStory): string { return story.sprint ?? SPRINT_NONE } export function filterStoriesForDeliveryGraph( stories: UserStory[], filters: DeliveryGraphFilters = {}, ): UserStory[] { const { versions, sprints, epics } = filters if (versions && versions.size === 0) { return [] } if (sprints && sprints.size === 0) { return [] } if (epics && epics.size === 0) { return [] } return stories.filter((s) => { if (versions && versions.size > 0 && !versions.has(s.version)) { return false } if (sprints && sprints.size > 0 && !sprints.has(sprintKeyForStory(s))) { return false } if (epics && epics.size > 0 && !epics.has(s.epic)) { return false } return true }) } /** Nodes = US in scope; edges = dependsOn (story → dependency). */ export function buildDeliveryGraph( stories: UserStory[], filters: DeliveryGraphFilters = {}, ): GraphModel { const scoped = filterStoriesForDeliveryGraph(stories, filters) const scopedIds = new Set(scoped.map((s) => s.id)) const nodes: GraphNode[] = scoped.map((s) => ({ id: s.id, label: s.title, version: s.version, sprint: s.sprint, status: s.status, })) const edges: GraphEdge[] = [] const seen = new Set<string>() for (const story of scoped) { for (const dep of story.dependsOn ?? []) { if (!scopedIds.has(dep)) { continue } const key = `${story.id}->${dep}` if (seen.has(key)) { continue } seen.add(key) edges.push({ from: story.id, to: dep }) } } nodes.sort((a, b) => a.id.localeCompare(b.id)) edges.sort((a, b) => `${a.from}${a.to}`.localeCompare(`${b.from}${b.to}`)) return { nodes, edges } } export function listDeliveryFilterOptions(stories: UserStory[]): { versions: string[] sprints: string[] } { const versions = [...new Set(stories.map((s) => s.version).filter(Boolean))].sort() const sprintIds = new Set(stories.map(sprintKeyForStory)) const sprints = [...sprintIds].sort((a, b) => a.localeCompare(b, undefined, { numeric: true })) return { versions, sprints } } export function sprintIdsInScope( stories: UserStory[], selectedVersions: ReadonlySet<string>, ): string[] { const ids = new Set( stories.filter((s) => selectedVersions.has(s.version)).map(sprintKeyForStory), ) return [...ids].sort((a, b) => a.localeCompare(b, undefined, { numeric: true })) } export function epicIdsInScope( stories: UserStory[], selectedVersions: ReadonlySet<string>, ): string[] { const ids = new Set( stories.filter((s) => selectedVersions.has(s.version)).map((s) => s.epic), ) return [...ids].sort((a, b) => a.localeCompare(b, undefined, { numeric: true })) } export function deliveryStoriesForGraphPayload(stories: UserStory[]): Array<{ id: string title: string version: string sprint: string | null epic: string status: string dependsOn: string[] }> { return stories.map((s) => ({ id: s.id, title: s.title, version: s.version, sprint: s.sprint, epic: s.epic, status: s.status, dependsOn: s.dependsOn ?? [], })) } |