# -*- coding: utf-8 -*-
"""
TeamWorks 宛 確認事項（第9回）を PDF にする。

第1回〜第8回（make_ddl_review_pdf.py / make_teamworks_qa2_pdf.py 〜
make_teamworks_qa8_pdf.py）の体裁をそのまま踏襲した続編。
A4 縦 / reportlab / 日本語フォント埋め込み / フッター罫線とページ番号。

フォントは BIZ UDGothic（日本語フォント）を埋め込む。
中国語向けフォント（SimSun / Microsoft YaHei / Noto Sans SC / STSong 等）は
漢字の字形が異なるため使わない。フォールバックにも混ぜないこと（CLAUDE.md §0-3）。

BIZ UDGothic は等幅（半角は 0.5em 固定）なので、コードの枠にもこのフォントを使う。
コードブロック内の空白は &nbsp; に置換して桁を保持する。

原稿は docs/handoff/TeamWorks確認事項_第9回_20260915.md。
文言・件数・表の内容は原稿から変更していない。
原稿の ★（星）は PDF には出さず、太字で表現する（第2回からの作法）。

★2026-09-16 の改訂★
  原稿に12章「DB接続経路について — お願い3件・確認6問」が加わった
  （設問 20問 → 26問 / お願い 10件 → 13件）。
  初版（2026-09-15）の末尾にあった「参考: テスト環境について（申し送り）」は、
  ★12章に吸収したので削除した★（重複させない）。
  このスクリプトは .md を読まず本文を転記しているため、原稿を直したら
  ★必ずこちらも直すこと★。直さないと PDF だけ古いまま残る。

★2026-09-17（2回目）の改訂★
  ★★テスト環境のDBに入っているデータは、1行残らず当方（DFA）が投入した
  検証用のダミーだと判明した★★（調査は docs/handoff/テストDBのデータ出所_20260917.md）。
  ★先方由来なのは「器」＝スキーマ・索引・型・サーバー変数・権限だけ★なので、
  ★「行の中身」を根拠にしていた記述を原稿が全部取り下げた★。追随させたのは次のとおり。
  1. ★文書ヘッダの改訂点ブロックを2つにした★
     （新設の「2026-09-17（2回目）」＋既存を「（1回目）」に降格）。
  2. ★はじめに に「おことわり — テスト環境のデータは当方が入れたものでした」
     （2行の表つき）と「本書の構成」を新設★。中心は「11章・12章」→
     ★「12章。あわせて3章・4章・6章」★。
  3. 構成表の 2 / 5 / 6 / 10 / 11 章を差し替え（5章・10章は種別「ご報告」・
     優先度「—」）。★件数は 質問25問 / お願い5件 / 参考情報1章★。
  4. 1章 … 引用ブロック3つ（行数はダミー由来で、見るべきは type/possible_keys/key
     という構造）＋1-4・1-5 の注記。計算過程の「1.45回」は★実測→当方の置き値★へ。
  5. ★2章 … 実測表（6列4行）を削除★。2-1 を全面差し替え、★Q2-2 を削除★、
     Q2-1 の選択肢を3行→2行。
  6. ★5章 … 章ごと差し替え（設問・表なし）★。
  7. 6章 … 「なぜ「清掃実施回数」が要るのか（Q6-2 の理由）」を新設。
  8. 7章 … 7-1 のコード枠を削除、Q7-4 のコード枠を2クエリに減らし引用ブロックを追加。
  9. 8章 … 8-1 の表を差し替え、★Q8-3 を「固定値」→「下限・上限・変更の頻度」★へ。
  10. 9章 … Q9-1 の末尾2行を差し替え。
  11. ★10章 … 章ごと差し替え（すでに発行済み。SHOW GRANTS のコード枠つき）★。
      ★IDENTIFIED BY PASSWORD のハッシュは絶対に載せない★（原稿も伏せてある）。
  12. ★11章 … 章タイトルを「清掃の開始時刻と、リアルタイム性について」に。
      Q11-1 は「取り下げ」ではなく★実装の確認★として作り直し、delay 集計表と
      SQL枠を削除、★Q11-4 を新設★★。
      ★★Q11-1 は生きた設問である（質問25問に数える）。「取り下げました」
        「解決しましたので取り下げ」「残り3問に縮小」と書かないこと★★ —
      ★初版の改訂ではそう書いてしまい、同じ節に生きた Q11-1 がある矛盾になった。
        2026-09-17 に本文側が直し、こちらも追随している。
        点検スクリプトの 12. の FORBIDDEN が旧文言の残存を見る★
  13. 末尾に「取り下げた項目」と★「取り下げではなく、お伺いする内容を改めた項目」★の
      2段落を追加（★Q11-1 は後者★）。
  ★12章は1文字も変えていない。★
  ★点検スクリプトの 12. の FORBIDDEN が、取り下げた語（2章の実測表・Q2-2・Q5-1・
    旧Q11-1・10章の依頼文）の残存を見る。文言を戻す場合はそちらも直すこと。★

★2026-09-17（1回目）の改訂★
  原稿が2点の新情報で書き換わった（★索引は TeamWorks が対応中★ /
  ★現状のロボット台数は30台未満★）。追随させたのは次のとおり。
  1. ★1章を「ご依頼（7件）・最優先」から「索引について（参考情報）」へ格下げ★。
     1-0（本章の位置づけ）を新設し、1-2〜1-5 の見出しから「ご依頼」を外し、
     1-6 を「①〜⑦の一覧（突き合わせ用）」の表に差し替えた。
  2. ★規模の前提を「施設200件」から「ロボット30台」に置き直した★
     （1-2 の走査行数表 2年＝約193万行 / 1-4 / 2-4 の桁あふれ 約9,100㎡）。
     計算過程のコード枠を 1-2・1-4・2-4 に足している。
  3. ★8章を「本番規模の見積もり」から「制御盤の報告間隔」に差し替え★。
     Q8-1・Q8-2 を削除（設問 28問 → ★26問★ / お願い 13件 → ★6件★）。
     ★番号は詰めず Q8-3 のまま★（原稿の断り書きどおり）。
  4. 文書ヘッダに「2026-09-17 の改訂点」の引用ブロックを足し、はじめに・
     構成表・おわりにを差し替えた。
  ★12章は1文字も変えていない。★
  ★点検スクリプト（check_teamworks_qa9_pdf.py）の 12. が、取り下げた語
    （「索引のご依頼」「ご依頼①②」「本番規模の見積もり」等）が PDF に
    残っていないかを見る。文言を戻す場合はそちらも直すこと。★

★第8回から足したもの★
  なし。部品（st / table / form_table / code / quote / callout / qhead /
  blank_box / bull / on_page）は make_teamworks_qa8_pdf.py をそのまま流用し、
  表データと build() の本文だけ差し替えている。

★原稿との差分（意図的なもの。文言は足していない）★
  1. 巻末に「ご回答欄」を付けた。第2回〜第8回と同じ作法。
     第9回の原稿には節としては無いが、設問の採番と要約だけを転記したもので、
     新しい主張は書いていない。
  2. 原稿の「## 2-2.」直下は Q2-1 / Q2-3 の2問で、見出しの「（2問）」と一致する
     （★2026-09-17 の2回目の改訂で Q2-2 を取り下げた。番号は詰めていない★）。
  3. 原稿の7章は Q7-5 が Q7-4 より前に置かれている。★原稿の順序のまま出す★。
     ★2026-09-17 の改訂で、Q7-5 の直前にその断り書き（引用ブロック）が
       原稿に加わった。順序を入れ替えるのではなく、断り書きごと出すこと★
  4. 原稿のはじめにの構成表は 1〜12 の12行。
     「次の12部構成です。」という本文も含め、原稿のまま出す。
     ★「N部構成」の N・構成表の行数・本文の章数の3つは必ず一致させること★
     （点検スクリプトの 7. がこの3つを別々に数えて突き合わせる）。

★列幅の決め方（第7回・第8回と同じ）★
wordWrap="CJK" は ASCII の語中でも改行する。そのため長い識別子は
  (1) 行頭に来るよう <br/> で明示的に改行してから置く
  (2) 列の内寸（列幅 - 左右パディング 12pt）が識別子の幅
      （BIZ UDGothic は半角 0.5em なので フォントサイズ × 文字数 ÷ 2）
      を上回るように列幅を決める
の2点で語中改行を避ける。

第9回でいちばん長い識別子は
  clean_status_histories_clean_timetable_id_index （47字）
  robot_error_reports_clean_timetable_id_index    （43字）
の2本で、いずれも ALTER TABLE 文（コード枠）の中にしか出てこない。
コード枠の内寸は 168mm - 14pt = 462pt、8.2pt での 47字は 192.7pt なので余裕がある。
本文・表に出る長い識別子は
  customers_group_id_foreign      （26字 → 8.8pt で 114.4pt = 40.4mm）
  control_reports_control_id_foreign（34字 → 9.5pt で 161.5pt = 57.0mm）
  facilities.customer_id          （22字 → 8.8pt で  96.8pt = 34.2mm）
  clean_status_histories          （22字）
で、それぞれ 7-4 の選択肢表の「内容」列 68mm、1-5 の本文（<br/> で行頭に落とす）、
7-5 の解釈表の「DBでの表現」列 90mm に収めている。
"""
import os

from reportlab.lib import colors
from reportlab.lib.enums import TA_LEFT
from reportlab.lib.pagesizes import A4
from reportlab.lib.styles import ParagraphStyle, getSampleStyleSheet
from reportlab.lib.units import mm
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont
from reportlab.lib.fonts import addMapping
from reportlab.platypus import (
    BaseDocTemplate, CondPageBreak, Frame, PageBreak, PageTemplate, Paragraph,
    Spacer, Table, TableStyle,
)

FONT_DIR = "C:/Windows/Fonts/"
pdfmetrics.registerFont(TTFont("JP", FONT_DIR + "BIZ-UDGothicR.ttc", subfontIndex=0))
pdfmetrics.registerFont(TTFont("JP-B", FONT_DIR + "BIZ-UDGothicB.ttc", subfontIndex=0))
# <b> タグを太字フォントへ対応づける
addMapping("JP", 0, 0, "JP")
addMapping("JP", 1, 0, "JP-B")

# 出力先。既定はこのスクリプトと同じディレクトリ。
# 環境変数 DWATCH_PDF_OUT で変えられる（例: 送付前に Downloads へ出す）。
OUT_DIR = os.environ.get("DWATCH_PDF_OUT",
                         os.path.dirname(os.path.abspath(__file__)))
OUT = os.path.join(OUT_DIR, "DWatch_確認事項_第9回_20260915.pdf")

# 第1回 PDF と同一のパレット
INK = colors.HexColor("#1e293b")
SUB = colors.HexColor("#475569")
MUT = colors.HexColor("#94a3b8")
LINE = colors.HexColor("#cbd5e1")
HEAD_BG = colors.HexColor("#f1f5f9")
CODE_BG = colors.HexColor("#f8fafc")
NG = colors.HexColor("#b91c1c")   # 実装が止まる区分の強調色
WN = colors.HexColor("#b45309")   # 定義の確認区分の強調色

ss = getSampleStyleSheet()


def st(name, size, leading, font="JP", color=INK, space_after=0, space_before=0,
       left=0, first=0):
    """段落スタイルを作る。left/first でぶら下げインデントを表現する。"""
    s = ParagraphStyle(
        name, parent=ss["Normal"], fontName=font, fontSize=size, leading=leading,
        textColor=color, alignment=TA_LEFT, spaceAfter=space_after,
        spaceBefore=space_before, wordWrap="CJK",
        leftIndent=left, firstLineIndent=first,
    )
    s.bulletFontName = font
    return s


S_TITLE = st("t", 17, 24, "JP-B", INK, 2)
S_SUB = st("s", 9.5, 15, "JP", SUB, 0)
S_H2 = st("h2", 12.5, 18, "JP-B", INK, 4, 10)
S_H3 = st("h3", 10.5, 16, "JP-B", INK, 3, 8)
S_H4 = st("h4", 9.8, 15, "JP-B", SUB, 2, 6)
S_BODY = st("b", 9.5, 15.5, "JP", INK, 4)
S_LEAD = st("ld", 9.5, 15.5, "JP", INK, 2)          # 直後にコード枠や表が続く導入文
S_NOTE = st("n", 9, 14.5, "JP", SUB, 4)
S_LIST = st("li", 9.5, 15.5, "JP", INK, 2, 0, 15, -15)   # 「1. 」のぶら下げ
S_BULL = st("bu", 9.5, 15.5, "JP", INK, 2, 0, 14, -14)   # 「・」のぶら下げ
S_CELL = st("c", 8.8, 13.8, "JP", INK)
S_CELL_S = st("cs", 8.8, 13.8, "JP", SUB)
S_CODE = st("cd", 8.2, 12.6, "JP", INK)
S_TH = st("th", 8.4, 12, "JP-B", SUB)
S_QUOTE = st("q", 9.2, 14.8, "JP", SUB)
S_CALL = st("ca", 10.5, 17, "JP-B", INK)   # 目立たせたい一文を囲む枠の中身
S_QH = st("qh", 10, 15.5, "JP-B", INK)     # 設問（Q2-1 …）の見出し帯


def P(t, s=S_CELL):
    return Paragraph(t, s)


def on_page(canvas, doc):
    """全ページ共通のフッター罫線・ページ番号。第1回の PDF と同一。"""
    canvas.saveState()
    canvas.setFont("JP", 8)
    canvas.setFillColor(MUT)
    canvas.drawString(18 * mm, 12 * mm, "D-Watch / DFA Robotics")
    canvas.drawRightString(A4[0] - 18 * mm, 12 * mm, "%d" % doc.page)
    canvas.setStrokeColor(LINE)
    canvas.setLineWidth(0.4)
    canvas.line(18 * mm, 15.5 * mm, A4[0] - 18 * mm, 15.5 * mm)
    canvas.restoreState()


TBL_STYLE = [
    ("FONTNAME", (0, 0), (-1, -1), "JP"),
    ("BACKGROUND", (0, 0), (-1, 0), HEAD_BG),
    ("LINEBELOW", (0, 0), (-1, 0), 0.6, LINE),
    ("LINEBELOW", (0, 1), (-1, -2), 0.3, LINE),
    ("VALIGN", (0, 0), (-1, -1), "TOP"),
    ("TOPPADDING", (0, 0), (-1, -1), 5),
    ("BOTTOMPADDING", (0, 0), (-1, -1), 5),
    ("LEFTPADDING", (0, 0), (-1, -1), 6),
    ("RIGHTPADDING", (0, 0), (-1, -1), 6),
]

# 空欄にご記入いただく表。空のセルにも枠が見えるよう GRID を引く。
FORM_STYLE = [
    ("FONTNAME", (0, 0), (-1, -1), "JP"),
    ("BACKGROUND", (0, 0), (-1, 0), HEAD_BG),
    ("GRID", (0, 0), (-1, -1), 0.3, LINE),
    ("LINEBELOW", (0, 0), (-1, 0), 0.6, LINE),
    ("VALIGN", (0, 0), (-1, -1), "TOP"),
    ("TOPPADDING", (0, 0), (-1, -1), 7),
    ("BOTTOMPADDING", (0, 0), (-1, -1), 7),
    ("LEFTPADDING", (0, 0), (-1, -1), 6),
    ("RIGHTPADDING", (0, 0), (-1, -1), 6),
]


def table(rows, widths, cell_styles=None, head_color=None):
    """rows[0] を見出し行とする表。cell_styles で列ごとの段落スタイルを指定する。"""
    if cell_styles is None:
        cell_styles = [S_CELL] * len(rows[0])
    data = [[P(c, S_TH) for c in rows[0]]]
    for r in rows[1:]:
        data.append([P(c, cell_styles[i]) for i, c in enumerate(r)])
    t = Table(data, colWidths=widths, repeatRows=1)
    style = list(TBL_STYLE)
    if head_color is not None:
        style.append(("TEXTCOLOR", (0, 1), (0, -1), head_color))
    t.setStyle(TableStyle(style))
    return t


def form_table(rows, widths, cell_styles=None):
    """ご記入いただく表。table() に GRID を足しただけ。空セルは &nbsp; で高さを持たせる。"""
    if cell_styles is None:
        cell_styles = [S_CELL] * len(rows[0])
    data = [[P(c, S_TH) for c in rows[0]]]
    for r in rows[1:]:
        data.append([P(c if c else "&nbsp;", cell_styles[i])
                     for i, c in enumerate(r)])
    t = Table(data, colWidths=widths, repeatRows=1)
    t.setStyle(TableStyle(list(FORM_STYLE)))
    return t


