hammerkit 1.6.0-alpha.3 → 1.7.0

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 (256) hide show
  1. package/README.md +4 -4
  2. package/build.schema.json +49 -2
  3. package/dist/cache/backends/local-cache-backend.js +58 -1
  4. package/dist/cache/backends/registry/registry-client.js +264 -0
  5. package/dist/cache/backends/registry/registry-credentials.js +81 -0
  6. package/dist/cache/backends/registry/registry-reference.js +36 -0
  7. package/dist/cache/backends/registry-cache-backend.js +145 -0
  8. package/dist/cache/backends/s3-cache-backend.js +99 -46
  9. package/dist/cache/cache-backend-registry.js +2 -0
  10. package/dist/cache/cache-inventory.js +84 -0
  11. package/dist/cache/cache-sync.js +77 -0
  12. package/dist/cache/explain.js +214 -0
  13. package/dist/cache/last-resolved.js +48 -0
  14. package/dist/cache/read-only.js +15 -0
  15. package/dist/cache/retention.js +61 -0
  16. package/dist/cli.js +43 -0
  17. package/dist/docker/using-container.js +5 -1
  18. package/dist/executer/archive-task-entry.js +21 -0
  19. package/dist/executer/await-completed-dependencies.js +32 -14
  20. package/dist/executer/cache-metadata.js +9 -2
  21. package/dist/executer/docker-service.js +2 -1
  22. package/dist/executer/docker-task.js +2 -1
  23. package/dist/executer/dry-run.js +73 -0
  24. package/dist/executer/event-cache.js +6 -5
  25. package/dist/executer/execute-command.js +21 -3
  26. package/dist/executer/execute-work-service.js +10 -1
  27. package/dist/executer/execute-work-task.js +95 -25
  28. package/dist/executer/execute-work-tree.js +26 -1
  29. package/dist/executer/get-scheduler-execute-result.js +4 -1
  30. package/dist/executer/lazy-resolver.js +15 -3
  31. package/dist/executer/reset-work-tree.js +2 -0
  32. package/dist/executer/run-summary.js +102 -0
  33. package/dist/executer/scheduler/enqueue-next.js +15 -60
  34. package/dist/executer/scheduler/state-key.js +78 -0
  35. package/dist/kubernetes/await-running-state.js +73 -19
  36. package/dist/log.js +16 -6
  37. package/dist/logging/live-logger.js +2 -1
  38. package/dist/optimizer/calculate-checksum.js +8 -2
  39. package/dist/optimizer/get-work-cache-stats.js +66 -16
  40. package/dist/optimizer/work-task-cache-description.js +34 -5
  41. package/dist/planner/project-root.js +26 -0
  42. package/dist/planner/utils/append-work-service.js +2 -1
  43. package/dist/planner/utils/append-work-task.js +7 -1
  44. package/dist/planner/utils/parse-work-source.js +35 -30
  45. package/dist/planner/utils/portable-path.js +30 -0
  46. package/dist/planner/work-cache-id.js +2 -2
  47. package/dist/planner/work-graph-serializer.js +103 -0
  48. package/dist/planner/work-instance-id.js +15 -0
  49. package/dist/planner/work-runtime-docker.js +53 -15
  50. package/dist/planner/work-runtime-kubernetes.js +35 -3
  51. package/dist/planner/work-runtime-local.js +9 -3
  52. package/dist/planner/work-service-id.js +16 -4
  53. package/dist/program.js +235 -1
  54. package/dist/schema/build-file-base-task-schema.js +4 -0
  55. package/dist/schema/cache-schema.js +21 -2
  56. package/dist/schema/duration-schema.js +18 -0
  57. package/dist/schema/reference-parser.js +3 -0
  58. package/dist/schema/size-schema.js +18 -0
  59. package/dist/schema/work-scope-parser.js +15 -2
  60. package/dist/utils/units.js +87 -0
  61. package/package.json +15 -4
  62. package/.agents/FEEDBACK.md +0 -74
  63. package/.agents/skills/before-commit/SKILL.md +0 -101
  64. package/.agents/skills/do-the-work/SKILL.md +0 -64
  65. package/.agents/skills/fix-ci/SKILL.md +0 -98
  66. package/.agents/skills/speckit-git-commit/SKILL.md +0 -53
  67. package/.agents/skills/speckit-git-feature/SKILL.md +0 -72
  68. package/.agents/skills/speckit-git-initialize/SKILL.md +0 -54
  69. package/.agents/skills/speckit-git-remote/SKILL.md +0 -50
  70. package/.agents/skills/speckit-git-validate/SKILL.md +0 -54
  71. package/.devcontainer/Dockerfile +0 -5
  72. package/.devcontainer/devcontainer.json +0 -40
  73. package/.gitbook.yaml +0 -5
  74. package/.specify/extensions/.registry +0 -37
  75. package/.specify/extensions/git/README.md +0 -100
  76. package/.specify/extensions/git/commands/speckit.git.commit.md +0 -48
  77. package/.specify/extensions/git/commands/speckit.git.feature.md +0 -67
  78. package/.specify/extensions/git/commands/speckit.git.initialize.md +0 -49
  79. package/.specify/extensions/git/commands/speckit.git.remote.md +0 -45
  80. package/.specify/extensions/git/commands/speckit.git.validate.md +0 -49
  81. package/.specify/extensions/git/config-template.yml +0 -62
  82. package/.specify/extensions/git/extension.yml +0 -140
  83. package/.specify/extensions/git/git-config.yml +0 -62
  84. package/.specify/extensions.yml +0 -149
  85. package/.specify/feature.json +0 -3
  86. package/.specify/init-options.json +0 -10
  87. package/.specify/integration.json +0 -15
  88. package/.specify/integrations/claude.manifest.json +0 -16
  89. package/.specify/integrations/speckit.manifest.json +0 -17
  90. package/.specify/memory/constitution.md +0 -162
  91. package/.specify/templates/checklist-template.md +0 -40
  92. package/.specify/templates/constitution-template.md +0 -50
  93. package/.specify/templates/plan-template.md +0 -113
  94. package/.specify/templates/spec-template.md +0 -131
  95. package/.specify/templates/tasks-template.md +0 -252
  96. package/.specify/workflows/speckit/workflow.yml +0 -77
  97. package/.specify/workflows/workflow-registry.json +0 -13
  98. package/AGENTS.md +0 -94
  99. package/Dockerfile +0 -25
  100. package/FOLLOWUPS.md +0 -34
  101. package/SUMMARY.md +0 -68
  102. package/VALUES.md +0 -53
  103. package/dist/cache/auto-pull.spec.js +0 -123
  104. package/dist/cache/backends/local-cache-backend.spec.js +0 -72
  105. package/dist/cache/backends/s3-cache-backend.spec.js +0 -118
  106. package/dist/cache/cache-backend-registry.spec.js +0 -22
  107. package/dist/cache/resolve-cache.spec.js +0 -52
  108. package/dist/cli.spec.js +0 -23
  109. package/dist/console/get-logger.spec.js +0 -23
  110. package/dist/docker/get-default-platform.spec.js +0 -29
  111. package/dist/docker/longest-common-prefix.spec.js +0 -26
  112. package/dist/docker/package.spec.js +0 -145
  113. package/dist/docker/remove-container.spec.js +0 -32
  114. package/dist/docker/using-container.spec.js +0 -90
  115. package/dist/environment/merge-environment-variables.spec.js +0 -24
  116. package/dist/environment/replace-env-variables.spec.js +0 -47
  117. package/dist/executer/abort.spec.js +0 -18
  118. package/dist/executer/environment-mock.spec.js +0 -22
  119. package/dist/executer/execute.spec.js +0 -51
  120. package/dist/executer/get-service-containers.spec.js +0 -59
  121. package/dist/executer/get-service-env-hints.spec.js +0 -65
  122. package/dist/executer/label-values.spec.js +0 -61
  123. package/dist/executer/lazy-resolver.spec.js +0 -20
  124. package/dist/executer/process-manager.spec.js +0 -54
  125. package/dist/executer/scheduler/enqueue-next.spec.js +0 -136
  126. package/dist/executer/state-resolver.spec.js +0 -37
  127. package/dist/executer/state.spec.js +0 -66
  128. package/dist/executer/states.spec.js +0 -14
  129. package/dist/file/get-file-context.spec.js +0 -178
  130. package/dist/file/move-files.spec.js +0 -48
  131. package/dist/kubernetes/ensure-http-route.spec.js +0 -70
  132. package/dist/kubernetes/ensure-namespace.spec.js +0 -39
  133. package/dist/kubernetes/resolve-pod-name.spec.js +0 -81
  134. package/dist/log.spec.js +0 -131
  135. package/dist/logging/grouped-logger.spec.js +0 -84
  136. package/dist/logging/interactive-logger.spec.js +0 -57
  137. package/dist/logging/live-logger.spec.js +0 -37
  138. package/dist/logging/loggers.spec.js +0 -154
  139. package/dist/optimizer/calculate-checksum.spec.js +0 -34
  140. package/dist/optimizer/get-cache-directory.spec.js +0 -59
  141. package/dist/optimizer/get-work-cache-stats.spec.js +0 -79
  142. package/dist/optimizer/work-task-cache-description.spec.js +0 -45
  143. package/dist/parser/default-build-file.spec.js +0 -48
  144. package/dist/parser/parse-label-arguments.spec.js +0 -27
  145. package/dist/parser/read-build-file.spec.js +0 -89
  146. package/dist/parser/read-env-file.spec.js +0 -42
  147. package/dist/planner/utils/append-work-service.spec.js +0 -71
  148. package/dist/planner/utils/get-container-user.spec.js +0 -30
  149. package/dist/planner/utils/get-default-kube-config.spec.js +0 -11
  150. package/dist/planner/utils/normalize-path.spec.js +0 -24
  151. package/dist/planner/utils/parse-work-command.spec.js +0 -21
  152. package/dist/planner/utils/parse-work-generate.spec.js +0 -45
  153. package/dist/planner/utils/parse-work-mount.spec.js +0 -62
  154. package/dist/planner/utils/parse-work-mounts.spec.js +0 -19
  155. package/dist/planner/utils/parse-work-port.spec.js +0 -24
  156. package/dist/planner/utils/parse-work-ports.spec.js +0 -24
  157. package/dist/planner/utils/parse-work-source.spec.js +0 -58
  158. package/dist/planner/utils/parse-work-volume.spec.js +0 -18
  159. package/dist/planner/utils/plan-work-tasks.spec.js +0 -30
  160. package/dist/planner/utils/plan-work-volume.spec.js +0 -33
  161. package/dist/planner/utils/split-name.spec.js +0 -16
  162. package/dist/planner/utils/template-value.spec.js +0 -14
  163. package/dist/planner/validate.spec.js +0 -251
  164. package/dist/planner/work-cache-id.spec.js +0 -173
  165. package/dist/planner/work-runtime-local.spec.js +0 -193
  166. package/dist/program-commands.spec.js +0 -84
  167. package/dist/program.spec.js +0 -43
  168. package/dist/schema/build-file-schema.spec.js +0 -61
  169. package/dist/schema/doc-build-files.spec.js +0 -223
  170. package/dist/schema/parse-error.spec.js +0 -18
  171. package/dist/schema/reference-parser.spec.js +0 -83
  172. package/dist/schema/schema-parser.spec.js +0 -80
  173. package/dist/schema/type-guards.spec.js +0 -23
  174. package/dist/schema/work-scope-parser.spec.js +0 -66
  175. package/dist/service/update-service-status.spec.js +0 -27
  176. package/dist/testing/ensure-local-registry.js +0 -134
  177. package/dist/testing/example-test-suite.js +0 -63
  178. package/dist/testing/expect.js +0 -82
  179. package/dist/testing/get-test-suite.js +0 -9
  180. package/dist/testing/http-get.js +0 -21
  181. package/dist/testing/integration/cache.spec.js +0 -49
  182. package/dist/testing/integration/cancellation.spec.js +0 -45
  183. package/dist/testing/integration/clean.spec.js +0 -43
  184. package/dist/testing/integration/cmd.spec.js +0 -15
  185. package/dist/testing/integration/concurrency.spec.js +0 -19
  186. package/dist/testing/integration/docker-package-registry.spec.js +0 -44
  187. package/dist/testing/integration/docker-service-crash.spec.js +0 -36
  188. package/dist/testing/integration/docker.spec.js +0 -17
  189. package/dist/testing/integration/env.spec.js +0 -48
  190. package/dist/testing/integration/environment.spec.js +0 -97
  191. package/dist/testing/integration/error.spec.js +0 -19
  192. package/dist/testing/integration/export.spec.js +0 -22
  193. package/dist/testing/integration/extend.spec.js +0 -29
  194. package/dist/testing/integration/glob.spec.js +0 -40
  195. package/dist/testing/integration/include.spec.js +0 -31
  196. package/dist/testing/integration/invalid.spec.js +0 -17
  197. package/dist/testing/integration/invalid_loop.spec.js +0 -56
  198. package/dist/testing/integration/k8s-state-cache.spec.js +0 -41
  199. package/dist/testing/integration/kubernetes-service.spec.js +0 -148
  200. package/dist/testing/integration/kubernetes.spec.js +0 -99
  201. package/dist/testing/integration/labels.spec.js +0 -42
  202. package/dist/testing/integration/local.spec.js +0 -14
  203. package/dist/testing/integration/monorepo.spec.js +0 -15
  204. package/dist/testing/integration/multi-line.spec.js +0 -17
  205. package/dist/testing/integration/reference.spec.js +0 -21
  206. package/dist/testing/integration/services.spec.js +0 -38
  207. package/dist/testing/integration/store.spec.js +0 -52
  208. package/dist/testing/integration/unknown.spec.js +0 -28
  209. package/dist/testing/integration/validate.spec.js +0 -160
  210. package/dist/testing/integration/watch-dependency.spec.js +0 -33
  211. package/dist/testing/integration/watch.spec.js +0 -45
  212. package/dist/testing/requires-kubernetes.js +0 -18
  213. package/dist/testing/requires-linux-containers.js +0 -18
  214. package/dist/testing/requires-windows-containers.js +0 -18
  215. package/dist/testing/test-case.js +0 -75
  216. package/dist/testing/test-streams.js +0 -28
  217. package/dist/testing/test-suite-setup.js +0 -3
  218. package/dist/testing/test-suite.js +0 -3
  219. package/dist/testing/testing-timeout.js +0 -32
  220. package/dist/utils/abort-event.spec.js +0 -46
  221. package/dist/utils/buffer-context.spec.js +0 -38
  222. package/dist/utils/ci.spec.js +0 -53
  223. package/dist/utils/debouncer.spec.js +0 -58
  224. package/dist/utils/emitter.spec.js +0 -34
  225. package/dist/utils/empty-writable.spec.js +0 -18
  226. package/dist/utils/environment-config.spec.js +0 -26
  227. package/dist/utils/fail-never.spec.js +0 -9
  228. package/dist/utils/sleep.spec.js +0 -22
  229. package/dist/version.spec.js +0 -12
  230. package/specs/build-file/caches/spec.md +0 -114
  231. package/specs/build-file/environment-variables/spec.md +0 -82
  232. package/specs/build-file/includes/spec.md +0 -60
  233. package/specs/build-file/references/spec.md +0 -56
  234. package/specs/build-file/spec.md +0 -62
  235. package/specs/cli/clean/spec.md +0 -85
  236. package/specs/cli/down/spec.md +0 -85
  237. package/specs/cli/execute/spec.md +0 -101
  238. package/specs/cli/init/spec.md +0 -64
  239. package/specs/cli/ls/spec.md +0 -66
  240. package/specs/cli/package/spec.md +0 -110
  241. package/specs/cli/spec.md +0 -73
  242. package/specs/cli/store-restore/spec.md +0 -74
  243. package/specs/cli/up/spec.md +0 -80
  244. package/specs/cli/validate/spec.md +0 -70
  245. package/specs/labels/spec.md +0 -91
  246. package/specs/service/container/spec.md +0 -120
  247. package/specs/service/kubernetes/spec.md +0 -97
  248. package/specs/service/spec.md +0 -105
  249. package/specs/task/caching/spec.md +0 -107
  250. package/specs/task/container/spec.md +0 -118
  251. package/specs/task/dependencies/spec.md +0 -70
  252. package/specs/task/extending/spec.md +0 -74
  253. package/specs/task/kubernetes/spec.md +0 -125
  254. package/specs/task/needs/spec.md +0 -59
  255. package/specs/task/spec.md +0 -87
  256. package/specs/task/watching/spec.md +0 -69
