# -*- coding: utf-8 -*-
"""第9回 PDF「確認事項（第9回）」の自己点検。

原稿 (docs/handoff/TeamWorks確認事項_第9回_20260915.md) の文言が
すべて PDF に含まれているか、フォントが日本語フォントで埋め込まれているかを見る。
PDF のパスは第1引数で渡せる。省略時はこのスクリプトと同じディレクトリの
DWatch_確認事項_第9回_20260915.pdf。終了コードは OK なら 0、NG なら 1。

★このスクリプトは生成スクリプト（make_teamworks_qa9_pdf.py）を import しない★
見るのは「PDF から抽出したテキスト」と、下に転記した原稿の文言だけ。
生成側の実装が変わっても、この点検の結果は変わらない。

check_teamworks_qa8_pdf.py（第8回用）の構成を踏襲。第9回で変えたところ:
  ★4. ALTER TABLE 7本★ — 本書の中心は索引のご依頼。SQL が7本そろっているかを
     索引名・列名まで見る（第8回には無かった検査）。
  ★5. 実測値★ — EXPLAIN の出力・行数・バージョン等、原稿に書いた数値が
     PDF に落ちているかを見る。数字は1文字欠けても意味が変わるため。
  ★7. 章と設問の採番★ — 第9回は★12章★（2026-09-16 の統合版。初版は11章）。
     ★設問は「Qn-m.」形式の見出しを数えて 26 問★
     （初版は22問。12章で6問増え、2026-09-17 に Q8-1 / Q8-2 を取り下げて2問減った）。
     ※原稿12章の見出しにある「確認6問」は12章の中だけの数。書面全体の設問数は別物。
     「本文に見出しがあること」だけを必須にし、出現回数は参考表示にとどめる。
     余計な設問番号が紛れていないか（Q\\d+-\\d+ の全出現）も見る。
  ★9. 空白ページ★ — フッターだけのページが無いこと。
  ★10. 認証情報★ — 第8回は「IP アドレスが1つも出ないこと」を検査していたが、
     第9回は実測環境のホスト 133.125.238.144 を本文に書いている。
     そこで「許可した1つ以外の IP が出ないこと」「mysql:// の認証情報が
     伏せ字であること」「password 等が出ないこと」に変えた。
     ★133.125.238.144 は12章に意図して載せてあるので出てよい。
       DBユーザー名は 2026-10-08 に原稿・PDF から除いた（12章は「参照用アカウント@%」）。
       .env.local から実行時に読み、出ていないことを照合する（値はこのスクリプトに
       書かない・出力にも出さない）。
       パスワードは絶対に出さない★

★2026-09-16 の改訂★
  原稿に12章「DB接続経路について — お願い3件・確認6問」が加わり、初版の末尾に
  あった「参考: テスト環境について（申し送り）」は12章に吸収されて消えた。
  そこで must から参考節の文言を外し、12章の文言を足している。
  ★検査そのものは1つも削っていない★（must / 実測値 / 識別子 / 章・設問の
  突き合わせはむしろ増えている）。変えたのは「何を期待するか」だけ。

★2026-09-17（5回目）の改訂★
  ★前回報告した本文側の2点を、監督が原稿で直した★。追随させたところ:
    3. must … 1-1 の「以下、すべて実測です」を★4行（実行したもの／行数の出所／
       見てほしいのは実行計画の形／おことわり参照）★に差し替え。
       7-2 に ★Q7-5 → Q7-4 の並びの断り書き（引用ブロック）★を追加。
    12. FORBIDDEN … ★旧「以下、すべて実測です。推測ではありません」を追加★。
       ★「実測」と書くときは数字の出所を併記する（当方の作業規約）★ので、
       出所を書かずに言い切る表現が復活しないようにする。
  ★検査は1つも削っていない★（must は差し替え＋9件増、FORBIDDEN は2件増）。

★2026-09-17（4回目）の改訂★
  ★末尾の1文を、対象を限定した形に直した★。
    3. must … 「いずれもご対応は不要です」→
       ★「上の「本改訂で取り下げた項目」については、ご対応は不要です」★。
    12. FORBIDDEN … ★旧文言「いずれもご対応は不要です」を追加★。
       この文は ★Q11-1（本書で唯一「ご回答をお願いします」と書いた設問）の
       段落の直後★に来るため、「いずれも」と書くと回答依頼を打ち消して読める。
  ★検査は1つも削っていない★（must は差し替え、FORBIDDEN は1件増）。

★2026-09-17（3回目）の改訂★
  ★★本文側が、11章の矛盾（「Q11-1 は取り下げます」と書きながら同じ節に
  生きた Q11-1 がある）を直した★★。期待値を追随させたところ:
    3. must … ヘッダの改訂点ブロック / 本書の構成 / 構成表の11章の行 /
       11-1 の見出し / 末尾のまとめ（★「取り下げた項目」と「取り下げではなく、
       お伺いする内容を改めた項目」の2段落に分かれた★）を差し替え。
    12. FORBIDDEN … ★「Q11-1 を取り下げた」という趣旨の旧文言を追加★
       （「Q11-1 は取り下げます」「Q11-1 は解決」「残り3問に縮小」
       「started_at を使う形で解決しましたので取り下げ」
       「初版より縮小しております」、および末尾の列挙に混ざった形）。
       ★同じ矛盾を二度出さないための検査★。
  ★設問数は25問のまま。Q11-1 は生きた設問として QNOS に残る
    （WITHDRAWN_Q には入れない）★。
  ★検査は1つも削っていない★。★12章は引き続き1文字も変わっていない★。

★2026-09-17（2回目）の改訂★
  ★テストDBの行が1行残らず当方（DFA）由来のダミーだと判明し、原稿が
  「行の中身」を根拠にした記述を全部取り下げた★。期待値を追随させたところ:
    3. must … ヘッダの改訂点ブロック（2つになった）・おことわり・本書の構成・
       構成表（2 / 5 / 6 / 10 / 11 章）・1章の引用3つ・2章（実測表と Q2-2 を削除）・
       5章（章ごと差し替え）・6-2 の「なぜ清掃実施回数が要るのか」・7章・8章・
       9章・10章（章ごと差し替え）・11章（作り直し＋Q11-4 新設）・末尾の
       取り下げまとめを差し替え。★件数は 質問25問 / お願い5件 / 参考情報1章★。
    5. 実測値 … ★ダミー由来で原稿から消えた値（0.93%（5行） / 2026-08-14 /
       1 のみ / controls は11行 / facilities は12行 / 2章の実データの数値 /
       11章の delay）を、原稿の新しい表現に置き換えた★。
       ★消した値は 12. の FORBIDDEN に移してあるので、検査は減っていない★。
    6. 識別子 … clean_task_reports.started_at / TIMESTAMPDIFF は原稿から消えた。
       ★IDENT から外したのではなく、12. の FORBIDDEN に移した★
       （docs/spec.md と同じ扱い）。controls は WATCH に移した
       （出るなら無傷であること。出なくてもよい）。
    7. 採番 … ★設問 26問 → 25問★。Q2-2 / Q5-1 を取り下げ（WITHDRAWN_Q へ）、
       ★Q11-4 を新設★。★番号は詰めないので Q2-3 / Q8-3 はそのまま★。
       章見出し・構成表は 2 / 5 / 10 / 11 章を差し替え。
    10. 認証情報 … ★10章に SHOW GRANTS の出力を載せたので、"GRANT " の検出を
       「原稿にある2行だけ許す」形に絞った（検査は外していない）★。
       ★IDENTIFIED BY / password 等は従来どおり1つでも出たら NG★。
    12. FORBIDDEN … ★取り下げた語をさらに足した★ —
       2章の実測表の文言 / Q2-2 / Q5-1 / 旧Q11-1 / 10章の依頼文。
  ★検査は1つも削っていない★。
  ★12章は原稿が1文字も変わっていないので、12章の期待値は触っていない★。

★2026-09-17（1回目）の改訂★
  原稿が「索引はご対応中（依頼を取り下げ）」「規模の前提はロボット30台未満」で
  書き換わった。期待値を追随させたところ:
    3. must … 1章（1-0 / 1-1 / 1-2 の置き直し / 1-6 の一覧）・2-4・8章・
       おわりに・文書ヘッダの改訂点ブロックを差し替え。
    5. 実測値 … 走査行数を 約193万行 / 44塊 / 43,860行 に、桁あふれを
       約9,100㎡ に差し替え。初版の数字（6,430万行 / 1,370㎡）は
       ★原稿に「初版では〜」として残っているので引き続き必須★。
    7. 採番 … ★設問 28問 → 26問★（Q8-1 / Q8-2 を取り下げ）。
       ★番号は詰めないので Q8-3 は Q8-3 のまま★。取り下げた2つは
       文書ヘッダの「旧 Q8-1 / Q8-2」という言及としてだけ出てよい
       （ALLOWED_MENTION_Q）。★設問見出し「Q8-1.」「Q8-2.」の形は禁止★。
       章見出し・構成表も1章と8章を差し替え。
  ★新設: 12. 取り下げた語が残っていないか★
    今回の改訂の主旨は「依頼に見えないこと」なので、そこを機械で守る。
    ・FORBIDDEN … 取り下げた見出し・文言がそのまま残っていないか
    ・WORD_ONLY_IN … 「最優先」「ご依頼」「施設200件」「1,370」のように
      ★語そのものは原稿に残っているが、出てよい文脈が決まっている★もの。
      許された文脈を抽出テキストから取り除いてから、語が残らないかを見る。
  ★検査は1つも削っていない★（must は 12. のぶんだけ増えている）。
  ★12章は原稿が1文字も変わっていないので、12章の期待値は触っていない★。

★must に「ページを跨ぐ語」を入れないこと★
抽出テキストにフッター（D-Watch / DFA Robotics とページ番号）が挟まり、
空白を詰めても一致しない。第3回で bigint auto_increment が実際に引っかかった。
このため must の1項目は1文までにとどめ、長い段落は分けて入れている。
第9回では保険として、フッターを落としたテキストでも照合している
（strip_footer / has）。ただしフッターの文言が変われば効かないので、
上の決まりごとは守ること。

★6 の検査があっても、表の列幅崩れの目視確認は省かないこと★
6 は「識別子が行の途中で割れていないか」を抽出テキストで見るだけで、
枠のはみ出し・改ページ位置・重なりは見られない（pdf/README.md）。

必要なもの: pypdf。reportlab は 8.（豆腐の検査）にだけ使い、
無ければその項目を飛ばす（他の検査は走る）。
"""
import os
import re
import sys

try:
    sys.stdout.reconfigure(errors="backslashreplace")  # cp932 で落ちないように
except Exception:
    pass

try:
    from pypdf import PdfReader
except ImportError:
    print("★NG★ pypdf が入っていない。`pip install pypdf` が必要。")
    sys.exit(1)

PDF = (sys.argv[1] if len(sys.argv) > 1 else
       os.path.join(os.path.dirname(os.path.abspath(__file__)),
                    "DWatch_確認事項_第9回_20260915.pdf"))
if not os.path.exists(PDF):
    print("★NG★ PDF が見つからない: %s" % PDF)
    print("先に make_teamworks_qa9_pdf.py を実行するか、"
          "PDF のパスを第1引数で渡すこと。")
    sys.exit(1)

raw = open(PDF, "rb").read()
reader = PdfReader(PDF)
pages = [p.extract_text() or "" for p in reader.pages]
text = "\n".join(pages)


def sq(s):
    """空白（全角・NBSP・ゼロ幅を含む）とバッククォートを落とす。

    バッククォートを落とすのは、原稿の `識別子` が PDF 側でそのまま
    バッククォート付きで出ても、外れて出ても、同じように照合するため。
    """
    return re.sub(r"[\s\u00a0\u3000\u200b\ufeff]+", "", s).replace("`", "")


flat = sq(text)
text_nb = text.replace("`", "")   # 改行は残したまま（語中改行の検査に使う）


def strip_footer(p):
    """ページのフッター（D-Watch / DFA Robotics + ページ番号）を落とす。

    抽出テキストではフッターが各ページの先頭に入る。落としておかないと、
    ページを跨ぐ文がフッターで分断されて一致しなくなる
    （第3回で bigint auto_increment がこれで引っかかった）。
    フッターの文言が変わった場合は何も落とさない。そのときは
    従来どおり「ページを跨ぐ語を must に入れない」運用で守る。
    """
    p = re.sub(r"D-Watch\s*/\s*DFA\s*Robotics\s*\d*\s*", "", p)
    return p


flat_nofoot = sq("".join(strip_footer(p) for p in pages))


def has(s):
    """原稿の文言が PDF にあるか。フッターを挟んだ場合も拾う。"""
    t = sq(s)
    return t in flat or t in flat_nofoot

print("== 1. ページ数とサイズ ==")
print("ページ数:", len(reader.pages), " サイズ:", len(raw), "bytes",
      "(%.1f KB)" % (len(raw) / 1024))
print("抽出文字数:", len(text), " 空白を詰めた文字数:", len(flat))
empty_extract = len(flat) < 2000
if empty_extract:
    print("  ★NG★ 抽出できた文字が少なすぎる。テキストが図形化されている疑い")

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 を含むフォント名:", [b.decode() for b in biz])
if not biz:
    print("  ★NG★ BIZ UDGothic が埋め込まれていない")
# 埋め込まれているフォントが BIZ UDGothic だけであること。
# サブセット接頭辞（AAAAAA+）が付いているものが埋め込みフォント。
# 接頭辞の無いものは reportlab が必ず1つ書き出す標準14フォント（Helvetica）で、
# フォントファイルを持たず本文にも使われていない。第7回・第8回の 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 の数が合わない")
# 太字・細字の2本がそろっているか（参考表示）
bold = [f for f in embedded if "Bold" in f]
print("うち Bold:", bold or "なし", " Regular:",
      [f for f in embedded if "Bold" not in f] or "なし")
# 中国語向けフォントが混ざっていないこと（CLAUDE.md §0-3）
CN = [b"SimSun", b"NSimSun", b"SimHei", b"MicrosoftYaHei", b"MSYaHei",
      b"MSGothic", b"NotoSansSC", b"NotoSerifSC", b"NotoSansCJKsc",
      b"SourceHanSansSC", b"SourceHanSerifSC", b"FangSong", b"KaiTi",
      b"STSong", b"STHeiti", b"STKaiti", b"STFangsong", b"PingFang",
      b"DengXian", b"MingLiU", b"PMingLiU", b"DFKai", b"YouYuan", b"LiSu"]
