@webpieces/nx-webpieces-rules 0.4.947 → 0.4.948

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 (42) hide show
  1. package/README.md +20 -11
  2. package/package.json +7 -7
  3. package/src/index.d.ts +1 -1
  4. package/src/index.js +3 -1
  5. package/src/index.js.map +1 -1
  6. package/src/lib/graph-color-modes.d.ts +18 -12
  7. package/src/lib/graph-color-modes.js +18 -20
  8. package/src/lib/graph-color-modes.js.map +1 -1
  9. package/src/lib/graph-legend.d.ts +11 -3
  10. package/src/lib/graph-legend.js +25 -9
  11. package/src/lib/graph-legend.js.map +1 -1
  12. package/src/lib/graph-mode-state.client.d.ts +2 -1
  13. package/src/lib/graph-mode-state.client.js +3 -1
  14. package/src/lib/graph-mode-state.client.js.map +1 -1
  15. package/src/lib/graph-page-shell.js +2 -2
  16. package/src/lib/graph-page-shell.js.map +1 -1
  17. package/src/lib/graph-page-styles.js +4 -1
  18. package/src/lib/graph-page-styles.js.map +1 -1
  19. package/src/lib/graph-product-paint.client.d.ts +92 -0
  20. package/src/lib/graph-product-paint.client.js +196 -0
  21. package/src/lib/graph-product-paint.client.js.map +1 -0
  22. package/src/lib/graph-products.d.ts +54 -6
  23. package/src/lib/graph-products.js +62 -9
  24. package/src/lib/graph-products.js.map +1 -1
  25. package/src/lib/graph-render-model.d.ts +12 -12
  26. package/src/lib/graph-render-model.js +12 -14
  27. package/src/lib/graph-render-model.js.map +1 -1
  28. package/src/lib/graph-visualizer.client.d.ts +13 -1
  29. package/src/lib/graph-visualizer.client.js +27 -4
  30. package/src/lib/graph-visualizer.client.js.map +1 -1
  31. package/src/lib/graph-visualizer.d.ts +3 -3
  32. package/src/lib/graph-visualizer.js +13 -12
  33. package/src/lib/graph-visualizer.js.map +1 -1
  34. package/src/lib/runtime-goal-comparison.js +1 -1
  35. package/src/lib/runtime-goal-comparison.js.map +1 -1
  36. package/src/lib/runtime-html-page.js +3 -2
  37. package/src/lib/runtime-html-page.js.map +1 -1
  38. package/src/lib/runtime-layout.js +1 -1
  39. package/src/lib/runtime-layout.js.map +1 -1
  40. package/src/lib/runtime-viewer-adapter.d.ts +4 -1
  41. package/src/lib/runtime-viewer-adapter.js +6 -13
  42. package/src/lib/runtime-viewer-adapter.js.map +1 -1
