若くない何かの悩み

何かをdisっているときは、たいていツンデレですので大目に見てやってください。

マスタースケジュールの理論とコインチェックにおける実践

この記事はコインチェック株式会社(以下、コインチェック)のアドベントカレンダー4日目の記事です。


こんにちは。Kuniwak です。 最近は Software Engineer in Test の殻を破り(?)、PMO 的な役割でプロジェクトのプロセスを設計し、マスタースケジュールを計算しています。

みなさんはプロジェクトのスケジュールをどのように決めていますか? この記事ではスケジュール策定の背後にある理論とマスタースケジュールの作成方法を説明します。 また紹介するスケジュールの理論に基づいたツールである pfd-tools を紹介します。

TL;DR

  • PFD と要素表を書けばマスタースケジュールを機械的に計算できる(pfd-tools の pfdplan & planmaster コマンド)
  • スケジュールの理論を使うと改善すべき箇所がピンポイントでわかる(pfd-tools の criticalpath コマンド)
  • 現実のプロジェクトに適用でき、実際にマスタースケジュール策定に使える

説明の準備

マスタースケジュールとは

本記事ではマスタースケジュールをプロジェクトの定めた各工程の開始・完了時刻を定めたものとして定義しています(下図は適当に作った例です)。

マスタースケジュールの例

なおマスタースケジュールに正式に合意された定義はありません。さまざまな定義が存在します(ここでは他の定義は取り上げません)。

プロセスとは

プロセスとは成果物を入力し、成果物を出力する活動です1

プロジェクトの全体を小さなプロセスに分解し、それらを大局的な工程にマッピングするとマスタースケジュールを計算できます。 たとえば、機能Aを構成する機能A1と機能A2がありこれらが完成しないと機能Aが完成しないとします。 A1とA2のいずれも設計、実装、システムテストをされるとすると、機能Aの大局的な工程は設計(A1設計、A2設計)、実装(A1実装、A2実装)、システムテスト(A1システムテスト、A2システムテスト)のように局所的なプロセスにマッピングできます:

局所的なプロセス 大局的な工程
要求分析 要求分析
仕様記述 仕様記述
機能A1設計 設計
機能A2設計 設計
機能A1実装 実装
機能A2実装 実装
機能A1システムテスト システムテスト
機能A2システムテスト システムテスト
システム統合テスト システム統合テスト
ユーザー受け入れテスト ユーザー受け入れテスト
リリース リリース

大局的な工程の開始時刻はマッピングされたプロセスのうち最早のものの開始時刻で、工程の終了時刻は最早の次工程の開始時刻とマッピングされたプロセスのうち最遅のものの終了時刻の早い方です。

Process Flow Diagram(PFD) とは

Process Flow Diagram(PFD)とはプロセスを成果物変換の連なりであらわすプロセスの表現形式の1つです。清水吉男氏により提案されました。

PFD では成果物を矩形2、プロセスを楕円形であらわし、プロセスへの成果物の入出力を矢線であらわします。

PFD の例

この矢線によって PFD 上でプロセスの依存関係が明示されます。 たとえばあるプロセスを1つ選び矢線を逆方向にたどると、選んだプロセスを開始するためにあらかじめ完了しているべきプロセスがわかります3

矢線には、手戻りを意味する特別な矢線であるフィードバック線があります4。 フィードバック線の先のプロセスは、矢線の元の成果物の更新により手戻りして再実行しなければならないことを意味します。 代表的なフィードバック線はレビューコメントやバグチケットから実装への矢線です。

フィードバックのあるPFDの例

厳密にいうと、成果物からプロセスへ向かう実線の矢線は、矢線の元の成果物が完成していなければ矢線の先のプロセスを実行できないという意味を持ちます。 これに対し、破線の矢線であるフィードバック線は矢線の元の成果物が完成していなくとも矢線の先のプロセスを実行できるという違いがあります。 バグチケットがなくても実装は開始できますが、バグチケットが出たら修正(実装の再実行)が必要になるということです。

PFD に書ききれない情報(詳細な説明等)は要素表に書き出します。 要素表は成果物に関する要素表である成果物表とプロセスに関するプロセス表があります。

表1. 成果物表の例

成果物ID 説明
D1 コンパイルすれば動作するプログラムのコード。
D2 コードレビューによって付けられた D1 へのコメント。
D3 D1 が D4 を満たしていない入力を記載したバグチケット。
D4 D1 の満たすべき仕様。

表2. プロセス表の例

プロセスID 説明
P1 仕様にもとづきコードを書く。レビューコメントおよびバグチケットが出なくなるまで修正を繰り返す。
P2 実装していない者がコードをレビューする。
P3 仕様に照らして欠陥を発見する。

重要な概念として 初期成果物最終成果物 があります:

  • 初期成果物 は、どんなプロセスからも出力されない、すなわち所与の成果物です
  • 最終成果物 は、どんなプロセスの入力にもならない、すなわちプロジェクトの目当ての成果物です。プロジェクトは 最終成果物 を出力することを目的としています

スケジュールの理論

スケジュールの理論とは、プロセスの依存関係および各プロセスの所要時間を与えるととりえるプロセスの実行順序とその所要時間を定める体系です。 スケジュールの理論を使うと、プロセスの実行順序がプロセスの依存関係に沿っているかどうかの判定やその実行順序を採用した場合のトータルの所要時間を計算できます。

スケジュールの理論として PERT 図を与える Program Evaluation and Review Technique(PERT)や資源制約付きスケジューリング問題(Resource-Constrained Project Scheduling Problem; RCPSP)の各種モデルが知られています。 今回は PFD を元にした3つのモデルを紹介します:

  1. 無限資源・フィードバック線なし
  2. 有限資源・フィードバック線なし
  3. 有限資源・フィードバック線あり(pfd-tools で fsm と呼んでいるものです)

今回紹介するモデルの中では、資源無制限・フィードバック線なしのモデルが最も簡単です。 そのため、まず資源無制限・フィードバック線なしのものを説明します。 次に資源の制限を考慮したモデル、次にさらにフィードバック線も考慮したモデルへとステップアップしていきます。

モデルの紹介

無限資源・フィードバック線なし

このモデルは、あるプロセスの入力成果物がすべて揃い、かつそのプロセスが一度も実行されたことがなければそのプロセスを 実行可能 としたものです。 フィードバック線がないので手戻りはなく、どんなプロセスも1回より多く実行されることはありません。 PERT はこのモデルに相当します。

このモデルは資源が無制限であるため単純ですが非現実的な前提をもっています。 ふつう、プロセスの実行には実行者という資源が必要で、現実的には実行者は同時に複数のことをできません(できたとしても能率が落ちます)。 そのためこの方法で計算したスケジュールは現実的なそれより所要時間を短くしがちです。 もしより現実的な所要時間を計算したい場合は後に説明する資源の制限を考慮したモデルを使う必要があります。

例として手戻りのないソフトウェア開発プロセスの実行が進展していく様子を示します。

無限資源・フィードバック線なしのモデルの実行が進展していく様子の例

全体の所要時間は次のように計算されます。 プロセスが複数あればそれは並列化直列のいずれかで接続されています。 並列プロセスの所要時間は並列に実行されるプロセスの所要時間のうち最大のものです。 直列なプロセスの所要時間は実行されるプロセスの実行時間の総和です。 これを再帰的に計算すると所要時間が一意に定まります。

有限資源・フィードバック線なし

このモデルは、あるプロセスの入力成果物がすべて揃い、かつそのプロセスが一度も実行されたことがなく、かつ必要な資源を占有できれば、そのプロセスを 実行可能 としたものです。 フィードバック線がないので手戻りがないことは変わりません。 前のモデルに加えて、資源の占有という条件が加わっています。 すなわち、同時に実行される複数のプロセスはある資源を同時に使用することができないということです。 前のモデルでは条件をすべて満たしたものをすべて実行することにできましたが、このモデルでは資源に被りのないように実行するプロセスを選ばないといけません。

このモデルではプロセスの依存関係に加え、プロセスごとに必要資源とそのときの消費作業量の関係を与えなければなりません。 たとえばAさんのみが担当できるプロセスでは、必要資源はAさん1人で消費作業量は1人分(=1人日)を与えるとよいです。 他にも複数人で手分けできる場合、消費作業量は必要資源の数に応じて大きくなるようにします。

例として手戻りのないソフトウェア開発プロセスの実行が進展していく様子を示します。

有限資源・フィードバック線なしのモデルの実行が進展していく様子の例

時刻2で同時に実行されるプロセス(時刻1→2の P2, P3)の必要資源に被りがあったためP2またはP3のどちらか一方しか実行できなくなっています。この図では P2 を先に実行し、P2 が終わったら P3 を実行する実行計画を選んでいます。

全体の所要時間の計算方法は状態遷移モデルで定義されます。 状態には時刻と各プロセスの残作業量が記録されています。

  • 初期状態 では時刻0、残り作業量はあらかじめ見積もっておいた所要作業量と同じです
  • 次状態の決め方は次のとおりです。現在の状態で 割当可能 なプロセスを得ます。 割当可能 とは、あるプロセスの入力成果物がすべて揃い、かつその残り時間が0でない(1度も実行が完了していないことと同値)ことです。定義より、割当可能 ならば必要資源を割り当てるとかならず 実行可能 です。この 割当可能 なプロセスの部分集合のうち、集合内のプロセスのすべての組み合わせにおいて必要資源集合に共通部分のないものが 有効な割当集合 です。現在の状態から 有効な割当集合 の要素を1つ選ぶと消費作業量がわかるので現在の残作業量から消費作業量を差し引きいずれかのプロセスの残作業量が 0 になる最短の経過時間を計算し、これを次の状態の時刻とします。現在時刻と次の状態の時刻の差を計算し、その 時間経過 * 消費作業量 を差し引いて次の残作業量を定めます。こうして 有効な割当集合 ごとに次の状態を定めます(つまり割当を状態遷移ラベルとした状態遷移モデルになっています)
  • ある状態のすべてのプロセスの残作業量が0であれば(=すべてのプロセスが1度は実行されたら)、その状態を完了状態と呼びます。どんなプロセスかつどんな遷移経路でも 完了状態 にいつか到達します5

ここで 初期状態 から 完了状態 に至るまでの割当の列(=遷移経路)を 実行計画 といいます。 任意の 実行計画 からガントチャートを計算できるため 実行計画 は重要な概念です。 各 実行計画 の所要時間は 完了状態 の時刻です。 ふつう、 実行計画 は複数ありえます。 そのため所要時間が最短となる 実行計画 を探索するとよいです。 最短の保証が必要であればダイクストラ法、最短ではないがそこそこのものが欲しければビームサーチ、とにかく短い処理時間で結果を出したければ貪欲法を利用できます。

有限資源・フィードバック線あり

このモデルは、あるプロセスのフィードバックされた入力成果物をのぞいたすべての入力成果物が揃い、かつフィードバックされる入力成果物を含めたいずれかの成果物の版が更新されてから一度も実行されておらず、かつ必要な資源を占有できれば 実行可能 としたものです。 このモデルが pfd-tools に搭載されています。

