@highstate/backend 0.9.18 → 0.9.19

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 (331) hide show
  1. package/dist/chunk-5WVU2AK4.js +1535 -0
  2. package/dist/chunk-5WVU2AK4.js.map +1 -0
  3. package/dist/{chunk-OU5OQBLB.js → chunk-I7BWSAN6.js} +3 -28
  4. package/dist/{chunk-OU5OQBLB.js.map → chunk-I7BWSAN6.js.map} +1 -1
  5. package/dist/chunk-VB4YL327.js +139 -0
  6. package/dist/chunk-VB4YL327.js.map +1 -0
  7. package/dist/database/local/prisma.config.js +26 -0
  8. package/dist/database/local/prisma.config.js.map +1 -0
  9. package/dist/highstate.manifest.json +2 -1
  10. package/dist/index.js +7587 -7291
  11. package/dist/index.js.map +1 -1
  12. package/dist/library/package-resolution-worker.js +1 -1
  13. package/dist/library/package-resolution-worker.js.map +1 -1
  14. package/dist/library/worker/main.js +35 -29
  15. package/dist/library/worker/main.js.map +1 -1
  16. package/dist/shared/index.js +2 -2
  17. package/package.json +18 -9
  18. package/prisma/backend/_schema/layout.prisma +7 -0
  19. package/prisma/backend/_schema/library.prisma +17 -0
  20. package/prisma/backend/_schema/project.prisma +101 -0
  21. package/prisma/backend/_schema/pulumi.prisma +17 -0
  22. package/prisma/backend/postgresql/main.prisma +17 -0
  23. package/prisma/backend/sqlite/main.prisma +17 -0
  24. package/prisma/backend/sqlite/migrations/20250817070609_initiial/migration.sql +34 -0
  25. package/prisma/backend/sqlite/migrations/20250817104948_add_fields/migration.sql +59 -0
  26. package/prisma/backend/sqlite/migrations/20250818082732_add_models/migration.sql +41 -0
  27. package/prisma/backend/sqlite/migrations/20250818083106_a/migration.sql +19 -0
  28. package/prisma/backend/sqlite/migrations/20250818101945_hi/migration.sql +1 -0
  29. package/prisma/backend/sqlite/migrations/20250819082315_a/migration.sql +5 -0
  30. package/prisma/backend/sqlite/migrations/migration_lock.toml +3 -0
  31. package/prisma/project/api-key.prisma +27 -0
  32. package/prisma/project/artifact.prisma +52 -0
  33. package/prisma/project/custom-status.prisma +46 -0
  34. package/prisma/project/evaluation.prisma +35 -0
  35. package/prisma/project/instance.prisma +160 -0
  36. package/prisma/project/layout.prisma +23 -0
  37. package/prisma/project/lock.prisma +18 -0
  38. package/prisma/project/main.prisma +17 -0
  39. package/prisma/project/migrations/20250816081310_initial/migration.sql +300 -0
  40. package/prisma/project/migrations/20250816082523_test/migration.sql +72 -0
  41. package/prisma/project/migrations/20250818065643_update/migration.sql +42 -0
  42. package/prisma/project/migrations/20250818070758_a/migration.sql +8 -0
  43. package/prisma/project/migrations/20250818070913_a/migration.sql +8 -0
  44. package/prisma/project/migrations/20250818082720_add_motels/migration.sql +11 -0
  45. package/prisma/project/migrations/20250818112523_hello/migration.sql +35 -0
  46. package/prisma/project/migrations/20250819082305_a/migration.sql +14 -0
  47. package/prisma/project/migrations/20250819165004_add_missing_fields/migration.sql +216 -0
  48. package/prisma/project/migrations/20250819171309_a/migration.sql +22 -0
  49. package/prisma/project/migrations/20250820113949_a/migration.sql +66 -0
  50. package/prisma/project/migrations/20250820144256_b/migration.sql +31 -0
  51. package/prisma/project/migrations/20250820145547_a/migration.sql +24 -0
  52. package/prisma/project/migrations/20250820182517_b/migration.sql +2 -0
  53. package/prisma/project/migrations/20250821172324_a/migration.sql +2 -0
  54. package/prisma/project/migrations/20250822081339_a/migration.sql +219 -0
  55. package/prisma/project/migrations/20250822083742_b/migration.sql +1 -0
  56. package/prisma/project/migrations/20250822105134_boom/migration.sql +1 -0
  57. package/prisma/project/migrations/20250822141028_b/migration.sql +1 -0
  58. package/prisma/project/migrations/20250822142342_b/migration.sql +16 -0
  59. package/prisma/project/migrations/20250824072720_a/migration.sql +1 -0
  60. package/prisma/project/migrations/20250824093656_b/migration.sql +21 -0
  61. package/prisma/project/migrations/20250825082518_a/migration.sql +1 -0
  62. package/prisma/project/migrations/20250825085343_b/migration.sql +1 -0
  63. package/prisma/project/migrations/20250825091312_a/migration.sql +1 -0
  64. package/prisma/project/migrations/20250903095431_hi/migration.sql +44 -0
  65. package/prisma/project/migrations/20250903174255_a/migration.sql +24 -0
  66. package/prisma/project/migrations/20250908095205_hi/migration.sql +18 -0
  67. package/prisma/project/migrations/20250909155857_hi/migration.sql +15 -0
  68. package/prisma/project/migrations/migration_lock.toml +3 -0
  69. package/prisma/project/model.prisma +37 -0
  70. package/prisma/project/operation.prisma +148 -0
  71. package/prisma/project/page.prisma +41 -0
  72. package/prisma/project/secret.prisma +42 -0
  73. package/prisma/project/service-account.prisma +36 -0
  74. package/prisma/project/terminal.prisma +90 -0
  75. package/prisma/project/trigger.prisma +31 -0
  76. package/prisma/project/unlock-method.prisma +32 -0
  77. package/prisma/project/worker.prisma +138 -0
  78. package/src/artifact/abstractions.ts +13 -13
  79. package/src/artifact/encryption.ts +30 -54
  80. package/src/artifact/factory.ts +6 -9
  81. package/src/artifact/local.ts +33 -46
  82. package/src/business/api-key.ts +24 -36
  83. package/src/business/artifact.test.ts +978 -0
  84. package/src/business/artifact.ts +136 -216
  85. package/src/business/evaluation.ts +328 -0
  86. package/src/business/index.ts +5 -2
  87. package/src/business/instance-lock.test.ts +1060 -0
  88. package/src/business/instance-lock.ts +387 -78
  89. package/src/business/instance-state.test.ts +735 -0
  90. package/src/business/instance-state.ts +582 -337
  91. package/src/business/operation.test.ts +439 -0
  92. package/src/business/operation.ts +174 -208
  93. package/src/business/project-model.ts +258 -0
  94. package/src/business/project-unlock.ts +168 -126
  95. package/src/business/project.ts +287 -179
  96. package/src/business/secret.test.ts +465 -130
  97. package/src/business/secret.ts +186 -217
  98. package/src/business/settings.test.ts +695 -0
  99. package/src/business/settings.ts +855 -0
  100. package/src/business/terminal-session.ts +90 -0
  101. package/src/business/unit-extra.test.ts +539 -0
  102. package/src/business/unit-extra.ts +160 -0
  103. package/src/business/worker.test.ts +356 -579
  104. package/src/business/worker.ts +238 -339
  105. package/src/common/codebase.ts +65 -0
  106. package/src/common/index.ts +3 -5
  107. package/src/common/logger.ts +5 -0
  108. package/src/common/utils.ts +4 -3
  109. package/src/config.ts +10 -11
  110. package/src/database/_generated/backend/postgresql/client.ts +72 -0
  111. package/src/database/_generated/backend/postgresql/commonInputTypes.ts +350 -0
  112. package/src/database/_generated/backend/postgresql/enums.ts +13 -0
  113. package/src/database/_generated/backend/postgresql/internal/class.ts +320 -0
  114. package/src/database/_generated/backend/postgresql/internal/prismaNamespace.ts +1238 -0
  115. package/src/database/_generated/backend/postgresql/models/Library.ts +1263 -0
  116. package/src/database/_generated/backend/postgresql/models/Project.ts +2175 -0
  117. package/src/database/_generated/backend/postgresql/models/ProjectModelStorage.ts +1263 -0
  118. package/src/database/_generated/backend/postgresql/models/ProjectSpace.ts +1602 -0
  119. package/src/database/_generated/backend/postgresql/models/PulumiBackend.ts +1263 -0
  120. package/src/database/_generated/backend/postgresql/models/UserWorkspaseLayout.ts +1065 -0
  121. package/src/database/_generated/backend/postgresql/models.ts +16 -0
  122. package/src/database/_generated/backend/postgresql/pjtg.ts +182 -0
  123. package/src/database/_generated/backend/sqlite/client.ts +72 -0
  124. package/src/database/_generated/backend/sqlite/commonInputTypes.ts +331 -0
  125. package/src/database/_generated/backend/sqlite/enums.ts +13 -0
  126. package/src/database/_generated/backend/sqlite/internal/class.ts +318 -0
  127. package/src/database/_generated/backend/sqlite/internal/prismaNamespace.ts +1207 -0
  128. package/src/database/_generated/backend/sqlite/models/Library.ts +1261 -0
  129. package/src/database/_generated/backend/sqlite/models/Project.ts +2169 -0
  130. package/src/database/_generated/backend/sqlite/models/ProjectModelStorage.ts +1261 -0
  131. package/src/database/_generated/backend/sqlite/models/ProjectSpace.ts +1599 -0
  132. package/src/database/_generated/backend/sqlite/models/PulumiBackend.ts +1261 -0
  133. package/src/database/_generated/backend/sqlite/models/UserWorkspaseLayout.ts +1063 -0
  134. package/src/database/_generated/backend/sqlite/models.ts +16 -0
  135. package/src/database/_generated/backend/sqlite/pjtg.ts +182 -0
  136. package/src/database/_generated/project/client.ts +204 -0
  137. package/src/database/_generated/project/commonInputTypes.ts +827 -0
  138. package/src/database/_generated/project/enums.ts +104 -0
  139. package/src/database/_generated/project/internal/class.ts +479 -0
  140. package/src/database/_generated/project/internal/prismaNamespace.ts +2974 -0
  141. package/src/database/_generated/project/models/ApiKey.ts +1506 -0
  142. package/src/database/_generated/project/models/Artifact.ts +2051 -0
  143. package/src/database/_generated/project/models/HubModel.ts +1125 -0
  144. package/src/database/_generated/project/models/InstanceCustomStatus.ts +1713 -0
  145. package/src/database/_generated/project/models/InstanceEvaluationState.ts +1312 -0
  146. package/src/database/_generated/project/models/InstanceLock.ts +1268 -0
  147. package/src/database/_generated/project/models/InstanceModel.ts +1125 -0
  148. package/src/database/_generated/project/models/InstanceOperationState.ts +1707 -0
  149. package/src/database/_generated/project/models/InstanceState.ts +4613 -0
  150. package/src/database/_generated/project/models/Operation.ts +1647 -0
  151. package/src/database/_generated/project/models/OperationLog.ts +1455 -0
  152. package/src/database/_generated/project/models/Page.ts +1838 -0
  153. package/src/database/_generated/project/models/Secret.ts +1692 -0
  154. package/src/database/_generated/project/models/ServiceAccount.ts +2165 -0
  155. package/src/database/_generated/project/models/Terminal.ts +2038 -0
  156. package/src/database/_generated/project/models/TerminalSession.ts +1454 -0
  157. package/src/database/_generated/project/models/TerminalSessionLog.ts +1280 -0
  158. package/src/database/_generated/project/models/Trigger.ts +1430 -0
  159. package/src/database/_generated/project/models/UnlockMethod.ts +1220 -0
  160. package/src/database/_generated/project/models/UserCompositeViewport.ts +1280 -0
  161. package/src/database/_generated/project/models/UserProjectViewport.ts +1059 -0
  162. package/src/database/_generated/project/models/Worker.ts +1459 -0
  163. package/src/database/_generated/project/models/WorkerUnitRegistration.ts +1524 -0
  164. package/src/database/_generated/project/models/WorkerVersion.ts +1974 -0
  165. package/src/database/_generated/project/models/WorkerVersionLog.ts +1318 -0
  166. package/src/database/_generated/project/models.ts +35 -0
  167. package/src/database/_generated/project/pjtg.ts +182 -0
  168. package/src/database/abstractions.ts +19 -0
  169. package/src/database/factory.ts +37 -0
  170. package/src/database/index.ts +6 -0
  171. package/src/database/local/backend.ts +134 -0
  172. package/src/database/local/index.ts +3 -0
  173. package/src/database/local/meta.ts +46 -0
  174. package/src/database/local/prisma.config.ts +25 -0
  175. package/src/database/local/project.ts +39 -0
  176. package/src/database/manager.ts +181 -0
  177. package/src/database/migrate.ts +35 -0
  178. package/src/database/prisma.ts +56 -0
  179. package/src/database/well-known.ts +38 -0
  180. package/src/index.ts +4 -4
  181. package/src/library/abstractions.ts +3 -5
  182. package/src/library/factory.ts +1 -1
  183. package/src/library/local.ts +81 -26
  184. package/src/library/package-resolution-worker.ts +1 -1
  185. package/src/library/worker/evaluator.ts +40 -23
  186. package/src/library/worker/loader.lite.ts +1 -1
  187. package/src/library/worker/main.ts +3 -10
  188. package/src/library/worker/protocol.ts +0 -1
  189. package/src/lock/index.ts +0 -1
  190. package/src/lock/manager.ts +0 -10
  191. package/src/orchestrator/manager.ts +190 -104
  192. package/src/orchestrator/operation-context.ts +357 -0
  193. package/src/orchestrator/operation-plan.destroy.test.md +357 -0
  194. package/src/orchestrator/operation-plan.destroy.test.ts +775 -0
  195. package/src/orchestrator/operation-plan.fixtures.ts +213 -0
  196. package/src/orchestrator/operation-plan.md +198 -0
  197. package/src/orchestrator/operation-plan.refresh.test.md +199 -0
  198. package/src/orchestrator/operation-plan.refresh.test.ts +367 -0
  199. package/src/orchestrator/operation-plan.ts +709 -0
  200. package/src/orchestrator/operation-plan.update.test.md +485 -0
  201. package/src/orchestrator/operation-plan.update.test.ts +1066 -0
  202. package/src/orchestrator/operation-workset.ts +233 -578
  203. package/src/orchestrator/operation.ts +435 -948
  204. package/src/orchestrator/plan-test-builder.ts +267 -0
  205. package/src/project-model/abstractions.ts +118 -0
  206. package/src/project-model/backends/codebase.ts +365 -0
  207. package/src/project-model/backends/database.ts +440 -0
  208. package/src/project-model/errors.ts +81 -0
  209. package/src/project-model/factory.ts +24 -0
  210. package/src/project-model/index.ts +4 -0
  211. package/src/project-model/utils.test.ts +544 -0
  212. package/src/project-model/utils.ts +242 -0
  213. package/src/pubsub/abstractions.ts +10 -1
  214. package/src/pubsub/factory.ts +4 -4
  215. package/src/pubsub/index.ts +1 -0
  216. package/src/pubsub/manager.ts +29 -13
  217. package/src/pubsub/memory.ts +31 -0
  218. package/src/runner/abstractions.ts +33 -41
  219. package/src/runner/artifact-env.ts +19 -8
  220. package/src/runner/factory.ts +6 -6
  221. package/src/runner/force-abort.ts +3 -6
  222. package/src/runner/local.ts +64 -67
  223. package/src/runner/pulumi.ts +23 -63
  224. package/src/services.ts +181 -123
  225. package/src/shared/models/backend/index.ts +3 -1
  226. package/src/shared/models/backend/library.ts +9 -1
  227. package/src/shared/models/backend/project.ts +43 -42
  228. package/src/shared/models/backend/pulumi.ts +14 -0
  229. package/src/shared/models/backend/unlock-method.ts +1 -1
  230. package/src/shared/models/backend/well-known.ts +58 -0
  231. package/src/shared/models/base.ts +40 -26
  232. package/src/shared/models/errors.ts +82 -1
  233. package/src/shared/models/index.ts +3 -2
  234. package/src/shared/models/prisma.ts +36 -0
  235. package/src/shared/models/project/api-key.ts +37 -59
  236. package/src/shared/models/project/artifact.ts +16 -76
  237. package/src/shared/models/project/custom-status.ts +12 -0
  238. package/src/shared/models/project/index.ts +8 -7
  239. package/src/shared/models/project/lock.ts +10 -78
  240. package/src/shared/models/project/model.ts +19 -1
  241. package/src/shared/models/project/operation.ts +222 -99
  242. package/src/shared/models/project/page.ts +37 -48
  243. package/src/shared/models/project/secret.ts +29 -89
  244. package/src/shared/models/project/service-account.ts +12 -17
  245. package/src/shared/models/project/state.ts +100 -407
  246. package/src/shared/models/project/terminal.ts +75 -88
  247. package/src/shared/models/project/trigger.ts +13 -49
  248. package/src/shared/models/project/unlock-method.ts +20 -26
  249. package/src/shared/models/project/worker.ts +89 -90
  250. package/src/shared/resolvers/graph-resolver.ts +21 -0
  251. package/src/shared/resolvers/index.ts +1 -1
  252. package/src/shared/resolvers/input-hash.ts +24 -14
  253. package/src/shared/resolvers/input.ts +1 -1
  254. package/src/shared/resolvers/registry.ts +5 -4
  255. package/src/shared/resolvers/state.ts +12 -1
  256. package/src/shared/resolvers/validation.ts +7 -3
  257. package/src/shared/utils/index.ts +1 -2
  258. package/src/shared/utils/promise-tracker.ts +30 -3
  259. package/src/terminal/abstractions.ts +1 -1
  260. package/src/terminal/docker.ts +3 -3
  261. package/src/terminal/manager.ts +102 -118
  262. package/src/test-utils/database.ts +119 -0
  263. package/src/test-utils/index.ts +2 -0
  264. package/src/test-utils/services.ts +134 -0
  265. package/src/unlock/abstractions.ts +5 -23
  266. package/src/unlock/memory.ts +9 -14
  267. package/src/worker/abstractions.ts +7 -4
  268. package/src/worker/docker.ts +14 -19
  269. package/src/worker/manager.ts +366 -97
  270. package/dist/chunk-NAAIDR4U.js +0 -8499
  271. package/dist/chunk-NAAIDR4U.js.map +0 -1
  272. package/dist/chunk-Y7DXREVO.js +0 -1745
  273. package/dist/chunk-Y7DXREVO.js.map +0 -1
  274. package/dist/magic-string.es-5ABAC4JN.js +0 -1292
  275. package/dist/magic-string.es-5ABAC4JN.js.map +0 -1
  276. package/src/business/__traces__/secret/update-instance-secrets/create-and-delete-secrets-simultaneously.md +0 -356
  277. package/src/business/__traces__/secret/update-instance-secrets/create-new-secrets-for-instance.md +0 -274
  278. package/src/business/__traces__/secret/update-instance-secrets/delete-existing-secrets.md +0 -223
  279. package/src/business/__traces__/secret/update-instance-secrets/no-op-when-no-changes.md +0 -147
  280. package/src/business/__traces__/secret/update-instance-secrets/update-existing-secrets.md +0 -280
  281. package/src/business/__traces__/worker/update-unit-registrations/add-new-unit-registration-when-other-exists.md +0 -360
  282. package/src/business/__traces__/worker/update-unit-registrations/add-new-unit-registration.md +0 -215
  283. package/src/business/__traces__/worker/update-unit-registrations/create-multiple-workers-with-different-identities.md +0 -427
  284. package/src/business/__traces__/worker/update-unit-registrations/handle-nonexistent-registration-id-gracefully.md +0 -217
  285. package/src/business/__traces__/worker/update-unit-registrations/no-op-when-no-changes.md +0 -132
  286. package/src/business/__traces__/worker/update-unit-registrations/recreate-worker-when-image-changes.md +0 -454
  287. package/src/business/__traces__/worker/update-unit-registrations/recreate-worker-when-image-version-changes.md +0 -426
  288. package/src/business/__traces__/worker/update-unit-registrations/recreate-worker-with-same-identity-reuses-service-account.md +0 -372
  289. package/src/business/__traces__/worker/update-unit-registrations/remove-one-of-multiple-unit-registrations.md +0 -383
  290. package/src/business/__traces__/worker/update-unit-registrations/remove-unit-registration.md +0 -245
  291. package/src/business/__traces__/worker/update-unit-registrations/update-existing-unit-registration-when-params-change.md +0 -174
  292. package/src/business/__traces__/worker/update-unit-registrations/update-params-and-image-simultaneously.md +0 -432
  293. package/src/business/__traces__/worker/update-unit-registrations/worker-with-multiple-registrations-not-deleted-when-one-removed.md +0 -220
  294. package/src/business/backend-unlock.ts +0 -10
  295. package/src/common/clock.ts +0 -18
  296. package/src/common/performance.ts +0 -44
  297. package/src/common/random.ts +0 -68
  298. package/src/common/test/index.ts +0 -2
  299. package/src/common/test/render.ts +0 -98
  300. package/src/common/test/tracer.ts +0 -359
  301. package/src/hotstate/abstractions.ts +0 -48
  302. package/src/hotstate/factory.ts +0 -17
  303. package/src/hotstate/index.ts +0 -3
  304. package/src/hotstate/manager.ts +0 -192
  305. package/src/hotstate/memory.ts +0 -100
  306. package/src/hotstate/validation.ts +0 -100
  307. package/src/lock/test.ts +0 -108
  308. package/src/project/abstractions.ts +0 -78
  309. package/src/project/evaluation.ts +0 -248
  310. package/src/project/factory.ts +0 -11
  311. package/src/project/index.ts +0 -3
  312. package/src/project/local.ts +0 -417
  313. package/src/pubsub/local.ts +0 -36
  314. package/src/pubsub/validation.ts +0 -33
  315. package/src/shared/utils/args.ts +0 -25
  316. package/src/state/abstractions.ts +0 -289
  317. package/src/state/encryption.ts +0 -98
  318. package/src/state/factory.ts +0 -20
  319. package/src/state/index.ts +0 -7
  320. package/src/state/local/backend.ts +0 -106
  321. package/src/state/local/collection.ts +0 -361
  322. package/src/state/local/index.ts +0 -2
  323. package/src/state/manager.ts +0 -890
  324. package/src/state/memory/backend.ts +0 -70
  325. package/src/state/memory/collection.ts +0 -270
  326. package/src/state/memory/index.ts +0 -2
  327. package/src/state/repository/index.ts +0 -2
  328. package/src/state/repository/repository.index.ts +0 -193
  329. package/src/state/repository/repository.ts +0 -507
  330. package/src/state/test.ts +0 -457
  331. /package/src/{state → database/local}/keyring.ts +0 -0
