@webpieces/nx-webpieces-rules 0.4.899 → 0.4.901

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 (38) hide show
  1. package/package.json +7 -7
  2. package/src/lib/graph-generator.d.ts +3 -1
  3. package/src/lib/graph-generator.js +22 -3
  4. package/src/lib/graph-generator.js.map +1 -1
  5. package/src/lib/graph-legend.js +2 -2
  6. package/src/lib/graph-legend.js.map +1 -1
  7. package/src/lib/graph-loader.d.ts +2 -2
  8. package/src/lib/graph-loader.js +2 -2
  9. package/src/lib/graph-loader.js.map +1 -1
  10. package/src/lib/graph-metadata.d.ts +1 -1
  11. package/src/lib/graph-metadata.js +3 -4
  12. package/src/lib/graph-metadata.js.map +1 -1
  13. package/src/lib/graph-mode-state.client.js +1 -1
  14. package/src/lib/graph-mode-state.client.js.map +1 -1
  15. package/src/lib/graph-page-shell.d.ts +1 -1
  16. package/src/lib/graph-page-shell.js +2 -2
  17. package/src/lib/graph-page-shell.js.map +1 -1
  18. package/src/lib/graph-products.d.ts +24 -42
  19. package/src/lib/graph-products.js +31 -127
  20. package/src/lib/graph-products.js.map +1 -1
  21. package/src/lib/graph-sorter.d.ts +3 -6
  22. package/src/lib/graph-sorter.js.map +1 -1
  23. package/src/lib/product-dependencies.d.ts +5 -3
  24. package/src/lib/product-dependencies.js +6 -5
  25. package/src/lib/product-dependencies.js.map +1 -1
  26. package/src/lib/product-resolver.d.ts +5 -6
  27. package/src/lib/product-resolver.js +5 -6
  28. package/src/lib/product-resolver.js.map +1 -1
  29. package/src/lib/runtime-graph-model.d.ts +4 -6
  30. package/src/lib/runtime-graph-model.js.map +1 -1
  31. package/src/lib/runtime-graph.js +1 -1
  32. package/src/lib/runtime-graph.js.map +1 -1
  33. package/src/lib/tag-truth.d.ts +2 -2
  34. package/src/lib/tag-truth.js +2 -2
  35. package/src/lib/tag-truth.js.map +1 -1
  36. package/src/lib/visible-transitive-reduction.d.ts +33 -0
  37. package/src/lib/visible-transitive-reduction.js +60 -0
  38. package/src/lib/visible-transitive-reduction.js.map +1 -0
@@ -2,34 +2,28 @@
2
2
  /**
3
3
  * Products on the architecture graphs (#1179).
4
4
  *
5
- * A monorepo can hold several PRODUCTS, each with its own clients and servers. A product is declared
6
- * on its entry points with the nx tag `product:<name>` (product-resolver.ts); everything else a
7
- * product consists of is DERIVED here, never declared:
5
+ * A monorepo can hold several PRODUCTS, each with its own clients and servers. A project declares the
6
+ * products it belongs to with the nx tag `product:<name>` (product-resolver.ts). Membership is TAGS
7
+ * ONLY (#1216) on both graphs — nothing is inherited over an edge:
8
8
  *
9
- * - ProductMembership — the dependency graph. A project's OWN `product:` tags are FINAL (#1201): a
10
- * project that declares any keeps exactly its declared set. An UNTAGGED project belongs to every
11
- * product whose tagged project reaches it by a downward walk over `dependsOn` — and that walk stops
12
- * at a tagged project, whose own dependencies are reached by its own seed walk. That closure is
13
- * everything that can change the product's behaviour, so it is what a reviewer of that product
14
- * reviews. It must start from EVERY entry point, clients and servers both: a client reaches its
15
- * server only over HTTP, never through an nx edge, so a server is in its product only because it is
16
- * tagged. Why final: a project is one product's, never two by accident — the example apps depend on
17
- * the platform libraries, and a platform library tagged `product:platform` must not be painted
18
- * `example` because an example reaches it. A published library whose entry points live in OTHER
19
- * repos carries its own tag for exactly that reason: no in-repo walk can ever reach it correctly.
20
- * - RuntimeProductMembership — the runtime graph, under the same rule. A service that already carries
21
- * products (declared, or derived on the dependency graph above) keeps exactly them; a service with
22
- * none belongs to every product whose service reaches it over RUNTIME `dependsOn`, including through
23
- * a queue to its consumers, and that walk stops at a service with products of its own. A call from
24
- * one product into another product's service still shows — as the runtime EDGE between them.
9
+ * - ProductMembership — the dependency graph. A project's products are exactly its own declared
10
+ * `product:` tags. An UNTAGGED project is in no product, however many tagged projects depend on it,
11
+ * and renders as "no product" — a box that still needs a tag. A consumer that wants every project
12
+ * assigned lists the library roles in `product-tags.roles`, so an untagged library fails generation
13
+ * instead of quietly sitting outside every product (and outside the product dependency matrix,
14
+ * which skips any edge with an untagged end — product-dependencies.ts).
15
+ * - RuntimeProductMembership — the runtime graph. A service's products are exactly its project's
16
+ * declared `product:` tags. A service is never put in a product because another service calls it or
17
+ * feeds its queue; a call from one product into another product's service shows as the runtime EDGE
18
+ * between them. A service whose project carries no tag is in no product.
25
19
  * - ProductPalette — the Product color mode's colors: a fixed palette of eight assigned in sorted
26
20
  * product-name order, a neutral fill for a box shared by EVERY product, and a dashed white box for
27
21
  * one in no product.
28
22
  * - ProductBoundary — which dependency edges cross from one product into another (#1204), drawn
29
23
  * dashed in the target product's color in the Product color mode.
30
24
  *
31
- * Membership is stable per checkout (it depends on tags and edges, never on a diff), so it is written
32
- * into the committed dependencies.json / runtime-dependencies.json rather than into a sidecar.
25
+ * Membership is stable per checkout (it depends on tags only, never on a diff), so it is written into
26
+ * the committed dependencies.json / runtime-dependencies.json rather than into a sidecar.
33
27
  */
34
28
  Object.defineProperty(exports, "__esModule", { value: true });
35
29
  exports.RuntimeProductMembership = exports.ProductMembership = exports.ProductBoundary = exports.ProductPalette = exports.PRODUCT_NONE_BORDER = exports.PRODUCT_NONE_FILL = exports.PRODUCT_SHARED_FILL = exports.PRODUCT_PALETTE_SIZE = exports.ProductColor = void 0;
@@ -129,131 +123,41 @@ class ProductBoundary {
129
123
  }
130
124
  exports.ProductBoundary = ProductBoundary;
131
125
  /**
132
- * The architecture graph's product membership: declared seeds walked DOWN over `dependsOn`, where a
133
- * project's own declared products are final.
126
+ * The architecture graph's product membership: exactly each project's declared `product:` tags (#1216).
134
127
  */
