@depup/nx 23.2.1-depup.8 → 23.3.0-depup.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 (350) hide show
  1. package/README.md +7 -18
  2. package/changes.json +9 -53
  3. package/dist/bin/init-local.js +7 -1
  4. package/dist/nx-cloud-internals.d.ts +1 -0
  5. package/dist/nx-cloud-internals.js +9 -0
  6. package/dist/plugins/package-json.js +1 -1
  7. package/dist/src/command-line/add/add.js +5 -1
  8. package/dist/src/command-line/affected/affected.js +70 -9
  9. package/dist/src/command-line/affected/command-object.js +1 -1
  10. package/dist/src/command-line/format/format.js +2 -18
  11. package/dist/src/command-line/graph/graph.d.ts +31 -0
  12. package/dist/src/command-line/graph/graph.js +113 -20
  13. package/dist/src/command-line/init/implementation/add-nx-to-monorepo.js +0 -3
  14. package/dist/src/command-line/init/implementation/add-nx-to-nest.js +0 -3
  15. package/dist/src/command-line/init/implementation/add-nx-to-npm-repo.js +0 -3
  16. package/dist/src/command-line/init/implementation/angular/index.js +0 -3
  17. package/dist/src/command-line/init/implementation/angular/legacy-angular-versions.js +0 -3
  18. package/dist/src/command-line/init/implementation/format.js +2 -3
  19. package/dist/src/command-line/init/implementation/utils.d.ts +0 -1
  20. package/dist/src/command-line/init/implementation/utils.js +6 -9
  21. package/dist/src/command-line/init/init-v2.js +0 -3
  22. package/dist/src/command-line/migrate/agentic/capture-generator-output.d.ts +25 -0
  23. package/dist/src/command-line/migrate/agentic/capture-generator-output.js +121 -5
  24. package/dist/src/command-line/migrate/agentic/close-agent-session.d.ts +21 -0
  25. package/dist/src/command-line/migrate/agentic/close-agent-session.js +126 -0
  26. package/dist/src/command-line/migrate/agentic/definitions.d.ts +24 -0
  27. package/dist/src/command-line/migrate/agentic/definitions.js +79 -20
  28. package/dist/src/command-line/migrate/agentic/format-command.d.ts +14 -0
  29. package/dist/src/command-line/migrate/agentic/format-command.js +44 -0
  30. package/dist/src/command-line/migrate/agentic/handoff-gitignore.d.ts +3 -3
  31. package/dist/src/command-line/migrate/agentic/handoff-gitignore.js +3 -3
  32. package/dist/src/command-line/migrate/agentic/handoff.d.ts +50 -17
  33. package/dist/src/command-line/migrate/agentic/handoff.js +166 -24
  34. package/dist/src/command-line/migrate/agentic/instruction-files.d.ts +23 -0
  35. package/dist/src/command-line/migrate/agentic/instruction-files.js +41 -0
  36. package/dist/src/command-line/migrate/agentic/master/invocations.d.ts +12 -0
  37. package/dist/src/command-line/migrate/agentic/master/invocations.js +54 -0
  38. package/dist/src/command-line/migrate/agentic/master/run-master-session.d.ts +16 -0
  39. package/dist/src/command-line/migrate/agentic/master/run-master-session.js +175 -0
  40. package/dist/src/command-line/migrate/agentic/master/spawn-master.d.ts +31 -0
  41. package/dist/src/command-line/migrate/agentic/master/spawn-master.js +222 -0
  42. package/dist/src/command-line/migrate/agentic/prompts/final-validation.d.ts +13 -0
  43. package/dist/src/command-line/migrate/agentic/prompts/final-validation.js +41 -0
  44. package/dist/src/command-line/migrate/agentic/prompts/fragments.d.ts +17 -0
  45. package/dist/src/command-line/migrate/agentic/prompts/fragments.js +90 -0
  46. package/dist/src/command-line/migrate/agentic/prompts/generic-validation.js +4 -3
  47. package/dist/src/command-line/migrate/agentic/prompts/system-prompt.d.ts +25 -1
  48. package/dist/src/command-line/migrate/agentic/prompts/system-prompt.js +57 -34
  49. package/dist/src/command-line/migrate/agentic/run-step.js +28 -4
  50. package/dist/src/command-line/migrate/agentic/runner.d.ts +3 -15
  51. package/dist/src/command-line/migrate/agentic/runner.js +57 -239
  52. package/dist/src/command-line/migrate/agentic/terminal-repair.d.ts +1 -0
  53. package/dist/src/command-line/migrate/agentic/terminal-repair.js +27 -0
  54. package/dist/src/command-line/migrate/agentic/types.d.ts +23 -8
  55. package/dist/src/command-line/migrate/agentic/types.js +10 -7
  56. package/dist/src/command-line/migrate/agentic/windows-cmd.d.ts +33 -0
  57. package/dist/src/command-line/migrate/agentic/windows-cmd.js +88 -0
  58. package/dist/src/command-line/migrate/command-object.d.ts +2 -0
  59. package/dist/src/command-line/migrate/command-object.js +11 -1
  60. package/dist/src/command-line/migrate/deferred-output.d.ts +44 -0
  61. package/dist/src/command-line/migrate/deferred-output.js +92 -0
  62. package/dist/src/command-line/migrate/execute-migration.d.ts +9 -3
  63. package/dist/src/command-line/migrate/execute-migration.js +53 -16
  64. package/dist/src/command-line/migrate/migrate-analytics.d.ts +34 -9
  65. package/dist/src/command-line/migrate/migrate-analytics.js +71 -10
  66. package/dist/src/command-line/migrate/migrate-commits.d.ts +5 -4
  67. package/dist/src/command-line/migrate/migrate-commits.js +16 -6
  68. package/dist/src/command-line/migrate/migrate-config.d.ts +5 -2
  69. package/dist/src/command-line/migrate/migrate-config.js +19 -11
  70. package/dist/src/command-line/migrate/migrate-output.d.ts +3 -7
  71. package/dist/src/command-line/migrate/migrate-output.js +3 -7
  72. package/dist/src/command-line/migrate/migrate.d.ts +3 -0
  73. package/dist/src/command-line/migrate/migrate.js +183 -46
  74. package/dist/src/command-line/migrate/resolve-package-version.js +2 -19
  75. package/dist/src/command-line/migrate/run/agent-output.d.ts +11 -0
  76. package/dist/src/command-line/migrate/run/agent-output.js +21 -2
  77. package/dist/src/command-line/migrate/run/agent-work-payload.d.ts +37 -0
  78. package/dist/src/command-line/migrate/run/agent-work-payload.js +156 -0
  79. package/dist/src/command-line/migrate/run/atomic-write.d.ts +8 -0
  80. package/dist/src/command-line/migrate/run/atomic-write.js +17 -0
  81. package/dist/src/command-line/migrate/run/broker.d.ts +150 -0
  82. package/dist/src/command-line/migrate/run/broker.js +568 -0
  83. package/dist/src/command-line/migrate/run/clean-retry.d.ts +12 -0
  84. package/dist/src/command-line/migrate/run/clean-retry.js +99 -0
  85. package/dist/src/command-line/migrate/run/existing-run-report.d.ts +55 -0
  86. package/dist/src/command-line/migrate/run/existing-run-report.js +227 -0
  87. package/dist/src/command-line/migrate/run/give-up.d.ts +41 -0
  88. package/dist/src/command-line/migrate/run/give-up.js +148 -0
  89. package/dist/src/command-line/migrate/run/index.d.ts +9 -6
  90. package/dist/src/command-line/migrate/run/index.js +21 -2
  91. package/dist/src/command-line/migrate/run/issues.d.ts +127 -0
  92. package/dist/src/command-line/migrate/run/issues.js +1121 -0
  93. package/dist/src/command-line/migrate/run/orchestrator.d.ts +59 -1
  94. package/dist/src/command-line/migrate/run/orchestrator.js +1529 -457
  95. package/dist/src/command-line/migrate/run/run-id.d.ts +0 -8
  96. package/dist/src/command-line/migrate/run/run-id.js +0 -30
  97. package/dist/src/command-line/migrate/run/run-state.d.ts +77 -17
  98. package/dist/src/command-line/migrate/run/run-state.js +263 -32
  99. package/dist/src/command-line/migrate/run/runbook.d.ts +18 -0
  100. package/dist/src/command-line/migrate/run/runbook.js +118 -0
  101. package/dist/src/command-line/migrate/run/state-lock.d.ts +33 -2
  102. package/dist/src/command-line/migrate/run/state-lock.js +128 -2
  103. package/dist/src/command-line/migrate/run/state-machine.d.ts +50 -1
  104. package/dist/src/command-line/migrate/run/state-machine.js +339 -26
  105. package/dist/src/command-line/migrate/run/util.d.ts +3 -1
  106. package/dist/src/command-line/migrate/run/util.js +24 -3
  107. package/dist/src/command-line/migrate/run/worker.js +290 -129
  108. package/dist/src/command-line/migrate/step-actions.d.ts +1 -1
  109. package/dist/src/command-line/migrate/step-actions.js +7 -1
  110. package/dist/src/command-line/migrate/text.d.ts +6 -0
  111. package/dist/src/command-line/migrate/text.js +25 -0
  112. package/dist/src/command-line/migrate/version-skew-guard.js +2 -0
  113. package/dist/src/command-line/nx-cloud/connect/connect-to-nx-cloud.js +5 -11
  114. package/dist/src/command-line/release/publish.js +11 -8
  115. package/dist/src/command-line/release/utils/get-touched-projects-for-group.js +3 -3
  116. package/dist/src/command-line/release/utils/release-graph.js +1 -1
  117. package/dist/src/command-line/release/utils/remote-release-clients/github.js +5 -8
  118. package/dist/src/command-line/release/utils/remote-release-clients/gitlab.js +3 -3
  119. package/dist/src/command-line/release/utils/remote-release-clients/remote-release-client.d.ts +4 -2
  120. package/dist/src/command-line/release/utils/remote-release-clients/remote-release-client.js +37 -3
  121. package/dist/src/command-line/release/version/version-actions.d.ts +3 -0
  122. package/dist/src/command-line/reset/reset.js +10 -4
  123. package/dist/src/command-line/run/run-one.js +5 -1
  124. package/dist/src/command-line/run-many/run-many.js +6 -12
  125. package/dist/src/command-line/show/command-object.js +1 -1
  126. package/dist/src/command-line/show/projects.js +52 -1
  127. package/dist/src/command-line/yargs-utils/shared-options.d.ts +5 -1
  128. package/dist/src/command-line/yargs-utils/shared-options.js +38 -4
  129. package/dist/src/config/task-graph.d.ts +1 -1
  130. package/dist/src/config/workspace-json-project-json.d.ts +23 -1
  131. package/dist/src/core/graph/main.js +1 -1
  132. package/dist/src/core/graph/runtime.js +1 -1
  133. package/dist/src/core/graph/styles.js +1 -1
  134. package/dist/src/daemon/client/client.d.ts +14 -8
  135. package/dist/src/daemon/client/client.js +41 -12
  136. package/dist/src/daemon/message-types/get-plugin-capabilities.d.ts +5 -0
  137. package/dist/src/daemon/message-types/get-plugin-capabilities.js +11 -0
  138. package/dist/src/daemon/message-types/run-tasks-execution-hooks.d.ts +3 -2
  139. package/dist/src/daemon/message-types/select-affected-tasks.d.ts +21 -0
  140. package/dist/src/daemon/message-types/select-affected-tasks.js +11 -0
  141. package/dist/src/daemon/message-types/ultracache-configuration-version.d.ts +8 -0
  142. package/dist/src/daemon/message-types/ultracache-configuration-version.js +2 -0
  143. package/dist/src/daemon/server/dotenv-graph-changes.d.ts +1 -1
  144. package/dist/src/daemon/server/dotenv-graph-changes.js +2 -2
  145. package/dist/src/daemon/server/handle-get-plugin-capabilities.d.ts +2 -0
  146. package/dist/src/daemon/server/handle-get-plugin-capabilities.js +13 -0
  147. package/dist/src/daemon/server/handle-hash-tasks.d.ts +10 -2
  148. package/dist/src/daemon/server/handle-hash-tasks.js +18 -3
  149. package/dist/src/daemon/server/handle-select-affected-tasks.d.ts +3 -0
  150. package/dist/src/daemon/server/handle-select-affected-tasks.js +26 -0
  151. package/dist/src/daemon/server/handle-tasks-execution-hooks.d.ts +3 -2
  152. package/dist/src/daemon/server/{handle-outputs-changes.d.ts → handle-watch-events.d.ts} +4 -4
  153. package/dist/src/daemon/server/{handle-outputs-changes.js → handle-watch-events.js} +36 -37
  154. package/dist/src/daemon/server/outputs-tracking.d.ts +7 -19
  155. package/dist/src/daemon/server/outputs-tracking.js +7 -107
  156. package/dist/src/daemon/server/planning-context.d.ts +8 -0
  157. package/dist/src/daemon/server/planning-context.js +18 -0
  158. package/dist/src/daemon/server/project-graph-incremental-recomputation.d.ts +22 -7
  159. package/dist/src/daemon/server/project-graph-incremental-recomputation.js +104 -74
  160. package/dist/src/daemon/server/restart-checks.d.ts +22 -0
  161. package/dist/src/daemon/server/restart-checks.js +77 -0
  162. package/dist/src/daemon/server/server.d.ts +1 -0
  163. package/dist/src/daemon/server/server.js +54 -30
  164. package/dist/src/daemon/server/shutdown-utils.d.ts +0 -5
  165. package/dist/src/daemon/server/shutdown-utils.js +3 -26
  166. package/dist/src/daemon/server/ultracache-configuration-state.d.ts +7 -0
  167. package/dist/src/daemon/server/ultracache-configuration-state.js +36 -0
  168. package/dist/src/devkit-internals.d.ts +3 -1
  169. package/dist/src/devkit-internals.js +8 -3
  170. package/dist/src/executors/run-commands/run-commands.impl.d.ts +2 -1
  171. package/dist/src/executors/run-commands/run-commands.impl.js +19 -5
  172. package/dist/src/executors/run-commands/running-tasks.d.ts +5 -4
  173. package/dist/src/executors/run-commands/running-tasks.js +13 -13
  174. package/dist/src/hasher/create-task-hasher.d.ts +8 -1
  175. package/dist/src/hasher/create-task-hasher.js +13 -3
  176. package/dist/src/hasher/hash-plan-inspector.js +2 -2
  177. package/dist/src/hasher/hash-task.d.ts +2 -2
  178. package/dist/src/hasher/hash-task.js +28 -17
  179. package/dist/src/hasher/native-task-hasher-impl.d.ts +20 -4
  180. package/dist/src/hasher/native-task-hasher-impl.js +75 -9
  181. package/dist/src/hasher/task-hasher.d.ts +24 -7
  182. package/dist/src/hasher/task-hasher.js +27 -8
  183. package/dist/src/hasher/task-planning-context.d.ts +13 -0
  184. package/dist/src/hasher/task-planning-context.js +12 -0
  185. package/dist/src/native/index.d.ts +582 -21
  186. package/dist/src/native/index.js +3 -3
  187. package/dist/src/native/native-bindings.js +13 -1
  188. package/dist/src/native/nx.wasm32-wasi.debug.wasm +0 -0
  189. package/dist/src/native/nx.wasm32-wasi.wasm +0 -0
  190. package/dist/src/native/transform-objects.d.ts +11 -2
  191. package/dist/src/native/transform-objects.js +46 -0
  192. package/dist/src/nx-cloud/generators/connect-to-nx-cloud/connect-to-nx-cloud.js +15 -8
  193. package/dist/src/nx-cloud/update-manager.d.ts +3 -0
  194. package/dist/src/nx-cloud/update-manager.js +19 -13
  195. package/dist/src/nx-cloud/utilities/client.d.ts +1 -1
  196. package/dist/src/nx-cloud/utilities/client.js +1 -1
  197. package/dist/src/nx-cloud/utilities/is-workspace-claimed.js +2 -1
  198. package/dist/src/nx-cloud/utilities/nx-cloud-http-client.d.ts +5 -0
  199. package/dist/src/nx-cloud/utilities/nx-cloud-http-client.js +16 -0
  200. package/dist/src/nx-cloud/utilities/url-shorten.js +17 -10
  201. package/dist/src/plugins/js/index.js +45 -64
  202. package/dist/src/plugins/js/lock-file/lock-file.d.ts +4 -4
  203. package/dist/src/plugins/js/lock-file/lock-file.js +6 -7
  204. package/dist/src/plugins/js/lock-file/pnpm-parser.js +1 -1
  205. package/dist/src/plugins/js/lock-file/utils/pnpm-normalizer.d.ts +2 -0
  206. package/dist/src/plugins/js/lock-file/utils/pnpm-normalizer.js +22 -0
  207. package/dist/src/plugins/js/project-graph/affected/lock-file-changes.d.ts +10 -2
  208. package/dist/src/plugins/js/project-graph/affected/lock-file-changes.js +51 -29
  209. package/dist/src/plugins/js/project-graph/affected/npm-packages.d.ts +10 -2
  210. package/dist/src/plugins/js/project-graph/affected/npm-packages.js +32 -11
  211. package/dist/src/plugins/js/project-graph/build-dependencies/target-project-locator.js +22 -18
  212. package/dist/src/plugins/js/utils/register.js +13 -4
  213. package/dist/src/plugins/package-json/create-nodes.d.ts +2 -2
  214. package/dist/src/plugins/package-json/create-nodes.js +13 -13
  215. package/dist/src/project-graph/affected/affected-project-graph-models.d.ts +14 -0
  216. package/dist/src/project-graph/affected/affected-project-graph.js +5 -18
  217. package/dist/src/project-graph/affected/affected-projects.d.ts +7 -0
  218. package/dist/src/project-graph/affected/affected-projects.js +42 -0
  219. package/dist/src/project-graph/affected/affected-reasons.d.ts +116 -0
  220. package/dist/src/project-graph/affected/affected-reasons.js +459 -0
  221. package/dist/src/project-graph/affected/affected-tasks.d.ts +103 -0
  222. package/dist/src/project-graph/affected/affected-tasks.js +386 -0
  223. package/dist/src/project-graph/affected/print-explanation.d.ts +11 -0
  224. package/dist/src/project-graph/affected/print-explanation.js +44 -0
  225. package/dist/src/project-graph/error-types.d.ts +5 -0
  226. package/dist/src/project-graph/error-types.js +44 -5
  227. package/dist/src/project-graph/file-utils.d.ts +2 -0
  228. package/dist/src/project-graph/file-utils.js +12 -3
  229. package/dist/src/project-graph/nx-deps-cache.d.ts +5 -2
  230. package/dist/src/project-graph/nx-deps-cache.js +4 -4
  231. package/dist/src/project-graph/plugins/get-plugins.d.ts +3 -1
  232. package/dist/src/project-graph/plugins/get-plugins.js +60 -82
  233. package/dist/src/project-graph/plugins/in-process-loader.d.ts +1 -1
  234. package/dist/src/project-graph/plugins/in-process-loader.js +1 -4
  235. package/dist/src/project-graph/plugins/isolation/index.d.ts +1 -1
  236. package/dist/src/project-graph/plugins/isolation/index.js +3 -1
  237. package/dist/src/project-graph/plugins/isolation/isolated-plugin.d.ts +8 -0
  238. package/dist/src/project-graph/plugins/isolation/isolated-plugin.js +42 -6
  239. package/dist/src/project-graph/plugins/isolation/load-isolated-plugin.d.ts +10 -1
  240. package/dist/src/project-graph/plugins/isolation/load-isolated-plugin.js +67 -17
  241. package/dist/src/project-graph/plugins/isolation/messaging.d.ts +3 -2
  242. package/dist/src/project-graph/plugins/isolation/plugin-worker.js +5 -1
  243. package/dist/src/project-graph/plugins/loaded-nx-plugin.d.ts +5 -2
  244. package/dist/src/project-graph/plugins/loaded-nx-plugin.js +14 -1
  245. package/dist/src/project-graph/plugins/nx-plugin-capabilities.d.ts +6 -0
  246. package/dist/src/project-graph/plugins/nx-plugin-capabilities.js +28 -0
  247. package/dist/src/project-graph/plugins/task-results-stub.d.ts +50 -0
  248. package/dist/src/project-graph/plugins/task-results-stub.js +111 -0
  249. package/dist/src/project-graph/plugins/tasks-execution-hooks.d.ts +3 -2
  250. package/dist/src/project-graph/plugins/tasks-execution-hooks.js +14 -1
  251. package/dist/src/project-graph/project-graph.js +43 -42
  252. package/dist/src/project-graph/utils/project-configuration/target-merging.js +67 -0
  253. package/dist/src/project-graph/utils/project-configuration/target-normalization.js +83 -0
  254. package/dist/src/project-graph/utils/project-configuration/utils.d.ts +1 -0
  255. package/dist/src/project-graph/utils/project-configuration/utils.js +1 -0
  256. package/dist/src/tasks-runner/batch/run-batch.js +15 -0
  257. package/dist/src/tasks-runner/cache.d.ts +37 -2
  258. package/dist/src/tasks-runner/cache.js +42 -1
  259. package/dist/src/tasks-runner/create-task-graph.d.ts +24 -1
  260. package/dist/src/tasks-runner/create-task-graph.js +57 -2
  261. package/dist/src/tasks-runner/default-tasks-runner.js +1 -1
  262. package/dist/src/tasks-runner/forked-process-task-runner.d.ts +1 -1
  263. package/dist/src/tasks-runner/forked-process-task-runner.js +11 -10
  264. package/dist/src/tasks-runner/init-tasks-runner.d.ts +7 -2
  265. package/dist/src/tasks-runner/init-tasks-runner.js +15 -7
  266. package/dist/src/tasks-runner/is-tui-enabled.js +4 -3
  267. package/dist/src/tasks-runner/life-cycle.d.ts +7 -0
  268. package/dist/src/tasks-runner/life-cycle.js +7 -0
  269. package/dist/src/tasks-runner/life-cycles/summary-terminal-output-life-cycle.d.ts +35 -0
  270. package/dist/src/tasks-runner/life-cycles/summary-terminal-output-life-cycle.js +146 -0
  271. package/dist/src/tasks-runner/prune-task-graph.d.ts +17 -0
  272. package/dist/src/tasks-runner/prune-task-graph.js +45 -0
  273. package/dist/src/tasks-runner/run-command.d.ts +41 -5
  274. package/dist/src/tasks-runner/run-command.js +98 -22
  275. package/dist/src/tasks-runner/running-tasks/batch-process.d.ts +93 -19
  276. package/dist/src/tasks-runner/running-tasks/batch-process.js +184 -88
  277. package/dist/src/tasks-runner/task-io-service.d.ts +11 -3
  278. package/dist/src/tasks-runner/task-io-service.js +14 -5
  279. package/dist/src/tasks-runner/task-orchestrator.d.ts +71 -4
  280. package/dist/src/tasks-runner/task-orchestrator.js +266 -66
  281. package/dist/src/tasks-runner/tasks-runner.d.ts +2 -0
  282. package/dist/src/tasks-runner/terminal-output-path.d.ts +11 -0
  283. package/dist/src/tasks-runner/terminal-output-path.js +18 -0
  284. package/dist/src/tasks-runner/utils.d.ts +5 -0
  285. package/dist/src/tasks-runner/utils.js +14 -2
  286. package/dist/src/ultracache/config.d.ts +21 -0
  287. package/dist/src/ultracache/config.js +29 -0
  288. package/dist/src/ultracache/fetch.d.ts +21 -0
  289. package/dist/src/ultracache/fetch.js +43 -0
  290. package/dist/src/ultracache/overrides.d.ts +17 -0
  291. package/dist/src/ultracache/overrides.js +61 -0
  292. package/dist/src/ultracache/report.d.ts +13 -0
  293. package/dist/src/ultracache/report.js +62 -0
  294. package/dist/src/ultracache/store.d.ts +41 -0
  295. package/dist/src/ultracache/store.js +158 -0
  296. package/dist/src/utils/ab-testing.d.ts +4 -1
  297. package/dist/src/utils/ab-testing.js +22 -19
  298. package/dist/src/utils/chunkify.d.ts +1 -7
  299. package/dist/src/utils/chunkify.js +2 -8
  300. package/dist/src/utils/command-line-utils.d.ts +20 -0
  301. package/dist/src/utils/command-line-utils.js +10 -3
  302. package/dist/src/utils/create-cli-options.d.ts +6 -0
  303. package/dist/src/utils/create-cli-options.js +29 -0
  304. package/dist/src/utils/db-connection.d.ts +2 -0
  305. package/dist/src/utils/db-connection.js +2 -0
  306. package/dist/src/utils/formatters/index.d.ts +6 -0
  307. package/dist/src/utils/formatters/index.js +5 -0
  308. package/dist/src/utils/formatters/prettier.d.ts +0 -8
  309. package/dist/src/utils/formatters/prettier.js +4 -26
  310. package/dist/src/utils/git-utils.d.ts +19 -0
  311. package/dist/src/utils/git-utils.js +97 -39
  312. package/dist/src/utils/handle-errors.js +5 -43
  313. package/dist/src/utils/http-client.d.ts +27 -0
  314. package/dist/src/utils/http-client.js +193 -0
  315. package/dist/src/utils/installed-nx-version.js +2 -48
  316. package/dist/src/utils/json-diff.d.ts +4 -2
  317. package/dist/src/utils/json-diff.js +6 -94
  318. package/dist/src/utils/min-release-age/packument.js +1 -1
  319. package/dist/src/utils/nx-cloud-utils.d.ts +2 -0
  320. package/dist/src/utils/nx-cloud-utils.js +6 -1
  321. package/dist/src/utils/output.d.ts +22 -3
  322. package/dist/src/utils/output.js +28 -5
  323. package/dist/src/utils/package-json.d.ts +25 -0
  324. package/dist/src/utils/package-json.js +49 -6
  325. package/dist/src/utils/package-manager.d.ts +6 -0
  326. package/dist/src/utils/package-manager.js +10 -0
  327. package/dist/src/utils/plugins/plugin-capabilities.js +7 -2
  328. package/dist/src/utils/project-graph-utils.d.ts +2 -0
  329. package/dist/src/utils/project-graph-utils.js +12 -0
  330. package/dist/src/utils/provenance.js +20 -4
  331. package/dist/src/utils/serialize-overrides-into-command-line.js +2 -4
  332. package/dist/src/utils/workspace-context.d.ts +51 -3
  333. package/dist/src/utils/workspace-context.js +174 -10
  334. package/package.json +36 -98
  335. package/schemas/nx-schema.json +38 -0
  336. package/schemas/project-schema.json +35 -0
  337. package/dist/src/daemon/server/file-watching/route-workspace-changes.d.ts +0 -9
  338. package/dist/src/daemon/server/file-watching/route-workspace-changes.js +0 -76
  339. package/dist/src/daemon/server/watcher.d.ts +0 -18
  340. package/dist/src/daemon/server/watcher.js +0 -126
  341. package/dist/src/nx-cloud/utilities/axios.d.ts +0 -4
  342. package/dist/src/nx-cloud/utilities/axios.js +0 -17
  343. package/dist/src/project-graph/affected/locators/project-glob-changes.d.ts +0 -2
  344. package/dist/src/project-graph/affected/locators/project-glob-changes.js +0 -47
  345. package/dist/src/project-graph/affected/locators/workspace-json-changes.d.ts +0 -4
  346. package/dist/src/project-graph/affected/locators/workspace-json-changes.js +0 -47
  347. package/dist/src/project-graph/affected/locators/workspace-projects.d.ts +0 -3
  348. package/dist/src/project-graph/affected/locators/workspace-projects.js +0 -74
  349. package/dist/src/utils/collapse-expanded-outputs.d.ts +0 -10
  350. package/dist/src/utils/collapse-expanded-outputs.js +0 -72
