pm-graph 2026.6.2 → 2026.6.4

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.
package/CHANGELOG.md CHANGED
@@ -1,6 +1,28 @@
1
1
  # Changelog
2
2
 
3
- ## 2026.6.2 - 2026-06-02
3
+ ## 2026.6.4 - 2026-06-04
4
+
5
+ ### Added
6
+
7
+ - Add topo-sort, impact, and dependency-depth analytics ([pm-graph-r5yz](https://github.com/unbraind/pm-graph/blob/main/.agents/pm/features/pm-graph-r5yz.toon))
8
+
9
+ ## 2026.06.02-1 - 2026-06-02
10
+
11
+ ### Added
12
+
13
+ - Deepen pm-graph with offline analytics + new export formats ([pm-graph-1dpk](https://github.com/unbraind/pm-graph/blob/main/.agents/pm/features/pm-graph-1dpk.toon))
14
+ - Unit tests for analytics + new renderers ([pm-graph-f9c3](https://github.com/unbraind/pm-graph/blob/main/.agents/pm/tasks/pm-graph-f9c3.toon))
15
+ - graph exporter: add graphml + plantuml formats ([pm-graph-79f2](https://github.com/unbraind/pm-graph/blob/main/.agents/pm/tasks/pm-graph-79f2.toon))
16
+
17
+ ### Other
18
+
19
+ - pm-graph critical-path: longest blocking chain ([pm-graph-f1k9](https://github.com/unbraind/pm-graph/blob/main/.agents/pm/tasks/pm-graph-f1k9.toon))
20
+ - pm-graph path: shortest dependency path \(BFS\) ([pm-graph-vtlu](https://github.com/unbraind/pm-graph/blob/main/.agents/pm/tasks/pm-graph-vtlu.toon))
21
+ - pm-graph cycles: detect dependency cycles, CI exit code ([pm-graph-h0xs](https://github.com/unbraind/pm-graph/blob/main/.agents/pm/tasks/pm-graph-h0xs.toon))
22
+ - pm-graph analyze: offline graph-health report ([pm-graph-d9tz](https://github.com/unbraind/pm-graph/blob/main/.agents/pm/tasks/pm-graph-d9tz.toon))
23
+ - Decision: analytics operate on STRUCTURAL edges only ([pm-graph-89nw](https://github.com/unbraind/pm-graph/blob/main/.agents/pm/tasks/pm-graph-89nw.toon))
24
+
25
+ ## 2026.06.02 - 2026-06-02
4
26
 
5
27
  ### Added
6
28
 
package/README.md CHANGED
@@ -4,7 +4,9 @@ Knowledge graph and dependency graph extension for [pm CLI](https://github.com/u
4
4
 
5
5
  The extension reads the current workspace through `pm list-all --json` and `pm deps <id> --json`, then turns items, parent links, `blocked_by` metadata, dependency metadata, tags, statuses, types, assignees, sprints, and releases into graph nodes and relationships.
6
6
 
7
- It can sync that graph into Neo4j, or export it offline to **Mermaid**, **Graphviz DOT**, **JSON Graph**, or **Cypher** via `pm graph export` — with neighborhood (`--root`/`--depth`) and edge-type (`--edges`) shaping.
7
+ It can sync that graph into Neo4j, or export it offline to **Mermaid**, **Graphviz DOT**, **JSON Graph**, **Cypher**, **GraphML**, or **PlantUML** via `pm graph export` — with neighborhood (`--root`/`--depth`) and edge-type (`--edges`) shaping.
8
+
9
+ It also ships **offline graph analytics** (no Neo4j required): `pm pm-graph analyze`, `cycles`, `path`, `critical-path`, `topo-sort`, and `impact` run dependency-cycle detection, shortest-path, longest-chain, topological ordering, downstream-impact, orphan/root/leaf and centrality analysis directly against your workspace.
8
10
 
9
11
  ## Quick Start
10
12
 
@@ -54,7 +56,7 @@ pm install github.com/unbraind/pm-graph --force
54
56
  | `NEO4J_DATABASE` | No | Target database (defaults to server default) |
55
57
  | `PM_GRAPH_PROJECT_KEY` | No | Override the project key (defaults to workspace directory name) |
56
58
 
57
- The commands `export` and `cypher` do not require Neo4j at all.
59
+ The commands `export`, `cypher`, `analyze`, `cycles`, `path`, `critical-path`, `topo-sort`, and `impact`, plus the `pm graph export` exporter, do not require Neo4j at all.
58
60
 
59
61
  ## Commands
60
62
 
@@ -73,7 +75,7 @@ Example output:
73
75
  "ok": true,
74
76
  "source": "pm-graph",
75
77
  "neo4jConfigured": true,
76
- "version": "0.1.4"
78
+ "version": "0.2.0"
77
79
  }
78
80
  ```
79
81
 
@@ -136,6 +138,8 @@ pm graph export --format mermaid # Mermaid graph TD to std
136
138
  pm graph export --format dot --output graph.dot # Graphviz DOT to a file
137
139
  pm graph export --format json --output graph.json # JSON Graph (nodes/edges)
138
140
  pm graph export --format cypher # parameterized Cypher (commented params)
141
+ pm graph export --format graphml --output graph.graphml # GraphML XML for yEd / Gephi / NetworkX
142
+ pm graph export --format plantuml --output graph.puml # PlantUML object diagram
139
143
  ```
140
144
 
141
145
  Shape the exported graph:
@@ -156,7 +160,7 @@ pm graph export --format json --include-closed
156
160
 
157
161
  | Flag | Values | Default | Description |
158
162
  |---|---|---|---|
159
- | `--format` | `cypher` \| `mermaid` \| `dot` \| `json` | `json` | Output format. `mermaid` emits a `graph TD` flowchart; `dot` emits Graphviz `digraph`; `json` emits a JSON Graph (`nodes`/`edges`) document; `cypher` reuses the parameterized Neo4j import statements. |
163
+ | `--format` | `cypher` \| `mermaid` \| `dot` \| `json` \| `graphml` \| `plantuml` | `json` | Output format. `mermaid` emits a `graph TD` flowchart; `dot` emits Graphviz `digraph`; `json` emits a JSON Graph (`nodes`/`edges`) document; `cypher` reuses the parameterized Neo4j import statements; `graphml` emits a valid GraphML XML document for yEd / Gephi / NetworkX; `plantuml` emits a `@startuml`…`@enduml` object diagram. |
160
164
  | `--output <file>` | path | — | Write to this file instead of stdout. |
161
165
  | `--root <id>` | item id | — | Restrict the graph to the neighborhood around this node. |
162
166
  | `--depth <n>` | non-negative integer | unlimited | Max hops from `--root` (undirected). Only meaningful with `--root`. |
@@ -221,8 +225,8 @@ Example output (Neo4j connected):
221
225
  "nodeCount": 18,
222
226
  "relationshipCount": 11,
223
227
  "lastSyncedAt": "2026-05-14T10:00:00.000Z",
224
- "syncVersion": "0.1.4",
225
- "version": "0.1.4"
228
+ "syncVersion": "0.2.0",
229
+ "version": "0.2.0"
226
230
  }
227
231
  ```
228
232
 
@@ -236,7 +240,7 @@ Example output (Neo4j not configured):
236
240
  "projectKey": "my-project",
237
241
  "workspace": "/path/to/workspace",
238
242
  "localItemCount": 15,
239
- "version": "0.1.4"
243
+ "version": "0.2.0"
240
244
  }
241
245
  ```
242
246
 
@@ -284,6 +288,113 @@ Example output:
284
288
  }
285
289
  ```
286
290
 
291
+ ## Offline Analytics
292
+
293
+ These commands analyze the workspace dependency graph **entirely offline** — no Neo4j required. They operate on **structural edges only** (`BLOCKED_BY`, `CHILD_OF`, and dependency edges such as `BLOCKS`/`RELATED`) between real items; facet edges (type/status/assignee/sprint/release) and tag edges are deliberately excluded so cycle, path, and centrality results stay meaningful.
294
+
295
+ Closed/canceled items are excluded by default; pass `--include-closed` to keep them. `analyze`, `cycles`, `critical-path`, and `topo-sort` also accept `--root <id>` / `--depth <n>` to scope the analysis to a neighborhood.
296
+
297
+ ### `pm pm-graph analyze`
298
+
299
+ Comprehensive graph-health report: dependency-cycle count, orphan items (no edges), root items (no incoming dependency), leaf items (no outgoing dependency), longest dependency chain, top-N degree-centrality items, connected-component count, and blocked-item count.
300
+
301
+ ```bash
302
+ pm pm-graph analyze --json
303
+ pm pm-graph analyze --root pm-ep18 --depth 2 --json
304
+ pm pm-graph analyze --include-closed --json
305
+ ```
306
+
307
+ Example output (abbreviated):
308
+
309
+ ```json
310
+ {
311
+ "ok": true,
312
+ "itemCount": 7,
313
+ "structuralEdgeCount": 6,
314
+ "cycleCount": 1,
315
+ "cycles": [["pm-oj3m", "pm-zc3a", "pm-oj3m"]],
316
+ "orphans": ["pm-op0k"],
317
+ "roots": ["pm-ep18"],
318
+ "leaves": ["pm-hd71"],
319
+ "longestChainLength": 4,
320
+ "longestChain": ["pm-ep18", "pm-k849", "pm-p2q3", "pm-hd71"],
321
+ "connectedComponents": 3,
322
+ "blockedItemCount": 5,
323
+ "topDegreeCentrality": [{ "id": "pm-ep18", "degree": 2, "inDegree": 0, "outDegree": 2 }],
324
+ "maxDepth": 3,
325
+ "depthByItem": [{ "id": "pm-hd71", "depth": 3 }, { "id": "pm-p2q3", "depth": 2 }]
326
+ }
327
+ ```
328
+
329
+ The `maxDepth` and `depthByItem` fields report the **dependency depth** of each item — the number of edges on the longest directed structural path starting at the item (its distance to a leaf along blocker edges). A leaf has depth `0`; `maxDepth` equals the critical-path depth. `depthByItem` is sorted deepest-first. These fields are additive; all previously emitted fields are unchanged.
330
+
331
+ ### `pm pm-graph cycles`
332
+
333
+ Detect and list dependency cycles. Each cycle is printed as an ordered id path (first id equals last). **Exits with code 1 when any cycle exists** (so it can gate CI) and exits 0 when there are none.
334
+
335
+ ```bash
336
+ pm pm-graph cycles # human-readable; exit 1 if cycles found
337
+ pm pm-graph cycles --json # machine-readable
338
+ ```
339
+
340
+ ```jsonc
341
+ // no cycles -> exit 0
342
+ { "ok": true, "cycleCount": 0, "cycles": [] }
343
+ ```
344
+
345
+ ### `pm pm-graph path`
346
+
347
+ Shortest **directed** dependency path between two item ids via BFS over structural edges. Missing arguments yield a usage error (exit 2); an unknown id yields a not-found error (exit 3).
348
+
349
+ ```bash
350
+ pm pm-graph path pm-ep18 pm-hd71 --json
351
+ ```
352
+
353
+ ```json
354
+ { "ok": true, "from": "pm-ep18", "to": "pm-hd71", "found": true, "path": ["pm-ep18", "pm-hd71"], "length": 1 }
355
+ ```
356
+
357
+ ### `pm pm-graph critical-path`
358
+
359
+ The longest chain of blocking dependencies through the workspace — the critical path — as an ordered id list with its length. Cycle-safe.
360
+
361
+ ```bash
362
+ pm pm-graph critical-path --json
363
+ ```
364
+
365
+ ```json
366
+ { "ok": true, "length": 4, "path": ["pm-ep18", "pm-k849", "pm-p2q3", "pm-hd71"] }
367
+ ```
368
+
369
+ ### `pm pm-graph topo-sort`
370
+
371
+ Emit a valid **topological execution order** of items over structural edges (Kahn's algorithm), so each item is listed only after the items it depends on. Ties are broken by ascending id for deterministic output. **Exits with code 1 when a dependency cycle prevents a complete ordering** (CI-usable), reporting the cycle members and the resolvable prefix. Accepts `--root`/`--depth`/`--include-closed`.
372
+
373
+ ```bash
374
+ pm pm-graph topo-sort --json
375
+ ```
376
+
377
+ ```json
378
+ {
379
+ "ok": true,
380
+ "count": 6,
381
+ "cyclic": false,
382
+ "order": ["pm-p030", "pm-ijd7", "pm-vc4q", "pm-yol1", "pm-0wtl", "pm-8k1m"]
383
+ }
384
+ ```
385
+
386
+ ### `pm pm-graph impact`
387
+
388
+ List every item **transitively blocked-by / downstream** of a given item id — the reverse-reachable set following edge direction. Complements `path` and `neighbors`. A missing id yields a not-found error (exit 3); no id yields a usage error (exit 2).
389
+
390
+ ```bash
391
+ pm pm-graph impact pm-vc4q --json
392
+ ```
393
+
394
+ ```json
395
+ { "ok": true, "id": "pm-vc4q", "count": 3, "impacted": ["pm-0wtl", "pm-8k1m", "pm-yol1"] }
396
+ ```
397
+
287
398
  ## Graph Model
288
399
 
289
400
  - **`PmItem`** nodes for real pm items, labelled with item type (e.g. `task`, `bug`).
package/dist/index.d.ts CHANGED
@@ -14,8 +14,133 @@ type ExtensionApi = {
14
14
  registerCommand(command: RegisterCommand): void;
15
15
  registerExporter(name: string, exporter: Exporter): void;
16
16
  };
17
- export type ExportFormat = "cypher" | "mermaid" | "dot" | "json";
17
+ type GraphNode = {
18
+ id: string;
19
+ labels: string[];
20
+ properties: Record<string, unknown>;
21
+ };
22
+ type GraphRelationship = {
23
+ from: string;
24
+ to: string;
25
+ type: string;
26
+ properties: Record<string, unknown>;
27
+ };
28
+ type Graph = {
29
+ generatedAt: string;
30
+ workspace: string;
31
+ projectKey: string;
32
+ nodes: GraphNode[];
33
+ relationships: GraphRelationship[];
34
+ };
35
+ export type ExportFormat = "cypher" | "mermaid" | "dot" | "json" | "graphml" | "plantuml";
18
36
  export type EdgeFilter = "deps" | "tags" | "all";
37
+ /**
38
+ * Render a valid GraphML XML document (consumable by yEd / Gephi / NetworkX).
39
+ * Declares string keys for node title/type/status/labels and edge type, then
40
+ * emits one <node> per graph node and one <edge> per relationship.
41
+ */
42
+ export declare function renderGraphml(graph: Graph): string;
43
+ /**
44
+ * Render a PlantUML object diagram (`@startuml`…`@enduml`) with one object per
45
+ * node and one arrow per relationship, the relationship type as the arrow
46
+ * label. Renders with PlantUML / Structurizr / many docs toolchains.
47
+ */
48
+ export declare function renderPlantuml(graph: Graph): string;
49
+ type StructuralEdge = {
50
+ from: string;
51
+ to: string;
52
+ type: string;
53
+ };
54
+ /**
55
+ * Detect all elementary directed cycles among structural edges using an
56
+ * iterative DFS with a recursion stack. Returns each cycle as an ordered id
57
+ * path whose first and last ids are equal (e.g. [E, F, E]). Cycles are
58
+ * de-duplicated by their canonical rotation so A->B->A and B->A->B collapse.
59
+ */
60
+ export declare function findCycles(nodes: string[], edges: StructuralEdge[]): string[][];
61
+ /**
62
+ * Shortest directed path from `from` to `to` over structural edges (BFS).
63
+ * Returns the ordered id path (inclusive of both endpoints) or null if no path
64
+ * exists. Returns [from] when from === to.
65
+ */
66
+ export declare function shortestPath(edges: StructuralEdge[], from: string, to: string): string[] | null;
67
+ /**
68
+ * Longest dependency chain (critical path) over structural edges. Uses a
69
+ * memoised DFS that is safe on cyclic graphs (nodes on the active recursion
70
+ * stack are skipped, so a cycle cannot inflate the chain infinitely). Returns
71
+ * the ordered id list of the longest simple chain found.
72
+ */
73
+ export declare function longestChain(nodes: string[], edges: StructuralEdge[]): string[];
74
+ /**
75
+ * Topological execution order over structural edges using Kahn's algorithm.
76
+ *
77
+ * Edges point from an item to its blocker/dependency (e.g. B --BLOCKED_BY--> A
78
+ * means "B is blocked by A", so A must be done before B). A valid execution
79
+ * order therefore lists a node only after every node it points to. We compute
80
+ * that order by treating out-edges as prerequisites: repeatedly emit nodes whose
81
+ * out-degree (unsatisfied prerequisites) has dropped to zero.
82
+ *
83
+ * Returns `{ order, cycleNodes }`. When the graph is acyclic, `order` contains
84
+ * every node and `cycleNodes` is empty. When a cycle exists, the nodes that
85
+ * could not be ordered are returned in `cycleNodes` (and `order` holds the
86
+ * resolvable prefix). Ties are broken by ascending id for deterministic output.
87
+ */
88
+ export declare function topoSort(nodes: string[], edges: StructuralEdge[]): {
89
+ order: string[];
90
+ cycleNodes: string[];
91
+ };
92
+ /**
93
+ * Reverse-reachable set from `start` over structural edges: every node that can
94
+ * reach `start` by following edge direction (i.e. everything transitively
95
+ * blocked-by / downstream of `start`). With edges pointing item -> blocker, the
96
+ * dependents of X are the nodes with an edge INTO X, so we walk edges backwards
97
+ * via a reverse adjacency (BFS). Excludes `start` itself. Result is sorted.
98
+ */
99
+ export declare function reverseReachable(edges: StructuralEdge[], start: string): string[];
100
+ /**
101
+ * Longest-path depth per node: the number of edges on the longest directed
102
+ * structural path STARTING at the node (its distance to a leaf along blocker
103
+ * edges). A leaf (no outgoing edge) has depth 0. Cycle-safe: nodes on the active
104
+ * recursion stack are skipped so a cycle cannot inflate depth infinitely. This
105
+ * is the "longest path from any root" metric expressed per node, since the
106
+ * deepest node is exactly the far end of the critical path.
107
+ */
108
+ export declare function dependencyDepths(nodes: string[], edges: StructuralEdge[]): Map<string, number>;
109
+ type AnalyzeReport = {
110
+ workspace: string;
111
+ projectKey: string;
112
+ itemCount: number;
113
+ structuralEdgeCount: number;
114
+ cycleCount: number;
115
+ cycles: string[][];
116
+ orphanCount: number;
117
+ orphans: string[];
118
+ rootCount: number;
119
+ roots: string[];
120
+ leafCount: number;
121
+ leaves: string[];
122
+ longestChainLength: number;
123
+ longestChain: string[];
124
+ connectedComponents: number;
125
+ blockedItemCount: number;
126
+ blockedItems: string[];
127
+ topDegreeCentrality: Array<{
128
+ id: string;
129
+ degree: number;
130
+ inDegree: number;
131
+ outDegree: number;
132
+ }>;
133
+ maxDepth: number;
134
+ depthByItem: Array<{
135
+ id: string;
136
+ depth: number;
137
+ }>;
138
+ };
139
+ /**
140
+ * Compute a comprehensive offline graph-health report from a shaped graph.
141
+ * All analytics operate on structural edges between item nodes only.
142
+ */
143
+ export declare function analyzeGraph(graph: Graph, topN?: number): AnalyzeReport;
19
144
  export declare function activate(api: ExtensionApi): void;
20
145
  declare const _default: {
21
146
  activate: typeof activate;