@tagma/sdk 0.6.3 → 0.6.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 (73) hide show
  1. package/README.md +8 -5
  2. package/dist/dag.test.d.ts +2 -0
  3. package/dist/dag.test.d.ts.map +1 -0
  4. package/dist/dag.test.js +42 -0
  5. package/dist/dag.test.js.map +1 -0
  6. package/dist/engine-ports.test.d.ts +2 -0
  7. package/dist/engine-ports.test.d.ts.map +1 -0
  8. package/dist/engine-ports.test.js +378 -0
  9. package/dist/engine-ports.test.js.map +1 -0
  10. package/dist/engine.d.ts.map +1 -1
  11. package/dist/engine.js +194 -21
  12. package/dist/engine.js.map +1 -1
  13. package/dist/pipeline-runner.d.ts.map +1 -1
  14. package/dist/pipeline-runner.js +3 -0
  15. package/dist/pipeline-runner.js.map +1 -1
  16. package/dist/ports.d.ts +118 -0
  17. package/dist/ports.d.ts.map +1 -0
  18. package/dist/ports.js +365 -0
  19. package/dist/ports.js.map +1 -0
  20. package/dist/ports.test.d.ts +2 -0
  21. package/dist/ports.test.d.ts.map +1 -0
  22. package/dist/ports.test.js +262 -0
  23. package/dist/ports.test.js.map +1 -0
  24. package/dist/prompt-doc.d.ts +35 -1
  25. package/dist/prompt-doc.d.ts.map +1 -1
  26. package/dist/prompt-doc.js +110 -0
  27. package/dist/prompt-doc.js.map +1 -1
  28. package/dist/prompt-doc.test.d.ts +2 -0
  29. package/dist/prompt-doc.test.d.ts.map +1 -0
  30. package/dist/prompt-doc.test.js +145 -0
  31. package/dist/prompt-doc.test.js.map +1 -0
  32. package/dist/runner.d.ts +17 -0
  33. package/dist/runner.d.ts.map +1 -1
  34. package/dist/runner.js +171 -8
  35. package/dist/runner.js.map +1 -1
  36. package/dist/runner.test.d.ts +2 -0
  37. package/dist/runner.test.d.ts.map +1 -0
  38. package/dist/runner.test.js +119 -0
  39. package/dist/runner.test.js.map +1 -0
  40. package/dist/schema-ports.test.d.ts +2 -0
  41. package/dist/schema-ports.test.d.ts.map +1 -0
  42. package/dist/schema-ports.test.js +219 -0
  43. package/dist/schema-ports.test.js.map +1 -0
  44. package/dist/schema.d.ts.map +1 -1
  45. package/dist/schema.js +8 -0
  46. package/dist/schema.js.map +1 -1
  47. package/dist/sdk.d.ts +3 -1
  48. package/dist/sdk.d.ts.map +1 -1
  49. package/dist/sdk.js +5 -1
  50. package/dist/sdk.js.map +1 -1
  51. package/dist/validate-raw-ports.test.d.ts +2 -0
  52. package/dist/validate-raw-ports.test.d.ts.map +1 -0
  53. package/dist/validate-raw-ports.test.js +157 -0
  54. package/dist/validate-raw-ports.test.js.map +1 -0
  55. package/dist/validate-raw.d.ts.map +1 -1
  56. package/dist/validate-raw.js +141 -0
  57. package/dist/validate-raw.js.map +1 -1
  58. package/package.json +2 -7
  59. package/src/dag.test.ts +56 -0
  60. package/src/engine-ports.test.ts +404 -0
  61. package/src/engine.ts +231 -24
  62. package/src/pipeline-runner.ts +3 -0
  63. package/src/ports.test.ts +301 -0
  64. package/src/ports.ts +442 -0
  65. package/src/prompt-doc.test.ts +174 -0
  66. package/src/prompt-doc.ts +121 -1
  67. package/src/runner.test.ts +142 -0
  68. package/src/runner.ts +198 -8
  69. package/src/schema-ports.test.ts +236 -0
  70. package/src/schema.ts +8 -0
  71. package/src/sdk.ts +14 -0
  72. package/src/validate-raw-ports.test.ts +198 -0
  73. package/src/validate-raw.ts +155 -1
