@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
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  /**
3
- * Impact mode's data: what one COMPARISON's build looks like, in three shades (plus untouched grey).
3
+ * Impact mode's data: what one COMPARISON's build looks like, and WHY each project is in it.
4
4
  *
5
5
  * There are two comparisons, and only two (#1163). Both end at the WORKING TREE, so uncommitted and
6
6
  * untracked work counts in either:
@@ -26,15 +26,13 @@
26
26
  * - AFFECTED is `nx show projects --affected --base=<the comparison's base> --json`, with NO
27
27
  * `--head`, so uncommitted and untracked work counts. nx already gets right everything a
28
28
  * hand-rolled diff gets wrong: lockfile and package.json changes resolved per project, root/global
29
- * inputs, namedInputs, implicitDependencies, nested roots and deleted files. This uses its public
30
- * CLI surface only, never nx internals, and never NX_BASE/NX_HEAD, whose value depends on whoever
31
- * launched the process.
32
- * - TOUCHED is the subset of that set which OWNS a changed file, by nx's own file-to-project
33
- * ownership (the project file map from `createProjectFileMapUsingProjectGraph`, public devkit API)
34
- * — never a path-prefix guess. The ownership map is computed ONCE and shared by every comparison.
35
- * A project whose only change is a DELETED file is not in that map (the file is gone), so it shows
36
- * as affected rather than touched. The changed-file list is `DiffScope.getChangedFiles`, the same
37
- * base→working-tree diff (with untracked files) every code rule uses.
29
+ * inputs, namedInputs, implicitDependencies, nested roots and deleted files. It never reads
30
+ * NX_BASE/NX_HEAD, whose value depends on whoever launched the process.
31
+ * - WHY each affected project is in it (#1264) — changed, dependent (with the edge path), implicitly
32
+ * touched (with the target, glob and file) or root-file touched — comes from nx's OWN affected
33
+ * locators, run in-process against the workspace's own nx (graph-impact-reasons.ts). They are
34
+ * loaded ONCE and shared by every comparison. The changed-file list is `DiffScope.getChangedFiles`,
35
+ * the same base→working-tree diff (with untracked files) every code rule uses.
38
36
  *
39
37
  * The answer changes per checkout, so it never goes into the committed dependencies.html or
40
38
  * dependencies.json (both stay byte-stable). It goes into a SIDECAR,
@@ -54,13 +52,15 @@
54
52
  * generation.
55
53
  */
56
54
  Object.defineProperty(exports, "__esModule", { value: true });
57
- exports.ImpactRefresh = exports.ImpactSidecar = exports.NxImpactScanner = exports.SharedOwners = exports.ProjectFileOwners = exports.NxCli = exports.ImpactScopeResolver = exports.ImpactGit = exports.ImpactPlan = exports.ImpactComparison = exports.ImpactUnavailable = exports.ImpactSidecarData = exports.ImpactReport = exports.GENERATE_IMPACT_KINDS = exports.VISUALIZE_IMPACT_KINDS = exports.ImpactKind = exports.UNCOMMITTED_SUFFIX = exports.ROOT_COMMIT_REASON = exports.NOTHING_CHANGED_ON_BRANCH = exports.IMPACT_SIDECAR_SRC = exports.IMPACT_SIDECAR_FILE = exports.IMPACT_SIDECAR_DIR = void 0;
55
+ exports.ImpactRefresh = exports.ImpactSidecar = exports.NxImpactScanner = exports.SharedContext = exports.AffectedContextLoader = exports.AffectedContext = exports.NxCli = exports.ImpactScopeResolver = exports.ImpactGit = exports.ImpactPlan = exports.ImpactComparison = exports.ImpactUnavailable = exports.ImpactSidecarData = exports.ImpactCounts = exports.ImpactReport = exports.GENERATE_IMPACT_KINDS = exports.VISUALIZE_IMPACT_KINDS = exports.ImpactKind = exports.UNCOMMITTED_SUFFIX = exports.ROOT_COMMIT_REASON = exports.NOTHING_CHANGED_ON_BRANCH = exports.IMPACT_SIDECAR_SRC = exports.IMPACT_SIDECAR_FILE = exports.IMPACT_SIDECAR_DIR = void 0;
58
56
  const tslib_1 = require("tslib");
