イミュータブルなデータレジストリの設計: Alloy による形式検証

前回に引き続き、data-registryサービスを実装していく。 今回はdatasetVersionの生成ロジック周りを設計する。

はじめに

データを登録するためのサービスが必要ということがわかったので、下の記事で、データ登録サービス: data-registryの構築に必要なセットアップを終え、ハンドラの実装まで完了した。

作業の進捗は下記の通り:

  • API契約を書く
  • ルーティングの動作確認
  • db セットアップ
  • リポジトリ・ハンドラの実装
  • datasetVersion生成ロジックの設計
  • リファクタリング
  • エラーハンドリングの追加

データセットの生成ロジックは、Alloyでモデル検証しつつ設計する。

要求と要件の整理

正常ケースのレスポンスとして、まずUUID形式のversionを返してほしい旨についてはOpenAPIに既に書いた。

{
  "version": "019f8bff-a5b8-7014-bfc8-41561eaac24c"
}

将来的にはもっと有用なレスポンスを返したほうがよさそうだが、ひとまずの仕様としてシンプルなこの動作について、ロジックを設計したい。

ここで設計するのは、この文字列を何のアルゴリズムで得るかという話ではなく、この値がレスポンスとして返るまでにどんな事項を考慮しないといけないかという話。 コンシューマのユースケース支援の観点と、バックエンドサービスとしての堅牢性や処理速度をバランスさせながら考えていく。

要求(Requirements)

仕様を検討するにあたり、data-registryのコンシューマのユースケースを想定し、要求を洗い出したい。 こちらの記事で定義したユビキタス言語を使い、要求を次のように整理する:

ID 説明
R001 同一の登録要求に対して、常に同じ応答を返すこと
R002 同一の版に対する取得要求には、常に同じ内容を返すこと
R003 評価データまたはパラメータセットの登録成功後、その版を取得できること
R004 評価データまたはパラメータセットの登録に失敗した場合、コンシューマが失敗を検知できること
R005 登録要求に対して有限時間内に応答すること
R006 評価データ、パラメータセットまたはシナリオを、通常の利用対象から外せること
R007 評価データまたはパラメータセットを修正した場合でも、修正前の版を取得できること
R008 任意の版を指定して取得できること
R009 外部公開済みの版を訂正した場合でも、訂正前の版を追跡できること
R010 アーカイブと、機密情報などを除去するための物理削除を区別できること
R011 評価データおよびパラメータセットの版の履歴を追跡できること

immutableなサービスにしようと意気込んでいたのでR010は当初入れてなかったが、リソースのリビジョン機能をサポートするAPIでは物理削除が必須という記述を読んだので追加した1

システム要件(System Requirements)

上で整理した要求から、下記のシステム要件が導かれた:

ID 説明
SR001 各評価データおよびパラメータセットは、版を用いて管理されなければならない
SR002 登録後の版の内容を変更してはならない
SR003 評価データまたはパラメータセットを修正するときは、既存の版を更新せず、新しい版を作成しなければならない
SR004 新しい版は、修正元となった版を0個または1個参照しなければならない
SR005 同一の登録要求識別子を持つ登録要求に対しては同一の結果を返し、新たな版を重複して作成してはならない
SR006 登録処理が成功した場合、取得APIから登録された版の内容を取得できなければならない
SR007 版を指定した取得要求には、その版に記録された内容を返さなければならない
SR008 評価データおよびパラメータセットは、それぞれ高々1つの最新版を持たなければならない
SR009 新しい版の登録成功後、最新版は新しい版を指さなければならない
SR010 新しい版が登録されても、過去の版を変更または削除してはならない

これらの要件を出発点とし、データ登録機能を提供するバックエンドサービスとしてあるべき姿に照らして、追加で必要な要件をモデル検証であぶり出していく。

モデル検証にはAlloy 6を利用する。 動作環境は下記の通り:

  • macOS 15.6.1(Apple M1 Max)
  • Alloy Analyzer 6.1.0.20211119T054551

要件を満たすモデルの探索

システム基本構造の定義

data-registryサービスのドメイン構造として、下記を表現することにした:

# Alloy signature 意味
1 Resource ユビキタス言語より
2 AnnualAssessment ユビキタス言語より
3 AssessmentTarget ユビキタス言語より
4 AssessmentDataRevision 評価データの版
5 ParameterSetRevision パラメータセットの版
6 Registry data-registryサービス

その他、Alloyによるシミュレーションを動かすために必要なstutterなどを足して、下記のコードから始めることにする:

commit 46d22f240c3b20868818dcbf6d5b09dba5d161d7
Author: Akira Hayashi <rindrics@gmail.com>
Date:   Thu Jul 30 06:04:25 2026 +0900

    feat: define fundamental model structure

diff --git a/services/data-registry/docs/model.als b/services/data-registry/docs/model.als
new file mode 100644
index 0000000..36a45f9
--- /dev/null
+++ b/services/data-registry/docs/model.als
@@ -0,0 +1,84 @@
+module DataRegistry
+
+/*  ============================================================
+    1. System Requirements
+    ============================================================ */
+// SR001: AssessmentData and ParameterSet, along with their respective versions, must be managed separately.
+// SR002: Each version must be assigned a unique identifier, and the contents must not be modified after registration.
+// SR003: When modifying AssessmentData or ParameterSet, a new version must be created instead of updating the existing version.
+// SR004: A new version must reference exactly zero or one previous version as its source.
+// SR005: For registration requests with identical IdempotencyKey, the same result must be returned without creating duplicate versions.
+// SR006: After successful registration, the registered version must be retrievable via the retrieval API.
+// SR007: Retrieval requests specifying a version must return the contents recorded in that version.
+// SR008: AssessmentData and ParameterSet must each have at most one current version.
+// SR009: After successful registration of a new version, the current version must point to the new version.
+// SR010: Past versions must not be modified or deleted even after a new version is registered.
+
+/*  ============================================================
+    2. Domain
+    ============================================================ */
+
+sig Resource {}
+sig AnnualAssessment {}
+
+sig AssessmentTarget {
+	resource: one Resource,
+	year: one AnnualAssessment
+}
+
+sig AssessmentDataRevision {}
+sig ParameterSetRevision {}
+
+/*  ============================================================
+    3. State
+    ============================================================ */
+
+one sig Registry {
+	var assessmentData: AssessmentTarget -> lone AssessmentDataRevision,
+	var parameterSet: AssessmentTarget -> lone ParameterSetRevision
+}
+
+/*  ============================================================
+    4. Operations
+    ============================================================ */
+
+pred init {
+	no Registry.assessmentData
+	no Registry.parameterSet
+}
+
+pred registerAssessmentData[at: AssessmentTarget, adr: AssessmentDataRevision] {
+	Registry.assessmentData' = Registry.assessmentData + at -> adr
+	Registry.parameterSet' = Registry.parameterSet
+}
+
+pred registerParameterSet[at: AssessmentTarget, psr: ParameterSetRevision] {
+	Registry.assessmentData' = Registry.assessmentData
+	Registry.parameterSet' = Registry.parameterSet + at -> psr
+}
+
+pred stutter {
+	Registry.assessmentData' = Registry.assessmentData
+	Registry.parameterSet' = Registry.parameterSet
+}
+
+pred step {
+	stutter
+	or (some at: AssessmentTarget, adr: AssessmentDataRevision | registerAssessmentData[at, adr])
+	or (some at: AssessmentTarget, psr: ParameterSetRevision | registerParameterSet[at, psr])
+}
+
+fact traces {
+	init
+	always step
+}
+
+/*  ============================================================
+    5. Properties
+    ============================================================ */
+
+/*  ============================================================
+    6. Sanity
+    ============================================================ */
+
+run {} for 2 but 1..3 steps

runを実行してみると、下図のような関係が表示された:

システムの初期状態

システムの初期状態

(Alloyのおさらい)この図が意味していることは下記だ:

  • AssessmentTarger0AnnualAssessmentResource1 からなる評価対象である
  • AssessmentTarger1 も同じ年の評価だが、対象資源は Resource0 である
  • Registry にはまだなにも登録されていない(= エッジが一本もない)
  • AssessmentDataRevision という概念がモデリングされているが使われていない(= データが登録されていない)

registerParameterSet predicateをstepに含めているにも関わらずデータが登録されていないことになっている理由は下記だ:

  • a. step内でregisterParameterSetの遷移がorの一つの選択肢として定義されているだけであり、その遷移が少なくとも一度選択されることは保証されていないため
    • stutterが常に選択可能なら、すべてのステップでstutterだけが選択され、登録が一度も起きないトレースも許される
  • b. Alloyによるケース探索ではインスタンスから生成されがちだから2

逆に、データが登録された状況を見てみたければ下記のようにすれば良い:

  pred step {
- 	stutter
- 	or (some at: AssessmentTarget, adr: AssessmentDataRevision | registerAssessmentData[at, adr])
- 	or (some at: AssessmentTarget, psr: ParameterSetRevision | registerParameterSet[at, psr])
+   some at: AssessmentTarget, adr: AssessmentDataRevision | registerAssessmentData[at, adr]
  }

↑力技でやるならこう。

こちらのほうがスマートか↓

