ma-agents 3.15.5 → 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.5",
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);
@@ -12188,16 +12190,83 @@ function renderDocumentPage(fileRec, manifest, projectRoot, navFlags, resolveAbs
12188
12190
  "</article>",
12189
12191
  "</div>"
12190
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
+ }
12191
12200
  const html = pageShell({
12192
12201
  title,
12193
12202
  body: pageBody,
12194
12203
  manifest,
12195
12204
  pageType: "document",
12196
12205
  baseHref: "../",
12197
- navFlags
12206
+ navFlags,
12207
+ scripts
12198
12208
  });
12199
12209
  const articleText = `${title} ${renderedHtml}`.replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim();
12200
- 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}`;
12201
12270
  }
12202
12271
 
12203
12272
  // lib/knowledge-atlas/src/render/search.ts
@@ -12410,8 +12479,8 @@ function esc6(s) {
12410
12479
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
12411
12480
  }
12412
12481
  function writeHtml(filePath, content) {
12413
- fs7.mkdirSync(path7.dirname(filePath), { recursive: true });
12414
- 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" });
12415
12484
  }
12416
12485
  function renderHomePage(manifest, navFlags) {
12417
12486
  const { projectTitle, generatedAt, counts, nodes, edges } = manifest;
@@ -12548,35 +12617,47 @@ function renderHomePage(manifest, navFlags) {
12548
12617
  function renderSite(manifest, outDir, resolveAbsPath) {
12549
12618
  const written = [];
12550
12619
  const docBodies = /* @__PURE__ */ new Map();
12551
- fs7.mkdirSync(outDir, { recursive: true });
12552
- fs7.mkdirSync(path7.join(outDir, "docs"), { recursive: true });
12620
+ fs8.mkdirSync(outDir, { recursive: true });
12621
+ fs8.mkdirSync(path8.join(outDir, "docs"), { recursive: true });
12553
12622
  const fileIds = manifest.files.map((f) => f.path);
12554
12623
  const slugMap = buildSlugMap(fileIds);
12555
12624
  const navFlags = {
12556
- hasGraphPage: fs7.existsSync(path7.join(outDir, "graph.html")),
12557
- hasDiagramsPage: fs7.existsSync(path7.join(outDir, "diagrams.html")),
12558
- 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"))
12559
12628
  };
12629
+ let anyMermaid = false;
12630
+ let anyDrawio = false;
12560
12631
  for (const file of manifest.files) {
12561
12632
  const slug = slugMap.get(file.path) ?? filePathToSlug(file.path);
12562
- const { html, articleText } = renderDocumentPage(file, manifest, manifest.projectRoot, navFlags, resolveAbsPath);
12633
+ const { html, articleText, usedMermaid, usedDrawio } = renderDocumentPage(file, manifest, manifest.projectRoot, navFlags, resolveAbsPath);
12563
12634
  const relPath = `docs/${slug}.html`;
12564
- writeHtml(path7.join(outDir, relPath), html);
12635
+ writeHtml(path8.join(outDir, relPath), html);
12565
12636
  written.push(relPath);
12637
+ if (usedMermaid) anyMermaid = true;
12638
+ if (usedDrawio) anyDrawio = true;
12566
12639
  docBodies.set(file.path, articleText);
12567
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
+ }
12568
12649
  const searchHtml = renderSearchPageHtml(manifest, docBodies, navFlags);
12569
- writeHtml(path7.join(outDir, "search.html"), searchHtml);
12650
+ writeHtml(path8.join(outDir, "search.html"), searchHtml);
12570
12651
  written.push("search.html");
12571
12652
  const indexHtml = renderHomePage(manifest, navFlags);
12572
- writeHtml(path7.join(outDir, "index.html"), indexHtml);
12653
+ writeHtml(path8.join(outDir, "index.html"), indexHtml);
12573
12654
  written.push("index.html");
12574
12655
  return written.sort();
12575
12656
  }
12576
12657
 
12577
12658
  // lib/knowledge-atlas/src/render/graph.ts
12578
- var fs8 = __toESM(require("fs"));
12579
- var path8 = __toESM(require("path"));
12659
+ var fs9 = __toESM(require("fs"));
12660
+ var path9 = __toESM(require("path"));
12580
12661
 
12581
12662
  // lib/knowledge-atlas/src/render/force-graph-umd.ts
12582
12663
  var FORCE_GRAPH_UMD = `// Version 1.51.4 force-graph - https://github.com/vasturiano/force-graph
