///|
struct WebAsset {
id : String
asset_type : String
depends_on : Array[String]
duration : Int
resource : String
priority : Int
} derive(FromJson)
///|
struct WebRequest {
assets : Array[WebAsset]
failed_asset_id : String
target_window : Int
search_budget : Int
} derive(FromJson)
///|
struct WebImpact {
asset_id : String
path : Array[String]
} derive(FromJson, ToJson)
///|
struct WebScheduleEntry {
asset_id : String
resource : String
start : Int
finish : Int
} derive(FromJson, ToJson)
///|
struct WebAlgorithm {
name : String
makespan : Int
elapsed_ms : Int
schedule : Array[WebScheduleEntry]
} derive(FromJson, ToJson)
///|
struct WebResult {
dependency_order : Array[String]
affected : Array[WebImpact]
baselines : Array[WebAlgorithm]
optimized : WebAlgorithm
lower_bound : Int
critical_path : Array[String]
critical_path_duration : Int
target_met : Bool
bottleneck_resource : String?
bottleneck_load : Int
} derive(FromJson, ToJson)
///|
struct EngineReply {
ok : Bool
result : WebResult?
error : String?
} derive(FromJson, ToJson)
///|
suberror WebError {
InvalidInput(String)
}
///|
fn asset_ref(asset : WebAsset) -> @continuity_forge.AssetRef raise {
match asset.asset_type {
"Service" =>
@continuity_forge.AssetRef::Service(
@continuity_forge.ServiceId::new(value=asset.id),
)
"Component" =>
@continuity_forge.AssetRef::Component(
@continuity_forge.ComponentId::new(value=asset.id),
)
_ => raise WebError::InvalidInput("unknown asset_type: \{asset.asset_type}")
}
}
///|
fn ref_id(asset : @continuity_forge.AssetRef) -> String {
match asset {
@continuity_forge.AssetRef::Service(id) => id.value
@continuity_forge.AssetRef::Component(id) => id.value
}
}
///|
fn web_algorithm(
result : @scheduling.AnalysisResult,
name : String,
elapsed_ms : Int,
) -> WebAlgorithm {
let schedule : Array[WebScheduleEntry] = []
for entry in result.schedule {
schedule.push({
asset_id: entry.task_id,
resource: entry.resource_id,
start: entry.start,
finish: entry.finish,
})
}
{ name, makespan: result.makespan, elapsed_ms, schedule, }
}
///|
fn compute(request : WebRequest) -> WebResult raise {
if request.target_window < 0 || request.search_budget < 0 {
raise WebError::InvalidInput(
"target_window and search_budget must be nonnegative",
)
}
let refs : Array[@continuity_forge.AssetRef] = []
let domain_edges : Array[@continuity_forge.Dependency] = []
let tasks : Array[@scheduling.Task] = []
let edges : Array[@scheduling.Dependency] = []
let resources : Array[@scheduling.Resource] = []
for asset in request.assets {
refs.push(asset_ref(asset))
tasks.push(
@scheduling.Task::new(
id=asset.id,
duration=asset.duration,
priority=asset.priority,
resource_id=asset.resource,
),
)
let mut exists = false
for resource in resources {
if resource.id == asset.resource {
exists = true
}
}
if !exists {
resources.push(@scheduling.Resource::new(id=asset.resource))
}
}
for asset in request.assets {
for prerequisite in asset.depends_on {
let mut dependency_ref : @continuity_forge.AssetRef? = None
for candidate in request.assets {
if candidate.id == prerequisite {
dependency_ref = Some(asset_ref(candidate))
}
}
let dependency_ref = match dependency_ref {
Some(value) => value
None =>
raise WebError::InvalidInput("unknown dependency: \{prerequisite}")
}
domain_edges.push(
@continuity_forge.define_dependency(
dependent=asset_ref(asset),
dependency=dependency_ref,
),
)
edges.push(
@scheduling.Dependency::new(
task_id=asset.id,
prerequisite_id=prerequisite,
),
)
}
}
let dependency_order : Array[String] = []
for
item in @continuity_forge.order_dependencies(
assets=refs,
dependencies=domain_edges,
) {
dependency_order.push(ref_id(item))
}
let affected : Array[WebImpact] = []
if request.failed_asset_id != "" {
let mut failed : @continuity_forge.AssetRef? = None
for asset in request.assets {
if asset.id == request.failed_asset_id {
failed = Some(asset_ref(asset))
}
}
let failed = match failed {
Some(value) => value
None =>
raise WebError::InvalidInput(
"unknown failed_asset_id: \{request.failed_asset_id}",
)
}
let impact = @continuity_forge.simulate_impact(
failed_asset=failed,
assets=refs,
dependencies=domain_edges,
)
for item in impact.paths {
let path : Array[String] = []
for node in item.path {
path.push(ref_id(node))
}
affected.push({ asset_id: ref_id(item.affected_asset), path, })
}
}
let problem = @scheduling.SchedulingProblem::new(
tasks~,
dependencies=edges,
constraints=@scheduling.Constraints::new(resources~, target_makespan=None),
)
let baselines : Array[WebAlgorithm] = []
for
baseline in [
@scheduling.Baseline::StableTopological,
@scheduling.Baseline::RecoveryHeuristic,
@scheduling.Baseline::CriticalPath,
] {
let started = @env.now()
let result = @scheduling.run_baseline(problem, baseline)
baselines.push(
web_algorithm(
result,
result.explanation.algorithm,
(@env.now() - started).to_int(),
),
)
}
let started = @env.now()
let optimization = @scheduling.optimize_schedule(
problem,
request.search_budget,
)
let optimized_elapsed_ms = (@env.now() - started).to_int()
let critical_path = @scheduling.critical_path(problem)
let mut bottleneck_resource : String? = None
let mut bottleneck_load = 0
for resource in resources {
let mut load = 0
for task in tasks {
if task.resource_id == resource.id {
load = load + task.duration
}
}
if load > bottleneck_load {
bottleneck_resource = Some(resource.id)
bottleneck_load = load
}
}
{
dependency_order,
affected,
baselines,
optimized: web_algorithm(
optimization.result,
"budgeted-search",
optimized_elapsed_ms,
),
lower_bound: optimization.lower_bound,
critical_path: critical_path.task_ids,
critical_path_duration: critical_path.duration,
target_met: optimization.result.makespan <= request.target_window,
bottleneck_resource,
bottleneck_load,
}
}
///|
/// Browser-facing JSON boundary. Domain and scheduling errors stay structured.
pub fn analyze_workspace(input : String) -> String {
let reply = try {
let request : WebRequest = @json.from_json(@json.parse(input))
let result = compute(request)
EngineReply::{ ok: true, result: Some(result), error: None, }
} catch {
WebError::InvalidInput(message) =>
EngineReply::{ ok: false, result: None, error: Some(message), }
@continuity_forge.ModelError::EmptyField(message) =>
EngineReply::{
ok: false,
result: None,
error: Some("empty field: \{message}"),
}
@continuity_forge.ModelError::SelfDependency(message) =>
EngineReply::{
ok: false,
result: None,
error: Some("self dependency: \{message}"),
}
@continuity_forge.ModelError::DuplicateAsset(message) =>
EngineReply::{
ok: false,
result: None,
error: Some("duplicate asset: \{message}"),
}
@continuity_forge.ModelError::MissingAsset(message) =>
EngineReply::{
ok: false,
result: None,
error: Some("missing asset: \{message}"),
}
@continuity_forge.ModelError::DuplicateDependency(message) =>
EngineReply::{
ok: false,
result: None,
error: Some("duplicate dependency: \{message}"),
}
@continuity_forge.ModelError::DependencyCycle(message) =>
EngineReply::{
ok: false,
result: None,
error: Some("dependency cycle: \{message}"),
}
@scheduling.SchedulingError::InvalidTask(message)
| @scheduling.SchedulingError::InvalidResource(message)
| @scheduling.SchedulingError::InvalidDependency(message)
| @scheduling.SchedulingError::InvalidConstraint(message)
| @scheduling.SchedulingError::InvalidSchedule(message) =>
EngineReply::{ ok: false, result: None, error: Some(message), }
error =>
EngineReply::{ ok: false, result: None, error: Some(error.to_string()), }
}
@json.to_json(reply).stringify()
}
///|
fn main {
}