@@ -0,0 +1,1121 @@
1
+ "use strict";
2
+ // The run's issue ledger. Agents supply only the handoff's `issues` /
3
+ // `issueUpdates`; ids, fingerprints, routing, claims and archiving are nx's.
4
+ Object.defineProperty(exports, "__esModule", { value: true });
5
+ exports.issueFingerprint = void 0;
6
+ exports.parseHandoffIssues = parseHandoffIssues;
7
+ exports.mintUnresolvedIssue = mintUnresolvedIssue;
8
+ exports.applyReportedIssues = applyReportedIssues;
9
+ exports.issueIdsForCommit = issueIdsForCommit;
10
+ exports.attachIssueIdsToCommitEntry = attachIssueIdsToCommitEntry;
11
+ exports.enrichCommitEntryIssueIds = enrichCommitEntryIssueIds;
12
+ exports.reopenResolutionsForStep = reopenResolutionsForStep;
13
+ exports.claimIssuesForStep = claimIssuesForStep;
14
+ exports.settleUnclaimableIssues = settleUnclaimableIssues;
15
+ exports.renderIssueDigestLines = renderIssueDigestLines;
16
+ exports.renderUnresolvedIssueLines = renderUnresolvedIssueLines;
17
+ exports.unresolvedIssues = unresolvedIssues;
18
+ exports.hasUnresolvedIssues = hasUnresolvedIssues;
19
+ exports.issueArchivePath = issueArchivePath;
20
+ exports.warnReconstructedArchives = warnReconstructedArchives;
21
+ exports.archiveIssues = archiveIssues;
22
+ exports.applicationArchivesIntact = applicationArchivesIntact;
23
+ const crypto_1 = require("crypto");
24
+ const fs_1 = require("fs");
25
+ const path_1 = require("path");
26
+ const fileutils_1 = require("../../../utils/fileutils");
27
+ const atomic_write_1 = require("./atomic-write");
28
+ const types_1 = require("../agentic/types");
29
+ const text_1 = require("../text");
30
+ const agent_output_1 = require("./agent-output");
31
+ const run_state_1 = require("./run-state");
32
+ Object.defineProperty(exports, "issueFingerprint", { enumerable: true, get: function () { return run_state_1.issueFingerprint; } });
33
+ const state_machine_1 = require("./state-machine");
34
+ // Reserves summary space for the attempt count and the failure.
35
+ const MAX_UNRESOLVED_ID_CHARS = 200;
36
+ const ISSUES_DIR_NAME = 'issues';
37
+ // Bounds on one agent-written handoff's issue report. A report over a bound is
38
+ // rejected whole for the agent to fix, never trimmed.
39
+ const MAX_ISSUES_PER_HANDOFF = 20;
40
+ const MAX_ISSUE_UPDATES_PER_HANDOFF = 20;
41
+ const MAX_SUMMARY_CHARS = 500;
42
+ const MAX_DETAIL_CHARS = 8192;
43
+ const MAX_NOTE_CHARS = 1000;
44
+ const MAX_APPLICABLE_MIGRATIONS = 50;
45
+ // Bounds on the digest rendered into dispensed steps. Overflow here is
46
+ // truncated or counted, never rejected the way an over-bound report is. The
47
+ // byte bound covers the whole serialized digest, fixed lines included.
48
+ const MAX_DIGEST_ENTRIES = 20;
49
+ const MAX_DIGEST_BYTES = 8192;
50
+ const DIGEST_SUMMARY_CHARS = 200;
51
+ function invalid(reason) {
52
+ return { ok: false, reason };
53
+ }
54
+ function isPlainObject(value) {
55
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
56
+ }
57
+ // Unknown keys reject rather than strip: a typoed optional ("dispositon")
58
+ // would otherwise fall back to its default and mis-route the issue silently.
59
+ function unknownKey(value, allowed) {
60
+ return Object.keys(value).find((key) => !allowed.includes(key));
61
+ }
62
+ // Collapsed and truncated because the value is agent-written and the rejection
63
+ // reason is rendered into dispensed step content.
64
+ function quoted(value) {
65
+ const text = typeof value === 'string' ? value : JSON.stringify(value);
66
+ const collapsed = (0, text_1.singleLine)(String(text));
67
+ return JSON.stringify(collapsed.length > 100 ? `${collapsed.slice(0, 100)}...` : collapsed);
68
+ }
69
+ /**
70
+ * Validates a handoff's `issues` / `issueUpdates` against the run's current
71
+ * state. Absent fields parse as empty; any invalid entry rejects the whole
72
+ * report with a reason the awaiting dispense hands back to the agent. Also
73
+ * rejects unrecognized top-level handoff fields: a misspelled "issueUpates"
74
+ * would otherwise read as an absent report and be discarded with the handoff.
75
+ */
76
+ function parseHandoffIssues(extras, state, reportingStep) {
77
+ if (extras !== undefined) {
78
+ const unknown = unknownKey(extras, ['outcome', 'issues', 'issueUpdates']);
79
+ if (unknown !== undefined) {
80
+ return invalid(`the handoff has an unrecognized field ${quoted(unknown)}; the fields accepted next to "status" and "summary" are "outcome", "issues" and "issueUpdates"`);
81
+ }
82
+ if (extras['outcome'] !== undefined && extras['outcome'] !== 'skipped') {
83
+ return invalid(`the handoff has an unrecognized "outcome" value ${quoted(extras['outcome'])}; the only accepted value is "skipped"`);
84
+ }
85
+ }
86
+ const rawIssues = extras?.['issues'];
87
+ const rawUpdates = extras?.['issueUpdates'];
88
+ const issues = [];
89
+ const updates = [];
90
+ if (rawIssues !== undefined) {
91
+ if (!Array.isArray(rawIssues)) {
92
+ return invalid(`the "issues" field must be an array`);
93
+ }
94
+ if (rawIssues.length > MAX_ISSUES_PER_HANDOFF) {
95
+ return invalid(`the "issues" field lists ${rawIssues.length} entries; at most ${MAX_ISSUES_PER_HANDOFF} are accepted per handoff`);
96
+ }
97
+ const seenFingerprints = new Set();
98
+ for (let i = 0; i < rawIssues.length; i++) {
99
+ const parsed = parseReportedIssue(rawIssues[i], i, state);
100
+ if (typeof parsed === 'string')
101
+ return invalid(parsed);
102
+ // Rejected rather than folded: array order would decide which disposition
103
+ // lands, since the second entry folds into the first's just-minted one.
104
+ const fingerprint = (0, run_state_1.issueFingerprint)(parsed.summary);
105
+ if (seenFingerprints.has(fingerprint)) {
106
+ return invalid(`"issues"[${i}] repeats an earlier entry's summary; report each problem once, with its final disposition`);
107
+ }
108
+ seenFingerprints.add(fingerprint);
109
+ issues.push(parsed);
110
+ }
111
+ }
112
+ if (rawUpdates !== undefined) {
113
+ if (!Array.isArray(rawUpdates)) {
114
+ return invalid(`the "issueUpdates" field must be an array`);
115
+ }
116
+ if (rawUpdates.length > MAX_ISSUE_UPDATES_PER_HANDOFF) {
117
+ return invalid(`the "issueUpdates" field lists ${rawUpdates.length} entries; at most ${MAX_ISSUE_UPDATES_PER_HANDOFF} are accepted per handoff`);
118
+ }
119
+ const seenIds = new Set();
120
+ for (let i = 0; i < rawUpdates.length; i++) {
121
+ const parsed = parseIssueUpdate(rawUpdates[i], i, state, reportingStep);
122
+ if (typeof parsed === 'string')
123
+ return invalid(parsed);
124
+ // Rejected rather than applied in order: the ledger records only the final
125
+ // disposition, so sequence would carry meaning nothing persists.
126
+ if (seenIds.has(parsed.id)) {
127
+ return invalid(`"issueUpdates" lists ${parsed.id} more than once; report one final disposition per issue`);
128
+ }
129
+ seenIds.add(parsed.id);
130
+ updates.push(parsed);
131
+ }
132
+ }
133
+ // Rejected rather than ordered: applyReportedIssues applies reports before
134
+ // updates, so the fixed order would silently decide the final disposition.
135
+ if (issues.length > 0 && updates.length > 0) {
136
+ const updatedIds = new Set(updates.map((u) => u.id));
137
+ for (let i = 0; i < issues.length; i++) {
138
+ const existing = (state.issues ?? []).find((s) => s.fingerprint === (0, run_state_1.issueFingerprint)(issues[i].summary));
139
+ if (existing !== undefined && updatedIds.has(existing.id)) {
140
+ return invalid(`"issues"[${i}] describes ${existing.id}, which "issueUpdates" also references; report the problem or update the assignment, not both`);
141
+ }
142
+ }
143
+ }
144
+ return { ok: true, issues, updates };
145
+ }
146
+ function parseReportedIssue(value, index, state) {
147
+ const label = `"issues"[${index}]`;
148
+ if (!isPlainObject(value)) {
149
+ return `${label} must be an object`;
150
+ }
151
+ const unknown = unknownKey(value, [
152
+ 'summary',
153
+ 'detail',
154
+ 'applicableMigrations',
155
+ 'disposition',
156
+ ]);
157
+ if (unknown !== undefined) {
158
+ return `${label} has an unrecognized field ${quoted(unknown)}; allowed fields are "summary", "detail", "applicableMigrations" and "disposition"`;
159
+ }
160
+ // Validated after singleLine: U+0085 is a terminator to singleLine but not
161
+ // to trim, so a raw check would accept a summary that normalizes empty.
162
+ const summary = typeof value.summary === 'string'
163
+ ? (0, text_1.singleLine)(value.summary).trim()
164
+ : undefined;
165
+ if (summary === undefined ||
166
+ summary.length === 0 ||
167
+ summary.length > MAX_SUMMARY_CHARS) {
168
+ return `${label} needs a non-empty "summary" string of at most ${MAX_SUMMARY_CHARS} characters`;
169
+ }
170
+ const detail = value.detail;
171
+ if (detail !== undefined &&
172
+ (typeof detail !== 'string' || detail.length > MAX_DETAIL_CHARS)) {
173
+ return `${label} has a "detail" that is not a string of at most ${MAX_DETAIL_CHARS} characters`;
174
+ }
175
+ const applicable = value.applicableMigrations;
176
+ if (applicable !== 'unknown') {
177
+ if (!Array.isArray(applicable) ||
178
+ applicable.length === 0 ||
179
+ applicable.length > MAX_APPLICABLE_MIGRATIONS) {
180
+ return `${label} needs an "applicableMigrations" that is either the string "unknown" or an array of 1 to ${MAX_APPLICABLE_MIGRATIONS} migration identifiers`;
181
+ }
182
+ for (const identifier of applicable) {
183
+ if (typeof identifier !== 'string') {
184
+ return `${label}.applicableMigrations entries must be strings`;
185
+ }
186
+ if (mappedStepIds(identifier, state).length === 0) {
187
+ return `${label}.applicableMigrations names ${quoted(identifier)}, which is not in this run's plan; use an exact "<package>:<name>" id or a bare package name from the plan, or "unknown"`;
188
+ }
189
+ }
190
+ }
191
+ const disposition = value.disposition;
192
+ if (disposition !== undefined &&
193
+ !run_state_1.MIGRATE_ISSUE_DISPOSITIONS.includes(disposition)) {
194
+ return `${label} has a "disposition" that is not one of ${run_state_1.MIGRATE_ISSUE_DISPOSITIONS.map((d) => `"${d}"`).join(', ')}`;
195
+ }
196
+ return {
197
+ summary,
198
+ ...(detail !== undefined ? { detail: detail } : {}),
199
+ applicableMigrations: applicable === 'unknown' ? 'unknown' : applicable,
200
+ ...(disposition !== undefined
201
+ ? { disposition: disposition }
202
+ : {}),
203
+ };
204
+ }
205
+ function parseIssueUpdate(value, index, state, reportingStep) {
206
+ const label = `"issueUpdates"[${index}]`;
207
+ if (!isPlainObject(value)) {
208
+ return `${label} must be an object`;
209
+ }
210
+ const unknown = unknownKey(value, ['id', 'disposition', 'note']);
211
+ if (unknown !== undefined) {
212
+ return `${label} has an unrecognized field ${quoted(unknown)}; allowed fields are "id", "disposition" and "note"`;
213
+ }
214
+ const issue = typeof value.id === 'string'
215
+ ? (state.issues ?? []).find((i) => i.id === value.id)
216
+ : undefined;
217
+ if (!issue) {
218
+ return `${label} references ${quoted(value.id)}, which is not an issue nx has recorded for this run`;
219
+ }
220
+ const refusal = issueUpdateRefusal(issue, reportingStep);
221
+ if (refusal !== null) {
222
+ return `${label} references ${issue.id}, which ${refusal}`;
223
+ }
224
+ const disposition = value.disposition;
225
+ if (disposition !== 'resolved' && disposition !== 'deferred-final') {
226
+ return `${label} needs a "disposition" of "resolved" or "deferred-final"`;
227
+ }
228
+ const note = value.note;
229
+ if (note !== undefined &&
230
+ (typeof note !== 'string' || note.length > MAX_NOTE_CHARS)) {
231
+ return `${label} has a "note" that is not a string of at most ${MAX_NOTE_CHARS} characters`;
232
+ }
233
+ return {
234
+ id: issue.id,
235
+ disposition,
236
+ ...(note !== undefined ? { note: note } : {}),
237
+ };
238
+ }
239
+ // Claims serialize the migration steps that could fix an issue. The final
240
+ // validation pass runs alone after all of them, so it takes every issue still
241
+ // open instead: by then any it could claim has been deferred to it.
242
+ function issueUpdateRefusal(issue, reportingStep) {
243
+ switch (reportingStep.kind) {
244
+ case 'migration':
245
+ return issue.claimedByStepId === reportingStep.id
246
+ ? null
247
+ : 'is not assigned to this step; only issues the dispensed digest marks assigned to the current step can be updated';
248
+ case 'final-validation':
249
+ return issue.disposition !== 'resolved'
250
+ ? null
251
+ : 'is already resolved; the pass may update any issue still open';
252
+ default: {
253
+ const exhaustive = reportingStep;
254
+ throw new Error(`Unrecognized step: ${JSON.stringify(exhaustive)}`);
255
+ }
256
+ }
257
+ }
258
+ // A bare identifier matches the whole package name, scoped names included:
259
+ // nothing splits on '/'. Step ids are unique, so the result needs no dedup.
260
+ function mappedStepIds(identifier, state) {
261
+ if (identifier.includes(':')) {
262
+ return state.steps
263
+ .filter((s) => s.kind === 'migration' && s.migrationId === identifier)
264
+ .map((s) => s.id);
265
+ }
266
+ return state.steps
267
+ .filter((s) => s.kind === 'migration' &&
268
+ (0, state_machine_1.splitMigrationId)(s.migrationId).package === identifier)
269
+ .map((s) => s.id);
270
+ }
271
+ /**
272
+ * Mints the run issue carrying a given-up step's last failure to the completion
273
+ * report: unscoped and deferred, so no later step claims it, with the migration
274
+ * id in the summary so identical failures stay apart. A report with the same
275
+ * text is taken over, not merged: the fingerprint is derived from the summary,
276
+ * so the ledger cannot hold both, and a merge would keep the report's scope.
277
+ * Same locking contract as {@link applyReportedIssues}.
278
+ */
279
+ function mintUnresolvedIssue(state, step) {
280
+ const attempts = `${step.attempt} attempt${step.attempt === 1 ? '' : 's'}`;
281
+ let subject;
282
+ switch (step.kind) {
283
+ case 'migration':
284
+ subject = `Migration ${abbreviatedMigrationId(step.migrationId)}`;
285
+ break;
286
+ case 'final-validation':
287
+ subject = 'The final validation pass';
288
+ break;
289
+ default: {
290
+ const exhaustive = step;
291
+ throw new Error(`Unrecognized step: ${JSON.stringify(exhaustive)}`);
292
+ }
293
+ }
294
+ const prefix = `${subject} was left unresolved after ${attempts}: `;
295
+ const room = MAX_SUMMARY_CHARS - prefix.length;
296
+ const detail = (0, state_machine_1.unresolvedFailureDetail)(step);
297
+ const summary = prefix +
298
+ (detail.length > room ? `${detail.slice(0, room - 3)}...` : detail);
299
+ const ledger = state.issues ?? [];
300
+ const index = ledger.findIndex((i) => i.fingerprint === (0, run_state_1.issueFingerprint)(summary));
301
+ if (index === -1) {
302
+ const application = applyReportedIssues(state, step, [{ summary, applicableMigrations: 'unknown' }], []);
303
+ return { application, issueId: application.newIssues[0].entry.id };
304
+ }
305
+ const { claimedByStepId: _claim, resolvedByStepId: _credit, resolvedAtCommitCount: _fence, ...rest } = ledger[index];
306
+ const entry = {
307
+ ...rest,
308
+ applicableStepIds: 'unknown',
309
+ disposition: 'deferred-final',
310
+ };
311
+ const issues = [...ledger];
312
+ issues[index] = entry;
313
+ return {
314
+ application: {
315
+ state: { ...state, issues },
316
+ newIssues: [],
317
+ updates: [
318
+ { issueId: entry.id, stepId: step.id, disposition: 'deferred-final' },
319
+ ],
320
+ },
321
+ issueId: entry.id,
322
+ };
323
+ }
324
+ /**
325
+ * Pure. Must run on state read fresh inside the fold's locked write, or ids
326
+ * collide and the claim checks test state that is no longer on disk.
327
+ */
328
+ function applyReportedIssues(state, reportingStep, issues, updates,
329
+ // The index of the entry that will carry this application's resolutions:
330
+ // the next append, unless a parent session already recorded it.
331
+ resolvedAtCommitCount = state.commits.length) {
332
+ const ledger = [...(state.issues ?? [])];
333
+ const newIssues = [];
334
+ const archiveUpdates = [];
335
+ // Allocate past the highest existing suffix, not from the ledger length: the
336
+ // reader accepts sparse ids, and a re-minted id would overwrite another
337
+ // issue's archive file. BigInt because a number suffix stalls at 2^53.
338
+ let nextIdNumber = ledger.reduce((max, i) => {
339
+ const suffix = BigInt(i.id.slice('issue-'.length));
340
+ return suffix > max ? suffix : max;
341
+ }, 0n) + 1n;
342
+ for (const report of issues) {
343
+ const fingerprint = (0, run_state_1.issueFingerprint)(report.summary);
344
+ const existingIndex = ledger.findIndex((i) => i.fingerprint === fingerprint);
345
+ if (existingIndex !== -1) {
346
+ const existing = ledger[existingIndex];
347
+ const outcome = applyDuplicateReport(existing, report, reportingStep, state, resolvedAtCommitCount);
348
+ if (outcome.entry !== existing) {
349
+ ledger[existingIndex] = outcome.entry;
350
+ }
351
+ if (outcome.archive) {
352
+ archiveUpdates.push({
353
+ issueId: existing.id,
354
+ stepId: reportingStep.id,
355
+ disposition: outcome.entry.disposition,
356
+ ...(report.applicableMigrations !== 'unknown'
357
+ ? { applicableMigrations: report.applicableMigrations }
358
+ : {}),
359
+ ...(report.detail !== undefined ? { detail: report.detail } : {}),
360
+ });
361
+ }
362
+ continue;
363
+ }
364
+ const applicableStepIds = report.applicableMigrations === 'unknown'
365
+ ? 'unknown'
366
+ : mapApplicableSteps(report.applicableMigrations, state);
367
+ const disposition = resolveDisposition(report, applicableStepIds, state);
368
+ // Reachable only at the reader's 18-digit bound. Throwing hands the caller's
369
+ // archive error handling the failure instead of writing a state it rejects.
370
+ const id = `issue-${nextIdNumber++}`;
371
+ if (!run_state_1.ISSUE_ID.test(id)) {
372
+ throw new Error(`the issue ledger cannot allocate "${id}": the id suffix is out of range`);
373
+ }
374
+ const entry = {
375
+ id,
376
+ fingerprint,
377
+ summary: report.summary,
378
+ reportedByStepId: reportingStep.id,
379
+ applicableStepIds,
380
+ disposition,
381
+ ...(disposition === 'resolved'
382
+ ? {
383
+ resolvedByStepId: reportingStep.id,
384
+ resolvedAtCommitCount,
385
+ }
386
+ : {}),
387
+ };
388
+ ledger.push(entry);
389
+ newIssues.push({ entry, report });
390
+ }
391
+ for (const update of updates) {
392
+ const index = ledger.findIndex((i) => i.id === update.id);
393
+ // Ids were validated against this same state by the caller.
394
+ if (index === -1)
395
+ continue;
396
+ const existing = ledger[index];
397
+ if (existing.disposition !== update.disposition) {
398
+ // The claim and the resolution credit both die with the move: only 'recorded'
399
+ // issues carry an assignment, and the stamp belongs to this resolution alone.
400
+ const { resolvedByStepId: _dropped, resolvedAtCommitCount: _fence, claimedByStepId: _released, ...rest } = existing;
401
+ ledger[index] =
402
+ update.disposition === 'resolved'
403
+ ? {
404
+ ...rest,
405
+ disposition: 'resolved',
406
+ resolvedByStepId: reportingStep.id,
407
+ resolvedAtCommitCount,
408
+ }
409
+ : { ...rest, disposition: update.disposition };
410
+ }
411
+ // Archived even when the disposition did not move: the note may carry
412
+ // context the ledger has no field for.
413
+ archiveUpdates.push({
414
+ issueId: update.id,
415
+ stepId: reportingStep.id,
416
+ disposition: update.disposition,
417
+ ...(update.note !== undefined ? { note: update.note } : {}),
418
+ });
419
+ }
420
+ return {
421
+ state: { ...state, issues: ledger },
422
+ newIssues,
423
+ updates: archiveUpdates,
424
+ };
425
+ }
426
+ function reportIntent(report) {
427
+ switch (report.disposition) {
428
+ case 'resolved':
429
+ return 'resolved';
430
+ case 'deferred-final':
431
+ return 'deferred-final';
432
+ case 'recorded':
433
+ case undefined:
434
+ return 'unresolved';
435
+ default:
436
+ return unreachable(report.disposition);
437
+ }
438
+ }
439
+ /**
440
+ * Folds a duplicate report into its existing entry, one cell per (disposition x
441
+ * intent) pair. A recorded entry another live step holds blocks a resolve or
442
+ * defer down to a refinement: an assignment made in that step's digest cannot
443
+ * be revoked from outside it. Every cell that changed the entry, and every
444
+ * report carrying detail, must archive: the fold removes the handoff, so
445
+ * unarchived detail is lost.
446
+ */
447
+ function applyDuplicateReport(existing, report, reportingStep, state, resolvedAtCommitCount) {
448
+ const intent = reportIntent(report);
449
+ const merged = mergedApplicableStepIds(existing, report, state);
450
+ // Set inclusion, not a length compare: a corrupt entry with duplicate scope
451
+ // ids would otherwise read the normalizing merge as new routing.
452
+ const addedStepIds = !Array.isArray(merged)
453
+ ? []
454
+ : !Array.isArray(existing.applicableStepIds)
455
+ ? merged
456
+ : merged.filter((id) => !existing.applicableStepIds.includes(id));
457
+ const widened = addedStepIds.length > 0;
458
+ // Only a live assignment blocks: a terminal claimant can never hand back
459
+ // another handoff, so its claim is stale bookkeeping, not ownership.
460
+ const claimant = existing.disposition === 'recorded' &&
461
+ existing.claimedByStepId !== undefined
462
+ ? state.steps.find((s) => s.id === existing.claimedByStepId)
463
+ : undefined;
464
+ const blocked = claimant !== undefined &&
465
+ claimant.id !== reportingStep.id &&
466
+ !run_state_1.TERMINAL_STEP_STATUSES.has(claimant.status);
467
+ const refine = () => ({
468
+ entry: widened ? { ...existing, applicableStepIds: merged } : existing,
469
+ archive: widened || report.detail !== undefined,
470
+ });
471
+ const transition = (disposition) => {
472
+ const { resolvedByStepId: _credit, resolvedAtCommitCount: _fence, claimedByStepId: _claim, ...rest } = existing;
473
+ return {
474
+ entry: {
475
+ ...rest,
476
+ applicableStepIds: merged,
477
+ disposition,
478
+ ...(disposition === 'resolved'
479
+ ? {
480
+ resolvedByStepId: reportingStep.id,
481
+ resolvedAtCommitCount,
482
+ }
483
+ : {}),
484
+ },
485
+ archive: true,
486
+ };
487
+ };
488
+ switch (existing.disposition) {
489
+ case 'recorded':
490
+ switch (intent) {
491
+ case 'resolved':
492
+ return blocked ? refine() : transition('resolved');
493
+ case 'deferred-final':
494
+ return blocked ? refine() : transition('deferred-final');
495
+ case 'unresolved':
496
+ return refine();
497
+ default:
498
+ return unreachable(intent);
499
+ }
500
+ case 'resolved':
501
+ // A duplicate report over a resolved entry is a recurrence: resolved issues
502
+ // leave the digest, so the reporter hit the problem again, not echoed it.
503
+ switch (intent) {
504
+ case 'resolved':
505
+ // Not a no-op: the recurrence re-credits the new resolver and re-stamps.
506
+ return transition('resolved');
507
+ case 'deferred-final':
508
+ case 'unresolved':
509
+ return transition(resolveDisposition(report, merged, state));
510
+ default:
511
+ return unreachable(intent);
512
+ }
513
+ case 'deferred-final':
514
+ switch (intent) {
515
+ case 'resolved':
516
+ // Deferred entries have no assignment, so any step that actually
517
+ // fixed the problem may resolve them.
518
+ return transition('resolved');
519
+ case 'deferred-final':
520
+ return refine();
521
+ case 'unresolved':
522
+ // Revive only on newly ADDED routing that can still claim it: deferred entries
523
+ // stay digest-visible, so a repeat with no new scope may just be echoing one,
524
+ // and testing `merged` instead would let a route to finished work reopen it.
525
+ return hasClaimableStep(addedStepIds, stepStatusIndex(state))
526
+ ? transition('recorded')
527
+ : refine();
528
+ default:
529
+ return unreachable(intent);
530
+ }
531
+ default:
532
+ return unreachable(existing.disposition);
533
+ }
534
+ }
535
+ function unreachable(value) {
536
+ throw new Error(`unexpected value: ${JSON.stringify(value)}`);
537
+ }
538
+ // A commit carries the resolution only if it names the resolving step AND
539
+ // postdates the stamp: the same step's earlier commits must not vouch for it.
540
+ function resolutionCarried(state, issue) {
541
+ return state.commits
542
+ .slice(issue.resolvedAtCommitCount ?? 0)
543
+ .some((c) => c.kind === 'landed' &&
544
+ (c.issueIds ?? []).includes(issue.id) &&
545
+ c.stepIds.includes(issue.resolvedByStepId));
546
+ }
547
+ /**
548
+ * The issue ids a landed commit naming `stepIds` carries. Attribution rides on
549
+ * the persisted `resolvedByStepId`, so a resolution whose own commit attempt
550
+ * failed still reaches the later commit that absorbs that step's tree.
551
+ */
552
+ function issueIdsForCommit(state, stepIds) {
553
+ const issues = state.issues;
554
+ if (!issues)
555
+ return [];
556
+ return issues
557
+ .filter((i) => i.disposition === 'resolved' &&
558
+ i.resolvedByStepId !== undefined &&
559
+ stepIds.includes(i.resolvedByStepId) &&
560
+ !resolutionCarried(state, i))
561
+ .map((i) => i.id);
562
+ }
563
+ /**
564
+ * Attaches the issue ids a landed entry carries; non-landed entries pass
565
+ * through. Every commit append (worker, fold, adopt) routes through this so
566
+ * attribution cannot diverge between terminal paths.
567
+ */
568
+ function attachIssueIdsToCommitEntry(state, entry) {
569
+ if (entry.kind !== 'landed')
570
+ return entry;
571
+ const issueIds = issueIdsForCommit(state, entry.stepIds);
572
+ return issueIds.length > 0 ? { ...entry, issueIds } : entry;
573
+ }
574
+ /**
575
+ * Adds to a landed entry the resolutions it can now carry, keeping the ids it
576
+ * has; other entries pass through. Repeating it is a no-op, since
577
+ * `issueIdsForCommit` skips resolutions an entry already carries.
578
+ */
579
+ function enrichCommitEntryIssueIds(state, index) {
580
+ const entry = state.commits[index];
581
+ if (entry.kind !== 'landed')
582
+ return state;
583
+ const existing = entry.issueIds ?? [];
584
+ const added = issueIdsForCommit(state, entry.stepIds).filter((id) => !existing.includes(id));
585
+ if (added.length === 0)
586
+ return state;
587
+ return {
588
+ ...state,
589
+ commits: state.commits.map((c, i) => i === index ? { ...c, issueIds: [...existing, ...added] } : c),
590
+ };
591
+ }
592
+ /**
593
+ * Reverts resolutions not carried by a landed commit and releases claims
594
+ * during a reset-backed retry. Returned disposition changes form the archive
595
+ * trail; claim releases do not.
596
+ */
597
+ function reopenResolutionsForStep(state, stepId) {
598
+ const issues = state.issues;
599
+ if (!issues)
600
+ return { state, updates: [] };
601
+ const updates = [];
602
+ let changed = false;
603
+ const next = issues.map((issue) => {
604
+ if (issue.disposition === 'resolved' &&
605
+ issue.resolvedByStepId === stepId &&
606
+ !resolutionCarried(state, issue)) {
607
+ const { resolvedByStepId: _dropped, resolvedAtCommitCount: _fence, claimedByStepId: _stale, ...rest } = issue;
608
+ const disposition = hasClaimableStep(issue.applicableStepIds, stepStatusIndex(state))
609
+ ? 'recorded'
610
+ : 'deferred-final';
611
+ updates.push({ issueId: issue.id, stepId, disposition });
612
+ changed = true;
613
+ return { ...rest, disposition };
614
+ }
615
+ if (issue.claimedByStepId === stepId) {
616
+ const { claimedByStepId: _released, ...rest } = issue;
617
+ changed = true;
618
+ return rest;
619
+ }
620
+ return issue;
621
+ });
622
+ return changed
623
+ ? { state: { ...state, issues: next }, updates }
624
+ : { state, updates };
625
+ }
626
+ // Unknown applicability is deferred, not recorded: nothing can claim it, so a
627
+ // 'recorded' entry would sit waiting for an assignment that never comes.
628
+ function resolveDisposition(report, applicableStepIds, state) {
629
+ const disposition = report.disposition ??
630
+ (applicableStepIds === 'unknown' ? 'deferred-final' : 'recorded');
631
+ if (disposition !== 'recorded')
632
+ return disposition;
633
+ return hasClaimableStep(applicableStepIds, stepStatusIndex(state))
634
+ ? 'recorded'
635
+ : 'deferred-final';
636
+ }
637
+ function mergedApplicableStepIds(existing, report, state) {
638
+ if (report.applicableMigrations === 'unknown') {
639
+ return existing.applicableStepIds;
640
+ }
641
+ const combined = new Set([
642
+ ...(Array.isArray(existing.applicableStepIds)
643
+ ? existing.applicableStepIds
644
+ : []),
645
+ ...mapApplicableSteps(report.applicableMigrations, state),
646
+ ]);
647
+ return state.steps.map((s) => s.id).filter((id) => combined.has(id));
648
+ }
649
+ // Non-terminal is the boundary, not 'pending': a just-failed step can be
650
+ // re-armed, and its retry's dispense claims recorded issues.
651
+ function hasClaimableStep(applicableStepIds, stepStatus) {
652
+ if (applicableStepIds === 'unknown')
653
+ return false;
654
+ return applicableStepIds.some((id) => {
655
+ const status = stepStatus.get(id);
656
+ return status !== undefined && !run_state_1.TERMINAL_STEP_STATUSES.has(status);
657
+ });
658
+ }
659
+ // Built once per pass: a bare package can route every issue to every
660
+ // same-package step, so a per-lookup scan of `state.steps` goes cubic.
661
+ function stepStatusIndex(state) {
662
+ return new Map(state.steps.map((s) => [s.id, s.status]));
663
+ }
664
+ function mapApplicableSteps(identifiers, state) {
665
+ const mapped = new Set();
666
+ for (const identifier of identifiers) {
667
+ for (const stepId of mappedStepIds(identifier, state)) {
668
+ mapped.add(stepId);
669
+ }
670
+ }
671
+ return state.steps.map((s) => s.id).filter((id) => mapped.has(id));
672
+ }
673
+ /**
674
+ * Claims the still-recorded issues applicable to the step being dispensed, up
675
+ * to what the digest can publish. Must run inside the dispense's locked write:
676
+ * serial dispensing is what makes reassignment to the next applicable step
677
+ * deterministic when an earlier one left the issue unresolved.
678
+ */
679
+ function claimIssuesForStep(state, stepId, runId) {
680
+ const issues = state.issues;
681
+ if (!issues)
682
+ return state;
683
+ // Capped to what the digest publishes: an unlisted assignment is invisible,
684
+ // since issueUpdates may only name digest-marked issues. The instruction line
685
+ // is reserved unconditionally; it renders with any surviving claim, and
686
+ // over-reserving only shortens the claimed prefix.
687
+ const budget = digestEntryByteBudget([``, stepDigestHeading(runId), ASSIGNMENT_INSTRUCTION], unresolvedIssues(state).length, runId);
688
+ const applicableTo = (issue) => issue.disposition === 'recorded' &&
689
+ Array.isArray(issue.applicableStepIds) &&
690
+ issue.applicableStepIds.includes(stepId);
691
+ const applicable = issues.filter(applicableTo);
692
+ const kept = new Set(applicable.slice(0, boundedPrefixCount(applicable.map((issue) => digestEntry(issue, 'assigned to this step')), budget)));
693
+ let changed = false;
694
+ const next = issues.map((issue) => {
695
+ if (!applicableTo(issue))
696
+ return issue;
697
+ if (kept.has(issue)) {
698
+ if (issue.claimedByStepId === stepId)
699
+ return issue;
700
+ changed = true;
701
+ return { ...issue, claimedByStepId: stepId };
702
+ }
703
+ if (issue.claimedByStepId === stepId) {
704
+ changed = true;
705
+ const { claimedByStepId: _dropped, ...rest } = issue;
706
+ return rest;
707
+ }
708
+ return issue;
709
+ });
710
+ return changed ? { ...state, issues: next } : state;
711
+ }
712
+ /**
713
+ * Before rendering, release terminal claims and defer issues no remaining
714
+ * step can claim; report-time routing can become stale as steps finish. Do
715
+ * not archive Nx-driven progression.
716
+ */
717
+ function settleUnclaimableIssues(state) {
718
+ const issues = state.issues;
719
+ if (!issues)
720
+ return state;
721
+ const stepStatus = stepStatusIndex(state);
722
+ let changed = false;
723
+ const next = issues.map((issue) => {
724
+ if (issue.disposition !== 'recorded')
725
+ return issue;
726
+ if (!hasClaimableStep(issue.applicableStepIds, stepStatus)) {
727
+ changed = true;
728
+ // The claim goes with the demotion: a deferred issue is assigned
729
+ // nowhere, and the state reader rejects a claim on a non-recorded issue.
730
+ const { claimedByStepId: _dropped, ...rest } = issue;
731
+ return { ...rest, disposition: 'deferred-final' };
732
+ }
733
+ // A terminal assignee can never hand back another handoff, so its claim
734
+ // blocks nothing and would read as active ownership.
735
+ const claimantStatus = issue.claimedByStepId !== undefined
736
+ ? stepStatus.get(issue.claimedByStepId)
737
+ : undefined;
738
+ if (claimantStatus !== undefined &&
739
+ run_state_1.TERMINAL_STEP_STATUSES.has(claimantStatus)) {
740
+ changed = true;
741
+ const { claimedByStepId: _released, ...rest } = issue;
742
+ return rest;
743
+ }
744
+ return issue;
745
+ });
746
+ return changed ? { ...state, issues: next } : state;
747
+ }
748
+ function issuesDirRef(runId) {
749
+ return `${types_1.MIGRATE_RUNS_RELATIVE_DIR}/${runId}/${ISSUES_DIR_NAME}/`;
750
+ }
751
+ /**
752
+ * The bounded "Known issues" digest a work dispense carries. Assigned entries
753
+ * lead and share claimIssuesForStep's prefix bound, so every kept claim appears.
754
+ */
755
+ function renderIssueDigestLines(state, currentStepId, runId) {
756
+ const unresolved = unresolvedIssues(state);
757
+ if (unresolved.length === 0)
758
+ return [];
759
+ const currentStep = state.steps.find((s) => s.id === currentStepId);
760
+ // Deferred issues are the pass's own work; every other step reads them as
761
+ // out of reach.
762
+ const passDispense = currentStep?.kind === 'final-validation';
763
+ const assigned = passDispense
764
+ ? unresolved
765
+ : unresolved.filter((i) => i.disposition === 'recorded' && i.claimedByStepId === currentStepId);
766
+ const entries = passDispense
767
+ ? assigned.map((i) => digestEntry(i, i.disposition === 'deferred-final'
768
+ ? 'deferred to this step'
769
+ : 'assigned to this step'))
770
+ : [
771
+ ...assigned.map((i) => digestEntry(i, 'assigned to this step')),
772
+ ...unresolved
773
+ .filter((i) => i.disposition === 'recorded' &&
774
+ i.claimedByStepId !== currentStepId)
775
+ .map((i) => digestEntry(i, 'recorded')),
776
+ ...unresolved
777
+ .filter((i) => i.disposition === 'deferred-final')
778
+ .map((i) => digestEntry(i, 'deferred past the migration steps')),
779
+ ];
780
+ const fixedLines = [
781
+ ``,
782
+ stepDigestHeading(runId),
783
+ ...(assigned.length > 0 ? [ASSIGNMENT_INSTRUCTION] : []),
784
+ ];
785
+ const budget = digestEntryByteBudget(fixedLines, entries.length, runId);
786
+ if (budget < 0) {
787
+ return [``, DIGEST_FALLBACK_LINE];
788
+ }
789
+ const lines = [
790
+ ``,
791
+ stepDigestHeading(runId),
792
+ ...boundedEntryLines(entries, runId, budget),
793
+ ];
794
+ if (assigned.length > 0) {
795
+ lines.push(ASSIGNMENT_INSTRUCTION);
796
+ }
797
+ return lines;
798
+ }
799
+ function renderUnresolvedIssueLines(state, runId) {
800
+ const unresolved = unresolvedIssues(state);
801
+ if (unresolved.length === 0)
802
+ return [];
803
+ const entries = [
804
+ ...unresolved
805
+ .filter((i) => i.disposition === 'recorded')
806
+ .map((i) => digestEntry(i, 'recorded')),
807
+ ...unresolved
808
+ .filter((i) => i.disposition === 'deferred-final')
809
+ .map((i) => digestEntry(i, 'deferred past the migration steps')),
810
+ ];
811
+ const noun = unresolved.length === 1 ? 'issue' : 'issues';
812
+ const verb = unresolved.length === 1 ? 'remains' : 'remain';
813
+ const heading = `${unresolved.length} reported ${noun} ${verb} unresolved (details under ${issuesDirRef(runId)}):`;
814
+ const budget = digestEntryByteBudget([heading], entries.length, runId);
815
+ if (budget < 0) {
816
+ return [
817
+ `${unresolved.length} reported ${noun} ${verb} unresolved; see ${ISSUES_DIR_FALLBACK_REF}.`,
818
+ ];
819
+ }
820
+ return [heading, ...boundedEntryLines(entries, runId, budget)];
821
+ }
822
+ function unresolvedIssues(state) {
823
+ return (state.issues ?? []).filter((i) => i.disposition !== 'resolved');
824
+ }
825
+ function hasUnresolvedIssues(state) {
826
+ return unresolvedIssues(state).length > 0;
827
+ }
828
+ function stepDigestHeading(runId) {
829
+ return `Known issues reported earlier in this run (details under ${issuesDirRef(runId)}):`;
830
+ }
831
+ const ASSIGNMENT_INSTRUCTION = `Fix the issues assigned to this step within its scope where you can, and report each result in the handoff's "issueUpdates" field.`;
832
+ // Points at the run's issues dir without embedding the run id, whose length is
833
+ // what made the normal digest impossible.
834
+ const ISSUES_DIR_FALLBACK_REF = `the '${ISSUES_DIR_NAME}' directory inside the run's directory under ${types_1.MIGRATE_RUNS_RELATIVE_DIR}/`;
835
+ const DIGEST_FALLBACK_LINE = `Known issues reported earlier in this run are not listed because the digest would exceed its size limit; see ${ISSUES_DIR_FALLBACK_REF}.`;
836
+ function overflowLine(omitted, runId) {
837
+ return ` ...and ${omitted} more not listed; see ${issuesDirRef(runId)}.`;
838
+ }
839
+ /**
840
+ * Byte budget left for entry lines after reserving the digest's fixed lines
841
+ * out of MAX_DIGEST_BYTES: the given fixed lines, the worst-case overflow line
842
+ * (every entry omitted), and one newline separator per line the digest could
843
+ * render. Derived from the serialized lines themselves, so a run id of any
844
+ * length is priced in. claimIssuesForStep and the renderers share it: a claim
845
+ * kept under it is guaranteed a rendered line, and reserving lines that may
846
+ * not render only shortens the listing. Negative when the fixed prose alone
847
+ * cannot fit.
848
+ */
849
+ function digestEntryByteBudget(fixedLines, totalEntries, runId) {
850
+ const reserved = [...fixedLines, overflowLine(totalEntries, runId)];
851
+ const maxLines = Math.min(totalEntries, MAX_DIGEST_ENTRIES) + reserved.length;
852
+ const reservedBytes = reserved.reduce((sum, line) => sum + Buffer.byteLength(line, 'utf8'), 0) +
853
+ (maxLines - 1);
854
+ return MAX_DIGEST_BYTES - reservedBytes;
855
+ }
856
+ function digestEntry(issue, label) {
857
+ // The marker counts against the cap: a truncated summary renders at exactly
858
+ // DIGEST_SUMMARY_CHARS characters.
859
+ const summary = issue.summary.length > DIGEST_SUMMARY_CHARS
860
+ ? `${issue.summary.slice(0, DIGEST_SUMMARY_CHARS - 3)}...`
861
+ : issue.summary;
862
+ return ` - ${issue.id} (${label}): ${summary}`;
863
+ }
864
+ function boundedEntryLines(entries, runId, budget) {
865
+ const lines = entries.slice(0, boundedPrefixCount(entries, budget));
866
+ const omitted = entries.length - lines.length;
867
+ if (omitted > 0) {
868
+ lines.push(overflowLine(omitted, runId));
869
+ }
870
+ return lines;
871
+ }
872
+ // A prefix only: skipping an oversized entry would let a later, lower-priority
873
+ // entry displace an earlier assigned one.
874
+ function boundedPrefixCount(entries, budget) {
875
+ let count = 0;
876
+ let bytes = 0;
877
+ for (const entry of entries) {
878
+ const entryBytes = Buffer.byteLength(entry, 'utf8');
879
+ if (count >= MAX_DIGEST_ENTRIES || bytes + entryBytes > budget) {
880
+ break;
881
+ }
882
+ count++;
883
+ bytes += entryBytes;
884
+ }
885
+ return count;
886
+ }
887
+ function issuesDir(runDirPath) {
888
+ return (0, path_1.join)(runDirPath, ISSUES_DIR_NAME);
889
+ }
890
+ // `issueId` is nx-assigned (`issue-<n>`), so the join cannot leave the
891
+ // directory.
892
+ function issueArchivePath(runDirPath, issueId) {
893
+ return (0, path_1.join)(issuesDir(runDirPath), `${issueId}.json`);
894
+ }
895
+ // The runbook points every later consumer at issues/<id>.json for the full
896
+ // details, so a rebuild from run-state fields (the reported detail is gone
897
+ // with the lost file) must not stay silent.
898
+ function warnReconstructedArchives(issueIds) {
899
+ if (issueIds.length === 0)
900
+ return;
901
+ (0, agent_output_1.warnToAgent)({
902
+ title: `The archived details for ${issueIds.join(', ')} were missing or unreadable and were rebuilt from the run state; the originally reported detail is lost.`,
903
+ });
904
+ }
905
+ /**
906
+ * Archives an application's new issues (with the detail the ledger does not
907
+ * carry) and appends its updates to the archived files. Failures propagate;
908
+ * the caller then keeps the handoff file as the fallback detail store.
909
+ * `reconstructedIds` is filled as it goes rather than returned alone: a retry
910
+ * finds a rebuilt shell healthy, so a throw-interrupted pass is the only
911
+ * chance to warn that an archive was lost.
912
+ */
913
+ function archiveIssues(runDirPath, application, reconstructedIds = []) {
914
+ for (const { entry, report } of application.newIssues) {
915
+ const record = { ...newIssueArchiveRecord(entry, report), updates: [] };
916
+ writeIssueArchive(runDirPath, entry.id, record);
917
+ }
918
+ // One append per issue, so an application's whole batch lands in a single
919
+ // atomic write a crash cannot split.
920
+ const batches = batchUpdatesByIssue(application.updates);
921
+ for (const [issueId, batch] of batches) {
922
+ const reconstructed = appendIssueUpdatesToArchive(runDirPath, issueId, batch, application.state);
923
+ if (reconstructed) {
924
+ reconstructedIds.push(issueId);
925
+ }
926
+ }
927
+ return reconstructedIds;
928
+ }
929
+ /**
930
+ * Verifies the archive state expected after phase 1. New-issue fields must
931
+ * match the report and have no updates; existing-issue updates must end with
932
+ * this application's whole batch.
933
+ */
934
+ function applicationArchivesIntact(runDirPath, application) {
935
+ const readRaw = (issueId) => {
936
+ try {
937
+ const parsed = JSON.parse((0, fs_1.readFileSync)(issueArchivePath(runDirPath, issueId), 'utf-8'));
938
+ return isPlainObject(parsed) ? parsed : null;
939
+ }
940
+ catch {
941
+ return null;
942
+ }
943
+ };
944
+ for (const { entry, report } of application.newIssues) {
945
+ const archive = readRaw(entry.id);
946
+ if (archive === null ||
947
+ !isHealthyArchive(archive, entry.id, application.state)) {
948
+ return false;
949
+ }
950
+ const expected = newIssueArchiveRecord(entry, report);
951
+ const survives = NEW_ISSUE_ARCHIVE_KEYS.every((key) => JSON.stringify(archive[key]) === JSON.stringify(expected[key]));
952
+ if (!survives ||
953
+ !Array.isArray(archive.updates) ||
954
+ archive.updates.length > 0) {
955
+ return false;
956
+ }
957
+ }
958
+ for (const [issueId, batch] of batchUpdatesByIssue(application.updates)) {
959
+ const archive = readRaw(issueId);
960
+ if (archive === null ||
961
+ !isHealthyArchive(archive, issueId, application.state)) {
962
+ return false;
963
+ }
964
+ const records = Array.isArray(archive.updates) ? archive.updates : [];
965
+ const expectedBatch = batch.map(archiveUpdateRecord);
966
+ const tail = records.slice(records.length - expectedBatch.length);
967
+ if (JSON.stringify(tail) !== JSON.stringify(expectedBatch)) {
968
+ return false;
969
+ }
970
+ }
971
+ return true;
972
+ }
973
+ const NEW_ISSUE_ARCHIVE_KEYS = [
974
+ 'id',
975
+ 'fingerprint',
976
+ 'summary',
977
+ 'reportedByStepId',
978
+ 'applicableMigrations',
979
+ 'reportedDisposition',
980
+ 'disposition',
981
+ 'detail',
982
+ ];
983
+ // Hashes the full id so ids sharing a visible prefix stay distinct.
984
+ function abbreviatedMigrationId(migrationId) {
985
+ if (migrationId.length <= MAX_UNRESOLVED_ID_CHARS)
986
+ return migrationId;
987
+ const digest = (0, crypto_1.createHash)('sha256')
988
+ .update(migrationId)
989
+ .digest('hex')
990
+ .slice(0, 8);
991
+ return `${migrationId.slice(0, MAX_UNRESOLVED_ID_CHARS - 12)}...[${digest}]`;
992
+ }
993
+ // Shared by the write path and the intactness check so their shapes cannot
994
+ // drift.
995
+ function newIssueArchiveRecord(entry, report) {
996
+ return {
997
+ id: entry.id,
998
+ fingerprint: entry.fingerprint,
999
+ summary: entry.summary,
1000
+ reportedByStepId: entry.reportedByStepId,
1001
+ applicableMigrations: report.applicableMigrations,
1002
+ // Both dispositions: the ladder may start the entry somewhere other than what
1003
+ // the reporter asked for, and the file keeps both.
1004
+ ...(report.disposition !== undefined
1005
+ ? { reportedDisposition: report.disposition }
1006
+ : {}),
1007
+ disposition: entry.disposition,
1008
+ ...(report.detail !== undefined ? { detail: report.detail } : {}),
1009
+ };
1010
+ }
1011
+ function batchUpdatesByIssue(updates) {
1012
+ const batches = new Map();
1013
+ for (const update of updates) {
1014
+ const batch = batches.get(update.issueId) ?? [];
1015
+ batch.push(update);
1016
+ batches.set(update.issueId, batch);
1017
+ }
1018
+ return batches;
1019
+ }
1020
+ // Shared by the append path and the intactness check so their shapes cannot
1021
+ // drift.
1022
+ function archiveUpdateRecord(update) {
1023
+ return {
1024
+ stepId: update.stepId,
1025
+ disposition: update.disposition,
1026
+ ...(update.note !== undefined ? { note: update.note } : {}),
1027
+ ...(update.detail !== undefined ? { detail: update.detail } : {}),
1028
+ ...(update.applicableMigrations !== undefined
1029
+ ? { applicableMigrations: update.applicableMigrations }
1030
+ : {}),
1031
+ };
1032
+ }
1033
+ // True when the archive had to be rebuilt (missing or corrupt).
1034
+ function appendIssueUpdatesToArchive(runDirPath, issueId, batch, state) {
1035
+ const filePath = issueArchivePath(runDirPath, issueId);
1036
+ // Missing or corrupt archives are rebuilt from run state, which stays
1037
+ // authoritative for dispositions. Any other read error rethrows rather than
1038
+ // overwrite: it may hide an intact file, the only copy of the issue's detail.
1039
+ let existing;
1040
+ let reconstructed = false;
1041
+ try {
1042
+ const parsed = JSON.parse((0, fs_1.readFileSync)(filePath, 'utf-8'));
1043
+ if (isHealthyArchive(parsed, issueId, state)) {
1044
+ existing = parsed;
1045
+ }
1046
+ else {
1047
+ existing = reconstructedArchiveShell(issueId, state);
1048
+ reconstructed = true;
1049
+ }
1050
+ }
1051
+ catch (e) {
1052
+ const code = e.code;
1053
+ if (code !== undefined && code !== 'ENOENT') {
1054
+ throw e;
1055
+ }
1056
+ existing = reconstructedArchiveShell(issueId, state);
1057
+ reconstructed = true;
1058
+ }
1059
+ const updates = Array.isArray(existing.updates) ? existing.updates : [];
1060
+ const records = batch.map(archiveUpdateRecord);
1061
+ // Skip a batch the archive already ends with: a crash between these writes and
1062
+ // the fold's state write replays the same application. Compared as a whole
1063
+ // suffix, since one application can legally append several records per issue.
1064
+ const tail = updates.slice(updates.length - records.length);
1065
+ if (tail.length === records.length &&
1066
+ JSON.stringify(tail) === JSON.stringify(records)) {
1067
+ return reconstructed;
1068
+ }
1069
+ writeIssueArchive(runDirPath, issueId, {
1070
+ ...existing,
1071
+ updates: [...updates, ...records],
1072
+ });
1073
+ return reconstructed;
1074
+ }
1075
+ // Identity only: id, fingerprint, summary and reporter must match the ledger
1076
+ // entry. Everything else is informational and deliberately unpoliced, since
1077
+ // run.json decides behavior and authenticating archive history is a non-goal.
1078
+ // A hollow or mismatched file rebuilds, rather than hiding the loss.
1079
+ function isHealthyArchive(value, issueId, state) {
1080
+ if (!isPlainObject(value))
1081
+ return false;
1082
+ if (value.id !== issueId)
1083
+ return false;
1084
+ const entry = (state.issues ?? []).find((i) => i.id === issueId);
1085
+ if (entry !== undefined) {
1086
+ return (value.fingerprint === entry.fingerprint &&
1087
+ value.summary === entry.summary &&
1088
+ value.reportedByStepId === entry.reportedByStepId);
1089
+ }
1090
+ if (typeof value.fingerprint === 'string' &&
1091
+ typeof value.summary === 'string' &&
1092
+ typeof value.reportedByStepId === 'string') {
1093
+ return true;
1094
+ }
1095
+ return value.reconstructed === true;
1096
+ }
1097
+ // The originally reported detail is gone with the file; `reconstructed` keeps
1098
+ // that loss visible to whoever the digest points here.
1099
+ function reconstructedArchiveShell(issueId, state) {
1100
+ const entry = state.issues?.find((i) => i.id === issueId);
1101
+ if (!entry)
1102
+ return { id: issueId, reconstructed: true };
1103
+ return {
1104
+ id: entry.id,
1105
+ fingerprint: entry.fingerprint,
1106
+ summary: entry.summary,
1107
+ reportedByStepId: entry.reportedByStepId,
1108
+ applicableMigrations: Array.isArray(entry.applicableStepIds)
1109
+ ? entry.applicableStepIds.map((stepId) => {
1110
+ const step = state.steps.find((s) => s.id === stepId);
1111
+ return step?.kind === 'migration' ? step.migrationId : stepId;
1112
+ })
1113
+ : 'unknown',
1114
+ reconstructed: true,
1115
+ };
1116
+ }
1117
+ function writeIssueArchive(runDirPath, issueId, content) {
1118
+ (0, fs_1.mkdirSync)(issuesDir(runDirPath), { recursive: true });
1119
+ const filePath = issueArchivePath(runDirPath, issueId);
1120
+ (0, atomic_write_1.publishFileAtomically)(filePath, (tmpPath) => (0, fileutils_1.writeJsonFile)(tmpPath, content));
1121
+ }