@webpieces/nx-webpieces-rules 0.4.908 → 0.4.910

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 (35) hide show
  1. package/README.md +15 -1
  2. package/package.json +7 -7
  3. package/src/lib/api-usage/external-systems.js +2 -0
  4. package/src/lib/api-usage/external-systems.js.map +1 -1
  5. package/src/lib/graph-filter.client.d.ts +2 -1
  6. package/src/lib/graph-filter.client.js +16 -4
  7. package/src/lib/graph-filter.client.js.map +1 -1
  8. package/src/lib/graph-level-bands.d.ts +3 -0
  9. package/src/lib/graph-level-bands.js +24 -8
  10. package/src/lib/graph-level-bands.js.map +1 -1
  11. package/src/lib/runtime-external-triggers.js +1 -1
  12. package/src/lib/runtime-external-triggers.js.map +1 -1
  13. package/src/lib/runtime-graph-io.js +29 -1
  14. package/src/lib/runtime-graph-io.js.map +1 -1
  15. package/src/lib/runtime-graph-levels.d.ts +2 -20
  16. package/src/lib/runtime-graph-levels.js +8 -30
  17. package/src/lib/runtime-graph-levels.js.map +1 -1
  18. package/src/lib/runtime-graph-model.d.ts +21 -13
  19. package/src/lib/runtime-graph-model.js +18 -0
  20. package/src/lib/runtime-graph-model.js.map +1 -1
  21. package/src/lib/runtime-graph.d.ts +1 -1
  22. package/src/lib/runtime-graph.js +15 -6
  23. package/src/lib/runtime-graph.js.map +1 -1
  24. package/src/lib/runtime-layout.d.ts +6 -0
  25. package/src/lib/runtime-layout.js +31 -0
  26. package/src/lib/runtime-layout.js.map +1 -0
  27. package/src/lib/runtime-node-levels.d.ts +13 -0
  28. package/src/lib/runtime-node-levels.js +118 -0
  29. package/src/lib/runtime-node-levels.js.map +1 -0
  30. package/src/lib/runtime-remote-calls.js +2 -0
  31. package/src/lib/runtime-remote-calls.js.map +1 -1
  32. package/src/lib/runtime-viewer-adapter.js +47 -5
  33. package/src/lib/runtime-viewer-adapter.js.map +1 -1
  34. package/src/lib/runtime-visualizer.js +30 -31
  35. package/src/lib/runtime-visualizer.js.map +1 -1
package/README.md CHANGED
@@ -124,7 +124,21 @@ Every row is one level. A library's level is its dependency depth, but every ser
124
124
  app (`role:server` / `role:client` / `role:app`) is pinned to the TOP row, one above the highest
125
125
  library, so the top row is exactly the entry points (a `role:bundle` sits above the apps it
126
126
  aggregates; a server orchestrator above the servers it boots). The `level` in `dependencies.json`
127
- is that promoted level. The runtime graph keeps its call-depth levels.
127
+ is that promoted level. The runtime graph stores a level and `levelBecause` for every service, queue, external system,
128
+ remote service and trigger in `runtime-dependencies.json`. Outbound systems and remote services are
129
+ L0; services start at L1 and sit one above their highest callee. Foreign queues sit one above their
130
+ consumer and one below their foreign producers. A self-queue shares its consumer's level and does
131
+ not raise it. Contract-only pubsub flows are stored under `contractQueues`, without inventing method
132
+ producers. Foreign queue cycles fail with the queue named in the cycle path; existing explicit legacy
133
+ RPC cuts remain honored. Every trigger shares the top row, one above the highest non-trigger node.
134
+ `maxLevel` records the highest generated level. The renderer ranks all visible node kinds using these
135
+ stored numbers; inbound callers and outbound sinks for the same vendor have separate boxes.
136
+ Only downward real edges constrain ranks. Upward and same-level feedback edges remain visible with
137
+ `constraint=false`. Pure self-queues occupy a presentation-only sub-row below their service, within
138
+ the same logical level; mixed foreign/self queues stay at their saved foreign level. Every queue box
139
+ shows its saved `L#` badge. Filtering and color-mode rebuilds preserve these rows and constraints.
140
+ The layout regression suite runs `dot -Tplain` and requires Graphviz, installed explicitly by CI.
141
+ After upgrading, regenerate old RPC-only JSON with `pnpm nx run architecture:generate`.
128
142
 
129
143
  - **Runtime** — where the code can run. One runtime is a solid fill. One runtime plus a
130
144
  specialization (angular/react inside browser, express inside node) is nested: a frame in the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webpieces/nx-webpieces-rules",
3
- "version": "0.4.908",
3
+ "version": "0.4.910",
4
4
  "description": "Nx-specific webpieces validation rules, graph tooling, executors, and inference plugin.",
5
5
  "type": "commonjs",
6
6
  "main": "./src/index.js",
@@ -20,12 +20,12 @@
20
20
  "README.md"
21
21
  ],
22
22
  "dependencies": {
23
- "@webpieces/api-doc-model": "0.4.908",
24
- "@webpieces/core-util": "0.4.908",
25
- "@webpieces/eslint-rules": "0.4.908",
26
- "@webpieces/rules-config": "0.4.908",
23
+ "@webpieces/api-doc-model": "0.4.910",
24
+ "@webpieces/core-util": "0.4.910",
25
+ "@webpieces/eslint-rules": "0.4.910",
26
+ "@webpieces/rules-config": "0.4.910",
27
27
  "madge": "8.0.0",
28
- "@webpieces/rules-sdk": "0.4.908"
28
+ "@webpieces/rules-sdk": "0.4.910"
29
29
  },
30
30
  "peerDependencies": {
31
31
  "@nx/devkit": ">=18.0.0"
@@ -41,7 +41,7 @@
41
41
  "access": "public"
42
42
  },
43
43
  "devDependencies": {
44
- "@webpieces/tooling-testkit": "0.4.908"
44
+ "@webpieces/tooling-testkit": "0.4.910"
45
45
  },
46
46
  "types": "./src/index.d.ts",