@@ -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 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 (#1244): eight fixed saturated colors, each\n * product hashed (SHA-256 of its name) to a preferred one, with collisions resolved in sorted-name\n * order. Both viewers build it over the FULL declared product set, so they agree. Neutral means\n * shared by EVERY product; a dashed white box belongs to none.\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 { createHash } from 'node:crypto';\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 ) {}\n}\n\n/** One of the palette's eight colors, and the text color that reads on it. Data-only. */\nexport class ProductPaletteSlot {\n constructor(\n public readonly color: string,\n public readonly fontColor: string,\n ) {}\n}\n\n/** The eight product colors, indexed by slot. Distinct from every runtime and role color. */\nexport const PRODUCT_PALETTE_SLOTS: readonly ProductPaletteSlot[] = [\n new ProductPaletteSlot('#3f6fb5', LIGHT_TEXT),\n new ProductPaletteSlot('#f28e2b', DARK_TEXT),\n new ProductPaletteSlot('#c0392b', LIGHT_TEXT),\n new ProductPaletteSlot('#5cb8b2', DARK_TEXT),\n new ProductPaletteSlot('#3d8b3d', LIGHT_TEXT),\n new ProductPaletteSlot('#edc948', DARK_TEXT),\n new ProductPaletteSlot('#8e5ea2', LIGHT_TEXT),\n new ProductPaletteSlot('#ff9da7', DARK_TEXT),\n];\n\n/** A box shared by EVERY product. */\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 /**\n * @param products the FULL declared product set. Collision resolution depends on the set, so a\n * viewer showing a subset must still pass every declared product, or its colors would\n * disagree with the other viewer's.\n */\n constructor(products: readonly string[]) {\n const sorted = [...new Set(products)].sort();\n const taken = new Set<number>();\n this.colors = sorted.map((product: string): ProductColor => {\n const slot = this.freeSlot(this.preferredSlot(product), taken);\n taken.add(slot);\n return this.paint(product, slot);\n });\n }\n\n /** The slot a product's name hashes to: SHA-256 of the name, first byte, mod the palette size. */\n preferredSlot(product: string): number {\n return createHash('sha256').update(product, 'utf8').digest()[0] % PRODUCT_PALETTE_SLOTS.length;\n }\n\n /** The preferred slot when free, else the next free one (wrapping); with all taken, the preferred one. */\n private freeSlot(preferred: number, taken: ReadonlySet<number>): number {\n for (let step = 0; step < PRODUCT_PALETTE_SLOTS.length; step++) {\n const slot = (preferred + step) % PRODUCT_PALETTE_SLOTS.length;\n if (!taken.has(slot)) return slot;\n }\n return preferred;\n }\n\n private paint(product: string, slot: number): ProductColor {\n const entry = PRODUCT_PALETTE_SLOTS[slot];\n return new ProductColor(product, entry.color, entry.fontColor);\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 // The assignment contract (#1244): the slot SHA-256 of the name prefers, with collisions\n // resolved in sorted-name order over the whole set — the earlier name keeps a contested slot,\n // the later takes the next free one. So adding a product recolors nobody unless it hashes onto\n // a taken slot AND sorts before its holder. Never assign by sorted index: every addition would\n // shift every later product. A product outside the set gets its preferred slot.\n return (\n this.colors.find((color: ProductColor): boolean => color.product === product) ??\n this.paint(product, this.preferredSlot(product))\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\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"]}
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 (#1244): eight fixed saturated colors, each\n * product hashed (SHA-256 of its name) to a preferred one, with collisions resolved in sorted-name\n * order. Both viewers build it over the FULL declared product set, so they agree, and a color is\n * never reassigned when a product is toggled OFF.\n * - ProductTemplate — what a page needs to paint one box in the Product color mode (#1319). Each\n * product's legend row is a toggle and the page repaints in the browser from the ON set\n * (WpProductPainter, graph-product-paint.client.ts), so the box's product statement is computed\n * there, not here.\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 while that product is ON.\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 { createHash } from 'node:crypto';\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 ) {}\n}\n\n/** One of the palette's eight colors, and the text color that reads on it. Data-only. */\nexport class ProductPaletteSlot {\n constructor(\n public readonly color: string,\n public readonly fontColor: string,\n ) {}\n}\n\n/** The eight product colors, indexed by slot. Distinct from every runtime and role color. */\nexport const PRODUCT_PALETTE_SLOTS: readonly ProductPaletteSlot[] = [\n new ProductPaletteSlot('#3f6fb5', LIGHT_TEXT),\n new ProductPaletteSlot('#f28e2b', DARK_TEXT),\n new ProductPaletteSlot('#c0392b', LIGHT_TEXT),\n new ProductPaletteSlot('#5cb8b2', DARK_TEXT),\n new ProductPaletteSlot('#3d8b3d', LIGHT_TEXT),\n new ProductPaletteSlot('#edc948', DARK_TEXT),\n new ProductPaletteSlot('#8e5ea2', LIGHT_TEXT),\n new ProductPaletteSlot('#ff9da7', DARK_TEXT),\n];\n\n/** A box shared by EVERY product, drawn neutral only while every product is ON (#1319). */\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/**\n * A box carrying only products switched OFF (#1319): light grey with a grey border, so it stays\n * visible — hiding boxes is the Filter's job — and never reads as the neutral shared fill.\n */\nexport const PRODUCT_OFF_FILL = '#eef0f4';\nexport const PRODUCT_OFF_BORDER = '#b4b9c4';\n\n/**\n * How many products are ON when a page loads (#1319): the first ones in legend (sorted-name) order.\n * Always this many, on every load — the selection is never persisted.\n */\nexport const PRODUCTS_ON_AT_LOAD = 2;\n\n/** Whether a ProductTemplate paints a styled box or restyles a contextual node's kind statement. */\nexport enum ProductTemplateKind {\n /** A project or service box: the page writes its whole statement. */\n BOX = 'box',\n /** A runtime queue, clock, datastore or vendor: the page recolors its kind statement's fill. */\n CONTEXT = 'context',\n}\n\n/**\n * What the page needs to write one node's Product-mode statement for any ON set (#1319). Data-only.\n * A BOX statement is `head` + the paint's attributes + the label for its text color; a CONTEXT\n * statement is `head` (the kind statement) with its fill replaced.\n */\nexport class ProductTemplate {\n constructor(\n public readonly kind: ProductTemplateKind,\n /** BOX: ` \"id\" [`. CONTEXT: the node's whole kind statement. */\n public readonly head: string,\n /** BOX: the HTML label written in dark text; '' for CONTEXT. */\n public readonly darkLabel: string,\n /** BOX: the HTML label written in light text; '' for CONTEXT. */\n public readonly lightLabel: string,\n ) {}\n}\n\nexport class ProductPalette {\n /** Every declared product, sorted, each with its color. */\n readonly colors: ProductColor[];\n\n /**\n * @param products the FULL declared product set. Collision resolution depends on the set, so a\n * viewer showing a subset must still pass every declared product, or its colors would\n * disagree with the other viewer's.\n */\n constructor(products: readonly string[]) {\n const sorted = [...new Set(products)].sort();\n const taken = new Set<number>();\n this.colors = sorted.map((product: string): ProductColor => {\n const slot = this.freeSlot(this.preferredSlot(product), taken);\n taken.add(slot);\n return this.paint(product, slot);\n });\n }\n\n /** The slot a product's name hashes to: SHA-256 of the name, first byte, mod the palette size. */\n preferredSlot(product: string): number {\n return createHash('sha256').update(product, 'utf8').digest()[0] % PRODUCT_PALETTE_SLOTS.length;\n }\n\n /** The preferred slot when free, else the next free one (wrapping); with all taken, the preferred one. */\n private freeSlot(preferred: number, taken: ReadonlySet<number>): number {\n for (let step = 0; step < PRODUCT_PALETTE_SLOTS.length; step++) {\n const slot = (preferred + step) % PRODUCT_PALETTE_SLOTS.length;\n if (!taken.has(slot)) return slot;\n }\n return preferred;\n }\n\n private paint(product: string, slot: number): ProductColor {\n const entry = PRODUCT_PALETTE_SLOTS[slot];\n return new ProductColor(product, entry.color, entry.fontColor);\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 // The assignment contract (#1244): the slot SHA-256 of the name prefers, with collisions\n // resolved in sorted-name order over the whole set — the earlier name keeps a contested slot,\n // the later takes the next free one. So adding a product recolors nobody unless it hashes onto\n // a taken slot AND sorts before its holder. Never assign by sorted index: every addition would\n // shift every later product. A product outside the set gets its preferred slot.\n return (\n this.colors.find((color: ProductColor): boolean => color.product === product) ??\n this.paint(product, this.preferredSlot(product))\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\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 * Since #1319 that is the first of those products that is ON; with none ON the edge draws plain.\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 /**\n * The products the edge crosses into, in the target's order: the target's products its source\n * lacks, else (a target narrower than its source) every product of the target. Empty when the\n * target carries every product of the source. The page draws the edge in the first one that is ON.\n */\n crossedInto(from: readonly string[], to: readonly string[]): string[] {\n if (from.length === 0 || to.length === 0) return [];\n if (from.every((product: string): boolean => to.includes(product))) return [];\n const added = to.filter((product: string): boolean => !from.includes(product));\n return added.length > 0 ? added : [...to];\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"]}
@@ -32,18 +32,18 @@ export declare class RenderEdge {
32
32
  readonly dot: string;
33
33
  readonly id: string;
34
34
  /**
35
- * The statement the architecture page's Product color mode draws instead of `dot` (#1204): a
36
- * dependency crossing into another product, dashed in that product's color. Null for every
37
- * other edge, which draws `dot` in every mode.
35
+ * The products a dependency crosses into (#1204, ProductBoundary), in the target's order. The
36
+ * page's Product color mode draws the edge dashed in the first one that is ON (#1319); empty
37
+ * for every other edge, which draws `dot` in every mode.
38
38
  */
39
- readonly productDot: string | null;
39
+ readonly crossesInto: string[];
40
40
  constructor(from: string, to: string, dot: string, id: string,
41
41
  /**
42
- * The statement the architecture page's Product color mode draws instead of `dot` (#1204): a
43
- * dependency crossing into another product, dashed in that product's color. Null for every
44
- * other edge, which draws `dot` in every mode.
42
+ * The products a dependency crosses into (#1204, ProductBoundary), in the target's order. The
43
+ * page's Product color mode draws the edge dashed in the first one that is ON (#1319); empty
44
+ * for every other edge, which draws `dot` in every mode.
45
45
  */
46
- productDot?: string | null);
46
+ crossesInto?: string[]);
47
47
  }
48
48
  /** The immutable drawable snapshot carried by a generated page. */
49
49
  export declare class GraphRenderModel {
@@ -70,11 +70,11 @@ export declare class GraphRenderModel {
70
70
  styledNode(facts: NodeFacts, modes: NodeModeDots): string;
71
71
  completeEndpoints(): void;
72
72
  /**
73
- * One edge. `productAttrs`, when not empty, is the attribute list the Product color mode adds to
74
- * the statement (a later list overrides an earlier one in DOT), e.g. a cross-product dependency's
75
- * dash and color; `fullDot` always carries the plain statement.
73
+ * One edge. `crossesInto`, when not empty, names the products a cross-product dependency crosses
74
+ * into: the page's Product color mode dashes it in the first ON one; `fullDot` always carries the
75
+ * plain statement.
76
76
  */
77
- edge(from: string, to: string, dot: string, productAttrs?: string): string;
77
+ edge(from: string, to: string, dot: string, crossesInto?: string[]): string;
78
78
  }
79
79
  /**
80
80
  * What a runtime page carries for its Goal vs Current color mode (#1239), serialized into the page
@@ -47,19 +47,19 @@ class RenderEdge {
47
47
  to;
48
48
  dot;
49
49
  id;
50
- productDot;
50
+ crossesInto;
51
51
  constructor(from, to, dot, id,
52
52
  /**
53
- * The statement the architecture page's Product color mode draws instead of `dot` (#1204): a
54
- * dependency crossing into another product, dashed in that product's color. Null for every
55
- * other edge, which draws `dot` in every mode.
53
+ * The products a dependency crosses into (#1204, ProductBoundary), in the target's order. The
54
+ * page's Product color mode draws the edge dashed in the first one that is ON (#1319); empty
55
+ * for every other edge, which draws `dot` in every mode.
56
56
  */
57
- productDot = null) {
57
+ crossesInto = []) {
58
58
  this.from = from;
59
59
  this.to = to;
60
60
  this.dot = dot;
61
61
  this.id = id;
62
- this.productDot = productDot;
62
+ this.crossesInto = crossesInto;
63
63
  }
64
64
  }
