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.
@@ -16218,6 +16218,221 @@ function getUpcomingData(store) {
16218
16218
  function getSprintSummaryData(store, sprintId) {
16219
16219
  return collectSprintSummaryData(store, sprintId);
16220
16220
  }
16221
+ var SIBLING_CAP = 8;
16222
+ var ARTIFACT_ID_PATTERN = /\b([A-Z]{1,3}-\d{3,})\b/g;
16223
+ function getArtifactRelationships(store, docId) {
16224
+ const doc = store.get(docId);
16225
+ if (!doc) return null;
16226
+ const fm = doc.frontmatter;
16227
+ const allDocs = store.list();
16228
+ const docIndex = new Map(allDocs.map((d) => [d.frontmatter.id, d]));
16229
+ const origins = [];
16230
+ const parents = [];
16231
+ const children = [];
16232
+ const external = [];
16233
+ const edges = [];
16234
+ const seen = /* @__PURE__ */ new Set([docId]);
16235
+ const addIfExists = (id, relationship, bucket) => {
16236
+ if (seen.has(id)) return false;
16237
+ const target = docIndex.get(id);
16238
+ if (!target) return false;
16239
+ seen.add(id);
16240
+ bucket.push({
16241
+ id: target.frontmatter.id,
16242
+ title: target.frontmatter.title,
16243
+ type: target.frontmatter.type,
16244
+ status: target.frontmatter.status,
16245
+ relationship
16246
+ });
16247
+ return true;
16248
+ };
16249
+ const parentId = fm.aboutArtifact;
16250
+ if (parentId && addIfExists(parentId, "parent", parents)) {
16251
+ edges.push({ from: parentId, to: docId });
16252
+ }
16253
+ const linkedEpics = normalizeLinkedEpics(fm.linkedEpic);
16254
+ for (const epicId of linkedEpics) {
16255
+ if (addIfExists(epicId, "epic", parents)) {
16256
+ edges.push({ from: epicId, to: docId });
16257
+ }
16258
+ const epicDoc = docIndex.get(epicId);
16259
+ if (epicDoc) {
16260
+ const features = normalizeLinkedFeatures(epicDoc.frontmatter.linkedFeature);
16261
+ for (const fid of features) {
16262
+ if (addIfExists(fid, "feature", parents)) {
16263
+ edges.push({ from: fid, to: epicId });
16264
+ }
16265
+ }
16266
+ }
16267
+ }
16268
+ const tags = fm.tags ?? [];
16269
+ for (const tag of tags) {
16270
+ if (tag.startsWith("sprint:")) {
16271
+ const sprintId = tag.slice(7);
16272
+ if (addIfExists(sprintId, "sprint", parents)) {
16273
+ edges.push({ from: sprintId, to: docId });
16274
+ }
16275
+ }
16276
+ }
16277
+ for (const tag of tags) {
16278
+ if (tag.startsWith("source:")) {
16279
+ const sourceId = tag.slice(7);
16280
+ if (addIfExists(sourceId, "source", origins)) {
16281
+ edges.push({ from: sourceId, to: docId });
16282
+ }
16283
+ }
16284
+ }
16285
+ const sourceField = fm.source;
16286
+ if (sourceField && /^[A-Z]{1,3}-\d{3,}$/.test(sourceField)) {
16287
+ if (addIfExists(sourceField, "source", origins)) {
16288
+ edges.push({ from: sourceField, to: docId });
16289
+ }
16290
+ }
16291
+ for (const d of allDocs) {
16292
+ if (d.frontmatter.aboutArtifact === docId) {
16293
+ if (addIfExists(d.frontmatter.id, "child", children)) {
16294
+ edges.push({ from: docId, to: d.frontmatter.id });
16295
+ }
16296
+ }
16297
+ }
16298
+ if (fm.type === "epic") {
16299
+ const epicTag = `epic:${docId}`;
16300
+ for (const d of allDocs) {
16301
+ const dfm = d.frontmatter;
16302
+ const dLinkedEpics = normalizeLinkedEpics(dfm.linkedEpic);
16303
+ const dTags = dfm.tags ?? [];
16304
+ if (dLinkedEpics.includes(docId) || dTags.includes(epicTag)) {
16305
+ if (addIfExists(dfm.id, "child", children)) {
16306
+ edges.push({ from: docId, to: dfm.id });
16307
+ }
16308
+ }
16309
+ }
16310
+ }
16311
+ if (parentId) {
16312
+ let siblingCount = 0;
16313
+ for (const d of allDocs) {
16314
+ if (siblingCount >= SIBLING_CAP) break;
16315
+ if (d.frontmatter.aboutArtifact === parentId && d.frontmatter.id !== docId) {
16316
+ if (addIfExists(d.frontmatter.id, "sibling", children)) {
16317
+ edges.push({ from: parentId, to: d.frontmatter.id });
16318
+ siblingCount++;
16319
+ }
16320
+ }
16321
+ }
16322
+ }
16323
+ const jiraKey = fm.jiraKey;
16324
+ const jiraUrl = fm.jiraUrl;
16325
+ if (jiraKey) {
16326
+ external.push({
16327
+ id: jiraKey,
16328
+ title: jiraUrl ?? `Jira: ${jiraKey}`,
16329
+ type: "jira",
16330
+ status: "",
16331
+ relationship: "jira"
16332
+ });
16333
+ edges.push({ from: docId, to: jiraKey });
16334
+ }
16335
+ if (doc.content) {
16336
+ const matches = doc.content.matchAll(ARTIFACT_ID_PATTERN);
16337
+ for (const m of matches) {
16338
+ const refId = m[1];
16339
+ if (refId !== docId && docIndex.has(refId)) {
16340
+ if (addIfExists(refId, "mentioned", external)) {
16341
+ edges.push({ from: docId, to: refId });
16342
+ }
16343
+ }
16344
+ }
16345
+ }
16346
+ return {
16347
+ origins,
16348
+ parents,
16349
+ self: {
16350
+ id: fm.id,
16351
+ title: fm.title,
16352
+ type: fm.type,
16353
+ status: fm.status,
16354
+ relationship: "self"
16355
+ },
16356
+ children,
16357
+ external,
16358
+ edges
16359
+ };
16360
+ }
16361
+ function getArtifactLineageEvents(store, docId) {
16362
+ const doc = store.get(docId);
16363
+ if (!doc) return [];
16364
+ const fm = doc.frontmatter;
16365
+ const events = [];
16366
+ if (fm.created) {
16367
+ events.push({
16368
+ date: fm.created,
16369
+ type: "created",
16370
+ label: `${fm.id} created`
16371
+ });
16372
+ }
16373
+ const tags = fm.tags ?? [];
16374
+ for (const tag of tags) {
16375
+ if (tag.startsWith("source:")) {
16376
+ const sourceId = tag.slice(7);
16377
+ const sourceDoc = store.get(sourceId);
16378
+ if (sourceDoc) {
16379
+ events.push({
16380
+ date: sourceDoc.frontmatter.created,
16381
+ type: "source-linked",
16382
+ label: `Originated from ${sourceId} \u2014 ${sourceDoc.frontmatter.title}`,
16383
+ relatedId: sourceId
16384
+ });
16385
+ }
16386
+ }
16387
+ }
16388
+ const allDocs = store.list();
16389
+ for (const d of allDocs) {
16390
+ if (d.frontmatter.aboutArtifact === docId) {
16391
+ events.push({
16392
+ date: d.frontmatter.created,
16393
+ type: "child-spawned",
16394
+ label: `Spawned ${d.frontmatter.type} ${d.frontmatter.id} \u2014 ${d.frontmatter.title}`,
16395
+ relatedId: d.frontmatter.id
16396
+ });
16397
+ }
16398
+ }
16399
+ if (fm.type === "epic") {
16400
+ const epicTag = `epic:${docId}`;
16401
+ for (const d of allDocs) {
16402
+ if (d.frontmatter.aboutArtifact === docId) continue;
16403
+ const dLinkedEpics = normalizeLinkedEpics(d.frontmatter.linkedEpic);
16404
+ const dTags = d.frontmatter.tags ?? [];
16405
+ if (dLinkedEpics.includes(docId) || dTags.includes(epicTag)) {
16406
+ events.push({
16407
+ date: d.frontmatter.created,
16408
+ type: "child-spawned",
16409
+ label: `Linked ${d.frontmatter.type} ${d.frontmatter.id} \u2014 ${d.frontmatter.title}`,
16410
+ relatedId: d.frontmatter.id
16411
+ });
16412
+ }
16413
+ }
16414
+ }
16415
+ const history = fm.assessmentHistory ?? [];
16416
+ for (const entry of history) {
16417
+ if (entry.generatedAt) {
16418
+ events.push({
16419
+ date: entry.generatedAt,
16420
+ type: "assessment",
16421
+ label: "Assessment performed"
16422
+ });
16423
+ }
16424
+ }
16425
+ const lastSync = fm.lastJiraSyncAt;
16426
+ if (lastSync) {
16427
+ events.push({
16428
+ date: lastSync,
16429
+ type: "jira-sync",
16430
+ label: `Synced with Jira ${fm.jiraKey ?? ""}`
16431
+ });
16432
+ }
16433
+ events.sort((a, b) => (b.date ?? "").localeCompare(a.date ?? ""));
16434
+ return events;
16435
+ }
16221
16436
 
16222
16437
  // src/reports/gar/collector.ts
16223
16438
  var DONE_STATUSES4 = /* @__PURE__ */ new Set(["done", "closed", "resolved", "cancelled"]);
@@ -20841,20 +21056,38 @@ async function _assessArtifactRecursive(store, client, host, options, visited, d
20841
21056
  }
20842
21057
  }
