仕様書を版ごとの差分で持つ
「Hora Kit の設計」シリーズの 12 本目。人が書いた設計書は、更新されずに腐るか、最新の状態に上書きされて過去が消えるかのどちらかになりがちです。仕様書を版ごとの差分で持ち、「1.0.0 のとき何を作ると決めたのか」が 1.1.0 を作った後でも読める仕組みを書きます。
1. はじめに
この記事は、弊社(Open Reach Tech)が OSS にした AI 開発フレームワーク『Hora Kit』の設計を解説するシリーズの 12 本目です。メイン記事はこちらです。
本当の意味での自動開発を実現するためのAI開発フレームワーク『Hora Kit』をOSSにしました
用語の確認。 Hora Kit で打つコマンドは
/horaの 1 つだけです。/horaが状況に応じて/hora-spec(仕様書を書く)、/hora-setup(実装リポジトリを作る)、/hora-plan(版を確定し、機能一覧と契約を書く)、/hora-build(1 機能を 18 の関所に通す)、/hora-accept(検収する)を順に呼びます。/hora-hotfixだけは/horaが呼ばず、人が直接打ちます。docs はこのうち/hora-specを「決める側」、残りを「作る側」と呼んでいて、この記事でもその呼び方を使います。
メイン記事で「保守で一番助かっているのは、バージョンが仕様書ごと残り続けること」と書きました。この記事ではその仕組みを書きます。
設計書が最新の状態に上書きされ続けて、いつの時点の話なのか分からなくなった経験がある方は多いと思います。そういう方に向けた話です。
2. 人が書いた設計書は、更新されずに腐る
先に、この設計が解いている問題を書きます。
設計書は、書かれた時点では正しいです。その後、機能が足され、変更され、削られます。設計書は更新されるかもしれないし、されないかもしれません。半年後に設計書を開いたとき、そこに書いてあることが「今の製品」なのか「最初の製品」なのか「途中のどこか」なのか、誰にも分かりません。
保守で一番困るのはこれです。「1.0.0 のとき何を作ると決めたのか」が、1.1.0 を作った後には読めなくなります。
3. 2 版目からは差分だけを書く
Hora Kit の仕様書は、リリースの版ごとに置かれます。

specs/
1.0.0/spec.md 最初の版。全文
1.1.0/spec.md 1.0.0 に対する差分だけ
1.2.0/spec.md 1.1.0 に対する差分だけ
2 つ目の版から、spec.md は 1 つ前の版に対する差分です。対話から出てくるのは、その版自身の文書情報と新しい機能だけで、それ以外は書かれません。
# <project name> design document
## 1. Document information ← 常に書き直す。製品のバージョンが変わったから
## 13. CSV export ← そして、この版が変える節だけ
過去の版の spec.md に手が入ることはありません。1.0.0 の文書は、1.0.0 を作ったときの決定の記録として、そのまま残ります。
/hora-plan が読むのは「解決された文書」で、1.0.0 の全文に 1.1.0 の差分を重ねたものです。人が読むときも同じで、その版の完全な仕様が欲しければ解決し、その版で何が変わったかが欲しければ差分を読みます。
4. 資料も版ごとにコピーする
annex/ や sources/ に置いた資料も、版ごとに自分のコピーを持ちます。
理由を docs はこう書いています。1 つのファイルを両方の版から参照すると、1.1.0 のためにそれを編集した瞬間、1.0.0 が何に対して書かれたのかが黙って変わってしまう、と。ディスク容量より、記録の正しさを取っているわけです。1.1.0 でも必要な ER 図は、specs/1.1.0/annex/ に自分のコピーを持ちます。
5. 引き継ぎは推測ではなく確認
差分方式には 1 つ落とし穴があります。「前の版から変わっていない」と「誰も見ていない」が、文面上は区別できないことです。
だから /hora-spec は、この版が触らない節を持つステージで、直前の版の答えを提示して確認を取り、引き継ぎを書き添えて通過します。
<!-- carried: 1.0.0's numbers, confirmed unchanged -->
推測ではなく確認です。引き継ぎは、走らなかったステージと見分けのつかない唯一の通過なので、確認した記録を残さないと区別がつかなくなります。
ステージ 6(セキュリティ)と 7(全体レビュー)だけは、その版が足すものについて決して引き継ぎません。新しい操作の呼び出し可能者は、それを導入した版で必ず述べます。ステージ 7 は差分ではなく解決された文書を読みます。1.0.0 と矛盾する新しい操作は、差分では見えず、解決すると見えるからです。
6. 版番号は、リリース済みかどうかで決まる
新しい版を切る境界は、変更の大きさではありません。その版がリリース済みかどうかです。判定材料は hora リポジトリのタグで、release.yml が main へのマージ時に作ります。
git fetch --tags && git tag -l '1.0.0' # 空 = 未リリース
| 扱い | |
|---|---|
| 未リリース | 追加も変更も削除も受け付け、版番号は変わらない。利用者がいないので、契約を変えても誰も壊れない |
| リリース済み | 手を触れない。次の版で行う |
リリース直前の仕様変更は、まったく正常な扱いです。手戻りであって、互換性の破壊ではないからです。2 版目からの番号は、感覚ではなく .hora/contracts/ の差分に対して判定します。フィールドや型が追加だけなら minor、何も追加されていなければ patch です。
7. git のブランチも版に対応する

main には直接コミットしません。作業は release/<version> の上で行い、その版の spec.md が作業対象です。各機能の実装は feature/<feature-id> に載り、ゲート境界で release/<version> にマージされます。版全体の検収掃引が通ると、release/<version> が main にマージされ、タグが打たれます。
release/<version> は rebase されません。一度マージされたものが巻き戻されたり書き換えられたりすることはありません。例外は 1 つだけで、hotfix/* が main に入ったときです。/hora は毎回の起動時と release/<version> への各マージ直後に origin/main を確認し、動いていれば追従します。追従は使い捨ての temp ブランチ上で組み立て、最後に 1 回だけ release/<version> を動かします。
8. 保守で弊社がどう使っているか
メイン記事に書いた通り、弊社では 100 万行近いシステムをこの方式で保守しています。
「この機能は、いつ、なぜ、こういう仕様になったのか」を追うとき、開くのは specs/<その版>/spec.md です。その版で変わった節だけが書かれているので、差分を読めば「その版で何を決めたか」が分かります。前の版の文書は変わっていないので、「その前はどうだったか」も同じ方法で追えます。設計書が 1 つしかなく、それが最新の状態に上書きされ続けるやり方では、これができません。
設計書を「最新の状態」として持つのではなく、「決定の履歴」として持つ。これが、弊社が保守でこの方式を重宝している理由です。
9. まとめ
仕様書は版ごとに置き、2 版目からは差分だけを書く。過去の版には手を入れず、資料も版ごとにコピーする。「変わっていない」は推測せず、確認して印を付ける。版を切る境界は、変更の大きさではなくリリース済みかどうか。
設計書の鮮度に悩んでいる方の参考になれば幸いです。
原典はこちらです。 https://github.com/openreachtech/hora-core/blob/main/docs/quick-start.ja.mdhttps://github.com/openreachtech/hora-core/blob/main/kit/skills/hora/references/spec-format.mdhttps://github.com/openreachtech/hora-core/blob/main/kit/skills/hora/references/commits.md