65
65
  exports.RenderEdge = RenderEdge;
@@ -124,16 +124,14 @@ class GraphRenderModel {
124
124
  }
125
125
  }
126
126
  /**
127
- * One edge. `productAttrs`, when not empty, is the attribute list the Product color mode adds to
128
- * the statement (a later list overrides an earlier one in DOT), e.g. a cross-product dependency's
129
- * dash and color; `fullDot` always carries the plain statement.
127
+ * One edge. `crossesInto`, when not empty, names the products a cross-product dependency crosses
128
+ * into: the page's Product color mode dashes it in the first ON one; `fullDot` always carries the
129
+ * plain statement.
130
130
  */
131
- edge(from, to, dot, productAttrs = '') {
131
+ edge(from, to, dot, crossesInto = []) {
132
132
  const id = `wp-real-edge-${this.edges.length}`;
133
- const base = `${dot.slice(0, -2)} [id="${id}"]`;
134
- const statement = `${base};\n`;
135
- const productDot = productAttrs === '' ? null : `${base} [${productAttrs}];\n`;
136
- this.edges.push(new RenderEdge(from, to, statement, id, productDot));
133
+ const statement = `${dot.slice(0, -2)} [id="${id}"];\n`;
134
+ this.edges.push(new RenderEdge(from, to, statement, id, crossesInto));
137
135
  return statement;
138
136
  }
139
137
  }
