bmad-dashboard 1.0.14 → 1.0.16

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": "bmad-dashboard",
3
- "version": "1.0.14",
3
+ "version": "1.0.16",
4
4
  "description": "Local dashboard to view BMAD project state (phase, artifacts, stories, sprint) in real time",
5
5
  "type": "module",
6
6
  "bin": {
@@ -11,7 +11,7 @@ import {
11
11
 
12
12
  const ARCH_STEP_IDS = [1, 2, 3, 4, 5, 6, 7, 8]
13
13
 
14
- /** Match a single AC line: bold type (Given/When/Then/And) or plain type, then text. Support both - and * bullets. */
14
+ /** Match AC line: bold or plain Given/When/Then/And; support - and * bullets */
15
15
  const AC_LINE_BOLD = /^[-*]\s*\*\*(Given|When|Then|And)\s*:?\s*(?:\*\*\s*)?(.+)$/
16
16
  const AC_LINE_PLAIN = /^[-*]\s*(Given|When|Then|And)\s*:?\s*(.+)$/
17
17
 
@@ -20,7 +20,6 @@ function parseStoryBody(body, num, title, statusByKey) {
20
20
  const rest = (body || '').replace(/\r\n/g, '\n').replace(/\r/g, '\n').trim()
21
21
  let userStory = ''
22
22
  const acceptanceCriteria = []
23
- // Multiple possible markers for start of acceptance criteria block (case-insensitive)
24
23
  const acMarkers = [
25
24
  /\*\*Acceptance Criteria\s*:?\s*\*\*/i,
26
25
  /\*\*Acceptance criteria\s*:?\s*\*\*/i,
@@ -28,6 +27,8 @@ function parseStoryBody(body, num, title, statusByKey) {
28
27
  /^Acceptance criteria\s*:?\s*$/im,
29
28
  /^#+\s*Acceptance Criteria\s*:?\s*$/im,
30
29
  /^#+\s*Acceptance criteria\s*:?\s*$/im,
30
+ /^Acceptance Criteria\s*:?\s*\n/im,
31
+ /\bAC\s*:?\s*\n/im,
31
32
  ]
32
33
  let acIndex = -1
33
34
  let acBlockStart = 0
@@ -47,14 +48,18 @@ function parseStoryBody(body, num, title, statusByKey) {
47
48
  const acBlock = rest.slice(acBlockStart).trim()
48
49
  const acLines = acBlock.split(/\n/).map((line) => line.trim()).filter(Boolean)
49
50
  for (const line of acLines) {
50
- if (line.startsWith('###')) break // next story or heading
51
+ if (line.startsWith('###')) break
51
52
  let m = line.match(AC_LINE_BOLD)
52
53
  if (!m) m = line.match(AC_LINE_PLAIN)
53
54
  if (m) acceptanceCriteria.push({ type: m[1], text: m[2].trim() })
54
55
  else if ((line.startsWith('- ') || line.startsWith('* ')) && line.length > 2)
55
56
  acceptanceCriteria.push({ type: null, text: line.slice(2).trim() })
57
+ else {
58
+ const numBullet = line.match(/^\d+\.\s+(.+)$/)
59
+ if (numBullet && numBullet[1].trim().length > 0)
60
+ acceptanceCriteria.push({ type: null, text: numBullet[1].trim() })
61
+ }
56
62
  }
57
- // If we found AC header but no criteria matched, take any list items in the block
58
63
  if (acceptanceCriteria.length === 0 && acBlock.length > 0) {
59
64
  for (const line of acLines) {
60
65
  if (line.startsWith('###')) break
@@ -64,18 +69,24 @@ function parseStoryBody(body, num, title, statusByKey) {
64
69
  }
65
70
  }
66
71
  }
67
- // Fallback: no AC header but body has bullet list that looks like criteria (Given/When/Then/And)
68
72
  if (acceptanceCriteria.length === 0 && rest) {
69
73
  const lines = rest.split(/\n/)
70
74
  for (const line of lines) {
71
75
  const t = line.trim()
76
+ if (t.startsWith('###')) continue
72
77
  let m = t.match(AC_LINE_BOLD)
73
78
  if (!m) m = t.match(AC_LINE_PLAIN)
74
79
  if (m) acceptanceCriteria.push({ type: m[1], text: m[2].trim() })
75
80
  else if ((t.startsWith('- ') || t.startsWith('* ')) && t.length > 2)
76
81
  acceptanceCriteria.push({ type: null, text: t.slice(2).trim() })
82
+ else {
83
+ const numBullet = t.match(/^\d+\.\s+(.+)$/)
84
+ if (numBullet && numBullet[1].trim().length > 0)
85
+ acceptanceCriteria.push({ type: null, text: numBullet[1].trim() })
86
+ }
77
87
  }
78
88
  }
89
+
79
90
  if (!userStory && rest) {
80
91
  const first = rest.split(/\n\n+/)[0]
81
92
  if (first && first.length > 10) userStory = first.replace(/\n/g, ' ').trim()
@@ -218,9 +229,9 @@ export function getArtifactDetails(projectRootParam, planPath, implPath, sprintD
218
229
 
219
230
  for (let i = 0; i < epicTitles.length; i++) {
220
231
  const block = epicBlocks[i] || ''
221
- const storyRe = /^### Story (\d+)\.(\d+):\s*(.+)$/gm
232
+ const storyRe = /^### Story (\d+)\.(\d+)\s*[:-]\s*(.+)$/gm
222
233
  const storyMatches = [...block.matchAll(storyRe)]
223
- const rawBodies = block.split(/^### Story \d+\.\d+:/m).slice(1)
234
+ const rawBodies = block.split(/^### Story \d+\.\d+\s*[:-]\s*/m).slice(1)
224
235
  const stories = storyMatches.map((m, j) => {
225
236
  const num = `${m[1]}.${m[2]}`
226
237
  const title = m[3].trim()