@@ -13039,9 +13120,9 @@ function renderGraphPage(manifest, outDir) {
13039
13120
  ""
13040
13121
  // trailing newline
13041
13122
  ].join("\n");
13042
- const outPath = path8.join(outDir, "graph.html");
13043
- fs8.mkdirSync(outDir, { recursive: true });
13044
- 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" });
13045
13126
  return ["graph.html"];
13046
13127
  }
13047
13128
  function buildAppScript() {
@@ -13351,8 +13432,8 @@ function buildAppScript() {
13351
13432
  }
13352
13433
 
13353
13434
  // lib/knowledge-atlas/src/render/diagrams.ts
13354
- var fs9 = __toESM(require("fs"));
13355
- var path9 = __toESM(require("path"));
13435
+ var fs10 = __toESM(require("fs"));
13436
+ var path10 = __toESM(require("path"));
13356
13437
  function esc8(s) {
13357
13438
  return (s ?? "").toString().replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
13358
13439
  }
@@ -13919,15 +14000,15 @@ function renderDiagrams(manifest, outDir) {
13919
14000
  ""
13920
14001
  // trailing newline
13921
14002
  ].join("\n");
13922
- const outPath = path9.join(outDir, "diagrams.html");
13923
- fs9.mkdirSync(outDir, { recursive: true });
13924
- 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" });
13925
14006
  return ["diagrams.html"];
13926
14007
  }
13927
14008
 
13928
14009
  // lib/knowledge-atlas/src/render/orphan.ts
13929
- var fs10 = __toESM(require("fs"));
13930
- var path10 = __toESM(require("path"));
14010
+ var fs11 = __toESM(require("fs"));
14011
+ var path11 = __toESM(require("path"));
13931
14012
  function esc9(s) {
13932
14013
  return (s ?? "").toString().replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
13933
14014
  }
@@ -14289,16 +14370,16 @@ function renderOrphanPage(manifest, outDir) {
14289
14370
  ""
14290
14371
  // trailing newline
14291
14372
  ].join("\n");
14292
- const outPath = path10.join(outDir, "orphans.html");
14293
- fs10.mkdirSync(outDir, { recursive: true });
14294
- 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" });
14295
14376
  return ["orphans.html"];
14296
14377
  }
14297
14378
 
14298
14379
  // lib/knowledge-atlas/src/generate.ts
14299
14380
  var MANIFEST_VERSION = 3;
14300
14381
  function normPath(p) {
14301
- return path11.resolve(p).replace(/[\\/]+$/, "");
14382
+ return path12.resolve(p).replace(/[\\/]+$/, "");
14302
14383
  }
