@webpieces/nx-webpieces-rules 0.4.919 → 0.4.921

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 (49) hide show
  1. package/package.json +7 -7
  2. package/src/executors/generate/executor.d.ts +7 -0
  3. package/src/executors/generate/executor.js +13 -5
  4. package/src/executors/generate/executor.js.map +1 -1
  5. package/src/executors/validate-architecture-unchanged/executor.d.ts +1 -20
  6. package/src/executors/validate-architecture-unchanged/executor.js +2 -45
  7. package/src/executors/validate-architecture-unchanged/executor.js.map +1 -1
  8. package/src/executors/visualize/executor.d.ts +8 -1
  9. package/src/executors/visualize/executor.js +11 -3
  10. package/src/executors/visualize/executor.js.map +1 -1
  11. package/src/executors/visualize-runtime/executor.js +2 -1
  12. package/src/executors/visualize-runtime/executor.js.map +1 -1
  13. package/src/executors/visualize-runtime-goal/executor.js +2 -1
  14. package/src/executors/visualize-runtime-goal/executor.js.map +1 -1
  15. package/src/lib/current-architecture.d.ts +26 -0
  16. package/src/lib/current-architecture.js +56 -0
  17. package/src/lib/current-architecture.js.map +1 -0
  18. package/src/lib/graph-color-modes.d.ts +29 -11
  19. package/src/lib/graph-color-modes.js +40 -14
  20. package/src/lib/graph-color-modes.js.map +1 -1
  21. package/src/lib/graph-edge-diff.d.ts +115 -0
  22. package/src/lib/graph-edge-diff.js +263 -0
  23. package/src/lib/graph-edge-diff.js.map +1 -0
  24. package/src/lib/graph-impact-reasons.d.ts +160 -0
  25. package/src/lib/graph-impact-reasons.js +412 -0
  26. package/src/lib/graph-impact-reasons.js.map +1 -0
  27. package/src/lib/graph-impact.d.ts +100 -48
  28. package/src/lib/graph-impact.js +176 -105
  29. package/src/lib/graph-impact.js.map +1 -1
  30. package/src/lib/graph-legend.d.ts +9 -2
  31. package/src/lib/graph-legend.js +31 -4
  32. package/src/lib/graph-legend.js.map +1 -1
  33. package/src/lib/graph-mode-state.client.d.ts +96 -2
  34. package/src/lib/graph-mode-state.client.js +343 -3
  35. package/src/lib/graph-mode-state.client.js.map +1 -1
  36. package/src/lib/graph-navigation.js +4 -2
  37. package/src/lib/graph-navigation.js.map +1 -1
  38. package/src/lib/graph-page-shell.d.ts +5 -3
  39. package/src/lib/graph-page-shell.js +7 -4
  40. package/src/lib/graph-page-shell.js.map +1 -1
  41. package/src/lib/graph-visualizer.client.d.ts +4 -0
  42. package/src/lib/graph-visualizer.client.js +36 -9
  43. package/src/lib/graph-visualizer.client.js.map +1 -1
  44. package/src/lib/runtime-html-page.js +2 -2
  45. package/src/lib/runtime-html-page.js.map +1 -1
  46. package/src/lib/runtime-viewer-adapter.js +1 -1
  47. package/src/lib/runtime-viewer-adapter.js.map +1 -1
  48. package/src/native-actions.js +1 -1
  49. package/src/native-actions.js.map +1 -1
