sysprom 1.27.2 → 1.28.1

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.
@@ -24,6 +24,7 @@ declare const optsSchema: z.ZodObject<{
24
24
  }>>;
25
25
  cluster: z.ZodOptional<z.ZodBoolean>;
26
26
  connectedOnly: z.ZodOptional<z.ZodBoolean>;
27
+ links: z.ZodOptional<z.ZodBoolean>;
27
28
  }, z.core.$strip>;
28
29
  export declare const graphCommand: CommandDef<typeof noArgs, typeof optsSchema>;
29
30
  export {};
@@ -1,6 +1,7 @@
1
1
  /// <reference types="node" />
2
2
  import * as z from "zod";
3
3
  import { graphOp } from "../../operations/index.js";
4
+ import { buildExternalRefClickMap } from "../../operations/graph-shared.js";
4
5
  import { readOpts, loadDoc } from "../shared.js";
5
6
  const optsSchema = readOpts.extend({
6
7
  format: z.enum(["mermaid", "dot"]).optional().describe("Output format"),
@@ -36,6 +37,10 @@ const optsSchema = readOpts.extend({
36
37
  .boolean()
37
38
  .optional()
38
39
  .describe("Only show nodes that have relationships"),
40
+ links: z
41
+ .boolean()
42
+ .optional()
43
+ .describe("Add click hyperlinks from external references to Mermaid nodes"),
39
44
  });
40
45
  export const graphCommand = {
41
46
  name: "graph",
@@ -54,9 +59,13 @@ export const graphCommand = {
54
59
  const relTypes = opts.relTypes
55
60
  ? opts.relTypes.split(",").map((s) => s.trim())
56
61
  : undefined;
62
+ const format = opts.format ?? "mermaid";
63
+ const clickTargets = opts.links && format === "mermaid"
64
+ ? Object.fromEntries(buildExternalRefClickMap(doc.nodes))
65
+ : undefined;
57
66
  const output = graphOp({
58
67
  doc,
59
- format: opts.format ?? "mermaid",
68
+ format,
60
69
  typeFilter: opts.type,
61
70
  nodeTypes,
62
71
  nodeIds,
@@ -65,6 +74,7 @@ export const graphCommand = {
65
74
  cluster: opts.cluster ?? true,
66
75
  labelMode: opts.labelMode ?? "friendly",
67
76
  connectedOnly: opts.connectedOnly ?? false,
77
+ clickTargets,
68
78
  });
69
79
  console.log(output);
70
80
  }
@@ -33,6 +33,7 @@ declare const optsSchema: z.ZodObject<{
33
33
  RL: "RL";
34
34
  LR: "LR";
35
35
  }>>;
36
+ diagramLinks: z.ZodOptional<z.ZodBoolean>;
36
37
  }, z.core.$strict>;
37
38
  export declare const json2mdCommand: CommandDef<z.ZodObject, typeof optsSchema>;
38
39
  export {};
@@ -37,6 +37,10 @@ const optsSchema = z
37
37
  .enum(["LR", "TD", "RL", "BT"])
38
38
  .optional()
39
39
  .describe("Override layout for dependency diagrams"),
40
+ diagramLinks: z
41
+ .boolean()
42
+ .optional()
43
+ .describe("Add click hyperlinks to Mermaid diagram nodes"),
40
44
  })
41
45
  .strict();
42
46
  export const json2mdCommand = {
@@ -71,6 +75,7 @@ export const json2mdCommand = {
71
75
  jsonToMarkdown(raw, outputPath, {
72
76
  form,
73
77
  embedDiagrams: opts.embedDiagrams,
78
+ diagramLinks: opts.diagramLinks,
74
79
  // forward labelMode for embedded diagrams (default friendly)
75
80
  labelMode: opts.labelMode ?? "friendly",
76
81
  relationshipLayout: opts.relationshipLayout,
@@ -277,11 +277,6 @@ const removeRefSubcommand = {
277
277
  const opts = removeRefOpts.parse(rawOpts);
278
278
  const loaded = loadDoc(opts.path);
279
279
  const { doc } = loaded;
280
- removeExternalReferenceOp({
281
- doc,
282
- nodeId: args.id,
283
- identifier: opts.identifier,
284
- });
285
280
  const newDoc = removeExternalReferenceOp({
286
281
  doc,
287
282
  nodeId: args.id,
@@ -4,6 +4,7 @@ type DiagramLayout = "LR" | "TD" | "RL" | "BT";
4
4
  export interface ConvertOptions {
5
5
  form: "single-file" | "multi-doc";
6
6
  embedDiagrams?: boolean;
7
+ diagramLinks?: boolean;
7
8
  labelMode?: "friendly" | "compact";
8
9
  relationshipLayout?: DiagramLayout;
9
10
  refinementLayout?: DiagramLayout;
@@ -12,6 +13,7 @@ export interface ConvertOptions {
12
13
  }
13
14
  interface MarkdownRenderOptions {
14
15
  embedDiagrams?: boolean;
16
+ diagramLinks?: boolean;
15
17
  labelMode?: "friendly" | "compact";
16
18
  relationshipLayout?: DiagramLayout;
17
19
  refinementLayout?: DiagramLayout;
@@ -424,6 +424,7 @@ function generateDiagramsFile(doc, opts) {
424
424
  labelMode: opts?.labelMode ?? "friendly",
425
425
  cluster: true,
426
426
  connectedOnly: false,
427
+ clickTargets: opts?.clickTargets,
427
428
  }));
428
429
  lines.push("```");
429
430
  lines.push("");
@@ -432,6 +433,7 @@ function generateDiagramsFile(doc, opts) {
432
433
  format: "mermaid",
433
434
  layout: opts?.refinementLayout ?? "TD",
434
435
  labelMode: opts?.labelMode ?? "friendly",
436
+ clickTargets: opts?.clickTargets,
435
437
  });
436
438
  if (refinement.includes("-->")) {
437
439
  lines.push("## Refinement Chain");
@@ -446,6 +448,7 @@ function generateDiagramsFile(doc, opts) {
446
448
  format: "mermaid",
447
449
  layout: opts?.decisionLayout ?? "TD",
448
450
  labelMode: opts?.labelMode ?? "friendly",
451
+ clickTargets: opts?.clickTargets,
449
452
  });
450
453
  if (decisions.includes("-->")) {
451
454
  lines.push("## Decision Map");
@@ -460,6 +463,7 @@ function generateDiagramsFile(doc, opts) {
460
463
  format: "mermaid",
461
464
  layout: opts?.dependencyLayout ?? "LR",
462
465
  labelMode: opts?.labelMode ?? "friendly",
466
+ clickTargets: opts?.clickTargets,
463
467
  });
464
468
  if (dependencies.includes("-->") || dependencies.includes("-.->")) {
465
469
  lines.push("## Dependency Graph");
@@ -471,6 +475,23 @@ function generateDiagramsFile(doc, opts) {
471
475
  }
472
476
  return lines.join("\n") + "\n";
473
477
  }
478
+ // ---------------------------------------------------------------------------
479
+ // Public API
480
+ // ---------------------------------------------------------------------------
481
+ /** Build a click target map from node anchors for use in embedded diagrams. */
482
+ function buildAnchorClickMap(nodes, nodeMap, currentFile) {
483
+ const targets = {};
484
+ for (const node of nodes) {
485
+ const loc = nodeMap.get(node.id);
486
+ if (!loc)
487
+ continue;
488
+ targets[node.id] =
489
+ loc.file === "" || loc.file === currentFile
490
+ ? `#${loc.anchor}`
491
+ : `./${loc.file}#${loc.anchor}`;
492
+ }
493
+ return targets;
494
+ }
474
495
  /**
475
496
  * Convert a SysProM document to a single Markdown string.
476
497
  * @param doc - The SysProM document to convert.
@@ -518,6 +539,9 @@ export function jsonToMarkdownSingle(doc, options) {
518
539
  if (options?.embedDiagrams &&
519
540
  doc.relationships &&
520
541
  doc.relationships.length > 0) {
542
+ const clickTargets = options?.diagramLinks
543
+ ? buildAnchorClickMap(doc.nodes, nodeMap, "")
544
+ : undefined;
521
545
  lines.push("## Diagrams");
522
546
  lines.push("");
523
547
  lines.push("### Relationship Graph");
@@ -530,6 +554,7 @@ export function jsonToMarkdownSingle(doc, options) {
530
554
  labelMode: options?.labelMode ?? "friendly",
531
555
  cluster: true,
532
556
  connectedOnly: false,
557
+ clickTargets,
533
558
  }));
534
559
  lines.push("```");
535
560
  lines.push("");
@@ -591,12 +616,16 @@ export function jsonToMarkdownMultiDoc(doc, outDir, options) {
591
616
  if (options?.embedDiagrams &&
592
617
  doc.relationships &&
593
618
  doc.relationships.length > 0) {
619
+ const clickTargets = options?.diagramLinks
620
+ ? buildAnchorClickMap(doc.nodes, nodeMap, "DIAGRAMS.md")
621
+ : undefined;
594
622
  writeFileSync(join(outDir, "DIAGRAMS.md"), generateDiagramsFile(doc, {
595
623
  labelMode: options?.labelMode ?? "friendly",
596
624
  relationshipLayout: options?.relationshipLayout,
597
625
  refinementLayout: options?.refinementLayout,
598
626
  decisionLayout: options?.decisionLayout,
599
627
  dependencyLayout: options?.dependencyLayout,
628
+ clickTargets,
600
629
  }));
601
630
  }
602
631
  // Subsystem folders or single files
@@ -662,6 +691,7 @@ export function jsonToMarkdown(doc, output, options) {
662
691
  if (options.form === "single-file") {
663
692
  writeFileSync(output, jsonToMarkdownSingle(doc, {
664
693
  embedDiagrams: options.embedDiagrams,
694
+ diagramLinks: options.diagramLinks,
665
695
  labelMode: options.labelMode,
666
696
  relationshipLayout: options.relationshipLayout,
667
697
  refinementLayout: options.refinementLayout,
@@ -672,6 +702,7 @@ export function jsonToMarkdown(doc, output, options) {
672
702
  else {
673
703
  jsonToMarkdownMultiDoc(doc, output, {
674
704
  embedDiagrams: options.embedDiagrams,
705
+ diagramLinks: options.diagramLinks,
675
706
  labelMode: options.labelMode,
676
707
  relationshipLayout: options.relationshipLayout,
677
708
  refinementLayout: options.refinementLayout,
@@ -272,4 +272,5 @@ export declare const graphDecisionOp: import("./define-operation.js").DefinedOpe
272
272
  friendly: "friendly";
273
273
  compact: "compact";
274
274
  }>>;
275
+ clickTargets: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
275
276
  }, z.core.$strip>, z.ZodString>;
@@ -1,7 +1,7 @@
1
1
  import * as z from "zod";
2
2
  import { defineOperation } from "./define-operation.js";
3
3
  import { SysProMDocument } from "../schema.js";
4
- import { sanitiseMermaidId, mermaidShapeForNode, renderMermaidNode, renderMermaidClassDefs, mermaidClassForNode, dotNodeAttrsWithMode, renderRelationshipLabel, } from "./graph-shared.js";
4
+ import { sanitiseMermaidId, mermaidShapeForNode, renderMermaidNode, renderMermaidClassDefs, mermaidClassForNode, dotNodeAttrsWithMode, renderRelationshipLabel, renderMermaidClickDirectives, } from "./graph-shared.js";
5
5
  const DECISION_REL_TYPES = new Set([
6
6
  "must_preserve",
7
7
  "affects",
@@ -23,7 +23,7 @@ function collectDecisionMap(doc, seedIds) {
23
23
  const nodes = doc.nodes.filter((n) => nodeIds.has(n.id));
24
24
  return { nodes, rels };
25
25
  }
26
- function generateDecisionMermaid(nodes, rels, labelMode) {
26
+ function generateDecisionMermaid(nodes, rels, labelMode, clickMap) {
27
27
  const lines = [];
28
28
  lines.push("graph TD");
29
29
  for (const def of renderMermaidClassDefs()) {
@@ -43,6 +43,7 @@ function generateDecisionMermaid(nodes, rels, labelMode) {
43
43
  const label = renderRelationshipLabel(rel);
44
44
  lines.push(` ${fromId} ${style}|${label}| ${toId}`);
45
45
  }
46
+ lines.push(...renderMermaidClickDirectives(nodes, clickMap));
46
47
  return lines.join("\n");
47
48
  }
48
49
  function generateDecisionDot(nodes, rels, layout, labelMode) {
@@ -80,14 +81,18 @@ export const graphDecisionOp = defineOperation({
80
81
  seedIds: z.array(z.string()).optional(),
81
82
  layout: z.enum(["LR", "TD", "RL", "BT"]).default("TD"),
82
83
  labelMode: z.enum(["friendly", "compact"]).default("friendly"),
84
+ clickTargets: z.record(z.string(), z.string()).optional(),
83
85
  }),
84
86
  output: z.string(),
85
- fn({ doc, format, seedIds, layout, labelMode }) {
87
+ fn({ doc, format, seedIds, layout, labelMode, clickTargets }) {
86
88
  const { nodes, rels } = collectDecisionMap(doc, seedIds);
87
89
  if (format === "dot") {
88
90
  return generateDecisionDot(nodes, rels, layout, labelMode);
89
91
  }
90
- const mermaid = generateDecisionMermaid(nodes, rels, labelMode);
92
+ const clickMap = clickTargets
93
+ ? new Map(Object.entries(clickTargets))
94
+ : undefined;
95
+ const mermaid = generateDecisionMermaid(nodes, rels, labelMode, clickMap);
91
96
  const mermaidLines = mermaid.split("\n");
92
97
  mermaidLines[0] = `graph ${layout}`;
93
98
  return mermaidLines.join("\n");
@@ -272,4 +272,5 @@ export declare const graphDependencyOp: import("./define-operation.js").DefinedO
272
272
  friendly: "friendly";
273
273
  compact: "compact";
274
274
  }>>;
275
+ clickTargets: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
275
276
  }, z.core.$strip>, z.ZodString>;
@@ -1,7 +1,7 @@
1
1
  import * as z from "zod";
2
2
  import { defineOperation } from "./define-operation.js";
3
3
  import { SysProMDocument } from "../schema.js";
4
- import { sanitiseMermaidId, mermaidShapeForNode, renderMermaidNode, renderMermaidClassDefs, mermaidClassForNode, dotNodeAttrsWithMode, renderRelationshipLabel, } from "./graph-shared.js";
4
+ import { sanitiseMermaidId, mermaidShapeForNode, renderMermaidNode, renderMermaidClassDefs, mermaidClassForNode, dotNodeAttrsWithMode, renderRelationshipLabel, renderMermaidClickDirectives, } from "./graph-shared.js";
5
5
  const DEPENDENCY_REL_TYPES = new Set([
6
6
  "depends_on",
7
7
  "constrained_by",
@@ -33,7 +33,7 @@ function collectDependencyGraph(doc, seedIds) {
33
33
  const nodes = doc.nodes.filter((n) => nodeIds.has(n.id));
34
34
  return { nodes, rels };
35
35
  }
36
- function generateDependencyMermaid(nodes, rels, labelMode) {
36
+ function generateDependencyMermaid(nodes, rels, labelMode, clickMap) {
37
37
  const lines = [];
38
38
  lines.push("graph LR");
39
39
  for (const def of renderMermaidClassDefs()) {
@@ -52,6 +52,7 @@ function generateDependencyMermaid(nodes, rels, labelMode) {
52
52
  const label = renderRelationshipLabel(rel);
53
53
  lines.push(` ${fromId} -->|${label}| ${toId}`);
54
54
  }
55
+ lines.push(...renderMermaidClickDirectives(nodes, clickMap));
55
56
  return lines.join("\n");
56
57
  }
57
58
  function generateDependencyDot(nodes, rels, layout, labelMode) {
@@ -86,14 +87,18 @@ export const graphDependencyOp = defineOperation({
86
87
  seedIds: z.array(z.string()).optional(),
87
88
  layout: z.enum(["LR", "TD", "RL", "BT"]).default("LR"),
88
89
  labelMode: z.enum(["friendly", "compact"]).default("friendly"),
90
+ clickTargets: z.record(z.string(), z.string()).optional(),
89
91
  }),
90
92
  output: z.string(),
91
- fn({ doc, format, seedIds, layout, labelMode }) {
93
+ fn({ doc, format, seedIds, layout, labelMode, clickTargets }) {
92
94
  const { nodes, rels } = collectDependencyGraph(doc, seedIds);
93
95
  if (format === "dot") {
94
96
  return generateDependencyDot(nodes, rels, layout, labelMode);
95
97
  }
96
- const mermaid = generateDependencyMermaid(nodes, rels, labelMode);
98
+ const clickMap = clickTargets
99
+ ? new Map(Object.entries(clickTargets))
100
+ : undefined;
101
+ const mermaid = generateDependencyMermaid(nodes, rels, labelMode, clickMap);
97
102
  const mermaidLines = mermaid.split("\n");
98
103
  mermaidLines[0] = `graph ${layout}`;
99
104
  return mermaidLines.join("\n");
@@ -272,4 +272,5 @@ export declare const graphRefinementOp: import("./define-operation.js").DefinedO
272
272
  friendly: "friendly";
273
273
  compact: "compact";
274
274
  }>>;
275
+ clickTargets: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
275
276
  }, z.core.$strip>, z.ZodString>;
@@ -1,7 +1,7 @@
1
1
  import * as z from "zod";
2
2
  import { defineOperation } from "./define-operation.js";
3
3
  import { SysProMDocument } from "../schema.js";
4
- import { sanitiseMermaidId, mermaidShapeForNode, renderMermaidNode, renderMermaidClassDefs, mermaidClassForNode, dotNodeAttrsWithMode, renderRelationshipLabel, } from "./graph-shared.js";
4
+ import { sanitiseMermaidId, mermaidShapeForNode, renderMermaidNode, renderMermaidClassDefs, mermaidClassForNode, dotNodeAttrsWithMode, renderRelationshipLabel, renderMermaidClickDirectives, } from "./graph-shared.js";
5
5
  const REFINEMENT_REL_TYPES = new Set(["refines", "realises", "implements"]);
6
6
  function collectRefinementChain(doc, seedIds) {
7
7
  const rels = (doc.relationships ?? []).filter((r) => REFINEMENT_REL_TYPES.has(r.type));
@@ -29,7 +29,7 @@ function collectRefinementChain(doc, seedIds) {
29
29
  const nodes = doc.nodes.filter((n) => nodeIds.has(n.id));
30
30
  return { nodes, rels };
31
31
  }
32
- function generateRefinementMermaid(nodes, rels, labelMode) {
32
+ function generateRefinementMermaid(nodes, rels, labelMode, clickMap) {
33
33
  const lines = [];
34
34
  lines.push("graph TD");
35
35
  for (const def of renderMermaidClassDefs()) {
@@ -48,6 +48,7 @@ function generateRefinementMermaid(nodes, rels, labelMode) {
48
48
  const label = renderRelationshipLabel(rel);
49
49
  lines.push(` ${fromId} -->|${label}| ${toId}`);
50
50
  }
51
+ lines.push(...renderMermaidClickDirectives(nodes, clickMap));
51
52
  return lines.join("\n");
52
53
  }
53
54
  function generateRefinementDot(nodes, rels, layout, labelMode) {
@@ -82,14 +83,18 @@ export const graphRefinementOp = defineOperation({
82
83
  seedIds: z.array(z.string()).optional(),
83
84
  layout: z.enum(["LR", "TD", "RL", "BT"]).default("TD"),
84
85
  labelMode: z.enum(["friendly", "compact"]).default("friendly"),
86
+ clickTargets: z.record(z.string(), z.string()).optional(),
85
87
  }),
86
88
  output: z.string(),
87
- fn({ doc, format, seedIds, layout, labelMode }) {
89
+ fn({ doc, format, seedIds, layout, labelMode, clickTargets }) {
88
90
  const { nodes, rels } = collectRefinementChain(doc, seedIds);
89
91
  if (format === "dot") {
90
92
  return generateRefinementDot(nodes, rels, layout, labelMode);
91
93
  }
92
- const mermaid = generateRefinementMermaid(nodes, rels, labelMode);
94
+ const clickMap = clickTargets
95
+ ? new Map(Object.entries(clickTargets))
96
+ : undefined;
97
+ const mermaid = generateRefinementMermaid(nodes, rels, labelMode, clickMap);
93
98
  const mermaidLines = mermaid.split("\n");
94
99
  mermaidLines[0] = `graph ${layout}`;
95
100
  return mermaidLines.join("\n");
@@ -114,3 +114,18 @@ export declare function filterRelationships(rels: Relationship[], opts: GraphFil
114
114
  * @example
115
115
  */
116
116
  export declare function applyConnectedOnly(nodes: Node[], rels: Relationship[]): Node[];
117
+ /** Map from raw node ID to click target URL. */
118
+ export type MermaidClickMap = Map<string, string>;
119
+ /**
120
+ * Render Mermaid click directives for nodes that have a click target.
121
+ * @param nodes - Nodes to render click directives for
122
+ * @param clickMap - Map from node ID to target URL
123
+ * @returns Lines like `click INT1 "url" "tooltip"`
124
+ */
125
+ export declare function renderMermaidClickDirectives(nodes: Node[], clickMap: MermaidClickMap | undefined): string[];
126
+ /**
127
+ * Build a MermaidClickMap from nodes' external_references.
128
+ * Uses the first external reference's identifier as the URL.
129
+ * @param nodes - Nodes to extract external references from
130
+ */
131
+ export declare function buildExternalRefClickMap(nodes: Node[]): MermaidClickMap;
@@ -272,3 +272,36 @@ export function applyConnectedOnly(nodes, rels) {
272
272
  }
273
273
  return nodes.filter((n) => connectedIds.has(n.id));
274
274
  }
275
+ /**
276
+ * Render Mermaid click directives for nodes that have a click target.
277
+ * @param nodes - Nodes to render click directives for
278
+ * @param clickMap - Map from node ID to target URL
279
+ * @returns Lines like `click INT1 "url" "tooltip"`
280
+ */
281
+ export function renderMermaidClickDirectives(nodes, clickMap) {
282
+ if (!clickMap || clickMap.size === 0)
283
+ return [];
284
+ const lines = [""];
285
+ for (const node of nodes) {
286
+ const url = clickMap.get(node.id);
287
+ if (!url)
288
+ continue;
289
+ const safeId = sanitiseMermaidId(node.id);
290
+ lines.push(` click ${safeId} "${url}" "${node.name}"`);
291
+ }
292
+ return lines;
293
+ }
294
+ /**
295
+ * Build a MermaidClickMap from nodes' external_references.
296
+ * Uses the first external reference's identifier as the URL.
297
+ * @param nodes - Nodes to extract external references from
298
+ */
299
+ export function buildExternalRefClickMap(nodes) {
300
+ const map = new Map();
301
+ for (const node of nodes) {
302
+ const ref = node.external_references?.[0];
303
+ if (ref)
304
+ map.set(node.id, ref.identifier);
305
+ }
306
+ return map;
307
+ }
@@ -277,4 +277,5 @@ export declare const graphOp: import("./define-operation.js").DefinedOperation<z
277
277
  compact: "compact";
278
278
  }>>;
279
279
  connectedOnly: z.ZodDefault<z.ZodBoolean>;
280
+ clickTargets: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
280
281
  }, z.core.$strip>, z.ZodString>;
@@ -1,8 +1,7 @@
1
1
  import * as z from "zod";
2
2
  import { defineOperation } from "./define-operation.js";
3
3
  import { SysProMDocument } from "../schema.js";
4
- import { sanitiseMermaidId, NODE_CATEGORIES, mermaidShapeForNode, renderMermaidNode, renderMermaidClassDefs, mermaidClassForNode, dotNodeAttrsWithMode, filterNodes, filterRelationships, applyConnectedOnly, } from "./graph-shared.js";
5
- import { renderRelationshipLabel } from "./graph-shared.js";
4
+ import { sanitiseMermaidId, NODE_CATEGORIES, mermaidShapeForNode, renderMermaidNode, renderMermaidClassDefs, mermaidClassForNode, dotNodeAttrsWithMode, filterNodes, filterRelationships, applyConnectedOnly, renderRelationshipLabel, renderMermaidClickDirectives, } from "./graph-shared.js";
6
5
  // ---------------------------------------------------------------------------
7
6
  // DOT generation
8
7
  // ---------------------------------------------------------------------------
@@ -61,7 +60,7 @@ function generateDot(nodes, rels, cluster, layout, labelMode) {
61
60
  // ---------------------------------------------------------------------------
62
61
  // Mermaid generation
63
62
  // ---------------------------------------------------------------------------
64
- function generateMermaid(nodes, rels, cluster, layout, labelMode) {
63
+ function generateMermaid(nodes, rels, cluster, layout, labelMode, clickMap) {
65
64
  const lines = [];
66
65
  lines.push(`graph ${layout}`);
67
66
  for (const def of renderMermaidClassDefs()) {
@@ -112,12 +111,13 @@ function generateMermaid(nodes, rels, cluster, layout, labelMode) {
112
111
  }
113
112
  lines.push(` ${edge}`);
114
113
  }
114
+ lines.push(...renderMermaidClickDirectives(nodes, clickMap));
115
115
  return lines.join("\n");
116
116
  }
117
117
  // ---------------------------------------------------------------------------
118
118
  // Graph generation
119
119
  // ---------------------------------------------------------------------------
120
- function generateGraph(doc, format, filterOpts, layout, cluster, labelMode) {
120
+ function generateGraph(doc, format, filterOpts, layout, cluster, labelMode, clickMap) {
121
121
  let nodes = filterNodes(doc.nodes, filterOpts);
122
122
  const nodeIds = new Set(nodes.map((n) => n.id));
123
123
  let rels = filterRelationships(doc.relationships ?? [], filterOpts, nodeIds);
@@ -134,7 +134,7 @@ function generateGraph(doc, format, filterOpts, layout, cluster, labelMode) {
134
134
  const dotLayout = layout === "TD" ? "LR" : layout;
135
135
  return generateDot(nodes, rels, cluster, dotLayout, labelMode);
136
136
  }
137
- return generateMermaid(nodes, rels, cluster, layout, labelMode);
137
+ return generateMermaid(nodes, rels, cluster, layout, labelMode, clickMap);
138
138
  }
139
139
  /** Generate a graph of a SysProM document in Mermaid or DOT format, with optional filtering. */
140
140
  export const graphOp = defineOperation({
@@ -151,15 +151,19 @@ export const graphOp = defineOperation({
151
151
  cluster: z.boolean().default(true),
152
152
  labelMode: z.enum(["friendly", "compact"]).default("friendly"),
153
153
  connectedOnly: z.boolean().default(false),
154
+ clickTargets: z.record(z.string(), z.string()).optional(),
154
155
  }),
155
156
  output: z.string(),
156
- fn({ doc, format, typeFilter, nodeTypes, nodeIds, relTypes, layout, cluster, labelMode, connectedOnly, }) {
157
+ fn({ doc, format, typeFilter, nodeTypes, nodeIds, relTypes, layout, cluster, labelMode, connectedOnly, clickTargets, }) {
157
158
  const filterOpts = {
158
159
  nodeTypes,
159
160
  nodeIds,
160
161
  relTypes: relTypes ?? (typeFilter ? [typeFilter] : undefined),
161
162
  connectedOnly,
162
163
  };
163
- return generateGraph(doc, format, filterOpts, layout, cluster, labelMode);
164
+ const clickMap = clickTargets
165
+ ? new Map(Object.entries(clickTargets))
166
+ : undefined;
167
+ return generateGraph(doc, format, filterOpts, layout, cluster, labelMode, clickMap);
164
168
  },
165
169
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sysprom",
3
- "version": "1.27.2",
3
+ "version": "1.28.1",
4
4
  "description": "SysProM — System Provenance Model CLI and library",
5
5
  "author": "ExaDev",
6
6
  "homepage": "https://exadev.github.io/SysProM",