prjct-cli 0.18.2 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +82 -0
- package/CLAUDE.md +74 -211
- package/core/agentic/prompt-builder.ts +3 -7
- package/core/command-registry/optional-commands.ts +0 -20
- package/core/infrastructure/command-installer/command-installer.ts +8 -1
- package/core/infrastructure/command-installer/global-config.ts +31 -1
- package/core/infrastructure/command-installer/index.ts +1 -1
- package/core/infrastructure/setup.ts +3 -0
- package/package.json +3 -17
- package/templates/agentic/agents/uxui.md +210 -0
- package/templates/commands/bug.md +219 -41
- package/templates/commands/done.md +57 -258
- package/templates/commands/feature.md +368 -80
- package/templates/commands/now.md +72 -277
- package/templates/commands/ship.md +167 -246
- package/templates/commands/sync.md +62 -3
- package/templates/commands/test.md +160 -20
- package/templates/global/CLAUDE.md +40 -205
- package/templates/global/docs/agents.md +88 -0
- package/templates/global/docs/architecture.md +103 -0
- package/templates/global/docs/commands.md +98 -0
- package/templates/global/docs/validation.md +95 -0
- package/bin/dev.js +0 -216
- package/bin/serve.js +0 -361
- package/packages/web/README.md +0 -36
- package/packages/web/app/api/claude/sessions/route.ts +0 -44
- package/packages/web/app/api/claude/status/route.ts +0 -34
- package/packages/web/app/api/projects/[id]/icon/route.ts +0 -33
- package/packages/web/app/api/projects/[id]/momentum/route.ts +0 -257
- package/packages/web/app/api/projects/[id]/route.ts +0 -29
- package/packages/web/app/api/projects/[id]/stats/route.ts +0 -41
- package/packages/web/app/api/projects/[id]/status/route.ts +0 -21
- package/packages/web/app/api/projects/route.ts +0 -16
- package/packages/web/app/api/sessions/current/route.ts +0 -132
- package/packages/web/app/api/sessions/history/route.ts +0 -204
- package/packages/web/app/error.tsx +0 -34
- package/packages/web/app/favicon.ico +0 -0
- package/packages/web/app/globals.css +0 -198
- package/packages/web/app/layout.tsx +0 -53
- package/packages/web/app/loading.tsx +0 -7
- package/packages/web/app/not-found.tsx +0 -25
- package/packages/web/app/page.tsx +0 -12
- package/packages/web/app/project/[id]/code/layout.tsx +0 -18
- package/packages/web/app/project/[id]/code/page.tsx +0 -408
- package/packages/web/app/project/[id]/error.tsx +0 -41
- package/packages/web/app/project/[id]/loading.tsx +0 -9
- package/packages/web/app/project/[id]/not-found.tsx +0 -27
- package/packages/web/app/project/[id]/page.tsx +0 -384
- package/packages/web/app/project/[id]/reports/page.tsx +0 -59
- package/packages/web/app/project/[id]/reports/print/page.tsx +0 -58
- package/packages/web/app/sessions/page.tsx +0 -165
- package/packages/web/app/settings/page.tsx +0 -151
- package/packages/web/components/ActivityTimeline/ActivityTimeline.constants.ts +0 -2
- package/packages/web/components/ActivityTimeline/ActivityTimeline.tsx +0 -49
- package/packages/web/components/ActivityTimeline/ActivityTimeline.types.ts +0 -8
- package/packages/web/components/ActivityTimeline/hooks/index.ts +0 -2
- package/packages/web/components/ActivityTimeline/hooks/useExpandable.ts +0 -9
- package/packages/web/components/ActivityTimeline/hooks/useGroupedEvents.ts +0 -23
- package/packages/web/components/ActivityTimeline/index.ts +0 -2
- package/packages/web/components/AgentsCard/AgentsCard.tsx +0 -93
- package/packages/web/components/AgentsCard/AgentsCard.types.ts +0 -14
- package/packages/web/components/AgentsCard/index.ts +0 -2
- package/packages/web/components/AppSidebar/AppSidebar.tsx +0 -316
- package/packages/web/components/AppSidebar/index.ts +0 -1
- package/packages/web/components/BackLink/BackLink.tsx +0 -18
- package/packages/web/components/BackLink/BackLink.types.ts +0 -5
- package/packages/web/components/BackLink/index.ts +0 -2
- package/packages/web/components/BentoCard/BentoCard.constants.ts +0 -16
- package/packages/web/components/BentoCard/BentoCard.tsx +0 -48
- package/packages/web/components/BentoCard/BentoCard.types.ts +0 -15
- package/packages/web/components/BentoCard/index.ts +0 -2
- package/packages/web/components/BentoCardSkeleton/BentoCardSkeleton.constants.ts +0 -9
- package/packages/web/components/BentoCardSkeleton/BentoCardSkeleton.tsx +0 -18
- package/packages/web/components/BentoCardSkeleton/BentoCardSkeleton.types.ts +0 -5
- package/packages/web/components/BentoCardSkeleton/index.ts +0 -2
- package/packages/web/components/BentoGrid/BentoGrid.tsx +0 -18
- package/packages/web/components/BentoGrid/BentoGrid.types.ts +0 -4
- package/packages/web/components/BentoGrid/index.ts +0 -2
- package/packages/web/components/BlockersCard/BlockersCard.tsx +0 -75
- package/packages/web/components/BlockersCard/BlockersCard.types.ts +0 -12
- package/packages/web/components/BlockersCard/index.ts +0 -2
- package/packages/web/components/CommandBar/CommandBar.tsx +0 -67
- package/packages/web/components/CommandBar/index.ts +0 -1
- package/packages/web/components/CommandButton/CommandButton.tsx +0 -46
- package/packages/web/components/CommandButton/index.ts +0 -1
- package/packages/web/components/ConnectionStatus/ConnectionStatus.tsx +0 -29
- package/packages/web/components/ConnectionStatus/index.ts +0 -1
- package/packages/web/components/DashboardContent/DashboardContent.tsx +0 -284
- package/packages/web/components/DashboardContent/index.ts +0 -1
- package/packages/web/components/DateGroup/DateGroup.tsx +0 -18
- package/packages/web/components/DateGroup/DateGroup.types.ts +0 -6
- package/packages/web/components/DateGroup/DateGroup.utils.ts +0 -11
- package/packages/web/components/DateGroup/index.ts +0 -2
- package/packages/web/components/EmptyState/EmptyState.tsx +0 -76
- package/packages/web/components/EmptyState/EmptyState.types.ts +0 -11
- package/packages/web/components/EmptyState/index.ts +0 -2
- package/packages/web/components/EventRow/EventRow.constants.ts +0 -10
- package/packages/web/components/EventRow/EventRow.tsx +0 -49
- package/packages/web/components/EventRow/EventRow.types.ts +0 -7
- package/packages/web/components/EventRow/EventRow.utils.ts +0 -49
- package/packages/web/components/EventRow/index.ts +0 -2
- package/packages/web/components/ExpandButton/ExpandButton.tsx +0 -18
- package/packages/web/components/ExpandButton/ExpandButton.types.ts +0 -6
- package/packages/web/components/ExpandButton/index.ts +0 -2
- package/packages/web/components/HealthGradientBackground/HealthGradientBackground.tsx +0 -14
- package/packages/web/components/HealthGradientBackground/HealthGradientBackground.types.ts +0 -5
- package/packages/web/components/HealthGradientBackground/HealthGradientBackground.utils.ts +0 -13
- package/packages/web/components/HealthGradientBackground/index.ts +0 -2
- package/packages/web/components/HeroSection/HeroSection.tsx +0 -92
- package/packages/web/components/HeroSection/HeroSection.types.ts +0 -14
- package/packages/web/components/HeroSection/HeroSection.utils.ts +0 -11
- package/packages/web/components/HeroSection/hooks/index.ts +0 -2
- package/packages/web/components/HeroSection/hooks/useCountUp.ts +0 -45
- package/packages/web/components/HeroSection/hooks/useWeeklyActivity.ts +0 -18
- package/packages/web/components/HeroSection/index.ts +0 -2
- package/packages/web/components/IdeasCard/IdeasCard.tsx +0 -115
- package/packages/web/components/IdeasCard/IdeasCard.types.ts +0 -10
- package/packages/web/components/IdeasCard/index.ts +0 -2
- package/packages/web/components/InsightMessage/InsightMessage.tsx +0 -9
- package/packages/web/components/InsightMessage/InsightMessage.types.ts +0 -3
- package/packages/web/components/InsightMessage/index.ts +0 -2
- package/packages/web/components/Logo/Logo.tsx +0 -65
- package/packages/web/components/Logo/index.ts +0 -1
- package/packages/web/components/MarkdownContent/MarkdownContent.tsx +0 -123
- package/packages/web/components/MarkdownContent/index.ts +0 -1
- package/packages/web/components/MasonryGrid/MasonryGrid.tsx +0 -18
- package/packages/web/components/MasonryGrid/index.ts +0 -1
- package/packages/web/components/MomentumWidget/MomentumWidget.tsx +0 -119
- package/packages/web/components/MomentumWidget/MomentumWidget.types.ts +0 -16
- package/packages/web/components/MomentumWidget/index.ts +0 -2
- package/packages/web/components/NowCard/NowCard.tsx +0 -118
- package/packages/web/components/NowCard/NowCard.types.ts +0 -16
- package/packages/web/components/NowCard/index.ts +0 -2
- package/packages/web/components/PageHeader/PageHeader.tsx +0 -24
- package/packages/web/components/PageHeader/index.ts +0 -1
- package/packages/web/components/ProgressRing/ProgressRing.constants.ts +0 -20
- package/packages/web/components/ProgressRing/ProgressRing.tsx +0 -51
- package/packages/web/components/ProgressRing/ProgressRing.types.ts +0 -11
- package/packages/web/components/ProgressRing/index.ts +0 -2
- package/packages/web/components/ProjectAvatar/ProjectAvatar.tsx +0 -54
- package/packages/web/components/ProjectAvatar/index.ts +0 -1
- package/packages/web/components/ProjectColorDot/ProjectColorDot.tsx +0 -37
- package/packages/web/components/ProjectColorDot/index.ts +0 -1
- package/packages/web/components/ProjectSelectorModal/ProjectSelectorModal.tsx +0 -104
- package/packages/web/components/ProjectSelectorModal/index.ts +0 -1
- package/packages/web/components/Providers/Providers.tsx +0 -48
- package/packages/web/components/Providers/index.ts +0 -1
- package/packages/web/components/QueueCard/QueueCard.tsx +0 -125
- package/packages/web/components/QueueCard/QueueCard.types.ts +0 -12
- package/packages/web/components/QueueCard/QueueCard.utils.ts +0 -12
- package/packages/web/components/QueueCard/index.ts +0 -2
- package/packages/web/components/RecoverCard/RecoverCard.tsx +0 -72
- package/packages/web/components/RecoverCard/RecoverCard.types.ts +0 -16
- package/packages/web/components/RecoverCard/index.ts +0 -2
- package/packages/web/components/RoadmapCard/RoadmapCard.tsx +0 -145
- package/packages/web/components/RoadmapCard/RoadmapCard.types.ts +0 -16
- package/packages/web/components/RoadmapCard/index.ts +0 -2
- package/packages/web/components/ShipsCard/ShipsCard.tsx +0 -95
- package/packages/web/components/ShipsCard/ShipsCard.types.ts +0 -14
- package/packages/web/components/ShipsCard/ShipsCard.utils.ts +0 -4
- package/packages/web/components/ShipsCard/index.ts +0 -2
- package/packages/web/components/SparklineChart/SparklineChart.tsx +0 -40
- package/packages/web/components/SparklineChart/SparklineChart.types.ts +0 -6
- package/packages/web/components/SparklineChart/index.ts +0 -2
- package/packages/web/components/StatsMasonry/StatsMasonry.tsx +0 -95
- package/packages/web/components/StatsMasonry/index.ts +0 -1
- package/packages/web/components/StreakCard/StreakCard.constants.ts +0 -2
- package/packages/web/components/StreakCard/StreakCard.tsx +0 -55
- package/packages/web/components/StreakCard/StreakCard.types.ts +0 -4
- package/packages/web/components/StreakCard/index.ts +0 -2
- package/packages/web/components/TasksCounter/TasksCounter.tsx +0 -14
- package/packages/web/components/TasksCounter/TasksCounter.types.ts +0 -3
- package/packages/web/components/TasksCounter/index.ts +0 -2
- package/packages/web/components/TechStackBadges/TechStackBadges.tsx +0 -28
- package/packages/web/components/TechStackBadges/index.ts +0 -1
- package/packages/web/components/TerminalDock/DockToggleTab.tsx +0 -29
- package/packages/web/components/TerminalDock/TerminalDock.tsx +0 -386
- package/packages/web/components/TerminalDock/TerminalDockTab.tsx +0 -130
- package/packages/web/components/TerminalDock/TerminalTabBar.tsx +0 -142
- package/packages/web/components/TerminalDock/index.ts +0 -2
- package/packages/web/components/TerminalTabs/TerminalTab.tsx +0 -95
- package/packages/web/components/TerminalTabs/TerminalTabs.tsx +0 -211
- package/packages/web/components/TerminalTabs/index.ts +0 -1
- package/packages/web/components/VelocityBadge/VelocityBadge.tsx +0 -32
- package/packages/web/components/VelocityBadge/VelocityBadge.types.ts +0 -3
- package/packages/web/components/VelocityBadge/index.ts +0 -2
- package/packages/web/components/VelocityCard/VelocityCard.tsx +0 -73
- package/packages/web/components/VelocityCard/VelocityCard.types.ts +0 -7
- package/packages/web/components/VelocityCard/index.ts +0 -2
- package/packages/web/components/WeeklyReports/PrintableReport.tsx +0 -259
- package/packages/web/components/WeeklyReports/ReportPreviewCard.tsx +0 -187
- package/packages/web/components/WeeklyReports/WeekCalendar.tsx +0 -288
- package/packages/web/components/WeeklyReports/WeeklyReports.tsx +0 -149
- package/packages/web/components/WeeklyReports/index.ts +0 -4
- package/packages/web/components/WeeklySparkline/WeeklySparkline.tsx +0 -25
- package/packages/web/components/WeeklySparkline/WeeklySparkline.types.ts +0 -4
- package/packages/web/components/WeeklySparkline/index.ts +0 -2
- package/packages/web/components/charts/SessionsChart.tsx +0 -175
- package/packages/web/components/ui/alert-dialog.tsx +0 -157
- package/packages/web/components/ui/badge.tsx +0 -46
- package/packages/web/components/ui/button.tsx +0 -60
- package/packages/web/components/ui/card.tsx +0 -92
- package/packages/web/components/ui/chart.tsx +0 -385
- package/packages/web/components/ui/dialog.tsx +0 -143
- package/packages/web/components/ui/drawer.tsx +0 -135
- package/packages/web/components/ui/dropdown-menu.tsx +0 -257
- package/packages/web/components/ui/input.tsx +0 -21
- package/packages/web/components/ui/scroll-area.tsx +0 -58
- package/packages/web/components/ui/select.tsx +0 -187
- package/packages/web/components/ui/sheet.tsx +0 -139
- package/packages/web/components/ui/tabs.tsx +0 -66
- package/packages/web/components/ui/tooltip.tsx +0 -61
- package/packages/web/components.json +0 -22
- package/packages/web/context/GlobalTerminalContext.tsx +0 -538
- package/packages/web/context/TerminalContext.tsx +0 -45
- package/packages/web/context/TerminalTabsContext.tsx +0 -181
- package/packages/web/eslint.config.mjs +0 -18
- package/packages/web/hooks/useClaudeTerminal.ts +0 -425
- package/packages/web/hooks/useProjectStats.ts +0 -93
- package/packages/web/hooks/useProjects.ts +0 -73
- package/packages/web/lib/actions/projects.ts +0 -15
- package/packages/web/lib/commands.ts +0 -81
- package/packages/web/lib/format.ts +0 -23
- package/packages/web/lib/generate-week-report.ts +0 -285
- package/packages/web/lib/parse-prjct-files.ts +0 -1123
- package/packages/web/lib/project-colors.ts +0 -58
- package/packages/web/lib/projects.ts +0 -506
- package/packages/web/lib/pty.ts +0 -101
- package/packages/web/lib/query-config.ts +0 -44
- package/packages/web/lib/services/index.ts +0 -9
- package/packages/web/lib/services/projects.server.ts +0 -66
- package/packages/web/lib/services/stats.server.ts +0 -562
- package/packages/web/lib/unified-loader.ts +0 -396
- package/packages/web/lib/utils.ts +0 -6
- package/packages/web/next-env.d.ts +0 -6
- package/packages/web/next.config.ts +0 -7
- package/packages/web/package.json +0 -57
- package/packages/web/postcss.config.mjs +0 -7
- package/packages/web/public/file.svg +0 -1
- package/packages/web/public/globe.svg +0 -1
- package/packages/web/public/next.svg +0 -1
- package/packages/web/public/vercel.svg +0 -1
- package/packages/web/public/window.svg +0 -1
- package/packages/web/server.ts +0 -312
- package/packages/web/tsconfig.json +0 -34
- package/templates/commands/serve.md +0 -121
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Projects Service (Server-only)
|
|
3
|
-
*
|
|
4
|
-
* MD-First Architecture: Reads directly from MD files.
|
|
5
|
-
* No JSON fallback - MD is the source of truth.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import 'server-only'
|
|
9
|
-
import { cache } from 'react'
|
|
10
|
-
import { getProjects as getProjectsList, getProject as getMdProject } from '@/lib/projects'
|
|
11
|
-
|
|
12
|
-
// Types for project data
|
|
13
|
-
export interface ProjectJson {
|
|
14
|
-
projectId: string
|
|
15
|
-
name: string
|
|
16
|
-
repoPath?: string | null
|
|
17
|
-
techStack: string[]
|
|
18
|
-
fileCount: number
|
|
19
|
-
commitCount: number
|
|
20
|
-
createdAt: string
|
|
21
|
-
lastSync: string
|
|
22
|
-
version?: string | null
|
|
23
|
-
// Counts - DRY: single source of truth for dashboard/detail
|
|
24
|
-
shippedCount: number
|
|
25
|
-
nextTasksCount: number
|
|
26
|
-
completionRate: number
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Get single project by ID - cached per request
|
|
31
|
-
*
|
|
32
|
-
* MD-First: Uses MD files as source of truth
|
|
33
|
-
*/
|
|
34
|
-
export const getProject = cache(async (projectId: string): Promise<ProjectJson | null> => {
|
|
35
|
-
try {
|
|
36
|
-
const project = await getMdProject(projectId)
|
|
37
|
-
if (project) {
|
|
38
|
-
return {
|
|
39
|
-
projectId: project.id,
|
|
40
|
-
name: project.name,
|
|
41
|
-
repoPath: project.repoPath,
|
|
42
|
-
techStack: project.techStack || [],
|
|
43
|
-
fileCount: project.filesCount ? parseInt(project.filesCount) : 0,
|
|
44
|
-
commitCount: project.commitsCount ? parseInt(project.commitsCount) : 0,
|
|
45
|
-
createdAt: new Date().toISOString(),
|
|
46
|
-
lastSync: new Date().toISOString(),
|
|
47
|
-
version: project.version || null,
|
|
48
|
-
// DRY: Pass through counts from lib/projects.ts (single source of truth)
|
|
49
|
-
shippedCount: project.shippedCount ?? 0,
|
|
50
|
-
nextTasksCount: project.nextTasksCount ?? 0,
|
|
51
|
-
completionRate: project.completionRate ?? 0
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
} catch {
|
|
55
|
-
// Project not found
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
return null
|
|
59
|
-
})
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Get all projects - cached per request
|
|
63
|
-
*/
|
|
64
|
-
export const getProjects = cache(async () => {
|
|
65
|
-
return getProjectsList()
|
|
66
|
-
})
|
|
@@ -1,562 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Stats Service (Server-only)
|
|
3
|
-
*
|
|
4
|
-
* MD-First Architecture: Reads directly from MD files.
|
|
5
|
-
* No JSON fallback - MD is the source of truth.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import 'server-only'
|
|
9
|
-
import { cache } from 'react'
|
|
10
|
-
import { exec } from 'child_process'
|
|
11
|
-
import { promisify } from 'util'
|
|
12
|
-
import { getProjectStats as getMdStats, type ProjectStats, type SessionDay } from '@/lib/parse-prjct-files'
|
|
13
|
-
import { getProjects } from './projects.server'
|
|
14
|
-
|
|
15
|
-
// Types for MD-based stats
|
|
16
|
-
export interface StateJson {
|
|
17
|
-
currentTask: {
|
|
18
|
-
id?: string
|
|
19
|
-
description: string
|
|
20
|
-
startedAt?: string
|
|
21
|
-
sessionId?: string
|
|
22
|
-
feature?: string
|
|
23
|
-
agent?: string
|
|
24
|
-
} | null
|
|
25
|
-
previousTask?: {
|
|
26
|
-
id?: string
|
|
27
|
-
description: string
|
|
28
|
-
status: string
|
|
29
|
-
startedAt?: string
|
|
30
|
-
pausedAt?: string
|
|
31
|
-
} | null
|
|
32
|
-
lastUpdated?: string
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export interface QueueTask {
|
|
36
|
-
id: string
|
|
37
|
-
description: string
|
|
38
|
-
priority: 'low' | 'medium' | 'high' | 'critical'
|
|
39
|
-
type: 'feature' | 'bug' | 'improvement' | 'chore'
|
|
40
|
-
completed: boolean
|
|
41
|
-
createdAt: string
|
|
42
|
-
completedAt?: string
|
|
43
|
-
section: 'active' | 'backlog' | 'previously_active'
|
|
44
|
-
agent?: string
|
|
45
|
-
originFeature?: string
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export interface QueueJson {
|
|
49
|
-
tasks: QueueTask[]
|
|
50
|
-
lastUpdated: string
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export interface MetricsJson {
|
|
54
|
-
recentActivity?: Array<{ timestamp: string; type?: string; description?: string; action?: string }>
|
|
55
|
-
velocity?: { tasksPerDay?: number }
|
|
56
|
-
currentSprint?: { tasksCompleted?: number }
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export interface Blocker {
|
|
60
|
-
task: string
|
|
61
|
-
reason: string
|
|
62
|
-
since: string
|
|
63
|
-
daysBlocked: number
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
export interface ProjectInsights {
|
|
67
|
-
healthScore: number
|
|
68
|
-
estimateAccuracy: number
|
|
69
|
-
blockers: Blocker[]
|
|
70
|
-
recommendations: string[]
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
export interface RoadmapFeature {
|
|
74
|
-
name: string
|
|
75
|
-
status?: 'pending' | 'active' | 'shipped' | 'completed'
|
|
76
|
-
tasks: Array<{
|
|
77
|
-
description: string
|
|
78
|
-
completed: boolean
|
|
79
|
-
}>
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
export interface ShippedItem {
|
|
83
|
-
name: string
|
|
84
|
-
date?: string
|
|
85
|
-
shippedAt?: string
|
|
86
|
-
duration?: string
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
export interface UnifiedJsonData {
|
|
90
|
-
state: StateJson | null
|
|
91
|
-
queue: QueueJson | null
|
|
92
|
-
metrics: MetricsJson | null
|
|
93
|
-
insights: ProjectInsights
|
|
94
|
-
agents: Array<{
|
|
95
|
-
name: string
|
|
96
|
-
role?: string
|
|
97
|
-
description?: string
|
|
98
|
-
successRate?: number
|
|
99
|
-
tasksCompleted?: number
|
|
100
|
-
bestFor?: string[]
|
|
101
|
-
}>
|
|
102
|
-
ideas: { ideas: Array<{ text: string; status?: string; priority?: string }> } | null
|
|
103
|
-
roadmap: { features: RoadmapFeature[] } | null
|
|
104
|
-
shipped: { items: ShippedItem[] } | null
|
|
105
|
-
outcomes: Array<{ type: string }>
|
|
106
|
-
hasJsonData: boolean
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// Activity type for recent activity tracking
|
|
110
|
-
export interface RecentActivity {
|
|
111
|
-
timestamp: string
|
|
112
|
-
type: string
|
|
113
|
-
description?: string
|
|
114
|
-
duration?: string
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
const execAsync = promisify(exec)
|
|
118
|
-
|
|
119
|
-
/**
|
|
120
|
-
* Global stats for dashboard (userName, totalProjects)
|
|
121
|
-
*/
|
|
122
|
-
export interface GlobalStats {
|
|
123
|
-
userName: string
|
|
124
|
-
totalProjects: number
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
async function getGitUserName(): Promise<string> {
|
|
128
|
-
try {
|
|
129
|
-
const { stdout } = await execAsync('git config user.name')
|
|
130
|
-
return stdout.trim() || 'Developer'
|
|
131
|
-
} catch {
|
|
132
|
-
return 'Developer'
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
/**
|
|
137
|
-
* Get global stats for dashboard - cached per request
|
|
138
|
-
*/
|
|
139
|
-
export const getGlobalStats = cache(async (): Promise<GlobalStats> => {
|
|
140
|
-
const [projects, userName] = await Promise.all([
|
|
141
|
-
getProjects(),
|
|
142
|
-
getGitUserName()
|
|
143
|
-
])
|
|
144
|
-
|
|
145
|
-
return {
|
|
146
|
-
userName,
|
|
147
|
-
totalProjects: projects.length
|
|
148
|
-
}
|
|
149
|
-
})
|
|
150
|
-
|
|
151
|
-
/**
|
|
152
|
-
* Unified stats result that works with both JSON and legacy formats
|
|
153
|
-
*/
|
|
154
|
-
export interface StatsResult {
|
|
155
|
-
state: StateJson | null
|
|
156
|
-
queue: QueueJson | null
|
|
157
|
-
metrics: MetricsJson | null
|
|
158
|
-
insights: ProjectInsights
|
|
159
|
-
agents: UnifiedJsonData['agents']
|
|
160
|
-
ideas: UnifiedJsonData['ideas']
|
|
161
|
-
roadmap: UnifiedJsonData['roadmap']
|
|
162
|
-
shipped: UnifiedJsonData['shipped']
|
|
163
|
-
outcomes: UnifiedJsonData['outcomes']
|
|
164
|
-
hasData: boolean
|
|
165
|
-
isLegacy: boolean
|
|
166
|
-
legacyStats?: ProjectStats
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
const DEFAULT_INSIGHTS: ProjectInsights = {
|
|
170
|
-
healthScore: 0,
|
|
171
|
-
estimateAccuracy: 0,
|
|
172
|
-
blockers: [],
|
|
173
|
-
recommendations: ['Run /p:sync to initialize project']
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
const EMPTY_STATS_RESULT: StatsResult = {
|
|
177
|
-
state: null,
|
|
178
|
-
queue: null,
|
|
179
|
-
metrics: null,
|
|
180
|
-
insights: DEFAULT_INSIGHTS,
|
|
181
|
-
agents: [],
|
|
182
|
-
ideas: null,
|
|
183
|
-
roadmap: null,
|
|
184
|
-
shipped: null,
|
|
185
|
-
outcomes: [],
|
|
186
|
-
hasData: false,
|
|
187
|
-
isLegacy: false
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
/**
|
|
191
|
-
* Calculate estimate accuracy from sessions
|
|
192
|
-
* Returns percentage of tasks completed within ±20% of estimate
|
|
193
|
-
*/
|
|
194
|
-
function calculateEstimateAccuracy(sessions: SessionDay[]): number {
|
|
195
|
-
let tasksWithEstimate = 0
|
|
196
|
-
let accurateTasks = 0
|
|
197
|
-
|
|
198
|
-
for (const session of sessions) {
|
|
199
|
-
for (const event of session.events) {
|
|
200
|
-
// Look for task_complete events with estimate and actual duration
|
|
201
|
-
if (event.type === 'task_complete' || event.type === 'session_completed') {
|
|
202
|
-
const e = event as { estimate?: string | number; duration?: string | number; actual?: number }
|
|
203
|
-
if (e.estimate && (e.duration || e.actual)) {
|
|
204
|
-
tasksWithEstimate++
|
|
205
|
-
|
|
206
|
-
// Parse estimate (e.g., "2h" -> 7200 seconds, or raw number)
|
|
207
|
-
const estimateSec = typeof e.estimate === 'number'
|
|
208
|
-
? e.estimate
|
|
209
|
-
: parseTimeToSeconds(String(e.estimate))
|
|
210
|
-
|
|
211
|
-
// Parse actual
|
|
212
|
-
const actualSec = typeof e.actual === 'number'
|
|
213
|
-
? e.actual
|
|
214
|
-
: typeof e.duration === 'number'
|
|
215
|
-
? e.duration
|
|
216
|
-
: parseTimeToSeconds(String(e.duration || '0'))
|
|
217
|
-
|
|
218
|
-
if (estimateSec > 0 && actualSec > 0) {
|
|
219
|
-
const ratio = actualSec / estimateSec
|
|
220
|
-
// Within ±20% is "accurate"
|
|
221
|
-
if (ratio >= 0.8 && ratio <= 1.2) {
|
|
222
|
-
accurateTasks++
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
return tasksWithEstimate > 0
|
|
231
|
-
? Math.round((accurateTasks / tasksWithEstimate) * 100)
|
|
232
|
-
: 0
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
/**
|
|
236
|
-
* Parse time string to seconds (e.g., "2h" -> 7200, "30m" -> 1800)
|
|
237
|
-
*/
|
|
238
|
-
function parseTimeToSeconds(time: string): number {
|
|
239
|
-
const hours = time.match(/(\d+(?:\.\d+)?)\s*h/i)
|
|
240
|
-
const minutes = time.match(/(\d+(?:\.\d+)?)\s*m/i)
|
|
241
|
-
const seconds = time.match(/(\d+(?:\.\d+)?)\s*s/i)
|
|
242
|
-
|
|
243
|
-
let total = 0
|
|
244
|
-
if (hours) total += parseFloat(hours[1]) * 3600
|
|
245
|
-
if (minutes) total += parseFloat(minutes[1]) * 60
|
|
246
|
-
if (seconds) total += parseFloat(seconds[1])
|
|
247
|
-
|
|
248
|
-
// If just a number, assume seconds
|
|
249
|
-
if (total === 0 && /^\d+$/.test(time.trim())) {
|
|
250
|
-
total = parseInt(time.trim())
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
return total
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
/**
|
|
257
|
-
* Extract blockers from timeline events
|
|
258
|
-
*/
|
|
259
|
-
function extractBlockers(timeline: Array<{ ts: string; type: string }>): Blocker[] {
|
|
260
|
-
const blockers: Blocker[] = []
|
|
261
|
-
const now = new Date()
|
|
262
|
-
|
|
263
|
-
for (const event of timeline) {
|
|
264
|
-
// Look for pause events with reason "blocked"
|
|
265
|
-
if (event.type === 'pause' || event.type === 'session_paused') {
|
|
266
|
-
const e = event as { reason?: string; note?: string; task?: string; ts: string }
|
|
267
|
-
if (e.reason === 'blocked') {
|
|
268
|
-
const pauseDate = new Date(e.ts)
|
|
269
|
-
const daysBlocked = Math.floor((now.getTime() - pauseDate.getTime()) / (1000 * 60 * 60 * 24))
|
|
270
|
-
|
|
271
|
-
blockers.push({
|
|
272
|
-
task: e.task || 'Unknown task',
|
|
273
|
-
reason: e.note || 'Blocked',
|
|
274
|
-
since: e.ts,
|
|
275
|
-
daysBlocked
|
|
276
|
-
})
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
// Only return unresolved blockers (check if there's a resume after the pause)
|
|
282
|
-
// For now, return all - we'll refine this when we have resume tracking
|
|
283
|
-
return blockers.slice(0, 5) // Top 5 blockers
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
/**
|
|
287
|
-
* Get project stats - cached per request
|
|
288
|
-
*
|
|
289
|
-
* MD-First Architecture: MD files are the source of truth.
|
|
290
|
-
* No JSON fallback - all data comes from MD.
|
|
291
|
-
*/
|
|
292
|
-
export const getStats = cache(async (projectId: string): Promise<StatsResult> => {
|
|
293
|
-
try {
|
|
294
|
-
const mdStats = await getMdStats(projectId)
|
|
295
|
-
|
|
296
|
-
// Check if we have meaningful data
|
|
297
|
-
const hasData = Boolean(
|
|
298
|
-
mdStats.currentTask ||
|
|
299
|
-
mdStats.queue.length > 0 ||
|
|
300
|
-
mdStats.shipped.length > 0 ||
|
|
301
|
-
mdStats.timeline.length > 0 ||
|
|
302
|
-
mdStats.summary.totalEvents > 0
|
|
303
|
-
)
|
|
304
|
-
|
|
305
|
-
if (hasData) {
|
|
306
|
-
// Calculate real metrics
|
|
307
|
-
const estimateAccuracy = calculateEstimateAccuracy(mdStats.sessions)
|
|
308
|
-
const blockers = extractBlockers(mdStats.timeline)
|
|
309
|
-
|
|
310
|
-
return {
|
|
311
|
-
...EMPTY_STATS_RESULT,
|
|
312
|
-
insights: {
|
|
313
|
-
healthScore: calculateHealthScoreV2(mdStats, estimateAccuracy, blockers),
|
|
314
|
-
estimateAccuracy,
|
|
315
|
-
blockers,
|
|
316
|
-
recommendations: generateRecommendations(mdStats, estimateAccuracy, blockers)
|
|
317
|
-
},
|
|
318
|
-
hasData: true,
|
|
319
|
-
isLegacy: true,
|
|
320
|
-
legacyStats: mdStats
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
} catch {
|
|
324
|
-
// MD parsing failed - return empty stats
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
return EMPTY_STATS_RESULT
|
|
328
|
-
})
|
|
329
|
-
|
|
330
|
-
/**
|
|
331
|
-
* Calculate health score V2 - based on real data
|
|
332
|
-
*
|
|
333
|
-
* Formula:
|
|
334
|
-
* - estimateAccuracy (25): % of tasks within ±20% of estimate
|
|
335
|
-
* - completionRate (25): tasks completed / tasks started (last 7 days)
|
|
336
|
-
* - noBlockers (25): penalize if blocked tasks exist
|
|
337
|
-
* - recentActivity (25): days active in last week
|
|
338
|
-
*/
|
|
339
|
-
function calculateHealthScoreV2(
|
|
340
|
-
stats: ProjectStats,
|
|
341
|
-
estimateAccuracy: number,
|
|
342
|
-
blockers: Blocker[]
|
|
343
|
-
): number {
|
|
344
|
-
const { timeline, sessions, currentTask } = stats
|
|
345
|
-
|
|
346
|
-
// Estimate accuracy score (0-25)
|
|
347
|
-
// If no estimates yet, give benefit of doubt (15/25)
|
|
348
|
-
const accuracyScore = estimateAccuracy > 0
|
|
349
|
-
? Math.round((estimateAccuracy / 100) * 25)
|
|
350
|
-
: 15
|
|
351
|
-
|
|
352
|
-
// Completion rate (0-25)
|
|
353
|
-
const recentSessions = sessions.slice(0, 7)
|
|
354
|
-
let tasksStarted = 0
|
|
355
|
-
let tasksCompleted = 0
|
|
356
|
-
for (const session of recentSessions) {
|
|
357
|
-
tasksStarted += session.tasksStarted
|
|
358
|
-
tasksCompleted += session.tasksCompleted
|
|
359
|
-
}
|
|
360
|
-
const completionRate = tasksStarted > 0 ? tasksCompleted / tasksStarted : 0
|
|
361
|
-
const completionScore = Math.round(Math.min(1, completionRate) * 25)
|
|
362
|
-
|
|
363
|
-
// No blockers score (0-25)
|
|
364
|
-
// Full points if no blockers, -5 per blocker, -10 per blocker > 3 days
|
|
365
|
-
let blockerPenalty = 0
|
|
366
|
-
for (const blocker of blockers) {
|
|
367
|
-
blockerPenalty += blocker.daysBlocked > 3 ? 10 : 5
|
|
368
|
-
}
|
|
369
|
-
const blockerScore = Math.max(0, 25 - blockerPenalty)
|
|
370
|
-
|
|
371
|
-
// Activity score (0-25)
|
|
372
|
-
// Count unique active days in last 7 days
|
|
373
|
-
const lastWeek = new Date()
|
|
374
|
-
lastWeek.setDate(lastWeek.getDate() - 7)
|
|
375
|
-
const recentDays = new Set(
|
|
376
|
-
timeline
|
|
377
|
-
.filter(e => new Date(e.ts) > lastWeek)
|
|
378
|
-
.map(e => e.ts.split('T')[0])
|
|
379
|
-
)
|
|
380
|
-
// Bonus for having current task
|
|
381
|
-
const activityBase = Math.min(7, recentDays.size) * 3 // up to 21
|
|
382
|
-
const currentTaskBonus = currentTask ? 4 : 0
|
|
383
|
-
const activityScore = Math.min(25, activityBase + currentTaskBonus)
|
|
384
|
-
|
|
385
|
-
return Math.min(100, accuracyScore + completionScore + blockerScore + activityScore)
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
/**
|
|
389
|
-
* Legacy health score calculation (kept for fallback)
|
|
390
|
-
*/
|
|
391
|
-
function calculateHealthFromMd(stats: ProjectStats): number {
|
|
392
|
-
const { metrics, currentTask, queue, timeline } = stats
|
|
393
|
-
const velocity = metrics?.velocity?.tasksPerDay ?? 0
|
|
394
|
-
const hasCurrentTask = Boolean(currentTask)
|
|
395
|
-
const queueSize = queue?.length ?? 0
|
|
396
|
-
const recentActivity = timeline?.slice(0, 7).length ?? 0
|
|
397
|
-
|
|
398
|
-
const velocityScore = Math.min(30, velocity * 15)
|
|
399
|
-
const taskScore = hasCurrentTask ? 20 : 0
|
|
400
|
-
const queueScore = queueSize > 0 && queueSize < 15 ? 20 : queueSize === 0 ? 5 : 10
|
|
401
|
-
const activityScore = Math.min(30, recentActivity * 5)
|
|
402
|
-
|
|
403
|
-
return Math.min(100, Math.round(velocityScore + taskScore + queueScore + activityScore))
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
/**
|
|
407
|
-
* Generate recommendations based on MD stats and insights
|
|
408
|
-
*/
|
|
409
|
-
function generateRecommendations(
|
|
410
|
-
stats: ProjectStats,
|
|
411
|
-
estimateAccuracy: number,
|
|
412
|
-
blockers: Blocker[]
|
|
413
|
-
): string[] {
|
|
414
|
-
const recommendations: string[] = []
|
|
415
|
-
|
|
416
|
-
// Blocker-based recommendations (highest priority)
|
|
417
|
-
if (blockers.length > 0) {
|
|
418
|
-
const oldestBlocker = blockers.reduce((a, b) => a.daysBlocked > b.daysBlocked ? a : b)
|
|
419
|
-
if (oldestBlocker.daysBlocked > 3) {
|
|
420
|
-
recommendations.push(`Blocker "${oldestBlocker.reason}" is ${oldestBlocker.daysBlocked} days old - needs attention`)
|
|
421
|
-
} else {
|
|
422
|
-
recommendations.push(`${blockers.length} blocked task(s) - review blockers`)
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
// Estimate accuracy recommendations
|
|
427
|
-
if (estimateAccuracy > 0 && estimateAccuracy < 50) {
|
|
428
|
-
recommendations.push('Estimates often off - consider adding 30% buffer')
|
|
429
|
-
} else if (estimateAccuracy >= 80) {
|
|
430
|
-
recommendations.push('Great estimation accuracy - keep it up!')
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
// Task state recommendations
|
|
434
|
-
if (!stats.currentTask) {
|
|
435
|
-
recommendations.push('Start a task with /p:now')
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
if (stats.queue.length === 0) {
|
|
439
|
-
recommendations.push('Add tasks to queue with /p:next')
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
if (stats.ideas.pending.length > 10) {
|
|
443
|
-
recommendations.push('Review and prioritize pending ideas')
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
if (stats.agents.length === 0) {
|
|
447
|
-
recommendations.push('Run /p:sync to generate agents')
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
// Default positive message
|
|
451
|
-
if (recommendations.length === 0) {
|
|
452
|
-
recommendations.push('Keep shipping!')
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
return recommendations.slice(0, 4) // Max 4 recommendations
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
/**
|
|
459
|
-
* Calculate streak from metrics (pure function, no mutation)
|
|
460
|
-
*/
|
|
461
|
-
export function calculateStreak(metrics: MetricsJson | null): number {
|
|
462
|
-
if (!metrics?.recentActivity?.length) return 0
|
|
463
|
-
|
|
464
|
-
const activityDates = new Set(
|
|
465
|
-
metrics.recentActivity.map((a: { timestamp: string }) => new Date(a.timestamp).toISOString().split('T')[0])
|
|
466
|
-
)
|
|
467
|
-
|
|
468
|
-
const today = new Date()
|
|
469
|
-
today.setHours(0, 0, 0, 0)
|
|
470
|
-
|
|
471
|
-
// Generate last 30 days as array
|
|
472
|
-
const days = Array.from({ length: 30 }, (_, i) => {
|
|
473
|
-
const date = new Date(today)
|
|
474
|
-
date.setDate(date.getDate() - i)
|
|
475
|
-
return date.toISOString().split('T')[0]
|
|
476
|
-
})
|
|
477
|
-
|
|
478
|
-
// Find first gap (day without activity)
|
|
479
|
-
const firstGapIndex = days.findIndex(date => !activityDates.has(date))
|
|
480
|
-
|
|
481
|
-
// If today has no activity, check if yesterday does
|
|
482
|
-
if (firstGapIndex === 0) {
|
|
483
|
-
const yesterdayHasActivity = activityDates.has(days[1])
|
|
484
|
-
if (!yesterdayHasActivity) return 0
|
|
485
|
-
// Start counting from yesterday
|
|
486
|
-
const remainingDays = days.slice(1)
|
|
487
|
-
const gapFromYesterday = remainingDays.findIndex(date => !activityDates.has(date))
|
|
488
|
-
return gapFromYesterday === -1 ? remainingDays.length : gapFromYesterday
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
return firstGapIndex === -1 ? days.length : firstGapIndex
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
/**
|
|
496
|
-
* Get insight message based on stats
|
|
497
|
-
*/
|
|
498
|
-
export function getInsightMessage(stats: StatsResult, streak: number): string {
|
|
499
|
-
if (!stats.hasData) return 'Run /p:sync to get started'
|
|
500
|
-
if (stats.state?.currentTask) return `Working on: ${stats.state.currentTask.description}`
|
|
501
|
-
if (streak >= 7) return `${streak} day streak! You're on fire! 🔥`
|
|
502
|
-
if (streak >= 3) return `${streak} day streak - keep it going!`
|
|
503
|
-
|
|
504
|
-
const queueLength = stats.queue?.tasks?.filter(t => !t.completed).length ?? 0
|
|
505
|
-
if (queueLength > 0) return `${queueLength} tasks in queue`
|
|
506
|
-
return 'Ready to start working'
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
/**
|
|
510
|
-
* Calculate velocity change percentage
|
|
511
|
-
*/
|
|
512
|
-
export function getVelocityChange(velocity: number): number {
|
|
513
|
-
if (velocity > 2) return 15
|
|
514
|
-
if (velocity > 1) return 5
|
|
515
|
-
if (velocity > 0) return 0
|
|
516
|
-
return -10
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
/**
|
|
520
|
-
* Get weekly velocity data from metrics (last 7 days)
|
|
521
|
-
*/
|
|
522
|
-
export function getWeeklyVelocityData(metrics: MetricsJson | null): number[] {
|
|
523
|
-
if (!metrics?.recentActivity?.length) return []
|
|
524
|
-
|
|
525
|
-
const today = new Date()
|
|
526
|
-
|
|
527
|
-
return Array.from({ length: 7 }, (_, i) => {
|
|
528
|
-
const date = new Date(today)
|
|
529
|
-
date.setDate(date.getDate() - (6 - i))
|
|
530
|
-
const dateStr = date.toISOString().split('T')[0]
|
|
531
|
-
|
|
532
|
-
return (metrics.recentActivity || []).filter((e: { timestamp: string }) =>
|
|
533
|
-
e.timestamp?.startsWith(dateStr)
|
|
534
|
-
).length
|
|
535
|
-
})
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
/**
|
|
539
|
-
* Calculate health score from stats
|
|
540
|
-
*/
|
|
541
|
-
export function calculateHealthScore(stats: StatsResult): number {
|
|
542
|
-
if (!stats.hasData) return 0
|
|
543
|
-
if (stats.insights.healthScore > 0) return stats.insights.healthScore
|
|
544
|
-
|
|
545
|
-
// Fallback for legacy
|
|
546
|
-
if (stats.isLegacy && stats.legacyStats) {
|
|
547
|
-
const { metrics, currentTask, queue, timeline } = stats.legacyStats
|
|
548
|
-
const velocity = metrics?.velocity?.tasksPerDay ?? 0
|
|
549
|
-
const hasCurrentTask = Boolean(currentTask)
|
|
550
|
-
const queueSize = queue?.length ?? 0
|
|
551
|
-
const recentActivity = timeline?.slice(0, 7).length ?? 0
|
|
552
|
-
|
|
553
|
-
const velocityScore = Math.min(30, velocity * 15)
|
|
554
|
-
const taskScore = hasCurrentTask ? 20 : 0
|
|
555
|
-
const queueScore = queueSize > 0 && queueSize < 15 ? 20 : queueSize === 0 ? 5 : 10
|
|
556
|
-
const activityScore = Math.min(30, recentActivity * 5)
|
|
557
|
-
|
|
558
|
-
return Math.min(100, Math.round(velocityScore + taskScore + queueScore + activityScore))
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
return 50
|
|
562
|
-
}
|