Skip to content

ドキュメントビルド環境の現代化(Sphinx 1.3.6 → 9.1.0 / Python 3.12 / docutils 0.22) - #729

Open
kumagoro1202 wants to merge 5 commits into
developfrom
sphinx-modernization
Open

ドキュメントビルド環境の現代化(Sphinx 1.3.6 → 9.1.0 / Python 3.12 / docutils 0.22)#729
kumagoro1202 wants to merge 5 commits into
developfrom
sphinx-modernization

Conversation

@kumagoro1202

Copy link
Copy Markdown
Contributor

概要

#727(成果物バイト完全一致を条件とした周辺ツールチェーン更新)の次段階として、HTML生成の中核(Sphinx / docutils / テーマ / Javaドメイン拡張)を含むビルド環境全体を現代化します。

Sphinx本体のメジャー更新(1.3.6 → 9.1.0)を伴うため、今回は成果物HTMLに差異が生じます。そこで合否条件を「見た目(テーマ・配色等)の変化は許容。ただし (a) 本文テキストの欠損・欠落・文字化けが1件もないこと、(b) 内部リンク・アンカー・画像・ダウンロード資材の破損が1件もないこと」と定め、専用の意味的差分検証器を作成して新旧の全ビルド成果物を機械検証しました。

結果は 988ページ完全1:1対応(欠損0・追加0)・本文情報の欠損0件・リンク等の破損0件 です。残る差異は活字表現上の表示差598件のみで、全件を機械分類し原因を特定済みです(後述。旧環境が設定意図に反して行っていた引用符自動変換の消失559件+脚注後方参照の表記差39件)。

変更内容(before → after)

項目 before after
Sphinx 1.3.6(2016年) 9.1.0
Python(Docker基底) 3.10.20(python:3.10.20-slim) 3.12(python:3.12-slim。Sphinx 9.1がPython 3.12以上必須のため)
docutils 0.15.2 0.22.4(Sphinx 9.1の許容上限が<0.23のため0.22系を採用)
sphinx-rtd-theme 0.2.4 3.1.0(Sphinx 9.x / docutils 0.22対応を公式明記した版)
Pygments 2.4.2 2.20.0
javasphinx PyPI 0.9.15(アーカイブ済み・Sphinx 9で起動不能) _extensions/javasphinx/ へ同梱化(vendoring)(詳細後述)
requirements 全依存の手動ピン requirements.in(直接依存)+ requirements.txt(pip-compile生成lock)の2層構成(lockはpython:3.12-slimコンテナ内で生成)
textlint用Python依存 ビルド環境と同居 requirements-lint.txt 新設(docutils 0.15.2+docutils-ast-writer 0.1.2+six。Python 3.10前提)
Dockerfile 単一ステージ マルチステージ(デフォルト=docs: Python 3.12+Sphinx 9 / --target lint: Python 3.10+Node 22。旧parserシム・setuptoolsピンはlintステージにのみ維持)
conf.py(ja/en対称に同修正) add_javascriptadd_js_file(Sphinx 4.0で削除されたAPIの置換)、extlinksキャプションの'%s'化、無害な迷い込み行[extensions]の清掃、_extensions/のsys.path追加

javasphinxの同梱化(vendoring)について

  • 原本 bronto/javasphinx は2019年にアーカイブ済みで、Sphinx 9では起動不能です(Sphinx 4で削除された sphinx.locale.l_ のImportError)。保守されたフォーク・後継は存在しません。
  • 本リポジトリでの実用途は :java:extdoc: ロール(日英計3,948箇所)と .. java:method:: ディレクティブ(日英各1箇所)のみのため、必要部分(__init__.py / domain.py / extdoc.py / formatter.py / util.py)を _extensions/javasphinx/ に同梱しました。ビルド未使用のapidoc系(apidoc.py / compiler.py / htmlrst.py)は同梱せず、lxml / beautifulsoup4 / future への依存を解消しています。
  • ライセンスは原本のApache License 2.0全文をLICENSEとして同梱し、各ソースの著作権ヘッダも原本のまま保持。README.mdに出所(bronto/javasphinx 0.9.15)・経緯・変更点を明記しています。
  • 原本からの変更は次の2点のみです。
    1. domain.py: from sphinx.locale import l__(Sphinx 4以降対応の最小パッチ)
    2. extdoc.py: 現行Sphinxは明示タイトルの正規表現が変更され、ターゲットに < を含む Class.<init>(...) 記法(日英4ファイル・計6箇所)が解釈エラーになるため、extdocロール内に限定して旧来の解釈を実装(rst本文は一切変更せず、:ref:等の他ロールへの副作用もありません)
  • .. java:method:: のシグネチャ解析に必要な javalang==0.13.0 は維持しています。

