# -*- coding: utf-8 -*-
"""第2回 PDF「確認事項(第2回)」の自己点検。
原稿 (docs/handoff/TeamWorks確認事項_第2回_20260903.md) の文言が
すべて PDF に含まれているか、フォントが日本語フォントで埋め込まれているかを見る。
PDF のパスは第1引数で渡せる。省略時はこのスクリプトと同じディレクトリ。
check_ddl_review_pdf.py(第1回用)と同じ構成。
"""
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_確認事項_第2回_20260903.pdf"))
if not os.path.exists(PDF):
raise SystemExit("PDF が見つからない: %s\n"
"先に make_teamworks_qa2_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 = [
# 表題・前文
"確認事項(第2回)", "TeamWorks(DB開発側)", "作成日:2026-09-03",
"DFARobotics", "clean_watch_20260902.sql",
"第1回「実DDL照合結果と確認事項」に続く確認事項です",
"取得元が特定できなかった項目",
"Q1とQ2は、解決しないと該当画面を実装できません",
"すでに存在するのに当方が見落としている可能性",
# サマリ表
"A.実装が止まる", "取得元が特定できず、該当画面を実装できない",
"B.定義の確認", "取り違えると誤った値を表示する",
"C.ご回答を受けた追加確認", "第1回のご回答の反映にあたり確認したい点",
"D.継続", "第1回でお願いしたもののうち、まだ動きがないもの",
"回答不要",
# Q1 前文と候補テーブル
"Q1D-Security画面のデータ取得元",
"F-06D-Secログ", "F-07D-Secアラート",
"これらに直接対応する名前のテーブルが見当たりませんでした",
"ds_devices/ds_alertsのような名前のテーブルはありません",
"controls", "control_reports", "control_error_histories",
"security_statuses", "security_status_histories",
"serial_number", "verified_at", "deleted_at", "occurred_at",
# Q1-1
"Q1-1D-Secログ画面(デバイス一覧)",
"controls.serial_number", "facilities.facility_name",
"通信状態", "最終通信日時", "control_reports.reported_at",
"定期的な生存報告という理解で合っていますか",
"デバイスごとの備考を持つカラムはありますか",
# Q1-2
"Q1-2controls.statusのコード値定義をご教示ください",
"tinyintunsigned", "取りうる値と意味が分かりません",
"「異常」はcontrols.statusから判定できますか",
"からの経過時間で判定する想定でよいですか",
"何分(何時間)で通信断とみなすかの運用上の基準はありますか",
"6時間以上通信なし", "根拠のない仮の値です",
# Q1-3
"Q1-3D-Secアラート画面",
"control_error_historiesが実体だと考えています",
"control_error_histories.occurred_at",
"sections.section_name", "controls.serial_number",
"control_error_histories.status", "control_error_histories.comment",
"responded_at/resolved_at",
"アラート種別", "セキュリティ解除", "通信断",
"詳細(アラートの本文)",
"statusが種別を兼ねているのでしょうか",
"セキュリティ復帰失敗", "検知側が書く詳細とは別だと考えています",
# Q1-4
"Q1-4security_statuses/security_status_historiesの位置づけ",
"既定値1", "既定値が1である理由も",
"セキュリティの現在状態を持つもの",
"解除されたまま戻っていない",
"security_statusesの変更履歴という理解で合っていますか",
# Q2
"Q2事前チェック(60分前/30分前)時点のロボット状態",
"清掃開始の60分前と30分前のロボットの状態",
"待機中", "充電中", "ハンドルアップ", "緊急停止", "オフライン", "セキュリティ有効",
"後から参照できるテーブルが見当たりません",
"robots.status", "section_robot_statuses.robot_status",
"clean_timetables.precheck", "robot_instruct_histories",
"過去の値は残りません", "現時点では未確定",
"0:本清掃/1:1回目/2:2回目",
"そのときの状態がどこにもありません",
"指示の履歴であり、状態のスナップショットではないと理解しています",
"どのテーブルのどのカラムに記録されますか",
"現状では画面に出せない", "P2以降に送るか",
"robots.status(smallint)のコード値定義をご教示ください",
"1:清掃中", "-1:清掃完了(セキュリティ無効)", "statusのほうは未回答",
"配下のrobots.statusから算出するルールは決まりましたか",
# Q3
"Q3clean_schedules.weeksの曜日の並び順",
"varchar(7)", "曜日ごとの実施有無を7桁で持つ理解です",
"左から順に「日・月・火・水・木・金・土」で合っていますか",
"「月・火・水・木・金・土・日」",
"並び順を取り違えると別の曜日として表示されます",
"1000000を日曜と読むか月曜と読むか",
"1が実施・0が非実施で合っていますか",
# Q4
"Q4ステータス系カラムのコード値定義(まとめ)",
"clean_statuses.clean_status", "残りは次の7つです",
"robot_error_reports.status", "smallint", "なし",
"「未対応/対応中/完了」の3状態で扱う想定です",
"control_error_histories.statusと同じ体系でしょうか",
# C 追加確認
"C.第1回のご回答を受けた追加確認(3件)",
"ご回答をいただきありがとうございました。すべて反映しています",
"C-1A-3(contact系の型)はDDL側を修正いただけますか",
"contact_typeは両テーブルともtinyint.",
"emailは両テーブルともvarchar(100)で定義してください.",
"まだ食い違ったまま",
"customer_contacts.contact_type", "bigintunsigned",
"facility_contacts.email", "varchar(255)",
"実DDLを正としてスキーマを機械的に取り込む運用",
"DDL側を直していただく前提",
"DDL側を修正いただけるという理解で合っていますか",
"反映の目安時期をお知らせください",
"C-2B-1/B-2/B-3の修正を含むDDLをいつ頂けますか",
"外部キー3本(B-1)・インデックス1本(B-2)・timestampのDEFAULT(B-3)",
"修正が入るたびにDDLを頂き直す必要があります",
"DDLを再度いただけますか",
"A-2のauto_incrementも同じ修正に含まれる",
"AUTO_INCREMENTが付いていません",
"C-3APIが画面に返すIDはerror_idで確定してよいですか",
"とりあえずerror_idを主キーと位置付け対応する予定です",
"アルファベットを含んでいるので,数値型にすることはできません",
"PrecheckItem.idをnumberからstringへ変更し、値にはerror_idを入れる",
"この方針で問題ないですか",
"運用を以て最終的な回答を求める",
"後でid側に一意制約が付く可能性はありますか",
"切り替えられる形で実装しておきます",
# D 継続
"D.継続(1件)",
"まだ動きがないもの",
"DB更新用アカウントの発行",
"のみUPDATE可",
# 参考: 第1回のご回答
"参考:第1回のご回答で解決した事項",
"A-1", "A-2", "A-3", "B-1", "B-2", "B-3",
"error_idを主キーと位置付けて対応する予定",
"id もerror_idも英字を含むため数値型にはできない".replace(" ", ""),
"number→string",
"PATCH/prechecks/{id}/statusの設計に着手できます",
"採番はデータベース。auto_incrementで採番する",
"元テーブルと履歴テーブルは1対1",
"ビューアーはこの2テーブルを参照しないため、実装への影響はありません",
"反映後のDDLをdbpullで取り込みます",
"制約(外部キー3本)は後から追加する",
"孤児行を想定した実装はやめます",
"インデックスも後から追加する。現状は20施設弱",
"当面の性能懸念は小さい",
"指摘どおりDEFAULTを追加する",
"created_at:新規作成時、updated_at:新規作成時/更新時",
# 参考: 受領DDL
"参考:受領DDLの確認により解決していた事項",
"既に満たされていました", "対応不要です",
"responded_at/resolved_atの",
"両テーブルに既に存在します",
"datetimeNULL可", "綴りもresponded_atで正しくなっています",
"D-Secアラートへの書き込み可否",
"既に更新可4カラムに含まれています",
"追加の権限は不要という理解です",
# 当方判断
"参考:当方で判断して進める項目(回答不要)",
"clean_schedulesにserial_numberが無い件",
"sections経由でrobotsを引きます",
"1清掃エリア=1ロボット", "一意に定まります",
"clean_schedules.end_timeが24〜29時を取る件",
"画面表示は「翌04:30」の形にします",
# 末尾
"D-Security画面と清掃詳細ログのデータ取得層に着手できます",
"表示が誤る箇所のため早めにご確認いただけると助かります",
"とくにC-2(修正版DDLの受領時期)の目安をいただけると助かります",
]
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)