ma-agents 3.15.4 → 3.16.0

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.
@@ -37,7 +37,7 @@
37
37
  "name": "ma-skills",
38
38
  "source": "./",
39
39
  "description": "ma-agents extension module providing enterprise SDLC personas and operational workflow skills.",
40
- "version": "3.15.4",
40
+ "version": "3.16.0",
41
41
  "author": {
42
42
  "name": "Alon Mayaffit"
43
43
  },
@@ -2005,11 +2005,11 @@ var require_lunr = __commonJS({
2005
2005
  });
2006
2006
 
2007
2007
  // lib/knowledge-atlas/src/cli.ts
2008
- var path12 = __toESM(require("path"));
2008
+ var path13 = __toESM(require("path"));
2009
2009
 
2010
2010
  // lib/knowledge-atlas/src/generate.ts
2011
- var fs11 = __toESM(require("fs"));
2012
- var path11 = __toESM(require("path"));
2011
+ var fs12 = __toESM(require("fs"));
2012
+ var path12 = __toESM(require("path"));
2013
2013
 
2014
2014
  // lib/knowledge-atlas/src/scan/scanner.ts
2015
2015
  var fs2 = __toESM(require("fs"));
@@ -5636,8 +5636,8 @@ function mergeRecords(unchanged, reprocessed, removed) {
5636
5636
  }
5637
5637
 
5638
5638
  // lib/knowledge-atlas/src/render/site.ts
5639
- var fs7 = __toESM(require("fs"));
5640
- var path7 = __toESM(require("path"));
5639
+ var fs8 = __toESM(require("fs"));
5640
+ var path8 = __toESM(require("path"));
5641
5641
 
5642
5642
  // lib/knowledge-atlas/src/render/design-system.ts
