@tagma/sdk 0.6.7 → 0.6.9
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/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +108 -9
- package/dist/engine.js.map +1 -1
- package/dist/ports.d.ts +53 -1
- package/dist/ports.d.ts.map +1 -1
- package/dist/ports.js +142 -2
- package/dist/ports.js.map +1 -1
- package/dist/runner.d.ts.map +1 -1
- package/dist/runner.js +19 -6
- package/dist/runner.js.map +1 -1
- package/dist/sdk.d.ts +5 -3
- package/dist/sdk.d.ts.map +1 -1
- package/dist/sdk.js +3 -1
- package/dist/sdk.js.map +1 -1
- package/dist/validate-raw.d.ts.map +1 -1
- package/dist/validate-raw.js +240 -31
- package/dist/validate-raw.js.map +1 -1
- package/dist/yaml-compiler.d.ts +18 -0
- package/dist/yaml-compiler.d.ts.map +1 -0
- package/dist/yaml-compiler.js +59 -0
- package/dist/yaml-compiler.js.map +1 -0
- package/package.json +6 -1
- package/src/engine-ports-mixed.test.ts +499 -0
- package/src/engine.ts +118 -9
- package/src/ports.test.ts +170 -0
- package/src/ports.ts +231 -3
- package/src/runner.test.ts +3 -3
- package/src/runner.ts +21 -5
- package/src/sdk.ts +15 -2
- package/src/validate-raw-ports.test.ts +234 -49
- package/src/validate-raw.ts +269 -34
- package/src/yaml-compiler.ts +83 -0
package/src/validate-raw.ts
CHANGED
|
@@ -6,7 +6,13 @@
|
|
|
6
6
|
//
|
|
7
7
|
// Returns a flat list of ValidationError objects. An empty array means valid.
|
|
8
8
|
|
|
9
|
-
import type {
|
|
9
|
+
import type {
|
|
10
|
+
PortDef,
|
|
11
|
+
PortType,
|
|
12
|
+
RawPipelineConfig,
|
|
13
|
+
RawTaskConfig,
|
|
14
|
+
RawTrackConfig,
|
|
15
|
+
} from './types';
|
|
10
16
|
import {
|
|
11
17
|
isValidTaskId,
|
|
12
18
|
qualifyTaskId,
|
|
@@ -16,6 +22,24 @@ import {
|
|
|
16
22
|
} from './task-ref';
|
|
17
23
|
import { extractInputReferences } from './ports';
|
|
18
24
|
|
|
25
|
+
interface QidEntry {
|
|
26
|
+
readonly track: RawTrackConfig;
|
|
27
|
+
readonly task: RawTaskConfig;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** qid → {track, task} lookup built once per validation pass. */
|
|
31
|
+
function buildQidIndex(config: RawPipelineConfig): Map<string, QidEntry> {
|
|
32
|
+
const idx = new Map<string, QidEntry>();
|
|
33
|
+
for (const track of config.tracks ?? []) {
|
|
34
|
+
if (!track.id) continue;
|
|
35
|
+
for (const task of track.tasks ?? []) {
|
|
36
|
+
if (!task.id) continue;
|
|
37
|
+
idx.set(qualifyTaskId(track.id, task.id), { track, task });
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return idx;
|
|
41
|
+
}
|
|
42
|
+
|
|
19
43
|
const DURATION_RE = /^(\d*\.?\d+)\s*(s|m|h|d)$/;
|
|
20
44
|
function isValidDuration(input: string): boolean {
|
|
21
45
|
return DURATION_RE.test(input.trim());
|
|
@@ -125,6 +149,9 @@ export function validateRaw(
|
|
|
125
149
|
// Shared with dag.ts so "ambiguous" / "not found" stay consistent — refs
|
|
126
150
|
// that buildDag later throws on will be reported here as errors first.
|
|
127
151
|
const index = buildTaskIndex(config);
|
|
152
|
+
// Full qid → {track, task} index used by port-inference validation
|
|
153
|
+
// to walk a Prompt task's neighbors without re-scanning the tracks.
|
|
154
|
+
const qidIndex = buildQidIndex(config);
|
|
128
155
|
|
|
129
156
|
// ── Per-track validation ──
|
|
130
157
|
const seenTrackIds = new Set<string>();
|
|
@@ -286,7 +313,7 @@ export function validateRaw(
|
|
|
286
313
|
}
|
|
287
314
|
|
|
288
315
|
// ── Port declaration checks ──
|
|
289
|
-
validateTaskPorts(task, taskPath, errors);
|
|
316
|
+
validateTaskPorts(task, track.id, taskPath, qidIndex, index, errors);
|
|
290
317
|
|
|
291
318
|
// ── depends_on reference checks ──
|
|
292
319
|
if (task.depends_on && task.depends_on.length > 0) {
|
|
@@ -440,20 +467,40 @@ function validatePortList(
|
|
|
440
467
|
|
|
441
468
|
function validateTaskPorts(
|
|
442
469
|
task: RawTaskConfig,
|
|
470
|
+
trackId: string,
|
|
443
471
|
taskPath: string,
|
|
472
|
+
qidIndex: Map<string, QidEntry>,
|
|
473
|
+
index: TaskIndex,
|
|
444
474
|
errors: ValidationError[],
|
|
445
475
|
): void {
|
|
446
476
|
const ports = task.ports;
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
//
|
|
451
|
-
//
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
477
|
+
const isPromptTask = typeof task.prompt === 'string' && typeof task.command !== 'string';
|
|
478
|
+
const isCommandTask = typeof task.command === 'string' && typeof task.prompt !== 'string';
|
|
479
|
+
|
|
480
|
+
// ─── Prompt tasks do not declare ports ──
|
|
481
|
+
//
|
|
482
|
+
// A Prompt Task's I/O contract is inferred from direct-neighbor
|
|
483
|
+
// Command Tasks at runtime (see `inferPromptPorts` in ports.ts).
|
|
484
|
+
// Declaring `ports` on a Prompt Task is always a configuration
|
|
485
|
+
// mistake: the declared shape would be silently ignored in favour of
|
|
486
|
+
// the inferred one, and the two drifting out of sync is the exact bug
|
|
487
|
+
// the inference design eliminates.
|
|
488
|
+
if (isPromptTask && ports !== undefined) {
|
|
489
|
+
errors.push({
|
|
490
|
+
path: `${taskPath}.ports`,
|
|
491
|
+
message:
|
|
492
|
+
`Task "${task.id}": prompt tasks do not declare ports — their I/O is ` +
|
|
493
|
+
`inferred from direct-neighbor Command tasks. Remove the "ports" field ` +
|
|
494
|
+
`and declare the corresponding inputs/outputs on the upstream/downstream ` +
|
|
495
|
+
`Command tasks instead.`,
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// ─── Collect placeholder references ──
|
|
500
|
+
// `{{inputs.X}}` is valid in both prompt and command text. The set of
|
|
501
|
+
// names a task may legally reference differs by task kind:
|
|
502
|
+
// - Command Task: its own declared `ports.inputs`
|
|
503
|
+
// - Prompt Task: the union of direct-upstream Command outputs
|
|
457
504
|
const referenced = new Set<string>();
|
|
458
505
|
if (typeof task.prompt === 'string') {
|
|
459
506
|
for (const n of extractInputReferences(task.prompt)) referenced.add(n);
|
|
@@ -461,42 +508,230 @@ function validateTaskPorts(
|
|
|
461
508
|
if (typeof task.command === 'string') {
|
|
462
509
|
for (const n of extractInputReferences(task.command)) referenced.add(n);
|
|
463
510
|
}
|
|
511
|
+
|
|
512
|
+
let availableInputs: Set<string>;
|
|
513
|
+
if (isPromptTask) {
|
|
514
|
+
availableInputs = collectUpstreamCommandOutputNames(task, trackId, qidIndex, index);
|
|
515
|
+
} else {
|
|
516
|
+
// Command Task (or the pathological both-keys case, which is caught
|
|
517
|
+
// earlier as a separate error — tolerate it here).
|
|
518
|
+
availableInputs = new Set<string>(
|
|
519
|
+
ports && Array.isArray(ports.inputs)
|
|
520
|
+
? ports.inputs.filter((p): p is PortDef => !!p && typeof p === 'object').map((p) => p.name)
|
|
521
|
+
: [],
|
|
522
|
+
);
|
|
523
|
+
}
|
|
524
|
+
|
|
464
525
|
for (const name of referenced) {
|
|
465
|
-
if (!
|
|
526
|
+
if (!availableInputs.has(name)) {
|
|
527
|
+
const hint = isPromptTask
|
|
528
|
+
? `no upstream Command task exports an output port named "${name}"`
|
|
529
|
+
: `no such input port is declared`;
|
|
466
530
|
errors.push({
|
|
467
531
|
path: taskPath,
|
|
468
|
-
message: `Task "${task.id}": references "{{inputs.${name}}}" but
|
|
532
|
+
message: `Task "${task.id}": references "{{inputs.${name}}}" but ${hint}`,
|
|
469
533
|
});
|
|
470
534
|
}
|
|
471
535
|
}
|
|
472
536
|
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
//
|
|
476
|
-
//
|
|
477
|
-
//
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
537
|
+
// ─── Structural port validation — Command Tasks only ──
|
|
538
|
+
//
|
|
539
|
+
// Prompt tasks already errored above if they tried to declare ports;
|
|
540
|
+
// running the per-port structural validator on the ignored object
|
|
541
|
+
// would just produce duplicate noise.
|
|
542
|
+
if (isCommandTask && ports) {
|
|
543
|
+
validatePortList(ports.inputs, `${taskPath}.ports.inputs`, 'inputs', errors);
|
|
544
|
+
validatePortList(ports.outputs, `${taskPath}.ports.outputs`, 'outputs', errors);
|
|
545
|
+
|
|
546
|
+
// Warn on declared-but-unused inputs. Not fatal — a user may want
|
|
547
|
+
// to surface an input as a data-flow hint for the editor even when
|
|
548
|
+
// the command doesn't template it explicitly.
|
|
549
|
+
if (typeof task.command === 'string' && Array.isArray(ports.inputs)) {
|
|
550
|
+
for (const port of ports.inputs) {
|
|
551
|
+
if (!port || typeof port !== 'object') continue;
|
|
552
|
+
if (!referenced.has(port.name)) {
|
|
553
|
+
errors.push({
|
|
554
|
+
path: `${taskPath}.ports.inputs`,
|
|
555
|
+
severity: 'warning',
|
|
556
|
+
message: `Task "${task.id}": command does not reference {{inputs.${port.name}}} — declared input is unused`,
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// Validate that fully-qualified `from` references point to direct
|
|
563
|
+
// dependencies. The runtime's findUpstreamValue only scans dependsOn,
|
|
564
|
+
// so a from that skips the dependency list will always miss at run
|
|
565
|
+
// time and block the task with a cryptic "missing required input".
|
|
566
|
+
if (Array.isArray(ports.inputs)) {
|
|
567
|
+
for (const port of ports.inputs) {
|
|
568
|
+
if (!port || typeof port !== 'object' || typeof port.from !== 'string' || !port.from.includes('.')) {
|
|
569
|
+
continue;
|
|
570
|
+
}
|
|
571
|
+
const dot = port.from.lastIndexOf('.');
|
|
572
|
+
const upstreamId = port.from.slice(0, dot);
|
|
573
|
+
const deps = task.depends_on ?? [];
|
|
574
|
+
const isDirectDep = deps.some((dep) => {
|
|
575
|
+
const resolved = resolveTaskRef(dep, trackId, index);
|
|
576
|
+
return resolved.kind === 'resolved' && resolved.qid === upstreamId;
|
|
577
|
+
});
|
|
578
|
+
if (!isDirectDep) {
|
|
579
|
+
errors.push({
|
|
580
|
+
path: `${taskPath}.ports.inputs`,
|
|
581
|
+
message: `Task "${task.id}": port "${port.name}" from "${port.from}" references task "${upstreamId}" which is not a direct dependency (must be listed in depends_on)`,
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
// ─── Prompt-task inferred-port conflict checks ──
|
|
589
|
+
//
|
|
590
|
+
// Static counterparts to the runtime checks `inferPromptPorts` runs.
|
|
591
|
+
// These surface problems at author-time in the editor so the user
|
|
592
|
+
// fixes them before a run, rather than hitting a "blocked" task.
|
|
593
|
+
if (isPromptTask) {
|
|
594
|
+
validateInferredPromptPortConflicts(task, trackId, taskPath, qidIndex, index, errors);
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
/**
|
|
599
|
+
* Walk the direct-upstream Commands of a Prompt Task and collect every
|
|
600
|
+
* output port name they export. Prompt upstreams contribute nothing —
|
|
601
|
+
* they pass free text via continue_from, not structured ports — so we
|
|
602
|
+
* skip them. This mirrors exactly what the engine does at runtime in
|
|
603
|
+
* `inferPromptPorts`, keeping the editor and runtime views aligned.
|
|
604
|
+
*/
|
|
605
|
+
function collectUpstreamCommandOutputNames(
|
|
606
|
+
task: RawTaskConfig,
|
|
607
|
+
trackId: string,
|
|
608
|
+
qidIndex: Map<string, QidEntry>,
|
|
609
|
+
index: TaskIndex,
|
|
610
|
+
): Set<string> {
|
|
611
|
+
const names = new Set<string>();
|
|
612
|
+
for (const dep of task.depends_on ?? []) {
|
|
613
|
+
const r = resolveTaskRef(dep, trackId, index);
|
|
614
|
+
if (r.kind !== 'resolved') continue;
|
|
615
|
+
const entry = qidIndex.get(r.qid);
|
|
616
|
+
if (!entry) continue;
|
|
617
|
+
// Only Command tasks contribute — Prompt upstreams pass free text.
|
|
618
|
+
if (typeof entry.task.command !== 'string') continue;
|
|
619
|
+
const outputs = entry.task.ports?.outputs;
|
|
620
|
+
if (!Array.isArray(outputs)) continue;
|
|
621
|
+
for (const port of outputs) {
|
|
622
|
+
if (port && typeof port === 'object' && typeof port.name === 'string') {
|
|
623
|
+
names.add(port.name);
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
return names;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
/**
|
|
631
|
+
* Detect the two kinds of collision that would block a Prompt Task at
|
|
632
|
+
* runtime — report them at validate-time so the editor lights them up
|
|
633
|
+
* before a run is attempted.
|
|
634
|
+
*
|
|
635
|
+
* 1. Input collision: two direct-upstream Commands both export an
|
|
636
|
+
* output with the same name. Command→Command would let the
|
|
637
|
+
* downstream disambiguate with `from:`; Prompt tasks have no port
|
|
638
|
+
* declarations and therefore no escape hatch.
|
|
639
|
+
* 2. Output collision: two direct-downstream Commands declare inputs
|
|
640
|
+
* with the same name but incompatible shapes (different type, or
|
|
641
|
+
* different enum sets). A single LLM emission cannot satisfy both.
|
|
642
|
+
*/
|
|
643
|
+
function validateInferredPromptPortConflicts(
|
|
644
|
+
task: RawTaskConfig,
|
|
645
|
+
trackId: string,
|
|
646
|
+
taskPath: string,
|
|
647
|
+
qidIndex: Map<string, QidEntry>,
|
|
648
|
+
index: TaskIndex,
|
|
649
|
+
errors: ValidationError[],
|
|
650
|
+
): void {
|
|
651
|
+
// ─── Input collision ──
|
|
652
|
+
const producersByName = new Map<string, string[]>();
|
|
653
|
+
for (const dep of task.depends_on ?? []) {
|
|
654
|
+
const r = resolveTaskRef(dep, trackId, index);
|
|
655
|
+
if (r.kind !== 'resolved') continue;
|
|
656
|
+
const entry = qidIndex.get(r.qid);
|
|
657
|
+
if (!entry || typeof entry.task.command !== 'string') continue;
|
|
658
|
+
const outputs = entry.task.ports?.outputs;
|
|
659
|
+
if (!Array.isArray(outputs)) continue;
|
|
660
|
+
for (const port of outputs) {
|
|
661
|
+
if (!port || typeof port !== 'object' || typeof port.name !== 'string') continue;
|
|
662
|
+
const list = producersByName.get(port.name) ?? [];
|
|
663
|
+
list.push(r.qid);
|
|
664
|
+
producersByName.set(port.name, list);
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
for (const [name, producers] of producersByName) {
|
|
668
|
+
if (producers.length > 1) {
|
|
669
|
+
errors.push({
|
|
670
|
+
path: taskPath,
|
|
671
|
+
message:
|
|
672
|
+
`Task "${task.id}": upstream Commands ${producers.join(', ')} all export ` +
|
|
673
|
+
`"${name}" — prompt tasks cannot disambiguate (no "from:" binding available). ` +
|
|
674
|
+
`Rename the output on one of the upstream Commands.`,
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
// ─── Output collision ──
|
|
680
|
+
//
|
|
681
|
+
// Walk every task in the pipeline once and check whether it depends on
|
|
682
|
+
// us. We reuse the shared qidIndex + TaskIndex for the lookup; small
|
|
683
|
+
// pipelines stay O(tasks), which is fine for validate-raw (it already
|
|
684
|
+
// O(tasks) elsewhere).
|
|
685
|
+
const taskQid = qualifyTaskId(trackId, task.id);
|
|
686
|
+
const consumerShapeByName = new Map<
|
|
687
|
+
string,
|
|
688
|
+
{ readonly shape: string; readonly firstConsumer: string }
|
|
689
|
+
>();
|
|
690
|
+
const reported = new Set<string>();
|
|
691
|
+
for (const [downstreamQid, entry] of qidIndex) {
|
|
692
|
+
if (downstreamQid === taskQid) continue;
|
|
693
|
+
if (typeof entry.task.command !== 'string') continue; // only downstream Commands contribute
|
|
694
|
+
const deps = entry.task.depends_on ?? [];
|
|
695
|
+
let dependsOnUs = false;
|
|
696
|
+
for (const d of deps) {
|
|
697
|
+
const r = resolveTaskRef(d, entry.track.id, index);
|
|
698
|
+
if (r.kind === 'resolved' && r.qid === taskQid) {
|
|
699
|
+
dependsOnUs = true;
|
|
700
|
+
break;
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
if (!dependsOnUs) continue;
|
|
704
|
+
const inputs = entry.task.ports?.inputs;
|
|
705
|
+
if (!Array.isArray(inputs)) continue;
|
|
706
|
+
for (const port of inputs) {
|
|
707
|
+
if (!port || typeof port !== 'object' || typeof port.name !== 'string') continue;
|
|
708
|
+
const shape = portShapeKey(port);
|
|
709
|
+
const prior = consumerShapeByName.get(port.name);
|
|
710
|
+
if (!prior) {
|
|
711
|
+
consumerShapeByName.set(port.name, { shape, firstConsumer: downstreamQid });
|
|
712
|
+
continue;
|
|
713
|
+
}
|
|
714
|
+
if (prior.shape !== shape && !reported.has(port.name)) {
|
|
715
|
+
reported.add(port.name);
|
|
490
716
|
errors.push({
|
|
491
|
-
path:
|
|
492
|
-
|
|
493
|
-
|
|
717
|
+
path: taskPath,
|
|
718
|
+
message:
|
|
719
|
+
`Task "${task.id}": downstream Commands ${prior.firstConsumer} and ` +
|
|
720
|
+
`${downstreamQid} disagree on the shape of inferred output "${port.name}" — ` +
|
|
721
|
+
`a single LLM emission cannot satisfy both. Rename on one side.`,
|
|
494
722
|
});
|
|
495
723
|
}
|
|
496
724
|
}
|
|
497
725
|
}
|
|
498
726
|
}
|
|
499
727
|
|
|
728
|
+
/** Minimal shape fingerprint for conflict detection: type + enum set. */
|
|
729
|
+
function portShapeKey(port: PortDef): string {
|
|
730
|
+
if (port.type !== 'enum') return String(port.type);
|
|
731
|
+
const enums = Array.isArray(port.enum) ? [...port.enum].sort().join('|') : '';
|
|
732
|
+
return `enum:${enums}`;
|
|
733
|
+
}
|
|
734
|
+
|
|
500
735
|
function detectCycles(config: RawPipelineConfig, index: TaskIndex): ValidationError[] {
|
|
501
736
|
// Build adjacency: qualifiedId → [resolved dep qualifiedIds]
|
|
502
737
|
const adj = new Map<string, string[]>();
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { parseYaml } from './schema';
|
|
2
|
+
import { validateRaw } from './validate-raw';
|
|
3
|
+
import type { ValidationError, KnownPluginTypes } from './validate-raw';
|
|
4
|
+
import type { RawPipelineConfig } from './types';
|
|
5
|
+
|
|
6
|
+
export interface YamlCompileResult {
|
|
7
|
+
readonly timestamp: string;
|
|
8
|
+
readonly sourceName: string;
|
|
9
|
+
readonly success: boolean;
|
|
10
|
+
readonly parseOk: boolean;
|
|
11
|
+
readonly validation: {
|
|
12
|
+
readonly errors: ReadonlyArray<Pick<ValidationError, 'path' | 'message'>>;
|
|
13
|
+
readonly warnings: ReadonlyArray<Pick<ValidationError, 'path' | 'message'>>;
|
|
14
|
+
};
|
|
15
|
+
readonly summary: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface CompileYamlOptions {
|
|
19
|
+
readonly sourceName?: string;
|
|
20
|
+
readonly knownTypes?: KnownPluginTypes;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function compileYamlContent(
|
|
24
|
+
content: string,
|
|
25
|
+
opts: CompileYamlOptions = {},
|
|
26
|
+
): YamlCompileResult {
|
|
27
|
+
const timestamp = new Date().toISOString();
|
|
28
|
+
const sourceName = opts.sourceName ?? 'untitled';
|
|
29
|
+
|
|
30
|
+
let config: RawPipelineConfig;
|
|
31
|
+
try {
|
|
32
|
+
config = parseYaml(content);
|
|
33
|
+
} catch (err) {
|
|
34
|
+
return {
|
|
35
|
+
timestamp,
|
|
36
|
+
sourceName,
|
|
37
|
+
success: false,
|
|
38
|
+
parseOk: false,
|
|
39
|
+
validation: { errors: [], warnings: [] },
|
|
40
|
+
summary: `YAML parse error: ${errorMessage(err)}`,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
let errors: ValidationError[];
|
|
45
|
+
try {
|
|
46
|
+
errors = validateRaw(config, opts.knownTypes);
|
|
47
|
+
} catch (err) {
|
|
48
|
+
return {
|
|
49
|
+
timestamp,
|
|
50
|
+
sourceName,
|
|
51
|
+
success: false,
|
|
52
|
+
parseOk: true,
|
|
53
|
+
validation: { errors: [], warnings: [] },
|
|
54
|
+
summary: `Validation crashed: ${errorMessage(err)}`,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const validationErrors = errors.filter((e) => e.severity === 'error' || e.severity == null);
|
|
59
|
+
const validationWarnings = errors.filter((e) => e.severity === 'warning');
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
timestamp,
|
|
63
|
+
sourceName,
|
|
64
|
+
success: validationErrors.length === 0,
|
|
65
|
+
parseOk: true,
|
|
66
|
+
validation: {
|
|
67
|
+
errors: validationErrors.map((e) => ({ path: e.path, message: e.message })),
|
|
68
|
+
warnings: validationWarnings.map((e) => ({ path: e.path, message: e.message })),
|
|
69
|
+
},
|
|
70
|
+
summary:
|
|
71
|
+
validationErrors.length === 0
|
|
72
|
+
? validationWarnings.length === 0
|
|
73
|
+
? 'Valid pipeline configuration'
|
|
74
|
+
: `Valid with ${validationWarnings.length} warning(s)`
|
|
75
|
+
: `Invalid: ${validationErrors.length} error(s), ${validationWarnings.length} warning(s)`,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function errorMessage(err: unknown): string {
|
|
80
|
+
if (err instanceof Error && err.message) return err.message;
|
|
81
|
+
if (typeof err === 'string') return err;
|
|
82
|
+
return 'Unknown error';
|
|
83
|
+
}
|