@webpieces/nx-webpieces-rules 0.4.882 → 0.4.883

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.
Files changed (76) hide show
  1. package/README.md +40 -13
  2. package/package.json +7 -7
  3. package/src/configs/tag-truth-configs.d.ts +15 -0
  4. package/src/configs/tag-truth-configs.js +20 -1
  5. package/src/configs/tag-truth-configs.js.map +1 -1
  6. package/src/executors/generate/executor.js +3 -0
  7. package/src/executors/generate/executor.js.map +1 -1
  8. package/src/index.d.ts +6 -3
  9. package/src/index.js +15 -1
  10. package/src/index.js.map +1 -1
  11. package/src/lib/graph-color-modes.d.ts +50 -6
  12. package/src/lib/graph-color-modes.js +88 -8
  13. package/src/lib/graph-color-modes.js.map +1 -1
  14. package/src/lib/graph-comparator.js +2 -0
  15. package/src/lib/graph-comparator.js.map +1 -1
  16. package/src/lib/graph-entry-levels.d.ts +35 -0
  17. package/src/lib/graph-entry-levels.js +60 -0
  18. package/src/lib/graph-entry-levels.js.map +1 -0
  19. package/src/lib/graph-impact.d.ts +5 -5
  20. package/src/lib/graph-impact.js +5 -5
  21. package/src/lib/graph-impact.js.map +1 -1
  22. package/src/lib/graph-legend.d.ts +11 -1
  23. package/src/lib/graph-legend.js +44 -6
  24. package/src/lib/graph-legend.js.map +1 -1
  25. package/src/lib/graph-level-bands.js +3 -1
  26. package/src/lib/graph-level-bands.js.map +1 -1
  27. package/src/lib/graph-loader.d.ts +7 -1
  28. package/src/lib/graph-loader.js +10 -1
  29. package/src/lib/graph-loader.js.map +1 -1
  30. package/src/lib/graph-metadata.d.ts +2 -0
  31. package/src/lib/graph-metadata.js +19 -0
  32. package/src/lib/graph-metadata.js.map +1 -1
  33. package/src/lib/graph-page-shell.d.ts +28 -6
  34. package/src/lib/graph-page-shell.js +55 -18
  35. package/src/lib/graph-page-shell.js.map +1 -1
  36. package/src/lib/graph-page-styles.js +3 -1
  37. package/src/lib/graph-page-styles.js.map +1 -1
  38. package/src/lib/graph-products.d.ts +79 -0
  39. package/src/lib/graph-products.js +199 -0
  40. package/src/lib/graph-products.js.map +1 -0
  41. package/src/lib/graph-render-model.d.ts +13 -2
  42. package/src/lib/graph-render-model.js +36 -4
  43. package/src/lib/graph-render-model.js.map +1 -1
  44. package/src/lib/graph-sorter.d.ts +13 -0
  45. package/src/lib/graph-sorter.js.map +1 -1
  46. package/src/lib/graph-visualizer.client.d.ts +20 -10
  47. package/src/lib/graph-visualizer.client.js +67 -31
  48. package/src/lib/graph-visualizer.client.js.map +1 -1
  49. package/src/lib/graph-visualizer.js +7 -6
  50. package/src/lib/graph-visualizer.js.map +1 -1
  51. package/src/lib/product-resolver.d.ts +38 -0
  52. package/src/lib/product-resolver.js +63 -0
  53. package/src/lib/product-resolver.js.map +1 -0
  54. package/src/lib/runtime-graph-model.d.ts +7 -0
  55. package/src/lib/runtime-graph-model.js.map +1 -1
  56. package/src/lib/runtime-graph.js +3 -0
  57. package/src/lib/runtime-graph.js.map +1 -1
  58. package/src/lib/runtime-html-page.d.ts +7 -0
  59. package/src/lib/runtime-html-page.js +29 -0
  60. package/src/lib/runtime-html-page.js.map +1 -1
  61. package/src/lib/runtime-visualizer.client.d.ts +7 -0
  62. package/src/lib/runtime-visualizer.client.js +32 -0
  63. package/src/lib/runtime-visualizer.client.js.map +1 -1
  64. package/src/lib/runtime-visualizer.js +3 -1
  65. package/src/lib/runtime-visualizer.js.map +1 -1
  66. package/src/lib/saved-snapshot.js +6 -2
  67. package/src/lib/saved-snapshot.js.map +1 -1
  68. package/src/lib/tag-truth.d.ts +20 -3
  69. package/src/lib/tag-truth.js +48 -7
  70. package/src/lib/tag-truth.js.map +1 -1
  71. package/src/nx-policy-registry.js +1 -0
  72. package/src/nx-policy-registry.js.map +1 -1
  73. package/src/rule-help.js +1 -0
  74. package/src/rule-help.js.map +1 -1
  75. package/src/rule-settings.js +12 -1
  76. package/src/rule-settings.js.map +1 -1
@@ -0,0 +1,63 @@
1
+ "use strict";
2
+ /**
3
+ * Product Resolver (#1179)
4
+ *
5
+ * Reads the products a project DECLARES with the nx tag `product:<name>` on its project.json:
6
+ *
7
+ * "tags": ["framework:angular", "role:client", "product:lang"]
8
+ *
9
+ * A monorepo holding several products tags each product's ENTRY POINTS (servers, clients, apps —
10
+ * the `product-tags` rule makes that required) and the architecture graphs derive everything else:
11
+ * a library belongs to every product whose tagged project reaches it (graph-products.ts), so a
12
+ * library never carries a hand-maintained product tag.
13
+ *
14
+ * Several tags are allowed (`product:bugfixer` + `product:helper`). Each name must be lowercase
15
+ * kebab (`lang`, `job-finder`); a malformed tag (`product:`, `product:Lang`) is a problem that fails
16
+ * generation with the project named — the same way two `role:` tags do (role-resolver.ts).
17
+ *
18
+ * Why a tag and not a config key: it moves with the project, nx understands it for free
19
+ * (`nx show projects --projects tag:product:lang`), and a tag an older plugin does not know is ignored,
20
+ * so a consumer can tag BEFORE the release that reads the tags ships.
21
+ */
22
+ Object.defineProperty(exports, "__esModule", { value: true });
23
+ exports.ProductResolver = exports.ProductResolution = exports.PRODUCT_TAG_PREFIX = void 0;
24
+ exports.PRODUCT_TAG_PREFIX = 'product:';
25
+ /** Lowercase kebab: letters and digits, single dashes between words, no leading/trailing dash. */
26
+ const PRODUCT_NAME = /^[a-z0-9]+(-[a-z0-9]+)*$/;
27
+ class ProductResolution {
28
+ products;
29
+ problems;
30
+ constructor(
31
+ /** The declared product names, sorted and de-duplicated; empty when the project declares none. */
32
+ products,
33
+ /** One line per malformed tag, naming the project; empty when every tag is well formed. */
34
+ problems) {
35
+ this.products = products;
36
+ this.problems = problems;
37
+ }
38
+ }
39
+ exports.ProductResolution = ProductResolution;
40
+ class ProductResolver {
41
+ /** Every `product:` tag the project carries, validated. */
42
+ resolve(info) {
43
+ const products = new Set();
44
+ const problems = [];
45
+ for (const tag of info.tags) {
46
+ if (!tag.startsWith(exports.PRODUCT_TAG_PREFIX))
47
+ continue;
48
+ const name = tag.slice(exports.PRODUCT_TAG_PREFIX.length);
49
+ if (this.isValidName(name)) {
50
+ products.add(name);
51
+ continue;
52
+ }
53
+ problems.push(`${info.name}: product tag '${tag}' is malformed — the name after 'product:' must be lowercase ` +
54
+ `kebab (e.g. 'product:lang', 'product:job-finder')`);
55
+ }
56
+ return new ProductResolution([...products].sort(), problems);
57
+ }
58
+ isValidName(name) {
59
+ return PRODUCT_NAME.test(name);
60
+ }
61
+ }
62
+ exports.ProductResolver = ProductResolver;
63
+ //# sourceMappingURL=product-resolver.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"product-resolver.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/product-resolver.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;GAmBG;;;AAIU,QAAA,kBAAkB,GAAG,UAAU,CAAC;AAE7C,kGAAkG;AAClG,MAAM,YAAY,GAAG,0BAA0B,CAAC;AAEhD,MAAa,iBAAiB;IAGN;IAEA;IAJpB;IACI,kGAAkG;IAClF,QAAkB;IAClC,2FAA2F;IAC3E,QAAkB;QAFlB,aAAQ,GAAR,QAAQ,CAAU;QAElB,aAAQ,GAAR,QAAQ,CAAU;IACnC,CAAC;CACP;AAPD,8CAOC;AAED,MAAa,eAAe;IACxB,2DAA2D;IAC3D,OAAO,CAAC,IAAiB;QACrB,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAU,CAAC;QACnC,MAAM,QAAQ,GAAa,EAAE,CAAC;QAC9B,KAAK,MAAM,GAAG,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC;YAC1B,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,0BAAkB,CAAC;gBAAE,SAAS;YAClD,MAAM,IAAI,GAAG,GAAG,CAAC,KAAK,CAAC,0BAAkB,CAAC,MAAM,CAAC,CAAC;YAClD,IAAI,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,EAAE,CAAC;gBACzB,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;gBACnB,SAAS;YACb,CAAC;YACD,QAAQ,CAAC,IAAI,CACT,GAAG,IAAI,CAAC,IAAI,kBAAkB,GAAG,+DAA+D;gBAC5F,mDAAmD,CAC1D,CAAC;QACN,CAAC;QACD,OAAO,IAAI,iBAAiB,CAAC,CAAC,GAAG,QAAQ,CAAC,CAAC,IAAI,EAAE,EAAE,QAAQ,CAAC,CAAC;IACjE,CAAC;IAED,WAAW,CAAC,IAAY;QACpB,OAAO,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACnC,CAAC;CACJ;AAvBD,0CAuBC","sourcesContent":["/**\n * Product Resolver (#1179)\n *\n * Reads the products a project DECLARES with the nx tag `product:<name>` on its project.json:\n *\n * \"tags\": [\"framework:angular\", \"role:client\", \"product:lang\"]\n *\n * A monorepo holding several products tags each product's ENTRY POINTS (servers, clients, apps —\n * the `product-tags` rule makes that required) and the architecture graphs derive everything else:\n * a library belongs to every product whose tagged project reaches it (graph-products.ts), so a\n * library never carries a hand-maintained product tag.\n *\n * Several tags are allowed (`product:bugfixer` + `product:helper`). Each name must be lowercase\n * kebab (`lang`, `job-finder`); a malformed tag (`product:`, `product:Lang`) is a problem that fails\n * generation with the project named — the same way two `role:` tags do (role-resolver.ts).\n *\n * Why a tag and not a config key: it moves with the project, nx understands it for free\n * (`nx show projects --projects tag:product:lang`), and a tag an older plugin does not know is ignored,\n * so a consumer can tag BEFORE the release that reads the tags ships.\n */\n\nimport { ProjectInfo } from './project-info';\n\nexport const PRODUCT_TAG_PREFIX = 'product:';\n\n/** Lowercase kebab: letters and digits, single dashes between words, no leading/trailing dash. */\nconst PRODUCT_NAME = /^[a-z0-9]+(-[a-z0-9]+)*$/;\n\nexport class ProductResolution {\n constructor(\n /** The declared product names, sorted and de-duplicated; empty when the project declares none. */\n public readonly products: string[],\n /** One line per malformed tag, naming the project; empty when every tag is well formed. */\n public readonly problems: string[],\n ) {}\n}\n\nexport class ProductResolver {\n /** Every `product:` tag the project carries, validated. */\n resolve(info: ProjectInfo): ProductResolution {\n const products = new Set<string>();\n const problems: string[] = [];\n for (const tag of info.tags) {\n if (!tag.startsWith(PRODUCT_TAG_PREFIX)) continue;\n const name = tag.slice(PRODUCT_TAG_PREFIX.length);\n if (this.isValidName(name)) {\n products.add(name);\n continue;\n }\n problems.push(\n `${info.name}: product tag '${tag}' is malformed — the name after 'product:' must be lowercase ` +\n `kebab (e.g. 'product:lang', 'product:job-finder')`,\n );\n }\n return new ProductResolution([...products].sort(), problems);\n }\n\n isValidName(name: string): boolean {\n return PRODUCT_NAME.test(name);\n }\n}\n"]}
@@ -53,6 +53,13 @@ export interface RuntimeService {
53
53
  * nx tag.
54
54
  */
55
55
  drawOnGraph?: boolean;
56
+ /**
57
+ * The products this service belongs to (#1179), sorted: every product whose tagged service
58
+ * (its dependencies.json `products`) reaches it over RUNTIME `dependsOn`, through a queue to its
59
+ * consumers included — so a call into another product's service shows in this product's view.
60
+ * Absent for a service in no product. See graph-products.ts.
61
+ */
62
+ products?: string[];
56
63
  }
