@highstate/backend 0.19.1 → 0.21.1

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 (134) hide show
  1. package/dist/chunk-b05q6fm2.js +37 -0
  2. package/dist/{chunk-V2NILDHS.js → chunk-gxjwa93h.js} +704 -604
  3. package/dist/{chunk-X2WG3WGL.js → chunk-vzdz6chj.js} +18 -15
  4. package/dist/highstate.manifest.json +4 -4
  5. package/dist/index.js +7350 -3514
  6. package/dist/library/package-resolution-worker.js +121 -10
  7. package/dist/library/worker/main.js +31 -17
  8. package/dist/shared/index.js +254 -4
  9. package/package.json +19 -20
  10. package/prisma/backend/_schema/object.prisma +12 -0
  11. package/prisma/backend/sqlite/migrations/20260222113554_add_object_tracking/migration.sql +7 -0
  12. package/prisma/project/artifact.prisma +3 -0
  13. package/prisma/project/entity.prisma +125 -0
  14. package/prisma/project/instance.prisma +6 -0
  15. package/prisma/project/migrations/20260301210131_add_entity_tracking/migration.sql +70 -0
  16. package/prisma/project/migrations/20260302212734_add_resource_hooks_flag/migration.sql +1 -0
  17. package/prisma/project/operation.prisma +3 -0
  18. package/src/artifact/factory.ts +3 -2
  19. package/src/business/artifact.test.ts +22 -2
  20. package/src/business/artifact.ts +7 -1
  21. package/src/business/entity-snapshot.test.ts +684 -0
  22. package/src/business/entity-snapshot.ts +904 -0
  23. package/src/business/evaluation.test.ts +56 -0
  24. package/src/business/evaluation.ts +102 -22
  25. package/src/business/global-search.test.ts +344 -0
  26. package/src/business/global-search.ts +902 -0
  27. package/src/business/index.ts +4 -0
  28. package/src/business/instance-lock.ts +58 -74
  29. package/src/business/instance-state.test.ts +15 -1
  30. package/src/business/instance-state.ts +37 -14
  31. package/src/business/object-ref-index.test.ts +140 -0
  32. package/src/business/object-ref-index.ts +193 -0
  33. package/src/business/operation.test.ts +15 -1
  34. package/src/business/operation.ts +4 -0
  35. package/src/business/project-model.ts +154 -13
  36. package/src/business/project-unlock.ts +25 -2
  37. package/src/business/project.ts +9 -0
  38. package/src/business/secret.test.ts +35 -2
  39. package/src/business/secret.ts +32 -9
  40. package/src/business/settings.ts +761 -0
  41. package/src/business/unit-output.test.ts +477 -0
  42. package/src/business/unit-output.ts +461 -0
  43. package/src/business/worker.ts +55 -4
  44. package/src/database/_generated/backend/postgresql/browser.ts +6 -0
  45. package/src/database/_generated/backend/postgresql/client.ts +6 -0
  46. package/src/database/_generated/backend/postgresql/internal/class.ts +23 -5
  47. package/src/database/_generated/backend/postgresql/internal/prismaNamespace.ts +89 -5
  48. package/src/database/_generated/backend/postgresql/internal/prismaNamespaceBrowser.ts +9 -0
  49. package/src/database/_generated/backend/postgresql/models/Object.ts +1076 -0
  50. package/src/database/_generated/backend/postgresql/models.ts +1 -0
  51. package/src/database/_generated/backend/sqlite/browser.ts +6 -0
  52. package/src/database/_generated/backend/sqlite/client.ts +6 -0
  53. package/src/database/_generated/backend/sqlite/internal/class.ts +23 -5
  54. package/src/database/_generated/backend/sqlite/internal/prismaNamespace.ts +89 -5
  55. package/src/database/_generated/backend/sqlite/internal/prismaNamespaceBrowser.ts +9 -0
  56. package/src/database/_generated/backend/sqlite/models/Object.ts +1074 -0
  57. package/src/database/_generated/backend/sqlite/models.ts +1 -0
  58. package/src/database/_generated/project/browser.ts +23 -0
  59. package/src/database/_generated/project/client.ts +23 -0
  60. package/src/database/_generated/project/commonInputTypes.ts +87 -53
  61. package/src/database/_generated/project/enums.ts +8 -0
  62. package/src/database/_generated/project/internal/class.ts +53 -5
  63. package/src/database/_generated/project/internal/prismaNamespace.ts +367 -13
  64. package/src/database/_generated/project/internal/prismaNamespaceBrowser.ts +48 -1
  65. package/src/database/_generated/project/models/Artifact.ts +199 -11
  66. package/src/database/_generated/project/models/Entity.ts +1274 -0
  67. package/src/database/_generated/project/models/EntitySnapshot.ts +2389 -0
  68. package/src/database/_generated/project/models/EntitySnapshotContent.ts +1260 -0
  69. package/src/database/_generated/project/models/EntitySnapshotReference.ts +1449 -0
  70. package/src/database/_generated/project/models/InstanceState.ts +361 -1
  71. package/src/database/_generated/project/models/Operation.ts +148 -3
  72. package/src/database/_generated/project/models/OperationLog.ts +0 -4
  73. package/src/database/_generated/project/models.ts +4 -0
  74. package/src/database/migration.ts +3 -0
  75. package/src/library/find-package-json.test.ts +77 -0
  76. package/src/library/find-package-json.ts +149 -0
  77. package/src/library/package-resolution-worker.ts +7 -3
  78. package/src/library/worker/evaluator.ts +7 -1
  79. package/src/orchestrator/manager.ts +7 -0
  80. package/src/orchestrator/operation-context.captured-outputs.test.ts +118 -0
  81. package/src/orchestrator/operation-context.ts +154 -16
  82. package/src/orchestrator/operation-plan.destroy.test.md +33 -12
  83. package/src/orchestrator/operation-plan.destroy.test.ts +140 -2
  84. package/src/orchestrator/operation-plan.fixtures.ts +2 -0
  85. package/src/orchestrator/operation-plan.md +4 -1
  86. package/src/orchestrator/operation-plan.ts +286 -92
  87. package/src/orchestrator/operation-plan.update.test.md +286 -11
  88. package/src/orchestrator/operation-plan.update.test.ts +656 -5
  89. package/src/orchestrator/operation-workset.ts +72 -22
  90. package/src/orchestrator/operation.cancel.test.ts +4 -0
  91. package/src/orchestrator/operation.composite.test.ts +341 -0
  92. package/src/orchestrator/operation.destroy.test.ts +4 -0
  93. package/src/orchestrator/operation.output-validation.failure.test.ts +124 -0
  94. package/src/orchestrator/operation.preview.test.ts +4 -0
  95. package/src/orchestrator/operation.refresh.test.ts +4 -0
  96. package/src/orchestrator/operation.test-utils.ts +52 -13
  97. package/src/orchestrator/operation.ts +230 -68
  98. package/src/orchestrator/operation.update.failure.test.ts +4 -0
  99. package/src/orchestrator/operation.update.skip.test.ts +196 -0
  100. package/src/orchestrator/operation.update.test.ts +4 -0
  101. package/src/orchestrator/plan-test-builder.ts +1 -0
  102. package/src/orchestrator/unit-input-values.test.ts +450 -0
  103. package/src/orchestrator/unit-input-values.ts +281 -0
  104. package/src/pubsub/manager.ts +3 -0
  105. package/src/runner/abstractions.ts +23 -54
  106. package/src/runner/factory.ts +3 -3
  107. package/src/runner/force-abort.ts +7 -2
  108. package/src/runner/local.ts +116 -87
  109. package/src/runner/pulumi.ts +3 -5
  110. package/src/services.ts +53 -2
  111. package/src/shared/models/prisma.ts +1 -0
  112. package/src/shared/models/project/entity.ts +121 -0
  113. package/src/shared/models/project/index.ts +1 -0
  114. package/src/shared/models/project/operation.ts +61 -3
  115. package/src/shared/models/project/state.ts +10 -0
  116. package/src/shared/models/project/worker.ts +7 -0
  117. package/src/shared/resolvers/effective-output-type.test.ts +494 -0
  118. package/src/shared/resolvers/effective-output-type.ts +162 -0
  119. package/src/shared/resolvers/index.ts +1 -0
  120. package/src/shared/resolvers/input.ts +59 -9
  121. package/src/shared/utils/index.ts +1 -0
  122. package/src/shared/utils/stable-json.ts +41 -0
  123. package/src/terminal/manager.ts +6 -0
  124. package/src/terminal/run.sh.ts +9 -4
  125. package/src/worker/manager.ts +97 -1
  126. package/LICENSE +0 -21
  127. package/dist/chunk-I7BWSAN6.js +0 -49
  128. package/dist/chunk-I7BWSAN6.js.map +0 -1
  129. package/dist/chunk-V2NILDHS.js.map +0 -1
  130. package/dist/chunk-X2WG3WGL.js.map +0 -1
  131. package/dist/index.js.map +0 -1
  132. package/dist/library/package-resolution-worker.js.map +0 -1
  133. package/dist/library/worker/main.js.map +0 -1
  134. package/dist/shared/index.js.map +0 -1