135
128
  class ProductMembership {
136
129
  /**
137
130
  * @param seeds project → the products it DECLARES (its `product:` tags)
138
- * @returns project → its sorted products: exactly its declared set when it declares any; otherwise
139
- * every product whose tagged project reaches it without passing through another tagged
140
- * project. A project in no product is absent, so with no tag anywhere the map is empty.
131
+ * @returns project → its sorted declared products. An untagged project is absent — it is in no
132
+ * product, however many tagged projects depend on it — so with no tag anywhere the map is
133
+ * empty.
141
134
  */
142
135
  compute(seeds, graph) {
143
- const declared = new Map();
144
- seeds.forEach((products, seed) => {
145
- if (products.length > 0 && graph[seed] !== undefined)
146
- declared.set(seed, products);
147
- });
148
- const members = new Map();
149
- declared.forEach((products, seed) => {
150
- members.set(seed, new Set(products));
151
- });
152
- declared.forEach((products, seed) => {
153
- for (const project of this.closure(seed, graph, declared)) {
154
- const set = members.get(project) ?? new Set();
155
- for (const product of products)
156
- set.add(product);
157
- members.set(project, set);
158
- }
159
- });
160
136
  const result = new Map();
161
- members.forEach((set, project) => {
162
- result.set(project, [...set].sort());
137
+ seeds.forEach((products, project) => {
138
+ if (products.length > 0 && graph[project] !== undefined)
139
+ result.set(project, [...new Set(products)].sort());
163
140
  });
164
141
  return result;
165
142
  }
166
- /**
167
- * Every UNTAGGED project `seed` reaches through `dependsOn`. A tagged project is neither collected
168
- * nor walked through: its products are final, and its own seed walk covers its dependencies.
169
- */
170
- closure(seed, graph, declared) {
171
- const seen = new Set([seed]);
172
- const reached = new Set();
173
- const stack = [seed];
174
- while (stack.length > 0) {
175
- const project = stack.pop();
176
- for (const dep of graph[project]?.dependsOn ?? []) {
177
- if (seen.has(dep))
178
- continue;
179
- seen.add(dep);
180
- if (declared.has(dep))
181
- continue;
182
- reached.add(dep);
183
- stack.push(dep);
184
- }
185
- }
186
- return reached;
187
- }
188
143
  }
189
144
  exports.ProductMembership = ProductMembership;
190
145
  /**
191
- * The runtime graph's product membership: services that carry products walked over RUNTIME
192
- * `dependsOn`, where a service's own products are final.
146
+ * The runtime graph's product membership: exactly each service's project's declared `product:` tags
147
+ * (#1216). Never derived from who calls a service or feeds its queue.
193
148
  */
194
149
  class RuntimeProductMembership {
195
150
  /**
196
- * Stamp `products` onto every service of `graph` that some product reaches. Seeds are the
197
- * services whose dependencies.json entry carries `products`, and each keeps exactly those; a
198
- * service with none gets every product whose seed reaches it without passing through another
199
- * seed. A `queue:<Api.method>` dependency is followed to that queue's consumers. A service in no
200
- * product gets no field.
151
+ * Stamp `products` onto every service of `graph` whose project declares any. `projects` is the
152
+ * dependency graph, whose `products` are exactly each project's declared tags (ProductMembership);
153
+ * a service whose project declares none gets no field.
201
154
  */
202
155
  apply(graph, projects) {
203
- const seeds = new Map();
204
- for (const name of Object.keys(graph.services)) {
205
- const own = projects[name]?.products ?? [];
206
- if (own.length > 0)
207
- seeds.set(name, own);
208
- }
209
- const members = new Map();
210
- seeds.forEach((own, name) => {
211
- members.set(name, new Set(own));
212
- });
213
- seeds.forEach((own, name) => {
214
- for (const reached of this.closure(name, graph, seeds)) {
215
- const set = members.get(reached) ?? new Set();
216
- for (const product of own)
217
- set.add(product);
218
- members.set(reached, set);
219
- }
220
- });
221
- members.forEach((set, name) => {
222
- const service = graph.services[name];
223
- if (service !== undefined)
224
- service.products = [...set].sort();
225
- });
226
- }
227
- /** Every service WITHOUT products of its own that `seed` reaches; a seed is neither collected nor walked through. */
228
- closure(seed, graph, seeds) {
229
- const seen = new Set([seed]);
230
- const reached = new Set();
231
- const stack = [seed];
232
- while (stack.length > 0) {
233
- const name = stack.pop();
234
- for (const next of this.callees(name, graph)) {
235
- if (seen.has(next) || graph.services[next] === undefined)
236
- continue;
237
- seen.add(next);
238
- if (seeds.has(next))
239
- continue;
240
- reached.add(next);
241
- stack.push(next);
242
- }
243
- }
244
- return reached;
245
- }
246
- /** The services one service calls: direct ones, and the consumers behind every queue it feeds. */
247
- callees(name, graph) {
248
- const callees = [];
249
- for (const dep of graph.services[name]?.dependsOn ?? []) {
250
- if (!dep.startsWith('queue:')) {
251
- callees.push(dep);
252
- continue;
253
- }
254
- callees.push(...(graph.queues[dep.slice('queue:'.length)]?.consumedBy ?? []));
156
+ for (const [name, service] of Object.entries(graph.services)) {
157
+ const declared = projects[name]?.products ?? [];
158
+ if (declared.length > 0)
159
+ service.products = [...declared].sort();
255
160
  }
256
- return callees;
257
161
  }
258
162
  }
259
163
  exports.RuntimeProductMembership = RuntimeProductMembership;
@@ -1 +1 @@
1
- {"version":3,"file":"graph-products.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/graph-products.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Products on the architecture graphs (#1179).\n *\n * A monorepo can hold several PRODUCTS, each with its own clients and servers. A product is declared\n * on its entry points with the nx tag `product:<name>` (product-resolver.ts); everything else a\n * product consists of is DERIVED here, never declared:\n *\n * - ProductMembership — the dependency graph. A project's OWN `product:` tags are FINAL (#1201): a\n * project that declares any keeps exactly its declared set. An UNTAGGED project belongs to every\n * product whose tagged project reaches it by a downward walk over `dependsOn` — and that walk stops\n * at a tagged project, whose own dependencies are reached by its own seed walk. That closure is\n * everything that can change the product's behaviour, so it is what a reviewer of that product\n * reviews. It must start from EVERY entry point, clients and servers both: a client reaches its\n * server only over HTTP, never through an nx edge, so a server is in its product only because it is\n * tagged. Why final: a project is one product's, never two by accident — the example apps depend on\n * the platform libraries, and a platform library tagged `product:platform` must not be painted\n * `example` because an example reaches it. A published library whose entry points live in OTHER\n * repos carries its own tag for exactly that reason: no in-repo walk can ever reach it correctly.\n * - RuntimeProductMembership — the runtime graph, under the same rule. A service that already carries\n * products (declared, or derived on the dependency graph above) keeps exactly them; a service with\n * none belongs to every product whose service reaches it over RUNTIME `dependsOn`, including through\n * a queue to its consumers, and that walk stops at a service with products of its own. A call from\n * one product into another product's service still shows — as the runtime EDGE between them.\n * - ProductPalette — the Product color mode's colors: a fixed palette of eight assigned in sorted\n * product-name order, a neutral fill for a box shared by EVERY product, and a dashed white box for\n * one in no product.\n * - ProductBoundary — which dependency edges cross from one product into another (#1204), drawn\n * dashed in the target product's color in the Product color mode.\n *\n * Membership is stable per checkout (it depends on tags and edges, never on a diff), so it is written\n * into the committed dependencies.json / runtime-dependencies.json rather than into a sidecar.\n */\n\nimport type { EnhancedGraph } from './graph-sorter';\nimport type { RuntimeGraph } from './runtime-graph-model';\n\n// The same two text colors graph-color-modes.ts writes on every fill. Spelled here rather than\n// imported because graph-color-modes.ts imports THIS file (for the palette), and a file-import cycle\n// is refused by `no-file-import-cycles`.\nconst DARK_TEXT = '#1a1c22';\nconst LIGHT_TEXT = '#ffffff';\n\n/** One product's color, and the text color that reads on it. Data-only. */\nexport class ProductColor {\n constructor(\n public readonly product: string,\n public readonly color: string,\n public readonly fontColor: string,\n /** False for a product past the palette's eight, which is painted with the neutral fill. */\n public readonly inPalette: boolean,\n ) {}\n}\n\n/** The eight product colors, in assignment order. Distinct from every runtime and role color. */\nconst PRODUCT_COLORS: readonly (readonly [string, string])[] = [\n ['#3f6fb5', LIGHT_TEXT],\n ['#f28e2b', DARK_TEXT],\n ['#c0392b', LIGHT_TEXT],\n ['#5cb8b2', DARK_TEXT],\n ['#3d8b3d', LIGHT_TEXT],\n ['#edc948', DARK_TEXT],\n ['#8e5ea2', LIGHT_TEXT],\n ['#ff9da7', DARK_TEXT],\n];\n\n/** How many products get a color of their own; any further product uses the neutral fill. */\nexport const PRODUCT_PALETTE_SIZE = PRODUCT_COLORS.length;\n\n/** A box shared by EVERY product, and every product past the palette's eight. */\nexport const PRODUCT_SHARED_FILL = '#c9ced8';\n/** A box in no product: white, with a dashed border. */\nexport const PRODUCT_NONE_FILL = '#ffffff';\nexport const PRODUCT_NONE_BORDER = '#8a90a0';\n\nexport class ProductPalette {\n /** Every declared product, sorted, each with its color. */\n readonly colors: ProductColor[];\n\n constructor(products: readonly string[]) {\n const sorted = [...new Set(products)].sort();\n this.colors = sorted.map((product: string, index: number): ProductColor => {\n const entry = PRODUCT_COLORS[index];\n if (entry === undefined) return new ProductColor(product, PRODUCT_SHARED_FILL, DARK_TEXT, false);\n return new ProductColor(product, entry[0], entry[1], true);\n });\n }\n\n /** Every product carried by a project of the graph, as one palette. */\n // webpieces-disable no-function-outside-class -- static factory of this class\n static fromGraph(graph: EnhancedGraph): ProductPalette {\n const all: string[] = [];\n for (const entry of Object.values(graph)) all.push(...(entry.products ?? []));\n return new ProductPalette(all);\n }\n\n names(): string[] {\n return this.colors.map((color: ProductColor): string => color.product);\n }\n\n colorOf(product: string): ProductColor {\n return (\n this.colors.find((color: ProductColor): boolean => color.product === product) ??\n new ProductColor(product, PRODUCT_SHARED_FILL, DARK_TEXT, false)\n );\n }\n\n /** True for a box carrying every product of a workspace that declares two or more. */\n sharedByAll(products: readonly string[]): boolean {\n return this.colors.length > 1 && this.colors.every((color: ProductColor): boolean => products.includes(color.product));\n }\n\n /** True when some product is past the palette's eight, so the legend must say it is neutral. */\n overflows(): boolean {\n return this.colors.some((color: ProductColor): boolean => !color.inPalette);\n }\n}\n\n/**\n * Where a dependency edge crosses a product boundary (#1204), for the Product color mode: an edge\n * A -> B crosses when B does not carry every product of A, and it is drawn dashed in the color of the\n * product it crosses INTO, so the boundaries read at a glance while same-product edges stay quiet.\n * Whether a crossing is ALLOWED is the product dependency matrix's call (product-dependencies.ts):\n * with `product-tags` ON, generation already refused every crossing the matrix does not allow, so\n * every crossing drawn is an allowed one.\n */\nexport class ProductBoundary {\n /** The product the edge crosses into, or null when the target carries every product of the source. */\n crossedInto(from: readonly string[], to: readonly string[]): string | null {\n if (from.length === 0 || to.length === 0) return null;\n if (from.every((product: string): boolean => to.includes(product))) return null;\n return to.find((product: string): boolean => !from.includes(product)) ?? to[0];\n }\n}\n\n/**\n * The architecture graph's product membership: declared seeds walked DOWN over `dependsOn`, where a\n * project's own declared products are final.\n */\nexport class ProductMembership {\n /**\n * @param seeds project → the products it DECLARES (its `product:` tags)\n * @returns project → its sorted products: exactly its declared set when it declares any; otherwise\n * every product whose tagged project reaches it without passing through another tagged\n * project. A project in no product is absent, so with no tag anywhere the map is empty.\n */\n compute(seeds: ReadonlyMap<string, readonly string[]>, graph: EnhancedGraph): Map<string, string[]> {\n const declared = new Map<string, readonly string[]>();\n seeds.forEach((products: readonly string[], seed: string): void => {\n if (products.length > 0 && graph[seed] !== undefined) declared.set(seed, products);\n });\n const members = new Map<string, Set<string>>();\n declared.forEach((products: readonly string[], seed: string): void => {\n members.set(seed, new Set(products));\n });\n declared.forEach((products: readonly string[], seed: string): void => {\n for (const project of this.closure(seed, graph, declared)) {\n const set = members.get(project) ?? new Set<string>();\n for (const product of products) set.add(product);\n members.set(project, set);\n }\n });\n const result = new Map<string, string[]>();\n members.forEach((set: Set<string>, project: string): void => {\n result.set(project, [...set].sort());\n });\n return result;\n }\n\n /**\n * Every UNTAGGED project `seed` reaches through `dependsOn`. A tagged project is neither collected\n * nor walked through: its products are final, and its own seed walk covers its dependencies.\n */\n private closure(seed: string, graph: EnhancedGraph, declared: ReadonlyMap<string, readonly string[]>): Set<string> {\n const seen = new Set<string>([seed]);\n const reached = new Set<string>();\n const stack = [seed];\n while (stack.length > 0) {\n const project = stack.pop() as string;\n for (const dep of graph[project]?.dependsOn ?? []) {\n if (seen.has(dep)) continue;\n seen.add(dep);\n if (declared.has(dep)) continue;\n reached.add(dep);\n stack.push(dep);\n }\n }\n return reached;\n }\n}\n\n/**\n * The runtime graph's product membership: services that carry products walked over RUNTIME\n * `dependsOn`, where a service's own products are final.\n */\nexport class RuntimeProductMembership {\n /**\n * Stamp `products` onto every service of `graph` that some product reaches. Seeds are the\n * services whose dependencies.json entry carries `products`, and each keeps exactly those; a\n * service with none gets every product whose seed reaches it without passing through another\n * seed. A `queue:<Api.method>` dependency is followed to that queue's consumers. A service in no\n * product gets no field.\n */\n apply(graph: RuntimeGraph, projects: EnhancedGraph): void {\n const seeds = new Map<string, readonly string[]>();\n for (const name of Object.keys(graph.services)) {\n const own = projects[name]?.products ?? [];\n if (own.length > 0) seeds.set(name, own);\n }\n const members = new Map<string, Set<string>>();\n seeds.forEach((own: readonly string[], name: string): void => {\n members.set(name, new Set(own));\n });\n seeds.forEach((own: readonly string[], name: string): void => {\n for (const reached of this.closure(name, graph, seeds)) {\n const set = members.get(reached) ?? new Set<string>();\n for (const product of own) set.add(product);\n members.set(reached, set);\n }\n });\n members.forEach((set: Set<string>, name: string): void => {\n const service = graph.services[name];\n if (service !== undefined) service.products = [...set].sort();\n });\n }\n\n /** Every service WITHOUT products of its own that `seed` reaches; a seed is neither collected nor walked through. */\n private closure(seed: string, graph: RuntimeGraph, seeds: ReadonlyMap<string, readonly string[]>): Set<string> {\n const seen = new Set<string>([seed]);\n const reached = new Set<string>();\n const stack = [seed];\n while (stack.length > 0) {\n const name = stack.pop() as string;\n for (const next of this.callees(name, graph)) {\n if (seen.has(next) || graph.services[next] === undefined) continue;\n seen.add(next);\n if (seeds.has(next)) continue;\n reached.add(next);\n stack.push(next);\n }\n }\n return reached;\n }\n\n /** The services one service calls: direct ones, and the consumers behind every queue it feeds. */\n private callees(name: string, graph: RuntimeGraph): string[] {\n const callees: string[] = [];\n for (const dep of graph.services[name]?.dependsOn ?? []) {\n if (!dep.startsWith('queue:')) {\n callees.push(dep);\n continue;\n }\n callees.push(...(graph.queues[dep.slice('queue:'.length)]?.consumedBy ?? []));\n }\n return callees;\n }\n}\n"]}
1
+ {"version":3,"file":"graph-products.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/graph-products.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Products on the architecture graphs (#1179).\n *\n * A monorepo can hold several PRODUCTS, each with its own clients and servers. A project declares the\n * products it belongs to with the nx tag `product:<name>` (product-resolver.ts). Membership is TAGS\n * ONLY (#1216) on both graphs — nothing is inherited over an edge:\n *\n * - ProductMembership — the dependency graph. A project's products are exactly its own declared\n * `product:` tags. An UNTAGGED project is in no product, however many tagged projects depend on it,\n * and renders as \"no product\" — a box that still needs a tag. A consumer that wants every project\n * assigned lists the library roles in `product-tags.roles`, so an untagged library fails generation\n * instead of quietly sitting outside every product (and outside the product dependency matrix,\n * which skips any edge with an untagged end — product-dependencies.ts).\n * - RuntimeProductMembership — the runtime graph. A service's products are exactly its project's\n * declared `product:` tags. A service is never put in a product because another service calls it or\n * feeds its queue; a call from one product into another product's service shows as the runtime EDGE\n * between them. A service whose project carries no tag is in no product.\n * - ProductPalette — the Product color mode's colors: a fixed palette of eight assigned in sorted\n * product-name order, a neutral fill for a box shared by EVERY product, and a dashed white box for\n * one in no product.\n * - ProductBoundary — which dependency edges cross from one product into another (#1204), drawn\n * dashed in the target product's color in the Product color mode.\n *\n * Membership is stable per checkout (it depends on tags only, never on a diff), so it is written into\n * the committed dependencies.json / runtime-dependencies.json rather than into a sidecar.\n */\n\nimport type { EnhancedGraph } from './graph-sorter';\nimport type { RuntimeGraph } from './runtime-graph-model';\n\n// The same two text colors graph-color-modes.ts writes on every fill. Spelled here rather than\n// imported because graph-color-modes.ts imports THIS file (for the palette), and a file-import cycle\n// is refused by `no-file-import-cycles`.\nconst DARK_TEXT = '#1a1c22';\nconst LIGHT_TEXT = '#ffffff';\n\n/** One product's color, and the text color that reads on it. Data-only. */\nexport class ProductColor {\n constructor(\n public readonly product: string,\n public readonly color: string,\n public readonly fontColor: string,\n /** False for a product past the palette's eight, which is painted with the neutral fill. */\n public readonly inPalette: boolean,\n ) {}\n}\n\n/** The eight product colors, in assignment order. Distinct from every runtime and role color. */\nconst PRODUCT_COLORS: readonly (readonly [string, string])[] = [\n ['#3f6fb5', LIGHT_TEXT],\n ['#f28e2b', DARK_TEXT],\n ['#c0392b', LIGHT_TEXT],\n ['#5cb8b2', DARK_TEXT],\n ['#3d8b3d', LIGHT_TEXT],\n ['#edc948', DARK_TEXT],\n ['#8e5ea2', LIGHT_TEXT],\n ['#ff9da7', DARK_TEXT],\n];\n\n/** How many products get a color of their own; any further product uses the neutral fill. */\nexport const PRODUCT_PALETTE_SIZE = PRODUCT_COLORS.length;\n\n/** A box shared by EVERY product, and every product past the palette's eight. */\nexport const PRODUCT_SHARED_FILL = '#c9ced8';\n/** A box in no product: white, with a dashed border. */\nexport const PRODUCT_NONE_FILL = '#ffffff';\nexport const PRODUCT_NONE_BORDER = '#8a90a0';\n\nexport class ProductPalette {\n /** Every declared product, sorted, each with its color. */\n readonly colors: ProductColor[];\n\n constructor(products: readonly string[]) {\n const sorted = [...new Set(products)].sort();\n this.colors = sorted.map((product: string, index: number): ProductColor => {\n const entry = PRODUCT_COLORS[index];\n if (entry === undefined) return new ProductColor(product, PRODUCT_SHARED_FILL, DARK_TEXT, false);\n return new ProductColor(product, entry[0], entry[1], true);\n });\n }\n\n /** Every product carried by a project of the graph, as one palette. */\n // webpieces-disable no-function-outside-class -- static factory of this class\n static fromGraph(graph: EnhancedGraph): ProductPalette {\n const all: string[] = [];\n for (const entry of Object.values(graph)) all.push(...(entry.products ?? []));\n return new ProductPalette(all);\n }\n\n names(): string[] {\n return this.colors.map((color: ProductColor): string => color.product);\n }\n\n colorOf(product: string): ProductColor {\n return (\n this.colors.find((color: ProductColor): boolean => color.product === product) ??\n new ProductColor(product, PRODUCT_SHARED_FILL, DARK_TEXT, false)\n );\n }\n\n /** True for a box carrying every product of a workspace that declares two or more. */\n sharedByAll(products: readonly string[]): boolean {\n return this.colors.length > 1 && this.colors.every((color: ProductColor): boolean => products.includes(color.product));\n }\n\n /** True when some product is past the palette's eight, so the legend must say it is neutral. */\n overflows(): boolean {\n return this.colors.some((color: ProductColor): boolean => !color.inPalette);\n }\n}\n\n/**\n * Where a dependency edge crosses a product boundary (#1204), for the Product color mode: an edge\n * A -> B crosses when B does not carry every product of A, and it is drawn dashed in the color of the\n * product it crosses INTO, so the boundaries read at a glance while same-product edges stay quiet.\n * Whether a crossing is ALLOWED is the product dependency matrix's call (product-dependencies.ts):\n * with `product-tags` ON, generation already refused every crossing the matrix does not allow, so\n * every crossing drawn is an allowed one.\n */\nexport class ProductBoundary {\n /** The product the edge crosses into, or null when the target carries every product of the source. */\n crossedInto(from: readonly string[], to: readonly string[]): string | null {\n if (from.length === 0 || to.length === 0) return null;\n if (from.every((product: string): boolean => to.includes(product))) return null;\n return to.find((product: string): boolean => !from.includes(product)) ?? to[0];\n }\n}\n\n/**\n * The architecture graph's product membership: exactly each project's declared `product:` tags (#1216).\n */\nexport class ProductMembership {\n /**\n * @param seeds project → the products it DECLARES (its `product:` tags)\n * @returns project → its sorted declared products. An untagged project is absent — it is in no\n * product, however many tagged projects depend on it — so with no tag anywhere the map is\n * empty.\n */\n compute(seeds: ReadonlyMap<string, readonly string[]>, graph: EnhancedGraph): Map<string, string[]> {\n const result = new Map<string, string[]>();\n seeds.forEach((products: readonly string[], project: string): void => {\n if (products.length > 0 && graph[project] !== undefined) result.set(project, [...new Set(products)].sort());\n });\n return result;\n }\n}\n\n/**\n * The runtime graph's product membership: exactly each service's project's declared `product:` tags\n * (#1216). Never derived from who calls a service or feeds its queue.\n */\nexport class RuntimeProductMembership {\n /**\n * Stamp `products` onto every service of `graph` whose project declares any. `projects` is the\n * dependency graph, whose `products` are exactly each project's declared tags (ProductMembership);\n * a service whose project declares none gets no field.\n */\n apply(graph: RuntimeGraph, projects: EnhancedGraph): void {\n for (const [name, service] of Object.entries(graph.services)) {\n const declared = projects[name]?.products ?? [];\n if (declared.length > 0) service.products = [...declared].sort();\n }\n }\n}\n"]}
@@ -62,12 +62,9 @@ export interface GraphEntry {
62
62
  */
63
63
  cutLegacyCycle?: string[];
64
64
  /**
65
- * The products this project belongs to (#1179), sorted. A project carrying `product:<name>` tags
66
- * has exactly those — its declared tags are FINAL (#1201). An untagged project has every product
67
- * whose tagged project reaches it by a downward walk over `dependsOn` that does not pass through
68
- * another tagged project. It is the project's blast radius — a change here is a change to each of
69
- * these products. Absent when no product reaches it (and everywhere when no project carries a
70
- * `product:` tag). See graph-products.ts.
65
+ * The products this project belongs to (#1179), sorted: exactly its declared `product:<name>` tags
66
+ * (#1216). Absent for an untagged project — it is in no product, however many tagged projects
67
+ * depend on it. See graph-products.ts.
71
68
  */
72
69
  products?: string[];
73
70
  shortDescription?: string;
@@ -1 +1 @@
1
- {"version":3,"file":"graph-sorter.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/graph-sorter.ts"],"names":[],"mappings":";AAAA;;;;;;;GAOG;;AAqGH,4DAyCC;AASD,wDAgBC;AApKD,iDAAsD;AAyFtD;;;;;;;;GAQG;AACH,SAAgB,wBAAwB,CAAC,KAA+B;IACpE,MAAM,MAAM,GAAe,EAAE,CAAC;IAC9B,MAAM,SAAS,GAAG,IAAI,GAAG,EAAU,CAAC;IACpC,MAAM,WAAW,GAAG,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAEvC,OAAO,SAAS,CAAC,IAAI,GAAG,WAAW,CAAC,MAAM,EAAE,CAAC;QACzC,MAAM,YAAY,GAAa,EAAE,CAAC;QAElC,KAAK,MAAM,OAAO,IAAI,WAAW,EAAE,CAAC;YAChC,IAAI,SAAS,CAAC,GAAG,CAAC,OAAO,CAAC;gBAAE,SAAS;YAErC,MAAM,IAAI,GAAG,KAAK,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC;YAClC,uEAAuE;YACvE,MAAM,mBAAmB,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,SAAS,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC;YAEpE,IAAI,mBAAmB,EAAE,CAAC;gBACtB,YAAY,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;YAC/B,CAAC;QACL,CAAC;QAED,IAAI,YAAY,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC5B,gFAAgF;YAChF,mFAAmF;YACnF,kFAAkF;YAClF,IAAI,mCAAoB,EAAE,CAAC,aAAa,CAAC,KAAK,EAAE,mBAAmB,CAAC,CAAC;YACrE,sFAAsF;YACtF,MAAM,SAAS,GAAG,WAAW,CAAC,MAAM,CAAC,CAAC,CAAS,EAAW,EAAE,CAAC,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;YAChF,MAAM,IAAI,KAAK,CACX,+EAA+E,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CACxG,CAAC;QACN,CAAC;QAED,4DAA4D;QAC5D,YAAY,CAAC,IAAI,EAAE,CAAC;QACpB,MAAM,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;QAE1B,oBAAoB;QACpB,YAAY,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;IAClD,CAAC;IAED,OAAO,MAAM,CAAC;AAClB,CAAC;AAED;;;;;;GAMG;AACH,SAAgB,sBAAsB,CAAC,KAA+B;IAClE,MAAM,MAAM,GAAG,wBAAwB,CAAC,KAAK,CAAC,CAAC;IAC/C,MAAM,MAAM,GAAkB,EAAE,CAAC;IAEjC,+DAA+D;IAC/D,MAAM,CAAC,OAAO,CAAC,CAAC,KAAK,EAAE,UAAU,EAAE,EAAE;QACjC,KAAK,MAAM,OAAO,IAAI,KAAK,EAAE,CAAC;YAC1B,6CAA6C;YAC7C,MAAM,CAAC,OAAO,CAAC,GAAG;gBACd,KAAK,EAAE,UAAU;gBACjB,SAAS,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE;aAC3C,CAAC;QACN,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,OAAO,MAAM,CAAC;AAClB,CAAC","sourcesContent":["/**\n * Graph Sorter\n *\n * Performs topological sorting on the dependency graph to:\n * 1. Detect circular dependencies (fails if cycle found)\n * 2. Assign level numbers to each project (level 0 = no deps, level 1 = depends on level 0, etc.)\n * 3. Group projects into layers for deterministic ordering\n */\n\nimport type { ProjectApiRelations } from './api-usage/api-relations';\nimport { ProjectCycleDetector } from './graph-cycles';\n\n/**\n * Graph entry with level metadata plus AI-oriented metadata filled in by\n * enrichGraph() (lib/graph-metadata.ts) before the graph is saved:\n * - framework: the project's libType — the SET of runtime environments it is\n * validated to run in, drawn from browser | react | angular | node | express\n * (e.g. [\"browser\",\"node\"]); from its `framework:` nx tags (source of truth)\n * or inferred from package.json deps\n * - shortDescription: summary extracted from the project's responsibilities.md\n * - responsibilitiesFile: repo-relative path to the FULL responsibilities doc\n * - designFile: repo-relative path to the generated DI design.json (only for\n * project.json projects)\n * - apiRelations: for each api-lib in `dependsOn`, WHY the edge exists — the\n * APIs this project implements (serves) and/or uses (calls), each with its\n * transport (rpc | pubsub). Derived by scanning source (see api-usage/).\n */\nexport interface GraphEntry {\n /**\n * The project's row: its dependency depth (0 = no deps) — except that every server, client and\n * app (APP_ROLES) is pinned to the TOP level, and a bundle above the apps it aggregates (#1179,\n * graph-entry-levels.ts). Every dependency still sits strictly below its dependent.\n */\n level: number;\n dependsOn: string[];\n framework?: string[];\n role?: string;\n /**\n * The name CLIENTS address this app by at runtime (`new ClientConfig('helper-fsdb')`) — its\n * deployed service name, DECLARED in project.json as metadata.webpieces.serviceName. Absent for\n * anything nothing calls by name (libraries, browser apps). See service-name-resolver.ts.\n */\n serviceName?: string;\n /**\n * The service(s) this project's clients call when the call site cannot carry a literal\n * `ClientConfig` — the symmetric half of `serviceName`. DECLARED in project.json as\n * metadata.webpieces.callsService, either a single service name (every untargeted `uses` aims\n * there) or an `{ apiClassName: serviceName }` map (a client that calls several). Absent for a\n * project that never calls anything, or one whose call sites all carry literals.\n * See service-name-resolver.ts.\n */\n callsService?: string | Record<string, string>;\n /**\n * When false, the project is hidden from the rendered architecture graphs\n * (its box AND every edge touching it are omitted from dependencies.html and\n * the runtime graph). It stays in this JSON so the data view is complete.\n * Absent means drawn (the default). From the project's `drawOnGraph:` nx tag.\n */\n drawOnGraph?: boolean;\n /**\n * Target service names whose RUNTIME edge from this project is cut out of leveling and cycle\n * detection, from this project's `cutLegacyCycle:<target>` nx tags. Per EDGE, not per project.\n * An admission of legacy debt, never an approved pattern — see cut-legacy-cycle-resolver.ts.\n * Absent (the overwhelmingly normal case) means this project cuts nothing.\n */\n cutLegacyCycle?: string[];\n /**\n * The products this project belongs to (#1179), sorted. A project carrying `product:<name>` tags\n * has exactly those — its declared tags are FINAL (#1201). An untagged project has every product\n * whose tagged project reaches it by a downward walk over `dependsOn` that does not pass through\n * another tagged project. It is the project's blast radius — a change here is a change to each of\n * these products. Absent when no product reaches it (and everywhere when no project carries a\n * `product:` tag). See graph-products.ts.\n */\n products?: string[];\n shortDescription?: string;\n responsibilitiesFile?: string;\n designFile?: string;\n apiRelations?: ProjectApiRelations;\n runtimeDeclaration?: string;\n runtimeComposition?: boolean;\n /**\n * The @webpieces runtime packages this project's OWN package.json declares — http-routing,\n * http-server, http-client-node, http-client-browser, cloudtasks-client. Omitted when it\n * declares none, so TOTAL absence across the file means the file predates this field and\n * nothing is auto-hidden (see runtime-graph.ts). Core/util/logging packages are deliberately\n * not markers; see runtime-participant-resolver.ts for why.\n *\n * The runtime graph ORs this over a node's library closure: a server whose whole webpieces\n * stack comes from a shared bootstrap lib participates even though it declares nothing itself.\n */\n webpiecesRuntime?: string[];\n}\n\n/**\n * Enhanced graph format with level information\n */\nexport type EnhancedGraph = Record<string, GraphEntry>;\n\n/**\n * Compute topological layers for dependency graph using Kahn's algorithm\n *\n * Projects are grouped into layers where each layer only depends on previous layers.\n * Throws an error if a circular dependency is detected.\n *\n * @param graph - Dependency graph { project: [deps] }\n * @returns Array of layers, each containing sorted project names\n */\nexport function computeTopologicalLayers(graph: Record<string, string[]>): string[][] {\n const layers: string[][] = [];\n const processed = new Set<string>();\n const allProjects = Object.keys(graph);\n\n while (processed.size < allProjects.length) {\n const currentLayer: string[] = [];\n\n for (const project of allProjects) {\n if (processed.has(project)) continue;\n\n const deps = graph[project] || [];\n // Check if all dependencies are in previous layers (already processed)\n const allDepsInPrevLayers = deps.every((dep) => processed.has(dep));\n\n if (allDepsInPrevLayers) {\n currentLayer.push(project);\n }\n }\n\n if (currentLayer.length === 0) {\n // No progress made = circular dependency. ProjectCycleDetector is the ONE cycle\n // implementation in this package — it names every chain, in project keys, with the\n // fix — so this reports through it rather than keeping a second, weaker DFS here.\n new ProjectCycleDetector().assertAcyclic(graph, 'the project graph');\n // Unreachable: Kahn only stalls on a cycle, and the assert above throws on every one.\n const remaining = allProjects.filter((p: string): boolean => !processed.has(p));\n throw new Error(\n `Topological sort stalled with no cycle to report — projects left unlayered: ${remaining.join(', ')}`,\n );\n }\n\n // Sort alphabetically within layer for deterministic output\n currentLayer.sort();\n layers.push(currentLayer);\n\n // Mark as processed\n currentLayer.forEach((p) => processed.add(p));\n }\n\n return layers;\n}\n\n/**\n * Sort graph in topological order with alphabetical sorting within layers\n * Returns enhanced format with level metadata\n *\n * @param graph - Unsorted dependency graph { project: [deps] }\n * @returns Sorted graph with level metadata { project: { level: number, dependsOn: [deps] } }\n */\nexport function sortGraphTopologically(graph: Record<string, string[]>): EnhancedGraph {\n const layers = computeTopologicalLayers(graph);\n const result: EnhancedGraph = {};\n\n // Add projects layer by layer (dependencies before dependents)\n layers.forEach((layer, levelIndex) => {\n for (const project of layer) {\n // Already sorted alphabetically within layer\n result[project] = {\n level: levelIndex,\n dependsOn: (graph[project] || []).sort(),\n };\n }\n });\n\n return result;\n}\n"]}
1
+ {"version":3,"file":"graph-sorter.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/graph-sorter.ts"],"names":[],"mappings":";AAAA;;;;;;;GAOG;;AAkGH,4DAyCC;AASD,wDAgBC;AAjKD,iDAAsD;AAsFtD;;;;;;;;GAQG;AACH,SAAgB,wBAAwB,CAAC,KAA+B;IACpE,MAAM,MAAM,GAAe,EAAE,CAAC;IAC9B,MAAM,SAAS,GAAG,IAAI,GAAG,EAAU,CAAC;IACpC,MAAM,WAAW,GAAG,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAEvC,OAAO,SAAS,CAAC,IAAI,GAAG,WAAW,CAAC,MAAM,EAAE,CAAC;QACzC,MAAM,YAAY,GAAa,EAAE,CAAC;QAElC,KAAK,MAAM,OAAO,IAAI,WAAW,EAAE,CAAC;YAChC,IAAI,SAAS,CAAC,GAAG,CAAC,OAAO,CAAC;gBAAE,SAAS;YAErC,MAAM,IAAI,GAAG,KAAK,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC;YAClC,uEAAuE;YACvE,MAAM,mBAAmB,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,SAAS,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC;YAEpE,IAAI,mBAAmB,EAAE,CAAC;gBACtB,YAAY,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;YAC/B,CAAC;QACL,CAAC;QAED,IAAI,YAAY,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC5B,gFAAgF;YAChF,mFAAmF;YACnF,kFAAkF;YAClF,IAAI,mCAAoB,EAAE,CAAC,aAAa,CAAC,KAAK,EAAE,mBAAmB,CAAC,CAAC;YACrE,sFAAsF;YACtF,MAAM,SAAS,GAAG,WAAW,CAAC,MAAM,CAAC,CAAC,CAAS,EAAW,EAAE,CAAC,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;YAChF,MAAM,IAAI,KAAK,CACX,+EAA+E,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CACxG,CAAC;QACN,CAAC;QAED,4DAA4D;QAC5D,YAAY,CAAC,IAAI,EAAE,CAAC;QACpB,MAAM,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;QAE1B,oBAAoB;QACpB,YAAY,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;IAClD,CAAC;IAED,OAAO,MAAM,CAAC;AAClB,CAAC;AAED;;;;;;GAMG;AACH,SAAgB,sBAAsB,CAAC,KAA+B;IAClE,MAAM,MAAM,GAAG,wBAAwB,CAAC,KAAK,CAAC,CAAC;IAC/C,MAAM,MAAM,GAAkB,EAAE,CAAC;IAEjC,+DAA+D;IAC/D,MAAM,CAAC,OAAO,CAAC,CAAC,KAAK,EAAE,UAAU,EAAE,EAAE;QACjC,KAAK,MAAM,OAAO,IAAI,KAAK,EAAE,CAAC;YAC1B,6CAA6C;YAC7C,MAAM,CAAC,OAAO,CAAC,GAAG;gBACd,KAAK,EAAE,UAAU;gBACjB,SAAS,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE;aAC3C,CAAC;QACN,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,OAAO,MAAM,CAAC;AAClB,CAAC","sourcesContent":["/**\n * Graph Sorter\n *\n * Performs topological sorting on the dependency graph to:\n * 1. Detect circular dependencies (fails if cycle found)\n * 2. Assign level numbers to each project (level 0 = no deps, level 1 = depends on level 0, etc.)\n * 3. Group projects into layers for deterministic ordering\n */\n\nimport type { ProjectApiRelations } from './api-usage/api-relations';\nimport { ProjectCycleDetector } from './graph-cycles';\n\n/**\n * Graph entry with level metadata plus AI-oriented metadata filled in by\n * enrichGraph() (lib/graph-metadata.ts) before the graph is saved:\n * - framework: the project's libType — the SET of runtime environments it is\n * validated to run in, drawn from browser | react | angular | node | express\n * (e.g. [\"browser\",\"node\"]); from its `framework:` nx tags (source of truth)\n * or inferred from package.json deps\n * - shortDescription: summary extracted from the project's responsibilities.md\n * - responsibilitiesFile: repo-relative path to the FULL responsibilities doc\n * - designFile: repo-relative path to the generated DI design.json (only for\n * project.json projects)\n * - apiRelations: for each api-lib in `dependsOn`, WHY the edge exists — the\n * APIs this project implements (serves) and/or uses (calls), each with its\n * transport (rpc | pubsub). Derived by scanning source (see api-usage/).\n */\nexport interface GraphEntry {\n /**\n * The project's row: its dependency depth (0 = no deps) — except that every server, client and\n * app (APP_ROLES) is pinned to the TOP level, and a bundle above the apps it aggregates (#1179,\n * graph-entry-levels.ts). Every dependency still sits strictly below its dependent.\n */\n level: number;\n dependsOn: string[];\n framework?: string[];\n role?: string;\n /**\n * The name CLIENTS address this app by at runtime (`new ClientConfig('helper-fsdb')`) — its\n * deployed service name, DECLARED in project.json as metadata.webpieces.serviceName. Absent for\n * anything nothing calls by name (libraries, browser apps). See service-name-resolver.ts.\n */\n serviceName?: string;\n /**\n * The service(s) this project's clients call when the call site cannot carry a literal\n * `ClientConfig` — the symmetric half of `serviceName`. DECLARED in project.json as\n * metadata.webpieces.callsService, either a single service name (every untargeted `uses` aims\n * there) or an `{ apiClassName: serviceName }` map (a client that calls several). Absent for a\n * project that never calls anything, or one whose call sites all carry literals.\n * See service-name-resolver.ts.\n */\n callsService?: string | Record<string, string>;\n /**\n * When false, the project is hidden from the rendered architecture graphs\n * (its box AND every edge touching it are omitted from dependencies.html and\n * the runtime graph). It stays in this JSON so the data view is complete.\n * Absent means drawn (the default). From the project's `drawOnGraph:` nx tag.\n */\n drawOnGraph?: boolean;\n /**\n * Target service names whose RUNTIME edge from this project is cut out of leveling and cycle\n * detection, from this project's `cutLegacyCycle:<target>` nx tags. Per EDGE, not per project.\n * An admission of legacy debt, never an approved pattern — see cut-legacy-cycle-resolver.ts.\n * Absent (the overwhelmingly normal case) means this project cuts nothing.\n */\n cutLegacyCycle?: string[];\n /**\n * The products this project belongs to (#1179), sorted: exactly its declared `product:<name>` tags\n * (#1216). Absent for an untagged project — it is in no product, however many tagged projects\n * depend on it. See graph-products.ts.\n */\n products?: string[];\n shortDescription?: string;\n responsibilitiesFile?: string;\n designFile?: string;\n apiRelations?: ProjectApiRelations;\n runtimeDeclaration?: string;\n runtimeComposition?: boolean;\n /**\n * The @webpieces runtime packages this project's OWN package.json declares — http-routing,\n * http-server, http-client-node, http-client-browser, cloudtasks-client. Omitted when it\n * declares none, so TOTAL absence across the file means the file predates this field and\n * nothing is auto-hidden (see runtime-graph.ts). Core/util/logging packages are deliberately\n * not markers; see runtime-participant-resolver.ts for why.\n *\n * The runtime graph ORs this over a node's library closure: a server whose whole webpieces\n * stack comes from a shared bootstrap lib participates even though it declares nothing itself.\n */\n webpiecesRuntime?: string[];\n}\n\n/**\n * Enhanced graph format with level information\n */\nexport type EnhancedGraph = Record<string, GraphEntry>;\n\n/**\n * Compute topological layers for dependency graph using Kahn's algorithm\n *\n * Projects are grouped into layers where each layer only depends on previous layers.\n * Throws an error if a circular dependency is detected.\n *\n * @param graph - Dependency graph { project: [deps] }\n * @returns Array of layers, each containing sorted project names\n */\nexport function computeTopologicalLayers(graph: Record<string, string[]>): string[][] {\n const layers: string[][] = [];\n const processed = new Set<string>();\n const allProjects = Object.keys(graph);\n\n while (processed.size < allProjects.length) {\n const currentLayer: string[] = [];\n\n for (const project of allProjects) {\n if (processed.has(project)) continue;\n\n const deps = graph[project] || [];\n // Check if all dependencies are in previous layers (already processed)\n const allDepsInPrevLayers = deps.every((dep) => processed.has(dep));\n\n if (allDepsInPrevLayers) {\n currentLayer.push(project);\n }\n }\n\n if (currentLayer.length === 0) {\n // No progress made = circular dependency. ProjectCycleDetector is the ONE cycle\n // implementation in this package — it names every chain, in project keys, with the\n // fix — so this reports through it rather than keeping a second, weaker DFS here.\n new ProjectCycleDetector().assertAcyclic(graph, 'the project graph');\n // Unreachable: Kahn only stalls on a cycle, and the assert above throws on every one.\n const remaining = allProjects.filter((p: string): boolean => !processed.has(p));\n throw new Error(\n `Topological sort stalled with no cycle to report — projects left unlayered: ${remaining.join(', ')}`,\n );\n }\n\n // Sort alphabetically within layer for deterministic output\n currentLayer.sort();\n layers.push(currentLayer);\n\n // Mark as processed\n currentLayer.forEach((p) => processed.add(p));\n }\n\n return layers;\n}\n\n/**\n * Sort graph in topological order with alphabetical sorting within layers\n * Returns enhanced format with level metadata\n *\n * @param graph - Unsorted dependency graph { project: [deps] }\n * @returns Sorted graph with level metadata { project: { level: number, dependsOn: [deps] } }\n */\nexport function sortGraphTopologically(graph: Record<string, string[]>): EnhancedGraph {\n const layers = computeTopologicalLayers(graph);\n const result: EnhancedGraph = {};\n\n // Add projects layer by layer (dependencies before dependents)\n layers.forEach((layer, levelIndex) => {\n for (const project of layer) {\n // Already sorted alphabetically within layer\n result[project] = {\n level: levelIndex,\n dependsOn: (graph[project] || []).sort(),\n };\n }\n });\n\n return result;\n}\n"]}
@@ -16,9 +16,11 @@
16
16
  * only on itself; `[]` as the whole field means no product depends on another.
