///|
/// Policy bundle manifest and migration.
///
/// A bundle is a named, versioned rule set with a dependency-ordered
/// migration list: each migration declares the API version it targets,
/// its compiled program entries and the deprecations it removes. The
/// bundle_validate operation checks structural integrity (unique
/// versions, strictly ascending targets, non-empty rules per version,
/// migration chaining), reports the resolved chain from a requested
/// source version to the bundle head, and emits the final program a
/// caller would run after applying every step.
///|
fn bundle_validate_request(request : Value) -> Value raise {
let versions : Map[String, Bool] = Map([])
let manifest = arr(get(request, "versions"))
if manifest.is_empty() {
raise input_error("E3002", "bundle needs at least one version")
}
let steps : Array[Value] = []
let chain : Array[String] = []
let mut previous : String? = None
let mut rules_total = 0
for item in manifest {
let version = str(get(item, "version"))
if versions.contains(version) {
raise input_error("E4001", "duplicate bundle version: " + version)
}
versions[version] = true
match previous {
None => ()
Some(before) => chain.push(before + " -> " + version)
}
previous = Some(version)
let rules = arr(get(item, "rules"))
if rules.is_empty() {
raise input_error("E4003", "bundle version " + version + " has no rules")
}
let compiled = compile_rules_request(record([("rules", Array(rules))]))
let program = get(compiled, "program")
rules_total += rules.length()
let deprecations = match get(item, "deprecates") {
Null => []
listed => string_list(listed)
}
for removed in deprecations {
if !versions.contains(removed) {
raise input_error(
"E4004",
"version " + version + " deprecates unknown version " + removed,
)
}
}
steps.push(
record([
("version", String(version)),
("rule_count", Number(rules.length().to_double())),
("program", program),
("duplicate_conditions", get(compiled, "duplicate_conditions")),
("deprecates", strings(deprecations)),
]),
)
}
let head = match previous {
Some(last) => last
None => raise input_error("E3002", "bundle needs at least one version")
}
record([
("head", String(head)),
("chain", strings(chain)),
("steps", Array(steps)),
("rule_count_total", Number(rules_total.to_double())),
])
}
///|
/// Resolves a migration path: which bundle steps apply between the
/// caller's current version and the head, in application order.
fn bundle_resolve_request(request : Value) -> Value raise {
let current = str(get(request, "current"))
let validated = bundle_validate_request(request)
let steps = arr(get(validated, "steps"))
let pending : Array[Value] = []
let mut active = false
let mut applied_rules = 0
let head = str(get(validated, "head"))
for step in steps {
let version = str(get(step, "version"))
if version == current {
active = true
continue
}
if active || current == "" {
pending.push(step)
applied_rules += match get(step, "rule_count") {
Number(count) => count.to_int()
_ => 0
}
}
}
if current != "" && !active && current != head {
raise input_error(
"E4004",
"current version " + current + " is not in this bundle",
)
}
record([
("head", String(head)),
("pending_steps", Array(pending)),
("pending_count", Number(pending.length().to_double())),
("up_to_date", Bool(pending.length() == 0)),
("pending_rule_count", Number(applied_rules.to_double())),
])
}