59
57
  const child_process_1 = require("child_process");
60
58
  const fs = tslib_1.__importStar(require("fs"));
61
59
  const path = tslib_1.__importStar(require("path"));
62
60
  const devkit_1 = require("@nx/devkit");
63
61
  const rules_config_1 = require("@webpieces/rules-config");
62
+ const graph_impact_reasons_1 = require("./graph-impact-reasons");
63
+ const graph_edge_diff_1 = require("./graph-edge-diff");
64
64
  const graph_names_1 = require("./graph-names");
65
65
  const toError_1 = require("../toError");
66
66
  /** Relative to the architecture/ directory the page lives in; the page's `<script src>`. */
@@ -95,11 +95,15 @@ class ImpactReport {
95
95
  reason;
96
96
  base;
97
97
  touched;
98
- affected;
98
+ dependents;
99
+ implicit;
100
+ rootFile;
101
+ unexplained;
99
102
  buildInputs;
100
103
  changedFiles;
101
104
  dependencies;
102
105
  globalFiles;
106
+ reasons;
103
107
  constructor(kind,
104
108
  /** What the page says this scan compares, e.g. "Changed on this branch (since abc1234)". */
105
109
  label,
@@ -109,10 +113,16 @@ class ImpactReport {
109
113
  reason,
110
114
  /** The base's short sha ('' when there is none). */
111
115
  base,
112
- /** Affected AND owning a changed file (node ids). */
116
+ /** Affected AND owning a changed file (node ids): "changed". */
113
117
  touched,
114
- /** Affected but owning no changed file (node ids): their ci re-runs. */
115
- affected,
118
+ /** Affected through dependency edges from a changed or touched project (node ids). */
119
+ dependents,
120
+ /** Affected because a `{workspaceRoot}` target input matched a changed file (node ids). */
121
+ implicit,
122
+ /** Affected by a lockfile, package.json, tsconfig.base.json, nx.json or deleted-config change (node ids). */
123
+ rootFile,
124
+ /** Affected, and nothing here could say why (node ids). Never shown as a dependent. */
125
+ unexplained,
116
126
  /**
117
127
  * Transitive dependencies of the affected set that are NOT affected (node ids). `ci` dependsOn
118
128
  * `^build`, so they must exist as build output although their inputs did not change: nx
@@ -122,16 +132,16 @@ class ImpactReport {
122
132
  buildInputs, changedFiles,
123
133
  /**
124
134
  * Transitive dependencies of the TOUCHED set that are not touched themselves (node ids): the
125
- * Filter popover's "Changed + dependencies". ("Changed + dependents" is touched + affected,
126
- * which nx already computed as the dependents whose tests re-run.)
135
+ * Filter popover's "Changed + dependencies". ("Changed + dependents" is every affected project.)
127
136
  */
128
137
  dependencies,
129
138
  /**
130
139
  * Changed files no project owns (repo-relative, sorted): workspace-global inputs such as
131
- * pnpm-lock.yaml. When nothing is touched they are WHY everything is affected, and the
132
- * legend names them. Empty when nx's file ownership was unavailable — then nothing is known.
140
+ * pnpm-lock.yaml. Empty when nx's locators were unavailable — then nothing is known.
133
141
  */
134
- globalFiles) {
142
+ globalFiles,
143
+ /** WHY each affected project is in the build (graph-impact-reasons.ts): one per affected node. */
144
+ reasons) {
135
145
  this.kind = kind;
136
146
  this.label = label;
137
147
  this.dirty = dirty;
@@ -139,21 +149,56 @@ class ImpactReport {
139
149
  this.reason = reason;
140
150
  this.base = base;
141
151
  this.touched = touched;
142
- this.affected = affected;
152
+ this.dependents = dependents;
153
+ this.implicit = implicit;
154
+ this.rootFile = rootFile;
155
+ this.unexplained = unexplained;
143
156
  this.buildInputs = buildInputs;
144
157
  this.changedFiles = changedFiles;
145
158
  this.dependencies = dependencies;
146
159
  this.globalFiles = globalFiles;
160
+ this.reasons = reasons;
147
161
  }
148
162
  }
149
163
  exports.ImpactReport = ImpactReport;
164
+ /** The one-line count split, shared by the executor's console line (the page builds the same line). */
165
+ class ImpactCounts {
166
+ /** `8 changed · 3 dependents · 22 implicitly touched (runtime-wiring-check inputs) · 47 dependencies`. */
167
+ line(report) {
168
+ const parts = [`${report.touched.length} changed`, `${report.dependents.length} dependents`];
169
+ if (report.implicit.length > 0)
170
+ parts.push(`${report.implicit.length} implicitly touched${this.targets(report)}`);
171
+ if (report.rootFile.length > 0)
172
+ parts.push(`${report.rootFile.length} root-file touched`);
173
+ if (report.unexplained.length > 0)
174
+ parts.push(`${report.unexplained.length} unexplained`);
175
+ parts.push(`${report.buildInputs.length} dependencies`);
176
+ return parts.join(' · ');
177
+ }
178
+ targets(report) {
179
+ const targets = new Set();
180
+ for (const reason of report.reasons)
181
+ if (reason.kind === graph_impact_reasons_1.AffectedClass.IMPLICIT && reason.target !== '')
182
+ targets.add(reason.target);
183
+ return targets.size === 0 ? '' : ` (${[...targets].sort().join(', ')} inputs)`;
184
+ }
185
+ }
186
+ exports.ImpactCounts = ImpactCounts;
150
187
  /** The sidecar's one global: every scan, and the one the page shows first. */
151
188
  class ImpactSidecarData {
152
189
  scans;
153
190
  defaultKind;
154
- constructor(scans, defaultKind) {
191
+ edges;
192
+ constructor(scans, defaultKind,
193
+ /**
194
+ * The edges this branch gained or lost since its FORK POINT, the live graph against the fork
195
+ * point's committed one (graph-edge-diff.ts, #1264). One per sidecar: it does not depend on
196
+ * which scan is shown.
197
+ */
198
+ edges) {
155
199
  this.scans = scans;
156
200
  this.defaultKind = defaultKind;
201
+ this.edges = edges;
157
202
  }
158
203
  }
159
204
  exports.ImpactSidecarData = ImpactSidecarData;
@@ -311,28 +356,50 @@ class NxCli {
311
356
  }
312
357
  }
313
358
  exports.NxCli = NxCli;
314
- /** nx's own file → project ownership, from the public devkit file map. Injected for specs. */
315
- class ProjectFileOwners {
316
- async owners() {
317
- const fileMap = await (0, devkit_1.createProjectFileMapUsingProjectGraph)(await (0, devkit_1.createProjectGraphAsync)());
318
- const owners = new Map();
319
- for (const project of Object.keys(fileMap)) {
320
- for (const data of fileMap[project])
321
- owners.set(data.file, project);
322
- }
323
- return owners;
359
+ /**
360
+ * The workspace's nx facts every comparison classifies against: its project graph (with npm nodes
361
+ * and every edge), nx.json, package.json, and nx's own affected locators. Subclassed by specs.
362
+ */
363
+ class AffectedContext {
364
+ graph;
365
+ nxJson;
366
+ packageJson;
367
+ classifier;
368
+ nx;
369
+ constructor(graph, nxJson, packageJson, classifier, nx) {
370
+ this.graph = graph;
371
+ this.nxJson = nxJson;
372
+ this.packageJson = packageJson;
373
+ this.classifier = classifier;
374
+ this.nx = nx;
375
+ }
376
+ /** nx's FileChange list for `files`, base → working tree. */
377
+ fileChanges(files, base) {
378
+ if (this.nx === null)
379
+ throw new Error('no workspace nx to read file changes with');
380
+ return this.nx.fileChanges(files, base);
381
+ }
382
+ }
383
+ exports.AffectedContext = AffectedContext;
384
+ /** Loads the workspace's nx facts. Injected so specs never load the real project graph. */
385
+ class AffectedContextLoader {
386
+ async load(workspaceRoot) {
387
+ const nx = new graph_impact_reasons_1.WorkspaceNx(workspaceRoot);
388
+ return new AffectedContext(await (0, devkit_1.createProjectGraphAsync)(), nx.nxJson(), nx.packageJson(), new graph_impact_reasons_1.AffectedClassifier(nx.locators()), nx);
324
389
  }
325
390
  }
326
- exports.ProjectFileOwners = ProjectFileOwners;
391
+ exports.AffectedContextLoader = AffectedContextLoader;
327
392
  /**
328
- * nx's ownership map, computed at most ONCE however many comparisons ask for it. Failing is not
329
- * fatal: the set then shows in one shade, all "affected".
393
+ * The context, loaded at most ONCE however many comparisons ask for it. Failing is not fatal: every
394
+ * affected project is then "unexplained", with this error as its reason — never a dependent.
330
395
  */
331
- class SharedOwners {
332
- ownership;
396
+ class SharedContext {
397
+ loader;
398
+ workspaceRoot;
333
399
  pending = null;
334
- constructor(ownership) {
335
- this.ownership = ownership;
400
+ constructor(loader, workspaceRoot) {
401
+ this.loader = loader;
402
+ this.workspaceRoot = workspaceRoot;
336
403
  }
337
404
  get() {
338
405
  if (this.pending === null)
@@ -340,60 +407,45 @@ class SharedOwners {
340
407
  return this.pending;
341
408
  }
342
409
  async load() {
343
- // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- the second shade is optional
410
+ // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- the reasons are optional; the affected set still shows
344
411
  try {
345
- return await this.ownership.owners();
412
+ return await this.loader.load(this.workspaceRoot);
346
413
  }
347
414
  catch (err) {
348
415
  const error = (0, toError_1.toError)(err);
349
- console.warn(`⚠️ Impact: nx file ownership unavailable (${error.message}); showing one shade`);
350
- return new Map();
416
+ console.warn(`⚠️ Impact: nx's affected locators unavailable (${error.message}); reasons unknown`);
417
+ return error;
351
418
  }
352
419
  }
353
420
  }
354
- exports.SharedOwners = SharedOwners;
355
- /** The sets one scan found; an empty one is still an answer. */
356
- class ImpactSets {
357
- touched;
358
- affected;
359
- buildInputs;
360
- dependencies;
361
- globalFiles;
362
- constructor(touched = [], affected = [], buildInputs = [], dependencies = [], globalFiles = []) {
363
- this.touched = touched;
364
- this.affected = affected;
365
- this.buildInputs = buildInputs;
366
- this.dependencies = dependencies;
367
- this.globalFiles = globalFiles;
368
- }
369
- }
421
+ exports.SharedContext = SharedContext;
370
422
  class NxImpactScanner {
371
423
  diffScope;
372
424
  nx;
373
- ownership;
425
+ loader;
374
426
  names = new graph_names_1.GraphNames();
375
- constructor(diffScope = new rules_config_1.DiffScope(), nx = new NxCli(), ownership = new ProjectFileOwners()) {
427
+ constructor(diffScope = new rules_config_1.DiffScope(), nx = new NxCli(), loader = new AffectedContextLoader()) {
376
428
  this.diffScope = diffScope;
377
429
  this.nx = nx;
378
- this.ownership = ownership;
430
+ this.loader = loader;
379
431
  }
380
- /** Every comparison, in order, sharing one ownership map. */
432
+ /** Every comparison, in order, sharing one nx context. */
381
433
  async scanAll(workspaceRoot, graph, comparisons) {
382
- const owners = new SharedOwners(this.ownership);
434
+ const context = new SharedContext(this.loader, workspaceRoot);
383
435
  const results = [];
384
436
  for (const comparison of comparisons)
385
- results.push(await this.scan(workspaceRoot, graph, comparison, owners));
437
+ results.push(await this.scan(workspaceRoot, graph, comparison, context));
386
438
  return results;
387
439
  }
388
440
  /** One comparison: nx affected from ITS base, with NO `--head`, so uncommitted work counts. */
389
- async scan(workspaceRoot, graph, comparison, owners = new SharedOwners(this.ownership)) {
441
+ async scan(workspaceRoot, graph, comparison, context = new SharedContext(this.loader, workspaceRoot)) {
390
442
  const base = comparison.base;
391
443
  if (base === null)
392
- return this.report(comparison, false, comparison.reason, '', 0, new ImpactSets());
444
+ return this.empty(comparison, false, comparison.reason, '', 0);
393
445
  const json = this.nx.run(workspaceRoot, ['show', 'projects', '--affected', `--base=${base}`, '--json'], NX_TIMEOUT_MS);
394
446
  if (json === null)
395
447
  return new ImpactUnavailable(`nx show projects --affected failed or took over ${NX_TIMEOUT_MS / 1000}s`, comparison.label);
396
- const affectedSet = this.parse(json, graph);
448
+ const affectedSet = this.parse(json);
397
449
  if (affectedSet === null)
398
450
  return new ImpactUnavailable('nx show projects --affected printed something other than a JSON list', comparison.label);
399
451
  const options = new rules_config_1.ChangedFilesOptions();
@@ -401,13 +453,26 @@ class NxImpactScanner {
401
453
  options.includeDeletions = true;
402
454
  const files = this.diffScope.getChangedFiles(workspaceRoot, base, undefined, options);
403
455
  const short = base.slice(0, 7);
404
- if (affectedSet.length === 0)
405
- return this.report(comparison, true, this.emptyNote(comparison, files.length, short), short, files.length, new ImpactSets());
406
- const map = await owners.get();
407
- const touched = this.touched(affectedSet, files, map);
408
- const rest = affectedSet.filter((project) => !touched.includes(project));
409
- const sets = new ImpactSets(this.ids(touched), this.ids(rest), this.ids(this.buildInputs(affectedSet, graph)), this.ids(this.buildInputs(touched, graph)), this.globalFiles(files, map));
410
- return this.report(comparison, true, '', short, files.length, sets);
456
+ const drawn = affectedSet.filter((project) => graph[project] !== undefined);
457
+ if (drawn.length === 0)
458
+ return this.empty(comparison, true, this.emptyNote(comparison, files.length, short), short, files.length);
459
+ const classification = await this.classify(await context.get(), files, base, affectedSet);
460
+ return this.report(comparison, short, files.length, graph, classification);
461
+ }
462
+ /** nx's locators' answer; any failure makes every affected project "unexplained", with the reason. */
463
+ async classify(context, files, base, affected) {
464
+ // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- the reasons are optional; a locator failure leaves them unknown
465
+ try {
466
+ if (context instanceof Error)
467
+ throw context;
468
+ const changes = context.fileChanges(files, base);
469
+ return await context.classifier.classify(new graph_impact_reasons_1.AffectedInputs(context.graph, context.nxJson, context.packageJson, changes, affected));
470
+ }
471
+ catch (err) {
472
+ const error = (0, toError_1.toError)(err);
473
+ const why = `nx's affected locators could not run here (${error.message}), so why nx marked it is unknown`;
474
+ return new graph_impact_reasons_1.AffectedClassification([...affected].sort().map((project) => new graph_impact_reasons_1.AffectedReason(project, graph_impact_reasons_1.AffectedClass.UNEXPLAINED, [], [], '', '', why)), []);
475
+ }
411
476
  }
412
477
  /**
413
478
  * An empty scan is still an ANSWER (available, zero sets): a fresh branch with a clean tree has
@@ -420,12 +485,6 @@ class NxImpactScanner {
420
485
  return `Nothing changed since ${short}`;
421
486
  return `Nothing affected since ${short}`;
422
487
  }
423
- /** Changed files no project owns; none are claimed when ownership itself was unavailable. */
424
- globalFiles(files, owners) {
425
- if (owners.size === 0)
426
- return [];
427
- return files.filter((file) => !owners.has(file)).sort();
428
- }
429
488
  /**
430
489
  * Every transitive dependency of `roots` that is not itself one of them: a pure downward walk over
431
490
  * the graph's own dependsOn edges, no nx call. Of the affected set these are the build inputs; of
@@ -449,27 +508,15 @@ class NxImpactScanner {
449
508
  }
450
509
  return inputs.sort();
451
510
  }
452
- /** Affected projects that own one of the changed files, by nx's own ownership. */
453
- touched(affected, files, owners) {
454
- const owning = new Set();
455
- for (const file of files) {
456
- const owner = owners.get(file);
457
- if (owner !== undefined)
458
- owning.add(owner);
459
- }
460
- return affected.filter((project) => owning.has(project));
461
- }
462
- /** The JSON list, narrowed to projects the graph draws; null when it is not a list of names. */
463
- parse(json, graph) {
511
+ /** The JSON list of project names, sorted; null when it is not a list of names. */
512
+ parse(json) {
464
513
  // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- untrusted CLI output; anything unparseable is reported as a reason
465
514
  try {
466
515
  // webpieces-disable no-any-unknown -- CLI JSON is untrusted until narrowed below
467
516
  const parsed = JSON.parse(json);
468
517
  if (!Array.isArray(parsed))
469
518
  return null;
470
- return parsed
471
- .filter((name) => typeof name === 'string' && graph[name] !== undefined)
472
- .sort();
519
+ return parsed.filter((name) => typeof name === 'string').sort();
473
520
  }
474
521
  catch (err) {
475
522
  const error = (0, toError_1.toError)(err);
@@ -477,8 +524,15 @@ class NxImpactScanner {
477
524
  return null;
478
525
  }
479
526
  }
480
- report(comparison, available, reason, base, changedFiles, sets) {
481
- return new ImpactReport(comparison.kind, comparison.label, comparison.dirty, available, reason, base, sets.touched, sets.affected, sets.buildInputs, changedFiles, sets.dependencies, sets.globalFiles);
527
+ /** The drawn projects only: a reason for a project the page does not draw has nowhere to show. */
528
+ report(comparison, short, changedFiles, graph, classification) {
529
+ const drawn = (projects) => projects.filter((project) => graph[project] !== undefined);
530
+ const touched = drawn(classification.of(graph_impact_reasons_1.AffectedClass.CHANGED));
531
+ const affected = drawn(classification.reasons.map((reason) => reason.project));
532
+ return new ImpactReport(comparison.kind, comparison.label, comparison.dirty, true, '', short, this.ids(touched), this.ids(drawn(classification.of(graph_impact_reasons_1.AffectedClass.DEPENDENT))), this.ids(drawn(classification.of(graph_impact_reasons_1.AffectedClass.IMPLICIT))), this.ids(drawn(classification.of(graph_impact_reasons_1.AffectedClass.ROOT_FILE))), this.ids(drawn(classification.of(graph_impact_reasons_1.AffectedClass.UNEXPLAINED))), this.ids(this.buildInputs(affected, graph)), changedFiles, this.ids(this.buildInputs(touched, graph)), classification.globalFiles, classification.reasons.filter((reason) => graph[reason.project] !== undefined));
533
+ }
534
+ empty(comparison, available, reason, base, changedFiles) {
535
+ return new ImpactReport(comparison.kind, comparison.label, comparison.dirty, available, reason, base, [], [], [], [], [], [], changedFiles, [], [], []);
482
536
  }
483
537
  ids(projects) {
484
538
  return projects.map((project) => this.names.getNodeId(project));
@@ -519,16 +573,19 @@ class ImpactRefresh {
519
573
  scanner;
520
574
  sidecar;
521
575
  resolver;
522
- constructor(scanner = new NxImpactScanner(), sidecar = new ImpactSidecar(), resolver = new ImpactScopeResolver()) {
576
+ edges;
577
+ constructor(scanner = new NxImpactScanner(), sidecar = new ImpactSidecar(), resolver = new ImpactScopeResolver(), edges = new graph_edge_diff_1.EdgeChangeScanner()) {
523
578
  this.scanner = scanner;
524
579
  this.sidecar = sidecar;
525
580
  this.resolver = resolver;
581
+ this.edges = edges;
526
582
  }
527
583
  /**
528
584
  * Scans the comparisons in `kinds` that exist here (`VISUALIZE_IMPACT_KINDS` /
529
- * `GENERATE_IMPACT_KINDS`). Never throws — Impact is optional, generation is not.
585
+ * `GENERATE_IMPACT_KINDS`), and the edge changes since the fork point against the LIVE graphs
586
+ * `live` computes. Never throws — Impact is optional, generation is not.
530
587
  */
531
- async run(architectureDir, workspaceRoot, graph, kinds) {
588
+ async run(architectureDir, workspaceRoot, graph, kinds, live) {
532
589
  const started = Date.now();
533
590
  // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- impact is optional; any failure becomes a reason, never a failed generation
534
591
  try {
@@ -546,8 +603,12 @@ class ImpactRefresh {
546
603
  this.sidecar.remove(architectureDir);
547
604
  return [...lines, `ℹ️ Impact disabled, no sidecar written (${cost})`].join('\n');
548
605
  }
549
- const file = this.sidecar.write(architectureDir, new ImpactSidecarData(scans, this.defaultKind(scans, plan.defaultKind)));
550
- return [...lines, file === null ? `⚠️ Impact sidecar not written (${cost})` : `→ ${file} (${cost})`].join('\n');
606
+ const fork = plan.comparisons.find((comparison) => comparison.kind === ImpactKind.BRANCH);
607
+ const edges = await this.edges.scan(workspaceRoot, fork?.base ?? null, live);
608
+ lines.push(this.edgeLine(edges));
609
+ const data = new ImpactSidecarData(scans, this.defaultKind(scans, plan.defaultKind), edges);
610
+ const file = this.sidecar.write(architectureDir, data);
611
+ return [...lines, file === null ? `⚠️ Impact sidecar not written (${Date.now() - started} ms)` : `→ ${file} (${Date.now() - started} ms)`].join('\n');
551
612
  }
552
613
  catch (err) {
553
614
  const error = (0, toError_1.toError)(err);
@@ -555,6 +616,17 @@ class ImpactRefresh {
555
616
  return `⚠️ Impact skipped: ${error.message}`;
556
617
  }
557
618
  }
619
+ /** `✅ Edges since abc1234: dependencies 1 gained · 1 lost; runtime 0 gained · 0 lost`, or why not. */
620
+ edgeLine(edges) {
621
+ const half = (name, reason, changes) => {
622
+ if (reason !== '')
623
+ return `${name}: ${reason}`;
624
+ const gained = changes.filter((change) => change.change === graph_edge_diff_1.EdgeChangeKind.GAINED).length;
625
+ return `${name} ${gained} gained · ${changes.length - gained} lost`;
626
+ };
627
+ const head = edges.base === '' ? 'ℹ️ Edges' : `✅ Edges since ${edges.base}`;
628
+ return `${head}: ${half('dependencies', edges.dependencyReason, edges.dependency)}; ${half('runtime', edges.runtimeReason, edges.runtime)}`;
629
+ }
558
630
  /** The plan's default when that scan is available, else the first available one. */
559
631
  defaultKind(scans, preferred) {
560
632
  const usable = scans.filter((scan) => scan.available);
@@ -571,8 +643,7 @@ class ImpactRefresh {
571
643
  return `ℹ️ Impact · ${result.label}: unavailable — ${result.reason}`;
572
644
  if (result.reason !== '')
573
645
  return `✅ Impact · ${result.label}: ${result.reason}`;
574
- return (`✅ Impact · ${result.label}: ${result.touched.length} changed · ${result.affected.length} dependents · ` +
575
- `${result.buildInputs.length} dependencies, from ${result.changedFiles} changed file(s)`);
646
+ return `✅ Impact · ${result.label}: ${new ImpactCounts().line(result)}, from ${result.changedFiles} changed file(s)`;
576
647
  }
577
648
  }
578
649
  exports.ImpactRefresh = ImpactRefresh;