ma-agents 3.15.1 → 3.15.3

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.1",
40
+ "version": "3.15.3",
41
41
  "author": {
42
42
  "name": "Alon Mayaffit"
43
43
  },
@@ -2106,7 +2106,9 @@ function classifyFile(fileId) {
2106
2106
  if (/^\d+-\d+-[a-z0-9][a-z0-9-]*\.md$/.test(basename4)) return "story";
2107
2107
  if (/product-brief/.test(basename4)) return "product-brief";
2108
2108
  if (lowerFileId.includes("/briefs/")) return "brief";
2109
- if (/prd/.test(basename4) || /product-requirements/.test(basename4)) return "prd";
2109
+ if ((/prd/.test(basename4) || /product-requirements/.test(basename4)) && !/(validation|report|checklist|review|cohesion|assessment)/.test(basename4)) {
2110
+ return "prd";
2111
+ }
2110
2112
  if (/architecture/.test(basename4)) return "architecture";
2111
2113
  if (/epic/.test(basename4)) return "epics";
2112
2114
  if (basename4.endsWith(".md") || basename4.endsWith(".yaml") || basename4.endsWith(".yml")) {
@@ -12154,14 +12156,23 @@ function renderDocumentPage(fileRec, manifest, projectRoot, navFlags, resolveAbs
12154
12156
  const statusBadgesHtml = renderStatusBadges(fileRec.statuses);
12155
12157
  const existingIds = /* @__PURE__ */ new Set();
12156
12158
  for (const m of renderedHtml.matchAll(/\sid="([^"]+)"/g)) existingIds.add(m[1]);
12157
- const anchorTargets = [];
12159
+ const escRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
12160
+ const inlinePlacements = [];
12161
+ const fallbackAnchors = [];
12158
12162
  for (const node of fileNodes) {
12159
- if (node.anchor && !existingIds.has(node.anchor)) {
12160
- anchorTargets.push(`<span class="entity-anchor" id="${esc4(node.anchor)}"></span>`);
12161
- existingIds.add(node.anchor);
12162
- }
12163
- }
12164
- const anchorTargetsHtml = anchorTargets.join("\n");
12163
+ const a = node.anchor;
12164
+ if (!a || existingIds.has(a)) continue;
12165
+ existingIds.add(a);
12166
+ const m = new RegExp('<a\\b[^>]*href="[^"]*#' + escRe(a) + '"', "i").exec(renderedHtml);
12167
+ if (m) inlinePlacements.push({ idx: m.index, anchor: a });
12168
+ else fallbackAnchors.push(a);
12169
+ }
12170
+ inlinePlacements.sort((x, y) => y.idx - x.idx);
12171
+ for (const p of inlinePlacements) {
12172
+ const span = `<span class="entity-anchor" id="${esc4(p.anchor)}"></span>`;
12173
+ renderedHtml = renderedHtml.slice(0, p.idx) + span + renderedHtml.slice(p.idx);
12174
+ }
12175
+ const anchorTargetsHtml = fallbackAnchors.map((a) => `<span class="entity-anchor" id="${esc4(a)}"></span>`).join("\n");
12165
12176
  const backLinkHtml = '<a class="back-link" href="../index.html">&larr; Back to Home</a>';
12166
12177
  const pageBody = [
12167
12178
  '<div class="content-wrap">',
@@ -13061,6 +13072,7 @@ function buildAppScript() {
13061
13072
  // mouse (finding #8: tooltip was shown with display:block but no left/top,
13062
13073
  // so it rendered at the bottom of <main>).
13063
13074
  var lastHoverNode = null;
13075
+ var selectedLink = null; // edge highlighted on click: drawn bold + its two endpoints ringed
13064
13076
 
13065
13077
  function getFilteredData() {
13066
13078
  var nodes = allNodes;
@@ -13183,6 +13195,18 @@ function buildAppScript() {
13183
13195
  ctx.fillStyle = n.color || '#888';
13184
13196
  traceNodeShapePath(ctx, n.shape, n.x, n.y, size);
13185
13197
  ctx.fill();
13198
+ // Ring the two endpoints of the selected edge so both ends are easy to
13199
+ // find, however far apart they sit in the layout.
13200
+ if (selectedLink) {
13201
+ var s = selectedLink.source, t = selectedLink.target;
13202
+ if (n === s || n === t) {
13203
+ ctx.beginPath();
13204
+ ctx.arc(n.x, n.y, size + 3.5, 0, 2 * Math.PI);
13205
+ ctx.strokeStyle = '#111827';
13206
+ ctx.lineWidth = 1.8;
13207
+ ctx.stroke();
13208
+ }
13209
+ }
13186
13210
  })
13187
13211
  // Matching hit-test paint: without this, force-graph's click/hover
13188
13212
  // detection falls back to a generic area that no longer matches the
@@ -13196,12 +13220,30 @@ function buildAppScript() {
13196
13220
  })
13197
13221
  .linkSource('source')
13198
13222
  .linkTarget('target')
13199
- .linkColor(function(e) { return e.color || '#aaa'; })
13223
+ // Selected edge is drawn dark + thick so you can trace it end to end.
13224
+ .linkColor(function(e) { return e === selectedLink ? '#111827' : (e.color || '#aaa'); })
13225
+ .linkWidth(function(e) { return e === selectedLink ? 3.5 : (e.dashed ? 0.5 : 1); })
13200
13226
  // Story 29.8 - dashed stroke for weak references edges; solid otherwise.
13201
13227
  .linkLineDash(function(e) { return e.dashed ? [4, 3] : null; })
13202
- .linkDirectionalArrowLength(4)
13228
+ .linkDirectionalArrowLength(function(e) { return e === selectedLink ? 7 : 4; })
13203
13229
  .linkDirectionalArrowRelPos(1)
13204
13230
  .linkCurvature(0.1)
13231
+ // Click an edge to highlight it (bold + ringed endpoints); click it again
13232
+ // or click empty space to clear. Re-applying the accessors forces an
13233
+ // immediate repaint of the new selection state.
13234
+ .onLinkClick(function(e) {
13235
+ selectedLink = (selectedLink === e) ? null : e;
13236
+ graph.linkWidth(graph.linkWidth()).linkColor(graph.linkColor());
13237
+ })
13238
+ .onLinkHover(function(e) {
13239
+ if (container) container.style.cursor = e ? 'pointer' : 'default';
13240
+ if (!tooltip) return;
13241
+ if (!e) { tooltip.style.display = 'none'; return; }
13242
+ var s = (e.source && e.source.label) || e.source;
13243
+ var t = (e.target && e.target.label) || e.target;
13244
+ tooltip.style.display = 'block';
13245
+ tooltip.textContent = s + ' \u2192 ' + t + (e.kind ? ' [' + e.kind + ']' : '');
13246
+ })
13205
13247
  .onNodeClick(function(n) {
13206
13248
  // Navigate to the reading view via the server-resolved doc page (the
13207
13249
  // 'page' field, e.g. "docs/<slug>.html"), NOT the raw fileId \u2014 the
@@ -13242,6 +13284,10 @@ function buildAppScript() {
13242
13284
  .onBackgroundClick(function() {
13243
13285
  if (tooltip) tooltip.style.display = 'none';
13244
13286
  lastHoverNode = null;
13287
+ if (selectedLink) {
13288
+ selectedLink = null;
13289
+ graph.linkWidth(graph.linkWidth()).linkColor(graph.linkColor());
13290
+ }
13245
13291
  });
13246
13292
  }
13247
13293
 
@@ -299,16 +299,36 @@ export function renderDocumentPage(
299
299
  // to a heading slug (`story-11-oauth2-integration`). We emit an invisible
300
300
  // anchor target for every file node whose anchor is not already a heading id,
301
301
  // so every cross-link `…#anchor` resolves to a real location on the page.
302
+ // Place each entity's anchor at the FIRST place that entity is mentioned in
303
+ // the body (its first linkified occurrence: an <a … href="…#anchor">), so a
304
+ // cross-link or graph-node click lands where the entity is actually discussed.
305
+ // Previously every anchor was emitted as one clump at the top of the article,
306
+ // so every #anchor (e.g. all 9 epics) resolved to the same spot. Anchors with
307
+ // no linked mention fall back to the top clump.
302
308
  const existingIds = new Set<string>();
303
309
  for (const m of renderedHtml.matchAll(/\sid="([^"]+)"/g)) existingIds.add(m[1]!);
304
- const anchorTargets: string[] = [];
310
+ const escRe = (s: string) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
311
+ const inlinePlacements: { idx: number; anchor: string }[] = [];
312
+ const fallbackAnchors: string[] = [];
305
313
  for (const node of fileNodes) {
306
- if (node.anchor && !existingIds.has(node.anchor)) {
307
- anchorTargets.push(`<span class="entity-anchor" id="${esc(node.anchor)}"></span>`);
308
- existingIds.add(node.anchor);
309
- }
314
+ const a = node.anchor;
315
+ if (!a || existingIds.has(a)) continue;
316
+ 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);
319
+ if (m) inlinePlacements.push({ idx: m.index, anchor: a });
320
+ else fallbackAnchors.push(a);
321
+ }
322
+ // Insert the invisible anchor spans just before their target links. Apply from
323
+ // the last position to the first so earlier indices stay valid.
324
+ inlinePlacements.sort((x, y) => y.idx - x.idx);
325
+ for (const p of inlinePlacements) {
326
+ const span = `<span class="entity-anchor" id="${esc(p.anchor)}"></span>`;
327
+ renderedHtml = renderedHtml.slice(0, p.idx) + span + renderedHtml.slice(p.idx);
310
328
  }
311
- const anchorTargetsHtml = anchorTargets.join('\n');
329
+ const anchorTargetsHtml = fallbackAnchors
330
+ .map(a => `<span class="entity-anchor" id="${esc(a)}"></span>`)
331
+ .join('\n');
312
332
 
313
333
  // Back link
314
334
  const backLinkHtml = '<a class="back-link" href="../index.html">&larr; Back to Home</a>';
@@ -648,6 +648,7 @@ function buildAppScript(): string {
648
648
  // mouse (finding #8: tooltip was shown with display:block but no left/top,
649
649
  // so it rendered at the bottom of <main>).
650
650
  var lastHoverNode = null;
651
+ var selectedLink = null; // edge highlighted on click: drawn bold + its two endpoints ringed
651
652
 
652
653
  function getFilteredData() {
653
654
  var nodes = allNodes;
@@ -770,6 +771,18 @@ function buildAppScript(): string {
770
771
  ctx.fillStyle = n.color || '#888';
771
772
  traceNodeShapePath(ctx, n.shape, n.x, n.y, size);
772
773
  ctx.fill();
774
+ // Ring the two endpoints of the selected edge so both ends are easy to
775
+ // find, however far apart they sit in the layout.
776
+ if (selectedLink) {
777
+ var s = selectedLink.source, t = selectedLink.target;
778
+ if (n === s || n === t) {
779
+ ctx.beginPath();
780
+ ctx.arc(n.x, n.y, size + 3.5, 0, 2 * Math.PI);
781
+ ctx.strokeStyle = '#111827';
782
+ ctx.lineWidth = 1.8;
783
+ ctx.stroke();
784
+ }
785
+ }
773
786
  })
774
787
  // Matching hit-test paint: without this, force-graph's click/hover
775
788
  // detection falls back to a generic area that no longer matches the
@@ -783,12 +796,30 @@ function buildAppScript(): string {
783
796
  })
784
797
  .linkSource('source')
785
798
  .linkTarget('target')
786
- .linkColor(function(e) { return e.color || '#aaa'; })
799
+ // Selected edge is drawn dark + thick so you can trace it end to end.
800
+ .linkColor(function(e) { return e === selectedLink ? '#111827' : (e.color || '#aaa'); })
801
+ .linkWidth(function(e) { return e === selectedLink ? 3.5 : (e.dashed ? 0.5 : 1); })
787
802
  // Story 29.8 - dashed stroke for weak references edges; solid otherwise.
788
803
  .linkLineDash(function(e) { return e.dashed ? [4, 3] : null; })
789
- .linkDirectionalArrowLength(4)
804
+ .linkDirectionalArrowLength(function(e) { return e === selectedLink ? 7 : 4; })
790
805
  .linkDirectionalArrowRelPos(1)
791
806
  .linkCurvature(0.1)
807
+ // Click an edge to highlight it (bold + ringed endpoints); click it again
808
+ // or click empty space to clear. Re-applying the accessors forces an
809
+ // immediate repaint of the new selection state.
810
+ .onLinkClick(function(e) {
811
+ selectedLink = (selectedLink === e) ? null : e;
812
+ graph.linkWidth(graph.linkWidth()).linkColor(graph.linkColor());
813
+ })
814
+ .onLinkHover(function(e) {
815
+ if (container) container.style.cursor = e ? 'pointer' : 'default';
816
+ if (!tooltip) return;
817
+ if (!e) { tooltip.style.display = 'none'; return; }
818
+ var s = (e.source && e.source.label) || e.source;
819
+ var t = (e.target && e.target.label) || e.target;
820
+ tooltip.style.display = 'block';
821
+ tooltip.textContent = s + ' → ' + t + (e.kind ? ' [' + e.kind + ']' : '');
822
+ })
792
823
  .onNodeClick(function(n) {
793
824
  // Navigate to the reading view via the server-resolved doc page (the
794
825
  // 'page' field, e.g. "docs/<slug>.html"), NOT the raw fileId — the
@@ -829,6 +860,10 @@ function buildAppScript(): string {
829
860
  .onBackgroundClick(function() {
830
861
  if (tooltip) tooltip.style.display = 'none';
831
862
  lastHoverNode = null;
863
+ if (selectedLink) {
864
+ selectedLink = null;
865
+ graph.linkWidth(graph.linkWidth()).linkColor(graph.linkColor());
866
+ }
832
867
  });
833
868
  }
834
869
 
@@ -126,8 +126,20 @@ export function classifyFile(fileId: string): DocType {
126
126
  // e.g. briefs/epic-overview.md must be 'brief', not 'epics'.
127
127
  if (lowerFileId.includes('/briefs/')) return 'brief';
128
128
 
129
- // PRD: filename contains 'prd' or matches product-requirements
130
- if (/prd/.test(basename) || /product-requirements/.test(basename)) return 'prd';
129
+ // PRD: filename contains 'prd' or matches product-requirements — but NOT a
130
+ // derivative document *about* the PRD (validation report, checklist, review,
131
+ // cohesion/assessment report). Those score or critique the requirements; the
132
+ // requirements themselves live in the PRD proper. Classifying e.g.
133
+ // `prd-validation-report.md` as 'prd' made it a rival definition-site, so an
134
+ // FR the PRD actually defines could anchor in the report instead — the real
135
+ // Prophesee bug where FR1/FR3/FR21 links pointed at prd-validation-report.html
136
+ // rather than the PRD. Let those fall through to 'report'.
137
+ if (
138
+ (/prd/.test(basename) || /product-requirements/.test(basename)) &&
139
+ !/(validation|report|checklist|review|cohesion|assessment)/.test(basename)
140
+ ) {
141
+ return 'prd';
142
+ }
131
143
 
132
144
  // Architecture document
133
145
  if (/architecture/.test(basename)) return 'architecture';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ma-agents",
3
- "version": "3.15.1",
3
+ "version": "3.15.3",
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": {