@webpieces/nx-webpieces-rules 0.4.653 → 0.4.655
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/package.json +6 -6
- package/src/branch-identity-inputs.d.ts +27 -0
- package/src/branch-identity-inputs.js +33 -0
- package/src/branch-identity-inputs.js.map +1 -0
- package/src/executors/generate/executor.js +12 -0
- package/src/executors/generate/executor.js.map +1 -1
- package/src/executors/validate-no-architecture-cycles/executor.js +12 -7
- package/src/executors/validate-no-architecture-cycles/executor.js.map +1 -1
- package/src/lib/graph-cycles.d.ts +96 -0
- package/src/lib/graph-cycles.js +182 -0
- package/src/lib/graph-cycles.js.map +1 -0
- package/src/lib/graph-names.d.ts +49 -6
- package/src/lib/graph-names.js +88 -7
- package/src/lib/graph-names.js.map +1 -1
- package/src/lib/graph-responsibilities.d.ts +1 -1
- package/src/lib/graph-responsibilities.js +6 -2
- package/src/lib/graph-responsibilities.js.map +1 -1
- package/src/lib/graph-sorter.js +7 -39
- package/src/lib/graph-sorter.js.map +1 -1
- package/src/lib/graph-visualizer.d.ts +10 -0
- package/src/lib/graph-visualizer.js +33 -8
- package/src/lib/graph-visualizer.js.map +1 -1
- package/src/lib/runtime-cycles.d.ts +5 -2
- package/src/lib/runtime-cycles.js +5 -2
- package/src/lib/runtime-cycles.js.map +1 -1
- package/src/lib/runtime-visualizer.js +29 -14
- package/src/lib/runtime-visualizer.js.map +1 -1
- package/src/plugin.js +8 -1
- package/src/plugin.js.map +1 -1
- package/src/validation-targets.js +4 -3
- package/src/validation-targets.js.map +1 -1
package/src/lib/graph-names.d.ts
CHANGED
|
@@ -1,17 +1,60 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Graph Names
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
4
|
+
* The two names a project has in the visualization, kept apart on purpose:
|
|
5
|
+
*
|
|
6
|
+
* - its NODE ID — the identity every DOT node, edge endpoint, `rank=same` member, lock-dropdown
|
|
7
|
+
* value and responsibilities `data-node` attribute is keyed on. It is the project KEY, verbatim.
|
|
8
|
+
* - its SHORT NAME — the human-facing label, with the `@scope/` prefix stripped.
|
|
9
|
+
*
|
|
10
|
+
* They used to be the same string, and that was a silent, badly-wrong-picture bug. A workspace may
|
|
11
|
+
* legitimately hold `@scope/public-api` (an L0 contract lib) AND `public-api` (the L6 server that
|
|
12
|
+
* serves it) — two DISTINCT projects that strip to the same short name. Keying the DOT on the short
|
|
13
|
+
* name fused them into one node, and graphviz UNIONS any two `rank=same` sets sharing a node, so
|
|
14
|
+
* L0's rank and L6's rank became one rank: the entire L0 band rendered on the L6 row, the invisible
|
|
15
|
+
* anchor chain that pins level order became unsatisfiable (it demands L0 strictly below L6 while the
|
|
16
|
+
* union says they are equal) and was discarded, and the server's dependency on the contract lib drew
|
|
17
|
+
* as a self-loop on the fused box.
|
|
18
|
+
*
|
|
19
|
+
* So identity is the project key and nothing else — collision is structurally impossible now rather
|
|
20
|
+
* than merely unlikely — and {@link GraphNames.assertUniqueNodeIds} asserts that, so a future change
|
|
21
|
+
* that reintroduces a lossy id fails generation loudly instead of drawing a wrong graph in silence.
|
|
22
|
+
*
|
|
23
|
+
* Kept in its own class so GraphVisualizer and ResponsibilitiesRenderer can both depend on it
|
|
24
|
+
* without a circular dependency between them.
|
|
9
25
|
*/
|
|
26
|
+
/** One project key and the level it sits on — what a collision report has to name. */
|
|
27
|
+
export declare class NodeIdOwner {
|
|
28
|
+
readonly projectKey: string;
|
|
29
|
+
readonly level: number;
|
|
30
|
+
constructor(projectKey: string, level: number);
|
|
31
|
+
}
|
|
32
|
+
/** Thrown when two distinct project keys would be drawn as the same DOT node. */
|
|
33
|
+
export declare class NodeIdCollisionError extends Error {
|
|
34
|
+
constructor(message: string);
|
|
35
|
+
}
|
|
10
36
|
export declare class GraphNames {
|
|
11
37
|
/**
|
|
12
|
-
*
|
|
38
|
+
* The DOT/DOM identity of a project: its project KEY, unchanged.
|
|
39
|
+
*
|
|
40
|
+
* This is deliberately a named method rather than an inlined `project` at ten call sites — the
|
|
41
|
+
* node id is one decision, and it has to be made in one place or the DOT, the lock dropdown and
|
|
42
|
+
* the responsibilities cards drift apart and the hover/lock wiring silently stops matching.
|
|
43
|
+
*/
|
|
44
|
+
getNodeId(projectKey: string): string;
|
|
45
|
+
/**
|
|
46
|
+
* The human-facing display name: scope stripped.
|
|
13
47
|
* '@scope/name' → 'name'
|
|
14
48
|
* 'name' → 'name'
|
|
49
|
+
*
|
|
50
|
+
* LABELS ONLY. Never use this as an identity — see this file's header for what that cost.
|
|
15
51
|
*/
|
|
16
52
|
getShortName(name: string): string;
|
|
53
|
+
/**
|
|
54
|
+
* Assert that every project draws as its own node.
|
|
55
|
+
*
|
|
56
|
+
* Fails LOUDLY, naming both colliding project keys and their levels, because the failure mode it
|
|
57
|
+
* guards is a graph that renders happily and says something false.
|
|
58
|
+
*/
|
|
59
|
+
assertUniqueNodeIds(owners: NodeIdOwner[]): void;
|
|
17
60
|
}
|
package/src/lib/graph-names.js
CHANGED
|
@@ -2,23 +2,104 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Graph Names
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
5
|
+
* The two names a project has in the visualization, kept apart on purpose:
|
|
6
|
+
*
|
|
7
|
+
* - its NODE ID — the identity every DOT node, edge endpoint, `rank=same` member, lock-dropdown
|
|
8
|
+
* value and responsibilities `data-node` attribute is keyed on. It is the project KEY, verbatim.
|
|
9
|
+
* - its SHORT NAME — the human-facing label, with the `@scope/` prefix stripped.
|
|
10
|
+
*
|
|
11
|
+
* They used to be the same string, and that was a silent, badly-wrong-picture bug. A workspace may
|
|
12
|
+
* legitimately hold `@scope/public-api` (an L0 contract lib) AND `public-api` (the L6 server that
|
|
13
|
+
* serves it) — two DISTINCT projects that strip to the same short name. Keying the DOT on the short
|
|
14
|
+
* name fused them into one node, and graphviz UNIONS any two `rank=same` sets sharing a node, so
|
|
15
|
+
* L0's rank and L6's rank became one rank: the entire L0 band rendered on the L6 row, the invisible
|
|
16
|
+
* anchor chain that pins level order became unsatisfiable (it demands L0 strictly below L6 while the
|
|
17
|
+
* union says they are equal) and was discarded, and the server's dependency on the contract lib drew
|
|
18
|
+
* as a self-loop on the fused box.
|
|
19
|
+
*
|
|
20
|
+
* So identity is the project key and nothing else — collision is structurally impossible now rather
|
|
21
|
+
* than merely unlikely — and {@link GraphNames.assertUniqueNodeIds} asserts that, so a future change
|
|
22
|
+
* that reintroduces a lossy id fails generation loudly instead of drawing a wrong graph in silence.
|
|
23
|
+
*
|
|
24
|
+
* Kept in its own class so GraphVisualizer and ResponsibilitiesRenderer can both depend on it
|
|
25
|
+
* without a circular dependency between them.
|
|
10
26
|
*/
|
|
11
27
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
-
exports.GraphNames = void 0;
|
|
28
|
+
exports.GraphNames = exports.NodeIdCollisionError = exports.NodeIdOwner = void 0;
|
|
29
|
+
/** One project key and the level it sits on — what a collision report has to name. */
|
|
30
|
+
class NodeIdOwner {
|
|
31
|
+
projectKey;
|
|
32
|
+
level;
|
|
33
|
+
constructor(projectKey, level) {
|
|
34
|
+
this.projectKey = projectKey;
|
|
35
|
+
this.level = level;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
exports.NodeIdOwner = NodeIdOwner;
|
|
39
|
+
/** Thrown when two distinct project keys would be drawn as the same DOT node. */
|
|
40
|
+
class NodeIdCollisionError extends Error {
|
|
41
|
+
constructor(message) {
|
|
42
|
+
super(message);
|
|
43
|
+
this.name = 'NodeIdCollisionError';
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
exports.NodeIdCollisionError = NodeIdCollisionError;
|
|
13
47
|
class GraphNames {
|
|
14
48
|
/**
|
|
15
|
-
*
|
|
49
|
+
* The DOT/DOM identity of a project: its project KEY, unchanged.
|
|
50
|
+
*
|
|
51
|
+
* This is deliberately a named method rather than an inlined `project` at ten call sites — the
|
|
52
|
+
* node id is one decision, and it has to be made in one place or the DOT, the lock dropdown and
|
|
53
|
+
* the responsibilities cards drift apart and the hover/lock wiring silently stops matching.
|
|
54
|
+
*/
|
|
55
|
+
getNodeId(projectKey) {
|
|
56
|
+
return projectKey;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* The human-facing display name: scope stripped.
|
|
16
60
|
* '@scope/name' → 'name'
|
|
17
61
|
* 'name' → 'name'
|
|
62
|
+
*
|
|
63
|
+
* LABELS ONLY. Never use this as an identity — see this file's header for what that cost.
|
|
18
64
|
*/
|
|
19
65
|
getShortName(name) {
|
|
20
66
|
return name.includes('/') ? name.split('/').pop() : name;
|
|
21
67
|
}
|
|
68
|
+
/**
|
|
69
|
+
* Assert that every project draws as its own node.
|
|
70
|
+
*
|
|
71
|
+
* Fails LOUDLY, naming both colliding project keys and their levels, because the failure mode it
|
|
72
|
+
* guards is a graph that renders happily and says something false.
|
|
73
|
+
*/
|
|
74
|
+
assertUniqueNodeIds(owners) {
|
|
75
|
+
const byNodeId = new Map();
|
|
76
|
+
for (const owner of owners) {
|
|
77
|
+
const nodeId = this.getNodeId(owner.projectKey);
|
|
78
|
+
const existing = byNodeId.get(nodeId);
|
|
79
|
+
if (existing === undefined)
|
|
80
|
+
byNodeId.set(nodeId, [owner]);
|
|
81
|
+
else
|
|
82
|
+
existing.push(owner);
|
|
83
|
+
}
|
|
84
|
+
const collisions = [];
|
|
85
|
+
for (const nodeId of [...byNodeId.keys()].sort()) {
|
|
86
|
+
const sharers = byNodeId.get(nodeId);
|
|
87
|
+
if (sharers.length < 2)
|
|
88
|
+
continue;
|
|
89
|
+
const named = sharers
|
|
90
|
+
.map((owner) => `"${owner.projectKey}" (L${owner.level})`)
|
|
91
|
+
.sort()
|
|
92
|
+
.join(' and ');
|
|
93
|
+
collisions.push(` node "${nodeId}" would be drawn for ${named}`);
|
|
94
|
+
}
|
|
95
|
+
if (collisions.length === 0)
|
|
96
|
+
return;
|
|
97
|
+
throw new NodeIdCollisionError('Two distinct projects map to the same graph node, so the rendered architecture graph '
|
|
98
|
+
+ 'would fuse them (and fuse their dependency levels into one row):\n'
|
|
99
|
+
+ collisions.sort().join('\n')
|
|
100
|
+
+ '\nFix: getNodeId() must stay a 1:1 function of the project key — it is the project '
|
|
101
|
+
+ 'key verbatim. Only getShortName() may drop the scope, and only for LABELS.');
|
|
102
|
+
}
|
|
22
103
|
}
|
|
23
104
|
exports.GraphNames = GraphNames;
|
|
24
105
|
//# sourceMappingURL=graph-names.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"graph-names.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/graph-names.ts"],"names":[],"mappings":";AAAA
|
|
1
|
+
{"version":3,"file":"graph-names.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/graph-names.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;;;AAEH,sFAAsF;AACtF,MAAa,WAAW;IAEA;IACA;IAFpB,YACoB,UAAkB,EAClB,KAAa;QADb,eAAU,GAAV,UAAU,CAAQ;QAClB,UAAK,GAAL,KAAK,CAAQ;IAC9B,CAAC;CACP;AALD,kCAKC;AAED,iFAAiF;AACjF,MAAa,oBAAqB,SAAQ,KAAK;IAC3C,YAAY,OAAe;QACvB,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,sBAAsB,CAAC;IACvC,CAAC;CACJ;AALD,oDAKC;AAED,MAAa,UAAU;IACnB;;;;;;OAMG;IACH,SAAS,CAAC,UAAkB;QACxB,OAAO,UAAU,CAAC;IACtB,CAAC;IAED;;;;;;OAMG;IACH,YAAY,CAAC,IAAY;QACrB,OAAO,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,EAAG,CAAC,CAAC,CAAC,IAAI,CAAC;IAC9D,CAAC;IAED;;;;;OAKG;IACH,mBAAmB,CAAC,MAAqB;QACrC,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAyB,CAAC;QAClD,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;YACzB,MAAM,MAAM,GAAG,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC;YAChD,MAAM,QAAQ,GAAG,QAAQ,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;YACtC,IAAI,QAAQ,KAAK,SAAS;gBAAE,QAAQ,CAAC,GAAG,CAAC,MAAM,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC;;gBACrD,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAC9B,CAAC;QACD,MAAM,UAAU,GAAa,EAAE,CAAC;QAChC,KAAK,MAAM,MAAM,IAAI,CAAC,GAAG,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC;YAC/C,MAAM,OAAO,GAAG,QAAQ,CAAC,GAAG,CAAC,MAAM,CAAkB,CAAC;YACtD,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC;gBAAE,SAAS;YACjC,MAAM,KAAK,GAAG,OAAO;iBAChB,GAAG,CAAC,CAAC,KAAkB,EAAU,EAAE,CAAC,IAAI,KAAK,CAAC,UAAU,OAAO,KAAK,CAAC,KAAK,GAAG,CAAC;iBAC9E,IAAI,EAAE;iBACN,IAAI,CAAC,OAAO,CAAC,CAAC;YACnB,UAAU,CAAC,IAAI,CAAC,WAAW,MAAM,wBAAwB,KAAK,EAAE,CAAC,CAAC;QACtE,CAAC;QACD,IAAI,UAAU,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO;QACpC,MAAM,IAAI,oBAAoB,CAC1B,uFAAuF;cACrF,oEAAoE;cACpE,UAAU,CAAC,IAAI,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC;cAC5B,qFAAqF;cACrF,4EAA4E,CAAC,CAAC;IACxF,CAAC;CACJ;AAvDD,gCAuDC","sourcesContent":["/**\n * Graph Names\n *\n * The two names a project has in the visualization, kept apart on purpose:\n *\n * - its NODE ID — the identity every DOT node, edge endpoint, `rank=same` member, lock-dropdown\n * value and responsibilities `data-node` attribute is keyed on. It is the project KEY, verbatim.\n * - its SHORT NAME — the human-facing label, with the `@scope/` prefix stripped.\n *\n * They used to be the same string, and that was a silent, badly-wrong-picture bug. A workspace may\n * legitimately hold `@scope/public-api` (an L0 contract lib) AND `public-api` (the L6 server that\n * serves it) — two DISTINCT projects that strip to the same short name. Keying the DOT on the short\n * name fused them into one node, and graphviz UNIONS any two `rank=same` sets sharing a node, so\n * L0's rank and L6's rank became one rank: the entire L0 band rendered on the L6 row, the invisible\n * anchor chain that pins level order became unsatisfiable (it demands L0 strictly below L6 while the\n * union says they are equal) and was discarded, and the server's dependency on the contract lib drew\n * as a self-loop on the fused box.\n *\n * So identity is the project key and nothing else — collision is structurally impossible now rather\n * than merely unlikely — and {@link GraphNames.assertUniqueNodeIds} asserts that, so a future change\n * that reintroduces a lossy id fails generation loudly instead of drawing a wrong graph in silence.\n *\n * Kept in its own class so GraphVisualizer and ResponsibilitiesRenderer can both depend on it\n * without a circular dependency between them.\n */\n\n/** One project key and the level it sits on — what a collision report has to name. */\nexport class NodeIdOwner {\n constructor(\n public readonly projectKey: string,\n public readonly level: number,\n ) {}\n}\n\n/** Thrown when two distinct project keys would be drawn as the same DOT node. */\nexport class NodeIdCollisionError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'NodeIdCollisionError';\n }\n}\n\nexport class GraphNames {\n /**\n * The DOT/DOM identity of a project: its project KEY, unchanged.\n *\n * This is deliberately a named method rather than an inlined `project` at ten call sites — the\n * node id is one decision, and it has to be made in one place or the DOT, the lock dropdown and\n * the responsibilities cards drift apart and the hover/lock wiring silently stops matching.\n */\n getNodeId(projectKey: string): string {\n return projectKey;\n }\n\n /**\n * The human-facing display name: scope stripped.\n * '@scope/name' → 'name'\n * 'name' → 'name'\n *\n * LABELS ONLY. Never use this as an identity — see this file's header for what that cost.\n */\n getShortName(name: string): string {\n return name.includes('/') ? name.split('/').pop()! : name;\n }\n\n /**\n * Assert that every project draws as its own node.\n *\n * Fails LOUDLY, naming both colliding project keys and their levels, because the failure mode it\n * guards is a graph that renders happily and says something false.\n */\n assertUniqueNodeIds(owners: NodeIdOwner[]): void {\n const byNodeId = new Map<string, NodeIdOwner[]>();\n for (const owner of owners) {\n const nodeId = this.getNodeId(owner.projectKey);\n const existing = byNodeId.get(nodeId);\n if (existing === undefined) byNodeId.set(nodeId, [owner]);\n else existing.push(owner);\n }\n const collisions: string[] = [];\n for (const nodeId of [...byNodeId.keys()].sort()) {\n const sharers = byNodeId.get(nodeId) as NodeIdOwner[];\n if (sharers.length < 2) continue;\n const named = sharers\n .map((owner: NodeIdOwner): string => `\"${owner.projectKey}\" (L${owner.level})`)\n .sort()\n .join(' and ');\n collisions.push(` node \"${nodeId}\" would be drawn for ${named}`);\n }\n if (collisions.length === 0) return;\n throw new NodeIdCollisionError(\n 'Two distinct projects map to the same graph node, so the rendered architecture graph '\n + 'would fuse them (and fuse their dependency levels into one row):\\n'\n + collisions.sort().join('\\n')\n + '\\nFix: getNodeId() must stay a 1:1 function of the project key — it is the project '\n + 'key verbatim. Only getShortName() may drop the scope, and only for LABELS.');\n }\n}\n"]}
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* dependency level HIGH → LOW (top-level apps first, deepest libs last) so a
|
|
7
7
|
* reader scrolls from the runnable servers/clients down into the libraries.
|
|
8
8
|
*
|
|
9
|
-
* Each card carries `data-node="<
|
|
9
|
+
* Each card carries `data-node="<projectKey>"` matching the graph box's title, so
|
|
10
10
|
* the page script can filter the list to just the locked box's chain (see
|
|
11
11
|
* graph-visualizer.client.ts). The summary paragraph is the `shortDescription`
|
|
12
12
|
* already on each graph entry; the expanded body is the rendered markdown file.
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* dependency level HIGH → LOW (top-level apps first, deepest libs last) so a
|
|
8
8
|
* reader scrolls from the runnable servers/clients down into the libraries.
|
|
9
9
|
*
|
|
10
|
-
* Each card carries `data-node="<
|
|
10
|
+
* Each card carries `data-node="<projectKey>"` matching the graph box's title, so
|
|
11
11
|
* the page script can filter the list to just the locked box's chain (see
|
|
12
12
|
* graph-visualizer.client.ts). The summary paragraph is the `shortDescription`
|
|
13
13
|
* already on each graph entry; the expanded body is the rendered markdown file.
|
|
@@ -56,11 +56,15 @@ class ResponsibilitiesRenderer {
|
|
|
56
56
|
`</section>`);
|
|
57
57
|
}
|
|
58
58
|
renderCard(project, entry, workspaceRoot) {
|
|
59
|
+
// data-node is the graph box's IDENTITY (the project key, matching the SVG <title>); the
|
|
60
|
+
// heading stays the pretty short name. Keying the attribute on the short name is what let
|
|
61
|
+
// two same-named projects share one card's visibility — see graph-names.ts.
|
|
62
|
+
const nodeId = this.names.getNodeId(project);
|
|
59
63
|
const shortName = this.names.getShortName(project);
|
|
60
64
|
const summary = entry.shortDescription ?? '';
|
|
61
65
|
const summaryHtml = summary ? ` — ${this.escapeHtml(summary)}` : '';
|
|
62
66
|
const body = this.readBody(entry, workspaceRoot);
|
|
63
|
-
return (`<details class="wp-resp-card" data-node="${this.escapeHtml(
|
|
67
|
+
return (`<details class="wp-resp-card" data-node="${this.escapeHtml(nodeId)}">` +
|
|
64
68
|
`<summary><span class="wp-resp-level">L${entry.level}</span> ` +
|
|
65
69
|
`<strong>${this.escapeHtml(shortName)}</strong>${summaryHtml}</summary>` +
|
|
66
70
|
`<div class="wp-resp-body">${body}</div>` +
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"graph-responsibilities.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/graph-responsibilities.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;GAYG;;;;AAEH,+CAAyB;AACzB,mDAA6B;AAE7B,+CAA2C;AAE3C;;;;GAIG;AACH,MAAM,iBAAiB;IACnB,MAAM,GAAG,KAAK,CAAC;CAClB;AAED,MAAa,wBAAwB;IAChB,KAAK,GAAG,IAAI,wBAAU,EAAE,CAAC;IAE1C;;;;OAIG;IACH,eAAe,CAAC,KAAoB,EAAE,aAAqB;QACvD,MAAM,QAAQ,GAAG,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACpC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAS,EAAE,CAAS,EAAU,EAAE;YAC3C,MAAM,SAAS,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC;YAClD,IAAI,SAAS,KAAK,CAAC;gBAAE,OAAO,SAAS,CAAC;YACtC,OAAO,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC;QAC9B,CAAC,CAAC,CAAC;QAEH,MAAM,KAAK,GAAa,EAAE,CAAC;QAC3B,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;YAC7B,qEAAqE;YACrE,4CAA4C;YAC5C,IAAI,KAAK,CAAC,OAAO,CAAC,CAAC,WAAW,KAAK,KAAK;gBAAE,SAAS;YACnD,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,OAAO,EAAE,KAAK,CAAC,OAAO,CAAC,EAAE,aAAa,CAAC,CAAC,CAAC;QACxE,CAAC;QAED,OAAO,CACH,oCAAoC;YACpC,8CAA8C;YAC9C,+FAA+F;YAC/F,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC;YAChB,YAAY,CACf,CAAC;IACN,CAAC;IAEO,UAAU,CAAC,OAAe,EAAE,KAAiB,EAAE,aAAqB;QACxE,MAAM,SAAS,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC;QACnD,MAAM,OAAO,GAAG,KAAK,CAAC,gBAAgB,IAAI,EAAE,CAAC;QAC7C,MAAM,WAAW,GAAG,OAAO,CAAC,CAAC,CAAC,MAAM,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACpE,MAAM,IAAI,GAAG,IAAI,CAAC,QAAQ,CAAC,KAAK,EAAE,aAAa,CAAC,CAAC;QACjD,OAAO,CACH,4CAA4C,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC,IAAI;YAC1E,yCAAyC,KAAK,CAAC,KAAK,UAAU;YAC9D,WAAW,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC,YAAY,WAAW,YAAY;YACxE,6BAA6B,IAAI,QAAQ;YACzC,YAAY,CACf,CAAC;IACN,CAAC;IAED;;;;OAIG;IACK,QAAQ,CAAC,KAAiB,EAAE,aAAqB;QACrD,MAAM,IAAI,GAAG,KAAK,CAAC,oBAAoB,CAAC;QACxC,IAAI,CAAC,IAAI;YAAE,OAAO,kEAAkE,CAAC;QACrF,MAAM,YAAY,GAAG,IAAI,CAAC,IAAI,CAAC,aAAa,EAAE,IAAI,CAAC,CAAC;QACpD,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,YAAY,CAAC,EAAE,CAAC;YAC/B,OAAO,kBAAkB,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,YAAY,CAAC;QAC/D,CAAC;QACD,OAAO,IAAI,CAAC,cAAc,CAAC,EAAE,CAAC,YAAY,CAAC,YAAY,EAAE,OAAO,CAAC,CAAC,CAAC;IACvE,CAAC;IAED;;;;;;OAMG;IACK,cAAc,CAAC,QAAgB;QACnC,MAAM,GAAG,GAAa,EAAE,CAAC;QACzB,MAAM,KAAK,GAAG,IAAI,iBAAiB,EAAE,CAAC;QACtC,KAAK,MAAM,OAAO,IAAI,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;YACzC,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,IAAI,EAAE,EAAE,GAAG,EAAE,KAAK,CAAC,CAAC;QAChD,CAAC;QACD,IAAI,KAAK,CAAC,MAAM;YAAE,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QACpC,OAAO,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC1B,CAAC;IAEO,UAAU,CAAC,IAAY,EAAE,GAAa,EAAE,KAAwB;QACpE,MAAM,OAAO,GAAG,mBAAmB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC/C,MAAM,MAAM,GAAG,eAAe,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC1C,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACpB,IAAI,KAAK,CAAC,MAAM,EAAE,CAAC;gBAAC,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;gBAAC,KAAK,CAAC,MAAM,GAAG,KAAK,CAAC;YAAC,CAAC;YAC9D,OAAO;QACX,CAAC;QACD,IAAI,OAAO,EAAE,CAAC;YACV,IAAI,KAAK,CAAC,MAAM,EAAE,CAAC;gBAAC,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;gBAAC,KAAK,CAAC,MAAM,GAAG,KAAK,CAAC;YAAC,CAAC;YAC9D,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,+BAA+B;YACjF,GAAG,CAAC,IAAI,CAAC,KAAK,KAAK,IAAI,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,KAAK,GAAG,CAAC,CAAC;YACrF,OAAO;QACX,CAAC;QACD,IAAI,MAAM,EAAE,CAAC;YACT,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC;gBAAC,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;gBAAC,KAAK,CAAC,MAAM,GAAG,IAAI,CAAC;YAAC,CAAC;YAC7D,GAAG,CAAC,IAAI,CAAC,OAAO,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC;YACtE,OAAO;QACX,CAAC;QACD,IAAI,KAAK,CAAC,MAAM,EAAE,CAAC;YAAC,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;YAAC,KAAK,CAAC,MAAM,GAAG,KAAK,CAAC;QAAC,CAAC;QAC9D,GAAG,CAAC,IAAI,CAAC,MAAM,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,CAAC;IACnE,CAAC;IAED;;;OAGG;IACK,YAAY,CAAC,OAAe;QAChC,OAAO,OAAO;aACT,OAAO,CAAC,YAAY,EAAE,iBAAiB,CAAC;aACxC,OAAO,CAAC,kBAAkB,EAAE,qBAAqB,CAAC;aAClD,OAAO,CAAC,sBAAsB,EAAE,eAAe,CAAC,CAAC;IAC1D,CAAC;IAED;;OAEG;IACK,UAAU,CAAC,IAAY;QAC3B,OAAO,IAAI;aACN,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC;aACtB,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC;aACrB,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC;aACrB,OAAO,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;IACjC,CAAC;CACJ;AA1HD,4DA0HC","sourcesContent":["/**\n * Responsibilities Section\n *\n * Renders every module's full `responsibilities.md` as a collapsible card below\n * the dependency graph in architecture/dependencies.html. Cards are ordered by\n * dependency level HIGH → LOW (top-level apps first, deepest libs last) so a\n * reader scrolls from the runnable servers/clients down into the libraries.\n *\n * Each card carries `data-node=\"<shortName>\"` matching the graph box's title, so\n * the page script can filter the list to just the locked box's chain (see\n * graph-visualizer.client.ts). The summary paragraph is the `shortDescription`\n * already on each graph entry; the expanded body is the rendered markdown file.\n */\n\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport type { EnhancedGraph, GraphEntry } from './graph-sorter';\nimport { GraphNames } from './graph-names';\n\n/**\n * Mutable cursor state threaded through the markdown line renderer: whether we\n * are currently inside an open `<ul>` (so the next blank line / heading / non-\n * bullet closes it).\n */\nclass MarkdownListState {\n inList = false;\n}\n\nexport class ResponsibilitiesRenderer {\n private readonly names = new GraphNames();\n\n /**\n * Build the responsibilities section HTML: one collapsible card per module,\n * sorted by level descending (tie-break by name ascending). Returned as a\n * <section> that GraphVisualizer injects below the graph.\n */\n generateSection(graph: EnhancedGraph, workspaceRoot: string): string {\n const projects = Object.keys(graph);\n projects.sort((a: string, b: string): number => {\n const levelDiff = graph[b].level - graph[a].level;\n if (levelDiff !== 0) return levelDiff;\n return a.localeCompare(b);\n });\n\n const cards: string[] = [];\n for (const project of projects) {\n // Hidden projects (drawOnGraph:false) are omitted from the graph, so\n // they get no responsibilities card either.\n if (graph[project].drawOnGraph === false) continue;\n cards.push(this.renderCard(project, graph[project], workspaceRoot));\n }\n\n return (\n `<section id=\"wp-responsibilities\">` +\n `<h2>Responsibilities (level high → low)</h2>` +\n `<p class=\"hint\">Lock a box above to narrow this list to just that box's dependency chain.</p>` +\n cards.join('\\n') +\n `</section>`\n );\n }\n\n private renderCard(project: string, entry: GraphEntry, workspaceRoot: string): string {\n const shortName = this.names.getShortName(project);\n const summary = entry.shortDescription ?? '';\n const summaryHtml = summary ? ` — ${this.escapeHtml(summary)}` : '';\n const body = this.readBody(entry, workspaceRoot);\n return (\n `<details class=\"wp-resp-card\" data-node=\"${this.escapeHtml(shortName)}\">` +\n `<summary><span class=\"wp-resp-level\">L${entry.level}</span> ` +\n `<strong>${this.escapeHtml(shortName)}</strong>${summaryHtml}</summary>` +\n `<div class=\"wp-resp-body\">${body}</div>` +\n `</details>`\n );\n }\n\n /**\n * Read a module's responsibilities.md body. Generation already guarantees the\n * file exists (metadata validation throws otherwise); a fallback line guards\n * the edge case where the graph JSON is stale relative to disk.\n */\n private readBody(entry: GraphEntry, workspaceRoot: string): string {\n const file = entry.responsibilitiesFile;\n if (!file) return '<p><em>No responsibilities.md recorded for this module.</em></p>';\n const absolutePath = path.join(workspaceRoot, file);\n if (!fs.existsSync(absolutePath)) {\n return `<p><em>Missing ${this.escapeHtml(file)}.</em></p>`;\n }\n return this.renderMarkdown(fs.readFileSync(absolutePath, 'utf-8'));\n }\n\n /**\n * Minimal markdown → HTML for a responsibilities.md body. Handles ATX headings\n * (`#`..`######`), `-`/`*` bullet lists, and blank-line-separated paragraphs.\n * Everything is HTML-escaped first; only a small inline set is re-marked. This\n * is deliberately tiny — responsibilities.md files are short and structured,\n * and the repo has no markdown dependency.\n */\n private renderMarkdown(markdown: string): string {\n const out: string[] = [];\n const state = new MarkdownListState();\n for (const rawLine of markdown.split('\\n')) {\n this.renderLine(rawLine.trim(), out, state);\n }\n if (state.inList) out.push('</ul>');\n return out.join('\\n');\n }\n\n private renderLine(line: string, out: string[], state: MarkdownListState): void {\n const heading = /^(#{1,6})\\s+(.*)$/.exec(line);\n const bullet = /^[-*]\\s+(.*)$/.exec(line);\n if (line.length === 0) {\n if (state.inList) { out.push('</ul>'); state.inList = false; }\n return;\n }\n if (heading) {\n if (state.inList) { out.push('</ul>'); state.inList = false; }\n const level = Math.min(heading[1].length + 3, 6); // # → h4, ## → h5, deeper → h6\n out.push(`<h${level}>${this.renderInline(this.escapeHtml(heading[2]))}</h${level}>`);\n return;\n }\n if (bullet) {\n if (!state.inList) { out.push('<ul>'); state.inList = true; }\n out.push(`<li>${this.renderInline(this.escapeHtml(bullet[1]))}</li>`);\n return;\n }\n if (state.inList) { out.push('</ul>'); state.inList = false; }\n out.push(`<p>${this.renderInline(this.escapeHtml(line))}</p>`);\n }\n\n /**\n * Render inline markdown spans within an already HTML-escaped line: `code` →\n * <code>, **bold** → <strong>, *italic* → <em>.\n */\n private renderInline(escaped: string): string {\n return escaped\n .replace(/`([^`]+)`/g, '<code>$1</code>')\n .replace(/\\*\\*([^*]+)\\*\\*/g, '<strong>$1</strong>')\n .replace(/(^|[^*])\\*([^*]+)\\*/g, '$1<em>$2</em>');\n }\n\n /**\n * Escape a string for safe embedding in HTML text/attributes.\n */\n private escapeHtml(text: string): string {\n return text\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"');\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"graph-responsibilities.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/graph-responsibilities.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Responsibilities Section\n *\n * Renders every module's full `responsibilities.md` as a collapsible card below\n * the dependency graph in architecture/dependencies.html. Cards are ordered by\n * dependency level HIGH → LOW (top-level apps first, deepest libs last) so a\n * reader scrolls from the runnable servers/clients down into the libraries.\n *\n * Each card carries `data-node=\"<projectKey>\"` matching the graph box's title, so\n * the page script can filter the list to just the locked box's chain (see\n * graph-visualizer.client.ts). The summary paragraph is the `shortDescription`\n * already on each graph entry; the expanded body is the rendered markdown file.\n */\n\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport type { EnhancedGraph, GraphEntry } from './graph-sorter';\nimport { GraphNames } from './graph-names';\n\n/**\n * Mutable cursor state threaded through the markdown line renderer: whether we\n * are currently inside an open `<ul>` (so the next blank line / heading / non-\n * bullet closes it).\n */\nclass MarkdownListState {\n inList = false;\n}\n\nexport class ResponsibilitiesRenderer {\n private readonly names = new GraphNames();\n\n /**\n * Build the responsibilities section HTML: one collapsible card per module,\n * sorted by level descending (tie-break by name ascending). Returned as a\n * <section> that GraphVisualizer injects below the graph.\n */\n generateSection(graph: EnhancedGraph, workspaceRoot: string): string {\n const projects = Object.keys(graph);\n projects.sort((a: string, b: string): number => {\n const levelDiff = graph[b].level - graph[a].level;\n if (levelDiff !== 0) return levelDiff;\n return a.localeCompare(b);\n });\n\n const cards: string[] = [];\n for (const project of projects) {\n // Hidden projects (drawOnGraph:false) are omitted from the graph, so\n // they get no responsibilities card either.\n if (graph[project].drawOnGraph === false) continue;\n cards.push(this.renderCard(project, graph[project], workspaceRoot));\n }\n\n return (\n `<section id=\"wp-responsibilities\">` +\n `<h2>Responsibilities (level high → low)</h2>` +\n `<p class=\"hint\">Lock a box above to narrow this list to just that box's dependency chain.</p>` +\n cards.join('\\n') +\n `</section>`\n );\n }\n\n private renderCard(project: string, entry: GraphEntry, workspaceRoot: string): string {\n // data-node is the graph box's IDENTITY (the project key, matching the SVG <title>); the\n // heading stays the pretty short name. Keying the attribute on the short name is what let\n // two same-named projects share one card's visibility — see graph-names.ts.\n const nodeId = this.names.getNodeId(project);\n const shortName = this.names.getShortName(project);\n const summary = entry.shortDescription ?? '';\n const summaryHtml = summary ? ` — ${this.escapeHtml(summary)}` : '';\n const body = this.readBody(entry, workspaceRoot);\n return (\n `<details class=\"wp-resp-card\" data-node=\"${this.escapeHtml(nodeId)}\">` +\n `<summary><span class=\"wp-resp-level\">L${entry.level}</span> ` +\n `<strong>${this.escapeHtml(shortName)}</strong>${summaryHtml}</summary>` +\n `<div class=\"wp-resp-body\">${body}</div>` +\n `</details>`\n );\n }\n\n /**\n * Read a module's responsibilities.md body. Generation already guarantees the\n * file exists (metadata validation throws otherwise); a fallback line guards\n * the edge case where the graph JSON is stale relative to disk.\n */\n private readBody(entry: GraphEntry, workspaceRoot: string): string {\n const file = entry.responsibilitiesFile;\n if (!file) return '<p><em>No responsibilities.md recorded for this module.</em></p>';\n const absolutePath = path.join(workspaceRoot, file);\n if (!fs.existsSync(absolutePath)) {\n return `<p><em>Missing ${this.escapeHtml(file)}.</em></p>`;\n }\n return this.renderMarkdown(fs.readFileSync(absolutePath, 'utf-8'));\n }\n\n /**\n * Minimal markdown → HTML for a responsibilities.md body. Handles ATX headings\n * (`#`..`######`), `-`/`*` bullet lists, and blank-line-separated paragraphs.\n * Everything is HTML-escaped first; only a small inline set is re-marked. This\n * is deliberately tiny — responsibilities.md files are short and structured,\n * and the repo has no markdown dependency.\n */\n private renderMarkdown(markdown: string): string {\n const out: string[] = [];\n const state = new MarkdownListState();\n for (const rawLine of markdown.split('\\n')) {\n this.renderLine(rawLine.trim(), out, state);\n }\n if (state.inList) out.push('</ul>');\n return out.join('\\n');\n }\n\n private renderLine(line: string, out: string[], state: MarkdownListState): void {\n const heading = /^(#{1,6})\\s+(.*)$/.exec(line);\n const bullet = /^[-*]\\s+(.*)$/.exec(line);\n if (line.length === 0) {\n if (state.inList) { out.push('</ul>'); state.inList = false; }\n return;\n }\n if (heading) {\n if (state.inList) { out.push('</ul>'); state.inList = false; }\n const level = Math.min(heading[1].length + 3, 6); // # → h4, ## → h5, deeper → h6\n out.push(`<h${level}>${this.renderInline(this.escapeHtml(heading[2]))}</h${level}>`);\n return;\n }\n if (bullet) {\n if (!state.inList) { out.push('<ul>'); state.inList = true; }\n out.push(`<li>${this.renderInline(this.escapeHtml(bullet[1]))}</li>`);\n return;\n }\n if (state.inList) { out.push('</ul>'); state.inList = false; }\n out.push(`<p>${this.renderInline(this.escapeHtml(line))}</p>`);\n }\n\n /**\n * Render inline markdown spans within an already HTML-escaped line: `code` →\n * <code>, **bold** → <strong>, *italic* → <em>.\n */\n private renderInline(escaped: string): string {\n return escaped\n .replace(/`([^`]+)`/g, '<code>$1</code>')\n .replace(/\\*\\*([^*]+)\\*\\*/g, '<strong>$1</strong>')\n .replace(/(^|[^*])\\*([^*]+)\\*/g, '$1<em>$2</em>');\n }\n\n /**\n * Escape a string for safe embedding in HTML text/attributes.\n */\n private escapeHtml(text: string): string {\n return text\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"');\n }\n}\n"]}
|
package/src/lib/graph-sorter.js
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
11
|
exports.computeTopologicalLayers = computeTopologicalLayers;
|
|
12
12
|
exports.sortGraphTopologically = sortGraphTopologically;
|
|
13
|
+
const graph_cycles_1 = require("./graph-cycles");
|
|
13
14
|
/**
|
|
14
15
|
* Compute topological layers for dependency graph using Kahn's algorithm
|
|
15
16
|
*
|
|
@@ -36,13 +37,13 @@ function computeTopologicalLayers(graph) {
|
|
|
36
37
|
}
|
|
37
38
|
}
|
|
38
39
|
if (currentLayer.length === 0) {
|
|
39
|
-
// No progress made = circular dependency
|
|
40
|
+
// No progress made = circular dependency. ProjectCycleDetector is the ONE cycle
|
|
41
|
+
// implementation in this package — it names every chain, in project keys, with the
|
|
42
|
+
// fix — so this reports through it rather than keeping a second, weaker DFS here.
|
|
43
|
+
new graph_cycles_1.ProjectCycleDetector().assertAcyclic(graph, 'the project graph');
|
|
44
|
+
// Unreachable: Kahn only stalls on a cycle, and the assert above throws on every one.
|
|
40
45
|
const remaining = allProjects.filter((p) => !processed.has(p));
|
|
41
|
-
|
|
42
|
-
const cycleInfo = findCycle(graph, remaining);
|
|
43
|
-
throw new Error(`Circular dependency detected among: ${remaining.join(', ')}\n` +
|
|
44
|
-
(cycleInfo ? `Cycle: ${cycleInfo}\n` : '') +
|
|
45
|
-
'Fix: Remove one of the dependencies to break the cycle.');
|
|
46
|
+
throw new Error(`Topological sort stalled with no cycle to report — projects left unlayered: ${remaining.join(', ')}`);
|
|
46
47
|
}
|
|
47
48
|
// Sort alphabetically within layer for deterministic output
|
|
48
49
|
currentLayer.sort();
|
|
@@ -52,39 +53,6 @@ function computeTopologicalLayers(graph) {
|
|
|
52
53
|
}
|
|
53
54
|
return layers;
|
|
54
55
|
}
|
|
55
|
-
/**
|
|
56
|
-
* Try to find and describe a cycle in the graph
|
|
57
|
-
*/
|
|
58
|
-
function findCycle(graph, remaining) {
|
|
59
|
-
const visited = new Set();
|
|
60
|
-
const path = [];
|
|
61
|
-
function dfs(node) {
|
|
62
|
-
if (path.includes(node)) {
|
|
63
|
-
const cycleStart = path.indexOf(node);
|
|
64
|
-
return [...path.slice(cycleStart), node].join(' -> ');
|
|
65
|
-
}
|
|
66
|
-
if (visited.has(node))
|
|
67
|
-
return null;
|
|
68
|
-
visited.add(node);
|
|
69
|
-
path.push(node);
|
|
70
|
-
const deps = graph[node] || [];
|
|
71
|
-
for (const dep of deps) {
|
|
72
|
-
if (remaining.includes(dep)) {
|
|
73
|
-
const result = dfs(dep);
|
|
74
|
-
if (result)
|
|
75
|
-
return result;
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
path.pop();
|
|
79
|
-
return null;
|
|
80
|
-
}
|
|
81
|
-
for (const node of remaining) {
|
|
82
|
-
const cycle = dfs(node);
|
|
83
|
-
if (cycle)
|
|
84
|
-
return cycle;
|
|
85
|
-
}
|
|
86
|
-
return null;
|
|
87
|
-
}
|
|
88
56
|
/**
|
|
89
57
|
* Sort graph in topological order with alphabetical sorting within layers
|
|
90
58
|
* Returns enhanced format with level metadata
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"graph-sorter.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/graph-sorter.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Graph Sorter\n *\n * Performs topological sorting on the dependency graph to:\n * 1. Detect circular dependencies (fails if cycle found)\n * 2. Assign level numbers to each project (level 0 = no deps, level 1 = depends on level 0, etc.)\n * 3. Group projects into layers for deterministic ordering\n */\n\nimport type { ProjectApiRelations } from './api-usage/api-relations';\n\n/**\n * Graph entry with level metadata plus AI-oriented metadata filled in by\n * enrichGraph() (lib/graph-metadata.ts) before the graph is saved:\n * - framework: the project's libType — the SET of runtime environments it is\n * validated to run in, drawn from browser | react | angular | node | express\n * (e.g. [\"browser\",\"node\"]); from its `framework:` nx tags (source of truth)\n * or inferred from package.json deps\n * - shortDescription: summary extracted from the project's responsibilities.md\n * - responsibilitiesFile: repo-relative path to the FULL responsibilities doc\n * - designFile: repo-relative path to the generated DI design.json (only for\n * project.json projects)\n * - apiRelations: for each api-lib in `dependsOn`, WHY the edge exists — the\n * APIs this project implements (serves) and/or uses (calls), each with its\n * transport (rpc | pubsub). Derived by scanning source (see api-usage/).\n */\nexport interface GraphEntry {\n level: number;\n dependsOn: string[];\n framework?: string[];\n role?: string;\n /**\n * The name CLIENTS address this app by at runtime (`new ClientConfig('helper-fsdb')`) — its\n * deployed service name, DECLARED in project.json as metadata.webpieces.serviceName. Absent for\n * anything nothing calls by name (libraries, browser apps). See service-name-resolver.ts.\n */\n serviceName?: string;\n /**\n * The service(s) this project's clients call when the call site cannot carry a literal\n * `ClientConfig` — the symmetric half of `serviceName`. DECLARED in project.json as\n * metadata.webpieces.callsService, either a single service name (every untargeted `uses` aims\n * there) or an `{ apiClassName: serviceName }` map (a client that calls several). Absent for a\n * project that never calls anything, or one whose call sites all carry literals.\n * See service-name-resolver.ts.\n */\n callsService?: string | Record<string, string>;\n /**\n * When false, the project is hidden from the rendered architecture graphs\n * (its box AND every edge touching it are omitted from dependencies.html and\n * the runtime graph). It stays in this JSON so the data view is complete.\n * Absent means drawn (the default). From the project's `drawOnGraph:` nx tag.\n */\n drawOnGraph?: boolean;\n shortDescription?: string;\n responsibilitiesFile?: string;\n designFile?: string;\n apiRelations?: ProjectApiRelations;\n /**\n * The @webpieces runtime packages this project's OWN package.json declares — http-routing,\n * http-server, http-client-node, http-client-browser, cloudtasks-client. Omitted when it\n * declares none, so TOTAL absence across the file means the file predates this field and\n * nothing is auto-hidden (see runtime-graph.ts). Core/util/logging packages are deliberately\n * not markers; see runtime-participant-resolver.ts for why.\n *\n * The runtime graph ORs this over a node's library closure: a server whose whole webpieces\n * stack comes from a shared bootstrap lib participates even though it declares nothing itself.\n */\n webpiecesRuntime?: string[];\n}\n\n/**\n * Enhanced graph format with level information\n */\nexport type EnhancedGraph = Record<string, GraphEntry>;\n\n/**\n * Compute topological layers for dependency graph using Kahn's algorithm\n *\n * Projects are grouped into layers where each layer only depends on previous layers.\n * Throws an error if a circular dependency is detected.\n *\n * @param graph - Dependency graph { project: [deps] }\n * @returns Array of layers, each containing sorted project names\n */\nexport function computeTopologicalLayers(graph: Record<string, string[]>): string[][] {\n const layers: string[][] = [];\n const processed = new Set<string>();\n const allProjects = Object.keys(graph);\n\n while (processed.size < allProjects.length) {\n const currentLayer: string[] = [];\n\n for (const project of allProjects) {\n if (processed.has(project)) continue;\n\n const deps = graph[project] || [];\n // Check if all dependencies are in previous layers (already processed)\n const allDepsInPrevLayers = deps.every((dep) => processed.has(dep));\n\n if (allDepsInPrevLayers) {\n currentLayer.push(project);\n }\n }\n\n if (currentLayer.length === 0) {\n // No progress made = circular dependency detected\n const remaining = allProjects.filter((p) => !processed.has(p));\n\n // Try to identify the cycle\n const cycleInfo = findCycle(graph, remaining);\n\n throw new Error(\n `Circular dependency detected among: ${remaining.join(', ')}\\n` +\n (cycleInfo ? `Cycle: ${cycleInfo}\\n` : '') +\n 'Fix: Remove one of the dependencies to break the cycle.'\n );\n }\n\n // Sort alphabetically within layer for deterministic output\n currentLayer.sort();\n layers.push(currentLayer);\n\n // Mark as processed\n currentLayer.forEach((p) => processed.add(p));\n }\n\n return layers;\n}\n\n/**\n * Try to find and describe a cycle in the graph\n */\nfunction findCycle(graph: Record<string, string[]>, remaining: string[]): string | null {\n const visited = new Set<string>();\n const path: string[] = [];\n\n function dfs(node: string): string | null {\n if (path.includes(node)) {\n const cycleStart = path.indexOf(node);\n return [...path.slice(cycleStart), node].join(' -> ');\n }\n if (visited.has(node)) return null;\n\n visited.add(node);\n path.push(node);\n\n const deps = graph[node] || [];\n for (const dep of deps) {\n if (remaining.includes(dep)) {\n const result = dfs(dep);\n if (result) return result;\n }\n }\n\n path.pop();\n return null;\n }\n\n for (const node of remaining) {\n const cycle = dfs(node);\n if (cycle) return cycle;\n }\n\n return null;\n}\n\n/**\n * Sort graph in topological order with alphabetical sorting within layers\n * Returns enhanced format with level metadata\n *\n * @param graph - Unsorted dependency graph { project: [deps] }\n * @returns Sorted graph with level metadata { project: { level: number, dependsOn: [deps] } }\n */\nexport function sortGraphTopologically(graph: Record<string, string[]>): EnhancedGraph {\n const layers = computeTopologicalLayers(graph);\n const result: EnhancedGraph = {};\n\n // Add projects layer by layer (dependencies before dependents)\n layers.forEach((layer, levelIndex) => {\n for (const project of layer) {\n // Already sorted alphabetically within layer\n result[project] = {\n level: levelIndex,\n dependsOn: (graph[project] || []).sort(),\n };\n }\n });\n\n return result;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"graph-sorter.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/graph-sorter.ts"],"names":[],"mappings":";AAAA;;;;;;;GAOG;;AA8EH,4DAwCC;AASD,wDAgBC;AA5ID,iDAAsD;AAkEtD;;;;;;;;GAQG;AACH,SAAgB,wBAAwB,CAAC,KAA+B;IACpE,MAAM,MAAM,GAAe,EAAE,CAAC;IAC9B,MAAM,SAAS,GAAG,IAAI,GAAG,EAAU,CAAC;IACpC,MAAM,WAAW,GAAG,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAEvC,OAAO,SAAS,CAAC,IAAI,GAAG,WAAW,CAAC,MAAM,EAAE,CAAC;QACzC,MAAM,YAAY,GAAa,EAAE,CAAC;QAElC,KAAK,MAAM,OAAO,IAAI,WAAW,EAAE,CAAC;YAChC,IAAI,SAAS,CAAC,GAAG,CAAC,OAAO,CAAC;gBAAE,SAAS;YAErC,MAAM,IAAI,GAAG,KAAK,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC;YAClC,uEAAuE;YACvE,MAAM,mBAAmB,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,SAAS,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC;YAEpE,IAAI,mBAAmB,EAAE,CAAC;gBACtB,YAAY,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;YAC/B,CAAC;QACL,CAAC;QAED,IAAI,YAAY,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC5B,gFAAgF;YAChF,mFAAmF;YACnF,kFAAkF;YAClF,IAAI,mCAAoB,EAAE,CAAC,aAAa,CAAC,KAAK,EAAE,mBAAmB,CAAC,CAAC;YACrE,sFAAsF;YACtF,MAAM,SAAS,GAAG,WAAW,CAAC,MAAM,CAAC,CAAC,CAAS,EAAW,EAAE,CAAC,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;YAChF,MAAM,IAAI,KAAK,CACX,+EAA+E,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QAC/G,CAAC;QAED,4DAA4D;QAC5D,YAAY,CAAC,IAAI,EAAE,CAAC;QACpB,MAAM,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;QAE1B,oBAAoB;QACpB,YAAY,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;IAClD,CAAC;IAED,OAAO,MAAM,CAAC;AAClB,CAAC;AAED;;;;;;GAMG;AACH,SAAgB,sBAAsB,CAAC,KAA+B;IAClE,MAAM,MAAM,GAAG,wBAAwB,CAAC,KAAK,CAAC,CAAC;IAC/C,MAAM,MAAM,GAAkB,EAAE,CAAC;IAEjC,+DAA+D;IAC/D,MAAM,CAAC,OAAO,CAAC,CAAC,KAAK,EAAE,UAAU,EAAE,EAAE;QACjC,KAAK,MAAM,OAAO,IAAI,KAAK,EAAE,CAAC;YAC1B,6CAA6C;YAC7C,MAAM,CAAC,OAAO,CAAC,GAAG;gBACd,KAAK,EAAE,UAAU;gBACjB,SAAS,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE;aAC3C,CAAC;QACN,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,OAAO,MAAM,CAAC;AAClB,CAAC","sourcesContent":["/**\n * Graph Sorter\n *\n * Performs topological sorting on the dependency graph to:\n * 1. Detect circular dependencies (fails if cycle found)\n * 2. Assign level numbers to each project (level 0 = no deps, level 1 = depends on level 0, etc.)\n * 3. Group projects into layers for deterministic ordering\n */\n\nimport type { ProjectApiRelations } from './api-usage/api-relations';\nimport { ProjectCycleDetector } from './graph-cycles';\n\n/**\n * Graph entry with level metadata plus AI-oriented metadata filled in by\n * enrichGraph() (lib/graph-metadata.ts) before the graph is saved:\n * - framework: the project's libType — the SET of runtime environments it is\n * validated to run in, drawn from browser | react | angular | node | express\n * (e.g. [\"browser\",\"node\"]); from its `framework:` nx tags (source of truth)\n * or inferred from package.json deps\n * - shortDescription: summary extracted from the project's responsibilities.md\n * - responsibilitiesFile: repo-relative path to the FULL responsibilities doc\n * - designFile: repo-relative path to the generated DI design.json (only for\n * project.json projects)\n * - apiRelations: for each api-lib in `dependsOn`, WHY the edge exists — the\n * APIs this project implements (serves) and/or uses (calls), each with its\n * transport (rpc | pubsub). Derived by scanning source (see api-usage/).\n */\nexport interface GraphEntry {\n level: number;\n dependsOn: string[];\n framework?: string[];\n role?: string;\n /**\n * The name CLIENTS address this app by at runtime (`new ClientConfig('helper-fsdb')`) — its\n * deployed service name, DECLARED in project.json as metadata.webpieces.serviceName. Absent for\n * anything nothing calls by name (libraries, browser apps). See service-name-resolver.ts.\n */\n serviceName?: string;\n /**\n * The service(s) this project's clients call when the call site cannot carry a literal\n * `ClientConfig` — the symmetric half of `serviceName`. DECLARED in project.json as\n * metadata.webpieces.callsService, either a single service name (every untargeted `uses` aims\n * there) or an `{ apiClassName: serviceName }` map (a client that calls several). Absent for a\n * project that never calls anything, or one whose call sites all carry literals.\n * See service-name-resolver.ts.\n */\n callsService?: string | Record<string, string>;\n /**\n * When false, the project is hidden from the rendered architecture graphs\n * (its box AND every edge touching it are omitted from dependencies.html and\n * the runtime graph). It stays in this JSON so the data view is complete.\n * Absent means drawn (the default). From the project's `drawOnGraph:` nx tag.\n */\n drawOnGraph?: boolean;\n shortDescription?: string;\n responsibilitiesFile?: string;\n designFile?: string;\n apiRelations?: ProjectApiRelations;\n /**\n * The @webpieces runtime packages this project's OWN package.json declares — http-routing,\n * http-server, http-client-node, http-client-browser, cloudtasks-client. Omitted when it\n * declares none, so TOTAL absence across the file means the file predates this field and\n * nothing is auto-hidden (see runtime-graph.ts). Core/util/logging packages are deliberately\n * not markers; see runtime-participant-resolver.ts for why.\n *\n * The runtime graph ORs this over a node's library closure: a server whose whole webpieces\n * stack comes from a shared bootstrap lib participates even though it declares nothing itself.\n */\n webpiecesRuntime?: string[];\n}\n\n/**\n * Enhanced graph format with level information\n */\nexport type EnhancedGraph = Record<string, GraphEntry>;\n\n/**\n * Compute topological layers for dependency graph using Kahn's algorithm\n *\n * Projects are grouped into layers where each layer only depends on previous layers.\n * Throws an error if a circular dependency is detected.\n *\n * @param graph - Dependency graph { project: [deps] }\n * @returns Array of layers, each containing sorted project names\n */\nexport function computeTopologicalLayers(graph: Record<string, string[]>): string[][] {\n const layers: string[][] = [];\n const processed = new Set<string>();\n const allProjects = Object.keys(graph);\n\n while (processed.size < allProjects.length) {\n const currentLayer: string[] = [];\n\n for (const project of allProjects) {\n if (processed.has(project)) continue;\n\n const deps = graph[project] || [];\n // Check if all dependencies are in previous layers (already processed)\n const allDepsInPrevLayers = deps.every((dep) => processed.has(dep));\n\n if (allDepsInPrevLayers) {\n currentLayer.push(project);\n }\n }\n\n if (currentLayer.length === 0) {\n // No progress made = circular dependency. ProjectCycleDetector is the ONE cycle\n // implementation in this package — it names every chain, in project keys, with the\n // fix — so this reports through it rather than keeping a second, weaker DFS here.\n new ProjectCycleDetector().assertAcyclic(graph, 'the project graph');\n // Unreachable: Kahn only stalls on a cycle, and the assert above throws on every one.\n const remaining = allProjects.filter((p: string): boolean => !processed.has(p));\n throw new Error(\n `Topological sort stalled with no cycle to report — projects left unlayered: ${remaining.join(', ')}`);\n }\n\n // Sort alphabetically within layer for deterministic output\n currentLayer.sort();\n layers.push(currentLayer);\n\n // Mark as processed\n currentLayer.forEach((p) => processed.add(p));\n }\n\n return layers;\n}\n\n/**\n * Sort graph in topological order with alphabetical sorting within layers\n * Returns enhanced format with level metadata\n *\n * @param graph - Unsorted dependency graph { project: [deps] }\n * @returns Sorted graph with level metadata { project: { level: number, dependsOn: [deps] } }\n */\nexport function sortGraphTopologically(graph: Record<string, string[]>): EnhancedGraph {\n const layers = computeTopologicalLayers(graph);\n const result: EnhancedGraph = {};\n\n // Add projects layer by layer (dependencies before dependents)\n layers.forEach((layer, levelIndex) => {\n for (const project of layer) {\n // Already sorted alphabetically within layer\n result[project] = {\n level: levelIndex,\n dependsOn: (graph[project] || []).sort(),\n };\n }\n });\n\n return result;\n}\n"]}
|
|
@@ -33,6 +33,7 @@ export declare class GraphVisualizer {
|
|
|
33
33
|
private readonly names;
|
|
34
34
|
private readonly responsibilities;
|
|
35
35
|
private readonly bandLayout;
|
|
36
|
+
private readonly cycles;
|
|
36
37
|
/**
|
|
37
38
|
* How to obtain the browser client's text. Injected so HTML generation does not depend on BUILD
|
|
38
39
|
* ORDER: the default reads the compiled sibling, which exists in the tarball and in dist but NOT in
|
|
@@ -99,6 +100,15 @@ export declare class GraphVisualizer {
|
|
|
99
100
|
* Generate Graphviz DOT format from the graph
|
|
100
101
|
*/
|
|
101
102
|
generateDot(graph: EnhancedGraph, title?: string): string;
|
|
103
|
+
/**
|
|
104
|
+
* Refuse to draw a graph that would render something false.
|
|
105
|
+
*
|
|
106
|
+
* Two conditions, both of which produced a confidently-wrong picture with no warning at all:
|
|
107
|
+
* - two projects sharing a node id fuse into one box, which UNIONS their two rank sets and
|
|
108
|
+
* collapses two whole dependency levels onto one row (see graph-names.ts);
|
|
109
|
+
* - a cycle makes the level numbers every row is keyed on meaningless (see graph-cycles.ts).
|
|
110
|
+
*/
|
|
111
|
+
private assertDrawable;
|
|
102
112
|
/**
|
|
103
113
|
* The visible projects grouped into one band per dependency level, ordered HIGHEST LEVEL FIRST
|
|
104
114
|
* so the emitted bands read top-to-bottom with L0 last (hidden projects are omitted, so no
|
|
@@ -19,6 +19,7 @@ const path = tslib_1.__importStar(require("path"));
|
|
|
19
19
|
const child_process_1 = require("child_process");
|
|
20
20
|
const graph_names_1 = require("./graph-names");
|
|
21
21
|
const graph_level_bands_1 = require("./graph-level-bands");
|
|
22
|
+
const graph_cycles_1 = require("./graph-cycles");
|
|
22
23
|
const graph_responsibilities_1 = require("./graph-responsibilities");
|
|
23
24
|
const toError_1 = require("../toError");
|
|
24
25
|
/**
|
|
@@ -76,6 +77,7 @@ class GraphVisualizer {
|
|
|
76
77
|
names = new graph_names_1.GraphNames();
|
|
77
78
|
responsibilities = new graph_responsibilities_1.ResponsibilitiesRenderer();
|
|
78
79
|
bandLayout = new graph_level_bands_1.LevelBandLayout();
|
|
80
|
+
cycles = new graph_cycles_1.ProjectCycleDetector();
|
|
79
81
|
/**
|
|
80
82
|
* How to obtain the browser client's text. Injected so HTML generation does not depend on BUILD
|
|
81
83
|
* ORDER: the default reads the compiled sibling, which exists in the tarball and in dist but NOT in
|
|
@@ -194,6 +196,7 @@ class GraphVisualizer {
|
|
|
194
196
|
* Generate Graphviz DOT format from the graph
|
|
195
197
|
*/
|
|
196
198
|
generateDot(graph, title = 'Monorepo Dependency Architecture') {
|
|
199
|
+
this.assertDrawable(graph);
|
|
197
200
|
let dot = 'digraph Architecture {\n';
|
|
198
201
|
dot += ' rankdir=TB;\n';
|
|
199
202
|
dot += ' node [shape=box, style=filled, fontname="Arial"];\n';
|
|
@@ -210,6 +213,24 @@ class GraphVisualizer {
|
|
|
210
213
|
dot += '}\n';
|
|
211
214
|
return dot;
|
|
212
215
|
}
|
|
216
|
+
/**
|
|
217
|
+
* Refuse to draw a graph that would render something false.
|
|
218
|
+
*
|
|
219
|
+
* Two conditions, both of which produced a confidently-wrong picture with no warning at all:
|
|
220
|
+
* - two projects sharing a node id fuse into one box, which UNIONS their two rank sets and
|
|
221
|
+
* collapses two whole dependency levels onto one row (see graph-names.ts);
|
|
222
|
+
* - a cycle makes the level numbers every row is keyed on meaningless (see graph-cycles.ts).
|
|
223
|
+
*/
|
|
224
|
+
assertDrawable(graph) {
|
|
225
|
+
const owners = [];
|
|
226
|
+
const adjacency = {};
|
|
227
|
+
for (const project of Object.keys(graph)) {
|
|
228
|
+
owners.push(new graph_names_1.NodeIdOwner(project, graph[project].level));
|
|
229
|
+
adjacency[project] = graph[project].dependsOn ?? [];
|
|
230
|
+
}
|
|
231
|
+
this.names.assertUniqueNodeIds(owners);
|
|
232
|
+
this.cycles.assertAcyclic(adjacency, 'architecture/dependencies.json');
|
|
233
|
+
}
|
|
213
234
|
/**
|
|
214
235
|
* The visible projects grouped into one band per dependency level, ordered HIGHEST LEVEL FIRST
|
|
215
236
|
* so the emitted bands read top-to-bottom with L0 last (hidden projects are omitted, so no
|
|
@@ -223,11 +244,11 @@ class GraphVisualizer {
|
|
|
223
244
|
if (this.isHidden(graph[project]))
|
|
224
245
|
continue;
|
|
225
246
|
const level = graph[project].level;
|
|
226
|
-
const
|
|
227
|
-
if (
|
|
228
|
-
byLevel.set(level, [this.names.
|
|
247
|
+
const nodeIds = byLevel.get(level);
|
|
248
|
+
if (nodeIds === undefined)
|
|
249
|
+
byLevel.set(level, [this.names.getNodeId(project)]);
|
|
229
250
|
else
|
|
230
|
-
|
|
251
|
+
nodeIds.push(this.names.getNodeId(project));
|
|
231
252
|
}
|
|
232
253
|
const levels = [...byLevel.keys()].sort((a, b) => b - a);
|
|
233
254
|
return levels.map((level) => new graph_level_bands_1.LevelBand(level, [...byLevel.get(level)].sort()));
|
|
@@ -242,6 +263,7 @@ class GraphVisualizer {
|
|
|
242
263
|
const info = graph[project];
|
|
243
264
|
if (this.isHidden(info))
|
|
244
265
|
continue;
|
|
266
|
+
const nodeId = this.names.getNodeId(project);
|
|
245
267
|
const shortName = this.names.getShortName(project);
|
|
246
268
|
const frameworks = info.framework ?? [];
|
|
247
269
|
const role = info.role ?? 'lib';
|
|
@@ -251,7 +273,8 @@ class GraphVisualizer {
|
|
|
251
273
|
const link = href ? `, URL="${href}", target="_blank"` : '';
|
|
252
274
|
const envSet = `[${frameworks.join(', ')}]`;
|
|
253
275
|
const labelMeta = `L${info.level} · ${envSet} · ${role}`;
|
|
254
|
-
|
|
276
|
+
// Identity is the project key; the LABEL is the pretty short name.
|
|
277
|
+
dot += ` "${nodeId}" [fillcolor="${color}"${border}${link}, label="${shortName}\\n(${labelMeta})"];\n`;
|
|
255
278
|
}
|
|
256
279
|
return dot;
|
|
257
280
|
}
|
|
@@ -264,13 +287,13 @@ class GraphVisualizer {
|
|
|
264
287
|
const info = graph[project];
|
|
265
288
|
if (this.isHidden(info))
|
|
266
289
|
continue;
|
|
267
|
-
const
|
|
290
|
+
const nodeId = this.names.getNodeId(project);
|
|
268
291
|
for (const dep of info.dependsOn || []) {
|
|
269
292
|
// Both endpoints must be visible — an edge to/from a hidden box
|
|
270
293
|
// is dropped so no connection dangles into empty space.
|
|
271
294
|
if (graph[dep] !== undefined && this.isHidden(graph[dep]))
|
|
272
295
|
continue;
|
|
273
|
-
dot += this.edgeDot(
|
|
296
|
+
dot += this.edgeDot(nodeId, this.names.getNodeId(dep), info.apiRelations?.[dep]);
|
|
274
297
|
}
|
|
275
298
|
}
|
|
276
299
|
return dot;
|
|
@@ -324,8 +347,10 @@ class GraphVisualizer {
|
|
|
324
347
|
for (const project of projects) {
|
|
325
348
|
if (this.isHidden(graph[project]))
|
|
326
349
|
continue;
|
|
350
|
+
// The VALUE is the node id the SVG is keyed on; the TEXT stays the pretty short name.
|
|
351
|
+
const nodeId = this.names.getNodeId(project);
|
|
327
352
|
const shortName = this.names.getShortName(project);
|
|
328
|
-
options += `<option value="${
|
|
353
|
+
options += `<option value="${nodeId}">L${graph[project].level} · ${shortName}</option>`;
|
|
329
354
|
}
|
|
330
355
|
return `<div class="wp-lock-control">
|
|
331
356
|
<label for="wp-lock">🔒 Lock a box (dim the rest & filter responsibilities):</label>
|