@prmichaelsen/acp-visualizer 0.1.5 → 0.1.7

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@prmichaelsen/acp-visualizer",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "type": "module",
5
5
  "description": "Browser-based dashboard for visualizing ACP progress.yaml data",
6
6
  "bin": {
@@ -1,6 +1,3 @@
1
- import { watch } from 'fs'
2
- import { getProgressYamlPath } from './config'
3
-
4
1
  type Controller = ReadableStreamDefaultController
5
2
 
6
3
  let watcher: {
@@ -8,9 +5,12 @@ let watcher: {
8
5
  removeClient: (controller: Controller) => void
9
6
  } | null = null
10
7
 
11
- export function getFileWatcher() {
8
+ export async function getFileWatcher() {
12
9
  if (watcher) return watcher
13
10
 
11
+ const { watch } = await import('fs')
12
+ const { getProgressYamlPath } = await import('./config')
13
+
14
14
  const filePath = getProgressYamlPath()
15
15
  const clients = new Set<Controller>()
16
16
 
@@ -186,6 +186,34 @@ milestones:
186
186
  expect(result.milestones[0].id).toBe('milestone_1')
187
187
  })
188
188
 
189
+ it('maps task keys to milestone IDs when formats differ', () => {
190
+ const yaml = `
191
+ milestones:
192
+ - id: M1
193
+ name: First
194
+ status: completed
195
+ - id: M2
196
+ name: Second
197
+ status: in_progress
198
+
199
+ tasks:
200
+ milestone_1:
201
+ - id: t1
202
+ name: Task A
203
+ status: completed
204
+ milestone_2:
205
+ - id: t2
206
+ name: Task B
207
+ status: in_progress
208
+ `
209
+ const result = parseProgressYaml(yaml)
210
+ // Tasks keyed as milestone_1 should map to milestone ID M1
211
+ expect(result.tasks['M1']).toHaveLength(1)
212
+ expect(result.tasks['M1'][0].name).toBe('Task A')
213
+ expect(result.tasks['M2']).toHaveLength(1)
214
+ expect(result.tasks['M2'][0].name).toBe('Task B')
215
+ })
216
+
189
217
  it('handles null values in dates', () => {
190
218
  const yaml = `
191
219
  milestones:
@@ -35,6 +35,7 @@ const TASK_ALIASES: Record<string, string> = {
35
35
  done_date: 'completed_date',
36
36
  filename: 'file',
37
37
  path: 'file',
38
+ document: 'file',
38
39
  milestone: 'milestone_id',
39
40
  }
40
41
 
@@ -171,12 +172,35 @@ function normalizeTask(raw: unknown, milestoneId: string, index: number): Task {
171
172
  }
172
173
  }
173
174
 
174
- function normalizeTasks(raw: unknown): Record<string, Task[]> {
175
+ function normalizeTasks(raw: unknown, milestones: Milestone[]): Record<string, Task[]> {
175
176
  const result: Record<string, Task[]> = {}
176
177
  const obj = asRecord(raw)
177
- for (const [milestoneId, tasks] of Object.entries(obj)) {
178
+
179
+ // Build a map from task key patterns to milestone IDs.
180
+ // Handles mismatch: tasks keyed as "milestone_1" but milestone.id = "M1"
181
+ const keyToMilestoneId = new Map<string, string>()
182
+ for (let i = 0; i < milestones.length; i++) {
183
+ const m = milestones[i]
184
+ // The task key might be the milestone ID itself, or "milestone_N"
185
+ keyToMilestoneId.set(m.id, m.id)
186
+ keyToMilestoneId.set(m.id.toLowerCase(), m.id)
187
+ keyToMilestoneId.set(`milestone_${i + 1}`, m.id)
188
+ // Also handle "milestone_N" where N matches the numeric part of "MN"
189
+ const numMatch = m.id.match(/(\d+)/)
190
+ if (numMatch) {
191
+ keyToMilestoneId.set(`milestone_${numMatch[1]}`, m.id)
192
+ }
193
+ }
194
+
195
+ for (const [rawKey, tasks] of Object.entries(obj)) {
178
196
  if (Array.isArray(tasks)) {
179
- result[milestoneId] = tasks.map((t, i) => normalizeTask(t, milestoneId, i))
197
+ // Resolve the key to a milestone ID, or keep as-is
198
+ const milestoneId = keyToMilestoneId.get(rawKey) || rawKey
199
+ const existing = result[milestoneId] || []
200
+ result[milestoneId] = [
201
+ ...existing,
202
+ ...tasks.map((t, i) => normalizeTask(t, milestoneId, existing.length + i)),
203
+ ]
180
204
  }
181
205
  }
182
206
  return result
@@ -248,10 +272,12 @@ export function parseProgressYaml(raw: string): ProgressData {
248
272
  }
249
273
  const d = doc as Record<string, unknown>
250
274
 
275
+ const milestones = normalizeMilestones(d.milestones)
276
+
251
277
  return {
252
278
  project: normalizeProject(d.project),
253
- milestones: normalizeMilestones(d.milestones),
254
- tasks: normalizeTasks(d.tasks),
279
+ milestones,
280
+ tasks: normalizeTasks(d.tasks, milestones),
255
281
  recent_work: normalizeWorkEntries(d.recent_work),
256
282
  next_steps: normalizeStringArray(d.next_steps),
257
283
  notes: normalizeStringArray(d.notes),
@@ -2,7 +2,7 @@ import { HeadContent, Scripts, createRootRoute, Outlet } from '@tanstack/react-r
2
2
  import { useAutoRefresh } from '../lib/useAutoRefresh'
3
3
  import { Sidebar } from '../components/Sidebar'
4
4
  import { Header } from '../components/Header'
5
- import { ProgressDatabaseService } from '../services/progress-database.service'
5
+ import { getProgressData } from '../services/progress-database.service'
6
6
  import type { ProgressData } from '../lib/types'
7
7
 
8
8
  import appCss from '../styles.css?url'
@@ -12,7 +12,7 @@ export const Route = createRootRoute({
12
12
  let progressData: ProgressData | null = null
13
13
 
14
14
  try {
15
- const result = ProgressDatabaseService.getProgressData()
15
+ const result = await getProgressData()
16
16
  if (result.ok) {
17
17
  progressData = result.data
18
18
  }
@@ -5,7 +5,7 @@ export const Route = createFileRoute('/api/watch')({
5
5
  server: {
6
6
  handlers: {
7
7
  GET: async () => {
8
- const watcher = getFileWatcher()
8
+ const watcher = await getFileWatcher()
9
9
 
10
10
  const stream = new ReadableStream({
11
11
  start(controller) {
@@ -1,14 +1,17 @@
1
- import { readFileSync } from 'fs'
2
- import { parseProgressYaml } from '../lib/yaml-loader'
3
- import { getProgressYamlPath } from '../lib/config'
1
+ import { createServerFn } from '@tanstack/react-start'
4
2
  import type { ProgressData } from '../lib/types'
5
3
 
6
4
  export type ProgressResult =
7
5
  | { ok: true; data: ProgressData }
8
6
  | { ok: false; error: 'FILE_NOT_FOUND' | 'PARSE_ERROR'; message: string; path: string }
9
7
 
10
- export class ProgressDatabaseService {
11
- static getProgressData(): ProgressResult {
8
+ export const getProgressData = createServerFn({ method: 'GET' }).handler(
9
+ async (): Promise<ProgressResult> => {
10
+ // Dynamic imports keep fs and yaml-loader out of the client bundle
11
+ const { readFileSync } = await import('fs')
12
+ const { parseProgressYaml } = await import('../lib/yaml-loader')
13
+ const { getProgressYamlPath } = await import('../lib/config')
14
+
12
15
  const filePath = getProgressYamlPath()
13
16
 
14
17
  try {
@@ -42,5 +45,5 @@ export class ProgressDatabaseService {
42
45
  path: filePath,
43
46
  }
44
47
  }
45
- }
46
- }
48
+ },
49
+ )