Files
raphael a4599b3b9d config: multi-error YAML validator + --validate-config CLI flag
The existing loader throws ConfigError on the first problem it hits.
A customer with a tool-specific equipment.yaml that has six issues
sees one, fixes, restarts, sees the next, fixes, restarts — six
edit-restart cycles before the server even binds.  Day-1 friction
is the top support ticket source in fab integrations.

This commit adds a parallel validator that does a separate read-only
pass and surfaces *every* issue at once:

  $ secs_server --validate-config \
      --config equipment.yaml \
      --state-table control_state.yaml
  [error] equipment.yaml:5  svids[0].type — unknown SECS-II type `WTF`
  [error] equipment.yaml:7  alarms[0].category — value 200 out of range [0, 127]
  [error] equipment.yaml:9  host_commands[0].emit_ceid — CEID 999 not declared in `ceids` section
  3 error(s), 0 warning(s) across 4 files

What it catches:
- Missing required fields (device.model_name, .software_rev, …)
- Range violations (alarm category must be 0–127, spool streams 1–127,
  device.id fits u16, etc.)
- Unknown enum values (SECS-II types, HCACK values, control/PJ/CJ
  state and event names — using the right case + snake convention
  the runtime parsers enforce)
- Duplicate IDs within svids / dvids / ecids / ceids / alarms,
  duplicate PPIDs in recipes, duplicate command names in host_commands
- Referential integrity: host_commands[*].emit_ceid must exist in
  ceids; host_commands[*].set_alarm must exist in alarms;
  emit_on_control_change must exist in ceids
- PJ-table-specific: `NoState` sentinel rejected as `initial`,
  `from`, or `to` (matches loader's existing runtime check)
- yaml-cpp Mark → 1-based line numbers when available

What it doesn't catch (out of scope this round):
- JSON Schema for editor red-squigglies (future)
- Deep semantic checks across state-table reachability
- ECID min/max value parsing (would need numeric type coupling)

Tests cover: clean file passes; multi-error YAML surfaces every issue
on a single pass; line numbers populate; control_state /
process_job_state / control_job_state casing conventions;
format_issues_to renders both severities; the shipped
data/equipment.yaml etc. validate cleanly (regression tripwire if
anyone breaks the demo configs).

INTEGRATION.md §2.3 calls out the flag and suggests CI use.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-09 14:32:09 +02:00

58 lines
2.0 KiB
C++

#pragma once
#include <cstdint>
#include <optional>
#include <ostream>
#include <string>
#include <vector>
// Multi-error config validator.
//
// The existing `config::load_*` functions throw `ConfigError` on the
// first problem they encounter — that's fine for runtime but terrible
// for customers iterating on a new tool's YAML. This validator does a
// separate read-only pass that collects every issue it can find, so a
// `--validate-config` invocation surfaces the whole list in one shot.
//
// Each issue carries the file, line (1-based, from yaml-cpp's Mark when
// available), a structured field path (e.g. "svids[2].type"), and a
// human message. Stream the issues to stderr with
// `format_issues_to(os, issues)` for the canonical presentation.
namespace secsgem::config {
struct ConfigIssue {
enum class Severity : uint8_t { Error = 0, Warning = 1 };
std::string file;
std::optional<int> line; // 1-based; nullopt when unknown
std::string field_path; // e.g. "host_commands[3].emit_ceid"
std::string message;
Severity severity = Severity::Error;
};
class ConfigValidator {
public:
// Each validate_* method reads the given YAML and appends issues.
// Returns false if any new errors were emitted by this call (warnings
// do not flip the return value).
bool validate_equipment(const std::string& yaml_path);
bool validate_control_state(const std::string& yaml_path);
bool validate_process_job_state(const std::string& yaml_path);
bool validate_control_job_state(const std::string& yaml_path);
const std::vector<ConfigIssue>& issues() const { return issues_; }
bool has_errors() const;
std::size_t error_count() const;
std::size_t warning_count() const;
private:
std::vector<ConfigIssue> issues_;
};
// Pretty-prints every issue, one per line, in a format intended for
// stderr. Errors are tagged `[error]`, warnings `[warn]`.
void format_issues_to(std::ostream& os,
const std::vector<ConfigIssue>& issues);
} // namespace secsgem::config