@@ -0,0 +1,301 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+ import {
3
+ extractInputReferences,
4
+ extractTaskOutputs,
5
+ resolveTaskInputs,
6
+ substituteInputs,
7
+ } from './ports';
8
+ import type { Permissions, PortDef, TaskConfig } from './types';
9
+
10
+ const PERMS: Permissions = { read: true, write: false, execute: false };
11
+
12
+ function task(overrides: Partial<TaskConfig> & { id: string }): TaskConfig {
13
+ return {
14
+ name: overrides.id,
15
+ permissions: PERMS,
16
+ ...overrides,
17
+ };
18
+ }
19
+
20
+ // ─── substituteInputs ────────────────────────────────────────────────
21
+
22
+ describe('substituteInputs', () => {
23
+ test('replaces single placeholder with string value', () => {
24
+ const { text, unresolved } = substituteInputs('hello {{inputs.name}}', { name: 'world' });
25
+ expect(text).toBe('hello world');
26
+ expect(unresolved).toEqual([]);
27
+ });
28
+
29
+ test('allows optional whitespace inside braces', () => {
30
+ const { text } = substituteInputs('{{ inputs.name }} / {{inputs.name}}', { name: 'x' });
31
+ expect(text).toBe('x / x');
32
+ });
33
+
34
+ test('stringifies number / boolean values verbatim', () => {
35
+ const { text } = substituteInputs(
36
+ 'n={{inputs.n}} b={{inputs.b}}',
37
+ { n: 42, b: true },
38
+ );
39
+ expect(text).toBe('n=42 b=true');
40
+ });
41
+
42
+ test('JSON-stringifies object values', () => {
43
+ const { text } = substituteInputs('payload={{inputs.p}}', {
44
+ p: { a: 1, b: 'x' },
45
+ });
46
+ expect(text).toBe('payload={"a":1,"b":"x"}');
47
+ });
48
+
49
+ test('renders unknown placeholder empty and reports it', () => {
50
+ const { text, unresolved } = substituteInputs('hello {{inputs.missing}}', {});
51
+ expect(text).toBe('hello ');
52
+ expect(unresolved).toEqual(['missing']);
53
+ });
54
+
55
+ test('renders null / undefined as empty and reports', () => {
56
+ const { text, unresolved } = substituteInputs('a={{inputs.a}} b={{inputs.b}}', {
57
+ a: null,
58
+ b: undefined,
59
+ });
60
+ expect(text).toBe('a= b=');
61
+ expect([...unresolved].sort()).toEqual(['a', 'b']);
62
+ });
63
+
64
+ test('leaves malformed placeholders alone', () => {
65
+ const { text } = substituteInputs('{{inputs.a.b}} {{inputs.}}', { a: 'x' });
66
+ expect(text).toBe('{{inputs.a.b}} {{inputs.}}');
67
+ });
68
+
69
+ test('handles circular objects without throwing', () => {
70
+ const obj: Record<string, unknown> = { self: null };
71
+ obj.self = obj;
72
+ const { text, unresolved } = substituteInputs('{{inputs.x}}', { x: obj });
73
+ expect(text).toBe('');
74
+ expect(unresolved).toEqual(['x']);
75
+ });
76
+ });
77
+
78
+ describe('extractInputReferences', () => {
79
+ test('returns unique referenced names', () => {
80
+ const refs = extractInputReferences(
81
+ 'get {{inputs.city}} for id={{inputs.id}} and {{inputs.city}} again',
82
+ );
83
+ expect(refs.sort()).toEqual(['city', 'id']);
84
+ });
85
+
86
+ test('returns empty for text without placeholders', () => {
87
+ expect(extractInputReferences('no placeholders here')).toEqual([]);
88
+ });
89
+ });
90
+
91
+ // ─── resolveTaskInputs ────────────────────────────────────────────────
92
+
93
+ const cityPort: PortDef = { name: 'city', type: 'string', required: true };
94
+ const idPort: PortDef = { name: 'id', type: 'number', required: true };
95
+
96
+ describe('resolveTaskInputs', () => {
97
+ test('no declared inputs → ready with empty map', () => {
98
+ const t = task({ id: 'downstream', command: 'echo' });
99
+ const res = resolveTaskInputs(t, new Map(), []);
100
+ expect(res).toEqual({ kind: 'ready', inputs: {}, missingOptional: [] });
101
+ });
102
+
103
+ test('matches inputs by name across upstream outputs', () => {
104
+ const t = task({
105
+ id: 'downstream',
106
+ command: 'echo',
107
+ ports: { inputs: [cityPort, idPort] },
108
+ });
109
+ const upstream = new Map<string, Record<string, unknown>>([
110
+ ['t.prompt', { city: 'Shanghai' }],
111
+ ['t.other', { id: 42 }],
112
+ ]);
113
+ const res = resolveTaskInputs(t, upstream, ['t.prompt', 't.other']);
114
+ expect(res.kind).toBe('ready');
115
+ if (res.kind !== 'ready') return;
116
+ expect(res.inputs).toEqual({ city: 'Shanghai', id: 42 });
117
+ });
118
+
119
+ test('required missing blocks with a readable reason', () => {
120
+ const t = task({
121
+ id: 'downstream',
122
+ command: 'echo',
123
+ ports: { inputs: [cityPort, idPort] },
124
+ });
125
+ const res = resolveTaskInputs(t, new Map(), ['t.x']);
126
+ expect(res.kind).toBe('blocked');
127
+ if (res.kind !== 'blocked') return;
128
+ expect([...res.missingRequired].sort()).toEqual(['city', 'id']);
129
+ expect(res.reason).toMatch(/city.*id|id.*city/);
130
+ });
131
+
132
+ test('optional missing yields ready but reports missingOptional', () => {
133
+ const optional: PortDef = { name: 'note', type: 'string' };
134
+ const t = task({
135
+ id: 'downstream',
136
+ command: 'echo',
137
+ ports: { inputs: [optional] },
138
+ });
139
+ const res = resolveTaskInputs(t, new Map(), []);
140
+ expect(res.kind).toBe('ready');
141
+ if (res.kind !== 'ready') return;
142
+ expect(res.inputs).toEqual({});
143
+ expect(res.missingOptional).toEqual(['note']);
144
+ });
145
+
146
+ test('applies default for missing optional', () => {
147
+ const optional: PortDef = { name: 'note', type: 'string', default: 'n/a' };
148
+ const t = task({
149
+ id: 'd',
150
+ command: 'echo',
151
+ ports: { inputs: [optional] },
152
+ });
153
+ const res = resolveTaskInputs(t, new Map(), []);
154
+ expect(res.kind).toBe('ready');
155
+ if (res.kind !== 'ready') return;
156
+ expect(res.inputs).toEqual({ note: 'n/a' });
157
+ });
158
+
159
+ test('ambiguous multi-upstream match blocks unless disambiguated', () => {
160
+ const t = task({
161
+ id: 'd',
162
+ command: 'echo',
163
+ ports: { inputs: [cityPort] },
164
+ });
165
+ const upstream = new Map<string, Record<string, unknown>>([
166
+ ['t.a', { city: 'Shanghai' }],
167
+ ['t.b', { city: 'Beijing' }],
168
+ ]);
169
+ const res = resolveTaskInputs(t, upstream, ['t.a', 't.b']);
170
+ expect(res.kind).toBe('blocked');
171
+ if (res.kind !== 'blocked') return;
172
+ expect(res.ambiguous.length).toBe(1);
173
+ expect(res.ambiguous[0]!.port).toBe('city');
174
+ expect([...res.ambiguous[0]!.producers].sort()).toEqual(['t.a', 't.b']);
175
+ });
176
+
177
+ test('explicit fully-qualified "from" wins over name-match ambiguity', () => {
178
+ const explicit: PortDef = {
179
+ name: 'city',
180
+ type: 'string',
181
+ required: true,
182
+ from: 't.b.city',
183
+ };
184
+ const t = task({
185
+ id: 'd',
186
+ command: 'echo',
187
+ ports: { inputs: [explicit] },
188
+ });
189
+ const upstream = new Map<string, Record<string, unknown>>([
190
+ ['t.a', { city: 'Shanghai' }],
191
+ ['t.b', { city: 'Beijing' }],
192
+ ]);
193
+ const res = resolveTaskInputs(t, upstream, ['t.a', 't.b']);
194
+ expect(res.kind).toBe('ready');
195
+ if (res.kind !== 'ready') return;
196
+ expect(res.inputs).toEqual({ city: 'Beijing' });
197
+ });
198
+
199
+ test('coerces numeric strings to number type', () => {
200
+ const t = task({
201
+ id: 'd',
202
+ command: 'echo',
203
+ ports: { inputs: [idPort] },
204
+ });
205
+ const upstream = new Map<string, Record<string, unknown>>([['t.a', { id: '42' }]]);
206
+ const res = resolveTaskInputs(t, upstream, ['t.a']);
207
+ expect(res.kind).toBe('ready');
208
+ if (res.kind !== 'ready') return;
209
+ expect(res.inputs.id).toBe(42);
210
+ });
211
+
212
+ test('flags type-coercion failures as blocked', () => {
213
+ const t = task({
214
+ id: 'd',
215
+ command: 'echo',
216
+ ports: { inputs: [idPort] },
217
+ });
218
+ const upstream = new Map<string, Record<string, unknown>>([['t.a', { id: 'nope' }]]);
219
+ const res = resolveTaskInputs(t, upstream, ['t.a']);
220
+ expect(res.kind).toBe('blocked');
221
+ if (res.kind !== 'blocked') return;
222
+ expect(res.typeErrors.length).toBe(1);
223
+ expect(res.typeErrors[0]!.port).toBe('id');
224
+ });
225
+
226
+ test('enforces enum membership', () => {
227
+ const colorPort: PortDef = {
228
+ name: 'color',
229
+ type: 'enum',
230
+ enum: ['red', 'green'],
231
+ required: true,
232
+ };
233
+ const t = task({
234
+ id: 'd',
235
+ command: 'echo',
236
+ ports: { inputs: [colorPort] },
237
+ });
238
+ const upstream = new Map<string, Record<string, unknown>>([['t.a', { color: 'blue' }]]);
239
+ const res = resolveTaskInputs(t, upstream, ['t.a']);
240
+ expect(res.kind).toBe('blocked');
241
+ if (res.kind !== 'blocked') return;
242
+ expect(res.typeErrors[0]!.port).toBe('color');
243
+ });
244
+ });
245
+
246
+ // ─── extractTaskOutputs ──────────────────────────────────────────────
247
+
248
+ describe('extractTaskOutputs', () => {
249
+ const outputs = [
250
+ { name: 'city', type: 'string' as const },
251
+ { name: 'temp', type: 'number' as const },
252
+ ];
253
+
254
+ test('no declared outputs → empty map, null diagnostic', () => {
255
+ const r = extractTaskOutputs(undefined, 'anything', null);
256
+ expect(r.outputs).toEqual({});
257
+ expect(r.diagnostic).toBeNull();
258
+ });
259
+
260
+ test('parses last-line JSON object as source record', () => {
261
+ const stdout = 'some log\nmore log\n{"city":"Shanghai","temp":23}\n';
262
+ const r = extractTaskOutputs({ outputs }, stdout, null);
263
+ expect(r.outputs).toEqual({ city: 'Shanghai', temp: 23 });
264
+ expect(r.diagnostic).toBeNull();
265
+ });
266
+
267
+ test('falls back to whole-source JSON when last line is a closing brace', () => {
268
+ const stdout = '{\n "city": "Shanghai",\n "temp": 23\n}\n';
269
+ const r = extractTaskOutputs({ outputs }, stdout, null);
270
+ expect(r.outputs).toEqual({ city: 'Shanghai', temp: 23 });
271
+ });
272
+
273
+ test('prefers normalizedOutput over stdout when provided', () => {
274
+ const stdout = '{"city":"Wrong","temp":0}';
275
+ const normalized = '{"city":"Shanghai","temp":23}';
276
+ const r = extractTaskOutputs({ outputs }, stdout, normalized);
277
+ expect(r.outputs).toEqual({ city: 'Shanghai', temp: 23 });
278
+ });
279
+
280
+ test('reports missing keys as diagnostic, keeps resolved keys', () => {
281
+ const r = extractTaskOutputs({ outputs }, '{"city":"Shanghai"}', null);
282
+ expect(r.outputs).toEqual({ city: 'Shanghai' });
283
+ expect(r.diagnostic).toContain('missing key "temp"');
284
+ });
285
+
286
+ test('reports coercion failure and skips bad port', () => {
287
+ const r = extractTaskOutputs(
288
+ { outputs },
289
+ '{"city":"Shanghai","temp":"not-a-number"}',
290
+ null,
291
+ );
292
+ expect(r.outputs).toEqual({ city: 'Shanghai' });
293
+ expect(r.diagnostic).toContain('"temp"');
294
+ });
295
+
296
+ test('reports diagnostic when no JSON can be parsed', () => {
297
+ const r = extractTaskOutputs({ outputs }, 'plain text output\nnothing json\n', null);
298
+ expect(r.outputs).toEqual({});
299
+ expect(r.diagnostic).toContain('could not find a final-line JSON object');
300
+ });
301
+ });