/// engine_dag_mc.mbt —— Monte Carlo 概率式完工预测(R94,Van Slyke 1963 蒸馏)。
///
/// PERT 家族确定性分析(dag_slack,R71)的概率式补充:对每个任务按难度档
/// ([难度梯度 X])取三点三角分布时长,整网模拟 N 次,得完工时间分布
/// (min/mean/p50/p90/max)+ 按期概率 P(完工 ≤ deadline) + 关键度
/// (任务出现在"最长路径"的频率——谁最可能成为真实瓶颈,含近关键路径)。
/// 确定性可复现(seed + 每样本独立状态);纯读无副作用。
///|
/// xorshift32 一步(返回 (新状态, [0, 2^31) 的整数))。Int 为 64 位,用 32 位掩码
/// 保持 js/native 双 target 数值一致。
fn mc_xorshift(state : Int) -> (Int, Int) {
let mut x = state
x = (x ^ (x << 13)) & 0xffffffff
x = (x ^ (x >> 17)) & 0xffffffff
x = (x ^ (x << 5)) & 0xffffffff
(x, x & 0x7fffffff)
}
///|
/// 牛顿法平方根(core 无 sqrt;20 次迭代足够收敛,同 R89 sqrt_approx 模式)。
fn mc_sqrt(x : Double) -> Double {
if x <= 0.0 {
return 0.0
}
let mut g = if x >= 1.0 { x } else { 1.0 }
for _i = 0; _i < 20; _i = _i + 1 {
g = (g + x / g) * 0.5
}
g
}
///|
/// 三角分布采样(a (Int, Int) {
let (s2, v) = mc_xorshift(state)
let denom = (b - a).to_double()
let u = v.to_double() / 2147483648.0
let dur = if u < (m - a).to_double() / denom {
a.to_double() + mc_sqrt(u * denom * (m - a).to_double())
} else {
b.to_double() - mc_sqrt((1.0 - u) * denom * (b - m).to_double())
}
(s2, dur.ceil().to_int())
}
///|
/// 难度档 → 三点三角分布(易/中/难;缺省档按中)。
fn mc_durations(tier : String) -> (Int, Int, Int) {
match tier {
"易" => (2, 3, 5)
"难" => (5, 8, 14)
_ => (3, 5, 8)
}
}
///|
/// 作用域任务集:root_task_id 为空 → 该 ns 全部任务;否则取其子树
/// (id==root 或 parent 链可达 root,传递闭包,天然环保护)。
fn FistEngine::mc_scope_tasks(
self : FistEngine,
ns : String,
root_task_id : String,
) -> Array[@core.Task] {
let all = self.list_in_ns(ns)
if root_task_id == "" {
return all
}
let scope : Map[String, Bool] = Map([])
scope.set(root_task_id, true)
let mut changed = true
while changed {
changed = false
for t in all {
let in_parent = match t.parent_id {
Some(p) => scope.get(p).unwrap_or(false)
None => false
}
if in_parent && not(scope.get(t.get_id()).unwrap_or(false)) {
scope.set(t.get_id(), true)
changed = true
}
}
}
all.filter(fn(t) { scope.get(t.get_id()).unwrap_or(false) })
}
///|
/// 单样本内:以 id 结尾的最长完成时间(沿 depends_on 回溯;memo 化 DFS,含环保护)。
/// dur 为该样本各任务时长。返回 finish(id) = max(finish(dep)) + dur(id)。
fn FistEngine::mc_finish(
self : FistEngine,
id : String,
dur : Map[String, Int],
memo : Map[String, Int],
visiting : Map[String, Bool],
) -> Int {
match memo.get(id) {
Some(v) => return v
None => ()
}
match visiting.get(id) {
Some(_) => return dur.get(id).unwrap_or(0)
None => ()
}
visiting.set(id, true)
let own = dur.get(id).unwrap_or(0)
let mut best = own
match self.get_task(id) {
Some(t) =>
for d in t.depends_on {
let f = self.mc_finish(d, dur, memo, visiting)
if f + own > best {
best = f + own
}
}
None => ()
}
visiting.remove(id)
memo.set(id, best)
best
}
///|
/// Monte Carlo 概率式完工预测(R94,Van Slyke 1963 首倡 MCS 求网络完工分布)。
/// 对作用域内每个任务按难度档采样三角分布时长,整网模拟 samples 次,
/// 输出完工时间分布(min/mean/p50/p90/max)+ 按期概率 + 关键度排行。
/// deadline>0 时给出 P(完工 ≤ deadline);seed 固定则结果可复现。
pub fn FistEngine::dag_mc(
self : FistEngine,
ns? : String = "default",
root_task_id? : String = "",
deadline? : Int = 0,
samples? : Int = 1000,
seed? : Int = 42,
) -> Json {
let tasks = self.mc_scope_tasks(ns, root_task_id)
if tasks.is_empty() {
let im = Map::new()
im.set("insufficient", Json::boolean(true))
im.set(
"note",
Json::string(
"作用域内无任务(ns / root_task_id 为空或不存在)——无法预测完工。",
),
)
return Json::object(im)
}
// BUG-24:只有下界钳制的 samples 会让单次 MCP 调用无限膨胀(实测 samples=200000
// 阻塞 >100s,而客户端超时 30s ——调用方看到的是"无响应"而不是结果)。
// 上界 5000 是按 p90 收敛够用 + 落在 30s 预算内定的;被钳制时如实回显,不假装跑满。
let n_requested = if samples < 10 { 10 } else { samples }
let mc_cap = 5000
let n = if n_requested > mc_cap { mc_cap } else { n_requested }
let makespans : Array[Int] = []
let critical_hits : Map[String, Int] = Map([])
for t in tasks {
critical_hits.set(t.get_id(), 0)
}
for s = 0; s < n; s = s + 1 {
let mut state = seed + s * 7919
let dur : Map[String, Int] = Map([])
for t in tasks {
let tier = extract_difficulty(t.description)
let (a, m, b) = mc_durations(tier)
let (s2, d) = mc_tri_sample(state, a, m, b)
state = s2
dur.set(t.get_id(), d)
}
let memo : Map[String, Int] = Map([])
let visiting : Map[String, Bool] = Map([])
let mut ms = 0
for t in tasks {
let f = self.mc_finish(t.get_id(), dur, memo, visiting)
if f > ms {
ms = f
}
}
makespans.push(ms)
for t in tasks {
let f = memo.get(t.get_id()).unwrap_or(0)
if f == ms {
critical_hits.set(
t.get_id(),
critical_hits.get(t.get_id()).unwrap_or(0) + 1,
)
}
}
}
// 统计:排序取分位
let sorted = makespans.copy()
sorted.sort()
let sum = makespans.fold_left(init=0, fn(acc, x) { acc + x })
let mean = sum.to_double() / n.to_double()
let p50 = sorted[(n - 1) / 2]
let p90 = sorted[if n * 9 / 10 >= n { n - 1 } else { n * 9 / 10 }]
let minv = sorted[0]
let maxv = sorted[n - 1]
// 按期概率
let mut on_time = 0
for ms in makespans {
if deadline > 0 && ms <= deadline {
on_time = on_time + 1
}
}
let p_on_time = if deadline > 0 {
on_time.to_double() / n.to_double()
} else {
0.0
}
// 关键度排行(top 10;手写选择排序降序,规避 tuple 比较器 API 差异)
let crit_arr : Array[(String, Int)] = []
for t in tasks {
crit_arr.push((t.get_id(), critical_hits.get(t.get_id()).unwrap_or(0)))
}
for i = 0; i < crit_arr.length(); i = i + 1 {
for j = i + 1; j < crit_arr.length(); j = j + 1 {
if crit_arr[j].1 > crit_arr[i].1 {
let tmp = crit_arr[i]
crit_arr[i] = crit_arr[j]
crit_arr[j] = tmp
}
}
}
let crit_json : Array[Json] = []
let top = if crit_arr.length() > 10 { 10 } else { crit_arr.length() }
for i = 0; i < top; i = i + 1 {
let cm = Map::new()
cm.set("task_id", Json::string(crit_arr[i].0))
cm.set(
"critical_freq",
Json::number(crit_arr[i].1.to_double() / n.to_double()),
)
crit_json.push(Json::object(cm))
}
// 组装返回
let mk = Map::new()
mk.set("min", Json::number(minv.to_double()))
mk.set("mean", Json::number(mean))
mk.set("p50", Json::number(p50.to_double()))
mk.set("p90", Json::number(p90.to_double()))
mk.set("max", Json::number(maxv.to_double()))
let sc = Map::new()
sc.set("ns", Json::string(ns))
sc.set("root_task_id", Json::string(root_task_id))
sc.set("tasks", Json::number(tasks.length().to_double()))
let m = Map::new()
m.set("scope", Json::object(sc))
m.set("samples", Json::number(n.to_double()))
// BUG-24:如实回显"要了多少 / 实际跑了多少 / 为什么",调用方不用读源码就能知道被钳制
m.set("samples_requested", Json::number(n_requested.to_double()))
m.set("samples_capped", Json::boolean(n != n_requested))
if n != n_requested {
m.set(
"cap_note",
Json::string(
"samples 被钳制到 \{n}(上限 5000):本工具跑在 MCP 同步调用里,客户端超时 30s," +
"跑满 \{n_requested} 会让调用方只看到「无响应」。要多样本请分批换 seed 自行聚合。",
),
)
}
m.set("deadline", Json::number(deadline.to_double()))
m.set("p_on_time", Json::number(p_on_time))
m.set("makespan", Json::object(mk))
m.set("criticality", Json::array(crit_json))
m.set(
"note",
Json::string(
"Van Slyke 1963 Monte Carlo PERT:整网模拟 N 次得完工分布(克服单关键路径/merge bias);关键度=任务出现在最长路径的频率,含近关键路径。难度档→三点三角分布时长;seed 固定可复现。",
),
)
Json::object(m)
}