@tagma/sdk 0.7.3 → 0.7.5

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 (230) hide show
  1. package/README.md +85 -57
  2. package/dist/approval.d.ts +2 -12
  3. package/dist/approval.d.ts.map +1 -1
  4. package/dist/approval.js +1 -90
  5. package/dist/approval.js.map +1 -1
  6. package/dist/bootstrap.d.ts +1 -1
  7. package/dist/bootstrap.d.ts.map +1 -1
  8. package/dist/completions/file-exists.js +1 -1
  9. package/dist/completions/file-exists.js.map +1 -1
  10. package/dist/completions/output-check.d.ts.map +1 -1
  11. package/dist/completions/output-check.js +17 -4
  12. package/dist/completions/output-check.js.map +1 -1
  13. package/dist/config.d.ts +4 -4
  14. package/dist/config.d.ts.map +1 -1
  15. package/dist/config.js +2 -2
  16. package/dist/config.js.map +1 -1
  17. package/dist/dataflow.d.ts +3 -0
  18. package/dist/dataflow.d.ts.map +1 -0
  19. package/dist/dataflow.js +2 -0
  20. package/dist/dataflow.js.map +1 -0
  21. package/dist/drivers/opencode.d.ts.map +1 -1
  22. package/dist/drivers/opencode.js +23 -71
  23. package/dist/drivers/opencode.js.map +1 -1
  24. package/dist/engine.d.ts +5 -56
  25. package/dist/engine.d.ts.map +1 -1
  26. package/dist/engine.js +7 -297
  27. package/dist/engine.js.map +1 -1
  28. package/dist/index.d.ts +4 -6
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +2 -4
  31. package/dist/index.js.map +1 -1
  32. package/dist/logger.d.ts +2 -60
  33. package/dist/logger.d.ts.map +1 -1
  34. package/dist/logger.js +1 -153
  35. package/dist/logger.js.map +1 -1
  36. package/dist/middlewares/static-context.d.ts.map +1 -1
  37. package/dist/middlewares/static-context.js +1 -2
  38. package/dist/middlewares/static-context.js.map +1 -1
  39. package/dist/pipeline-runner.d.ts.map +1 -1
  40. package/dist/pipeline-runner.js +2 -2
  41. package/dist/pipeline-runner.js.map +1 -1
  42. package/dist/plugins.d.ts +2 -2
  43. package/dist/plugins.d.ts.map +1 -1
  44. package/dist/plugins.js +1 -1
  45. package/dist/plugins.js.map +1 -1
  46. package/dist/runner.d.ts +1 -35
  47. package/dist/runner.d.ts.map +1 -1
  48. package/dist/runner.js +1 -610
  49. package/dist/runner.js.map +1 -1
  50. package/dist/runtime/adapters/stdin-approval.d.ts +2 -0
  51. package/dist/runtime/adapters/stdin-approval.d.ts.map +1 -0
  52. package/dist/runtime/adapters/stdin-approval.js +2 -0
  53. package/dist/runtime/adapters/stdin-approval.js.map +1 -0
  54. package/dist/runtime/adapters/websocket-approval.d.ts +2 -0
  55. package/dist/runtime/adapters/websocket-approval.d.ts.map +1 -0
  56. package/dist/runtime/adapters/websocket-approval.js +2 -0
  57. package/dist/runtime/adapters/websocket-approval.js.map +1 -0
  58. package/dist/runtime/bun-process-runner.d.ts +2 -0
  59. package/dist/runtime/bun-process-runner.d.ts.map +1 -0
  60. package/dist/runtime/bun-process-runner.js +2 -0
  61. package/dist/runtime/bun-process-runner.js.map +1 -0
  62. package/dist/runtime.d.ts +2 -8
  63. package/dist/runtime.d.ts.map +1 -1
  64. package/dist/runtime.js +1 -7
  65. package/dist/runtime.js.map +1 -1
  66. package/dist/schema.d.ts.map +1 -1
  67. package/dist/schema.js +3 -4
  68. package/dist/schema.js.map +1 -1
  69. package/dist/tagma.d.ts +3 -4
  70. package/dist/tagma.d.ts.map +1 -1
  71. package/dist/tagma.js +2 -3
  72. package/dist/tagma.js.map +1 -1
  73. package/dist/triggers/file.d.ts.map +1 -1
  74. package/dist/triggers/file.js +74 -108
  75. package/dist/triggers/file.js.map +1 -1
  76. package/dist/triggers/manual.d.ts.map +1 -1
  77. package/dist/triggers/manual.js +1 -2
  78. package/dist/triggers/manual.js.map +1 -1
  79. package/dist/types.d.ts +1 -2
  80. package/dist/types.d.ts.map +1 -1
  81. package/dist/types.js +1 -12
  82. package/dist/types.js.map +1 -1
  83. package/dist/utils-api.d.ts +1 -1
  84. package/dist/utils-api.d.ts.map +1 -1
  85. package/dist/utils-api.js +1 -1
  86. package/dist/utils-api.js.map +1 -1
  87. package/dist/validate-raw.d.ts.map +1 -1
  88. package/dist/validate-raw.js +5 -12
  89. package/dist/validate-raw.js.map +1 -1
  90. package/package.json +20 -22
  91. package/dist/adapters/stdin-approval.d.ts +0 -6
  92. package/dist/adapters/stdin-approval.d.ts.map +0 -1
  93. package/dist/adapters/stdin-approval.js +0 -90
  94. package/dist/adapters/stdin-approval.js.map +0 -1
  95. package/dist/adapters/websocket-approval.d.ts +0 -28
  96. package/dist/adapters/websocket-approval.d.ts.map +0 -1
  97. package/dist/adapters/websocket-approval.js +0 -147
  98. package/dist/adapters/websocket-approval.js.map +0 -1
  99. package/dist/core/dataflow.d.ts +0 -23
  100. package/dist/core/dataflow.d.ts.map +0 -1
  101. package/dist/core/dataflow.js +0 -99
  102. package/dist/core/dataflow.js.map +0 -1
  103. package/dist/core/log-prune.d.ts +0 -16
  104. package/dist/core/log-prune.d.ts.map +0 -1
  105. package/dist/core/log-prune.js +0 -34
  106. package/dist/core/log-prune.js.map +0 -1
  107. package/dist/core/preflight.d.ts +0 -13
  108. package/dist/core/preflight.d.ts.map +0 -1
  109. package/dist/core/preflight.js +0 -61
  110. package/dist/core/preflight.js.map +0 -1
  111. package/dist/core/run-context.d.ts +0 -55
  112. package/dist/core/run-context.d.ts.map +0 -1
  113. package/dist/core/run-context.js +0 -158
  114. package/dist/core/run-context.js.map +0 -1
  115. package/dist/core/run-state.d.ts +0 -25
  116. package/dist/core/run-state.d.ts.map +0 -1
  117. package/dist/core/run-state.js +0 -93
  118. package/dist/core/run-state.js.map +0 -1
  119. package/dist/core/scheduler.d.ts +0 -13
  120. package/dist/core/scheduler.d.ts.map +0 -1
  121. package/dist/core/scheduler.js +0 -35
  122. package/dist/core/scheduler.js.map +0 -1
  123. package/dist/core/task-executor.d.ts +0 -13
  124. package/dist/core/task-executor.d.ts.map +0 -1
  125. package/dist/core/task-executor.js +0 -601
  126. package/dist/core/task-executor.js.map +0 -1
  127. package/dist/core/trigger-errors.d.ts +0 -9
  128. package/dist/core/trigger-errors.d.ts.map +0 -1
  129. package/dist/core/trigger-errors.js +0 -15
  130. package/dist/core/trigger-errors.js.map +0 -1
  131. package/dist/dag.d.ts +0 -45
  132. package/dist/dag.d.ts.map +0 -1
  133. package/dist/dag.js +0 -177
  134. package/dist/dag.js.map +0 -1
  135. package/dist/hooks.d.ts +0 -73
  136. package/dist/hooks.d.ts.map +0 -1
  137. package/dist/hooks.js +0 -106
  138. package/dist/hooks.js.map +0 -1
  139. package/dist/pipeline-definition.d.ts +0 -3
  140. package/dist/pipeline-definition.d.ts.map +0 -1
  141. package/dist/pipeline-definition.js +0 -4
  142. package/dist/pipeline-definition.js.map +0 -1
  143. package/dist/ports.d.ts +0 -196
  144. package/dist/ports.d.ts.map +0 -1
  145. package/dist/ports.js +0 -688
  146. package/dist/ports.js.map +0 -1
  147. package/dist/prompt-doc.d.ts +0 -70
  148. package/dist/prompt-doc.d.ts.map +0 -1
  149. package/dist/prompt-doc.js +0 -154
  150. package/dist/prompt-doc.js.map +0 -1
  151. package/dist/registry.d.ts +0 -67
  152. package/dist/registry.d.ts.map +0 -1
  153. package/dist/registry.js +0 -293
  154. package/dist/registry.js.map +0 -1
  155. package/dist/task-ref.d.ts +0 -55
  156. package/dist/task-ref.d.ts.map +0 -1
  157. package/dist/task-ref.js +0 -103
  158. package/dist/task-ref.js.map +0 -1
  159. package/dist/utils.d.ts +0 -13
  160. package/dist/utils.d.ts.map +0 -1
  161. package/dist/utils.js +0 -177
  162. package/dist/utils.js.map +0 -1
  163. package/src/adapters/stdin-approval.ts +0 -106
  164. package/src/adapters/websocket-approval.ts +0 -224
  165. package/src/approval.ts +0 -131
  166. package/src/bootstrap.ts +0 -55
  167. package/src/completions/exit-code.ts +0 -34
  168. package/src/completions/file-exists.ts +0 -66
  169. package/src/completions/output-check.test.ts +0 -50
  170. package/src/completions/output-check.ts +0 -92
  171. package/src/config-ops.test.ts +0 -70
  172. package/src/config-ops.ts +0 -328
  173. package/src/config.ts +0 -26
  174. package/src/core/dataflow.test.ts +0 -166
  175. package/src/core/dataflow.ts +0 -161
  176. package/src/core/log-prune.test.ts +0 -58
  177. package/src/core/log-prune.ts +0 -43
  178. package/src/core/preflight.test.ts +0 -49
  179. package/src/core/preflight.ts +0 -89
  180. package/src/core/run-context.test.ts +0 -256
  181. package/src/core/run-context.ts +0 -211
  182. package/src/core/run-state.test.ts +0 -98
  183. package/src/core/run-state.ts +0 -122
  184. package/src/core/scheduler.test.ts +0 -83
  185. package/src/core/scheduler.ts +0 -42
  186. package/src/core/task-executor.ts +0 -743
  187. package/src/core/trigger-errors.ts +0 -15
  188. package/src/dag.test.ts +0 -56
  189. package/src/dag.ts +0 -245
  190. package/src/drivers/opencode.ts +0 -410
  191. package/src/engine-ports-mixed.test.ts +0 -156
  192. package/src/engine-ports.test.ts +0 -166
  193. package/src/engine-task-type.test.ts +0 -56
  194. package/src/engine.ts +0 -458
  195. package/src/hooks.ts +0 -193
  196. package/src/index.ts +0 -33
  197. package/src/logger.ts +0 -182
  198. package/src/middlewares/static-context.ts +0 -49
  199. package/src/pipeline-definition.ts +0 -5
  200. package/src/pipeline-runner.test.ts +0 -91
  201. package/src/pipeline-runner.ts +0 -194
  202. package/src/plugin-registry.test.ts +0 -382
  203. package/src/plugins.ts +0 -21
  204. package/src/ports.test.ts +0 -678
  205. package/src/ports.ts +0 -925
  206. package/src/prompt-doc.test.ts +0 -174
  207. package/src/prompt-doc.ts +0 -169
  208. package/src/registry.ts +0 -353
  209. package/src/runner.test.ts +0 -142
  210. package/src/runner.ts +0 -666
  211. package/src/runtime.ts +0 -20
  212. package/src/schema-ports.test.ts +0 -172
  213. package/src/schema.test.ts +0 -213
  214. package/src/schema.ts +0 -379
  215. package/src/tagma.test.ts +0 -155
  216. package/src/tagma.ts +0 -62
  217. package/src/task-ref.test.ts +0 -401
  218. package/src/task-ref.ts +0 -121
  219. package/src/triggers/file.ts +0 -164
  220. package/src/triggers/manual.ts +0 -86
  221. package/src/types.ts +0 -18
  222. package/src/utils-api.ts +0 -8
  223. package/src/utils.test.ts +0 -28
  224. package/src/utils.ts +0 -203
  225. package/src/validate-raw-plugin-types.test.ts +0 -60
  226. package/src/validate-raw-ports.test.ts +0 -136
  227. package/src/validate-raw.ts +0 -852
  228. package/src/yaml-compiler.test.ts +0 -108
  229. package/src/yaml-compiler.ts +0 -110
  230. package/src/yaml.ts +0 -11
