@webpieces/nx-webpieces-rules 0.4.487 → 0.4.489

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 (44) hide show
  1. package/package.json +6 -6
  2. package/src/executors/generate/executor.js +42 -19
  3. package/src/executors/generate/executor.js.map +1 -1
  4. package/src/executors/validate-api-relations/executor.js +6 -1
  5. package/src/executors/validate-api-relations/executor.js.map +1 -1
  6. package/src/executors/validate-architecture-unchanged/executor.js +65 -15
  7. package/src/executors/validate-architecture-unchanged/executor.js.map +1 -1
  8. package/src/executors/validate-eslint-sync/executor.js +3 -2
  9. package/src/executors/validate-eslint-sync/executor.js.map +1 -1
  10. package/src/executors/validate-packagejson/executor.js +4 -2
  11. package/src/executors/validate-packagejson/executor.js.map +1 -1
  12. package/src/executors/validate-runtime-architecture/executor.js +3 -1
  13. package/src/executors/validate-runtime-architecture/executor.js.map +1 -1
  14. package/src/executors/validate-versions-locked/executor.js +3 -2
  15. package/src/executors/validate-versions-locked/executor.js.map +1 -1
  16. package/src/lib/api-usage/api-ast.d.ts +71 -0
  17. package/src/lib/api-usage/api-ast.js +250 -0
  18. package/src/lib/api-usage/api-ast.js.map +1 -0
  19. package/src/lib/api-usage/api-relations.d.ts +72 -3
  20. package/src/lib/api-usage/api-relations.js.map +1 -1
  21. package/src/lib/api-usage/api-scanner.d.ts +43 -4
  22. package/src/lib/api-usage/api-scanner.js +121 -103
  23. package/src/lib/api-usage/api-scanner.js.map +1 -1
  24. package/src/lib/graph-loader.d.ts +21 -2
  25. package/src/lib/graph-loader.js +38 -4
  26. package/src/lib/graph-loader.js.map +1 -1
  27. package/src/lib/rule-gate.d.ts +8 -5
  28. package/src/lib/rule-gate.js +8 -5
  29. package/src/lib/rule-gate.js.map +1 -1
  30. package/src/lib/runtime-config.d.ts +7 -1
  31. package/src/lib/runtime-config.js +7 -1
  32. package/src/lib/runtime-config.js.map +1 -1
  33. package/src/lib/runtime-graph-io.d.ts +17 -0
  34. package/src/lib/runtime-graph-io.js +59 -0
  35. package/src/lib/runtime-graph-io.js.map +1 -0
  36. package/src/lib/runtime-graph-model.d.ts +121 -0
  37. package/src/lib/runtime-graph-model.js +14 -0
  38. package/src/lib/runtime-graph-model.js.map +1 -0
  39. package/src/lib/runtime-graph.d.ts +6 -75
  40. package/src/lib/runtime-graph.js +163 -51
  41. package/src/lib/runtime-graph.js.map +1 -1
  42. package/src/lib/runtime-visualizer.d.ts +5 -0
  43. package/src/lib/runtime-visualizer.js +63 -6
  44. package/src/lib/runtime-visualizer.js.map +1 -1
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runtime-graph-model.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/runtime-graph-model.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;GAUG","sourcesContent":["/**\n * Runtime Graph model\n *\n * The serialization DTOs for architecture/runtime-dependencies.json. Split out of runtime-graph.ts,\n * which owns the DERIVATION (and had grown past the file-size limit), so the committed data shape\n * can be read on its own — it is what every consumer of the file, and any future Terraform\n * cross-check, actually programs against.\n *\n * Interfaces rather than classes: these are parsed straight out of JSON with `JSON.parse`, so a\n * class would only ever be a shape assertion over a plain object, never a constructed instance.\n */\n\nimport type { ApiTransport } from './api-usage/api-relations';\n\nexport interface RuntimeService {\n level: number;\n /**\n * The name clients address this service by (`new ClientConfig('helper-fsdb')`), declared in its\n * project.json. Absent for a service nothing calls by name (e.g. a browser app).\n */\n serviceName?: string;\n /**\n * The service(s) this node's clients call when the call site carries no literal `ClientConfig`,\n * declared in its project.json (metadata.webpieces.callsService). A single name, or an\n * `{ apiClassName: serviceName }` map. Absent when the node declares no target. Mirrors\n * GraphEntry.callsService; it is the CALLING-side counterpart of `serviceName`.\n */\n callsService?: string | Record<string, string>;\n implements: string[];\n /**\n * apiClassName -> the LIBRARY project whose apiRelations declared that implements, for the apis\n * this service serves through an embedded library rather than its own source (e.g. a shared\n * route-registration lib). Answers \"who implements WarmupApi, and where did that come from?\",\n * which previously required walking the dependsOn closure by hand.\n */\n implementsVia?: Record<string, string>;\n uses: string[];\n dependsOn: string[];\n /**\n * When false, this service is hidden from the rendered runtime graph (its\n * node AND every edge touching it are omitted from the HTML/DOT). It stays\n * in runtime-dependencies.json so the data view is complete. Absent means\n * drawn (the default). Mirrors GraphEntry.drawOnGraph from the `drawOnGraph:`\n * nx tag.\n */\n drawOnGraph?: boolean;\n}\n\nexport interface RuntimeApi {\n implementedBy: string[];\n usedBy: string[];\n /** Transport of this API — 'rpc' (direct call) or 'pubsub' (delivered through a queue). */\n type?: ApiTransport;\n /**\n * The api-lib project that OWNS this contract. For a contract nothing in-repo implements, this\n * is the external library the calls leave the repo through (`lib-firestore`, `lib-gmail`), which\n * is what the runtime viz labels its terminal external nodes with.\n */\n owner?: string;\n}\n\nexport interface RuntimeEdge {\n from: string;\n to: string;\n via: string[];\n /**\n * Transport of this edge. 'rpc' → a direct call arrow. 'pubsub' → the producer enqueues and the\n * consumer is delivered later, so the runtime viz draws it as producer → QUEUE → consumer.\n * Edges are split by transport, so every edge is a single kind.\n */\n type?: ApiTransport;\n /**\n * `\"ApiClassName.methodName\"` — the queue this edge flows through. Present iff `type` is\n * 'pubsub'. Queues are per METHOD, not per service pair, because that is the unit Cloud Tasks\n * (and Terraform) actually create, so two services exchanging three queued methods are three\n * queues rather than one arrow.\n */\n queue?: string;\n}\n\n/**\n * One Cloud Tasks queue: the async seam between a producer and a consumer, at METHOD granularity.\n *\n * `producedBy` and `consumedBy` are deliberately not symmetric in confidence — see\n * {@link ApiRef.methodsInferred}. The consumer is derived from `addRoutes` plus the contract's\n * method table and is exact; the producer is attributed to every queued method of the contract it\n * built a client for, because which methods it enqueues is not statically recoverable.\n */\nexport interface RuntimeQueue {\n api: string;\n method: string;\n /** `@Queue(...)` override, else `${Api}-${method}` — the name Terraform must match 1:1. */\n queueName: string;\n producedBy: string[];\n consumedBy: string[];\n}\n\n/**\n * An endpoint driven by something that is NOT an in-repo caller — a clock or an outside system.\n * These never appear as runtime EDGES (there is no in-repo `from`), which is exactly why they were\n * invisible until now: a nightly sweep and a GCP push subscription are real runtime entry points\n * with real Terraform behind them, and the graph showed neither.\n */\nexport interface RuntimeTrigger {\n /** 'cron' → a scheduler fires it; 'external' → a system outside this repo posts to it. */\n kind: 'cron' | 'external';\n api: string;\n method: string;\n /** The service that SERVES the endpoint (the arrow's head). */\n service: string;\n /** Present for 'cron': the Cloud Scheduler job / queue name Terraform must match. */\n queueName?: string;\n}\n\nexport interface RuntimeUnresolved {\n service: string;\n api: string;\n}\n\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"]}
@@ -28,74 +28,10 @@
28
28
  * one, so it must never degrade silently.