57
64
  export interface RuntimeApi {
58
65
  implementedBy: string[];
@@ -1 +1 @@
1
- {"version":3,"file":"runtime-graph-model.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/runtime-graph-model.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;GAUG","sourcesContent":["/**\n * Runtime Graph model\n *\n * The serialization DTOs for architecture/runtime-dependencies.json. Split out of runtime-graph.ts,\n * which owns the DERIVATION (and had grown past the file-size limit), so the committed data shape\n * can be read on its own — it is what every consumer of the file, and any future Terraform\n * cross-check, actually programs against.\n *\n * Interfaces rather than classes: these are parsed straight out of JSON with `JSON.parse`, so a\n * class would only ever be a shape assertion over a plain object, never a constructed instance.\n */\n\nimport type {\n ApiRef,\n ApiTransport,\n ExternalSystemDeclaration,\n ExternalSystemKind,\n} from './api-usage/api-relations';\n\nexport interface RuntimeService {\n level: number;\n /**\n * The project's declared nx role — 'server' or 'client' (a library is never a node). Carried so\n * the viz can LABEL the node with what it actually is: it used to infer the role from\n * `implements.length > 0`, which labelled every relation-less server \"client\". Optional so a\n * runtime-dependencies.json written before this field existed still parses and still renders\n * exactly as it did (the visualizer falls back to the old inference).\n */\n role?: string;\n /**\n * The name clients address this service by (`new ClientConfig('helper-fsdb')`), declared in its\n * project.json. Absent for a service nothing calls by name (e.g. a browser app).\n */\n serviceName?: string;\n /**\n * The service(s) this node's clients call when the call site carries no literal `ClientConfig`,\n * declared in its project.json (metadata.webpieces.callsService). A single name, or an\n * `{ apiClassName: serviceName }` map. Absent when the node declares no target. Mirrors\n * GraphEntry.callsService; it is the CALLING-side counterpart of `serviceName`.\n */\n callsService?: string | Record<string, string>;\n implements: string[];\n /**\n * apiClassName -> the LIBRARY project whose apiRelations declared that implements, for the apis\n * this service serves through an embedded library rather than its own source (e.g. a shared\n * route-registration lib). Answers \"who implements WarmupApi, and where did that come from?\",\n * which previously required walking the dependsOn closure by hand.\n */\n implementsVia?: Record<string, string>;\n uses: string[];\n dependsOn: string[];\n /** Reviewed binding provenance and runtime conditions, retained for compact detail views. */\n wiringUses?: ApiRef[];\n wiringImplements?: ApiRef[];\n /**\n * When false, this service is hidden from the rendered runtime graph (its\n * node AND every edge touching it are omitted from the HTML/DOT). It stays\n * in runtime-dependencies.json so the data view is complete. Absent means\n * drawn (the default). Mirrors GraphEntry.drawOnGraph from the `drawOnGraph:`\n * nx tag.\n */\n drawOnGraph?: boolean;\n}\n\nexport interface RuntimeApi {\n implementedBy: string[];\n usedBy: string[];\n /** Transport of this API — 'rpc' (direct call) or 'pubsub' (delivered through a queue). */\n type?: ApiTransport;\n /**\n * The api-lib project that OWNS this contract. For a contract nothing in-repo implements, this\n * is the external library the calls leave the repo through (`lib-firestore`, `lib-gmail`), which\n * is what the runtime viz labels its terminal external nodes with.\n */\n owner?: string;\n /**\n * Set when the contract carries an `@externalSystem <kind> [label]` JSDoc tag — the vendor seam\n * declaring WHAT it is a seam to, so the viz can draw firestore as a database rather than as the\n * same grey box as every other external. Absent on everything else.\n */\n externalSystem?: ExternalSystemDeclaration;\n}\n\nexport interface RuntimeEdge {\n from: string;\n to: string;\n via: string[];\n /**\n * Transport of this edge. 'rpc' → a direct call arrow. 'pubsub' → the producer enqueues and the\n * consumer is delivered later, so the runtime viz draws it as producer → QUEUE → consumer.\n * Edges are split by transport, so every edge is a single kind.\n */\n type?: ApiTransport;\n /**\n * `\"ApiClassName.methodName\"` — the queue this edge flows through. Present iff `type` is\n * 'pubsub'. Queues are per METHOD, not per service pair, because that is the unit Cloud Tasks\n * (and Terraform) actually create, so two services exchanging three queued methods are three\n * queues rather than one arrow.\n */\n queue?: string;\n /**\n * True when the CALLING project carries a `cutLegacyCycle:<target>` nx tag naming this edge's\n * target — an admission that this hop closes a REAL cycle that is being tolerated as legacy\n * debt, not a claim that it is harmless. The edge is still drawn (dashed, labelled \"legacy\n * cycle\"); it is only excluded from leveling and cycle detection. Absent on every other edge.\n */\n cutLegacyCycle?: boolean;\n}\n\n/**\n * One Cloud Tasks queue: the async seam between a producer and a consumer, at METHOD granularity.\n *\n * `producedBy` and `consumedBy` are deliberately not symmetric in confidence — see\n * {@link ApiRef.methodsInferred}. The consumer is derived from `addRoutes` plus the contract's\n * method table and is exact; the producer is attributed to every queued method of the contract it\n * built a client for, because which methods it enqueues is not statically recoverable.\n */\nexport interface RuntimeQueue {\n api: string;\n method: string;\n /** `@Queue(...)` override, else `${Api}-${method}` — the name Terraform must match 1:1. */\n queueName: string;\n producedBy: string[];\n consumedBy: string[];\n}\n\n/**\n * An endpoint driven by something that is NOT an in-repo caller — a clock or an outside system.\n * These never appear as runtime EDGES (there is no in-repo `from`), which is exactly why they were\n * invisible until now: a nightly sweep and a GCP push subscription are real runtime entry points\n * with real Terraform behind them, and the graph showed neither.\n */\nexport interface RuntimeTrigger {\n /** 'cron' → a scheduler fires it; 'external' → a system outside this repo posts to it. */\n kind: 'cron' | 'external';\n api: string;\n method: string;\n /** The service that SERVES the endpoint (the arrow's head). */\n service: string;\n /** Present for 'cron': the Cloud Scheduler job / queue name Terraform must match. */\n queueName?: string;\n /**\n * Present for 'external': WHO outside this repo posts to it, declared as\n * `@Endpoint(POST, p, WRITE, EXTERNAL, { calledBy: 'twilio' })`.\n *\n * The SAME `(kind,label)` an OUTBOUND {@link RuntimeExternalSystem} carries, so an inbound\n * `saas twilio` and an outbound `saas twilio` share a node identity and converge on ONE box —\n * `label` is the identity here too, not display text.\n *\n * Optional only for graphs generated BEFORE the caller was required; generation now fails rather\n * than emitting an `external` method without one.\n */\n caller?: ExternalSystemDeclaration;\n}\n\nexport interface RuntimeUnresolved {\n service: string;\n api: string;\n}\n\n/**\n * A system outside this repo that a service TALKS TO, drawn with a shape that says what it is\n * (a database as a cylinder, a bucket as a folder) instead of the one grey box every external\n * used to collapse into.\n *\n * Two declaration sites feed this, because the two real cases differ in whether a contract exists:\n *\n * - **Wrapped** — the repo has a vendor seam (`FirestoreAdminApi`), so the kind is declared with an\n * `@externalSystem <kind> [label]` JSDoc tag on the contract. A decorator cannot go on a TS\n * `interface`, and these seams are interfaces, so JSDoc is the only marker that fits in place.\n * - **Unwrapped** — the service opens the connection itself with no contract to mark (a `pg.Pool`,\n * a TypeORM `DataSource`), so the declaration is an `external:<kind>:<identity>` nx tag on that\n * project's project.json.\n *\n * `label` is the node IDENTITY, not just display text: two services declaring `postgres` converge on\n * ONE node with two arrows into it, rather than drawing a database each.\n */\nexport interface RuntimeExternalSystem {\n kind: ExternalSystemKind;\n /** Display name AND node identity — declarations sharing a label share a node. */\n label: string;\n /** Services with a direct arrow to it. No transitive fan-out: a tag speaks only for its project. */\n usedBy: string[];\n /** Contracts flowing over it. Empty for the unwrapped (tag-declared) case — there are none. */\n apis: string[];\n}\n\nexport interface RuntimeGraph {\n services: Record<string, RuntimeService>;\n apis: Record<string, RuntimeApi>;\n runtimeEdges: RuntimeEdge[];\n unresolvedUses: RuntimeUnresolved[];\n /** `\"Api.method\"` -> the queue between its producers and its consumers. */\n queues: Record<string, RuntimeQueue>;\n /** Clock- and outside-driven entry points, sorted for determinism. */\n triggers: RuntimeTrigger[];\n /**\n * Declared external systems, keyed by identity. Optional so every graph written before this\n * existed still parses — an absent map means \"nothing declared\", which renders exactly as before.\n */\n externalSystems?: Record<string, RuntimeExternalSystem>;\n}\n"]}
1
+ {"version":3,"file":"runtime-graph-model.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/runtime-graph-model.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;GAUG","sourcesContent":["/**\n * Runtime Graph model\n *\n * The serialization DTOs for architecture/runtime-dependencies.json. Split out of runtime-graph.ts,\n * which owns the DERIVATION (and had grown past the file-size limit), so the committed data shape\n * can be read on its own — it is what every consumer of the file, and any future Terraform\n * cross-check, actually programs against.\n *\n * Interfaces rather than classes: these are parsed straight out of JSON with `JSON.parse`, so a\n * class would only ever be a shape assertion over a plain object, never a constructed instance.\n */\n\nimport type {\n ApiRef,\n ApiTransport,\n ExternalSystemDeclaration,\n ExternalSystemKind,\n} from './api-usage/api-relations';\n\nexport interface RuntimeService {\n level: number;\n /**\n * The project's declared nx role — 'server' or 'client' (a library is never a node). Carried so\n * the viz can LABEL the node with what it actually is: it used to infer the role from\n * `implements.length > 0`, which labelled every relation-less server \"client\". Optional so a\n * runtime-dependencies.json written before this field existed still parses and still renders\n * exactly as it did (the visualizer falls back to the old inference).\n */\n role?: string;\n /**\n * The name clients address this service by (`new ClientConfig('helper-fsdb')`), declared in its\n * project.json. Absent for a service nothing calls by name (e.g. a browser app).\n */\n serviceName?: string;\n /**\n * The service(s) this node's clients call when the call site carries no literal `ClientConfig`,\n * declared in its project.json (metadata.webpieces.callsService). A single name, or an\n * `{ apiClassName: serviceName }` map. Absent when the node declares no target. Mirrors\n * GraphEntry.callsService; it is the CALLING-side counterpart of `serviceName`.\n */\n callsService?: string | Record<string, string>;\n implements: string[];\n /**\n * apiClassName -> the LIBRARY project whose apiRelations declared that implements, for the apis\n * this service serves through an embedded library rather than its own source (e.g. a shared\n * route-registration lib). Answers \"who implements WarmupApi, and where did that come from?\",\n * which previously required walking the dependsOn closure by hand.\n */\n implementsVia?: Record<string, string>;\n uses: string[];\n dependsOn: string[];\n /** Reviewed binding provenance and runtime conditions, retained for compact detail views. */\n wiringUses?: ApiRef[];\n wiringImplements?: ApiRef[];\n /**\n * When false, this service is hidden from the rendered runtime graph (its\n * node AND every edge touching it are omitted from the HTML/DOT). It stays\n * in runtime-dependencies.json so the data view is complete. Absent means\n * drawn (the default). Mirrors GraphEntry.drawOnGraph from the `drawOnGraph:`\n * nx tag.\n */\n drawOnGraph?: boolean;\n /**\n * The products this service belongs to (#1179), sorted: every product whose tagged service\n * (its dependencies.json `products`) reaches it over RUNTIME `dependsOn`, through a queue to its\n * consumers included — so a call into another product's service shows in this product's view.\n * Absent for a service in no product. See graph-products.ts.\n */\n products?: string[];\n}\n\nexport interface RuntimeApi {\n implementedBy: string[];\n usedBy: string[];\n /** Transport of this API — 'rpc' (direct call) or 'pubsub' (delivered through a queue). */\n type?: ApiTransport;\n /**\n * The api-lib project that OWNS this contract. For a contract nothing in-repo implements, this\n * is the external library the calls leave the repo through (`lib-firestore`, `lib-gmail`), which\n * is what the runtime viz labels its terminal external nodes with.\n */\n owner?: string;\n /**\n * Set when the contract carries an `@externalSystem <kind> [label]` JSDoc tag — the vendor seam\n * declaring WHAT it is a seam to, so the viz can draw firestore as a database rather than as the\n * same grey box as every other external. Absent on everything else.\n */\n externalSystem?: ExternalSystemDeclaration;\n}\n\nexport interface RuntimeEdge {\n from: string;\n to: string;\n via: string[];\n /**\n * Transport of this edge. 'rpc' → a direct call arrow. 'pubsub' → the producer enqueues and the\n * consumer is delivered later, so the runtime viz draws it as producer → QUEUE → consumer.\n * Edges are split by transport, so every edge is a single kind.\n */\n type?: ApiTransport;\n /**\n * `\"ApiClassName.methodName\"` — the queue this edge flows through. Present iff `type` is\n * 'pubsub'. Queues are per METHOD, not per service pair, because that is the unit Cloud Tasks\n * (and Terraform) actually create, so two services exchanging three queued methods are three\n * queues rather than one arrow.\n */\n queue?: string;\n /**\n * True when the CALLING project carries a `cutLegacyCycle:<target>` nx tag naming this edge's\n * target — an admission that this hop closes a REAL cycle that is being tolerated as legacy\n * debt, not a claim that it is harmless. The edge is still drawn (dashed, labelled \"legacy\n * cycle\"); it is only excluded from leveling and cycle detection. Absent on every other edge.\n */\n cutLegacyCycle?: boolean;\n}\n\n/**\n * One Cloud Tasks queue: the async seam between a producer and a consumer, at METHOD granularity.\n *\n * `producedBy` and `consumedBy` are deliberately not symmetric in confidence — see\n * {@link ApiRef.methodsInferred}. The consumer is derived from `addRoutes` plus the contract's\n * method table and is exact; the producer is attributed to every queued method of the contract it\n * built a client for, because which methods it enqueues is not statically recoverable.\n */\nexport interface RuntimeQueue {\n api: string;\n method: string;\n /** `@Queue(...)` override, else `${Api}-${method}` — the name Terraform must match 1:1. */\n queueName: string;\n producedBy: string[];\n consumedBy: string[];\n}\n\n/**\n * An endpoint driven by something that is NOT an in-repo caller — a clock or an outside system.\n * These never appear as runtime EDGES (there is no in-repo `from`), which is exactly why they were\n * invisible until now: a nightly sweep and a GCP push subscription are real runtime entry points\n * with real Terraform behind them, and the graph showed neither.\n */\nexport interface RuntimeTrigger {\n /** 'cron' → a scheduler fires it; 'external' → a system outside this repo posts to it. */\n kind: 'cron' | 'external';\n api: string;\n method: string;\n /** The service that SERVES the endpoint (the arrow's head). */\n service: string;\n /** Present for 'cron': the Cloud Scheduler job / queue name Terraform must match. */\n queueName?: string;\n /**\n * Present for 'external': WHO outside this repo posts to it, declared as\n * `@Endpoint(POST, p, WRITE, EXTERNAL, { calledBy: 'twilio' })`.\n *\n * The SAME `(kind,label)` an OUTBOUND {@link RuntimeExternalSystem} carries, so an inbound\n * `saas twilio` and an outbound `saas twilio` share a node identity and converge on ONE box —\n * `label` is the identity here too, not display text.\n *\n * Optional only for graphs generated BEFORE the caller was required; generation now fails rather\n * than emitting an `external` method without one.\n */\n caller?: ExternalSystemDeclaration;\n}\n\nexport interface RuntimeUnresolved {\n service: string;\n api: string;\n}\n\n/**\n * A system outside this repo that a service TALKS TO, drawn with a shape that says what it is\n * (a database as a cylinder, a bucket as a folder) instead of the one grey box every external\n * used to collapse into.\n *\n * Two declaration sites feed this, because the two real cases differ in whether a contract exists:\n *\n * - **Wrapped** — the repo has a vendor seam (`FirestoreAdminApi`), so the kind is declared with an\n * `@externalSystem <kind> [label]` JSDoc tag on the contract. A decorator cannot go on a TS\n * `interface`, and these seams are interfaces, so JSDoc is the only marker that fits in place.\n * - **Unwrapped** — the service opens the connection itself with no contract to mark (a `pg.Pool`,\n * a TypeORM `DataSource`), so the declaration is an `external:<kind>:<identity>` nx tag on that\n * project's project.json.\n *\n * `label` is the node IDENTITY, not just display text: two services declaring `postgres` converge on\n * ONE node with two arrows into it, rather than drawing a database each.\n */\nexport interface RuntimeExternalSystem {\n kind: ExternalSystemKind;\n /** Display name AND node identity — declarations sharing a label share a node. */\n label: string;\n /** Services with a direct arrow to it. No transitive fan-out: a tag speaks only for its project. */\n usedBy: string[];\n /** Contracts flowing over it. Empty for the unwrapped (tag-declared) case — there are none. */\n apis: string[];\n}\n\nexport interface RuntimeGraph {\n services: Record<string, RuntimeService>;\n apis: Record<string, RuntimeApi>;\n runtimeEdges: RuntimeEdge[];\n unresolvedUses: RuntimeUnresolved[];\n /** `\"Api.method\"` -> the queue between its producers and its consumers. */\n queues: Record<string, RuntimeQueue>;\n /** Clock- and outside-driven entry points, sorted for determinism. */\n triggers: RuntimeTrigger[];\n /**\n * Declared external systems, keyed by identity. Optional so every graph written before this\n * existed still parses — an absent map means \"nothing declared\", which renders exactly as before.\n */\n externalSystems?: Record<string, RuntimeExternalSystem>;\n}\n"]}
@@ -36,6 +36,7 @@ const api_relations_1 = require("./api-usage/api-relations");
36
36
  const runtime_graph_decls_1 = require("./runtime-graph-decls");
37
37
  const runtime_graph_sorters_1 = require("./runtime-graph-sorters");
38
38
  const external_systems_1 = require("./api-usage/external-systems");
39
+ const graph_products_1 = require("./graph-products");
39
40
  const runtime_graph_levels_1 = require("./runtime-graph-levels");
40
41
  // Persistence lives in runtime-graph-io.ts; re-exported for the same reason as the model types.
41
42
  var runtime_graph_io_1 = require("./runtime-graph-io");
@@ -172,6 +173,8 @@ class RuntimeGraphDeriver {
172
173
  queues: edgeResult.queues,
173
174
  triggers: this.buildTriggers(decls),
174
175
  };
176
+ // #1179: each product's services — its tagged ones plus everything they reach at runtime.
177
+ new graph_products_1.RuntimeProductMembership().apply(graph, this.projects);
175
178
  // A destination whose address arrives at RUNTIME is declared on its CONTRACT
176
179
  // (`@externalSystem runtime partner-webhooks`), so it needs no branch of its own here: it is
177
180
  // resolved by the same call every other declared system is, converges on one node when two
@@ -1 +1 @@
1
- {"version":3,"file":"runtime-graph.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/runtime-graph.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Runtime Graph\n *\n * Derives the runtime microservice graph SOLELY from architecture/dependencies.json\n * (the single source of truth): each project's `apiRelations` carry which api\n * classes it implements/uses and their transport (rpc | pubsub). Both\n * `architecture:generate` and `architecture:validate-runtime-architecture` call\n * the SAME `deriveRuntimeGraph`, so the committed graph and the validated graph\n * can never diverge.\n *\n * The runtime edge Z -> X (Z depends on X at runtime) is INFERRED: Z `uses` api\n * Y and X `implements` api Y. This edge does not exist in the compile-time\n * dependencies.json (both Z and X only compile-depend on the api library Y).\n *\n * WHICH X is decided by the call site, not by \"everyone who implements Y\", in\n * priority order: (1) a literal at the call site —\n * `createRpcClient(Y, new ClientConfig('helper-fsdb'))` — kept as\n * `ApiRef.targetService`; else (2) the calling project's declared `callsService`\n * (project.json metadata.webpieces.callsService), the symmetric half of\n * `serviceName` for the shared-library case where the client is built once from a\n * config field so no literal can sit at the call site; else (3) fan-out. A named\n * target from (1) or (2) is matched against each node's DECLARED `serviceName`.\n * Fanning an edge out to every implementer is catastrophic for a company-wide\n * contract registered in a shared library — it manufactures calls that cannot\n * happen, and cycles that do not exist. When a target cannot be resolved the old\n * fan-out still happens, but a warning names the call site (see\n * RuntimeGraphReport.warnings): a wrong-but-green graph is worse than a failing\n * one, so it must never degrade silently.\n */\n\nimport { sortGraphTopologically } from './graph-sorter';\nimport type { EnhancedGraph, GraphEntry } from './graph-sorter';\nimport type {\n ApiContracts,\n ApiMethodMeta,\n ApiRef,\n ApiTransport,\n ExternalSystemDecls,\n} from './api-usage/api-relations';\nimport { sortApiRefs } from './api-usage/api-relations';\nimport { RelationSink } from './runtime-graph-decls';\nimport type { ScanDecl } from './runtime-graph-decls';\nimport { dedupApiRefs, sortedQueues, sortedRecord, sortUnresolved } from './runtime-graph-sorters';\nimport { attachExternalSystems, resolveExternalSystems } from './api-usage/external-systems';\nimport type {\n RuntimeApi,\n RuntimeEdge,\n RuntimeExternalSystem,\n RuntimeGraph,\n RuntimeQueue,\n RuntimeService,\n RuntimeTrigger,\n RuntimeUnresolved,\n} from './runtime-graph-model';\nimport { toError } from '../toError';\n\n// The committed data shape lives in runtime-graph-model.ts; re-exported here so every existing\n// `from './runtime-graph'` import keeps working and there is still one obvious place to import from.\nexport type {\n RuntimeApi,\n RuntimeEdge,\n RuntimeExternalSystem,\n RuntimeGraph,\n RuntimeQueue,\n RuntimeService,\n RuntimeTrigger,\n RuntimeUnresolved,\n} from './runtime-graph-model';\nimport { adjacencyFromEdges, applyCycleCuts, assignLevels } from './runtime-graph-levels';\n\n// Persistence lives in runtime-graph-io.ts; re-exported for the same reason as the model types.\nexport {\n DEFAULT_RUNTIME_GRAPH_PATH,\n saveRuntimeGraph,\n runtimeGraphFileExists,\n loadRuntimeGraph,\n serializeRuntimeGraph,\n} from './runtime-graph-io';\n\ninterface EdgeResult {\n edges: RuntimeEdge[];\n unresolved: RuntimeUnresolved[];\n queues: Record<string, RuntimeQueue>;\n}\n\n/**\n * The derived graph PLUS everything the derivation had to guess at. `warnings` is deliberately not\n * part of RuntimeGraph: it is not committed data, it is the report that stops a guessed edge from\n * passing for a derived one. Executors print it.\n */\nexport class RuntimeGraphReport {\n constructor(\n public readonly graph: RuntimeGraph,\n /** Human-readable lines naming every call site whose target the graph could not pin down. */\n public readonly warnings: string[],\n /**\n * Call sites that name a target the repo does not contain. Unlike a warning these FAIL the\n * build: the contract IS served in-repo, so the name is a typo or a stale rename, and the\n * only reason it stayed invisible is that the graph quietly fanned the edge out instead.\n * (A call to a service outside the repo never reaches here — nothing in-repo implements its\n * contract, so it is `unresolvedUses`.)\n */\n public readonly problems: string[] = [],\n /**\n * role:server nodes omitted from the DRAWING because they declare no webpieces runtime\n * package anywhere in their library closure and serve/call nothing in-repo. Neither a\n * warning nor a problem — this is intended behavior, and shouting on every clean run is how\n * warnings stop being read. Deliberately NOT persisted: a graph file that records its own\n * omissions gets committed and stops being read. Executors print it instead, so the\n * omission is always visible and never silent.\n */\n public readonly autoHidden: string[] = [],\n ) {}\n}\n\n/**\n * Derives the runtime microservice graph from architecture/dependencies.json `apiRelations`:\n * implementers × users per API, split by transport. An rpc edge is a direct call; a pubsub edge flows\n * through a queue (drawn producer → queue → consumer by the visualizer).\n *\n * Nodes are the runnable apps only — role:server / role:client. A library is NEVER a node; its\n * implements/uses (a shared client-factory or route-registration lib) are attributed to the\n * servers/clients that embed it, walked transitively over dependsOn (see collectDecls).\n */\nclass RuntimeGraphDeriver {\n /** apiClassName -> the api-lib project that owns the contract (from the apiRelations key). */\n private readonly apiOwners = new Map<string, string>();\n /** Addressable name -> the runtime node answering to it; how a targeted call resolves. */\n private readonly nodeByServiceName = new Map<string, string>();\n private readonly warnings: string[] = [];\n private readonly problems: string[] = [];\n /** role:server nodes hidden by isNonParticipantServer, in the order buildServices met them. */\n private readonly autoHidden: string[] = [];\n\n /**\n * False when NO project in the graph carries `webpiecesRuntime`, i.e. the file was written\n * before the field existed. Auto-hiding is then off ENTIRELY, so an old dependencies.json\n * renders exactly as it did — absence of the field must never be read as \"declares none\".\n * Any repo regenerated after this change has at least one webpieces project, so the guard\n * cannot wrongly suppress the feature.\n */\n private readonly markersKnown: boolean;\n\n constructor(\n private readonly projects: EnhancedGraph,\n /** Project names tagged drawOnGraph:false — kept in the JSON but flagged so the viz omits them. */\n private readonly hiddenProjects: Set<string>,\n /**\n * The per-contract method tables, read from the generated `architecture/apis/<Api>.json`\n * files (ApiContractFiles). A contract with no entry — no file, or no endpoints — makes every\n * pubsub edge through it fall back to one unnamed queue per service pair instead of failing.\n */\n private readonly apiContracts: ApiContracts = {},\n /**\n * Declared external systems from dependencies.json, keyed by identity. Empty means nothing\n * was declared, which renders exactly as it did before declarations existed.\n */\n private readonly externalSystemDecls: ExternalSystemDecls = {},\n ) {\n this.markersKnown = Object.values(projects).some(\n (entry: GraphEntry) => entry.webpiecesRuntime !== undefined,\n );\n // A node ALWAYS answers to its own module name, so a repo whose deployed names match its\n // project names needs no declaration at all — and no alias can ever redirect 'ai-chat' away\n // from the ai-chat module. Module names are therefore claimed FIRST and are unshadowable.\n for (const name of Object.keys(projects).sort()) {\n if (this.isNode(name)) this.nodeByServiceName.set(name, name);\n }\n // `serviceName` then adds the alias a repo needs when its deployed name is NOT its module\n // name (helper-svr serving 'helper-portal'). Colliding with a module is a misconfiguration,\n // not a precedence question: the alias is silently unreachable, so say so.\n for (const name of Object.keys(projects).sort()) {\n const declared = projects[name].serviceName;\n if (!this.isNode(name) || declared === undefined) continue;\n const claimant = this.nodeByServiceName.get(declared);\n if (claimant === undefined) {\n this.nodeByServiceName.set(declared, name);\n } else if (claimant !== name) {\n this.problems.push(\n `${name} declares serviceName '${declared}', but that is already the module name of ` +\n `${claimant} — the alias can never be reached. Rename one of them.`,\n );\n }\n }\n }\n\n assemble(): RuntimeGraphReport {\n const decls = this.collectDecls();\n const apis = this.buildApis(decls);\n const edgeResult = this.buildEdges(decls, apis);\n // Cuts are stamped BEFORE buildServices, which is where leveling reads the edges.\n this.problems.push(\n ...applyCycleCuts(edgeResult.edges, this.projects, this.nodeByServiceName),\n );\n const services = this.buildServices(decls, edgeResult.edges);\n const apisObj: Record<string, RuntimeApi> = {};\n for (const api of Array.from(apis.keys()).sort()) apisObj[api] = apis.get(api)!;\n const graph: RuntimeGraph = {\n services,\n apis: apisObj,\n runtimeEdges: edgeResult.edges,\n unresolvedUses: edgeResult.unresolved,\n queues: edgeResult.queues,\n triggers: this.buildTriggers(decls),\n };\n // A destination whose address arrives at RUNTIME is declared on its CONTRACT\n // (`@externalSystem runtime partner-webhooks`), so it needs no branch of its own here: it is\n // resolved by the same call every other declared system is, converges on one node when two\n // services deliver over the same contract, and is watched by the same drift check.\n const systems = resolveExternalSystems(this.externalSystemDecls, services);\n attachExternalSystems(graph, systems);\n return new RuntimeGraphReport(\n graph,\n this.warnings,\n this.problems,\n [...this.autoHidden].sort(),\n );\n }\n\n /**\n * The clock- and outside-driven entry points: for every contract a node IMPLEMENTS, each method\n * declared `cron` or `external` becomes a trigger pointing AT that node.\n *\n * Driven off `implements` rather than `uses` on purpose — these have no in-repo caller at all,\n * which is why they never produced an edge and stayed invisible.\n */\n private buildTriggers(decls: ScanDecl[]): RuntimeTrigger[] {\n const triggers: RuntimeTrigger[] = [];\n for (const decl of decls) {\n for (const ref of decl.implementsApis) {\n for (const method of this.methodsOf(ref.api)) {\n if (method.kind !== 'cron' && method.kind !== 'external') continue;\n const trigger: RuntimeTrigger = {\n kind: method.kind,\n api: ref.api,\n method: method.name,\n service: decl.name,\n };\n if (method.kind === 'cron') trigger.queueName = method.queueName;\n // ONLY for 'external' (a clock needs no vendor name), and only when declared —\n // a graph committed before callers were required has none and must still render.\n if (method.kind === 'external' && method.caller !== undefined)\n trigger.caller = method.caller;\n triggers.push(trigger);\n }\n }\n }\n return triggers.sort(\n (a: RuntimeTrigger, b: RuntimeTrigger) =>\n a.kind.localeCompare(b.kind) ||\n a.api.localeCompare(b.api) ||\n a.method.localeCompare(b.method) ||\n a.service.localeCompare(b.service),\n );\n }\n\n /** The committed method table for a contract, or [] when it declares none (e.g. a vendor seam). */\n private methodsOf(api: string): ApiMethodMeta[] {\n return this.apiContracts[api]?.methods ?? [];\n }\n\n /**\n * One ScanDecl per RUNTIME NODE (role:server / role:client). A node's effective\n * relations are its OWN apiRelations PLUS those of every library in its transitive\n * dependsOn closure: a lib that calls createRpcClient/addRoutes runs inside the\n * server/client that embeds it, so the relation is attributed to that node — the\n * lib is never a runtime node. Recursion stops at other nodes (each server/client\n * owns its own relations). dependsOn is transitively reduced, but reduction\n * preserves reachability, so the recursive walk still reaches every lib a node\n * embeds through libraries.\n */\n private collectDecls(): ScanDecl[] {\n const decls: ScanDecl[] = [];\n for (const name of Object.keys(this.projects).sort()) {\n if (!this.isNode(name)) continue;\n const sink = new RelationSink(name);\n this.collectEffectiveRelations(name, sink, new Set<string>([name]));\n // EVERY node gets a decl, including one with zero relations. Gating this on \"has at\n // least one implements/uses\" silently DELETED any deployed server whose work arrives\n // from outside the repo (a pull subscription): the diagram looked complete while missing\n // a running service — confidently wrong, not visibly incomplete. isNode() is the whole\n // test; drawOnGraph:false is the ONE opt-out, and such a node renders as an isolated box.\n decls.push({\n name,\n implementsApis: dedupApiRefs(sortApiRefs(sink.implementsApis)),\n usesApis: dedupApiRefs(sortApiRefs(sink.usesApis)),\n implementsVia: sink.implementsVia,\n markerVia: sink.markerVia,\n });\n }\n return decls;\n }\n\n /** True when a project is a runtime node — a server or client app (never a library). */\n private isNode(name: string): boolean {\n const role = this.projects[name]?.role;\n return role === 'server' || role === 'client';\n }\n\n /**\n * True for a role:server that speaks NO webpieces runtime package anywhere in its library\n * closure AND serves/calls no in-repo contract — a legacy Express/NestJS service sitting in the\n * same monorepo. This graph is built entirely out of webpieces contracts, so such a service can\n * only ever draw as a disconnected box: it has no edges by construction and never will.\n *\n * It stays a NODE and stays in runtime-dependencies.json (drawOnGraph:false), which is the whole\n * difference between hiding it and the silent deletion #542 removed — the data view still shows\n * a deployed role:server, only the picture drops it. Removing it from isNode() instead would\n * also drop it from nodeByServiceName, so a targeted ClientConfig('orders-manager') from a drawn\n * service would stop resolving and decay into an unresolved use: a graph that got quietly WORSE\n * while looking cleaner. The COMPILE-TIME graph (dependencies.html) still draws these projects;\n * they genuinely exist, and only the runtime drawing claims to show webpieces services.\n *\n * role:client is deliberately never auto-hidden: a browser app can legitimately declare no\n * marker at all, and nothing checks clients for presence the way checkServersPresent checks\n * servers, so there is no equivalent failure to prevent.\n */\n private isNonParticipantServer(decl: ScanDecl): boolean {\n if (!this.markersKnown) return false;\n if (this.projects[decl.name]?.role !== 'server') return false;\n if (decl.markerVia.size > 0) return false;\n // Belt-and-braces: serving or calling an in-repo contract IS participation, whatever the\n // package list says. This is what keeps a mechanism we did not anticipate from being blanked.\n return decl.implementsApis.length === 0 && decl.usesApis.length === 0;\n }\n\n /**\n * Accumulate `name`'s own api relations, then recurse through its LIBRARY deps\n * (skipping other nodes, which own their own relations). `visited` guards against\n * re-walking a lib reachable by more than one path (and any cycle).\n */\n private collectEffectiveRelations(\n name: string,\n sink: RelationSink,\n visited: Set<string>,\n includeRelations: boolean = true,\n ): void {\n const entry = this.projects[name];\n if (entry === undefined) return;\n sink.addMarkers(entry.webpiecesRuntime, name);\n const relations = entry.apiRelations;\n if (includeRelations && relations !== undefined) {\n for (const owner of Object.keys(relations).sort()) {\n for (const ref of relations[owner].implements) {\n this.apiOwners.set(ref.api, owner);\n sink.addImplements(ref, ref.declaredVia ?? name);\n }\n for (const ref of relations[owner].uses) {\n this.apiOwners.set(ref.api, owner);\n sink.addUses(ref);\n }\n }\n }\n for (const dep of entry.dependsOn) {\n if (visited.has(dep)) continue;\n visited.add(dep);\n if (this.isNode(dep)) continue; // another server/client owns its own relations\n this.collectEffectiveRelations(\n dep,\n sink,\n visited,\n includeRelations && entry.runtimeComposition !== true,\n );\n }\n }\n\n /** apiClassName -> { implementedBy, usedBy, type }. */\n private buildApis(decls: ScanDecl[]): Map<string, RuntimeApi> {\n const apis = new Map<string, RuntimeApi>();\n const ensure = (api: string, type: ApiTransport): RuntimeApi => {\n let entry = apis.get(api);\n if (!entry) {\n entry = { implementedBy: [], usedBy: [], type };\n apis.set(api, entry);\n }\n return entry;\n };\n for (const decl of decls) {\n for (const ref of decl.implementsApis)\n ensure(ref.api, ref.type).implementedBy.push(decl.name);\n for (const ref of decl.usesApis) ensure(ref.api, ref.type).usedBy.push(decl.name);\n }\n for (const api of apis.keys()) {\n const entry = apis.get(api)!;\n entry.implementedBy.sort();\n entry.usedBy = Array.from(new Set(entry.usedBy)).sort();\n const owner = this.apiOwners.get(api);\n if (owner !== undefined) entry.owner = owner;\n }\n return apis;\n }\n\n /** Inferred edges U -> I via api A, split by transport; plus uses with no implementer. */\n private buildEdges(decls: ScanDecl[], apis: Map<string, RuntimeApi>): EdgeResult {\n const viaByKey = new Map<string, Set<string>>();\n const unresolved: RuntimeUnresolved[] = [];\n const queues = new Map<string, RuntimeQueue>();\n for (const decl of decls) {\n for (const ref of decl.usesApis) {\n const implementers = apis.get(ref.api)?.implementedBy ?? [];\n if (implementers.length === 0) {\n // Nobody in-repo serves it. For a vendor contract that is the ANSWER, not a gap:\n // the call leaves the repo, and the viz terminates it at a dashed vendor node.\n unresolved.push({ service: decl.name, api: ref.api });\n continue;\n }\n for (const target of this.targetsFor(decl.name, ref, implementers)) {\n // A service calling ITSELF synchronously is noise; a service ENQUEUEING to\n // itself is a real, common topology (deferring its own work), and dropping it\n // was hiding the single most interesting thing about a queue.\n if (target === decl.name && ref.type !== 'pubsub') continue;\n if (ref.type === 'pubsub') {\n this.addQueuedEdges(decl.name, target, ref.api, viaByKey, queues);\n continue;\n }\n const key = `${decl.name} ${target} ${ref.type}`;\n if (!viaByKey.has(key)) viaByKey.set(key, new Set());\n viaByKey.get(key)!.add(ref.api);\n }\n }\n }\n return {\n edges: this.edgesFromKeys(viaByKey),\n unresolved: sortUnresolved(unresolved),\n queues: sortedQueues(queues),\n };\n }\n\n /**\n * One edge PER QUEUED METHOD of `api`, plus the queue each flows through.\n *\n * Per method rather than per service pair because that is the unit Cloud Tasks and Terraform\n * create: two services exchanging three queued methods run three independently-configured,\n * independently-backed-up queues, and collapsing them into one arrow hides which one is stuck.\n *\n * A contract with no method table (no `architecture/apis/<Api>.json` for it)\n * degrades to a single unnamed queue for the pair — the old behavior — rather than vanishing.\n */\n private addQueuedEdges(\n from: string,\n to: string,\n api: string,\n viaByKey: Map<string, Set<string>>,\n queues: Map<string, RuntimeQueue>,\n ): void {\n const queued = this.methodsOf(api).filter((m: ApiMethodMeta) => m.kind === 'cloudtasks');\n if (queued.length === 0) {\n const key = `${from} ${to} pubsub `;\n if (!viaByKey.has(key)) viaByKey.set(key, new Set());\n viaByKey.get(key)!.add(api);\n return;\n }\n for (const method of queued) {\n const queueKey = `${api}.${method.name}`;\n const key = `${from} ${to} pubsub ${queueKey}`;\n if (!viaByKey.has(key)) viaByKey.set(key, new Set());\n viaByKey.get(key)!.add(api);\n\n let queue = queues.get(queueKey);\n if (queue === undefined) {\n queue = {\n api,\n method: method.name,\n // Every cloudtasks method carries a queueName; the fallback covers a\n // dependencies.json written before queueName became kind-specific.\n queueName: method.queueName ?? `${api}-${method.name}`,\n producedBy: [],\n consumedBy: [],\n };\n queues.set(queueKey, queue);\n }\n if (!queue.producedBy.includes(from)) queue.producedBy.push(from);\n if (!queue.consumedBy.includes(to)) queue.consumedBy.push(to);\n }\n }\n\n /**\n * WHICH implementers this one `uses` reaches. Resolution order (most specific wins):\n * 1. a literal `ClientConfig` at the call site (`ref.targetService`) — names ONE node;\n * 2. else the calling project's declared `callsService` (project.json) — for the shared-library\n * case where the literal cannot sit at the call site, it lives one indirection away in the app;\n * 3. else the historical fan-out — the only safe superset — recording WHY, so a fanned-out (i.e.\n * possibly fictional) edge can never pass for a derived one.\n * A named target (1 or 2) that resolves to no module, or to a module that does not serve the\n * contract, is a PROBLEM (fails the build), never a silent drop.\n */\n private targetsFor(user: string, ref: ApiRef, implementers: string[]): string[] {\n const literal = ref.targetService;\n if (literal !== undefined)\n return this.resolveNamedTarget(user, ref, implementers, literal, 'literal');\n\n const declared = this.callsServiceFor(user, ref.api);\n if (declared !== undefined)\n return this.resolveNamedTarget(user, ref, implementers, declared, 'callsService');\n\n return this.untargetedFanOut(user, ref, implementers);\n }\n\n /**\n * The declared `callsService` target for this node's use of `api`, or undefined when none applies.\n * A string declaration aims every untargeted use at one service; a map aims per api-class.\n */\n private callsServiceFor(user: string, api: string): string | undefined {\n const declared = this.projects[user]?.callsService;\n if (declared === undefined) return undefined;\n if (typeof declared === 'string') return declared;\n return declared[api];\n }\n\n /**\n * Resolve a NAMED target (from a call-site literal or a project-level `callsService`) to the one\n * module that serves it. A name no module answers to, or a module that does not serve the\n * contract, fails the build — the wording names which declaration to fix, `source` deciding it.\n */\n private resolveNamedTarget(\n user: string,\n ref: ApiRef,\n implementers: string[],\n wanted: string,\n source: 'literal' | 'callsService',\n ): string[] {\n const node = this.nodeByServiceName.get(wanted);\n const served = `${implementers.length} module(s) serve this contract (${implementers.join(', ')})`;\n if (node === undefined) {\n this.problems.push(\n source === 'callsService'\n ? `${user} declares metadata.webpieces.callsService '${wanted}' in its project.json, but NO ` +\n `module answers to that name. ${served}. Point callsService at a module name, or declare ` +\n `metadata.webpieces.serviceName: '${wanted}' on the module that serves \"${ref.api}\".`\n : `${user} calls \"${ref.api}\" at service '${wanted}', but NO module answers to that name. ` +\n `${served}. Either use the module name, or declare metadata.webpieces.serviceName: ` +\n `'${wanted}' on the module that serves it (and translate any environment prefix in ` +\n `ClientRegistry.setDeriver, not here).`,\n );\n return implementers;\n }\n if (!implementers.includes(node)) {\n const via =\n source === 'callsService'\n ? `${user} declares callsService '${wanted}' (module ${node})`\n : `${user} calls \"${ref.api}\" at service '${wanted}' (module ${node})`;\n this.problems.push(\n `${via}, but ${node} does NOT serve \"${ref.api}\" — ${implementers.join(', ')} do. At runtime that ` +\n `call has nothing to answer it.`,\n );\n return implementers;\n }\n return [node];\n }\n\n /**\n * A use with no literal client config. One implementer is unambiguous, so it stays silent; more\n * than one means every edge but one is fiction — exactly the failure this mechanism exists to\n * stop — so it is reported even though the graph still (conservatively) draws them all.\n */\n private untargetedFanOut(user: string, ref: ApiRef, implementers: string[]): string[] {\n if (implementers.length > 1) {\n this.warnings.push(\n `${user} uses \"${ref.api}\" with no literal client config, and ${implementers.length} services ` +\n `implement it (${implementers.join(', ')}) — an edge is drawn to EVERY one, so all but one ` +\n `are fiction. Name the target: createRpcClient(${ref.api}, new ClientConfig('<serviceName>', ClientRole.SERVER)); ` +\n `or, when the client is built in a shared library (no literal can sit at the call site), ` +\n `declare metadata.webpieces.callsService: '<serviceName>' on ${user}'s project.json.`,\n );\n }\n return implementers;\n }\n\n /**\n * Rebuild edges from their `\"from to type queue\"` keys. The 4th part is the queue (\"Api.method\")\n * and is empty for every non-queued edge; it is part of the KEY so two queued methods between\n * the same pair stay two edges instead of collapsing into one.\n */\n private edgesFromKeys(viaByKey: Map<string, Set<string>>): RuntimeEdge[] {\n const edges: RuntimeEdge[] = [];\n for (const key of viaByKey.keys()) {\n const parts = key.split(' ');\n const edge: RuntimeEdge = {\n from: parts[0],\n to: parts[1],\n via: Array.from(viaByKey.get(key)!).sort(),\n type: parts[2] as ApiTransport,\n };\n if (parts[3] !== undefined && parts[3] !== '') edge.queue = parts[3];\n edges.push(edge);\n }\n edges.sort(\n (a: RuntimeEdge, b: RuntimeEdge) =>\n a.from.localeCompare(b.from) ||\n a.to.localeCompare(b.to) ||\n (a.type ?? '').localeCompare(b.type ?? '') ||\n (a.queue ?? '').localeCompare(b.queue ?? ''),\n );\n return edges;\n }\n\n private buildServices(decls: ScanDecl[], edges: RuntimeEdge[]): Record<string, RuntimeService> {\n const services: Record<string, RuntimeService> = {};\n for (const decl of decls) {\n // A queued hop depends on the QUEUE, not on the peer: the producer hands off and returns,\n // so naming the consumer here would assert a coupling that does not exist (and would make\n // a service that defers work to itself look self-dependent).\n const dependsOn = Array.from(\n new Set(\n edges\n .filter((e: RuntimeEdge) => e.from === decl.name)\n .map((e: RuntimeEdge) =>\n e.queue === undefined ? e.to : `queue:${e.queue}`,\n ),\n ),\n ).sort();\n // Keys are written in this order; an undefined value is omitted by JSON.stringify, so\n // the committed JSON stays clean AND deterministic without conditional assembly.\n const service: RuntimeService = {\n level: 0,\n role: this.projects[decl.name]?.role, // labels a relation-less node for what it IS\n serviceName: this.projects[decl.name]?.serviceName,\n callsService: this.projects[decl.name]?.callsService,\n implements: decl.implementsApis.map((r: ApiRef) => r.api),\n implementsVia:\n decl.implementsVia.size > 0 ? sortedRecord(decl.implementsVia) : undefined,\n // One api used against two services is two refs but ONE api in this list.\n uses: Array.from(new Set(decl.usesApis.map((r: ApiRef) => r.api))),\n dependsOn,\n wiringUses: decl.usesApis.some((ref) => ref.declaredVia !== undefined)\n ? decl.usesApis\n : undefined,\n wiringImplements: decl.implementsApis.some((ref) => ref.declaredVia !== undefined)\n ? decl.implementsApis\n : undefined,\n };\n services[decl.name] = service;\n // The explicit tag wins and is NOT reported as auto-hidden — somebody asked for it.\n if (this.hiddenProjects.has(decl.name)) {\n service.drawOnGraph = false;\n } else if (this.isNonParticipantServer(decl)) {\n service.drawOnGraph = false;\n this.autoHidden.push(decl.name);\n }\n }\n const levels = assignLevels(adjacencyFromEdges(Object.keys(services), edges));\n for (const name of Object.keys(services)) services[name].level = levels[name] ?? 0;\n return services;\n }\n}\n\n/**\n * Derive the runtime graph from dependencies.json's project `apiRelations` — the\n * single source of truth shared by generate + validate. `hiddenProjects`\n * (drawOnGraph:false, defaults to none) are kept in the graph but flagged so the\n * runtime visualizer omits their nodes + edges.\n */\n// webpieces-disable no-function-outside-class -- module entry point for the runtime graph derivation\nexport function deriveRuntimeGraph(\n projects: EnhancedGraph,\n hiddenProjects: Set<string> = new Set<string>(),\n apiContracts: ApiContracts = {},\n): RuntimeGraph {\n return deriveRuntimeGraphReport(projects, hiddenProjects, apiContracts).graph;\n}\n\n/**\n * The same derivation, plus the warnings it produced (every edge it had to GUESS at). Executors use\n * this form and print the warnings; `deriveRuntimeGraph` is the convenience form for callers that\n * only want the data. The warnings are deliberately kept OUT of runtime-dependencies.json — a graph\n * file that records its own doubts would just get committed and stop being read.\n */\n// webpieces-disable no-function-outside-class -- module entry point for the runtime graph derivation\nexport function deriveRuntimeGraphReport(\n projects: EnhancedGraph,\n hiddenProjects: Set<string> = new Set<string>(),\n apiContracts: ApiContracts = {},\n externalSystems: ExternalSystemDecls = {},\n): RuntimeGraphReport {\n return new RuntimeGraphDeriver(\n projects,\n hiddenProjects,\n apiContracts,\n externalSystems,\n ).assemble();\n}\n\n// Levels + adjacency live in runtime-graph-levels.ts; re-exported for the same reason the model types\n// and the io helpers above are — one obvious place to import the runtime graph from.\nexport { runtimeAdjacency } from './runtime-graph-levels';\n"]}
1
+ {"version":3,"file":"runtime-graph.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/runtime-graph.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Runtime Graph\n *\n * Derives the runtime microservice graph SOLELY from architecture/dependencies.json\n * (the single source of truth): each project's `apiRelations` carry which api\n * classes it implements/uses and their transport (rpc | pubsub). Both\n * `architecture:generate` and `architecture:validate-runtime-architecture` call\n * the SAME `deriveRuntimeGraph`, so the committed graph and the validated graph\n * can never diverge.\n *\n * The runtime edge Z -> X (Z depends on X at runtime) is INFERRED: Z `uses` api\n * Y and X `implements` api Y. This edge does not exist in the compile-time\n * dependencies.json (both Z and X only compile-depend on the api library Y).\n *\n * WHICH X is decided by the call site, not by \"everyone who implements Y\", in\n * priority order: (1) a literal at the call site —\n * `createRpcClient(Y, new ClientConfig('helper-fsdb'))` — kept as\n * `ApiRef.targetService`; else (2) the calling project's declared `callsService`\n * (project.json metadata.webpieces.callsService), the symmetric half of\n * `serviceName` for the shared-library case where the client is built once from a\n * config field so no literal can sit at the call site; else (3) fan-out. A named\n * target from (1) or (2) is matched against each node's DECLARED `serviceName`.\n * Fanning an edge out to every implementer is catastrophic for a company-wide\n * contract registered in a shared library — it manufactures calls that cannot\n * happen, and cycles that do not exist. When a target cannot be resolved the old\n * fan-out still happens, but a warning names the call site (see\n * RuntimeGraphReport.warnings): a wrong-but-green graph is worse than a failing\n * one, so it must never degrade silently.\n */\n\nimport { sortGraphTopologically } from './graph-sorter';\nimport type { EnhancedGraph, GraphEntry } from './graph-sorter';\nimport type {\n ApiContracts,\n ApiMethodMeta,\n ApiRef,\n ApiTransport,\n ExternalSystemDecls,\n} from './api-usage/api-relations';\nimport { sortApiRefs } from './api-usage/api-relations';\nimport { RelationSink } from './runtime-graph-decls';\nimport type { ScanDecl } from './runtime-graph-decls';\nimport { dedupApiRefs, sortedQueues, sortedRecord, sortUnresolved } from './runtime-graph-sorters';\nimport { attachExternalSystems, resolveExternalSystems } from './api-usage/external-systems';\nimport { RuntimeProductMembership } from './graph-products';\nimport type {\n RuntimeApi,\n RuntimeEdge,\n RuntimeExternalSystem,\n RuntimeGraph,\n RuntimeQueue,\n RuntimeService,\n RuntimeTrigger,\n RuntimeUnresolved,\n} from './runtime-graph-model';\nimport { toError } from '../toError';\n\n// The committed data shape lives in runtime-graph-model.ts; re-exported here so every existing\n// `from './runtime-graph'` import keeps working and there is still one obvious place to import from.\nexport type {\n RuntimeApi,\n RuntimeEdge,\n RuntimeExternalSystem,\n RuntimeGraph,\n RuntimeQueue,\n RuntimeService,\n RuntimeTrigger,\n RuntimeUnresolved,\n} from './runtime-graph-model';\nimport { adjacencyFromEdges, applyCycleCuts, assignLevels } from './runtime-graph-levels';\n\n// Persistence lives in runtime-graph-io.ts; re-exported for the same reason as the model types.\nexport {\n DEFAULT_RUNTIME_GRAPH_PATH,\n saveRuntimeGraph,\n runtimeGraphFileExists,\n loadRuntimeGraph,\n serializeRuntimeGraph,\n} from './runtime-graph-io';\n\ninterface EdgeResult {\n edges: RuntimeEdge[];\n unresolved: RuntimeUnresolved[];\n queues: Record<string, RuntimeQueue>;\n}\n\n/**\n * The derived graph PLUS everything the derivation had to guess at. `warnings` is deliberately not\n * part of RuntimeGraph: it is not committed data, it is the report that stops a guessed edge from\n * passing for a derived one. Executors print it.\n */\nexport class RuntimeGraphReport {\n constructor(\n public readonly graph: RuntimeGraph,\n /** Human-readable lines naming every call site whose target the graph could not pin down. */\n public readonly warnings: string[],\n /**\n * Call sites that name a target the repo does not contain. Unlike a warning these FAIL the\n * build: the contract IS served in-repo, so the name is a typo or a stale rename, and the\n * only reason it stayed invisible is that the graph quietly fanned the edge out instead.\n * (A call to a service outside the repo never reaches here — nothing in-repo implements its\n * contract, so it is `unresolvedUses`.)\n */\n public readonly problems: string[] = [],\n /**\n * role:server nodes omitted from the DRAWING because they declare no webpieces runtime\n * package anywhere in their library closure and serve/call nothing in-repo. Neither a\n * warning nor a problem — this is intended behavior, and shouting on every clean run is how\n * warnings stop being read. Deliberately NOT persisted: a graph file that records its own\n * omissions gets committed and stops being read. Executors print it instead, so the\n * omission is always visible and never silent.\n */\n public readonly autoHidden: string[] = [],\n ) {}\n}\n\n/**\n * Derives the runtime microservice graph from architecture/dependencies.json `apiRelations`:\n * implementers × users per API, split by transport. An rpc edge is a direct call; a pubsub edge flows\n * through a queue (drawn producer → queue → consumer by the visualizer).\n *\n * Nodes are the runnable apps only — role:server / role:client. A library is NEVER a node; its\n * implements/uses (a shared client-factory or route-registration lib) are attributed to the\n * servers/clients that embed it, walked transitively over dependsOn (see collectDecls).\n */\nclass RuntimeGraphDeriver {\n /** apiClassName -> the api-lib project that owns the contract (from the apiRelations key). */\n private readonly apiOwners = new Map<string, string>();\n /** Addressable name -> the runtime node answering to it; how a targeted call resolves. */\n private readonly nodeByServiceName = new Map<string, string>();\n private readonly warnings: string[] = [];\n private readonly problems: string[] = [];\n /** role:server nodes hidden by isNonParticipantServer, in the order buildServices met them. */\n private readonly autoHidden: string[] = [];\n\n /**\n * False when NO project in the graph carries `webpiecesRuntime`, i.e. the file was written\n * before the field existed. Auto-hiding is then off ENTIRELY, so an old dependencies.json\n * renders exactly as it did — absence of the field must never be read as \"declares none\".\n * Any repo regenerated after this change has at least one webpieces project, so the guard\n * cannot wrongly suppress the feature.\n */\n private readonly markersKnown: boolean;\n\n constructor(\n private readonly projects: EnhancedGraph,\n /** Project names tagged drawOnGraph:false — kept in the JSON but flagged so the viz omits them. */\n private readonly hiddenProjects: Set<string>,\n /**\n * The per-contract method tables, read from the generated `architecture/apis/<Api>.json`\n * files (ApiContractFiles). A contract with no entry — no file, or no endpoints — makes every\n * pubsub edge through it fall back to one unnamed queue per service pair instead of failing.\n */\n private readonly apiContracts: ApiContracts = {},\n /**\n * Declared external systems from dependencies.json, keyed by identity. Empty means nothing\n * was declared, which renders exactly as it did before declarations existed.\n */\n private readonly externalSystemDecls: ExternalSystemDecls = {},\n ) {\n this.markersKnown = Object.values(projects).some(\n (entry: GraphEntry) => entry.webpiecesRuntime !== undefined,\n );\n // A node ALWAYS answers to its own module name, so a repo whose deployed names match its\n // project names needs no declaration at all — and no alias can ever redirect 'ai-chat' away\n // from the ai-chat module. Module names are therefore claimed FIRST and are unshadowable.\n for (const name of Object.keys(projects).sort()) {\n if (this.isNode(name)) this.nodeByServiceName.set(name, name);\n }\n // `serviceName` then adds the alias a repo needs when its deployed name is NOT its module\n // name (helper-svr serving 'helper-portal'). Colliding with a module is a misconfiguration,\n // not a precedence question: the alias is silently unreachable, so say so.\n for (const name of Object.keys(projects).sort()) {\n const declared = projects[name].serviceName;\n if (!this.isNode(name) || declared === undefined) continue;\n const claimant = this.nodeByServiceName.get(declared);\n if (claimant === undefined) {\n this.nodeByServiceName.set(declared, name);\n } else if (claimant !== name) {\n this.problems.push(\n `${name} declares serviceName '${declared}', but that is already the module name of ` +\n `${claimant} — the alias can never be reached. Rename one of them.`,\n );\n }\n }\n }\n\n assemble(): RuntimeGraphReport {\n const decls = this.collectDecls();\n const apis = this.buildApis(decls);\n const edgeResult = this.buildEdges(decls, apis);\n // Cuts are stamped BEFORE buildServices, which is where leveling reads the edges.\n this.problems.push(\n ...applyCycleCuts(edgeResult.edges, this.projects, this.nodeByServiceName),\n );\n const services = this.buildServices(decls, edgeResult.edges);\n const apisObj: Record<string, RuntimeApi> = {};\n for (const api of Array.from(apis.keys()).sort()) apisObj[api] = apis.get(api)!;\n const graph: RuntimeGraph = {\n services,\n apis: apisObj,\n runtimeEdges: edgeResult.edges,\n unresolvedUses: edgeResult.unresolved,\n queues: edgeResult.queues,\n triggers: this.buildTriggers(decls),\n };\n // #1179: each product's services — its tagged ones plus everything they reach at runtime.\n new RuntimeProductMembership().apply(graph, this.projects);\n // A destination whose address arrives at RUNTIME is declared on its CONTRACT\n // (`@externalSystem runtime partner-webhooks`), so it needs no branch of its own here: it is\n // resolved by the same call every other declared system is, converges on one node when two\n // services deliver over the same contract, and is watched by the same drift check.\n const systems = resolveExternalSystems(this.externalSystemDecls, services);\n attachExternalSystems(graph, systems);\n return new RuntimeGraphReport(\n graph,\n this.warnings,\n this.problems,\n [...this.autoHidden].sort(),\n );\n }\n\n /**\n * The clock- and outside-driven entry points: for every contract a node IMPLEMENTS, each method\n * declared `cron` or `external` becomes a trigger pointing AT that node.\n *\n * Driven off `implements` rather than `uses` on purpose — these have no in-repo caller at all,\n * which is why they never produced an edge and stayed invisible.\n */\n private buildTriggers(decls: ScanDecl[]): RuntimeTrigger[] {\n const triggers: RuntimeTrigger[] = [];\n for (const decl of decls) {\n for (const ref of decl.implementsApis) {\n for (const method of this.methodsOf(ref.api)) {\n if (method.kind !== 'cron' && method.kind !== 'external') continue;\n const trigger: RuntimeTrigger = {\n kind: method.kind,\n api: ref.api,\n method: method.name,\n service: decl.name,\n };\n if (method.kind === 'cron') trigger.queueName = method.queueName;\n // ONLY for 'external' (a clock needs no vendor name), and only when declared —\n // a graph committed before callers were required has none and must still render.\n if (method.kind === 'external' && method.caller !== undefined)\n trigger.caller = method.caller;\n triggers.push(trigger);\n }\n }\n }\n return triggers.sort(\n (a: RuntimeTrigger, b: RuntimeTrigger) =>\n a.kind.localeCompare(b.kind) ||\n a.api.localeCompare(b.api) ||\n a.method.localeCompare(b.method) ||\n a.service.localeCompare(b.service),\n );\n }\n\n /** The committed method table for a contract, or [] when it declares none (e.g. a vendor seam). */\n private methodsOf(api: string): ApiMethodMeta[] {\n return this.apiContracts[api]?.methods ?? [];\n }\n\n /**\n * One ScanDecl per RUNTIME NODE (role:server / role:client). A node's effective\n * relations are its OWN apiRelations PLUS those of every library in its transitive\n * dependsOn closure: a lib that calls createRpcClient/addRoutes runs inside the\n * server/client that embeds it, so the relation is attributed to that node — the\n * lib is never a runtime node. Recursion stops at other nodes (each server/client\n * owns its own relations). dependsOn is transitively reduced, but reduction\n * preserves reachability, so the recursive walk still reaches every lib a node\n * embeds through libraries.\n */\n private collectDecls(): ScanDecl[] {\n const decls: ScanDecl[] = [];\n for (const name of Object.keys(this.projects).sort()) {\n if (!this.isNode(name)) continue;\n const sink = new RelationSink(name);\n this.collectEffectiveRelations(name, sink, new Set<string>([name]));\n // EVERY node gets a decl, including one with zero relations. Gating this on \"has at\n // least one implements/uses\" silently DELETED any deployed server whose work arrives\n // from outside the repo (a pull subscription): the diagram looked complete while missing\n // a running service — confidently wrong, not visibly incomplete. isNode() is the whole\n // test; drawOnGraph:false is the ONE opt-out, and such a node renders as an isolated box.\n decls.push({\n name,\n implementsApis: dedupApiRefs(sortApiRefs(sink.implementsApis)),\n usesApis: dedupApiRefs(sortApiRefs(sink.usesApis)),\n implementsVia: sink.implementsVia,\n markerVia: sink.markerVia,\n });\n }\n return decls;\n }\n\n /** True when a project is a runtime node — a server or client app (never a library). */\n private isNode(name: string): boolean {\n const role = this.projects[name]?.role;\n return role === 'server' || role === 'client';\n }\n\n /**\n * True for a role:server that speaks NO webpieces runtime package anywhere in its library\n * closure AND serves/calls no in-repo contract — a legacy Express/NestJS service sitting in the\n * same monorepo. This graph is built entirely out of webpieces contracts, so such a service can\n * only ever draw as a disconnected box: it has no edges by construction and never will.\n *\n * It stays a NODE and stays in runtime-dependencies.json (drawOnGraph:false), which is the whole\n * difference between hiding it and the silent deletion #542 removed — the data view still shows\n * a deployed role:server, only the picture drops it. Removing it from isNode() instead would\n * also drop it from nodeByServiceName, so a targeted ClientConfig('orders-manager') from a drawn\n * service would stop resolving and decay into an unresolved use: a graph that got quietly WORSE\n * while looking cleaner. The COMPILE-TIME graph (dependencies.html) still draws these projects;\n * they genuinely exist, and only the runtime drawing claims to show webpieces services.\n *\n * role:client is deliberately never auto-hidden: a browser app can legitimately declare no\n * marker at all, and nothing checks clients for presence the way checkServersPresent checks\n * servers, so there is no equivalent failure to prevent.\n */\n private isNonParticipantServer(decl: ScanDecl): boolean {\n if (!this.markersKnown) return false;\n if (this.projects[decl.name]?.role !== 'server') return false;\n if (decl.markerVia.size > 0) return false;\n // Belt-and-braces: serving or calling an in-repo contract IS participation, whatever the\n // package list says. This is what keeps a mechanism we did not anticipate from being blanked.\n return decl.implementsApis.length === 0 && decl.usesApis.length === 0;\n }\n\n /**\n * Accumulate `name`'s own api relations, then recurse through its LIBRARY deps\n * (skipping other nodes, which own their own relations). `visited` guards against\n * re-walking a lib reachable by more than one path (and any cycle).\n */\n private collectEffectiveRelations(\n name: string,\n sink: RelationSink,\n visited: Set<string>,\n includeRelations: boolean = true,\n ): void {\n const entry = this.projects[name];\n if (entry === undefined) return;\n sink.addMarkers(entry.webpiecesRuntime, name);\n const relations = entry.apiRelations;\n if (includeRelations && relations !== undefined) {\n for (const owner of Object.keys(relations).sort()) {\n for (const ref of relations[owner].implements) {\n this.apiOwners.set(ref.api, owner);\n sink.addImplements(ref, ref.declaredVia ?? name);\n }\n for (const ref of relations[owner].uses) {\n this.apiOwners.set(ref.api, owner);\n sink.addUses(ref);\n }\n }\n }\n for (const dep of entry.dependsOn) {\n if (visited.has(dep)) continue;\n visited.add(dep);\n if (this.isNode(dep)) continue; // another server/client owns its own relations\n this.collectEffectiveRelations(\n dep,\n sink,\n visited,\n includeRelations && entry.runtimeComposition !== true,\n );\n }\n }\n\n /** apiClassName -> { implementedBy, usedBy, type }. */\n private buildApis(decls: ScanDecl[]): Map<string, RuntimeApi> {\n const apis = new Map<string, RuntimeApi>();\n const ensure = (api: string, type: ApiTransport): RuntimeApi => {\n let entry = apis.get(api);\n if (!entry) {\n entry = { implementedBy: [], usedBy: [], type };\n apis.set(api, entry);\n }\n return entry;\n };\n for (const decl of decls) {\n for (const ref of decl.implementsApis)\n ensure(ref.api, ref.type).implementedBy.push(decl.name);\n for (const ref of decl.usesApis) ensure(ref.api, ref.type).usedBy.push(decl.name);\n }\n for (const api of apis.keys()) {\n const entry = apis.get(api)!;\n entry.implementedBy.sort();\n entry.usedBy = Array.from(new Set(entry.usedBy)).sort();\n const owner = this.apiOwners.get(api);\n if (owner !== undefined) entry.owner = owner;\n }\n return apis;\n }\n\n /** Inferred edges U -> I via api A, split by transport; plus uses with no implementer. */\n private buildEdges(decls: ScanDecl[], apis: Map<string, RuntimeApi>): EdgeResult {\n const viaByKey = new Map<string, Set<string>>();\n const unresolved: RuntimeUnresolved[] = [];\n const queues = new Map<string, RuntimeQueue>();\n for (const decl of decls) {\n for (const ref of decl.usesApis) {\n const implementers = apis.get(ref.api)?.implementedBy ?? [];\n if (implementers.length === 0) {\n // Nobody in-repo serves it. For a vendor contract that is the ANSWER, not a gap:\n // the call leaves the repo, and the viz terminates it at a dashed vendor node.\n unresolved.push({ service: decl.name, api: ref.api });\n continue;\n }\n for (const target of this.targetsFor(decl.name, ref, implementers)) {\n // A service calling ITSELF synchronously is noise; a service ENQUEUEING to\n // itself is a real, common topology (deferring its own work), and dropping it\n // was hiding the single most interesting thing about a queue.\n if (target === decl.name && ref.type !== 'pubsub') continue;\n if (ref.type === 'pubsub') {\n this.addQueuedEdges(decl.name, target, ref.api, viaByKey, queues);\n continue;\n }\n const key = `${decl.name} ${target} ${ref.type}`;\n if (!viaByKey.has(key)) viaByKey.set(key, new Set());\n viaByKey.get(key)!.add(ref.api);\n }\n }\n }\n return {\n edges: this.edgesFromKeys(viaByKey),\n unresolved: sortUnresolved(unresolved),\n queues: sortedQueues(queues),\n };\n }\n\n /**\n * One edge PER QUEUED METHOD of `api`, plus the queue each flows through.\n *\n * Per method rather than per service pair because that is the unit Cloud Tasks and Terraform\n * create: two services exchanging three queued methods run three independently-configured,\n * independently-backed-up queues, and collapsing them into one arrow hides which one is stuck.\n *\n * A contract with no method table (no `architecture/apis/<Api>.json` for it)\n * degrades to a single unnamed queue for the pair — the old behavior — rather than vanishing.\n */\n private addQueuedEdges(\n from: string,\n to: string,\n api: string,\n viaByKey: Map<string, Set<string>>,\n queues: Map<string, RuntimeQueue>,\n ): void {\n const queued = this.methodsOf(api).filter((m: ApiMethodMeta) => m.kind === 'cloudtasks');\n if (queued.length === 0) {\n const key = `${from} ${to} pubsub `;\n if (!viaByKey.has(key)) viaByKey.set(key, new Set());\n viaByKey.get(key)!.add(api);\n return;\n }\n for (const method of queued) {\n const queueKey = `${api}.${method.name}`;\n const key = `${from} ${to} pubsub ${queueKey}`;\n if (!viaByKey.has(key)) viaByKey.set(key, new Set());\n viaByKey.get(key)!.add(api);\n\n let queue = queues.get(queueKey);\n if (queue === undefined) {\n queue = {\n api,\n method: method.name,\n // Every cloudtasks method carries a queueName; the fallback covers a\n // dependencies.json written before queueName became kind-specific.\n queueName: method.queueName ?? `${api}-${method.name}`,\n producedBy: [],\n consumedBy: [],\n };\n queues.set(queueKey, queue);\n }\n if (!queue.producedBy.includes(from)) queue.producedBy.push(from);\n if (!queue.consumedBy.includes(to)) queue.consumedBy.push(to);\n }\n }\n\n /**\n * WHICH implementers this one `uses` reaches. Resolution order (most specific wins):\n * 1. a literal `ClientConfig` at the call site (`ref.targetService`) — names ONE node;\n * 2. else the calling project's declared `callsService` (project.json) — for the shared-library\n * case where the literal cannot sit at the call site, it lives one indirection away in the app;\n * 3. else the historical fan-out — the only safe superset — recording WHY, so a fanned-out (i.e.\n * possibly fictional) edge can never pass for a derived one.\n * A named target (1 or 2) that resolves to no module, or to a module that does not serve the\n * contract, is a PROBLEM (fails the build), never a silent drop.\n */\n private targetsFor(user: string, ref: ApiRef, implementers: string[]): string[] {\n const literal = ref.targetService;\n if (literal !== undefined)\n return this.resolveNamedTarget(user, ref, implementers, literal, 'literal');\n\n const declared = this.callsServiceFor(user, ref.api);\n if (declared !== undefined)\n return this.resolveNamedTarget(user, ref, implementers, declared, 'callsService');\n\n return this.untargetedFanOut(user, ref, implementers);\n }\n\n /**\n * The declared `callsService` target for this node's use of `api`, or undefined when none applies.\n * A string declaration aims every untargeted use at one service; a map aims per api-class.\n */\n private callsServiceFor(user: string, api: string): string | undefined {\n const declared = this.projects[user]?.callsService;\n if (declared === undefined) return undefined;\n if (typeof declared === 'string') return declared;\n return declared[api];\n }\n\n /**\n * Resolve a NAMED target (from a call-site literal or a project-level `callsService`) to the one\n * module that serves it. A name no module answers to, or a module that does not serve the\n * contract, fails the build — the wording names which declaration to fix, `source` deciding it.\n */\n private resolveNamedTarget(\n user: string,\n ref: ApiRef,\n implementers: string[],\n wanted: string,\n source: 'literal' | 'callsService',\n ): string[] {\n const node = this.nodeByServiceName.get(wanted);\n const served = `${implementers.length} module(s) serve this contract (${implementers.join(', ')})`;\n if (node === undefined) {\n this.problems.push(\n source === 'callsService'\n ? `${user} declares metadata.webpieces.callsService '${wanted}' in its project.json, but NO ` +\n `module answers to that name. ${served}. Point callsService at a module name, or declare ` +\n `metadata.webpieces.serviceName: '${wanted}' on the module that serves \"${ref.api}\".`\n : `${user} calls \"${ref.api}\" at service '${wanted}', but NO module answers to that name. ` +\n `${served}. Either use the module name, or declare metadata.webpieces.serviceName: ` +\n `'${wanted}' on the module that serves it (and translate any environment prefix in ` +\n `ClientRegistry.setDeriver, not here).`,\n );\n return implementers;\n }\n if (!implementers.includes(node)) {\n const via =\n source === 'callsService'\n ? `${user} declares callsService '${wanted}' (module ${node})`\n : `${user} calls \"${ref.api}\" at service '${wanted}' (module ${node})`;\n this.problems.push(\n `${via}, but ${node} does NOT serve \"${ref.api}\" — ${implementers.join(', ')} do. At runtime that ` +\n `call has nothing to answer it.`,\n );\n return implementers;\n }\n return [node];\n }\n\n /**\n * A use with no literal client config. One implementer is unambiguous, so it stays silent; more\n * than one means every edge but one is fiction — exactly the failure this mechanism exists to\n * stop — so it is reported even though the graph still (conservatively) draws them all.\n */\n private untargetedFanOut(user: string, ref: ApiRef, implementers: string[]): string[] {\n if (implementers.length > 1) {\n this.warnings.push(\n `${user} uses \"${ref.api}\" with no literal client config, and ${implementers.length} services ` +\n `implement it (${implementers.join(', ')}) — an edge is drawn to EVERY one, so all but one ` +\n `are fiction. Name the target: createRpcClient(${ref.api}, new ClientConfig('<serviceName>', ClientRole.SERVER)); ` +\n `or, when the client is built in a shared library (no literal can sit at the call site), ` +\n `declare metadata.webpieces.callsService: '<serviceName>' on ${user}'s project.json.`,\n );\n }\n return implementers;\n }\n\n /**\n * Rebuild edges from their `\"from to type queue\"` keys. The 4th part is the queue (\"Api.method\")\n * and is empty for every non-queued edge; it is part of the KEY so two queued methods between\n * the same pair stay two edges instead of collapsing into one.\n */\n private edgesFromKeys(viaByKey: Map<string, Set<string>>): RuntimeEdge[] {\n const edges: RuntimeEdge[] = [];\n for (const key of viaByKey.keys()) {\n const parts = key.split(' ');\n const edge: RuntimeEdge = {\n from: parts[0],\n to: parts[1],\n via: Array.from(viaByKey.get(key)!).sort(),\n type: parts[2] as ApiTransport,\n };\n if (parts[3] !== undefined && parts[3] !== '') edge.queue = parts[3];\n edges.push(edge);\n }\n edges.sort(\n (a: RuntimeEdge, b: RuntimeEdge) =>\n a.from.localeCompare(b.from) ||\n a.to.localeCompare(b.to) ||\n (a.type ?? '').localeCompare(b.type ?? '') ||\n (a.queue ?? '').localeCompare(b.queue ?? ''),\n );\n return edges;\n }\n\n private buildServices(decls: ScanDecl[], edges: RuntimeEdge[]): Record<string, RuntimeService> {\n const services: Record<string, RuntimeService> = {};\n for (const decl of decls) {\n // A queued hop depends on the QUEUE, not on the peer: the producer hands off and returns,\n // so naming the consumer here would assert a coupling that does not exist (and would make\n // a service that defers work to itself look self-dependent).\n const dependsOn = Array.from(\n new Set(\n edges\n .filter((e: RuntimeEdge) => e.from === decl.name)\n .map((e: RuntimeEdge) =>\n e.queue === undefined ? e.to : `queue:${e.queue}`,\n ),\n ),\n ).sort();\n // Keys are written in this order; an undefined value is omitted by JSON.stringify, so\n // the committed JSON stays clean AND deterministic without conditional assembly.\n const service: RuntimeService = {\n level: 0,\n role: this.projects[decl.name]?.role, // labels a relation-less node for what it IS\n serviceName: this.projects[decl.name]?.serviceName,\n callsService: this.projects[decl.name]?.callsService,\n implements: decl.implementsApis.map((r: ApiRef) => r.api),\n implementsVia:\n decl.implementsVia.size > 0 ? sortedRecord(decl.implementsVia) : undefined,\n // One api used against two services is two refs but ONE api in this list.\n uses: Array.from(new Set(decl.usesApis.map((r: ApiRef) => r.api))),\n dependsOn,\n wiringUses: decl.usesApis.some((ref) => ref.declaredVia !== undefined)\n ? decl.usesApis\n : undefined,\n wiringImplements: decl.implementsApis.some((ref) => ref.declaredVia !== undefined)\n ? decl.implementsApis\n : undefined,\n };\n services[decl.name] = service;\n // The explicit tag wins and is NOT reported as auto-hidden — somebody asked for it.\n if (this.hiddenProjects.has(decl.name)) {\n service.drawOnGraph = false;\n } else if (this.isNonParticipantServer(decl)) {\n service.drawOnGraph = false;\n this.autoHidden.push(decl.name);\n }\n }\n const levels = assignLevels(adjacencyFromEdges(Object.keys(services), edges));\n for (const name of Object.keys(services)) services[name].level = levels[name] ?? 0;\n return services;\n }\n}\n\n/**\n * Derive the runtime graph from dependencies.json's project `apiRelations` — the\n * single source of truth shared by generate + validate. `hiddenProjects`\n * (drawOnGraph:false, defaults to none) are kept in the graph but flagged so the\n * runtime visualizer omits their nodes + edges.\n */\n// webpieces-disable no-function-outside-class -- module entry point for the runtime graph derivation\nexport function deriveRuntimeGraph(\n projects: EnhancedGraph,\n hiddenProjects: Set<string> = new Set<string>(),\n apiContracts: ApiContracts = {},\n): RuntimeGraph {\n return deriveRuntimeGraphReport(projects, hiddenProjects, apiContracts).graph;\n}\n\n/**\n * The same derivation, plus the warnings it produced (every edge it had to GUESS at). Executors use\n * this form and print the warnings; `deriveRuntimeGraph` is the convenience form for callers that\n * only want the data. The warnings are deliberately kept OUT of runtime-dependencies.json — a graph\n * file that records its own doubts would just get committed and stop being read.\n */\n// webpieces-disable no-function-outside-class -- module entry point for the runtime graph derivation\nexport function deriveRuntimeGraphReport(\n projects: EnhancedGraph,\n hiddenProjects: Set<string> = new Set<string>(),\n apiContracts: ApiContracts = {},\n externalSystems: ExternalSystemDecls = {},\n): RuntimeGraphReport {\n return new RuntimeGraphDeriver(\n projects,\n hiddenProjects,\n apiContracts,\n externalSystems,\n ).assemble();\n}\n\n// Levels + adjacency live in runtime-graph-levels.ts; re-exported for the same reason the model types\n// and the io helpers above are — one obvious place to import the runtime graph from.\nexport { runtimeAdjacency } from './runtime-graph-levels';\n"]}
@@ -18,6 +18,13 @@ export declare class RuntimeHtmlPage {
18
18
  private readonly nodeMenu;
19
19
  constructor(clientJs?: () => string, filterJs?: () => string);
20
20
  render(model: GraphRenderModel, title: string, graph: RuntimeGraph, showExternalNodes?: boolean): string;
21
+ /**
22
+ * The product chip row (#1179): one chip per product a service belongs to. Pressing chips keeps
23
+ * the UNION of their services (each product's tagged services plus everything they reach at
24
+ * runtime) and the queues, triggers and systems attached to them; it intersects with Filter
25
+ * Unconnected. Omitted when no service belongs to a product. The page always opens unfiltered.
26
+ */
27
+ productChips(graph: RuntimeGraph): string;
21
28
  /**
22
29
  * The browser half lives in runtime-visualizer.client.ts (matching graph-visualizer.client.ts)
23
30
  * rather than in a template literal here: it renders with @viz-js/viz v3, redraws every queue
@@ -45,6 +45,7 @@ class RuntimeHtmlPage {
45
45
  ${this.snapshot.html()}
46
46
  <p class="hint">💡 <strong>Click any box</strong> for its menu — <strong>Lock</strong> dims every other box and every arrow so one service, queue, datastore or external system stands alone; <strong>Unlock</strong> restores the whole picture.</p>
47
47
  <p class="hint"><strong>Filter Unconnected</strong> keeps incoming and outgoing chains and compacts the picture. <strong>Turn off Filter</strong> restores it.</p>
48
+ ${this.productChips(graph)}
48
49
  ${this.filterAssets.html()}
49
50
  <div id="graph"></div>
50
51
  ${(0, runtime_viz_theme_1.legendHtml)()}
@@ -56,6 +57,29 @@ class RuntimeHtmlPage {
56
57
  </body>
57
58
  </html>`;
58
59
  }
60
+ /**
61
+ * The product chip row (#1179): one chip per product a service belongs to. Pressing chips keeps
62
+ * the UNION of their services (each product's tagged services plus everything they reach at
63
+ * runtime) and the queues, triggers and systems attached to them; it intersects with Filter
64
+ * Unconnected. Omitted when no service belongs to a product. The page always opens unfiltered.
65
+ */
66
+ productChips(graph) {
67
+ const products = new Set();
68
+ for (const service of Object.values(graph.services)) {
69
+ if (service.drawOnGraph === false)
70
+ continue;
71
+ for (const product of service.products ?? [])
72
+ products.add(product);
73
+ }
74
+ if (products.size === 0)
75
+ return '';
76
+ const chips = [...products]
77
+ .sort()
78
+ .map((product) => `<button type="button" class="wp-chip" data-wp-product="${product}" aria-pressed="false">${product}</button>`)
79
+ .join('');
80
+ return (`<div class="wp-product-filter" id="wp-product-filter" role="group" aria-label="Product filter">` +
81
+ `<span class="wp-product-label">Product <small>any match</small></span>${chips}</div>`);
82
+ }
59
83
  /**
60
84
  * The browser half lives in runtime-visualizer.client.ts (matching graph-visualizer.client.ts)
61
85
  * rather than in a template literal here: it renders with @viz-js/viz v3, redraws every queue
@@ -116,6 +140,11 @@ class RuntimeHtmlPage {
116
140
  overflow-x: auto;
117
141
  }
118
142
  .sw { flex: 0 0 auto; display: inline-flex; }
143
+ .wp-product-filter { display: flex; flex-wrap: wrap; justify-content: center; align-items: center; gap: 6px; margin: 0 0 14px; }
144
+ .wp-product-label { color: #555; margin-right: 4px; }
145
+ .wp-product-label small { color: #888; }
146
+ .wp-chip { border: 1px solid #b8bcc8; background: white; color: #333; border-radius: 999px; padding: 3px 11px; font: inherit; font-size: 13px; cursor: pointer; }
147
+ .wp-chip[aria-pressed="true"] { background: #4b44c8; border-color: #4b44c8; color: white; }
119
148
  code { background: #f2f2f2; padding: 1px 4px; border-radius: 3px; font-family: monospace; }
120
149
  ${new graph_navigation_1.GraphNavigation(graph_navigation_1.NavigationLayout.TOOLBAR).styles()}
121
150
  @media (max-width: 900px) { .legend-columns { grid-template-columns: 1fr; } }`;
@@ -1 +1 @@
1
- {"version":3,"file":"runtime-html-page.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/runtime-html-page.ts"],"names":[],"mappings":";;;AAAA,6DAA2E;AAC3E,uDAAmD;AAEnD,yDAAuE;AACvE,qDAAiD;AACjD,yDAAkF;AAClF,uDAAkD;AAClD,2DAAiD;AAEjD;;;;;;;;GAQG;AACH,MAAa,eAAe;IAOH;IAEA;IARJ,YAAY,GAAG,IAAI,sCAAiB,EAAE,CAAC;IACvC,QAAQ,GAAG,IAAI,8BAAa,EAAE,CAAC;IAChD,sGAAsG;IACrF,QAAQ,GAAG,IAAI,+BAAa,EAAE,CAAC;IAEhD,YACqB,WAAyB,GAAW,EAAE,CACnD,IAAA,qCAAkB,EAAC,8BAA8B,CAAC,EACrC,WAAyB,GAAW,EAAE,CACnD,IAAA,qCAAkB,EAAC,wBAAwB,CAAC;QAH/B,aAAQ,GAAR,QAAQ,CAC6B;QACrC,aAAQ,GAAR,QAAQ,CACuB;IACjD,CAAC;IAEJ,MAAM,CACF,KAAuB,EACvB,KAAa,EACb,KAAmB,EACnB,oBAA6B,IAAI;QAEjC,OAAO;;;;;;;aAOF,KAAK;;aAEL,IAAI,CAAC,MAAM,EAAE;;;UAGhB,KAAK;MACT,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE;;;MAGpB,IAAI,CAAC,YAAY,CAAC,IAAI,EAAE;;MAExB,IAAA,8BAAU,GAAE;cACJ,IAAI,CAAC,QAAQ,CAAC,MAAM,EAAE;cACtB,IAAI,CAAC,QAAQ,EAAE;cACf,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC;cAClB,IAAI,kCAAe,CAAC,mCAAgB,CAAC,OAAO,CAAC,CAAC,MAAM,EAAE;cACtD,IAAI,gCAAc,CAAC,KAAK,EAAE,iBAAiB,CAAC,CAAC,MAAM,EAAE;;QAE3D,CAAC;IACL,CAAC;IAED;;;;;;OAMG;IACK,MAAM,CAAC,KAAuB;QAClC,OAAO,IAAI,CAAC,QAAQ,EAAE,CAAC,KAAK,CAAC,2CAAwB,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC;IAC/F,CAAC;IAED;;;;;;;;OAQG;IACK,MAAM;QACV,OAAO;;;;;;;UAOL,IAAI,CAAC,QAAQ,CAAC,MAAM,EAAE;UACtB,IAAI,CAAC,YAAY,CAAC,MAAM,EAAE;UAC1B,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC,QAAQ,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;UAgCjC,IAAI,kCAAe,CAAC,mCAAgB,CAAC,OAAO,CAAC,CAAC,MAAM,EAAE;sFACsB,CAAC;IACnF,CAAC;CACJ;AAhHD,0CAgHC","sourcesContent":["import { GraphRenderModel, GraphFilterAssets } from './graph-render-model';\nimport { RuntimeDetails } from './runtime-details';\nimport type { RuntimeGraph } from './runtime-graph-model';\nimport { GraphNavigation, NavigationLayout } from './graph-navigation';\nimport { SavedSnapshot } from './saved-snapshot';\nimport { CLIENT_MODEL_PLACEHOLDER, readCompiledClient } from './graph-visualizer';\nimport { GraphNodeMenu } from './graph-node-menu';\nimport { legendHtml } from './runtime-viz-theme';\n\n/**\n * The runtime-architecture HTML page: its styles, the shared floating node menu, the graph host\n * and the legend.\n *\n * A CLASS rather than a bag of module functions so the client-text seam can be a constructor\n * parameter the way GraphVisualizer's is: the default reads the COMPILED sibling, which exists in\n * dist and in the published tarball but NOT in a source checkout, and a unit test running from\n * source hands in the text itself instead of requiring the package to have been built first.\n */\nexport class RuntimeHtmlPage {\n private readonly filterAssets = new GraphFilterAssets();\n private readonly snapshot = new SavedSnapshot();\n /** The ONE floating-node-menu implementation, shared with dependencies.html and every design.html. */\n private readonly nodeMenu = new GraphNodeMenu();\n\n constructor(\n private readonly clientJs: () => string = (): string =>\n readCompiledClient('runtime-visualizer.client.js'),\n private readonly filterJs: () => string = (): string =>\n readCompiledClient('graph-filter.client.js'),\n ) {}\n\n render(\n model: GraphRenderModel,\n title: string,\n graph: RuntimeGraph,\n showExternalNodes: boolean = true,\n ): string {\n return `<!DOCTYPE html>\n<html>\n<head>\n <!-- REQUIRED: the cron node's label is a literal ⏰, and the DOT is embedded in this file. With\n no declared charset the browser falls back to a locale guess and renders it as mojibake\n (\"â °\") whenever the page is served without a charset header. -->\n <meta charset=\"utf-8\">\n <title>${title}</title>\n <script src=\"https://cdn.jsdelivr.net/npm/@viz-js/viz@3.28.0/dist/viz-global.js\"></script>\n <style>${this.styles()}</style>\n</head>\n<body>\n <h1>${title}</h1>\n ${this.snapshot.html()}\n <p class=\"hint\">💡 <strong>Click any box</strong> for its menu — <strong>Lock</strong> dims every other box and every arrow so one service, queue, datastore or external system stands alone; <strong>Unlock</strong> restores the whole picture.</p>\n <p class=\"hint\"><strong>Filter Unconnected</strong> keeps incoming and outgoing chains and compacts the picture. <strong>Turn off Filter</strong> restores it.</p>\n ${this.filterAssets.html()}\n <div id=\"graph\"></div>\n ${legendHtml()}\n <script>${this.nodeMenu.script()}</script>\n <script>${this.filterJs()}</script>\n <script>${this.script(model)}</script>\n <script>${new GraphNavigation(NavigationLayout.TOOLBAR).script()}</script>\n <script>${new RuntimeDetails(graph, showExternalNodes).script()}</script>\n</body>\n</html>`;\n }\n\n /**\n * The browser half lives in runtime-visualizer.client.ts (matching graph-visualizer.client.ts)\n * rather than in a template literal here: it renders with @viz-js/viz v3, redraws every queue\n * node as a true horizontal cylinder, and wires the shared node menu onto every box — more\n * logic than belongs inline in a .ts string. The substitution is a blind split/join, so the\n * render-model placeholder must appear EXACTLY ONCE in the client.\n */\n private script(model: GraphRenderModel): string {\n return this.clientJs().split(CLIENT_MODEL_PLACEHOLDER).join(this.filterAssets.json(model));\n }\n\n /**\n * Page styles, including the shared menu stylesheet: the clickable cursor + blue glow on every\n * box, and the dim/undim rules the lock toggles, scoped to this page's `#graph` host. Those two\n * blocks are shared verbatim with architecture/dependencies.html and every design.html.\n *\n * The legend swatches are hand-drawn inline SVG on purpose: the alternative is shelling out to\n * Graphviz at generate time, which would make writing the HTML depend on a `dot` binary being\n * installed — a dependency this tool does not otherwise have, since rendering is client-side.\n */\n private styles(): string {\n return `\n body { margin: 0; padding: 20px; font-family: Arial, sans-serif; background: #f5f5f5; }\n h1 { text-align: center; color: #333; }\n .hint { text-align: center; color: #555; margin: 0 0 16px; }\n /* Every box is clickable and opens the shared floating menu, so the menu's own stylesheet\n * carries the cursor + blue glow and the dim/undim rules the lock toggles. Shared verbatim\n * with architecture/dependencies.html and every project's design.html. */\n ${this.nodeMenu.styles()}\n ${this.filterAssets.styles()}\n ${this.nodeMenu.dimStyles('#graph')}\n #graph { text-align: center; background: white; padding: 20px; border-radius: 8px; overflow-x: auto; }\n #graph svg { max-width: 100%; height: auto; }\n .legend {\n margin: 20px auto;\n max-width: 1100px;\n padding: 15px 20px;\n background: white;\n border-radius: 8px;\n box-shadow: 0 2px 4px rgba(0,0,0,0.1);\n }\n .legend h2 { margin-top: 0; color: #333; }\n .legend-columns { display: grid; grid-template-columns: repeat(3, 1fr); gap: 28px; align-items: start; }\n .legend-col h3 { margin: 0 0 10px; color: #333; font-size: 15px; border-bottom: 1px solid #eee; padding-bottom: 5px; }\n .legend-item { margin: 9px 0; display: flex; align-items: center; gap: 10px; line-height: 1.4; color: #444; }\n /* Prose rows carry no swatch, so they must NOT be flex containers: flex would promote every\n * inline <strong>/<em>/<code> to a flex item and shred the sentence into columns. */\n .legend-note { margin: 9px 0; line-height: 1.5; color: #444; }\n .legend-box-anatomy {\n margin: 0 0 12px;\n padding: 8px 10px;\n background: #f7f7f7;\n border-radius: 4px;\n font-family: monospace;\n font-size: 12px;\n line-height: 1.5;\n color: #333;\n white-space: pre;\n overflow-x: auto;\n }\n .sw { flex: 0 0 auto; display: inline-flex; }\n code { background: #f2f2f2; padding: 1px 4px; border-radius: 3px; font-family: monospace; }\n ${new GraphNavigation(NavigationLayout.TOOLBAR).styles()}\n @media (max-width: 900px) { .legend-columns { grid-template-columns: 1fr; } }`;\n }\n}\n"]}
1
+ {"version":3,"file":"runtime-html-page.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/runtime-html-page.ts"],"names":[],"mappings":";;;AAAA,6DAA2E;AAC3E,uDAAmD;AAEnD,yDAAuE;AACvE,qDAAiD;AACjD,yDAAkF;AAClF,uDAAkD;AAClD,2DAAiD;AAEjD;;;;;;;;GAQG;AACH,MAAa,eAAe;IAOH;IAEA;IARJ,YAAY,GAAG,IAAI,sCAAiB,EAAE,CAAC;IACvC,QAAQ,GAAG,IAAI,8BAAa,EAAE,CAAC;IAChD,sGAAsG;IACrF,QAAQ,GAAG,IAAI,+BAAa,EAAE,CAAC;IAEhD,YACqB,WAAyB,GAAW,EAAE,CACnD,IAAA,qCAAkB,EAAC,8BAA8B,CAAC,EACrC,WAAyB,GAAW,EAAE,CACnD,IAAA,qCAAkB,EAAC,wBAAwB,CAAC;QAH/B,aAAQ,GAAR,QAAQ,CAC6B;QACrC,aAAQ,GAAR,QAAQ,CACuB;IACjD,CAAC;IAEJ,MAAM,CACF,KAAuB,EACvB,KAAa,EACb,KAAmB,EACnB,oBAA6B,IAAI;QAEjC,OAAO;;;;;;;aAOF,KAAK;;aAEL,IAAI,CAAC,MAAM,EAAE;;;UAGhB,KAAK;MACT,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE;;;MAGpB,IAAI,CAAC,YAAY,CAAC,KAAK,CAAC;MACxB,IAAI,CAAC,YAAY,CAAC,IAAI,EAAE;;MAExB,IAAA,8BAAU,GAAE;cACJ,IAAI,CAAC,QAAQ,CAAC,MAAM,EAAE;cACtB,IAAI,CAAC,QAAQ,EAAE;cACf,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC;cAClB,IAAI,kCAAe,CAAC,mCAAgB,CAAC,OAAO,CAAC,CAAC,MAAM,EAAE;cACtD,IAAI,gCAAc,CAAC,KAAK,EAAE,iBAAiB,CAAC,CAAC,MAAM,EAAE;;QAE3D,CAAC;IACL,CAAC;IAED;;;;;OAKG;IACH,YAAY,CAAC,KAAmB;QAC5B,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAU,CAAC;QACnC,KAAK,MAAM,OAAO,IAAI,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,EAAE,CAAC;YAClD,IAAI,OAAO,CAAC,WAAW,KAAK,KAAK;gBAAE,SAAS;YAC5C,KAAK,MAAM,OAAO,IAAI,OAAO,CAAC,QAAQ,IAAI,EAAE;gBAAE,QAAQ,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;QACxE,CAAC;QACD,IAAI,QAAQ,CAAC,IAAI,KAAK,CAAC;YAAE,OAAO,EAAE,CAAC;QACnC,MAAM,KAAK,GAAG,CAAC,GAAG,QAAQ,CAAC;aACtB,IAAI,EAAE;aACN,GAAG,CACA,CAAC,OAAe,EAAU,EAAE,CACxB,0DAA0D,OAAO,0BAA0B,OAAO,WAAW,CACpH;aACA,IAAI,CAAC,EAAE,CAAC,CAAC;QACd,OAAO,CACH,iGAAiG;YACjG,yEAAyE,KAAK,QAAQ,CACzF,CAAC;IACN,CAAC;IAED;;;;;;OAMG;IACK,MAAM,CAAC,KAAuB;QAClC,OAAO,IAAI,CAAC,QAAQ,EAAE,CAAC,KAAK,CAAC,2CAAwB,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC;IAC/F,CAAC;IAED;;;;;;;;OAQG;IACK,MAAM;QACV,OAAO;;;;;;;UAOL,IAAI,CAAC,QAAQ,CAAC,MAAM,EAAE;UACtB,IAAI,CAAC,YAAY,CAAC,MAAM,EAAE;UAC1B,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC,QAAQ,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;UAqCjC,IAAI,kCAAe,CAAC,mCAAgB,CAAC,OAAO,CAAC,CAAC,MAAM,EAAE;sFACsB,CAAC;IACnF,CAAC;CACJ;AAhJD,0CAgJC","sourcesContent":["import { GraphRenderModel, GraphFilterAssets } from './graph-render-model';\nimport { RuntimeDetails } from './runtime-details';\nimport type { RuntimeGraph } from './runtime-graph-model';\nimport { GraphNavigation, NavigationLayout } from './graph-navigation';\nimport { SavedSnapshot } from './saved-snapshot';\nimport { CLIENT_MODEL_PLACEHOLDER, readCompiledClient } from './graph-visualizer';\nimport { GraphNodeMenu } from './graph-node-menu';\nimport { legendHtml } from './runtime-viz-theme';\n\n/**\n * The runtime-architecture HTML page: its styles, the shared floating node menu, the graph host\n * and the legend.\n *\n * A CLASS rather than a bag of module functions so the client-text seam can be a constructor\n * parameter the way GraphVisualizer's is: the default reads the COMPILED sibling, which exists in\n * dist and in the published tarball but NOT in a source checkout, and a unit test running from\n * source hands in the text itself instead of requiring the package to have been built first.\n */\nexport class RuntimeHtmlPage {\n private readonly filterAssets = new GraphFilterAssets();\n private readonly snapshot = new SavedSnapshot();\n /** The ONE floating-node-menu implementation, shared with dependencies.html and every design.html. */\n private readonly nodeMenu = new GraphNodeMenu();\n\n constructor(\n private readonly clientJs: () => string = (): string =>\n readCompiledClient('runtime-visualizer.client.js'),\n private readonly filterJs: () => string = (): string =>\n readCompiledClient('graph-filter.client.js'),\n ) {}\n\n render(\n model: GraphRenderModel,\n title: string,\n graph: RuntimeGraph,\n showExternalNodes: boolean = true,\n ): string {\n return `<!DOCTYPE html>\n<html>\n<head>\n <!-- REQUIRED: the cron node's label is a literal ⏰, and the DOT is embedded in this file. With\n no declared charset the browser falls back to a locale guess and renders it as mojibake\n (\"â °\") whenever the page is served without a charset header. -->\n <meta charset=\"utf-8\">\n <title>${title}</title>\n <script src=\"https://cdn.jsdelivr.net/npm/@viz-js/viz@3.28.0/dist/viz-global.js\"></script>\n <style>${this.styles()}</style>\n</head>\n<body>\n <h1>${title}</h1>\n ${this.snapshot.html()}\n <p class=\"hint\">💡 <strong>Click any box</strong> for its menu — <strong>Lock</strong> dims every other box and every arrow so one service, queue, datastore or external system stands alone; <strong>Unlock</strong> restores the whole picture.</p>\n <p class=\"hint\"><strong>Filter Unconnected</strong> keeps incoming and outgoing chains and compacts the picture. <strong>Turn off Filter</strong> restores it.</p>\n ${this.productChips(graph)}\n ${this.filterAssets.html()}\n <div id=\"graph\"></div>\n ${legendHtml()}\n <script>${this.nodeMenu.script()}</script>\n <script>${this.filterJs()}</script>\n <script>${this.script(model)}</script>\n <script>${new GraphNavigation(NavigationLayout.TOOLBAR).script()}</script>\n <script>${new RuntimeDetails(graph, showExternalNodes).script()}</script>\n</body>\n</html>`;\n }\n\n /**\n * The product chip row (#1179): one chip per product a service belongs to. Pressing chips keeps\n * the UNION of their services (each product's tagged services plus everything they reach at\n * runtime) and the queues, triggers and systems attached to them; it intersects with Filter\n * Unconnected. Omitted when no service belongs to a product. The page always opens unfiltered.\n */\n productChips(graph: RuntimeGraph): string {\n const products = new Set<string>();\n for (const service of Object.values(graph.services)) {\n if (service.drawOnGraph === false) continue;\n for (const product of service.products ?? []) products.add(product);\n }\n if (products.size === 0) return '';\n const chips = [...products]\n .sort()\n .map(\n (product: string): string =>\n `<button type=\"button\" class=\"wp-chip\" data-wp-product=\"${product}\" aria-pressed=\"false\">${product}</button>`,\n )\n .join('');\n return (\n `<div class=\"wp-product-filter\" id=\"wp-product-filter\" role=\"group\" aria-label=\"Product filter\">` +\n `<span class=\"wp-product-label\">Product <small>any match</small></span>${chips}</div>`\n );\n }\n\n /**\n * The browser half lives in runtime-visualizer.client.ts (matching graph-visualizer.client.ts)\n * rather than in a template literal here: it renders with @viz-js/viz v3, redraws every queue\n * node as a true horizontal cylinder, and wires the shared node menu onto every box — more\n * logic than belongs inline in a .ts string. The substitution is a blind split/join, so the\n * render-model placeholder must appear EXACTLY ONCE in the client.\n */\n private script(model: GraphRenderModel): string {\n return this.clientJs().split(CLIENT_MODEL_PLACEHOLDER).join(this.filterAssets.json(model));\n }\n\n /**\n * Page styles, including the shared menu stylesheet: the clickable cursor + blue glow on every\n * box, and the dim/undim rules the lock toggles, scoped to this page's `#graph` host. Those two\n * blocks are shared verbatim with architecture/dependencies.html and every design.html.\n *\n * The legend swatches are hand-drawn inline SVG on purpose: the alternative is shelling out to\n * Graphviz at generate time, which would make writing the HTML depend on a `dot` binary being\n * installed — a dependency this tool does not otherwise have, since rendering is client-side.\n */\n private styles(): string {\n return `\n body { margin: 0; padding: 20px; font-family: Arial, sans-serif; background: #f5f5f5; }\n h1 { text-align: center; color: #333; }\n .hint { text-align: center; color: #555; margin: 0 0 16px; }\n /* Every box is clickable and opens the shared floating menu, so the menu's own stylesheet\n * carries the cursor + blue glow and the dim/undim rules the lock toggles. Shared verbatim\n * with architecture/dependencies.html and every project's design.html. */\n ${this.nodeMenu.styles()}\n ${this.filterAssets.styles()}\n ${this.nodeMenu.dimStyles('#graph')}\n #graph { text-align: center; background: white; padding: 20px; border-radius: 8px; overflow-x: auto; }\n #graph svg { max-width: 100%; height: auto; }\n .legend {\n margin: 20px auto;\n max-width: 1100px;\n padding: 15px 20px;\n background: white;\n border-radius: 8px;\n box-shadow: 0 2px 4px rgba(0,0,0,0.1);\n }\n .legend h2 { margin-top: 0; color: #333; }\n .legend-columns { display: grid; grid-template-columns: repeat(3, 1fr); gap: 28px; align-items: start; }\n .legend-col h3 { margin: 0 0 10px; color: #333; font-size: 15px; border-bottom: 1px solid #eee; padding-bottom: 5px; }\n .legend-item { margin: 9px 0; display: flex; align-items: center; gap: 10px; line-height: 1.4; color: #444; }\n /* Prose rows carry no swatch, so they must NOT be flex containers: flex would promote every\n * inline <strong>/<em>/<code> to a flex item and shred the sentence into columns. */\n .legend-note { margin: 9px 0; line-height: 1.5; color: #444; }\n .legend-box-anatomy {\n margin: 0 0 12px;\n padding: 8px 10px;\n background: #f7f7f7;\n border-radius: 4px;\n font-family: monospace;\n font-size: 12px;\n line-height: 1.5;\n color: #333;\n white-space: pre;\n overflow-x: auto;\n }\n .sw { flex: 0 0 auto; display: inline-flex; }\n .wp-product-filter { display: flex; flex-wrap: wrap; justify-content: center; align-items: center; gap: 6px; margin: 0 0 14px; }\n .wp-product-label { color: #555; margin-right: 4px; }\n .wp-product-label small { color: #888; }\n .wp-chip { border: 1px solid #b8bcc8; background: white; color: #333; border-radius: 999px; padding: 3px 11px; font: inherit; font-size: 13px; cursor: pointer; }\n .wp-chip[aria-pressed=\"true\"] { background: #4b44c8; border-color: #4b44c8; color: white; }\n code { background: #f2f2f2; padding: 1px 4px; border-radius: 3px; font-family: monospace; }\n ${new GraphNavigation(NavigationLayout.TOOLBAR).styles()}\n @media (max-width: 900px) { .legend-columns { grid-template-columns: 1fr; } }`;\n }\n}\n"]}
@@ -55,6 +55,13 @@ declare class RuntimeNodeMenu {
55
55
  }