このモデルではすべてのプロセスは2回以上実行されえます。 前のモデルに加えて、版の更新を処理したか否かという条件が加わっており、成果物の版という概念をもちます。 前のモデルでは成果物はただ1回のみ作成されるので版という概念はありませんでした。

フィードバック成果物にはこれ以上変更がおきない最終版を定めておきます。 最終版に到達した成果物はそれ以降版に変更がないとみなします。 そうしないと版の更新がなくならないので永遠にプロセスを実行し続けられてしまうからです。 プロセスは複数回実行されうるので、2回目以降の所要作業量の計算方法を用意しておく必要があります。 ふつう現実では手戻りするごとに所要作業量は減っていきます。 今回の方法では繰り返すごとに所要作業量は前回の x% になる、のようにモデリングしています。

例として小規模ソフトウェアの開発プロセスの実行が進展していく様子を示します(進展の様子は pfd-tools の pfdrun コマンドや pfdrungraph コマンドから確認できます)。

有限資源・フィードバック線ありのモデルの実行が進展していく様子の例

全体の所要時間の計算方法は前のモデルの状態遷移モデルと似ています。 状態には時刻と各プロセスの残作業量、プロセスごとに版が更新されてからまだ利用されていない成果物の集合、成果物ごとに版数が記録されています。

  1. 初期状態 では時刻0、残り作業量は所要作業量と同じ、初期成果物 を入力とするプロセスについて 初期成果物 は版が更新されてから利用されていないとします
  2. 割当可能性 は前のモデルと少しだけ定義が異なります。あるプロセスが 割当可能 とは、そのプロセスにフィードバックされた入力成果物をのぞいたすべての入力成果物が揃い、入力成果物の版が更新されてからまだ利用していないものがあることです。割当可能 なら必要資源を割り当てられればかならず 実行可能 です。この 割当可能 なプロセスの部分集合のうち、集合内のプロセスのすべての組み合わせにおいて資源集合に共通部分のないものが 有効な割当集合 です。現在の状態から 有効な割当集合 から要素を1つ選ぶと消費作業量がわかるので現在の残作業量から消費作業量を差し引きいずれかのプロセスの残作業量が 0 になる最短の経過時間を計算し、これを次の状態の時刻とします。現在時刻と次の状態の時刻の差を計算し、その時間経過x消費作業量を差し引き次の残作業量のベースを定めます。このベースから残作業量が0のプロセスについて再実行する場合の所要時間分だけ回復(今回の事例ではプロセスごとに再実行所要時間割合xを定めておき、所要時間 * x^実行回数 で計算しました。プロセスの実行回数はそのプロセスの出力成果物の版と一致します)して次の残作業量を定めます。またベースから完了したプロセスがわかるので完了したプロセス P の入力成果物の未利用フラグを降ろし、P の出力成果物を入力とするプロセス P' ごとに未利用のフラグを立てます。こうして有効な割り当てごと次の状態が定まります。これも前のモデルと同じく割当を状態遷移ラベルとした状態遷移モデルになっています
  3. ある状態ですべてのプロセスが 割当不能 であればその状態を 完了状態 と呼びます。どんなプロセスかつどんな遷移経路でもいつか完了状態に到達できるかはまだ証明しきれていませんが経験的には真です

前のモデルと同様に、初期状態 から 完了状態 に至るまでの割当の列(=遷移経路)を 実行計画 といいます。 任意の 実行計画からガントチャートを計算できるため 実行計画 は重要な概念です。 各実行計画の所要時間は、完了状態の時刻です。 前のモデルと同様、ふつう実行計画は複数ありうるので所要時間が最短となる実行計画を探索するとよいです。 最短の保証が必要であればダイクストラ法、最短ではないがそこそこのものが欲しければビームサーチ、とにかく短い処理時間で結果を出したければ貪欲法を利用するとよいでしょう。

紹介しなかった重要な要素

紙幅の都合で紹介できませんでしたが実際のマスタースケジュールを計算するために必要な2つの概念があるので紹介しておきます。

時刻つき初期成果物

初期成果物が利用可能となる時刻を与えられるようにする拡張です。 プロジェクト外のプロセスによって未来に作成され納期が約束されている成果物をこれで表現します。

トリガー

プロセスの割当可能性に追加条件を与える拡張です。 テストは直前のテストレベルのテストが完了していないと普通開始しません。 この制約を前のテストレベルのフィードバックループが完了していないと開始できないというトリガーで表現できます。

マスタースケジュール

前述の通りプロセスと工程をマッピングするとその開始時刻と終了時刻からマスタースケジュールを計算できます。 開始時刻と終了時刻はすべて実行計画に含まれているので、実行計画とプロセスから大局的な工程へのマッピングさえあればマスタースケジュールを計算可能です(pfd-tools では planmaster がこれを担当します)。

クリティカルパス

クリティカルパス とは、所要時間または所要作業量を増やすと全体の所要時間が増えるプロセスからなる集合のことです。 たとえば並列のプロセスA、B があり、それぞれの所要時間を 5、 10 とします。

プロセスAとBのガントチャート

プロセスBの所要時間を増やせば全体の所要時間は増えますがプロセスAの所要時間を増やしても全体の所要時間は増えません。

プロセスAとBの所要時間が伸びた場合の比較

つまりBは クリティカルパス に含まれており、Aは含まれていません。

クリティカルパス 上のプロセスの所要時間が予定よりも伸びると全体の所要時間が伸びます。 そのため クリティカルパス 上のプロセスは要注意なプロセスです。

また クリティカルパス ではないプロセスの所要時間または所要作業量を増やしても全体の所要時間は増えません。 この増やしても全体の所要時間を変えない最大の幅のことを 全余裕 (Total Float)と呼びます。 クリティカルパスに含まれるすべてのプロセスの全余裕は0です。

プロセスAとBの全余裕

まだ所要時間または所要作業量を見積もれていないプロセスがあるとき、その 全余裕 を計算し、直列な部分の所要時間の総和が全余裕を超えるかどうかをインタビューすると迅速に全体の所要時間が伸びそうかどうかを判断できます。 全余裕 を超える場合、そのプロセスは クリティカルパス になるので要注意です。

工期短縮においても クリティカルパス が重要です。 工期短縮をするには、次のサイクルを何度も繰り返す必要があります:

  1. クリティカルパス を特定する
  2. その クリティカルパス 上のすべてのプロセスを改善する
  3. 1 に戻る

繰り返す必要があるのは、ある クリティカルパス 上のプロセスの所要時間または所要作業量を減らしていくと、あるところから全体の所要時間が減らなくなることがあるからです。 たとえば並列のプロセスA、Bの例では、プロセスBの時間を5より短くしても、全体の所要時間は5より短くはなりません。

プロセスBの最小弾性値

これは クリティカルパス がBを含むものからAを含むものに切り替わったため起きています。

このように クリティカルパス 上のプロセスにはその所要時間または所要作業量を減らすと全体の所要時間を減らせる最小の所要時間または所要作業量があるときがあります。 これを 最小弾性値 と呼びます6最小弾性値 のあるプロセスは、そのプロセスを改善すれば全体の所要時間を短縮できるため注目すべきです。

クリティカルパスを特定するには、実行計画を固定し、調べたいプロセスの所要作業量を伸ばし全体の所要作業量が伸びることを確認していくとよいです。 消費作業量は1と仮定し、調べたいプロセスの所用作業量を伸ばす幅を x とし、全体の所要時間が y だけ増えたとします。x - y全余裕 です。 全余裕 が 0 であるプロセスは クリティカルパス に含まれています。 こうしてクリティカルパスの特定ができます(criticalpath コマンドで同じことをしています)。

また同様に消費作業量が1であると仮定し、クリティカルパス 上の調べたいプロセスの所用作業量を 0 にし、全体の所要時間が z だけ減ったとします。所用作業量 - z最小弾性値 です。 こうして 最小弾性値 を計算できます(criticalpath コマンドで同じことをしています)。

全体の所要時間の短縮

クリティカルパス を特定し、その クリティカルパス 上のすべてのプロセスを改善すると全体の所要時間を短縮できます。 プロセスの改善には次のような方法があります。

効率化

直列なプロセスを並列にする

直列部分を並列にするグラフ操作に相当します。 所要時間が総和からmax関数に変わるので所要時間を短縮できます。 ただ本来予定されていた成果物を利用せず前段階の成果物を利用するように変更するため投機的になることが多いです。

手分け可能にする

プロセス表の資源割当と消費作業量の組の列操作に相当します。 空いている資源があれば、これを活用して消費作業量を増やせるので所要時間を短縮できます。 たとえば成果物を分解し依存のない構成部分を手分けして作成できるようにすると実現できます。

Cost から Delivery への転換

ありものを使う

ある成果物から逆向きに到達可能な要素を除去し、初期成果物 に置き換えるグラフ操作に相当します。 除去されたプロセスが クリティカルパス に含まれるとそれらの所要時間を0にできるので大幅な所要時間を短縮できます。 ただしありものの調達コストが増えます。

ツールによって自動化する

プロセス表の資源割当制限緩和操作と消費作業量の増加操作に相当します。 ツールの利用にコストがかかる場合はコストが増えます。

資源を追加する

資源表への行追加操作に相当します。 有効な資源割当集合が増えるため手分け可能なプロセスがあれば消費作業量を増やせて所要時間を減らせます。 遅れているプロジェクトへの増員はプロジェクトを余計に遅らせることは人月の神話で有名です。 ただし PFD と要素表が十分に記述できていれば学習コストと連絡コストはそこまでかからないと見込めるためブルックスほど筆者は悲観的ではありません。

Quality から Delivery への転換

スコープを縮小し、プロセスの所要時間または所要作業量を縮小する

プロセス表の所要時間または所要作業量を小さくする表操作に相当します。 到達可能なすべての成果物(最終成果物を含む)のスコープ縮小を伴うことに注意が必要です。

成果物の品質低下リスクを許容し、プロセスの所要時間を減らす

プロセス表の所要時間または所要作業量を小さくする表操作に相当します。 最終的な品質が酷いことになるリスクを負います。なるべく避けるべきです。

本事例のスケジュール策定方法

本事例では前述のスケジュールの理論の3つ目のモデル(資源制限・フィードバック線あり)を利用して、次のようにスケジュールを策定しました。実際にこの計画通りのスケジュール、品質で達成できるかどうかはこれから明らかになります。

$ # PFD を Google Drive 上の diagrams.net を使って関係者と共同で作成した。これを手元にダウンロードしておく:
$ cat ./pfd.drawio
<mxfile host="65bd71144e">
    <diagram id="ZC_KSqUXoirMDHc_9cWT" name="P0">
...

$ # pfdrenum で採番しておく:
$ pfdrenum -inplace ./pfd.drawio

$ # Google Drive 上の PFD へすぐ上書きしておく。

$ # 空の複合成果物表を作成しておく:
$ printf "ID\tDescription\tDeliverables\n" > ./cd.tsv