29
29
  */
30
30
  import type { EnhancedGraph } from './graph-sorter';
31
- import type { ApiTransport } from './api-usage/api-relations';
32
- export declare const DEFAULT_RUNTIME_GRAPH_PATH = "architecture/runtime-dependencies.json";
33
- export interface RuntimeService {
34
- level: number;
35
- /**
36
- * The name clients address this service by (`new ClientConfig('helper-fsdb')`), declared in its
37
- * project.json. Absent for a service nothing calls by name (e.g. a browser app).
38
- */
39
- serviceName?: string;
40
- /**
41
- * The service(s) this node's clients call when the call site carries no literal `ClientConfig`,
42
- * declared in its project.json (metadata.webpieces.callsService). A single name, or an
43
- * `{ apiClassName: serviceName }` map. Absent when the node declares no target. Mirrors
44
- * GraphEntry.callsService; it is the CALLING-side counterpart of `serviceName`.
45
- */
46
- callsService?: string | Record<string, string>;
47
- implements: string[];
48
- /**
49
- * apiClassName -> the LIBRARY project whose apiRelations declared that implements, for the apis
50
- * this service serves through an embedded library rather than its own source (e.g. a shared
51
- * route-registration lib). Answers "who implements WarmupApi, and where did that come from?",
52
- * which previously required walking the dependsOn closure by hand.
53
- */
54
- implementsVia?: Record<string, string>;
55
- uses: string[];
56
- dependsOn: string[];
57
- /**
58
- * When false, this service is hidden from the rendered runtime graph (its
59
- * node AND every edge touching it are omitted from the HTML/DOT). It stays
60
- * in runtime-dependencies.json so the data view is complete. Absent means
61
- * drawn (the default). Mirrors GraphEntry.drawOnGraph from the `drawOnGraph:`
62
- * nx tag.
63
- */
64
- drawOnGraph?: boolean;
65
- }
66
- export interface RuntimeApi {
67
- implementedBy: string[];
68
- usedBy: string[];
69
- /** Transport of this API — 'rpc' (direct call) or 'pubsub' (delivered through a queue). */
70
- type?: ApiTransport;
71
- /**
72
- * The api-lib project that OWNS this contract. For a contract nothing in-repo implements, this
73
- * is the external library the calls leave the repo through (`lib-firestore`, `lib-gmail`), which
74
- * is what the runtime viz labels its terminal external nodes with.
75
- */
76
- owner?: string;
77
- }
78
- export interface RuntimeEdge {
79
- from: string;
80
- to: string;
81
- via: string[];
82
- /**
83
- * Transport of this edge. 'rpc' → a direct call arrow. 'pubsub' → the producer enqueues and the
84
- * consumer is delivered later, so the runtime viz draws it as producer → QUEUE → consumer.
85
- * Edges are split by transport, so every edge is a single kind.
86
- */
87
- type?: ApiTransport;
88
- }
89
- export interface RuntimeUnresolved {
90
- service: string;
91
- api: string;
92
- }
93
- export interface RuntimeGraph {
94
- services: Record<string, RuntimeService>;
95
- apis: Record<string, RuntimeApi>;
96
- runtimeEdges: RuntimeEdge[];
97
- unresolvedUses: RuntimeUnresolved[];
98
- }
31
+ import type { ApiContracts } from './api-usage/api-relations';
32
+ import type { RuntimeGraph } from './runtime-graph-model';
33
+ export type { RuntimeApi, RuntimeEdge, RuntimeGraph, RuntimeQueue, RuntimeService, RuntimeTrigger, RuntimeUnresolved, } from './runtime-graph-model';
34
+ export { DEFAULT_RUNTIME_GRAPH_PATH, saveRuntimeGraph, runtimeGraphFileExists, loadRuntimeGraph, serializeRuntimeGraph, } from './runtime-graph-io';
99
35
  /**
100
36
  * The derived graph PLUS everything the derivation had to guess at. `warnings` is deliberately not
101
37
  * part of RuntimeGraph: it is not committed data, it is the report that stops a guessed edge from
@@ -133,16 +69,11 @@ export declare function runtimeAdjacency(graph: RuntimeGraph): Record<string, st
133
69
  * (drawOnGraph:false, defaults to none) are kept in the graph but flagged so the
134
70
  * runtime visualizer omits their nodes + edges.
135
71
  */
