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