5643
5643
  var DESIGN_SYSTEM_CSS = `/* Knowledge Atlas \u2014 design system (Story 29.3) */
@@ -12098,8 +12098,7 @@ function processMermaidFences(html) {
12098
12098
  hasMermaid = true;
12099
12099
  return [
12100
12100
  '<figure class="mermaid-block" aria-label="Mermaid diagram">',
12101
- '<figcaption class="mermaid-note">Mermaid diagram (source)</figcaption>',
12102
- `<div class="mermaid"><pre class="mermaid-fallback">${codeContent.trim()}</pre></div>`,
12101
+ `<pre class="mermaid">${codeContent.trim()}</pre>`,
12103
12102
  "</figure>"
12104
12103
  ].join("\n");
12105
12104
  });
@@ -12119,6 +12118,9 @@ function getFileNodes(fileId, nodes) {
12119
12118
  function renderDocumentPage(fileRec, manifest, projectRoot, navFlags, resolveAbsPath) {
12120
12119
  const absPath = resolveAbsPath?.(fileRec.path) ?? path6.join(projectRoot, fileRec.path);
12121
12120
  const title = fileTitle(fileRec.path);
12121
+ if (/\.drawio(\.xml)?$/i.test(fileRec.path)) {
12122
+ return renderDrawioPage(fileRec, absPath, title, manifest, navFlags);
12123
+ }
12122
12124
  let rawContent = "";
12123
12125
  try {
12124
12126
  rawContent = fs6.readFileSync(absPath, "utf-8");
@@ -12139,7 +12141,7 @@ function renderDocumentPage(fileRec, manifest, projectRoot, navFlags, resolveAbs
12139
12141
  const tocEntries = extractToc(markdownContent);
12140
12142
  let renderedHtml = md.render(markdownContent);
12141
12143
  renderedHtml = addHeadingAnchors(renderedHtml);
12142
- const { html: mermaidHtml } = processMermaidFences(renderedHtml);
12144
+ const { html: mermaidHtml, hasMermaid } = processMermaidFences(renderedHtml);
12143
12145
  renderedHtml = mermaidHtml;
12144
12146
  renderedHtml = rewriteAuthoredLinks(renderedHtml, manifest, fileRec.path);
12145
12147
  renderedHtml = linkifyHtml(renderedHtml, manifest, fileRec.path);
@@ -12163,7 +12165,9 @@ function renderDocumentPage(fileRec, manifest, projectRoot, navFlags, resolveAbs
12163
12165
  const a = node.anchor;
12164
12166
  if (!a || existingIds.has(a)) continue;
12165
12167
  existingIds.add(a);
12166
- const m = new RegExp('<a\\b[^>]*href="[^"]*#' + escRe(a) + '"', "i").exec(renderedHtml);
12168
+ const ra = escRe(a);
12169
+ let m = new RegExp('<a\\b[^>]*href="[^"]*#' + ra + '"[^>]*>[^<]*</a>\\s*:', "i").exec(renderedHtml);
12170
+ if (!m) m = new RegExp('<a\\b[^>]*href="[^"]*#' + ra + '"', "i").exec(renderedHtml);
12167
12171
  if (m) inlinePlacements.push({ idx: m.index, anchor: a });
12168
12172
  else fallbackAnchors.push(a);
12169
12173
  }
@@ -12186,16 +12190,83 @@ function renderDocumentPage(fileRec, manifest, projectRoot, navFlags, resolveAbs
12186
12190
  "</article>",
12187
12191
  "</div>"
12188
12192
  ].filter(Boolean).join("\n");
12193
+ const scripts = [];
12194
+ if (hasMermaid) {
12195
+ scripts.push('<script src="../assets/mermaid.min.js"></script>');
12196
+ scripts.push(
12197
+ '<script>try{mermaid.initialize({startOnLoad:true,securityLevel:"strict",theme:"neutral"});}catch(e){}</script>'
12198
+ );
12199
+ }
12189
12200
  const html = pageShell({
12190
12201
  title,
12191
12202
  body: pageBody,
12192
12203
  manifest,
12193
12204
  pageType: "document",
12194
12205
  baseHref: "../",
12195
- navFlags
12206
+ navFlags,
12207
+ scripts
12196
12208
  });
12197
12209
  const articleText = `${title} ${renderedHtml}`.replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim();
12198
- return { html, articleText };
12210
+ return { html, articleText, usedMermaid: hasMermaid, usedDrawio: false };
12211
+ }
12212
+ function renderDrawioPage(fileRec, absPath, title, manifest, navFlags) {
12213
+ let xml = "";
12214
+ try {
12215
+ xml = fs6.readFileSync(absPath, "utf-8").replace(/\r\n/g, "\n");
12216
+ } catch {
12217
+ xml = "";
12218
+ }
12219
+ const config2 = { highlight: "#3572b0", nav: true, resize: true, "toolbar-nohide": true, toolbar: "zoom layers", xml };
12220
+ const dataAttr = esc4(JSON.stringify(config2));
12221
+ const body = [
12222
+ '<div class="content-wrap">',
12223
+ '<a class="back-link" href="../index.html">&larr; Back to Home</a>',
12224
+ '<article class="doc-body">',
12225
+ `<h1>${esc4(title)}</h1>`,
12226
+ '<figure class="drawio-block" aria-label="draw.io diagram">',
12227
+ xml ? `<div class="mxgraph" style="max-width:100%;border:1px solid var(--border,#ddd);border-radius:6px" data-mxgraph="${dataAttr}"></div>` : "<p><em>(draw.io file could not be read.)</em></p>",
12228
+ '<noscript><p class="mermaid-note">This is a draw.io diagram; enable JavaScript to render it.</p></noscript>',
12229
+ "</figure>",
12230
+ "</article>",
12231
+ "</div>"
12232
+ ].filter(Boolean).join("\n");
12233
+ const scripts = [
12234
+ '<script>window.DRAWIO_BASE_URL=".";window.PROXY_URL=".";window.STYLE_PATH=".";window.RESOURCE_BASE=".";window.SHAPES_PATH=".";window.STENCIL_PATH=".";window.IMAGE_PATH=".";</script>',
12235
+ '<script src="../assets/drawio-viewer.min.js"></script>'
12236
+ ];
12237
+ const html = pageShell({
12238
+ title,
12239
+ body,
12240
+ manifest,
12241
+ pageType: "document",
12242
+ baseHref: "../",
12243
+ navFlags,
12244
+ scripts
12245
+ });
12246
+ return { html, articleText: title, usedMermaid: false, usedDrawio: true };
12247
+ }
12248
+
12249
+ // lib/knowledge-atlas/src/render/vendor.ts
12250
+ var fs7 = __toESM(require("fs"));
12251
+ var path7 = __toESM(require("path"));
12252
+ function vendorDir() {
12253
+ return path7.join(__dirname, "..", "vendor");
12254
+ }
12255
+ function readVendorAsset(name) {
12256
+ const p = path7.join(vendorDir(), name);
12257
+ try {
12258
+ return fs7.readFileSync(p, "utf-8");
12259
+ } catch {
12260
+ return null;
12261
+ }
12262
+ }
12263
+ function writeVendorAsset(outDir, name) {
12264
+ const content = readVendorAsset(name);
12265
+ if (content == null) return null;
12266
+ const assetsDir = path7.join(outDir, "assets");
12267
+ fs7.mkdirSync(assetsDir, { recursive: true });
12268
+ fs7.writeFileSync(path7.join(assetsDir, name), content.replace(/\r\n/g, "\n"), { encoding: "utf-8" });
12269
+ return `assets/${name}`;
12199
12270
  }
12200
12271
 
12201
12272
  // lib/knowledge-atlas/src/render/search.ts
@@ -12408,8 +12479,8 @@ function esc6(s) {
12408
12479
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
12409
12480
  }
12410
12481
  function writeHtml(filePath, content) {
12411
- fs7.mkdirSync(path7.dirname(filePath), { recursive: true });
12412
- fs7.writeFileSync(filePath, content.replace(/\r\n/g, "\n"), { encoding: "utf-8" });
12482
+ fs8.mkdirSync(path8.dirname(filePath), { recursive: true });
12483
+ fs8.writeFileSync(filePath, content.replace(/\r\n/g, "\n"), { encoding: "utf-8" });
12413
12484
  }
12414
12485
  function renderHomePage(manifest, navFlags) {
12415
12486
  const { projectTitle, generatedAt, counts, nodes, edges } = manifest;
@@ -12546,35 +12617,47 @@ function renderHomePage(manifest, navFlags) {
12546
12617
  function renderSite(manifest, outDir, resolveAbsPath) {
12547
12618
  const written = [];
12548
12619
  const docBodies = /* @__PURE__ */ new Map();
12549
- fs7.mkdirSync(outDir, { recursive: true });
12550
- fs7.mkdirSync(path7.join(outDir, "docs"), { recursive: true });
12620
+ fs8.mkdirSync(outDir, { recursive: true });
12621
+ fs8.mkdirSync(path8.join(outDir, "docs"), { recursive: true });
12551
12622
  const fileIds = manifest.files.map((f) => f.path);
12552
12623
  const slugMap = buildSlugMap(fileIds);
12553
12624
  const navFlags = {
12554
- hasGraphPage: fs7.existsSync(path7.join(outDir, "graph.html")),
12555
- hasDiagramsPage: fs7.existsSync(path7.join(outDir, "diagrams.html")),
12556
- hasOrphansPage: fs7.existsSync(path7.join(outDir, "orphans.html"))
12625
+ hasGraphPage: fs8.existsSync(path8.join(outDir, "graph.html")),
12626
+ hasDiagramsPage: fs8.existsSync(path8.join(outDir, "diagrams.html")),
12627
+ hasOrphansPage: fs8.existsSync(path8.join(outDir, "orphans.html"))
12557
12628
  };
12629
+ let anyMermaid = false;
12630
+ let anyDrawio = false;
12558
12631
  for (const file of manifest.files) {
12559
12632
  const slug = slugMap.get(file.path) ?? filePathToSlug(file.path);
12560
- const { html, articleText } = renderDocumentPage(file, manifest, manifest.projectRoot, navFlags, resolveAbsPath);
12633
+ const { html, articleText, usedMermaid, usedDrawio } = renderDocumentPage(file, manifest, manifest.projectRoot, navFlags, resolveAbsPath);
12561
12634
  const relPath = `docs/${slug}.html`;
12562
- writeHtml(path7.join(outDir, relPath), html);
12635
+ writeHtml(path8.join(outDir, relPath), html);
12563
12636
  written.push(relPath);
12637
+ if (usedMermaid) anyMermaid = true;
12638
+ if (usedDrawio) anyDrawio = true;
12564
12639
  docBodies.set(file.path, articleText);
12565
12640
  }
12641
+ if (anyMermaid) {
12642
+ const rel = writeVendorAsset(outDir, "mermaid.min.js");
12643
+ if (rel) written.push(rel);
12644
+ }
12645
+ if (anyDrawio) {
12646
+ const rel = writeVendorAsset(outDir, "drawio-viewer.min.js");
12647
+ if (rel) written.push(rel);
12648
+ }
12566
12649
  const searchHtml = renderSearchPageHtml(manifest, docBodies, navFlags);
12567
- writeHtml(path7.join(outDir, "search.html"), searchHtml);
12650
+ writeHtml(path8.join(outDir, "search.html"), searchHtml);
12568
12651
  written.push("search.html");
12569
12652
  const indexHtml = renderHomePage(manifest, navFlags);
12570
- writeHtml(path7.join(outDir, "index.html"), indexHtml);
12653
+ writeHtml(path8.join(outDir, "index.html"), indexHtml);
12571
12654
  written.push("index.html");
12572
12655
  return written.sort();
12573
12656
  }
12574
12657
 
12575
12658
  // lib/knowledge-atlas/src/render/graph.ts
12576
- var fs8 = __toESM(require("fs"));
12577
- var path8 = __toESM(require("path"));
12659
+ var fs9 = __toESM(require("fs"));
12660
+ var path9 = __toESM(require("path"));
12578
12661
 
12579
12662
  // lib/knowledge-atlas/src/render/force-graph-umd.ts
12580
12663
  var FORCE_GRAPH_UMD = `// Version 1.51.4 force-graph - https://github.com/vasturiano/force-graph