- run {} for 2 but 1..3 steps
+ run { eventually (some Registry.assessmentData) } for 2 but 1..3 steps

実行すると、下図のようにデータが登録された状態を一応見ることができる:

データが登録されたケースの一例

データが登録されたケースの一例

図示のためのこの変更は不要だったので元に戻しておく。

ここから、各要件をAlloyで形式化し、全ての要件が常に満たされるシステム仕様となっているかを検証していく。 「すべての要件が常に」と強めに書いたのは、各要件は私が定義した: つまり調整余地があるもなので、要件全体として整合するようにしたいからだ3

SR001: 版による管理

SR001: 各評価データおよびパラメータセットは、版を用いて管理されなければならない

この要件は下記のように表現できる。

     5. Properties
     ============================================================ */
 
+assert SR001_revision_belongs_to_at_most_one_target {
+    all adr: AssessmentDataRevision |
+        lone Registry.assessmentData.adr
+
+    all psr: ParameterSetRevision |
+        lone Registry.parameterSet.psr
+}
+
+check SR001_revision_belongs_to_at_most_one_target for 2 but 1..3 steps
+
+pred SR001_revisions_can_be_registered {
+	eventually (some at: AssessmentTarget, adr: AssessmentDataRevision,
+		psr: ParameterSetRevision |
+		Registry.assessmentData[at] = adr
+		and Registry.parameterSet[at] = psr)
+}
+
+run SR001_revisions_can_be_registered for 2 but 1 AssessmentTarget, 1..3 steps
+
 /*  ============================================================
     6. Sanity
     ============================================================ */

Execute Allしてみる:

Executing "Check SR001_revision_belongs_to_at_most_one_target for 2 but 1..3 steps"
   Actual scopes: 2 Resource, 2 AnnualAssessment, 2 AssessmentTarget, 2 AssessmentDataRevision, 2 ParameterSetRevision, exactly 1 Registry
   Solver=sat4j Steps=1..3 Bitwidth=4 MaxSeq=2 SkolemDepth=1 Symmetry=20 Mode=batch
   1..3 steps. 1553 vars. 108 primary vars. 2564 clauses. 10ms.
   No counterexample found. Assertion may be valid. 0ms.

Executing "Run SR001_revisions_can_be_registered for 2 but 1..3 steps, 1 AssessmentTarget"
   Actual scopes: 2 Resource, 2 AnnualAssessment, 1 AssessmentTarget, 2 AssessmentDataRevision, 2 ParameterSetRevision, exactly 1 Registry
   Solver=sat4j Steps=1..3 Bitwidth=4 MaxSeq=2 SkolemDepth=1 Symmetry=20 Mode=batch
   1..3 steps. 2479 vars. 198 primary vars. 3967 clauses. 8ms.
   . found. Predicate is consistent. 3ms.

Executing "Run run$3 for 2 but 1..3 steps"
   Actual scopes: 2 Resource, 2 AnnualAssessment, 2 AssessmentTarget, 2 AssessmentDataRevision, 2 ParameterSetRevision, exactly 1 Registry
   Solver=sat4j Steps=1..3 Bitwidth=4 MaxSeq=2 SkolemDepth=1 Symmetry=20 Mode=batch
   1..1 steps. 2769 vars. 225 primary vars. 4415 clauses. 1ms.
   . found. Predicate is consistent. 2ms.

3 commands were executed. The results are:
   #1: No counterexample found. SR001_revision_belongs_to_at_most_one_target may be valid.
   #2: Instance found. SR001_revisions_can_be_registered is consistent.
   #3: Instance found. run$3 is consistent.

↑反例なし、全要件を満たすインスタンスあり(以下 OK ✅ と表現)。

SR002: 不変性

SR002: 登録後の版の内容を変更してはならない

これは下のように表現する:

  year: one AnnualAssessment
 }
 
-sig AssessmentDataRevision {}
-sig ParameterSetRevision {}
+sig AssessmentData {}
+sig ParameterSet {}
+
+sig RevisionId {}
+
+sig AssessmentDataRevision {
+    id: one RevisionId,
+    assessmentData: one AssessmentData
+}
+
+sig ParameterSetRevision {
+    id: one RevisionId,
+    parameterSet: one ParameterSet
+}
+
+fact revisionIdUnique {
+	all r1, r2: AssessmentDataRevision |
+		r1.id = r2.id implies r1 = r2
+	all r1, r2: ParameterSetRevision |
+		r1.id = r2.id implies r1 = r2
+}
 
 /*  ============================================================
     3. State
@@ -96,6 +115,28 @@ pred SR001_revisions_can_be_registered {
 
 run SR001_revisions_can_be_registered for 2 but 1 AssessmentTarget, 1..3 steps
 
+// SR002: The contents of a revision should not be modified after registration.
+// (Automatically satisfied by definition)
+
+pred SR002_registered_revision_contents_remain_available {
+	eventually (some at: AssessmentTarget,
+		adr: AssessmentDataRevision, psr: ParameterSetRevision,
+		data: AssessmentData, params: ParameterSet |
+		Registry.assessmentData[at] = adr
+		and adr.assessmentData = data
+		and Registry.parameterSet[at] = psr
+		and psr.parameterSet = params
+		and after (
+			Registry.assessmentData[at] = adr
+			and adr.assessmentData = data
+			and Registry.parameterSet[at] = psr
+			and psr.parameterSet = params
+		))
+}
+
+run SR002_registered_revision_contents_remain_available
+	for 2 but 1 AssessmentTarget, 1..4 steps
+
 /*  ============================================================
     6. Sanity
     ============================================================ */

↑各Revisionに、非varのフィールドとして one AssessmentDataone ParameterSet を定義することによって保証する(保証といっても実装時の契約ではあるが)。

assertは追加していないが、この変更によって不整合が出ていないかをExecute Allで確認する:

4 commands were executed. The results are:
   #1: No counterexample found. SR001_revision_belongs_to_at_most_one_target may be valid.
   #2: Instance found. SR001_revisions_can_be_registered is consistent.
   #3: Instance found. SR002_registered_revision_contents_remain_available is consistent.
   #4: Instance found. run$4 is consistent.

OK ✅

こちらは全体のsanity check:

現時点のsanity checkを持たすモデル

現時点のsanity checkを持たすモデル

AssessmentDataRevisionParameterSetRevision が導入されたことで、モデルの構造が変わったのがわかる(スペース的に辛いのでノード数を減らしている)。

また、SR002の正常ケースの例をひとつ見てみたいので、明示的にegistered_revision_contents_remain_availableというrunも追加した:

SR002を満たすケースの一例

SR002を満たすケースの一例

…いや待て。 あるAssessmentDataRevisonと、あるParameterSetRevisionが同じRevisionIdを持ちうることになっているのか? それは意図と違う。

確かにちゃんとモデリングできていなかった:

 sig AssessmentData {}
 sig ParameterSet {}
 
-sig RevisionId {}
+abstract sig RevisionId {}
+sig AssessmentDataRevisionId, ParameterSetRevisionId extends RevisionId {}
 
 sig AssessmentDataRevision {
-    id: one RevisionId,
+    id: one AssessmentDataRevisionId,
     assessmentData: one AssessmentData
 }
 
 sig ParameterSetRevision {
-    id: one RevisionId,
+    id: one ParameterSetRevisionId,
     parameterSet: one ParameterSet
 }

もう一度状態を見てみる:

4 commands were executed. The results are:
   #1: No counterexample found. SR001_revision_belongs_to_at_most_one_target may be valid.
   #2: Instance found. SR001_revisions_can_be_registered is consistent.
   #3: Instance found. SR002_registered_cont_available is consistent.
   #4: Instance found. run$4 is consistent.

OK ✅

これで、AssessmentDataRevisonとParameterSetRevisionはそれぞれのRevisionIdを持つようになった。

SR002を満たすケースの一例(修正版)

SR002を満たすケースの一例(修正版)

Alloyの可視化機能に助けられた。

SR003: 不変性その2

SR003 : 評価データまたはパラメータセットを修正するときは、既存の版を更新せず、新しい版を作成しなければならない

 sig AssessmentDataRevisionId, ParameterSetRevisionId extends RevisionId {}
 
+// SR002: Each revision has unique identifier
 sig AssessmentDataRevision {
 id: one AssessmentDataRevisionId,
-	assessmentData: one AssessmentData
+	assessmentData: one AssessmentData,
+	previousSource: lone AssessmentDataRevision
 }
 
 sig ParameterSetRevision {
  id: one ParameterSetRevisionId,
- parameterSet: one ParameterSet
+	parameterSet: one ParameterSet,
+	previousSource: lone ParameterSetRevision
 }
 
