# -*- coding: utf-8 -*-
"""第8回 PDF「確認事項（第8回）」の自己点検。

原稿 (docs/handoff/TeamWorks確認事項_第8回_20260911.md) の文言が
すべて PDF に含まれているか、フォントが日本語フォントで埋め込まれているかを見る。
PDF のパスは第1引数で渡せる。省略時はこのスクリプトと同じディレクトリ。

check_teamworks_qa7_pdf.py（第7回用）と同じ構成。第8回で足した検査は2つ。
  ★5. 設問の採番★ — 原稿は10章25問（Q1-1 〜 Q10-1）。
     本文と巻末のご回答欄の両方に、25問すべての採番が出ていることを見る。
  ★6. 豆腐（グリフ欠け）★ — 抽出テキストの全文字を BIZ UDGothic の cmap に
     引き当て、字が無い文字（□ になる文字）が1つも無いことを見る。
     Σ / ÷ / ◎○△× / 〜 / ＝ など、第8回で新しく出てきた記号があるため。

★must に「ページを跨ぐ語」を入れないこと★
抽出テキストにフッター（D-Watch / DFA Robotics とページ番号）が挟まり、
空白を詰めても一致しません。第3回で bigint auto_increment が実際に引っかかりました。

★4 の検査があっても、表の列幅崩れの目視確認は省かないこと★
4 は「識別子が行の途中で割れていないか」を抽出テキストで見るだけで、
枠のはみ出し・改ページ位置・重なりは見られません（pdf/README.md）。
"""
import os
import re
import sys

from pypdf import PdfReader
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont

PDF = (sys.argv[1] if len(sys.argv) > 1 else
       os.path.join(os.path.dirname(os.path.abspath(__file__)),
                    "DWatch_確認事項_第8回_20260911.pdf"))
if not os.path.exists(PDF):
    raise SystemExit("PDF が見つからない: %s\n"
                     "先に make_teamworks_qa8_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 が埋め込まれていない")
# 埋め込まれているフォントが BIZ UDGothic だけであること。
# サブセット接頭辞（AAAAAA+）が付いているものが埋め込みフォント。
# 接頭辞の無いものは reportlab が必ず1つ書き出す標準14フォント（Helvetica）で、
# フォントファイルを持たず本文にも使われていない。第7回の PDF にも同じ1件がある。
allfonts = sorted(set(re.findall(rb"/BaseFont\s*/([A-Za-z0-9+\-,._]+)", raw)))
embedded = [f.decode() for f in allfonts if re.match(rb"^[A-Z]{6}\+", f)]
standard = [f.decode() for f in allfonts if not re.match(rb"^[A-Z]{6}\+", f)]
print("埋め込みフォント:", embedded)
print("非埋め込みの標準フォント（reportlab の既定・未使用）:", standard)
notbiz = [f for f in embedded if "BIZ" not in f]
if notbiz:
    print("  ★NG★ BIZ UDGothic 以外のフォントが埋め込まれている:", notbiz)
if len(embedded) != raw.count(b"/FontFile2"):
    print("  ★注意★ 埋め込み名の数と /FontFile2 の数が合わない")
# 中国語向けフォントが混ざっていないこと（CLAUDE.md §0-3）
CN = [b"SimSun", b"SimHei", b"MicrosoftYaHei", b"MSYaHei", b"MSGothic",
      b"NotoSansSC", b"NotoSerifSC", b"SourceHanSansSC", b"SourceHanSerifSC",
      b"FangSong", b"KaiTi", b"STSong", b"STHeiti", b"PingFang"]
hit = [n.decode() for n in CN if n in raw]
print("中国語向けフォント:", hit if hit else "混入なし")
if hit:
    print("  ★NG★ 中国語向けフォントが混ざっている")

