bmad-viewer 0.3.0 → 0.3.2

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-viewer",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "Visual dashboard for BMAD (Boring Maintainable Agile Development) projects. Wiki browser + sprint status viewer with live reload.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -369,10 +369,21 @@ function buildProjectData(outputPath, aggregator) {
369
369
  ...content,
370
370
  });
371
371
 
372
- // Parse stories from epics.md
372
+ // Parse stories and epics from epics.md
373
373
  if (name === 'epics' && ext === '.md' && content.raw) {
374
374
  const storyContents = parseStoriesFromEpics(content.raw, aggregator);
375
375
  project.storyContents = storyContents;
376
+
377
+ // Build epics from markdown when no sprint-status.yaml was found
378
+ if (project.epics.length === 0) {
379
+ project.epics = parseEpicsFromMarkdown(content.raw, storyContents);
380
+ // Build story stats from epics.md stories
381
+ for (const epic of project.epics) {
382
+ project.stories.total += epic.stories.length;
383
+ project.stories.pending += epic.stories.length; // all backlog by default
384
+ project.storyList.push(...epic.stories);
385
+ }
386
+ }
376
387
  }
377
388
  }
378
389
  }
@@ -539,6 +550,36 @@ function parseStoriesFromEpics(raw, aggregator) {
539
550
  return storyMap;
540
551
  }
541
552
 
553
+ /**
554
+ * Parse epics and their stories from epics.md markdown.
555
+ * Epics follow the pattern: ## Epic N: Title
556
+ * Stories follow the pattern: ### Story N.M: Title
557
+ */
558
+ function parseEpicsFromMarkdown(raw, storyContents) {
559
+ const epicRegex = /^## Epic (\d+):\s*(.+)$/gm;
560
+ const epicMap = {};
561
+ let match;
562
+
563
+ while ((match = epicRegex.exec(raw)) !== null) {
564
+ const num = match[1];
565
+ const name = match[2].trim();
566
+ epicMap[num] = { id: `epic-${num}`, num, name, status: 'backlog', stories: [] };
567
+ }
568
+
569
+ // Assign stories to their epics
570
+ for (const [key, storyData] of Object.entries(storyContents)) {
571
+ const epicNum = key.split('-')[0];
572
+ const story = { id: key, title: storyData.title, status: 'backlog', epic: epicNum };
573
+ if (epicMap[epicNum]) {
574
+ epicMap[epicNum].stories.push(story);
575
+ } else {
576
+ epicMap[epicNum] = { id: `epic-${epicNum}`, num: epicNum, name: `Epic ${epicNum}`, status: 'backlog', stories: [story] };
577
+ }
578
+ }
579
+
580
+ return Object.values(epicMap).sort((a, b) => Number(a.num) - Number(b.num));
581
+ }
582
+
542
583
  /**
543
584
  * Parse epic names from YAML comments like "# Epic 1: Fundación del Proyecto"
544
585
  */
@@ -26,12 +26,14 @@ export function parseYaml(filePath) {
26
26
  */
27
27
  export function parseYamlContent(content, source = 'unknown') {
28
28
  try {
29
- const data = yaml.load(content);
29
+ const docs = yaml.loadAll(content);
30
+ const nonNull = docs.filter(d => d !== undefined && d !== null);
30
31
 
31
- if (data === undefined || data === null) {
32
+ if (nonNull.length === 0) {
32
33
  return createResult(null, [], [`Empty YAML content in ${source}`]);
33
34
  }
34
35
 
36
+ const data = nonNull.length === 1 ? nonNull[0] : Object.assign({}, ...nonNull);
35
37
  return createResult(data, [], []);
36
38
  } catch (error) {
37
39
  const warnings = [`Failed to parse ${source}`];