@webpieces/nx-webpieces-rules 0.4.460 → 0.4.461
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 +16 -4
- package/src/executors/generate/executor.js.map +1 -1
- package/src/executors/validate-runtime-architecture/executor.js +4 -1
- package/src/executors/validate-runtime-architecture/executor.js.map +1 -1
- package/src/executors/visualize-runtime/executor.js +4 -1
- package/src/executors/visualize-runtime/executor.js.map +1 -1
- package/src/lib/api-usage/api-relations.d.ts +21 -1
- package/src/lib/api-usage/api-relations.js +14 -2
- package/src/lib/api-usage/api-relations.js.map +1 -1
- package/src/lib/api-usage/api-scanner.d.ts +2 -0
- package/src/lib/api-usage/api-scanner.js +45 -4
- package/src/lib/api-usage/api-scanner.js.map +1 -1
- package/src/lib/graph-loader.js +1 -0
- package/src/lib/graph-loader.js.map +1 -1
- package/src/lib/graph-metadata.d.ts +2 -0
- package/src/lib/graph-metadata.js +16 -0
- package/src/lib/graph-metadata.js.map +1 -1
- package/src/lib/graph-sorter.d.ts +6 -0
- package/src/lib/graph-sorter.js.map +1 -1
- package/src/lib/graph-visualizer.d.ts +15 -0
- package/src/lib/graph-visualizer.js +37 -5
- package/src/lib/graph-visualizer.js.map +1 -1
- package/src/lib/runtime-config.d.ts +4 -1
- package/src/lib/runtime-config.js +5 -1
- package/src/lib/runtime-config.js.map +1 -1
- package/src/lib/runtime-graph.d.ts +48 -0
- package/src/lib/runtime-graph.js +175 -26
- package/src/lib/runtime-graph.js.map +1 -1
- package/src/lib/runtime-visualizer.d.ts +29 -2
- package/src/lib/runtime-visualizer.js +121 -5
- package/src/lib/runtime-visualizer.js.map +1 -1
- package/src/lib/service-name-resolver.d.ts +49 -0
- package/src/lib/service-name-resolver.js +103 -0
- package/src/lib/service-name-resolver.js.map +1 -0
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
* "runtime-architecture": {
|
|
9
9
|
* "mode": "ON", // "OFF" disables the whole feature
|
|
10
10
|
* "ignoreModifiedUntilEpoch": 0, // whole-rule punt (epoch seconds)
|
|
11
|
-
* "allowedCycles": [ { "services": ["a","b"], "reason": "...", "until": 1771931925 } ]
|
|
11
|
+
* "allowedCycles": [ { "services": ["a","b"], "reason": "...", "until": 1771931925 } ],
|
|
12
|
+
* "showExternalNodes": true // draw firestore/gmail/... as terminal nodes
|
|
12
13
|
* }
|
|
13
14
|
*/
|
|
14
15
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
@@ -32,6 +33,9 @@ function loadRuntimeConfig(workspaceRoot) {
|
|
|
32
33
|
ignoreModifiedUntilEpoch: typeof raw.ignoreModifiedUntilEpoch === 'number' ? raw.ignoreModifiedUntilEpoch : undefined,
|
|
33
34
|
ignoreRuleWhileOnBranch: typeof raw.ignoreRuleWhileOnBranch === 'string' ? raw.ignoreRuleWhileOnBranch : undefined,
|
|
34
35
|
allowedCycles: Array.isArray(raw.allowedCycles) ? raw.allowedCycles.filter(isUsableCycle) : [],
|
|
36
|
+
// Opt-OUT: the external systems are the ones that page you at 3am, so they are drawn unless
|
|
37
|
+
// a repo explicitly says its external surface is too noisy to be useful.
|
|
38
|
+
showExternalNodes: raw.showExternalNodes !== false,
|
|
35
39
|
};
|
|
36
40
|
}
|
|
37
41
|
/**
|
|
@@ -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;;;;;;;;;;;;GAYG;;;AAsCH,8CAeC;AAQD,8CAEC;AAMD,sCAGC;AAGD,8BAEC;AA3ED,0DAA0F;AAE7E,QAAA,iBAAiB,GAAG,sBAAsB,CAAC;AA6BxD,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;QAC9F,4FAA4F;QAC5F,yEAAyE;QACzE,iBAAiB,EAAE,GAAG,CAAC,iBAAiB,KAAK,KAAK;KACrD,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 * \"showExternalNodes\": true // draw firestore/gmail/... as terminal nodes\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 /** Render the dashed external terminal nodes in the runtime viz (default true). */\n showExternalNodes: boolean;\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 showExternalNodes?: boolean;\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 // Opt-OUT: the external systems are the ones that page you at 3am, so they are drawn unless\n // a repo explicitly says its external surface is too noisy to be useful.\n showExternalNodes: raw.showExternalNodes !== false,\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"]}
|
|
@@ -11,13 +11,35 @@
|
|
|
11
11
|
* The runtime edge Z -> X (Z depends on X at runtime) is INFERRED: Z `uses` api
|
|
12
12
|
* Y and X `implements` api Y. This edge does not exist in the compile-time
|
|
13
13
|
* dependencies.json (both Z and X only compile-depend on the api library Y).
|
|
14
|
+
*
|
|
15
|
+
* WHICH X is decided by the call site, not by "everyone who implements Y":
|
|
16
|
+
* `createRpcClient(Y, new ClientConfig('helper-fsdb'))` names its target, kept as
|
|
17
|
+
* `ApiRef.targetService`, and matched against each node's DECLARED `serviceName`
|
|
18
|
+
* (project.json metadata.webpieces.serviceName). Fanning an edge out to every
|
|
19
|
+
* implementer instead is catastrophic for a company-wide contract registered in a
|
|
20
|
+
* shared library — it manufactures calls that cannot happen, and cycles that do
|
|
21
|
+
* not exist. When a target cannot be resolved the old fan-out still happens, but
|
|
22
|
+
* a warning names the call site (see RuntimeGraphReport.warnings): a wrong-but-
|
|
23
|
+
* green graph is worse than a failing one, so it must never degrade silently.
|
|
14
24
|
*/
|
|
15
25
|
import type { EnhancedGraph } from './graph-sorter';
|
|
16
26
|
import type { ApiTransport } from './api-usage/api-relations';
|
|
17
27
|
export declare const DEFAULT_RUNTIME_GRAPH_PATH = "architecture/runtime-dependencies.json";
|
|
18
28
|
export interface RuntimeService {
|
|
19
29
|
level: number;
|
|
30
|
+
/**
|
|
31
|
+
* The name clients address this service by (`new ClientConfig('helper-fsdb')`), declared in its
|
|
32
|
+
* project.json. Absent for a service nothing calls by name (e.g. a browser app).
|
|
33
|
+
*/
|
|
34
|
+
serviceName?: string;
|
|
20
35
|
implements: string[];
|
|
36
|
+
/**
|
|
37
|
+
* apiClassName -> the LIBRARY project whose apiRelations declared that implements, for the apis
|
|
38
|
+
* this service serves through an embedded library rather than its own source (e.g. a shared
|
|
39
|
+
* route-registration lib). Answers "who implements WarmupApi, and where did that come from?",
|
|
40
|
+
* which previously required walking the dependsOn closure by hand.
|
|
41
|
+
*/
|
|
42
|
+
implementsVia?: Record<string, string>;
|
|
21
43
|
uses: string[];
|
|
22
44
|
dependsOn: string[];
|
|
23
45
|
/**
|
|
@@ -34,6 +56,12 @@ export interface RuntimeApi {
|
|
|
34
56
|
usedBy: string[];
|
|
35
57
|
/** Transport of this API — 'rpc' (direct call) or 'pubsub' (delivered through a queue). */
|
|
36
58
|
type?: ApiTransport;
|
|
59
|
+
/**
|
|
60
|
+
* The api-lib project that OWNS this contract. For a contract nothing in-repo implements, this
|
|
61
|
+
* is the external library the calls leave the repo through (`lib-firestore`, `lib-gmail`), which
|
|
62
|
+
* is what the runtime viz labels its terminal external nodes with.
|
|
63
|
+
*/
|
|
64
|
+
owner?: string;
|
|
37
65
|
}
|
|
38
66
|
export interface RuntimeEdge {
|
|
39
67
|
from: string;
|
|
@@ -56,6 +84,19 @@ export interface RuntimeGraph {
|
|
|
56
84
|
runtimeEdges: RuntimeEdge[];
|
|
57
85
|
unresolvedUses: RuntimeUnresolved[];
|
|
58
86
|
}
|
|
87
|
+
/**
|
|
88
|
+
* The derived graph PLUS everything the derivation had to guess at. `warnings` is deliberately not
|
|
89
|
+
* part of RuntimeGraph: it is not committed data, it is the report that stops a guessed edge from
|
|
90
|
+
* passing for a derived one. Executors print it.
|
|
91
|
+
*/
|
|
92
|
+
export declare class RuntimeGraphReport {
|
|
93
|
+
readonly graph: RuntimeGraph;
|
|
94
|
+
/** Human-readable lines naming every call site whose target the graph could not pin down. */
|
|
95
|
+
readonly warnings: string[];
|
|
96
|
+
constructor(graph: RuntimeGraph,
|
|
97
|
+
/** Human-readable lines naming every call site whose target the graph could not pin down. */
|
|
98
|
+
warnings: string[]);
|
|
99
|
+
}
|
|
59
100
|
/** Adjacency (service -> [targets]) from a loaded runtime graph. */
|
|
60
101
|
export declare function runtimeAdjacency(graph: RuntimeGraph): Record<string, string[]>;
|
|
61
102
|
/**
|
|
@@ -65,6 +106,13 @@ export declare function runtimeAdjacency(graph: RuntimeGraph): Record<string, st
|
|
|
65
106
|
* runtime visualizer omits their nodes + edges.
|
|
66
107
|
*/
|
|
67
108
|
export declare function deriveRuntimeGraph(projects: EnhancedGraph, hiddenProjects?: Set<string>): RuntimeGraph;
|
|
109
|
+
/**
|
|
110
|
+
* The same derivation, plus the warnings it produced (every edge it had to GUESS at). Executors use
|
|
111
|
+
* this form and print the warnings; `deriveRuntimeGraph` is the convenience form for callers that
|
|
112
|
+
* only want the data. The warnings are deliberately kept OUT of runtime-dependencies.json — a graph
|
|
113
|
+
* file that records its own doubts would just get committed and stop being read.
|
|
114
|
+
*/
|
|
115
|
+
export declare function deriveRuntimeGraphReport(projects: EnhancedGraph, hiddenProjects?: Set<string>): RuntimeGraphReport;
|
|
68
116
|
export declare function saveRuntimeGraph(graph: RuntimeGraph, workspaceRoot: string, graphPath?: string): void;
|
|
69
117
|
export declare function runtimeGraphFileExists(workspaceRoot: string, graphPath?: string): boolean;
|
|
70
118
|
export declare function loadRuntimeGraph(workspaceRoot: string, graphPath?: string): RuntimeGraph | null;
|
package/src/lib/runtime-graph.js
CHANGED
|
@@ -12,11 +12,22 @@
|
|
|
12
12
|
* The runtime edge Z -> X (Z depends on X at runtime) is INFERRED: Z `uses` api
|
|
13
13
|
* Y and X `implements` api Y. This edge does not exist in the compile-time
|
|
14
14
|
* dependencies.json (both Z and X only compile-depend on the api library Y).
|
|
15
|
+
*
|
|
16
|
+
* WHICH X is decided by the call site, not by "everyone who implements Y":
|
|
17
|
+
* `createRpcClient(Y, new ClientConfig('helper-fsdb'))` names its target, kept as
|
|
18
|
+
* `ApiRef.targetService`, and matched against each node's DECLARED `serviceName`
|
|
19
|
+
* (project.json metadata.webpieces.serviceName). Fanning an edge out to every
|
|
20
|
+
* implementer instead is catastrophic for a company-wide contract registered in a
|
|
21
|
+
* shared library — it manufactures calls that cannot happen, and cycles that do
|
|
22
|
+
* not exist. When a target cannot be resolved the old fan-out still happens, but
|
|
23
|
+
* a warning names the call site (see RuntimeGraphReport.warnings): a wrong-but-
|
|
24
|
+
* green graph is worse than a failing one, so it must never degrade silently.
|
|
15
25
|
*/
|
|
16
26
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
-
exports.DEFAULT_RUNTIME_GRAPH_PATH = void 0;
|
|
27
|
+
exports.RuntimeGraphReport = exports.DEFAULT_RUNTIME_GRAPH_PATH = void 0;
|
|
18
28
|
exports.runtimeAdjacency = runtimeAdjacency;
|
|
19
29
|
exports.deriveRuntimeGraph = deriveRuntimeGraph;
|
|
30
|
+
exports.deriveRuntimeGraphReport = deriveRuntimeGraphReport;
|
|
20
31
|
exports.saveRuntimeGraph = saveRuntimeGraph;
|
|
21
32
|
exports.runtimeGraphFileExists = runtimeGraphFileExists;
|
|
22
33
|
exports.loadRuntimeGraph = loadRuntimeGraph;
|
|
@@ -28,6 +39,22 @@ const graph_sorter_1 = require("./graph-sorter");
|
|
|
28
39
|
const api_relations_1 = require("./api-usage/api-relations");
|
|
29
40
|
const toError_1 = require("../toError");
|
|
30
41
|
exports.DEFAULT_RUNTIME_GRAPH_PATH = 'architecture/runtime-dependencies.json';
|
|
42
|
+
/**
|
|
43
|
+
* The derived graph PLUS everything the derivation had to guess at. `warnings` is deliberately not
|
|
44
|
+
* part of RuntimeGraph: it is not committed data, it is the report that stops a guessed edge from
|
|
45
|
+
* passing for a derived one. Executors print it.
|
|
46
|
+
*/
|
|
47
|
+
class RuntimeGraphReport {
|
|
48
|
+
graph;
|
|
49
|
+
warnings;
|
|
50
|
+
constructor(graph,
|
|
51
|
+
/** Human-readable lines naming every call site whose target the graph could not pin down. */
|
|
52
|
+
warnings) {
|
|
53
|
+
this.graph = graph;
|
|
54
|
+
this.warnings = warnings;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
exports.RuntimeGraphReport = RuntimeGraphReport;
|
|
31
58
|
/** Adjacency (service -> [targets]) used for leveling + cycle checks. */
|
|
32
59
|
function adjacencyFromEdges(serviceNames, edges) {
|
|
33
60
|
const adj = {};
|
|
@@ -61,6 +88,32 @@ function assignLevels(adjacency) {
|
|
|
61
88
|
}
|
|
62
89
|
return levels;
|
|
63
90
|
}
|
|
91
|
+
/**
|
|
92
|
+
* Accumulates one node's effective relations while its dependsOn closure is walked, keeping the
|
|
93
|
+
* PROVENANCE the walk would otherwise throw away: which library contributed an implements, and
|
|
94
|
+
* which api-lib owns each contract.
|
|
95
|
+
*/
|
|
96
|
+
class RelationSink {
|
|
97
|
+
node;
|
|
98
|
+
implementsApis = [];
|
|
99
|
+
usesApis = [];
|
|
100
|
+
implementsVia = new Map();
|
|
101
|
+
constructor(
|
|
102
|
+
/** The runtime node these relations are attributed to. */
|
|
103
|
+
node) {
|
|
104
|
+
this.node = node;
|
|
105
|
+
}
|
|
106
|
+
/** `from` is the project whose apiRelations declared this — the node itself, or a lib it embeds. */
|
|
107
|
+
addImplements(ref, from) {
|
|
108
|
+
this.implementsApis.push(ref);
|
|
109
|
+
// First contributor wins, matching dedupApiRefs' keep-the-first rule on the ref list.
|
|
110
|
+
if (from !== this.node && !this.implementsVia.has(ref.api))
|
|
111
|
+
this.implementsVia.set(ref.api, from);
|
|
112
|
+
}
|
|
113
|
+
addUses(ref) {
|
|
114
|
+
this.usesApis.push(ref);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
64
117
|
/**
|
|
65
118
|
* Derives the runtime microservice graph from architecture/dependencies.json `apiRelations`:
|
|
66
119
|
* implementers × users per API, split by transport. An rpc edge is a direct call; a pubsub edge flows
|
|
@@ -73,11 +126,24 @@ function assignLevels(adjacency) {
|
|
|
73
126
|
class RuntimeGraphDeriver {
|
|
74
127
|
projects;
|
|
75
128
|
hiddenProjects;
|
|
129
|
+
/** apiClassName -> the api-lib project that owns the contract (from the apiRelations key). */
|
|
130
|
+
apiOwners = new Map();
|
|
131
|
+
/** Declared serviceName -> the runtime node declaring it; how a targeted call resolves. */
|
|
132
|
+
nodeByServiceName = new Map();
|
|
133
|
+
warnings = [];
|
|
76
134
|
constructor(projects,
|
|
77
135
|
/** Project names tagged drawOnGraph:false — kept in the JSON but flagged so the viz omits them. */
|
|
78
136
|
hiddenProjects) {
|
|
79
137
|
this.projects = projects;
|
|
80
138
|
this.hiddenProjects = hiddenProjects;
|
|
139
|
+
for (const name of Object.keys(projects).sort()) {
|
|
140
|
+
const declared = projects[name].serviceName;
|
|
141
|
+
// A duplicate is already a metadata-validation failure at generate time; first (sorted)
|
|
142
|
+
// wins here purely so derivation stays deterministic rather than order-dependent.
|
|
143
|
+
if (this.isNode(name) && declared !== undefined && !this.nodeByServiceName.has(declared)) {
|
|
144
|
+
this.nodeByServiceName.set(declared, name);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
81
147
|
}
|
|
82
148
|
assemble() {
|
|
83
149
|
const decls = this.collectDecls();
|
|
@@ -87,7 +153,13 @@ class RuntimeGraphDeriver {
|
|
|
87
153
|
const apisObj = {};
|
|
88
154
|
for (const api of Array.from(apis.keys()).sort())
|
|
89
155
|
apisObj[api] = apis.get(api);
|
|
90
|
-
|
|
156
|
+
const graph = {
|
|
157
|
+
services,
|
|
158
|
+
apis: apisObj,
|
|
159
|
+
runtimeEdges: edgeResult.edges,
|
|
160
|
+
unresolvedUses: edgeResult.unresolved,
|
|
161
|
+
};
|
|
162
|
+
return new RuntimeGraphReport(graph, this.warnings);
|
|
91
163
|
}
|
|
92
164
|
/**
|
|
93
165
|
* One ScanDecl per RUNTIME NODE (role:server / role:client). A node's effective
|
|
@@ -104,14 +176,14 @@ class RuntimeGraphDeriver {
|
|
|
104
176
|
for (const name of Object.keys(this.projects).sort()) {
|
|
105
177
|
if (!this.isNode(name))
|
|
106
178
|
continue;
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
if (implementsApis.length > 0 || usesApis.length > 0) {
|
|
179
|
+
const sink = new RelationSink(name);
|
|
180
|
+
this.collectEffectiveRelations(name, sink, new Set([name]));
|
|
181
|
+
if (sink.implementsApis.length > 0 || sink.usesApis.length > 0) {
|
|
111
182
|
decls.push({
|
|
112
183
|
name,
|
|
113
|
-
implementsApis: dedupApiRefs((0, api_relations_1.sortApiRefs)(implementsApis)),
|
|
114
|
-
usesApis: dedupApiRefs((0, api_relations_1.sortApiRefs)(usesApis)),
|
|
184
|
+
implementsApis: dedupApiRefs((0, api_relations_1.sortApiRefs)(sink.implementsApis)),
|
|
185
|
+
usesApis: dedupApiRefs((0, api_relations_1.sortApiRefs)(sink.usesApis)),
|
|
186
|
+
implementsVia: sink.implementsVia,
|
|
115
187
|
});
|
|
116
188
|
}
|
|
117
189
|
}
|
|
@@ -127,15 +199,21 @@ class RuntimeGraphDeriver {
|
|
|
127
199
|
* (skipping other nodes, which own their own relations). `visited` guards against
|
|
128
200
|
* re-walking a lib reachable by more than one path (and any cycle).
|
|
129
201
|
*/
|
|
130
|
-
collectEffectiveRelations(name,
|
|
202
|
+
collectEffectiveRelations(name, sink, visited) {
|
|
131
203
|
const entry = this.projects[name];
|
|
132
204
|
if (entry === undefined)
|
|
133
205
|
return;
|
|
134
206
|
const relations = entry.apiRelations;
|
|
135
207
|
if (relations !== undefined) {
|
|
136
|
-
for (const owner of Object.keys(relations)) {
|
|
137
|
-
|
|
138
|
-
|
|
208
|
+
for (const owner of Object.keys(relations).sort()) {
|
|
209
|
+
for (const ref of relations[owner].implements) {
|
|
210
|
+
this.apiOwners.set(ref.api, owner);
|
|
211
|
+
sink.addImplements(ref, name);
|
|
212
|
+
}
|
|
213
|
+
for (const ref of relations[owner].uses) {
|
|
214
|
+
this.apiOwners.set(ref.api, owner);
|
|
215
|
+
sink.addUses(ref);
|
|
216
|
+
}
|
|
139
217
|
}
|
|
140
218
|
}
|
|
141
219
|
for (const dep of entry.dependsOn) {
|
|
@@ -144,7 +222,7 @@ class RuntimeGraphDeriver {
|
|
|
144
222
|
visited.add(dep);
|
|
145
223
|
if (this.isNode(dep))
|
|
146
224
|
continue; // another server/client owns its own relations
|
|
147
|
-
this.collectEffectiveRelations(dep,
|
|
225
|
+
this.collectEffectiveRelations(dep, sink, visited);
|
|
148
226
|
}
|
|
149
227
|
}
|
|
150
228
|
/** apiClassName -> { implementedBy, usedBy, type }. */
|
|
@@ -164,9 +242,13 @@ class RuntimeGraphDeriver {
|
|
|
164
242
|
for (const ref of decl.usesApis)
|
|
165
243
|
ensure(ref.api, ref.type).usedBy.push(decl.name);
|
|
166
244
|
}
|
|
167
|
-
for (const
|
|
245
|
+
for (const api of apis.keys()) {
|
|
246
|
+
const entry = apis.get(api);
|
|
168
247
|
entry.implementedBy.sort();
|
|
169
|
-
entry.usedBy.sort();
|
|
248
|
+
entry.usedBy = Array.from(new Set(entry.usedBy)).sort();
|
|
249
|
+
const owner = this.apiOwners.get(api);
|
|
250
|
+
if (owner !== undefined)
|
|
251
|
+
entry.owner = owner;
|
|
170
252
|
}
|
|
171
253
|
return apis;
|
|
172
254
|
}
|
|
@@ -181,10 +263,10 @@ class RuntimeGraphDeriver {
|
|
|
181
263
|
unresolved.push({ service: decl.name, api: ref.api });
|
|
182
264
|
continue;
|
|
183
265
|
}
|
|
184
|
-
for (const target of implementers) {
|
|
266
|
+
for (const target of this.targetsFor(decl.name, ref, implementers)) {
|
|
185
267
|
if (target === decl.name)
|
|
186
268
|
continue;
|
|
187
|
-
const key = `${decl.name}
|
|
269
|
+
const key = `${decl.name} ${target} ${ref.type}`;
|
|
188
270
|
if (!viaByKey.has(key))
|
|
189
271
|
viaByKey.set(key, new Set());
|
|
190
272
|
viaByKey.get(key).add(ref.api);
|
|
@@ -193,10 +275,47 @@ class RuntimeGraphDeriver {
|
|
|
193
275
|
}
|
|
194
276
|
return { edges: this.edgesFromKeys(viaByKey), unresolved: sortUnresolved(unresolved) };
|
|
195
277
|
}
|
|
278
|
+
/**
|
|
279
|
+
* WHICH implementers this one `uses` reaches. A call site naming its target resolves to exactly
|
|
280
|
+
* ONE node; anything else falls back to the historical fan-out — the only safe superset — and
|
|
281
|
+
* records WHY, so a fanned-out (i.e. possibly fictional) edge can never pass for a derived one.
|
|
282
|
+
*/
|
|
283
|
+
targetsFor(user, ref, implementers) {
|
|
284
|
+
const wanted = ref.targetService;
|
|
285
|
+
if (wanted === undefined)
|
|
286
|
+
return this.untargetedFanOut(user, ref, implementers);
|
|
287
|
+
const node = this.nodeByServiceName.get(wanted);
|
|
288
|
+
if (node === undefined) {
|
|
289
|
+
this.warnings.push(`${user} uses "${ref.api}" against service '${wanted}', which NO project declares — ` +
|
|
290
|
+
`falling back to one edge per implementer (${implementers.join(', ')}). Declare it as ` +
|
|
291
|
+
`metadata.webpieces.serviceName in the serving project's project.json.`);
|
|
292
|
+
return implementers;
|
|
293
|
+
}
|
|
294
|
+
if (!implementers.includes(node)) {
|
|
295
|
+
this.warnings.push(`${user} uses "${ref.api}" against service '${wanted}' (${node}), but ${node} does NOT ` +
|
|
296
|
+
`implement it — implementers are ${implementers.join(', ')}. Falling back to one edge per ` +
|
|
297
|
+
`implementer; at runtime that call has nothing to answer it.`);
|
|
298
|
+
return implementers;
|
|
299
|
+
}
|
|
300
|
+
return [node];
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* A use with no literal client config. One implementer is unambiguous, so it stays silent; more
|
|
304
|
+
* than one means every edge but one is fiction — exactly the failure this mechanism exists to
|
|
305
|
+
* stop — so it is reported even though the graph still (conservatively) draws them all.
|
|
306
|
+
*/
|
|
307
|
+
untargetedFanOut(user, ref, implementers) {
|
|
308
|
+
if (implementers.length > 1) {
|
|
309
|
+
this.warnings.push(`${user} uses "${ref.api}" with no literal client config, and ${implementers.length} services ` +
|
|
310
|
+
`implement it (${implementers.join(', ')}) — an edge is drawn to EVERY one, so all but one ` +
|
|
311
|
+
`are fiction. Name the target: createRpcClient(${ref.api}, new ClientConfig('<serviceName>')).`);
|
|
312
|
+
}
|
|
313
|
+
return implementers;
|
|
314
|
+
}
|
|
196
315
|
edgesFromKeys(viaByKey) {
|
|
197
316
|
const edges = [];
|
|
198
317
|
for (const key of viaByKey.keys()) {
|
|
199
|
-
const parts = key.split('
|
|
318
|
+
const parts = key.split(' ');
|
|
200
319
|
edges.push({ from: parts[0], to: parts[1], via: Array.from(viaByKey.get(key)).sort(), type: parts[2] });
|
|
201
320
|
}
|
|
202
321
|
edges.sort((a, b) => a.from.localeCompare(b.from) || a.to.localeCompare(b.to) || (a.type ?? '').localeCompare(b.type ?? ''));
|
|
@@ -206,12 +325,18 @@ class RuntimeGraphDeriver {
|
|
|
206
325
|
const services = {};
|
|
207
326
|
for (const decl of decls) {
|
|
208
327
|
const dependsOn = Array.from(new Set(edges.filter((e) => e.from === decl.name).map((e) => e.to))).sort();
|
|
209
|
-
|
|
328
|
+
// Keys are written in this order; an undefined value is omitted by JSON.stringify, so
|
|
329
|
+
// the committed JSON stays clean AND deterministic without conditional assembly.
|
|
330
|
+
const service = {
|
|
210
331
|
level: 0,
|
|
332
|
+
serviceName: this.projects[decl.name]?.serviceName,
|
|
211
333
|
implements: decl.implementsApis.map((r) => r.api),
|
|
212
|
-
|
|
334
|
+
implementsVia: decl.implementsVia.size > 0 ? sortedRecord(decl.implementsVia) : undefined,
|
|
335
|
+
// One api used against two services is two refs but ONE api in this list.
|
|
336
|
+
uses: Array.from(new Set(decl.usesApis.map((r) => r.api))),
|
|
213
337
|
dependsOn,
|
|
214
338
|
};
|
|
339
|
+
services[decl.name] = service;
|
|
215
340
|
if (this.hiddenProjects.has(decl.name))
|
|
216
341
|
services[decl.name].drawOnGraph = false;
|
|
217
342
|
}
|
|
@@ -229,26 +354,50 @@ class RuntimeGraphDeriver {
|
|
|
229
354
|
*/
|
|
230
355
|
// webpieces-disable no-function-outside-class -- module entry point for the runtime graph derivation
|
|
231
356
|
function deriveRuntimeGraph(projects, hiddenProjects = new Set()) {
|
|
357
|
+
return deriveRuntimeGraphReport(projects, hiddenProjects).graph;
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* The same derivation, plus the warnings it produced (every edge it had to GUESS at). Executors use
|
|
361
|
+
* this form and print the warnings; `deriveRuntimeGraph` is the convenience form for callers that
|
|
362
|
+
* only want the data. The warnings are deliberately kept OUT of runtime-dependencies.json — a graph
|
|
363
|
+
* file that records its own doubts would just get committed and stop being read.
|
|
364
|
+
*/
|
|
365
|
+
// webpieces-disable no-function-outside-class -- module entry point for the runtime graph derivation
|
|
366
|
+
function deriveRuntimeGraphReport(projects, hiddenProjects = new Set()) {
|
|
232
367
|
return new RuntimeGraphDeriver(projects, hiddenProjects).assemble();
|
|
233
368
|
}
|
|
234
|
-
/** Drop duplicate api refs
|
|
235
|
-
*
|
|
236
|
-
*
|
|
369
|
+
/** Drop duplicate api refs, keeping the first — needed after a node absorbs the same api from both
|
|
370
|
+
* its own relations and an embedded lib's. Keyed by api AND target service: the same contract aimed
|
|
371
|
+
* at two different services is two distinct relations (two distinct edges), not a duplicate. Input
|
|
372
|
+
* is pre-sorted, so output stays deterministic. */
|
|
237
373
|
// webpieces-disable no-function-outside-class -- pure list helper, matches the sibling helpers in this file
|
|
238
374
|
function dedupApiRefs(refs) {
|
|
239
375
|
const seen = new Set();
|
|
240
376
|
const out = [];
|
|
241
377
|
for (const ref of refs) {
|
|
242
|
-
|
|
378
|
+
const key = (0, api_relations_1.apiRefKey)(ref);
|
|
379
|
+
if (seen.has(key))
|
|
243
380
|
continue;
|
|
244
|
-
seen.add(
|
|
381
|
+
seen.add(key);
|
|
245
382
|
out.push(ref);
|
|
246
383
|
}
|
|
247
384
|
return out;
|
|
248
385
|
}
|
|
386
|
+
/** Sort a Map into a plain object with sorted keys, so the committed JSON is deterministic. */
|
|
387
|
+
// webpieces-disable no-function-outside-class -- pure data helper, matches the sibling helpers in this file
|
|
388
|
+
function sortedRecord(map) {
|
|
389
|
+
const out = {};
|
|
390
|
+
for (const key of [...map.keys()].sort())
|
|
391
|
+
out[key] = map.get(key);
|
|
392
|
+
return out;
|
|
393
|
+
}
|
|
394
|
+
/** Sort AND de-duplicate: one api used against two targets must not be reported unresolved twice. */
|
|
249
395
|
// webpieces-disable no-function-outside-class -- pure sort helper, matches the sibling helpers in this file
|
|
250
396
|
function sortUnresolved(unresolved) {
|
|
251
|
-
|
|
397
|
+
const byKey = new Map();
|
|
398
|
+
for (const entry of unresolved)
|
|
399
|
+
byKey.set(`${entry.service} ${entry.api}`, entry);
|
|
400
|
+
return [...byKey.values()].sort((a, b) => a.service.localeCompare(b.service) || a.api.localeCompare(b.api));
|
|
252
401
|
}
|
|
253
402
|
/** Deterministic JSON (sorted keys + arrays already sorted during assembly). */
|
|
254
403
|
function formatRuntimeJson(graph) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runtime-graph.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/runtime-graph.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Runtime Graph\n *\n * Derives the runtime microservice graph SOLELY from architecture/dependencies.json\n * (the single source of truth): each project's `apiRelations` carry which api\n * classes it implements/uses and their transport (rpc | pubsub). Both\n * `architecture:generate` and `architecture:validate-runtime-architecture` call\n * the SAME `deriveRuntimeGraph`, so the committed graph and the validated graph\n * can never diverge.\n *\n * The runtime edge Z -> X (Z depends on X at runtime) is INFERRED: Z `uses` api\n * Y and X `implements` api Y. This edge does not exist in the compile-time\n * dependencies.json (both Z and X only compile-depend on the api library Y).\n */\n\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport { sortGraphTopologically } from './graph-sorter';\nimport type { EnhancedGraph } from './graph-sorter';\nimport type { ApiRef, ApiTransport } from './api-usage/api-relations';\nimport { sortApiRefs } from './api-usage/api-relations';\nimport { toError } from '../toError';\n\nexport const DEFAULT_RUNTIME_GRAPH_PATH = 'architecture/runtime-dependencies.json';\n\nexport interface RuntimeService {\n level: number;\n implements: string[];\n uses: string[];\n dependsOn: string[];\n /**\n * When false, this service is hidden from the rendered runtime graph (its\n * node AND every edge touching it are omitted from the HTML/DOT). It stays\n * in runtime-dependencies.json so the data view is complete. Absent means\n * drawn (the default). Mirrors GraphEntry.drawOnGraph from the `drawOnGraph:`\n * nx tag.\n */\n drawOnGraph?: boolean;\n}\n\nexport interface RuntimeApi {\n implementedBy: string[];\n usedBy: string[];\n /** Transport of this API — 'rpc' (direct call) or 'pubsub' (delivered through a queue). */\n type?: ApiTransport;\n}\n\nexport interface RuntimeEdge {\n from: string;\n to: string;\n via: string[];\n /**\n * Transport of this edge. 'rpc' → a direct call arrow. 'pubsub' → the producer enqueues and the\n * consumer is delivered later, so the runtime viz draws it as producer → QUEUE → consumer.\n * Edges are split by transport, so every edge is a single kind.\n */\n type?: ApiTransport;\n}\n\nexport interface RuntimeUnresolved {\n service: string;\n api: string;\n}\n\nexport interface RuntimeGraph {\n services: Record<string, RuntimeService>;\n apis: Record<string, RuntimeApi>;\n runtimeEdges: RuntimeEdge[];\n unresolvedUses: RuntimeUnresolved[];\n}\n\ninterface EdgeResult {\n edges: RuntimeEdge[];\n unresolved: RuntimeUnresolved[];\n}\n\n/** Adjacency (service -> [targets]) used for leveling + cycle checks. */\nfunction adjacencyFromEdges(serviceNames: string[], edges: RuntimeEdge[]): Record<string, string[]> {\n const adj: Record<string, string[]> = {};\n for (const name of serviceNames) adj[name] = [];\n for (const edge of edges) {\n if (!adj[edge.from]) adj[edge.from] = [];\n adj[edge.from].push(edge.to);\n }\n return adj;\n}\n\n/** Adjacency (service -> [targets]) from a loaded runtime graph. */\nexport function runtimeAdjacency(graph: RuntimeGraph): Record<string, string[]> {\n return adjacencyFromEdges(Object.keys(graph.services), graph.runtimeEdges);\n}\n\n/** Assign levels via topological sort; falls back to level 0 when a cycle exists. */\nfunction assignLevels(adjacency: Record<string, string[]>): Record<string, number> {\n const levels: Record<string, number> = {};\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const sorted = sortGraphTopologically(adjacency);\n for (const name of Object.keys(sorted)) levels[name] = sorted[name].level;\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n for (const name of Object.keys(adjacency)) levels[name] = 0;\n }\n return levels;\n}\n\n/** One project's implements/uses at api-CLASS granularity, from dependencies.json apiRelations. */\ninterface ScanDecl {\n name: string;\n implementsApis: ApiRef[];\n usesApis: ApiRef[];\n}\n\n/**\n * Derives the runtime microservice graph from architecture/dependencies.json `apiRelations`:\n * implementers × users per API, split by transport. An rpc edge is a direct call; a pubsub edge flows\n * through a queue (drawn producer → queue → consumer by the visualizer).\n *\n * Nodes are the runnable apps only — role:server / role:client. A library is NEVER a node; its\n * implements/uses (a shared client-factory or route-registration lib) are attributed to the\n * servers/clients that embed it, walked transitively over dependsOn (see collectDecls).\n */\nclass RuntimeGraphDeriver {\n constructor(\n private readonly projects: EnhancedGraph,\n /** Project names tagged drawOnGraph:false — kept in the JSON but flagged so the viz omits them. */\n private readonly hiddenProjects: Set<string>\n ) {}\n\n assemble(): RuntimeGraph {\n const decls = this.collectDecls();\n const apis = this.buildApis(decls);\n const edgeResult = this.buildEdges(decls, apis);\n const services = this.buildServices(decls, edgeResult.edges);\n const apisObj: Record<string, RuntimeApi> = {};\n for (const api of Array.from(apis.keys()).sort()) apisObj[api] = apis.get(api)!;\n return { services, apis: apisObj, runtimeEdges: edgeResult.edges, unresolvedUses: edgeResult.unresolved };\n }\n\n /**\n * One ScanDecl per RUNTIME NODE (role:server / role:client). A node's effective\n * relations are its OWN apiRelations PLUS those of every library in its transitive\n * dependsOn closure: a lib that calls createRpcClient/addRoutes runs inside the\n * server/client that embeds it, so the relation is attributed to that node — the\n * lib is never a runtime node. Recursion stops at other nodes (each server/client\n * owns its own relations). dependsOn is transitively reduced, but reduction\n * preserves reachability, so the recursive walk still reaches every lib a node\n * embeds through libraries.\n */\n private collectDecls(): ScanDecl[] {\n const decls: ScanDecl[] = [];\n for (const name of Object.keys(this.projects).sort()) {\n if (!this.isNode(name)) continue;\n const implementsApis: ApiRef[] = [];\n const usesApis: ApiRef[] = [];\n this.collectEffectiveRelations(name, implementsApis, usesApis, new Set<string>([name]));\n if (implementsApis.length > 0 || usesApis.length > 0) {\n decls.push({\n name,\n implementsApis: dedupApiRefs(sortApiRefs(implementsApis)),\n usesApis: dedupApiRefs(sortApiRefs(usesApis)),\n });\n }\n }\n return decls;\n }\n\n /** True when a project is a runtime node — a server or client app (never a library). */\n private isNode(name: string): boolean {\n const role = this.projects[name]?.role;\n return role === 'server' || role === 'client';\n }\n\n /**\n * Accumulate `name`'s own api relations, then recurse through its LIBRARY deps\n * (skipping other nodes, which own their own relations). `visited` guards against\n * re-walking a lib reachable by more than one path (and any cycle).\n */\n private collectEffectiveRelations(name: string, impl: ApiRef[], uses: ApiRef[], visited: Set<string>): void {\n const entry = this.projects[name];\n if (entry === undefined) return;\n const relations = entry.apiRelations;\n if (relations !== undefined) {\n for (const owner of Object.keys(relations)) {\n impl.push(...relations[owner].implements);\n uses.push(...relations[owner].uses);\n }\n }\n for (const dep of entry.dependsOn) {\n if (visited.has(dep)) continue;\n visited.add(dep);\n if (this.isNode(dep)) continue; // another server/client owns its own relations\n this.collectEffectiveRelations(dep, impl, uses, visited);\n }\n }\n\n /** apiClassName -> { implementedBy, usedBy, type }. */\n private buildApis(decls: ScanDecl[]): Map<string, RuntimeApi> {\n const apis = new Map<string, RuntimeApi>();\n const ensure = (api: string, type: ApiTransport): RuntimeApi => {\n let entry = apis.get(api);\n if (!entry) {\n entry = { implementedBy: [], usedBy: [], type };\n apis.set(api, entry);\n }\n return entry;\n };\n for (const decl of decls) {\n for (const ref of decl.implementsApis) ensure(ref.api, ref.type).implementedBy.push(decl.name);\n for (const ref of decl.usesApis) ensure(ref.api, ref.type).usedBy.push(decl.name);\n }\n for (const entry of apis.values()) {\n entry.implementedBy.sort();\n entry.usedBy.sort();\n }\n return apis;\n }\n\n /** Inferred edges U -> I via api A, split by transport; plus uses with no implementer. */\n private buildEdges(decls: ScanDecl[], apis: Map<string, RuntimeApi>): EdgeResult {\n const viaByKey = new Map<string, Set<string>>();\n const unresolved: RuntimeUnresolved[] = [];\n for (const decl of decls) {\n for (const ref of decl.usesApis) {\n const implementers = apis.get(ref.api)?.implementedBy ?? [];\n if (implementers.length === 0) {\n unresolved.push({ service: decl.name, api: ref.api });\n continue;\n }\n for (const target of implementers) {\n if (target === decl.name) continue;\n const key = `${decl.name}\u0000${target}\u0000${ref.type}`;\n if (!viaByKey.has(key)) viaByKey.set(key, new Set());\n viaByKey.get(key)!.add(ref.api);\n }\n }\n }\n return { edges: this.edgesFromKeys(viaByKey), unresolved: sortUnresolved(unresolved) };\n }\n\n private edgesFromKeys(viaByKey: Map<string, Set<string>>): RuntimeEdge[] {\n const edges: RuntimeEdge[] = [];\n for (const key of viaByKey.keys()) {\n const parts = key.split('\u0000');\n edges.push({ from: parts[0], to: parts[1], via: Array.from(viaByKey.get(key)!).sort(), type: parts[2] as ApiTransport });\n }\n edges.sort(\n (a: RuntimeEdge, b: RuntimeEdge) =>\n a.from.localeCompare(b.from) || a.to.localeCompare(b.to) || (a.type ?? '').localeCompare(b.type ?? ''),\n );\n return edges;\n }\n\n private buildServices(decls: ScanDecl[], edges: RuntimeEdge[]): Record<string, RuntimeService> {\n const services: Record<string, RuntimeService> = {};\n for (const decl of decls) {\n const dependsOn = Array.from(\n new Set(edges.filter((e: RuntimeEdge) => e.from === decl.name).map((e: RuntimeEdge) => e.to)),\n ).sort();\n services[decl.name] = {\n level: 0,\n implements: decl.implementsApis.map((r: ApiRef) => r.api),\n uses: decl.usesApis.map((r: ApiRef) => r.api),\n dependsOn,\n };\n if (this.hiddenProjects.has(decl.name)) services[decl.name].drawOnGraph = false;\n }\n const levels = assignLevels(adjacencyFromEdges(Object.keys(services), edges));\n for (const name of Object.keys(services)) services[name].level = levels[name] ?? 0;\n return services;\n }\n}\n\n/**\n * Derive the runtime graph from dependencies.json's project `apiRelations` — the\n * single source of truth shared by generate + validate. `hiddenProjects`\n * (drawOnGraph:false, defaults to none) are kept in the graph but flagged so the\n * runtime visualizer omits their nodes + edges.\n */\n// webpieces-disable no-function-outside-class -- module entry point for the runtime graph derivation\nexport function deriveRuntimeGraph(\n projects: EnhancedGraph,\n hiddenProjects: Set<string> = new Set<string>()\n): RuntimeGraph {\n return new RuntimeGraphDeriver(projects, hiddenProjects).assemble();\n}\n\n/** Drop duplicate api refs (same api class), keeping the first — needed after a node absorbs the\n * same api from both its own relations and an embedded lib's. Input is pre-sorted, so output stays\n * deterministic. */\n// webpieces-disable no-function-outside-class -- pure list helper, matches the sibling helpers in this file\nfunction dedupApiRefs(refs: ApiRef[]): ApiRef[] {\n const seen = new Set<string>();\n const out: ApiRef[] = [];\n for (const ref of refs) {\n if (seen.has(ref.api)) continue;\n seen.add(ref.api);\n out.push(ref);\n }\n return out;\n}\n\n// webpieces-disable no-function-outside-class -- pure sort helper, matches the sibling helpers in this file\nfunction sortUnresolved(unresolved: RuntimeUnresolved[]): RuntimeUnresolved[] {\n return [...unresolved].sort(\n (a: RuntimeUnresolved, b: RuntimeUnresolved) => a.service.localeCompare(b.service) || a.api.localeCompare(b.api),\n );\n}\n\n/** Deterministic JSON (sorted keys + arrays already sorted during assembly). */\nfunction formatRuntimeJson(graph: RuntimeGraph): string {\n return JSON.stringify(graph, null, 4) + '\\n';\n}\n\nexport function saveRuntimeGraph(\n graph: RuntimeGraph,\n workspaceRoot: string,\n graphPath: string = DEFAULT_RUNTIME_GRAPH_PATH,\n): void {\n const fullPath = path.join(workspaceRoot, graphPath);\n const dir = path.dirname(fullPath);\n if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });\n fs.writeFileSync(fullPath, formatRuntimeJson(graph), 'utf-8');\n}\n\nexport function runtimeGraphFileExists(\n workspaceRoot: string,\n graphPath: string = DEFAULT_RUNTIME_GRAPH_PATH,\n): boolean {\n return fs.existsSync(path.join(workspaceRoot, graphPath));\n}\n\nexport function loadRuntimeGraph(\n workspaceRoot: string,\n graphPath: string = DEFAULT_RUNTIME_GRAPH_PATH,\n): RuntimeGraph | null {\n const fullPath = path.join(workspaceRoot, graphPath);\n if (!fs.existsSync(fullPath)) return null;\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n return JSON.parse(fs.readFileSync(fullPath, 'utf-8')) as RuntimeGraph;\n } catch (err: unknown) {\n const error = toError(err);\n throw new Error(`Failed to load runtime graph from ${fullPath}`, { cause: error });\n }\n}\n\n/** Serialize for an in-memory equality check (matches the on-disk format). */\nexport function serializeRuntimeGraph(graph: RuntimeGraph): string {\n return formatRuntimeJson(graph);\n}\n"]}
|
|
1
|
+
{"version":3,"file":"runtime-graph.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/runtime-graph.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Runtime Graph\n *\n * Derives the runtime microservice graph SOLELY from architecture/dependencies.json\n * (the single source of truth): each project's `apiRelations` carry which api\n * classes it implements/uses and their transport (rpc | pubsub). Both\n * `architecture:generate` and `architecture:validate-runtime-architecture` call\n * the SAME `deriveRuntimeGraph`, so the committed graph and the validated graph\n * can never diverge.\n *\n * The runtime edge Z -> X (Z depends on X at runtime) is INFERRED: Z `uses` api\n * Y and X `implements` api Y. This edge does not exist in the compile-time\n * dependencies.json (both Z and X only compile-depend on the api library Y).\n *\n * WHICH X is decided by the call site, not by \"everyone who implements Y\":\n * `createRpcClient(Y, new ClientConfig('helper-fsdb'))` names its target, kept as\n * `ApiRef.targetService`, and matched against each node's DECLARED `serviceName`\n * (project.json metadata.webpieces.serviceName). Fanning an edge out to every\n * implementer instead is catastrophic for a company-wide contract registered in a\n * shared library — it manufactures calls that cannot happen, and cycles that do\n * not exist. When a target cannot be resolved the old fan-out still happens, but\n * a warning names the call site (see RuntimeGraphReport.warnings): a wrong-but-\n * green graph is worse than a failing one, so it must never degrade silently.\n */\n\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport { sortGraphTopologically } from './graph-sorter';\nimport type { EnhancedGraph } from './graph-sorter';\nimport type { ApiRef, ApiTransport } from './api-usage/api-relations';\nimport { apiRefKey, sortApiRefs } from './api-usage/api-relations';\nimport { toError } from '../toError';\n\nexport const DEFAULT_RUNTIME_GRAPH_PATH = 'architecture/runtime-dependencies.json';\n\nexport interface RuntimeService {\n level: number;\n /**\n * The name clients address this service by (`new ClientConfig('helper-fsdb')`), declared in its\n * project.json. Absent for a service nothing calls by name (e.g. a browser app).\n */\n serviceName?: string;\n implements: string[];\n /**\n * apiClassName -> the LIBRARY project whose apiRelations declared that implements, for the apis\n * this service serves through an embedded library rather than its own source (e.g. a shared\n * route-registration lib). Answers \"who implements WarmupApi, and where did that come from?\",\n * which previously required walking the dependsOn closure by hand.\n */\n implementsVia?: Record<string, string>;\n uses: string[];\n dependsOn: string[];\n /**\n * When false, this service is hidden from the rendered runtime graph (its\n * node AND every edge touching it are omitted from the HTML/DOT). It stays\n * in runtime-dependencies.json so the data view is complete. Absent means\n * drawn (the default). Mirrors GraphEntry.drawOnGraph from the `drawOnGraph:`\n * nx tag.\n */\n drawOnGraph?: boolean;\n}\n\nexport interface RuntimeApi {\n implementedBy: string[];\n usedBy: string[];\n /** Transport of this API — 'rpc' (direct call) or 'pubsub' (delivered through a queue). */\n type?: ApiTransport;\n /**\n * The api-lib project that OWNS this contract. For a contract nothing in-repo implements, this\n * is the external library the calls leave the repo through (`lib-firestore`, `lib-gmail`), which\n * is what the runtime viz labels its terminal external nodes with.\n */\n owner?: string;\n}\n\nexport interface RuntimeEdge {\n from: string;\n to: string;\n via: string[];\n /**\n * Transport of this edge. 'rpc' → a direct call arrow. 'pubsub' → the producer enqueues and the\n * consumer is delivered later, so the runtime viz draws it as producer → QUEUE → consumer.\n * Edges are split by transport, so every edge is a single kind.\n */\n type?: ApiTransport;\n}\n\nexport interface RuntimeUnresolved {\n service: string;\n api: string;\n}\n\nexport interface RuntimeGraph {\n services: Record<string, RuntimeService>;\n apis: Record<string, RuntimeApi>;\n runtimeEdges: RuntimeEdge[];\n unresolvedUses: RuntimeUnresolved[];\n}\n\ninterface EdgeResult {\n edges: RuntimeEdge[];\n unresolved: RuntimeUnresolved[];\n}\n\n/**\n * The derived graph PLUS everything the derivation had to guess at. `warnings` is deliberately not\n * part of RuntimeGraph: it is not committed data, it is the report that stops a guessed edge from\n * passing for a derived one. Executors print it.\n */\nexport class RuntimeGraphReport {\n constructor(\n public readonly graph: RuntimeGraph,\n /** Human-readable lines naming every call site whose target the graph could not pin down. */\n public readonly warnings: string[],\n ) {}\n}\n\n/** Adjacency (service -> [targets]) used for leveling + cycle checks. */\nfunction adjacencyFromEdges(serviceNames: string[], edges: RuntimeEdge[]): Record<string, string[]> {\n const adj: Record<string, string[]> = {};\n for (const name of serviceNames) adj[name] = [];\n for (const edge of edges) {\n if (!adj[edge.from]) adj[edge.from] = [];\n adj[edge.from].push(edge.to);\n }\n return adj;\n}\n\n/** Adjacency (service -> [targets]) from a loaded runtime graph. */\nexport function runtimeAdjacency(graph: RuntimeGraph): Record<string, string[]> {\n return adjacencyFromEdges(Object.keys(graph.services), graph.runtimeEdges);\n}\n\n/** Assign levels via topological sort; falls back to level 0 when a cycle exists. */\nfunction assignLevels(adjacency: Record<string, string[]>): Record<string, number> {\n const levels: Record<string, number> = {};\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const sorted = sortGraphTopologically(adjacency);\n for (const name of Object.keys(sorted)) levels[name] = sorted[name].level;\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n for (const name of Object.keys(adjacency)) levels[name] = 0;\n }\n return levels;\n}\n\n/** One project's implements/uses at api-CLASS granularity, from dependencies.json apiRelations. */\ninterface ScanDecl {\n name: string;\n implementsApis: ApiRef[];\n usesApis: ApiRef[];\n /** apiClassName -> the embedded LIBRARY project that declared the implements (never the node itself). */\n implementsVia: Map<string, string>;\n}\n\n/**\n * Accumulates one node's effective relations while its dependsOn closure is walked, keeping the\n * PROVENANCE the walk would otherwise throw away: which library contributed an implements, and\n * which api-lib owns each contract.\n */\nclass RelationSink {\n readonly implementsApis: ApiRef[] = [];\n readonly usesApis: ApiRef[] = [];\n readonly implementsVia = new Map<string, string>();\n\n constructor(\n /** The runtime node these relations are attributed to. */\n private readonly node: string,\n ) {}\n\n /** `from` is the project whose apiRelations declared this — the node itself, or a lib it embeds. */\n addImplements(ref: ApiRef, from: string): void {\n this.implementsApis.push(ref);\n // First contributor wins, matching dedupApiRefs' keep-the-first rule on the ref list.\n if (from !== this.node && !this.implementsVia.has(ref.api)) this.implementsVia.set(ref.api, from);\n }\n\n addUses(ref: ApiRef): void {\n this.usesApis.push(ref);\n }\n}\n\n/**\n * Derives the runtime microservice graph from architecture/dependencies.json `apiRelations`:\n * implementers × users per API, split by transport. An rpc edge is a direct call; a pubsub edge flows\n * through a queue (drawn producer → queue → consumer by the visualizer).\n *\n * Nodes are the runnable apps only — role:server / role:client. A library is NEVER a node; its\n * implements/uses (a shared client-factory or route-registration lib) are attributed to the\n * servers/clients that embed it, walked transitively over dependsOn (see collectDecls).\n */\nclass RuntimeGraphDeriver {\n /** apiClassName -> the api-lib project that owns the contract (from the apiRelations key). */\n private readonly apiOwners = new Map<string, string>();\n /** Declared serviceName -> the runtime node declaring it; how a targeted call resolves. */\n private readonly nodeByServiceName = new Map<string, string>();\n private readonly warnings: string[] = [];\n\n constructor(\n private readonly projects: EnhancedGraph,\n /** Project names tagged drawOnGraph:false — kept in the JSON but flagged so the viz omits them. */\n private readonly hiddenProjects: Set<string>\n ) {\n for (const name of Object.keys(projects).sort()) {\n const declared = projects[name].serviceName;\n // A duplicate is already a metadata-validation failure at generate time; first (sorted)\n // wins here purely so derivation stays deterministic rather than order-dependent.\n if (this.isNode(name) && declared !== undefined && !this.nodeByServiceName.has(declared)) {\n this.nodeByServiceName.set(declared, name);\n }\n }\n }\n\n assemble(): RuntimeGraphReport {\n const decls = this.collectDecls();\n const apis = this.buildApis(decls);\n const edgeResult = this.buildEdges(decls, apis);\n const services = this.buildServices(decls, edgeResult.edges);\n const apisObj: Record<string, RuntimeApi> = {};\n for (const api of Array.from(apis.keys()).sort()) apisObj[api] = apis.get(api)!;\n const graph: RuntimeGraph = {\n services,\n apis: apisObj,\n runtimeEdges: edgeResult.edges,\n unresolvedUses: edgeResult.unresolved,\n };\n return new RuntimeGraphReport(graph, this.warnings);\n }\n\n /**\n * One ScanDecl per RUNTIME NODE (role:server / role:client). A node's effective\n * relations are its OWN apiRelations PLUS those of every library in its transitive\n * dependsOn closure: a lib that calls createRpcClient/addRoutes runs inside the\n * server/client that embeds it, so the relation is attributed to that node — the\n * lib is never a runtime node. Recursion stops at other nodes (each server/client\n * owns its own relations). dependsOn is transitively reduced, but reduction\n * preserves reachability, so the recursive walk still reaches every lib a node\n * embeds through libraries.\n */\n private collectDecls(): ScanDecl[] {\n const decls: ScanDecl[] = [];\n for (const name of Object.keys(this.projects).sort()) {\n if (!this.isNode(name)) continue;\n const sink = new RelationSink(name);\n this.collectEffectiveRelations(name, sink, new Set<string>([name]));\n if (sink.implementsApis.length > 0 || sink.usesApis.length > 0) {\n decls.push({\n name,\n implementsApis: dedupApiRefs(sortApiRefs(sink.implementsApis)),\n usesApis: dedupApiRefs(sortApiRefs(sink.usesApis)),\n implementsVia: sink.implementsVia,\n });\n }\n }\n return decls;\n }\n\n /** True when a project is a runtime node — a server or client app (never a library). */\n private isNode(name: string): boolean {\n const role = this.projects[name]?.role;\n return role === 'server' || role === 'client';\n }\n\n /**\n * Accumulate `name`'s own api relations, then recurse through its LIBRARY deps\n * (skipping other nodes, which own their own relations). `visited` guards against\n * re-walking a lib reachable by more than one path (and any cycle).\n */\n private collectEffectiveRelations(name: string, sink: RelationSink, visited: Set<string>): void {\n const entry = this.projects[name];\n if (entry === undefined) return;\n const relations = entry.apiRelations;\n if (relations !== undefined) {\n for (const owner of Object.keys(relations).sort()) {\n for (const ref of relations[owner].implements) {\n this.apiOwners.set(ref.api, owner);\n sink.addImplements(ref, name);\n }\n for (const ref of relations[owner].uses) {\n this.apiOwners.set(ref.api, owner);\n sink.addUses(ref);\n }\n }\n }\n for (const dep of entry.dependsOn) {\n if (visited.has(dep)) continue;\n visited.add(dep);\n if (this.isNode(dep)) continue; // another server/client owns its own relations\n this.collectEffectiveRelations(dep, sink, visited);\n }\n }\n\n /** apiClassName -> { implementedBy, usedBy, type }. */\n private buildApis(decls: ScanDecl[]): Map<string, RuntimeApi> {\n const apis = new Map<string, RuntimeApi>();\n const ensure = (api: string, type: ApiTransport): RuntimeApi => {\n let entry = apis.get(api);\n if (!entry) {\n entry = { implementedBy: [], usedBy: [], type };\n apis.set(api, entry);\n }\n return entry;\n };\n for (const decl of decls) {\n for (const ref of decl.implementsApis) ensure(ref.api, ref.type).implementedBy.push(decl.name);\n for (const ref of decl.usesApis) ensure(ref.api, ref.type).usedBy.push(decl.name);\n }\n for (const api of apis.keys()) {\n const entry = apis.get(api)!;\n entry.implementedBy.sort();\n entry.usedBy = Array.from(new Set(entry.usedBy)).sort();\n const owner = this.apiOwners.get(api);\n if (owner !== undefined) entry.owner = owner;\n }\n return apis;\n }\n\n /** Inferred edges U -> I via api A, split by transport; plus uses with no implementer. */\n private buildEdges(decls: ScanDecl[], apis: Map<string, RuntimeApi>): EdgeResult {\n const viaByKey = new Map<string, Set<string>>();\n const unresolved: RuntimeUnresolved[] = [];\n for (const decl of decls) {\n for (const ref of decl.usesApis) {\n const implementers = apis.get(ref.api)?.implementedBy ?? [];\n if (implementers.length === 0) {\n unresolved.push({ service: decl.name, api: ref.api });\n continue;\n }\n for (const target of this.targetsFor(decl.name, ref, implementers)) {\n if (target === decl.name) continue;\n const key = `${decl.name} ${target} ${ref.type}`;\n if (!viaByKey.has(key)) viaByKey.set(key, new Set());\n viaByKey.get(key)!.add(ref.api);\n }\n }\n }\n return { edges: this.edgesFromKeys(viaByKey), unresolved: sortUnresolved(unresolved) };\n }\n\n /**\n * WHICH implementers this one `uses` reaches. A call site naming its target resolves to exactly\n * ONE node; anything else falls back to the historical fan-out — the only safe superset — and\n * records WHY, so a fanned-out (i.e. possibly fictional) edge can never pass for a derived one.\n */\n private targetsFor(user: string, ref: ApiRef, implementers: string[]): string[] {\n const wanted = ref.targetService;\n if (wanted === undefined) return this.untargetedFanOut(user, ref, implementers);\n\n const node = this.nodeByServiceName.get(wanted);\n if (node === undefined) {\n this.warnings.push(\n `${user} uses \"${ref.api}\" against service '${wanted}', which NO project declares — ` +\n `falling back to one edge per implementer (${implementers.join(', ')}). Declare it as ` +\n `metadata.webpieces.serviceName in the serving project's project.json.`,\n );\n return implementers;\n }\n if (!implementers.includes(node)) {\n this.warnings.push(\n `${user} uses \"${ref.api}\" against service '${wanted}' (${node}), but ${node} does NOT ` +\n `implement it — implementers are ${implementers.join(', ')}. Falling back to one edge per ` +\n `implementer; at runtime that call has nothing to answer it.`,\n );\n return implementers;\n }\n return [node];\n }\n\n /**\n * A use with no literal client config. One implementer is unambiguous, so it stays silent; more\n * than one means every edge but one is fiction — exactly the failure this mechanism exists to\n * stop — so it is reported even though the graph still (conservatively) draws them all.\n */\n private untargetedFanOut(user: string, ref: ApiRef, implementers: string[]): string[] {\n if (implementers.length > 1) {\n this.warnings.push(\n `${user} uses \"${ref.api}\" with no literal client config, and ${implementers.length} services ` +\n `implement it (${implementers.join(', ')}) — an edge is drawn to EVERY one, so all but one ` +\n `are fiction. Name the target: createRpcClient(${ref.api}, new ClientConfig('<serviceName>')).`,\n );\n }\n return implementers;\n }\n\n private edgesFromKeys(viaByKey: Map<string, Set<string>>): RuntimeEdge[] {\n const edges: RuntimeEdge[] = [];\n for (const key of viaByKey.keys()) {\n const parts = key.split(' ');\n edges.push({ from: parts[0], to: parts[1], via: Array.from(viaByKey.get(key)!).sort(), type: parts[2] as ApiTransport });\n }\n edges.sort(\n (a: RuntimeEdge, b: RuntimeEdge) =>\n a.from.localeCompare(b.from) || a.to.localeCompare(b.to) || (a.type ?? '').localeCompare(b.type ?? ''),\n );\n return edges;\n }\n\n private buildServices(decls: ScanDecl[], edges: RuntimeEdge[]): Record<string, RuntimeService> {\n const services: Record<string, RuntimeService> = {};\n for (const decl of decls) {\n const dependsOn = Array.from(\n new Set(edges.filter((e: RuntimeEdge) => e.from === decl.name).map((e: RuntimeEdge) => e.to)),\n ).sort();\n // Keys are written in this order; an undefined value is omitted by JSON.stringify, so\n // the committed JSON stays clean AND deterministic without conditional assembly.\n const service: RuntimeService = {\n level: 0,\n serviceName: this.projects[decl.name]?.serviceName,\n implements: decl.implementsApis.map((r: ApiRef) => r.api),\n implementsVia: decl.implementsVia.size > 0 ? sortedRecord(decl.implementsVia) : undefined,\n // One api used against two services is two refs but ONE api in this list.\n uses: Array.from(new Set(decl.usesApis.map((r: ApiRef) => r.api))),\n dependsOn,\n };\n services[decl.name] = service;\n if (this.hiddenProjects.has(decl.name)) services[decl.name].drawOnGraph = false;\n }\n const levels = assignLevels(adjacencyFromEdges(Object.keys(services), edges));\n for (const name of Object.keys(services)) services[name].level = levels[name] ?? 0;\n return services;\n }\n}\n\n/**\n * Derive the runtime graph from dependencies.json's project `apiRelations` — the\n * single source of truth shared by generate + validate. `hiddenProjects`\n * (drawOnGraph:false, defaults to none) are kept in the graph but flagged so the\n * runtime visualizer omits their nodes + edges.\n */\n// webpieces-disable no-function-outside-class -- module entry point for the runtime graph derivation\nexport function deriveRuntimeGraph(\n projects: EnhancedGraph,\n hiddenProjects: Set<string> = new Set<string>()\n): RuntimeGraph {\n return deriveRuntimeGraphReport(projects, hiddenProjects).graph;\n}\n\n/**\n * The same derivation, plus the warnings it produced (every edge it had to GUESS at). Executors use\n * this form and print the warnings; `deriveRuntimeGraph` is the convenience form for callers that\n * only want the data. The warnings are deliberately kept OUT of runtime-dependencies.json — a graph\n * file that records its own doubts would just get committed and stop being read.\n */\n// webpieces-disable no-function-outside-class -- module entry point for the runtime graph derivation\nexport function deriveRuntimeGraphReport(\n projects: EnhancedGraph,\n hiddenProjects: Set<string> = new Set<string>()\n): RuntimeGraphReport {\n return new RuntimeGraphDeriver(projects, hiddenProjects).assemble();\n}\n\n/** Drop duplicate api refs, keeping the first — needed after a node absorbs the same api from both\n * its own relations and an embedded lib's. Keyed by api AND target service: the same contract aimed\n * at two different services is two distinct relations (two distinct edges), not a duplicate. Input\n * is pre-sorted, so output stays deterministic. */\n// webpieces-disable no-function-outside-class -- pure list helper, matches the sibling helpers in this file\nfunction dedupApiRefs(refs: ApiRef[]): ApiRef[] {\n const seen = new Set<string>();\n const out: ApiRef[] = [];\n for (const ref of refs) {\n const key = apiRefKey(ref);\n if (seen.has(key)) continue;\n seen.add(key);\n out.push(ref);\n }\n return out;\n}\n\n/** Sort a Map into a plain object with sorted keys, so the committed JSON is deterministic. */\n// webpieces-disable no-function-outside-class -- pure data helper, matches the sibling helpers in this file\nfunction sortedRecord(map: Map<string, string>): Record<string, string> {\n const out: Record<string, string> = {};\n for (const key of [...map.keys()].sort()) out[key] = map.get(key)!;\n return out;\n}\n\n/** Sort AND de-duplicate: one api used against two targets must not be reported unresolved twice. */\n// webpieces-disable no-function-outside-class -- pure sort helper, matches the sibling helpers in this file\nfunction sortUnresolved(unresolved: RuntimeUnresolved[]): RuntimeUnresolved[] {\n const byKey = new Map<string, RuntimeUnresolved>();\n for (const entry of unresolved) byKey.set(`${entry.service} ${entry.api}`, entry);\n return [...byKey.values()].sort(\n (a: RuntimeUnresolved, b: RuntimeUnresolved) => a.service.localeCompare(b.service) || a.api.localeCompare(b.api),\n );\n}\n\n/** Deterministic JSON (sorted keys + arrays already sorted during assembly). */\nfunction formatRuntimeJson(graph: RuntimeGraph): string {\n return JSON.stringify(graph, null, 4) + '\\n';\n}\n\nexport function saveRuntimeGraph(\n graph: RuntimeGraph,\n workspaceRoot: string,\n graphPath: string = DEFAULT_RUNTIME_GRAPH_PATH,\n): void {\n const fullPath = path.join(workspaceRoot, graphPath);\n const dir = path.dirname(fullPath);\n if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });\n fs.writeFileSync(fullPath, formatRuntimeJson(graph), 'utf-8');\n}\n\nexport function runtimeGraphFileExists(\n workspaceRoot: string,\n graphPath: string = DEFAULT_RUNTIME_GRAPH_PATH,\n): boolean {\n return fs.existsSync(path.join(workspaceRoot, graphPath));\n}\n\nexport function loadRuntimeGraph(\n workspaceRoot: string,\n graphPath: string = DEFAULT_RUNTIME_GRAPH_PATH,\n): RuntimeGraph | null {\n const fullPath = path.join(workspaceRoot, graphPath);\n if (!fs.existsSync(fullPath)) return null;\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n return JSON.parse(fs.readFileSync(fullPath, 'utf-8')) as RuntimeGraph;\n } catch (err: unknown) {\n const error = toError(err);\n throw new Error(`Failed to load runtime graph from ${fullPath}`, { cause: error });\n }\n}\n\n/** Serialize for an in-memory equality check (matches the on-disk format). */\nexport function serializeRuntimeGraph(graph: RuntimeGraph): string {\n return formatRuntimeJson(graph);\n}\n"]}
|