@prmichaelsen/acp-visualizer 0.1.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.
Files changed (180) hide show
  1. package/README.md +68 -0
  2. package/agent/commands/acp.clarification-address.md +417 -0
  3. package/agent/commands/acp.clarification-capture.md +386 -0
  4. package/agent/commands/acp.clarification-create.md +437 -0
  5. package/agent/commands/acp.clarifications-research.md +326 -0
  6. package/agent/commands/acp.command-create.md +432 -0
  7. package/agent/commands/acp.design-create.md +286 -0
  8. package/agent/commands/acp.design-reference.md +355 -0
  9. package/agent/commands/acp.handoff.md +270 -0
  10. package/agent/commands/acp.index.md +423 -0
  11. package/agent/commands/acp.init.md +546 -0
  12. package/agent/commands/acp.package-create.md +895 -0
  13. package/agent/commands/acp.package-info.md +212 -0
  14. package/agent/commands/acp.package-install.md +539 -0
  15. package/agent/commands/acp.package-list.md +280 -0
  16. package/agent/commands/acp.package-publish.md +541 -0
  17. package/agent/commands/acp.package-remove.md +293 -0
  18. package/agent/commands/acp.package-search.md +307 -0
  19. package/agent/commands/acp.package-update.md +361 -0
  20. package/agent/commands/acp.package-validate.md +540 -0
  21. package/agent/commands/acp.pattern-create.md +386 -0
  22. package/agent/commands/acp.plan.md +587 -0
  23. package/agent/commands/acp.proceed.md +882 -0
  24. package/agent/commands/acp.project-create.md +675 -0
  25. package/agent/commands/acp.project-info.md +312 -0
  26. package/agent/commands/acp.project-list.md +226 -0
  27. package/agent/commands/acp.project-remove.md +379 -0
  28. package/agent/commands/acp.project-set.md +227 -0
  29. package/agent/commands/acp.project-update.md +307 -0
  30. package/agent/commands/acp.projects-restore.md +228 -0
  31. package/agent/commands/acp.projects-sync.md +347 -0
  32. package/agent/commands/acp.report.md +407 -0
  33. package/agent/commands/acp.resume.md +239 -0
  34. package/agent/commands/acp.sessions.md +301 -0
  35. package/agent/commands/acp.status.md +293 -0
  36. package/agent/commands/acp.sync.md +364 -0
  37. package/agent/commands/acp.task-create.md +500 -0
  38. package/agent/commands/acp.update.md +302 -0
  39. package/agent/commands/acp.validate.md +466 -0
  40. package/agent/commands/acp.version-check-for-updates.md +276 -0
  41. package/agent/commands/acp.version-check.md +191 -0
  42. package/agent/commands/acp.version-update.md +289 -0
  43. package/agent/commands/command.template.md +339 -0
  44. package/agent/commands/git.commit.md +526 -0
  45. package/agent/commands/git.init.md +514 -0
  46. package/agent/commands/tanstack-cloudflare.deploy.md +272 -0
  47. package/agent/commands/tanstack-cloudflare.tail.md +275 -0
  48. package/agent/design/.gitkeep +0 -0
  49. package/agent/design/design.template.md +154 -0
  50. package/agent/design/local.dashboard-layout-routing.md +288 -0
  51. package/agent/design/local.data-model-yaml-parsing.md +310 -0
  52. package/agent/design/local.search-filtering.md +331 -0
  53. package/agent/design/local.server-api-auto-refresh.md +235 -0
  54. package/agent/design/local.table-tree-views.md +299 -0
  55. package/agent/design/local.visualizer-requirements.md +349 -0
  56. package/agent/design/requirements.template.md +387 -0
  57. package/agent/index/.gitkeep +0 -0
  58. package/agent/index/acp.core.yaml +137 -0
  59. package/agent/index/local.main.template.yaml +37 -0
  60. package/agent/manifest.template.yaml +13 -0
  61. package/agent/manifest.yaml +302 -0
  62. package/agent/milestones/.gitkeep +0 -0
  63. package/agent/milestones/milestone-1-project-scaffold-data-pipeline.md +67 -0
  64. package/agent/milestones/milestone-1-{title}.template.md +206 -0
  65. package/agent/milestones/milestone-2-dashboard-views-interaction.md +79 -0
  66. package/agent/package.template.yaml +86 -0
  67. package/agent/patterns/.gitkeep +0 -0
  68. package/agent/patterns/bootstrap.template.md +1237 -0
  69. package/agent/patterns/pattern.template.md +382 -0
  70. package/agent/patterns/tanstack-cloudflare.acl-permissions.md +332 -0
  71. package/agent/patterns/tanstack-cloudflare.action-bar-item.md +416 -0
  72. package/agent/patterns/tanstack-cloudflare.api-route-handlers.md +401 -0
  73. package/agent/patterns/tanstack-cloudflare.auth-session-management.md +387 -0
  74. package/agent/patterns/tanstack-cloudflare.card-and-list.md +271 -0
  75. package/agent/patterns/tanstack-cloudflare.chat-engine.md +353 -0
  76. package/agent/patterns/tanstack-cloudflare.confirmation-tokens.md +346 -0
  77. package/agent/patterns/tanstack-cloudflare.durable-objects-websocket.md +516 -0
  78. package/agent/patterns/tanstack-cloudflare.email-service.md +431 -0
  79. package/agent/patterns/tanstack-cloudflare.expander.md +98 -0
  80. package/agent/patterns/tanstack-cloudflare.fcm-push.md +115 -0
  81. package/agent/patterns/tanstack-cloudflare.firebase-anonymous-sessions.md +441 -0
  82. package/agent/patterns/tanstack-cloudflare.firebase-auth.md +348 -0
  83. package/agent/patterns/tanstack-cloudflare.firebase-firestore.md +550 -0
  84. package/agent/patterns/tanstack-cloudflare.firebase-storage.md +369 -0
  85. package/agent/patterns/tanstack-cloudflare.form-controls.md +145 -0
  86. package/agent/patterns/tanstack-cloudflare.global-search-context.md +93 -0
  87. package/agent/patterns/tanstack-cloudflare.image-carousel.md +126 -0
  88. package/agent/patterns/tanstack-cloudflare.library-services.md +553 -0
  89. package/agent/patterns/tanstack-cloudflare.lightbox.md +169 -0
  90. package/agent/patterns/tanstack-cloudflare.markdown-content.md +115 -0
  91. package/agent/patterns/tanstack-cloudflare.mention-suggestions.md +98 -0
  92. package/agent/patterns/tanstack-cloudflare.modal.md +156 -0
  93. package/agent/patterns/tanstack-cloudflare.nextjs-to-tanstack-routing.md +461 -0
  94. package/agent/patterns/tanstack-cloudflare.notifications-engine.md +151 -0
  95. package/agent/patterns/tanstack-cloudflare.oauth-token-refresh.md +90 -0
  96. package/agent/patterns/tanstack-cloudflare.og-metadata.md +296 -0
  97. package/agent/patterns/tanstack-cloudflare.pagination.md +442 -0
  98. package/agent/patterns/tanstack-cloudflare.pill-input.md +220 -0
  99. package/agent/patterns/tanstack-cloudflare.provider-adapter.md +401 -0
  100. package/agent/patterns/tanstack-cloudflare.rate-limiting.md +323 -0
  101. package/agent/patterns/tanstack-cloudflare.scheduled-tasks.md +338 -0
  102. package/agent/patterns/tanstack-cloudflare.searchable-settings.md +375 -0
  103. package/agent/patterns/tanstack-cloudflare.slide-over.md +129 -0
  104. package/agent/patterns/tanstack-cloudflare.ssr-preload.md +571 -0
  105. package/agent/patterns/tanstack-cloudflare.third-party-api-integration.md +508 -0
  106. package/agent/patterns/tanstack-cloudflare.toast-system.md +142 -0
  107. package/agent/patterns/tanstack-cloudflare.unified-header.md +280 -0
  108. package/agent/patterns/tanstack-cloudflare.user-scoped-collections.md +628 -0
  109. package/agent/patterns/tanstack-cloudflare.websocket-manager.md +237 -0
  110. package/agent/patterns/tanstack-cloudflare.wrangler-configuration.md +358 -0
  111. package/agent/patterns/tanstack-cloudflare.zod-schema-validation.md +336 -0
  112. package/agent/progress.template.yaml +161 -0
  113. package/agent/progress.yaml +145 -0
  114. package/agent/schemas/package.schema.yaml +276 -0
  115. package/agent/scripts/acp.common.sh +1781 -0
  116. package/agent/scripts/acp.install.sh +333 -0
  117. package/agent/scripts/acp.package-create.sh +924 -0
  118. package/agent/scripts/acp.package-info.sh +288 -0
  119. package/agent/scripts/acp.package-install.sh +893 -0
  120. package/agent/scripts/acp.package-list.sh +311 -0
  121. package/agent/scripts/acp.package-publish.sh +420 -0
  122. package/agent/scripts/acp.package-remove.sh +348 -0
  123. package/agent/scripts/acp.package-search.sh +156 -0
  124. package/agent/scripts/acp.package-update.sh +517 -0
  125. package/agent/scripts/acp.package-validate.sh +1018 -0
  126. package/agent/scripts/acp.uninstall.sh +85 -0
  127. package/agent/scripts/acp.version-check-for-updates.sh +98 -0
  128. package/agent/scripts/acp.version-check.sh +47 -0
  129. package/agent/scripts/acp.version-update.sh +176 -0
  130. package/agent/scripts/acp.yaml-parser.sh +985 -0
  131. package/agent/scripts/acp.yaml-validate.sh +205 -0
  132. package/agent/tasks/.gitkeep +0 -0
  133. package/agent/tasks/milestone-1-project-scaffold-data-pipeline/task-1-initialize-tanstack-start-project.md +210 -0
  134. package/agent/tasks/milestone-1-project-scaffold-data-pipeline/task-2-implement-data-model-yaml-parser.md +294 -0
  135. package/agent/tasks/milestone-1-project-scaffold-data-pipeline/task-3-build-server-api-data-loading.md +193 -0
  136. package/agent/tasks/milestone-1-project-scaffold-data-pipeline/task-4-add-auto-refresh-sse.md +262 -0
  137. package/agent/tasks/milestone-2-dashboard-views-interaction/task-10-polish-integration-testing.md +156 -0
  138. package/agent/tasks/milestone-2-dashboard-views-interaction/task-5-build-dashboard-layout-routing.md +178 -0
  139. package/agent/tasks/milestone-2-dashboard-views-interaction/task-6-build-overview-page.md +141 -0
  140. package/agent/tasks/milestone-2-dashboard-views-interaction/task-7-implement-milestone-table-view.md +153 -0
  141. package/agent/tasks/milestone-2-dashboard-views-interaction/task-8-implement-milestone-tree-view.md +174 -0
  142. package/agent/tasks/milestone-2-dashboard-views-interaction/task-9-implement-search-filtering.md +233 -0
  143. package/agent/tasks/task-1-{title}.template.md +244 -0
  144. package/bin/visualize.mjs +84 -0
  145. package/package.json +48 -0
  146. package/src/components/ExtraFieldsBadge.tsx +15 -0
  147. package/src/components/FilterBar.tsx +33 -0
  148. package/src/components/Header.tsx +23 -0
  149. package/src/components/MilestoneTable.tsx +167 -0
  150. package/src/components/MilestoneTree.tsx +84 -0
  151. package/src/components/ProgressBar.tsx +20 -0
  152. package/src/components/SearchInput.tsx +22 -0
  153. package/src/components/Sidebar.tsx +54 -0
  154. package/src/components/StatusBadge.tsx +23 -0
  155. package/src/components/StatusDot.tsx +12 -0
  156. package/src/components/TaskList.tsx +36 -0
  157. package/src/components/ViewToggle.tsx +31 -0
  158. package/src/lib/config.ts +8 -0
  159. package/src/lib/file-watcher.ts +43 -0
  160. package/src/lib/search.ts +48 -0
  161. package/src/lib/types.ts +73 -0
  162. package/src/lib/useAutoRefresh.ts +31 -0
  163. package/src/lib/useCollapse.ts +31 -0
  164. package/src/lib/useFilteredData.ts +55 -0
  165. package/src/lib/yaml-loader-real.spec.ts +47 -0
  166. package/src/lib/yaml-loader.spec.ts +201 -0
  167. package/src/lib/yaml-loader.ts +265 -0
  168. package/src/routeTree.gen.ts +140 -0
  169. package/src/router.tsx +10 -0
  170. package/src/routes/__root.tsx +75 -0
  171. package/src/routes/api/watch.ts +29 -0
  172. package/src/routes/index.tsx +115 -0
  173. package/src/routes/milestones.tsx +50 -0
  174. package/src/routes/search.tsx +84 -0
  175. package/src/routes/tasks.tsx +63 -0
  176. package/src/services/progress-database.service.ts +46 -0
  177. package/src/styles.css +25 -0
  178. package/tsconfig.json +24 -0
  179. package/vite.config.ts +16 -0
  180. package/vitest.config.ts +27 -0