lint環境を分離した理由

textlint-plugin-rst が依存する docutils-ast-writer 0.1.2 は use_2to3 を用いる旧世代パッケージで、Python 3.12では導入経路が存在しません(distutils削除によりsetuptools 57.5.0自体が動作しない)。textlintはHTML出力に関与しないため、lint環境をPython 3.10のDockerステージとして分離しました。成果物への影響はありません。

検証の観点と方法

新旧2つのビルド済みHTMLツリーを意味レベルで比較する検証器(semdiff.py。本PRの tools/doc-build-verifier/ に同梱)を作成し、以下の4観点を機械検証しました。比較対象は同一コミットのrstソースを旧環境・新環境それぞれでフルビルドした2ツリーです。

  1. ページの1:1対応 — 全ページの欠損・追加を全数検査(対応欠落=NG)。
  2. 逐語コピーページのバイト同一_static/**/*.html(yuidoc生成のJS APIドキュメント等)はSphinxが変換せず複製するため、より厳格なバイト同一で照合。
  3. 本文テキストのマルチセット包含検査 — 本文コンテナ *[role="main"] からブロック要素単位で可読テキストを抽出(script/style/パーマリンク¶は除去。正規化は連続空白の畳み込みのみで、Unicode正規化は情報改変になり得るため不採用=バイト等価比較)。旧ページの全テキスト片が新ページに含まれるかを多重集合で照合し、見つからない片はtext-missingとしてNG。文字化けもバイト等価比較によりtext-missingとして検出されます。新テーマ由来の追加文言(UI文字列等)は許容(包含照合)。
  4. リンク・アンカー・画像・DL資材の解決検査 — 本文域の a[href] / img[src] 等を全数列挙し、内部リンク・アンカーは新ビルド内で解決するか(サイト内自己整合)を検査。画像・DL資材は参照経路をキーに内容SHA-256の多重集合で照合(版間のファイル名ハッシュ規則変化に頑健。内容が失われればNG)。旧ビルドに元から存在する破損は除外し、新規リグレッションのみNG。旧アンカーIDが新に存在しない差分(外部からの深リンク切れの可能性)は合否と別にreport-onlyで全件記録。

さらに、検証方式自体の穴を塞ぐ検査を併用しています。

  • 本文抽出セレクタの被覆率検査(coverage): 全ページの全可読テキストノードが「本文コンテナ配下 ∪ 既知テーマUI(nav/フッタ等の許容リスト)配下」のいずれかに属することを構造検査。抽出漏れによるfalse-PASSを防ぎます。
  • 検証器の自己テスト(canary): 基準ビルドを故意破壊して検証器自体の検出能力を確認。15ケース全PASS(内訳: 被覆率全数検査1件+真陽性9件+真陰性5件)。
    • 真陽性(破壊を確実にNG検出): 一文の後半削除 / ページ削除 / 参照画像の削除・別内容への差替 / 内部リンク破壊 / アンカーid削除 / DL資材削除 / 文字化け(U+FFFD)注入 / 本文コンテナ外へのテキスト注入
    • 真陰性(無害な差異を誤検出しない): 完全同一コピー / span分割・class追加・UI文言追加 / 画像ファイル名変更+href整合更新(内容同一) / アンカーidのビルド内整合改名 / 旧側からの削除(包含照合の仕様確認)