47
47
  "bin": {
@@ -126,6 +126,8 @@ function resolveExternalSystems(decls, services) {
126
126
  out[identity] = {
127
127
  kind: decl.kind,
128
128
  label: decl.label,
129
+ level: 0,
130
+ levelBecause: 'outbound sink',
129
131
  usedBy: [...usedBy].sort(),
130
132
  apis: [...decl.apis].sort(),
131
133
  };
@@ -1 +1 @@
1
- {"version":3,"file":"external-systems.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/nx-webpieces-rules/src/lib/api-usage/external-systems.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * External system declarations\n *\n * A system OUTSIDE this repo that a service talks to — a database, a bucket, a cache. Until now\n * every one of them collapsed into the same grey dashed box, so `lib-firestore` (a datastore) was\n * indistinguishable from an HTTP service the repo happens not to implement. Declaring what a system\n * IS lets the runtime viz draw it as the thing it is.\n *\n * Split out of api-scanner.ts and runtime-graph.ts (both already at their file-size limit) so the\n * two halves of the feature — DECLARING a system and RESOLVING it to arrows — sit together.\n *\n * The two declaration sites exist 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 TS `interface` cannot carry a\n * decorator, and these seams are interfaces, so JSDoc is the only marker that fits in place.\n * - **unwrapped** — the service opens the connection itself (a `pg.Pool`, a TypeORM `DataSource`)\n * and there is no contract to mark, so the declaration is an `external:<kind>:<identity>` nx tag\n * on that PROJECT. Wrapping such a datastore purely to gain a marker is not worth it: a TypeORM\n * facade never closes, unlike the ~8 hand-picked methods a firestore seam needs.\n *\n * Both resolve to the same `(kind, identity)` pair, and identity is the NODE identity — two projects\n * declaring `postgres` converge on one cylinder with an arrow each, rather than drawing a database\n * apiece.\n */\n\nimport type { ApiClassInfo, ExternalSystemDecl, ExternalSystemDecls, ExternalSystemKind } from './api-relations';\nimport { isExternalSystemKind } from './api-relations';\nimport type { ProjectInfo } from '../project-info';\nimport type { RuntimeExternalSystem, RuntimeGraph, RuntimeService } from '../runtime-graph-model';\n\n/** nx tag prefix declaring an external system: `external:<kind>:<identity>`. */\nconst EXTERNAL_TAG_PREFIX = 'external:';\n\n/** The two halves of a parsed `external:<kind>:<identity>` nx tag. */\nclass ExternalTag {\n constructor(\n public readonly kind: ExternalSystemKind,\n public readonly identity: string,\n ) {}\n}\n\n/**\n * `external:<kind>:<identity>` -> its parts, or null for any other tag.\n *\n * Returns null (rather than throwing) on an unknown kind: this prefix shares a tag list with\n * `framework:` and `role:`, and hard-failing the whole graph generation over one malformed tag is a\n * worse outcome than not drawing one node.\n */\n// webpieces-disable no-function-outside-class -- pure parser, matching the sibling builders in this file\nfunction parseExternalTag(tag: string): ExternalTag | null {\n if (!tag.startsWith(EXTERNAL_TAG_PREFIX)) return null;\n const parts = tag.slice(EXTERNAL_TAG_PREFIX.length).split(':');\n if (parts.length !== 2) return null;\n const kind = parts[0].toLowerCase();\n const identity = parts[1].trim();\n if (!isExternalSystemKind(kind) || identity === '') return null;\n return new ExternalTag(kind, identity);\n}\n\n/**\n * The committed `externalSystems` table for architecture/dependencies.json, merging both declaration\n * sites into one identity-keyed map.\n *\n * A system may legitimately carry both: a repo can wrap a datastore behind a contract in one service\n * and open it directly in another. Takes the api index rather than the whole scan result so this\n * module never has to import api-scanner, which imports it.\n */\n// webpieces-disable no-function-outside-class -- table builder, mirrors buildApiContracts in api-scanner.ts\nexport function buildExternalSystems(\n apiIndex: Map<string, ApiClassInfo>,\n projectInfos: Map<string, ProjectInfo>,\n): ExternalSystemDecls {\n const systems: ExternalSystemDecls = {};\n // First declaration wins the kind, so a later typo cannot silently reshape an already-drawn node.\n const ensure = (identity: string, kind: ExternalSystemKind, label: string): ExternalSystemDecl => {\n if (systems[identity] === undefined) systems[identity] = { kind, label, apis: [], projects: [] };\n return systems[identity];\n };\n\n for (const api of [...apiIndex.keys()].sort()) {\n const declared = apiIndex.get(api)!.externalSystem;\n if (declared === undefined) continue;\n ensure(declared.label, declared.kind, declared.label).apis.push(api);\n }\n\n for (const name of [...projectInfos.keys()].sort()) {\n for (const tag of projectInfos.get(name)!.tags) {\n const parsed = parseExternalTag(tag);\n if (parsed === null) continue;\n ensure(parsed.identity, parsed.kind, parsed.identity).projects.push(name);\n }\n }\n return systems;\n}\n\n/**\n * Resolve declarations into drawable nodes: which services actually get an arrow to each system.\n *\n * Two resolutions, matching the two declaration sites. A CONTRACT-declared system is reached by\n * every service that `uses` one of its contracts, so the arrows follow real call sites. A\n * TAG-declared system is reached ONLY by the tagged project itself — a tag asserts \"I open this\n * connection\", and fanning it out to dependents would invent arrows nobody wrote (a service that\n * depends on the entity library only for a DTO type does not talk to the database).\n *\n * A system nothing reaches is dropped rather than drawn floating: a declaration whose users all\n * disappeared is stale, and an unconnected node on the graph reads as a live dependency.\n */\n// webpieces-disable no-function-outside-class -- pure resolver, matching the sibling builders in this file\nexport function resolveExternalSystems(\n decls: ExternalSystemDecls,\n services: Record<string, RuntimeService>,\n): Record<string, RuntimeExternalSystem> {\n const out: Record<string, RuntimeExternalSystem> = {};\n for (const identity of Object.keys(decls).sort()) {\n const decl = decls[identity];\n const usedBy = new Set<string>();\n for (const name of Object.keys(services)) {\n if (services[name].uses.some((api: string) => decl.apis.includes(api))) usedBy.add(name);\n }\n // Only when the tagged project is itself a runtime node — tagging a plain library would\n // otherwise draw an arrow leaving a box that does not exist on this graph.\n for (const project of decl.projects) {\n if (services[project] !== undefined) usedBy.add(project);\n }\n if (usedBy.size === 0) continue;\n out[identity] = {\n kind: decl.kind,\n label: decl.label,\n usedBy: [...usedBy].sort(),\n apis: [...decl.apis].sort(),\n };\n }\n return out;\n}\n\n/**\n * Hang the resolved systems off the graph, and STAMP each declaring contract with its declaration.\n *\n * The stamp is what stops the same system being drawn twice: the visualizer skips an\n * `unresolvedUses` entry whose contract carries one, because that contract has already been drawn\n * with a real shape rather than as the generic grey box it would otherwise fall back to.\n *\n * A graph with nothing declared is left completely untouched — no empty key is written — so a repo\n * that adopts none of this keeps a byte-identical runtime-dependencies.json.\n */\n// webpieces-disable no-function-outside-class -- pure graph mutator, matching the sibling builders in this file\nexport function attachExternalSystems(graph: RuntimeGraph, systems: Record<string, RuntimeExternalSystem>): void {\n if (Object.keys(systems).length === 0) return;\n graph.externalSystems = systems;\n for (const system of Object.values(systems)) {\n for (const api of system.apis) {\n const entry = graph.apis[api];\n if (entry !== undefined) entry.externalSystem = { kind: system.kind, label: system.label };\n }\n }\n}\n"]}
1
+ {"version":3,"file":"external-systems.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/nx-webpieces-rules/src/lib/api-usage/external-systems.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * External system declarations\n *\n * A system OUTSIDE this repo that a service talks to — a database, a bucket, a cache. Until now\n * every one of them collapsed into the same grey dashed box, so `lib-firestore` (a datastore) was\n * indistinguishable from an HTTP service the repo happens not to implement. Declaring what a system\n * IS lets the runtime viz draw it as the thing it is.\n *\n * Split out of api-scanner.ts and runtime-graph.ts (both already at their file-size limit) so the\n * two halves of the feature — DECLARING a system and RESOLVING it to arrows — sit together.\n *\n * The two declaration sites exist 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 TS `interface` cannot carry a\n * decorator, and these seams are interfaces, so JSDoc is the only marker that fits in place.\n * - **unwrapped** — the service opens the connection itself (a `pg.Pool`, a TypeORM `DataSource`)\n * and there is no contract to mark, so the declaration is an `external:<kind>:<identity>` nx tag\n * on that PROJECT. Wrapping such a datastore purely to gain a marker is not worth it: a TypeORM\n * facade never closes, unlike the ~8 hand-picked methods a firestore seam needs.\n *\n * Both resolve to the same `(kind, identity)` pair, and identity is the NODE identity — two projects\n * declaring `postgres` converge on one cylinder with an arrow each, rather than drawing a database\n * apiece.\n */\n\nimport type { ApiClassInfo, ExternalSystemDecl, ExternalSystemDecls, ExternalSystemKind } from './api-relations';\nimport { isExternalSystemKind } from './api-relations';\nimport type { ProjectInfo } from '../project-info';\nimport type { RuntimeExternalSystem, RuntimeGraph, RuntimeService } from '../runtime-graph-model';\n\n/** nx tag prefix declaring an external system: `external:<kind>:<identity>`. */\nconst EXTERNAL_TAG_PREFIX = 'external:';\n\n/** The two halves of a parsed `external:<kind>:<identity>` nx tag. */\nclass ExternalTag {\n constructor(\n public readonly kind: ExternalSystemKind,\n public readonly identity: string,\n ) {}\n}\n\n/**\n * `external:<kind>:<identity>` -> its parts, or null for any other tag.\n *\n * Returns null (rather than throwing) on an unknown kind: this prefix shares a tag list with\n * `framework:` and `role:`, and hard-failing the whole graph generation over one malformed tag is a\n * worse outcome than not drawing one node.\n */\n// webpieces-disable no-function-outside-class -- pure parser, matching the sibling builders in this file\nfunction parseExternalTag(tag: string): ExternalTag | null {\n if (!tag.startsWith(EXTERNAL_TAG_PREFIX)) return null;\n const parts = tag.slice(EXTERNAL_TAG_PREFIX.length).split(':');\n if (parts.length !== 2) return null;\n const kind = parts[0].toLowerCase();\n const identity = parts[1].trim();\n if (!isExternalSystemKind(kind) || identity === '') return null;\n return new ExternalTag(kind, identity);\n}\n\n/**\n * The committed `externalSystems` table for architecture/dependencies.json, merging both declaration\n * sites into one identity-keyed map.\n *\n * A system may legitimately carry both: a repo can wrap a datastore behind a contract in one service\n * and open it directly in another. Takes the api index rather than the whole scan result so this\n * module never has to import api-scanner, which imports it.\n */\n// webpieces-disable no-function-outside-class -- table builder, mirrors buildApiContracts in api-scanner.ts\nexport function buildExternalSystems(\n apiIndex: Map<string, ApiClassInfo>,\n projectInfos: Map<string, ProjectInfo>,\n): ExternalSystemDecls {\n const systems: ExternalSystemDecls = {};\n // First declaration wins the kind, so a later typo cannot silently reshape an already-drawn node.\n const ensure = (identity: string, kind: ExternalSystemKind, label: string): ExternalSystemDecl => {\n if (systems[identity] === undefined) systems[identity] = { kind, label, apis: [], projects: [] };\n return systems[identity];\n };\n\n for (const api of [...apiIndex.keys()].sort()) {\n const declared = apiIndex.get(api)!.externalSystem;\n if (declared === undefined) continue;\n ensure(declared.label, declared.kind, declared.label).apis.push(api);\n }\n\n for (const name of [...projectInfos.keys()].sort()) {\n for (const tag of projectInfos.get(name)!.tags) {\n const parsed = parseExternalTag(tag);\n if (parsed === null) continue;\n ensure(parsed.identity, parsed.kind, parsed.identity).projects.push(name);\n }\n }\n return systems;\n}\n\n/**\n * Resolve declarations into drawable nodes: which services actually get an arrow to each system.\n *\n * Two resolutions, matching the two declaration sites. A CONTRACT-declared system is reached by\n * every service that `uses` one of its contracts, so the arrows follow real call sites. A\n * TAG-declared system is reached ONLY by the tagged project itself — a tag asserts \"I open this\n * connection\", and fanning it out to dependents would invent arrows nobody wrote (a service that\n * depends on the entity library only for a DTO type does not talk to the database).\n *\n * A system nothing reaches is dropped rather than drawn floating: a declaration whose users all\n * disappeared is stale, and an unconnected node on the graph reads as a live dependency.\n */\n// webpieces-disable no-function-outside-class -- pure resolver, matching the sibling builders in this file\nexport function resolveExternalSystems(\n decls: ExternalSystemDecls,\n services: Record<string, RuntimeService>,\n): Record<string, RuntimeExternalSystem> {\n const out: Record<string, RuntimeExternalSystem> = {};\n for (const identity of Object.keys(decls).sort()) {\n const decl = decls[identity];\n const usedBy = new Set<string>();\n for (const name of Object.keys(services)) {\n if (services[name].uses.some((api: string) => decl.apis.includes(api))) usedBy.add(name);\n }\n // Only when the tagged project is itself a runtime node — tagging a plain library would\n // otherwise draw an arrow leaving a box that does not exist on this graph.\n for (const project of decl.projects) {\n if (services[project] !== undefined) usedBy.add(project);\n }\n if (usedBy.size === 0) continue;\n out[identity] = {\n kind: decl.kind,\n label: decl.label,\n level: 0,\n levelBecause: 'outbound sink',\n usedBy: [...usedBy].sort(),\n apis: [...decl.apis].sort(),\n };\n }\n return out;\n}\n\n/**\n * Hang the resolved systems off the graph, and STAMP each declaring contract with its declaration.\n *\n * The stamp is what stops the same system being drawn twice: the visualizer skips an\n * `unresolvedUses` entry whose contract carries one, because that contract has already been drawn\n * with a real shape rather than as the generic grey box it would otherwise fall back to.\n *\n * A graph with nothing declared is left completely untouched — no empty key is written — so a repo\n * that adopts none of this keeps a byte-identical runtime-dependencies.json.\n */\n// webpieces-disable no-function-outside-class -- pure graph mutator, matching the sibling builders in this file\nexport function attachExternalSystems(graph: RuntimeGraph, systems: Record<string, RuntimeExternalSystem>): void {\n if (Object.keys(systems).length === 0) return;\n graph.externalSystems = systems;\n for (const system of Object.values(systems)) {\n for (const api of system.apis) {\n const entry = graph.apis[api];\n if (entry !== undefined) entry.externalSystem = { kind: system.kind, label: system.label };\n }\n }\n}\n"]}
@@ -10,7 +10,8 @@ declare class WpGraphChain {
10
10
  declare class WpRenderBand {
11
11
  readonly level: number;
12
12
  readonly nodeNames: string[];
13
- constructor(level: number, nodeNames: string[]);
13
+ readonly selfQueueNames: string[];
14
+ constructor(level: number, nodeNames: string[], selfQueueNames: string[]);
14
15
  }
15
16
  /** Original records plus fresh ranks: removed nodes cannot reappear via a rank/edge. */
16
17
  declare class WpFilteredDot {
@@ -43,9 +43,11 @@ class WpGraphChain {
43
43
  class WpRenderBand {
44
44
  level;
45
45
  nodeNames;
46
- constructor(level, nodeNames) {
46
+ selfQueueNames;
47
+ constructor(level, nodeNames, selfQueueNames) {
47
48
  this.level = level;
48
49
  this.nodeNames = nodeNames;
50
+ this.selfQueueNames = selfQueueNames;
49
51
  }
50
52
  }
51
53
  /** Original records plus fresh ranks: removed nodes cannot reappear via a rank/edge. */
@@ -64,7 +66,7 @@ class WpFilteredDot {
64
66
  const nodes = this.model.nodes.filter((node) => retained.has(node.id));
65
67
  const edges = this.model.edges.filter((edge) => retained.has(edge.from) && retained.has(edge.to));
66
68
  const bands = this.model.bands
67
- .map((band) => new WpRenderBand(band.level, band.nodeNames.filter((id) => retained.has(id))))
69
+ .map((band) => new WpRenderBand(band.level, band.nodeNames.filter((id) => retained.has(id)), band.selfQueueNames.filter((id) => retained.has(id))))
68
70
  .filter((band) => keepEmptyLevels || band.nodeNames.length > 0);
69
71
  return (this.model.header +
70
72
  nodes.map((node) => nodeDot(node)).join('') +
@@ -84,12 +86,22 @@ class WpFilteredDot {
84
86
  : '[shape=plaintext, style="", width=0.01, height=0.15, margin=0, fontsize=9, fontcolor="#9a9eab", ' +
85
87
  `label="L${band.level} · no matching projects", class="wp-layout wp-empty-level"]`;
86
88
  dot += ` "${anchor}" ${attrs};\n { rank=same; "${anchor}"; `;
87
- dot += band.nodeNames.map((id) => `${JSON.stringify(id)}; `).join('') + '}\n';
89
+ dot +=
90
+ band.nodeNames
91
+ .filter((id) => !band.selfQueueNames.includes(id))
92
+ .map((id) => `${JSON.stringify(id)}; `)
93
+ .join('') + '}\n';
94
+ if (band.selfQueueNames.length > 0) {
95
+ const subrow = `${anchor}_selfqueues`;
96
+ dot += ` "${subrow}" ${nodeAttrs};\n { rank=same; "${subrow}"; `;
97
+ dot += band.selfQueueNames.map((id) => `${JSON.stringify(id)}; `).join('') + '}\n';
98
+ dot += ` "${anchor}" -> "${subrow}" ${edgeAttrs};\n`;
99
+ }
88
100
  }
89
101
  for (let i = 0; i + 1 < bands.length; i++) {
90
102
  const upper = bands[i];
91
103
  const lower = bands[i + 1];
92
- const from = `__wp_layout_L${upper.level}`;
104
+ const from = `__wp_layout_L${upper.level}${upper.selfQueueNames.length === 0 ? '' : '_selfqueues'}`;
93
105
  const to = `__wp_layout_L${lower.level}`;
94
106
  if (upper.nodeNames.length <= 10 && lower.nodeNames.length <= 10) {
95
107
  dot += ` "${from}" -> "${to}" ${edgeAttrs};\n`;
@@ -1 +1 @@
1
- {"version":3,"file":"graph-filter.client.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/graph-filter.client.ts"],"names":[],"mappings":"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","sourcesContent":["/** Plain browser helpers shared by architecture and runtime; no CommonJS imports. */\nclass WpGraphChain {\n readonly inNodes = new Map<string, Set<string>>();\n readonly outNodes = new Map<string, Set<string>>();\n\n constructor(private readonly model: RenderModelJson) {\n const ids = new Set(model.nodes.map((node) => node.id));\n for (const edge of model.edges) {\n if (!ids.has(edge.from) || !ids.has(edge.to)) continue;\n this.add(this.outNodes, edge.from, edge.to);\n this.add(this.inNodes, edge.to, edge.from);\n }\n }\n\n private add(map: Map<string, Set<string>>, from: string, to: string): void {\n if (!map.has(from)) map.set(from, new Set());\n map.get(from)?.add(to);\n }\n\n nodes(anchor: string): Set<string> {\n const retained = new Set<string>();\n if (!this.model.nodes.some((node) => node.id === anchor)) return retained;\n for (const direction of [this.inNodes, this.outNodes]) {\n const visited = new Set([anchor]);\n const stack = [anchor];\n while (stack.length > 0) {\n const id = stack.pop() as string;\n retained.add(id);\n for (const next of direction.get(id) ?? []) {\n if (visited.has(next)) continue;\n visited.add(next);\n stack.push(next);\n }\n }\n }\n return retained;\n }\n}\n\nclass WpRenderBand {\n constructor(\n readonly level: number,\n readonly nodeNames: string[],\n ) {}\n}\n\n/** Original records plus fresh ranks: removed nodes cannot reappear via a rank/edge. */\nclass WpFilteredDot {\n constructor(private readonly model: RenderModelJson) {}\n\n /**\n * `nodeDot` picks each box's statement — the architecture page passes its current color mode's —\n * and `edgeDot` each edge's (the Product mode dashes a cross-product dependency).\n * `keepEmptyLevels` keeps a level whose boxes were all filtered out as a thin labeled band, so\n * every remaining box stays in its own L-row (the architecture page); otherwise it is dropped.\n */\n render(\n retained: Set<string>,\n nodeDot: (node: RenderNodeJson) => string = (node: RenderNodeJson): string => node.dot,\n keepEmptyLevels = false,\n edgeDot: (edge: RenderEdgeJson) => string = (edge: RenderEdgeJson): string => edge.dot,\n ): string {\n const nodes = this.model.nodes.filter((node: RenderNodeJson): boolean => retained.has(node.id));\n const edges = this.model.edges.filter(\n (edge: RenderEdgeJson): boolean => retained.has(edge.from) && retained.has(edge.to),\n );\n const bands = this.model.bands\n .map(\n (band: RenderBandJson): WpRenderBand =>\n new WpRenderBand(\n band.level,\n band.nodeNames.filter((id: string): boolean => retained.has(id)),\n ),\n )\n .filter((band: WpRenderBand): boolean => keepEmptyLevels || band.nodeNames.length > 0);\n return (\n this.model.header +\n nodes.map((node: RenderNodeJson): string => nodeDot(node)).join('') +\n this.bandDot(bands) +\n edges.map((edge: RenderEdgeJson): string => edgeDot(edge)).join('') +\n this.model.footer\n );\n }\n\n private bandDot(bands: WpRenderBand[]): string {\n const nodeAttrs =\n '[style=invis, shape=point, width=0.01, height=0.01, label=\"\", class=\"wp-layout\"]';\n const edgeAttrs = '[style=invis, class=\"wp-layout\"]';\n let dot = '';\n for (const band of bands) {\n const anchor = `__wp_layout_L${band.level}`;\n // The same bytes as LevelBandLayout.emptyBandAttrs (graph-level-bands.ts).\n const attrs =\n band.nodeNames.length > 0\n ? nodeAttrs\n : '[shape=plaintext, style=\"\", width=0.01, height=0.15, margin=0, fontsize=9, fontcolor=\"#9a9eab\", ' +\n `label=\"L${band.level} · no matching projects\", class=\"wp-layout wp-empty-level\"]`;\n dot += ` \"${anchor}\" ${attrs};\\n { rank=same; \"${anchor}\"; `;\n dot += band.nodeNames.map((id) => `${JSON.stringify(id)}; `).join('') + '}\\n';\n }\n for (let i = 0; i + 1 < bands.length; i++) {\n const upper = bands[i];\n const lower = bands[i + 1];\n const from = `__wp_layout_L${upper.level}`;\n const to = `__wp_layout_L${lower.level}`;\n if (upper.nodeNames.length <= 10 && lower.nodeNames.length <= 10) {\n dot += ` \"${from}\" -> \"${to}\" ${edgeAttrs};\\n`;\n continue;\n }\n const spacer = `__wp_layout_spacer_L${upper.level}_L${lower.level}`;\n dot += ` \"${spacer}\" ${nodeAttrs};\\n { rank=same; \"${spacer}\"; }\\n`;\n dot += ` \"${from}\" -> \"${spacer}\" ${edgeAttrs};\\n \"${spacer}\" -> \"${to}\" ${edgeAttrs};\\n`;\n }\n return dot;\n }\n}\n\n/** Page-owned state survives replacements; rendering failures leave the usable SVG intact. */\nabstract class WpFilterPage {\n protected readonly chain: WpGraphChain;\n protected locked: string | null = null;\n protected anchor: string | null = null;\n protected retained: Set<string>;\n private viz: VizInstance | null = null;\n\n constructor(protected readonly model: RenderModelJson) {\n this.chain = new WpGraphChain(model);\n this.retained = new Set(model.nodes.map((node) => node.id));\n }\n\n render(): void {\n document\n .getElementById('wp-filter-off')\n ?.addEventListener('click', () => this.filter(null));\n this.wireControls();\n Viz.instance()\n .then((viz) => {\n this.viz = viz;\n this.filter(null);\n })\n // webpieces-disable no-any-unknown -- promise rejections may carry any JavaScript value\n .catch((err: unknown): void =>\n this.error(err instanceof Error ? err : new Error(String(err))),\n );\n }\n\n protected captureBinding(_svg: SVGSVGElement | null): () => void {\n return (): void => {};\n }\n protected wireControls(): void {}\n protected abstract wireSvg(svg: SVGSVGElement): void;\n protected prepareSvg(_svg: SVGSVGElement): void {}\n /** The statement drawn for one box. The architecture page answers with its color mode's. */\n protected nodeDot(node: RenderNodeJson): string {\n return node.dot;\n }\n /** The statement drawn for one edge. The architecture page answers with its color mode's. */\n protected edgeDot(edge: RenderEdgeJson): string {\n return edge.dot;\n }\n /** Whether an unfiltered draw may use the model's precomputed `fullDot` verbatim. */\n protected usesFullDot(): boolean {\n return true;\n }\n /**\n * The page's own filter on top of \"Hide unconnected\": the architecture page intersects the set\n * with its Filter popover (changes, runtime, role). Everything, by default.\n */\n protected narrow(retained: Set<string>): Set<string> {\n return retained;\n }\n /** Whether a level emptied by filtering stays as a thin labeled band (see WpFilteredDot.render). */\n protected keepsEmptyLevels(): boolean {\n return false;\n }\n\n /**\n * Draw again with the current filter — after a color-mode switch. A control outside the graph\n * (a drawer button) keeps keyboard focus; the redraw would otherwise move it into the graph.\n */\n redraw(): void {\n const active = document.activeElement;\n const host = document.getElementById('graph');\n const outside = active instanceof HTMLElement && host !== null && !host.contains(active);\n this.filter(this.anchor);\n if (outside && active.isConnected) active.focus({ preventScroll: true });\n }\n\n filterItem(name: string): WpNodeMenuItem {\n return new WpNodeMenuItem(\n this.anchor === null ? 'Filter Unconnected' : 'Turn off Filter',\n () => this.filter(this.anchor === null ? name : null),\n );\n }\n\n filter(anchor: string | null): void {\n if (this.viz === null) return;\n const host = document.getElementById('graph');\n if (host === null) return;\n const retained = this.narrow(\n anchor === null\n ? new Set(this.model.nodes.map((node: RenderNodeJson): string => node.id))\n : this.chain.nodes(anchor),\n );\n const previousAnchor = this.anchor;\n const previousRetained = this.retained;\n const previousSvg = host.querySelector('svg');\n const focusName = WpNodeMenu.focusedName(previousSvg);\n const rollback = this.captureBinding(previousSvg);\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- browser transaction restores the previous usable graph on failure\n try {\n const dot =\n anchor === null && this.usesFullDot()\n ? this.model.fullDot\n : new WpFilteredDot(this.model).render(\n retained,\n (node: RenderNodeJson): string => this.nodeDot(node),\n this.keepsEmptyLevels(),\n (edge: RenderEdgeJson): string => this.edgeDot(edge),\n );\n const svg = this.viz.renderSVGElement(dot);\n this.prepareSvg(svg);\n WpNodeMenu.close();\n this.anchor = anchor;\n this.retained = retained;\n host.replaceChildren(svg);\n this.wireSvg(svg);\n this.indicator();\n WpNodeMenu.restoreFocus(svg, focusName);\n this.error(null);\n // webpieces-disable no-any-unknown -- JavaScript may throw any value at this browser boundary\n } catch (err: unknown) {\n //const error = toError(err);\n this.anchor = previousAnchor;\n this.retained = previousRetained;\n if (previousSvg !== null) host.replaceChildren(previousSvg);\n else host.replaceChildren();\n rollback();\n if (previousSvg !== null) WpNodeMenu.restoreFocus(previousSvg, focusName);\n this.error(err instanceof Error ? err : new Error(String(err)));\n }\n }\n\n private indicator(): void {\n const status = document.getElementById('wp-filter-status');\n const label = document.getElementById('wp-filter-anchor');\n if (status !== null) status.hidden = this.anchor === null;\n if (label !== null) label.textContent = this.anchor;\n if (this.anchor !== null) document.getElementById('wp-filter-off')?.focus();\n else document.querySelector<SVGGElement>('#graph g.wp-node-clickable')?.focus();\n }\n\n private error(err: Error | null): void {\n const el = document.getElementById('wp-graph-error');\n if (el === null) return;\n el.hidden = err === null;\n el.textContent =\n err === null\n ? ''\n : `Graph rendering failed: ${err.message}. Try Turn off Filter or reload the page.`;\n }\n}\n"]}
1
+ {"version":3,"file":"graph-filter.client.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/graph-filter.client.ts"],"names":[],"mappings":"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","sourcesContent":["/** Plain browser helpers shared by architecture and runtime; no CommonJS imports. */\nclass WpGraphChain {\n readonly inNodes = new Map<string, Set<string>>();\n readonly outNodes = new Map<string, Set<string>>();\n\n constructor(private readonly model: RenderModelJson) {\n const ids = new Set(model.nodes.map((node) => node.id));\n for (const edge of model.edges) {\n if (!ids.has(edge.from) || !ids.has(edge.to)) continue;\n this.add(this.outNodes, edge.from, edge.to);\n this.add(this.inNodes, edge.to, edge.from);\n }\n }\n\n private add(map: Map<string, Set<string>>, from: string, to: string): void {\n if (!map.has(from)) map.set(from, new Set());\n map.get(from)?.add(to);\n }\n\n nodes(anchor: string): Set<string> {\n const retained = new Set<string>();\n if (!this.model.nodes.some((node) => node.id === anchor)) return retained;\n for (const direction of [this.inNodes, this.outNodes]) {\n const visited = new Set([anchor]);\n const stack = [anchor];\n while (stack.length > 0) {\n const id = stack.pop() as string;\n retained.add(id);\n for (const next of direction.get(id) ?? []) {\n if (visited.has(next)) continue;\n visited.add(next);\n stack.push(next);\n }\n }\n }\n return retained;\n }\n}\n\nclass WpRenderBand {\n constructor(\n readonly level: number,\n readonly nodeNames: string[],\n readonly selfQueueNames: string[],\n ) {}\n}\n\n/** Original records plus fresh ranks: removed nodes cannot reappear via a rank/edge. */\nclass WpFilteredDot {\n constructor(private readonly model: RenderModelJson) {}\n\n /**\n * `nodeDot` picks each box's statement — the architecture page passes its current color mode's —\n * and `edgeDot` each edge's (the Product mode dashes a cross-product dependency).\n * `keepEmptyLevels` keeps a level whose boxes were all filtered out as a thin labeled band, so\n * every remaining box stays in its own L-row (the architecture page); otherwise it is dropped.\n */\n render(\n retained: Set<string>,\n nodeDot: (node: RenderNodeJson) => string = (node: RenderNodeJson): string => node.dot,\n keepEmptyLevels = false,\n edgeDot: (edge: RenderEdgeJson) => string = (edge: RenderEdgeJson): string => edge.dot,\n ): string {\n const nodes = this.model.nodes.filter((node: RenderNodeJson): boolean =>\n retained.has(node.id),\n );\n const edges = this.model.edges.filter(\n (edge: RenderEdgeJson): boolean => retained.has(edge.from) && retained.has(edge.to),\n );\n const bands = this.model.bands\n .map(\n (band: RenderBandJson): WpRenderBand =>\n new WpRenderBand(\n band.level,\n band.nodeNames.filter((id: string): boolean => retained.has(id)),\n band.selfQueueNames.filter((id: string): boolean => retained.has(id)),\n ),\n )\n .filter((band: WpRenderBand): boolean => keepEmptyLevels || band.nodeNames.length > 0);\n return (\n this.model.header +\n nodes.map((node: RenderNodeJson): string => nodeDot(node)).join('') +\n this.bandDot(bands) +\n edges.map((edge: RenderEdgeJson): string => edgeDot(edge)).join('') +\n this.model.footer\n );\n }\n\n private bandDot(bands: WpRenderBand[]): string {\n const nodeAttrs =\n '[style=invis, shape=point, width=0.01, height=0.01, label=\"\", class=\"wp-layout\"]';\n const edgeAttrs = '[style=invis, class=\"wp-layout\"]';\n let dot = '';\n for (const band of bands) {\n const anchor = `__wp_layout_L${band.level}`;\n // The same bytes as LevelBandLayout.emptyBandAttrs (graph-level-bands.ts).\n const attrs =\n band.nodeNames.length > 0\n ? nodeAttrs\n : '[shape=plaintext, style=\"\", width=0.01, height=0.15, margin=0, fontsize=9, fontcolor=\"#9a9eab\", ' +\n `label=\"L${band.level} · no matching projects\", class=\"wp-layout wp-empty-level\"]`;\n dot += ` \"${anchor}\" ${attrs};\\n { rank=same; \"${anchor}\"; `;\n dot +=\n band.nodeNames\n .filter((id) => !band.selfQueueNames.includes(id))\n .map((id) => `${JSON.stringify(id)}; `)\n .join('') + '}\\n';\n if (band.selfQueueNames.length > 0) {\n const subrow = `${anchor}_selfqueues`;\n dot += ` \"${subrow}\" ${nodeAttrs};\\n { rank=same; \"${subrow}\"; `;\n dot += band.selfQueueNames.map((id) => `${JSON.stringify(id)}; `).join('') + '}\\n';\n dot += ` \"${anchor}\" -> \"${subrow}\" ${edgeAttrs};\\n`;\n }\n }\n for (let i = 0; i + 1 < bands.length; i++) {\n const upper = bands[i];\n const lower = bands[i + 1];\n const from = `__wp_layout_L${upper.level}${upper.selfQueueNames.length === 0 ? '' : '_selfqueues'}`;\n const to = `__wp_layout_L${lower.level}`;\n if (upper.nodeNames.length <= 10 && lower.nodeNames.length <= 10) {\n dot += ` \"${from}\" -> \"${to}\" ${edgeAttrs};\\n`;\n continue;\n }\n const spacer = `__wp_layout_spacer_L${upper.level}_L${lower.level}`;\n dot += ` \"${spacer}\" ${nodeAttrs};\\n { rank=same; \"${spacer}\"; }\\n`;\n dot += ` \"${from}\" -> \"${spacer}\" ${edgeAttrs};\\n \"${spacer}\" -> \"${to}\" ${edgeAttrs};\\n`;\n }\n return dot;\n }\n}\n\n/** Page-owned state survives replacements; rendering failures leave the usable SVG intact. */\nabstract class WpFilterPage {\n protected readonly chain: WpGraphChain;\n protected locked: string | null = null;\n protected anchor: string | null = null;\n protected retained: Set<string>;\n private viz: VizInstance | null = null;\n\n constructor(protected readonly model: RenderModelJson) {\n this.chain = new WpGraphChain(model);\n this.retained = new Set(model.nodes.map((node) => node.id));\n }\n\n render(): void {\n document\n .getElementById('wp-filter-off')\n ?.addEventListener('click', () => this.filter(null));\n this.wireControls();\n Viz.instance()\n .then((viz) => {\n this.viz = viz;\n this.filter(null);\n })\n // webpieces-disable no-any-unknown -- promise rejections may carry any JavaScript value\n .catch((err: unknown): void =>\n this.error(err instanceof Error ? err : new Error(String(err))),\n );\n }\n\n protected captureBinding(_svg: SVGSVGElement | null): () => void {\n return (): void => {};\n }\n protected wireControls(): void {}\n protected abstract wireSvg(svg: SVGSVGElement): void;\n protected prepareSvg(_svg: SVGSVGElement): void {}\n /** The statement drawn for one box. The architecture page answers with its color mode's. */\n protected nodeDot(node: RenderNodeJson): string {\n return node.dot;\n }\n /** The statement drawn for one edge. The architecture page answers with its color mode's. */\n protected edgeDot(edge: RenderEdgeJson): string {\n return edge.dot;\n }\n /** Whether an unfiltered draw may use the model's precomputed `fullDot` verbatim. */\n protected usesFullDot(): boolean {\n return true;\n }\n /**\n * The page's own filter on top of \"Hide unconnected\": the architecture page intersects the set\n * with its Filter popover (changes, runtime, role). Everything, by default.\n */\n protected narrow(retained: Set<string>): Set<string> {\n return retained;\n }\n /** Whether a level emptied by filtering stays as a thin labeled band (see WpFilteredDot.render). */\n protected keepsEmptyLevels(): boolean {\n return false;\n }\n\n /**\n * Draw again with the current filter — after a color-mode switch. A control outside the graph\n * (a drawer button) keeps keyboard focus; the redraw would otherwise move it into the graph.\n */\n redraw(): void {\n const active = document.activeElement;\n const host = document.getElementById('graph');\n const outside = active instanceof HTMLElement && host !== null && !host.contains(active);\n this.filter(this.anchor);\n if (outside && active.isConnected) active.focus({ preventScroll: true });\n }\n\n filterItem(name: string): WpNodeMenuItem {\n return new WpNodeMenuItem(\n this.anchor === null ? 'Filter Unconnected' : 'Turn off Filter',\n () => this.filter(this.anchor === null ? name : null),\n );\n }\n\n filter(anchor: string | null): void {\n if (this.viz === null) return;\n const host = document.getElementById('graph');\n if (host === null) return;\n const retained = this.narrow(\n anchor === null\n ? new Set(this.model.nodes.map((node: RenderNodeJson): string => node.id))\n : this.chain.nodes(anchor),\n );\n const previousAnchor = this.anchor;\n const previousRetained = this.retained;\n const previousSvg = host.querySelector('svg');\n const focusName = WpNodeMenu.focusedName(previousSvg);\n const rollback = this.captureBinding(previousSvg);\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- browser transaction restores the previous usable graph on failure\n try {\n const dot =\n anchor === null && this.usesFullDot()\n ? this.model.fullDot\n : new WpFilteredDot(this.model).render(\n retained,\n (node: RenderNodeJson): string => this.nodeDot(node),\n this.keepsEmptyLevels(),\n (edge: RenderEdgeJson): string => this.edgeDot(edge),\n );\n const svg = this.viz.renderSVGElement(dot);\n this.prepareSvg(svg);\n WpNodeMenu.close();\n this.anchor = anchor;\n this.retained = retained;\n host.replaceChildren(svg);\n this.wireSvg(svg);\n this.indicator();\n WpNodeMenu.restoreFocus(svg, focusName);\n this.error(null);\n // webpieces-disable no-any-unknown -- JavaScript may throw any value at this browser boundary\n } catch (err: unknown) {\n //const error = toError(err);\n this.anchor = previousAnchor;\n this.retained = previousRetained;\n if (previousSvg !== null) host.replaceChildren(previousSvg);\n else host.replaceChildren();\n rollback();\n if (previousSvg !== null) WpNodeMenu.restoreFocus(previousSvg, focusName);\n this.error(err instanceof Error ? err : new Error(String(err)));\n }\n }\n\n private indicator(): void {\n const status = document.getElementById('wp-filter-status');\n const label = document.getElementById('wp-filter-anchor');\n if (status !== null) status.hidden = this.anchor === null;\n if (label !== null) label.textContent = this.anchor;\n if (this.anchor !== null) document.getElementById('wp-filter-off')?.focus();\n else document.querySelector<SVGGElement>('#graph g.wp-node-clickable')?.focus();\n }\n\n private error(err: Error | null): void {\n const el = document.getElementById('wp-graph-error');\n if (el === null) return;\n el.hidden = err === null;\n el.textContent =\n err === null\n ? ''\n : `Graph rendering failed: ${err.message}. Try Turn off Filter or reload the page.`;\n }\n}\n"]}
@@ -3,6 +3,8 @@
3
3
  * invisible anchor that pins the band's vertical position.
4
4
  */
5
5
  export declare class LevelBand {
6
+ /** Pure self-queues retain this level but occupy a presentation-only row below its services. */
7
+ selfQueueNames: string[];
6
8
  level: number;
7
9
  /** Short names of the visible projects on this level, sorted for deterministic output. */
8
10
  nodeNames: string[];
@@ -17,6 +19,7 @@ export declare class LevelBandLayout {
17
19
  * whichever ones exist.
18
20
  */
19
21
  dot(bands: LevelBand[]): string;
22
+ private selfQueueRow;
20
23
  /**
21
24
  * A level with NO box left in it (the page's Filter removed them all) is never dropped: the next
22
25
  * level would slide up into its row, so L0 would stop being the bottom row. Its anchor becomes a
@@ -44,6 +44,8 @@ const LAYOUT_EDGE_ATTRS = `[style=invis, class="${LAYOUT_CLASS}"]`;
44
44
  * invisible anchor that pins the band's vertical position.
45
45
  */
46
46
  class LevelBand {
47
+ /** Pure self-queues retain this level but occupy a presentation-only row below its services. */
48
+ selfQueueNames = [];
47
49
  level;
48
50
  /** Short names of the visible projects on this level, sorted for deterministic output. */
49
51
  nodeNames;
@@ -69,15 +71,26 @@ class LevelBandLayout {
69
71
  const attrs = band.nodeNames.length === 0 ? this.emptyBandAttrs(band.level) : LAYOUT_NODE_ATTRS;
70
72
  dot += ` "${band.anchorName()}" ${attrs};\n`;
71
73
  dot += ` { rank=same; "${band.anchorName()}"; `;
72
- for (const name of band.nodeNames)
74
+ for (const name of band.nodeNames.filter((node) => !band.selfQueueNames.includes(node)))
73
75
  dot += `"${(0, dot_syntax_1.dotValue)(name)}"; `;
74
76
  dot += '}\n';
77
+ dot += this.selfQueueRow(band);
75
78
  }
76
79
  for (let i = 0; i + 1 < bands.length; i++) {
77
80
  dot += this.bandGap(bands[i], bands[i + 1]);
78
81
  }
79
82
  return dot;
80
83
  }
84
+ selfQueueRow(band) {
85
+ if (band.selfQueueNames.length === 0)
86
+ return '';
87
+ const anchor = `${band.anchorName()}_selfqueues`;
88
+ return (` "${anchor}" ${LAYOUT_NODE_ATTRS};\n` +
89
+ ` { rank=same; "${anchor}"; ` +
90
+ band.selfQueueNames.map((name) => `"${(0, dot_syntax_1.dotValue)(name)}"; `).join('') +
91
+ '}\n' +
92
+ ` "${band.anchorName()}" -> "${anchor}" ${LAYOUT_EDGE_ATTRS};\n`);
93
+ }
81
94
  /**
82
95
  * A level with NO box left in it (the page's Filter removed them all) is never dropped: the next
83
96
  * level would slide up into its row, so L0 would stop being the bottom row. Its anchor becomes a
@@ -96,16 +109,19 @@ class LevelBandLayout {
96
109
  * straight through the row below.
97
110
  */
98
111
  bandGap(upper, lower) {
99
- const crowded = upper.nodeNames.length > SPACER_MIN_BAND_SIZE
100
- || lower.nodeNames.length > SPACER_MIN_BAND_SIZE;
112
+ const upperAnchor = upper.selfQueueNames.length === 0
113
+ ? upper.anchorName()
114
+ : `${upper.anchorName()}_selfqueues`;
115
+ const crowded = upper.nodeNames.length > SPACER_MIN_BAND_SIZE ||
116
+ lower.nodeNames.length > SPACER_MIN_BAND_SIZE;
101
117
  if (!crowded) {
102
- return ` "${upper.anchorName()}" -> "${lower.anchorName()}" ${LAYOUT_EDGE_ATTRS};\n`;
118
+ return ` "${upperAnchor}" -> "${lower.anchorName()}" ${LAYOUT_EDGE_ATTRS};\n`;
103
119
  }
104
120
  const spacer = `__wp_layout_spacer_L${upper.level}_L${lower.level}`;
105
- return ` "${spacer}" ${LAYOUT_NODE_ATTRS};\n`
106
- + ` { rank=same; "${spacer}"; }\n`
107
- + ` "${upper.anchorName()}" -> "${spacer}" ${LAYOUT_EDGE_ATTRS};\n`
108
- + ` "${spacer}" -> "${lower.anchorName()}" ${LAYOUT_EDGE_ATTRS};\n`;
121
+ return (` "${spacer}" ${LAYOUT_NODE_ATTRS};\n` +
122
+ ` { rank=same; "${spacer}"; }\n` +
123
+ ` "${upperAnchor}" -> "${spacer}" ${LAYOUT_EDGE_ATTRS};\n` +
124
+ ` "${spacer}" -> "${lower.anchorName()}" ${LAYOUT_EDGE_ATTRS};\n`);
109
125
  }
110
126
  }
111
127
  exports.LevelBandLayout = LevelBandLayout;
@@ -1 +1 @@
1
- {"version":3,"file":"graph-level-bands.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/graph-level-bands.ts"],"names":[],"mappings":";;;AAAA,6CAAwC;AAExC;;;;;;;;;;;;;;;;;;;GAmBG;AAEH;;;;GAIG;AACH,MAAM,oBAAoB,GAAG,EAAE,CAAC;AAEhC;;;;;GAKG;AACH,MAAM,YAAY,GAAG,WAAW,CAAC;AAEjC,wFAAwF;AACxF,MAAM,iBAAiB,GACnB,wEAAwE,YAAY,IAAI,CAAC;AAE7F,sFAAsF;AACtF,MAAM,iBAAiB,GAAG,wBAAwB,YAAY,IAAI,CAAC;AAEnE;;;GAGG;AACH,MAAa,SAAS;IAClB,KAAK,CAAS;IACd,0FAA0F;IAC1F,SAAS,CAAW;IAEpB,YAAY,KAAa,EAAE,SAAmB;QAC1C,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;QACnB,IAAI,CAAC,SAAS,GAAG,SAAS,CAAC;IAC/B,CAAC;IAED,UAAU;QACN,OAAO,gBAAgB,IAAI,CAAC,KAAK,EAAE,CAAC;IACxC,CAAC;CACJ;AAbD,8BAaC;AAED,oFAAoF;AACpF,MAAa,eAAe;IACxB;;;;OAIG;IACH,GAAG,CAAC,KAAkB;QAClB,IAAI,GAAG,GAAG,EAAE,CAAC;QACb,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;YACvB,MAAM,KAAK,GAAG,IAAI,CAAC,SAAS,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,iBAAiB,CAAC;YAChG,GAAG,IAAI,MAAM,IAAI,CAAC,UAAU,EAAE,KAAK,KAAK,KAAK,CAAC;YAC9C,GAAG,IAAI,mBAAmB,IAAI,CAAC,UAAU,EAAE,KAAK,CAAC;YACjD,KAAK,MAAM,IAAI,IAAI,IAAI,CAAC,SAAS;gBAAE,GAAG,IAAI,IAAI,IAAA,qBAAQ,EAAC,IAAI,CAAC,KAAK,CAAC;YAClE,GAAG,IAAI,KAAK,CAAC;QACjB,CAAC;QACD,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;YACxC,GAAG,IAAI,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;QAChD,CAAC;QACD,OAAO,GAAG,CAAC;IACf,CAAC;IAED;;;;;;OAMG;IACH,cAAc,CAAC,KAAa;QACxB,OAAO,CACH,kGAAkG;YAClG,WAAW,KAAK,oCAAoC,YAAY,mBAAmB,CACtF,CAAC;IACN,CAAC;IAED;;;;;OAKG;IACK,OAAO,CAAC,KAAgB,EAAE,KAAgB;QAC9C,MAAM,OAAO,GAAG,KAAK,CAAC,SAAS,CAAC,MAAM,GAAG,oBAAoB;eACtD,KAAK,CAAC,SAAS,CAAC,MAAM,GAAG,oBAAoB,CAAC;QACrD,IAAI,CAAC,OAAO,EAAE,CAAC;YACX,OAAO,MAAM,KAAK,CAAC,UAAU,EAAE,SAAS,KAAK,CAAC,UAAU,EAAE,KAAK,iBAAiB,KAAK,CAAC;QAC1F,CAAC;QACD,MAAM,MAAM,GAAG,uBAAuB,KAAK,CAAC,KAAK,KAAK,KAAK,CAAC,KAAK,EAAE,CAAC;QACpE,OAAO,MAAM,MAAM,KAAK,iBAAiB,KAAK;cACxC,mBAAmB,MAAM,QAAQ;cACjC,MAAM,KAAK,CAAC,UAAU,EAAE,SAAS,MAAM,KAAK,iBAAiB,KAAK;cAClE,MAAM,MAAM,SAAS,KAAK,CAAC,UAAU,EAAE,KAAK,iBAAiB,KAAK,CAAC;IAC7E,CAAC;CACJ;AArDD,0CAqDC","sourcesContent":["import { dotValue } from './dot-syntax';\n\n/**\n * Level bands for the architecture graph.\n *\n * Every row of architecture/dependencies.html is ONE dependency level: the highest level on top,\n * descending as you read down, L0 — the foundation everything is built on — always last. The level\n * is the one stored in dependencies.json, where every server, client and app is already pinned to the\n * top level (graph-entry-levels.ts, #1179), so the top band is exactly the entry points.\n *\n * `{ rank=same; ... }` alone does not deliver that, and assuming it did was the bug. It ties a\n * level's boxes to one row but says nothing about where that row goes, so graphviz derived each\n * row's position from the edges. A level containing a box with no visible edges — a leaf sdk, an\n * api-lib whose only consumers are hidden — is unconstrained, so it was placed at the top rank and\n * dragged its ENTIRE level up with it (verified against graphviz: that is what put the L0 sdks in\n * the top row, and what parked an L0 lib in the same row as the L6 servers).\n *\n * So the ordering is STATED here rather than inferred: one invisible anchor per band, sitting in\n * that band's rank set, chained top-to-bottom with invisible edges. Ranks are then a function of\n * level and nothing else. The anchors — never the real boxes — carry the chain, so no fake\n * dependency is ever drawn between two projects.\n */\n\n/**\n * A band bigger than this gets a blank spacer band inserted next to it, so the long edges leaving a\n * crowded row have vertical room to fan out instead of cutting straight through the row below.\n * Eleven-plus boxes in a row is where that became unreadable in practice.\n */\nconst SPACER_MIN_BAND_SIZE = 10;\n\n/**\n * The class stamped on every node and edge that exists ONLY to pin the layout. Graphviz drops\n * `style=invis` elements from its SVG entirely, so nothing carrying this class reaches the page\n * today; the class is how the DOT tells the browser client which elements are scaffolding, and the\n * client skips them when indexing so an anchor can never be walked as a dependency.\n */\nconst LAYOUT_CLASS = 'wp-layout';\n\n/** Attributes shared by every invisible layout node (rank anchors and spacer bands). */\nconst LAYOUT_NODE_ATTRS =\n `[style=invis, shape=point, width=0.01, height=0.01, label=\"\", class=\"${LAYOUT_CLASS}\"]`;\n\n/** Attributes for the invisible edges that force one band strictly above the next. */\nconst LAYOUT_EDGE_ATTRS = `[style=invis, class=\"${LAYOUT_CLASS}\"]`;\n\n/**\n * One horizontal band of the graph: every visible project sharing a dependency level, plus the\n * invisible anchor that pins the band's vertical position.\n */\nexport class LevelBand {\n level: number;\n /** Short names of the visible projects on this level, sorted for deterministic output. */\n nodeNames: string[];\n\n constructor(level: number, nodeNames: string[]) {\n this.level = level;\n this.nodeNames = nodeNames;\n }\n\n anchorName(): string {\n return `__wp_layout_L${this.level}`;\n }\n}\n\n/** Emits the DOT that pins each band to its own rank, in descending level order. */\nexport class LevelBandLayout {\n /**\n * @param bands the visible projects grouped by level, HIGHEST LEVEL FIRST. Levels need not be\n * contiguous — only the levels actually present get a band, and the chain links\n * whichever ones exist.\n */\n dot(bands: LevelBand[]): string {\n let dot = '';\n for (const band of bands) {\n const attrs = band.nodeNames.length === 0 ? this.emptyBandAttrs(band.level) : LAYOUT_NODE_ATTRS;\n dot += ` \"${band.anchorName()}\" ${attrs};\\n`;\n dot += ` { rank=same; \"${band.anchorName()}\"; `;\n for (const name of band.nodeNames) dot += `\"${dotValue(name)}\"; `;\n dot += '}\\n';\n }\n for (let i = 0; i + 1 < bands.length; i++) {\n dot += this.bandGap(bands[i], bands[i + 1]);\n }\n return dot;\n }\n\n /**\n * A level with NO box left in it (the page's Filter removed them all) is never dropped: the next\n * level would slide up into its row, so L0 would stop being the bottom row. Its anchor becomes a\n * thin plain-text band instead — visible, labeled, still in the chain — so every remaining box\n * stays in its own L-row. Still `wp-layout`: never clickable, never walked as a dependency.\n * graph-filter.client.ts mirrors these bytes (a global browser script cannot import).\n */\n emptyBandAttrs(level: number): string {\n return (\n '[shape=plaintext, style=\"\", width=0.01, height=0.15, margin=0, fontsize=9, fontcolor=\"#9a9eab\", ' +\n `label=\"L${level} · no matching projects\", class=\"${LAYOUT_CLASS} wp-empty-level\"]`\n );\n }\n\n /**\n * The ordering chain between two adjacent bands. When either band is crowded\n * (> SPACER_MIN_BAND_SIZE boxes), an empty spacer band goes between them: the edges leaving a\n * wide row then have a whole rank of vertical room to fan out in, instead of being drawn\n * straight through the row below.\n */\n private bandGap(upper: LevelBand, lower: LevelBand): string {\n const crowded = upper.nodeNames.length > SPACER_MIN_BAND_SIZE\n || lower.nodeNames.length > SPACER_MIN_BAND_SIZE;\n if (!crowded) {\n return ` \"${upper.anchorName()}\" -> \"${lower.anchorName()}\" ${LAYOUT_EDGE_ATTRS};\\n`;\n }\n const spacer = `__wp_layout_spacer_L${upper.level}_L${lower.level}`;\n return ` \"${spacer}\" ${LAYOUT_NODE_ATTRS};\\n`\n + ` { rank=same; \"${spacer}\"; }\\n`\n + ` \"${upper.anchorName()}\" -> \"${spacer}\" ${LAYOUT_EDGE_ATTRS};\\n`\n + ` \"${spacer}\" -> \"${lower.anchorName()}\" ${LAYOUT_EDGE_ATTRS};\\n`;\n }\n}\n"]}
1
+ {"version":3,"file":"graph-level-bands.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/graph-level-bands.ts"],"names":[],"mappings":"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","sourcesContent":["import { dotValue } from './dot-syntax';\n\n/**\n * Level bands for the architecture graph.\n *\n * Every row of architecture/dependencies.html is ONE dependency level: the highest level on top,\n * descending as you read down, L0 — the foundation everything is built on — always last. The level\n * is the one stored in dependencies.json, where every server, client and app is already pinned to the\n * top level (graph-entry-levels.ts, #1179), so the top band is exactly the entry points.\n *\n * `{ rank=same; ... }` alone does not deliver that, and assuming it did was the bug. It ties a\n * level's boxes to one row but says nothing about where that row goes, so graphviz derived each\n * row's position from the edges. A level containing a box with no visible edges — a leaf sdk, an\n * api-lib whose only consumers are hidden — is unconstrained, so it was placed at the top rank and\n * dragged its ENTIRE level up with it (verified against graphviz: that is what put the L0 sdks in\n * the top row, and what parked an L0 lib in the same row as the L6 servers).\n *\n * So the ordering is STATED here rather than inferred: one invisible anchor per band, sitting in\n * that band's rank set, chained top-to-bottom with invisible edges. Ranks are then a function of\n * level and nothing else. The anchors — never the real boxes — carry the chain, so no fake\n * dependency is ever drawn between two projects.\n */\n\n/**\n * A band bigger than this gets a blank spacer band inserted next to it, so the long edges leaving a\n * crowded row have vertical room to fan out instead of cutting straight through the row below.\n * Eleven-plus boxes in a row is where that became unreadable in practice.\n */\nconst SPACER_MIN_BAND_SIZE = 10;\n\n/**\n * The class stamped on every node and edge that exists ONLY to pin the layout. Graphviz drops\n * `style=invis` elements from its SVG entirely, so nothing carrying this class reaches the page\n * today; the class is how the DOT tells the browser client which elements are scaffolding, and the\n * client skips them when indexing so an anchor can never be walked as a dependency.\n */\nconst LAYOUT_CLASS = 'wp-layout';\n\n/** Attributes shared by every invisible layout node (rank anchors and spacer bands). */\nconst LAYOUT_NODE_ATTRS = `[style=invis, shape=point, width=0.01, height=0.01, label=\"\", class=\"${LAYOUT_CLASS}\"]`;\n\n/** Attributes for the invisible edges that force one band strictly above the next. */\nconst LAYOUT_EDGE_ATTRS = `[style=invis, class=\"${LAYOUT_CLASS}\"]`;\n\n/**\n * One horizontal band of the graph: every visible project sharing a dependency level, plus the\n * invisible anchor that pins the band's vertical position.\n */\nexport class LevelBand {\n /** Pure self-queues retain this level but occupy a presentation-only row below its services. */\n selfQueueNames: string[] = [];\n level: number;\n /** Short names of the visible projects on this level, sorted for deterministic output. */\n nodeNames: string[];\n\n constructor(level: number, nodeNames: string[]) {\n this.level = level;\n this.nodeNames = nodeNames;\n }\n\n anchorName(): string {\n return `__wp_layout_L${this.level}`;\n }\n}\n\n/** Emits the DOT that pins each band to its own rank, in descending level order. */\nexport class LevelBandLayout {\n /**\n * @param bands the visible projects grouped by level, HIGHEST LEVEL FIRST. Levels need not be\n * contiguous — only the levels actually present get a band, and the chain links\n * whichever ones exist.\n */\n dot(bands: LevelBand[]): string {\n let dot = '';\n for (const band of bands) {\n const attrs =\n band.nodeNames.length === 0 ? this.emptyBandAttrs(band.level) : LAYOUT_NODE_ATTRS;\n dot += ` \"${band.anchorName()}\" ${attrs};\\n`;\n dot += ` { rank=same; \"${band.anchorName()}\"; `;\n for (const name of band.nodeNames.filter((node) => !band.selfQueueNames.includes(node)))\n dot += `\"${dotValue(name)}\"; `;\n dot += '}\\n';\n dot += this.selfQueueRow(band);\n }\n for (let i = 0; i + 1 < bands.length; i++) {\n dot += this.bandGap(bands[i], bands[i + 1]);\n }\n return dot;\n }\n\n private selfQueueRow(band: LevelBand): string {\n if (band.selfQueueNames.length === 0) return '';\n const anchor = `${band.anchorName()}_selfqueues`;\n return (\n ` \"${anchor}\" ${LAYOUT_NODE_ATTRS};\\n` +\n ` { rank=same; \"${anchor}\"; ` +\n band.selfQueueNames.map((name) => `\"${dotValue(name)}\"; `).join('') +\n '}\\n' +\n ` \"${band.anchorName()}\" -> \"${anchor}\" ${LAYOUT_EDGE_ATTRS};\\n`\n );\n }\n\n /**\n * A level with NO box left in it (the page's Filter removed them all) is never dropped: the next\n * level would slide up into its row, so L0 would stop being the bottom row. Its anchor becomes a\n * thin plain-text band instead — visible, labeled, still in the chain — so every remaining box\n * stays in its own L-row. Still `wp-layout`: never clickable, never walked as a dependency.\n * graph-filter.client.ts mirrors these bytes (a global browser script cannot import).\n */\n emptyBandAttrs(level: number): string {\n return (\n '[shape=plaintext, style=\"\", width=0.01, height=0.15, margin=0, fontsize=9, fontcolor=\"#9a9eab\", ' +\n `label=\"L${level} · no matching projects\", class=\"${LAYOUT_CLASS} wp-empty-level\"]`\n );\n }\n\n /**\n * The ordering chain between two adjacent bands. When either band is crowded\n * (> SPACER_MIN_BAND_SIZE boxes), an empty spacer band goes between them: the edges leaving a\n * wide row then have a whole rank of vertical room to fan out in, instead of being drawn\n * straight through the row below.\n */\n private bandGap(upper: LevelBand, lower: LevelBand): string {\n const upperAnchor =\n upper.selfQueueNames.length === 0\n ? upper.anchorName()\n : `${upper.anchorName()}_selfqueues`;\n const crowded =\n upper.nodeNames.length > SPACER_MIN_BAND_SIZE ||\n lower.nodeNames.length > SPACER_MIN_BAND_SIZE;\n if (!crowded) {\n return ` \"${upperAnchor}\" -> \"${lower.anchorName()}\" ${LAYOUT_EDGE_ATTRS};\\n`;\n }\n const spacer = `__wp_layout_spacer_L${upper.level}_L${lower.level}`;\n return (\n ` \"${spacer}\" ${LAYOUT_NODE_ATTRS};\\n` +\n ` { rank=same; \"${spacer}\"; }\\n` +\n ` \"${upperAnchor}\" -> \"${spacer}\" ${LAYOUT_EDGE_ATTRS};\\n` +\n ` \"${spacer}\" -> \"${lower.anchorName()}\" ${LAYOUT_EDGE_ATTRS};\\n`\n );\n }\n}\n"]}
@@ -32,7 +32,7 @@ class RuntimeExternalTriggers {
32
32
  const caller = trigger.caller;
33
33
  const from = caller === undefined
34
34
  ? `inbound__${trigger.api.replace(/[^A-Za-z0-9_]/g, '_')}`
35
- : `system__${caller.label.replace(/[^A-Za-z0-9_]/g, '_')}`;
35
+ : `system__caller__${caller.label.replace(/[^A-Za-z0-9_]/g, '_')}`;
36
36
  const key = JSON.stringify([from, trigger.service]);
37
37
  if (!groups.has(key))
38
38
  groups.set(key, new RuntimeExternalTriggerGroup(from, trigger.service));
@@ -1 +1 @@
1
- {"version":3,"file":"runtime-external-triggers.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/runtime-external-triggers.ts"],"names":[],"mappings":";;;AAEA,+EAA+E;AAC/E,MAAa,2BAA2B;IAIvB;IACA;IAJJ,QAAQ,GAAqB,EAAE,CAAC;IAEzC,YACa,IAAY,EACZ,EAAU;QADV,SAAI,GAAJ,IAAI,CAAQ;QACZ,OAAE,GAAF,EAAE,CAAQ;IACpB,CAAC;IAEJ,IAAI;QACA,OAAO,CAAC,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,OAAuB,EAAE,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;IAC5F,CAAC;IAED,KAAK;QACD,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;QACzB,IAAI,IAAI,CAAC,MAAM,GAAG,CAAC;YAAE,OAAO,SAAS,IAAI,CAAC,MAAM,KAAK,CAAC;QACtD,MAAM,OAAO,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,OAAuB,EAAE,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC;QACxF,OAAO,OAAO,CAAC,IAAI,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClF,CAAC;CACJ;AAlBD,kEAkBC;AAED,MAAa,uBAAuB;IACvB,MAAM,CAAgC;IAE/C,YAAY,QAA0B;QAClC,MAAM,MAAM,GAAG,IAAI,GAAG,EAAuC,CAAC;QAC9D,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;YAC7B,IAAI,OAAO,CAAC,IAAI,KAAK,UAAU;gBAAE,SAAS;YAC1C,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;YAC9B,MAAM,IAAI,GACN,MAAM,KAAK,SAAS;gBAChB,CAAC,CAAC,YAAY,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,gBAAgB,EAAE,GAAG,CAAC,EAAE;gBAC1D,CAAC,CAAC,WAAW,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,gBAAgB,EAAE,GAAG,CAAC,EAAE,CAAC;YACnE,MAAM,GAAG,GAAG,IAAI,CAAC,SAAS,CAAC,CAAC,IAAI,EAAE,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC;YACpD,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC;gBAChB,MAAM,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI,2BAA2B,CAAC,IAAI,EAAE,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC;YAC5E,MAAM,CAAC,GAAG,CAAC,GAAG,CAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QAC5C,CAAC;QACD,IAAI,CAAC,MAAM,GAAG,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC;IACvC,CAAC;CACJ;AAnBD,0DAmBC","sourcesContent":["import type { RuntimeTrigger } from './runtime-graph-model';\n\n/** One external caller/service pair, shared by DOT emission and popup data. */\nexport class RuntimeExternalTriggerGroup {\n readonly triggers: RuntimeTrigger[] = [];\n\n constructor(\n readonly from: string,\n readonly to: string,\n ) {}\n\n apis(): string[] {\n return [...new Set(this.triggers.map((trigger: RuntimeTrigger) => trigger.api))].sort();\n }\n\n label(): string {\n const apis = this.apis();\n if (apis.length > 1) return `Uses (${apis.length}) ▾`;\n const methods = new Set(this.triggers.map((trigger: RuntimeTrigger) => trigger.method));\n return methods.size === 1 ? `${apis[0]}.${this.triggers[0].method}` : apis[0];\n }\n}\n\nexport class RuntimeExternalTriggers {\n readonly groups: RuntimeExternalTriggerGroup[];\n\n constructor(triggers: RuntimeTrigger[]) {\n const groups = new Map<string, RuntimeExternalTriggerGroup>();\n for (const trigger of triggers) {\n if (trigger.kind !== 'external') continue;\n const caller = trigger.caller;\n const from =\n caller === undefined\n ? `inbound__${trigger.api.replace(/[^A-Za-z0-9_]/g, '_')}`\n : `system__${caller.label.replace(/[^A-Za-z0-9_]/g, '_')}`;\n const key = JSON.stringify([from, trigger.service]);\n if (!groups.has(key))\n groups.set(key, new RuntimeExternalTriggerGroup(from, trigger.service));\n groups.get(key)!.triggers.push(trigger);\n }\n this.groups = [...groups.values()];\n }\n}\n"]}
1
+ {"version":3,"file":"runtime-external-triggers.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/runtime-external-triggers.ts"],"names":[],"mappings":";;;AAEA,+EAA+E;AAC/E,MAAa,2BAA2B;IAIvB;IACA;IAJJ,QAAQ,GAAqB,EAAE,CAAC;IAEzC,YACa,IAAY,EACZ,EAAU;QADV,SAAI,GAAJ,IAAI,CAAQ;QACZ,OAAE,GAAF,EAAE,CAAQ;IACpB,CAAC;IAEJ,IAAI;QACA,OAAO,CAAC,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,OAAuB,EAAE,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;IAC5F,CAAC;IAED,KAAK;QACD,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;QACzB,IAAI,IAAI,CAAC,MAAM,GAAG,CAAC;YAAE,OAAO,SAAS,IAAI,CAAC,MAAM,KAAK,CAAC;QACtD,MAAM,OAAO,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,OAAuB,EAAE,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC;QACxF,OAAO,OAAO,CAAC,IAAI,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClF,CAAC;CACJ;AAlBD,kEAkBC;AAED,MAAa,uBAAuB;IACvB,MAAM,CAAgC;IAE/C,YAAY,QAA0B;QAClC,MAAM,MAAM,GAAG,IAAI,GAAG,EAAuC,CAAC;QAC9D,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;YAC7B,IAAI,OAAO,CAAC,IAAI,KAAK,UAAU;gBAAE,SAAS;YAC1C,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;YAC9B,MAAM,IAAI,GACN,MAAM,KAAK,SAAS;gBAChB,CAAC,CAAC,YAAY,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,gBAAgB,EAAE,GAAG,CAAC,EAAE;gBAC1D,CAAC,CAAC,mBAAmB,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,gBAAgB,EAAE,GAAG,CAAC,EAAE,CAAC;YAC3E,MAAM,GAAG,GAAG,IAAI,CAAC,SAAS,CAAC,CAAC,IAAI,EAAE,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC;YACpD,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC;gBAChB,MAAM,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI,2BAA2B,CAAC,IAAI,EAAE,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC;YAC5E,MAAM,CAAC,GAAG,CAAC,GAAG,CAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QAC5C,CAAC;QACD,IAAI,CAAC,MAAM,GAAG,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC;IACvC,CAAC;CACJ;AAnBD,0DAmBC","sourcesContent":["import type { RuntimeTrigger } from './runtime-graph-model';\n\n/** One external caller/service pair, shared by DOT emission and popup data. */\nexport class RuntimeExternalTriggerGroup {\n readonly triggers: RuntimeTrigger[] = [];\n\n constructor(\n readonly from: string,\n readonly to: string,\n ) {}\n\n apis(): string[] {\n return [...new Set(this.triggers.map((trigger: RuntimeTrigger) => trigger.api))].sort();\n }\n\n label(): string {\n const apis = this.apis();\n if (apis.length > 1) return `Uses (${apis.length}) ▾`;\n const methods = new Set(this.triggers.map((trigger: RuntimeTrigger) => trigger.method));\n return methods.size === 1 ? `${apis[0]}.${this.triggers[0].method}` : apis[0];\n }\n}\n\nexport class RuntimeExternalTriggers {\n readonly groups: RuntimeExternalTriggerGroup[];\n\n constructor(triggers: RuntimeTrigger[]) {\n const groups = new Map<string, RuntimeExternalTriggerGroup>();\n for (const trigger of triggers) {\n if (trigger.kind !== 'external') continue;\n const caller = trigger.caller;\n const from =\n caller === undefined\n ? `inbound__${trigger.api.replace(/[^A-Za-z0-9_]/g, '_')}`\n : `system__caller__${caller.label.replace(/[^A-Za-z0-9_]/g, '_')}`;\n const key = JSON.stringify([from, trigger.service]);\n if (!groups.has(key))\n groups.set(key, new RuntimeExternalTriggerGroup(from, trigger.service));\n groups.get(key)!.triggers.push(trigger);\n }\n this.groups = [...groups.values()];\n }\n}\n"]}
@@ -18,6 +18,8 @@ exports.serializeRuntimeGraph = serializeRuntimeGraph;
18
18
  const tslib_1 = require("tslib");
19
19
  const fs = tslib_1.__importStar(require("fs"));
20
20
  const path = tslib_1.__importStar(require("path"));
21
+ const rules_config_1 = require("@webpieces/rules-config");
22
+ const runtime_config_1 = require("./runtime-config");
21
23
  const toError_1 = require("../toError");
22
24
  exports.DEFAULT_RUNTIME_GRAPH_PATH = 'architecture/runtime-dependencies.json';
23
25
  /** Deterministic JSON (sorted keys + arrays already sorted during assembly). */
@@ -42,18 +44,44 @@ function loadRuntimeGraph(workspaceRoot, graphPath = exports.DEFAULT_RUNTIME_GRA
42
44
  const fullPath = path.join(workspaceRoot, graphPath);
43
45
  if (!fs.existsSync(fullPath))
44
46
  return null;
47
+ let graph;
45
48
  // eslint-disable-next-line @webpieces/no-unmanaged-exceptions
46
49
  try {
47
- return JSON.parse(fs.readFileSync(fullPath, 'utf-8'));
50
+ graph = JSON.parse(fs.readFileSync(fullPath, 'utf-8'));
48
51
  }
49
52
  catch (err) {
50
53
  const error = (0, toError_1.toError)(err);
51
54
  throw new Error(`Failed to load runtime graph from ${fullPath}`, { cause: error });
52
55
  }
56
+ new RuntimeLevelSchema().validate(graph);
57
+ return graph;
53
58
  }
54
59
  /** Serialize for an in-memory equality check (matches the on-disk format). */
55
60
  // webpieces-disable no-function-outside-class -- module-scope file IO, matching saveGraph/loadBlessedGraph in graph-loader.ts
56
61
  function serializeRuntimeGraph(graph) {
57
62
  return formatRuntimeJson(graph);
58
63
  }
64
+ /** Old RPC-only JSON must be regenerated, never silently rendered with missing ranks. */
65
+ class RuntimeLevelSchema {
66
+ validate(graph) {
67
+ const nodes = [
68
+ ...Object.values(graph.services),
69
+ ...Object.values(graph.queues),
70
+ ...Object.values(graph.contractQueues ?? {}),
71
+ ...graph.triggers,
72
+ ...Object.values(graph.externalSystems ?? {}),
73
+ ...Object.values(graph.remoteServices ?? {}),
74
+ ...graph.unresolvedUses,
75
+ ];
76
+ if (graph.contractQueues !== undefined &&
77
+ Number.isInteger(graph.maxLevel) &&
78
+ nodes.every((node) => Number.isInteger(node.level) &&
79
+ node.level >= 0 &&
80
+ typeof node.levelBecause === 'string'))
81
+ return;
82
+ throw new rules_config_1.RuleFailError(runtime_config_1.RUNTIME_RULE_NAME, 'runtime-dependencies.json requires levels for every node and contractQueues. Regenerate with architecture:generate.', undefined, undefined, [
83
+ new rules_config_1.Option('Run pnpm nx run architecture:generate and commit runtime-dependencies.json.', true),
84
+ ]);
85
+ }
86
+ }
59
87
  //# sourceMappingURL=runtime-graph-io.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"runtime-graph-io.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/runtime-graph-io.ts"],"names":[],"mappings":";AAAA;;;;;;;;;GASG;;;AAgBH,4CASC;AAGD,wDAKC;AAGD,4CAaC;AAID,sDAEC;;AArDD,+CAAyB;AACzB,mDAA6B;AAE7B,wCAAqC;AAExB,QAAA,0BAA0B,GAAG,wCAAwC,CAAC;AAEnF,gFAAgF;AAChF,8HAA8H;AAC9H,SAAS,iBAAiB,CAAC,KAAmB;IAC1C,OAAO,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,GAAG,IAAI,CAAC;AACjD,CAAC;AAED,8HAA8H;AAC9H,SAAgB,gBAAgB,CAC5B,KAAmB,EACnB,aAAqB,EACrB,YAAoB,kCAA0B;IAE9C,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,aAAa,EAAE,SAAS,CAAC,CAAC;IACrD,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;IACnC,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,GAAG,CAAC;QAAE,EAAE,CAAC,SAAS,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAChE,EAAE,CAAC,aAAa,CAAC,QAAQ,EAAE,iBAAiB,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC,CAAC;AAClE,CAAC;AAED,8HAA8H;AAC9H,SAAgB,sBAAsB,CAClC,aAAqB,EACrB,YAAoB,kCAA0B;IAE9C,OAAO,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,aAAa,EAAE,SAAS,CAAC,CAAC,CAAC;AAC9D,CAAC;AAED,8HAA8H;AAC9H,SAAgB,gBAAgB,CAC5B,aAAqB,EACrB,YAAoB,kCAA0B;IAE9C,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,aAAa,EAAE,SAAS,CAAC,CAAC;IACrD,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,QAAQ,CAAC;QAAE,OAAO,IAAI,CAAC;IAC1C,8DAA8D;IAC9D,IAAI,CAAC;QACD,OAAO,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,YAAY,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAiB,CAAC;IAC1E,CAAC;IAAC,OAAO,GAAY,EAAE,CAAC;QACpB,MAAM,KAAK,GAAG,IAAA,iBAAO,EAAC,GAAG,CAAC,CAAC;QAC3B,MAAM,IAAI,KAAK,CAAC,qCAAqC,QAAQ,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAC;IACvF,CAAC;AACL,CAAC;AAED,8EAA8E;AAC9E,8HAA8H;AAC9H,SAAgB,qBAAqB,CAAC,KAAmB;IACrD,OAAO,iBAAiB,CAAC,KAAK,CAAC,CAAC;AACpC,CAAC","sourcesContent":["/**\n * Runtime Graph persistence\n *\n * Reading, writing and canonical-serializing architecture/runtime-dependencies.json. Split out of\n * runtime-graph.ts (which owns the derivation and had grown past the file-size limit).\n *\n * `formatRuntimeJson` is the ONE canonical rendering: validate-runtime-architecture compares the\n * freshly derived graph to the committed one as STRINGS, so any second way of writing this file\n * would show up as unfixable drift.\n */\n\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport type { RuntimeGraph } from './runtime-graph-model';\nimport { toError } from '../toError';\n\nexport const DEFAULT_RUNTIME_GRAPH_PATH = 'architecture/runtime-dependencies.json';\n\n/** Deterministic JSON (sorted keys + arrays already sorted during assembly). */\n// webpieces-disable no-function-outside-class -- module-scope file IO, matching saveGraph/loadBlessedGraph in graph-loader.ts\nfunction formatRuntimeJson(graph: RuntimeGraph): string {\n return JSON.stringify(graph, null, 4) + '\\n';\n}\n\n// webpieces-disable no-function-outside-class -- module-scope file IO, matching saveGraph/loadBlessedGraph in graph-loader.ts\nexport function saveRuntimeGraph(\n graph: RuntimeGraph,\n workspaceRoot: string,\n graphPath: string = DEFAULT_RUNTIME_GRAPH_PATH,\n): void {\n const fullPath = path.join(workspaceRoot, graphPath);\n const dir = path.dirname(fullPath);\n if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });\n fs.writeFileSync(fullPath, formatRuntimeJson(graph), 'utf-8');\n}\n\n// webpieces-disable no-function-outside-class -- module-scope file IO, matching saveGraph/loadBlessedGraph in graph-loader.ts\nexport function runtimeGraphFileExists(\n workspaceRoot: string,\n graphPath: string = DEFAULT_RUNTIME_GRAPH_PATH,\n): boolean {\n return fs.existsSync(path.join(workspaceRoot, graphPath));\n}\n\n// webpieces-disable no-function-outside-class -- module-scope file IO, matching saveGraph/loadBlessedGraph in graph-loader.ts\nexport function loadRuntimeGraph(\n workspaceRoot: string,\n graphPath: string = DEFAULT_RUNTIME_GRAPH_PATH,\n): RuntimeGraph | null {\n const fullPath = path.join(workspaceRoot, graphPath);\n if (!fs.existsSync(fullPath)) return null;\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n return JSON.parse(fs.readFileSync(fullPath, 'utf-8')) as RuntimeGraph;\n } catch (err: unknown) {\n const error = toError(err);\n throw new Error(`Failed to load runtime graph from ${fullPath}`, { cause: error });\n }\n}\n\n/** Serialize for an in-memory equality check (matches the on-disk format). */\n// webpieces-disable no-function-outside-class -- module-scope file IO, matching saveGraph/loadBlessedGraph in graph-loader.ts\nexport function serializeRuntimeGraph(graph: RuntimeGraph): string {\n return formatRuntimeJson(graph);\n}\n"]}
1
+ {"version":3,"file":"runtime-graph-io.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/runtime-graph-io.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Runtime Graph persistence\n *\n * Reading, writing and canonical-serializing architecture/runtime-dependencies.json. Split out of\n * runtime-graph.ts (which owns the derivation and had grown past the file-size limit).\n *\n * `formatRuntimeJson` is the ONE canonical rendering: validate-runtime-architecture compares the\n * freshly derived graph to the committed one as STRINGS, so any second way of writing this file\n * would show up as unfixable drift.\n */\n\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport { RuleFailError, Option } from '@webpieces/rules-config';\nimport { RUNTIME_RULE_NAME } from './runtime-config';\nimport type { RuntimeGraph } from './runtime-graph-model';\nimport { toError } from '../toError';\n\nexport const DEFAULT_RUNTIME_GRAPH_PATH = 'architecture/runtime-dependencies.json';\n\n/** Deterministic JSON (sorted keys + arrays already sorted during assembly). */\n// webpieces-disable no-function-outside-class -- module-scope file IO, matching saveGraph/loadBlessedGraph in graph-loader.ts\nfunction formatRuntimeJson(graph: RuntimeGraph): string {\n return JSON.stringify(graph, null, 4) + '\\n';\n}\n\n// webpieces-disable no-function-outside-class -- module-scope file IO, matching saveGraph/loadBlessedGraph in graph-loader.ts\nexport function saveRuntimeGraph(\n graph: RuntimeGraph,\n workspaceRoot: string,\n graphPath: string = DEFAULT_RUNTIME_GRAPH_PATH,\n): void {\n const fullPath = path.join(workspaceRoot, graphPath);\n const dir = path.dirname(fullPath);\n if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });\n fs.writeFileSync(fullPath, formatRuntimeJson(graph), 'utf-8');\n}\n\n// webpieces-disable no-function-outside-class -- module-scope file IO, matching saveGraph/loadBlessedGraph in graph-loader.ts\nexport function runtimeGraphFileExists(\n workspaceRoot: string,\n graphPath: string = DEFAULT_RUNTIME_GRAPH_PATH,\n): boolean {\n return fs.existsSync(path.join(workspaceRoot, graphPath));\n}\n\n// webpieces-disable no-function-outside-class -- module-scope file IO, matching saveGraph/loadBlessedGraph in graph-loader.ts\nexport function loadRuntimeGraph(\n workspaceRoot: string,\n graphPath: string = DEFAULT_RUNTIME_GRAPH_PATH,\n): RuntimeGraph | null {\n const fullPath = path.join(workspaceRoot, graphPath);\n if (!fs.existsSync(fullPath)) return null;\n let graph: RuntimeGraph;\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n graph = JSON.parse(fs.readFileSync(fullPath, 'utf-8')) as RuntimeGraph;\n } catch (err: unknown) {\n const error = toError(err);\n throw new Error(`Failed to load runtime graph from ${fullPath}`, { cause: error });\n }\n new RuntimeLevelSchema().validate(graph);\n return graph;\n}\n\n/** Serialize for an in-memory equality check (matches the on-disk format). */\n// webpieces-disable no-function-outside-class -- module-scope file IO, matching saveGraph/loadBlessedGraph in graph-loader.ts\nexport function serializeRuntimeGraph(graph: RuntimeGraph): string {\n return formatRuntimeJson(graph);\n}\n\n/** Old RPC-only JSON must be regenerated, never silently rendered with missing ranks. */\nclass RuntimeLevelSchema {\n validate(graph: RuntimeGraph): void {\n const nodes = [\n ...Object.values(graph.services),\n ...Object.values(graph.queues),\n ...Object.values(graph.contractQueues ?? {}),\n ...graph.triggers,\n ...Object.values(graph.externalSystems ?? {}),\n ...Object.values(graph.remoteServices ?? {}),\n ...graph.unresolvedUses,\n ];\n if (\n graph.contractQueues !== undefined &&\n Number.isInteger(graph.maxLevel) &&\n nodes.every(\n (node) =>\n Number.isInteger(node.level) &&\n node.level >= 0 &&\n typeof node.levelBecause === 'string',\n )\n )\n return;\n throw new RuleFailError(\n RUNTIME_RULE_NAME,\n 'runtime-dependencies.json requires levels for every node and contractQueues. Regenerate with architecture:generate.',\n undefined,\n undefined,\n [\n new Option(\n 'Run pnpm nx run architecture:generate and commit runtime-dependencies.json.',\n true,\n ),\n ],\n );\n }\n}\n"]}
@@ -1,5 +1,5 @@
1
1
  import type { EnhancedGraph } from './graph-sorter';
2
- import type { RuntimeEdge, RuntimeGraph } from './runtime-graph-model';
2
+ import type { RuntimeEdge } from './runtime-graph-model';
3
3
  /**
4
4
  * LEVELS and ADJACENCY for the runtime graph — how deep each service sits, and who calls whom.
5
5
  *
@@ -8,23 +8,7 @@ import type { RuntimeEdge, RuntimeGraph } from './runtime-graph-model';
8
8
  * computation over the result, with no knowledge of contracts at all. Splitting it also keeps
9
9
  * runtime-graph.ts under the file-size limit, which is the visible symptom of the same thing.
10
10
  */
11
- /**
12
- * Adjacency (service -> [targets]) used for leveling + cycle checks.
13
- *
14
- * TWO KINDS OF EDGE ARE EXCLUDED, and the second is modelled on the first:
15
- *
16
- * PUBSUB EDGES. A queue is precisely the thing that decouples producer from consumer: the producer
17
- * returns as soon as the task is enqueued and never waits on the consumer, so a queued hop is not a
18
- * runtime dependency in the sense levels and cycle detection mean. Counting them would make the
19
- * common and correct `A → queue → A` (a service deferring its own work) an architecture cycle, and
20
- * would rank services by an ordering that does not constrain deploy or startup.
21
- *
22
- * EDGES STAMPED `cutLegacyCycle`. The calling project carries a `cutLegacyCycle:<target>` nx tag
23
- * (see cut-legacy-cycle-resolver.ts) ADMITTING that this hop closes a real cycle which is being
24
- * tolerated as legacy debt. Unlike a queued hop the coupling is real, so the edge is still drawn —
25
- * it is only kept out of leveling and cycle detection, which are the two things a cycle makes
26
- * meaningless.
27
- */
11
+ /** RPC adjacency for callers working with an edge list alone. Full runtime layering uses RuntimeNodeLevels. */
28
12
  export declare function adjacencyFromEdges(serviceNames: string[], edges: RuntimeEdge[]): Record<string, string[]>;
29
13
  /**
30
14
  * Stamp `cutLegacyCycle` on every runtime edge a project declared a cut for — the DECLARATION
@@ -49,8 +33,6 @@ export declare function adjacencyFromEdges(serviceNames: string[], edges: Runtim
49
33
  * target, so a tag may name a module name or a declared serviceName.
50
34
  */
51
35
  export declare function applyCycleCuts(edges: RuntimeEdge[], projects: EnhancedGraph, nodeByServiceName: Map<string, string>): string[];
52
- /** Adjacency (service -> [targets]) from a loaded runtime graph. */
53
- export declare function runtimeAdjacency(graph: RuntimeGraph): Record<string, string[]>;
54
36
  /**
55
37
  * Assign levels via topological sort — and THROW on a cycle rather than levelling one.
56
38
  *