mrvn-cli 0.5.25 → 0.5.27

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/marvin.js CHANGED
@@ -15170,6 +15170,221 @@ function getUpcomingData(store) {
15170
15170
  function getSprintSummaryData(store, sprintId) {
15171
15171
  return collectSprintSummaryData(store, sprintId);
15172
15172
  }
15173
+ var SIBLING_CAP = 8;
15174
+ var ARTIFACT_ID_PATTERN = /\b([A-Z]{1,3}-\d{3,})\b/g;
15175
+ function getArtifactRelationships(store, docId) {
15176
+ const doc = store.get(docId);
15177
+ if (!doc) return null;
15178
+ const fm = doc.frontmatter;
15179
+ const allDocs = store.list();
15180
+ const docIndex = new Map(allDocs.map((d) => [d.frontmatter.id, d]));
15181
+ const origins = [];
15182
+ const parents = [];
15183
+ const children = [];
15184
+ const external = [];
15185
+ const edges = [];
15186
+ const seen = /* @__PURE__ */ new Set([docId]);
15187
+ const addIfExists = (id, relationship, bucket) => {
15188
+ if (seen.has(id)) return false;
15189
+ const target = docIndex.get(id);
15190
+ if (!target) return false;
15191
+ seen.add(id);
15192
+ bucket.push({
15193
+ id: target.frontmatter.id,
15194
+ title: target.frontmatter.title,
15195
+ type: target.frontmatter.type,
15196
+ status: target.frontmatter.status,
15197
+ relationship
15198
+ });
15199
+ return true;
15200
+ };
15201
+ const parentId = fm.aboutArtifact;
15202
+ if (parentId && addIfExists(parentId, "parent", parents)) {
15203
+ edges.push({ from: parentId, to: docId });
15204
+ }
15205
+ const linkedEpics = normalizeLinkedEpics(fm.linkedEpic);
15206
+ for (const epicId of linkedEpics) {
15207
+ if (addIfExists(epicId, "epic", parents)) {
15208
+ edges.push({ from: epicId, to: docId });
15209
+ }
15210
+ const epicDoc = docIndex.get(epicId);
15211
+ if (epicDoc) {
15212
+ const features = normalizeLinkedFeatures(epicDoc.frontmatter.linkedFeature);
15213
+ for (const fid of features) {
15214
+ if (addIfExists(fid, "feature", parents)) {
15215
+ edges.push({ from: fid, to: epicId });
15216
+ }
15217
+ }
15218
+ }
15219
+ }
15220
+ const tags = fm.tags ?? [];
15221
+ for (const tag of tags) {
15222
+ if (tag.startsWith("sprint:")) {
15223
+ const sprintId = tag.slice(7);
15224
+ if (addIfExists(sprintId, "sprint", parents)) {
15225
+ edges.push({ from: sprintId, to: docId });
15226
+ }
15227
+ }
15228
+ }
15229
+ for (const tag of tags) {
15230
+ if (tag.startsWith("source:")) {
15231
+ const sourceId = tag.slice(7);
15232
+ if (addIfExists(sourceId, "source", origins)) {
15233
+ edges.push({ from: sourceId, to: docId });
15234
+ }
15235
+ }
15236
+ }
15237
+ const sourceField = fm.source;
15238
+ if (sourceField && /^[A-Z]{1,3}-\d{3,}$/.test(sourceField)) {
15239
+ if (addIfExists(sourceField, "source", origins)) {
15240
+ edges.push({ from: sourceField, to: docId });
15241
+ }
15242
+ }
15243
+ for (const d of allDocs) {
15244
+ if (d.frontmatter.aboutArtifact === docId) {
15245
+ if (addIfExists(d.frontmatter.id, "child", children)) {
15246
+ edges.push({ from: docId, to: d.frontmatter.id });
15247
+ }
15248
+ }
15249
+ }
15250
+ if (fm.type === "epic") {
15251
+ const epicTag = `epic:${docId}`;
15252
+ for (const d of allDocs) {
15253
+ const dfm = d.frontmatter;
15254
+ const dLinkedEpics = normalizeLinkedEpics(dfm.linkedEpic);
15255
+ const dTags = dfm.tags ?? [];
15256
+ if (dLinkedEpics.includes(docId) || dTags.includes(epicTag)) {
15257
+ if (addIfExists(dfm.id, "child", children)) {
15258
+ edges.push({ from: docId, to: dfm.id });
15259
+ }
15260
+ }
15261
+ }
15262
+ }
15263
+ if (parentId) {
15264
+ let siblingCount = 0;
15265
+ for (const d of allDocs) {
15266
+ if (siblingCount >= SIBLING_CAP) break;
15267
+ if (d.frontmatter.aboutArtifact === parentId && d.frontmatter.id !== docId) {
15268
+ if (addIfExists(d.frontmatter.id, "sibling", children)) {
15269
+ edges.push({ from: parentId, to: d.frontmatter.id });
15270
+ siblingCount++;
15271
+ }
15272
+ }
15273
+ }
15274
+ }
15275
+ const jiraKey = fm.jiraKey;
15276
+ const jiraUrl = fm.jiraUrl;
15277
+ if (jiraKey) {
15278
+ external.push({
15279
+ id: jiraKey,
15280
+ title: jiraUrl ?? `Jira: ${jiraKey}`,
15281
+ type: "jira",
15282
+ status: "",
15283
+ relationship: "jira"
15284
+ });
15285
+ edges.push({ from: docId, to: jiraKey });
15286
+ }
15287
+ if (doc.content) {
15288
+ const matches = doc.content.matchAll(ARTIFACT_ID_PATTERN);
15289
+ for (const m of matches) {
15290
+ const refId = m[1];
15291
+ if (refId !== docId && docIndex.has(refId)) {
15292
+ if (addIfExists(refId, "mentioned", external)) {
15293
+ edges.push({ from: docId, to: refId });
15294
+ }
15295
+ }
15296
+ }
15297
+ }
15298
+ return {
15299
+ origins,
15300
+ parents,
15301
+ self: {
15302
+ id: fm.id,
15303
+ title: fm.title,
15304
+ type: fm.type,
15305
+ status: fm.status,
15306
+ relationship: "self"
15307
+ },
15308
+ children,
15309
+ external,
15310
+ edges
15311
+ };
15312
+ }
15313
+ function getArtifactLineageEvents(store, docId) {
15314
+ const doc = store.get(docId);
15315
+ if (!doc) return [];
15316
+ const fm = doc.frontmatter;
15317
+ const events = [];
15318
+ if (fm.created) {
15319
+ events.push({
15320
+ date: fm.created,
15321
+ type: "created",
15322
+ label: `${fm.id} created`
15323
+ });
15324
+ }
15325
+ const tags = fm.tags ?? [];
15326
+ for (const tag of tags) {
15327
+ if (tag.startsWith("source:")) {
15328
+ const sourceId = tag.slice(7);
15329
+ const sourceDoc = store.get(sourceId);
15330
+ if (sourceDoc) {
15331
+ events.push({
15332
+ date: sourceDoc.frontmatter.created,
15333
+ type: "source-linked",
15334
+ label: `Originated from ${sourceId} \u2014 ${sourceDoc.frontmatter.title}`,
15335
+ relatedId: sourceId
15336
+ });
15337
+ }
15338
+ }
15339
+ }
15340
+ const allDocs = store.list();
15341
+ for (const d of allDocs) {
15342
+ if (d.frontmatter.aboutArtifact === docId) {
15343
+ events.push({
15344
+ date: d.frontmatter.created,
15345
+ type: "child-spawned",
15346
+ label: `Spawned ${d.frontmatter.type} ${d.frontmatter.id} \u2014 ${d.frontmatter.title}`,
15347
+ relatedId: d.frontmatter.id
15348
+ });
15349
+ }
15350
+ }
15351
+ if (fm.type === "epic") {
15352
+ const epicTag = `epic:${docId}`;
15353
+ for (const d of allDocs) {
15354
+ if (d.frontmatter.aboutArtifact === docId) continue;
15355
+ const dLinkedEpics = normalizeLinkedEpics(d.frontmatter.linkedEpic);
15356
+ const dTags = d.frontmatter.tags ?? [];
15357
+ if (dLinkedEpics.includes(docId) || dTags.includes(epicTag)) {
15358
+ events.push({
15359
+ date: d.frontmatter.created,
15360
+ type: "child-spawned",
15361
+ label: `Linked ${d.frontmatter.type} ${d.frontmatter.id} \u2014 ${d.frontmatter.title}`,
15362
+ relatedId: d.frontmatter.id
15363
+ });
15364
+ }
15365
+ }
15366
+ }
15367
+ const history = fm.assessmentHistory ?? [];
15368
+ for (const entry of history) {
15369
+ if (entry.generatedAt) {
15370
+ events.push({
15371
+ date: entry.generatedAt,
15372
+ type: "assessment",
15373
+ label: "Assessment performed"
15374
+ });
15375
+ }
15376
+ }
15377
+ const lastSync = fm.lastJiraSyncAt;
15378
+ if (lastSync) {
15379
+ events.push({
15380
+ date: lastSync,
15381
+ type: "jira-sync",
15382
+ label: `Synced with Jira ${fm.jiraKey ?? ""}`
15383
+ });
15384
+ }
15385
+ events.sort((a, b) => (b.date ?? "").localeCompare(a.date ?? ""));
15386
+ return events;
15387
+ }
15173
15388
 
15174
15389
  // src/reports/gar/collector.ts
15175
15390
  var DONE_STATUSES4 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
@@ -20560,6 +20775,65 @@ a.artifact-link:hover {
20560
20775
  .flow-line-lit { stroke: var(--accent) !important; stroke-width: 2 !important; }
20561
20776
  .flow-line-dim { opacity: 0.08; }
20562
20777
 
20778
+ /* Relationship graph: self-node emphasis */
20779
+ .flow-self {
20780
+ border-left-width: 4px;
20781
+ background: var(--bg-hover);
20782
+ box-shadow: 0 0 0 1px var(--accent-dim);
20783
+ }
20784
+ .flow-self .flow-node-id {
20785
+ color: var(--accent);
20786
+ font-weight: 600;
20787
+ }
20788
+
20789
+ /* Relationship graph: external nodes */
20790
+ .flow-external {
20791
+ border-left-color: var(--text-dim);
20792
+ border-left-style: dashed;
20793
+ }
20794
+
20795
+ /* Relationship graph: empty state */
20796
+ .flow-empty {
20797
+ padding: 2rem;
20798
+ text-align: center;
20799
+ color: var(--text-dim);
20800
+ font-size: 0.85rem;
20801
+ }
20802
+
20803
+ /* Lineage timeline */
20804
+ .lineage-timeline {
20805
+ margin-top: 1.5rem;
20806
+ }
20807
+ .lineage-timeline h3 {
20808
+ font-size: 1rem;
20809
+ font-weight: 600;
20810
+ margin-bottom: 0.75rem;
20811
+ }
20812
+ .lineage-entry {
20813
+ display: flex;
20814
+ gap: 0.5rem;
20815
+ padding: 0.4rem 0;
20816
+ padding-left: 0.25rem;
20817
+ }
20818
+ .lineage-marker {
20819
+ flex-shrink: 0;
20820
+ font-size: 0.7rem;
20821
+ line-height: 1.4rem;
20822
+ }
20823
+ .lineage-content {
20824
+ display: flex;
20825
+ flex-direction: column;
20826
+ gap: 0.1rem;
20827
+ }
20828
+ .lineage-date {
20829
+ font-size: 0.7rem;
20830
+ color: var(--text-dim);
20831
+ font-family: var(--mono);
20832
+ }
20833
+ .lineage-label {
20834
+ font-size: 0.85rem;
20835
+ }
20836
+
20563
20837
  /* Gantt truncation note */
20564
20838
  .mermaid-note {
20565
20839
  font-size: 0.75rem;
@@ -21486,357 +21760,27 @@ function documentsPage(data) {
21486
21760
  `;
21487
21761
  }
21488
21762
 
21489
- // src/web/templates/pages/document-detail.ts
21490
- function documentDetailPage(doc) {
21491
- const fm = doc.frontmatter;
21492
- const label = typeLabel(fm.type);
21493
- const skipKeys = /* @__PURE__ */ new Set(["title", "type", "assessmentHistory", "assessmentSummary"]);
21494
- const entries = Object.entries(fm).filter(
21495
- ([key, value]) => !skipKeys.has(key) && value != null && typeof value !== "object"
21496
- );
21497
- const arrayEntries = Object.entries(fm).filter(
21498
- ([key, value]) => !skipKeys.has(key) && Array.isArray(value) && value.every((v) => typeof v === "string")
21499
- );
21500
- const allEntries = [
21501
- ...entries.filter(([, v]) => !Array.isArray(v)),
21502
- ...arrayEntries
21503
- ];
21504
- const dtDd = allEntries.map(([key, value]) => {
21505
- let rendered;
21506
- if (key === "status") {
21507
- rendered = statusBadge(value);
21508
- } else if (key === "tags" && Array.isArray(value)) {
21509
- rendered = value.map((t) => `<span class="badge badge-default">${escapeHtml(t)}</span>`).join(" ");
21510
- } else if (key === "created" || key === "updated" || key === "lastAssessedAt" || key === "lastJiraSyncAt") {
21511
- rendered = formatDate(value);
21512
- } else {
21513
- rendered = linkArtifactIds(escapeHtml(String(value)));
21514
- }
21515
- return `<dt>${escapeHtml(key)}</dt><dd>${rendered}</dd>`;
21516
- }).join("\n ");
21517
- const rawHistory = Array.isArray(fm.assessmentHistory) ? fm.assessmentHistory : fm.assessmentSummary && typeof fm.assessmentSummary === "object" ? [fm.assessmentSummary] : [];
21518
- const assessmentHistory = rawHistory.filter(isValidAssessmentEntry).sort((a, b) => (b.generatedAt ?? "").localeCompare(a.generatedAt ?? ""));
21519
- const timelineHtml = assessmentHistory.length > 0 ? renderAssessmentTimeline(assessmentHistory) : "";
21520
- return `
21521
- <div class="breadcrumb">
21522
- <a href="/">Overview</a><span class="sep">/</span>
21523
- <a href="/docs/${fm.type}">${escapeHtml(label)}s</a><span class="sep">/</span>
21524
- ${escapeHtml(fm.id)}
21525
- </div>
21526
-
21527
- <div class="page-header">
21528
- <h2>${escapeHtml(fm.title)}${integrationIcons(fm)}</h2>
21529
- <div class="subtitle">${escapeHtml(fm.id)} &middot; ${escapeHtml(label)}</div>
21530
- </div>
21531
-
21532
- <div class="detail-meta">
21533
- <dl>
21534
- ${dtDd}
21535
- </dl>
21536
- </div>
21537
-
21538
- ${doc.content.trim() ? `<div class="detail-content">${renderMarkdown(doc.content)}</div>` : ""}
21539
-
21540
- ${timelineHtml}
21541
- `;
21542
- }
21543
- function isValidAssessmentEntry(value) {
21544
- if (typeof value !== "object" || value === null) return false;
21545
- const obj = value;
21546
- if (typeof obj.generatedAt !== "string") return false;
21547
- if (obj.signals !== void 0 && !Array.isArray(obj.signals)) return false;
21548
- return true;
21549
- }
21550
- function normalizeEntry(entry) {
21551
- return {
21552
- generatedAt: entry.generatedAt ?? "",
21553
- commentSummary: typeof entry.commentSummary === "string" ? entry.commentSummary : null,
21554
- commentAnalysisProgress: typeof entry.commentAnalysisProgress === "number" ? entry.commentAnalysisProgress : null,
21555
- signals: Array.isArray(entry.signals) ? entry.signals.filter((s) => typeof s === "string") : [],
21556
- childCount: typeof entry.childCount === "number" ? entry.childCount : 0,
21557
- childDoneCount: typeof entry.childDoneCount === "number" ? entry.childDoneCount : 0,
21558
- childRollupProgress: typeof entry.childRollupProgress === "number" ? entry.childRollupProgress : null,
21559
- linkedIssueCount: typeof entry.linkedIssueCount === "number" ? entry.linkedIssueCount : 0
21560
- };
21561
- }
21562
- function renderAssessmentTimeline(history) {
21563
- const entries = history.map((raw, i) => {
21564
- const entry = normalizeEntry(raw);
21565
- const date5 = entry.generatedAt ? formatDate(entry.generatedAt) : "Unknown date";
21566
- const time3 = entry.generatedAt?.slice(11, 16) ?? "";
21567
- const isLatest = i === 0;
21568
- const parts = [];
21569
- if (entry.commentSummary) {
21570
- parts.push(`<div class="assessment-comment">${linkArtifactIds(escapeHtml(entry.commentSummary))}</div>`);
21571
- }
21572
- if (entry.commentAnalysisProgress !== null) {
21573
- parts.push(`<div class="assessment-stat">\u{1F4CA} Comment-derived progress: <strong>${entry.commentAnalysisProgress}%</strong></div>`);
21574
- }
21575
- if (entry.childCount > 0) {
21576
- const bar = progressBarHtml(entry.childRollupProgress ?? 0);
21577
- parts.push(`<div class="assessment-stat">\u{1F476} Children: ${entry.childDoneCount}/${entry.childCount} done ${bar} ${entry.childRollupProgress ?? 0}%</div>`);
21578
- }
21579
- if (entry.linkedIssueCount > 0) {
21580
- parts.push(`<div class="assessment-stat">\u{1F517} Linked issues: ${entry.linkedIssueCount}</div>`);
21581
- }
21582
- if (entry.signals.length > 0) {
21583
- const signalItems = entry.signals.map((s) => `<li>${linkArtifactIds(escapeHtml(s))}</li>`).join("");
21584
- parts.push(`<ul class="assessment-signals">${signalItems}</ul>`);
21585
- }
21586
- return `
21587
- <div class="assessment-entry${isLatest ? " assessment-latest" : ""}">
21588
- <div class="assessment-header">
21589
- <span class="assessment-date">${escapeHtml(date5)} ${escapeHtml(time3)}</span>
21590
- ${isLatest ? '<span class="badge badge-default">Latest</span>' : ""}
21591
- </div>
21592
- ${parts.join("\n")}
21593
- </div>`;
21594
- });
21595
- return `
21596
- <div class="assessment-timeline">
21597
- <h3>Assessment History</h3>
21598
- ${entries.join("\n")}
21599
- </div>`;
21600
- }
21601
- function progressBarHtml(pct) {
21602
- const filled = Math.round(Math.max(0, Math.min(100, pct)) / 10);
21603
- const empty = 10 - filled;
21604
- return `<span class="progress-bar-inline">${"\u2588".repeat(filled)}${"\u2591".repeat(empty)}</span>`;
21763
+ // src/web/templates/mermaid.ts
21764
+ function sanitize(text, maxLen = 40) {
21765
+ const cleaned = text.replace(/["'`]/g, "").replace(/[\r\n]+/g, " ");
21766
+ return cleaned.length > maxLen ? cleaned.slice(0, maxLen - 1) + "\u2026" : cleaned;
21605
21767
  }
21606
-
21607
- // src/web/persona-views.ts
21608
- var VIEWS = /* @__PURE__ */ new Map();
21609
- var PAGE_RENDERERS = /* @__PURE__ */ new Map();
21610
- function registerPersonaView(config2) {
21611
- VIEWS.set(config2.shortName, config2);
21768
+ function mermaidBlock(definition, extraClass) {
21769
+ const cls = ["mermaid-container", extraClass].filter(Boolean).join(" ");
21770
+ return `<div class="${cls}"><pre class="mermaid">
21771
+ ${definition}
21772
+ </pre></div>`;
21612
21773
  }
21613
- function registerPersonaPage(persona, pageId, renderer) {
21614
- PAGE_RENDERERS.set(`${persona}/${pageId}`, renderer);
21774
+ function placeholder(message) {
21775
+ return `<div class="mermaid-container mermaid-empty"><p>${message}</p></div>`;
21615
21776
  }
21616
- function getPersonaView(mode) {
21617
- if (!mode) return void 0;
21618
- return VIEWS.get(mode);
21777
+ function toMs(date5) {
21778
+ return (/* @__PURE__ */ new Date(date5 + "T00:00:00")).getTime();
21619
21779
  }
21620
- function getPersonaPageRenderer(persona, pageId) {
21621
- return PAGE_RENDERERS.get(`${persona}/${pageId}`);
21622
- }
21623
- function getAllPersonaViews() {
21624
- return [...VIEWS.values()];
21625
- }
21626
- var VALID_PERSONAS = /* @__PURE__ */ new Set(["po", "dm", "tl"]);
21627
- function parsePersonaFromUrl(params) {
21628
- const value = params.get("persona")?.toLowerCase();
21629
- if (value && VALID_PERSONAS.has(value)) return value;
21630
- return null;
21631
- }
21632
- function parsePersonaFromPath(pathname) {
21633
- const match = pathname.match(/^\/(po|dm|tl)(?:\/|$)/);
21634
- return match ? match[1] : null;
21635
- }
21636
- function resolvePersona(pathname, params) {
21637
- return parsePersonaFromPath(pathname) ?? parsePersonaFromUrl(params);
21638
- }
21639
- var SHARED_NAV_ITEMS = [
21640
- { pageId: "timeline", label: "Timeline" },
21641
- { pageId: "board", label: "Board" },
21642
- { pageId: "upcoming", label: "Upcoming" },
21643
- { pageId: "sprint-summary", label: "Sprint Summary" },
21644
- { pageId: "gar", label: "GAR Report" },
21645
- { pageId: "health", label: "Health" }
21646
- ];
21647
-
21648
- // src/web/templates/pages/persona-picker.ts
21649
- function personaPickerPage() {
21650
- const views = getAllPersonaViews();
21651
- const cards = views.map(
21652
- (v) => `
21653
- <a href="/${v.shortName}/dashboard" class="persona-picker-card" style="--persona-card-accent: ${v.color}">
21654
- <div class="persona-picker-name">${escapeHtml(v.displayName)}</div>
21655
- <div class="persona-picker-desc">${escapeHtml(v.description)}</div>
21656
- </a>`
21657
- ).join("\n");
21658
- return `
21659
- <div class="persona-picker">
21660
- <h2>Choose Your View</h2>
21661
- <p class="persona-picker-subtitle">Select a role to see a curated dashboard with the pages most relevant to you.</p>
21662
- <div class="persona-picker-grid">
21663
- ${cards}
21664
- </div>
21665
- </div>`;
21666
- }
21667
-
21668
- // src/reports/sprint-summary/risk-assessment.ts
21669
- import { query as query2 } from "@anthropic-ai/claude-agent-sdk";
21670
- var SYSTEM_PROMPT2 = `You are a delivery management assistant generating a data-driven risk assessment.
21671
-
21672
- IMPORTANT: All the data you need is provided in the user message below. Do NOT attempt to look up, search for, or request additional information. Analyze ONLY the data given and produce your assessment immediately.
21673
-
21674
- Produce a concise markdown assessment with these sections:
21675
-
21676
- ## Status Assessment
21677
- One-line verdict: is this risk actively being mitigated, stalled, or escalating?
21678
-
21679
- ## Related Activity
21680
- What actions, decisions, or contributions are connected to this risk? How are they progressing? Be specific \u2014 reference artifact IDs from the data provided.
21681
-
21682
- ## Trajectory
21683
- Based on the data (status of related items, time remaining, ownership), is this risk trending toward resolution or toward becoming a blocker? Explain your reasoning with concrete evidence.
21684
-
21685
- ## Recommendation
21686
- One concrete next step to move this risk toward resolution.
21687
-
21688
- Rules:
21689
- - Reference artifact IDs, dates, owners, and statuses from the provided data
21690
- - Keep the tone professional and direct
21691
- - Do NOT speculate beyond what the data supports \u2014 if information is insufficient, say so explicitly
21692
- - Do NOT ask for more information or say you will look things up \u2014 everything you need is in the prompt
21693
- - Produce the full assessment text directly`;
21694
- async function generateRiskAssessment(data, riskId, store) {
21695
- const risk = data.risks.find((r) => r.id === riskId);
21696
- if (!risk) return "Risk not found in sprint data.";
21697
- const prompt = buildSingleRiskPrompt(data, risk, store);
21698
- const result = query2({
21699
- prompt,
21700
- options: {
21701
- systemPrompt: SYSTEM_PROMPT2,
21702
- maxTurns: 1,
21703
- tools: [],
21704
- allowedTools: []
21705
- }
21706
- });
21707
- for await (const msg of result) {
21708
- if (msg.type === "assistant") {
21709
- const text = msg.message.content.find(
21710
- (b) => b.type === "text"
21711
- );
21712
- if (text) return text.text;
21713
- }
21714
- }
21715
- return "Unable to generate risk assessment.";
21716
- }
21717
- function buildSingleRiskPrompt(data, risk, store) {
21718
- const sections = [];
21719
- sections.push(`# Sprint: ${data.sprint.id} \u2014 ${data.sprint.title}`);
21720
- if (data.sprint.goal) sections.push(`Goal: ${data.sprint.goal}`);
21721
- sections.push(`Days remaining: ${data.timeline.daysRemaining} / ${data.timeline.totalDays}`);
21722
- sections.push(`Completion: ${data.workItems.completionPct}%`);
21723
- sections.push("");
21724
- const doc = store.get(risk.id);
21725
- sections.push(`# RISK: ${risk.id} \u2014 ${risk.title}`);
21726
- sections.push(`Type: ${risk.type}`);
21727
- if (doc) {
21728
- sections.push(`Status: ${doc.frontmatter.status}`);
21729
- if (doc.frontmatter.owner) sections.push(`Owner: ${doc.frontmatter.owner}`);
21730
- if (doc.frontmatter.assignee) sections.push(`Assignee: ${doc.frontmatter.assignee}`);
21731
- if (doc.frontmatter.priority) sections.push(`Priority: ${doc.frontmatter.priority}`);
21732
- if (doc.frontmatter.dueDate) sections.push(`Due date: ${doc.frontmatter.dueDate}`);
21733
- if (doc.frontmatter.created) sections.push(`Created: ${doc.frontmatter.created.slice(0, 10)}`);
21734
- const tags = doc.frontmatter.tags ?? [];
21735
- if (tags.length > 0) sections.push(`Tags: ${tags.join(", ")}`);
21736
- if (doc.content.trim()) {
21737
- sections.push(`
21738
- Description:
21739
- ${doc.content.trim()}`);
21740
- }
21741
- const allDocs = store.list();
21742
- const relatedIds = /* @__PURE__ */ new Set();
21743
- for (const d of allDocs) {
21744
- if (d.frontmatter.aboutArtifact === risk.id) {
21745
- relatedIds.add(d.frontmatter.id);
21746
- }
21747
- }
21748
- const idPattern = /\b([A-Z]-\d{3,})\b/g;
21749
- let match;
21750
- while ((match = idPattern.exec(doc.content)) !== null) {
21751
- relatedIds.add(match[1]);
21752
- }
21753
- const significantTags = tags.filter(
21754
- (t) => !t.startsWith("sprint:") && !t.startsWith("focus:") && t !== "risk"
21755
- );
21756
- if (significantTags.length > 0) {
21757
- for (const d of allDocs) {
21758
- if (d.frontmatter.id === risk.id) continue;
21759
- const dTags = d.frontmatter.tags ?? [];
21760
- if (significantTags.some((t) => dTags.includes(t))) {
21761
- relatedIds.add(d.frontmatter.id);
21762
- }
21763
- }
21764
- }
21765
- const about = doc.frontmatter.aboutArtifact;
21766
- if (about) {
21767
- relatedIds.add(about);
21768
- for (const d of allDocs) {
21769
- if (d.frontmatter.aboutArtifact === about && d.frontmatter.id !== risk.id) {
21770
- relatedIds.add(d.frontmatter.id);
21771
- }
21772
- }
21773
- }
21774
- const relatedDocs = [...relatedIds].map((id) => store.get(id)).filter((d) => d != null).slice(0, 20);
21775
- if (relatedDocs.length > 0) {
21776
- sections.push(`
21777
- ## Related Documents (${relatedDocs.length})`);
21778
- for (const rd of relatedDocs) {
21779
- const owner = rd.frontmatter.owner ?? "unowned";
21780
- const summary = rd.content.trim().slice(0, 300);
21781
- sections.push(
21782
- `- ${rd.frontmatter.id} (${rd.frontmatter.type}) [${rd.frontmatter.status}] \u2014 ${rd.frontmatter.title}`
21783
- );
21784
- sections.push(` Owner: ${owner}${rd.frontmatter.dueDate ? `, Due: ${rd.frontmatter.dueDate}` : ""}`);
21785
- if (summary) sections.push(` Summary: ${summary}${rd.content.trim().length > 300 ? "..." : ""}`);
21786
- }
21787
- }
21788
- }
21789
- sections.push("");
21790
- sections.push(`---`);
21791
- sections.push(`
21792
- Generate the risk assessment for ${risk.id} based on the data above.`);
21793
- return sections.join("\n");
21794
- }
21795
-
21796
- // src/web/templates/persona-switcher.ts
21797
- function renderPersonaSwitcher(current, _currentPath) {
21798
- const views = getAllPersonaViews();
21799
- if (views.length === 0) return "";
21800
- const options = views.map(
21801
- (v) => `<option value="${v.shortName}"${current === v.shortName ? " selected" : ""}>${escapeHtml(v.displayName)}</option>`
21802
- ).join("\n ");
21803
- return `
21804
- <div class="persona-switcher">
21805
- <label class="persona-label" for="persona-select">View</label>
21806
- <select class="persona-select" id="persona-select" onchange="switchPersona(this.value)">
21807
- ${options}
21808
- </select>
21809
- </div>
21810
- <script>
21811
- function switchPersona(value) {
21812
- if (value) {
21813
- window.location.href = '/' + value + '/dashboard';
21814
- }
21815
- }
21816
- </script>`;
21817
- }
21818
-
21819
- // src/web/templates/mermaid.ts
21820
- function sanitize(text, maxLen = 40) {
21821
- const cleaned = text.replace(/["'`]/g, "").replace(/[\r\n]+/g, " ");
21822
- return cleaned.length > maxLen ? cleaned.slice(0, maxLen - 1) + "\u2026" : cleaned;
21823
- }
21824
- function mermaidBlock(definition, extraClass) {
21825
- const cls = ["mermaid-container", extraClass].filter(Boolean).join(" ");
21826
- return `<div class="${cls}"><pre class="mermaid">
21827
- ${definition}
21828
- </pre></div>`;
21829
- }
21830
- function placeholder(message) {
21831
- return `<div class="mermaid-container mermaid-empty"><p>${message}</p></div>`;
21832
- }
21833
- function toMs(date5) {
21834
- return (/* @__PURE__ */ new Date(date5 + "T00:00:00")).getTime();
21835
- }
21836
- function fmtDate(ms) {
21837
- const d = new Date(ms);
21838
- const months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
21839
- return `${months[d.getMonth()]} ${d.getDate()}`;
21780
+ function fmtDate(ms) {
21781
+ const d = new Date(ms);
21782
+ const months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
21783
+ return `${months[d.getMonth()]} ${d.getDate()}`;
21840
21784
  }
21841
21785
  function buildTimelineGantt(data, maxSprints = 6) {
21842
21786
  const sprintsWithDates = data.sprints.filter((s) => s.startDate && s.endDate).sort((a, b) => a.startDate < b.startDate ? -1 : 1);
@@ -22101,89 +22045,656 @@ function buildArtifactFlowchart(data) {
22101
22045
  return visited;
22102
22046
  }
22103
22047
 
22104
- function highlight(hoveredId) {
22105
- var connected = findConnected(hoveredId);
22106
- container.querySelectorAll('.flow-node').forEach(function(n) {
22107
- if (connected[n.dataset.flowId]) {
22108
- n.classList.add('flow-lit');
22109
- n.classList.remove('flow-dim');
22110
- } else {
22111
- n.classList.add('flow-dim');
22112
- n.classList.remove('flow-lit');
22113
- }
22114
- });
22115
- svg.querySelectorAll('path').forEach(function(p) {
22116
- if (connected[p.dataset.from] && connected[p.dataset.to]) {
22117
- p.classList.add('flow-line-lit');
22118
- p.classList.remove('flow-line-dim');
22119
- } else {
22120
- p.classList.add('flow-line-dim');
22121
- p.classList.remove('flow-line-lit');
22122
- }
22123
- });
22124
- }
22048
+ function highlight(hoveredId) {
22049
+ var connected = findConnected(hoveredId);
22050
+ container.querySelectorAll('.flow-node').forEach(function(n) {
22051
+ if (connected[n.dataset.flowId]) {
22052
+ n.classList.add('flow-lit');
22053
+ n.classList.remove('flow-dim');
22054
+ } else {
22055
+ n.classList.add('flow-dim');
22056
+ n.classList.remove('flow-lit');
22057
+ }
22058
+ });
22059
+ svg.querySelectorAll('path').forEach(function(p) {
22060
+ if (connected[p.dataset.from] && connected[p.dataset.to]) {
22061
+ p.classList.add('flow-line-lit');
22062
+ p.classList.remove('flow-line-dim');
22063
+ } else {
22064
+ p.classList.add('flow-line-dim');
22065
+ p.classList.remove('flow-line-lit');
22066
+ }
22067
+ });
22068
+ }
22069
+
22070
+ function clearHighlight() {
22071
+ container.querySelectorAll('.flow-node').forEach(function(n) { n.classList.remove('flow-lit', 'flow-dim'); });
22072
+ svg.querySelectorAll('path').forEach(function(p) { p.classList.remove('flow-line-lit', 'flow-line-dim'); });
22073
+ }
22074
+
22075
+ var activeId = null;
22076
+ container.addEventListener('click', function(e) {
22077
+ // Let the ID link navigate normally
22078
+ if (e.target.closest('a')) return;
22079
+
22080
+ var node = e.target.closest('.flow-node');
22081
+ var clickedId = node ? node.dataset.flowId : null;
22082
+
22083
+ if (!clickedId || clickedId === activeId) {
22084
+ activeId = null;
22085
+ clearHighlight();
22086
+ return;
22087
+ }
22088
+
22089
+ activeId = clickedId;
22090
+ highlight(clickedId);
22091
+ });
22092
+
22093
+ function drawAndHighlight() {
22094
+ drawLines();
22095
+ if (activeId) highlight(activeId);
22096
+ }
22097
+
22098
+ requestAnimationFrame(function() { setTimeout(drawAndHighlight, 100); });
22099
+ window.addEventListener('resize', drawAndHighlight);
22100
+ container.addEventListener('scroll', drawAndHighlight);
22101
+ new ResizeObserver(drawAndHighlight).observe(container);
22102
+ })();
22103
+ </script>`;
22104
+ }
22105
+ function buildStatusPie(title, counts) {
22106
+ const entries = Object.entries(counts).filter(([, v]) => v > 0);
22107
+ if (entries.length === 0) {
22108
+ return placeholder(`No data for ${title}.`);
22109
+ }
22110
+ const lines = [`pie title ${sanitize(title, 60)}`];
22111
+ for (const [label, count] of entries) {
22112
+ lines.push(` "${sanitize(label, 30)}" : ${count}`);
22113
+ }
22114
+ return mermaidBlock(lines.join("\n"));
22115
+ }
22116
+ function buildHealthGauge(categories) {
22117
+ const valid = categories.filter((c) => c.total > 0);
22118
+ if (valid.length === 0) {
22119
+ return placeholder("No completeness data available.");
22120
+ }
22121
+ const pies = valid.map((cat) => {
22122
+ const incomplete = cat.total - cat.complete;
22123
+ const lines = [
22124
+ `pie title ${sanitize(cat.name, 30)}`,
22125
+ ` "Complete" : ${cat.complete}`,
22126
+ ` "Incomplete" : ${incomplete}`
22127
+ ];
22128
+ return mermaidBlock(lines.join("\n"));
22129
+ });
22130
+ return `<div class="mermaid-row">${pies.join("\n")}</div>`;
22131
+ }
22132
+
22133
+ // src/web/templates/artifact-graph.ts
22134
+ function buildArtifactRelationGraph(data) {
22135
+ const hasContent = data.origins.length > 0 || data.parents.length > 0 || data.children.length > 0 || data.external.length > 0;
22136
+ if (!hasContent) {
22137
+ return `<div class="flow-diagram flow-empty"><p>No relationships found for this artifact.</p></div>`;
22138
+ }
22139
+ const edges = data.edges;
22140
+ const renderNode = (id, title, status, type) => {
22141
+ const href = type === "jira" ? title.startsWith("http") ? title : "#" : `/docs/${type}/${id}`;
22142
+ const target = type === "jira" ? ' target="_blank" rel="noopener"' : "";
22143
+ const cls = type === "jira" ? "flow-node flow-external" : `flow-node ${statusClass(status)}`;
22144
+ const displayTitle = type === "jira" ? "Jira Issue" : sanitize(title, 35);
22145
+ const displayId = type === "jira" ? `${id} \u2197` : id;
22146
+ return `<div class="${cls}" data-flow-id="${escapeHtml(id)}">
22147
+ <a class="flow-node-id" href="${escapeHtml(href)}"${target}>${escapeHtml(displayId)}</a>
22148
+ <span class="flow-node-title">${escapeHtml(displayTitle)}</span>
22149
+ </div>`;
22150
+ };
22151
+ const selfNode = `<div class="flow-node flow-self ${statusClass(data.self.status)}" data-flow-id="${escapeHtml(data.self.id)}">
22152
+ <span class="flow-node-id">${escapeHtml(data.self.id)}</span>
22153
+ <span class="flow-node-title">${escapeHtml(sanitize(data.self.title, 35))}</span>
22154
+ </div>`;
22155
+ const columns = [];
22156
+ if (data.origins.length > 0) {
22157
+ columns.push({
22158
+ header: "Origins",
22159
+ nodes: data.origins.map((a) => renderNode(a.id, a.title, a.status, a.type)).join("\n")
22160
+ });
22161
+ }
22162
+ if (data.parents.length > 0) {
22163
+ columns.push({
22164
+ header: "Parents",
22165
+ nodes: data.parents.map((a) => renderNode(a.id, a.title, a.status, a.type)).join("\n")
22166
+ });
22167
+ }
22168
+ columns.push({
22169
+ header: data.self.type.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase()),
22170
+ nodes: selfNode
22171
+ });
22172
+ if (data.children.length > 0) {
22173
+ columns.push({
22174
+ header: "Children",
22175
+ nodes: data.children.map((a) => renderNode(a.id, a.title, a.status, a.type)).join("\n")
22176
+ });
22177
+ }
22178
+ if (data.external.length > 0) {
22179
+ columns.push({
22180
+ header: "External",
22181
+ nodes: data.external.map((a) => renderNode(a.id, a.title, a.status, a.type)).join("\n")
22182
+ });
22183
+ }
22184
+ const columnsHtml = columns.map((col) => `
22185
+ <div class="flow-column">
22186
+ <div class="flow-column-header">${escapeHtml(col.header)}</div>
22187
+ ${col.nodes}
22188
+ </div>`).join("\n");
22189
+ const edgesJson = JSON.stringify(edges);
22190
+ return `
22191
+ <div class="flow-diagram" id="rel-graph">
22192
+ <svg class="flow-lines" id="rel-lines"></svg>
22193
+ <div class="flow-columns">
22194
+ ${columnsHtml}
22195
+ </div>
22196
+ </div>
22197
+ <script>
22198
+ (function() {
22199
+ var edges = ${edgesJson};
22200
+ var container = document.getElementById('rel-graph');
22201
+ var svg = document.getElementById('rel-lines');
22202
+ if (!container || !svg) return;
22203
+
22204
+ var fwd = {};
22205
+ var bwd = {};
22206
+ edges.forEach(function(e) {
22207
+ if (!fwd[e.from]) fwd[e.from] = [];
22208
+ if (!bwd[e.to]) bwd[e.to] = [];
22209
+ fwd[e.from].push(e.to);
22210
+ bwd[e.to].push(e.from);
22211
+ });
22212
+
22213
+ function drawLines() {
22214
+ var rect = container.getBoundingClientRect();
22215
+ var scrollW = container.scrollWidth;
22216
+ var scrollH = container.scrollHeight;
22217
+ svg.setAttribute('width', scrollW);
22218
+ svg.setAttribute('height', scrollH);
22219
+ svg.innerHTML = '';
22220
+
22221
+ var scrollLeft = container.scrollLeft;
22222
+ var scrollTop = container.scrollTop;
22223
+
22224
+ edges.forEach(function(edge) {
22225
+ var fromEl = container.querySelector('[data-flow-id="' + edge.from + '"]');
22226
+ var toEl = container.querySelector('[data-flow-id="' + edge.to + '"]');
22227
+ if (!fromEl || !toEl) return;
22228
+
22229
+ var fr = fromEl.getBoundingClientRect();
22230
+ var tr = toEl.getBoundingClientRect();
22231
+ var x1 = fr.right - rect.left + scrollLeft;
22232
+ var y1 = fr.top + fr.height / 2 - rect.top + scrollTop;
22233
+ var x2 = tr.left - rect.left + scrollLeft;
22234
+ var y2 = tr.top + tr.height / 2 - rect.top + scrollTop;
22235
+ var mx = (x1 + x2) / 2;
22236
+
22237
+ var path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
22238
+ path.setAttribute('d', 'M' + x1 + ',' + y1 + ' C' + mx + ',' + y1 + ' ' + mx + ',' + y2 + ' ' + x2 + ',' + y2);
22239
+ path.setAttribute('fill', 'none');
22240
+ path.setAttribute('stroke', '#2a2e3a');
22241
+ path.setAttribute('stroke-width', '1.5');
22242
+ path.dataset.from = edge.from;
22243
+ path.dataset.to = edge.to;
22244
+ svg.appendChild(path);
22245
+ });
22246
+ }
22247
+
22248
+ function findConnected(startId) {
22249
+ var visited = {};
22250
+ visited[startId] = true;
22251
+ var queue = [startId];
22252
+ while (queue.length) {
22253
+ var id = queue.shift();
22254
+ (fwd[id] || []).forEach(function(n) {
22255
+ if (!visited[n]) { visited[n] = true; queue.push(n); }
22256
+ });
22257
+ }
22258
+ queue = [startId];
22259
+ while (queue.length) {
22260
+ var id = queue.shift();
22261
+ (bwd[id] || []).forEach(function(n) {
22262
+ if (!visited[n]) { visited[n] = true; queue.push(n); }
22263
+ });
22264
+ }
22265
+ return visited;
22266
+ }
22267
+
22268
+ function highlight(hoveredId) {
22269
+ var connected = findConnected(hoveredId);
22270
+ container.querySelectorAll('.flow-node').forEach(function(n) {
22271
+ if (connected[n.dataset.flowId]) {
22272
+ n.classList.add('flow-lit'); n.classList.remove('flow-dim');
22273
+ } else {
22274
+ n.classList.add('flow-dim'); n.classList.remove('flow-lit');
22275
+ }
22276
+ });
22277
+ svg.querySelectorAll('path').forEach(function(p) {
22278
+ if (connected[p.dataset.from] && connected[p.dataset.to]) {
22279
+ p.classList.add('flow-line-lit'); p.classList.remove('flow-line-dim');
22280
+ } else {
22281
+ p.classList.add('flow-line-dim'); p.classList.remove('flow-line-lit');
22282
+ }
22283
+ });
22284
+ }
22285
+
22286
+ function clearHighlight() {
22287
+ container.querySelectorAll('.flow-node').forEach(function(n) { n.classList.remove('flow-lit', 'flow-dim'); });
22288
+ svg.querySelectorAll('path').forEach(function(p) { p.classList.remove('flow-line-lit', 'flow-line-dim'); });
22289
+ }
22290
+
22291
+ var activeId = null;
22292
+ container.addEventListener('click', function(e) {
22293
+ if (e.target.closest('a')) return;
22294
+ var node = e.target.closest('.flow-node');
22295
+ var clickedId = node ? node.dataset.flowId : null;
22296
+ if (!clickedId || clickedId === activeId) {
22297
+ activeId = null; clearHighlight(); return;
22298
+ }
22299
+ activeId = clickedId;
22300
+ highlight(clickedId);
22301
+ });
22302
+
22303
+ function drawAndHighlight() {
22304
+ drawLines();
22305
+ if (activeId) highlight(activeId);
22306
+ }
22307
+
22308
+ requestAnimationFrame(function() { setTimeout(drawAndHighlight, 100); });
22309
+ window.addEventListener('resize', drawAndHighlight);
22310
+ container.addEventListener('scroll', drawAndHighlight);
22311
+ new ResizeObserver(drawAndHighlight).observe(container);
22312
+ })();
22313
+ </script>`;
22314
+ }
22315
+ var EVENT_ICONS = {
22316
+ "created": "\u{1F7E2}",
22317
+ "source-linked": "\u{1F535}",
22318
+ "child-spawned": "\u{1F7E1}",
22319
+ "assessment": "\u{1F7E3}",
22320
+ "jira-sync": "\u{1F537}"
22321
+ };
22322
+ function buildLineageTimeline(events) {
22323
+ if (events.length === 0) {
22324
+ return "";
22325
+ }
22326
+ const entries = events.map((event) => {
22327
+ const icon = EVENT_ICONS[event.type] ?? "\u26AA";
22328
+ const date5 = event.date ? formatDate(event.date) : "";
22329
+ const time3 = event.date?.slice(11, 16) ?? "";
22330
+ const label = linkArtifactIds(escapeHtml(event.label));
22331
+ return `
22332
+ <div class="lineage-entry lineage-${escapeHtml(event.type)}">
22333
+ <div class="lineage-marker">${icon}</div>
22334
+ <div class="lineage-content">
22335
+ <span class="lineage-date">${escapeHtml(date5)} ${escapeHtml(time3)}</span>
22336
+ <span class="lineage-label">${label}</span>
22337
+ </div>
22338
+ </div>`;
22339
+ });
22340
+ return `
22341
+ <div class="lineage-timeline">
22342
+ <h3>Lineage</h3>
22343
+ ${entries.join("\n")}
22344
+ </div>`;
22345
+ }
22346
+
22347
+ // src/web/templates/pages/document-detail.ts
22348
+ function documentDetailPage(doc, store) {
22349
+ const fm = doc.frontmatter;
22350
+ const label = typeLabel(fm.type);
22351
+ const skipKeys = /* @__PURE__ */ new Set(["title", "type", "assessmentHistory", "assessmentSummary"]);
22352
+ const entries = Object.entries(fm).filter(
22353
+ ([key, value]) => !skipKeys.has(key) && value != null && typeof value !== "object"
22354
+ );
22355
+ const arrayEntries = Object.entries(fm).filter(
22356
+ ([key, value]) => !skipKeys.has(key) && Array.isArray(value) && value.every((v) => typeof v === "string")
22357
+ );
22358
+ const allEntries = [
22359
+ ...entries.filter(([, v]) => !Array.isArray(v)),
22360
+ ...arrayEntries
22361
+ ];
22362
+ const dtDd = allEntries.map(([key, value]) => {
22363
+ let rendered;
22364
+ if (key === "status") {
22365
+ rendered = statusBadge(value);
22366
+ } else if (key === "tags" && Array.isArray(value)) {
22367
+ rendered = value.map((t) => `<span class="badge badge-default">${escapeHtml(t)}</span>`).join(" ");
22368
+ } else if (key === "created" || key === "updated" || key === "lastAssessedAt" || key === "lastJiraSyncAt") {
22369
+ rendered = formatDate(value);
22370
+ } else {
22371
+ rendered = linkArtifactIds(escapeHtml(String(value)));
22372
+ }
22373
+ return `<dt>${escapeHtml(key)}</dt><dd>${rendered}</dd>`;
22374
+ }).join("\n ");
22375
+ const rawHistory = Array.isArray(fm.assessmentHistory) ? fm.assessmentHistory : fm.assessmentSummary && typeof fm.assessmentSummary === "object" ? [fm.assessmentSummary] : [];
22376
+ const assessmentHistory = rawHistory.filter(isValidAssessmentEntry).sort((a, b) => (b.generatedAt ?? "").localeCompare(a.generatedAt ?? ""));
22377
+ const timelineHtml = assessmentHistory.length > 0 ? renderAssessmentTimeline(assessmentHistory) : "";
22378
+ return `
22379
+ <div class="breadcrumb">
22380
+ <a href="/">Overview</a><span class="sep">/</span>
22381
+ <a href="/docs/${fm.type}">${escapeHtml(label)}s</a><span class="sep">/</span>
22382
+ ${escapeHtml(fm.id)}
22383
+ </div>
22384
+
22385
+ <div class="page-header">
22386
+ <h2>${escapeHtml(fm.title)}${integrationIcons(fm)}</h2>
22387
+ <div class="subtitle">${escapeHtml(fm.id)} &middot; ${escapeHtml(label)}</div>
22388
+ </div>
22389
+
22390
+ <div class="detail-meta">
22391
+ <dl>
22392
+ ${dtDd}
22393
+ </dl>
22394
+ </div>
22395
+
22396
+ ${doc.content.trim() ? `<div class="detail-content">${renderMarkdown(doc.content)}</div>` : ""}
22397
+
22398
+ ${timelineHtml}
22399
+
22400
+ ${store ? renderRelationshipsAndLineage(store, fm.id) : ""}
22401
+ `;
22402
+ }
22403
+ function renderRelationshipsAndLineage(store, docId) {
22404
+ const parts = [];
22405
+ const relationships = getArtifactRelationships(store, docId);
22406
+ if (relationships) {
22407
+ const graphHtml = buildArtifactRelationGraph(relationships);
22408
+ parts.push(collapsibleSection("rel-graph-" + docId, "Relationships", graphHtml));
22409
+ }
22410
+ const events = getArtifactLineageEvents(store, docId);
22411
+ if (events.length > 0) {
22412
+ const lineageHtml = buildLineageTimeline(events);
22413
+ parts.push(collapsibleSection("lineage-" + docId, "Lineage", lineageHtml, { defaultCollapsed: true }));
22414
+ }
22415
+ return parts.join("\n");
22416
+ }
22417
+ function isValidAssessmentEntry(value) {
22418
+ if (typeof value !== "object" || value === null) return false;
22419
+ const obj = value;
22420
+ if (typeof obj.generatedAt !== "string") return false;
22421
+ if (obj.signals !== void 0 && !Array.isArray(obj.signals)) return false;
22422
+ return true;
22423
+ }
22424
+ function normalizeEntry(entry) {
22425
+ return {
22426
+ generatedAt: entry.generatedAt ?? "",
22427
+ commentSummary: typeof entry.commentSummary === "string" ? entry.commentSummary : null,
22428
+ commentAnalysisProgress: typeof entry.commentAnalysisProgress === "number" ? entry.commentAnalysisProgress : null,
22429
+ signals: Array.isArray(entry.signals) ? entry.signals.filter((s) => typeof s === "string") : [],
22430
+ childCount: typeof entry.childCount === "number" ? entry.childCount : 0,
22431
+ childDoneCount: typeof entry.childDoneCount === "number" ? entry.childDoneCount : 0,
22432
+ childRollupProgress: typeof entry.childRollupProgress === "number" ? entry.childRollupProgress : null,
22433
+ linkedIssueCount: typeof entry.linkedIssueCount === "number" ? entry.linkedIssueCount : 0,
22434
+ blockerProgress: typeof entry.blockerProgress === "number" ? entry.blockerProgress : null,
22435
+ totalBlockers: typeof entry.totalBlockers === "number" ? entry.totalBlockers : 0,
22436
+ resolvedBlockers: typeof entry.resolvedBlockers === "number" ? entry.resolvedBlockers : 0
22437
+ };
22438
+ }
22439
+ function renderAssessmentTimeline(history) {
22440
+ const entries = history.map((raw, i) => {
22441
+ const entry = normalizeEntry(raw);
22442
+ const date5 = entry.generatedAt ? formatDate(entry.generatedAt) : "Unknown date";
22443
+ const time3 = entry.generatedAt?.slice(11, 16) ?? "";
22444
+ const isLatest = i === 0;
22445
+ const parts = [];
22446
+ if (entry.commentSummary) {
22447
+ parts.push(`<div class="assessment-comment">${linkArtifactIds(escapeHtml(entry.commentSummary))}</div>`);
22448
+ }
22449
+ if (entry.commentAnalysisProgress !== null) {
22450
+ parts.push(`<div class="assessment-stat">\u{1F4CA} Comment-derived progress: <strong>${entry.commentAnalysisProgress}%</strong></div>`);
22451
+ }
22452
+ if (entry.childCount > 0) {
22453
+ const bar = progressBarHtml(entry.childRollupProgress ?? 0);
22454
+ parts.push(`<div class="assessment-stat">\u{1F476} Children: ${entry.childDoneCount}/${entry.childCount} done ${bar} ${entry.childRollupProgress ?? 0}%</div>`);
22455
+ }
22456
+ if (entry.totalBlockers > 0) {
22457
+ const bar = progressBarHtml(entry.blockerProgress ?? 0);
22458
+ parts.push(`<div class="assessment-stat">\u{1F6A7} Blockers: ${entry.resolvedBlockers}/${entry.totalBlockers} resolved ${bar} ${entry.blockerProgress ?? 0}%</div>`);
22459
+ }
22460
+ if (entry.linkedIssueCount > 0) {
22461
+ parts.push(`<div class="assessment-stat">\u{1F517} Linked issues: ${entry.linkedIssueCount}</div>`);
22462
+ }
22463
+ if (entry.signals.length > 0) {
22464
+ const signalItems = entry.signals.map((s) => `<li>${linkArtifactIds(escapeHtml(s))}</li>`).join("");
22465
+ parts.push(`<ul class="assessment-signals">${signalItems}</ul>`);
22466
+ }
22467
+ return `
22468
+ <div class="assessment-entry${isLatest ? " assessment-latest" : ""}">
22469
+ <div class="assessment-header">
22470
+ <span class="assessment-date">${escapeHtml(date5)} ${escapeHtml(time3)}</span>
22471
+ ${isLatest ? '<span class="badge badge-default">Latest</span>' : ""}
22472
+ </div>
22473
+ ${parts.join("\n")}
22474
+ </div>`;
22475
+ });
22476
+ return `
22477
+ <div class="assessment-timeline">
22478
+ <h3>Assessment History</h3>
22479
+ ${entries.join("\n")}
22480
+ </div>`;
22481
+ }
22482
+ function progressBarHtml(pct) {
22483
+ const filled = Math.round(Math.max(0, Math.min(100, pct)) / 10);
22484
+ const empty = 10 - filled;
22485
+ return `<span class="progress-bar-inline">${"\u2588".repeat(filled)}${"\u2591".repeat(empty)}</span>`;
22486
+ }
22487
+
22488
+ // src/web/persona-views.ts
22489
+ var VIEWS = /* @__PURE__ */ new Map();
22490
+ var PAGE_RENDERERS = /* @__PURE__ */ new Map();
22491
+ function registerPersonaView(config2) {
22492
+ VIEWS.set(config2.shortName, config2);
22493
+ }
22494
+ function registerPersonaPage(persona, pageId, renderer) {
22495
+ PAGE_RENDERERS.set(`${persona}/${pageId}`, renderer);
22496
+ }
22497
+ function getPersonaView(mode) {
22498
+ if (!mode) return void 0;
22499
+ return VIEWS.get(mode);
22500
+ }
22501
+ function getPersonaPageRenderer(persona, pageId) {
22502
+ return PAGE_RENDERERS.get(`${persona}/${pageId}`);
22503
+ }
22504
+ function getAllPersonaViews() {
22505
+ return [...VIEWS.values()];
22506
+ }
22507
+ var VALID_PERSONAS = /* @__PURE__ */ new Set(["po", "dm", "tl"]);
22508
+ function parsePersonaFromUrl(params) {
22509
+ const value = params.get("persona")?.toLowerCase();
22510
+ if (value && VALID_PERSONAS.has(value)) return value;
22511
+ return null;
22512
+ }
22513
+ function parsePersonaFromPath(pathname) {
22514
+ const match = pathname.match(/^\/(po|dm|tl)(?:\/|$)/);
22515
+ return match ? match[1] : null;
22516
+ }
22517
+ function resolvePersona(pathname, params) {
22518
+ return parsePersonaFromPath(pathname) ?? parsePersonaFromUrl(params);
22519
+ }
22520
+ var SHARED_NAV_ITEMS = [
22521
+ { pageId: "timeline", label: "Timeline" },
22522
+ { pageId: "board", label: "Board" },
22523
+ { pageId: "upcoming", label: "Upcoming" },
22524
+ { pageId: "sprint-summary", label: "Sprint Summary" },
22525
+ { pageId: "gar", label: "GAR Report" },
22526
+ { pageId: "health", label: "Health" }
22527
+ ];
22528
+
22529
+ // src/web/templates/pages/persona-picker.ts
22530
+ function personaPickerPage() {
22531
+ const views = getAllPersonaViews();
22532
+ const cards = views.map(
22533
+ (v) => `
22534
+ <a href="/${v.shortName}/dashboard" class="persona-picker-card" style="--persona-card-accent: ${v.color}">
22535
+ <div class="persona-picker-name">${escapeHtml(v.displayName)}</div>
22536
+ <div class="persona-picker-desc">${escapeHtml(v.description)}</div>
22537
+ </a>`
22538
+ ).join("\n");
22539
+ return `
22540
+ <div class="persona-picker">
22541
+ <h2>Choose Your View</h2>
22542
+ <p class="persona-picker-subtitle">Select a role to see a curated dashboard with the pages most relevant to you.</p>
22543
+ <div class="persona-picker-grid">
22544
+ ${cards}
22545
+ </div>
22546
+ </div>`;
22547
+ }
22125
22548
 
22126
- function clearHighlight() {
22127
- container.querySelectorAll('.flow-node').forEach(function(n) { n.classList.remove('flow-lit', 'flow-dim'); });
22128
- svg.querySelectorAll('path').forEach(function(p) { p.classList.remove('flow-line-lit', 'flow-line-dim'); });
22129
- }
22549
+ // src/reports/sprint-summary/risk-assessment.ts
22550
+ import { query as query2 } from "@anthropic-ai/claude-agent-sdk";
22551
+ var SYSTEM_PROMPT2 = `You are a delivery management assistant generating a data-driven risk assessment.
22130
22552
 
22131
- var activeId = null;
22132
- container.addEventListener('click', function(e) {
22133
- // Let the ID link navigate normally
22134
- if (e.target.closest('a')) return;
22553
+ IMPORTANT: All the data you need is provided in the user message below. Do NOT attempt to look up, search for, or request additional information. Analyze ONLY the data given and produce your assessment immediately.
22135
22554
 
22136
- var node = e.target.closest('.flow-node');
22137
- var clickedId = node ? node.dataset.flowId : null;
22555
+ Produce a concise markdown assessment with these sections:
22138
22556
 
22139
- if (!clickedId || clickedId === activeId) {
22140
- activeId = null;
22141
- clearHighlight();
22142
- return;
22143
- }
22557
+ ## Status Assessment
22558
+ One-line verdict: is this risk actively being mitigated, stalled, or escalating?
22144
22559
 
22145
- activeId = clickedId;
22146
- highlight(clickedId);
22147
- });
22560
+ ## Related Activity
22561
+ What actions, decisions, or contributions are connected to this risk? How are they progressing? Be specific \u2014 reference artifact IDs from the data provided.
22148
22562
 
22149
- function drawAndHighlight() {
22150
- drawLines();
22151
- if (activeId) highlight(activeId);
22152
- }
22563
+ ## Trajectory
22564
+ Based on the data (status of related items, time remaining, ownership), is this risk trending toward resolution or toward becoming a blocker? Explain your reasoning with concrete evidence.
22153
22565
 
22154
- requestAnimationFrame(function() { setTimeout(drawAndHighlight, 100); });
22155
- window.addEventListener('resize', drawAndHighlight);
22156
- container.addEventListener('scroll', drawAndHighlight);
22157
- new ResizeObserver(drawAndHighlight).observe(container);
22158
- })();
22159
- </script>`;
22160
- }
22161
- function buildStatusPie(title, counts) {
22162
- const entries = Object.entries(counts).filter(([, v]) => v > 0);
22163
- if (entries.length === 0) {
22164
- return placeholder(`No data for ${title}.`);
22165
- }
22166
- const lines = [`pie title ${sanitize(title, 60)}`];
22167
- for (const [label, count] of entries) {
22168
- lines.push(` "${sanitize(label, 30)}" : ${count}`);
22566
+ ## Recommendation
22567
+ One concrete next step to move this risk toward resolution.
22568
+
22569
+ Rules:
22570
+ - Reference artifact IDs, dates, owners, and statuses from the provided data
22571
+ - Keep the tone professional and direct
22572
+ - Do NOT speculate beyond what the data supports \u2014 if information is insufficient, say so explicitly
22573
+ - Do NOT ask for more information or say you will look things up \u2014 everything you need is in the prompt
22574
+ - Produce the full assessment text directly`;
22575
+ async function generateRiskAssessment(data, riskId, store) {
22576
+ const risk = data.risks.find((r) => r.id === riskId);
22577
+ if (!risk) return "Risk not found in sprint data.";
22578
+ const prompt = buildSingleRiskPrompt(data, risk, store);
22579
+ const result = query2({
22580
+ prompt,
22581
+ options: {
22582
+ systemPrompt: SYSTEM_PROMPT2,
22583
+ maxTurns: 1,
22584
+ tools: [],
22585
+ allowedTools: []
22586
+ }
22587
+ });
22588
+ for await (const msg of result) {
22589
+ if (msg.type === "assistant") {
22590
+ const text = msg.message.content.find(
22591
+ (b) => b.type === "text"
22592
+ );
22593
+ if (text) return text.text;
22594
+ }
22169
22595
  }
22170
- return mermaidBlock(lines.join("\n"));
22596
+ return "Unable to generate risk assessment.";
22171
22597
  }
22172
- function buildHealthGauge(categories) {
22173
- const valid = categories.filter((c) => c.total > 0);
22174
- if (valid.length === 0) {
22175
- return placeholder("No completeness data available.");
22598
+ function buildSingleRiskPrompt(data, risk, store) {
22599
+ const sections = [];
22600
+ sections.push(`# Sprint: ${data.sprint.id} \u2014 ${data.sprint.title}`);
22601
+ if (data.sprint.goal) sections.push(`Goal: ${data.sprint.goal}`);
22602
+ sections.push(`Days remaining: ${data.timeline.daysRemaining} / ${data.timeline.totalDays}`);
22603
+ sections.push(`Completion: ${data.workItems.completionPct}%`);
22604
+ sections.push("");
22605
+ const doc = store.get(risk.id);
22606
+ sections.push(`# RISK: ${risk.id} \u2014 ${risk.title}`);
22607
+ sections.push(`Type: ${risk.type}`);
22608
+ if (doc) {
22609
+ sections.push(`Status: ${doc.frontmatter.status}`);
22610
+ if (doc.frontmatter.owner) sections.push(`Owner: ${doc.frontmatter.owner}`);
22611
+ if (doc.frontmatter.assignee) sections.push(`Assignee: ${doc.frontmatter.assignee}`);
22612
+ if (doc.frontmatter.priority) sections.push(`Priority: ${doc.frontmatter.priority}`);
22613
+ if (doc.frontmatter.dueDate) sections.push(`Due date: ${doc.frontmatter.dueDate}`);
22614
+ if (doc.frontmatter.created) sections.push(`Created: ${doc.frontmatter.created.slice(0, 10)}`);
22615
+ const tags = doc.frontmatter.tags ?? [];
22616
+ if (tags.length > 0) sections.push(`Tags: ${tags.join(", ")}`);
22617
+ if (doc.content.trim()) {
22618
+ sections.push(`
22619
+ Description:
22620
+ ${doc.content.trim()}`);
22621
+ }
22622
+ const allDocs = store.list();
22623
+ const relatedIds = /* @__PURE__ */ new Set();
22624
+ for (const d of allDocs) {
22625
+ if (d.frontmatter.aboutArtifact === risk.id) {
22626
+ relatedIds.add(d.frontmatter.id);
22627
+ }
22628
+ }
22629
+ const idPattern = /\b([A-Z]-\d{3,})\b/g;
22630
+ let match;
22631
+ while ((match = idPattern.exec(doc.content)) !== null) {
22632
+ relatedIds.add(match[1]);
22633
+ }
22634
+ const significantTags = tags.filter(
22635
+ (t) => !t.startsWith("sprint:") && !t.startsWith("focus:") && t !== "risk"
22636
+ );
22637
+ if (significantTags.length > 0) {
22638
+ for (const d of allDocs) {
22639
+ if (d.frontmatter.id === risk.id) continue;
22640
+ const dTags = d.frontmatter.tags ?? [];
22641
+ if (significantTags.some((t) => dTags.includes(t))) {
22642
+ relatedIds.add(d.frontmatter.id);
22643
+ }
22644
+ }
22645
+ }
22646
+ const about = doc.frontmatter.aboutArtifact;
22647
+ if (about) {
22648
+ relatedIds.add(about);
22649
+ for (const d of allDocs) {
22650
+ if (d.frontmatter.aboutArtifact === about && d.frontmatter.id !== risk.id) {
22651
+ relatedIds.add(d.frontmatter.id);
22652
+ }
22653
+ }
22654
+ }
22655
+ const relatedDocs = [...relatedIds].map((id) => store.get(id)).filter((d) => d != null).slice(0, 20);
22656
+ if (relatedDocs.length > 0) {
22657
+ sections.push(`
22658
+ ## Related Documents (${relatedDocs.length})`);
22659
+ for (const rd of relatedDocs) {
22660
+ const owner = rd.frontmatter.owner ?? "unowned";
22661
+ const summary = rd.content.trim().slice(0, 300);
22662
+ sections.push(
22663
+ `- ${rd.frontmatter.id} (${rd.frontmatter.type}) [${rd.frontmatter.status}] \u2014 ${rd.frontmatter.title}`
22664
+ );
22665
+ sections.push(` Owner: ${owner}${rd.frontmatter.dueDate ? `, Due: ${rd.frontmatter.dueDate}` : ""}`);
22666
+ if (summary) sections.push(` Summary: ${summary}${rd.content.trim().length > 300 ? "..." : ""}`);
22667
+ }
22668
+ }
22176
22669
  }
22177
- const pies = valid.map((cat) => {
22178
- const incomplete = cat.total - cat.complete;
22179
- const lines = [
22180
- `pie title ${sanitize(cat.name, 30)}`,
22181
- ` "Complete" : ${cat.complete}`,
22182
- ` "Incomplete" : ${incomplete}`
22183
- ];
22184
- return mermaidBlock(lines.join("\n"));
22185
- });
22186
- return `<div class="mermaid-row">${pies.join("\n")}</div>`;
22670
+ sections.push("");
22671
+ sections.push(`---`);
22672
+ sections.push(`
22673
+ Generate the risk assessment for ${risk.id} based on the data above.`);
22674
+ return sections.join("\n");
22675
+ }
22676
+
22677
+ // src/web/templates/persona-switcher.ts
22678
+ function renderPersonaSwitcher(current, _currentPath) {
22679
+ const views = getAllPersonaViews();
22680
+ if (views.length === 0) return "";
22681
+ const options = views.map(
22682
+ (v) => `<option value="${v.shortName}"${current === v.shortName ? " selected" : ""}>${escapeHtml(v.displayName)}</option>`
22683
+ ).join("\n ");
22684
+ return `
22685
+ <div class="persona-switcher">
22686
+ <label class="persona-label" for="persona-select">View</label>
22687
+ <select class="persona-select" id="persona-select" onchange="switchPersona(this.value)">
22688
+ ${options}
22689
+ </select>
22690
+ </div>
22691
+ <script>
22692
+ function switchPersona(value) {
22693
+ if (value) {
22694
+ window.location.href = '/' + value + '/dashboard';
22695
+ }
22696
+ }
22697
+ </script>`;
22187
22698
  }
22188
22699
 
22189
22700
  // src/web/templates/pages/po/dashboard.ts
@@ -25318,7 +25829,7 @@ function handleRequest(req, res, store, projectName, navGroups) {
25318
25829
  notFound(res, projectName, navGroups, pathname, persona, pOpts);
25319
25830
  return;
25320
25831
  }
25321
- const body = documentDetailPage(doc);
25832
+ const body = documentDetailPage(doc, store);
25322
25833
  respond(res, layout({ title: `${id} \u2014 ${doc.frontmatter.title}`, activePath: `/docs/${type}`, projectName, navGroups, persona, ...pOpts }, body));
25323
25834
  return;
25324
25835
  }
@@ -26768,6 +27279,15 @@ function analyzeLinkedIssueSignals(linkedIssues, frontmatter, jiraKey, proposedU
26768
27279
  });
26769
27280
  }
26770
27281
  }
27282
+ function computeBlockerProgress(linkedIssues, prerequisiteWeight) {
27283
+ const blockerLinks = linkedIssues.filter(
27284
+ (l) => BLOCKER_LINK_PATTERNS.some((p) => l.relationship.toLowerCase().includes(p.split(" ")[0]))
27285
+ );
27286
+ if (blockerLinks.length === 0) return null;
27287
+ const resolved = blockerLinks.filter((l) => l.isDone).length;
27288
+ const blockerProgress = Math.round(resolved / blockerLinks.length * prerequisiteWeight * 100);
27289
+ return { blockerProgress, totalBlockers: blockerLinks.length, resolvedBlockers: resolved };
27290
+ }
26771
27291
  var LINKED_COMMENT_ANALYSIS_PROMPT = `You are a delivery management assistant analyzing Jira comments from linked issues for progress signals.
26772
27292
 
26773
27293
  For each linked issue below, read the comments and produce a 1-sentence summary focused on: impact on the parent issue, blockers, or decisions.
@@ -27181,6 +27701,39 @@ async function _assessArtifactRecursive(store, client, host, options, visited, d
27181
27701
  });
27182
27702
  }
27183
27703
  }
27704
+ const prerequisiteWeight = options.prerequisiteWeight ?? 0.3;
27705
+ const blockerResult = computeBlockerProgress(linkedIssues, prerequisiteWeight);
27706
+ let blockerProgressValue = null;
27707
+ let totalBlockersCount = 0;
27708
+ let resolvedBlockersCount = 0;
27709
+ if (blockerResult && !fm.progressOverride && !DONE_STATUSES15.has(fm.status)) {
27710
+ blockerProgressValue = blockerResult.blockerProgress;
27711
+ totalBlockersCount = blockerResult.totalBlockers;
27712
+ resolvedBlockersCount = blockerResult.resolvedBlockers;
27713
+ const lastProgressUpdate = findLast(proposedUpdates, (u) => u.artifactId === fm.id && u.field === "progress");
27714
+ const implementationProgress = lastProgressUpdate ? lastProgressUpdate.proposedValue : currentProgress;
27715
+ const combinedProgress = Math.round(
27716
+ blockerResult.blockerProgress + implementationProgress * (1 - prerequisiteWeight)
27717
+ );
27718
+ const estimatedProgress = Math.max(currentProgress, combinedProgress);
27719
+ if (estimatedProgress !== currentProgress && estimatedProgress !== implementationProgress) {
27720
+ for (let i = proposedUpdates.length - 1; i >= 0; i--) {
27721
+ if (proposedUpdates[i].artifactId === fm.id && proposedUpdates[i].field === "progress") {
27722
+ proposedUpdates.splice(i, 1);
27723
+ }
27724
+ }
27725
+ proposedUpdates.push({
27726
+ artifactId: fm.id,
27727
+ field: "progress",
27728
+ currentValue: currentProgress,
27729
+ proposedValue: estimatedProgress,
27730
+ reason: `Blocker resolution (${resolvedBlockersCount}/${totalBlockersCount}) + implementation \u2192 dependency-weighted progress ${estimatedProgress}%`
27731
+ });
27732
+ }
27733
+ } else if (blockerResult) {
27734
+ totalBlockersCount = blockerResult.totalBlockers;
27735
+ resolvedBlockersCount = blockerResult.resolvedBlockers;
27736
+ }
27184
27737
  const signals = buildSignals(commentSignals, linkedIssues, statusDrift, proposedMarvinStatus);
27185
27738
  const appliedUpdates = [];
27186
27739
  if (options.applyUpdates && proposedUpdates.length > 0) {
@@ -27223,7 +27776,10 @@ async function _assessArtifactRecursive(store, client, host, options, visited, d
27223
27776
  commentAnalysisProgress,
27224
27777
  signals,
27225
27778
  children,
27226
- linkedIssues
27779
+ linkedIssues,
27780
+ blockerProgressValue,
27781
+ totalBlockersCount,
27782
+ resolvedBlockersCount
27227
27783
  );
27228
27784
  const existingHistory = Array.isArray(fm.assessmentHistory) ? fm.assessmentHistory : [];
27229
27785
  const legacySummary = fm.assessmentSummary;
@@ -27272,6 +27828,9 @@ async function _assessArtifactRecursive(store, client, host, options, visited, d
27272
27828
  commentAnalysisProgress,
27273
27829
  linkedIssues,
27274
27830
  linkedIssueSignals,
27831
+ blockerProgress: blockerProgressValue,
27832
+ totalBlockers: totalBlockersCount,
27833
+ resolvedBlockers: resolvedBlockersCount,
27275
27834
  children,
27276
27835
  proposedUpdates: options.applyUpdates ? [] : proposedUpdates,
27277
27836
  appliedUpdates,
@@ -27451,6 +28010,9 @@ function emptyArtifactReport(artifactId, errors) {
27451
28010
  commentAnalysisProgress: null,
27452
28011
  linkedIssues: [],
27453
28012
  linkedIssueSignals: [],
28013
+ blockerProgress: null,
28014
+ totalBlockers: 0,
28015
+ resolvedBlockers: 0,
27454
28016
  children: [],
27455
28017
  proposedUpdates: [],
27456
28018
  appliedUpdates: [],
@@ -27458,7 +28020,7 @@ function emptyArtifactReport(artifactId, errors) {
27458
28020
  errors
27459
28021
  };
27460
28022
  }
27461
- function buildAssessmentSummary(commentSummary, commentAnalysisProgress, signals, children, linkedIssues) {
28023
+ function buildAssessmentSummary(commentSummary, commentAnalysisProgress, signals, children, linkedIssues, blockerProgress = null, totalBlockers = 0, resolvedBlockers = 0) {
27462
28024
  const childProgressValues = children.map((c) => {
27463
28025
  const updates = c.appliedUpdates.length > 0 ? c.appliedUpdates : c.proposedUpdates;
27464
28026
  const lastStatus = findLast(updates, (u) => u.field === "status");
@@ -27482,7 +28044,10 @@ function buildAssessmentSummary(commentSummary, commentAnalysisProgress, signals
27482
28044
  childCount: children.length,
27483
28045
  childDoneCount,
27484
28046
  childRollupProgress,
27485
- linkedIssueCount: linkedIssues.length
28047
+ linkedIssueCount: linkedIssues.length,
28048
+ blockerProgress,
28049
+ totalBlockers,
28050
+ resolvedBlockers
27486
28051
  };
27487
28052
  }
27488
28053
  function formatArtifactReport(report) {
@@ -27518,6 +28083,12 @@ function formatArtifactReport(report) {
27518
28083
  }
27519
28084
  parts.push("");
27520
28085
  }
28086
+ if (report.totalBlockers > 0) {
28087
+ parts.push(`## Blocker Resolution`);
28088
+ const bpLabel = report.blockerProgress !== null ? `${report.blockerProgress}%` : "n/a (skipped)";
28089
+ parts.push(` ${report.resolvedBlockers}/${report.totalBlockers} blockers resolved \u2192 ${bpLabel} prerequisite progress`);
28090
+ parts.push("");
28091
+ }
27521
28092
  if (report.children.length > 0) {
27522
28093
  const doneCount = report.children.filter((c) => DONE_STATUSES15.has(c.marvinStatus)).length;
27523
28094
  const childProgress = Math.round(
@@ -28416,10 +28987,11 @@ function createJiraTools(store, projectConfig) {
28416
28987
  // --- Single-artifact assessment ---
28417
28988
  tool20(
28418
28989
  "assess_artifact",
28419
- "Deep assessment of a single Marvin artifact (task, action, or epic). Fetches live Jira status, analyzes comments with LLM, traverses all linked issues, detects drift, rolls up child progress, and extracts contextual signals (blockers, unblocks, handoffs, superseded work).",
28990
+ "Deep assessment of a single Marvin artifact (task, action, or epic). Fetches live Jira status, analyzes comments with LLM, traverses all linked issues, detects drift, rolls up child progress, computes dependency-weighted progress from blocker resolution, and extracts contextual signals (blockers, unblocks, handoffs, superseded work).",
28420
28991
  {
28421
28992
  artifactId: external_exports.string().describe("Marvin artifact ID (e.g. 'T-063', 'A-151', 'E-003')"),
28422
- applyUpdates: external_exports.boolean().optional().describe("Apply proposed status/progress updates to the artifact (default false)")
28993
+ applyUpdates: external_exports.boolean().optional().describe("Apply proposed status/progress updates to the artifact (default false)"),
28994
+ prerequisiteWeight: external_exports.number().min(0).max(1).optional().describe("Weight for blocker-resolution progress signal (0-1, default 0.3). Portion of effort attributed to dependency readiness.")
28423
28995
  },
28424
28996
  async (args) => {
28425
28997
  const jira = createJiraClient(jiraUserConfig);
@@ -28431,6 +29003,7 @@ function createJiraTools(store, projectConfig) {
28431
29003
  {
28432
29004
  artifactId: args.artifactId,
28433
29005
  applyUpdates: args.applyUpdates ?? false,
29006
+ prerequisiteWeight: args.prerequisiteWeight,
28434
29007
  statusMap
28435
29008
  }
28436
29009
  );
@@ -33845,7 +34418,7 @@ function createProgram() {
33845
34418
  const program2 = new Command();
33846
34419
  program2.name("marvin").description(
33847
34420
  "AI-powered product development assistant with Product Owner, Delivery Manager, and Technical Lead personas"
33848
- ).version("0.5.25");
34421
+ ).version("0.5.27");
33849
34422
  program2.command("init").description("Initialize a new Marvin project in the current directory").action(async () => {
33850
34423
  await initCommand();
33851
34424
  });