@webpieces/nx-webpieces-rules 0.4.527 → 0.4.528
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/validate-runtime-architecture/executor.d.ts +10 -0
- package/src/executors/validate-runtime-architecture/executor.js +28 -1
- package/src/executors/validate-runtime-architecture/executor.js.map +1 -1
- package/src/lib/runtime-graph-model.d.ts +8 -0
- package/src/lib/runtime-graph-model.js.map +1 -1
- package/src/lib/runtime-graph.js +12 -8
- package/src/lib/runtime-graph.js.map +1 -1
- package/src/lib/runtime-visualizer.js +4 -1
- package/src/lib/runtime-visualizer.js.map +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webpieces/nx-webpieces-rules",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.528",
|
|
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",
|
|
@@ -22,11 +22,11 @@
|
|
|
22
22
|
"bin/**/*"
|
|
23
23
|
],
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@webpieces/ai-hook-rules": "0.4.
|
|
26
|
-
"@webpieces/code-rules": "0.4.
|
|
27
|
-
"@webpieces/eslint-rules": "0.4.
|
|
28
|
-
"@webpieces/pr-gate": "0.4.
|
|
29
|
-
"@webpieces/rules-config": "0.4.
|
|
25
|
+
"@webpieces/ai-hook-rules": "0.4.528",
|
|
26
|
+
"@webpieces/code-rules": "0.4.528",
|
|
27
|
+
"@webpieces/eslint-rules": "0.4.528",
|
|
28
|
+
"@webpieces/pr-gate": "0.4.528",
|
|
29
|
+
"@webpieces/rules-config": "0.4.528",
|
|
30
30
|
"madge": "8.0.0"
|
|
31
31
|
},
|
|
32
32
|
"peerDependencies": {
|
|
@@ -13,9 +13,19 @@
|
|
|
13
13
|
* Usage: nx run architecture:validate-runtime-architecture
|
|
14
14
|
*/
|
|
15
15
|
import type { ExecutorContext } from '@nx/devkit';
|
|
16
|
+
import type { EnhancedGraph } from '../../lib/graph-sorter';
|
|
17
|
+
import type { RuntimeGraph } from '../../lib/runtime-graph';
|
|
16
18
|
export interface ValidateRuntimeArchitectureOptions {
|
|
17
19
|
}
|
|
18
20
|
export interface ExecutorResult {
|
|
19
21
|
success: boolean;
|
|
20
22
|
}
|
|
23
|
+
/**
|
|
24
|
+
* Every `role:server` project must have a NODE in the runtime graph. A pure set difference against
|
|
25
|
+
* what the derivation produced, and it exists because the derivation once dropped relation-less
|
|
26
|
+
* servers silently: the diagram claimed to be complete while a deployed service was missing from it.
|
|
27
|
+
* Now the only sanctioned way for a server to be absent from the drawing is `drawOnGraph:false`, and
|
|
28
|
+
* anything else that ever removes one fails the build by name instead of disappearing quietly.
|
|
29
|
+
*/
|
|
30
|
+
export declare function checkServersPresent(projects: EnhancedGraph, hiddenProjects: Set<string>, graph: RuntimeGraph): string[];
|
|
21
31
|
export default function runExecutor(_options: ValidateRuntimeArchitectureOptions, context: ExecutorContext): Promise<ExecutorResult>;
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
* Usage: nx run architecture:validate-runtime-architecture
|
|
15
15
|
*/
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.checkServersPresent = checkServersPresent;
|
|
17
18
|
exports.default = runExecutor;
|
|
18
19
|
const graph_loader_1 = require("../../lib/graph-loader");
|
|
19
20
|
const runtime_graph_1 = require("../../lib/runtime-graph");
|
|
@@ -47,6 +48,28 @@ function checkCycles(graph, allowed) {
|
|
|
47
48
|
}
|
|
48
49
|
return problems;
|
|
49
50
|
}
|
|
51
|
+
/**
|
|
52
|
+
* Every `role:server` project must have a NODE in the runtime graph. A pure set difference against
|
|
53
|
+
* what the derivation produced, and it exists because the derivation once dropped relation-less
|
|
54
|
+
* servers silently: the diagram claimed to be complete while a deployed service was missing from it.
|
|
55
|
+
* Now the only sanctioned way for a server to be absent from the drawing is `drawOnGraph:false`, and
|
|
56
|
+
* anything else that ever removes one fails the build by name instead of disappearing quietly.
|
|
57
|
+
*/
|
|
58
|
+
// webpieces-disable no-function-outside-class -- executor step helper, matches checkCycles/checkUnchanged in this file
|
|
59
|
+
function checkServersPresent(projects, hiddenProjects, graph) {
|
|
60
|
+
const problems = [];
|
|
61
|
+
for (const name of Object.keys(projects).sort()) {
|
|
62
|
+
if (projects[name].role !== 'server')
|
|
63
|
+
continue;
|
|
64
|
+
if (hiddenProjects.has(name))
|
|
65
|
+
continue;
|
|
66
|
+
if (graph.services[name] !== undefined)
|
|
67
|
+
continue;
|
|
68
|
+
problems.push(`role:server '${name}' has no node in the runtime graph. Tag it drawOnGraph:false in its ` +
|
|
69
|
+
`project.json if that is intended.`);
|
|
70
|
+
}
|
|
71
|
+
return problems;
|
|
72
|
+
}
|
|
50
73
|
/** Returns an unchanged-check message, or null if the committed graph matches. */
|
|
51
74
|
function checkUnchanged(workspaceRoot, current) {
|
|
52
75
|
if (!(0, runtime_graph_1.runtimeGraphFileExists)(workspaceRoot)) {
|
|
@@ -87,7 +110,11 @@ async function runExecutor(_options, context) {
|
|
|
87
110
|
console.warn(`⚠️ ${warning}`);
|
|
88
111
|
// A call site naming a service no module answers to FAILS: the contract is served in-repo, so
|
|
89
112
|
// the name is a typo or a stale rename, and the graph only ever hid it by fanning the edge out.
|
|
90
|
-
const problems = [
|
|
113
|
+
const problems = [
|
|
114
|
+
...report.problems,
|
|
115
|
+
...checkCycles(graph, config.allowedCycles),
|
|
116
|
+
...checkServersPresent(depsFile.projects, hiddenProjects, graph),
|
|
117
|
+
];
|
|
91
118
|
const unchanged = checkUnchanged(workspaceRoot, graph);
|
|
92
119
|
if (unchanged)
|
|
93
120
|
problems.push(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":"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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 deriveRuntimeGraphReport,\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 // apiContracts comes from the SAME loaded file, so the queue/trigger data the derivation sees\n // here is byte-identical to what generate saw in memory.\n const report = deriveRuntimeGraphReport(\n depsFile.projects,\n hiddenProjects,\n depsFile.apiContracts,\n depsFile.externalSystems,\n );\n const graph = report.graph;\n for (const warning of report.warnings) console.warn(`⚠️ ${warning}`);\n\n // A call site naming a service no module answers to FAILS: the contract is served in-repo, so\n // the name is a typo or a stale rename, and the graph only ever hid it by fanning the edge out.\n const problems: string[] = [...report.problems, ...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"]}
|
|
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 type { EnhancedGraph } from '../../lib/graph-sorter';\nimport {\n deriveRuntimeGraphReport,\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/**\n * Every `role:server` project must have a NODE in the runtime graph. A pure set difference against\n * what the derivation produced, and it exists because the derivation once dropped relation-less\n * servers silently: the diagram claimed to be complete while a deployed service was missing from it.\n * Now the only sanctioned way for a server to be absent from the drawing is `drawOnGraph:false`, and\n * anything else that ever removes one fails the build by name instead of disappearing quietly.\n */\n// webpieces-disable no-function-outside-class -- executor step helper, matches checkCycles/checkUnchanged in this file\nexport function checkServersPresent(\n projects: EnhancedGraph,\n hiddenProjects: Set<string>,\n graph: RuntimeGraph,\n): string[] {\n const problems: string[] = [];\n for (const name of Object.keys(projects).sort()) {\n if (projects[name].role !== 'server') continue;\n if (hiddenProjects.has(name)) continue;\n if (graph.services[name] !== undefined) continue;\n problems.push(\n `role:server '${name}' has no node in the runtime graph. Tag it drawOnGraph:false in its ` +\n `project.json if that is intended.`,\n );\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 // apiContracts comes from the SAME loaded file, so the queue/trigger data the derivation sees\n // here is byte-identical to what generate saw in memory.\n const report = deriveRuntimeGraphReport(\n depsFile.projects,\n hiddenProjects,\n depsFile.apiContracts,\n depsFile.externalSystems,\n );\n const graph = report.graph;\n for (const warning of report.warnings) console.warn(`⚠️ ${warning}`);\n\n // A call site naming a service no module answers to FAILS: the contract is served in-repo, so\n // the name is a typo or a stale rename, and the graph only ever hid it by fanning the edge out.\n const problems: string[] = [\n ...report.problems,\n ...checkCycles(graph, config.allowedCycles),\n ...checkServersPresent(depsFile.projects, hiddenProjects, graph),\n ];\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"]}
|
|
@@ -12,6 +12,14 @@
|
|
|
12
12
|
import type { ApiTransport, ExternalSystemDeclaration, ExternalSystemKind } from './api-usage/api-relations';
|
|
13
13
|
export interface RuntimeService {
|
|
14
14
|
level: number;
|
|
15
|
+
/**
|
|
16
|
+
* The project's declared nx role — 'server' or 'client' (a library is never a node). Carried so
|
|
17
|
+
* the viz can LABEL the node with what it actually is: it used to infer the role from
|
|
18
|
+
* `implements.length > 0`, which labelled every relation-less server "client". Optional so a
|
|
19
|
+
* runtime-dependencies.json written before this field existed still parses and still renders
|
|
20
|
+
* exactly as it did (the visualizer falls back to the old inference).
|
|
21
|
+
*/
|
|
22
|
+
role?: string;
|
|
15
23
|
/**
|
|
16
24
|
* The name clients address this service by (`new ClientConfig('helper-fsdb')`), declared in its
|
|
17
25
|
* project.json. Absent for a service nothing calls by name (e.g. a browser app).
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runtime-graph-model.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/runtime-graph-model.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;GAUG","sourcesContent":["/**\n * Runtime Graph model\n *\n * The serialization DTOs for architecture/runtime-dependencies.json. Split out of runtime-graph.ts,\n * which owns the DERIVATION (and had grown past the file-size limit), so the committed data shape\n * can be read on its own — it is what every consumer of the file, and any future Terraform\n * cross-check, actually programs against.\n *\n * Interfaces rather than classes: these are parsed straight out of JSON with `JSON.parse`, so a\n * class would only ever be a shape assertion over a plain object, never a constructed instance.\n */\n\nimport type { ApiTransport, ExternalSystemDeclaration, ExternalSystemKind } from './api-usage/api-relations';\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 /**\n * The service(s) this node's clients call when the call site carries no literal `ClientConfig`,\n * declared in its project.json (metadata.webpieces.callsService). A single name, or an\n * `{ apiClassName: serviceName }` map. Absent when the node declares no target. Mirrors\n * GraphEntry.callsService; it is the CALLING-side counterpart of `serviceName`.\n */\n callsService?: string | Record<string, 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 * Set when the contract carries an `@externalSystem <kind> [label]` JSDoc tag — the vendor seam\n * declaring WHAT it is a seam to, so the viz can draw firestore as a database rather than as the\n * same grey box as every other external. Absent on everything else.\n */\n externalSystem?: ExternalSystemDeclaration;\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 * `\"ApiClassName.methodName\"` — the queue this edge flows through. Present iff `type` is\n * 'pubsub'. Queues are per METHOD, not per service pair, because that is the unit Cloud Tasks\n * (and Terraform) actually create, so two services exchanging three queued methods are three\n * queues rather than one arrow.\n */\n queue?: string;\n}\n\n/**\n * One Cloud Tasks queue: the async seam between a producer and a consumer, at METHOD granularity.\n *\n * `producedBy` and `consumedBy` are deliberately not symmetric in confidence — see\n * {@link ApiRef.methodsInferred}. The consumer is derived from `addRoutes` plus the contract's\n * method table and is exact; the producer is attributed to every queued method of the contract it\n * built a client for, because which methods it enqueues is not statically recoverable.\n */\nexport interface RuntimeQueue {\n api: string;\n method: string;\n /** `@Queue(...)` override, else `${Api}-${method}` — the name Terraform must match 1:1. */\n queueName: string;\n producedBy: string[];\n consumedBy: string[];\n}\n\n/**\n * An endpoint driven by something that is NOT an in-repo caller — a clock or an outside system.\n * These never appear as runtime EDGES (there is no in-repo `from`), which is exactly why they were\n * invisible until now: a nightly sweep and a GCP push subscription are real runtime entry points\n * with real Terraform behind them, and the graph showed neither.\n */\nexport interface RuntimeTrigger {\n /** 'cron' → a scheduler fires it; 'external' → a system outside this repo posts to it. */\n kind: 'cron' | 'external';\n api: string;\n method: string;\n /** The service that SERVES the endpoint (the arrow's head). */\n service: string;\n /** Present for 'cron': the Cloud Scheduler job / queue name Terraform must match. */\n queueName?: string;\n}\n\nexport interface RuntimeUnresolved {\n service: string;\n api: string;\n}\n\n/**\n * A system outside this repo that a service TALKS TO, drawn with a shape that says what it is\n * (a database as a cylinder, a bucket as a folder) instead of the one grey box every external\n * used to collapse into.\n *\n * Two declaration sites feed this, because the two real cases differ in whether a contract exists:\n *\n * - **Wrapped** — the repo has a vendor seam (`FirestoreAdminApi`), so the kind is declared with an\n * `@externalSystem <kind> [label]` JSDoc tag on the contract. A decorator cannot go on a TS\n * `interface`, and these seams are interfaces, so JSDoc is the only marker that fits in place.\n * - **Unwrapped** — the service opens the connection itself with no contract to mark (a `pg.Pool`,\n * a TypeORM `DataSource`), so the declaration is an `external:<kind>:<identity>` nx tag on that\n * project's project.json.\n *\n * `label` is the node IDENTITY, not just display text: two services declaring `postgres` converge on\n * ONE node with two arrows into it, rather than drawing a database each.\n */\nexport interface RuntimeExternalSystem {\n kind: ExternalSystemKind;\n /** Display name AND node identity — declarations sharing a label share a node. */\n label: string;\n /** Services with a direct arrow to it. No transitive fan-out: a tag speaks only for its project. */\n usedBy: string[];\n /** Contracts flowing over it. Empty for the unwrapped (tag-declared) case — there are none. */\n apis: string[];\n}\n\nexport interface RuntimeGraph {\n services: Record<string, RuntimeService>;\n apis: Record<string, RuntimeApi>;\n runtimeEdges: RuntimeEdge[];\n unresolvedUses: RuntimeUnresolved[];\n /** `\"Api.method\"` -> the queue between its producers and its consumers. */\n queues: Record<string, RuntimeQueue>;\n /** Clock- and outside-driven entry points, sorted for determinism. */\n triggers: RuntimeTrigger[];\n /**\n * Declared external systems, keyed by identity. Optional so every graph written before this\n * existed still parses — an absent map means \"nothing declared\", which renders exactly as before.\n */\n externalSystems?: Record<string, RuntimeExternalSystem>;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"runtime-graph-model.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/runtime-graph-model.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;GAUG","sourcesContent":["/**\n * Runtime Graph model\n *\n * The serialization DTOs for architecture/runtime-dependencies.json. Split out of runtime-graph.ts,\n * which owns the DERIVATION (and had grown past the file-size limit), so the committed data shape\n * can be read on its own — it is what every consumer of the file, and any future Terraform\n * cross-check, actually programs against.\n *\n * Interfaces rather than classes: these are parsed straight out of JSON with `JSON.parse`, so a\n * class would only ever be a shape assertion over a plain object, never a constructed instance.\n */\n\nimport type { ApiTransport, ExternalSystemDeclaration, ExternalSystemKind } from './api-usage/api-relations';\n\nexport interface RuntimeService {\n level: number;\n /**\n * The project's declared nx role — 'server' or 'client' (a library is never a node). Carried so\n * the viz can LABEL the node with what it actually is: it used to infer the role from\n * `implements.length > 0`, which labelled every relation-less server \"client\". Optional so a\n * runtime-dependencies.json written before this field existed still parses and still renders\n * exactly as it did (the visualizer falls back to the old inference).\n */\n role?: string;\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 /**\n * The service(s) this node's clients call when the call site carries no literal `ClientConfig`,\n * declared in its project.json (metadata.webpieces.callsService). A single name, or an\n * `{ apiClassName: serviceName }` map. Absent when the node declares no target. Mirrors\n * GraphEntry.callsService; it is the CALLING-side counterpart of `serviceName`.\n */\n callsService?: string | Record<string, 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 * Set when the contract carries an `@externalSystem <kind> [label]` JSDoc tag — the vendor seam\n * declaring WHAT it is a seam to, so the viz can draw firestore as a database rather than as the\n * same grey box as every other external. Absent on everything else.\n */\n externalSystem?: ExternalSystemDeclaration;\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 * `\"ApiClassName.methodName\"` — the queue this edge flows through. Present iff `type` is\n * 'pubsub'. Queues are per METHOD, not per service pair, because that is the unit Cloud Tasks\n * (and Terraform) actually create, so two services exchanging three queued methods are three\n * queues rather than one arrow.\n */\n queue?: string;\n}\n\n/**\n * One Cloud Tasks queue: the async seam between a producer and a consumer, at METHOD granularity.\n *\n * `producedBy` and `consumedBy` are deliberately not symmetric in confidence — see\n * {@link ApiRef.methodsInferred}. The consumer is derived from `addRoutes` plus the contract's\n * method table and is exact; the producer is attributed to every queued method of the contract it\n * built a client for, because which methods it enqueues is not statically recoverable.\n */\nexport interface RuntimeQueue {\n api: string;\n method: string;\n /** `@Queue(...)` override, else `${Api}-${method}` — the name Terraform must match 1:1. */\n queueName: string;\n producedBy: string[];\n consumedBy: string[];\n}\n\n/**\n * An endpoint driven by something that is NOT an in-repo caller — a clock or an outside system.\n * These never appear as runtime EDGES (there is no in-repo `from`), which is exactly why they were\n * invisible until now: a nightly sweep and a GCP push subscription are real runtime entry points\n * with real Terraform behind them, and the graph showed neither.\n */\nexport interface RuntimeTrigger {\n /** 'cron' → a scheduler fires it; 'external' → a system outside this repo posts to it. */\n kind: 'cron' | 'external';\n api: string;\n method: string;\n /** The service that SERVES the endpoint (the arrow's head). */\n service: string;\n /** Present for 'cron': the Cloud Scheduler job / queue name Terraform must match. */\n queueName?: string;\n}\n\nexport interface RuntimeUnresolved {\n service: string;\n api: string;\n}\n\n/**\n * A system outside this repo that a service TALKS TO, drawn with a shape that says what it is\n * (a database as a cylinder, a bucket as a folder) instead of the one grey box every external\n * used to collapse into.\n *\n * Two declaration sites feed this, because the two real cases differ in whether a contract exists:\n *\n * - **Wrapped** — the repo has a vendor seam (`FirestoreAdminApi`), so the kind is declared with an\n * `@externalSystem <kind> [label]` JSDoc tag on the contract. A decorator cannot go on a TS\n * `interface`, and these seams are interfaces, so JSDoc is the only marker that fits in place.\n * - **Unwrapped** — the service opens the connection itself with no contract to mark (a `pg.Pool`,\n * a TypeORM `DataSource`), so the declaration is an `external:<kind>:<identity>` nx tag on that\n * project's project.json.\n *\n * `label` is the node IDENTITY, not just display text: two services declaring `postgres` converge on\n * ONE node with two arrows into it, rather than drawing a database each.\n */\nexport interface RuntimeExternalSystem {\n kind: ExternalSystemKind;\n /** Display name AND node identity — declarations sharing a label share a node. */\n label: string;\n /** Services with a direct arrow to it. No transitive fan-out: a tag speaks only for its project. */\n usedBy: string[];\n /** Contracts flowing over it. Empty for the unwrapped (tag-declared) case — there are none. */\n apis: string[];\n}\n\nexport interface RuntimeGraph {\n services: Record<string, RuntimeService>;\n apis: Record<string, RuntimeApi>;\n runtimeEdges: RuntimeEdge[];\n unresolvedUses: RuntimeUnresolved[];\n /** `\"Api.method\"` -> the queue between its producers and its consumers. */\n queues: Record<string, RuntimeQueue>;\n /** Clock- and outside-driven entry points, sorted for determinism. */\n triggers: RuntimeTrigger[];\n /**\n * Declared external systems, keyed by identity. Optional so every graph written before this\n * existed still parses — an absent map means \"nothing declared\", which renders exactly as before.\n */\n externalSystems?: Record<string, RuntimeExternalSystem>;\n}\n"]}
|
package/src/lib/runtime-graph.js
CHANGED
|
@@ -272,14 +272,17 @@ class RuntimeGraphDeriver {
|
|
|
272
272
|
continue;
|
|
273
273
|
const sink = new RelationSink(name);
|
|
274
274
|
this.collectEffectiveRelations(name, sink, new Set([name]));
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
275
|
+
// EVERY node gets a decl, including one with zero relations. Gating this on "has at
|
|
276
|
+
// least one implements/uses" silently DELETED any deployed server whose work arrives
|
|
277
|
+
// from outside the repo (a pull subscription): the diagram looked complete while missing
|
|
278
|
+
// a running service — confidently wrong, not visibly incomplete. isNode() is the whole
|
|
279
|
+
// test; drawOnGraph:false is the ONE opt-out, and such a node renders as an isolated box.
|
|
280
|
+
decls.push({
|
|
281
|
+
name,
|
|
282
|
+
implementsApis: dedupApiRefs((0, api_relations_1.sortApiRefs)(sink.implementsApis)),
|
|
283
|
+
usesApis: dedupApiRefs((0, api_relations_1.sortApiRefs)(sink.usesApis)),
|
|
284
|
+
implementsVia: sink.implementsVia,
|
|
285
|
+
});
|
|
283
286
|
}
|
|
284
287
|
return decls;
|
|
285
288
|
}
|
|
@@ -540,6 +543,7 @@ class RuntimeGraphDeriver {
|
|
|
540
543
|
// the committed JSON stays clean AND deterministic without conditional assembly.
|
|
541
544
|
const service = {
|
|
542
545
|
level: 0,
|
|
546
|
+
role: this.projects[decl.name]?.role, // labels a relation-less node for what it IS
|
|
543
547
|
serviceName: this.projects[decl.name]?.serviceName,
|
|
544
548
|
callsService: this.projects[decl.name]?.callsService,
|
|
545
549
|
implements: decl.implementsApis.map((r) => r.api),
|
|
@@ -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 * WHICH X is decided by the call site, not by \"everyone who implements Y\", in\n * priority order: (1) a literal at the call site —\n * `createRpcClient(Y, new ClientConfig('helper-fsdb'))` — kept as\n * `ApiRef.targetService`; else (2) the calling project's declared `callsService`\n * (project.json metadata.webpieces.callsService), the symmetric half of\n * `serviceName` for the shared-library case where the client is built once from a\n * config field so no literal can sit at the call site; else (3) fan-out. A named\n * target from (1) or (2) is matched against each node's DECLARED `serviceName`.\n * Fanning an edge out to every implementer is catastrophic for a company-wide\n * contract registered in a shared library — it manufactures calls that cannot\n * happen, and cycles that do not exist. When a target cannot be resolved the old\n * fan-out still happens, but a warning names the call site (see\n * RuntimeGraphReport.warnings): a wrong-but-green graph is worse than a failing\n * one, so it must never degrade silently.\n */\n\nimport { sortGraphTopologically } from './graph-sorter';\nimport type { EnhancedGraph } from './graph-sorter';\nimport type {\n ApiContracts,\n ApiMethodMeta,\n ApiRef,\n ApiTransport,\n ExternalSystemDecls,\n} from './api-usage/api-relations';\nimport { apiRefKey, sortApiRefs } from './api-usage/api-relations';\nimport { attachExternalSystems, resolveExternalSystems } from './api-usage/external-systems';\nimport type {\n RuntimeApi,\n RuntimeEdge,\n RuntimeExternalSystem,\n RuntimeGraph,\n RuntimeQueue,\n RuntimeService,\n RuntimeTrigger,\n RuntimeUnresolved,\n} from './runtime-graph-model';\nimport { toError } from '../toError';\n\n// The committed data shape lives in runtime-graph-model.ts; re-exported here so every existing\n// `from './runtime-graph'` import keeps working and there is still one obvious place to import from.\nexport type {\n RuntimeApi,\n RuntimeEdge,\n RuntimeExternalSystem,\n RuntimeGraph,\n RuntimeQueue,\n RuntimeService,\n RuntimeTrigger,\n RuntimeUnresolved,\n} from './runtime-graph-model';\n\n// Persistence lives in runtime-graph-io.ts; re-exported for the same reason as the model types.\nexport {\n DEFAULT_RUNTIME_GRAPH_PATH,\n saveRuntimeGraph,\n runtimeGraphFileExists,\n loadRuntimeGraph,\n serializeRuntimeGraph,\n} from './runtime-graph-io';\n\ninterface EdgeResult {\n edges: RuntimeEdge[];\n unresolved: RuntimeUnresolved[];\n queues: Record<string, RuntimeQueue>;\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 * Call sites that name a target the repo does not contain. Unlike a warning these FAIL the\n * build: the contract IS served in-repo, so the name is a typo or a stale rename, and the\n * only reason it stayed invisible is that the graph quietly fanned the edge out instead.\n * (A call to a service outside the repo never reaches here — nothing in-repo implements its\n * contract, so it is `unresolvedUses`.)\n */\n public readonly problems: string[] = [],\n ) {}\n}\n\n/**\n * Adjacency (service -> [targets]) used for leveling + cycle checks.\n *\n * PUBSUB EDGES ARE EXCLUDED. A queue is precisely the thing that decouples producer from consumer:\n * the producer returns as soon as the task is enqueued and never waits on the consumer, so a queued\n * hop is not a runtime dependency in the sense levels and cycle detection mean. Counting them would\n * make the common and correct `A → queue → A` (a service deferring its own work) an architecture\n * cycle, and would rank services by an ordering that does not constrain deploy or startup.\n */\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 (edge.type === 'pubsub') continue;\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 /** Addressable name -> the runtime node answering to it; how a targeted call resolves. */\n private readonly nodeByServiceName = new Map<string, string>();\n private readonly warnings: string[] = [];\n private readonly problems: 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 * The committed per-contract method table from dependencies.json. Empty means the file\n * predates `apiContracts`: every pubsub edge then falls back to one unnamed queue per\n * service pair, exactly as before, instead of failing on a missing table.\n */\n private readonly apiContracts: ApiContracts = {},\n /**\n * Declared external systems from dependencies.json, keyed by identity. Empty means nothing\n * was declared, which renders exactly as it did before declarations existed.\n */\n private readonly externalSystemDecls: ExternalSystemDecls = {},\n ) {\n // A node ALWAYS answers to its own module name, so a repo whose deployed names match its\n // project names needs no declaration at all — and no alias can ever redirect 'ai-chat' away\n // from the ai-chat module. Module names are therefore claimed FIRST and are unshadowable.\n for (const name of Object.keys(projects).sort()) {\n if (this.isNode(name)) this.nodeByServiceName.set(name, name);\n }\n // `serviceName` then adds the alias a repo needs when its deployed name is NOT its module\n // name (helper-svr serving 'helper-portal'). Colliding with a module is a misconfiguration,\n // not a precedence question: the alias is silently unreachable, so say so.\n for (const name of Object.keys(projects).sort()) {\n const declared = projects[name].serviceName;\n if (!this.isNode(name) || declared === undefined) continue;\n const claimant = this.nodeByServiceName.get(declared);\n if (claimant === undefined) {\n this.nodeByServiceName.set(declared, name);\n } else if (claimant !== name) {\n this.problems.push(\n `${name} declares serviceName '${declared}', but that is already the module name of ` +\n `${claimant} — the alias can never be reached. Rename one of them.`,\n );\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 queues: edgeResult.queues,\n triggers: this.buildTriggers(decls),\n };\n attachExternalSystems(graph, resolveExternalSystems(this.externalSystemDecls, services));\n return new RuntimeGraphReport(graph, this.warnings, this.problems);\n }\n\n /**\n * The clock- and outside-driven entry points: for every contract a node IMPLEMENTS, each method\n * declared `cron` or `external` becomes a trigger pointing AT that node.\n *\n * Driven off `implements` rather than `uses` on purpose — these have no in-repo caller at all,\n * which is why they never produced an edge and stayed invisible.\n */\n private buildTriggers(decls: ScanDecl[]): RuntimeTrigger[] {\n const triggers: RuntimeTrigger[] = [];\n for (const decl of decls) {\n for (const ref of decl.implementsApis) {\n for (const method of this.methodsOf(ref.api)) {\n if (method.kind !== 'cron' && method.kind !== 'external') continue;\n const trigger: RuntimeTrigger = {\n kind: method.kind,\n api: ref.api,\n method: method.name,\n service: decl.name,\n };\n if (method.kind === 'cron') trigger.queueName = method.queueName;\n triggers.push(trigger);\n }\n }\n }\n return triggers.sort(\n (a: RuntimeTrigger, b: RuntimeTrigger) =>\n a.kind.localeCompare(b.kind) ||\n a.api.localeCompare(b.api) ||\n a.method.localeCompare(b.method) ||\n a.service.localeCompare(b.service),\n );\n }\n\n /** The committed method table for a contract, or [] when it declares none (e.g. a vendor seam). */\n private methodsOf(api: string): ApiMethodMeta[] {\n return this.apiContracts[api]?.methods ?? [];\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 const queues = new Map<string, RuntimeQueue>();\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 // Nobody in-repo serves it. For a vendor contract that is the ANSWER, not a gap:\n // the call leaves the repo, and the viz terminates it at a dashed vendor node.\n unresolved.push({ service: decl.name, api: ref.api });\n continue;\n }\n for (const target of this.targetsFor(decl.name, ref, implementers)) {\n // A service calling ITSELF synchronously is noise; a service ENQUEUEING to\n // itself is a real, common topology (deferring its own work), and dropping it\n // was hiding the single most interesting thing about a queue.\n if (target === decl.name && ref.type !== 'pubsub') continue;\n if (ref.type === 'pubsub') {\n this.addQueuedEdges(decl.name, target, ref.api, viaByKey, queues);\n continue;\n }\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 {\n edges: this.edgesFromKeys(viaByKey),\n unresolved: sortUnresolved(unresolved),\n queues: sortedQueues(queues),\n };\n }\n\n /**\n * One edge PER QUEUED METHOD of `api`, plus the queue each flows through.\n *\n * Per method rather than per service pair because that is the unit Cloud Tasks and Terraform\n * create: two services exchanging three queued methods run three independently-configured,\n * independently-backed-up queues, and collapsing them into one arrow hides which one is stuck.\n *\n * A contract with no committed method table (a dependencies.json predating `apiContracts`)\n * degrades to a single unnamed queue for the pair — the old behavior — rather than vanishing.\n */\n private addQueuedEdges(\n from: string,\n to: string,\n api: string,\n viaByKey: Map<string, Set<string>>,\n queues: Map<string, RuntimeQueue>,\n ): void {\n const queued = this.methodsOf(api).filter((m: ApiMethodMeta) => m.kind === 'cloudtasks');\n if (queued.length === 0) {\n const key = `${from} ${to} pubsub `;\n if (!viaByKey.has(key)) viaByKey.set(key, new Set());\n viaByKey.get(key)!.add(api);\n return;\n }\n for (const method of queued) {\n const queueKey = `${api}.${method.name}`;\n const key = `${from} ${to} pubsub ${queueKey}`;\n if (!viaByKey.has(key)) viaByKey.set(key, new Set());\n viaByKey.get(key)!.add(api);\n\n let queue = queues.get(queueKey);\n if (queue === undefined) {\n queue = {\n api,\n method: method.name,\n // Every cloudtasks method carries a queueName; the fallback covers a\n // dependencies.json written before queueName became kind-specific.\n queueName: method.queueName ?? `${api}-${method.name}`,\n producedBy: [],\n consumedBy: [],\n };\n queues.set(queueKey, queue);\n }\n if (!queue.producedBy.includes(from)) queue.producedBy.push(from);\n if (!queue.consumedBy.includes(to)) queue.consumedBy.push(to);\n }\n }\n\n /**\n * WHICH implementers this one `uses` reaches. Resolution order (most specific wins):\n * 1. a literal `ClientConfig` at the call site (`ref.targetService`) — names ONE node;\n * 2. else the calling project's declared `callsService` (project.json) — for the shared-library\n * case where the literal cannot sit at the call site, it lives one indirection away in the app;\n * 3. else the historical fan-out — the only safe superset — recording WHY, so a fanned-out (i.e.\n * possibly fictional) edge can never pass for a derived one.\n * A named target (1 or 2) that resolves to no module, or to a module that does not serve the\n * contract, is a PROBLEM (fails the build), never a silent drop.\n */\n private targetsFor(user: string, ref: ApiRef, implementers: string[]): string[] {\n const literal = ref.targetService;\n if (literal !== undefined) return this.resolveNamedTarget(user, ref, implementers, literal, 'literal');\n\n const declared = this.callsServiceFor(user, ref.api);\n if (declared !== undefined) return this.resolveNamedTarget(user, ref, implementers, declared, 'callsService');\n\n return this.untargetedFanOut(user, ref, implementers);\n }\n\n /**\n * The declared `callsService` target for this node's use of `api`, or undefined when none applies.\n * A string declaration aims every untargeted use at one service; a map aims per api-class.\n */\n private callsServiceFor(user: string, api: string): string | undefined {\n const declared = this.projects[user]?.callsService;\n if (declared === undefined) return undefined;\n if (typeof declared === 'string') return declared;\n return declared[api];\n }\n\n /**\n * Resolve a NAMED target (from a call-site literal or a project-level `callsService`) to the one\n * module that serves it. A name no module answers to, or a module that does not serve the\n * contract, fails the build — the wording names which declaration to fix, `source` deciding it.\n */\n private resolveNamedTarget(\n user: string,\n ref: ApiRef,\n implementers: string[],\n wanted: string,\n source: 'literal' | 'callsService',\n ): string[] {\n const node = this.nodeByServiceName.get(wanted);\n const served = `${implementers.length} module(s) serve this contract (${implementers.join(', ')})`;\n if (node === undefined) {\n this.problems.push(\n source === 'callsService'\n ? `${user} declares metadata.webpieces.callsService '${wanted}' in its project.json, but NO ` +\n `module answers to that name. ${served}. Point callsService at a module name, or declare ` +\n `metadata.webpieces.serviceName: '${wanted}' on the module that serves \"${ref.api}\".`\n : `${user} calls \"${ref.api}\" at service '${wanted}', but NO module answers to that name. ` +\n `${served}. Either use the module name, or declare metadata.webpieces.serviceName: ` +\n `'${wanted}' on the module that serves it (and translate any environment prefix in ` +\n `ClientRegistry.setDeriver, not here).`,\n );\n return implementers;\n }\n if (!implementers.includes(node)) {\n const via =\n source === 'callsService'\n ? `${user} declares callsService '${wanted}' (module ${node})`\n : `${user} calls \"${ref.api}\" at service '${wanted}' (module ${node})`;\n this.problems.push(\n `${via}, but ${node} does NOT serve \"${ref.api}\" — ${implementers.join(', ')} do. At runtime that ` +\n `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 `or, when the client is built in a shared library (no literal can sit at the call site), ` +\n `declare metadata.webpieces.callsService: '<serviceName>' on ${user}'s project.json.`,\n );\n }\n return implementers;\n }\n\n /**\n * Rebuild edges from their `\"from to type queue\"` keys. The 4th part is the queue (\"Api.method\")\n * and is empty for every non-queued edge; it is part of the KEY so two queued methods between\n * the same pair stay two edges instead of collapsing into one.\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 const edge: RuntimeEdge = {\n from: parts[0],\n to: parts[1],\n via: Array.from(viaByKey.get(key)!).sort(),\n type: parts[2] as ApiTransport,\n };\n if (parts[3] !== undefined && parts[3] !== '') edge.queue = parts[3];\n edges.push(edge);\n }\n edges.sort(\n (a: RuntimeEdge, b: RuntimeEdge) =>\n a.from.localeCompare(b.from) ||\n a.to.localeCompare(b.to) ||\n (a.type ?? '').localeCompare(b.type ?? '') ||\n (a.queue ?? '').localeCompare(b.queue ?? ''),\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 // A queued hop depends on the QUEUE, not on the peer: the producer hands off and returns,\n // so naming the consumer here would assert a coupling that does not exist (and would make\n // a service that defers work to itself look self-dependent).\n const dependsOn = Array.from(\n new Set(\n edges\n .filter((e: RuntimeEdge) => e.from === decl.name)\n .map((e: RuntimeEdge) => (e.queue === undefined ? e.to : `queue:${e.queue}`)),\n ),\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 callsService: this.projects[decl.name]?.callsService,\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 apiContracts: ApiContracts = {},\n): RuntimeGraph {\n return deriveRuntimeGraphReport(projects, hiddenProjects, apiContracts).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 apiContracts: ApiContracts = {},\n externalSystems: ExternalSystemDecls = {},\n): RuntimeGraphReport {\n return new RuntimeGraphDeriver(projects, hiddenProjects, apiContracts, externalSystems).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/** Queues as a key-sorted object, with each producer/consumer list sorted, for a deterministic file. */\n// webpieces-disable no-function-outside-class -- pure data helper, matches the sibling helpers in this file\nfunction sortedQueues(queues: Map<string, RuntimeQueue>): Record<string, RuntimeQueue> {\n const out: Record<string, RuntimeQueue> = {};\n for (const key of [...queues.keys()].sort()) {\n const queue = queues.get(key)!;\n queue.producedBy.sort();\n queue.consumedBy.sort();\n out[key] = queue;\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"]}
|
|
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\", in\n * priority order: (1) a literal at the call site —\n * `createRpcClient(Y, new ClientConfig('helper-fsdb'))` — kept as\n * `ApiRef.targetService`; else (2) the calling project's declared `callsService`\n * (project.json metadata.webpieces.callsService), the symmetric half of\n * `serviceName` for the shared-library case where the client is built once from a\n * config field so no literal can sit at the call site; else (3) fan-out. A named\n * target from (1) or (2) is matched against each node's DECLARED `serviceName`.\n * Fanning an edge out to every implementer is catastrophic for a company-wide\n * contract registered in a shared library — it manufactures calls that cannot\n * happen, and cycles that do not exist. When a target cannot be resolved the old\n * fan-out still happens, but a warning names the call site (see\n * RuntimeGraphReport.warnings): a wrong-but-green graph is worse than a failing\n * one, so it must never degrade silently.\n */\n\nimport { sortGraphTopologically } from './graph-sorter';\nimport type { EnhancedGraph } from './graph-sorter';\nimport type {\n ApiContracts,\n ApiMethodMeta,\n ApiRef,\n ApiTransport,\n ExternalSystemDecls,\n} from './api-usage/api-relations';\nimport { apiRefKey, sortApiRefs } from './api-usage/api-relations';\nimport { attachExternalSystems, resolveExternalSystems } from './api-usage/external-systems';\nimport type {\n RuntimeApi,\n RuntimeEdge,\n RuntimeExternalSystem,\n RuntimeGraph,\n RuntimeQueue,\n RuntimeService,\n RuntimeTrigger,\n RuntimeUnresolved,\n} from './runtime-graph-model';\nimport { toError } from '../toError';\n\n// The committed data shape lives in runtime-graph-model.ts; re-exported here so every existing\n// `from './runtime-graph'` import keeps working and there is still one obvious place to import from.\nexport type {\n RuntimeApi,\n RuntimeEdge,\n RuntimeExternalSystem,\n RuntimeGraph,\n RuntimeQueue,\n RuntimeService,\n RuntimeTrigger,\n RuntimeUnresolved,\n} from './runtime-graph-model';\n\n// Persistence lives in runtime-graph-io.ts; re-exported for the same reason as the model types.\nexport {\n DEFAULT_RUNTIME_GRAPH_PATH,\n saveRuntimeGraph,\n runtimeGraphFileExists,\n loadRuntimeGraph,\n serializeRuntimeGraph,\n} from './runtime-graph-io';\n\ninterface EdgeResult {\n edges: RuntimeEdge[];\n unresolved: RuntimeUnresolved[];\n queues: Record<string, RuntimeQueue>;\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 * Call sites that name a target the repo does not contain. Unlike a warning these FAIL the\n * build: the contract IS served in-repo, so the name is a typo or a stale rename, and the\n * only reason it stayed invisible is that the graph quietly fanned the edge out instead.\n * (A call to a service outside the repo never reaches here — nothing in-repo implements its\n * contract, so it is `unresolvedUses`.)\n */\n public readonly problems: string[] = [],\n ) {}\n}\n\n/**\n * Adjacency (service -> [targets]) used for leveling + cycle checks.\n *\n * PUBSUB EDGES ARE EXCLUDED. A queue is precisely the thing that decouples producer from consumer:\n * the producer returns as soon as the task is enqueued and never waits on the consumer, so a queued\n * hop is not a runtime dependency in the sense levels and cycle detection mean. Counting them would\n * make the common and correct `A → queue → A` (a service deferring its own work) an architecture\n * cycle, and would rank services by an ordering that does not constrain deploy or startup.\n */\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 (edge.type === 'pubsub') continue;\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 /** Addressable name -> the runtime node answering to it; how a targeted call resolves. */\n private readonly nodeByServiceName = new Map<string, string>();\n private readonly warnings: string[] = [];\n private readonly problems: 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 * The committed per-contract method table from dependencies.json. Empty means the file\n * predates `apiContracts`: every pubsub edge then falls back to one unnamed queue per\n * service pair, exactly as before, instead of failing on a missing table.\n */\n private readonly apiContracts: ApiContracts = {},\n /**\n * Declared external systems from dependencies.json, keyed by identity. Empty means nothing\n * was declared, which renders exactly as it did before declarations existed.\n */\n private readonly externalSystemDecls: ExternalSystemDecls = {},\n ) {\n // A node ALWAYS answers to its own module name, so a repo whose deployed names match its\n // project names needs no declaration at all — and no alias can ever redirect 'ai-chat' away\n // from the ai-chat module. Module names are therefore claimed FIRST and are unshadowable.\n for (const name of Object.keys(projects).sort()) {\n if (this.isNode(name)) this.nodeByServiceName.set(name, name);\n }\n // `serviceName` then adds the alias a repo needs when its deployed name is NOT its module\n // name (helper-svr serving 'helper-portal'). Colliding with a module is a misconfiguration,\n // not a precedence question: the alias is silently unreachable, so say so.\n for (const name of Object.keys(projects).sort()) {\n const declared = projects[name].serviceName;\n if (!this.isNode(name) || declared === undefined) continue;\n const claimant = this.nodeByServiceName.get(declared);\n if (claimant === undefined) {\n this.nodeByServiceName.set(declared, name);\n } else if (claimant !== name) {\n this.problems.push(\n `${name} declares serviceName '${declared}', but that is already the module name of ` +\n `${claimant} — the alias can never be reached. Rename one of them.`,\n );\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 queues: edgeResult.queues,\n triggers: this.buildTriggers(decls),\n };\n attachExternalSystems(graph, resolveExternalSystems(this.externalSystemDecls, services));\n return new RuntimeGraphReport(graph, this.warnings, this.problems);\n }\n\n /**\n * The clock- and outside-driven entry points: for every contract a node IMPLEMENTS, each method\n * declared `cron` or `external` becomes a trigger pointing AT that node.\n *\n * Driven off `implements` rather than `uses` on purpose — these have no in-repo caller at all,\n * which is why they never produced an edge and stayed invisible.\n */\n private buildTriggers(decls: ScanDecl[]): RuntimeTrigger[] {\n const triggers: RuntimeTrigger[] = [];\n for (const decl of decls) {\n for (const ref of decl.implementsApis) {\n for (const method of this.methodsOf(ref.api)) {\n if (method.kind !== 'cron' && method.kind !== 'external') continue;\n const trigger: RuntimeTrigger = {\n kind: method.kind,\n api: ref.api,\n method: method.name,\n service: decl.name,\n };\n if (method.kind === 'cron') trigger.queueName = method.queueName;\n triggers.push(trigger);\n }\n }\n }\n return triggers.sort(\n (a: RuntimeTrigger, b: RuntimeTrigger) =>\n a.kind.localeCompare(b.kind) ||\n a.api.localeCompare(b.api) ||\n a.method.localeCompare(b.method) ||\n a.service.localeCompare(b.service),\n );\n }\n\n /** The committed method table for a contract, or [] when it declares none (e.g. a vendor seam). */\n private methodsOf(api: string): ApiMethodMeta[] {\n return this.apiContracts[api]?.methods ?? [];\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 // EVERY node gets a decl, including one with zero relations. Gating this on \"has at\n // least one implements/uses\" silently DELETED any deployed server whose work arrives\n // from outside the repo (a pull subscription): the diagram looked complete while missing\n // a running service — confidently wrong, not visibly incomplete. isNode() is the whole\n // test; drawOnGraph:false is the ONE opt-out, and such a node renders as an isolated box.\n decls.push({\n name,\n implementsApis: dedupApiRefs(sortApiRefs(sink.implementsApis)),\n usesApis: dedupApiRefs(sortApiRefs(sink.usesApis)),\n implementsVia: sink.implementsVia,\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 const queues = new Map<string, RuntimeQueue>();\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 // Nobody in-repo serves it. For a vendor contract that is the ANSWER, not a gap:\n // the call leaves the repo, and the viz terminates it at a dashed vendor node.\n unresolved.push({ service: decl.name, api: ref.api });\n continue;\n }\n for (const target of this.targetsFor(decl.name, ref, implementers)) {\n // A service calling ITSELF synchronously is noise; a service ENQUEUEING to\n // itself is a real, common topology (deferring its own work), and dropping it\n // was hiding the single most interesting thing about a queue.\n if (target === decl.name && ref.type !== 'pubsub') continue;\n if (ref.type === 'pubsub') {\n this.addQueuedEdges(decl.name, target, ref.api, viaByKey, queues);\n continue;\n }\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 {\n edges: this.edgesFromKeys(viaByKey),\n unresolved: sortUnresolved(unresolved),\n queues: sortedQueues(queues),\n };\n }\n\n /**\n * One edge PER QUEUED METHOD of `api`, plus the queue each flows through.\n *\n * Per method rather than per service pair because that is the unit Cloud Tasks and Terraform\n * create: two services exchanging three queued methods run three independently-configured,\n * independently-backed-up queues, and collapsing them into one arrow hides which one is stuck.\n *\n * A contract with no committed method table (a dependencies.json predating `apiContracts`)\n * degrades to a single unnamed queue for the pair — the old behavior — rather than vanishing.\n */\n private addQueuedEdges(\n from: string,\n to: string,\n api: string,\n viaByKey: Map<string, Set<string>>,\n queues: Map<string, RuntimeQueue>,\n ): void {\n const queued = this.methodsOf(api).filter((m: ApiMethodMeta) => m.kind === 'cloudtasks');\n if (queued.length === 0) {\n const key = `${from} ${to} pubsub `;\n if (!viaByKey.has(key)) viaByKey.set(key, new Set());\n viaByKey.get(key)!.add(api);\n return;\n }\n for (const method of queued) {\n const queueKey = `${api}.${method.name}`;\n const key = `${from} ${to} pubsub ${queueKey}`;\n if (!viaByKey.has(key)) viaByKey.set(key, new Set());\n viaByKey.get(key)!.add(api);\n\n let queue = queues.get(queueKey);\n if (queue === undefined) {\n queue = {\n api,\n method: method.name,\n // Every cloudtasks method carries a queueName; the fallback covers a\n // dependencies.json written before queueName became kind-specific.\n queueName: method.queueName ?? `${api}-${method.name}`,\n producedBy: [],\n consumedBy: [],\n };\n queues.set(queueKey, queue);\n }\n if (!queue.producedBy.includes(from)) queue.producedBy.push(from);\n if (!queue.consumedBy.includes(to)) queue.consumedBy.push(to);\n }\n }\n\n /**\n * WHICH implementers this one `uses` reaches. Resolution order (most specific wins):\n * 1. a literal `ClientConfig` at the call site (`ref.targetService`) — names ONE node;\n * 2. else the calling project's declared `callsService` (project.json) — for the shared-library\n * case where the literal cannot sit at the call site, it lives one indirection away in the app;\n * 3. else the historical fan-out — the only safe superset — recording WHY, so a fanned-out (i.e.\n * possibly fictional) edge can never pass for a derived one.\n * A named target (1 or 2) that resolves to no module, or to a module that does not serve the\n * contract, is a PROBLEM (fails the build), never a silent drop.\n */\n private targetsFor(user: string, ref: ApiRef, implementers: string[]): string[] {\n const literal = ref.targetService;\n if (literal !== undefined) return this.resolveNamedTarget(user, ref, implementers, literal, 'literal');\n\n const declared = this.callsServiceFor(user, ref.api);\n if (declared !== undefined) return this.resolveNamedTarget(user, ref, implementers, declared, 'callsService');\n\n return this.untargetedFanOut(user, ref, implementers);\n }\n\n /**\n * The declared `callsService` target for this node's use of `api`, or undefined when none applies.\n * A string declaration aims every untargeted use at one service; a map aims per api-class.\n */\n private callsServiceFor(user: string, api: string): string | undefined {\n const declared = this.projects[user]?.callsService;\n if (declared === undefined) return undefined;\n if (typeof declared === 'string') return declared;\n return declared[api];\n }\n\n /**\n * Resolve a NAMED target (from a call-site literal or a project-level `callsService`) to the one\n * module that serves it. A name no module answers to, or a module that does not serve the\n * contract, fails the build — the wording names which declaration to fix, `source` deciding it.\n */\n private resolveNamedTarget(\n user: string,\n ref: ApiRef,\n implementers: string[],\n wanted: string,\n source: 'literal' | 'callsService',\n ): string[] {\n const node = this.nodeByServiceName.get(wanted);\n const served = `${implementers.length} module(s) serve this contract (${implementers.join(', ')})`;\n if (node === undefined) {\n this.problems.push(\n source === 'callsService'\n ? `${user} declares metadata.webpieces.callsService '${wanted}' in its project.json, but NO ` +\n `module answers to that name. ${served}. Point callsService at a module name, or declare ` +\n `metadata.webpieces.serviceName: '${wanted}' on the module that serves \"${ref.api}\".`\n : `${user} calls \"${ref.api}\" at service '${wanted}', but NO module answers to that name. ` +\n `${served}. Either use the module name, or declare metadata.webpieces.serviceName: ` +\n `'${wanted}' on the module that serves it (and translate any environment prefix in ` +\n `ClientRegistry.setDeriver, not here).`,\n );\n return implementers;\n }\n if (!implementers.includes(node)) {\n const via =\n source === 'callsService'\n ? `${user} declares callsService '${wanted}' (module ${node})`\n : `${user} calls \"${ref.api}\" at service '${wanted}' (module ${node})`;\n this.problems.push(\n `${via}, but ${node} does NOT serve \"${ref.api}\" — ${implementers.join(', ')} do. At runtime that ` +\n `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 `or, when the client is built in a shared library (no literal can sit at the call site), ` +\n `declare metadata.webpieces.callsService: '<serviceName>' on ${user}'s project.json.`,\n );\n }\n return implementers;\n }\n\n /**\n * Rebuild edges from their `\"from to type queue\"` keys. The 4th part is the queue (\"Api.method\")\n * and is empty for every non-queued edge; it is part of the KEY so two queued methods between\n * the same pair stay two edges instead of collapsing into one.\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 const edge: RuntimeEdge = {\n from: parts[0],\n to: parts[1],\n via: Array.from(viaByKey.get(key)!).sort(),\n type: parts[2] as ApiTransport,\n };\n if (parts[3] !== undefined && parts[3] !== '') edge.queue = parts[3];\n edges.push(edge);\n }\n edges.sort(\n (a: RuntimeEdge, b: RuntimeEdge) =>\n a.from.localeCompare(b.from) ||\n a.to.localeCompare(b.to) ||\n (a.type ?? '').localeCompare(b.type ?? '') ||\n (a.queue ?? '').localeCompare(b.queue ?? ''),\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 // A queued hop depends on the QUEUE, not on the peer: the producer hands off and returns,\n // so naming the consumer here would assert a coupling that does not exist (and would make\n // a service that defers work to itself look self-dependent).\n const dependsOn = Array.from(\n new Set(\n edges\n .filter((e: RuntimeEdge) => e.from === decl.name)\n .map((e: RuntimeEdge) => (e.queue === undefined ? e.to : `queue:${e.queue}`)),\n ),\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 role: this.projects[decl.name]?.role, // labels a relation-less node for what it IS\n serviceName: this.projects[decl.name]?.serviceName,\n callsService: this.projects[decl.name]?.callsService,\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 apiContracts: ApiContracts = {},\n): RuntimeGraph {\n return deriveRuntimeGraphReport(projects, hiddenProjects, apiContracts).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 apiContracts: ApiContracts = {},\n externalSystems: ExternalSystemDecls = {},\n): RuntimeGraphReport {\n return new RuntimeGraphDeriver(projects, hiddenProjects, apiContracts, externalSystems).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/** Queues as a key-sorted object, with each producer/consumer list sorted, for a deterministic file. */\n// webpieces-disable no-function-outside-class -- pure data helper, matches the sibling helpers in this file\nfunction sortedQueues(queues: Map<string, RuntimeQueue>): Record<string, RuntimeQueue> {\n const out: Record<string, RuntimeQueue> = {};\n for (const key of [...queues.keys()].sort()) {\n const queue = queues.get(key)!;\n queue.producedBy.sort();\n queue.consumedBy.sort();\n out[key] = queue;\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"]}
|
|
@@ -153,7 +153,10 @@ function implementsEntries(svc) {
|
|
|
153
153
|
*/
|
|
154
154
|
// webpieces-disable no-function-outside-class -- DOT label builder, matching getShortName in this file
|
|
155
155
|
function nodeLabel(name, svc) {
|
|
156
|
-
|
|
156
|
+
// The DECLARED role when the graph carries one; the old inference only as a fallback for a
|
|
157
|
+
// runtime-dependencies.json committed before `role` existed. Inferring it labelled every server
|
|
158
|
+
// with no implements as a "client", which is exactly wrong for a queue-driven service.
|
|
159
|
+
const role = svc.role ?? (svc.implements.length > 0 ? 'server' : 'client');
|
|
157
160
|
// The declared name is quoted for the reader — those quotes MUST be DOT-escaped, or they end
|
|
158
161
|
// the label string and the whole graph stops parsing.
|
|
159
162
|
const declared = svc.serviceName === undefined ? '' : `, \\"${(0, dot_syntax_1.dotValue)(svc.serviceName)}\\"`;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runtime-visualizer.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/runtime-visualizer.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Runtime Visualizer\n *\n * Renders the runtime microservice graph (services + inferred Z -> X edges,\n * each labeled with the api(s) they flow over) to DOT + interactive HTML in\n * tmp/webpieces/runtime-architecture.{dot,html}.\n *\n * Each service node names the contracts it IMPLEMENTS. That list used to be\n * collapsed into a server/client boolean and thrown away — leaving an api that a\n * server serves but nothing in-repo calls completely invisible, and making a\n * correct api design look like a detection failure. What a service USES is NOT\n * listed: every use already draws an outgoing arrow labeled with the same\n * contract, so repeating it in the box only made every box wider.\n *\n * Shape says what a node IS and line style says what a call IS. Solid = rpc: the\n * request follows the arrow and the response flows back. Dashed = event: it flows\n * in the arrow's direction and returns once it is queued. A queue is a sideways\n * cylinder, a datastore an upright one.\n *\n * Calls that leave the repo (a contract NOTHING in-repo implements — firestore,\n * gmail, ...) are drawn as terminal nodes, so the vendor systems that actually\n * page you at 3am stop being missing from the picture. One that DECLARES what it\n * is, via an `@externalSystem` JSDoc tag or an `external:` nx tag, gets the shape\n * of the thing it is; the rest stay generic dashed boxes. They are RENDER-ONLY:\n * derivation, levels and cycle detection never see them.\n *\n * The same is true in the other direction for endpoints nothing in-repo CALLS: a\n * `cron` method hangs off a clock and an `external` method off a dashed inbound\n * box. Those are the entry points that wake a service up at 3am, and a graph\n * built only from in-repo callers cannot show them at all.\n */\n\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport type { RuntimeGraph, RuntimeEdge, RuntimeQueue, RuntimeService, RuntimeTrigger } from './runtime-graph';\nimport { dotValue, recordValue, assertValidDot } from './dot-syntax';\n\nconst LEVEL_COLORS: Record<number, string> = {\n 0: '#E8F5E9',\n 1: '#E3F2FD',\n 2: '#FFF3E0',\n 3: '#FCE4EC',\n};\n\nconst QUEUE_FILL = '#FFF3E0';\n\n/**\n * The queue node is an `Mrecord` whose FIRST field is empty, which draws a rounded outline with a\n * vertical cap line near one end — a cylinder lying on its side, distinguishing a queue from the\n * upright cylinder that now means a database.\n *\n * Two things here are load-bearing and easy to break:\n *\n * 1. NO surrounding `{}`. Record fields lay out along the rank direction, and this graph is\n * `rankdir=TB` (see {@link generateRuntimeDot}), where the default is horizontal — which is what\n * we want. Adding braces TOGGLES that, turning the cap line into a band across the top.\n * 2. The leading space is the empty field. The record parser trims it to nothing, which is the\n * point; it must survive as its own field, so the `|` cannot be dropped.\n *\n * Graphviz has no sideways cylinder and never has: `orientation=` is documented as rotating POLYGON\n * shapes, and `cylinder` is drawn with beziers, so it silently ignores the attribute (graphviz issue\n * #2244, open since 2022 and still reproducible on 13.0.0). This is the closest native shape.\n */\nconst QUEUE_SHAPE = 'Mrecord';\nconst QUEUE_LABEL_PREFIX = ' |';\n\n/**\n * Marker class stamped on every queue node. Graphviz copies `class` straight into the rendered\n * `<g class=\"node wp_queue\">`, which is how runtime-visualizer.client.js finds these nodes and\n * redraws them as true horizontal cylinders in the browser.\n *\n * A CLASS rather than an id prefix, because queue-kind EXTERNAL systems are queues too and share the\n * `system__` id space with databases — which must stay upright. Underscored, not hyphenated: DOT\n * emits a hyphen as `-`, which is harmless but needlessly surprising to anyone reading the SVG.\n */\nconst QUEUE_CLASS = 'wp_queue';\n\n/** Fill for the upright cylinder standing for an external DATASTORE (firestore, postgres, ...). */\nconst DATABASE_FILL = '#E1F5FE';\n\n/** Shape per external-system kind. Anything unrecognised falls back to the generic dashed box. */\nconst EXTERNAL_SHAPES: Record<string, string> = {\n database: 'cylinder',\n cache: 'cylinder',\n queue: 'Mrecord',\n storage: 'folder',\n};\n\n/** Fill per external-system kind, paired with {@link EXTERNAL_SHAPES}. */\nconst EXTERNAL_FILLS: Record<string, string> = {\n database: DATABASE_FILL,\n cache: DATABASE_FILL,\n queue: QUEUE_FILL,\n storage: '#F3E5F5',\n};\n\n/** Fill + border for the dashed terminal node standing for a system outside this repo. */\nconst EXTERNAL_FILL = '#FAFAFA';\nconst EXTERNAL_BORDER = '#9E9E9E';\n\n/** Fill + border for the clock node standing for a scheduler-driven endpoint. */\nconst CRON_FILL = '#FFF9C4';\nconst CRON_BORDER = '#F9A825';\n\n/** Apis per line inside a node label — beyond this the box grows wider than it is readable. */\nconst APIS_PER_LABEL_LINE = 3;\n\n/** Separator for the (service, external-library) grouping key; illegal in both project names. */\nconst PAIR_SEP = '|';\n\n/** Render options for the runtime graph. */\nexport class RuntimeVizOptions {\n constructor(\n /**\n * Draw the dashed terminal nodes for contracts nothing in-repo implements. On by default;\n * a repo whose external surface is noisy can turn them off in webpieces.config.json\n * (runtime-architecture.showExternalNodes).\n */\n public readonly showExternalNodes: boolean = true,\n ) {}\n}\n\nfunction getShortName(name: string): string {\n return name.includes('/') ? name.split('/').pop()! : name;\n}\n\n/** Chunk a list into `\\n`-separated label lines of at most APIS_PER_LABEL_LINE entries. */\n// webpieces-disable no-function-outside-class -- DOT label builder, matching getShortName in this file\nfunction labelList(entries: string[]): string {\n const lines: string[] = [];\n const safe = entries.map((entry: string) => dotValue(entry));\n for (let i = 0; i < safe.length; i += APIS_PER_LABEL_LINE) {\n lines.push(safe.slice(i, i + APIS_PER_LABEL_LINE).join(', '));\n }\n return lines.join('\\\\n');\n}\n\n/**\n * The implemented-api entries for a node label. An api served through an EMBEDDED LIBRARY is\n * annotated with that library, because \"who implements WarmupApi?\" otherwise requires knowing that\n * the derivation walks the dependsOn closure and then walking it by hand.\n */\n// webpieces-disable no-function-outside-class -- DOT label builder, matching getShortName in this file\nfunction implementsEntries(svc: RuntimeService): string[] {\n return svc.implements.map((api: string) => {\n const via = svc.implementsVia?.[api];\n return via === undefined ? api : `${api} (via ${getShortName(via)})`;\n });\n}\n\n/**\n * The full node label: name, role/level/declared service name, then the contracts it SERVES.\n *\n * What a service USES is deliberately absent. Every `uses` entry already draws an outgoing arrow —\n * to the implementing service, to a queue, or to an external node — and the arrow carries the same\n * contract name as its label, so listing them in the box restated the picture and made every box\n * wider than it needed to be. `implements` stays because it has no such arrow: an api a service\n * serves but nothing in-repo calls is invisible otherwise.\n *\n * The one case this loses information is `showExternalNodes:false`, where an outbound call draws no\n * node and therefore no arrow. That is precisely what that opt-out asks for, and the legend says so.\n */\n// webpieces-disable no-function-outside-class -- DOT label builder, matching getShortName in this file\nfunction nodeLabel(name: string, svc: RuntimeService): string {\n const role = svc.implements.length > 0 ? 'server' : 'client';\n // The declared name is quoted for the reader — those quotes MUST be DOT-escaped, or they end\n // the label string and the whole graph stops parsing.\n const declared = svc.serviceName === undefined ? '' : `, \\\\\"${dotValue(svc.serviceName)}\\\\\"`;\n let label = `${dotValue(getShortName(name))}\\\\n(${role}, L${svc.level}${declared})`;\n if (svc.implements.length > 0) label += `\\\\nimplements: ${labelList(implementsEntries(svc))}`;\n return label;\n}\n\n/**\n * DOT for ONE runtime edge. rpc → a direct labeled SOLID arrow (producer calls consumer, response\n * comes back). pubsub → the producer enqueues and the consumer is delivered later, so we draw\n * producer → QUEUE → consumer through a sideways-cylinder queue node with DASHED arrows.\n *\n * Solid vs dashed is the graph's one line-level distinction: solid is a call that returns a\n * response, dashed is an event that returns as soon as it is queued.\n */\n// webpieces-disable no-function-outside-class -- DOT string builder, matching getShortName in this file\nfunction edgeDot(edge: RuntimeEdge, queues: Record<string, RuntimeQueue>): string {\n const from = dotValue(getShortName(edge.from));\n const to = dotValue(getShortName(edge.to));\n // Kept RAW: an ordinary edge label needs dotValue, the record-mode queue label needs\n // recordValue, and recordValue already applies dotValue — escaping here would double it.\n const viaRaw = edge.via.map((v: string) => getShortName(v)).join(', ');\n if (edge.type !== 'pubsub') {\n return ` \"${from}\" -> \"${to}\" [label=\"${dotValue(viaRaw)}\"];\\n`;\n }\n // The queue node is identified by the METHOD, not by the (from,to) pair, so every producer and\n // consumer of one queue converges on ONE box — including a service that enqueues to itself,\n // which then renders as a visible loop through its queue instead of vanishing.\n const queueId = edge.queue === undefined ? `queue__${from}__${to}` : `queue__${dotId(edge.queue)}`;\n const queueName = edge.queue === undefined ? undefined : queues[edge.queue]?.queueName;\n // Record-mode label: the text must clear recordValue(), and QUEUE_LABEL_PREFIX supplies the\n // empty leading field that draws the cylinder's end cap.\n const body =\n edge.queue === undefined\n ? `${recordValue(viaRaw)}\\\\nqueue`\n : `${recordValue(edge.queue)}\\\\nqueue: ${recordValue(queueName ?? edge.queue)}`;\n return (\n ` \"${queueId}\" [shape=${QUEUE_SHAPE}, style=\"filled\", fillcolor=\"${QUEUE_FILL}\", ` +\n `class=\"${QUEUE_CLASS}\", label=\"${QUEUE_LABEL_PREFIX}${body}\"];\\n` +\n ` \"${from}\" -> \"${queueId}\" [label=\"enqueue\", style=dashed];\\n` +\n ` \"${queueId}\" -> \"${to}\" [label=\"deliver\", style=dashed];\\n`\n );\n}\n\n/** A DOT-safe node-id fragment: anything but letters, digits and `_` becomes `_`. */\n// webpieces-disable no-function-outside-class -- DOT id builder, matching getShortName in this file\nfunction dotId(raw: string): string {\n return raw.replace(/[^A-Za-z0-9_]/g, '_');\n}\n\n/**\n * The clock and outside-system nodes for endpoints NOTHING in-repo calls.\n *\n * A cron sweep and a GCP push subscription are real runtime entry points with real Terraform behind\n * them, but they produce no runtime EDGE (there is no in-repo caller), so until now they were simply\n * absent — a server's most operationally interesting endpoint could be invisible on its own graph.\n * Both are drawn pointing INTO the service that serves them, the opposite direction from\n * {@link externalDot}'s outbound vendor calls.\n */\n// webpieces-disable no-function-outside-class -- DOT string builder, matching getShortName in this file\nfunction triggerDot(graph: RuntimeGraph, hidden: Set<string>): string {\n const triggers = graph.triggers.filter((t: RuntimeTrigger) => !hidden.has(t.service));\n if (triggers.length === 0) return '';\n\n let dot = '\\n // Entry points nothing in this repo calls: a clock, or a system outside the repo.\\n';\n for (const trigger of triggers) {\n const service = dotValue(getShortName(trigger.service));\n const label = dotValue(`${trigger.api}.${trigger.method}`);\n if (trigger.kind === 'cron') {\n const id = `cron__${dotId(`${trigger.api}_${trigger.method}`)}`;\n const schedule = dotValue(trigger.queueName ?? `${trigger.api}-${trigger.method}`);\n dot +=\n ` \"${id}\" [shape=circle, style=\"filled\", fillcolor=\"${CRON_FILL}\", ` +\n `color=\"${CRON_BORDER}\", label=\"⏰\\\\ncron\"];\\n` +\n ` \"${id}\" -> \"${service}\" [label=\"${label}\\\\n${schedule}\", color=\"${CRON_BORDER}\"];\\n`;\n continue;\n }\n const id = `inbound__${dotId(trigger.api)}`;\n dot +=\n ` \"${id}\" [shape=box, style=\"dashed,filled\", fillcolor=\"${EXTERNAL_FILL}\", ` +\n `color=\"${EXTERNAL_BORDER}\", label=\"${dotValue(trigger.api)}\\\\n(external caller)\"];\\n` +\n ` \"${id}\" -> \"${service}\" [label=\"${label}\", style=dashed, color=\"${EXTERNAL_BORDER}\"];\\n`;\n }\n return dot;\n}\n\n/**\n * The DECLARED external systems — the ones that said what they are, so they get a shape that says\n * it: a cylinder for a database, a folder for a bucket. Everything undeclared falls through to\n * {@link externalDot}'s generic grey box, which is why adding this broke nothing existing.\n *\n * The arrows are SOLID. A call to firestore or postgres is synchronous — it returns a value — and\n * the graph's rule is that solid means \"response comes back\". Being outside the repo is carried by\n * the node's shape, never by the line style; conflating the two is what made a blocking database\n * read look like an event.\n */\n// webpieces-disable no-function-outside-class -- DOT string builder, matching getShortName in this file\nfunction externalSystemsDot(graph: RuntimeGraph, hidden: Set<string>): string {\n const systems = graph.externalSystems ?? {};\n const ids = Object.keys(systems).sort();\n if (ids.length === 0) return '';\n\n let dot = '\\n // Declared external systems — drawn with the shape of what they actually are.\\n';\n for (const id of ids) {\n const system = systems[id];\n const shape = EXTERNAL_SHAPES[system.kind] ?? 'box';\n const fill = EXTERNAL_FILLS[system.kind] ?? EXTERNAL_FILL;\n // An Mrecord-shaped system needs the same empty leading field as a queue node, or it\n // renders as a plain box and silently loses the sideways-cylinder read.\n const isQueue = shape === QUEUE_SHAPE;\n const prefix = isQueue ? QUEUE_LABEL_PREFIX : '';\n const text = isQueue ? recordValue(system.label) : dotValue(system.label);\n // Only a queue-kind system is marked: a database here is an UPRIGHT cylinder and must not be\n // caught by the browser-side reshaping that lays queues on their side.\n const marker = isQueue ? `class=\"${QUEUE_CLASS}\", ` : '';\n dot +=\n ` \"system__${dotId(id)}\" [shape=${shape}, style=\"filled\", fillcolor=\"${fill}\", ` +\n `${marker}label=\"${prefix}${text}\\\\n(external ${dotValue(system.kind)})\"];\\n`;\n }\n for (const id of ids) {\n const system = systems[id];\n const via = system.apis.length === 0 ? '' : ` [label=\"${labelList([...system.apis].sort())}\"]`;\n for (const service of [...system.usedBy].sort()) {\n if (hidden.has(service)) continue;\n dot += ` \"${dotValue(getShortName(service))}\" -> \"system__${dotId(id)}\"${via};\\n`;\n }\n }\n return dot;\n}\n\n/**\n * The dashed terminal nodes + edges for calls that LEAVE the repo. Built from `unresolvedUses` —\n * a contract used by a node and implemented by nobody in-repo — which the derivation already\n * computes and which was, until now, only ever printed as a warning.\n *\n * Grouped by the api-lib that owns the contracts, so a service reaching three firestore contracts\n * draws ONE `lib-firestore (external)` box rather than three. These are drawn, never derived: they\n * are absent from levels, cycle detection and the transitive implements attribution.\n *\n * A contract carrying an `@externalSystem` declaration is skipped here — {@link externalSystemsDot}\n * has already drawn it with a real shape, and rendering it in both places would double the node.\n */\n// webpieces-disable no-function-outside-class -- DOT string builder, matching getShortName in this file\nfunction externalDot(graph: RuntimeGraph, hidden: Set<string>): string {\n // \"service|externalName\" -> the apis flowing over it.\n const apisByPair = new Map<string, string[]>();\n for (const use of graph.unresolvedUses) {\n if (hidden.has(use.service)) continue;\n if (graph.apis[use.api]?.externalSystem !== undefined) continue;\n const external = dotValue(getShortName(graph.apis[use.api]?.owner ?? use.api));\n const key = `${use.service}${PAIR_SEP}${external}`;\n if (!apisByPair.has(key)) apisByPair.set(key, []);\n apisByPair.get(key)!.push(use.api);\n }\n if (apisByPair.size === 0) return '';\n\n let dot = '\\n // Systems outside this repo — no in-repo service implements these contracts.\\n';\n // The node ID is prefixed so an external library can never collide with a service of the same\n // short name; only the label carries the bare name.\n const externals = new Set([...apisByPair.keys()].map((key: string) => key.split(PAIR_SEP)[1]));\n for (const external of [...externals].sort()) {\n dot +=\n ` \"external__${external}\" [shape=box, style=\"dashed,filled\", fillcolor=\"${EXTERNAL_FILL}\", ` +\n `color=\"${EXTERNAL_BORDER}\", label=\"${external}\\\\n(external)\"];\\n`;\n }\n for (const key of [...apisByPair.keys()].sort()) {\n const parts = key.split(PAIR_SEP);\n const service = parts[0];\n const external = parts[1];\n const via = labelList(apisByPair.get(key)!.sort());\n // SOLID: this is a synchronous call that returns a value. Dashed is reserved for events, and\n // \"outside the repo\" is already said by the node's dashed border.\n dot +=\n ` \"${dotValue(getShortName(service))}\" -> \"external__${external}\" ` +\n `[label=\"${via}\", color=\"${EXTERNAL_BORDER}\"];\\n`;\n }\n return dot;\n}\n\n/** Build the Graphviz DOT for the runtime service graph. */\n// webpieces-disable no-function-outside-class -- module entry point, matching the sibling builders here\nexport function generateRuntimeDot(\n graph: RuntimeGraph,\n title: string = 'WebPieces Runtime Architecture',\n options: RuntimeVizOptions = new RuntimeVizOptions(),\n): string {\n let dot = 'digraph RuntimeArchitecture {\\n';\n dot += ' rankdir=TB;\\n';\n dot += ' node [shape=box, style=\"filled,rounded\", fontname=\"Arial\"];\\n';\n dot += ' edge [fontname=\"Arial\", fontsize=10];\\n\\n';\n\n // Services tagged drawOnGraph:false stay in the JSON but are omitted here —\n // both their node and any edge touching them are dropped from the render.\n const hidden = new Set(\n Object.keys(graph.services).filter((name: string) => graph.services[name].drawOnGraph === false)\n );\n\n for (const name of Object.keys(graph.services)) {\n if (hidden.has(name)) continue;\n const svc = graph.services[name];\n const color = LEVEL_COLORS[svc.level] || '#F5F5F5';\n dot += ` \"${dotValue(getShortName(name))}\" [fillcolor=\"${color}\", label=\"${nodeLabel(name, svc)}\"];\\n`;\n }\n\n dot += '\\n';\n\n for (const edge of graph.runtimeEdges) {\n if (hidden.has(edge.from) || hidden.has(edge.to)) continue;\n dot += edgeDot(edge, graph.queues);\n }\n\n dot += triggerDot(graph, hidden);\n\n if (options.showExternalNodes) {\n dot += externalSystemsDot(graph, hidden);\n dot += externalDot(graph, hidden);\n }\n\n dot += '\\n labelloc=\"t\";\\n';\n dot += ` label=\"${dotValue(title)}\\\\n(from architecture/runtime-dependencies.json)\";\\n`;\n dot += ' fontsize=20;\\n';\n dot += '}\\n';\n // Nothing downstream parses this DOT until a human opens the page, so parse-shape is checked\n // HERE — a graph that cannot render is a generation failure, not a blank page to discover later.\n assertValidDot(dot, 'runtime-architecture.dot');\n return dot;\n}\n\n/**\n * Inline SVG swatches for the legend, hand-drawn to match what Graphviz emits for each shape.\n *\n * Hand-drawn on purpose: the alternative is shelling out to Graphviz at generate time, which would\n * make writing the HTML depend on a `dot` binary being installed — a dependency this tool does not\n * otherwise have, since rendering happens in the browser.\n */\nclass LegendSwatches {\n readonly service =\n '<svg width=\"46\" height=\"26\"><rect x=\"1\" y=\"3\" width=\"44\" height=\"20\" rx=\"7\" fill=\"#E8F5E9\" stroke=\"#333\"/></svg>';\n /**\n * A cylinder on its side — the SAME geometry runtime-visualizer.client.js draws on the real\n * node, so the legend cannot drift from the picture it explains.\n */\n readonly queue =\n `<svg width=\"46\" height=\"26\"><path d=\"M9,5 H37 A8,8 0 0 1 37,21 H9 A8,8 0 0 1 9,5 Z\" fill=\"${QUEUE_FILL}\" stroke=\"#333\"/>` +\n '<path d=\"M9,5 A8,8 0 0 1 9,21\" fill=\"none\" stroke=\"#333\"/></svg>';\n readonly database =\n `<svg width=\"46\" height=\"26\"><path d=\"M8,7 a15,4 0 0 1 30,0 v12 a15,4 0 0 1 -30,0 z\" fill=\"${DATABASE_FILL}\" stroke=\"#333\"/>` +\n '<path d=\"M8,7 a15,4 0 0 0 30,0\" fill=\"none\" stroke=\"#333\"/></svg>';\n readonly storage =\n '<svg width=\"46\" height=\"26\"><path d=\"M2,22 V6 H16 l3,3 H44 V22 Z\" fill=\"#F3E5F5\" stroke=\"#333\"/></svg>';\n readonly external =\n `<svg width=\"46\" height=\"26\"><rect x=\"1\" y=\"3\" width=\"44\" height=\"20\" fill=\"${EXTERNAL_FILL}\" ` +\n `stroke=\"${EXTERNAL_BORDER}\" stroke-dasharray=\"4,3\"/></svg>`;\n readonly cron =\n `<svg width=\"46\" height=\"26\"><circle cx=\"23\" cy=\"13\" r=\"11\" fill=\"${CRON_FILL}\" stroke=\"${CRON_BORDER}\"/>` +\n '<text x=\"23\" y=\"18\" font-size=\"12\" text-anchor=\"middle\">⏰</text></svg>';\n readonly solid =\n '<svg width=\"60\" height=\"20\"><line x1=\"2\" y1=\"10\" x2=\"48\" y2=\"10\" stroke=\"#333\" stroke-width=\"1.5\"/>' +\n '<path d=\"M48,6 L57,10 L48,14 Z\" fill=\"#333\"/></svg>';\n readonly dashed =\n '<svg width=\"60\" height=\"20\"><line x1=\"2\" y1=\"10\" x2=\"48\" y2=\"10\" stroke=\"#333\" stroke-width=\"1.5\" ' +\n 'stroke-dasharray=\"5,4\"/><path d=\"M48,6 L57,10 L48,14 Z\" fill=\"#333\"/></svg>';\n readonly scheduled =\n `<svg width=\"60\" height=\"20\"><line x1=\"2\" y1=\"10\" x2=\"48\" y2=\"10\" stroke=\"${CRON_BORDER}\" stroke-width=\"1.5\"/>` +\n `<path d=\"M48,6 L57,10 L48,14 Z\" fill=\"${CRON_BORDER}\"/></svg>`;\n}\n\n/**\n * The legend. Three columns — what a box IS, what a line MEANS, how to read a box — replacing the\n * three paragraphs of prose that used to restate the picture in words. Styled after\n * {@link GraphVisualizer}'s legend so the two graphs in this repo look like one tool.\n */\n// webpieces-disable no-function-outside-class -- HTML builder, matching the sibling builders in this file\nfunction legendHtml(): string {\n const sw = new LegendSwatches();\n const item = (swatch: string, text: string): string =>\n `<div class=\"legend-item\"><span class=\"sw\">${swatch}</span><span>${text}</span></div>`;\n return `<div class=\"legend\">\n <h2>Legend</h2>\n <div class=\"legend-columns\">\n <div class=\"legend-col\">\n <h3>Node shapes — <em>what a box is</em></h3>\n ${item(sw.service, '<strong>service</strong> — a deployable in this repo; fill is its dependency level')}\n ${item(sw.queue, '<strong>queue</strong> — one box <em>per method</em>, the unit Cloud Tasks and Terraform actually create')}\n ${item(sw.database, '<strong>database</strong> — a datastore outside this repo')}\n ${item(sw.storage, '<strong>object storage</strong> — a bucket outside this repo')}\n ${item(sw.external, '<strong>external system</strong> — outside this repo; nothing here implements it. Pointing <strong>OUT</strong> = a system this repo calls (firestore, gmail). Pointing <strong>IN</strong> = an endpoint driven from outside (a Pub/Sub push, a Gmail or Twilio webhook).')}\n ${item(sw.cron, '<strong>cron</strong> — a scheduler fires this endpoint')}\n </div>\n <div class=\"legend-col\">\n <h3>Lines — <em>what a call is</em></h3>\n ${item(sw.solid, '<strong>solid = rpc</strong> — the request follows the arrow, the response flows back')}\n ${item(sw.dashed, '<strong>dashed = event</strong> — asynchronous: the event flows in the direction of the arrow and returns once it is in the queue')}\n ${item(sw.scheduled, '<strong>scheduled</strong> — a cron invocation')}\n <div class=\"legend-note\"><em>Every line is labeled with the contract the call flows over. A service that enqueues to itself loops through its own queue — a queue decouples the two sides, so it is not a dependency cycle.</em></div>\n </div>\n <div class=\"legend-col\">\n <h3>Reading a box</h3>\n <pre class=\"legend-box-anatomy\">name\n(server|client, L#)\nimplements: <contracts it serves>\n</pre>\n <div class=\"legend-note\">A box lists only what it <strong>serves</strong>. What it <em>calls</em> is on its outgoing arrows.</div>\n <div class=\"legend-note\"><code>(via <lib>)</code> = served through an embedded library, not its own source.</div>\n </div>\n </div>\n </div>`;\n}\n\nfunction generateRuntimeHtml(dot: string, title: string): string {\n // The browser half lives in a plain .js asset (matching graph-visualizer.client.js) rather than\n // in a template literal here: it renders with @viz-js/viz v3 AND redraws every queue node as a\n // true horizontal cylinder, which is more logic than belongs inline in a .ts string.\n const clientJs = fs.readFileSync(path.join(__dirname, 'runtime-visualizer.client.js'), 'utf-8');\n const script = clientJs.split('__DOT__').join(JSON.stringify(dot));\n return `<!DOCTYPE html>\n<html>\n<head>\n <!-- REQUIRED: the cron node's label is a literal ⏰, and the DOT is embedded in this file. With\n no declared charset the browser falls back to a locale guess and renders it as mojibake\n (\"â °\") whenever the page is served without a charset header. -->\n <meta charset=\"utf-8\">\n <title>${title}</title>\n <script src=\"https://cdn.jsdelivr.net/npm/@viz-js/viz@3.28.0/dist/viz-global.js\"></script>\n <style>\n body { margin: 0; padding: 20px; font-family: Arial, sans-serif; background: #f5f5f5; }\n h1 { text-align: center; color: #333; }\n #graph { text-align: center; background: white; padding: 20px; border-radius: 8px; overflow-x: auto; }\n #graph svg { max-width: 100%; height: auto; }\n .legend {\n margin: 20px auto;\n max-width: 1100px;\n padding: 15px 20px;\n background: white;\n border-radius: 8px;\n box-shadow: 0 2px 4px rgba(0,0,0,0.1);\n }\n .legend h2 { margin-top: 0; color: #333; }\n .legend-columns { display: grid; grid-template-columns: repeat(3, 1fr); gap: 28px; align-items: start; }\n .legend-col h3 { margin: 0 0 10px; color: #333; font-size: 15px; border-bottom: 1px solid #eee; padding-bottom: 5px; }\n .legend-item { margin: 9px 0; display: flex; align-items: center; gap: 10px; line-height: 1.4; color: #444; }\n /* Prose rows carry no swatch, so they must NOT be flex containers: flex would promote every\n * inline <strong>/<em>/<code> to a flex item and shred the sentence into columns. */\n .legend-note { margin: 9px 0; line-height: 1.5; color: #444; }\n .legend-box-anatomy {\n margin: 0 0 12px;\n padding: 8px 10px;\n background: #f7f7f7;\n border-radius: 4px;\n font-family: monospace;\n font-size: 12px;\n line-height: 1.5;\n color: #333;\n white-space: pre;\n overflow-x: auto;\n }\n .sw { flex: 0 0 auto; display: inline-flex; }\n code { background: #f2f2f2; padding: 1px 4px; border-radius: 3px; font-family: monospace; }\n @media (max-width: 900px) { .legend-columns { grid-template-columns: 1fr; } }\n </style>\n</head>\n<body>\n <h1>${title}</h1>\n <div id=\"graph\"></div>\n ${legendHtml()}\n <script>${script}</script>\n</body>\n</html>`;\n}\n\nexport interface RuntimeVisualizationPaths {\n dotPath: string;\n htmlPath: string;\n}\n\n/** Write the DOT + HTML renderings to tmp/webpieces/. */\nexport function writeRuntimeVisualization(\n graph: RuntimeGraph,\n workspaceRoot: string,\n title: string = 'WebPieces Runtime Architecture',\n options: RuntimeVizOptions = new RuntimeVizOptions(),\n): RuntimeVisualizationPaths {\n const outputDir = path.join(workspaceRoot, 'tmp', 'webpieces');\n if (!fs.existsSync(outputDir)) fs.mkdirSync(outputDir, { recursive: true });\n\n const dot = generateRuntimeDot(graph, title, options);\n const dotPath = path.join(outputDir, 'runtime-architecture.dot');\n fs.writeFileSync(dotPath, dot, 'utf-8');\n\n const htmlPath = path.join(outputDir, 'runtime-architecture.html');\n fs.writeFileSync(htmlPath, generateRuntimeHtml(dot, title), 'utf-8');\n\n return { dotPath, htmlPath };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"runtime-visualizer.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/runtime-visualizer.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Runtime Visualizer\n *\n * Renders the runtime microservice graph (services + inferred Z -> X edges,\n * each labeled with the api(s) they flow over) to DOT + interactive HTML in\n * tmp/webpieces/runtime-architecture.{dot,html}.\n *\n * Each service node names the contracts it IMPLEMENTS. That list used to be\n * collapsed into a server/client boolean and thrown away — leaving an api that a\n * server serves but nothing in-repo calls completely invisible, and making a\n * correct api design look like a detection failure. What a service USES is NOT\n * listed: every use already draws an outgoing arrow labeled with the same\n * contract, so repeating it in the box only made every box wider.\n *\n * Shape says what a node IS and line style says what a call IS. Solid = rpc: the\n * request follows the arrow and the response flows back. Dashed = event: it flows\n * in the arrow's direction and returns once it is queued. A queue is a sideways\n * cylinder, a datastore an upright one.\n *\n * Calls that leave the repo (a contract NOTHING in-repo implements — firestore,\n * gmail, ...) are drawn as terminal nodes, so the vendor systems that actually\n * page you at 3am stop being missing from the picture. One that DECLARES what it\n * is, via an `@externalSystem` JSDoc tag or an `external:` nx tag, gets the shape\n * of the thing it is; the rest stay generic dashed boxes. They are RENDER-ONLY:\n * derivation, levels and cycle detection never see them.\n *\n * The same is true in the other direction for endpoints nothing in-repo CALLS: a\n * `cron` method hangs off a clock and an `external` method off a dashed inbound\n * box. Those are the entry points that wake a service up at 3am, and a graph\n * built only from in-repo callers cannot show them at all.\n */\n\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport type { RuntimeGraph, RuntimeEdge, RuntimeQueue, RuntimeService, RuntimeTrigger } from './runtime-graph';\nimport { dotValue, recordValue, assertValidDot } from './dot-syntax';\n\nconst LEVEL_COLORS: Record<number, string> = {\n 0: '#E8F5E9',\n 1: '#E3F2FD',\n 2: '#FFF3E0',\n 3: '#FCE4EC',\n};\n\nconst QUEUE_FILL = '#FFF3E0';\n\n/**\n * The queue node is an `Mrecord` whose FIRST field is empty, which draws a rounded outline with a\n * vertical cap line near one end — a cylinder lying on its side, distinguishing a queue from the\n * upright cylinder that now means a database.\n *\n * Two things here are load-bearing and easy to break:\n *\n * 1. NO surrounding `{}`. Record fields lay out along the rank direction, and this graph is\n * `rankdir=TB` (see {@link generateRuntimeDot}), where the default is horizontal — which is what\n * we want. Adding braces TOGGLES that, turning the cap line into a band across the top.\n * 2. The leading space is the empty field. The record parser trims it to nothing, which is the\n * point; it must survive as its own field, so the `|` cannot be dropped.\n *\n * Graphviz has no sideways cylinder and never has: `orientation=` is documented as rotating POLYGON\n * shapes, and `cylinder` is drawn with beziers, so it silently ignores the attribute (graphviz issue\n * #2244, open since 2022 and still reproducible on 13.0.0). This is the closest native shape.\n */\nconst QUEUE_SHAPE = 'Mrecord';\nconst QUEUE_LABEL_PREFIX = ' |';\n\n/**\n * Marker class stamped on every queue node. Graphviz copies `class` straight into the rendered\n * `<g class=\"node wp_queue\">`, which is how runtime-visualizer.client.js finds these nodes and\n * redraws them as true horizontal cylinders in the browser.\n *\n * A CLASS rather than an id prefix, because queue-kind EXTERNAL systems are queues too and share the\n * `system__` id space with databases — which must stay upright. Underscored, not hyphenated: DOT\n * emits a hyphen as `-`, which is harmless but needlessly surprising to anyone reading the SVG.\n */\nconst QUEUE_CLASS = 'wp_queue';\n\n/** Fill for the upright cylinder standing for an external DATASTORE (firestore, postgres, ...). */\nconst DATABASE_FILL = '#E1F5FE';\n\n/** Shape per external-system kind. Anything unrecognised falls back to the generic dashed box. */\nconst EXTERNAL_SHAPES: Record<string, string> = {\n database: 'cylinder',\n cache: 'cylinder',\n queue: 'Mrecord',\n storage: 'folder',\n};\n\n/** Fill per external-system kind, paired with {@link EXTERNAL_SHAPES}. */\nconst EXTERNAL_FILLS: Record<string, string> = {\n database: DATABASE_FILL,\n cache: DATABASE_FILL,\n queue: QUEUE_FILL,\n storage: '#F3E5F5',\n};\n\n/** Fill + border for the dashed terminal node standing for a system outside this repo. */\nconst EXTERNAL_FILL = '#FAFAFA';\nconst EXTERNAL_BORDER = '#9E9E9E';\n\n/** Fill + border for the clock node standing for a scheduler-driven endpoint. */\nconst CRON_FILL = '#FFF9C4';\nconst CRON_BORDER = '#F9A825';\n\n/** Apis per line inside a node label — beyond this the box grows wider than it is readable. */\nconst APIS_PER_LABEL_LINE = 3;\n\n/** Separator for the (service, external-library) grouping key; illegal in both project names. */\nconst PAIR_SEP = '|';\n\n/** Render options for the runtime graph. */\nexport class RuntimeVizOptions {\n constructor(\n /**\n * Draw the dashed terminal nodes for contracts nothing in-repo implements. On by default;\n * a repo whose external surface is noisy can turn them off in webpieces.config.json\n * (runtime-architecture.showExternalNodes).\n */\n public readonly showExternalNodes: boolean = true,\n ) {}\n}\n\nfunction getShortName(name: string): string {\n return name.includes('/') ? name.split('/').pop()! : name;\n}\n\n/** Chunk a list into `\\n`-separated label lines of at most APIS_PER_LABEL_LINE entries. */\n// webpieces-disable no-function-outside-class -- DOT label builder, matching getShortName in this file\nfunction labelList(entries: string[]): string {\n const lines: string[] = [];\n const safe = entries.map((entry: string) => dotValue(entry));\n for (let i = 0; i < safe.length; i += APIS_PER_LABEL_LINE) {\n lines.push(safe.slice(i, i + APIS_PER_LABEL_LINE).join(', '));\n }\n return lines.join('\\\\n');\n}\n\n/**\n * The implemented-api entries for a node label. An api served through an EMBEDDED LIBRARY is\n * annotated with that library, because \"who implements WarmupApi?\" otherwise requires knowing that\n * the derivation walks the dependsOn closure and then walking it by hand.\n */\n// webpieces-disable no-function-outside-class -- DOT label builder, matching getShortName in this file\nfunction implementsEntries(svc: RuntimeService): string[] {\n return svc.implements.map((api: string) => {\n const via = svc.implementsVia?.[api];\n return via === undefined ? api : `${api} (via ${getShortName(via)})`;\n });\n}\n\n/**\n * The full node label: name, role/level/declared service name, then the contracts it SERVES.\n *\n * What a service USES is deliberately absent. Every `uses` entry already draws an outgoing arrow —\n * to the implementing service, to a queue, or to an external node — and the arrow carries the same\n * contract name as its label, so listing them in the box restated the picture and made every box\n * wider than it needed to be. `implements` stays because it has no such arrow: an api a service\n * serves but nothing in-repo calls is invisible otherwise.\n *\n * The one case this loses information is `showExternalNodes:false`, where an outbound call draws no\n * node and therefore no arrow. That is precisely what that opt-out asks for, and the legend says so.\n */\n// webpieces-disable no-function-outside-class -- DOT label builder, matching getShortName in this file\nfunction nodeLabel(name: string, svc: RuntimeService): string {\n // The DECLARED role when the graph carries one; the old inference only as a fallback for a\n // runtime-dependencies.json committed before `role` existed. Inferring it labelled every server\n // with no implements as a \"client\", which is exactly wrong for a queue-driven service.\n const role = svc.role ?? (svc.implements.length > 0 ? 'server' : 'client');\n // The declared name is quoted for the reader — those quotes MUST be DOT-escaped, or they end\n // the label string and the whole graph stops parsing.\n const declared = svc.serviceName === undefined ? '' : `, \\\\\"${dotValue(svc.serviceName)}\\\\\"`;\n let label = `${dotValue(getShortName(name))}\\\\n(${role}, L${svc.level}${declared})`;\n if (svc.implements.length > 0) label += `\\\\nimplements: ${labelList(implementsEntries(svc))}`;\n return label;\n}\n\n/**\n * DOT for ONE runtime edge. rpc → a direct labeled SOLID arrow (producer calls consumer, response\n * comes back). pubsub → the producer enqueues and the consumer is delivered later, so we draw\n * producer → QUEUE → consumer through a sideways-cylinder queue node with DASHED arrows.\n *\n * Solid vs dashed is the graph's one line-level distinction: solid is a call that returns a\n * response, dashed is an event that returns as soon as it is queued.\n */\n// webpieces-disable no-function-outside-class -- DOT string builder, matching getShortName in this file\nfunction edgeDot(edge: RuntimeEdge, queues: Record<string, RuntimeQueue>): string {\n const from = dotValue(getShortName(edge.from));\n const to = dotValue(getShortName(edge.to));\n // Kept RAW: an ordinary edge label needs dotValue, the record-mode queue label needs\n // recordValue, and recordValue already applies dotValue — escaping here would double it.\n const viaRaw = edge.via.map((v: string) => getShortName(v)).join(', ');\n if (edge.type !== 'pubsub') {\n return ` \"${from}\" -> \"${to}\" [label=\"${dotValue(viaRaw)}\"];\\n`;\n }\n // The queue node is identified by the METHOD, not by the (from,to) pair, so every producer and\n // consumer of one queue converges on ONE box — including a service that enqueues to itself,\n // which then renders as a visible loop through its queue instead of vanishing.\n const queueId = edge.queue === undefined ? `queue__${from}__${to}` : `queue__${dotId(edge.queue)}`;\n const queueName = edge.queue === undefined ? undefined : queues[edge.queue]?.queueName;\n // Record-mode label: the text must clear recordValue(), and QUEUE_LABEL_PREFIX supplies the\n // empty leading field that draws the cylinder's end cap.\n const body =\n edge.queue === undefined\n ? `${recordValue(viaRaw)}\\\\nqueue`\n : `${recordValue(edge.queue)}\\\\nqueue: ${recordValue(queueName ?? edge.queue)}`;\n return (\n ` \"${queueId}\" [shape=${QUEUE_SHAPE}, style=\"filled\", fillcolor=\"${QUEUE_FILL}\", ` +\n `class=\"${QUEUE_CLASS}\", label=\"${QUEUE_LABEL_PREFIX}${body}\"];\\n` +\n ` \"${from}\" -> \"${queueId}\" [label=\"enqueue\", style=dashed];\\n` +\n ` \"${queueId}\" -> \"${to}\" [label=\"deliver\", style=dashed];\\n`\n );\n}\n\n/** A DOT-safe node-id fragment: anything but letters, digits and `_` becomes `_`. */\n// webpieces-disable no-function-outside-class -- DOT id builder, matching getShortName in this file\nfunction dotId(raw: string): string {\n return raw.replace(/[^A-Za-z0-9_]/g, '_');\n}\n\n/**\n * The clock and outside-system nodes for endpoints NOTHING in-repo calls.\n *\n * A cron sweep and a GCP push subscription are real runtime entry points with real Terraform behind\n * them, but they produce no runtime EDGE (there is no in-repo caller), so until now they were simply\n * absent — a server's most operationally interesting endpoint could be invisible on its own graph.\n * Both are drawn pointing INTO the service that serves them, the opposite direction from\n * {@link externalDot}'s outbound vendor calls.\n */\n// webpieces-disable no-function-outside-class -- DOT string builder, matching getShortName in this file\nfunction triggerDot(graph: RuntimeGraph, hidden: Set<string>): string {\n const triggers = graph.triggers.filter((t: RuntimeTrigger) => !hidden.has(t.service));\n if (triggers.length === 0) return '';\n\n let dot = '\\n // Entry points nothing in this repo calls: a clock, or a system outside the repo.\\n';\n for (const trigger of triggers) {\n const service = dotValue(getShortName(trigger.service));\n const label = dotValue(`${trigger.api}.${trigger.method}`);\n if (trigger.kind === 'cron') {\n const id = `cron__${dotId(`${trigger.api}_${trigger.method}`)}`;\n const schedule = dotValue(trigger.queueName ?? `${trigger.api}-${trigger.method}`);\n dot +=\n ` \"${id}\" [shape=circle, style=\"filled\", fillcolor=\"${CRON_FILL}\", ` +\n `color=\"${CRON_BORDER}\", label=\"⏰\\\\ncron\"];\\n` +\n ` \"${id}\" -> \"${service}\" [label=\"${label}\\\\n${schedule}\", color=\"${CRON_BORDER}\"];\\n`;\n continue;\n }\n const id = `inbound__${dotId(trigger.api)}`;\n dot +=\n ` \"${id}\" [shape=box, style=\"dashed,filled\", fillcolor=\"${EXTERNAL_FILL}\", ` +\n `color=\"${EXTERNAL_BORDER}\", label=\"${dotValue(trigger.api)}\\\\n(external caller)\"];\\n` +\n ` \"${id}\" -> \"${service}\" [label=\"${label}\", style=dashed, color=\"${EXTERNAL_BORDER}\"];\\n`;\n }\n return dot;\n}\n\n/**\n * The DECLARED external systems — the ones that said what they are, so they get a shape that says\n * it: a cylinder for a database, a folder for a bucket. Everything undeclared falls through to\n * {@link externalDot}'s generic grey box, which is why adding this broke nothing existing.\n *\n * The arrows are SOLID. A call to firestore or postgres is synchronous — it returns a value — and\n * the graph's rule is that solid means \"response comes back\". Being outside the repo is carried by\n * the node's shape, never by the line style; conflating the two is what made a blocking database\n * read look like an event.\n */\n// webpieces-disable no-function-outside-class -- DOT string builder, matching getShortName in this file\nfunction externalSystemsDot(graph: RuntimeGraph, hidden: Set<string>): string {\n const systems = graph.externalSystems ?? {};\n const ids = Object.keys(systems).sort();\n if (ids.length === 0) return '';\n\n let dot = '\\n // Declared external systems — drawn with the shape of what they actually are.\\n';\n for (const id of ids) {\n const system = systems[id];\n const shape = EXTERNAL_SHAPES[system.kind] ?? 'box';\n const fill = EXTERNAL_FILLS[system.kind] ?? EXTERNAL_FILL;\n // An Mrecord-shaped system needs the same empty leading field as a queue node, or it\n // renders as a plain box and silently loses the sideways-cylinder read.\n const isQueue = shape === QUEUE_SHAPE;\n const prefix = isQueue ? QUEUE_LABEL_PREFIX : '';\n const text = isQueue ? recordValue(system.label) : dotValue(system.label);\n // Only a queue-kind system is marked: a database here is an UPRIGHT cylinder and must not be\n // caught by the browser-side reshaping that lays queues on their side.\n const marker = isQueue ? `class=\"${QUEUE_CLASS}\", ` : '';\n dot +=\n ` \"system__${dotId(id)}\" [shape=${shape}, style=\"filled\", fillcolor=\"${fill}\", ` +\n `${marker}label=\"${prefix}${text}\\\\n(external ${dotValue(system.kind)})\"];\\n`;\n }\n for (const id of ids) {\n const system = systems[id];\n const via = system.apis.length === 0 ? '' : ` [label=\"${labelList([...system.apis].sort())}\"]`;\n for (const service of [...system.usedBy].sort()) {\n if (hidden.has(service)) continue;\n dot += ` \"${dotValue(getShortName(service))}\" -> \"system__${dotId(id)}\"${via};\\n`;\n }\n }\n return dot;\n}\n\n/**\n * The dashed terminal nodes + edges for calls that LEAVE the repo. Built from `unresolvedUses` —\n * a contract used by a node and implemented by nobody in-repo — which the derivation already\n * computes and which was, until now, only ever printed as a warning.\n *\n * Grouped by the api-lib that owns the contracts, so a service reaching three firestore contracts\n * draws ONE `lib-firestore (external)` box rather than three. These are drawn, never derived: they\n * are absent from levels, cycle detection and the transitive implements attribution.\n *\n * A contract carrying an `@externalSystem` declaration is skipped here — {@link externalSystemsDot}\n * has already drawn it with a real shape, and rendering it in both places would double the node.\n */\n// webpieces-disable no-function-outside-class -- DOT string builder, matching getShortName in this file\nfunction externalDot(graph: RuntimeGraph, hidden: Set<string>): string {\n // \"service|externalName\" -> the apis flowing over it.\n const apisByPair = new Map<string, string[]>();\n for (const use of graph.unresolvedUses) {\n if (hidden.has(use.service)) continue;\n if (graph.apis[use.api]?.externalSystem !== undefined) continue;\n const external = dotValue(getShortName(graph.apis[use.api]?.owner ?? use.api));\n const key = `${use.service}${PAIR_SEP}${external}`;\n if (!apisByPair.has(key)) apisByPair.set(key, []);\n apisByPair.get(key)!.push(use.api);\n }\n if (apisByPair.size === 0) return '';\n\n let dot = '\\n // Systems outside this repo — no in-repo service implements these contracts.\\n';\n // The node ID is prefixed so an external library can never collide with a service of the same\n // short name; only the label carries the bare name.\n const externals = new Set([...apisByPair.keys()].map((key: string) => key.split(PAIR_SEP)[1]));\n for (const external of [...externals].sort()) {\n dot +=\n ` \"external__${external}\" [shape=box, style=\"dashed,filled\", fillcolor=\"${EXTERNAL_FILL}\", ` +\n `color=\"${EXTERNAL_BORDER}\", label=\"${external}\\\\n(external)\"];\\n`;\n }\n for (const key of [...apisByPair.keys()].sort()) {\n const parts = key.split(PAIR_SEP);\n const service = parts[0];\n const external = parts[1];\n const via = labelList(apisByPair.get(key)!.sort());\n // SOLID: this is a synchronous call that returns a value. Dashed is reserved for events, and\n // \"outside the repo\" is already said by the node's dashed border.\n dot +=\n ` \"${dotValue(getShortName(service))}\" -> \"external__${external}\" ` +\n `[label=\"${via}\", color=\"${EXTERNAL_BORDER}\"];\\n`;\n }\n return dot;\n}\n\n/** Build the Graphviz DOT for the runtime service graph. */\n// webpieces-disable no-function-outside-class -- module entry point, matching the sibling builders here\nexport function generateRuntimeDot(\n graph: RuntimeGraph,\n title: string = 'WebPieces Runtime Architecture',\n options: RuntimeVizOptions = new RuntimeVizOptions(),\n): string {\n let dot = 'digraph RuntimeArchitecture {\\n';\n dot += ' rankdir=TB;\\n';\n dot += ' node [shape=box, style=\"filled,rounded\", fontname=\"Arial\"];\\n';\n dot += ' edge [fontname=\"Arial\", fontsize=10];\\n\\n';\n\n // Services tagged drawOnGraph:false stay in the JSON but are omitted here —\n // both their node and any edge touching them are dropped from the render.\n const hidden = new Set(\n Object.keys(graph.services).filter((name: string) => graph.services[name].drawOnGraph === false)\n );\n\n for (const name of Object.keys(graph.services)) {\n if (hidden.has(name)) continue;\n const svc = graph.services[name];\n const color = LEVEL_COLORS[svc.level] || '#F5F5F5';\n dot += ` \"${dotValue(getShortName(name))}\" [fillcolor=\"${color}\", label=\"${nodeLabel(name, svc)}\"];\\n`;\n }\n\n dot += '\\n';\n\n for (const edge of graph.runtimeEdges) {\n if (hidden.has(edge.from) || hidden.has(edge.to)) continue;\n dot += edgeDot(edge, graph.queues);\n }\n\n dot += triggerDot(graph, hidden);\n\n if (options.showExternalNodes) {\n dot += externalSystemsDot(graph, hidden);\n dot += externalDot(graph, hidden);\n }\n\n dot += '\\n labelloc=\"t\";\\n';\n dot += ` label=\"${dotValue(title)}\\\\n(from architecture/runtime-dependencies.json)\";\\n`;\n dot += ' fontsize=20;\\n';\n dot += '}\\n';\n // Nothing downstream parses this DOT until a human opens the page, so parse-shape is checked\n // HERE — a graph that cannot render is a generation failure, not a blank page to discover later.\n assertValidDot(dot, 'runtime-architecture.dot');\n return dot;\n}\n\n/**\n * Inline SVG swatches for the legend, hand-drawn to match what Graphviz emits for each shape.\n *\n * Hand-drawn on purpose: the alternative is shelling out to Graphviz at generate time, which would\n * make writing the HTML depend on a `dot` binary being installed — a dependency this tool does not\n * otherwise have, since rendering happens in the browser.\n */\nclass LegendSwatches {\n readonly service =\n '<svg width=\"46\" height=\"26\"><rect x=\"1\" y=\"3\" width=\"44\" height=\"20\" rx=\"7\" fill=\"#E8F5E9\" stroke=\"#333\"/></svg>';\n /**\n * A cylinder on its side — the SAME geometry runtime-visualizer.client.js draws on the real\n * node, so the legend cannot drift from the picture it explains.\n */\n readonly queue =\n `<svg width=\"46\" height=\"26\"><path d=\"M9,5 H37 A8,8 0 0 1 37,21 H9 A8,8 0 0 1 9,5 Z\" fill=\"${QUEUE_FILL}\" stroke=\"#333\"/>` +\n '<path d=\"M9,5 A8,8 0 0 1 9,21\" fill=\"none\" stroke=\"#333\"/></svg>';\n readonly database =\n `<svg width=\"46\" height=\"26\"><path d=\"M8,7 a15,4 0 0 1 30,0 v12 a15,4 0 0 1 -30,0 z\" fill=\"${DATABASE_FILL}\" stroke=\"#333\"/>` +\n '<path d=\"M8,7 a15,4 0 0 0 30,0\" fill=\"none\" stroke=\"#333\"/></svg>';\n readonly storage =\n '<svg width=\"46\" height=\"26\"><path d=\"M2,22 V6 H16 l3,3 H44 V22 Z\" fill=\"#F3E5F5\" stroke=\"#333\"/></svg>';\n readonly external =\n `<svg width=\"46\" height=\"26\"><rect x=\"1\" y=\"3\" width=\"44\" height=\"20\" fill=\"${EXTERNAL_FILL}\" ` +\n `stroke=\"${EXTERNAL_BORDER}\" stroke-dasharray=\"4,3\"/></svg>`;\n readonly cron =\n `<svg width=\"46\" height=\"26\"><circle cx=\"23\" cy=\"13\" r=\"11\" fill=\"${CRON_FILL}\" stroke=\"${CRON_BORDER}\"/>` +\n '<text x=\"23\" y=\"18\" font-size=\"12\" text-anchor=\"middle\">⏰</text></svg>';\n readonly solid =\n '<svg width=\"60\" height=\"20\"><line x1=\"2\" y1=\"10\" x2=\"48\" y2=\"10\" stroke=\"#333\" stroke-width=\"1.5\"/>' +\n '<path d=\"M48,6 L57,10 L48,14 Z\" fill=\"#333\"/></svg>';\n readonly dashed =\n '<svg width=\"60\" height=\"20\"><line x1=\"2\" y1=\"10\" x2=\"48\" y2=\"10\" stroke=\"#333\" stroke-width=\"1.5\" ' +\n 'stroke-dasharray=\"5,4\"/><path d=\"M48,6 L57,10 L48,14 Z\" fill=\"#333\"/></svg>';\n readonly scheduled =\n `<svg width=\"60\" height=\"20\"><line x1=\"2\" y1=\"10\" x2=\"48\" y2=\"10\" stroke=\"${CRON_BORDER}\" stroke-width=\"1.5\"/>` +\n `<path d=\"M48,6 L57,10 L48,14 Z\" fill=\"${CRON_BORDER}\"/></svg>`;\n}\n\n/**\n * The legend. Three columns — what a box IS, what a line MEANS, how to read a box — replacing the\n * three paragraphs of prose that used to restate the picture in words. Styled after\n * {@link GraphVisualizer}'s legend so the two graphs in this repo look like one tool.\n */\n// webpieces-disable no-function-outside-class -- HTML builder, matching the sibling builders in this file\nfunction legendHtml(): string {\n const sw = new LegendSwatches();\n const item = (swatch: string, text: string): string =>\n `<div class=\"legend-item\"><span class=\"sw\">${swatch}</span><span>${text}</span></div>`;\n return `<div class=\"legend\">\n <h2>Legend</h2>\n <div class=\"legend-columns\">\n <div class=\"legend-col\">\n <h3>Node shapes — <em>what a box is</em></h3>\n ${item(sw.service, '<strong>service</strong> — a deployable in this repo; fill is its dependency level')}\n ${item(sw.queue, '<strong>queue</strong> — one box <em>per method</em>, the unit Cloud Tasks and Terraform actually create')}\n ${item(sw.database, '<strong>database</strong> — a datastore outside this repo')}\n ${item(sw.storage, '<strong>object storage</strong> — a bucket outside this repo')}\n ${item(sw.external, '<strong>external system</strong> — outside this repo; nothing here implements it. Pointing <strong>OUT</strong> = a system this repo calls (firestore, gmail). Pointing <strong>IN</strong> = an endpoint driven from outside (a Pub/Sub push, a Gmail or Twilio webhook).')}\n ${item(sw.cron, '<strong>cron</strong> — a scheduler fires this endpoint')}\n </div>\n <div class=\"legend-col\">\n <h3>Lines — <em>what a call is</em></h3>\n ${item(sw.solid, '<strong>solid = rpc</strong> — the request follows the arrow, the response flows back')}\n ${item(sw.dashed, '<strong>dashed = event</strong> — asynchronous: the event flows in the direction of the arrow and returns once it is in the queue')}\n ${item(sw.scheduled, '<strong>scheduled</strong> — a cron invocation')}\n <div class=\"legend-note\"><em>Every line is labeled with the contract the call flows over. A service that enqueues to itself loops through its own queue — a queue decouples the two sides, so it is not a dependency cycle.</em></div>\n </div>\n <div class=\"legend-col\">\n <h3>Reading a box</h3>\n <pre class=\"legend-box-anatomy\">name\n(server|client, L#)\nimplements: <contracts it serves>\n</pre>\n <div class=\"legend-note\">A box lists only what it <strong>serves</strong>. What it <em>calls</em> is on its outgoing arrows.</div>\n <div class=\"legend-note\"><code>(via <lib>)</code> = served through an embedded library, not its own source.</div>\n </div>\n </div>\n </div>`;\n}\n\nfunction generateRuntimeHtml(dot: string, title: string): string {\n // The browser half lives in a plain .js asset (matching graph-visualizer.client.js) rather than\n // in a template literal here: it renders with @viz-js/viz v3 AND redraws every queue node as a\n // true horizontal cylinder, which is more logic than belongs inline in a .ts string.\n const clientJs = fs.readFileSync(path.join(__dirname, 'runtime-visualizer.client.js'), 'utf-8');\n const script = clientJs.split('__DOT__').join(JSON.stringify(dot));\n return `<!DOCTYPE html>\n<html>\n<head>\n <!-- REQUIRED: the cron node's label is a literal ⏰, and the DOT is embedded in this file. With\n no declared charset the browser falls back to a locale guess and renders it as mojibake\n (\"â °\") whenever the page is served without a charset header. -->\n <meta charset=\"utf-8\">\n <title>${title}</title>\n <script src=\"https://cdn.jsdelivr.net/npm/@viz-js/viz@3.28.0/dist/viz-global.js\"></script>\n <style>\n body { margin: 0; padding: 20px; font-family: Arial, sans-serif; background: #f5f5f5; }\n h1 { text-align: center; color: #333; }\n #graph { text-align: center; background: white; padding: 20px; border-radius: 8px; overflow-x: auto; }\n #graph svg { max-width: 100%; height: auto; }\n .legend {\n margin: 20px auto;\n max-width: 1100px;\n padding: 15px 20px;\n background: white;\n border-radius: 8px;\n box-shadow: 0 2px 4px rgba(0,0,0,0.1);\n }\n .legend h2 { margin-top: 0; color: #333; }\n .legend-columns { display: grid; grid-template-columns: repeat(3, 1fr); gap: 28px; align-items: start; }\n .legend-col h3 { margin: 0 0 10px; color: #333; font-size: 15px; border-bottom: 1px solid #eee; padding-bottom: 5px; }\n .legend-item { margin: 9px 0; display: flex; align-items: center; gap: 10px; line-height: 1.4; color: #444; }\n /* Prose rows carry no swatch, so they must NOT be flex containers: flex would promote every\n * inline <strong>/<em>/<code> to a flex item and shred the sentence into columns. */\n .legend-note { margin: 9px 0; line-height: 1.5; color: #444; }\n .legend-box-anatomy {\n margin: 0 0 12px;\n padding: 8px 10px;\n background: #f7f7f7;\n border-radius: 4px;\n font-family: monospace;\n font-size: 12px;\n line-height: 1.5;\n color: #333;\n white-space: pre;\n overflow-x: auto;\n }\n .sw { flex: 0 0 auto; display: inline-flex; }\n code { background: #f2f2f2; padding: 1px 4px; border-radius: 3px; font-family: monospace; }\n @media (max-width: 900px) { .legend-columns { grid-template-columns: 1fr; } }\n </style>\n</head>\n<body>\n <h1>${title}</h1>\n <div id=\"graph\"></div>\n ${legendHtml()}\n <script>${script}</script>\n</body>\n</html>`;\n}\n\nexport interface RuntimeVisualizationPaths {\n dotPath: string;\n htmlPath: string;\n}\n\n/** Write the DOT + HTML renderings to tmp/webpieces/. */\nexport function writeRuntimeVisualization(\n graph: RuntimeGraph,\n workspaceRoot: string,\n title: string = 'WebPieces Runtime Architecture',\n options: RuntimeVizOptions = new RuntimeVizOptions(),\n): RuntimeVisualizationPaths {\n const outputDir = path.join(workspaceRoot, 'tmp', 'webpieces');\n if (!fs.existsSync(outputDir)) fs.mkdirSync(outputDir, { recursive: true });\n\n const dot = generateRuntimeDot(graph, title, options);\n const dotPath = path.join(outputDir, 'runtime-architecture.dot');\n fs.writeFileSync(dotPath, dot, 'utf-8');\n\n const htmlPath = path.join(outputDir, 'runtime-architecture.html');\n fs.writeFileSync(htmlPath, generateRuntimeHtml(dot, title), 'utf-8');\n\n return { dotPath, htmlPath };\n}\n"]}
|