粗排引擎 (Rough-Cut Scheduler) 虛擬碼
來源:OOA 共用物件 OOA:domain-model OOA 上游 functional spec:fp 最後同步 commit:
初版狀態:living doc — 程式碼異動時必須同步本檔
✅ OOA 回填已套用(2026-06-08,共演化產生)
下列項目原為本份虛擬碼展開時與 OOA(步驟 1–2)的 drift,已於 2026-06-08 回填 rough-cut-scheduler.ooa.md(步驟 3 SOLID 一併收尾)與 ../domain-model/domain-model.ooa.md(#6/#7)。保留為變更記錄;簽核前以 consistency-audit 覆驗一致。
| # | 回填項 | 來源 § | 原因 |
|---|---|---|---|
| 1 | LeanPlayCalculator.compute():void → 回傳 Map<OperationCoordinate, leanPlayDate> | §2.1 | C 案(回傳值好測、快照不變性靠結構) |
| 2 | LsdCalculator.compute():void → 回傳 Map<OperationCoordinate, lsd> | §4.1 | 同上 |
| 3 | SequenceAssigner.assign():void → 回傳 Map<OperationCoordinate, sequence> | §5.1 | 同上 |
| 4 | WorkingNode | — | C 案不需要(取消) |
| 5 | CapacityOverlay.base(FREEZE)邊界註記:由引擎外小模組備妥、引擎唯讀 | §1.1.2 / §3 | §RCS-FS-9 引擎不設定 Freeze |
| 6 | VSM.lastNode() 新增 | §2.1.1 | §RCS-FS-1 節拍點 fallback「末站」 |
| 7 | VSM.upstreamOf/downstreamOf 語意澄清:回傳遞移節點集、且 VSM 可依(反向)拓樸序走訪全節點(allNodes) | §2.2 / §2.4 / §2.5 / §4.1 | 多處正反推都靠它 |
| 8 | CapacityOverlay.occupy 支援「連續日區間」(OOA 目前單日) | §3.3 | 節拍點佔 L 連續日 |
| 9 | CapacityOverlay.inRun 需可依 (wc, day) 查詢(OOA 為 flat List<Occupation>、Occupation 不帶 day) | §3.1 / §3.3 / §3.4 | 按日查 IN_RUN |
| 10 | CapacityOverlay 補 isAvailable(wc, day);effectiveRemaining 標「本版未用/供未來細顆粒」 | §3.1 / §3.4 | D5 二元可用語意 |
0. 名詞表
| 名詞 | 中文 | 定義 |
|---|---|---|
RoughCutScheduler | 粗排引擎 | 編排者;run(orders, capacity, params),純試算、不寫回 |
LeanPlayCalculator | LeanPlay 計算器 | 節拍點錨定正推+拉動上游/推動下游 |
LsdCalculator | LSD 計算器 | 自交期反推、純 leadtime、不看產能 |
SequenceAssigner | 投產順序指派 | 全單混排依完工日排序 |
LeadTimeStrategy | 前置時間策略 | per-run 選一;leadTimeDays(node) |
SchedulingContext | 排程情境 | 本 run 狀態:起排日、選定策略(leadTime + 資源分配)、產能 overlay、資源主檔 ResourcePool(§7.0) |
CapacityOverlay | 產能 overlay | 包 FREEZE(唯讀基準)+ 本 run IN_RUN 帳(鍵=資源×日);跑完丟棄 |
DayRange | 日期區間 | 小 VO:節拍點佔用起訖 (start, finish);stackCapacity 回傳 |
ProductionOrder | 投產單 | 輸入單位;poNo / qty / dueDate / prioritySeq,組一棵 VSM |
VSM / VSMNode | 製程樹/工序節點 | 樹狀拓樸(多上游匯流);takt() 取節拍點 |
DataBox | 製程時間參數 | 每節點 CT(製造)/CO(換模)等 |
OperationCoordinate | 工序座標 | 節點識別=產能佔用 occupier(接縫);帶 qty |
AvailableCapacity / CapacitySlot | 可用產能/產能格 | FREEZE 唯讀基準;鍵=資源×日(2026-06-09 下沉,原工作站×日;見 §7) |
Occupation / CapacityOccupationSource | 佔用/佔用來源 | FREEZE(引擎外)|IN_RUN(本 run 暫佔) |
RoughScheduleResult / ScheduledNode | 粗排結果/節點結果 | 輸出;每節點掛 leanPlayDate / lsd / sequence(不可變快照) |
takt | 節拍點 | VSM 的節奏錨點;無指定則 fallback 末站(§RCS-FS-1) |
| 定錨 anchor | — | 自起排日正推求節拍點「最早可開工日」(純 leadtime) |
| 拉動上游 pull | — | 由節拍點當日往前 JIT、緊鄰不留閒置(§RCS-FS-15) |
| 推動下游 push | — | 由節拍點當日往後推到末站(隔天開工) |
leadDays | 前置天數 | ceil((CT×qty+CO)/480),本期策略(§RCS-FS-6) |
leanPlayDate | LeanPlay 交期 | 節點完工日(節拍點錨定) |
lsd | 最晚開工日 | 節點**最晚開工日**(交期反推) |
sequence | 投產順序 | 輸出名次,依 leanPlayDate 全域排(≠ prioritySeq) |
prioritySeq | 進入順序 | 輸入處理/排擠先後(§RCS-FS-12,≠ sequence) |
startDate | 起排日 | LeanPlay 正推起點,預設今天+1(§RCS-FS-11) |
Resource | 資源 | 產能承載主體(鍵=資源×日);持 WorkCapability、id(如 LA01/LA02)(§7) |
WorkCapability | 工作能力 | 資源能做的製程-工作站集合 Map<ProcessId, Set<WorkCenterId>>;canPerform 本期 exact(§7.1) |
ResourcePool | 資源主檔 | 「有哪些資源、各能做什麼」;eligible(processId, workCenterId) 查 eligible 資源(§7.1) |
ResourceAllocationStrategy | 資源分配策略 | per-run 選一;同日多台候選 select(candidates) 挑一台。impl:IdAscendingStrategy(本期)/MostConstrainedFirstStrategy(Stage 3)(§7.4) |
ResourceRun | 資源佔用區間 | 小 VO:earliestRun 回傳 (resourceId, start, finish);resourceId 留引擎內、不外露(§7.3) |
NoEligibleResourceError | 無可用資源例外 | eligible 空集 → fail-fast 中止(§RCS-FS-19 / §7.2) |
eligible | 適用資源 | 工作能力涵蓋某節點製程-工作站的資源集合(§7.1) |
名詞表必須涵蓋所有出現在後續虛擬碼中的非標準詞彙。未列入者不得出現在虛擬碼中。
1. 粗排編排(Orchestration) §RCS-PC-1
1.1 逐張試算與彙整
對應實作:com.leanplay.roughcut.application.RoughCutScheduler#run
輸入:
orders: 一批ProductionOrder(含dueDate、prioritySeq)capacity:AvailableCapacity,進來已含 FREEZE 佔用(引擎外小模組備妥,§RCS-FS-9)params:SchedulingParams(起排日=今天+1、per-run 選定的LeadTimeStrategy)
輸出:
RoughScheduleResult:全批節點,每節點{leanPlayDate, lsd, sequence}(不可變快照)
虛擬碼:
function run(orders, capacity, params):
// 1.1.1 空批次守衛(§RCS-FS-8)
if orders is empty:
return RoughScheduleResult(nodes = [])
// 1.1.2 建立 per-run 情境(跑完丟棄、不寫回)
// capacity 進來已含 FREEZE;引擎只讀、只加 IN_RUN
overlay := CapacityOverlay(base = capacity)
ctx := SchedulingContext(startDate = params.startDate, // 預設今天+1(§RCS-FS-11)
strategy = params.strategy, // per-run 選定(§RCS-FS-6)
overlay = overlay)
// 1.1.3 依 prioritySeq 升冪排序(處理先後 ≠ 交期;§RCS-FS-12)
ordered := orders sorted by prioritySeq ascending
// 1.1.4 逐張試算,邊算邊併入工作帳(區域變數,非 ctx 狀態)
working := emptyMap() // coordinate → { leanPlayDate, lsd }
for order in ordered:
leanPlayDates := leanPlayCalculator.compute(order, ctx) // 正推;佔用 overlay(§2.1)
lsdDates := lsdCalculator.compute(order, ctx) // 反推;不碰 overlay(§4.1)
for node in order.vsm.allNodes:
working[node.coordinate] := { leanPlayDate: leanPlayDates[node],
lsd: lsdDates[node] }
// 1.1.5 全單混排:依 leanPlayDate 全域排序 → 投產順序(§5.1)
sequenceByNode := sequenceAssigner.assign(working)
// 1.1.6 凍結快照彙整(此處才生不可變 ScheduledNode、可回溯)
scheduled := []
for coordinate, dates in working:
scheduled += ScheduledNode(coordinate = coordinate,
dataBox = dataBoxOf(coordinate), // 快照複製輸入
leanPlayDate = dates.leanPlayDate,
lsd = dates.lsd,
sequence = sequenceByNode[coordinate])
return RoughScheduleResult(nodes = scheduled)設計理由:
- 計算結果用回傳值(C 案)而非 mutate ctx:
compute()必有「佔用 overlay」這個無法避免的副作用;把「答案」改回傳值後,測試只需斷言回傳值,副作用面最小、最好測。快照不變性靠「§1.1.6 才生ScheduledNode」結構保證,不靠紀律。替代方案 A(per-runWorkingNode)與 B(半可變ScheduledNode)皆放棄。 - 依
prioritySeq而非交期排序:§RCS-FS-12 明定處理先後=進入順序;交期早但prioritySeq在後者會被排擠(這是刻意可觀察的後果)。
例外情境:
- 本層不 raise;空批次回空結果(§RCS-FS-8),不報錯。
測試對應:
- 正常:functional-spec §UC A1 正常值(單張、產能充足,序 1..9)
- 邊界:functional-spec §RCS-FS-12 + §RCS-FS-2 + §RCS-FS-15(雙張,5002 被排擠,18 節點混排 1..18)
- 異常:functional-spec §RCS-FS-8(空批次 → 空結果)
2. LeanPlay 計算(節拍點錨定) §RCS-PC-2
2.1 LeanPlay 主流程
對應實作:com.leanplay.roughcut.domain.LeanPlayCalculator#compute
輸入:
order: 單張ProductionOrder(內含 VSM)ctx:SchedulingContext(起排日、策略、overlay)
輸出:
Map<OperationCoordinate, leanPlayDate>:本單各節點完工日
虛擬碼:
function compute(order, ctx): // 回傳 coordinate → leanPlayDate
vsm := order.vsm
leanPlay := emptyMap()
// 2.1.1 取節拍點(無指定 → fallback 末站;§RCS-FS-1)
takt := vsm.takt()
if takt is null:
takt := vsm.lastNode()
// 2.1.2 定錨:自起排日正推求節拍點「最早可開工日」(§2.2)
earliest := anchorTakt(vsm, takt, ctx)
// 2.1.3 有限產能堆疊:定下節拍點當日並佔用(§7.2,多資源版;原 §2.3 已廢棄)
taktDays := stackCapacity(takt, earliest, ctx) // DayRange(start, finish)
leanPlay[takt.coordinate] := taktDays.finish
// 2.1.4 拉動上游 N-x:由節拍點開工日往前 JIT(§2.4)
pullUpstream(vsm, takt, taktDays.start, ctx, leanPlay)
// 2.1.5 推動下游 N+y:由節拍點完工日往後推到末站(§2.5)
pushDownstream(vsm, takt, taktDays.finish, ctx, leanPlay)
return leanPlay設計理由:
- 只有節拍點查/佔產能,上游下游純 leadtime:粗排聚焦瓶頸(節拍點),其餘站假設產能足(§RCS-FS-2 只發生在節拍點)。
- 順序刻意為 定錨→堆疊→拉動→推動:因 §2.4 拉動是從「堆疊後定下的」節拍點開工日往前算,節拍點若被 §RCS-FS-2 排擠後移,上游自然整段跟著後移(§RCS-FS-15),不需額外邏輯。
測試對應:
- 正常:functional-spec §UC A1 正常值(A 06-06~07、產品 LeanPlay 06-10)
- 邊界:functional-spec §RCS-FS-1(無節拍點 → 末站為節拍點)
- 邊界:functional-spec §RCS-FS-2 + §RCS-FS-15(5002 節拍點與上游整段後移)
2.2 定錨(正推求節拍點最早可開工日)
對應實作:com.leanplay.roughcut.domain.LeanPlayCalculator#anchorTakt(private)
輸入:
vsm、takt、ctx
輸出:
虛擬碼:
function anchorTakt(vsm, takt, ctx):
earliestFinish := emptyMap()
// 2.2.1 沿上游拓樸序(葉站 → 近節拍點)逐站正推
for node in vsm.upstreamOf(takt) in topological order:
if node has no upstream: // 葉站
start := ctx.startDate // §RCS-FS-11 起排日
else:
start := max(earliestFinish[u] for u in node.upstream()) + 1 // 隔天開工
earliestFinish[node] := start + leadDays(node, ctx) - 1
// 2.2.2 節拍點開工 = 其直接上游中最晚完工 + 1(無上游 → 即起排日)
if takt has upstream:
return max(earliestFinish[u] for u in takt.upstream()) + 1
return ctx.startDate設計理由:
測試對應:
- 正常:functional-spec §UC A1(C 分支為關鍵路徑 → A 最早 06-06)
- 邊界:functional-spec §RCS-FS-11(葉站自起排日 06-03 開工)
2.3 有限產能堆疊(節拍點,§RCS-FS-2) → §7.2
廢棄(2026-06-09):單資源版被多資源版 §7.2 取代。原文(含虛擬碼)移至 §廢棄歷史;現行實作見 §7.2。
2.4 拉動上游(反向 JIT,§RCS-FS-15)
對應實作:com.leanplay.roughcut.domain.LeanPlayCalculator#pullUpstream(private)
輸入:
vsm、takt、taktStart、ctx、leanPlay(就地寫入)
輸出:
- 無回傳;寫入各上游節點的
leanPlayDate(完工日)
虛擬碼:
function pullUpstream(vsm, takt, taktStart, ctx, leanPlay):
startOf := emptyMap()
startOf[takt] := taktStart
// 2.4.1 由節拍點往上游反向拓樸序(近節拍點 → 葉站)逐站反推
for node in vsm.upstreamOf(takt) in reverse topological order:
// 完工 = 朝節拍點方向最早需要它的下游之開工日 − 1(緊鄰、JIT、不空等)
finish := min(startOf[d] for d in node.downstream()) - 1
start := finish - (leadDays(node, ctx) - 1)
startOf[node] := start
leanPlay[node.coordinate] := finish設計理由:
- 反向 JIT(緊鄰不空等)而非「停在最早」:短分支若停在最早可開工日會留閒置;JIT 把它拉到緊鄰節拍點完工。toy 中 B 分支被拉到 B2 06-05(不停在 06-04 空等)。
- 用
min(下游開工日):樹狀匯流時上游節點理論上只有一個朝節拍點的下游,用 min 對「萬一多下游」也安全(緊鄰最早需要它的那個)。
測試對應:
- 正常:functional-spec §UC A1(C1 06-03、B1 06-04、C2 06-04、B2 06-05、C3 06-05)
- 邊界:functional-spec §RCS-FS-15(5002 上游由 06-03
05 整段移到 06-0507)
2.5 推動下游(正推)
對應實作:com.leanplay.roughcut.domain.LeanPlayCalculator#pushDownstream(private)
輸入:
vsm、takt、taktFinish、ctx、leanPlay(就地寫入)
輸出:
- 無回傳;寫入各下游節點的
leanPlayDate;末站完工=產品 LeanPlay 交期
虛擬碼:
function pushDownstream(vsm, takt, taktFinish, ctx, leanPlay):
finishOf := emptyMap()
finishOf[takt] := taktFinish
// 2.5.1 由節拍點往下游拓樸序(近節拍點 → 末站)逐站正推
for node in vsm.downstreamOf(takt) in topological order:
start := max(finishOf[u] for u in node.upstream()) + 1 // 隔天開工;匯流取最晚上游
finish := start + leadDays(node, ctx) - 1
finishOf[node] := finish
leanPlay[node.coordinate] := finish測試對應:
- 正常:functional-spec §UC A1(D1 06-08、D2 06-09、D3 06-10;產品 LeanPlay 06-10)
3. 產能 overlay(引擎側 IN_RUN) §RCS-PC-3
overlay 多資源版(鍵=資源×日)見 §7.3,為現行實作。 原 §3.1
isAvailable(wc)/§3.2earliestRun(wc)/§3.3occupy(wc)(單資源(wc,day))已廢棄(2026-06-09),原文移至 §廢棄歷史。本節僅保留 §3.4(未用、供未來細顆粒)。
3.4 有效剩餘(Duration 版,本版未用)
對應實作:com.leanplay.roughcut.domain.CapacityOverlay#effectiveRemaining
輸入:
wc、day
輸出:
虛擬碼:
function effectiveRemaining(wc, day):
// 3.4.1 base 已扣 FREEZE(唯讀);再扣本 run IN_RUN。本版二元判斷用 §3.1,此法供未來細顆粒
return base.slotOf(wc, day).remaining() - sumInRunAmount(wc, day)設計理由:
- 本版(D5 二元)不依賴此法;保留 Duration 介面以利未來「同站可並行多單/分鐘級」擴充,屆時 §7.3.5(資源版
isAvailable)改以effectiveRemaining >= 所需判斷。
測試對應:
- 本版無直接 I/O 對應(未用);未來細顆粒度版再補。
4. LSD 反推 §RCS-PC-4
4.1 自交期反推各節點最晚開工日
對應實作:com.leanplay.roughcut.domain.LsdCalculator#compute
輸入:
order(含dueDate與 VSM)、ctx
輸出:
Map<OperationCoordinate, lsd>:各節點最晚開工日
虛擬碼:
function compute(order, ctx): // 回傳 coordinate → lsd(最晚開工日)
vsm := order.vsm
lsd := emptyMap()
// 4.1.1 自末站往上游反推(反向拓樸序:末站 → 葉),不看產能、不碰 overlay(§RCS-FS-13)
for node in vsm.allNodes in reverse topological order:
if node has no downstream: // 末站(產品輸出)
finish := order.dueDate // 交期即末站最晚完工
else:
finish := min(lsd[d] for d in node.downstream()) - 1 // 緊鄰下游最晚開工的前一天
lsd[node.coordinate] := finish - (leadDays(node, ctx) - 1) // 反推得最晚開工日
return lsd設計理由:
- 反推、不看產能(§RCS-FS-13):LSD 是「最晚不逾期」的純 leadtime 線,與 LeanPlay 的正推(看產能)方向相反;兩者之差即各節點餘裕。
lsd記最晚開工、leanPlayDate記完工(刻意不對稱):兩者定義使然(LSD=最晚開工、LeanPlay 交期=完工)。- §RCS-FS-3 不加守衛:交期早於今日時反推自然落到過去,演算法照常產出、不擋——引擎不判 overdue(§RCS-FS-7),逾期比對由下游做。
測試對應:
- 正常:functional-spec §UC A1(D3 06-15、A 06-11、C1 06-08…)
- 異常:functional-spec §RCS-FS-3(交期 06-01 → LSD 落過去如 05-25,不擋)
5. 投產順序 §RCS-PC-5
5.1 全單混排排序
對應實作:com.leanplay.roughcut.domain.SequenceAssigner#assign
輸入:
working:Map<OperationCoordinate, {leanPlayDate, lsd}>(全批所有單所有節點)
輸出:
Map<OperationCoordinate, sequence>:1..N 連續名次
虛擬碼:
function assign(working):
// 5.1.1 全單混排:所有節點依 leanPlayDate 升冪排序(§RCS-FS-12)
ordered := working.entries sorted by leanPlayDate ascending // 同日先後不具意義,穩定排序即可
// 5.1.2 連續編號 1..N
sequenceByNode := emptyMap()
seq := 1
for entry in ordered:
sequenceByNode[entry.coordinate] := seq
seq := seq + 1
return sequenceByNode設計理由:
sequence(輸出)≠prioritySeq(輸入):§1.1.3 用prioritySeq決定排擠先後;此處sequence是輸出、依完工日全域排(§RCS-FS-12)。- 同完工日先後不具意義:穩定排序即可,不額外定 tie-break 規則(functional-spec I/O 範例已註明)。
測試對應:
- 正常:functional-spec §UC A1(單張 1..9)
- 邊界:functional-spec §RCS-FS-12(雙張 18 節點混排 1..18;A:5001=9 / 5002=14)
6. 前置時間策略(per-run 選一) §RCS-PC-6
6.1 策略介面
對應實作:com.leanplay.roughcut.domain.strategy.LeadTimeStrategy#leadTimeDays
輸入:
node:VSMNode(含dataBox、coordinate.qty)
輸出:
- 整數天數
虛擬碼:
// 6.1.1 介面:per-run 選定一個(§RCS-FS-6 / 未解 #9)
interface LeadTimeStrategy:
function leadTimeDays(node): // 回傳整數天數設計理由:
- per-run 選定一個:整個 run 用同一策略,存於
SchedulingContext;切換策略不改演算法、只換 impl(OCP)。
測試對應:
- 介面本身無 I/O;見各 impl。
6.2 CtTimesQtyPlusCo(本期)
對應實作:com.leanplay.roughcut.domain.strategy.CtTimesQtyPlusCo#leadTimeDays
輸入:
node
輸出:
ceil((CT×qty + CO) / 480)天
虛擬碼:
function leadTimeDays(node):
// 6.2.1 本期:計造時間 + 換模,換算工作日
ct := node.dataBox.cycleTime // 分/件
co := node.dataBox.changeOverTime // 分(換模)
qty := node.coordinate.qty
minutes := ct * qty + co
return ceilDiv(minutes, WORK_MINUTES_PER_DAY) // 480 分/工作日設計理由:
minutes = 0(如 qty=0)→ 0 天,不加保底:忠於 spec(未提保底)、以 qty>0 為前提。若日後要保底改max(1, ceilDiv(...)),屬 §RCS-FS-6 規則微調。WORK_MINUTES_PER_DAY = 480為本期固定常數;班別不同需參數化(屬行事曆/strategy config,超出本期)。
測試對應:
- 正常:functional-spec §RCS-FS-6 / §I/O 範例(B1=1、A=2、D3=1 天,qty=100)
6.3 CtTimesQty(方案 2,保留切換)
對應實作:com.leanplay.roughcut.domain.strategy.CtTimesQty#leadTimeDays
虛擬碼:
function leadTimeDays(node):
// 6.3.1 不計換模 CO
minutes := node.dataBox.cycleTime * node.coordinate.qty
return ceilDiv(minutes, WORK_MINUTES_PER_DAY)測試對應:
- 本期未啟用(保留切換);啟用時對齊 functional-spec §RCS-FS-6 方案 2。
6.4 FixedDays(方案 3,保留切換)
對應實作:com.leanplay.roughcut.domain.strategy.FixedDays#leadTimeDays
虛擬碼:
function leadTimeDays(node):
// 6.4.1 生管給固定天數,依工作站查表
wc := node.coordinate.workCenterId
if wc not in daysByWorkCenter:
raise MissingFixedLeadTimeException // #9 殘留:「固定天數如何給」未定
return daysByWorkCenter[wc]例外情境:
- §6.4.1 —
MissingFixedLeadTimeExceptionwhen 某工作站未提供固定天數(functional-spec §RCS-FS-6 未解 #9)
測試對應:
- 本期未啟用(保留切換);啟用時對齊 functional-spec §RCS-FS-6 方案 3、#9 釐清後補。
7. 多資源 eligibility(棋盤,§RCS-FS-16–§RCS-FS-19) §RCS-PC-7
2026-06-09 eligibility cascade(承 fp #17)。產能承載由「工作站×日」下沉到「資源×日」:一個節拍點(製程-工作站)對應多台資源(如 L 底下 LA01/LA02),某節點只能用工作能力涵蓋其製程-工作站的 eligible 資源。原單資源版(§2.3、§3.1–§3.3)已廢棄、移至 §廢棄歷史;本節為現行實作。 粗排不指派實際資源、輸出不含資源;資源為 overlay 內部暫時記帳。
前向註記(2026-06-11|計畫,未動 tag/code):本節 eligibility(能力匹配=製程-工作站找資源)將抽出至跨 feature 共用模組 資源篩選 resource-selection(
RS,§RS-UR-1/-8/-12)。屆時 §7.1 改為委派 RS 第 1 層;粗排僅用其能力匹配層、僅在節拍點呼叫(§RS-UR-8),且與細排為共用同一機制、差異僅最後產能表示方式(§RS-UR-12)。重構真正執行時走 mid-stage edit →consistency-audit→ 重打rough-cut-scheduler/spec-vN+1→ code repo 跟進;在那之前本 spec 的 § 與 code 不變、不重新簽核(§RS-UR-9)。此註記為附加說明,不改任何 § 編號或行為。
7.0 編排層入參 delta(run/ctx)
append-only:不重寫 §1.1;此處記與 §1 的差異,實作 §1.1 時依此。
RoughCutScheduler.run入參加resourcePool(domain-modelResourcePool):run(orders, capacity, resourcePool, params)。SchedulingParams加allocationStrategy(per-run 選定的ResourceAllocationStrategy,比照strategy)。- §1.1.2
SchedulingContext多掛resourcePool與allocationStrategy;overlay仍包capacity(FREEZE 唯讀基準),鍵改資源×日。 - §2.1.3 仍呼叫
stackCapacity,但實作為多資源版 §7.2(上游拉動 §2.4/下游推動 §2.5 零改動)。
對應實作:com.leanplay.roughcut.application.RoughCutScheduler#run(入參 delta)、…SchedulingContext 建構
7.1 eligible 查詢(exact 製程-工作站匹配)
對應實作:
- §7.1.1
com.leanplay.domain.ResourcePool#eligible - §7.1.2
com.leanplay.domain.WorkCapability#canPerform
虛擬碼:
function eligible(processId, workCenterId): // ResourcePool;回傳 eligible 資源清單
// 7.1.1 逐資源比對能力(本期 exact)
result := []
for resource in resources:
if resource.canPerform(processId, workCenterId):
result += resource
return result
function canPerform(processId, workCenterId): // WorkCapability
// 7.1.2 該製程下可做的工作站集合是否含 workCenterId(未來「鄰近」relax 點就在這)
workStations := byProcess.get(processId) // 無此製程 → 空集
return workCenterId in workStations設計理由:
- 匹配邏輯收斂在
canPerform:eligible只掃資源、不懂匹配規則;本期 exact、未來「鄰近工作站」只改 §7.1.2 一處(呼應 OOA「relax 點集中於此」)。
測試對應:
- 正常:functional-spec eligibility I/O 正常值(PartA
LASR@L→{LA01, LA02}) - 邊界:functional-spec §RCS-FS-17 + eligibility I/O 邊界值(PartB
LASR@L2→{LA02}only) - 異常:functional-spec §RCS-FS-19(
LASR@L9→ 空集;fail-fast 在 §7.2 觸發)
7.2 多資源版 stackCapacity(取代 §2.3)
對應實作:com.leanplay.roughcut.domain.LeanPlayCalculator#stackCapacity(private;code 實作此多資源版)
虛擬碼:
function stackCapacity(takt, earliestStart, ctx): // 多資源版(取代 §2.3)
need := leadDays(takt, ctx)
// 7.2.1 查 eligible 資源(§7.1)
eligible := ctx.resourcePool.eligible(takt.coordinate.processId,
takt.coordinate.workCenterId)
// 7.2.2 eligible 空集 → fail-fast(§RCS-FS-19;資料/設定錯誤,異於 §RCS-FS-8 空批次容忍)
if eligible is empty:
raise NoEligibleResourceError(takt.coordinate)
// 7.2.3 在 eligible 集合上掃天找最早可佔,同日多台交 strategy 挑(§7.3)
run := ctx.overlay.earliestRun(eligible, earliestStart, need, ctx.allocationStrategy)
// 7.2.4 佔用挑中的資源 need 連續日(IN_RUN;resourceId 留引擎內、不外露)
ctx.overlay.occupy(run.resourceId, run.start, run.finish, takt.coordinate)
return DayRange(run.start, run.finish) // 對外只給節拍點當日起訖設計理由:
- §RCS-FS-19 拋、§RCS-FS-8 回空——刻意不對稱:eligible 空集是「沒有任何資源能做此製程-工作站」,屬資料/設定錯誤,fail-fast 中止整次粗排;空批次是正常的「沒單可排」,回空結果。
resourceId只進occupy、不進回傳:佔用必須記在某台資源上(不然 eligibility 記帳失真),但對外只回DayRange——呼應 FS「輸出不含機台、機台×日僅內部記帳」。- §2.1 主流程不動:差異全藏本段內,上游拉動/下游推動零改動。
測試對應:
- 正常:functional-spec eligibility I/O 正常值(5201/5202 皆
LASR@L→ 各佔一台、同日 06-03 並行,§RCS-FS-16) - 邊界:functional-spec eligibility I/O 邊界值(5301/5302 皆
LASR@L2、僅{LA02}→ 串行 06-03/06-04,§RCS-FS-16/§RCS-FS-17) - 異常:functional-spec §RCS-FS-19(eligible 空集 →
NoEligibleResourceError)
7.3 overlay 多資源版 earliestRun + 資源版 isAvailable/occupy(取代 §3.1–§3.3)
對應實作:
- §7.3.1–3
com.leanplay.roughcut.domain.CapacityOverlay#earliestRun(取代 §3.2) - §7.3.4
…CapacityOverlay#hasConsecutiveRun(private,新輔助) - §7.3.5
…CapacityOverlay#isAvailable(取代 §3.1) - §7.3.6
…CapacityOverlay#occupy(取代 §3.3)
虛擬碼:
function earliestRun(eligible, fromDay, need, strategy): // 回傳 ResourceRun(挑中資源 + 起訖)
day := fromDay
while true:
// 7.3.1 收集當天有 need 連續可用日的 eligible 資源(候選)
candidates := []
for resource in eligible:
if hasConsecutiveRun(resource.id, day, need):
candidates += resource
// 7.3.2 有候選 → strategy 挑一台(§7.4);同日多台=棋盤並行的選台點
if candidates is not empty:
chosen := strategy.select(candidates)
return ResourceRun(resourceId = chosen.id, start = day, finish = day + need - 1)
// 7.3.3 全 eligible 皆湊不出 need 連續日 → 往後一天(§RCS-FS-16;N=1 退化即 §RCS-FS-2)
day := day + 1
function hasConsecutiveRun(resourceId, fromDay, need): // 私有輔助
// 7.3.4 自 fromDay 起算 need 個連續可用日(單資源視角)
run := 0
cursor := fromDay
while run < need and isAvailable(resourceId, cursor):
run := run + 1
cursor := cursor + 1
return run == need
function isAvailable(resourceId, day): // 資源版(取代 §3.1)
// 7.3.5 該資源當日完全未被佔(FREEZE 或本 run IN_RUN)
return base.isAvailable(resourceId, day) // FREEZE + 行事曆(唯讀基準,§RCS-FS-9)
and noInRunOccupation(resourceId, day) // 本 run 尚未佔此資源此日
function occupy(resourceId, startDay, endDay, occupier): // 資源版(取代 §3.3)
// 7.3.6 對區間每日記一筆 IN_RUN 佔用(鍵=資源×日;base 唯讀)
for day from startDay to endDay:
inRun.add(resourceId, day, Occupation(occupier = occupier,
source = IN_RUN,
amount = fullDay(resourceId, day)))設計理由:
- day-major 掃天(找最早「任一 eligible 資源可佔」日):對齊 FS「所有 eligible 皆不足才往後一天」。先掃日、再在該日多台候選間選——保證全域最早日,選台只是同日 tie-break。N=1 退化即 §3.2 木桶。
- 抽
hasConsecutiveRun私有:多資源要對每台重複「單資源掃 need 連續日」,抽出讓 §7.3.1 讀得清,語意同 §3.2。 isAvailable/occupy鍵改resourceId:inRun由 (wc,day) 改 (resourceId,day),對齊 OOA 鍵下沉。
測試對應:
- 正常:functional-spec eligibility I/O 正常值(5201→某台、5202→另一台,同日 06-03 並行)
- 邊界:functional-spec eligibility I/O 邊界值(5302:LA02 於 06-03 滿 → §7.3.3 推 06-04、LA01 空著卻不 eligible,§RCS-FS-16/§RCS-FS-17)
- 邊界:functional-spec §RCS-FS-2(N=1 退化:單資源被佔 → 往後一天)
7.4 資源分配策略(ResourceAllocationStrategy,per-run 選一)
對應實作:
- §7.4.1
com.leanplay.roughcut.domain.strategy.ResourceAllocationStrategy#select - §7.4.2
…strategy.IdAscendingStrategy#select - §7.4.3
…strategy.MostConstrainedFirstStrategy#select(Stage 3,未實作)
虛擬碼:
// 7.4.1 介面:select 在同日候選中挑一台(§RCS-FS-18)
interface ResourceAllocationStrategy:
function select(candidates): // candidates: 非空 eligible 資源清單;回傳挑中 resource
// 7.4.2 本期 default:resource id 最小者(悲觀下界・§RCS-FS-18)
function select(candidates): // IdAscendingStrategy
return candidate with min id
// 7.4.3 MostConstrainedFirstStrategy — Stage 3 才實作、本期不掛載(FS #17/Q6)
// 規則:挑 capability 最窄者(通用台留給受限零件,減少不必要排擠)
// 介面與簽章已足(candidates 帶 capability);此處不展開虛擬碼,避免替未拍板細節背書設計理由:
- 比照 §6
LeadTimeStrategyper-run 選定、OCP:換分配規則只加 impl、不動 §7.3 掃天主體。 candidates保證非空:§7.3.2 只在候選非空時呼叫,策略不需處理空集(空集已在 §7.2.2 fail-fast)。- IdAscending=任意但確定的 tie-break(悲觀下界);最佳化延後 §7.4.3,故意不寫虛擬碼。
測試對應:
- §RCS-FS-18:functional-spec eligibility I/O 正常值(同日多台時挑 id 最小者)
- §7.4.3:Stage 3,本期無 I/O 對應
廢棄歷史
為保留追溯,廢棄段落不刪除,僅標記;集中於此底部區塊,主流程不留並存版本。
§2.3 有限產能堆疊(單資源,§RCS-FS-2) → 後繼 §7.2
廢棄 2026-06-09(eligibility cascade);多資源版見 §7.2(查 eligible → fail-fast → 掃天)。原虛擬碼:
function stackCapacity(takt, earliestStart, ctx):
need := leadDays(takt, ctx)
// 2.3.1 自最早可開工日找連續可用日,不足往後一天(§RCS-FS-2 排擠)
start := ctx.overlay.earliestRun(takt.workCenter, earliestStart, need)
finish := start + need - 1
ctx.overlay.occupy(takt.workCenter, start, finish, takt.coordinate)
return DayRange(start, finish)§3.1 二元可用判斷 isAvailable(wc, day) → 後繼 §7.3.5
isAvailable(wc, day)廢棄 2026-06-09(鍵下沉資源×日)。原虛擬碼:
function isAvailable(wc, day):
return base.isAvailable(wc, day) // FREEZE + 行事曆(§RCS-FS-9)
and noInRunOccupation(wc, day)§3.2 找最早連續可用日 earliestRun(wc, fromDay, need) → 後繼 §7.3.1–3
earliestRun(wc, fromDay, need)廢棄 2026-06-09(改吃 eligible 集合 + strategy → 回 ResourceRun)。原虛擬碼:
function earliestRun(wc, fromDay, need):
day := fromDay
while true:
run := 0
cursor := day
while run < need and isAvailable(wc, cursor):
run := run + 1
cursor := cursor + 1
if run == need:
return day
day := day + 1§3.3 佔用 occupy(wc, …) → 後繼 §7.3.6
occupy(wc, …)廢棄 2026-06-09(鍵下沉資源×日)。原虛擬碼:
function occupy(wc, startDay, endDay, occupier):
for day from startDay to endDay:
inRun.add(wc, day, Occupation(occupier = occupier,
source = IN_RUN,
amount = fullDay(wc, day)))簽核
- 編輯者:Alan / 日期:2026/06/09
- Reviewer:Alan / 日期:2026/06/09