何を検査するか
chobo check は帳簿を読んで、二つのことを言います。一つは診断で、エラーがあれば帳簿はそこで止まり、警告なら止まりません。もう一つは、エラーの無い帳簿についての、振替の操作ごとの拒否されうる理由です。帳簿を呼んだときに起きることを言うときは、そこに至る操作の列を見つけ、参照インタプリタで流して確かめてから言います。そうした操作の列が見つからなければ、何も言いません。
操作ごとの拒否されうる理由
ポイントの例は、会員ごとのポイントを 0 から 100 万までに保ちます。
$ chobo check examples/points/points.ja.book --lang ja
examples/points/points.ja.book: ok
付与.hold 拒否されうる理由: 上限超え(ポイント(会員) at most 1000000)、key_conflict、already_refused
付与.post 拒否されうる理由: key_conflict、already_voided、over_hold、no_such_hold
付与.void 拒否されうる理由: already_posted、no_such_hold
利用.hold 拒否されうる理由: ポイント不足(ポイント(会員) at least 0)、key_conflict、already_refused
利用.post 拒否されうる理由: key_conflict、already_voided、expired、over_hold、no_such_hold
利用.void 拒否されうる理由: already_posted、expired、no_such_hold
失効.do 拒否されうる理由: ポイント不足(ポイント(会員) at least 0)、key_conflict、already_refused
…
並びうる理由は帳簿から決まり、次のものがあります。
- 移動が取る勘定の下限と、移動が入れる勘定の上限に付けた理由。
key_conflict:同じキーの二つの呼び出しが、額か、キーに入っていないほかの引数で違いうるとき。already_refused:境界が呼び出しを拒否しうるとき。拒否された呼び出しも、キーを使ったことになります。same_account:移動が、同じ種類の二つの勘定のあいだのとき。- 仮押さえの状態から来る理由。
どれも、そこに至る操作を検査が見つけて流したときにだけ並びます。購入で付くポイントは期限が切れないので、付与 が expired で拒否されることはありません。付与は入れるだけなので、ポイント不足 で拒否されることもありません。
--format json は、その操作の列を、シナリオのステップとして、それぞれの結果(result)と一緒に出します。すでに 100 万ポイントある会員にポイントを付与すると、上限超え で拒否されます。
$ chobo check examples/points/points.ja.book --format json --lang ja
{
"v": 2,
"files": [
{
"file": "examples/points/points.ja.book",
"ok": true,
"diagnostics": [],
"report": [
{
"kind": "付与",
"op": "hold",
"refusals": [
{
"name": "上限超え",
"because": "bound",
"spends_key": true,
"example": [
{
"op": "hold",
"kind": "付与",
"args": {
"購入": "購入-1",
"会員": "会員-2",
"数": 1000000
},
"result": "done"
},
{
"op": "post",
"kind": "付与",
"args": {
"購入": "購入-1"
},
"result": "done"
},
{
"op": "hold",
"kind": "付与",
"args": {
"購入": "購入-3",
"会員": "会員-2",
"数": 1
},
"result": "refused",
"reason": "上限超え"
}
],
…
この一覧は、その操作を走らせるワークフローのタスクが、エラーとして宣言するものです。chobo api も、同じ例と一緒にほかのツールへ渡します(形式、英語)。探し方には限りがあります。額には 1、2、3、5、10、100、1000 を試し、取る勘定を満たすために、その勘定へ入れる振替を三つ先までたどります。額が 0 のときにしか起きない理由や、それより深いところでしか起きない理由は並びません。
診断
診断は、コードと行と、何がいけないかを言います。エラーがあると帳簿はそこで止まり、直すまでは、走らせることもビルドすることも描くこともできません。読めない行があると、そこから先は推測になるので、ほかの検査もしません。エラーの多くは、帳簿の書き方についてのものです(tests/fixtures/キー.book)。
エラー[E030]: キー.book:9:10: 振替 `入荷` に `key` の行がありません。どの振替にも冪等のキーが要ります。同じ注文で一度だけ動かすなら `key 注文` のように、一度だけにしたい引数を並べてください
9 | transfer 入荷(伝票: string, sku: string, 数: 個)
エラー[E031]: キー.book:12:11: 額の引数 `数` はキーに入れられません。入れると、額だけが違う二つの呼び出しが、別々の振替として両方通ってしまいます
12 | key 注文, 数
エラー[E030]: キー.book:15:7: キーの `伝票番号` は振替 `返品` の引数にありません
15 | key 伝票番号
| コード | 見つけるもの |
|---|---|
| E001〜E005 | 読めない行、宣言されていない名前や二度宣言された名前、勘定に渡す引数の数、型 |
| E010、E011、E014 | 単位をまたぐ移動、単位で書けない数、お金でない単位に書いた税込・税抜 |
| E012、E013 | どう呼んでも元と先が同じ勘定になる移動、移動の無い振替 |
| E020〜E023 | 境界の無い帳簿の中の勘定、境界のある外の勘定、どの残高も収まらない境界、理由の名前の無い境界 |
| E030、E031 | キーの無い振替、キーに入った額 |
| E040、E041 | 終わり方を書いていない仮押さえ、範囲の外の有効期限 |
四つの警告は、帳簿を呼んだときに起きることについてのもので、そこに至る操作の列が付きます。検査はその列を流して確かめています。一つ目は、移動の順序のせいで拒否される振替です。二つの移動を合わせれば足りる場合でも拒否されます(tests/fixtures/順序.book)。
警告[W103]: 順序.book:15:3: 1 つ目の移動が 店の売上(店) から取るのは、2 つ目の移動が 店の売上(店) へ入れるより前です。そのとき 店の売上(店) が足りないと、二つの移動を合わせれば足りる場合でも 売上不足 で拒否されます
15 | move 手数料 from 店の売上(店) to 手数料収入
そうなる例:
1 売上.do(注文: 注文-1, 店: 店-2, 代金: 1, 手数料: 1) 売上不足 で拒否される(1 つ目の移動が 店の売上(店-2) から 1 を取ろうとしたときの残高は、確定 0、出ていく仮押さえ 0)
ヒント: 店の売上(店) へ入れる移動を先に書いてください
仮押さえでは、一つの移動が入れる分は入ってくる仮押さえになり、同じ仮押さえのほかの移動はそれを取れません。移動をどの順に書いても同じです。
警告[W104]: 仮押さえの順序.book:14:3: 仮押さえでは、1 つ目の移動が 預り(注文) へ入れる額は入ってくる仮押さえになり、2 つ目の移動が 預り(注文) から取るときに数えられません。預り(注文) にはじめから足りるだけの残高が無ければ、預り不足 で拒否されます
14 | move 代金 from 預り(注文) to 店への支払
そうなる例:
1 決済.hold(注文: 注文-1, 代金: 1) 預り不足 で拒否される(2 つ目の移動が 預り(注文-1) から 1 を取ろうとしたときの残高は、確定 0、出ていく仮押さえ 0、入ってくる仮押さえ 1)
ヒント: 取る分をはじめから 預り(注文) に入れておくか、確定のあとで取る別の振替にしてください
| コード | 見つけるもの |
|---|---|
| W101 | 残高がたまる一方の勘定。そこから取る移動が無い |
| W102 | いつも拒否される振替。取る勘定へ入れるものが無い |
| W103 | 移動の順序のせいで拒否される振替 |
| W104 | 同じ仮押さえのほかの移動が入れる分に頼る移動 |
| W105、W106 | 使われない宣言、効かない境界 |
| W107 | --diff-base で比べたリビジョンから無くなった勘定や振替(下で説明します) |
| W901 | ファイルの文字列かコメントに書かれた鍵。診断が言うのは鍵の種類と頭の部分と長さだけで、鍵そのものは出さない |
出力先にかかわるエラーが四つあり、こちらは chobo build が出します。TigerBeetle の一つのリクエストに入らない長さの振替のチェーン(E060。253 件で止まります)、PostgreSQL の識別子の 63 バイトを超える名前(E061)、MySQL の系統のテーブルと制約の名前にできない帳簿の名前(E062)、Rust のターゲットで、Rust の識別子に使えない字を持つ名前(E063)です。そのまま流すと、PostgreSQL は長い名前を黙って切り詰めてしまいます。日本語の字は UTF-8 で一字 3 バイトなので、英語の名前より早くこの長さに届きます。MySQL の名前は字数で数えて 64 字までなので、帳簿の名前は 51 字まで使えますが、基本多言語面の外の文字(𠮷 など)は、警告だけで ? に書き換えられてしまいます。PostgreSQL 向けにだけビルドする帳簿が、TigerBeetle や MySQL の制限で止まることはありません。Rust のクライアントは帳簿の名前をそのまま構造体のフィールドや型の名前にするので、十進でない数字(² など)や、先頭の結合文字は書けません(tests/fixtures/識別子.book)。
エラー[E063]: 識別子.book:11:37: 引数の名前 `面積m²` に ²(U+00B2)があります。Rust の識別子には、この字を使えません
11 | transfer 増築(許可: string, 建物: string, 面積m²: 平米)
診断コードには、全部で 32 種類のコードの一つずつについて、いつ出るか、どう直すか、それを出す最小の帳簿を、chobo explain --all の出力のとおりに載せています。一つだけ読むなら chobo explain <コード> です。このサイトに載せた chobo の診断は、どれも chobo のテストの golden ファイルから取っています。golden ファイルは、テスト用の帳簿ごとに、英語と日本語の診断を、検査の出力のとおりに持っています。
データベースと合わなくなる変更
帳簿のバージョンは勘定や振替の中身に入らないので、バージョンを上げても、データベースにある残高と仮押さえはそのまま続きます。chobo check --diff-base <リビジョン> は、帳簿を git のそのリビジョンの帳簿と比べ、データベースがついていけない変更をエラーにします。tests/fixtures/変更.before.book の帳簿は、在庫を 0 以上に保ち、入荷を伝票と SKU の組ごとに一度だけ通します。
book 変更 v1
…
account 在庫(sku: string) : 個
at least 0 refused as 在庫切れ
…
transfer 入荷(伝票: string, sku: string, 数: 個)
key 伝票, sku
move 数 from 仕入先 to 在庫(sku)
transfer 引当(注文: string, sku: string, 数: 個)
key 注文, sku
pending expires after 30 minutes
次のバージョンでは、棚に 3 個は残し、1000 個までしか置かないようにします。入荷のキーを伝票だけにし、注文の在庫は 1 時間押さえます。返品棚と、それを使う二つの振替はやめます。
book 変更 v2
…
account 在庫(sku: string) : 個
at least 3 refused as 在庫切れ
at most 1000 refused as 置き場不足
…
transfer 入荷(伝票: string, sku: string, 数: 個)
key 伝票
…
pending expires after 1 hour
警告[W107]: 変更.book:1:1: HEAD にあった勘定 `返品棚` がありません。その残高はデータベースに残ったままになります
1 | book 変更 v2
警告[W107]: 変更.book:1:1: HEAD にあった振替 `返品` がありません。呼んでいる途中の操作があれば、リトライできなくなります
1 | book 変更 v2
警告[W107]: 変更.book:1:1: HEAD にあった振替 `戻し` がありません。呼んでいる途中の操作があれば、リトライできなくなります
1 | book 変更 v2
エラー[E050]: 変更.book:5:9: 勘定 `在庫` の下限(0 → 3)、上限(なし → 1000)が HEAD のときと違います。すでにある勘定は前の定義のまま残るので、chobo はこの変更を扱えません
5 | account 在庫(sku: string) : 個
ヒント: 新しい名前の勘定を宣言し、残高を移す振替を書いてください
エラー[E051]: 変更.book:11:10: 振替 `入荷` のキーが HEAD のときと違います。呼んでいる途中の操作をリトライすると key_conflict で拒否されます
11 | transfer 入荷(伝票: string, sku: string, 数: 個)
ヒント: 新しい名前の振替を宣言し、前の振替は、その仮押さえがどれも終わるまで残しておいてください
エラー[E051]: 変更.book:14:10: 振替 `引当` の仮押さえの終わり方が HEAD のときと違います。呼んでいる途中の操作をリトライすると key_conflict で拒否され、押さえ中の仮押さえを確定できなくなります
14 | transfer 引当(注文: string, sku: string, 数: 個)
ヒント: 新しい名前の振替を宣言し、前の振替は、その仮押さえがどれも終わるまで残しておいてください
データベースの側も、呼ばれたときに同じことを守ります。PostgreSQL と MySQL の系統は、勘定を作ったときの境界を行に持っていて、帳簿の境界がそれと違えば、呼び出しをエラー CB001 で止めます。TigerBeetle のクライアントは、勘定か、その境界のために最初に流す振替が、すでに別の定義でできているのを見つけて失敗します。境界を変えるには、新しい名前の勘定の種類を宣言し、残高を移す振替を書きます。あとで変わる枠は、はじめから一つの勘定にしておきます。
言語をまたぐ検査
ワークフローと規則のあるプロジェクトでは、ritsu check が、ワークフローが帳簿を呼ぶところで帳簿を確かめます。規則が振替に渡す額、その額で振替が拒否されうる理由とタスクが処理するエラー、確定するまでの待ちと仮押さえの有効期限です。それぞれの例はほかの言語と組み合わせるにあります。