feat: dependency resolver (Phase 1) + formatter + doctor + assertion evaluation

Resolver (Phase 1):
- resolve::resolve() takes SystemConfig + Registry → BuildPlan
- Three-layer feature/config merge (uses config::resolve_package)
- Feature-gated dependencies: deps with feature=X skipped if feature disabled
- Topological sort via Kahn's algorithm (BFS on in-degree)
- Cycle detection, missing package warnings
- CLI: kappa resolve <config> [<package>]

Formatter:
- format_package() + format_config() → canonical output
- Consistent 4-space indent, canonical declaration order

Doctor:
- check_package() + check_config() → warnings for common issues
- Missing fields, empty configs, root shell, feature warnings

Assertion evaluation:
- evaluate_assertions() resolves dotted field paths
- Supports == and != operators for config validation
- kappa validate now runs assertion checks

Review fixes:
- plan.steps.empty() exit code corrected to 0
- Removed unused <unordered_set> include
- Eliminated duplicate merge logic (uses config::resolve_package)
This commit is contained in:
2026-07-30 00:36:38 -04:00
parent 0c1e82d66b
commit ee9f280346
10 changed files with 746 additions and 23 deletions
+84
View File
@@ -0,0 +1,84 @@
#include "kappa/config/eval.hpp"
namespace kappa::config {
std::string resolve_field(const dsl::SystemConfig& cfg, std::string_view path) {
auto dot = path.find('.');
auto ns = path.substr(0, dot);
if (ns == "boot") {
auto key = path.substr(dot + 1);
if (key == "kernel") { return cfg.boot.kernel; }
if (key == "init") { return cfg.boot.init; }
if (key == "efi") { return cfg.boot.efi; }
if (key == "swap") { return cfg.boot.swap; }
if (key == "root") { return cfg.boot.root; }
if (key == "bootloader") { return cfg.boot.bootloader; }
auto it = cfg.boot.params.find(std::string(key));
if (it != cfg.boot.params.end()) { return it->second; }
}
if (ns == "system") {
auto key = path.substr(dot + 1);
if (key == "hostname") { return cfg.system.hostname; }
if (key == "timezone") { return cfg.system.timezone; }
}
if (ns == "features") {
auto key = path.substr(dot + 1);
auto it = cfg.system.features.find(std::string(key));
if (it != cfg.system.features.end()) {
return it->second.enabled ? "true" : "false";
}
}
return "";
}
std::vector<AssertFailure> evaluate_assertions(const dsl::SystemConfig& cfg) {
std::vector<AssertFailure> failures;
for (auto& a : cfg.assertions) {
auto actual = resolve_field(cfg, a.field);
bool pass = false;
if (a.op == "==") {
pass = (actual == a.value);
} else if (a.op == "!=") {
pass = (actual != a.value);
}
if (!pass) {
failures.push_back({a.message, a.field, a.value, actual});
}
}
return failures;
}
std::unordered_map<std::string, dsl::ServiceInit> resolve_services(
const dsl::SystemConfig& cfg,
const std::unordered_map<std::string, dsl::PackageDef>& packages)
{
std::unordered_map<std::string, dsl::ServiceInit> resolved;
auto init_system = cfg.boot.init;
for (auto& svc : cfg.services) {
if (!svc.enable) { continue; }
auto pit = packages.find(svc.name);
if (pit == packages.end()) { continue; }
auto& pkg = pit->second;
if (pkg.service.empty()) { continue; }
auto it = pkg.service.find(init_system);
if (it != pkg.service.end()) {
resolved[svc.name] = it->second;
}
}
return resolved;
}
} // namespace kappa::config