@@ -0,0 +1,709 @@
1
+ import type { InstanceId, InstanceModel } from "@highstate/contract"
2
+ import type {
3
+ InstanceState,
4
+ OperationOptions,
5
+ OperationPhase,
6
+ OperationPhaseInstance,
7
+ OperationType,
8
+ } from "../shared"
9
+ import type { OperationContext } from "./operation-context"
10
+ import { isVirtualGhostInstance } from "../shared"
11
+
12
+ type CompositeType = "unknown" | "compositional" | "substantive"
13
+ type InclusionReason =
14
+ | "explicit"
15
+ | "dependency"
16
+ | "dependent_cascade"
17
+ | "composite_child"
18
+ | "parent_composite"
19
+ | "ghost_cleanup"
20
+
21
+ interface WorkState {
22
+ included: Map<InstanceId, InclusionReason>
23
+ compositeTypes: Map<InstanceId, CompositeType>
24
+ pendingWork: Set<InstanceId>
25
+ changed: boolean
26
+ // track relationships for message generation
27
+ dependencyRequiredBy: Map<InstanceId, InstanceId> // dependency -> dependent
28
+ childTriggeringParent: Map<InstanceId, InstanceId> // parent -> child that caused inclusion
29
+ forceFlags: Map<InstanceId, "dependencies" | "children"> // track force reasons
30
+ }
31
+
32
+ export function createOperationPlan(
33
+ context: OperationContext,
34
+ type: OperationType,
35
+ requestedInstanceIds: string[],
36
+ options: OperationOptions,
37
+ ): OperationPhase[] {
38
+ // handle preview restrictions
39
+ if (type === "preview") {
40
+ validatePreviewRestrictions(context, requestedInstanceIds)
41
+ }
42
+
43
+ // initialize work state
44
+ const workState: WorkState = {
45
+ included: new Map(),
46
+ compositeTypes: new Map(),
47
+ pendingWork: new Set(),
48
+ changed: true,
49
+ dependencyRequiredBy: new Map(),
50
+ childTriggeringParent: new Map(),
51
+ forceFlags: new Map(),
52
+ }
53
+
54
+ // seed with explicit requests
55
+ for (const instanceId of requestedInstanceIds) {
56
+ workState.included.set(instanceId as InstanceId, "explicit")
57
+ workState.pendingWork.add(instanceId as InstanceId)
58
+ }
59
+
60
+ // work loop - iterate until stabilized
61
+ let iteration = 0
62
+ while (workState.changed && iteration < 100) {
63
+ iteration++
64
+ workState.changed = false
65
+
66
+ const workItems = Array.from(workState.pendingWork)
67
+ workState.pendingWork.clear()
68
+
69
+ for (const instanceId of workItems) {
70
+ processInstance(instanceId, workState, context, options, type)
71
+ }
72
+
73
+ // ensure all instances get at least one chance to be processed
74
+ if (iteration === 1) {
75
+ for (const instanceId of context.getInstanceIds()) {
76
+ if (!workState.pendingWork.has(instanceId) && !workItems.includes(instanceId)) {
77
+ workState.pendingWork.add(instanceId)
78
+ workState.changed = true
79
+ }
80
+ }
81
+ }
82
+ }
83
+
84
+ if (iteration >= 100) {
85
+ throw new Error(`Operation plan creation did not converge after 100 iterations`)
86
+ }
87
+
88
+ // create ordered phases
89
+ return createOrderedPhases(workState, context, type, options)
90
+ }
91
+
92
+ function validatePreviewRestrictions(
93
+ context: OperationContext,
94
+ requestedInstanceIds: string[],
95
+ ): void {
96
+ for (const instanceId of requestedInstanceIds) {
97
+ const dependents = context.getDependentStates(instanceId as InstanceId)
98
+ if (dependents.length > 0) {
99
+ throw new Error(
100
+ `Preview operation not allowed for instance ${instanceId} - has dependent instances`,
101
+ )
102
+ }
103
+ }
104
+ }
105
+
106
+ function processInstance(
107
+ instanceId: InstanceId,
108
+ workState: WorkState,
109
+ context: OperationContext,
110
+ options: OperationOptions,
111
+ operationType: OperationType,
112
+ ): void {
113
+ const instance = context.getInstance(instanceId)
114
+
115
+ // update composite classification
116
+ updateCompositeClassification(instance, workState, context)
117
+
118
+ // apply operation-specific inclusion rules
119
+ if (operationType === "update" || operationType === "preview") {
120
+ processUpdateInclusions(instance, workState, context, options)
121
+ }
122
+ if (operationType === "refresh") {
123
+ processRefreshInclusions(instance, workState, context, options)
124
+ }
125
+ if (operationType === "destroy" || operationType === "recreate") {
126
+ processDestroyInclusions(instance, workState, context, options)
127
+ }
128
+
129
+ // propagate to related instances
130
+ propagateToRelated(instance, workState)
131
+ }
132
+
133
+ function updateCompositeClassification(
134
+ instance: ReturnType<OperationContext["getInstance"]>,
135
+ workState: WorkState,
136
+ context: OperationContext,
137
+ ): void {
138
+ if (instance.kind !== "composite") {
139
+ return
140
+ }
141
+
142
+ const currentType = workState.compositeTypes.get(instance.id) ?? "unknown"
143
+ let newType: CompositeType = "unknown"
144
+ const inclusionReason = workState.included.get(instance.id)
145
+
146
+ // check if explicitly requested
147
+ if (inclusionReason === "explicit") {
148
+ newType = "substantive"
149
+ }
150
+
151
+ if (workState.included.has(instance.id) && inclusionReason !== "explicit") {
152
+ // check if any children are included due to external dependencies
153
+ const children = context.getInstanceChildren(instance.id)
154
+ let hasExternalDependencyChildren = false
155
+ for (const child of children) {
156
+ const reason = workState.included.get(child.id)
157
+ if (reason === "dependency" || reason === "dependent_cascade") {
158
+ // check if this dependency is external to this composite
159
+ const requiredBy = workState.dependencyRequiredBy.get(child.id)
160
+ if (requiredBy) {
161
+ const requiredByInstance = context.getInstance(requiredBy)
162
+ // if the requiring instance is not a child of this composite, it's external
163
+ if (requiredByInstance.parentId !== instance.id) {
164
+ hasExternalDependencyChildren = true
165
+ break
166
+ }
167
+ }
168
+ }
169
+ }
170
+
171
+ newType = hasExternalDependencyChildren ? "substantive" : "compositional"
172
+ }
173
+
174
+ // if classification changed, mark for re-processing
175
+ if (newType !== currentType) {
176
+ workState.compositeTypes.set(instance.id, newType)
177
+ workState.pendingWork.add(instance.id)
178
+ workState.changed = true
179
+
180
+ // re-process children when parent classification changes
181
+ const children = context.getInstanceChildren(instance.id)
182
+ for (const child of children) {
183
+ workState.pendingWork.add(child.id)
184
+ }
185
+ }
186
+ }
187
+
188
+ function processUpdateInclusions(
189
+ instance: InstanceModel,
190
+ workState: WorkState,
191
+ context: OperationContext,
192
+ options: OperationOptions,
193
+ ): void {
194
+ // check if should be included as composite child
195
+ if (instance.parentId) {
196
+ // check if this instance is a descendant of any substantive composite
197
+ const substantiveAncestor = findSubstantiveAncestor(instance.parentId, workState, context)
198
+
199
+ if (substantiveAncestor) {
200
+ const isInstanceOutdated = isOutdated(instance, context)
201
+ const shouldInclude =
202
+ options.forceUpdateChildren ||
203
+ (!options.allowPartialCompositeInstanceUpdate && isInstanceOutdated)
204
+
205
+ if (shouldInclude && !workState.included.has(instance.id)) {
206
+ include(instance.id, "composite_child", workState, {
207
+ forceFlag: options.forceUpdateChildren ? "children" : undefined,
208
+ })
209
+ }
210
+ }
211
+ }
212
+
213
+ // process dependencies if this instance is included
214
+ if (workState.included.has(instance.id)) {
215
+ const dependencies = context.getDependencies(instance.id)
216
+ for (const depInstance of dependencies) {
217
+ const shouldInclude = options.forceUpdateDependencies || isOutdated(depInstance, context)
218
+
219
+ if (shouldInclude && !workState.included.has(depInstance.id)) {
220
+ include(depInstance.id, "dependency", workState, {
221
+ requiredBy: instance.id,
222
+ forceFlag: options.forceUpdateDependencies ? "dependencies" : undefined,
223
+ })
224
+ }
225
+ }
226
+ }
227
+ }
228
+
229
+ function processRefreshInclusions(
230
+ instance: InstanceModel,
231
+ workState: WorkState,
232
+ context: OperationContext,
233
+ options: OperationOptions,
234
+ ): void {
235
+ // check if should be included as composite child
236
+ if (instance.parentId) {
237
+ // check if this instance is a descendant of any substantive composite
238
+ const substantiveAncestor = findSubstantiveAncestor(instance.parentId, workState, context)
239
+
240
+ if (substantiveAncestor) {
241
+ const isInstanceOutdated = isOutdated(instance, context)
242
+ const shouldInclude =
243
+ options.forceUpdateChildren ||
244
+ (!options.allowPartialCompositeInstanceUpdate && isInstanceOutdated)
245
+
246
+ if (shouldInclude && !workState.included.has(instance.id)) {
247
+ include(instance.id, "composite_child", workState, {
248
+ forceFlag: options.forceUpdateChildren ? "children" : undefined,
249
+ })
250
+ }
251
+ }
252
+ }
253
+
254
+ // process dependencies if this instance is included
255
+ // key difference: only include dependencies if forced, not if outdated
256
+ if (workState.included.has(instance.id)) {
257
+ const dependencies = context.getDependencies(instance.id)
258
+ for (const depInstance of dependencies) {
259
+ const shouldInclude = options.forceUpdateDependencies
260
+
261
+ if (shouldInclude && !workState.included.has(depInstance.id)) {
262
+ include(depInstance.id, "dependency", workState, {
263
+ requiredBy: instance.id,
264
+ forceFlag: options.forceUpdateDependencies ? "dependencies" : undefined,
265
+ })
266
+ }
267
+ }
268
+ }
269
+ }
270
+
271
+ function processDestroyInclusions(
272
+ instance: InstanceModel,
273
+ workState: WorkState,
274
+ context: OperationContext,
275
+ options: OperationOptions,
276
+ ): void {
277
+ // check if should be included as composite child
278
+ if (instance.parentId) {
279
+ const parentType = workState.compositeTypes.get(instance.parentId)
280
+ if (parentType === "substantive" && !workState.included.has(instance.id)) {
281
+ // all children of substantive composites being destroyed must be included
282
+ // when partial destruction is disabled
283
+ if (!options.allowPartialCompositeInstanceDestruction) {
284
+ include(instance.id, "composite_child", workState)
285
+ }
286
+ }
287
+ }
288
+
289
+ // process dependents if this instance is included and cascade enabled
290
+ if (workState.included.has(instance.id) && options.destroyDependentInstances) {
291
+ const dependentStates = context.getDependentStates(instance.id)
292
+
293
+ for (const dependentState of dependentStates) {
294
+ if (!workState.included.has(dependentState.instanceId)) {
295
+ include(dependentState.instanceId, "dependent_cascade", workState, {
296
+ requiredBy: instance.id,
297
+ })
298
+ }
299
+ }
300
+ }
301
+ }
302
+
303
+ function propagateToRelated(instance: InstanceModel, workState: WorkState): void {
304
+ // check if this instance should propagate upward
305
+ // composites included as "parent_composite" should not propagate upward (compositional boundary)
306
+ if (instance.kind === "composite") {
307
+ const inclusionReason = workState.included.get(instance.id)
308
+ if (inclusionReason === "parent_composite") {
309
+ // compositional boundary - don't propagate upward
310
+ return
311
+ }
312
+ }
313
+
314
+ // propagate upward to parent if instance is included
315
+ if (
316
+ workState.included.has(instance.id) &&
317
+ instance.parentId &&
318
+ !workState.included.has(instance.parentId)
319
+ ) {
320
+ include(instance.parentId, "parent_composite", workState, {
321
+ triggeringChild: instance.id,
322
+ })
323
+ }
324
+ }
325
+
326
+ function findSubstantiveAncestor(
327
+ instanceId: InstanceId,
328
+ workState: WorkState,
329
+ context: OperationContext,
330
+ ): InstanceId | null {
331
+ let currentId: InstanceId | undefined = instanceId
332
+
333
+ // walk up the parent chain looking for a substantive composite
334
+ while (currentId) {
335
+ const compositeType = workState.compositeTypes.get(currentId)
336
+ if (compositeType === "substantive") {
337
+ return currentId
338
+ }
339
+
340
+ const instance = context.getInstance(currentId)
341
+ currentId = instance.parentId
342
+ }
343
+
344
+ return null
345
+ }
346
+
347
+ function include(
348
+ instanceId: InstanceId,
349
+ reason: InclusionReason,
350
+ workState: WorkState,
351
+ context?: {
352
+ requiredBy?: InstanceId
353
+ triggeringChild?: InstanceId
354
+ forceFlag?: "dependencies" | "children"
355
+ },
356
+ ): void {
357
+ const existing = workState.included.get(instanceId)
358
+ if (existing) {
359
+ return
360
+ }
361
+
362
+ workState.included.set(instanceId, reason)
363
+ workState.pendingWork.add(instanceId)
364
+ workState.changed = true
365
+
366
+ // track relationships for message generation
367
+ if (context?.requiredBy) {
368
+ workState.dependencyRequiredBy.set(instanceId, context.requiredBy)
369
+ }
370
+ if (context?.triggeringChild) {
371
+ workState.childTriggeringParent.set(instanceId, context.triggeringChild)
372
+ }
373
+ if (context?.forceFlag) {
374
+ workState.forceFlags.set(instanceId, context.forceFlag)
375
+ }
376
+ }
377
+
378
+ function createOrderedPhases(
379
+ workState: WorkState,
380
+ context: OperationContext,
381
+ type: OperationType,
382
+ options: OperationOptions,
383
+ ): OperationPhase[] {
384
+ const phases: OperationPhase[] = []
385
+ const includedIds = Array.from(workState.included.keys())
386
+
387
+ if (type === "update" || type === "preview" || type === "refresh") {
388
+ // filter instances that actually need updating
389
+ const instancesNeedingUpdate = includedIds.filter(id => {
390
+ const instance = context.getInstance(id)
391
+ const inclusionReason = workState.included.get(id)
392
+
393
+ // always include if outdated or forced
394
+ if (isOutdated(instance, context)) return true
395
+ if (inclusionReason === "dependency" && options.forceUpdateDependencies) return true
396
+ if (inclusionReason === "composite_child" && options.forceUpdateChildren) return true
397
+
398
+ // include explicit requests, but composites only if they have non-ghost children
399
+ if (inclusionReason === "explicit") {
400
+ if (instance.kind === "composite") {
401
+ // for composites, only include if they have non-ghost children that need updating
402
+ const children = context.getInstanceChildren(id)
403
+ return children.some(child => {
404
+ if (!workState.included.has(child.id)) return false
405
+ const childState = context.getState(child.id)
406
+ return !isVirtualGhostInstance(childState)
407
+ })
408
+ }
409
+ return true
410
+ }
411
+
412
+ // include parent composites only if they have children needing updates
413
+ if (inclusionReason === "parent_composite") {
414
+ const children = context.getInstanceChildren(id)
415
+ return children.some(
416
+ child =>
417
+ workState.included.has(child.id) &&
418
+ workState.included.get(child.id) !== "parent_composite",
419
+ )
420
+ }
421
+
422
+ // include other types (dependency, composite_child, etc.)
423
+ return true
424
+ })
425
+
426
+ const updateInstances = topologicalSort(instancesNeedingUpdate, context, false)
427
+ .map(id => createPhaseInstance(id, context, workState))
428
+ .filter(inst => inst !== null) as OperationPhaseInstance[]
429
+
430
+ if (updateInstances.length > 0) {
431
+ const phaseType = type === "refresh" ? "refresh" : "update"
432
+ phases.push({ type: phaseType, instances: updateInstances })
433
+ }
434
+
435
+ // handle ghost cleanup for updates (but not for refresh operations)
436
+ if (type !== "refresh") {
437
+ const compositesNeedingGhostCleanup = new Set<InstanceId>()
438
+ for (const instanceId of includedIds) {
439
+ const instance = context.getInstance(instanceId)
440
+ if (instance.kind !== "composite") continue
441
+
442
+ const compositeType = workState.compositeTypes.get(instanceId)
443
+ if (compositeType !== "substantive") continue
444
+
445
+ // check if this composite has ghost children
446
+ const children = context.getInstanceChildren(instanceId)
447
+ const hasGhostChildren = children.some(child => {
448
+ const state = context.getState(child.id)
449
+ return isVirtualGhostInstance(state)
450
+ })
451
+
452
+ if (hasGhostChildren) {
453
+ compositesNeedingGhostCleanup.add(instanceId)
454
+ }
455
+ }
456
+ const ghostInstances = findGhostCleanup(context, compositesNeedingGhostCleanup)
457
+
458
+ if (ghostInstances.length > 0) {
459
+ const sortedGhosts = topologicalSort(
460
+ ghostInstances.map(g => g.id),
461
+ context,
462
+ true,
463
+ )
464
+ .map(id => createPhaseInstance(id, context, workState))
465
+ .filter(inst => inst !== null) as OperationPhaseInstance[]
466
+
467
+ if (sortedGhosts.length > 0) {
468
+ phases.push({ type: "destroy", instances: sortedGhosts })
469
+ }
470
+ }
471
+ }
472
+ }
473
+
474
+ if (type === "destroy") {
475
+ const destroyInstances = topologicalSort(includedIds, context, true)
476
+ .map(id => createPhaseInstance(id, context, workState))
477
+ .filter(inst => inst !== null) as OperationPhaseInstance[]
478
+
479
+ if (destroyInstances.length > 0) {
480
+ phases.push({ type: "destroy", instances: destroyInstances })
481
+ }
482
+ }
483
+
484
+ if (type === "recreate") {
485
+ const destroyInstances = topologicalSort(includedIds, context, true)
486
+ .map(id => createPhaseInstance(id, context, workState))
487
+ .filter(inst => inst !== null) as OperationPhaseInstance[]
488
+
489
+ const updateInstances = topologicalSort(includedIds, context, false)
490
+ .map(id => createPhaseInstance(id, context, workState))
491
+ .filter(inst => inst !== null) as OperationPhaseInstance[]
492
+
493
+ if (destroyInstances.length > 0) {
494
+ phases.push({ type: "destroy", instances: destroyInstances })
495
+ }
496
+ if (updateInstances.length > 0) {
497
+ phases.push({ type: "update", instances: updateInstances })
498
+ }
499
+ }
500
+
501
+ return phases
502
+ }
503
+
504
+ function createPhaseInstance(
505
+ instanceId: InstanceId,
506
+ context: OperationContext,
507
+ workState?: WorkState,
508
+ ): OperationPhaseInstance | null {
509
+ const instance = context.getInstance(instanceId)
510
+ let message = "included in operation" // fallback
511
+
512
+ if (workState) {
513
+ const inclusionReason = workState.included.get(instanceId)
514
+ const requiredBy = workState.dependencyRequiredBy.get(instanceId)
515
+ const triggeringChild = workState.childTriggeringParent.get(instanceId)
516
+ const forceFlag = workState.forceFlags.get(instanceId)
517
+ const instanceState = context.getState(instanceId)
518
+
519
+ message = generateContextualMessage(
520
+ context,
521
+ instanceId,
522
+ inclusionReason,
523
+ instanceState,
524
+ requiredBy,
525
+ triggeringChild,
526
+ forceFlag,
527
+ )
528
+ }
529
+
530
+ return {
531
+ id: instanceId,
532
+ parentId: instance.parentId,
533
+ message,
534
+ }
535
+ }
536
+
537
+ function generateContextualMessage(
538
+ context: OperationContext,
539
+ instanceId: InstanceId,
540
+ inclusionReason: InclusionReason | undefined,
541
+ instanceState?: InstanceState,
542
+ requiredBy?: InstanceId,
543
+ triggeringChild?: InstanceId,
544
+ forceFlag?: "dependencies" | "children",
545
+ ): string {
546
+ function getInstanceStateType(
547
+ state?: InstanceState,
548
+ ): "failed" | "undeployed" | "changed" | "up-to-date" {
549
+ if (!state) return "undeployed"
550
+ if (state.status === "failed") return "failed"
551
+ if (state.status === "undeployed") return "undeployed"
552
+
553
+ const instance = context.getInstance(instanceId)
554
+
555
+ // composites are containers and cannot be changed/outdated
556
+ if (instance.kind === "composite") {
557
+ return "up-to-date"
558
+ }
559
+
560
+ // check if changed by using same logic as isOutdated (for units only)
561
+ const { inputHash: expectedHash } = context.inputHashResolver.requireOutput(instanceId)
562
+ if (state.inputHash !== expectedHash) return "changed"
563
+
564
+ return "up-to-date"
565
+ }
566
+
567
+ const stateType = getInstanceStateType(instanceState)
568
+
569
+ switch (inclusionReason) {
570
+ case "explicit":
571
+ return "explicitly requested"
572
+
573
+ case "dependency":
574
+ if (forceFlag === "dependencies") {
575
+ return `required by "${requiredBy}" (forced by options)`
576
+ }
577
+ switch (stateType) {
578
+ case "failed":
579
+ return `failed and required by "${requiredBy}"`
580
+ case "undeployed":
581
+ return `undeployed and required by "${requiredBy}"`
582
+ case "changed":
583
+ return `changed and required by "${requiredBy}"`
584
+ default:
585
+ return `required by "${requiredBy}"`
586
+ }
587
+
588
+ case "dependent_cascade":
589
+ return `dependent of destroyed "${requiredBy}"`
590
+
591
+ case "composite_child":
592
+ if (forceFlag === "children") {
593
+ return "child of included parent (forced by options)"
594
+ }
595
+ switch (stateType) {
596
+ case "failed":
597
+ return "failed and child of included parent"
598
+ case "undeployed":
599
+ return "undeployed and child of included parent"
600
+ case "changed":
601
+ return "changed and child of included parent"
602
+ default:
603
+ return "child of included parent"
604
+ }
605
+
606
+ case "parent_composite":
607
+ return `parent of included child "${triggeringChild}"`
608
+
609
+ case "ghost_cleanup":
610
+ return "ghost cleanup"
611
+
612
+ default:
613
+ return "included in operation"
614
+ }
615
+ }
616
+
617
+ function findGhostCleanup(
618
+ context: OperationContext,
619
+ compositesNeedingGhostCleanup: Set<InstanceId>,
620
+ ): OperationPhaseInstance[] {
621
+ const ghosts: OperationPhaseInstance[] = []
622
+
623
+ // find ghost instances and their parent composites that need cleanup
624
+ for (const instanceId of compositesNeedingGhostCleanup) {
625
+ const instance = context.getInstance(instanceId)
626
+ if (instance.kind !== "composite") continue
627
+
628
+ // add the composite itself for destroy if needed
629
+ ghosts.push({
630
+ id: instanceId,
631
+ parentId: instance.parentId,
632
+ message: "included in operation",
633
+ })
634
+
635
+ // find ghost children
636
+ const children = context.getInstanceChildren(instanceId)
637
+ for (const child of children) {
638
+ const state = context.getState(child.id)
639
+ if (isVirtualGhostInstance(state)) {
640
+ ghosts.push({
641
+ id: child.id,
642
+ parentId: child.parentId,
643
+ message: "ghost cleanup",
644
+ })
645
+ }
646
+ }
647
+ }
648
+
649
+ return ghosts
650
+ }
651
+
652
+ function isOutdated(instance: InstanceModel, context: OperationContext): boolean {
653
+ // composite instances cannot be outdated - they are containers, not deployable units
654
+ if (instance.kind === "composite") {
655
+ return false
656
+ }
657
+
658
+ const state = context.getState(instance.id)
659
+
660
+ if (state.status === "failed" || state.status === "undeployed") {
661
+ return true
662
+ }
663
+
664
+ // check if input hash has changed by comparing with expected hash from resolver
665
+ const { inputHash: expectedHash } = context.inputHashResolver.requireOutput(instance.id)
666
+ return state.inputHash !== expectedHash
667
+ }
668
+
669
+ function topologicalSort(
670
+ instanceIds: InstanceId[],
671
+ context: OperationContext,
672
+ reverse: boolean,
673
+ ): InstanceId[] {
674
+ // simple topological sort implementation
675
+ const visited = new Set<InstanceId>()
676
+ const result: InstanceId[] = []
677
+ const visiting = new Set<InstanceId>()
678
+
679
+ function visit(instanceId: InstanceId): void {
680
+ if (visiting.has(instanceId)) {
681
+ // circular dependency detected - skip for now
682
+ return
683
+ }
684
+ if (visited.has(instanceId)) {
685
+ return
686
+ }
687
+
688
+ visiting.add(instanceId)
689
+
690
+ const related = reverse
691
+ ? context.getDependentStates(instanceId).map(state => state.instanceId)
692
+ : context.getDependencies(instanceId).map(dep => dep.id)
693
+ for (const relatedId of related) {
694
+ if (instanceIds.includes(relatedId)) {
695
+ visit(relatedId)
696
+ }
697
+ }
698
+
699
+ visiting.delete(instanceId)
700
+ visited.add(instanceId)
701
+ result.push(instanceId)
702
+ }
703
+
704
+ for (const instanceId of instanceIds) {
705
+ visit(instanceId)
706
+ }
707
+
708
+ return result
709
+ }