@@ -0,0 +1,494 @@
1
+ import type {
2
+ ComponentModel,
3
+ EntityModel,
4
+ HubInput,
5
+ InstanceInput,
6
+ InstanceModel,
7
+ } from "@highstate/contract"
8
+ import { describe, expect, test } from "vitest"
9
+ import { type InstanceTypeContext, resolveEffectiveOutputType } from "./effective-output-type"
10
+
11
+ function createEntity(
12
+ type: EntityModel["type"],
13
+ inclusions?: EntityModel["inclusions"],
14
+ ): EntityModel {
15
+ return {
16
+ type,
17
+ inclusions,
18
+ schema: {},
19
+ meta: {
20
+ title: type,
21
+ },
22
+ definitionHash: 1,
23
+ }
24
+ }
25
+
26
+ function createInputSpec(
27
+ type: EntityModel["type"],
28
+ options?: { fromInput?: string; multiple?: boolean },
29
+ ) {
30
+ return {
31
+ type,
32
+ fromInput: options?.fromInput,
33
+ required: true,
34
+ multiple: options?.multiple ?? false,
35
+ meta: {
36
+ title: type,
37
+ },
38
+ }
39
+ }
40
+
41
+ function createComponent(options: {
42
+ type: ComponentModel["type"]
43
+ inputs?: Record<string, ReturnType<typeof createInputSpec>>
44
+ outputs?: Record<string, ReturnType<typeof createInputSpec>>
45
+ }): ComponentModel {
46
+ return {
47
+ type: options.type,
48
+ kind: "unit",
49
+ args: {},
50
+ inputs: options.inputs ?? {},
51
+ outputs: options.outputs ?? {},
52
+ meta: {
53
+ title: options.type,
54
+ defaultNamePrefix: "test",
55
+ },
56
+ definitionHash: 1,
57
+ }
58
+ }
59
+
60
+ function createInstance(options: {
61
+ id: InstanceModel["id"]
62
+ type: InstanceModel["type"]
63
+ inputs?: Record<string, InstanceInput[]>
64
+ hubInputs?: Record<string, HubInput[]>
65
+ injectionInputs?: HubInput[]
66
+ }): InstanceModel {
67
+ return {
68
+ id: options.id,
69
+ name: "instance",
70
+ type: options.type,
71
+ kind: "unit",
72
+ inputs: options.inputs,
73
+ hubInputs: options.hubInputs,
74
+ injectionInputs: options.injectionInputs,
75
+ }
76
+ }
77
+
78
+ function createResolver(contexts: Record<string, InstanceTypeContext>) {
79
+ return (instanceId: string) => contexts[instanceId]
80
+ }
81
+
82
+ describe("resolveEffectiveOutputType", () => {
83
+ test.concurrent("returns declared output type when no fromInput is used", () => {
84
+ const entities = {
85
+ "example.base.v1": createEntity("example.base.v1"),
86
+ }
87
+
88
+ const instance = createInstance({
89
+ id: "example.writer.v1:writer-1",
90
+ type: "example.writer.v1",
91
+ })
92
+
93
+ const component = createComponent({
94
+ type: "example.writer.v1",
95
+ outputs: {
96
+ value: createInputSpec("example.base.v1"),
97
+ },
98
+ })
99
+
100
+ const resolved = resolveEffectiveOutputType({
101
+ input: { instanceId: instance.id, output: "value" },
102
+ fallbackType: "example.fallback.v1",
103
+ getInstanceContext: createResolver({
104
+ [instance.id]: {
105
+ instance,
106
+ component,
107
+ entities,
108
+ },
109
+ }),
110
+ })
111
+
112
+ expect(resolved).toBe("example.base.v1")
113
+ })
114
+
115
+ test.concurrent("forwards output type from a single direct input", () => {
116
+ const entities = {
117
+ "example.base.v1": createEntity("example.base.v1"),
118
+ "example.child.v1": createEntity("example.child.v1"),
119
+ }
120
+
121
+ const source = createInstance({
122
+ id: "example.source.v1:source-1",
123
+ type: "example.source.v1",
124
+ })
125
+
126
+ const sourceComponent = createComponent({
127
+ type: "example.source.v1",
128
+ outputs: {
129
+ value: createInputSpec("example.child.v1"),
130
+ },
131
+ })
132
+
133
+ const forwarder = createInstance({
134
+ id: "example.forwarder.v1:forwarder-1",
135
+ type: "example.forwarder.v1",
136
+ inputs: {
137
+ source: [{ instanceId: source.id, output: "value" }],
138
+ },
139
+ })
140
+
141
+ const forwarderComponent = createComponent({
142
+ type: "example.forwarder.v1",
143
+ inputs: {
144
+ source: createInputSpec("example.base.v1"),
145
+ },
146
+ outputs: {
147
+ value: createInputSpec("example.base.v1", { fromInput: "source" }),
148
+ },
149
+ })
150
+
151
+ const resolved = resolveEffectiveOutputType({
152
+ input: { instanceId: forwarder.id, output: "value" },
153
+ fallbackType: "example.base.v1",
154
+ getInstanceContext: createResolver({
155
+ [source.id]: {
156
+ instance: source,
157
+ component: sourceComponent,
158
+ entities,
159
+ },
160
+ [forwarder.id]: {
161
+ instance: forwarder,
162
+ component: forwarderComponent,
163
+ entities,
164
+ },
165
+ }),
166
+ })
167
+
168
+ expect(resolved).toBe("example.child.v1")
169
+ })
170
+
171
+ test.concurrent("falls back when forwarded source input has hub inputs", () => {
172
+ const entities = {
173
+ "example.base.v1": createEntity("example.base.v1"),
174
+ "example.child.v1": createEntity("example.child.v1"),
175
+ }
176
+
177
+ const source = createInstance({
178
+ id: "example.source.v1:source-1",
179
+ type: "example.source.v1",
180
+ })
181
+
182
+ const sourceComponent = createComponent({
183
+ type: "example.source.v1",
184
+ outputs: {
185
+ value: createInputSpec("example.child.v1"),
186
+ },
187
+ })
188
+
189
+ const forwarder = createInstance({
190
+ id: "example.forwarder.v1:forwarder-1",
191
+ type: "example.forwarder.v1",
192
+ inputs: {
193
+ source: [{ instanceId: source.id, output: "value" }],
194
+ },
195
+ hubInputs: {
196
+ source: [{ hubId: "hub1" }],
197
+ },
198
+ })
199
+
200
+ const forwarderComponent = createComponent({
201
+ type: "example.forwarder.v1",
202
+ inputs: {
203
+ source: createInputSpec("example.base.v1"),
204
+ },
205
+ outputs: {
206
+ value: createInputSpec("example.base.v1", { fromInput: "source" }),
207
+ },
208
+ })
209
+
210
+ const resolved = resolveEffectiveOutputType({
211
+ input: { instanceId: forwarder.id, output: "value" },
212
+ fallbackType: "example.fallback.v1",
213
+ getInstanceContext: createResolver({
214
+ [source.id]: {
215
+ instance: source,
216
+ component: sourceComponent,
217
+ entities,
218
+ },
219
+ [forwarder.id]: {
220
+ instance: forwarder,
221
+ component: forwarderComponent,
222
+ entities,
223
+ },
224
+ }),
225
+ })
226
+
227
+ expect(resolved).toBe("example.base.v1")
228
+ })
229
+
230
+ test.concurrent("falls back when forwarded source input has multiple direct inputs", () => {
231
+ const entities = {
232
+ "example.base.v1": createEntity("example.base.v1"),
233
+ "example.child.v1": createEntity("example.child.v1"),
234
+ }
235
+
236
+ const source = createInstance({
237
+ id: "example.source.v1:source-1",
238
+ type: "example.source.v1",
239
+ })
240
+
241
+ const sourceComponent = createComponent({
242
+ type: "example.source.v1",
243
+ outputs: {
244
+ value: createInputSpec("example.child.v1"),
245
+ },
246
+ })
247
+
248
+ const forwarder = createInstance({
249
+ id: "example.forwarder.v1:forwarder-1",
250
+ type: "example.forwarder.v1",
251
+ inputs: {
252
+ source: [
253
+ { instanceId: source.id, output: "value" },
254
+ { instanceId: source.id, output: "value" },
255
+ ],
256
+ },
257
+ })
258
+
259
+ const forwarderComponent = createComponent({
260
+ type: "example.forwarder.v1",
261
+ inputs: {
262
+ source: createInputSpec("example.base.v1"),
263
+ },
264
+ outputs: {
265
+ value: createInputSpec("example.base.v1", { fromInput: "source" }),
266
+ },
267
+ })
268
+
269
+ const resolved = resolveEffectiveOutputType({
270
+ input: { instanceId: forwarder.id, output: "value" },
271
+ fallbackType: "example.fallback.v1",
272
+ getInstanceContext: createResolver({
273
+ [source.id]: {
274
+ instance: source,
275
+ component: sourceComponent,
276
+ entities,
277
+ },
278
+ [forwarder.id]: {
279
+ instance: forwarder,
280
+ component: forwarderComponent,
281
+ entities,
282
+ },
283
+ }),
284
+ })
285
+
286
+ expect(resolved).toBe("example.base.v1")
287
+ })
288
+
289
+ test.concurrent("applies path traversal to the resolved type", () => {
290
+ const entities = {
291
+ "example.base.v1": createEntity("example.base.v1", [
292
+ {
293
+ type: "example.child.v1",
294
+ field: "child",
295
+ required: true,
296
+ multiple: false,
297
+ },
298
+ ]),
299
+ "example.child.v1": createEntity("example.child.v1", [
300
+ {
301
+ type: "example.leaf.v1",
302
+ field: "leaf",
303
+ required: true,
304
+ multiple: false,
305
+ },
306
+ ]),
307
+ "example.leaf.v1": createEntity("example.leaf.v1"),
308
+ }
309
+
310
+ const instance = createInstance({
311
+ id: "example.writer.v1:writer-1",
312
+ type: "example.writer.v1",
313
+ })
314
+
315
+ const component = createComponent({
316
+ type: "example.writer.v1",
317
+ outputs: {
318
+ value: createInputSpec("example.base.v1"),
319
+ },
320
+ })
321
+
322
+ const resolved = resolveEffectiveOutputType({
323
+ input: {
324
+ instanceId: instance.id,
325
+ output: "value",
326
+ path: "child.leaf",
327
+ },
328
+ fallbackType: "example.fallback.v1",
329
+ getInstanceContext: createResolver({
330
+ [instance.id]: {
331
+ instance,
332
+ component,
333
+ entities,
334
+ },
335
+ }),
336
+ })
337
+
338
+ expect(resolved).toBe("example.leaf.v1")
339
+ })
340
+
341
+ test.concurrent("returns root type when path is invalid", () => {
342
+ const entities = {
343
+ "example.base.v1": createEntity("example.base.v1", [
344
+ {
345
+ type: "example.child.v1",
346
+ field: "child",
347
+ required: true,
348
+ multiple: false,
349
+ },
350
+ ]),
351
+ "example.child.v1": createEntity("example.child.v1"),
352
+ }
353
+
354
+ const instance = createInstance({
355
+ id: "example.writer.v1:writer-1",
356
+ type: "example.writer.v1",
357
+ })
358
+
359
+ const component = createComponent({
360
+ type: "example.writer.v1",
361
+ outputs: {
362
+ value: createInputSpec("example.base.v1"),
363
+ },
364
+ })
365
+
366
+ const resolved = resolveEffectiveOutputType({
367
+ input: {
368
+ instanceId: instance.id,
369
+ output: "value",
370
+ path: "missing.segment",
371
+ },
372
+ fallbackType: "example.fallback.v1",
373
+ getInstanceContext: createResolver({
374
+ [instance.id]: {
375
+ instance,
376
+ component,
377
+ entities,
378
+ },
379
+ }),
380
+ })
381
+
382
+ expect(resolved).toBe("example.base.v1")
383
+ })
384
+
385
+ test.concurrent(
386
+ "uses direct inclusion when no path is provided and fallback matches inclusion type",
387
+ () => {
388
+ const entities = {
389
+ "example.identity.v1": createEntity("example.identity.v1", [
390
+ {
391
+ type: "example.peer.v1",
392
+ field: "peer",
393
+ required: true,
394
+ multiple: false,
395
+ },
396
+ ]),
397
+ "example.peer.v1": createEntity("example.peer.v1"),
398
+ }
399
+
400
+ const identity = createInstance({
401
+ id: "example.identity.v1:identity-1",
402
+ type: "example.identity.v1",
403
+ })
404
+
405
+ const component = createComponent({
406
+ type: "example.identity.v1",
407
+ outputs: {
408
+ identity: createInputSpec("example.identity.v1"),
409
+ },
410
+ })
411
+
412
+ const resolved = resolveEffectiveOutputType({
413
+ input: {
414
+ instanceId: identity.id,
415
+ output: "identity",
416
+ },
417
+ fallbackType: "example.peer.v1",
418
+ getInstanceContext: createResolver({
419
+ [identity.id]: {
420
+ instance: identity,
421
+ component,
422
+ entities,
423
+ },
424
+ }),
425
+ })
426
+
427
+ expect(resolved).toBe("example.peer.v1")
428
+ },
429
+ )
430
+
431
+ test.concurrent("handles forwarding cycles without recursion errors", () => {
432
+ const entities = {
433
+ "example.base.v1": createEntity("example.base.v1"),
434
+ }
435
+
436
+ const instanceA = createInstance({
437
+ id: "example.a.v1:a-1",
438
+ type: "example.a.v1",
439
+ inputs: {
440
+ source: [{ instanceId: "example.b.v1:b-1", output: "value" }],
441
+ },
442
+ })
443
+
444
+ const instanceB = createInstance({
445
+ id: "example.b.v1:b-1",
446
+ type: "example.b.v1",
447
+ inputs: {
448
+ source: [{ instanceId: "example.a.v1:a-1", output: "value" }],
449
+ },
450
+ })
451
+
452
+ const componentA = createComponent({
453
+ type: "example.a.v1",
454
+ inputs: {
455
+ source: createInputSpec("example.base.v1"),
456
+ },
457
+ outputs: {
458
+ value: createInputSpec("example.base.v1", { fromInput: "source" }),
459
+ },
460
+ })
461
+
462
+ const componentB = createComponent({
463
+ type: "example.b.v1",
464
+ inputs: {
465
+ source: createInputSpec("example.base.v1"),
466
+ },
467
+ outputs: {
468
+ value: createInputSpec("example.base.v1", { fromInput: "source" }),
469
+ },
470
+ })
471
+
472
+ const resolved = resolveEffectiveOutputType({
473
+ input: {
474
+ instanceId: instanceA.id,
475
+ output: "value",
476
+ },
477
+ fallbackType: "example.fallback.v1",
478
+ getInstanceContext: createResolver({
479
+ [instanceA.id]: {
480
+ instance: instanceA,
481
+ component: componentA,
482
+ entities,
483
+ },
484
+ [instanceB.id]: {
485
+ instance: instanceB,
486
+ component: componentB,
487
+ entities,
488
+ },
489
+ }),
490
+ })
491
+
492
+ expect(resolved).toBe("example.base.v1")
493
+ })
494
+ })
@@ -0,0 +1,162 @@
1
+ import type { ComponentModel, EntityModel, InstanceInput, InstanceModel } from "@highstate/contract"
2
+
3
+ export type InstanceTypeContext = {
4
+ instance: InstanceModel
5
+ component: ComponentModel
6
+ entities: Readonly<Record<string, EntityModel>>
7
+ }
8
+
9
+ export type ResolveEffectiveOutputTypeOptions = {
10
+ input: InstanceInput
11
+ fallbackType: string
12
+ getInstanceContext: (instanceId: string) => InstanceTypeContext | undefined
13
+ visited?: Set<string>
14
+ }
15
+
16
+ /**
17
+ * Resolves the effective entity type of a connected output.
18
+ *
19
+ * The function starts from the connected output declaration and then applies
20
+ * two additional transformations used by the platform:
21
+ *
22
+ * 1. `fromInput` forwarding.
23
+ * If the output references `fromInput`, it follows that input edge and reuses
24
+ * the upstream effective type when the forwarding source is deterministic.
25
+ *
26
+ * 2. Input `path` traversal.
27
+ * If the edge provides a `path`, the type is narrowed by following entity
28
+ * inclusions segment-by-segment.
29
+ *
30
+ * The function is intentionally conservative.
31
+ * If a forwarding source is ambiguous (missing, multiple, hub/injection-backed,
32
+ * unknown node/entity, or cyclic), it falls back to the declared type.
33
+ *
34
+ * @param options.input The connected instance output reference being consumed.
35
+ * @param options.fallbackType The type to use when resolution cannot continue safely.
36
+ * @param options.getInstanceContext A lookup that returns instance/component/entities for an instance ID.
37
+ * @param options.visited A recursion guard set used to prevent forwarding cycles.
38
+ * @returns The effective resolved output entity type.
39
+ */
40
+ export function resolveEffectiveOutputType(options: ResolveEffectiveOutputTypeOptions): string {
41
+ const visited = options.visited ?? new Set<string>()
42
+
43
+ const producer = options.getInstanceContext(options.input.instanceId)
44
+ if (!producer) {
45
+ return resolveTypeByPathOrFallbackInclusion({
46
+ rootType: options.fallbackType,
47
+ path: options.input.path,
48
+ entities: undefined,
49
+ fallbackType: options.fallbackType,
50
+ })
51
+ }
52
+
53
+ const outputSpec = producer.component.outputs[options.input.output]
54
+ if (!outputSpec) {
55
+ return resolveTypeByPathOrFallbackInclusion({
56
+ rootType: options.fallbackType,
57
+ path: options.input.path,
58
+ entities: producer.entities,
59
+ fallbackType: options.fallbackType,
60
+ })
61
+ }
62
+
63
+ let effectiveType: string = outputSpec.type
64
+
65
+ if (outputSpec.fromInput) {
66
+ const visitedKey = `${producer.instance.id}:${options.input.output}`
67
+ if (!visited.has(visitedKey)) {
68
+ visited.add(visitedKey)
69
+
70
+ const forwardedInputName = outputSpec.fromInput
71
+ const forwardedFallback = producer.component.inputs[forwardedInputName]?.type ?? effectiveType
72
+ const hasHubInputs = (producer.instance.hubInputs?.[forwardedInputName]?.length ?? 0) > 0
73
+ const hasInjectionInputs = (producer.instance.injectionInputs?.length ?? 0) > 0
74
+ const directInputs = producer.instance.inputs?.[forwardedInputName] ?? []
75
+
76
+ if (!hasHubInputs && !hasInjectionInputs && directInputs.length === 1) {
77
+ const forwardedInput = directInputs[0]
78
+ if (forwardedInput) {
79
+ effectiveType = resolveEffectiveOutputType({
80
+ input: forwardedInput,
81
+ fallbackType: forwardedFallback,
82
+ getInstanceContext: options.getInstanceContext,
83
+ visited,
84
+ })
85
+ } else {
86
+ effectiveType = forwardedFallback
87
+ }
88
+ } else {
89
+ effectiveType = forwardedFallback
90
+ }
91
+ }
92
+ }
93
+
94
+ return resolveTypeByPathOrFallbackInclusion({
95
+ rootType: effectiveType,
96
+ path: options.input.path,
97
+ entities: producer.entities,
98
+ fallbackType: options.fallbackType,
99
+ })
100
+ }
101
+
102
+ function resolveTypeByPathOrFallbackInclusion(options: {
103
+ rootType: string
104
+ path: string | undefined
105
+ entities: Readonly<Record<string, EntityModel>> | undefined
106
+ fallbackType: string
107
+ }): string {
108
+ const { rootType, path, entities, fallbackType } = options
109
+
110
+ if (!path || !entities) {
111
+ return resolveTypeByImplicitInclusion(rootType, fallbackType, entities)
112
+ }
113
+
114
+ let currentType = rootType
115
+ const segments = path.split(".")
116
+
117
+ for (const segment of segments) {
118
+ const entity = entities[currentType]
119
+ if (!entity) {
120
+ return rootType
121
+ }
122
+
123
+ const inclusion = entity.inclusions?.find(inc => inc.field === segment)
124
+ if (!inclusion) {
125
+ return rootType
126
+ }
127
+
128
+ currentType = inclusion.type
129
+ }
130
+
131
+ return currentType
132
+ }
133
+
134
+ function resolveTypeByImplicitInclusion(
135
+ rootType: string,
136
+ fallbackType: string,
137
+ entities: Readonly<Record<string, EntityModel>> | undefined,
138
+ ): string {
139
+ if (!entities) {
140
+ return rootType
141
+ }
142
+
143
+ if (rootType === fallbackType) {
144
+ return rootType
145
+ }
146
+
147
+ const rootEntity = entities[rootType]
148
+ if (!rootEntity) {
149
+ return rootType
150
+ }
151
+
152
+ if (rootEntity.extensions?.includes(fallbackType)) {
153
+ return rootType
154
+ }
155
+
156
+ const inclusion = rootEntity.inclusions?.find(inc => inc.type === fallbackType)
157
+ if (inclusion) {
158
+ return fallbackType
159
+ }
160
+
161
+ return rootType
162
+ }
@@ -1,3 +1,4 @@
1
+ export * from "./effective-output-type"
1
2
  export * from "./graph-resolver"
2
3
  export * from "./input"
3
4
  export * from "./input-hash"