# -*- coding: utf-8 -*-
"""第3回 PDF「確認事項(第3回)」の自己点検。
原稿 (docs/handoff/TeamWorks確認事項_第3回_20260903.md) の文言が
すべて PDF に含まれているか、フォントが日本語フォントで埋め込まれているかを見る。
PDF のパスは第1引数で渡せる。省略時はこのスクリプトと同じディレクトリ。
check_teamworks_qa2_pdf.py(第2回用)と同じ構成。
"""
import os
import re
import sys
from pypdf import PdfReader
PDF = (sys.argv[1] if len(sys.argv) > 1 else
os.path.join(os.path.dirname(os.path.abspath(__file__)),
"DWatch_確認事項_第3回_20260903.pdf"))
if not os.path.exists(PDF):
raise SystemExit("PDF が見つからない: %s\n"
"先に make_teamworks_qa3_pdf.py を実行するか、"
"PDF のパスを第1引数で渡すこと。" % PDF)
raw = open(PDF, "rb").read()
reader = PdfReader(PDF)
pages = [p.extract_text() for p in reader.pages]
text = "\n".join(pages)
flat = re.sub(r"\s+", "", text)
print("== 1. ページ数とサイズ ==")
print("ページ数:", len(reader.pages), " サイズ:", len(raw), "bytes",
"(%.1f KB)" % (len(raw) / 1024))
print("抽出文字数:", len(text))
print("\n== 2. フォント埋め込み ==")
print("/FontFile2 出現回数:", raw.count(b"/FontFile2"))
biz = sorted(set(re.findall(rb"/([A-Z]{6}\+[A-Za-z0-9\-]*BIZ[A-Za-z0-9\-]*)", raw)))
print("BIZUDGothic を含むフォント名:", biz)
if not biz:
print(" ★NG★ BIZ UDGothic が埋め込まれていない")
# 中国語向けフォントが混ざっていないこと(CLAUDE.md §0-3)
CN = [b"SimSun", b"SimHei", b"MicrosoftYaHei", b"MSYaHei",
b"NotoSansSC", b"NotoSerifSC", b"SourceHanSansSC", b"SourceHanSerifSC"]
hit = [n.decode() for n in CN if n in raw]
print("中国語向けフォント:", hit if hit else "混入なし")
if hit:
print(" ★NG★ 中国語向けフォントが混ざっている")
print("\n== 3. 原稿の全項目が含まれるか ==")
must = [
# 表題・前文
"確認事項(第3回)", "TeamWorks(DB開発側)", "作成日:2026-09-03",
"DFARobotics", "clean_watch_20260902.sql",
"ご回答をありがとうございました",
"Q1・Q2で止まっていたD-Security画面と清掃詳細ログの設計が進められます",
"本書は次の3部構成です",
# サマリ表
"1.ご質問への回答", "第2回でいただいた逆質問へのお答え",
"2.当方で変更するもの", "回答不要", "認識に相違があればご指摘ください",
"3.追加でお伺いしたいこと", "D-1・D-2が解決しないと画面の中身が埋まりません",
# 1-1
"1-1.「備考ってありましたでしょうか?(見落とし?)」について",
"DB側の項目ではありません。こちら(ビューアー)側で持つべきものでした",
"セキュリティ復帰失敗", "6時間以上通信なし",
"アラートは生死報告がないの1種類だけ、詳細文は無い",
"検知側が出す情報ではなく、運用担当が手で書き留めるメモ",
"ビューアー側に入力欄を作り、当方の独自ストアに保存します",
"DB側の対応は不要です",
"点検記録や運用設定と同じ扱いで、メインDBには書き込みません",
"control_error_histories.comment",
"アラート1件ごとに運用担当が対応時に書き込む欄",
"更新可4カラムの1つ",
"デバイスごとの備考とは別物",
# 1-2
"1-2.「切り替えられる形にした場合のデメリットは?」について",
"恒久的なコストはほぼありません",
"切り替える瞬間に2つ、避けられない影響があります",
"それ以前に画面が持っていたIDが全部無効になります",
"古いIDで更新をかけて404になります",
"運用が動いていない時間帯に切り替えていただく",
"切替をまたぐと、同じレコードが別のIDで記録され、ログの追跡が切れます",
"切り替えた日時を当方の記録に残しておけば読み替えられる",
"コードに分岐が残る問題は、作り方で消せます",
"IDをどちらにするかの決定を1つの関数に閉じ込め",
"分岐は1箇所だけで済み、テストもその関数に2パターン書けば足ります",
"切替が現実的に不可能になります",
"idとerror_idのどちらを使うか",
"型は両方ともvarcharのまま",
"bigintauto_increment",
"破壊的変更",
# 2. 変更するもの
"2.当方で変更するもの(回答不要)",
"2-1", "2-2", "2-3", "2-4",
"D-Secログの「通信状態」を3状態(正常/通信断/異常)から2状態(正常/異常)に変更",
"controls.statusが0/1の2値で",
"通信断と異常は同じもの",
"D-Secアラートの「種別」列を落とす",
"制御盤で発生するアラートは「生死報告がない」の1種類だけ",
"アラートの「詳細」列も落とす",
"種別が1つしかないため詳細文が存在しない",
"control_reports.reported_atからの経過時間で行い",
"link_schedules.intervalsから取る",
# 3. D-1
"3.追加でお伺いしたいこと(4件)",
"D-1", "link_schedules.intervalsの書式",
"VarChar(120)", "時間帯別の設定(単位:秒)", "書式が分かりません",
"実際に入っている値の例を1〜2件いただけますか",
"0-6:600,6-22:120,22-24:600",
"JSONなのか、区切り文字は何か",
"これが分からないと通信断の判定が実装できません",
# 3. D-2
"D-2", "robot_error_reports.error_typeに実際に入る値",
"清掃開始の60分前・30分前のロボット状態",
"robots.statusのコード表と突き合わせると",
"3つが対応先不明",
"待機中", "0Standby",
"充電中", "201ChargingLine/202ChargingPile",
"セキュリティ有効", "security_statuses.status=1から判定",
"ハンドルアップ", "緊急停止", "オフライン", "該当なし",
"PUDUCloud由来で列挙が未定義",
"実際に入っている値をご存じの範囲で列挙いただけますか",
"分類ルールはこちらで作り、未知の値は「その他」に寄せます",
# 3. D-3
"D-3", "「セキュリティ解除されたまま」の判定",
"制御盤のアラートは生死報告がないだけ",
"セキュリティが解除されたまま戻っていない",
"security_statuses.status=0(セキュリティ無効)が続いている状態から",
"当方で導出してよいでしょうか",
"制御盤側でこれを検知してどこかに記録している、ということはありませんか",
# 3. D-4
"D-4", "PrecheckItem.idの型",
"型はvarcharのままという理解でよいですか",
# 末尾
"D-Secログの通信断判定と清掃詳細ログの状態表示が完成します",
"画面とAPIの骨格は作れますので、作業は止まりません",
"C(修正版DDL)は本日中とのこと、ありがとうございます",
"prismadbpullで機械的に取り込み、差分をご報告します",
]
miss = [m for m in must if re.sub(r"\s+", "", m) not in flat]
print("検査項目:", len(must), " 欠落:", len(miss))
for m in miss:
print(" ★欠落★", m)
print("\n== 4. 判定 ==")
ok = (not miss) and bool(biz) and (not hit)
print("結果:", "OK" if ok else "NG")
sys.exit(0 if ok else 1)