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:
+134
-3
@@ -1,6 +1,10 @@
|
||||
#include "kappa/cli/diagnostic.hpp"
|
||||
#include "kappa/config/eval.hpp"
|
||||
#include "kappa/dsl/parser.hpp"
|
||||
#include "kappa/dsl/system.hpp"
|
||||
#include "kappa/resolve/plan.hpp"
|
||||
#include "kappa/tools/doctor.hpp"
|
||||
#include "kappa/tools/format.hpp"
|
||||
|
||||
#include <cstdlib>
|
||||
#include <fstream>
|
||||
@@ -23,6 +27,9 @@ Subcommands:
|
||||
parse-package <file> Parse and validate a package definition (.kap)
|
||||
parse-config <file> Parse and validate a system configuration
|
||||
validate <file> Validate any kappa file (package or config)
|
||||
format <file> Format a .kap file to canonical style (printed to stdout)
|
||||
doctor <file> Check a .kap file for issues and warnings
|
||||
resolve <config> Resolve a build plan from a system config
|
||||
|
||||
Options:
|
||||
-h, --help Show this help message
|
||||
@@ -89,7 +96,10 @@ int main(int argc, char* argv[]) {
|
||||
|
||||
bool valid_subcommand = (subcommand == "parse-package")
|
||||
|| (subcommand == "parse-config")
|
||||
|| (subcommand == "validate");
|
||||
|| (subcommand == "validate")
|
||||
|| (subcommand == "format")
|
||||
|| (subcommand == "doctor")
|
||||
|| (subcommand == "resolve");
|
||||
|
||||
if (!valid_subcommand) {
|
||||
std::cerr << "error: unknown subcommand '" << subcommand << "'\n\n";
|
||||
@@ -146,8 +156,27 @@ int main(int argc, char* argv[]) {
|
||||
return 0;
|
||||
} catch (const std::runtime_error&) {
|
||||
try {
|
||||
dsl::parse_system_config(source);
|
||||
std::cout << file_arg << ": valid system configuration\n";
|
||||
auto cfg = dsl::parse_system_config(source);
|
||||
auto failures = config::evaluate_assertions(cfg);
|
||||
if (failures.empty()) {
|
||||
std::cout << file_arg << ": valid system configuration ("
|
||||
<< cfg.packages.size() << " packages, "
|
||||
<< cfg.services.size() << " services, "
|
||||
<< cfg.users.size() << " users)\n";
|
||||
} else {
|
||||
std::cerr << file_arg << ": assertion failures\n";
|
||||
for (auto& f : failures) {
|
||||
std::cerr << " \"" << f.message << "\"\n"
|
||||
<< " " << f.field << " = \""
|
||||
<< f.actual << "\"";
|
||||
if (f.expected.empty()) {
|
||||
std::cerr << " (must not be empty)\n";
|
||||
} else {
|
||||
std::cerr << " (expected \"" << f.expected << "\")\n";
|
||||
}
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
} catch (const std::runtime_error& e) {
|
||||
handle_parse_error(file_arg, source, e);
|
||||
@@ -156,5 +185,107 @@ int main(int argc, char* argv[]) {
|
||||
}
|
||||
}
|
||||
|
||||
if (subcommand == "format") {
|
||||
try {
|
||||
auto pkg = dsl::parse(source);
|
||||
tools::format_package(std::cout, pkg);
|
||||
return 0;
|
||||
} catch (const std::runtime_error&) {
|
||||
try {
|
||||
auto cfg = dsl::parse_system_config(source);
|
||||
tools::format_config(std::cout, cfg);
|
||||
return 0;
|
||||
} catch (const std::runtime_error& e) {
|
||||
handle_parse_error(file_arg, source, e);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (subcommand == "doctor") {
|
||||
try {
|
||||
auto pkg = dsl::parse(source);
|
||||
auto diags = tools::check_package(pkg);
|
||||
if (diags.empty()) {
|
||||
std::cout << file_arg << ": no issues found\n";
|
||||
} else {
|
||||
for (auto& d : diags) {
|
||||
std::cerr << (d.severity == tools::DiagSeverity::Error
|
||||
? "error" : "warning")
|
||||
<< ": " << d.message << "\n";
|
||||
}
|
||||
}
|
||||
return diags.empty() ? 0 : 1;
|
||||
} catch (const std::runtime_error&) {
|
||||
try {
|
||||
auto cfg = dsl::parse_system_config(source);
|
||||
auto diags = tools::check_config(cfg);
|
||||
if (diags.empty()) {
|
||||
std::cout << file_arg << ": no issues found\n";
|
||||
} else {
|
||||
for (auto& d : diags) {
|
||||
std::cerr << (d.severity == tools::DiagSeverity::Error
|
||||
? "error" : "warning")
|
||||
<< ": " << d.message << "\n";
|
||||
}
|
||||
}
|
||||
return diags.empty() ? 0 : 1;
|
||||
} catch (const std::runtime_error& e) {
|
||||
handle_parse_error(file_arg, source, e);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (subcommand == "resolve") {
|
||||
try {
|
||||
auto cfg = dsl::parse_system_config(source);
|
||||
|
||||
resolve::Registry registry;
|
||||
auto* pkg_arg = (argc > 3) ? argv[3] : nullptr;
|
||||
if (pkg_arg != nullptr) {
|
||||
auto pkg_src = read_file(pkg_arg);
|
||||
auto pkg = dsl::parse(pkg_src);
|
||||
registry[pkg.name] = std::move(pkg);
|
||||
}
|
||||
|
||||
auto plan = resolve::resolve(cfg, registry);
|
||||
|
||||
if (!plan.missing.empty()) {
|
||||
for (auto& m : plan.missing) {
|
||||
std::cerr << "warning: package '" << m
|
||||
<< "' not found in registry\n";
|
||||
}
|
||||
}
|
||||
|
||||
if (!plan.cycles.empty()) {
|
||||
std::cerr << "error: dependency cycle detected:\n";
|
||||
for (auto& c : plan.cycles) {
|
||||
std::cerr << " " << c << "\n";
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
std::cout << plan.steps.size() << " packages in build order:\n";
|
||||
for (auto& step : plan.steps) {
|
||||
std::cout << " " << step.name << " (" << step.dependencies.size()
|
||||
<< " deps";
|
||||
if (!step.features.empty()) {
|
||||
std::cout << ", features:";
|
||||
for (auto& [k, f] : step.features) {
|
||||
if (f.enabled && !f.flag.empty()) {
|
||||
std::cout << " " << k;
|
||||
}
|
||||
}
|
||||
}
|
||||
std::cout << ")\n";
|
||||
}
|
||||
return plan.steps.empty() && plan.missing.empty() ? 0 : 0;
|
||||
} catch (const std::runtime_error& e) {
|
||||
handle_parse_error(file_arg, source, e);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
return 1; // unreachable
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user