Newer
Older
client_dwatch / docs / handoff / pdf / check_ddl_review_pdf.py
# -*- coding: utf-8 -*-
"""第1回 PDF「実DDL照合結果と確認事項」の自己点検。

原稿 (docs/handoff/TeamWorks実DDL照合結果_第1回_20260903.md) の文言が
すべて PDF に含まれているか、フォントが日本語フォントで埋め込まれているかを見る。
PDF のパスは第1引数で渡せる。省略時はこのスクリプトと同じディレクトリ。
"""
import os
import re
import sys

from pypdf import PdfReader
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont
from reportlab.lib.units import mm

PDF = (sys.argv[1] if len(sys.argv) > 1 else
       os.path.join(os.path.dirname(os.path.abspath(__file__)),
                    "D-Watch_実DDL照合結果_20260903.pdf"))
if not os.path.exists(PDF):
    raise SystemExit("PDF が見つからない: %s
"
                     "先に make_ddl_review_pdf.py を実行するか、"
                     "PDF のパスを第1引数で渡すこと。" % PDF)

raw = open(PDF, "rb").read()
reader = PdfReader(PDF)
pages = [p.extract_text() for p in reader.pages]
text = "\n".join(pages)
flat = re.sub(r"\s+", "", text)

print("== 1. ページ数とサイズ ==")
print("ページ数:", len(reader.pages), " サイズ:", len(raw), "bytes",
      "(%.1f KB)" % (len(raw) / 1024))
print("抽出文字数:", len(text))

print("\n== 2. フォント埋め込み ==")
print("/FontFile2 出現回数:", raw.count(b"/FontFile2"))
print("BIZUDGothic を含むフォント名:",
      sorted(set(re.findall(rb"/([A-Z]{6}\+[A-Za-z0-9\-]*BIZ[A-Za-z0-9\-]*)", raw))))
for i, p in enumerate(reader.pages, 1):
    fonts = p.get("/Resources", {}).get("/Font", {})
    names = []
    for k in fonts:
        fo = fonts[k].get_object()
        names.append(str(fo.get("/BaseFont")))
    print("  p%d:" % i, sorted(set(names)))

print("\n== 3. 原稿の全項目が含まれるか ==")
must = [
    # 表題・前文
    "実DDL照合結果と確認事項", "TeamWorks(DB開発側)", "作成日:2026-09-03",
    "DFARobotics", "clean_watch_20260902.sql", "CREATETABLE39個",
    "Prismaスキーマと機械的に照合",
    "prismadbpullで生成したスキーマと比較",
    "DDLを直接パースしてカラム定義を抽出し、比較",
    "差分は49件でした", "42件はこちらで実DDLに合わせるだけで完結",
    "こちらだけでは判断できない3件", "共有しておきたい3件", "仕様の確認1件",
    # サマリ表
    "A.合意が必要", "返答が無いと実装を決められない",
    "B.情報共有", "返答は必須でない。DB側でしか対処できないものを含む",
    "C.仕様確認", "以前の合意事項の解釈",
    "こちらで完結する42件については本書に記載していません",
    # A-1
    "A-1", "robot_error_reportsの主キーとidの意味", "最も影響が大きい項目です",
    "主キーでもUNIQUEでもない。制約なし",
    "robot_error_reports_report_id_unique",
    "PrecheckItem.id:number", "PATCH/prechecks/{id}/status",
    "idは一意でないため、その値で1行を特定できません",
    "idは文字列なので、numberとして扱うと不一致が起きます",
    "更新できるのはerror_idだけです",
    "このため対応状況の更新(PATCH)が設計として成立しません",
    "idに一意制約を付ける予定はありますか",
    "こちらのAPIが画面に返すべきIDはidとerror_idのどちらですか",
    "こちらの型をnumberからstringに変更します",
    # A-2
    "A-2", "履歴テーブルのidは誰が採番するのか",
    "clean_status_historiesの状態",
    "AUTO_INCREMENTではない", "clean_status_histories_id_unique",
    "1つの清掃状態につき履歴を1件しか作れません",
    "clean_timetable_histories.idも同様にAUTO_INCREMENTがありません",
    "アプリ側で連番を振る想定でしょうか",
    "同じ清掃状態の履歴が複数回作られることはない前提でよろしいですか",
    "採番方法が決まらないと投入できません",
    # A-3
    "A-3", "先の回答(contact系の型)はいつ反映されますか",
    "先の確認事項B-6について",
    "typoです。contact_typeはunsignedtinyintとします。emailはvarchar(100)とします",
    "customer_contacts.contact_type", "customer_contacts.email",
    "facility_contacts.contact_type", "facility_contacts.email",
    "bigintunsigned", "tinyintunsigned(一致)", "varchar(255)",
    "顧客側と施設側で型が食い違った状態が残っています",
    "当面は現行のDDLに合わせて実装してよろしいですか",
    "変更を入れる時期の目安があればお知らせください",
    "JavaScriptのbigint型として受け取ることになり",
    # B-1
    "B.情報共有(3件)", "返答は必須ではありません。こちらは現状のまま実装を進められます",
    "B-1", "外部キーが3本欠けています",
    "定義書ではfkと記載されているものの、実DDLに制約が見当たりませんでした",
    "clean_statuses", "clean_timetable_id", "clean_timetables(id)",
    "clean_status_histories", "robot_task_versions", "clean_schedule_id",
    "clean_schedules(id)",
    "参照先が存在しない行(孤児行)を想定した実装にします",
    "返答をお待ちせず、孤児行を想定した実装で進めます",
    # B-2
    "B-2", "インデックスが1つ欠けています(DB側でしか対処できません)",
    "robot_task_versions.clean_schedule_idにインデックスがありません",
    "MySQLによる自動作成も行われていません",
    "300施設分のタスク履歴が積み上がるテーブルのため",
    "こちら側に回避手段がないため、DB側でのご対応をお願いしたい項目です",
    # B-3
    "B-3", "timestampNOTNULLにDEFAULT句がありません",
    "clean_task_reports", "created_at,updated_at",
    "security_status_histories", "reported_at",
    "INSERT時に明示的な値の指定が必要です",
    "created_atに既定値が無いのは珍しい形のため念のため共有します",
    "こちらは参照のみのため実装への影響はありません",
    # C-1
    "C.仕様確認(1件)", "C-1", "アドバイス規則R3の条件に「出発失敗」を含みますか",
    "R3=完了率が△以下の状態が2日以上連続",
    "R4=出発失敗または△以下の状態が3日以上連続",
    "「連続問題日数」という1つの値を見ており、区別できません",
    "現状では2日連続の出発失敗でR3が発火します",
    "R3の条件に出発失敗を含めるのが正しいですか",
    "出発失敗はR4だけの条件ですか",
    "実装方針が変わります",
    # 参考表 8 行
    "参考:照合により解決した事項",
    "対応日時のカラム名", "responded_at(正しい綴り)。定義書のresponsed_atは誤記と確定",
    "control_reportsのシリアル番号", "serial_number。定義書のsireal_numberは誤記と確定",
    "clean_statusの-1問題", "実DDLは符号ありtinyint。定義書の型表記が誤りで、備考が正しかった",
    "makersテーブル", "DDLに存在せず、devicesからの外部キーも無し。ご回答どおり",
    "controls.facility_idの外部キー", "存在を確認。ご回答どおり",
    "clean_timetables.precheckの既定値", "0(本清掃)。定義書の9は誤りと確定",
    "clean_link_schedules", "不要テーブルとご確認済み。link_schedulesを参照します",
    "section_robot_statuses.indicatived_at", "綴り違いではなく、列自体が廃止されていました",
    "28テーブル中12テーブルが実DDLと完全一致していました",
    # 末尾
    "本書のA(3件)についてご回答をいただければ",
    "対応状況の更新機能とダミーデータによる検証に着手できます",
    "B・Cについては実装を止めるものではありませんので、お手隙の際にご確認ください",
]
ng = [m for m in must if re.sub(r"\s+", "", m) not in flat]
print("検査項目:", len(must), " 欠落:", len(ng))
for m in ng:
    print("  NG:", m)

