From 6ede0e525649845d94cb0e07a893bd854cc322ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A5=88=E8=89=AF=20=E8=8B=B1=E6=A8=B9?= Date: Tue, 27 Oct 2026 20:28:45 +0900 Subject: [PATCH 1/7] =?UTF-8?q?=E9=9B=9B=E5=BD=A2=E3=81=AE=E3=83=95?= =?UTF-8?q?=E3=83=83=E3=82=AF:=20=E7=B4=A0=E3=81=AE=20URL=20=E3=81=AE?= =?UTF-8?q?=E4=B8=AD=E3=81=AE=20Q&A=20=E3=81=AE=E7=95=AA=E5=8F=B7=E3=82=92?= =?UTF-8?q?=E6=9B=B8=E3=81=8D=E6=8F=9B=E3=81=88=E3=81=AA=E3=81=84=E3=83=BB?= =?UTF-8?q?decision=5Ftable=20=E3=81=AE=E5=8D=B0=E3=81=A7=E6=B1=BA?= =?UTF-8?q?=E5=AE=9A=E8=A1=A8=E3=82=92=E6=8F=8F=E3=81=8F=EF=BC=88#376?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 汎用の CSV の印は足さない(仕様の穴として推奨で決めた。確認は #480)。 Fixes #376 Fixes #341 Fixes #369 Co-Authored-By: Claude Opus 5.5 --- .claude-plugin/marketplace.json | 2 +- docs/plans/issue-376.md | 102 ++++++++++++++++++ plugins/spec-loop/.claude-plugin/plugin.json | 2 +- plugins/spec-loop/references/tree.md | 9 +- plugins/spec-loop/skills/feasibility/SKILL.md | 4 +- plugins/spec-loop/skills/proposal/SKILL.md | 4 +- plugins/spec-loop/skills/req-loop/SKILL.md | 4 +- plugins/spec-loop/skills/scaffold/SKILL.md | 4 +- plugins/spec-loop/skills/spec-review/SKILL.md | 4 +- .../templates/mkdocs/docs/decision-table.md | 6 ++ .../templates/mkdocs/hooks/spec_loop_pages.py | 48 ++++++++- scripts/test-init-mkdocs.py | 71 +++++++++++- 12 files changed, 244 insertions(+), 16 deletions(-) create mode 100644 docs/plans/issue-376.md create mode 100644 plugins/spec-loop/templates/mkdocs/docs/decision-table.md diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 448012d5..5c4bd103 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -41,7 +41,7 @@ { "name": "spec-loop", "source": "./plugins/spec-loop", - "version": "0.26.0", + "version": "0.28.0", "author": { "name": "hdknr" }, "description": "要求側のループ(要求確定・仕様レビュー・実現性とコスト・提案書)と、dev-loop への引き渡しを担うプラグイン。現在のスキルは req-loop(Q&A 表への起票(推奨と理由つき)・回答の反映・レビュー会用 PPTX の生成で、利用者の回答待ちで止まる)と spec-review(開発者向けの仕様を書き下し、書き込みツールを持たない 3 つの判定役に別ティアでレビューさせ、利用者に見える結果が変わる指摘は共有の Q&A 表に差し戻し、費用・運用だけが変わる選択は内部の Q&A 表に記録し、範囲の外の場面と実現の手段の指摘は仕様に足さない。抜ける条件を満たさなければ抜けたと報告しない)と feasibility(規模から見積りとスケジュールを更新し、ガントチャートをスクリプトで生成し、費用付きの質問を Q&A 表に起票する。予算責任者の合意待ちで止まる)と proposal(同じ版の 2 つのサインオフのタグが同じ共有の範囲(docs/v/shared/ ほか)を指すかをスクリプトで照合し、一致し、かつ暫定の回答が残っていないときだけ提案書と仕様書(PPTX も選べる)を生成し、利用者にまだ確かめていない暫定の決定を合意と分けて載せる。v2 以降は前の版との比較を載せた変更の提案書になる。決裁者の承認待ちで止まる)と scaffold(確定した仕様を中間表現 spec_ir.json に書き起こし、OpenAPI・型・実装が無いと失敗する Red テストと dev-loop に渡す Issue の本文をスクリプトで生成して、1 機能 = Issue 1 件で登録する。v2 以降は前の版と比べて機能を追加・変更・削除に分け、変わった機能だけを Issue にする。Issue の登録で止まる)の 5 本" } diff --git a/docs/plans/issue-376.md b/docs/plans/issue-376.md new file mode 100644 index 00000000..cdfd3056 --- /dev/null +++ b/docs/plans/issue-376.md @@ -0,0 +1,102 @@ +# Issue #376 — 雛形のフックで素の URL を壊さず、決定表を描く(#341・#369 のまとめ) + +雛形のフック(`templates/mkdocs/hooks/spec_loop_pages.py`)の 2 つの穴を 1 周で直す——素の本文の URL の中の Q&A の番号を +リンクに書き換えて URL を壊す(#341)、決定表(`decision_table.csv`)を描く印が無く CSV への直リンクが開けない(#369)。 + +**不在の周**である——出所は `/dev-loop:cruise 474 --absent`(2026-10-27、利用者が会話で「-absent でまわします」と指示)。 +台帳 `issue-474` の確かめた線: `仕様の穴 = 推奨で決めて起票`・`直す線 = 既定`・`マージ = 関門を通ればマージ`・`周の途中の課題 = 台帳に足して直す` +(出所: cruise の確かめた線(台帳 `issue-474`))。**このリポジトリのマージはサイトを再デプロイするので `マージ` の線は効かない**(#476)——PR まで作って止まる。 + +## 変更範囲 + +触る: + +- `plugins/spec-loop/templates/mkdocs/hooks/spec_loop_pages.py` — 素の URL を除いて番号をリンクにする・`decision_table` の描画関数・docstring +- `plugins/spec-loop/templates/mkdocs/docs/decision-table.md` — 決定表の表示用ページ(新規。`init-mkdocs.py` は雛形を全部写すので本体は変えない) +- `scripts/test-init-mkdocs.py` — 素の URL・決定表の試料と変異、`tree.md` の照合の語 +- `plugins/spec-loop/references/tree.md` — ツリー・写すもの・`decision_table` の印・CSV へ直接リンクしない決まり・0.28.0 の取り込み +- `plugins/spec-loop/.claude-plugin/plugin.json` — 版(0.26.0 → 0.28.0。機能の追加で minor。#477 が 0.26.1・#479 が 0.27.0 に上げるので重ならない値) +- `.claude-plugin/marketplace.json` — 同じ版 +- `plugins/spec-loop/skills/*/SKILL.md` — 同じ版の 2 行 +- `docs/plans/issue-376.md` — この計画 + +触らない: + +- `init-mkdocs.py`(雛形のディレクトリを全部写す——新しいページも写る) +- 汎用の CSV の印(下の「仕様の穴」で足さないと決めた) +- `qa-csv.md` の「本文での番号」(URL の中は元々番号として書く場所ではない) + +### 同じことを述べている箇所の数 + +「フックが描くもの」: フックの docstring の表・`tree.md` の「ドキュメントのツール」の 2 箇所。「URL の中は変えない」: フックの docstring だけ。分けない——同じ決定の写し。 + +## 仕様の穴(不在の線で推奨により決めた) + +Issue の受入基準 B の「版の中の任意の CSV を描く汎用の印にするかを決める」。**線は `仕様の穴 = 推奨で決めて起票`**。確認の Issue を起票する。 + +- 選択肢: (a) `decision_table` の印だけ足す / (b) 汎用の印(``)も足す +- **決めた値: (a)** +- 理由: 決定表は spec-loop の決まりで `shared/` に置く標準の表で、どの正本リポジトリにもある。汎用の印は引数(パス)を取る新しい印の形になり、 + 版をまたぐ扱い(重ねない・どの版のパスか)と、`shared/` の外(`spec/`)の表を利用者のサイトに描いてよいかを決める必要がある—— + 実データ(#369)で描いた仕様の CSV は `spec/data/features.csv` などで、置き場所の決まりが spec-loop に無い。足すなら別の Issue で決める。 + +URL の守り方(受入基準 A の「`/` を見る小さな修正か、URL 全体を除く形か」)は**実現の手段**なので、開発者として URL 全体を除く形に決めた(`/` 以外の前置き `?id=`・`#` も守れる)。 + +## 乗るデプロイ経路 + +プラグイン(`plugins/spec-loop/`)の版を上げてマージ。マージで main へ push されるとサイトが再デプロイされる(`docs/plans/` を足す)。 + +## 周の在り処 + +- ブランチ: `issue/376-hook-url-decision-table` +- worktree: `/Users/hdknr/Projects/hdknr/claude-code-setup/.claude/worktrees/issue-376` +- 起点: `eab5e13` +- base: 参照 `origin/main`(`refs/remotes/origin/HEAD`)→ `eab5e1387a3fa7587aed615754fcb7bc35cd75e0` + +## verify の受入基準 + +Issue 本文は検査対象であって要件ではない。 + +不変条件: + +- **I1(#341)**: 素の本文の URL(`https://ex.com/Q-001`・`http://…/docs/Q-001.md`・`…?id=Q-001`・`…#Q-001`・`www.…`・日本語の直後の URL)の中の番号を書き換えない。 +- **I2(陽性対照)**: 地の文の番号(`(Q-001)`)は引き続きリンクになる。見出し・コード・リンクの中を変えない今までの決まりも変わらない。 +- **I3(#369)**: 印 `decision_table` が、各版の `docs/v/shared/decision_table.csv` を見出しの行どおりに版ごとの節に描く(セルはエスケープ・短い行は埋める・BOM を読む・空白だけの行は飛ばす)。無い版は飛ばし、1 つも無ければそう書く。読めない・見出しの無い表は警告。 +- **I4**: `init-mkdocs.py` が `docs/decision-table.md` を写す(空の根に雛形をすべて写す試料)。 +- **I5**: `tree.md` に、CSV へ直接リンクしない決まりと、写し済みのフックの取り込み(#339 の手順 4 に乗せる)がある。 + +経路: + +| 経路 | 守り方 | 確かめ方 | +| --- | --- | --- | +| `on_page_content` → `link_numbers` の文字の書き換え | 文字の中の URL を先に除いてから番号を置き換える | 試料: 素の URL 6 形と地の文の番号(`hook_links_bare_url`)。URL を見ない・URL の中も書き換える・URL の後ろを書き換えない・`www.` を見ない変異 | +| `on_page_markdown` → `RENDERERS` の `decision_table` | 描画関数 | 試料: 版ごと・見出し・エスケープ・短い行・長い行・BOM・空白だけの行・無い版・無い・読めない・見出しが無い | +| `init-mkdocs.py` の写し | 雛形のディレクトリを全部写す(変えない) | 既存の「空の根には雛形をすべて写す」が新しいページも見る | + +各経路の守りを外す変異で赤になる(陽性対照つき)。 + +生成物: 図の書き出し・`skill-metrics.py` の生成ブロックは該当しない——**生成物なし**。 + +未証明: **実際の MkDocs(Python-Markdown と Material)で描いた HTML** に当てていない——試料は描いた HTML に見立てた文字列(今までの #324 の試料と同じ形。CI に MkDocs と Material の組が無い)。人間のレビューで、正本リポジトリで `mkdocs build` して URL と決定表を目で見ることをお願いする。 + +## 未解決の判断 + +上の「仕様の穴」を確認の Issue #480 で利用者に渡す(台帳の `decisions` に記録)。 + +## 関門の進捗(再開点) + +- 直す線: `既定`(出所: cruise の確かめた線(台帳 `issue-474`)) +- 割り目(手順 4 の完了時): 不在のため訊かず続けた +- 実験: 試料(`test-init-mkdocs.py` 51 件・変異 100 件) +- Verifier 1 パス目: 未 +- `/code-review` 1 パス目: 未 + +## 起票 + +- この周の起票: 1 件(仕様の穴の確認の Issue。起票の承認は不在のため訊いていない) + +## 既知の限界・決着済みの論点 + +- 汎用の CSV の印は足さない(仕様の穴として推奨で決めた。確認の Issue)。 +- URL は `<スキーム>://` か `www.` で始まり、空白・`<`・`>`・引用符の手前までとみなす——**URL の直後に空白を置かずに続けた文字(`https://ex.com/aでQ-001を見る`)は URL の一部と読む**ので、その中の番号はリンクにならない +(壊すより残すほうに倒している。URL の終わりを全角の文字で切ると、日本語を含む URL を壊す)。 diff --git a/plugins/spec-loop/.claude-plugin/plugin.json b/plugins/spec-loop/.claude-plugin/plugin.json index cc8d7cf6..94343814 100644 --- a/plugins/spec-loop/.claude-plugin/plugin.json +++ b/plugins/spec-loop/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "spec-loop", "description": "要求側のループ(要求確定・仕様レビュー・実現性とコスト・提案書)と、dev-loop への引き渡しを担うプラグイン。現在のスキルは req-loop(Q&A 表への起票(推奨と理由つき)・回答の反映・レビュー会用 PPTX の生成で、利用者の回答待ちで止まる)と spec-review(開発者向けの仕様を書き下し、書き込みツールを持たない 3 つの判定役に別ティアでレビューさせ、利用者に見える結果が変わる指摘は共有の Q&A 表に差し戻し、費用・運用だけが変わる選択は内部の Q&A 表に記録し、範囲の外の場面と実現の手段の指摘は仕様に足さない。抜ける条件を満たさなければ抜けたと報告しない)と feasibility(規模から見積りとスケジュールを更新し、ガントチャートをスクリプトで生成し、費用付きの質問を Q&A 表に起票する。予算責任者の合意待ちで止まる)と proposal(同じ版の 2 つのサインオフのタグが同じ共有の範囲(docs/v/shared/ ほか)を指すかをスクリプトで照合し、一致し、かつ暫定の回答が残っていないときだけ提案書と仕様書(PPTX も選べる)を生成し、利用者にまだ確かめていない暫定の決定を合意と分けて載せる。v2 以降は前の版との比較を載せた変更の提案書になる。決裁者の承認待ちで止まる)と scaffold(確定した仕様を中間表現 spec_ir.json に書き起こし、OpenAPI・型・実装が無いと失敗する Red テストと dev-loop に渡す Issue の本文をスクリプトで生成して、1 機能 = Issue 1 件で登録する。v2 以降は前の版と比べて機能を追加・変更・削除に分け、変わった機能だけを Issue にする。Issue の登録で止まる)の 5 本", - "version": "0.26.0", + "version": "0.28.0", "author": { "name": "hdknr" } } diff --git a/plugins/spec-loop/references/tree.md b/plugins/spec-loop/references/tree.md index 0acdb851..cec76284 100644 --- a/plugins/spec-loop/references/tree.md +++ b/plugins/spec-loop/references/tree.md @@ -33,6 +33,7 @@ build/merged/ # 版を重ね合わせた全体(生成物。comm spec-review/v.csv # 仕様レビューの実行の記録(版ごと。commit する。docs/ の外——列と理由は spec-exit.md の「実行の上限と記録」) docs/index.md # 表紙(下の「ドキュメントのツール」。版の外) docs/qa.md # Q&A 表の表示用ページ(同上。正本は各版の shared/qa.csv) +docs/decision-table.md # 決定表の表示用ページ(同上。正本は各版の shared/decision_table.csv) mkdocs.yml # MkDocs の設定(同上) hooks/spec_loop_pages.py # 表示用ページを CSV から作るフック(同上) ``` @@ -155,11 +156,14 @@ python3 /scripts/init-mkdocs.py [<正本 在れば触らず、足りない依存・行を示すので、足すかどうかは利用者が決める。 - 写すもの: `mkdocs.yml`(Material・`use_directory_urls: false`・nav は書かず自動)・`pyproject.toml`(uv。 **MkDocs は 2 未満に固定**——2.0 は非互換の変更が予告されている)・`.gitignore`(`build/`・`site/`・`.venv/`・`__pycache__/`)・ - `hooks/spec_loop_pages.py`・`docs/index.md`・`docs/qa.md`。閲覧は `uv run mkdocs serve`。 + `hooks/spec_loop_pages.py`・`docs/index.md`・`docs/qa.md`・`docs/decision-table.md`。閲覧は `uv run mkdocs serve`。 - **CSV は Markdown に写さない。** 表示用のページに印(``)を書き、フックが CSV から描く。 `qa` の印は、各版の `docs/v/shared/qa.csv` を**版ごとの節**に描く(重ね合わせない。アンカーは `v-q-001`)。 + `decision_table` の印は、各版の `docs/v/shared/decision_table.csv` を**見出しの行どおり**に版ごとの節に描く(#369)。 CSV の文字は表のセルにだけ置く(段落に置くと、行頭の `!!! ` や字下げがブロックの記法に読まれる)。 ほかの CSV を描くときは、フックに描画関数を足す。 +- **要求・仕様から CSV へ直接リンクしない**——ブラウザでは CSV のページが開けない(#369)。リンクは表示用のページ + (`docs/qa.md`・`docs/decision-table.md`。版の節は `#v1` のようなアンカー)へ張る。 - **本文の Q&A の番号(`Q-008`)は、フックが Q&A 表(`docs/qa.md`)のアンカーへのリンクにする**(描画関数が描いた表のセルを含む。 Q&A 表のページそのものは除く)。リンクする版・置き換えないところはフックの docstring を正とする。 **本文の書き方の決まりは `qa-csv.md` の「本文での番号」を正とする。** @@ -168,6 +172,9 @@ python3 /scripts/init-mkdocs.py [<正本 **印つきの番号(`Q-S-008`。spec-loop 0.24.0 から)をリンクにするのは 0.24.0 以降のフックだけ**——それより前に写したフックは 印つきの番号を素通しし、リンクにも「表に無い番号」の警告にもしない(黙って素の文字のまま出る)。0.24.0 に上げたら、 印つきの番号を起票する前に、同じ手順 4 の形でフックを取り込む。 + **spec-loop 0.28.0 で、フックは素の本文の URL の中の番号を書き換えなくなり(#341)、`decision_table` の印を描くようになった(#369)**—— + 同じ手順 4 の形でフックを取り込み、雛形の `docs/decision-table.md` を写す(在れば変えない)。それまで CSV へ直接張っていたリンクは + `docs/decision-table.md` へ張り替える。 - **表示用のページは `shared/` に置かない**(`docs/qa.md` のように版の外に置く)。`shared/` は 2 つのサインオフが比べる範囲 (`signoff-tags.md`)なので、表示用のファイルを入れると比べる中身が増える。版の外の名前は重ね合わせにも入らない(`versions.md`)。 - **フックはサーバーの起動時にしか読まれない**——直したら `mkdocs serve` を起動し直す。 diff --git a/plugins/spec-loop/skills/feasibility/SKILL.md b/plugins/spec-loop/skills/feasibility/SKILL.md index 0b40556a..a799d0c3 100644 --- a/plugins/spec-loop/skills/feasibility/SKILL.md +++ b/plugins/spec-loop/skills/feasibility/SKILL.md @@ -5,8 +5,8 @@ description: "実現性・コストのループを 1 回分進める。要求・ # feasibility スキル - -> **このスキルの版: 0.26.0**(プラグイン `spec-loop`)。 + +> **このスキルの版: 0.28.0**(プラグイン `spec-loop`)。 > 手元で読まれている版がリポジトリの最新より古いなら、 > `/plugin marketplace update` → `/plugin update spec-loop@claude-code-setup` の順に実行し、 > Claude Code を再起動する([#63](https://github.com/hdknr/claude-code-setup/issues/63))。 diff --git a/plugins/spec-loop/skills/proposal/SKILL.md b/plugins/spec-loop/skills/proposal/SKILL.md index b44b6709..fee029c6 100644 --- a/plugins/spec-loop/skills/proposal/SKILL.md +++ b/plugins/spec-loop/skills/proposal/SKILL.md @@ -5,8 +5,8 @@ description: "提案書のループを 1 回分進める。利用者の要求確 # proposal スキル - -> **このスキルの版: 0.26.0**(プラグイン `spec-loop`)。 + +> **このスキルの版: 0.28.0**(プラグイン `spec-loop`)。 > 手元で読まれている版がリポジトリの最新より古いなら、 > `/plugin marketplace update` → `/plugin update spec-loop@claude-code-setup` の順に実行し、 > Claude Code を再起動する([#63](https://github.com/hdknr/claude-code-setup/issues/63))。 diff --git a/plugins/spec-loop/skills/req-loop/SKILL.md b/plugins/spec-loop/skills/req-loop/SKILL.md index 8aacf123..a85f2d0c 100644 --- a/plugins/spec-loop/skills/req-loop/SKILL.md +++ b/plugins/spec-loop/skills/req-loop/SKILL.md @@ -5,8 +5,8 @@ description: "要求確定のループを 1 周(= レビュー会 1 回分) # req-loop スキル - -> **このスキルの版: 0.26.0**(プラグイン `spec-loop`)。 + +> **このスキルの版: 0.28.0**(プラグイン `spec-loop`)。 > 手元で読まれている版がリポジトリの最新より古いなら、 > `/plugin marketplace update` → `/plugin update spec-loop@claude-code-setup` の順に実行し、 > Claude Code を再起動する([#63](https://github.com/hdknr/claude-code-setup/issues/63))。 diff --git a/plugins/spec-loop/skills/scaffold/SKILL.md b/plugins/spec-loop/skills/scaffold/SKILL.md index 32e29739..fb40d87c 100644 --- a/plugins/spec-loop/skills/scaffold/SKILL.md +++ b/plugins/spec-loop/skills/scaffold/SKILL.md @@ -5,8 +5,8 @@ description: "確定した仕様を dev-loop の入力(Issue)に変える引 # scaffold スキル - -> **このスキルの版: 0.26.0**(プラグイン `spec-loop`)。 + +> **このスキルの版: 0.28.0**(プラグイン `spec-loop`)。 > 手元で読まれている版がリポジトリの最新より古いなら、 > `/plugin marketplace update` → `/plugin update spec-loop@claude-code-setup` の順に実行し、 > Claude Code を再起動する([#63](https://github.com/hdknr/claude-code-setup/issues/63))。 diff --git a/plugins/spec-loop/skills/spec-review/SKILL.md b/plugins/spec-loop/skills/spec-review/SKILL.md index 4246fd1d..dd1d45da 100644 --- a/plugins/spec-loop/skills/spec-review/SKILL.md +++ b/plugins/spec-loop/skills/spec-review/SKILL.md @@ -5,8 +5,8 @@ description: "仕様レビューのループを 1 回分進める。確定した # spec-review スキル - -> **このスキルの版: 0.26.0**(プラグイン `spec-loop`)。 + +> **このスキルの版: 0.28.0**(プラグイン `spec-loop`)。 > 手元で読まれている版がリポジトリの最新より古いなら、 > `/plugin marketplace update` → `/plugin update spec-loop@claude-code-setup` の順に実行し、 > Claude Code を再起動する([#63](https://github.com/hdknr/claude-code-setup/issues/63))。 diff --git a/plugins/spec-loop/templates/mkdocs/docs/decision-table.md b/plugins/spec-loop/templates/mkdocs/docs/decision-table.md new file mode 100644 index 00000000..4b7c0fb0 --- /dev/null +++ b/plugins/spec-loop/templates/mkdocs/docs/decision-table.md @@ -0,0 +1,6 @@ +# 決定表 + +正本は各版の `shared/decision_table.csv` で、このページはそこから作る(このページに表を書き写さない)。 +要求・仕様から決定表を指すときは、CSV ではなくこのページへリンクする。 + + diff --git a/plugins/spec-loop/templates/mkdocs/hooks/spec_loop_pages.py b/plugins/spec-loop/templates/mkdocs/hooks/spec_loop_pages.py index 6d66c04c..598d4623 100644 --- a/plugins/spec-loop/templates/mkdocs/hooks/spec_loop_pages.py +++ b/plugins/spec-loop/templates/mkdocs/hooks/spec_loop_pages.py @@ -6,6 +6,7 @@ | 印 | 描くもの | | --- | --- | | `qa` | 各版の `docs/v/shared/qa.csv` を、版ごとの節に(件数・一覧・質問ごとの詳細。推奨の選択肢と理由も) | +| `decision_table` | 各版の `docs/v/shared/decision_table.csv` を、版ごとの節に(見出しの行どおりの表。#369) | **描くのは共有の層の表だけ**——内部の層の表(`docs/v/spec/qa-dev.csv`)は描かない(開発者が決めて記録するもの。 spec-loop の `references/qa-csv.md` の「層」)。 @@ -21,6 +22,8 @@ - セルの文字は HTML と Markdown の記号をエスケープし、改行は `
` にする(表を壊さない)。 **CSV の文字は表のセルにだけ置く**——段落や箇条書きに置くと、行頭の `!!! `・字下げ・`1. ` がブロックの記法に読まれる。 - 描くものを足すときは、描画関数を書いて `RENDERERS` に足す。 +- **決定表(`decision_table`)は列を決めない**——見出しの行をそのまま表の見出しにする(列の決まりは正本リポジトリが持つ)。 + 見出しの行が空・空白だけなら警告する。版は重ね合わせない(Q&A 表と同じ)。 **本文の Q&A の番号(`Q-008`)は、Q&A 表のページ(`qa.md`)のアンカーへのリンクにする**(正本は番号のまま。 決まりは spec-loop の `references/qa-csv.md` の「本文での番号」)。`on_page_content` が**描いた後の HTML** に掛けるので、 @@ -36,6 +39,9 @@ - **リンクにする版の Q&A 表に無い番号はリンクにせず警告する**(打ち間違いを拾う)。版の下のページなら、 その版より新しい版にだけある番号もここに入る。読めない表の番号は数えない(表の警告は Q&A 表のページで出る)。 - Q&A 表のページ(`qa` の印を持つページ)と、`qa.md` の無いサイトは変えない。 +- **素の本文に書いた URL(`https://ex.com/Q-001`・`…?id=Q-001`・`…#Q-001`)の中の番号は変えない**(#341)—— + `` の中は上の決まりで変えないが、素の URL は MkDocs が `` にしないので、文字の中から URL を先に除く。 + URL は `<スキーム>://` か `www.` で始まり、空白・`<`・`>`・引用符の手前までとみなす。 """ from __future__ import annotations @@ -61,6 +67,9 @@ QA_OPTION_SEPARATOR = " / " QA_PAGE = "qa.md" +DECISION_TABLE = "decision_table.csv" +# 素の本文の URL(#341)。番号のリンク化はこの中を変えない +URL = re.compile(r"(?:[A-Za-z][A-Za-z0-9+.-]*://|www\.)[^\s<>\"']+") # 起票元ごとの番号の印(`Q-S-001`。#278)。印の無い前の形の番号(`Q-001`)もリンクにする QA_MARKS = "RSFD" QA_NUMBER = re.compile(rf"(? str: return "\n".join(out) +def render_decision_table(docs_dir: Path) -> str: + """各版の `shared/decision_table.csv` を、見出しの行どおりの表にする(#369)。""" + out: list[str] = [] + for version, root in versions(docs_dir): + path = root / "shared" / DECISION_TABLE + if not path.is_file(): # 決定表の無い版は飛ばす + continue + try: + with path.open(newline="", encoding="utf-8-sig") as f: + rows = [row for row in csv.reader(f) if any(cell.strip() for cell in row)] + if not rows: # 空白だけの行は読み飛ばしたので、残った最初の行が見出し + raise ValueError("見出しの行が無い") + except (OSError, ValueError, csv.Error) as e: # UnicodeDecodeError は ValueError の子 + out += [problem(f"{path.relative_to(docs_dir)}: {e}"), ""] + continue + header, body = rows[0], rows[1:] + out += [f"## v{version}", "", "| " + " | ".join(text(cell) for cell in header) + " |", + "| " + " | ".join("---" for _ in header) + " |"] + out += ["| " + " | ".join(text(cell) for cell in (row + [""] * (len(header) - len(row)))[:len(header)]) + " |" + for row in body] + out.append("") + if not out: + return "決定表(`docs/v/shared/decision_table.csv`)はまだ無い。\n" + return "\n".join(out) + + RENDERERS = { "qa": render_qa, + "decision_table": render_decision_table, } @@ -201,17 +237,25 @@ def link(match: re.Match) -> str: return number return f'{number}' + def link_text(between: str) -> str: + """文字の中の番号をリンクにする。素の URL の中は変えない(#341)。""" + parts, start = [], 0 + for url in URL.finditer(between): + parts += [QA_NUMBER.sub(link, between[start:url.start()]), url.group(0)] + start = url.end() + return "".join(parts + [QA_NUMBER.sub(link, between[start:])]) + out, last = [], 0 inside = {name: 0 for name in UNLINKED} # 開いている要素の数(入れ子・閉じ忘れの多い HTML でも数で追う) for match in MARKUP.finditer(content): between = content[last:match.start()] - out += [between if any(inside.values()) else QA_NUMBER.sub(link, between), match.group(0)] + out += [between if any(inside.values()) else link_text(between), match.group(0)] last = match.end() name = (match.group(2) or "").lower() if name in inside and not match.group(0).endswith("/>"): inside[name] = max(inside[name] - 1, 0) if match.group(1) else inside[name] + 1 between = content[last:] - out.append(between if any(inside.values()) else QA_NUMBER.sub(link, between)) + out.append(between if any(inside.values()) else link_text(between)) for number in unknown: log.warning("spec-loop: %s: %s は %sにも無いので、リンクにしない(読めない表は数えない)", src_path, number, scope) diff --git a/scripts/test-init-mkdocs.py b/scripts/test-init-mkdocs.py index 5db1e163..340c4972 100644 --- a/scripts/test-init-mkdocs.py +++ b/scripts/test-init-mkdocs.py @@ -692,6 +692,49 @@ def merge_ignores_pages(plugin: Path, work: Path) -> bool: name.endswith(("qa.md", "index.md")) for name in merged) + +# 素の本文の URL の中の番号は変えない(#341)。`/`・`?id=`・`#` の前置き、日本語の直後の URL も。地の文の番号は変える(陽性対照) +BARE_URLS = ("https://ex.com/Q-001 と http://h/docs/Q-001.md と https://ex.com/?id=Q-001 と https://ex.com/#Q-001 と " + "www.ex.com/Q-002 と 参照https://ex.com/a/Q-002 と") + + +def hook_links_bare_url(plugin: Path, work: Path) -> bool: + docs = link_site(work) + out, warnings = link(plugin, docs, f"