17
17
  * - An edge A -> B of the DEPENDENCY graph is legal when, for EVERY product p of A, B carries p or a
18
18
  * product p's entry allows. A multi-product B satisfies p when ANY of its products is allowed.
19
- * - The check reads the RESOLVED membership (`products` in dependencies.json): a project's own tags
20
- * are final, an untagged project inherits from the tagged projects that reach it. A project in no
21
- * product constrains nothing.
19
+ * - The check reads `products` in dependencies.json, which is exactly each project's declared tags
20
+ * (#1216). An edge with an UNTAGGED end is skipped: an untagged project is in no product and
21
+ * constrains nothing. So an untagged library is a hole in the check (example app -> untagged lib
22
+ * -> platform lib is never judged); a consumer closes it by listing its library roles in
23
+ * `product-tags.roles`, which makes every such project carry a tag.
22
24
  * - An entry `name`, or an `allowedToDependOn` value, that no `product:` tag uses fails naming it, and
23
25
  * so does a duplicate `name`: a typo must never silently allow or forbid nothing.
24
26
  *
@@ -17,9 +17,11 @@
17
17
  * only on itself; `[]` as the whole field means no product depends on another.
18
18
  * - An edge A -> B of the DEPENDENCY graph is legal when, for EVERY product p of A, B carries p or a
19
19
  * product p's entry allows. A multi-product B satisfies p when ANY of its products is allowed.
20
- * - The check reads the RESOLVED membership (`products` in dependencies.json): a project's own tags
21
- * are final, an untagged project inherits from the tagged projects that reach it. A project in no
22
- * product constrains nothing.
20
+ * - The check reads `products` in dependencies.json, which is exactly each project's declared tags
21
+ * (#1216). An edge with an UNTAGGED end is skipped: an untagged project is in no product and
22
+ * constrains nothing. So an untagged library is a hole in the check (example app -> untagged lib
23
+ * -> platform lib is never judged); a consumer closes it by listing its library roles in
24
+ * `product-tags.roles`, which makes every such project carry a tag.
23
25
  * - An entry `name`, or an `allowedToDependOn` value, that no `product:` tag uses fails naming it, and
24
26
  * so does a duplicate `name`: a typo must never silently allow or forbid nothing.
25
27
  *
@@ -89,8 +91,7 @@ class ProductDependencyMatrix {
89
91
  continue;
90
92
  for (const dep of [...(graph[name].dependsOn ?? [])].sort()) {
91
93
  const to = graph[dep]?.products ?? [];
92
- // Membership is inherited down every edge, so a product's dependency always carries a
93
- // product; a target with none is outside the graph and constrains nothing.
94
+ // #1216: an untagged target is in no product and constrains nothing — see the header.
94
95
  if (to.length === 0)
95
96
  continue;
96
97
  for (const product of from) {
@@ -1 +1 @@
1
- {"version":3,"file":"product-dependencies.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/product-dependencies.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The product dependency matrix (#1204): which products may depend on which.\n *\n * Product membership (graph-products.ts) says which product OWNS each box; this says which products\n * may DEPEND on which, so a platform library quietly importing example code is a generation failure\n * instead of one more edge on the graph. It is the second half of the `product-tags` rule — one switch\n * turns the tags requirement and this matrix on and off together.\n *\n * \"productDependencies\": [\n * { \"name\": \"example\", \"allowedToDependOn\": [\"platform\", \"openapi\"] },\n * { \"name\": \"openapi\", \"allowedToDependOn\": [\"platform\"] }\n * ]\n *\n * Semantics:\n * - A product may always depend on itself (implied, never listed). A product with no entry may depend\n * only on itself; `[]` as the whole field means no product depends on another.\n * - An edge A -> B of the DEPENDENCY graph is legal when, for EVERY product p of A, B carries p or a\n * product p's entry allows. A multi-product B satisfies p when ANY of its products is allowed.\n * - The check reads the RESOLVED membership (`products` in dependencies.json): a project's own tags\n * are final, an untagged project inherits from the tagged projects that reach it. A project in no\n * product constrains nothing.\n * - An entry `name`, or an `allowedToDependOn` value, that no `product:` tag uses fails naming it, and\n * so does a duplicate `name`: a typo must never silently allow or forbid nothing.\n *\n * The runtime graph (HTTP / queue calls between products' services) is deliberately NOT checked:\n * RuntimeProductMembership shows a cross-product call as coupling for a reviewer to see.\n */\n\nimport type { EnhancedGraph } from './graph-sorter';\nimport { ProductDependencyEntry } from '../configs/tag-truth-configs';\n\nconst RULE = 'product-tags';\nconst FIELD = `${RULE}.productDependencies`;\n\nexport class ProductDependencyMatrix {\n /** product → the products it may depend on, itself included. */\n private readonly allowed = new Map<string, Set<string>>();\n\n constructor(public readonly entries: readonly ProductDependencyEntry[]) {\n for (const entry of entries) {\n const set = this.allowed.get(entry.name) ?? new Set<string>([entry.name]);\n for (const other of entry.allowedToDependOn) set.add(other);\n this.allowed.set(entry.name, set);\n }\n }\n\n /** The products a project of `product` may depend on: itself plus its entry's list. */\n allowedFor(product: string): Set<string> {\n return this.allowed.get(product) ?? new Set<string>([product]);\n }\n\n /**\n * Every problem: the matrix's own (unknown names, duplicates), then every illegal edge.\n *\n * @param declared every product some project's `product:` tag names\n */\n problems(graph: EnhancedGraph, declared: ReadonlySet<string>): string[] {\n return [...this.configProblems(declared), ...this.edgeProblems(graph)];\n }\n\n /** A `name` or `allowedToDependOn` value no `product:` tag uses, and a duplicate `name`. */\n configProblems(declared: ReadonlySet<string>): string[] {\n const problems: string[] = [];\n const known = [...declared].sort();\n const knownText = known.length === 0 ? 'none — no project carries a product: tag' : known.join(', ');\n const seen = new Set<string>();\n for (const entry of this.entries) {\n if (seen.has(entry.name)) {\n problems.push(\n `${FIELD}: product '${entry.name}' has two entries — merge them into one ` +\n `{ \"name\": \"${entry.name}\", \"allowedToDependOn\": [...] } so its allowance is stated once.`,\n );\n }\n seen.add(entry.name);\n if (!declared.has(entry.name)) {\n problems.push(\n `${FIELD}: entry \"name\": \"${entry.name}\" matches no product: tag (products in use: ${knownText}) — ` +\n 'fix the typo, or delete the entry.',\n );\n }\n for (const other of entry.allowedToDependOn) {\n if (declared.has(other)) continue;\n problems.push(\n `${FIELD}: \"${entry.name}\" allowedToDependOn \"${other}\", which matches no product: tag ` +\n `(products in use: ${knownText}) — fix the typo, or remove it from the list.`,\n );\n }\n }\n return problems;\n }\n\n /** Every dependency edge, once per product of its source that none of its target's products is allowed for. */\n edgeProblems(graph: EnhancedGraph): string[] {\n const problems: string[] = [];\n for (const name of Object.keys(graph).sort()) {\n const from = graph[name].products ?? [];\n if (from.length === 0) continue;\n for (const dep of [...(graph[name].dependsOn ?? [])].sort()) {\n const to = graph[dep]?.products ?? [];\n // Membership is inherited down every edge, so a product's dependency always carries a\n // product; a target with none is outside the graph and constrains nothing.\n if (to.length === 0) continue;\n for (const product of from) {\n if (this.satisfies(product, to)) continue;\n problems.push(this.violation(name, from, dep, to, product));\n }\n }\n }\n return problems;\n }\n\n /** True when `to` carries `product` or some product `product` may depend on. */\n private satisfies(product: string, to: readonly string[]): boolean {\n const allowed = this.allowedFor(product);\n return to.some((other: string): boolean => allowed.has(other));\n }\n\n private violation(name: string, from: readonly string[], dep: string, to: readonly string[], product: string): string {\n const allowed = [...this.allowedFor(product)].sort();\n return (\n `${RULE}: '${name}' [${from.join(', ')}] -> '${dep}' [${to.join(', ')}] crosses a product boundary — product ` +\n `'${product}' may depend only on [${allowed.join(', ')}]. Either move the code so the dependency stays ` +\n `inside an allowed product, or ${this.cure(to, product)} in webpieces.config.json (a reviewed one-line change).`\n );\n }\n\n /** The config edit that would allow the edge: one of the target's products, any of which is enough. */\n private cure(to: readonly string[], product: string): string {\n const pick = to[0];\n const anyOf = to.length > 1 ? ` (any one of [${to.join(', ')}] is enough)` : '';\n const entry = this.entries.find((each: ProductDependencyEntry): boolean => each.name === product);\n const edit =\n entry === undefined\n ? `add { \"name\": \"${product}\", \"allowedToDependOn\": [\"${pick}\"] } to ${FIELD}`\n : `add \"${pick}\" to the \"${product}\" entry's allowedToDependOn in ${FIELD}`;\n return edit + anyOf;\n }\n}\n"]}
1
+ {"version":3,"file":"product-dependencies.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/product-dependencies.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The product dependency matrix (#1204): which products may depend on which.\n *\n * Product membership (graph-products.ts) says which product OWNS each box; this says which products\n * may DEPEND on which, so a platform library quietly importing example code is a generation failure\n * instead of one more edge on the graph. It is the second half of the `product-tags` rule — one switch\n * turns the tags requirement and this matrix on and off together.\n *\n * \"productDependencies\": [\n * { \"name\": \"example\", \"allowedToDependOn\": [\"platform\", \"openapi\"] },\n * { \"name\": \"openapi\", \"allowedToDependOn\": [\"platform\"] }\n * ]\n *\n * Semantics:\n * - A product may always depend on itself (implied, never listed). A product with no entry may depend\n * only on itself; `[]` as the whole field means no product depends on another.\n * - An edge A -> B of the DEPENDENCY graph is legal when, for EVERY product p of A, B carries p or a\n * product p's entry allows. A multi-product B satisfies p when ANY of its products is allowed.\n * - The check reads `products` in dependencies.json, which is exactly each project's declared tags\n * (#1216). An edge with an UNTAGGED end is skipped: an untagged project is in no product and\n * constrains nothing. So an untagged library is a hole in the check (example app -> untagged lib\n * -> platform lib is never judged); a consumer closes it by listing its library roles in\n * `product-tags.roles`, which makes every such project carry a tag.\n * - An entry `name`, or an `allowedToDependOn` value, that no `product:` tag uses fails naming it, and\n * so does a duplicate `name`: a typo must never silently allow or forbid nothing.\n *\n * The runtime graph (HTTP / queue calls between products' services) is deliberately NOT checked:\n * RuntimeProductMembership shows a cross-product call as coupling for a reviewer to see.\n */\n\nimport type { EnhancedGraph } from './graph-sorter';\nimport { ProductDependencyEntry } from '../configs/tag-truth-configs';\n\nconst RULE = 'product-tags';\nconst FIELD = `${RULE}.productDependencies`;\n\nexport class ProductDependencyMatrix {\n /** product → the products it may depend on, itself included. */\n private readonly allowed = new Map<string, Set<string>>();\n\n constructor(public readonly entries: readonly ProductDependencyEntry[]) {\n for (const entry of entries) {\n const set = this.allowed.get(entry.name) ?? new Set<string>([entry.name]);\n for (const other of entry.allowedToDependOn) set.add(other);\n this.allowed.set(entry.name, set);\n }\n }\n\n /** The products a project of `product` may depend on: itself plus its entry's list. */\n allowedFor(product: string): Set<string> {\n return this.allowed.get(product) ?? new Set<string>([product]);\n }\n\n /**\n * Every problem: the matrix's own (unknown names, duplicates), then every illegal edge.\n *\n * @param declared every product some project's `product:` tag names\n */\n problems(graph: EnhancedGraph, declared: ReadonlySet<string>): string[] {\n return [...this.configProblems(declared), ...this.edgeProblems(graph)];\n }\n\n /** A `name` or `allowedToDependOn` value no `product:` tag uses, and a duplicate `name`. */\n configProblems(declared: ReadonlySet<string>): string[] {\n const problems: string[] = [];\n const known = [...declared].sort();\n const knownText = known.length === 0 ? 'none — no project carries a product: tag' : known.join(', ');\n const seen = new Set<string>();\n for (const entry of this.entries) {\n if (seen.has(entry.name)) {\n problems.push(\n `${FIELD}: product '${entry.name}' has two entries — merge them into one ` +\n `{ \"name\": \"${entry.name}\", \"allowedToDependOn\": [...] } so its allowance is stated once.`,\n );\n }\n seen.add(entry.name);\n if (!declared.has(entry.name)) {\n problems.push(\n `${FIELD}: entry \"name\": \"${entry.name}\" matches no product: tag (products in use: ${knownText}) — ` +\n 'fix the typo, or delete the entry.',\n );\n }\n for (const other of entry.allowedToDependOn) {\n if (declared.has(other)) continue;\n problems.push(\n `${FIELD}: \"${entry.name}\" allowedToDependOn \"${other}\", which matches no product: tag ` +\n `(products in use: ${knownText}) — fix the typo, or remove it from the list.`,\n );\n }\n }\n return problems;\n }\n\n /** Every dependency edge, once per product of its source that none of its target's products is allowed for. */\n edgeProblems(graph: EnhancedGraph): string[] {\n const problems: string[] = [];\n for (const name of Object.keys(graph).sort()) {\n const from = graph[name].products ?? [];\n if (from.length === 0) continue;\n for (const dep of [...(graph[name].dependsOn ?? [])].sort()) {\n const to = graph[dep]?.products ?? [];\n // #1216: an untagged target is in no product and constrains nothing — see the header.\n if (to.length === 0) continue;\n for (const product of from) {\n if (this.satisfies(product, to)) continue;\n problems.push(this.violation(name, from, dep, to, product));\n }\n }\n }\n return problems;\n }\n\n /** True when `to` carries `product` or some product `product` may depend on. */\n private satisfies(product: string, to: readonly string[]): boolean {\n const allowed = this.allowedFor(product);\n return to.some((other: string): boolean => allowed.has(other));\n }\n\n private violation(name: string, from: readonly string[], dep: string, to: readonly string[], product: string): string {\n const allowed = [...this.allowedFor(product)].sort();\n return (\n `${RULE}: '${name}' [${from.join(', ')}] -> '${dep}' [${to.join(', ')}] crosses a product boundary — product ` +\n `'${product}' may depend only on [${allowed.join(', ')}]. Either move the code so the dependency stays ` +\n `inside an allowed product, or ${this.cure(to, product)} in webpieces.config.json (a reviewed one-line change).`\n );\n }\n\n /** The config edit that would allow the edge: one of the target's products, any of which is enough. */\n private cure(to: readonly string[], product: string): string {\n const pick = to[0];\n const anyOf = to.length > 1 ? ` (any one of [${to.join(', ')}] is enough)` : '';\n const entry = this.entries.find((each: ProductDependencyEntry): boolean => each.name === product);\n const edit =\n entry === undefined\n ? `add { \"name\": \"${product}\", \"allowedToDependOn\": [\"${pick}\"] } to ${FIELD}`\n : `add \"${pick}\" to the \"${product}\" entry's allowedToDependOn in ${FIELD}`;\n return edit + anyOf;\n }\n}\n"]}
@@ -5,12 +5,11 @@
5
5
  *
6
6
  * "tags": ["framework:angular", "role:client", "product:lang"]
7
7
  *
8
- * A monorepo holding several products tags each product's ENTRY POINTS (servers, clients, apps —
9
- * the `product-tags` rule makes that required) and the architecture graphs derive the rest: an
10
- * UNTAGGED library belongs to every product whose tagged project reaches it (graph-products.ts).
11
- * A library MAY carry its own tag — a published library whose entry points live in other repos, which
12
- * no in-repo walk can reach correctly — and a project's declared tags are FINAL (#1201): a tagged
13
- * project keeps exactly its declared set, and no other product's walk paints it or passes through it.
8
+ * Membership on both architecture graphs is TAGS ONLY (#1216, graph-products.ts): a project belongs
9
+ * to exactly the products it declares here, and an UNTAGGED project is in no product — it renders as
10
+ * "no product", a box that still needs a tag. Nothing is inherited over a dependency or a runtime call.
11
+ * The `product-tags` rule requires a tag on every project whose role it lists; a consumer that wants
12
+ * every project assigned lists its library roles there too.
14
13
  *
15
14
  * Several tags are allowed (`product:bugfixer` + `product:helper`). Each name must be lowercase
16
15
  * kebab (`lang`, `job-finder`); a malformed tag (`product:`, `product:Lang`) is a problem that fails
@@ -6,12 +6,11 @@
6
6
  *
7
7
  * "tags": ["framework:angular", "role:client", "product:lang"]
8
8
  *
9
- * A monorepo holding several products tags each product's ENTRY POINTS (servers, clients, apps —
10
- * the `product-tags` rule makes that required) and the architecture graphs derive the rest: an
11
- * UNTAGGED library belongs to every product whose tagged project reaches it (graph-products.ts).
12
- * A library MAY carry its own tag — a published library whose entry points live in other repos, which
13
- * no in-repo walk can reach correctly — and a project's declared tags are FINAL (#1201): a tagged
14
- * project keeps exactly its declared set, and no other product's walk paints it or passes through it.
9
+ * Membership on both architecture graphs is TAGS ONLY (#1216, graph-products.ts): a project belongs
10
+ * to exactly the products it declares here, and an UNTAGGED project is in no product — it renders as
11
+ * "no product", a box that still needs a tag. Nothing is inherited over a dependency or a runtime call.
12
+ * The `product-tags` rule requires a tag on every project whose role it lists; a consumer that wants
13
+ * every project assigned lists its library roles there too.
15
14
  *
16
15
  * Several tags are allowed (`product:bugfixer` + `product:helper`). Each name must be lowercase
17
16
  * kebab (`lang`, `job-finder`); a malformed tag (`product:`, `product:Lang`) is a problem that fails
@@ -1 +1 @@
1
- {"version":3,"file":"product-resolver.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/product-resolver.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;;;AAIU,QAAA,kBAAkB,GAAG,UAAU,CAAC;AAE7C,kGAAkG;AAClG,MAAM,YAAY,GAAG,0BAA0B,CAAC;AAEhD,MAAa,iBAAiB;IAGN;IAEA;IAJpB;IACI,kGAAkG;IAClF,QAAkB;IAClC,2FAA2F;IAC3E,QAAkB;QAFlB,aAAQ,GAAR,QAAQ,CAAU;QAElB,aAAQ,GAAR,QAAQ,CAAU;IACnC,CAAC;CACP;AAPD,8CAOC;AAED,MAAa,eAAe;IACxB,2DAA2D;IAC3D,OAAO,CAAC,IAAiB;QACrB,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAU,CAAC;QACnC,MAAM,QAAQ,GAAa,EAAE,CAAC;QAC9B,KAAK,MAAM,GAAG,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC;YAC1B,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,0BAAkB,CAAC;gBAAE,SAAS;YAClD,MAAM,IAAI,GAAG,GAAG,CAAC,KAAK,CAAC,0BAAkB,CAAC,MAAM,CAAC,CAAC;YAClD,IAAI,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,EAAE,CAAC;gBACzB,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;gBACnB,SAAS;YACb,CAAC;YACD,QAAQ,CAAC,IAAI,CACT,GAAG,IAAI,CAAC,IAAI,kBAAkB,GAAG,+DAA+D;gBAC5F,mDAAmD,CAC1D,CAAC;QACN,CAAC;QACD,OAAO,IAAI,iBAAiB,CAAC,CAAC,GAAG,QAAQ,CAAC,CAAC,IAAI,EAAE,EAAE,QAAQ,CAAC,CAAC;IACjE,CAAC;IAED,WAAW,CAAC,IAAY;QACpB,OAAO,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACnC,CAAC;CACJ;AAvBD,0CAuBC","sourcesContent":["/**\n * Product Resolver (#1179)\n *\n * Reads the products a project DECLARES with the nx tag `product:<name>` on its project.json:\n *\n * \"tags\": [\"framework:angular\", \"role:client\", \"product:lang\"]\n *\n * A monorepo holding several products tags each product's ENTRY POINTS (servers, clients, apps —\n * the `product-tags` rule makes that required) and the architecture graphs derive the rest: an\n * UNTAGGED library belongs to every product whose tagged project reaches it (graph-products.ts).\n * A library MAY carry its own tag — a published library whose entry points live in other repos, which\n * no in-repo walk can reach correctly — and a project's declared tags are FINAL (#1201): a tagged\n * project keeps exactly its declared set, and no other product's walk paints it or passes through it.\n *\n * Several tags are allowed (`product:bugfixer` + `product:helper`). Each name must be lowercase\n * kebab (`lang`, `job-finder`); a malformed tag (`product:`, `product:Lang`) is a problem that fails\n * generation with the project named — the same way two `role:` tags do (role-resolver.ts).\n *\n * Why a tag and not a config key: it moves with the project, nx understands it for free\n * (`nx show projects --projects tag:product:lang`), and a tag an older plugin does not know is ignored,\n * so a consumer can tag BEFORE the release that reads the tags ships.\n */\n\nimport { ProjectInfo } from './project-info';\n\nexport const PRODUCT_TAG_PREFIX = 'product:';\n\n/** Lowercase kebab: letters and digits, single dashes between words, no leading/trailing dash. */\nconst PRODUCT_NAME = /^[a-z0-9]+(-[a-z0-9]+)*$/;\n\nexport class ProductResolution {\n constructor(\n /** The declared product names, sorted and de-duplicated; empty when the project declares none. */\n public readonly products: string[],\n /** One line per malformed tag, naming the project; empty when every tag is well formed. */\n public readonly problems: string[],\n ) {}\n}\n\nexport class ProductResolver {\n /** Every `product:` tag the project carries, validated. */\n resolve(info: ProjectInfo): ProductResolution {\n const products = new Set<string>();\n const problems: string[] = [];\n for (const tag of info.tags) {\n if (!tag.startsWith(PRODUCT_TAG_PREFIX)) continue;\n const name = tag.slice(PRODUCT_TAG_PREFIX.length);\n if (this.isValidName(name)) {\n products.add(name);\n continue;\n }\n problems.push(\n `${info.name}: product tag '${tag}' is malformed — the name after 'product:' must be lowercase ` +\n `kebab (e.g. 'product:lang', 'product:job-finder')`,\n );\n }\n return new ProductResolution([...products].sort(), problems);\n }\n\n isValidName(name: string): boolean {\n return PRODUCT_NAME.test(name);\n }\n}\n"]}
1
+ {"version":3,"file":"product-resolver.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/product-resolver.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;;;AAIU,QAAA,kBAAkB,GAAG,UAAU,CAAC;AAE7C,kGAAkG;AAClG,MAAM,YAAY,GAAG,0BAA0B,CAAC;AAEhD,MAAa,iBAAiB;IAGN;IAEA;IAJpB;IACI,kGAAkG;IAClF,QAAkB;IAClC,2FAA2F;IAC3E,QAAkB;QAFlB,aAAQ,GAAR,QAAQ,CAAU;QAElB,aAAQ,GAAR,QAAQ,CAAU;IACnC,CAAC;CACP;AAPD,8CAOC;AAED,MAAa,eAAe;IACxB,2DAA2D;IAC3D,OAAO,CAAC,IAAiB;QACrB,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAU,CAAC;QACnC,MAAM,QAAQ,GAAa,EAAE,CAAC;QAC9B,KAAK,MAAM,GAAG,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC;YAC1B,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,0BAAkB,CAAC;gBAAE,SAAS;YAClD,MAAM,IAAI,GAAG,GAAG,CAAC,KAAK,CAAC,0BAAkB,CAAC,MAAM,CAAC,CAAC;YAClD,IAAI,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,EAAE,CAAC;gBACzB,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;gBACnB,SAAS;YACb,CAAC;YACD,QAAQ,CAAC,IAAI,CACT,GAAG,IAAI,CAAC,IAAI,kBAAkB,GAAG,+DAA+D;gBAC5F,mDAAmD,CAC1D,CAAC;QACN,CAAC;QACD,OAAO,IAAI,iBAAiB,CAAC,CAAC,GAAG,QAAQ,CAAC,CAAC,IAAI,EAAE,EAAE,QAAQ,CAAC,CAAC;IACjE,CAAC;IAED,WAAW,CAAC,IAAY;QACpB,OAAO,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACnC,CAAC;CACJ;AAvBD,0CAuBC","sourcesContent":["/**\n * Product Resolver (#1179)\n *\n * Reads the products a project DECLARES with the nx tag `product:<name>` on its project.json:\n *\n * \"tags\": [\"framework:angular\", \"role:client\", \"product:lang\"]\n *\n * Membership on both architecture graphs is TAGS ONLY (#1216, graph-products.ts): a project belongs\n * to exactly the products it declares here, and an UNTAGGED project is in no product — it renders as\n * \"no product\", a box that still needs a tag. Nothing is inherited over a dependency or a runtime call.\n * The `product-tags` rule requires a tag on every project whose role it lists; a consumer that wants\n * every project assigned lists its library roles there too.\n *\n * Several tags are allowed (`product:bugfixer` + `product:helper`). Each name must be lowercase\n * kebab (`lang`, `job-finder`); a malformed tag (`product:`, `product:Lang`) is a problem that fails\n * generation with the project named — the same way two `role:` tags do (role-resolver.ts).\n *\n * Why a tag and not a config key: it moves with the project, nx understands it for free\n * (`nx show projects --projects tag:product:lang`), and a tag an older plugin does not know is ignored,\n * so a consumer can tag BEFORE the release that reads the tags ships.\n */\n\nimport { ProjectInfo } from './project-info';\n\nexport const PRODUCT_TAG_PREFIX = 'product:';\n\n/** Lowercase kebab: letters and digits, single dashes between words, no leading/trailing dash. */\nconst PRODUCT_NAME = /^[a-z0-9]+(-[a-z0-9]+)*$/;\n\nexport class ProductResolution {\n constructor(\n /** The declared product names, sorted and de-duplicated; empty when the project declares none. */\n public readonly products: string[],\n /** One line per malformed tag, naming the project; empty when every tag is well formed. */\n public readonly problems: string[],\n ) {}\n}\n\nexport class ProductResolver {\n /** Every `product:` tag the project carries, validated. */\n resolve(info: ProjectInfo): ProductResolution {\n const products = new Set<string>();\n const problems: string[] = [];\n for (const tag of info.tags) {\n if (!tag.startsWith(PRODUCT_TAG_PREFIX)) continue;\n const name = tag.slice(PRODUCT_TAG_PREFIX.length);\n if (this.isValidName(name)) {\n products.add(name);\n continue;\n }\n problems.push(\n `${info.name}: product tag '${tag}' is malformed — the name after 'product:' must be lowercase ` +\n `kebab (e.g. 'product:lang', 'product:job-finder')`,\n );\n }\n return new ProductResolution([...products].sort(), problems);\n }\n\n isValidName(name: string): boolean {\n return PRODUCT_NAME.test(name);\n }\n}\n"]}
@@ -54,12 +54,10 @@ export interface RuntimeService {
54
54
  */
55
55
  drawOnGraph?: boolean;
56
56
  /**
57
- * The products this service belongs to (#1179), sorted. A service whose dependencies.json entry
58
- * carries `products` keeps exactly those — they are final (#1201). A service with none gets every
59
- * product whose service reaches it over RUNTIME `dependsOn` (through a queue to its consumers
60
- * included) without passing through another service that carries products; a call into another
61
- * product's service shows as the runtime edge, not as a product. Absent for a service in no
62
- * product. See graph-products.ts.
57
+ * The products this service belongs to (#1179), sorted: exactly its project's declared
58
+ * `product:<name>` tags (#1216). Never derived from a runtime call or a queue; a call into another
59
+ * product's service shows as the runtime edge, not as a product. Absent for a service whose
60
+ * project is untagged. See graph-products.ts.
63
61
  */
64
62
  products?: string[];
65
63
  }
@@ -1 +1 @@
1
- {"version":3,"file":"runtime-graph-model.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/runtime-graph-model.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;GAUG","sourcesContent":["/**\n * Runtime Graph model\n *\n * The serialization DTOs for architecture/runtime-dependencies.json. Split out of runtime-graph.ts,\n * which owns the DERIVATION (and had grown past the file-size limit), so the committed data shape\n * can be read on its own — it is what every consumer of the file, and any future Terraform\n * cross-check, actually programs against.\n *\n * Interfaces rather than classes: these are parsed straight out of JSON with `JSON.parse`, so a\n * class would only ever be a shape assertion over a plain object, never a constructed instance.\n */\n\nimport type {\n ApiRef,\n ApiTransport,\n ExternalSystemDeclaration,\n ExternalSystemKind,\n} from './api-usage/api-relations';\n\nexport interface RuntimeService {\n level: number;\n /**\n * The project's declared nx role — 'server' or 'client' (a library is never a node). Carried so\n * the viz can LABEL the node with what it actually is: it used to infer the role from\n * `implements.length > 0`, which labelled every relation-less server \"client\". Optional so a\n * runtime-dependencies.json written before this field existed still parses and still renders\n * exactly as it did (the visualizer falls back to the old inference).\n */\n role?: string;\n /**\n * The name clients address this service by (`new ClientConfig('helper-fsdb')`), declared in its\n * project.json. Absent for a service nothing calls by name (e.g. a browser app).\n */\n serviceName?: string;\n /**\n * The service(s) this node's clients call when the call site carries no literal `ClientConfig`,\n * declared in its project.json (metadata.webpieces.callsService). A single name, or an\n * `{ apiClassName: serviceName }` map. Absent when the node declares no target. Mirrors\n * GraphEntry.callsService; it is the CALLING-side counterpart of `serviceName`.\n */\n callsService?: string | Record<string, string>;\n implements: string[];\n /**\n * apiClassName -> the LIBRARY project whose apiRelations declared that implements, for the apis\n * this service serves through an embedded library rather than its own source (e.g. a shared\n * route-registration lib). Answers \"who implements WarmupApi, and where did that come from?\",\n * which previously required walking the dependsOn closure by hand.\n */\n implementsVia?: Record<string, string>;\n uses: string[];\n dependsOn: string[];\n /** Reviewed binding provenance and runtime conditions, retained for compact detail views. */\n wiringUses?: ApiRef[];\n wiringImplements?: ApiRef[];\n /**\n * When false, this service is hidden from the rendered runtime graph (its\n * node AND every edge touching it are omitted from the HTML/DOT). It stays\n * in runtime-dependencies.json so the data view is complete. Absent means\n * drawn (the default). Mirrors GraphEntry.drawOnGraph from the `drawOnGraph:`\n * nx tag.\n */\n drawOnGraph?: boolean;\n /**\n * The products this service belongs to (#1179), sorted. A service whose dependencies.json entry\n * carries `products` keeps exactly those — they are final (#1201). A service with none gets every\n * product whose service reaches it over RUNTIME `dependsOn` (through a queue to its consumers\n * included) without passing through another service that carries products; a call into another\n * product's service shows as the runtime edge, not as a product. Absent for a service in no\n * product. See graph-products.ts.\n */\n products?: string[];\n}\n\nexport interface RuntimeApi {\n implementedBy: string[];\n usedBy: string[];\n /** Transport of this API — 'rpc' (direct call) or 'pubsub' (delivered through a queue). */\n type?: ApiTransport;\n /**\n * The api-lib project that OWNS this contract. For a contract nothing in-repo implements, this\n * is the external library the calls leave the repo through (`lib-firestore`, `lib-gmail`), which\n * is what the runtime viz labels its terminal external nodes with.\n */\n owner?: string;\n /**\n * Set when the contract carries an `@externalSystem <kind> [label]` JSDoc tag — the vendor seam\n * declaring WHAT it is a seam to, so the viz can draw firestore as a database rather than as the\n * same grey box as every other external. Absent on everything else.\n */\n externalSystem?: ExternalSystemDeclaration;\n}\n\nexport interface RuntimeEdge {\n from: string;\n to: string;\n via: string[];\n /**\n * Transport of this edge. 'rpc' → a direct call arrow. 'pubsub' → the producer enqueues and the\n * consumer is delivered later, so the runtime viz draws it as producer → QUEUE → consumer.\n * Edges are split by transport, so every edge is a single kind.\n */\n type?: ApiTransport;\n /**\n * `\"ApiClassName.methodName\"` — the queue this edge flows through. Present iff `type` is\n * 'pubsub'. Queues are per METHOD, not per service pair, because that is the unit Cloud Tasks\n * (and Terraform) actually create, so two services exchanging three queued methods are three\n * queues rather than one arrow.\n */\n queue?: string;\n /**\n * True when the CALLING project carries a `cutLegacyCycle:<target>` nx tag naming this edge's\n * target — an admission that this hop closes a REAL cycle that is being tolerated as legacy\n * debt, not a claim that it is harmless. The edge is still drawn (dashed, labelled \"legacy\n * cycle\"); it is only excluded from leveling and cycle detection. Absent on every other edge.\n */\n cutLegacyCycle?: boolean;\n}\n\n/**\n * One Cloud Tasks queue: the async seam between a producer and a consumer, at METHOD granularity.\n *\n * `producedBy` and `consumedBy` are deliberately not symmetric in confidence — see\n * {@link ApiRef.methodsInferred}. The consumer is derived from `addRoutes` plus the contract's\n * method table and is exact; the producer is attributed to every queued method of the contract it\n * built a client for, because which methods it enqueues is not statically recoverable.\n */\nexport interface RuntimeQueue {\n api: string;\n method: string;\n /** `@Queue(...)` override, else `${Api}-${method}` — the name Terraform must match 1:1. */\n queueName: string;\n producedBy: string[];\n consumedBy: string[];\n}\n\n/**\n * An endpoint driven by something that is NOT an in-repo caller — a clock or an outside system.\n * These never appear as runtime EDGES (there is no in-repo `from`), which is exactly why they were\n * invisible until now: a nightly sweep and a GCP push subscription are real runtime entry points\n * with real Terraform behind them, and the graph showed neither.\n */\nexport interface RuntimeTrigger {\n /** 'cron' → a scheduler fires it; 'external' → a system outside this repo posts to it. */\n kind: 'cron' | 'external';\n api: string;\n method: string;\n /** The service that SERVES the endpoint (the arrow's head). */\n service: string;\n /** Present for 'cron': the Cloud Scheduler job / queue name Terraform must match. */\n queueName?: string;\n /**\n * Present for 'external': WHO outside this repo posts to it, declared as\n * `@Endpoint(POST, p, WRITE, EXTERNAL, { calledBy: 'twilio' })`.\n *\n * The SAME `(kind,label)` an OUTBOUND {@link RuntimeExternalSystem} carries, so an inbound\n * `saas twilio` and an outbound `saas twilio` share a node identity and converge on ONE box —\n * `label` is the identity here too, not display text.\n *\n * Optional only for graphs generated BEFORE the caller was required; generation now fails rather\n * than emitting an `external` method without one.\n */\n caller?: ExternalSystemDeclaration;\n}\n\nexport interface RuntimeUnresolved {\n service: string;\n api: string;\n}\n\n/**\n * A system outside this repo that a service TALKS TO, drawn with a shape that says what it is\n * (a database as a cylinder, a bucket as a folder) instead of the one grey box every external\n * used to collapse into.\n *\n * Two declaration sites feed this, because the two real cases differ in whether a contract exists:\n *\n * - **Wrapped** — the repo has a vendor seam (`FirestoreAdminApi`), so the kind is declared with an\n * `@externalSystem <kind> [label]` JSDoc tag on the contract. A decorator cannot go on a TS\n * `interface`, and these seams are interfaces, so JSDoc is the only marker that fits in place.\n * - **Unwrapped** — the service opens the connection itself with no contract to mark (a `pg.Pool`,\n * a TypeORM `DataSource`), so the declaration is an `external:<kind>:<identity>` nx tag on that\n * project's project.json.\n *\n * `label` is the node IDENTITY, not just display text: two services declaring `postgres` converge on\n * ONE node with two arrows into it, rather than drawing a database each.\n */\nexport interface RuntimeExternalSystem {\n kind: ExternalSystemKind;\n /** Display name AND node identity — declarations sharing a label share a node. */\n label: string;\n /** Services with a direct arrow to it. No transitive fan-out: a tag speaks only for its project. */\n usedBy: string[];\n /** Contracts flowing over it. Empty for the unwrapped (tag-declared) case — there are none. */\n apis: string[];\n}\n\n/**\n * One caller's calls into a {@link RuntimeRemoteService}, one entry per (caller, transport): `via`\n * lists the contracts it calls there.\n */\nexport interface RuntimeRemoteCall {\n from: string;\n via: string[];\n type: ApiTransport;\n}\n\n/**\n * A service OUTSIDE this workspace (another repo, a VM, partner endpoints) declared in the reviewed\n * `remoteServices` table of dependencies.json (#1203). It is a SERVICE — it implements our contracts —\n * so it is drawn as a service node, distinct from a vendor external system. Render-only, like the\n * external systems: it is never leveled or cycle-checked, because its own calls are not in this repo.\n */\nexport interface RuntimeRemoteService {\n /** Display text from the reviewed declaration; the key is the identity. */\n label?: string;\n /** The contracts it is declared to implement, as `Api (transport)`, in reviewed order. */\n implements: string[];\n /** Workspace services that target it, sorted by caller then transport. */\n calledBy: RuntimeRemoteCall[];\n}\n\nexport interface RuntimeGraph {\n services: Record<string, RuntimeService>;\n apis: Record<string, RuntimeApi>;\n runtimeEdges: RuntimeEdge[];\n unresolvedUses: RuntimeUnresolved[];\n /** `\"Api.method\"` -> the queue between its producers and its consumers. */\n queues: Record<string, RuntimeQueue>;\n /** Clock- and outside-driven entry points, sorted for determinism. */\n triggers: RuntimeTrigger[];\n /**\n * Declared external systems, keyed by identity. Optional so every graph written before this\n * existed still parses — an absent map means \"nothing declared\", which renders exactly as before.\n */\n externalSystems?: Record<string, RuntimeExternalSystem>;\n /**\n * Reviewed out-of-workspace services, keyed by service name (#1203). Absent when none are\n * declared, so every graph written before this existed parses and renders exactly as before.\n */\n remoteServices?: Record<string, RuntimeRemoteService>;\n}\n"]}
1
+ {"version":3,"file":"runtime-graph-model.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/runtime-graph-model.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;GAUG","sourcesContent":["/**\n * Runtime Graph model\n *\n * The serialization DTOs for architecture/runtime-dependencies.json. Split out of runtime-graph.ts,\n * which owns the DERIVATION (and had grown past the file-size limit), so the committed data shape\n * can be read on its own — it is what every consumer of the file, and any future Terraform\n * cross-check, actually programs against.\n *\n * Interfaces rather than classes: these are parsed straight out of JSON with `JSON.parse`, so a\n * class would only ever be a shape assertion over a plain object, never a constructed instance.\n */\n\nimport type {\n ApiRef,\n ApiTransport,\n ExternalSystemDeclaration,\n ExternalSystemKind,\n} from './api-usage/api-relations';\n\nexport interface RuntimeService {\n level: number;\n /**\n * The project's declared nx role — 'server' or 'client' (a library is never a node). Carried so\n * the viz can LABEL the node with what it actually is: it used to infer the role from\n * `implements.length > 0`, which labelled every relation-less server \"client\". Optional so a\n * runtime-dependencies.json written before this field existed still parses and still renders\n * exactly as it did (the visualizer falls back to the old inference).\n */\n role?: string;\n /**\n * The name clients address this service by (`new ClientConfig('helper-fsdb')`), declared in its\n * project.json. Absent for a service nothing calls by name (e.g. a browser app).\n */\n serviceName?: string;\n /**\n * The service(s) this node's clients call when the call site carries no literal `ClientConfig`,\n * declared in its project.json (metadata.webpieces.callsService). A single name, or an\n * `{ apiClassName: serviceName }` map. Absent when the node declares no target. Mirrors\n * GraphEntry.callsService; it is the CALLING-side counterpart of `serviceName`.\n */\n callsService?: string | Record<string, string>;\n implements: string[];\n /**\n * apiClassName -> the LIBRARY project whose apiRelations declared that implements, for the apis\n * this service serves through an embedded library rather than its own source (e.g. a shared\n * route-registration lib). Answers \"who implements WarmupApi, and where did that come from?\",\n * which previously required walking the dependsOn closure by hand.\n */\n implementsVia?: Record<string, string>;\n uses: string[];\n dependsOn: string[];\n /** Reviewed binding provenance and runtime conditions, retained for compact detail views. */\n wiringUses?: ApiRef[];\n wiringImplements?: ApiRef[];\n /**\n * When false, this service is hidden from the rendered runtime graph (its\n * node AND every edge touching it are omitted from the HTML/DOT). It stays\n * in runtime-dependencies.json so the data view is complete. Absent means\n * drawn (the default). Mirrors GraphEntry.drawOnGraph from the `drawOnGraph:`\n * nx tag.\n */\n drawOnGraph?: boolean;\n /**\n * The products this service belongs to (#1179), sorted: exactly its project's declared\n * `product:<name>` tags (#1216). Never derived from a runtime call or a queue; a call into another\n * product's service shows as the runtime edge, not as a product. Absent for a service whose\n * project is untagged. See graph-products.ts.\n */\n products?: string[];\n}\n\nexport interface RuntimeApi {\n implementedBy: string[];\n usedBy: string[];\n /** Transport of this API — 'rpc' (direct call) or 'pubsub' (delivered through a queue). */\n type?: ApiTransport;\n /**\n * The api-lib project that OWNS this contract. For a contract nothing in-repo implements, this\n * is the external library the calls leave the repo through (`lib-firestore`, `lib-gmail`), which\n * is what the runtime viz labels its terminal external nodes with.\n */\n owner?: string;\n /**\n * Set when the contract carries an `@externalSystem <kind> [label]` JSDoc tag — the vendor seam\n * declaring WHAT it is a seam to, so the viz can draw firestore as a database rather than as the\n * same grey box as every other external. Absent on everything else.\n */\n externalSystem?: ExternalSystemDeclaration;\n}\n\nexport interface RuntimeEdge {\n from: string;\n to: string;\n via: string[];\n /**\n * Transport of this edge. 'rpc' → a direct call arrow. 'pubsub' → the producer enqueues and the\n * consumer is delivered later, so the runtime viz draws it as producer → QUEUE → consumer.\n * Edges are split by transport, so every edge is a single kind.\n */\n type?: ApiTransport;\n /**\n * `\"ApiClassName.methodName\"` — the queue this edge flows through. Present iff `type` is\n * 'pubsub'. Queues are per METHOD, not per service pair, because that is the unit Cloud Tasks\n * (and Terraform) actually create, so two services exchanging three queued methods are three\n * queues rather than one arrow.\n */\n queue?: string;\n /**\n * True when the CALLING project carries a `cutLegacyCycle:<target>` nx tag naming this edge's\n * target — an admission that this hop closes a REAL cycle that is being tolerated as legacy\n * debt, not a claim that it is harmless. The edge is still drawn (dashed, labelled \"legacy\n * cycle\"); it is only excluded from leveling and cycle detection. Absent on every other edge.\n */\n cutLegacyCycle?: boolean;\n}\n\n/**\n * One Cloud Tasks queue: the async seam between a producer and a consumer, at METHOD granularity.\n *\n * `producedBy` and `consumedBy` are deliberately not symmetric in confidence — see\n * {@link ApiRef.methodsInferred}. The consumer is derived from `addRoutes` plus the contract's\n * method table and is exact; the producer is attributed to every queued method of the contract it\n * built a client for, because which methods it enqueues is not statically recoverable.\n */\nexport interface RuntimeQueue {\n api: string;\n method: string;\n /** `@Queue(...)` override, else `${Api}-${method}` — the name Terraform must match 1:1. */\n queueName: string;\n producedBy: string[];\n consumedBy: string[];\n}\n\n/**\n * An endpoint driven by something that is NOT an in-repo caller — a clock or an outside system.\n * These never appear as runtime EDGES (there is no in-repo `from`), which is exactly why they were\n * invisible until now: a nightly sweep and a GCP push subscription are real runtime entry points\n * with real Terraform behind them, and the graph showed neither.\n */\nexport interface RuntimeTrigger {\n /** 'cron' → a scheduler fires it; 'external' → a system outside this repo posts to it. */\n kind: 'cron' | 'external';\n api: string;\n method: string;\n /** The service that SERVES the endpoint (the arrow's head). */\n service: string;\n /** Present for 'cron': the Cloud Scheduler job / queue name Terraform must match. */\n queueName?: string;\n /**\n * Present for 'external': WHO outside this repo posts to it, declared as\n * `@Endpoint(POST, p, WRITE, EXTERNAL, { calledBy: 'twilio' })`.\n *\n * The SAME `(kind,label)` an OUTBOUND {@link RuntimeExternalSystem} carries, so an inbound\n * `saas twilio` and an outbound `saas twilio` share a node identity and converge on ONE box —\n * `label` is the identity here too, not display text.\n *\n * Optional only for graphs generated BEFORE the caller was required; generation now fails rather\n * than emitting an `external` method without one.\n */\n caller?: ExternalSystemDeclaration;\n}\n\nexport interface RuntimeUnresolved {\n service: string;\n api: string;\n}\n\n/**\n * A system outside this repo that a service TALKS TO, drawn with a shape that says what it is\n * (a database as a cylinder, a bucket as a folder) instead of the one grey box every external\n * used to collapse into.\n *\n * Two declaration sites feed this, because the two real cases differ in whether a contract exists:\n *\n * - **Wrapped** — the repo has a vendor seam (`FirestoreAdminApi`), so the kind is declared with an\n * `@externalSystem <kind> [label]` JSDoc tag on the contract. A decorator cannot go on a TS\n * `interface`, and these seams are interfaces, so JSDoc is the only marker that fits in place.\n * - **Unwrapped** — the service opens the connection itself with no contract to mark (a `pg.Pool`,\n * a TypeORM `DataSource`), so the declaration is an `external:<kind>:<identity>` nx tag on that\n * project's project.json.\n *\n * `label` is the node IDENTITY, not just display text: two services declaring `postgres` converge on\n * ONE node with two arrows into it, rather than drawing a database each.\n */\nexport interface RuntimeExternalSystem {\n kind: ExternalSystemKind;\n /** Display name AND node identity — declarations sharing a label share a node. */\n label: string;\n /** Services with a direct arrow to it. No transitive fan-out: a tag speaks only for its project. */\n usedBy: string[];\n /** Contracts flowing over it. Empty for the unwrapped (tag-declared) case — there are none. */\n apis: string[];\n}\n\n/**\n * One caller's calls into a {@link RuntimeRemoteService}, one entry per (caller, transport): `via`\n * lists the contracts it calls there.\n */\nexport interface RuntimeRemoteCall {\n from: string;\n via: string[];\n type: ApiTransport;\n}\n\n/**\n * A service OUTSIDE this workspace (another repo, a VM, partner endpoints) declared in the reviewed\n * `remoteServices` table of dependencies.json (#1203). It is a SERVICE — it implements our contracts —\n * so it is drawn as a service node, distinct from a vendor external system. Render-only, like the\n * external systems: it is never leveled or cycle-checked, because its own calls are not in this repo.\n */\nexport interface RuntimeRemoteService {\n /** Display text from the reviewed declaration; the key is the identity. */\n label?: string;\n /** The contracts it is declared to implement, as `Api (transport)`, in reviewed order. */\n implements: string[];\n /** Workspace services that target it, sorted by caller then transport. */\n calledBy: RuntimeRemoteCall[];\n}\n\nexport interface RuntimeGraph {\n services: Record<string, RuntimeService>;\n apis: Record<string, RuntimeApi>;\n runtimeEdges: RuntimeEdge[];\n unresolvedUses: RuntimeUnresolved[];\n /** `\"Api.method\"` -> the queue between its producers and its consumers. */\n queues: Record<string, RuntimeQueue>;\n /** Clock- and outside-driven entry points, sorted for determinism. */\n triggers: RuntimeTrigger[];\n /**\n * Declared external systems, keyed by identity. Optional so every graph written before this\n * existed still parses — an absent map means \"nothing declared\", which renders exactly as before.\n */\n externalSystems?: Record<string, RuntimeExternalSystem>;\n /**\n * Reviewed out-of-workspace services, keyed by service name (#1203). Absent when none are\n * declared, so every graph written before this existed parses and renders exactly as before.\n */\n remoteServices?: Record<string, RuntimeRemoteService>;\n}\n"]}