nx 23.3.0-beta.4 → 23.3.0-beta.6

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 (113) hide show
  1. package/dist/nx-cloud-internals.d.ts +1 -0
  2. package/dist/nx-cloud-internals.js +9 -0
  3. package/dist/plugins/package-json.js +1 -1
  4. package/dist/src/command-line/affected/affected.js +51 -9
  5. package/dist/src/command-line/graph/graph.d.ts +31 -0
  6. package/dist/src/command-line/graph/graph.js +71 -17
  7. package/dist/src/command-line/release/publish.js +7 -11
  8. package/dist/src/command-line/release/utils/get-touched-projects-for-group.js +3 -3
  9. package/dist/src/command-line/run/run-one.js +5 -1
  10. package/dist/src/command-line/run-many/run-many.js +6 -12
  11. package/dist/src/command-line/show/projects.js +30 -1
  12. package/dist/src/config/task-graph.d.ts +1 -1
  13. package/dist/src/config/workspace-json-project-json.d.ts +21 -1
  14. package/dist/src/core/graph/main.js +1 -1
  15. package/dist/src/core/graph/runtime.js +1 -1
  16. package/dist/src/core/graph/styles.js +1 -1
  17. package/dist/src/daemon/client/client.d.ts +7 -2
  18. package/dist/src/daemon/client/client.js +15 -2
  19. package/dist/src/daemon/message-types/io-snapshot-version.d.ts +5 -0
  20. package/dist/src/daemon/message-types/io-snapshot-version.js +2 -0
  21. package/dist/src/daemon/message-types/select-affected-tasks.d.ts +18 -0
  22. package/dist/src/daemon/message-types/select-affected-tasks.js +11 -0
  23. package/dist/src/daemon/server/handle-hash-tasks.d.ts +2 -0
  24. package/dist/src/daemon/server/handle-hash-tasks.js +5 -3
  25. package/dist/src/daemon/server/handle-select-affected-tasks.d.ts +3 -0
  26. package/dist/src/daemon/server/handle-select-affected-tasks.js +23 -0
  27. package/dist/src/daemon/server/io-snapshots-state.d.ts +7 -0
  28. package/dist/src/daemon/server/io-snapshots-state.js +35 -0
  29. package/dist/src/daemon/server/planning-context.d.ts +8 -0
  30. package/dist/src/daemon/server/planning-context.js +18 -0
  31. package/dist/src/daemon/server/server.d.ts +1 -0
  32. package/dist/src/daemon/server/server.js +6 -0
  33. package/dist/src/executors/run-commands/run-commands.impl.d.ts +2 -1
  34. package/dist/src/executors/run-commands/run-commands.impl.js +19 -5
  35. package/dist/src/executors/run-commands/running-tasks.d.ts +5 -4
  36. package/dist/src/executors/run-commands/running-tasks.js +13 -13
  37. package/dist/src/hasher/create-task-hasher.d.ts +8 -1
  38. package/dist/src/hasher/create-task-hasher.js +13 -3
  39. package/dist/src/hasher/hash-plan-inspector.js +2 -2
  40. package/dist/src/hasher/hash-task.d.ts +2 -2
  41. package/dist/src/hasher/hash-task.js +7 -1
  42. package/dist/src/hasher/native-task-hasher-impl.d.ts +13 -5
  43. package/dist/src/hasher/native-task-hasher-impl.js +38 -15
  44. package/dist/src/hasher/task-hasher.d.ts +17 -9
  45. package/dist/src/hasher/task-hasher.js +16 -11
  46. package/dist/src/hasher/task-planning-context.d.ts +13 -0
  47. package/dist/src/hasher/task-planning-context.js +12 -0
  48. package/dist/src/io-snapshots/config.d.ts +23 -0
  49. package/dist/src/io-snapshots/config.js +32 -0
  50. package/dist/src/io-snapshots/fetch.d.ts +21 -0
  51. package/dist/src/io-snapshots/fetch.js +43 -0
  52. package/dist/src/io-snapshots/outputs.d.ts +12 -0
  53. package/dist/src/io-snapshots/outputs.js +33 -0
  54. package/dist/src/io-snapshots/overrides.d.ts +12 -0
  55. package/dist/src/io-snapshots/overrides.js +49 -0
  56. package/dist/src/io-snapshots/report.d.ts +13 -0
  57. package/dist/src/io-snapshots/report.js +68 -0
  58. package/dist/src/io-snapshots/store.d.ts +37 -0
  59. package/dist/src/io-snapshots/store.js +162 -0
  60. package/dist/src/native/index.d.ts +264 -2
  61. package/dist/src/native/native-bindings.js +10 -0
  62. package/dist/src/native/nx.wasm32-wasi.debug.wasm +0 -0
  63. package/dist/src/native/nx.wasm32-wasi.wasm +0 -0
  64. package/dist/src/native/transform-objects.d.ts +11 -2
  65. package/dist/src/native/transform-objects.js +46 -0
  66. package/dist/src/nx-cloud/update-manager.d.ts +3 -0
  67. package/dist/src/plugins/js/project-graph/affected/lock-file-changes.d.ts +10 -2
  68. package/dist/src/plugins/js/project-graph/affected/lock-file-changes.js +51 -29
  69. package/dist/src/plugins/js/project-graph/affected/npm-packages.d.ts +10 -2
  70. package/dist/src/plugins/js/project-graph/affected/npm-packages.js +32 -11
  71. package/dist/src/plugins/package-json/create-nodes.d.ts +2 -2
  72. package/dist/src/plugins/package-json/create-nodes.js +13 -13
  73. package/dist/src/project-graph/affected/affected-project-graph-models.d.ts +14 -0
  74. package/dist/src/project-graph/affected/affected-project-graph.js +5 -18
  75. package/dist/src/project-graph/affected/affected-projects.d.ts +7 -0
  76. package/dist/src/project-graph/affected/affected-projects.js +42 -0
  77. package/dist/src/project-graph/affected/affected-tasks.d.ts +90 -0
  78. package/dist/src/project-graph/affected/affected-tasks.js +214 -0
  79. package/dist/src/project-graph/file-utils.js +6 -2
  80. package/dist/src/project-graph/utils/project-configuration/target-merging.js +67 -0
  81. package/dist/src/project-graph/utils/project-configuration/target-normalization.js +83 -0
  82. package/dist/src/project-graph/utils/project-configuration/utils.d.ts +1 -0
  83. package/dist/src/project-graph/utils/project-configuration/utils.js +1 -0
  84. package/dist/src/tasks-runner/create-task-graph.d.ts +24 -1
  85. package/dist/src/tasks-runner/create-task-graph.js +56 -2
  86. package/dist/src/tasks-runner/forked-process-task-runner.js +3 -3
  87. package/dist/src/tasks-runner/init-tasks-runner.d.ts +7 -2
  88. package/dist/src/tasks-runner/init-tasks-runner.js +17 -6
  89. package/dist/src/tasks-runner/run-command.d.ts +41 -5
  90. package/dist/src/tasks-runner/run-command.js +84 -18
  91. package/dist/src/tasks-runner/task-io-service.d.ts +11 -3
  92. package/dist/src/tasks-runner/task-io-service.js +14 -5
  93. package/dist/src/tasks-runner/task-orchestrator.js +1 -1
  94. package/dist/src/tasks-runner/utils.d.ts +5 -0
  95. package/dist/src/tasks-runner/utils.js +9 -0
  96. package/dist/src/utils/db-connection.d.ts +1 -0
  97. package/dist/src/utils/db-connection.js +1 -0
  98. package/dist/src/utils/json-diff.d.ts +4 -2
  99. package/dist/src/utils/json-diff.js +6 -94
  100. package/dist/src/utils/nx-cloud-utils.d.ts +2 -0
  101. package/dist/src/utils/nx-cloud-utils.js +6 -1
  102. package/dist/src/utils/project-graph-utils.d.ts +2 -0
  103. package/dist/src/utils/project-graph-utils.js +12 -0
  104. package/package.json +22 -13
  105. package/readme-template.md +16 -0
  106. package/schemas/nx-schema.json +34 -0
  107. package/schemas/project-schema.json +31 -0
  108. package/dist/src/project-graph/affected/locators/project-glob-changes.d.ts +0 -2
  109. package/dist/src/project-graph/affected/locators/project-glob-changes.js +0 -40
  110. package/dist/src/project-graph/affected/locators/workspace-json-changes.d.ts +0 -4
  111. package/dist/src/project-graph/affected/locators/workspace-json-changes.js +0 -47
  112. package/dist/src/project-graph/affected/locators/workspace-projects.d.ts +0 -3
  113. package/dist/src/project-graph/affected/locators/workspace-projects.js +0 -74