実行コマンド:

python3 tools/doc-build-verifier/semdiff.py coverage NEW_TREE --json cov_new.json   # 被覆率検査。exit 0=100%被覆
python3 tools/doc-build-verifier/semdiff.py verify OLD_TREE NEW_TREE --json report.json   # 合否検証。exit 0=PASS / 2=差異あり
python3 tools/doc-build-verifier/canary_suite.py BASELINE_TREE WORK_DIR   # 検証器の自己テスト

(依存: Python 3.12+lxml)

実測結果

  • 旧(基準): developの c241906 を従来環境(Sphinx 1.3.6 / Python 3.10)でja+enフルビルドした988ページ
  • 新: 本ブランチを新環境(Sphinx 9.1.0 / Python 3.12 / docutils 0.22.4)でja+enフルビルドした988ページ(ビルド実施時点のcommitは 19a641b。それ以降の本ブランチの差分は検証器 tools/doc-build-verifier/ の追加のみで、ビルド成果物には影響しません)
検証項目 実測値
ページ対応 988ページ完全1:1(欠損0・追加0)
逐語コピーページ 318/318 バイト同一
本文テキスト包含 text-missing 598件 → 全件が表示差(次節)・本文情報の欠損0件
リンク・アンカー・画像・DL資材 新規リグレッション0(新ビルド内の破損参照も0)
旧→新アンカーIDの差集合(report-only) 0件(外部サイトからの深リンクが切れる懸念なし)
本文抽出セレクタ被覆率 100.0000%(666ページ・被覆漏れ0)
gray(人手判定が必要な差異) 0件
生成系UIページ genindex.html / search.html(ja+en)計4件は存在確認のみ(本文がrst由来でないため・仕様どおり)

検証器の実出力:

[semdiff coverage] pages=666 coverage=100.0000% gaps=0 no-main=0 verdict=PASS
[semdiff verify] verdict=NG ng=598 gray=0 compared=666 (byte-identical=0, display-only=482)

新環境ビルドの完全性: ja 334rst→495HTML(実測4分00秒・警告3件)/ en 332rst→493HTML(4分14秒・警告1件)、総2426ファイル・全17トップレベルセクション。警告は全てJSPコードブロック内の日本語句読点によるハイライト失敗→relaxedモード自動回復(misc.highlighting_failure)で、可読テキストへの影響はありません。rst参照系警告(unknown target等)は0件です。

表示差598件の内訳と根本原因

検証器は意図的に「バイト等価・正規化なし」で設計しているため、意味を変えない活字差もNGとして検出します(検出自体は正当な動作です)。NG 598件(184ページ: ja 54 / en 130)を、検証器と同一の抽出・正規化ロジックを用いた照合スクリプトで全件機械分類しました(残余0件)。

分類 件数 内容
(a) 引用符・約物の活字差 559 旧: “ ” ‘ ’ – — …(自動変換後) / 新: " ' -- --- ...(ソース原文のまま)。逆写像を適用すると旧本文の全文が新側に存在することを機械照合で確認
(b) 脚注後方参照の表記差 39 旧: (1, 2)(カンマ後スペース) / 新: (1,2)。脚注本文は全量新側に存在
本文情報の欠損 0

(a)の根本原因: 本リポジトリのja/en docutils.conf には以前から smart_quotes: false が明示されています(かぎ括弧の自動変換を無効化する意図のコメント付き)。ところが旧Sphinx 1.3.6は、この設定が及ばないHTMLライタ層のSmartyPants変換(html_use_smartypantsデフォルト有効)で引用符類を自動変換していました。つまり旧ビルドの曲引用符は、リポジトリ自身の設定意図に反して出力されていたものです。新Sphinx 9は smart_quotes: false を正しく尊重し、ソース原文の記号をそのまま出力します。

例(ja/about_nablarch/versionup_policy.rst、ソースは "architect"):

出力
@Published(tag = “architect”)(自動変換)
@Published(tag = "architect")(ソースどおり)

