///|
pub struct CiPlanStep {
name : String
command : String
purpose : String
} derive(Debug, Eq)
///|
pub struct CiPlan {
target : String
target_known : Bool
target_generator_status_id : String
target_generator_available : Bool
budget_profile : String
render_budget_profile : String
inspection_max_route_risk_score : Int
inspection_max_update_payload_bytes : Int
inspection_max_update_operations : Int
inspection_max_unwindowed_list_children : Int
inspection_max_unkeyed_list_children : Int
inspection_max_duplicate_list_keys : Int
inspection_max_backend_events : Int
inspection_max_backend_streams : Int
inspection_max_backend_reviews : Int
inspection_max_backend_cancels : Int
inspection_max_backend_retries : Int
inspection_max_scene_assets : Int
inspection_max_scene_remote_assets : Int
inspection_max_scene_package_bytes : Int
inspection_gate_summary : String
profile_max_route_diagnostic_routes : Int
profile_max_route_diagnostics : Int
profile_max_unwindowed_list_children : Int
profile_max_unkeyed_list_children : Int
profile_max_duplicate_list_keys : Int
profile_max_communication_unresolved_reviews : Int
profile_max_tool_result_missing_acks : Int
profile_max_agent_duplicate_stream_chunks : Int
profile_max_snapshot_full_replaces : Int
profile_max_scene_thread_orphans : Int
profile_max_scene_camera_clamped : Int
profile_max_scene_surface_fallbacks : Int
profile_max_scene_insecure_remote_assets : Int
profile_max_scene_unapproved_remote_assets : Int
profile_max_scene_deferred_assets : Int
profile_max_scene_quality_issues : Int
profile_max_degraded_scenes : Int
profile_gate_summary : String
examples : Array[String]
snapshot_dir : String
step_count : Int
steps : Array[CiPlanStep]
diagnostics : Array[String]
summary : String
} derive(Debug, Eq)
///|
pub fn ci_plan_step(
name : String,
command : String,
purpose : String,
) -> CiPlanStep {
ci_step(name, command, purpose)
}
///|
pub fn ci_plan(
target? : String = "wechat",
budget_profile? : String = "tight",
render_budget_profile? : String = "",
examples? : Array[String] = ["agent_map"],
snapshot_dir? : String = "_build/bunnia/snapshots",
post_build_steps? : Array[CiPlanStep] = [],
) -> CiPlan {
let active_render_budget_profile = if render_budget_profile == "" {
budget_profile
} else {
render_budget_profile
}
let target_support = platform_target_support(target~)
let diagnostics = ci_plan_diagnostics(
target_support, budget_profile, active_render_budget_profile,
)
let inspection_budget = ci_plan_inspection_budget(budget_profile)
let profile_budget = ci_plan_profile_budget(budget_profile)
let inspection_gate_summary = "Bunnia CI inspection gate: budget=\{budget_profile} max_route_risk=\{inspection_budget.max_route_risk_score} max_update_payload=\{inspection_budget.max_update_payload_bytes} max_update_ops=\{inspection_budget.max_update_operations} max_unwindowed_list_children=\{inspection_budget.max_unwindowed_list_children} max_unkeyed_list_children=\{inspection_budget.max_unkeyed_list_children} max_duplicate_list_keys=\{inspection_budget.max_duplicate_list_keys} max_backend_events=\{inspection_budget.max_backend_events} max_backend_streams=\{inspection_budget.max_backend_streams} max_backend_reviews=\{inspection_budget.max_backend_reviews} max_backend_cancels=\{inspection_budget.max_backend_cancels} max_backend_retries=\{inspection_budget.max_backend_retries} max_scene_assets=\{inspection_budget.max_scene_assets} max_scene_remote_assets=\{inspection_budget.max_scene_remote_assets} max_scene_package_bytes=\{inspection_budget.max_scene_package_bytes}"
let profile_gate_summary = "Bunnia CI profile gate: budget=\{budget_profile} max_route_diagnostic_routes=\{profile_budget.max_route_diagnostic_routes} max_route_diagnostics=\{profile_budget.max_route_diagnostics} max_unwindowed_list_children=\{profile_budget.max_unwindowed_list_children} max_unkeyed_list_children=\{profile_budget.max_unkeyed_list_children} max_duplicate_list_keys=\{profile_budget.max_duplicate_list_keys} max_communication_unresolved_reviews=\{profile_budget.max_communication_unresolved_reviews} max_tool_result_missing_acks=\{profile_budget.max_tool_result_missing_acks} max_agent_duplicate_stream_chunks=\{profile_budget.max_agent_duplicate_stream_chunks} max_snapshot_full_replaces=\{profile_budget.max_snapshot_full_replaces} max_scene_thread_orphans=\{profile_budget.max_scene_thread_orphans} max_scene_camera_clamped=\{profile_budget.max_scene_camera_clamped} max_scene_surface_fallbacks=\{profile_budget.max_scene_surface_fallbacks} max_scene_insecure_remote_assets=\{profile_budget.max_scene_insecure_remote_assets} max_scene_unapproved_remote_assets=\{profile_budget.max_scene_unapproved_remote_assets} max_scene_deferred_assets=\{profile_budget.max_scene_deferred_assets} max_scene_quality_issues=\{profile_budget.max_scene_quality_issues} max_degraded_scenes=\{profile_budget.max_degraded_scenes}"
let steps : Array[CiPlanStep] = []
steps.push(ci_step("check", "moon check", "type-check every package"))
steps.push(ci_step("test", "moon test", "run framework and example tests"))
steps.push(ci_step("info", "moon info", "regenerate public interface files"))
steps.push(ci_step("fmt", "moon fmt", "format MoonBit source files"))
steps.push(
ci_step("diff-check", "git diff --check", "reject whitespace regressions"),
)
steps.push(
ci_step(
"diff-clean", "git diff --exit-code", "reject uncommitted interface or formatting changes",
),
)
steps.push(
ci_step(
"boundary", "sh scripts/validate_boundaries.sh", "reject product-specific framework API or dependency leaks",
),
)
steps.push(
ci_step(
"limits",
"moon run cmd/main -- limits --target \{target}",
"inspect platform component mappings and capability limits",
),
)
for example in examples {
if target == "wechat" {
steps.push(
ci_step(
"inspect-\{example}",
"moon run cmd/main -- inspect --target \{target} --example \{example} --strict --budget \{budget_profile} --render-budget \{active_render_budget_profile}",
"route-risk and generated-pressure inspection for \{example}",
),
)
steps.push(
ci_step(
"build-\{example}",
"moon run cmd/main -- build --target \{target} --example \{example} --strict --budget \{budget_profile} --render-budget \{active_render_budget_profile}",
"strict generated-output budget gate for \{example}",
),
)
for step in post_build_steps {
steps.push(step)
}
steps.push(
ci_step(
"watch-once-\{example}",
"moon run cmd/main -- watch --target \{target} --example \{example} --out _build/bunnia/watch/\{target}/\{example} --once --strict --budget \{budget_profile} --render-budget \{active_render_budget_profile}",
"one-shot watch-path generation check for \{example}",
),
)
steps.push(
ci_step(
"snapshot-\{example}",
"moon run cmd/main -- snapshot --target \{target} --example \{example} --snapshot-out \{snapshot_dir}/\{target}/\{example}.snapshot.txt --strict --budget \{budget_profile} --render-budget \{active_render_budget_profile}",
"deterministic generated-output snapshot for \{example}",
),
)
} else {
steps.push(
ci_step(
"inspect-\{example}",
"moon run cmd/main -- inspect --target \{target} --example \{example} --strict --budget \{budget_profile} --render-budget \{active_render_budget_profile}",
"generic mini-app route and file pressure inspection for \{example}",
),
)
steps.push(
ci_step(
"build-\{example}",
"moon run cmd/main -- build --target \{target} --example \{example} --strict --budget \{budget_profile} --render-budget \{active_render_budget_profile}",
"strict generic mini-app lowering check for \{example}",
),
)
for step in post_build_steps {
steps.push(step)
}
steps.push(
ci_step(
"watch-once-\{example}",
"moon run cmd/main -- watch --target \{target} --example \{example} --out _build/bunnia/watch/\{target}/\{example} --once --strict --budget \{budget_profile} --render-budget \{active_render_budget_profile}",
"one-shot generic mini-app watch-path generation check for \{example}",
),
)
steps.push(
ci_step(
"snapshot-\{example}",
"moon run cmd/main -- snapshot --target \{target} --example \{example} --snapshot-out \{snapshot_dir}/\{target}/\{example}.snapshot.txt --strict --budget \{budget_profile} --render-budget \{active_render_budget_profile}",
"deterministic generic mini-app snapshot for \{example}",
),
)
}
}
steps.push(
ci_step(
"scaffold-smoke", "sh scripts/scaffold_smoke.sh", "generate and validate the standalone starter scaffold",
),
)
{
target,
target_known: target_support.known,
target_generator_status_id: target_support.generator_status_id,
target_generator_available: target_support.generator_available,
budget_profile,
render_budget_profile: active_render_budget_profile,
inspection_max_route_risk_score: inspection_budget.max_route_risk_score,
inspection_max_update_payload_bytes: inspection_budget.max_update_payload_bytes,
inspection_max_update_operations: inspection_budget.max_update_operations,
inspection_max_unwindowed_list_children: inspection_budget.max_unwindowed_list_children,
inspection_max_unkeyed_list_children: inspection_budget.max_unkeyed_list_children,
inspection_max_duplicate_list_keys: inspection_budget.max_duplicate_list_keys,
inspection_max_backend_events: inspection_budget.max_backend_events,
inspection_max_backend_streams: inspection_budget.max_backend_streams,
inspection_max_backend_reviews: inspection_budget.max_backend_reviews,
inspection_max_backend_cancels: inspection_budget.max_backend_cancels,
inspection_max_backend_retries: inspection_budget.max_backend_retries,
inspection_max_scene_assets: inspection_budget.max_scene_assets,
inspection_max_scene_remote_assets: inspection_budget.max_scene_remote_assets,
inspection_max_scene_package_bytes: inspection_budget.max_scene_package_bytes,
inspection_gate_summary,
profile_max_route_diagnostic_routes: profile_budget.max_route_diagnostic_routes,
profile_max_route_diagnostics: profile_budget.max_route_diagnostics,
profile_max_unwindowed_list_children: profile_budget.max_unwindowed_list_children,
profile_max_unkeyed_list_children: profile_budget.max_unkeyed_list_children,
profile_max_duplicate_list_keys: profile_budget.max_duplicate_list_keys,
profile_max_communication_unresolved_reviews: profile_budget.max_communication_unresolved_reviews,
profile_max_tool_result_missing_acks: profile_budget.max_tool_result_missing_acks,
profile_max_agent_duplicate_stream_chunks: profile_budget.max_agent_duplicate_stream_chunks,
profile_max_snapshot_full_replaces: profile_budget.max_snapshot_full_replaces,
profile_max_scene_thread_orphans: profile_budget.max_scene_thread_orphans,
profile_max_scene_camera_clamped: profile_budget.max_scene_camera_clamped,
profile_max_scene_surface_fallbacks: profile_budget.max_scene_surface_fallbacks,
profile_max_scene_insecure_remote_assets: profile_budget.max_scene_insecure_remote_assets,
profile_max_scene_unapproved_remote_assets: profile_budget.max_scene_unapproved_remote_assets,
profile_max_scene_deferred_assets: profile_budget.max_scene_deferred_assets,
profile_max_scene_quality_issues: profile_budget.max_scene_quality_issues,
profile_max_degraded_scenes: profile_budget.max_degraded_scenes,
profile_gate_summary,
examples,
snapshot_dir,
step_count: steps.length(),
steps,
diagnostics,
summary: "Bunnia CI plan: target=\{target} target_generator_status=\{target_support.generator_status_id} target_generator=\{target_support.generator_available} budget=\{budget_profile} render_budget=\{active_render_budget_profile} inspection_max_route_risk=\{inspection_budget.max_route_risk_score} inspection_max_update_payload=\{inspection_budget.max_update_payload_bytes} inspection_max_update_ops=\{inspection_budget.max_update_operations} inspection_max_unwindowed_list_children=\{inspection_budget.max_unwindowed_list_children} inspection_max_unkeyed_list_children=\{inspection_budget.max_unkeyed_list_children} inspection_max_duplicate_list_keys=\{inspection_budget.max_duplicate_list_keys} inspection_max_backend_events=\{inspection_budget.max_backend_events} inspection_max_backend_streams=\{inspection_budget.max_backend_streams} inspection_max_backend_reviews=\{inspection_budget.max_backend_reviews} inspection_max_backend_cancels=\{inspection_budget.max_backend_cancels} inspection_max_backend_retries=\{inspection_budget.max_backend_retries} inspection_max_scene_assets=\{inspection_budget.max_scene_assets} inspection_max_scene_remote_assets=\{inspection_budget.max_scene_remote_assets} inspection_max_scene_package_bytes=\{inspection_budget.max_scene_package_bytes} profile_max_route_diagnostic_routes=\{profile_budget.max_route_diagnostic_routes} profile_max_route_diagnostics=\{profile_budget.max_route_diagnostics} profile_max_unwindowed_list_children=\{profile_budget.max_unwindowed_list_children} profile_max_unkeyed_list_children=\{profile_budget.max_unkeyed_list_children} profile_max_duplicate_list_keys=\{profile_budget.max_duplicate_list_keys} profile_max_communication_unresolved_reviews=\{profile_budget.max_communication_unresolved_reviews} profile_max_tool_result_missing_acks=\{profile_budget.max_tool_result_missing_acks} profile_max_agent_duplicate_stream_chunks=\{profile_budget.max_agent_duplicate_stream_chunks} profile_max_snapshot_full_replaces=\{profile_budget.max_snapshot_full_replaces} profile_max_scene_surface_fallbacks=\{profile_budget.max_scene_surface_fallbacks} profile_max_scene_insecure_remote_assets=\{profile_budget.max_scene_insecure_remote_assets} profile_max_scene_unapproved_remote_assets=\{profile_budget.max_scene_unapproved_remote_assets} profile_max_scene_deferred_assets=\{profile_budget.max_scene_deferred_assets} profile_max_scene_quality_issues=\{profile_budget.max_scene_quality_issues} profile_max_degraded_scenes=\{profile_budget.max_degraded_scenes} examples=\{examples.length()} steps=\{steps.length()} diagnostics=\{diagnostics.length()} snapshot_dir=\{snapshot_dir}",
}
}
///|
pub fn ci_plan_shell_script(plan : CiPlan) -> String {
let out = StringBuilder()
out.write_string("#!/usr/bin/env sh\n")
out.write_string("set -eu\n\n")
out.write_string("# Generated by Bunnia ci-plan --script.\n")
out.write_string("# \{plan.summary}\n")
out.write_string("# \{plan.inspection_gate_summary}\n\n")
out.write_string("# \{plan.profile_gate_summary}\n\n")
if plan.diagnostics.length() > 0 {
write_shell_echo(out, "Bunnia CI plan diagnostics:")
for item in plan.diagnostics {
write_shell_echo(out, item)
}
out.write_string("exit 1\n")
} else {
for step in plan.steps {
write_shell_echo(out, "==> \{step.name}")
out.write_string("# \{step.purpose}\n")
out.write_string(step.command)
out.write_string("\n\n")
}
}
out.to_string()
}
///|
fn ci_plan_inspection_budget(name : String) -> ProjectInspectionBudget {
if is_named_budget_profile(name) {
inspection_budget_for_profile(name)
} else {
inspection_budget_for_profile("default")
}
}
///|
fn ci_plan_profile_budget(name : String) -> BuildProfileBudget {
if is_named_budget_profile(name) {
build_profile_budget_for_profile(name)
} else {
build_profile_budget_for_profile("default")
}
}
///|
fn ci_plan_diagnostics(
target_support : PlatformTargetSupport,
budget_profile : String,
render_budget_profile : String,
) -> Array[String] {
let output : Array[String] = []
if !target_support.generator_available {
for item in target_support.diagnostics {
output.push("ci-\{item}")
}
}
if !is_named_budget_profile(budget_profile) {
output.push("ci-budget-profile-unknown:\{budget_profile}")
}
if !is_named_budget_profile(render_budget_profile) {
output.push("ci-render-budget-profile-unknown:\{render_budget_profile}")
}
output
}
///|
fn ci_step(name : String, command : String, purpose : String) -> CiPlanStep {
{ name, command, purpose }
}
///|
fn write_shell_echo(out : StringBuilder, text : String) -> Unit {
out.write_string("printf '%s\\n' ")
write_shell_double_quoted(out, text)
out.write_string("\n")
}
///|
fn write_shell_double_quoted(out : StringBuilder, text : String) -> Unit {
out.write_string("\"")
for i in 0..