cn_hit = [n.decode() for n in CN if n in raw]
print("中国語向けフォント:", cn_hit if cn_hit else "混入なし")
if cn_hit:
    print("  ★NG★ 中国語向けフォントが混ざっている")

print("\n== 3. 原稿の主要な文言が含まれるか ==")
must = [
    # ---- 表題・はじめに ----
    "確認事項（第9回）", "TeamWorks",
    "第8回（2026-09-11）にはご回答をいただき、ありがとうございました",
    "いただいた回答をもとに、タイムゾーン・エラー率・エリア別完了率の実装を直しております",
    "第8回の Q4（サマリの率・分子・分母の関係）は「多いので後で回答」との留保をいただいた",
    "取り下げておりませんので、引き続きお願いいたします（本書の3章に再掲します）",
    "テスト環境のDBに接続して実測を行ったところ、確認事項が新たに発生しました",
    # 2026-09-17（2回目）の改訂点（文書ヘッダの引用ブロック。2つある）
    "2026-09-17（2回目）の改訂点",
    "テスト環境のDBに入っているデータは、当方（DFA）が入れた"
    "検証用のダミーデータでありました",
    "つきましては「行の中身」を根拠にしていた記述を、取り下げるか、"
    "当方の検証用データでの観測である旨を明記する形に改めております",
    "2章は実データの話を削り、仕様の確認のみに縮めました（Q2-2 は取り下げ）",
    "5章（面積の単位）は弊社で「㎡」と確定しましたので、"
    "設問（Q5-1）を取り下げました",
    "8章 Q8-3 は「固定値」ではなく「運用上の下限・上限・変更の頻度」を"
    "伺う形に改めました",
    "10章（DB更新用アカウント）はすでに権限が発行されておりました。"
    "当方の確認漏れです。ご依頼を取り下げました",
    # ★Q11-1 は「取り下げ」ではなく「お伺いする内容を改めた」。
    #   旧文言の残存は 12. の FORBIDDEN が見る★
    "11章 Q11-1（実際の清掃開始時刻）は お伺いする内容を改めました",
    "「どこにありますか」ではなく",
    "「started_at を使って実装してよろしいでしょうか」",
    "という確認に変えております。あわせて Q11-4 を新たに立てました",
    # 2026-09-17（1回目）の改訂点
    "2026-09-17（1回目）の改訂点",
    "索引（1章）はご対応中と伺いましたので、ご依頼を取り下げ参考情報に改めました",
    "規模の前提を「施設200件（仮置き）」から「ロボット30台未満（弊社把握）」に"
    "置き直しました（1章・2-4）",
    "台数・施設数の設問（旧 Q8-1 / Q8-2）を取り下げました"
    "（弊社側で決めるべきことのため）",
    # はじめに: おことわり（2026-09-17 の2回目の改訂で新設）
    "おことわり — テスト環境のデータは当方が入れたものでした",
    "本書の初版（2026-09-15）では、テスト環境のDBの「行の中身」を"
    "根拠にした記述がございました",
    "その後、当方で確認いたしましたところ、テスト環境に入っているデータは"
    "当方（DFA）が開発用に投入した検証用のダミーデータでした",
    "そのため、行数・値の分布・特定のテーブルが0行であること等を根拠にした指摘は、"
    "貴社のバッチや収集処理の挙動を示すものではありませんでした",
    "本改訂では、これらを次のように仕分けております",
    # おことわりの表（セルごと）
    "構造から分かること",
    "DDL / SHOW CREATE TABLE / 索引の有無 / possible_keys = null / "
    "カラムの有無・型 / サーバー変数 / 権限",
    "そのまま根拠として記載しております",
    "行の中身から", "読み取っていたこと",
    "行数・値の分布・「全件が同じ値」「0行である」等",
    "取り下げるか、「当方の検証用データでの観測です」と明記しております",
    "紛らわしいことを申し上げ、失礼いたしました",
    # はじめに: 本書の構成
    "本書の構成",
    "索引の件については、ご対応中と伺いました",
    "つきましては1章はご依頼を取り下げ、参考情報に改めております"
    "（詳細は1章の冒頭に記しました）",
    "そのうえで本書の中心は、12章（DB接続経路）です",
    "12章は期限が「ステージングに実データが入る前」という性質のもので、"
    "過ぎますと作り直しになります",
    "あわせて、3章・4章・6章（サマリの計算式と分子・分母）が"
    "当方の実装に直結いたします",
    "11章は、Q11-1（実際の清掃開始時刻）について、お伺いする内容を"
    "「どこにありますか」から",
    "「この理解で実装してよろしいでしょうか」",
    "に改めております",
    "本書は次の12部構成です",
    "質問25問 / お願い5件 / 参考情報1章",
    # 冒頭の構成表（セルごとに分けて入れる。行をまたぐと一致しないため）
    "内容", "種別", "優先度",
    "ご対応中と伺いました。当方の EXPLAIN 実測と、当方が見ていた列の一覧です",
    "参考情報", "お願い", "ご報告",
    "仕様の確認のみに縮めました", "質問", "高", "再掲",
    "当期と確定期間で定義が食い違う恐れ",
    "弊社で㎡と確定しました。設問は取り下げです", "中",
    "group_summaries が存在しません",
    "画面に分類を出すために必要です",
    "control_reports の行数を決めます。台数は当方で把握しました",
    "DDLは4回目のお願いです", "低",
    "発行済みでした。ご依頼を取り下げます",
    "Q11-1 は「実装の確認」に改めました。Q11-4 を新設（4問）",
    "7章は貴社の担当領域（DBとバッチ）への変更を伴いますので、"
    "「お願い」として書いております",
    "ご負担や副作用のご懸念があれば、その旨をお知らせください",
    "当方で別の手を検討します",
    # ---- 1章 索引について（2026-09-17 に依頼を取り下げ、参考情報に） ----
    "1. 索引について（ご対応中と伺いました。参考情報です）",
    "1-0. 本章の位置づけ — ご依頼は取り下げております",
    "索引についてはご対応中と伺いました。つきましては本章は依頼ではなく参考情報です",
    "ご対応が完了した時点で、残っているものがあれば改めてご相談します",
    "本章に残しておりますのは、当方が EXPLAIN で実測した結果と、"
    "当方が見ていた列の一覧です",
    "貴社が入れられた索引で足りているかどうかを、そちらで突き合わせて"
    "いただけるように残しております",
    "この一覧のとおりにしていただきたい、という意味ではありません",
    "初版（2026-09-15）では本章を「最優先のご依頼（7件）」として"
    "書いておりました",
    "本改訂（2026-09-17）で取り下げております。"
    "行き違いがございましたら申し訳ございません",
    "1-1. なぜこの列を見ていたか",
    "当方は清掃詳細ログの表示期間を",
    "それまでの上限31日から、上限2年（731日）に広げました",
    "そのために EXPLAIN で実行計画を確認したところ",
    "索引が無いために全表走査になっている",
    "以下の①〜⑦は、そのとき当方が見ていた列です",
    "現状（データが少ないうち）は問題が表に出ません",
    "問題になるのは、本番のデータが積み上がってからです",
    # ★「実測」と書くときは数字の出所を併記する（当方の作業規約）。
    #   旧「以下、すべて実測です。推測ではありません」は 12. の FORBIDDEN★
    "以下の SHOW CREATE TABLE と EXPLAIN は、",
    "テスト環境で実際に実行したものです。推測ではありません",
    "ただし、そこに出てくる行数（rows / r_rows）は"
    "当方が入れた検証用データの件数です",
    "申し上げたいのは type / possible_keys / key という"
    "実行計画の形のほうで、行数ではございません",
    "詳しくは冒頭の「おことわり」をご覧ください",
    "なお ALTER TABLE の形で書いておりますのは、"
    "列と順序を誤解なくお伝えするためです",
    "当方でこれを実行することはございません"
    "（当方はメインDBのスキーマを変更しない方針です）",
    "1-2. ①② — 索引が1本も無い列",
    "根拠（EXPLAIN の出力そのまま）",
    # 行数の読み方について（2026-09-17 の2回目の改訂で新設した引用ブロック）
    "行数の読み方について",
    "以下の rows / r_rows は、",
    "当方が入れた検証用データでの行数",
    "です（「おことわり」参照）",
    "本番の行数を示すものではありません",
    "一方、type / possible_keys / key は",
    "表の構造（索引の有無）から決まる",
    "もので、",
    "行数によって変わりません",
    "本章で申し上げたいのは後者です",
    "clean_status_histories を clean_timetable_id IN (5件) で引いたとき",
    "robot_error_reports を同様に引いたとき",
    "possible_keys = null ＝ 使える索引が1本もありません",
    "表の全行を読んで、そのうち一致した行だけを残しています",
    "（上の 538行・0.93% は",
    "当方の検証用データでの値",
    "これは統計に基づくコスト判断ではなく、",
    "構造的な全表走査",
    "行数が増えても",
    "索引を選ぶようにはなりません",
    "SHOW CREATE TABLE での確認",
    "clean_timetable_id には索引も FK もありません",
    "clean_timetable_id には索引がありません",
    "影響の大きさ（2026-09-17 に前提を置き直しました）",
    "当方のアプリは、この2つのテーブルを",
    "clean_timetable_id IN (…) の形でしか引きません",
    "IN の要素数が1000を超えると1000件ずつの塊に分けて投げますが",
    "塊ごとに全表走査が起きます",
    "初版ではこの走査行数を「施設200件」という当方の仮置きで外挿しておりました",
    "その後、弊社で現状のロボット台数を確認したところ",
    "30台未満",
    "でしたので、置き直しております",
    "台数の根拠は、弊社（DFA）自身の把握「現状30台未満」（2026-09-17）です",
    "貴社への質問ではございません",
    # 計算過程（コード枠。行ごとに分けて入れる）
    # ★2026-09-17（2回目）の改訂で「実測」→「当方の置き値」に変わった★
    "計算過程",
    "下の「1台1日2回」は、貴社のデータから得た値ではありません",
    "当方が入れた検証用データの密度（31日 / 12エリア / 538行 ＝ "
    "1.45 回/日/エリア）を切り上げた、",
    "当方の置き値",
    "実運用の清掃回数が決まりましたら、当方から置き直してお知らせいたします",
    "1) 1ロボット（＝1清掃エリア）あたりの1日の清掃予定回数",
    "当方の置き値（当方の検証用データの密度 1.45 回/日/エリア を"
    "切り上げたもの）",
    "→ 2 回/日/台 として計算する",
    "2) 2年ぶんの行数（当方の画面の期間上限は 731日です）",
    "30台 × 2回/日 × 731日 = 43,860行",
    "clean_timetables（precheck = 0）の行数",
    "clean_status_histories も同数とみなす",
    "（当方の検証用データでは 1対1 でしたが、"
    "本来の仕様は 2章 Q2-3 でお伺いしております）",
    "3) IN の塊の数",
    "43,860 ÷ 1,000",
    "44塊",
    "4) 走査行数（1塊につき全表走査1回）",
    "44塊 × 43,860行 = 約 193万行",
    # 走査行数の表
    "表示期間", "塊の数", "今日1日",
    "ロボット30台で外挿", "2年（731日）",
    "初版（施設200件での外挿）では「2年で約6,430万行」としておりました",
    "約 1/33 になります",
    # 数字が小さくなったことについて
    "数字が小さくなったことについて、事実だけ申し添えます",
    "「だから問題ない」と断定するつもりはございません",
    "分かっていることは次の3点です",
    "全表走査は行数によらず構造的に起きます",
    "統計に基づくコスト判断ではないためです",
    "清掃詳細ログは、この全表走査を",
    "2つの経路（本体と連続日数の算出）で別々に撃ちます",
    "2年の表示1回あたり、合計で 約386万行の走査になります",
    "上の 43,860行 は「1台1日2回」への切り上げです",
    "塊の数と1塊あたりの行数が両方とも台数・回数に比例するため、"
    "走査行数はそれらに対して二乗で効きます",
    "台数が倍になれば走査行数は4倍になります",
    "1-3. ③④ — 複合索引（clean_timetables）",
    "当方の主クエリは",
    "常に precheck = 0 AND start_at >= ? AND start_at < ? の AND",
    "start_at 単独の索引はありますが、実行計画では使われていません",
    "2年範囲の主クエリ",
    "施設で絞った場合も同様です",
    "2年範囲 + facility_id IN (1,2,3)",
    "単一列の索引が2本とも候補に挙がりながら、どちらも使われていません",
    "これが複合索引を必要とする形そのものです",
    # ③④ はコスト判断（2026-09-17 の2回目の改訂で新設した引用ブロック）
    "ただし、ここは①②とは性質が異なります",
    "③④は possible_keys に索引が挙がっておりますので、",
    "使うかどうかはコスト判断",
    "上の実行計画は当方の検証用データ（少量）で測ったものですので、"
    "行数が増えれば選択が変わる可能性があります",
    "そのため③④は、①②ほど強くは申し上げられません",
    "2本を1本にまとめない理由",
    "(facility_id, precheck, start_at) の1本に寄せる案もありえますが",
    "施設で絞らない呼び出し（画面の既定）が主経路",
    "precheck = 0 AND start_at が引けません",
    "そのため当方は2本別々のものとして見ておりました",
    "なお clean_timetables の既存索引は6本すべて単一列で、",
    "2026-09-13 時点では複合索引は1本もありませんでした",
    "1-4. ⑤ — 期間での集計（面積の累計表示に関係します）",
    "当方は",
    "清掃総面積を期間で絞って集計する画面",
    "を追加しております（本書2章と関連）",
    "その集計は clean_status_histories を期間で絞って SUM() を取る形になりますが",
    "started_at に索引が無いため、期間で絞っても全行を走査します",
    # 1-4 の注記（2026-09-17 の2回目の改訂で新設）
    "（rows は当方の検証用データでの行数です。",
    "申し上げたいのは「started_at に索引が無い」という構造のほう",
    "で、こちらは SHOW CREATE TABLE で確認しております）",
    "ここでも規模の前提を置き直しております",
    "当方の社内資料は 「1日約360行・年間約13万行・保持2年」 を想定して"
    "おりましたが、",
    "これは施設200件規模に相当する数字でした",
    "ロボット30台 × 2回/日 × 365日 = 約 2.2万行/年（2年で 約4.4万行）",
    "規模は当初想定の 1/6 ほどになりますが、"
    "「期間で絞っても全行を読む」という形そのものは行数によらず変わりません",
    "1-5. ⑥⑦ — D-Security 制御盤（第8回以降に判明）",
    "⑥の根拠 — 同じテーブル・同じ索引で、列を変えただけで実行計画が切り替わりました",
    "制御盤ごとの「最終通信時刻」を取るため MAX(reported_at) を使っています",
    "MAX() の対象", "索引に含まれるか",
    "reported_at（現行）", "含まれない",
    "id（参考の実験）", "含まれる",
    # ⑥の表の注記（2026-09-17 の2回目の改訂で新設）
    "（rows は当方の検証用データでの行数です。",
    "ご覧いただきたいのは type と Extra が切り替わった点",
    "InnoDB の二次索引には主キーが暗黙に付く",
    "物理的に (control_id, id) の複合索引として働いています",
    "2列目が reported_at になれば、",
    "MAX(reported_at) にも同じ効果が及びます",
    "control_reports は心拍の報告が積み上がるテーブルで、"
    "本番では最も行数が多くなる見込みです",
    "8章の報告間隔次第で1,100万行〜7,000万行。"
    "制御盤11台という当方の置き値での試算です",
    "ここが全表走査のままですと、",
    "D-Sログ画面が開けなくなります",
    "清掃側（①〜⑤）とは事情が異なります",
    "清掃側の行数は台数と清掃回数で決まりますが、control_reports は",
    "時間あたりの報告間隔で決まる",
    "ため、台数が少なくても行数は積み上がります",
    "そのため上の「ロボット30台」の置き直しは、⑥⑦には効きません",
    "⑦の根拠",
    "D-Secアラートの絞り込みで occurred_at の範囲指定を使いますが、索引がありません",
    # 1-6 ①〜⑦の一覧（2026-09-17 に新設）
    "1-6. ①〜⑦の一覧（突き合わせ用）",
    "貴社のご対応内容と突き合わせていただくための一覧です",
    "テーブル", "当方が見ていた列", "（順序も含みます）",
    "2026-09-13 時点の実測",
    "索引なし（possible_keys = null）",
    "(precheck, start_at)", "(facility_id, start_at)",
    "(control_id, reported_at)",
    "単一列索引はあるが実行計画で未使用", "同上",
    "索引なし",
    "(control_id) のみ。", "MAX(reported_at) がフルスキャン",
    "申し添え",
    "①〜⑦のテーブルは、いずれも",
    "貴社のデータ収集処理が書き込むテーブル",
    "索引を足すぶん、書き込みがわずかに遅くなります",
    "その兼ね合いのご判断は貴社にお任せいたします",
    "当方の見立てでは ①② が突出しています",
    "③〜⑦は、①②の後で差し支えありません",
    "当方はメインDBのスキーマを変更しない方針です",
    "上記を当方で実行することはございません",
    "ご対応が完了しましたら、その旨をお知らせいただけますと、"
    "当方で EXPLAIN を測り直し、残りがあるかどうかを確認いたします",
    # ---- 2章 サマリの面積（2026-09-17 の2回目の改訂で実測表と Q2-2 を削除） ----
    "サマリの面積は、期間内の合計でしょうか",
    "2-1. 本章は仕様の確認のみに縮めております",
    "初版では、テスト環境の facility_summaries / customer_summaries の値を"
    "並べて「合計に見えません」と申し上げておりました",
    "この値は当方が入れた検証用のダミーデータでしたので、"
    "当該の指摘は取り下げます",
    "お騒がせいたしました",
    "残しますのは、実装のために必要な仕様の確認だけです",
    "「清掃総面積の累計」を画面に表示する機能",
    "を作っており、その数字をサマリから取るのか、"
    "clean_status_histories から毎回その場集計するのかを決める必要があります",
    "（後者を選ぶ場合に効くのが、1章の⑤の列です）",
    "2-2. お伺いしたいこと（2問）",
    "集計バッチは、task_area / clean_area に 期間内の合計 を"
    "書き込む仕様でしょうか",
    "選択肢",
    "期間内の合計を書く",
    "（当方はサマリをそのまま足し合わせて総面積を出します）",
    "合計ではない",
    "（1回ぶんの代表値・平均など。その場合、定義をお教えください）",
    "B の場合、当方は clean_status_histories から毎回その場集計する形に"
    "いたします",
    "「サマリに合計を入れるほうが筋がよい」というご判断であれば、"
    "そちらに合わせます",
    "clean_status_histories は何行できますでしょうか",
    "面積の合計をSQLの SUM() で取るか、行を1本に畳んでから足すかが、これで変わります",
    "必ず1行",
    "（清掃1回につき1行）",
    "複数行できる",
    "（清掃前 → 清掃中 → 清掃終了 のように状態ごとに積む）",
    "「清掃状態履歴」というテーブル名からは、B の設計ではないかとも思われます",
    "一方で clean_status_histories にはカラムとしての状態"
    "（clean_status_trans 相当）が無く、この点は A のようにも読めます",
    "どちらとも決めかねましたので、お教えください",
    "B の場合、SUM() で足すと同じ清掃の面積を何度も数えてしまいます",
    "そして、それは例外もエラーも出さずに、ただ数字が大きくなるだけです",
    "当方は安全側（B の前提で、最新の1行に畳んでから足す）で実装いたしますが、",
    "A と確定できるのであれば、そのほうが速くなります",
    # Q2-2 の取り下げの断り書き（2026-09-17 の2回目の改訂で新設）
    "設問番号について",
    "初版の Q2-2（「合計でない場合の正しい経路をお教えください」）は"
    "取り下げました",
    "（Q2-1 の B に当方の対応を書きましたので、"
    "独立の設問にする必要がなくなったためです）",
    "番号は詰めずに Q2-3 のままにしております",
    "参考: 年次サマリの桁あふれについて（優先度は低いです）",
    "ご依頼でも質問でもなく、気づいた点のご報告です",
    "もしサマリに期間内の合計を入れる場合（Q2-1 が A の場合）、"
    "年次の行が上限に当たる可能性があります",
    "前提", "上限に達する条件",
    "ロボット30台・年365日",
    "1台1日あたり 約9,100㎡ を超えると、年次行があふれます",
    "1億㎡ ÷ 30台 ÷ 365日 ≒ 9,132 ㎡/台/日",
    "これは当方の計算であり、実測ではありません",
    "（当方の手元には実際の店舗面積の値がございませんので、"
    "9,100㎡ が現実的な値かどうかは判断できておりません）",
    "初版では「施設200件」を前提に 約1,370㎡ としておりましたが、"
    "弊社で現状のロボット台数を確認したところ30台未満でしたので、"
    "置き直しております（1章と同じ前提です）",
    "桁あふれの可能性はそのぶん遠のきました",
    "週次・月次では問題になりません。P1 で当方が読むのも週次・月次のみです",
    "年次サマリを作られる際にご一考いただければ、という程度のものです",
    # ---- 3章 第8回 Q4 の残り ----
    "第8回 Q4 の残り（サマリの率・分子・分母の関係）",
    "第8回で「多いので後で回答」との留保をいただいた件です",
    "取り下げておりません",
    "お伺いしたい内容は第8回の4章と同じです。要点だけ再掲します",
    "サマリに入る各率（clean_ratio 等）の",
    "計算式",
    "分子・分母に対応するカラム名",
    "件数列（task_count 等）が",
    "何を数えているか",
    "（予定の数か、実施の数か）",
    "2章の面積の件と同じ枠の話ですので、あわせてご回答いただけますと助かります",
    # ---- 4章 error_count ----
    "サマリの error_count の集計範囲",
    "第8回 A7 でいただいたご回答が、",
    "「バッチが実際にそうしている」のか「当方への助言」なのか",
    "を読み取れませんでした",
    "当期（進行中の週・月）は当方が自前で集計するため、",
    "定義が食い違うと、確定期間と当期でグラフが段差になります",
    "お伺いしたいこと（3問）",
    "事前チェック（precheck = 30 / 60）を除外して集計されていますでしょうか",
    "除外している",
    "（本清掃 precheck = 0 のみを数えている）",
    "除外していない",
    "（事前チェックのエラーも数えている）",
    "clean_timetable_id が null のエラー行は、サマリの error_count に"
    "含まれますでしょうか",
    "clean_error が 2（セキュリティ有効）/ 3（D-S制御盤異常）の清掃は、"
    "サマリの error_count に含まれますでしょうか",
    "第8回 A6-1 で「2 / 3 では robot_error_reports が作られない」との"
    "ご回答をいただいております",
    "その場合、サマリ側でも数えていない、という理解でよろしいでしょうか",
    # ---- 5章 面積の単位（2026-09-17 の2回目の改訂で章ごと差し替え） ----
    "5. 面積の単位（設問を取り下げます）",
    "初版では「task_area / clean_area の単位は平方メートル（㎡）でしょうか」と"
    "お伺いしておりました（Q5-1）",
    "本件は弊社（DFA）で「㎡」と確定いたしましたので、設問を取り下げます",
    "画面には ㎡ として表示いたします",
    "貴社にご対応いただくことはございません",
    "万一「㎡ではない」という事情がございましたら、その場合のみお知らせください",
    # ---- 6章 グループ単位のサマリ ----
    "グループ単位のサマリ",
    "group_summaries に相当するテーブルが見当たりません",
    "サマリ系のテーブルは次の4本と認識しております",
    "（customer_id を参照）", "（facility_id を参照）",
    "グループ（groups）単位のサマリがありません",
    "ホールディングス単位のKPIを画面に出す",
    "予定です（本書7章と関連）",
    "グループ単位の集計は、どちらの想定でしょうか",
    "当方が customer_summaries の複数行を合算する",
    "（テーブル追加は不要）",
    "group_summaries を追加してバッチで集計する",
    "グループ単位の集計は想定していない",
    "A の場合、率は「率の平均」ではなく「Σ分子 ÷ Σ分母」で再計算いたします",
    "分子・分母がサマリに入っている前提ですので、3章のご回答と関係します",
    "ところが、4つの率のうち2つは分子・分母が揃っておりません",
    "本節は、テーブルのカラム構成（DDL）と当方の計算式を突き合わせたもので、"
    "行の中身には依存しておりません",
    "サマリのカラムを当方の計算式（要件定義）と突き合わせたところ、次のようになりました",
    "率", "計算式（当方の定義）", "サマリの分子", "サマリの分母", "合算できるか",
    "清掃完了率 clean_ratio", "Σ清掃面積 ÷ Σ対象面積", "できる",
    "出発成功率 launch_ratio", "定刻出発回数 ÷ 予定回数", "式が未確認",
    "稼働率 operation_ratio", "該当カラムが無い", "できない",
    "エラー発生率 error_ratio", "エラー発生回数 ÷ 清掃",
    "「清掃実施回数」に当たるカラムが見当たりませんでした",
    # なぜ「清掃実施回数」が要るのか（2026-09-17 の2回目の改訂で新設）
    "なぜ「清掃実施回数」が要るのか（Q6-2 の理由）",
    "当方の4つの率のうち、2つは「清掃実施回数」を必要とします",
    "稼働率       = 清掃実施回数   ÷ 清掃予定回数 × 100",
    "エラー発生率 = エラー発生回数 ÷ 清掃実施回数 × 100",
    "つまり「実施回数」は、一方の率では分母、もう一方の率でも分母に当たります",
    "そして task_count は、予定回数か実施回数かのどちらか一方にしかなりません",
    "task_count が何であれば", "困ること",
    "予定回数", "（当方の理解）",
    "実施回数が無い",
    "稼働率の分子・エラー発生率の分母が出せません",
    "実施回数",
    "予定回数が無い", "稼働率の分母が出せません",
    "いずれにせよ、現在のカラム構成では2つの率のうち"
    "少なくとも片方が計算できません",
    "（task_count が実際にどちらなのかは、3章でお伺いしております）",
    "これは合算のときだけの話ではありません",
    "確定した期間（前週・前月）の稼働率・エラー発生率そのものが、"
    "サマリからは出せません",
    "進行中の期間は当方が詳細テーブルからその場集計しますので出せますが、"
    "確定期間はサマリを読む設計ですので、そこで数字が欠けます",
    "複数顧客を合算する場合は、さらに事情が重くなります",
    "率の平均を取るのは誤りで、分子と分母をそれぞれ足してから割る"
    "必要がありますが、この2つはその材料がありません",
    "サマリに「清掃実施回数」のカラムを追加していただくことは可能でしょうか",
    "追加できる",
    "（customer_summaries / facility_summaries に1カラムずつ）",
    "すでに既存のカラムで表現されている",
    "（どれか教えてください）",
    "追加は難しい",
    "C の場合、当方は確定期間（前週・前月）の稼働率・エラー発生率を"
    "画面に出さず、清掃完了率と総清掃面積だけを出す形にいたします",
    "進行中の期間だけ2つの率が出て、確定期間では消える、という見え方になります",
    "初版では「効いてくるのは2社目から」と書いておりましたが、"
    "これは合算の話に限った見立てでした",
    "上のとおり、1社であっても確定期間の2つの率が出せませんので、訂正いたします",
    "お急ぎいただく必要はございませんが、設計を決めたいので"
    "方針だけ伺えますと助かります",
    # ---- 7章 マスタのデータ投入 ----
    "グループ・顧客マスタのデータ投入",
    "7-1. 背景",
    "「ホールディングス → グループ（ブランド）→ 店舗」の3階層の絞り込み",
    "テーブルの構造（groups → customers.group_id → facilities.customer_id）は",
    "すでに3階層になっており、当方から追加をお願いする必要はございません",
    # ★2026-09-17（2回目）の改訂でコード枠が本文になった★
    "ただし、当方の手元には本番の中身がございません",
    "テスト環境に入っておりますのは",
    "当方が入れた検証用のダミー",
    "（groups 1行 / customers 1行 / facilities 12行・"
    "全行が同じ customer_id）だけです",
    "そのため当方では、3階層の絞り込みが実データでどう見えるかを"
    "確かめられておりません",
    "7-2. お願いと、確認したいこと",
    "お願い: 本番の groups / customers のデータ投入をお願いいたします",
    "時期の目安だけでも結構です",
    "当方はそれまで、検証用の複数グループ・複数顧客のデータを自前で用意して進めます",
    "customers は、法人と屋号のどちらの粒度で入る想定でしょうか",
    "当方は「屋号（ブランド）」の粒度で画面を作る方針です",
    "例", "屋号",
    "ドン・キホーテ / MEGAドン・キホーテ / アピタ / ピアゴ",
    "法人",
    "株式会社ドン・キホーテ / 株式会社ユニー",
    "A と B では、同じ法人の中で屋号を分けられるかどうかが変わります",
    "貴社の収集処理がどちらの粒度で customers を作る想定かをお教えください",
    "屋号を持たない顧客は、どう表現されますでしょうか",
    "当方は「屋号が無い顧客もありうる」という前提で画面を作ります",
    "customers.group_id は",
    "0 がセンチネル（グループなし）",
    "と認識しておりますが、",
    "屋号そのものが無い場合に customer_name に何が入るか",
    "をお教えください",
    "facilities.shop_id（varchar(20)、既定 '0'）は何を表す列でしょうか",
    "テーブル定義書からは読み取れず、",
    "テスト環境の値も当方が入れたダミー（全行 \"0\"）でしたので、"
    "値から推し量ることもできませんでした",
    "顧客側の店舗コードでしょうか。分類の手がかりに使えるかを知りたく存じます",
    # Q7-5 → Q7-4 の並びの断り書き（2026-09-17 の改訂で新設した引用ブロック）
    "設問の並びについて",
    "本節は Q7-5 → Q7-4 の順に並んでおります",
    "Q7-4（group_id = 0 が投入できない件）が Q7-5 のご回答を"
    "前提とする内容のため、この順にしております",
    "当方の管理表がこの番号で追っておりますので、番号は振り直しておりません",
    "「屋号を持たない顧客」は、3階層のどの段が空くという意味でしょうか",
    "当方の中でも解釈が割れましたので、貴社のDBの想定を伺わせてください",
    "当方が作ろうとしている階層はこの3段です",
    "「屋号を持たない顧客がありうる」と伺っておりますが、",
    "どの段が空くのかで設計が変わります",
    "解釈", "空くのは", "DBでの表現",
    "真ん中（屋号）",
    "店舗がホールディングスの直下にぶら下がる",
    "表現できません",
    "facilities.customer_id は NOT NULL で customers を指すため、中間を飛ばせません",
    "上（ホールディングス）",
    "屋号はあるが、属する持株会社が無い",
    "customers.group_id = 0（＝ Q7-4 の件）",
    "② であれば、Q7-4 が解決すればそのまま作れます",
    "① の場合、customers にダミーの行を1件作って、そこに店舗をぶら下げることになります",
    "その場合、customer_name に何を入れるか（空文字は避けたく存じます）を"
    "お決めいただく必要があります",
    "貴社の収集処理が customers をどう作る想定かをお教えいただければ、"
    "当方はそれに合わせます",
    "customers.group_id = 0（グループなし）は、現状のDDLでは投入できないように見えます",
    "これは質問というより、実DDLの矛盾のご報告です",
    "「NOT NULL / 既定値 0 / 0 はグループなしを表すセンチネル」",
    "と理解しております（貴社より、索引の効率を理由にこの方式を採る旨の"
    "ご回答をいただいております）",
    "ところが、実際のDDLでは 0 を投入できません",
    "当方が接続して確認した内容です",
    "SHOW CREATE TABLE customers （抜粋）",
    "（外部キー制約は有効です）",
    "NO_AUTO_VALUE_ON_ZERO は含まれておりません",
    "外部キー制約が有効である以上、group_id = 0 の顧客を INSERT するには、"
    "groups に id = 0 の行が存在していなければなりません",
    "group_id を省略して INSERT した場合も、既定値の 0 が入るため同じです",
    "そして groups.id は AUTO_INCREMENT ですので、",
    "id = 0 の行を入れること自体に NO_AUTO_VALUE_ON_ZERO が必要",
    "ですが、上のとおり sql_mode には含まれておりません",
    # 申し添え（2026-09-17 の2回目の改訂で新設した引用ブロック）
    "申し添え",
    "テスト環境の groups には id = 0 の行がございませんでしたが、",
    "この表の中身は当方が入れた検証用のダミー",
    "ですので、貴社の想定を示すものではありません",
    "そのため本件は「実データがこうなっている」ではなく、"
    "「DDLと sql_mode の組み合わせ上こうなる」というご報告です",
    "Q7-4 でお伺いしたいこと",
    "当方はメインDBのスキーマを変更しない方針のため、当方では直せません",
    "どの形がよいか、貴社のご判断をお聞かせください",
    "備考",
    "groups に id = 0 の番人行を1件入れる",
    "INSERT には NO_AUTO_VALUE_ON_ZERO が必要になります",
    "外部キー制約 customers_group_id_foreign を外す",
    "センチネル方式と両立します",
    "group_id を nullable にする",
    "索引効率を理由にセンチネル方式を採られた経緯があるため、蒸し返しになります",
    "グループなしの顧客は存在しない",
    "（必ずどこかのグループに属する）",
    "この場合、当方は「グループなし」の表示を作りません",
    "D であれば、対応は不要です。その旨だけお知らせください",
    # ---- 8章 制御盤の報告間隔（2026-09-17 に差し替え） ----
    "8. 制御盤の報告間隔",
    "8-1. 台数については、当方で把握いたしました（設問を取り下げます）",
    "初版では「P1 稼働時点の施設数」「ロボット・D-S制御盤の台数」の2問を"
    "貴社にお伺いしておりました",
    "この2問は取り下げます",
    "理由: 本番の規模は弊社（DFA）側で決めるべきことであり、"
    "貴社にお伺いする筋のものではありませんでした",
    "当方の記録にもその旨が残っておりました",
    "現時点の当方の把握は次のとおりです。参考までにお知らせいたします",
    "項目", "当方の把握", "出典",
    "ロボット台数", "現状 30台未満", "弊社（DFA）の把握。2026-09-17",
    "D-S制御盤の台数", "（試算では", "11台", "と置いております）",
    "施設数",
    "未確定", "弊社で確定させます",
    "上の「11台」は、当方がテスト環境に入れた検証用データの台数を"
    "そのまま置き値にしたものです",
    "実運用の台数ではありません",
    "1章・2-4 の外挿は、この「ロボット30台」を前提に置き直しております",
    "台数が大きく動いた場合は、当方から改めてお知らせいたします",
    "お伺いしたいこと（1問）",
    # ★2026-09-17（2回目）の改訂で「固定値」→「下限・上限・変更の頻度」★
    "link_schedules.intervals（制御盤の報告間隔・秒）は、"
    "運用上どの範囲で動きますでしょうか",
    "初版では「実運用値をお教えください」と、固定の1つの値を伺う形で"
    "書いておりました",
    "その後、この値は運用上変わりうるものだと分かりましたので、聞き方を改めます",
    "お教えいただきたいのは、次の3点です",
    "伺いたいこと",
    "運用上の下限（最短の報告間隔）", "「10秒より短くはしない」など",
    "運用上の上限（最長の報告間隔）", "「長くても60秒」など",
    "変更の頻度",
    "「初期設定から基本的に変えない」／「制御盤ごとに個別に決める」／"
    "「運用中に随時変える」など",
    "おおよそで構いません",
    "「だいたいこの範囲」が分かれば十分です",
    "なぜ範囲と頻度を伺うのか",
    "ここが control_reports の行数を決めるためです",
    "そして、行数の見積もりが6倍変わります",
    "報告間隔", "2年間の行数（制御盤11台と置いた試算）",
    "60秒（定義書の例）",
    "（第2回のご回答の例）",
    "1,100万行と7,000万行とでは、索引の要否（1-5 の⑥）も、"
    "画面の作り方も変わります",
    "下限が分かれば「最大でどこまで積み上がるか」を押さえられますし、"
    "変更の頻度が分かれば、その前提がいつまで持つかを判断できます",
    "なお、上の台数（ロボット30台未満）はこの試算には効きません",
    "control_reports の行数は台数よりも報告間隔で決まるためです"
    "（1-5 ⑥に同じことを書いております）",
    "設問番号について",
    "本章の設問は Q8-3 のみとなりますが、",
    "番号は詰めずに Q8-3 のままにしております",
    "（当方側の管理表がこの番号で対応を追っているためです。ご了承ください）",
    # ---- 9章 軽い確認 + DDL ----
    "軽い確認2件",
    "DDL未反映2件",
    "第8回 A9-2 の robot_task_reports は、DDL 上の clean_task_reports の"
    "ことでしょうか",
    "テーブル一覧に robot_task_reports が見当たりませんでした",
    "なお、仮にそうであっても当方の実装は clean_status_histories を"
    "読んでおりますので、",
    "影響はございません。表記の確認のみです",
    "あわせて、clean_task_reports に 今後データが入る予定があるか も"
    "お教えいただけますと助かります",
    "テスト環境のこの表には行がございませんが、",
    "そこに入っているデータは当方が用意した検証用のものですので、"
    "貴社の収集処理が書くかどうかは当方では分かりません",
    "1回の清掃の中の、区域別の内訳は取得できますでしょうか",
    "（聞き直し）",
    "第8回でもお伺いした件です",
    "当方が実装できたのは",
    "「1清掃エリア（＝1ロボット）につき1つの完了率」",
    "売り場・通路・レジ前といった内訳",
    "ではありません",
    "取得できない場合は「できない」とお答えいただければ、当方はこの機能を諦めます",
    "お願い: DDL 未反映の2件（4回目）",
    "急ぎません。お手すきの折で結構です",
    "clean_status_histories.id の AUTO_INCREMENT",
    "security_status_histories.reported_at の DEFAULT",
    # ---- 10章 DB更新用アカウント（2026-09-17 の2回目の改訂で章ごと差し替え） ----
    "10. DB更新用アカウント（すでに発行済みでした。ご依頼を取り下げます）",
    "第5回（2026-09-07）・第6回（2026-09-08）・第8回（2026-09-11）から"
    "継続してお願いしておりました件です",
    "当方が接続して確認いたしましたところ、必要な UPDATE 権限は"
    "すでに付与されておりました",
    "SHOW GRANTS FOR CURRENT_USER();",
    "（2026-09-17 に当方が実行）",
    "当方が必要としていたのは、この2つのテーブルの"
    "status / comment / responded_at / resolved_at の更新のみです",
    "すでに満たされております",
    "ご対応済みであることに気づかず、3回にわたって重ねてお願いして"
    "しまいました。当方の確認漏れです。失礼いたしました",
    "本件は取り下げます。貴社にご対応いただくことはございません",
    # ---- 11章 清掃の開始時刻とリアルタイム性（2026-09-17 の2回目の改訂で作り直し） ----
    "11. 清掃の開始時刻と、リアルタイム性について",
    "11-1. Q11-1 は、お伺いする内容を改めました",
    "初版では「ロボットが実際に清掃を開始した時刻は、"
    "どのテーブル・どのカラムに入りますでしょうか」と"
    "お伺いしておりました（Q11-1）",
    "そのように申し上げた根拠は2つございましたが、いずれも当方側の事情でした",
    "テーブル定義書の started_at の備考が"
    "「清掃スケジュールの清掃開始日時を格納する」と読め、",
    "予定時刻の写しのように見えた",
    "テスト環境で予定時刻との差を取ったところ全件0分だったこと",
    "ただしこのデータは当方が入れた検証用のダミーで、"
    "予定時刻をそのまま started_at にも入れていたものでした",
    "弊社内で確認いたしましたところ、clean_status_histories.started_at を"
    "実際の清掃開始時刻として扱ってよい、との結論になりました",
    "誤った前提でお手を煩わせるところでした。失礼いたしました",
    "つきましては、お伺いする内容を「どこにありますか」から"
    "「この理解で実装してしまってよろしいでしょうか」に"
    "改めさせていただきます",
    # Q11-1（作り直した設問）
    "clean_status_histories.started_at を実際の清掃開始時刻として"
    "実装を進めますが、問題ございませんでしょうか",
    "当方が書こうとしている実装は次のとおりです",
    "実開始時刻  = clean_status_histories.started_at",
    "遅延        = started_at − clean_timetables.start_at",
    "出発失敗    = 予定開始時刻 + 猶予30分 を過ぎても started_at が入らない",
    "確認させていただきたい理由",
    "「清掃スケジュールの清掃開始日時を格納する」",
    "予定時刻の写しとも読めてしまう",
    "もし実際には予定時刻が入る列でしたら、上の「出発失敗」は"
    "永久に0件になります",
    "エラーにもならず、画面上は「全件が時間どおりに出発した」と"
    "表示され続けます",
    "そのため、実装前に一言だけ確認させていただきたい次第です",
    "ご回答", "当方の動き",
    "A. その理解で問題ない", "このまま実装します",
    "（ご回答はこの一言で結構です）",
    "B. started_at には予定時刻が入る",
    "実装を止め、実開始時刻の所在を改めてご相談します",
    "貴社の収集処理の都合で「いまは予定時刻を書いているが、"
    "今後は実開始時刻を書く」といった事情がございましたら、"
    "その旨もお教えいただけますと助かります",
    # 11-2 背景
    "11-2. 背景（残る3問の前提）",
    "当方は、清掃が",
    "「予定時刻から遅れて始まった／始まらなかった」",
    "という状態を",
    "画面に出そうとしております",
    "当方の要件定義では、清掃失敗を次のように定めております",
    "予定開始時刻 +",
    "猶予30分",
    "を経過してもタスク開始が確認できない",
    "started_at で遅延そのものは出せるようになりましたが、",
    "次の2点は started_at だけでは埋まりません",
    "started_at が入らなかったとき、",
    "「指示すら出ていない」のか「指示は出たが動き出さなかった」のか",
    "が区別できません（→ Q11-2）",
    "貴社のサマリに入っている「出発成功」と、当方の日次判定とで",
    "定義がそろっているか",
    "が分かりません（→ Q11-3）",
    "お伺いしたいこと（Q11-1 のほかに3問）",
    "clean_statuses.clean_status_trans が 1（清掃指示）で止まったケースは、"
    "履歴として残りますでしょうか",
    "定義書によれば clean_status_trans は",
    "0: 清掃開始前, 1: 清掃指示, 2: 清掃中",
    "と推移します",
    "「1（清掃指示）で止まった ＝ 指示は出したが動き出さなかった」と読めますので、",
    "これが取れれば出発失敗を判定できます",
    "ただし clean_status_histories にこのカラムが無く、clean_statuses（現在の状態）は",
    "清掃ごとに上書きされるため、過去を遡ることができません",
    "履歴に残る仕組みがあるかをお教えください",
    "facility_summaries.launch_count / launch_ratio（出発成功数・出発成功率）の"
    "計算式をお教えください",
    "サマリには、貴社が計算済みの「出発成功数・出発成功率」のカラムが実在しております",
    "つまり貴社側では「出発に成功したか」を判定できている、ということかと存じます",
    "定義書の備考は",
    "「補足を参照.」",
    "とあるのみで、",
    "補足の本文を受領しておりません",
    "この判定式が分かれば、当方も同じ定義で日次の判定ができるようになります",
    # Q11-4（2026-09-17 の2回目の改訂で新設）
    "clean_status_histories.started_at のほかに、より短い間隔で"
    "更新されるテーブル・カラムはございますでしょうか",
    "新しくお伺いする設問です",
    "当方は現在、清掃の進み具合を clean_status_histories から読んでおります",
    "これよりも短い間隔で更新される（＝よりリアルタイムに近い）"
    "テーブル・カラムがあるのであれば、お教えください",
    "具体的な候補を挙げてお伺いすることは、あえていたしません",
    "当方で「このテーブルではないか」と当たりをつけますと、推測が混じります",
    "貴社の収集処理が書き込んでいるもののうち、更新間隔が短いものを"
    "お示しいただければ、当方でそれに合わせます",
    "あわせて、差し支えなければ次も添えていただけますと助かります",
    "そのテーブル・カラム名",
    "おおよその更新間隔",
    "（「数秒ごと」「1分ごと」など、おおよそで構いません）",
    "履歴として積まれるのか、上書きされるのか",
    "無い場合は「無い」とお答えいただければ、当方は "
    "clean_status_histories のままで進めます",
    "ご回答がいただけるまでの当方の対応",
    "Q11-2 / Q11-3 のいずれも取れない場合、当方は「出発失敗」と「未実施」を",
    "同じものとして扱います",
    "（どちらも clean_error = 1 で判定します）",
    "Q11-4 にご回答がない場合は、clean_status_histories を読む"
    "現在の形のまま進めます",
    "いずれも機能を止めるものではありませんので、ご回答は急ぎません",
    "ただ、取れるのであれば取りたい、という性質のものです",
    # ---- 12章 DB接続経路（2026-09-16 統合。初版の「参考: テスト環境」を吸収） ----
    "12. DB接続経路について",
    "お願い3件・確認6問",
    "本章は、第9回の初版（2026-09-15）の末尾に",
    "「参考: テスト環境について（申し送り）」として",
    "情報共有のみで記載していた内容を、正式なお願いとして書き直したものです",
    "別便にはせず、本書に統合しております。重複はありません",
    "お願い（12-3〜12-5）と質問（12-6）を分けて書いております",
    # 12-1 期限
    "12-1. 期限",
    "ステージングに実データが入る前",
    "日付ではなく「イベント」で切っております",
    "時点", "状態", "作り直しのコスト",
    "いま", "（検証用データのみ）",
    "実データは入っておりません",
    "ほぼゼロ", "アカウントを作り直しても、失うものがありません",
    "実データが入った後",
    "平文の経路を通った資格情報を前提に、運用が始まってしまいます",
    "「作り直す」しか手がなくなります",
    "過ぎるとどうなるか",
    "実データ投入後に同じことを行う場合、次が全部必要になります",
    "DBアカウントの再発行",
    "接続している側（当方のアプリ、貴社のデータ収集処理・集計バッチ）を",
    "いったん全部止めて差し替える必要があります",
    "TLS の有効化",
    "サーバ再起動を伴う可能性があり、夜間の清掃データ収集を止められません",
    "（D-Watch は",
    "夜間に自動実行された清掃を翌朝確認する運用",
    "接続元IPの制限",
    "実データが流れている状態で絞りますと、漏れた接続元からデータ収集が止まります",
    "つまり「止める調整」が発生します。いまであれば、その調整が要りません",
    "だからこそ、実データが入る前にお願いしております",
    # 12-2 実測値
    "12-2. 実測値（いつ・誰が・どう測ったか）",
    "推測ではありません。実測です",
    "項目", "内容",
    "対象",
    "テスト環境 133.125.238.144:13306 / DB clean_watch",
    "（本書1-1に記載のものと同じサーバーです）",
    "サーバーの", "バージョン",
    "測った人", "DFA（本プロジェクトの作業者）",
    "測った日",
    "2026-09-15 に本書の初版へ転記しました",
    "測り方",
    "貴社より受領した参照用アカウント",
    "（SELECT のみ）でテスト環境に接続し、",
    "サーバー変数を照会した",
    "変数／設定", "実測値", "意味",
    "サーバーが TLS を提供していません",
    "暗号化されていない接続を、サーバーが受け付けます",
    "参照用アカウントの", "接続元ホスト",
    "% は「どこからでも」。接続元の制限がありません",
    "本章の作成にあたり、テストDBへの再接続は行っておりません",
    "（意図的です）",
    "参照専用のアカウントではありますが、接続条件が現在も揃っているかが不明なため",
    "上記は「2026-09-13 に実測された値」です",
    "すでにご対応済みでしたら、12-3〜12-5 は読み飛ばしてください",
    "なぜ問題か",
    "重なりますと、接続は必ず平文になります",
    "クライアント側で「TLSを使う」と指定しても、サーバーが提供していないため"
    "暗号化されません",
    "接続元が %（どこからでも）のため、経路は公開IP上にあります",
    "DB接続の資格情報が経路上で保護されていない状態",
    "現在は検証用データのみのため実害はありません。問題になるのは実データが"
    "入った後です",
    "なお、このサーバーは弊社（DFA）が契約し、貴社に構築・保守をお願いしている"
    "ものと認識しております",
    "そのため、実施のご依頼は貴社宛といたしました。この認識に誤りがございましたら、"
    "その旨をお知らせください",
    # 12-3 お願い1
    "12-3. お願い1: 接続元の制限（本章の中で最優先です）",
    "の % を、",
    "固定IP",
    "（または閉域ネットワーク内のみ）に絞っていただけますでしょうか",
    "なぜ TLS より先にお願いするのか",
    "当方が利用しているDB接続用のライブラリに、",
    "「TLS を有効にしても、条件によっては接続時の資格情報が経路上に平文で"
    "出てしまう」という既知の問題",
    "が報告されております",
    "当方が 2026-09-16 に自動検査ツールで確認した実測です",
    "これは弊社内製アプリケーション側の事情ですので、どう解くかは弊社で"
    "判断いたします",
    "貴社にご対応をお願いするものではありません",
    "ただし1点だけ、貴社へのお願いの優先順に関係いたします",
    "この問題がある間は、",
    "TLS を有効にしていただいても資格情報が保護されない可能性が残ります",
    "そのため当方としては、TLS の有効化よりも先に「経路そのものを閉じる」",
    "＝接続元IPの制限のほうが効くと考えております",
    "恐れ入りますが、12-4（TLS）より 12-3（接続元の制限）を"
    "先にご検討いただけますと幸いです",
    # 12-4 お願い2
    "12-4. お願い2: TLS の有効化",
    "を有効にし、可能であれば",
    "12-3 の後で構いません",
    "なお 12-6 の Q12-4 が YES（VMとDBを閉域で結べる）となる場合、"
    "本項は不要になる可能性があります",
    # 12-5 お願い3
    "12-5. お願い3: 本番用DBアカウントの新規発行",
    "本番接続に使うアカウントは、現在のテスト用とは別のものを"
    "新規に発行していただけますでしょうか",
    "ユーザー名・パスワードとも",
    "お願いいたします",
    "理由",
    "過去に、実在のDBユーザー名が当方リポジトリのコミットに含まれたことがあります",
    "2026-09-10 のコミットで除去済みですが、",
    "Git の履歴には残っております",
    "リポジトリは非公開ですが、",
    "アクセス権を持つ全員が履歴を読めます",
    "パスワードは当該コミットには含まれておりません",
    "（架空の値のみでした）",
    "問題はユーザー名が既知になっている点です",
    "ユーザー名が既知である以上、12-2 の",
    "（どこからでも接続可）と組み合わさりますと、推測の手間が減ります",
    "あわせて、このアカウントの認証情報は、受け渡しの過程で平文のまま"
    "当方側の記録に残っております",
    "本番用は別の値を新規に発行していただくのが確実",
    "と考えております",
    "お願いしていないこと",
    "Git 履歴の書き換えはお願いしておりません",
    "（当方でも実施いたしません）",
    "「発行し直す」ほうで解いていただきたい、というお願いです",
    "アカウントの発行元は貴社ですので、",
    "再発行は貴社にしかお願いできません",
    "タイミングは 12-1 のとおり、ステージングに実データが入る前です",
    # 12-6 確認6問
    "12-6. 確認: 本番構成について（6問）",
    "ここからは質問です。お願いではありません",
    "Q12-1 以外は YES / NO でお答えいただけます",
    "なぜお伺いするのか",
    "12-3〜12-5 の解き方（＝「経路を閉じる」）が成立するかどうかが、"
    "本番の構成にかかっております",
    "VM と DB が同一の閉域ネットワークに入れるのであれば",
    "12-3・12-4 は「初めから起きない」",
    "公開IP越しに接続する構成であれば",
    "現在の状態がそのまま本番に持ち越されます",
    "当方は「閉じた構成になる見込み」と考えておりますが、"
    "これは当方の読み取りであり、",
    "明示の確認をいただいた記録がございません",
    "推測のまま本番に進みますと、12-1 の「作り直し」に直結いたします",
    "当方の現在の理解と、その根拠",
    "当方の理解", "根拠", "確認の有無",
    "本番のDBは",
    "貴社のご回答（2026-09-10）「本番サーバはMariaDBです」",
    "確認済み",
    "さくらインターネットの",
    "データベースアプライアンス",
    "同ご回答「さくらインターネットのデータベースアプライアンスは"
    "PostgreSQLかMariaDBなので」",
    "さくらのクラウド VM",
    "（Node.js 常駐が必要なため）",
    "当方側の想定", "未確認",
    "「アプリ＝VM」「DB＝アプライアンス」という",
    "別サービスの組み合わせである",
    "上記2・3からの読み取り",
    "VM と DB が同一ネットワークで閉域接続される",
    "すなわち、貴社のご回答は「なぜ MariaDB なのか」のご説明であり、",
    "「本番の構成はこうなります」というご説明ではありませんでした。"
    "当方が読み取っただけです",
    "そこで、読み取りが合っているかどうかを確認させてください",
    "アプリサーバー（VM）とDBサーバーは、それぞれどちらが用意する"
    "想定でしょうか",
    "当方の中で理解が揃っておりません。まずここを確定させていただきたく存じます",
    "対象", "選択肢",
    "アプリサーバー（Node.js を載せる VM）",
    "DBサーバー（データベースアプライアンス）",
    "貴社が用意 / 弊社が用意 / 未定",
    "弊社の記録には「アプリ側は弊社」と読める記述と"
    "「貴社に用意していただく」と読める記述が",
    "混在しております",
    "どちらが正かは貴社のご認識に合わせますので、お教えください",
    "本番のDBは、さくらインターネットの",
    "（＝VM上に自前で構築した MariaDB ではない）で間違いないでしょうか",
    "本番のアプリ（Node.js）は、",
    "に載せる想定で間違いないでしょうか",
    "その VM と DBアプライアンスは、",
    "同一の閉域ネットワーク（プライベート接続）で結べますでしょうか",
    "Q12-4 が YES の場合、",
    "DB側で公開IPからの接続を完全に閉じることは可能",
    "でしょうか",
    "Q12-4 が NO の場合（公開IP越しの接続になる場合）、",
    "TLS を有効にできます",
    "YES / NO / 条件つき",
    "YES / NO / 該当なし",
    "NO や「条件つき」の場合は、差し支えなければ理由を1行だけ"
    "添えていただけますと助かります",
    "当方はそれを前提に、12-3〜12-5 のお願いの内容を組み直します",
    # 12-7
    "12-7. 当方（DFA）側で済ませたこと",
    "丸投げではない、というご説明のために書いております",
    "2026-09-13 に当方のアプリコードを棚卸しいたしました",
    "危険な箇所は1件も見つかっておりません",
    "生SQL 0件 / 並べ替えのカラム名は固定リテラル1箇所 /",
    "where にユーザー入力なし / エラー本文に内部情報を載せている箇所 0件 /",
    "DBへの書き込み 0件 / リポジトリ内に実在の認証情報なし",
    "あわせて、生SQL禁止の静的解析ルール化・パスパラメータ検証の全体一致化・",
    "CSVの数式インジェクション対策・本番前セキュリティ確認チェックリストの整備・",
    "DBユーザー名の既定値をコードから除去する対応を済ませております",
    "問題があるのはコードではなく、DBへ至る「経路」のほうです。"
    "そして経路は、当方のコードでは閉じられません",
    "そのため本章の3件を、正式にお願いとしてお渡しいたします",
    # ---- 末尾 ----
    "以上です。ご確認のほど、よろしくお願いいたします",
    # 取り下げ項目のまとめ（2026-09-17 の2回目の改訂で新設）
    # ★Q11-1 は「取り下げた項目」ではなく「お伺いする内容を改めた項目」。
    #   段落が分かれていること自体を見る★
    "本改訂で取り下げた項目",
    "（面積の単位。弊社で㎡と確定）",
    "（DB更新用アカウント。すでに発行済みでした）",
    "取り下げではなく、お伺いする内容を改めた項目",
    "11章 Q11-1",
    "「実際の清掃開始時刻はどこにありますか」→",
    "「started_at を使って実装してよろしいでしょうか」",
    "こちらはご回答をお願いいたします（A / B の一言で結構です）",
    # ★対象を「取り下げた項目」に限定した文。旧「いずれもご対応は不要です」は
    #   直前の Q11-1 の回答依頼まで打ち消して読めるため 12. で禁止している★
    "上の「本改訂で取り下げた項目」については、ご対応は不要です",
    "あわせて、テスト環境の「行の中身」を根拠にしていた指摘を"
    "取り下げております（冒頭の「おことわり」をご覧ください）",
    "1章（索引）はご対応中と伺いましたので、依頼を取り下げ、"
    "参考情報に改めております",
    "ご対応が完了しましたら、その旨をお知らせください。"
    "当方で EXPLAIN を測り直します",
    "12章は、期限が「ステージングに実データが入る前」という性質のものです",
    "まずは 12-6 の Q12-1〜Q12-6 だけでも先にお答えいただけますと、",
    "12-3〜12-5 のお願いの内容を確定できます",
    # ---- 表紙の統合の断り書き ----
    "本書は、第9回確認事項に、DB接続経路についての申し送り",
    "（お願い3件・確認6問）を統合したものです",
    "別便は出しません。第9回として1通にまとめてお送りいたします。"
    "統合した内容は12章です",
]

