1use serde_json::Value;
41
42#[derive(Clone, Copy)]
45pub enum Form {
46 Bool,
48 U64,
51 F64,
53 Str,
55 StrList,
57 Duration,
59 ParamRef,
62 GkSource,
64 GkExpr,
66 PathExpr,
69 MetricSelector,
71 Vocab(&'static [&'static str]),
73 Node(&'static dyn EnumerableNode),
75 NamedMap(&'static dyn EnumerableNode),
78 ListOf(&'static dyn EnumerableNode),
80 MapOf(&'static Form),
83 ListOfForm(&'static Form),
85 FreeMap,
87 FreeScalar,
89}
90
91impl std::fmt::Debug for Form {
92 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
93 match self {
94 Form::Bool => write!(f, "Bool"),
95 Form::U64 => write!(f, "U64"),
96 Form::F64 => write!(f, "F64"),
97 Form::Str => write!(f, "Str"),
98 Form::StrList => write!(f, "StrList"),
99 Form::Duration => write!(f, "Duration"),
100 Form::ParamRef => write!(f, "ParamRef"),
101 Form::GkSource => write!(f, "GkSource"),
102 Form::GkExpr => write!(f, "GkExpr"),
103 Form::PathExpr => write!(f, "PathExpr"),
104 Form::MetricSelector => write!(f, "MetricSelector"),
105 Form::Vocab(v) => write!(f, "Vocab({v:?})"),
106 Form::Node(n) => write!(f, "Node({})", n.kind()),
107 Form::NamedMap(n) => write!(f, "NamedMap({})", n.kind()),
108 Form::ListOf(n) => write!(f, "ListOf({})", n.kind()),
109 Form::MapOf(inner) => write!(f, "MapOf({inner:?})"),
110 Form::ListOfForm(inner) => write!(f, "ListOfForm({inner:?})"),
111 Form::FreeMap => write!(f, "FreeMap"),
112 Form::FreeScalar => write!(f, "FreeScalar"),
113 }
114 }
115}
116
117impl PartialEq for Form {
118 fn eq(&self, other: &Self) -> bool {
119 use Form::*;
120 match (self, other) {
121 (Vocab(a), Vocab(b)) => a == b,
122 (Node(a), Node(b)) | (NamedMap(a), NamedMap(b)) | (ListOf(a), ListOf(b)) => {
123 a.kind() == b.kind()
124 }
125 (MapOf(a), MapOf(b)) | (ListOfForm(a), ListOfForm(b)) => a == b,
126 _ => std::mem::discriminant(self) == std::mem::discriminant(other),
127 }
128 }
129}
130
131#[derive(Debug, Clone, Copy)]
134pub struct ElementSpec {
135 pub name: &'static str,
136 pub forms: &'static [Form],
137 pub required: bool,
138 pub aliases: &'static [&'static str],
142 pub doc: &'static str,
143}
144
145const fn el(name: &'static str, forms: &'static [Form], doc: &'static str) -> ElementSpec {
146 ElementSpec {
147 name,
148 forms,
149 required: false,
150 aliases: &[],
151 doc,
152 }
153}
154
155#[derive(Debug, Clone, Copy, PartialEq, Eq)]
159pub enum Mode {
160 Partial,
161 Complete,
162}
163
164#[derive(Debug, Clone)]
166pub struct Violation {
167 pub path: String,
168 pub message: String,
169}
170
171pub trait EnumerableNode: Sync {
173 fn kind(&self) -> &'static str;
175 fn doc(&self) -> &'static str;
177 fn elements(&self, partial: Option<&Value>) -> Vec<ElementSpec>;
182 fn open_surface(&self) -> Option<&'static str> {
186 None
187 }
188
189 fn validate(&self, value: &Value, mode: Mode) -> Vec<Violation>
195 where
196 Self: Sized,
197 {
198 let mut out = Vec::new();
199 validate_node(self, value, mode, self.kind(), &mut out);
200 out
201 }
202}
203
204pub static WORKLOAD: WorkloadNode = WorkloadNode;
207pub static PHASE: PhaseNode = PhaseNode;
208pub static OP: OpNode = OpNode;
209pub static POLL: PollNode = PollNode;
210pub static SCENARIO: ScenarioNode = ScenarioNode;
211pub static STOP_WHEN: StopWhenNode = StopWhenNode;
212pub static METRIC: MetricNode = MetricNode;
213pub static ABSTRACT: AbstractNode = AbstractNode;
214pub static EVALUATIONS: EvaluationsNode = EvaluationsNode;
215pub static RELEVANCY: RelevancyNode = RelevancyNode;
216pub static BINDING_CLASSES: BindingClassesNode = BindingClassesNode;
217pub static MEMO: MemoNode = MemoNode;
218pub static OP_POLL: OpPollNode = OpPollNode;
219pub static TRIES: TriesNode = TriesNode;
220pub static BACKOFF: BackoffNode = BackoffNode;
221pub static THROTTLE: ThrottleNode = ThrottleNode;
222pub static DELAY: DelayNode = DelayNode;
223pub static CONTINUE_IF: ContinueIfNode = ContinueIfNode;
224pub static CHECKPOINT: CheckpointNode = CheckpointNode;
225pub static OPTIMIZE: OptimizeNode = OptimizeNode;
226
227pub static ALL_NODES: &[&dyn EnumerableNode] = &[
231 &WORKLOAD,
232 &PHASE,
233 &OP,
234 &POLL,
235 &SCENARIO,
236 &STOP_WHEN,
237 &METRIC,
238 &ABSTRACT,
239 &EVALUATIONS,
240 &RELEVANCY,
241 &BINDING_CLASSES,
242 &MEMO,
243 &OP_POLL,
244 &TRIES,
245 &BACKOFF,
246 &THROTTLE,
247 &DELAY,
248 &CONTINUE_IF,
249 &CHECKPOINT,
250 &OPTIMIZE,
251];
252
253pub fn node_by_kind(kind: &str) -> Option<&'static dyn EnumerableNode> {
256 ALL_NODES.iter().copied().find(|n| n.kind() == kind)
257}
258
259pub fn root() -> &'static dyn EnumerableNode {
261 &WORKLOAD
262}
263
264const STOP_EFFECTS: &[&str] = &["stop", "fail", "abort"];
267
268pub static PHASE_ELEMENTS: &[ElementSpec] = &[
269 el(
270 "cycles",
271 &[Form::U64, Form::ParamRef, Form::Str],
272 "stanza count; string forms: {param}, extent sigils (==ops:N, ===auto) — runtime-parsed",
273 ),
274 el(
275 "concurrency",
276 &[Form::U64, Form::ParamRef, Form::Str],
277 "async fibers ({param} or bare wire name resolves in scope)",
278 ),
279 el(
280 "rate",
281 &[Form::F64, Form::ParamRef],
282 "ops/sec; {param}/iter-var resolves at the phase gather",
283 ),
284 el(
285 "daemon",
286 &[Form::Bool],
287 "runs concurrently with foreground siblings; stopped when they complete",
288 ),
289 el("adapter", &[Form::Str], "adapter override for this phase"),
290 el(
291 "errors",
292 &[Form::Str],
293 "error-routing spec (e.g. \"count,retry\", \".*:counter\")",
294 ),
295 el(
296 "tries",
297 &[Form::U64, Form::Node(&TRIES)],
298 "total attempts; map form adds backoff (carries tries_backoff)",
299 ),
300 el(
301 "interval",
302 &[Form::Duration],
303 "declarative-only today: phase re-run pacing",
304 ),
305 el("repeat", &[Form::U64], "bound for interval"),
306 el(
307 "error_rate_max",
308 &[Form::F64],
309 "opt-in error-rate circuit breaker",
310 ),
311 el(
312 "timeout",
313 &[Form::Duration, Form::ParamRef],
314 "governance bound; expiry = Interrupted+Failed (timeout)",
315 ),
316 el(
317 "stop_when",
318 &[Form::ListOf(&STOP_WHEN)],
319 "SRD-83 stop conditions",
320 ),
321 el(
322 "throttle",
323 &[Form::Bool, Form::Node(&THROTTLE)],
324 "adaptive backpressure governor: windowed attempt-failure fraction walks a dynamic control (true = defaults)",
325 ),
326 el(
327 "tags",
328 &[Form::Str],
329 "tag FILTER selecting ops from blocks (exclusive with inline ops)",
330 ),
331 el(
332 "ops",
333 &[Form::NamedMap(&OP), Form::ListOf(&OP)],
334 "inline op templates (exclusive with tags selector)",
335 ),
336 el("for_each", &[Form::Str], "phase sweep: \"var in expr\""),
337 el(
338 "continue_if",
339 &[Form::GkExpr, Form::Node(&CONTINUE_IF)],
340 "pre-entry gate bounding a for_each sweep",
341 ),
342 el(
343 "loop_scope",
344 &[Form::Vocab(&["clean", "inherit"])],
345 "loop-context seeding for for_each",
346 ),
347 el(
348 "iter_scope",
349 &[Form::Vocab(&["clean", "inherit"])],
350 "iteration seeding for for_each",
351 ),
352 el(
353 "checkpoint",
354 &[
355 Form::Vocab(&["idempotent", "disabled"]),
356 Form::Node(&CHECKPOINT),
357 ],
358 "skip-on-resume eligibility (SRD-44/106)",
359 ),
360 el(
361 "status_metrics",
362 &[Form::StrList],
363 "metric-name globs surfaced on the status line",
364 ),
365 el(
366 "bindings",
367 &[Form::GkSource, Form::FreeMap],
368 "phase-scope polydat bindings (map = legacy form)",
369 ),
370 el(
371 "metrics",
372 &[
373 Form::NamedMap(&METRIC),
374 Form::GkExpr,
375 Form::ListOfForm(&Form::GkExpr),
376 ],
377 "completion-time phase metrics (SRD-40b sugars: bare expr / list)",
378 ),
379 el(
380 "dimensions",
381 &[Form::FreeMap],
382 "label declarations owned by this tier",
383 ),
384 el(
385 "poll",
386 &[Form::Node(&POLL)],
387 "SRD-75 phase-poll loop (forbids concurrency > 1)",
388 ),
389 el(
390 "optimize",
391 &[Form::GkExpr, Form::Node(&OPTIMIZE)],
392 "SRD-86 optimizer dispatch (string = objective sugar)",
393 ),
394 el(
395 "key_metrics",
396 &[Form::FreeMap],
397 "SRD-113 key-metric designations: column -> agg(family), aggregate mandatory",
398 ),
399];
400
401pub static OP_MODEL_ELEMENTS: &[ElementSpec] = &[
402 el("name", &[Form::Str], "implicit — set by the op's map key"),
403 el("description", &[Form::Str], "op description"),
404 el("desc", &[Form::Str], "legacy alias of description"),
405 el(
406 "bindings",
407 &[Form::GkSource, Form::FreeMap],
408 "op-scope polydat bindings (map = legacy form)",
409 ),
410 el(
411 "params",
412 &[Form::FreeMap],
413 "activity-level params excised from adapter fields",
414 ),
415 el("tags", &[Form::FreeMap], "tags for filtering and metadata"),
416 el(
417 "if",
418 &[Form::GkExpr],
419 "per-cycle condition; falsy skips the op",
420 ),
421 el(
422 "delay",
423 &[Form::Str, Form::Node(&DELAY)],
424 "pre/post-op delay wire name(s)",
425 ),
426 el(
427 "evaluations",
428 &[Form::Node(&EVALUATIONS)],
429 "closed-vocab validation/scoring wrapper",
430 ),
431 el(
432 "capture",
433 &[Form::MapOf(&Form::PathExpr)],
434 "wire ← path-expression extraction map",
435 ),
436 el(
437 "metrics",
438 &[
439 Form::NamedMap(&METRIC),
440 Form::GkExpr,
441 Form::ListOfForm(&Form::GkExpr),
442 ],
443 "per-cycle synthetic metrics (SRD-40b sugars: bare expr / list)",
444 ),
445 el(
446 "result",
447 &[Form::FreeMap, Form::GkSource],
448 "SRD-66 result bindings (named paths / source)",
449 ),
450 el(
451 "traverse",
452 &[Form::FreeMap],
453 "customizes the result traversal layer",
454 ),
455 el(
456 "measure",
457 &[Form::FreeMap, Form::Str, Form::ParamRef],
458 "measurement config (string = template form)",
459 ),
460 el(
461 "abstract",
462 &[Form::Node(&ABSTRACT)],
463 "SRD-108 typed interface slot (blueprint side)",
464 ),
465 el(
466 "uses",
467 &[Form::Str],
468 "instantiate the named `op_templates:` definition",
469 ),
470 el(
471 "daemon",
472 &[Form::Bool, Form::U64],
473 "op-level daemon fiber (N = max concurrent)",
474 ),
475 el(
476 "daemon_cancel_grace_ms",
477 &[Form::U64],
478 "daemon cancel grace",
479 ),
480 el("while", &[Form::GkExpr], "op-level loop guard"),
481 el(
482 "rate",
483 &[Form::F64, Form::Str],
484 "op-level pacing (number or rate string like \"5/s\")",
485 ),
486];
487
488pub static OP_ACTIVITY_ELEMENTS: &[ElementSpec] = &[
492 el("ratio", &[Form::U64], "op dispatch ratio within the stanza"),
493 el("adapter", &[Form::Str], "adapter override for this op"),
494 el("driver", &[Form::Str], "driver alias override"),
495 el(
496 "space",
497 &[Form::Str, Form::ParamRef],
498 "adapter space selector",
499 ),
500 el("instrument", &[Form::Bool], "per-op instrumentation toggle"),
501 el("start-timers", &[Form::StrList], "timer starts"),
502 el("stop-timers", &[Form::StrList], "timer stops"),
503 el(
504 "verify",
505 &[
506 Form::ListOfForm(&Form::FreeMap),
507 Form::FreeMap,
508 Form::GkExpr,
509 ],
510 "response verification clauses (string = predicate expression)",
511 ),
512 el(
513 "relevancy",
514 &[Form::Node(&RELEVANCY)],
515 "legacy top-level shorthand for evaluations.relevancy",
516 ),
517 el("strict", &[Form::Bool], "strict verification"),
518 el(
519 "poll",
520 &[Form::Node(&OP_POLL)],
521 "op-level poll loop (loops ONLY this op)",
522 ),
523 el("poll_interval_ms", &[Form::U64], "legacy op-poll interval"),
524 el("timeout_ms", &[Form::U64], "per-op timeout"),
525 el(
526 "poll_metric_name",
527 &[Form::Str],
528 "legacy op-poll metric name",
529 ),
530 el("emit", &[Form::Str], "emission override"),
531 el(
532 "batch",
533 &[Form::U64, Form::ParamRef, Form::Str],
534 "batch row cap: literal, {param}, or bare wire name",
535 ),
536 el(
537 "max_batch_size",
538 &[Form::U64, Form::GkExpr],
539 "batch byte budget (literal or GK call)",
540 ),
541 el(
542 "batchtype",
543 &[Form::Vocab(&["logged", "unlogged", "counter"])],
544 "CQL batch type",
545 ),
546 el(
547 "memo",
548 &[Form::Node(&MEMO), Form::Str],
549 "operator-facing before/after notes",
550 ),
551 el(
552 "gutter",
553 &[Form::FreeMap, Form::Str],
554 "status-gutter template (runtime-resolved)",
555 ),
556 el(
557 "readout",
558 &[Form::Vocab(&["visible", "hidden"])],
559 "SRD-63 op-level status visibility",
560 ),
561 el("errors", &[Form::Str], "per-op error-routing override"),
562 el(
563 "tries",
564 &[Form::U64, Form::Node(&TRIES)],
565 "per-op total-attempts sigil",
566 ),
567];
568
569pub static OP_STMT_ELEMENTS: &[ElementSpec] = &[
570 el("stmt", &[Form::FreeScalar], "canonical statement payload"),
571 el("op", &[Form::FreeScalar], "statement payload alias"),
572 el("ops", &[Form::FreeScalar], "statement payload alias"),
573 el("operations", &[Form::FreeScalar], "statement payload alias"),
574 el("statement", &[Form::FreeScalar], "statement payload alias"),
575 el("statements", &[Form::FreeScalar], "statement payload alias"),
576];
577
578pub static POLL_ELEMENTS: &[ElementSpec] = &[
579 ElementSpec {
580 name: "until",
581 forms: &[Form::GkExpr],
582 required: true,
583 aliases: &[],
584 doc: "predicate over captures; re-evaluated per iteration",
585 },
586 el(
587 "interval_ms",
588 &[Form::U64, Form::ParamRef],
589 "sleep between iterations (default 1000)",
590 ),
591 el(
592 "timeout_ms",
593 &[Form::U64, Form::ParamRef],
594 "wall-clock cap (default 300000)",
595 ),
596 el(
597 "max_error_retries",
598 &[Form::U64, Form::ParamRef],
599 "tolerated consecutive retryable errors",
600 ),
601 el(
602 "metric_name",
603 &[Form::Str],
604 "gauge written on successful loop exit (unit from suffix)",
605 ),
606 el(
607 "on_timeout",
608 &[Form::Vocab(&["error", "abort"])],
609 "deadline disposition",
610 ),
611 el(
612 "require",
613 &[Form::MetricSelector, Form::StrList],
614 "strict-gate selectors that must resolve",
615 ),
616];
617
618pub static SCENARIO_NODE_ELEMENTS: &[ElementSpec] = &[
619 el(
620 "for_each",
621 &[Form::Str, Form::StrList],
622 "iteration: \"var in expr\" (list = union of sub-spaces)",
623 ),
624 el(
625 "for",
626 &[Form::Str, Form::StrList],
627 "alias of for_each (list = union of sub-spaces)",
628 ),
629 el(
630 "anchor",
631 &[Form::Str],
632 "SRD-113 view anchor: one synthesized table row per iteration",
633 ),
634 el("scenario", &[Form::Str], "include another scenario by name"),
635 el(
636 "scenarios",
637 &[Form::Str, Form::ListOf(&SCENARIO)],
638 "scenario include(s) — plural takes a heterogeneous list",
639 ),
640 el(
641 "for_combinations",
642 &[Form::FreeMap],
643 "cross-product map form: var → expr",
644 ),
645 el(
646 "do_while",
647 &[Form::GkExpr],
648 "run children while true (test after)",
649 ),
650 el(
651 "do_until",
652 &[Form::GkExpr],
653 "run children until true (test after)",
654 ),
655 el(
656 "bindings",
657 &[Form::GkSource, Form::FreeMap],
658 "scenario-node bindings",
659 ),
660 el("set", &[Form::FreeMap], "param shadows over the child tree"),
661 el(
662 "phases",
663 &[Form::ListOf(&SCENARIO)],
664 "child nodes under a structural key",
665 ),
666 el(
667 "counter",
668 &[Form::Str],
669 "loop counter wire for do_while / do_until",
670 ),
671];
672
673pub static STOP_WHEN_ELEMENTS: &[ElementSpec] = &[
674 ElementSpec {
675 name: "when",
676 forms: &[Form::GkExpr, Form::ParamRef],
677 required: true,
678 aliases: &["condition"],
679 doc: "predicate over runtime-state wires; {param} interpolates",
680 },
681 el(
682 "condition",
683 &[Form::GkExpr, Form::ParamRef],
684 "serde alias of when",
685 ),
686 el(
687 "effect",
688 &[Form::Vocab(STOP_EFFECTS)],
689 "disposition on fire (alias: action; default fail)",
690 ),
691 el(
692 "action",
693 &[Form::Vocab(STOP_EFFECTS)],
694 "canonical key for effect",
695 ),
696 el(
697 "each",
698 &[Form::StrList, Form::Str],
699 "detection scope selector(s)",
700 ),
701 el("per", &[Form::StrList, Form::Str], "serde alias of each"),
702 el("trigger", &[Form::Str], "firing trigger"),
703 el("pulse", &[Form::FreeMap, Form::Str], "firing axis"),
704 el(
705 "at",
706 &[Form::Str],
707 "ACTION target scope — where the effect lands",
708 ),
709];
710
711pub static METRIC_ELEMENTS: &[ElementSpec] = &[
712 ElementSpec {
713 name: "value",
714 forms: &[Form::GkExpr],
715 required: true,
716 aliases: &[],
717 doc: "polydat expression over in-scope wires",
718 },
719 el(
720 "family",
721 &[Form::Str],
722 "family override (default: the metric's map key)",
723 ),
724 el(
725 "kind",
726 &[Form::Vocab(&["gauge", "counter", "histogram"])],
727 "instrument kind",
728 ),
729 el("unit", &[Form::Str], "unit hint"),
730 el("format", &[Form::Str], "render format hint"),
731 el(
732 "cell",
733 &[Form::FreeMap],
734 "dimension placement: label → wire (SRD coordinate cells)",
735 ),
736];
737
738pub static ABSTRACT_ELEMENTS: &[ElementSpec] = &[
739 el(
740 "needs",
741 &[Form::MapOf(&Form::FreeScalar)],
742 "wires the blueprint GUARANTEES: name → port-type keyword",
743 ),
744 el(
745 "yields",
746 &[Form::MapOf(&Form::FreeScalar)],
747 "wires the implementation must CAPTURE: name → port-type keyword",
748 ),
749 el(
750 "results",
751 &[Form::MapOf(&Form::FreeScalar)],
752 "wires delivered via result: projections: name → port-type keyword",
753 ),
754];
755
756pub static EVALUATIONS_ELEMENTS: &[ElementSpec] = &[
757 el(
758 "relevancy",
759 &[Form::Node(&RELEVANCY)],
760 "recall/ndcg scoring against ground truth",
761 ),
762 el("verify", &[Form::FreeMap], "field-equality verification"),
763];
764
765pub static RELEVANCY_ELEMENTS: &[ElementSpec] = &[
766 ElementSpec {
767 name: "actual",
768 forms: &[Form::Str],
769 required: true,
770 aliases: &[],
771 doc: "wire carrying retrieved values (bare wire name)",
772 },
773 ElementSpec {
774 name: "expected",
775 forms: &[Form::Str],
776 required: true,
777 aliases: &[],
778 doc: "wire carrying ground truth (bare wire name)",
779 },
780 el(
781 "k",
782 &[Form::U64, Form::Str],
783 "evaluation depth (literal or bare wire name)",
784 ),
785 el(
786 "r",
787 &[Form::U64, Form::Str],
788 "retrieved window (literal or bare wire name)",
789 ),
790 el(
791 "functions",
792 &[Form::ListOfForm(&Form::Vocab(&["recall", "ndcg"]))],
793 "scoring functions",
794 ),
795];
796
797pub static MEMO_ELEMENTS: &[ElementSpec] = &[
798 el(
799 "before",
800 &[Form::Str, Form::ParamRef],
801 "note before dispatch ({wire} interpolation)",
802 ),
803 el(
804 "after",
805 &[Form::Str, Form::ParamRef],
806 "note after completion ({wire} interpolation)",
807 ),
808];
809
810pub static OP_POLL_ELEMENTS: &[ElementSpec] = &[
811 el(
812 "mode",
813 &[Form::Vocab(&["await_empty"])],
814 "loop-until mode over the result body",
815 ),
816 el("until", &[Form::GkExpr], "predicate alternative to mode"),
817 el("interval_ms", &[Form::U64], "sleep between iterations"),
818 el("timeout_ms", &[Form::U64], "wall-clock cap"),
819 el(
820 "max_error_retries",
821 &[Form::U64],
822 "tolerated consecutive retryable errors",
823 ),
824 el("metric_name", &[Form::Str], "gauge on successful exit"),
825 el(
826 "memo",
827 &[Form::Str, Form::ParamRef, Form::Node(&MEMO)],
828 "per-iteration operator note ({wire} interpolation)",
829 ),
830 el(
831 "progress",
832 &[Form::Str, Form::ParamRef],
833 "derived-progress template overriding the cycle bar",
834 ),
835];
836
837pub static TRIES_ELEMENTS: &[ElementSpec] = &[
838 el("count", &[Form::U64], "total attempts"),
839 el(
840 "backoff",
841 &[Form::Node(&BACKOFF)],
842 "retry backoff overrides",
843 ),
844];
845
846pub static THROTTLE_ELEMENTS: &[ElementSpec] = &[
847 el(
848 "high",
849 &[Form::F64],
850 "windowed attempt-failure fraction that triggers back-off (default 0.05)",
851 ),
852 el("low", &[Form::F64], "recovery threshold (default high/5)"),
853 el(
854 "control",
855 &[Form::Vocab(&["concurrency", "rate"])],
856 "the dynamic control the governor walks",
857 ),
858 el(
859 "start",
860 &[Form::F64],
861 "initial offered value (slow-start seed; default = floor — declare higher only for known-robust targets)",
862 ),
863 el("floor", &[Form::F64], "never throttle below this value"),
864 el(
865 "window",
866 &[Form::Duration],
867 "evaluation window (default 2s)",
868 ),
869];
870
871pub static BACKOFF_ELEMENTS: &[ElementSpec] = &[
872 el("ratio", &[Form::F64], "growth ratio"),
873 el("min", &[Form::Duration], "floor"),
874 el("max", &[Form::Duration], "ceiling"),
875];
876
877pub static DELAY_ELEMENTS: &[ElementSpec] = &[
878 el("before", &[Form::Str], "pre-op delay wire name"),
879 el("after", &[Form::Str], "post-op delay wire name"),
880];
881
882pub static CONTINUE_IF_ELEMENTS: &[ElementSpec] = &[
883 ElementSpec {
884 name: "when",
885 forms: &[Form::GkExpr],
886 required: true,
887 aliases: &["condition"],
888 doc: "pre-entry predicate (bare wires via for_iteration scope)",
889 },
890 el("condition", &[Form::GkExpr], "serde alias of when"),
891 el(
892 "each",
893 &[Form::StrList, Form::Str],
894 "evaluation scope selector(s)",
895 ),
896 el("per", &[Form::StrList, Form::Str], "serde alias of each"),
897];
898
899pub static CHECKPOINT_ELEMENTS: &[ElementSpec] = &[
900 el("idempotent", &[Form::Bool], "prereq-class skip eligibility"),
901 el("hashed", &[Form::Bool], "provenance hashing toggle"),
902 el(
903 "verify",
904 &[Form::FreeMap],
905 "verify op-template body run before Skip",
906 ),
907];
908
909pub static OPTIMIZE_ELEMENTS: &[ElementSpec] = &[
910 el("method", &[Form::Str], "optimizer method (default sweep)"),
911 ElementSpec {
912 name: "objective",
913 forms: &[Form::GkExpr],
914 required: true,
915 aliases: &[],
916 doc: "objective wire expression (string sugar = whole block)",
917 },
918 el("servo", &[Form::StrList], "servo axis wires"),
919 el("max_evals", &[Form::U64], "evaluation budget"),
920 el("seed", &[Form::U64], "search seed"),
921];
922
923pub static WORKLOAD_ELEMENTS: &[ElementSpec] = &[
924 el(
925 "description",
926 &[Form::Str],
927 "workload description (required for curated catalog entries)",
928 ),
929 el(
930 "extends",
931 &[Form::Str],
932 "parent document (path or catalog name)",
933 ),
934 el(
935 "implements",
936 &[Form::Str],
937 "blueprint this implementation binds (SRD-108)",
938 ),
939 el(
940 "stick_session",
941 &[Form::Bool],
942 "SRD-106 re-attach-by-default",
943 ),
944 el("params", &[Form::FreeMap], "declared params with defaults"),
945 el(
946 "bindings",
947 &[Form::GkSource, Form::FreeMap],
948 "workload-root polydat bindings (map = legacy name→expr form)",
949 ),
950 el("phases", &[Form::NamedMap(&PHASE)], "phase definitions"),
951 el("scenarios", &[Form::NamedMap(&SCENARIO)], "scenario trees"),
952 el(
953 "ops",
954 &[Form::NamedMap(&OP), Form::ListOf(&OP)],
955 "top-level ops (legacy inline shape)",
956 ),
957 el(
958 "op",
959 &[Form::FreeScalar, Form::FreeMap],
960 "inline single-op shorthand",
961 ),
962 el(
963 "blocks",
964 &[Form::FreeMap],
965 "named op blocks for tag selection",
966 ),
967 el(
968 "op_templates",
969 &[Form::NamedMap(&OP)],
970 "reusable op definitions an op instantiates with `uses:`",
971 ),
972 el("tags", &[Form::FreeMap], "document tags"),
973 el(
974 "stop_when",
975 &[Form::ListOf(&STOP_WHEN)],
976 "workload-shell stop conditions",
977 ),
978 el(
979 "status_metrics",
980 &[Form::StrList],
981 "doc-root status-line default",
982 ),
983 el(
984 "report",
985 &[Form::FreeMap],
986 "SRD-46 report block (directive grammar: crate::report::vocab)",
987 ),
988 el("readouts", &[Form::FreeMap], "SRD-63 readout slot bindings"),
989 el(
990 "wrappers",
991 &[Form::FreeMap],
992 "SRD-32a wrapper-order override",
993 ),
994];
995
996pub static BINDING_CLASS_ELEMENTS: &[ElementSpec] = &[
1000 el("const", &[Form::GkSource], "construction-time constant"),
1001 el(
1002 "cursor",
1003 &[Form::GkSource],
1004 "extent-driving cursor declaration",
1005 ),
1006 el(
1007 "volatile",
1008 &[Form::GkSource],
1009 "re-evaluated per pull; acknowledges non-determinism",
1010 ),
1011 el(
1012 "shared",
1013 &[Form::GkSource],
1014 "phase-scope shared cell (write-through across ops)",
1015 ),
1016 el(
1017 "extern",
1018 &[Form::GkSource],
1019 "externally-written slot (captures, injected wires)",
1020 ),
1021 el(
1022 "input",
1023 &[Form::GkSource],
1024 "kernel coordinate input (kernel-plane; not workload-authored)",
1025 ),
1026];
1027
1028macro_rules! simple_node {
1031 ($ty:ident, $kind:literal, $doc:literal, $table:ident) => {
1032 pub struct $ty;
1033 impl EnumerableNode for $ty {
1034 fn kind(&self) -> &'static str {
1035 $kind
1036 }
1037 fn doc(&self) -> &'static str {
1038 $doc
1039 }
1040 fn elements(&self, _partial: Option<&Value>) -> Vec<ElementSpec> {
1041 $table.to_vec()
1042 }
1043 }
1044 };
1045}
1046
1047simple_node!(
1048 WorkloadNode,
1049 "workload",
1050 "workload document root",
1051 WORKLOAD_ELEMENTS
1052);
1053simple_node!(PollNode, "poll", "SRD-75 phase-poll loop", POLL_ELEMENTS);
1054pub struct ScenarioNode;
1055impl EnumerableNode for ScenarioNode {
1056 fn kind(&self) -> &'static str {
1057 "scenario"
1058 }
1059 fn doc(&self) -> &'static str {
1060 "scenario-tree node (bare string = phase name; object = structural node)"
1061 }
1062 fn elements(&self, _partial: Option<&Value>) -> Vec<ElementSpec> {
1063 SCENARIO_NODE_ELEMENTS.to_vec()
1064 }
1065 fn open_surface(&self) -> Option<&'static str> {
1066 Some(
1067 "legacy command-string entries (`name: \"run …\"`) — string \
1068 values pass through; structural keys are closed and the \
1069 parser rejects unknown ones with non-string values",
1070 )
1071 }
1072}
1073simple_node!(
1074 StopWhenNode,
1075 "stop_when",
1076 "SRD-83 stop condition",
1077 STOP_WHEN_ELEMENTS
1078);
1079simple_node!(
1080 MetricNode,
1081 "metric",
1082 "synthetic metric declaration (SRD-40b)",
1083 METRIC_ELEMENTS
1084);
1085simple_node!(
1086 AbstractNode,
1087 "abstract",
1088 "SRD-108 typed interface slot",
1089 ABSTRACT_ELEMENTS
1090);
1091simple_node!(
1092 EvaluationsNode,
1093 "evaluations",
1094 "post-execution scoring wrapper",
1095 EVALUATIONS_ELEMENTS
1096);
1097simple_node!(
1098 RelevancyNode,
1099 "relevancy",
1100 "recall/ndcg scoring config",
1101 RELEVANCY_ELEMENTS
1102);
1103simple_node!(
1104 BindingClassesNode,
1105 "binding_classes",
1106 "GK binding declaration classes (grammar lives in polydat)",
1107 BINDING_CLASS_ELEMENTS
1108);
1109simple_node!(
1110 MemoNode,
1111 "memo",
1112 "operator-facing memo notes",
1113 MEMO_ELEMENTS
1114);
1115simple_node!(
1116 OpPollNode,
1117 "op_poll",
1118 "op-level poll loop",
1119 OP_POLL_ELEMENTS
1120);
1121simple_node!(TriesNode, "tries", "tries map form", TRIES_ELEMENTS);
1122simple_node!(
1123 BackoffNode,
1124 "backoff",
1125 "retry backoff overrides",
1126 BACKOFF_ELEMENTS
1127);
1128simple_node!(
1129 ThrottleNode,
1130 "throttle",
1131 "adaptive backpressure governor",
1132 THROTTLE_ELEMENTS
1133);
1134simple_node!(DelayNode, "delay", "pre/post-op delay", DELAY_ELEMENTS);
1135simple_node!(
1136 ContinueIfNode,
1137 "continue_if",
1138 "SRD-101 pre-entry gate",
1139 CONTINUE_IF_ELEMENTS
1140);
1141simple_node!(
1142 CheckpointNode,
1143 "checkpoint",
1144 "checkpoint declaration map form",
1145 CHECKPOINT_ELEMENTS
1146);
1147simple_node!(
1148 OptimizeNode,
1149 "optimize",
1150 "SRD-86 optimize block",
1151 OPTIMIZE_ELEMENTS
1152);
1153
1154pub struct PhaseNode;
1155impl EnumerableNode for PhaseNode {
1156 fn kind(&self) -> &'static str {
1157 "phase"
1158 }
1159 fn doc(&self) -> &'static str {
1160 "one measured phase"
1161 }
1162 fn elements(&self, partial: Option<&Value>) -> Vec<ElementSpec> {
1163 let mut out: Vec<ElementSpec> = PHASE_ELEMENTS.to_vec();
1164 if let Some(Value::Object(map)) = partial {
1165 if map.contains_key("poll") {
1168 for e in &mut out {
1169 if e.name == "concurrency" {
1170 e.forms = &[Form::Vocab(&["1"])];
1171 e.doc = "pinned to 1 under poll: (SRD-75 serial-cycle loop)";
1172 }
1173 }
1174 }
1175 if map.contains_key("ops") {
1178 out.retain(|e| e.name != "tags");
1179 } else if map.contains_key("tags") {
1180 out.retain(|e| e.name != "ops");
1181 }
1182 }
1183 out
1184 }
1185}
1186
1187pub struct OpNode;
1188impl EnumerableNode for OpNode {
1189 fn kind(&self) -> &'static str {
1190 "op"
1191 }
1192 fn doc(&self) -> &'static str {
1193 "one op template"
1194 }
1195 fn elements(&self, partial: Option<&Value>) -> Vec<ElementSpec> {
1196 let mut out: Vec<ElementSpec> = OP_MODEL_ELEMENTS.to_vec();
1197 for e in OP_ACTIVITY_ELEMENTS {
1201 if !out.iter().any(|m| m.name == e.name) {
1202 out.push(*e);
1203 }
1204 }
1205 let is_abstract = matches!(partial, Some(Value::Object(m)) if m.contains_key("abstract"));
1206 if !is_abstract {
1207 out.extend_from_slice(OP_STMT_ELEMENTS);
1211 }
1212 out
1213 }
1214 fn open_surface(&self) -> Option<&'static str> {
1215 Some(
1216 "adapter op-payload fields — per-adapter surface \
1217 (closed for http/testkit via known_op_fields, open for cql)",
1218 )
1219 }
1220}
1221
1222fn form_accepts_scalar(form: &Form, v: &Value) -> bool {
1225 match form {
1226 Form::Bool => {
1227 matches!(v, Value::Bool(_))
1228 || matches!(v, Value::Number(n) if n.as_u64().is_some_and(|u| u <= 1))
1229 || matches!(v, Value::String(s)
1230 if matches!(s.as_str(), "true" | "false" | "on" | "off" | "yes" | "no"))
1231 }
1232 Form::U64 => {
1233 matches!(v, Value::Number(n) if n.as_u64().is_some())
1234 || matches!(v, Value::String(s) if s.trim().parse::<u64>().is_ok())
1235 }
1236 Form::F64 => {
1237 v.is_number() || matches!(v, Value::String(s) if s.trim().parse::<f64>().is_ok())
1238 }
1239 Form::Str
1240 | Form::GkSource
1241 | Form::GkExpr
1242 | Form::PathExpr
1243 | Form::MetricSelector
1244 | Form::Duration => v.is_string() || v.is_number(),
1245 Form::ParamRef => matches!(v, Value::String(s) if s.contains('{') && s.contains('}')),
1246 Form::StrList => match v {
1247 Value::Array(items) => items.iter().all(Value::is_string),
1248 Value::String(_) => true,
1249 _ => false,
1250 },
1251 Form::Vocab(words) => {
1252 let rendered = match v {
1256 Value::String(s) => s.trim().to_string(),
1257 Value::Number(n) => n.to_string(),
1258 Value::Bool(b) => b.to_string(),
1259 _ => return false,
1260 };
1261 words.iter().any(|w| w.eq_ignore_ascii_case(&rendered))
1262 }
1263 Form::FreeMap => v.is_object(),
1264 Form::FreeScalar => !v.is_object() || v.is_object(), Form::Node(_)
1267 | Form::NamedMap(_)
1268 | Form::ListOf(_)
1269 | Form::MapOf(_)
1270 | Form::ListOfForm(_) => false,
1271 }
1272}
1273
1274fn validate_against_forms(
1275 forms: &[Form],
1276 value: &Value,
1277 mode: Mode,
1278 path: &str,
1279 out: &mut Vec<Violation>,
1280) {
1281 for form in forms {
1284 match form {
1285 Form::Node(node) => {
1286 if value.is_object() {
1287 validate_node(*node, value, mode, path, out);
1288 return;
1289 }
1290 }
1291 Form::NamedMap(node) => {
1292 if let Value::Object(map) = value {
1293 for (name, child) in map {
1294 validate_node(*node, child, mode, &format!("{path}.{name}"), out);
1295 }
1296 return;
1297 }
1298 }
1299 Form::ListOf(node) => {
1300 if let Value::Array(items) = value {
1301 for (i, child) in items.iter().enumerate() {
1302 if child.is_object() {
1306 validate_node(*node, child, mode, &format!("{path}[{i}]"), out);
1307 }
1308 }
1309 return;
1310 }
1311 }
1312 Form::MapOf(inner) => {
1313 if let Value::Object(map) = value {
1314 for (name, child) in map {
1315 if !form_accepts_scalar(inner, child) {
1316 out.push(Violation {
1317 path: format!("{path}.{name}"),
1318 message: format!("value does not conform to {inner:?}"),
1319 });
1320 }
1321 }
1322 return;
1323 }
1324 }
1325 Form::ListOfForm(inner) => {
1326 if let Value::Array(items) = value {
1327 for (i, child) in items.iter().enumerate() {
1328 if !form_accepts_scalar(inner, child) && !matches!(**inner, Form::FreeMap) {
1329 out.push(Violation {
1330 path: format!("{path}[{i}]"),
1331 message: format!("entry does not conform to {inner:?}"),
1332 });
1333 }
1334 }
1335 return;
1336 }
1337 }
1338 scalar => {
1339 if form_accepts_scalar(scalar, value) {
1340 return;
1341 }
1342 }
1343 }
1344 }
1345 out.push(Violation {
1346 path: path.to_string(),
1347 message: format!("value conforms to none of the declared forms {forms:?}"),
1348 });
1349}
1350
1351fn validate_node(
1352 node: &dyn EnumerableNode,
1353 value: &Value,
1354 mode: Mode,
1355 path: &str,
1356 out: &mut Vec<Violation>,
1357) {
1358 if let Value::Array(items) = value {
1359 for (i, item) in items.iter().enumerate() {
1363 if item.is_object() {
1364 validate_node(node, item, mode, &format!("{path}[{i}]"), out);
1365 }
1366 }
1367 return;
1368 }
1369 let Value::Object(map) = value else {
1370 return;
1375 };
1376 let elements = node.elements(Some(value));
1377 for (key, child) in map {
1378 if child.is_null() {
1382 continue;
1383 }
1384 match elements.iter().find(|e| e.name == key.as_str()) {
1385 Some(spec) => {
1386 validate_against_forms(spec.forms, child, mode, &format!("{path}.{key}"), out);
1387 }
1388 None => {
1389 if node.open_surface().is_none() {
1390 out.push(Violation {
1391 path: format!("{path}.{key}"),
1392 message: format!("unknown element on closed node kind '{}'", node.kind()),
1393 });
1394 }
1395 }
1396 }
1397 }
1398 if mode == Mode::Complete {
1399 for spec in &elements {
1400 let satisfied =
1401 map.contains_key(spec.name) || spec.aliases.iter().any(|a| map.contains_key(*a));
1402 if spec.required && !satisfied {
1403 out.push(Violation {
1404 path: path.to_string(),
1405 message: format!(
1406 "required element '{}' missing on '{}'",
1407 spec.name,
1408 node.kind()
1409 ),
1410 });
1411 }
1412 }
1413 }
1414}
1415
1416pub fn validate_workload(ast: &Value, mode: Mode) -> Vec<Violation> {
1419 let mut out = Vec::new();
1420 validate_node(&WORKLOAD, ast, mode, "workload", &mut out);
1421 out
1422}
1423
1424pub fn discover_at<'v>(
1433 partial: &'v Value,
1434 path: &[&str],
1435) -> Option<(&'static dyn EnumerableNode, Vec<ElementSpec>)> {
1436 let mut node: &'static dyn EnumerableNode = root();
1437 let mut value: Option<&'v Value> = Some(partial);
1438
1439 let mut segs = path.iter().peekable();
1440 while let Some(seg) = segs.next() {
1441 let elements = node.elements(value);
1442 let spec = elements.iter().find(|e| e.name == *seg)?;
1443 let child_value = value.and_then(|v| v.get(*seg));
1444 let mut next: Option<&'static dyn EnumerableNode> = None;
1445 let mut named_map = false;
1446 let mut list = false;
1447 for f in spec.forms {
1448 match f {
1452 Form::Node(n) => {
1453 next = Some(*n);
1454 }
1455 Form::NamedMap(n) => {
1456 if next.is_none() || matches!(child_value, Some(v) if v.is_object()) {
1457 next = Some(*n);
1458 named_map = true;
1459 list = false;
1460 }
1461 }
1462 Form::ListOf(n) => {
1463 if next.is_none() || matches!(child_value, Some(v) if v.is_array()) {
1464 next = Some(*n);
1465 list = true;
1466 named_map = false;
1467 }
1468 }
1469 _ => {}
1470 }
1471 }
1472 node = next?;
1473 if named_map || list {
1474 match segs.next() {
1475 Some(name) => {
1476 value = child_value.and_then(|v| {
1477 if list {
1478 name.parse::<usize>().ok().and_then(|i| v.get(i))
1479 } else {
1480 v.get(*name)
1481 }
1482 });
1483 }
1484 None => {
1485 return Some((node, node.elements(None)));
1486 }
1487 }
1488 } else {
1489 value = child_value;
1490 }
1491 }
1492 let elements = node.elements(value);
1493 Some((node, elements))
1494}
1495
1496#[cfg(test)]
1497mod tests {
1498 use super::*;
1499
1500 fn names(specs: &[ElementSpec]) -> Vec<&'static str> {
1501 specs.iter().map(|e| e.name).collect()
1502 }
1503
1504 fn fields_of<'de, T: serde::Deserialize<'de>>() -> Option<&'static [&'static str]> {
1508 use std::cell::Cell;
1509 struct Probe<'a>(&'a Cell<Option<&'static [&'static str]>>);
1510 impl<'de, 'a> serde::Deserializer<'de> for Probe<'a> {
1511 type Error = serde::de::value::Error;
1512 fn deserialize_struct<V>(
1513 self,
1514 _name: &'static str,
1515 fields: &'static [&'static str],
1516 _visitor: V,
1517 ) -> Result<V::Value, Self::Error>
1518 where
1519 V: serde::de::Visitor<'de>,
1520 {
1521 self.0.set(Some(fields));
1522 Err(serde::de::Error::custom("field probe"))
1523 }
1524 fn deserialize_any<V>(self, _visitor: V) -> Result<V::Value, Self::Error>
1525 where
1526 V: serde::de::Visitor<'de>,
1527 {
1528 Err(serde::de::Error::custom("field probe"))
1529 }
1530 serde::forward_to_deserialize_any! {
1531 bool i8 i16 i32 i64 i128 u8 u16 u32 u64 u128 f32 f64
1532 char str string bytes byte_buf option unit unit_struct
1533 newtype_struct seq tuple tuple_struct map enum
1534 identifier ignored_any
1535 }
1536 }
1537 let cell = Cell::new(None);
1538 let _ = T::deserialize(Probe(&cell));
1539 cell.get()
1540 }
1541
1542 #[test]
1546 fn phase_table_matches_the_model_struct() {
1547 let probed =
1548 fields_of::<crate::model::WorkloadPhase>().expect("WorkloadPhase is a serde struct");
1549 let mut probed: Vec<&str> = probed.to_vec();
1550 probed.retain(|f| *f != "tries_backoff");
1551 probed.sort_unstable();
1552 let mut table = names(PHASE_ELEMENTS);
1553 table.sort_unstable();
1554 assert_eq!(
1555 probed, table,
1556 "PHASE_ELEMENTS drifted from the WorkloadPhase struct — \
1557 update the spec table (and its forms/docs) in lock-step"
1558 );
1559 }
1560
1561 #[test]
1565 fn serde_backed_tables_match_their_structs() {
1566 let probed = fields_of::<crate::model::StopConditionSpec>()
1567 .expect("StopConditionSpec is a serde struct");
1568 for f in probed {
1569 assert!(
1570 STOP_WHEN_ELEMENTS.iter().any(|e| e.name == *f),
1571 "STOP_WHEN_ELEMENTS missing struct field '{f}'"
1572 );
1573 }
1574 let probed =
1575 fields_of::<crate::model::ContinueIfSpec>().expect("ContinueIfSpec is a serde struct");
1576 for f in probed {
1577 assert!(
1578 CONTINUE_IF_ELEMENTS.iter().any(|e| e.name == *f),
1579 "CONTINUE_IF_ELEMENTS missing struct field '{f}'"
1580 );
1581 }
1582 }
1583
1584 #[test]
1585 fn tables_are_duplicate_free_and_nodes_registered() {
1586 let mut kinds = std::collections::BTreeSet::new();
1587 for n in ALL_NODES {
1588 assert!(kinds.insert(n.kind()), "duplicate node kind '{}'", n.kind());
1589 }
1590 for n in ALL_NODES {
1591 let mut seen = std::collections::BTreeSet::new();
1592 for e in n.elements(None) {
1593 assert!(
1594 seen.insert(e.name),
1595 "node '{}': duplicate element '{}'",
1596 n.kind(),
1597 e.name
1598 );
1599 for f in e.forms {
1605 if let Form::Node(c) | Form::NamedMap(c) | Form::ListOf(c) = f {
1606 assert!(
1607 ALL_NODES.iter().any(|k| k.kind() == c.kind()),
1608 "kind '{}' not in ALL_NODES",
1609 c.kind()
1610 );
1611 }
1612 }
1613 }
1614 }
1615 }
1616
1617 #[test]
1618 fn discovery_walks_a_partial_ast_with_narrowing() {
1619 let partial: Value = serde_yaml::from_str(
1620 r#"
1621phases:
1622 load:
1623 poll:
1624 until: "x == 1"
1625 ops:
1626 probe:
1627 stmt: "S"
1628"#,
1629 )
1630 .expect("yaml");
1631
1632 let (node, elements) = discover_at(&partial, &[]).expect("root");
1633 assert_eq!(node.kind(), "workload");
1634 assert!(elements.iter().any(|e| e.name == "phases"));
1635
1636 let (node, elements) = discover_at(&partial, &["phases", "load"]).expect("phase");
1637 assert_eq!(node.kind(), "phase");
1638 let conc = elements
1639 .iter()
1640 .find(|e| e.name == "concurrency")
1641 .expect("concurrency");
1642 assert_eq!(conc.forms, &[Form::Vocab(&["1"])]);
1643 assert!(
1644 !elements.iter().any(|e| e.name == "tags"),
1645 "inline ops exclude the tags selector"
1646 );
1647
1648 let (node, elements) = discover_at(&partial, &["phases", "load", "poll"]).expect("poll");
1649 assert_eq!(node.kind(), "poll");
1650 assert!(elements.iter().any(|e| e.name == "until" && e.required));
1651
1652 let (node, elements) =
1653 discover_at(&partial, &["phases", "load", "ops", "probe"]).expect("op");
1654 assert_eq!(node.kind(), "op");
1655 assert!(elements.iter().any(|e| e.name == "stmt"));
1656
1657 let (node, _) = discover_at(&partial, &["phases"]).expect("phase kind");
1658 assert_eq!(node.kind(), "phase");
1659
1660 assert!(discover_at(&partial, &["phases", "load", "nope", "deeper"]).is_none());
1661 }
1662
1663 #[test]
1664 fn abstract_ops_drop_the_statement_payload() {
1665 let partial: Value = serde_yaml::from_str(
1666 r#"
1667phases:
1668 p:
1669 ops:
1670 slot:
1671 abstract:
1672 needs:
1673 q: vec_f32
1674"#,
1675 )
1676 .expect("yaml");
1677 let (_, elements) = discover_at(&partial, &["phases", "p", "ops", "slot"]).expect("op");
1678 assert!(
1679 !elements.iter().any(|e| e.name == "stmt"),
1680 "abstract slots carry no statement payload"
1681 );
1682 assert!(elements.iter().any(|e| e.name == "abstract"));
1683 }
1684
1685 #[test]
1686 fn validation_accepts_a_well_formed_document() {
1687 let ast: Value = serde_yaml::from_str(
1688 r#"
1689description: ok
1690params:
1691 p0: "3"
1692phases:
1693 load:
1694 cycles: 5
1695 concurrency: 2
1696 bindings: |
1697 const c := 1
1698 ops:
1699 ins:
1700 stmt: "X {p0}"
1701 capture:
1702 w: /0/v
1703scenarios:
1704 default:
1705 - load
1706"#,
1707 )
1708 .expect("yaml");
1709 let violations = validate_workload(&ast, Mode::Complete);
1710 assert!(
1711 violations.is_empty(),
1712 "unexpected violations: {violations:#?}"
1713 );
1714 }
1715
1716 #[test]
1717 fn validation_flags_bad_forms_and_missing_required() {
1718 let ast: Value = serde_yaml::from_str(
1719 r#"
1720phases:
1721 load:
1722 rate: "not-a-number"
1723 checkpoint: "bogus-mode"
1724 poll:
1725 interval_ms: 50
1726 metrics:
1727 m0:
1728 kind: "sideways"
1729"#,
1730 )
1731 .expect("yaml");
1732 let violations = validate_workload(&ast, Mode::Complete);
1733 let paths: Vec<&str> = violations.iter().map(|v| v.path.as_str()).collect();
1734 assert!(
1735 paths.iter().any(|p| p.ends_with("rate")),
1736 "rate form violation expected: {violations:#?}"
1737 );
1738 assert!(
1739 paths.iter().any(|p| p.ends_with("checkpoint")),
1740 "checkpoint vocab violation expected"
1741 );
1742 assert!(
1743 paths.iter().any(|p| p.ends_with("kind")),
1744 "metric kind vocab violation expected"
1745 );
1746 assert!(
1748 violations.iter().any(|v| v.message.contains("'until'")),
1749 "poll.until required-missing expected: {violations:#?}"
1750 );
1751 let partial = validate_workload(&ast, Mode::Partial);
1753 assert!(
1754 !partial.iter().any(|v| v.message.contains("required")),
1755 "partial mode must not demand required elements"
1756 );
1757 }
1758}