print("\n== 3. 原稿の全項目が含まれるか ==")
must = [
    # 表題・前文
    "確認事項（第8回）", "TeamWorks（DB開発側）",
    "作成日:2026-09-11", "DFARobotics",
    "はじめに（第7回がまだ未回答です）",
    "第7回（2026-09-10送付）へのご回答は、本書作成時点でまだいただいておりません",
    "第7回の3件は取り下げません。引き続きお願いいたします",
    # はじめに の表
    "第7回でお願いしていたこと", "状態",
    "接続先（ホスト名・ポート）", "未回答。全体を通じて最優先です",
    "clean_timetables.precheckの", "値の範囲と反映時期", "未回答",
    "DDLの未反映4件", "（A-2/A-3b/B-2/B-3b）",
    "未回答（お手すきの折で結構です）",
    "あわせて、DB更新用アカウントの発行は、"
    "第5回（2026-09-07）・第6回（2026-09-08）から継続してお願いしている件です",
    "第7回では書き漏らしてしまいましたが、取り下げたわけではありません",
    "本書の10章に、あらためて記載しております。急ぎではございません",
    "本書は第7回の追加です。第7回の内容を差し替えるものではありません",
    "そのうえで、ローカルの検証環境でレポート画面まで動かし、"
    "あわせて未確認事項の棚卸しを行ったところ、確認事項が10件になりました",
    "うち2件（1章・2章）は、放置すると画面に出る内容そのものが壊れる性質のものです",
    "本書は次の10部構成です。すべて質問です。1問1答の形にしております",
    # 前文の表（10部構成）
    "内容", "優先度",
    "1.未実施の清掃にも", "履歴の行は作られるか",
    "当方の設計の土台です", "最優先",
    "2.DATETIMEの", "タイムゾーン", "日本時間の壁時計値か、UTCか",
    "3.実際の開始・終了時刻は", "どこにあるか",
    "started_at/ended_atは", "予定時刻に見えます", "高",
    "4.サマリの率・分子・分母の", "関係",
    "計算式と件数列の定義を", "まとめてお伺いします",
    "5.未実施行", "（clean_error=1）の", "他カラムの値",
    "一部はお願い", "（収集処理の変更を伴います）", "中",
    "6.clean_errorが", "2/3/4のときの扱い", "エラー報告・完了率・優先順位",
    "robot_error_reports.clean_timetable_id", "はどちらの行を指すか",
    "エラー率が0%になる", "可能性があります",
    "8.control_error_historiesは", "実際に使われるか",
    "D-Secアラート画面の", "作り直しに関わります",
    "9.軽い確認2件", "バッテリー履歴/エリア別完了率", "低",
    "10.継続のお願い:", "DB更新用アカウント",
    "第5回・第6回からの継続。", "時期の目安だけでも", "最古の待ち",
    "なお、5と4の後段は貴社の担当領域（DBとバッチ）への変更を伴いますので、"
    "「お願い」として書いております",
    "そのままで差し支えない、というご判断であれば、"
    "その旨をお知らせいただければ当方で対応します",
    # 1章
    "1.清掃が実施されなかった場合も、clean_status_historiesに行は作られますでしょ"
    "うか—最優先",
    "1-1.当方は「作られる」という前提で実装しております",
    "当方は、清掃詳細ログの「出発したかどうか」を次のように判定し、"
    "すでに実装済みです",
    "出発しなかった", "=clean_status_historiesに行がある",
    "かつその行のclean_errorが1（異常(清掃未実施)）",
    "つまり「清掃が実施されなかった場合も、clean_error=1の行が1行作られる」"
    "という前提です",
    "この前提が、本作業の設計そのものの土台になっております",
    "1-2.前提が外れた場合に何が起きるか（正直に書きます）",
    "当方は「履歴の行が無い過去の清掃」を『出発失敗』として画面に出す"
    "設計にしております",
    "したがって、未実施のときに行が作られない運用ですと、"
    "「結果がまだ書き込まれていないだけの清掃」まで出発失敗として並びます（過剰検知）",
    "画面が真っ赤になる一方で、例外もエラーログも出ません",
    "そのため、当方だけでは正誤を判断できません",
    "1-3.お伺いしたいこと（2問）",
    "Q1-1.清掃が実施されなかった場合、clean_status_historiesに"
    "行は作られますでしょうか",
    "選択肢",
    "作られる（clean_error=1の行が1行できる）",
    "作られない（実施された清掃にだけ行ができる）",
    "場合による（条件をお教えください）",
    "Q1-2.1つのclean_timetable_idに対し、clean_status_historiesは必ず1行でしょうか",
    "必ず1行", "複数行になることがある（どういうときかをお教えください）",
    "補足", "当方は現在、複数行あった場合はended_atが最新の1件を採用する",
    "という規則で実装しております",
    "この選び方で差し支えないかもあわせてお教えいただけますと助かります",
    "1-4.なぜお伺いするか",
    "前提が外れても「出発失敗」という機能自体は残せますが、"
    "上記のとおり過剰検知になります",
    "運用担当が毎朝見る画面ですので、誤検知が続くと画面そのものが"
    "信用されなくなります",
    "本書のなかで、当方の実装への影響がいちばん大きい項目です。"
    "このため冒頭に置いております",
    # 2章
    "2.DATETIMEに入っているのは日本時間の壁時計値でしょうか、UTCでしょうか—",
    "2-1.当方の側で起きていること（実測）",
    "当方のアプリは、DATETIMEの値をUTCとして読んだうえで、さらに9時間足しています",
    "DBが日本時間の壁時計値を持っている場合、画面に出る時刻が9時間ずれます",
    "内訳は次の2段です。いずれも当方のコードを実測して確認したものです",
    "何が起きるか",
    "当方が使っているPrismaのMariaDB用アダプタは、"
    "DATETIMEをUTCの値として読みます",
    "（newDate(値+\"Z\")という実装です）",
    "この挙動は設定で変更できません。接続文字列にtimezoneを指定しても効きません",
    "（アダプタがドライバ側の変換を通らないためです）",
    "その値を画面用の日本時間に直す処理が、さらに9時間足します",
    "具体例です",
    "DBのclean_timetables.start_atの生値", "DBが日本時間の場合、実際は",
    "当方の画面に出る時刻",
    "2026-09-1102:00:00", "9月11日深夜2時の清掃", "9月11日11:00",
    "2-2.表示だけの問題ではありません",
    "未来の予定を一覧から外す判定にも、この時刻を使っています",
    "D-Watchは「夜間に自動実行された清掃を、翌朝DFAの運用担当が確認する」"
    "という使い方を想定しています",
    "上の例で、運用担当が翌朝8時に画面を開くと、"
    "深夜2時の清掃が「11:00の予定」と読まれます",
    "まだ来ていない予定として扱われ、一覧から消えます",
    "つまり、確認したい清掃がちょうど画面に出ない、という壊れ方をします",
    "このため最優先でお伺いしております",
    "2-3.お伺いしたいこと（4問）",
    "Q2-1.DATETIME列に格納されているのは、次のどちらでしょうか",
    "日本時間の壁時計値（深夜2時の清掃なら02:00:00が入っている）",
    "UTCの壁時計値（深夜2時の清掃なら17:00:00（前日）が入っている）",
    "列によって違う（違う場合、どの列がどちらかをお教えください）",
    "対象として特に知りたい列は次のとおりです",
    "テーブル", "列",
    "clean_timetables", "start_at/end_at",
    "clean_status_histories", "started_at/ended_at",
    "clean_statuses",
    "robot_error_reports", "errored_at/responded_at/resolved_at",
    "control_error_histories", "同種の日時列",
    "Q2-2.データ収集処理が書き込むときの基準は、次のどれでしょうか",
    "サーバのローカル時刻（日本時間）で書いている",
    "UTCに直してから書いている",
    "PuduCloudから返ってきた値をそのまま書いている"
    "（この場合、Pudu側がどちらかもお分かりでしたら）",
    "Q2-3.実データを1行だけご提示いただけませんでしょうか",
    "clean_timetablesの任意の1行について、次の2つをお教えください",
    "教えていただきたいもの",
    "start_atの生値（例:2026-09-1102:00:00）",
    "それが実際には何時の清掃を指すか（例:9月11日の深夜2時の清掃）",
    "この2つがあれば、当方で機械的に検証できます",
    "1行で結構です。以後この件でお伺いすることはありません",
    "なお、第7回でお願いしている接続先をいただければ、"
    "生値そのものは当方で見られます",
    "ただしその値が実際に何時の清掃を指すのかは、当方には分かりません",
    "ので、2番だけはどうしてもお伺いする必要があります",
    "Q2-4.TIMESTAMP型の列について、本番DBのtime_zone設定をお教えください",
    "created_at/updated_atのほか、control_reports.reported_at/",
    "security_status_histories.reported_atはTIMESTAMP型です",
    "TIMESTAMPはDATETIMEと違い、"
    "セッションのtime_zone設定によって読み出される値が変わります",
    "このため、DATETIMEとは別にお伺いしております",
    "本番DBのtime_zoneの設定値（globalとsessionの既定値）",
    "データ収集処理が接続する際にtime_zoneを設定していますか（していれば、その値）",
    "2-4.なぜお伺いするか",
    "当方から伺っているのは「おそらく日本時間です」までで、確定しておりません",
    "推測で実装すると、ずれていた場合に"
    "「画面には出ているが1つもデータが無い日がある」という形で表面化します",
    "例外もエラーも出ないため、気づくのが遅れます",
    "上の2-2のとおり、運用そのものが成り立たなくなる性質の問題ですので、"
    "当方の判断で決めてしまわず、お伺いすることにしました",
    # 3章
    "3.clean_status_historiesのstarted_at/ended_atは予定時刻でしょうか",
    "3-1.現状の理解",
    "テーブル定義書の備考に、次のようにございました",
    "定義書の備考",
    "clean_status_histories.started_at",
    "「清掃スケジュールの清掃開始日時を格納する」",
    "clean_status_histories.ended_at",
    "「清掃スケジュールの清掃終了日時を格納する」",
    "この書き方ですと、実際に清掃を始めた時刻ではなく、予定の時刻に読めます",
    "いっぽうclean_statuses側の同名の列（started_at/ended_at）は",
    "備考が空欄で、NULL許可", "になっております",
    "こちらが実績時刻なのかもしれない、とも読めます",
    "当方には、どちらが正か判断できませんでした",
    "3-2.お伺いしたいこと（2問）",
    "Q3-1.clean_status_histories.started_at/ended_atは予定時刻という理解で"
    "合っていますでしょうか",
    "合っている。予定時刻が入る",
    "実績時刻が入る（定義書の備考が実態と違う）",
    "開始と終了で違う（違う場合、どちらがどちらかをお教えください）",
    "Q3-2.Aの場合、実際に開始・終了した時刻はどこかに保持されますでしょうか",
    "clean_statuses.started_at/ended_atが実績時刻",
    "clean_task_reports側に実績時刻がある（列名をお教えください）",
    "実績時刻は保持していない", "その他（お教えください）",
    "3-3.なぜお伺いするか",
    "清掃詳細ログの画面に、清掃の開始・終了時刻を表示します",
    "予定時刻を実績として出してしまうと、"
    "運用担当が「定刻どおりに動いた」と誤読します",
    "同じ清掃に履歴が複数ある場合、"
    "当方はended_atが最新の1件を採用する実装にしています（Q1-2）",
    "予定時刻だとすると、この選び方の根拠が変わります",
    "C（保持していない）でも当方の実装は動きます",
    "その場合は、画面に予定時刻である旨を明示する形に直します",
    # 4章
    "4.サマリの率・分子・分母の関係をお教えください",
    "本節の後段のお願いは、バッチの変更を伴う可能性があります。"
    "そうと分かるように書いております",
    "4-1.お伺いしたいことの全体像",
    "サマリテーブル（customer_summaries/facility_summaries）には",
    "率4列（operation_ratio/launch_ratio",
    "/clean_ratio/error_ratio）と",
    "件数3列（task_count/launch_count/error_count）がございます",
    "当方は、この率と件数の関係が読み取れずにおります",
    "関連する内容が分散しないよう、1つの節にまとめてお伺いいたします",
    "定義書の比率4列の備考は「補足を参照.」とのみ記載されており、"
    "その「補足」の該当部分が、当方の受領範囲に含まれておりませんでした",
    "もし文書としてお持ちでしたら、それをお送りいただくだけで本節（",
    "Q4-1〜Q4-4）はすべて解決します",
    "4-2.お伺いしたいこと（4問）",
    "Q4-1.率4列は、それぞれどの式で算出されていますでしょうか",
    "分子と分母が何かを、1行ずつお教えいただけますと助かります",
    "当方の理解", "ご回答（分子÷分母）",
    "operation_ratio", "（稼働率）", "清掃実施回数÷清掃予定回数",
    "launch_ratio", "（出発成功率）", "定刻出発回数÷清掃予定回数",
    "clean_ratio", "（清掃完了率）", "Σ清掃面積÷Σ清掃対象面積", "（面積ベース）",
    "error_ratio", "（エラー発生率）", "エラー発生回数÷清掃実施回数",
    "特に分からずにおります点",
    "件数列はtask_count/launch_count/error_countの3つだけで、",
    "operation_ratioの分子にあたる「清掃実施回数」の列が見当たりません",
    "このため当方の実装では、当期（進行中の週・月）の稼働率と"
    "出発成功率が必ず同じ値になっております",
    "これが正しいのかどうかを判断できずにおります",
    "Q4-2.件数3列の定義をお教えください（特にlaunch_count）",
    "当方は、清掃詳細ログの画面で「その清掃が出発したかどうか」を"
    "次のように判定しており、実装済みです",
    "出発した", "かつその行のclean_errorが1（異常(清掃未実施)）ではない",
    "サマリのlaunch_countも同じ定義でしょうか",
    "同じ。clean_error!=1の件数",
    "clean_error=0（正常）のみを数える（2/3/4も出発していない扱い）",
    "「定刻」を判定している（予定時刻から一定時間内に開始したものだけ。"
    "その場合、猶予時間もお教えください）",
    "あわせて、task_count（清掃予定回数と理解しております）と"
    "error_countの数え方も一言でお教えいただけますと助かります",
    "Q4-3.率カラムと、件数カラムから再計算した値は一致しますでしょうか",
    "当方の検証用ダミーデータで検算したところ、一致しない行がございました",
    "先にお断りいたします", "当方はまだ実データに接続できておりません",
    "下記はいただいたDDLから当方がローカルに作ったダミーデータに対する検算です",
    "差が出た原因が当方の作ったデータ側にある可能性が高いと考えております",
    "貴社のバッチに問題がある、という指摘ではありません",
    "項目", "実測値",
    "突き合わせた行数", "施設サマリ24行",
    "食い違った行数", "6行", "最大の差", "0.5ポイント", "例",
    "facility_id=4:", "launch_ratio=84.5に対し、",
    "launch_count/task_count×100=85.00",
    "一致する", "丸めのぶんだけ、わずかにずれることがある",
    "率と件数は別の元データから作っているため、必ずしも一致しない",
    "Q4-4.率カラムの丸め方をお教えください",
    "double(4,1)（小数第一位）に格納する際、"
    "小数第二位以下をどう処理されていますでしょうか",
    "四捨五入", "切り捨て", "切り上げ",
    "お願い（Q4-3がB/Cの場合）",
    "もし率と件数が一致しない作りでしたら、一致する形にしていただけないでしょうか",
    "バッチの変更を伴うお願いですので、"
    "難しければ「一致しません」とお知らせいただくだけで結構です",
    "その場合、当方は差が出ても異常とみなさない実装にします",
    "4-3.なぜお伺いするか",
    "定義が違うと、レポート画面の「出発成功率」と、"
    "清掃詳細ログの「出発」の件数が食い違います",
    "同じ画面群の中で数が合わないと、運用担当はどちらを信じてよいか分かりません",
    "当方は、サマリの値を当方の計算で上書きしない方針です",
    "貴社のバッチが出した値をそのまま表示しますので、"
    "定義を合わせておく必要があります",
    "当方の設計方針では、サマリは「丸める前の小数」と「分子・分母」の両方を持ち、"
    "検算できることを前提としております",
    "貴社が件数列を用意してくださっているのも、この趣旨と理解しております",
    "実データに接続したあと差が出たときに、「当方の実装の誤り」なのか"
    "「バッチの仕様どおり」なのかを切り分けられません",
    "先に「一致するはず／しないはず」が分かっていれば、当方だけで判断できます",
    # 5章
    "5.未実施の行（clean_error=1）の、ほかの列には何が入りますでしょうか",
    "本節は、1章のQ1-1がA（行が作られる）だった場合のお伺いです",
    "Q5-2の後半は、収集処理の変更を伴う「お願い」です。"
    "そうと分かるように書いております",
    "5-1.お伺いしたいこと（2問）",
    "Q5-1.clean_error=1の行のclean_statusには何が入りますでしょうか",
    "clean_status_histories.clean_statusの備考は、定義書上",
    "「2:清掃終了(セキュリティ有効),-1:清掃完了(",
    "セキュリティ無効)」の2値のみ", "で、未実施を表す値がありません",
    "既定値の0が入る", "2が入る", "-1が入る",
    "Q5-2.task_area/clean_area/clean_ratioは0でしょうか",
    "3列とも0",
    "task_areaには予定していた清掃対象面積が入る（clean_area/clean_ratioは0）",
    "あわせて、お願いがございます（収集処理の変更を伴います）",
    "もしA（3列とも0）でしたら、task_areaにだけは"
    "「予定していた清掃対象面積」を入れていただけないでしょうか",
    "難しい場合は、現状のままで結構です",
    "当方はclean_errorを見て未実施を判別する実装にしておりますので、"
    "「0が入る」と確定していれば、それに合わせて実装します",
    "値が分かることが第一で、変更は二の次です",
    "5-2.なぜお伺いするか",
    "清掃完了率が面積ベース（Σ清掃実施面積÷Σ清掃対象面積）だからです",
    "未実施の行のtask_areaが0だと、その清掃が分母から丸ごと抜けます",
    "結果として「実施できなかった日が多い施設ほど、完了率が高く出る」",
    "という逆転が起きます。予定面積が入っていれば、分母に残り、実態に近い率になります",
    "また、clean_ratioについても理由がございます",
    "当方はclean_ratio=0を「出発はしたが、清掃がほとんど進まなかった」として"
    "×判定で表示しております",
    "つまりclean_ratioだけを見ても、「0%しか清掃できなかった」と"
    "「そもそも実施していない」を区別できません",
    "現在はclean_errorで区別できておりますので、支障はありません",
    "ただし、未実施の行に何が入るかを取り違えると、この区別が崩れます",
    # 6章
    "6.clean_errorが2/3/4のときの扱いについて",
    "6-1.現状の理解",
    "clean_status_histories.clean_errorには、次の値があると伺っております",
    "0:正常", "1:異常(清掃未実施)", "2:異常(セキュリティ有効)",
    "3:異常(D-S制御盤異常)", "4:異常(清掃ロボット異常)",
    "当方は現在、「1以外は未実施ではない（＝出発した）」として扱っております",
    "そのうえで、2/3/4の行について次の点が分かりませんでした",
    "6-2.お伺いしたいこと（6問）",
    "Q6-1.clean_errorが2/3/4のとき、robot_error_reportsにも"
    "行は作られますでしょうか",
    "2/3/4のいずれも作られる", "4（清掃ロボット異常）のときだけ作られる",
    "作られない（履歴のclean_errorにだけ記録される）",
    "Q6-2.作られる場合、その行のclean_timetable_idは埋まりますでしょうか",
    "埋まる（どの清掃のエラーかを対応づけられる）", "空（NULL）のことがある",
    "埋まる場合、それが「事前チェックの行」と「本清掃の行」のどちらを指すかは、",
    "7章のQ7-1でお伺いしております",
    "Q6-3.2/3/4の行のclean_ratioと面積（task_area/clean_area）には"
    "何が入りますでしょうか",
    "途中まで清掃した実際の値が入る（例:40%まで進んで異常終了したらclean_ratio=40）",
    "0が入る（異常なので実績は記録しない）",
    "値によって違う（違う場合、どれがどちらかをお教えください）",
    "Q6-4.1回の清掃で複数の異常が同時に起きた場合、clean_errorは"
    "どれになりますでしょうか",
    "clean_errorは1カラムですので、1つの値しか入りません",
    "優先順位の規則がございましたらお教えください",
    "（例:「1が最優先」「後から起きたもので上書き」など）",
    "Q6-5.clean_errorに5以上の値が入る可能性はございますでしょうか",
    "unsignedtinyint（0〜255）ですので、将来の拡張余地があると理解しております",
    "0〜4のみ。5以上は入らない",
    "入りうる（予定されている値がありましたらお教えください）",
    "Q6-6.clean_statuses.clean_errorとclean_status_histories.clean_errorの"
    "使い分けをお教えください",
    "同じコード表ですが、定義書の論理名が異なっております",
    "（clean_statuses側が「エラー」、",
    "clean_status_histories側が「結果」）",
    "同じ意味。clean_statusesが最新、履歴側が過去分",
    "意味が違う（違いをお教えください）",
    "当方は現在、履歴側（clean_status_histories）だけを読んでおります",
    "これで差し支えないかもあわせてお教えいただけますと助かります",
    "6-3.なぜお伺いするか",
    "当方のアラート画面は、robot_error_reportsを元に作っております",
    "したがってQ6-1がC（作られない）の場合、履歴側にだけ異常が記録され、"
    "その清掃はアラートに一切上がってきません",
    "運用担当は、清掃詳細ログを1行ずつ見に行かないと気づけないことになります",
    "この場合は、当方が履歴側も見てアラートを組み立てる実装に変えます",
    "逆にA（両方に作られる）の場合は、"
    "同じ出来事を二重に数えないよう気をつける必要があります",
    "Q6-2でclean_timetable_idが埋まるのであれば、"
    "当方で突き合わせて重複を除けます",
    "Q6-3は、清掃詳細ログの完了率と◎○△×判定の意味が変わります",
    "B（0が入る）ですと、途中まで清掃できた清掃も一律×になります",
    "Q6-5は、当方が「1以外は出発した」という実装にしているためです",
    "5以上に「未実施」を意味する値が増えると、"
    "その清掃が出発した扱いになってしまいます",
    "いずれも当方で対応できます。どちらなのかが分かれば十分です",
    # 7章
    "7.robot_error_reports.clean_timetable_idは、"
    "事前チェックの行と本清掃の行のどちらを指しますでしょうか",
    "7-1.現状の理解",
    "clean_timetablesには、本清掃の行と"
    "その事前チェックの行（precheckが1/2、parent_idで本清掃を指す）"
    "があると理解しております",
    "当方は「robot_error_reports.clean_timetable_idは"
    "事前チェックの行を指す」という前提で実装しております",
    "7-2.お伺いしたいこと（1問）",
    "Q7-1.robot_error_reports.clean_timetable_idはどちらの行を"
    "指しますでしょうか",
    "事前チェックの行を指す（precheckが1/2の行）",
    "本清掃の行を指す（precheckが0の行）",
    "エラーの発生時点による（清掃中に発生したエラーがどちらに付くかを"
    "お教えください）",
    "あわせて、清掃中（本清掃の実行中）に発生したエラーは、"
    "どちらの行に紐づくかをお教えください",
    "7-3.なぜお伺いするか",
    "レポート画面のエラー発生率の分子を、"
    "clean_timetable_idから本清掃の行へたどり直して数えております",
    "このため、本清掃の行に直接付く運用ですと、たどり直しが空振りし、"
    "そのエラーが1件も数えられません",
    "結果としてエラー発生率が0%と表示されますが、例外もエラーログも出ません",
    "「エラーが無い」と読める画面になってしまうため、お伺いしております",
    # 8章
    "8.control_error_historiesは、実際に使われるテーブルでしょうか",
    "8-1.資料のあいだで食い違いがございます",
    "当方の受領資料のなかで、このテーブルの位置づけが食い違っております",
    "出典", "記載",
    "テーブル定義書の役割欄",
    "「現時点では未使用.清掃状態,清掃状態履歴で確認できるため.」",
    "第2回のご回答",
    "このテーブルのstatusの値域（0:未対応/1:対応中/2:対応済）を",
    "直接お教えいただいております",
    "同・制御盤の生死報告について",
    "「制御盤の生死報告が来ない場合は、サーバーから",
    "『制御盤異常』としてエラーが上がる」",
    "DB更新権限のお願い（10章）",
    "このテーブルの4カラムを更新対象に含めております",
    "当方は「使われる」と解釈し、D-Secアラート画面（F-07）を"
    "このテーブルから作っております",
    "8-2.お伺いしたいこと（1問）",
    "Q8-1.control_error_historiesには、実際にデータが入りますでしょうか",
    "入る（定義書の役割欄の注記が古い）",
    "入らない・未使用（制御盤異常はどのテーブルで確認すればよいかを"
    "お教えください）",
    "今は入らないが、今後入れる予定（時期の目安がありましたら）",
    "8-3.なぜお伺いするか",
    "B（未使用）の場合、D-Secアラート画面が常に空になります",
    "画面としては表示されるものの、1件も出ないという状態です",
    "この場合は、当方が別のテーブルから組み立て直す作り替えが必要になります",
    "実データに接続すれば件数は当方で数えられますが、"
    "「たまたま0件」なのか「そもそも入らない設計」なのかは、"
    "当方には判断できません",
    "このテーブルについては、これまで一度もお伺いしておりませんでした。恐れ入ります",
    # 9章
    "9.軽い確認を2件、お願いいたします",
    "どちらも「無ければ無いで結構です」という性質のものです",
    "9-1.お伺いしたいこと（2問）",
    "Q9-1.事前チェック時点のバッテリー残量を保持するご予定はございますでしょうか",
    "robots.batteryは現在値のみで、履歴が無いと理解しております",
    "このため、事前チェック（60分前/30分前）の時点でバッテリーが何%だったか",
    "を、当方から取得する手段がございません",
    "保持する予定は無い",
    "保持する予定がある（どのテーブル・列になるかをお教えください）",
    "A（予定が無い）でしたら、当方は事前チェック画面のバッテリー欄を"
    "恒久的に空欄として確定いたします",
    "その確認だけができれば十分です。追加のご対応は不要です",
    "Q9-2.PuduCloudから、清掃エリア別の完了率は取得できますでしょうか",
    "要件定義の段階から未解決のまま持ち越している項目です",
    "清掃1回のなかで、エリアごとに何%清掃できたか", "という粒度のデータです",
    "取得できる（どのテーブルに入る想定かをお教えください）",
    "取得できない（PuduCloudAPIが返さない）",
    "未確認（その旨をお知らせいただければ、当方はBと同じ扱いで進めます）",
    "BまたはCでしたら、当方はこの機能をPhaseB送りとする判断材料にいたします",
    "現在は、該当のAPIを「未実装」を返す形で仮置きしております",
    "9-2.なぜお伺いするか",
    "どちらも「無い」と分かれば、当方は画面から外す・PhaseBに送る、"
    "という判断ができます",
    "分からないままですと、実装するかどうかを決められず、仮置きのまま残り続けます",
    "貴社に何かをご対応いただきたいという趣旨ではございません",
    # 10章
    "10.継続のお願い:DB更新用アカウントの発行（第5回・第6回からの継続）",
    "10-1.お願いしている内容（既に範囲は合意済みです）",
    "下記の2テーブル・4カラムのみUPDATE可、"
    "というアカウントの発行をお願いしております",
    "更新するカラム",
    "status/comment/responded_at/resolved_at",
    "範囲についてはすでに合意いただいており、変更のお願いはございません",
    "第6回では「後で必要なテーブルに更新権限を付与する」との"
    "ご連絡をいただいております",
    "10-2.お伺いしたいこと（1問）",
    "Q10-1.発行の時期の目安をお教えいただけませんでしょうか",
    "急ぎではございません", "「いつ頃になりそうか」という目安だけで結構です",
    "未定でしたら「未定」とお知らせいただくだけでも助かります",
    "10-3.なぜお伺いするか",
    "2026-09-03からお願いしている件で、"
    "当方の外部待ちのなかでいちばん古いものです",
    "第7回では書き漏らしてしまい、失礼いたしました",
    "当方の作業は止まっておりません",
    "アラート（F-03）とD-Secアラート（F-07）の「対応状況を更新する」機能は、"
    "画面まで実装済みで、書き込み先を差し替えるだけの状態です",
    "ただし、対応状況を更新するAPIだけは、"
    "書き込み先が無いため未実装のままとなっております",
    "時期の目安が分かれば、そこに合わせて段取りいたします",
    # おわりに
    "おわりに",
    "本書では10件をお伺いしております。数が多くなってしまい、恐れ入ります",
    "まずは、この3つだけでも",
    "お忙しいと存じますので、優先度の高い3つを先に挙げます",
    "この3つにご回答いただければ、当方は先へ進めます",
    "順",
    "（Q1-1）", "A/Bのどちらかを選んでいただくだけです",
    "外れていると、正常な清掃まで「出発失敗」として並びます（過剰検知）",
    "（特にQ2-3の実データ1行）",
    "ずれていると、翌朝の確認時に深夜の清掃が画面から消えます",
    "運用そのものが成り立ちません",
    "Q2-3の1行をいただければ、あとは当方で検証できます",
    "4.サマリの率・分子・分母", "（Q4-1〜Q4-4）",
    "レポート画面の数字が、清掃詳細ログの件数と食い違います",
    "「補足」の文書があれば、それをお送りいただくだけで解決します",
    "残りの優先度", "理由",
    "の指す先",
    "エラー発生率が0%と表示される可能性があります。例外が出ないため気づけません",
    "3.実際の開始・終了時刻の所在",
    "画面に予定時刻を実績として出してしまうためです",
    "6.clean_error2/3/4の扱い",
    "アラートに上がるか、完了率がどうなるかが変わります",
    "使われるか", "未使用でしたらD-Secアラート画面を作り直します",
    "5.未実施行の他カラムの値", "値が分かれば十分です。変更は二の次です",
    "10.DB更新用アカウント",
    "最古の待ち（2026-09-03〜）ですが、急ぎません。時期の目安だけで結構です",
    "「無い」と分かれば当方で判断できます",
    "第7回の3件について",
    "第7回でお願いしている3件（接続先/precheckの値域/DDLの未反映4件）は、"
    "本書によって取り下げるものではありません",
    "とりわけ接続先は、本書の10件を含めた全体を通じて最優先です",
    "接続できれば、当方で数えられるものは当方で確認しますので、"
    "その分だけ貴社への照会が減ります",
    "（本書の1・2・4・5・6・8も、接続できていれば当方で実データを見て",
    "部分的には", "自力で確認できたものです",
    "ただし「その値が何を意味するか」"
    "「たまたま0件なのか、そもそも入らないのか」は当方には判断できませんので、"
    "いずれにせよお伺いすることになります",
    "お忙しいところ恐れ入りますが、よろしくお願いいたします",
    # ご回答欄
    "ご回答欄",
    "この表に一言だけご記入いただく形でも、"
    "下の自由記入欄にまとめてご記入いただく形でも結構です",
    "上位3つ（Q1-1/Q2-3/Q4-1〜Q4-4）だけでも助かります",
    "質問番号", "質問の要約", "ご回答",
    "未実施の清掃にもclean_status_historiesの行は作られるか",
    "（A/B/C）",
    "1つのclean_timetable_idに履歴は必ず1行か（A/B）",
    "DATETIMEは日本時間の壁時計値か、UTCか（A/B/C）",
    "収集処理が書き込むときの基準（A/B/C）",
    "clean_timetablesの1行のstart_atの生値と、", "それが指す実際の時刻",
    "本番DBのtime_zone設定と、収集処理の接続時の設定",
    "clean_status_histories.started_at/ended_atは", "予定時刻か（A/B/C）",
    "実績時刻の所在（A/B/C/D）",
    "率4列の計算式（分子÷分母）。または「補足」の文書",
    "task_count/launch_count/error_countの定義",
    "（launch_countはA/B/C/D）",
    "率と件数から再計算した値は一致するか（A/B/C）",
    "率の丸め方（A/B/C）＋検算のお願いの可否",
    "clean_error=1の行のclean_status（A/B/C/D）",
    "同・task_area/clean_area/clean_ratio（A/B/C）", "＋面積のお願いの可否",
    "clean_error2/3/4でエラー報告も作られるか", "（A/B/C/D）",
    "その行のclean_timetable_idは埋まるか（A/B）",
    "2/3/4の行のclean_ratioと面積（A/B/C）",
    "複数の異常が同時に起きた場合のclean_errorの優先順位",
    "clean_errorに5以上が入りうるか（A/B）",
    "clean_statuses.clean_errorと履歴側の使い分け（A/B）",
    "robot_error_reports.clean_timetable_idは",
    "事前チェック行か本清掃行か（A/B/C）",
    "control_error_historiesにデータは入るか（A/B/C）",
    "事前チェック時点のバッテリー残量を保持する予定（A/B）",
    "PuduCloudからエリア別完了率は取得できるか（A/B/C）",
    "DB更新用アカウントの発行時期の目安（未定でも結構です）",
    "自由記入欄",
    "第7回の3件（未回答分）",
    "接続先（ホスト名またはIP/ポート）",
    "接続元IP制限の有無/SSL-TLSの要否/ステージングか本番か",
    "clean_timetables.precheckの値の範囲・反映時期・",
    "既存データの扱い・新旧混在期間の有無",
    "（A-2採番/A-3b桁数/B-2索引/B-3bDEFAULT）",
    "引き続きよろしくお願いします",
]

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_status_histories", "clean_statuses",
    "clean_status_histories.started_at", "clean_status_histories.ended_at",
    "clean_status_histories.clean_status",
    "clean_status_histories.clean_error",
    "clean_statuses.clean_error", "clean_statuses.started_at",
    "clean_timetables", "clean_timetable_id",
    "clean_timetables.start_at", "clean_timetables.precheck",
    "clean_task_reports",
    "robot_error_reports", "robot_error_reports.clean_timetable_id",
    "control_error_histories", "control_reports.reported_at",
    "security_status_histories.reported_at",
    "customer_summaries", "facility_summaries",
    "operation_ratio", "launch_ratio", "clean_ratio", "error_ratio",
    "task_count", "launch_count", "error_count",
    "task_area", "clean_area",
    "started_at", "ended_at", "start_at", "end_at",
    "errored_at", "responded_at", "resolved_at",
    "created_at", "updated_at", "parent_id",
    "robots.battery", "facility_id = 4",
    "DATETIME", "TIMESTAMP", "time_zone", "timezone",
    "unsigned tinyint", "double(4,1)",
    "new Date(値 + \"Z\")",
    "Pudu Cloud", "Pudu Cloud API", "Prisma", "MariaDB",
    "SSL-TLS", "UPDATE",
    "launch_count / task_count × 100",
    "status / comment / responded_at / resolved_at",
]
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. 設問の採番（10章25問） ==")
QNOS = (["Q1-1", "Q1-2"] + ["Q2-%d" % i for i in range(1, 5)] +
        ["Q3-1", "Q3-2"] + ["Q4-%d" % i for i in range(1, 5)] +
        ["Q5-1", "Q5-2"] + ["Q6-%d" % i for i in range(1, 7)] +
        ["Q7-1", "Q8-1", "Q9-1", "Q9-2", "Q10-1"])