$ # PFD からプロセス表を作成する:
$ pfdtable -p ./pfd.drawio -cd cd.tsv -t ap > ./ap.tsv

$ # プロセス表に拡張カラムを追加しておく。
$ edit ./ap.tsv
$ head -1 ./ap.tsv
ID      Description     予想作業量      予想手戻り作業量割合    必要資源        開始条件        グループ        マイルストーン

$ # PFD から成果物表を作成する:
$ pfdtable -p ./pfd.drawio -cd cd.tsv -t ad > ./ad.tsv

$ # PFD から合成プロセス表を作成する:
$ pfdtable -p ./pfd.drawio -cd cd.tsv -t cp > ./cp.tsv

$ # PFD とプロセス表から資源表を作成する:
$ pfdtable -p ./pfd.drawio -cd cd.tsv -t r > ./r.tsv

$ # マスタースケジュール用のプロセスグループ表を作成する(pfdtable はまだ未対応):
$ edit ./g.tsv

$ # マスタースケジュール用のマイルストーン表を作成する(pfdtable はまだ未対応):
$ edit ./m.tsv

$ # PFD 上のすべてのプロセスを関係者と共同で2点見積り(楽観と悲観)した。過去データがなかったため当てずっぽうで繰り返し再実行所要時間割合と成果物の最終版数を決めた
$ # ./ap1.tsv に楽観の予想作業量を記載する:
$ cp ./ap{,1}.tsv
$ edit ./ap1.tsv

$ # ./ap2.tsv に悲観の予想作業量を記載する:
$ cp ./ap{,2}.tsv
$ edit ./ap2.tsv

$ # いちいちすべての要素表を指定するのは面倒なのでプロジェクトファイルを作成しておく:
$ echo '{"pfd":"pfd.drawio","atomic_process_table":"ap1.tsv","atomic_deliverable_table":"ad.tsv","resource_table":"r.tsv","composite_deliverable_table":"cd.tsv","milestone_table":"m.tsv","group_table":"g.tsv"}' > ./project1.json
$ echo '{"pfd":"pfd.drawio","atomic_process_table":"ap2.tsv","atomic_deliverable_table":"ad.tsv","resource_table":"r.tsv","composite_deliverable_table":"cd.tsv","milestone_table":"m.tsv","group_table":"g.tsv"}' > ./project2.json

$ # 整合性を確認する。pfdlint が正常終了したら問題なし。異常終了したら正常終了するようになるまで問題を修正する:
$ pfdlint -f ./project1.json
$ pfdlint -f ./project2.json