package/src/ports.test.ts DELETED
@@ -1,678 +0,0 @@
1
- import { describe, expect, test } from 'bun:test';
2
- import {
3
- extractInputReferences,
4
- extractTaskBindingOutputs,
5
- extractTaskOutputs,
6
- inferPromptPorts,
7
- resolveTaskBindingInputs,
8
- resolveTaskInputs,
9
- substituteInputs,
10
- } from './ports';
11
- import type { Permissions, PortDef, TaskConfig } from './types';
12
-
13
- const PERMS: Permissions = { read: true, write: false, execute: false };
14
-
15
- function task(overrides: Partial<TaskConfig> & { id: string }): TaskConfig {
16
- return {
17
- name: overrides.id,
18
- permissions: PERMS,
19
- ...overrides,
20
- };
21
- }
22
-
23
- // ─── substituteInputs ────────────────────────────────────────────────
24
-
25
- describe('substituteInputs', () => {
26
- test('replaces single placeholder with string value', () => {
27
- const { text, unresolved } = substituteInputs('hello {{inputs.name}}', { name: 'world' });
28
- expect(text).toBe('hello world');
29
- expect(unresolved).toEqual([]);
30
- });
31
-
32
- test('allows optional whitespace inside braces', () => {
33
- const { text } = substituteInputs('{{ inputs.name }} / {{inputs.name}}', { name: 'x' });
34
- expect(text).toBe('x / x');
35
- });
36
-
37
- test('stringifies number / boolean values verbatim', () => {
38
- const { text } = substituteInputs(
39
- 'n={{inputs.n}} b={{inputs.b}}',
40
- { n: 42, b: true },
41
- );
42
- expect(text).toBe('n=42 b=true');
43
- });
44
-
45
- test('JSON-stringifies object values', () => {
46
- const { text } = substituteInputs('payload={{inputs.p}}', {
47
- p: { a: 1, b: 'x' },
48
- });
49
- expect(text).toBe('payload={"a":1,"b":"x"}');
50
- });
51
-
52
- test('renders unknown placeholder empty and reports it', () => {
53
- const { text, unresolved } = substituteInputs('hello {{inputs.missing}}', {});
54
- expect(text).toBe('hello ');
55
- expect(unresolved).toEqual(['missing']);
56
- });
57
-
58
- test('renders null / undefined as empty and reports', () => {
59
- const { text, unresolved } = substituteInputs('a={{inputs.a}} b={{inputs.b}}', {
60
- a: null,
61
- b: undefined,
62
- });
63
- expect(text).toBe('a= b=');
64
- expect([...unresolved].sort()).toEqual(['a', 'b']);
65
- });
66
-
67
- test('leaves malformed placeholders alone', () => {
68
- const { text } = substituteInputs('{{inputs.a.b}} {{inputs.}}', { a: 'x' });
69
- expect(text).toBe('{{inputs.a.b}} {{inputs.}}');
70
- });
71
-
72
- test('handles circular objects without throwing', () => {
73
- const obj: Record<string, unknown> = { self: null };
74
- obj.self = obj;
75
- const { text, unresolved } = substituteInputs('{{inputs.x}}', { x: obj });
76
- expect(text).toBe('');
77
- expect(unresolved).toEqual(['x']);
78
- });
79
- });
80
-
81
- describe('extractInputReferences', () => {
82
- test('returns unique referenced names', () => {
83
- const refs = extractInputReferences(
84
- 'get {{inputs.city}} for id={{inputs.id}} and {{inputs.city}} again',
85
- );
86
- expect(refs.sort()).toEqual(['city', 'id']);
87
- });
88
-
89
- test('returns empty for text without placeholders', () => {
90
- expect(extractInputReferences('no placeholders here')).toEqual([]);
91
- });
92
- });
93
-
94
- // ─── resolveTaskInputs ────────────────────────────────────────────────
95
-
96
- const cityPort: PortDef = { name: 'city', type: 'string', required: true };
97
- const idPort: PortDef = { name: 'id', type: 'number', required: true };
98
-
99
- describe('resolveTaskInputs', () => {
100
- test('no declared inputs → ready with empty map', () => {
101
- const t = task({ id: 'downstream', command: 'echo' });
102
- const res = resolveTaskInputs(t, new Map(), []);
103
- expect(res).toEqual({ kind: 'ready', inputs: {}, missingOptional: [] });
104
- });
105
-
106
- test('matches inputs by name across upstream outputs', () => {
107
- const t = task({
108
- id: 'downstream',
109
- command: 'echo',
110
- ports: { inputs: [cityPort, idPort] },
111
- });
112
- const upstream = new Map<string, Record<string, unknown>>([
113
- ['t.prompt', { city: 'Shanghai' }],
114
- ['t.other', { id: 42 }],
115
- ]);
116
- const res = resolveTaskInputs(t, upstream, ['t.prompt', 't.other']);
117
- expect(res.kind).toBe('ready');
118
- if (res.kind !== 'ready') return;
119
- expect(res.inputs).toEqual({ city: 'Shanghai', id: 42 });
120
- });
121
-
122
- test('required missing blocks with a readable reason', () => {
123
- const t = task({
124
- id: 'downstream',
125
- command: 'echo',
126
- ports: { inputs: [cityPort, idPort] },
127
- });
128
- const res = resolveTaskInputs(t, new Map(), ['t.x']);
129
- expect(res.kind).toBe('blocked');
130
- if (res.kind !== 'blocked') return;
131
- expect([...res.missingRequired].sort()).toEqual(['city', 'id']);
132
- expect(res.reason).toMatch(/city.*id|id.*city/);
133
- });
134
-
135
- test('optional missing yields ready but reports missingOptional', () => {
136
- const optional: PortDef = { name: 'note', type: 'string' };
137
- const t = task({
138
- id: 'downstream',
139
- command: 'echo',
140
- ports: { inputs: [optional] },
141
- });
142
- const res = resolveTaskInputs(t, new Map(), []);
143
- expect(res.kind).toBe('ready');
144
- if (res.kind !== 'ready') return;
145
- expect(res.inputs).toEqual({});
146
- expect(res.missingOptional).toEqual(['note']);
147
- });
148
-
149
- test('applies default for missing optional', () => {
150
- const optional: PortDef = { name: 'note', type: 'string', default: 'n/a' };
151
- const t = task({
152
- id: 'd',
153
- command: 'echo',
154
- ports: { inputs: [optional] },
155
- });
156
- const res = resolveTaskInputs(t, new Map(), []);
157
- expect(res.kind).toBe('ready');
158
- if (res.kind !== 'ready') return;
159
- expect(res.inputs).toEqual({ note: 'n/a' });
160
- });
161
-
162
- test('ambiguous multi-upstream match blocks unless disambiguated', () => {
163
- const t = task({
164
- id: 'd',
165
- command: 'echo',
166
- ports: { inputs: [cityPort] },
167
- });
168
- const upstream = new Map<string, Record<string, unknown>>([
169
- ['t.a', { city: 'Shanghai' }],
170
- ['t.b', { city: 'Beijing' }],
171
- ]);
172
- const res = resolveTaskInputs(t, upstream, ['t.a', 't.b']);
173
- expect(res.kind).toBe('blocked');
174
- if (res.kind !== 'blocked') return;
175
- expect(res.ambiguous.length).toBe(1);
176
- expect(res.ambiguous[0]!.port).toBe('city');
177
- expect([...res.ambiguous[0]!.producers].sort()).toEqual(['t.a', 't.b']);
178
- });
179
-
180
- test('explicit fully-qualified "from" wins over name-match ambiguity', () => {
181
- const explicit: PortDef = {
182
- name: 'city',
183
- type: 'string',
184
- required: true,
185
- from: 't.b.city',
186
- };
187
- const t = task({
188
- id: 'd',
189
- command: 'echo',
190
- ports: { inputs: [explicit] },
191
- });
192
- const upstream = new Map<string, Record<string, unknown>>([
193
- ['t.a', { city: 'Shanghai' }],
194
- ['t.b', { city: 'Beijing' }],
195
- ]);
196
- const res = resolveTaskInputs(t, upstream, ['t.a', 't.b']);
197
- expect(res.kind).toBe('ready');
198
- if (res.kind !== 'ready') return;
199
- expect(res.inputs).toEqual({ city: 'Beijing' });
200
- });
201
-
202
- test('coerces numeric strings to number type', () => {
203
- const t = task({
204
- id: 'd',
205
- command: 'echo',
206
- ports: { inputs: [idPort] },
207
- });
208
- const upstream = new Map<string, Record<string, unknown>>([['t.a', { id: '42' }]]);
209
- const res = resolveTaskInputs(t, upstream, ['t.a']);
210
- expect(res.kind).toBe('ready');
211
- if (res.kind !== 'ready') return;
212
- expect(res.inputs.id).toBe(42);
213
- });
214
-
215
- test('flags type-coercion failures as blocked', () => {
216
- const t = task({
217
- id: 'd',
218
- command: 'echo',
219
- ports: { inputs: [idPort] },
220
- });
221
- const upstream = new Map<string, Record<string, unknown>>([['t.a', { id: 'nope' }]]);
222
- const res = resolveTaskInputs(t, upstream, ['t.a']);
223
- expect(res.kind).toBe('blocked');
224
- if (res.kind !== 'blocked') return;
225
- expect(res.typeErrors.length).toBe(1);
226
- expect(res.typeErrors[0]!.port).toBe('id');
227
- });
228
-
229
- test('enforces enum membership', () => {
230
- const colorPort: PortDef = {
231
- name: 'color',
232
- type: 'enum',
233
- enum: ['red', 'green'],
234
- required: true,
235
- };
236
- const t = task({
237
- id: 'd',
238
- command: 'echo',
239
- ports: { inputs: [colorPort] },
240
- });
241
- const upstream = new Map<string, Record<string, unknown>>([['t.a', { color: 'blue' }]]);
242
- const res = resolveTaskInputs(t, upstream, ['t.a']);
243
- expect(res.kind).toBe('blocked');
244
- if (res.kind !== 'blocked') return;
245
- expect(res.typeErrors[0]!.port).toBe('color');
246
- });
247
- });
248
-
249
- // ─── resolveTaskBindingInputs ────────────────────────────────────────
250
-
251
- describe('resolveTaskBindingInputs', () => {
252
- test('coerces typed unified inputs from upstream outputs', () => {
253
- const t = task({
254
- id: 'downstream',
255
- command: 'echo',
256
- inputs: {
257
- id: { from: 't.up.outputs.id', type: 'number', required: true },
258
- enabled: { value: 'true', type: 'boolean' },
259
- },
260
- });
261
- const upstream = new Map([
262
- [
263
- 't.up',
264
- {
265
- outputs: { id: '42' },
266
- stdout: '',
267
- stderr: '',
268
- normalizedOutput: null,
269
- exitCode: 0,
270
- },
271
- ],
272
- ]);
273
- const res = resolveTaskBindingInputs(t, upstream, ['t.up']);
274
- expect(res.kind).toBe('ready');
275
- if (res.kind !== 'ready') return;
276
- expect(res.inputs).toEqual({ id: 42, enabled: true });
277
- });
278
-
279
- test('blocks typed unified input coercion failures', () => {
280
- const t = task({
281
- id: 'downstream',
282
- command: 'echo',
283
- inputs: {
284
- id: { from: 't.up.outputs.id', type: 'number', required: true },
285
- },
286
- });
287
- const upstream = new Map([
288
- [
289
- 't.up',
290
- {
291
- outputs: { id: 'not-a-number' },
292
- stdout: '',
293
- stderr: '',
294
- normalizedOutput: null,
295
- exitCode: 0,
296
- },
297
- ],
298
- ]);
299
- const res = resolveTaskBindingInputs(t, upstream, ['t.up']);
300
- expect(res.kind).toBe('blocked');
301
- if (res.kind !== 'blocked') return;
302
- expect(res.typeErrors).toEqual([{ input: 'id', reason: 'expected number, got string' }]);
303
- });
304
-
305
- test('resolves literal values and defaults without requiring ports', () => {
306
- const t = task({
307
- id: 'downstream',
308
- command: 'echo',
309
- inputs: {
310
- city: { value: 'Shanghai' },
311
- mode: { from: 't.up.outputs.missing', default: 'quick' },
312
- },
313
- });
314
- const res = resolveTaskBindingInputs(t, new Map(), ['t.up']);
315
- expect(res).toEqual({
316
- kind: 'ready',
317
- inputs: { city: 'Shanghai', mode: 'quick' },
318
- missingOptional: [],
319
- });
320
- });
321
-
322
- test('resolves values from a direct upstream output and stdout', () => {
323
- const t = task({
324
- id: 'downstream',
325
- command: 'echo',
326
- inputs: {
327
- city: { from: 't.up.outputs.city' },
328
- raw: { from: 't.up.stdout' },
329
- },
330
- });
331
- const upstream = new Map([
332
- [
333
- 't.up',
334
- {
335
- outputs: { city: 'Shanghai' },
336
- stdout: 'raw text\n',
337
- stderr: '',
338
- normalizedOutput: null,
339
- exitCode: 0,
340
- },
341
- ],
342
- ]);
343
- const res = resolveTaskBindingInputs(t, upstream, ['t.up']);
344
- expect(res.kind).toBe('ready');
345
- if (res.kind !== 'ready') return;
346
- expect(res.inputs).toEqual({ city: 'Shanghai', raw: 'raw text\n' });
347
- });
348
-
349
- test('blocks required missing bindings with a readable reason', () => {
350
- const t = task({
351
- id: 'downstream',
352
- command: 'echo',
353
- inputs: {
354
- city: { from: 't.up.outputs.city', required: true },
355
- },
356
- });
357
- const res = resolveTaskBindingInputs(t, new Map(), ['t.up']);
358
- expect(res.kind).toBe('blocked');
359
- if (res.kind !== 'blocked') return;
360
- expect(res.missingRequired).toEqual(['city']);
361
- expect(res.reason).toContain('missing required binding input(s): city');
362
- });
363
-
364
- test('detects ambiguous loose output name matches', () => {
365
- const t = task({
366
- id: 'downstream',
367
- command: 'echo',
368
- inputs: {
369
- val: { from: 'outputs.val', required: true },
370
- },
371
- });
372
- const upstream = new Map([
373
- ['t.a', { outputs: { val: 'a' }, stdout: '', stderr: '', normalizedOutput: null, exitCode: 0 }],
374
- ['t.b', { outputs: { val: 'b' }, stdout: '', stderr: '', normalizedOutput: null, exitCode: 0 }],
375
- ]);
376
- const res = resolveTaskBindingInputs(t, upstream, ['t.a', 't.b']);
377
- expect(res.kind).toBe('blocked');
378
- if (res.kind !== 'blocked') return;
379
- expect(res.ambiguous[0]).toEqual({ input: 'val', producers: ['t.a', 't.b'] });
380
- });
381
- });
382
-
383
- // ─── extractTaskOutputs ──────────────────────────────────────────────
384
-
385
- describe('extractTaskOutputs', () => {
386
- const outputs = [
387
- { name: 'city', type: 'string' as const },
388
- { name: 'temp', type: 'number' as const },
389
- ];
390
-
391
- test('no declared outputs → empty map, null diagnostic', () => {
392
- const r = extractTaskOutputs(undefined, 'anything', null);
393
- expect(r.outputs).toEqual({});
394
- expect(r.diagnostic).toBeNull();
395
- });
396
-
397
- test('parses last-line JSON object as source record', () => {
398
- const stdout = 'some log\nmore log\n{"city":"Shanghai","temp":23}\n';
399
- const r = extractTaskOutputs({ outputs }, stdout, null);
400
- expect(r.outputs).toEqual({ city: 'Shanghai', temp: 23 });
401
- expect(r.diagnostic).toBeNull();
402
- });
403
-
404
- test('falls back to whole-source JSON when last line is a closing brace', () => {
405
- const stdout = '{\n "city": "Shanghai",\n "temp": 23\n}\n';
406
- const r = extractTaskOutputs({ outputs }, stdout, null);
407
- expect(r.outputs).toEqual({ city: 'Shanghai', temp: 23 });
408
- });
409
-
410
- test('prefers normalizedOutput over stdout when provided', () => {
411
- const stdout = '{"city":"Wrong","temp":0}';
412
- const normalized = '{"city":"Shanghai","temp":23}';
413
- const r = extractTaskOutputs({ outputs }, stdout, normalized);
414
- expect(r.outputs).toEqual({ city: 'Shanghai', temp: 23 });
415
- });
416
-
417
- test('reports missing keys as diagnostic, keeps resolved keys', () => {
418
- const r = extractTaskOutputs({ outputs }, '{"city":"Shanghai"}', null);
419
- expect(r.outputs).toEqual({ city: 'Shanghai' });
420
- expect(r.diagnostic).toContain('missing key "temp"');
421
- });
422
-
423
- test('reports coercion failure and skips bad port', () => {
424
- const r = extractTaskOutputs(
425
- { outputs },
426
- '{"city":"Shanghai","temp":"not-a-number"}',
427
- null,
428
- );
429
- expect(r.outputs).toEqual({ city: 'Shanghai' });
430
- expect(r.diagnostic).toContain('"temp"');
431
- });
432
-
433
- test('reports diagnostic when no JSON can be parsed', () => {
434
- const r = extractTaskOutputs({ outputs }, 'plain text output\nnothing json\n', null);
435
- expect(r.outputs).toEqual({});
436
- expect(r.diagnostic).toContain('could not find a final-line JSON object');
437
- });
438
- });
439
-
440
- // ─── extractTaskBindingOutputs ───────────────────────────────────────
441
-
442
- describe('extractTaskBindingOutputs', () => {
443
- test('coerces typed unified outputs from final-line JSON', () => {
444
- const r = extractTaskBindingOutputs(
445
- {
446
- id: { type: 'number' },
447
- ok: { from: 'json.success', type: 'boolean' },
448
- },
449
- 'log\n{"id":"42","success":"true"}\n',
450
- '',
451
- null,
452
- );
453
- expect(r.outputs).toEqual({ id: 42, ok: true });
454
- expect(r.diagnostic).toBeNull();
455
- });
456
-
457
- test('diagnoses typed unified output coercion failures', () => {
458
- const r = extractTaskBindingOutputs(
459
- {
460
- id: { type: 'number' },
461
- },
462
- '{"id":"nope"}',
463
- '',
464
- null,
465
- );
466
- expect(r.outputs).toEqual({});
467
- expect(r.diagnostic).toContain('"id": expected number, got string');
468
- });
469
-
470
- test('extracts loose outputs from final-line JSON by default', () => {
471
- const r = extractTaskBindingOutputs(
472
- {
473
- city: {},
474
- temp: { from: 'json.temperature' },
475
- },
476
- 'log\n{"city":"Shanghai","temperature":23}\n',
477
- '',
478
- null,
479
- );
480
- expect(r.outputs).toEqual({ city: 'Shanghai', temp: 23 });
481
- expect(r.diagnostic).toBeNull();
482
- });
483
-
484
- test('can publish whole stdout and normalizedOutput as named outputs', () => {
485
- const r = extractTaskBindingOutputs(
486
- {
487
- raw: { from: 'stdout' },
488
- normalized: { from: 'normalizedOutput' },
489
- },
490
- 'raw text\n',
491
- '',
492
- 'normalized text',
493
- );
494
- expect(r.outputs).toEqual({ raw: 'raw text\n', normalized: 'normalized text' });
495
- });
496
-
497
- test('uses defaults for missing loose outputs without failing extraction', () => {
498
- const r = extractTaskBindingOutputs(
499
- {
500
- city: { default: 'Unknown' },
501
- },
502
- 'not json\n',
503
- '',
504
- null,
505
- );
506
- expect(r.outputs).toEqual({ city: 'Unknown' });
507
- expect(r.diagnostic).toBeNull();
508
- });
509
- });
510
-
511
- // ─── inferPromptPorts ───────────────────────────────────────────────
512
-
513
- describe('inferPromptPorts', () => {
514
- test('inputs are taken from direct-upstream Command outputs', () => {
515
- const r = inferPromptPorts({
516
- upstreams: [
517
- {
518
- taskId: 't.up',
519
- outputs: [
520
- { name: 'city', type: 'string' },
521
- { name: 'id', type: 'number' },
522
- ],
523
- },
524
- ],
525
- downstreams: [],
526
- });
527
- expect(r.inputConflicts).toEqual([]);
528
- expect(r.outputConflicts).toEqual([]);
529
- expect(r.ports.inputs).toHaveLength(2);
530
- expect(r.ports.inputs?.map((p) => p.name).sort()).toEqual(['city', 'id']);
531
- // Inferred inputs default to required: the LLM wouldn't see a real
532
- // value if the upstream failed to produce one.
533
- expect(r.ports.inputs?.every((p) => p.required === true)).toBe(true);
534
- expect(r.ports.outputs).toBeUndefined();
535
- });
536
-
537
- test('outputs are taken from direct-downstream Command inputs', () => {
538
- const r = inferPromptPorts({
539
- upstreams: [],
540
- downstreams: [
541
- {
542
- taskId: 't.down',
543
- inputs: [
544
- { name: 'greeting', type: 'string', required: true },
545
- { name: 'target', type: 'string', default: 'world' },
546
- ],
547
- },
548
- ],
549
- });
550
- expect(r.outputConflicts).toEqual([]);
551
- expect(r.ports.outputs?.map((p) => p.name).sort()).toEqual(['greeting', 'target']);
552
- // Outputs drop input-only fields (required, default, from).
553
- for (const p of r.ports.outputs ?? []) {
554
- expect(p).not.toHaveProperty('required');
555
- expect(p).not.toHaveProperty('default');
556
- expect(p).not.toHaveProperty('from');
557
- }
558
- expect(r.ports.inputs).toBeUndefined();
559
- });
560
-
561
- test('Prompt neighbors (outputs undefined) contribute nothing', () => {
562
- const r = inferPromptPorts({
563
- upstreams: [
564
- { taskId: 't.up', outputs: undefined }, // Prompt upstream
565
- ],
566
- downstreams: [
567
- { taskId: 't.down', inputs: undefined }, // Prompt downstream
568
- ],
569
- });
570
- expect(r.ports).toEqual({});
571
- expect(r.inputConflicts).toEqual([]);
572
- expect(r.outputConflicts).toEqual([]);
573
- });
574
-
575
- test('two upstreams with the same output name produce an input conflict', () => {
576
- const r = inferPromptPorts({
577
- upstreams: [
578
- { taskId: 't.a', outputs: [{ name: 'city', type: 'string' }] },
579
- { taskId: 't.b', outputs: [{ name: 'city', type: 'string' }] },
580
- ],
581
- downstreams: [],
582
- });
583
- expect(r.inputConflicts).toHaveLength(1);
584
- expect(r.inputConflicts[0]!.portName).toBe('city');
585
- expect(r.inputConflicts[0]!.producers.map((p) => p.taskId).sort()).toEqual(['t.a', 't.b']);
586
- expect(r.inputConflicts[0]!.reason).toMatch(/cannot disambiguate/);
587
- });
588
-
589
- test('two downstreams with compatible input types merge silently', () => {
590
- const r = inferPromptPorts({
591
- upstreams: [],
592
- downstreams: [
593
- {
594
- taskId: 't.d1',
595
- inputs: [{ name: 'date', type: 'string', required: true }],
596
- },
597
- {
598
- taskId: 't.d2',
599
- inputs: [{ name: 'date', type: 'string', required: false }],
600
- },
601
- ],
602
- });
603
- expect(r.outputConflicts).toEqual([]);
604
- expect(r.ports.outputs).toHaveLength(1);
605
- expect(r.ports.outputs![0]!.name).toBe('date');
606
- expect(r.ports.outputs![0]!.type).toBe('string');
607
- });
608
-
609
- test('two downstreams with incompatible input types produce an output conflict', () => {
610
- const r = inferPromptPorts({
611
- upstreams: [],
612
- downstreams: [
613
- { taskId: 't.d1', inputs: [{ name: 'date', type: 'string' }] },
614
- { taskId: 't.d2', inputs: [{ name: 'date', type: 'number' }] },
615
- ],
616
- });
617
- expect(r.outputConflicts).toHaveLength(1);
618
- expect(r.outputConflicts[0]!.portName).toBe('date');
619
- expect(r.outputConflicts[0]!.reason).toMatch(/conflicting type requirements/);
620
- });
621
-
622
- test('enum ports with differing value sets are incompatible', () => {
623
- const r = inferPromptPorts({
624
- upstreams: [],
625
- downstreams: [
626
- {
627
- taskId: 't.d1',
628
- inputs: [{ name: 'bucket', type: 'enum', enum: ['a', 'b'] }],
629
- },
630
- {
631
- taskId: 't.d2',
632
- inputs: [{ name: 'bucket', type: 'enum', enum: ['a', 'c'] }],
633
- },
634
- ],
635
- });
636
- expect(r.outputConflicts).toHaveLength(1);
637
- });
638
-
639
- test('enum ports with identical value sets merge', () => {
640
- const r = inferPromptPorts({
641
- upstreams: [],
642
- downstreams: [
643
- {
644
- taskId: 't.d1',
645
- inputs: [{ name: 'bucket', type: 'enum', enum: ['a', 'b'] }],
646
- },
647
- {
648
- taskId: 't.d2',
649
- inputs: [{ name: 'bucket', type: 'enum', enum: ['b', 'a'] }], // different order, same set
650
- },
651
- ],
652
- });
653
- expect(r.outputConflicts).toEqual([]);
654
- expect(r.ports.outputs).toHaveLength(1);
655
- });
656
-
657
- test('description and enum propagate from the first occurrence', () => {
658
- const r = inferPromptPorts({
659
- upstreams: [
660
- {
661
- taskId: 't.up',
662
- outputs: [
663
- {
664
- name: 'kind',
665
- type: 'enum',
666
- enum: ['hot', 'cold'],
667
- description: 'Weather kind',
668
- },
669
- ],
670
- },
671
- ],
672
- downstreams: [],
673
- });
674
- const port = r.ports.inputs![0]!;
675
- expect(port.description).toBe('Weather kind');
676
- expect(port.enum).toEqual(['hot', 'cold']);
677
- });
678
- });