20843
21058
  if (options.applyUpdates) {
20844
- const assessmentSummary = buildAssessmentSummary(
21059
+ const newEntry = buildAssessmentSummary(
20845
21060
  commentSummary,
20846
21061
  commentAnalysisProgress,
20847
21062
  signals,
20848
21063
  children,
20849
21064
  linkedIssues
20850
21065
  );
21066
+ const existingHistory = Array.isArray(fm.assessmentHistory) ? fm.assessmentHistory : [];
21067
+ const legacySummary = fm.assessmentSummary;
21068
+ const allEntries = [newEntry, ...existingHistory];
21069
+ if (legacySummary?.generatedAt) {
21070
+ allEntries.push(legacySummary);
21071
+ }
21072
+ const MAX_HISTORY = 100;
21073
+ const seen = /* @__PURE__ */ new Set();
21074
+ const assessmentHistory = allEntries.filter((entry) => {
21075
+ if (!entry.generatedAt) return false;
21076
+ if (seen.has(entry.generatedAt)) return false;
21077
+ seen.add(entry.generatedAt);
21078
+ return true;
21079
+ }).sort((a, b) => (b.generatedAt ?? "").localeCompare(a.generatedAt ?? "")).slice(0, MAX_HISTORY);
20851
21080
  try {
20852
- store.update(fm.id, {
20853
- assessmentSummary,
20854
- lastAssessedAt: assessmentSummary.generatedAt
20855
- });
21081
+ const payload = {
21082
+ assessmentHistory,
21083
+ lastAssessedAt: newEntry.generatedAt
21084
+ };
21085
+ if (fm.assessmentSummary !== void 0) {
21086
+ payload.assessmentSummary = void 0;
21087
+ }
21088
+ store.update(fm.id, payload);
20856
21089
  } catch (err) {
20857
- errors.push(`Failed to persist assessment summary: ${err instanceof Error ? err.message : String(err)}`);
21090
+ errors.push(`Failed to persist assessment history: ${err instanceof Error ? err.message : String(err)}`);
20858
21091
  }
20859
21092
  }
20860
21093
  return {
@@ -22981,8 +23214,30 @@ function inline(text) {
22981
23214
  s = s.replace(/__([^_]+)__/g, "<strong>$1</strong>");
22982
23215
  s = s.replace(/\*([^*]+)\*/g, "<em>$1</em>");
22983
23216
  s = s.replace(/_([^_]+)_/g, "<em>$1</em>");
23217
+ s = linkArtifactIds(s);
22984
23218
  return s;
22985
23219
  }
23220
+ var ID_PREFIX_TO_TYPE = (() => {
23221
+ const entries = [];
23222
+ for (const [type, prefix] of Object.entries(CORE_ID_PREFIXES)) {
23223
+ entries.push([prefix, type]);
23224
+ }
23225
+ for (const reg of COMMON_REGISTRATIONS) {
23226
+ if (!entries.some(([p]) => p === reg.idPrefix)) {
23227
+ entries.push([reg.idPrefix, reg.type]);
23228
+ }
23229
+ }
23230
+ entries.sort((a, b) => b[0].length - a[0].length);
23231
+ return new Map(entries);
23232
+ })();
23233
+ function linkArtifactIds(html) {
23234
+ return html.replace(/\b([A-Z]{1,3})-(\d{3,})\b/g, (match, prefix, num) => {
23235
+ const type = ID_PREFIX_TO_TYPE.get(prefix);
23236
+ if (!type) return match;
23237
+ const id = `${prefix}-${num}`;
23238
+ return `<a href="/docs/${type}/${id}" class="artifact-link">${match}</a>`;
23239
+ });
23240
+ }
22986
23241
  function layout(opts, body) {
22987
23242
  const switcherHtml = opts.personaSwitcherHtml ?? "";
22988
23243
  let navHtml;
@@ -23694,6 +23949,75 @@ tr:hover td {
23694
23949
  margin: 0.75rem 0;
23695
23950
  }
23696
23951
 
23952
+ /* Artifact cross-links */
23953
+ a.artifact-link {
23954
+ color: var(--accent);
23955
+ text-decoration: none;
23956
+ font-weight: 500;
23957
+ border-bottom: 1px dotted var(--accent);
23958
+ }
23959
+ a.artifact-link:hover {
23960
+ border-bottom-style: solid;
23961
+ }
23962
+
23963
+ /* Assessment timeline */
23964
+ .assessment-timeline {
23965
+ margin-top: 1.5rem;
23966
+ }
23967
+ .assessment-timeline h3 {
23968
+ font-size: 1rem;
23969
+ font-weight: 600;
23970
+ margin-bottom: 0.75rem;
23971
+ }
23972
+ .assessment-entry {
23973
+ background: var(--bg-card);
23974
+ border: 1px solid var(--border);
23975
+ border-radius: var(--radius);
23976
+ padding: 0.75rem 1rem;
23977
+ margin-bottom: 0.75rem;
23978
+ }
23979
+ .assessment-entry.assessment-latest {
23980
+ border-left: 3px solid var(--accent);
23981
+ }
23982
+ .assessment-header {
23983
+ display: flex;
23984
+ align-items: center;
23985
+ gap: 0.5rem;
23986
+ margin-bottom: 0.5rem;
23987
+ }
23988
+ .assessment-date {
23989
+ font-size: 0.8rem;
23990
+ color: var(--text-dim);
23991
+ font-family: var(--mono);
23992
+ }
23993
+ .assessment-comment {
23994
+ font-size: 0.875rem;
23995
+ line-height: 1.6;
23996
+ margin-bottom: 0.5rem;
23997
+ }
23998
+ .assessment-stat {
23999
+ font-size: 0.8rem;
24000
+ color: var(--text-dim);
24001
+ margin-bottom: 0.25rem;
24002
+ }
24003
+ .assessment-stat strong {
24004
+ color: var(--text);
24005
+ }
24006
+ .assessment-signals {
24007
+ list-style: none;
24008
+ padding: 0;
24009
+ margin: 0.5rem 0 0;
24010
+ }
24011
+ .assessment-signals li {
24012
+ font-size: 0.8rem;
24013
+ padding: 0.15rem 0;
24014
+ }
24015
+ .progress-bar-inline {
24016
+ font-family: var(--mono);
24017
+ font-size: 0.75rem;
24018
+ letter-spacing: -0.5px;
24019
+ }
24020
+
23697
24021
  /* Filters */
23698
24022
  .filters {
23699
24023
  display: flex;
@@ -23928,6 +24252,65 @@ tr:hover td {
23928
24252
  .flow-line-lit { stroke: var(--accent) !important; stroke-width: 2 !important; }
23929
24253
  .flow-line-dim { opacity: 0.08; }
23930
24254
 
24255
+ /* Relationship graph: self-node emphasis */
24256
+ .flow-self {
24257
+ border-left-width: 4px;
24258
+ background: var(--bg-hover);
24259
+ box-shadow: 0 0 0 1px var(--accent-dim);
24260
+ }
24261
+ .flow-self .flow-node-id {
24262
+ color: var(--accent);
24263
+ font-weight: 600;
24264
+ }
24265
+
24266
+ /* Relationship graph: external nodes */
24267
+ .flow-external {
24268
+ border-left-color: var(--text-dim);
24269
+ border-left-style: dashed;
24270
+ }
24271
+
24272
+ /* Relationship graph: empty state */
24273
+ .flow-empty {
24274
+ padding: 2rem;
24275
+ text-align: center;
24276
+ color: var(--text-dim);
24277
+ font-size: 0.85rem;
24278
+ }
24279
+
24280
+ /* Lineage timeline */
24281
+ .lineage-timeline {
24282
+ margin-top: 1.5rem;
24283
+ }
24284
+ .lineage-timeline h3 {
24285
+ font-size: 1rem;
24286
+ font-weight: 600;
24287
+ margin-bottom: 0.75rem;
24288
+ }
24289
+ .lineage-entry {
24290
+ display: flex;
24291
+ gap: 0.5rem;
24292
+ padding: 0.4rem 0;
24293
+ padding-left: 0.25rem;
24294
+ }
24295
+ .lineage-marker {
24296
+ flex-shrink: 0;
24297
+ font-size: 0.7rem;
24298
+ line-height: 1.4rem;
24299
+ }
24300
+ .lineage-content {
24301
+ display: flex;
24302
+ flex-direction: column;
24303
+ gap: 0.1rem;
24304
+ }
24305
+ .lineage-date {
24306
+ font-size: 0.7rem;
24307
+ color: var(--text-dim);
24308
+ font-family: var(--mono);
24309
+ }
24310
+ .lineage-label {
24311
+ font-size: 0.85rem;
24312
+ }
24313
+
23931
24314
  /* Gantt truncation note */
23932
24315
  .mermaid-note {
23933
24316
  font-size: 0.75rem;
@@ -24854,767 +25237,1072 @@ function documentsPage(data) {
24854
25237
  `;
24855
25238
  }
24856
25239
 
24857
- // src/web/templates/pages/document-detail.ts
24858
- function documentDetailPage(doc) {
24859
- const fm = doc.frontmatter;
24860
- const label = typeLabel(fm.type);
24861
- const skipKeys = /* @__PURE__ */ new Set(["title", "type"]);
24862
- const entries = Object.entries(fm).filter(
24863
- ([key]) => !skipKeys.has(key) && fm[key] != null
24864
- );
24865
- const dtDd = entries.map(([key, value]) => {
24866
- let rendered;
24867
- if (key === "status") {
24868
- rendered = statusBadge(value);
24869
- } else if (key === "tags" && Array.isArray(value)) {
24870
- rendered = value.map((t) => `<span class="badge badge-default">${escapeHtml(t)}</span>`).join(" ");
24871
- } else if (key === "created" || key === "updated") {
24872
- rendered = formatDate(value);
24873
- } else {
24874
- rendered = escapeHtml(String(value));
24875
- }
24876
- return `<dt>${escapeHtml(key)}</dt><dd>${rendered}</dd>`;
24877
- }).join("\n ");
24878
- return `
24879
- <div class="breadcrumb">
24880
- <a href="/">Overview</a><span class="sep">/</span>
24881
- <a href="/docs/${fm.type}">${escapeHtml(label)}s</a><span class="sep">/</span>
24882
- ${escapeHtml(fm.id)}
24883
- </div>
24884
-
24885
- <div class="page-header">
24886
- <h2>${escapeHtml(fm.title)}${integrationIcons(fm)}</h2>
24887
- <div class="subtitle">${escapeHtml(fm.id)} &middot; ${escapeHtml(label)}</div>
24888
- </div>
24889
-
24890
- <div class="detail-meta">
24891
- <dl>
24892
- ${dtDd}
24893
- </dl>
24894
- </div>
24895
-
24896
- ${doc.content.trim() ? `<div class="detail-content">${renderMarkdown(doc.content)}</div>` : ""}
24897
- `;
24898
- }
24899
-
24900
- // src/web/persona-views.ts
24901
- var VIEWS = /* @__PURE__ */ new Map();
24902
- var PAGE_RENDERERS = /* @__PURE__ */ new Map();
24903
- function registerPersonaView(config2) {
24904
- VIEWS.set(config2.shortName, config2);
24905
- }
24906
- function registerPersonaPage(persona, pageId, renderer) {
24907
- PAGE_RENDERERS.set(`${persona}/${pageId}`, renderer);
24908
- }
24909
- function getPersonaView(mode) {
24910
- if (!mode) return void 0;
24911
- return VIEWS.get(mode);
24912
- }
24913
- function getPersonaPageRenderer(persona, pageId) {
24914
- return PAGE_RENDERERS.get(`${persona}/${pageId}`);
25240
+ // src/web/templates/mermaid.ts
25241
+ function sanitize(text, maxLen = 40) {
25242
+ const cleaned = text.replace(/["'`]/g, "").replace(/[\r\n]+/g, " ");
25243
+ return cleaned.length > maxLen ? cleaned.slice(0, maxLen - 1) + "\u2026" : cleaned;
24915
25244
  }
24916
- function getAllPersonaViews() {
24917
- return [...VIEWS.values()];
25245
+ function mermaidBlock(definition, extraClass) {
25246
+ const cls = ["mermaid-container", extraClass].filter(Boolean).join(" ");
25247
+ return `<div class="${cls}"><pre class="mermaid">
25248
+ ${definition}
25249
+ </pre></div>`;
24918
25250
  }
24919
- var VALID_PERSONAS = /* @__PURE__ */ new Set(["po", "dm", "tl"]);
24920
- function parsePersonaFromUrl(params) {
24921
- const value = params.get("persona")?.toLowerCase();
24922
- if (value && VALID_PERSONAS.has(value)) return value;
24923
- return null;
25251
+ function placeholder(message) {
25252
+ return `<div class="mermaid-container mermaid-empty"><p>${message}</p></div>`;
24924
25253
  }
24925
- function parsePersonaFromPath(pathname) {
24926
- const match = pathname.match(/^\/(po|dm|tl)(?:\/|$)/);
24927
- return match ? match[1] : null;
25254
+ function toMs(date5) {
25255
+ return (/* @__PURE__ */ new Date(date5 + "T00:00:00")).getTime();
24928
25256
  }
24929
- function resolvePersona(pathname, params) {
24930
- return parsePersonaFromPath(pathname) ?? parsePersonaFromUrl(params);
25257
+ function fmtDate(ms) {
25258
+ const d = new Date(ms);
25259
+ const months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
25260
+ return `${months[d.getMonth()]} ${d.getDate()}`;
24931
25261
  }
24932
- var SHARED_NAV_ITEMS = [
24933
- { pageId: "timeline", label: "Timeline" },
24934
- { pageId: "board", label: "Board" },
24935
- { pageId: "upcoming", label: "Upcoming" },
24936
- { pageId: "sprint-summary", label: "Sprint Summary" },
24937
- { pageId: "gar", label: "GAR Report" },
24938
- { pageId: "health", label: "Health" }
24939
- ];
24940
-
24941
- // src/web/templates/pages/persona-picker.ts
24942
- function personaPickerPage() {
24943
- const views = getAllPersonaViews();
24944
- const cards = views.map(
24945
- (v) => `
24946
- <a href="/${v.shortName}/dashboard" class="persona-picker-card" style="--persona-card-accent: ${v.color}">
24947
- <div class="persona-picker-name">${escapeHtml(v.displayName)}</div>
24948
- <div class="persona-picker-desc">${escapeHtml(v.description)}</div>
24949
- </a>`
24950
- ).join("\n");
24951
- return `
24952
- <div class="persona-picker">
24953
- <h2>Choose Your View</h2>
24954
- <p class="persona-picker-subtitle">Select a role to see a curated dashboard with the pages most relevant to you.</p>
24955
- <div class="persona-picker-grid">
24956
- ${cards}
25262
+ function buildTimelineGantt(data, maxSprints = 6) {
25263
+ const sprintsWithDates = data.sprints.filter((s) => s.startDate && s.endDate).sort((a, b) => a.startDate < b.startDate ? -1 : 1);
25264
+ if (sprintsWithDates.length === 0) {
25265
+ return placeholder("No timeline data available \u2014 sprints need start and end dates.");
25266
+ }
25267
+ const truncated = sprintsWithDates.length > maxSprints;
25268
+ const visibleSprints = truncated ? sprintsWithDates.slice(-maxSprints) : sprintsWithDates;
25269
+ const hiddenCount = sprintsWithDates.length - visibleSprints.length;
25270
+ const epicMap = new Map(data.epics.map((e) => [e.id, e]));
25271
+ const allStarts = visibleSprints.map((s) => toMs(s.startDate));
25272
+ const allEnds = visibleSprints.map((s) => toMs(s.endDate));
25273
+ const timelineStart = Math.min(...allStarts);
25274
+ const timelineEnd = Math.max(...allEnds);
25275
+ const span = timelineEnd - timelineStart || 1;
25276
+ const pct = (ms) => (ms - timelineStart) / span * 100;
25277
+ const DAY = 864e5;
25278
+ const markers = [];
25279
+ let tick = timelineStart;
25280
+ const startDay = new Date(tick).getDay();
25281
+ tick += (8 - startDay) % 7 * DAY;
25282
+ while (tick <= timelineEnd) {
25283
+ const left = pct(tick);
25284
+ markers.push(
25285
+ `<div class="gantt-marker" style="left:${left.toFixed(2)}%"><span>${fmtDate(tick)}</span></div>`
25286
+ );
25287
+ tick += 7 * DAY;
25288
+ }
25289
+ const gridLines = [];
25290
+ let gridTick = timelineStart;
25291
+ const gridStartDay = new Date(gridTick).getDay();
25292
+ gridTick += (8 - gridStartDay) % 7 * DAY;
25293
+ while (gridTick <= timelineEnd) {
25294
+ gridLines.push(`<div class="gantt-grid-line" style="left:${pct(gridTick).toFixed(2)}%"></div>`);
25295
+ gridTick += 7 * DAY;
25296
+ }
25297
+ const sprintBoundaries = /* @__PURE__ */ new Set();
25298
+ for (const sprint of visibleSprints) {
25299
+ sprintBoundaries.add(toMs(sprint.startDate));
25300
+ sprintBoundaries.add(toMs(sprint.endDate));
25301
+ }
25302
+ const sprintLines = [...sprintBoundaries].map(
25303
+ (ms) => `<div class="gantt-sprint-line" style="left:${pct(ms).toFixed(2)}%"></div>`
25304
+ );
25305
+ const now = Date.now();
25306
+ let todayMarker = "";
25307
+ if (now >= timelineStart && now <= timelineEnd) {
25308
+ todayMarker = `<div class="gantt-today" style="left:${pct(now).toFixed(2)}%"></div>`;
25309
+ }
25310
+ const sprintBlocks = visibleSprints.map((sprint) => {
25311
+ const sStart = toMs(sprint.startDate);
25312
+ const sEnd = toMs(sprint.endDate);
25313
+ const left = pct(sStart).toFixed(2);
25314
+ const width = (pct(sEnd) - pct(sStart)).toFixed(2);
25315
+ return `<div class="gantt-sprint-block" style="left:${left}%;width:${width}%">${sanitize(sprint.id, 20)}</div>`;
25316
+ }).join("");
25317
+ const sprintBandRow = `<div class="gantt-row gantt-sprint-band-row">
25318
+ <div class="gantt-label gantt-section-label">Sprints</div>
25319
+ <div class="gantt-track gantt-sprint-band">${sprintBlocks}</div>
25320
+ </div>`;
25321
+ const epicSpanMap = /* @__PURE__ */ new Map();
25322
+ for (const sprint of visibleSprints) {
25323
+ const sStart = toMs(sprint.startDate);
25324
+ const sEnd = toMs(sprint.endDate);
25325
+ for (const eid of sprint.linkedEpics) {
25326
+ if (!epicMap.has(eid)) continue;
25327
+ const existing = epicSpanMap.get(eid);
25328
+ if (existing) {
25329
+ existing.startMs = Math.min(existing.startMs, sStart);
25330
+ existing.endMs = Math.max(existing.endMs, sEnd);
25331
+ } else {
25332
+ epicSpanMap.set(eid, { startMs: sStart, endMs: sEnd });
25333
+ }
25334
+ }
25335
+ }
25336
+ const sortedEpicIds = [...epicSpanMap.keys()].sort((a, b) => {
25337
+ const aSpan = epicSpanMap.get(a);
25338
+ const bSpan = epicSpanMap.get(b);
25339
+ if (aSpan.startMs !== bSpan.startMs) return aSpan.startMs - bSpan.startMs;
25340
+ return a.localeCompare(b);
25341
+ });
25342
+ const epicRows = sortedEpicIds.map((eid) => {
25343
+ const epic = epicMap.get(eid);
25344
+ const { startMs, endMs } = epicSpanMap.get(eid);
25345
+ const cls = epic.status === "done" || epic.status === "completed" ? "gantt-bar-done" : epic.status === "in-progress" || epic.status === "active" ? "gantt-bar-active" : epic.status === "blocked" ? "gantt-bar-blocked" : "gantt-bar-default";
25346
+ const left = pct(startMs).toFixed(2);
25347
+ const width = (pct(endMs) - pct(startMs)).toFixed(2);
25348
+ const label = sanitize(epic.id + " " + epic.title);
25349
+ return `<div class="gantt-row">
25350
+ <div class="gantt-label">${label}</div>
25351
+ <div class="gantt-track">
25352
+ <div class="gantt-bar ${cls}" style="left:${left}%;width:${width}%"></div>
25353
+ </div>
25354
+ </div>`;
25355
+ }).join("\n");
25356
+ const note = truncated ? `<div class="mermaid-note">${hiddenCount} earlier sprint${hiddenCount > 1 ? "s" : ""} not shown</div>` : "";
25357
+ return `${note}
25358
+ <div class="gantt">
25359
+ <div class="gantt-chart">
25360
+ <div class="gantt-header">
25361
+ <div class="gantt-label"></div>
25362
+ <div class="gantt-track gantt-dates">${markers.join("")}</div>
25363
+ </div>
25364
+ ${sprintBandRow}
25365
+ ${epicRows}
25366
+ </div>
25367
+ <div class="gantt-overlay">
25368
+ <div class="gantt-label"></div>
25369
+ <div class="gantt-track">${gridLines.join("")}${sprintLines.join("")}${todayMarker}</div>
24957
25370
  </div>
24958
25371
  </div>`;
24959
25372
  }
25373
+ function statusClass(status) {
25374
+ const s = status.toLowerCase();
25375
+ if (s === "done" || s === "completed") return "flow-done";
25376
+ if (s === "in-progress" || s === "active") return "flow-active";
25377
+ if (s === "blocked") return "flow-blocked";
25378
+ return "flow-default";
25379
+ }
25380
+ function buildArtifactFlowchart(data) {
25381
+ if (data.features.length === 0 && data.epics.length === 0) {
25382
+ return placeholder("No artifact relationships found \u2014 create features and epics to see the hierarchy.");
25383
+ }
25384
+ const edges = [];
25385
+ const epicsByFeature = /* @__PURE__ */ new Map();
25386
+ for (const epic of data.epics) {
25387
+ for (const fid of epic.linkedFeature) {
25388
+ if (!epicsByFeature.has(fid)) epicsByFeature.set(fid, []);
25389
+ epicsByFeature.get(fid).push(epic.id);
25390
+ edges.push({ from: fid, to: epic.id });
25391
+ }
25392
+ }
25393
+ const sprintsByEpic = /* @__PURE__ */ new Map();
25394
+ for (const sprint of data.sprints) {
25395
+ for (const eid of sprint.linkedEpics) {
25396
+ if (!sprintsByEpic.has(eid)) sprintsByEpic.set(eid, []);
25397
+ sprintsByEpic.get(eid).push(sprint.id);
25398
+ edges.push({ from: eid, to: sprint.id });
25399
+ }
25400
+ }
25401
+ const connectedFeatureIds = new Set(epicsByFeature.keys());
25402
+ const connectedEpicIds = /* @__PURE__ */ new Set();
25403
+ for (const ids of epicsByFeature.values()) ids.forEach((id) => connectedEpicIds.add(id));
25404
+ for (const ids of sprintsByEpic.values()) ids.forEach(() => {
25405
+ });
25406
+ for (const eid of sprintsByEpic.keys()) connectedEpicIds.add(eid);
25407
+ const connectedSprintIds = /* @__PURE__ */ new Set();
25408
+ for (const ids of sprintsByEpic.values()) ids.forEach((id) => connectedSprintIds.add(id));
25409
+ const features = data.features.filter((f) => connectedFeatureIds.has(f.id));
25410
+ const epics = data.epics.filter((e) => connectedEpicIds.has(e.id));
25411
+ const sprints = data.sprints.filter((s) => connectedSprintIds.has(s.id)).sort((a, b) => (a.startDate ?? "").localeCompare(b.startDate ?? ""));
25412
+ if (features.length === 0 && epics.length === 0) {
25413
+ return placeholder("No artifact relationships found \u2014 link epics to features and sprints.");
25414
+ }
25415
+ const renderNode = (id, title, status, type) => `<div class="flow-node ${statusClass(status)}" data-flow-id="${id}">
25416
+ <a class="flow-node-id" href="/docs/${type}/${id}">${id}</a>
25417
+ <span class="flow-node-title">${sanitize(title, 35)}</span>
25418
+ </div>`;
25419
+ const featuresHtml = features.map((f) => renderNode(f.id, f.title, f.status, "feature")).join("\n");
25420
+ const epicsHtml = epics.map((e) => renderNode(e.id, e.title, e.status, "epic")).join("\n");
25421
+ const sprintsHtml = sprints.map((s) => renderNode(s.id, s.title, s.status, "sprint")).join("\n");
25422
+ const edgesJson = JSON.stringify(edges);
25423
+ return `
25424
+ <div class="flow-diagram" id="flow-diagram">
25425
+ <svg class="flow-lines" id="flow-lines"></svg>
25426
+ <div class="flow-columns">
25427
+ <div class="flow-column">
25428
+ <div class="flow-column-header">Features</div>
25429
+ ${featuresHtml}
25430
+ </div>
25431
+ <div class="flow-column">
25432
+ <div class="flow-column-header">Epics</div>
25433
+ ${epicsHtml}
25434
+ </div>
25435
+ <div class="flow-column">
25436
+ <div class="flow-column-header">Sprints</div>
25437
+ ${sprintsHtml}
25438
+ </div>
25439
+ </div>
25440
+ </div>
25441
+ <script>
25442
+ (function() {
25443
+ var edges = ${edgesJson};
25444
+ var container = document.getElementById('flow-diagram');
25445
+ var svg = document.getElementById('flow-lines');
25446
+ if (!container || !svg) return;
24960
25447
 
24961
- // src/reports/sprint-summary/risk-assessment.ts
24962
- import { query as query3 } from "@anthropic-ai/claude-agent-sdk";
24963
- var SYSTEM_PROMPT2 = `You are a delivery management assistant generating a data-driven risk assessment.
24964
-
24965
- 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.
25448
+ // Build directed adjacency maps for traversal
25449
+ var fwd = {}; // from \u2192 [to] (Feature\u2192Epic, Epic\u2192Sprint)
25450
+ var bwd = {}; // to \u2192 [from] (Sprint\u2192Epic, Epic\u2192Feature)
25451
+ edges.forEach(function(e) {
25452
+ if (!fwd[e.from]) fwd[e.from] = [];
25453
+ if (!bwd[e.to]) bwd[e.to] = [];
25454
+ fwd[e.from].push(e.to);
25455
+ bwd[e.to].push(e.from);
25456
+ });
24966
25457
 
24967
- Produce a concise markdown assessment with these sections:
25458
+ function drawLines() {
25459
+ var rect = container.getBoundingClientRect();
25460
+ var scrollW = container.scrollWidth;
25461
+ var scrollH = container.scrollHeight;
25462
+ svg.setAttribute('width', scrollW);
25463
+ svg.setAttribute('height', scrollH);
25464
+ svg.innerHTML = '';
24968
25465
 
24969
- ## Status Assessment
24970
- One-line verdict: is this risk actively being mitigated, stalled, or escalating?
25466
+ // Use scroll offsets so lines align with scrolled content
25467
+ var scrollLeft = container.scrollLeft;
25468
+ var scrollTop = container.scrollTop;
24971
25469
 
24972
- ## Related Activity
24973
- What actions, decisions, or contributions are connected to this risk? How are they progressing? Be specific \u2014 reference artifact IDs from the data provided.
25470
+ edges.forEach(function(edge) {
25471
+ var fromEl = container.querySelector('[data-flow-id="' + edge.from + '"]');
25472
+ var toEl = container.querySelector('[data-flow-id="' + edge.to + '"]');
25473
+ if (!fromEl || !toEl) return;
24974
25474
 
24975
- ## Trajectory
24976
- 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.
25475
+ var fr = fromEl.getBoundingClientRect();
25476
+ var tr = toEl.getBoundingClientRect();
25477
+ var x1 = fr.right - rect.left + scrollLeft;
25478
+ var y1 = fr.top + fr.height / 2 - rect.top + scrollTop;
25479
+ var x2 = tr.left - rect.left + scrollLeft;
25480
+ var y2 = tr.top + tr.height / 2 - rect.top + scrollTop;
25481
+ var mx = (x1 + x2) / 2;
24977
25482
 
24978
- ## Recommendation
24979
- One concrete next step to move this risk toward resolution.
25483
+ var path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
25484
+ path.setAttribute('d', 'M' + x1 + ',' + y1 + ' C' + mx + ',' + y1 + ' ' + mx + ',' + y2 + ' ' + x2 + ',' + y2);
25485
+ path.setAttribute('fill', 'none');
25486
+ path.setAttribute('stroke', '#2a2e3a');
25487
+ path.setAttribute('stroke-width', '1.5');
25488
+ path.dataset.from = edge.from;
25489
+ path.dataset.to = edge.to;
25490
+ svg.appendChild(path);
25491
+ });
25492
+ }
24980
25493
 
24981
- Rules:
24982
- - Reference artifact IDs, dates, owners, and statuses from the provided data
24983
- - Keep the tone professional and direct
24984
- - Do NOT speculate beyond what the data supports \u2014 if information is insufficient, say so explicitly
24985
- - Do NOT ask for more information or say you will look things up \u2014 everything you need is in the prompt
24986
- - Produce the full assessment text directly`;
24987
- async function generateRiskAssessment(data, riskId, store) {
24988
- const risk = data.risks.find((r) => r.id === riskId);
24989
- if (!risk) return "Risk not found in sprint data.";
24990
- const prompt = buildSingleRiskPrompt(data, risk, store);
24991
- const result = query3({
24992
- prompt,
24993
- options: {
24994
- systemPrompt: SYSTEM_PROMPT2,
24995
- maxTurns: 1,
24996
- tools: [],
24997
- allowedTools: []
24998
- }
24999
- });
25000
- for await (const msg of result) {
25001
- if (msg.type === "assistant") {
25002
- const text = msg.message.content.find(
25003
- (b) => b.type === "text"
25004
- );
25005
- if (text) return text.text;
25006
- }
25007
- }
25008
- return "Unable to generate risk assessment.";
25009
- }
25010
- function buildSingleRiskPrompt(data, risk, store) {
25011
- const sections = [];
25012
- sections.push(`# Sprint: ${data.sprint.id} \u2014 ${data.sprint.title}`);
25013
- if (data.sprint.goal) sections.push(`Goal: ${data.sprint.goal}`);
25014
- sections.push(`Days remaining: ${data.timeline.daysRemaining} / ${data.timeline.totalDays}`);
25015
- sections.push(`Completion: ${data.workItems.completionPct}%`);
25016
- sections.push("");
25017
- const doc = store.get(risk.id);
25018
- sections.push(`# RISK: ${risk.id} \u2014 ${risk.title}`);
25019
- sections.push(`Type: ${risk.type}`);
25020
- if (doc) {
25021
- sections.push(`Status: ${doc.frontmatter.status}`);
25022
- if (doc.frontmatter.owner) sections.push(`Owner: ${doc.frontmatter.owner}`);
25023
- if (doc.frontmatter.assignee) sections.push(`Assignee: ${doc.frontmatter.assignee}`);
25024
- if (doc.frontmatter.priority) sections.push(`Priority: ${doc.frontmatter.priority}`);
25025
- if (doc.frontmatter.dueDate) sections.push(`Due date: ${doc.frontmatter.dueDate}`);
25026
- if (doc.frontmatter.created) sections.push(`Created: ${doc.frontmatter.created.slice(0, 10)}`);
25027
- const tags = doc.frontmatter.tags ?? [];
25028
- if (tags.length > 0) sections.push(`Tags: ${tags.join(", ")}`);
25029
- if (doc.content.trim()) {
25030
- sections.push(`
25031
- Description:
25032
- ${doc.content.trim()}`);
25033
- }
25034
- const allDocs = store.list();
25035
- const relatedIds = /* @__PURE__ */ new Set();
25036
- for (const d of allDocs) {
25037
- if (d.frontmatter.aboutArtifact === risk.id) {
25038
- relatedIds.add(d.frontmatter.id);
25039
- }
25040
- }
25041
- const idPattern = /\b([A-Z]-\d{3,})\b/g;
25042
- let match;
25043
- while ((match = idPattern.exec(doc.content)) !== null) {
25044
- relatedIds.add(match[1]);
25045
- }
25046
- const significantTags = tags.filter(
25047
- (t) => !t.startsWith("sprint:") && !t.startsWith("focus:") && t !== "risk"
25048
- );
25049
- if (significantTags.length > 0) {
25050
- for (const d of allDocs) {
25051
- if (d.frontmatter.id === risk.id) continue;
25052
- const dTags = d.frontmatter.tags ?? [];
25053
- if (significantTags.some((t) => dTags.includes(t))) {
25054
- relatedIds.add(d.frontmatter.id);
25494
+ // Find directly related nodes via directed traversal
25495
+ // Follows forward edges (Feature\u2192Epic\u2192Sprint) and backward edges
25496
+ // (Sprint\u2192Epic\u2192Feature) separately to avoid sideways expansion
25497
+ function findConnected(startId) {
25498
+ var visited = {};
25499
+ visited[startId] = true;
25500
+ // Traverse forward (from\u2192to direction)
25501
+ var queue = [startId];
25502
+ while (queue.length) {
25503
+ var id = queue.shift();
25504
+ (fwd[id] || []).forEach(function(neighbor) {
25505
+ if (!visited[neighbor]) {
25506
+ visited[neighbor] = true;
25507
+ queue.push(neighbor);
25508
+ }
25509
+ });
25055
25510
  }
25056
- }
25057
- }
25058
- const about = doc.frontmatter.aboutArtifact;
25059
- if (about) {
25060
- relatedIds.add(about);
25061
- for (const d of allDocs) {
25062
- if (d.frontmatter.aboutArtifact === about && d.frontmatter.id !== risk.id) {
25063
- relatedIds.add(d.frontmatter.id);
25511
+ // Traverse backward (to\u2192from direction)
25512
+ queue = [startId];
25513
+ while (queue.length) {
25514
+ var id = queue.shift();
25515
+ (bwd[id] || []).forEach(function(neighbor) {
25516
+ if (!visited[neighbor]) {
25517
+ visited[neighbor] = true;
25518
+ queue.push(neighbor);
25519
+ }
25520
+ });
25064
25521
  }
25522
+ return visited;
25065
25523
  }
25066
- }
25067
- const relatedDocs = [...relatedIds].map((id) => store.get(id)).filter((d) => d != null).slice(0, 20);
25068
- if (relatedDocs.length > 0) {
25069
- sections.push(`
25070
- ## Related Documents (${relatedDocs.length})`);
25071
- for (const rd of relatedDocs) {
25072
- const owner = rd.frontmatter.owner ?? "unowned";
25073
- const summary = rd.content.trim().slice(0, 300);
25074
- sections.push(
25075
- `- ${rd.frontmatter.id} (${rd.frontmatter.type}) [${rd.frontmatter.status}] \u2014 ${rd.frontmatter.title}`
25076
- );
25077
- sections.push(` Owner: ${owner}${rd.frontmatter.dueDate ? `, Due: ${rd.frontmatter.dueDate}` : ""}`);
25078
- if (summary) sections.push(` Summary: ${summary}${rd.content.trim().length > 300 ? "..." : ""}`);
25079
- }
25080
- }
25081
- }
25082
- sections.push("");
25083
- sections.push(`---`);
25084
- sections.push(`
25085
- Generate the risk assessment for ${risk.id} based on the data above.`);
25086
- return sections.join("\n");
25087
- }
25088
-
25089
- // src/personas/builtin/product-owner.ts
25090
- var productOwner = {
25091
- id: "product-owner",
25092
- name: "Product Owner",
25093
- shortName: "po",
25094
- description: "Focuses on product vision, stakeholder needs, backlog prioritization, and value delivery.",
25095
- systemPrompt: `You are Marvin, acting as a **Product Owner**. Your role is to help the team maximize the value delivered by the product.
25096
-
25097
- ## Core Responsibilities
25098
- - Define and communicate the product vision and strategy
25099
- - Manage and prioritize the product backlog
25100
- - Ensure stakeholder needs are understood and addressed
25101
- - Make decisions about scope, priority, and trade-offs
25102
- - Accept or reject work results based on acceptance criteria
25103
25524
 
25104
- ## How You Work
25105
- - Ask clarifying questions to understand business value and user needs
25106
- - Create and refine decisions (D-xxx) for important product choices
25107
- - Track questions (Q-xxx) that need stakeholder input
25108
- - Define acceptance criteria for features and deliverables
25109
- - Prioritize actions (A-xxx) based on business value
25525
+ function highlight(hoveredId) {
25526
+ var connected = findConnected(hoveredId);
25527
+ container.querySelectorAll('.flow-node').forEach(function(n) {
25528
+ if (connected[n.dataset.flowId]) {
25529
+ n.classList.add('flow-lit');
25530
+ n.classList.remove('flow-dim');
25531
+ } else {
25532
+ n.classList.add('flow-dim');
25533
+ n.classList.remove('flow-lit');
25534
+ }
25535
+ });
25536
+ svg.querySelectorAll('path').forEach(function(p) {
25537
+ if (connected[p.dataset.from] && connected[p.dataset.to]) {
25538
+ p.classList.add('flow-line-lit');
25539
+ p.classList.remove('flow-line-dim');
25540
+ } else {
25541
+ p.classList.add('flow-line-dim');
25542
+ p.classList.remove('flow-line-lit');
25543
+ }
25544
+ });
25545
+ }
25110
25546
 
25111
- ## Communication Style
25112
- - Business-oriented language, avoid unnecessary technical jargon
25113
- - Focus on outcomes and value, not implementation details
25114
- - Be decisive but transparent about trade-offs
25115
- - Challenge assumptions that don't align with product goals`,
25116
- focusAreas: [
25117
- "Product vision and strategy",
25118
- "Backlog management",
25119
- "Stakeholder communication",
25120
- "Value delivery",
25121
- "Acceptance criteria",
25122
- "Feature definition and prioritization"
25123
- ],
25124
- documentTypes: ["decision", "question", "action", "feature"],
25125
- contributionTypes: ["stakeholder-feedback", "acceptance-result", "priority-change", "market-insight"]
25126
- };
25547
+ function clearHighlight() {
25548
+ container.querySelectorAll('.flow-node').forEach(function(n) { n.classList.remove('flow-lit', 'flow-dim'); });
25549
+ svg.querySelectorAll('path').forEach(function(p) { p.classList.remove('flow-line-lit', 'flow-line-dim'); });
25550
+ }
25127
25551
 
25128
- // src/personas/builtin/delivery-manager.ts
25129
- var deliveryManager = {
25130
- id: "delivery-manager",
25131
- name: "Delivery Manager",
25132
- shortName: "dm",
25133
- description: "Focuses on project delivery, risk management, team coordination, and process governance.",
25134
- systemPrompt: `You are Marvin, acting as a **Delivery Manager**. Your role is to ensure the project is delivered on time, within scope, and with managed risks.
25552
+ var activeId = null;
25553
+ container.addEventListener('click', function(e) {
25554
+ // Let the ID link navigate normally
25555
+ if (e.target.closest('a')) return;
25135
25556
 
25136
- ## Core Responsibilities
25137
- - Track project progress and identify blockers
25138
- - Manage risks, issues, and dependencies
25139
- - Coordinate between team members and stakeholders
25140
- - Ensure governance processes are followed (decisions logged, actions tracked)
25141
- - Facilitate meetings and ensure outcomes are captured
25557
+ var node = e.target.closest('.flow-node');
25558
+ var clickedId = node ? node.dataset.flowId : null;
25142
25559
 
25143
- ## How You Work
25144
- - Review open actions (A-xxx) and follow up on overdue items
25145
- - Ensure every action has a dueDate \u2014 use update_action to backfill existing ones
25146
- - Assign actions to sprints when sprint planning is active, using the sprints parameter
25147
- - Ensure decisions (D-xxx) are properly documented with rationale
25148
- - Track questions (Q-xxx) and ensure they get answered
25149
- - Monitor project health and flag risks early
25150
- - Create meeting notes and ensure action items are assigned
25560
+ if (!clickedId || clickedId === activeId) {
25561
+ activeId = null;
25562
+ clearHighlight();
25563
+ return;
25564
+ }
25151
25565
 
25152
- ## Communication Style
25153
- - Process-oriented but pragmatic
25154
- - Focus on status, risks, and blockers
25155
- - Be proactive about follow-ups and deadlines
25156
- - Keep stakeholders informed with concise updates`,
25157
- focusAreas: [
25158
- "Project delivery",
25159
- "Risk management",
25160
- "Team coordination",
25161
- "Process governance",
25162
- "Status tracking",
25163
- "Epic scheduling and tracking",
25164
- "Sprint planning and tracking"
25165
- ],
25166
- documentTypes: ["action", "decision", "meeting", "question", "feature", "epic", "task", "sprint"],
25167
- contributionTypes: ["risk-finding", "blocker-report", "dependency-update", "status-assessment"]
25168
- };
25566
+ activeId = clickedId;
25567
+ highlight(clickedId);
25568
+ });
25169
25569
 
25170
- // src/personas/builtin/tech-lead.ts
25171
- var techLead = {
25172
- id: "tech-lead",
25173
- name: "Technical Lead",
25174
- shortName: "tl",
25175
- description: "Focuses on technical architecture, code quality, technical decisions, and implementation guidance.",
25176
- systemPrompt: `You are Marvin, acting as a **Technical Lead**. Your role is to guide the team on technical decisions and ensure high-quality implementation.
25570
+ function drawAndHighlight() {
25571
+ drawLines();
25572
+ if (activeId) highlight(activeId);
25573
+ }
25177
25574
 
25178
- ## Core Responsibilities
25179
- - Define and maintain technical architecture
25180
- - Make and document technical decisions with clear rationale
25181
- - Review technical approaches and identify potential issues
25182
- - Guide the team on best practices and patterns
25183
- - Evaluate technical risks and propose mitigations
25184
-
25185
- ## How You Work
25186
- - Create decisions (D-xxx) for significant technical choices (framework, architecture, patterns)
25187
- - Document technical questions (Q-xxx) that need investigation or proof-of-concept
25188
- - Define technical actions (A-xxx) for implementation tasks
25189
- - Consider non-functional requirements (performance, security, maintainability)
25190
- - Provide clear technical guidance with examples when helpful
25191
-
25192
- ## Communication Style
25193
- - Technical but accessible \u2014 explain complex concepts clearly
25194
- - Evidence-based decision making with documented trade-offs
25195
- - Pragmatic about technical debt vs. delivery speed
25196
- - Focus on maintainability and long-term sustainability`,
25197
- focusAreas: [
25198
- "Technical architecture",
25199
- "Code quality",
25200
- "Technical decisions",
25201
- "Implementation guidance",
25202
- "Non-functional requirements",
25203
- "Epic creation and scoping",
25204
- "Task creation and breakdown",
25205
- "Sprint scoping and technical execution"
25206
- ],
25207
- documentTypes: ["decision", "action", "question", "epic", "task", "sprint"],
25208
- contributionTypes: ["action-result", "spike-findings", "technical-assessment", "architecture-review"]
25209
- };
25210
-
25211
- // src/personas/registry.ts
25212
- var BUILTIN_PERSONAS = [
25213
- productOwner,
25214
- deliveryManager,
25215
- techLead
25216
- ];
25217
- function getPersona(idOrShortName) {
25218
- const key = idOrShortName.toLowerCase();
25219
- return BUILTIN_PERSONAS.find(
25220
- (p) => p.id === key || p.shortName === key
25221
- );
25575
+ requestAnimationFrame(function() { setTimeout(drawAndHighlight, 100); });
25576
+ window.addEventListener('resize', drawAndHighlight);
25577
+ container.addEventListener('scroll', drawAndHighlight);
25578
+ new ResizeObserver(drawAndHighlight).observe(container);
25579
+ })();
25580
+ </script>`;
25222
25581
  }
25223
- function listPersonas() {
25224
- return [...BUILTIN_PERSONAS];
25582
+ function buildStatusPie(title, counts) {
25583
+ const entries = Object.entries(counts).filter(([, v]) => v > 0);
25584
+ if (entries.length === 0) {
25585
+ return placeholder(`No data for ${title}.`);
25586
+ }
25587
+ const lines = [`pie title ${sanitize(title, 60)}`];
25588
+ for (const [label, count] of entries) {
25589
+ lines.push(` "${sanitize(label, 30)}" : ${count}`);
25590
+ }
25591
+ return mermaidBlock(lines.join("\n"));
25592
+ }
25593
+ function buildHealthGauge(categories) {
25594
+ const valid = categories.filter((c) => c.total > 0);
25595
+ if (valid.length === 0) {
25596
+ return placeholder("No completeness data available.");
25597
+ }
25598
+ const pies = valid.map((cat) => {
25599
+ const incomplete = cat.total - cat.complete;
25600
+ const lines = [
25601
+ `pie title ${sanitize(cat.name, 30)}`,
25602
+ ` "Complete" : ${cat.complete}`,
25603
+ ` "Incomplete" : ${incomplete}`
25604
+ ];
25605
+ return mermaidBlock(lines.join("\n"));
25606
+ });
25607
+ return `<div class="mermaid-row">${pies.join("\n")}</div>`;
25225
25608
  }
25226
25609
 
25227
- // src/web/templates/persona-switcher.ts
25228
- function renderPersonaSwitcher(current, _currentPath) {
25229
- const views = getAllPersonaViews();
25230
- if (views.length === 0) return "";
25231
- const options = views.map(
25232
- (v) => `<option value="${v.shortName}"${current === v.shortName ? " selected" : ""}>${escapeHtml(v.displayName)}</option>`
25233
- ).join("\n ");
25610
+ // src/web/templates/artifact-graph.ts
25611
+ function buildArtifactRelationGraph(data) {
25612
+ const hasContent = data.origins.length > 0 || data.parents.length > 0 || data.children.length > 0 || data.external.length > 0;
25613
+ if (!hasContent) {
25614
+ return `<div class="flow-diagram flow-empty"><p>No relationships found for this artifact.</p></div>`;
25615
+ }
25616
+ const edges = data.edges;
25617
+ const renderNode = (id, title, status, type) => {
25618
+ const href = type === "jira" ? title.startsWith("http") ? title : "#" : `/docs/${type}/${id}`;
25619
+ const target = type === "jira" ? ' target="_blank" rel="noopener"' : "";
25620
+ const cls = type === "jira" ? "flow-node flow-external" : `flow-node ${statusClass(status)}`;
25621
+ const displayTitle = type === "jira" ? "Jira Issue" : sanitize(title, 35);
25622
+ const displayId = type === "jira" ? `${id} \u2197` : id;
25623
+ return `<div class="${cls}" data-flow-id="${escapeHtml(id)}">
25624
+ <a class="flow-node-id" href="${escapeHtml(href)}"${target}>${escapeHtml(displayId)}</a>
25625
+ <span class="flow-node-title">${escapeHtml(displayTitle)}</span>
25626
+ </div>`;
25627
+ };
25628
+ const selfNode = `<div class="flow-node flow-self ${statusClass(data.self.status)}" data-flow-id="${escapeHtml(data.self.id)}">
25629
+ <span class="flow-node-id">${escapeHtml(data.self.id)}</span>
25630
+ <span class="flow-node-title">${escapeHtml(sanitize(data.self.title, 35))}</span>
25631
+ </div>`;
25632
+ const columns = [];
25633
+ if (data.origins.length > 0) {
25634
+ columns.push({
25635
+ header: "Origins",
25636
+ nodes: data.origins.map((a) => renderNode(a.id, a.title, a.status, a.type)).join("\n")
25637
+ });
25638
+ }
25639
+ if (data.parents.length > 0) {
25640
+ columns.push({
25641
+ header: "Parents",
25642
+ nodes: data.parents.map((a) => renderNode(a.id, a.title, a.status, a.type)).join("\n")
25643
+ });
25644
+ }
25645
+ columns.push({
25646
+ header: data.self.type.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase()),
25647
+ nodes: selfNode
25648
+ });
25649
+ if (data.children.length > 0) {
25650
+ columns.push({
25651
+ header: "Children",
25652
+ nodes: data.children.map((a) => renderNode(a.id, a.title, a.status, a.type)).join("\n")
25653
+ });
25654
+ }
25655
+ if (data.external.length > 0) {
25656
+ columns.push({
25657
+ header: "External",
25658
+ nodes: data.external.map((a) => renderNode(a.id, a.title, a.status, a.type)).join("\n")
25659
+ });
25660
+ }
25661
+ const columnsHtml = columns.map((col) => `
25662
+ <div class="flow-column">
25663
+ <div class="flow-column-header">${escapeHtml(col.header)}</div>
25664
+ ${col.nodes}
25665
+ </div>`).join("\n");
25666
+ const edgesJson = JSON.stringify(edges);
25234
25667
  return `
25235
- <div class="persona-switcher">
25236
- <label class="persona-label" for="persona-select">View</label>
25237
- <select class="persona-select" id="persona-select" onchange="switchPersona(this.value)">
25238
- ${options}
25239
- </select>
25668
+ <div class="flow-diagram" id="rel-graph">
25669
+ <svg class="flow-lines" id="rel-lines"></svg>
25670
+ <div class="flow-columns">
25671
+ ${columnsHtml}
25672
+ </div>
25240
25673
  </div>
25241
25674
  <script>
25242
- function switchPersona(value) {
25243
- if (value) {
25244
- window.location.href = '/' + value + '/dashboard';
25675
+ (function() {
25676
+ var edges = ${edgesJson};
25677
+ var container = document.getElementById('rel-graph');
25678
+ var svg = document.getElementById('rel-lines');
25679
+ if (!container || !svg) return;
25680
+
25681
+ var fwd = {};
25682
+ var bwd = {};
25683
+ edges.forEach(function(e) {
25684
+ if (!fwd[e.from]) fwd[e.from] = [];
25685
+ if (!bwd[e.to]) bwd[e.to] = [];
25686
+ fwd[e.from].push(e.to);
25687
+ bwd[e.to].push(e.from);
25688
+ });
25689
+
25690
+ function drawLines() {
25691
+ var rect = container.getBoundingClientRect();
25692
+ var scrollW = container.scrollWidth;
25693
+ var scrollH = container.scrollHeight;
25694
+ svg.setAttribute('width', scrollW);
25695
+ svg.setAttribute('height', scrollH);
25696
+ svg.innerHTML = '';
25697
+
25698
+ var scrollLeft = container.scrollLeft;
25699
+ var scrollTop = container.scrollTop;
25700
+
25701
+ edges.forEach(function(edge) {
25702
+ var fromEl = container.querySelector('[data-flow-id="' + edge.from + '"]');
25703
+ var toEl = container.querySelector('[data-flow-id="' + edge.to + '"]');
25704
+ if (!fromEl || !toEl) return;
25705
+
25706
+ var fr = fromEl.getBoundingClientRect();
25707
+ var tr = toEl.getBoundingClientRect();
25708
+ var x1 = fr.right - rect.left + scrollLeft;
25709
+ var y1 = fr.top + fr.height / 2 - rect.top + scrollTop;
25710
+ var x2 = tr.left - rect.left + scrollLeft;
25711
+ var y2 = tr.top + tr.height / 2 - rect.top + scrollTop;
25712
+ var mx = (x1 + x2) / 2;
25713
+
25714
+ var path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
25715
+ path.setAttribute('d', 'M' + x1 + ',' + y1 + ' C' + mx + ',' + y1 + ' ' + mx + ',' + y2 + ' ' + x2 + ',' + y2);
25716
+ path.setAttribute('fill', 'none');
25717
+ path.setAttribute('stroke', '#2a2e3a');
25718
+ path.setAttribute('stroke-width', '1.5');
25719
+ path.dataset.from = edge.from;
25720
+ path.dataset.to = edge.to;
25721
+ svg.appendChild(path);
25722
+ });
25723
+ }
25724
+
25725
+ function findConnected(startId) {
25726
+ var visited = {};
25727
+ visited[startId] = true;
25728
+ var queue = [startId];
25729
+ while (queue.length) {
25730
+ var id = queue.shift();
25731
+ (fwd[id] || []).forEach(function(n) {
25732
+ if (!visited[n]) { visited[n] = true; queue.push(n); }
25733
+ });
25734
+ }
25735
+ queue = [startId];
25736
+ while (queue.length) {
25737
+ var id = queue.shift();
25738
+ (bwd[id] || []).forEach(function(n) {
25739
+ if (!visited[n]) { visited[n] = true; queue.push(n); }
25740
+ });
25245
25741
  }
25742
+ return visited;
25246
25743
  }
25247
- </script>`;
25248
- }
25249
25744
 
25250
- // src/web/templates/mermaid.ts
25251
- function sanitize(text, maxLen = 40) {
25252
- const cleaned = text.replace(/["'`]/g, "").replace(/[\r\n]+/g, " ");
25253
- return cleaned.length > maxLen ? cleaned.slice(0, maxLen - 1) + "\u2026" : cleaned;
25254
- }
25255
- function mermaidBlock(definition, extraClass) {
25256
- const cls = ["mermaid-container", extraClass].filter(Boolean).join(" ");
25257
- return `<div class="${cls}"><pre class="mermaid">
25258
- ${definition}
25259
- </pre></div>`;
25260
- }
25261
- function placeholder(message) {
25262
- return `<div class="mermaid-container mermaid-empty"><p>${message}</p></div>`;
25263
- }
25264
- function toMs(date5) {
25265
- return (/* @__PURE__ */ new Date(date5 + "T00:00:00")).getTime();
25266
- }
25267
- function fmtDate(ms) {
25268
- const d = new Date(ms);
25269
- const months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
25270
- return `${months[d.getMonth()]} ${d.getDate()}`;
25745
+ function highlight(hoveredId) {
25746
+ var connected = findConnected(hoveredId);
25747
+ container.querySelectorAll('.flow-node').forEach(function(n) {
25748
+ if (connected[n.dataset.flowId]) {
25749
+ n.classList.add('flow-lit'); n.classList.remove('flow-dim');
25750
+ } else {
25751
+ n.classList.add('flow-dim'); n.classList.remove('flow-lit');
25752
+ }
25753
+ });
25754
+ svg.querySelectorAll('path').forEach(function(p) {
25755
+ if (connected[p.dataset.from] && connected[p.dataset.to]) {
25756
+ p.classList.add('flow-line-lit'); p.classList.remove('flow-line-dim');
25757
+ } else {
25758
+ p.classList.add('flow-line-dim'); p.classList.remove('flow-line-lit');
25759
+ }
25760
+ });
25761
+ }
25762
+
25763
+ function clearHighlight() {
25764
+ container.querySelectorAll('.flow-node').forEach(function(n) { n.classList.remove('flow-lit', 'flow-dim'); });
25765
+ svg.querySelectorAll('path').forEach(function(p) { p.classList.remove('flow-line-lit', 'flow-line-dim'); });
25766
+ }
25767
+
25768
+ var activeId = null;
25769
+ container.addEventListener('click', function(e) {
25770
+ if (e.target.closest('a')) return;
25771
+ var node = e.target.closest('.flow-node');
25772
+ var clickedId = node ? node.dataset.flowId : null;
25773
+ if (!clickedId || clickedId === activeId) {
25774
+ activeId = null; clearHighlight(); return;
25775
+ }
25776
+ activeId = clickedId;
25777
+ highlight(clickedId);
25778
+ });
25779
+
25780
+ function drawAndHighlight() {
25781
+ drawLines();
25782
+ if (activeId) highlight(activeId);
25783
+ }
25784
+
25785
+ requestAnimationFrame(function() { setTimeout(drawAndHighlight, 100); });
25786
+ window.addEventListener('resize', drawAndHighlight);
25787
+ container.addEventListener('scroll', drawAndHighlight);
25788
+ new ResizeObserver(drawAndHighlight).observe(container);
25789
+ })();
25790
+ </script>`;
25271
25791
  }
25272
- function buildTimelineGantt(data, maxSprints = 6) {
25273
- const sprintsWithDates = data.sprints.filter((s) => s.startDate && s.endDate).sort((a, b) => a.startDate < b.startDate ? -1 : 1);
25274
- if (sprintsWithDates.length === 0) {
25275
- return placeholder("No timeline data available \u2014 sprints need start and end dates.");
25792
+ var EVENT_ICONS = {
25793
+ "created": "\u{1F7E2}",
25794
+ "source-linked": "\u{1F535}",
25795
+ "child-spawned": "\u{1F7E1}",
25796
+ "assessment": "\u{1F7E3}",
25797
+ "jira-sync": "\u{1F537}"
25798
+ };
25799
+ function buildLineageTimeline(events) {
25800
+ if (events.length === 0) {
25801
+ return "";
25276
25802
  }
25277
- const truncated = sprintsWithDates.length > maxSprints;
25278
- const visibleSprints = truncated ? sprintsWithDates.slice(-maxSprints) : sprintsWithDates;
25279
- const hiddenCount = sprintsWithDates.length - visibleSprints.length;
25280
- const epicMap = new Map(data.epics.map((e) => [e.id, e]));
25281
- const allStarts = visibleSprints.map((s) => toMs(s.startDate));
25282
- const allEnds = visibleSprints.map((s) => toMs(s.endDate));
25283
- const timelineStart = Math.min(...allStarts);
25284
- const timelineEnd = Math.max(...allEnds);
25285
- const span = timelineEnd - timelineStart || 1;
25286
- const pct = (ms) => (ms - timelineStart) / span * 100;
25287
- const DAY = 864e5;
25288
- const markers = [];
25289
- let tick = timelineStart;
25290
- const startDay = new Date(tick).getDay();
25291
- tick += (8 - startDay) % 7 * DAY;
25292
- while (tick <= timelineEnd) {
25293
- const left = pct(tick);
25294
- markers.push(
25295
- `<div class="gantt-marker" style="left:${left.toFixed(2)}%"><span>${fmtDate(tick)}</span></div>`
25296
- );
25297
- tick += 7 * DAY;
25298
- }
25299
- const gridLines = [];
25300
- let gridTick = timelineStart;
25301
- const gridStartDay = new Date(gridTick).getDay();
25302
- gridTick += (8 - gridStartDay) % 7 * DAY;
25303
- while (gridTick <= timelineEnd) {
25304
- gridLines.push(`<div class="gantt-grid-line" style="left:${pct(gridTick).toFixed(2)}%"></div>`);
25305
- gridTick += 7 * DAY;
25306
- }
25307
- const sprintBoundaries = /* @__PURE__ */ new Set();
25308
- for (const sprint of visibleSprints) {
25309
- sprintBoundaries.add(toMs(sprint.startDate));
25310
- sprintBoundaries.add(toMs(sprint.endDate));
25311
- }
25312
- const sprintLines = [...sprintBoundaries].map(
25313
- (ms) => `<div class="gantt-sprint-line" style="left:${pct(ms).toFixed(2)}%"></div>`
25314
- );
25315
- const now = Date.now();
25316
- let todayMarker = "";
25317
- if (now >= timelineStart && now <= timelineEnd) {
25318
- todayMarker = `<div class="gantt-today" style="left:${pct(now).toFixed(2)}%"></div>`;
25319
- }
25320
- const sprintBlocks = visibleSprints.map((sprint) => {
25321
- const sStart = toMs(sprint.startDate);
25322
- const sEnd = toMs(sprint.endDate);
25323
- const left = pct(sStart).toFixed(2);
25324
- const width = (pct(sEnd) - pct(sStart)).toFixed(2);
25325
- return `<div class="gantt-sprint-block" style="left:${left}%;width:${width}%">${sanitize(sprint.id, 20)}</div>`;
25326
- }).join("");
25327
- const sprintBandRow = `<div class="gantt-row gantt-sprint-band-row">
25328
- <div class="gantt-label gantt-section-label">Sprints</div>
25329
- <div class="gantt-track gantt-sprint-band">${sprintBlocks}</div>
25330
- </div>`;
25331
- const epicSpanMap = /* @__PURE__ */ new Map();
25332
- for (const sprint of visibleSprints) {
25333
- const sStart = toMs(sprint.startDate);
25334
- const sEnd = toMs(sprint.endDate);
25335
- for (const eid of sprint.linkedEpics) {
25336
- if (!epicMap.has(eid)) continue;
25337
- const existing = epicSpanMap.get(eid);
25338
- if (existing) {
25339
- existing.startMs = Math.min(existing.startMs, sStart);
25340
- existing.endMs = Math.max(existing.endMs, sEnd);
25341
- } else {
25342
- epicSpanMap.set(eid, { startMs: sStart, endMs: sEnd });
25343
- }
25344
- }
25345
- }
25346
- const sortedEpicIds = [...epicSpanMap.keys()].sort((a, b) => {
25347
- const aSpan = epicSpanMap.get(a);
25348
- const bSpan = epicSpanMap.get(b);
25349
- if (aSpan.startMs !== bSpan.startMs) return aSpan.startMs - bSpan.startMs;
25350
- return a.localeCompare(b);
25351
- });
25352
- const epicRows = sortedEpicIds.map((eid) => {
25353
- const epic = epicMap.get(eid);
25354
- const { startMs, endMs } = epicSpanMap.get(eid);
25355
- const cls = epic.status === "done" || epic.status === "completed" ? "gantt-bar-done" : epic.status === "in-progress" || epic.status === "active" ? "gantt-bar-active" : epic.status === "blocked" ? "gantt-bar-blocked" : "gantt-bar-default";
25356
- const left = pct(startMs).toFixed(2);
25357
- const width = (pct(endMs) - pct(startMs)).toFixed(2);
25358
- const label = sanitize(epic.id + " " + epic.title);
25359
- return `<div class="gantt-row">
25360
- <div class="gantt-label">${label}</div>
25361
- <div class="gantt-track">
25362
- <div class="gantt-bar ${cls}" style="left:${left}%;width:${width}%"></div>
25803
+ const entries = events.map((event) => {
25804
+ const icon = EVENT_ICONS[event.type] ?? "\u26AA";
25805
+ const date5 = event.date ? formatDate(event.date) : "";
25806
+ const time3 = event.date?.slice(11, 16) ?? "";
25807
+ const label = linkArtifactIds(escapeHtml(event.label));
25808
+ return `
25809
+ <div class="lineage-entry lineage-${escapeHtml(event.type)}">
25810
+ <div class="lineage-marker">${icon}</div>
25811
+ <div class="lineage-content">
25812
+ <span class="lineage-date">${escapeHtml(date5)} ${escapeHtml(time3)}</span>
25813
+ <span class="lineage-label">${label}</span>
25363
25814
  </div>
25364
25815
  </div>`;
25365
- }).join("\n");
25366
- const note = truncated ? `<div class="mermaid-note">${hiddenCount} earlier sprint${hiddenCount > 1 ? "s" : ""} not shown</div>` : "";
25367
- return `${note}
25368
- <div class="gantt">
25369
- <div class="gantt-chart">
25370
- <div class="gantt-header">
25371
- <div class="gantt-label"></div>
25372
- <div class="gantt-track gantt-dates">${markers.join("")}</div>
25373
- </div>
25374
- ${sprintBandRow}
25375
- ${epicRows}
25376
- </div>
25377
- <div class="gantt-overlay">
25378
- <div class="gantt-label"></div>
25379
- <div class="gantt-track">${gridLines.join("")}${sprintLines.join("")}${todayMarker}</div>
25380
- </div>
25816
+ });
25817
+ return `
25818
+ <div class="lineage-timeline">
25819
+ <h3>Lineage</h3>
25820
+ ${entries.join("\n")}
25381
25821
  </div>`;
25382
25822
  }
25383
- function statusClass(status) {
25384
- const s = status.toLowerCase();
25385
- if (s === "done" || s === "completed") return "flow-done";
25386
- if (s === "in-progress" || s === "active") return "flow-active";
25387
- if (s === "blocked") return "flow-blocked";
25388
- return "flow-default";
25823
+
25824
+ // src/web/templates/pages/document-detail.ts
25825
+ function documentDetailPage(doc, store) {
25826
+ const fm = doc.frontmatter;
25827
+ const label = typeLabel(fm.type);
25828
+ const skipKeys = /* @__PURE__ */ new Set(["title", "type", "assessmentHistory", "assessmentSummary"]);
25829
+ const entries = Object.entries(fm).filter(
25830
+ ([key, value]) => !skipKeys.has(key) && value != null && typeof value !== "object"
25831
+ );
25832
+ const arrayEntries = Object.entries(fm).filter(
25833
+ ([key, value]) => !skipKeys.has(key) && Array.isArray(value) && value.every((v) => typeof v === "string")
25834
+ );
25835
+ const allEntries = [
25836
+ ...entries.filter(([, v]) => !Array.isArray(v)),
25837
+ ...arrayEntries
25838
+ ];
25839
+ const dtDd = allEntries.map(([key, value]) => {
25840
+ let rendered;
25841
+ if (key === "status") {
25842
+ rendered = statusBadge(value);
25843
+ } else if (key === "tags" && Array.isArray(value)) {
25844
+ rendered = value.map((t) => `<span class="badge badge-default">${escapeHtml(t)}</span>`).join(" ");
25845
+ } else if (key === "created" || key === "updated" || key === "lastAssessedAt" || key === "lastJiraSyncAt") {
25846
+ rendered = formatDate(value);
25847
+ } else {
25848
+ rendered = linkArtifactIds(escapeHtml(String(value)));
25849
+ }
25850
+ return `<dt>${escapeHtml(key)}</dt><dd>${rendered}</dd>`;
25851
+ }).join("\n ");
25852
+ const rawHistory = Array.isArray(fm.assessmentHistory) ? fm.assessmentHistory : fm.assessmentSummary && typeof fm.assessmentSummary === "object" ? [fm.assessmentSummary] : [];
25853
+ const assessmentHistory = rawHistory.filter(isValidAssessmentEntry).sort((a, b) => (b.generatedAt ?? "").localeCompare(a.generatedAt ?? ""));
25854
+ const timelineHtml = assessmentHistory.length > 0 ? renderAssessmentTimeline(assessmentHistory) : "";
25855
+ return `
25856
+ <div class="breadcrumb">
25857
+ <a href="/">Overview</a><span class="sep">/</span>
25858
+ <a href="/docs/${fm.type}">${escapeHtml(label)}s</a><span class="sep">/</span>
25859
+ ${escapeHtml(fm.id)}
25860
+ </div>
25861
+
25862
+ <div class="page-header">
25863
+ <h2>${escapeHtml(fm.title)}${integrationIcons(fm)}</h2>
25864
+ <div class="subtitle">${escapeHtml(fm.id)} &middot; ${escapeHtml(label)}</div>
25865
+ </div>
25866
+
25867
+ <div class="detail-meta">
25868
+ <dl>
25869
+ ${dtDd}
25870
+ </dl>
25871
+ </div>
25872
+
25873
+ ${doc.content.trim() ? `<div class="detail-content">${renderMarkdown(doc.content)}</div>` : ""}
25874
+
25875
+ ${timelineHtml}
25876
+
25877
+ ${store ? renderRelationshipsAndLineage(store, fm.id) : ""}
25878
+ `;
25389
25879
  }
25390
- function buildArtifactFlowchart(data) {
25391
- if (data.features.length === 0 && data.epics.length === 0) {
25392
- return placeholder("No artifact relationships found \u2014 create features and epics to see the hierarchy.");
25880
+ function renderRelationshipsAndLineage(store, docId) {
25881
+ const parts = [];
25882
+ const relationships = getArtifactRelationships(store, docId);
25883
+ if (relationships) {
25884
+ const graphHtml = buildArtifactRelationGraph(relationships);
25885
+ parts.push(collapsibleSection("rel-graph-" + docId, "Relationships", graphHtml));
25393
25886
  }
25394
- const edges = [];
25395
- const epicsByFeature = /* @__PURE__ */ new Map();
25396
- for (const epic of data.epics) {
25397
- for (const fid of epic.linkedFeature) {
25398
- if (!epicsByFeature.has(fid)) epicsByFeature.set(fid, []);
25399
- epicsByFeature.get(fid).push(epic.id);
25400
- edges.push({ from: fid, to: epic.id });
25401
- }
25887
+ const events = getArtifactLineageEvents(store, docId);
25888
+ if (events.length > 0) {
25889
+ const lineageHtml = buildLineageTimeline(events);
25890
+ parts.push(collapsibleSection("lineage-" + docId, "Lineage", lineageHtml, { defaultCollapsed: true }));
25402
25891
  }
25403
- const sprintsByEpic = /* @__PURE__ */ new Map();
25404
- for (const sprint of data.sprints) {
25405
- for (const eid of sprint.linkedEpics) {
25406
- if (!sprintsByEpic.has(eid)) sprintsByEpic.set(eid, []);
25407
- sprintsByEpic.get(eid).push(sprint.id);
25408
- edges.push({ from: eid, to: sprint.id });
25892
+ return parts.join("\n");
25893
+ }
25894
+ function isValidAssessmentEntry(value) {
25895
+ if (typeof value !== "object" || value === null) return false;
25896
+ const obj = value;
25897
+ if (typeof obj.generatedAt !== "string") return false;
25898
+ if (obj.signals !== void 0 && !Array.isArray(obj.signals)) return false;
25899
+ return true;
25900
+ }
25901
+ function normalizeEntry(entry) {
25902
+ return {
25903
+ generatedAt: entry.generatedAt ?? "",
25904
+ commentSummary: typeof entry.commentSummary === "string" ? entry.commentSummary : null,
25905
+ commentAnalysisProgress: typeof entry.commentAnalysisProgress === "number" ? entry.commentAnalysisProgress : null,
25906
+ signals: Array.isArray(entry.signals) ? entry.signals.filter((s) => typeof s === "string") : [],
25907
+ childCount: typeof entry.childCount === "number" ? entry.childCount : 0,
25908
+ childDoneCount: typeof entry.childDoneCount === "number" ? entry.childDoneCount : 0,
25909
+ childRollupProgress: typeof entry.childRollupProgress === "number" ? entry.childRollupProgress : null,
25910
+ linkedIssueCount: typeof entry.linkedIssueCount === "number" ? entry.linkedIssueCount : 0
25911
+ };
25912
+ }
25913
+ function renderAssessmentTimeline(history) {
25914
+ const entries = history.map((raw, i) => {
25915
+ const entry = normalizeEntry(raw);
25916
+ const date5 = entry.generatedAt ? formatDate(entry.generatedAt) : "Unknown date";
25917
+ const time3 = entry.generatedAt?.slice(11, 16) ?? "";
25918
+ const isLatest = i === 0;
25919
+ const parts = [];
25920
+ if (entry.commentSummary) {
25921
+ parts.push(`<div class="assessment-comment">${linkArtifactIds(escapeHtml(entry.commentSummary))}</div>`);
25922
+ }
25923
+ if (entry.commentAnalysisProgress !== null) {
25924
+ parts.push(`<div class="assessment-stat">\u{1F4CA} Comment-derived progress: <strong>${entry.commentAnalysisProgress}%</strong></div>`);
25925
+ }
25926
+ if (entry.childCount > 0) {
25927
+ const bar = progressBarHtml(entry.childRollupProgress ?? 0);
25928
+ parts.push(`<div class="assessment-stat">\u{1F476} Children: ${entry.childDoneCount}/${entry.childCount} done ${bar} ${entry.childRollupProgress ?? 0}%</div>`);
25929
+ }
25930
+ if (entry.linkedIssueCount > 0) {
25931
+ parts.push(`<div class="assessment-stat">\u{1F517} Linked issues: ${entry.linkedIssueCount}</div>`);
25932
+ }
25933
+ if (entry.signals.length > 0) {
25934
+ const signalItems = entry.signals.map((s) => `<li>${linkArtifactIds(escapeHtml(s))}</li>`).join("");
25935
+ parts.push(`<ul class="assessment-signals">${signalItems}</ul>`);
25409
25936
  }
25410
- }
25411
- const connectedFeatureIds = new Set(epicsByFeature.keys());
25412
- const connectedEpicIds = /* @__PURE__ */ new Set();
25413
- for (const ids of epicsByFeature.values()) ids.forEach((id) => connectedEpicIds.add(id));
25414
- for (const ids of sprintsByEpic.values()) ids.forEach(() => {
25937
+ return `
25938
+ <div class="assessment-entry${isLatest ? " assessment-latest" : ""}">
25939
+ <div class="assessment-header">
25940
+ <span class="assessment-date">${escapeHtml(date5)} ${escapeHtml(time3)}</span>
25941
+ ${isLatest ? '<span class="badge badge-default">Latest</span>' : ""}
25942
+ </div>
25943
+ ${parts.join("\n")}
25944
+ </div>`;
25415
25945
  });
25416
- for (const eid of sprintsByEpic.keys()) connectedEpicIds.add(eid);
25417
- const connectedSprintIds = /* @__PURE__ */ new Set();
25418
- for (const ids of sprintsByEpic.values()) ids.forEach((id) => connectedSprintIds.add(id));
25419
- const features = data.features.filter((f) => connectedFeatureIds.has(f.id));
25420
- const epics = data.epics.filter((e) => connectedEpicIds.has(e.id));
25421
- const sprints = data.sprints.filter((s) => connectedSprintIds.has(s.id)).sort((a, b) => (a.startDate ?? "").localeCompare(b.startDate ?? ""));
25422
- if (features.length === 0 && epics.length === 0) {
25423
- return placeholder("No artifact relationships found \u2014 link epics to features and sprints.");
25424
- }
25425
- const renderNode = (id, title, status, type) => `<div class="flow-node ${statusClass(status)}" data-flow-id="${id}">
25426
- <a class="flow-node-id" href="/docs/${type}/${id}">${id}</a>
25427
- <span class="flow-node-title">${sanitize(title, 35)}</span>
25946
+ return `
25947
+ <div class="assessment-timeline">
25948
+ <h3>Assessment History</h3>
25949
+ ${entries.join("\n")}
25428
25950
  </div>`;
25429
- const featuresHtml = features.map((f) => renderNode(f.id, f.title, f.status, "feature")).join("\n");
25430
- const epicsHtml = epics.map((e) => renderNode(e.id, e.title, e.status, "epic")).join("\n");
25431
- const sprintsHtml = sprints.map((s) => renderNode(s.id, s.title, s.status, "sprint")).join("\n");
25432
- const edgesJson = JSON.stringify(edges);
25951
+ }
25952
+ function progressBarHtml(pct) {
25953
+ const filled = Math.round(Math.max(0, Math.min(100, pct)) / 10);
25954
+ const empty = 10 - filled;
25955
+ return `<span class="progress-bar-inline">${"\u2588".repeat(filled)}${"\u2591".repeat(empty)}</span>`;
25956
+ }
25957
+
25958
+ // src/web/persona-views.ts
25959
+ var VIEWS = /* @__PURE__ */ new Map();
25960
+ var PAGE_RENDERERS = /* @__PURE__ */ new Map();
25961
+ function registerPersonaView(config2) {
25962
+ VIEWS.set(config2.shortName, config2);
25963
+ }
25964
+ function registerPersonaPage(persona, pageId, renderer) {
25965
+ PAGE_RENDERERS.set(`${persona}/${pageId}`, renderer);
25966
+ }
25967
+ function getPersonaView(mode) {
25968
+ if (!mode) return void 0;
25969
+ return VIEWS.get(mode);
25970
+ }
25971
+ function getPersonaPageRenderer(persona, pageId) {
25972
+ return PAGE_RENDERERS.get(`${persona}/${pageId}`);
25973
+ }
25974
+ function getAllPersonaViews() {
25975
+ return [...VIEWS.values()];
25976
+ }
25977
+ var VALID_PERSONAS = /* @__PURE__ */ new Set(["po", "dm", "tl"]);
25978
+ function parsePersonaFromUrl(params) {
25979
+ const value = params.get("persona")?.toLowerCase();
25980
+ if (value && VALID_PERSONAS.has(value)) return value;
25981
+ return null;
25982
+ }
25983
+ function parsePersonaFromPath(pathname) {
25984
+ const match = pathname.match(/^\/(po|dm|tl)(?:\/|$)/);
25985
+ return match ? match[1] : null;
25986
+ }
25987
+ function resolvePersona(pathname, params) {
25988
+ return parsePersonaFromPath(pathname) ?? parsePersonaFromUrl(params);
25989
+ }
25990
+ var SHARED_NAV_ITEMS = [
25991
+ { pageId: "timeline", label: "Timeline" },
25992
+ { pageId: "board", label: "Board" },
25993
+ { pageId: "upcoming", label: "Upcoming" },
25994
+ { pageId: "sprint-summary", label: "Sprint Summary" },
25995
+ { pageId: "gar", label: "GAR Report" },
25996
+ { pageId: "health", label: "Health" }
25997
+ ];
25998
+
25999
+ // src/web/templates/pages/persona-picker.ts
26000
+ function personaPickerPage() {
26001
+ const views = getAllPersonaViews();
26002
+ const cards = views.map(
26003
+ (v) => `
26004
+ <a href="/${v.shortName}/dashboard" class="persona-picker-card" style="--persona-card-accent: ${v.color}">
26005
+ <div class="persona-picker-name">${escapeHtml(v.displayName)}</div>
26006
+ <div class="persona-picker-desc">${escapeHtml(v.description)}</div>
26007
+ </a>`
26008
+ ).join("\n");
25433
26009
  return `
25434
- <div class="flow-diagram" id="flow-diagram">
25435
- <svg class="flow-lines" id="flow-lines"></svg>
25436
- <div class="flow-columns">
25437
- <div class="flow-column">
25438
- <div class="flow-column-header">Features</div>
25439
- ${featuresHtml}
25440
- </div>
25441
- <div class="flow-column">
25442
- <div class="flow-column-header">Epics</div>
25443
- ${epicsHtml}
25444
- </div>
25445
- <div class="flow-column">
25446
- <div class="flow-column-header">Sprints</div>
25447
- ${sprintsHtml}
25448
- </div>
26010
+ <div class="persona-picker">
26011
+ <h2>Choose Your View</h2>
26012
+ <p class="persona-picker-subtitle">Select a role to see a curated dashboard with the pages most relevant to you.</p>
26013
+ <div class="persona-picker-grid">
26014
+ ${cards}
25449
26015
  </div>
25450
- </div>
25451
- <script>
25452
- (function() {
25453
- var edges = ${edgesJson};
25454
- var container = document.getElementById('flow-diagram');
25455
- var svg = document.getElementById('flow-lines');
25456
- if (!container || !svg) return;
26016
+ </div>`;
26017
+ }
25457
26018
 
25458
- // Build directed adjacency maps for traversal
25459
- var fwd = {}; // from \u2192 [to] (Feature\u2192Epic, Epic\u2192Sprint)
25460
- var bwd = {}; // to \u2192 [from] (Sprint\u2192Epic, Epic\u2192Feature)
25461
- edges.forEach(function(e) {
25462
- if (!fwd[e.from]) fwd[e.from] = [];
25463
- if (!bwd[e.to]) bwd[e.to] = [];
25464
- fwd[e.from].push(e.to);
25465
- bwd[e.to].push(e.from);
25466
- });
26019
+ // src/reports/sprint-summary/risk-assessment.ts
26020
+ import { query as query3 } from "@anthropic-ai/claude-agent-sdk";
26021
+ var SYSTEM_PROMPT2 = `You are a delivery management assistant generating a data-driven risk assessment.
25467
26022
 
25468
- function drawLines() {
25469
- var rect = container.getBoundingClientRect();
25470
- var scrollW = container.scrollWidth;
25471
- var scrollH = container.scrollHeight;
25472
- svg.setAttribute('width', scrollW);
25473
- svg.setAttribute('height', scrollH);
25474
- svg.innerHTML = '';
26023
+ 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.
25475
26024
 
25476
- // Use scroll offsets so lines align with scrolled content
25477
- var scrollLeft = container.scrollLeft;
25478
- var scrollTop = container.scrollTop;
26025
+ Produce a concise markdown assessment with these sections:
25479
26026
 
25480
- edges.forEach(function(edge) {
25481
- var fromEl = container.querySelector('[data-flow-id="' + edge.from + '"]');
25482
- var toEl = container.querySelector('[data-flow-id="' + edge.to + '"]');
25483
- if (!fromEl || !toEl) return;
26027
+ ## Status Assessment
26028
+ One-line verdict: is this risk actively being mitigated, stalled, or escalating?
26029
+
26030
+ ## Related Activity
26031
+ What actions, decisions, or contributions are connected to this risk? How are they progressing? Be specific \u2014 reference artifact IDs from the data provided.
26032
+
26033
+ ## Trajectory
26034
+ 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.
26035
+
26036
+ ## Recommendation
26037
+ One concrete next step to move this risk toward resolution.
26038
+
26039
+ Rules:
26040
+ - Reference artifact IDs, dates, owners, and statuses from the provided data
26041
+ - Keep the tone professional and direct
26042
+ - Do NOT speculate beyond what the data supports \u2014 if information is insufficient, say so explicitly
26043
+ - Do NOT ask for more information or say you will look things up \u2014 everything you need is in the prompt
26044
+ - Produce the full assessment text directly`;
26045
+ async function generateRiskAssessment(data, riskId, store) {
26046
+ const risk = data.risks.find((r) => r.id === riskId);
26047
+ if (!risk) return "Risk not found in sprint data.";
26048
+ const prompt = buildSingleRiskPrompt(data, risk, store);
26049
+ const result = query3({
26050
+ prompt,
26051
+ options: {
26052
+ systemPrompt: SYSTEM_PROMPT2,
26053
+ maxTurns: 1,
26054
+ tools: [],
26055
+ allowedTools: []
26056
+ }
26057
+ });
26058
+ for await (const msg of result) {
26059
+ if (msg.type === "assistant") {
26060
+ const text = msg.message.content.find(
26061
+ (b) => b.type === "text"
26062
+ );
26063
+ if (text) return text.text;
26064
+ }
26065
+ }
26066
+ return "Unable to generate risk assessment.";
26067
+ }
26068
+ function buildSingleRiskPrompt(data, risk, store) {
26069
+ const sections = [];
26070
+ sections.push(`# Sprint: ${data.sprint.id} \u2014 ${data.sprint.title}`);
26071
+ if (data.sprint.goal) sections.push(`Goal: ${data.sprint.goal}`);
26072
+ sections.push(`Days remaining: ${data.timeline.daysRemaining} / ${data.timeline.totalDays}`);
26073
+ sections.push(`Completion: ${data.workItems.completionPct}%`);
26074
+ sections.push("");
26075
+ const doc = store.get(risk.id);
26076
+ sections.push(`# RISK: ${risk.id} \u2014 ${risk.title}`);
26077
+ sections.push(`Type: ${risk.type}`);
26078
+ if (doc) {
26079
+ sections.push(`Status: ${doc.frontmatter.status}`);
26080
+ if (doc.frontmatter.owner) sections.push(`Owner: ${doc.frontmatter.owner}`);
26081
+ if (doc.frontmatter.assignee) sections.push(`Assignee: ${doc.frontmatter.assignee}`);
26082
+ if (doc.frontmatter.priority) sections.push(`Priority: ${doc.frontmatter.priority}`);
26083
+ if (doc.frontmatter.dueDate) sections.push(`Due date: ${doc.frontmatter.dueDate}`);
26084
+ if (doc.frontmatter.created) sections.push(`Created: ${doc.frontmatter.created.slice(0, 10)}`);
26085
+ const tags = doc.frontmatter.tags ?? [];
26086
+ if (tags.length > 0) sections.push(`Tags: ${tags.join(", ")}`);
26087
+ if (doc.content.trim()) {
26088
+ sections.push(`
26089
+ Description:
26090
+ ${doc.content.trim()}`);
26091
+ }
26092
+ const allDocs = store.list();
26093
+ const relatedIds = /* @__PURE__ */ new Set();
26094
+ for (const d of allDocs) {
26095
+ if (d.frontmatter.aboutArtifact === risk.id) {
26096
+ relatedIds.add(d.frontmatter.id);
26097
+ }
26098
+ }
26099
+ const idPattern = /\b([A-Z]-\d{3,})\b/g;
26100
+ let match;
26101
+ while ((match = idPattern.exec(doc.content)) !== null) {
26102
+ relatedIds.add(match[1]);
26103
+ }
26104
+ const significantTags = tags.filter(
26105
+ (t) => !t.startsWith("sprint:") && !t.startsWith("focus:") && t !== "risk"
26106
+ );
26107
+ if (significantTags.length > 0) {
26108
+ for (const d of allDocs) {
26109
+ if (d.frontmatter.id === risk.id) continue;
26110
+ const dTags = d.frontmatter.tags ?? [];
26111
+ if (significantTags.some((t) => dTags.includes(t))) {
26112
+ relatedIds.add(d.frontmatter.id);
26113
+ }
26114
+ }
26115
+ }
26116
+ const about = doc.frontmatter.aboutArtifact;
26117
+ if (about) {
26118
+ relatedIds.add(about);
26119
+ for (const d of allDocs) {
26120
+ if (d.frontmatter.aboutArtifact === about && d.frontmatter.id !== risk.id) {
26121
+ relatedIds.add(d.frontmatter.id);
26122
+ }
26123
+ }
26124
+ }
26125
+ const relatedDocs = [...relatedIds].map((id) => store.get(id)).filter((d) => d != null).slice(0, 20);
26126
+ if (relatedDocs.length > 0) {
26127
+ sections.push(`
26128
+ ## Related Documents (${relatedDocs.length})`);
26129
+ for (const rd of relatedDocs) {
26130
+ const owner = rd.frontmatter.owner ?? "unowned";
26131
+ const summary = rd.content.trim().slice(0, 300);
26132
+ sections.push(
26133
+ `- ${rd.frontmatter.id} (${rd.frontmatter.type}) [${rd.frontmatter.status}] \u2014 ${rd.frontmatter.title}`
26134
+ );
26135
+ sections.push(` Owner: ${owner}${rd.frontmatter.dueDate ? `, Due: ${rd.frontmatter.dueDate}` : ""}`);
26136
+ if (summary) sections.push(` Summary: ${summary}${rd.content.trim().length > 300 ? "..." : ""}`);
26137
+ }
26138
+ }
26139
+ }
26140
+ sections.push("");
26141
+ sections.push(`---`);
26142
+ sections.push(`
26143
+ Generate the risk assessment for ${risk.id} based on the data above.`);
26144
+ return sections.join("\n");
26145
+ }
26146
+
26147
+ // src/personas/builtin/product-owner.ts
26148
+ var productOwner = {
26149
+ id: "product-owner",
26150
+ name: "Product Owner",
26151
+ shortName: "po",
26152
+ description: "Focuses on product vision, stakeholder needs, backlog prioritization, and value delivery.",
26153
+ systemPrompt: `You are Marvin, acting as a **Product Owner**. Your role is to help the team maximize the value delivered by the product.
26154
+
26155
+ ## Core Responsibilities
26156
+ - Define and communicate the product vision and strategy
26157
+ - Manage and prioritize the product backlog
26158
+ - Ensure stakeholder needs are understood and addressed
26159
+ - Make decisions about scope, priority, and trade-offs
26160
+ - Accept or reject work results based on acceptance criteria
25484
26161
 
25485
- var fr = fromEl.getBoundingClientRect();
25486
- var tr = toEl.getBoundingClientRect();
25487
- var x1 = fr.right - rect.left + scrollLeft;
25488
- var y1 = fr.top + fr.height / 2 - rect.top + scrollTop;
25489
- var x2 = tr.left - rect.left + scrollLeft;
25490
- var y2 = tr.top + tr.height / 2 - rect.top + scrollTop;
25491
- var mx = (x1 + x2) / 2;
26162
+ ## How You Work
26163
+ - Ask clarifying questions to understand business value and user needs
26164
+ - Create and refine decisions (D-xxx) for important product choices
26165
+ - Track questions (Q-xxx) that need stakeholder input
26166
+ - Define acceptance criteria for features and deliverables
26167
+ - Prioritize actions (A-xxx) based on business value
25492
26168
 
25493
- var path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
25494
- path.setAttribute('d', 'M' + x1 + ',' + y1 + ' C' + mx + ',' + y1 + ' ' + mx + ',' + y2 + ' ' + x2 + ',' + y2);
25495
- path.setAttribute('fill', 'none');
25496
- path.setAttribute('stroke', '#2a2e3a');
25497
- path.setAttribute('stroke-width', '1.5');
25498
- path.dataset.from = edge.from;
25499
- path.dataset.to = edge.to;
25500
- svg.appendChild(path);
25501
- });
25502
- }
26169
+ ## Communication Style
26170
+ - Business-oriented language, avoid unnecessary technical jargon
26171
+ - Focus on outcomes and value, not implementation details
26172
+ - Be decisive but transparent about trade-offs
26173
+ - Challenge assumptions that don't align with product goals`,
26174
+ focusAreas: [
26175
+ "Product vision and strategy",
26176
+ "Backlog management",
26177
+ "Stakeholder communication",
26178
+ "Value delivery",
26179
+ "Acceptance criteria",
26180
+ "Feature definition and prioritization"
26181
+ ],
26182
+ documentTypes: ["decision", "question", "action", "feature"],
26183
+ contributionTypes: ["stakeholder-feedback", "acceptance-result", "priority-change", "market-insight"]
26184
+ };
25503
26185
 
25504
- // Find directly related nodes via directed traversal
25505
- // Follows forward edges (Feature\u2192Epic\u2192Sprint) and backward edges
25506
- // (Sprint\u2192Epic\u2192Feature) separately to avoid sideways expansion
25507
- function findConnected(startId) {
25508
- var visited = {};
25509
- visited[startId] = true;
25510
- // Traverse forward (from\u2192to direction)
25511
- var queue = [startId];
25512
- while (queue.length) {
25513
- var id = queue.shift();
25514
- (fwd[id] || []).forEach(function(neighbor) {
25515
- if (!visited[neighbor]) {
25516
- visited[neighbor] = true;
25517
- queue.push(neighbor);
25518
- }
25519
- });
25520
- }
25521
- // Traverse backward (to\u2192from direction)
25522
- queue = [startId];
25523
- while (queue.length) {
25524
- var id = queue.shift();
25525
- (bwd[id] || []).forEach(function(neighbor) {
25526
- if (!visited[neighbor]) {
25527
- visited[neighbor] = true;
25528
- queue.push(neighbor);
25529
- }
25530
- });
25531
- }
25532
- return visited;
25533
- }
26186
+ // src/personas/builtin/delivery-manager.ts
26187
+ var deliveryManager = {
26188
+ id: "delivery-manager",
26189
+ name: "Delivery Manager",
26190
+ shortName: "dm",
26191
+ description: "Focuses on project delivery, risk management, team coordination, and process governance.",
26192
+ systemPrompt: `You are Marvin, acting as a **Delivery Manager**. Your role is to ensure the project is delivered on time, within scope, and with managed risks.
25534
26193
 
25535
- function highlight(hoveredId) {
25536
- var connected = findConnected(hoveredId);
25537
- container.querySelectorAll('.flow-node').forEach(function(n) {
25538
- if (connected[n.dataset.flowId]) {
25539
- n.classList.add('flow-lit');
25540
- n.classList.remove('flow-dim');
25541
- } else {
25542
- n.classList.add('flow-dim');
25543
- n.classList.remove('flow-lit');
25544
- }
25545
- });
25546
- svg.querySelectorAll('path').forEach(function(p) {
25547
- if (connected[p.dataset.from] && connected[p.dataset.to]) {
25548
- p.classList.add('flow-line-lit');
25549
- p.classList.remove('flow-line-dim');
25550
- } else {
25551
- p.classList.add('flow-line-dim');
25552
- p.classList.remove('flow-line-lit');
25553
- }
25554
- });
25555
- }
26194
+ ## Core Responsibilities
26195
+ - Track project progress and identify blockers
26196
+ - Manage risks, issues, and dependencies
26197
+ - Coordinate between team members and stakeholders
26198
+ - Ensure governance processes are followed (decisions logged, actions tracked)
26199
+ - Facilitate meetings and ensure outcomes are captured
25556
26200
 
25557
- function clearHighlight() {
25558
- container.querySelectorAll('.flow-node').forEach(function(n) { n.classList.remove('flow-lit', 'flow-dim'); });
25559
- svg.querySelectorAll('path').forEach(function(p) { p.classList.remove('flow-line-lit', 'flow-line-dim'); });
25560
- }
26201
+ ## How You Work
26202
+ - Review open actions (A-xxx) and follow up on overdue items
26203
+ - Ensure every action has a dueDate \u2014 use update_action to backfill existing ones
26204
+ - Assign actions to sprints when sprint planning is active, using the sprints parameter
26205
+ - Ensure decisions (D-xxx) are properly documented with rationale
26206
+ - Track questions (Q-xxx) and ensure they get answered
26207
+ - Monitor project health and flag risks early
26208
+ - Create meeting notes and ensure action items are assigned
25561
26209
 
25562
- var activeId = null;
25563
- container.addEventListener('click', function(e) {
25564
- // Let the ID link navigate normally
25565
- if (e.target.closest('a')) return;
26210
+ ## Communication Style
26211
+ - Process-oriented but pragmatic
26212
+ - Focus on status, risks, and blockers
26213
+ - Be proactive about follow-ups and deadlines
26214
+ - Keep stakeholders informed with concise updates`,
26215
+ focusAreas: [
26216
+ "Project delivery",
26217
+ "Risk management",
26218
+ "Team coordination",
26219
+ "Process governance",
26220
+ "Status tracking",
26221
+ "Epic scheduling and tracking",
26222
+ "Sprint planning and tracking"
26223
+ ],
26224
+ documentTypes: ["action", "decision", "meeting", "question", "feature", "epic", "task", "sprint"],
26225
+ contributionTypes: ["risk-finding", "blocker-report", "dependency-update", "status-assessment"]
26226
+ };
25566
26227
 
25567
- var node = e.target.closest('.flow-node');
25568
- var clickedId = node ? node.dataset.flowId : null;
26228
+ // src/personas/builtin/tech-lead.ts
26229
+ var techLead = {
26230
+ id: "tech-lead",
26231
+ name: "Technical Lead",
26232
+ shortName: "tl",
26233
+ description: "Focuses on technical architecture, code quality, technical decisions, and implementation guidance.",
26234
+ systemPrompt: `You are Marvin, acting as a **Technical Lead**. Your role is to guide the team on technical decisions and ensure high-quality implementation.
25569
26235
 
25570
- if (!clickedId || clickedId === activeId) {
25571
- activeId = null;
25572
- clearHighlight();
25573
- return;
25574
- }
26236
+ ## Core Responsibilities
26237
+ - Define and maintain technical architecture
26238
+ - Make and document technical decisions with clear rationale
26239
+ - Review technical approaches and identify potential issues
26240
+ - Guide the team on best practices and patterns
26241
+ - Evaluate technical risks and propose mitigations
25575
26242
 
25576
- activeId = clickedId;
25577
- highlight(clickedId);
25578
- });
26243
+ ## How You Work
26244
+ - Create decisions (D-xxx) for significant technical choices (framework, architecture, patterns)
26245
+ - Document technical questions (Q-xxx) that need investigation or proof-of-concept
26246
+ - Define technical actions (A-xxx) for implementation tasks
26247
+ - Consider non-functional requirements (performance, security, maintainability)
26248
+ - Provide clear technical guidance with examples when helpful
25579
26249
 
25580
- function drawAndHighlight() {
25581
- drawLines();
25582
- if (activeId) highlight(activeId);
25583
- }
26250
+ ## Communication Style
26251
+ - Technical but accessible \u2014 explain complex concepts clearly
26252
+ - Evidence-based decision making with documented trade-offs
26253
+ - Pragmatic about technical debt vs. delivery speed
26254
+ - Focus on maintainability and long-term sustainability`,
26255
+ focusAreas: [
26256
+ "Technical architecture",
26257
+ "Code quality",
26258
+ "Technical decisions",
26259
+ "Implementation guidance",
26260
+ "Non-functional requirements",
26261
+ "Epic creation and scoping",
26262
+ "Task creation and breakdown",
26263
+ "Sprint scoping and technical execution"
26264
+ ],
26265
+ documentTypes: ["decision", "action", "question", "epic", "task", "sprint"],
26266
+ contributionTypes: ["action-result", "spike-findings", "technical-assessment", "architecture-review"]
26267
+ };
25584
26268
 
25585
- requestAnimationFrame(function() { setTimeout(drawAndHighlight, 100); });
25586
- window.addEventListener('resize', drawAndHighlight);
25587
- container.addEventListener('scroll', drawAndHighlight);
25588
- new ResizeObserver(drawAndHighlight).observe(container);
25589
- })();
25590
- </script>`;
26269
+ // src/personas/registry.ts
26270
+ var BUILTIN_PERSONAS = [
26271
+ productOwner,
26272
+ deliveryManager,
26273
+ techLead
26274
+ ];
26275
+ function getPersona(idOrShortName) {
26276
+ const key = idOrShortName.toLowerCase();
26277
+ return BUILTIN_PERSONAS.find(
26278
+ (p) => p.id === key || p.shortName === key
26279
+ );
25591
26280
  }
25592
- function buildStatusPie(title, counts) {
25593
- const entries = Object.entries(counts).filter(([, v]) => v > 0);
25594
- if (entries.length === 0) {
25595
- return placeholder(`No data for ${title}.`);
25596
- }
25597
- const lines = [`pie title ${sanitize(title, 60)}`];
25598
- for (const [label, count] of entries) {
25599
- lines.push(` "${sanitize(label, 30)}" : ${count}`);
25600
- }
25601
- return mermaidBlock(lines.join("\n"));
26281
+ function listPersonas() {
26282
+ return [...BUILTIN_PERSONAS];
25602
26283
  }
25603
- function buildHealthGauge(categories) {
25604
- const valid = categories.filter((c) => c.total > 0);
25605
- if (valid.length === 0) {
25606
- return placeholder("No completeness data available.");
25607
- }
25608
- const pies = valid.map((cat) => {
25609
- const incomplete = cat.total - cat.complete;
25610
- const lines = [
25611
- `pie title ${sanitize(cat.name, 30)}`,
25612
- ` "Complete" : ${cat.complete}`,
25613
- ` "Incomplete" : ${incomplete}`
25614
- ];
25615
- return mermaidBlock(lines.join("\n"));
25616
- });
25617
- return `<div class="mermaid-row">${pies.join("\n")}</div>`;
26284
+
26285
+ // src/web/templates/persona-switcher.ts
26286
+ function renderPersonaSwitcher(current, _currentPath) {
26287
+ const views = getAllPersonaViews();
26288
+ if (views.length === 0) return "";
26289
+ const options = views.map(
26290
+ (v) => `<option value="${v.shortName}"${current === v.shortName ? " selected" : ""}>${escapeHtml(v.displayName)}</option>`
26291
+ ).join("\n ");
26292
+ return `
26293
+ <div class="persona-switcher">
26294
+ <label class="persona-label" for="persona-select">View</label>
26295
+ <select class="persona-select" id="persona-select" onchange="switchPersona(this.value)">
26296
+ ${options}
26297
+ </select>
26298
+ </div>
26299
+ <script>
26300
+ function switchPersona(value) {
26301
+ if (value) {
26302
+ window.location.href = '/' + value + '/dashboard';
26303
+ }
26304
+ }
26305
+ </script>`;
25618
26306
  }
25619
26307
 
25620
26308
  // src/web/templates/pages/po/dashboard.ts
@@ -28749,7 +29437,7 @@ function handleRequest(req, res, store, projectName, navGroups) {
28749
29437
  notFound(res, projectName, navGroups, pathname, persona, pOpts);
28750
29438
  return;
28751
29439
  }
28752
- const body = documentDetailPage(doc);
29440
+ const body = documentDetailPage(doc, store);
28753
29441
  respond(res, layout({ title: `${id} \u2014 ${doc.frontmatter.title}`, activePath: `/docs/${type}`, projectName, navGroups, persona, ...pOpts }, body));
28754
29442
  return;
28755
29443
  }