{BARE_URLS}(Q-001)

\n", "index.md") + return not warnings and out == f"

{BARE_URLS}({a('Q-001', 'qa.html#v1-q-001')})

\n" + + +def hook_decision_table(plugin: Path, work: Path) -> bool: + docs = work / "docs" + write(docs, {"v1/shared/decision_table.csv": "\ufeff条件,結果\nA|x,y\n\n , \n", + "v2/requirements/a.md": "# a\n", + "v3/shared/decision_table.csv": "列1,列2,列3\n1,2\n4,5,6,7\n"}) + out, warnings = render(plugin, docs, "# 決定表\n\n\n", src="decision-table.md") + v1, v3 = out.index("## v1"), out.index("## v3") + return (not warnings and v1 < v3 and "## v2" not in out + and "| 条件 | 結果 |\n| --- | --- |\n| A\\|x | <b>y</b> |\n\n## v3" in out + and "| 列1 | 列2 | 列3 |\n| --- | --- | --- |\n| 1 | 2 | |\n| 4 | 5 | 6 |\n" in out[v3:] + and "") + return not warnings and "決定表(`docs/v/shared/decision_table.csv`)はまだ無い" in out + + +def hook_decision_table_unreadable(plugin: Path, work: Path) -> bool: + docs = work / "docs" + (docs / "v1" / "shared").mkdir(parents=True) + (docs / "v1" / "shared" / "decision_table.csv").write_bytes(b"\xff\xfe") + (docs / "v2" / "shared").mkdir(parents=True) + (docs / "v2" / "shared" / "decision_table.csv").write_text(" , \n", encoding="utf-8") + out, warnings = render(plugin, docs, "") + return (len(warnings) == 2 and "v1/shared/decision_table.csv" in warnings[0] + and "v2/shared/decision_table.csv: 見出しの行が無い" in warnings[1] and out.count("表示できない") == 2) + + CASES = { "空の根には雛形をすべて写す": init_empty, "mkdocs.yml が在れば何も写さない": init_config_exists, @@ -739,11 +782,36 @@ def merge_ignores_pages(plugin: Path, work: Path) -> bool: "Q&A 表のページは変えない": hook_links_not_qa_page, "描画した表のセルも置き換える": hook_links_rendered, "qa.md が無ければ変えない": hook_links_no_qa_page, + "素の URL の中の番号は変えない": hook_links_bare_url, + "決定表を版ごとに見出しの行どおりに描く": hook_decision_table, + "決定表が無ければそう書く": hook_decision_table_none, + "読めない・見出しの無い決定表は警告": hook_decision_table_unreadable, "重ね合わせに表示用のファイルが混ざらない": merge_ignores_pages, } # (名前, 壊す対象, 壊す前, 壊した後, 殺すはずの試料の名前)。2 箇所を壊すときは (壊す前, 壊した後) の列を渡す MUTATIONS: list[tuple] = [ + ("素の URL の中も書き換える", HOOK, " parts += [QA_NUMBER.sub(link, between[start:url.start()]), url.group(0)]", + " parts += [QA_NUMBER.sub(link, between[start:url.start()]), QA_NUMBER.sub(link, url.group(0))]", + "素の URL の中の番号は変えない"), + ("URL を見ない", HOOK, " for url in URL.finditer(between):", " for url in []:", "素の URL の中の番号は変えない"), + ("www. の URL を見ない", HOOK, 'URL = re.compile(r"(?:[A-Za-z][A-Za-z0-9+.-]*://|www\\.)', 'URL = re.compile(r"(?:[A-Za-z][A-Za-z0-9+.-]*://)', + "素の URL の中の番号は変えない"), + ("URL の後ろを書き換えない", HOOK, 'return "".join(parts + [QA_NUMBER.sub(link, between[start:])])', + 'return "".join(parts + [between[start:]])', "素の URL の中の番号は変えない"), + ("決定表を描かない", HOOK, ' "decision_table": render_decision_table,\n', "", "決定表を版ごとに見出しの行どおりに描く"), + ("決定表の見出しを描かない", HOOK, '"| " + " | ".join(text(cell) for cell in header) + " |",', + '"| " + " | ".join("" for cell in header) + " |",', "決定表を版ごとに見出しの行どおりに描く"), + ("決定表をエスケープしない", HOOK, '" | ".join(text(cell) for cell in (row + [""]', '" | ".join(cell for cell in (row + [""]', + "決定表を版ごとに見出しの行どおりに描く"), + ("決定表の短い行を埋めない", HOOK, "(row + [\"\"] * (len(header) - len(row)))[:len(header)]", "row", + "決定表を版ごとに見出しの行どおりに描く"), + ("決定表の BOM を読まない", HOOK, 'with path.open(newline="", encoding="utf-8-sig") as f:', + 'with path.open(newline="", encoding="utf-8") as f:', "決定表を版ごとに見出しの行どおりに描く"), + ("決定表の空白だけの行も描く", HOOK, "rows = [row for row in csv.reader(f) if any(cell.strip() for cell in row)]", + "rows = [row for row in csv.reader(f) if row]", "決定表を版ごとに見出しの行どおりに描く"), + ("見出しの無い決定表を黙る", HOOK, ' if not rows: # 空白だけの行は', ' if False: # 空白だけの行は', + "読めない・見出しの無い決定表は警告"), ("mkdocs.yml が在っても写す", INIT, " if os.path.lexists(root / name):\n", " if False:\n", "mkdocs.yml が在れば何も写さない"), ("mkdocs.yaml を設定とみなさない", INIT, 'CONFIG_NAMES = ("mkdocs.yml", "mkdocs.yaml")', 'CONFIG_NAMES = ("mkdocs.yml",)', @@ -979,7 +1047,8 @@ def check_reference() -> None: text = REFERENCE.read_text(encoding="utf-8") check("references/tree.md に「ドキュメントのツール」の節が無い", "\n## ドキュメントのツール\n" in text) section = text.split("\n## ドキュメントのツール\n", 1)[-1].split("\n## ", 1)[0] - for word in ("scripts/init-mkdocs.py", "mkdocs.yml", "docs/qa.md", "shared/", "`qa-csv.md` の「本文での番号」"): + for word in ("scripts/init-mkdocs.py", "mkdocs.yml", "docs/qa.md", "docs/decision-table.md", "shared/", + "`qa-csv.md` の「本文での番号」", "CSV へ直接リンクしない"): check(f"references/tree.md の「ドキュメントのツール」に {word} が無い", word in section) qa = (PLUGIN / "references" / "qa-csv.md").read_text(encoding="utf-8") check("references/qa-csv.md に「本文での番号」の節が無い", "\n## 本文での番号\n" in qa) From 7b446138dc17206109ec2c5d7a34cf35fa7d978b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A5=88=E8=89=AF=20=E8=8B=B1=E6=A8=B9?= Date: Tue, 27 Oct 2026 20:29:02 +0900 Subject: [PATCH 2/7] =?UTF-8?q?issue-376:=20=E5=86=8D=E9=96=8B=E7=82=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- docs/plans/issue-376.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/plans/issue-376.md b/docs/plans/issue-376.md index cdfd3056..70ef5268 100644 --- a/docs/plans/issue-376.md +++ b/docs/plans/issue-376.md @@ -88,8 +88,9 @@ Issue 本文は検査対象であって要件ではない。 - 直す線: `既定`(出所: cruise の確かめた線(台帳 `issue-474`)) - 割り目(手順 4 の完了時): 不在のため訊かず続けた - 実験: 試料(`test-init-mkdocs.py` 51 件・変異 100 件) -- Verifier 1 パス目: 未 -- `/code-review` 1 パス目: 未 +- 1 パス目の起点: `6ede0e5`(この後のコミットは計画ファイルだけ) +- Verifier 1 パス目: 起動(sonnet) +- `/code-review` 1 パス目: 起動(high) ## 起票 From 9184f8d4f6e2d62cb03c77376b274d6a7d6fdf2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A5=88=E8=89=AF=20=E8=8B=B1=E6=A8=B9?= Date: Tue, 27 Oct 2026 20:34:13 +0900 Subject: [PATCH 3/7] =?UTF-8?q?=E9=9B=9B=E5=BD=A2=E3=81=AE=E3=83=95?= =?UTF-8?q?=E3=83=83=E3=82=AF:=20=E6=B1=BA=E5=AE=9A=E8=A1=A8=E3=81=AE?= =?UTF-8?q?=E9=95=B7=E3=81=84=E8=A1=8C=E3=82=92=E5=88=87=E3=82=89=E3=81=AA?= =?UTF-8?q?=E3=81=84=E3=83=BB=E8=A6=8B=E5=87=BA=E3=81=97=E3=81=AE=E7=A9=BA?= =?UTF-8?q?=E3=82=92=E8=AD=A6=E5=91=8A=E3=83=BBURL=20=E3=82=92=E5=85=A8?= =?UTF-8?q?=E8=A7=92=E3=81=AE=E5=8F=A5=E8=AA=AD=E7=82=B9=E3=81=A8=E5=AE=9F?= =?UTF-8?q?=E4=BD=93=E5=8F=82=E7=85=A7=E3=81=A7=E7=B5=82=E3=81=88=E3=82=8B?= =?UTF-8?q?=EF=BC=88#376=20=E3=81=AE=201=20=E3=83=91=E3=82=B9=E7=9B=AE?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- docs/plans/issue-376.md | 27 ++++++++- .../templates/mkdocs/hooks/spec_loop_pages.py | 22 +++++--- scripts/test-init-mkdocs.py | 56 +++++++++++++++---- 3 files changed, 84 insertions(+), 21 deletions(-) diff --git a/docs/plans/issue-376.md b/docs/plans/issue-376.md index 70ef5268..7a219c9a 100644 --- a/docs/plans/issue-376.md +++ b/docs/plans/issue-376.md @@ -89,8 +89,10 @@ Issue 本文は検査対象であって要件ではない。 - 割り目(手順 4 の完了時): 不在のため訊かず続けた - 実験: 試料(`test-init-mkdocs.py` 51 件・変異 100 件) - 1 パス目の起点: `6ede0e5`(この後のコミットは計画ファイルだけ) -- Verifier 1 パス目: 起動(sonnet) -- `/code-review` 1 パス目: 起動(high) +- Verifier 1 パス目: 通した——反証 2 件(V1・V2) +- `/code-review` 1 パス目: 通した——指摘 8 件(CR1〜CR8) +- 1 パス目の指摘対応: V1・CR2・CR5・V2・CR4・CR1・CR3 を直した。差分は 0 行でない → 2 パス目を回す +- 割り目(2 パス目の前): 不在のため訊かず続けた ## 起票 @@ -99,5 +101,26 @@ Issue 本文は検査対象であって要件ではない。 ## 既知の限界・決着済みの論点 - 汎用の CSV の印は足さない(仕様の穴として推奨で決めた。確認の Issue)。 +- URL の終わり: 1 パス目の指摘で、空白・`<`・`>`・引用符に加えて、描いた HTML の実体参照(`<` など)と全角の括弧・句読点で終えるようにした。 + **半角の記号(`.`・`)`)は URL の中にも現れるので区切りにしない**——`https://ex.com/a.Q-001`・`(https://ex.com/a)Q-001` の番号はリンクにならない。 + +### 1 パス目の指摘の採否 + +Verifier(sonnet): 反証 2 件。`/code-review`(high): 8 件。どちらも `git status --porcelain` は前後とも空。MkDocs は手元に無く、実際の `mkdocs build` には当てていない(未証明のまま)。 + +| # | 指摘 | 実物で失敗を示せるか | 破る受入基準 | 採否 | +| --- | --- | --- | --- | --- | +| V1・CR2 | 決定表の見出しより長い行の余りを黙って切る(`build-proposal.py` の仕様書は埋める) | 示せる(Verifier・`/code-review` の直接実行: `1,2,3` → `| 1 | 2 |`) | I3(表を描く——セルが消える) | 直す(見出しを空で埋めて全部の列を描く) | +| CR5 | URL の後ろの全角の句読点・閉じ括弧(`(https://…)。Q-001`)で、次の番号がリンクにならない(直す前はなっていた) | 示せる(`/code-review` の試料) | I2 | 直す(全角の括弧・句読点で URL を終える) | +| V2 | URL の直前に空白なしの番号(`Q-001https://…`)がスキームに飲まれる | 示せる(Verifier の直接実行) | I2 | 直す(スキームを英字だけに) | +| CR4 | 描いた HTML の `<`・`>`・`"` で URL が終わらない | 示せる(`/code-review` の試料) | I2 | 直す(実体参照で URL を終える) | +| CR1 | 見出しの行が空だと、次の行を見出しに繰り上げて黙る(docstring は警告すると言う) | 示せる(`/code-review` の試料) | I3(見出しの無い表は警告) | 直す(最初の行を見出しとし、空なら警告) | +| CR3 | 「空の根に雛形をすべて写す」は 6 件以上しか見ないので、`decision-table.md` が無くても通る | 示せる(試料の条件 `len(expected) >= 6`) | I4 | 直す(ページの在る・印・写ることを見る試料と変異) | +| CR6 | 決定表の読み方が `read_qa`・`build-proposal.py` の写しで、突き合わせが無い | 示せない(ずれは V1 で直した分だけ) | 無し | 残課題 | +| CR7 | `mailto:Q-001@ex.com` のように `//` の無いスキームの中の番号を書き換える | 示せる(`/code-review` の試料) | 無し(I1 の列挙の外) | 残課題 | +| CR8 | `qa.csv` を定数にしていない(`DECISION_TABLE` だけ定数) | 示せない(保守の手間) | 無し | 残課題 | + +1 パス目の対応で URL の正規表現を書き直したとき、変異 3 件が生き残った——否定の文字クラスと先読みが重なり、片方を外しても振る舞いが変わらなかった(等価変異)。 +先読みだけで終える形(`\S+?(?=…)`)に縮め、振る舞いに効かない条件(終わりの半角の句読点を外す)は落とした。 - URL は `<スキーム>://` か `www.` で始まり、空白・`<`・`>`・引用符の手前までとみなす——**URL の直後に空白を置かずに続けた文字(`https://ex.com/aでQ-001を見る`)は URL の一部と読む**ので、その中の番号はリンクにならない (壊すより残すほうに倒している。URL の終わりを全角の文字で切ると、日本語を含む URL を壊す)。 diff --git a/plugins/spec-loop/templates/mkdocs/hooks/spec_loop_pages.py b/plugins/spec-loop/templates/mkdocs/hooks/spec_loop_pages.py index 598d4623..c377b2f4 100644 --- a/plugins/spec-loop/templates/mkdocs/hooks/spec_loop_pages.py +++ b/plugins/spec-loop/templates/mkdocs/hooks/spec_loop_pages.py @@ -23,7 +23,8 @@ **CSV の文字は表のセルにだけ置く**——段落や箇条書きに置くと、行頭の `!!! `・字下げ・`1. ` がブロックの記法に読まれる。 - 描くものを足すときは、描画関数を書いて `RENDERERS` に足す。 - **決定表(`decision_table`)は列を決めない**——見出しの行をそのまま表の見出しにする(列の決まりは正本リポジトリが持つ)。 - 見出しの行が空・空白だけなら警告する。版は重ね合わせない(Q&A 表と同じ)。 + **最初の行が見出し**で、空・空白だけなら警告する(次の行を見出しに繰り上げない)。見出しより長い行があれば、 + 見出しの右を空で埋めて**全部の列を描く**(黙って切らない。`build-proposal.py` の仕様書と同じ)。版は重ね合わせない(Q&A 表と同じ)。 **本文の Q&A の番号(`Q-008`)は、Q&A 表のページ(`qa.md`)のアンカーへのリンクにする**(正本は番号のまま。 決まりは spec-loop の `references/qa-csv.md` の「本文での番号」)。`on_page_content` が**描いた後の HTML** に掛けるので、 @@ -41,7 +42,9 @@ - Q&A 表のページ(`qa` の印を持つページ)と、`qa.md` の無いサイトは変えない。 - **素の本文に書いた URL(`https://ex.com/Q-001`・`…?id=Q-001`・`…#Q-001`)の中の番号は変えない**(#341)—— `` の中は上の決まりで変えないが、素の URL は MkDocs が `` にしないので、文字の中から URL を先に除く。 - URL は `<スキーム>://` か `www.` で始まり、空白・`<`・`>`・引用符の手前までとみなす。 + URL は `<英字のスキーム>://` か `www.` で始まり、空白・`<`・`>`・引用符(描いた HTML の `<`・`>`・`"`・`'` も)・ + 全角の括弧と句読点(`()「」『』。、`)の手前までとみなす(文の区切りの後ろの番号をリンクにするため)。 + 半角の記号(`.`・`)` など)は URL の中にも現れるので区切りにしない——`https://ex.com/a.Q-001` の番号はリンクにならない。 """ from __future__ import annotations @@ -69,7 +72,7 @@ QA_PAGE = "qa.md" DECISION_TABLE = "decision_table.csv" # 素の本文の URL(#341)。番号のリンク化はこの中を変えない -URL = re.compile(r"(?:[A-Za-z][A-Za-z0-9+.-]*://|www\.)[^\s<>\"']+") +URL = re.compile(r"(?:[A-Za-z]+://|www\.)\S+?(?=[\s<>\"'()「」『』。、]|&(?:lt|gt|quot|#39|#x27);|$)") # 起票元ごとの番号の印(`Q-S-001`。#278)。印の無い前の形の番号(`Q-001`)もリンクにする QA_MARKS = "RSFD" QA_NUMBER = re.compile(rf"(? str: continue try: with path.open(newline="", encoding="utf-8-sig") as f: - rows = [row for row in csv.reader(f) if any(cell.strip() for cell in row)] - if not rows: # 空白だけの行は読み飛ばしたので、残った最初の行が見出し - raise ValueError("見出しの行が無い") + reader = csv.reader(f) + header = next(reader, []) + body = [row for row in reader if "".join(row).strip()] + if not any(cell.strip() for cell in header): # 最初の行が見出し——空なら次の行を繰り上げずに警告 + raise ValueError("見出しの行が無い(最初の行が空)") except (OSError, ValueError, csv.Error) as e: # UnicodeDecodeError は ValueError の子 out += [problem(f"{path.relative_to(docs_dir)}: {e}"), ""] continue - header, body = rows[0], rows[1:] + width = max([len(header)] + [len(row) for row in body]) # 見出しより長い行も切らない + header = header + [""] * (width - len(header)) out += [f"## v{version}", "", "| " + " | ".join(text(cell) for cell in header) + " |", "| " + " | ".join("---" for _ in header) + " |"] - out += ["| " + " | ".join(text(cell) for cell in (row + [""] * (len(header) - len(row)))[:len(header)]) + " |" + out += ["| " + " | ".join(text(cell) for cell in row + [""] * (width - len(row))) + " |" for row in body] out.append("") if not out: diff --git a/scripts/test-init-mkdocs.py b/scripts/test-init-mkdocs.py index 340c4972..fbd9f81b 100644 --- a/scripts/test-init-mkdocs.py +++ b/scripts/test-init-mkdocs.py @@ -698,10 +698,17 @@ def merge_ignores_pages(plugin: Path, work: Path) -> bool: "www.ex.com/Q-002 と 参照https://ex.com/a/Q-002 と") +# URL の後ろの全角の句読点・閉じ括弧・描いた HTML の実体参照で URL を終える。URL の直前の番号も URL に入れない(#376 の 1 パス目) +AFTER_URLS = ("(https://ex.com/a)。{q}で 、https://ex.com/b、{q} <https://ex.com/c>{q} https://ex.com/d"{q} " + "{q}https://ex.com/e https://ex.com/f. {q}") + + def hook_links_bare_url(plugin: Path, work: Path) -> bool: docs = link_site(work) out, warnings = link(plugin, docs, f"

{BARE_URLS}(Q-001)

\n", "index.md") - return not warnings and out == f"

{BARE_URLS}({a('Q-001', 'qa.html#v1-q-001')})

\n" + after, warned = link(plugin, docs, f"

{AFTER_URLS.format(q='Q-001')}

\n", "index.md") + return (not warnings and out == f"

{BARE_URLS}({a('Q-001', 'qa.html#v1-q-001')})

\n" + and not warned and after == f"

{AFTER_URLS.format(q=a('Q-001', 'qa.html#v1-q-001'))}

\n") def hook_decision_table(plugin: Path, work: Path) -> bool: @@ -713,7 +720,7 @@ def hook_decision_table(plugin: Path, work: Path) -> bool: v1, v3 = out.index("## v1"), out.index("## v3") return (not warnings and v1 < v3 and "## v2" not in out and "| 条件 | 結果 |\n| --- | --- |\n| A\\|x | <b>y</b> |\n\n## v3" in out - and "| 列1 | 列2 | 列3 |\n| --- | --- | --- |\n| 1 | 2 | |\n| 4 | 5 | 6 |\n" in out[v3:] + and "| 列1 | 列2 | 列3 | |\n| --- | --- | --- | --- |\n| 1 | 2 | | |\n| 4 | 5 | 6 | 7 |\n" in out[v3:] and "") - return (len(warnings) == 2 and "v1/shared/decision_table.csv" in warnings[0] - and "v2/shared/decision_table.csv: 見出しの行が無い" in warnings[1] and out.count("表示できない") == 2) + return (len(warnings) == 3 and "v1/shared/decision_table.csv" in warnings[0] + and "v2/shared/decision_table.csv: 見出しの行が無い" in warnings[1] + and "v3/shared/decision_table.csv: 見出しの行が無い" in warnings[2] + and out.count("表示できない") == 3 and "| 条件 | 結果 |" not in out) + + +# 雛形の決定表のページ(I4)——在って、印を持ち、空の根に写る(#376 の 1 パス目) +def init_decision_table_page(plugin: Path, work: Path) -> bool: + page = plugin / "templates" / "mkdocs" / "docs" / "decision-table.md" + if not page.is_file() or "" not in page.read_text(encoding="utf-8"): + return False + root = work / "root" + root.mkdir(parents=True) + code, _ = run_init(plugin, root) + copied = root / "docs" / "decision-table.md" + return code == 0 and copied.is_file() and copied.read_bytes() == page.read_bytes() CASES = { @@ -786,6 +809,7 @@ def hook_decision_table_unreadable(plugin: Path, work: Path) -> bool: "決定表を版ごとに見出しの行どおりに描く": hook_decision_table, "決定表が無ければそう書く": hook_decision_table_none, "読めない・見出しの無い決定表は警告": hook_decision_table_unreadable, + "雛形の決定表のページは印を持ち、空の根に写る": init_decision_table_page, "重ね合わせに表示用のファイルが混ざらない": merge_ignores_pages, } @@ -795,23 +819,33 @@ def hook_decision_table_unreadable(plugin: Path, work: Path) -> bool: " parts += [QA_NUMBER.sub(link, between[start:url.start()]), QA_NUMBER.sub(link, url.group(0))]", "素の URL の中の番号は変えない"), ("URL を見ない", HOOK, " for url in URL.finditer(between):", " for url in []:", "素の URL の中の番号は変えない"), - ("www. の URL を見ない", HOOK, 'URL = re.compile(r"(?:[A-Za-z][A-Za-z0-9+.-]*://|www\\.)', 'URL = re.compile(r"(?:[A-Za-z][A-Za-z0-9+.-]*://)', + ("www. の URL を見ない", HOOK, 'URL = re.compile(r"(?:[A-Za-z]+://|www\\.)', 'URL = re.compile(r"(?:[A-Za-z]+://)', "素の URL の中の番号は変えない"), ("URL の後ろを書き換えない", HOOK, 'return "".join(parts + [QA_NUMBER.sub(link, between[start:])])', 'return "".join(parts + [between[start:]])', "素の URL の中の番号は変えない"), ("決定表を描かない", HOOK, ' "decision_table": render_decision_table,\n', "", "決定表を版ごとに見出しの行どおりに描く"), ("決定表の見出しを描かない", HOOK, '"| " + " | ".join(text(cell) for cell in header) + " |",', '"| " + " | ".join("" for cell in header) + " |",', "決定表を版ごとに見出しの行どおりに描く"), - ("決定表をエスケープしない", HOOK, '" | ".join(text(cell) for cell in (row + [""]', '" | ".join(cell for cell in (row + [""]', + ("決定表をエスケープしない", HOOK, '" | ".join(text(cell) for cell in row + [""]', '" | ".join(cell for cell in row + [""]', + "決定表を版ごとに見出しの行どおりに描く"), + ("決定表の短い行を埋めない", HOOK, "for cell in row + [\"\"] * (width - len(row))) + \" |\"", "for cell in row) + \" |\"", "決定表を版ごとに見出しの行どおりに描く"), - ("決定表の短い行を埋めない", HOOK, "(row + [\"\"] * (len(header) - len(row)))[:len(header)]", "row", + ("決定表の長い行を切る", HOOK, " width = max([len(header)] + [len(row) for row in body])", " width = len(header)", "決定表を版ごとに見出しの行どおりに描く"), ("決定表の BOM を読まない", HOOK, 'with path.open(newline="", encoding="utf-8-sig") as f:', 'with path.open(newline="", encoding="utf-8") as f:', "決定表を版ごとに見出しの行どおりに描く"), - ("決定表の空白だけの行も描く", HOOK, "rows = [row for row in csv.reader(f) if any(cell.strip() for cell in row)]", - "rows = [row for row in csv.reader(f) if row]", "決定表を版ごとに見出しの行どおりに描く"), - ("見出しの無い決定表を黙る", HOOK, ' if not rows: # 空白だけの行は', ' if False: # 空白だけの行は', + ("決定表の空白だけの行も描く", HOOK, 'body = [row for row in reader if "".join(row).strip()]', + "body = [row for row in reader if row]", "決定表を版ごとに見出しの行どおりに描く"), + ("見出しの無い決定表を黙る", HOOK, " if not any(cell.strip() for cell in header): # 最初の行が見出し", + " if not header: # 最初の行が見出し", "読めない・見出しの無い決定表は警告"), + ("空の見出しの次の行を繰り上げる", HOOK, " header = next(reader, [])\n", + " header = next((row for row in reader if any(cell.strip() for cell in row)), [])\n", "読めない・見出しの無い決定表は警告"), + ("URL を全角の句読点で終えない", HOOK, "(?=[\\s<>\\\"'()「」『』。、]|", "(?=[\\s<>\\\"']|", "素の URL の中の番号は変えない"), + ("URL を実体参照で終えない", HOOK, "|&(?:lt|gt|quot|#39|#x27);|$)", "|$)", "素の URL の中の番号は変えない"), + ("スキームに数字と記号を許す", HOOK, '(?:[A-Za-z]+://|www', '(?:[A-Za-z][A-Za-z0-9+.-]*://|www', "素の URL の中の番号は変えない"), + ("雛形の決定表のページに印が無い", "templates/mkdocs/docs/decision-table.md", "", "", + "雛形の決定表のページは印を持ち、空の根に写る"), ("mkdocs.yml が在っても写す", INIT, " if os.path.lexists(root / name):\n", " if False:\n", "mkdocs.yml が在れば何も写さない"), ("mkdocs.yaml を設定とみなさない", INIT, 'CONFIG_NAMES = ("mkdocs.yml", "mkdocs.yaml")', 'CONFIG_NAMES = ("mkdocs.yml",)', From dcac64cc4438292955a157f069c8bb5885c38fbe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A5=88=E8=89=AF=20=E8=8B=B1=E6=A8=B9?= Date: Tue, 27 Oct 2026 20:38:16 +0900 Subject: [PATCH 4/7] =?UTF-8?q?issue-376:=202=20=E3=83=91=E3=82=B9?= =?UTF-8?q?=E7=9B=AE=E3=81=AE=E5=86=8D=E9=96=8B=E7=82=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- docs/plans/issue-376.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/plans/issue-376.md b/docs/plans/issue-376.md index 7a219c9a..bbc624e7 100644 --- a/docs/plans/issue-376.md +++ b/docs/plans/issue-376.md @@ -93,6 +93,9 @@ Issue 本文は検査対象であって要件ではない。 - `/code-review` 1 パス目: 通した——指摘 8 件(CR1〜CR8) - 1 パス目の指摘対応: V1・CR2・CR5・V2・CR4・CR1・CR3 を直した。差分は 0 行でない → 2 パス目を回す - 割り目(2 パス目の前): 不在のため訊かず続けた +- 2 パス目の起点: `9184f8d`(この後のコミットは計画ファイルだけ) +- Verifier 2 パス目: 起動(sonnet) +- `/code-review` 2 パス目: 起動(high) ## 起票 From 3e252e5f086277145433ad715216251ebc9ef1a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A5=88=E8=89=AF=20=E8=8B=B1=E6=A8=B9?= Date: Tue, 27 Oct 2026 20:45:27 +0900 Subject: [PATCH 5/7] =?UTF-8?q?=E9=9B=9B=E5=BD=A2=E3=81=AE=E3=83=95?= =?UTF-8?q?=E3=83=83=E3=82=AF:=20URL=20=E3=81=AE=E5=85=A8=E8=A7=92?= =?UTF-8?q?=E3=81=AE=E4=B8=B8=E6=8B=AC=E5=BC=A7=E3=82=92=E5=AF=BE=E3=81=A7?= =?UTF-8?q?=E6=95=B0=E3=81=88=E3=83=BB=E5=85=A8=E8=A7=92=E3=81=AE=E5=8F=A5?= =?UTF-8?q?=E8=AA=AD=E7=82=B9=E3=81=A7=E7=B5=82=E3=81=88=E3=83=BB=E6=AE=8B?= =?UTF-8?q?=E3=82=8A=E3=81=AE=E6=AC=A1=E3=81=AE=20URL=20=E3=82=82=E8=A6=8B?= =?UTF-8?q?=E3=82=8B=E3=80=81=E8=A9=A6=E6=96=99=E3=82=92=E8=B6=B3=E3=81=99?= =?UTF-8?q?=EF=BC=88#376=20=E3=81=AE=202=20=E3=83=91=E3=82=B9=E7=9B=AE?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- docs/plans/issue-376.md | 28 +++++++++++++++-- .../templates/mkdocs/hooks/spec_loop_pages.py | 31 ++++++++++++++++--- scripts/test-init-mkdocs.py | 29 ++++++++++++----- 3 files changed, 73 insertions(+), 15 deletions(-) diff --git a/docs/plans/issue-376.md b/docs/plans/issue-376.md index bbc624e7..c8d3c2ff 100644 --- a/docs/plans/issue-376.md +++ b/docs/plans/issue-376.md @@ -77,7 +77,10 @@ Issue 本文は検査対象であって要件ではない。 生成物: 図の書き出し・`skill-metrics.py` の生成ブロックは該当しない——**生成物なし**。 -未証明: **実際の MkDocs(Python-Markdown と Material)で描いた HTML** に当てていない——試料は描いた HTML に見立てた文字列(今までの #324 の試料と同じ形。CI に MkDocs と Material の組が無い)。人間のレビューで、正本リポジトリで `mkdocs build` して URL と決定表を目で見ることをお願いする。 +未証明: ~~実際の MkDocs に当てていない~~——**2 パス目の指摘(P4)で、main の作業ツリーの `.venv` に mkdocs 1.6.1・Markdown 3.10.2・Material があると分かり、 +当てた**(`scratchpad/mkdocs376.py`。雛形を一時ディレクトリに写し、素の URL・末尾の URL・全角の丸括弧を含む URL・`(URL)。番号`・引用符・箇条書きの末尾・決定表の長い行を描いた。 +URL の中の番号は変わらず、地の文の番号はリンクになり、決定表は全部の列が出た)。**CI では当てていない**(CI に MkDocs と Material の組が無い)。 +**Markdown の強調が URL を割る形(`https://ex.com/__init__/Q-001`)は直していない**(P3。残課題)。 ## 未解決の判断 @@ -94,8 +97,10 @@ Issue 本文は検査対象であって要件ではない。 - 1 パス目の指摘対応: V1・CR2・CR5・V2・CR4・CR1・CR3 を直した。差分は 0 行でない → 2 パス目を回す - 割り目(2 パス目の前): 不在のため訊かず続けた - 2 パス目の起点: `9184f8d`(この後のコミットは計画ファイルだけ) -- Verifier 2 パス目: 起動(sonnet) -- `/code-review` 2 パス目: 起動(high) +- Verifier 2 パス目: 通した——反証 0 件 +- `/code-review` 2 パス目: 通した——指摘 10 件(P1〜P10) +- 2 パス目の指摘対応: P1・P2・P4(計画の文言と実際の MkDocs での確認)・P5・P6・P7 を直した。終点は下の狭いパスに渡すコミット +- 狭いパス: 起動(sonnet) ## 起票 @@ -125,5 +130,22 @@ Verifier(sonnet): 反証 2 件。`/code-review`(high): 8 件。どちら 1 パス目の対応で URL の正規表現を書き直したとき、変異 3 件が生き残った——否定の文字クラスと先読みが重なり、片方を外しても振る舞いが変わらなかった(等価変異)。 先読みだけで終える形(`\S+?(?=…)`)に縮め、振る舞いに効かない条件(終わりの半角の句読点を外す)は落とした。 + +### 2 パス目の指摘の採否 + +Verifier(sonnet): 反証 0 件。`/code-review`(high): 10 件。どちらも `git status --porcelain` は前後とも空。 + +| # | 指摘 | 実物で失敗を示せるか | 破る受入基準 | 採否 | +| --- | --- | --- | --- | --- | +| P1 | 文字の区間の終わり(`

` の直前)の URL の試料が無く、`|$` を外す変異が生き残る | 示せる(`/code-review` の変異と再現) | I1 | 直す(試料と変異) | +| P2 | 全角の丸括弧で URL を切るので、パスに `(…)` を含む URL が割れて番号がリンクになる | 示せる(直接実行で再現) | I1 | 直す(全角の丸括弧は対で数える) | +| P3 | Markdown の強調が URL を割る(`https://ex.com/__init__/Q-001`)と、後ろの区間の番号がリンクになる | 示せる(実際の Python-Markdown で再現) | 無し(I1 の列挙の外。直すには区間をまたいで URL を見る設計の変更で、2 パス目では関門を通らない) | 残課題 | +| P4 | 未証明の理由「手元に MkDocs が無い」は誤り(main の `.venv` にある) | 示せる(`.venv` で import と描画) | — (計画ファイル) | 直す(実際の MkDocs で当て、未証明を測った範囲まで狭めた) | +| P5 | 生の `"`・`'`・`'`・かぎ括弧で URL を終えることを試料が見ない(外す変異が生き残る) | 示せる(`/code-review` の変異) | I2 | 直す(試料と変異) | +| P6 | `tree.md` の 0.28.0 の取り込みの段落を消しても照合が通る | 示せる(`check_reference` の語) | I5 | 直す(照合の語を足した) | +| P7 | 全角の `,.:;!?【】〈〉《》` で URL が終わらず、後ろの番号がリンクにならない(直す前はなっていた) | 示せる(`/code-review` の再現) | I2 | 直す(全角の句読点・かぎ括弧の集合を広げた) | +| P8 | `path.is_file()` が `try` の外——3.12 では権限の誤りで例外、3.14 では黙って飛ばす | 示せない(走らせていない) | 無し | 残課題(`qa.csv` も同じ形) | +| P9 | `[A-Za-z]+://` に左の錨が無く、長い英字の並びで 2 乗の時間 | 示せる(`/code-review` の計測: 16,000 字で 0.37 秒) | 無し(性能) | 残課題 | +| P10 | 読めない・見出しの無い決定表は警告だけで `## v` の見出しを出さない(前の版の節に入り、`#v` が無くなる) | 示せない(走らせていない) | 無し | 残課題 | - URL は `<スキーム>://` か `www.` で始まり、空白・`<`・`>`・引用符の手前までとみなす——**URL の直後に空白を置かずに続けた文字(`https://ex.com/aでQ-001を見る`)は URL の一部と読む**ので、その中の番号はリンクにならない (壊すより残すほうに倒している。URL の終わりを全角の文字で切ると、日本語を含む URL を壊す)。 diff --git a/plugins/spec-loop/templates/mkdocs/hooks/spec_loop_pages.py b/plugins/spec-loop/templates/mkdocs/hooks/spec_loop_pages.py index c377b2f4..6c37b19b 100644 --- a/plugins/spec-loop/templates/mkdocs/hooks/spec_loop_pages.py +++ b/plugins/spec-loop/templates/mkdocs/hooks/spec_loop_pages.py @@ -43,7 +43,9 @@ - **素の本文に書いた URL(`https://ex.com/Q-001`・`…?id=Q-001`・`…#Q-001`)の中の番号は変えない**(#341)—— `
` の中は上の決まりで変えないが、素の URL は MkDocs が `` にしないので、文字の中から URL を先に除く。 URL は `<英字のスキーム>://` か `www.` で始まり、空白・`<`・`>`・引用符(描いた HTML の `<`・`>`・`"`・`'` も)・ - 全角の括弧と句読点(`()「」『』。、`)の手前までとみなす(文の区切りの後ろの番号をリンクにするため)。 + 全角の句読点・かぎ括弧(`。、,.:;!?「」『』【】〈〉《》`)の手前までとみなす(文の区切りの後ろの番号をリンクにするため)。 + **全角の丸括弧は対で数える**——URL の中で開いた `(` に対応する `)` は URL の中、対の無い `)` で終える + (`https://ja.wikipedia.org/wiki/仕様(改訂)/Q-001` を割らず、`(https://ex.com/a)。Q-001` の番号はリンクにする)。 半角の記号(`.`・`)` など)は URL の中にも現れるので区切りにしない——`https://ex.com/a.Q-001` の番号はリンクにならない。 """ @@ -72,7 +74,24 @@ QA_PAGE = "qa.md" DECISION_TABLE = "decision_table.csv" # 素の本文の URL(#341)。番号のリンク化はこの中を変えない -URL = re.compile(r"(?:[A-Za-z]+://|www\.)\S+?(?=[\s<>\"'()「」『』。、]|&(?:lt|gt|quot|#39|#x27);|$)") +URL = re.compile(r"(?:[A-Za-z]+://|www\.)\S+?(?=[\s<>\"']|&(?:lt|gt|quot|#39|#x27);|$)") +# URL を終える全角の文字(丸括弧は対で数えるので url_end が見る) +URL_STOP = set("。、,.:;!?「」『』【】〈〉《》") + + +def url_end(candidate: str) -> int: + """URL の候補のうち、URL とみなす長さ。全角の句読点・かぎ括弧と、対の無い全角の閉じ丸括弧の手前で終える。""" + depth = 0 + for index, char in enumerate(candidate): + if char in URL_STOP: + return index + if char == "(": + depth += 1 + elif char == ")": + if depth == 0: + return index + depth -= 1 + return len(candidate) # 起票元ごとの番号の印(`Q-S-001`。#278)。印の無い前の形の番号(`Q-001`)もリンクにする QA_MARKS = "RSFD" QA_NUMBER = re.compile(rf"(? str: def link_text(between: str) -> str: """文字の中の番号をリンクにする。素の URL の中は変えない(#341)。""" parts, start = [], 0 - for url in URL.finditer(between): - parts += [QA_NUMBER.sub(link, between[start:url.start()]), url.group(0)] - start = url.end() + # 全角の文字で終えた URL の残りから探し直す(残りの中の次の URL も見る) + while (url := URL.search(between, start)) is not None: + end = url.start() + url_end(url.group(0)) + parts += [QA_NUMBER.sub(link, between[start:url.start()]), between[url.start():end]] + start = end return "".join(parts + [QA_NUMBER.sub(link, between[start:])]) out, last = [], 0 diff --git a/scripts/test-init-mkdocs.py b/scripts/test-init-mkdocs.py index fbd9f81b..53818fd8 100644 --- a/scripts/test-init-mkdocs.py +++ b/scripts/test-init-mkdocs.py @@ -700,15 +700,21 @@ def merge_ignores_pages(plugin: Path, work: Path) -> bool: # URL の後ろの全角の句読点・閉じ括弧・描いた HTML の実体参照で URL を終える。URL の直前の番号も URL に入れない(#376 の 1 パス目) AFTER_URLS = ("(https://ex.com/a)。{q}で 、https://ex.com/b、{q} <https://ex.com/c>{q} https://ex.com/d"{q} " - "{q}https://ex.com/e https://ex.com/f. {q}") + "{q}https://ex.com/e https://ex.com/f. {q} https://ex.com/g\"{q} https://ex.com/h'{q} https://ex.com/i'{q} " + "「https://ex.com/j」{q} https://ex.com/k,{q} https://ex.com/l:{q} 【https://ex.com/m】{q} " + "(https://ex.com/n)、https://ex.com/o/Q-002 と (https://ex.com/r){q}") +# URL の中の全角の丸括弧(対)と、文字の区間の終わりの URL(#376 の 2 パス目) +INSIDE_URLS = ("https://ja.wikipedia.org/wiki/仕様(改訂)/Q-001 と https://ex.com/p/Q-001") def hook_links_bare_url(plugin: Path, work: Path) -> bool: docs = link_site(work) out, warnings = link(plugin, docs, f"