$ # 楽観側の実行計画を作成する:
$ pfdplan -f ./project1.json -poor -out-format plan-json | tee ./plan.json
{
  "initial_state": {
    "time": 0,
...

$ # 実行計画を Google Sheets の Timeline ビューで読める形に変換する:
$ pfdtimeline -f ./project1.json -start 2025-04-01 -not-biz-days <(holidays -locale ja) ./plan.json | tee ./timeline1.tsv
AtomicProcess   NumOfComplete   AllocatedResources      Description     StartTime       EndTime Start   End
P1      0       Biz     要求分析する    2025-04-08 10:00:00     2025-04-22 10:00:00     5       15
...

$ # Google Sheets に貼り付ける:
$ pbcopy < ./timeline1.tsv

$ # 悲観側の実行計画を作成する:
$ pfdplan -f ./project1.json -poor -out-format plan-json | tee ./plan.json
{
  "initial_state": {
    "time": 0,
...

$ # 実行計画を Google Sheets の Timeline ビューで読める形に変換する:
$ pfdtimeline -f ./project1.json -start 2025-04-01 -not-biz-days <(holidays -locale ja) ./plan.json | tee ./timeline2.tsv
AtomicProcess   NumOfComplete   AllocatedResources      Description     StartTime       EndTime Start   End
P1      0       Biz     要求分析する    2025-04-08 10:00:00     2025-04-22 10:00:00     5       15
...

$ # Google Sheets に貼り付ける:
$ pbcopy < ./timeline2.tsv

$ # Google の Timeline ビューで実行計画を確認し、意図していないプロセスの依存関係、トリガーがないか確認し、問題があれば原因究明に pfdquery 等を使いながら PFD やプロセス表を修正する。
$ pfdquery -f ./project.json P20
QUERY   SOURCE  RESULT
P20     PFD[desc]       IP制限状態でリリースする
...

$ # 楽観・悲観のリリース時刻の差を確認し、不確実性としてレポートしておく。
$ # 楽観xバッファ1.5倍でマスタースケジュールを作成する:
$ planmaster -ap ./ap1.tsv -g ./g.tsv -m ./m.tsv -p ./plan.json -b 1.5 -start 2025-04-01 -not-biz-days <(holidays -locale ja) | tee ./master.tsv
Group   GroupDescription        Milestone       MilestoneDescription    Start   End
Admin   管理者向け機能  M1      要求分析        2025-04-10 14:30:00     2025-05-02 14:30:00
...

$ # Google Sheets にマスタースケジュールを貼り付ける
$ pbcopy < ./master.tsv

$ # この時点で納期が間に合うかどうかを確認する。今回は間に合ってなかったのでプロセスの改善を開始した。
$ # クリティカルパス分析をする。全余裕(TOTAL_FLOAT)が 0 なプロセスがクリティカルパスに含まれている:
$ criticalpath -f ./project.json -poor | tee ./criticalpath.tsv
ATOMIC_PROCESS  TOTAL_FLOAT     MINIMUM_ELASTICITY
P1      0.00    10.00
P2      35.73   5.00
...

$ # クリティカルパス上の全プロセスについて、スコープアウト可能なオプションを担当者へヒアリングし、責任者の承認を得てスコープアウトする。スコープ縮小後の予想作業量に修正する。
$ edit ./ap1.tsv
$ edit ./ap2.tsv

$ # クリティカルパス分析を繰り返し、目標に達するまでスコープ縮小を続ける。目標に達したらそのスコープを確定し、マスタースケジュールを再計算する。
$ pfdplan -f ./project1.json -poor -out-format plan-json | tee ./plan.json
$ planmaster -ap ./ap1.tsv -g ./g.tsv -m ./m.tsv -p ./plan.json -b 1.5 -start 2025-04-01 -not-biz-days <(holidays -locale ja) | tee ./master.tsv
Group   GroupDescription        Milestone       MilestoneDescription    Start   End
Admin   管理者向け機能  M1      要求分析        2025-04-10 14:30:00     2025-05-02 14:30:00

$ # Google Sheets のマスタースケジュールを更新して作業完了。
$ pbcopy < ./master.tsv

途中で登場した成果物の内容(本記事用に適当に作成しています):

PFD(./pfd.drawio

マスタースケジュールを計算するために作成した PFD の例

プロセス表(./ap1.tsv

ID Description 予想作業量 予想手戻り作業量割合 必要資源 開始条件 グループ マイルストーン
P1 要求分析する 10 0.1 Biz:1 User,Admin M1
P2 画面遷移仕様を記述する 10 0.1 Front,Designer:2;Designer:1 User,Admin M2
P3 インフラ設計する 5 0.1 SRE:1 User,Admin M3a
P4 APIを設計する 8 0.1 App:1 User,Admin M3a
P5 テーブル設計する 10 0.1 App:1 User,Admin M3a
P6 サーバーを実装する 20 0.1 App:1 User M3b
P7 管理画面を実装する 5 0.1 App:1 Admin M3b
P8 仕様を妥当性検証する 10 0.1 SET:1 \complete(*) User,Admin M2
P9 正当性検証を設計する 10 0.1 TE:1 User M3c
P10 クライアントを実装する 15 0.1 Front:1 User M3b
P11 画面表示仕様を記述する 10 0.1 Front,Designer:2;Designer:1 User M2
P12 使用性をレビューする 7 0.1 Designer:1 User M2
P13 SRE によるコードレビュー 3 0.1 SRE:1 \complete(*) User M3b
P14 管理オペレーションを設計する 5 0.1 Ope:1 Admin M3a
P15 オペレータによる受入テスト 5 0.1 Ope:1 \complete(*) Admin M4
P16 サーバーとクライアントを結合する 0.2 0.1 App,Front:1 User M3b
P17 開発環境でシステムテストする 10 0.1 TE:1 \complete(*) User M4
P18 ステージング環境で外部サービスと結合する 0.2 0.1 App:1 User M3b
P19 システム統合テストする 5 0.1 TE:1 \complete(*) User M5
P20 IP制限状態でリリースする 0 0.1 SRE:1 \complete(*) User,Admin M6
P21 本番検証する 1 0.1 TE:1 \complete(*) User,Admin M6
P22 IP制限解除する 0 0.1 SRE:1 User,Admin M7

合成プロセス表(./cp.tsv

ID Description
(空) (空)

成果物表(./ad.tsv

ID Description 利用可能時刻 最大版
D1 リリースされユーザーが利用できる新機能システム。 - -
D4 使用性を改善する画面表示仕様の Figma 上のコメント。 - 2
D5 安全性・活性・到達可能性違反を引き起こす入力。 - 1
D7 GitHub 上の SRE によるコードレビューコメント。 - 2
D8 オペレータによる受入テストバグチケット。 - 2
D9 CS が受信したユーザーからサービスへの改善要望。 0 -
D10 経営戦略。どのようなユーザーを今後狙っていくかが指示されている。 5 -
D11 外部サービスX。 15 -
D12 機能要求。活性と到達可能性の形式で表現されている。 - -
D13 非機能要求。安全性と性能要件の形式で表現されている。 - -
D14 ユーザーのペルソナ。 0 -
D15 画面遷移仕様。画面を状態とし、イベント・ガード・事後条件の3つ組を遷移に付与した画面遷移図。 - -
D16 インフラ構成図。 - -
D17 シグネチャ、事前条件・事後条件の組で表現されたAPI仕様。 - -
D18 DDLで表現されたテーブルのテーブル定義書。 - -
D19 サーバー実装。 - -
D20 オペレータが操作する管理画面の Web サーバー実装。 - -
D21 ユーザー向け機能に関連するテストケース。 - -
D22 クライアント実装。 - -
D23 画面表示仕様。画面ごとに画面UIの代表的なレイアウトおよび画面UI間の相互作用が記述されている。 - -
D24 管理オペレーション手順書。想定される管理オペレーションが必要な状況ごとに対応する管理オペレーションの手順が書かれている。 - -
D25 開発した新機能システム。 - -
D26 外部サービスXと統合された新機能システム。 - -
D27 IP制限が社内向けに制限された外部サービスXと統合された新機能システム。まだユーザーはサービスを利用できない。 - -
D3.1 JIRA 上の本番検証由来のバグチケット。 - 1
D3.2 JIRA 上のシステム統合テスト由来のバグチケット。 - 2
D3.3 JIRA 上のシステムテスト由来のバグチケット。 - 3

複合成果物表(./cd.tsv

ID Description Deliverables
D3 BTS上のユーザー向け機能のバグチケット。 D3.1,D3.2,D3.3

プロセスグループ表(./g.tsv

ID Description
User ユーザー向け機能
Admin 管理者向け機能

マイルストーン表(./m.tsv

ID Description Groups Successors
M1 要求分析 User,Admin M2
M2 仕様記述 User,Admin M3a,M3b,M3c
M3a 設計 User,Admin M4
M3b 実装 User,Admin M4
M3c テスト設計 User,Admin M4
M4 システムテスト User,Admin M5
M5 システム統合テスト User,Admin M6
M6 本番検証 User,Admin M7
M7 リリース User,Admin

終わりに

この記事ではマスタースケジュールの理論とコインチェックにおける実践を紹介しました。 本記事の内容をまとめると次のようになります:

  • PFD と要素表を書けばマスタースケジュールを機械的に計算できる
  • スケジュールの理論を使うと改善すべき箇所がピンポイントでわかる
  • 現実のプロジェクトに適用でき、実際にスケジュール作成に使える

参考文献


  1. 「インプットをアウトプットに変換する,相互に関連する又は相互に作用する一連の活動」ISO25000:2017
  2. 清水氏の提唱したオリジナルでは成果物を書類記号であらわします。また書類記号に拘らず成果物に合わせてわかりやすい記号を使うとよいともしています。ここでは説明を単純に保つため矩形で統一しています。
  3. プロセスの依存関係をあらわす代表的な表現形式に PERT 図(またはアローダイアグラム)があります。成果物をイベント、プロセスを矢線とし、そのプロセスの所要時間を矢線に付属すると PFD を PERT 図に変換できます。つまり PFD は PERT 図で成果物を強調したスーパーセットとみなせます。等価ではなくスーパーセットであるのは後述するフィードバック線があるからです。フィードバック線は PERT 図には含まれません。
  4. 清水氏の提唱したオリジナルの定義には破線の矢線はありません。オリジナルの定義にある更新の方向を明らかにするためのアレンジです。
  5. PFD はフィードバック線を除けば有限な DAG であり、かつ消費作業量は 0 より大きいですから、どんな状態でも完了状態でなければ 割当可能 なプロセスが1つはあります。したがって遷移するたびに残作業量の総和が狭義単調減少します。そのためいつかかならず完了状態に到達します。
  6. これは筆者の命名です。ここでは 弾性 を局所のプロセスの所要時間または所要作業量を増減させると全体の所要時間を増減させることとしています。このように弾性を定義すると、 全余裕 に所要時間を足したものが 最大非弾性値 となります。最小弾性値 に対応する PERT の用語はないようです。

5年前に私の中で起きた理解しやすさのブレークスルーを振り返る

10年前から文章の理解しやすさの認識がだいぶ変わった。昔は論理式やそれに相当する固い説明がどっさり書かれている文章を理解しやすいとは思わなかったが、今ではコンテキストさえ掴んでいれば論理式やそれに相当する固い説明で書かれた文章の方が理解しやすいと感じる。たとえば論理式やそれに相当する固い説明がどっさり(本当にどっさり)書いてある ソフトウェア科学基礎型システム入門 を理解しやすいと今は思う。たとえば、ソフトウェア科学基礎には以下の記述がある:

s が無限回含まれるとは,任意のi ≥0 について,s = s j となるあるj ≥ i が存在することである.

── ソフトウェア科学基礎 p.198

昔だったら、この記述を理解しやすいとはきっと思わなかっただろう。「s が無限回含まれる」なんて論理式の説明がなくともわかるし固い説明なんてしなくていいのではと思っていははずだ。しかし今ではこの記述を理解しやすいと感じ、さらにこのような記述はあるべきだとまで思っている。

この認識の変化はなぜ起きたのだろうか。振り返ると、数学基礎論の初歩を理解したことが鍵だったように思う。

数学基礎論 の初歩を理解すると、論理式の意味の理解や、形式的な証明をできるようになる。私はここに とてつもない価値 を感じている。しかし世間を見渡してみればこの要因の価値がわかる人とわからない人の真っ二つに分かれるのではないか。そこで価値がわからない人向けに説明を試みる。この価値のわからない人は「論理式の意味がわからなくとも、形式的な証明ができなくとも、何か物事を理解することはできる」と考えていると思う。そこで思考実験をしてみよう。ある難しい理論を易しく説明すると謳う文献が理論の当てはまる例を1つ2つだけ紹介している。この文献を読めばその理論を精密に理解できるだろうか?私は例をいくつか知っているだけでは深い理解にはならないと思う。深い理解とはその理論の対象について多くの主張の帰結を短時間に回答できることだと考えているからである。この定義の上で考えると、例を2つ3つ紹介するような易しい文献を読むだけでは多くの主張の帰結を回答できるようにはならない。ただ短時間のうちに紹介されている例については主張の帰結を回答できるようになる効果はあると言えるだろう。ただ、具体例はわかりやすいがそれだけの説明だと他にどんな例があるかの推測を正確はできない。一方、論理式やそれに相当する固い説明で書かれた文書は親切にも例が書かれていない限り短時間に主張の帰結を回答できるようにはならない。しかし、ひとたび時間をかければ急激に帰結を回答できる主張が増加していき、易しい文献の理解度をいつか追い越す。これを図にすると次のようになる:

易しい文献と固い文献の理解度の時間的変化

この差は思考実験のしやすさに由来している。論理式やそれに相当する固い説明は具体的な対象を割り当ててその真偽を厳密に判断できるため、読者側で実験しすぐに結果が得られる。しかし例を2つ3つ紹介されただけでは、正確に他の例を思いつくことには手間がかかる。似ている例を直感的に挙げることはできるだろうが、それが本当に説明されている理論に当てはまるのかどうかの検証に時間がかかるからである。余談であるが、よく練られた文献は論理式や固い説明を書きつつ例を添えることでそれぞれのいいとこどりをしている(代わりに文量が増えるため鈍器と化す)。このことを一般化すると、個別の例の説明は素早く理解度を上げられるが深い理解には至れず、膨大な例を端的に記述できる説明は深い理解に至れるが抽象的なため理解に時間を要するということだろう。そして深い理解に至るためには抽象的な説明を精密に記述できる言語が必要なのである。そしてそのような言語として論理式が知られている。だから論理式の意味を知ることが何かを深く理解する上で鍵になってくるのである。

ただ論理式を知るだけでは片手落ちである。論理式の意味がわかるだけでは思考実験をしにくいからである。論理式は、決まった手順で論証することで前提が正しければ帰結もまた正しいと言える性質を備えている1。ここでこのような論証のことを 形式的証明 という。数学の証明もそれが正しければ形式的に証明できる2。ただし義務教育で習う数学のほとんどの証明は非形式的な証明であることから形式的な証明に馴染みのある人は多くない。形式的な証明の最大のメリットは機械による支援が受けられることである。定理証明支援系という形式的な証明を支援するツールがある。IsabelleRocqLean などさまざまなものが知られている。これらを使うと、主張が形式的な論証に沿っているかの確認だけでなく、主張の自動証明や反例探索ができる。これによって、論理式を使って機械に支援されて主張を実験できるのである。

もうひとつ、言語化できないものを人は理解できないという点も指摘しておきたい。思考は言語に制約されることを痛感したできごとを2つ紹介しておく。

10年前の私にはさっぱり証明の思いつかなかった Promise.all と race の組み合わせによる待ち合わせの表現力の証明を、その5年後にできるようになった。私が10年前に証明を思いつかなかった理由はあり得るケースを網羅的に表現する言語を知らなかったからである。その5年後には 操作的意味論をかじり、また状態遷移モデルを扱えるようになったため、前者なら「どのように評価されても」、後者なら「どんな遷移経路でも」と表現できることを知っていた。 この知っている言語の差が 10 年前の私に証明できなかった主張をその 5 年後には(否定的に)証明できるようになったという違いを生んだ。

また 10 年前の私はGUIアプリケーションの仕様とは何かを言語化することができなかった。10年経った今では画面仕様書への静的検査器 を開発できるほど、GUIアプリケーションの仕様とは何かを説明できる。10年前と今の違いは2つある。1つは仕様とは何かという理解の深さの違い、もう1つは膨大な遷移経路のあるモデルを端的に表現するために知っている語彙の多さの違いである。10年前の私は仕様が何かをはっきりと説明することはできず、これまで読んできた「仕様書」と呼ばれるものの共通項を挙げるぐらいしかできなかった。今の私は仕様がどんな実装についてもその正誤を機械的に判定する基準であると知っている。また、GUI アプリケーションは画面を行って戻ってをいくらでも繰り返せることが多く、その遷移経路の数は無限大であることが多い。すると仕様で許容したい遷移経路の集合や、実装で生じる遷移経路の集合を現実的な記述量で表現する言語が必要になる。10年前の私はそれを知らなかった。10年経った今の私は 時相論理CSP による表現を知っている。そのため、与えられた実装が仕様を満たすかどうか機械的に判断する基準を端的に記述できる。

前者も後者も、言語を知ることでできなかったことができることに変わっている。言語がこんなにも私のできることを広げてくれたのである。それにともなって理解しやすさの感覚が変わったのだろう。

まとめ

文献の理解しやすさの感覚の変化を振り返った。今の私は次のように考えている:

  • 深い理解は個々の事例からではなく網羅的な思考実験によりもたらされる
  • 効率的な思考実験の鍵は論理式と形式的証明である
  • 人間の思考は言語に制約される。できないことをできることに変えるため言語を知ろう

  1. 健全な論理体系であれば。
  2. 完全な論理体系であれば。

仕様の静的検査器のブログに対する匿名ダイアリーへの回答

仕様の静的検査器のブログ記事について質問や添削を匿名ダイアリーでいただいたので私の意見を回答します。

anond.hatelabo.jp

前書き

まず上記の匿名ダイアリー記事を書いてくださった方へ。記事を閲覧しさらにご意見を書いてくださったことに感謝いたします。 おそらく著者様は検証に携わる立場の方なのかなと推察します。いただいたご意見は大変参考になりました。 ここに私からの回答を付け加えることで、記事の訂正及び補足ができると嬉しいなと思っています。

本文

「実装のふるまいを誤っていると判断する」のは本当に仕様の欠陥といえるのか?

→ テストで気づくことを想定しているんだろうけど、テストは仕様書から作られるわけで、テスト設計ミスでは?

「本来正しいと意図した実装の振る舞いを誤っていると判断したり、その逆に誤っていると意図した実装を正しいと判断する仕様には欠陥があります」の読点前の部分に対する疑問でしょうか。以下はそうだと仮定して話を進めます。もしそうでなければ読み飛ばしてください。

まず元記事の文を図解し、元記事の意図していた仕様の定義を確認します。 というのも仕様には複数の定義があり、この記事ではそのどれを意図しているのかを明らかにしなければよい議論にならないからです。 なお元記事で採用している仕様の定義は、数理論理学に立脚した仕様の定義で形式仕様と呼ばれるものです。

図1の矩形は過去・現在・未来にわたってありえるすべての実装の集合を意味しています。

元記事における仕様は、図2のようにこの実装の集合を正誤判定によって2つの部分集合へと分割する役目をもちます。

仕様記述をする際、仕様記述者の脳内には意図した仕様があるはずです。 これが図3の左上です。 実際に記述された仕様は図3の右上です。 この左上と右上が一致しなかったとき仕様に欠陥があります(図3の②と④の部分)。

元記事の読点前の部分は図3の②を指し、読点後の部分は図3の④を指します。

図3の②の典型的な例は記事中に示している矛盾した記述です。 他にもパスワード入力画面にあるはずのパスワード入力フォームの配置の指示を書き忘れてしまってパスワード記入フォームが存在しないことが正解という仕様書になってしまっていたケースもこの例に当てはまります。 このいずれの例も「テスト設計ミス」と呼ばれることに違和感があります。

「誤っていると意図した実装を正しいと判断する仕様」は、通常の開発において起こり得るのか?

要求定義〜仕様作成〜テスト設計の流れを踏む以上、このような逆転は起こりにくいのでは?

この話は入力によって出力が一意に決まるシステム、すなわち関数的なシステムの仕様である、事前条件・事後条件の組を表で表したときに起こりうる例がわかりやすいと思います。 事前条件・事後条件については「契約による設計」に解説があります。 大雑把にいうと、事前条件はシステムに入力してよい入力を決める条件です。 事後条件は、事前条件を満たす入力について、どのような出力を返さないといけないかを決める条件です。この事前条件・事後条件の表し方はいくつかあります。 ここでは入出力の表として表現したとし、事前条件を満たさない入力は行を書かかないこととします。 これには2つの理由があります。

  1. 事前条件を満たさない入力は無数にあり、その全てを表に書ききると膨大になってしまい読みづらくなるため
  2. 事前条件を満たさない入力について実装はどのように振る舞ってもよいため

例えば FizzBuzz 関数の仕様をこの規則にしたがって表にすると次のようになります:

入力 出力
1 "1"
2 "2"
3 "Fizz"
4 "4"
5 "Buzz"
... ...

注目して欲しいのは、0 以下の整数や浮動小数点数、文字列などは事前条件を満たさないことから表に書いていないところです。 このときFizzBuzz 関数の仕様の表に 100 のときの入力だけ書き忘れてしまったとしましょう。 すると100 の入力は事前条件を満たさないということになり、本来実装に100を入力すると"Buzz"が出るべきなのにどのように振る舞っても正しいと判定されてしまいます。 つまり100以外の入力についてはすべて仕様を満たすが100の入力に対する出力を"100"とする実装や"FizzBuzz"とする実装が正しいと判定されてしまいます。 これはまさに誤っている意図した実装を正しいと判定してしまう仕様を書いてしまった例になっています。

実装が顧客の要求にたまたま合っていたとしても、それが仕様書に沿っていないなら「正しい実装」とはいえないのでは?

→ この場合、実装がたまたまあっていただけで、テストもされていない状況。仕様も実装も両方が誤っていたというべきでは?

仕様と同じように要求にも複数の定義があるため用語を整理してから解釈を説明します。 私は Michel Jackson の要求の定義を採用しています。 この定義では、問題領域に属する事柄が要求であり、機械領域に属する事柄が仕様であるとされます。 また、仕様が要求を満たしかつ実装が仕様を満たすならば自動的に実装は要求を満たす、というふうに要求と仕様、実装の間の関係が満たすべき性質が与えられています。

ここで、要求と仕様、要求と実装のそれぞれの間で満たすべき性質を妥当性(Validation)といい、仕様と実装の間で満たすべき性質を正当性(Verification)といいます。 この定義では、おっしゃる状況は実装が妥当であるが正当ではない状況ということになります。

そして元記事では意図しない妥当性違反のみをスコープとし、意図した妥当性違反と正当性違反についてはスコープ外1とするよう構成しました。 この立場からおっしゃる状況を解釈すると、仕様書に沿っていない実装は正当でない、つまりおっしゃる通り正しくない、となります。 元記事はこの解釈で一貫して書いているつもりです。

「さて」が不要

接続語としての意味が弱く、文の格調を下げている印象。

参考になりました。ありがとうございます。

仕様や満たすという言葉の用法の定義直後に「仕様の欠陥」の話が始まり、論点が拡散する

仕様の性質や運用について触れた後に、欠陥の話題を出した方が構成として自然。

「運用」を仕様という成果物を取り巻くプロセスというふうに解釈して回答します。 もしそうでない場合は読み飛ばしてください。

仕様がどう使われるかによって仕様の備えるべき性質が決まります。 したがって仕様という成果物を取り巻くプロセスが重要であるというご指摘はごもっともです。 ただそれが元記事において説明する優先度は低いと判断しここに関する説明をしていません。

今回の構成は、仕様の欠陥を見つける静的検査器を実装した成果を説明するために以下の構成をとっています:

  1. 用語定義

    用語の混乱を避けるために元記事で用いる「仕様」および「仕様を満たす」、「仕様の欠陥」の意味を定義した

  2. 問題提起

    仕様の欠陥はあるべきではないが現実にはあるというギャップを問題として提起した

  3. 解決方法

    仕様の欠陥を防ぐアプローチとして仕様書の形式を整え静的検査器を実装し解決を頃みた

  4. 今後の展望

論点が拡散するように見えたのは1の文量が多く、そこが主題だとご認識いただいたからかもしれません。 ただ私は3を主題として設定しているつもりです。 そのため仕様という成果物を取り巻くプロセスについてはあまり言及していません。 1が長くなってしまったのは「仕様」という複数の定義をもつ語を混乱しないように解釈してもらうための苦肉の措置でした。

仕様における「正しさ」の基準と、実装やテストの誤りの区別が曖昧

仕様の欠陥と、テスト設計・実装のミスは分けて扱うべき。

元記事では次のようにそれぞれの欠陥を定義しました:

仕様の欠陥
意図と表現物の相違による妥当性違反、意図と表現物の相違によらない妥当性違反(元記事のスコープ外)
実装の欠陥
正当性違反(元記事のスコープ外)、妥当性違反(元記事のスコープ外)

これによればいずれも厳密に区別される認識です。 そうでない場合、ぜひご指摘をいただきたいです。

例示が適切でない

 → 「仕様と実装がどちらも間違っていたが、結果的に要求に近かっただけ」のケースであり、 「正しい実装を誤っていると判断した仕様」ではない。

言及先の文を見つけられませんでした。 よろしければ言及先の文をご教示願います。

「無価値」という語の使用が不用意に攻撃的

仕様のすべてが誤っているわけではない状況であるので、特定の部分が誤っている・矛盾しているという事実の指摘にとどめるべきで、感情的な評価語は避けた方が望ましい。

「無価値」という言葉が感情的な評価語であるという認識を持っていませんでした。 そのような解釈があることに気づけていなかったため、ご指摘に感謝いたします。

私の解釈では、ソフトウェア開発プロセスに登場するすべての成果物はそれに誤りがなければ(客観的な)価値があるというという認識を持っています。 ここから仮に(客観的に)無価値なものがあるとしたらその成果物に誤りがある、というふうに背理法で導くことが本記事の意図でした。 客観的な価値評価は合理的なソフトウェア開発プロセスにおいて極めて重要な概念であると認識しています。 「無価値」を感情的な評価語であるとするのはおそらく主観による評価であるからだと思っていますが元記事ではそれを意図していません。

ていうか、仕様矛盾の例示いらなくない?

単なる文言の誤りをここまで冗長に書く必要ないし、無用に攻撃的だしで、なんだかなという感じ。ここ以外もなんだけど、全体として実装やってるやつは悪くないんだ!って気持ちがあふれてる感じがする。気持ちはわかるが書きたいことと関係なくない?

これはのちに登場する静的解析期の検査ルール12に対する事前の説明を意図して書いたものです。 特定の誰かを悪くいう意図はありませんでした。 そのような意図に取られる表現であったことをお詫びいたします。

添削後( 「仕様に欠陥があるとどうなるか」も別に言いたいことと直接関係ないだろうし軽く触れる程度でいいでしょってことでマージした場合)

仕様の定義にはいくつかの解釈がありますが、ここでは「仕様」を、要件定義に基づいて作成され、実装の正しい振る舞いを定める基準と定義します。実装が正しいと判定される場合、それは実装が仕様を満たしていることを意味します。

要件定義を元に作成された仕様に誤りがあった場合、実装の段階でその誤りに気づくことは難しいことが多いです。このような誤りは、通常、顧客レビュー(受け入れテストやUAT)で判明します。しかし、顧客とのコミュニケーションコストや調整が必要になるため、テスト段階で問題を発見するよりも、対応に要する工数が多くなりがちです。

この添削は私の意図した構成から大きく逸れたものになっています。 前述の通り、ここは用語を厳密に定義することでその後の説明の意味解釈の混乱を避けるために用意したセクションです。 「要件定義」や「要件定義に基づく」といった複数の定義があり混乱している用語がその定義の説明なしに登場することは望ましくありません。 また「仕様」の定義の説明がほとんどなくなってしまったことも残念に感じます。

後書き

上記の匿名ダイアリー記事を書いてくださった方へ。記事を閲覧しさらにご意見を書いてくださったことに感謝いたします。 私の見解や意図を説明してみましたのでもしよろしければまたご意見・ご指摘をいただけると嬉しく思います。


  1. 元記事の該当部分「別の欠陥の例として、仕様が意図どおりだったとしても、その仕様を満たした実装が解決したかった問題を解決できなかった(e.g. 売り上げが目標に至らなかった)というものもあり得ます。 ただし本記事ではこの種類の欠陥はスコープ外としています」
  2. 元記事の該当部分「PlantUMLで記述された状態遷移図と各画面それぞれのUI要素表のインタラクション列に出現する画面IDを付き合わせた辺の整合性の検査5。画面遷移図内のすべての遷移がUI要素表にあり、かつUI要素表にあるすべての遷移が画面遷移図にあればOK、それ以外はNGと判定」

画面仕様書への静的検査器を実装したらたくさんの欠陥を発見できた話

社のブログで最近の成果を公開しました。ぜひご覧ください。

swet.dena.com

画面仕様書への静的検査器を実装したらたくさんの欠陥を発見できた話 --- SWET第二グループの[Kuniwak](https://kuniwak.com/)です。本記事では画面仕様(後述)の仕様書に対する静的検査器を開発した事例について紹介します。 ## 伝えたいこと 1. 画面表示と画面遷移を記述する仕様書は機械可読にできる 2. 仕様書が機械可読であれば仕様の静的検査ができる 3. 静的検査によって自身の担当範囲の15%の画面から計40件弱の欠陥を発見した 4. 機械可読な仕様書にはさらなる応用が見込める ## おさらい:仕様とは 仕様の定義はいくつかあります。 ここでは仕様とは実装の正しい振る舞いを定める基準とします。 ある実装が正しいと判定されることを、実装が仕様を満たしたといいます。 誰による判定でも実装が仕様を満たしたかどうかの判定結果は一致すべきです。 さて実装の欠陥と同様に、仕様にも欠陥が生じえます。 本来正しいと意図した実装の振る舞いを誤っていると判断したり、その逆に誤っていると意図した実装を正しいと判断する仕様には欠陥があります。 仕様の欠陥の典型的な例の1つに矛盾した記述の存在があります。 矛盾した記述を含む仕様はどんな実装でも満たせません。 たとえば、ある箇所でログイン画面のボタンのテキストが「ログイン」と指示されているのに、別の箇所で「サインイン」と指示されている場合、どんな実装でもこの両方を同時に満たすことはできません。 すると、仕様を満たせる実装が1つもないということになります。 仕様を満たせる実装が1つもない仕様は無価値ですから、実際には正しいとしたかった実装はあったはずでしょう。 まさにこの例は正しいとしたかった実装を誤っていると判断する仕様になっています。 別の欠陥の例として、仕様が意図どおりだったとしても、その仕様を満たした実装が解決したかった問題を解決できなかった(e.g. 売り上げが目標に至らなかった)というものもあり得ます。 ただし本記事ではこの種類の欠陥はスコープ外としています。 ## 仕様に欠陥があるとどうなるか さて、仕様に欠陥があるとどうなるのでしょうか。 開発プロセスにおける仕様の利用者は主に2つです: * 実装者:実装者の役割は仕様を満たす実装を提供すること * 検証者:検証者の役割は実装が仕様を満たしていることを確認すること 仕様に欠陥があると、それぞれの利用者に次の悪影響があります: 1. 本来不要だったコミュニケーションコストの発生 - 実装者または検証者が仕様を不審に思えれば実装前に仕様への質問でこれを発見できます。ただし本来不要であったコミュニケーションです 2. 無駄な実装・検証コストの発生 - 検証者が不審に思えなければそのとおりに実装されてしまい、仕様策定者がその実装を触るまで意図どおりでないことに気づけません。運よく出荷前に気づけた場合でも一部または全部の実装が無駄になります 3. 信頼の失墜 - 運悪く出荷前に気付けなければ、エンドユーザーからのお問い合わせ等で発覚することになります。その場合は実装が無駄になるうえ、エンドユーザーからの信頼を損ないかねません ここまでのまとめです: 1. 仕様とはシステムの正しい振る舞いを定める基準です 2. 実装と同様に仕様にも欠陥を考えられます 3. 仕様に欠陥があると余計なコストの発生や信頼の失墜が発生しえます ## 画面仕様とは 本記事で扱う仕様は、GUIアプリケーションの画面の見た目と画面間の遷移にまつわるものです。 この仕様を**画面仕様**と呼び、今回は画面表示仕様(後述)と画面遷移仕様(後述)の2つの組であるとします。 ここで**画面**とは主にGUIの見た目によってグルーピングされたアプリケーションの状態の集合です[^1]。 たとえば一般的なログイン画面を例に画面について考えてみましょう。 ログインという操作には入力フォームの入力状態や認証サーバーとの通信状態が関与します。 これらのありえる組み合わせからなる状態の集合がログイン画面です。 [^1]: 実際には見た目が似ていても別の画面として扱った方がよいこともあります。たとえば一般ユーザーと特権ユーザーが別れているサービスがあったとして、それぞれのログイン画面の見た目を似せることはできますが、そこに到達するまでの経路およびそこから遷移する先の画面が大きく異なる場合、別の画面として扱った方がわかりやすくできるでしょう。 また画面内の状態遷移や画面間の遷移は、UI要素への操作(例:クリックやホバー、スクロール)やサーバーとの通信、時刻などを引き金として起こります。 この引き金のことを**イベント**と呼びます。 ### 画面表示仕様とは 多くの画面は入力フォームやボタンなどのUI要素を決まった位置に配置されています。 画面内のそれぞれの状態ごとに、UI要素をどんな見た目でどの位置に配置するかを指示する仕様が**画面表示仕様**です。 なお個々のUI要素のとりうる状態や見た目、受け付けるイベントについて画面表示仕様とは別にあらかじめ**UI要素仕様**として定めるのが一般的です。 そうすることで複数の画面で共通するUI要素の仕様をそれぞれの画面表示仕様内に重複して記述しなくともよくなります。 本記事ではUI要素の表示や状態遷移にまつわる仕様をUI要素仕様で別に定めているとし、画面表示仕様では(1)UI要素の配置と、(2)画面内のUI要素間の相互作用による振る舞いを記述することとします。 ここで**UI要素間の相互作用**とは、イベントによって複数のUI要素の状態が連動することをいいます[^2]。 たとえば画面に2つのUI要素として入力フォームとテキストが配置されているとします。 そして入力フォームへキーボード入力というUI操作をおこなった結果、その入力が入力規則に違反していればテキストにエラーメッセージが表示され、違反していなければテキストを非表示にされる、という振る舞いはUI要素間の相互作用による典型的な振る舞いです。 [^2]: CSPにおける並行合成を意図しています。 ここで画面表示仕様の例を、UI要素の配置とUI要素間の相互作用による振る舞いの順でみていきましょう。 まずUI要素の配置は、状態によってUI要素の配置が変わらなければ代表的な状態におけるUI要素の配置を示せば十分です。 たとえば次のログイン画面が状態ごとにUI要素の配置が変わらないとすると、このスクリーンショットで十分です。 !(https://cdn-ak.f.st-hatena.com/images/fotolife/s/swet-blog/20250416/20250416150013.png) 状態によってUI要素の配置が異なればそれらの配置の代表的な状態の配置を示せば十分です。 次にUI要素の間の相互作用は、UI要素の配置図に加えて何らかの表現でそれを記述する必要があります。 本事例ではこれを自然言語によって記述しました。 前述の入力フォームとテキストの振る舞いの説明のように記述しています。 なお実装がこの画面表示仕様を満たしたと判断する基準は、大雑把にいうと仕様と実装に同じイベントを与えた結果の見た目が一致することです[^3]。 この画面表示仕様の指示する見た目の集合はUI要素の配置とUI要素ごとの状態から計算できます。 [^3]: 仕様と実装の両方が決定的かつ内部イベントによる遷移を含まない場合です。非決定的な場合はトレース実行後の仕様と実装の両方の見た目は集合となります。このときは実装の見た目の集合が仕様の見た目の集合に包含されるか否かで判定します。内部イベントによる遷移を含む場合は不安定な状態を比較対象から取り除くとよいでしょう。このときクライアントとサーバー間の通信中の画面表示の指示もしたいことがほとんどですから通信によるイベントを隠蔽しない工夫が必要になるでしょう。 ### 画面遷移仕様とは ほとんどのGUIアプリケーションは、ユーザーが画面を遷移しながら操作していくことを意図されています。 このような画面内の状態を点とし、イベントを辺としたラベル付き有向グラフを本記事では**画面遷移仕様**と呼びます。 たとえばログイン画面内のユーザー名・パスワードが未入力な状態`S_0`に正しいユーザー名`Taro`とそのパスワードの組を入力すると`S_Taro`へと遷移する場合、`S_0`と`S_Taro`の間をユーザー`Taro`の正しい認証情報の入力という辺で結びます[^4]。 [^4]: この図はわかりやすさのために素朴に状態とイベントを描いています。実用的には状態変数やガード、事後条件を使ってより見通しのよい図にする方がよいでしょう。 !(https://cdn-ak.f.st-hatena.com/images/fotolife/s/swet-blog/20250414/20250414134448.png) この状態遷移グラフの表現方法はいくつかあります。 上に示した状態遷移図や状態遷移表がその代表的な候補です。 なお実装がこの画面遷移仕様を満たしたと判断する方法については説明が長くなるため割愛します。 本事例では[Communicating Sequential Process (CSP)知りたい方は](https://ja.wikipedia.org/wiki/Communicating_Sequential_Processes)という理論を背景にしているため、気になる方はCSPにおける「詳細化」という概念を調べてください。 CSPおよび詳細化については「[並行システムの検証と実装(磯部 祥尚 著、近代科学社)](https://www.kindaikagaku.co.jp/book_list/detail/9784764904354/)」がわかりやすいです。 ## 画面仕様を機械可読にする方法 今回の取り組みは[Confluence Wikiマークアップ](https://ja.confluence.atlassian.com/doc/confluence-wiki-markup-251003035.html)で記述された既存の仕様書が画面仕様として十分な情報を持っていないところからスタートしました。 そこで既存の仕様書が画面仕様として十分な情報を持てるように画面ごとにそれぞれを次のようなUI要素の配置図(画像左)とUI要素表(画像右)の組を書くようにしました。 ![](https://cdn-ak.f.st-hatena.com/images/fotolife/s/swet-blog/20250414/20250414134453.png) この`Scr.001`は画面IDで`ログイン画面`は画面名です。 本事例の仕様書ではすべての画面に画面名だけでなくIDをつけました。 画面はさまざまな箇所で言及され(例:UI要素表内の画面遷移)、その際に言及先の画面を一意に特定するためのIDが必要になるためです。 画像左はUI要素の配置図で画面表示仕様の一部です。 画像右はUI要素ごとにIDや種類、表示条件、表示内容、インタラクションを記述します。 表示条件や表示内容は画面表示仕様の一部です。 インタラクションは画面遷移仕様の一部です。 このようにUI要素表を組むことで、Confluence Wikiマークアップを解析すれば画面遷移仕様を読み取れるようになります。 また状態遷移図は画像ではなくPlantUMLマクロで描画することによって機械可読にしています。 ## 機械可読な画面仕様への静的検査 機械可読な画面仕様に対して静的な検査が可能です。 今回の事例ではGo言語で6000行弱の静的検査器を実装しました。 この静的検査器は23の検査ルールを持っています。 この検査ルールは、エンジニアの間で事前に洗い出しておいた仕様インスペクション観点がもとになっています。 この事前に洗い出された仕様インスペクション観点は20ほどあり、いくつかを抜粋します: 1. 画面遷移図とUI要素表が整合すること 2. インタラクション可能なUI要素(ボタン・チェックボックス等)にインタラクションの記載があること 3. UI要素の種類がリストのものについてはリスト内のUI要素の並び順の指示が明確であること 4. 動的画像は表示範囲に対して画像の大きさが異なるとき拡大縮小の指示が明確であること 5. ... 今回はこれらの観点のうち8つを自動化できました。 上の例であれば次のような基準で実装されています: 1. PlantUMLで記述された状態遷移図と各画面それぞれのUI要素表のインタラクション列に出現する画面IDを付き合わせた辺の整合性の検査[^5]。画面遷移図内のすべての遷移がUI要素表にあり、かつUI要素表にあるすべての遷移が画面遷移図にあればOK、それ以外はNGと判定 2. 種類がボタンなどの場合にインタラクションの列に記載があればOK、なければNGと判定 3. 種類が動的または静的リストの場合、「順」という文字が表示内容に出現すればOK、なければNGと判定 4. 種類が動的画像の場合、「拡大」「縮小」「見切れ」という文字列のいずれかが出現すればOK、それ以外はNGと判定 5. ... [^5]: 本来は画面間の大雑把な遷移関係ではなく、個々の状態間の遷移関係を特定できるように状態変数やガードや事後条件を機械可読な形式で記述すべきです。しかし機械可読な形式が人間にとって読みやすいとは限らないことから今回の事例でそこまでは踏み込めませんでした。自然言語での説明にとどまっています。 また記載ミスや記載漏れによって欠陥が見逃されないように、補助的な仕様インスペクション観点を追加で15設けています。 たとえば: 1. 重複したIDがなければOK、あればNGと判定 2. TODOという文字列を含まなければOK、含めばNGと判定 3. テキストや画像はそれが静的なのか動的なのかが指示されていればOK、なければNGと判定 4. ... 余談ですが、静的検査器の実装は検査ルール1つにつき、UI要素レイアウト、UI要素表、画面遷移図のいずれかを入力とし、発見された欠陥のリストを返す関数として実装しています。 このようにすることで検査ルール間の独立性が高まり、検査ルールの追加や削除、変更を容易にできます。 また検査対象外の要素は無視するようにした方がよいです。 こうすることで仕様に対して画一的な表現を強制されなくなり、より適した表現(例:画面内の複雑な状態遷移を状態遷移図で表現する、複雑な条件判定をフローチャートで表現する、など)を仕様書へ埋め込めるようになります。 その表現についても検査対象にしたければ、その表現だけを検査する追加のルールをあとで実装すればよいのです。 ## 画面仕様の静的検査を導入した結果 この静的検査器を使うことで、プログラマーに仕様が渡ってくる前に15%の画面から計40の欠陥を発見できました。 また検査器によって欠陥が摘出された後の仕様書への質問は、静的検査器を使っていない他事例の平均的な質問数より少ない傾向にあることがわかっています。 ## 機械可読な画面仕様の課題 非プログラマーによって機械可読な仕様書を保守する場合、機械可読に保つハードルが高いとわかりました。 そのため残念ながら今回実装した静的検査器は継続的な運用には至れませんでした。 プログラマーが仕様書を保守するように役割を変更すれば解決できるかもしれません。 そのように役割を変更する場合、仕様書の保守にかかるプログラマーの工数の捻出が課題になるでしょう。 後述するような機械可読な仕様書による実装の自動生成分でまかなえるかどうかがポイントになりそうです。 ## 機械可読な仕様の応用 画面仕様やその周辺を機械可読にすることで仕様策定を取り巻くプロセスの一部を自動化できることがわかっています。 たとえば本事例では[仕様策定プロセス](https://cdn-ak.f.st-hatena.com/images/fotolife/s/swet-blog/20250416/20250416171648.png)のP3とP4が自動化されました。 P3についてはUI要素の配置図はFigma上のオブジェクトと紐づけることでFigma APIを使い機械的な画像の更新を実現しています[^6]。 [^6]: ConfluenceにFigmaを埋め込めるFigma Widgetが利用可能でしたが動作が重すぎるため利用しませんでした。 また仕様を機械可読にすれば実装の自動生成や検証項目の自動生成をできるかもしれません。 完全な自動生成は難しいかもしれませんが、部分的な自動生成であれば実現しやすいと推測しています。 たとえば典型的なE2Eテストは画面遷移仕様に基づく検証です。 今回紹介した画面遷移仕様にいくつかUI要素のID等のヒントを与えればE2Eテストの自動生成が可能に思えます。 また機械可読な仕様の検索や解釈をLLM向けに支援するMCPサーバーを考えられます。 これは実装や検証項目、テストの自動生成や、それらのプロセスの支援の役に立つかもしれません。 ## まとめ 1. 画面表示と画面遷移を記述する仕様書は機械可読にできる 2. 仕様書が機械可読であれば仕様の静的検査ができる 3. 静的検査によって自身の担当範囲の15%の画面から計40件弱の欠陥を発見した 4. 機械可読な仕様書にはさらなる応用が見込める

子供の命名のために名前を探索するツールを作った

子供が産まれるのに備え子供の名前を探索するツールを作りました。Linux、macOS、Windows で次のように名前の候補を列挙してくれます:

$ name search --space full 山田 --max-length 2 < ./filter.json | tee result.tsv
評点    画数    名前    読み    性別    天格    地格    人格    外格    総格
14      16      丈辞    ジョウジ        男性    吉      大吉    吉      大吉    大大吉
13      21      丈騎    タケキ  男性    吉      大吉    吉      大吉    大吉
...

github.com

名前探索器を開発した背景

12月に第二子が産まれました。第一子は人力で名前を探索したところあまり良い名前が思い浮かばず苦労した経験があります。最終的には友人の手を借りて命名したのですが、今回もまたお世話になるのは申し訳なかったため、システム的な解決を図りました。

開発した名前探索器の紹介

kuniwak/name は、常用漢字と人名用漢字、ひらがな、カタカナからなる文字列の空間から条件に当てはまる名前を探索します。探索の条件として JSON 形式でフィルタを指定します。フィルタによって空間内の文字列それぞれが判定され、フィルタの結果が真なら結果に残り、偽なら結果から取り除かれます。フィルタには次の要素を使えます:

説明 構文
{"true": {}} {"true": {}}
{"false": {}} {"false": {}}
論理積 {"and": [filter...]} {"and": [{"yomiCount": {"rune": "ア", "count": {"equal": 1}}}, {"yomiCount": {"rune": "イ", "count": {"equal": 1}}}]}
論理和 {"or": [filter...]} {"or": [{"yomiCount": {"rune": "ア", "count": {"equal": 1}}}, {"yomiCount": {"rune": "イ", "count": {"equal": 1}}}]}
否定論理 {"not": filter} {"not": {"yomiCount": {"rune": "ア", "count": {"equal": 1}}}}
性別 {"sex": sex} {"sex": "asexual"}
長さ {"length": count} {"length": 3}
読み仮名のモーラ数 {"mora": count} {"mora": {"equal": 3}}
よくある読み仮名 {"commonYomi": {}} {"commonYomi": {}}
画数 {"strokes": count} {"strokes": {"lessThan": 25}}
五格それぞれの最小値 {"minRank": 0-4}4=大大吉, 3=大吉, 2=吉, 1=凶, 0=大凶) {"minRank": 3}
五格の合計値の最小値 {"minTotalRank": byte} {"minTotalRank": 11}
指定した読み仮名の数 {"yomiCount": {"rune": string, "count": count}} {"yomiCount": {"rune": "ア", "count": {"equal": 1}}}
読み仮名のマッチ {"yomi": match} {"yomi": {"equal": "タロウ"}}
漢字のマッチ {"kanji": match} {"kanji": {"equal": "タロウ"}}
指定した漢字の数 {"kanjiCount": {"rune": string, "count": count}} {"kanjiCount": {"rune": "漢", equal": 1}}
count {"equal": byte} or {"lessThan": byte} or {"greaterThan": byte} {"lessThan": 1}
match {"equal": string} or {"startWith": string} or {"endWith": string} {"startWith": "タロ"}
sex "asexual" or "male" or "female" {"sex": "asexual"}

ちなみに私の使ったフィルタは次のとおりです:

{
  "and": [
    {"sex": "male"},
    {"mora": {"equal": 3}},
    {"minRank": 2},
    {"minTotalRank": 11},
    {"commonYomi": {}},
    {"length": {"equal": 2}},
    {
      "or": [
        {
          "and": [
            {"yomiCount": {"rune": "", "count": {"equal": 1}}},
            {"yomiCount": {"rune": "", "count": {"equal": 0}}},
            {"yomiCount": {"rune": "", "count": {"lessThan": 2}}},
            {"yomiCount": {"rune": "", "count": {"equal": 0}}}
          ]
        },
        {
          "and": [
            {"yomiCount": {"rune": "", "count": {"equal": 0}}},
            {"yomiCount": {"rune": "", "count": {"equal": 1}}},
            {"yomiCount": {"rune": "", "count": {"lessThan": 2}}},
            {"yomiCount": {"rune": "", "count": {"equal": 0}}}
          ]
        },
        {
          "and": [
            {"yomiCount": {"rune": "", "count": {"equal": 0}}},
            {"yomiCount": {"rune": "", "count": {"equal": 0}}},
            {"yomiCount": {"rune": "", "count": {"equal": 0}}},
            {"yomiCount": {"rune": "", "count": {"equal": 1}}}
          ]
        }
      ]
    },
    {"kanjiCount": {"rune": "", "count": {"equal": 0}}},
    {"kanjiCount": {"rune": "", "count": {"equal": 0}}},
    {"kanjiCount": {"rune": "", "count": {"equal": 0}}},
    {"kanjiCount": {"rune": "", "count": {"equal": 0}}},
    {"kanjiCount": {"rune": "", "count": {"equal": 0}}},
    {"kanjiCount": {"rune": "", "count": {"equal": 0}}},
    {"kanjiCount": {"rune": "", "count": {"equal": 0}}},
    {"kanjiCount": {"rune": "", "count": {"equal": 0}}}
  ]
}

このフィルタでは最低限の姓名判断のほか、性別の指定、モーラ数の制限、長さの制限、よくある名前の読みとの一致、親と子の名前の同一性の制限(第一子との公平性のため)、望ましくない漢字の除去をしています。姓名判断を条件に加えた理由は、自分の名前を姓名判断にかけてみるときちんと良いものであることがわかり親はちゃんと考えてつけたんだなあということがわかるからです。

このフィルタによって、私の苗字では全空間探索によりおよそ 4500 件ほどの名前の候補が得られます。あとはこれを Google Sheets 等にまとめ、好ましい名前を抽出するとよいでしょう。5000件未満であれば 1h 未満で目視による選別を終えられます。

仕組み

このツールは全空間探索と頻出空間探索の2つの探索モードを持っています:

  • 全空間探索
    • 常用漢字と人名用漢字、ひらがな、カタカナからなる文字列の空間から名前を探索します。低速ですが候補数は多くなります。探索する名前の長さの上限値に2より大きい数を指定すると候補数が爆発的に増えるため非常に低速になります。名前の読みは MeCab によって推定します。
  • 頻出空間探索
    • s1r-J/jinmei-dict を使い、よくある名前とその読みのペアからなる空間から探索します。高速ですが候補数は少なくなります。

この探索範囲から得られた名前の候補について性別やモーラ数を推定します。性別の推定には ENAMDICT/JMnedict を使っています。そうして得られた名前の候補と付加情報をもとにフィルタの条件を満たすもののみを表示します。

読みの推定方法の模索

前述の通り、読みの推定には MeCab を使っています。辞書として NEologd を使うと現代的な読みを出力してくれます。なお MeCab を使う前には常用漢字 + 人名用漢字の漢字ごとに標準的な読みのリストのデカルト積を取っていました。ただあまりにも精度が悪かったために別の方法を模索し neologd/namelti にいきつきました(Namelti の紹介記事)。Namelti は NEologd の辞書を使った MeCab により人名の読みを推定するツールです。

ちなみに Namelti の精度の評価には苦労しました。そのままではビルドできないためパッチをあてる必要がありました。最終的に Docker 上でビルドに成功し(kuniwak/debian-namelti として公開)、その精度がかなりよいことがわかりました。ただ Namelti そのままを使おうとするとプロセスが別になるため IPC が必要となり、この IPC は名前の候補数だけ呼び出されるため呼び出しのオーバーヘッドが大きいです。Namelti の実装を読んだところ、MeCab を素朴に使っているだけということがわかったため MeCab の Go 言語バインディングである shogo82148/go-mecab を使い、IPC を使わず動作するように Go 言語で Namelti を再実装しました。

Windows から cgo で MeCab を呼び出す

一番苦労したのは Windows 環境の cgo 経由で MeCab を呼び出すことです。shogo82148/go-mecab では CGO_LDFLAGSCGO_CFLAGSmecab-config が指示する値で設定するよう指示しています。ただ Windows のコマンドプロンプトや PowerShell から、Bash で実装されている mecab-config そのままを呼び出すことができないこと、また C:\Program Files\MeCab のような空白を含むパス配下にあるヘッダやオブジェクトファイルがあると cgo で意図しない位置でパラメータの区切りと判定されることがハードルとなりました。そこで C# で mecab-config を再実装し、これらの問題を解決しました(kuniwak/mecab-config-windows)。これは dotnet tool install -g MecabConfig でインストールできます。この実装では次のように空白を含むパスでも cgo に渡るパラメータが正しく解釈されるようにエスケープしています:

$ mecab-config --libs
"-LC:\Program Files\MeCab\lib" -lmecab -lstdc++

ただぶっちゃけ、WSL2 で起動する方が百万倍楽だと思います。WSL2 で使ってください。

終わりに

こうして作った名前の探索器から得られた候補から子供に名前をつけられました。

あまりに BLE マクロを Nature Remo 上でデバッグするのが苦行だったので Swift 用の開発環境を整えた

TL;DR

Android nRF Connect の BLE マクロのサブセットを macOS/iOS/... 上で開発する環境を用意しました。

github.com

import Foundation
import BLEMacroEasy

// You can find your iPhone's UUID by running the following command in Terminal:
// $ git clone https://github.com/Kuniwak/swift-ble-macro
// $ cd swift-ble-macro
// $ swift run ble discover
let myIPhoneUUID = UUID(uuidString: "********-****-****-****-************")!
let myMacro = try String(contentsOf: URL(string: "https://ble-macro.kuniwak.com/iphone/battery-level.xml")!)

try await run(macroXMLString: myMacro, on: myIPhoneUUID) { data in
    // This handler is called when every value read from the peripheral.
    let batteryLevel = Int(data[0])
    print("\(batteryLevel)%")
}

macOS/iOS/... 上で BLE マクロを実行する機能のほかマクロの開発を助ける CLI を提供しています。BLE マクロ実行機能については README を見てください。CLI は BLE デバイスのスキャン、マクロのバリデーション、マクロの実行、対話的なマクロ実行をサポートしています:

$ # BLE デバイスをスキャンする
$ ble discover
00000000-0000-0000-0000-000000000000    Example Device 1    -78
11111111-1111-1111-1111-111111111111    Example Device 2    -47
22222222-2222-2222-2222-222222222222    Example Device 3    -54
...

$ # Ctrl+C でスキャンを中断する

$ # BLE マクロを実行する
$ ble run path/to/your/ble-macro.xml --uuid 00000000-0000-0000-0000-000000000000

$ # BLE マクロを対話的に実行する
$ ble repl --uuid 00000000-0000-0000-0000-000000000000
connecting...
connected

(ble) ?
write-command, w, wc    Write to a characteristic without a response
write-descriptor, wd    Write to a descriptor
write-request, req      Write to a characteristic with a response
read, r Read from a characteristic
discovery-service, ds   Discover services
discovery-characteristics, dc   Discover characteristics
discovery-descriptor, dd        Discover descriptors
q, quit Quit the REPL

(ble) dc
180A 2A29 read
180A 2A24 read
D0611E78-BBB4-4591-A5F8-487910AE4366 8667556C-9A37-4C91-84ED-54EE27D90049 write/write/notify/extendedProperties
9FA480E0-4967-4542-9390-D343DC5D04AE AF0BADB1-5B99-43CD-917A-A77BC549E3CC write/write/notify/extendedProperties
180F 2A19 read/notify
1805 2A2B read/notify
1805 2A0F readk

(ble) r 180F 2A19
58

なお開発した BLE マクロは Kuniwak/ble-macro で公開しています。

背景

Nature Remo に BLE(Bluetooth Low Energy)マクロ機能が搭載されました(公式アナウンス)。対応機種(Nature Remo3、Nature Remo mini2 ファミリー)の Remo をお使いなら BLE デバイスを Remo から操作できます。そこで BLE で操作できる我が家の Philips Hue のフルカラー電球x3を Remo から操作すべく BLE マクロの開発を始めました(無駄にフルカラーなのはこの動画を見て淡い憧れがあったため)。

我が家の Philips Hue

Remo のサポートしている BLE マクロは Android nRF Connect のサブセットです。マクロは XML で記述します。例えば Hue を白昼色で点灯させるには次のように記述します:

<macro name="hue-daylight-white" icon="BRIGHTNESS_HIGH">
    <assert-service description="Hue" uuid="932c32bd-0000-47a2-835a-a8d455b859dd">
        <assert-characteristic description="Combined" uuid="932c32bd-0007-47a2-835a-a8d455b859dd">
            <property name="WRITE" requirement="MANDATORY"/>
        </assert-characteristic>
    </assert-service>
    <write description="Set to Daylight White" characteristic-uuid="932c32bd-0007-47a2-835a-a8d455b859dd" service-uuid="932c32bd-0000-47a2-835a-a8d455b859dd" value="0101010201fe0302fa0005020100" type="WRITE_REQUEST" />
</macro>

BLE マクロでは service や characteristic という操作窓口で値の読み書きを指示できます。有名なデバイスならばこれらは 第三者による仕様の推測 である程度詳細がわかります。ただマクロを記述して Remo で動作する状態に持っていくまでにはそれなりに試行錯誤が必要になります。この試行錯誤がとにかく苦行でした

公式曰く Android なら nRF Connect for Mobile アプリで BLE マクロの記録から実行までをサポートしているらしく楽にマクロを開発できるようです。しかし手元には Android 端末がありませんでした。iOS 用に同じ開発者の 同名のアプリ がありますがこれにはマクロ機能がついていません。そのため最初の頃は Remo に BLE マクロを食べさせてみて理由のほとんどわからないエラーに直面して、BLE マクロを勘で書き換えて…というサイクルを回していました。

エラーに詳細な情報がない様子

このサイクルがあまりに不毛であったため Android の nRF Connect for Mobile と同様の機能をもつ環境をなんとか整えられないかと思い Kuniwak/swift-ble-macro を開発しました。nRF Connect for Mobile よりも大幅に機能は少ないですが、Remo で動作する BLE マクロを開発するには十分な機能を持っています。ぜひご活用ください。

なおBLE マクロを開発するには既存のアプリがどんな通信を BLE デバイスとしているかをキャプチャすることも重要です。詳しくは次のブログを読むとやり方がわかるでしょう:

harumi.sakura.ne.jp

BLE マクロを開発した際の苦労

Hue や Switchbot でさまざまな苦労をしたのでここで供養しておきます。

Hue から insufficient encryption エラーが返ってくる

Hue はペアリングしないと操作できないようです。ペアリングしていない central から read/write しようとすると insufficient encryption エラーが発生します。なおペアリングされるのは最初にペアリングされたデバイスのみです(後述する方法でこれを回避できる)。例えば初回に Hue のアプリで接続してしまうとそれ以降のデバイス(e.g. Remo、Google Nest mini)からは操作を受け付けてくれなくなってしまいます。これを回避するにペアリングしてある Hue の公式アプリ > 設定 > 音声アシスタント > Google Home > 検出可能にする、の操作が必要です。この操作によってごく短時間(1minほど?)ペアリングを受け付けてくれるようになります。これは service 0000fe0f-0000-1000-8000-00805f9b34fb の characteristic 97fe6561-2004-4f62-86e9-b71ee2da3d220x01 を書き込むことで再現できます(検出可能にする BLE マクロ)。

ペアリングし損ねた Hue のファクトリリセット方法がわからない

公式アプリや Mac などにペアリングする前に Google Nest mini などにペアリングされてしまうと前述の insufficient encryption 問題によって公式アプリや Mac などから操作ができなくなります。ファクトリリセットは BLE による操作で実現されているため公式アプリがペアリングされていない状況ではファクトリリセットが必要になります。

Hue をファクトリリセットする方法として点灯と消灯のサイクルを回す方法が紹介されています。しかし私の Hue(モデル LCA009、ファームウェアバージョン v1.116.3)ではうまく動きませんでした。代わりに、Hue の電源を壁面のスイッチ等から落としてから再び電源を入れると、ごく短時間 & かなりの近距離であれば Hue の公式アプリで検出可能になることを発見しました。そこからファクトリリセットができました。

Switchbot を操作しようとしても反応しない

Switchbot も BLE によって操作ができるデバイスです。ただこいつもなかなかの曲者です。v6.6 なら Discord の開発者コミュニティに紹介されているマクロ で操作ができるのですが v6.3 では操作を受け付けてくれません。試行錯誤しているうち v6.3 でも characteristic cba20002-224d-11e6-9fb8-0002a5d5c51b に 0x01 を書き込む前に cba20003-224d-11e6-9fb8-0002a5d5c51b で通知の購読をすると爪が動くことを発見しました。ということで v6.3 で動くマクロは以下になります:

<macro name="switchbot-push" icon="PLAY">
    <assert-service description="Switchbot" uuid="cba20d00-224d-11e6-9fb8-0002a5d5c51b">
        <assert-characteristic description="Push" uuid="cba20002-224d-11e6-9fb8-0002a5d5c51b">
            <property name="WRITE" requirement="MANDATORY"/>
        </assert-characteristic>
        <assert-characteristic description="Configuration" uuid="cba20003-224d-11e6-9fb8-0002a5d5c51b">
            <property name="NOTIFY" requirement="MANDATORY"/>
            <assert-cccd />
        </assert-characteristic>
    </assert-service>
    <write-descriptor description="Enable notifications" characteristic-uuid="cba20003-224d-11e6-9fb8-0002a5d5c51b" service-uuid="cba20d00-224d-11e6-9fb8-0002a5d5c51b" uuid="00002902-0000-1000-8000-00805f9b34fb" value="0100" />
    <write description="Write 0x570100" characteristic-uuid="cba20002-224d-11e6-9fb8-0002a5d5c51b" service-uuid="cba20d00-224d-11e6-9fb8-0002a5d5c51b" value="570100" type="WRITE_REQUEST" />
    <wait-for-notification description="Wait for notification" characteristic-uuid="cba20003-224d-11e6-9fb8-0002a5d5c51b" service-uuid="cba20d00-224d-11e6-9fb8-0002a5d5c51b" timeout="5000" />
</macro>

お世話になった製品・文献

最後にお世話になった製品・文献を紹介します:

github.com

github.com