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
+296
View File
@@ -0,0 +1,296 @@
#include "kappa/tools/format.hpp"
namespace kappa::tools {
namespace {
struct Indent {
int n = 0;
Indent(int i) : n(i) {}
friend std::ostream& operator<<(std::ostream& os, const Indent& in) {
for (int i = 0; i < in.n; ++i) { os << " "; }
return os;
}
};
static void write_env(std::ostream& os, int d,
const std::vector<dsl::EnvEntry>& entries) {
if (entries.empty()) { return; }
os << Indent(d) << "env {\n";
for (auto& e : entries) {
os << Indent(d + 1) << e.key << (e.soft ? " ?= " : " = ")
<< '"' << e.value << "\"\n";
}
os << Indent(d) << "}\n";
}
static void write_features(std::ostream& os, int d,
const std::unordered_map<std::string, dsl::FeatureDef>& feats) {
if (feats.empty()) { return; }
os << Indent(d) << "features {\n";
for (auto& [k, f] : feats) {
if (f.flag.empty() && !f.force) {
os << Indent(d + 1) << k << " = " << (f.enabled ? "true" : "false") << "\n";
} else {
os << Indent(d + 1) << k << " = { enabled = "
<< (f.enabled ? "true" : "false");
if (f.force) { os << ", force = true"; }
if (!f.flag.empty()) { os << ", flag = \"" << f.flag << "\""; }
os << " }\n";
}
}
os << Indent(d) << "}\n";
}
static void write_phase(std::ostream& os, int d, const char* name,
const dsl::Phase& phase) {
if (phase.commands.empty()) { return; }
os << Indent(d) << name << " {\n";
for (auto& cmd : phase.commands) {
os << Indent(d + 1) << cmd << "\n";
}
os << Indent(d) << "}\n";
}
static void write_boot_block(std::ostream& os, int d, const dsl::BootBlock& boot) {
os << Indent(d) << "boot {\n";
auto w = [&](const char* k, const std::string& v) {
if (!v.empty()) { os << Indent(d + 1) << k << " = \"" << v << "\"\n"; }
};
w("kernel", boot.kernel);
w("init", boot.init);
w("efi", boot.efi);
w("swap", boot.swap);
w("root", boot.root);
w("bootloader", boot.bootloader);
for (auto& [k, v] : boot.params) {
os << Indent(d + 1) << k << " = \"" << v << "\"\n";
}
os << Indent(d) << "}\n";
}
} // namespace
void format_package(std::ostream& os, const dsl::PackageDef& pkg) {
os << "package \"" << pkg.name << "\" {\n";
int d = 1;
if (!pkg.version.empty()) {
auto c = pkg.const_keys.contains("version") ? "const " : "";
os << Indent(d) << c << "version = \"" << pkg.version << "\"\n";
}
if (!pkg.source.empty()) {
auto c = pkg.const_keys.contains("source") ? "const " : "";
os << Indent(d) << c << "source = \"" << pkg.source << "\"\n";
}
if (!pkg.license.empty()) {
os << Indent(d) << "license = \"" << pkg.license << "\"\n";
}
if (!pkg.provides.empty()) {
os << Indent(d) << "provides = [";
for (std::size_t i = 0; i < pkg.provides.size(); ++i) {
if (i > 0) { os << ", "; }
os << '"' << pkg.provides[i] << '"';
}
os << "]\n";
}
if (!pkg.outputs.empty()) {
os << Indent(d) << "outputs = [";
for (std::size_t i = 0; i < pkg.outputs.size(); ++i) {
if (i > 0) { os << ", "; }
os << '"' << pkg.outputs[i] << '"';
}
os << "]\n";
}
if (!pkg.patches.empty()) {
os << Indent(d) << "patches = [\n";
for (auto& p : pkg.patches) {
if (p.sha256.empty()) {
os << Indent(d + 1) << '"' << p.url << "\",\n";
} else {
os << Indent(d + 1) << "{ url = \"" << p.url
<< "\", sha256 = \"" << p.sha256
<< "\", level = " << p.level << " },\n";
}
}
os << Indent(d) << "]\n";
}
if (!pkg.depends.empty()) {
os << Indent(d) << "depends = [\n";
for (auto& dep : pkg.depends) {
os << Indent(d + 1);
if (dep.version.empty() && dep.output.empty() && dep.feature.empty()) {
os << '"' << dep.name << '"';
} else {
os << "{ name = \"" << dep.name << '"';
if (!dep.version.empty()) { os << ", version = \"" << dep.version << '"'; }
if (!dep.output.empty()) { os << ", output = \"" << dep.output << '"'; }
if (!dep.feature.empty()) { os << ", feature = \"" << dep.feature << '"'; }
os << " }";
}
os << ",\n";
}
os << Indent(d) << "]\n";
}
write_features(os, d, pkg.features);
write_env(os, d, pkg.env_entries);
for (auto& cf : pkg.config_files) {
os << Indent(d) << "config {\n";
os << Indent(d + 1) << "file \"" << cf.path
<< "\" mode = \"" << cf.mode << "\" {\n";
for (auto& [k, v] : cf.entries) {
os << Indent(d + 2) << k << " = " << v << "\n";
}
os << Indent(d + 1) << "}\n";
os << Indent(d) << "}\n";
}
if (!pkg.service.empty()) {
os << Indent(d) << "service {\n";
for (auto& [init, si] : pkg.service) {
os << Indent(d + 1) << init << " {\n";
if (!si.exec.empty()) { os << Indent(d + 2) << "exec = \"" << si.exec << "\"\n"; }
if (!si.type.empty()) { os << Indent(d + 2) << "type = \"" << si.type << "\"\n"; }
if (!si.user.empty()) { os << Indent(d + 2) << "user = \"" << si.user << "\"\n"; }
if (!si.ports.empty()) {
os << Indent(d + 2) << "ports = [";
for (std::size_t i = 0; i < si.ports.size(); ++i) {
if (i > 0) { os << ", "; }
os << si.ports[i];
}
os << "]\n";
}
for (auto& [k, v] : si.env) {
os << Indent(d + 2) << k << " = \"" << v << "\"\n";
}
os << Indent(d + 1) << "}\n";
}
os << Indent(d) << "}\n";
}
if (!pkg.assertions.empty()) {
os << Indent(d) << "assert {\n";
for (auto& a : pkg.assertions) {
os << Indent(d + 1) << '"' << a.message << "\" : "
<< a.field << " " << a.op;
if (!a.value.empty()) { os << " \"" << a.value << '"'; }
os << "\n";
}
os << Indent(d) << "}\n";
}
write_phase(os, d, "prepare", pkg.prepare);
write_phase(os, d, "build", pkg.build);
write_phase(os, d, "check", pkg.check);
write_phase(os, d, "install", pkg.install);
os << "}\n";
}
void format_config(std::ostream& os, const dsl::SystemConfig& cfg) {
if (!cfg.imports.empty()) {
os << "imports = [";
for (std::size_t i = 0; i < cfg.imports.size(); ++i) {
if (i > 0) { os << ", "; }
os << '"' << cfg.imports[i] << '"';
}
os << "]\n\n";
}
if (!cfg.assertions.empty()) {
os << "assert {\n";
for (auto& a : cfg.assertions) {
os << " \"" << a.message << "\" : "
<< a.field << " " << a.op;
if (!a.value.empty()) { os << " \"" << a.value << '"'; }
os << "\n";
}
os << "}\n\n";
}
auto& s = cfg.system;
os << "system {\n";
if (!s.hostname.empty()) { os << " hostname = \"" << s.hostname << "\"\n"; }
if (!s.timezone.empty()) { os << " timezone = \"" << s.timezone << "\"\n"; }
write_features(os, 1, s.features);
write_env(os, 1, s.env);
if (!s.config.empty()) {
os << " config {\n";
for (auto& [k, v] : s.config) {
os << " " << k << " = \"" << v << "\"\n";
}
os << " }\n";
}
if (s.rollback.keep > 0) {
os << " rollback {\n keep = " << s.rollback.keep << "\n }\n";
}
os << "}\n\n";
if (!cfg.packages.empty()) {
os << "packages {\n";
for (auto& p : cfg.packages) {
os << " " << p.name;
if (p.version.empty() && p.features.empty() && p.config.empty()) {
os << " {}\n";
continue;
}
os << " {\n";
if (!p.version.empty()) { os << " version = \"" << p.version << "\"\n"; }
write_features(os, 2, p.features);
if (!p.config.empty()) {
os << " config {\n";
for (auto& [k, v] : p.config) {
os << " " << k << " = " << v << "\n";
}
os << " }\n";
}
os << " }\n";
}
os << "}\n\n";
}
if (!cfg.services.empty()) {
os << "services {\n";
for (auto& svc : cfg.services) {
os << " " << svc.name << " {\n";
os << " enable = " << (svc.enable ? "true" : "false") << "\n";
for (auto& [k, v] : svc.config) {
os << " " << k << " = " << v << "\n";
}
os << " }\n";
}
os << "}\n\n";
}
write_boot_block(os, 0, cfg.boot);
os << "\n";
if (!cfg.users.empty()) {
os << "users {\n";
for (auto& u : cfg.users) {
os << " " << u.name << " {\n";
if (!u.shell.empty()) { os << " shell = \"" << u.shell << "\"\n"; }
if (!u.groups.empty()) {
os << " groups = [";
for (std::size_t i = 0; i < u.groups.size(); ++i) {
if (i > 0) { os << ", "; }
os << '"' << u.groups[i] << '"';
}
os << "]\n";
}
for (auto& [k, v] : u.extra) {
os << " " << k << " = \"" << v << "\"\n";
}
os << " }\n";
}
os << "}\n";
}
}
} // namespace kappa::tools