def code(lines, width):
    """コードを等幅フォントの枠に入れる。空白は &nbsp; にして桁を保つ。"""
    esc = []
    for ln in lines:
        ln = ln.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
        esc.append(ln.replace(" ", "&nbsp;"))
    t = Table([[P("<br/>".join(esc), S_CODE)]], colWidths=[width])
    t.setStyle(TableStyle([
        ("FONTNAME", (0, 0), (-1, -1), "JP"),
        ("BACKGROUND", (0, 0), (-1, -1), CODE_BG),
        ("LINEBEFORE", (0, 0), (0, -1), 1.8, LINE),
        ("VALIGN", (0, 0), (-1, -1), "TOP"),
        ("TOPPADDING", (0, 0), (-1, -1), 6),
        ("BOTTOMPADDING", (0, 0), (-1, -1), 6),
        ("LEFTPADDING", (0, 0), (-1, -1), 8),
        ("RIGHTPADDING", (0, 0), (-1, -1), 6),
    ]))
    return t


def quote(text, width):
    """補足・お断りの引用。左に縦罫線を引く。"""
    t = Table([[P(text, S_QUOTE)]], colWidths=[width])
    t.setStyle(TableStyle([
        ("FONTNAME", (0, 0), (-1, -1), "JP"),
        ("LINEBEFORE", (0, 0), (0, -1), 1.8, MUT),
        ("VALIGN", (0, 0), (-1, -1), "TOP"),
        ("TOPPADDING", (0, 0), (-1, -1), 3),
        ("BOTTOMPADDING", (0, 0), (-1, -1), 3),
        ("LEFTPADDING", (0, 0), (-1, -1), 8),
        ("RIGHTPADDING", (0, 0), (-1, -1), 0),
    ]))
    return t


def callout(text, width):
    """一番伝えたい一文を薄い背景の枠に入れる（引用ではないので本文色）。"""
    t = Table([[P(text, S_CALL)]], colWidths=[width])
    t.setStyle(TableStyle([
        ("FONTNAME", (0, 0), (-1, -1), "JP"),
        ("BACKGROUND", (0, 0), (-1, -1), CODE_BG),
        ("LINEBEFORE", (0, 0), (0, -1), 1.8, MUT),
        ("VALIGN", (0, 0), (-1, -1), "TOP"),
        ("TOPPADDING", (0, 0), (-1, -1), 7),
        ("BOTTOMPADDING", (0, 0), (-1, -1), 7),
        ("LEFTPADDING", (0, 0), (-1, -1), 10),
        ("RIGHTPADDING", (0, 0), (-1, -1), 8),
    ]))
    return t


def qhead(text, width):
    """設問（Q2-1 …）の見出し帯。25問あるので節見出しと見分けがつくようにする。"""
    t = Table([[P(text, S_QH)]], colWidths=[width])
    t.setStyle(TableStyle([
        ("FONTNAME", (0, 0), (-1, -1), "JP"),
        ("BACKGROUND", (0, 0), (-1, -1), HEAD_BG),
        ("LINEBEFORE", (0, 0), (0, -1), 2.2, MUT),
        ("VALIGN", (0, 0), (-1, -1), "TOP"),
        ("TOPPADDING", (0, 0), (-1, -1), 5),
        ("BOTTOMPADDING", (0, 0), (-1, -1), 5),
        ("LEFTPADDING", (0, 0), (-1, -1), 8),
        ("RIGHTPADDING", (0, 0), (-1, -1), 8),
    ]))
    return t


def blank_box(width, height):
    """自由記入欄の空枠。"""
    t = Table([[P("&nbsp;", S_CELL)]], colWidths=[width], rowHeights=[height])
    t.setStyle(TableStyle([
        ("FONTNAME", (0, 0), (-1, -1), "JP"),
        ("BOX", (0, 0), (-1, -1), 0.5, LINE),
        ("VALIGN", (0, 0), (-1, -1), "TOP"),
    ]))
    return t


def bull(items):
    """「・」形式の箇条書き。"""
    return [Paragraph("・%s" % t, S_BULL) for t in items]


def numlist(items):
    """「1. 」形式の番号付き箇条書き。"""
    return [Paragraph("%d. %s" % (i + 1, t), S_LIST)
            for i, t in enumerate(items)]


# ---------------------------------------------------------------- 表データ
# はじめに: おことわり（2026-09-17 の2回目の改訂で新設）
DISCLAIM_ROWS = [
    ["種別", "例", "本書での扱い"],
    ["<b>構造から分かること</b>",
     "DDL / SHOW CREATE TABLE / 索引の有無 / possible_keys = null / "
     "カラムの有無・型 / サーバー変数 / 権限",
     "<b>そのまま根拠として記載しております</b>"],
    ["<b>行の中身から<br/>読み取っていたこと</b>",
     "行数・値の分布・「全件が同じ値」「0行である」等",
     "<b>取り下げるか、「当方の検証用データでの観測です」と"
     "明記しております</b>"],
]

# はじめに: 12部構成（原稿の表のまま）
SUMMARY_ROWS = [
    ["#", "内容", "種別", "優先度"],
    ["<b>1. 索引について</b>",
     "<b>ご対応中と伺いました。当方の EXPLAIN 実測と、"
     "当方が見ていた列の一覧です</b>",
     "<b>参考情報</b>", "<b>—</b>"],
    ["<b>2. サマリの面積は期間内の合計でしょうか</b>",
     "<b>仕様の確認のみに縮めました</b>", "質問", "<b>高</b>"],
    ["<b>3. 第8回 Q4 の残り<br/>（サマリの率・分子・分母）</b>",
     "<b>再掲</b>", "質問", "高"],
    ["<b>4. サマリの error_count の集計範囲</b>",
     "<b>当期と確定期間で定義が食い違う恐れ</b>", "質問", "高"],
    ["<b>5. 面積の単位</b>",
     "<b>弊社で㎡と確定しました。設問は取り下げです</b>",
     "<b>ご報告</b>", "<b>—</b>"],
    ["<b>6. グループ単位のサマリ</b>",
     "<b>group_summaries が存在しません</b>", "質問", "<b>高</b>"],
    ["<b>7. グループ・顧客マスタのデータ投入</b>",
     "<b>画面に分類を出すために必要です</b>", "<b>お願い</b>", "中"],
    ["<b>8. 制御盤の報告間隔</b>",
     "<b>control_reports の行数を決めます。台数は当方で把握しました</b>",
     "質問", "中"],
    ["<b>9. 軽い確認2件 + DDL未反映2件</b>",
     "<b>DDLは4回目のお願いです</b>", "質問", "低"],
    ["<b>10. DB更新用アカウント</b>",
     "<b>発行済みでした。ご依頼を取り下げます</b>",
     "<b>ご報告</b>", "<b>—</b>"],
    ["<b>11. 清掃の開始時刻と<br/>リアルタイム性</b>",
     "<b>Q11-1 は「実装の確認」に改めました。Q11-4 を新設（4問）</b>",
     "質問", "中"],
    ["<b>12. DB接続経路についての<br/>お願い3件・確認6問</b>",
     "<b>実測つき。期限は「ステージングに実データが入る前」です</b>",
     "<b>お願い＋質問</b>", "<b>高</b>"],
]

# 1-2. 影響の大きさ（★2026-09-17 にロボット30台の前提で置き直した★）
SCAN_ROWS = [
    ["表示期間", "塊の数",
     "走査行数<br/>（clean_status_histories・<b>ロボット30台で外挿</b>）"],
    ["今日1日", "1", "約 4.4万行"],
    ["<b>2年（731日）</b>", "<b>44</b>", "<b>約 193万行</b>"],
]

# 1-6. ①〜⑦の一覧（突き合わせ用。2026-09-17 に新設）
# ★列幅★ control_error_histories（24字 = 8.8pt で 105.6pt = 37.3mm）が
# 語中で割れないよう「テーブル」列の内寸（列幅 - 4.2mm）でこれを上回るようにする。
INDEX_LIST_ROWS = [
    ["#", "テーブル", "当方が見ていた列<br/>（順序も含みます）",
     "2026-09-13 時点の実測"],
    ["<b>①</b>", "clean_status_histories", "clean_timetable_id",
     "<b>索引なし（possible_keys = null）</b>"],
    ["<b>②</b>", "robot_error_reports", "clean_timetable_id",
     "<b>索引なし（possible_keys = null）</b>"],
    ["<b>③</b>", "clean_timetables", "(precheck, start_at)",
     "単一列索引はあるが実行計画で未使用"],
    ["<b>④</b>", "clean_timetables", "(facility_id, start_at)", "同上"],
    ["<b>⑤</b>", "clean_status_histories", "started_at", "<b>索引なし</b>"],
    ["<b>⑥</b>", "control_reports", "(control_id, reported_at)",
     "(control_id) のみ。<br/>MAX(reported_at) がフルスキャン"],
    ["<b>⑦</b>", "control_error_histories", "occurred_at", "<b>索引なし</b>"],
]

# 1-5. ⑥の根拠
MAXCOL_ROWS = [
    ["MAX() の対象", "索引に含まれるか", "type", "rows", "Extra"],
    ["reported_at（現行）", "<b>含まれない</b>",
     "index（<b>フルスキャン</b>）", "14", "Using where"],
    ["id（参考の実験）", "<b>含まれる</b>", "<b>range</b>", "<b>3</b>",
     "<b>Using index for group-by</b>"],
]

# 2-2. Q2-1
# ★2026-09-17（2回目）の改訂で、2-1 の実測表（6列4行）は削除した。
#   テストDBの行は当方が入れたダミーだったため（原稿 2-1）。
#   選択肢の C（「まだバッチが動いていない」）も同時に消えて A / B の2行になった★
Q21_ROWS = [
    ["選択肢", "内容"],
    ["<b>A</b>",
     "<b>期間内の合計を書く</b>"
     "（当方はサマリをそのまま足し合わせて総面積を出します）"],
    ["B", "<b>合計ではない</b>（1回ぶんの代表値・平均など。その場合、定義をお教えください）"],
]

Q23_ROWS = [
    ["選択肢", "内容"],
    ["<b>A</b>", "<b>必ず1行</b>（清掃1回につき1行）"],
    ["B", "<b>複数行できる</b>（清掃前 → 清掃中 → 清掃終了 のように状態ごとに積む）"],
]

# 2-4. 年次サマリの桁あふれ
OVERFLOW_ROWS = [
    ["前提", "上限に達する条件"],
    ["<b>ロボット30台・年365日</b>",
     "<b>1台1日あたり 約9,100㎡ を超えると、年次行があふれます</b>"],
]

# 4. error_count
Q41_ROWS = [
    ["選択肢", "内容"],
    ["A", "<b>除外している</b>（本清掃 precheck = 0 のみを数えている）"],
    ["B", "<b>除外していない</b>（事前チェックのエラーも数えている）"],
]

# 6-1
Q61_ROWS = [
    ["選択肢", "内容"],
    ["<b>A</b>",
     "<b>当方が customer_summaries の複数行を合算する</b>（テーブル追加は不要）"],
    ["B", "<b>group_summaries を追加してバッチで集計する</b>"],
    ["C", "<b>グループ単位の集計は想定していない</b>"],
]

# 6-2
RATIO_ROWS = [
    ["率", "計算式（当方の定義）", "サマリの分子", "サマリの分母", "合算できるか"],
    ["清掃完了率<br/>clean_ratio", "Σ清掃面積 ÷ Σ対象面積", "clean_area",
     "task_area", "<b>できる</b>"],
    ["出発成功率<br/>launch_ratio", "定刻出発回数 ÷ 予定回数", "launch_count",
     "task_count ?", "<b>式が未確認</b>"],
    ["<b>稼働率<br/>operation_ratio</b>", "清掃<b>実施</b>回数 ÷ 予定回数",
     "<b>該当カラムが無い</b>", "task_count ?", "<b>できない</b>"],
    ["<b>エラー発生率<br/>error_ratio</b>",
     "エラー発生回数 ÷ 清掃<b>実施</b>回数", "error_count",
     "<b>該当カラムが無い</b>", "<b>できない</b>"],
]

# 6-2. なぜ「清掃実施回数」が要るのか（2026-09-17 の2回目の改訂で新設）
TASKCOUNT_ROWS = [
    ["task_count が何であれば", "困ること"],
    ["<b>予定回数</b>（当方の理解）",
     "<b>実施回数が無い</b> → "
     "<b>稼働率の分子・エラー発生率の分母が出せません</b>"],
    ["<b>実施回数</b>",
     "<b>予定回数が無い</b> → <b>稼働率の分母が出せません</b>"],
]

Q62_ROWS = [
    ["選択肢", "内容"],
    ["<b>A</b>",
     "<b>追加できる</b>（customer_summaries / facility_summaries に1カラムずつ）"],
    ["B", "<b>すでに既存のカラムで表現されている</b>（どれか教えてください）"],
    ["C", "<b>追加は難しい</b>"],
]

# 7-2. Q7-1
Q71_ROWS = [
    ["選択肢", "例"],
    ["<b>A</b>",
     "<b>屋号</b>（ドン・キホーテ / MEGAドン・キホーテ / アピタ / ピアゴ …）"],
    ["B", "<b>法人</b>（株式会社ドン・キホーテ / 株式会社ユニー）"],
]

# 7-2. Q7-5
LAYER_ROWS = [
    ["解釈", "空くのは", "例", "DBでの表現"],
    ["<b>①</b>", "<b>真ん中（屋号）</b>",
     "店舗がホールディングスの直下にぶら下がる",
     "<b>表現できません</b> — facilities.customer_id は NOT NULL で "
     "customers を指すため、中間を飛ばせません"],
    ["<b>②</b>", "<b>上（ホールディングス）</b>",
     "屋号はあるが、属する持株会社が無い",
     "customers.group_id = 0（＝ Q7-4 の件）"],
]

# 7-2. Q7-4
Q74_ROWS = [
    ["選択肢", "内容", "備考"],
    ["<b>A</b>", "<b>groups に id = 0 の番人行を1件入れる</b>",
     "<b>INSERT には NO_AUTO_VALUE_ON_ZERO が必要になります</b>"],
    ["B", "<b>外部キー制約 customers_group_id_foreign を外す</b>",
     "センチネル方式と両立します"],
    ["C", "<b>group_id を nullable にする</b>",
     "<b>索引効率を理由にセンチネル方式を採られた経緯があるため、"
     "蒸し返しになります</b>"],
    ["D", "<b>グループなしの顧客は存在しない</b>（必ずどこかのグループに属する）",
     "<b>この場合、当方は「グループなし」の表示を作りません</b>"],
]

# 8. 報告間隔
INTERVAL_ROWS = [
    ["報告間隔", "2年間の行数（<b>制御盤11台と置いた試算</b>）"],
    ["60秒（定義書の例）", "約 1,100万行"],
    ["<b>10秒</b>（第2回のご回答の例）", "<b>約 7,000万行</b>"],
]

# 8. Q8-3（2026-09-17 の2回目の改訂で「固定値」→「下限・上限・変更の頻度」）
Q83_ROWS = [
    ["#", "伺いたいこと", "例"],
    ["<b>a</b>", "<b>運用上の下限（最短の報告間隔）</b>",
     "「10秒より短くはしない」など"],
    ["<b>b</b>", "<b>運用上の上限（最長の報告間隔）</b>",
     "「長くても60秒」など"],
    ["<b>c</b>", "<b>変更の頻度</b>",
     "「初期設定から基本的に変えない」／"
     "「制御盤ごとに個別に決める」／「運用中に随時変える」など"],
]

# 8-1. 当方の把握（2026-09-17 に新設。旧 Q8-1 / Q8-2 の差し替え）
# ★2回目の改訂で「テスト環境の controls は11行」等の行数根拠を外した
#   （テストDBの行は当方が入れたダミーだったため）★
SCALE_ROWS = [
    ["項目", "当方の把握", "出典"],
    ["<b>ロボット台数</b>", "<b>現状 30台未満</b>",
     "<b>弊社（DFA）の把握。2026-09-17</b>"],
    ["D-S制御盤の台数",
     "<b>未確定</b>（試算では <b>11台</b> と置いております）",
     "<b>弊社で確定させます</b>"],
    ["施設数", "<b>未確定</b>", "<b>弊社で確定させます</b>"],
]

# 9. DDL 未反映
DDL_ROWS = [
    ["#", "内容"],
    ["1", "clean_status_histories.id の AUTO_INCREMENT"],
    ["2", "security_status_histories.reported_at の DEFAULT"],
]

# 11. Q11-1（2026-09-17 の2回目の改訂で作り直した）
# ★旧 Q11-1「実開始時刻はどこにありますか」（A / B / C の選択肢表）と、
#   11-2 の delay 集計表・SQL 枠は削除した。started_at を使う形で解決したため★
Q111_ROWS = [
    ["ご回答", "当方の動き"],
    ["<b>A. その理解で問題ない</b>",
     "<b>このまま実装します</b>（<b>ご回答はこの一言で結構です</b>）"],
    ["<b>B. started_at には予定時刻が入る</b>",
     "<b>実装を止め、実開始時刻の所在を改めてご相談します</b>"],
]

