Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
{
"name": "spec-loop",
"source": "./plugins/spec-loop",
"version": "0.30.0",
"version": "0.31.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<n>/shared/ ほか)を指すかをスクリプトで照合し、一致し、かつ暫定の回答が残っていないときだけ提案書と仕様書(PPTX も選べる)を生成し、利用者にまだ確かめていない暫定の決定を合意と分けて載せる。v2 以降は前の版との比較を載せた変更の提案書になる。決裁者の承認待ちで止まる)と scaffold(確定した仕様を中間表現 spec_ir.json に書き起こし、OpenAPI・型・実装が無いと失敗する Red テストと dev-loop に渡す Issue の本文をスクリプトで生成して、1 機能 = Issue 1 件で登録する。v2 以降は前の版と比べて機能を追加・変更・削除に分け、変わった機能だけを Issue にする。Issue の登録で止まる)の 5 本"
}
Expand Down
157 changes: 157 additions & 0 deletions docs/plans/issue-376.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion plugins/spec-loop/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -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<n>/shared/ ほか)を指すかをスクリプトで照合し、一致し、かつ暫定の回答が残っていないときだけ提案書と仕様書(PPTX も選べる)を生成し、利用者にまだ確かめていない暫定の決定を合意と分けて載せる。v2 以降は前の版との比較を載せた変更の提案書になる。決裁者の承認待ちで止まる)と scaffold(確定した仕様を中間表現 spec_ir.json に書き起こし、OpenAPI・型・実装が無いと失敗する Red テストと dev-loop に渡す Issue の本文をスクリプトで生成して、1 機能 = Issue 1 件で登録する。v2 以降は前の版と比べて機能を追加・変更・削除に分け、変わった機能だけを Issue にする。Issue の登録で止まる)の 5 本",
"version": "0.30.0",
"version": "0.31.0",
"author": { "name": "hdknr" }
}
9 changes: 8 additions & 1 deletion plugins/spec-loop/references/tree.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ build/merged/ # 版を重ね合わせた全体(生成物。comm
spec-review/v<n>.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 から作るフック(同上)
```
Expand Down Expand Up @@ -156,11 +157,14 @@ python3 <spec-loop の base ディレクトリ>/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 に写さない。** 表示用のページに印(`<!-- spec-loop: qa -->`)を書き、フックが CSV から描く。
`qa` の印は、各版の `docs/v<n>/shared/qa.csv` を**版ごとの節**に描く(重ね合わせない。アンカーは `v<n>-q-001`)。
`decision_table` の印は、各版の `docs/v<n>/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` の「本文での番号」を正とする。**
Expand All @@ -169,6 +173,9 @@ python3 <spec-loop の base ディレクトリ>/scripts/init-mkdocs.py [<正本
**印つきの番号(`Q-S-008`。spec-loop 0.24.0 から)をリンクにするのは 0.24.0 以降のフックだけ**——それより前に写したフックは
印つきの番号を素通しし、リンクにも「表に無い番号」の警告にもしない(黙って素の文字のまま出る)。0.24.0 に上げたら、
印つきの番号を起票する前に、同じ手順 4 の形でフックを取り込む。
**spec-loop 0.31.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` を起動し直す。
Expand Down
4 changes: 2 additions & 2 deletions plugins/spec-loop/skills/feasibility/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ description: "実現性・コストのループを 1 回分進める。要求・

# feasibility スキル

<!-- skill-version: 0.30.0 -->
> **このスキルの版: 0.30.0**(プラグイン `spec-loop`)。
<!-- skill-version: 0.31.0 -->
> **このスキルの版: 0.31.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))。
Expand Down
4 changes: 2 additions & 2 deletions plugins/spec-loop/skills/proposal/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ description: "提案書のループを 1 回分進める。利用者の要求確

# proposal スキル

<!-- skill-version: 0.30.0 -->
> **このスキルの版: 0.30.0**(プラグイン `spec-loop`)。
<!-- skill-version: 0.31.0 -->
> **このスキルの版: 0.31.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))。
Expand Down
4 changes: 2 additions & 2 deletions plugins/spec-loop/skills/req-loop/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ description: "要求確定のループを 1 周(= レビュー会 1 回分)

# req-loop スキル

<!-- skill-version: 0.30.0 -->
> **このスキルの版: 0.30.0**(プラグイン `spec-loop`)。
<!-- skill-version: 0.31.0 -->
> **このスキルの版: 0.31.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))。
Expand Down
4 changes: 2 additions & 2 deletions plugins/spec-loop/skills/scaffold/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ description: "確定した仕様を dev-loop の入力(Issue)に変える引

# scaffold スキル

<!-- skill-version: 0.30.0 -->
> **このスキルの版: 0.30.0**(プラグイン `spec-loop`)。
<!-- skill-version: 0.31.0 -->
> **このスキルの版: 0.31.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))。
Expand Down
4 changes: 2 additions & 2 deletions plugins/spec-loop/skills/spec-review/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ description: "仕様レビューのループを 1 回分進める。確定した

# spec-review スキル