print("設問数:", len(QNOS))
# 本文の設問見出し（「Q1-1. 」の形）と、巻末のご回答欄（単独の「Q1-1」）の両方
no_head = [q for q in QNOS if (q + ".") not in flat]
# ご回答欄は「Q1-1」+「未実施の清掃にも」のように後ろが「.」ではない
counts = {q: len(re.findall(re.escape(q) + r"(?![0-9\-])", flat)) for q in QNOS}
no_sheet = [q for q, n in counts.items() if n < 2]
print("本文に設問見出しが無いもの:", no_head if no_head else "なし")
print("出現が1回だけ（ご回答欄に無い疑い）:", no_sheet if no_sheet else "なし")

print("\n== 6. 豆腐（グリフ欠け） ==")
# 抽出テキストの全文字を BIZ UDGothic の cmap に引き当てる。
# 字が無い文字は PDF 上で □ になる。Σ / ÷ / ◎○△× / 〜 / ＝ 等の確認。
pdfmetrics.registerFont(TTFont("JP", "C:/Windows/Fonts/BIZ-UDGothicR.ttc",
                               subfontIndex=0))
pdfmetrics.registerFont(TTFont("JP-B", "C:/Windows/Fonts/BIZ-UDGothicB.ttc",
                               subfontIndex=0))