@@ -0,0 +1,263 @@
1
+ "use strict";
2
+ /**
3
+ * Edge changes against the base (#1264): which dependency and runtime edges this branch GAINED or
4
+ * LOST, so "why is this building?" is answerable for a project whose only change is an edge.
5
+ *
6
+ * BASE is the FORK POINT — the branch comparison's `git merge-base HEAD origin/main` — read as
7
+ * `git show <forkPoint>:architecture/dependencies.json` and `…:architecture/runtime-dependencies.json`.
8
+ * Main keeps both regenerated and validated (`validate-architecture-unchanged`), so the fork point's
9
+ * copies are trustworthy.
10
+ *
11
+ * CURRENT is the graph computed LIVE when the page is refreshed (`LiveGraphSource`: the same pipeline
12
+ * `architecture:generate` runs), NEVER HEAD's committed architecture/*.json. Whether or not the branch
13
+ * already regenerated and committed those files must not matter: comparing against HEAD's committed
14
+ * copy would show no change at all once it had been regenerated.
15
+ *
16
+ * Neither file at the fork point (first adoption) is an answer, not an error: no edge diff, and the
17
+ * page says why. Nothing here ever throws into generation.
18
+ */
19
+ Object.defineProperty(exports, "__esModule", { value: true });
20
+ exports.EdgeChangeScanner = exports.EdgeDiffer = exports.GitFileAt = exports.FixedLiveGraphs = exports.LiveGraphSource = exports.LiveGraphs = exports.EdgeDiffReport = exports.EdgeChange = exports.EdgeRelation = exports.EdgeChangeKind = void 0;
21
+ const child_process_1 = require("child_process");
22
+ const graph_loader_1 = require("./graph-loader");
23
+ const runtime_graph_io_1 = require("./runtime-graph-io");
24
+ const runtime_graph_1 = require("./runtime-graph");
25
+ const api_contract_files_1 = require("./api-contract-files");
26
+ const current_architecture_1 = require("./current-architecture");
27
+ const toError_1 = require("../toError");
28
+ var EdgeChangeKind;
29
+ (function (EdgeChangeKind) {
30
+ EdgeChangeKind["GAINED"] = "gained";
31
+ EdgeChangeKind["LOST"] = "lost";
32
+ })(EdgeChangeKind || (exports.EdgeChangeKind = EdgeChangeKind = {}));
33
+ var EdgeRelation;
34
+ (function (EdgeRelation) {
35
+ /** A build dependency (architecture/dependencies.json `dependsOn`). */
36
+ EdgeRelation["DEPENDS"] = "depends";
37
+ /** A runtime call (architecture/runtime-dependencies.json `runtimeEdges`). */
38
+ EdgeRelation["CALLS"] = "calls";
39
+ })(EdgeRelation || (exports.EdgeRelation = EdgeRelation = {}));
40
+ /** One gained or lost edge, `from` → `to` (node ids), with the words the page shows for it. */
41
+ class EdgeChange {
42
+ from;
43
+ to;
44
+ change;
45
+ relation;
46
+ apis;
47
+ text;
48
+ constructor(from, to, change, relation,
49
+ /** CALLS: the APIs gained or lost on this edge, sorted. Empty for DEPENDS. */
50
+ apis,
51
+ /** "new on this branch: a now depends on b" / "a no longer calls b (SaveApi)". */
52
+ text) {
53
+ this.from = from;
54
+ this.to = to;
55
+ this.change = change;
56
+ this.relation = relation;
57
+ this.apis = apis;
58
+ this.text = text;
59
+ }
60
+ }
61
+ exports.EdgeChange = EdgeChange;
62
+ /** The edge changes of the dependency graph and of the runtime graph, or why each is not shown. */
63
+ class EdgeDiffReport {
64
+ base;
65
+ dependencyReason;
66
+ dependency;
67
+ runtimeReason;
68
+ runtime;
69
+ constructor(
70
+ /** The fork point's short sha ('' when there is none). */
71
+ base,
72
+ /** '' when `dependency` was computed; otherwise why the page shows no dependency edge changes. */
73
+ dependencyReason, dependency,
74
+ /** '' when `runtime` was computed; otherwise why the page shows no runtime edge changes. */
75
+ runtimeReason, runtime) {
76
+ this.base = base;
77
+ this.dependencyReason = dependencyReason;
78
+ this.dependency = dependency;
79
+ this.runtimeReason = runtimeReason;
80
+ this.runtime = runtime;
81
+ }
82
+ }
83
+ exports.EdgeDiffReport = EdgeDiffReport;
84
+ /** The two graphs the branch has NOW, computed live. `runtime` null when it could not be derived. */
85
+ class LiveGraphs {
86
+ projects;
87
+ runtime;
88
+ runtimeReason;
89
+ constructor(projects, runtime,
90
+ /** Why `runtime` is null ('' otherwise). */
91
+ runtimeReason = '') {
92
+ this.projects = projects;
93
+ this.runtime = runtime;
94
+ this.runtimeReason = runtimeReason;
95
+ }
96
+ }
97
+ exports.LiveGraphs = LiveGraphs;
98
+ /**
99
+ * Computes the live graphs: `CurrentGraphBuilder` (generate's own pipeline, writing nothing) and the
100
+ * runtime graph derived from it exactly as generate derives it. `architecture:generate` passes the
101
+ * graphs it just built instead (`FixedLiveGraphs`), so nothing is computed twice.
102
+ */
103
+ class LiveGraphSource {
104
+ async graphs(workspaceRoot) {
105
+ const current = await new current_architecture_1.CurrentGraphBuilder().build(workspaceRoot);
106
+ const hidden = new Set(Object.keys(current.graph).filter((name) => current.graph[name].drawOnGraph === false));
107
+ // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- the runtime half is optional; the dependency half still shows
108
+ try {
109
+ const contracts = new api_contract_files_1.ApiContractFiles().load(workspaceRoot, graph_loader_1.DEFAULT_GRAPH_PATH, current.apiContractFiles);
110
+ const report = (0, runtime_graph_1.deriveRuntimeGraphReport)(current.graph, hidden, contracts, current.externalSystems, current.remoteServices);
111
+ return new LiveGraphs(current.graph, report.graph);
112
+ }
113
+ catch (err) {
114
+ const error = (0, toError_1.toError)(err);
115
+ return new LiveGraphs(current.graph, null, `the live runtime graph could not be derived (${error.message})`);
116
+ }
117
+ }
118
+ }
119
+ exports.LiveGraphSource = LiveGraphSource;
120
+ /** The graphs a caller already holds (architecture:generate just built them). */
121
+ class FixedLiveGraphs extends LiveGraphSource {
122
+ live;
123
+ constructor(live) {
124
+ super();
125
+ this.live = live;
126
+ }
127
+ async graphs() {
128
+ return this.live;
129
+ }
130
+ }
131
+ exports.FixedLiveGraphs = FixedLiveGraphs;
132
+ /** A committed file as it was at a commit. Injected so specs never spawn git. */
133
+ class GitFileAt {
134
+ /** The file's text at `sha`; null when it did not exist there (or git cannot answer). */
135
+ read(workspaceRoot, sha, file) {
136
+ const shown = (0, child_process_1.spawnSync)('git', ['show', `${sha}:${file}`], {
137
+ cwd: workspaceRoot,
138
+ encoding: 'utf8',
139
+ stdio: 'pipe',
140
+ maxBuffer: 64 * 1024 * 1024,
141
+ });
142
+ return shown.status === 0 ? shown.stdout : null;
143
+ }
144
+ }
145
+ exports.GitFileAt = GitFileAt;
146
+ /** The base graphs' edges: `from|to` → the APIs on that edge (empty for a dependency). */
147
+ class EdgeSet {
148
+ edges = new Map();
149
+ add(from, to, apis) {
150
+ const key = `${from}\u0000${to}`;
151
+ const known = this.edges.get(key) ?? new Set();
152
+ for (const api of apis)
153
+ known.add(api);
154
+ this.edges.set(key, known);
155
+ }
156
+ }
157
+ /** Diffs base against live; pure. */
158
+ class EdgeDiffer {
159
+ dependency(base, live) {
160
+ return this.compare(this.dependencyEdges(base), this.dependencyEdges(live), EdgeRelation.DEPENDS);
161
+ }
162
+ runtime(base, live) {
163
+ return this.compare(this.runtimeEdges(base), this.runtimeEdges(live), EdgeRelation.CALLS);
164
+ }
165
+ dependencyEdges(graph) {
166
+ const set = new EdgeSet();
167
+ for (const name of Object.keys(graph))
168
+ for (const dep of graph[name].dependsOn ?? [])
169
+ set.add(name, dep, []);
170
+ return set;
171
+ }
172
+ runtimeEdges(graph) {
173
+ const set = new EdgeSet();
174
+ for (const edge of graph.runtimeEdges ?? [])
175
+ set.add(edge.from, edge.to, edge.via ?? []);
176
+ return set;
177
+ }
178
+ /** Per edge: gained when absent at base; lost when absent now; for a call, the APIs that came or went. */
179
+ compare(base, live, relation) {
180
+ const changes = [];
181
+ const keys = [...new Set([...base.edges.keys(), ...live.edges.keys()])].sort();
182
+ for (const key of keys) {
183
+ const ends = key.split('\u0000');
184
+ const before = base.edges.get(key);
185
+ const after = live.edges.get(key);
186
+ const gained = after === undefined ? [] : [...after].filter((api) => before === undefined || !before.has(api));
187
+ const lost = before === undefined ? [] : [...before].filter((api) => after === undefined || !after.has(api));
188
+ if (before === undefined || gained.length > 0)
189
+ changes.push(this.change(ends[0], ends[1], EdgeChangeKind.GAINED, relation, gained.sort()));
190
+ if (after === undefined || lost.length > 0)
191
+ changes.push(this.change(ends[0], ends[1], EdgeChangeKind.LOST, relation, lost.sort()));
192
+ }
193
+ return changes;
194
+ }
195
+ change(from, to, kind, relation, apis) {
196
+ const verb = relation === EdgeRelation.DEPENDS ? 'depends on' : 'calls';
197
+ const via = apis.length === 0 ? '' : ` (${apis.join(', ')})`;
198
+ const text = kind === EdgeChangeKind.GAINED
199
+ ? `new on this branch: ${from} now ${verb} ${to}${via}`
200
+ : `${from} no longer ${verb} ${to}${via}`;
201
+ return new EdgeChange(from, to, kind, relation, apis, text);
202
+ }
203
+ }
204
+ exports.EdgeDiffer = EdgeDiffer;
205
+ /** Reads the fork point's committed graphs and diffs them against the live ones. Never throws. */
206
+ class EdgeChangeScanner {
207
+ git;
208
+ differ;
209
+ constructor(git = new GitFileAt(), differ = new EdgeDiffer()) {
210
+ this.git = git;
211
+ this.differ = differ;
212
+ }
213
+ async scan(workspaceRoot, forkPoint, source) {
214
+ if (forkPoint === null)
215
+ return this.none('Edge changes compare a branch with its fork point; there is no branch comparison here (main or a detached HEAD)');
216
+ const short = forkPoint.slice(0, 7);
217
+ // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- edge changes are optional; a failure becomes the reason the page shows
218
+ try {
219
+ const baseProjects = this.projectsAt(workspaceRoot, forkPoint);
220
+ const baseRuntime = this.runtimeAt(workspaceRoot, forkPoint);
221
+ if (baseProjects === null && baseRuntime === null)
222
+ return this.none(`No architecture graph at the fork point ${short} (first adoption), so there is nothing to compare edges against`, short);
223
+ const live = await source.graphs(workspaceRoot);
224
+ const dependencyReason = baseProjects === null ? `No ${graph_loader_1.DEFAULT_GRAPH_PATH} at the fork point ${short}` : '';
225
+ const runtimeReason = baseRuntime === null ? `No ${runtime_graph_io_1.DEFAULT_RUNTIME_GRAPH_PATH} at the fork point ${short}` : live.runtimeReason;
226
+ return new EdgeDiffReport(short, dependencyReason, baseProjects === null ? [] : this.differ.dependency(baseProjects, live.projects), runtimeReason, baseRuntime === null || live.runtime === null ? [] : this.differ.runtime(baseRuntime, live.runtime));
227
+ }
228
+ catch (err) {
229
+ const error = (0, toError_1.toError)(err);
230
+ return this.none(`Edge changes unavailable: ${error.message}`, short);
231
+ }
232
+ }
233
+ /** No edge changes to show, with the one reason the page says instead. */
234
+ none(reason, base = '') {
235
+ return new EdgeDiffReport(base, reason, [], reason, []);
236
+ }
237
+ /** dependencies.json's `projects` at the fork point; null when absent or not that shape. */
238
+ projectsAt(workspaceRoot, sha) {
239
+ const parsed = this.parse(this.git.read(workspaceRoot, sha, graph_loader_1.DEFAULT_GRAPH_PATH));
240
+ const projects = parsed === null ? undefined : parsed.projects;
241
+ return projects !== undefined && typeof projects === 'object' && projects !== null ? projects : null;
242
+ }
243
+ runtimeAt(workspaceRoot, sha) {
244
+ const parsed = this.parse(this.git.read(workspaceRoot, sha, runtime_graph_io_1.DEFAULT_RUNTIME_GRAPH_PATH));
245
+ return parsed !== null && Array.isArray(parsed.runtimeEdges) ? parsed : null;
246
+ }
247
+ // webpieces-disable no-any-unknown -- committed JSON from another commit is untrusted until narrowed by the caller
248
+ parse(text) {
249
+ if (text === null)
250
+ return null;
251
+ // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- an unreadable file at the fork point is the same as no file there
252
+ try {
253
+ return JSON.parse(text);
254
+ }
255
+ catch (err) {
256
+ const error = (0, toError_1.toError)(err);
257
+ void error; // not JSON at the fork point: treated as absent
258
+ return null;
259
+ }
260
+ }
261
+ }
262
+ exports.EdgeChangeScanner = EdgeChangeScanner;
263
+ //# sourceMappingURL=graph-edge-diff.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"graph-edge-diff.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/graph-edge-diff.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Edge changes against the base (#1264): which dependency and runtime edges this branch GAINED or\n * LOST, so \"why is this building?\" is answerable for a project whose only change is an edge.\n *\n * BASE is the FORK POINT — the branch comparison's `git merge-base HEAD origin/main` — read as\n * `git show <forkPoint>:architecture/dependencies.json` and `…:architecture/runtime-dependencies.json`.\n * Main keeps both regenerated and validated (`validate-architecture-unchanged`), so the fork point's\n * copies are trustworthy.\n *\n * CURRENT is the graph computed LIVE when the page is refreshed (`LiveGraphSource`: the same pipeline\n * `architecture:generate` runs), NEVER HEAD's committed architecture/*.json. Whether or not the branch\n * already regenerated and committed those files must not matter: comparing against HEAD's committed\n * copy would show no change at all once it had been regenerated.\n *\n * Neither file at the fork point (first adoption) is an answer, not an error: no edge diff, and the\n * page says why. Nothing here ever throws into generation.\n */\n\nimport { spawnSync } from 'child_process';\nimport { DEFAULT_GRAPH_PATH } from './graph-loader';\nimport type { EnhancedGraph } from './graph-sorter';\nimport { DEFAULT_RUNTIME_GRAPH_PATH } from './runtime-graph-io';\nimport type { RuntimeGraph } from './runtime-graph-model';\nimport { deriveRuntimeGraphReport } from './runtime-graph';\nimport { ApiContractFiles } from './api-contract-files';\nimport { CurrentGraphBuilder } from './current-architecture';\nimport { toError } from '../toError';\n\nexport enum EdgeChangeKind {\n GAINED = 'gained',\n LOST = 'lost',\n}\n\nexport enum EdgeRelation {\n /** A build dependency (architecture/dependencies.json `dependsOn`). */\n DEPENDS = 'depends',\n /** A runtime call (architecture/runtime-dependencies.json `runtimeEdges`). */\n CALLS = 'calls',\n}\n\n/** One gained or lost edge, `from` → `to` (node ids), with the words the page shows for it. */\nexport class EdgeChange {\n constructor(\n public readonly from: string,\n public readonly to: string,\n public readonly change: EdgeChangeKind,\n public readonly relation: EdgeRelation,\n /** CALLS: the APIs gained or lost on this edge, sorted. Empty for DEPENDS. */\n public readonly apis: string[],\n /** \"new on this branch: a now depends on b\" / \"a no longer calls b (SaveApi)\". */\n public readonly text: string,\n ) {}\n}\n\n/** The edge changes of the dependency graph and of the runtime graph, or why each is not shown. */\nexport class EdgeDiffReport {\n constructor(\n /** The fork point's short sha ('' when there is none). */\n public readonly base: string,\n /** '' when `dependency` was computed; otherwise why the page shows no dependency edge changes. */\n public readonly dependencyReason: string,\n public readonly dependency: EdgeChange[],\n /** '' when `runtime` was computed; otherwise why the page shows no runtime edge changes. */\n public readonly runtimeReason: string,\n public readonly runtime: EdgeChange[],\n ) {}\n}\n\n/** The one field of a committed dependencies.json the base side reads. */\ninterface SavedProjectsFile {\n projects?: EnhancedGraph;\n}\n\n/** The two graphs the branch has NOW, computed live. `runtime` null when it could not be derived. */\nexport class LiveGraphs {\n constructor(\n public readonly projects: EnhancedGraph,\n public readonly runtime: RuntimeGraph | null,\n /** Why `runtime` is null ('' otherwise). */\n public readonly runtimeReason: string = '',\n ) {}\n}\n\n/**\n * Computes the live graphs: `CurrentGraphBuilder` (generate's own pipeline, writing nothing) and the\n * runtime graph derived from it exactly as generate derives it. `architecture:generate` passes the\n * graphs it just built instead (`FixedLiveGraphs`), so nothing is computed twice.\n */\nexport class LiveGraphSource {\n async graphs(workspaceRoot: string): Promise<LiveGraphs> {\n const current = await new CurrentGraphBuilder().build(workspaceRoot);\n const hidden = new Set<string>(Object.keys(current.graph).filter((name: string): boolean => current.graph[name].drawOnGraph === false));\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- the runtime half is optional; the dependency half still shows\n try {\n const contracts = new ApiContractFiles().load(workspaceRoot, DEFAULT_GRAPH_PATH, current.apiContractFiles);\n const report = deriveRuntimeGraphReport(current.graph, hidden, contracts, current.externalSystems, current.remoteServices);\n return new LiveGraphs(current.graph, report.graph);\n } catch (err: unknown) {\n const error = toError(err);\n return new LiveGraphs(current.graph, null, `the live runtime graph could not be derived (${error.message})`);\n }\n }\n}\n\n/** The graphs a caller already holds (architecture:generate just built them). */\nexport class FixedLiveGraphs extends LiveGraphSource {\n constructor(private readonly live: LiveGraphs) {\n super();\n }\n\n override async graphs(): Promise<LiveGraphs> {\n return this.live;\n }\n}\n\n/** A committed file as it was at a commit. Injected so specs never spawn git. */\nexport class GitFileAt {\n /** The file's text at `sha`; null when it did not exist there (or git cannot answer). */\n read(workspaceRoot: string, sha: string, file: string): string | null {\n const shown = spawnSync('git', ['show', `${sha}:${file}`], {\n cwd: workspaceRoot,\n encoding: 'utf8',\n stdio: 'pipe',\n maxBuffer: 64 * 1024 * 1024,\n });\n return shown.status === 0 ? shown.stdout : null;\n }\n}\n\n/** The base graphs' edges: `from|to` → the APIs on that edge (empty for a dependency). */\nclass EdgeSet {\n readonly edges = new Map<string, Set<string>>();\n\n add(from: string, to: string, apis: string[]): void {\n const key = `${from}\\u0000${to}`;\n const known = this.edges.get(key) ?? new Set<string>();\n for (const api of apis) known.add(api);\n this.edges.set(key, known);\n }\n}\n\n/** Diffs base against live; pure. */\nexport class EdgeDiffer {\n dependency(base: EnhancedGraph, live: EnhancedGraph): EdgeChange[] {\n return this.compare(this.dependencyEdges(base), this.dependencyEdges(live), EdgeRelation.DEPENDS);\n }\n\n runtime(base: RuntimeGraph, live: RuntimeGraph): EdgeChange[] {\n return this.compare(this.runtimeEdges(base), this.runtimeEdges(live), EdgeRelation.CALLS);\n }\n\n private dependencyEdges(graph: EnhancedGraph): EdgeSet {\n const set = new EdgeSet();\n for (const name of Object.keys(graph)) for (const dep of graph[name].dependsOn ?? []) set.add(name, dep, []);\n return set;\n }\n\n private runtimeEdges(graph: RuntimeGraph): EdgeSet {\n const set = new EdgeSet();\n for (const edge of graph.runtimeEdges ?? []) set.add(edge.from, edge.to, edge.via ?? []);\n return set;\n }\n\n /** Per edge: gained when absent at base; lost when absent now; for a call, the APIs that came or went. */\n private compare(base: EdgeSet, live: EdgeSet, relation: EdgeRelation): EdgeChange[] {\n const changes: EdgeChange[] = [];\n const keys = [...new Set<string>([...base.edges.keys(), ...live.edges.keys()])].sort();\n for (const key of keys) {\n const ends = key.split('\\u0000');\n const before = base.edges.get(key);\n const after = live.edges.get(key);\n const gained = after === undefined ? [] : [...after].filter((api: string): boolean => before === undefined || !before.has(api));\n const lost = before === undefined ? [] : [...before].filter((api: string): boolean => after === undefined || !after.has(api));\n if (before === undefined || gained.length > 0)\n changes.push(this.change(ends[0], ends[1], EdgeChangeKind.GAINED, relation, gained.sort()));\n if (after === undefined || lost.length > 0)\n changes.push(this.change(ends[0], ends[1], EdgeChangeKind.LOST, relation, lost.sort()));\n }\n return changes;\n }\n\n private change(from: string, to: string, kind: EdgeChangeKind, relation: EdgeRelation, apis: string[]): EdgeChange {\n const verb = relation === EdgeRelation.DEPENDS ? 'depends on' : 'calls';\n const via = apis.length === 0 ? '' : ` (${apis.join(', ')})`;\n const text =\n kind === EdgeChangeKind.GAINED\n ? `new on this branch: ${from} now ${verb} ${to}${via}`\n : `${from} no longer ${verb} ${to}${via}`;\n return new EdgeChange(from, to, kind, relation, apis, text);\n }\n}\n\n/** Reads the fork point's committed graphs and diffs them against the live ones. Never throws. */\nexport class EdgeChangeScanner {\n constructor(\n private readonly git: GitFileAt = new GitFileAt(),\n private readonly differ: EdgeDiffer = new EdgeDiffer(),\n ) {}\n\n async scan(workspaceRoot: string, forkPoint: string | null, source: LiveGraphSource): Promise<EdgeDiffReport> {\n if (forkPoint === null)\n return this.none('Edge changes compare a branch with its fork point; there is no branch comparison here (main or a detached HEAD)');\n const short = forkPoint.slice(0, 7);\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- edge changes are optional; a failure becomes the reason the page shows\n try {\n const baseProjects = this.projectsAt(workspaceRoot, forkPoint);\n const baseRuntime = this.runtimeAt(workspaceRoot, forkPoint);\n if (baseProjects === null && baseRuntime === null)\n return this.none(`No architecture graph at the fork point ${short} (first adoption), so there is nothing to compare edges against`, short);\n const live = await source.graphs(workspaceRoot);\n const dependencyReason = baseProjects === null ? `No ${DEFAULT_GRAPH_PATH} at the fork point ${short}` : '';\n const runtimeReason = baseRuntime === null ? `No ${DEFAULT_RUNTIME_GRAPH_PATH} at the fork point ${short}` : live.runtimeReason;\n return new EdgeDiffReport(\n short,\n dependencyReason,\n baseProjects === null ? [] : this.differ.dependency(baseProjects, live.projects),\n runtimeReason,\n baseRuntime === null || live.runtime === null ? [] : this.differ.runtime(baseRuntime, live.runtime),\n );\n } catch (err: unknown) {\n const error = toError(err);\n return this.none(`Edge changes unavailable: ${error.message}`, short);\n }\n }\n\n /** No edge changes to show, with the one reason the page says instead. */\n private none(reason: string, base = ''): EdgeDiffReport {\n return new EdgeDiffReport(base, reason, [], reason, []);\n }\n\n /** dependencies.json's `projects` at the fork point; null when absent or not that shape. */\n private projectsAt(workspaceRoot: string, sha: string): EnhancedGraph | null {\n const parsed = this.parse(this.git.read(workspaceRoot, sha, DEFAULT_GRAPH_PATH));\n const projects = parsed === null ? undefined : (parsed as SavedProjectsFile).projects;\n return projects !== undefined && typeof projects === 'object' && projects !== null ? projects : null;\n }\n\n private runtimeAt(workspaceRoot: string, sha: string): RuntimeGraph | null {\n const parsed = this.parse(this.git.read(workspaceRoot, sha, DEFAULT_RUNTIME_GRAPH_PATH));\n return parsed !== null && Array.isArray((parsed as RuntimeGraph).runtimeEdges) ? (parsed as RuntimeGraph) : null;\n }\n\n // webpieces-disable no-any-unknown -- committed JSON from another commit is untrusted until narrowed by the caller\n private parse(text: string | null): unknown {\n if (text === null) return null;\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- an unreadable file at the fork point is the same as no file there\n try {\n return JSON.parse(text);\n } catch (err: unknown) {\n const error = toError(err);\n void error; // not JSON at the fork point: treated as absent\n return null;\n }\n }\n}\n"]}
@@ -0,0 +1,160 @@
1
+ /**
2
+ * WHY nx put each project into the affected set (#1264): the Impact view never claims a reason the
3
+ * graph cannot show.
4
+ *
5
+ * `nx show projects --affected` answers WHICH projects build; it does not say why. Painting every
6
+ * affected project that owns no changed file as "dependent: uses a changed project" was false for the
7
+ * projects nx marked through an IMPLICIT touch — a `{workspaceRoot}/...` target input matching a
8
+ * changed file — or through a ROOT FILE (a lockfile, package.json, tsconfig.base.json, nx.json). Those
9
+ * projects use no changed project, so the graph around them shows nothing changed and the view looked
10
+ * broken.
11
+ *
12
+ * So every affected project is classified by nx's OWN locators — the exact functions `filterAffected`
13
+ * runs (nx/src/project-graph/affected/affected-project-graph.js) — never a re-derivation of them:
14
+ *
15
+ * | class | nx locator | what the view says |
16
+ * |---|---|---|
17
+ * | changed | `getTouchedProjects` (workspace-projects) | the changed files it owns |
18
+ * | root-file touched | `nx.json` via `getImplicitlyTouchedProjects`; lockfile, package.json and tsconfig.base.json via the js plugin's locators; a deleted project config via `getTouchedProjectsFromProjectGlobChanges` | the root file, and which locator |
19
+ * | implicitly touched | `getImplicitlyTouchedProjects` (a `{workspaceRoot}` input glob) | the target, the glob, and the changed file it matched |
20
+ * | dependent | none: reached from one of the above over nx's REVERSED dependency edges, exactly as `filterAffectedProjects` walks them | the edge path to the nearest one (`a -> b -> c`) |
21
+ * | unexplained | — | nx marked it and nothing here explains it; said out loud, never guessed |
22
+ *
23
+ * A project with several reasons gets the first class in that table's order (changed, root-file,
24
+ * implicit, dependent). The locators are loaded from the WORKSPACE's own nx (the one that produced the
25
+ * affected set), resolved the way node resolves it from the workspace root. When they cannot be
26
+ * loaded, every affected project is "unexplained" with the load error as its reason — never a
27
+ * dependent.
28
+ */
29
+ import type { NxJsonConfiguration, ProjectGraph } from '@nx/devkit';
30
+ import type { FileChange } from 'nx/src/project-graph/file-utils';
31
+ import type { PackageJson } from 'nx/src/utils/package-json';
32
+ import type { TouchedProjectLocator } from 'nx/src/project-graph/affected/affected-project-graph-models';
33
+ /** The class a project's colour and click explanation come from. Values are the page's status keys. */
34
+ export declare enum AffectedClass {
35
+ CHANGED = "touched",
36
+ DEPENDENT = "dependent",
37
+ IMPLICIT = "implicit",
38
+ ROOT_FILE = "rootFile",
39
+ UNEXPLAINED = "unexplained"
40
+ }
41
+ /** Why one affected project is in the build. Exactly one per affected project. */
42
+ export declare class AffectedReason {
43
+ /** The node id (project name). */
44
+ readonly project: string;
45
+ readonly kind: AffectedClass;
46
+ /** CHANGED: every changed file it owns. IMPLICIT / ROOT_FILE: the changed file that matched. */
47
+ readonly files: string[];
48
+ /** DEPENDENT: `[project, …, the nearest changed or touched project]`, following dependency edges. */
49
+ readonly path: string[];
50
+ /** IMPLICIT: the target whose inputs matched. */
51
+ readonly target: string;
52
+ /** IMPLICIT: the input glob as written, e.g. `{workspaceRoot}/libs/shared/wiring.ts`. */
53
+ readonly input: string;
54
+ /** One sentence for the click panel. */
55
+ readonly detail: string;
56
+ constructor(
57
+ /** The node id (project name). */
58
+ project: string, kind: AffectedClass,
59
+ /** CHANGED: every changed file it owns. IMPLICIT / ROOT_FILE: the changed file that matched. */
60
+ files: string[],
61
+ /** DEPENDENT: `[project, …, the nearest changed or touched project]`, following dependency edges. */
62
+ path: string[],
63
+ /** IMPLICIT: the target whose inputs matched. */
64
+ target: string,
65
+ /** IMPLICIT: the input glob as written, e.g. `{workspaceRoot}/libs/shared/wiring.ts`. */
66
+ input: string,
67
+ /** One sentence for the click panel. */
68
+ detail: string);
69
+ }
70
+ /** The six locators `filterAffected` runs, as functions. Injected so specs pass nx's own imports. */
71
+ export declare class NxAffectedLocators {
72
+ readonly touched: TouchedProjectLocator;
73
+ readonly implicit: TouchedProjectLocator;
74
+ readonly projectGlobs: TouchedProjectLocator;
75
+ readonly lockFile: TouchedProjectLocator;
76
+ readonly npmPackages: TouchedProjectLocator;
77
+ readonly tsConfig: TouchedProjectLocator;
78
+ constructor(touched: TouchedProjectLocator, implicit: TouchedProjectLocator, projectGlobs: TouchedProjectLocator, lockFile: TouchedProjectLocator, npmPackages: TouchedProjectLocator, tsConfig: TouchedProjectLocator);
79
+ }
80
+ /** Everything the classification reads; built from the workspace's nx, or by hand in a spec. */
81
+ export declare class AffectedInputs {
82
+ readonly graph: ProjectGraph;
83
+ readonly nxJson: NxJsonConfiguration;
84
+ readonly packageJson: PackageJson;
85
+ readonly changes: FileChange[];
86
+ /** nx's affected set (project names), from `nx show projects --affected`. */
87
+ readonly affected: string[];
88
+ constructor(graph: ProjectGraph, nxJson: NxJsonConfiguration, packageJson: PackageJson, changes: FileChange[],
89
+ /** nx's affected set (project names), from `nx show projects --affected`. */
90
+ affected: string[]);
91
+ }
92
+ /** The classification of one scan. */
93
+ export declare class AffectedClassification {
94
+ /** One per affected project, sorted by project. */
95
+ readonly reasons: AffectedReason[];
96
+ /** Changed files no project owns (sorted): the workspace-global inputs. */
97
+ readonly globalFiles: string[];
98
+ constructor(
99
+ /** One per affected project, sorted by project. */
100
+ reasons: AffectedReason[],
101
+ /** Changed files no project owns (sorted): the workspace-global inputs. */
102
+ globalFiles: string[]);
103
+ of(kind: AffectedClass): string[];
104
+ }
105
+ /** Classifies an affected set with nx's own locators. Pure given its inputs; spawns nothing itself. */
106
+ export declare class AffectedClassifier {
107
+ private readonly locators;
108
+ constructor(locators: NxAffectedLocators);
109
+ classify(inputs: AffectedInputs): Promise<AffectedClassification>;
110
+ private isOwned;
111
+ /** project → the changed files it owns, by nx's own file → project locator. */
112
+ private owned;
113
+ /**
114
+ * nx.json, the lockfile, package.json, tsconfig.base.json and a deleted project config. A locator
115
+ * that answers even with NO change (nx.json's own `projectsAffectedByDependencyUpdates` list) is
116
+ * named as that configuration, never as a file.
117
+ */
118
+ private rootFiles;
119
+ private rootFileLocators;
120
+ private run;
121
+ /** `{workspaceRoot}` input globs: which target, which glob, which changed file. */
122
+ private implicit;
123
+ /**
124
+ * Which target, input and file: each candidate input is handed to nx's OWN implicit locator, alone,
125
+ * on a copy of the project carrying only that target, so the matcher is always nx's. Named inputs
126
+ * are opened (nx.json's merged with the project's, as the locator merges them).
127
+ */
128
+ private explainImplicit;
129
+ /** Every input, with named inputs opened (cycle-safe). */
130
+ private leaves;
131
+ private fileset;
132
+ private probe;
133
+ /**
134
+ * Dependents: a breadth-first walk over the REVERSED dependency edges from every changed and
135
+ * touched project (and every npm package a root file touched), the walk nx's own
136
+ * `filterAffectedProjects` does — so the first seed reached is the NEAREST one. A project one edge
137
+ * from a touched npm package is root-file touched itself ("uses npm:x: package.json changed").
138
+ */
139
+ private dependents;
140
+ /** For each project reachable from `starts` over reversed edges, the next hop toward the nearest start. */
141
+ private nearest;
142
+ private dependent;
143
+ unexplained(project: string, why?: string): AffectedReason;
144
+ /** First reason wins (changed, root-file, implicit, dependent); only affected projects count. */
145
+ private seed;
146
+ private reason;
147
+ }
148
+ /** The workspace's own nx, loaded the way node resolves it from the workspace root. */
149
+ export declare class WorkspaceNx {
150
+ private readonly workspaceRoot;
151
+ constructor(workspaceRoot: string);
152
+ locators(): NxAffectedLocators;
153
+ /** nx's own FileChange list: base → working tree (no head), as `nx affected` builds it. */
154
+ fileChanges(files: string[], base: string): FileChange[];
155
+ nxJson(): NxJsonConfiguration;
156
+ packageJson(): PackageJson;
157
+ /** A file at `revision`, or in the working tree with none; '' when it does not exist there. */
158
+ private readAt;
159
+ private load;
160
+ }