Newer
Older
client_dwatch / docs / handoff / pdf / check_teamworks_qa6_pdf.py
# -*- coding: utf-8 -*-
"""第6回 PDF「確認事項(第6回)」の自己点検。

原稿 (docs/handoff/TeamWorks確認事項_第6回_20260908.md) の文言が
すべて PDF に含まれているか、フォントが日本語フォントで埋め込まれているかを見る。
PDF のパスは第1引数で渡せる。省略時はこのスクリプトと同じディレクトリ。

check_teamworks_qa5_pdf.py(第5回用)と同じ構成に、
「4. 識別子の語中改行」の検査を足したもの。

★must に「ページを跨ぐ語」を入れないこと★
抽出テキストにフッター(D-Watch / DFA Robotics とページ番号)が挟まり、
空白を詰めても一致しません。第3回で bigint auto_increment が実際に引っかかりました。

★4 の検査があっても、表の列幅崩れの目視確認は省かないこと★
4 は「識別子が行の途中で割れていないか」を抽出テキストで見るだけで、
枠のはみ出し・改ページ位置・重なりは見られません(pdf/README.md)。
"""
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_確認事項_第6回_20260908.pdf"))
if not os.path.exists(PDF):
    raise SystemExit("PDF が見つからない: %s\n"
                     "先に make_teamworks_qa6_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 = [
    # 表題・前文
    "確認事項(第6回)", "TeamWorks(DB開発側)",
    "作成日:2026-09-08", "DFARobotics",
    "参照用アカウントを発行いただき、ありがとうございました",
    "GRANTSELECTONclean_watch.*", "DB名clean_watch",
    "2026-09-08に受領しております",
    "また第5回以降にいただいた",
    "error_typeの24値", "error_levelの4値",
    "いずれもすでに反映しています",
    "本書は次の4部構成です",
    "質問は2件です。今回は質問を2件に絞ってあります",
    "エラー種別についてお伺いする予定だった件は",
    "当方で確認できるようになりましたので取り下げました",
    # 前文の表
    "解決した件のご共有", "いただいた回答をどう反映したかのご報告",
    "接続先(ホスト名・ポート)", "のご連絡",
    "あとはこれだけで接続できます", "最優先",
    "最新のDDLのお願い", "第5回からの継続。A-1/A-3/B-3b",
    "継続と保留", "DB更新用アカウント/B-1・B-2は保留で了解しております",
    "回答不要", "高",
    # 1-1
    "1-1.参照用アカウントを受領しました",
    "にて発行いただきました", "ありがとうございます",
    "認証情報は当方の環境変数ファイルにのみ保管し",
    "文書・リポジトリには残していません",
    "あとは接続先さえ分かれば繋がる状態です",
    # 1-2
    "対応表を作成しました",
    "Y-1/Y-2のご回答をありがとうございました",
    "24値がすべてEmergencyStopのような英字の識別子であること",
    "その一覧を整理した資料を作成し",
    "実装の突き合わせ先としています",
    "varchar(100)という長さも、この形であれば納得がいきました",
    # 1-3
    "エラー種別の扱いを変更しました",
    "貴社側の追加対応は不要です",
    "7分類", "ハンドルアップ", "LostLocation", "オフライン",
    "ブラシエラー", "緊急停止", "充電不足", "その他",
    "に寄せて表示する設計でおりました",
    "いただいた24値と突き合わせたところ",
    "7分類のうち3つに対応する値が見当たりませんでした",
    "ここで当方が推測で対応付けを行うと",
    "現場に誤った指示を出すおそれがあります",
    "モーター異常を「ブラシエラー」と表示してしまう",
    "そこで方針を変更し",
    "DBに格納されている値を、変換せずそのまま画面に表示する方式にしました",
    "画面にはEmergencyStopのような識別子がそのまま出ます",
    "これは意図した挙動です",
    "実データの分布を確認したうえで、必要になった時点で表示名を整えます",
    "貴社側で追加の対応が必要になることはありません",
    "貴社が後から値を追加された場合でも",
    "当方の画面が壊れなくなります",
    # 1-4
    "Warning/Eventの非格納も、そのまま前提にしました",
    "はファイルに記録するけど",
    "データベースに情報として格納しない予定",
    "とのご注記を承知しました",
    "当方はFatal/Errorの2つだけが読める前提で実装します",
    "重要度の表示もこの2段階を前提に設計しています",
    "内容をみて後から判断するかもしれない",
    "コード自体は4値すべてを受け取れる形にしてあります",
    "当方の画面は壊れません",
    # 1-5
    "1-5.エラー種別についての照会は、当方での確認に切り替えました",
    "第5回・第6回で",
    "ハンドルアップやブラシエラーがerror_typeのどの値に当たるか",
    "をお伺いする予定にしておりました",
    "1-3のとおり、格納されている値をそのまま表示する方式に変更しました",
    "分類の対応付けについてお伺いする必要がなくなりました",
    "あわせて",
    "参照用アカウントをいただきましたので、"
    "error_typeに実際に入っている値は当方で確認できます",
    "これらの照会は取り下げます",
    "実データを確認して疑問が残った場合は、あらためてお伺いします",
    "具体的な件数や値を添えてお尋ねします",
    "ご判断いただきやすいかと存じます",
    # 2. 接続先
    "をお教えいただけますでしょうか",
    "アカウントはいただきましたので、あとは",
    "接続先のホスト名(またはIPアドレス)と、ポート番号",
    "をお教えいただければ接続できます",
    "あわせて確認させてください",
    "接続元IPの制限はありますか",
    "当方の接続元をお伝えします", "のでお知らせください",
    "SSL/TLS接続は必須でしょうか",
    "証明書の受け渡し方法", "もお教えください",
    "接続先はステージング環境でしょうか、本番環境でしょうか",
    "参照のみ/負荷のかけ方", "を合わせたいためです",
    "なぜ急いでお願いしているか",
    "9月中にPPIH様向けにデモを実施する可能性があります",
    "実データでお見せできるかどうかが、この接続情報で決まります",
    "サンプルデータでの動作確認は済んでおり",
    "画面自体は動いています",
    "実データに接続できれば、そのままデモに使えます",
    "接続情報だけが、現時点で当方から埋められない部分です",
    "接続でき次第、当方で確認する内容を先に共有しておきます",
    "貴社への照会をこれ以上増やさないためです",
    "繋がった時点で照会が減ります",
    "robot_error_reports.error_type",
    "実際に出現する値と件数の分布",
    "第6回1-5(取り下げた照会の裏取り)",
    "robots.status", "実際に出現する値",
    "第2回でいただいたコード表", "との突き合わせ",
    "週次サマリのweekに", "6が入る行があるか", "第5回X-1",
    "week=0/month=0ANDweek=0の粒度", "第5回X-2",
    "月跨ぎ週が", "2行に分かれて格納されているか", "第5回X-3",
    "error_levelにWarning/Eventが入っていないか", "第6回1-4",
    "貴社への照会を減らせます",
    "確認した結果は、こちらからご報告します",
    # 3. DDL
    "最新のDDLをいただけますでしょうか(第5回からの継続)",
    "第4回で", "A-1/A-3/B-3bは対応済み",
    "とのご回答をいただきましたが",
    "受領済みのclean_watch_20260907.sql",
    "2026-09-0710:29受領", "には入っておりませんでした",
    "3つの方法", "09-02版との全体差分",
    "CREATETABLEブロックからの項目抽出",
    "追加されるはずの制約名の全文検索",
    "いずれも同じ結果でした",
    "お願いしていた7項目のうち、反映されていたのは2項目です",
    "受領済みDDLの状態",
    "error_idを主キーと位置付けて対応",
    "未反映", "PRIMARYKEY無し。UNIQUEKEYはerror_idのみ",
    "contact_type/emailの型",
    "一部のみ",
    "customer_contacts.contact_typeはtinyintunsignedに修正済み",
    "facility_contacts.emailがvarchar(255)のまま",
    "security_status_histories.reported_at", "のDEFAULT",
    "timestampNOTNULLのまま",
    "反映いただいていた2件",
    "clean_task_reportsのcreated_at/updated_atのDEFAULT",
    "当方のスキーマにも取り込み済みです",
    "参照用アカウントをいただいたので、当方でも確認できるようになりました",
    "prismadbpullでスキーマを取り込めば",
    "DDLの再受領を待たずに現状を確認できます",
    "この点でも接続情報(第2部)が効いてきます",
    "取り込めるのは「今こうなっている」という状態だけで",
    "それが意図された最終形かどうかは当方には判断できません",
    "まとまった段階でDDLをいただけると確実です",
    "受領しましたら、当方でその日のうちに照合してご報告します",
    "第5回と同じ運用です",
    "急かす意図はありません",
    "いずれも当方の開発を止めていません",
    "状況が分かれば十分です",
    # 4. 継続と保留
    "4.継続と保留",
    "継続:DB更新用アカウントの発行(第1回から)",
    "status/comment/responded_at/resolved_at",
    "の4カラムのみ", "UPDATE可、という権限をお願いしております",
    "後で必要なテーブルに更新権限を付与する",
    "とのご連絡をいただいており、承知しております",
    "恐れ入りますが、発行の見込みをお教えいただけますでしょうか",
    "当方の作業は止まっておりません",
    "対応状況の更新機能は画面まで実装済みで",
    "書き込み先を差し替えるだけの状態です",
    "時期の目安さえ分かれば、そこに合わせて段取りします",
    "保留で了解しているもの:B-1/B-2",
    "外部キー3本とインデックスについて「後で登録する」との",
    "保留で了解しております", "当方の開発は止まっていません",
    "登録された際に、DDLの再受領時にあわせて確認します",
    "データ保持ポリシーについて(催促ではありません)",
    "過去データをどれだけ遡って保持するかについては",
    "当方の都合で後回しにしております",
    "画面の期間指定の上限に関わる話ですが、P1の実装には影響しません",
    "貴社へのお願いではありませんので、ご対応は不要です",
    # おわりに
    "おわりに",
    "今回は、お伺いすることを2件に絞りました",
    "優先度をつけるとすれば次の順です",
    "アカウントはいただいたので、これだけで接続できます",
    "9月中のデモを実データで行えるかがここで決まります",
    "受領後、当方でその日のうちに照合してご報告します",
    "1(接続先)が最優先です",
    "これだけ先にいただければ、当方で確認できることが一気に増えます",
    "第2部の表に挙げた6件は、繋がった時点で当方で確認できますので",
    "その分だけ貴社への照会が減ります",
    "引き続きよろしくお願いします",
]

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. 識別子の語中改行 ==")
# wordWrap="CJK" は ASCII の語中でも折り返す。折り返されると抽出テキストに
# 改行が入るため、空白を詰めない生の抽出テキストで原形のまま探す。
IDENT = [
    "clean_watch", "GRANT SELECT ON clean_watch.*",
    "error_type", "error_level", "EmergencyStop", "LostLocation",
    "varchar(100)", "Fatal / Error",
    "robot_error_reports.error_type", "robots.status",
    "week = 0 / month = 0 AND week = 0",
    "clean_watch_20260907.sql",
    "customer_contacts.contact_type", "facility_contacts.email",
    "security_status_histories.reported_at",
    "clean_task_reports", "control_error_histories",
    "robot_error_reports", "prisma db pull",
    "created_at / updated_at",
    "status / comment / responded_at / resolved_at",
    "tinyint unsigned", "varchar(255)", "timestamp NOT NULL",
    "PRIMARY KEY", "UNIQUE KEY", "CREATE TABLE", "SSL/TLS",
]
broken = [i for i in IDENT if i not in text]
print("検査した識別子:", len(IDENT), " 語中改行:", len(broken))
for b in broken:
    print("  ★語中で割れている★", b)

print("\n== 5. 認証情報が載っていないこと ==")
# 参照用アカウントのユーザー名・パスワードは PDF に書かない(原稿にも無い)
SECRET = ["password", "PASSWORD", "IDENTIFIED BY", "passwd", "pwd="]
leak = [s for s in SECRET if s in text]
print("認証情報らしき語:", leak if leak else "なし")

print("\n== 6. 判定 ==")
ok = (not miss) and bool(biz) and (not hit) and (not broken) and (not leak)
print("結果:", "OK" if ok else "NG")
sys.exit(0 if ok else 1)