/// 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 : Int32) -> (Int32, Int32) {
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 (Int32, Int32) {
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) -> (Int32, Int32, Int32) {
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 && !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)。
///
/// R12-R5 性能:`deps` 参数传入预计算的 `Map[String, Array[String]]`,
/// 避免在 5000 样本 × N 任务下每次递归都调用 `self.get_task(id)`(SqliteStore
/// 下是 DB 查询)。`mc_scope_tasks` 已把作用域任务载入内存,`depends_on`
/// 在该次调用内不变,故只构建一次。
fn FistEngine::mc_finish(
self : FistEngine,
id : String,
dur : Map[String, Int32],
memo : Map[String, Int32],
visiting : Map[String, Bool],
deps : Map[String, Array[String]],
) -> Int32 {
match memo.get(id) {
Some(v) => return v
None => ()
}
match visiting.get(id) {
// BUG-204:环命中返回 -1 哨兵(采样 dur 恒 >= 0,故 f + own <= own <= best,
// 环路径不参与 max)——旧实现返回节点自身 dur,A→B→A 会让 A 被累加 2 次。
// 不用 Int.MIN_VALUE:与 own 相加会溢出。
Some(_) => return -1
None => ()
}
visiting.set(id, true)
let own = dur.get(id).unwrap_or(0)
let mut best = own
// BUG-212:BUG-204 的 -1 哨兵只在「自身被重入」时生效,外层收到 -1 后照样累加内层 own
// (`f + own > best` 对 f=-1 因 own-1 <= own 不生效),于是环 A↔B 的 A 把 B 的 own 累进 best,
// makespan 把环当正常最长路径。这里把 -1 沿依赖链向上传播:任一后继在环上 → 自身也在环上。
let mut cycle = false
match deps.get(id) {
Some(arr) =>
for d in arr {
let f = self.mc_finish(d, dur, memo, visiting, deps)
if f == -1 {
cycle = true
}
if f + own > best {
best = f + own
}
}
None => ()
}
visiting.remove(id)
if cycle {
memo.set(id, -1)
return -1
}
memo.set(id, best)
best
}
///|
/// 从 id 起的最长链长度(含 own,**看后继**):own + max over succ(id) of mc_suffix_from(succ)。
/// mc_finish 只算前驱,单独用它判「在最长路径上」会漏掉最长路径的**中间节点**
/// (BUG-203:线性链 A→B 只有 B 被判关键,A 明明在唯一最长路径上却 critical_freq=0)。
/// 关键度判据必须前向+后向相加减 own == makespan,故补这个后向 pass。
fn mc_suffix_from(
id : String,
dur : Map[String, Int32],
memo_suf : Map[String, Int32],
visiting : Map[String, Bool],
succ : Map[String, Array[String]],
) -> Int32 {
match memo_suf.get(id) {
Some(v) => return v
None => ()
}
match visiting.get(id) {
// 环命中返回 -1 哨兵(与 mc_finish 的 BUG-204 同口径),环路径不参与 max。
Some(_) => return -1
None => ()
}
visiting.set(id, true)
let own = dur.get(id).unwrap_or(0)
let mut best = own
// BUG-212:同 mc_finish——-1 哨兵必须沿后继链向上传播,否则环上节点返回正数。
let mut cycle = false
match succ.get(id) {
Some(arr) =>
for s in arr {
let f = mc_suffix_from(s, dur, memo_suf, visiting, succ)
if f == -1 {
cycle = true
}
if f + own > best {
best = f + own
}
}
None => ()
}
visiting.remove(id)
if cycle {
memo_suf.set(id, -1)
return -1
}
memo_suf.set(id, best)
best
}
///|
/// 最近秩分位索引:n 个有序样本(升序)取 p 分位(0..100)应用的 0-based 索引。
/// BUG-206:旧公式 p50 用 (n-1)/2、p90 用 n*9/10,两条取法不对称——p90 比最近秩
/// ceil(p*n/100)-1 恒高 1,系统性高估 p90 一个样本值。这里统一为 (n-1)*p/100。
pub fn mc_percentile_index(n : Int32, p : Int32) -> Int32 {
if n <= 0 {
return 0
}
if p < 0 {
return 0
}
if p >= 100 {
return n - 1
}
(n - 1) * p / 100
}
///|
/// Monte Carlo 概率式完工预测(R94,Van Slyke 1963 首倡 MCS 求网络完工分布)。
/// 对作用域内每个任务按难度档采样三角分布时长,整网模拟 samples 次,
/// 输出完工时间分布(min/mean/p50/p90/max)+ 按期概率 + 关键度排行。
/// deadline>0 时给出 P(完工 ≤ deadline);seed 固定则结果可复现。
/// 空作用域返回 { insufficient:true, note },无 p_on_time/makespan/criticality
/// (无法预测,不伪造——BUG-182 文档契约)。
pub fn FistEngine::dag_mc(
self : FistEngine,
ns~ : String = "default",
root_task_id~ : String = "",
deadline~ : Int32 = 0,
samples~ : Int32 = 1000,
seed~ : Int32 = 42,
) -> Json {
// BUG-250:负 deadline 过去被静默接受——`deadline > 0` 守卫让 p_on_time 恒为 0,
// 而回显里 deadline=-5 与 p_on_time=0 并置,读起来像"按期概率 0%",实际是"根本没问"。
// 与 samples 同族做**边界钳制**:负值→0(=未设期限,不计算 p_on_time),回显如实标注。
let deadline_requested = deadline
let d = if deadline < 0 { 0 } else { deadline }
let tasks = self.mc_scope_tasks(ns, root_task_id)
if tasks.is_empty() {
let im = Map([])
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[Int32] = []
let critical_hits : Map[String, Int32] = Map([])
for t in tasks {
critical_hits.set(t.get_id(), 0)
}
// R12-R3 性能:`extract_difficulty` 与 `mc_durations` 与样本无关,提前算一次。
// 旧代码把它放在 `for s` 内,导致 5000 样本 × N 任务的**重复难度解析**——
// 每个任务每样本都做一遍正则/字符串扫描。预计算后每样本只做一次三角形采样。
// 用 `(t.get_id(), a, m, b)` 三元组数组,避免 Map 的 String 哈希开销。
let tier_cache : Array[(String, Int32, Int32, Int32)] = []
for t in tasks {
let tier = extract_difficulty(t.description)
let (a, m, b) = mc_durations(tier)
tier_cache.push((t.get_id(), a, m, b))
}
// R12-R5 性能:把 depends_on 预计算成 `Map[String, Array[String]]`,样本循环外只建一次。
// 旧代码在 `mc_finish` 递归里每次调 `self.get_task(id)`——SqliteStore 下是 DB 查询,
// 5000 样本 × N 任务 = 最多 100 万次 DB 读。预计算后每样本零 DB 访问。
let deps : Map[String, Array[String]] = Map([])
for t in tasks {
deps.set(t.get_id(), t.depends_on)
}
// BUG-203:后继关系由 deps 反解(t 是 d 的前驱 ⇔ d 是 t 的后继),供后向 pass 用。
let succ : Map[String, Array[String]] = Map([])
for t in tasks {
succ.set(t.get_id(), [])
}
for (_tid, arr) in deps {
for d in arr {
let cur = succ.get(d).unwrap_or([])
let ns = Array::make(cur.length() + 1, "")
for i = 0; i < cur.length(); i = i + 1 {
ns[i] = cur[i]
}
ns[cur.length()] = _tid
succ.set(d, ns)
}
}
for s = 0; s < n; s = s + 1 {
// BUG-208:xorshift32 的唯一年轻态就是 0 自身——state=0 时恒回 (0,0),u 恒为 0,
// 每个任务时长退化为三角分布下界 a,全部 samples 相同 → "分布" 退化为单点
// (min==max、p50==p90、critical_freq 恒 1.0),调用方却看到一份像样的分布 JSON。
// seed 由 MCP 参数直通,无钳制;seed=0 或 seed≥2^32(`& 0xffffffff` 截掉高位)都会落到 0。
// 这里把 state 钳制到 [1, 0x7fffffff],非零时 xorshift 恒非零。
let mut state = seed + s * 7919
state = state & 0x7fffffff
if state <= 0 {
state = 1
}
let dur : Map[String, Int32] = Map([])
for (tid, a, m, b) in tier_cache {
let (s2, d) = mc_tri_sample(state, a, m, b)
state = s2
dur.set(tid, d)
}
let memo : Map[String, Int32] = 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, deps)
if f > ms {
ms = f
}
}
makespans.push(ms)
// BUG-203 判据:t 在最长路径上 ⇔ 前向 finish + 后向 suffix - own == makespan。
// 只判 `finish == ms` 会漏掉最长路径的中间节点(前驱),把关键度系统性错报。
let memo_suf : Map[String, Int32] = Map([])
let visiting_suf : Map[String, Bool] = Map([])
for t in tasks {
ignore(mc_suffix_from(t.get_id(), dur, memo_suf, visiting_suf, succ))
}
for t in tasks {
let f = memo.get(t.get_id()).unwrap_or(0)
let sf = memo_suf.get(t.get_id()).unwrap_or(0)
let own = dur.get(t.get_id()).unwrap_or(0)
if ms > 0 && f >= 0 && sf >= 0 && f + sf - own == 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(init=0, fn(acc, x) { acc + x })
// R1 bugfind(BUG-142 家族):静态扫描发现 unprotected-division——`n` 虽经钳制 >=10,
// 但判据要能被确定性地证明非零,不靠「调用点总是传入合法值」的约定。
let n_safe = if n > 0 { n } else { 1 }
let mean = sum.to_double() / n_safe.to_double()
// BUG-206:p50/p90 同一口径 mc_percentile_index(最近秩 (n-1)*p/100)。
let idx50 = mc_percentile_index(n_safe, 50)
let idx90 = mc_percentile_index(n_safe, 90)
let p50 = sorted[idx50]
let p90 = sorted[idx90]
let minv = if n_safe > 0 { sorted[0] } else { 0 }
let maxv = if n_safe > 0 { sorted[n_safe - 1] } else { 0 }
// 按期概率
let mut on_time = 0
for ms in makespans {
if d > 0 && ms <= d {
on_time = on_time + 1
}
}
let p_on_time = if d > 0 && n_safe > 0 {
on_time.to_double() / n_safe.to_double()
} else {
0.0
}
// 关键度排行(top 10;手写选择排序降序,规避 tuple 比较器 API 差异)
let crit_arr : Array[(String, Int32)] = []
for t in tasks {
crit_arr.push((t.get_id(), critical_hits.get(t.get_id()).unwrap_or(0)))
}
// R12-R9 性能:旧实现对**全量 N 个任务**做完整选择排序(O(N²))。
// 但下游只用 top 10(crit_json 只 push 前 10 名),所以只需**部分选择**:
// 每轮只把当前最大值移到前段,k=top_k 次即止,O(k×N)。
let top_k = if crit_arr.length() > 10 { 10 } else { crit_arr.length() }
for i = 0; i < top_k; i = i + 1 {
let mut best_idx = i
for j = i + 1; j < crit_arr.length(); j = j + 1 {
if crit_arr[j].1 > crit_arr[best_idx].1 {
best_idx = j
}
}
if best_idx != i {
let tmp = crit_arr[i]
crit_arr[i] = crit_arr[best_idx]
crit_arr[best_idx] = tmp
}
}
let crit_json : Array[Json] = []
for i = 0; i < top_k; i = i + 1 {
let cm = Map([])
cm.set("task_id", Json::string(crit_arr[i].0))
cm.set(
"critical_freq",
Json::number(crit_arr[i].1.to_double() / n_safe.to_double()),
)
crit_json.push(Json::object(cm))
}
// 组装返回
let mk = Map([])
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([])
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([])
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(d.to_double()))
m.set("deadline_requested", Json::number(deadline_requested.to_double()))
if deadline_requested < 0 {
m.set("deadline_capped", Json::boolean(true))
m.set(
"deadline_cap_note",
Json::string(
"deadline 为负(\{deadline_requested}):已按未设期限钳制到 0,不计算 p_on_time。" +
"负期限没有实际含义——`deadline > 0` 才问 P(≤deadline),负值回 p_on_time=0 会读成「0% 按期」的假结论。",
),
)
} else {
m.set("deadline_capped", Json::boolean(false))
}
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)
}