136
- export declare function deriveRuntimeGraph(projects: EnhancedGraph, hiddenProjects?: Set<string>): RuntimeGraph;
72
+ export declare function deriveRuntimeGraph(projects: EnhancedGraph, hiddenProjects?: Set<string>, apiContracts?: ApiContracts): RuntimeGraph;
137
73
  /**
138
74
  * The same derivation, plus the warnings it produced (every edge it had to GUESS at). Executors use
139
75
  * this form and print the warnings; `deriveRuntimeGraph` is the convenience form for callers that
140
76
  * only want the data. The warnings are deliberately kept OUT of runtime-dependencies.json — a graph
141
77
  * file that records its own doubts would just get committed and stop being read.
142
78
  */
143
- export declare function deriveRuntimeGraphReport(projects: EnhancedGraph, hiddenProjects?: Set<string>): RuntimeGraphReport;
144
- export declare function saveRuntimeGraph(graph: RuntimeGraph, workspaceRoot: string, graphPath?: string): void;
145
- export declare function runtimeGraphFileExists(workspaceRoot: string, graphPath?: string): boolean;
146
- export declare function loadRuntimeGraph(workspaceRoot: string, graphPath?: string): RuntimeGraph | null;
147
- /** Serialize for an in-memory equality check (matches the on-disk format). */
148
- export declare function serializeRuntimeGraph(graph: RuntimeGraph): string;
79
+ export declare function deriveRuntimeGraphReport(projects: EnhancedGraph, hiddenProjects?: Set<string>, apiContracts?: ApiContracts): RuntimeGraphReport;
@@ -29,21 +29,20 @@
29
29
  * one, so it must never degrade silently.
