@webpieces/nx-webpieces-rules 0.4.918 → 0.4.920

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.
@@ -1 +1 @@
1
- {"version":3,"file":"graph-mode-state.client.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/graph-mode-state.client.ts"],"names":[],"mappings":"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","sourcesContent":["class WpModeState {\n /** `goal` (Goal vs Current, #1239) exists on runtime pages only; the shell renders it nowhere else. */\n static readonly MODES = ['runtime', 'architecture', 'impact', 'product', 'goal'];\n static readonly NAMES = ['Runtime', 'Architecture', 'Impact', 'Product', 'Goal vs Current'];\n static readonly SUBTITLES = [\n 'where the code can run',\n 'servers · clients · APIs',\n 'what changed',\n 'which products it belongs to',\n 'Current → Goal proposal',\n ];\n readonly scans: ImpactJson[] = window.__WP_IMPACT__?.scans ?? [];\n kind = '';\n mode = 'runtime';\n\n /**\n * @param hasProducts some box belongs to a product, so the Product mode has something to show\n * @param goalUnavailable '' when Goal vs Current can be shown, else why not (#1239)\n */\n constructor(\n private readonly hasProducts: boolean,\n private readonly viewer: string,\n private readonly goalUnavailable: string,\n ) {\n this.kind = this.initialKind();\n this.mode = this.initial();\n }\n\n private storageKey(kind: string): string {\n return `wp-${this.viewer}-graph-${kind}`;\n }\n\n /** '' when Product can be shown, else why not. */\n productReason(): string {\n return this.hasProducts\n ? ''\n : 'No products here: tag each project product:<name> in its project.json, then regenerate.';\n }\n\n /** '' when Goal vs Current can be shown, else why not (a missing or unusable input, never \"no differences\"). */\n goalReason(): string {\n return this.goalUnavailable;\n }\n\n /** The active scan; null when the page has no Impact data. */\n get impact(): ImpactJson | null {\n return this.scans.find((scan: ImpactJson): boolean => scan.kind === this.kind) ?? null;\n }\n\n /** '' when Impact can be shown, else the one line the drawer and the Filter popover say instead. */\n impactReason(): string {\n const impact = this.impact;\n if (\n impact === null &&\n this.viewer === 'runtime' &&\n document.body.dataset['wpProjectFacts'] === 'false'\n )\n return 'No saved project facts for Impact. Run pnpm nx run architecture:generate, then pnpm arch:visualize-runtime.';\n if (impact === null)\n return `No impact data here. Run pnpm arch:visualize${this.viewer === 'runtime' ? '-runtime' : ''} to compute it — no full regenerate needed.`;\n return impact.available ? '' : `Impact unavailable (${impact.label}): ${impact.reason}.`;\n }\n\n /** Switch the active scan; false (and nothing changes) for a missing or unavailable one. */\n setKind(kind: string): boolean {\n if (!this.kindAvailable(kind)) return false;\n this.kind = kind;\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- storage is a per-viewer convenience; a blocked store must not break the page\n try {\n localStorage.setItem(this.storageKey('impact-kind'), kind);\n // webpieces-disable no-any-unknown -- browsers may throw any value from blocked storage\n } catch (err: unknown) {\n //const error = toError(err);\n void err;\n }\n return true;\n }\n\n kindAvailable(kind: string): boolean {\n return this.scans.some((scan: ImpactJson): boolean => scan.kind === kind && scan.available);\n }\n\n private initialKind(): string {\n const stored = this.read(this.storageKey('impact-kind'));\n if (stored !== null && this.kindAvailable(stored)) return stored;\n const preferred = window.__WP_IMPACT__?.defaultKind ?? '';\n if (this.scans.some((scan: ImpactJson): boolean => scan.kind === preferred))\n return preferred;\n return this.scans[0]?.kind ?? '';\n }\n\n available(mode: string): boolean {\n if (!WpModeState.MODES.includes(mode)) return false;\n if (mode === 'impact') return this.impactReason() === '';\n if (mode === 'product') return this.productReason() === '';\n if (mode === 'goal') return this.goalReason() === '';\n return true;\n }\n\n name(mode: string): string {\n return WpModeState.NAMES[WpModeState.MODES.indexOf(mode)] ?? mode;\n }\n\n subtitle(mode: string): string {\n return WpModeState.SUBTITLES[WpModeState.MODES.indexOf(mode)] ?? '';\n }\n\n /** False (and nothing changes) for an unknown or unavailable mode. */\n set(mode: string): boolean {\n if (!this.available(mode)) return false;\n this.mode = mode;\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- storage is a per-viewer convenience; a blocked store must not break the page\n try {\n localStorage.setItem(this.storageKey('mode'), mode);\n if (location.hash !== `#${mode}`) history.replaceState(null, '', `#${mode}`);\n // webpieces-disable no-any-unknown -- browsers may throw any value from blocked storage\n } catch (err: unknown) {\n //const error = toError(err);\n void err;\n }\n return true;\n }\n\n /** Which impact shade a box gets, from the ACTIVE scan. */\n status(id: string): 'touched' | 'affected' | 'buildInput' | 'untouched' {\n const impact = this.impact;\n if (impact?.touched.includes(id)) return 'touched';\n if (impact?.affected.includes(id)) return 'affected';\n if (impact?.buildInputs.includes(id)) return 'buildInput';\n return 'untouched';\n }\n\n private initial(): string {\n const hashed = location.hash.slice(1);\n if (this.available(hashed)) return hashed;\n const stored = this.read(this.storageKey('mode'));\n if (stored !== null && this.available(stored)) return stored;\n return 'runtime';\n }\n\n private read(key: string): string | null {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- storage is a per-viewer convenience; a blocked store reads as \"nothing stored\"\n try {\n return localStorage.getItem(key);\n // webpieces-disable no-any-unknown -- browsers may throw any value from blocked storage\n } catch (err: unknown) {\n //const error = toError(err);\n void err;\n return null;\n }\n }\n}\n"]}
1
+ {"version":3,"file":"graph-mode-state.client.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/graph-mode-state.client.ts"],"names":[],"mappings":"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","sourcesContent":["class WpModeState {\n /** `goal` (Goal vs Current, #1239) exists on runtime pages only; the shell renders it nowhere else. */\n static readonly MODES = ['runtime', 'architecture', 'impact', 'product', 'goal'];\n static readonly NAMES = ['Runtime', 'Architecture', 'Impact', 'Product', 'Goal vs Current'];\n static readonly SUBTITLES = [\n 'where the code can run',\n 'servers · clients · APIs',\n 'what changed',\n 'which products it belongs to',\n 'Current → Goal proposal',\n ];\n readonly scans: ImpactJson[] = window.__WP_IMPACT__?.scans ?? [];\n kind = '';\n mode = 'runtime';\n\n /**\n * @param hasProducts some box belongs to a product, so the Product mode has something to show\n * @param goalUnavailable '' when Goal vs Current can be shown, else why not (#1239)\n */\n constructor(\n private readonly hasProducts: boolean,\n private readonly viewer: string,\n private readonly goalUnavailable: string,\n ) {\n this.kind = this.initialKind();\n this.mode = this.initial();\n }\n\n private storageKey(kind: string): string {\n return `wp-${this.viewer}-graph-${kind}`;\n }\n\n /** '' when Product can be shown, else why not. */\n productReason(): string {\n return this.hasProducts\n ? ''\n : 'No products here: tag each project product:<name> in its project.json, then regenerate.';\n }\n\n /** '' when Goal vs Current can be shown, else why not (a missing or unusable input, never \"no differences\"). */\n goalReason(): string {\n return this.goalUnavailable;\n }\n\n /** The active scan; null when the page has no Impact data. */\n get impact(): ImpactJson | null {\n return this.scans.find((scan: ImpactJson): boolean => scan.kind === this.kind) ?? null;\n }\n\n /** '' when Impact can be shown, else the one line the drawer and the Filter popover say instead. */\n impactReason(): string {\n const impact = this.impact;\n if (\n impact === null &&\n this.viewer === 'runtime' &&\n document.body.dataset['wpProjectFacts'] === 'false'\n )\n return 'No saved project facts for Impact. Run pnpm nx run architecture:generate, then pnpm arch:visualize-runtime.';\n if (impact === null)\n return `No impact data here. Run pnpm arch:visualize${this.viewer === 'runtime' ? '-runtime' : ''} to compute it — no full regenerate needed.`;\n return impact.available ? '' : `Impact unavailable (${impact.label}): ${impact.reason}.`;\n }\n\n /** Switch the active scan; false (and nothing changes) for a missing or unavailable one. */\n setKind(kind: string): boolean {\n if (!this.kindAvailable(kind)) return false;\n this.kind = kind;\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- storage is a per-viewer convenience; a blocked store must not break the page\n try {\n localStorage.setItem(this.storageKey('impact-kind'), kind);\n // webpieces-disable no-any-unknown -- browsers may throw any value from blocked storage\n } catch (err: unknown) {\n //const error = toError(err);\n void err;\n }\n return true;\n }\n\n kindAvailable(kind: string): boolean {\n return this.scans.some((scan: ImpactJson): boolean => scan.kind === kind && scan.available);\n }\n\n private initialKind(): string {\n const stored = this.read(this.storageKey('impact-kind'));\n if (stored !== null && this.kindAvailable(stored)) return stored;\n const preferred = window.__WP_IMPACT__?.defaultKind ?? '';\n if (this.scans.some((scan: ImpactJson): boolean => scan.kind === preferred))\n return preferred;\n return this.scans[0]?.kind ?? '';\n }\n\n available(mode: string): boolean {\n if (!WpModeState.MODES.includes(mode)) return false;\n if (mode === 'impact') return this.impactReason() === '';\n if (mode === 'product') return this.productReason() === '';\n if (mode === 'goal') return this.goalReason() === '';\n return true;\n }\n\n name(mode: string): string {\n return WpModeState.NAMES[WpModeState.MODES.indexOf(mode)] ?? mode;\n }\n\n subtitle(mode: string): string {\n return WpModeState.SUBTITLES[WpModeState.MODES.indexOf(mode)] ?? '';\n }\n\n /** False (and nothing changes) for an unknown or unavailable mode. */\n set(mode: string): boolean {\n if (!this.available(mode)) return false;\n this.mode = mode;\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- storage is a per-viewer convenience; a blocked store must not break the page\n try {\n localStorage.setItem(this.storageKey('mode'), mode);\n if (location.hash !== `#${mode}`) history.replaceState(null, '', `#${mode}`);\n // webpieces-disable no-any-unknown -- browsers may throw any value from blocked storage\n } catch (err: unknown) {\n //const error = toError(err);\n void err;\n }\n return true;\n }\n\n /** Which impact shade a box gets, from the ACTIVE scan: WHY nx included it (#1264). */\n status(id: string): 'touched' | 'dependent' | 'implicit' | 'rootFile' | 'unexplained' | 'buildInput' | 'untouched' {\n const impact = this.impact;\n if (impact?.touched.includes(id)) return 'touched';\n if (impact?.dependents.includes(id)) return 'dependent';\n if (impact?.implicit.includes(id)) return 'implicit';\n if (impact?.rootFile.includes(id)) return 'rootFile';\n if (impact?.unexplained.includes(id)) return 'unexplained';\n if (impact?.buildInputs.includes(id)) return 'buildInput';\n return 'untouched';\n }\n\n /** Every affected node id of the active scan, whatever its reason. */\n affected(): string[] {\n const impact = this.impact;\n if (impact === null) return [];\n return [...impact.touched, ...impact.dependents, ...impact.implicit, ...impact.rootFile, ...impact.unexplained];\n }\n\n /** Why `id` is in the active scan's build; null when it is not affected. */\n reason(id: string): AffectedReasonJson | null {\n return this.impact?.reasons.find((reason: AffectedReasonJson): boolean => reason.project === id) ?? null;\n }\n\n private initial(): string {\n const hashed = location.hash.slice(1);\n if (this.available(hashed)) return hashed;\n const stored = this.read(this.storageKey('mode'));\n if (stored !== null && this.available(stored)) return stored;\n return 'runtime';\n }\n\n private read(key: string): string | null {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- storage is a per-viewer convenience; a blocked store reads as \"nothing stored\"\n try {\n return localStorage.getItem(key);\n // webpieces-disable no-any-unknown -- browsers may throw any value from blocked storage\n } catch (err: unknown) {\n //const error = toError(err);\n void err;\n return null;\n }\n }\n}\n\n/**\n * What the \"why\" panel says about one affected project (#1264): never a reason the graph cannot show.\n * Pure text, so a spec reads it without a browser; GraphPage puts it in a panel.\n */\nclass WpImpactWhy {\n static readonly TITLES: Record<string, string> = {\n touched: 'Changed',\n dependent: 'Dependent',\n implicit: 'Implicitly touched',\n rootFile: 'Root-file touched',\n unexplained: 'Unexplained',\n };\n\n /**\n * `8 changed · 3 dependents · 22 implicitly touched (runtime-wiring-check inputs) · 47 dependencies`\n * — the same split ImpactCounts (graph-impact.ts) prints on the console.\n */\n counts(impact: ImpactJson): string {\n const parts = [`${impact.touched.length} changed`, `${impact.dependents.length} dependents`];\n if (impact.implicit.length > 0) {\n const targets = [\n ...new Set(\n impact.reasons\n .filter((reason: AffectedReasonJson): boolean => reason.kind === 'implicit' && reason.target !== '')\n .map((reason: AffectedReasonJson): string => reason.target),\n ),\n ].sort();\n parts.push(`${impact.implicit.length} implicitly touched${targets.length === 0 ? '' : ` (${targets.join(', ')} inputs)`}`);\n }\n if (impact.rootFile.length > 0) parts.push(`${impact.rootFile.length} root-file touched`);\n if (impact.unexplained.length > 0) parts.push(`${impact.unexplained.length} unexplained`);\n parts.push(`${impact.buildInputs.length} dependencies`);\n return parts.join(' · ');\n }\n\n title(reason: AffectedReasonJson): string {\n return `${WpImpactWhy.TITLES[reason.kind] ?? reason.kind}: ${reason.project}`;\n }\n\n lines(reason: AffectedReasonJson): string[] {\n if (reason.kind === 'touched') {\n const owns = reason.files.length === 1 ? 'this changed file' : `these ${reason.files.length} changed files`;\n return [`It owns ${owns}:`, ...reason.files];\n }\n if (reason.kind === 'dependent')\n return [\n 'It uses a changed or touched project through these dependency edges, so its tests and build re-run:',\n reason.path.join(' -> '),\n ];\n if (reason.kind === 'implicit')\n return [\n reason.target === '' ? reason.detail : `${reason.target} input ${reason.input} matched ${reason.files.join(', ')}`,\n 'nx marks every project whose target input glob matches a changed file. It uses no changed project.',\n ];\n if (reason.kind === 'rootFile')\n return [\n reason.detail,\n ...(reason.path.length > 0 ? [reason.path.join(' -> ')] : []),\n 'nx marked it for a workspace root file; it uses no changed project.',\n ];\n return [reason.detail];\n }\n}\n\n/**\n * The \"why is this in the build?\" panel (#1264): opened from an affected box's menu in Impact mode.\n * Text nodes only, so no project name is parsed as HTML.\n */\nclass ImpactWhyPanel {\n private readonly panel = document.createElement('div');\n private readonly text = new WpImpactWhy();\n private anchor: SVGGElement | null = null;\n\n constructor() {\n this.panel.className = 'wp-graph-details wp-impact-why';\n this.panel.hidden = true;\n this.panel.setAttribute('role', 'dialog');\n this.panel.setAttribute('aria-label', 'Why this project is in the build');\n document.body.append(this.panel);\n document.addEventListener('keydown', (ev: KeyboardEvent) => {\n if (ev.key !== 'Escape' || this.panel.hidden) return;\n ev.stopPropagation();\n this.close();\n }, true);\n document.addEventListener('click', (ev: MouseEvent) => {\n const target = ev.target;\n if (target instanceof Node && !this.panel.contains(target) && !this.anchor?.contains(target)) this.close();\n });\n }\n\n open(node: SVGGElement, reason: AffectedReasonJson): void {\n this.anchor = node;\n const heading = document.createElement('strong');\n heading.textContent = this.text.title(reason);\n const list = document.createElement('ul');\n for (const line of this.text.lines(reason)) {\n const item = document.createElement('li');\n item.textContent = line;\n list.append(item);\n }\n const close = document.createElement('button');\n close.type = 'button';\n close.textContent = 'Close';\n close.addEventListener('click', () => this.close());\n this.panel.replaceChildren(heading, list, close);\n this.panel.hidden = false;\n const box = node.getBoundingClientRect();\n this.panel.style.left = `${Math.max(12, Math.min(box.left, innerWidth - this.panel.offsetWidth - 12))}px`;\n this.panel.style.top = `${Math.max(12, Math.min(box.bottom + 6, innerHeight - this.panel.offsetHeight - 12))}px`;\n close.focus({ preventScroll: true });\n }\n\n close(): void {\n if (this.panel.hidden) return;\n this.panel.hidden = true;\n const anchor = this.anchor;\n this.anchor = null;\n if (anchor?.isConnected) anchor.focus({ preventScroll: true });\n }\n}\n"]}
@@ -13,9 +13,9 @@
13
13
  import { ProductColor } from './graph-products';