@@ -23,9 +23,14 @@ function getTaskDetails() {
23
23
  }
24
24
  return taskDetails;
25
25
  }
26
- async function hashTasksThatDoNotDependOnOutputsOfOtherTasks(hasher, projectGraph, taskGraph, nxJson, tasksDetails) {
26
+ async function hashTasksThatDoNotDependOnOutputsOfOtherTasks(hasher, projectGraph, taskGraph, nxJson, tasksDetails, ioSnapshots) {
27
27
  performance.mark('hashMultipleTasks:start');
28
28
  const projects = (0, project_graph_1.readProjectsConfigurationFromProjectGraph)(projectGraph).projects;
29
+ // A snapshot can make a task depend on producer outputs its declared
30
+ // inputs never mentioned; those hash after their producers too.
31
+ const deferredBySnapshot = ioSnapshots
32
+ ? new Set((0, native_1.getIoSnapshotDeferredTaskIds)(ioSnapshots, taskGraph))
33
+ : null;
29
34
  const tasks = Object.values(taskGraph.tasks);
30
35
  const tasksWithHashers = await Promise.all(tasks.map(async (task) => {
31
36
  const customHasher = (0, utils_1.getCustomHasher)(task, projects);
@@ -37,6 +42,7 @@ async function hashTasksThatDoNotDependOnOutputsOfOtherTasks(hasher, projectGrap
37
42
  // a continuous server, which only planning reveals.
38
43
  const candidates = tasksWithHashers
39
44
  .filter(({ task, customHasher }) => !customHasher &&
45
+ !deferredBySnapshot?.has(task.id) &&
40
46
  !readsDependencyOutputs(task, taskGraph, projectGraph, nxJson))
41
47
  .map((t) => t.task);
42
48
  const perTaskEnvs = {};
@@ -1,10 +1,12 @@
1
1
  import { NxJsonConfiguration } from '../config/nx-json';
2
2
  import { ProjectGraph } from '../config/project-graph';
3
3
  import { Task, TaskGraph } from '../config/task-graph';
4
- import { ExternalObject, FileData, HasherOptions, HashPlanner, ProjectGraph as NativeProjectGraph, NxWorkspaceFilesExternals, TaskHasher } from '../native';
4
+ import { ExternalObject, FileData, HasherOptions, HashPlanner, IoSnapshots, ProjectGraph as NativeProjectGraph, NxWorkspaceFilesExternals, TaskHasher } from '../native';
5
5
  import type { IgnoredIndexReader } from '../native';
6
+ import type { TaskPlanningContext } from './task-planning-context';
6
7
  import { PartialHash, TaskHasherImpl } from './task-hasher';
7
8
  export declare class NativeTaskHasherImpl implements TaskHasherImpl {
9
+ private readonly projectGraph;
8
10
  hasher: TaskHasher;
9
11
  planner: HashPlanner;
10
12
  projectGraphRef: ExternalObject<NativeProjectGraph>;
@@ -19,8 +21,14 @@ export declare class NativeTaskHasherImpl implements TaskHasherImpl {
19
21
  private upfrontPlans;
20
22
  constructor(workspaceRoot: string, nxJson: NxJsonConfiguration, projectGraph: ProjectGraph, externals: NxWorkspaceFilesExternals, options: {
21
23
  selectivelyHashTsConfig: boolean;
22
- });
23
- hashTask(task: Task, taskGraph: TaskGraph, env: NodeJS.ProcessEnv, cwd?: string, collectInputs?: boolean): Promise<PartialHash>;
24
- hashTasks(tasks: Task[], taskGraph: TaskGraph, perTaskEnvs: Record<string, NodeJS.ProcessEnv>, cwd?: string, collectInputs?: boolean): Promise<PartialHash[]>;
25
- hashTasksUpfront(tasks: Task[], taskGraph: TaskGraph, perTaskEnvs: Record<string, NodeJS.ProcessEnv>, cwd?: string, collectInputs?: boolean): Promise<Record<string, PartialHash>>;
24
+ }, planningContext?: TaskPlanningContext);
25
+ hashTask(task: Task, taskGraph: TaskGraph, env: NodeJS.ProcessEnv, cwd?: string, collectInputs?: boolean, ioSnapshots?: IoSnapshots): Promise<PartialHash>;
26
+ hashTasks(tasks: Task[], taskGraph: TaskGraph, perTaskEnvs: Record<string, NodeJS.ProcessEnv>, cwd?: string, collectInputs?: boolean, ioSnapshots?: IoSnapshots): Promise<PartialHash[]>;
27
+ /**
28
+ * Plans through the native planner. With a snapshot set, the tasks JS resolves as
29
+ * custom-hashed or opted out ride along so the planner's eligibility walk
30
+ * withholds their snapshots.
31
+ */
32
+ private plan;
33
+ hashTasksUpfront(tasks: Task[], taskGraph: TaskGraph, perTaskEnvs: Record<string, NodeJS.ProcessEnv>, cwd?: string, collectInputs?: boolean, ioSnapshots?: IoSnapshots): Promise<Record<string, PartialHash>>;
26
34
  }
@@ -6,14 +6,19 @@ const transform_objects_1 = require("../native/transform-objects");
6
6
  const typescript_1 = require("../plugins/js/utils/typescript");
7
7
  const task_io_service_1 = require("../tasks-runner/task-io-service");
8
8
  const fileutils_1 = require("../utils/fileutils");
9
+ const overrides_1 = require("../io-snapshots/overrides");
9
10
  class NativeTaskHasherImpl {
10
- constructor(workspaceRoot, nxJson, projectGraph, externals, options) {
11
+ constructor(workspaceRoot, nxJson, projectGraph, externals, options, planningContext) {
12
+ this.projectGraph = projectGraph;
11
13
  /**
12
14
  * Plans of the last up-front batch and the task graph they were built for.
13
15
  * The tasks that batch deferred hash from them later without planning again.
14
16
  */
15
17
  this.upfrontPlans = null;
16
- this.projectGraphRef = (0, native_1.transferProjectGraph)((0, transform_objects_1.transformProjectGraphForRust)(projectGraph));
18
+ // A shared planner was built over its ref, so the two stay paired.
19
+ this.projectGraphRef =
20
+ planningContext?.projectGraphRef ??
21
+ (0, transform_objects_1.transformProjectGraphForRust)(projectGraph);
17
22
  this.allWorkspaceFilesRef = externals.allWorkspaceFiles;
18
23
  this.projectFileMapRef = externals.projectFiles;
19
24
  this.ignoredIndexRef = externals.ignoredIndex;
@@ -27,14 +32,15 @@ class NativeTaskHasherImpl {
27
32
  delete tsconfig.compilerOptions.paths;
28
33
  }
29
34
  }
30
- this.planner = new native_1.HashPlanner(nxJson, this.projectGraphRef);
35
+ this.planner =
36
+ planningContext?.planner ?? new native_1.HashPlanner(nxJson, this.projectGraphRef);
31
37
  this.hasher = new native_1.TaskHasher(workspaceRoot, this.projectGraphRef, this.projectFileMapRef, this.allWorkspaceFilesRef, Buffer.from(JSON.stringify(tsconfig)), paths, rootTsConfigPath, options, this.ignoredIndexRef);
32
38
  }
33
- async hashTask(task, taskGraph, env, cwd, collectInputs) {
34
- const hashes = await this.hashTasks([task], taskGraph, { [task.id]: env }, cwd, collectInputs);
39
+ async hashTask(task, taskGraph, env, cwd, collectInputs, ioSnapshots) {
40
+ const hashes = await this.hashTasks([task], taskGraph, { [task.id]: env }, cwd, collectInputs, ioSnapshots);
35
41
  return hashes[0];
36
42
  }
37
- async hashTasks(tasks, taskGraph, perTaskEnvs, cwd, collectInputs) {
43
+ async hashTasks(tasks, taskGraph, perTaskEnvs, cwd, collectInputs, ioSnapshots) {
38
44
  const envs = perTaskEnvs;
39
45
  const shouldCollectInputs = collectInputs ?? (0, task_io_service_1.getTaskIOService)().hasTaskInputSubscribers();
40
46
  const resolvedCwd = cwd ?? process.cwd();
@@ -42,20 +48,32 @@ class NativeTaskHasherImpl {
42
48
  let unplanned = tasks.map((t) => t.id);
43
49
  if (this.upfrontPlans &&
44
50
  unplanned.some((id) => this.upfrontPlans.taskIds.has(id)) &&
45
- this.upfrontPlans.fingerprint === taskGraphFingerprint(taskGraph)) {
51
+ this.upfrontPlans.fingerprint ===
52
+ taskGraphFingerprint(taskGraph, ioSnapshots)) {
46
53
  Object.assign(hashes, this.hasher.hashPlansFor(this.upfrontPlans.plans, unplanned, envs, resolvedCwd, shouldCollectInputs));
47
54
  unplanned = unplanned.filter((id) => !(id in hashes));
48
55
  }
49
56
  if (unplanned.length > 0) {
50
- const plans = this.planner.getPlansReference(unplanned, taskGraph);
57
+ const plans = this.plan(unplanned, taskGraph, ioSnapshots);
51
58
  Object.assign(hashes, this.hasher.hashPlans(plans, envs, resolvedCwd, shouldCollectInputs));
52
59
  }
53
60
  return tasks.map((t) => hashes[t.id]);
54
61
  }
55
- async hashTasksUpfront(tasks, taskGraph, perTaskEnvs, cwd, collectInputs) {
56
- const plans = this.planner.getPlansReference(tasks.map((t) => t.id), taskGraph);
62
+ /**
63
+ * Plans through the native planner. With a snapshot set, the tasks JS resolves as
64
+ * custom-hashed or opted out ride along so the planner's eligibility walk
65
+ * withholds their snapshots.
66
+ */
67
+ plan(taskIds, taskGraph, ioSnapshots) {
68
+ if (!ioSnapshots) {
69
+ return this.planner.getPlansReference(taskIds, taskGraph);
70
+ }
71
+ return this.planner.getPlansReference(taskIds, taskGraph, ioSnapshots, (0, overrides_1.ioSnapshotEligibilityOptions)(this.projectGraph, taskGraph));
72
+ }
73
+ async hashTasksUpfront(tasks, taskGraph, perTaskEnvs, cwd, collectInputs, ioSnapshots) {
74
+ const plans = this.plan(tasks.map((t) => t.id), taskGraph, ioSnapshots);
57
75
  this.upfrontPlans = {
58
- fingerprint: taskGraphFingerprint(taskGraph),
76
+ fingerprint: taskGraphFingerprint(taskGraph, ioSnapshots),
59
77
  taskIds: new Set(tasks.map((t) => t.id)),
60
78
  plans,
61
79
  };
@@ -66,13 +84,18 @@ class NativeTaskHasherImpl {
66
84
  exports.NativeTaskHasherImpl = NativeTaskHasherImpl;
67
85
  /**
68
86
  * Everything the planner reads from a task graph: each task's target and
69
- * outputs, and the graph's edges. The project graph and nx.json are fixed for
70
- * the life of a hasher, so equal fingerprints mean equal plans. A run's
87
+ * outputs, and the graph's edges, plus the snapshot set the plans were
88
+ * built against. The project graph and nx.json are fixed for the life of a
89
+ * hasher, so equal fingerprints mean equal plans. A run's
71
90
  * results (hash, timings) are left out so hashing one task does not
72
91
  * invalidate the plans of the rest.
73
92
  */
74
- function taskGraphFingerprint(taskGraph) {
75
- const parts = [];
93
+ function taskGraphFingerprint(taskGraph, ioSnapshots) {
94
+ const parts = [
95
+ ioSnapshots
96
+ ? `${ioSnapshots.commit}@${ioSnapshots.resolution.fetchedAt}`
97
+ : '',
98
+ ];
76
99
  for (const id of Object.keys(taskGraph.tasks).sort()) {
77
100
  const task = taskGraph.tasks[id];
78
101
  parts.push(id, task.target.project, task.target.target, task.target.configuration ?? '', (task.outputs ?? []).join(','), (taskGraph.dependencies[id] ?? []).join(','), (taskGraph.continuousDependencies[id] ?? []).join(','));
@@ -2,8 +2,10 @@ import { FileData, ProjectGraph, ProjectGraphProjectNode } from '../config/proje
2
2
  import { NxJsonConfiguration } from '../config/nx-json';
3
3
  import { Task, TaskGraph } from '../config/task-graph';
4
4
  import { DaemonClient } from '../daemon/client/client';
5
+ import type { IoSnapshotVersion } from '../daemon/message-types/io-snapshot-version';
5
6
  import { InputDefinition } from '../config/workspace-json-project-json';
6
- import { HashInputs, NxWorkspaceFilesExternals } from '../native';
7
+ import type { TaskPlanningContext } from './task-planning-context';
8
+ import { HashInputs, IoSnapshots, NxWorkspaceFilesExternals } from '../native';
7
9
  export { HashInputs };
8
10
  /**
9
11
  * A data structure returned by the default hasher.
@@ -74,15 +76,16 @@ export interface TaskHasherImpl {
74
76
  * single shared env should construct `{ [task.id]: env }` for every
75
77
  * task.
76
78
  */
77
- hashTasks(tasks: Task[], taskGraph: TaskGraph, perTaskEnvs: Record<string, NodeJS.ProcessEnv>, cwd?: string, collectInputs?: boolean): Promise<PartialHash[]>;
78
- hashTask(task: Task, taskGraph: TaskGraph, env: NodeJS.ProcessEnv, cwd?: string, collectInputs?: boolean): Promise<PartialHash>;
79
- hashTasksUpfront(tasks: Task[], taskGraph: TaskGraph, perTaskEnvs: Record<string, NodeJS.ProcessEnv>, cwd?: string, collectInputs?: boolean): Promise<Record<string, PartialHash>>;
79
+ hashTasks(tasks: Task[], taskGraph: TaskGraph, perTaskEnvs: Record<string, NodeJS.ProcessEnv>, cwd?: string, collectInputs?: boolean, ioSnapshots?: IoSnapshots): Promise<PartialHash[]>;
80
+ hashTask(task: Task, taskGraph: TaskGraph, env: NodeJS.ProcessEnv, cwd?: string, collectInputs?: boolean, ioSnapshots?: IoSnapshots): Promise<PartialHash>;
81
+ hashTasksUpfront(tasks: Task[], taskGraph: TaskGraph, perTaskEnvs: Record<string, NodeJS.ProcessEnv>, cwd?: string, collectInputs?: boolean, ioSnapshots?: IoSnapshots): Promise<Record<string, PartialHash>>;
80
82
  }
81
83
  export type Hasher = TaskHasher;
82
84
  export declare class DaemonBasedTaskHasher implements TaskHasher {
83
85
  private readonly daemonClient;
84
86
  private readonly runnerOptions;
85
- constructor(daemonClient: DaemonClient, runnerOptions: any);
87
+ private readonly ioSnapshots?;
88
+ constructor(daemonClient: DaemonClient, runnerOptions: any, ioSnapshots?: IoSnapshotVersion);
86
89
  hashTasks(tasks: Task[], taskGraph: TaskGraph, envOrPerTaskEnvs: NodeJS.ProcessEnv | Record<string, NodeJS.ProcessEnv>): Promise<Hash[]>;
87
90
  hashTasksUpfront(tasks: Task[], taskGraph: TaskGraph, perTaskEnvs: Record<string, NodeJS.ProcessEnv>): Promise<Record<string, Hash>>;
88
91
  hashTask(task: Task, taskGraph?: TaskGraph, env?: NodeJS.ProcessEnv): Promise<Hash>;
@@ -92,11 +95,16 @@ export declare class InProcessTaskHasher implements TaskHasher {
92
95
  private readonly nxJson;
93
96
  private readonly externalRustReferences;
94
97
  private readonly options;
98
+ private readonly planningContext?;
99
+ /** This run's snapshot set; the daemon passes one per request instead. */
100
+ private readonly ioSnapshots?;
95
101
  private taskHasher;
96
- constructor(projectGraph: ProjectGraph, nxJson: NxJsonConfiguration, externalRustReferences: NxWorkspaceFilesExternals | null, options: any);
97
- hashTasks(tasks: Task[], taskGraph: TaskGraph, envOrPerTaskEnvs: NodeJS.ProcessEnv | Record<string, NodeJS.ProcessEnv>, cwd?: string, collectInputs?: boolean): Promise<Hash[]>;
98
- hashTasksUpfront(tasks: Task[], taskGraph: TaskGraph, perTaskEnvs: Record<string, NodeJS.ProcessEnv>, cwd?: string, collectInputs?: boolean): Promise<Record<string, Hash>>;
99
- hashTask(task: Task, taskGraph?: TaskGraph, env?: NodeJS.ProcessEnv, cwd?: string, collectInputs?: boolean): Promise<Hash>;
102
+ constructor(projectGraph: ProjectGraph, nxJson: NxJsonConfiguration, externalRustReferences: NxWorkspaceFilesExternals | null, options: any, planningContext?: TaskPlanningContext,
103
+ /** This run's snapshot set; the daemon passes one per request instead. */
104
+ ioSnapshots?: IoSnapshots);
105
+ hashTasks(tasks: Task[], taskGraph: TaskGraph, envOrPerTaskEnvs: NodeJS.ProcessEnv | Record<string, NodeJS.ProcessEnv>, cwd?: string, collectInputs?: boolean, ioSnapshots?: IoSnapshots): Promise<Hash[]>;
106
+ hashTasksUpfront(tasks: Task[], taskGraph: TaskGraph, perTaskEnvs: Record<string, NodeJS.ProcessEnv>, cwd?: string, collectInputs?: boolean, ioSnapshots?: IoSnapshots): Promise<Record<string, Hash>>;
107
+ hashTask(task: Task, taskGraph?: TaskGraph, env?: NodeJS.ProcessEnv, cwd?: string, collectInputs?: boolean, ioSnapshots?: IoSnapshots): Promise<Hash>;
100
108
  private createHashDetails;
101
109
  private hashCommand;
102
110
  }
@@ -44,17 +44,18 @@ function normalizePerTaskEnvs(tasks, arg) {
44
44
  return arg;
45
45
  }
46
46
  class DaemonBasedTaskHasher {
47
- constructor(daemonClient, runnerOptions) {
47
+ constructor(daemonClient, runnerOptions, ioSnapshots) {
48
48
  this.daemonClient = daemonClient;
49
49
  this.runnerOptions = runnerOptions;
50
+ this.ioSnapshots = ioSnapshots;
50
51
  }
51
52
  async hashTasks(tasks, taskGraph, envOrPerTaskEnvs) {
52
53
  const collectInputs = (0, task_io_service_1.getTaskIOService)().hasTaskInputSubscribers();
53
- return this.daemonClient.hashTasks(this.runnerOptions, tasks, taskGraph, normalizePerTaskEnvs(tasks, envOrPerTaskEnvs), process.cwd(), collectInputs);
54
+ return this.daemonClient.hashTasks(this.runnerOptions, tasks, taskGraph, normalizePerTaskEnvs(tasks, envOrPerTaskEnvs), process.cwd(), collectInputs, this.ioSnapshots);
54
55
  }
55
56
  async hashTasksUpfront(tasks, taskGraph, perTaskEnvs) {
56
57
  const collectInputs = (0, task_io_service_1.getTaskIOService)().hasTaskInputSubscribers();
57
- return this.daemonClient.hashTasksUpfront(this.runnerOptions, tasks, taskGraph, perTaskEnvs, process.cwd(), collectInputs);
58
+ return this.daemonClient.hashTasksUpfront(this.runnerOptions, tasks, taskGraph, perTaskEnvs, process.cwd(), collectInputs, this.ioSnapshots);
58
59
  }
59
60
  async hashTask(task, taskGraph, env) {
60
61
  return (await this.hashTasks([task], taskGraph, {
@@ -64,21 +65,25 @@ class DaemonBasedTaskHasher {
64
65
  }
65
66
  exports.DaemonBasedTaskHasher = DaemonBasedTaskHasher;
66
67
  class InProcessTaskHasher {
67
- constructor(projectGraph, nxJson, externalRustReferences, options) {
68
+ constructor(projectGraph, nxJson, externalRustReferences, options, planningContext,
69
+ /** This run's snapshot set; the daemon passes one per request instead. */
70
+ ioSnapshots) {
68
71
  this.projectGraph = projectGraph;
69
72
  this.nxJson = nxJson;
70
73
  this.externalRustReferences = externalRustReferences;
71
74
  this.options = options;
75
+ this.planningContext = planningContext;
76
+ this.ioSnapshots = ioSnapshots;
72
77
  this.taskHasher = new native_task_hasher_impl_1.NativeTaskHasherImpl(workspace_root_1.workspaceRoot, this.nxJson, this.projectGraph, this.externalRustReferences, {
73
78
  selectivelyHashTsConfig: this.options?.selectivelyHashTsConfig ?? false,
74
- });
79
+ }, this.planningContext);
75
80
  }
76
- async hashTasks(tasks, taskGraph, envOrPerTaskEnvs, cwd, collectInputs) {
77
- const hashes = await this.taskHasher.hashTasks(tasks, taskGraph, normalizePerTaskEnvs(tasks, envOrPerTaskEnvs), cwd ?? process.cwd(), collectInputs);
81
+ async hashTasks(tasks, taskGraph, envOrPerTaskEnvs, cwd, collectInputs, ioSnapshots) {
82
+ const hashes = await this.taskHasher.hashTasks(tasks, taskGraph, normalizePerTaskEnvs(tasks, envOrPerTaskEnvs), cwd ?? process.cwd(), collectInputs, ioSnapshots ?? this.ioSnapshots);
78
83
  return tasks.map((task, index) => this.createHashDetails(task, hashes[index]));
79
84
  }
80
- async hashTasksUpfront(tasks, taskGraph, perTaskEnvs, cwd, collectInputs) {
81
- const hashes = await this.taskHasher.hashTasksUpfront(tasks, taskGraph, perTaskEnvs, cwd ?? process.cwd(), collectInputs);
85
+ async hashTasksUpfront(tasks, taskGraph, perTaskEnvs, cwd, collectInputs, ioSnapshots) {
86
+ const hashes = await this.taskHasher.hashTasksUpfront(tasks, taskGraph, perTaskEnvs, cwd ?? process.cwd(), collectInputs, ioSnapshots ?? this.ioSnapshots);
82
87
  const result = {};
83
88
  for (const task of tasks) {
84
89
  if (hashes[task.id]) {
@@ -87,8 +92,8 @@ class InProcessTaskHasher {
87
92
  }
88
93
  return result;
89
94
  }
90
- async hashTask(task, taskGraph, env, cwd, collectInputs) {
91
- const res = await this.taskHasher.hashTask(task, taskGraph, env ?? process.env, cwd ?? process.cwd(), collectInputs);
95
+ async hashTask(task, taskGraph, env, cwd, collectInputs, ioSnapshots) {
96
+ const res = await this.taskHasher.hashTask(task, taskGraph, env ?? process.env, cwd ?? process.cwd(), collectInputs, ioSnapshots ?? this.ioSnapshots);
92
97
  return this.createHashDetails(task, res);
93
98
  }
94
99
  createHashDetails(task, res) {
@@ -0,0 +1,13 @@
1
+ import { NxJsonConfiguration } from '../config/nx-json';
2
+ import { ProjectGraph } from '../config/project-graph';
3
+ import { ExternalObject, HashPlanner, ProjectGraph as NativeProjectGraph } from '../native';
4
+ /**
5
+ * The graph copied into Rust and the planner built over it, shared by selection
6
+ * and hashing. The planner remembers its last plans, so the run's hashing reuses
7
+ * what selection planned.
8
+ */
9
+ export interface TaskPlanningContext {
10
+ projectGraphRef: ExternalObject<NativeProjectGraph>;
11
+ planner: HashPlanner;
12
+ }
13
+ export declare function createTaskPlanningContext(projectGraph: ProjectGraph, nxJson: NxJsonConfiguration): TaskPlanningContext;
@@ -0,0 +1,12 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createTaskPlanningContext = createTaskPlanningContext;
4
+ const native_1 = require("../native");
5
+ const transform_objects_1 = require("../native/transform-objects");
6
+ function createTaskPlanningContext(projectGraph, nxJson) {
7
+ const projectGraphRef = (0, transform_objects_1.transformProjectGraphForRust)(projectGraph);
8
+ return {
9
+ projectGraphRef,
10
+ planner: new native_1.HashPlanner(nxJson, projectGraphRef),
11
+ };
12
+ }
@@ -0,0 +1,23 @@
1
+ import type { NxJsonConfiguration } from '../config/nx-json';
2
+ export interface IoSnapshotCloudOptions {
3
+ accessToken?: string;
4
+ nxCloudId?: string;
5
+ url?: string;
6
+ cloud?: boolean;
7
+ }
8
+ export interface IoSnapshotEnv {
9
+ NX_IO_SNAPSHOTS?: string;
10
+ NX_CLOUD_USE_IO_SNAPSHOTS?: string;
11
+ NX_NO_CLOUD?: string;
12
+ /** Whether the run's env holds an Nx Cloud token. */
13
+ hasNxCloudToken?: boolean;
14
+ }
15
+ /**
16
+ * Off unless a CI run opts in with `NX_IO_SNAPSHOTS=true`, or the Nx Cloud
17
+ * client's `NX_CLOUD_USE_IO_SNAPSHOTS=true`, in a workspace that uses Nx Cloud,
18
+ * since there is nothing to fetch from otherwise. `env` defaults to this
19
+ * process's.
20
+ */
21
+ export declare function isIoSnapshotFetchEnabled(nxJson: NxJsonConfiguration, runnerOptions?: IoSnapshotCloudOptions, env?: IoSnapshotEnv): boolean;
22
+ /** The subset of this run's environment the decision reads. */
23
+ export declare function ioSnapshotEnv(env?: NodeJS.ProcessEnv): IoSnapshotEnv;
@@ -0,0 +1,32 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isIoSnapshotFetchEnabled = isIoSnapshotFetchEnabled;
4
+ exports.ioSnapshotEnv = ioSnapshotEnv;
5
+ const is_ci_1 = require("../utils/is-ci");
6
+ const nx_cloud_utils_1 = require("../utils/nx-cloud-utils");
7
+ /**
8
+ * Off unless a CI run opts in with `NX_IO_SNAPSHOTS=true`, or the Nx Cloud
9
+ * client's `NX_CLOUD_USE_IO_SNAPSHOTS=true`, in a workspace that uses Nx Cloud,
10
+ * since there is nothing to fetch from otherwise. `env` defaults to this
11
+ * process's.
12
+ */
13
+ function isIoSnapshotFetchEnabled(nxJson, runnerOptions = {}, env = ioSnapshotEnv()) {
14
+ if (env.NX_NO_CLOUD === 'true' ||
15
+ nxJson.neverConnectToCloud ||
16
+ runnerOptions.cloud === false ||
17
+ (env.NX_IO_SNAPSHOTS !== 'true' &&
18
+ env.NX_CLOUD_USE_IO_SNAPSHOTS !== 'true') ||
19
+ !(0, is_ci_1.isCI)()) {
20
+ return false;
21
+ }
22
+ return !!env.hasNxCloudToken || (0, nx_cloud_utils_1.isNxCloudConfigured)(nxJson);
23
+ }
24
+ /** The subset of this run's environment the decision reads. */
25
+ function ioSnapshotEnv(env = process.env) {
26
+ return {
27
+ NX_IO_SNAPSHOTS: env.NX_IO_SNAPSHOTS,
28
+ NX_CLOUD_USE_IO_SNAPSHOTS: env.NX_CLOUD_USE_IO_SNAPSHOTS,
29
+ NX_NO_CLOUD: env.NX_NO_CLOUD,
30
+ hasNxCloudToken: !!(env.NX_CLOUD_ACCESS_TOKEN || env.NX_CLOUD_AUTH_TOKEN),
31
+ };
32
+ }
@@ -0,0 +1,21 @@
1
+ import type { IoSnapshotCloudOptions } from './config';
2
+ /** The Nx Cloud client's `readIoSnapshots` contract, as far as nx uses it. */
3
+ export interface ReadIoSnapshotsOptions {
4
+ workspaceRoot?: string;
5
+ nxCloudOptions?: IoSnapshotCloudOptions;
6
+ timeoutMs?: number;
7
+ }
8
+ export interface ReadIoSnapshot {
9
+ commit: string;
10
+ inputs: readonly string[];
11
+ outputs: readonly string[];
12
+ }
13
+ export interface ReadIoSnapshotsResult {
14
+ snapshots: Readonly<Record<string, ReadIoSnapshot>>;
15
+ }
16
+ /**
17
+ * Reads HEAD's snapshot set from Nx Cloud. Throws with a `code` saying why it
18
+ * could not: the client's own, or `NO_CLOUD_CLIENT`, `UNSUPPORTED_CLIENT` or
19
+ * `NO_SNAPSHOTS`.
20
+ */
21
+ export declare function fetchIoSnapshots(runnerOptions: IoSnapshotCloudOptions): Promise<ReadIoSnapshotsResult>;
@@ -0,0 +1,43 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.fetchIoSnapshots = fetchIoSnapshots;
4
+ const resolution_helpers_1 = require("../nx-cloud/resolution-helpers");
5
+ const update_manager_1 = require("../nx-cloud/update-manager");
6
+ const workspace_root_1 = require("../utils/workspace-root");
7
+ const READ_TIMEOUT_MS = 10_000;
8
+ /**
9
+ * Reads HEAD's snapshot set from Nx Cloud. Throws with a `code` saying why it
10
+ * could not: the client's own, or `NO_CLOUD_CLIENT`, `UNSUPPORTED_CLIENT` or
11
+ * `NO_SNAPSHOTS`.
12
+ */
13
+ async function fetchIoSnapshots(runnerOptions) {
14
+ const client = await loadCloudClient(runnerOptions).catch((e) => {
15
+ throw codedError('NO_CLOUD_CLIENT', e instanceof Error ? e.message : String(e));
16
+ });
17
+ if (!client) {
18
+ throw codedError('NO_CLOUD_CLIENT', 'The Nx Cloud client could not be loaded');
19
+ }
20
+ if (typeof client.readIoSnapshots !== 'function') {
21
+ throw codedError('UNSUPPORTED_CLIENT', 'The installed Nx Cloud client does not expose I/O snapshots; update nx-cloud');
22
+ }
23
+ const result = await client.readIoSnapshots({
24
+ workspaceRoot: workspace_root_1.workspaceRoot,
25
+ nxCloudOptions: runnerOptions,
26
+ timeoutMs: READ_TIMEOUT_MS,
27
+ });
28
+ if (!result) {
29
+ // The client returns `null` only for a `knownUpdatedAt` match, which nx
30
+ // never sends; treat it as nothing to serve rather than a broken reply.
31
+ throw codedError('NO_SNAPSHOTS', 'Nx Cloud returned no I/O snapshot set');
32
+ }
33
+ return result;
34
+ }
35
+ async function loadCloudClient(runnerOptions) {
36
+ const client = (await (0, update_manager_1.verifyOrUpdateNxCloudClient)(runnerOptions))
37
+ ?.nxCloudClient;
38
+ client?.configureLightClientRequire()((0, resolution_helpers_1.findAncestorNodeModules)(__dirname, []));
39
+ return client;
40
+ }
41
+ function codedError(code, message) {
42
+ return Object.assign(new Error(message), { code });
43
+ }
@@ -0,0 +1,12 @@
1
+ import type { ProjectGraph } from '../config/project-graph';
2
+ import type { TaskGraph } from '../config/task-graph';
3
+ import { type IoSnapshots } from '../native';
4
+ /**
5
+ * Extends each eligible task's outputs in place to `declared ∪ observed`,
6
+ * declared first, as a set of exact strings. Ineligible tasks keep their
7
+ * declared outputs untouched. Idempotent. Runs before hashing so the task
8
+ * graph the hasher, the cache, and the deferral check see carries the union.
9
+ * A graph already extended from `snapshots` is skipped: DTE workers pass the
10
+ * same graph for every task they run.
11
+ */
12
+ export declare function applyIoSnapshotOutputs(projectGraph: ProjectGraph, taskGraph: TaskGraph, snapshots: IoSnapshots): void;
@@ -0,0 +1,33 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.applyIoSnapshotOutputs = applyIoSnapshotOutputs;
4
+ const native_1 = require("../native");
5
+ const overrides_1 = require("./overrides");
6
+ /** The sets already applied to each task graph object. */
7
+ const applied = new WeakMap();
8
+ /**
9
+ * Extends each eligible task's outputs in place to `declared ∪ observed`,
10
+ * declared first, as a set of exact strings. Ineligible tasks keep their
11
+ * declared outputs untouched. Idempotent. Runs before hashing so the task
12
+ * graph the hasher, the cache, and the deferral check see carries the union.
13
+ * A graph already extended from `snapshots` is skipped: DTE workers pass the
14
+ * same graph for every task they run.
15
+ */
16
+ function applyIoSnapshotOutputs(projectGraph, taskGraph, snapshots) {
17
+ if (applied.get(taskGraph)?.has(snapshots)) {
18
+ return;
19
+ }
20
+ // Same walk as hashing, already confined to the workspace and outside
21
+ // node_modules/.nx/.git; ineligible tasks are absent.
22
+ const observed = (0, native_1.getObservedIoSnapshotOutputs)(snapshots, taskGraph, (0, overrides_1.ioSnapshotEligibilityOptions)(projectGraph, taskGraph));
23
+ for (const [taskId, outputs] of Object.entries(observed)) {
24
+ const task = taskGraph.tasks[taskId];
25
+ if (task) {
26
+ task.outputs = [...new Set([...task.outputs, ...outputs])];
27
+ }
28
+ }
29
+ // Marked only once applied, so a call that threw is retried.
30
+ const sets = applied.get(taskGraph) ?? new WeakSet();
31
+ sets.add(snapshots);
32
+ applied.set(taskGraph, sets);
33
+ }
@@ -0,0 +1,12 @@
1
+ import type { ProjectGraph } from '../config/project-graph';
2
+ import type { TaskGraph } from '../config/task-graph';
3
+ import { type IoSnapshotEligibilityOptions, type IoSnapshotReport, type IoSnapshots } from '../native';
4
+ /** The eligibility walk's view of the run's tasks, as JS resolves it. */
5
+ export declare function ioSnapshotEligibilityOptions(projectGraph: ProjectGraph, taskGraph: TaskGraph): IoSnapshotEligibilityOptions;
6
+ /**
7
+ * Reports which tasks in `taskGraph` hash from `snapshots` and why the rest
8
+ * do not, with the same eligibility walk the planner uses, without building a
9
+ * planner (no project-graph transfer). Never fetches, never throws. Feeds
10
+ * the `--verbose` run summary.
11
+ */
12
+ export declare function buildIoSnapshotOverrides(projectGraph: ProjectGraph, taskGraph: TaskGraph, snapshots: IoSnapshots): IoSnapshotReport;
@@ -0,0 +1,49 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ioSnapshotEligibilityOptions = ioSnapshotEligibilityOptions;
4
+ exports.buildIoSnapshotOverrides = buildIoSnapshotOverrides;
5
+ const native_1 = require("../native");
6
+ const project_graph_1 = require("../project-graph/project-graph");
7
+ const utils_1 = require("../tasks-runner/utils");
8
+ const customHasherMemo = new WeakMap();
9
+ /**
10
+ * Tasks whose executor ships a custom hasher; they are never hashed from a
11
+ * snapshot. Detected here because executors are resolved in JS, by the
12
+ * factory's presence only — invoking it would load user modules.
13
+ */
14
+ function customHasherTaskIds(projectGraph, taskGraph) {
15
+ const memoized = customHasherMemo.get(taskGraph);
16
+ if (memoized) {
17
+ return memoized;
18
+ }
19
+ const projects = (0, project_graph_1.readProjectsConfigurationFromProjectGraph)(projectGraph).projects;
20
+ const ids = Object.values(taskGraph.tasks)
21
+ .filter((task) => {
22
+ try {
23
+ return !!(0, utils_1.getExecutorForTask)(task, projects).hasherFactory;
24
+ }
25
+ catch {
26
+ // An unresolvable executor fails later, at execution; it is not a
27
+ // reason to withhold a snapshot here.
28
+ return false;
29
+ }
30
+ })
31
+ .map((task) => task.id);
32
+ customHasherMemo.set(taskGraph, ids);
33
+ return ids;
34
+ }
35
+ /** The eligibility walk's view of the run's tasks, as JS resolves it. */
36
+ function ioSnapshotEligibilityOptions(projectGraph, taskGraph) {
37
+ return {
38
+ customHasherTaskIds: customHasherTaskIds(projectGraph, taskGraph),
39
+ };
40
+ }
41
+ /**
42
+ * Reports which tasks in `taskGraph` hash from `snapshots` and why the rest
43
+ * do not, with the same eligibility walk the planner uses, without building a
44
+ * planner (no project-graph transfer). Never fetches, never throws. Feeds
45
+ * the `--verbose` run summary.
46
+ */
47
+ function buildIoSnapshotOverrides(projectGraph, taskGraph, snapshots) {
48
+ return (0, native_1.getIoSnapshotReport)(snapshots, taskGraph, ioSnapshotEligibilityOptions(projectGraph, taskGraph));
49
+ }
@@ -0,0 +1,13 @@
1
+ import type { IoSnapshotReport } from '../native';
2
+ export interface IoSnapshotSummary {
3
+ /** The heading, e.g. "I/O snapshots: 12 tasks hashed from snapshot, 3 fell back". */
4
+ line: string;
5
+ /** Per-reason detail under it. */
6
+ bodyLines: string[];
7
+ }
8
+ /**
9
+ * Formats the once-per-run verbose summary of a run that resolved a set:
10
+ * `result` is what hashing used, `status` whether the set was fetched or
11
+ * already stored.
12
+ */
13
+ export declare function formatIoSnapshotSummary(result: IoSnapshotReport, status: 'fetched' | 'cached'): IoSnapshotSummary;
@@ -0,0 +1,68 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.formatIoSnapshotSummary = formatIoSnapshotSummary;
4
+ /**
5
+ * Formats the once-per-run verbose summary of a run that resolved a set:
6
+ * `result` is what hashing used, `status` whether the set was fetched or
7
+ * already stored.
8
+ */
9
+ function formatIoSnapshotSummary(result, status) {
10
+ const used = result.used.length;
11
+ // `unusable-output` is raised on used tasks, possibly several per task:
12
+ // count withheld tasks, not diagnostics.
13
+ const usedTasks = new Set(result.used);
14
+ const withheld = result.diagnostics.filter((d) => d.taskId != null && !usedTasks.has(d.taskId));
15
+ const byReason = countByReason(withheld);
16
+ const fellBack = new Set(withheld.map((d) => d.taskId)).size;
17
+ const setLevel = result.diagnostics.find((d) => d.taskId == null);
18
+ const withOutputs = result.tasksWithOutputs?.length ?? 0;
19
+ const line = setLevel
20
+ ? `I/O snapshots: none used (unreadable set: ${setLevel.message})`
21
+ : `I/O snapshots: ${plural(used, 'task')} hashed from snapshot${withOutputs ? ` (${withOutputs} with observed outputs)` : ''}, ${plural(fellBack, 'task')} fell back${fellBack ? ` (${summarizeReasons(byReason)})` : ''}`;
22
+ const bodyLines = [`set: ${status}`];
23
+ bodyLines.push(`commit ${result.resolution.requestedCommit}, ${result.resolution.tasks} tasks in set`);
24
+ for (const d of result.diagnostics) {
25
+ bodyLines.push(describeDiagnostic(d));
26
+ }
27
+ return { line, bodyLines };
28
+ }
29
+ function describeDiagnostic(d) {
30
+ switch (d.reason) {
31
+ case 'unreadable-set':
32
+ return `unreadable snapshot set: ${d.message}`;
33
+ case 'disabled':
34
+ return `${d.taskId}: ultracache.mode is off`;
35
+ case 'autofix-disabled':
36
+ return `${d.taskId}: ultracache.mode is not on`;
37
+ case 'custom-hasher':
38
+ return `${d.taskId}: uses a custom hasher`;
39
+ case 'missing':
40
+ return `${d.taskId}: no snapshot for this task`;
41
+ case 'invalid-glob':
42
+ return `${d.taskId}: snapshot glob "${d.glob}" is not a valid files glob`;
43
+ case 'escapes-workspace':
44
+ return `${d.taskId}: snapshot glob "${d.glob}" escapes the workspace`;
45
+ case 'unusable-output':
46
+ return `${d.taskId}: observed write "${d.glob}" is not usable as an output, so it is not cached`;
47
+ default:
48
+ return `${d.taskId ? `${d.taskId}: ` : ''}${d.reason}`;
49
+ }
50
+ }
51
+ function countByReason(diagnostics) {
52
+ const counts = new Map();
53
+ for (const d of diagnostics) {
54
+ if (d.taskId != null) {
55
+ counts.set(d.reason, (counts.get(d.reason) ?? 0) + 1);
56
+ }
57
+ }
58
+ return counts;
59
+ }
60
+ function summarizeReasons(counts) {
61
+ return [...counts.entries()]
62
+ .sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
63
+ .map(([reason, n]) => `${n} ${reason}`)
64
+ .join(', ');
65
+ }
66
+ function plural(n, noun) {
67
+ return `${n} ${noun}${n === 1 ? '' : 's'}`;
68
+ }