# 11. Q11-4（2026-09-17 の2回目の改訂で新設）
Q114_ROWS = [
    ["#", "伺いたいこと"],
    ["<b>a</b>", "<b>そのテーブル・カラム名</b>"],
    ["<b>b</b>",
     "<b>おおよその更新間隔</b>"
     "（「数秒ごと」「1分ごと」など、おおよそで構いません）"],
    ["<b>c</b>", "<b>履歴として積まれるのか、上書きされるのか</b>"],
]

# 12-1. 期限の対比
DEADLINE_ROWS = [
    ["時点", "状態", "作り直しのコスト"],
    ["<b>いま<br/>（検証用データのみ）</b>",
     "<b>実データは入っておりません</b>",
     "<b>ほぼゼロ</b>。アカウントを作り直しても、失うものがありません"],
    ["<b>ステージングに<br/>実データが入った後</b>",
     "<b>平文の経路を通った資格情報を前提に、"
     "運用が始まってしまいます</b>",
     "<b>「作り直す」しか手がなくなります</b>"],
]

# 12-2. 実測のメタ情報（いつ・誰が・どう測ったか）
MEASURE_META_ROWS = [
    ["項目", "内容"],
    ["<b>対象</b>",
     "<b>テスト環境 133.125.238.144:13306 / DB clean_watch</b>"
     "（本書1-1に記載のものと同じサーバーです）"],
    ["<b>サーバーの<br/>バージョン</b>",
     "<b>VERSION() = 10.11.19-MariaDB-ubu2204</b>"],
    ["<b>測った人</b>", "<b>DFA（本プロジェクトの作業者）</b>"],
    ["<b>測った日</b>",
     "<b>2026-09-13</b>（<b>2026-09-15 に本書の初版へ転記しました</b>）"],
    ["<b>測り方</b>",
     "<b>貴社より受領した参照用アカウント</b>（SELECT のみ）で"
     "テスト環境に接続し、<b>サーバー変数を照会した</b>"],
]

# 12-2. 実測値。★列幅は require_secure_transport（24字 = 8.8pt で 105.6pt
# = 37.3mm）が語中で割れないよう、内寸（列幅 - 左右パディング 12pt = 4.2mm）で
# これを上回るように 46mm にしている★
TESTENV_ROWS = [
    ["変数／設定", "実測値", "意味"],
    ["have_ssl", "<b>DISABLED</b>",
     "<b>サーバーが TLS を提供していません</b>"],
    ["require_secure_transport", "<b>OFF</b>",
     "<b>暗号化されていない接続を、サーバーが受け付けます</b>"],
    ["参照用アカウントの<br/>接続元ホスト", "<b>参照用アカウント@%</b>",
     "<b>% は「どこからでも」。接続元の制限がありません</b>"],
]

# 12-6. 当方の現在の理解と、その根拠
UNDERSTAND_ROWS = [
    ["#", "当方の理解", "根拠", "確認の有無"],
    ["1", "本番のDBは <b>MariaDB 10.11</b>",
     "<b>貴社のご回答（2026-09-10）"
     "「本番サーバはMariaDBです」</b>", "<b>確認済み</b>"],
    ["2", "DBは <b>さくらインターネットの<br/>データベースアプライアンス</b>",
     "同ご回答「さくらインターネットの"
     "データベースアプライアンスはPostgreSQLかMariaDBなので」",
     "<b>確認済み</b>"],
    ["3", "アプリは <b>さくらのクラウド VM</b>"
     "（Node.js 常駐が必要なため）",
     "<b>当方側の想定</b>", "<b>未確認</b>"],
    ["4", "<b>「アプリ＝VM」「DB＝アプライアンス」という"
     "別サービスの組み合わせである</b>",
     "<b>上記2・3からの読み取り</b>", "<b>未確認</b>"],
    ["5", "<b>VM と DB が同一ネットワークで閉域接続される</b>",
     "<b>当方側の想定</b>", "<b>未確認</b>"],
]

# 12-6. Q12-1 の選択肢
Q121_ROWS = [
    ["#", "対象", "選択肢"],
    ["<b>a</b>", "<b>アプリサーバー（Node.js を載せる VM）</b>",
     "<b>貴社が用意 / 弊社が用意 / 未定</b>"],
    ["<b>b</b>", "<b>DBサーバー（データベースアプライアンス）</b>",
     "<b>貴社が用意 / 弊社が用意 / 未定</b>"],
]

# ご回答欄（原稿の設問の採番と要約を転記したもの）
ANSWER_ROWS = [
    ["質問番号", "質問の要約", "ご回答"],
    ["<b>1章</b>",
     "<b>索引（参考情報）— ご対応が完了しましたら、その旨を<br/>"
     "お知らせください</b>", ""],
    ["<b>Q2-1</b>",
     "<b>集計バッチは task_area / clean_area に期間内の合計を<br/>"
     "書き込む仕様か（A / B）</b>", ""],
    # ★Q2-2 は 2026-09-17（2回目）に取り下げた。番号は詰めず Q2-3 のまま★
    ["<b>Q2-3</b>",
     "1つの clean_timetable_id に clean_status_histories は<br/>"
     "何行できるか（A / B）", ""],
    ["<b>3章</b>",
     "<b>第8回 Q4 の残り（率の計算式・分子・分母・件数列）</b>", ""],
    ["Q4-1", "error_count は事前チェックを除外しているか（A / B）", ""],
    ["Q4-2", "clean_timetable_id が null のエラー行は含まれるか", ""],
    ["Q4-3", "clean_error が 2 / 3 の清掃は含まれるか", ""],
    # ★Q5-1 は 2026-09-17（2回目）に取り下げた（弊社で ㎡ と確定）★
    ["Q6-1", "グループ単位の集計の想定（A / B / C）", ""],
    ["Q6-2", "「清掃実施回数」のカラムを追加できるか（A / B / C）", ""],
    ["<b>7章</b>",
     "<b>本番の groups / customers のデータ投入の時期の目安</b>", ""],
    ["Q7-1", "customers は法人と屋号のどちらの粒度か（A / B）", ""],
    ["Q7-2", "屋号を持たない顧客の customer_name に入る値", ""],
    ["Q7-3", "facilities.shop_id は何を表す列か", ""],
    ["Q7-5", "「屋号を持たない顧客」はどの段が空くか（① / ②）", ""],
    ["<b>Q7-4</b>",
     "<b>customers.group_id = 0 が投入できない件<br/>（A / B / C / D）</b>", ""],
    # ★Q8-1 / Q8-2 は 2026-09-17 に取り下げた（当方側で決めることのため）。
    #   番号は詰めず Q8-3 のまま残す（原稿8章の断り書きどおり）★
    ["Q8-3",
     "link_schedules.intervals の運用上の下限・上限・<br/>変更の頻度"
     "（a / b / c）", ""],
    ["Q9-1", "robot_task_reports は clean_task_reports のことか", ""],
    ["Q9-2", "1回の清掃の中の、区域別の内訳は取得できるか", ""],
    ["9章", "DDL 未反映の2件（AUTO_INCREMENT / DEFAULT）", ""],
    # ★10章は 2026-09-17（2回目）にご依頼を取り下げた（すでに発行済みでした）★
    ["<b>Q11-1</b>",
     "<b>clean_status_histories.started_at を実開始時刻として<br/>"
     "実装してよいか（A / B）</b>", ""],
    ["Q11-2", "clean_status_trans = 1 で止まったケースは履歴に残るか", ""],
    ["<b>Q11-3</b>",
     "<b>launch_count / launch_ratio の計算式</b>", ""],
    ["Q11-4",
     "started_at より短い間隔で更新されるテーブル・カラムは<br/>"
     "あるか（a / b / c）", ""],
    ["<b>12章</b>",
     "<b>お願い3件: 接続元IPの制限 / TLS の有効化 /<br/>"
     "本番用DBアカウントの新規発行</b>", ""],
    ["<b>Q12-1</b>",
     "<b>アプリVM（a）とDBサーバー（b）は、それぞれ<br/>"
     "どちらが用意するか</b>", ""],
    ["Q12-2", "本番のDBはデータベースアプライアンスか<br/>（YES / NO）", ""],
    ["Q12-3", "本番のアプリはさくらのクラウドのVMか<br/>（YES / NO）", ""],
    ["<b>Q12-4</b>",
     "<b>VM と DBアプライアンスは同一の閉域ネットワークで<br/>"
     "結べるか（YES / NO）</b>", ""],
    ["Q12-5", "YES の場合、公開IPからの接続を完全に閉じられるか"
     "<br/>（YES / NO / 条件つき）", ""],
    ["Q12-6", "NO の場合、TLS を有効にできるか"
     "<br/>（YES / NO / 該当なし）", ""],
]