@@ -1 +1 @@
1
- {"version":3,"file":"graph-render-model.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/graph-render-model.ts"],"names":[],"mappings":"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","sourcesContent":["import { dotValue } from './dot-syntax';\nimport { LevelBand } from './graph-level-bands';\nimport { LegendFacts, NodeFacts, NodeModeDots } from './graph-color-modes';\n\n/**\n * Renderer-owned records, captured while emitting DOT, never recovered by parsing labels.\n *\n * `dot` is the statement `fullDot` carries. `modes` carries per-mode statements in both viewers; `tags` carries declared service/project\n * metadata. Runtime auxiliary entities leave tags null so facets count services and retain context\n * separately.\n *\n * `products` is set on BOTH graphs (#1179): the products a box belongs to, which each page's product\n * filter matches on. Empty for a box in no product.\n */\nexport class RenderNode {\n constructor(\n public readonly id: string,\n public readonly dot: string,\n public readonly modes: NodeModeDots | null,\n public readonly tags: NodeTags | null,\n public readonly products: string[],\n public readonly queueKeys: string[] = [],\n ) {}\n}\n\n/** What the architecture page's Filter matches a box on: its level, role and framework tags. */\nexport class NodeTags {\n constructor(\n public readonly level: number,\n public readonly role: string,\n public readonly frameworks: string[],\n ) {}\n}\n\nexport class RenderEdge {\n constructor(\n public readonly from: string,\n public readonly to: string,\n public readonly dot: string,\n public readonly id: string,\n /**\n * The statement the architecture page's Product color mode draws instead of `dot` (#1204): a\n * dependency crossing into another product, dashed in that product's color. Null for every\n * other edge, which draws `dot` in every mode.\n */\n public readonly productDot: string | null = null,\n ) {}\n}\n\n/** The immutable drawable snapshot carried by a generated page. */\nexport class GraphRenderModel {\n viewer: 'architecture' | 'runtime' = 'architecture';\n readonly nodes: RenderNode[] = [];\n readonly edges: RenderEdge[] = [];\n bands: LevelBand[] = [];\n fullDot = '';\n header = '';\n footer = '';\n /** The colors and roles in use, so the page's legend lists exactly those (architecture graph). */\n legend = new LegendFacts();\n /** Runtime pages only (#1239): the Goal vs Current overlay, or why it is unavailable. */\n comparison: RuntimeComparisonView | null = null;\n\n node(id: string, dot: string, products: string[] = [], queueKeys: string[] = []): string {\n this.nodes.push(new RenderNode(id, dot, null, null, products, queueKeys));\n return dot;\n }\n\n /**\n * Every product a declared box (a queue, a cron trigger, an external system) inherits from the\n * services its edges touch (#1179): it belongs to whichever product's services it is attached to.\n * Called by the runtime graph once every edge is emitted; RenderNode is immutable, so a node gains\n * its products by being replaced.\n */\n attachProducts(isService: (id: string) => boolean): void {\n const byId = new Map<string, RenderNode>();\n for (const node of this.nodes) byId.set(node.id, node);\n for (let index = 0; index < this.nodes.length; index++) {\n const node = this.nodes[index];\n if (isService(node.id)) continue;\n const inherited = new Set<string>();\n for (const edge of this.edges) {\n const other =\n edge.from === node.id ? edge.to : edge.to === node.id ? edge.from : null;\n if (other === null || !isService(other)) continue;\n for (const product of byId.get(other)?.products ?? []) inherited.add(product);\n }\n if (inherited.size === 0) continue;\n this.nodes[index] = new RenderNode(\n node.id,\n node.dot,\n node.modes,\n node.tags,\n [...inherited].sort(),\n node.queueKeys,\n );\n }\n }\n\n /** A box drawn differently per color mode; `fullDot` carries its RUNTIME statement. */\n styledNode(facts: NodeFacts, modes: NodeModeDots): string {\n this.nodes.push(\n new RenderNode(\n facts.nodeId,\n modes.runtime,\n modes,\n new NodeTags(facts.level, facts.role, facts.frameworks),\n facts.products,\n ),\n );\n return modes.runtime;\n }\n\n completeEndpoints(): void {\n const ids = new Set(this.nodes.map((node) => node.id));\n for (const edge of this.edges) {\n for (const id of [edge.from, edge.to]) {\n if (ids.has(id)) continue;\n ids.add(id);\n this.node(id, ` \"${dotValue(id)}\";\\n`);\n }\n }\n }\n\n /**\n * One edge. `productAttrs`, when not empty, is the attribute list the Product color mode adds to\n * the statement (a later list overrides an earlier one in DOT), e.g. a cross-product dependency's\n * dash and color; `fullDot` always carries the plain statement.\n */\n edge(from: string, to: string, dot: string, productAttrs: string = ''): string {\n const id = `wp-real-edge-${this.edges.length}`;\n const base = `${dot.slice(0, -2)} [id=\"${id}\"]`;\n const statement = `${base};\\n`;\n const productDot = productAttrs === '' ? null : `${base} [${productAttrs}];\\n`;\n this.edges.push(new RenderEdge(from, to, statement, id, productDot));\n return statement;\n }\n}\n\n/**\n * What a runtime page carries for its Goal vs Current color mode (#1239), serialized into the page\n * with the render model so it works opened straight from disk (`file://`, no fetch).\n *\n * Never a silent \"no differences\": an unavailable comparison says why (a missing or unusable input,\n * naming its path and repair), and identical inputs say so explicitly with the normal graph context.\n */\nexport class RuntimeComparisonView {\n constructor(\n public readonly available: boolean,\n /** Why the mode is unavailable; '' when available. */\n public readonly reason: string,\n /** Always \"Current → Goal\": the direction every added/removed word is read in. */\n public readonly direction: string,\n /** One line: counts per status, or the explicit no-differences result. */\n public readonly summary: string,\n /** The overlay drawn in this mode; null when unavailable. */\n public readonly model: GraphRenderModel | null,\n /** Overlay render-node id -> before/after detail lines. */\n public readonly nodeDetails: Record<string, string[]>,\n /** Overlay render-edge id (`wp-real-edge-N`) -> before/after detail lines. */\n public readonly edgeDetails: Record<string, string[]>,\n /** Every added, removed and changed fact, one line each, for the legend. */\n public readonly differences: string[],\n ) {}\n}\n\n/** Shared filter chrome and script-safe JSON for pages opened directly from disk. */\nexport class GraphFilterAssets {\n json<T>(value: T): string {\n return JSON.stringify(value).replace(/</g, '\\\\u003c');\n }\n\n html(): string {\n return (\n '<p id=\"wp-filter-status\" hidden>Filtering: <strong id=\"wp-filter-anchor\"></strong> ' +\n '<button id=\"wp-filter-off\" type=\"button\">Turn off Filter</button></p>' +\n '<p id=\"wp-graph-error\" role=\"alert\" hidden></p>'\n );\n }\n\n styles(): string {\n return (\n '#wp-filter-status { text-align:center; } #wp-graph-error { color:#b71c1c; } ' +\n '#graph .wp-filter-anchor polygon, #graph .wp-filter-anchor path, ' +\n '#graph .wp-filter-anchor ellipse { stroke:#8b3cf0; stroke-width:3; }'\n );\n }\n}\n"]}
1
+ {"version":3,"file":"graph-render-model.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/graph-render-model.ts"],"names":[],"mappings":"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","sourcesContent":["import { dotValue } from './dot-syntax';\nimport { LevelBand } from './graph-level-bands';\nimport { LegendFacts, NodeFacts, NodeModeDots } from './graph-color-modes';\n\n/**\n * Renderer-owned records, captured while emitting DOT, never recovered by parsing labels.\n *\n * `dot` is the statement `fullDot` carries. `modes` carries per-mode statements in both viewers; `tags` carries declared service/project\n * metadata. Runtime auxiliary entities leave tags null so facets count services and retain context\n * separately.\n *\n * `products` is set on BOTH graphs (#1179): the products a box belongs to, which each page's product\n * filter matches on. Empty for a box in no product.\n */\nexport class RenderNode {\n constructor(\n public readonly id: string,\n public readonly dot: string,\n public readonly modes: NodeModeDots | null,\n public readonly tags: NodeTags | null,\n public readonly products: string[],\n public readonly queueKeys: string[] = [],\n ) {}\n}\n\n/** What the architecture page's Filter matches a box on: its level, role and framework tags. */\nexport class NodeTags {\n constructor(\n public readonly level: number,\n public readonly role: string,\n public readonly frameworks: string[],\n ) {}\n}\n\nexport class RenderEdge {\n constructor(\n public readonly from: string,\n public readonly to: string,\n public readonly dot: string,\n public readonly id: string,\n /**\n * The products a dependency crosses into (#1204, ProductBoundary), in the target's order. The\n * page's Product color mode draws the edge dashed in the first one that is ON (#1319); empty\n * for every other edge, which draws `dot` in every mode.\n */\n public readonly crossesInto: string[] = [],\n ) {}\n}\n\n/** The immutable drawable snapshot carried by a generated page. */\nexport class GraphRenderModel {\n viewer: 'architecture' | 'runtime' = 'architecture';\n readonly nodes: RenderNode[] = [];\n readonly edges: RenderEdge[] = [];\n bands: LevelBand[] = [];\n fullDot = '';\n header = '';\n footer = '';\n /** The colors and roles in use, so the page's legend lists exactly those (architecture graph). */\n legend = new LegendFacts();\n /** Runtime pages only (#1239): the Goal vs Current overlay, or why it is unavailable. */\n comparison: RuntimeComparisonView | null = null;\n\n node(id: string, dot: string, products: string[] = [], queueKeys: string[] = []): string {\n this.nodes.push(new RenderNode(id, dot, null, null, products, queueKeys));\n return dot;\n }\n\n /**\n * Every product a declared box (a queue, a cron trigger, an external system) inherits from the\n * services its edges touch (#1179): it belongs to whichever product's services it is attached to.\n * Called by the runtime graph once every edge is emitted; RenderNode is immutable, so a node gains\n * its products by being replaced.\n */\n attachProducts(isService: (id: string) => boolean): void {\n const byId = new Map<string, RenderNode>();\n for (const node of this.nodes) byId.set(node.id, node);\n for (let index = 0; index < this.nodes.length; index++) {\n const node = this.nodes[index];\n if (isService(node.id)) continue;\n const inherited = new Set<string>();\n for (const edge of this.edges) {\n const other =\n edge.from === node.id ? edge.to : edge.to === node.id ? edge.from : null;\n if (other === null || !isService(other)) continue;\n for (const product of byId.get(other)?.products ?? []) inherited.add(product);\n }\n if (inherited.size === 0) continue;\n this.nodes[index] = new RenderNode(\n node.id,\n node.dot,\n node.modes,\n node.tags,\n [...inherited].sort(),\n node.queueKeys,\n );\n }\n }\n\n /** A box drawn differently per color mode; `fullDot` carries its RUNTIME statement. */\n styledNode(facts: NodeFacts, modes: NodeModeDots): string {\n this.nodes.push(\n new RenderNode(\n facts.nodeId,\n modes.runtime,\n modes,\n new NodeTags(facts.level, facts.role, facts.frameworks),\n facts.products,\n ),\n );\n return modes.runtime;\n }\n\n completeEndpoints(): void {\n const ids = new Set(this.nodes.map((node) => node.id));\n for (const edge of this.edges) {\n for (const id of [edge.from, edge.to]) {\n if (ids.has(id)) continue;\n ids.add(id);\n this.node(id, ` \"${dotValue(id)}\";\\n`);\n }\n }\n }\n\n /**\n * One edge. `crossesInto`, when not empty, names the products a cross-product dependency crosses\n * into: the page's Product color mode dashes it in the first ON one; `fullDot` always carries the\n * plain statement.\n */\n edge(from: string, to: string, dot: string, crossesInto: string[] = []): string {\n const id = `wp-real-edge-${this.edges.length}`;\n const statement = `${dot.slice(0, -2)} [id=\"${id}\"];\\n`;\n this.edges.push(new RenderEdge(from, to, statement, id, crossesInto));\n return statement;\n }\n}\n\n/**\n * What a runtime page carries for its Goal vs Current color mode (#1239), serialized into the page\n * with the render model so it works opened straight from disk (`file://`, no fetch).\n *\n * Never a silent \"no differences\": an unavailable comparison says why (a missing or unusable input,\n * naming its path and repair), and identical inputs say so explicitly with the normal graph context.\n */\nexport class RuntimeComparisonView {\n constructor(\n public readonly available: boolean,\n /** Why the mode is unavailable; '' when available. */\n public readonly reason: string,\n /** Always \"Current → Goal\": the direction every added/removed word is read in. */\n public readonly direction: string,\n /** One line: counts per status, or the explicit no-differences result. */\n public readonly summary: string,\n /** The overlay drawn in this mode; null when unavailable. */\n public readonly model: GraphRenderModel | null,\n /** Overlay render-node id -> before/after detail lines. */\n public readonly nodeDetails: Record<string, string[]>,\n /** Overlay render-edge id (`wp-real-edge-N`) -> before/after detail lines. */\n public readonly edgeDetails: Record<string, string[]>,\n /** Every added, removed and changed fact, one line each, for the legend. */\n public readonly differences: string[],\n ) {}\n}\n\n/** Shared filter chrome and script-safe JSON for pages opened directly from disk. */\nexport class GraphFilterAssets {\n json<T>(value: T): string {\n return JSON.stringify(value).replace(/</g, '\\\\u003c');\n }\n\n html(): string {\n return (\n '<p id=\"wp-filter-status\" hidden>Filtering: <strong id=\"wp-filter-anchor\"></strong> ' +\n '<button id=\"wp-filter-off\" type=\"button\">Turn off Filter</button></p>' +\n '<p id=\"wp-graph-error\" role=\"alert\" hidden></p>'\n );\n }\n\n styles(): string {\n return (\n '#wp-filter-status { text-align:center; } #wp-graph-error { color:#b71c1c; } ' +\n '#graph .wp-filter-anchor polygon, #graph .wp-filter-anchor path, ' +\n '#graph .wp-filter-anchor ellipse { stroke:#8b3cf0; stroke-width:3; }'\n );\n }\n}\n"]}
@@ -127,6 +127,13 @@ declare class GraphPage extends WpFilterPage {
127
127
  private readonly drawer;
128
128
  private readonly why;
129
129
  private readonly edges;
130
+ /**
131
+ * The products switched ON in the Product legend (#1319): the first two in legend order at EVERY
132
+ * load. Never persisted — no storage, no hash — so the page stays stateless.
133
+ */
134
+ private readonly toggles;
135
+ private readonly productsOn;
136
+ private painter;
130
137
  /** In Impact mode a box's menu leads with WHY it is in the build, and its edge changes (#1264). */
131
138
  impactItems(name: string, node: SVGGElement): WpNodeMenuItem[];
132
139
  protected captureBinding(_svg: SVGSVGElement | null): () => void;
@@ -136,7 +143,10 @@ declare class GraphPage extends WpFilterPage {
136
143
  /** Goal vs Current draws its overlay topology; every other mode draws the page's own graph. */
137
144
  private syncModel;
138
145
  protected nodeDot(node: RenderNodeJson): string;
139
- /** Product mode dashes a dependency that crosses into another product (#1204); every other mode draws it plain. */
146
+ /**
147
+ * Product mode dashes a dependency that crosses into another product (#1204) while that product is
148
+ * ON (#1319); every other mode draws it plain.
149
+ */
140
150
  protected edgeDot(edge: RenderEdgeJson): string;
141
151
  protected usesFullDot(): boolean;
142
152
  protected narrow(retained: Set<string>): Set<string>;
@@ -155,6 +165,8 @@ declare class GraphPage extends WpFilterPage {
155
165
  setLock(name: string | null): void;
156
166
  clearSelection(): void;
157
167
  setMode(mode: string): void;
168
+ /** A product legend toggle (#1319): repaint at once from the new ON set. */
169
+ setProductOn(product: string, on: boolean): void;
158
170
  /** Switch the active Impact scan: Impact recolours and an active change-scope filter re-applies. */
159
171
  setImpactKind(kind: string): void;
160
172
  /** The drawer's "Hide unconnected": filter to the locked box's chain, or turn the filter off. */
@@ -673,6 +673,13 @@ class GraphPage extends WpFilterPage {
673
673
  drawer = new GraphDrawer(this);
674
674
  why = new ImpactWhyPanel();
675
675
  edges = new WpEdgeOverlay(this.modes);
676
+ /**
677
+ * The products switched ON in the Product legend (#1319): the first two in legend order at EVERY
678
+ * load. Never persisted — no storage, no hash — so the page stays stateless.
679
+ */
680
+ toggles = new WpProductToggles();
681
+ productsOn = new WpProductSelection().initialOn(this.toggles.products());
682
+ painter = this.toggles.painter(this.base.legend.palette.colors, this.productsOn);
676
683
  /** In Impact mode a box's menu leads with WHY it is in the build, and its edge changes (#1264). */
677
684
  impactItems(name, node) {
678
685
  if (this.modes.mode !== 'impact')
@@ -698,6 +705,7 @@ class GraphPage extends WpFilterPage {
698
705
  wireControls() {
699
706
  this.syncModel();
700
707
  this.drawer.wire();
708
+ this.toggles.wire((product, on) => this.setProductOn(product, on));
701
709
  this.syncControls();
702
710
  }
703
711
  /** '' when this page can show Goal vs Current, else why not. */
@@ -729,13 +737,16 @@ class GraphPage extends WpFilterPage {
729
737
  if (this.modes.mode === 'impact')
730
738
  return modes[this.modes.status(node.id)];
731
739
  if (this.modes.mode === 'product')
732
- return modes.product;
740
+ return this.painter.nodeDot(modes.product, node.products);
733
741
  return modes.runtime;
734
742
  }
735
- /** Product mode dashes a dependency that crosses into another product (#1204); every other mode draws it plain. */
743
+ /**
744
+ * Product mode dashes a dependency that crosses into another product (#1204) while that product is
745
+ * ON (#1319); every other mode draws it plain.
746
+ */
736
747
  edgeDot(edge) {
737
- if (this.modes.mode === 'product' && edge.productDot !== null)
738
- return edge.productDot;
748
+ if (this.modes.mode === 'product')
749
+ return this.painter.edgeDot(edge);
739
750
  if (this.modes.mode === 'impact')
740
751
  return this.edges.edgeDot(edge) ?? edge.dot;
741
752
  return edge.dot;
@@ -803,6 +814,17 @@ class GraphPage extends WpFilterPage {
803
814
  this.syncControls();
804
815
  this.redraw();
805
816
  }
817
+ /** A product legend toggle (#1319): repaint at once from the new ON set. */
818
+ setProductOn(product, on) {
819
+ if (on)
820
+ this.productsOn.add(product);
821
+ else
822
+ this.productsOn.delete(product);
823
+ this.painter = this.toggles.painter(this.base.legend.palette.colors, this.productsOn);
824
+ this.syncControls();
825
+ if (this.modes.mode === 'product')
826
+ this.redraw();
827
+ }
806
828
  /** Switch the active Impact scan: Impact recolours and an active change-scope filter re-applies. */
807
829
  setImpactKind(kind) {
808
830
  if (kind === this.modes.kind || !this.modes.setKind(kind))
@@ -858,6 +880,7 @@ class GraphPage extends WpFilterPage {
858
880
  syncControls() {
859
881
  document.body.dataset['wpMode'] = this.modes.mode;
860
882
  this.drawer.sync(this.modes, this.locked, this.anchor !== null, this.applied);
883
+ this.toggles.sync(this.productsOn);
861
884
  const count = this.model.nodes.filter(node => this.retained.has(node.id) && this.projects.isService(node)).length;
862
885
  const empty = document.getElementById('wp-empty');
863
886
  if (empty !== null)