@@ -13037,9 +13120,9 @@ function renderGraphPage(manifest, outDir) {
13037
13120
  ""
13038
13121
  // trailing newline
13039
13122
  ].join("\n");
13040
- const outPath = path8.join(outDir, "graph.html");
13041
- fs8.mkdirSync(outDir, { recursive: true });
13042
- fs8.writeFileSync(outPath, html.replace(/\r\n/g, "\n"), { encoding: "utf-8" });
13123
+ const outPath = path9.join(outDir, "graph.html");
13124
+ fs9.mkdirSync(outDir, { recursive: true });
13125
+ fs9.writeFileSync(outPath, html.replace(/\r\n/g, "\n"), { encoding: "utf-8" });
13043
13126
  return ["graph.html"];
13044
13127
  }
13045
13128
  function buildAppScript() {
@@ -13349,8 +13432,8 @@ function buildAppScript() {
13349
13432
  }
13350
13433
 
13351
13434
  // lib/knowledge-atlas/src/render/diagrams.ts
13352
- var fs9 = __toESM(require("fs"));
13353
- var path9 = __toESM(require("path"));
13435
+ var fs10 = __toESM(require("fs"));
13436
+ var path10 = __toESM(require("path"));
13354
13437
  function esc8(s) {
13355
13438
  return (s ?? "").toString().replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
13356
13439
  }
@@ -13917,15 +14000,15 @@ function renderDiagrams(manifest, outDir) {
13917
14000
  ""
13918
14001
  // trailing newline
13919
14002
  ].join("\n");
13920
- const outPath = path9.join(outDir, "diagrams.html");
13921
- fs9.mkdirSync(outDir, { recursive: true });
13922
- fs9.writeFileSync(outPath, html.replace(/\r\n/g, "\n"), { encoding: "utf-8" });
14003
+ const outPath = path10.join(outDir, "diagrams.html");
14004
+ fs10.mkdirSync(outDir, { recursive: true });
14005
+ fs10.writeFileSync(outPath, html.replace(/\r\n/g, "\n"), { encoding: "utf-8" });
13923
14006
  return ["diagrams.html"];
13924
14007
  }
