粗排引擎 (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 覆驗一致。

#回填項來源 §原因
1LeanPlayCalculator.compute()void → 回傳 Map<OperationCoordinate, leanPlayDate>§2.1C 案(回傳值好測、快照不變性靠結構)
2LsdCalculator.compute()void → 回傳 Map<OperationCoordinate, lsd>§4.1同上
3SequenceAssigner.assign()void → 回傳 Map<OperationCoordinate, sequence>§5.1同上
4新增 WorkingNodeC 案不需要(取消)
5CapacityOverlay.baseFREEZE)邊界註記:由引擎外小模組備妥、引擎唯讀§1.1.2 / §3§RCS-FS-9 引擎不設定 Freeze
6VSM.lastNode() 新增§2.1.1§RCS-FS-1 節拍點 fallback「末站」
7VSM.upstreamOf/downstreamOf 語意澄清:回傳遞移節點集、且 VSM 可依(反向)拓樸序走訪全節點(allNodes§2.2 / §2.4 / §2.5 / §4.1多處正反推都靠它
8CapacityOverlay.occupy 支援「連續日區間」(OOA 目前單日)§3.3節拍點佔 L 連續日
9CapacityOverlay.inRun 需可依 (wc, day) 查詢(OOA 為 flat List<Occupation>Occupation 不帶 day)§3.1 / §3.3 / §3.4按日查 IN_RUN
10CapacityOverlayisAvailable(wc, day)effectiveRemaining 標「本版未用/供未來細顆粒」§3.1 / §3.4D5 二元可用語意

0. 名詞表

名詞中文定義
RoughCutScheduler粗排引擎編排者;run(orders, capacity, params),純試算、不寫回
LeanPlayCalculatorLeanPlay 計算器節拍點錨定正推+拉動上游/推動下游
LsdCalculatorLSD 計算器自交期反推、純 leadtime、不看產能
SequenceAssigner投產順序指派全單混排依完工日排序
LeadTimeStrategy前置時間策略per-run 選一;leadTimeDays(node)
SchedulingContext排程情境本 run 狀態:起排日、選定策略(leadTime + 資源分配)、產能 overlay、資源主檔 ResourcePool(§7.0)
CapacityOverlay產能 overlayFREEZE(唯讀基準)+ 本 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
leanPlayDateLeanPlay 交期節點完工日節拍點錨定)
lsd最晚開工日節點**最晚開工日**(交期反推)
sequence投產順序輸出名次,依 leanPlayDate 全域排(≠ prioritySeq
prioritySeq進入順序輸入處理/排擠先後(§RCS-FS-12,≠ sequence
startDate起排日LeanPlay 正推起點,預設今天+1(§RCS-FS-11
Resource資源產能承載主體(鍵=資源×日);持 WorkCapabilityid(如 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(含 dueDateprioritySeq
  • 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 ctxcompute() 必有「佔用 overlay」這個無法避免的副作用;把「答案」改回傳值後,測試只需斷言回傳值,副作用面最小、最好測。快照不變性靠「§1.1.6 才生 ScheduledNode」結構保證,不靠紀律。替代方案 A(per-run WorkingNode)與 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

設計理由

測試對應


2.2 定錨(正推求節拍點最早可開工日)

對應實作com.leanplay.roughcut.domain.LeanPlayCalculator#anchorTakt(private)

輸入

  • vsmtaktctx

輸出

虛擬碼

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

設計理由

  • 先正推再(§2.4)反推:一開始無任何錨點,得先用「自葉站往後推」沿**關鍵路徑(最長上游分支)**求出節拍點能落的最早日;錨點定下後,上游才改由節拍點往前拉。

測試對應

  • 正常: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)

輸入

  • vsmtakttaktStartctxleanPlay(就地寫入)

輸出

  • 無回傳;寫入各上游節點的 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-0305 整段移到 06-0507)

2.5 推動下游(正推)

對應實作com.leanplay.roughcut.domain.LeanPlayCalculator#pushDownstream(private)

輸入

  • vsmtakttaktFinishctxleanPlay(就地寫入)

輸出

  • 無回傳;寫入各下游節點的 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.2 earliestRun(wc)/§3.3 occupy(wc)(單資源 (wc,day)已廢棄(2026-06-09),原文移至 §廢棄歷史。本節僅保留 §3.4(未用、供未來細顆粒)。


3.4 有效剩餘(Duration 版,本版未用)

對應實作com.leanplay.roughcut.domain.CapacityOverlay#effectiveRemaining

輸入

  • wcday

輸出

  • Duration:base 剩餘(已扣 FREEZE)再扣本 run IN_RUN

虛擬碼

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(含 dueDateVSM)、ctx

輸出

虛擬碼

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-13LSD 是「最晚不逾期」的純 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(含 dataBoxcoordinate.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 — MissingFixedLeadTimeException when 某工作站未提供固定天數(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-selectionRS§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-model ResourcePool):run(orders, capacity, resourcePool, params)
  • SchedulingParamsallocationStrategy(per-run 選定的 ResourceAllocationStrategy,比照 strategy)。
  • §1.1.2 SchedulingContext 多掛 resourcePoolallocationStrategyoverlay 仍包 capacityFREEZE 唯讀基準),鍵改資源×日。
  • §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

設計理由

  • 匹配邏輯收斂在 canPerformeligible 只掃資源、不懂匹配規則;本期 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-19LASR@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-19eligible 空集 → NoEligibleResourceError

7.3 overlay 多資源版 earliestRun + 資源版 isAvailableoccupy(取代 §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。
  • isAvailableoccupy 鍵改 resourceIdinRun 由 (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 LeadTimeStrategy per-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

廢棄 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

廢棄 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

廢棄 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