print("\n== 4. 長いカラム名が途中で折り返していないか ==")
pdfmetrics.registerFont(TTFont("JP", "C:/Windows/Fonts/BIZ-UDGothicR.ttc", subfontIndex=0))
# (識別子, 列幅mm, 左右パディング合計pt, フォントサイズ)
cases = [
    ("customer_contacts.contact_type", 52, 12, 8.2),
    ("facility_contacts.contact_type", 52, 12, 8.2),
    ("customer_contacts.email", 52, 12, 8.2),
    ("clean_status_histories", 48, 12, 8.2),
    ("robot_task_versions", 48, 12, 8.2),
    ("clean_timetable_id", 42, 12, 8.2),
    ("clean_schedule_id", 42, 12, 8.2),
    ("clean_timetables(id)", 174 - 90, 12, 8.2),
    ("security_status_histories", 55, 12, 8.2),
    ("created_at, updated_at", 174 - 55, 12, 8.2),
    ("section_robot_statuses.indicatived_at", 66, 12, 8.2),
    ("clean_timetables.precheck の既定値", 66, 12, 8.2),
    ("control_reports のシリアル番号", 66, 12, 8.2),
    ("robot_error_reports_report_id_unique", 174 - 6 - 14, 14, 8.2),
]
worst = 0.0
for s, w_mm, pad, size in cases:
    need = pdfmetrics.stringWidth(s, "JP", size)
    avail = w_mm * mm - pad
    ratio = need / avail
    worst = max(worst, ratio)
    print("  %-40s 必要 %6.1fpt / 空き %6.1fpt  %s (使用率 %.0f%%)"
          % (s, need, avail, "OK" if need <= avail else "折返し!", ratio * 100))

# 抽出テキスト上でも分断されていないことを確認(改行が入っていないか)
print("\n  抽出テキスト内での連続性:")
for s in ["customer_contacts.contact_type", "facility_contacts.contact_type",
          "section_robot_statuses.indicatived_at", "clean_status_histories",
          "security_status_histories", "robot_error_reports_report_id_unique",
          "robot_task_versions.clean_schedule_id", "clean_status_histories_id_unique"]:
    print("   ", s, "->", "連続" if s in text else "分断/未検出")

print("\n== 禁止文字の検査 ==")
bad = [c for c in text if c in "\u2605\u2606" or 0x1F300 <= ord(c) <= 0x1FAFF
       or 0x2070 <= ord(c) <= 0x209F]
print("★・絵文字・上付下付:", "なし" if not bad else set(bad))
src = open(os.path.join(os.path.dirname(PDF), "make_ddl_review_pdf.py"),
           encoding="utf-8").read()
print("スクリプト内の★:", "なし" if "\u2605" not in src else "あり")

print("\n結果:", "OK" if (not ng and not bad and worst <= 1.0) else "要修正")