@webpieces/nx-webpieces-rules 0.3.333 → 0.3.334
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 +1 -1
- package/src/executors/generate/executor.js.map +1 -1
- package/src/executors/visualize/executor.js +3 -2
- package/src/executors/visualize/executor.js.map +1 -1
- package/src/executors/visualize-runtime/executor.js +1 -1
- package/src/executors/visualize-runtime/executor.js.map +1 -1
- package/src/lib/graph-names.d.ts +17 -0
- package/src/lib/graph-names.js +24 -0
- package/src/lib/graph-names.js.map +1 -0
- package/src/lib/graph-responsibilities.d.ts +48 -0
- package/src/lib/graph-responsibilities.js +152 -0
- package/src/lib/graph-responsibilities.js.map +1 -0
- package/src/lib/graph-visualizer.client.js +127 -0
- package/src/lib/graph-visualizer.d.ts +77 -27
- package/src/lib/graph-visualizer.js +271 -257
- package/src/lib/graph-visualizer.js.map +1 -1
- package/src/scripts/wp-design-visualize.js +1 -1
- package/src/scripts/wp-design-visualize.js.map +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webpieces/nx-webpieces-rules",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.334",
|
|
4
4
|
"description": "Nx-specific webpieces validation rules and graph tooling. Bundles all @webpieces rule packages with Nx graph validators and an inference plugin.",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"main": "./src/index.js",
|
|
@@ -21,11 +21,11 @@
|
|
|
21
21
|
"README.md"
|
|
22
22
|
],
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"@webpieces/ai-hook-rules": "0.3.
|
|
25
|
-
"@webpieces/code-rules": "0.3.
|
|
26
|
-
"@webpieces/eslint-rules": "0.3.
|
|
27
|
-
"@webpieces/pr-gate": "0.3.
|
|
28
|
-
"@webpieces/rules-config": "0.3.
|
|
24
|
+
"@webpieces/ai-hook-rules": "0.3.334",
|
|
25
|
+
"@webpieces/code-rules": "0.3.334",
|
|
26
|
+
"@webpieces/eslint-rules": "0.3.334",
|
|
27
|
+
"@webpieces/pr-gate": "0.3.334",
|
|
28
|
+
"@webpieces/rules-config": "0.3.334",
|
|
29
29
|
"madge": "8.0.0"
|
|
30
30
|
},
|
|
31
31
|
"peerDependencies": {
|
|
@@ -61,7 +61,7 @@ async function runExecutor(options, context) {
|
|
|
61
61
|
console.log('✅ Graph saved successfully');
|
|
62
62
|
// Step 4b: Write the committed, clickable HTML view next to the JSON so
|
|
63
63
|
// dependencies.html regenerates in lock-step with dependencies.json.
|
|
64
|
-
const vizPaths =
|
|
64
|
+
const vizPaths = new graph_visualizer_1.GraphVisualizer().writeVisualization(enhancedGraph, workspaceRoot);
|
|
65
65
|
console.log(`✅ Wrote ${vizPaths.htmlPath}`);
|
|
66
66
|
// Step 5: Generate the runtime microservice graph at the same time
|
|
67
67
|
await generateRuntimeGraph(workspaceRoot);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"executor.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/nx-webpieces-rules/src/executors/generate/executor.ts"],"names":[],"mappings":";AAAA;;;;;;;GAOG;;AA2CH,8BAyDC;AAjGD,0DAAwD;AACxD,+DAAiE;AACjE,yDAAgE;AAChE,yDAAmD;AACnD,6DAAoG;AACpG,
|
|
1
|
+
{"version":3,"file":"executor.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/nx-webpieces-rules/src/executors/generate/executor.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Generate Executor\n *\n * Generates the architecture dependency graph and saves it to architecture/dependencies.json.\n *\n * Usage:\n * nx run architecture:generate\n */\n\nimport type { ExecutorContext } from '@nx/devkit';\nimport { writeTemplate } from '@webpieces/rules-config';\nimport { generateReducedGraph } from '../../lib/graph-generator';\nimport { sortGraphTopologically } from '../../lib/graph-sorter';\nimport { saveGraph } from '../../lib/graph-loader';\nimport { collectProjectInfo, enrichGraph, MetadataValidationError } from '../../lib/graph-metadata';\nimport { GraphVisualizer } from '../../lib/graph-visualizer';\nimport { buildWorkspaceModel } from '../../lib/runtime-markers';\nimport { assembleRuntimeGraph, saveRuntimeGraph } from '../../lib/runtime-graph';\nimport { loadRuntimeConfig } from '../../lib/runtime-config';\nimport { toError } from '../../toError';\n\nexport interface GenerateExecutorOptions {\n graphPath?: string;\n}\n\nexport interface ExecutorResult {\n success: boolean;\n}\n\n/**\n * Generate the runtime microservice graph alongside the compile-time graph, so\n * one regenerate produces both committed files. Skipped when the\n * runtime-architecture rule is OFF or no apiProjectPaths are configured.\n */\nasync function generateRuntimeGraph(workspaceRoot: string): Promise<void> {\n const config = loadRuntimeConfig(workspaceRoot);\n if (config.off || config.servicePaths.length === 0) {\n console.log('⏭️ Runtime graph skipped (runtime-architecture OFF or no servicePaths)');\n return;\n }\n console.log('📡 Generating runtime graph from service-contract.json files...');\n const model = await buildWorkspaceModel(workspaceRoot, config.apiProjectPaths, config.servicePaths);\n const runtimeGraph = assembleRuntimeGraph(model, workspaceRoot);\n saveRuntimeGraph(runtimeGraph, workspaceRoot);\n const serviceCount = Object.keys(runtimeGraph.services).length;\n console.log(\n `✅ Runtime graph saved (${serviceCount} services, ${runtimeGraph.runtimeEdges.length} runtime edges)`,\n );\n}\n\nexport default async function runExecutor(\n options: GenerateExecutorOptions,\n context: ExecutorContext\n): Promise<ExecutorResult> {\n const graphPath = options.graphPath;\n const workspaceRoot = context.root;\n\n console.log('\\n📊 Architecture Graph Generator\\n');\n\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n // Step 1: Build the full graph from nx, then transitively reduce it to the view\n console.log(\"📊 Generating dependency graph from nx's project graph...\");\n const reducedGraph = await generateReducedGraph();\n\n // Step 2: Topological sort (to assign levels for visualization)\n console.log('🔄 Computing topological layers...');\n const enhancedGraph = sortGraphTopologically(reducedGraph);\n\n // Step 3: Enrich with AI metadata (framework, shortDescription, file\n // pointers). This VALIDATES (responsibilities.md required per project)\n // and throws before any write, so a failure never clobbers the file.\n console.log('🏷️ Enriching graph with framework + responsibilities metadata...');\n const projectInfos = await collectProjectInfo();\n enrichGraph(enhancedGraph, projectInfos, workspaceRoot);\n\n // Step 4: Save the graph\n console.log('💾 Saving graph to architecture/dependencies.json...');\n saveGraph(enhancedGraph, workspaceRoot, graphPath);\n console.log('✅ Graph saved successfully');\n\n // Step 4b: Write the committed, clickable HTML view next to the JSON so\n // dependencies.html regenerates in lock-step with dependencies.json.\n const vizPaths = new GraphVisualizer().writeVisualization(enhancedGraph, workspaceRoot);\n console.log(`✅ Wrote ${vizPaths.htmlPath}`);\n\n // Step 5: Generate the runtime microservice graph at the same time\n await generateRuntimeGraph(workspaceRoot);\n\n // Print summary\n const projectCount = Object.keys(enhancedGraph).length;\n const levels = new Set(Object.values(enhancedGraph).map((e) => e.level));\n console.log(`\\n📈 Graph Summary:`);\n console.log(` Projects: ${projectCount}`);\n console.log(` Levels: ${levels.size} (0-${Math.max(...levels)})`);\n\n return { success: true };\n } catch (err: unknown) {\n const error = toError(err);\n console.error('❌ Graph generation failed:', error.message);\n if (error instanceof MetadataValidationError) {\n const mdPath = writeTemplate(workspaceRoot, 'webpieces.responsibilities.md');\n console.error('');\n console.error('⚠️ *** Refer to ' + mdPath + ' for how to author responsibilities.md files *** ⚠️');\n }\n return { success: false };\n }\n}\n"]}
|
|
@@ -30,11 +30,12 @@ async function runExecutor(options, context) {
|
|
|
30
30
|
const graph = graphFile.projects;
|
|
31
31
|
// Generate visualization
|
|
32
32
|
console.log('🎨 Generating visualization...');
|
|
33
|
-
const
|
|
33
|
+
const visualizer = new graph_visualizer_1.GraphVisualizer();
|
|
34
|
+
const vizPaths = visualizer.writeVisualization(graph, workspaceRoot);
|
|
34
35
|
console.log(`✅ Generated: ${vizPaths.htmlPath}`);
|
|
35
36
|
// Try to open in browser
|
|
36
37
|
console.log('\n🌐 Opening visualization in browser...');
|
|
37
|
-
if (
|
|
38
|
+
if (visualizer.openVisualization(vizPaths.htmlPath)) {
|
|
38
39
|
console.log('✅ Browser opened');
|
|
39
40
|
}
|
|
40
41
|
else {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"executor.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/nx-webpieces-rules/src/executors/visualize/executor.ts"],"names":[],"mappings":";AAAA;;;;;;;;GAQG;;AAeH,
|
|
1
|
+
{"version":3,"file":"executor.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/nx-webpieces-rules/src/executors/visualize/executor.ts"],"names":[],"mappings":";AAAA;;;;;;;;GAQG;;AAeH,8BA0CC;AAtDD,yDAA0D;AAC1D,iEAA6D;AAC7D,2CAAwC;AAUzB,KAAK,UAAU,WAAW,CACrC,OAAiC,EACjC,OAAwB;IAExB,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,CAAC;IACpC,MAAM,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC;IAEnC,OAAO,CAAC,GAAG,CAAC,mCAAmC,CAAC,CAAC;IAEjD,8DAA8D;IAC9D,IAAI,CAAC;QACD,uBAAuB;QACvB,OAAO,CAAC,GAAG,CAAC,2BAA2B,CAAC,CAAC;QACzC,MAAM,SAAS,GAAG,IAAA,+BAAgB,EAAC,aAAa,EAAE,SAAS,CAAC,CAAC;QAE7D,IAAI,CAAC,SAAS,EAAE,CAAC;YACb,OAAO,CAAC,KAAK,CAAC,0DAA0D,CAAC,CAAC;YAC1E,OAAO,CAAC,KAAK,CAAC,4CAA4C,CAAC,CAAC;YAC5D,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;QAC9B,CAAC;QACD,MAAM,KAAK,GAAG,SAAS,CAAC,QAAQ,CAAC;QAEjC,yBAAyB;QACzB,OAAO,CAAC,GAAG,CAAC,gCAAgC,CAAC,CAAC;QAC9C,MAAM,UAAU,GAAG,IAAI,kCAAe,EAAE,CAAC;QACzC,MAAM,QAAQ,GAAG,UAAU,CAAC,kBAAkB,CAAC,KAAK,EAAE,aAAa,CAAC,CAAC;QACrE,OAAO,CAAC,GAAG,CAAC,gBAAgB,QAAQ,CAAC,QAAQ,EAAE,CAAC,CAAC;QAEjD,yBAAyB;QACzB,OAAO,CAAC,GAAG,CAAC,0CAA0C,CAAC,CAAC;QACxD,IAAI,UAAU,CAAC,iBAAiB,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC;YAClD,OAAO,CAAC,GAAG,CAAC,kBAAkB,CAAC,CAAC;QACpC,CAAC;aAAM,CAAC;YACJ,OAAO,CAAC,GAAG,CAAC,2CAA2C,QAAQ,CAAC,QAAQ,EAAE,CAAC,CAAC;QAChF,CAAC;QAED,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;IAC7B,CAAC;IAAC,OAAO,GAAY,EAAE,CAAC;QACpB,MAAM,KAAK,GAAG,IAAA,iBAAO,EAAC,GAAG,CAAC,CAAC;QAC3B,OAAO,CAAC,KAAK,CAAC,yBAAyB,EAAE,KAAK,CAAC,OAAO,CAAC,CAAC;QACxD,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;IAC9B,CAAC;AACL,CAAC","sourcesContent":["/**\n * Visualize Executor\n *\n * Generates visual representations of the architecture graph (DOT + HTML)\n * and opens the visualization in a browser.\n *\n * Usage:\n * nx run architecture:visualize\n */\n\nimport type { ExecutorContext } from '@nx/devkit';\nimport { loadBlessedGraph } from '../../lib/graph-loader';\nimport { GraphVisualizer } from '../../lib/graph-visualizer';\nimport { toError } from '../../toError';\n\nexport interface VisualizeExecutorOptions {\n graphPath?: string;\n}\n\nexport interface ExecutorResult {\n success: boolean;\n}\n\nexport default async function runExecutor(\n options: VisualizeExecutorOptions,\n context: ExecutorContext\n): Promise<ExecutorResult> {\n const graphPath = options.graphPath;\n const workspaceRoot = context.root;\n\n console.log('\\n🎨 Architecture Visualization\\n');\n\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n // Load the saved graph\n console.log('📂 Loading saved graph...');\n const graphFile = loadBlessedGraph(workspaceRoot, graphPath);\n\n if (!graphFile) {\n console.error('❌ No saved graph found at architecture/dependencies.json');\n console.error(' Run: nx run architecture:generate first');\n return { success: false };\n }\n const graph = graphFile.projects;\n\n // Generate visualization\n console.log('🎨 Generating visualization...');\n const visualizer = new GraphVisualizer();\n const vizPaths = visualizer.writeVisualization(graph, workspaceRoot);\n console.log(`✅ Generated: ${vizPaths.htmlPath}`);\n\n // Try to open in browser\n console.log('\\n🌐 Opening visualization in browser...');\n if (visualizer.openVisualization(vizPaths.htmlPath)) {\n console.log('✅ Browser opened');\n } else {\n console.log(`⚠️ Could not auto-open. Open manually: ${vizPaths.htmlPath}`);\n }\n\n return { success: true };\n } catch (err: unknown) {\n const error = toError(err);\n console.error('❌ Visualization failed:', error.message);\n return { success: false };\n }\n}\n"]}
|
|
@@ -28,7 +28,7 @@ async function runExecutor(_options, context) {
|
|
|
28
28
|
console.log(`✅ Generated: ${vizPaths.dotPath}`);
|
|
29
29
|
console.log(`✅ Generated: ${vizPaths.htmlPath}`);
|
|
30
30
|
console.log('\n🌐 Opening visualization in browser...');
|
|
31
|
-
if (
|
|
31
|
+
if (new graph_visualizer_1.GraphVisualizer().openVisualization(vizPaths.htmlPath)) {
|
|
32
32
|
console.log('✅ Browser opened');
|
|
33
33
|
}
|
|
34
34
|
else {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"executor.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/nx-webpieces-rules/src/executors/visualize-runtime/executor.ts"],"names":[],"mappings":";AAAA;;;;;;;GAOG;;AAgBH,8BAkCC;AA/CD,2DAA2D;AAC3D,qEAAyE;AACzE,
|
|
1
|
+
{"version":3,"file":"executor.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/nx-webpieces-rules/src/executors/visualize-runtime/executor.ts"],"names":[],"mappings":";AAAA;;;;;;;GAOG;;AAgBH,8BAkCC;AA/CD,2DAA2D;AAC3D,qEAAyE;AACzE,iEAA6D;AAC7D,2CAAwC;AAUzB,KAAK,UAAU,WAAW,CACrC,QAAiC,EACjC,OAAwB;IAExB,MAAM,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC;IAEnC,OAAO,CAAC,GAAG,CAAC,2CAA2C,CAAC,CAAC;IAEzD,8DAA8D;IAC9D,IAAI,CAAC;QACD,MAAM,KAAK,GAAG,IAAA,gCAAgB,EAAC,aAAa,CAAC,CAAC;QAC9C,IAAI,CAAC,KAAK,EAAE,CAAC;YACT,OAAO,CAAC,KAAK,CAAC,mDAAmD,CAAC,CAAC;YACnE,OAAO,CAAC,KAAK,CAAC,4CAA4C,CAAC,CAAC;YAC5D,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;QAC9B,CAAC;QAED,MAAM,QAAQ,GAAG,IAAA,8CAAyB,EAAC,KAAK,EAAE,aAAa,CAAC,CAAC;QACjE,OAAO,CAAC,GAAG,CAAC,gBAAgB,QAAQ,CAAC,OAAO,EAAE,CAAC,CAAC;QAChD,OAAO,CAAC,GAAG,CAAC,gBAAgB,QAAQ,CAAC,QAAQ,EAAE,CAAC,CAAC;QAEjD,OAAO,CAAC,GAAG,CAAC,0CAA0C,CAAC,CAAC;QACxD,IAAI,IAAI,kCAAe,EAAE,CAAC,iBAAiB,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC;YAC7D,OAAO,CAAC,GAAG,CAAC,kBAAkB,CAAC,CAAC;QACpC,CAAC;aAAM,CAAC;YACJ,OAAO,CAAC,GAAG,CAAC,2CAA2C,QAAQ,CAAC,QAAQ,EAAE,CAAC,CAAC;QAChF,CAAC;QAED,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;IAC7B,CAAC;IAAC,OAAO,GAAY,EAAE,CAAC;QACpB,MAAM,KAAK,GAAG,IAAA,iBAAO,EAAC,GAAG,CAAC,CAAC;QAC3B,OAAO,CAAC,KAAK,CAAC,iCAAiC,EAAE,KAAK,CAAC,OAAO,CAAC,CAAC;QAChE,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;IAC9B,CAAC;AACL,CAAC","sourcesContent":["/**\n * Visualize Runtime Executor\n *\n * Renders the runtime microservice graph (committed\n * architecture/runtime-dependencies.json) to DOT + HTML and opens it.\n *\n * Usage: nx run architecture:visualize-runtime\n */\n\nimport type { ExecutorContext } from '@nx/devkit';\nimport { loadRuntimeGraph } from '../../lib/runtime-graph';\nimport { writeRuntimeVisualization } from '../../lib/runtime-visualizer';\nimport { GraphVisualizer } from '../../lib/graph-visualizer';\nimport { toError } from '../../toError';\n\nexport interface VisualizeRuntimeOptions {\n // No options.\n}\n\nexport interface ExecutorResult {\n success: boolean;\n}\n\nexport default async function runExecutor(\n _options: VisualizeRuntimeOptions,\n context: ExecutorContext,\n): Promise<ExecutorResult> {\n const workspaceRoot = context.root;\n\n console.log('\\n🎨 Runtime Microservice Visualization\\n');\n\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const graph = loadRuntimeGraph(workspaceRoot);\n if (!graph) {\n console.error('❌ No architecture/runtime-dependencies.json found');\n console.error(' Run: nx run architecture:generate first');\n return { success: false };\n }\n\n const vizPaths = writeRuntimeVisualization(graph, workspaceRoot);\n console.log(`✅ Generated: ${vizPaths.dotPath}`);\n console.log(`✅ Generated: ${vizPaths.htmlPath}`);\n\n console.log('\\n🌐 Opening visualization in browser...');\n if (new GraphVisualizer().openVisualization(vizPaths.htmlPath)) {\n console.log('✅ Browser opened');\n } else {\n console.log(`⚠️ Could not auto-open. Open manually: ${vizPaths.htmlPath}`);\n }\n\n return { success: true };\n } catch (err: unknown) {\n const error = toError(err);\n console.error('❌ Runtime visualization failed:', error.message);\n return { success: false };\n }\n}\n"]}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Graph Names
|
|
3
|
+
*
|
|
4
|
+
* Shared helper for turning a scoped project name into the short display name
|
|
5
|
+
* used everywhere in the visualization (graph box titles, the lock-control
|
|
6
|
+
* options, and the responsibilities cards). Kept in its own class so both
|
|
7
|
+
* GraphVisualizer and ResponsibilitiesRenderer can depend on it without a
|
|
8
|
+
* circular dependency between them.
|
|
9
|
+
*/
|
|
10
|
+
export declare class GraphNames {
|
|
11
|
+
/**
|
|
12
|
+
* Remove scope from name for display
|
|
13
|
+
* '@scope/name' → 'name'
|
|
14
|
+
* 'name' → 'name'
|
|
15
|
+
*/
|
|
16
|
+
getShortName(name: string): string;
|
|
17
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Graph Names
|
|
4
|
+
*
|
|
5
|
+
* Shared helper for turning a scoped project name into the short display name
|
|
6
|
+
* used everywhere in the visualization (graph box titles, the lock-control
|
|
7
|
+
* options, and the responsibilities cards). Kept in its own class so both
|
|
8
|
+
* GraphVisualizer and ResponsibilitiesRenderer can depend on it without a
|
|
9
|
+
* circular dependency between them.
|
|
10
|
+
*/
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.GraphNames = void 0;
|
|
13
|
+
class GraphNames {
|
|
14
|
+
/**
|
|
15
|
+
* Remove scope from name for display
|
|
16
|
+
* '@scope/name' → 'name'
|
|
17
|
+
* 'name' → 'name'
|
|
18
|
+
*/
|
|
19
|
+
getShortName(name) {
|
|
20
|
+
return name.includes('/') ? name.split('/').pop() : name;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
exports.GraphNames = GraphNames;
|
|
24
|
+
//# sourceMappingURL=graph-names.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"graph-names.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/graph-names.ts"],"names":[],"mappings":";AAAA;;;;;;;;GAQG;;;AAEH,MAAa,UAAU;IACnB;;;;OAIG;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;CACJ;AATD,gCASC","sourcesContent":["/**\n * Graph Names\n *\n * Shared helper for turning a scoped project name into the short display name\n * used everywhere in the visualization (graph box titles, the lock-control\n * options, and the responsibilities cards). Kept in its own class so both\n * GraphVisualizer and ResponsibilitiesRenderer can depend on it without a\n * circular dependency between them.\n */\n\nexport class GraphNames {\n /**\n * Remove scope from name for display\n * '@scope/name' → 'name'\n * 'name' → 'name'\n */\n getShortName(name: string): string {\n return name.includes('/') ? name.split('/').pop()! : name;\n }\n}\n"]}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Responsibilities Section
|
|
3
|
+
*
|
|
4
|
+
* Renders every module's full `responsibilities.md` as a collapsible card below
|
|
5
|
+
* the dependency graph in architecture/dependencies.html. Cards are ordered by
|
|
6
|
+
* dependency level HIGH → LOW (top-level apps first, deepest libs last) so a
|
|
7
|
+
* reader scrolls from the runnable servers/clients down into the libraries.
|
|
8
|
+
*
|
|
9
|
+
* Each card carries `data-node="<shortName>"` matching the graph box's title, so
|
|
10
|
+
* the page script can filter the list to just the locked box's chain (see
|
|
11
|
+
* graph-visualizer.client.js). The summary paragraph is the `shortDescription`
|
|
12
|
+
* already on each graph entry; the expanded body is the rendered markdown file.
|
|
13
|
+
*/
|
|
14
|
+
import type { EnhancedGraph } from './graph-sorter';
|
|
15
|
+
export declare class ResponsibilitiesRenderer {
|
|
16
|
+
private readonly names;
|
|
17
|
+
/**
|
|
18
|
+
* Build the responsibilities section HTML: one collapsible card per module,
|
|
19
|
+
* sorted by level descending (tie-break by name ascending). Returned as a
|
|
20
|
+
* <section> that GraphVisualizer injects below the graph.
|
|
21
|
+
*/
|
|
22
|
+
generateSection(graph: EnhancedGraph, workspaceRoot: string): string;
|
|
23
|
+
private renderCard;
|
|
24
|
+
/**
|
|
25
|
+
* Read a module's responsibilities.md body. Generation already guarantees the
|
|
26
|
+
* file exists (metadata validation throws otherwise); a fallback line guards
|
|
27
|
+
* the edge case where the graph JSON is stale relative to disk.
|
|
28
|
+
*/
|
|
29
|
+
private readBody;
|
|
30
|
+
/**
|
|
31
|
+
* Minimal markdown → HTML for a responsibilities.md body. Handles ATX headings
|
|
32
|
+
* (`#`..`######`), `-`/`*` bullet lists, and blank-line-separated paragraphs.
|
|
33
|
+
* Everything is HTML-escaped first; only a small inline set is re-marked. This
|
|
34
|
+
* is deliberately tiny — responsibilities.md files are short and structured,
|
|
35
|
+
* and the repo has no markdown dependency.
|
|
36
|
+
*/
|
|
37
|
+
private renderMarkdown;
|
|
38
|
+
private renderLine;
|
|
39
|
+
/**
|
|
40
|
+
* Render inline markdown spans within an already HTML-escaped line: `code` →
|
|
41
|
+
* <code>, **bold** → <strong>, *italic* → <em>.
|
|
42
|
+
*/
|
|
43
|
+
private renderInline;
|
|
44
|
+
/**
|
|
45
|
+
* Escape a string for safe embedding in HTML text/attributes.
|
|
46
|
+
*/
|
|
47
|
+
private escapeHtml;
|
|
48
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Responsibilities Section
|
|
4
|
+
*
|
|
5
|
+
* Renders every module's full `responsibilities.md` as a collapsible card below
|
|
6
|
+
* the dependency graph in architecture/dependencies.html. Cards are ordered by
|
|
7
|
+
* dependency level HIGH → LOW (top-level apps first, deepest libs last) so a
|
|
8
|
+
* reader scrolls from the runnable servers/clients down into the libraries.
|
|
9
|
+
*
|
|
10
|
+
* Each card carries `data-node="<shortName>"` matching the graph box's title, so
|
|
11
|
+
* the page script can filter the list to just the locked box's chain (see
|
|
12
|
+
* graph-visualizer.client.js). The summary paragraph is the `shortDescription`
|
|
13
|
+
* already on each graph entry; the expanded body is the rendered markdown file.
|
|
14
|
+
*/
|
|
15
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
|
+
exports.ResponsibilitiesRenderer = void 0;
|
|
17
|
+
const tslib_1 = require("tslib");
|
|
18
|
+
const fs = tslib_1.__importStar(require("fs"));
|
|
19
|
+
const path = tslib_1.__importStar(require("path"));
|
|
20
|
+
const graph_names_1 = require("./graph-names");
|
|
21
|
+
/**
|
|
22
|
+
* Mutable cursor state threaded through the markdown line renderer: whether we
|
|
23
|
+
* are currently inside an open `<ul>` (so the next blank line / heading / non-
|
|
24
|
+
* bullet closes it).
|
|
25
|
+
*/
|
|
26
|
+
class MarkdownListState {
|
|
27
|
+
inList = false;
|
|
28
|
+
}
|
|
29
|
+
class ResponsibilitiesRenderer {
|
|
30
|
+
names = new graph_names_1.GraphNames();
|
|
31
|
+
/**
|
|
32
|
+
* Build the responsibilities section HTML: one collapsible card per module,
|
|
33
|
+
* sorted by level descending (tie-break by name ascending). Returned as a
|
|
34
|
+
* <section> that GraphVisualizer injects below the graph.
|
|
35
|
+
*/
|
|
36
|
+
generateSection(graph, workspaceRoot) {
|
|
37
|
+
const projects = Object.keys(graph);
|
|
38
|
+
projects.sort((a, b) => {
|
|
39
|
+
const levelDiff = graph[b].level - graph[a].level;
|
|
40
|
+
if (levelDiff !== 0)
|
|
41
|
+
return levelDiff;
|
|
42
|
+
return a.localeCompare(b);
|
|
43
|
+
});
|
|
44
|
+
const cards = [];
|
|
45
|
+
for (const project of projects) {
|
|
46
|
+
cards.push(this.renderCard(project, graph[project], workspaceRoot));
|
|
47
|
+
}
|
|
48
|
+
return (`<section id="wp-responsibilities">` +
|
|
49
|
+
`<h2>Responsibilities (level high → low)</h2>` +
|
|
50
|
+
`<p class="hint">Lock a box above to narrow this list to just that box's dependency chain.</p>` +
|
|
51
|
+
cards.join('\n') +
|
|
52
|
+
`</section>`);
|
|
53
|
+
}
|
|
54
|
+
renderCard(project, entry, workspaceRoot) {
|
|
55
|
+
const shortName = this.names.getShortName(project);
|
|
56
|
+
const summary = entry.shortDescription ?? '';
|
|
57
|
+
const summaryHtml = summary ? ` — ${this.escapeHtml(summary)}` : '';
|
|
58
|
+
const body = this.readBody(entry, workspaceRoot);
|
|
59
|
+
return (`<details class="wp-resp-card" data-node="${this.escapeHtml(shortName)}">` +
|
|
60
|
+
`<summary><span class="wp-resp-level">L${entry.level}</span> ` +
|
|
61
|
+
`<strong>${this.escapeHtml(shortName)}</strong>${summaryHtml}</summary>` +
|
|
62
|
+
`<div class="wp-resp-body">${body}</div>` +
|
|
63
|
+
`</details>`);
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Read a module's responsibilities.md body. Generation already guarantees the
|
|
67
|
+
* file exists (metadata validation throws otherwise); a fallback line guards
|
|
68
|
+
* the edge case where the graph JSON is stale relative to disk.
|
|
69
|
+
*/
|
|
70
|
+
readBody(entry, workspaceRoot) {
|
|
71
|
+
const file = entry.responsibilitiesFile;
|
|
72
|
+
if (!file)
|
|
73
|
+
return '<p><em>No responsibilities.md recorded for this module.</em></p>';
|
|
74
|
+
const absolutePath = path.join(workspaceRoot, file);
|
|
75
|
+
if (!fs.existsSync(absolutePath)) {
|
|
76
|
+
return `<p><em>Missing ${this.escapeHtml(file)}.</em></p>`;
|
|
77
|
+
}
|
|
78
|
+
return this.renderMarkdown(fs.readFileSync(absolutePath, 'utf-8'));
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Minimal markdown → HTML for a responsibilities.md body. Handles ATX headings
|
|
82
|
+
* (`#`..`######`), `-`/`*` bullet lists, and blank-line-separated paragraphs.
|
|
83
|
+
* Everything is HTML-escaped first; only a small inline set is re-marked. This
|
|
84
|
+
* is deliberately tiny — responsibilities.md files are short and structured,
|
|
85
|
+
* and the repo has no markdown dependency.
|
|
86
|
+
*/
|
|
87
|
+
renderMarkdown(markdown) {
|
|
88
|
+
const out = [];
|
|
89
|
+
const state = new MarkdownListState();
|
|
90
|
+
for (const rawLine of markdown.split('\n')) {
|
|
91
|
+
this.renderLine(rawLine.trim(), out, state);
|
|
92
|
+
}
|
|
93
|
+
if (state.inList)
|
|
94
|
+
out.push('</ul>');
|
|
95
|
+
return out.join('\n');
|
|
96
|
+
}
|
|
97
|
+
renderLine(line, out, state) {
|
|
98
|
+
const heading = /^(#{1,6})\s+(.*)$/.exec(line);
|
|
99
|
+
const bullet = /^[-*]\s+(.*)$/.exec(line);
|
|
100
|
+
if (line.length === 0) {
|
|
101
|
+
if (state.inList) {
|
|
102
|
+
out.push('</ul>');
|
|
103
|
+
state.inList = false;
|
|
104
|
+
}
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
if (heading) {
|
|
108
|
+
if (state.inList) {
|
|
109
|
+
out.push('</ul>');
|
|
110
|
+
state.inList = false;
|
|
111
|
+
}
|
|
112
|
+
const level = Math.min(heading[1].length + 3, 6); // # → h4, ## → h5, deeper → h6
|
|
113
|
+
out.push(`<h${level}>${this.renderInline(this.escapeHtml(heading[2]))}</h${level}>`);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
if (bullet) {
|
|
117
|
+
if (!state.inList) {
|
|
118
|
+
out.push('<ul>');
|
|
119
|
+
state.inList = true;
|
|
120
|
+
}
|
|
121
|
+
out.push(`<li>${this.renderInline(this.escapeHtml(bullet[1]))}</li>`);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
if (state.inList) {
|
|
125
|
+
out.push('</ul>');
|
|
126
|
+
state.inList = false;
|
|
127
|
+
}
|
|
128
|
+
out.push(`<p>${this.renderInline(this.escapeHtml(line))}</p>`);
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Render inline markdown spans within an already HTML-escaped line: `code` →
|
|
132
|
+
* <code>, **bold** → <strong>, *italic* → <em>.
|
|
133
|
+
*/
|
|
134
|
+
renderInline(escaped) {
|
|
135
|
+
return escaped
|
|
136
|
+
.replace(/`([^`]+)`/g, '<code>$1</code>')
|
|
137
|
+
.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>')
|
|
138
|
+
.replace(/(^|[^*])\*([^*]+)\*/g, '$1<em>$2</em>');
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Escape a string for safe embedding in HTML text/attributes.
|
|
142
|
+
*/
|
|
143
|
+
escapeHtml(text) {
|
|
144
|
+
return text
|
|
145
|
+
.replace(/&/g, '&')
|
|
146
|
+
.replace(/</g, '<')
|
|
147
|
+
.replace(/>/g, '>')
|
|
148
|
+
.replace(/"/g, '"');
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
exports.ResponsibilitiesRenderer = ResponsibilitiesRenderer;
|
|
152
|
+
//# sourceMappingURL=graph-responsibilities.js.map
|
|
@@ -0,0 +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.js). 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 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"]}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Browser-side script for architecture/dependencies.html (inlined into a <script>
|
|
3
|
+
* tag by graph-visualizer.ts, which replaces the __DOT__ placeholder with the
|
|
4
|
+
* JSON-encoded Graphviz DOT). Kept as a plain .js asset — NOT a TypeScript
|
|
5
|
+
* template literal — so the dim/highlight/lock logic can define ordinary browser
|
|
6
|
+
* functions without tripping the TypeScript lint rules that scan .ts template
|
|
7
|
+
* strings. Copied into dist by the build's assets glob and read via readFileSync.
|
|
8
|
+
*
|
|
9
|
+
* After viz.js renders the SVG, wireHoverHighlight indexes nodes/edges and:
|
|
10
|
+
* - hovering a box dims the rest and lights its full ancestor+descendant chain;
|
|
11
|
+
* - the #wp-lock dropdown LOCKS one box's chain (dim persists on mouse-leave)
|
|
12
|
+
* and filters the responsibilities cards below the graph to just that chain.
|
|
13
|
+
* "All" (empty value) clears the lock and shows every card.
|
|
14
|
+
*/
|
|
15
|
+
(function () {
|
|
16
|
+
var dot = __DOT__;
|
|
17
|
+
var viz = new Viz();
|
|
18
|
+
viz.renderSVGElement(dot)
|
|
19
|
+
.then(function (element) {
|
|
20
|
+
document.getElementById('graph').appendChild(element);
|
|
21
|
+
wireHoverHighlight(element);
|
|
22
|
+
})
|
|
23
|
+
.catch(function (err) {
|
|
24
|
+
console.error(err);
|
|
25
|
+
document.getElementById('graph').innerHTML = '<pre>' + err + '</pre>';
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
function wireHoverHighlight(svg) {
|
|
29
|
+
var nodeByName = new Map();
|
|
30
|
+
svg.querySelectorAll('g.node').forEach(function (g) {
|
|
31
|
+
var t = g.querySelector('title');
|
|
32
|
+
if (t) nodeByName.set(t.textContent.trim(), g);
|
|
33
|
+
});
|
|
34
|
+
// Directed adjacency: in* = entering (up/ancestors), out* = leaving (down/deps).
|
|
35
|
+
var inEdges = new Map(), outEdges = new Map(), inNodes = new Map(), outNodes = new Map();
|
|
36
|
+
function ensure(map, key) {
|
|
37
|
+
var v = map.get(key);
|
|
38
|
+
if (!v) { v = new Set(); map.set(key, v); }
|
|
39
|
+
return v;
|
|
40
|
+
}
|
|
41
|
+
svg.querySelectorAll('g.edge').forEach(function (edge) {
|
|
42
|
+
var t = edge.querySelector('title');
|
|
43
|
+
if (!t) return;
|
|
44
|
+
var idx = t.textContent.indexOf('->');
|
|
45
|
+
if (idx < 0) return;
|
|
46
|
+
var from = t.textContent.slice(0, idx).trim();
|
|
47
|
+
var to = t.textContent.slice(idx + 2).trim();
|
|
48
|
+
ensure(outEdges, from).add(edge);
|
|
49
|
+
ensure(inEdges, to).add(edge);
|
|
50
|
+
ensure(outNodes, from).add(to);
|
|
51
|
+
ensure(inNodes, to).add(from);
|
|
52
|
+
});
|
|
53
|
+
function clear() {
|
|
54
|
+
svg.classList.remove('wp-dim');
|
|
55
|
+
svg.querySelectorAll('.wp-focus, .wp-neighbor, .wp-hl').forEach(function (el) {
|
|
56
|
+
el.classList.remove('wp-focus', 'wp-neighbor', 'wp-hl');
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
function highlight(name, focusEl) {
|
|
60
|
+
clear();
|
|
61
|
+
svg.classList.add('wp-dim');
|
|
62
|
+
focusEl.classList.add('wp-focus');
|
|
63
|
+
// Transitively light ancestors (up) then descendants (down): edges
|
|
64
|
+
// reached -> wp-hl, boxes -> wp-neighbor. visited guards cycles.
|
|
65
|
+
[[inNodes, inEdges], [outNodes, outEdges]].forEach(function (dir) {
|
|
66
|
+
var visited = new Set();
|
|
67
|
+
var stack = [name];
|
|
68
|
+
while (stack.length) {
|
|
69
|
+
var cur = stack.pop();
|
|
70
|
+
(dir[1].get(cur) || []).forEach(function (e) { e.classList.add('wp-hl'); });
|
|
71
|
+
(dir[0].get(cur) || []).forEach(function (next) {
|
|
72
|
+
if (visited.has(next)) return;
|
|
73
|
+
visited.add(next);
|
|
74
|
+
stack.push(next);
|
|
75
|
+
var g = nodeByName.get(next);
|
|
76
|
+
if (g) g.classList.add('wp-neighbor');
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
// locked = the box the dropdown pinned (or null). Hover still works on top
|
|
82
|
+
// of a lock; leaving a box restores the locked view instead of clearing, so
|
|
83
|
+
// the pinned subgraph stays visible as you scroll to its responsibilities.
|
|
84
|
+
var locked = null;
|
|
85
|
+
function relight() {
|
|
86
|
+
if (locked) {
|
|
87
|
+
var lg = nodeByName.get(locked);
|
|
88
|
+
if (lg) highlight(locked, lg);
|
|
89
|
+
} else {
|
|
90
|
+
clear();
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
nodeByName.forEach(function (g, name) {
|
|
94
|
+
g.addEventListener('mouseenter', function () { highlight(name, g); });
|
|
95
|
+
g.addEventListener('mouseleave', relight);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// Filter the responsibilities cards to the locked box's chain by reading the
|
|
99
|
+
// .wp-focus/.wp-neighbor classes highlight() just set — no second graph walk.
|
|
100
|
+
var lockSelect = document.getElementById('wp-lock');
|
|
101
|
+
function filterCards() {
|
|
102
|
+
var lit = new Set();
|
|
103
|
+
if (locked) {
|
|
104
|
+
svg.querySelectorAll('.wp-focus, .wp-neighbor').forEach(function (el) {
|
|
105
|
+
var t = el.querySelector('title');
|
|
106
|
+
if (t) lit.add(t.textContent.trim());
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
document.querySelectorAll('.wp-resp-card').forEach(function (card) {
|
|
110
|
+
var nm = card.getAttribute('data-node');
|
|
111
|
+
card.classList.toggle('wp-hidden', !(!locked || lit.has(nm)));
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
if (lockSelect) {
|
|
115
|
+
lockSelect.addEventListener('change', function () {
|
|
116
|
+
locked = lockSelect.value || null;
|
|
117
|
+
if (locked) {
|
|
118
|
+
var g = nodeByName.get(locked);
|
|
119
|
+
if (g) highlight(locked, g);
|
|
120
|
+
} else {
|
|
121
|
+
clear();
|
|
122
|
+
}
|
|
123
|
+
filterCards();
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
})();
|