@@ -0,0 +1,214 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.explainWorkTree = exports.explainTask = exports.describeCause = void 0;
4
+ const state_key_1 = require("../executer/scheduler/state-key");
5
+ const work_task_cache_description_1 = require("../optimizer/work-task-cache-description");
6
+ const get_cache_directory_1 = require("../optimizer/get-cache-directory");
7
+ const last_resolved_1 = require("./last-resolved");
8
+ const work_instance_id_1 = require("../planner/work-instance-id");
9
+ function describeCause(cause) {
10
+ switch (cause.kind) {
11
+ case 'no-src':
12
+ return 'no src declared (uncacheable)';
13
+ case 'no-src-files':
14
+ return 'src matches no files (uncacheable)';
15
+ case 'dependency-uncacheable':
16
+ return `dependency always runs: ${cause.identifier}`;
17
+ case 'caching-disabled':
18
+ return 'caching disabled';
19
+ case 'never-cached':
20
+ return 'never cached';
21
+ case 'cache-format-changed':
22
+ return 'cache format changed';
23
+ case 'source-changed':
24
+ return `source changed: ${cause.identifier}`;
25
+ case 'source-added':
26
+ return `source added: ${cause.identifier}`;
27
+ case 'source-removed':
28
+ return `source removed: ${cause.identifier}`;
29
+ case 'env-changed':
30
+ return `environment variable changed: ${cause.identifier}`;
31
+ case 'command-changed':
32
+ return 'command changed';
33
+ case 'image-changed':
34
+ return 'image changed';
35
+ case 'dependency-changed':
36
+ return `dependency changed: ${cause.identifier}`;
37
+ }
38
+ }
39
+ exports.describeCause = describeCause;
40
+ async function safeCurrentStateKey(item, environment) {
41
+ // currentStateKey is read-only (a file read for local, a container *list* for
42
+ // docker/k8s — never a start). If the runtime is unreachable (offline, no
43
+ // daemon) explain must still answer, so failures degrade to "no runtime state".
44
+ try {
45
+ return await item.runtime.currentStateKey(environment);
46
+ }
47
+ catch {
48
+ return null;
49
+ }
50
+ }
51
+ async function safeHas(item, stateKey, environment) {
52
+ try {
53
+ return await item.data.caching.backend.has(item.id(), stateKey, environment);
54
+ }
55
+ catch {
56
+ return false;
57
+ }
58
+ }
59
+ function diffDescription(current, previous, causes) {
60
+ if (JSON.stringify(current.cmds) !== JSON.stringify(previous.cmds)) {
61
+ causes.push({ kind: 'command-changed' });
62
+ }
63
+ if (current.image !== previous.image) {
64
+ causes.push({ kind: 'image-changed' });
65
+ }
66
+ // shell / mounts / cwd / generates all alter how the task runs; the closed
67
+ // cause vocabulary folds them into "command changed".
68
+ if (current.shell !== previous.shell ||
69
+ JSON.stringify(current.mounts) !== JSON.stringify(previous.mounts) ||
70
+ current.cwd !== previous.cwd ||
71
+ JSON.stringify(current.generates) !== JSON.stringify(previous.generates)) {
72
+ if (!causes.some((c) => c.kind === 'command-changed')) {
73
+ causes.push({ kind: 'command-changed' });
74
+ }
75
+ }
76
+ const currentEnvs = current.envs ?? {};
77
+ const previousEnvs = previous.envs ?? {};
78
+ const envKeys = Array.from(new Set([...Object.keys(currentEnvs), ...Object.keys(previousEnvs)])).sort();
79
+ for (const key of envKeys) {
80
+ if (currentEnvs[key] !== previousEnvs[key]) {
81
+ causes.push({ kind: 'env-changed', identifier: key });
82
+ }
83
+ }
84
+ }
85
+ function diffStats(current, previous, method, causes) {
86
+ const currentFiles = current.files ?? {};
87
+ const previousFiles = previous.files ?? {};
88
+ const paths = Array.from(new Set([...Object.keys(currentFiles), ...Object.keys(previousFiles)])).sort();
89
+ for (const path of paths) {
90
+ const now = currentFiles[path];
91
+ const before = previousFiles[path];
92
+ if (now && !before) {
93
+ causes.push({ kind: 'source-added', identifier: path });
94
+ }
95
+ else if (!now && before) {
96
+ causes.push({ kind: 'source-removed', identifier: path });
97
+ }
98
+ else if (now && before) {
99
+ const changed = method === 'modify-date' ? now.lastModified !== before.lastModified : now.checksum !== before.checksum;
100
+ if (changed) {
101
+ causes.push({ kind: 'source-changed', identifier: path });
102
+ }
103
+ }
104
+ }
105
+ }
106
+ function isAlwaysRun(explanation) {
107
+ return (explanation.status === 'uncacheable' &&
108
+ explanation.causes.some((c) => c.kind === 'no-src' || c.kind === 'no-src-files' || c.kind === 'dependency-uncacheable'));
109
+ }
110
+ // Explain a single task, recursively explaining its dependencies so a downstream
111
+ // task whose own inputs are unchanged is attributed to the upstream dependency
112
+ // that actually changed. Memoized by task id; `visiting` guards against a cyclic
113
+ // graph (already rejected by the planner, but explain runs before scheduling).
114
+ async function explainTask(item, defaultCacheMethod, environment, memo = new Map(), visiting = new Set()) {
115
+ const existing = memo.get(item.id());
116
+ if (existing) {
117
+ return existing;
118
+ }
119
+ if (visiting.has(item.id())) {
120
+ // Cycle: report neutrally rather than recursing forever.
121
+ return { taskId: item.id(), taskName: item.name, status: 'miss', causes: [{ kind: 'never-cached' }] };
122
+ }
123
+ visiting.add(item.id());
124
+ const { stateKey, stats, resolved, provable, unmatched } = await (0, state_key_1.computeStateKey)(item, defaultCacheMethod, environment);
125
+ // Mirrors the scheduler: a task that cannot be proven up to date always runs
126
+ // (see computeStateKey), whatever a cache holds for it.
127
+ if (!provable) {
128
+ const causes = [];
129
+ const declared = item.data.src.filter((src) => !src.inherited);
130
+ if (declared.length === 0) {
131
+ causes.push({ kind: 'no-src' });
132
+ }
133
+ else if (unmatched.length === declared.length) {
134
+ causes.push({ kind: 'no-src-files' });
135
+ }
136
+ for (const dep of item.deps) {
137
+ const depExplanation = await explainTask(dep, defaultCacheMethod, environment, memo, visiting);
138
+ if (isAlwaysRun(depExplanation)) {
139
+ causes.push({ kind: 'dependency-uncacheable', identifier: dep.name });
140
+ }
141
+ }
142
+ const explanation = { taskId: item.id(), taskName: item.name, status: 'uncacheable', causes };
143
+ memo.set(item.id(), explanation);
144
+ visiting.delete(item.id());
145
+ return explanation;
146
+ }
147
+ if (resolved.method === 'none') {
148
+ const explanation = {
149
+ taskId: item.id(),
150
+ taskName: item.name,
151
+ status: 'uncacheable',
152
+ causes: [{ kind: 'caching-disabled' }],
153
+ };
154
+ memo.set(item.id(), explanation);
155
+ visiting.delete(item.id());
156
+ return explanation;
157
+ }
158
+ const runtimeStateKey = await safeCurrentStateKey(item, environment);
159
+ const hasEntry = await safeHas(item, stateKey, environment);
160
+ const predictedHit = runtimeStateKey === stateKey || hasEntry;
161
+ if (predictedHit) {
162
+ const explanation = { taskId: item.id(), taskName: item.name, status: 'hit', causes: [] };
163
+ memo.set(item.id(), explanation);
164
+ visiting.delete(item.id());
165
+ return explanation;
166
+ }
167
+ const causes = [];
168
+ const record = await (0, last_resolved_1.readLastResolvedRecord)(environment, item);
169
+ if (record === null) {
170
+ const legacyDesc = await environment.file.exists((0, get_cache_directory_1.getCacheDescriptionFile)((0, work_instance_id_1.getWorkInstanceId)(item)));
171
+ if (runtimeStateKey === null && !hasEntry && !legacyDesc) {
172
+ causes.push({ kind: 'never-cached' });
173
+ }
174
+ else {
175
+ // Prior evidence of a run, but no diffable breakdown (pre-feature entry).
176
+ causes.push({ kind: 'cache-format-changed' });
177
+ }
178
+ }
179
+ else {
180
+ // records store env values as digests, so compare in the stored form
181
+ diffDescription((0, work_task_cache_description_1.storedCacheDescription)((0, work_task_cache_description_1.getWorkTaskCacheDescription)(item)), record.description, causes);
182
+ diffStats(stats, record.stats, resolved.method, causes);
183
+ }
184
+ for (const dep of item.deps) {
185
+ const depExplanation = await explainTask(dep, defaultCacheMethod, environment, memo, visiting);
186
+ if (depExplanation.status !== 'hit') {
187
+ causes.push({ kind: 'dependency-changed', identifier: dep.name });
188
+ }
189
+ }
190
+ if (causes.length === 0) {
191
+ // A miss we could not attribute (e.g. the artifact was pruned/evicted though
192
+ // metadata records a prior run) — treat as no usable cache entry.
193
+ causes.push({ kind: 'never-cached' });
194
+ }
195
+ const explanation = { taskId: item.id(), taskName: item.name, status: 'miss', causes };
196
+ memo.set(item.id(), explanation);
197
+ visiting.delete(item.id());
198
+ return explanation;
199
+ }
200
+ exports.explainTask = explainTask;
201
+ // Explain every task in scope (the work tree already includes dependencies),
202
+ // deterministically ordered by name. Read-only: executes no command, starts no
203
+ // container/service, performs no cache push/pull (only metadata `has` lookups).
204
+ async function explainWorkTree(workTree, defaultCacheMethod, environment) {
205
+ const memo = new Map();
206
+ const tasks = Object.values(workTree.tasks).sort((a, b) => a.name.localeCompare(b.name));
207
+ const results = [];
208
+ for (const task of tasks) {
209
+ results.push(await explainTask(task, defaultCacheMethod, environment, memo));
210
+ }
211
+ return results;
212
+ }
213
+ exports.explainWorkTree = explainWorkTree;
214
+ //# sourceMappingURL=explain.js.map
@@ -0,0 +1,48 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.readLastResolvedRecord = exports.writeLastResolvedRecord = exports.getLastResolvedFile = void 0;
4
+ const crypto_1 = require("crypto");
5
+ const path_1 = require("path");
6
+ const work_task_cache_description_1 = require("../optimizer/work-task-cache-description");
7
+ // Stored in the project's `.hammerkit` dir (local-first, alongside the local
8
+ // runtime state file) keyed by task name. The name is sanitized for the
9
+ // filesystem and suffixed with a hash of the full name to avoid collisions
10
+ // between names that sanitize to the same string (e.g. referenced `lib:build`).
11
+ function getLastResolvedFile(task) {
12
+ const sanitized = task.name.replace(/[^a-z0-9-_]+/gi, '_').slice(0, 80);
13
+ const suffix = (0, crypto_1.createHash)('sha1').update(task.name).digest('hex').slice(0, 8);
14
+ return (0, path_1.join)(task.data.cwd, '.hammerkit', 'last-resolved', `${sanitized}-${suffix}.json`);
15
+ }
16
+ exports.getLastResolvedFile = getLastResolvedFile;
17
+ async function writeLastResolvedRecord(environment, task, record) {
18
+ const file = getLastResolvedFile(task);
19
+ await environment.file.createDirectory((0, path_1.join)(task.data.cwd, '.hammerkit', 'last-resolved'));
20
+ const payload = {
21
+ taskId: task.id(),
22
+ taskName: task.name,
23
+ description: (0, work_task_cache_description_1.storedCacheDescription)(record.description),
24
+ stats: record.stats,
25
+ };
26
+ await environment.file.writeFile(file, JSON.stringify(payload));
27
+ }
28
+ exports.writeLastResolvedRecord = writeLastResolvedRecord;
29
+ async function readLastResolvedRecord(environment, task) {
30
+ const file = getLastResolvedFile(task);
31
+ if (!(await environment.file.exists(file))) {
32
+ return null;
33
+ }
34
+ try {
35
+ const parsed = JSON.parse(await environment.file.read(file));
36
+ // A record written by an older hammerkit (or otherwise truncated) lacks the
37
+ // component breakdown explain needs — signal that rather than crashing.
38
+ if (!parsed || typeof parsed !== 'object' || !parsed.description || !parsed.stats) {
39
+ return null;
40
+ }
41
+ return parsed;
42
+ }
43
+ catch {
44
+ return null;
45
+ }
46
+ }
47
+ exports.readLastResolvedRecord = readLastResolvedRecord;
48
+ //# sourceMappingURL=last-resolved.js.map
@@ -0,0 +1,15 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isCacheReadOnly = exports.CACHE_READ_ONLY_ENV = void 0;
4
+ exports.CACHE_READ_ONLY_ENV = 'HAMMERKIT_CACHE_READ_ONLY';
5
+ // Read-only is on when the flag is passed or the env var is set to a truthy
6
+ // value, so a sandbox image can enforce it once for every invocation.
7
+ function isCacheReadOnly(flag, processEnvs) {
8
+ if (flag) {
9
+ return true;
10
+ }
11
+ const value = (processEnvs[exports.CACHE_READ_ONLY_ENV] ?? '').trim().toLowerCase();
12
+ return value !== '' && value !== '0' && value !== 'false' && value !== 'no';
13
+ }
14
+ exports.isCacheReadOnly = isCacheReadOnly;
15
+ //# sourceMappingURL=read-only.js.map
@@ -0,0 +1,61 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.planRetention = void 0;
4
+ function recency(entry) {
5
+ return entry.lastAccessedAt ?? entry.createdAt;
6
+ }
7
+ // most recent first; entries without a timestamp count as the oldest
8
+ function byRecencyDesc(a, b) {
9
+ return (recency(b) ?? -Infinity) - (recency(a) ?? -Infinity);
10
+ }
11
+ const entries = (count) => `${count} ${count === 1 ? 'entry' : 'entries'}`;
12
+ // Decide which cache entries a retention policy evicts. Pure: listing and
13
+ // removing is the caller's job. Applied in order keepPerTask → maxAge →
14
+ // maxSize, each on what the previous step kept. Evicting never causes a false
15
+ // hit, only a rebuild.
16
+ function planRetention(all, policy, now) {
17
+ const evict = [];
18
+ const unavailable = [];
19
+ let keep = [...all];
20
+ if (policy.keepPerTask !== undefined) {
21
+ const byTask = new Map();
22
+ for (const entry of keep) {
23
+ byTask.set(entry.taskId, [...(byTask.get(entry.taskId) ?? []), entry]);
24
+ }
25
+ keep = [];
26
+ for (const versions of byTask.values()) {
27
+ versions.sort(byRecencyDesc);
28
+ keep.push(...versions.slice(0, policy.keepPerTask));
29
+ evict.push(...versions.slice(policy.keepPerTask));
30
+ }
31
+ }
32
+ if (policy.maxAge !== undefined) {
33
+ const maxAge = policy.maxAge;
34
+ const undated = keep.filter((entry) => recency(entry) === null);
35
+ if (undated.length > 0) {
36
+ unavailable.push(`max-age: ${entries(undated.length)} ${undated.length === 1 ? 'has' : 'have'} no timestamp and ${undated.length === 1 ? 'was' : 'were'} kept`);
37
+ }
38
+ evict.push(...keep.filter((entry) => recency(entry) !== null && now - recency(entry) > maxAge));
39
+ keep = keep.filter((entry) => !evict.includes(entry));
40
+ }
41
+ if (policy.maxSize !== undefined) {
42
+ const unsized = keep.filter((entry) => entry.size === null);
43
+ if (unsized.length > 0) {
44
+ unavailable.push(`max-size: the backend cannot report the size of ${entries(unsized.length)}`);
45
+ }
46
+ else {
47
+ let total = keep.reduce((sum, entry) => sum + entry.size, 0);
48
+ for (const entry of [...keep].sort(byRecencyDesc).reverse()) {
49
+ if (total <= policy.maxSize) {
50
+ break;
51
+ }
52
+ evict.push(entry);
53
+ total -= entry.size;
54
+ }
55
+ keep = keep.filter((entry) => !evict.includes(entry));
56
+ }
57
+ }
58
+ return { keep, evict, unavailable };
59
+ }
60
+ exports.planRetention = planRetention;
61
+ //# sourceMappingURL=retention.js.map
package/dist/cli.js CHANGED
@@ -14,6 +14,11 @@ const get_scheduler_execute_result_1 = require("./executer/get-scheduler-execute
14
14
  const reset_work_tree_1 = require("./executer/reset-work-tree");
15
15
  const execute_work_tree_1 = require("./executer/execute-work-tree");
16
16
  const package_1 = require("./docker/package");
17
+ const explain_1 = require("./cache/explain");
18
+ const work_graph_serializer_1 = require("./planner/work-graph-serializer");
19
+ const dry_run_1 = require("./executer/dry-run");
20
+ const cache_sync_1 = require("./cache/cache-sync");
21
+ const cache_inventory_1 = require("./cache/cache-inventory");
17
22
  const isCliTask = (val) => val.type === 'task';
18
23
  exports.isCliTask = isCliTask;
19
24
  const isCliService = (val) => val.type === 'service';
@@ -48,6 +53,10 @@ class Cli {
48
53
  workers: options?.workers ?? 0,
49
54
  processManager,
50
55
  type,
56
+ explain: options?.explain ?? false,
57
+ cacheReadOnly: options?.cacheReadOnly ?? false,
58
+ timeout: options?.timeout ?? null,
59
+ skipDeps: options?.skipDeps ?? true,
51
60
  });