13925
14008
 
13926
14009
  // lib/knowledge-atlas/src/render/orphan.ts
13927
- var fs10 = __toESM(require("fs"));
13928
- var path10 = __toESM(require("path"));
14010
+ var fs11 = __toESM(require("fs"));
14011
+ var path11 = __toESM(require("path"));
13929
14012
  function esc9(s) {
13930
14013
  return (s ?? "").toString().replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
13931
14014
  }
@@ -14287,16 +14370,16 @@ function renderOrphanPage(manifest, outDir) {
14287
14370
  ""
14288
14371
  // trailing newline
14289
14372
  ].join("\n");
14290
- const outPath = path10.join(outDir, "orphans.html");
14291
- fs10.mkdirSync(outDir, { recursive: true });
14292
- fs10.writeFileSync(outPath, html.replace(/\r\n/g, "\n"), { encoding: "utf-8" });
14373
+ const outPath = path11.join(outDir, "orphans.html");
14374
+ fs11.mkdirSync(outDir, { recursive: true });
14375
+ fs11.writeFileSync(outPath, html.replace(/\r\n/g, "\n"), { encoding: "utf-8" });
14293
14376
  return ["orphans.html"];
14294
14377
  }
14295
14378
 
14296
14379
  // lib/knowledge-atlas/src/generate.ts
14297
14380
  var MANIFEST_VERSION = 3;
14298
14381
  function normPath(p) {
14299
- return path11.resolve(p).replace(/[\\/]+$/, "");
14382
+ return path12.resolve(p).replace(/[\\/]+$/, "");
14300
14383
  }