30
30
  */
31
31
  Object.defineProperty(exports, "__esModule", { value: true });
32
- exports.RuntimeGraphReport = exports.DEFAULT_RUNTIME_GRAPH_PATH = void 0;
32
+ exports.RuntimeGraphReport = exports.serializeRuntimeGraph = exports.loadRuntimeGraph = exports.runtimeGraphFileExists = exports.saveRuntimeGraph = exports.DEFAULT_RUNTIME_GRAPH_PATH = void 0;
33
33
  exports.runtimeAdjacency = runtimeAdjacency;
34
34
  exports.deriveRuntimeGraph = deriveRuntimeGraph;
35
35
  exports.deriveRuntimeGraphReport = deriveRuntimeGraphReport;
36
- exports.saveRuntimeGraph = saveRuntimeGraph;
37
- exports.runtimeGraphFileExists = runtimeGraphFileExists;
38
- exports.loadRuntimeGraph = loadRuntimeGraph;
39
- exports.serializeRuntimeGraph = serializeRuntimeGraph;
40
- const tslib_1 = require("tslib");
41
- const fs = tslib_1.__importStar(require("fs"));
42
- const path = tslib_1.__importStar(require("path"));
43
36
  const graph_sorter_1 = require("./graph-sorter");
44
37
  const api_relations_1 = require("./api-usage/api-relations");
45
38
  const toError_1 = require("../toError");
