@webpieces/nx-webpieces-rules 0.4.892 → 0.4.894
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 +7 -7
- package/src/executors/generate/executor.js +11 -7
- package/src/executors/generate/executor.js.map +1 -1
- package/src/executors/validate-architecture-unchanged/executor.d.ts +4 -2
- package/src/executors/validate-architecture-unchanged/executor.js +32 -5
- package/src/executors/validate-architecture-unchanged/executor.js.map +1 -1
- package/src/executors/validate-runtime-architecture/executor.js +1 -1
- package/src/executors/validate-runtime-architecture/executor.js.map +1 -1
- package/src/lib/graph-loader.d.ts +16 -2
- package/src/lib/graph-loader.js +34 -4
- package/src/lib/graph-loader.js.map +1 -1
- package/src/lib/remote-service-node-id.d.ts +8 -0
- package/src/lib/remote-service-node-id.js +15 -0
- package/src/lib/remote-service-node-id.js.map +1 -0
- package/src/lib/remote-services.d.ts +53 -0
- package/src/lib/remote-services.js +123 -0
- package/src/lib/remote-services.js.map +1 -0
- package/src/lib/runtime-details.d.ts +7 -0
- package/src/lib/runtime-details.js +27 -0
- package/src/lib/runtime-details.js.map +1 -1
- package/src/lib/runtime-graph-model.d.ts +28 -0
- package/src/lib/runtime-graph-model.js.map +1 -1
- package/src/lib/runtime-graph.d.ts +3 -2
- package/src/lib/runtime-graph.js +15 -3
- package/src/lib/runtime-graph.js.map +1 -1
- package/src/lib/runtime-remote-calls.d.ts +35 -0
- package/src/lib/runtime-remote-calls.js +87 -0
- package/src/lib/runtime-remote-calls.js.map +1 -0
- package/src/lib/runtime-visualizer.js +41 -0
- package/src/lib/runtime-visualizer.js.map +1 -1
- package/src/lib/runtime-viz-theme.d.ts +7 -0
- package/src/lib/runtime-viz-theme.js +11 -1
- package/src/lib/runtime-viz-theme.js.map +1 -1
- package/src/lib/runtime-wiring/approved-graph.js +2 -29
- package/src/lib/runtime-wiring/approved-graph.js.map +1 -1
- package/src/lib/runtime-wiring/service-targets.d.ts +22 -0
- package/src/lib/runtime-wiring/service-targets.js +95 -0
- package/src/lib/runtime-wiring/service-targets.js.map +1 -0
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Remote service calls in the runtime graph (#1203)
|
|
4
|
+
*
|
|
5
|
+
* A `uses` whose named target (the call-site `targetService`, else the caller's `callsService`) is a
|
|
6
|
+
* REVIEWED remote service — declared in dependencies.json `remoteServices`, outside this workspace —
|
|
7
|
+
* is claimed here instead of being resolved against the workspace's own nodes. Without this the call
|
|
8
|
+
* either decayed into an "unresolved" vendor box (nothing in-repo implements the contract) or failed as
|
|
9
|
+
* a name no module answers to (something in-repo does).
|
|
10
|
+
*
|
|
11
|
+
* Workspace names always win: a remote name that collides with a node is reported as a problem and
|
|
12
|
+
* never claims the call, so a stray declaration cannot redirect a call away from a real module.
|
|
13
|
+
*/
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.RemoteCallCollector = void 0;
|
|
16
|
+
class RemoteCallCollector {
|
|
17
|
+
remotes;
|
|
18
|
+
isWorkspaceName;
|
|
19
|
+
problems;
|
|
20
|
+
/** remote name -> "caller transport" -> contracts called there. */
|
|
21
|
+
calls = new Map();
|
|
22
|
+
constructor(remotes,
|
|
23
|
+
/** True when a workspace runtime node answers to the name. */
|
|
24
|
+
isWorkspaceName, problems) {
|
|
25
|
+
this.remotes = remotes;
|
|
26
|
+
this.isWorkspaceName = isWorkspaceName;
|
|
27
|
+
this.problems = problems;
|
|
28
|
+
for (const name of Object.keys(remotes).sort())
|
|
29
|
+
if (isWorkspaceName(name))
|
|
30
|
+
problems.push(`remoteServices declares '${name}', but a workspace module already answers to that name — a remote ` +
|
|
31
|
+
`service must live OUTSIDE this workspace. Remove the remoteServices entry or rename one of them.`);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* True when `wanted` names a declared remote service, which then owns this use. A remote that does
|
|
35
|
+
* not declare the contract + transport is a problem; the call is still drawn to it, so the picture
|
|
36
|
+
* shows exactly what the declaration is missing.
|
|
37
|
+
*/
|
|
38
|
+
claim(user, ref, wanted) {
|
|
39
|
+
if (wanted === undefined)
|
|
40
|
+
return false;
|
|
41
|
+
const remote = this.remotes[wanted];
|
|
42
|
+
if (remote === undefined || this.isWorkspaceName(wanted))
|
|
43
|
+
return false;
|
|
44
|
+
if (!remote.implementsApi(ref.api, ref.type))
|
|
45
|
+
this.problems.push(`${user} calls "${ref.api}" (${ref.type}) at remote service '${wanted}', but remoteServices.${wanted}.implements ` +
|
|
46
|
+
`does not declare it. Add { "api": "${ref.api}", "type": "${ref.type}" } after review, or fix the target.`);
|
|
47
|
+
let byCaller = this.calls.get(wanted);
|
|
48
|
+
if (byCaller === undefined) {
|
|
49
|
+
byCaller = new Map();
|
|
50
|
+
this.calls.set(wanted, byCaller);
|
|
51
|
+
}
|
|
52
|
+
const key = `${user} ${ref.type}`;
|
|
53
|
+
if (!byCaller.has(key))
|
|
54
|
+
byCaller.set(key, new Set());
|
|
55
|
+
byCaller.get(key).add(ref.api);
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
/** Every DECLARED remote service, called or not, onto the graph. Absent when none are declared. */
|
|
59
|
+
attach(graph) {
|
|
60
|
+
const names = Object.keys(this.remotes).sort();
|
|
61
|
+
if (names.length === 0)
|
|
62
|
+
return;
|
|
63
|
+
const remoteServices = {};
|
|
64
|
+
for (const name of names) {
|
|
65
|
+
const decl = this.remotes[name];
|
|
66
|
+
remoteServices[name] = {
|
|
67
|
+
label: decl.label,
|
|
68
|
+
implements: decl.implements.map((entry) => `${entry.api} (${entry.type})`),
|
|
69
|
+
calledBy: this.calledBy(name),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
graph.remoteServices = remoteServices;
|
|
73
|
+
}
|
|
74
|
+
calledBy(name) {
|
|
75
|
+
const byCaller = this.calls.get(name) ?? new Map();
|
|
76
|
+
return [...byCaller.keys()].sort().map((key) => {
|
|
77
|
+
const parts = key.split(' ');
|
|
78
|
+
return {
|
|
79
|
+
from: parts[0],
|
|
80
|
+
via: [...byCaller.get(key)].sort(),
|
|
81
|
+
type: parts[1],
|
|
82
|
+
};
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
exports.RemoteCallCollector = RemoteCallCollector;
|
|
87
|
+
//# sourceMappingURL=runtime-remote-calls.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runtime-remote-calls.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/runtime-remote-calls.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Remote service calls in the runtime graph (#1203)\n *\n * A `uses` whose named target (the call-site `targetService`, else the caller's `callsService`) is a\n * REVIEWED remote service — declared in dependencies.json `remoteServices`, outside this workspace —\n * is claimed here instead of being resolved against the workspace's own nodes. Without this the call\n * either decayed into an \"unresolved\" vendor box (nothing in-repo implements the contract) or failed as\n * a name no module answers to (something in-repo does).\n *\n * Workspace names always win: a remote name that collides with a node is reported as a problem and\n * never claims the call, so a stray declaration cannot redirect a call away from a real module.\n */\n\nimport type { ApiRef, ApiTransport } from './api-usage/api-relations';\nimport type { RemoteServiceApi, RemoteServices } from './remote-services';\nimport type { RuntimeGraph, RuntimeRemoteCall, RuntimeRemoteService } from './runtime-graph-model';\n\nexport class RemoteCallCollector {\n /** remote name -> \"caller transport\" -> contracts called there. */\n private readonly calls = new Map<string, Map<string, Set<string>>>();\n\n constructor(\n private readonly remotes: RemoteServices,\n /** True when a workspace runtime node answers to the name. */\n private readonly isWorkspaceName: (name: string) => boolean,\n private readonly problems: string[],\n ) {\n for (const name of Object.keys(remotes).sort())\n if (isWorkspaceName(name))\n problems.push(\n `remoteServices declares '${name}', but a workspace module already answers to that name — a remote ` +\n `service must live OUTSIDE this workspace. Remove the remoteServices entry or rename one of them.`,\n );\n }\n\n /**\n * True when `wanted` names a declared remote service, which then owns this use. A remote that does\n * not declare the contract + transport is a problem; the call is still drawn to it, so the picture\n * shows exactly what the declaration is missing.\n */\n claim(user: string, ref: ApiRef, wanted: string | undefined): boolean {\n if (wanted === undefined) return false;\n const remote = this.remotes[wanted];\n if (remote === undefined || this.isWorkspaceName(wanted)) return false;\n if (!remote.implementsApi(ref.api, ref.type))\n this.problems.push(\n `${user} calls \"${ref.api}\" (${ref.type}) at remote service '${wanted}', but remoteServices.${wanted}.implements ` +\n `does not declare it. Add { \"api\": \"${ref.api}\", \"type\": \"${ref.type}\" } after review, or fix the target.`,\n );\n let byCaller = this.calls.get(wanted);\n if (byCaller === undefined) {\n byCaller = new Map<string, Set<string>>();\n this.calls.set(wanted, byCaller);\n }\n const key = `${user} ${ref.type}`;\n if (!byCaller.has(key)) byCaller.set(key, new Set<string>());\n byCaller.get(key)!.add(ref.api);\n return true;\n }\n\n /** Every DECLARED remote service, called or not, onto the graph. Absent when none are declared. */\n attach(graph: RuntimeGraph): void {\n const names = Object.keys(this.remotes).sort();\n if (names.length === 0) return;\n const remoteServices: Record<string, RuntimeRemoteService> = {};\n for (const name of names) {\n const decl = this.remotes[name];\n remoteServices[name] = {\n label: decl.label,\n implements: decl.implements.map(\n (entry: RemoteServiceApi) => `${entry.api} (${entry.type})`,\n ),\n calledBy: this.calledBy(name),\n };\n }\n graph.remoteServices = remoteServices;\n }\n\n private calledBy(name: string): RuntimeRemoteCall[] {\n const byCaller = this.calls.get(name) ?? new Map<string, Set<string>>();\n return [...byCaller.keys()].sort().map((key: string): RuntimeRemoteCall => {\n const parts = key.split(' ');\n return {\n from: parts[0],\n via: [...byCaller.get(key)!].sort(),\n type: parts[1] as ApiTransport,\n };\n });\n }\n}\n"]}
|
|
@@ -57,6 +57,7 @@ const dot_syntax_1 = require("./dot-syntax");
|
|
|
57
57
|
const graph_render_model_1 = require("./graph-render-model");
|
|
58
58
|
const runtime_viewer_adapter_1 = require("./runtime-viewer-adapter");
|
|
59
59
|
const runtime_html_page_1 = require("./runtime-html-page");
|
|
60
|
+
const remote_service_node_id_1 = require("./remote-service-node-id");
|
|
60
61
|
const runtime_viz_theme_1 = require("./runtime-viz-theme");
|
|
61
62
|
/** Apis per line inside a node label — beyond this the box grows wider than it is readable. */
|
|
62
63
|
const APIS_PER_LABEL_LINE = 3;
|
|
@@ -411,6 +412,45 @@ function externalDot(graph, hidden, model) {
|
|
|
411
412
|
}
|
|
412
413
|
return dot;
|
|
413
414
|
}
|
|
415
|
+
/**
|
|
416
|
+
* The reviewed out-of-workspace services (#1203): service-shaped nodes (rounded, teal, bold outline,
|
|
417
|
+
* "(remote service)"), with an arrow from every workspace service that targets one. An rpc call is a
|
|
418
|
+
* solid arrow and a queued call a dashed "enqueue" arrow, exactly as between two in-repo services.
|
|
419
|
+
*
|
|
420
|
+
* Drawn whatever `showExternalNodes` says: these are OUR services, not vendor systems, and hiding them
|
|
421
|
+
* would hide calls the reviewed graph asserts exist.
|
|
422
|
+
*/
|
|
423
|
+
// webpieces-disable no-function-outside-class -- DOT string builder, matching getShortName in this file
|
|
424
|
+
function remoteServicesDot(graph, hidden, model) {
|
|
425
|
+
const remotes = graph.remoteServices ?? {};
|
|
426
|
+
const names = Object.keys(remotes).sort();
|
|
427
|
+
if (names.length === 0)
|
|
428
|
+
return '';
|
|
429
|
+
let dot = '\n // Remote services — ours, but outside this workspace (reviewed remoteServices).\n';
|
|
430
|
+
for (const name of names) {
|
|
431
|
+
const remote = remotes[name];
|
|
432
|
+
const id = new remote_service_node_id_1.RemoteServiceNodeId().of(name);
|
|
433
|
+
// The label is display text; the service name stays visible because it is what `uses` target.
|
|
434
|
+
const title = remote.label === undefined
|
|
435
|
+
? (0, dot_syntax_1.dotValue)(name)
|
|
436
|
+
: `${(0, dot_syntax_1.dotValue)(remote.label)}\\n${(0, dot_syntax_1.dotValue)(name)}`;
|
|
437
|
+
dot += model.node(id, ` "${id}" [shape=box, style="filled,rounded,bold", fillcolor="${runtime_viz_theme_1.REMOTE_SERVICE_FILL}", ` +
|
|
438
|
+
`color="${runtime_viz_theme_1.REMOTE_SERVICE_BORDER}", label="${title}\\n(remote service)\\n` +
|
|
439
|
+
`Implements (${remote.implements.length})"];\n`);
|
|
440
|
+
}
|
|
441
|
+
for (const name of names) {
|
|
442
|
+
const id = new remote_service_node_id_1.RemoteServiceNodeId().of(name);
|
|
443
|
+
for (const call of remotes[name].calledBy) {
|
|
444
|
+
if (hidden.has(call.from))
|
|
445
|
+
continue;
|
|
446
|
+
const via = call.via.length > 2 ? `Uses (${call.via.length}) ▾` : labelList(call.via);
|
|
447
|
+
const label = call.type === 'pubsub' ? `${via}\\nenqueue` : via;
|
|
448
|
+
const style = call.type === 'pubsub' ? ', style=dashed' : '';
|
|
449
|
+
dot += model.edge(call.from, id, ` "${serviceNodeId(call.from)}" -> "${id}" [label="${label}", color="${runtime_viz_theme_1.REMOTE_SERVICE_BORDER}"${style}];\n`);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
return dot;
|
|
453
|
+
}
|
|
414
454
|
/** Build the Graphviz DOT for the runtime service graph. */
|
|
415
455
|
// webpieces-disable no-function-outside-class -- module entry point, matching the sibling builders here
|
|
416
456
|
function generateRuntimeDot(graph, title = 'WebPieces Runtime Architecture', options = new runtime_viz_options_1.RuntimeVizOptions()) {
|
|
@@ -448,6 +488,7 @@ function generateRuntimeRenderModel(graph, title = 'WebPieces Runtime Architectu
|
|
|
448
488
|
dot += model.edge(edge.from, edge.to, edgeDot(edge));
|
|
449
489
|
}
|
|
450
490
|
dot += queuesDot(graph, hidden, model);
|
|
491
|
+
dot += remoteServicesDot(graph, hidden, model);
|
|
451
492
|
dot += triggerDot(graph, hidden, options, model);
|
|
452
493
|
if (options.showExternalNodes) {
|
|
453
494
|
dot += externalSystemsDot(graph, hidden, model);
|
|
@@ -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":["import { LevelBandLayout } from './graph-level-bands';\n/**\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. A queue box lists ONE LINE PER QUEUE: the\n * unit underneath is still the METHOD (what Terraform creates, and what\n * runtime-dependencies.json records), but queues of one contract sharing the same\n * producers and consumers are drawn together instead of as adjacent near-identical\n * boxes — every queue is still named, only the node count drops.\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 box naming the\n * CALLER that posts to it (`twilio`), in the same id space as the outbound\n * systems, so a vendor we both call and are called by is ONE box. Those are the\n * entry points that wake a service up at 3am, and a graph built only from in-repo\n * callers cannot show them at all.\n *\n * EVERY node on the rendered page is clickable and opens the SHARED floating menu\n * (graph-node-menu.ts — one implementation, also used by architecture/dependencies.html\n * and every project's design.html). Lock/Unlock pins separate complete incoming/outgoing chains, with hover adding a temporary chain. There is no \"View Design\"\n * item here — a node is a running service, queue, datastore or third-party system, not\n * an nx project, so no design.html exists to point at and the item is absent rather than\n * dead. Filter Unconnected follows separate incoming/outgoing chains and compacts the rendered\n * topology; Turn off Filter restores the full picture. Lock remains independent of Filter.\n */\n\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport type {\n RuntimeGraph,\n RuntimeEdge,\n RuntimeQueue,\n RuntimeService,\n RuntimeTrigger,\n} from './runtime-graph';\nimport { dotValue, recordValue, assertValidDot } from './dot-syntax';\nimport { GraphRenderModel } from './graph-render-model';\nimport { RuntimeViewerAdapter } from './runtime-viewer-adapter';\nimport { RuntimeHtmlPage } from './runtime-html-page';\nimport {\n QUEUE_FILL,\n QUEUE_SHAPE,\n QUEUE_LABEL_PREFIX,\n QUEUE_CLASS,\n EXTERNAL_SHAPES,\n EXTERNAL_FILLS,\n EXTERNAL_FILL,\n EXTERNAL_BORDER,\n CRON_FILL,\n CRON_BORDER,\n CUT_CYCLE_EDGE_ATTRS,\n} from './runtime-viz-theme';\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\nimport { RuntimeVizOptions } from './runtime-viz-options';\nexport { RuntimeVizOptions } from './runtime-viz-options';\n\n/**\n * The DISPLAY name of a project/service: scope stripped. LABELS ONLY.\n *\n * Never an identity. `@scope/public-api` and `public-api` are two distinct projects that strip to\n * the same string, and keying nodes or edges on that fuses them into one box (and draws the edge\n * between them as a self-loop). See {@link serviceNodeId} and lib/graph-names.ts.\n */\nfunction getShortName(name: string): string {\n return name.includes('/') ? name.split('/').pop()! : name;\n}\n\n/** The DOT identity of a runtime service: its project key, escaped — never the display name. */\n// webpieces-disable no-function-outside-class -- DOT id builder, matching getShortName in this file\nfunction serviceNodeId(name: string): string {\n return dotValue(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 * DOT for ONE NON-QUEUED runtime edge: a direct labeled SOLID arrow (producer calls consumer, the\n * response comes back). Queued (pubsub) hops are drawn by {@link queuesDot} instead, because they\n * are merged across methods and therefore cannot be emitted one edge at a time.\n *\n * Dashed is the graph's one line-level distinction, and it now says one of two things: an event that\n * returns as soon as it is queued, or (in red, labelled \"legacy cycle\") an edge a `cutLegacyCycle:`\n * tag holds out of leveling. A cut edge is still DRAWN — erasing the arrow would erase exactly the\n * debt the tag records — it is only drawn as the debt it is. See lib/cut-legacy-cycle-resolver.ts.\n */\n// webpieces-disable no-function-outside-class -- DOT string builder, matching getShortName in this file\nfunction edgeDot(edge: RuntimeEdge): string {\n const from = serviceNodeId(edge.from);\n const to = serviceNodeId(edge.to);\n const viaRaw =\n edge.via.length > 2\n ? `Uses (${edge.via.length}) ▾`\n : edge.via.map((v: string) => getShortName(v)).join(', ');\n const cut = edge.cutLegacyCycle === true;\n const label = cut ? `${dotValue(viaRaw)}\\\\nlegacy cycle` : dotValue(viaRaw);\n return ` \"${from}\" -> \"${to}\" [label=\"${label}\"${cut ? CUT_CYCLE_EDGE_ATTRS : ''}];\\n`;\n}\n\n/** The VISIBLE producers and consumers of one queue, gathered from the edges that survived hiding. */\nclass QueueEndpoints {\n readonly producers = new Set<string>();\n readonly consumers = new Set<string>();\n}\n\n/**\n * One drawn queue BOX: the queue keys (`Api.method`) it shows, and the endpoints they all share.\n * Several keys land in one box only when they agree on all three of contract, producers and\n * consumers — see {@link queuesDot}.\n */\nclass QueueGroup {\n constructor(\n public readonly members: string[],\n public readonly producers: string[],\n public readonly consumers: string[],\n ) {}\n}\n\n/**\n * The label line for ONE queue inside a box: `Api.method`, and the Terraform queue name ONLY when it\n * is not the derived `${Api}-${method}`.\n *\n * Printing a derived name was pure restatement — the line above it already said `Api.method`, so\n * every queue box carried a second line that added nothing and doubled its height. A `@Queue(...)`\n * OVERRIDE is the opposite: that string appears nowhere else on the graph, and it is the one string\n * Terraform must match, so it stays.\n */\n// webpieces-disable no-function-outside-class -- DOT label builder, matching getShortName in this file\nfunction queueLine(key: string, queue: RuntimeQueue | undefined): string {\n const line = recordValue(key);\n if (queue === undefined) return line;\n if (queue.queueName === `${queue.api}-${queue.method}`) return line;\n return `${line}\\\\nqueue: ${recordValue(queue.queueName)}`;\n}\n\n/** The node statement + enqueue/deliver arrows for one queue box. Service identities remain raw; only emitted DOT is escaped. */\n// webpieces-disable no-function-outside-class -- DOT string builder, matching getShortName in this file\nfunction queueBoxDot(\n id: string,\n body: string,\n producers: string[],\n consumers: string[],\n model: GraphRenderModel,\n contractCount: number,\n queueKeys: string[],\n): string {\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. Drop it and the node silently\n // degrades to a plain box.\n let dot = model.node(\n id,\n ` \"${id}\" [shape=${QUEUE_SHAPE}, style=\"filled\", fillcolor=\"${QUEUE_FILL}\", ` +\n `class=\"${QUEUE_CLASS}\", label=\"${QUEUE_LABEL_PREFIX}${body}\"];\\n`,\n [],\n queueKeys,\n );\n for (const producer of producers)\n dot += model.edge(\n producer,\n id,\n ` \"${serviceNodeId(producer)}\" -> \"${id}\" [label=\"Uses (${contractCount}) ▾\\\\nenqueue\", style=dashed];\\n`,\n );\n for (const consumer of consumers)\n dot += model.edge(\n id,\n consumer,\n ` \"${id}\" -> \"${serviceNodeId(consumer)}\" [label=\"Uses (${contractCount}) ▾\\\\ndeliver\", style=dashed];\\n`,\n );\n return dot;\n}\n\n/**\n * Group the queued hops into boxes. The key is (contract, producer SET, consumer SET) — NOT the\n * (from,to) pair of some edge.\n *\n * A queue node converges EVERY producer and consumer of its method onto one box, so keying by a\n * service PAIR would re-split a two-producer queue into two boxes and invent topology nobody wrote.\n * Only queues of the same contract whose producer and consumer sets are IDENTICAL may share a box;\n * anything else stays separate, because merging it would assert a routing that does not exist.\n */\n// webpieces-disable no-function-outside-class -- DOT string builder, matching getShortName in this file\nfunction groupQueues(byQueue: Map<string, QueueEndpoints>, graph: RuntimeGraph): QueueGroup[] {\n const groups = new Map<string, QueueGroup>();\n // Sorted keys → the first member of every group, and therefore its node id, is deterministic.\n for (const key of [...byQueue.keys()].sort()) {\n const ends = byQueue.get(key)!;\n const producers = [...ends.producers].sort();\n const consumers = [...ends.consumers].sort();\n const api = graph.queues[key]?.api ?? key.split('.')[0];\n const mergeKey = [api, producers.join(','), consumers.join(',')].join(PAIR_SEP);\n const existing = groups.get(mergeKey);\n if (existing === undefined)\n groups.set(mergeKey, new QueueGroup([key], producers, consumers));\n else existing.members.push(key);\n }\n return [...groups.values()];\n}\n\n/**\n * The queued half of the graph: producer → QUEUE → consumer, drawn with a sideways-cylinder node and\n * DASHED arrows (an event returns as soon as it is queued).\n *\n * One BOX may list several queues, one per line. The unit Cloud Tasks and Terraform create is still\n * the METHOD — that is what runtime-dependencies.json records and this render never changes it — but\n * two methods of one contract flowing between exactly the same producers and consumers drew two\n * adjacent boxes whose only difference was the method name, and each carried a `queue:` line that\n * merely restated it. Listing them inside ONE box keeps every queue named (you can still see which\n * one is stuck) at a fraction of the node count.\n */\n// webpieces-disable no-function-outside-class -- DOT string builder, matching getShortName in this file\nfunction queuesDot(graph: RuntimeGraph, hidden: Set<string>, model: GraphRenderModel): string {\n const queued = graph.runtimeEdges.filter(\n (e: RuntimeEdge) => e.type === 'pubsub' && !hidden.has(e.from) && !hidden.has(e.to),\n );\n if (queued.length === 0) return '';\n\n let dot =\n '\\n // Queued hops. Each LINE in a box is one Cloud Tasks queue; queues of one contract\\n' +\n ' // sharing the same producers AND consumers are drawn in a single box.\\n';\n\n // A contract with no method table (no architecture/apis/<Api>.json for it) has no\n // per-method queue at all, so it keeps the historical unnamed per-pair box.\n const byQueue = new Map<string, QueueEndpoints>();\n for (const edge of queued) {\n const from = edge.from;\n const to = edge.to;\n if (edge.queue === undefined) {\n // Kept RAW: recordValue already applies dotValue, so escaping here would double it.\n const viaRaw =\n edge.via.length > 2\n ? `Uses (${edge.via.length}) ▾`\n : edge.via.map((v: string) => getShortName(v)).join(', ');\n dot += queueBoxDot(\n `queue__${dotId(edge.from)}__${dotId(edge.to)}`,\n `${recordValue(viaRaw)}\\\\nqueue`,\n [from],\n [to],\n model,\n edge.via.length,\n [`${from}->${to}`],\n );\n continue;\n }\n if (!byQueue.has(edge.queue)) byQueue.set(edge.queue, new QueueEndpoints());\n byQueue.get(edge.queue)!.producers.add(from);\n byQueue.get(edge.queue)!.consumers.add(to);\n }\n\n for (const group of groupQueues(byQueue, graph)) {\n const body = group.members\n .map((key: string) => queueLine(key, graph.queues[key]))\n .join('\\\\n');\n dot += queueBoxDot(\n `queue__${dotId(group.members[0])}`,\n body,\n group.producers,\n group.consumers,\n model,\n 1,\n group.members,\n );\n }\n return dot;\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 * ONE external-system node statement: the shape/fill of what the system IS, its label, and a\n * parenthesised subtitle saying which way it faces.\n *\n * Shared by the OUTBOUND systems ({@link externalSystemsDot}) and the INBOUND callers\n * ({@link triggerDot}) on purpose. They emit into the SAME `system__<identity>` id space, so a\n * vendor this repo both calls and is called by is one box with arrows in both directions — and a\n * copy-pasted attribute string would have let the two drift until they stopped being one box.\n */\n// webpieces-disable no-function-outside-class -- DOT string builder, matching getShortName in this file\nfunction externalSystemNodeDot(\n identity: string,\n kind: string,\n label: string,\n subtitle: string,\n model: GraphRenderModel,\n): string {\n const shape = EXTERNAL_SHAPES[kind] ?? 'box';\n const fill = EXTERNAL_FILLS[kind] ?? EXTERNAL_FILL;\n // An Mrecord-shaped system needs the same empty leading field as a queue node, or it renders as\n // 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(label) : dotValue(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 return model.node(\n `system__${dotId(identity)}`,\n ` \"system__${dotId(identity)}\" [shape=${shape}, style=\"filled\", fillcolor=\"${fill}\", ` +\n `${marker}label=\"${prefix}${text}\\\\n(${subtitle})\"];\\n`,\n );\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 * The inbound box names the CALLER (`twilio`), never the contract. Naming the contract was the whole\n * bug: it restated what the service box directly below already says, while the one fact the box\n * exists to convey — which vendor is posting to us — appeared nowhere. The contract keeps its place\n * on the EDGE label, where `WhatsAppApi.inbound` reads as \"…posts to this method\".\n *\n * Identity is the caller's label in the SAME `system__` space {@link externalSystemsDot} uses, which\n * buys three things at once: two vendors hitting one contract are two boxes, one vendor hitting three\n * methods is one box with three arrows, and a vendor this repo also CALLS is that same single box.\n * `options` is passed in for the last of those — when that outbound half will be drawn, this half\n * must not restate the node statement.\n */\n// webpieces-disable no-function-outside-class -- DOT string builder, matching getShortName in this file\nfunction triggerDot(\n graph: RuntimeGraph,\n hidden: Set<string>,\n options: RuntimeVizOptions,\n model: GraphRenderModel,\n): string {\n const triggers = graph.triggers.filter((t: RuntimeTrigger) => !hidden.has(t.service));\n if (triggers.length === 0) return '';\n\n // Identities externalSystemsDot is about to draw; skipped here so the node is stated ONCE.\n const drawnOutbound = new Set(\n options.showExternalNodes ? Object.keys(graph.externalSystems ?? {}) : [],\n );\n const emitted = new Set<string>();\n let dot =\n '\\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 = serviceNodeId(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 model.node(\n id,\n ` \"${id}\" [shape=circle, style=\"filled\", fillcolor=\"${CRON_FILL}\", ` +\n `color=\"${CRON_BORDER}\", label=\"⏰\\\\ncron\"];\\n`,\n ) +\n model.edge(\n id,\n trigger.service,\n ` \"${id}\" -> \"${service}\" [label=\"${label}\\\\n${schedule}\", color=\"${CRON_BORDER}\"];\\n`,\n );\n continue;\n }\n const caller = trigger.caller;\n // Undeclared: only possible for a graph generated BEFORE the caller was required (generation\n // now fails instead). DOTTED and question-marked, so \"we were never told who this is\" cannot\n // be mistaken for a named vendor.\n const id =\n caller === undefined\n ? `inbound__${dotId(trigger.api)}`\n : `system__${dotId(caller.label)}`;\n if (!emitted.has(id) && !(caller !== undefined && drawnOutbound.has(caller.label))) {\n emitted.add(id);\n dot +=\n caller === undefined\n ? model.node(\n id,\n ` \"${id}\" [shape=box, style=\"dotted,filled\", fillcolor=\"${EXTERNAL_FILL}\", ` +\n `color=\"${EXTERNAL_BORDER}\", label=\"${dotValue(trigger.api)}\\\\n? unknown caller\"];\\n`,\n )\n : externalSystemNodeDot(\n caller.label,\n caller.kind,\n caller.label,\n 'external caller',\n model,\n );\n }\n dot += model.edge(\n id,\n trigger.service,\n ` \"${id}\" -> \"${service}\" [label=\"${label}\", style=dashed, color=\"${EXTERNAL_BORDER}\"];\\n`,\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(\n graph: RuntimeGraph,\n hidden: Set<string>,\n model: GraphRenderModel,\n): string {\n const systems = graph.externalSystems ?? {};\n const ids = Object.keys(systems).sort();\n if (ids.length === 0) return '';\n\n let dot =\n '\\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 dot += externalSystemNodeDot(\n id,\n system.kind,\n system.label,\n `external ${dotValue(system.kind)}`,\n model,\n );\n }\n for (const id of ids) {\n const system = systems[id];\n const via =\n system.apis.length === 0\n ? ''\n : ` [label=\"${system.apis.length > 2 ? `Uses (${system.apis.length}) ▾` : labelList([...system.apis].sort())}\"]`;\n for (const service of [...system.usedBy].sort()) {\n if (hidden.has(service)) continue;\n dot += model.edge(\n service,\n `system__${dotId(id)}`,\n ` \"${serviceNodeId(service)}\" -> \"system__${dotId(id)}\"${via};\\n`,\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>, model: GraphRenderModel): 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 // Keyed on the OWNER's full name, never its short name: two scoped api-libs whose short\n // names match are two external systems, and fusing them draws one box for both.\n const external = 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, and built\n // from the owner's FULL name so two same-short-named owners stay two boxes; only the label\n // 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 += model.node(\n `external__${external}`,\n ` \"external__${dotValue(external)}\" [shape=box, style=\"dashed,filled\", fillcolor=\"${EXTERNAL_FILL}\", ` +\n `color=\"${EXTERNAL_BORDER}\", label=\"${dotValue(getShortName(external))}\\\\n(external)\"];\\n`,\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 apis = apisByPair.get(key)!.sort();\n const via = apis.length > 2 ? `Uses (${apis.length}) ▾` : labelList(apis);\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 += model.edge(\n service,\n `external__${external}`,\n ` \"${serviceNodeId(service)}\" -> \"external__${dotValue(external)}\" ` +\n `[label=\"${via}\", color=\"${EXTERNAL_BORDER}\"];\\n`,\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 return generateRuntimeRenderModel(graph, title, options).fullDot;\n}\n\n/** Capture synthetic drawable nodes and edges at their original emission sites. */\n// webpieces-disable no-function-outside-class -- renderer entry point matching generateRuntimeDot\nexport function generateRuntimeRenderModel(\n graph: RuntimeGraph,\n title: string = 'WebPieces Runtime Architecture',\n options: RuntimeVizOptions = new RuntimeVizOptions(),\n): GraphRenderModel {\n const model = new GraphRenderModel();\n model.viewer = 'runtime';\n const adapter = new RuntimeViewerAdapter(graph, options.projects);\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=12];\\n\\n';\n\n model.header = dot;\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(\n (name: string) => graph.services[name].drawOnGraph === false,\n ),\n );\n\n for (const name of Object.keys(graph.services)) {\n if (hidden.has(name)) continue;\n const svc = graph.services[name];\n dot += adapter.service(model, name, svc);\n }\n\n model.bands = adapter.levelBands(model);\n dot += new LevelBandLayout().dot(model.bands);\n dot += '\\n';\n\n for (const edge of graph.runtimeEdges) {\n if (hidden.has(edge.from) || hidden.has(edge.to)) continue;\n // Queued hops are merged across methods, so they cannot be emitted one edge at a time.\n if (edge.type === 'pubsub') continue;\n dot += model.edge(edge.from, edge.to, edgeDot(edge));\n }\n\n dot += queuesDot(graph, hidden, model);\n dot += triggerDot(graph, hidden, options, model);\n\n if (options.showExternalNodes) {\n dot += externalSystemsDot(graph, hidden, model);\n dot += externalDot(graph, hidden, model);\n }\n\n const footerStart = dot.length;\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 model.footer = dot.slice(footerStart);\n model.completeEndpoints();\n // #1179: a queue, trigger or external system belongs to the products of the services it touches.\n model.attachProducts((id: string): boolean => graph.services[id] !== undefined);\n adapter.contextModes(model);\n model.fullDot = dot;\n return model;\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 model = generateRuntimeRenderModel(graph, title, options);\n const dot = model.fullDot;\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, new RuntimeHtmlPage().render(model, title, graph, options), '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":["import { LevelBandLayout } from './graph-level-bands';\n/**\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. A queue box lists ONE LINE PER QUEUE: the\n * unit underneath is still the METHOD (what Terraform creates, and what\n * runtime-dependencies.json records), but queues of one contract sharing the same\n * producers and consumers are drawn together instead of as adjacent near-identical\n * boxes — every queue is still named, only the node count drops.\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 box naming the\n * CALLER that posts to it (`twilio`), in the same id space as the outbound\n * systems, so a vendor we both call and are called by is ONE box. Those are the\n * entry points that wake a service up at 3am, and a graph built only from in-repo\n * callers cannot show them at all.\n *\n * EVERY node on the rendered page is clickable and opens the SHARED floating menu\n * (graph-node-menu.ts — one implementation, also used by architecture/dependencies.html\n * and every project's design.html). Lock/Unlock pins separate complete incoming/outgoing chains, with hover adding a temporary chain. There is no \"View Design\"\n * item here — a node is a running service, queue, datastore or third-party system, not\n * an nx project, so no design.html exists to point at and the item is absent rather than\n * dead. Filter Unconnected follows separate incoming/outgoing chains and compacts the rendered\n * topology; Turn off Filter restores the full picture. Lock remains independent of Filter.\n */\n\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport type {\n RuntimeGraph,\n RuntimeEdge,\n RuntimeQueue,\n RuntimeService,\n RuntimeTrigger,\n} from './runtime-graph';\nimport { dotValue, recordValue, assertValidDot } from './dot-syntax';\nimport { GraphRenderModel } from './graph-render-model';\nimport { RuntimeViewerAdapter } from './runtime-viewer-adapter';\nimport { RuntimeHtmlPage } from './runtime-html-page';\nimport { RemoteServiceNodeId } from './remote-service-node-id';\nimport {\n QUEUE_FILL,\n QUEUE_SHAPE,\n QUEUE_LABEL_PREFIX,\n QUEUE_CLASS,\n EXTERNAL_SHAPES,\n EXTERNAL_FILLS,\n EXTERNAL_FILL,\n EXTERNAL_BORDER,\n CRON_FILL,\n CRON_BORDER,\n CUT_CYCLE_EDGE_ATTRS,\n REMOTE_SERVICE_FILL,\n REMOTE_SERVICE_BORDER,\n} from './runtime-viz-theme';\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\nimport { RuntimeVizOptions } from './runtime-viz-options';\nexport { RuntimeVizOptions } from './runtime-viz-options';\n\n/**\n * The DISPLAY name of a project/service: scope stripped. LABELS ONLY.\n *\n * Never an identity. `@scope/public-api` and `public-api` are two distinct projects that strip to\n * the same string, and keying nodes or edges on that fuses them into one box (and draws the edge\n * between them as a self-loop). See {@link serviceNodeId} and lib/graph-names.ts.\n */\nfunction getShortName(name: string): string {\n return name.includes('/') ? name.split('/').pop()! : name;\n}\n\n/** The DOT identity of a runtime service: its project key, escaped — never the display name. */\n// webpieces-disable no-function-outside-class -- DOT id builder, matching getShortName in this file\nfunction serviceNodeId(name: string): string {\n return dotValue(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 * DOT for ONE NON-QUEUED runtime edge: a direct labeled SOLID arrow (producer calls consumer, the\n * response comes back). Queued (pubsub) hops are drawn by {@link queuesDot} instead, because they\n * are merged across methods and therefore cannot be emitted one edge at a time.\n *\n * Dashed is the graph's one line-level distinction, and it now says one of two things: an event that\n * returns as soon as it is queued, or (in red, labelled \"legacy cycle\") an edge a `cutLegacyCycle:`\n * tag holds out of leveling. A cut edge is still DRAWN — erasing the arrow would erase exactly the\n * debt the tag records — it is only drawn as the debt it is. See lib/cut-legacy-cycle-resolver.ts.\n */\n// webpieces-disable no-function-outside-class -- DOT string builder, matching getShortName in this file\nfunction edgeDot(edge: RuntimeEdge): string {\n const from = serviceNodeId(edge.from);\n const to = serviceNodeId(edge.to);\n const viaRaw =\n edge.via.length > 2\n ? `Uses (${edge.via.length}) ▾`\n : edge.via.map((v: string) => getShortName(v)).join(', ');\n const cut = edge.cutLegacyCycle === true;\n const label = cut ? `${dotValue(viaRaw)}\\\\nlegacy cycle` : dotValue(viaRaw);\n return ` \"${from}\" -> \"${to}\" [label=\"${label}\"${cut ? CUT_CYCLE_EDGE_ATTRS : ''}];\\n`;\n}\n\n/** The VISIBLE producers and consumers of one queue, gathered from the edges that survived hiding. */\nclass QueueEndpoints {\n readonly producers = new Set<string>();\n readonly consumers = new Set<string>();\n}\n\n/**\n * One drawn queue BOX: the queue keys (`Api.method`) it shows, and the endpoints they all share.\n * Several keys land in one box only when they agree on all three of contract, producers and\n * consumers — see {@link queuesDot}.\n */\nclass QueueGroup {\n constructor(\n public readonly members: string[],\n public readonly producers: string[],\n public readonly consumers: string[],\n ) {}\n}\n\n/**\n * The label line for ONE queue inside a box: `Api.method`, and the Terraform queue name ONLY when it\n * is not the derived `${Api}-${method}`.\n *\n * Printing a derived name was pure restatement — the line above it already said `Api.method`, so\n * every queue box carried a second line that added nothing and doubled its height. A `@Queue(...)`\n * OVERRIDE is the opposite: that string appears nowhere else on the graph, and it is the one string\n * Terraform must match, so it stays.\n */\n// webpieces-disable no-function-outside-class -- DOT label builder, matching getShortName in this file\nfunction queueLine(key: string, queue: RuntimeQueue | undefined): string {\n const line = recordValue(key);\n if (queue === undefined) return line;\n if (queue.queueName === `${queue.api}-${queue.method}`) return line;\n return `${line}\\\\nqueue: ${recordValue(queue.queueName)}`;\n}\n\n/** The node statement + enqueue/deliver arrows for one queue box. Service identities remain raw; only emitted DOT is escaped. */\n// webpieces-disable no-function-outside-class -- DOT string builder, matching getShortName in this file\nfunction queueBoxDot(\n id: string,\n body: string,\n producers: string[],\n consumers: string[],\n model: GraphRenderModel,\n contractCount: number,\n queueKeys: string[],\n): string {\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. Drop it and the node silently\n // degrades to a plain box.\n let dot = model.node(\n id,\n ` \"${id}\" [shape=${QUEUE_SHAPE}, style=\"filled\", fillcolor=\"${QUEUE_FILL}\", ` +\n `class=\"${QUEUE_CLASS}\", label=\"${QUEUE_LABEL_PREFIX}${body}\"];\\n`,\n [],\n queueKeys,\n );\n for (const producer of producers)\n dot += model.edge(\n producer,\n id,\n ` \"${serviceNodeId(producer)}\" -> \"${id}\" [label=\"Uses (${contractCount}) ▾\\\\nenqueue\", style=dashed];\\n`,\n );\n for (const consumer of consumers)\n dot += model.edge(\n id,\n consumer,\n ` \"${id}\" -> \"${serviceNodeId(consumer)}\" [label=\"Uses (${contractCount}) ▾\\\\ndeliver\", style=dashed];\\n`,\n );\n return dot;\n}\n\n/**\n * Group the queued hops into boxes. The key is (contract, producer SET, consumer SET) — NOT the\n * (from,to) pair of some edge.\n *\n * A queue node converges EVERY producer and consumer of its method onto one box, so keying by a\n * service PAIR would re-split a two-producer queue into two boxes and invent topology nobody wrote.\n * Only queues of the same contract whose producer and consumer sets are IDENTICAL may share a box;\n * anything else stays separate, because merging it would assert a routing that does not exist.\n */\n// webpieces-disable no-function-outside-class -- DOT string builder, matching getShortName in this file\nfunction groupQueues(byQueue: Map<string, QueueEndpoints>, graph: RuntimeGraph): QueueGroup[] {\n const groups = new Map<string, QueueGroup>();\n // Sorted keys → the first member of every group, and therefore its node id, is deterministic.\n for (const key of [...byQueue.keys()].sort()) {\n const ends = byQueue.get(key)!;\n const producers = [...ends.producers].sort();\n const consumers = [...ends.consumers].sort();\n const api = graph.queues[key]?.api ?? key.split('.')[0];\n const mergeKey = [api, producers.join(','), consumers.join(',')].join(PAIR_SEP);\n const existing = groups.get(mergeKey);\n if (existing === undefined)\n groups.set(mergeKey, new QueueGroup([key], producers, consumers));\n else existing.members.push(key);\n }\n return [...groups.values()];\n}\n\n/**\n * The queued half of the graph: producer → QUEUE → consumer, drawn with a sideways-cylinder node and\n * DASHED arrows (an event returns as soon as it is queued).\n *\n * One BOX may list several queues, one per line. The unit Cloud Tasks and Terraform create is still\n * the METHOD — that is what runtime-dependencies.json records and this render never changes it — but\n * two methods of one contract flowing between exactly the same producers and consumers drew two\n * adjacent boxes whose only difference was the method name, and each carried a `queue:` line that\n * merely restated it. Listing them inside ONE box keeps every queue named (you can still see which\n * one is stuck) at a fraction of the node count.\n */\n// webpieces-disable no-function-outside-class -- DOT string builder, matching getShortName in this file\nfunction queuesDot(graph: RuntimeGraph, hidden: Set<string>, model: GraphRenderModel): string {\n const queued = graph.runtimeEdges.filter(\n (e: RuntimeEdge) => e.type === 'pubsub' && !hidden.has(e.from) && !hidden.has(e.to),\n );\n if (queued.length === 0) return '';\n\n let dot =\n '\\n // Queued hops. Each LINE in a box is one Cloud Tasks queue; queues of one contract\\n' +\n ' // sharing the same producers AND consumers are drawn in a single box.\\n';\n\n // A contract with no method table (no architecture/apis/<Api>.json for it) has no\n // per-method queue at all, so it keeps the historical unnamed per-pair box.\n const byQueue = new Map<string, QueueEndpoints>();\n for (const edge of queued) {\n const from = edge.from;\n const to = edge.to;\n if (edge.queue === undefined) {\n // Kept RAW: recordValue already applies dotValue, so escaping here would double it.\n const viaRaw =\n edge.via.length > 2\n ? `Uses (${edge.via.length}) ▾`\n : edge.via.map((v: string) => getShortName(v)).join(', ');\n dot += queueBoxDot(\n `queue__${dotId(edge.from)}__${dotId(edge.to)}`,\n `${recordValue(viaRaw)}\\\\nqueue`,\n [from],\n [to],\n model,\n edge.via.length,\n [`${from}->${to}`],\n );\n continue;\n }\n if (!byQueue.has(edge.queue)) byQueue.set(edge.queue, new QueueEndpoints());\n byQueue.get(edge.queue)!.producers.add(from);\n byQueue.get(edge.queue)!.consumers.add(to);\n }\n\n for (const group of groupQueues(byQueue, graph)) {\n const body = group.members\n .map((key: string) => queueLine(key, graph.queues[key]))\n .join('\\\\n');\n dot += queueBoxDot(\n `queue__${dotId(group.members[0])}`,\n body,\n group.producers,\n group.consumers,\n model,\n 1,\n group.members,\n );\n }\n return dot;\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 * ONE external-system node statement: the shape/fill of what the system IS, its label, and a\n * parenthesised subtitle saying which way it faces.\n *\n * Shared by the OUTBOUND systems ({@link externalSystemsDot}) and the INBOUND callers\n * ({@link triggerDot}) on purpose. They emit into the SAME `system__<identity>` id space, so a\n * vendor this repo both calls and is called by is one box with arrows in both directions — and a\n * copy-pasted attribute string would have let the two drift until they stopped being one box.\n */\n// webpieces-disable no-function-outside-class -- DOT string builder, matching getShortName in this file\nfunction externalSystemNodeDot(\n identity: string,\n kind: string,\n label: string,\n subtitle: string,\n model: GraphRenderModel,\n): string {\n const shape = EXTERNAL_SHAPES[kind] ?? 'box';\n const fill = EXTERNAL_FILLS[kind] ?? EXTERNAL_FILL;\n // An Mrecord-shaped system needs the same empty leading field as a queue node, or it renders as\n // 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(label) : dotValue(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 return model.node(\n `system__${dotId(identity)}`,\n ` \"system__${dotId(identity)}\" [shape=${shape}, style=\"filled\", fillcolor=\"${fill}\", ` +\n `${marker}label=\"${prefix}${text}\\\\n(${subtitle})\"];\\n`,\n );\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 * The inbound box names the CALLER (`twilio`), never the contract. Naming the contract was the whole\n * bug: it restated what the service box directly below already says, while the one fact the box\n * exists to convey — which vendor is posting to us — appeared nowhere. The contract keeps its place\n * on the EDGE label, where `WhatsAppApi.inbound` reads as \"…posts to this method\".\n *\n * Identity is the caller's label in the SAME `system__` space {@link externalSystemsDot} uses, which\n * buys three things at once: two vendors hitting one contract are two boxes, one vendor hitting three\n * methods is one box with three arrows, and a vendor this repo also CALLS is that same single box.\n * `options` is passed in for the last of those — when that outbound half will be drawn, this half\n * must not restate the node statement.\n */\n// webpieces-disable no-function-outside-class -- DOT string builder, matching getShortName in this file\nfunction triggerDot(\n graph: RuntimeGraph,\n hidden: Set<string>,\n options: RuntimeVizOptions,\n model: GraphRenderModel,\n): string {\n const triggers = graph.triggers.filter((t: RuntimeTrigger) => !hidden.has(t.service));\n if (triggers.length === 0) return '';\n\n // Identities externalSystemsDot is about to draw; skipped here so the node is stated ONCE.\n const drawnOutbound = new Set(\n options.showExternalNodes ? Object.keys(graph.externalSystems ?? {}) : [],\n );\n const emitted = new Set<string>();\n let dot =\n '\\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 = serviceNodeId(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 model.node(\n id,\n ` \"${id}\" [shape=circle, style=\"filled\", fillcolor=\"${CRON_FILL}\", ` +\n `color=\"${CRON_BORDER}\", label=\"⏰\\\\ncron\"];\\n`,\n ) +\n model.edge(\n id,\n trigger.service,\n ` \"${id}\" -> \"${service}\" [label=\"${label}\\\\n${schedule}\", color=\"${CRON_BORDER}\"];\\n`,\n );\n continue;\n }\n const caller = trigger.caller;\n // Undeclared: only possible for a graph generated BEFORE the caller was required (generation\n // now fails instead). DOTTED and question-marked, so \"we were never told who this is\" cannot\n // be mistaken for a named vendor.\n const id =\n caller === undefined\n ? `inbound__${dotId(trigger.api)}`\n : `system__${dotId(caller.label)}`;\n if (!emitted.has(id) && !(caller !== undefined && drawnOutbound.has(caller.label))) {\n emitted.add(id);\n dot +=\n caller === undefined\n ? model.node(\n id,\n ` \"${id}\" [shape=box, style=\"dotted,filled\", fillcolor=\"${EXTERNAL_FILL}\", ` +\n `color=\"${EXTERNAL_BORDER}\", label=\"${dotValue(trigger.api)}\\\\n? unknown caller\"];\\n`,\n )\n : externalSystemNodeDot(\n caller.label,\n caller.kind,\n caller.label,\n 'external caller',\n model,\n );\n }\n dot += model.edge(\n id,\n trigger.service,\n ` \"${id}\" -> \"${service}\" [label=\"${label}\", style=dashed, color=\"${EXTERNAL_BORDER}\"];\\n`,\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(\n graph: RuntimeGraph,\n hidden: Set<string>,\n model: GraphRenderModel,\n): string {\n const systems = graph.externalSystems ?? {};\n const ids = Object.keys(systems).sort();\n if (ids.length === 0) return '';\n\n let dot =\n '\\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 dot += externalSystemNodeDot(\n id,\n system.kind,\n system.label,\n `external ${dotValue(system.kind)}`,\n model,\n );\n }\n for (const id of ids) {\n const system = systems[id];\n const via =\n system.apis.length === 0\n ? ''\n : ` [label=\"${system.apis.length > 2 ? `Uses (${system.apis.length}) ▾` : labelList([...system.apis].sort())}\"]`;\n for (const service of [...system.usedBy].sort()) {\n if (hidden.has(service)) continue;\n dot += model.edge(\n service,\n `system__${dotId(id)}`,\n ` \"${serviceNodeId(service)}\" -> \"system__${dotId(id)}\"${via};\\n`,\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>, model: GraphRenderModel): 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 // Keyed on the OWNER's full name, never its short name: two scoped api-libs whose short\n // names match are two external systems, and fusing them draws one box for both.\n const external = 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, and built\n // from the owner's FULL name so two same-short-named owners stay two boxes; only the label\n // 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 += model.node(\n `external__${external}`,\n ` \"external__${dotValue(external)}\" [shape=box, style=\"dashed,filled\", fillcolor=\"${EXTERNAL_FILL}\", ` +\n `color=\"${EXTERNAL_BORDER}\", label=\"${dotValue(getShortName(external))}\\\\n(external)\"];\\n`,\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 apis = apisByPair.get(key)!.sort();\n const via = apis.length > 2 ? `Uses (${apis.length}) ▾` : labelList(apis);\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 += model.edge(\n service,\n `external__${external}`,\n ` \"${serviceNodeId(service)}\" -> \"external__${dotValue(external)}\" ` +\n `[label=\"${via}\", color=\"${EXTERNAL_BORDER}\"];\\n`,\n );\n }\n return dot;\n}\n\n/**\n * The reviewed out-of-workspace services (#1203): service-shaped nodes (rounded, teal, bold outline,\n * \"(remote service)\"), with an arrow from every workspace service that targets one. An rpc call is a\n * solid arrow and a queued call a dashed \"enqueue\" arrow, exactly as between two in-repo services.\n *\n * Drawn whatever `showExternalNodes` says: these are OUR services, not vendor systems, and hiding them\n * would hide calls the reviewed graph asserts exist.\n */\n// webpieces-disable no-function-outside-class -- DOT string builder, matching getShortName in this file\nfunction remoteServicesDot(graph: RuntimeGraph, hidden: Set<string>, model: GraphRenderModel): string {\n const remotes = graph.remoteServices ?? {};\n const names = Object.keys(remotes).sort();\n if (names.length === 0) return '';\n\n let dot = '\\n // Remote services — ours, but outside this workspace (reviewed remoteServices).\\n';\n for (const name of names) {\n const remote = remotes[name];\n const id = new RemoteServiceNodeId().of(name);\n // The label is display text; the service name stays visible because it is what `uses` target.\n const title =\n remote.label === undefined\n ? dotValue(name)\n : `${dotValue(remote.label)}\\\\n${dotValue(name)}`;\n dot += model.node(\n id,\n ` \"${id}\" [shape=box, style=\"filled,rounded,bold\", fillcolor=\"${REMOTE_SERVICE_FILL}\", ` +\n `color=\"${REMOTE_SERVICE_BORDER}\", label=\"${title}\\\\n(remote service)\\\\n` +\n `Implements (${remote.implements.length})\"];\\n`,\n );\n }\n for (const name of names) {\n const id = new RemoteServiceNodeId().of(name);\n for (const call of remotes[name].calledBy) {\n if (hidden.has(call.from)) continue;\n const via = call.via.length > 2 ? `Uses (${call.via.length}) ▾` : labelList(call.via);\n const label = call.type === 'pubsub' ? `${via}\\\\nenqueue` : via;\n const style = call.type === 'pubsub' ? ', style=dashed' : '';\n dot += model.edge(\n call.from,\n id,\n ` \"${serviceNodeId(call.from)}\" -> \"${id}\" [label=\"${label}\", color=\"${REMOTE_SERVICE_BORDER}\"${style}];\\n`,\n );\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 return generateRuntimeRenderModel(graph, title, options).fullDot;\n}\n\n/** Capture synthetic drawable nodes and edges at their original emission sites. */\n// webpieces-disable no-function-outside-class -- renderer entry point matching generateRuntimeDot\nexport function generateRuntimeRenderModel(\n graph: RuntimeGraph,\n title: string = 'WebPieces Runtime Architecture',\n options: RuntimeVizOptions = new RuntimeVizOptions(),\n): GraphRenderModel {\n const model = new GraphRenderModel();\n model.viewer = 'runtime';\n const adapter = new RuntimeViewerAdapter(graph, options.projects);\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=12];\\n\\n';\n\n model.header = dot;\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(\n (name: string) => graph.services[name].drawOnGraph === false,\n ),\n );\n\n for (const name of Object.keys(graph.services)) {\n if (hidden.has(name)) continue;\n const svc = graph.services[name];\n dot += adapter.service(model, name, svc);\n }\n\n model.bands = adapter.levelBands(model);\n dot += new LevelBandLayout().dot(model.bands);\n dot += '\\n';\n\n for (const edge of graph.runtimeEdges) {\n if (hidden.has(edge.from) || hidden.has(edge.to)) continue;\n // Queued hops are merged across methods, so they cannot be emitted one edge at a time.\n if (edge.type === 'pubsub') continue;\n dot += model.edge(edge.from, edge.to, edgeDot(edge));\n }\n\n dot += queuesDot(graph, hidden, model);\n dot += remoteServicesDot(graph, hidden, model);\n dot += triggerDot(graph, hidden, options, model);\n\n if (options.showExternalNodes) {\n dot += externalSystemsDot(graph, hidden, model);\n dot += externalDot(graph, hidden, model);\n }\n\n const footerStart = dot.length;\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 model.footer = dot.slice(footerStart);\n model.completeEndpoints();\n // #1179: a queue, trigger or external system belongs to the products of the services it touches.\n model.attachProducts((id: string): boolean => graph.services[id] !== undefined);\n adapter.contextModes(model);\n model.fullDot = dot;\n return model;\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 model = generateRuntimeRenderModel(graph, title, options);\n const dot = model.fullDot;\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, new RuntimeHtmlPage().render(model, title, graph, options), 'utf-8');\n\n return { dotPath, htmlPath };\n}\n"]}
|
|
@@ -56,6 +56,13 @@ export declare const EXTERNAL_FILLS: Record<string, string>;
|
|
|
56
56
|
/** Fill + border for the dashed terminal node standing for a system outside this repo. */
|
|
57
57
|
export declare const EXTERNAL_FILL = "#FAFAFA";
|
|
58
58
|
export declare const EXTERNAL_BORDER = "#9E9E9E";
|
|
59
|
+
/**
|
|
60
|
+
* Fill + border for a REMOTE SERVICE (#1203): one of OUR services that lives outside this workspace
|
|
61
|
+
* (another repo, a VM). Rounded like an in-repo service, because it is one — it implements our
|
|
62
|
+
* contracts — but teal with a bold outline, so it never reads as a vendor box or as a deployable here.
|
|
63
|
+
*/
|
|
64
|
+
export declare const REMOTE_SERVICE_FILL = "#E0F2F1";
|
|
65
|
+
export declare const REMOTE_SERVICE_BORDER = "#00796B";
|
|
59
66
|
/** Fill + border for the clock node standing for a scheduler-driven endpoint. */
|
|
60
67
|
/** The colour of a `cutLegacyCycle` edge — red, because it is drawing DEBT, not a design. */
|
|
61
68
|
export declare const CUT_CYCLE_COLOR = "#D32F2F";
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* cycle-free.
|
|
13
13
|
*/
|
|
14
14
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
-
exports.CRON_BORDER = exports.CRON_FILL = exports.CUT_CYCLE_EDGE_ATTRS = exports.CUT_CYCLE_COLOR = exports.EXTERNAL_BORDER = exports.EXTERNAL_FILL = exports.EXTERNAL_FILLS = exports.EXTERNAL_SHAPES = exports.RUNTIME_HOST_FILL = exports.DATABASE_FILL = exports.QUEUE_CLASS = exports.QUEUE_LABEL_PREFIX = exports.QUEUE_SHAPE = exports.QUEUE_FILL = exports.LEVEL_COLORS = void 0;
|
|
15
|
+
exports.CRON_BORDER = exports.CRON_FILL = exports.CUT_CYCLE_EDGE_ATTRS = exports.CUT_CYCLE_COLOR = exports.REMOTE_SERVICE_BORDER = exports.REMOTE_SERVICE_FILL = exports.EXTERNAL_BORDER = exports.EXTERNAL_FILL = exports.EXTERNAL_FILLS = exports.EXTERNAL_SHAPES = exports.RUNTIME_HOST_FILL = exports.DATABASE_FILL = exports.QUEUE_CLASS = exports.QUEUE_LABEL_PREFIX = exports.QUEUE_SHAPE = exports.QUEUE_FILL = exports.LEVEL_COLORS = void 0;
|
|
16
16
|
exports.legendHtml = legendHtml;
|
|
17
17
|
exports.LEVEL_COLORS = {
|
|
18
18
|
0: '#E8F5E9',
|
|
@@ -79,6 +79,13 @@ exports.EXTERNAL_FILLS = {
|
|
|
79
79
|
/** Fill + border for the dashed terminal node standing for a system outside this repo. */
|
|
80
80
|
exports.EXTERNAL_FILL = '#FAFAFA';
|
|
81
81
|
exports.EXTERNAL_BORDER = '#9E9E9E';
|
|
82
|
+
/**
|
|
83
|
+
* Fill + border for a REMOTE SERVICE (#1203): one of OUR services that lives outside this workspace
|
|
84
|
+
* (another repo, a VM). Rounded like an in-repo service, because it is one — it implements our
|
|
85
|
+
* contracts — but teal with a bold outline, so it never reads as a vendor box or as a deployable here.
|
|
86
|
+
*/
|
|
87
|
+
exports.REMOTE_SERVICE_FILL = '#E0F2F1';
|
|
88
|
+
exports.REMOTE_SERVICE_BORDER = '#00796B';
|
|
82
89
|
/** Fill + border for the clock node standing for a scheduler-driven endpoint. */
|
|
83
90
|
/** The colour of a `cutLegacyCycle` edge — red, because it is drawing DEBT, not a design. */
|
|
84
91
|
exports.CUT_CYCLE_COLOR = '#D32F2F';
|
|
@@ -102,6 +109,8 @@ class LegendSwatches {
|
|
|
102
109
|
storage = '<svg width="46" height="26"><path d="M2,22 V6 H16 l3,3 H44 V22 Z" fill="#F3E5F5" stroke="#333"/></svg>';
|
|
103
110
|
external = `<svg width="46" height="26"><rect x="1" y="3" width="44" height="20" fill="${exports.EXTERNAL_FILL}" ` +
|
|
104
111
|
`stroke="${exports.EXTERNAL_BORDER}" stroke-dasharray="4,3"/></svg>`;
|
|
112
|
+
remote = `<svg width="46" height="26"><rect x="1" y="3" width="44" height="20" rx="7" fill="${exports.REMOTE_SERVICE_FILL}" ` +
|
|
113
|
+
`stroke="${exports.REMOTE_SERVICE_BORDER}" stroke-width="2"/></svg>`;
|
|
105
114
|
cron = `<svg width="46" height="26"><circle cx="23" cy="13" r="11" fill="${exports.CRON_FILL}" stroke="${exports.CRON_BORDER}"/>` +
|
|
106
115
|
'<text x="23" y="18" font-size="12" text-anchor="middle">⏰</text></svg>';
|
|
107
116
|
solid = '<svg width="60" height="20"><line x1="2" y1="10" x2="48" y2="10" stroke="#333" stroke-width="1.5"/>' +
|
|
@@ -126,6 +135,7 @@ function legendHtml() {
|
|
|
126
135
|
<div class="legend-col">
|
|
127
136
|
<h3>Node shapes — <em>what a box is</em></h3>
|
|
128
137
|
${item(sw.service, '<strong>service</strong> — a deployable in this repo; fill is its dependency level')}
|
|
138
|
+
${item(sw.remote, '<strong>remote service</strong> — one of our services that lives OUTSIDE this workspace (another repo, a VM, partner endpoints), declared in the reviewed <code>remoteServices</code> table of dependencies.json. It implements our contracts, so it is a service, not a vendor system.')}
|
|
129
139
|
${item(sw.queue, '<strong>queue</strong> — each <em>line</em> in the box is one Cloud Tasks queue, the unit Terraform actually creates. Queues of one contract that flow between the <em>same</em> producer and consumer share a box; every one is still named, so you can always see <em>which</em> queue is stuck.')}
|
|
130
140
|
${item(sw.database, '<strong>database</strong> — a datastore outside this repo')}
|
|
131
141
|
${item(sw.storage, '<strong>object storage</strong> — a bucket outside this repo')}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runtime-viz-theme.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/runtime-viz-theme.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The runtime graph's VISUAL VOCABULARY, and the legend that explains it.\n *\n * Every colour, shape and fill the drawing uses lives here, next to the legend swatches that teach\n * the reader what each one means. They are one unit on purpose: the legend's job is to be a true\n * account of the picture, and the fastest way for a legend to start lying is for it to sit in a\n * different file from the constants it describes and drift as those change.\n *\n * Imported by runtime-visualizer.ts, which owns the DRAWING (which nodes and edges to emit). This\n * module knows nothing about the graph model, which is what keeps the dependency one-way and\n * cycle-free.\n */\n\nexport const LEVEL_COLORS: Record<number, string> = {\n 0: '#E8F5E9',\n 1: '#E3F2FD',\n 2: '#FFF3E0',\n 3: '#FCE4EC',\n};\n\nexport const 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 */\nexport const QUEUE_SHAPE = 'Mrecord';\nexport const 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.ts 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 */\nexport const QUEUE_CLASS = 'wp_queue';\n\n/** Fill for the upright cylinder standing for an external DATASTORE (firestore, postgres, ...). */\nexport const DATABASE_FILL = '#E1F5FE';\n\n/**\n * Fill for a destination whose ADDRESS arrives at runtime (a partner-registered webhook URL). Amber\n * rather than the neutral external grey on purpose: this is the one outbound hop where the address\n * itself is attacker-influenced data, and the graph exists to make it findable.\n */\nexport const RUNTIME_HOST_FILL = '#FFE0B2';\n\n/** Shape per external-system kind. Anything unrecognised falls back to the generic dashed box. */\nexport const EXTERNAL_SHAPES: Record<string, string> = {\n database: 'cylinder',\n cache: 'cylinder',\n queue: 'Mrecord',\n storage: 'folder',\n // A destination dialled by address rather than by name: a doubled outline, because what is on\n // the far side is not one system we can name but whatever the data said.\n runtime: 'doubleoctagon',\n};\n\n/** Fill per external-system kind, paired with {@link EXTERNAL_SHAPES}. */\nexport const EXTERNAL_FILLS: Record<string, string> = {\n database: DATABASE_FILL,\n cache: DATABASE_FILL,\n queue: QUEUE_FILL,\n storage: '#F3E5F5',\n runtime: RUNTIME_HOST_FILL,\n};\n\n/** Fill + border for the dashed terminal node standing for a system outside this repo. */\nexport const EXTERNAL_FILL = '#FAFAFA';\nexport const EXTERNAL_BORDER = '#9E9E9E';\n\n/** Fill + border for the clock node standing for a scheduler-driven endpoint. */\n/** The colour of a `cutLegacyCycle` edge — red, because it is drawing DEBT, not a design. */\nexport const CUT_CYCLE_COLOR = '#D32F2F';\n\n/**\n * DOT attributes appended to a `cutLegacyCycle` edge: dashed and red so it reads as debt, and\n * `constraint=false` so it does not pull on a ranking it was deliberately excluded from.\n */\nexport const CUT_CYCLE_EDGE_ATTRS = `, style=dashed, color=\"${CUT_CYCLE_COLOR}\", fontcolor=\"${CUT_CYCLE_COLOR}\", constraint=false`;\n\nexport const CRON_FILL = '#FFF9C4';\nexport const CRON_BORDER = '#F9A825';\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.ts 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\nexport function 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> — each <em>line</em> in the box is one Cloud Tasks queue, the unit Terraform actually creates. Queues of one contract that flow between the <em>same</em> producer and consumer share a box; every one is still named, so you can always see <em>which</em> queue is stuck.')}\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, and the box names the <strong>CALLER</strong> (<code>twilio</code>), not our contract — the contract and method are on the arrow. One vendor is ONE box however many endpoints it posts to, and the same box whether we call it or it calls us. A dotted box labelled <code>? unknown caller</code> means the endpoint never declared one.')}\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 names its contracts inline or exposes them in a Uses dropdown. 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 (N)\nUses (N)\n</pre>\n <div class=\"legend-note\">Hover, focus, or click <strong>Implements</strong> and <strong>Uses</strong> for complete qualified lists. One/two API arrows show names; larger groups have a Uses dropdown.</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"]}
|
|
1
|
+
{"version":3,"file":"runtime-viz-theme.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/runtime-viz-theme.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The runtime graph's VISUAL VOCABULARY, and the legend that explains it.\n *\n * Every colour, shape and fill the drawing uses lives here, next to the legend swatches that teach\n * the reader what each one means. They are one unit on purpose: the legend's job is to be a true\n * account of the picture, and the fastest way for a legend to start lying is for it to sit in a\n * different file from the constants it describes and drift as those change.\n *\n * Imported by runtime-visualizer.ts, which owns the DRAWING (which nodes and edges to emit). This\n * module knows nothing about the graph model, which is what keeps the dependency one-way and\n * cycle-free.\n */\n\nexport const LEVEL_COLORS: Record<number, string> = {\n 0: '#E8F5E9',\n 1: '#E3F2FD',\n 2: '#FFF3E0',\n 3: '#FCE4EC',\n};\n\nexport const 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 */\nexport const QUEUE_SHAPE = 'Mrecord';\nexport const 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.ts 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 */\nexport const QUEUE_CLASS = 'wp_queue';\n\n/** Fill for the upright cylinder standing for an external DATASTORE (firestore, postgres, ...). */\nexport const DATABASE_FILL = '#E1F5FE';\n\n/**\n * Fill for a destination whose ADDRESS arrives at runtime (a partner-registered webhook URL). Amber\n * rather than the neutral external grey on purpose: this is the one outbound hop where the address\n * itself is attacker-influenced data, and the graph exists to make it findable.\n */\nexport const RUNTIME_HOST_FILL = '#FFE0B2';\n\n/** Shape per external-system kind. Anything unrecognised falls back to the generic dashed box. */\nexport const EXTERNAL_SHAPES: Record<string, string> = {\n database: 'cylinder',\n cache: 'cylinder',\n queue: 'Mrecord',\n storage: 'folder',\n // A destination dialled by address rather than by name: a doubled outline, because what is on\n // the far side is not one system we can name but whatever the data said.\n runtime: 'doubleoctagon',\n};\n\n/** Fill per external-system kind, paired with {@link EXTERNAL_SHAPES}. */\nexport const EXTERNAL_FILLS: Record<string, string> = {\n database: DATABASE_FILL,\n cache: DATABASE_FILL,\n queue: QUEUE_FILL,\n storage: '#F3E5F5',\n runtime: RUNTIME_HOST_FILL,\n};\n\n/** Fill + border for the dashed terminal node standing for a system outside this repo. */\nexport const EXTERNAL_FILL = '#FAFAFA';\nexport const EXTERNAL_BORDER = '#9E9E9E';\n\n/**\n * Fill + border for a REMOTE SERVICE (#1203): one of OUR services that lives outside this workspace\n * (another repo, a VM). Rounded like an in-repo service, because it is one — it implements our\n * contracts — but teal with a bold outline, so it never reads as a vendor box or as a deployable here.\n */\nexport const REMOTE_SERVICE_FILL = '#E0F2F1';\nexport const REMOTE_SERVICE_BORDER = '#00796B';\n\n/** Fill + border for the clock node standing for a scheduler-driven endpoint. */\n/** The colour of a `cutLegacyCycle` edge — red, because it is drawing DEBT, not a design. */\nexport const CUT_CYCLE_COLOR = '#D32F2F';\n\n/**\n * DOT attributes appended to a `cutLegacyCycle` edge: dashed and red so it reads as debt, and\n * `constraint=false` so it does not pull on a ranking it was deliberately excluded from.\n */\nexport const CUT_CYCLE_EDGE_ATTRS = `, style=dashed, color=\"${CUT_CYCLE_COLOR}\", fontcolor=\"${CUT_CYCLE_COLOR}\", constraint=false`;\n\nexport const CRON_FILL = '#FFF9C4';\nexport const CRON_BORDER = '#F9A825';\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.ts 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 remote =\n `<svg width=\"46\" height=\"26\"><rect x=\"1\" y=\"3\" width=\"44\" height=\"20\" rx=\"7\" fill=\"${REMOTE_SERVICE_FILL}\" ` +\n `stroke=\"${REMOTE_SERVICE_BORDER}\" stroke-width=\"2\"/></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\nexport function 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.remote, '<strong>remote service</strong> — one of our services that lives OUTSIDE this workspace (another repo, a VM, partner endpoints), declared in the reviewed <code>remoteServices</code> table of dependencies.json. It implements our contracts, so it is a service, not a vendor system.')}\n ${item(sw.queue, '<strong>queue</strong> — each <em>line</em> in the box is one Cloud Tasks queue, the unit Terraform actually creates. Queues of one contract that flow between the <em>same</em> producer and consumer share a box; every one is still named, so you can always see <em>which</em> queue is stuck.')}\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, and the box names the <strong>CALLER</strong> (<code>twilio</code>), not our contract — the contract and method are on the arrow. One vendor is ONE box however many endpoints it posts to, and the same box whether we call it or it calls us. A dotted box labelled <code>? unknown caller</code> means the endpoint never declared one.')}\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 names its contracts inline or exposes them in a Uses dropdown. 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 (N)\nUses (N)\n</pre>\n <div class=\"legend-note\">Hover, focus, or click <strong>Implements</strong> and <strong>Uses</strong> for complete qualified lists. One/two API arrows show names; larger groups have a Uses dropdown.</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"]}
|
|
@@ -9,6 +9,7 @@ const codec_1 = require("./codec");
|
|
|
9
9
|
const assembler_1 = require("./assembler");
|
|
10
10
|
const graph_loader_1 = require("../graph-loader");
|
|
11
11
|
const api_contract_files_1 = require("../api-contract-files");
|
|
12
|
+
const service_targets_1 = require("./service-targets");
|
|
12
13
|
/** Assembly consumes approvals only. Source verification is a separate per-project build target. */
|
|
13
14
|
class ApprovedWiringGraph {
|
|
14
15
|
attach(workspaceRoot, graph, infos, graphPath = graph_loader_1.DEFAULT_GRAPH_PATH) {
|
|
@@ -70,39 +71,11 @@ class ApprovedWiringGraph {
|
|
|
70
71
|
this.validateTargets(workspaceRoot, graphPath, graph);
|
|
71
72
|
}
|
|
72
73
|
validateTargets(workspaceRoot, graphPath, graph) {
|
|
73
|
-
const hosts = new Map();
|
|
74
|
-
for (const [name, entry] of Object.entries(graph)) {
|
|
75
|
-
if (entry.serviceName === undefined)
|
|
76
|
-
continue;
|
|
77
|
-
if (hosts.has(entry.serviceName))
|
|
78
|
-
this.fail(`Duplicate service identity ${entry.serviceName}: ${hosts.get(entry.serviceName)} and ${name}.`);
|
|
79
|
-
hosts.set(entry.serviceName, name);
|
|
80
|
-
}
|
|
81
74
|
const saved = (0, graph_loader_1.loadBlessedGraph)(workspaceRoot, graphPath);
|
|
82
75
|
if (saved === null)
|
|
83
76
|
this.fail('Missing saved contract references for approved runtime wiring.');
|
|
84
77
|
const contracts = new api_contract_files_1.ApiContractFiles().load(workspaceRoot, graphPath, saved.apiContractFiles);
|
|
85
|
-
|
|
86
|
-
for (const [owner, relation] of Object.entries(entry.apiRelations ?? {})) {
|
|
87
|
-
for (const ref of [...relation.implements, ...relation.uses]) {
|
|
88
|
-
if (ref.type !== 'external' &&
|
|
89
|
-
(contracts[ref.api]?.owner !== owner ||
|
|
90
|
-
contracts[ref.api]?.apiKind !== ref.type))
|
|
91
|
-
this.fail(`${name}: unknown qualified contract ${owner}#${ref.api}.`);
|
|
92
|
-
}
|
|
93
|
-
for (const ref of relation.uses) {
|
|
94
|
-
if (ref.targetService === undefined)
|
|
95
|
-
continue;
|
|
96
|
-
if (Object.values(saved.externalSystems).some((system) => system.apis.includes(ref.api)))
|
|
97
|
-
continue;
|
|
98
|
-
const provider = hosts.get(ref.targetService);
|
|
99
|
-
const implemented = provider === undefined
|
|
100
|
-
? undefined
|
|
101
|
-
: graph[provider].apiRelations?.[owner]?.implements;
|
|
102
|
-
if (!implemented?.some((api) => api.api === ref.api && api.type === ref.type))
|
|
103
|
-
this.fail(`${name}: target ${ref.targetService} does not implement ${owner}#${ref.api} (${ref.type}).`);
|
|
104
|
-
}
|
|
105
|
-
}
|
|
78
|
+
new service_targets_1.ServiceTargetValidator().validate(graph, contracts, saved.externalSystems, saved.remoteServices);
|
|
106
79
|
}
|
|
107
80
|
fail(message) {
|
|
108
81
|
throw new rules_config_1.RuleFailError('validate-runtime-architecture', message, undefined, undefined, [
|