@tagma/sdk 0.6.9 → 0.6.11
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 +58 -10
- package/dist/config-ops.d.ts.map +1 -1
- package/dist/config-ops.js +14 -0
- package/dist/config-ops.js.map +1 -1
- package/dist/pipeline-runner.d.ts +1 -1
- package/dist/pipeline-runner.d.ts.map +1 -1
- package/dist/pipeline-runner.js +20 -0
- package/dist/pipeline-runner.js.map +1 -1
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +20 -1
- package/dist/registry.js.map +1 -1
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +40 -8
- package/dist/schema.js.map +1 -1
- package/dist/task-ref.d.ts.map +1 -1
- package/dist/task-ref.js +2 -0
- package/dist/task-ref.js.map +1 -1
- package/dist/utils.js +3 -3
- package/dist/utils.js.map +1 -1
- package/dist/validate-raw.d.ts +1 -0
- package/dist/validate-raw.d.ts.map +1 -1
- package/dist/validate-raw.js +74 -3
- package/dist/validate-raw.js.map +1 -1
- package/dist/yaml-compiler.d.ts.map +1 -1
- package/dist/yaml-compiler.js +23 -5
- package/dist/yaml-compiler.js.map +1 -1
- package/package.json +2 -2
- package/src/completions/output-check.test.ts +50 -0
- package/src/config-ops.test.ts +70 -0
- package/src/config-ops.ts +11 -0
- package/src/pipeline-runner.test.ts +95 -0
- package/src/pipeline-runner.ts +18 -1
- package/src/plugin-registry.test.ts +18 -0
- package/src/registry.ts +25 -1
- package/src/schema.test.ts +113 -1
- package/src/schema.ts +45 -10
- package/src/task-ref.ts +1 -0
- package/src/utils.test.ts +28 -0
- package/src/utils.ts +3 -3
- package/src/validate-raw-plugin-types.test.ts +60 -0
- package/src/validate-raw.ts +78 -4
- package/src/yaml-compiler.test.ts +108 -0
- package/src/yaml-compiler.ts +32 -5
package/src/validate-raw.ts
CHANGED
|
@@ -32,6 +32,7 @@ function buildQidIndex(config: RawPipelineConfig): Map<string, QidEntry> {
|
|
|
32
32
|
const idx = new Map<string, QidEntry>();
|
|
33
33
|
for (const track of config.tracks ?? []) {
|
|
34
34
|
if (!track.id) continue;
|
|
35
|
+
if (!Array.isArray(track.tasks)) continue;
|
|
35
36
|
for (const task of track.tasks ?? []) {
|
|
36
37
|
if (!task.id) continue;
|
|
37
38
|
idx.set(qualifyTaskId(track.id, task.id), { track, task });
|
|
@@ -55,6 +56,7 @@ const isValidId = isValidTaskId;
|
|
|
55
56
|
|
|
56
57
|
const VALID_ON_FAILURE = new Set(['skip_downstream', 'stop_all', 'ignore']);
|
|
57
58
|
const VALID_REASONING_EFFORT = new Set(['low', 'medium', 'high']);
|
|
59
|
+
const PERMISSION_FIELDS = ['read', 'write', 'execute'] as const;
|
|
58
60
|
|
|
59
61
|
// Built-in plugin types always known to the SDK core, regardless of which
|
|
60
62
|
// external plugin packages are installed. These MUST stay in sync with the
|
|
@@ -67,6 +69,7 @@ const BUILTIN_COMPLETION_TYPES: ReadonlySet<string> = new Set([
|
|
|
67
69
|
'output_check',
|
|
68
70
|
]);
|
|
69
71
|
const BUILTIN_MIDDLEWARE_TYPES: ReadonlySet<string> = new Set(['static_context']);
|
|
72
|
+
const BUILTIN_DRIVER_TYPES: ReadonlySet<string> = new Set(['opencode']);
|
|
70
73
|
|
|
71
74
|
/**
|
|
72
75
|
* Optional second argument to `validateRaw`: the set of plugin types currently
|
|
@@ -78,6 +81,7 @@ const BUILTIN_MIDDLEWARE_TYPES: ReadonlySet<string> = new Set(['static_context']
|
|
|
78
81
|
* this argument and no plugin warnings will be produced.
|
|
79
82
|
*/
|
|
80
83
|
export interface KnownPluginTypes {
|
|
84
|
+
readonly drivers?: readonly string[];
|
|
81
85
|
readonly triggers?: readonly string[];
|
|
82
86
|
readonly completions?: readonly string[];
|
|
83
87
|
readonly middlewares?: readonly string[];
|
|
@@ -121,6 +125,9 @@ export function validateRaw(
|
|
|
121
125
|
const knownTriggers = knownTypes
|
|
122
126
|
? new Set<string>([...BUILTIN_TRIGGER_TYPES, ...(knownTypes.triggers ?? [])])
|
|
123
127
|
: null;
|
|
128
|
+
const knownDrivers = knownTypes
|
|
129
|
+
? new Set<string>([...BUILTIN_DRIVER_TYPES, ...(knownTypes.drivers ?? [])])
|
|
130
|
+
: null;
|
|
124
131
|
const knownCompletions = knownTypes
|
|
125
132
|
? new Set<string>([...BUILTIN_COMPLETION_TYPES, ...(knownTypes.completions ?? [])])
|
|
126
133
|
: null;
|
|
@@ -138,8 +145,20 @@ export function validateRaw(
|
|
|
138
145
|
message: `Invalid reasoning_effort "${config.reasoning_effort}". Expected "low", "medium", or "high".`,
|
|
139
146
|
});
|
|
140
147
|
}
|
|
148
|
+
if (knownDrivers && config.driver && !knownDrivers.has(config.driver)) {
|
|
149
|
+
errors.push({
|
|
150
|
+
path: 'driver',
|
|
151
|
+
message: `Unknown driver type "${config.driver}"`,
|
|
152
|
+
severity: 'warning',
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
validatePermissions(config.permissions, 'permissions', errors);
|
|
141
156
|
|
|
142
|
-
if (!
|
|
157
|
+
if (!Array.isArray(config.tracks)) {
|
|
158
|
+
errors.push({ path: 'tracks', message: 'pipeline.tracks must be an array' });
|
|
159
|
+
return errors;
|
|
160
|
+
}
|
|
161
|
+
if (config.tracks.length === 0) {
|
|
143
162
|
errors.push({ path: 'tracks', message: 'At least one track is required' });
|
|
144
163
|
return errors; // No point going further without tracks
|
|
145
164
|
}
|
|
@@ -156,8 +175,13 @@ export function validateRaw(
|
|
|
156
175
|
// ── Per-track validation ──
|
|
157
176
|
const seenTrackIds = new Set<string>();
|
|
158
177
|
for (let ti = 0; ti < config.tracks.length; ti++) {
|
|
159
|
-
const
|
|
178
|
+
const maybeTrack = config.tracks[ti] as unknown;
|
|
160
179
|
const trackPath = `tracks[${ti}]`;
|
|
180
|
+
if (!maybeTrack || typeof maybeTrack !== 'object' || Array.isArray(maybeTrack)) {
|
|
181
|
+
errors.push({ path: trackPath, message: `Track ${ti} must be an object` });
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
const track = maybeTrack as RawTrackConfig;
|
|
161
185
|
|
|
162
186
|
if (!track.id?.trim()) {
|
|
163
187
|
errors.push({ path: `${trackPath}.id`, message: 'Track id is required' });
|
|
@@ -186,6 +210,14 @@ export function validateRaw(
|
|
|
186
210
|
message: `Invalid reasoning_effort "${track.reasoning_effort}". Expected "low", "medium", or "high".`,
|
|
187
211
|
});
|
|
188
212
|
}
|
|
213
|
+
if (knownDrivers && track.driver && !knownDrivers.has(track.driver)) {
|
|
214
|
+
errors.push({
|
|
215
|
+
path: `${trackPath}.driver`,
|
|
216
|
+
message: `Unknown driver type "${track.driver}"`,
|
|
217
|
+
severity: 'warning',
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
validatePermissions(track.permissions, `${trackPath}.permissions`, errors);
|
|
189
221
|
|
|
190
222
|
// Track-level middlewares can reference a plugin that was uninstalled
|
|
191
223
|
// after the YAML was written — surface a warning so the user notices
|
|
@@ -203,7 +235,14 @@ export function validateRaw(
|
|
|
203
235
|
}
|
|
204
236
|
}
|
|
205
237
|
|
|
206
|
-
if (!
|
|
238
|
+
if (!Array.isArray(track.tasks)) {
|
|
239
|
+
errors.push({
|
|
240
|
+
path: `${trackPath}.tasks`,
|
|
241
|
+
message: `Track "${track.id || ti}": tasks must be an array`,
|
|
242
|
+
});
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
if (track.tasks.length === 0) {
|
|
207
246
|
errors.push({
|
|
208
247
|
path: `${trackPath}.tasks`,
|
|
209
248
|
message: `Track "${track.id || ti}": must have at least one task`,
|
|
@@ -276,6 +315,14 @@ export function validateRaw(
|
|
|
276
315
|
message: `Invalid reasoning_effort "${task.reasoning_effort}". Expected "low", "medium", or "high".`,
|
|
277
316
|
});
|
|
278
317
|
}
|
|
318
|
+
if (knownDrivers && task.driver && !knownDrivers.has(task.driver)) {
|
|
319
|
+
errors.push({
|
|
320
|
+
path: `${taskPath}.driver`,
|
|
321
|
+
message: `Unknown driver type "${task.driver}"`,
|
|
322
|
+
severity: 'warning',
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
validatePermissions(task.permissions, `${taskPath}.permissions`, errors);
|
|
279
326
|
|
|
280
327
|
// ── Plugin type warnings (trigger / completion / middlewares) ──
|
|
281
328
|
// Only fire when the host supplied a `knownTypes` snapshot, so offline
|
|
@@ -348,7 +395,7 @@ export function validateRaw(
|
|
|
348
395
|
});
|
|
349
396
|
} else if (
|
|
350
397
|
!task.depends_on ||
|
|
351
|
-
!task.depends_on.some((dep) => {
|
|
398
|
+
!task.depends_on.some((dep: string) => {
|
|
352
399
|
const depResolved = resolveTaskRef(dep, track.id, index);
|
|
353
400
|
return depResolved.kind === 'resolved' && depResolved.qid === resolved.qid;
|
|
354
401
|
})
|
|
@@ -374,6 +421,32 @@ export function validateRaw(
|
|
|
374
421
|
return errors;
|
|
375
422
|
}
|
|
376
423
|
|
|
424
|
+
function validatePermissions(
|
|
425
|
+
value: unknown,
|
|
426
|
+
basePath: string,
|
|
427
|
+
errors: ValidationError[],
|
|
428
|
+
): void {
|
|
429
|
+
if (value === undefined) return;
|
|
430
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
431
|
+
errors.push({
|
|
432
|
+
path: basePath,
|
|
433
|
+
message: 'permissions must be an object with read/write/execute booleans',
|
|
434
|
+
});
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
const p = value as Record<string, unknown>;
|
|
438
|
+
for (const field of PERMISSION_FIELDS) {
|
|
439
|
+
const path = `${basePath}.${field}`;
|
|
440
|
+
if (!(field in p)) {
|
|
441
|
+
errors.push({ path, message: `permissions.${field} is required` });
|
|
442
|
+
continue;
|
|
443
|
+
}
|
|
444
|
+
if (typeof p[field] !== 'boolean') {
|
|
445
|
+
errors.push({ path, message: `permissions.${field} must be a boolean` });
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
377
450
|
const VALID_PORT_TYPES: ReadonlySet<PortType> = new Set([
|
|
378
451
|
'string',
|
|
379
452
|
'number',
|
|
@@ -738,6 +811,7 @@ function detectCycles(config: RawPipelineConfig, index: TaskIndex): ValidationEr
|
|
|
738
811
|
|
|
739
812
|
for (const track of config.tracks) {
|
|
740
813
|
if (!track.id) continue;
|
|
814
|
+
if (!Array.isArray(track.tasks)) continue;
|
|
741
815
|
for (const task of track.tasks ?? []) {
|
|
742
816
|
if (!task.id) continue;
|
|
743
817
|
const qid = qualifyTaskId(track.id, task.id);
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import { compileYamlContent } from './yaml-compiler';
|
|
3
|
+
|
|
4
|
+
describe('compileYamlContent', () => {
|
|
5
|
+
test('reports YAML syntax failures as parse errors', () => {
|
|
6
|
+
const result = compileYamlContent('pipeline:\n name: [');
|
|
7
|
+
|
|
8
|
+
expect(result.parseOk).toBe(false);
|
|
9
|
+
expect(result.success).toBe(false);
|
|
10
|
+
expect(result.validation.errors).toEqual([]);
|
|
11
|
+
expect(result.summary).toMatch(/^YAML parse error:/);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
test('reports missing top-level pipeline as a validation error', () => {
|
|
15
|
+
const result = compileYamlContent('name: Missing Pipeline\n');
|
|
16
|
+
|
|
17
|
+
expect(result.parseOk).toBe(true);
|
|
18
|
+
expect(result.success).toBe(false);
|
|
19
|
+
expect(result.validation.errors).toEqual([
|
|
20
|
+
{ path: 'pipeline', message: 'Top-level "pipeline" key is required' },
|
|
21
|
+
]);
|
|
22
|
+
expect(result.summary).toBe('Invalid: 1 error(s), 0 warning(s)');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test('reports non-array tracks as a validation error, not a parse failure', () => {
|
|
26
|
+
const result = compileYamlContent(`
|
|
27
|
+
pipeline:
|
|
28
|
+
name: Bad
|
|
29
|
+
tracks:
|
|
30
|
+
id: not-an-array
|
|
31
|
+
`);
|
|
32
|
+
|
|
33
|
+
expect(result.parseOk).toBe(true);
|
|
34
|
+
expect(result.success).toBe(false);
|
|
35
|
+
expect(result.validation.errors).toEqual([
|
|
36
|
+
{ path: 'tracks', message: 'pipeline.tracks must be an array' },
|
|
37
|
+
]);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('reports non-array task lists as validation errors, not validation crashes', () => {
|
|
41
|
+
const result = compileYamlContent(`
|
|
42
|
+
pipeline:
|
|
43
|
+
name: Bad Tasks
|
|
44
|
+
tracks:
|
|
45
|
+
- id: t
|
|
46
|
+
name: T
|
|
47
|
+
tasks:
|
|
48
|
+
id: not-an-array
|
|
49
|
+
`);
|
|
50
|
+
|
|
51
|
+
expect(result.parseOk).toBe(true);
|
|
52
|
+
expect(result.success).toBe(false);
|
|
53
|
+
expect(result.validation.errors).toEqual([
|
|
54
|
+
{ path: 'tracks[0].tasks', message: 'Track "t": tasks must be an array' },
|
|
55
|
+
]);
|
|
56
|
+
expect(result.summary).not.toMatch(/Validation crashed/);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test('routes schema errors through validation when YAML syntax is valid', () => {
|
|
60
|
+
const result = compileYamlContent(`
|
|
61
|
+
pipeline:
|
|
62
|
+
name: Missing Track Name
|
|
63
|
+
tracks:
|
|
64
|
+
- id: main
|
|
65
|
+
tasks:
|
|
66
|
+
- id: task
|
|
67
|
+
prompt: hello
|
|
68
|
+
`);
|
|
69
|
+
|
|
70
|
+
expect(result.parseOk).toBe(true);
|
|
71
|
+
expect(result.success).toBe(false);
|
|
72
|
+
expect(result.validation.errors).toContainEqual({
|
|
73
|
+
path: 'tracks[0].name',
|
|
74
|
+
message: 'Track name is required',
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test('validates pipeline, track, and task permissions shape', () => {
|
|
79
|
+
const result = compileYamlContent(`
|
|
80
|
+
pipeline:
|
|
81
|
+
name: Bad Permissions
|
|
82
|
+
permissions: { read: true, write: "yes", execute: false }
|
|
83
|
+
tracks:
|
|
84
|
+
- id: main
|
|
85
|
+
name: Main
|
|
86
|
+
permissions: { read: true, execute: false }
|
|
87
|
+
tasks:
|
|
88
|
+
- id: task
|
|
89
|
+
prompt: hello
|
|
90
|
+
permissions: nope
|
|
91
|
+
`);
|
|
92
|
+
|
|
93
|
+
expect(result.parseOk).toBe(true);
|
|
94
|
+
expect(result.success).toBe(false);
|
|
95
|
+
expect(result.validation.errors).toContainEqual({
|
|
96
|
+
path: 'permissions.write',
|
|
97
|
+
message: 'permissions.write must be a boolean',
|
|
98
|
+
});
|
|
99
|
+
expect(result.validation.errors).toContainEqual({
|
|
100
|
+
path: 'tracks[0].permissions.write',
|
|
101
|
+
message: 'permissions.write is required',
|
|
102
|
+
});
|
|
103
|
+
expect(result.validation.errors).toContainEqual({
|
|
104
|
+
path: 'tracks[0].tasks[0].permissions',
|
|
105
|
+
message: 'permissions must be an object with read/write/execute booleans',
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
});
|
package/src/yaml-compiler.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import yaml from 'js-yaml';
|
|
2
2
|
import { validateRaw } from './validate-raw';
|
|
3
3
|
import type { ValidationError, KnownPluginTypes } from './validate-raw';
|
|
4
4
|
import type { RawPipelineConfig } from './types';
|
|
@@ -27,9 +27,9 @@ export function compileYamlContent(
|
|
|
27
27
|
const timestamp = new Date().toISOString();
|
|
28
28
|
const sourceName = opts.sourceName ?? 'untitled';
|
|
29
29
|
|
|
30
|
-
let
|
|
30
|
+
let doc: unknown;
|
|
31
31
|
try {
|
|
32
|
-
|
|
32
|
+
doc = yaml.load(content);
|
|
33
33
|
} catch (err) {
|
|
34
34
|
return {
|
|
35
35
|
timestamp,
|
|
@@ -41,6 +41,12 @@ export function compileYamlContent(
|
|
|
41
41
|
};
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
const envelopeErrors = validateEnvelope(doc);
|
|
45
|
+
if (envelopeErrors.length > 0) {
|
|
46
|
+
return buildValidationResult(timestamp, sourceName, envelopeErrors);
|
|
47
|
+
}
|
|
48
|
+
const config = (doc as { pipeline: RawPipelineConfig }).pipeline;
|
|
49
|
+
|
|
44
50
|
let errors: ValidationError[];
|
|
45
51
|
try {
|
|
46
52
|
errors = validateRaw(config, opts.knownTypes);
|
|
@@ -55,8 +61,29 @@ export function compileYamlContent(
|
|
|
55
61
|
};
|
|
56
62
|
}
|
|
57
63
|
|
|
58
|
-
|
|
59
|
-
|
|
64
|
+
return buildValidationResult(timestamp, sourceName, errors);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function validateEnvelope(doc: unknown): ValidationError[] {
|
|
68
|
+
if (!doc || typeof doc !== 'object' || Array.isArray(doc) || !('pipeline' in doc)) {
|
|
69
|
+
return [{ path: 'pipeline', message: 'Top-level "pipeline" key is required' }];
|
|
70
|
+
}
|
|
71
|
+
const pipeline = (doc as Record<string, unknown>).pipeline;
|
|
72
|
+
if (!pipeline || typeof pipeline !== 'object' || Array.isArray(pipeline)) {
|
|
73
|
+
return [{ path: 'pipeline', message: 'pipeline must be an object' }];
|
|
74
|
+
}
|
|
75
|
+
return [];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function buildValidationResult(
|
|
79
|
+
timestamp: string,
|
|
80
|
+
sourceName: string,
|
|
81
|
+
diagnostics: readonly ValidationError[],
|
|
82
|
+
): YamlCompileResult {
|
|
83
|
+
const validationErrors = diagnostics.filter(
|
|
84
|
+
(e) => e.severity === 'error' || e.severity == null,
|
|
85
|
+
);
|
|
86
|
+
const validationWarnings = diagnostics.filter((e) => e.severity === 'warning');
|
|
60
87
|
|
|
61
88
|
return {
|
|
62
89
|
timestamp,
|