///|
pub(all) struct PlanPolicy {
format_version : Int
allow_low_risk : Bool
allow_medium_risk : Bool
allow_high_risk : Bool
require_regenerable : Bool
maximum_items : Int
maximum_reclaimable_bytes : Int64
allowed_rule_ids : Array[String]
protected_patterns : Array[String]
} derive(Eq, Debug, ToJson, FromJson)
///|
pub(all) enum PolicyViolationKind {
RiskNotAllowed
NotRegenerable
TooManyItems
ReclaimLimitExceeded
RuleNotAllowed
ProtectedPath
ExistingPlanIssues
} derive(Eq, Debug, ToJson, FromJson)
///|
pub(all) struct PolicyViolation {
kind : PolicyViolationKind
path : String
message : String
} derive(Eq, Debug, ToJson, FromJson)
///|
pub fn PlanPolicy::conservative() -> PlanPolicy {
PlanPolicy::{
format_version: 1,
allow_low_risk: true,
allow_medium_risk: false,
allow_high_risk: false,
require_regenerable: true,
maximum_items: 1000,
maximum_reclaimable_bytes: 10L * 1024L * 1024L * 1024L,
allowed_rule_ids: [],
protected_patterns: [".git", ".git/*", "src", "src/*", "LICENSE", "README*"],
}
}
///|
pub fn PlanPolicy::permissive() -> PlanPolicy {
PlanPolicy::{
format_version: 1,
allow_low_risk: true,
allow_medium_risk: true,
allow_high_risk: false,
require_regenerable: false,
maximum_items: 10000,
maximum_reclaimable_bytes: 100L * 1024L * 1024L * 1024L,
allowed_rule_ids: [],
protected_patterns: [".git", ".git/*"],
}
}
///|
fn policy_allows_risk(policy : PlanPolicy, risk : RiskLevel) -> Bool {
match risk {
Low => policy.allow_low_risk
Medium => policy.allow_medium_risk
High => policy.allow_high_risk
}
}
///|
fn policy_allows_rule(policy : PlanPolicy, rule_id : String) -> Bool {
policy.allowed_rule_ids.is_empty() ||
policy.allowed_rule_ids.any(fn(allowed) { allowed == rule_id })
}
///|
fn policy_protects_path(policy : PlanPolicy, path : String) -> Bool {
matches_any_pattern(path, policy.protected_patterns)
}
///|
pub fn review_plan_policy(
plan : CleanupPlan,
policy : PlanPolicy,
) -> Array[PolicyViolation] {
let violations : Array[PolicyViolation] = []
if !plan.issues.is_empty() {
violations.push(PolicyViolation::{
kind: ExistingPlanIssues,
path: "",
message: "The cleanup plan contains " +
plan.issues.length().to_string() +
" unresolved planning issues.",
})
}
if plan.items.length() > policy.maximum_items {
violations.push(PolicyViolation::{
kind: TooManyItems,
path: "",
message: "The plan contains " +
plan.items.length().to_string() +
" items, exceeding the policy limit of " +
policy.maximum_items.to_string() +
".",
})
}
if plan.reclaimable_size > policy.maximum_reclaimable_bytes {
violations.push(PolicyViolation::{
kind: ReclaimLimitExceeded,
path: "",
message: "The plan would move " +
format_bytes(plan.reclaimable_size) +
", exceeding the policy limit of " +
format_bytes(policy.maximum_reclaimable_bytes) +
".",
})
}
for item in plan.items {
if !policy_allows_risk(policy, item.risk) {
violations.push(PolicyViolation::{
kind: RiskNotAllowed,
path: item.source,
message: "The " + risk_name(item.risk) + " risk level is not allowed.",
})
}
if policy.require_regenerable && !item.regenerable {
violations.push(PolicyViolation::{
kind: NotRegenerable,
path: item.source,
message: "Policy requires every candidate to be regenerable.",
})
}
if !policy_allows_rule(policy, item.rule_id) {
violations.push(PolicyViolation::{
kind: RuleNotAllowed,
path: item.source,
message: "Rule " + item.rule_id + " is not on the policy allowlist.",
})
}
if policy_protects_path(policy, item.source) {
violations.push(PolicyViolation::{
kind: ProtectedPath,
path: item.source,
message: "The path matches a protected pattern.",
})
}
}
violations
}
///|
pub fn render_policy_review(violations : Array[PolicyViolation]) -> String {
let lines : Array[String] = ["MoonDisk cleanup policy review"]
if violations.is_empty() {
lines.push("Approved: the cleanup plan satisfies every configured guard.")
} else {
lines.push(
"Rejected: " + violations.length().to_string() + " violations found.",
)
for violation in violations {
let location = if violation.path == "" {
""
} else {
" [" + violation.path + "]"
}
lines.push("- " + violation.message + location)
}
}
lines.join("\n")
}
///|
pub fn plan_policy_to_json(policy : PlanPolicy) -> String {
ToJson::to_json(policy).stringify(indent=2)
}
///|
pub fn plan_policy_from_json(source : String) -> PlanPolicy raise {
let policy : PlanPolicy = @json.from_json(@json.parse(source))
if policy.format_version != 1 {
raise Failure("Unsupported cleanup policy version")
}
if policy.maximum_items < 0 || policy.maximum_reclaimable_bytes < 0L {
raise Failure("Cleanup policy limits cannot be negative")
}
for pattern in policy.protected_patterns {
if pattern.trim().is_empty() {
raise Failure("Protected path patterns cannot be empty")
}
}
policy
}
///|
pub async fn write_plan_policy(path : String, policy : PlanPolicy) -> Unit {
@fs.write_file(path, plan_policy_to_json(policy))
}
///|
pub async fn read_plan_policy(path : String) -> PlanPolicy {
plan_policy_from_json(@fs.read_file(path).text())
}