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-serve.js
CHANGED
|
@@ -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"]);
|
|
@@ -24037,6 +24252,65 @@ a.artifact-link:hover {
|
|
|
24037
24252
|
.flow-line-lit { stroke: var(--accent) !important; stroke-width: 2 !important; }
|
|
24038
24253
|
.flow-line-dim { opacity: 0.08; }
|
|
24039
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
|
+
|
|
24040
24314
|
/* Gantt truncation note */
|
|
24041
24315
|
.mermaid-note {
|
|
24042
24316
|
font-size: 0.75rem;
|
|
@@ -24963,842 +25237,1072 @@ function documentsPage(data) {
|
|
|
24963
25237
|
`;
|
|
24964
25238
|
}
|
|
24965
25239
|
|
|
24966
|
-
// src/web/templates/
|
|
24967
|
-
function
|
|
24968
|
-
const
|
|
24969
|
-
|
|
24970
|
-
const skipKeys = /* @__PURE__ */ new Set(["title", "type", "assessmentHistory", "assessmentSummary"]);
|
|
24971
|
-
const entries = Object.entries(fm).filter(
|
|
24972
|
-
([key, value]) => !skipKeys.has(key) && value != null && typeof value !== "object"
|
|
24973
|
-
);
|
|
24974
|
-
const arrayEntries = Object.entries(fm).filter(
|
|
24975
|
-
([key, value]) => !skipKeys.has(key) && Array.isArray(value) && value.every((v) => typeof v === "string")
|
|
24976
|
-
);
|
|
24977
|
-
const allEntries = [
|
|
24978
|
-
...entries.filter(([, v]) => !Array.isArray(v)),
|
|
24979
|
-
...arrayEntries
|
|
24980
|
-
];
|
|
24981
|
-
const dtDd = allEntries.map(([key, value]) => {
|
|
24982
|
-
let rendered;
|
|
24983
|
-
if (key === "status") {
|
|
24984
|
-
rendered = statusBadge(value);
|
|
24985
|
-
} else if (key === "tags" && Array.isArray(value)) {
|
|
24986
|
-
rendered = value.map((t) => `<span class="badge badge-default">${escapeHtml(t)}</span>`).join(" ");
|
|
24987
|
-
} else if (key === "created" || key === "updated" || key === "lastAssessedAt" || key === "lastJiraSyncAt") {
|
|
24988
|
-
rendered = formatDate(value);
|
|
24989
|
-
} else {
|
|
24990
|
-
rendered = linkArtifactIds(escapeHtml(String(value)));
|
|
24991
|
-
}
|
|
24992
|
-
return `<dt>${escapeHtml(key)}</dt><dd>${rendered}</dd>`;
|
|
24993
|
-
}).join("\n ");
|
|
24994
|
-
const rawHistory = Array.isArray(fm.assessmentHistory) ? fm.assessmentHistory : fm.assessmentSummary && typeof fm.assessmentSummary === "object" ? [fm.assessmentSummary] : [];
|
|
24995
|
-
const assessmentHistory = rawHistory.filter(isValidAssessmentEntry).sort((a, b) => (b.generatedAt ?? "").localeCompare(a.generatedAt ?? ""));
|
|
24996
|
-
const timelineHtml = assessmentHistory.length > 0 ? renderAssessmentTimeline(assessmentHistory) : "";
|
|
24997
|
-
return `
|
|
24998
|
-
<div class="breadcrumb">
|
|
24999
|
-
<a href="/">Overview</a><span class="sep">/</span>
|
|
25000
|
-
<a href="/docs/${fm.type}">${escapeHtml(label)}s</a><span class="sep">/</span>
|
|
25001
|
-
${escapeHtml(fm.id)}
|
|
25002
|
-
</div>
|
|
25003
|
-
|
|
25004
|
-
<div class="page-header">
|
|
25005
|
-
<h2>${escapeHtml(fm.title)}${integrationIcons(fm)}</h2>
|
|
25006
|
-
<div class="subtitle">${escapeHtml(fm.id)} · ${escapeHtml(label)}</div>
|
|
25007
|
-
</div>
|
|
25008
|
-
|
|
25009
|
-
<div class="detail-meta">
|
|
25010
|
-
<dl>
|
|
25011
|
-
${dtDd}
|
|
25012
|
-
</dl>
|
|
25013
|
-
</div>
|
|
25014
|
-
|
|
25015
|
-
${doc.content.trim() ? `<div class="detail-content">${renderMarkdown(doc.content)}</div>` : ""}
|
|
25016
|
-
|
|
25017
|
-
${timelineHtml}
|
|
25018
|
-
`;
|
|
25019
|
-
}
|
|
25020
|
-
function isValidAssessmentEntry(value) {
|
|
25021
|
-
if (typeof value !== "object" || value === null) return false;
|
|
25022
|
-
const obj = value;
|
|
25023
|
-
if (typeof obj.generatedAt !== "string") return false;
|
|
25024
|
-
if (obj.signals !== void 0 && !Array.isArray(obj.signals)) return false;
|
|
25025
|
-
return true;
|
|
25026
|
-
}
|
|
25027
|
-
function normalizeEntry(entry) {
|
|
25028
|
-
return {
|
|
25029
|
-
generatedAt: entry.generatedAt ?? "",
|
|
25030
|
-
commentSummary: typeof entry.commentSummary === "string" ? entry.commentSummary : null,
|
|
25031
|
-
commentAnalysisProgress: typeof entry.commentAnalysisProgress === "number" ? entry.commentAnalysisProgress : null,
|
|
25032
|
-
signals: Array.isArray(entry.signals) ? entry.signals.filter((s) => typeof s === "string") : [],
|
|
25033
|
-
childCount: typeof entry.childCount === "number" ? entry.childCount : 0,
|
|
25034
|
-
childDoneCount: typeof entry.childDoneCount === "number" ? entry.childDoneCount : 0,
|
|
25035
|
-
childRollupProgress: typeof entry.childRollupProgress === "number" ? entry.childRollupProgress : null,
|
|
25036
|
-
linkedIssueCount: typeof entry.linkedIssueCount === "number" ? entry.linkedIssueCount : 0
|
|
25037
|
-
};
|
|
25038
|
-
}
|
|
25039
|
-
function renderAssessmentTimeline(history) {
|
|
25040
|
-
const entries = history.map((raw, i) => {
|
|
25041
|
-
const entry = normalizeEntry(raw);
|
|
25042
|
-
const date5 = entry.generatedAt ? formatDate(entry.generatedAt) : "Unknown date";
|
|
25043
|
-
const time3 = entry.generatedAt?.slice(11, 16) ?? "";
|
|
25044
|
-
const isLatest = i === 0;
|
|
25045
|
-
const parts = [];
|
|
25046
|
-
if (entry.commentSummary) {
|
|
25047
|
-
parts.push(`<div class="assessment-comment">${linkArtifactIds(escapeHtml(entry.commentSummary))}</div>`);
|
|
25048
|
-
}
|
|
25049
|
-
if (entry.commentAnalysisProgress !== null) {
|
|
25050
|
-
parts.push(`<div class="assessment-stat">\u{1F4CA} Comment-derived progress: <strong>${entry.commentAnalysisProgress}%</strong></div>`);
|
|
25051
|
-
}
|
|
25052
|
-
if (entry.childCount > 0) {
|
|
25053
|
-
const bar = progressBarHtml(entry.childRollupProgress ?? 0);
|
|
25054
|
-
parts.push(`<div class="assessment-stat">\u{1F476} Children: ${entry.childDoneCount}/${entry.childCount} done ${bar} ${entry.childRollupProgress ?? 0}%</div>`);
|
|
25055
|
-
}
|
|
25056
|
-
if (entry.linkedIssueCount > 0) {
|
|
25057
|
-
parts.push(`<div class="assessment-stat">\u{1F517} Linked issues: ${entry.linkedIssueCount}</div>`);
|
|
25058
|
-
}
|
|
25059
|
-
if (entry.signals.length > 0) {
|
|
25060
|
-
const signalItems = entry.signals.map((s) => `<li>${linkArtifactIds(escapeHtml(s))}</li>`).join("");
|
|
25061
|
-
parts.push(`<ul class="assessment-signals">${signalItems}</ul>`);
|
|
25062
|
-
}
|
|
25063
|
-
return `
|
|
25064
|
-
<div class="assessment-entry${isLatest ? " assessment-latest" : ""}">
|
|
25065
|
-
<div class="assessment-header">
|
|
25066
|
-
<span class="assessment-date">${escapeHtml(date5)} ${escapeHtml(time3)}</span>
|
|
25067
|
-
${isLatest ? '<span class="badge badge-default">Latest</span>' : ""}
|
|
25068
|
-
</div>
|
|
25069
|
-
${parts.join("\n")}
|
|
25070
|
-
</div>`;
|
|
25071
|
-
});
|
|
25072
|
-
return `
|
|
25073
|
-
<div class="assessment-timeline">
|
|
25074
|
-
<h3>Assessment History</h3>
|
|
25075
|
-
${entries.join("\n")}
|
|
25076
|
-
</div>`;
|
|
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;
|
|
25077
25244
|
}
|
|
25078
|
-
function
|
|
25079
|
-
const
|
|
25080
|
-
|
|
25081
|
-
|
|
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>`;
|
|
25082
25250
|
}
|
|
25083
|
-
|
|
25084
|
-
|
|
25085
|
-
var VIEWS = /* @__PURE__ */ new Map();
|
|
25086
|
-
var PAGE_RENDERERS = /* @__PURE__ */ new Map();
|
|
25087
|
-
function registerPersonaView(config2) {
|
|
25088
|
-
VIEWS.set(config2.shortName, config2);
|
|
25251
|
+
function placeholder(message) {
|
|
25252
|
+
return `<div class="mermaid-container mermaid-empty"><p>${message}</p></div>`;
|
|
25089
25253
|
}
|
|
25090
|
-
function
|
|
25091
|
-
|
|
25254
|
+
function toMs(date5) {
|
|
25255
|
+
return (/* @__PURE__ */ new Date(date5 + "T00:00:00")).getTime();
|
|
25092
25256
|
}
|
|
25093
|
-
function
|
|
25094
|
-
|
|
25095
|
-
|
|
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()}`;
|
|
25096
25261
|
}
|
|
25097
|
-
function
|
|
25098
|
-
|
|
25099
|
-
|
|
25100
|
-
|
|
25101
|
-
|
|
25102
|
-
|
|
25103
|
-
|
|
25104
|
-
|
|
25105
|
-
const
|
|
25106
|
-
|
|
25107
|
-
|
|
25108
|
-
|
|
25109
|
-
|
|
25110
|
-
const
|
|
25111
|
-
|
|
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>
|
|
25370
|
+
</div>
|
|
25371
|
+
</div>`;
|
|
25112
25372
|
}
|
|
25113
|
-
function
|
|
25114
|
-
|
|
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";
|
|
25115
25379
|
}
|
|
25116
|
-
|
|
25117
|
-
|
|
25118
|
-
|
|
25119
|
-
|
|
25120
|
-
|
|
25121
|
-
|
|
25122
|
-
|
|
25123
|
-
|
|
25124
|
-
|
|
25125
|
-
|
|
25126
|
-
|
|
25127
|
-
|
|
25128
|
-
|
|
25129
|
-
|
|
25130
|
-
|
|
25131
|
-
|
|
25132
|
-
|
|
25133
|
-
|
|
25134
|
-
|
|
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);
|
|
25135
25423
|
return `
|
|
25136
|
-
<div class="
|
|
25137
|
-
<
|
|
25138
|
-
<
|
|
25139
|
-
|
|
25140
|
-
|
|
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>
|
|
25141
25439
|
</div>
|
|
25142
|
-
</div
|
|
25143
|
-
|
|
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;
|
|
25144
25447
|
|
|
25145
|
-
//
|
|
25146
|
-
|
|
25147
|
-
var
|
|
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
|
+
});
|
|
25148
25457
|
|
|
25149
|
-
|
|
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 = '';
|
|
25150
25465
|
|
|
25151
|
-
|
|
25466
|
+
// Use scroll offsets so lines align with scrolled content
|
|
25467
|
+
var scrollLeft = container.scrollLeft;
|
|
25468
|
+
var scrollTop = container.scrollTop;
|
|
25152
25469
|
|
|
25153
|
-
|
|
25154
|
-
|
|
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;
|
|
25155
25474
|
|
|
25156
|
-
|
|
25157
|
-
|
|
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;
|
|
25158
25482
|
|
|
25159
|
-
|
|
25160
|
-
|
|
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
|
+
}
|
|
25161
25493
|
|
|
25162
|
-
|
|
25163
|
-
|
|
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
|
+
});
|
|
25510
|
+
}
|
|
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
|
+
});
|
|
25521
|
+
}
|
|
25522
|
+
return visited;
|
|
25523
|
+
}
|
|
25164
25524
|
|
|
25165
|
-
|
|
25166
|
-
|
|
25167
|
-
-
|
|
25168
|
-
|
|
25169
|
-
-
|
|
25170
|
-
-
|
|
25171
|
-
|
|
25172
|
-
|
|
25173
|
-
|
|
25174
|
-
|
|
25175
|
-
|
|
25176
|
-
|
|
25177
|
-
|
|
25178
|
-
|
|
25179
|
-
|
|
25180
|
-
|
|
25181
|
-
|
|
25182
|
-
|
|
25183
|
-
|
|
25184
|
-
|
|
25185
|
-
if (msg.type === "assistant") {
|
|
25186
|
-
const text = msg.message.content.find(
|
|
25187
|
-
(b) => b.type === "text"
|
|
25188
|
-
);
|
|
25189
|
-
if (text) return text.text;
|
|
25190
|
-
}
|
|
25191
|
-
}
|
|
25192
|
-
return "Unable to generate risk assessment.";
|
|
25193
|
-
}
|
|
25194
|
-
function buildSingleRiskPrompt(data, risk, store) {
|
|
25195
|
-
const sections = [];
|
|
25196
|
-
sections.push(`# Sprint: ${data.sprint.id} \u2014 ${data.sprint.title}`);
|
|
25197
|
-
if (data.sprint.goal) sections.push(`Goal: ${data.sprint.goal}`);
|
|
25198
|
-
sections.push(`Days remaining: ${data.timeline.daysRemaining} / ${data.timeline.totalDays}`);
|
|
25199
|
-
sections.push(`Completion: ${data.workItems.completionPct}%`);
|
|
25200
|
-
sections.push("");
|
|
25201
|
-
const doc = store.get(risk.id);
|
|
25202
|
-
sections.push(`# RISK: ${risk.id} \u2014 ${risk.title}`);
|
|
25203
|
-
sections.push(`Type: ${risk.type}`);
|
|
25204
|
-
if (doc) {
|
|
25205
|
-
sections.push(`Status: ${doc.frontmatter.status}`);
|
|
25206
|
-
if (doc.frontmatter.owner) sections.push(`Owner: ${doc.frontmatter.owner}`);
|
|
25207
|
-
if (doc.frontmatter.assignee) sections.push(`Assignee: ${doc.frontmatter.assignee}`);
|
|
25208
|
-
if (doc.frontmatter.priority) sections.push(`Priority: ${doc.frontmatter.priority}`);
|
|
25209
|
-
if (doc.frontmatter.dueDate) sections.push(`Due date: ${doc.frontmatter.dueDate}`);
|
|
25210
|
-
if (doc.frontmatter.created) sections.push(`Created: ${doc.frontmatter.created.slice(0, 10)}`);
|
|
25211
|
-
const tags = doc.frontmatter.tags ?? [];
|
|
25212
|
-
if (tags.length > 0) sections.push(`Tags: ${tags.join(", ")}`);
|
|
25213
|
-
if (doc.content.trim()) {
|
|
25214
|
-
sections.push(`
|
|
25215
|
-
Description:
|
|
25216
|
-
${doc.content.trim()}`);
|
|
25217
|
-
}
|
|
25218
|
-
const allDocs = store.list();
|
|
25219
|
-
const relatedIds = /* @__PURE__ */ new Set();
|
|
25220
|
-
for (const d of allDocs) {
|
|
25221
|
-
if (d.frontmatter.aboutArtifact === risk.id) {
|
|
25222
|
-
relatedIds.add(d.frontmatter.id);
|
|
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
|
+
});
|
|
25223
25545
|
}
|
|
25224
|
-
|
|
25225
|
-
|
|
25226
|
-
|
|
25227
|
-
|
|
25228
|
-
relatedIds.add(match[1]);
|
|
25229
|
-
}
|
|
25230
|
-
const significantTags = tags.filter(
|
|
25231
|
-
(t) => !t.startsWith("sprint:") && !t.startsWith("focus:") && t !== "risk"
|
|
25232
|
-
);
|
|
25233
|
-
if (significantTags.length > 0) {
|
|
25234
|
-
for (const d of allDocs) {
|
|
25235
|
-
if (d.frontmatter.id === risk.id) continue;
|
|
25236
|
-
const dTags = d.frontmatter.tags ?? [];
|
|
25237
|
-
if (significantTags.some((t) => dTags.includes(t))) {
|
|
25238
|
-
relatedIds.add(d.frontmatter.id);
|
|
25239
|
-
}
|
|
25546
|
+
|
|
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'); });
|
|
25240
25550
|
}
|
|
25241
|
-
|
|
25242
|
-
|
|
25243
|
-
|
|
25244
|
-
|
|
25245
|
-
|
|
25246
|
-
|
|
25247
|
-
|
|
25551
|
+
|
|
25552
|
+
var activeId = null;
|
|
25553
|
+
container.addEventListener('click', function(e) {
|
|
25554
|
+
// Let the ID link navigate normally
|
|
25555
|
+
if (e.target.closest('a')) return;
|
|
25556
|
+
|
|
25557
|
+
var node = e.target.closest('.flow-node');
|
|
25558
|
+
var clickedId = node ? node.dataset.flowId : null;
|
|
25559
|
+
|
|
25560
|
+
if (!clickedId || clickedId === activeId) {
|
|
25561
|
+
activeId = null;
|
|
25562
|
+
clearHighlight();
|
|
25563
|
+
return;
|
|
25248
25564
|
}
|
|
25565
|
+
|
|
25566
|
+
activeId = clickedId;
|
|
25567
|
+
highlight(clickedId);
|
|
25568
|
+
});
|
|
25569
|
+
|
|
25570
|
+
function drawAndHighlight() {
|
|
25571
|
+
drawLines();
|
|
25572
|
+
if (activeId) highlight(activeId);
|
|
25249
25573
|
}
|
|
25250
|
-
|
|
25251
|
-
|
|
25252
|
-
|
|
25253
|
-
|
|
25254
|
-
|
|
25255
|
-
|
|
25256
|
-
|
|
25257
|
-
|
|
25258
|
-
|
|
25259
|
-
|
|
25260
|
-
|
|
25261
|
-
|
|
25262
|
-
if (summary) sections.push(` Summary: ${summary}${rd.content.trim().length > 300 ? "..." : ""}`);
|
|
25263
|
-
}
|
|
25264
|
-
}
|
|
25574
|
+
|
|
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>`;
|
|
25581
|
+
}
|
|
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}.`);
|
|
25265
25586
|
}
|
|
25266
|
-
|
|
25267
|
-
|
|
25268
|
-
|
|
25269
|
-
|
|
25270
|
-
return
|
|
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"));
|
|
25271
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>`;
|
|
25608
|
+
}
|
|
25609
|
+
|
|
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);
|
|
25667
|
+
return `
|
|
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>
|
|
25673
|
+
</div>
|
|
25674
|
+
<script>
|
|
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;
|
|
25272
25680
|
|
|
25273
|
-
|
|
25274
|
-
var
|
|
25275
|
-
|
|
25276
|
-
|
|
25277
|
-
|
|
25278
|
-
|
|
25279
|
-
|
|
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
|
+
});
|
|
25280
25689
|
|
|
25281
|
-
|
|
25282
|
-
|
|
25283
|
-
|
|
25284
|
-
|
|
25285
|
-
|
|
25286
|
-
|
|
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 = '';
|
|
25287
25697
|
|
|
25288
|
-
|
|
25289
|
-
|
|
25290
|
-
- Create and refine decisions (D-xxx) for important product choices
|
|
25291
|
-
- Track questions (Q-xxx) that need stakeholder input
|
|
25292
|
-
- Define acceptance criteria for features and deliverables
|
|
25293
|
-
- Prioritize actions (A-xxx) based on business value
|
|
25698
|
+
var scrollLeft = container.scrollLeft;
|
|
25699
|
+
var scrollTop = container.scrollTop;
|
|
25294
25700
|
|
|
25295
|
-
|
|
25296
|
-
|
|
25297
|
-
|
|
25298
|
-
|
|
25299
|
-
- Challenge assumptions that don't align with product goals`,
|
|
25300
|
-
focusAreas: [
|
|
25301
|
-
"Product vision and strategy",
|
|
25302
|
-
"Backlog management",
|
|
25303
|
-
"Stakeholder communication",
|
|
25304
|
-
"Value delivery",
|
|
25305
|
-
"Acceptance criteria",
|
|
25306
|
-
"Feature definition and prioritization"
|
|
25307
|
-
],
|
|
25308
|
-
documentTypes: ["decision", "question", "action", "feature"],
|
|
25309
|
-
contributionTypes: ["stakeholder-feedback", "acceptance-result", "priority-change", "market-insight"]
|
|
25310
|
-
};
|
|
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;
|
|
25311
25705
|
|
|
25312
|
-
|
|
25313
|
-
var
|
|
25314
|
-
|
|
25315
|
-
|
|
25316
|
-
|
|
25317
|
-
|
|
25318
|
-
|
|
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;
|
|
25319
25713
|
|
|
25320
|
-
|
|
25321
|
-
|
|
25322
|
-
|
|
25323
|
-
|
|
25324
|
-
-
|
|
25325
|
-
|
|
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
|
+
}
|
|
25326
25724
|
|
|
25327
|
-
|
|
25328
|
-
|
|
25329
|
-
|
|
25330
|
-
|
|
25331
|
-
|
|
25332
|
-
|
|
25333
|
-
|
|
25334
|
-
|
|
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
|
+
});
|
|
25741
|
+
}
|
|
25742
|
+
return visited;
|
|
25743
|
+
}
|
|
25335
25744
|
|
|
25336
|
-
|
|
25337
|
-
|
|
25338
|
-
-
|
|
25339
|
-
|
|
25340
|
-
-
|
|
25341
|
-
|
|
25342
|
-
|
|
25343
|
-
|
|
25344
|
-
|
|
25345
|
-
|
|
25346
|
-
|
|
25347
|
-
|
|
25348
|
-
|
|
25349
|
-
|
|
25350
|
-
|
|
25351
|
-
|
|
25352
|
-
}
|
|
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
|
+
}
|
|
25353
25762
|
|
|
25354
|
-
|
|
25355
|
-
|
|
25356
|
-
|
|
25357
|
-
|
|
25358
|
-
shortName: "tl",
|
|
25359
|
-
description: "Focuses on technical architecture, code quality, technical decisions, and implementation guidance.",
|
|
25360
|
-
systemPrompt: `You are Marvin, acting as a **Technical Lead**. Your role is to guide the team on technical decisions and ensure high-quality implementation.
|
|
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
|
+
}
|
|
25361
25767
|
|
|
25362
|
-
|
|
25363
|
-
|
|
25364
|
-
|
|
25365
|
-
|
|
25366
|
-
|
|
25367
|
-
|
|
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
|
+
});
|
|
25368
25779
|
|
|
25369
|
-
|
|
25370
|
-
|
|
25371
|
-
|
|
25372
|
-
|
|
25373
|
-
- Consider non-functional requirements (performance, security, maintainability)
|
|
25374
|
-
- Provide clear technical guidance with examples when helpful
|
|
25780
|
+
function drawAndHighlight() {
|
|
25781
|
+
drawLines();
|
|
25782
|
+
if (activeId) highlight(activeId);
|
|
25783
|
+
}
|
|
25375
25784
|
|
|
25376
|
-
|
|
25377
|
-
|
|
25378
|
-
|
|
25379
|
-
|
|
25380
|
-
|
|
25381
|
-
|
|
25382
|
-
|
|
25383
|
-
|
|
25384
|
-
|
|
25385
|
-
|
|
25386
|
-
|
|
25387
|
-
|
|
25388
|
-
|
|
25389
|
-
"Sprint scoping and technical execution"
|
|
25390
|
-
],
|
|
25391
|
-
documentTypes: ["decision", "action", "question", "epic", "task", "sprint"],
|
|
25392
|
-
contributionTypes: ["action-result", "spike-findings", "technical-assessment", "architecture-review"]
|
|
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>`;
|
|
25791
|
+
}
|
|
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}"
|
|
25393
25798
|
};
|
|
25799
|
+
function buildLineageTimeline(events) {
|
|
25800
|
+
if (events.length === 0) {
|
|
25801
|
+
return "";
|
|
25802
|
+
}
|
|
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>
|
|
25814
|
+
</div>
|
|
25815
|
+
</div>`;
|
|
25816
|
+
});
|
|
25817
|
+
return `
|
|
25818
|
+
<div class="lineage-timeline">
|
|
25819
|
+
<h3>Lineage</h3>
|
|
25820
|
+
${entries.join("\n")}
|
|
25821
|
+
</div>`;
|
|
25822
|
+
}
|
|
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)} · ${escapeHtml(label)}</div>
|
|
25865
|
+
</div>
|
|
25866
|
+
|
|
25867
|
+
<div class="detail-meta">
|
|
25868
|
+
<dl>
|
|
25869
|
+
${dtDd}
|
|
25870
|
+
</dl>
|
|
25871
|
+
</div>
|
|
25394
25872
|
|
|
25395
|
-
|
|
25396
|
-
|
|
25397
|
-
|
|
25398
|
-
|
|
25399
|
-
|
|
25400
|
-
|
|
25401
|
-
function getPersona(idOrShortName) {
|
|
25402
|
-
const key = idOrShortName.toLowerCase();
|
|
25403
|
-
return BUILTIN_PERSONAS.find(
|
|
25404
|
-
(p) => p.id === key || p.shortName === key
|
|
25405
|
-
);
|
|
25873
|
+
${doc.content.trim() ? `<div class="detail-content">${renderMarkdown(doc.content)}</div>` : ""}
|
|
25874
|
+
|
|
25875
|
+
${timelineHtml}
|
|
25876
|
+
|
|
25877
|
+
${store ? renderRelationshipsAndLineage(store, fm.id) : ""}
|
|
25878
|
+
`;
|
|
25406
25879
|
}
|
|
25407
|
-
function
|
|
25408
|
-
|
|
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));
|
|
25886
|
+
}
|
|
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 }));
|
|
25891
|
+
}
|
|
25892
|
+
return parts.join("\n");
|
|
25409
25893
|
}
|
|
25410
|
-
|
|
25411
|
-
|
|
25412
|
-
|
|
25413
|
-
|
|
25414
|
-
if (
|
|
25415
|
-
|
|
25416
|
-
|
|
25417
|
-
|
|
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>`);
|
|
25936
|
+
}
|
|
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>`;
|
|
25945
|
+
});
|
|
25418
25946
|
return `
|
|
25419
|
-
<div class="
|
|
25420
|
-
<
|
|
25421
|
-
|
|
25422
|
-
|
|
25423
|
-
|
|
25424
|
-
|
|
25425
|
-
|
|
25426
|
-
|
|
25427
|
-
|
|
25428
|
-
window.location.href = '/' + value + '/dashboard';
|
|
25429
|
-
}
|
|
25430
|
-
}
|
|
25431
|
-
</script>`;
|
|
25947
|
+
<div class="assessment-timeline">
|
|
25948
|
+
<h3>Assessment History</h3>
|
|
25949
|
+
${entries.join("\n")}
|
|
25950
|
+
</div>`;
|
|
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>`;
|
|
25432
25956
|
}
|
|
25433
25957
|
|
|
25434
|
-
// src/web/
|
|
25435
|
-
|
|
25436
|
-
|
|
25437
|
-
|
|
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);
|
|
25438
25963
|
}
|
|
25439
|
-
function
|
|
25440
|
-
|
|
25441
|
-
return `<div class="${cls}"><pre class="mermaid">
|
|
25442
|
-
${definition}
|
|
25443
|
-
</pre></div>`;
|
|
25964
|
+
function registerPersonaPage(persona, pageId, renderer) {
|
|
25965
|
+
PAGE_RENDERERS.set(`${persona}/${pageId}`, renderer);
|
|
25444
25966
|
}
|
|
25445
|
-
function
|
|
25446
|
-
return
|
|
25967
|
+
function getPersonaView(mode) {
|
|
25968
|
+
if (!mode) return void 0;
|
|
25969
|
+
return VIEWS.get(mode);
|
|
25447
25970
|
}
|
|
25448
|
-
function
|
|
25449
|
-
return
|
|
25971
|
+
function getPersonaPageRenderer(persona, pageId) {
|
|
25972
|
+
return PAGE_RENDERERS.get(`${persona}/${pageId}`);
|
|
25450
25973
|
}
|
|
25451
|
-
function
|
|
25452
|
-
|
|
25453
|
-
const months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
|
|
25454
|
-
return `${months[d.getMonth()]} ${d.getDate()}`;
|
|
25974
|
+
function getAllPersonaViews() {
|
|
25975
|
+
return [...VIEWS.values()];
|
|
25455
25976
|
}
|
|
25456
|
-
|
|
25457
|
-
|
|
25458
|
-
|
|
25459
|
-
|
|
25460
|
-
|
|
25461
|
-
|
|
25462
|
-
|
|
25463
|
-
const
|
|
25464
|
-
|
|
25465
|
-
|
|
25466
|
-
|
|
25467
|
-
|
|
25468
|
-
|
|
25469
|
-
|
|
25470
|
-
|
|
25471
|
-
|
|
25472
|
-
|
|
25473
|
-
|
|
25474
|
-
|
|
25475
|
-
|
|
25476
|
-
|
|
25477
|
-
|
|
25478
|
-
|
|
25479
|
-
|
|
25480
|
-
|
|
25481
|
-
|
|
25482
|
-
|
|
25483
|
-
|
|
25484
|
-
|
|
25485
|
-
|
|
25486
|
-
|
|
25487
|
-
|
|
25488
|
-
|
|
25489
|
-
|
|
25490
|
-
|
|
25491
|
-
|
|
25492
|
-
|
|
25493
|
-
|
|
25494
|
-
sprintBoundaries.add(toMs(sprint.endDate));
|
|
25495
|
-
}
|
|
25496
|
-
const sprintLines = [...sprintBoundaries].map(
|
|
25497
|
-
(ms) => `<div class="gantt-sprint-line" style="left:${pct(ms).toFixed(2)}%"></div>`
|
|
25498
|
-
);
|
|
25499
|
-
const now = Date.now();
|
|
25500
|
-
let todayMarker = "";
|
|
25501
|
-
if (now >= timelineStart && now <= timelineEnd) {
|
|
25502
|
-
todayMarker = `<div class="gantt-today" style="left:${pct(now).toFixed(2)}%"></div>`;
|
|
25503
|
-
}
|
|
25504
|
-
const sprintBlocks = visibleSprints.map((sprint) => {
|
|
25505
|
-
const sStart = toMs(sprint.startDate);
|
|
25506
|
-
const sEnd = toMs(sprint.endDate);
|
|
25507
|
-
const left = pct(sStart).toFixed(2);
|
|
25508
|
-
const width = (pct(sEnd) - pct(sStart)).toFixed(2);
|
|
25509
|
-
return `<div class="gantt-sprint-block" style="left:${left}%;width:${width}%">${sanitize(sprint.id, 20)}</div>`;
|
|
25510
|
-
}).join("");
|
|
25511
|
-
const sprintBandRow = `<div class="gantt-row gantt-sprint-band-row">
|
|
25512
|
-
<div class="gantt-label gantt-section-label">Sprints</div>
|
|
25513
|
-
<div class="gantt-track gantt-sprint-band">${sprintBlocks}</div>
|
|
25514
|
-
</div>`;
|
|
25515
|
-
const epicSpanMap = /* @__PURE__ */ new Map();
|
|
25516
|
-
for (const sprint of visibleSprints) {
|
|
25517
|
-
const sStart = toMs(sprint.startDate);
|
|
25518
|
-
const sEnd = toMs(sprint.endDate);
|
|
25519
|
-
for (const eid of sprint.linkedEpics) {
|
|
25520
|
-
if (!epicMap.has(eid)) continue;
|
|
25521
|
-
const existing = epicSpanMap.get(eid);
|
|
25522
|
-
if (existing) {
|
|
25523
|
-
existing.startMs = Math.min(existing.startMs, sStart);
|
|
25524
|
-
existing.endMs = Math.max(existing.endMs, sEnd);
|
|
25525
|
-
} else {
|
|
25526
|
-
epicSpanMap.set(eid, { startMs: sStart, endMs: sEnd });
|
|
25527
|
-
}
|
|
25528
|
-
}
|
|
25529
|
-
}
|
|
25530
|
-
const sortedEpicIds = [...epicSpanMap.keys()].sort((a, b) => {
|
|
25531
|
-
const aSpan = epicSpanMap.get(a);
|
|
25532
|
-
const bSpan = epicSpanMap.get(b);
|
|
25533
|
-
if (aSpan.startMs !== bSpan.startMs) return aSpan.startMs - bSpan.startMs;
|
|
25534
|
-
return a.localeCompare(b);
|
|
25535
|
-
});
|
|
25536
|
-
const epicRows = sortedEpicIds.map((eid) => {
|
|
25537
|
-
const epic = epicMap.get(eid);
|
|
25538
|
-
const { startMs, endMs } = epicSpanMap.get(eid);
|
|
25539
|
-
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";
|
|
25540
|
-
const left = pct(startMs).toFixed(2);
|
|
25541
|
-
const width = (pct(endMs) - pct(startMs)).toFixed(2);
|
|
25542
|
-
const label = sanitize(epic.id + " " + epic.title);
|
|
25543
|
-
return `<div class="gantt-row">
|
|
25544
|
-
<div class="gantt-label">${label}</div>
|
|
25545
|
-
<div class="gantt-track">
|
|
25546
|
-
<div class="gantt-bar ${cls}" style="left:${left}%;width:${width}%"></div>
|
|
25547
|
-
</div>
|
|
25548
|
-
</div>`;
|
|
25549
|
-
}).join("\n");
|
|
25550
|
-
const note = truncated ? `<div class="mermaid-note">${hiddenCount} earlier sprint${hiddenCount > 1 ? "s" : ""} not shown</div>` : "";
|
|
25551
|
-
return `${note}
|
|
25552
|
-
<div class="gantt">
|
|
25553
|
-
<div class="gantt-chart">
|
|
25554
|
-
<div class="gantt-header">
|
|
25555
|
-
<div class="gantt-label"></div>
|
|
25556
|
-
<div class="gantt-track gantt-dates">${markers.join("")}</div>
|
|
25557
|
-
</div>
|
|
25558
|
-
${sprintBandRow}
|
|
25559
|
-
${epicRows}
|
|
25560
|
-
</div>
|
|
25561
|
-
<div class="gantt-overlay">
|
|
25562
|
-
<div class="gantt-label"></div>
|
|
25563
|
-
<div class="gantt-track">${gridLines.join("")}${sprintLines.join("")}${todayMarker}</div>
|
|
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");
|
|
26009
|
+
return `
|
|
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}
|
|
25564
26015
|
</div>
|
|
25565
26016
|
</div>`;
|
|
25566
26017
|
}
|
|
25567
|
-
|
|
25568
|
-
|
|
25569
|
-
|
|
25570
|
-
|
|
25571
|
-
|
|
25572
|
-
|
|
25573
|
-
|
|
25574
|
-
|
|
25575
|
-
|
|
25576
|
-
|
|
25577
|
-
|
|
25578
|
-
|
|
25579
|
-
|
|
25580
|
-
|
|
25581
|
-
|
|
25582
|
-
|
|
25583
|
-
|
|
25584
|
-
|
|
26018
|
+
|
|
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.
|
|
26022
|
+
|
|
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.
|
|
26024
|
+
|
|
26025
|
+
Produce a concise markdown assessment with these sections:
|
|
26026
|
+
|
|
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: []
|
|
25585
26056
|
}
|
|
25586
|
-
}
|
|
25587
|
-
|
|
25588
|
-
|
|
25589
|
-
|
|
25590
|
-
|
|
25591
|
-
|
|
25592
|
-
|
|
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;
|
|
25593
26064
|
}
|
|
25594
26065
|
}
|
|
25595
|
-
|
|
25596
|
-
|
|
25597
|
-
|
|
25598
|
-
|
|
25599
|
-
});
|
|
25600
|
-
|
|
25601
|
-
|
|
25602
|
-
|
|
25603
|
-
|
|
25604
|
-
const
|
|
25605
|
-
|
|
25606
|
-
|
|
25607
|
-
|
|
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
|
+
}
|
|
25608
26139
|
}
|
|
25609
|
-
|
|
25610
|
-
|
|
25611
|
-
|
|
25612
|
-
|
|
25613
|
-
|
|
25614
|
-
|
|
25615
|
-
const sprintsHtml = sprints.map((s) => renderNode(s.id, s.title, s.status, "sprint")).join("\n");
|
|
25616
|
-
const edgesJson = JSON.stringify(edges);
|
|
25617
|
-
return `
|
|
25618
|
-
<div class="flow-diagram" id="flow-diagram">
|
|
25619
|
-
<svg class="flow-lines" id="flow-lines"></svg>
|
|
25620
|
-
<div class="flow-columns">
|
|
25621
|
-
<div class="flow-column">
|
|
25622
|
-
<div class="flow-column-header">Features</div>
|
|
25623
|
-
${featuresHtml}
|
|
25624
|
-
</div>
|
|
25625
|
-
<div class="flow-column">
|
|
25626
|
-
<div class="flow-column-header">Epics</div>
|
|
25627
|
-
${epicsHtml}
|
|
25628
|
-
</div>
|
|
25629
|
-
<div class="flow-column">
|
|
25630
|
-
<div class="flow-column-header">Sprints</div>
|
|
25631
|
-
${sprintsHtml}
|
|
25632
|
-
</div>
|
|
25633
|
-
</div>
|
|
25634
|
-
</div>
|
|
25635
|
-
<script>
|
|
25636
|
-
(function() {
|
|
25637
|
-
var edges = ${edgesJson};
|
|
25638
|
-
var container = document.getElementById('flow-diagram');
|
|
25639
|
-
var svg = document.getElementById('flow-lines');
|
|
25640
|
-
if (!container || !svg) return;
|
|
25641
|
-
|
|
25642
|
-
// Build directed adjacency maps for traversal
|
|
25643
|
-
var fwd = {}; // from \u2192 [to] (Feature\u2192Epic, Epic\u2192Sprint)
|
|
25644
|
-
var bwd = {}; // to \u2192 [from] (Sprint\u2192Epic, Epic\u2192Feature)
|
|
25645
|
-
edges.forEach(function(e) {
|
|
25646
|
-
if (!fwd[e.from]) fwd[e.from] = [];
|
|
25647
|
-
if (!bwd[e.to]) bwd[e.to] = [];
|
|
25648
|
-
fwd[e.from].push(e.to);
|
|
25649
|
-
bwd[e.to].push(e.from);
|
|
25650
|
-
});
|
|
25651
|
-
|
|
25652
|
-
function drawLines() {
|
|
25653
|
-
var rect = container.getBoundingClientRect();
|
|
25654
|
-
var scrollW = container.scrollWidth;
|
|
25655
|
-
var scrollH = container.scrollHeight;
|
|
25656
|
-
svg.setAttribute('width', scrollW);
|
|
25657
|
-
svg.setAttribute('height', scrollH);
|
|
25658
|
-
svg.innerHTML = '';
|
|
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
|
+
}
|
|
25659
26146
|
|
|
25660
|
-
|
|
25661
|
-
|
|
25662
|
-
|
|
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.
|
|
25663
26154
|
|
|
25664
|
-
|
|
25665
|
-
|
|
25666
|
-
|
|
25667
|
-
|
|
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
|
|
25668
26161
|
|
|
25669
|
-
|
|
25670
|
-
|
|
25671
|
-
|
|
25672
|
-
|
|
25673
|
-
|
|
25674
|
-
|
|
25675
|
-
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
|
|
25676
26168
|
|
|
25677
|
-
|
|
25678
|
-
|
|
25679
|
-
|
|
25680
|
-
|
|
25681
|
-
|
|
25682
|
-
|
|
25683
|
-
|
|
25684
|
-
|
|
25685
|
-
|
|
25686
|
-
|
|
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
|
+
};
|
|
25687
26185
|
|
|
25688
|
-
|
|
25689
|
-
|
|
25690
|
-
|
|
25691
|
-
|
|
25692
|
-
|
|
25693
|
-
|
|
25694
|
-
|
|
25695
|
-
var queue = [startId];
|
|
25696
|
-
while (queue.length) {
|
|
25697
|
-
var id = queue.shift();
|
|
25698
|
-
(fwd[id] || []).forEach(function(neighbor) {
|
|
25699
|
-
if (!visited[neighbor]) {
|
|
25700
|
-
visited[neighbor] = true;
|
|
25701
|
-
queue.push(neighbor);
|
|
25702
|
-
}
|
|
25703
|
-
});
|
|
25704
|
-
}
|
|
25705
|
-
// Traverse backward (to\u2192from direction)
|
|
25706
|
-
queue = [startId];
|
|
25707
|
-
while (queue.length) {
|
|
25708
|
-
var id = queue.shift();
|
|
25709
|
-
(bwd[id] || []).forEach(function(neighbor) {
|
|
25710
|
-
if (!visited[neighbor]) {
|
|
25711
|
-
visited[neighbor] = true;
|
|
25712
|
-
queue.push(neighbor);
|
|
25713
|
-
}
|
|
25714
|
-
});
|
|
25715
|
-
}
|
|
25716
|
-
return visited;
|
|
25717
|
-
}
|
|
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.
|
|
25718
26193
|
|
|
25719
|
-
|
|
25720
|
-
|
|
25721
|
-
|
|
25722
|
-
|
|
25723
|
-
|
|
25724
|
-
|
|
25725
|
-
} else {
|
|
25726
|
-
n.classList.add('flow-dim');
|
|
25727
|
-
n.classList.remove('flow-lit');
|
|
25728
|
-
}
|
|
25729
|
-
});
|
|
25730
|
-
svg.querySelectorAll('path').forEach(function(p) {
|
|
25731
|
-
if (connected[p.dataset.from] && connected[p.dataset.to]) {
|
|
25732
|
-
p.classList.add('flow-line-lit');
|
|
25733
|
-
p.classList.remove('flow-line-dim');
|
|
25734
|
-
} else {
|
|
25735
|
-
p.classList.add('flow-line-dim');
|
|
25736
|
-
p.classList.remove('flow-line-lit');
|
|
25737
|
-
}
|
|
25738
|
-
});
|
|
25739
|
-
}
|
|
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
|
|
25740
26200
|
|
|
25741
|
-
|
|
25742
|
-
|
|
25743
|
-
|
|
25744
|
-
|
|
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
|
|
25745
26209
|
|
|
25746
|
-
|
|
25747
|
-
|
|
25748
|
-
|
|
25749
|
-
|
|
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
|
+
};
|
|
25750
26227
|
|
|
25751
|
-
|
|
25752
|
-
|
|
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.
|
|
25753
26235
|
|
|
25754
|
-
|
|
25755
|
-
|
|
25756
|
-
|
|
25757
|
-
|
|
25758
|
-
|
|
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
|
|
25759
26242
|
|
|
25760
|
-
|
|
25761
|
-
|
|
25762
|
-
|
|
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
|
|
25763
26249
|
|
|
25764
|
-
|
|
25765
|
-
|
|
25766
|
-
|
|
25767
|
-
|
|
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
|
+
};
|
|
25768
26268
|
|
|
25769
|
-
|
|
25770
|
-
|
|
25771
|
-
|
|
25772
|
-
|
|
25773
|
-
|
|
25774
|
-
|
|
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
|
+
);
|
|
25775
26280
|
}
|
|
25776
|
-
function
|
|
25777
|
-
|
|
25778
|
-
if (entries.length === 0) {
|
|
25779
|
-
return placeholder(`No data for ${title}.`);
|
|
25780
|
-
}
|
|
25781
|
-
const lines = [`pie title ${sanitize(title, 60)}`];
|
|
25782
|
-
for (const [label, count] of entries) {
|
|
25783
|
-
lines.push(` "${sanitize(label, 30)}" : ${count}`);
|
|
25784
|
-
}
|
|
25785
|
-
return mermaidBlock(lines.join("\n"));
|
|
26281
|
+
function listPersonas() {
|
|
26282
|
+
return [...BUILTIN_PERSONAS];
|
|
25786
26283
|
}
|
|
25787
|
-
|
|
25788
|
-
|
|
25789
|
-
|
|
25790
|
-
|
|
25791
|
-
|
|
25792
|
-
const
|
|
25793
|
-
|
|
25794
|
-
|
|
25795
|
-
|
|
25796
|
-
|
|
25797
|
-
|
|
25798
|
-
|
|
25799
|
-
|
|
25800
|
-
|
|
25801
|
-
|
|
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>`;
|
|
25802
26306
|
}
|
|
25803
26307
|
|
|
25804
26308
|
// src/web/templates/pages/po/dashboard.ts
|
|
@@ -28933,7 +29437,7 @@ function handleRequest(req, res, store, projectName, navGroups) {
|
|
|
28933
29437
|
notFound(res, projectName, navGroups, pathname, persona, pOpts);
|
|
28934
29438
|
return;
|
|
28935
29439
|
}
|
|
28936
|
-
const body = documentDetailPage(doc);
|
|
29440
|
+
const body = documentDetailPage(doc, store);
|
|
28937
29441
|
respond(res, layout({ title: `${id} \u2014 ${doc.frontmatter.title}`, activePath: `/docs/${type}`, projectName, navGroups, persona, ...pOpts }, body));
|
|
28938
29442
|
return;
|
|
28939
29443
|
}
|