@webpieces/nx-webpieces-rules 0.4.652 → 0.4.654
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/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-level-bands.d.ts +45 -0
- package/src/lib/graph-level-bands.js +97 -0
- package/src/lib/graph-level-bands.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.client.d.ts +12 -0
- package/src/lib/graph-visualizer.client.js +16 -0
- package/src/lib/graph-visualizer.client.js.map +1 -1
- package/src/lib/graph-visualizer.d.ts +19 -0
- package/src/lib/graph-visualizer.js +56 -24
- 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
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Level bands for the architecture graph.
|
|
4
|
+
*
|
|
5
|
+
* Every row of architecture/dependencies.html is ONE dependency level: the highest level on top,
|
|
6
|
+
* descending as you read down, L0 — the foundation everything is built on — always last.
|
|
7
|
+
*
|
|
8
|
+
* `{ rank=same; ... }` alone does not deliver that, and assuming it did was the bug. It ties a
|
|
9
|
+
* level's boxes to one row but says nothing about where that row goes, so graphviz derived each
|
|
10
|
+
* row's position from the edges. A level containing a box with no visible edges — a leaf sdk, an
|
|
11
|
+
* api-lib whose only consumers are hidden — is unconstrained, so it was placed at the top rank and
|
|
12
|
+
* dragged its ENTIRE level up with it (verified against graphviz: that is what put the L0 sdks in
|
|
13
|
+
* the top row, and what parked an L0 lib in the same row as the L6 servers).
|
|
14
|
+
*
|
|
15
|
+
* So the ordering is STATED here rather than inferred: one invisible anchor per band, sitting in
|
|
16
|
+
* that band's rank set, chained top-to-bottom with invisible edges. Ranks are then a function of
|
|
17
|
+
* level and nothing else. The anchors — never the real boxes — carry the chain, so no fake
|
|
18
|
+
* dependency is ever drawn between two projects.
|
|
19
|
+
*/
|
|
20
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
21
|
+
exports.LevelBandLayout = exports.LevelBand = void 0;
|
|
22
|
+
/**
|
|
23
|
+
* A band bigger than this gets a blank spacer band inserted next to it, so the long edges leaving a
|
|
24
|
+
* crowded row have vertical room to fan out instead of cutting straight through the row below.
|
|
25
|
+
* Eleven-plus boxes in a row is where that became unreadable in practice.
|
|
26
|
+
*/
|
|
27
|
+
const SPACER_MIN_BAND_SIZE = 10;
|
|
28
|
+
/**
|
|
29
|
+
* The class stamped on every node and edge that exists ONLY to pin the layout. Graphviz drops
|
|
30
|
+
* `style=invis` elements from its SVG entirely, so nothing carrying this class reaches the page
|
|
31
|
+
* today; the class is how the DOT tells the browser client which elements are scaffolding, and the
|
|
32
|
+
* client skips them when indexing so an anchor can never be walked as a dependency.
|
|
33
|
+
*/
|
|
34
|
+
const LAYOUT_CLASS = 'wp-layout';
|
|
35
|
+
/** Attributes shared by every invisible layout node (rank anchors and spacer bands). */
|
|
36
|
+
const LAYOUT_NODE_ATTRS = `[style=invis, shape=point, width=0.01, height=0.01, label="", class="${LAYOUT_CLASS}"]`;
|
|
37
|
+
/** Attributes for the invisible edges that force one band strictly above the next. */
|
|
38
|
+
const LAYOUT_EDGE_ATTRS = `[style=invis, class="${LAYOUT_CLASS}"]`;
|
|
39
|
+
/**
|
|
40
|
+
* One horizontal band of the graph: every visible project sharing a dependency level, plus the
|
|
41
|
+
* invisible anchor that pins the band's vertical position.
|
|
42
|
+
*/
|
|
43
|
+
class LevelBand {
|
|
44
|
+
level;
|
|
45
|
+
/** Short names of the visible projects on this level, sorted for deterministic output. */
|
|
46
|
+
nodeNames;
|
|
47
|
+
constructor(level, nodeNames) {
|
|
48
|
+
this.level = level;
|
|
49
|
+
this.nodeNames = nodeNames;
|
|
50
|
+
}
|
|
51
|
+
anchorName() {
|
|
52
|
+
return `__wp_layout_L${this.level}`;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
exports.LevelBand = LevelBand;
|
|
56
|
+
/** Emits the DOT that pins each band to its own rank, in descending level order. */
|
|
57
|
+
class LevelBandLayout {
|
|
58
|
+
/**
|
|
59
|
+
* @param bands the visible projects grouped by level, HIGHEST LEVEL FIRST. Levels need not be
|
|
60
|
+
* contiguous — only the levels actually present get a band, and the chain links
|
|
61
|
+
* whichever ones exist.
|
|
62
|
+
*/
|
|
63
|
+
dot(bands) {
|
|
64
|
+
let dot = '';
|
|
65
|
+
for (const band of bands) {
|
|
66
|
+
dot += ` "${band.anchorName()}" ${LAYOUT_NODE_ATTRS};\n`;
|
|
67
|
+
dot += ` { rank=same; "${band.anchorName()}"; `;
|
|
68
|
+
for (const name of band.nodeNames)
|
|
69
|
+
dot += `"${name}"; `;
|
|
70
|
+
dot += '}\n';
|
|
71
|
+
}
|
|
72
|
+
for (let i = 0; i + 1 < bands.length; i++) {
|
|
73
|
+
dot += this.bandGap(bands[i], bands[i + 1]);
|
|
74
|
+
}
|
|
75
|
+
return dot;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* The ordering chain between two adjacent bands. When either band is crowded
|
|
79
|
+
* (> SPACER_MIN_BAND_SIZE boxes), an empty spacer band goes between them: the edges leaving a
|
|
80
|
+
* wide row then have a whole rank of vertical room to fan out in, instead of being drawn
|
|
81
|
+
* straight through the row below.
|
|
82
|
+
*/
|
|
83
|
+
bandGap(upper, lower) {
|
|
84
|
+
const crowded = upper.nodeNames.length > SPACER_MIN_BAND_SIZE
|
|
85
|
+
|| lower.nodeNames.length > SPACER_MIN_BAND_SIZE;
|
|
86
|
+
if (!crowded) {
|
|
87
|
+
return ` "${upper.anchorName()}" -> "${lower.anchorName()}" ${LAYOUT_EDGE_ATTRS};\n`;
|
|
88
|
+
}
|
|
89
|
+
const spacer = `__wp_layout_spacer_L${upper.level}_L${lower.level}`;
|
|
90
|
+
return ` "${spacer}" ${LAYOUT_NODE_ATTRS};\n`
|
|
91
|
+
+ ` { rank=same; "${spacer}"; }\n`
|
|
92
|
+
+ ` "${upper.anchorName()}" -> "${spacer}" ${LAYOUT_EDGE_ATTRS};\n`
|
|
93
|
+
+ ` "${spacer}" -> "${lower.anchorName()}" ${LAYOUT_EDGE_ATTRS};\n`;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
exports.LevelBandLayout = LevelBandLayout;
|
|
97
|
+
//# sourceMappingURL=graph-level-bands.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"graph-level-bands.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/graph-level-bands.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;GAiBG;;;AAEH;;;;GAIG;AACH,MAAM,oBAAoB,GAAG,EAAE,CAAC;AAEhC;;;;;GAKG;AACH,MAAM,YAAY,GAAG,WAAW,CAAC;AAEjC,wFAAwF;AACxF,MAAM,iBAAiB,GACnB,wEAAwE,YAAY,IAAI,CAAC;AAE7F,sFAAsF;AACtF,MAAM,iBAAiB,GAAG,wBAAwB,YAAY,IAAI,CAAC;AAEnE;;;GAGG;AACH,MAAa,SAAS;IAClB,KAAK,CAAS;IACd,0FAA0F;IAC1F,SAAS,CAAW;IAEpB,YAAY,KAAa,EAAE,SAAmB;QAC1C,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;QACnB,IAAI,CAAC,SAAS,GAAG,SAAS,CAAC;IAC/B,CAAC;IAED,UAAU;QACN,OAAO,gBAAgB,IAAI,CAAC,KAAK,EAAE,CAAC;IACxC,CAAC;CACJ;AAbD,8BAaC;AAED,oFAAoF;AACpF,MAAa,eAAe;IACxB;;;;OAIG;IACH,GAAG,CAAC,KAAkB;QAClB,IAAI,GAAG,GAAG,EAAE,CAAC;QACb,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;YACvB,GAAG,IAAI,MAAM,IAAI,CAAC,UAAU,EAAE,KAAK,iBAAiB,KAAK,CAAC;YAC1D,GAAG,IAAI,mBAAmB,IAAI,CAAC,UAAU,EAAE,KAAK,CAAC;YACjD,KAAK,MAAM,IAAI,IAAI,IAAI,CAAC,SAAS;gBAAE,GAAG,IAAI,IAAI,IAAI,KAAK,CAAC;YACxD,GAAG,IAAI,KAAK,CAAC;QACjB,CAAC;QACD,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;YACxC,GAAG,IAAI,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;QAChD,CAAC;QACD,OAAO,GAAG,CAAC;IACf,CAAC;IAED;;;;;OAKG;IACK,OAAO,CAAC,KAAgB,EAAE,KAAgB;QAC9C,MAAM,OAAO,GAAG,KAAK,CAAC,SAAS,CAAC,MAAM,GAAG,oBAAoB;eACtD,KAAK,CAAC,SAAS,CAAC,MAAM,GAAG,oBAAoB,CAAC;QACrD,IAAI,CAAC,OAAO,EAAE,CAAC;YACX,OAAO,MAAM,KAAK,CAAC,UAAU,EAAE,SAAS,KAAK,CAAC,UAAU,EAAE,KAAK,iBAAiB,KAAK,CAAC;QAC1F,CAAC;QACD,MAAM,MAAM,GAAG,uBAAuB,KAAK,CAAC,KAAK,KAAK,KAAK,CAAC,KAAK,EAAE,CAAC;QACpE,OAAO,MAAM,MAAM,KAAK,iBAAiB,KAAK;cACxC,mBAAmB,MAAM,QAAQ;cACjC,MAAM,KAAK,CAAC,UAAU,EAAE,SAAS,MAAM,KAAK,iBAAiB,KAAK;cAClE,MAAM,MAAM,SAAS,KAAK,CAAC,UAAU,EAAE,KAAK,iBAAiB,KAAK,CAAC;IAC7E,CAAC;CACJ;AAtCD,0CAsCC","sourcesContent":["/**\n * Level bands for the architecture graph.\n *\n * Every row of architecture/dependencies.html is ONE dependency level: the highest level on top,\n * descending as you read down, L0 — the foundation everything is built on — always last.\n *\n * `{ rank=same; ... }` alone does not deliver that, and assuming it did was the bug. It ties a\n * level's boxes to one row but says nothing about where that row goes, so graphviz derived each\n * row's position from the edges. A level containing a box with no visible edges — a leaf sdk, an\n * api-lib whose only consumers are hidden — is unconstrained, so it was placed at the top rank and\n * dragged its ENTIRE level up with it (verified against graphviz: that is what put the L0 sdks in\n * the top row, and what parked an L0 lib in the same row as the L6 servers).\n *\n * So the ordering is STATED here rather than inferred: one invisible anchor per band, sitting in\n * that band's rank set, chained top-to-bottom with invisible edges. Ranks are then a function of\n * level and nothing else. The anchors — never the real boxes — carry the chain, so no fake\n * dependency is ever drawn between two projects.\n */\n\n/**\n * A band bigger than this gets a blank spacer band inserted next to it, so the long edges leaving a\n * crowded row have vertical room to fan out instead of cutting straight through the row below.\n * Eleven-plus boxes in a row is where that became unreadable in practice.\n */\nconst SPACER_MIN_BAND_SIZE = 10;\n\n/**\n * The class stamped on every node and edge that exists ONLY to pin the layout. Graphviz drops\n * `style=invis` elements from its SVG entirely, so nothing carrying this class reaches the page\n * today; the class is how the DOT tells the browser client which elements are scaffolding, and the\n * client skips them when indexing so an anchor can never be walked as a dependency.\n */\nconst LAYOUT_CLASS = 'wp-layout';\n\n/** Attributes shared by every invisible layout node (rank anchors and spacer bands). */\nconst LAYOUT_NODE_ATTRS =\n `[style=invis, shape=point, width=0.01, height=0.01, label=\"\", class=\"${LAYOUT_CLASS}\"]`;\n\n/** Attributes for the invisible edges that force one band strictly above the next. */\nconst LAYOUT_EDGE_ATTRS = `[style=invis, class=\"${LAYOUT_CLASS}\"]`;\n\n/**\n * One horizontal band of the graph: every visible project sharing a dependency level, plus the\n * invisible anchor that pins the band's vertical position.\n */\nexport class LevelBand {\n level: number;\n /** Short names of the visible projects on this level, sorted for deterministic output. */\n nodeNames: string[];\n\n constructor(level: number, nodeNames: string[]) {\n this.level = level;\n this.nodeNames = nodeNames;\n }\n\n anchorName(): string {\n return `__wp_layout_L${this.level}`;\n }\n}\n\n/** Emits the DOT that pins each band to its own rank, in descending level order. */\nexport class LevelBandLayout {\n /**\n * @param bands the visible projects grouped by level, HIGHEST LEVEL FIRST. Levels need not be\n * contiguous — only the levels actually present get a band, and the chain links\n * whichever ones exist.\n */\n dot(bands: LevelBand[]): string {\n let dot = '';\n for (const band of bands) {\n dot += ` \"${band.anchorName()}\" ${LAYOUT_NODE_ATTRS};\\n`;\n dot += ` { rank=same; \"${band.anchorName()}\"; `;\n for (const name of band.nodeNames) dot += `\"${name}\"; `;\n dot += '}\\n';\n }\n for (let i = 0; i + 1 < bands.length; i++) {\n dot += this.bandGap(bands[i], bands[i + 1]);\n }\n return dot;\n }\n\n /**\n * The ordering chain between two adjacent bands. When either band is crowded\n * (> SPACER_MIN_BAND_SIZE boxes), an empty spacer band goes between them: the edges leaving a\n * wide row then have a whole rank of vertical room to fan out in, instead of being drawn\n * straight through the row below.\n */\n private bandGap(upper: LevelBand, lower: LevelBand): string {\n const crowded = upper.nodeNames.length > SPACER_MIN_BAND_SIZE\n || lower.nodeNames.length > SPACER_MIN_BAND_SIZE;\n if (!crowded) {\n return ` \"${upper.anchorName()}\" -> \"${lower.anchorName()}\" ${LAYOUT_EDGE_ATTRS};\\n`;\n }\n const spacer = `__wp_layout_spacer_L${upper.level}_L${lower.level}`;\n return ` \"${spacer}\" ${LAYOUT_NODE_ATTRS};\\n`\n + ` { rank=same; \"${spacer}\"; }\\n`\n + ` \"${upper.anchorName()}\" -> \"${spacer}\" ${LAYOUT_EDGE_ATTRS};\\n`\n + ` \"${spacer}\" -> \"${lower.anchorName()}\" ${LAYOUT_EDGE_ATTRS};\\n`;\n }\n}\n"]}
|
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":"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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=\"<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":";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,yFAAyF;QACzF,0FAA0F;QAC1F,4EAA4E;QAC5E,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC;QAC7C,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,MAAM,CAAC,IAAI;YACvE,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;AA9HD,4DA8HC","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"]}
|
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The class graph-visualizer.ts stamps on the invisible rank anchors, spacer bands and ordering
|
|
3
|
+
* edges that pin each level to its own row. They are layout scaffolding, not architecture, so the
|
|
4
|
+
* indexes below skip them: an anchor must never be walkable as a dependency, or hovering one box
|
|
5
|
+
* would light boxes it has nothing to do with.
|
|
6
|
+
*
|
|
7
|
+
* Graphviz drops `style=invis` elements from its SVG entirely (verified against the emitted SVG),
|
|
8
|
+
* so today nothing carrying this class reaches the DOM at all. The skip is the guarantee that the
|
|
9
|
+
* scaffolding stays inert if that ever stops being true — the DOT is the only place that decides
|
|
10
|
+
* what is scaffolding, and it says so with this class.
|
|
11
|
+
*/
|
|
12
|
+
declare const LAYOUT_CLASS = "wp-layout";
|
|
1
13
|
/** One traversal direction: the edges to light, and the nodes to walk on to. */
|
|
2
14
|
declare class Direction {
|
|
3
15
|
readonly nodes: Map<string, Set<string>>;
|
|
@@ -20,6 +20,18 @@
|
|
|
20
20
|
// The DOT placeholder and the `Viz` global are declared once in viz-client-globals.d.ts — both
|
|
21
21
|
// scripts share a global scope, so they cannot each declare them. Do NOT spell the placeholder token
|
|
22
22
|
// in a comment: the inliner is a blind split/join, so every literal occurrence gets the whole DOT.
|
|
23
|
+
/**
|
|
24
|
+
* The class graph-visualizer.ts stamps on the invisible rank anchors, spacer bands and ordering
|
|
25
|
+
* edges that pin each level to its own row. They are layout scaffolding, not architecture, so the
|
|
26
|
+
* indexes below skip them: an anchor must never be walkable as a dependency, or hovering one box
|
|
27
|
+
* would light boxes it has nothing to do with.
|
|
28
|
+
*
|
|
29
|
+
* Graphviz drops `style=invis` elements from its SVG entirely (verified against the emitted SVG),
|
|
30
|
+
* so today nothing carrying this class reaches the DOM at all. The skip is the guarantee that the
|
|
31
|
+
* scaffolding stays inert if that ever stops being true — the DOT is the only place that decides
|
|
32
|
+
* what is scaffolding, and it says so with this class.
|
|
33
|
+
*/
|
|
34
|
+
const LAYOUT_CLASS = 'wp-layout';
|
|
23
35
|
/** One traversal direction: the edges to light, and the nodes to walk on to. */
|
|
24
36
|
class Direction {
|
|
25
37
|
nodes;
|
|
@@ -61,6 +73,8 @@ class GraphHighlighter {
|
|
|
61
73
|
}
|
|
62
74
|
indexNodes() {
|
|
63
75
|
this.svg.querySelectorAll('g.node').forEach((g) => {
|
|
76
|
+
if (g.classList.contains(LAYOUT_CLASS))
|
|
77
|
+
return;
|
|
64
78
|
const title = g.querySelector('title');
|
|
65
79
|
if (title !== null && title.textContent !== null) {
|
|
66
80
|
this.nodeByName.set(title.textContent.trim(), g);
|
|
@@ -69,6 +83,8 @@ class GraphHighlighter {
|
|
|
69
83
|
}
|
|
70
84
|
indexEdges() {
|
|
71
85
|
this.svg.querySelectorAll('g.edge').forEach((edge) => {
|
|
86
|
+
if (edge.classList.contains(LAYOUT_CLASS))
|
|
87
|
+
return;
|
|
72
88
|
const title = edge.querySelector('title');
|
|
73
89
|
const text = title === null ? null : title.textContent;
|
|
74
90
|
if (text === null)
|