46
- exports.DEFAULT_RUNTIME_GRAPH_PATH = 'architecture/runtime-dependencies.json';
39
+ // Persistence lives in runtime-graph-io.ts; re-exported for the same reason as the model types.
40
+ var runtime_graph_io_1 = require("./runtime-graph-io");
41
+ Object.defineProperty(exports, "DEFAULT_RUNTIME_GRAPH_PATH", { enumerable: true, get: function () { return runtime_graph_io_1.DEFAULT_RUNTIME_GRAPH_PATH; } });
42
+ Object.defineProperty(exports, "saveRuntimeGraph", { enumerable: true, get: function () { return runtime_graph_io_1.saveRuntimeGraph; } });
43
+ Object.defineProperty(exports, "runtimeGraphFileExists", { enumerable: true, get: function () { return runtime_graph_io_1.runtimeGraphFileExists; } });
44
+ Object.defineProperty(exports, "loadRuntimeGraph", { enumerable: true, get: function () { return runtime_graph_io_1.loadRuntimeGraph; } });
45
+ Object.defineProperty(exports, "serializeRuntimeGraph", { enumerable: true, get: function () { return runtime_graph_io_1.serializeRuntimeGraph; } });
47
46
  /**
48
47
  * The derived graph PLUS everything the derivation had to guess at. `warnings` is deliberately not
49
48
  * part of RuntimeGraph: it is not committed data, it is the report that stops a guessed edge from
@@ -70,12 +69,22 @@ class RuntimeGraphReport {
70
69
  }
71
70
  }
72
71
  exports.RuntimeGraphReport = RuntimeGraphReport;
73
- /** Adjacency (service -> [targets]) used for leveling + cycle checks. */
72
+ /**
73
+ * Adjacency (service -> [targets]) used for leveling + cycle checks.
74
+ *
75
+ * PUBSUB EDGES ARE EXCLUDED. A queue is precisely the thing that decouples producer from consumer:
76
+ * the producer returns as soon as the task is enqueued and never waits on the consumer, so a queued
77
+ * hop is not a runtime dependency in the sense levels and cycle detection mean. Counting them would
78
+ * make the common and correct `A → queue → A` (a service deferring its own work) an architecture
79
+ * cycle, and would rank services by an ordering that does not constrain deploy or startup.
80
+ */
74
81
  function adjacencyFromEdges(serviceNames, edges) {
75
82
  const adj = {};
76
83
  for (const name of serviceNames)
77
84
  adj[name] = [];
78
85
  for (const edge of edges) {
86
+ if (edge.type === 'pubsub')
87
+ continue;
79
88
  if (!adj[edge.from])
80
89
  adj[edge.from] = [];
81
90
  adj[edge.from].push(edge.to);
@@ -141,6 +150,7 @@ class RelationSink {
141
150
  class RuntimeGraphDeriver {
142
151
  projects;
143
152
  hiddenProjects;
153
+ apiContracts;
144
154
  /** apiClassName -> the api-lib project that owns the contract (from the apiRelations key). */
145
155
  apiOwners = new Map();
146
156
  /** Addressable name -> the runtime node answering to it; how a targeted call resolves. */
@@ -149,9 +159,16 @@ class RuntimeGraphDeriver {
149
159
  problems = [];
150
160
  constructor(projects,
151
161
  /** Project names tagged drawOnGraph:false — kept in the JSON but flagged so the viz omits them. */
152
- hiddenProjects) {
162
+ hiddenProjects,
163
+ /**
164
+ * The committed per-contract method table from dependencies.json. Empty means the file
165
+ * predates `apiContracts`: every pubsub edge then falls back to one unnamed queue per
166
+ * service pair, exactly as before, instead of failing on a missing table.
167
+ */
168
+ apiContracts = {}) {
153
169
  this.projects = projects;
154
170
  this.hiddenProjects = hiddenProjects;
171
+ this.apiContracts = apiContracts;
155
172
  // A node ALWAYS answers to its own module name, so a repo whose deployed names match its
156
173
  // project names needs no declaration at all — and no alias can ever redirect 'ai-chat' away
157
174
  // from the ai-chat module. Module names are therefore claimed FIRST and are unshadowable.
@@ -189,9 +206,46 @@ class RuntimeGraphDeriver {
189
206
  apis: apisObj,
190
207
  runtimeEdges: edgeResult.edges,
191
208
  unresolvedUses: edgeResult.unresolved,
209
+ queues: edgeResult.queues,
210
+ triggers: this.buildTriggers(decls),
192
211
  };
193
212
  return new RuntimeGraphReport(graph, this.warnings, this.problems);
194
213
  }
214
+ /**
215
+ * The clock- and outside-driven entry points: for every contract a node IMPLEMENTS, each method
216
+ * declared `cron` or `external` becomes a trigger pointing AT that node.
217
+ *
218
+ * Driven off `implements` rather than `uses` on purpose — these have no in-repo caller at all,
219
+ * which is why they never produced an edge and stayed invisible.
220
+ */
221
+ buildTriggers(decls) {
222
+ const triggers = [];
223
+ for (const decl of decls) {
224
+ for (const ref of decl.implementsApis) {
225
+ for (const method of this.methodsOf(ref.api)) {
226
+ if (method.kind !== 'cron' && method.kind !== 'external')
227
+ continue;
228
+ const trigger = {
229
+ kind: method.kind,
230
+ api: ref.api,
231
+ method: method.name,
232
+ service: decl.name,
233
+ };
234
+ if (method.kind === 'cron')
235
+ trigger.queueName = method.queueName;
236
+ triggers.push(trigger);
237
+ }
238
+ }
239
+ }
240
+ return triggers.sort((a, b) => a.kind.localeCompare(b.kind) ||
241
+ a.api.localeCompare(b.api) ||
242
+ a.method.localeCompare(b.method) ||
243
+ a.service.localeCompare(b.service));
244
+ }
245
+ /** The committed method table for a contract, or [] when it declares none (e.g. a vendor seam). */
246
+ methodsOf(api) {
247
+ return this.apiContracts[api]?.methods ?? [];
248
+ }
195
249
  /**
196
250
  * One ScanDecl per RUNTIME NODE (role:server / role:client). A node's effective
197
251
  * relations are its OWN apiRelations PLUS those of every library in its transitive
@@ -287,16 +341,26 @@ class RuntimeGraphDeriver {
287
341
  buildEdges(decls, apis) {
288
342
  const viaByKey = new Map();
289
343
  const unresolved = [];
344
+ const queues = new Map();
290
345
  for (const decl of decls) {
291
346
  for (const ref of decl.usesApis) {
292
347
  const implementers = apis.get(ref.api)?.implementedBy ?? [];
293
348
  if (implementers.length === 0) {
349
+ // Nobody in-repo serves it. For a vendor contract that is the ANSWER, not a gap:
350
+ // the call leaves the repo, and the viz terminates it at a dashed vendor node.
294
351
  unresolved.push({ service: decl.name, api: ref.api });
295
352
  continue;
296
353
  }
297
354
  for (const target of this.targetsFor(decl.name, ref, implementers)) {
298
- if (target === decl.name)
355
+ // A service calling ITSELF synchronously is noise; a service ENQUEUEING to
356
+ // itself is a real, common topology (deferring its own work), and dropping it
357
+ // was hiding the single most interesting thing about a queue.
358
+ if (target === decl.name && ref.type !== 'pubsub')
359
+ continue;
360
+ if (ref.type === 'pubsub') {
361
+ this.addQueuedEdges(decl.name, target, ref.api, viaByKey, queues);
299
362
  continue;
363
+ }
300
364
  const key = `${decl.name} ${target} ${ref.type}`;
301
365
  if (!viaByKey.has(key))
302
366
  viaByKey.set(key, new Set());
@@ -304,7 +368,53 @@ class RuntimeGraphDeriver {
304
368
  }
305
369
  }
306
370
  }
307
- return { edges: this.edgesFromKeys(viaByKey), unresolved: sortUnresolved(unresolved) };
371
+ return {
372
+ edges: this.edgesFromKeys(viaByKey),
373
+ unresolved: sortUnresolved(unresolved),
374
+ queues: sortedQueues(queues),
375
+ };
376
+ }
377
+ /**
378
+ * One edge PER QUEUED METHOD of `api`, plus the queue each flows through.
379
+ *
380
+ * Per method rather than per service pair because that is the unit Cloud Tasks and Terraform
381
+ * create: two services exchanging three queued methods run three independently-configured,
382
+ * independently-backed-up queues, and collapsing them into one arrow hides which one is stuck.
383
+ *
384
+ * A contract with no committed method table (a dependencies.json predating `apiContracts`)
385
+ * degrades to a single unnamed queue for the pair — the old behavior — rather than vanishing.
386
+ */
387
+ addQueuedEdges(from, to, api, viaByKey, queues) {
388
+ const queued = this.methodsOf(api).filter((m) => m.kind === 'cloudtasks');
389
+ if (queued.length === 0) {
390
+ const key = `${from} ${to} pubsub `;
391
+ if (!viaByKey.has(key))
392
+ viaByKey.set(key, new Set());
393
+ viaByKey.get(key).add(api);
394
+ return;
395
+ }
396
+ for (const method of queued) {
397
+ const queueKey = `${api}.${method.name}`;
398
+ const key = `${from} ${to} pubsub ${queueKey}`;
399
+ if (!viaByKey.has(key))
400
+ viaByKey.set(key, new Set());
401
+ viaByKey.get(key).add(api);
402
+ let queue = queues.get(queueKey);
403
+ if (queue === undefined) {
404
+ queue = {
405
+ api,
406
+ method: method.name,
407
+ queueName: method.queueName,
408
+ producedBy: [],
409
+ consumedBy: [],
410
+ };
411
+ queues.set(queueKey, queue);
412
+ }
413
+ if (!queue.producedBy.includes(from))
414
+ queue.producedBy.push(from);
415
+ if (!queue.consumedBy.includes(to))
416
+ queue.consumedBy.push(to);
417
+ }
308
418
  }
309
419
  /**
310
420
  * WHICH implementers this one `uses` reaches. Resolution order (most specific wins):
@@ -381,19 +491,40 @@ class RuntimeGraphDeriver {
381
491
  }
382
492
  return implementers;
383
493
  }
494
+ /**
495
+ * Rebuild edges from their `"from to type queue"` keys. The 4th part is the queue ("Api.method")
496
+ * and is empty for every non-queued edge; it is part of the KEY so two queued methods between
497
+ * the same pair stay two edges instead of collapsing into one.
498
+ */
384
499
  edgesFromKeys(viaByKey) {
385
500
  const edges = [];
386
501
  for (const key of viaByKey.keys()) {
387
502
  const parts = key.split(' ');
388
- edges.push({ from: parts[0], to: parts[1], via: Array.from(viaByKey.get(key)).sort(), type: parts[2] });
503
+ const edge = {
504
+ from: parts[0],
505
+ to: parts[1],
506
+ via: Array.from(viaByKey.get(key)).sort(),
507
+ type: parts[2],
508
+ };
509
+ if (parts[3] !== undefined && parts[3] !== '')
510
+ edge.queue = parts[3];
511
+ edges.push(edge);
389
512
  }
390
- edges.sort((a, b) => a.from.localeCompare(b.from) || a.to.localeCompare(b.to) || (a.type ?? '').localeCompare(b.type ?? ''));
513
+ edges.sort((a, b) => a.from.localeCompare(b.from) ||
514
+ a.to.localeCompare(b.to) ||
515
+ (a.type ?? '').localeCompare(b.type ?? '') ||
516
+ (a.queue ?? '').localeCompare(b.queue ?? ''));
391
517
  return edges;
392
518
  }
393
519
  buildServices(decls, edges) {
394
520
  const services = {};
395
521
  for (const decl of decls) {
396
- const dependsOn = Array.from(new Set(edges.filter((e) => e.from === decl.name).map((e) => e.to))).sort();
522
+ // A queued hop depends on the QUEUE, not on the peer: the producer hands off and returns,
523
+ // so naming the consumer here would assert a coupling that does not exist (and would make
524
+ // a service that defers work to itself look self-dependent).
525
+ const dependsOn = Array.from(new Set(edges
526
+ .filter((e) => e.from === decl.name)
527
+ .map((e) => (e.queue === undefined ? e.to : `queue:${e.queue}`)))).sort();
397
528
  // Keys are written in this order; an undefined value is omitted by JSON.stringify, so
398
529
  // the committed JSON stays clean AND deterministic without conditional assembly.
399
530
  const service = {
@@ -423,8 +554,8 @@ class RuntimeGraphDeriver {
423
554
  * runtime visualizer omits their nodes + edges.
424
555
  */
425
556
  // webpieces-disable no-function-outside-class -- module entry point for the runtime graph derivation
426
- function deriveRuntimeGraph(projects, hiddenProjects = new Set()) {
427
- return deriveRuntimeGraphReport(projects, hiddenProjects).graph;
557
+ function deriveRuntimeGraph(projects, hiddenProjects = new Set(), apiContracts = {}) {
558
+ return deriveRuntimeGraphReport(projects, hiddenProjects, apiContracts).graph;
428
559
  }
429
560
  /**
430
561
  * The same derivation, plus the warnings it produced (every edge it had to GUESS at). Executors use
@@ -433,8 +564,8 @@ function deriveRuntimeGraph(projects, hiddenProjects = new Set()) {
433
564
  * file that records its own doubts would just get committed and stop being read.
434
565
  */
435
566
  // webpieces-disable no-function-outside-class -- module entry point for the runtime graph derivation
436
- function deriveRuntimeGraphReport(projects, hiddenProjects = new Set()) {
437
- return new RuntimeGraphDeriver(projects, hiddenProjects).assemble();
567
+ function deriveRuntimeGraphReport(projects, hiddenProjects = new Set(), apiContracts = {}) {
568
+ return new RuntimeGraphDeriver(projects, hiddenProjects, apiContracts).assemble();
438
569
  }
439
570
  /** Drop duplicate api refs, keeping the first — needed after a node absorbs the same api from both
440
571
  * its own relations and an embedded lib's. Keyed by api AND target service: the same contract aimed
@@ -453,6 +584,18 @@ function dedupApiRefs(refs) {
453
584
  }
454
585
  return out;
455
586
  }
587
+ /** Queues as a key-sorted object, with each producer/consumer list sorted, for a deterministic file. */
588
+ // webpieces-disable no-function-outside-class -- pure data helper, matches the sibling helpers in this file
589
+ function sortedQueues(queues) {
590
+ const out = {};
591
+ for (const key of [...queues.keys()].sort()) {
592
+ const queue = queues.get(key);
593
+ queue.producedBy.sort();
594
+ queue.consumedBy.sort();
595
+ out[key] = queue;
596
+ }
597
+ return out;
598
+ }
456
599
  /** Sort a Map into a plain object with sorted keys, so the committed JSON is deterministic. */
457
600
  // webpieces-disable no-function-outside-class -- pure data helper, matches the sibling helpers in this file
458
601
  function sortedRecord(map) {
@@ -469,35 +612,4 @@ function sortUnresolved(unresolved) {
469
612
  byKey.set(`${entry.service} ${entry.api}`, entry);
470
613
  return [...byKey.values()].sort((a, b) => a.service.localeCompare(b.service) || a.api.localeCompare(b.api));
471
614
  }
472
- /** Deterministic JSON (sorted keys + arrays already sorted during assembly). */
473
- function formatRuntimeJson(graph) {
474
- return JSON.stringify(graph, null, 4) + '\n';
475
- }
476
- function saveRuntimeGraph(graph, workspaceRoot, graphPath = exports.DEFAULT_RUNTIME_GRAPH_PATH) {
477
- const fullPath = path.join(workspaceRoot, graphPath);
478
- const dir = path.dirname(fullPath);
479
- if (!fs.existsSync(dir))
480
- fs.mkdirSync(dir, { recursive: true });
481
- fs.writeFileSync(fullPath, formatRuntimeJson(graph), 'utf-8');
482
- }
483
- function runtimeGraphFileExists(workspaceRoot, graphPath = exports.DEFAULT_RUNTIME_GRAPH_PATH) {
484
- return fs.existsSync(path.join(workspaceRoot, graphPath));
485
- }
486
- function loadRuntimeGraph(workspaceRoot, graphPath = exports.DEFAULT_RUNTIME_GRAPH_PATH) {
487
- const fullPath = path.join(workspaceRoot, graphPath);
488
- if (!fs.existsSync(fullPath))
489
- return null;
490
- // eslint-disable-next-line @webpieces/no-unmanaged-exceptions
491
- try {
492
- return JSON.parse(fs.readFileSync(fullPath, 'utf-8'));
493
- }
494
- catch (err) {
495
- const error = (0, toError_1.toError)(err);
496
- throw new Error(`Failed to load runtime graph from ${fullPath}`, { cause: error });
497
- }
498
- }
499
- /** Serialize for an in-memory equality check (matches the on-disk format). */
500
- function serializeRuntimeGraph(graph) {
501
- return formatRuntimeJson(graph);
502
- }
503
615
  //# sourceMappingURL=runtime-graph.js.map