def build():
    doc = BaseDocTemplate(
        OUT, pagesize=A4,
        leftMargin=18 * mm, rightMargin=18 * mm,
        topMargin=18 * mm, bottomMargin=20 * mm,
        title="D-Watch 確認事項（第9回）",
        author="DFA Robotics",
        subject="サマリの面積・実際の清掃開始時刻・DB接続経路ほかのご確認",
    )
    frame = Frame(doc.leftMargin, doc.bottomMargin, doc.width, doc.height, id="f")
    doc.addPageTemplates([PageTemplate(id="p", frames=[frame], onPage=on_page)])

    W = doc.width          # 174mm
    CW = W - 6 * mm        # 引用・コード枠の幅
    story = []

    # ---------------------------------------------------------- 表題と前文
    story.append(Paragraph("確認事項（第9回）", S_TITLE))
    story.append(Paragraph(
        # ★2026-09-17 の日付が増えて1行に収まらなくなったので、
        #   「作成者:」の前で明示的に改行する（自動折り返しだと
        #   DFA / Robotics が割れる）★
        "宛先: TeamWorks（DB開発側）　作成: 2026-09-17 改訂"
        "（初版 2026-09-15 / 統合 2026-09-16）<br/>"
        "作成者: DFA Robotics", S_SUB))
    story.append(Spacer(1, 5))
    story.append(quote(
        "<b>2026-09-17（2回目）の改訂点</b>: "
        "<b>テスト環境のDBに入っているデータは、当方（DFA）が入れた"
        "検証用のダミーデータでありました</b>。"
        "<b>つきましては「行の中身」を根拠にしていた記述を、"
        "取り下げるか、当方の検証用データでの観測である旨を明記する"
        "形に改めております</b> / "
        "<b>2章は実データの話を削り、仕様の確認のみに縮めました"
        "（Q2-2 は取り下げ）</b> / "
        "<b>5章（面積の単位）は弊社で「㎡」と確定しましたので、"
        "設問（Q5-1）を取り下げました</b> / "
        "<b>8章 Q8-3 は「固定値」ではなく「運用上の下限・上限・変更の頻度」"
        "を伺う形に改めました</b> / "
        "<b>10章（DB更新用アカウント）はすでに権限が発行されておりました。"
        "当方の確認漏れです。ご依頼を取り下げました</b> / "
        "<b>11章 Q11-1（実際の清掃開始時刻）は お伺いする内容を改めました</b>。"
        "「どこにありますか」ではなく "
        "<b>「started_at を使って実装してよろしいでしょうか」</b> という"
        "確認に変えております。あわせて <b>Q11-4</b> を新たに立てました",
        CW))
    story.append(Spacer(1, 5))
    story.append(quote(
        "<b>2026-09-17（1回目）の改訂点</b>: "
        "<b>索引（1章）はご対応中と伺いましたので、ご依頼を取り下げ"
        "参考情報に改めました</b> / "
        "<b>規模の前提を「施設200件（仮置き）」から"
        "「ロボット30台未満（弊社把握）」に置き直しました（1章・2-4）</b> / "
        "<b>台数・施設数の設問（旧 Q8-1 / Q8-2）を取り下げました"
        "（弊社側で決めるべきことのため）</b>", CW))
    story.append(Spacer(1, 5))
    story.append(quote(
        "<b>本書は、第9回確認事項に、DB接続経路についての申し送り"
        "（お願い3件・確認6問）を統合したものです。</b><br/>"
        "<b>別便は出しません。第9回として1通にまとめてお送りいたします。"
        "統合した内容は12章です。</b>", CW))
    story.append(Spacer(1, 9))

    story.append(Paragraph("はじめに", S_H2))
    story.append(Paragraph(
        "<b>第8回（2026-09-11）にはご回答をいただき、ありがとうございました。</b>"
        "<b>いただいた回答をもとに、タイムゾーン・エラー率・"
        "エリア別完了率の実装を直しております。</b>", S_BODY))
    story.append(Paragraph(
        "<b>第8回の Q4（サマリの率・分子・分母の関係）は"
        "「多いので後で回答」との留保をいただいたままです。"
        "取り下げておりませんので、引き続きお願いいたします"
        "（本書の3章に再掲します）。</b>", S_BODY))
    story.append(Paragraph(
        "そのうえで、<b>テスト環境のDBに接続して実測を行ったところ、"
        "確認事項が新たに発生しました。</b>", S_BODY))

    # -------------------------------- おことわり（2026-09-17 の2回目の改訂で新設）
    story.append(CondPageBreak(90 * mm))
    story.append(Paragraph(
        "おことわり — テスト環境のデータは当方が入れたものでした", S_H2))
    story.append(Paragraph(
        "<b>本書の初版（2026-09-15）では、テスト環境のDBの「行の中身」を"
        "根拠にした記述がございました。</b>"
        "<b>その後、当方で確認いたしましたところ、テスト環境に入っている"
        "データは当方（DFA）が開発用に投入した検証用のダミーデータでした。"
        "</b>", S_BODY))
    story.append(Paragraph(
        "<b>そのため、行数・値の分布・特定のテーブルが0行であること等を"
        "根拠にした指摘は、貴社のバッチや収集処理の挙動を"
        "示すものではありませんでした。</b>", S_BODY))
    story.append(Paragraph(
        "<b>本改訂では、これらを次のように仕分けております。</b>", S_LEAD))
    story.append(Spacer(1, 3))
    story.append(table(DISCLAIM_ROWS, [40 * mm, 66 * mm, W - 106 * mm],
                       [S_CELL, S_CELL, S_CELL]))
    story.append(Spacer(1, 6))
    story.append(Paragraph(
        "<b>紛らわしいことを申し上げ、失礼いたしました。</b>", S_BODY))

    # -------------------------------- 本書の構成
    story.append(CondPageBreak(60 * mm))
    story.append(Paragraph("本書の構成", S_H2))
    story.append(Spacer(1, 2))
    story.append(callout(
        "<b>索引の件については、ご対応中と伺いました。</b><br/>"
        "<b>つきましては1章はご依頼を取り下げ、参考情報に改めております"
        "（詳細は1章の冒頭に記しました）。</b><br/>"
        "<b>そのうえで本書の中心は、12章（DB接続経路）です。</b><br/>"
        "<b>12章は期限が「ステージングに実データが入る前」という"
        "性質のもので、過ぎますと作り直しになります。</b>", CW))
    story.append(Spacer(1, 6))
    story.append(Paragraph(
        "<b>あわせて、3章・4章・6章（サマリの計算式と分子・分母）が"
        "当方の実装に直結いたします。</b>"
        "<b>11章は、Q11-1（実際の清掃開始時刻）について、お伺いする内容を"
        "「どこにありますか」から "
        "<b>「この理解で実装してよろしいでしょうか」</b> に"
        "改めております。</b>", S_BODY))
    story.append(Spacer(1, 2))
    # ★12行になって表の高さが 147mm。表紙の前文と合わせると1ページに収まらず、
    #   最終行（12章）だけが次ページに落ちる。表は目次なので割らずに送り、
    #   導入の一文（「次の12部構成です。」）も表と一緒に送る★
    story.append(CondPageBreak(157 * mm))
    story.append(Paragraph(
        "本書は次の12部構成です"
        "（<b>質問25問 / お願い5件 / 参考情報1章</b>）。", S_LEAD))
    story.append(Spacer(1, 3))
    # 種別の列は「お願い＋質問」（6字 = 8.8pt で 52.8pt = 18.6mm）が
    # 1行に収まるよう 26mm にしている（内寸 = 26mm - 4.2mm = 21.8mm）
    story.append(table(SUMMARY_ROWS, [52 * mm, W - 102 * mm, 26 * mm, 24 * mm],
                       [S_CELL, S_CELL, S_CELL, S_CELL]))
    story.append(Spacer(1, 6))
    story.append(Paragraph(
        "<b>7章は貴社の担当領域（DBとバッチ）への変更を伴いますので、"
        "「お願い」として書いております。</b>"
        "<b>ご負担や副作用のご懸念があれば、その旨をお知らせください。"
        "当方で別の手を検討します。</b>", S_BODY))

    # ---------------------------------------------------------- 1. 索引
    story.append(CondPageBreak(75 * mm))
    story.append(Paragraph(
        "1. 索引について（ご対応中と伺いました。参考情報です）", S_H2))

    story.append(Paragraph(
        "1-0. 本章の位置づけ — ご依頼は取り下げております", S_H3))
    story.append(Paragraph(
        "<b>索引についてはご対応中と伺いました。"
        "つきましては本章は依頼ではなく参考情報です。</b>"
        "<b>ご対応が完了した時点で、残っているものがあれば"
        "改めてご相談します。</b>", S_BODY))
    story.append(Paragraph(
        "<b>本章に残しておりますのは、当方が EXPLAIN で実測した結果と、"
        "当方が見ていた列の一覧です。</b>"
        "<b>貴社が入れられた索引で足りているかどうかを、そちらで"
        "突き合わせていただけるように残しております。</b>"
        "<b>この一覧のとおりにしていただきたい、という意味では"
        "ありません。</b>", S_BODY))
    story.append(Spacer(1, 2))
    story.append(quote(
        "<b>初版（2026-09-15）では本章を「最優先のご依頼（7件）」として"
        "書いておりました。本改訂（2026-09-17）で取り下げております。"
        "行き違いがございましたら申し訳ございません。</b>", CW))
    story.append(Spacer(1, 6))

    story.append(CondPageBreak(70 * mm))
    story.append(Paragraph("1-1. なぜこの列を見ていたか", S_H3))
    story.append(Paragraph(
        "当方は清掃詳細ログの表示期間を <b>それまでの上限31日から、"
        "上限2年（731日）に広げました</b>。", S_BODY))
    story.append(Paragraph(
        "そのために EXPLAIN で実行計画を確認したところ、"
        "<b>索引が無いために全表走査になっている経路が"
        "複数見つかりました。</b> "
        "以下の①〜⑦は、そのとき当方が見ていた列です。", S_BODY))
    story.append(Paragraph(
        "<b>現状（データが少ないうち）は問題が表に出ません。</b>"
        "<b>問題になるのは、本番のデータが積み上がってからです。</b>",
        S_BODY))
    # ★「実測」と書くときは数字の出所を併記する（当方の作業規約）。
    #   旧「以下、すべて実測です。推測ではありません。」は出所を書かずに
    #   言い切っていたので差し替えた。★点検スクリプトの 12. が旧文言を見る★
    story.append(Paragraph(
        "以下の SHOW CREATE TABLE と EXPLAIN は、"
        "<b>テスト環境で実際に実行したものです。推測ではありません。</b>",
        S_BODY))
    story.append(Paragraph(
        "<b>ただし、そこに出てくる行数（rows / r_rows）は"
        "当方が入れた検証用データの件数です。</b>"
        "<b>申し上げたいのは type / possible_keys / key という"
        "実行計画の形のほうで、行数ではございません。</b>"
        "<b>詳しくは冒頭の「おことわり」をご覧ください。</b>", S_BODY))
    story.append(Paragraph("実測環境:", S_LEAD))
    story.append(Spacer(1, 3))
    story.append(code([
        "mysql://…@133.125.238.144:13306/clean_watch",
        "VERSION() = 10.11.19-MariaDB-ubu2204",
    ], CW))
    story.append(Spacer(1, 6))
    story.append(Paragraph(
        "<b>なお ALTER TABLE の形で書いておりますのは、"
        "列と順序を誤解なくお伝えするためです。</b>"
        "<b>当方でこれを実行することはございません"
        "（当方はメインDBのスキーマを変更しない方針です）。</b>", S_BODY))

    story.append(CondPageBreak(80 * mm))
    story.append(Paragraph(
        "1-2. ①② — 索引が1本も無い列", S_H3))
    story.append(Spacer(1, 2))
    story.append(code([
        "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`);",
    ], CW))
    story.append(Spacer(1, 6))

    story.append(CondPageBreak(60 * mm))
    story.append(Paragraph("根拠（EXPLAIN の出力そのまま）", S_H4))
    story.append(quote(
        "<b>行数の読み方について</b><br/>"
        "以下の rows / r_rows は、<b>当方が入れた検証用データでの行数</b>です"
        "（「おことわり」参照）。<b>本番の行数を示すものではありません。</b><br/>"
        "一方、type / possible_keys / key は "
        "<b>表の構造（索引の有無）から決まる</b>もので、"
        "<b>行数によって変わりません。</b> "
        "<b>本章で申し上げたいのは後者です。</b>", CW))
    story.append(Spacer(1, 6))
    story.append(Paragraph(
        "clean_status_histories を clean_timetable_id IN (5件) で"
        "引いたとき:", S_LEAD))
    story.append(Spacer(1, 3))
    story.append(code([
        "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",
    ], CW))
    story.append(Spacer(1, 6))
    story.append(Paragraph(
        "robot_error_reports を同様に引いたとき:", S_LEAD))
    story.append(Spacer(1, 3))
    story.append(code([
        "type=ALL | possible_keys=null | key=null",
        "rows=49 | r_rows=49.00 | filtered=100 | r_filtered=4.08 | "
        "Extra=Using where",
    ], CW))
    story.append(Spacer(1, 6))
    story.append(Paragraph(
        "<b>possible_keys = null ＝ 使える索引が1本もありません。</b>"
        "<b>表の全行を読んで、そのうち一致した行だけを残しています</b>"
        "（上の 538行・0.93% は <b>当方の検証用データでの値</b>です）。",
        S_BODY))
    story.append(Paragraph(
        "これは統計に基づくコスト判断ではなく、<b>構造的な全表走査</b>です。"
        "行数が増えても索引を選ぶようにはなりません。", S_BODY))

    story.append(CondPageBreak(75 * mm))
    story.append(Paragraph("SHOW CREATE TABLE での確認", S_H4))
    story.append(Spacer(1, 2))
    story.append(code([
        "-- clean_status_histories",
        "UNIQUE KEY `clean_status_histories_id_unique`           (`id`)",
        "KEY        `clean_status_histories_facility_id_foreign` (`facility_id`)",
        "KEY        `clean_status_histories_section_id_foreign`  (`section_id`)",
        "-- clean_timetable_id には索引も FK もありません",
        "",
        "-- robot_error_reports",
        "UNIQUE KEY `robot_error_reports_report_id_unique`    (`error_id`)",
        "KEY        `robot_error_reports_facility_id_foreign` (`facility_id`)",
        "KEY        `robot_error_reports_section_id_foreign`  (`section_id`)",
        "KEY        `robot_error_reports_robot_id_foreign`    (`robot_id`)",
        "-- clean_timetable_id には索引がありません",
    ], CW))
    story.append(Spacer(1, 6))

    story.append(CondPageBreak(70 * mm))
    story.append(Paragraph(
        "影響の大きさ（2026-09-17 に前提を置き直しました）", S_H4))
    story.append(Paragraph(
        "当方のアプリは、この2つのテーブルを "
        "<b>clean_timetable_id IN (…) の形でしか引きません。</b> "
        "IN の要素数が1000を超えると1000件ずつの塊に分けて投げますが、"
        "<b>塊ごとに全表走査が起きます。</b>", S_BODY))
    story.append(Paragraph(
        "<b>初版ではこの走査行数を「施設200件」という"
        "当方の仮置きで外挿しておりました。</b>"
        "<b>その後、弊社で現状のロボット台数を確認したところ "
        "30台未満 でしたので、置き直しております。</b>", S_BODY))
    story.append(Paragraph(
        "<b>台数の根拠は、弊社（DFA）自身の把握「現状30台未満」"
        "（2026-09-17）です。</b>"
        "<b>貴社への質問ではございません。</b>", S_BODY))

    story.append(CondPageBreak(95 * mm))
    story.append(Paragraph("計算過程", S_H4))
    story.append(quote(
        "<b>下の「1台1日2回」は、貴社のデータから得た値ではありません。</b><br/>"
        "当方が入れた検証用データの密度"
        "（31日 / 12エリア / 538行 ＝ 1.45 回/日/エリア）を切り上げた、"
        "<b>当方の置き値</b>です。<br/>"
        "<b>実運用の清掃回数が決まりましたら、"
        "当方から置き直してお知らせいたします。</b>", CW))
    story.append(Spacer(1, 6))
    story.append(code([
        "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万行",
    ], CW))
    story.append(Spacer(1, 6))

    story.append(CondPageBreak(45 * mm))
    story.append(table(SCAN_ROWS, [26 * mm, 20 * mm, W - 46 * mm],
                       [S_CELL, S_CELL, S_CELL]))
    story.append(Spacer(1, 6))
    story.append(Paragraph(
        "<b>初版（施設200件での外挿）では「2年で約6,430万行」と"
        "しておりました。約 1/33 になります。</b>", S_BODY))

    story.append(CondPageBreak(70 * mm))
    story.append(Paragraph(
        "数字が小さくなったことについて、事実だけ申し添えます", S_H4))
    story.append(Paragraph(
        "<b>「だから問題ない」と断定するつもりはございません。</b> "
        "分かっていることは次の3点です。", S_LEAD))
    story.append(Spacer(1, 2))
    story.extend(bull([
        "possible_keys = null である以上、"
        "<b>全表走査は行数によらず構造的に起きます</b>。"
        "<b>行数が増えても索引を選ぶようにはなりません</b>"
        "（統計に基づくコスト判断ではないためです）",
        "清掃詳細ログは、この全表走査を "
        "<b>2つの経路（本体と連続日数の算出）で別々に撃ちます</b>。"
        "2年の表示1回あたり、合計で 約386万行の走査になります",
        "上の 43,860行 は「1台1日2回」への切り上げです。"
        "<b>塊の数と1塊あたりの行数が両方とも台数・回数に比例するため、"
        "走査行数はそれらに対して二乗で効きます</b>。"
        "台数が倍になれば走査行数は4倍になります",
    ]))

    story.append(CondPageBreak(80 * mm))
    story.append(Paragraph(
        "1-3. ③④ — 複合索引（clean_timetables）", S_H3))
    story.append(Spacer(1, 2))
    story.append(code([
        "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`);",
    ], CW))
    story.append(Spacer(1, 6))

    story.append(CondPageBreak(75 * mm))
    story.append(Paragraph("根拠", S_H4))
    story.append(Paragraph(
        "当方の主クエリは <b>常に precheck = 0 AND start_at &gt;= ? "
        "AND start_at &lt; ? の AND</b> です。"
        "start_at 単独の索引はありますが、実行計画では使われていません。",
        S_LEAD))
    story.append(Spacer(1, 3))
    story.append(code([
        "-- 2年範囲の主クエリ",
        "type=ALL | possible_keys=clean_timetables_start_at_index | key=null | "
        "key_len=null",
        "rows=1044 | r_rows=538.00 | Extra=Using where; Using filesort",
    ], CW))
    story.append(Spacer(1, 6))
    story.append(Paragraph("施設で絞った場合も同様です。", S_LEAD))
    story.append(Spacer(1, 3))
    story.append(code([
        "-- 2年範囲 + facility_id IN (1,2,3)",
        "type=ALL | possible_keys=clean_timetables_facility_id_foreign,"
        "clean_timetables_start_at_index",
        "key=null | rows=1044 | r_rows=175.00 | filtered=32.47 | "
        "Extra=Using where; Using filesort",
    ], CW))
    story.append(Spacer(1, 6))
    story.append(Paragraph(
        "<b>単一列の索引が2本とも候補に挙がりながら、"
        "どちらも使われていません。</b>"
        "これが複合索引を必要とする形そのものです。", S_BODY))
    story.append(Spacer(1, 2))
    story.append(quote(
        "<b>ただし、ここは①②とは性質が異なります。</b><br/>"
        "③④は possible_keys に索引が挙がっておりますので、"
        "<b>使うかどうかはコスト判断</b>です。<br/>"
        "<b>上の実行計画は当方の検証用データ（少量）で測ったものですので、"
        "行数が増えれば選択が変わる可能性があります。</b><br/>"
        "そのため③④は、①②ほど強くは申し上げられません。", CW))
    story.append(Spacer(1, 6))

    story.append(CondPageBreak(45 * mm))
    story.append(Paragraph("2本を1本にまとめない理由", S_H4))
    story.append(Paragraph(
        "(facility_id, precheck, start_at) の1本に寄せる案もありえますが、"
        "<b>施設で絞らない呼び出し（画面の既定）が主経路</b>であり、"
        "その1本では precheck = 0 AND start_at が引けません。"
        "<b>そのため当方は2本別々のものとして見ておりました。</b>", S_BODY))
    story.append(Paragraph(
        "なお clean_timetables の既存索引は6本すべて単一列で、"
        "<b>2026-09-13 時点では複合索引は1本もありませんでした</b>。",
        S_BODY))

    story.append(CondPageBreak(80 * mm))
    story.append(Paragraph(
        "1-4. ⑤ — 期間での集計（面積の累計表示に関係します）", S_H3))
    story.append(Spacer(1, 2))
    story.append(code([
        "ALTER TABLE `clean_status_histories`",
        "  ADD INDEX `clean_status_histories_started_at_index` (`started_at`);",
    ], CW))
    story.append(Spacer(1, 6))
    story.append(Paragraph("根拠", S_H4))
    story.append(Paragraph(
        "当方は <b>清掃総面積を期間で絞って集計する画面</b> を"
        "追加しております（本書2章と関連）。"
        "その集計は clean_status_histories を期間で絞って SUM() を取る"
        "形になりますが、<b>started_at に索引が無いため、"
        "期間で絞っても全行を走査します。</b>", S_LEAD))
    story.append(Spacer(1, 3))
    story.append(code([
        "EXPLAIN SELECT facility_id, SUM(clean_area) FROM "
        "clean_status_histories",
        " WHERE started_at >= '2026-09-01' AND started_at < '2026-09-14' "
        "GROUP BY facility_id;",
        "→ type=index | key=clean_status_histories_facility_id_foreign | "
        "rows=538 | Extra=Using where",
    ], CW))
    story.append(Spacer(1, 6))
    story.append(Paragraph(
        "（rows は当方の検証用データでの行数です。"
        "<b>申し上げたいのは「started_at に索引が無い」という構造のほう</b>で、"
        "こちらは SHOW CREATE TABLE で確認しております）", S_NOTE))
    story.append(Paragraph(
        "<b>ここでも規模の前提を置き直しております。</b>"
        "当方の社内資料は 「1日約360行・年間約13万行・保持2年」 を"
        "想定しておりましたが、"
        "これは施設200件規模に相当する数字でした。", S_LEAD))
    story.append(Spacer(1, 3))
    story.append(code([
        "ロボット30台 × 2回/日 × 365日 = 約 2.2万行/年"
        "（2年で 約4.4万行）",
    ], CW))
    story.append(Spacer(1, 6))
    story.append(Paragraph(
        "<b>規模は当初想定の 1/6 ほどになりますが、"
        "「期間で絞っても全行を読む」という形そのものは"
        "行数によらず変わりません。</b>", S_BODY))

    story.append(CondPageBreak(80 * mm))
    story.append(Paragraph(
        "1-5. ⑥⑦ — D-Security 制御盤（第8回以降に判明）", S_H3))
    story.append(Spacer(1, 2))
    story.append(code([
        "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`);",
    ], CW))
    story.append(Spacer(1, 6))

    story.append(CondPageBreak(70 * mm))
    story.append(Paragraph(
        "⑥の根拠 — <b>同じテーブル・同じ索引で、列を変えただけで"
        "実行計画が切り替わりました</b>", S_H4))
    story.append(Paragraph(
        "制御盤ごとの「最終通信時刻」を取るため MAX(reported_at) を"
        "使っています。", S_LEAD))
    story.append(Spacer(1, 3))
    story.append(table(MAXCOL_ROWS,
                       [32 * mm, 24 * mm, 30 * mm, 14 * mm, W - 100 * mm],
                       [S_CELL, S_CELL, S_CELL, S_CELL, S_CELL]))
    story.append(Spacer(1, 4))
    story.append(Paragraph(
        "（rows は当方の検証用データでの行数です。"
        "<b>ご覧いただきたいのは type と Extra が切り替わった点</b>です）",
        S_NOTE))
    story.append(Spacer(1, 2))
    story.append(Paragraph(
        "<b>InnoDB の二次索引には主キーが暗黙に付く</b>ため、既存の<br/>"
        "control_reports_control_id_foreign は物理的に "
        "(control_id, id) の複合索引として働いています。"
        "<b>2列目が reported_at になれば、MAX(reported_at) にも"
        "同じ効果が及びます。</b>", S_BODY))
    story.append(Paragraph(
        "<b>control_reports は心拍の報告が積み上がるテーブルで、"
        "本番では最も行数が多くなる見込みです</b>"
        "（<b>8章の報告間隔次第で1,100万行〜7,000万行。"
        "制御盤11台という当方の置き値での試算です</b>）。"
        "<b>ここが全表走査のままですと、D-Sログ画面が開けなくなります。</b>",
        S_BODY))
    story.append(Paragraph(
        "<b>清掃側（①〜⑤）とは事情が異なります。</b> "
        "清掃側の行数は台数と清掃回数で決まりますが、"
        "control_reports は <b>時間あたりの報告間隔で決まる</b>ため、"
        "台数が少なくても行数は積み上がります。"
        "<b>そのため上の「ロボット30台」の置き直しは、⑥⑦には効きません。"
        "</b>", S_BODY))

    story.append(CondPageBreak(35 * mm))
    story.append(Paragraph("⑦の根拠", S_H4))
    story.append(Paragraph(
        "D-Secアラートの絞り込みで occurred_at の範囲指定を使いますが、"
        "索引がありません。", S_BODY))

    story.append(CondPageBreak(95 * mm))
    story.append(Paragraph("1-6. ①〜⑦の一覧（突き合わせ用）", S_H3))
    story.append(Paragraph(
        "<b>貴社のご対応内容と突き合わせていただくための一覧です。</b>",
        S_LEAD))
    story.append(Spacer(1, 3))
    # 「テーブル」列は control_error_histories（24字 = 8.8pt で 37.3mm）が
    # 語中で割れないよう 44mm（内寸 39.8mm）にしている
    story.append(table(INDEX_LIST_ROWS,
                       [10 * mm, 44 * mm, 44 * mm, W - 98 * mm],
                       [S_CELL, S_CELL, S_CELL, S_CELL]))
    story.append(Spacer(1, 6))
    story.append(Paragraph("申し添え", S_H4))
    story.extend(bull([
        "①〜⑦のテーブルは、いずれも "
        "<b>貴社のデータ収集処理が書き込むテーブル</b>です。"
        "<b>索引を足すぶん、書き込みがわずかに遅くなります。</b>"
        "<b>その兼ね合いのご判断は貴社にお任せいたします。</b>",
        "<b>当方の見立てでは ①② が突出しています</b>"
        "（索引が1本も無い列のため）。"
        "<b>③〜⑦は、①②の後で差し支えありません。</b>",
        "<b>当方はメインDBのスキーマを変更しない方針です。</b> "
        "<b>上記を当方で実行することはございません。</b>",
        "<b>ご対応が完了しましたら、その旨をお知らせいただけますと、"
        "当方で EXPLAIN を測り直し、残りがあるかどうかを"
        "確認いたします。</b>",
    ]))

    # ---------------------------------------------------------- 2. サマリの面積
    story.append(CondPageBreak(85 * mm))
    story.append(Paragraph(
        "2. サマリの面積は、期間内の合計でしょうか", S_H2))

    story.append(Paragraph(
        "2-1. 本章は仕様の確認のみに縮めております", S_H3))
    story.append(Paragraph(
        "<b>初版では、テスト環境の facility_summaries / "
        "customer_summaries の値を並べて「合計に見えません」と"
        "申し上げておりました。</b>"
        "<b>この値は当方が入れた検証用のダミーデータでしたので、"
        "当該の指摘は取り下げます。</b>"
        "<b>お騒がせいたしました。</b>", S_BODY))
    story.append(Paragraph(
        "<b>残しますのは、実装のために必要な仕様の確認だけです。</b>",
        S_BODY))
    story.append(Paragraph(
        "当方は <b>「清掃総面積の累計」を画面に表示する機能</b> を"
        "作っており、その数字をサマリから取るのか、"
        "clean_status_histories から毎回その場集計するのかを"
        "決める必要があります"
        "（後者を選ぶ場合に効くのが、1章の⑤の列です）。", S_BODY))

    story.append(CondPageBreak(85 * mm))
    story.append(Paragraph("2-2. お伺いしたいこと（2問）", S_H3))
    story.append(Spacer(1, 2))
    story.append(qhead(
        "Q2-1. 集計バッチは、task_area / clean_area に "
        "期間内の合計 を書き込む仕様でしょうか", CW))
    story.append(Spacer(1, 4))
    story.append(table(Q21_ROWS, [16 * mm, W - 16 * mm], [S_CELL, S_CELL]))
    story.append(Spacer(1, 6))
    story.append(Paragraph(
        "<b>B の場合、当方は clean_status_histories から"
        "毎回その場集計する形にいたします。</b>"
        "<b>「サマリに合計を入れるほうが筋がよい」というご判断であれば、"
        "そちらに合わせます。</b>", S_BODY))

    story.append(CondPageBreak(90 * mm))
    story.append(qhead(
        "Q2-3. 1つの clean_timetable_id に対し、clean_status_histories は"
        "何行できますでしょうか", CW))
    story.append(Spacer(1, 4))
    story.append(Paragraph(
        "<b>面積の合計をSQLの SUM() で取るか、行を1本に畳んでから足すかが、"
        "これで変わります。</b>", S_LEAD))
    story.append(Spacer(1, 3))
    story.append(table(Q23_ROWS, [16 * mm, W - 16 * mm], [S_CELL, S_CELL]))
    story.append(Spacer(1, 6))
    story.append(Paragraph(
        "<b>「清掃状態履歴」というテーブル名からは、"
        "B の設計ではないかとも思われます。</b>"
        "<b>一方で clean_status_histories にはカラムとしての状態"
        "（clean_status_trans 相当）が無く、この点は A のようにも"
        "読めます。</b>"
        "<b>どちらとも決めかねましたので、お教えください。</b>", S_BODY))
    story.append(Paragraph(
        "<b>B の場合、SUM() で足すと同じ清掃の面積を"
        "何度も数えてしまいます。</b>"
        "<b>そして、それは例外もエラーも出さずに、"
        "ただ数字が大きくなるだけです。</b>", S_BODY))
    story.append(Paragraph(
        "<b>当方は安全側（B の前提で、最新の1行に畳んでから足す）で"
        "実装いたしますが、A と確定できるのであれば、"
        "そのほうが速くなります。</b>", S_BODY))
    story.append(Spacer(1, 2))
    story.append(quote(
        "<b>設問番号について</b>: "
        "初版の Q2-2（「合計でない場合の正しい経路をお教えください」）は"
        "取り下げました"
        "（Q2-1 の B に当方の対応を書きましたので、"
        "独立の設問にする必要がなくなったためです）。"
        "<b>番号は詰めずに Q2-3 のままにしております。</b>", CW))

    story.append(CondPageBreak(85 * mm))
    story.append(Paragraph(
        "2-4. 参考: 年次サマリの桁あふれについて（優先度は低いです）", S_H3))
    story.append(Paragraph(
        "<b>ご依頼でも質問でもなく、気づいた点のご報告です。</b>", S_LEAD))
    story.append(Spacer(1, 3))
    story.append(code([
        "facility_summaries.task_area / clean_area   double(14,6)  "
        "→ 上限 約 1億㎡",
        "customer_summaries.task_area / clean_area   double(14,6)  → 同上",
    ], CW))
    story.append(Spacer(1, 6))
    story.append(Paragraph(
        "<b>もしサマリに期間内の合計を入れる場合（Q2-1 が A の場合）、"
        "年次の行が上限に当たる可能性があります。</b>", S_LEAD))
    story.append(Spacer(1, 3))
    story.append(table(OVERFLOW_ROWS, [44 * mm, W - 44 * mm],
                       [S_CELL, S_CELL]))
    story.append(Spacer(1, 6))
    story.append(code([
        "1億㎡ ÷ 30台 ÷ 365日 ≒ 9,132 ㎡/台/日",
    ], CW))
    story.append(Spacer(1, 6))
    story.append(Paragraph(
        "<b>これは当方の計算であり、実測ではありません</b>"
        "（当方の手元には実際の店舗面積の値がございませんので、"
        "9,100㎡ が現実的な値かどうかは判断できておりません）。", S_BODY))
    story.append(Paragraph(
        "<b>初版では「施設200件」を前提に 約1,370㎡ としておりましたが、"
        "弊社で現状のロボット台数を確認したところ30台未満でしたので、"
        "置き直しております（1章と同じ前提です）。</b>"
        "<b>桁あふれの可能性はそのぶん遠のきました。</b>", S_BODY))
    story.append(Paragraph(
        "<b>週次・月次では問題になりません。"
        "P1 で当方が読むのも週次・月次のみです。</b>"
        "<b>年次サマリを作られる際にご一考いただければ、"
        "という程度のものです。</b>", S_BODY))

    # ---------------------------------------------------------- 3. 第8回 Q4
    story.append(CondPageBreak(70 * mm))
    story.append(Paragraph(
        "3. 第8回 Q4 の残り（サマリの率・分子・分母の関係）— 再掲", S_H2))
    story.append(Paragraph(
        "第8回で「多いので後で回答」との留保をいただいた件です。"
        "<b>取り下げておりません。</b>", S_BODY))
    story.append(Paragraph(
        "お伺いしたい内容は第8回の4章と同じです。要点だけ再掲します。",
        S_LEAD))
    story.append(Spacer(1, 2))
    story.extend(bull([
        "サマリに入る各率（clean_ratio 等）の <b>計算式</b>",
        "<b>分子・分母に対応するカラム名</b>",
        "件数列（task_count 等）が <b>何を数えているか</b>"
        "（予定の数か、実施の数か）",
    ]))
    story.append(Spacer(1, 2))
    story.append(Paragraph(
        "<b>2章の面積の件と同じ枠の話ですので、"
        "あわせてご回答いただけますと助かります。</b>", S_BODY))

    # ---------------------------------------------------------- 4. error_count
    story.append(CondPageBreak(80 * mm))
    story.append(Paragraph("4. サマリの error_count の集計範囲", S_H2))
    story.append(Paragraph(
        "第8回 A7 でいただいたご回答が、"
        "<b>「バッチが実際にそうしている」のか「当方への助言」なのか</b>"
        "を読み取れませんでした。"
        "<b>当期（進行中の週・月）は当方が自前で集計するため、"
        "定義が食い違うと、確定期間と当期でグラフが段差になります。</b>",
        S_BODY))

    story.append(Paragraph("お伺いしたいこと（3問）", S_H3))
    story.append(Spacer(1, 2))
    story.append(qhead(
        "Q4-1. サマリの error_count は、事前チェック"
        "（precheck = 30 / 60）を除外して集計されていますでしょうか", CW))
    story.append(Spacer(1, 4))
    story.append(table(Q41_ROWS, [16 * mm, W - 16 * mm], [S_CELL, S_CELL]))
    story.append(Spacer(1, 8))

    # Q4-2 は原稿に本文が無く帯だけなので、単独でページ末に取り残されないよう
    # Q4-3 の帯まで入る高さを確保してから置く
    story.append(CondPageBreak(62 * mm))
    story.append(qhead(
        "Q4-2. clean_timetable_id が null のエラー行は、サマリの "
        "error_count に含まれますでしょうか", CW))
    story.append(Spacer(1, 8))

    story.append(CondPageBreak(55 * mm))
    story.append(qhead(
        "Q4-3. clean_error が 2（セキュリティ有効）/ "
        "3（D-S制御盤異常）の清掃は、サマリの error_count に"
        "含まれますでしょうか", CW))
    story.append(Spacer(1, 4))
    story.append(Paragraph(
        "<b>第8回 A6-1 で「2 / 3 では robot_error_reports が作られない」"
        "とのご回答をいただいております。</b>"
        "<b>その場合、サマリ側でも数えていない、"
        "という理解でよろしいでしょうか。</b>", S_BODY))

    # ---------------------------------------------------------- 5. 面積の単位
    # ★2026-09-17（2回目）の改訂で章ごと差し替えた。設問（Q5-1）も表も無い★
    story.append(CondPageBreak(55 * mm))
    story.append(Paragraph("5. 面積の単位（設問を取り下げます）", S_H2))
    story.append(Paragraph(
        "<b>初版では「task_area / clean_area の単位は平方メートル（㎡）"
        "でしょうか」とお伺いしておりました（Q5-1）。</b>", S_BODY))
    story.append(Spacer(1, 2))
    story.append(callout(
        "<b>本件は弊社（DFA）で「㎡」と確定いたしましたので、"
        "設問を取り下げます。</b><br/>"
        "<b>画面には ㎡ として表示いたします。</b>", CW))
    story.append(Spacer(1, 6))
    story.append(Paragraph(
        "<b>貴社にご対応いただくことはございません。</b>"
        "<b>万一「㎡ではない」という事情がございましたら、"
        "その場合のみお知らせください。</b>", S_BODY))

    # ---------------------------------------------------------- 6. グループ
    story.append(CondPageBreak(85 * mm))
    story.append(Paragraph("6. グループ単位のサマリ", S_H2))

    story.append(Paragraph(
        "6-1. group_summaries に相当するテーブルが見当たりません", S_H3))
    story.append(Paragraph(
        "サマリ系のテーブルは次の4本と認識しております。", S_LEAD))
    story.append(Spacer(1, 3))
    story.append(code([
        "customer_summaries        / customer_error_summaries   "
        "（customer_id を参照）",
        "facility_summaries        / facility_error_summaries   "
        "（facility_id を参照）",
    ], CW))
    story.append(Spacer(1, 6))
    story.append(Paragraph(
        "<b>グループ（groups）単位のサマリがありません。</b>", S_BODY))
    story.append(Paragraph(
        "当方は <b>ホールディングス単位のKPIを画面に出す</b> 予定です"
        "（本書7章と関連）。", S_BODY))

    story.append(CondPageBreak(75 * mm))
    story.append(Paragraph("お伺いしたいこと（1問）", S_H3))
    story.append(Spacer(1, 2))
    story.append(qhead(
        "Q6-1. グループ単位の集計は、どちらの想定でしょうか", CW))
    story.append(Spacer(1, 4))
    story.append(table(Q61_ROWS, [16 * mm, W - 16 * mm], [S_CELL, S_CELL]))
    story.append(Spacer(1, 6))
    story.append(Paragraph(
        "<b>A の場合、率は「率の平均」ではなく"
        "「Σ分子 ÷ Σ分母」で再計算いたします。</b>"
        "<b>分子・分母がサマリに入っている前提ですので、"
        "3章のご回答と関係します。</b>", S_BODY))

    story.append(CondPageBreak(95 * mm))
    story.append(Paragraph(
        "6-2. ところが、4つの率のうち2つは分子・分母が揃っておりません",
        S_H3))
    story.append(Paragraph(
        "<b>本節は、テーブルのカラム構成（DDL）と当方の計算式を"
        "突き合わせたもので、行の中身には依存しておりません。</b>",
        S_BODY))
    story.append(Paragraph(
        "サマリのカラムを当方の計算式（要件定義）と突き合わせたところ、"
        "次のようになりました。", S_LEAD))
    story.append(Spacer(1, 3))
    story.append(table(RATIO_ROWS,
                       [30 * mm, 48 * mm, 30 * mm, 28 * mm, W - 136 * mm],
                       [S_CELL, S_CELL, S_CELL, S_CELL, S_CELL]))
    story.append(Spacer(1, 6))
    story.append(Paragraph(
        "<b>「清掃実施回数」に当たるカラムが見当たりませんでした。</b>",
        S_BODY))

    # なぜ「清掃実施回数」が要るのか（2026-09-17 の2回目の改訂で新設）
    story.append(CondPageBreak(95 * mm))
    story.append(Paragraph(
        "なぜ「清掃実施回数」が要るのか（Q6-2 の理由）", S_H4))
    story.append(Paragraph(
        "<b>当方の4つの率のうち、2つは「清掃実施回数」を必要とします。</b>",
        S_LEAD))
    story.append(Spacer(1, 3))
    story.append(code([
        "稼働率       = 清掃実施回数   ÷ 清掃予定回数 × 100",
        "エラー発生率 = エラー発生回数 ÷ 清掃実施回数 × 100",
    ], CW))
    story.append(Spacer(1, 6))
    story.append(Paragraph(
        "<b>つまり「実施回数」は、一方の率では分母、"
        "もう一方の率でも分母に当たります。</b>"
        "<b>そして task_count は、予定回数か実施回数かの"
        "どちらか一方にしかなりません。</b>", S_LEAD))
    story.append(Spacer(1, 3))
    story.append(table(TASKCOUNT_ROWS, [50 * mm, W - 50 * mm],
                       [S_CELL, S_CELL]))
    story.append(Spacer(1, 6))
    story.append(Paragraph(
        "<b>いずれにせよ、現在のカラム構成では2つの率のうち"
        "少なくとも片方が計算できません。</b>"
        "（task_count が実際にどちらなのかは、"
        "3章でお伺いしております）", S_BODY))
    story.append(Paragraph(
        "<b>これは合算のときだけの話ではありません。</b>", S_BODY))
    story.append(Spacer(1, 2))
    story.append(callout(
        "<b>確定した期間（前週・前月）の稼働率・エラー発生率そのものが、"
        "サマリからは出せません。</b>", CW))
    story.append(Spacer(1, 6))
    story.append(Paragraph(
        "<b>進行中の期間は当方が詳細テーブルからその場集計しますので"
        "出せますが、確定期間はサマリを読む設計ですので、"
        "そこで数字が欠けます。</b>", S_BODY))
    story.append(Paragraph(
        "<b>複数顧客を合算する場合は、さらに事情が重くなります。</b>"
        "<b>率の平均を取るのは誤りで、分子と分母をそれぞれ足してから"
        "割る必要がありますが、この2つはその材料がありません。</b>",
        S_BODY))

    story.append(CondPageBreak(75 * mm))
    story.append(qhead(
        "Q6-2. サマリに「清掃実施回数」のカラムを追加していただくことは"
        "可能でしょうか", CW))
    story.append(Spacer(1, 4))
    story.append(table(Q62_ROWS, [16 * mm, W - 16 * mm], [S_CELL, S_CELL]))
    story.append(Spacer(1, 6))
    story.append(Paragraph(
        "<b>C の場合、当方は確定期間（前週・前月）の稼働率・"
        "エラー発生率を画面に出さず、"
        "清掃完了率と総清掃面積だけを出す形にいたします。</b>"
        "<b>進行中の期間だけ2つの率が出て、確定期間では消える、"
        "という見え方になります。</b>", S_BODY))
    story.append(Paragraph(
        "<b>初版では「効いてくるのは2社目から」と書いておりましたが、"
        "これは合算の話に限った見立てでした。</b>"
        "<b>上のとおり、1社であっても確定期間の2つの率が出せませんので、"
        "訂正いたします。</b>", S_BODY))
    story.append(Paragraph(
        "<b>お急ぎいただく必要はございませんが、設計を決めたいので"
        "方針だけ伺えますと助かります。</b>", S_BODY))

    # ---------------------------------------------------------- 7. マスタ投入
    story.append(CondPageBreak(85 * mm))
    story.append(Paragraph(
        "7. グループ・顧客マスタのデータ投入のお願い", S_H2))

    story.append(Paragraph("7-1. 背景", S_H3))
    story.append(Paragraph(
        "当方は画面に <b>「ホールディングス → グループ（ブランド）→ 店舗」"
        "の3階層の絞り込み</b> を出す予定です。", S_BODY))
    story.append(Paragraph(
        "テーブルの構造"
        "（groups → customers.group_id → facilities.customer_id）は "
        "<b>すでに3階層になっており、"
        "当方から追加をお願いする必要はございません。</b>", S_BODY))
    # ★2026-09-17（2回目）の改訂でコード枠を削除した
    #   （テスト環境の中身は当方が入れたダミーだったため）★
    story.append(Paragraph(
        "<b>ただし、当方の手元には本番の中身がございません。</b>", S_BODY))
    story.append(Paragraph(
        "テスト環境に入っておりますのは <b>当方が入れた検証用のダミー</b>"
        "（groups 1行 / customers 1行 / "
        "facilities 12行・全行が同じ customer_id）だけです。"
        "<b>そのため当方では、3階層の絞り込みが実データで"
        "どう見えるかを確かめられておりません。</b>", S_BODY))

    story.append(CondPageBreak(60 * mm))
    story.append(Paragraph("7-2. お願いと、確認したいこと", S_H3))
    story.append(Spacer(1, 2))
    story.append(qhead(
        "お願い: 本番の groups / customers のデータ投入を"
        "お願いいたします", CW))
    story.append(Spacer(1, 4))
    story.append(Paragraph(
        "<b>時期の目安だけでも結構です。</b>"
        "<b>当方はそれまで、検証用の複数グループ・複数顧客のデータを"
        "自前で用意して進めます。</b>", S_BODY))

    story.append(CondPageBreak(75 * mm))
    story.append(qhead(
        "Q7-1. customers は、法人と屋号のどちらの粒度で"
        "入る想定でしょうか", CW))
    story.append(Spacer(1, 4))
    story.append(Paragraph(
        "<b>当方は「屋号（ブランド）」の粒度で画面を作る方針です。</b>",
        S_LEAD))
    story.append(Spacer(1, 3))
    story.append(table(Q71_ROWS, [16 * mm, W - 16 * mm], [S_CELL, S_CELL]))
    story.append(Spacer(1, 6))
    story.append(Paragraph(
        "<b>A と B では、同じ法人の中で屋号を分けられるかどうかが"
        "変わります。</b>"
        "<b>貴社の収集処理がどちらの粒度で customers を作る想定かを"
        "お教えください。</b>", S_BODY))

    story.append(CondPageBreak(55 * mm))
    story.append(qhead(
        "Q7-2. 屋号を持たない顧客は、どう表現されますでしょうか", CW))
    story.append(Spacer(1, 4))
    story.append(Paragraph(
        "<b>当方は「屋号が無い顧客もありうる」という前提で"
        "画面を作ります。</b> "
        "customers.group_id は <b>0 がセンチネル（グループなし）</b> と"
        "認識しておりますが、"
        "<b>屋号そのものが無い場合に customer_name に何が入るか</b>を"
        "お教えください。", S_BODY))

    story.append(CondPageBreak(50 * mm))
    story.append(qhead(
        "Q7-3. facilities.shop_id（varchar(20)、既定 '0'）は"
        "何を表す列でしょうか", CW))
    story.append(Spacer(1, 4))
    story.append(Paragraph(
        "テーブル定義書からは読み取れず、"
        "<b>テスト環境の値も当方が入れたダミー（全行 \"0\"）でしたので、"
        "値から推し量ることもできませんでした。</b>", S_BODY))
    story.append(Paragraph(
        "<b>顧客側の店舗コードでしょうか。"
        "分類の手がかりに使えるかを知りたく存じます。</b>", S_BODY))

    # ★原稿の7章は Q7-5 が Q7-4 より前に置かれている。その断り書き
    #   （2026-09-17 の改訂で新設）。引用ブロックと Q7-5 の帯は必ず同じページに★
    story.append(CondPageBreak(112 * mm))
    story.append(Spacer(1, 2))
    story.append(quote(
        "<b>設問の並びについて</b><br/>"
        "<b>本節は Q7-5 → Q7-4 の順に並んでおります</b>。"
        "Q7-4（group_id = 0 が投入できない件）が Q7-5 のご回答を"
        "前提とする内容のため、この順にしております。<br/>"
        "<b>当方の管理表がこの番号で追っておりますので、"
        "番号は振り直しておりません。</b>", CW))
    story.append(Spacer(1, 8))
    story.append(qhead(
        "Q7-5. 「屋号を持たない顧客」は、3階層のどの段が空くという"
        "意味でしょうか", CW))
    story.append(Spacer(1, 4))
    story.append(Paragraph(
        "<b>当方の中でも解釈が割れましたので、"
        "貴社のDBの想定を伺わせてください。</b>", S_BODY))
    story.append(Paragraph(
        "当方が作ろうとしている階層はこの3段です。", S_LEAD))
    story.append(Spacer(1, 3))
    story.append(code([
        "ホールディングス  →  屋号（ブランド）  →  店舗",
        "   groups              customers          facilities",
    ], CW))
    story.append(Spacer(1, 6))
    story.append(Paragraph(
        "「屋号を持たない顧客がありうる」と伺っておりますが、"
        "<b>どの段が空くのかで設計が変わります。</b>", S_LEAD))
    story.append(Spacer(1, 3))
    story.append(table(LAYER_ROWS, [12 * mm, 28 * mm, 44 * mm, W - 84 * mm],
                       [S_CELL, S_CELL, S_CELL, S_CELL]))
    story.append(Spacer(1, 6))
    story.append(Paragraph(
        "<b>② であれば、Q7-4 が解決すればそのまま作れます。</b>", S_BODY))
    story.append(Paragraph(
        "<b>① の場合、customers にダミーの行を1件作って、"
        "そこに店舗をぶら下げることになります。</b>"
        "<b>その場合、customer_name に何を入れるか"
        "（空文字は避けたく存じます）をお決めいただく必要があります。</b>",
        S_BODY))
    story.append(Paragraph(
        "<b>貴社の収集処理が customers をどう作る想定かを"
        "お教えいただければ、当方はそれに合わせます。</b>", S_BODY))

    story.append(CondPageBreak(95 * mm))
    story.append(qhead(
        "Q7-4. customers.group_id = 0（グループなし）は、"
        "現状のDDLでは投入できないように見えます", CW))
    story.append(Spacer(1, 4))
    story.append(Paragraph(
        "<b>これは質問というより、実DDLの矛盾のご報告です。</b>", S_BODY))
    story.append(Paragraph(
        "当方は customers.group_id について、"
        "<b>「NOT NULL / 既定値 0 / 0 はグループなしを表すセンチネル」</b>"
        "と理解しております"
        "（貴社より、索引の効率を理由にこの方式を採る旨の"
        "ご回答をいただいております）。", S_BODY))
    story.append(Paragraph(
        "<b>ところが、実際のDDLでは 0 を投入できません。</b> "
        "当方が接続して確認した内容です。", S_LEAD))
    story.append(Spacer(1, 3))
    story.append(code([
        "-- SHOW CREATE TABLE customers （抜粋）",
        "`group_id` bigint(20) unsigned NOT NULL DEFAULT 0,",
        "CONSTRAINT `customers_group_id_foreign` FOREIGN KEY (`group_id`) "
        "REFERENCES `groups` (`id`)",
    ], CW))
    story.append(Spacer(1, 6))
    # ★2026-09-17（2回目）の改訂で「SELECT id FROM groups … 1 のみ」の行を
    #   削除した（テスト環境の groups の中身は当方が入れたダミーのため）★
    story.append(code([
        "SELECT @@foreign_key_checks;           →  1"
        "（外部キー制約は有効です）",
        "SELECT @@sql_mode;                     →  NO_AUTO_VALUE_ON_ZERO は"
        "含まれておりません",
    ], CW))
    story.append(Spacer(1, 6))
    story.append(Paragraph(
        "<b>外部キー制約が有効である以上、group_id = 0 の顧客を "
        "INSERT するには、groups に id = 0 の行が"
        "存在していなければなりません。</b>"
        "<b>group_id を省略して INSERT した場合も、"
        "既定値の 0 が入るため同じです。</b>", S_BODY))
    story.append(Paragraph(
        "そして groups.id は AUTO_INCREMENT ですので、"
        "<b>id = 0 の行を入れること自体に NO_AUTO_VALUE_ON_ZERO が必要</b>"
        "ですが、上のとおり sql_mode には含まれておりません。", S_BODY))
    story.append(Spacer(1, 2))
    story.append(quote(
        "<b>申し添え</b>: テスト環境の groups には id = 0 の行が"
        "ございませんでしたが、"
        "<b>この表の中身は当方が入れた検証用のダミー</b>ですので、"
        "貴社の想定を示すものではありません。<br/>"
        "<b>そのため本件は「実データがこうなっている」ではなく、"
        "「DDLと sql_mode の組み合わせ上こうなる」というご報告です。</b>",
        CW))

    story.append(CondPageBreak(85 * mm))
    story.append(Paragraph("Q7-4 でお伺いしたいこと", S_H3))
    story.append(Paragraph(
        "<b>当方はメインDBのスキーマを変更しない方針のため、"
        "当方では直せません。</b>"
        "<b>どの形がよいか、貴社のご判断をお聞かせください。</b>", S_LEAD))
    story.append(Spacer(1, 3))
    story.append(table(Q74_ROWS, [16 * mm, 68 * mm, W - 84 * mm],
                       [S_CELL, S_CELL, S_CELL]))
    story.append(Spacer(1, 6))
    story.append(Paragraph(
        "<b>D であれば、対応は不要です。その旨だけお知らせください。</b>",
        S_BODY))

    # ---------------------------------------------------------- 8. 報告間隔
    story.append(CondPageBreak(85 * mm))
    story.append(Paragraph("8. 制御盤の報告間隔", S_H2))

    story.append(Paragraph(
        "8-1. 台数については、当方で把握いたしました（設問を取り下げます）",
        S_H3))
    story.append(Paragraph(
        "初版では「P1 稼働時点の施設数」「ロボット・D-S制御盤の台数」の"
        "2問を貴社にお伺いしておりました。"
        "<b>この2問は取り下げます。</b>", S_BODY))
    story.append(Paragraph(
        "<b>理由: 本番の規模は弊社（DFA）側で決めるべきことであり、"
        "貴社にお伺いする筋のものではありませんでした。"
        "当方の記録にもその旨が残っておりました。</b>", S_BODY))
    story.append(Paragraph(
        "<b>現時点の当方の把握は次のとおりです。"
        "参考までにお知らせいたします。</b>", S_LEAD))
    story.append(Spacer(1, 3))
    story.append(table(SCALE_ROWS, [30 * mm, 74 * mm, W - 104 * mm],
                       [S_CELL, S_CELL, S_CELL]))
    story.append(Spacer(1, 6))
    story.append(Paragraph(
        "<b>上の「11台」は、当方がテスト環境に入れた検証用データの台数を"
        "そのまま置き値にしたものです。</b>"
        "実運用の台数ではありません。", S_BODY))
    story.append(Paragraph(
        "<b>1章・2-4 の外挿は、この「ロボット30台」を前提に"
        "置き直しております。</b>"
        "<b>台数が大きく動いた場合は、当方から改めてお知らせいたします。</b>",
        S_BODY))

    story.append(CondPageBreak(85 * mm))
    story.append(Paragraph("お伺いしたいこと（1問）", S_H3))
    story.append(Spacer(1, 2))
    story.append(qhead(
        "Q8-3. link_schedules.intervals（制御盤の報告間隔・秒）は、"
        "運用上どの範囲で動きますでしょうか", CW))
    story.append(Spacer(1, 4))
    story.append(Paragraph(
        "<b>初版では「実運用値をお教えください」と、"
        "固定の1つの値を伺う形で書いておりました。</b>"
        "<b>その後、この値は運用上変わりうるものだと分かりましたので、"
        "聞き方を改めます。</b>", S_BODY))
    story.append(Paragraph(
        "<b>お教えいただきたいのは、次の3点です。</b>", S_LEAD))
    story.append(Spacer(1, 3))
    story.append(table(Q83_ROWS, [10 * mm, 62 * mm, W - 72 * mm],
                       [S_CELL, S_CELL, S_CELL]))
    story.append(Spacer(1, 6))
    story.append(Paragraph(
        "<b>おおよそで構いません。</b>"
        "<b>「だいたいこの範囲」が分かれば十分です。</b>", S_BODY))

    story.append(CondPageBreak(75 * mm))
    story.append(Paragraph("なぜ範囲と頻度を伺うのか", S_H4))
    story.append(Paragraph(
        "<b>ここが control_reports の行数を決めるためです。</b>"
        "<b>そして、行数の見積もりが6倍変わります。</b>", S_LEAD))
    story.append(Spacer(1, 3))
    story.append(table(INTERVAL_ROWS, [60 * mm, W - 60 * mm],
                       [S_CELL, S_CELL]))
    story.append(Spacer(1, 6))
    story.append(Paragraph(
        "<b>1,100万行と7,000万行とでは、索引の要否（1-5 の⑥）も、"
        "画面の作り方も変わります。</b>"
        "<b>下限が分かれば「最大でどこまで積み上がるか」を"
        "押さえられますし、変更の頻度が分かれば、"
        "その前提がいつまで持つかを判断できます。</b>", S_BODY))
    story.append(Paragraph(
        "<b>なお、上の台数（ロボット30台未満）はこの試算には効きません。</b>"
        "control_reports の行数は台数よりも報告間隔で決まるためです"
        "（1-5 ⑥に同じことを書いております）。", S_BODY))
    story.append(Spacer(1, 2))
    story.append(quote(
        "<b>設問番号について</b>: 本章の設問は Q8-3 のみとなりますが、"
        "<b>番号は詰めずに Q8-3 のままにしております</b>"
        "（当方側の管理表がこの番号で対応を追っているためです。"
        "ご了承ください）。", CW))

    # ---------------------------------------------------------- 9. 軽い確認
    story.append(CondPageBreak(80 * mm))
    story.append(Paragraph("9. 軽い確認2件 + DDL未反映2件", S_H2))
    story.append(Spacer(1, 2))
    story.append(qhead(
        "Q9-1. 第8回 A9-2 の robot_task_reports は、DDL 上の "
        "clean_task_reports のことでしょうか", CW))
    story.append(Spacer(1, 4))
    story.append(Paragraph(
        "<b>テーブル一覧に robot_task_reports が"
        "見当たりませんでした。</b>"
        "<b>なお、仮にそうであっても当方の実装は "
        "clean_status_histories を読んでおりますので、"
        "影響はございません。表記の確認のみです。</b>", S_BODY))
    story.append(Paragraph(
        "<b>あわせて、clean_task_reports に "
        "今後データが入る予定があるか もお教えいただけますと"
        "助かります。</b>", S_BODY))
    story.append(Paragraph(
        "テスト環境のこの表には行がございませんが、"
        "<b>そこに入っているデータは当方が用意した検証用のものですので、"
        "貴社の収集処理が書くかどうかは当方では分かりません</b>。",
        S_BODY))

    story.append(CondPageBreak(60 * mm))
    story.append(qhead(
        "Q9-2. 1回の清掃の中の、区域別の内訳は取得できますでしょうか"
        "（聞き直し）", CW))
    story.append(Spacer(1, 4))
    story.append(Paragraph(
        "<b>第8回でもお伺いした件です。</b> "
        "当方が実装できたのは "
        "<b>「1清掃エリア（＝1ロボット）につき1つの完了率」</b> までで、"
        "<b>売り場・通路・レジ前といった内訳</b>ではありません。", S_BODY))
    story.append(Paragraph(
        "<b>取得できない場合は「できない」とお答えいただければ、"
        "当方はこの機能を諦めます。</b>", S_BODY))

    story.append(CondPageBreak(60 * mm))
    story.append(qhead("お願い: DDL 未反映の2件（4回目）", CW))
    story.append(Spacer(1, 4))
    story.append(Paragraph(
        "<b>急ぎません。お手すきの折で結構です。</b>", S_LEAD))
    story.append(Spacer(1, 3))
    story.append(table(DDL_ROWS, [10 * mm, W - 10 * mm], [S_CODE, S_CELL]))
    story.append(Spacer(1, 6))

    # ---------------------------------------------------------- 10. 更新用アカウント
    # ★2026-09-17（2回目）の改訂で章ごと差し替えた。すでに発行済みだった★
    # ★SHOW GRANTS の出力を載せるが、IDENTIFIED BY PASSWORD のハッシュは
    #   絶対に載せない（原稿も伏せてある。点検スクリプトの 10. が見ている）★
    story.append(CondPageBreak(80 * mm))
    story.append(Paragraph(
        "10. DB更新用アカウント"
        "（すでに発行済みでした。ご依頼を取り下げます）", S_H2))
    story.append(Paragraph(
        "<b>第5回（2026-09-07）・第6回（2026-09-08）・"
        "第8回（2026-09-11）から継続してお願いしておりました件です。</b>",
        S_BODY))
    story.append(Spacer(1, 2))
    story.append(callout(
        "<b>当方が接続して確認いたしましたところ、必要な UPDATE 権限は"
        "すでに付与されておりました。</b>", CW))
    story.append(Spacer(1, 6))
    story.append(code([
        "SHOW GRANTS FOR CURRENT_USER();   （2026-09-17 に当方が実行）",
        "→ GRANT UPDATE ON `clean_watch`.`robot_error_reports` …",
        "→ GRANT UPDATE ON `clean_watch`.`control_error_histories` …",
    ], CW))
    story.append(Spacer(1, 6))
    story.append(Paragraph(
        "<b>当方が必要としていたのは、この2つのテーブルの"
        "status / comment / responded_at / resolved_at の"
        "更新のみです。</b>"
        "<b>すでに満たされております。</b>", S_BODY))
    story.append(Paragraph(
        "<b>ご対応済みであることに気づかず、3回にわたって重ねて"
        "お願いしてしまいました。当方の確認漏れです。"
        "失礼いたしました。</b>", S_BODY))
    story.append(Paragraph(
        "<b>本件は取り下げます。"
        "貴社にご対応いただくことはございません。</b>", S_BODY))

    # ---------------------------------------------------------- 11. 開始時刻
    # ★2026-09-17（2回目）の改訂で作り直した。
    #   旧 Q11-1（実開始時刻はどこか）・11-2 の delay 集計表・SQL 枠は削除。
    #   Q11-1 は「この理解で実装してよいか」という確認に置き換え、
    #   Q11-4（よりリアルタイムに近い更新先）を新設した★
    story.append(CondPageBreak(85 * mm))
    story.append(Paragraph(
        "11. 清掃の開始時刻と、リアルタイム性について", S_H2))

    story.append(Paragraph(
        "11-1. Q11-1 は、お伺いする内容を改めました", S_H3))
    story.append(Paragraph(
        "<b>初版では「ロボットが実際に清掃を開始した時刻は、"
        "どのテーブル・どのカラムに入りますでしょうか」と"
        "お伺いしておりました（Q11-1）。</b>", S_BODY))
    story.append(Paragraph(
        "<b>そのように申し上げた根拠は2つございましたが、"
        "いずれも当方側の事情でした。</b>", S_LEAD))
    story.append(Spacer(1, 2))
    story.extend(numlist([
        "テーブル定義書の started_at の備考が"
        "「清掃スケジュールの清掃開始日時を格納する」と読め、"
        "<b>予定時刻の写しのように見えた</b>こと",
        "テスト環境で予定時刻との差を取ったところ全件0分だったこと — "
        "<b>ただしこのデータは当方が入れた検証用のダミーで、"
        "予定時刻をそのまま started_at にも入れていたものでした</b>",
    ]))
    story.append(Spacer(1, 4))
    story.append(callout(
        "<b>弊社内で確認いたしましたところ、"
        "clean_status_histories.started_at を実際の清掃開始時刻として"
        "扱ってよい、との結論になりました。</b>", CW))
    story.append(Spacer(1, 6))
    story.append(Paragraph(
        "<b>誤った前提でお手を煩わせるところでした。"
        "失礼いたしました。</b>"
        "<b>つきましては、お伺いする内容を「どこにありますか」から"
        "「この理解で実装してしまってよろしいでしょうか」に"
        "改めさせていただきます。</b>", S_BODY))

    story.append(CondPageBreak(100 * mm))
    story.append(qhead(
        "Q11-1. clean_status_histories.started_at を実際の清掃開始時刻"
        "として実装を進めますが、問題ございませんでしょうか", CW))
    story.append(Spacer(1, 4))
    story.append(Paragraph(
        "<b>当方が書こうとしている実装は次のとおりです。</b>", S_LEAD))
    story.append(Spacer(1, 3))
    story.append(code([
        "実開始時刻  = clean_status_histories.started_at",
        "遅延        = started_at − clean_timetables.start_at",
        "出発失敗    = 予定開始時刻 + 猶予30分 を過ぎても "
        "started_at が入らない",
    ], CW))
    story.append(Spacer(1, 6))
    story.append(Paragraph(
        "<b>確認させていただきたい理由</b> — テーブル定義書の "
        "clean_status_histories.started_at の備考が "
        "<b>「清掃スケジュールの清掃開始日時を格納する」</b> と"
        "書かれており、<b>予定時刻の写しとも読めてしまう</b>ためです。",
        S_BODY))
    story.append(Spacer(1, 2))
    story.append(callout(
        "<b>もし実際には予定時刻が入る列でしたら、上の「出発失敗」は"
        "永久に0件になります。</b><br/>"
        "<b>エラーにもならず、画面上は「全件が時間どおりに出発した」と"
        "表示され続けます。</b>", CW))
    story.append(Spacer(1, 6))
    story.append(Paragraph(
        "<b>そのため、実装前に一言だけ確認させていただきたい次第です。</b>",
        S_LEAD))
    story.append(Spacer(1, 3))
    story.append(table(Q111_ROWS, [56 * mm, W - 56 * mm], [S_CELL, S_CELL]))
    story.append(Spacer(1, 6))
    story.append(Paragraph(
        "<b>貴社の収集処理の都合で「いまは予定時刻を書いているが、"
        "今後は実開始時刻を書く」といった事情がございましたら、"
        "その旨もお教えいただけますと助かります。</b>", S_BODY))

    story.append(CondPageBreak(90 * mm))
    story.append(Paragraph("11-2. 背景（残る3問の前提）", S_H3))
    story.append(Paragraph(
        "当方は、清掃が <b>「予定時刻から遅れて始まった／"
        "始まらなかった」</b> という状態を画面に出そうと"
        "しております。", S_BODY))
    story.append(Paragraph(
        "当方の要件定義では、清掃失敗を次のように定めております。", S_LEAD))
    story.append(Spacer(1, 3))
    story.append(quote(
        "予定開始時刻 + <b>猶予30分</b> を経過しても"
        "タスク開始が確認できない", CW))
    story.append(Spacer(1, 6))
    story.append(Paragraph(
        "started_at で遅延そのものは出せるようになりましたが、"
        "<b>次の2点は started_at だけでは埋まりません</b>。", S_LEAD))
    story.append(Spacer(1, 2))
    story.extend(bull([
        "started_at が入らなかったとき、"
        "<b>「指示すら出ていない」のか「指示は出たが動き出さなかった」のか</b>"
        " が区別できません（→ Q11-2）",
        "貴社のサマリに入っている「出発成功」と、当方の日次判定とで "
        "<b>定義がそろっているか</b> が分かりません（→ Q11-3）",
    ]))
    story.append(Spacer(1, 4))
    # ★見出しだけがページ末に取り残されないよう、Q11-2 の帯まで入る高さを
    #   確保してから見出しを置く★
    story.append(CondPageBreak(95 * mm))
    story.append(Paragraph(
        "お伺いしたいこと（Q11-1 のほかに3問）", S_H3))
    story.append(Spacer(1, 2))
    story.append(qhead(
        "Q11-2. clean_statuses.clean_status_trans が 1（清掃指示）で"
        "止まったケースは、履歴として残りますでしょうか", CW))
    story.append(Spacer(1, 4))
    story.append(Paragraph(
        "定義書によれば clean_status_trans は<br/>"
        "0: 清掃開始前, 1: 清掃指示, 2: 清掃中, … と推移します。", S_BODY))
    story.append(Paragraph(
        "<b>「1（清掃指示）で止まった ＝ 指示は出したが動き出さなかった」"
        "と読めますので、これが取れれば出発失敗を判定できます。</b>",
        S_BODY))
    story.append(Paragraph(
        "<b>ただし clean_status_histories にこのカラムが無く、"
        "clean_statuses（現在の状態）は清掃ごとに上書きされるため、"
        "過去を遡ることができません。</b>", S_BODY))
    story.append(Paragraph(
        "<b>履歴に残る仕組みがあるかをお教えください。</b>", S_BODY))

    story.append(CondPageBreak(80 * mm))
    story.append(qhead(
        "Q11-3. facility_summaries.launch_count / launch_ratio"
        "（出発成功数・出発成功率）の計算式をお教えください", CW))
    story.append(Spacer(1, 4))
    story.append(Paragraph(
        "<b>サマリには、貴社が計算済みの「出発成功数・出発成功率」の"
        "カラムが実在しております。</b>"
        "<b>つまり貴社側では「出発に成功したか」を判定できている、"
        "ということかと存じます。</b>", S_BODY))
    story.append(Paragraph(
        "定義書の備考は <b>「補足を参照.」</b> とあるのみで、"
        "<b>補足の本文を受領しておりません。</b>", S_BODY))
    story.append(Paragraph(
        "<b>この判定式が分かれば、当方も同じ定義で"
        "日次の判定ができるようになります。</b>", S_BODY))

    # Q11-4（2026-09-17 の2回目の改訂で新設）
    story.append(CondPageBreak(95 * mm))
    story.append(qhead(
        "Q11-4. clean_status_histories.started_at のほかに、"
        "より短い間隔で更新されるテーブル・カラムは"
        "ございますでしょうか", CW))
    story.append(Spacer(1, 4))
    story.append(Paragraph(
        "<b>新しくお伺いする設問です。</b>", S_BODY))
    story.append(Paragraph(
        "当方は現在、清掃の進み具合を clean_status_histories から"
        "読んでおります。"
        "<b>これよりも短い間隔で更新される（＝よりリアルタイムに近い）"
        "テーブル・カラムがあるのであれば、お教えください。</b>", S_BODY))
    story.append(Paragraph(
        "<b>具体的な候補を挙げてお伺いすることは、あえていたしません。</b>"
        "<b>当方で「このテーブルではないか」と当たりをつけますと、"
        "推測が混じります。</b>"
        "<b>貴社の収集処理が書き込んでいるもののうち、"
        "更新間隔が短いものをお示しいただければ、当方でそれに合わせます。</b>",
        S_BODY))
    story.append(Paragraph(
        "<b>あわせて、差し支えなければ次も添えていただけますと"
        "助かります。</b>", S_LEAD))
    story.append(Spacer(1, 3))
    story.append(table(Q114_ROWS, [10 * mm, W - 10 * mm], [S_CELL, S_CELL]))
    story.append(Spacer(1, 6))
    story.append(Paragraph(
        "<b>無い場合は「無い」とお答えいただければ、当方は "
        "clean_status_histories のままで進めます。</b>", S_BODY))

    story.append(CondPageBreak(55 * mm))
    story.append(Paragraph(
        "11-3. ご回答がいただけるまでの当方の対応", S_H3))
    story.append(Paragraph(
        "<b>Q11-2 / Q11-3 のいずれも取れない場合、当方は"
        "「出発失敗」と「未実施」を同じものとして扱います</b>"
        "（どちらも clean_error = 1 で判定します）。", S_BODY))
    story.append(Paragraph(
        "<b>Q11-4 にご回答がない場合は、clean_status_histories を読む"
        "現在の形のまま進めます。</b>", S_BODY))
    story.append(Paragraph(
        "<b>いずれも機能を止めるものではありませんので、"
        "ご回答は急ぎません。</b>"
        "<b>ただ、取れるのであれば取りたい、"
        "という性質のものです。</b>", S_BODY))

    # ------------------------------------------------ 12. DB接続経路
    # 初版（2026-09-15）の「参考: テスト環境について（申し送り）」は
    # ★この章に吸収した★。同じ内容を2箇所に書かないこと。
    story.append(CondPageBreak(85 * mm))
    story.append(Paragraph(
        "12. DB接続経路について — お願い3件・確認6問", S_H2))
    story.append(Paragraph(
        "<b>本章は、第9回の初版（2026-09-15）の末尾に"
        "「参考: テスト環境について（申し送り）」として"
        "情報共有のみで記載していた内容を、"
        "正式なお願いとして書き直したものです。</b>"
        "<b>別便にはせず、本書に統合しております。重複はありません。</b>",
        S_BODY))
    story.append(Paragraph(
        "<b>お願い（12-3〜12-5）と質問（12-6）を分けて"
        "書いております。</b>", S_BODY))

    story.append(CondPageBreak(75 * mm))
    story.append(Paragraph(
        "12-1. 期限 — ステージングに実データが入る前", S_H3))
    story.append(Paragraph(
        "<b>日付ではなく「イベント」で切っております。</b>", S_LEAD))
    story.append(Spacer(1, 3))
    story.append(table(DEADLINE_ROWS,
                       [42 * mm, 56 * mm, W - 98 * mm],
                       [S_CELL, S_CELL, S_CELL]))
    story.append(Spacer(1, 6))
    story.append(Paragraph(
        "<b>過ぎるとどうなるか</b> — <b>実データ投入後に同じことを"
        "行う場合、次が全部必要になります。</b>", S_LEAD))
    story.append(Spacer(1, 2))
    story.extend(numlist([
        "<b>DBアカウントの再発行</b> → <b>接続している側"
        "（当方のアプリ、貴社のデータ収集処理・集計バッチ）を"
        "いったん全部止めて差し替える必要があります</b>",
        "<b>TLS の有効化</b> → <b>サーバ再起動を伴う可能性があり、"
        "夜間の清掃データ収集を止められません</b>"
        "（D-Watch は<b>夜間に自動実行された清掃を翌朝確認する運用</b>です）",
        "<b>接続元IPの制限</b> → <b>実データが流れている状態で"
        "絞りますと、漏れた接続元からデータ収集が止まります</b>",
    ]))
    story.append(Spacer(1, 4))
    story.append(callout(
        "<b>つまり「止める調整」が発生します。"
        "いまであれば、その調整が要りません。</b><br/>"
        "<b>だからこそ、実データが入る前にお願いしております。</b>", CW))

    story.append(CondPageBreak(95 * mm))
    story.append(Paragraph(
        "12-2. 実測値（いつ・誰が・どう測ったか）", S_H3))
    story.append(Paragraph(
        "<b>推測ではありません。実測です。</b>", S_LEAD))
    story.append(Spacer(1, 3))
    story.append(table(MEASURE_META_ROWS, [30 * mm, W - 30 * mm],
                       [S_CELL, S_CELL]))
    story.append(Spacer(1, 6))
    story.append(table(TESTENV_ROWS, [46 * mm, 26 * mm, W - 72 * mm],
                       [S_CELL, S_CELL, S_CELL]))
    story.append(Spacer(1, 6))
    story.append(quote(
        "<b>本章の作成にあたり、テストDBへの再接続は行っておりません"
        "（意図的です）</b>。"
        "<b>参照専用のアカウントではありますが、接続条件が現在も"
        "揃っているかが不明なため</b>、"
        "<b>上記は「2026-09-13 に実測された値」です</b>。<br/>"
        "<b>すでにご対応済みでしたら、12-3〜12-5 は"
        "読み飛ばしてください。</b>", CW))
    story.append(Spacer(1, 6))

    story.append(CondPageBreak(60 * mm))
    story.append(Paragraph("<b>なぜ問題か</b>:", S_LEAD))
    story.append(Spacer(1, 2))
    story.extend(bull([
        "<b>have_ssl = DISABLED と require_secure_transport = OFF が"
        "重なりますと、接続は必ず平文になります</b>。"
        "<b>クライアント側で「TLSを使う」と指定しても、"
        "サーバーが提供していないため暗号化されません</b>",
        "<b>接続元が %（どこからでも）のため、"
        "経路は公開IP上にあります</b>",
        "この2つが重なりますと、"
        "<b>DB接続の資格情報が経路上で保護されていない状態</b>"
        "になります",
        "<b>現在は検証用データのみのため実害はありません。"
        "問題になるのは実データが入った後です</b>",
    ]))
    story.append(Spacer(1, 4))
    story.append(Paragraph(
        "<b>なお、このサーバーは弊社（DFA）が契約し、"
        "貴社に構築・保守をお願いしているものと認識しております。</b>"
        "<b>そのため、実施のご依頼は貴社宛といたしました。"
        "この認識に誤りがございましたら、その旨をお知らせください。</b>",
        S_BODY))

    story.append(CondPageBreak(70 * mm))
    story.append(Paragraph(
        "12-3. お願い1: 接続元の制限（本章の中で最優先です）", S_H3))
    story.append(Paragraph(
        "参照用アカウント@% の % を、<b>固定IP</b>"
        "（または閉域ネットワーク内のみ）に"
        "絞っていただけますでしょうか。", S_BODY))
    story.append(Paragraph("なぜ TLS より先にお願いするのか", S_H4))
    story.append(Paragraph(
        "当方が利用しているDB接続用のライブラリに、"
        "<b>「TLS を有効にしても、条件によっては接続時の資格情報が"
        "経路上に平文で出てしまう」という既知の問題</b>が"
        "報告されております"
        "（<b>当方が 2026-09-16 に自動検査ツールで確認した実測です</b>）。",
        S_BODY))
    story.append(Paragraph(
        "<b>これは弊社内製アプリケーション側の事情ですので、"
        "どう解くかは弊社で判断いたします。"
        "貴社にご対応をお願いするものではありません。</b>", S_BODY))
    story.append(Paragraph(
        "<b>ただし1点だけ、貴社へのお願いの優先順に関係いたします。</b>"
        "この問題がある間は、"
        "<b>TLS を有効にしていただいても資格情報が保護されない"
        "可能性が残ります</b>。", S_BODY))
    story.append(Spacer(1, 2))
    story.append(callout(
        "<b>そのため当方としては、TLS の有効化よりも先に"
        "「経路そのものを閉じる」＝接続元IPの制限のほうが効くと"
        "考えております。</b><br/>"
        "<b>恐れ入りますが、12-4（TLS）より 12-3（接続元の制限）を"
        "先にご検討いただけますと幸いです。</b>", CW))

    story.append(CondPageBreak(45 * mm))
    story.append(Paragraph("12-4. お願い2: TLS の有効化", S_H3))
    story.append(Paragraph(
        "have_ssl を有効にし、可能であれば "
        "require_secure_transport = ON に"
        "していただけますでしょうか。", S_BODY))
    story.append(Paragraph("<b>12-3 の後で構いません。</b>", S_BODY))
    story.append(Paragraph(
        "<b>なお 12-6 の Q12-4 が YES（VMとDBを閉域で結べる）と"
        "なる場合、本項は不要になる可能性があります。</b>", S_BODY))

    story.append(CondPageBreak(90 * mm))
    story.append(Paragraph(
        "12-5. お願い3: 本番用DBアカウントの新規発行", S_H3))
    story.append(Spacer(1, 2))
    story.append(callout(
        "<b>本番接続に使うアカウントは、現在のテスト用とは別のものを"
        "新規に発行していただけますでしょうか。</b><br/>"
        "<b>ユーザー名・パスワードとも</b>お願いいたします。", CW))
    story.append(Spacer(1, 6))
    story.append(Paragraph("理由", S_H4))
    story.extend(bull([
        "<b>過去に、実在のDBユーザー名が当方リポジトリのコミットに"
        "含まれたことがあります</b>"
        "（<b>2026-09-10 のコミットで除去済みですが、"
        "Git の履歴には残っております</b>）。"
        "リポジトリは非公開ですが、"
        "<b>アクセス権を持つ全員が履歴を読めます</b>",
        "<b>パスワードは当該コミットには含まれておりません</b>"
        "（架空の値のみでした）。"
        "<b>問題はユーザー名が既知になっている点です</b>",
        "<b>ユーザー名が既知である以上、12-2 の 参照用アカウント@%"
        "（どこからでも接続可）と組み合わさりますと、"
        "推測の手間が減ります</b>",
        "<b>あわせて、このアカウントの認証情報は、受け渡しの過程で"
        "平文のまま当方側の記録に残っております</b>。"
        "この点からも、"
        "<b>本番用は別の値を新規に発行していただくのが確実</b>と"
        "考えております",
    ]))
    story.append(Spacer(1, 4))
    story.append(Paragraph("お願いしていないこと", S_H4))
    story.extend(bull([
        "<b>Git 履歴の書き換えはお願いしておりません</b>"
        "（当方でも実施いたしません）。"
        "<b>「発行し直す」ほうで解いていただきたい、"
        "というお願いです</b>",
        "アカウントの発行元は貴社ですので、"
        "<b>再発行は貴社にしかお願いできません</b>",
    ]))
    story.append(Spacer(1, 4))
    story.append(Paragraph(
        "<b>タイミングは 12-1 のとおり、"
        "ステージングに実データが入る前です。</b>", S_BODY))

    story.append(CondPageBreak(90 * mm))
    story.append(Paragraph(
        "12-6. 確認: 本番構成について（6問）", S_H3))
    story.append(Paragraph(
        "<b>ここからは質問です。お願いではありません。</b>"
        "<b>Q12-1 以外は YES / NO でお答えいただけます。</b>", S_BODY))
    story.append(Paragraph("なぜお伺いするのか", S_H4))
    story.append(Paragraph(
        "<b>12-3〜12-5 の解き方（＝「経路を閉じる」）が"
        "成立するかどうかが、本番の構成にかかっております</b>。",
        S_LEAD))
    story.append(Spacer(1, 2))
    story.extend(bull([
        "VM と DB が同一の閉域ネットワークに入れるのであれば → "
        "<b>12-3・12-4 は「初めから起きない」</b>",
        "公開IP越しに接続する構成であれば → "
        "<b>現在の状態がそのまま本番に持ち越されます</b>",
    ]))
    story.append(Spacer(1, 4))
    story.append(Paragraph(
        "<b>当方は「閉じた構成になる見込み」と考えておりますが、"
        "これは当方の読み取りであり、"
        "明示の確認をいただいた記録がございません。</b>"
        "<b>推測のまま本番に進みますと、"
        "12-1 の「作り直し」に直結いたします。</b>", S_BODY))

    story.append(CondPageBreak(85 * mm))
    story.append(Paragraph("当方の現在の理解と、その根拠", S_H4))
    story.append(Spacer(1, 2))
    story.append(table(UNDERSTAND_ROWS,
                       [8 * mm, 56 * mm, 62 * mm, W - 126 * mm],
                       [S_CELL, S_CELL, S_CELL_S, S_CELL]))
    story.append(Spacer(1, 6))
    story.append(Paragraph(
        "<b>すなわち、貴社のご回答は「なぜ MariaDB なのか」の"
        "ご説明であり、「本番の構成はこうなります」という"
        "ご説明ではありませんでした。当方が読み取っただけです。</b>",
        S_BODY))
    story.append(Paragraph(
        "<b>そこで、読み取りが合っているかどうかを"
        "確認させてください。</b>", S_BODY))

    story.append(CondPageBreak(75 * mm))
    story.append(qhead(
        "Q12-1. アプリサーバー（VM）とDBサーバーは、"
        "それぞれどちらが用意する想定でしょうか", CW))
    story.append(Spacer(1, 4))
    story.append(Paragraph(
        "<b>当方の中で理解が揃っておりません。"
        "まずここを確定させていただきたく存じます。</b>", S_LEAD))
    story.append(Spacer(1, 3))
    story.append(table(Q121_ROWS, [12 * mm, 70 * mm, W - 82 * mm],
                       [S_CELL, S_CELL, S_CELL]))
    story.append(Spacer(1, 6))
    story.append(Paragraph(
        "<b>弊社の記録には「アプリ側は弊社」と読める記述と"
        "「貴社に用意していただく」と読める記述が"
        "混在しております。</b>"
        "<b>どちらが正かは貴社のご認識に合わせますので、"
        "お教えください。</b>", S_BODY))

    story.append(CondPageBreak(60 * mm))
    story.append(qhead(
        "Q12-2. 本番のDBは、さくらインターネットの"
        "データベースアプライアンス"
        "（＝VM上に自前で構築した MariaDB ではない）で"
        "間違いないでしょうか", CW))
    story.append(Spacer(1, 4))
    story.append(Paragraph("YES / NO", S_BODY))

    story.append(CondPageBreak(50 * mm))
    story.append(qhead(
        "Q12-3. 本番のアプリ（Node.js）は、"
        "さくらのクラウドの VM に載せる想定で"
        "間違いないでしょうか", CW))
    story.append(Spacer(1, 4))
    story.append(Paragraph("YES / NO", S_BODY))

    story.append(CondPageBreak(50 * mm))
    story.append(qhead(
        "Q12-4. その VM と DBアプライアンスは、"
        "同一の閉域ネットワーク（プライベート接続）で"
        "結べますでしょうか", CW))
    story.append(Spacer(1, 4))
    story.append(Paragraph("YES / NO", S_BODY))

    story.append(CondPageBreak(50 * mm))
    story.append(qhead(
        "Q12-5. Q12-4 が YES の場合、"
        "DB側で公開IPからの接続を完全に閉じることは"
        "可能でしょうか", CW))
    story.append(Spacer(1, 4))
    story.append(Paragraph("YES / NO / 条件つき", S_BODY))

    story.append(CondPageBreak(60 * mm))
    story.append(qhead(
        "Q12-6. Q12-4 が NO の場合（公開IP越しの接続になる場合）、"
        "TLS を有効にできますでしょうか", CW))
    story.append(Spacer(1, 4))
    story.append(Paragraph("YES / NO / 該当なし", S_BODY))
    story.append(Spacer(1, 2))
    story.append(Paragraph(
        "<b>NO や「条件つき」の場合は、差し支えなければ理由を"
        "1行だけ添えていただけますと助かります。</b>"
        "<b>当方はそれを前提に、12-3〜12-5 のお願いの内容を"
        "組み直します。</b>", S_BODY))

    story.append(CondPageBreak(70 * mm))
    story.append(Paragraph(
        "12-7. 当方（DFA）側で済ませたこと", S_H3))
    story.append(Paragraph(
        "<b>丸投げではない、というご説明のために書いております。</b>",
        S_BODY))
    story.append(Paragraph(
        "2026-09-13 に当方のアプリコードを棚卸しいたしました。"
        "<b>危険な箇所は1件も見つかっておりません</b>"
        "（生SQL 0件 / 並べ替えのカラム名は固定リテラル1箇所 / "
        "where にユーザー入力なし / "
        "エラー本文に内部情報を載せている箇所 0件 / "
        "DBへの書き込み 0件 / リポジトリ内に実在の認証情報なし）。",
        S_BODY))
    story.append(Paragraph(
        "あわせて、生SQL禁止の静的解析ルール化・"
        "パスパラメータ検証の全体一致化・"
        "CSVの数式インジェクション対策・"
        "本番前セキュリティ確認チェックリストの整備・"
        "DBユーザー名の既定値をコードから除去する対応を"
        "済ませております。", S_BODY))
    story.append(Spacer(1, 2))
    story.append(callout(
        "<b>問題があるのはコードではなく、DBへ至る「経路」のほうです。"
        "そして経路は、当方のコードでは閉じられません。</b><br/>"
        "<b>そのため本章の3件を、正式にお願いとしてお渡しいたします。</b>",
        CW))
    story.append(Spacer(1, 6))
    story.append(Paragraph(
        "<b>ご負担や副作用のご懸念があれば、その旨をお知らせください。"
        "当方で別の手を検討します。</b>", S_BODY))

    # ---------------------------------------------------------- おわりに
    story.append(CondPageBreak(60 * mm))
    story.append(Paragraph(
        "<b>以上です。ご確認のほど、よろしくお願いいたします。</b>",
        S_BODY))
    # ★Q11-1 は「取り下げ」ではない。取り下げた項目の列挙に混ぜないこと
    #   （点検スクリプトの 12. の FORBIDDEN が、混ざった形の残存を見る）★
    story.append(Paragraph(
        "<b>本改訂で取り下げた項目</b>: <b>5章 Q5-1</b>"
        "（面積の単位。弊社で㎡と確定）/ <b>10章</b>"
        "（DB更新用アカウント。すでに発行済みでした）/ "
        "<b>2章 Q2-2</b>。", S_BODY))
    story.append(Paragraph(
        "<b>取り下げではなく、お伺いする内容を改めた項目</b>: "
        "<b>11章 Q11-1</b> — 「実際の清掃開始時刻はどこにありますか」→ "
        "<b>「started_at を使って実装してよろしいでしょうか」</b>。"
        "<b>こちらはご回答をお願いいたします"
        "（A / B の一言で結構です）。</b>", S_BODY))
    # ★この文は Q11-1（本書で唯一「ご回答をお願いします」と書いた設問）の
    #   段落の直後に来る。「いずれもご対応は不要です」と書くと、せっかくの
    #   回答依頼を打ち消して読めてしまう。★対象を「取り下げた項目」に限定すること★
    #   （点検スクリプトの 12. の FORBIDDEN が旧文言の復活を見る）★
    story.append(Paragraph(
        "<b>上の「本改訂で取り下げた項目」については、"
        "ご対応は不要です。</b>"
        "<b>あわせて、テスト環境の「行の中身」を根拠にしていた指摘を"
        "取り下げております（冒頭の「おことわり」をご覧ください）。</b>",
        S_BODY))
    story.append(Spacer(1, 2))
    story.append(callout(
        "<b>1章（索引）はご対応中と伺いましたので、依頼を取り下げ、"
        "参考情報に改めております。</b><br/>"
        "<b>ご対応が完了しましたら、その旨をお知らせください。"
        "当方で EXPLAIN を測り直します。</b>", CW))
    story.append(Spacer(1, 6))
    story.append(callout(
        "<b>12章は、期限が「ステージングに実データが入る前」という"
        "性質のものです。</b><br/>"
        "<b>まずは 12-6 の Q12-1〜Q12-6 だけでも先にお答えいただけますと、"
        "12-3〜12-5 のお願いの内容を確定できます。</b>", CW))

    # ---------------------------------------------------------- ご回答欄
    # ご回答欄は印刷してご記入いただく想定なので、必ず新しいページから始める
    story.append(PageBreak())
    story.append(Paragraph("ご回答欄", S_TITLE))
    story.append(Paragraph(
        "<b>この表に一言だけご記入いただく形でも、"
        "下の自由記入欄にまとめてご記入いただく形でも結構です。</b>"
        "<b>選択肢のある設問は、記号（A / B / C …）に○を付けて"
        "いただくだけで結構です。</b>", S_LEAD))
    story.append(Spacer(1, 4))
    story.append(form_table(ANSWER_ROWS, [18 * mm, 100 * mm, W - 118 * mm],
                            [S_CELL, S_CELL, S_CELL]))
    story.append(Spacer(1, 8))

    story.append(CondPageBreak(60 * mm))
    story.append(Paragraph("自由記入欄", S_H3))
    story.append(Spacer(1, 3))
    story.append(blank_box(W, 42 * mm))
    story.append(Spacer(1, 6))
    story.append(Paragraph("引き続きよろしくお願いします。", S_NOTE))

    doc.build(story)
    print("生成:", OUT)


if __name__ == "__main__":
    build()
