@webpieces/nx-webpieces-rules 0.3.347 → 0.3.349
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/executors.json +0 -5
- package/package.json +6 -6
- package/src/executors/generate/executor.js +9 -8
- package/src/executors/generate/executor.js.map +1 -1
- package/src/executors/validate-runtime-architecture/executor.js +16 -7
- package/src/executors/validate-runtime-architecture/executor.js.map +1 -1
- package/src/lib/runtime-config.d.ts +0 -3
- package/src/lib/runtime-config.js +0 -3
- package/src/lib/runtime-config.js.map +1 -1
- package/src/lib/runtime-graph.d.ts +12 -11
- package/src/lib/runtime-graph.js +89 -121
- package/src/lib/runtime-graph.js.map +1 -1
- package/src/plugin.js +1 -6
- package/src/plugin.js.map +1 -1
- package/src/runtime-targets.d.ts +0 -2
- package/src/runtime-targets.js +1 -14
- package/src/runtime-targets.js.map +1 -1
- package/src/executors/validate-runtime-markers/executor.d.ts +0 -25
- package/src/executors/validate-runtime-markers/executor.js +0 -91
- package/src/executors/validate-runtime-markers/executor.js.map +0 -1
- package/src/executors/validate-runtime-markers/schema.json +0 -8
- package/src/lib/runtime-markers.d.ts +0 -58
- package/src/lib/runtime-markers.js +0 -130
- package/src/lib/runtime-markers.js.map +0 -1
package/executors.json
CHANGED
|
@@ -15,11 +15,6 @@
|
|
|
15
15
|
"schema": "./src/executors/visualize-runtime/schema.json",
|
|
16
16
|
"description": "Render the runtime microservice graph (runtime-dependencies.json)"
|
|
17
17
|
},
|
|
18
|
-
"validate-runtime-markers": {
|
|
19
|
-
"implementation": "./src/executors/validate-runtime-markers/executor",
|
|
20
|
-
"schema": "./src/executors/validate-runtime-markers/schema.json",
|
|
21
|
-
"description": "Per-project: validate live.json matches the project's api-project dependencies"
|
|
22
|
-
},
|
|
23
18
|
"validate-runtime-architecture": {
|
|
24
19
|
"implementation": "./src/executors/validate-runtime-architecture/executor",
|
|
25
20
|
"schema": "./src/executors/validate-runtime-architecture/schema.json",
|
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.349",
|
|
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.349",
|
|
25
|
+
"@webpieces/code-rules": "0.3.349",
|
|
26
|
+
"@webpieces/eslint-rules": "0.3.349",
|
|
27
|
+
"@webpieces/pr-gate": "0.3.349",
|
|
28
|
+
"@webpieces/rules-config": "0.3.349",
|
|
29
29
|
"madge": "8.0.0"
|
|
30
30
|
},
|
|
31
31
|
"peerDependencies": {
|
|
@@ -19,14 +19,15 @@ const graph_visualizer_1 = require("../../lib/graph-visualizer");
|
|
|
19
19
|
const runtime_graph_1 = require("../../lib/runtime-graph");
|
|
20
20
|
const toError_1 = require("../../toError");
|
|
21
21
|
/**
|
|
22
|
-
* Generate the runtime microservice graph alongside the compile-time graph, from the
|
|
23
|
-
*
|
|
24
|
-
*
|
|
22
|
+
* Generate the runtime microservice graph alongside the compile-time graph, DERIVED from the same
|
|
23
|
+
* dependencies.json (its per-project apiRelations) — one regenerate produces both committed files, and
|
|
24
|
+
* validate derives from the SAME source so they can't diverge. rpc APIs become direct runtime edges;
|
|
25
|
+
* pubsub APIs become edges the viz draws through a queue.
|
|
25
26
|
*/
|
|
26
27
|
// webpieces-disable no-function-outside-class -- executor step helper, like the rest of this executor file
|
|
27
|
-
function generateRuntimeGraph(workspaceRoot,
|
|
28
|
-
console.log('📡
|
|
29
|
-
const runtimeGraph = (0, runtime_graph_1.
|
|
28
|
+
function generateRuntimeGraph(workspaceRoot, graph, hiddenProjects) {
|
|
29
|
+
console.log('📡 Deriving runtime graph from dependencies.json (implements × uses per API)...');
|
|
30
|
+
const runtimeGraph = (0, runtime_graph_1.deriveRuntimeGraph)(graph, hiddenProjects);
|
|
30
31
|
(0, runtime_graph_1.saveRuntimeGraph)(runtimeGraph, workspaceRoot);
|
|
31
32
|
const serviceCount = Object.keys(runtimeGraph.services).length;
|
|
32
33
|
console.log(`✅ Runtime graph saved (${serviceCount} services, ${runtimeGraph.runtimeEdges.length} runtime edges)`);
|
|
@@ -53,7 +54,7 @@ async function runExecutor(options, context) {
|
|
|
53
54
|
// scanning source, so dependencies.json + the viz + the runtime graph all
|
|
54
55
|
// read the same derived truth.
|
|
55
56
|
console.log('🔎 Scanning source for implements/uses API relations...');
|
|
56
|
-
|
|
57
|
+
(0, api_scanner_1.scanAndAttachApiRelations)(workspaceRoot, enhancedGraph, projectInfos);
|
|
57
58
|
// Step 4: Save the graph
|
|
58
59
|
console.log('💾 Saving graph to architecture/dependencies.json...');
|
|
59
60
|
(0, graph_loader_1.saveGraph)(enhancedGraph, workspaceRoot, graphPath);
|
|
@@ -70,7 +71,7 @@ async function runExecutor(options, context) {
|
|
|
70
71
|
if (enhancedGraph[name].drawOnGraph === false)
|
|
71
72
|
hiddenProjects.add(name);
|
|
72
73
|
}
|
|
73
|
-
generateRuntimeGraph(workspaceRoot,
|
|
74
|
+
generateRuntimeGraph(workspaceRoot, enhancedGraph, hiddenProjects);
|
|
74
75
|
// Print summary
|
|
75
76
|
const projectCount = Object.keys(enhancedGraph).length;
|
|
76
77
|
const levels = new Set(Object.values(enhancedGraph).map((e) => e.level));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"executor.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/nx-webpieces-rules/src/executors/generate/executor.ts"],"names":[],"mappings":";AAAA;;;;;;;GAOG;;
|
|
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 { scanAndAttachApiRelations } from '../../lib/api-usage/api-scanner';\nimport type { EnhancedGraph } from '../../lib/graph-sorter';\nimport { GraphVisualizer } from '../../lib/graph-visualizer';\nimport { deriveRuntimeGraph, saveRuntimeGraph } from '../../lib/runtime-graph';\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, DERIVED from the same\n * dependencies.json (its per-project apiRelations) — one regenerate produces both committed files, and\n * validate derives from the SAME source so they can't diverge. rpc APIs become direct runtime edges;\n * pubsub APIs become edges the viz draws through a queue.\n */\n// webpieces-disable no-function-outside-class -- executor step helper, like the rest of this executor file\nfunction generateRuntimeGraph(workspaceRoot: string, graph: EnhancedGraph, hiddenProjects: Set<string>): void {\n console.log('📡 Deriving runtime graph from dependencies.json (implements × uses per API)...');\n const runtimeGraph = deriveRuntimeGraph(graph, hiddenProjects);\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 3b: Classify each api-lib edge (implements/uses + rpc/pubsub) by\n // scanning source, so dependencies.json + the viz + the runtime graph all\n // read the same derived truth.\n console.log('🔎 Scanning source for implements/uses API relations...');\n scanAndAttachApiRelations(workspaceRoot, enhancedGraph, projectInfos);\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 from the same scan.\n // Projects tagged drawOnGraph:false are threaded through so the runtime\n // graph hides them too (they stay flagged in runtime-dependencies.json).\n const hiddenProjects = new Set<string>();\n for (const name of Object.keys(enhancedGraph)) {\n if (enhancedGraph[name].drawOnGraph === false) hiddenProjects.add(name);\n }\n generateRuntimeGraph(workspaceRoot, enhancedGraph, hiddenProjects);\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"]}
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
*/
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
17
|
exports.default = runExecutor;
|
|
18
|
-
const
|
|
18
|
+
const graph_loader_1 = require("../../lib/graph-loader");
|
|
19
19
|
const runtime_graph_1 = require("../../lib/runtime-graph");
|
|
20
20
|
const runtime_cycles_1 = require("../../lib/runtime-cycles");
|
|
21
21
|
const runtime_config_1 = require("../../lib/runtime-config");
|
|
@@ -64,13 +64,22 @@ async function runExecutor(_options, context) {
|
|
|
64
64
|
console.log(`\n⏭️ Skipping ${runtime_config_1.RUNTIME_RULE_NAME} (mode: OFF)\n`);
|
|
65
65
|
return { success: true };
|
|
66
66
|
}
|
|
67
|
-
if (config.servicePaths.length === 0) {
|
|
68
|
-
console.log(`\n⏭️ ${runtime_config_1.RUNTIME_RULE_NAME}: no servicePaths configured — nothing to validate\n`);
|
|
69
|
-
return { success: true };
|
|
70
|
-
}
|
|
71
67
|
console.log('\n📡 Validating runtime microservice architecture\n');
|
|
72
|
-
|
|
73
|
-
|
|
68
|
+
// Derive the "current" runtime graph from the committed dependencies.json — the SAME source
|
|
69
|
+
// architecture:generate derives from — so the unchanged-check compares like-for-like and can
|
|
70
|
+
// never fail on a clean, freshly-generated tree. (validate-architecture-unchanged separately
|
|
71
|
+
// guarantees dependencies.json itself is fresh.)
|
|
72
|
+
const depsFile = (0, graph_loader_1.loadBlessedGraph)(workspaceRoot);
|
|
73
|
+
if (depsFile === null) {
|
|
74
|
+
console.error('❌ No architecture/dependencies.json — run: nx run architecture:generate');
|
|
75
|
+
return { success: false };
|
|
76
|
+
}
|
|
77
|
+
const hiddenProjects = new Set();
|
|
78
|
+
for (const name of Object.keys(depsFile.projects)) {
|
|
79
|
+
if (depsFile.projects[name].drawOnGraph === false)
|
|
80
|
+
hiddenProjects.add(name);
|
|
81
|
+
}
|
|
82
|
+
const graph = (0, runtime_graph_1.deriveRuntimeGraph)(depsFile.projects, hiddenProjects);
|
|
74
83
|
const problems = checkCycles(graph, config.allowedCycles);
|
|
75
84
|
const unchanged = checkUnchanged(workspaceRoot, graph);
|
|
76
85
|
if (unchanged)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"executor.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/nx-webpieces-rules/src/executors/validate-runtime-architecture/executor.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;GAaG;;AA+DH,
|
|
1
|
+
{"version":3,"file":"executor.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/nx-webpieces-rules/src/executors/validate-runtime-architecture/executor.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Validate Runtime Architecture Executor (workspace)\n *\n * Two workspace-level checks on the runtime microservice graph:\n * 1. No-cycles: every runtime cycle must be in the `allowedCycles` allowlist\n * with an unexpired `until`; any other cycle fails the build.\n * 2. Unchanged: the freshly-assembled graph must match the committed\n * architecture/runtime-dependencies.json (run `architecture:generate`).\n *\n * On/off + a whole-rule grace window come from webpieces.config.json\n * (rule: runtime-architecture).\n *\n * Usage: nx run architecture:validate-runtime-architecture\n */\n\nimport type { ExecutorContext } from '@nx/devkit';\nimport { loadBlessedGraph } from '../../lib/graph-loader';\nimport {\n deriveRuntimeGraph,\n loadRuntimeGraph,\n runtimeAdjacency,\n runtimeGraphFileExists,\n serializeRuntimeGraph,\n} from '../../lib/runtime-graph';\nimport type { RuntimeGraph } from '../../lib/runtime-graph';\nimport { findRuntimeCycles, cycleKey } from '../../lib/runtime-cycles';\nimport type { AllowedCycle } from '../../lib/runtime-config';\nimport { loadRuntimeConfig, runtimeReportOnly, RUNTIME_RULE_NAME } from '../../lib/runtime-config';\n\nexport interface ValidateRuntimeArchitectureOptions {\n // Config comes from webpieces.config.json at runtime.\n}\n\nexport interface ExecutorResult {\n success: boolean;\n}\n\n/** Allowlist keys (sorted-services -> entry) that are still in their grace window. */\nfunction activeAllowedKeys(allowed: AllowedCycle[]): Map<string, AllowedCycle> {\n const nowSeconds = Date.now() / 1000;\n const map = new Map<string, AllowedCycle>();\n for (const entry of allowed) {\n const active = entry.until === undefined || nowSeconds < entry.until;\n if (active) map.set(cycleKey(entry.services), entry);\n }\n return map;\n}\n\n/** Returns disallowed-cycle messages (empty = all cycles allowed or none). */\nfunction checkCycles(graph: RuntimeGraph, allowed: AllowedCycle[]): string[] {\n const cycles = findRuntimeCycles(runtimeAdjacency(graph));\n if (cycles.length === 0) return [];\n\n const activeAllow = activeAllowedKeys(allowed);\n const problems: string[] = [];\n for (const cycle of cycles) {\n const entry = activeAllow.get(cycle.key);\n if (entry) {\n console.log(`⏳ Allowed runtime cycle [${cycle.services.join(' <-> ')}] — ${entry.reason ?? 'no reason given'}`);\n continue;\n }\n problems.push(`runtime cycle: ${cycle.services.join(' -> ')} -> ${cycle.services[0]}`);\n }\n return problems;\n}\n\n/** Returns an unchanged-check message, or null if the committed graph matches. */\nfunction checkUnchanged(workspaceRoot: string, current: RuntimeGraph): string | null {\n if (!runtimeGraphFileExists(workspaceRoot)) {\n return 'No committed architecture/runtime-dependencies.json — run: nx run architecture:generate';\n }\n const saved = loadRuntimeGraph(workspaceRoot);\n if (saved && serializeRuntimeGraph(saved) === serializeRuntimeGraph(current)) return null;\n return 'Runtime graph changed since last commit — run: nx run architecture:generate and commit the result';\n}\n\nexport default async function runExecutor(\n _options: ValidateRuntimeArchitectureOptions,\n context: ExecutorContext,\n): Promise<ExecutorResult> {\n const workspaceRoot = context.root;\n const config = loadRuntimeConfig(workspaceRoot);\n\n if (config.off) {\n console.log(`\\n⏭️ Skipping ${RUNTIME_RULE_NAME} (mode: OFF)\\n`);\n return { success: true };\n }\n\n console.log('\\n📡 Validating runtime microservice architecture\\n');\n // Derive the \"current\" runtime graph from the committed dependencies.json — the SAME source\n // architecture:generate derives from — so the unchanged-check compares like-for-like and can\n // never fail on a clean, freshly-generated tree. (validate-architecture-unchanged separately\n // guarantees dependencies.json itself is fresh.)\n const depsFile = loadBlessedGraph(workspaceRoot);\n if (depsFile === null) {\n console.error('❌ No architecture/dependencies.json — run: nx run architecture:generate');\n return { success: false };\n }\n const hiddenProjects = new Set<string>();\n for (const name of Object.keys(depsFile.projects)) {\n if (depsFile.projects[name].drawOnGraph === false) hiddenProjects.add(name);\n }\n const graph = deriveRuntimeGraph(depsFile.projects, hiddenProjects);\n\n const problems: string[] = checkCycles(graph, config.allowedCycles);\n const unchanged = checkUnchanged(workspaceRoot, graph);\n if (unchanged) problems.push(unchanged);\n\n for (const u of graph.unresolvedUses) {\n console.log(`⚠️ ${u.service} uses \"${u.api}\" but no in-repo service implements it (external?)`);\n }\n\n if (problems.length === 0) {\n console.log('✅ Runtime architecture valid (no disallowed cycles, graph unchanged)\\n');\n return { success: true };\n }\n\n console.error('\\n❌ Runtime architecture validation failed:\\n');\n for (const p of problems) console.error(` - ${p}`);\n console.error('\\nAllow a cycle temporarily via runtime-architecture.allowedCycles (services + reason + until) in webpieces.config.json.\\n');\n\n const reportOnly = runtimeReportOnly(config);\n if (reportOnly.skip) {\n console.log(`⏳ Reported but not failing (${reportOnly.reason}).\\n`);\n return { success: true };\n }\n return { success: false };\n}\n"]}
|
|
@@ -6,7 +6,6 @@
|
|
|
6
6
|
*
|
|
7
7
|
* "runtime-architecture": {
|
|
8
8
|
* "mode": "ON", // "OFF" disables the whole feature
|
|
9
|
-
* "apiProjectPaths": ["libraries/apis/*"],
|
|
10
9
|
* "ignoreModifiedUntilEpoch": 0, // whole-rule punt (epoch seconds)
|
|
11
10
|
* "allowedCycles": [ { "services": ["a","b"], "reason": "...", "until": 1771931925 } ]
|
|
12
11
|
* }
|
|
@@ -20,8 +19,6 @@ export interface AllowedCycle {
|
|
|
20
19
|
}
|
|
21
20
|
export interface RuntimeRuleConfig {
|
|
22
21
|
off: boolean;
|
|
23
|
-
apiProjectPaths: string[];
|
|
24
|
-
servicePaths: string[];
|
|
25
22
|
ignoreModifiedUntilEpoch?: number;
|
|
26
23
|
ignoreRuleWhileOnBranch?: string;
|
|
27
24
|
allowedCycles: AllowedCycle[];
|
|
@@ -7,7 +7,6 @@
|
|
|
7
7
|
*
|
|
8
8
|
* "runtime-architecture": {
|
|
9
9
|
* "mode": "ON", // "OFF" disables the whole feature
|
|
10
|
-
* "apiProjectPaths": ["libraries/apis/*"],
|
|
11
10
|
* "ignoreModifiedUntilEpoch": 0, // whole-rule punt (epoch seconds)
|
|
12
11
|
* "allowedCycles": [ { "services": ["a","b"], "reason": "...", "until": 1771931925 } ]
|
|
13
12
|
* }
|
|
@@ -30,8 +29,6 @@ function loadRuntimeConfig(workspaceRoot) {
|
|
|
30
29
|
const raw = (rule?.options ?? {});
|
|
31
30
|
return {
|
|
32
31
|
off: rule?.isOff ?? false,
|
|
33
|
-
apiProjectPaths: Array.isArray(raw.apiProjectPaths) ? raw.apiProjectPaths : [],
|
|
34
|
-
servicePaths: Array.isArray(raw.servicePaths) ? raw.servicePaths : [],
|
|
35
32
|
ignoreModifiedUntilEpoch: typeof raw.ignoreModifiedUntilEpoch === 'number' ? raw.ignoreModifiedUntilEpoch : undefined,
|
|
36
33
|
ignoreRuleWhileOnBranch: typeof raw.ignoreRuleWhileOnBranch === 'string' ? raw.ignoreRuleWhileOnBranch : undefined,
|
|
37
34
|
allowedCycles: Array.isArray(raw.allowedCycles) ? raw.allowedCycles.filter(isUsableCycle) : [],
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runtime-config.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/runtime-config.ts"],"names":[],"mappings":";AAAA
|
|
1
|
+
{"version":3,"file":"runtime-config.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/runtime-config.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;GAWG;;;AAmCH,8CAYC;AAQD,8CAEC;AAMD,sCAGC;AAGD,8BAEC;AArED,0DAA0F;AAE7E,QAAA,iBAAiB,GAAG,sBAAsB,CAAC;AA0BxD,SAAS,aAAa,CAAC,KAAmB;IACtC,OAAO,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,QAAQ,CAAC,IAAI,KAAK,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC;AACtE,CAAC;AAED,sEAAsE;AACtE,SAAgB,iBAAiB,CAAC,aAAqB;IACnD,MAAM,MAAM,GAAG,IAAA,8BAAe,EAAC,aAAa,CAAC,CAAC,QAAQ,CAAC;IACvD,MAAM,IAAI,GAAG,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,yBAAiB,CAAC,CAAC;IACjD,MAAM,GAAG,GAAG,CAAC,IAAI,EAAE,OAAO,IAAI,EAAE,CAAmB,CAAC;IACpD,OAAO;QACH,GAAG,EAAE,IAAI,EAAE,KAAK,IAAI,KAAK;QACzB,wBAAwB,EACpB,OAAO,GAAG,CAAC,wBAAwB,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,wBAAwB,CAAC,CAAC,CAAC,SAAS;QAC/F,uBAAuB,EACnB,OAAO,GAAG,CAAC,uBAAuB,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,uBAAuB,CAAC,CAAC,CAAC,SAAS;QAC7F,aAAa,EAAE,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,aAAa,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,EAAE;KACjG,CAAC;AACN,CAAC;AAED;;;;;GAKG;AACH,SAAgB,iBAAiB,CAAC,MAAyB;IACvD,OAAO,IAAA,6BAAc,EAAC,MAAM,CAAC,wBAAwB,EAAE,MAAM,CAAC,uBAAuB,CAAC,CAAC;AAC3F,CAAC;AAED;;;GAGG;AACH,SAAgB,aAAa,CAAC,KAAyB;IACnD,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO,KAAK,CAAC;IACtC,OAAO,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,GAAG,KAAK,CAAC;AACrC,CAAC;AAED,wDAAwD;AACxD,SAAgB,SAAS,CAAC,KAAa;IACnC,OAAO,IAAI,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;AAC9D,CAAC","sourcesContent":["/**\n * Runtime Config\n *\n * Loads the `runtime-architecture` rule from webpieces.config.json and exposes\n * typed accessors shared by the generate + validate + visualize executors.\n *\n * \"runtime-architecture\": {\n * \"mode\": \"ON\", // \"OFF\" disables the whole feature\n * \"ignoreModifiedUntilEpoch\": 0, // whole-rule punt (epoch seconds)\n * \"allowedCycles\": [ { \"services\": [\"a\",\"b\"], \"reason\": \"...\", \"until\": 1771931925 } ]\n * }\n */\n\nimport { loadAndValidate, shouldSkipRule, SkipRuleResult } from '@webpieces/rules-config';\n\nexport const RUNTIME_RULE_NAME = 'runtime-architecture';\n\nexport interface AllowedCycle {\n services: string[];\n reason?: string;\n until?: number;\n}\n\nexport interface RuntimeRuleConfig {\n off: boolean;\n ignoreModifiedUntilEpoch?: number;\n ignoreRuleWhileOnBranch?: string;\n allowedCycles: AllowedCycle[];\n}\n\n/**\n * Typed view of the opaque webpieces.config.json option bag for this rule. The\n * config is trusted (it is the workspace's own file), so we cast once here and\n * defensively narrow arrays/numbers rather than threading `unknown` everywhere.\n */\ninterface RuntimeRuleRaw {\n ignoreModifiedUntilEpoch?: number;\n ignoreRuleWhileOnBranch?: string;\n allowedCycles?: AllowedCycle[];\n}\n\nfunction isUsableCycle(cycle: AllowedCycle): boolean {\n return Array.isArray(cycle.services) && cycle.services.length > 0;\n}\n\n/** Load the runtime-architecture rule config (with safe defaults). */\nexport function loadRuntimeConfig(workspaceRoot: string): RuntimeRuleConfig {\n const shared = loadAndValidate(workspaceRoot).resolved;\n const rule = shared.rules.get(RUNTIME_RULE_NAME);\n const raw = (rule?.options ?? {}) as RuntimeRuleRaw;\n return {\n off: rule?.isOff ?? false,\n ignoreModifiedUntilEpoch:\n typeof raw.ignoreModifiedUntilEpoch === 'number' ? raw.ignoreModifiedUntilEpoch : undefined,\n ignoreRuleWhileOnBranch:\n typeof raw.ignoreRuleWhileOnBranch === 'string' ? raw.ignoreRuleWhileOnBranch : undefined,\n allowedCycles: Array.isArray(raw.allowedCycles) ? raw.allowedCycles.filter(isUsableCycle) : [],\n };\n}\n\n/**\n * Whole-rule report-only window honoring BOTH escape hatches: skip while on the\n * named branch (ignoreRuleWhileOnBranch) or until the epoch passes\n * (ignoreModifiedUntilEpoch). When `.skip` is true, problems are reported but\n * the build is not failed.\n */\nexport function runtimeReportOnly(config: RuntimeRuleConfig): SkipRuleResult {\n return shouldSkipRule(config.ignoreModifiedUntilEpoch, config.ignoreRuleWhileOnBranch);\n}\n\n/**\n * Whole-rule grace window: while now < epoch, failures are reported but do not\n * fail the build (warn). Mirrors the other webpieces rules.\n */\nexport function isGraceActive(epoch: number | undefined): boolean {\n if (epoch === undefined) return false;\n return Date.now() / 1000 < epoch;\n}\n\n/** Format the epoch as an ISO date for log messages. */\nexport function epochDate(epoch: number): string {\n return new Date(epoch * 1000).toISOString().split('T')[0];\n}\n"]}
|
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Runtime Graph
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* `
|
|
6
|
-
*
|
|
4
|
+
* Derives the runtime microservice graph SOLELY from architecture/dependencies.json
|
|
5
|
+
* (the single source of truth): each project's `apiRelations` carry which api
|
|
6
|
+
* classes it implements/uses and their transport (rpc | pubsub). Both
|
|
7
|
+
* `architecture:generate` and `architecture:validate-runtime-architecture` call
|
|
8
|
+
* the SAME `deriveRuntimeGraph`, so the committed graph and the validated graph
|
|
9
|
+
* can never diverge.
|
|
7
10
|
*
|
|
8
11
|
* The runtime edge Z -> X (Z depends on X at runtime) is INFERRED: Z `uses` api
|
|
9
12
|
* Y and X `implements` api Y. This edge does not exist in the compile-time
|
|
10
13
|
* dependencies.json (both Z and X only compile-depend on the api library Y).
|
|
11
14
|
*/
|
|
12
|
-
import type {
|
|
13
|
-
import type { ApiScanResult } from './api-usage/api-scanner';
|
|
15
|
+
import type { EnhancedGraph } from './graph-sorter';
|
|
14
16
|
import type { ApiTransport } from './api-usage/api-relations';
|
|
15
17
|
export declare const DEFAULT_RUNTIME_GRAPH_PATH = "architecture/runtime-dependencies.json";
|
|
16
18
|
export interface RuntimeService {
|
|
@@ -56,14 +58,13 @@ export interface RuntimeGraph {
|
|
|
56
58
|
}
|
|
57
59
|
/** Adjacency (service -> [targets]) from a loaded runtime graph. */
|
|
58
60
|
export declare function runtimeAdjacency(graph: RuntimeGraph): Record<string, string[]>;
|
|
59
|
-
/** Assemble the full runtime graph from the workspace model + service contracts. */
|
|
60
|
-
export declare function assembleRuntimeGraph(model: WorkspaceModel, workspaceRoot: string): RuntimeGraph;
|
|
61
61
|
/**
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
62
|
+
* Derive the runtime graph from dependencies.json's project `apiRelations` — the
|
|
63
|
+
* single source of truth shared by generate + validate. `hiddenProjects`
|
|
64
|
+
* (drawOnGraph:false, defaults to none) are kept in the graph but flagged so the
|
|
65
|
+
* runtime visualizer omits their nodes + edges.
|
|
65
66
|
*/
|
|
66
|
-
export declare function
|
|
67
|
+
export declare function deriveRuntimeGraph(projects: EnhancedGraph, hiddenProjects?: Set<string>): RuntimeGraph;
|
|
67
68
|
export declare function saveRuntimeGraph(graph: RuntimeGraph, workspaceRoot: string, graphPath?: string): void;
|
|
68
69
|
export declare function runtimeGraphFileExists(workspaceRoot: string, graphPath?: string): boolean;
|
|
69
70
|
export declare function loadRuntimeGraph(workspaceRoot: string, graphPath?: string): RuntimeGraph | null;
|
package/src/lib/runtime-graph.js
CHANGED
|
@@ -2,9 +2,12 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Runtime Graph
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
* `
|
|
7
|
-
*
|
|
5
|
+
* Derives the runtime microservice graph SOLELY from architecture/dependencies.json
|
|
6
|
+
* (the single source of truth): each project's `apiRelations` carry which api
|
|
7
|
+
* classes it implements/uses and their transport (rpc | pubsub). Both
|
|
8
|
+
* `architecture:generate` and `architecture:validate-runtime-architecture` call
|
|
9
|
+
* the SAME `deriveRuntimeGraph`, so the committed graph and the validated graph
|
|
10
|
+
* can never diverge.
|
|
8
11
|
*
|
|
9
12
|
* The runtime edge Z -> X (Z depends on X at runtime) is INFERRED: Z `uses` api
|
|
10
13
|
* Y and X `implements` api Y. This edge does not exist in the compile-time
|
|
@@ -13,8 +16,7 @@
|
|
|
13
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
17
|
exports.DEFAULT_RUNTIME_GRAPH_PATH = void 0;
|
|
15
18
|
exports.runtimeAdjacency = runtimeAdjacency;
|
|
16
|
-
exports.
|
|
17
|
-
exports.assembleRuntimeGraphFromScan = assembleRuntimeGraphFromScan;
|
|
19
|
+
exports.deriveRuntimeGraph = deriveRuntimeGraph;
|
|
18
20
|
exports.saveRuntimeGraph = saveRuntimeGraph;
|
|
19
21
|
exports.runtimeGraphFileExists = runtimeGraphFileExists;
|
|
20
22
|
exports.loadRuntimeGraph = loadRuntimeGraph;
|
|
@@ -23,81 +25,9 @@ const tslib_1 = require("tslib");
|
|
|
23
25
|
const fs = tslib_1.__importStar(require("fs"));
|
|
24
26
|
const path = tslib_1.__importStar(require("path"));
|
|
25
27
|
const graph_sorter_1 = require("./graph-sorter");
|
|
26
|
-
const runtime_markers_1 = require("./runtime-markers");
|
|
27
28
|
const api_relations_1 = require("./api-usage/api-relations");
|
|
28
29
|
const toError_1 = require("../toError");
|
|
29
30
|
exports.DEFAULT_RUNTIME_GRAPH_PATH = 'architecture/runtime-dependencies.json';
|
|
30
|
-
/** Collect every service project (servicePaths), with its declarations resolved to projects. */
|
|
31
|
-
function collectServiceDecls(model, workspaceRoot) {
|
|
32
|
-
const decls = [];
|
|
33
|
-
for (const info of model.projects.values()) {
|
|
34
|
-
if (!info.isService)
|
|
35
|
-
continue;
|
|
36
|
-
// A service missing its contract still appears as a node (empty edges);
|
|
37
|
-
// validate-runtime-markers fails it separately for the missing file.
|
|
38
|
-
const contract = (0, runtime_markers_1.readServiceContract)(workspaceRoot, info.root);
|
|
39
|
-
const implementsPkgs = contract ? contract.implements : [];
|
|
40
|
-
const usesPkgs = contract ? contract.uses : [];
|
|
41
|
-
decls.push({
|
|
42
|
-
name: info.name,
|
|
43
|
-
implements: (0, runtime_markers_1.resolvePackageNames)(model, implementsPkgs).projects.sort(),
|
|
44
|
-
uses: (0, runtime_markers_1.resolvePackageNames)(model, usesPkgs).projects.sort(),
|
|
45
|
-
});
|
|
46
|
-
}
|
|
47
|
-
return decls.sort((a, b) => a.name.localeCompare(b.name));
|
|
48
|
-
}
|
|
49
|
-
function buildApiIndex(decls) {
|
|
50
|
-
const apis = new Map();
|
|
51
|
-
const ensure = (api) => {
|
|
52
|
-
let entry = apis.get(api);
|
|
53
|
-
if (!entry) {
|
|
54
|
-
entry = { implementedBy: [], usedBy: [] };
|
|
55
|
-
apis.set(api, entry);
|
|
56
|
-
}
|
|
57
|
-
return entry;
|
|
58
|
-
};
|
|
59
|
-
for (const decl of decls) {
|
|
60
|
-
for (const api of decl.implements)
|
|
61
|
-
ensure(api).implementedBy.push(decl.name);
|
|
62
|
-
for (const api of decl.uses)
|
|
63
|
-
ensure(api).usedBy.push(decl.name);
|
|
64
|
-
}
|
|
65
|
-
for (const entry of apis.values()) {
|
|
66
|
-
entry.implementedBy.sort();
|
|
67
|
-
entry.usedBy.sort();
|
|
68
|
-
}
|
|
69
|
-
return apis;
|
|
70
|
-
}
|
|
71
|
-
/** Build inferred runtime edges (Z -> X via Y) and the unresolved-uses list. */
|
|
72
|
-
function buildEdges(decls, apis) {
|
|
73
|
-
const viaByEdge = new Map();
|
|
74
|
-
const unresolved = [];
|
|
75
|
-
for (const decl of decls) {
|
|
76
|
-
for (const api of decl.uses) {
|
|
77
|
-
const implementers = apis.get(api)?.implementedBy ?? [];
|
|
78
|
-
if (implementers.length === 0) {
|
|
79
|
-
unresolved.push({ service: decl.name, api });
|
|
80
|
-
continue;
|
|
81
|
-
}
|
|
82
|
-
for (const target of implementers) {
|
|
83
|
-
if (target === decl.name)
|
|
84
|
-
continue;
|
|
85
|
-
const key = `${decl.name} ${target}`;
|
|
86
|
-
if (!viaByEdge.has(key))
|
|
87
|
-
viaByEdge.set(key, new Set());
|
|
88
|
-
viaByEdge.get(key).add(api);
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
const edges = [];
|
|
93
|
-
for (const key of viaByEdge.keys()) {
|
|
94
|
-
const parts = key.split(' ');
|
|
95
|
-
edges.push({ from: parts[0], to: parts[1], via: Array.from(viaByEdge.get(key)).sort() });
|
|
96
|
-
}
|
|
97
|
-
edges.sort((a, b) => a.from.localeCompare(b.from) || a.to.localeCompare(b.to));
|
|
98
|
-
unresolved.sort((a, b) => a.service.localeCompare(b.service) || a.api.localeCompare(b.api));
|
|
99
|
-
return { edges, unresolved };
|
|
100
|
-
}
|
|
101
31
|
/** Adjacency (service -> [targets]) used for leveling + cycle checks. */
|
|
102
32
|
function adjacencyFromEdges(serviceNames, edges) {
|
|
103
33
|
const adj = {};
|
|
@@ -131,41 +61,22 @@ function assignLevels(adjacency) {
|
|
|
131
61
|
}
|
|
132
62
|
return levels;
|
|
133
63
|
}
|
|
134
|
-
/** Assemble the full runtime graph from the workspace model + service contracts. */
|
|
135
|
-
function assembleRuntimeGraph(model, workspaceRoot) {
|
|
136
|
-
const decls = collectServiceDecls(model, workspaceRoot);
|
|
137
|
-
const apis = buildApiIndex(decls);
|
|
138
|
-
const edgeResult = buildEdges(decls, apis);
|
|
139
|
-
const services = {};
|
|
140
|
-
for (const decl of decls) {
|
|
141
|
-
const dependsOn = Array.from(new Set(edgeResult.edges.filter((e) => e.from === decl.name).map((e) => e.to))).sort();
|
|
142
|
-
services[decl.name] = { level: 0, implements: decl.implements, uses: decl.uses, dependsOn };
|
|
143
|
-
}
|
|
144
|
-
const levels = assignLevels(adjacencyFromEdges(Object.keys(services), edgeResult.edges));
|
|
145
|
-
for (const name of Object.keys(services))
|
|
146
|
-
services[name].level = levels[name] ?? 0;
|
|
147
|
-
const apisObj = {};
|
|
148
|
-
for (const api of Array.from(apis.keys()).sort())
|
|
149
|
-
apisObj[api] = apis.get(api);
|
|
150
|
-
return {
|
|
151
|
-
services,
|
|
152
|
-
apis: apisObj,
|
|
153
|
-
runtimeEdges: edgeResult.edges,
|
|
154
|
-
unresolvedUses: edgeResult.unresolved,
|
|
155
|
-
};
|
|
156
|
-
}
|
|
157
64
|
/**
|
|
158
|
-
*
|
|
159
|
-
*
|
|
160
|
-
*
|
|
65
|
+
* Derives the runtime microservice graph from architecture/dependencies.json `apiRelations`:
|
|
66
|
+
* implementers × users per API, split by transport. An rpc edge is a direct call; a pubsub edge flows
|
|
67
|
+
* through a queue (drawn producer → queue → consumer by the visualizer).
|
|
68
|
+
*
|
|
69
|
+
* Nodes are the runnable apps only — role:server / role:client. A library is NEVER a node; its
|
|
70
|
+
* implements/uses (a shared client-factory or route-registration lib) are attributed to the
|
|
71
|
+
* servers/clients that embed it, walked transitively over dependsOn (see collectDecls).
|
|
161
72
|
*/
|
|
162
|
-
class
|
|
163
|
-
|
|
73
|
+
class RuntimeGraphDeriver {
|
|
74
|
+
projects;
|
|
164
75
|
hiddenProjects;
|
|
165
|
-
constructor(
|
|
76
|
+
constructor(projects,
|
|
166
77
|
/** Project names tagged drawOnGraph:false — kept in the JSON but flagged so the viz omits them. */
|
|
167
78
|
hiddenProjects) {
|
|
168
|
-
this.
|
|
79
|
+
this.projects = projects;
|
|
169
80
|
this.hiddenProjects = hiddenProjects;
|
|
170
81
|
}
|
|
171
82
|
assemble() {
|
|
@@ -178,23 +89,64 @@ class ScanRuntimeAssembler {
|
|
|
178
89
|
apisObj[api] = apis.get(api);
|
|
179
90
|
return { services, apis: apisObj, runtimeEdges: edgeResult.edges, unresolvedUses: edgeResult.unresolved };
|
|
180
91
|
}
|
|
181
|
-
/**
|
|
92
|
+
/**
|
|
93
|
+
* One ScanDecl per RUNTIME NODE (role:server / role:client). A node's effective
|
|
94
|
+
* relations are its OWN apiRelations PLUS those of every library in its transitive
|
|
95
|
+
* dependsOn closure: a lib that calls createRpcClient/addRoutes runs inside the
|
|
96
|
+
* server/client that embeds it, so the relation is attributed to that node — the
|
|
97
|
+
* lib is never a runtime node. Recursion stops at other nodes (each server/client
|
|
98
|
+
* owns its own relations). dependsOn is transitively reduced, but reduction
|
|
99
|
+
* preserves reachability, so the recursive walk still reaches every lib a node
|
|
100
|
+
* embeds through libraries.
|
|
101
|
+
*/
|
|
182
102
|
collectDecls() {
|
|
183
103
|
const decls = [];
|
|
184
|
-
for (const name of
|
|
185
|
-
|
|
104
|
+
for (const name of Object.keys(this.projects).sort()) {
|
|
105
|
+
if (!this.isNode(name))
|
|
106
|
+
continue;
|
|
186
107
|
const implementsApis = [];
|
|
187
108
|
const usesApis = [];
|
|
188
|
-
|
|
189
|
-
implementsApis.push(...relations[owner].implements);
|
|
190
|
-
usesApis.push(...relations[owner].uses);
|
|
191
|
-
}
|
|
109
|
+
this.collectEffectiveRelations(name, implementsApis, usesApis, new Set([name]));
|
|
192
110
|
if (implementsApis.length > 0 || usesApis.length > 0) {
|
|
193
|
-
decls.push({
|
|
111
|
+
decls.push({
|
|
112
|
+
name,
|
|
113
|
+
implementsApis: dedupApiRefs((0, api_relations_1.sortApiRefs)(implementsApis)),
|
|
114
|
+
usesApis: dedupApiRefs((0, api_relations_1.sortApiRefs)(usesApis)),
|
|
115
|
+
});
|
|
194
116
|
}
|
|
195
117
|
}
|
|
196
118
|
return decls;
|
|
197
119
|
}
|
|
120
|
+
/** True when a project is a runtime node — a server or client app (never a library). */
|
|
121
|
+
isNode(name) {
|
|
122
|
+
const role = this.projects[name]?.role;
|
|
123
|
+
return role === 'server' || role === 'client';
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Accumulate `name`'s own api relations, then recurse through its LIBRARY deps
|
|
127
|
+
* (skipping other nodes, which own their own relations). `visited` guards against
|
|
128
|
+
* re-walking a lib reachable by more than one path (and any cycle).
|
|
129
|
+
*/
|
|
130
|
+
collectEffectiveRelations(name, impl, uses, visited) {
|
|
131
|
+
const entry = this.projects[name];
|
|
132
|
+
if (entry === undefined)
|
|
133
|
+
return;
|
|
134
|
+
const relations = entry.apiRelations;
|
|
135
|
+
if (relations !== undefined) {
|
|
136
|
+
for (const owner of Object.keys(relations)) {
|
|
137
|
+
impl.push(...relations[owner].implements);
|
|
138
|
+
uses.push(...relations[owner].uses);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
for (const dep of entry.dependsOn) {
|
|
142
|
+
if (visited.has(dep))
|
|
143
|
+
continue;
|
|
144
|
+
visited.add(dep);
|
|
145
|
+
if (this.isNode(dep))
|
|
146
|
+
continue; // another server/client owns its own relations
|
|
147
|
+
this.collectEffectiveRelations(dep, impl, uses, visited);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
198
150
|
/** apiClassName -> { implementedBy, usedBy, type }. */
|
|
199
151
|
buildApis(decls) {
|
|
200
152
|
const apis = new Map();
|
|
@@ -270,13 +222,29 @@ class ScanRuntimeAssembler {
|
|
|
270
222
|
}
|
|
271
223
|
}
|
|
272
224
|
/**
|
|
273
|
-
*
|
|
274
|
-
*
|
|
275
|
-
*
|
|
225
|
+
* Derive the runtime graph from dependencies.json's project `apiRelations` — the
|
|
226
|
+
* single source of truth shared by generate + validate. `hiddenProjects`
|
|
227
|
+
* (drawOnGraph:false, defaults to none) are kept in the graph but flagged so the
|
|
228
|
+
* runtime visualizer omits their nodes + edges.
|
|
276
229
|
*/
|
|
277
|
-
// webpieces-disable no-function-outside-class -- module entry point
|
|
278
|
-
function
|
|
279
|
-
return new
|
|
230
|
+
// webpieces-disable no-function-outside-class -- module entry point for the runtime graph derivation
|
|
231
|
+
function deriveRuntimeGraph(projects, hiddenProjects = new Set()) {
|
|
232
|
+
return new RuntimeGraphDeriver(projects, hiddenProjects).assemble();
|
|
233
|
+
}
|
|
234
|
+
/** Drop duplicate api refs (same api class), keeping the first — needed after a node absorbs the
|
|
235
|
+
* same api from both its own relations and an embedded lib's. Input is pre-sorted, so output stays
|
|
236
|
+
* deterministic. */
|
|
237
|
+
// webpieces-disable no-function-outside-class -- pure list helper, matches the sibling helpers in this file
|
|
238
|
+
function dedupApiRefs(refs) {
|
|
239
|
+
const seen = new Set();
|
|
240
|
+
const out = [];
|
|
241
|
+
for (const ref of refs) {
|
|
242
|
+
if (seen.has(ref.api))
|
|
243
|
+
continue;
|
|
244
|
+
seen.add(ref.api);
|
|
245
|
+
out.push(ref);
|
|
246
|
+
}
|
|
247
|
+
return out;
|
|
280
248
|
}
|
|
281
249
|
// webpieces-disable no-function-outside-class -- pure sort helper, matches the sibling helpers in this file
|
|
282
250
|
function sortUnresolved(unresolved) {
|