52
61
  }
53
62
  const result = (0, get_scheduler_execute_result_1.getSchedulerExecuteResult)(workTree);
@@ -104,6 +113,40 @@ class Cli {
104
113
  validate() {
105
114
  return (0, validate_1.validate)(this.workTree, this.environment);
106
115
  }
116
+ // Read-only cache prediction for every task in scope: executes no command,
117
+ // starts no container/service, performs no cache push/pull.
118
+ async explain(options) {
119
+ return (0, explain_1.explainWorkTree)(this.workTree, options?.cacheDefault ?? 'checksum', this.environment);
120
+ }
121
+ // Serialize the in-scope work graph for rendering (mermaid/dot). Pure read of
122
+ // the planner's graph — nothing executes.
123
+ graph(format) {
124
+ return (0, work_graph_serializer_1.serializeWorkGraph)(this.workTree, format);
125
+ }
126
+ // Ordered execution plan with predicted cache decisions, reusing the explain
127
+ // engine — executes nothing, starts nothing, performs no cache push/pull.
128
+ async dryRun(options) {
129
+ return (0, dry_run_1.planDryRun)(this.workTree, options?.cacheDefault ?? 'checksum', this.environment);
130
+ }
131
+ // Move cache entries between the tasks' own caches and a named remote without
132
+ // executing anything (`cache pull` / `cache push`).
133
+ async syncCache(options) {
134
+ return (0, cache_sync_1.syncCache)(this.workTree, options, this.environment);
135
+ }
136
+ // Entries of a named cache (default: the local `default` cache).
137
+ async listCache(cacheName) {
138
+ return (0, cache_inventory_1.listCache)(this.workTree, cacheName, this.environment);
139
+ }
140
+ retentionPolicy(cacheName, overrides) {
141
+ return (0, cache_inventory_1.retentionPolicyOf)(this.workTree, cacheName, overrides);
142
+ }
143
+ async pruneCache(cacheName, policy, dryRun = false) {
144
+ return (0, cache_inventory_1.pruneCache)(this.workTree, cacheName, policy, { dryRun }, this.environment);
145
+ }
146
+ // Apply declared retention to the local caches this build file uses.
147
+ async autoPrune() {
148
+ return (0, cache_inventory_1.autoPrune)(this.workTree, this.environment);
149
+ }
107
150
  task(name) {
108
151
  const task = this.workTree.tasks[name];
109
152
  if (!task) {
@@ -4,6 +4,7 @@ exports.usingContainer = void 0;
4
4
  const remove_container_1 = require("./remove-container");
5
5
  const log_1 = require("../log");
6
6
  const execute_docker_1 = require("../executer/execute-docker");
7
+ const work_instance_id_1 = require("../planner/work-instance-id");
7
8
  async function usingContainer(docker, item, createOptions, stateKey, callback) {
8
9
  let container = null;
9
10
  let succeeded = false;
@@ -22,7 +23,10 @@ async function usingContainer(docker, item, createOptions, stateKey, callback) {
22
23
  if (container) {
23
24
  await container.pause();
24
25
  }
25
- const containers = await docker.listContainers({ all: true, filters: { label: [`hammerkit-id=${item.id()}`] } });
26
+ const containers = await docker.listContainers({
27
+ all: true,
28
+ filters: { label: [`hammerkit-id=${(0, work_instance_id_1.getWorkInstanceId)(item)}`] },
29
+ });
26
30
  for (const container of containers) {
27
31
  item.status.write('debug', `found container ${container.Id}`);
28
32
  if (container.Labels['hammerkit-state'] != stateKey) {
@@ -0,0 +1,21 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.archiveTaskEntry = void 0;
4
+ const get_cache_directory_1 = require("../optimizer/get-cache-directory");
5
+ const work_instance_id_1 = require("../planner/work-instance-id");
6
+ const get_work_cache_stats_1 = require("../optimizer/get-work-cache-stats");
7
+ const cache_metadata_1 = require("./cache-metadata");
8
+ const work_task_cache_description_1 = require("../optimizer/work-task-cache-description");
9
+ // Package a task's current outputs as a cache entry (metadata + output
10
+ // archives) in its staging directory, and return that directory for a backend
11
+ // to push from. Used after a task ran, and by `cache push` for a task whose
12
+ // outputs are up to date in this checkout but were never stored in a backend.
13
+ async function archiveTaskEntry(work, environment) {
14
+ const cacheDir = (0, get_cache_directory_1.getCacheDirectory)((0, work_instance_id_1.getWorkInstanceId)(work));
15
+ const { stats } = await (0, get_work_cache_stats_1.getWorkItemCacheStats)(work, environment);
16
+ await (0, cache_metadata_1.writeCacheMetadata)(environment, work, stats, (0, work_task_cache_description_1.getWorkTaskCacheDescription)(work));
17
+ await work.runtime.archive(environment, cacheDir);
18
+ return cacheDir;
19
+ }
20
+ exports.archiveTaskEntry = archiveTaskEntry;
21
+ //# sourceMappingURL=archive-task-entry.js.map
@@ -1,19 +1,30 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.awaitRunningNeeds = exports.awaitRunningNeed = exports.awaitCompleted = exports.awaitCompletedDependencies = exports.awaitNoRequirements = exports.awaitRequirement = void 0;
3
+ exports.awaitRunningNeeds = exports.awaitRunningNeed = exports.awaitCompleted = exports.awaitCompletedDependencies = exports.awaitNoRequirements = exports.awaitDependentNeed = exports.awaitRequirement = void 0;
4
4
  const work_item_1 = require("../planner/work-item");
5
5
  const state_resolver_1 = require("./state-resolver");
6
- async function awaitRequirement(svc, abort) {
7
- await Promise.race(svc.requiredBy.map((required) => {
8
- if ((0, work_item_1.isWorkTaskItem)(required)) {
9
- return (0, state_resolver_1.awaitState)('await-requirement', required.state, (state) => state.type === 'ready', abort);
10
- }
11
- else {
12
- return (0, state_resolver_1.awaitState)('await-requirement', required.state, (state) => state.type === 'ready', abort);
13
- }
14
- }));
6
+ // Resolve true once any requirer is `ready` (about to run). With
7
+ // `untilAllDone`, resolve false instead when every requirer has finished
8
+ // without needing the service — e.g. all of them were cache hits — so the
9
+ // service is never started.
10
+ async function awaitRequirement(svc, abort, options = { untilAllDone: false }) {
11
+ const ready = Promise.race(svc.requiredBy.map((required) => (0, state_resolver_1.awaitState)('await-requirement', required.state, (state) => state.type === 'ready', abort))).then(() => true);
12
+ if (!options.untilAllDone) {
13
+ return ready;
14
+ }
15
+ const allDone = awaitNoRequirements(svc, abort).then(() => false);
16
+ return Promise.race([ready, allDone]);
15
17
  }
16
18
  exports.awaitRequirement = awaitRequirement;
19
+ // Resolve true once any dependent is `ready` — it missed the cache and is about
20
+ // to run, so it needs this task's outputs — and false once every dependent
21
+ // finished without needing it (all cache hits, or skipped themselves).
22
+ async function awaitDependentNeed(dependents, abort) {
23
+ const ready = Promise.race(dependents.map((dependent) => (0, state_resolver_1.awaitState)('await-dependent', dependent.state, (s) => s.type === 'ready', abort))).then(() => true);
24
+ const allDone = Promise.all(dependents.map((dependent) => (0, state_resolver_1.awaitState)('await-dependent-done', dependent.state, (s) => s.type === 'completed' || s.type === 'error' || s.type === 'crash' || s.type === 'canceled', abort))).then(() => false);
25
+ return Promise.race([ready, allDone]);
26
+ }
27
+ exports.awaitDependentNeed = awaitDependentNeed;
17
28
  async function awaitNoRequirements(svc, abort) {
18
29
  await Promise.all(svc.requiredBy.map((required) => {
19
30
  if ((0, work_item_1.isWorkTaskItem)(required)) {
@@ -36,8 +47,15 @@ function awaitCompleted(work, dep, abort) {
36
47
  return (0, state_resolver_1.isState)('await-completed-' + work.name, dep.state, isCompleted, abort);
37
48
  }
38
49
  exports.awaitCompleted = awaitCompleted;
39
- function awaitRunningNeed(work, dep, abort) {
40
- return (0, state_resolver_1.isState)('await-running-need-' + work.name, dep.state, isRunning, abort);
50
+ // Wait for a needed service to run. A service that reaches a terminal state
51
+ // first (it failed to start or exited) can never become ready, so the waiting
52
+ // item fails instead of waiting forever.
53
+ async function awaitRunningNeed(work, dep, abort) {
54
+ const state = await (0, state_resolver_1.isState)('await-running-need-' + work.name, dep.state, isRunningOrDone, abort);
55
+ if (state.type !== 'running') {
56
+ throw new Error(`needed service ${dep.name} ${state.type === 'end' ? 'stopped' : 'failed'} before it was ready`);
57
+ }
58
+ return state;
41
59
  }
42
60
  exports.awaitRunningNeed = awaitRunningNeed;
43
61
  async function awaitRunningNeeds(work, needs, abort) {
@@ -50,7 +68,7 @@ exports.awaitRunningNeeds = awaitRunningNeeds;
50
68
  function isCompleted(val) {
51
69
  return val.type === 'completed';
52
70
  }
53
- function isRunning(val) {
54
- return val.type === 'running';
71
+ function isRunningOrDone(val) {
72
+ return val.type === 'running' || val.type === 'end' || val.type === 'error' || val.type === 'canceled';
55
73
  }
56
74
  //# sourceMappingURL=await-completed-dependencies.js.map
@@ -2,10 +2,17 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.writeCacheMetadata = void 0;
4
4
  const get_cache_directory_1 = require("../optimizer/get-cache-directory");
5
- async function writeCacheMetadata(environment, taskId, stats, description) {
5
+ const work_task_cache_description_1 = require("../optimizer/work-task-cache-description");
6
+ const last_resolved_1 = require("../cache/last-resolved");
7
+ const work_instance_id_1 = require("../planner/work-instance-id");
8
+ async function writeCacheMetadata(environment, task, stats, description) {
9
+ const taskId = (0, work_instance_id_1.getWorkInstanceId)(task);
6
10
  await environment.file.createDirectory((0, get_cache_directory_1.getCacheDirectory)(taskId));
7
11
  await environment.file.writeFile((0, get_cache_directory_1.getCacheStatsFile)(taskId), JSON.stringify(stats));
8
- await environment.file.writeFile((0, get_cache_directory_1.getCacheDescriptionFile)(taskId), JSON.stringify(description));
12
+ await environment.file.writeFile((0, get_cache_directory_1.getCacheDescriptionFile)(taskId), JSON.stringify((0, work_task_cache_description_1.storedCacheDescription)(description)));
13
+ // Additive per-task-name record for `explain` — never affects the hit/miss
14
+ // decision, written wherever the id-keyed metadata is.
15
+ await (0, last_resolved_1.writeLastResolvedRecord)(environment, task, { description, stats });
9
16
  }
10
17
  exports.writeCacheMetadata = writeCacheMetadata;
11
18
  //# sourceMappingURL=cache-metadata.js.map
@@ -12,6 +12,7 @@ const execution_steps_1 = require("./execution-steps");
12
12
  const docker_task_1 = require("./docker-task");
13
13
  const replace_env_variables_1 = require("../environment/replace-env-variables");
14
14
  const get_service_containers_1 = require("./get-service-containers");
15
+ const work_instance_id_1 = require("../planner/work-instance-id");
15
16
  async function dockerService(docker, item, options, environment) {
16
17
  let container = null;
17
18
  try {
@@ -30,7 +31,7 @@ async function dockerService(docker, item, options, environment) {
30
31
  Env: Object.keys(envs).map((k) => `${k}=${envs[k]}`),
31
32
  Labels: {
32
33
  app: 'hammerkit',
33
- 'hammerkit-id': item.id(),
34
+ 'hammerkit-id': (0, work_instance_id_1.getWorkInstanceId)(item),
34
35
  'hammerkit-pid': process.pid.toString(),
35
36
  'hammerkit-type': 'service',
36
37
  'hammerkit-state': options.stateKey,
@@ -12,6 +12,7 @@ const tar_1 = require("tar");
12
12
  const replace_env_variables_1 = require("../environment/replace-env-variables");
13
13
  const get_container_binds_1 = require("./get-container-binds");
14
14
  const get_service_containers_1 = require("./get-service-containers");
15
+ const work_instance_id_1 = require("../planner/work-instance-id");
15
16
  function getNeedsNetwork(serviceContainers, needs) {
16
17
  const links = [];
17
18
  const hosts = [];
@@ -44,7 +45,7 @@ function buildCreateOptions(item, stateKey, serviceContainers, environment) {
44
45
  WorkingDir: (0, execute_docker_1.convertToPosixPath)(item.data.cwd),
45
46
  Labels: {
46
47
  app: 'hammerkit',
47
- 'hammerkit-id': item.id(),
48
+ 'hammerkit-id': (0, work_instance_id_1.getWorkInstanceId)(item),
48
49
  'hammerkit-pid': process.pid.toString(),
49
50
  'hammerkit-type': 'task',
50
51
  'hammerkit-state': stateKey,
@@ -0,0 +1,73 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.printDryRun = exports.planDryRun = void 0;
7
+ const colors_1 = __importDefault(require("colors"));
8
+ const validate_1 = require("../planner/validate");
9
+ const explain_1 = require("../cache/explain");
10
+ const log_1 = require("../log");
11
+ // Deterministic topological order (dependencies before dependents). Visited-set
12
+ // guards against runaway recursion; a real cycle is reported separately.
13
+ function topoOrder(workTree) {
14
+ const visited = new Set();
15
+ const order = [];
16
+ const visit = (task) => {
17
+ if (visited.has(task.id())) {
18
+ return;
19
+ }
20
+ visited.add(task.id());
21
+ for (const dep of [...task.deps].sort((a, b) => a.name.localeCompare(b.name))) {
22
+ visit(dep);
23
+ }
24
+ order.push(task);
25
+ };
26
+ for (const task of Object.values(workTree.tasks).sort((a, b) => a.name.localeCompare(b.name))) {
27
+ visit(task);
28
+ }
29
+ return order;
30
+ }
31
+ // Compute the ordered execution plan with a predicted cache decision per task,
32
+ // reusing the cache-explain engine — no command runs, no container/service
33
+ // starts, no cache push/pull occurs.
34
+ async function planDryRun(workTree, defaultCacheMethod, environment) {
35
+ for (const task of Object.values(workTree.tasks)) {
36
+ const path = (0, validate_1.hasDependencyCycle)(task, []);
37
+ if (path && path.length > 0) {
38
+ return { entries: [], cycle: path.map((n) => n.name) };
39
+ }
40
+ }
41
+ const explanations = await (0, explain_1.explainWorkTree)(workTree, defaultCacheMethod, environment);
42
+ const byName = new Map(explanations.map((e) => [e.taskName, e]));
43
+ const entries = [];
44
+ for (const task of topoOrder(workTree)) {
45
+ const explanation = byName.get(task.name);
46
+ entries.push({
47
+ taskId: task.id(),
48
+ taskName: task.name,
49
+ status: explanation?.status ?? 'miss',
50
+ causes: explanation?.causes ?? [],
51
+ });
52
+ }
53
+ return { entries, cycle: null };
54
+ }
55
+ exports.planDryRun = planDryRun;
56
+ function printDryRun(environment, plan) {
57
+ (0, log_1.printTitle)(environment, 'Dry run (no commands executed)');
58
+ if (plan.entries.length === 0) {
59
+ environment.stdout.write(' no tasks in scope\n');
60
+ return;
61
+ }
62
+ plan.entries.forEach((entry, index) => {
63
+ const label = entry.status === 'hit'
64
+ ? colors_1.default.green('cache hit')
65
+ : entry.status === 'uncacheable'
66
+ ? colors_1.default.grey('uncacheable')
67
+ : colors_1.default.yellow('cache miss');
68
+ const causes = entry.causes.length > 0 ? colors_1.default.grey(` (${entry.causes.map(explain_1.describeCause).join(', ')})`) : '';
69
+ environment.stdout.write(` ${index + 1}. ${entry.taskName}: ${label}${causes}\n`);
70
+ });
71
+ }
72
+ exports.printDryRun = printDryRun;
73
+ //# sourceMappingURL=dry-run.js.map