56
56
  declare class RuntimePage extends WpFilterPage {
57
57
  private lock;
58
+ /** The pressed product chips (#1179); empty keeps every box. */
59
+ private readonly products;
60
+ /** Each product chip toggles itself and redraws; the narrowing happens in narrow(). */
61
+ protected wireControls(): void;
62
+ /** Pressed chips keep the UNION of their products' boxes, intersected with Filter Unconnected. */
63
+ protected narrow(retained: Set<string>): Set<string>;
64
+ protected usesFullDot(): boolean;
58
65
  protected captureBinding(svg: SVGSVGElement | null): () => void;
59
66
  protected prepareSvg(svg: SVGSVGElement): void;
60
67
  protected wireSvg(svg: SVGSVGElement): void;
@@ -179,6 +179,38 @@ class RuntimeNodeMenu {
179
179
  }
180
180
  class RuntimePage extends WpFilterPage {
181
181
  lock = null;
182
+ /** The pressed product chips (#1179); empty keeps every box. */
183
+ products = new Set();
184
+ /** Each product chip toggles itself and redraws; the narrowing happens in narrow(). */
185
+ wireControls() {
186
+ document.querySelectorAll('[data-wp-product]').forEach((chip) => {
187
+ chip.addEventListener('click', () => {
188
+ const product = chip.dataset['wpProduct'] ?? '';
189
+ if (this.products.has(product))
190
+ this.products.delete(product);
191
+ else
192
+ this.products.add(product);
193
+ chip.setAttribute('aria-pressed', String(this.products.has(product)));
194
+ this.redraw();
195
+ });
196
+ });
197
+ }
198
+ /** Pressed chips keep the UNION of their products' boxes, intersected with Filter Unconnected. */
199
+ narrow(retained) {
200
+ if (this.products.size === 0)
201
+ return retained;
202
+ const kept = new Set();
203
+ for (const node of this.model.nodes) {
204
+ if (!retained.has(node.id))
205
+ continue;
206
+ if (node.products.some((product) => this.products.has(product)))
207
+ kept.add(node.id);
208
+ }
209
+ return kept;
210
+ }
211
+ usesFullDot() {
212
+ return this.products.size === 0;
213
+ }
182
214
  captureBinding(svg) {
183
215
  const previous = this.lock;
184
216
  return () => {