{BARE_URLS}(Q-001)

\n", "index.md") after, warned = link(plugin, docs, f"

{AFTER_URLS.format(q='Q-001')}

\n", "index.md") + inside, warned2 = link(plugin, docs, f"

{INSIDE_URLS}

\n
  • https://ex.com/q/Q-001
  • \n", "index.md") return (not warnings and out == f"

    {BARE_URLS}({a('Q-001', 'qa.html#v1-q-001')})

    \n" - and not warned and after == f"

    {AFTER_URLS.format(q=a('Q-001', 'qa.html#v1-q-001'))}

    \n") + and not warned and after == f"

    {AFTER_URLS.format(q=a('Q-001', 'qa.html#v1-q-001'))}

    \n" + and not warned2 and inside == f"

    {INSIDE_URLS}

    \n
  • https://ex.com/q/Q-001
  • \n") def hook_decision_table(plugin: Path, work: Path) -> bool: @@ -815,10 +821,11 @@ def init_decision_table_page(plugin: Path, work: Path) -> bool: # (名前, 壊す対象, 壊す前, 壊した後, 殺すはずの試料の名前)。2 箇所を壊すときは (壊す前, 壊した後) の列を渡す MUTATIONS: list[tuple] = [ - ("素の URL の中も書き換える", HOOK, " parts += [QA_NUMBER.sub(link, between[start:url.start()]), url.group(0)]", - " parts += [QA_NUMBER.sub(link, between[start:url.start()]), QA_NUMBER.sub(link, url.group(0))]", + ("素の URL の中も書き換える", HOOK, " parts += [QA_NUMBER.sub(link, between[start:url.start()]), between[url.start():end]]", + " parts += [QA_NUMBER.sub(link, between[start:url.start()]), QA_NUMBER.sub(link, between[url.start():end])]", + "素の URL の中の番号は変えない"), + ("URL を見ない", HOOK, " while (url := URL.search(between, start)) is not None:", " while (url := None) is not None:", "素の URL の中の番号は変えない"), - ("URL を見ない", HOOK, " for url in URL.finditer(between):", " for url in []:", "素の URL の中の番号は変えない"), ("www. の URL を見ない", HOOK, 'URL = re.compile(r"(?:[A-Za-z]+://|www\\.)', 'URL = re.compile(r"(?:[A-Za-z]+://)', "素の URL の中の番号は変えない"), ("URL の後ろを書き換えない", HOOK, 'return "".join(parts + [QA_NUMBER.sub(link, between[start:])])', @@ -841,7 +848,14 @@ def init_decision_table_page(plugin: Path, work: Path) -> bool: ("空の見出しの次の行を繰り上げる", HOOK, " header = next(reader, [])\n", " header = next((row for row in reader if any(cell.strip() for cell in row)), [])\n", "読めない・見出しの無い決定表は警告"), - ("URL を全角の句読点で終えない", HOOK, "(?=[\\s<>\\\"'()「」『』。、]|", "(?=[\\s<>\\\"']|", "素の URL の中の番号は変えない"), + ("URL を全角の句読点で終えない", HOOK, 'URL_STOP = set("。、,.:;!?「」『』【】〈〉《》")', 'URL_STOP = set()', + "素の URL の中の番号は変えない"), + ("URL の全角の丸括弧を対で数えない", HOOK, " depth += 1\n", " pass\n", "素の URL の中の番号は変えない"), + ("URL を対の無い閉じ丸括弧で終えない", HOOK, " if depth == 0:\n return index\n", " if depth == 0:\n continue\n", + "素の URL の中の番号は変えない"), + ("URL を引用符で終えない", HOOK, "(?=[\\s<>\\\"']|", "(?=[\\s<>]|", "素の URL の中の番号は変えない"), + ("URL を文字の区間の終わりで終えない", HOOK, "#x27);|$)", "#x27);)", "素の URL の中の番号は変えない"), + ("URL の残りの次の URL を見ない", HOOK, " start = end\n", " start = url.end()\n", "素の URL の中の番号は変えない"), ("URL を実体参照で終えない", HOOK, "|&(?:lt|gt|quot|#39|#x27);|$)", "|$)", "素の URL の中の番号は変えない"), ("スキームに数字と記号を許す", HOOK, '(?:[A-Za-z]+://|www', '(?:[A-Za-z][A-Za-z0-9+.-]*://|www', "素の URL の中の番号は変えない"), ("雛形の決定表のページに印が無い", "templates/mkdocs/docs/decision-table.md", "", "", @@ -1082,7 +1096,8 @@ def check_reference() -> None: check("references/tree.md に「ドキュメントのツール」の節が無い", "\n## ドキュメントのツール\n" in text) section = text.split("\n## ドキュメントのツール\n", 1)[-1].split("\n## ", 1)[0] for word in ("scripts/init-mkdocs.py", "mkdocs.yml", "docs/qa.md", "docs/decision-table.md", "shared/", - "`qa-csv.md` の「本文での番号」", "CSV へ直接リンクしない"): + "`qa-csv.md` の「本文での番号」", "CSV へ直接リンクしない", + "spec-loop 0.28.0 で、フックは素の本文の URL の中の番号を書き換えなくなり"): check(f"references/tree.md の「ドキュメントのツール」に {word} が無い", word in section) qa = (PLUGIN / "references" / "qa-csv.md").read_text(encoding="utf-8") check("references/qa-csv.md に「本文での番号」の節が無い", "\n## 本文での番号\n" in qa) From b8cd04ffb84bd58685bf5bd29a7d310adfe6d7d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A5=88=E8=89=AF=20=E8=8B=B1=E6=A8=B9?= Date: Tue, 27 Oct 2026 20:49:56 +0900 Subject: [PATCH 6/7] =?UTF-8?q?issue-376:=20=E7=8B=AD=E3=81=84=E3=83=91?= =?UTF-8?q?=E3=82=B9=E3=81=AE=E7=B5=90=E6=9E=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- docs/plans/issue-376.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/plans/issue-376.md b/docs/plans/issue-376.md index c8d3c2ff..c1ec4f13 100644 --- a/docs/plans/issue-376.md +++ b/docs/plans/issue-376.md @@ -99,8 +99,9 @@ URL の中の番号は変わらず、地の文の番号はリンクになり、 - 2 パス目の起点: `9184f8d`(この後のコミットは計画ファイルだけ) - Verifier 2 パス目: 通した——反証 0 件 - `/code-review` 2 パス目: 通した——指摘 10 件(P1〜P10) -- 2 パス目の指摘対応: P1・P2・P4(計画の文言と実際の MkDocs での確認)・P5・P6・P7 を直した。終点は下の狭いパスに渡すコミット -- 狭いパス: 起動(sonnet) +- 2 パス目の指摘対応: P1・P2・P4(計画の文言と実際の MkDocs での確認)・P5・P6・P7 を直した。起点 `9184f8d` → 終点 `3e252e5` +- 狭いパス: 通した(sonnet)——指摘 3 件(人間が決める): (1) 入れ子の全角の丸括弧(`a(b)c)Q-001`)の試料が無く `depth -= 1` を外す変異が生き残る見込み (2) 全角の `[]{}<>` で URL を終えない (3) 実体参照 `"`・`'` で URL を終えない。あわせて「対の無い全角の `)` で URL を終える」は I1 と緊張する(仕様どおり) +- PR: 作る(マージは止まる。#476) ## 起票 From 66180645864920541064679045149797420a973a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A5=88=E8=89=AF=20=E8=8B=B1=E6=A8=B9?= Date: Wed, 28 Oct 2026 18:00:02 +0900 Subject: [PATCH 7/7] =?UTF-8?q?docs/plans/issue-376.md:=20=E5=86=8D?= =?UTF-8?q?=E9=96=8B=E7=82=B9=E3=81=AB=20#500=20=E3=81=AE=E5=8F=96?= =?UTF-8?q?=E3=82=8A=E8=BE=BC=E3=81=BF=E3=82=92=E8=A8=98=E3=81=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- docs/plans/issue-376.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/plans/issue-376.md b/docs/plans/issue-376.md index 604d34cc..65680fe7 100644 --- a/docs/plans/issue-376.md +++ b/docs/plans/issue-376.md @@ -106,6 +106,7 @@ URL の中の番号は変わらず、地の文の番号はリンクになり、 - 続けて main が #485(spec-loop 0.26.2)で進んだので再度 merge。衝突は同じく版の行(7 ファイル)だけで、0.28.0 のまま(0.26.2 より上) - さらに main が #483(spec-loop 0.29.0)で進んだので再度 merge。衝突は版の行(7 ファイル)だけ。版を 0.31.0 に上げ直し、`tree.md` の「spec-loop 0.28.0 で」とその照合(`test-init-mkdocs.py`)も 0.31.0 に直した - さらに main が #479(spec-loop 0.30.0)で進んだので再度 merge。衝突は版の行(7 ファイル)だけで、0.31.0 のまま。`tree.md` とその照合の「0.31.0 で」もそのまま +- push の直前に main が #500(dev-loop のみ)で進んだので再度 merge。衝突なし ## 起票