ma-agents 3.15.0-beta.1 → 3.15.0-beta.2

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.0-beta.1",
40
+ "version": "3.15.0-beta.2",
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 path11 = __toESM(require("path"));
2008
+ var path12 = __toESM(require("path"));
2009
2009
 
2010
2010
  // lib/knowledge-atlas/src/generate.ts
2011
2011
  var fs11 = __toESM(require("fs"));
2012
- var path10 = __toESM(require("path"));
2012
+ var path11 = __toESM(require("path"));
2013
2013
 
2014
2014
  // lib/knowledge-atlas/src/scan/scanner.ts
2015
2015
  var fs2 = __toESM(require("fs"));
@@ -5635,7 +5635,7 @@ function mergeRecords(unchanged, reprocessed, removed) {
5635
5635
 
5636
5636
  // lib/knowledge-atlas/src/render/site.ts
5637
5637
  var fs7 = __toESM(require("fs"));
5638
- var path6 = __toESM(require("path"));
5638
+ var path7 = __toESM(require("path"));
5639
5639
 
5640
5640
  // lib/knowledge-atlas/src/render/design-system.ts
5641
5641
  var DESIGN_SYSTEM_CSS = `/* Knowledge Atlas \u2014 design system (Story 29.3) */
@@ -6301,7 +6301,7 @@ ${scripts.join("\n")}` : "";
6301
6301
 
6302
6302
  // lib/knowledge-atlas/src/render/document.ts
6303
6303
  var fs6 = __toESM(require("fs"));
6304
- var path5 = __toESM(require("path"));
6304
+ var path6 = __toESM(require("path"));
6305
6305
 
6306
6306
  // node_modules/markdown-it/lib/common/utils.mjs
6307
6307
  var utils_exports = {};
@@ -11694,6 +11694,7 @@ function renderBacklinksHtml(backlinks, manifest, fromRel = "docs/page.html") {
11694
11694
  }
11695
11695
 
11696
11696
  // lib/knowledge-atlas/src/render/linkify.ts
11697
+ var path5 = __toESM(require("path"));
11697
11698
  var ENTITY_PATTERNS = [
11698
11699
  /\bNFR\d+\b/g,
11699
11700
  // NFR62, NFR12, etc.
@@ -11822,6 +11823,63 @@ function linkifyText(text2, manifest, currentFileId, docFileIds, slugMap) {
11822
11823
  result += text2.slice(last);
11823
11824
  return result;
11824
11825
  }
11826
+ function decodeHrefEntities(value) {
11827
+ return value.replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#0?39;/g, "'");
11828
+ }
11829
+ function encodeHrefForAttr(value) {
11830
+ return value.replace(/&/g, "&amp;");
11831
+ }
11832
+ function resolveAuthoredHref(href, currentFileId, fileIdLookup, slugMap) {
11833
+ const trimmed = href.trim();
11834
+ if (trimmed === "") return null;
11835
+ if (/^(?:[a-zA-Z][a-zA-Z0-9+.-]*:|#|\/\/)/.test(trimmed)) return null;
11836
+ if (trimmed.startsWith("/")) return null;
11837
+ const hashIdx = trimmed.indexOf("#");
11838
+ const rawPath = hashIdx === -1 ? trimmed : trimmed.slice(0, hashIdx);
11839
+ const fragment = hashIdx === -1 ? "" : trimmed.slice(hashIdx);
11840
+ if (rawPath === "") return null;
11841
+ let decodedPath;
11842
+ try {
11843
+ decodedPath = decodeURIComponent(rawPath);
11844
+ } catch {
11845
+ decodedPath = rawPath;
11846
+ }
11847
+ const currentDir = path5.posix.dirname(currentFileId);
11848
+ const joined = currentDir === "." ? decodedPath : `${currentDir}/${decodedPath}`;
11849
+ const resolved = path5.posix.normalize(joined).replace(/\/+$/, "");
11850
+ if (resolved === ".." || resolved.startsWith("../")) return null;
11851
+ const matchedFileId = fileIdLookup.get(resolved.toLowerCase());
11852
+ if (!matchedFileId) return null;
11853
+ const targetSlug = slugMap.get(matchedFileId);
11854
+ if (!targetSlug) return null;
11855
+ if (matchedFileId === currentFileId) {
11856
+ return fragment || "#";
11857
+ }
11858
+ return `${targetSlug}.html${fragment}`;
11859
+ }
11860
+ function rewriteAuthoredLinks(html, manifest, currentFileId) {
11861
+ if (!html) return html;
11862
+ const fileIds = manifest.files.map((f) => f.path);
11863
+ if (fileIds.length === 0) return html;
11864
+ const slugMap = buildSlugMap(fileIds);
11865
+ const fileIdLookup = /* @__PURE__ */ new Map();
11866
+ for (const id of fileIds) {
11867
+ const key = id.toLowerCase();
11868
+ if (!fileIdLookup.has(key)) fileIdLookup.set(key, id);
11869
+ }
11870
+ return html.replace(/<a\b[^>]*>/gi, (tag) => {
11871
+ const hrefMatch = tag.match(/\bhref\s*=\s*(?:"([^"]*)"|'([^']*)')/i);
11872
+ if (!hrefMatch) return tag;
11873
+ const usesDoubleQuote = hrefMatch[1] !== void 0;
11874
+ const rawAttrValue = usesDoubleQuote ? hrefMatch[1] : hrefMatch[2];
11875
+ const decoded = decodeHrefEntities(rawAttrValue);
11876
+ const rewritten = resolveAuthoredHref(decoded, currentFileId, fileIdLookup, slugMap);
11877
+ if (rewritten === null) return tag;
11878
+ const quote = usesDoubleQuote ? '"' : "'";
11879
+ const newAttr = `href=${quote}${encodeHrefForAttr(rewritten)}${quote}`;
11880
+ return tag.slice(0, hrefMatch.index) + newAttr + tag.slice(hrefMatch.index + hrefMatch[0].length);
11881
+ });
11882
+ }
11825
11883
  var ALLOWED_TAGS = /* @__PURE__ */ new Set([
11826
11884
  "p",
11827
11885
  "a",
@@ -12053,7 +12111,7 @@ function getFileNodes(fileId, nodes) {
12053
12111
  return nodes.filter((n) => n.fileId === fileId);
12054
12112
  }
12055
12113
  function renderDocumentPage(fileRec, manifest, projectRoot, navFlags, resolveAbsPath) {
12056
- const absPath = resolveAbsPath?.(fileRec.path) ?? path5.join(projectRoot, fileRec.path);
12114
+ const absPath = resolveAbsPath?.(fileRec.path) ?? path6.join(projectRoot, fileRec.path);
12057
12115
  const title = fileTitle(fileRec.path);
12058
12116
  let rawContent = "";
12059
12117
  try {
@@ -12077,6 +12135,7 @@ function renderDocumentPage(fileRec, manifest, projectRoot, navFlags, resolveAbs
12077
12135
  renderedHtml = addHeadingAnchors(renderedHtml);
12078
12136
  const { html: mermaidHtml } = processMermaidFences(renderedHtml);
12079
12137
  renderedHtml = mermaidHtml;
12138
+ renderedHtml = rewriteAuthoredLinks(renderedHtml, manifest, fileRec.path);
12080
12139
  renderedHtml = linkifyHtml(renderedHtml, manifest, fileRec.path);
12081
12140
  renderedHtml = sanitizeHtml(renderedHtml);
12082
12141
  const fileIds = manifest.files.map((f) => f.path);
@@ -12334,7 +12393,7 @@ function esc6(s) {
12334
12393
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
12335
12394
  }
12336
12395
  function writeHtml(filePath, content) {
12337
- fs7.mkdirSync(path6.dirname(filePath), { recursive: true });
12396
+ fs7.mkdirSync(path7.dirname(filePath), { recursive: true });
12338
12397
  fs7.writeFileSync(filePath, content.replace(/\r\n/g, "\n"), { encoding: "utf-8" });
12339
12398
  }
12340
12399
  function renderHomePage(manifest, navFlags) {
@@ -12473,34 +12532,34 @@ function renderSite(manifest, outDir, resolveAbsPath) {
12473
12532
  const written = [];
12474
12533
  const docBodies = /* @__PURE__ */ new Map();
12475
12534
  fs7.mkdirSync(outDir, { recursive: true });
12476
- fs7.mkdirSync(path6.join(outDir, "docs"), { recursive: true });
12535
+ fs7.mkdirSync(path7.join(outDir, "docs"), { recursive: true });
12477
12536
  const fileIds = manifest.files.map((f) => f.path);
12478
12537
  const slugMap = buildSlugMap(fileIds);
12479
12538
  const navFlags = {
12480
- hasGraphPage: fs7.existsSync(path6.join(outDir, "graph.html")),
12481
- hasDiagramsPage: fs7.existsSync(path6.join(outDir, "diagrams.html")),
12482
- hasOrphansPage: fs7.existsSync(path6.join(outDir, "orphans.html"))
12539
+ hasGraphPage: fs7.existsSync(path7.join(outDir, "graph.html")),
12540
+ hasDiagramsPage: fs7.existsSync(path7.join(outDir, "diagrams.html")),
12541
+ hasOrphansPage: fs7.existsSync(path7.join(outDir, "orphans.html"))
12483
12542
  };
12484
12543
  for (const file of manifest.files) {
12485
12544
  const slug = slugMap.get(file.path) ?? filePathToSlug(file.path);
12486
12545
  const { html, articleText } = renderDocumentPage(file, manifest, manifest.projectRoot, navFlags, resolveAbsPath);
12487
12546
  const relPath = `docs/${slug}.html`;
12488
- writeHtml(path6.join(outDir, relPath), html);
12547
+ writeHtml(path7.join(outDir, relPath), html);
12489
12548
  written.push(relPath);
12490
12549
  docBodies.set(file.path, articleText);
12491
12550
  }
12492
12551
  const searchHtml = renderSearchPageHtml(manifest, docBodies, navFlags);
12493
- writeHtml(path6.join(outDir, "search.html"), searchHtml);
12552
+ writeHtml(path7.join(outDir, "search.html"), searchHtml);
12494
12553
  written.push("search.html");
12495
12554
  const indexHtml = renderHomePage(manifest, navFlags);
12496
- writeHtml(path6.join(outDir, "index.html"), indexHtml);
12555
+ writeHtml(path7.join(outDir, "index.html"), indexHtml);
12497
12556
  written.push("index.html");
12498
12557
  return written.sort();
12499
12558
  }
12500
12559
 
12501
12560
  // lib/knowledge-atlas/src/render/graph.ts
12502
12561
  var fs8 = __toESM(require("fs"));
12503
- var path7 = __toESM(require("path"));
12562
+ var path8 = __toESM(require("path"));
12504
12563
 
12505
12564
  // lib/knowledge-atlas/src/render/force-graph-umd.ts
12506
12565
  var FORCE_GRAPH_UMD = `// Version 1.51.4 force-graph - https://github.com/vasturiano/force-graph
@@ -12963,7 +13022,7 @@ function renderGraphPage(manifest, outDir) {
12963
13022
  ""
12964
13023
  // trailing newline
12965
13024
  ].join("\n");
12966
- const outPath = path7.join(outDir, "graph.html");
13025
+ const outPath = path8.join(outDir, "graph.html");
12967
13026
  fs8.mkdirSync(outDir, { recursive: true });
12968
13027
  fs8.writeFileSync(outPath, html.replace(/\r\n/g, "\n"), { encoding: "utf-8" });
12969
13028
  return ["graph.html"];
@@ -13217,7 +13276,7 @@ function buildAppScript() {
13217
13276
 
13218
13277
  // lib/knowledge-atlas/src/render/diagrams.ts
13219
13278
  var fs9 = __toESM(require("fs"));
13220
- var path8 = __toESM(require("path"));
13279
+ var path9 = __toESM(require("path"));
13221
13280
  function esc8(s) {
13222
13281
  return (s ?? "").toString().replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
13223
13282
  }
@@ -13784,7 +13843,7 @@ function renderDiagrams(manifest, outDir) {
13784
13843
  ""
13785
13844
  // trailing newline
13786
13845
  ].join("\n");
13787
- const outPath = path8.join(outDir, "diagrams.html");
13846
+ const outPath = path9.join(outDir, "diagrams.html");
13788
13847
  fs9.mkdirSync(outDir, { recursive: true });
13789
13848
  fs9.writeFileSync(outPath, html.replace(/\r\n/g, "\n"), { encoding: "utf-8" });
13790
13849
  return ["diagrams.html"];
@@ -13792,7 +13851,7 @@ function renderDiagrams(manifest, outDir) {
13792
13851
 
13793
13852
  // lib/knowledge-atlas/src/render/orphan.ts
13794
13853
  var fs10 = __toESM(require("fs"));
13795
- var path9 = __toESM(require("path"));
13854
+ var path10 = __toESM(require("path"));
13796
13855
  function esc9(s) {
13797
13856
  return (s ?? "").toString().replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
13798
13857
  }
@@ -14154,7 +14213,7 @@ function renderOrphanPage(manifest, outDir) {
14154
14213
  ""
14155
14214
  // trailing newline
14156
14215
  ].join("\n");
14157
- const outPath = path9.join(outDir, "orphans.html");
14216
+ const outPath = path10.join(outDir, "orphans.html");
14158
14217
  fs10.mkdirSync(outDir, { recursive: true });
14159
14218
  fs10.writeFileSync(outPath, html.replace(/\r\n/g, "\n"), { encoding: "utf-8" });
14160
14219
  return ["orphans.html"];
@@ -14163,7 +14222,7 @@ function renderOrphanPage(manifest, outDir) {
14163
14222
  // lib/knowledge-atlas/src/generate.ts
14164
14223
  var MANIFEST_VERSION = 2;
14165
14224
  function normPath(p) {
14166
- return path10.resolve(p).replace(/[\\/]+$/, "");
14225
+ return path11.resolve(p).replace(/[\\/]+$/, "");
14167
14226
  }
14168
14227
  function pathsEqual(a, b) {
14169
14228
  return normPath(a) === normPath(b);
@@ -14171,7 +14230,7 @@ function pathsEqual(a, b) {
14171
14230
  function isUnder(parent, child) {
14172
14231
  const p = normPath(parent);
14173
14232
  const c = normPath(child);
14174
- return c.startsWith(p + path10.sep);
14233
+ return c.startsWith(p + path11.sep);
14175
14234
  }
14176
14235
  var MAX_TIME_VALUE_MS = 864e13;
14177
14236
  function resolveTimestamp(now) {
@@ -14212,7 +14271,7 @@ function isoSecond(d) {
14212
14271
  return d.toISOString().replace(/\.\d{3}Z$/, "Z");
14213
14272
  }
14214
14273
  function deriveProjectTitle(projectRoot) {
14215
- const pkgPath = path10.join(projectRoot, "package.json");
14274
+ const pkgPath = path11.join(projectRoot, "package.json");
14216
14275
  try {
14217
14276
  if (fs11.existsSync(pkgPath)) {
14218
14277
  const raw = fs11.readFileSync(pkgPath, "utf-8");
@@ -14223,7 +14282,7 @@ function deriveProjectTitle(projectRoot) {
14223
14282
  }
14224
14283
  } catch {
14225
14284
  }
14226
- const base = path10.basename(projectRoot).trim();
14285
+ const base = path11.basename(projectRoot).trim();
14227
14286
  return base.length > 0 ? base : "Unknown Project";
14228
14287
  }
14229
14288
  function extractFileRecord(absPath, fileId, docType, contentHash2) {
@@ -14285,7 +14344,7 @@ function generate(opts) {
14285
14344
  if (!projectRoot || typeof projectRoot !== "string") {
14286
14345
  throw new Error("generate: projectRoot is required and must be a non-empty string");
14287
14346
  }
14288
- if (!path10.isAbsolute(projectRoot)) {
14347
+ if (!path11.isAbsolute(projectRoot)) {
14289
14348
  throw new Error(`generate: projectRoot must be an absolute path (got: ${projectRoot})`);
14290
14349
  }
14291
14350
  const rootStat = fs11.statSync(projectRoot, { throwIfNoEntry: false });
@@ -14293,11 +14352,11 @@ function generate(opts) {
14293
14352
  throw new Error(`generate: projectRoot does not exist or is not a directory: ${projectRoot}`);
14294
14353
  }
14295
14354
  const { sources, outputFolder, outputOverlap } = resolveKnowledgeSources(projectRoot);
14296
- const bmadOutputDir = path10.join(projectRoot, outputFolder);
14297
- const defaultOutDir = path10.join(bmadOutputDir, "knowledge-site");
14355
+ const bmadOutputDir = path11.join(projectRoot, outputFolder);
14356
+ const defaultOutDir = path11.join(bmadOutputDir, "knowledge-site");
14298
14357
  const outDir = outDirOverride ?? defaultOutDir;
14299
- const rel = path10.relative(projectRoot, outDir);
14300
- if (rel.startsWith("..") || path10.isAbsolute(rel)) {
14358
+ const rel = path11.relative(projectRoot, outDir);
14359
+ if (rel.startsWith("..") || path11.isAbsolute(rel)) {
14301
14360
  throw new Error(`generate: outDir escapes projectRoot: ${outDir}`);
14302
14361
  }
14303
14362
  const generatedAt = resolveTimestamp(opts.now);
@@ -14332,7 +14391,7 @@ function generate(opts) {
14332
14391
  const extraFileCount = graphFiles.length + diagramFiles.length + orphanFiles.length;
14333
14392
  const written = renderSite(manifest, outDir, resolveAbsPath);
14334
14393
  writeManifest(outDir, manifest);
14335
- const indexPath = path10.join(outDir, "index.html");
14394
+ const indexPath = path11.join(outDir, "index.html");
14336
14395
  return {
14337
14396
  outDir,
14338
14397
  indexPath,
@@ -14353,7 +14412,7 @@ function main() {
14353
14412
  usage();
14354
14413
  process.exit(args.length === 0 ? 1 : 0);
14355
14414
  }
14356
- const projectRoot = path11.resolve(args[0]);
14415
+ const projectRoot = path12.resolve(args[0]);
14357
14416
  let outDir;
14358
14417
  const outIdx = args.indexOf("--out");
14359
14418
  if (outIdx !== -1) {
@@ -14361,7 +14420,7 @@ function main() {
14361
14420
  process.stderr.write("[atlas] ERROR: --out requires a directory argument\n");
14362
14421
  process.exit(1);
14363
14422
  }
14364
- outDir = path11.resolve(args[outIdx + 1]);
14423
+ outDir = path12.resolve(args[outIdx + 1]);
14365
14424
  }
14366
14425
  try {
14367
14426
  const result = generate({ projectRoot, outDir });
@@ -36,7 +36,7 @@ import { headingToSlug } from '../extract/slug.js';
36
36
  import { pageShell } from './layout.js';
37
37
  import type { NavFlags } from './layout.js';
38
38
  import { gatherBacklinks, renderBacklinksHtml } from './backlinks.js';
39
- import { linkifyHtml, sanitizeHtml } from './linkify.js';
39
+ import { linkifyHtml, sanitizeHtml, rewriteAuthoredLinks } from './linkify.js';
40
40
  import { resolveSlug } from './paths.js';
41
41
  import type { FileRecord, KnowledgeManifest, GraphNode } from '../types.js';
42
42
 
@@ -262,6 +262,12 @@ export function renderDocumentPage(
262
262
  const { html: mermaidHtml } = processMermaidFences(renderedHtml);
263
263
  renderedHtml = mermaidHtml;
264
264
 
265
+ // Rewrite AUTHORED relative markdown cross-links (e.g. `[x](roadmap.md)`)
266
+ // to the RENDERED sibling doc page — bug fix (see render/linkify.ts for
267
+ // the full writeup). Must run before linkifyHtml/sanitizeHtml so the
268
+ // resolved href is what the safe-URL allowlist evaluates.
269
+ renderedHtml = rewriteAuthoredLinks(renderedHtml, manifest, fileRec.path);
270
+
265
271
  // Linkify entity tokens (now that HTML structure is stable)
266
272
  renderedHtml = linkifyHtml(renderedHtml, manifest, fileRec.path);
267
273
 
@@ -8,6 +8,7 @@
8
8
  * sanitizeHtml() — strip dangerous HTML from rendered markdown output
9
9
  */
10
10
 
11
+ import * as path from 'path';
11
12
  import type { KnowledgeManifest } from '../types.js';
12
13
  import { filePathToSlug, buildSlugMap } from './paths.js';
13
14
 
@@ -240,6 +241,173 @@ function linkifyText(
240
241
  return result;
241
242
  }
242
243
 
244
+ // ---------------------------------------------------------------------------
245
+ // rewriteAuthoredLinks — bug fix: authored relative `.md` cross-links
246
+ // ---------------------------------------------------------------------------
247
+ //
248
+ // CONFIRMED BUG (real Prophesys project, 18 broken links): reading-view docs
249
+ // contain AUTHORED markdown cross-links to sibling source docs, e.g. in
250
+ // `prd.md`: `[roadmap](roadmap.md#7-development-timetable)`. markdown-it
251
+ // renders these as plain `<a href="roadmap.md#...">` and `linkifyHtml` above
252
+ // only rewrites bare ENTITY TOKENS (FR26, Story 1.2, …) found in TEXT nodes —
253
+ // it never touches an href that already lives inside an authored `<a>` tag.
254
+ // Every reading view is written FLAT at `docs/<slug>.html` (render/paths.ts),
255
+ // so from `docs/prd.html` a raw `href="roadmap.md"` resolves to the
256
+ // non-existent `docs/roadmap.md` instead of the real `docs/roadmap.html`.
257
+ //
258
+ // This pass resolves the authored href against the SOURCE document's
259
+ // directory (not the rendered docs/ directory), looks the resolved path up
260
+ // in the SAME collision-free slug map every other renderer uses, and — only
261
+ // when it matches an actual scanned document — rewrites it to the sibling
262
+ // docs/-relative page. Anything that doesn't resolve to a scanned document
263
+ // (external URL, mailto:, in-page anchor, or a genuinely non-scanned file) is
264
+ // left byte-for-byte untouched: this pass never invents a target.
265
+
266
+ /**
267
+ * Decode the HTML character references markdown-it's link renderer can
268
+ * introduce into an href attribute value (notably `&amp;` for a literal `&`
269
+ * in the authored URL). Narrow by design — just enough to recover the
270
+ * original authored URL text for filesystem-path resolution; the decoded
271
+ * scheme/path is never emitted as-is (only a freshly-built `<slug>.html`
272
+ * href is written back), so there is no injection surface here.
273
+ */
274
+ function decodeHrefEntities(value: string): string {
275
+ return value
276
+ .replace(/&amp;/g, '&')
277
+ .replace(/&lt;/g, '<')
278
+ .replace(/&gt;/g, '>')
279
+ .replace(/&quot;/g, '"')
280
+ .replace(/&#0?39;/g, '\'');
281
+ }
282
+
283
+ /** Re-escape `&` for safe embedding back into an HTML attribute value. */
284
+ function encodeHrefForAttr(value: string): string {
285
+ return value.replace(/&/g, '&amp;');
286
+ }
287
+
288
+ /**
289
+ * Resolve a single authored href (already HTML-entity-decoded) against the
290
+ * SOURCE document's directory. Returns the rewritten docs/-relative href
291
+ * (`<slug>.html[#fragment]`, since every doc page lives flat under `docs/`)
292
+ * or `null` when the href should be left untouched:
293
+ * - external / scheme'd links (`http:`, `mailto:`, `javascript:`, …),
294
+ * - protocol-relative (`//host/...`) and in-page (`#frag`) links,
295
+ * - site-root-absolute paths (`/x`) — not this bug's shape,
296
+ * - anything that normalizes outside the scanned tree (`../../../etc`),
297
+ * - anything that does not resolve (case-insensitively) to a fileId
298
+ * actually present in the manifest — a genuinely non-scanned target;
299
+ * never invent a link for it.
300
+ */
301
+ function resolveAuthoredHref(
302
+ href: string,
303
+ currentFileId: string,
304
+ fileIdLookup: Map<string, string>,
305
+ slugMap: Map<string, string>,
306
+ ): string | null {
307
+ const trimmed = href.trim();
308
+ if (trimmed === '') return null;
309
+
310
+ // External schemes (http:, https:, mailto:, tel:, javascript:, vbscript:,
311
+ // data:, ...), in-page fragments, and protocol-relative URLs are never
312
+ // authored-doc cross-links — leave them exactly as authored/sanitized.
313
+ if (/^(?:[a-zA-Z][a-zA-Z0-9+.-]*:|#|\/\/)/.test(trimmed)) return null;
314
+ // Site-root-absolute path — not the relative-link shape this bug covers.
315
+ if (trimmed.startsWith('/')) return null;
316
+
317
+ const hashIdx = trimmed.indexOf('#');
318
+ const rawPath = hashIdx === -1 ? trimmed : trimmed.slice(0, hashIdx);
319
+ const fragment = hashIdx === -1 ? '' : trimmed.slice(hashIdx);
320
+ if (rawPath === '') return null; // defensive: pure-fragment already caught above
321
+
322
+ // URL-decode so `%20` (space), `%2E` etc. match the real on-disk filename.
323
+ let decodedPath: string;
324
+ try {
325
+ decodedPath = decodeURIComponent(rawPath);
326
+ } catch {
327
+ decodedPath = rawPath; // malformed percent-escape — fall back to raw text
328
+ }
329
+
330
+ // Resolve RELATIVE TO THE SOURCE DOC'S DIRECTORY (the authored-link base),
331
+ // never the rendered docs/ directory the page happens to live under.
332
+ const currentDir = path.posix.dirname(currentFileId);
333
+ const joined = currentDir === '.' ? decodedPath : `${currentDir}/${decodedPath}`;
334
+ const resolved = path.posix.normalize(joined).replace(/\/+$/, '');
335
+
336
+ // Escaped above the scanned root — cannot be a scanned fileId.
337
+ if (resolved === '..' || resolved.startsWith('../')) return null;
338
+
339
+ // Case-insensitive match (`.md` vs `.MD`, mixed-case filenames) against the
340
+ // ACTUAL scanned fileIds — the same identity space every other renderer
341
+ // (linkifyHtml, backlinks, search) resolves against.
342
+ const matchedFileId = fileIdLookup.get(resolved.toLowerCase());
343
+ if (!matchedFileId) return null; // not a scanned document — leave unchanged
344
+
345
+ const targetSlug = slugMap.get(matchedFileId);
346
+ if (!targetSlug) return null;
347
+
348
+ // Self-link (authored link back to its own file): collapse to an in-page
349
+ // anchor, mirroring resolveHref()'s same-page handling above, instead of a
350
+ // full-page href to the page it's already on.
351
+ if (matchedFileId === currentFileId) {
352
+ return fragment || '#';
353
+ }
354
+
355
+ return `${targetSlug}.html${fragment}`;
356
+ }
357
+
358
+ /**
359
+ * Rewrite AUTHORED relative markdown cross-links — `[x](roadmap.md)`,
360
+ * `[x](sub/y.md#anchor)`, `[x](../z.MD)`, spaces/`%20`, mixed-case
361
+ * `.md`/`.MD` — so they point at the RENDERED sibling doc page instead of
362
+ * the raw source-relative path markdown-it passed through verbatim.
363
+ *
364
+ * Only touches `href` attributes on `<a>` tags already present in `html`
365
+ * (i.e. AUTHORED links); it never invents new links from plain text — that
366
+ * remains `linkifyHtml`'s job. Must run BEFORE `sanitizeHtml` so the
367
+ * rewritten href is what the safe-URL allowlist evaluates (a relative
368
+ * `<slug>.html[#frag]` href always passes it, matching prior behavior for
369
+ * already-relative authored links — no new unsafe-URL surface is created).
370
+ * Order relative to `linkifyHtml` does not matter: that pass only touches
371
+ * bare text nodes, never existing `<a>` attributes.
372
+ */
373
+ export function rewriteAuthoredLinks(
374
+ html: string,
375
+ manifest: KnowledgeManifest,
376
+ currentFileId: string,
377
+ ): string {
378
+ if (!html) return html;
379
+
380
+ const fileIds = manifest.files.map(f => f.path);
381
+ if (fileIds.length === 0) return html;
382
+
383
+ const slugMap = buildSlugMap(fileIds);
384
+
385
+ // Case-insensitive fileId lookup. First-encountered (sorted-order) wins on
386
+ // a case-fold collision — deterministic, matching the scanner's own
387
+ // first-wins dedup semantics.
388
+ const fileIdLookup = new Map<string, string>();
389
+ for (const id of fileIds) {
390
+ const key = id.toLowerCase();
391
+ if (!fileIdLookup.has(key)) fileIdLookup.set(key, id);
392
+ }
393
+
394
+ return html.replace(/<a\b[^>]*>/gi, (tag) => {
395
+ const hrefMatch = tag.match(/\bhref\s*=\s*(?:"([^"]*)"|'([^']*)')/i);
396
+ if (!hrefMatch) return tag;
397
+
398
+ const usesDoubleQuote = hrefMatch[1] !== undefined;
399
+ const rawAttrValue = usesDoubleQuote ? hrefMatch[1]! : hrefMatch[2]!;
400
+ const decoded = decodeHrefEntities(rawAttrValue);
401
+
402
+ const rewritten = resolveAuthoredHref(decoded, currentFileId, fileIdLookup, slugMap);
403
+ if (rewritten === null) return tag;
404
+
405
+ const quote = usesDoubleQuote ? '"' : '\'';
406
+ const newAttr = `href=${quote}${encodeHrefForAttr(rewritten)}${quote}`;
407
+ return tag.slice(0, hrefMatch.index!) + newAttr + tag.slice(hrefMatch.index! + hrefMatch[0].length);
408
+ });
409
+ }
410
+
243
411
  // ---------------------------------------------------------------------------
244
412
  // sanitizeHtml
245
413
  // ---------------------------------------------------------------------------
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ma-agents",
3
- "version": "3.15.0-beta.1",
3
+ "version": "3.15.0-beta.2",
4
4
  "description": "NPX tool to install skills for AI coding agents (Claude Code, Gemini, Copilot, Kilocode, Cline, Cursor, Roo Code)",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -1,21 +1,21 @@
1
- {
2
- "name": "Git Workflow",
3
- "description": "MANDATORY worktree-based workflow for ALL file-changing activities. Enforces isolated feature branches, conventional commits, and PR-based merging.",
4
- "version": "2.1.0",
5
- "author": "AI Agent Skills",
6
- "tags": [
7
- "git",
8
- "worktrees",
9
- "workflow",
10
- "branching",
11
- "conventional-commits",
12
- "pull-requests",
13
- "multi-agent"
14
- ],
15
- "applies_when": [
16
- "committing changes",
17
- "creating branches or PRs",
18
- "any code writing or modification task"
19
- ],
20
- "always_load": true
21
- }
1
+ {
2
+ "name": "Git Workflow",
3
+ "description": "MANDATORY worktree-based workflow for ALL file-changing activities. Enforces isolated feature branches, conventional commits, and PR-based merging.",
4
+ "version": "2.1.0",
5
+ "author": "AI Agent Skills",
6
+ "tags": [
7
+ "git",
8
+ "worktrees",
9
+ "workflow",
10
+ "branching",
11
+ "conventional-commits",
12
+ "pull-requests",
13
+ "multi-agent"
14
+ ],
15
+ "applies_when": [
16
+ "committing changes",
17
+ "creating branches or PRs",
18
+ "any code writing or modification task"
19
+ ],
20
+ "always_load": true
21
+ }