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 +23 -1
- package/README.md +118 -7
- package/dist/index.d.ts +126 -1
- package/dist/index.js +792 -4
- package/dist/index.js.map +1 -1
- package/manifest.json +1 -1
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,28 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 2026.6.
|
|
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 **
|
|
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 `
|
|
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.
|
|
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.
|
|
225
|
-
"version": "0.
|
|
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.
|
|
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
|
-
|
|
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;
|