(b)の根本原因: docutils 0.22で脚注のHTML構造が <table> から <aside> へ変更された副作用で、脚注の後方参照リストの区切り表記が変わったものです。この文字列はビルダーが生成するナビゲーションUI文言であり、著者が書いた本文ではありません。

以上より、598件はいずれも意味的な情報欠損ではなく、ソースおよびリポジトリの設定意図に忠実な新挙動として受容する方針です。

脆弱性の状況

  • pip-audit: requirements.txt(docs)・requirements-lint.txt とも既知脆弱性0件。
  • npm audit: moderate 3件が残存(@hono/node-server<2.0.5のWindows限定path traversalが@modelcontextprotocol/sdk経由でtextlint≥15.5.1に連鎖するもの)。修正にはtextlint 15.5.0への後退(breaking)が必要なため、#727と同方針(npm auditは情報表示・合否判定外)で据え置き。lint環境限定でありHTML成果物には無関係です。
  • 参考: 現行developの旧pip依存にはGitHub報告の脆弱性13件(high 7 / moderate 5 / low 1)があり、本更新のマージによりpip側は解消される見込みです。

再現手順(第三者による追試)

1. 旧環境(基準)のビルド

git checkout c241906   # 本検証時のdevelop先端
docker build -t nablarch-document-build-old .
docker run --rm -v <クローンしたディレクトリ(フルパス)>:/root/document nablarch-document-build-old /bin/bash -c "cd /root/document; sphinx-build -d _build/.doctrees/ja -b html ja _build/html"
docker run --rm -v <クローンしたディレクトリ(フルパス)>:/root/document nablarch-document-build-old /bin/bash -c "cd /root/document; sphinx-build -d _build/.doctrees/en -b html en _build/html/en"

生成された _build/html(ja 495+en 493=988 HTMLページ)を OLD_TREE として退避します。

2. 新環境のビルド

git checkout sphinx-modernization
docker build -t nablarch-document-build .   # デフォルト=docsステージ(lintステージは --target lint)
docker run --rm -v <クローンしたディレクトリ(フルパス)>:/root/document nablarch-document-build /bin/bash -c "cd /root/document; sphinx-build -d _build/.doctrees/ja -b html ja _build/html"
docker run --rm -v <クローンしたディレクトリ(フルパス)>:/root/document nablarch-document-build /bin/bash -c "cd /root/document; sphinx-build -d _build/.doctrees/en -b html en _build/html/en"

期待結果: ja「build succeeded, 3 warnings.」/en「build succeeded, 1 warning.」(いずれもJSPハイライト警告のみ)。_build/htmlNEW_TREE とします。

3. 検証器の実行(Python 3.12+lxml)

検証器一式(semdiff.py / canary_suite.py / README / 自己テスト実行結果)は本PRのブランチに同梱されています。手順2で sphinx-modernization をcheckoutした時点で、クローン内の tools/doc-build-verifier/ 配下に揃っており、追加の入手作業は不要です。

python3 tools/doc-build-verifier/semdiff.py coverage NEW_TREE --json cov_new.json
# 期待: [semdiff coverage] pages=666 coverage=100.0000% gaps=0 no-main=0 verdict=PASS(exit 0)

python3 tools/doc-build-verifier/semdiff.py verify OLD_TREE NEW_TREE --json report.json
# 期待: [semdiff verify] verdict=NG ng=598 gray=0 compared=666 (byte-identical=0, display-only=482)(exit 2)

report.json のsummary期待値:

{
 "pages_old": 988, "pages_new": 988, "pages_compared": 666,
 "verbatim_pages_compared": 318, "verbatim_pages_identical": 318,
 "generated_pages_skipped": 4,
 "missing_pages": [], "added_pages": [],
 "ng_count": 598, "gray_count": 0,
 "byte_identical": 0, "display_only": 482, "text_pass_with_additions": 0,
 "old_build_broken_refs": 0, "new_build_broken_refs": 0,
 "broken_ref_regressions": 0
}

