mrvn-cli 0.4.5 → 0.4.6

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/dist/index.js CHANGED
@@ -15723,6 +15723,23 @@ function getUpcomingData(store) {
15723
15723
  }
15724
15724
 
15725
15725
  // src/web/templates/layout.ts
15726
+ function collapsibleSection(sectionId, title, content, opts) {
15727
+ const tag = opts?.titleTag ?? "div";
15728
+ const cls = opts?.titleClass ?? "section-title";
15729
+ const collapsed = opts?.defaultCollapsed ? " collapsed" : "";
15730
+ return `
15731
+ <div class="collapsible${collapsed}" data-section-id="${escapeHtml(sectionId)}">
15732
+ <${tag} class="${cls} collapsible-header" onclick="toggleSection(this)">
15733
+ <svg class="collapsible-chevron" viewBox="0 0 16 16" width="14" height="14" fill="currentColor">
15734
+ <path d="M4.94 5.72a.75.75 0 0 1 1.06-.02L8 7.56l1.97-1.84a.75.75 0 1 1 1.02 1.1l-2.5 2.34a.75.75 0 0 1-1.02 0l-2.5-2.34a.75.75 0 0 1-.03-1.06z"/>
15735
+ </svg>
15736
+ <span>${title}</span>
15737
+ </${tag}>
15738
+ <div class="collapsible-body">
15739
+ ${content}
15740
+ </div>
15741
+ </div>`;
15742
+ }
15726
15743
  function escapeHtml(str) {
15727
15744
  return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
15728
15745
  }
@@ -15890,6 +15907,32 @@ function layout(opts, body) {
15890
15907
  ${body}
15891
15908
  </main>
15892
15909
  </div>
15910
+ <script>
15911
+ function toggleSection(header) {
15912
+ var section = header.closest('.collapsible');
15913
+ if (!section) return;
15914
+ section.classList.toggle('collapsed');
15915
+ var id = section.getAttribute('data-section-id');
15916
+ if (id) {
15917
+ try {
15918
+ var state = JSON.parse(localStorage.getItem('marvin-collapsed') || '{}');
15919
+ state[id] = section.classList.contains('collapsed');
15920
+ localStorage.setItem('marvin-collapsed', JSON.stringify(state));
15921
+ } catch(e) {}
15922
+ }
15923
+ }
15924
+ // Restore collapsed state on load
15925
+ (function() {
15926
+ try {
15927
+ var state = JSON.parse(localStorage.getItem('marvin-collapsed') || '{}');
15928
+ document.querySelectorAll('.collapsible[data-section-id]').forEach(function(el) {
15929
+ var id = el.getAttribute('data-section-id');
15930
+ if (state[id] === true) el.classList.add('collapsed');
15931
+ else if (state[id] === false) el.classList.remove('collapsed');
15932
+ });
15933
+ } catch(e) {}
15934
+ })();
15935
+ </script>
15893
15936
  <script src="https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js"></script>
15894
15937
  <script>mermaid.initialize({
15895
15938
  startOnLoad: true,
@@ -16713,13 +16756,60 @@ tr:hover td {
16713
16756
  white-space: nowrap;
16714
16757
  }
16715
16758
 
16759
+ .gantt-grid-line {
16760
+ position: absolute;
16761
+ top: 0;
16762
+ bottom: 0;
16763
+ width: 1px;
16764
+ background: var(--border);
16765
+ opacity: 0.35;
16766
+ }
16767
+
16768
+ .gantt-sprint-line {
16769
+ position: absolute;
16770
+ top: 0;
16771
+ bottom: 0;
16772
+ width: 1px;
16773
+ background: var(--text-dim);
16774
+ opacity: 0.3;
16775
+ }
16776
+
16716
16777
  .gantt-today {
16717
16778
  position: absolute;
16718
16779
  top: 0;
16719
16780
  bottom: 0;
16720
- width: 2px;
16781
+ width: 3px;
16721
16782
  background: var(--red);
16722
- opacity: 0.7;
16783
+ opacity: 0.8;
16784
+ border-radius: 1px;
16785
+ }
16786
+
16787
+ /* Sprint band in timeline */
16788
+ .gantt-sprint-band-row {
16789
+ border-bottom: 1px solid var(--border);
16790
+ margin-bottom: 0.25rem;
16791
+ }
16792
+
16793
+ .gantt-sprint-band {
16794
+ height: 32px;
16795
+ }
16796
+
16797
+ .gantt-sprint-block {
16798
+ position: absolute;
16799
+ top: 2px;
16800
+ bottom: 2px;
16801
+ background: var(--bg-hover);
16802
+ border: 1px solid var(--border);
16803
+ border-radius: 4px;
16804
+ font-size: 0.65rem;
16805
+ color: var(--text-dim);
16806
+ display: flex;
16807
+ align-items: center;
16808
+ justify-content: center;
16809
+ overflow: hidden;
16810
+ white-space: nowrap;
16811
+ text-overflow: ellipsis;
16812
+ padding: 0 0.4rem;
16723
16813
  }
16724
16814
 
16725
16815
  /* Pie chart color overrides */
@@ -16781,6 +16871,40 @@ tr:hover td {
16781
16871
  }
16782
16872
 
16783
16873
  .text-dim { color: var(--text-dim); }
16874
+
16875
+ /* Collapsible sections */
16876
+ .collapsible-header {
16877
+ cursor: pointer;
16878
+ display: flex;
16879
+ align-items: center;
16880
+ gap: 0.4rem;
16881
+ user-select: none;
16882
+ }
16883
+
16884
+ .collapsible-header:hover {
16885
+ color: var(--accent);
16886
+ }
16887
+
16888
+ .collapsible-chevron {
16889
+ transition: transform 0.2s ease;
16890
+ flex-shrink: 0;
16891
+ }
16892
+
16893
+ .collapsible.collapsed .collapsible-chevron {
16894
+ transform: rotate(-90deg);
16895
+ }
16896
+
16897
+ .collapsible-body {
16898
+ overflow: hidden;
16899
+ max-height: 5000px;
16900
+ transition: max-height 0.3s ease, opacity 0.2s ease;
16901
+ opacity: 1;
16902
+ }
16903
+
16904
+ .collapsible.collapsed .collapsible-body {
16905
+ max-height: 0;
16906
+ opacity: 0;
16907
+ }
16784
16908
  `;
16785
16909
  }
16786
16910
 
@@ -16833,35 +16957,73 @@ function buildTimelineGantt(data, maxSprints = 6) {
16833
16957
  );
16834
16958
  tick += 7 * DAY;
16835
16959
  }
16960
+ const gridLines = [];
16961
+ let gridTick = timelineStart;
16962
+ const gridStartDay = new Date(gridTick).getDay();
16963
+ gridTick += (8 - gridStartDay) % 7 * DAY;
16964
+ while (gridTick <= timelineEnd) {
16965
+ gridLines.push(`<div class="gantt-grid-line" style="left:${pct(gridTick).toFixed(2)}%"></div>`);
16966
+ gridTick += 7 * DAY;
16967
+ }
16968
+ const sprintBoundaries = /* @__PURE__ */ new Set();
16969
+ for (const sprint of visibleSprints) {
16970
+ sprintBoundaries.add(toMs(sprint.startDate));
16971
+ sprintBoundaries.add(toMs(sprint.endDate));
16972
+ }
16973
+ const sprintLines = [...sprintBoundaries].map(
16974
+ (ms) => `<div class="gantt-sprint-line" style="left:${pct(ms).toFixed(2)}%"></div>`
16975
+ );
16836
16976
  const now = Date.now();
16837
16977
  let todayMarker = "";
16838
16978
  if (now >= timelineStart && now <= timelineEnd) {
16839
16979
  todayMarker = `<div class="gantt-today" style="left:${pct(now).toFixed(2)}%"></div>`;
16840
16980
  }
16841
- const rows = [];
16981
+ const sprintBlocks = visibleSprints.map((sprint) => {
16982
+ const sStart = toMs(sprint.startDate);
16983
+ const sEnd = toMs(sprint.endDate);
16984
+ const left = pct(sStart).toFixed(2);
16985
+ const width = (pct(sEnd) - pct(sStart)).toFixed(2);
16986
+ return `<div class="gantt-sprint-block" style="left:${left}%;width:${width}%">${sanitize(sprint.id, 20)}</div>`;
16987
+ }).join("");
16988
+ const sprintBandRow = `<div class="gantt-row gantt-sprint-band-row">
16989
+ <div class="gantt-label gantt-section-label">Sprints</div>
16990
+ <div class="gantt-track gantt-sprint-band">${sprintBlocks}</div>
16991
+ </div>`;
16992
+ const epicSpanMap = /* @__PURE__ */ new Map();
16842
16993
  for (const sprint of visibleSprints) {
16843
16994
  const sStart = toMs(sprint.startDate);
16844
16995
  const sEnd = toMs(sprint.endDate);
16845
- rows.push(`<div class="gantt-section-row">
16846
- <div class="gantt-label gantt-section-label">${sanitize(sprint.id + " " + sprint.title, 50)}</div>
16847
- <div class="gantt-track">
16848
- <div class="gantt-section-bg" style="left:${pct(sStart).toFixed(2)}%;width:${(pct(sEnd) - pct(sStart)).toFixed(2)}%"></div>
16849
- </div>
16850
- </div>`);
16851
- const linked = sprint.linkedEpics.map((eid) => epicMap.get(eid)).filter(Boolean);
16852
- const items = linked.length > 0 ? linked.map((e) => ({ label: sanitize(e.id + " " + e.title), status: e.status })) : [{ label: sanitize(sprint.title), status: sprint.status }];
16853
- for (const item of items) {
16854
- const cls = item.status === "done" || item.status === "completed" ? "gantt-bar-done" : item.status === "in-progress" || item.status === "active" ? "gantt-bar-active" : item.status === "blocked" ? "gantt-bar-blocked" : "gantt-bar-default";
16855
- const left = pct(sStart).toFixed(2);
16856
- const width = (pct(sEnd) - pct(sStart)).toFixed(2);
16857
- rows.push(`<div class="gantt-row">
16858
- <div class="gantt-label">${item.label}</div>
16996
+ for (const eid of sprint.linkedEpics) {
16997
+ if (!epicMap.has(eid)) continue;
16998
+ const existing = epicSpanMap.get(eid);
16999
+ if (existing) {
17000
+ existing.startMs = Math.min(existing.startMs, sStart);
17001
+ existing.endMs = Math.max(existing.endMs, sEnd);
17002
+ } else {
17003
+ epicSpanMap.set(eid, { startMs: sStart, endMs: sEnd });
17004
+ }
17005
+ }
17006
+ }
17007
+ const sortedEpicIds = [...epicSpanMap.keys()].sort((a, b) => {
17008
+ const aSpan = epicSpanMap.get(a);
17009
+ const bSpan = epicSpanMap.get(b);
17010
+ if (aSpan.startMs !== bSpan.startMs) return aSpan.startMs - bSpan.startMs;
17011
+ return a.localeCompare(b);
17012
+ });
17013
+ const epicRows = sortedEpicIds.map((eid) => {
17014
+ const epic = epicMap.get(eid);
17015
+ const { startMs, endMs } = epicSpanMap.get(eid);
17016
+ const cls = epic.status === "done" || epic.status === "completed" ? "gantt-bar-done" : epic.status === "in-progress" || epic.status === "active" ? "gantt-bar-active" : epic.status === "blocked" ? "gantt-bar-blocked" : "gantt-bar-default";
17017
+ const left = pct(startMs).toFixed(2);
17018
+ const width = (pct(endMs) - pct(startMs)).toFixed(2);
17019
+ const label = sanitize(epic.id + " " + epic.title);
17020
+ return `<div class="gantt-row">
17021
+ <div class="gantt-label">${label}</div>
16859
17022
  <div class="gantt-track">
16860
17023
  <div class="gantt-bar ${cls}" style="left:${left}%;width:${width}%"></div>
16861
17024
  </div>
16862
- </div>`);
16863
- }
16864
- }
17025
+ </div>`;
17026
+ }).join("\n");
16865
17027
  const note = truncated ? `<div class="mermaid-note">${hiddenCount} earlier sprint${hiddenCount > 1 ? "s" : ""} not shown</div>` : "";
16866
17028
  return `${note}
16867
17029
  <div class="gantt">
@@ -16870,11 +17032,12 @@ function buildTimelineGantt(data, maxSprints = 6) {
16870
17032
  <div class="gantt-label"></div>
16871
17033
  <div class="gantt-track gantt-dates">${markers.join("")}</div>
16872
17034
  </div>
16873
- ${rows.join("\n")}
17035
+ ${sprintBandRow}
17036
+ ${epicRows}
16874
17037
  </div>
16875
17038
  <div class="gantt-overlay">
16876
17039
  <div class="gantt-label"></div>
16877
- <div class="gantt-track">${todayMarker}</div>
17040
+ <div class="gantt-track">${gridLines.join("")}${sprintLines.join("")}${todayMarker}</div>
16878
17041
  </div>
16879
17042
  </div>`;
16880
17043
  }
@@ -17154,11 +17317,12 @@ function overviewPage(data, diagrams, navGroups) {
17154
17317
 
17155
17318
  <div class="section-title"><a href="/timeline">Project Timeline &rarr;</a></div>
17156
17319
 
17157
- <div class="section-title">Artifact Relationships</div>
17158
- ${buildArtifactFlowchart(diagrams)}
17320
+ ${collapsibleSection("overview-relationships", "Artifact Relationships", buildArtifactFlowchart(diagrams))}
17159
17321
 
17160
- <div class="section-title">Recent Activity</div>
17161
- ${data.recent.length > 0 ? `
17322
+ ${collapsibleSection(
17323
+ "overview-recent",
17324
+ "Recent Activity",
17325
+ data.recent.length > 0 ? `
17162
17326
  <div class="table-wrap">
17163
17327
  <table>
17164
17328
  <thead>
@@ -17174,7 +17338,8 @@ function overviewPage(data, diagrams, navGroups) {
17174
17338
  ${rows}
17175
17339
  </tbody>
17176
17340
  </table>
17177
- </div>` : `<div class="empty"><p>No documents yet.</p></div>`}
17341
+ </div>` : `<div class="empty"><p>No documents yet.</p></div>`
17342
+ )}
17178
17343
  `;
17179
17344
  }
17180
17345
 
@@ -17319,23 +17484,24 @@ function garPage(report) {
17319
17484
  <div class="label">Overall: ${escapeHtml(report.overall)}</div>
17320
17485
  </div>
17321
17486
 
17322
- <div class="gar-areas">
17323
- ${areaCards}
17324
- </div>
17487
+ ${collapsibleSection("gar-areas", "Areas", `<div class="gar-areas">${areaCards}</div>`)}
17325
17488
 
17326
- <div class="section-title">Status Distribution</div>
17327
- ${buildStatusPie("Action Status", {
17328
- Open: report.metrics.scope.open,
17329
- Done: report.metrics.scope.done,
17330
- "In Progress": Math.max(0, report.metrics.scope.total - report.metrics.scope.open - report.metrics.scope.done)
17331
- })}
17489
+ ${collapsibleSection(
17490
+ "gar-status-dist",
17491
+ "Status Distribution",
17492
+ buildStatusPie("Action Status", {
17493
+ Open: report.metrics.scope.open,
17494
+ Done: report.metrics.scope.done,
17495
+ "In Progress": Math.max(0, report.metrics.scope.total - report.metrics.scope.open - report.metrics.scope.done)
17496
+ })
17497
+ )}
17332
17498
  `;
17333
17499
  }
17334
17500
 
17335
17501
  // src/web/templates/pages/health.ts
17336
17502
  function healthPage(report, metrics) {
17337
17503
  const dotClass = `dot-${report.overall}`;
17338
- function renderSection(title, categories) {
17504
+ function renderSection(sectionId, title, categories) {
17339
17505
  const cards = categories.map(
17340
17506
  (cat) => `
17341
17507
  <div class="gar-area">
@@ -17347,10 +17513,9 @@ function healthPage(report, metrics) {
17347
17513
  ${cat.items.length > 0 ? `<ul>${cat.items.map((item) => `<li><span class="ref-id">${escapeHtml(item.id)}</span>${escapeHtml(item.detail)}</li>`).join("")}</ul>` : ""}
17348
17514
  </div>`
17349
17515
  ).join("\n");
17350
- return `
17351
- <div class="health-section-title">${escapeHtml(title)}</div>
17352
- <div class="gar-areas">${cards}</div>
17353
- `;
17516
+ return collapsibleSection(sectionId, title, `<div class="gar-areas">${cards}</div>`, {
17517
+ titleClass: "health-section-title"
17518
+ });
17354
17519
  }
17355
17520
  return `
17356
17521
  <div class="page-header">
@@ -17363,35 +17528,43 @@ function healthPage(report, metrics) {
17363
17528
  <div class="label">Overall: ${escapeHtml(report.overall)}</div>
17364
17529
  </div>
17365
17530
 
17366
- ${renderSection("Completeness", report.completeness)}
17367
-
17368
- <div class="health-section-title">Completeness Overview</div>
17369
- ${buildHealthGauge(
17370
- metrics ? Object.entries(metrics.completeness).map(([name, cat]) => ({
17371
- name: name.replace(/\b\w/g, (c) => c.toUpperCase()),
17372
- complete: cat.complete,
17373
- total: cat.total
17374
- })) : report.completeness.map((c) => {
17375
- const match = c.summary.match(/(\d+)\s*\/\s*(\d+)/);
17376
- return {
17377
- name: c.name,
17378
- complete: match ? parseInt(match[1], 10) : 0,
17379
- total: match ? parseInt(match[2], 10) : 0
17380
- };
17381
- })
17531
+ ${renderSection("health-completeness", "Completeness", report.completeness)}
17532
+
17533
+ ${collapsibleSection(
17534
+ "health-completeness-overview",
17535
+ "Completeness Overview",
17536
+ buildHealthGauge(
17537
+ metrics ? Object.entries(metrics.completeness).map(([name, cat]) => ({
17538
+ name: name.replace(/\b\w/g, (c) => c.toUpperCase()),
17539
+ complete: cat.complete,
17540
+ total: cat.total
17541
+ })) : report.completeness.map((c) => {
17542
+ const match = c.summary.match(/(\d+)\s*\/\s*(\d+)/);
17543
+ return {
17544
+ name: c.name,
17545
+ complete: match ? parseInt(match[1], 10) : 0,
17546
+ total: match ? parseInt(match[2], 10) : 0
17547
+ };
17548
+ })
17549
+ ),
17550
+ { titleClass: "health-section-title" }
17382
17551
  )}
17383
17552
 
17384
- ${renderSection("Process", report.process)}
17385
-
17386
- <div class="health-section-title">Process Summary</div>
17387
- ${metrics ? buildStatusPie("Process Health", {
17388
- Stale: metrics.process.stale.length,
17389
- "Aging Actions": metrics.process.agingActions.length,
17390
- Healthy: Math.max(
17391
- 0,
17392
- (metrics.completeness ? Object.values(metrics.completeness).reduce((sum, c) => sum + c.total, 0) : 0) - metrics.process.stale.length - metrics.process.agingActions.length
17393
- )
17394
- }) : ""}
17553
+ ${renderSection("health-process", "Process", report.process)}
17554
+
17555
+ ${collapsibleSection(
17556
+ "health-process-summary",
17557
+ "Process Summary",
17558
+ metrics ? buildStatusPie("Process Health", {
17559
+ Stale: metrics.process.stale.length,
17560
+ "Aging Actions": metrics.process.agingActions.length,
17561
+ Healthy: Math.max(
17562
+ 0,
17563
+ (metrics.completeness ? Object.values(metrics.completeness).reduce((sum, c) => sum + c.total, 0) : 0) - metrics.process.stale.length - metrics.process.agingActions.length
17564
+ )
17565
+ }) : "",
17566
+ { titleClass: "health-section-title" }
17567
+ )}
17395
17568
  `;
17396
17569
  }
17397
17570
 
@@ -17449,7 +17622,7 @@ function timelinePage(diagrams) {
17449
17622
  return `
17450
17623
  <div class="page-header">
17451
17624
  <h2>Project Timeline</h2>
17452
- <div class="subtitle">Sprint schedule with linked epics</div>
17625
+ <div class="subtitle">Epic timeline across sprints</div>
17453
17626
  </div>
17454
17627
 
17455
17628
  ${buildTimelineGantt(diagrams)}
@@ -17477,9 +17650,10 @@ function upcomingPage(data) {
17477
17650
  const hasActions = data.dueSoonActions.length > 0;
17478
17651
  const hasSprintTasks = data.dueSoonSprintTasks.length > 0;
17479
17652
  const hasTrending = data.trending.length > 0;
17480
- const actionsTable = hasActions ? `
17481
- <h3 class="section-title">Due Soon \u2014 Actions</h3>
17482
- <div class="table-wrap">
17653
+ const actionsTable = hasActions ? collapsibleSection(
17654
+ "upcoming-actions",
17655
+ "Due Soon \u2014 Actions",
17656
+ `<div class="table-wrap">
17483
17657
  <table>
17484
17658
  <thead>
17485
17659
  <tr>
@@ -17494,7 +17668,7 @@ function upcomingPage(data) {
17494
17668
  </thead>
17495
17669
  <tbody>
17496
17670
  ${data.dueSoonActions.map(
17497
- (a) => `
17671
+ (a) => `
17498
17672
  <tr class="${urgencyRowClass(a.urgency)}">
17499
17673
  <td><a href="/docs/action/${escapeHtml(a.id)}">${escapeHtml(a.id)}</a></td>
17500
17674
  <td>${escapeHtml(a.title)}</td>
@@ -17504,13 +17678,16 @@ function upcomingPage(data) {
17504
17678
  <td>${urgencyBadge(a.urgency)}</td>
17505
17679
  <td>${a.relatedTaskCount > 0 ? a.relatedTaskCount : "\u2014"}</td>
17506
17680
  </tr>`
17507
- ).join("")}
17681
+ ).join("")}
17508
17682
  </tbody>
17509
17683
  </table>
17510
- </div>` : "";
17511
- const sprintTasksTable = hasSprintTasks ? `
17512
- <h3 class="section-title">Due Soon \u2014 Sprint Tasks</h3>
17513
- <div class="table-wrap">
17684
+ </div>`,
17685
+ { titleTag: "h3" }
17686
+ ) : "";
17687
+ const sprintTasksTable = hasSprintTasks ? collapsibleSection(
17688
+ "upcoming-sprint-tasks",
17689
+ "Due Soon \u2014 Sprint Tasks",
17690
+ `<div class="table-wrap">
17514
17691
  <table>
17515
17692
  <thead>
17516
17693
  <tr>
@@ -17524,7 +17701,7 @@ function upcomingPage(data) {
17524
17701
  </thead>
17525
17702
  <tbody>
17526
17703
  ${data.dueSoonSprintTasks.map(
17527
- (t) => `
17704
+ (t) => `
17528
17705
  <tr class="${urgencyRowClass(t.urgency)}">
17529
17706
  <td><a href="/docs/task/${escapeHtml(t.id)}">${escapeHtml(t.id)}</a></td>
17530
17707
  <td>${escapeHtml(t.title)}</td>
@@ -17533,13 +17710,16 @@ function upcomingPage(data) {
17533
17710
  <td>${formatDate(t.sprintEndDate)}</td>
17534
17711
  <td>${urgencyBadge(t.urgency)}</td>
17535
17712
  </tr>`
17536
- ).join("")}
17713
+ ).join("")}
17537
17714
  </tbody>
17538
17715
  </table>
17539
- </div>` : "";
17540
- const trendingTable = hasTrending ? `
17541
- <h3 class="section-title">Trending</h3>
17542
- <div class="table-wrap">
17716
+ </div>`,
17717
+ { titleTag: "h3" }
17718
+ ) : "";
17719
+ const trendingTable = hasTrending ? collapsibleSection(
17720
+ "upcoming-trending",
17721
+ "Trending",
17722
+ `<div class="table-wrap">
17543
17723
  <table>
17544
17724
  <thead>
17545
17725
  <tr>
@@ -17554,7 +17734,7 @@ function upcomingPage(data) {
17554
17734
  </thead>
17555
17735
  <tbody>
17556
17736
  ${data.trending.map(
17557
- (t, i) => `
17737
+ (t, i) => `
17558
17738
  <tr>
17559
17739
  <td><span class="trending-rank">${i + 1}</span></td>
17560
17740
  <td><a href="/docs/${escapeHtml(t.type)}/${escapeHtml(t.id)}">${escapeHtml(t.id)}</a></td>
@@ -17564,10 +17744,12 @@ function upcomingPage(data) {
17564
17744
  <td><span class="trending-score">${t.score}</span></td>
17565
17745
  <td>${t.signals.map((s) => `<span class="signal-tag">${escapeHtml(s.factor)} +${s.points}</span>`).join(" ")}</td>
17566
17746
  </tr>`
17567
- ).join("")}
17747
+ ).join("")}
17568
17748
  </tbody>
17569
17749
  </table>
17570
- </div>` : "";
17750
+ </div>`,
17751
+ { titleTag: "h3" }
17752
+ ) : "";
17571
17753
  const emptyState = !hasActions && !hasSprintTasks && !hasTrending ? '<div class="empty"><p>No upcoming items or trending activity found.</p></div>' : "";
17572
17754
  return `
17573
17755
  <div class="page-header">
@@ -20865,8 +21047,8 @@ function gatherContext(store, focusFeature, includeDecisions = true, includeQues
20865
21047
  title: e.frontmatter.title,
20866
21048
  status: e.frontmatter.status,
20867
21049
  linkedFeature: normalizeLinkedFeatures(e.frontmatter.linkedFeature),
20868
- targetDate: e.frontmatter.targetDate ?? null,
20869
- estimatedEffort: e.frontmatter.estimatedEffort ?? null,
21050
+ targetDate: typeof e.frontmatter.targetDate === "string" ? e.frontmatter.targetDate : null,
21051
+ estimatedEffort: typeof e.frontmatter.estimatedEffort === "string" ? e.frontmatter.estimatedEffort : null,
20870
21052
  content: e.content,
20871
21053
  linkedTaskCount: tasks.filter(
20872
21054
  (t) => normalizeLinkedEpics(t.frontmatter.linkedEpic).includes(e.frontmatter.id)
@@ -20877,10 +21059,10 @@ function gatherContext(store, focusFeature, includeDecisions = true, includeQues
20877
21059
  title: t.frontmatter.title,
20878
21060
  status: t.frontmatter.status,
20879
21061
  linkedEpic: normalizeLinkedEpics(t.frontmatter.linkedEpic),
20880
- acceptanceCriteria: t.frontmatter.acceptanceCriteria ?? null,
20881
- technicalNotes: t.frontmatter.technicalNotes ?? null,
20882
- complexity: t.frontmatter.complexity ?? null,
20883
- estimatedPoints: t.frontmatter.estimatedPoints ?? null,
21062
+ acceptanceCriteria: typeof t.frontmatter.acceptanceCriteria === "string" ? t.frontmatter.acceptanceCriteria : null,
21063
+ technicalNotes: typeof t.frontmatter.technicalNotes === "string" ? t.frontmatter.technicalNotes : null,
21064
+ complexity: typeof t.frontmatter.complexity === "string" ? t.frontmatter.complexity : null,
21065
+ estimatedPoints: typeof t.frontmatter.estimatedPoints === "number" ? t.frontmatter.estimatedPoints : null,
20884
21066
  priority: t.frontmatter.priority ?? null
20885
21067
  })),
20886
21068
  decisions: allDecisions.map((d) => ({
@@ -25241,7 +25423,7 @@ function createProgram() {
25241
25423
  const program = new Command();
25242
25424
  program.name("marvin").description(
25243
25425
  "AI-powered product development assistant with Product Owner, Delivery Manager, and Technical Lead personas"
25244
- ).version("0.4.5");
25426
+ ).version("0.4.6");
25245
25427
  program.command("init").description("Initialize a new Marvin project in the current directory").action(async () => {
25246
25428
  await initCommand();
25247
25429
  });