miss = [m for m in must if not has(m)]
print("検査項目:", len(must), " 欠落:", len(miss))
for m in miss:
    print("  ★欠落★", m)

# 表題まわりは生成側の書式に幅があるため、固定文字列ではなく緩く見る
print("-- 表題まわり（書式に幅があるため緩く検査） --")
head_ng = []
# 改訂版は「作成: 2026-09-17 改訂（初版 2026-09-15 / 統合 2026-09-16）」。
# ★3つとも出ること★
if not re.search(r"2026[-/年]?0?9[-/月]?17", flat):
    head_ng.append("作成 2026-09-17 改訂")
if not re.search(r"2026[-/年]?0?9[-/月]?16", flat):
    head_ng.append("統合 2026-09-16")
if not re.search(r"2026[-/年]?0?9[-/月]?15", flat):
    head_ng.append("初版 2026-09-15")
if "DFARobotics" not in flat:
    head_ng.append("DFA Robotics")
print("見つかった日付らしき文字列:",
      sorted(set(re.findall(r"2026[-/]\d{2}[-/]\d{2}", flat)))[:12])
print("表題まわりの欠落:", head_ng if head_ng else "なし")

print("\n== 4. ALTER TABLE 7本 ==")
ALTERS = [
    ("①", "ALTER TABLE clean_status_histories ADD INDEX "
     "clean_status_histories_clean_timetable_id_index (clean_timetable_id)"),
    ("②", "ALTER TABLE robot_error_reports ADD INDEX "
     "robot_error_reports_clean_timetable_id_index (clean_timetable_id)"),
    ("③", "ALTER TABLE clean_timetables ADD INDEX "
     "clean_timetables_precheck_start_at_index (precheck, start_at)"),
    ("④", "ALTER TABLE clean_timetables ADD INDEX "
     "clean_timetables_facility_id_start_at_index (facility_id, start_at)"),
    ("⑤", "ALTER TABLE clean_status_histories ADD INDEX "
     "clean_status_histories_started_at_index (started_at)"),
    ("⑥", "ALTER TABLE control_reports ADD INDEX "
     "control_reports_control_id_reported_at_index (control_id, reported_at)"),
    ("⑦", "ALTER TABLE control_error_histories ADD INDEX "
     "control_error_histories_occurred_at_index (occurred_at)"),
]
alter_miss = [(n, s) for n, s in ALTERS if not has(s)]
n_alter = flat.count("ALTERTABLE")
# ★7本の SQL に加えて、1-1 の「なお ALTER TABLE の形で書いておりますのは、
#   列と順序を誤解なくお伝えするためです」で1回出る（2026-09-17 の改訂で追加）。
#   よって期待は 8 回★
N_ALTER_EXPECTED = 8
print("ALTER TABLE の出現回数:", n_alter,
      "（期待 %d = SQL 7本 + 1-1 の言及1回）" % N_ALTER_EXPECTED)
print("文として一致した本数:", len(ALTERS) - len(alter_miss), "/", len(ALTERS))
for n, s in alter_miss:
    print("  ★欠落★", n, s)
if n_alter != N_ALTER_EXPECTED:
    print("  ★NG★ ALTER TABLE の出現回数が %d ではない" % N_ALTER_EXPECTED)

print("\n== 5. 実測値（数値が落ちていないこと） ==")
MEASURED = [
    # 実測環境
    "133.125.238.144:13306", "clean_watch",
    "10.11.19-MariaDB-ubu2204", "VERSION()",
    # ①② の EXPLAIN
    "type=ALL", "possible_keys=null", "key=null", "key_len=null",
    "rows=538", "r_rows=538.00", "filtered=100", "r_filtered=0.93",
    "Extra=Using where",
    "rows=49", "r_rows=49.00", "r_filtered=4.08",
    # ★2026-09-17（2回目）の改訂で「0.93%（5行）」→「538行・0.93%」に
    #   （ダミー由来の値である旨を添える形に変わった）。
    #   旧表現は 12. の FORBIDDEN で残存を見ている★
    "上の 538行・0.93%",
    # 走査行数の外挿（★2026-09-17 にロボット30台で置き直した★）
    "約 4.4万行", "44塊", "約 193万行", "43,860行",
    "1.45 回/日/エリア", "2 回/日/台", "731日",
    "約386万行",
    # ★初版の数字は「初版では〜」として原稿に残っている★
    "約6,430万行", "約 1/33",
    # 1-4 の置き直し
    "約 2.2万行/年", "約4.4万行", "1/6",
    # ③④ の EXPLAIN
    "possible_keys=clean_timetables_start_at_index",
    "rows=1044", "r_rows=538.00",
    "possible_keys=clean_timetables_facility_id_foreign,"
    "clean_timetables_start_at_index",
    "r_rows=175.00", "filtered=32.47",
    "Using where; Using filesort",
    # ⑤
    "SUM(clean_area)", "'2026-09-01'", "'2026-09-14'",
    "type=index", "key=clean_status_histories_facility_id_foreign",
    "年間約13万行",
    # ⑥ の比較表
    "Using index for group-by",
    # ★2章の実データ（task_area 全24行 1,000 ちょうど 等）は
    #   2026-09-17（2回目）の改訂で原稿から消えた。当方が入れたダミーだったため。
    #   期待の置き場所は 12. の FORBIDDEN に移してある（検査は減っていない）★
    # 2-4 桁あふれ（★2026-09-17 に置き直した★。1,370 は「初版では」として残る）
    "double(14,6)", "約 1億㎡", "約9,100㎡", "9,132", "約1,370㎡",
    # 8章 報告間隔・当方の把握（★controls / facilities の行数は原稿から消えた★）
    "約 1,100万行", "約 7,000万行", "60秒", "10秒",
    "30台未満", "11台",
    # 1-2 の計算の置き値（★「実測」から「当方の置き値」に変わった★）
    "538行", "12エリア", "31日", "2026-09-17",
    # 11章（★delay の集計表と SQL は削除。始点は started_at に一本化★）
    "猶予30分",
    # 7章 DDL
    "bigint(20) unsigned NOT NULL DEFAULT 0",
    "@@foreign_key_checks", "@@sql_mode",
    # マスタの行数（7-1。ダミーである旨を添えたうえで原稿に残っている）
    "groups 1行 / customers 1行 / facilities 12行",
    # 12章 実測（2026-09-16 統合版）
    "have_ssl = DISABLED", "参照用アカウント@%",
    "require_secure_transport = OFF",
    "DISABLED", "OFF",
    "2026-09-13", "2026-09-10", "2026-09-16",
    "VERSION() = 10.11.19-MariaDB-ubu2204",
    "MariaDB 10.11",
]
meas_miss = [m for m in MEASURED if not has(m)]
print("検査した実測値:", len(MEASURED), " 欠落:", len(meas_miss))
for m in meas_miss:
    print("  ★欠落★", m)

print("\n== 6. 識別子の語中改行 ==")
# wordWrap="CJK" は ASCII の語中でも折り返す。折り返されると抽出テキストに
# 改行が入るため、空白を詰めない生の抽出テキストで原形のまま探す。
#   text_nb にある      → 無傷
#   flat にだけある     → ★語中で割れている★
#   どちらにも無い      → PDF に出ていない
IDENT = [
    # 索引名（今回の中心。長いので最も割れやすい）
    "clean_status_histories_clean_timetable_id_index",
    "robot_error_reports_clean_timetable_id_index",
    "clean_timetables_precheck_start_at_index",
    "clean_timetables_facility_id_start_at_index",
    "clean_status_histories_started_at_index",
    "control_reports_control_id_reported_at_index",
    "control_error_histories_occurred_at_index",
    # 既存の索引名（SHOW CREATE TABLE の抜粋）
    "clean_status_histories_id_unique",
    "clean_status_histories_facility_id_foreign",
    "clean_status_histories_section_id_foreign",
    "robot_error_reports_report_id_unique",
    "robot_error_reports_facility_id_foreign",
    "robot_error_reports_section_id_foreign",
    "robot_error_reports_robot_id_foreign",
    "clean_timetables_start_at_index",
    "clean_timetables_facility_id_foreign",
    "control_reports_control_id_foreign",
    "customers_group_id_foreign",
    # テーブル名
    "clean_status_histories", "clean_statuses", "clean_timetables",
    "clean_task_reports", "robot_error_reports", "robot_task_reports",
    "control_reports", "control_error_histories",
    "security_status_histories", "link_schedules",
    "customer_summaries", "customer_error_summaries",
    "facility_summaries", "facility_error_summaries",
    # ★controls は 2026-09-17（2回目）の改訂で原稿から消えた
    #   （8-1 の「テスト環境の controls は11行」がダミー由来だったため）。
    #   下の WATCH に移した（出るなら無傷であること）★
    "group_summaries", "groups", "customers", "facilities",
    # 修飾つきの列名
    "facility_summaries.launch_count",
    "clean_statuses.clean_status_trans",
    "clean_status_histories.started_at",
    "clean_status_histories.id",
    # ★clean_task_reports.started_at は 2026-09-17（2回目）の改訂で原稿から
    #   消えた（旧 Q11-1 の選択肢 A）。検査を外したのではなく、期待の置き場所を
    #   12. の FORBIDDEN に移してある（docs/spec.md と同じ扱い）★
    "clean_timetables.start_at",
    "security_status_histories.reported_at",
    "customers.group_id", "facilities.customer_id", "facilities.shop_id",
    "link_schedules.intervals",
    # 列名
    "clean_timetable_id", "clean_status_trans", "clean_error", "clean_ratio",
    "clean_area", "task_area", "task_count", "launch_count", "launch_ratio",
    "operation_ratio", "error_ratio", "error_count",
    "started_at", "start_at", "reported_at", "occurred_at", "precheck",
    "facility_id", "customer_id", "group_id", "section_id", "robot_id",
    "control_id", "shop_id", "customer_name", "error_id",
    "responded_at", "resolved_at",
    # SQL・設定値
    # ★TIMESTAMPDIFF は 2026-09-17（2回目）の改訂で原稿から消えた
    #   （11-2 の delay 集計 SQL ごと削除）。12. の FORBIDDEN に移してある★
    "NO_AUTO_VALUE_ON_ZERO", "AUTO_INCREMENT",
    "foreign_key_checks", "sql_mode", "have_ssl", "DISABLED",
    "require_secure_transport",
    "bigint(20)", "varchar(20)", "double(14,6)",
    "参照用アカウント@%", "clean_watch",
    "10.11.19-MariaDB-ubu2204", "133.125.238.144:13306",
    # ★docs/spec.md は 2026-09-17 の改訂で原稿から消えた★
    #   （1-4 が「当方の docs/spec.md は〜」→「当方の社内資料は〜」に変わった）。
    #   検査を外したのではなく、期待の置き場所を 12. の FORBIDDEN に移してある。
]
ident_ok, ident_broken, ident_absent = [], [], []
for i in IDENT:
    if i in text_nb:
        ident_ok.append(i)
    elif sq(i) in flat:
        ident_broken.append(i)
    else:
        ident_absent.append(i)
print("検査した識別子:", len(IDENT),
      " 無傷:", len(ident_ok),
      " 語中改行:", len(ident_broken),
      " PDF に出現せず:", len(ident_absent))
for b in ident_broken:
    print("  ★語中で割れている★", b)
for a in ident_absent:
    print("  ★PDF に出現しない★", a)

# 原稿には出てこないが、生成側が本文で書く可能性のある長い識別子。
# 出るなら無傷であること。出なくてもよい（欠落とはしない）。
WATCH = [
    "clean_status_histories.clean_timetable_id",
    "robot_error_reports.clean_timetable_id",
    "clean_status_histories.clean_error",
    "clean_status_histories.clean_area",
    # ★2026-09-17（2回目）の改訂で原稿から消えた表名。出るなら無傷であること★
    "controls",
]
watch_broken = [w for w in WATCH
                if w not in text_nb and sq(w) in flat]
watch_absent = [w for w in WATCH if w not in text_nb and sq(w) not in flat]
print("追加監視（出るなら無傷であること）:",
      "割れ", watch_broken if watch_broken else "なし",
      "/ 未出現", len(watch_absent), "件")

# 行末が "_" で終わる / 次行が "_" で始まる = 列挙に無い識別子が割れた形跡
tail = [ln for ln in text.split("\n") if ln.rstrip().endswith("_")]
head_us = [ln for ln in text.split("\n") if ln.lstrip().startswith("_")]
print("行末が _ で終わる行:", len(tail), " 行頭が _ で始まる行:", len(head_us))
for ln in (tail + head_us)[:10]:
    print("  ★割れている疑い★", ln.strip()[:60])

print("\n== 7. 章（12章）と設問（Qn-m. 形式の見出し）の採番 ==")
# ★ng は 7. の中でも使うのでここで用意する★
# （以前は 11. の直前で ng = [] としていたため、7. の「部数不一致」の
#   追記が未定義参照になり、かつ後から空リストで消えていた）
ng = []
# 本文の章見出し。冒頭の構成表にも同じ文言が出る章があるため、
# 「あること」だけを必須にし、出現回数は参考表示にとどめる。
CHAPTERS = [
    (1, "1. 索引について（ご対応中と伺いました。参考情報です）"),
    (2, "2. サマリの面積は、期間内の合計でしょうか"),
    (3, "3. 第8回 Q4 の残り（サマリの率・分子・分母の関係）"),
    (4, "4. サマリの error_count の集計範囲"),
    (5, "5. 面積の単位（設問を取り下げます）"),
    (6, "6. グループ単位のサマリ"),
    (7, "7. グループ・顧客マスタのデータ投入のお願い"),
    (8, "8. 制御盤の報告間隔"),
    (9, "9. 軽い確認2件 + DDL未反映2件"),
    (10, "10. DB更新用アカウント（すでに発行済みでした。ご依頼を取り下げます）"),
    (11, "11. 清掃の開始時刻と、リアルタイム性について"),
    (12, "12. DB接続経路について — お願い3件・確認6問"),
]
chap_miss = [(n, s) for n, s in CHAPTERS if not has(s)]
print("章の数:", len(CHAPTERS), " 見出しが見つからない章:",
      [n for n, _ in chap_miss] if chap_miss else "なし")
for n, s in chap_miss:
    print("  ★欠落★", n, s)
# 冒頭の構成表の行（12行。★本文の「12部構成です」と章の数と、この3つが一致すること★）
#
# ★経緯★: 初版の原稿は「9部構成です」と書きながら構成表が10行・章が11あった。
#   （11章を後から足したときに冒頭を直し忘れたため）。この点検が見つけて原稿を直している。
#   ★3つを別々に数えて突き合わせる形を崩さないこと★ — 1箇所だけ直して他が置き去りになる。
INDEX_ROWS = [
    "1. 索引について",
    "2. サマリの面積は期間内の合計でしょうか",
    "3. 第8回 Q4 の残り（サマリの率・分子・分母）",
    "4. サマリの error_count の集計範囲",
    "5. 面積の単位",
    "6. グループ単位のサマリ",
    "7. グループ・顧客マスタのデータ投入",
    "8. 制御盤の報告間隔",
    "9. 軽い確認2件 + DDL未反映2件",
    "10. DB更新用アカウント",
    "11. 清掃の開始時刻とリアルタイム性",
    "12. DB接続経路についてのお願い3件・確認6問",
]
row_miss = [r for r in INDEX_ROWS if not has(r)]
print("冒頭の構成表の行:", len(INDEX_ROWS), " 欠落:",
      row_miss if row_miss else "なし")

QNOS = (# ★Q2-2 は 2026-09-17（2回目）に取り下げた。番号は詰めず Q2-3 のまま★
        ["Q2-1", "Q2-3"] +
        ["Q4-1", "Q4-2", "Q4-3"] +
        # ★Q5-1 は 2026-09-17（2回目）に取り下げた（弊社で ㎡ と確定）★
        ["Q6-1", "Q6-2"] +
        ["Q7-1", "Q7-2", "Q7-3", "Q7-4", "Q7-5"] +
        # ★Q8-1 / Q8-2 は 2026-09-17 に取り下げた。番号は詰めず Q8-3 のまま★
        ["Q8-3"] +
        ["Q9-1", "Q9-2"] +
        # ★Q11-1 は取り下げではなく「実装の確認」に作り直した。Q11-4 は新設★
        ["Q11-1", "Q11-2", "Q11-3", "Q11-4"] +
        ["Q12-1", "Q12-2", "Q12-3", "Q12-4", "Q12-5", "Q12-6"])
# ★取り下げた設問番号★。「旧 Q8-1 / Q8-2 を取り下げました」「初版の Q2-2（…）」
# のような言及としてだけ出てよい。
# ★設問見出し「Q8-1.」のように番号＋ピリオドの形で出たら NG★（下で見る）。
WITHDRAWN_Q = ["Q8-1", "Q8-2", "Q2-2", "Q5-1"]
print("設問数:", len(QNOS), "（1章・3章・5章・10章は設問なし。原稿どおり）")
q_miss = [q for q in QNOS if (q + ".") not in flat]
print("本文に設問見出し（Qn-m. の形）が無いもの:", q_miss if q_miss else "なし")
# 取り下げた設問が見出しの形で残っていないこと
q_zombie = [q for q in WITHDRAWN_Q if (q + ".") in flat]
print("取り下げた設問（%s）の見出しが残っていないか:" % "/".join(WITHDRAWN_Q),
      q_zombie if q_zombie else "残っていない")
# 取り下げの言及そのものは原稿にあるので、出ていること（消し過ぎの検出）
q_mention = [q for q in WITHDRAWN_Q if q not in flat]
print("取り下げの言及（%s）:" % " / ".join(WITHDRAWN_Q),
      "欠落 %s" % q_mention if q_mention else "あり")
found_q = sorted(set(re.findall(r"Q\d+-\d+", flat)))
# 空白を詰めているため「Q2-3」の直後に数字が続くと「Q2-31」に見える。
# 既知の番号に数字が続いただけのものは誤検出として除く。
KNOWN_Q = QNOS + WITHDRAWN_Q
q_extra = [q for q in found_q if q not in KNOWN_Q
           and not any(q.startswith(k) and q[len(k):].isdigit() for k in KNOWN_Q)]
print("PDF に出てくる設問番号:", len(found_q), "種")
print("原稿に無い設問番号:", q_extra if q_extra else "なし")
counts = {q: len(re.findall(re.escape(q) + r"(?![0-9\-])", flat)) for q in QNOS}
print("出現回数（参考）:",
      ", ".join("%s:%d" % (q, counts[q]) for q in QNOS))
# 「本書は次のN部構成です」の N と、実際の章の数（参考表示。原稿は9と書いている）
m_bunsei = re.search(r"本書は次の(\d+)部構成", flat)
if m_bunsei:
    n_bunsei = int(m_bunsei.group(1))
    agreed = n_bunsei == len(INDEX_ROWS) == len(CHAPTERS)
    print("本文の「次のN部構成」:", n_bunsei,
          "／ 冒頭の構成表:", len(INDEX_ROWS), "行",
          "／ 実際の章:", len(CHAPTERS), "章",
          "→ 一致" if agreed else "→ ★不一致★（原稿の冒頭を直し忘れている可能性）")
    if not agreed:
        ng.append("部数・構成表の行数・章数が一致しない")
else:
    print("  ★注意★ 「本書は次のN部構成です」が見つからない")

print("\n== 8. 豆腐（グリフ欠け） ==")
# 抽出テキストの全文字を BIZ UDGothic の cmap に引き当てる。
# 字が無い文字は PDF 上で □ になる。㎡ / Σ / ÷ / → / ①〜⑦ / 〜 / ＝ 等の確認。
missing_r, missing_b = [], []
tofu_checked = False
try:
    from reportlab.pdfbase import pdfmetrics
    from reportlab.pdfbase.ttfonts import TTFont
    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]
    tofu_checked = True
    print("本文に出る文字種:", len(chars))
    print("BIZ UDGothic R に字が無い文字:", missing_r if missing_r else "なし")
    print("BIZ UDGothic B に字が無い文字:", missing_b if missing_b else "なし")
except Exception as e:
    print("  ★この検査は飛ばした★（reportlab かフォントが無い）:", e)
tofu = sorted(set(c for c in text if c in "\ufffd□"))
print("置換文字・豆腐そのものの混入:", tofu if tofu else "なし")

print("\n== 9. 空白ページ ==")
blank = []
for i, t in enumerate(pages, 1):
    body = sq(strip_footer(t))
    body = body.replace("D-Watch/DFARobotics", "")
    body = body.replace("D-Watch", "").replace("DFARobotics", "")
    body = re.sub(r"[\d/\-]+", "", body)
    print("  p%-3d 抽出 %5d 文字 / 本文 %5d 文字" % (i, len(t), len(body)))
    if len(body) < 20:
        blank.append(i)
print("空白ページ:", blank if blank else "なし")

print("\n== 10. 認証情報・接続情報 ==")
# 第9回は実測環境のホストを本文に書いている（テスト環境・共有済み）。
# 書いてよいのはこの1つだけ。ほかの IP・パスワードは載せない。
ALLOW_IP = {"133.125.238.144"}
SECRET = ["password", "PASSWORD", "Password", "IDENTIFIED BY", "passwd",
          "pwd=", "GRANT ", "jdbc:"]
# ★2026-09-17（2回目）の改訂で、10章に SHOW GRANTS の出力を載せた。
#   そこで "GRANT " の検査は外さず、★原稿にあるこの2行だけを許す★形に絞る。
#   ★IDENTIFIED BY PASSWORD のハッシュは絶対に載せない★ — こちらは
#   "IDENTIFIED BY" として従来どおり1つでも出たら NG のまま。
#   許す行を増やすときは、必ず原稿（.md）に同じ行があることを確かめること★
ALLOW_GRANT = [
    "GRANT UPDATE ON `clean_watch`.`robot_error_reports`",
    "GRANT UPDATE ON `clean_watch`.`control_error_histories`",
]
text_nogrant = text
for g in ALLOW_GRANT:
    # PDF 側でバッククォートが外れて出ることがあるので両方の形を落とす
    text_nogrant = text_nogrant.replace(g, "").replace(g.replace("`", ""), "")
leak = [s for s in SECRET if s in text_nogrant]
print("認証情報らしき語:", leak if leak else "なし")
# 許した GRANT 行が消え過ぎていないこと（10章ごと落ちたら気づけるように）
grant_missing = [g for g in ALLOW_GRANT if not has(g)]
print("10章の SHOW GRANTS の行（許可済み）:",
      "欠落 %s" % grant_missing if grant_missing else "2行とも出ている")
ips = sorted(set(re.findall(r"\b(?:\d{1,3}\.){3}\d{1,3}\b", text)))
bad_ips = [ip for ip in ips if ip not in ALLOW_IP]
print("IP アドレス:", ips if ips else "なし",
      "／ 想定外:", bad_ips if bad_ips else "なし")
# mysql:// の認証情報が伏せ字であること（原稿は mysql://…@host:port/db）
creds = re.findall(r"mysql://([^@\s]*)@", text)
bad_creds = [c for c in creds if c and c not in ("…", "...", "***")]
print("mysql:// の認証情報:", creds if creds else "なし",
      "／ 伏せ字でないもの:", bad_creds if bad_creds else "なし")
# ★2026-10-08★ .env.local の DB 認証情報が出ていないこと。
#   値はこのスクリプトに書かない・出力にも出さない（実行時に .env.local から読む。
#   check_teamworks_deploy_request_pdf.py と同じやり方）。
#   .env.local が無い環境では照合を飛ばし、その旨だけ表示する（判定には含めない）。
ENV = os.path.join(os.path.dirname(os.path.abspath(__file__)),
                   "..", "..", "..", ".env.local")
env_hit = []
if os.path.exists(ENV):
    env = {}
    for l in open(ENV, encoding="utf-8"):
        m = re.match(r"^\s*([A-Z0-9_]+)\s*=\s*(.*?)\s*$", l)
        if m:
            env[m.group(1)] = m.group(2).strip().strip('"').strip("'")
    secrets = [(k, env[k]) for k in ("DATABASE_USER", "DATABASE_PASSWORD",
                                     "DATABASE_WRITE_USER", "DATABASE_WRITE_PASSWORD")
               if env.get(k)]
    for k, v in secrets:
        if v in text or sq(v) in flat or sq(v) in flat_nofoot or v.encode("utf-8") in raw:
            env_hit.append(k)
    print("  .env.local の認証情報: %d 個の値を照合（値は表示しない） → %s"
          % (len(secrets), ("★検出★ " + ", ".join(env_hit)) if env_hit else "検出なし"))
else:
    print("  ※ .env.local が無いので、DB 認証情報の照合は飛ばした")

print("\n== 12. 取り下げた語が残っていないこと（2026-09-17 の改訂の主旨） ==")
# ★今回の改訂の主旨は「1章が依頼に見えないこと」「8章が規模の質問に見えないこと」。
#   文言を戻すと点検が素通りしてしまうので、ここで機械的に守る★
FORBIDDEN = [
    # 1章が「ご依頼」だった頃の見出し・文言
    "索引のご依頼",
    "ご依頼①②", "ご依頼③④", "ご依頼⑤", "ご依頼⑥⑦",
    "ご依頼にあたっての申し添え",
    "索引が1本も無い列（最優先）",
    "なぜ今お願いするのか",
    "1章のご依頼⑤の理由",
    "本書の中心は1章",
    "これは質問ではなくお願いであり",
    "1章と7章は貴社の担当領域",
    "恐れ入りますが2本別々でお願いします",
    "あわせてお願いいたします",
    "優先順位をつけるとすれば",
    "上記はすべて貴社への依頼であり",
    "1章の索引①②だけでも先に",
    "面積の累計表示に必要",
    "現在の上限31日から、上限2年に広げる",
    "しかし本番相当の行数になると",
    "当方の docs/spec.md は",   # 1-4 は「当方の社内資料は」に変わった
    "docs/spec.md",
    # 8章が「本番規模の見積もり」だった頃の見出し・文言
    "本番規模の見積もり",
    "当方の資料に見積もりが1件もありません",
    "P1 稼働時点の施設数の見込みをお教えください",
    "ロボット台数・D-S制御盤の台数の見込みをお教えください",
    "施設数・台数・行数の見積もりは1つも見つかりませんでした",
    "施設数・台数・報告間隔",
    "8章の見積もり次第で",
    # 施設200件を前提にしていた頃の数字・表
    "施設200件・年365日",
    "1施設1日あたり 約1,370㎡",
    "施設200件で外挿",
    "約 25万行",
    # ================================================================
    # ★2026-09-17（2回目）の改訂で足したもの★
    # テストDBの行が当方由来のダミーだと判明し、原稿が取り下げた文言。
    # 戻すと「先方のデータを根拠に指摘している」書面に逆戻りするので、
    # ここで機械的に守る。
    # ================================================================
    # --- 2章 2-1 の実測表と、その読み（章ごと削除した） ---
    "2-1. 実データが合計に見えません",
    "実データが合計に見えません",
    "facility_summaries と customer_summaries の実データを確認したところ",
    "task_area / clean_area が期間内の合計になっていないように見えます",
    "実測（facility_summaries 全24行 / customer_summaries 全2行）",
    "履歴からのΣtask_area",
    "メガバリューマート中央本店", "三ツ橋中央SC店", "バリューマート北町店",
    "気になっている点は3つです",
    "task_area が全24行とも 1,000 ちょうど",
    "8月の月次と9月第2週とで、施設ごとの clean_area が完全に一致しています",
    "期間が違うのに同じ値",
    "clean_ratio は clean_area ÷ task_area × 100 と一致しています",
    "941 / 1000 = 94.1",
    "941 / 713 / 881 / 748 / 904 / 632 / 801 / 872 / 550 / 855 / 920 / 810",
    # ★2-1 の表の「履歴からのΣtask_area」の実数。
    #   旧 MEASURED にあった値をそのまま移したもの（検査は減っていない）。
    #   「1,000」は 1-2 の「43,860 ÷ 1,000」に含まれるため、ここには入れない —
    #   代わりに上の「task_area が全24行とも 1,000 ちょうど」で見ている★
    "9,000", "13,000", "25,722", "25,094",
    # ★旧 Q7-4 の「groups は id = 1 のみ」。旧 MEASURED から移した★
    "→  1 のみ",
    "率とは整合していますので、値が壊れているわけではなさそうです",
    "実データは全538行が1対1でした",
    "clean_status_histories は実測で 1対1（538行 対 538行）",
    "538行を全部読んで、そのうち 0.93%（5行）だけを残しています",
    "0.93%（5行）",
    "実測（テストDB。2026-09-17 に再測",
    "安全側に切り上げ、2 回/日/台 として計算する",
    "2026-08-14",
    # --- 2章 Q2-2（取り下げた設問。見出しと本文） ---
    "Q2-2. B または C の場合",
    "B または C の場合、期間内の総清掃面積を得る正しい経路をお教えください",
    "期間内の総清掃面積を得る正しい経路",
    "まだバッチが動いていないため、現在の値に意味は無い",
    "期間内の合計である",
    "（＝上のデータは、バッチではなく手で入れた検証用データ）",
    "サマリが合計でない場合、clean_status_histories から毎回その場集計する"
    "ことになります",
    # --- 5章 Q5-1（取り下げた設問。見出しと本文） ---
    "Q5-1. task_area / clean_area の単位は平方メートル",
    # ★旧 Q5-1 の文そのものは、5章の取り下げの説明で引用されている。
    #   そちらは下の WORD_ONLY_IN で文脈を縛って見ている★
    "当方の資料には「㎡」と書かれておりますが、これは当方が書いたものです",
    "貴社からのご回答としては、単位を伺った記録がございません",
    "画面に単位を表示する以上、推測で書くわけにまいりませんので、お教えください",
    "なお実データは task_area が全行 1000 ちょうどで店舗規模による差がないため",
    "値の桁から単位を推定することもできませんでした",
    # --- 10章 の依頼文（すでに発行済みだったので取り下げた） ---
    "10. 継続のお願い: DB更新用アカウント",
    "継続のお願い: DB更新用アカウント",
    "第8回（2026-09-11）から継続してお願いしている件です",
    "急ぎませんが、取り下げてはおりません",
    "必要な権限は次の4カラムの UPDATE のみです",
    "時期の目安だけでもお知らせいただけますと、当方の作業順を"
    "組みやすくなります",
    "DB更新用アカウントの発行時期の目安",
    "第5回・第6回・第8回からの継続",
    "最古の待ち",
    # --- 11章 旧 Q11-1（「どこにありますか」だった頃の章・設問・実測） ---
    "11. 実際に清掃を開始した時刻は、どこにありますでしょうか",
    "実際に清掃を開始した時刻は、どこにありますでしょうか",
    "実際に清掃を開始した時刻はどこにありますか",
    "Q11-1. ロボットが実際に清掃を開始した時刻は",
    # ★旧 Q11-1 の文そのものは、11-1 の取り下げの説明で引用されている。
    #   そちらは下の WORD_ONLY_IN で文脈を縛って見ている★
    "11-2. 実際の開始時刻が見つかりませんでした",
    "実際の開始時刻が見つかりませんでした",
    "実開始時刻は保持していない",
    "clean_task_reports.started_at",
    "clean_task_reports は現在0行のため、当方では検証できませんでした",
    "clean_status_histories と対照的でしたので、A ではないかと推測しております",
    "clean_task_reports は現在0行です",
    "実データでも確かめました",
    "TIMESTAMPDIFF",
    "全件が0分でした",
    "予定時刻から予定時刻を引いているので、当然0分になります",
    "このカラムでは、遅延を計算することができません",
    "つまり clean_timetables.start_at（予定時刻）の写し、と読めます",
    "Q11-1 〜 Q11-3 のいずれも取れない場合",
    "本章は書面の末尾にありますが、優先度は高いです",
    "書面の末尾にありますが、優先度は高いです",
    # --- その他、ダミー由来だった記述 ---
    "テストグループ001", "テスト顧客001",
    "ただし、中身が入っておりません",
    "実データが全12行とも \"0\" のため、値から判断できませんでした",
    "テスト環境の controls は11行",
    "テスト環境の facilities は12行",
    "SELECT id FROM `groups` ORDER BY id;",
    "id = 0 の行はありません",
    "既定値が 0 であるにもかかわらず、groups に id = 0 の行が無いため",
    "実データの面積が全行 1,000 固定のため",
    "そのうえで本書の中心は、11章（実際の清掃開始時刻）と12章（DB接続経路）です",
    "なお P1 の対象顧客は1社のみですので、本件が実際に効いてくるのは2社目からです",
    "C の場合、当方はホールディングス単位の稼働率・エラー発生率を画面に出しません",
    "link_schedules.intervals（制御盤の報告間隔・秒）の実運用値を"
    "お教えください",
    "そのため本問だけを残しております",
    "質問26問 / お願い6件",
    # ================================================================
    # ★2026-09-17（3回目）の改訂で足したもの★
    # ★★Q11-1 は「取り下げ」ではなく「お伺いする内容を改めた」設問である★★。
    # 「取り下げます」と書きながら同じ節に生きた Q11-1 がある、という矛盾を
    # 一度出しているので、旧文言が戻らないことをここで機械的に守る。
    # ★Q11-1 は質問25問に数え続ける。設問見出し「Q11-1.」は出てよい
    #   （QNOS 側で「無いと NG」にしている）。禁じるのは "取り下げ" の言い回し★
    # ================================================================
    "11-1. Q11-1 は取り下げます",
    "Q11-1 は取り下げます",
    "Q11-1 は取り下げ",
    "Q11-1 は解決",
    "Q11-1 は、解決",
    "残り3問に縮小しました",
    "残り3問に縮小",
    "Q11-1（実際の清掃開始時刻）は started_at を使う形で解決しましたので取り下げ",
    "started_at を使う形で解決しましたので取り下げ",
    "代わりに Q11-4 を新たに立てております",
    "11章は、Q11-1（実際の清掃開始時刻）が started_at を使う形で"
    "解決いたしましたので、初版より縮小しております",
    "解決いたしましたので、初版より縮小しております",
    "初版より縮小しております",
    # 末尾のまとめで「取り下げた項目」の列挙に Q11-1 が混ざった形
    "11章 Q11-1（実際の清掃開始時刻。started_at を使う形で解決）",
    "（実際の清掃開始時刻。started_at を使う形で解決）",
    "Q11-1（実際の清掃開始時刻。",
    # ================================================================
    # ★2026-09-17（4回目）の改訂で足したもの★
    # ★★末尾のこの位置（Q11-1 の回答依頼の直後）に「いずれも」と書くと、
    #   本書で唯一お願いした回答まで打ち消して読めてしまう★★。
    # 対象を「本改訂で取り下げた項目」に限定した文が正
    #   （must 側に新しい文言を入れてある）。
    # ================================================================
    "いずれもご対応は不要です",
    # ================================================================
    # ★2026-09-17（5回目）の改訂で足したもの★
    # ★★「実測」と書くときは数字の出所を併記する★★（当方の作業規約）。
    # 1-1 の旧文は出所を書かずに「すべて実測」と言い切っていた。
    # 本書自身がその規約を破らないよう、ここで機械的に守る。
    # 新文は must 側（「テスト環境で実際に実行したものです」＋行数の出所3行）。
    # ================================================================
    "以下、すべて実測です。推測ではありません",
    "すべて実測です。推測ではありません",
]
forbidden_hit = [f for f in FORBIDDEN if has(f)]
print("取り下げた語の検査:", len(FORBIDDEN), "件  残存:", len(forbidden_hit))
for f in forbidden_hit:
    print("  ★残っている★", f)

# ★語そのものは原稿に残っているが、出てよい文脈が決まっているもの★
# 許された文脈を抽出テキストから取り除いたうえで、語が残らないかを見る。
WORD_ONLY_IN = {
    "最優先": [
        "「最優先のご依頼（7件）」として書いておりました",   # 1-0 の取り下げの説明
        "本章の中で最優先です",                              # 12-3（原稿のまま）
    ],
    "ご依頼": [
        "ご依頼を取り下げ参考情報に改めました",              # ヘッダの改訂点
        "ご依頼を取り下げました",                            # ヘッダの改訂点（10章）
        "つきましては1章はご依頼を取り下げ",                 # はじめに
        "ご依頼は取り下げております",                        # 1-0 の見出し
        "「最優先のご依頼（7件）」として書いておりました",   # 1-0 の本文
        "ご依頼でも質問でもなく",                            # 2-4
        "ご依頼を取り下げます",                              # 10章の見出し・構成表
        "実施のご依頼は貴社宛といたしました",                # 12-2
    ],
    "施設200件": [
        "初版ではこの走査行数を「施設200件」という当方の仮置きで"
        "外挿しておりました",                                # 1-2
        "初版（施設200件での外挿）では",                     # 1-2
        "施設200件規模に相当する数字",                       # 1-4
        "初版では「施設200件」を前提に",                     # 2-4
        "「施設200件（仮置き）」から",                       # ヘッダの改訂点
    ],
    "1,370": [
        "約1,370㎡ としておりましたが",                      # 2-4
    ],
    "6,430": [
        "「2年で約6,430万行」としておりました",              # 1-2
    ],
    # ★2026-09-17（2回目）に足したもの★
    # 取り下げた設問の文言そのものは、原稿が「初版では〜とお伺いしておりました」
    # として引用している。★引用としてだけ出てよく、設問としては出てはいけない★。
    # 設問見出しの形（「Q5-1.」「Q11-1. ロボットが〜」）は上の FORBIDDEN が見る。
    "task_area / clean_area の単位は平方メートル（㎡）でしょうか": [
        # 5章。取り下げの説明の中の引用
        "初版では「task_area / clean_area の単位は平方メートル（㎡）"
        "でしょうか」とお伺いしておりました（Q5-1）",
    ],
    "ロボットが実際に清掃を開始した時刻は、"
    "どのテーブル・どのカラムに入りますでしょうか": [
        # 11-1。取り下げの説明の中の引用
        "初版では「ロボットが実際に清掃を開始した時刻は、"
        "どのテーブル・どのカラムに入りますでしょうか」と"
        "お伺いしておりました（Q11-1）",
    ],
}
word_ng = []
for word, allowed in WORD_ONLY_IN.items():
    rest = flat_nofoot
    for a in allowed:
        rest = rest.replace(sq(a), "")
    n_all = flat_nofoot.count(sq(word))
    n_rest = rest.count(sq(word))
    print("  「%s」 全体 %d 回 / 許した文脈を除くと %d 回 → %s"
          % (word, n_all, n_rest, "OK" if n_rest == 0 else "★想定外の文脈★"))
    if n_all == 0:
        # 消し過ぎ（原稿には残っている語なので、1回も出ないのもおかしい）
        word_ng.append("「%s」が1回も出ていない" % word)
    if n_rest:
        word_ng.append("「%s」が許した文脈以外に %d 回" % (word, n_rest))

print("\n== 11. 判定 ==")
# ★ng は 7. で用意済み。ここで作り直さないこと（7. の追記が消える）★
if empty_extract:
    ng.append("テキストが抽出できない")
if not biz:
    ng.append("BIZ UDGothic が埋め込まれていない")
if notbiz:
    ng.append("BIZ UDGothic 以外の埋め込みフォント")
if cn_hit:
    ng.append("中国語向けフォントの混入")
if miss:
    ng.append("原稿の文言の欠落 %d 件" % len(miss))
if head_ng:
    ng.append("表題まわりの欠落 %d 件" % len(head_ng))
if alter_miss or n_alter != N_ALTER_EXPECTED:
    ng.append("ALTER TABLE 7本がそろっていない")
if meas_miss:
    ng.append("実測値の欠落 %d 件" % len(meas_miss))
if ident_broken or ident_absent:
    ng.append("識別子の語中改行/欠落 %d 件" % (len(ident_broken) + len(ident_absent)))
if watch_broken:
    ng.append("追加監視の識別子が割れている %d 件" % len(watch_broken))
if tail or head_us:
    ng.append("行末/行頭の _ による割れの疑い %d 件" % (len(tail) + len(head_us)))
if chap_miss:
    ng.append("章見出しの欠落 %d 件" % len(chap_miss))
if row_miss:
    ng.append("冒頭の構成表の欠落 %d 件" % len(row_miss))
if q_miss:
    ng.append("設問見出しの欠落 %d 件" % len(q_miss))
if q_extra:
    ng.append("原稿に無い設問番号 %d 件" % len(q_extra))
if q_zombie:
    ng.append("取り下げた設問の見出しが残っている %s" % q_zombie)
if q_mention:
    ng.append("取り下げた設問への言及が消えている %s" % q_mention)
if forbidden_hit:
    ng.append("取り下げた語が残っている %d 件" % len(forbidden_hit))
if word_ng:
    ng.append("語の文脈の検査 %d 件" % len(word_ng))
    for w in word_ng:
        print("  ★NG★", w)
if missing_r or missing_b or tofu:
    ng.append("グリフ欠け（豆腐）")
if blank:
    ng.append("空白ページ %s" % blank)
if leak or bad_ips or bad_creds or env_hit:
    ng.append("認証情報・想定外の接続情報")
if grant_missing:
    ng.append("10章の SHOW GRANTS の行が欠けている %d 件" % len(grant_missing))

if not tofu_checked:
    print("※ 8.（豆腐）は飛ばしている。判定には含めていない")
for n in ng:
    print("  ★NG★", n)
print("結果:", "OK" if not ng else "NG")
print("※ 表の列幅崩れ・改ページ位置は、この点検では見られない。"
      "全ページの目視確認は省かないこと（pdf/README.md）")
sys.exit(0 if not ng else 1)
