跳轉到

2026-W34 週報:離開電力樹的那一週

第四份週報,本週 3 張卡——比上週少兩張,而且兩天的缺口原因不同(見下面「節奏」)。

但這三張卡的份量不在數量。W31–W33 的十三張卡有一個共同前提,只是從來沒被說出口:每張卡都是電力樹上的一個節點,六格裡的「拓撲位置」永遠答得出「上游接誰、下游接誰」。 本週三張卡全部破了這個前提:

它住在哪 「上游接誰」怎麼答
dc-09b 電池測試制度 時間軸 答不出來。它的上游是 commissioning,下游是汰換決策
dc-09c 鋰電消防合規 第二棵樹(空間/防火區劃) 答不出來。它問的是「這個房間裡堆了多少可燃能量」
dc-10 STS 電力樹上,但關鍵事實在 有兩條上游邊,恰好一條導通——upstream_id: str 直接破功

一週之內,同一個資料模型被三種不同的方式證明「節點字典 + upstream_id」不夠用:一次是時間、一次是第二棵樹、一次是邊。 這不是巧合,是佇列走到電力鏈末端的必然——愈往下游,設備愈少,而規則愈多。

節奏(誠實紀錄):五個工作日只產出 3 張卡,8/19(三)與 8/21(五)沒有卡。兩天的原因不一樣,第二天的原因是本站自己的基礎設施

  • 8/19:無 commit、無 lock 殘留、無檔案異動,原因不明。當作單純的一天空窗。
  • 8/21 起dc-10 的檔案在 8/20 寫進去了,但 commit 沒有完成——.git/index.lock 停在 Aug 20 08:58,一直留到今天。這正是 CLAUDE.md 警告過的沙箱坑(不能刪檔 → git unlink 不掉自己的 lock),而它擋住了 8/21 之後的每一次排程與本機 git。本次週報開工時已清除 lock,並補上 feat(dc): card dc-10 static-transfer-switch 這筆遺失的 commit。