@@ -0,0 +1,31 @@
1
+ import { useRef, useState, useEffect } from 'react'
2
+
3
+ export function useCollapse(open: boolean) {
4
+ const ref = useRef<HTMLDivElement>(null)
5
+ const [height, setHeight] = useState<number | undefined>(open ? undefined : 0)
6
+
7
+ useEffect(() => {
8
+ const el = ref.current
9
+ if (!el) return
10
+
11
+ if (open) {
12
+ setHeight(el.scrollHeight)
13
+ const id = setTimeout(() => setHeight(undefined), 300)
14
+ return () => clearTimeout(id)
15
+ } else {
16
+ setHeight(el.scrollHeight)
17
+ requestAnimationFrame(() => {
18
+ requestAnimationFrame(() => setHeight(0))
19
+ })
20
+ }
21
+ }, [open])
22
+
23
+ return {
24
+ ref,
25
+ style: {
26
+ height: height != null ? `${height}px` : ('auto' as const),
27
+ overflow: 'hidden' as const,
28
+ transition: 'height 300ms cubic-bezier(0.4, 0, 0.2, 1)',
29
+ },
30
+ }
31
+ }
@@ -0,0 +1,55 @@
1
+ import { useMemo } from 'react'
2
+ import { buildSearchIndex } from './search'
3
+ import type { ProgressData, Status } from './types'
4
+
5
+ interface FilterState {
6
+ status: Status | 'all'
7
+ search: string
8
+ }
9
+
10
+ export function useFilteredData(
11
+ data: ProgressData | null,
12
+ filters: FilterState,
13
+ ): ProgressData | null {
14
+ return useMemo(() => {
15
+ if (!data) return null
16
+
17
+ let milestones = data.milestones
18
+ let tasks = { ...data.tasks }
19
+
20
+ // Status filter
21
+ if (filters.status !== 'all') {
22
+ milestones = milestones.filter((m) => m.status === filters.status)
23
+ tasks = Object.fromEntries(
24
+ Object.entries(tasks).map(([id, ts]) => [
25
+ id,
26
+ ts.filter((t) => t.status === filters.status),
27
+ ]),
28
+ )
29
+ }
30
+
31
+ // Search filter
32
+ if (filters.search.trim()) {
33
+ const index = buildSearchIndex(data)
34
+ const results = index.search(filters.search)
35
+ const matchedMilestoneIds = new Set(
36
+ results.map((r) => r.item.milestone.id),
37
+ )
38
+ const matchedTaskIds = new Set(
39
+ results.filter((r) => r.item.task).map((r) => r.item.task!.id),
40
+ )
41
+
42
+ milestones = milestones.filter((m) => matchedMilestoneIds.has(m.id))
43
+ tasks = Object.fromEntries(
44
+ Object.entries(tasks).map(([id, ts]) => [
45
+ id,
46
+ ts.filter(
47
+ (t) => matchedTaskIds.has(t.id) || matchedMilestoneIds.has(id),
48
+ ),
49
+ ]),
50
+ )
51
+ }
52
+
53
+ return { ...data, milestones, tasks }
54
+ }, [data, filters.status, filters.search])
55
+ }
@@ -0,0 +1,47 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { readFileSync } from 'fs'
3
+ import { resolve } from 'path'
4
+ import { parseProgressYaml } from './yaml-loader'
5
+
6
+ const PROJECT_ROOT = resolve(__dirname, '../../')
7
+
8
+ describe('parseProgressYaml with real files', () => {
9
+ it('parses the local project progress.yaml', () => {
10
+ const raw = readFileSync(resolve(PROJECT_ROOT, 'agent/progress.yaml'), 'utf-8')
11
+ const result = parseProgressYaml(raw)
12
+
13
+ expect(result.project.name).toBe('agent-context-protocol-visualizer')
14
+ expect(result.milestones.length).toBeGreaterThan(0)
15
+ expect(result.milestones[0].id).toBeTruthy()
16
+ })
17
+
18
+ it('parses a large real-world progress.yaml (remember-core)', () => {
19
+ const path = '/home/prmichaelsen/.acp/projects/remember-core/agent/progress.yaml'
20
+ let raw: string
21
+ try {
22
+ raw = readFileSync(path, 'utf-8')
23
+ } catch {
24
+ console.log('Skipping — remember-core progress.yaml not found')
25
+ return
26
+ }
27
+
28
+ const result = parseProgressYaml(raw)
29
+
30
+ expect(result.project.name).toBe('remember-core')
31
+ expect(result.milestones.length).toBeGreaterThan(0)
32
+
33
+ const totalTasks = Object.values(result.tasks).reduce(
34
+ (sum, ts) => sum + ts.length,
35
+ 0,
36
+ )
37
+ expect(totalTasks).toBeGreaterThan(0)
38
+
39
+ for (const m of result.milestones) {
40
+ expect(['completed', 'in_progress', 'not_started']).toContain(m.status)
41
+ }
42
+
43
+ console.log(
44
+ `Parsed: ${result.milestones.length} milestones, ${totalTasks} tasks, ${result.recent_work.length} work entries`,
45
+ )
46
+ })
47
+ })
@@ -0,0 +1,201 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { parseProgressYaml } from './yaml-loader'
3
+
4
+ describe('parseProgressYaml', () => {
5
+ it('parses a complete progress.yaml', () => {
6
+ const yaml = `
7
+ project:
8
+ name: test-project
9
+ version: 1.0.0
10
+ started: "2026-01-01"
11
+ status: in_progress
12
+ current_milestone: milestone_1
13
+ description: A test project
14
+
15
+ milestones:
16
+ - id: milestone_1
17
+ name: First Milestone
18
+ status: in_progress
19
+ progress: 50
20
+ started: "2026-01-01"
21
+ tasks_completed: 1
22
+ tasks_total: 2
23
+
24
+ tasks:
25
+ milestone_1:
26
+ - id: task_1
27
+ name: First Task
28
+ status: completed
29
+ file: agent/tasks/task-1.md
30
+ estimated_hours: "2"
31
+ completed_date: "2026-01-02"
32
+ - id: task_2
33
+ name: Second Task
34
+ status: not_started
35
+
36
+ recent_work:
37
+ - date: "2026-01-02"
38
+ description: Did stuff
39
+ items:
40
+ - item 1
41
+ - item 2
42
+
43
+ next_steps:
44
+ - Do next thing
45
+
46
+ notes:
47
+ - A note
48
+
49
+ current_blockers: []
50
+
51
+ progress:
52
+ planning: 100
53
+ implementation: 50
54
+ overall: 60
55
+ `
56
+ const result = parseProgressYaml(yaml)
57
+
58
+ expect(result.project.name).toBe('test-project')
59
+ expect(result.project.version).toBe('1.0.0')
60
+ expect(result.project.status).toBe('in_progress')
61
+ expect(result.milestones).toHaveLength(1)
62
+ expect(result.milestones[0].name).toBe('First Milestone')
63
+ expect(result.milestones[0].progress).toBe(50)
64
+ expect(result.tasks.milestone_1).toHaveLength(2)
65
+ expect(result.tasks.milestone_1[0].status).toBe('completed')
66
+ expect(result.tasks.milestone_1[1].status).toBe('not_started')
67
+ expect(result.recent_work).toHaveLength(1)
68
+ expect(result.recent_work[0].items).toEqual(['item 1', 'item 2'])
69
+ expect(result.next_steps).toEqual(['Do next thing'])
70
+ expect(result.progress.overall).toBe(60)
71
+ })
72
+
73
+ it('handles empty/minimal YAML', () => {
74
+ const result = parseProgressYaml('')
75
+ expect(result.project.name).toBe('Unknown')
76
+ expect(result.milestones).toEqual([])
77
+ expect(result.tasks).toEqual({})
78
+ })
79
+
80
+ it('handles totally invalid YAML', () => {
81
+ const result = parseProgressYaml('{{{{not yaml at all')
82
+ expect(result.project.name).toBe('Unknown')
83
+ expect(result.milestones).toEqual([])
84
+ })
85
+
86
+ it('handles missing sections gracefully', () => {
87
+ const yaml = `
88
+ project:
89
+ name: minimal
90
+ `
91
+ const result = parseProgressYaml(yaml)
92
+ expect(result.project.name).toBe('minimal')
93
+ expect(result.project.status).toBe('not_started')
94
+ expect(result.milestones).toEqual([])
95
+ expect(result.tasks).toEqual({})
96
+ expect(result.next_steps).toEqual([])
97
+ expect(result.notes).toEqual([])
98
+ expect(result.current_blockers).toEqual([])
99
+ })
100
+
101
+ it('fuzzy-matches status values', () => {
102
+ const yaml = `
103
+ milestones:
104
+ - id: m1
105
+ name: Done One
106
+ status: done
107
+ - id: m2
108
+ name: Active One
109
+ status: active
110
+ - id: m3
111
+ name: WIP One
112
+ status: wip
113
+ - id: m4
114
+ name: In Progress One
115
+ status: "in progress"
116
+ - id: m5
117
+ name: Complete One
118
+ status: complete
119
+ - id: m6
120
+ name: Started One
121
+ status: started
122
+ `
123
+ const result = parseProgressYaml(yaml)
124
+ expect(result.milestones[0].status).toBe('completed')
125
+ expect(result.milestones[1].status).toBe('in_progress')
126
+ expect(result.milestones[2].status).toBe('in_progress')
127
+ expect(result.milestones[3].status).toBe('in_progress')
128
+ expect(result.milestones[4].status).toBe('completed')
129
+ expect(result.milestones[5].status).toBe('in_progress')
130
+ })
131
+
132
+ it('preserves unknown properties in extra fields', () => {
133
+ const yaml = `
134
+ project:
135
+ name: test
136
+ custom_field: hello
137
+ priority: high
138
+
139
+ milestones:
140
+ - id: m1
141
+ name: Test
142
+ status: in_progress
143
+ risk_level: high
144
+ owner: alice
145
+ `
146
+ const result = parseProgressYaml(yaml)
147
+ expect(result.project.extra).toEqual({ custom_field: 'hello', priority: 'high' })
148
+ expect(result.milestones[0].extra).toEqual({ risk_level: 'high', owner: 'alice' })
149
+ })
150
+
151
+ it('resolves key aliases', () => {
152
+ const yaml = `
153
+ tasks:
154
+ m1:
155
+ - id: t1
156
+ title: Aliased Name
157
+ est_hours: "4"
158
+ done_date: "2026-01-05"
159
+ path: agent/tasks/t1.md
160
+ `
161
+ const result = parseProgressYaml(yaml)
162
+ const task = result.tasks.m1[0]
163
+ expect(task.name).toBe('Aliased Name')
164
+ expect(task.estimated_hours).toBe('4')
165
+ expect(task.completed_date).toBe('2026-01-05')
166
+ expect(task.file).toBe('agent/tasks/t1.md')
167
+ })
168
+
169
+ it('coerces string-for-array', () => {
170
+ const yaml = `
171
+ next_steps: "just one step"
172
+ notes: "single note"
173
+ `
174
+ const result = parseProgressYaml(yaml)
175
+ expect(result.next_steps).toEqual(['just one step'])
176
+ expect(result.notes).toEqual(['single note'])
177
+ })
178
+
179
+ it('generates IDs when missing', () => {
180
+ const yaml = `
181
+ milestones:
182
+ - name: No ID Milestone
183
+ status: not_started
184
+ `
185
+ const result = parseProgressYaml(yaml)
186
+ expect(result.milestones[0].id).toBe('milestone_1')
187
+ })
188
+
189
+ it('handles null values in dates', () => {
190
+ const yaml = `
191
+ milestones:
192
+ - id: m1
193
+ name: Test
194
+ started: null
195
+ completed: null
196
+ `
197
+ const result = parseProgressYaml(yaml)
198
+ expect(result.milestones[0].started).toBeNull()
199
+ expect(result.milestones[0].completed).toBeNull()
200
+ })
201
+ })
@@ -0,0 +1,265 @@
1
+ import yaml from 'js-yaml'
2
+ import type {
3
+ ProgressData,
4
+ ProjectMetadata,
5
+ Milestone,
6
+ Task,
7
+ WorkEntry,
8
+ DocumentationStats,
9
+ ProgressSummary,
10
+ Status,
11
+ ExtraFields,
12
+ } from './types'
13
+
14
+ // --- Key alias maps for agent drift ---
15
+
16
+ const PROJECT_ALIASES: Record<string, string> = {
17
+ project_name: 'name',
18
+ title: 'name',
19
+ }
20
+
21
+ const MILESTONE_ALIASES: Record<string, string> = {
22
+ title: 'name',
23
+ est_weeks: 'estimated_weeks',
24
+ weeks: 'estimated_weeks',
25
+ tasks_done: 'tasks_completed',
26
+ total_tasks: 'tasks_total',
27
+ }
28
+
29
+ const TASK_ALIASES: Record<string, string> = {
30
+ title: 'name',
31
+ est_hours: 'estimated_hours',
32
+ hours: 'estimated_hours',
33
+ estimate: 'estimated_hours',
34
+ completed: 'completed_date',
35
+ done_date: 'completed_date',
36
+ filename: 'file',
37
+ path: 'file',
38
+ milestone: 'milestone_id',
39
+ }
40
+
41
+ // --- Core helpers ---
42
+
43
+ function normalizeStatus(value: unknown): Status {
44
+ const s = String(value || 'not_started')
45
+ .toLowerCase()
46
+ .replace(/[\s-]/g, '_')
47
+ if (s === 'completed' || s === 'done' || s === 'complete') return 'completed'
48
+ if (s === 'in_progress' || s === 'active' || s === 'wip' || s === 'started') return 'in_progress'
49
+ return 'not_started'
50
+ }
51
+
52
+ function safeString(value: unknown, fallback = ''): string {
53
+ if (value == null) return fallback
54
+ return String(value)
55
+ }
56
+
57
+ function safeNumber(value: unknown, fallback = 0): number {
58
+ const n = Number(value)
59
+ return Number.isFinite(n) ? n : fallback
60
+ }
61
+
62
+ function normalizeStringArray(value: unknown): string[] {
63
+ if (!value) return []
64
+ if (typeof value === 'string') return [value]
65
+ if (!Array.isArray(value)) return []
66
+ return value.map(String)
67
+ }
68
+
69
+ function resolveAliases(
70
+ obj: Record<string, unknown>,
71
+ aliases: Record<string, string>,
72
+ ): Record<string, unknown> {
73
+ const resolved: Record<string, unknown> = {}
74
+ for (const [key, value] of Object.entries(obj)) {
75
+ const canonical = aliases[key] || key
76
+ // Don't overwrite if canonical already set
77
+ if (!(canonical in resolved)) {
78
+ resolved[canonical] = value
79
+ }
80
+ }
81
+ return resolved
82
+ }
83
+
84
+ function extractKnown(
85
+ obj: Record<string, unknown>,
86
+ knownKeys: string[],
87
+ ): { known: Record<string, unknown>; extra: ExtraFields } {
88
+ const known: Record<string, unknown> = {}
89
+ const extra: ExtraFields = {}
90
+ for (const [key, value] of Object.entries(obj)) {
91
+ if (knownKeys.includes(key)) {
92
+ known[key] = value
93
+ } else {
94
+ extra[key] = value
95
+ }
96
+ }
97
+ return { known, extra }
98
+ }
99
+
100
+ function asRecord(value: unknown): Record<string, unknown> {
101
+ if (value && typeof value === 'object' && !Array.isArray(value)) {
102
+ return value as Record<string, unknown>
103
+ }
104
+ return {}
105
+ }
106
+
107
+ // --- Entity normalizers ---
108
+
109
+ const PROJECT_KEYS = ['name', 'version', 'started', 'status', 'current_milestone', 'description']
110
+
111
+ function normalizeProject(raw: unknown): ProjectMetadata {
112
+ const obj = resolveAliases(asRecord(raw), PROJECT_ALIASES)
113
+ const { known, extra } = extractKnown(obj, PROJECT_KEYS)
114
+ return {
115
+ name: safeString(known.name, 'Untitled Project'),
116
+ version: safeString(known.version, '0.0.0'),
117
+ started: safeString(known.started),
118
+ status: normalizeStatus(known.status),
119
+ current_milestone: known.current_milestone ? safeString(known.current_milestone) : undefined,
120
+ description: safeString(known.description),
121
+ extra,
122
+ }
123
+ }
124
+
125
+ const MILESTONE_KEYS = [
126
+ 'id', 'name', 'status', 'progress', 'started', 'completed',
127
+ 'estimated_weeks', 'tasks_completed', 'tasks_total', 'notes',
128
+ ]
129
+
130
+ function normalizeMilestone(raw: unknown, index: number): Milestone {
131
+ const obj = resolveAliases(asRecord(raw), MILESTONE_ALIASES)
132
+ const { known, extra } = extractKnown(obj, MILESTONE_KEYS)
133
+ return {
134
+ id: safeString(known.id, `milestone_${index + 1}`),
135
+ name: safeString(known.name, `Milestone ${index + 1}`),
136
+ status: normalizeStatus(known.status),
137
+ progress: safeNumber(known.progress),
138
+ started: known.started ? safeString(known.started) : null,
139
+ completed: known.completed ? safeString(known.completed) : null,
140
+ estimated_weeks: safeString(known.estimated_weeks, '0'),
141
+ tasks_completed: safeNumber(known.tasks_completed),
142
+ tasks_total: safeNumber(known.tasks_total),
143
+ notes: safeString(known.notes),
144
+ extra,
145
+ }
146
+ }
147
+
148
+ function normalizeMilestones(raw: unknown): Milestone[] {
149
+ if (!Array.isArray(raw)) return []
150
+ return raw.map((item, i) => normalizeMilestone(item, i))
151
+ }
152
+
153
+ const TASK_KEYS = [
154
+ 'id', 'name', 'status', 'milestone_id', 'file',
155
+ 'estimated_hours', 'completed_date', 'notes',
156
+ ]
157
+
158
+ function normalizeTask(raw: unknown, milestoneId: string, index: number): Task {
159
+ const obj = resolveAliases(asRecord(raw), TASK_ALIASES)
160
+ const { known, extra } = extractKnown(obj, TASK_KEYS)
161
+ return {
162
+ id: safeString(known.id, `task_${index + 1}`),
163
+ name: safeString(known.name, `Task ${index + 1}`),
164
+ status: normalizeStatus(known.status),
165
+ milestone_id: safeString(known.milestone_id, milestoneId),
166
+ file: safeString(known.file),
167
+ estimated_hours: safeString(known.estimated_hours, '0'),
168
+ completed_date: known.completed_date ? safeString(known.completed_date) : null,
169
+ notes: safeString(known.notes),
170
+ extra,
171
+ }
172
+ }
173
+
174
+ function normalizeTasks(raw: unknown): Record<string, Task[]> {
175
+ const result: Record<string, Task[]> = {}
176
+ const obj = asRecord(raw)
177
+ for (const [milestoneId, tasks] of Object.entries(obj)) {
178
+ if (Array.isArray(tasks)) {
179
+ result[milestoneId] = tasks.map((t, i) => normalizeTask(t, milestoneId, i))
180
+ }
181
+ }
182
+ return result
183
+ }
184
+
185
+ function normalizeWorkEntry(raw: unknown): WorkEntry {
186
+ const obj = asRecord(raw)
187
+ const knownKeys = ['date', 'description', 'items']
188
+ const { known, extra } = extractKnown(obj, knownKeys)
189
+ return {
190
+ date: safeString(known.date),
191
+ description: safeString(known.description),
192
+ items: normalizeStringArray(known.items),
193
+ extra,
194
+ }
195
+ }
196
+
197
+ function normalizeWorkEntries(raw: unknown): WorkEntry[] {
198
+ if (!Array.isArray(raw)) return []
199
+ return raw.map(normalizeWorkEntry)
200
+ }
201
+
202
+ function normalizeDocStats(raw: unknown): DocumentationStats {
203
+ const obj = asRecord(raw)
204
+ return {
205
+ design_documents: safeNumber(obj.design_documents),
206
+ milestone_documents: safeNumber(obj.milestone_documents),
207
+ pattern_documents: safeNumber(obj.pattern_documents),
208
+ task_documents: safeNumber(obj.task_documents),
209
+ }
210
+ }
211
+
212
+ function normalizeProgress(raw: unknown): ProgressSummary {
213
+ const obj = asRecord(raw)
214
+ return {
215
+ planning: safeNumber(obj.planning),
216
+ implementation: safeNumber(obj.implementation),
217
+ overall: safeNumber(obj.overall),
218
+ }
219
+ }
220
+
221
+ // --- Main parser ---
222
+
223
+ const EMPTY_PROGRESS_DATA: ProgressData = {
224
+ project: {
225
+ name: 'Unknown',
226
+ version: '0.0.0',
227
+ started: '',
228
+ status: 'not_started',
229
+ description: '',
230
+ extra: {},
231
+ },
232
+ milestones: [],
233
+ tasks: {},
234
+ recent_work: [],
235
+ next_steps: [],
236
+ notes: [],
237
+ current_blockers: [],
238
+ documentation: { design_documents: 0, milestone_documents: 0, pattern_documents: 0, task_documents: 0 },
239
+ progress: { planning: 0, implementation: 0, overall: 0 },
240
+ }
241
+
242
+ export function parseProgressYaml(raw: string): ProgressData {
243
+ try {
244
+ // json: true allows duplicated keys (last wins) — common in agent-maintained YAML
245
+ const doc = yaml.load(raw, { json: true })
246
+ if (!doc || typeof doc !== 'object') {
247
+ return { ...EMPTY_PROGRESS_DATA }
248
+ }
249
+ const d = doc as Record<string, unknown>
250
+
251
+ return {
252
+ project: normalizeProject(d.project),
253
+ milestones: normalizeMilestones(d.milestones),
254
+ tasks: normalizeTasks(d.tasks),
255
+ recent_work: normalizeWorkEntries(d.recent_work),
256
+ next_steps: normalizeStringArray(d.next_steps),
257
+ notes: normalizeStringArray(d.notes),
258
+ current_blockers: normalizeStringArray(d.current_blockers),
259
+ documentation: normalizeDocStats(d.documentation),
260
+ progress: normalizeProgress(d.progress),
261
+ }
262
+ } catch {
263
+ return { ...EMPTY_PROGRESS_DATA }
264
+ }
265
+ }