All files / src load-delivery-graph.ts

61.11% Statements 77/126
72.09% Branches 31/43
60% Functions 12/20
61.11% Lines 77/126

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 ?? [],
  }))
}