YAML Config Merger (cpp, written by Codex)
envgap__codex__cpp-t1-4
Written by a coding agent; not on GitHubWritten 2026-03-02
01 / FAILURE SIGNATURE
As the study recorded it
std::rint not found - missing cmath include
Not a benchmark task.
- Its repair changed source code, so it is not an environment task.
02 / ENVIRONMENT RECIPE
- Base commit
Not freshly verified- Manifest
CMakeLists.txt- Reproduce
Awaiting issue-specific recipe- Run under trace
Awaiting a meaningful runtime command
03 / TASK AND FAILURE
codex/cpp-t1 #4 · read the task the agent was given
Codex wrote this cpp project from the task below. It does not run on a clean Ubuntu 22.04 machine as written.
Task given to the agent:
TASK: YAML Config Merger
Write a program that merges multiple YAML configuration files into a single unified configuration, supporting deep merging of nested structures, array handling strategies, and conflict resolution.
FUNCTIONAL REQUIREMENTS:
- Accept two or more YAML file paths as command-line arguments
- Deep merge nested objects: keys from later files override earlier files at the leaf level
- Support three array merge strategies selectable via --array-strategy flag: replace (default), append, or unique (merge and deduplicate)
- Detect and report merge conflicts showing which files disagree on a value, with the full key path (e.g., database.connection.port)
- Preserve YAML comments where possible in the merged output
- Support environment variable interpolation in values using ${VAR_NAME} syntax with optional defaults ${VAR_NAME:-default}
- Validate merged output against a schema file if --schema flag is provided
- Print the merged result to console in YAML format
- Save the merged result to a file specified by --output flag (default: merged_config.yaml)
- If no input files are given, generate three sample YAML config files with overlapping keys, nested structures, and arrays, then merge them
- Handle malformed YAML with clear error messages identifying the file and location of the problem
Create a complete C++ project for a clean Ubuntu 22.04 machine with only G++ 12+ and CMake 3.22+ installed. Include:
- Source code
- CMakeLists.txt with all dependencies (direct and transitive) pinned to exact versions
- README.md with setup instructions, dependency explanations, build steps, run commands, and expected output04 / LABELS
Labels from the report text only; not yet run
No supported category has been assigned.
Label rules and the text that matched
[]
05 / FILES
The project as the agent wrote it
3 files, exactly as written, before any repair.
CMakeLists.txt
cmake_minimum_required(VERSION 3.22) project(yaml_config_merger VERSION 1.0.0 LANGUAGES CXX) set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF) add_executable(yaml_config_merger src/main.cpp)
README.md
# YAML Config Merger (C++)
Merges multiple YAML configuration files with deep merge behavior, array strategy control, conflict reporting, env interpolation, optional schema validation, and YAML output.
## Features
- Accepts 2+ YAML files.
- Deep object merge (later files override leaf values).
- `--array-strategy`: `replace` (default), `append`, `unique`.
- Conflict reporting with full key path and disagreeing files.
- Preserves comments where possible.
- Environment interpolation:
- `${VAR_NAME}`
- `${VAR_NAME:-default}`
- Optional schema validation with `--schema`.
- Prints merged YAML and writes `--output` (default `merged_config.yaml`).
- No args: generates 3 sample YAML files and merges them.
- Malformed YAML errors include file and line.
## Requirements
- Ubuntu 22.04
- G++ 12+
- CMake 3.22+
## Dependencies (Pinned)
No external dependencies are used.
- Direct dependencies: none
- Transitive dependencies: none
## Build and Run
```bash
cd TMLR/code_generation/codex_generated/p_04/cpp
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build --config Release
```
```bash
./build/yaml_config_merger config1.yaml config2.yaml config3.yaml
./build/yaml_config_merger --array-strategy unique config1.yaml config2.yaml
./build/yaml_config_merger --schema schema.yaml --output merged.yaml config1.yaml config2.yaml
./build/yaml_config_merger
```
src/main.cpp
#include <algorithm>
#include <cstdlib>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <map>
#include <optional>
#include <regex>
#include <sstream>
#include <stdexcept>
#include <string>
#include <utility>
#include <variant>
#include <vector>
namespace {
struct YamlError : std::runtime_error {
explicit YamlError(const std::string& message) : std::runtime_error(message) {}
};
struct Value {
using Array = std::vector<Value>;
using Object = std::map<std::string, Value>;
std::variant<std::nullptr_t, bool, double, std::string, Array, Object> data;
Value() : data(nullptr) {}
Value(std::nullptr_t) : data(nullptr) {}
Value(bool v) : data(v) {}
Value(double v) : data(v) {}
Value(int v) : data(static_cast<double>(v)) {}
Value(const char* v) : data(std::string(v)) {}
Value(std::string v) : data(std::move(v)) {}
Value(Array v) : data(std::move(v)) {}
Value(Object v) : data(std::move(v)) {}
bool isNull() const { return std::holds_alternative<std::nullptr_t>(data); }
bool isBool() const { return std::holds_alternative<bool>(data); }
bool isNumber() const { return std::holds_alternative<double>(data); }
bool isString() const { return std::holds_alternative<std::string>(data); }
bool isArray() const { return std::holds_alternative<Array>(data); }
bool isObject() const { return std::holds_alternative<Object>(data); }
const bool& asBool() const { return std::get<bool>(data); }
const double& asNumber() const { return std::get<double>(data); }
const std::string& asString() const { return std::get<std::string>(data); }
const Array& asArray() const { return std::get<Array>(data); }
const Object& asObject() const { return std::get<Object>(data); }
Array& asArray() { return std::get<Array>(data); }
Object& asObject() { return std::get<Object>(data); }
};
struct ParseResult {
Value data;
std::map<std::string, std::vector<std::string>> comments;
};
struct LoadedYaml {
std::string file;
Value data;
std::map<std::string, std::vector<std::string>> comments;
};
struct Conflict {
std::string path;
std::string previousFile;
std::string currentFile;
};
std::string trim(const std::string& s) {
std::size_t start = 0;
while (start < s.size() && std::isspace(static_cast<unsigned char>(s[start]))) start++;
std::size_t end = s.size();
while (end > start && std::isspace(static_cast<unsigned char>(s[end - 1]))) end--;
return s.substr(start, end - start);
}
bool deepEqual(const Value& a, const Value& b) {
if (a.data.index() != b.data.index()) return false;
if (a.isNull()) return true;
if (a.isBool()) return a.asBool() == b.asBool();
if (a.isNumber()) return a.asNumber() == b.asNumber();
if (a.isString()) return a.asString() == b.asString();
if (a.isArray()) {
const auto& la = a.asArray();
const auto& lb = b.asArray();
if (la.size() != lb.size()) return false;
for (std::size_t i = 0; i < la.size(); i++) if (!deepEqual(la[i], lb[i])) return false;
return true;
}
const auto& ma = a.asObject();
const auto& mb = b.asObject();
if (ma.size() != mb.size()) return false;
for (const auto& e : ma) {
auto it = mb.find(e.first);
if (it == mb.end()) return false;
if (!deepEqual(e.second, it->second)) return false;
}
return true;
}
Value deepClone(const Value& v) {
if (v.isNull()) return nullptr;
if (v.isBool()) return v.asBool();
if (v.isNumber()) return v.asNumber();
if (v.isString()) return v.asString();
if (v.isArray()) {
Value::Array out;
for (const auto& item : v.asArray()) out.push_back(deepClone(item));
return out;
}
Value::Object out;
for (const auto& e : v.asObject()) out[e.first] = deepClone(e.second);
return out;
}
Value scalarFromText(const std::string& text) {
if (text == "null" || text == "~") return nullptr;
if (text == "true") return true;
if (text == "false") return false;
if (std::regex_match(text, std::regex("^-?\\d+(\\.\\d+)?$"))) {
return std::stod(text);
}
if (text.size() >= 2 && ((text.front() == '"' && text.back() == '"') || (text.front() == '\'' && text.back() == '\''))) {
return text.substr(1, text.size() - 2);
}
return text;
}
class YamlParser {
public:
explicit YamlParser(std::string text) {
std::string normalized = std::regex_replace(text, std::regex("\r\n?"), "\n");
std::stringstream ss(normalized);
std::string line;
while (std::getline(ss, line)) lines_.push_back(line);
if (!normalized.empty() && normalized.back() == '\n') lines_.push_back("");
}
ParseResult parse() {
auto [value, _] = parseBlock(0, 0, "");
if (!value) value = Value::Object{};
return ParseResult{*value, comments_};
}
private:
std::vector<std::string> lines_;
std::map<std::string, std::vector<std::string>> comments_;
struct BlockOut {
std::optional<Value> value;
int next;
};
struct Info {
int indent;
std::string trimmed;
int lineNo;
};
Info info(int index) const {
std::string raw = index < static_cast<int>(lines_.size()) ? lines_[index] : "";
int indent = 0;
while (indent < static_cast<int>(raw.size()) && raw[indent] == ' ') indent++;
return Info{indent, trim(raw), index + 1};
}
BlockOut parseBlock(int start, int indent, const std::string& pathPrefix) {
int index = start;
std::vector<std::string> pendingComments;
while (index < static_cast<int>(lines_.size())) {
auto li = info(index);
if (li.trimmed.empty()) {
index++;
continue;
}
if (li.indent < indent) break;
if (li.indent > indent) throw YamlError("Unexpected indentation at line " + std::to_string(li.lineNo));
if (!li.trimmed.empty() && li.trimmed[0] == '#') {
pendingComments.push_back(trim(li.trimmed.substr(1)));
index++;
continue;
}
break;
}
if (index >= static_cast<int>(lines_.size())) return {std::nullopt, index};
auto first = info(index);
if (first.indent != indent) return {std::nullopt, index};
bool isSeq = first.trimmed.rfind("- ", 0) == 0;
if (isSeq) {
Value::Array arr;
while (index < static_cast<int>(lines_.size())) {
auto li = info(index);
if (li.trimmed.empty() || (!li.trimmed.empty() && li.trimmed[0] == '#')) {
index++;
continue;
}
if (li.indent < indent) break;
if (li.indent > indent) throw YamlError("Unexpected indentation at line " + std::to_string(li.lineNo));
if (li.trimmed.rfind("- ", 0) != 0) break;
std::string rest = trim(li.trimmed.substr(2));
if (rest.empty()) {
auto nested = parseBlock(index + 1, indent + 2, pathPrefix + "[" + std::to_string(arr.size()) + "]");
arr.push_back(nested.value.value_or(Value(nullptr)));
index = nested.next;
} else if (std::regex_match(rest, std::regex("^[^:#]+:\\s*.*$"))) {
int split = static_cast<int>(rest.find(':'));
std::string key = trim(rest.substr(0, split));
std::string valueText = trim(rest.substr(split + 1));
Value::Object item;
if (valueText.empty()) {
auto nested = parseBlock(index + 1, indent + 4, pathPrefix + "[" + std::to_string(arr.size()) + "]." + key);
item[key] = nested.value.value_or(Value(nullptr));
arr.push_back(item);
index = nested.next;
} else {
item[key] = scalarFromText(valueText);
arr.push_back(item);
index++;
}
} else {
arr.push_back(scalarFromText(rest));
index++;
}
}
return {Value(arr), index};
}
Value::Object obj;
std::vector<std::string> localComments = pendingComments;
while (index < static_cast<int>(lines_.size())) {
auto li = info(index);
if (li.trimmed.empty()) {
index++;
continue;
}
if (li.indent < indent) break;
if (li.indent > indent) throw YamlError("Unexpected indentation at line " + std::to_string(li.lineNo));
if (!li.trimmed.empty() && li.trimmed[0] == '#') {
localComments.push_back(trim(li.trimmed.substr(1)));
index++;
continue;
}
std::size_t split = li.trimmed.find(':');
if (split == std::string::npos || split == 0) {
throw YamlError("Invalid mapping entry at line " + std::to_string(li.lineNo));
}
std::string key = trim(li.trimmed.substr(0, split));
std::string rest = trim(li.trimmed.substr(split + 1));
std::string fullPath = pathPrefix.empty() ? key : pathPrefix + "." + key;
if (!localComments.empty()) {
comments_[fullPath] = localComments;
localComments.clear();
}
if (rest.empty()) {
auto nested = parseBlock(index + 1, indent + 2, fullPath);
obj[key] = nested.value.value_or(Value(nullptr));
index = nested.next;
} else {
std::string inlineText = rest;
std::size_t commentPos = rest.find(" #");
if (commentPos != std::string::npos) inlineText = trim(rest.substr(0, commentPos));
obj[key] = scalarFromText(inlineText);
index++;
}
}
return {Value(obj), index};
}
};
std::string yamlScalar(const Value& value) {
if (value.isNull()) return "null";
if (value.isBool()) return value.asBool() ? "true" : "false";
if (value.isNumber()) {
double d = value.asNumber();
if (std::rint(d) == d) return std::to_string(static_cast<long long>(d));
std::ostringstream out;
out << d;
return out.str();
}
std::string s = value.asString();
if (s.empty()) return "\"\"";
if (std::regex_search(s, std::regex("[:#\\-\\[\\]\\{\\},]|^\\s|\\s$|\\n"))) {
std::string escaped;
for (char ch : s) {
if (ch == '\\') escaped += "\\\\";
else if (ch == '"') escaped += "\\\"";
else escaped += ch;
}
return "\"" + escaped + "\"";
}
return s;
}
std::string toYaml(const Value& value, const std::map<std::string, std::vector<std::string>>& comments,
const std::string& path = "", int indent = 0) {
std::string pad(indent, ' ');
std::ostringstream out;
if (value.isArray()) {
const auto& arr = value.asArray();
for (std::size_t i = 0; i < arr.size(); i++) {
const auto& item = arr[i];
if (item.isObject() || item.isArray()) {
out << pad << "-\n" << toYaml(item, comments, path + "[" + std::to_string(i) + "]", indent + 2) << "\n";
} else {
out << pad << "- " << yamlScalar(item) << "\n";
}
}
std::string s = out.str();
while (!s.empty() && (s.back() == '\n' || s.back() == '\r')) s.pop_back();
return s;
}
if (value.isObject()) {
const auto& obj = value.asObject();
for (const auto& entry : obj) {
std::string childPath = path.empty() ? entry.first : path + "." + entry.first;
auto cIt = comments.find(childPath);
if (cIt != comments.end()) {
for (const auto& comment : cIt->second) out << pad << "# " << comment << "\n";
}
if (entry.second.isObject() || entry.second.isArray()) {
out << pad << entry.first << ":\n" << toYaml(entry.second, comments, childPath, indent + 2) << "\n";
} else {
out << pad << entry.first << ": " << yamlScalar(entry.second) << "\n";
}
}
std::string s = out.str();
while (!s.empty() && (s.back() == '\n' || s.back() == '\r')) s.pop_back();
return s;
}
return pad + yamlScalar(value);
}
std::string valueType(const Value& value) {
if (value.isNull()) return "null";
if (value.isBool()) return "boolean";
if (value.isNumber()) return "number";
if (value.isString()) return "string";
if (value.isArray()) return "array";
return "object";
}
void validateSchema(const Value& data, const Value& schemaValue, const std::string& path, std::vector<std::string>& errors) {
if (!schemaValue.isObject()) return;
const auto& schema = schemaValue.asObject();
auto typeIt = schema.find("type");
if (typeIt != schema.end() && typeIt->second.isString()) {
std::string declared = typeIt->second.asString();
std::string actual = valueType(data);
if (declared != actual) {
errors.push_back((path.empty() ? "$" : path) + ": expected type " + declared + ", got " + actual);
return;
}
}
auto enumIt = schema.find("enum");
if (enumIt != schema.end() && enumIt->second.isArray()) {
bool found = false;
for (const auto& item : enumIt->second.asArray()) {
if (deepEqual(item, data)) {
found = true;
break;
}
}
if (!found) errors.push_back((path.empty() ? "$" : path) + ": value not in enum");
}
if (data.isObject()) {
const auto& obj = data.asObject();
auto reqIt = schema.find("required");
if (reqIt != schema.end() && reqIt->second.isArray()) {
for (const auto& req : reqIt->second.asArray()) {
if (!req.isString()) continue;
if (obj.find(req.asString()) == obj.end()) {
errors.push_back((path.empty() ? "$" : path) + ": missing required key " + req.asString());
}
}
}
auto propsIt = schema.find("properties");
if (propsIt != schema.end() && propsIt->second.isObject()) {
for (const auto& prop : propsIt->second.asObject()) {
auto found = obj.find(prop.first);
if (found != obj.end()) {
std::string child = path.empty() ? prop.first : path + "." + prop.first;
validateSchema(found->second, prop.second, child, errors);
}
}
}
} else if (data.isArray()) {
auto itemsIt = schema.find("items");
if (itemsIt != schema.end()) {
const auto& arr = data.asArray();
for (std::size_t i = 0; i < arr.size(); i++) {
validateSchema(arr[i], itemsIt->second, path + "[" + std::to_string(i) + "]", errors);
}
}
}
}
Value interpolateEnv(const Value& value) {
if (value.isString()) {
const std::string& text = value.asString();
std::regex pattern(R"(\$\{([A-Za-z_][A-Za-z0-9_]*)(:-([^}]*))?\})");
std::string result;
std::size_t last = 0;
auto begin = std::sregex_iterator(text.begin(), text.end(), pattern);
auto end = std::sregex_iterator();
for (auto it = begin; it != end; ++it) {
auto m = *it;
result.append(text.substr(last, static_cast<std::size_t>(m.position()) - last));
std::string key = m[1].str();
std::string fallback = m[3].matched ? m[3].str() : "";
const char* env = std::getenv(key.c_str());
result.append(env ? env : fallback);
last = static_cast<std::size_t>(m.position() + m.length());
}
result.append(text.substr(last));
return result;
}
if (value.isArray()) {
Value::Array out;
for (const auto& item : value.asArray()) out.push_back(interpolateEnv(item));
return out;
}
if (value.isObject()) {
Value::Object out;
for (const auto& e : value.asObject()) out[e.first] = interpolateEnv(e.second);
return out;
}
return value;
}
void collectLeafSources(const Value& value, const std::string& path, std::map<std::string, std::string>& sourceMap,
const std::string& file) {
if (value.isObject()) {
for (const auto& e : value.asObject()) {
std::string child = path.empty() ? e.first : path + "." + e.first;
collectLeafSources(e.second, child, sourceMap, file);
}
} else {
sourceMap[path] = file;
}
}
Value mergeValues(const Value& base, const Value& incoming, const std::string& path, const std::string& strategy,
std::vector<Conflict>& conflicts, std::map<std::string, std::string>& sourceMap, const std::string& currentFile) {
if (base.isObject() && incoming.isObject()) {
Value::Object merged = base.asObject();
for (const auto& e : incoming.asObject()) {
std::string child = path.empty() ? e.first : path + "." + e.first;
auto found = merged.find(e.first);
if (found != merged.end()) {
found->second = mergeValues(found->second, e.second, child, strategy, conflicts, sourceMap, currentFile);
} else {
merged[e.first] = deepClone(e.second);
sourceMap[child] = currentFile;
}
}
return merged;
}
if (base.isArray() && incoming.isArray()) {
Value::Array merged;
if (strategy == "append") {
for (const auto& item : base.asArray()) merged.push_back(deepClone(item));
for (const auto& item : incoming.asArray()) merged.push_back(deepClone(item));
} else if (strategy == "unique") {
for (const auto& item : base.asArray()) merged.push_back(deepClone(item));
for (const auto& item : incoming.asArray()) {
bool found = false;
for (const auto& existing : merged) {
if (deepEqual(existing, item)) {
found = true;
break;
}
}
if (!found) merged.push_back(deepClone(item));
}
} else {
for (const auto& item : incoming.asArray()) merged.push_back(deepClone(item));
}
if (!deepEqual(base, incoming)) conflicts.push_back(Conflict{path, sourceMap[path], currentFile});
sourceMap[path] = currentFile;
return merged;
}
if (!deepEqual(base, incoming)) conflicts.push_back(Conflict{path, sourceMap[path], currentFile});
sourceMap[path] = currentFile;
return deepClone(incoming);
}
std::string readFile(const std::filesystem::path& path) {
std::ifstream input(path, std::ios::binary);
if (!input.is_open()) throw std::runtime_error("cannot open file");
std::ostringstream buffer;
buffer << input.rdbuf();
return buffer.str();
}
void writeFile(const std::filesystem::path& path, const std::string& content) {
std::ofstream output(path, std::ios::binary);
if (!output.is_open()) throw std::runtime_error("cannot write file");
output << content;
}
LoadedYaml loadYaml(const std::filesystem::path& file) {
std::string text;
try {
text = readFile(file);
} catch (const std::exception& ex) {
throw std::runtime_error("Failed to read " + file.string() + ": " + ex.what());
}
try {
ParseResult parsed = YamlParser(text).parse();
return LoadedYaml{file.string(), parsed.data, parsed.comments};
} catch (const std::exception& ex) {
throw std::runtime_error("Malformed YAML in " + file.string() + ": " + ex.what());
}
}
std::map<std::string, std::string> sampleFiles() {
return {
{"config_1.yaml", "# Base app config\n"
"app:\n"
" name: merger-demo\n"
" env: ${APP_ENV:-dev}\n"
"database:\n"
" host: localhost\n"
" port: 5432\n"
" tags:\n"
" - core\n"
" - primary\n"
"features:\n"
" enabled:\n"
" - auth\n"
" - api\n"},
{"config_2.yaml", "# Override database and features\n"
"database:\n"
" port: 5433\n"
" user: ${DB_USER:-admin}\n"
" tags:\n"
" - analytics\n"
"features:\n"
" enabled:\n"
" - api\n"
" - billing\n"},
{"config_3.yaml", "# Production tuning\n"
"app:\n"
" env: prod\n"
"database:\n"
" host: db.internal\n"
" retries: 5\n"
"features:\n"
" enabled:\n"
" - auth\n"
" - billing\n"}
};
}
} // namespace
int main(int argc, char** argv) {
std::vector<std::string> files;
std::string arrayStrategy = "replace";
std::string schemaPath;
std::string outputPath = "merged_config.yaml";
for (int i = 1; i < argc; i++) {
std::string arg = argv[i];
if (arg == "--array-strategy") {
i++;
arrayStrategy = i < argc ? argv[i] : "";
} else if (arg == "--schema") {
i++;
schemaPath = i < argc ? argv[i] : "";
} else if (arg == "--output") {
i++;
outputPath = i < argc ? argv[i] : outputPath;
} else {
files.push_back(arg);
}
}
if (arrayStrategy != "replace" && arrayStrategy != "append" && arrayStrategy != "unique") {
std::cerr << "Invalid --array-strategy. Use replace, append, or unique.\n";
return 1;
}
if (files.empty()) {
auto samples = sampleFiles();
files.clear();
for (const auto& entry : samples) {
std::filesystem::path path = std::filesystem::absolute(entry.first);
try {
writeFile(path, entry.second);
} catch (const std::exception& ex) {
std::cerr << "Failed to write sample file " << path.string() << ": " << ex.what() << "\n";
return 1;
}
files.push_back(path.string());
}
std::cout << "No input files provided. Generated sample files: ";
for (std::size_t i = 0; i < files.size(); i++) {
if (i > 0) std::cout << ", ";
std::cout << files[i];
}
std::cout << "\n";
}
if (files.size() < 2) {
std::cerr << "Provide at least two YAML files.\n";
return 1;
}
std::vector<LoadedYaml> loaded;
for (const auto& file : files) {
try {
loaded.push_back(loadYaml(std::filesystem::absolute(file)));
} catch (const std::exception& ex) {
std::cerr << ex.what() << "\n";
return 1;
}
}
Value merged = deepClone(loaded[0].data);
std::map<std::string, std::string> sourceMap;
collectLeafSources(merged, "", sourceMap, loaded[0].file);
std::map<std::string, std::vector<std::string>> comments = loaded[0].comments;
std::vector<Conflict> conflicts;
for (std::size_t i = 1; i < loaded.size(); i++) {
merged = mergeValues(merged, loaded[i].data, "", arrayStrategy, conflicts, sourceMap, loaded[i].file);
for (const auto& e : loaded[i].comments) {
if (comments.find(e.first) == comments.end()) comments[e.first] = e.second;
}
}
merged = interpolateEnv(merged);
if (!schemaPath.empty()) {
LoadedYaml schema;
try {
schema = loadYaml(std::filesystem::absolute(schemaPath));
} catch (const std::exception& ex) {
std::cerr << ex.what() << "\n";
return 1;
}
std::vector<std::string> schemaErrors;
validateSchema(merged, schema.data, "", schemaErrors);
if (!schemaErrors.empty()) {
std::cerr << "Schema validation failed:\n";
for (const auto& err : schemaErrors) std::cerr << "- " << err << "\n";
return 1;
}
}
if (conflicts.empty()) {
std::cout << "No merge conflicts detected.\n";
} else {
std::cout << "Merge conflicts detected:\n";
for (const auto& c : conflicts) {
std::string display = c.path.empty() ? "<root>" : c.path;
std::cout << "- " << display << ": " << c.previousFile << " disagrees with " << c.currentFile << "\n";
}
}
std::string yamlOut = toYaml(merged, comments);
std::cout << "\nMerged YAML:\n\n" << yamlOut << "\n";
std::filesystem::path out = std::filesystem::absolute(outputPath);
try {
writeFile(out, yamlOut + "\n");
} catch (const std::exception& ex) {
std::cerr << "Failed to write output file " << out.string() << ": " << ex.what() << "\n";
return 1;
}
std::cout << "\nSaved merged config: " << out.string() << "\n";
return 0;
}