建議CLAUDE.md 說「每支排程 skill 開工前與收工後都要 rm -f .git/*.lock」——dc-daily-card 顯然只做了「開工前」或根本沒做。收工後那次才是關鍵,因為 lock 是 commit 動作自己留下的。另外可以加一道更便宜的保險:每次開工先檢查 git status 有沒有上一次遺留的 staged 變更,有就先補 commit 再開始今天的卡——本週若有這道檢查,8/21 不會空掉。

本週卡片

標題 這張卡最關鍵的一個事實
dc-09b 電池測試制度(IEEE 1188) 內阻沒有絕對門檻,只有相對基準線的偏差——而那條 baseline 必須在投入服務約 6 個月後量,錯過就永遠補不回來。同一筆讀值換個分母(baseline 3.20 → 串平均 3.80),35.9% 變 14.5%,判定完全翻轉
dc-09c 鋰電消防合規(NFPA 855 / UL 9540A) 消防合規用銘牌最大儲能,不是你模型裡的可用容量——同一組電池是 140 kWh 不是 112 kWh。而 25% LFL = 10,000 ppm,比 off-gas 偵測的工作範圍高 3–4 個數量級:它不是早期預警,是防爆的最後緩衝
dc-10 靜態切換開關 STS active_source 是邊的狀態不是節點的欄位,而「有兩個源」是宣稱不是事實——至少四件事會讓冗餘歸零卻不改變儀表板上任何數值。SCR 故障是全軌跡第一個 latched 告警:ack 掉之後設備仍然只剩一個源

串起來

一、三張卡,三種「不住在電力樹上」的事實

W33 的結論是「有些答案不是一個數,是一個數加上它的來歷」。本週更狠一層:有些事實根本不在這棵樹上。

                    ┌─────────────── 時間軸(dc-09b)───────────────┐
                    │ commissioning baseline → 季測 → 年測 → 汰換    │
                    │ 沒有協定、沒有感測器、只有一台儀器和一個工程師  │
                    └───────────────────┬───────────────────────────┘
                                        │ 描述同一批電池
電力樹:  … → LV dc-07 → UPS dc-08 → STS dc-10 → (dc-11 PDU)
                            │             ↑
                     電池 dc-09        邊上的狀態
                            │          active_source
                                        │ 同一批電池
                    ┌───────────────────┴───────────────────────────┐
                    │ 空間樹(dc-09c)                               │
                    │ Site → Building → Floor → FireCompartment → … │
                    │ 電力樹上並排的 A、B 兩路,在這裡可能是同一個節點 │
                    └───────────────────────────────────────────────┘

dc-09 那組電池同時是三棵結構的成員,而三棵結構的邊完全不同:

結構 邊的意義 往上走是為了
電力樹 誰餵誰 找源頭、算容量
時間軸 誰是誰的基準線 判斷「它現在還行不行」
空間樹 誰在誰的區劃裡 聚合能量(合規)

這是「DCIM 不能只做設備清單」的第二個具體證據(第一個是 W33 那條跨三實體的告警規則)。而且它比第一個更根本:W33 說的是「規則不能掛在設備上」,本週說的是「連『範圍』本身都由另一棵樹決定」——aggregate_energy() 該加總哪幾台,答案不在電力樹上。

二、消防要求回頭吃掉電力鏈的容量與續航——本週最反直覺的一條連鎖

dc-09c 演算 4 有一句容易滑過去的話:NFPA 855 2026 版 §4.10 要求排風機這類關鍵安全系統掛 NFPA 110/111 緊急電源。把它接回去看:

鋰電選型(dc-09)
   └→ 落入 NFPA 855 管制(dc-09c 演算 1:140 kWh,門檻 20 kWh,7 倍)
        └→ 需要連續機械排風 ≥ 1 CFM/ft²(60 m² 電池室 ≈ 1,100 m³/h)
             └→ 排風機必須掛發電機盤(2026 版 §4.10)
                  └→ 回頭增加 dc-05 發電機的必要容量
                       └→ 回頭縮短 dc-06 的燃油續航(16.40 h 那個數字要重算)
                            └→ 而 dc-06 的續航門檻是 Uptime Tier 的 12 h

一個電池化學的選型決定,經過消防規則,繞了一圈回來改變柴油續航。 這條邊在單線圖上不存在、在設備清單上不存在,只有在「兩棵樹疊在一起」時才看得見。

風機本身耗電不大(1,100 m³/h 級的排風機約 1 kW 量級,相對 MW 級機房是零頭),所以這條連鎖的價值不在數字大小,在於它證明了兩棵樹之間有真實的依賴邊。而且它是雙向的:消防規則要求排風機掛發電機(空間樹 → 電力樹),發電機的可用性又決定排風機在停電時還轉不轉(電力樹 → 空間樹)。

對模型的意涵FireCompartment 不能只是聚合能量的容器,它得能宣告「本區劃的安全系統依賴哪些電力節點」。這是兩棵樹之間的第一條橫向邊dc-10bDeviceRegistry 時要預留它的位置——否則第二輪冷卻鏈(dc-22 CRAH 接不接 UPS)會第二次撞上同一件事。

三、「冗餘暫時為零」本週一次出現三種,全部不可觀測

這是 W32「只有故障當下才會被用到的功能」與 W33「保護等級降級是狀態不是屬性」的合流版,本週把它推到最完整:

冗餘歸零的方式 持續多久 儀表板顯示
dc-09b 容量測試:串離線放電+再充電 8–24 h 10–27 h(放電只佔 1 h) 窗口關閉後轉綠,實際保護還沒回來
dc-09b 量內阻:並聯串要離線才準 季測,數小時 完全不顯示
dc-09c A、B 兩路電池同一防火區劃 永久(設計缺陷) 完全不顯示
dc-10 transfer_inhibit / SCR latched / 維修旁路 / 兩源上游合流 視情況 「Source 1 正常、Source 2 正常」仍為真

四行裡有三行是「儀表板不會變」。這已經不是告警設計問題,是資料模型問題:只要系統裡存在一個叫 source_countredundancy_level 的欄位,它就會在上面四種情境下全部說謊。

dc-10 給出了正確的形狀:redundancy_effective() 是衍生值,不是欄位,而且它的輸入包含 latched 告警與維護窗口。dc-09bMaintenanceWindow(start, end, recharge_until, effective_redundancy_delta) 是同一件事的時間版本。兩者應該接起來——見下面連貫性檢視 #2。

四、本週母題:判定是「事實 × 判準」的笛卡爾積

四週四個母題,這次是最抽象的一個,也是最快就能寫成 code 的一個:

週次 母題
W31 單一欄位裝不下一個事實(數字要帶著量測條件)
W32 有些事實根本不住在欄位裡(住在序列、存量、多對多裡)
W33 上限是多候選取 min/max,裸數字丟掉了最有用的資訊
W34 判定不是事實的屬性,是事實與判準的笛卡爾積——而判準本身是版本化的資料

三張卡各自獨立撞到它,用詞不同但形狀一模一樣:

事實 判準 判準為什麼必須是資料
dc-09b OhmicReading 4.35 mΩ ThresholdPolicy 20% / 30–50% / 50% 三個門檻服務三個目的:安全裕度/工程指引/商業契約。同一筆讀值同時是「該換了」與「不能索賠」
dc-09c EssUnit 140 kWh CodeRule NFPA855_2023 / 2026 / TW 2023 版有能量表、2026 版移除改 HMA、台灣指引又是另一套。三者會並存好幾年
dc-10 scr 狀態 + breaker_open latched 語意 判定是歷史的函數(曾經壞過、有沒有人來修),不是即時量測的函數

前兩張的結論完全一致,而且 dc-09c 自己點名了:「跟 dc-09bThresholdPolicy 是同一個形狀」。兩張卡看見了同構,卻各寫了一份 code——這是本週連貫性檢視的第一順位。

第三張是這個母題的邊界案例,值得單獨記:dc-10 的 SCR latched 告警把家族補到三個,且三個都不能只看即時遙測算出來

測試型  :last_verified_at 過期      → 「你不知道它還行不行」   (dc-05c / dc-06 / dc-07b / dc-09b)
可算型  :Isc(config) > Icw          → 「這個配置本身違規」     (dc-07b / dc-09c 能量閘門)
latched :曾經壞過且沒人來修          → 「它現在確實是壞的」     (dc-10)★ 新增

注意 dc-09c 的能量閘門落在「可算型」——枚舉區劃、加總銘牌、比對規則,不必等火災。W33 說過可算型的正確歸宿是互鎖而非告警;消防這裡的對應物是設計審查與 AHJ 竣工查驗,同樣是「在事情發生前用制度封死」,不是掛一條會被 ack 掉的告警。

自我測驗

Q1(事實回憶). 某 12 V block 的 commissioning baseline 是 3.20 mΩ,本季量到 4.35 mΩ,該串的串平均是 3.80 mΩ。這顆電池該換嗎?能跟廠商索賠嗎?

答案

兩個問題的答案不一樣,而且兩個都對。

分母 = baseline    :(4.35 − 3.20) / 3.20 = 35.9%
分母 = string_avg  :(4.35 − 3.80) / 3.80 = 14.5%   ← 判定完全翻轉

35.9% 的三種判定:

門檻 出處 判定 動作
20% CPBS 引 IEEE 1188 較新版本 超標 排汰換
30–50% IEEE 1188-2005 Annex C.4 原文「typically a change of 30% to 50% … is considered significant」 灰帶 加測、追趨勢
50% 廠商保固索賠門檻 未超標 索賠被打回

所以答案是:該換,但要不回錢。 這不是資料品質問題,是三個門檻服務三個不同目的(保守營運/工程指引/商業契約)。而且部分廠商用串平均當分母——換個分母,35.9% 直接變 14.5%,連「該換」都不成立了。

兩個容易漏的前置條件:baseline 必須在投入服務約 6 個月後量(新電池尚未完全化成,型錄值也不能用),而且必須是同一台儀器——換儀器 = baseline 作廢,instrument_id 不是稽核欄位,它是 trend 的分組鍵

對你的模型:門檻是策略表 ThresholdPolicy(kind, source, denominator, pct, action),多筆並存;denominator ∈ {baseline, string_avg} 必須是欄位;一筆讀值 fan-out 成多筆 Finding。把 status: "bad" 寫進 OhmicReading,等於把當下這套策略燒進歷史資料。

Q2(事實回憶). 你的 dc-09 模型裡 energy_kwh() 回傳 28.0。2N、每路 2 個 bank。消防合規上這座機房算幾 kWh?裝了 25% LFL 氫氣偵測器算不算有早期預警?

答案

140 kWh,不是 112;而且 LFL 偵測器不是早期預警。

銘牌 = 28.0 / 0.8(DoD) = 35.0 kWh   ← 合規用這個
4 × 35.0                 = 140.0 kWh
對鋰系門檻 20 kWh        → 7 倍,毫無疑問落入管制
(若誤用 usable:4 × 28.0 = 112.0,小 20%)

差這 20% 足以讓一個「算起來 580 kWh、以為過了 600 kWh 那道閘門」的設計實際上是 725 kWhdc-09 現在的 energy_kwh() 已經乘過 DoD,它是運維的數字,不是合規的數字——見連貫性檢視 #1,這是本週唯一一個會直接產生錯誤數字的問題。

LFL 那半:

$$25\%\ \text{LFL(H}_2) = 0.25 \times 4\ \text{vol}\% = 1\ \text{vol}\% = \mathbf{10{,}000\ ppm}$$

off-gas 偵測工作在 ppm 到 ppb,兩者差 3–4 個數量級。等 LFL 偵測器叫,房裡氫氣已累積到 off-gas 門檻的一萬倍——電池早就在排氣了。25% LFL 的用途是防止累積到爆炸下限(4:1 安全係數),不是提前告訴你哪顆 cell 快壞了。NFPA 75(2024 版)首次為鋰電 UPS 加入 off-gas 要求,正是因為傳統可燃氣體感測器不是合適的替代品

對你的模型nameplate_energy_kwhusable_energy_kwh 必須是兩個欄位;偵測器必須有 detector_class ∈ {off_gas, lfl, smoke, thermal, bms}告警動作與時間預算綁在 class 上——同一顆「氣體偵測」告警,off-gas 給你數十分鐘,LFL 給你的是疏散時間。塞進同一張表用同一套嚴重度分級,等於把最有價值的那三十分鐘丟掉。

Q3(應用). 廠商說「STS 轉換 4 ms,遠低於 IT 設備 20 ms 容忍度」。把台灣機房的真實最壞情況算出來。這條規則該掛在哪個實體上?

答案

台灣是 60 Hz,所以最壞是 5.50 ms,不是卡片表格裡那個 3.00 ms。

60 Hz:1 cycle = 16.67 ms
       1/4(快速 POG)      =  4.17 ms
       3/4(抗湧流 VSS)     = 12.50 ms

餘裕基準取 WP 62 實測重載 SMPS 的 18 ms(比 ITIC 的 20 ms 誠實):
       60 Hz + POG :18 − 4.17  = 13.83 ms
       60 Hz + VSS :18 − 12.50 =  5.50 ms   ← 本站真正的最壞
      (50 Hz + VSS:18 − 15.00 =  3.00 ms   ← 歐洲/中國,本站不適用)

餘裕從 13.83 掉到 5.50 ms,剩下不到四成。 觸發它的是一個完全不在 STS 身上的事實:下游若是變壓器型 PDU(dc-11),快速轉換會讓磁通不連續而產生湧流,必須開抗湧流演算法

另外兩個吃餘裕的變數同樣不在 STS 身上:市電頻率(型錄那個 4 ms 通常是 60 Hz 的數字,但別的地區不是)與下游負載率(18 ms 是重載實測,輕載長很多,但不能拿輕載數字做設計)。

掛在哪:三個輸入沒有一個是 Sts 的欄位,所以 effective_transfer_ms() 不該是 Sts 的 method。它是 algorithm × line_hz × downstream_has_transformer × load_pct 的函數,跟 W33 那條「電池 runtime > 發電機起動時間 × 安全係數」同一類——跨實體規則,必須是有 id、有輸入清單、有版本的一等公民。這是同一個結論第三次出現(dc-05c 合規規則、W33 Q4 電池 vs 發電機、本週 STS 轉換時間),Rule 這個實體到今天還不存在。

存成 transfer_time_ms = 4 的系統,在有人為了下游 PDU 把演算法切成抗湧流那天起,就開始說謊。

Q4(應用). 你把 A 路的年度電池容量測試排在週日 02:00–04:00,同一個週日 10:00–12:00 排 STS 維修旁路作業。兩個窗口在行事曆上不重疊。這樣排安全嗎?

答案

不安全,而且 10:00–12:00 那兩小時是本週三張卡疊起來的最壞情況。

02:00  A 路電池離線,接 DC load bank
03:00  放電結束(只佔 0.5–1 h)
04:00  行事曆上的 end —— 儀表板轉綠
04:00  開始再充電…… 要 8–24 h → recharge_until = 12:00 ~ 隔日 04:00
─────────────────────────────────────────────────────
10:00  STS 進 MAINTENANCE:電子元件隔離,沒有自動轉換
12:00  STS 作業結束

10:00–12:00 期間同時成立三件事

  1. A 路電池尚未充飽dc-09b 演算 4:總窗口 10–27 h,放電只佔其中 1 小時),這一路實際上沒有 ride-through;
  2. STS 在維修旁路dc-10):redundancy_effective() = False,下游單路設備完全沒有自動轉換;
  3. 儀表板全綠——第一項在行事曆上已經「結束」,第二項只是一條 INFO。

再加第四層:若 A、B 兩路電池在同一個防火區劃dc-09c),那麼連「還有另一路可以救」都是假的——電力樹上的 2N 在消防樹上是 1N,而這件事在單線圖上看不出來,只在建築平面圖上看得到。

對你的模型(三個具體結論):

  • MaintenanceWindow 的衝突檢查必須用 recharge_until,不是 end 兩個欄位獨立存在的唯一理由就是這題。順帶一提量內阻也不是免費的(並聯串要離線才準),季測也該進同一套衝突檢查。
  • 衝突檢查的對象不是「時間重疊」,是「effective_redundancy_delta 疊加後是否歸零」。 兩個窗口可以在時間上重疊而完全無害(不同故障域),也可以在時間上不重疊而致命(本題)。
  • redundancy_effective() 必須把「進行中的維護窗口」當輸入,跟它把 latched 告警當輸入是同一個道理——冗餘是算出來的,不是存出來的。

Q5(建模判斷). 本週三張卡產出了兩種「判定」:dc-09bdc-09cFinding,與 dc-10Alarm。它們是同一件事嗎?如果混用會怎樣?該怎麼收斂?

答案

不是同一件事,而且混用會長出兩套平行的告警系統——這是本週最該立刻決定的一件事。

先看它們的實際差異:

Finding(dc-09b / dc-09c) Alarm(dc-10)
怎麼產生 純函數evaluate(事實, 判準) → list[Finding] 有生命週期:raised → acked → reset
有沒有狀態 沒有。輸入不變,重跑結果就一樣 latched / acked / reset_at
重算會怎樣 隨時可全量重算,換了判準舊資料也能重評 不能重算——重算會抹掉「誰在什麼時候 ack 了什麼」
典型例子 「這批電池對 IEEE 20% 超標、對保固 50% 未超標」 「S1 SCR 短路,進線開關已跳開,等人來修」

混用的具體災難有兩種,方向相反:

  • FindingAlarm(給它 acked 欄位):判定變成有狀態的,於是換門檻之後不能重算——你會發現舊資料的判定是用兩年前的策略算的,跟新資料不可比。這正是 dc-09b Q3 那句「把 status: "bad" 寫進 OhmicReading 就等於把當下這套策略燒進歷史資料」的翻版,只是換了個容器。
  • AlarmFinding(每輪重算):latched 語意直接消失。SCR 修好前條件確實還在,看起來沒事;但一旦有人手動把 scr 標回 OK 而沒真的修,或任何一次重算的輸入漏了,那條「這台 STS 沒有冗餘」就會安靜消失。

收斂建議——兩層,不要合併:

@dataclass(frozen=True)
class Finding:                       # 純判定,可重算,無狀態
    subject_id: str                  # cell / compartment / bank …
    policy_id: str                   # 指向 ThresholdPolicy 或 CodeRule
    verdict: str                     # PASS | FAIL | REQUIRES_HMA | NOT_IN_SCOPE …
    computed_at: datetime
    inputs: dict                     # 讓判定可稽核、可重現

@dataclass
class Alarm:                         # 有生命週期的實體
    code: str                        # 穩定識別碼,去重/抑制/runbook 用
    severity: Sev
    entity_ids: list[str]            # 複數(W33 已定案)
    raised_at: datetime
    latched: bool = False
    acked: bool = False
    reset_at: datetime | None = None
    reset_by: str | None = None
    origin_finding_id: str | None = None    # ★ 兩層之間唯一的橋

關鍵是最後那一行Alarm 可以由 Finding 升上來,但不等於 Finding。判定持續為 FAIL 才升成 Alarm;Alarm 一旦升起就有自己的生命週期,不再隨判定消失而消失(latched 者尤其)。

三個立刻可驗證的約束:

  1. ack()latched 告警不得改變 active() ——允許 ack 消掉它,等於允許一個人用一次點擊把「這台 STS 沒有冗餘」變成看不見。
  2. Finding 不得有 ackedAlarm 不得有 policy_id(它應該透過 origin_finding_id 去查)。
  3. redundancy_effective() 讀的是 scr / breaker_open / mode / 維護窗口這些「事實」,不是讀 Alarm dc-10 自我檢核 Q3 寫「告警反過來成為狀態計算的來源」——這句話容易被實作成循環依賴alarms()redundancy_effective(),而 redundancy_effective()alarms())。正確的方向是:latched 的故障狀態住在 scr / breaker_open 上,Alarmredundancy_effective() 都是它的下游。 卡片的骨架其實做對了,但那句話的措辭會誤導未來的自己。

一句話總結本週的建模躍遷:W33 教你「有些答案是一個數加上它的來歷」,W34 教你「判定不是事實的屬性,是事實乘上判準——而判準有版本、有出處,判定本身還分成可重算的與有生命週期的兩種。」

間隔複習

本週抽兩週前(W32);四週前(W30)尚無卡片。

週次 抽兩週前 抽四週前
W33 W31 的 dc-02 尚無(W29 無卡片)
W34(本週) W32 的卡(dc-04 ~ dc-05c)✅ 尚無(W30 無卡片)
W35 W33 的卡(dc-06 ~ dc-09 W31 的卡(第二次)← 雙軌全開
W36 W34 的卡(dc-09b ~ dc-10 W32 的卡(第二次)

抽中 dc-05c NFPA 110 測試制度(W32,2026-08-07)。挑它的理由不是隨機:本週的 dc-09b 犯了跟它一模一樣的錯,而且是同一個機制。

複習題(W32 dc-05c × 本週 dc-09b). dc-05c 說 30% 月測門檻的分母是「ESP 銘牌」。為什麼不能用降載後的 kW?這個錯誤在本週用另一個面貌又出現了一次——在哪裡?兩者的共同機制是什麼?

答案

dc-05c 那半:

NFPA 110 月測要求帶到銘牌額定的 30% 以上,而分母是 ESP(緊急備用額定)銘牌——五個 rating class 裡最大的那個,而且不套場址降載(溫度、海拔、進氣)。

正確:ESP 2750 kW × 30% = 825 kW
錯誤(用 COP):2100 × 30% = 630 kW
錯誤(再套場址降載,假設 −10%):1890 × 30% = 567 kW

選錯分母,門檻永遠偏小(825 → 567,少了 31%)。後果特別惡毒:你以為合規,實際上長期輕載,而長期輕載正是 wet stacking 的成因——這台機會因為「合規測試」本身而慢慢變壞。

本週的同一個錯誤(dc-09b 演算 2):

容量測試週期的升級條件之一是「已達 85% 預期服務壽命」。分母該用 service_life_est_years(估算值),不是 design_life_years(型錄值)。

正確:service_life 5.8 年 × 0.85 = 4.9 年 → 第 5 年起改年測
錯誤:design_life 10 年 × 0.85 = 8.5 年 → 第 8.5 年才改年測

晚了三年半,而那三年半正好是電池最可能掉下去的時段。

共同機制(這才是這題的重點):

選錯分母
  → 門檻/時點往「寬鬆」的方向移動
    → 系統不會報錯,因為兩個分母都是合法的數字、都存在於資料庫裡
      → 所有檢查都通過、所有儀表板都是綠的
        → 而設備正因為「合規」本身而在劣化(wet stacking/電池未被及時加密監測)

兩次的方向完全一致:錯誤的分母永遠讓標準變鬆,不會變嚴。 原因很簡單——會讓標準變嚴的錯誤會立刻造成誤報,有人會來查;讓標準變鬆的錯誤沒有人會發現。這類 bug 的偵測成本是不對稱的,所以必須在 schema 層擋掉,不能靠測試發現。

對你的模型:凡是「門檻 = 基準 × 係數」的地方,基準必須是具名的、有型別的、不可互換的design_life_yearsservice_life_est_years 分兩欄還不夠——dc-09b 的驗收表裡那行「design_life=10 誤填進 service_life → 仍是 2 年測」證明了只要它們是同型別的 float,就一定有人填錯。同理 esp_kw / cop_kw / prp_kw 也不能都是 float。W33 提的 Bound 值物件正好可以擴充成這個用途:讓「這是哪一個額定」變成型別的一部分,而不是欄位名稱的一部分。

這也是 W33 連貫性檢視 #5 排在第二順位的另一個理由——它現在同時解決兩個問題。

四週前(W30):尚無。 本軌跡起始於 2026-07-28,W30 無卡片。W35 起雙軌全開,屆時每份週報多一題(抽 W31 的 dc-01 ~ dc-03b,第二次複習)。

本週的 model code 連貫性檢視

先結算舊帳:

來源 項目 狀態
W33 #1 Assembly 名稱碰撞 ⚠️ 仍未收斂(本週三張卡沒碰 Assembly,但也沒修)
W33 #2 EnergyStore 抽象 ⚠️ 未動;dc-09c 又加了第三個能量欄位語意
W33 #3 DeviceRegistry 缺席 🟡 終於有排程了——dc-09b 加分題要求建、dc-09c 動手練習疊第二種遍歷、dc-10b 的動手練習就是它。欠四週,但下週會清掉
W33 #4 Alarm 形狀 本週被採納並擴充——dc-10 定義了 Alarm(code, severity, message, entity_ids, latched, acked)這是週報建議第一次真的進到卡片裡
W33 #5 Bound 值物件 ⚠️ 未動。但本週的間隔複習題給了它第二個用途(見上)
W33 #6 命名漂移 usable_* 部分收斂——dc-09cusable_energy_kwh(),符合建議的 usable_<unit>() 慣例
W31 #3 id 參照 vs 物件內含 本週事實上收斂了——OhmicReading.cell_idBaselineSet.bank_idEssUnit.compartment_idFireCompartment.parent_idAlarm.entity_ids 全部用 id。W33 訂的規則(持久化層一律 id)被三張卡一致遵守,四週來第一次有規則真的黏住

兩則好消息值得記下來:W33 的 Alarm 建議與 id 參照規則都被採納了。規律看起來是:具體到可以直接貼進 code 的建議會被採納,抽象的(EnergyStore 協定、Assembly 合併)不會。 下面的建議因此全部寫成可貼的 dataclass。

本週新增五處。

🔴 1. dc-09energy_kwh() 現在是錯的——本週唯一會產生錯誤數字的項目

dc-09 定義 energy_kwh() 時已經乘過 0.8 DoD,回傳 28.0。dc-09c 則明確要求合規要用 35.0(銘牌),並在誤解 3 裡點名這個坑,且自己的骨架寫成 nameplate_energy_kwh + usable_energy_kwh()

於是同一個模型裡有兩套相反的慣例dc-09energy_kwh() 是「已打折」,dc-09cnameplate_energy_kwh 是「未打折」。名字上完全看不出差別。

收斂建議(純機械,10 分鐘):

# dc-09 改名,並讓兩張卡共用同一組語意
nameplate_energy_kwh: float          # 銘牌,未打折 —— 合規/樓板承重/運輸法規
def usable_energy_kwh(self) -> float:    # = nameplate × dod —— runtime
    return self.nameplate_energy_kwh * self.dod
# 刪掉 energy_kwh():這個名字無法表達它是哪一個,留著就會有人用錯

這是本週第一順位,理由跟 W33 排序邏輯一致:它會產生錯誤數字,而且是靜默的——aggregate_energy() 誤用 usable 會小 20%,剛好足以讓 725 kWh 看起來像 580 kWh 而通過 600 kWh 那道閘門。dc-09c 的驗收表已經把這一行寫進去了(「若誤用 usable_energy_kwh() → 112.0 ← 跑出這個就是踩到誤解 3」),但驗收表擋不住 dc-09 那個舊名字

🔴 2. ThresholdPolicyCodeRule 是同構的,兩張卡都看見了,卻各寫了一份

dc-09c 自己寫著「跟 dc-09bThresholdPolicy同一個形狀」——然後定義了一個欄位完全不同的 CodeRule。逐項對照:

概念 ThresholdPolicy(dc-09b) CodeRule(dc-09c)
誰說的 source ∈ {ieee, vendor_warranty, internal} edition ∈ {NFPA855_2023, NFPA855_2026, TW_ESS_GUIDE}
管什麼 kind: "ohmic" \| "capacity" scope: unit \| group \| compartment \| applicability
對誰適用 (隱含:這串電池) chemistry: Chem
分母/基準 denominator ∈ {baseline, string_avg} (隱含:nameplate)
門檻 pct: float limit_kwh: float \| None
判定為真時 action: str requires_hma: bool

六格逐項對應,沒有一格是多出來的。 差別只在單位(% vs kWh)與詞彙(policy vs rule)。

收斂建議:一個 Policy 基底,兩個具體型別各自帶自己的閾值單位:

@dataclass(frozen=True)
class Policy:
    id: str
    authority: str            # "IEEE_1188" / "NFPA855_2023" / "TW_ESS_GUIDE" / "internal"
    scope: str                # 這條規則作用在哪個層級
    applies_when: dict        # {"chemistry": "LI_ION"} / {"kind": "ohmic"}
    basis: str                # ★ 分母/基準:baseline / string_avg / nameplate / esp
    limit: float | None       # None = 該版本移除此表(NFPA855_2026)
    limit_unit: str           # "pct" / "kwh" / "m"
    on_violation: str         # replace / investigate / warranty_claim / requires_hma

basis 那一欄是整個收斂的重點:它同時裝下 dc-09bdenominatordc-09c 的「用銘牌不是用 usable」、以及間隔複習題裡 dc-05c 的「分母是 ESP 銘牌」。三張卡、三個子系統,同一個「選錯基準就靜默放寬標準」的坑——把它變成一個必填欄位,是唯一能在 schema 層擋住它的做法。

SeparationRuledc-09c 的間距規則)也可以掛進來,limit_unit = "m"

🟡 3. Finding 被三處使用,但從來沒有被定義過

dc-09bevaluate() -> list[Finding]dc-09cevaluate_gates() -> list[Finding]check_separation() -> list[Finding]dc-09c 加分題「在 Finding 上加 source_edition」——四次引用,零次定義。它現在是模型裡出現頻率最高的型別,而它是一行 TODO 註解。

收斂建議:見自我測驗 Q5 的 dataclass。加分題要的 source_edition 不需要新欄位,policy_id 指向的 Policy.authority 就是它——這正是把 ThresholdPolicy / CodeRule 合併成 Policy 之後立刻拿到的好處,所以 #2 與 #3 應該一起做。

🟡 4. AlarmFinding 的分界沒有被畫出來(=自我測驗 Q5)

不重複展開。結論:兩層不合併,origin_finding_id 是唯一的橋Finding 不得有 ackedAlarm 不得有 policy_idredundancy_effective() 讀事實不讀告警。

特別提醒一句措辭dc-10 自我檢核 Q3 寫「告警反過來成為狀態計算的來源,第一次不只是輸出」——這句話是對的但危險,照字面實作會做出循環依賴。卡片骨架其實把 latched 狀態放在 scr / breaker_open 上(正確),建議在 dc-10b 開頭補一句澄清,否則三個月後的自己會照那句話重寫。

🟡 5. 兩棵樹的橫向邊沒有位置

dc-09c 要求 DeviceRegistry 支援 upstream(device)fire_compartment(device) 兩種遍歷——這部分講清楚了。但兩棵樹之間的依賴邊沒有:串起來第二節那條「排風機必須掛發電機盤」是空間樹的節點依賴電力樹的節點,既不是 upstream 也不是 fire_compartment

收斂建議FireCompartment 加一個 safety_system_feeds: list[str](指向 PowerEdgeDevice 的 id),並讓「本區劃的安全系統是否有緊急電源」成為一條可求值的 Policy(NFPA 855 2026 §4.10)。

為什麼現在就要留位置dc-10b 的動手練習正是建 DeviceRegistry這是四週以來唯一一次會動到它骨架的機會。第二輪冷卻鏈的 dc-22 CRAH(接不接 UPS)與 dc-20 儲冷槽會第二、三次撞上同一個形狀——那時再改,代價是七張卡。


優先序

  1. #1 energy_kwh() 改名 — 唯一會產生錯誤數字的,10 分鐘,先做。
  2. #2 + #3 Policy / Finding — 一起做(source_edition 因此免費)。basis 欄位同時封死了 dc-05c / dc-09b / dc-09c 三張卡的同一個坑,是本週投報率最高的一項
  3. #5 兩棵樹的橫向邊必須在 dc-10b 動手練習之前決定,因為那次就是建 DeviceRegistry。錯過要等到第二輪。
  4. 4 Alarm / Finding 分界 — 跟 #3 同時定案即可。

  5. W33 舊帳 #1 Assembly、#5 Bound、#2 EnergyStoreBound 現在有第二個用途(額定型別化),可以跟 #2 的 basis 一起想。

本週該問 facility 的問題

三張卡共提出 8 個問題,去重後選 3 個下週真的拿去問人的。挑選標準沿用前三週:「現在問還來得及改,晚問就固化了」。本週三題的固化時點分別是——已經在倒數/設計階段/commissioning

Q1 — 問 UPS/電池供應商 + 維運(⏳ 倒數中,錯過不可回溯)

「這批電池的 commissioning baseline 是誰量的、哪一天、用哪一台儀器?投入服務滿 6 個月那一筆有沒有?報告在哪? 量的時候有沒有記 cell 表面溫度(量負極柱)、浮充電壓、充電電流?讀值有沒有校正到 25 °C? 保固索賠認的分母是 baseline 還是 string average、門檻幾 %?需要幾筆週期讀值才受理? 電池間有沒有全年溫度曲線紀錄、月均幾度?」

為什麼是這題,而且為什麼最急:這是整條學習軌跡裡少數幾件真正不可回溯的事,跟 commissioning 同一類。

  • baseline 有一個窗口:太早(安裝當天)電池尚未完全化成、讀值不穩;太晚就已經劣化過,之後所有偏差都以一個「已經壞了一點」的零點在算。錯過就永遠補不回來,而且沒有任何告警會提醒你錯過了。
  • 沒有 baseline,保固從第一天起就是空的。IEEE 1188 原文說沒有基準線的內阻讀值 "difficult to interpret and of limited value"——你可以有一整年的高頻 BMS 遙測,卻在索賠時被打回。
  • 分母那題決定我們的內部門檻要不要比廠商更嚴(Q1 算過:同一筆讀值換分母,35.9% → 14.5%)。
  • 溫度曲線是 service_life_est_years 唯一的輸入。沒有它,「型錄寫 10 年、我們四年就要換」這件事永遠只能吵,而且間隔複習題證明了分母填錯會讓測試排程安靜遲到三年半

Q2 — 問 消防設備師 / 機電設計單位(必要時直接問 AHJ)(⚠️ 設計階段,蓋完就是拆牆)

轄區 AHJ 採用哪一版——NFPA 855 的 2023、2026,還是只走台灣的儲能系統消防安全管理指引? 這決定我們是守 50/250/600 kWh 表格,還是一定要做 HMA(危害減緩分析),預算與時程差很多。 A 路與 B 路的電池在不在同一個防火區劃?時效幾小時?平面圖在哪? 廠商那份 UL 9540A 報告測到第幾層——cell、module、unit 還是 installation? 只做到 module 層的報告不能拿來放寬間距。 排風機與 off-gas 偵測盤掛在哪一路電源、有沒有進發電機盤?(2026 版 §4.10) 台灣端:廠內建築物內設置是否已規劃 2 小時防火區劃?周邊退距是走 30 m 基準還是 3 m 縮短條件(2 小時防火牆 + 自動滅火)?」

為什麼是這題

  • 版本分歧是真的、而且會並存好幾年——2023 版有能量表、2026 版把表移除改成 HMA 預設(僅鉛酸與水系鎳基豁免,且須由具 ESS 經驗的 PE 主導、另加大型燃燒試驗 LSFT),而 IFC 2024 引用的仍是 2023 版文字。這不是誰對誰錯,是同一份規範的兩個時間切面。問清楚才知道 CodeRule 該填哪一版,也才知道要不要編 HMA 的預算。
  • A/B 同區劃是 2N 的隱形殺手dc-09c 說得直白——若同一區劃,2N 在消防事故上其實是 1N。而這件事在單線圖上看不出來,只在建築平面圖上看得到。這也是 W32 補考題(兩路市電同變電所、三台機共用油管)那個錯誤的第四次出現:用數量冒充獨立性。
  • UL 9540A 是 test method 不是認證,四層依序測、任一層未觀察到火焰傳播就可停止——所以「測過了」完全可能是「整櫃從未被燒過」。只有 unit 或 installation 層的結果才足以支撐小於 3 ft 的間距宣告。
  • 0.9 m 與 30 m 差 33 倍,因為量的根本不是同一件事(NFPA 量 unit 對 unit,台灣指引量 ESS 對周邊建物/道路)。把 3 ft 當成「離牆 3 呎就好」搬到台灣案子,是這裡最典型的誤讀。

Q3 — 問 控制系統承包商 / STS 供應商(⚠️ commissioning 前)

「STS 的轉換演算法設哪一種——快速(POG,1/4 cycle)還是抗湧流(VSS,3/4 cycle)下游有沒有變壓器型 PDU高負載電流抑制門檻設多少(出廠約 3 倍額定)、有沒有調過? Retransfer 設 Yes 還是 No、延遲幾秒(4 s–10 min)? SCR 的 latched 告警現場怎麼 reset、誰有權限、有沒有留記錄? 另外——你報的 MTBF 邊界畫在哪裡?含不含輸出斷路器?含不含旁路操作的人為誤動作?

為什麼是這題

  • 前兩問決定 Q3 算的那個餘裕:60 Hz + 抗湧流 = 5.50 ms,只剩快速轉換的四成。而「下游有沒有變壓器」是一個不在 STS 身上的事實,所以規格書上那個 4 ms 不會告訴你。
  • 抑制門檻決定 STS 會凍結多久:下游短路時 STS 依設計不動作,只能等上游斷路器清除(有選擇性協調約 0.3 s、開 ERMS 約 0.05 s,兩個都遠超 ITIC 的 20 ms)。這條把 STS 的可用性直接綁在 dc-07b 的保護協調設定上——同一次 commissioning 該一起驗。
  • latched reset 的權限與記錄,決定你能不能相信「告警消失 = 問題解決」。這是全軌跡第一個 ack 不掉的告警,如果現場的實務是「隨手 reset」,那 latched 這個欄位在資料庫裡就是裝飾。
  • MTBF 分歧是真的:Schneider WP 62(約 2010)說 400,000–1,000,000 h 且結論是小型負載別用 STS;PDI/Eaton 規格書(2017)說超過 2,000,000 h。兩邊很可能在算不同的邊界(WP 62 算整台、PDI 明寫算「關鍵交流輸出匯流排」,中間又隔七年)。所以可驗證的問法不是「STS 可不可靠」,而是「你報的邊界畫在哪」——WP 62 把人為誤操作列為主要失效模式之一,而它不會出現在任何規格書的 MTBF 裡。

這週沒選上、但別忘記的

  • 容量測試那天的替代供電路徑寫在哪份 MOP 裡?窗口算到放電結束還是算到充飽?(Q4 那兩小時就是這題沒問清楚的後果。)
  • 量內阻時並聯串要不要離線?季測有沒有進維護窗口衝突檢查?(很多人以為量內阻是非侵入式的。)
  • 偵測器清單:off-gas 有幾顆、裝在哪、跟 LFL 偵測器是不是同一套系統? 兩者不是同一件事的兩種精度,告警動作與時間預算完全不同
  • 鋰電室的樓板承重上限(W33 已列,仍未問;11,340 kg 對舊建物翻新是真實限制)。
  • STS 的 active_source 有沒有進歷史記錄?512 筆事件記錄與波形圖保存多久? 轉換只有幾毫秒,沒波形就查不出原因,而 512 筆很容易被日常事件洗掉。

下週

  • 佇列下一張:dc-10b STS 的兩源關係(相位同步窗與頻率漂移、拓撲獨立性 common ancestor、雙母線容量會計)。它的動手練習就是欠了四週的 DeviceRegistry——連貫性檢視 #5 那條「兩棵樹的橫向邊」必須在動筆前決定好,因為這是唯一一次會動到它骨架的機會。
  • #1(energy_kwh() 改名)請在下一張卡之前做完,10 分鐘,且它是本週唯一會產生錯誤數字的項目。#2 + #3(Policy / Finding)建議一起做basis 欄位同時封死三張卡的同一個坑。
  • 節奏檢討:本週 3 張卡,其中一天的空窗是 .git/index.lock 造成的。建議把「開工先檢查有無遺留 staged 變更 + 收工必清 lock」寫進 dc-daily-card 的 SKILL——這次的損失是一張卡加兩天的 push 停擺,下次可能更久,因為沒有任何東西會通知你。
  • ⚠️ commissioning 隨時可能開始,而且本週的 Q1 提醒了一件新的事:電池 baseline 的 6 個月窗口跟 commissioning 是兩個不同的時鐘。W32 Q3(ATS 五個計時器設定值 + NFPA 110 §7.13.4.1.4 分段時序記錄)、W33 Q2(ZSI 注入電流功能測試、tie 閉合互鎖驗證、arc flash 標籤)、本週 Q3(STS 演算法、抑制門檻、latched reset 權限)——這三份清單合起來就是現場驗收單,而 Q1 那份是驗收之後六個月才要做的事,最容易在專案結案後被忘掉。建議現在就把它排進行事曆。

已經先查到、留給 dc-10b 的材料

補寫本週報時為 dc-10 多查了幾筆專屬於「兩個源之間關係」的資料,dc-10 卡片刻意沒用(超出它的範圍),先寄放在這裡免得下次重查:

  • IEC 62310-3 的 STS 效能碼 XX YY B TS —— XX = 故障電流管理(CB 有整合斷路器/熔絲、可開斷;PC 只能承受不能開斷)|YY = 中性線處理(00 無/NC 兩輸入中性線併接/NS 切換分離/NI 隔離變壓器分離)|B = 轉換型式(B break-before-make/M make-before-break)|TS = 電壓限值。這四段應該原樣存成四個 enum 欄位,不要塞成一個型號字串——因為 YY 與站點接地系統之間有硬約束(下一點)。順帶一提 PC 級是第四次出現「條件式額定」:它的耐受值綁定一顆特定型號的 uR 快速熔絲,換絲即失效,跟 dc-07bIcc 同一個形狀。
  • 極數 × 接地系統是跨實體約束:TN-S 四線強制 4W4P(否則不平衡負載與接地阻抗差異造成環流;裝了 RCD 還會誤跳);TN-C 禁止切換 PEN;IT 有分佈中性線必須 4W4P,否則兩顆 IMD 並聯會量到阻抗下降而誤報。這是 dc-02 阻抗決定 dc-07b 合法性之後,第二個「A 的欄位決定 B 的合法性」——而且這次是站點層級欄位決定設備層級合法性。
  • 來源分歧(BBM vs MBB):Socomec 明說預設且建議 BBM,MBB「技術上可行但很少用,因為重疊兩個獨立源會產生不受控的環流」;但也有資料說同步源可做 MBB(重疊不到一個週期)。兩邊很可能在講不同的設備——UPS 內部的靜態開關(逆變器↔旁路,同一台 UPS 主動同步,確實是 MBB 重疊導通)vs 獨立兩源之間的 STS。同樣叫「靜態開關」,同步前提完全不同,這正是 dc-10b 該處理的分歧。
  • 相位同步窗有三個數字,而且量的不是同一件事:MBB 安全條件約 5 電氣度(另加電壓差 <5%、頻率差 <0.2 Hz)|一般 STS 可接受窗口 10–20 電氣度|Socomec 說本體可在高達 180° 相位差下切換,但負載未必受得了(馬達、變壓器),所以提供「非同步時禁止轉換」的設定。三個數字分別是「MBB 的安全條件」「典型設定」「設備本體能力」——別放同一欄。
  • 4 極 STS 的中性線是刻意重疊的:BBM 逐相進行,但中性線要重疊不超過 10 ms 以維持參考電位。所以「break-before-make」這個描述對相線成立、對中性線不成立
  • 可靠度數字的第三個版本:Socomec 給 λ_STS ≈ 150 FIT(MTBF 6.67×10⁶ h)、λ_UPS ≈ 2860 FIT(35 萬 h)、λ_cable ≈ 1 FIT/m。這跟 dc-10 已記錄的 WP 62(40 萬–100 萬 h)與 PDI(>200 萬 h)湊成三個版本。 dc-10b 可以拿它算「加一台 STS 到底划不划算」,而關鍵在於答案是上游共祖的函數,不是 STS 的函數:兩條路徑若共用一個高失效率的祖先,STS 帶來的改善會從一個數量級塌到接近 1。這正好是 ancestors() 那段程式碼的動機——也是連貫性檢視 #5 那條橫向邊為什麼要在動筆前決定。