mrvn-cli 0.5.24 → 0.5.26

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"]);
@@ -19522,8 +19737,30 @@ function inline(text) {
19522
19737
  s = s.replace(/__([^_]+)__/g, "<strong>$1</strong>");
19523
19738
  s = s.replace(/\*([^*]+)\*/g, "<em>$1</em>");
19524
19739
  s = s.replace(/_([^_]+)_/g, "<em>$1</em>");
19740
+ s = linkArtifactIds(s);
19525
19741
  return s;
19526
19742
  }
19743
+ var ID_PREFIX_TO_TYPE = (() => {
19744
+ const entries = [];
19745
+ for (const [type, prefix] of Object.entries(CORE_ID_PREFIXES)) {
19746
+ entries.push([prefix, type]);
19747
+ }
19748
+ for (const reg of COMMON_REGISTRATIONS) {
19749
+ if (!entries.some(([p]) => p === reg.idPrefix)) {
19750
+ entries.push([reg.idPrefix, reg.type]);
19751
+ }
19752
+ }
19753
+ entries.sort((a, b) => b[0].length - a[0].length);
19754
+ return new Map(entries);
19755
+ })();
19756
+ function linkArtifactIds(html) {
19757
+ return html.replace(/\b([A-Z]{1,3})-(\d{3,})\b/g, (match, prefix, num) => {
19758
+ const type = ID_PREFIX_TO_TYPE.get(prefix);
19759
+ if (!type) return match;
19760
+ const id = `${prefix}-${num}`;
19761
+ return `<a href="/docs/${type}/${id}" class="artifact-link">${match}</a>`;
19762
+ });
19763
+ }
19527
19764
  function layout(opts, body) {
19528
19765
  const switcherHtml = opts.personaSwitcherHtml ?? "";
19529
19766
  let navHtml;
@@ -20235,6 +20472,75 @@ tr:hover td {
20235
20472
  margin: 0.75rem 0;
20236
20473
  }
20237
20474
 
20475
+ /* Artifact cross-links */
20476
+ a.artifact-link {
20477
+ color: var(--accent);
20478
+ text-decoration: none;
20479
+ font-weight: 500;
20480
+ border-bottom: 1px dotted var(--accent);
20481
+ }
20482
+ a.artifact-link:hover {
20483
+ border-bottom-style: solid;
20484
+ }
20485
+
20486
+ /* Assessment timeline */
20487
+ .assessment-timeline {
20488
+ margin-top: 1.5rem;
20489
+ }
20490
+ .assessment-timeline h3 {
20491
+ font-size: 1rem;
20492
+ font-weight: 600;
20493
+ margin-bottom: 0.75rem;
20494
+ }
20495
+ .assessment-entry {
20496
+ background: var(--bg-card);
20497
+ border: 1px solid var(--border);
20498
+ border-radius: var(--radius);
20499
+ padding: 0.75rem 1rem;
20500
+ margin-bottom: 0.75rem;
20501
+ }
20502
+ .assessment-entry.assessment-latest {
20503
+ border-left: 3px solid var(--accent);
20504
+ }
20505
+ .assessment-header {
20506
+ display: flex;
20507
+ align-items: center;
20508
+ gap: 0.5rem;
20509
+ margin-bottom: 0.5rem;
20510
+ }
20511
+ .assessment-date {
20512
+ font-size: 0.8rem;
20513
+ color: var(--text-dim);
20514
+ font-family: var(--mono);
20515
+ }
20516
+ .assessment-comment {
20517
+ font-size: 0.875rem;
20518
+ line-height: 1.6;
20519
+ margin-bottom: 0.5rem;
20520
+ }
20521
+ .assessment-stat {
20522
+ font-size: 0.8rem;
20523
+ color: var(--text-dim);
20524
+ margin-bottom: 0.25rem;
20525
+ }
20526
+ .assessment-stat strong {
20527
+ color: var(--text);
20528
+ }
20529
+ .assessment-signals {
20530
+ list-style: none;
20531
+ padding: 0;
20532
+ margin: 0.5rem 0 0;
20533
+ }
20534
+ .assessment-signals li {
20535
+ font-size: 0.8rem;
20536
+ padding: 0.15rem 0;
20537
+ }
20538
+ .progress-bar-inline {
20539
+ font-family: var(--mono);
20540
+ font-size: 0.75rem;
20541
+ letter-spacing: -0.5px;
20542
+ }
20543
+
20238
20544
  /* Filters */
20239
20545
  .filters {
20240
20546
  display: flex;
@@ -20469,6 +20775,65 @@ tr:hover td {
20469
20775
  .flow-line-lit { stroke: var(--accent) !important; stroke-width: 2 !important; }
20470
20776
  .flow-line-dim { opacity: 0.08; }
20471
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
+
20472
20837
  /* Gantt truncation note */
20473
20838
  .mermaid-note {
20474
20839
  font-size: 0.75rem;
@@ -21395,282 +21760,27 @@ function documentsPage(data) {
21395
21760
  `;
21396
21761
  }
21397
21762
 
21398
- // src/web/templates/pages/document-detail.ts
21399
- function documentDetailPage(doc) {
21400
- const fm = doc.frontmatter;
21401
- const label = typeLabel(fm.type);
21402
- const skipKeys = /* @__PURE__ */ new Set(["title", "type"]);
21403
- const entries = Object.entries(fm).filter(
21404
- ([key]) => !skipKeys.has(key) && fm[key] != null
21405
- );
21406
- const dtDd = entries.map(([key, value]) => {
21407
- let rendered;
21408
- if (key === "status") {
21409
- rendered = statusBadge(value);
21410
- } else if (key === "tags" && Array.isArray(value)) {
21411
- rendered = value.map((t) => `<span class="badge badge-default">${escapeHtml(t)}</span>`).join(" ");
21412
- } else if (key === "created" || key === "updated") {
21413
- rendered = formatDate(value);
21414
- } else {
21415
- rendered = escapeHtml(String(value));
21416
- }
21417
- return `<dt>${escapeHtml(key)}</dt><dd>${rendered}</dd>`;
21418
- }).join("\n ");
21419
- return `
21420
- <div class="breadcrumb">
21421
- <a href="/">Overview</a><span class="sep">/</span>
21422
- <a href="/docs/${fm.type}">${escapeHtml(label)}s</a><span class="sep">/</span>
21423
- ${escapeHtml(fm.id)}
21424
- </div>
21425
-
21426
- <div class="page-header">
21427
- <h2>${escapeHtml(fm.title)}${integrationIcons(fm)}</h2>
21428
- <div class="subtitle">${escapeHtml(fm.id)} &middot; ${escapeHtml(label)}</div>
21429
- </div>
21430
-
21431
- <div class="detail-meta">
21432
- <dl>
21433
- ${dtDd}
21434
- </dl>
21435
- </div>
21436
-
21437
- ${doc.content.trim() ? `<div class="detail-content">${renderMarkdown(doc.content)}</div>` : ""}
21438
- `;
21439
- }
21440
-
21441
- // src/web/persona-views.ts
21442
- var VIEWS = /* @__PURE__ */ new Map();
21443
- var PAGE_RENDERERS = /* @__PURE__ */ new Map();
21444
- function registerPersonaView(config2) {
21445
- VIEWS.set(config2.shortName, config2);
21446
- }
21447
- function registerPersonaPage(persona, pageId, renderer) {
21448
- PAGE_RENDERERS.set(`${persona}/${pageId}`, renderer);
21449
- }
21450
- function getPersonaView(mode) {
21451
- if (!mode) return void 0;
21452
- return VIEWS.get(mode);
21453
- }
21454
- function getPersonaPageRenderer(persona, pageId) {
21455
- return PAGE_RENDERERS.get(`${persona}/${pageId}`);
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;
21456
21767
  }
21457
- function getAllPersonaViews() {
21458
- return [...VIEWS.values()];
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>`;
21459
21773
  }
21460
- var VALID_PERSONAS = /* @__PURE__ */ new Set(["po", "dm", "tl"]);
21461
- function parsePersonaFromUrl(params) {
21462
- const value = params.get("persona")?.toLowerCase();
21463
- if (value && VALID_PERSONAS.has(value)) return value;
21464
- return null;
21774
+ function placeholder(message) {
21775
+ return `<div class="mermaid-container mermaid-empty"><p>${message}</p></div>`;
21465
21776
  }
21466
- function parsePersonaFromPath(pathname) {
21467
- const match = pathname.match(/^\/(po|dm|tl)(?:\/|$)/);
21468
- return match ? match[1] : null;
21777
+ function toMs(date5) {
21778
+ return (/* @__PURE__ */ new Date(date5 + "T00:00:00")).getTime();
21469
21779
  }
21470
- function resolvePersona(pathname, params) {
21471
- return parsePersonaFromPath(pathname) ?? parsePersonaFromUrl(params);
21472
- }
21473
- var SHARED_NAV_ITEMS = [
21474
- { pageId: "timeline", label: "Timeline" },
21475
- { pageId: "board", label: "Board" },
21476
- { pageId: "upcoming", label: "Upcoming" },
21477
- { pageId: "sprint-summary", label: "Sprint Summary" },
21478
- { pageId: "gar", label: "GAR Report" },
21479
- { pageId: "health", label: "Health" }
21480
- ];
21481
-
21482
- // src/web/templates/pages/persona-picker.ts
21483
- function personaPickerPage() {
21484
- const views = getAllPersonaViews();
21485
- const cards = views.map(
21486
- (v) => `
21487
- <a href="/${v.shortName}/dashboard" class="persona-picker-card" style="--persona-card-accent: ${v.color}">
21488
- <div class="persona-picker-name">${escapeHtml(v.displayName)}</div>
21489
- <div class="persona-picker-desc">${escapeHtml(v.description)}</div>
21490
- </a>`
21491
- ).join("\n");
21492
- return `
21493
- <div class="persona-picker">
21494
- <h2>Choose Your View</h2>
21495
- <p class="persona-picker-subtitle">Select a role to see a curated dashboard with the pages most relevant to you.</p>
21496
- <div class="persona-picker-grid">
21497
- ${cards}
21498
- </div>
21499
- </div>`;
21500
- }
21501
-
21502
- // src/reports/sprint-summary/risk-assessment.ts
21503
- import { query as query2 } from "@anthropic-ai/claude-agent-sdk";
21504
- var SYSTEM_PROMPT2 = `You are a delivery management assistant generating a data-driven risk assessment.
21505
-
21506
- 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.
21507
-
21508
- Produce a concise markdown assessment with these sections:
21509
-
21510
- ## Status Assessment
21511
- One-line verdict: is this risk actively being mitigated, stalled, or escalating?
21512
-
21513
- ## Related Activity
21514
- What actions, decisions, or contributions are connected to this risk? How are they progressing? Be specific \u2014 reference artifact IDs from the data provided.
21515
-
21516
- ## Trajectory
21517
- 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.
21518
-
21519
- ## Recommendation
21520
- One concrete next step to move this risk toward resolution.
21521
-
21522
- Rules:
21523
- - Reference artifact IDs, dates, owners, and statuses from the provided data
21524
- - Keep the tone professional and direct
21525
- - Do NOT speculate beyond what the data supports \u2014 if information is insufficient, say so explicitly
21526
- - Do NOT ask for more information or say you will look things up \u2014 everything you need is in the prompt
21527
- - Produce the full assessment text directly`;
21528
- async function generateRiskAssessment(data, riskId, store) {
21529
- const risk = data.risks.find((r) => r.id === riskId);
21530
- if (!risk) return "Risk not found in sprint data.";
21531
- const prompt = buildSingleRiskPrompt(data, risk, store);
21532
- const result = query2({
21533
- prompt,
21534
- options: {
21535
- systemPrompt: SYSTEM_PROMPT2,
21536
- maxTurns: 1,
21537
- tools: [],
21538
- allowedTools: []
21539
- }
21540
- });
21541
- for await (const msg of result) {
21542
- if (msg.type === "assistant") {
21543
- const text = msg.message.content.find(
21544
- (b) => b.type === "text"
21545
- );
21546
- if (text) return text.text;
21547
- }
21548
- }
21549
- return "Unable to generate risk assessment.";
21550
- }
21551
- function buildSingleRiskPrompt(data, risk, store) {
21552
- const sections = [];
21553
- sections.push(`# Sprint: ${data.sprint.id} \u2014 ${data.sprint.title}`);
21554
- if (data.sprint.goal) sections.push(`Goal: ${data.sprint.goal}`);
21555
- sections.push(`Days remaining: ${data.timeline.daysRemaining} / ${data.timeline.totalDays}`);
21556
- sections.push(`Completion: ${data.workItems.completionPct}%`);
21557
- sections.push("");
21558
- const doc = store.get(risk.id);
21559
- sections.push(`# RISK: ${risk.id} \u2014 ${risk.title}`);
21560
- sections.push(`Type: ${risk.type}`);
21561
- if (doc) {
21562
- sections.push(`Status: ${doc.frontmatter.status}`);
21563
- if (doc.frontmatter.owner) sections.push(`Owner: ${doc.frontmatter.owner}`);
21564
- if (doc.frontmatter.assignee) sections.push(`Assignee: ${doc.frontmatter.assignee}`);
21565
- if (doc.frontmatter.priority) sections.push(`Priority: ${doc.frontmatter.priority}`);
21566
- if (doc.frontmatter.dueDate) sections.push(`Due date: ${doc.frontmatter.dueDate}`);
21567
- if (doc.frontmatter.created) sections.push(`Created: ${doc.frontmatter.created.slice(0, 10)}`);
21568
- const tags = doc.frontmatter.tags ?? [];
21569
- if (tags.length > 0) sections.push(`Tags: ${tags.join(", ")}`);
21570
- if (doc.content.trim()) {
21571
- sections.push(`
21572
- Description:
21573
- ${doc.content.trim()}`);
21574
- }
21575
- const allDocs = store.list();
21576
- const relatedIds = /* @__PURE__ */ new Set();
21577
- for (const d of allDocs) {
21578
- if (d.frontmatter.aboutArtifact === risk.id) {
21579
- relatedIds.add(d.frontmatter.id);
21580
- }
21581
- }
21582
- const idPattern = /\b([A-Z]-\d{3,})\b/g;
21583
- let match;
21584
- while ((match = idPattern.exec(doc.content)) !== null) {
21585
- relatedIds.add(match[1]);
21586
- }
21587
- const significantTags = tags.filter(
21588
- (t) => !t.startsWith("sprint:") && !t.startsWith("focus:") && t !== "risk"
21589
- );
21590
- if (significantTags.length > 0) {
21591
- for (const d of allDocs) {
21592
- if (d.frontmatter.id === risk.id) continue;
21593
- const dTags = d.frontmatter.tags ?? [];
21594
- if (significantTags.some((t) => dTags.includes(t))) {
21595
- relatedIds.add(d.frontmatter.id);
21596
- }
21597
- }
21598
- }
21599
- const about = doc.frontmatter.aboutArtifact;
21600
- if (about) {
21601
- relatedIds.add(about);
21602
- for (const d of allDocs) {
21603
- if (d.frontmatter.aboutArtifact === about && d.frontmatter.id !== risk.id) {
21604
- relatedIds.add(d.frontmatter.id);
21605
- }
21606
- }
21607
- }
21608
- const relatedDocs = [...relatedIds].map((id) => store.get(id)).filter((d) => d != null).slice(0, 20);
21609
- if (relatedDocs.length > 0) {
21610
- sections.push(`
21611
- ## Related Documents (${relatedDocs.length})`);
21612
- for (const rd of relatedDocs) {
21613
- const owner = rd.frontmatter.owner ?? "unowned";
21614
- const summary = rd.content.trim().slice(0, 300);
21615
- sections.push(
21616
- `- ${rd.frontmatter.id} (${rd.frontmatter.type}) [${rd.frontmatter.status}] \u2014 ${rd.frontmatter.title}`
21617
- );
21618
- sections.push(` Owner: ${owner}${rd.frontmatter.dueDate ? `, Due: ${rd.frontmatter.dueDate}` : ""}`);
21619
- if (summary) sections.push(` Summary: ${summary}${rd.content.trim().length > 300 ? "..." : ""}`);
21620
- }
21621
- }
21622
- }
21623
- sections.push("");
21624
- sections.push(`---`);
21625
- sections.push(`
21626
- Generate the risk assessment for ${risk.id} based on the data above.`);
21627
- return sections.join("\n");
21628
- }
21629
-
21630
- // src/web/templates/persona-switcher.ts
21631
- function renderPersonaSwitcher(current, _currentPath) {
21632
- const views = getAllPersonaViews();
21633
- if (views.length === 0) return "";
21634
- const options = views.map(
21635
- (v) => `<option value="${v.shortName}"${current === v.shortName ? " selected" : ""}>${escapeHtml(v.displayName)}</option>`
21636
- ).join("\n ");
21637
- return `
21638
- <div class="persona-switcher">
21639
- <label class="persona-label" for="persona-select">View</label>
21640
- <select class="persona-select" id="persona-select" onchange="switchPersona(this.value)">
21641
- ${options}
21642
- </select>
21643
- </div>
21644
- <script>
21645
- function switchPersona(value) {
21646
- if (value) {
21647
- window.location.href = '/' + value + '/dashboard';
21648
- }
21649
- }
21650
- </script>`;
21651
- }
21652
-
21653
- // src/web/templates/mermaid.ts
21654
- function sanitize(text, maxLen = 40) {
21655
- const cleaned = text.replace(/["'`]/g, "").replace(/[\r\n]+/g, " ");
21656
- return cleaned.length > maxLen ? cleaned.slice(0, maxLen - 1) + "\u2026" : cleaned;
21657
- }
21658
- function mermaidBlock(definition, extraClass) {
21659
- const cls = ["mermaid-container", extraClass].filter(Boolean).join(" ");
21660
- return `<div class="${cls}"><pre class="mermaid">
21661
- ${definition}
21662
- </pre></div>`;
21663
- }
21664
- function placeholder(message) {
21665
- return `<div class="mermaid-container mermaid-empty"><p>${message}</p></div>`;
21666
- }
21667
- function toMs(date5) {
21668
- return (/* @__PURE__ */ new Date(date5 + "T00:00:00")).getTime();
21669
- }
21670
- function fmtDate(ms) {
21671
- const d = new Date(ms);
21672
- const months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
21673
- 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()}`;
21674
21784
  }
21675
21785
  function buildTimelineGantt(data, maxSprints = 6) {
21676
21786
  const sprintsWithDates = data.sprints.filter((s) => s.startDate && s.endDate).sort((a, b) => a.startDate < b.startDate ? -1 : 1);
@@ -21880,145 +21990,705 @@ function buildArtifactFlowchart(data) {
21880
21990
  var scrollLeft = container.scrollLeft;
21881
21991
  var scrollTop = container.scrollTop;
21882
21992
 
21883
- edges.forEach(function(edge) {
21884
- var fromEl = container.querySelector('[data-flow-id="' + edge.from + '"]');
21885
- var toEl = container.querySelector('[data-flow-id="' + edge.to + '"]');
21886
- if (!fromEl || !toEl) return;
21993
+ edges.forEach(function(edge) {
21994
+ var fromEl = container.querySelector('[data-flow-id="' + edge.from + '"]');
21995
+ var toEl = container.querySelector('[data-flow-id="' + edge.to + '"]');
21996
+ if (!fromEl || !toEl) return;
21997
+
21998
+ var fr = fromEl.getBoundingClientRect();
21999
+ var tr = toEl.getBoundingClientRect();
22000
+ var x1 = fr.right - rect.left + scrollLeft;
22001
+ var y1 = fr.top + fr.height / 2 - rect.top + scrollTop;
22002
+ var x2 = tr.left - rect.left + scrollLeft;
22003
+ var y2 = tr.top + tr.height / 2 - rect.top + scrollTop;
22004
+ var mx = (x1 + x2) / 2;
22005
+
22006
+ var path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
22007
+ path.setAttribute('d', 'M' + x1 + ',' + y1 + ' C' + mx + ',' + y1 + ' ' + mx + ',' + y2 + ' ' + x2 + ',' + y2);
22008
+ path.setAttribute('fill', 'none');
22009
+ path.setAttribute('stroke', '#2a2e3a');
22010
+ path.setAttribute('stroke-width', '1.5');
22011
+ path.dataset.from = edge.from;
22012
+ path.dataset.to = edge.to;
22013
+ svg.appendChild(path);
22014
+ });
22015
+ }
22016
+
22017
+ // Find directly related nodes via directed traversal
22018
+ // Follows forward edges (Feature\u2192Epic\u2192Sprint) and backward edges
22019
+ // (Sprint\u2192Epic\u2192Feature) separately to avoid sideways expansion
22020
+ function findConnected(startId) {
22021
+ var visited = {};
22022
+ visited[startId] = true;
22023
+ // Traverse forward (from\u2192to direction)
22024
+ var queue = [startId];
22025
+ while (queue.length) {
22026
+ var id = queue.shift();
22027
+ (fwd[id] || []).forEach(function(neighbor) {
22028
+ if (!visited[neighbor]) {
22029
+ visited[neighbor] = true;
22030
+ queue.push(neighbor);
22031
+ }
22032
+ });
22033
+ }
22034
+ // Traverse backward (to\u2192from direction)
22035
+ queue = [startId];
22036
+ while (queue.length) {
22037
+ var id = queue.shift();
22038
+ (bwd[id] || []).forEach(function(neighbor) {
22039
+ if (!visited[neighbor]) {
22040
+ visited[neighbor] = true;
22041
+ queue.push(neighbor);
22042
+ }
22043
+ });
22044
+ }
22045
+ return visited;
22046
+ }
22047
+
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
+ };
22435
+ }
22436
+ function renderAssessmentTimeline(history) {
22437
+ const entries = history.map((raw, i) => {
22438
+ const entry = normalizeEntry(raw);
22439
+ const date5 = entry.generatedAt ? formatDate(entry.generatedAt) : "Unknown date";
22440
+ const time3 = entry.generatedAt?.slice(11, 16) ?? "";
22441
+ const isLatest = i === 0;
22442
+ const parts = [];
22443
+ if (entry.commentSummary) {
22444
+ parts.push(`<div class="assessment-comment">${linkArtifactIds(escapeHtml(entry.commentSummary))}</div>`);
22445
+ }
22446
+ if (entry.commentAnalysisProgress !== null) {
22447
+ parts.push(`<div class="assessment-stat">\u{1F4CA} Comment-derived progress: <strong>${entry.commentAnalysisProgress}%</strong></div>`);
22448
+ }
22449
+ if (entry.childCount > 0) {
22450
+ const bar = progressBarHtml(entry.childRollupProgress ?? 0);
22451
+ parts.push(`<div class="assessment-stat">\u{1F476} Children: ${entry.childDoneCount}/${entry.childCount} done ${bar} ${entry.childRollupProgress ?? 0}%</div>`);
22452
+ }
22453
+ if (entry.linkedIssueCount > 0) {
22454
+ parts.push(`<div class="assessment-stat">\u{1F517} Linked issues: ${entry.linkedIssueCount}</div>`);
22455
+ }
22456
+ if (entry.signals.length > 0) {
22457
+ const signalItems = entry.signals.map((s) => `<li>${linkArtifactIds(escapeHtml(s))}</li>`).join("");
22458
+ parts.push(`<ul class="assessment-signals">${signalItems}</ul>`);
22459
+ }
22460
+ return `
22461
+ <div class="assessment-entry${isLatest ? " assessment-latest" : ""}">
22462
+ <div class="assessment-header">
22463
+ <span class="assessment-date">${escapeHtml(date5)} ${escapeHtml(time3)}</span>
22464
+ ${isLatest ? '<span class="badge badge-default">Latest</span>' : ""}
22465
+ </div>
22466
+ ${parts.join("\n")}
22467
+ </div>`;
22468
+ });
22469
+ return `
22470
+ <div class="assessment-timeline">
22471
+ <h3>Assessment History</h3>
22472
+ ${entries.join("\n")}
22473
+ </div>`;
22474
+ }
22475
+ function progressBarHtml(pct) {
22476
+ const filled = Math.round(Math.max(0, Math.min(100, pct)) / 10);
22477
+ const empty = 10 - filled;
22478
+ return `<span class="progress-bar-inline">${"\u2588".repeat(filled)}${"\u2591".repeat(empty)}</span>`;
22479
+ }
22480
+
22481
+ // src/web/persona-views.ts
22482
+ var VIEWS = /* @__PURE__ */ new Map();
22483
+ var PAGE_RENDERERS = /* @__PURE__ */ new Map();
22484
+ function registerPersonaView(config2) {
22485
+ VIEWS.set(config2.shortName, config2);
22486
+ }
22487
+ function registerPersonaPage(persona, pageId, renderer) {
22488
+ PAGE_RENDERERS.set(`${persona}/${pageId}`, renderer);
22489
+ }
22490
+ function getPersonaView(mode) {
22491
+ if (!mode) return void 0;
22492
+ return VIEWS.get(mode);
22493
+ }
22494
+ function getPersonaPageRenderer(persona, pageId) {
22495
+ return PAGE_RENDERERS.get(`${persona}/${pageId}`);
22496
+ }
22497
+ function getAllPersonaViews() {
22498
+ return [...VIEWS.values()];
22499
+ }
22500
+ var VALID_PERSONAS = /* @__PURE__ */ new Set(["po", "dm", "tl"]);
22501
+ function parsePersonaFromUrl(params) {
22502
+ const value = params.get("persona")?.toLowerCase();
22503
+ if (value && VALID_PERSONAS.has(value)) return value;
22504
+ return null;
22505
+ }
22506
+ function parsePersonaFromPath(pathname) {
22507
+ const match = pathname.match(/^\/(po|dm|tl)(?:\/|$)/);
22508
+ return match ? match[1] : null;
22509
+ }
22510
+ function resolvePersona(pathname, params) {
22511
+ return parsePersonaFromPath(pathname) ?? parsePersonaFromUrl(params);
22512
+ }
22513
+ var SHARED_NAV_ITEMS = [
22514
+ { pageId: "timeline", label: "Timeline" },
22515
+ { pageId: "board", label: "Board" },
22516
+ { pageId: "upcoming", label: "Upcoming" },
22517
+ { pageId: "sprint-summary", label: "Sprint Summary" },
22518
+ { pageId: "gar", label: "GAR Report" },
22519
+ { pageId: "health", label: "Health" }
22520
+ ];
22521
+
22522
+ // src/web/templates/pages/persona-picker.ts
22523
+ function personaPickerPage() {
22524
+ const views = getAllPersonaViews();
22525
+ const cards = views.map(
22526
+ (v) => `
22527
+ <a href="/${v.shortName}/dashboard" class="persona-picker-card" style="--persona-card-accent: ${v.color}">
22528
+ <div class="persona-picker-name">${escapeHtml(v.displayName)}</div>
22529
+ <div class="persona-picker-desc">${escapeHtml(v.description)}</div>
22530
+ </a>`
22531
+ ).join("\n");
22532
+ return `
22533
+ <div class="persona-picker">
22534
+ <h2>Choose Your View</h2>
22535
+ <p class="persona-picker-subtitle">Select a role to see a curated dashboard with the pages most relevant to you.</p>
22536
+ <div class="persona-picker-grid">
22537
+ ${cards}
22538
+ </div>
22539
+ </div>`;
22540
+ }
22541
+
22542
+ // src/reports/sprint-summary/risk-assessment.ts
22543
+ import { query as query2 } from "@anthropic-ai/claude-agent-sdk";
22544
+ var SYSTEM_PROMPT2 = `You are a delivery management assistant generating a data-driven risk assessment.
22545
+
22546
+ 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.
22547
+
22548
+ Produce a concise markdown assessment with these sections:
22549
+
22550
+ ## Status Assessment
22551
+ One-line verdict: is this risk actively being mitigated, stalled, or escalating?
22552
+
22553
+ ## Related Activity
22554
+ What actions, decisions, or contributions are connected to this risk? How are they progressing? Be specific \u2014 reference artifact IDs from the data provided.
22555
+
22556
+ ## Trajectory
22557
+ 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.
21887
22558
 
21888
- var fr = fromEl.getBoundingClientRect();
21889
- var tr = toEl.getBoundingClientRect();
21890
- var x1 = fr.right - rect.left + scrollLeft;
21891
- var y1 = fr.top + fr.height / 2 - rect.top + scrollTop;
21892
- var x2 = tr.left - rect.left + scrollLeft;
21893
- var y2 = tr.top + tr.height / 2 - rect.top + scrollTop;
21894
- var mx = (x1 + x2) / 2;
22559
+ ## Recommendation
22560
+ One concrete next step to move this risk toward resolution.
21895
22561
 
21896
- var path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
21897
- path.setAttribute('d', 'M' + x1 + ',' + y1 + ' C' + mx + ',' + y1 + ' ' + mx + ',' + y2 + ' ' + x2 + ',' + y2);
21898
- path.setAttribute('fill', 'none');
21899
- path.setAttribute('stroke', '#2a2e3a');
21900
- path.setAttribute('stroke-width', '1.5');
21901
- path.dataset.from = edge.from;
21902
- path.dataset.to = edge.to;
21903
- svg.appendChild(path);
21904
- });
22562
+ Rules:
22563
+ - Reference artifact IDs, dates, owners, and statuses from the provided data
22564
+ - Keep the tone professional and direct
22565
+ - Do NOT speculate beyond what the data supports \u2014 if information is insufficient, say so explicitly
22566
+ - Do NOT ask for more information or say you will look things up \u2014 everything you need is in the prompt
22567
+ - Produce the full assessment text directly`;
22568
+ async function generateRiskAssessment(data, riskId, store) {
22569
+ const risk = data.risks.find((r) => r.id === riskId);
22570
+ if (!risk) return "Risk not found in sprint data.";
22571
+ const prompt = buildSingleRiskPrompt(data, risk, store);
22572
+ const result = query2({
22573
+ prompt,
22574
+ options: {
22575
+ systemPrompt: SYSTEM_PROMPT2,
22576
+ maxTurns: 1,
22577
+ tools: [],
22578
+ allowedTools: []
22579
+ }
22580
+ });
22581
+ for await (const msg of result) {
22582
+ if (msg.type === "assistant") {
22583
+ const text = msg.message.content.find(
22584
+ (b) => b.type === "text"
22585
+ );
22586
+ if (text) return text.text;
22587
+ }
22588
+ }
22589
+ return "Unable to generate risk assessment.";
22590
+ }
22591
+ function buildSingleRiskPrompt(data, risk, store) {
22592
+ const sections = [];
22593
+ sections.push(`# Sprint: ${data.sprint.id} \u2014 ${data.sprint.title}`);
22594
+ if (data.sprint.goal) sections.push(`Goal: ${data.sprint.goal}`);
22595
+ sections.push(`Days remaining: ${data.timeline.daysRemaining} / ${data.timeline.totalDays}`);
22596
+ sections.push(`Completion: ${data.workItems.completionPct}%`);
22597
+ sections.push("");
22598
+ const doc = store.get(risk.id);
22599
+ sections.push(`# RISK: ${risk.id} \u2014 ${risk.title}`);
22600
+ sections.push(`Type: ${risk.type}`);
22601
+ if (doc) {
22602
+ sections.push(`Status: ${doc.frontmatter.status}`);
22603
+ if (doc.frontmatter.owner) sections.push(`Owner: ${doc.frontmatter.owner}`);
22604
+ if (doc.frontmatter.assignee) sections.push(`Assignee: ${doc.frontmatter.assignee}`);
22605
+ if (doc.frontmatter.priority) sections.push(`Priority: ${doc.frontmatter.priority}`);
22606
+ if (doc.frontmatter.dueDate) sections.push(`Due date: ${doc.frontmatter.dueDate}`);
22607
+ if (doc.frontmatter.created) sections.push(`Created: ${doc.frontmatter.created.slice(0, 10)}`);
22608
+ const tags = doc.frontmatter.tags ?? [];
22609
+ if (tags.length > 0) sections.push(`Tags: ${tags.join(", ")}`);
22610
+ if (doc.content.trim()) {
22611
+ sections.push(`
22612
+ Description:
22613
+ ${doc.content.trim()}`);
22614
+ }
22615
+ const allDocs = store.list();
22616
+ const relatedIds = /* @__PURE__ */ new Set();
22617
+ for (const d of allDocs) {
22618
+ if (d.frontmatter.aboutArtifact === risk.id) {
22619
+ relatedIds.add(d.frontmatter.id);
21905
22620
  }
21906
-
21907
- // Find directly related nodes via directed traversal
21908
- // Follows forward edges (Feature\u2192Epic\u2192Sprint) and backward edges
21909
- // (Sprint\u2192Epic\u2192Feature) separately to avoid sideways expansion
21910
- function findConnected(startId) {
21911
- var visited = {};
21912
- visited[startId] = true;
21913
- // Traverse forward (from\u2192to direction)
21914
- var queue = [startId];
21915
- while (queue.length) {
21916
- var id = queue.shift();
21917
- (fwd[id] || []).forEach(function(neighbor) {
21918
- if (!visited[neighbor]) {
21919
- visited[neighbor] = true;
21920
- queue.push(neighbor);
21921
- }
21922
- });
21923
- }
21924
- // Traverse backward (to\u2192from direction)
21925
- queue = [startId];
21926
- while (queue.length) {
21927
- var id = queue.shift();
21928
- (bwd[id] || []).forEach(function(neighbor) {
21929
- if (!visited[neighbor]) {
21930
- visited[neighbor] = true;
21931
- queue.push(neighbor);
21932
- }
21933
- });
22621
+ }
22622
+ const idPattern = /\b([A-Z]-\d{3,})\b/g;
22623
+ let match;
22624
+ while ((match = idPattern.exec(doc.content)) !== null) {
22625
+ relatedIds.add(match[1]);
22626
+ }
22627
+ const significantTags = tags.filter(
22628
+ (t) => !t.startsWith("sprint:") && !t.startsWith("focus:") && t !== "risk"
22629
+ );
22630
+ if (significantTags.length > 0) {
22631
+ for (const d of allDocs) {
22632
+ if (d.frontmatter.id === risk.id) continue;
22633
+ const dTags = d.frontmatter.tags ?? [];
22634
+ if (significantTags.some((t) => dTags.includes(t))) {
22635
+ relatedIds.add(d.frontmatter.id);
21934
22636
  }
21935
- return visited;
21936
22637
  }
21937
-
21938
- function highlight(hoveredId) {
21939
- var connected = findConnected(hoveredId);
21940
- container.querySelectorAll('.flow-node').forEach(function(n) {
21941
- if (connected[n.dataset.flowId]) {
21942
- n.classList.add('flow-lit');
21943
- n.classList.remove('flow-dim');
21944
- } else {
21945
- n.classList.add('flow-dim');
21946
- n.classList.remove('flow-lit');
21947
- }
21948
- });
21949
- svg.querySelectorAll('path').forEach(function(p) {
21950
- if (connected[p.dataset.from] && connected[p.dataset.to]) {
21951
- p.classList.add('flow-line-lit');
21952
- p.classList.remove('flow-line-dim');
21953
- } else {
21954
- p.classList.add('flow-line-dim');
21955
- p.classList.remove('flow-line-lit');
21956
- }
21957
- });
22638
+ }
22639
+ const about = doc.frontmatter.aboutArtifact;
22640
+ if (about) {
22641
+ relatedIds.add(about);
22642
+ for (const d of allDocs) {
22643
+ if (d.frontmatter.aboutArtifact === about && d.frontmatter.id !== risk.id) {
22644
+ relatedIds.add(d.frontmatter.id);
22645
+ }
21958
22646
  }
21959
-
21960
- function clearHighlight() {
21961
- container.querySelectorAll('.flow-node').forEach(function(n) { n.classList.remove('flow-lit', 'flow-dim'); });
21962
- svg.querySelectorAll('path').forEach(function(p) { p.classList.remove('flow-line-lit', 'flow-line-dim'); });
22647
+ }
22648
+ const relatedDocs = [...relatedIds].map((id) => store.get(id)).filter((d) => d != null).slice(0, 20);
22649
+ if (relatedDocs.length > 0) {
22650
+ sections.push(`
22651
+ ## Related Documents (${relatedDocs.length})`);
22652
+ for (const rd of relatedDocs) {
22653
+ const owner = rd.frontmatter.owner ?? "unowned";
22654
+ const summary = rd.content.trim().slice(0, 300);
22655
+ sections.push(
22656
+ `- ${rd.frontmatter.id} (${rd.frontmatter.type}) [${rd.frontmatter.status}] \u2014 ${rd.frontmatter.title}`
22657
+ );
22658
+ sections.push(` Owner: ${owner}${rd.frontmatter.dueDate ? `, Due: ${rd.frontmatter.dueDate}` : ""}`);
22659
+ if (summary) sections.push(` Summary: ${summary}${rd.content.trim().length > 300 ? "..." : ""}`);
21963
22660
  }
22661
+ }
22662
+ }
22663
+ sections.push("");
22664
+ sections.push(`---`);
22665
+ sections.push(`
22666
+ Generate the risk assessment for ${risk.id} based on the data above.`);
22667
+ return sections.join("\n");
22668
+ }
21964
22669
 
21965
- var activeId = null;
21966
- container.addEventListener('click', function(e) {
21967
- // Let the ID link navigate normally
21968
- if (e.target.closest('a')) return;
21969
-
21970
- var node = e.target.closest('.flow-node');
21971
- var clickedId = node ? node.dataset.flowId : null;
21972
-
21973
- if (!clickedId || clickedId === activeId) {
21974
- activeId = null;
21975
- clearHighlight();
21976
- return;
22670
+ // src/web/templates/persona-switcher.ts
22671
+ function renderPersonaSwitcher(current, _currentPath) {
22672
+ const views = getAllPersonaViews();
22673
+ if (views.length === 0) return "";
22674
+ const options = views.map(
22675
+ (v) => `<option value="${v.shortName}"${current === v.shortName ? " selected" : ""}>${escapeHtml(v.displayName)}</option>`
22676
+ ).join("\n ");
22677
+ return `
22678
+ <div class="persona-switcher">
22679
+ <label class="persona-label" for="persona-select">View</label>
22680
+ <select class="persona-select" id="persona-select" onchange="switchPersona(this.value)">
22681
+ ${options}
22682
+ </select>
22683
+ </div>
22684
+ <script>
22685
+ function switchPersona(value) {
22686
+ if (value) {
22687
+ window.location.href = '/' + value + '/dashboard';
21977
22688
  }
21978
-
21979
- activeId = clickedId;
21980
- highlight(clickedId);
21981
- });
21982
-
21983
- function drawAndHighlight() {
21984
- drawLines();
21985
- if (activeId) highlight(activeId);
21986
22689
  }
21987
-
21988
- requestAnimationFrame(function() { setTimeout(drawAndHighlight, 100); });
21989
- window.addEventListener('resize', drawAndHighlight);
21990
- container.addEventListener('scroll', drawAndHighlight);
21991
- new ResizeObserver(drawAndHighlight).observe(container);
21992
- })();
21993
22690
  </script>`;
21994
22691
  }
21995
- function buildStatusPie(title, counts) {
21996
- const entries = Object.entries(counts).filter(([, v]) => v > 0);
21997
- if (entries.length === 0) {
21998
- return placeholder(`No data for ${title}.`);
21999
- }
22000
- const lines = [`pie title ${sanitize(title, 60)}`];
22001
- for (const [label, count] of entries) {
22002
- lines.push(` "${sanitize(label, 30)}" : ${count}`);
22003
- }
22004
- return mermaidBlock(lines.join("\n"));
22005
- }
22006
- function buildHealthGauge(categories) {
22007
- const valid = categories.filter((c) => c.total > 0);
22008
- if (valid.length === 0) {
22009
- return placeholder("No completeness data available.");
22010
- }
22011
- const pies = valid.map((cat) => {
22012
- const incomplete = cat.total - cat.complete;
22013
- const lines = [
22014
- `pie title ${sanitize(cat.name, 30)}`,
22015
- ` "Complete" : ${cat.complete}`,
22016
- ` "Incomplete" : ${incomplete}`
22017
- ];
22018
- return mermaidBlock(lines.join("\n"));
22019
- });
22020
- return `<div class="mermaid-row">${pies.join("\n")}</div>`;
22021
- }
22022
22692
 
22023
22693
  // src/web/templates/pages/po/dashboard.ts
22024
22694
  var DONE_STATUSES5 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
@@ -25152,7 +25822,7 @@ function handleRequest(req, res, store, projectName, navGroups) {
25152
25822
  notFound(res, projectName, navGroups, pathname, persona, pOpts);
25153
25823
  return;
25154
25824
  }
25155
- const body = documentDetailPage(doc);
25825
+ const body = documentDetailPage(doc, store);
25156
25826
  respond(res, layout({ title: `${id} \u2014 ${doc.frontmatter.title}`, activePath: `/docs/${type}`, projectName, navGroups, persona, ...pOpts }, body));
25157
25827
  return;
25158
25828
  }
@@ -27052,20 +27722,38 @@ async function _assessArtifactRecursive(store, client, host, options, visited, d
27052
27722
  }
27053
27723
  }
27054
27724
  if (options.applyUpdates) {
27055
- const assessmentSummary = buildAssessmentSummary(
27725
+ const newEntry = buildAssessmentSummary(
27056
27726
  commentSummary,
27057
27727
  commentAnalysisProgress,
27058
27728
  signals,
27059
27729
  children,
27060
27730
  linkedIssues
27061
27731
  );
27732
+ const existingHistory = Array.isArray(fm.assessmentHistory) ? fm.assessmentHistory : [];
27733
+ const legacySummary = fm.assessmentSummary;
27734
+ const allEntries = [newEntry, ...existingHistory];
27735
+ if (legacySummary?.generatedAt) {
27736
+ allEntries.push(legacySummary);
27737
+ }
27738
+ const MAX_HISTORY = 100;
27739
+ const seen = /* @__PURE__ */ new Set();
27740
+ const assessmentHistory = allEntries.filter((entry) => {
27741
+ if (!entry.generatedAt) return false;
27742
+ if (seen.has(entry.generatedAt)) return false;
27743
+ seen.add(entry.generatedAt);
27744
+ return true;
27745
+ }).sort((a, b) => (b.generatedAt ?? "").localeCompare(a.generatedAt ?? "")).slice(0, MAX_HISTORY);
27062
27746
  try {
27063
- store.update(fm.id, {
27064
- assessmentSummary,
27065
- lastAssessedAt: assessmentSummary.generatedAt
27066
- });
27747
+ const payload = {
27748
+ assessmentHistory,
27749
+ lastAssessedAt: newEntry.generatedAt
27750
+ };
27751
+ if (fm.assessmentSummary !== void 0) {
27752
+ payload.assessmentSummary = void 0;
27753
+ }
27754
+ store.update(fm.id, payload);
27067
27755
  } catch (err) {
27068
- errors.push(`Failed to persist assessment summary: ${err instanceof Error ? err.message : String(err)}`);
27756
+ errors.push(`Failed to persist assessment history: ${err instanceof Error ? err.message : String(err)}`);
27069
27757
  }
27070
27758
  }
27071
27759
  return {
@@ -33661,7 +34349,7 @@ function createProgram() {
33661
34349
  const program2 = new Command();
33662
34350
  program2.name("marvin").description(
33663
34351
  "AI-powered product development assistant with Product Owner, Delivery Manager, and Technical Lead personas"
33664
- ).version("0.5.24");
34352
+ ).version("0.5.26");
33665
34353
  program2.command("init").description("Initialize a new Marvin project in the current directory").action(async () => {
33666
34354
  await initCommand();
33667
34355
  });