<!-- skill-version: 0.30.0 -->
> **このスキルの版: 0.30.0**(プラグイン `spec-loop`)。
<!-- skill-version: 0.31.0 -->
> **このスキルの版: 0.31.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))。
Expand Down
6 changes: 6 additions & 0 deletions plugins/spec-loop/templates/mkdocs/docs/decision-table.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# 決定表

正本は各版の `shared/decision_table.csv` で、このページはそこから作る(このページに表を書き写さない)。
要求・仕様から決定表を指すときは、CSV ではなくこのページへリンクする。

<!-- spec-loop: decision_table -->
75 changes: 73 additions & 2 deletions plugins/spec-loop/templates/mkdocs/hooks/spec_loop_pages.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
| 印 | 描くもの |
| --- | --- |
| `qa` | 各版の `docs/v<n>/shared/qa.csv` を、版ごとの節に(件数・一覧・質問ごとの詳細。推奨の選択肢と理由も) |
| `decision_table` | 各版の `docs/v<n>/shared/decision_table.csv` を、版ごとの節に(見出しの行どおりの表。#369) |

**描くのは共有の層の表だけ**——内部の層の表(`docs/v<n>/spec/qa-dev.csv`)は描かない(開発者が決めて記録するもの。
spec-loop の `references/qa-csv.md` の「層」)。
Expand All @@ -21,6 +22,9 @@
- セルの文字は HTML と Markdown の記号をエスケープし、改行は `<br>` にする(表を壊さない)。
**CSV の文字は表のセルにだけ置く**——段落や箇条書きに置くと、行頭の `!!! `・字下げ・`1. ` がブロックの記法に読まれる。
- 描くものを足すときは、描画関数を書いて `RENDERERS` に足す。
- **決定表(`decision_table`)は列を決めない**——見出しの行をそのまま表の見出しにする(列の決まりは正本リポジトリが持つ)。
**最初の行が見出し**で、空・空白だけなら警告する(次の行を見出しに繰り上げない)。見出しより長い行があれば、
見出しの右を空で埋めて**全部の列を描く**(黙って切らない。`build-proposal.py` の仕様書と同じ)。版は重ね合わせない(Q&A 表と同じ)。

**本文の Q&A の番号(`Q-008`)は、Q&A 表のページ(`qa.md`)のアンカーへのリンクにする**(正本は番号のまま。
決まりは spec-loop の `references/qa-csv.md` の「本文での番号」)。`on_page_content` が**描いた後の HTML** に掛けるので、
Expand All @@ -36,6 +40,13 @@
- **リンクにする版の Q&A 表に無い番号はリンクにせず警告する**(打ち間違いを拾う)。版の下のページなら、
その版より新しい版にだけある番号もここに入る。読めない表の番号は数えない(表の警告は Q&A 表のページで出る)。
- Q&A 表のページ(`qa` の印を持つページ)と、`qa.md` の無いサイトは変えない。
- **素の本文に書いた URL(`https://ex.com/Q-001`・`…?id=Q-001`・`…#Q-001`)の中の番号は変えない**(#341)——
`<a>` の中は上の決まりで変えないが、素の URL は MkDocs が `<a>` にしないので、文字の中から URL を先に除く。
URL は `<英字のスキーム>://` か `www.` で始まり、空白・`<`・`>`・引用符(描いた HTML の `&lt;`・`&gt;`・`&quot;`・`&#x27;` も)・
全角の句読点・かぎ括弧(`。、,.:;!?「」『』【】〈〉《》`)の手前までとみなす(文の区切りの後ろの番号をリンクにするため)。
**全角の丸括弧は対で数える**——URL の中で開いた `(` に対応する `)` は URL の中、対の無い `)` で終える
(`https://ja.wikipedia.org/wiki/仕様(改訂)/Q-001` を割らず、`(https://ex.com/a)。Q-001` の番号はリンクにする)。
半角の記号(`.`・`)` など)は URL の中にも現れるので区切りにしない——`https://ex.com/a.Q-001` の番号はリンクにならない。
"""

from __future__ import annotations
Expand All @@ -61,6 +72,26 @@
QA_OPTION_SEPARATOR = " / "

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 を終える全角の文字(丸括弧は対で数えるので 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"(?<![A-Za-z0-9_-])Q-(?:[{QA_MARKS}]-)?[0-9]{{3,}}(?![A-Za-z0-9_-])")
Expand Down Expand Up @@ -165,8 +196,38 @@ def render_qa(docs_dir: Path) -> 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:
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
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 + [""] * (width - len(row))) + " |"
for row in body]
out.append("")
if not out:
return "決定表(`docs/v<n>/shared/decision_table.csv`)はまだ無い。\n"
return "\n".join(out)


RENDERERS = {
"qa": render_qa,
"decision_table": render_decision_table,
}


Expand Down Expand Up @@ -201,17 +262,27 @@ def link(match: re.Match) -> str:
return number
return f'<a href="{html.escape(target + "#" + anchor(version, number))}">{number}</a>'

def link_text(between: str) -> str:
"""文字の中の番号をリンクにする。素の URL の中は変えない(#341)。"""
parts, start = [], 0
# 全角の文字で終えた 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
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)
Expand Down
Loading
Loading