14303
14384
  function pathsEqual(a, b) {
14304
14385
  return normPath(a) === normPath(b);
@@ -14306,7 +14387,7 @@ function pathsEqual(a, b) {
14306
14387
  function isUnder(parent, child) {
14307
14388
  const p = normPath(parent);
14308
14389
  const c = normPath(child);
14309
- return c.startsWith(p + path11.sep);
14390
+ return c.startsWith(p + path12.sep);
14310
14391
  }
14311
14392
  var MAX_TIME_VALUE_MS = 864e13;
14312
14393
  function resolveTimestamp(now) {
@@ -14347,10 +14428,10 @@ function isoSecond(d) {
14347
14428
  return d.toISOString().replace(/\.\d{3}Z$/, "Z");
14348
14429
  }
14349
14430
  function deriveProjectTitle(projectRoot) {
14350
- const pkgPath = path11.join(projectRoot, "package.json");
14431
+ const pkgPath = path12.join(projectRoot, "package.json");
14351
14432
  try {
14352
- if (fs11.existsSync(pkgPath)) {
14353
- const raw = fs11.readFileSync(pkgPath, "utf-8");
14433
+ if (fs12.existsSync(pkgPath)) {
14434
+ const raw = fs12.readFileSync(pkgPath, "utf-8");
14354
14435
  const pkg = JSON.parse(raw);
14355
14436
  if (typeof pkg["name"] === "string" && pkg["name"].trim()) {
14356
14437
  return pkg["name"].trim();
@@ -14358,13 +14439,13 @@ function deriveProjectTitle(projectRoot) {
14358
14439
  }
14359
14440
  } catch {
14360
14441
  }
14361
- const base = path11.basename(projectRoot).trim();
14442
+ const base = path12.basename(projectRoot).trim();
14362
14443
  return base.length > 0 ? base : "Unknown Project";
14363
14444
  }
14364
14445
  function extractFileRecord(absPath, fileId, docType, contentHash2) {
14365
14446
  let content = "";
14366
14447
  try {
14367
- content = fs11.readFileSync(absPath, "utf-8");
14448
+ content = fs12.readFileSync(absPath, "utf-8");
14368
14449
  } catch {
14369
14450
  return { path: fileId, contentHash: contentHash2, type: docType, entities: [], statuses: [], edges: [] };
14370
14451
  }
@@ -14420,19 +14501,19 @@ function generate(opts) {
14420
14501
  if (!projectRoot || typeof projectRoot !== "string") {
14421
14502
  throw new Error("generate: projectRoot is required and must be a non-empty string");
14422
14503
  }
14423
- if (!path11.isAbsolute(projectRoot)) {
14504
+ if (!path12.isAbsolute(projectRoot)) {
14424
14505
  throw new Error(`generate: projectRoot must be an absolute path (got: ${projectRoot})`);
14425
14506
  }
14426
- const rootStat = fs11.statSync(projectRoot, { throwIfNoEntry: false });
14507
+ const rootStat = fs12.statSync(projectRoot, { throwIfNoEntry: false });
14427
14508
  if (!rootStat || !rootStat.isDirectory()) {
14428
14509
  throw new Error(`generate: projectRoot does not exist or is not a directory: ${projectRoot}`);
14429
14510
  }
14430
14511
  const { sources, outputFolder, outputOverlap } = resolveKnowledgeSources(projectRoot);
14431
- const bmadOutputDir = path11.join(projectRoot, outputFolder);
14432
- const defaultOutDir = path11.join(bmadOutputDir, "knowledge-site");
14512
+ const bmadOutputDir = path12.join(projectRoot, outputFolder);
14513
+ const defaultOutDir = path12.join(bmadOutputDir, "knowledge-site");
14433
14514
  const outDir = outDirOverride ?? defaultOutDir;
14434
- const rel = path11.relative(projectRoot, outDir);
14435
- if (rel.startsWith("..") || path11.isAbsolute(rel)) {
14515
+ const rel = path12.relative(projectRoot, outDir);
14516
+ if (rel.startsWith("..") || path12.isAbsolute(rel)) {
14436
14517
  throw new Error(`generate: outDir escapes projectRoot: ${outDir}`);
14437
14518
  }
14438
14519
  const generatedAt = resolveTimestamp(opts.now);
@@ -14443,14 +14524,14 @@ function generate(opts) {
14443
14524
  const isDefaultOut = pathsEqual(outDir, defaultOutDir);
14444
14525
  const sourceAtOrUnderOut = sources.some((s) => pathsEqual(s.absPath, outDir) || isUnder(outDir, s.absPath));
14445
14526
  if (isDefaultOut && !outputOverlap && !sourceAtOrUnderOut) {
14446
- fs11.rmSync(outDir, { recursive: true, force: true });
14527
+ fs12.rmSync(outDir, { recursive: true, force: true });
14447
14528
  } else if (isDefaultOut && (outputOverlap || sourceAtOrUnderOut)) {
14448
14529
  process.stderr.write(
14449
14530
  `[atlas] WARN: refusing to purge ${outDir} \u2014 a knowledge source resolves at or under it (data-loss guard)
14450
14531
  `
14451
14532
  );
14452
14533
  }
14453
- fs11.mkdirSync(outDir, { recursive: true });
14534
+ fs12.mkdirSync(outDir, { recursive: true });
14454
14535
  const safeRender = (label, fn) => {
14455
14536
  try {
14456
14537
  return fn();
@@ -14467,7 +14548,7 @@ function generate(opts) {
14467
14548
  const extraFileCount = graphFiles.length + diagramFiles.length + orphanFiles.length;
14468
14549
  const written = renderSite(manifest, outDir, resolveAbsPath);
14469
14550
  writeManifest(outDir, manifest);
14470
- const indexPath = path11.join(outDir, "index.html");
14551
+ const indexPath = path12.join(outDir, "index.html");
14471
14552
  return {
14472
14553
  outDir,
14473
14554
  indexPath,
@@ -14488,7 +14569,7 @@ function main() {
14488
14569
  usage();
14489
14570
  process.exit(args.length === 0 ? 1 : 0);
14490
14571
  }
14491
- const projectRoot = path12.resolve(args[0]);
14572
+ const projectRoot = path13.resolve(args[0]);
14492
14573
  let outDir;
14493
14574
  const outIdx = args.indexOf("--out");
14494
14575
  if (outIdx !== -1) {
@@ -14496,7 +14577,7 @@ function main() {
14496
14577
  process.stderr.write("[atlas] ERROR: --out requires a directory argument\n");
14497
14578
  process.exit(1);
14498
14579
  }
14499
- outDir = path12.resolve(args[outIdx + 1]);
14580
+ outDir = path13.resolve(args[outIdx + 1]);
14500
14581
  }
14501
14582
  try {
14502
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)`)
@@ -354,9 +368,19 @@ export function renderDocumentPage(
354
368
  '</div>',
355
369
  ].filter(Boolean).join('\n');
356
370
 
357
- // Reading views require zero JS — mermaid is rendered as a plain-text figure
358
- // and all links are static HTML (progressive-enhancement baseline, AC 7).
359
- // 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
+ }
360
384
  const html = pageShell({
361
385
  title,
362
386
  body: pageBody,
@@ -364,6 +388,7 @@ export function renderDocumentPage(
364
388
  pageType: 'document',
365
389
  baseHref: '../',
366
390
  navFlags,
391
+ scripts,
367
392
  });
368
393
 
369
394
  // Article-only plain text for the search index (SF-1): strip tags from the
@@ -374,5 +399,70 @@ export function renderDocumentPage(
374
399
  .replace(/\s+/g, ' ')
375
400
  .trim();
376
401
 
377
- 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 };
378
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);
@@ -0,0 +1,43 @@
1
+ /**
2
+ * lib/knowledge-atlas/src/render/vendor.ts
3
+ * ---------------------------------------------------------------------------
4
+ * Locate and read vendored browser runtimes that are too large to embed as
5
+ * inline strings in the bundle (mermaid, the drawio viewer). They ship as real
6
+ * files under `lib/knowledge-atlas/vendor/` and are copied verbatim into a
7
+ * generated site's `assets/` directory, referenced by a local relative
8
+ * `<script src="…">`. Nothing here is fetched over the network — the files are
9
+ * part of the committed, offline ma-agents distribution.
10
+ *
11
+ * The engine runs as the bundled `lib/knowledge-atlas/dist/atlas.cjs`, so the
12
+ * vendor directory is a sibling of `dist/` (`../vendor`).
13
+ */
14
+
15
+ import * as fs from 'fs';
16
+ import * as path from 'path';
17
+
18
+ /** Absolute path to the vendored-runtime directory, relative to the bundle. */
19
+ export function vendorDir(): string {
20
+ return path.join(__dirname, '..', 'vendor');
21
+ }
22
+
23
+ /** Read a vendored asset (e.g. "mermaid.min.js"); returns null if absent. */
24
+ export function readVendorAsset(name: string): string | null {
25
+ const p = path.join(vendorDir(), name);
26
+ try {
27
+ return fs.readFileSync(p, 'utf-8');
28
+ } catch {
29
+ return null;
30
+ }
31
+ }
32
+
33
+ /** Copy a vendored asset into `outDir/assets/<name>`. Returns the relative path
34
+ * written (e.g. "assets/mermaid.min.js"), or null if the asset is unavailable. */
35
+ export function writeVendorAsset(outDir: string, name: string): string | null {
36
+ const content = readVendorAsset(name);
37
+ if (content == null) return null;
38
+ const assetsDir = path.join(outDir, 'assets');
39
+ fs.mkdirSync(assetsDir, { recursive: true });
40
+ // LF-normalize for determinism (NFR62), consistent with writeHtml.
41
+ fs.writeFileSync(path.join(assetsDir, name), content.replace(/\r\n/g, '\n'), { encoding: 'utf-8' });
42
+ return `assets/${name}`;
43
+ }