-fact revisionIdUnique {
-	all r1, r2: AssessmentDataRevision |
-		r1.id = r2.id implies r1 = r2
-	all r1, r2: ParameterSetRevision |
-		r1.id = r2.id implies r1 = r2
+// SR002: Unique identifier enforcement
+fact revisionIdUniqueAssessmentData {
+	all adr1, adr2: AssessmentDataRevision |
+		adr1.id = adr2.id implies adr1 = adr2
+}
+
+fact revisionIdUniqueParameterSet {
+	all psr1, psr2: ParameterSetRevision |
+		psr1.id = psr2.id implies psr1 = psr2
+}
+
+fact noCyclicSource {
+	all adr: AssessmentDataRevision | adr not in adr.^previousSource
+	all psr: ParameterSetRevision | psr not in psr.^previousSource
 }
 
 /*  ============================================================
@@ -54,8 +66,8 @@ fact revisionIdUnique {
     ============================================================ */
 
 one sig Registry {
-	var assessmentData: AssessmentTarget -> lone AssessmentDataRevision,
-	var parameterSet: AssessmentTarget -> lone ParameterSetRevision
+	var currentAssessmentData: AssessmentTarget -> lone AssessmentDataRevision,
+	var currentParameterSet: AssessmentTarget -> lone ParameterSetRevision
 }
 
 /*  ============================================================
@@ -63,23 +75,33 @@ one sig Registry {
     ============================================================ */
 
 pred init {
-	no Registry.assessmentData
-	no Registry.parameterSet
+	no Registry.currentAssessmentData
+	no Registry.currentParameterSet
 }
 
 pred registerAssessmentData[at: AssessmentTarget, adr: AssessmentDataRevision] {
-	Registry.assessmentData' = Registry.assessmentData + at -> adr
-	Registry.parameterSet' = Registry.parameterSet
+	(no Registry.currentAssessmentData[at] and no adr.previousSource)
+	or (
+		some Registry.currentAssessmentData[at]
+		and adr.previousSource = Registry.currentAssessmentData[at]
+	)
+	Registry.currentAssessmentData' = Registry.currentAssessmentData ++ at -> adr
+	Registry.currentParameterSet' = Registry.currentParameterSet
 }
 
 pred registerParameterSet[at: AssessmentTarget, psr: ParameterSetRevision] {
-	Registry.assessmentData' = Registry.assessmentData
-	Registry.parameterSet' = Registry.parameterSet + at -> psr
+	(no Registry.currentParameterSet[at] and no psr.previousSource)
+	or (
+		some Registry.currentParameterSet[at]
+		and psr.previousSource = Registry.currentParameterSet[at]
+	)
+	Registry.currentAssessmentData' = Registry.currentAssessmentData
+	Registry.currentParameterSet' = Registry.currentParameterSet ++ at -> psr
 }
 
 pred stutter {
-	Registry.assessmentData' = Registry.assessmentData
-	Registry.parameterSet' = Registry.parameterSet
+	Registry.currentAssessmentData' = Registry.currentAssessmentData
+	Registry.currentParameterSet' = Registry.currentParameterSet
 }
 
 pred step {
@@ -97,40 +119,32 @@ fact traces {
     5. Properties
     ============================================================ */
 
-assert SR001_revision_belongs_to_at_most_one_target {
-    all adr: AssessmentDataRevision |
-        lone Registry.assessmentData.adr
-
-    all psr: ParameterSetRevision |
-        lone Registry.parameterSet.psr
-}
-
-check SR001_revision_belongs_to_at_most_one_target for 2 but 1..3 steps
+// SR001: At most one current version per AssessmentTarget
+// (Automatically satisfied by Registry definition: lone AssessmentDataRevision)
 
 pred SR001_revisions_can_be_registered {
 	eventually (some at: AssessmentTarget, adr: AssessmentDataRevision,
 		psr: ParameterSetRevision |
-		Registry.assessmentData[at] = adr
-		and Registry.parameterSet[at] = psr)
+		Registry.currentAssessmentData[at] = adr
+		and Registry.currentParameterSet[at] = psr)
 }
 
 run SR001_revisions_can_be_registered for 2 but 1 AssessmentTarget, 1..3 steps
 
 // SR002: The contents of a revision should not be modified after registration.
 // (Automatically satisfied by definition)
-
 pred SR002_registered_cont_available {
 	eventually (some at: AssessmentTarget,
 		adr: AssessmentDataRevision, psr: ParameterSetRevision,
 		data: AssessmentData, params: ParameterSet |
-		Registry.assessmentData[at] = adr
+		Registry.currentAssessmentData[at] = adr
 		and adr.assessmentData = data
-		and Registry.parameterSet[at] = psr
+		and Registry.currentParameterSet[at] = psr
 		and psr.parameterSet = params
 		and after (
-			Registry.assessmentData[at] = adr
+			Registry.currentAssessmentData[at] = adr
 			and adr.assessmentData = data
-			and Registry.parameterSet[at] = psr
+			and Registry.currentParameterSet[at] = psr
 			and psr.parameterSet = params
 		))
 }
@@ -138,6 +152,46 @@ pred SR002_registered_cont_available {
 run SR002_registered_cont_available
 	for 2 but 1 AssessmentTarget, 1..4 steps
 
+// SR003: When modifying, a new version must be created instead of updating the existing version
+assert SR003_new_version_on_change {
+	always (
+		(all at: AssessmentTarget |
+			(some Registry.currentAssessmentData[at] and some Registry.currentAssessmentData'[at])
+			implies Registry.currentAssessmentData[at] = Registry.currentAssessmentData'[at]
+				or Registry.currentAssessmentData'[at] in (Registry.currentAssessmentData[at]).~previousSource)
+		and
+		(all at: AssessmentTarget |
+			(some Registry.currentParameterSet[at] and some Registry.currentParameterSet'[at])
+			implies Registry.currentParameterSet[at] = Registry.currentParameterSet'[at]
+				or Registry.currentParameterSet'[at] in (Registry.currentParameterSet[at]).~previousSource)
+	)
+}
+
+check SR003_new_version_on_change for 2 but 1..3 steps
+
+pred SR003_assessment_data_can_be_changed_with_new_revision {
+	eventually (some at: AssessmentTarget,
+		source, newRevision: AssessmentDataRevision |
+		Registry.currentAssessmentData[at] = source
+		and source.assessmentData != newRevision.assessmentData
+		and newRevision.previousSource = source
+		and registerAssessmentData[at, newRevision])
+}
+
+pred SR003_parameter_set_can_be_changed_with_new_revision {
+	eventually (some at: AssessmentTarget,
+		source, newRevision: ParameterSetRevision |
+		Registry.currentParameterSet[at] = source
+		and source.parameterSet != newRevision.parameterSet
+		and newRevision.previousSource = source
+		and registerParameterSet[at, newRevision])
+}
+
+run SR003_assessment_data_can_be_changed_with_new_revision
+	for 2 but 1 AssessmentTarget, 1..3 steps
+run SR003_parameter_set_can_be_changed_with_new_revision
+	for 2 but 1 AssessmentTarget, 1..3 steps
+
 /*  ============================================================
     6. Sanity
     ============================================================ */

assert SR003_new_version_on_change の下記の部分は「current revisionが同じまま(変わらない)、または next revisionが current revision から派生している」を意味している:

implies Registry.currentAssessmentData[at] = Registry.currentAssessmentData'[at]
				or Registry.currentAssessmentData'[at] in (Registry.currentAssessmentData[at]).~previousSource)

Execute All:

6 commands were executed. The results are:
   #1: Instance found. SR001_revisions_can_be_registered is consistent.
   #2: Instance found. SR002_registered_cont_available is consistent.
   #3: No counterexample found. SR003_new_version_on_change may be valid.
   #4: Instance found. SR003_assessment_data_can_be_changed_with_new_revision is consistent.
   #5: Instance found. SR003_parameter_set_can_be_changed_with_new_revision is consistent.
   #6: Instance found. run$6 is consistent.

OK ✅

AssessmentDataが変わった際には、別のRevisionとなるようになっている:

SR003を満たすケースの一例

SR003を満たすケースの一例

(末尾の数字が交差しているが、これは自動付与されたものなので気にしなくて良いはず。)

SR004: 版の参照規則

SR004: 新しい版は、修正元となった版を0個または1個参照しなければならない

これは、previousSource: lone AssessmentDataRevisionlone によって、既に定義レベルで保証されている。 一応、状態を見てみたいのでrunを追加した:

 	for 2 but 1 AssessmentTarget, 1..3 steps
 
+// SR004: Each revision can reference exactly zero or one previous version
+// (Enforced by previousSource: lone field definition - no explicit assertion needed)
+
+pred SR004_root_and_derived_revisions_can_exist {
+	some rootAdr, derivedAdr: AssessmentDataRevision,
+		rootPsr, derivedPsr: ParameterSetRevision |
+		rootAdr != derivedAdr
+		and no rootAdr.previousSource
+		and derivedAdr.previousSource = rootAdr
+		and rootPsr != derivedPsr
+		and no rootPsr.previousSource
+		and derivedPsr.previousSource = rootPsr
+}
+
+run SR004_root_and_derived_revisions_can_exist
+	for 4 but
+	exactly 2 AssessmentDataRevision,
+	exactly 2 ParameterSetRevision,
+	exactly 2 AssessmentDataRevisionId,
+	exactly 2 ParameterSetRevisionId,
+	1..2 steps
+
 /*  ============================================================
     6. Sanity
     ============================================================ */

一例を見てみる:

SR004を満たすケースの一例

SR004を満たすケースの一例

なるほど、データの内容が同じでも親が違う状況が示されていて、この場合には違う版として扱われるらしい。 少し意図と違うが、これについては次のSR005で考える。

SR005: 冪等性

SR005: 同一の登録要求識別子を持つ登録要求に対しては同一の結果を返し、新たな版を重複して作成してはならない

先ほど述べた問題にも関係する冪等性。 下記のようにモデリングしてみる:

 sig AssessmentDataRevisionId, ParameterSetRevisionId extends RevisionId {}
 
+sig HashKey {}
+
+abstract sig Status {}
+one sig Success, Failure extends Status {}
+
+sig Response {
+	revision: lone (AssessmentDataRevision + ParameterSetRevision),
+	status: one Status
+}
+
 // SR002: Each revision has unique identifier
 sig AssessmentDataRevision {
 	id: one AssessmentDataRevisionId,
@@ -67,7 +77,9 @@ fact noCyclicSource {
 
 one sig Registry {
 	var currentAssessmentData: AssessmentTarget -> lone AssessmentDataRevision,
-	var currentParameterSet: AssessmentTarget -> lone ParameterSetRevision
+	var currentParameterSet: AssessmentTarget -> lone ParameterSetRevision,
+	// SR005: Track responses by HashKey (idempotency key) for idempotency verification
+	var responseLog: HashKey -> lone Response
 }
 
 /*  ============================================================
@@ -77,37 +89,80 @@ one sig Registry {
 pred init {
 	no Registry.currentAssessmentData
 	no Registry.currentParameterSet
+	no Registry.responseLog
 }
 
-pred registerAssessmentData[at: AssessmentTarget, adr: AssessmentDataRevision] {
-	(no Registry.currentAssessmentData[at] and no adr.previousSource)
+// SR005: Log response by idempotency key
+pred registerAssessmentData[
+	at: AssessmentTarget,
+	adr: AssessmentDataRevision,
+	idempotencyKey: HashKey
+] {
+	(
+		no Registry.responseLog[idempotencyKey]
+		and (
+			(no Registry.currentAssessmentData[at] and no adr.previousSource)
+			or (
+				some Registry.currentAssessmentData[at]
+				and adr.previousSource = Registry.currentAssessmentData[at]
+			)
+		)
+		and Registry.currentAssessmentData' = Registry.currentAssessmentData ++ at -> adr
+		and Registry.currentParameterSet' = Registry.currentParameterSet
+		and some r: Response |
+			r.revision = adr
+			and r.status = Success
+			and Registry.responseLog' = Registry.responseLog + idempotencyKey -> r
+	)
 	or (
-		some Registry.currentAssessmentData[at]
-		and adr.previousSource = Registry.currentAssessmentData[at]
+		some Registry.responseLog[idempotencyKey]
+		and Registry.currentAssessmentData' = Registry.currentAssessmentData
+		and Registry.currentParameterSet' = Registry.currentParameterSet
+		and Registry.responseLog' = Registry.responseLog
 	)
-	Registry.currentAssessmentData' = Registry.currentAssessmentData ++ at -> adr
-	Registry.currentParameterSet' = Registry.currentParameterSet
 }
 
-pred registerParameterSet[at: AssessmentTarget, psr: ParameterSetRevision] {
-	(no Registry.currentParameterSet[at] and no psr.previousSource)
+pred registerParameterSet[
+	at: AssessmentTarget,
+	psr: ParameterSetRevision,
+	idempotencyKey: HashKey
+] {
+	(
+		no Registry.responseLog[idempotencyKey]
+		and (
+			(no Registry.currentParameterSet[at] and no psr.previousSource)
+			or (
+				some Registry.currentParameterSet[at]
+				and psr.previousSource = Registry.currentParameterSet[at]
+			)
+		)
+		and Registry.currentAssessmentData' = Registry.currentAssessmentData
+		and Registry.currentParameterSet' = Registry.currentParameterSet ++ at -> psr
+		and some r: Response |
+			r.revision = psr
+			and r.status = Success
+			and Registry.responseLog' = Registry.responseLog + idempotencyKey -> r
+	)
 	or (
-		some Registry.currentParameterSet[at]
-		and psr.previousSource = Registry.currentParameterSet[at]
+		some Registry.responseLog[idempotencyKey]
+		and Registry.currentAssessmentData' = Registry.currentAssessmentData
+		and Registry.currentParameterSet' = Registry.currentParameterSet
+		and Registry.responseLog' = Registry.responseLog
 	)
-	Registry.currentAssessmentData' = Registry.currentAssessmentData
-	Registry.currentParameterSet' = Registry.currentParameterSet ++ at -> psr
 }
 
 pred stutter {
 	Registry.currentAssessmentData' = Registry.currentAssessmentData
 	Registry.currentParameterSet' = Registry.currentParameterSet
+	Registry.responseLog' = Registry.responseLog
 }
 
 pred step {
 	stutter
-	or (some at: AssessmentTarget, adr: AssessmentDataRevision | registerAssessmentData[at, adr])
-	or (some at: AssessmentTarget, psr: ParameterSetRevision | registerParameterSet[at, psr])
+	or (some at: AssessmentTarget, adr: AssessmentDataRevision, hk: HashKey |
+		registerAssessmentData[at, adr, hk])
+	or (some at: AssessmentTarget, psr: ParameterSetRevision, hk: HashKey |
+		registerParameterSet[at, psr, hk])
 }
 
 fact traces {
@@ -171,20 +226,22 @@ check SR003_new_version_on_change for 2 but 1..3 steps
 
 pred SR003_assessment_update {
 	eventually (some at: AssessmentTarget,
-		source, newRevision: AssessmentDataRevision |
+		source, newRevision: AssessmentDataRevision,
+		hk: HashKey |
 		Registry.currentAssessmentData[at] = source
 		and source.assessmentData != newRevision.assessmentData
 		and newRevision.previousSource = source
-		and registerAssessmentData[at, newRevision])
+		and registerAssessmentData[at, newRevision, hk])
 }
 
 pred SR003_parameter_update {
 	eventually (some at: AssessmentTarget,
-		source, newRevision: ParameterSetRevision |
+		source, newRevision: ParameterSetRevision,
+		hk: HashKey |
 		Registry.currentParameterSet[at] = source
 		and source.parameterSet != newRevision.parameterSet
 		and newRevision.previousSource = source
-		and registerParameterSet[at, newRevision])
+		and registerParameterSet[at, newRevision, hk])
 }
 
 run SR003_assessment_update
@@ -214,6 +271,51 @@ run SR004_revision_chain
 	exactly 2 ParameterSetRevisionId,
 	1..2 steps
 
+// SR005: Idempotency - same HashKey (idempotency key) returns same response
+assert SR005_idempotency {
+	always (
+		all hk: HashKey |
+			(some Registry.responseLog[hk]) implies always Registry.responseLog[hk] = Registry.responseLog'[hk]
+	)
+}
+
+check SR005_idempotency for 2 but 1..3 steps
+
+pred SR005_assessment_data_retry_returns_same_response {
+	some at: AssessmentTarget, adr: AssessmentDataRevision,
+		hk: HashKey, r: Response |
+		eventually (
+			no Registry.responseLog[hk]
+			and registerAssessmentData[at, adr, hk]
+			and Registry.responseLog'[hk] = r
+			and after (
+				Registry.responseLog[hk] = r
+				and registerAssessmentData[at, adr, hk]
+				and Registry.responseLog'[hk] = r
+			)
+		)
+}
+
+pred SR005_parameter_set_retry_returns_same_response {
+	some at: AssessmentTarget, psr: ParameterSetRevision,
+		hk: HashKey, r: Response |
+		eventually (
+			no Registry.responseLog[hk]
+			and registerParameterSet[at, psr, hk]
+			and Registry.responseLog'[hk] = r
+			and after (
+				Registry.responseLog[hk] = r
+				and registerParameterSet[at, psr, hk]
+				and Registry.responseLog'[hk] = r
+			)
+		)
+}
+
+run SR005_assessment_data_retry_returns_same_response
+	for 2 but 1 AssessmentTarget, 1..3 steps
+run SR005_parameter_set_retry_returns_same_response
+	for 2 but 1 AssessmentTarget, 1..3 steps
+
 /*  ============================================================
     6. Sanity
     ============================================================ */

var responseLog: HashKey -> lone Response の右辺がoneではなくloneなのは、データが未登録の状況を表現するため。 実装時における冪等性保証を助けるために、リクエストにはidempotency keyを使う想定。 このkeyの計算には、親とする版のIDも含める必要がある。 始めはAssessmentDataなどの内容のみからkeyを生成して、同一の内容の登録リクエストは拒否してしまおうと考えていたが、そうすると履歴の復元: 内容が同じデータの再登録の時に不都合が出そう。

ともあれ、同一性の定義については詳細設計に回すとして、ひとまず次に進む。

Execute All:

10 commands were executed. The results are:
   #1: Instance found. SR001_registration is consistent.
   #2: Instance found. SR002_content_stability is consistent.
   #3: No counterexample found. SR003_new_version_on_change may be valid.
   #4: Instance found. SR003_assessment_update is consistent.
   #5: Instance found. SR003_parameter_update is consistent.
   #6: Instance found. SR004_revision_chain is consistent.
   #7: No counterexample found. SR005_idempotency may be valid.
   #8: Instance found. SR005_assessment_retry is consistent.
   #9: Instance found. SR005_parameter_retry is consistent.
   #10: Instance found. run$10 is consistent.

↑OK :white_check_mark

SR005を満たすケースの一例

SR005を満たすケースの一例

↑同一のHashKeyとともに何度リクエストしても、Revisionは変化しない様子が示されている。

SR006: 取得可能性

SR006: 登録処理が成功した場合、取得APIから登録された版の内容を取得できなければならない

この要件は「成功レスポンスがあるならRevisionが存在する」として表現する:

 one sig Registry {
 	var currentAssessmentData: AssessmentTarget -> lone AssessmentDataRevision,
 	var currentParameterSet: AssessmentTarget -> lone ParameterSetRevision,
+	var registeredAssessmentData: AssessmentTarget -> set AssessmentDataRevision,
+	var registeredParameterSet: AssessmentTarget -> set ParameterSetRevision,
 	// SR005: Track responses by HashKey (idempotency key) for idempotency verification
 	var responseLog: HashKey -> lone Response
 }
@@ -89,6 +91,8 @@ one sig Registry {
 pred init {
 	no Registry.currentAssessmentData
 	no Registry.currentParameterSet
+	no Registry.registeredAssessmentData
+	no Registry.registeredParameterSet
 	no Registry.responseLog
 }
 
@@ -109,6 +113,8 @@ pred registerAssessmentData[
 		)
 		and Registry.currentAssessmentData' = Registry.currentAssessmentData ++ at -> adr
 		and Registry.currentParameterSet' = Registry.currentParameterSet
+		and Registry.registeredAssessmentData' = Registry.registeredAssessmentData + at -> adr
+		and Registry.registeredParameterSet' = Registry.registeredParameterSet
 		and some r: Response |
 			r.revision = adr
 			and r.status = Success
@@ -118,6 +124,8 @@ pred registerAssessmentData[
 		some Registry.responseLog[idempotencyKey]
 		and Registry.currentAssessmentData' = Registry.currentAssessmentData
 		and Registry.currentParameterSet' = Registry.currentParameterSet
+		and Registry.registeredAssessmentData' = Registry.registeredAssessmentData
+		and Registry.registeredParameterSet' = Registry.registeredParameterSet
 		and Registry.responseLog' = Registry.responseLog
 	)
 }
@@ -138,6 +146,8 @@ pred registerParameterSet[
 		)
 		and Registry.currentAssessmentData' = Registry.currentAssessmentData
 		and Registry.currentParameterSet' = Registry.currentParameterSet ++ at -> psr
+		and Registry.registeredAssessmentData' = Registry.registeredAssessmentData
+		and Registry.registeredParameterSet' = Registry.registeredParameterSet + at -> psr
 		and some r: Response |
 			r.revision = psr
 			and r.status = Success
@@ -147,6 +157,8 @@ pred registerParameterSet[
 		some Registry.responseLog[idempotencyKey]
 		and Registry.currentAssessmentData' = Registry.currentAssessmentData
 		and Registry.currentParameterSet' = Registry.currentParameterSet
+		and Registry.registeredAssessmentData' = Registry.registeredAssessmentData
+		and Registry.registeredParameterSet' = Registry.registeredParameterSet
 		and Registry.responseLog' = Registry.responseLog
 	)
 }
@@ -154,6 +166,8 @@ pred registerParameterSet[
 pred stutter {
 	Registry.currentAssessmentData' = Registry.currentAssessmentData
 	Registry.currentParameterSet' = Registry.currentParameterSet
+	Registry.registeredAssessmentData' = Registry.registeredAssessmentData
+	Registry.registeredParameterSet' = Registry.registeredParameterSet
 	Registry.responseLog' = Registry.responseLog
 }
 
@@ -316,6 +330,47 @@ run SR005_assessment_retry
 run SR005_parameter_retry
 	for 2 but 1 AssessmentTarget, 1..3 steps
 
+// SR006: After successful registration, the registered version must be retrievable
+// If response is logged as successful, the revision must remain registered
+assert SR006_retrievability {
+	always (
+		all hk: HashKey, r: Response |
+			(some Registry.responseLog[hk] and Registry.responseLog[hk] = r and r.status = Success)
+			implies (
+				(some at: AssessmentTarget, adr: AssessmentDataRevision |
+					r.revision = adr and adr in Registry.registeredAssessmentData[at])
+				or
+				(some at: AssessmentTarget, psr: ParameterSetRevision |
+					r.revision = psr and psr in Registry.registeredParameterSet[at])
+			)
+	)
+}
+
+check SR006_retrievability for 2 but 1..3 steps
+
+pred SR006_registered_assessment_data_can_be_retrieved {
+	eventually (some at: AssessmentTarget, adr: AssessmentDataRevision,
+		hk: HashKey, r: Response |
+		Registry.responseLog[hk] = r
+		and r.status = Success
+		and r.revision = adr
+		and adr in Registry.registeredAssessmentData[at])
+}
+
+pred SR006_registered_parameter_set_can_be_retrieved {
+	eventually (some at: AssessmentTarget, psr: ParameterSetRevision,
+		hk: HashKey, r: Response |
+		Registry.responseLog[hk] = r
+		and r.status = Success
+		and r.revision = psr
+		and psr in Registry.registeredParameterSet[at])
+}
+
+run SR006_registered_assessment_data_can_be_retrieved
+	for 2 but 1 AssessmentTarget, 1..3 steps
+run SR006_registered_parameter_set_can_be_retrieved
+	for 2 but 1 AssessmentTarget, 1..3 steps
+
 /*  ============================================================
     6. Sanity
     ============================================================ */

Execute All:

13 commands were executed. The results are:
   #1: Instance found. SR001_registration is consistent.
   #2: Instance found. SR002_content_stability is consistent.
   #3: No counterexample found. SR003_new_version_on_change may be valid.
   #4: Instance found. SR003_assessment_update is consistent.
   #5: Instance found. SR003_parameter_update is consistent.
   #6: Instance found. SR004_revision_chain is consistent.
   #7: No counterexample found. SR005_idempotency may be valid.
   #8: Instance found. SR005_assessment_retry is consistent.
   #9: Instance found. SR005_parameter_retry is consistent.
   #10: No counterexample found. SR006_retrievability may be valid.
   #11: Instance found. SR006_assessment_retrieval is consistent.
   #12: Instance found. SR006_parameter_retrieval is consistent.
   #13: Instance found. run$13 is consistent.

↑OK ✅

SR006の例示結果はSR005と変わらなかった。

SR007: 取得可能性その2

R007: 版を指定した取得要求には、その版に記録された内容を返さなければならない

これは原理的にはSR001SR002で既に満たされている:

 run SR006_parameter_retrieval
 	for 2 but 1 AssessmentTarget, 1..3 steps
 
+// SR007: Retrieval requests return the contents recorded in that version
+// (Automatically satisfied because revision content is immutable)
+
+pred SR007_assessment_data_can_be_retrieved_by_revision {
+	eventually (some at: AssessmentTarget, adr: AssessmentDataRevision,
+		data: AssessmentData |
+		adr in Registry.registeredAssessmentData[at]
+		and data = adr.assessmentData)
+}
+
+pred SR007_parameter_set_can_be_retrieved_by_revision {
+	eventually (some at: AssessmentTarget, psr: ParameterSetRevision,
+		params: ParameterSet |
+		psr in Registry.registeredParameterSet[at]
+		and params = psr.parameterSet)
+}
+
+run SR007_assessment_data_can_be_retrieved_by_revision
+	for 2 but 1 AssessmentTarget, 1..3 steps
+run SR007_parameter_set_can_be_retrieved_by_revision
+	for 2 but 1 AssessmentTarget, 1..3 steps
+
 /*  ============================================================
     6. Sanity
     ============================================================ */

実際の挙動がこれを満たすようにするのは実装時の責任。

15 commands were executed. The results are:
   #1: Instance found. SR001_registration is consistent.
   #2: Instance found. SR002_content_stability is consistent.
   #3: No counterexample found. SR003_new_version_on_change may be valid.
   #4: Instance found. SR003_assessment_update is consistent.
   #5: Instance found. SR003_parameter_update is consistent.
   #6: Instance found. SR004_revision_chain is consistent.
   #7: No counterexample found. SR005_idempotency may be valid.
   #8: Instance found. SR005_assessment_retry is consistent.
   #9: Instance found. SR005_parameter_retry is consistent.
   #10: No counterexample found. SR006_retrievability may be valid.
   #11: Instance found. SR006_assessment_retrieval is consistent.
   #12: Instance found. SR006_parameter_retrieval is consistent.
   #13: Instance found. SR007_assessment_by_revision is consistent.
   #14: Instance found. SR007_parameter_by_revision is consistent.
   #15: Instance found. run$15 is consistent.

↑OK ✅

今回の変更も、モデルに構造的な変化はなかった。

SR008: 最新版の参照

SR008 評価データおよびパラメータセットは、それぞれ高々1つの最新版を持たなければならない

この要件は、既にRegistryの設計レベルで保証されている。 保証しているのは、下記current-lone参照だ:

one sig Registry {
  one sig Registry {
	var currentAssessmentData: AssessmentTarget -> lone AssessmentDataRevision,
	var currentParameterSet: AssessmentTarget -> lone ParameterSetRevision,
	var registeredAssessmentData: AssessmentTarget -> set AssessmentDataRevision,
	var registeredParameterSet: AssessmentTarget -> set ParameterSetRevision,
	// SR005: Track responses by HashKey (idempotency key) for idempotency verification
	var responseLog: HashKey -> lone Response
}

一応、ここでも正常ケースを例示しておく:

 run SR007_parameter_by_revision
 	for 2 but 1 AssessmentTarget, 1..3 steps
 
+// SR008: AssessmentData and ParameterSet each have at most one current revision
+// (Enforced by the lone multiplicity of the current revision fields)
+
+pred SR008_assessment_data_has_one_current_among_registered_revisions {
+	eventually (some at: AssessmentTarget,
+		disj previousRevision, currentRevision: AssessmentDataRevision |
+		previousRevision + currentRevision in Registry.registeredAssessmentData[at]
+		and Registry.currentAssessmentData[at] = currentRevision)
+}
+
+pred SR008_parameter_set_has_one_current_among_registered_revisions {
+	eventually (some at: AssessmentTarget,
+		disj previousRevision, currentRevision: ParameterSetRevision |
+		previousRevision + currentRevision in Registry.registeredParameterSet[at]
+		and Registry.currentParameterSet[at] = currentRevision)
+}
+
+run SR008_assessment_data_has_one_current_among_registered_revisions
+	for 3 but 1 AssessmentTarget, 1..3 steps
+run SR008_parameter_set_has_one_current_among_registered_revisions
+	for 3 but 1 AssessmentTarget, 1..3 steps
+
 /*  ============================================================
     6. Sanity
     ============================================================ */

Execute All:

17 commands were executed. The results are:
   #1: Instance found. SR001_registration is consistent.
   #2: Instance found. SR002_content_stability is consistent.
   #3: No counterexample found. SR003_new_version_on_change may be valid.
   #4: Instance found. SR003_assessment_update is consistent.
   #5: Instance found. SR003_parameter_update is consistent.
   #6: Instance found. SR004_revision_chain is consistent.
   #7: No counterexample found. SR005_idempotency may be valid.
   #8: Instance found. SR005_assessment_retry is consistent.
   #9: Instance found. SR005_parameter_retry is consistent.
   #10: No counterexample found. SR006_retrievability may be valid.
   #11: Instance found. SR006_assessment_retrieval is consistent.
   #12: Instance found. SR006_parameter_retrieval is consistent.
   #13: Instance found. SR007_assessment_by_revision is consistent.
   #14: Instance found. SR007_parameter_by_revision is consistent.
   #15: Instance found. SR008_assessment_current is consistent.
   #16: Instance found. SR008_parameter_current is consistent.
   #17: Instance found. run$17 is consistent.

↑ OK ✅

正常系を図示する:

SR008を満たすケースの一例

SR008を満たすケースの一例

↑この例では、RegistryはAssessmentDataRevision0を最新版として参照している。

SR009: 登録成功後の最新版の参照

SR009: 新しい版の登録成功後、最新版は新しい版を指さなければならない

この要件は、registerAssessmentData などのpredの定義で保証済み。 具体的には下記の部分だ:

pred registerAssessmentData[at: AssessmentTarget, assessmentData: AssessmentData, idempotencyKey: HashKey] {
	some adr: AssessmentDataRevision |
		idempotencyKey = adr.id and
		adr.assessmentData = assessmentData and
		Registry.currentAssessmentData' = Registry.currentAssessmentData + at -> adr and
		Registry.currentParameterSet' = Registry.currentParameterSet and
		some r: Response |
			r.revision = adr and
			r.status = Success and
			Registry.responseLog' = Registry.responseLog + idempotencyKey -> r
}

この定義は、例えば下記のような状況を表現している:

  1. AssessmentTarget2026年度カタクチ太平洋currentAssessmentDatasomeRevisionHashに紐づくデータである状態
  2. 新しい登録リクエストが届く: 「2026年度カタクチ太平洋新しいassessmentData新しいidempotency key
  3. newRevisionHashと紐づいたcurrentAssessmentData2026年度のカタクチ太平洋の最新版として保持される

ここでも正常系を明示的に例示してみる:

 run SR008_parameter_current
 	for 3 but 1 AssessmentTarget, 1..3 steps
 
+// SR009: Successful registration makes the new revision current
+
+pred SR009_assessment_advance {
+	eventually (some at: AssessmentTarget,
+		disj previousRevision, newRevision: AssessmentDataRevision,
+		hk: HashKey |
+		Registry.currentAssessmentData[at] = previousRevision
+		and newRevision.previousSource = previousRevision
+		and no Registry.responseLog[hk]
+		and registerAssessmentData[at, newRevision, hk]
+		and Registry.currentAssessmentData'[at] = newRevision)
+}
+
+pred SR009_parameter_advance {
+	eventually (some at: AssessmentTarget,
+		disj previousRevision, newRevision: ParameterSetRevision,
+		hk: HashKey |
+		Registry.currentParameterSet[at] = previousRevision
+		and newRevision.previousSource = previousRevision
+		and no Registry.responseLog[hk]
+		and registerParameterSet[at, newRevision, hk]
+		and Registry.currentParameterSet'[at] = newRevision)
+}
+
+run SR009_assessment_advance
+	for 3 but 1 AssessmentTarget, 1..3 steps
+run SR009_parameter_advance
+	for 3 but 1 AssessmentTarget, 1..3 steps
+
 /*  ============================================================
     6. Sanity
     ============================================================ */

Execute Allはall green ✅

SR009を満たすケースの一例

SR009を満たすケースの一例

こちらも、モデルに構造的な変化は見られないように思う。

SR010: 不変性その3

SR010: 新しい版が登録されても、過去の版を変更または削除してはならない

こちらは実装の責任なのでモデル検証はスキップ。

モデル全体

ここまでの検証で、Alloyのコード全体は最終的に下記のようになった:

module DataRegistry

/*  ============================================================
    1. System Requirements
    ============================================================ */
// SR001: Each AssessmentData and ParameterSet should be managed through revisions.
// SR002: The contents of a revision should not be modified after registration.
// SR003: When modifying AssessmentData or ParameterSet, a new version must be created instead of updating the existing version.
// SR004: A new version must reference exactly zero or one previous version as its source.
// SR005: For registration requests with identical IdempotencyKey, the same result must be returned without creating duplicate versions.
// SR006: After successful registration, the registered version must be retrievable via the retrieval API.
// SR007: Retrieval requests specifying a version must return the contents recorded in that version.
// SR008: AssessmentData and ParameterSet must each have at most one current version.
// SR009: After successful registration of a new version, the current version must point to the new version.
// SR010: Past versions must not be modified or deleted even after a new version is registered.

/*  ============================================================
    2. Domain
    ============================================================ */

sig Resource {}
sig AnnualAssessment {}

sig AssessmentTarget {
	resource: one Resource,
    year: one AnnualAssessment
}

sig AssessmentData {}
sig ParameterSet {}

abstract sig RevisionId {}
sig AssessmentDataRevisionId, ParameterSetRevisionId extends RevisionId {}

sig HashKey {}

abstract sig Status {}
one sig Success, Failure extends Status {}

sig Response {
	revision: lone (AssessmentDataRevision + ParameterSetRevision),
	status: one Status
}

// SR002: Each revision has unique identifier
sig AssessmentDataRevision {
	id: one AssessmentDataRevisionId,
	assessmentData: one AssessmentData,
	previousSource: lone AssessmentDataRevision
}

sig ParameterSetRevision {
	id: one ParameterSetRevisionId,
	parameterSet: one ParameterSet,
	previousSource: lone ParameterSetRevision
}

// SR002: Unique identifier enforcement
fact revisionIdUniqueAssessmentData {
	all adr1, adr2: AssessmentDataRevision |
		adr1.id = adr2.id implies adr1 = adr2
}

fact revisionIdUniqueParameterSet {
	all psr1, psr2: ParameterSetRevision |
		psr1.id = psr2.id implies psr1 = psr2
}

fact noCyclicSource {
	all adr: AssessmentDataRevision | adr not in adr.^previousSource
	all psr: ParameterSetRevision | psr not in psr.^previousSource
}

/*  ============================================================
    3. State
    ============================================================ */

one sig Registry {
	var currentAssessmentData: AssessmentTarget -> lone AssessmentDataRevision,
	var currentParameterSet: AssessmentTarget -> lone ParameterSetRevision,
	var registeredAssessmentData: AssessmentTarget -> set AssessmentDataRevision,
	var registeredParameterSet: AssessmentTarget -> set ParameterSetRevision,
	// SR005: Track responses by HashKey (idempotency key) for idempotency verification
	var responseLog: HashKey -> lone Response
}

/*  ============================================================
    4. Operations
    ============================================================ */

pred init {
	no Registry.currentAssessmentData
	no Registry.currentParameterSet
	no Registry.registeredAssessmentData
	no Registry.registeredParameterSet
	no Registry.responseLog
}

// SR005: Log response by idempotency key
pred registerAssessmentData[
	at: AssessmentTarget,
	adr: AssessmentDataRevision,
	idempotencyKey: HashKey
] {
	(
		no Registry.responseLog[idempotencyKey]
		and (
	(no Registry.currentAssessmentData[at] and no adr.previousSource)
	or (
		some Registry.currentAssessmentData[at]
		and adr.previousSource = Registry.currentAssessmentData[at]
	)
		)
		and Registry.currentAssessmentData' = Registry.currentAssessmentData ++ at -> adr
		and Registry.currentParameterSet' = Registry.currentParameterSet
		and Registry.registeredAssessmentData' = Registry.registeredAssessmentData + at -> adr
		and Registry.registeredParameterSet' = Registry.registeredParameterSet
		and some r: Response |
			r.revision = adr
			and r.status = Success
			and Registry.responseLog' = Registry.responseLog + idempotencyKey -> r
	)
	or (
		some Registry.responseLog[idempotencyKey]
		and Registry.currentAssessmentData' = Registry.currentAssessmentData
		and Registry.currentParameterSet' = Registry.currentParameterSet
		and Registry.registeredAssessmentData' = Registry.registeredAssessmentData
		and Registry.registeredParameterSet' = Registry.registeredParameterSet
		and Registry.responseLog' = Registry.responseLog
	)
}

pred registerParameterSet[
	at: AssessmentTarget,
	psr: ParameterSetRevision,
	idempotencyKey: HashKey
] {
	(
		no Registry.responseLog[idempotencyKey]
		and (
	(no Registry.currentParameterSet[at] and no psr.previousSource)
	or (
		some Registry.currentParameterSet[at]
		and psr.previousSource = Registry.currentParameterSet[at]
	)
		)
		and Registry.currentAssessmentData' = Registry.currentAssessmentData
		and Registry.currentParameterSet' = Registry.currentParameterSet ++ at -> psr
		and Registry.registeredAssessmentData' = Registry.registeredAssessmentData
		and Registry.registeredParameterSet' = Registry.registeredParameterSet + at -> psr
		and some r: Response |
			r.revision = psr
			and r.status = Success
			and Registry.responseLog' = Registry.responseLog + idempotencyKey -> r
	)
	or (
		some Registry.responseLog[idempotencyKey]
		and Registry.currentAssessmentData' = Registry.currentAssessmentData
		and Registry.currentParameterSet' = Registry.currentParameterSet
		and Registry.registeredAssessmentData' = Registry.registeredAssessmentData
		and Registry.registeredParameterSet' = Registry.registeredParameterSet
		and Registry.responseLog' = Registry.responseLog
	)
}

pred stutter {
	Registry.currentAssessmentData' = Registry.currentAssessmentData
	Registry.currentParameterSet' = Registry.currentParameterSet
	Registry.registeredAssessmentData' = Registry.registeredAssessmentData
	Registry.registeredParameterSet' = Registry.registeredParameterSet
	Registry.responseLog' = Registry.responseLog
}

pred step {
	stutter
	or (some at: AssessmentTarget, adr: AssessmentDataRevision, hk: HashKey |
		registerAssessmentData[at, adr, hk])
	or (some at: AssessmentTarget, psr: ParameterSetRevision, hk: HashKey |
		registerParameterSet[at, psr, hk])
}

fact traces {
	init
	always step
}

/*  ============================================================
    5. Properties
    ============================================================ */

// SR001: At most one current version per AssessmentTarget
// (Automatically satisfied by Registry definition: lone AssessmentDataRevision)

pred SR001_registration {
	eventually (some at: AssessmentTarget, adr: AssessmentDataRevision,
		psr: ParameterSetRevision |
		Registry.currentAssessmentData[at] = adr
		and Registry.currentParameterSet[at] = psr)
}

run SR001_registration for 2 but 1 AssessmentTarget, 1..3 steps

// SR002: The contents of a revision should not be modified after registration.
// (Automatically satisfied by definition)
pred SR002_content_stability {
	eventually (some at: AssessmentTarget,
		adr: AssessmentDataRevision, psr: ParameterSetRevision,
		data: AssessmentData, params: ParameterSet |
		Registry.currentAssessmentData[at] = adr
		and adr.assessmentData = data
		and Registry.currentParameterSet[at] = psr
		and psr.parameterSet = params
		and after (
			Registry.currentAssessmentData[at] = adr
			and adr.assessmentData = data
			and Registry.currentParameterSet[at] = psr
			and psr.parameterSet = params
		))
}

run SR002_content_stability
	for 2 but 1 AssessmentTarget, 1..4 steps

// SR003: When modifying, a new version must be created instead of updating the existing version
assert SR003_new_version_on_change {
	always (
		(all at: AssessmentTarget |
			(some Registry.currentAssessmentData[at] and some Registry.currentAssessmentData'[at])
			implies Registry.currentAssessmentData[at] = Registry.currentAssessmentData'[at]
				or Registry.currentAssessmentData'[at] in (Registry.currentAssessmentData[at]).~previousSource)
		and
		(all at: AssessmentTarget |
			(some Registry.currentParameterSet[at] and some Registry.currentParameterSet'[at])
			implies Registry.currentParameterSet[at] = Registry.currentParameterSet'[at]
				or Registry.currentParameterSet'[at] in (Registry.currentParameterSet[at]).~previousSource)
	)
}

check SR003_new_version_on_change for 2 but 1..3 steps

pred SR003_assessment_update {
	eventually (some at: AssessmentTarget,
		source, newRevision: AssessmentDataRevision,
		hk: HashKey |
		Registry.currentAssessmentData[at] = source
		and source.assessmentData != newRevision.assessmentData
		and newRevision.previousSource = source
		and registerAssessmentData[at, newRevision, hk])
}

pred SR003_parameter_update {
	eventually (some at: AssessmentTarget,
		source, newRevision: ParameterSetRevision,
		hk: HashKey |
		Registry.currentParameterSet[at] = source
		and source.parameterSet != newRevision.parameterSet
		and newRevision.previousSource = source
		and registerParameterSet[at, newRevision, hk])
}

run SR003_assessment_update
	for 2 but 1 AssessmentTarget, 1..3 steps
run SR003_parameter_update
	for 2 but 1 AssessmentTarget, 1..3 steps

// SR004: Each revision can reference exactly zero or one previous version
// (Enforced by previousSource: lone field definition - no explicit assertion needed)

pred SR004_revision_chain {
	some rootAdr, derivedAdr: AssessmentDataRevision,
		rootPsr, derivedPsr: ParameterSetRevision |
		rootAdr != derivedAdr
		and no rootAdr.previousSource
		and derivedAdr.previousSource = rootAdr
		and rootPsr != derivedPsr
		and no rootPsr.previousSource
		and derivedPsr.previousSource = rootPsr
}

run SR004_revision_chain
	for 4 but
	exactly 2 AssessmentDataRevision,
	exactly 2 ParameterSetRevision,
	exactly 2 AssessmentDataRevisionId,
	exactly 2 ParameterSetRevisionId,
	1..2 steps

// SR005: Idempotency - same HashKey (idempotency key) returns same response
assert SR005_idempotency {
	always (
		all hk: HashKey |
			(some Registry.responseLog[hk]) implies always Registry.responseLog[hk] = Registry.responseLog'[hk]
	)
}

check SR005_idempotency for 2 but 1..3 steps

pred SR005_assessment_retry {
	some at: AssessmentTarget, adr: AssessmentDataRevision,
		hk: HashKey, r: Response |
		eventually (
			no Registry.responseLog[hk]
			and registerAssessmentData[at, adr, hk]
			and Registry.responseLog'[hk] = r
			and after (
				Registry.responseLog[hk] = r
				and registerAssessmentData[at, adr, hk]
				and Registry.responseLog'[hk] = r
			)
		)
}

pred SR005_parameter_retry {
	some at: AssessmentTarget, psr: ParameterSetRevision,
		hk: HashKey, r: Response |
		eventually (
			no Registry.responseLog[hk]
			and registerParameterSet[at, psr, hk]
			and Registry.responseLog'[hk] = r
			and after (
				Registry.responseLog[hk] = r
				and registerParameterSet[at, psr, hk]
				and Registry.responseLog'[hk] = r
			)
		)
}

run SR005_assessment_retry
	for 2 but 1 AssessmentTarget, 1..3 steps
run SR005_parameter_retry
	for 2 but 1 AssessmentTarget, 1..3 steps

// SR006: After successful registration, the registered version must be retrievable
// If response is logged as successful, the revision must remain registered
assert SR006_retrievability {
	always (
		all hk: HashKey, r: Response |
			(some Registry.responseLog[hk] and Registry.responseLog[hk] = r and r.status = Success)
			implies (
				(some at: AssessmentTarget, adr: AssessmentDataRevision |
					r.revision = adr and adr in Registry.registeredAssessmentData[at])
				or
				(some at: AssessmentTarget, psr: ParameterSetRevision |
					r.revision = psr and psr in Registry.registeredParameterSet[at])
			)
	)
}

check SR006_retrievability for 2 but 1..3 steps

pred SR006_assessment_retrieval {
	eventually (some at: AssessmentTarget, adr: AssessmentDataRevision,
		hk: HashKey, r: Response |
		Registry.responseLog[hk] = r
		and r.status = Success
		and r.revision = adr
		and adr in Registry.registeredAssessmentData[at])
}

pred SR006_parameter_retrieval {
	eventually (some at: AssessmentTarget, psr: ParameterSetRevision,
		hk: HashKey, r: Response |
		Registry.responseLog[hk] = r
		and r.status = Success
		and r.revision = psr
		and psr in Registry.registeredParameterSet[at])
}

run SR006_assessment_retrieval
	for 2 but 1 AssessmentTarget, 1..3 steps
run SR006_parameter_retrieval
	for 2 but 1 AssessmentTarget, 1..3 steps

// SR007: Retrieval requests return the contents recorded in that version
// (Automatically satisfied because revision content is immutable)

pred SR007_assessment_by_revision {
	eventually (some at: AssessmentTarget, adr: AssessmentDataRevision,
		data: AssessmentData |
		adr in Registry.registeredAssessmentData[at]
		and data = adr.assessmentData)
}

pred SR007_parameter_by_revision {
	eventually (some at: AssessmentTarget, psr: ParameterSetRevision,
		params: ParameterSet |
		psr in Registry.registeredParameterSet[at]
		and params = psr.parameterSet)
}

run SR007_assessment_by_revision
	for 2 but 1 AssessmentTarget, 1..3 steps
run SR007_parameter_by_revision
	for 2 but 1 AssessmentTarget, 1..3 steps

// SR008: AssessmentData and ParameterSet each have at most one current revision
// (Enforced by the lone multiplicity of the current revision fields)

pred SR008_assessment_current {
	eventually (some at: AssessmentTarget,
		disj previousRevision, currentRevision: AssessmentDataRevision |
		previousRevision + currentRevision in Registry.registeredAssessmentData[at]
		and Registry.currentAssessmentData[at] = currentRevision)
}

pred SR008_parameter_current {
	eventually (some at: AssessmentTarget,
		disj previousRevision, currentRevision: ParameterSetRevision |
		previousRevision + currentRevision in Registry.registeredParameterSet[at]
		and Registry.currentParameterSet[at] = currentRevision)
}

run SR008_assessment_current
	for 3 but 1 AssessmentTarget, 1..3 steps
run SR008_parameter_current
	for 3 but 1 AssessmentTarget, 1..3 steps

// SR009: Successful registration makes the new revision current

pred SR009_assessment_advance {
	eventually (some at: AssessmentTarget,
		disj previousRevision, newRevision: AssessmentDataRevision,
		hk: HashKey |
		Registry.currentAssessmentData[at] = previousRevision
		and newRevision.previousSource = previousRevision
		and no Registry.responseLog[hk]
		and registerAssessmentData[at, newRevision, hk]
		and Registry.currentAssessmentData'[at] = newRevision)
}

pred SR009_parameter_advance {
	eventually (some at: AssessmentTarget,
		disj previousRevision, newRevision: ParameterSetRevision,
		hk: HashKey |
		Registry.currentParameterSet[at] = previousRevision
		and newRevision.previousSource = previousRevision
		and no Registry.responseLog[hk]
		and registerParameterSet[at, newRevision, hk]
		and Registry.currentParameterSet'[at] = newRevision)
}

run SR009_assessment_advance
	for 3 but 1 AssessmentTarget, 1..3 steps
run SR009_parameter_advance
	for 3 but 1 AssessmentTarget, 1..3 steps

// SR010: Past versions must not be modified or deleted even after a new version is registered
// (Automatically satisfied by operation design: registerAssessmentData/registerParameterSet only extend
//  the mapping with new versions; they never modify or remove existing entries)

pred SR010_assessment_history {
	eventually (some at: AssessmentTarget,
		disj previousRevision, newRevision: AssessmentDataRevision,
		hk: HashKey |
		Registry.currentAssessmentData[at] = previousRevision
		and previousRevision in Registry.registeredAssessmentData[at]
		and newRevision.previousSource = previousRevision
		and no Registry.responseLog[hk]
		and registerAssessmentData[at, newRevision, hk]
		and previousRevision + newRevision in Registry.registeredAssessmentData'[at]
		and Registry.currentAssessmentData'[at] = newRevision)
}

pred SR010_parameter_history {
	eventually (some at: AssessmentTarget,
		disj previousRevision, newRevision: ParameterSetRevision,
		hk: HashKey |
		Registry.currentParameterSet[at] = previousRevision
		and previousRevision in Registry.registeredParameterSet[at]
		and newRevision.previousSource = previousRevision
		and no Registry.responseLog[hk]
		and registerParameterSet[at, newRevision, hk]
		and previousRevision + newRevision in Registry.registeredParameterSet'[at]
		and Registry.currentParameterSet'[at] = newRevision)
}

run SR010_assessment_history
	for 3 but 1 AssessmentTarget, 1..3 steps
run SR010_parameter_history
	for 3 but 1 AssessmentTarget, 1..3 steps

/*  ============================================================
    6. Sanity
    ============================================================ */

run {} for 2 but 1..3 steps

Execute Allの結果:

21 commands were executed. The results are:
   #1: Instance found. SR001_registration is consistent.
   #2: Instance found. SR002_content_stability is consistent.
   #3: No counterexample found. SR003_new_version_on_change may be valid.
   #4: Instance found. SR003_assessment_update is consistent.
   #5: Instance found. SR003_parameter_update is consistent.
   #6: Instance found. SR004_revision_chain is consistent.
   #7: No counterexample found. SR005_idempotency may be valid.
   #8: Instance found. SR005_assessment_retry is consistent.
   #9: Instance found. SR005_parameter_retry is consistent.
   #10: No counterexample found. SR006_retrievability may be valid.
   #11: Instance found. SR006_assessment_retrieval is consistent.
   #12: Instance found. SR006_parameter_retrieval is consistent.
   #13: Instance found. SR007_assessment_by_revision is consistent.
   #14: Instance found. SR007_parameter_by_revision is consistent.
   #15: Instance found. SR008_assessment_current is consistent.
   #16: Instance found. SR008_parameter_current is consistent.
   #17: Instance found. SR009_assessment_advance is consistent.
   #18: Instance found. SR009_parameter_advance is consistent.
   #19: Instance found. SR010_assessment_history is consistent.
   #20: Instance found. SR010_parameter_history is consistent.
   #21: Instance found. run$21 is consistent.

↑ OK ✅

学んだこと

今回は反省点が多い。 途中から雲行きの怪しさを感じていた。 まとめると下記だ:

  • 要件定義が雑すぎた
  • スコープを決めていなかった
  • 進め方の戦略がなかった

要件定義が雑すぎた

一つ一つの要件の検証にかなり時間をかけているわけだが、そもそもそれぞれの要件自体の吟味が足りていないかった。要件をかっちり決める手法として、昔(確か2022年くらい)この本を読んだことでICONIXを知ったのだが、当時は重厚すぎるような印象を持っていた。

今回設計しているdata-registryは、構想当時にはとてもシンプルなものという見立てだったが、少し設計してみると考えるべきことがどんどん出てくる(なお今、この設計でも観点が全然足りていないだろう)。

スコープを決めていなかった

今回は合計で10要件を検証したように書いているが、実は途中でどんどん要件が増えてしまい、最終的に20を超えてしまっていた。 収集がつかなくなりそうだったので、記事としてはいったん10の要件で一区切りとすることにした。 ちなみに区切りとするために捨てた機能は「アーカイブ」「物理削除」「外部公開」の三つ。 非機能要件としては並行性への対処を無視している。 時間の関係から、そもそも「機能」「スコープ」について言及していない内容になってしまったが、このやり方は是非真似しないでほしい(?)。

進め方の戦略がなかった

これは上記二つの反省点と同列ではなく、より上位の観点にある。 Alloyで検証することが決まっているような形で走り始めてしまったため、Alloyでの検証に向かないような要件が出てきたときに管理できなくなった。 今後の管理を考えると、サービスごとにユースケース・要求・要件にIDをふってドキュメント化しておくといいかもしれない。 そろそろカンバンを作るか。

・・・

反省点は多いし、設計にまだ穴もあるだろうが、そろそろ動くものを作りたい。 いや、その前に冪等性の定義とDB設計があるか。

作業の進捗としてはこんな感じだろうか:

  • API契約を書く
  • ルーティングの動作確認
  • db セットアップ
  • リポジトリ・ハンドラの実装
  • datasetVersion生成ロジックの設計
  • API基本設計
  • Revision生成ロジックの設計
  • データベース設計
  • リファクタリング
  • エラーハンドリングの追加

つづく

参考情報

  • Geewax (2022)『APIデザインパターン』マイナビ出版

  1. たまたま『APIデザインパターン』を読んでいたのだった。第28章「リソースリビジョン」がドンピシャの内容だった ↩︎

  2. 『抽象によるソフトウェア設計』でも解説されており、特異な例を見つけるのに有用とされている。それはたしかにそうかも。ちなみに、このAlloyの挙動を決めているのは解析エンジンの挙動。私の環境ではSAT4Jソルバーだったので、SAT4Jのコードを見てみたところ、この部分によって、解決フェーズでまずfalseが試されそうだった ↩︎

  3. Alloyによる探索が有限スコープなのは実用観点で脇に置いておく ↩︎