@tagma/sdk 0.6.11 → 0.6.12
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.
- package/README.md +35 -3
- package/dist/config-ops.d.ts +4 -2
- package/dist/config-ops.d.ts.map +1 -1
- package/dist/config-ops.js +16 -2
- package/dist/config-ops.js.map +1 -1
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +75 -27
- package/dist/engine.js.map +1 -1
- package/dist/ports.d.ts +23 -1
- package/dist/ports.d.ts.map +1 -1
- package/dist/ports.js +160 -0
- package/dist/ports.js.map +1 -1
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +7 -3
- package/dist/schema.js.map +1 -1
- package/dist/sdk.d.ts +2 -2
- package/dist/sdk.d.ts.map +1 -1
- package/dist/sdk.js +1 -1
- package/dist/sdk.js.map +1 -1
- package/dist/validate-raw.js +118 -0
- package/dist/validate-raw.js.map +1 -1
- package/package.json +2 -2
- package/src/config-ops.ts +12 -2
- package/src/engine-ports.test.ts +66 -0
- package/src/engine-task-type.test.ts +56 -0
- package/src/engine.ts +100 -26
- package/src/ports.test.ts +127 -0
- package/src/ports.ts +224 -1
- package/src/schema-ports.test.ts +86 -0
- package/src/schema.ts +7 -3
- package/src/sdk.ts +4 -0
- package/src/validate-raw-ports.test.ts +66 -0
- package/src/validate-raw.ts +137 -0
package/src/schema-ports.test.ts
CHANGED
|
@@ -13,6 +13,34 @@ const WORK_DIR = process.platform === 'win32' ? 'D:\\fake-work' : '/fake-work';
|
|
|
13
13
|
// ─── resolveConfig preserves ports ───────────────────────────────────
|
|
14
14
|
|
|
15
15
|
describe('resolveConfig — ports passthrough', () => {
|
|
16
|
+
test('raw lightweight bindings survive onto the resolved task', () => {
|
|
17
|
+
const raw: RawPipelineConfig = {
|
|
18
|
+
name: 'p',
|
|
19
|
+
tracks: [
|
|
20
|
+
{
|
|
21
|
+
id: 't',
|
|
22
|
+
name: 'T',
|
|
23
|
+
tasks: [
|
|
24
|
+
{
|
|
25
|
+
id: 'a',
|
|
26
|
+
command: 'echo "{{inputs.city}}"',
|
|
27
|
+
inputs: {
|
|
28
|
+
city: { from: 't.plan.outputs.city', required: true },
|
|
29
|
+
},
|
|
30
|
+
outputs: {
|
|
31
|
+
report: { from: 'json.reportPath' },
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
],
|
|
35
|
+
},
|
|
36
|
+
],
|
|
37
|
+
};
|
|
38
|
+
const resolved = resolveConfig(raw, WORK_DIR);
|
|
39
|
+
const task = resolved.tracks[0]!.tasks[0]!;
|
|
40
|
+
expect(task.inputs).toEqual(raw.tracks[0]!.tasks[0]!.inputs!);
|
|
41
|
+
expect(task.outputs).toEqual(raw.tracks[0]!.tasks[0]!.outputs!);
|
|
42
|
+
});
|
|
43
|
+
|
|
16
44
|
test('raw ports survive onto the resolved task', () => {
|
|
17
45
|
const raw: RawPipelineConfig = {
|
|
18
46
|
name: 'p',
|
|
@@ -83,6 +111,35 @@ describe('resolveConfig — ports passthrough', () => {
|
|
|
83
111
|
// ─── deresolvePipeline preserves ports ───────────────────────────────
|
|
84
112
|
|
|
85
113
|
describe('deresolvePipeline — ports round-trip', () => {
|
|
114
|
+
test('lightweight bindings round-trip', () => {
|
|
115
|
+
const raw: RawPipelineConfig = {
|
|
116
|
+
name: 'p',
|
|
117
|
+
tracks: [
|
|
118
|
+
{
|
|
119
|
+
id: 't',
|
|
120
|
+
name: 'T',
|
|
121
|
+
tasks: [
|
|
122
|
+
{
|
|
123
|
+
id: 'a',
|
|
124
|
+
command: 'echo "{{inputs.city}}"',
|
|
125
|
+
inputs: {
|
|
126
|
+
city: { from: 't.plan.outputs.city', required: true },
|
|
127
|
+
mode: { default: 'quick' },
|
|
128
|
+
},
|
|
129
|
+
outputs: {
|
|
130
|
+
raw: { from: 'stdout' },
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
],
|
|
134
|
+
},
|
|
135
|
+
],
|
|
136
|
+
};
|
|
137
|
+
const resolved = resolveConfig(raw, WORK_DIR);
|
|
138
|
+
const back = deresolvePipeline(resolved, WORK_DIR);
|
|
139
|
+
expect(back.tracks[0]!.tasks[0]!.inputs).toEqual(raw.tracks[0]!.tasks[0]!.inputs!);
|
|
140
|
+
expect(back.tracks[0]!.tasks[0]!.outputs).toEqual(raw.tracks[0]!.tasks[0]!.outputs!);
|
|
141
|
+
});
|
|
142
|
+
|
|
86
143
|
test('ports with both inputs and outputs round-trip', () => {
|
|
87
144
|
const raw: RawPipelineConfig = {
|
|
88
145
|
name: 'p',
|
|
@@ -200,6 +257,35 @@ describe('deresolvePipeline — ports round-trip', () => {
|
|
|
200
257
|
// ─── parseYaml accepts ports ─────────────────────────────────────────
|
|
201
258
|
|
|
202
259
|
describe('parseYaml — accepts ports declarations', () => {
|
|
260
|
+
test('real-world YAML with lightweight bindings parses cleanly', () => {
|
|
261
|
+
const text = `pipeline:
|
|
262
|
+
name: demo
|
|
263
|
+
tracks:
|
|
264
|
+
- id: t
|
|
265
|
+
name: Main
|
|
266
|
+
tasks:
|
|
267
|
+
- id: build
|
|
268
|
+
command: bun run build
|
|
269
|
+
outputs:
|
|
270
|
+
bundlePath: { from: json.bundlePath }
|
|
271
|
+
- id: test
|
|
272
|
+
depends_on: [build]
|
|
273
|
+
command: 'bun test "{{inputs.bundlePath}}"'
|
|
274
|
+
inputs:
|
|
275
|
+
bundlePath:
|
|
276
|
+
from: t.build.outputs.bundlePath
|
|
277
|
+
required: true
|
|
278
|
+
`;
|
|
279
|
+
const config = parseYaml(text);
|
|
280
|
+
const build = config.tracks[0]!.tasks[0]!;
|
|
281
|
+
const testTask = config.tracks[0]!.tasks[1]!;
|
|
282
|
+
expect(build.outputs!.bundlePath).toEqual({ from: 'json.bundlePath' });
|
|
283
|
+
expect(testTask.inputs!.bundlePath).toEqual({
|
|
284
|
+
from: 't.build.outputs.bundlePath',
|
|
285
|
+
required: true,
|
|
286
|
+
});
|
|
287
|
+
});
|
|
288
|
+
|
|
203
289
|
test('real-world YAML with ports parses cleanly', () => {
|
|
204
290
|
const text = `pipeline:
|
|
205
291
|
name: demo
|
package/src/schema.ts
CHANGED
|
@@ -97,7 +97,7 @@ function validateRawTask(task: RawTaskConfig, trackId: string): void {
|
|
|
97
97
|
throw new Error(`task "${task.id}": cannot have both "prompt" and "command"`);
|
|
98
98
|
}
|
|
99
99
|
// Empty-content tasks (e.g. `prompt: ''`) are allowed at parse time and
|
|
100
|
-
// flagged as
|
|
100
|
+
// flagged as hard validation errors by validate-raw.ts.
|
|
101
101
|
}
|
|
102
102
|
|
|
103
103
|
// ═══ Config Inheritance Resolution ═══
|
|
@@ -161,8 +161,10 @@ export function resolveConfig(raw: RawPipelineConfig, workDir: string): Pipeline
|
|
|
161
161
|
completion: rawTask.completion,
|
|
162
162
|
agent_profile: rawTask.agent_profile ?? rawTrack.agent_profile,
|
|
163
163
|
cwd: rawTask.cwd ? validatePath(rawTask.cwd, workDir) : trackCwd,
|
|
164
|
-
//
|
|
165
|
-
//
|
|
164
|
+
// Lightweight bindings and ports: no inheritance — they describe
|
|
165
|
+
// per-task data flow, not cross-task defaults.
|
|
166
|
+
inputs: rawTask.inputs,
|
|
167
|
+
outputs: rawTask.outputs,
|
|
166
168
|
ports: rawTask.ports,
|
|
167
169
|
};
|
|
168
170
|
});
|
|
@@ -309,6 +311,8 @@ export function deresolvePipeline(config: PipelineConfig, workDir: string): RawP
|
|
|
309
311
|
...(task.permissions && !permissionsEqual(task.permissions, track.permissions)
|
|
310
312
|
? { permissions: task.permissions }
|
|
311
313
|
: {}),
|
|
314
|
+
...(task.inputs && Object.keys(task.inputs).length > 0 ? { inputs: task.inputs } : {}),
|
|
315
|
+
...(task.outputs && Object.keys(task.outputs).length > 0 ? { outputs: task.outputs } : {}),
|
|
312
316
|
...(task.ports &&
|
|
313
317
|
((task.ports.inputs && task.ports.inputs.length > 0) ||
|
|
314
318
|
(task.ports.outputs && task.ports.outputs.length > 0))
|
package/src/sdk.ts
CHANGED
|
@@ -129,12 +129,16 @@ export {
|
|
|
129
129
|
export {
|
|
130
130
|
substituteInputs,
|
|
131
131
|
extractInputReferences,
|
|
132
|
+
resolveTaskBindingInputs,
|
|
132
133
|
resolveTaskInputs,
|
|
134
|
+
extractTaskBindingOutputs,
|
|
133
135
|
extractTaskOutputs,
|
|
134
136
|
inferPromptPorts,
|
|
135
137
|
} from './ports';
|
|
136
138
|
export type {
|
|
137
139
|
SubstituteResult,
|
|
140
|
+
BindingInputResolution,
|
|
141
|
+
UpstreamBindingData,
|
|
138
142
|
InputResolution,
|
|
139
143
|
ExtractResult,
|
|
140
144
|
PromptPortInference,
|
|
@@ -38,6 +38,12 @@ function portsErrors(errors: ReturnType<typeof validateRaw>): typeof errors {
|
|
|
38
38
|
);
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
function bindingErrors(errors: ReturnType<typeof validateRaw>): typeof errors {
|
|
42
|
+
return errors.filter(
|
|
43
|
+
(e) => e.path.includes('.inputs') || e.path.includes('.outputs'),
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
41
47
|
// ─── Structural validation (Command Tasks) ───────────────────────────
|
|
42
48
|
|
|
43
49
|
describe('validateRaw — port structure (command tasks)', () => {
|
|
@@ -123,6 +129,66 @@ describe('validateRaw — port structure (command tasks)', () => {
|
|
|
123
129
|
});
|
|
124
130
|
});
|
|
125
131
|
|
|
132
|
+
// ─── Lightweight binding validation ──────────────────────────────────
|
|
133
|
+
|
|
134
|
+
describe('validateRaw — lightweight task bindings', () => {
|
|
135
|
+
test('accepts top-level inputs for command placeholder references', () => {
|
|
136
|
+
const errors = errorsFor(
|
|
137
|
+
commandTask({
|
|
138
|
+
id: 'a',
|
|
139
|
+
command: 'echo {{inputs.city}}',
|
|
140
|
+
inputs: { city: { value: 'Shanghai' } },
|
|
141
|
+
}),
|
|
142
|
+
);
|
|
143
|
+
expect(errors.some((e) => e.message.includes('references "{{inputs.city}}"'))).toBe(false);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test('rejects non-object binding maps and entries', () => {
|
|
147
|
+
const errors = errorsFor(
|
|
148
|
+
commandTask({
|
|
149
|
+
id: 'a',
|
|
150
|
+
inputs: 'bad' as unknown as never,
|
|
151
|
+
outputs: { ok: 'bad' as unknown as never },
|
|
152
|
+
}),
|
|
153
|
+
);
|
|
154
|
+
const msgs = bindingErrors(errors).map((e) => e.message);
|
|
155
|
+
expect(msgs.some((m) => /task\.inputs must be an object/.test(m))).toBe(true);
|
|
156
|
+
expect(msgs.some((m) => /task\.outputs\.ok must be an object/.test(m))).toBe(true);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test('rejects invalid binding names and duplicate loose/strict names', () => {
|
|
160
|
+
const errors = errorsFor(
|
|
161
|
+
commandTask({
|
|
162
|
+
id: 'a',
|
|
163
|
+
inputs: { 'bad-name': { value: 'x' }, city: { value: 'Shanghai' } },
|
|
164
|
+
outputs: { report: { from: 'stdout' } },
|
|
165
|
+
ports: {
|
|
166
|
+
inputs: [{ name: 'city', type: 'string' }],
|
|
167
|
+
outputs: [{ name: 'report', type: 'string' }],
|
|
168
|
+
},
|
|
169
|
+
}),
|
|
170
|
+
);
|
|
171
|
+
const msgs = errors.map((e) => e.message);
|
|
172
|
+
expect(msgs.some((m) => /binding name "bad-name" is invalid/.test(m))).toBe(true);
|
|
173
|
+
expect(msgs.some((m) => /duplicates strict ports\.inputs/.test(m))).toBe(true);
|
|
174
|
+
expect(msgs.some((m) => /duplicates strict ports\.outputs/.test(m))).toBe(true);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test('fully-qualified binding sources must reference direct dependencies', () => {
|
|
178
|
+
const errors = validateRaw(
|
|
179
|
+
pipeline([
|
|
180
|
+
commandTask({ id: 'up', outputs: { city: {} } }),
|
|
181
|
+
commandTask({
|
|
182
|
+
id: 'down',
|
|
183
|
+
depends_on: [],
|
|
184
|
+
inputs: { city: { from: 't.up.outputs.city', required: true } },
|
|
185
|
+
}),
|
|
186
|
+
]),
|
|
187
|
+
);
|
|
188
|
+
expect(errors.some((e) => /not a direct dependency/.test(e.message))).toBe(true);
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
|
|
126
192
|
// ─── Input/output separation (Command Tasks) ─────────────────────────
|
|
127
193
|
|
|
128
194
|
describe('validateRaw — input vs output constraints (command tasks)', () => {
|
package/src/validate-raw.ts
CHANGED
|
@@ -538,6 +538,136 @@ function validatePortList(
|
|
|
538
538
|
}
|
|
539
539
|
}
|
|
540
540
|
|
|
541
|
+
function validateBindingMap(
|
|
542
|
+
value: unknown,
|
|
543
|
+
basePath: string,
|
|
544
|
+
kind: 'inputs' | 'outputs',
|
|
545
|
+
errors: ValidationError[],
|
|
546
|
+
): void {
|
|
547
|
+
if (value === undefined) return;
|
|
548
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
549
|
+
errors.push({ path: basePath, message: `task.${kind} must be an object map` });
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
const map = value as Record<string, unknown>;
|
|
554
|
+
for (const [name, rawBinding] of Object.entries(map)) {
|
|
555
|
+
const path = `${basePath}.${name}`;
|
|
556
|
+
if (!PORT_NAME_RE.test(name)) {
|
|
557
|
+
errors.push({
|
|
558
|
+
path,
|
|
559
|
+
message: `binding name "${name}" is invalid. Must match /^[A-Za-z_][A-Za-z0-9_]*$/.`,
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
if (!rawBinding || typeof rawBinding !== 'object' || Array.isArray(rawBinding)) {
|
|
563
|
+
errors.push({ path, message: `task.${kind}.${name} must be an object` });
|
|
564
|
+
continue;
|
|
565
|
+
}
|
|
566
|
+
const binding = rawBinding as Record<string, unknown>;
|
|
567
|
+
if ('from' in binding && typeof binding.from !== 'string') {
|
|
568
|
+
errors.push({ path: `${path}.from`, message: `task.${kind}.${name}.from must be a string` });
|
|
569
|
+
}
|
|
570
|
+
if (kind === 'inputs' && 'required' in binding && typeof binding.required !== 'boolean') {
|
|
571
|
+
errors.push({
|
|
572
|
+
path: `${path}.required`,
|
|
573
|
+
message: `task.inputs.${name}.required must be a boolean`,
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
if (kind === 'outputs' && typeof binding.from === 'string') {
|
|
577
|
+
const source = binding.from;
|
|
578
|
+
const ok =
|
|
579
|
+
source === 'stdout' ||
|
|
580
|
+
source === 'stderr' ||
|
|
581
|
+
source === 'normalizedOutput' ||
|
|
582
|
+
/^json\.[A-Za-z_][A-Za-z0-9_]*$/.test(source);
|
|
583
|
+
if (!ok) {
|
|
584
|
+
errors.push({
|
|
585
|
+
path: `${path}.from`,
|
|
586
|
+
message: `task.outputs.${name}.from must be stdout, stderr, normalizedOutput, or json.<key>`,
|
|
587
|
+
});
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
function validateBindingPortNameOverlap(
|
|
594
|
+
task: RawTaskConfig,
|
|
595
|
+
taskPath: string,
|
|
596
|
+
errors: ValidationError[],
|
|
597
|
+
): void {
|
|
598
|
+
const looseInputs = objectKeys(task.inputs);
|
|
599
|
+
const looseOutputs = objectKeys(task.outputs);
|
|
600
|
+
const strictInputs = new Set(
|
|
601
|
+
Array.isArray(task.ports?.inputs) ? task.ports.inputs.map((p) => p?.name) : [],
|
|
602
|
+
);
|
|
603
|
+
const strictOutputs = new Set(
|
|
604
|
+
Array.isArray(task.ports?.outputs) ? task.ports.outputs.map((p) => p?.name) : [],
|
|
605
|
+
);
|
|
606
|
+
|
|
607
|
+
for (const name of looseInputs) {
|
|
608
|
+
if (strictInputs.has(name)) {
|
|
609
|
+
errors.push({
|
|
610
|
+
path: `${taskPath}.inputs.${name}`,
|
|
611
|
+
message: `task input binding "${name}" duplicates strict ports.inputs; choose one layer for this name`,
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
for (const name of looseOutputs) {
|
|
616
|
+
if (strictOutputs.has(name)) {
|
|
617
|
+
errors.push({
|
|
618
|
+
path: `${taskPath}.outputs.${name}`,
|
|
619
|
+
message: `task output binding "${name}" duplicates strict ports.outputs; choose one layer for this name`,
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
function objectKeys(value: unknown): string[] {
|
|
626
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return [];
|
|
627
|
+
return Object.keys(value as Record<string, unknown>);
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
function validateInputBindingSources(
|
|
631
|
+
task: RawTaskConfig,
|
|
632
|
+
trackId: string,
|
|
633
|
+
taskPath: string,
|
|
634
|
+
index: TaskIndex,
|
|
635
|
+
errors: ValidationError[],
|
|
636
|
+
): void {
|
|
637
|
+
if (!task.inputs || typeof task.inputs !== 'object' || Array.isArray(task.inputs)) return;
|
|
638
|
+
for (const [name, rawBinding] of Object.entries(task.inputs)) {
|
|
639
|
+
if (!rawBinding || typeof rawBinding !== 'object' || Array.isArray(rawBinding)) continue;
|
|
640
|
+
const source = (rawBinding as Record<string, unknown>).from;
|
|
641
|
+
if (typeof source !== 'string') continue;
|
|
642
|
+
const upstreamId = bindingSourceTaskId(source);
|
|
643
|
+
if (!upstreamId) continue;
|
|
644
|
+
const deps = task.depends_on ?? [];
|
|
645
|
+
const isDirectDep = deps.some((dep) => {
|
|
646
|
+
const resolved = resolveTaskRef(dep, trackId, index);
|
|
647
|
+
return resolved.kind === 'resolved' && resolved.qid === upstreamId;
|
|
648
|
+
});
|
|
649
|
+
if (!isDirectDep) {
|
|
650
|
+
errors.push({
|
|
651
|
+
path: `${taskPath}.inputs.${name}.from`,
|
|
652
|
+
message: `Task "${task.id}": input binding "${name}" from "${source}" references task "${upstreamId}" which is not a direct dependency (must be listed in depends_on)`,
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
function bindingSourceTaskId(source: string): string | null {
|
|
659
|
+
const outputMarker = '.outputs.';
|
|
660
|
+
const outputIdx = source.lastIndexOf(outputMarker);
|
|
661
|
+
if (outputIdx > 0) return source.slice(0, outputIdx);
|
|
662
|
+
for (const field of ['stdout', 'stderr', 'normalizedOutput', 'exitCode']) {
|
|
663
|
+
const suffix = `.${field}`;
|
|
664
|
+
if (source.endsWith(suffix) && source.length > suffix.length) {
|
|
665
|
+
return source.slice(0, -suffix.length);
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
return null;
|
|
669
|
+
}
|
|
670
|
+
|
|
541
671
|
function validateTaskPorts(
|
|
542
672
|
task: RawTaskConfig,
|
|
543
673
|
trackId: string,
|
|
@@ -550,6 +680,11 @@ function validateTaskPorts(
|
|
|
550
680
|
const isPromptTask = typeof task.prompt === 'string' && typeof task.command !== 'string';
|
|
551
681
|
const isCommandTask = typeof task.command === 'string' && typeof task.prompt !== 'string';
|
|
552
682
|
|
|
683
|
+
validateBindingMap(task.inputs, `${taskPath}.inputs`, 'inputs', errors);
|
|
684
|
+
validateBindingMap(task.outputs, `${taskPath}.outputs`, 'outputs', errors);
|
|
685
|
+
validateBindingPortNameOverlap(task, taskPath, errors);
|
|
686
|
+
validateInputBindingSources(task, trackId, taskPath, index, errors);
|
|
687
|
+
|
|
553
688
|
// ─── Prompt tasks do not declare ports ──
|
|
554
689
|
//
|
|
555
690
|
// A Prompt Task's I/O contract is inferred from direct-neighbor
|
|
@@ -585,6 +720,7 @@ function validateTaskPorts(
|
|
|
585
720
|
let availableInputs: Set<string>;
|
|
586
721
|
if (isPromptTask) {
|
|
587
722
|
availableInputs = collectUpstreamCommandOutputNames(task, trackId, qidIndex, index);
|
|
723
|
+
for (const name of objectKeys(task.inputs)) availableInputs.add(name);
|
|
588
724
|
} else {
|
|
589
725
|
// Command Task (or the pathological both-keys case, which is caught
|
|
590
726
|
// earlier as a separate error — tolerate it here).
|
|
@@ -593,6 +729,7 @@ function validateTaskPorts(
|
|
|
593
729
|
? ports.inputs.filter((p): p is PortDef => !!p && typeof p === 'object').map((p) => p.name)
|
|
594
730
|
: [],
|
|
595
731
|
);
|
|
732
|
+
for (const name of objectKeys(task.inputs)) availableInputs.add(name);
|
|
596
733
|
}
|
|
597
734
|
|
|
598
735
|
for (const name of referenced) {
|