14
14
  /**
15
15
  * The Filter popover's "Changes on this branch" choices. The page computes each set from the Impact
16
- * sidecar (graph-impact.ts): CHANGED = touched; DEPENDENTS = touched + affected (nx affected: their
16
+ * sidecar (graph-impact.ts): CHANGED = touched; DEPENDENTS = every project nx affected, whatever its reason (their
17
17
  * tests re-run); DEPENDENCIES = touched + every transitive dependency of it; BUILD = touched +
18
- * affected + every build input — the dependencies of the DEPENDENTS too, which is why it is more than
18
+ * every affected project + every build input — the dependencies of the DEPENDENTS too, which is why it is more than
19
19
  * the union of the two rows above it.
20
20
  *
21
21
  * "Dependents" are what USE a project and "dependencies" what it uses (#1179). "Dependee" would mean
@@ -16,9 +16,9 @@ exports.GraphPageShell = exports.ShellParts = exports.SCOPE_OPTIONS = exports.Sc
16
16
  const graph_color_modes_1 = require("./graph-color-modes");
17
17
  /**
18
18
  * The Filter popover's "Changes on this branch" choices. The page computes each set from the Impact
19
- * sidecar (graph-impact.ts): CHANGED = touched; DEPENDENTS = touched + affected (nx affected: their
19
+ * sidecar (graph-impact.ts): CHANGED = touched; DEPENDENTS = every project nx affected, whatever its reason (their
20
20
  * tests re-run); DEPENDENCIES = touched + every transitive dependency of it; BUILD = touched +
21
- * affected + every build input — the dependencies of the DEPENDENTS too, which is why it is more than
21
+ * every affected project + every build input — the dependencies of the DEPENDENTS too, which is why it is more than
22
22
  * the union of the two rows above it.
23
23
  *
24
24
  * "Dependents" are what USE a project and "dependencies" what it uses (#1179). "Dependee" would mean
@@ -51,7 +51,7 @@ exports.ScopeOption = ScopeOption;
51
51
  exports.SCOPE_OPTIONS = [
52
52
  new ScopeOption(ChangeScope.EVERYTHING, 'All', 'every project'),
53
53
  new ScopeOption(ChangeScope.CHANGED, 'Changed', 'owns a changed file'),
54
- new ScopeOption(ChangeScope.DEPENDENTS, 'Changed + dependents', '+ everything that uses them (their tests re-run)'),
54
+ new ScopeOption(ChangeScope.DEPENDENTS, 'Changed + dependents', '+ everything else nx affected: dependents, implicitly and root-file touched (their tests re-run)'),
55
55
  new ScopeOption(ChangeScope.DEPENDENCIES, 'Changed + dependencies', '+ everything they use'),
56
56
  new ScopeOption(ChangeScope.BUILD, 'Changed + dependents + dependencies', 'everything CI builds for this branch'),
57
57
  ];
@@ -242,7 +242,7 @@ class GraphPageShell {
242
242
  <p>💡 <strong>Click any box</strong> for its menu: <strong>View Design</strong> (only where the project has a generated <strong>design.html</strong>, i.e. what the AI sees inside it), <strong>Lock/Unlock</strong> (the same lock as the Focus field), <strong>Filter Unconnected</strong> (removes unrelated boxes, keeping each L-number row).</p>
243
243
  <p><strong>Filter</strong> narrows the graph by what changed on this branch or in the last commit, by runtime, by role and by product; the groups combine. A product keeps its servers, clients and apps plus everything they depend on, so <strong>Product</strong> + <strong>Changed + dependents + dependencies</strong> is exactly what this branch builds that can affect that product. Every box keeps its own L-row, and a level left empty shows as a thin labeled band.</p>
244
244
  <p>🔦 <strong>Hover any box</strong> to trace its <em>entire</em> dependency chain — every ancestor above it and every dependency below it, with the lines between — while the rest of the graph dims.</p>
245
- <p><strong>Runtime</strong>: a box may depend on a box carrying every color it has. <strong>Architecture</strong>: what each box is in the system. <strong>Impact</strong>: what changed on this branch or in the last commit (amber) and its dependents (amber outline); with both available, a toggle picks which. <strong>Product</strong>: which products each box belongs to — one color per product, stripes for several, grey for every product, a dashed white box for none.</p>
245
+ <p><strong>Runtime</strong>: a box may depend on a box carrying every color it has. <strong>Architecture</strong>: what each box is in the system. <strong>Impact</strong>: what changed on this branch or in the last commit (amber), its dependents (amber outline), and what nx marked for another reason: an input glob (violet hatch) or a root file (teal). Click an affected box for why; with both available, a toggle picks which. <strong>Product</strong>: which products each box belongs to — one color per product, stripes for several, grey for every product, a dashed white box for none.</p>
246
246
  <p><kbd>/</kbd> focus the lock search · <kbd>1</kbd> <kbd>2</kbd> <kbd>3</kbd> <kbd>4</kbd> switch mode · <kbd>Esc</kbd> unlock</p>
247
247
  </div>`;
248
248
  }
@@ -1 +1 @@
1
- {"version":3,"file":"graph-page-shell.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/graph-page-shell.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The architecture page's shell (#1155): a collapsible dark drawer on the left holding every control,\n * and the graph filling the rest of the viewport.\n *\n * Before this, two hint paragraphs, a ~650px three-column legend, a lock dropdown and a zoom bar all\n * sat ABOVE the graph, so a laptop's first screen showed no graph at all. Now the hints are a \"?\"\n * popover, the legend lives in the drawer (and can pop out over the graph), zoom floats bottom-right\n * (graph-navigation.ts) and the responsibilities open in a right-hand panel.\n *\n * Markup only. Behaviour is graph-visualizer.client.ts; styling is graph-page-styles.ts. No element\n * carries a `style=` attribute — every look is a class.\n */\n\nimport { FRAMEWORK_ORDER, GRAPH_MODES, GraphMode, ModeInfo, ROLE_STYLES, RoleStyle, RUNTIME_GRAPH_MODES } from './graph-color-modes';\nimport { ProductColor } from './graph-products';\n\n/**\n * The Filter popover's \"Changes on this branch\" choices. The page computes each set from the Impact\n * sidecar (graph-impact.ts): CHANGED = touched; DEPENDENTS = touched + affected (nx affected: their\n * tests re-run); DEPENDENCIES = touched + every transitive dependency of it; BUILD = touched +\n * affected + every build input — the dependencies of the DEPENDENTS too, which is why it is more than\n * the union of the two rows above it.\n *\n * \"Dependents\" are what USE a project and \"dependencies\" what it uses (#1179). \"Dependee\" would mean\n * the thing depended ON — the opposite of the dependents row — so it is never used.\n */\nexport enum ChangeScope {\n EVERYTHING = 'everything',\n CHANGED = 'changed',\n DEPENDENTS = 'dependents',\n DEPENDENCIES = 'dependencies',\n BUILD = 'build',\n}\n\n/**\n * One change scope: its name and the grey second line under it, like the Color-by menu. These are the\n * ONE copy of the labels — the page script reads each name back from its `data-wp-scope-label` (the\n * top-bar pill), so the two cannot drift apart.\n */\nexport class ScopeOption {\n constructor(\n public readonly scope: ChangeScope,\n public readonly label: string,\n public readonly subtitle: string,\n ) {}\n}\n\nexport const SCOPE_OPTIONS: readonly ScopeOption[] = [\n new ScopeOption(ChangeScope.EVERYTHING, 'All', 'every project'),\n new ScopeOption(ChangeScope.CHANGED, 'Changed', 'owns a changed file'),\n new ScopeOption(ChangeScope.DEPENDENTS, 'Changed + dependents', '+ everything that uses them (their tests re-run)'),\n new ScopeOption(ChangeScope.DEPENDENCIES, 'Changed + dependencies', '+ everything they use'),\n new ScopeOption(ChangeScope.BUILD, 'Changed + dependents + dependencies', 'everything CI builds for this branch'),\n];\n\n/** Everything the shell frames, already rendered by its owner. */\nexport class ShellParts {\n constructor(\n public readonly title: string,\n public readonly lockControl: string,\n public readonly legend: string,\n public readonly edgeKey: string,\n public readonly filterStatus: string,\n public readonly snapshot: string,\n public readonly responsibilities: string,\n /**\n * One chip per product (#1179), sorted, each colored from the page's ONE palette (#1244);\n * empty hides the Filter's Product group.\n */\n public readonly products: ProductColor[],\n public readonly runtime: boolean = false,\n public readonly frameworks: readonly string[] = FRAMEWORK_ORDER,\n public readonly roles: readonly string[] = ROLE_STYLES.map((style: RoleStyle): string => style.role),\n public readonly emptyState: string = '',\n ) {}\n}\n\nexport class GraphPageShell {\n body(parts: ShellParts): string {\n return `<div class=\"wp-shell\" id=\"wp-shell\">\n ${this.drawer(parts)}\n <main class=\"wp-main\">\n ${this.topbar(parts)}\n <div id=\"graph\"></div>\n ${this.overlays(parts)}\n </main>\n</div>`;\n }\n\n private drawer(parts: ShellParts): string {\n return `<aside class=\"wp-side\" id=\"wp-side\" aria-label=\"Graph controls\">\n <div class=\"wp-brand\">${this.brandIcon()}<span>${parts.title}</span></div>\n <div class=\"wp-group\" role=\"group\" aria-label=\"Color mode\">\n <div class=\"wp-label\">Color by</div>\n ${this.modePulldown(parts.runtime ? RUNTIME_GRAPH_MODES : GRAPH_MODES)}\n <p class=\"wp-mode-reason\" id=\"wp-impact-reason\" hidden></p>\n ${this.impactKinds('drawer')}\n </div>\n <div class=\"wp-group\">\n <button type=\"button\" class=\"wp-filter-btn\" id=\"wp-filter-open\" aria-controls=\"wp-filter-pop\" aria-expanded=\"false\"><span>Filter</span><span class=\"wp-badge\" id=\"wp-filter-badge\" hidden>0</span></button>\n </div>\n <div class=\"wp-group\">\n <label class=\"wp-label\" for=\"wp-lock\">Focus</label>\n ${parts.lockControl}\n <button type=\"button\" class=\"wp-toggle\" id=\"wp-lock-toggle\" aria-pressed=\"false\">Lock <span class=\"wp-sw\" aria-hidden=\"true\"></span></button>\n <button type=\"button\" class=\"wp-toggle\" id=\"wp-filter-toggle\" aria-pressed=\"false\">Hide unconnected <span class=\"wp-sw\" aria-hidden=\"true\"></span></button>\n </div>\n <div class=\"wp-group\">\n <div class=\"wp-label wp-label-row\">Legend <button type=\"button\" class=\"wp-icon wp-icon-mini wp-icon-dark\" id=\"wp-legend-pop\" title=\"Pop the legend out over the graph\" aria-label=\"Pop the legend out over the graph\">⇱</button></div>\n ${parts.legend}\n </div>\n <div class=\"wp-group\">\n <div class=\"wp-label\">Edges</div>\n ${parts.edgeKey}\n </div>\n <div class=\"wp-foot\">\n <button type=\"button\" class=\"wp-link\" id=\"wp-resp-open\" aria-expanded=\"false\">Responsibilities</button>\n <button type=\"button\" class=\"wp-link\" id=\"wp-snapshot-open\" aria-expanded=\"false\">Saved snapshot</button>\n </div>\n </aside>`;\n }\n\n /**\n * \"Color by\": ONE pill-shaped trigger showing the current mode (name + subtitle) that opens a\n * menu of the four. Keys 1/2/3/4 and the #runtime / #architecture / #impact / #product links still\n * switch.\n */\n private modePulldown(modes: readonly ModeInfo[]): string {\n const first = modes[0];\n return `<div class=\"wp-pulldown\">\n <button type=\"button\" class=\"wp-pill-trigger\" id=\"wp-mode-trigger\" aria-haspopup=\"menu\" aria-controls=\"wp-mode-menu\" aria-expanded=\"false\">\n <span class=\"wp-pill-text\"><span class=\"wp-mode-name\" id=\"wp-mode-current\">${first.name}</span><span class=\"wp-mode-sub\" id=\"wp-mode-current-sub\">${first.subtitle}</span></span><span class=\"wp-caret\" aria-hidden=\"true\">▾</span>\n </button>\n <div class=\"wp-menu\" id=\"wp-mode-menu\" role=\"menu\" aria-label=\"Color by\" hidden>${modes.map((info: ModeInfo, index: number): string => this.modeItem(info, index)).join('')}</div>\n </div>`;\n }\n\n /** One menu option, two lines: the mode's name, then its grey subtitle. */\n private modeItem(info: ModeInfo, index: number): string {\n const checked = info.mode === GraphMode.RUNTIME ? 'true' : 'false';\n return (\n `<button type=\"button\" class=\"wp-mode\" role=\"menuitemradio\" data-wp-mode=\"${info.mode}\" aria-checked=\"${checked}\" title=\"Key ${index + 1}\">` +\n `<span class=\"wp-mode-name\">${info.name}</span><span class=\"wp-mode-sub\">${info.subtitle}</span></button>`\n );\n }\n\n /**\n * The Filter popover. Its four groups INTERSECT: a box is shown when it is in the chosen change\n * scope AND carries any selected runtime chip (when any is selected) AND any selected role chip\n * (when any is selected) AND belongs to any selected product (when any is selected). Nothing\n * applies until \"Show N projects\".\n */\n private filterPopover(parts: ShellParts): string {\n const scopes = SCOPE_OPTIONS.map(\n (option: ScopeOption): string =>\n `<label class=\"wp-scope\"><input type=\"radio\" name=\"wp-scope\" value=\"${option.scope}\"${option.scope === ChangeScope.EVERYTHING ? ' checked' : ''}>` +\n `<span class=\"wp-scope-text\"><span class=\"wp-scope-name\" data-wp-scope-label=\"${option.scope}\">${option.label}</span>` +\n `<span class=\"wp-scope-sub\">${parts.runtime && option.scope === ChangeScope.EVERYTHING ? 'every service' : option.subtitle}</span></span>` +\n `<span class=\"wp-count\" data-wp-scope-count=\"${option.scope}\"></span></label>`,\n ).join('');\n const chips = (group: string, values: readonly string[]): string =>\n values\n .map(\n (value: string): string =>\n `<button type=\"button\" class=\"wp-chip\" data-wp-chip-group=\"${group}\" data-wp-chip=\"${value}\" aria-pressed=\"false\">${value}</button>`,\n )\n .join('');\n return `<div class=\"wp-pop wp-filter-pop\" id=\"wp-filter-pop\" role=\"dialog\" aria-label=\"Filter ${parts.runtime ? 'services' : 'projects'}\" hidden>\n <header><span>Filter</span><button type=\"button\" class=\"wp-icon wp-icon-mini\" id=\"wp-filter-close\" aria-label=\"Close the filter\">✕</button></header>\n <fieldset class=\"wp-fgroup\" id=\"wp-scope-group\"><legend id=\"wp-scope-legend\">Changes on this branch</legend>${this.impactKinds('filter')}${scopes}<p class=\"wp-scope-reason\" id=\"wp-scope-reason\" hidden></p></fieldset>\n <fieldset class=\"wp-fgroup\"><legend>Runtime <small>any match</small></legend><div class=\"wp-chips\">${chips('runtime', parts.frameworks)}</div>${parts.runtime && parts.frameworks.length === 0 ? '<p>No declared framework metadata in the saved project graph. Refresh explicitly with architecture:generate.</p>' : ''}</fieldset>\n <fieldset class=\"wp-fgroup\"><legend>Role <small>any match</small></legend><div class=\"wp-chips\">${chips('role', parts.roles)}</div></fieldset>\n ${this.productGroup(parts.products)}\n <footer><button type=\"button\" class=\"wp-text-btn\" id=\"wp-filter-clear\">Clear all</button><button type=\"button\" class=\"wp-primary\" id=\"wp-filter-apply\">Show all ${parts.runtime ? 'services' : 'projects'}</button></footer>\n </div>`;\n }\n\n /**\n * The Product group (#1179): one chip per declared product, each with its palette swatch. Several\n * chips select the UNION of their members; the group intersects with the others. Omitted when the\n * workspace declares no product.\n */\n private productGroup(products: ProductColor[]): string {\n if (products.length === 0) return '';\n const chips = products\n .map(\n (color: ProductColor): string =>\n `<button type=\"button\" class=\"wp-chip\" data-wp-chip-group=\"product\" data-wp-chip=\"${color.product}\" aria-pressed=\"false\">` +\n `<svg class=\"wp-chip-swatch\" width=\"10\" height=\"10\" viewBox=\"0 0 10 10\" aria-hidden=\"true\"><circle cx=\"5\" cy=\"5\" r=\"4.5\" fill=\"${color.color}\"/></svg>` +\n `${color.product}</button>`,\n )\n .join('');\n return `<fieldset class=\"wp-fgroup\" id=\"wp-product-group\"><legend>Product <small>any match</small></legend><div class=\"wp-chips\">${chips}</div></fieldset>`;\n }\n\n /**\n * Which Impact scan the page shows (#1163): \"Changed on this branch\" (fork point → working tree)\n * or \"Last commit\" (HEAD^ → working tree). Hidden until the page finds TWO scans in the sidecar;\n * `where` tells the drawer's copy (shown only in Impact mode) from the Filter popover's.\n */\n private impactKinds(where: string): string {\n return (\n `<div class=\"wp-chips wp-impact-kinds\" data-wp-impact-kinds=\"${where}\" role=\"group\" aria-label=\"Compare\" hidden>` +\n '<button type=\"button\" class=\"wp-chip\" data-wp-impact-kind=\"branch\" aria-pressed=\"false\">Changed on this branch</button>' +\n '<button type=\"button\" class=\"wp-chip\" data-wp-impact-kind=\"commit\" aria-pressed=\"false\">Last commit</button></div>'\n );\n }\n\n private topbar(parts: ShellParts): string {\n return `<div class=\"wp-topbar\">\n <button type=\"button\" class=\"wp-icon\" id=\"wp-collapse\" aria-label=\"Toggle the controls drawer\" aria-controls=\"wp-side\" aria-expanded=\"true\">☰</button>\n <div class=\"wp-crumb\" id=\"wp-crumb\">Mode: <b>Runtime</b></div>\n <div class=\"wp-pills\" id=\"wp-filter-pills\" aria-label=\"Active filters\"></div>\n ${parts.filterStatus}\n <button type=\"button\" class=\"wp-icon\" id=\"wp-help-btn\" aria-label=\"How to read this graph\" aria-controls=\"wp-help\" aria-expanded=\"false\">?</button>\n </div>`;\n }\n\n private overlays(parts: ShellParts): string {\n return `${parts.emptyState}<div class=\"wp-pop wp-legend-popout\" id=\"wp-legend-popout\" hidden>\n <header><span id=\"wp-legend-pop-title\">Legend · Runtime</span><button type=\"button\" class=\"wp-icon wp-icon-mini\" id=\"wp-legend-close\" aria-label=\"Close the legend\">✕</button></header>\n ${parts.legend}\n </div>\n ${this.help(parts.runtime)}\n ${this.filterPopover(parts)}\n <div class=\"wp-pop wp-snapshot\" id=\"wp-snapshot\" hidden>${parts.snapshot}</div>\n <aside class=\"wp-panel\" id=\"wp-resp-panel\" aria-label=\"Responsibilities\" hidden>\n <header><span>Responsibilities</span><button type=\"button\" class=\"wp-icon wp-icon-mini\" id=\"wp-resp-close\" aria-label=\"Close responsibilities\">✕</button></header>\n ${parts.responsibilities}\n </aside>`;\n }\n\n /** The two hint paragraphs that used to sit above the graph, plus the keyboard shortcuts. */\n private help(runtime: boolean): string {\n if (runtime) return `<div class=\"wp-pop wp-help\" id=\"wp-help\" role=\"dialog\" aria-label=\"How to read this graph\" hidden>\n <header><span>Reading the runtime graph</span><button type=\"button\" class=\"wp-icon wp-icon-mini\" id=\"wp-help-close\" aria-label=\"Close help\">✕</button></header>\n <p>Rows preserve saved runtime call-depth levels. Boxes are services; horizontal cylinders are queues, upright cylinders datastores, and clocks scheduled triggers. Solid arrows are RPC calls, dashed arrows queued events; red arrows admit legacy cycles.</p>\n <p>Click a box for Lock, Filter Unconnected, or Implements. Implements in the box is a plain summary. Click Uses on a relationship line for its complete details. Hover traces chains without opening details.</p>\n <p>Lock pins separate incoming and outgoing chains; hover adds another chain temporarily. Hide unconnected removes unrelated boxes independently. Clear all clears facets, leaving Lock and chain filtering intact.</p>\n <p>Filters intersect declared runtime, role, product closure and Nx build scope. Counts describe services; attached contextual nodes are counted separately. Impact uses compile-project sets, never runtime reachability. Queues and vendors are context, never source owners or CI projects.</p>\n <p>Runtime uses declared framework colors, Architecture declared roles; contextual nodes keep their kind colors. Product colors come from each project's declared product: tags; contextual nodes inherit adjacent memberships. Unknown metadata is neutral. Missing saved metadata requires explicit architecture:generate.</p>\n <p><strong>Goal vs Current</strong> compares the saved goal (architecture/runtime-dependencies-goal.json) with the saved current graph, Current → Goal: green added, red dashed removed (still drawn), amber changed, grey unchanged. Click a box for its before/after details, or click an arrow.</p>\n <p><kbd>/</kbd> Focus search · <kbd>1</kbd>–<kbd>5</kbd> modes · <kbd>Esc</kbd> dismiss or unlock</p>\n </div>`;\n return `<div class=\"wp-pop wp-help\" id=\"wp-help\" role=\"dialog\" aria-label=\"How to read this graph\" hidden>\n <header><span>Reading the graph</span><button type=\"button\" class=\"wp-icon wp-icon-mini\" id=\"wp-help-close\" aria-label=\"Close help\">✕</button></header>\n <p>Every row is one dependency level: the HIGHEST level is the top row and L0, the foundation libraries, is always the bottom row. Servers, clients and apps always form the top row (a bundle sits above the apps it aggregates). Transitive dependencies are allowed but not drawn.</p>\n <p>💡 <strong>Click any box</strong> for its menu: <strong>View Design</strong> (only where the project has a generated <strong>design.html</strong>, i.e. what the AI sees inside it), <strong>Lock/Unlock</strong> (the same lock as the Focus field), <strong>Filter Unconnected</strong> (removes unrelated boxes, keeping each L-number row).</p>\n <p><strong>Filter</strong> narrows the graph by what changed on this branch or in the last commit, by runtime, by role and by product; the groups combine. A product keeps its servers, clients and apps plus everything they depend on, so <strong>Product</strong> + <strong>Changed + dependents + dependencies</strong> is exactly what this branch builds that can affect that product. Every box keeps its own L-row, and a level left empty shows as a thin labeled band.</p>\n <p>🔦 <strong>Hover any box</strong> to trace its <em>entire</em> dependency chain — every ancestor above it and every dependency below it, with the lines between — while the rest of the graph dims.</p>\n <p><strong>Runtime</strong>: a box may depend on a box carrying every color it has. <strong>Architecture</strong>: what each box is in the system. <strong>Impact</strong>: what changed on this branch or in the last commit (amber) and its dependents (amber outline); with both available, a toggle picks which. <strong>Product</strong>: which products each box belongs to — one color per product, stripes for several, grey for every product, a dashed white box for none.</p>\n <p><kbd>/</kbd> focus the lock search · <kbd>1</kbd> <kbd>2</kbd> <kbd>3</kbd> <kbd>4</kbd> switch mode · <kbd>Esc</kbd> unlock</p>\n </div>`;\n }\n\n private brandIcon(): string {\n return (\n '<svg width=\"22\" height=\"22\" viewBox=\"0 0 22 22\" aria-hidden=\"true\">' +\n '<rect x=\"1\" y=\"1\" width=\"8\" height=\"6\" rx=\"1.5\" fill=\"none\" stroke=\"#a46bf7\" stroke-width=\"1.6\"/>' +\n '<rect x=\"13\" y=\"1\" width=\"8\" height=\"6\" rx=\"1.5\" fill=\"none\" stroke=\"#e9eaf0\" stroke-width=\"1.6\"/>' +\n '<rect x=\"7\" y=\"15\" width=\"8\" height=\"6\" rx=\"1.5\" fill=\"none\" stroke=\"#e9eaf0\" stroke-width=\"1.6\"/>' +\n '<path d=\"M5 7v4h12V7M11 11v4\" fill=\"none\" stroke=\"#e9eaf0\" stroke-width=\"1.6\"/></svg>'\n );\n }\n}\n"]}
1
+ {"version":3,"file":"graph-page-shell.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/graph-page-shell.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The architecture page's shell (#1155): a collapsible dark drawer on the left holding every control,\n * and the graph filling the rest of the viewport.\n *\n * Before this, two hint paragraphs, a ~650px three-column legend, a lock dropdown and a zoom bar all\n * sat ABOVE the graph, so a laptop's first screen showed no graph at all. Now the hints are a \"?\"\n * popover, the legend lives in the drawer (and can pop out over the graph), zoom floats bottom-right\n * (graph-navigation.ts) and the responsibilities open in a right-hand panel.\n *\n * Markup only. Behaviour is graph-visualizer.client.ts; styling is graph-page-styles.ts. No element\n * carries a `style=` attribute — every look is a class.\n */\n\nimport { FRAMEWORK_ORDER, GRAPH_MODES, GraphMode, ModeInfo, ROLE_STYLES, RoleStyle, RUNTIME_GRAPH_MODES } from './graph-color-modes';\nimport { ProductColor } from './graph-products';\n\n/**\n * The Filter popover's \"Changes on this branch\" choices. The page computes each set from the Impact\n * sidecar (graph-impact.ts): CHANGED = touched; DEPENDENTS = every project nx affected, whatever its reason (their\n * tests re-run); DEPENDENCIES = touched + every transitive dependency of it; BUILD = touched +\n * every affected project + every build input — the dependencies of the DEPENDENTS too, which is why it is more than\n * the union of the two rows above it.\n *\n * \"Dependents\" are what USE a project and \"dependencies\" what it uses (#1179). \"Dependee\" would mean\n * the thing depended ON — the opposite of the dependents row — so it is never used.\n */\nexport enum ChangeScope {\n EVERYTHING = 'everything',\n CHANGED = 'changed',\n DEPENDENTS = 'dependents',\n DEPENDENCIES = 'dependencies',\n BUILD = 'build',\n}\n\n/**\n * One change scope: its name and the grey second line under it, like the Color-by menu. These are the\n * ONE copy of the labels — the page script reads each name back from its `data-wp-scope-label` (the\n * top-bar pill), so the two cannot drift apart.\n */\nexport class ScopeOption {\n constructor(\n public readonly scope: ChangeScope,\n public readonly label: string,\n public readonly subtitle: string,\n ) {}\n}\n\nexport const SCOPE_OPTIONS: readonly ScopeOption[] = [\n new ScopeOption(ChangeScope.EVERYTHING, 'All', 'every project'),\n new ScopeOption(ChangeScope.CHANGED, 'Changed', 'owns a changed file'),\n new ScopeOption(ChangeScope.DEPENDENTS, 'Changed + dependents', '+ everything else nx affected: dependents, implicitly and root-file touched (their tests re-run)'),\n new ScopeOption(ChangeScope.DEPENDENCIES, 'Changed + dependencies', '+ everything they use'),\n new ScopeOption(ChangeScope.BUILD, 'Changed + dependents + dependencies', 'everything CI builds for this branch'),\n];\n\n/** Everything the shell frames, already rendered by its owner. */\nexport class ShellParts {\n constructor(\n public readonly title: string,\n public readonly lockControl: string,\n public readonly legend: string,\n public readonly edgeKey: string,\n public readonly filterStatus: string,\n public readonly snapshot: string,\n public readonly responsibilities: string,\n /**\n * One chip per product (#1179), sorted, each colored from the page's ONE palette (#1244);\n * empty hides the Filter's Product group.\n */\n public readonly products: ProductColor[],\n public readonly runtime: boolean = false,\n public readonly frameworks: readonly string[] = FRAMEWORK_ORDER,\n public readonly roles: readonly string[] = ROLE_STYLES.map((style: RoleStyle): string => style.role),\n public readonly emptyState: string = '',\n ) {}\n}\n\nexport class GraphPageShell {\n body(parts: ShellParts): string {\n return `<div class=\"wp-shell\" id=\"wp-shell\">\n ${this.drawer(parts)}\n <main class=\"wp-main\">\n ${this.topbar(parts)}\n <div id=\"graph\"></div>\n ${this.overlays(parts)}\n </main>\n</div>`;\n }\n\n private drawer(parts: ShellParts): string {\n return `<aside class=\"wp-side\" id=\"wp-side\" aria-label=\"Graph controls\">\n <div class=\"wp-brand\">${this.brandIcon()}<span>${parts.title}</span></div>\n <div class=\"wp-group\" role=\"group\" aria-label=\"Color mode\">\n <div class=\"wp-label\">Color by</div>\n ${this.modePulldown(parts.runtime ? RUNTIME_GRAPH_MODES : GRAPH_MODES)}\n <p class=\"wp-mode-reason\" id=\"wp-impact-reason\" hidden></p>\n ${this.impactKinds('drawer')}\n </div>\n <div class=\"wp-group\">\n <button type=\"button\" class=\"wp-filter-btn\" id=\"wp-filter-open\" aria-controls=\"wp-filter-pop\" aria-expanded=\"false\"><span>Filter</span><span class=\"wp-badge\" id=\"wp-filter-badge\" hidden>0</span></button>\n </div>\n <div class=\"wp-group\">\n <label class=\"wp-label\" for=\"wp-lock\">Focus</label>\n ${parts.lockControl}\n <button type=\"button\" class=\"wp-toggle\" id=\"wp-lock-toggle\" aria-pressed=\"false\">Lock <span class=\"wp-sw\" aria-hidden=\"true\"></span></button>\n <button type=\"button\" class=\"wp-toggle\" id=\"wp-filter-toggle\" aria-pressed=\"false\">Hide unconnected <span class=\"wp-sw\" aria-hidden=\"true\"></span></button>\n </div>\n <div class=\"wp-group\">\n <div class=\"wp-label wp-label-row\">Legend <button type=\"button\" class=\"wp-icon wp-icon-mini wp-icon-dark\" id=\"wp-legend-pop\" title=\"Pop the legend out over the graph\" aria-label=\"Pop the legend out over the graph\">⇱</button></div>\n ${parts.legend}\n </div>\n <div class=\"wp-group\">\n <div class=\"wp-label\">Edges</div>\n ${parts.edgeKey}\n </div>\n <div class=\"wp-foot\">\n <button type=\"button\" class=\"wp-link\" id=\"wp-resp-open\" aria-expanded=\"false\">Responsibilities</button>\n <button type=\"button\" class=\"wp-link\" id=\"wp-snapshot-open\" aria-expanded=\"false\">Saved snapshot</button>\n </div>\n </aside>`;\n }\n\n /**\n * \"Color by\": ONE pill-shaped trigger showing the current mode (name + subtitle) that opens a\n * menu of the four. Keys 1/2/3/4 and the #runtime / #architecture / #impact / #product links still\n * switch.\n */\n private modePulldown(modes: readonly ModeInfo[]): string {\n const first = modes[0];\n return `<div class=\"wp-pulldown\">\n <button type=\"button\" class=\"wp-pill-trigger\" id=\"wp-mode-trigger\" aria-haspopup=\"menu\" aria-controls=\"wp-mode-menu\" aria-expanded=\"false\">\n <span class=\"wp-pill-text\"><span class=\"wp-mode-name\" id=\"wp-mode-current\">${first.name}</span><span class=\"wp-mode-sub\" id=\"wp-mode-current-sub\">${first.subtitle}</span></span><span class=\"wp-caret\" aria-hidden=\"true\">▾</span>\n </button>\n <div class=\"wp-menu\" id=\"wp-mode-menu\" role=\"menu\" aria-label=\"Color by\" hidden>${modes.map((info: ModeInfo, index: number): string => this.modeItem(info, index)).join('')}</div>\n </div>`;\n }\n\n /** One menu option, two lines: the mode's name, then its grey subtitle. */\n private modeItem(info: ModeInfo, index: number): string {\n const checked = info.mode === GraphMode.RUNTIME ? 'true' : 'false';\n return (\n `<button type=\"button\" class=\"wp-mode\" role=\"menuitemradio\" data-wp-mode=\"${info.mode}\" aria-checked=\"${checked}\" title=\"Key ${index + 1}\">` +\n `<span class=\"wp-mode-name\">${info.name}</span><span class=\"wp-mode-sub\">${info.subtitle}</span></button>`\n );\n }\n\n /**\n * The Filter popover. Its four groups INTERSECT: a box is shown when it is in the chosen change\n * scope AND carries any selected runtime chip (when any is selected) AND any selected role chip\n * (when any is selected) AND belongs to any selected product (when any is selected). Nothing\n * applies until \"Show N projects\".\n */\n private filterPopover(parts: ShellParts): string {\n const scopes = SCOPE_OPTIONS.map(\n (option: ScopeOption): string =>\n `<label class=\"wp-scope\"><input type=\"radio\" name=\"wp-scope\" value=\"${option.scope}\"${option.scope === ChangeScope.EVERYTHING ? ' checked' : ''}>` +\n `<span class=\"wp-scope-text\"><span class=\"wp-scope-name\" data-wp-scope-label=\"${option.scope}\">${option.label}</span>` +\n `<span class=\"wp-scope-sub\">${parts.runtime && option.scope === ChangeScope.EVERYTHING ? 'every service' : option.subtitle}</span></span>` +\n `<span class=\"wp-count\" data-wp-scope-count=\"${option.scope}\"></span></label>`,\n ).join('');\n const chips = (group: string, values: readonly string[]): string =>\n values\n .map(\n (value: string): string =>\n `<button type=\"button\" class=\"wp-chip\" data-wp-chip-group=\"${group}\" data-wp-chip=\"${value}\" aria-pressed=\"false\">${value}</button>`,\n )\n .join('');\n return `<div class=\"wp-pop wp-filter-pop\" id=\"wp-filter-pop\" role=\"dialog\" aria-label=\"Filter ${parts.runtime ? 'services' : 'projects'}\" hidden>\n <header><span>Filter</span><button type=\"button\" class=\"wp-icon wp-icon-mini\" id=\"wp-filter-close\" aria-label=\"Close the filter\">✕</button></header>\n <fieldset class=\"wp-fgroup\" id=\"wp-scope-group\"><legend id=\"wp-scope-legend\">Changes on this branch</legend>${this.impactKinds('filter')}${scopes}<p class=\"wp-scope-reason\" id=\"wp-scope-reason\" hidden></p></fieldset>\n <fieldset class=\"wp-fgroup\"><legend>Runtime <small>any match</small></legend><div class=\"wp-chips\">${chips('runtime', parts.frameworks)}</div>${parts.runtime && parts.frameworks.length === 0 ? '<p>No declared framework metadata in the saved project graph. Refresh explicitly with architecture:generate.</p>' : ''}</fieldset>\n <fieldset class=\"wp-fgroup\"><legend>Role <small>any match</small></legend><div class=\"wp-chips\">${chips('role', parts.roles)}</div></fieldset>\n ${this.productGroup(parts.products)}\n <footer><button type=\"button\" class=\"wp-text-btn\" id=\"wp-filter-clear\">Clear all</button><button type=\"button\" class=\"wp-primary\" id=\"wp-filter-apply\">Show all ${parts.runtime ? 'services' : 'projects'}</button></footer>\n </div>`;\n }\n\n /**\n * The Product group (#1179): one chip per declared product, each with its palette swatch. Several\n * chips select the UNION of their members; the group intersects with the others. Omitted when the\n * workspace declares no product.\n */\n private productGroup(products: ProductColor[]): string {\n if (products.length === 0) return '';\n const chips = products\n .map(\n (color: ProductColor): string =>\n `<button type=\"button\" class=\"wp-chip\" data-wp-chip-group=\"product\" data-wp-chip=\"${color.product}\" aria-pressed=\"false\">` +\n `<svg class=\"wp-chip-swatch\" width=\"10\" height=\"10\" viewBox=\"0 0 10 10\" aria-hidden=\"true\"><circle cx=\"5\" cy=\"5\" r=\"4.5\" fill=\"${color.color}\"/></svg>` +\n `${color.product}</button>`,\n )\n .join('');\n return `<fieldset class=\"wp-fgroup\" id=\"wp-product-group\"><legend>Product <small>any match</small></legend><div class=\"wp-chips\">${chips}</div></fieldset>`;\n }\n\n /**\n * Which Impact scan the page shows (#1163): \"Changed on this branch\" (fork point → working tree)\n * or \"Last commit\" (HEAD^ → working tree). Hidden until the page finds TWO scans in the sidecar;\n * `where` tells the drawer's copy (shown only in Impact mode) from the Filter popover's.\n */\n private impactKinds(where: string): string {\n return (\n `<div class=\"wp-chips wp-impact-kinds\" data-wp-impact-kinds=\"${where}\" role=\"group\" aria-label=\"Compare\" hidden>` +\n '<button type=\"button\" class=\"wp-chip\" data-wp-impact-kind=\"branch\" aria-pressed=\"false\">Changed on this branch</button>' +\n '<button type=\"button\" class=\"wp-chip\" data-wp-impact-kind=\"commit\" aria-pressed=\"false\">Last commit</button></div>'\n );\n }\n\n private topbar(parts: ShellParts): string {\n return `<div class=\"wp-topbar\">\n <button type=\"button\" class=\"wp-icon\" id=\"wp-collapse\" aria-label=\"Toggle the controls drawer\" aria-controls=\"wp-side\" aria-expanded=\"true\">☰</button>\n <div class=\"wp-crumb\" id=\"wp-crumb\">Mode: <b>Runtime</b></div>\n <div class=\"wp-pills\" id=\"wp-filter-pills\" aria-label=\"Active filters\"></div>\n ${parts.filterStatus}\n <button type=\"button\" class=\"wp-icon\" id=\"wp-help-btn\" aria-label=\"How to read this graph\" aria-controls=\"wp-help\" aria-expanded=\"false\">?</button>\n </div>`;\n }\n\n private overlays(parts: ShellParts): string {\n return `${parts.emptyState}<div class=\"wp-pop wp-legend-popout\" id=\"wp-legend-popout\" hidden>\n <header><span id=\"wp-legend-pop-title\">Legend · Runtime</span><button type=\"button\" class=\"wp-icon wp-icon-mini\" id=\"wp-legend-close\" aria-label=\"Close the legend\">✕</button></header>\n ${parts.legend}\n </div>\n ${this.help(parts.runtime)}\n ${this.filterPopover(parts)}\n <div class=\"wp-pop wp-snapshot\" id=\"wp-snapshot\" hidden>${parts.snapshot}</div>\n <aside class=\"wp-panel\" id=\"wp-resp-panel\" aria-label=\"Responsibilities\" hidden>\n <header><span>Responsibilities</span><button type=\"button\" class=\"wp-icon wp-icon-mini\" id=\"wp-resp-close\" aria-label=\"Close responsibilities\">✕</button></header>\n ${parts.responsibilities}\n </aside>`;\n }\n\n /** The two hint paragraphs that used to sit above the graph, plus the keyboard shortcuts. */\n private help(runtime: boolean): string {\n if (runtime) return `<div class=\"wp-pop wp-help\" id=\"wp-help\" role=\"dialog\" aria-label=\"How to read this graph\" hidden>\n <header><span>Reading the runtime graph</span><button type=\"button\" class=\"wp-icon wp-icon-mini\" id=\"wp-help-close\" aria-label=\"Close help\">✕</button></header>\n <p>Rows preserve saved runtime call-depth levels. Boxes are services; horizontal cylinders are queues, upright cylinders datastores, and clocks scheduled triggers. Solid arrows are RPC calls, dashed arrows queued events; red arrows admit legacy cycles.</p>\n <p>Click a box for Lock, Filter Unconnected, or Implements. Implements in the box is a plain summary. Click Uses on a relationship line for its complete details. Hover traces chains without opening details.</p>\n <p>Lock pins separate incoming and outgoing chains; hover adds another chain temporarily. Hide unconnected removes unrelated boxes independently. Clear all clears facets, leaving Lock and chain filtering intact.</p>\n <p>Filters intersect declared runtime, role, product closure and Nx build scope. Counts describe services; attached contextual nodes are counted separately. Impact uses compile-project sets, never runtime reachability. Queues and vendors are context, never source owners or CI projects.</p>\n <p>Runtime uses declared framework colors, Architecture declared roles; contextual nodes keep their kind colors. Product colors come from each project's declared product: tags; contextual nodes inherit adjacent memberships. Unknown metadata is neutral. Missing saved metadata requires explicit architecture:generate.</p>\n <p><strong>Goal vs Current</strong> compares the saved goal (architecture/runtime-dependencies-goal.json) with the saved current graph, Current → Goal: green added, red dashed removed (still drawn), amber changed, grey unchanged. Click a box for its before/after details, or click an arrow.</p>\n <p><kbd>/</kbd> Focus search · <kbd>1</kbd>–<kbd>5</kbd> modes · <kbd>Esc</kbd> dismiss or unlock</p>\n </div>`;\n return `<div class=\"wp-pop wp-help\" id=\"wp-help\" role=\"dialog\" aria-label=\"How to read this graph\" hidden>\n <header><span>Reading the graph</span><button type=\"button\" class=\"wp-icon wp-icon-mini\" id=\"wp-help-close\" aria-label=\"Close help\">✕</button></header>\n <p>Every row is one dependency level: the HIGHEST level is the top row and L0, the foundation libraries, is always the bottom row. Servers, clients and apps always form the top row (a bundle sits above the apps it aggregates). Transitive dependencies are allowed but not drawn.</p>\n <p>💡 <strong>Click any box</strong> for its menu: <strong>View Design</strong> (only where the project has a generated <strong>design.html</strong>, i.e. what the AI sees inside it), <strong>Lock/Unlock</strong> (the same lock as the Focus field), <strong>Filter Unconnected</strong> (removes unrelated boxes, keeping each L-number row).</p>\n <p><strong>Filter</strong> narrows the graph by what changed on this branch or in the last commit, by runtime, by role and by product; the groups combine. A product keeps its servers, clients and apps plus everything they depend on, so <strong>Product</strong> + <strong>Changed + dependents + dependencies</strong> is exactly what this branch builds that can affect that product. Every box keeps its own L-row, and a level left empty shows as a thin labeled band.</p>\n <p>🔦 <strong>Hover any box</strong> to trace its <em>entire</em> dependency chain — every ancestor above it and every dependency below it, with the lines between — while the rest of the graph dims.</p>\n <p><strong>Runtime</strong>: a box may depend on a box carrying every color it has. <strong>Architecture</strong>: what each box is in the system. <strong>Impact</strong>: what changed on this branch or in the last commit (amber), its dependents (amber outline), and what nx marked for another reason: an input glob (violet hatch) or a root file (teal). Click an affected box for why; with both available, a toggle picks which. <strong>Product</strong>: which products each box belongs to — one color per product, stripes for several, grey for every product, a dashed white box for none.</p>\n <p><kbd>/</kbd> focus the lock search · <kbd>1</kbd> <kbd>2</kbd> <kbd>3</kbd> <kbd>4</kbd> switch mode · <kbd>Esc</kbd> unlock</p>\n </div>`;\n }\n\n private brandIcon(): string {\n return (\n '<svg width=\"22\" height=\"22\" viewBox=\"0 0 22 22\" aria-hidden=\"true\">' +\n '<rect x=\"1\" y=\"1\" width=\"8\" height=\"6\" rx=\"1.5\" fill=\"none\" stroke=\"#a46bf7\" stroke-width=\"1.6\"/>' +\n '<rect x=\"13\" y=\"1\" width=\"8\" height=\"6\" rx=\"1.5\" fill=\"none\" stroke=\"#e9eaf0\" stroke-width=\"1.6\"/>' +\n '<rect x=\"7\" y=\"15\" width=\"8\" height=\"6\" rx=\"1.5\" fill=\"none\" stroke=\"#e9eaf0\" stroke-width=\"1.6\"/>' +\n '<path d=\"M5 7v4h12V7M11 11v4\" fill=\"none\" stroke=\"#e9eaf0\" stroke-width=\"1.6\"/></svg>'\n );\n }\n}\n"]}
@@ -125,6 +125,9 @@ declare class GraphPage extends WpFilterPage {
125
125
  private projects;
126
126
  private applied;
127
127
  private readonly drawer;
128
+ private readonly why;
129
+ /** In Impact mode, an affected box's menu leads with WHY it is in the build (#1264). */
130
+ whyItem(name: string, node: SVGGElement): WpNodeMenuItem | null;
128
131
  protected captureBinding(_svg: SVGSVGElement | null): () => void;
129
132
  protected wireControls(): void;
130
133
  /** '' when this page can show Goal vs Current, else why not. */
@@ -51,7 +51,7 @@ class WpProjectFilter {
51
51
  return null;
52
52
  const ids = [...impact.touched];
53
53
  if (scope === 'dependents' || scope === 'build')
54
- ids.push(...impact.affected);
54
+ ids.push(...this.modes.affected());
55
55
  if (scope === 'dependencies')
56
56
  ids.push(...impact.dependencies);
57
57
  if (scope === 'build')
@@ -178,6 +178,9 @@ class GraphHighlighter {
178
178
  const locked = this.page.lockSelection() === name;
179
179
  items.push(new WpNodeMenuItem(locked ? 'Unlock' : 'Lock', () => locked ? this.page.clearSelection() : this.page.setLock(name)));
180
180
  items.push(this.page.filterItem(name));
181
+ const why = this.page.whyItem(name, node);
182
+ if (why !== null)
183
+ items.unshift(why);
181
184
  const implemented = window.__WP_RUNTIME__?.implementsItem(name, node);
182
185
  if (implemented)
183
186
  items.push(implemented);
@@ -535,6 +538,10 @@ class GraphDrawer {
535
538
  document.querySelectorAll('[data-wp-impact-note]').forEach((note) => {
536
539
  note.textContent = this.impactNote(modes);
537
540
  });
541
+ // "unexplained" is a row only when the active scan has one.
542
+ document.querySelectorAll('[data-wp-legend="unexplained"]').forEach((row) => {
543
+ row.hidden = (modes.impact?.unexplained.length ?? 0) === 0;
544
+ });
538
545
  this.syncImpactKinds(modes);
539
546
  const title = this.byId('wp-legend-pop-title');
540
547
  if (title !== null)
@@ -609,9 +616,8 @@ class GraphDrawer {
609
616
  // An available scan with a note is an EMPTY one, e.g. "Nothing changed on this branch yet".
610
617
  if (impact.reason !== '')
611
618
  return `${impact.label}: ${impact.reason}.`;
612
- const stats = `${impact.label}: ${impact.touched.length} changed · ${impact.affected.length} dependents · ` +
613
- `${impact.buildInputs.length} dependencies — nx affected, ${impact.changedFiles} changed files.`;
614
- const cause = this.globalCause(impact);
619
+ const stats = `${impact.label}: ${new WpImpactWhy().counts(impact)} — nx affected, ${impact.changedFiles} changed files.`;
620
+ const cause = this.globalCause(impact, modes.affected().length);
615
621
  return cause === '' ? stats : `${cause}. ${stats}`;
616
622
  }
617
623
  /**
@@ -619,10 +625,9 @@ class GraphDrawer {
619
625
  * workspace-global input (a lockfile, the workspace manifest). That is correct for CI, and
620
626
  * baffling on a graph, so the note names the cause.
621
627
  */
622
- globalCause(impact) {
628
+ globalCause(impact, affected) {
623
629
  if (impact.touched.length > 0 || impact.changedFiles === 0)
624
630
  return '';
625
- const affected = impact.affected.length;
626
631
  const who = affected >= this.page.projectCount() ? 'Every project affected' : `${affected} projects affected`;
627
632
  const files = impact.globalFiles;
628
633
  if (files.length === 0)
@@ -666,6 +671,15 @@ class GraphPage extends WpFilterPage {
666
671
  projects = new WpProjectFilter(this.model, this.modes);
667
672
  applied = new WpFilterChoice();
668
673
  drawer = new GraphDrawer(this);
674
+ why = new ImpactWhyPanel();
675
+ /** In Impact mode, an affected box's menu leads with WHY it is in the build (#1264). */
676
+ whyItem(name, node) {
677
+ const reason = this.modes.mode === 'impact' ? this.modes.reason(name) : null;
678
+ if (reason === null)
679
+ return null;
680
+ const title = (WpImpactWhy.TITLES[reason.kind] ?? reason.kind).toLowerCase();
681
+ return new WpNodeMenuItem(`Why in this build? (${title})`, () => this.why.open(node, reason));
682
+ }
669
683
  captureBinding(_svg) {
670
684
  const previous = this.highlighter;
671
685
  return () => {