14301
14384
  function pathsEqual(a, b) {
14302
14385
  return normPath(a) === normPath(b);
@@ -14304,7 +14387,7 @@ function pathsEqual(a, b) {
14304
14387
  function isUnder(parent, child) {
14305
14388
  const p = normPath(parent);
14306
14389
  const c = normPath(child);
14307
- return c.startsWith(p + path11.sep);
14390
+ return c.startsWith(p + path12.sep);
14308
14391
  }
14309
14392
  var MAX_TIME_VALUE_MS = 864e13;
14310
14393
  function resolveTimestamp(now) {
@@ -14345,10 +14428,10 @@ function isoSecond(d) {
14345
14428
  return d.toISOString().replace(/\.\d{3}Z$/, "Z");
14346
14429
  }
14347
14430
  function deriveProjectTitle(projectRoot) {
14348
- const pkgPath = path11.join(projectRoot, "package.json");
14431
+ const pkgPath = path12.join(projectRoot, "package.json");
14349
14432
  try {
14350
- if (fs11.existsSync(pkgPath)) {
14351
- const raw = fs11.readFileSync(pkgPath, "utf-8");
14433
+ if (fs12.existsSync(pkgPath)) {
14434
+ const raw = fs12.readFileSync(pkgPath, "utf-8");
14352
14435
  const pkg = JSON.parse(raw);
14353
14436
  if (typeof pkg["name"] === "string" && pkg["name"].trim()) {
14354
14437
  return pkg["name"].trim();
@@ -14356,13 +14439,13 @@ function deriveProjectTitle(projectRoot) {
14356
14439
  }
14357
14440
  } catch {
14358
14441
  }
14359
- const base = path11.basename(projectRoot).trim();
14442
+ const base = path12.basename(projectRoot).trim();
14360
14443
  return base.length > 0 ? base : "Unknown Project";
14361
14444
  }
14362
14445
  function extractFileRecord(absPath, fileId, docType, contentHash2) {
14363
14446
  let content = "";
14364
14447
  try {
14365
- content = fs11.readFileSync(absPath, "utf-8");
14448
+ content = fs12.readFileSync(absPath, "utf-8");
14366
14449
  } catch {
14367
14450
  return { path: fileId, contentHash: contentHash2, type: docType, entities: [], statuses: [], edges: [] };
14368
14451
  }
@@ -14418,19 +14501,19 @@ function generate(opts) {
14418
14501
  if (!projectRoot || typeof projectRoot !== "string") {
14419
14502
  throw new Error("generate: projectRoot is required and must be a non-empty string");
14420
14503
  }
14421
- if (!path11.isAbsolute(projectRoot)) {
14504
+ if (!path12.isAbsolute(projectRoot)) {
14422
14505
  throw new Error(`generate: projectRoot must be an absolute path (got: ${projectRoot})`);
14423
14506
  }
14424
- const rootStat = fs11.statSync(projectRoot, { throwIfNoEntry: false });
14507
+ const rootStat = fs12.statSync(projectRoot, { throwIfNoEntry: false });
14425
14508
  if (!rootStat || !rootStat.isDirectory()) {
14426
14509
  throw new Error(`generate: projectRoot does not exist or is not a directory: ${projectRoot}`);
14427
14510
  }
14428
14511
  const { sources, outputFolder, outputOverlap } = resolveKnowledgeSources(projectRoot);
14429
- const bmadOutputDir = path11.join(projectRoot, outputFolder);
14430
- const defaultOutDir = path11.join(bmadOutputDir, "knowledge-site");
14512
+ const bmadOutputDir = path12.join(projectRoot, outputFolder);
14513
+ const defaultOutDir = path12.join(bmadOutputDir, "knowledge-site");
14431
14514
  const outDir = outDirOverride ?? defaultOutDir;
14432
- const rel = path11.relative(projectRoot, outDir);
14433
- if (rel.startsWith("..") || path11.isAbsolute(rel)) {
14515
+ const rel = path12.relative(projectRoot, outDir);
14516
+ if (rel.startsWith("..") || path12.isAbsolute(rel)) {
14434
14517
  throw new Error(`generate: outDir escapes projectRoot: ${outDir}`);
14435
14518
  }
14436
14519
  const generatedAt = resolveTimestamp(opts.now);
@@ -14441,14 +14524,14 @@ function generate(opts) {
14441
14524
  const isDefaultOut = pathsEqual(outDir, defaultOutDir);
14442
14525
  const sourceAtOrUnderOut = sources.some((s) => pathsEqual(s.absPath, outDir) || isUnder(outDir, s.absPath));
14443
14526
  if (isDefaultOut && !outputOverlap && !sourceAtOrUnderOut) {
14444
- fs11.rmSync(outDir, { recursive: true, force: true });
14527
+ fs12.rmSync(outDir, { recursive: true, force: true });
14445
14528
  } else if (isDefaultOut && (outputOverlap || sourceAtOrUnderOut)) {
14446
14529
  process.stderr.write(
14447
14530
  `[atlas] WARN: refusing to purge ${outDir} \u2014 a knowledge source resolves at or under it (data-loss guard)
14448
14531
  `
14449
14532
  );
14450
14533
  }
14451
- fs11.mkdirSync(outDir, { recursive: true });
14534
+ fs12.mkdirSync(outDir, { recursive: true });
14452
14535
  const safeRender = (label, fn) => {
14453
14536
  try {
14454
14537
  return fn();
@@ -14465,7 +14548,7 @@ function generate(opts) {
14465
14548
  const extraFileCount = graphFiles.length + diagramFiles.length + orphanFiles.length;
14466
14549
  const written = renderSite(manifest, outDir, resolveAbsPath);
14467
14550
  writeManifest(outDir, manifest);
14468
- const indexPath = path11.join(outDir, "index.html");
14551
+ const indexPath = path12.join(outDir, "index.html");
14469
14552
  return {
14470
14553
  outDir,
14471
14554
  indexPath,
@@ -14486,7 +14569,7 @@ function main() {
14486
14569
  usage();
14487
14570
  process.exit(args.length === 0 ? 1 : 0);
14488
14571
  }
14489
- const projectRoot = path12.resolve(args[0]);
14572
+ const projectRoot = path13.resolve(args[0]);
14490
14573
  let outDir;
14491
14574
  const outIdx = args.indexOf("--out");
14492
14575
  if (outIdx !== -1) {
@@ -14494,7 +14577,7 @@ function main() {
14494
14577
  process.stderr.write("[atlas] ERROR: --out requires a directory argument\n");
14495
14578
  process.exit(1);
14496
14579
  }
14497
- outDir = path12.resolve(args[outIdx + 1]);
14580
+ outDir = path13.resolve(args[outIdx + 1]);
14498
14581
  }
14499
14582
  try {
14500
14583
  const result = generate({ projectRoot, outDir });
@@ -151,12 +151,16 @@ function processMermaidFences(html: string): { html: string; hasMermaid: boolean
151
151
  const fenceRe = /<pre><code class="language-mermaid">([\s\S]*?)<\/code><\/pre>/gi;
152
152
  const result = html.replace(fenceRe, (_match, codeContent: string) => {
153
153
  hasMermaid = true;
154
- // codeContent is already HTML-entity-encoded by markdown-it; keep it encoded
155
- // (safe) and present it verbatim inside a labelled figure.
154
+ // `<pre class="mermaid">` is mermaid's default render target: the vendored
155
+ // runtime (assets/mermaid.min.js, injected by the shell when hasMermaid) reads
156
+ // the element's textContent — the HTML entities markdown-it emitted decode
157
+ // back to the literal diagram source — and replaces it with rendered SVG.
158
+ // With no JS the source stays legible (the plain-text degradation fallback).
159
+ // linkify skips <pre>, and sanitize preserves `<pre class="…">`, so the
160
+ // diagram source reaches the browser uncorrupted (no injected <a> tags).
156
161
  return [
157
162
  '<figure class="mermaid-block" aria-label="Mermaid diagram">',
158
- '<figcaption class="mermaid-note">Mermaid diagram (source)</figcaption>',
159
- `<div class="mermaid"><pre class="mermaid-fallback">${codeContent.trim()}</pre></div>`,
163
+ `<pre class="mermaid">${codeContent.trim()}</pre>`,
160
164
  '</figure>',
161
165
  ].join('\n');
162
166
  });
@@ -218,6 +222,10 @@ export interface RenderedDocument {
218
222
  html: string;
219
223
  /** Plain-text of the rendered article body only (for the search index). */
220
224
  articleText: string;
225
+ /** True if this page contains a mermaid diagram (needs the vendored runtime). */
226
+ usedMermaid: boolean;
227
+ /** True if this page is a rendered .drawio diagram (needs the vendored viewer). */
228
+ usedDrawio: boolean;
221
229
  }
222
230
 
223
231
  export function renderDocumentPage(
@@ -230,6 +238,12 @@ export function renderDocumentPage(
230
238
  const absPath = resolveAbsPath?.(fileRec.path) ?? path.join(projectRoot, fileRec.path);
231
239
  const title = fileTitle(fileRec.path);
232
240
 
241
+ // draw.io files are mxGraph XML, not markdown — render them via the vendored
242
+ // GraphViewer instead of the markdown pipeline (which would strip the XML).
243
+ if (/\.drawio(\.xml)?$/i.test(fileRec.path)) {
244
+ return renderDrawioPage(fileRec, absPath, title, manifest, navFlags);
245
+ }
246
+
233
247
  // Read markdown content
234
248
  let rawContent = '';
235
249
  try {
@@ -258,8 +272,8 @@ export function renderDocumentPage(
258
272
  // Add heading anchors for in-page navigation
259
273
  renderedHtml = addHeadingAnchors(renderedHtml);
260
274
 
261
- // Process mermaid fences (rendered as a self-contained plain-text figure)
262
- const { html: mermaidHtml } = processMermaidFences(renderedHtml);
275
+ // Process mermaid fences live `<pre class="mermaid">` render targets.
276
+ const { html: mermaidHtml, hasMermaid } = processMermaidFences(renderedHtml);
263
277
  renderedHtml = mermaidHtml;
264
278
 
265
279
  // Rewrite AUTHORED relative markdown cross-links (e.g. `[x](roadmap.md)`)
@@ -314,8 +328,15 @@ export function renderDocumentPage(
314
328
  const a = node.anchor;
315
329
  if (!a || existingIds.has(a)) continue;
316
330
  existingIds.add(a);
317
- // First <a> tag whose href ends with this exact anchor fragment.
318
- const m = new RegExp('<a\\b[^>]*href="[^"]*#' + escRe(a) + '"', 'i').exec(renderedHtml);
331
+ const ra = escRe(a);
332
+ // Prefer the DEFINITION occurrence a linkified mention immediately followed
333
+ // by a colon (the BMAD "**FRn:** description" / "## FRn: …" pattern) — over an
334
+ // earlier passing mention. Without this, an FR that a revision-history /
335
+ // changelog row lists near the top of the PRD ("…update FR21, FR91…") anchored
336
+ // there, so clicking the FR jumped to the changelog instead of its definition.
337
+ let m = new RegExp('<a\\b[^>]*href="[^"]*#' + ra + '"[^>]*>[^<]*</a>\\s*:', 'i').exec(renderedHtml);
338
+ // Otherwise fall back to the first linkified mention anywhere on the page.
339
+ if (!m) m = new RegExp('<a\\b[^>]*href="[^"]*#' + ra + '"', 'i').exec(renderedHtml);
319
340
  if (m) inlinePlacements.push({ idx: m.index, anchor: a });
320
341
  else fallbackAnchors.push(a);
321
342
  }
@@ -347,9 +368,19 @@ export function renderDocumentPage(
347
368
  '</div>',
348
369
  ].filter(Boolean).join('\n');
349
370
 
350
- // Reading views require zero JS — mermaid is rendered as a plain-text figure
351
- // and all links are static HTML (progressive-enhancement baseline, AC 7).
352
- // Doc pages live under docs/, so nav/search links need a "../" baseHref.
371
+ // Reading content is legible with zero JS (progressive-enhancement baseline,
372
+ // AC 7): all links are static HTML and a mermaid figure degrades to its
373
+ // legible source. When the page HAS a mermaid diagram we additionally inject
374
+ // the vendored runtime so it renders to SVG — this is the only script on a
375
+ // doc page, and only on pages that need it. Doc pages live under docs/, so
376
+ // nav/search/asset links need a "../" baseHref.
377
+ const scripts: string[] = [];
378
+ if (hasMermaid) {
379
+ scripts.push('<script src="../assets/mermaid.min.js"></script>');
380
+ scripts.push(
381
+ '<script>try{mermaid.initialize({startOnLoad:true,securityLevel:"strict",theme:"neutral"});}catch(e){}</script>',
382
+ );
383
+ }
353
384
  const html = pageShell({
354
385
  title,
355
386
  body: pageBody,
@@ -357,6 +388,7 @@ export function renderDocumentPage(
357
388
  pageType: 'document',
358
389
  baseHref: '../',
359
390
  navFlags,
391
+ scripts,
360
392
  });
361
393
 
362
394
  // Article-only plain text for the search index (SF-1): strip tags from the
@@ -367,5 +399,70 @@ export function renderDocumentPage(
367
399
  .replace(/\s+/g, ' ')
368
400
  .trim();
369
401
 
370
- return { html, articleText };
402
+ return { html, articleText, usedMermaid: hasMermaid, usedDrawio: false };
403
+ }
404
+
405
+ /**
406
+ * Render a draw.io (`.drawio`) file as a live diagram. draw.io files are
407
+ * mxGraph XML, not markdown — running them through the markdown/sanitize
408
+ * pipeline strips every mxGraph tag and yields a blank page. Instead we embed
409
+ * the raw XML in a draw.io GraphViewer element (`<div class="mxgraph">`) and
410
+ * load the vendored, URL-neutralized viewer, which renders it to SVG entirely
411
+ * offline. With no JS the page shows a short note + a link to the source file.
412
+ */
413
+ function renderDrawioPage(
414
+ fileRec: FileRecord,
415
+ absPath: string,
416
+ title: string,
417
+ manifest: KnowledgeManifest,
418
+ navFlags?: NavFlags,
419
+ ): RenderedDocument {
420
+ let xml = '';
421
+ try {
422
+ xml = fs.readFileSync(absPath, 'utf-8').replace(/\r\n/g, '\n');
423
+ } catch {
424
+ xml = '';
425
+ }
426
+
427
+ // GraphViewer reads a JSON config from data-mxgraph; `xml` carries the source.
428
+ // esc() HTML-escapes the JSON so the attribute is well-formed; the viewer
429
+ // HTML-decodes then JSON-parses it back to the original XML.
430
+ const config = { highlight: '#3572b0', nav: true, resize: true, 'toolbar-nohide': true, toolbar: 'zoom layers', xml };
431
+ const dataAttr = esc(JSON.stringify(config));
432
+
433
+ const body = [
434
+ '<div class="content-wrap">',
435
+ '<a class="back-link" href="../index.html">&larr; Back to Home</a>',
436
+ '<article class="doc-body">',
437
+ `<h1>${esc(title)}</h1>`,
438
+ '<figure class="drawio-block" aria-label="draw.io diagram">',
439
+ xml
440
+ ? `<div class="mxgraph" style="max-width:100%;border:1px solid var(--border,#ddd);border-radius:6px" data-mxgraph="${dataAttr}"></div>`
441
+ : '<p><em>(draw.io file could not be read.)</em></p>',
442
+ '<noscript><p class="mermaid-note">This is a draw.io diagram; enable JavaScript to render it.</p></noscript>',
443
+ '</figure>',
444
+ '</article>',
445
+ '</div>',
446
+ ].filter(Boolean).join('\n');
447
+
448
+ // Predefine the viewer's config globals to inert local values so it never
449
+ // reaches for a network endpoint (the vendored file's diagrams.net/font/icon
450
+ // URLs are already neutralized; this belt-and-suspenders covers the `||`
451
+ // fallbacks, which need a TRUTHY value — "" would fall through to the URL).
452
+ const scripts = [
453
+ '<script>window.DRAWIO_BASE_URL=".";window.PROXY_URL=".";window.STYLE_PATH=".";window.RESOURCE_BASE=".";window.SHAPES_PATH=".";window.STENCIL_PATH=".";window.IMAGE_PATH=".";</script>',
454
+ '<script src="../assets/drawio-viewer.min.js"></script>',
455
+ ];
456
+
457
+ const html = pageShell({
458
+ title,
459
+ body,
460
+ manifest,
461
+ pageType: 'document',
462
+ baseHref: '../',
463
+ navFlags,
464
+ scripts,
465
+ });
466
+
467
+ return { html, articleText: title, usedMermaid: false, usedDrawio: true };
371
468
  }
@@ -28,6 +28,7 @@ import * as path from 'path';
28
28
  import { pageShell } from './layout.js';
29
29
  import type { NavFlags } from './layout.js';
30
30
  import { renderDocumentPage } from './document.js';
31
+ import { writeVendorAsset } from './vendor.js';
31
32
  import { renderSearchPageHtml } from './search.js';
32
33
  import { filePathToSlug, buildSlugMap } from './paths.js';
33
34
  import type { KnowledgeManifest, FileRecord } from '../types.js';
@@ -289,19 +290,36 @@ export function renderSite(
289
290
  hasOrphansPage: fs.existsSync(path.join(outDir, 'orphans.html')),
290
291
  };
291
292
 
292
- // Step 1: Render document pages (reading views). Zero JS required.
293
+ // Step 1: Render document pages (reading views). Legible with zero JS; pages
294
+ // that contain a mermaid diagram additionally load the vendored runtime.
295
+ let anyMermaid = false;
296
+ let anyDrawio = false;
293
297
  for (const file of manifest.files) {
294
298
  const slug = slugMap.get(file.path) ?? filePathToSlug(file.path);
295
- const { html, articleText } = renderDocumentPage(file, manifest, manifest.projectRoot, navFlags, resolveAbsPath);
299
+ const { html, articleText, usedMermaid, usedDrawio } = renderDocumentPage(file, manifest, manifest.projectRoot, navFlags, resolveAbsPath);
296
300
  const relPath = `docs/${slug}.html`;
297
301
  writeHtml(path.join(outDir, relPath), html);
298
302
  written.push(relPath);
303
+ if (usedMermaid) anyMermaid = true;
304
+ if (usedDrawio) anyDrawio = true;
299
305
 
300
306
  // Index ONLY the article text (no nav/footer/TOC/status chrome), so search
301
307
  // hits reflect document content rather than shell boilerplate.
302
308
  docBodies.set(file.path, articleText);
303
309
  }
304
310
 
311
+ // Step 1b: Copy the vendored browser runtimes into assets/ — but only the ones
312
+ // a page actually references (mermaid runtime / draw.io viewer), so a site with
313
+ // no such diagrams carries no extra weight.
314
+ if (anyMermaid) {
315
+ const rel = writeVendorAsset(outDir, 'mermaid.min.js');
316
+ if (rel) written.push(rel);
317
+ }
318
+ if (anyDrawio) {
319
+ const rel = writeVendorAsset(outDir, 'drawio-viewer.min.js');
320
+ if (rel) written.push(rel);
321
+ }
322
+
305
323
  // Step 2: Render search.html (offline lunr search; PE fallback).
306
324
  const searchHtml = renderSearchPageHtml(manifest, docBodies, navFlags);
307
325
  writeHtml(path.join(outDir, 'search.html'), searchHtml);