ng 598件が全て前述の表示差(引用符559+脚注39)であること、missing_pagesadded_pagesbroken_ref_regressions が空/0であることが確認ポイントです。検証器の健全性は tools/doc-build-verifier/canary_suite.py(前述の15ケース)で確認できます。当方環境での実行結果は同ディレクトリの canary_results.txt に収録しています。

🤖 Generated with Claude Code

kumagoro1202 and others added 5 commits July 23, 2026 22:43
ビルド成果物を一切変えずにビルド環境を最新化する。

- ベースイメージ: python:3.8.18-slim (EOL) -> python:3.10.20-slim
  - Sphinx 1.3.6 は Python 3.11 の正規表現仕様変更で起動不能のため、
    現行ツールチェーンでの上限は 3.10 系
  - docutils-ast-writer の use_2to3 対応のため setuptools==57.5.0 に
    固定し --no-build-isolation でインストール
  - Python 3.10 で削除された stdlib parser モジュールの互換シムを追加
    (無指定コードブロックのハイライト判定を 3.8 時代と同一に保つため)
- Node.js: 実体は Debian 標準の 18 系だったものを NodeSource 22 系 LTS へ
  - 従来はベースイメージに curl が無く NodeSource セットアップが無言で
    失敗し、Debian 標準の nodejs 18.20.4 が入っていた
  - curl を明示導入し、スクリプトをファイル保存してから実行することで
    ダウンロード失敗をビルド失敗として検知するよう修正
- textlint: 12.2.1 -> 15.7.1 / patch-package: 6.4.7 -> 8.0.1
  - npm audit の high 深刻度 5 件を解消 (残りは moderate 3 件のみ:
    textlint 15 の MCP サーバ機能由来で lint 用途では影響なし)
  - textlint-plugin-rst 0.1.1 と既存パッチはそのまま動作
- Python 支援ライブラリ (requests / urllib3 / certifi / lxml ほか) を更新
  - HTML 生成に関与する Sphinx / docutils / Pygments / テーマ等は据え置き

検証: 更新前後の環境で日英ドキュメントをフルビルドし、全 2378 ファイルの
SHA-256 が完全一致することを確認 (更新前環境の 2 回ビルドで決定性も確認)。
textlint は同一入力に対し新旧で同一の検出結果を返すことを確認。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
HTML生成中核を保守が継続している最新ツールチェーンへ更新し、
継続開発可能な状態にする。

- Sphinx 1.3.6 -> 9.1.0 / docutils 0.15.2 -> 0.22.4 /
  sphinx-rtd-theme 0.2.4 -> 3.1.0 / Pygments 2.4.2 -> 2.20.0
- Python: 3.10 -> 3.12 (Sphinx 9.1 の下限要件)
- javasphinx 0.9.15 (2019年アーカイブ済み・現行Sphinxで起動不能) の
  必要部分 (Javaドメイン + :java:extdoc: ロール) を _extensions/ へ
  同梱し、外部の放棄パッケージへの依存を解消 (Apache-2.0、原本の
  ライセンス表記を保持)
  - sphinx.locale.l_ の削除対応 (l_ -> _)
  - コンストラクタ参照 Class.<init>(...) の明示タイトル解釈を
    ロール内に実装 (Sphinx 4 以降の正規表現変更への対応。
    rst本文は変更しない)
- conf.py: add_javascript -> add_js_file、extlinks キャプション '%s' 化
- requirements を直接依存 (requirements.in) + lock (requirements.txt) 構成へ変更
- Dockerfile をマルチステージ化し、HTML生成環境 (Python 3.12) と
  textlint環境 (Python 3.10 + docutils-ast-writer) を分離
- README を新環境の構築・ビルド手順へ更新
Sphinx更新前後のビルド済みHTMLツリーを意味レベルで比較する検証器
(semdiff.py)・その自己テスト (canary_suite.py) と実行結果・READMEを
同梱する。ビルド時にはロードされない独立ツールのため _extensions/
とは分離して配置。将来のツールチェーン更新時の回帰検証にも再利用可能。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
# Conflicts:
#	Dockerfile
#	README.md
#	requirements.txt
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant