コンテンツにスキップ

何を検査するか

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 が、ワークフローが帳簿を呼ぶところで帳簿を確かめます。規則が振替に渡す額、その額で振替が拒否されうる理由とタスクが処理するエラー、確定するまでの待ちと仮押さえの有効期限です。それぞれの例はほかの言語と組み合わせるにあります。