c2g_r = pdfmetrics.getFont("JP").face.charToGlyph
c2g_b = pdfmetrics.getFont("JP-B").face.charToGlyph
chars = sorted(set(text) - set("\n\r\t "))
missing_r = [c for c in chars if ord(c) not in c2g_r]
missing_b = [c for c in chars if ord(c) not in c2g_b]
print("本文に出る文字種:", len(chars))
print("BIZ UDGothic R に字が無い文字:", missing_r if missing_r else "なし")
print("BIZ UDGothic B に字が無い文字:", missing_b if missing_b else "なし")
tofu = [c for c in chars if c in "\ufffd□"]
print("置換文字・豆腐そのものの混入:", tofu if tofu else "なし")

print("\n== 7. 認証情報が載っていないこと ==")
# 参照用アカウント・更新用アカウントの認証情報は PDF に書かない（原稿にも無い）
SECRET = ["password", "PASSWORD", "Password", "IDENTIFIED BY", "passwd",
          "pwd=", "GRANT ", "@localhost", "mysql://", "jdbc:"]
leak = [s for s in SECRET if s in text]
print("認証情報らしき語:", leak if leak else "なし")
# ホスト名・IP アドレスらしきもの（第7回でまだ受領していないので出るはずがない）
ips = sorted(set(re.findall(r"\b(?:\d{1,3}\.){3}\d{1,3}\b", text)))
print("IP アドレスらしき文字列:", ips if ips else "なし")

print("\n== 8. 判定 ==")
ok = (not miss) and bool(biz) and (not notbiz) and (not hit) \
    and (not broken) and (not no_head) and (not no_sheet) \
    and (not missing_r) and (not missing_b) and (not tofu) \
    and (not leak) and (not ips)
print("結果:", "OK" if ok else "NG")
sys.exit(0 if ok else 1)
