fix(ppt): final critic cleanup — Medium-1 OOXML order + Info-1/2 docs
All checks were successful
CD Pipeline / deploy (push) Successful in 2m29s

清掉剩餘 critic finding(Medium-1 + Info-1 + Info-2):

Medium-1: _set_run_fonts 違反 OOXML CT_TextCharacterProperties 子元素順序
- 抽出 _insert_rpr_child(rPr, tag) helper:依 ECMA-376 §21.1.2.3 規定
  的順序表(_RPR_CHILD_ORDER)找正確位置插入
- 找第一個排在 target 之後的子元素 → insert 前面;否則 append
- 當前 from-scratch 場景安全;未來改讀模板 .pptx 時也不會踩雷
- 驗證:產生 monthly v3.1,399 個 rPr 全部符合 schema 順序,0 違反

Info-1: cleanup_expired_ppt_cache docstring 補語意說明
- 明確說明 dry_run=True/False 時 deleted_files / deleted_rows / freed_bytes
  欄位語意分別為「將刪除」預估值 vs「已實刪」實際值

Info-2: TEMPLATE_VERSIONS 標記退役 type
- growth / vendor / bcg 三個 type 從未實際落地(依 ADR-014 校正 2026-04-28)
- 加 DEPRECATED 註解,避免後人誤以為已支援

Info-3 SKIP(評估後不做):
- get_template_version import 改放模組頂部會 trigger ppt_generator 模組級
  REPORTS_DIR.mkdir() 副作用,read-only filesystem 環境會掛
- 保留延遲 import 是 graceful degradation 的 intentional 設計

至此 critic 38967ce 審查清單全綠:
- 0 critical, 2 HIGH (3b0b4b3), 4 medium (52c06f6 + 本 commit), 3 info

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
OoO
2026-05-02 17:39:27 +08:00
parent 52c06f6861
commit 1c81866541
2 changed files with 51 additions and 13 deletions

View File

@@ -2246,8 +2246,10 @@ def cleanup_expired_ppt_cache(days_old: int = 7, dry_run: bool = True) -> dict:
launchd / cron 排程務必顯式傳:
cleanup_expired_ppt_cache(days_old=7, dry_run=False)
回傳{'deleted_files': N, 'deleted_rows': N, 'freed_bytes': N, 'errors': [...]}
dry_run=True 時 deleted_* 為「將刪除」預估值
回傳統計欄位語意critic Info-1
dry_run=True deleted_files / deleted_rows / freed_bytes 為「將刪除」預估值
dry_run=False 時:上述為「已實刪」實際值
errors 為過程中發生例外的列表id + 錯誤訊息)
"""
from database.manager import DatabaseManager
from database.ppt_reports import PPTReport

View File

@@ -47,9 +47,12 @@ TEMPLATE_VERSIONS = {
'strategy': 'v3.0', # 2026-05-02 AI 頁去黑改暖紙 + 附錄頁
'competitor': 'v3.0', # 2026-05-02 AI 頁去黑改暖紙 + 附錄頁
'promo': 'v3.0', # 2026-05-02 AI 頁去黑改暖紙 + 附錄頁
'growth': 'v2.0',
'vendor': 'v2.0',
'bcg': 'v2.0',
# ── DEPRECATED以下 type 從未實際落地(依 ADR-014 校正 2026-04-28
# 函式 generate_growth_ppt / generate_vendor_ppt / generate_bcg_ppt 仍存在於本檔,
# 但路由層未綁定指令;保留版本字串避免如未來重啟時快取 schema 對不上。
'growth': 'v2.0', # DEPRECATED — 從未落地
'vendor': 'v2.0', # DEPRECATED — 從未落地
'bcg': 'v2.0', # DEPRECATED — 從未落地
}
@@ -156,26 +159,59 @@ def _add_rect(slide, l, t, w, h, fill_hex, line_hex=None):
return s
# OOXML CT_TextCharacterProperties 子元素順序(依 ECMA-376 第一部分 §21.1.2.3
# critic Medium-1SubElement 永遠 append 會違反 schema必須按此順序 insert
# 否則 LibreOffice / Keynote / 嚴格驗證器會拒絕讀取或丟棄 latin/ea 元素。
_RPR_CHILD_ORDER = [
'ln', 'noFill', 'solidFill', 'gradFill', 'blipFill', 'pattFill',
'effectLst', 'effectDag', 'highlight', 'uLnTx', 'uLn',
'uFillTx', 'uFill', 'latin', 'ea', 'cs', 'sym',
'hlinkClick', 'hlinkMouseOver', 'rtl', 'extLst',
]
_OOXML_DRAWING_NS = 'http://schemas.openxmlformats.org/drawingml/2006/main'
def _insert_rpr_child(rPr, tag: str):
"""在 rPr 下依 ECMA-376 schema 順序插入 <a:tag> 元素。
若 rPr 內已有此 tag先全部移除避免重複。
回傳新建的元素。
"""
from lxml import etree
nsmap = {'a': _OOXML_DRAWING_NS}
# 先清掉同名舊元素
for el in rPr.findall(f'a:{tag}', nsmap):
rPr.remove(el)
target_idx = _RPR_CHILD_ORDER.index(tag) if tag in _RPR_CHILD_ORDER else len(_RPR_CHILD_ORDER)
# 找第一個排在 target_idx 之後的子元素 → insert 在它前面
for i, child in enumerate(rPr):
local = etree.QName(child).localname
if local in _RPR_CHILD_ORDER and _RPR_CHILD_ORDER.index(local) > target_idx:
new_el = etree.Element(f'{{{_OOXML_DRAWING_NS}}}{tag}')
rPr.insert(i, new_el)
return new_el
# 沒有後序元素 → append
new_el = etree.SubElement(rPr, f'{{{_OOXML_DRAWING_NS}}}{tag}')
return new_el
def _set_run_fonts(run, latin_font: str = None, ea_font: str = None):
"""為單一 run 同時設定 Latin 字型與 EastAsian (CJK) 字型。
解法:直接寫入 a:rPr 下的 a:latin / a:ea 元素,避開 python-pptx
只能設一個 font.name 的限制 — 這是「中英分軌」呈現的關鍵。
critic Medium-1使用 _insert_rpr_child 維持 OOXML schema 規定的子元素順序,
讓未來若改成讀模板 .pptxrPr 內已有 hlinkClick/cs 等後序元素)時也能正確插入。
"""
if not latin_font and not ea_font:
return
try:
from lxml import etree
rPr = run._r.get_or_add_rPr()
nsmap = {'a': 'http://schemas.openxmlformats.org/drawingml/2006/main'}
if latin_font:
for el in rPr.findall('a:latin', nsmap):
rPr.remove(el)
latin_el = etree.SubElement(rPr, '{%s}latin' % nsmap['a'])
latin_el = _insert_rpr_child(rPr, 'latin')
latin_el.set('typeface', latin_font)
if ea_font:
for el in rPr.findall('a:ea', nsmap):
rPr.remove(el)
ea_el = etree.SubElement(rPr, '{%s}ea' % nsmap['a'])
ea_el = _insert_rpr_child(rPr, 'ea')
ea_el.set('typeface', ea_font)
except Exception:
# 失敗時退回 font.name 單軌設定