DESIGN.md@crates/dandori · git:20261031.e0bec7c · 2026-10-31 · sha256 42ef5a9a76c27edd

DESIGN.md@crates/dandori git:20261031.e0bec7cC

Immutable. This exact content never changes and is served at /api/v1/blob/42ef5a9a76c27edd.

# dandori 設計文書

業務ルールを呼ぶワークフローのための、型の付いた小さな言語。書いたものを走らせる前に検査し、AWS Step Functions(ASL)、Temporal(TypeScript、Python、Go)、AWS Lambda durable functions(TypeScript)、Argo Workflows(WorkflowTemplate の YAML)、pydantic-graph(Python)へコンパイルする。判断そのものは rulec の規則に書き、dandori はそれを ritsu の規則の口(rulec が答える)を通して読む。期日は koyomi の日付のファイルに、在庫や残高は chobo の帳簿に書き、dandori はそれぞれ ritsu の日付の口と帳簿の口を通して読む(1.16)。

名前は段取り(手順を前もって組むこと)から取った。ファイルの拡張子は `.flow`。

## 0. 全体像

```
.flow ── 構文解析 ── 名前と型の解決(規則・日付のファイル・帳簿は ritsu の口から読む)── 型を付けた構文木
                                                                     ├── 流れに沿った検査(案件の状態、代入、網羅、出口)
                                                                     ├── 参照インタプリタ ── シナリオの自動生成
                                                                     ├── 図(`dandori doc`):Mermaid を入れた Markdown と、実行を光らせる HTML
                                                                     ├── Step Functions(ASL、JSONata)と Lambda をつなぐコード
                                                                     ├── Temporal(TypeScript、Python、Go)と、タスクと規則のアクティビティ
                                                                     ├── Lambda durable functions(TypeScript)と、規則を呼ぶ Lambda
                                                                     ├── Argo Workflows(YAML)と、タスクと規則をコンテナで動かすプログラム
                                                                     └── pydantic-graph(Python)の、文ごとの節点のグラフ
```

### 0.1 前提

- **P1**:`.flow` 自身は判断しない。dandori の式は値を組み立てられる(レコード、リスト、値を埋め込んだ文字列)が、比較・算術・論理演算を持たない。分岐は、列挙・bool・オプショナルな値の `match` だけで、その値は規則かタスク(API、エージェント、自分で書くコード、人の承認)の結果である。抜けがあっては困る判断は rulec の規則にして、網羅と重なりを rulec に証明させるのを勧める。
- **P2**:rulec の中には入れない。dandori は rulec を、ritsu の規則の口(`ritsu_ports::Rules`)を通してだけ読む。規則の事実は型の付いたまま受け取り、`rulec doc` が描いたページは手を加えずに埋め込む。dandori のクレートは rulec のクレートに依存せず(ritsu の DESIGN 3.1)、口の答えは dandori を走らせる側が渡す。rulec は dandori を知らない。koyomi の日付のファイルと chobo の帳簿も同じく、日付の口(`ritsu_ports::Dates`)と帳簿の口(`ritsu_ports::Books`)を通してだけ読み、koyomi と chobo のクレートにも依存しない(1.16)。
- **P3**:意味を決めるのは参照インタプリタ一つ。五つのプラットフォームは、どれもそれと突き合わせて確かめる。
- **P4**:ループは回数に上限を書く。再帰は無い。だから実行履歴の長さに上限が見積もれる。
- **P5**:静的に言えないこと(外部のサービスが何を返すか、時計がいつを指すか)は、境界で実行時に確かめる。確かめて外れたら、その場で失敗させる。
- **P6**:プラットフォームによって意味が変わる機能は、その意味を出せるプラットフォームでだけ作り、ほかのプラットフォームでは E050 でエラーにする。キャンセルの後始末(`on cancel`)と、名前で送られてくるイベント(`event`)と、実行がいまどこにいるかを答えるサービスのメソッド(`status`、1.14)は、いまは Temporal だけが作れる。意味を変えず、呼び出し方や費用だけを変える項目(`queue`、`image`、規則の `local`)は、効かないプラットフォームでは何もしない。

P6 は、Temporal を主なプラットフォームにするときに足した。それまでは、どのプラットフォームでも書けることだけを言語に入れていた。Temporal でしか意味を持たない機能を言語から締め出すと、Temporal で書ける流れが狭くなる。かといって、ほかのプラットフォームで黙って別の意味にすれば、P3 が崩れる。そこで、作れるプラットフォームでは参照インタプリタと同じ意味で作り、作れないプラットフォームではエラーにすることにした。参照インタプリタが決める意味は一つのままである。

P2 は、はじめ「rulec の CLI の出力だけを読む」だった。rulec の小ささと、rulec の木で別に進む作業を、プロセスの境目で守るためである。ritsu に取り込んでからは、同じことをクレートの依存の決まりで守る(ritsu の DESIGN 0.2 の P3)。規則を JSON から組み立て直さないので、率の刻みを JSON Schema の説明の文から読むことも、rulec のバージョンごとに出力の違いを見分けることも無くなった(0.3)。

P1 は、はじめ「式は変数・そのフィールド・リテラルだけ」としていた。条件と計算を rulec の表に置けば、網羅と重なりが rulec の証明書つきで閉じるからである。しかし、タスクに渡すレコードを組み立てる、通知の文面に注文番号を入れる、といった形を整えるだけの操作は何も判断しない。これまで禁じると、形を整えるためだけのタスクや規則が要る。そこで、分岐と計算にかかわるもの(比較・算術・論理演算)だけを `.flow` の外(規則か、タスクの結果)に残し、値の組み立ては許すことにした(6 章)。

### 0.2 rulec との分担

rulec は、ワークフローを動かすこと(段階そのもの、段階のあいだの待ち、外への副作用)を対象の外に置いている。rulec のステートマシンは状態を呼び出し側に持たせ、生成する関数は純関数のままである。dandori は、その外側に置いた半分を受け持つ。

**決定**:別の言語として、rulec の外に作る。

理由は二つ。一つ、待ちと外への副作用と再試行は、rulec の表の行にならない。rulec がステートマシンを別の言語にせず宣言一つで済ませられたのは、遷移を表の行として書けたからで、ここでは逆になる。二つ、rulec は小さいことで証明できる言語であり、ワークフローを入れればその小ささが崩れる。

**費用**:検査・生成・確かめ方を、もう一揃い作ることになる。判断の中身の検査(網羅、重なり、単位、丸め)は rulec に残るので、dandori の検査はワークフローの形と案件の状態に絞れる。

つなぎ目は ritsu の規則の口だけにした(ritsu の DESIGN 3.2)。規則のファイルを渡すと、rulec が検査を通した規則について、分かっていることを型で返す。

| 口の答え | dandori が読むもの |
|---|---|
| `facts` | 規則の名前とバージョンと SHA-256、入力と出力(名前、ASCII の別名、型、単位、受け渡す整数での範囲。率は刻みつき)、列挙、ステートマシン(表の軸と、各行が受け付ける座標、行が書く値、遷移先、held)、前提、規則の Connect のサービスの形(パスとフィールドと列挙の値の名前、1.13)、生成された TypeScript と Python と Go の呼び出し方 |
| `doc` | 人が読むページとして描いた規則(Markdown と HTML)。言語と、ページの頭に書くファイルの名前を渡す。読み解かず、`dandori doc` とブラウザで試すページにそのまま入れる(5.2) |

口の答えは、dandori を走らせる側が渡す(`dandori::cli::run(引数, 口, 標準出力, 標準エラー)`)。`ritsu dandori` は rulec 自身の答え(`rulec::ports::Engine`)を渡し、規則を同じプロセスの中で読む。dandori のクレートのバイナリ(`dandori`)は rulec を持たないので、規則を読まない口を渡す。規則を使わないワークフローは前と同じに動き、`use rule` に来たところで、`ritsu dandori` で走らせるよう E005 で言う(ritsu の DESIGN 2.3)。ページの頭に書くのはファイルの名前だけで、その機械でのパスは入らない。ブラウザで試すページ(5.3)は rulec を持てないので、例の規則について口が答えたもの(事実と、描いたページ)を記録して持ち、記録から答える口で読む。

### 0.3 ritsu の土台へ移したもの

dandori は ritsu(七つの言語を一つにまとめる処理系)に取り込まれ、ほかの言語と重なっていたものを、ritsu の土台のクレートのものに替えた(ritsu の PLAN の C.11)。rulec とのつなぎは、ritsu の段階 D で、子プロセスと JSON から ritsu の規則の口に替えた(ritsu の PLAN の D.3。下の最後の項)。

- **二つの言語の文**:診断の文と注、そこに至る実行の一歩ずつ、それを組み立てる文の切れ端を、ritsu-base の `tr!("日本語", "English")` で書く。前は英語を先にして `(en, ja)` の組で書いていた。並びを替えただけで、文は一字も変えていない(全部の例とテストのフローの `check` を英語と日本語と JSON で、例の `build` を七つのプラットフォームで、`scenarios` と `doc` を出し、替える前と一字も違わないことを確かめた)。`src/rulec.rs` が返す組は、段階 D で `Text` にした。
- **言語の選び方**:`--lang`、`DANDORI_LANG`、`RITSU_LANG`、英語の順に読む。ritsu のどの言語も `RITSU_LANG` を読むので、一つの設定で全部の言語の文面を日本語にできる。環境変数は `ja_JP` のような長い書き方も日本語と読む(前は `ja` だけだった)。
- **コマンドの表**:コマンドとフラグを一つの表(`src/cli.rs`)に書き、`--help` の表示と引数の読み取りが同じ表を引く(ritsu-base の `cli`)。コマンドとフラグそのものは変えていない。変わったのは次のことである。
  - `dandori --version` が `dandori 0.1.0` を出す。
  - `dandori <コマンド> --help`(`dandori help <コマンド>` も同じ)が、そのコマンドの使い方、引数、フラグ、終了コード、例を出す。`dandori --help` と `dandori help` は全体の一覧を出す。どれも標準出力に出して 0 で終わる。前は `--help` も、使い方を標準エラーに出して 2 で終わっていた。
  - 表に無いフラグ、閉じた集合の外の値(`--format` は `check`・`build`・`run`・`scenarios` では `json` か `text`、`doc` では `html` か `md`。`--target` と `--lang` も)、値の無いフラグ、二度書いたフラグは、そう言って 2 で終わる。前は、どのコマンドもすべてのフラグを受け取って使わないものは黙って捨て、`--lang` の知らない値は英語として読み、二度書いたフラグは後のほうを使っていた。黙って捨てると、エージェントはフラグが効いたと思い込む(rulec の §12.1 と同じ決まり)。
  - フラグの値を `--format=json` のように `=` でつないでも書ける。
  - 引数の誤りを言う文は ritsu-base のもので、英語と日本語がある(前は英語だけ)。ファイルが一つも無いときは、そのコマンドの使い方の一行を出す。
- **生成するコードが名前に使えない語の表**(Python・Go・TypeScript)を、ritsu-emit の `copies` から読む。表の中身は前と同じで、生成物は一バイトも変わらない。Python 3.14 の `keyword` や Go 1.25 の事前宣言の識別子といった標準の表にそろえるかは、ritsu の段階 E で一つの生成パッケージを作るときに決める。
- **テスト**:一時ディレクトリ、Chrome を探して走らせること、golden の取り直しを、ritsu-testkit のものに替えた。SKIP の行は `SKIP: dandori: <理由>` の形になり、`RITSU_TEST_LEVEL` で段を選べる(`fast` なら、Node・Python・Go・Chrome や、Temporal・Argo・LocalStack が要るテストは、段で外れたと言って通る。規則は段階 D から同じプロセスの中で読むので、規則を読むだけのテストは `fast` でも走る)。golden は `DANDORI_BLESS=1` のほかに `RITSU_BLESS=1` でも取り直せる。
- **`.proto` の読み手**(ritsu の PLAN の D.10):`use proto` のファイルとそれが import するファイルを、ritsu の一つの読み手(`ritsu_proto`)で一つずつ読み、型の名前を、そのファイルから見えるファイル(自分、import した先、`import public` で渡された先)だけから引く。前は自分で書いた読み手で読み、読んだ全部のファイルから引いていた。どこに import を探すか(buf の置き方ならモジュールの根から、ほかはファイルのディレクトリから)、そのファイルを dandori の読み方(ディスク、またはブラウザで試すページが持つファイル)で読むこと、proto3 だけを読むこと(proto2 と edition は受け付けない)、読めなかった import があるときに解けない型の名前を書いたまま残すことは、dandori の側に残した。例とテストのフローの全部(`check` を英語と日本語と JSON で、例の `build` を七つのプラットフォームで、`scenarios`、`doc`、テストのフローの Temporal の `build`、701 回)の出力は、替える前と一字も違わない。ritsu の読み手のテストは、dandori の古い読み手と三つのリポジトリの全部の `.proto` で同じ結果になることを確かめたうえで、dandori が取るものを書いた golden と比べる形だけを残した。変わったのは、`.proto` として読めないファイルの言い方だけである。前は「line 5: expected `;`」と行と期待したものだけを言い、いまは「specs/order.proto:5:1: `;` is expected where `}` is」とファイルと列と、そこにあったものも言う(`use proto` の E016 の注。どちらも英語だけ)。

- **規則の口**(ritsu の PLAN の D.3):規則を、rulec を子プロセスで三回(`schema`、`certificate`、`api`)走らせて JSON から組み立てるのをやめ、ritsu の規則の口(`ritsu_ports::Rules`)から型のまま受け取る。`src/rulec.rs` は口の事実を dandori の形(`RuleInfo`)にするだけになり、JSON の読み手を消した。生成器は、生成されたコードの呼び方を、`rulec api` の JSON からではなく型(`Call`)から読む。`rulec doc` のページも口から受け取り、ページの言語を口に渡す。コマンドの本体は `dandori::cli::run` に移し、`main.rs` は規則を読まない口を渡してそれを呼ぶだけにした。替える前に、rulec のコーパスの 50 本と dandori の 14 本の規則のすべてで、口の事実から作る `RuleInfo` と、前の JSON の読み手が作った `RuleInfo` を、dandori が読む 1,939 の項目で突き合わせた。違ったのは 7 か所で、どれも前の読み手の読み違えだった。規則がたどる並び(`elements`)を入力の一つに数えて文字列と読んでいたこと(5 本。どちらでも E005 でエラーにする)と、無いことがある入力(`T?`)を文字列と読んでいたこと(2 本)である。率の刻み(11 列)、ステートマシン(5 本)、Connect のサービスの形(59 本)は、どれも同じだった。変わったことは次のとおり。
  - 無いことがある入力か出力(`T?`)を持つ規則を、`use rule` が E005 でエラーにする(1.13)。前は文字列と読み違えて受け取り、生成するコードも `Window | null` のような型を名前として読み込んで壊れていた。
  - 遷移を決める表が状態の列を持たないステートマシン(証明書の `axis` が null)を、表のどの行も、どの状態からでも当てはまるものとして読む。前は最初の軸を状態の軸と取り違え、どの呼び出しも当てはまらないと読んでいた。例とテストの規則と rulec のコーパスには、そういうステートマシンは無い。
  - 規則を読めないときの E005 の注は、rulec が言う診断を、コードと文と場所で一つずつ書く(前は、rulec が標準エラーに書いたものの頭の 6 行)。
  - `connect.enums` を言わない rulec(0.21.2 まで)のための読み方(rulec 自身の列挙の値の名前を組み立てて呼ぶこと、取り込んだ列挙を持つ規則の E005)は消した。ritsu の中の dandori が読む rulec は、いつもワークスペースの rulec だからである。
  - `DANDORI_RULEC` は無くなった。`--help` の最後の行は、規則を使うワークフローを `ritsu dandori` で走らせると言う。
  - 出力を書けなかったとき(読む側が先に閉じたパイプなど)、前は止まり(`println!` のパニック)、いまは書けなかった分を捨てて続ける。
  例とテストのフローの全部(`check` を英語と日本語と JSON で、例と `tests/flows` の `build` を七つのプラットフォームで、`scenarios`、`doc` を Markdown と HTML の二つの言語で、`run`。969 回)の出力を、替える前(rulec 0.22.1 を `DANDORI_RULEC` で渡したもの)と `ritsu dandori` で比べ、一字も違わなかった。

- **単位の型**(ritsu の PLAN の D.4):数の型(`Ty::Num`)は、単位を文字列ではなく ritsu の単位の型(`ritsu_units::Unit`)で持つ。二つの単位が同じかは、単位の型が表を引いて決める(`JPY` と `円` は同じ。`kg` と `g`、税込と税抜は違う。1.2)。型の綴り(ritsu-units が読む rulec の綴り)は、書いたとおりに出す。率の刻みは単位の型の刻みで持ち、刻みを綴りの文字列で作って読む関数(`rate_unit` と `rate_per`)を消した。範囲の端に単位を付けて書けるようにし、型の単位で数えた整数に換算する(1.3)。変わったことは次のとおり。
  - 規則の出力の `money[JPY, incl_tax]` を、`money[円, incl_tax]` を受け取るタスクに渡せる(ritsu の DESIGN 1.4 の例)。前は単位を文字列で比べ、E003 でエラーにしていた。
  - 単位の表に無い単位(`mass[foo]`)、税区分の誤り(`money[円, foo]`)、次元の誤り(`length[kg]`)を、E002 でエラーにする。前は種類の語(`mass` など)だけを確かめ、ほかは文字列として受け取っていた。注に、なぜ単位でないかを書く。
  - 温度の単位(`℃`、`℉`)を書けるようにした。前は字句の段で読めず、`temperature[℃]` は E001 だった。書けるのは型の `[` のすぐあとと、範囲の端の単位(`range >=41℉`)だけで、名前の一部にはならない(生成するコードの名前に書けない字なので、ほかのところではこれまでどおり E001)。
  - 単位を付けた範囲の端(1.3)。前は E001 だった。
  例とテストのフローの出力(969 回)は、`tests/fixtures/range_syntax.flow`(この変更を試すように書き直したもの)の `check` の三つのほかは、替える前と一字も違わない。生成物は、どのプラットフォームでも変わらない(値は、いままでどおり宣言した単位で数えた整数で運ぶ)。

- **中のものと参照**(ritsu の PLAN の D.6):ritsu の口の `Items` と `References` に答える `src/ports.rs` を足した。ritsu の参照の種類は `task`、`case`、`record`(下に `field`)、`enum`(下に `value`)、`input`、`output` で(ritsu の DESIGN 6.3)、名前は `.flow` に書いたとおり(日本語の名前もそのまま)である。どちらも構文まで読むだけで、規則は読まない。
  - 定義の文(yuen が端のハッシュを取る元)は、タスク・案件・レコードなら宣言の行とその下の塊の行、列挙・フィールド・入力・出力ならその行、列挙の値ならその名前である。どの行も、コメントと前後の空白を除き、文字列の外の続いた空白を一つにし、字下げを深さごとの空白二つに直す。レコードのコロンをそろえ直しても、字下げの幅を変えても、コメントを書き足しても、定義は変わらない。型や呼び方を変えれば変わる。
  - 参照は、`use rule`(呼び方を添える:下に何も書かなければ `use rule`、書けば `use rule … lambda`・`use rule … connect`・`use rule … local`、二つなら `use rule … lambda, local`)、`use proto`(`proto` の参照)、`use openapi`(`openapi` の参照。2026-10-06 から。前は `file`)と `use smithy`(`file` の参照)、`implements` のサービス、`connect` のタスクのサービスとメソッド、子の `flow` である。日付のファイルと帳簿を足してからは(1.16)、`use dates`(`koyomi` の参照。呼び方を添えて `use dates … lambda, local`)、`use book`(`chobo` の参照。`use book … lambda`)、帳簿の操作のタスクが指す振替(`chobo "<帳簿>" transfer <振替>`、`book`)も参照になる。サービスは package から見た名前で指す。型の中の参照(`<API>.<名前>`、`<規則>.<列挙>`、`follows`)と `http` の操作は入れていない(どれも、参照に出る行のファイルの中を指す)。
  - dandori のコマンドの出力は変わらない。

- **規則の名前のぶつかりと `explain`**(ritsu の D の二つ目の部分):規則の別名が、dandori が規則のまわりに書くコードの名前とぶつかると生成物が壊れるので(ritsu の PLAN 7.5)、そういう規則を `check` が E006 でエラーにする。別名が同じ二つの規則と、タスクと規則のアクティビティが同じ名前になるものもエラーにする。二つの規則のあいだで重なる名前(同じ名前の列挙、同じ単位の型)は、エラーにせず、生成するコードの側で避けるようにした(1.15)。あわせて、診断のコードの台帳(`src/codes.rs`)と `dandori explain <コード> | --all [--format markdown|json]` を足した(ritsu の DESIGN 4.3。台帳の形は ritsu-base のもの)。どのコードにも、いつ出るか、直し方、そのコードを出す最小の `.flow`(要る規則や `.proto` は隣に置く)があり、`tests/codes.rs` が、どの例も自分のコードを出すことと、検査が出しうるコードがどれも台帳にあることを確かめる。サイトの診断コードの表は、これまでどおり一行ずつの表のままにした。
  - 変わったこと:上の三つのフローが E006 になる(どの例とテストのフローも当たらない)。重なる名前を持つフローの `rules.ts` と `rules.py` が、壊れたものから通るものになる。`dandori explain` が加わり、`--help` の一覧に `explain` の行が増えた。

- **規則を読めない dandori のエラーの出し方**(ritsu の段階 E の最初の部分。ritsu の DESIGN 2.3):このクレートのバイナリ(規則を読めない口を持つ)で `use rule` のあるフローを確かめると、最初の `use rule` に新しい E018 を一つ出し、同じコマンドを `ritsu dandori` で走らせる形を注に書いて、exit 2 で終わる。規則を読めないことから起きるほかの診断は出さない。前は規則ごとに E005 を出し、規則の型を使うところに E002 が続いて、exit 1 だった(`examples/hotel/temporal/hotel.flow` で 16 件)。ritsu の受け取る側の三つの言語(dandori、yuen、sakai)を、同じ形のエラーの出し方にそろえた。E005 の説明から、このバイナリのことを外した。`--help` の終了コードの行に E018 を書いた。規則の口に `joined`(rulec がつながっているか)を足し、規則を読めない口(`NoRules`)は false を返す。
- **`ritsu check` に渡す `check` の結果**(ritsu の段階 E の最初の部分。ritsu の DESIGN 8.3):`dandori::ports::Engine::checked` が、渡された規則の口で、`dandori check` が印字するもの(診断一つずつのテキストと `--format json` のオブジェクト、通ったファイルの行)を、コマンドと同じ関数(`commands::render`、`commands::passed`)で作って渡す。コマンドの振る舞いは変えていない。日付と帳簿を足してからは、`checked_with` が規則の口に加えて日付の口と帳簿の口も受け取り、`ritsu check` はそれで渡す(前は規則の口だけで、`use dates` と `use book` のあるフローは `ritsu check` で E018 になっていた)。
- **言語をまたぐ検査に渡すもの**(ritsu の段階 E。ritsu の DESIGN 7.4〜7.8):ritsu の流れの口(`ritsu_ports::Flows`)の `crossings` が、dandori の検査を通るフローについて、ほかの言語を呼ぶところを全部渡す(`src/crossings.rs`)。dandori は自分では確かめず、ritsu-cross が相手の言語に尋ねて決める。
  - 規則、日付、帳簿の振替を呼ぶところごとに、渡す値がどこから来うるか(`Origin`)。範囲の求め方(1.3)と同じく、変数に値を入れるすべての場所を合わせる。koyomi の日付の日(`due.day`)と規則の出力(`fee.amount`)は、読むレコードの型から、どの日付や規則の答えかが分かる。数ならその範囲、何も言わないところ(範囲の無い入力やタスクの結果、日付で入ってくる値)ならその場所、`now` なら `now` である。
  - 期限のある仮押さえを追う案件ごとに、確定(`sends post`)と取消(`sends void`)の呼び出しまでの、仮押さえを作ってからの長さの最小と最大(秒)。フローの分岐をたどり、分かれた先ごとに数える。作る呼び出しの中で仮押さえはできるので、最小はその終わりから、最大はその始まりから数える。呼び出しは最小 0 秒、最大は `timeout` に、`retry` の回数だけの試しとあいだの待ちを足したもの(`timeout` の無いタスクは上限が無い。規則と日付の呼び出しは最大 33 秒。16 章)。`timeout until` の `event` と `callback` は、期限が下の `wait until` と同じ形の `x.at` なら、期限の最大と呼び出しを始めたときの最大の大きいほうに 1 秒を足したものまでで、ほかの期限は上限が無い。`wait` は書いた長さ。`wait until x.at` は、`x` が日付の入力に仮押さえのあとで読んだ `now` を渡した koyomi の日付の答えなら、koyomi が数えた入力から日付までの日数の最小から 1 日引いた日の、日付の時刻から、日数の最大の日の、日付の時刻まで(`now` はその日のどの時刻でもありうる)。日数は日付の口の `span` から読む。ほかの `wait until` は最小 0 秒で、上限が無い。`match` の分岐と呼び出しのハンドラーは、分かれた先を合わせる(最小の最小、最大の最大)。ただし、イベントを待つタスクの `on timeout` だけを書いたハンドラーは、タスクの `timeout`(無ければ一日)が過ぎてから動くので、最小にその長さを足す(`timeout until` なら、最小を期限の最小からにする。ほかのタスクのタイムアウトは、Temporal のハートビートなどで `timeout` より早く来ることがあるので足さない)。ループは最小 0 秒、最大は一回でいちばん長くかかる場合の長さを回数だけ。`on failure` と `on cancel` は、失敗やキャンセルが届きうるどの点よりも長くはない。確定や取消は、その呼び出しの中で効くので、最小は呼び出しの始まりまで、最大は終わりまでである。
  - `rule_calls`(X2 のために E.4 で足したもの)は、規則の口だけで読むので残した。日付と帳簿を使うフローは、`crossings` で読む。
  - コマンドの振る舞いは変えていない(例と `tests/flows` のフローの全部、50 本の `ritsu dandori check` の出力が、替える前と一字も違わない)。
- **呼ぶ操作**(sekisho の段階 D、2026-10-06。sekisho の DESIGN 4.6、8.3):ritsu の流れの口の `operation_calls` に答える(`src/ports.rs`)。dandori の検査を通るフローについて、タスクの呼び出しのうち、`use openapi` の `http` のタスクと `use proto` の `connect` のタスクのものを、行の順に渡す。ritsu-cross の X16 が、sekisho のゲートがワークフローに許すものと突き合わせる。
  - 操作は参照の書き方で、パスはルートから書く。OpenAPI は `openapi "<パス>" operation <operationId>` で、ritsu-base の `openapi` で文書を読み、タスクの方法とパスから操作を引く(`operationId` が無ければ `"POST /orders/{orderId}/refunds"` の形)。sekisho の `guards` と同じ読み手なので、同じ操作は同じ参照になる。proto は `proto "<パス>" service <サービス> method <メソッド>` で、サービスは package から見た名前である。
  - 拒まれたときのエラーは、タスクが宣言したエラーのうち状態が 403 のもの(`errors denied = 403`。Connect の `permission_denied` も 403 に読む)の名前である。
  - `use openapi` の無い `http` のタスク(`url` だけのもの)と、ルートの外の文書の操作は入れない。参照に書けないからである。
  - 帳簿の振替のタスク(`book`)も入れない(2026-10-07)。dandori が書くアクティビティは chobo のクライアントを呼ぶだけでゲートに尋ねず、chobo の拒否の理由には、認可で拒まれたことに当たるものが無い。拒まれることが起きず、起きたときの答えも無いので、X16 が確かめることが無い(sekisho の DESIGN 3.4)。
  - コマンドの振る舞いは変えていない。確かめ方は `tests/ports.rs` の `a_flow_says_which_operations_it_calls`(例の `hotel` の Stripe の操作、`fulfillment` の倉庫の `.proto`、一時ディレクトリのフローの三つの呼び出しと拒まれたときのエラー、検査を通らないフロー)。
- **生成物の頭**(ritsu の段階 E。ritsu の DESIGN 9.2):どのプラットフォームの生成物も、頭の二行を ritsu の生成器が共に使う形(ritsu-emit の `header`)にした。一行目は `Code generated by dandori <ritsu のバージョン>. DO NOT EDIT.`、二行目は `Source: <ファイル> (workflow <名前> v<版>, sha256:<16 桁>)` である。前の頭は `Code generated by dandori from <ファイル>. DO NOT EDIT.` の一行で、バージョンも中身のハッシュも無く、生成物がどの dandori で、`.flow` のどの中身から作られたかを、ファイルからは確かめられなかった。rulec・koyomi・chobo の生成物も同じ形なので、ritsu の一つの生成パッケージ(ritsu の DESIGN 9.3)の中で、どのファイルも同じように読める。ファイルは、`dandori build` では前と同じくファイルの名前、`ritsu gen` ではプロジェクトのルートからのパスで書く(モデルの `source_path`。ハッシュは `source_sha256`)。変わったのは全部の生成物の頭の行だけで、Temporal のビルド ID(`BUILD_ID`)は生成したコードのハッシュなので、これに合わせて変わる。ブラウザで試すページの `dandori.wasm` は作り直し、サイトの例のページと `presets.json` は記録し直した(規則のページが中に持つ、rulec の生成した JavaScript の頭も変わったため)。
- **生成する Python と `mypy --strict`**(ritsu の段階 E):ritsu の生成パッケージは `mypy --strict` を通ることを確かめる(ritsu の DESIGN 9.3)。Temporal の Python(と、同じ部品を使う pydantic-graph と Step Functions の Lambda の Python)の生成物は、それを通らなかった。直したのは次のところで、どれも型の書き方だけである。振る舞いは変わらない。
  - `types.py` の検査:フィールドを `v.get(…)` で読むと、`isinstance(v, dict)` で絞った型から `Any | None` が返り、`re.match` や大小の比較が型の検査でエラーになった。フィールドは `dd_field(v, …)`(`Any` を返す)で読む。
  - `io.py` の型の名前(`Answer`、`HttpRequest`、`JevTask`、`AgentCall`、`RuleWire`)を `dict` から `dict[str, Any]` にした。`_book_run` の引数と返す値も同じ。
  - 使うときに読み込む SDK(boto3、botocore、OpenAI の Agents SDK と openai、Anthropic の SDK)の `import` に、入っていなくても型の検査がエラーにならないよう、注(`# type: ignore[import-not-found, import-untyped, unused-ignore]`)を付けた。Agents SDK のクラスを継ぐところにも注を付けた。
  - `Any` を返していたところ(`workflow_of`、`message_of`、`client.py` の `status`、エージェントの答えの JSON とスキーマ)は、型の付いた値にしてから返す。
  - `rules.py` は、単位の付いた数を受け取る規則に、rulec の Python が定義する `NewType`(`GBPInclTax` など)で包んで渡す(rulec 自身のランナーと同じ)。前は `int` のまま渡していた。
  - `workflow.py` は、`T?` でない入力を `input["…"]` で読む(入力の検査が、そこにあることを確かめたあとなので)。
  直したあとも、ネストした関数の中で読む変数(`T | None` で宣言した `let` の変数。並列のイテレーションの中や、`dd.attempt` に渡すラムダの中)や、サービスを実装するフローのクライアントの型には、`mypy --strict` がエラーにするものが残っている(ritsu のテストのプロジェクト `通販` の受注のフローで 6 か所)。それを直すには、変数の宣言の型を流れに沿って決める必要があり、ここではしていない(7 章)。
- **サイトの置き場所**(ritsu の PLAN の F.7):サイト(英語と日本語のページ、例の図のページ、ブラウザで試すページ)を、ritsu のサイトの下の `website/dandori/` に移した(`git mv`)。設定(`zensical.toml` と `zensical.ja.toml`)は dandori のサイトのままで、`site_url` は `https://i2y.github.io/ritsu/dandori/`(日本語は `/ritsu/dandori/ja/`)、`repo_url` は ritsu のリポジトリにした。ritsu の `website/build.sh` が、`website/dandori/build.sh` で組んだものを `website/build/dandori/` に置く。Zensical は ritsu のサイトの `website/.venv` のものを使い、プレビューは ritsu の `website/serve.sh` でする(dandori の `serve.sh` は消した)。テスト(`tests/docs.rs`、`tests/doc.rs`、`tests/playground.rs`、`tests/skill.rs`)はこのクレートに置いたまま、サイトをワークスペースの根の `website/dandori/` で読む。`skills/sync.sh` と `website/dandori/tools/make_wasm.sh` も、そこから読み、そこへ書く(作り直した `dandori.wasm` は、前のものと一バイトも違わなかった)。ページの中の元のリポジトリへのリンクは、ritsu のリポジトリの `crates/dandori/` を指すようにし、入れ方のページは ritsu から入れる形にした。公開しているサイトは、切り替えるまで元のリポジトリの `https://i2y.github.io/dandori/` のままで、README とスキルからのサイトへのリンクもそれを指す。
- **ブラウザで試すページ**(ritsu の DESIGN 8.7 と 13.2。2026-10-05):サイトのブラウザで試すページ(5.3)を、ritsu のブラウザで試すページへの転送に替えた。ritsu のページでは、wasm32 にした ritsu の中で dandori が動き、このページが開いたフロー(下書きと、例のすべての版)を、フローが読むファイル(規則、API の記述、子のフロー、日付のファイル、帳簿)と一緒に、一つのプロジェクトとして開く。規則も書き換えられ、規則は rulec が、日付のファイルと帳簿は koyomi と chobo が、同じモジュールの中で読む。`website/dandori/docs/playground.md` と日本語のページは、ritsu のページへ送るだけのページにした。来たリンクのハッシュが `=` を含む(このページが出していた `#flow=…&view=…&target=…` は、ritsu のページも読む)ならそのまま渡し、そうでなければ、このページが開いていた下書き(`#flow=tests/fixtures/hotel_naive.flow`)を開く。ナビとトップページのボタンは、ritsu のページを直接指す。このページだけが使っていたもの(`src/wasm.rs`、`src/playground.rs`、`src/record.rs`、`sources` の `Playground`・`Recorder`・`Bundle` と記録から答える三つの口、`website/dandori/docs/playground/` の `dandori.wasm`・`presets.json`・`playground.js`・`playground.css`、`website/dandori/tools/make_wasm.sh`、`tests/playground.rs`)は消した。`tests/playground.rs` が確かめていたことは、ritsu の `tests/playground.rs` が ritsu のページに対して確かめる。例のフローがどれもプロジェクトであること、このページが並べたフローを同じ順に同じ出力先で並べること、ページの答えが同じファイルを置いたディレクトリでの `ritsu` のバイナリの出力と同じこと、wasm がライブラリと同じ答えを返すこと、プロジェクトに無い規則と子のフロー・自分を子として走らせるフローの編集、Chrome でこのページのリンクが同じフローを開くこと、である。

## 1. 言語

### 1.1 ファイルの形

```
workflow hotel_stay v1
description "When a stay is booked, hold an amount on the card, and capture it on the day of check-out"

use rule hold from "rules/hold_amount.rule"
  lambda "arn:aws:lambda:ap-northeast-1:123456789012:function:hold-amount"
use rule payment_intent from "rules/payment_intent.rule"

record PaymentIntent
  id     : string
  status : payment_intent.status

task confirm_intent(id: string) -> PaymentIntent
  http POST "https://api.stripe.com/v1/payment_intents/{id}/confirm" form
  connection "arn:aws:events:…"
  sends confirm
  errors card_declined = 402, unexpected_state = 400
  refused as unexpected_state
  key

case pi : PaymentIntent follows payment_intent.payment
  held capture_method = manual
  held confirmation_method = automatic
  external authenticate, settle, expire
  refused when refused = true

flow
  let quote = hold(room: booking.room, nights: booking.nights)
  match quote.handling
    review => succeed outcome = awaiting_review
    auto => pi <- create_intent(…)
  …

on failure
  …
```

字下げがブロックを表す。コメントは `#` から行末まで。名前には日本語が使え、生成物でもそのまま識別子になる(JavaScript と Step Functions の変数は Unicode の識別子を受け付ける)。`examples` の例は、英語の版と、同じディレクトリに置いた日本語の版(`<名前>.ja.flow`)の両方で書いてあり、`tests/flows` のフローは日本語の名前で書いてある(5 章)。

### 1.2 型

| 型 | 値 |
|---|---|
| `int` | 整数。rulec の単位の無い `number` もこれになる |
| `money[円, incl_tax]` など | 単位の付いた整数。rulec の書き方のまま書き、単位が違えば渡せない。単位は ritsu の単位の型(ritsu-units。rulec と同じ表)で読み、表に無い次元と単位、`incl_tax`・`excl_tax` のほかの税区分は E002 |
| `string` | 文字列 |
| `bool` | 真偽 |
| `timestamp` | UTC の RFC 3339(`2026-10-01T10:00:00Z`)。Step Functions の Wait が受け付ける形に合わせた。日付の端で範囲を書ける(1.3、20 章) |
| `date` | 日付(`2026-10-01`)。日付のファイルの日付と、rulec の日付の列の型。式は比べも足しもせず、運ぶだけで(1.16)、範囲は入ってくるところで確かめる(1.3、20 章) |
| 列挙 | `enum 結果 = 宿泊済 \| 確認待ち`、または規則の列挙 `payment_intent.status` |
| レコード | `record` で宣言する。規則の呼び出しは、その規則の出力のレコードを返す。自分自身を含められない(下の注) |
| `list[T]` | リスト。リストのリストは書けない(下の注) |
| `T?` | オプショナルな値。フィールドが無いときも null のときも「無い」として読む |
| `json` | どんな JSON でもよい値。中を見ずに受け渡すだけで、フィールドを読むことも `match` することもできない。入力やレコードのフィールドが無いときは、null として読む |
| `<API>.<名前>` | `use proto` で読んだ `.proto` のメッセージや列挙から作った型。`warehouse.ReserveResponse` のように、`use proto` に付けた名前のあとに書く(1.12) |

リテラルの整数は単位の付いた型にも渡せる。変数どうしは、単位が同じでなければ渡せない。同じ単位かは、単位の型が表を引いて決める。`money[JPY, incl_tax]` と `money[円, incl_tax]` は同じ単位なので渡せる(`JPY` は `円` の別の綴り)。`mass[kg]` と `mass[g]` は違う単位なので渡せない(E003)。dandori の式は計算しない(P1)ので、渡すところで黙って換算すれば、`.flow` が計算をすることになる。換算は規則に書く。型は書いたとおりの綴りで出す(`money[JPY, incl_tax]` は、診断でも生成物でも `money[JPY, incl_tax]`)。`T` の値は `T?` のところに、どんな値も `json` のところに渡せる。`T?` の値を `T` のところに渡すには、先に `match` で `none` と `some x` に分ける。

`T?` と `json` は、入力やレコードのフィールドが無いときも、null として読む(無いことと null を区別しない)。`json` は null も値に含むので、そう読んでも宣言した型から外れない。protobuf の JSON は、値が設定されていない `google.protobuf.Value` のフィールドを省くので、`json` になるフィールドは無いことがある(1.10、1.12)。無いことを拒否すると、正しいメッセージが、入口の検査で `Dandori.BadInput` に、結果の検査で `Dandori.BadResponse` になってしまう。null になるのは読んだときで、レコードをそのまま次に渡すときは、無いまま渡る。プラットフォームごとの読み方は 4.6 の表にある。

リストのリストを書けないのは、JSONata の配列の組み立てが、中の配列を平らにしてしまうからである(`[$a, $b]` は、`$a` が配列なら要素が一段上に出る)。内側のリストはレコードに入れれば運べる。`json` の値は配列のこともあるので、それをリストに入れるところ(リストのリテラル、`yield`)では、生成する JSONata が `$type` で配列かどうかを見て包む。

レコードは自分自身を含められない。フィールドの型が自分である(`next : Node`)、リストや `T?` を通して自分である(`next : Node?`、`children : list[Node]`)、ほかのレコードを通して自分である(甲が乙を、乙が甲を含む)のどれも E003 になる。木のように入れ子の深さが決まらない値は、含むところの型を `json` にして運ぶ。`.proto` のメッセージから作ったレコードも同じである(1.12)。

理由は三つある。一つ、シナリオの生成と Argo のジェネレーターは、型をたどって値や式を作る。自分を含む型では、たどる処理が終わらずにスタックを使い切る(手で書いた `record Node` に `next : Node?` を持たせて確かめた。`dandori scenarios` も `build --target argo` も落ちた)。値は有限でも、型をたどる処理は終わらない。二つ、Step Functions は型を確かめる式を展開して書くので、自分を含む型は、ある深さから先を確かめない。実行で入ってくる値を境界で確かめる(P5)という決まりが、そこで崩れる。三つ、エージェントの応答の Schema には、自分を含む型を書き切れない(E007、1.7)。検査で止めれば、どのプラットフォームでも同じに運べる形だけが通る。

費用は、木の中身を型で確かめられず、`json` として受け渡すだけになることである。自分を含むレコードを許し、全プラットフォームで再帰する型を扱えるようにする案は、Step Functions の型の確かめが深さで打ち切られるかぎり、P5 を守れないので採らなかった(6 章)。

### 1.3 範囲

数、順序のある列挙、日付、時刻には、取りうる値の範囲を書ける。書けるところは、入力と出力、レコードのフィールド、タスクの引数と結果である。書き方は rulec と同じで、型のあとに `range` と端を書く。端は片方だけでもよい。この節は数の範囲を言い、順序のある列挙の範囲と、日付と時刻の範囲は、節の終わりの二つの段で言う。

```
record Booking
  nights : int  range >=1 <=30

task create_intent(amount: money[JPY, incl_tax] range >=50 <=99999999, …) -> PaymentIntent
task count() -> int  range >=0 <=10
```

- 端は、型の単位で数えた整数で書く(`>=0`)。JSON で運ぶ数そのものである。rulec と同じく、単位を付けて書いてもよい(`>=1kg <=40kg`、`<=100万円`、率なら `<=50%`、温度なら `>=41℉`)。単位の字は rulec と同じで、ritsu-units の `chars` が決める(記号の `%`・`℃`・`℉` と、`%` として読む全角の `%`。8 章)。単位を付けた端は、型の単位で数えた整数に換算する(`mass[g]` の `>=1kg` は 1000、`rate[step 0.1%]` の `<=50%` は 500)。換算は端を読むときだけで、値は換算しない。整数にならない端(`mass[g]` の `>=1.5g`)、型の次元に無い単位の端(`money[円, incl_tax]` の `<=1kg`)、`int` に付けた単位は E003(`tests/fixtures/range_syntax.flow`)。前は、単位を付けた端を E001 でエラーにしていた。規則の入力と出力の範囲は、規則の口が渡す範囲(受け渡す整数での端)を、同じ数え方で読む。
- `T?` や `list[T]` に書いた範囲は、中の数の範囲になる。
- 数でも、順序のある列挙でも、日付でも時刻でもないもの(`string`、`bool` など)に書いた範囲と、数が一つも入らない範囲(`>=10 <=5`)は E003(順序のある列挙の範囲と、日付と時刻の範囲は、下の段)。

範囲は型ではなく、値を置くところ(フィールド、引数、入力)に付けた。rulec の列と同じ形で、型が等しいか、値を渡せるかの規則はそのままにできる。

範囲は二つのところで使う。

**入ってくる値は、走らせたときに確かめる(P5)。** ワークフローの入力、タスクの結果(コールバックとイベントを含む)、規則の結果の数が範囲の外なら、型の合わない値と同じく `Dandori.BadInput` か `Dandori.BadResponse` で失敗する。どのプラットフォームでも、型を確かめる式に範囲の比べを足してある。レコードなら、フィールドの範囲も確かめる。規則の結果の範囲は rulec が出すもので、rulec はそれを証明している。それでも確かめるのは、呼ぶ Lambda や関数が、ワークフローを検査したときと別のバージョンの規則に替わっていることがあるからである。エージェントの応答では、OpenAI の Schema に `minimum` と `maximum` を書く。Claude の構造化出力は数の範囲を受け付けないので、Schema の説明(`description`)に `>=1 <=30` と書き、結果が来てから確かめる。

範囲を足したり狭めたりすると、Temporal で走っている実行のうち、値がその外にあるものは再生で失敗に変わる。だから、その変更はバージョンを上げるか Worker Deployment Versioning で出す(5 章の再生)。

**出ていく値は、検査で確かめる。** 規則の入力、タスクの引数、書き出したレコードのフィールド、ワークフローの出力のように、範囲のあるところに値を渡すと、検査はその値の範囲を求め、収まるかを見る。

- 値の範囲は、リテラルならその数、入力・フィールド・タスクの結果・規則の出力なら、そこに書かれた範囲(規則なら rulec の出す範囲)である。`for` の変数は、回すリストの要素の範囲になり、`some x` の `x` は、分けた値の範囲になる。
- 変数の範囲は、その変数に入れるすべての値の範囲を合わせたもの。流れのどこで入れたかは区別しない。`match` の一方で 3、もう一方で 40 を入れた変数は、どこで読んでも `>=3 <=40` である。だから、実際には渡らない値のせいで E014 になることがある。そのときは変数を分ける。
- 収まらないことがあれば E014。範囲の分からない値(範囲を書いていない入力・フィールド・結果)が混じっていれば W104。W104 は、値の出どころに範囲を書けば消える。書いた範囲は、値が入ってくるところで走らせたときに確かめるので、そこから先はその範囲にあると言える。

dandori の式は算術を持たない(P1)ので、値の範囲は、出どころの範囲をそのまま運ぶだけで求まる。計算で範囲が広がることはない。

`tests/fixtures/ranges.flow` の検査の一部:

```
エラー[E014]: tests/fixtures/ranges.flow:35:1: `予約.泊数` は `>=1 <=60` で、規則 `与信` の `泊数` の範囲 `>=1 <=30` を外れることがあります
    35 |   let a = 与信(客室: 予約.客室, 泊数: 予約.泊数)
警告[W104]: tests/fixtures/ranges.flow:38:1: `延長` の範囲が分かりません(入力 `延長` に範囲がありません)。規則 `与信` の `泊数` が受け取るのは `>=1 <=30` です
    38 |   let c = 与信(客室: standard, 泊数: 延長)
```

**順序のある列挙の範囲**。順序のある列挙の値にも、値で範囲を書ける(`campaign : tiers.tier  range >=basic <=gold`)。rulec の規則は入力に書け(rulec の §15.210)、範囲に入らない値は、規則から生成したコードも rulec の評価器も入口で受け付けない。ritsu の規則の口は、その入力の型に、範囲に入る値の並び(列挙の順)を付けて渡す(ritsu の DESIGN 3.2、rulec の §15.251)。`.flow` では、数と同じ場所(ワークフローの入力と出力、レコードのフィールド、タスクの引数と結果)に書ける(1.20)。dandori はどちらも、数の範囲と同じ考えで確かめる。

- 範囲を書けるのは、規則が値を `<` で順に並べた列挙(`enum tier = basic < silver < gold < platinum`)だけである。`.flow` で宣言した列挙、`.proto` の列挙、規則が `|` で宣言した列挙に書いた範囲は E003。端は列挙の値で書き、片方は省ける(省いた端は、最初か最後の値になる)。列挙の値でない端と、値の一つも入らない範囲も E003。
- 入ってくる値は、数と同じく、走らせたときに確かめる。範囲を書いた入力・フィールド・タスクの結果が範囲の外なら、`Dandori.BadInput` か `Dandori.BadResponse` で失敗する。
- 変数がなりうる列挙の値は、数と同じく、その変数に入れるすべての値を合わせたものである。`.flow` が列挙の値を書けるのは、型の分かるところ(規則やタスクの呼び出しの引数など)だけで、書いた値はその値そのものになる。入力・レコードのフィールド・タスクの結果は、範囲を書いていればその範囲に入る値のどれか、書いていなければ列挙のどの値にもなりうる。規則の出力は範囲を持たないので、列挙のどの値にもなりうる。それを入れた変数も同じである。`match` の分岐の中で値を狭めることは、dandori の検査では数と同じくしない(ritsu の X4 に渡す値は狭める。18 章)。
- 範囲のある場所に、範囲に入らない値になりうる値を渡せば E014 にする。文は列挙の値で言い、範囲は、範囲に入る値の最初と最後で書く(rulec の書き方)。列挙のどの値になりうるかは型と範囲が決めるので、範囲の分からない値は無く、W104 は出ない。注は、値の出どころに範囲を書くか、`match` で分けて範囲に入る値を書いて渡すかを言う。
- 範囲に収めた値の `match` は、範囲に入る値だけで分ける。範囲の外の値の分岐は通らない分岐(E011)で、範囲に入る値の分岐が無ければ E010 である。

`tests/fixtures/enum_ranges.ja.flow` の検査の一部(英語の版は `enum_ranges.flow`。範囲の中の値だけを書いて渡すフローは `enum_ranges_held.flow` と `enum_ranges_held.ja.flow` で、検査を通り、どのプラットフォームにも組める):

```
エラー[E014]: tests/fixtures/enum_ranges.ja.flow:26:1: `プラチナ` は規則 `送料` の `キャンペーン` の範囲 `>=一般 <=ゴールド` の外です
    26 |   let b = 送料(会員: 会員.段階, キャンペーン: プラチナ, 小計: 小計)
エラー[E014]: tests/fixtures/enum_ranges.ja.flow:28:1: `キャンペーン` は `プラチナ` になることがあり、規則 `送料` の `キャンペーン` の範囲 `>=一般 <=ゴールド` を外れます
    28 |   let c = 送料(会員: 会員.段階, キャンペーン: キャンペーン, 小計: 小計)
  = 値の出どころ(ワークフローの入力、レコードのフィールド、タスクの結果)に `range >=一般 <=ゴールド` を書くか、`match` で分けて、範囲に入る値を書いて渡してください
```

**日付と時刻の範囲**(20 章)。`date` と `timestamp` にも、数と同じ場所に範囲を書ける。端は rulec・koyomi・kaname と同じ `YYYY-MM-DD` の日付で書く(`at : date  range >=2026-01-01 <=2027-12-31`)。時刻の範囲は、端の日を UTC の日として読み、端のあとに `offset +09:00` と書けば、そのオフセットの日として読む(`timestamp  range >=2026-01-01 <=2026-12-31 offset +09:00`)。ワークフローが動く期間も、同じ形で `workflow` の下に書く(`now range …`。1.16)。

- 暦に無い日、日の一つも入らない範囲、日付でない端、日付に書いた `offset`、無いオフセット(`+25:00`)は E003。
- 入ってくる値は、数と同じく、走らせたときに確かめる。比べ方は、どのプラットフォームでも文字列の比べで、日付は 10 文字全体を最初の日と最後の日と、時刻は先頭の 19 文字(秒まで)を、最初の秒と範囲のすぐあとの最初の秒と比べる。式は、これまでどおり日付も時刻も比べず足さない(P1)。
- 出ていく値は、数と同じく検査で確かめる(E014、W104)。値の範囲は、日付なら日、時刻なら秒で求め、文は日付と時刻で言う。期間を書いたワークフローの本体の `now` は期間の中の時刻、koyomi の日付の `day` は koyomi が数えた日の最初から最後までである。期間を書かないワークフローの `now` と、`on failure` と `on cancel` の `now` は、範囲の分からない値である。
- エージェントの応答の Schema には、OpenAI にも Claude にも、範囲を説明(`description`)に書く。

`tests/fixtures/date_ranges.ja.flow` の検査の一部(英語の版は `date_ranges.flow`):

```
エラー[E014]: tests/fixtures/date_ranges.ja.flow:33:1: `訪問.日` は `>=2026-01-01 <=2026-06-30` で、`予約する` の引数 `日` の範囲 `>=2026-03-01` を外れることがあります
    33 |   let 予約 = 予約する(id: 訪問.id, 日: 訪問.日)
エラー[E014]: tests/fixtures/date_ranges.ja.flow:41:1: `締め.day` は `>=2026-04-20 <=2026-07-20` で、`押さえる` の引数 `日` の範囲 `>=2026-01-01 <=2026-07-15` を外れることがあります
    41 |   押さえる(id: 訪問.id, 日: 締め.day)
```

### 1.4 宣言

- `workflow <名前> v<版> [implements <API>.<サービス>]`:ワークフローの名前とバージョン。`implements` を続けると、このワークフローが、`use proto` で読んだ `.proto` のそのサービスを実装する(1.14)。
- `use openapi|smithy|proto <名前> from "<パス>"`:呼ぶ API の記述を読む(1.10)。下に `url "<場所>"` を書くと、API の場所になる。`.proto` の場所が要るのは、`connect` のタスクで呼ぶときだけである。
- `use rule <名前> from "<パス>"`:規則を読む。Step Functions と durable functions から呼ぶなら、下に `lambda "<関数>"` を書く。下に `local` を書くと、Temporal ではローカルアクティビティとして呼ぶ(4.2)。ほかのプラットフォームでは何も変わらない。`lambda` の代わりに `connect "<URL>"` を書くと、どのプラットフォームでも、rulec が生成した規則の Connect のサービスをその URL で呼ぶ(1.13)。Step Functions から呼ぶなら、その下に `connection "<EventBridge の接続>"` も書く。要素の並びをたどる規則(rulec の `elements`)は、まだ読めない(E005。1.13)。出力の無い規則(rulec の `enums` のファイル)は、列挙を型として使えるが、呼べない(呼ぶところで E005。10 章)。
- `use dates <名前> from "<パス>"`:koyomi の日付のファイルを読む。ファイルの日付は `<名前>.<日付>(…)` で規則と同じく呼ぶ。下に `lambda "<関数>"`(Step Functions と durable functions で日付を計算する関数)と `local`(Temporal のローカルアクティビティ)を書ける(1.16)。
- `use book <名前> from "<パス>"`:chobo の帳簿を読む。タスクが `book <名前>.<振替>.<操作>` でその振替の操作になる。下に `lambda "<関数>"`(Step Functions で操作する関数)を書ける(1.16)。
- `task <名前>(<引数>) [-> <型>]`:外部のサービスへの呼び出し。`->` を書かないタスクは何も返さない(結果は読まない)。下に次の項目を書く。
  - 呼び出し方(1.6):`lambda "<関数>"`、`http <メソッド> "<URL>" [form]` と `connection "<EventBridge の接続>"`、`aws <サービス>:<操作>`、`agent [openai | claude] "<指示>"` と `model "<モデル>"`(1.7。Step Functions では `connection` も)のどれか一つ。子ワークフローとして呼ぶなら、プラットフォームごとに `workflow "<型>"`(Temporal)、`state machine "<ARN>"`(Step Functions)、`durable function "<関数>"`(durable functions)、`workflow template "<名前>"`(Argo)。子も dandori で書いたなら `flow "<パス>"`(1.9)。Temporal では `queue "<タスクキュー>"` で送り先のキューを選べる。Argo で利用者のコンテナを動かすなら `image "<イメージ>"`。
  - `errors <名前> [= <HTTP ステータス> | = <例外の名前>], …`:業務のエラー。`http` のタスクではステータスを、`aws` のタスクでは API の例外(`ConditionalCheckFailedException`、書かなければエラーの名前そのもの)を書く。`timeout` と `failure`(それ以外の失敗のすべて)は宣言しなくても使える。
  - `retry <n> times [every <時間>] [backoff <k>] [on <エラー>, …]`:`on` を書かなければ、`failure` と `timeout` をリトライ、宣言したエラーはリトライしない。
  - `timeout <時間>`(書かなければ、プラットフォームがそのタスクに許すだけ動く。4 章)か `timeout until <引数>`(答えを待つタスク(`event` か `callback`)だけ。呼び出しがその `timestamp` の引数に渡した時刻で、答えの待ちが終わる。16 章)、`key`(冪等キーを送る。`aws` のタスクでは `key ClientToken` のように、キーを受け取る API の引数を書く)、`idempotent`(二度しても一度と同じ)、`callback`(トークンや ID を渡して応答を待つ)。
  - 案件に何をするか:`starts <規則>.<ステートマシン> [then <イベント>, …]`、`sends <イベント>`、`observes`。`refused as <エラー>` は、ステートマシンがイベントを拒否したときに返ってくるエラー。
  - 帳簿の振替の操作:`book <帳簿>.<振替>.<操作>`(呼び出し方の一つ。操作は `do`・`hold`・`post`・`void`。1.16)。
- `case <名前> : <レコード> follows <規則>.<ステートマシン>`:rulec のステートマシンに従う案件。下に `held <入力> = <値>`、`external <イベント>, …`、`state <フィールド>`、`refused when <出力> = <値>` を書く。帳簿の仮押さえなら `case <名前> : <帳簿>.<振替> follows <帳簿>.<振替>`(2.7)。
- `inputs`・`outputs`:ワークフローの入力と出力。`T?` の出力は `succeed` で省ける。
- `flow`:本体。`on failure`:処理されなかったタスクのエラーのあとに走るブロック。`on cancel`:ワークフローがキャンセルされたときに走るブロック(2.6。Temporal だけ)。

### 1.5 文と式

| 文 | 意味 |
|---|---|
| `let x = t(a: …)` | タスクか規則を呼び、結果を `x` に入れる |
| `t(a: …)` | タスクを呼び、結果は読まない |
| `c <- t(a: …)` | 案件 `c` に対するタスクを呼び、結果を `c` に入れる |
| `let x[: T] = <値>` | 値を `x` に入れる。`{…}` のレコードには型を書く |
| `on <エラー>, … =>` | 呼び出しの下に書き、そのエラーを処理する。処理したあとは呼び出しの次へ進む |
| `match x.f` | 分岐 `<値>, … =>` を並べる。案件の状態で分けるときは、始まっていない案件の `none` も書ける。オプショナルな値では `none =>` と、列挙や bool なら値を、それ以外なら `some y =>`(`y` にその値が入る)を書く |
| `wait <n> seconds\|minutes\|hours\|days`、`wait until <timestamp>` | 待つ |
| `repeat at most <n> times` | 上限つきのループ。`break` で抜ける |
| `[let r =] for x in <リスト> at most <n> [in parallel[, <k> at a time]]` | リストの要素ごとに本体を回す。要素が `n` を超えていれば `Dandori.TooManyItems` で失敗する。`let r =` を付けると、本体の最後の行の `yield <値>` を集めたリストが `r` に入る |
| `pass` | 何もしない |
| `succeed <出力> = <値>, …` | 成功で終わる |
| `fail <エラー> ["<理由>"] [leaving <案件>, …]` | 失敗で終わる。`leaving` は、終わっていない案件をそのまま引き渡すという宣言 |

値に書けるのは、変数とそのフィールド、文字列・数・`true`・`false`・列挙の値、`none`(オプショナルな値のところだけ)、`now`(その文を走らせた時刻。1.16)、`{フィールド: 値, …}`(型は渡す先で決まる)、`[値, …]`、そして値を埋め込んだ文字列(`"注文 {受注.id} を出荷しました"`、波括弧そのものは `{{` と `}}`)である。文字列に埋め込めるのは文字列・数・bool・列挙・timestamp・date で、オプショナルな値は先に `match` で分ける。`fail` の理由にも埋め込める。

変数の型は、値を入れるすべての場所から決まる。一つの名前は一つの型を持ち、`T` と `T?` の両方を入れる名前は `T?` になる。

### 1.6 タスクの呼び出し方

タスクをプラットフォームがどう呼ぶかは、タスクに書いた呼び出し方で決まる。

| 呼び出し方 | Step Functions | Temporal | Lambda durable functions | Argo Workflows | pydantic-graph |
|---|---|---|---|---|---|
| `lambda` | Lambda の Task | dandori が書くアクティビティが関数を呼ぶ | dandori が書く step の中で関数を呼ぶ | dandori が書く実装をコンテナで動かす | dandori が書く関数が関数を呼ぶ |
| `http` | HTTP Task | dandori が書くアクティビティが `fetch` で送る | dandori が書く step の中で `fetch` で送る | 同じく、コンテナで `fetch` | 同じく、urllib で |
| `connect`(1.10) | HTTP Task(Connect の JSON) | dandori が書くアクティビティが `fetch` で送る | dandori が書く step の中で `fetch` で送る | 同じく、コンテナで `fetch` | 同じく、urllib で |
| `aws` | AWS SDK 統合 | dandori が書くアクティビティが AWS SDK で呼ぶ | dandori が書く step の中で AWS SDK で呼ぶ | 同じく、コンテナで AWS SDK | 同じく、boto3 で |
| `agent` | HTTP Task で OpenAI の Responses API を呼ぶ(Claude なら Messages API) | dandori が書くアクティビティが OpenAI の Agents SDK で呼ぶ(Claude なら Anthropic の SDK) | dandori が書く step の中で、同じ SDK で呼ぶ | 同じく、コンテナで | 同じく、Python の Agents SDK か Anthropic の SDK で |
| `book`(1.16) | 帳簿の Lambda 関数の Task(chobo の Python のクライアントで操作する関数を dandori が書く) | dandori が書くアクティビティが `Transport` の `book` で chobo のクライアントを呼ぶ | dandori が書く step の中で、同じく | 同じく、コンテナで | 同じく、`Transport` の `book` で |
| `state machine` | ネストした実行(`startExecution.sync:2`) | | | | |
| `flow`(1.9) | ネストした実行(`state machine` と) | 子の `<名前>_v<版>` の子ワークフローを、子のタスクキューで | invoke(`durable function` と) | 子の WorkflowTemplate から Workflow を作る | 利用者が書く関数 |
| `workflow` | | 子ワークフロー(`executeChild`) | | | |
| `durable function` | | | 別の durable function の invoke | | |
| `workflow template` | | | | その WorkflowTemplate から Workflow を作る | |
| `image` | | | | 利用者のイメージのコンテナ | |
| `event` | 作れない(E050) | 何も呼ばず、ワークフローに名前で送られてくる値を待つ(Update `dandori.event`) | 作れない(E050) | 作れない(E050、まだ) | 作れない(E050、まだ) |
| どれも無い | 作れない(E050) | 利用者が書くアクティビティ(`OwnTasks`) | 利用者が書く実装を step の中で呼ぶ(`OwnTasks`) | 作れない(E050) | 利用者が書く関数(`OwnTasks`) |

pydantic-graph では、ほかのプラットフォームで子ワークフローとして呼ぶタスクも、利用者が書く関数になる。

Temporal、durable functions、Argo、pydantic-graph で dandori が書く実装は、Step Functions が送るのと同じものを送る。Lambda の payload、HTTP のメソッド・URL・ヘッダ・本文、AWS の API の引数は、五つのプラットフォームで一字も違わない(5 章で突き合わせる)。送り方は `Transport`(`io.ts`、Python では `io.py`)を通すので、認証のヘッダ、AWS SDK のクライアントの設定、テストでのスタブは利用者が渡す。既定の `Transport` は `fetch` と AWS SDK for JavaScript v3 を使い、SDK は使うときに読み込む。既定で知っている AWS のサービスは 13 で、ほかのサービスは利用者の `Transport` が呼ぶ。

`callback` のタスクは、呼び出し方が `lambda` か `aws sqs:sendMessage` か、どれも無いもの。ID を渡す部分は、どの出力先でも一回 60 秒までで、`timeout` は答えの待ちの長さになる(Step Functions では、渡すことと待ちを合わせて `timeout` まで。17 章)。応答の戻り方はプラットフォームごとに違う。

- Step Functions:`.waitForTaskToken` で、トークンを `task_token` として payload に入れる。SQS ではメッセージの本文(レコードか `json` の `MessageBody`)に入れる。結果は `SendTaskSuccess`。
- Temporal:送る側(アクティビティ)に `callback_id` を渡し、結果は Update `dandori.answer`(`client.ts` の `answer`)で `{ callback_id, ok }` か `{ callback_id, error, message }` として受け取る(シグナル `dandori.callback` でも受け取る)。どのワークフローに送るかは `callback_id` から分かる(`io.workflowOf`)。
- durable functions:`createCallback` の ID を `callback_id` として渡し、結果は `SendDurableExecutionCallbackSuccess`。
- Argo:送る側のコンテナに `callback_id` を渡し、そのあとの suspend のステップで待つ。結果は `argo node set` で待ちのステップの出力 `answer` に入れ、`argo resume` で進める(4.4)。
- pydantic-graph:送る側の関数に `callback_id` を渡し、結果は同じプロセスの中で `Deps.callbacks.answer(callback_id, …)` として受け取る(4.5)。

`timeout until <引数>` を書いた `callback` のタスクは、渡された時刻まで答えを待つ。期限の引数も、ほかの引数と同じく ID と一緒に送る(答える人に期限を見せられる)。`aws` のタスクの引数は API の引数なので、`aws sqs:sendMessage` のコールバックには書けない(E007)。期限を過ぎてからのリトライはすぐまたタイムアウトするので、`timeout` をリトライする `retry`(`on` を書かないものも)も書けない(E007)。出力先ごとの待ち方は 16 章。

`event` のタスクは何も呼ばない。ワークフローは、そのタスクの名前のイベントが、ワークフロー ID を宛先にして送られてくるのを待つ(Temporal の Update `dandori.event`。`client.ts` の `send(client, workflowId, "<名前>", { ok: 値 })`)。承認の画面や運送会社の通知のように、送る側が注文番号のような業務の ID しか知らないときに、コールバックの ID を先に渡さずに済む。

```
# The carrier tells the workflow of the order what became of it, as often as it has news.
task 配達の知らせ() -> 注文
  event
  observes
  timeout 7 days
```

- 引数は、`timeout until` に書く時刻の一つのほかは取らない(何も送らないので)。呼び出し方、`callback`、`retry`、`key`、案件を始めることやイベントを送ることも書けない(E007)。結果の型、宣言したエラー(送る側がエラーの名前を送る)、`observes`、`timeout` と `timeout until` は、コールバックと同じに書ける。`timeout` が無ければ、コールバックと同じく一日でタイムアウトする。
- ワークフローは、待っているあいだのイベントだけを受け取る。待っていないイベントと、もう来たイベントは、Update の validator が拒否し、送った側に `WorkflowUpdateFailedError` が返る。送る側は、ワークフローが待つようになってから送り直す。いま何を待っているかは、クエリ `dandori.status` の `events` が返す。
- 並列のイテレーションの中では待てない(E009)。どのイテレーションも同じ名前のイベントを待つことになり、送る側がどのイテレーションに宛てるかを言えないからである。
- ほかのプラットフォームでは作れない(E050、P6)。Step Functions と durable functions が実行に値を送れるのは、タスクが渡すトークンや ID を通してだけである。Argo(名前で再開する suspend のステップ)と pydantic-graph(`Deps` に名前で届く値)は作れるはずだが、まだ書いていない。

AWS の API のエラーは、Step Functions では「サービスの接頭辞.例外の名前」になる(`DynamoDb.ConditionalCheckFailedException`)。接頭辞はサービスの名前を大文字にしただけではないので(`dynamodb` は `DynamoDb`、`sesv2` は `SesV2`)、Step Functions の開発者ガイドの一覧(417 のサービス)を `src/aws.rs` に表として持つ。一覧に無いサービスは E007 とする。

### 1.7 エージェント

`agent "<指示>"` のタスクは、引数をモデルに読ませ、タスクの型の値を返させる。問い合わせの文面から種類と注文番号を取り出す、返事を下書きする、といった読むことと書くことをモデルに任せ、決めることは rulec の規則に残す(`examples/inquiry`)。エージェントの応答も、外部のサービスが返すほかの値と同じに扱う。流れが分かれるのは、その値を `match` するところか、規則に渡したところだけなので、P1 は変わらない。

エージェントは OpenAI の Responses API を話すもの(`agent "<指示>"`、`agent openai "<指示>"` と書いても同じ)か、Claude のもの(`agent claude "<指示>"`)。前者は、`url` が無ければ OpenAI に、あれば、そのサーバーに送る(下の Open Responses の項)。例の AWS 版では、読むのが OpenAI、書くのが Claude である。

```
task read_inquiry(text: string) -> Reading
  agent "Read the text of a customer's inquiry, choose its kind, take out the order number if one is written, …"
  model "gpt-5.4-mini"
  connection "arn:aws:events:…:connection/openai/…"
  timeout 60 seconds
  retry 2 times every 10 seconds

task draft_reply(kind: routing.kind, point: string, order_id: string?, within: duration[h]) -> string
  agent claude "Draft the first reply to the inquiry, politely, in three sentences at most. …"
  model "claude-sonnet-5"
  connection "arn:aws:events:…:connection/claude/…"
  timeout 60 seconds
```

- **応答の型を JSON Schema にして、モデルをそれに従わせる。** OpenAI の Structured Outputs の strict の形で書く。レコードのフィールドはすべて必須でほかは許さず、`T?` は null との選択、列挙はその値(範囲を書いた列挙なら、範囲に入る値。1.20)、`timestamp` は Step Functions の Wait が受け付ける形の正規表現、単位の付いた数は単位を説明に書く。Structured Outputs は一番外がオブジェクトであることを求めるので、どの型でも `{"answer": …}` に包む。Claude の構造化出力にも同じ Schema を渡す。この形は Claude も受け付け、`timestamp` の正規表現も、Claude が受け付ける単純なもの(`^…$`、`[0-9]`、`{4}` のような回数、グループ)に収まる(Claude の文書で確かめた)。結果は、ほかのタスクと同じく型で確かめ、外れれば `Dandori.BadResponse` になる。
- **どのプラットフォームでも、モデルには同じものを渡す。** 渡すのは指示、引数を JSON にした文字列(引数は宣言の順)、応答の Schema の三つと、タスクに書いたときだけエフォートで、ほかのモデルの設定は足さない。Step Functions は HTTP Task から Responses API に送り、API キーは EventBridge の接続に置く(HTTP Task の定義には `Authorization` のヘッダを書けない)。ほかのプラットフォームで dandori が書く実装は、`Transport` の `agent` を通し、OpenAI の Agents SDK で走らせる。Agents SDK は GPT-5 系のモデルに推論の強さなどの設定を自分で足すので、既定の `Transport` は、エフォートを書いたタスクには `reasoning.effort` だけの設定を、ほかのタスクには空の設定を渡して、Step Functions と同じリクエストにしている。Agents SDK が使う OpenAI のクライアントは、既定では失敗を二度までリトライする(走らせて確かめた。500 を返すと三回送った)。既定の `Transport` はリトライしないクライアントを渡し、リトライはタスクの `retry` だけにする(Step Functions の HTTP Task も自分ではリトライしない)。API キーは `OPENAI_API_KEY` から読む。ほかのモデルのプロバイダーを使うときは、Agents SDK の実行の設定を `Transport` に渡す。
- **Open Responses を話すほかのサーバーにも送れる(`url`)。** Open Responses は、OpenAI の Responses API を、特定の会社に寄らない仕様として書き出したものである(2026 年 1 月。OpenAI・Hugging Face・OpenRouter・Ollama・vLLM・LM Studio・Vercel が加わり、Anthropic は加わっていない)。dandori が OpenAI に送るリクエスト(`model`・`instructions`・`input`・`text.format` の `json_schema`)は、この仕様の形そのものなので、送り先を変えればそのまま通る。タスクに `url "<ベース URL>"` を書くと、`<ベース URL>/responses` に送る。
  - Step Functions では、HTTP Task の送り先が変わるだけである。ほかのプラットフォームで dandori が書く実装は、SDK を通さず、Step Functions と同じ本文を HTTP(`fetch`、urllib)で送る。SDK は仕様に無いものを送ることがあり、それをどのサーバーも受け付けるとは限らないからである。
  - 結果は Step Functions の JSONata と同じく、メッセージの最初の `output_text` から読む。それが無ければ(拒否のときも)`failure`、エラーのステータスも `failure` で、リトライしない(`AgentStopped`・`AgentHttpError`)。
  - サーバーの鍵は、HTTP のタスクと同じく `Transport` の `headers` で足す。OpenAI の鍵をほかのサーバーに送ることはない。
  - サーバーが受け付ける Schema の大きさは分からないので、OpenAI の上限(E007)は OpenAI に送るときだけ当て、ほかは応答の検査に任せる。
  - Step Functions の HTTP Task は HTTPS の API しか呼べない。非公開の API でも、公開のドメイン名と広く信頼された証明書が要る。`http://` の送り先は E050 である(ふつうの `http` のタスクも同じにした)。EventBridge の接続はいつも鍵を持つので、鍵の要らないサーバーでも何かを入れる。
  - Claude のエージェントには `url` を書けない(E007)。Anthropic は Open Responses に加わっておらず、Ollama の Anthropic 互換の `/v1/messages` は `output_config` の `effort` しか読まず、応答の形(`format`)を読まない(ソースで確かめた)。
  - Ollama(0.32.13)は、`text.format` の `json_schema` で生成そのものを縛る。文書の項目の一覧に `text` は無いが、ソースでは受け付けている。走らせると、列挙に無い値で返すよう指示しても、応答は列挙の値にとどまった。Schema を外すと、ただの文章で返した。
- **Claude には、同じ三つを Messages API の形で渡す。** 指示はシステムプロンプトに、引数の JSON の文字列は user のメッセージ一つに、Schema は `output_config.format` に入れる。Messages API は応答の長さの上限(`max_tokens`)を必ず求めるので、どのプラットフォームも同じ 16,000 を送る。モデルが考えるのに使うトークンもこれに含まれる。ヘッダは `anthropic-version: 2023-06-01`。Step Functions の API キーは、EventBridge の接続に `x-api-key` のヘッダとして置く。ほかのプラットフォームで dandori が書く実装は、Anthropic の SDK(`@anthropic-ai/sdk`、Python では `anthropic`)で送り、API キーは `ANTHROPIC_API_KEY` から読む。この SDK も既定で失敗を二度までリトライするので、既定の `Transport` はそれを切る。
- **エフォート(`effort <レベル>`)は、書いたときだけ送る。** モデルが応答する前にどれだけ推論するかを、プロバイダーの API が受け付ける場所に入れる。OpenAI と Open Responses のエンドポイントには Responses API の `reasoning.effort`(`none`・`minimal`・`low`・`medium`・`high`・`xhigh`・`max`)、Claude には Messages API の `output_config.effort`(`low`・`medium`・`high`・`xhigh`・`max`)。レベルの一覧は、既定の `Transport` が使う SDK の型(Agents SDK の TypeScript 0.18.0 と Python 0.22.3、Anthropic の SDK の TypeScript 0.128.0 と Python 1.8.0)から取った。プロバイダーが受け付けないレベルと、エージェントでないタスクの `effort` は E007。どのプラットフォームも同じ場所に同じレベルを入れるので、Step Functions のリクエストとの突き合わせがそのまま効く。
  - 推論するかどうかはモデルごとに違い、検査では分からないので、モデルとの組み合わせはエラーにしない(P5 と同じく、動かしたときに分かる)。推論しないモデルにエフォートを送ると、その呼び出しは失敗する。Ollama(0.32.13)は、推論しないモデルへの `reasoning` を「does not support thinking」として 400 で拒否した。
- **Claude の応答の列挙は、大文字と小文字を区別せずに読む。** Claude の構造化出力は、列挙の値の大文字と小文字を守らないことがある(Claude の文書にそう書かれ、エラーも特別な終わり方も無く返る)。そこで、Claude のエージェントの応答にある列挙の値は、宣言した値と大文字と小文字だけが違えば、その値として読む。Step Functions では応答を読む JSONata の式で、ほかのプラットフォームでは dandori が書く実装(`io.fold`)で直す。大文字と小文字だけが違う値を並べた列挙を応答に含むと、どちらの値か決められないので E007。
- **エージェントは外部のデータを何も変えない。** だから `key` は書けず(E007)、`key` なしでリトライしても E030 にならない。ツールを持たせないので、読んで応答する一往復で終わる。
- **エラーは宣言しない**(E007)。モデルが拒否したとき、応答を読めなかったとき、呼び出しが失敗したときは `failure`、時間を過ぎたときは `timeout` になる。Claude では、応答が一つの手番として終わらなかったとき(`stop_reason` が `end_turn` でないとき。拒否と、上限で切れた応答)も `failure` になる。拒否も `failure` なので、`retry` でリトライでき、`on failure` で処理できる。
- `callback` にはできず、案件に何かするタスク(`starts`・`sends`・`observes`)にもできない(E007)。応答の型を書かないエージェントも E007。
- 応答の型が Schema に書けないもの(`json`、ほかのレコードを通して自分を含むレコード)と、プロバイダーの受け付ける大きさを超えるものも E007 にする。OpenAI の Structured Outputs は、オブジェクトのネストが 10 段、プロパティが 5,000 個、列挙の値が 1,000 個まで。Claude の構造化出力は、null との選択(`anyOf`)が一つのリクエストの中で 16 個までなので、`T?` が 16 個を超える応答は書けない。Claude には文書に数の無い上限(Schema から作る文法の大きさ)もあり、これは送るまで分からない。
- プロバイダーは `openai` と `claude` だけで、ほかの名前は E007。`url` の誤り(エージェントでも判断のタスク(1.11)でもないタスクの `url`、Claude の `url`、http:// でも https:// でもない `url`)も E007。

### 1.8 並列のイテレーション

`for … in parallel` の各イテレーションは同時に回る。それでも五つのプラットフォームで意味がそろうよう、次のように決めた。

- 各イテレーションは自分の変数を持つ。イテレーションの中で値を入れる変数は、そのイテレーションのものになり、イテレーションの外の変数に値を入れることはできない(中と外の両方で値を入れる名前は E009)。イテレーションの外の変数は読める。案件は動かせない(E009)。`break` と `succeed` は書けない(E009)。
- あるイテレーションが失敗しても、ほかのイテレーションは最後まで回る。すべてのイテレーションが終わってから、失敗したイテレーションがあれば、リストの順で最初のイテレーションの失敗でループが失敗する。タスクのエラーならそのあと `on failure` が走り、`fail` や結果の検査の失敗ならそのまま終わる。どのイテレーションが先に失敗したかはプラットフォームによって違いうるので、時間の順ではなくリストの順で決める。
- `yield` を集めたリストは、リストの順に並ぶ。

Step Functions の Map は、どれか一つのイテレーションが失敗すると Map ごと失敗してほかのイテレーションを止める。そこで、イテレーションの中のエラーはイテレーションの中ですべて処理し、イテレーションは「yield した値」か「どう失敗したか」を結果として終わる。Map のあとで、その結果の並びから最初の失敗を探す。

### 1.9 ほかの `.flow` を子として走らせる

子ワークフローも dandori で書いたなら、タスクに `flow "<パス>"` と書く(パスは親の `.flow` のある場所から)。検査は子の `.flow` を、それだけのときと同じに検査し、タスクをその入力・出力・失敗に合わせる(E015)。子が検査を通らなければ、その一つ目の誤りを添えて E015 にする。

```
task arrange_delivery(order_id: string, carrier: urgency.carrier, recipient: string?, extra: json) -> Delivery
  flow "../arrange_delivery.flow"
  errors NoVan
```

- **引数**:子の入力の名前で渡す。子の入力のうち `T?` でないものは、どれも渡す。子に無い名前の引数は書けない。
- **結果**:子は出力を一つのレコードとして返す。タスクの結果の型はレコードで(`json` でもよく、結果を読まないなら書かない)、そのフィールドは子の出力になければならない(`T?` のフィールドは無くてもよい)。子の出力のうち、タスクが読まないものはかまわない。
- **エラー**:タスクが宣言するエラーは、子の `fail` の名前のどれかでなければならない。宣言しない名前で子が失敗すれば、親では `failure` になる。
- **型は形で比べる。** 二つのファイルは、レコードと列挙をそれぞれ自分で宣言するからである。レコードはフィールドごとに、列挙は値で比べる。値の渡る向き(親の引数から子の入力へ、子の出力から親の結果へ)に、渡す側の列挙の値が受け取る側の値に含まれていればよい。オプショナルな値は、オプショナルな値を受け取るところにしか渡せない。
- **範囲も同じ向きに比べる**(1.3)。渡す側の範囲が、受け取る側の範囲に収まっていなければならない。受け取る側にだけ範囲があるときも E015 である(渡す側は、その外の値を渡しうる)。
- 自分を、直接にもほかのフローを通しても走らせるフローは E015(P4)。検査は、いま検査しているフローの並びを持っていて、子がその中にあれば止める。
- `flow` のタスクは、ほかに何も呼ばない(`lambda`・`http`・`aws`・`agent` と `image` は E007)。`callback`・`event`・`key` も、ほかの子ワークフローと同じく書けない。

呼び出し方は、子の `.flow` から決まる。

- Temporal:子ワークフローの型は子の `<名前>_v<版>` で、タスクキューも同じ名前にする。子の `worker` が待つキューである(4.2)。`workflow "<型>"` を書けば、その型で呼ぶ(キューは `queue` で選ぶ)。
- Argo:子の WorkflowTemplate(名前は子のワークフローの名前から作る。4.4)から Workflow を作り、その `dd_output` を結果として読む。
- Step Functions と durable functions:子を配った先の ARN は `.flow` からは分からないので、`state machine "<ARN>"` と `durable function "<ARN>"` を書く。書かなければ、ほかのタスクと同じく E050。
- pydantic-graph:ほかの子ワークフローと同じく、利用者が書く関数になる(子のグラフを走らせる関数など)。契約の検査は同じにかかる。
- 子の `fail` の名前を宣言したエラーとして処理できるのは、Temporal(子の `fail` は、その名前の `ApplicationFailure` で子ワークフローを終える)と durable functions(`ErrorData` に名前が入る。4.3)。Step Functions と Argo では、子のエラーを宣言したタスクは、これまでどおり E050。

### 1.10 API の記述

呼ぶ API に記述があれば、`use` で読み、タスクをそれに合わせる(E016)。

```
use openapi stripe from "specs/stripe.json"
use smithy sns from "specs/sns.json"
use proto warehouse from "specs/warehouse.proto"
  url "https://warehouse.example.com"

task confirm_intent(intent: string) -> PaymentIntent
  http POST stripe "/v1/payment_intents/{intent}/confirm"

task notify(TopicArn: string, Message: string) -> Sent
  aws sns:publish
  errors no_recipient = NotFoundException

task reserve_stock(sku: string, quantity: int) -> Reservation
  connect warehouse "StockService/Reserve"
  errors busy = resource_exhausted
```

- **OpenAPI**(OpenAPI 3 の JSON):`http <メソッド> <API> "<パス>"` は、記述のその操作を呼ぶ。URL は記述の最初のサーバー(`use` の下に `url` を書けば置き換わる)とパスで、本文を URL エンコードするかは記述が言う(`form` は書かない。書けば E007)。
- **Smithy**(AWS が公開している API モデルの JSON AST):`aws <サービス>:<操作>` は、サービスの名前で `use smithy` した記述があれば、その操作に合わせる(`publish` は `Publish`)。
- **protobuf**(proto3 の `.proto`):`connect <API> "<サービス>/<メソッド>"` は、そのメソッドを Connect のプロトコルで、JSON で呼ぶ。`.proto` はサービスの場所を言わないので、`use proto` の下に `url` を書く(`connect` で呼ぶのに無ければ E016)。型を作るだけ(1.12)や、サービスを実装するだけ(1.14)なら、`url` は要らない。

合わせるもの。

- **引数**:操作が受け取らない名前は書けない。操作が必ず要るものは渡す(オプショナルな値では足りない)。型と範囲は、値の渡る向きに比べる(1.9 と同じ)。GET と DELETE では、パスに入らない引数をクエリの引数と比べ、ほかでは本文と比べる。
- **結果**:タスクの結果の型は、操作のレスポンスの形に合わなければならない。レコードのフィールドはレスポンスにあるもので、レスポンスが省くことがあるもの(必須でないもの、null のことがあるもの)は `T?` にする。列挙は、操作が返しうる値をすべて持つ。レスポンスとレコードの食い違いは、フィールドごとに一つずつ言う。
- **エラー**:OpenAPI では、宣言したエラーのステータスで操作が返すこと(`4XX` や `default` も見る)。Smithy では、宣言した例外が、操作かサービスのエラーにあること。Connect では、エラーを Connect のコード(`not_found` など 16 個)で書き、コードの HTTP ステータスで見分ける(Connect のプロトコルの文書の対応表のとおり)。同じステータスになるコードを二つ宣言すると(`invalid_argument`・`failed_precondition`・`out_of_range` はどれも 400)、ほかの HTTP のタスクと同じく E007。
- **Smithy の `key <引数>`**:その引数が、操作の冪等トークン(`@idempotencyToken`)であること。

記述ごとの読み方。

- OpenAPI:`$ref` は、記述の中を指すものだけを読む。`anyOf` と `oneOf` は、送る値はどれか一つに合えばよく、レスポンスはどれに合っても読めなければならない。Stripe の展開できるフィールド(`x-expansionResources`。ID か、展開したオブジェクトのどちらか)は、展開を頼まないので ID として読む。YAML の記述は読まない(JSON に直して渡す)。
- Smithy:AWS のモデルは、レスポンスのメンバーが必ずあるとはほとんど言わない。だからレスポンスのメンバーを `T?` にしろとは言わない(無ければ、走らせたときに結果の検査が拒否する)。SQS の `sendMessage` の `MessageBody` には、レコードや `json` を渡せる(Step Functions も dandori が書く実装も、JSON の文字列にして送る)。コールバックのタスクの応答は、コールバックが持ってくるものなので、操作のレスポンスとは比べない。
- protobuf:レスポンスの JSON の名前は、フィールドの名前を lowerCamelCase にしたもの(`json_name` があればそれ)なので、タスクの結果のレコードもその名前で書く(別の名前なら、JSON の名前を示して E016)。送るときは、フィールドの名前でも JSON の名前でもよい(protobuf の JSON を読む側はどちらも読む)。64 ビットの整数は JSON では文字列で来るので `string` で読み、送るときは `string` でも `int` でもよい(protobuf の JSON を読む側は、文字列も数も受け付ける)。小数は dandori に型が無いので `json` で読む。ストリームのメソッドは呼べない。32 ビットの整数のフィールドの範囲は、Protovalidate のオプション(`buf.validate`)から読み、OpenAPI の `minimum` と `maximum` と同じく、引数と結果の範囲と比べる(1.12)。`import` は、ファイルのディレクトリが自分の package の形(`shop/v1`)で終わっていればその上(buf のモジュールのルート)から、次にファイルのディレクトリから探す。Google の well-known types、`buf/validate/validate.proto`、dandori のオプション(`dandori/v1/options.proto`、1.14)は、ファイルが無くても知っている。それ以外の import がディスクに無いとき(`google/api/annotations.proto` のように、オプションのためだけに import するファイルが多い)は、`use proto` ごとエラーにはせずに飛ばし、読めなかったファイルを記録する。そこにある型は分からないので、フローが書いた型(E002)やメソッドとタスクを合わせるところ(E016)でそれに当たったときに初めて、診断に「`google/api/annotations.proto` を読めなかったので、そこにある型は使えません」と添える。
- 読むのは、タスクの型がたどるところだけで、記述の全体を型にはしない。8 MB ある Stripe の記述でも、見るところだけの手間で済む。

**protobuf の JSON が省く既定値。** proto3 のフィールドのうち、値があるかどうかを持たないもの(`optional` でも、メッセージでも、`oneof` の一つでもないもの)は、既定値(空の文字列、0、false、列挙の最初の値、空のリストとマップ)のとき JSON から省かれる。protobuf で読む側は、省かれたフィールドを既定値として読む。dandori も同じに読む。`connect` のタスクのレスポンスは、省かれた(または null の)フィールドを既定値で埋めてから型を確かめる。レスポンスの中のメッセージと、メッセージのリストの中でも埋める。埋める表は `.proto` から作って生成するコードに書き込み、Step Functions ではレスポンスを読む JSONata の式で、ほかでは `io.fill` で埋める。値があるかを持つフィールドは埋めない(タスクの結果では `T?` にする)。ただし、`json` になるフィールド(`Struct` や `Value`。値が設定されていない `Value` も JSON から省かれる)は `T?` にせず、省かれていても null でも、null として読む(1.2)。列挙の最初の値(慣わしでは `…_UNSPECIFIED`)は、タスクの列挙に無くてもよい。そのときは、その値の結果を結果の検査が `Dandori.BadResponse` で拒否する。

**Connect の呼び出し。** POST で `<url>/<パッケージ>.<サービス>/<メソッド>` に、JSON の本文とヘッダ `Connect-Protocol-Version: 1` を送る。Step Functions の HTTP Task も、dandori が書く実装も同じものを送る(5 章で比べる)。`key` は、ほかの HTTP のタスクと同じく `Idempotency-Key` ヘッダで送る。

### 1.11 判断のモデル(Jev、System One の API のサーバー、OpenAI の Decisions API)

`jev` のタスクは、判断のモデルに型の付いた質問をする。送り先は三つある。TypeSafe の Jev(下)、Jev と同じ形の API(System One の API)を話すほかのサーバー(`url`。Ollama など)、OpenAI の Decisions API(`jev openai`)である。後の二つは、この節の終わりの「同じ種類の API のほかの送り先」に書く。

TypeSafe AI の Jev(2026 年 9 月 15 日に早期アクセス)は、判断のためのモデルで、文章を書かない。渡した入力(state)について型の付いた質問に答え、答えごとに確信度を返す。質問は三種類ある。choice は選択肢から一つを選び、選択肢ごとの確率と確信度を返す。score は順序のある段階(2〜10)のどこかを答え、段階の番号の期待値、段階ごとの確率、確信度を返す。noul は、はいの確率だけを返し、確信度は無い。一回のリクエストで複数の質問に並列に答える。送り先は `POST https://api.typesafe.ai/v1/systemone` で、キーは `Authorization: Bearer <キー>`。

**決定**:Jev をタスクの呼び出し方(`jev`)として足す。プラットフォームではない。`.flow` の分岐は、規則かタスクが返した列挙・bool・オプショナルな値の `match` だけで(P1)、Jev が答えるのはちょうど列挙と bool なので、Jev の答えはそのまま分岐に使え、P1 は変わらない。例は、問い合わせの種類を選ぶところ(`examples/inquiry`)と、審査の採点(`examples/review`)。

```
task pick_kind(text: string) -> routing.kind
  jev "Which kind of inquiry is this?"
    returns "The customer wants to send an item back or exchange it"
    …
  model "jev-1.13.0"
  confidence 0.8 else unsure

task score(purpose: string) -> Score
  jev
    verdict score "How clearly is the money for running the business?"
      reject "It is for something personal or speculative, or against the law"
      …
    sure confidence of verdict
  model "jev-1.13.0"
```

- **結果の型が質問になる。** 列挙は choice(`jev "<質問>"`。下に値ごとの意味を書き、書かない値は意味なし(null)で送る)、`jev score "<質問>"` は score(列挙の値を低い段階から並べ、すべてに意味を書く)。範囲を書いた列挙は、範囲に入る値の中から尋ねる(1.20)。bool は noul(`true` と `false` の意味を、両方書くか、どちらも書かない)。レコードの結果は `jev` だけを書き、フィールドごとに質問を書いて、一回のリクエストで全部を尋ねる。文字列・数・リスト・オプショナルな値は Jev が答えられないので E007。質問の ID は、フィールドの名前か、結果がその値そのものなら `answer`。引数は、引数の名前をキーにしたオブジェクトにして state に渡す(エージェントの入力と同じ順)。score の段階の意味を省けないのは、Jev が段階を意味だけで見分け、番号を見ないからである(TypeSafe の文書)。
- **答えの読み方は、どのプラットフォームでも同じ式にした。** choice は選んだ値、score は `score`(段階の番号の期待値)にいちばん近い段階、noul は、はいの確率が 0.5 を超えれば true。TypeSafe の文書が「一つの結果が要るなら近い段階に丸める」と書いているので、確率の最大の段階ではなく `score` を丸めた。丸めは `floor(score + 0.5)` で、ちょうど真ん中は上の段階になる。JSONata の `$round` と Python の `round` は偶数に丸めるので使わない。答えが無い、選択肢に無い値、確信度が無いか 0〜1 でないときは、その質問の値を null にし、結果の型の検査で `Dandori.BadResponse` になる。
- **確信度。** choice と score には、TypeSafe が確率の散らばりから計算した `confidence` がある。noul には無いので、選んだ答えの確率(はいなら p、いいえなら 1 − p)を確信度とする。TypeSafe の例も noul の値を 0.8 と 0.2 で三つに分けており、同じ意味になる。
- **確信度の下限は、宣言したエラーにする。** `confidence 0.8 else 迷い` と書くと、答えがすべてそろっていて、どれかの確信度が 0.8 より小さいとき、呼び出しは `迷い` で失敗する。この行が `迷い` を宣言する。フローは `on 迷い` で処理し(`on failure` も受ける)、数を比べないまま、TypeSafe の勧める「確信度で行き先を変える」形が書ける。確信度が足りない答えは変数に入れない。Step Functions では、Task の出力を Choice で確かめてから Pass で変数に入れる。Task の Assign で入れると、`on 迷い => pass` のあとに、前の値ではなく足りない答えが残ってしまう。Temporal などでは、答えを読むアクティビティがエラーを投げるので、変数には何も入らない。尋ね直してもほぼ同じ答えが返るので、`retry … on 迷い` は E007。bool だけの結果は確信度がいつも 0.5 以上なので、0.5 以下の下限も E007。
- **行動ごとのしきい値は規則に任せる。** 確信度はフィールドで受け取れる(`<フィールド> confidence of <フィールド>`、型は `rate[step <n>%]` で、刻みは 100% を割り切るもの)。値は刻みの個数で、切り捨てる(1% 刻みなら 0.87 は 87)。0.29 × 100 が 28.999… になる浮動小数の誤差を吸収するため、10 億分の 1 を足してから切り捨て、どのプラットフォームも同じ式にした。rulec の率も刻みの個数でやりとりするので、そのまま規則に渡せ、「そのまま承認するのは 90% 以上、却下は 80% 以上、あとは人」のような表に抜けが無いことを rulec が証明する(審査の例)。rulec の certificate は率の型を `rate` としか書かないので、刻みは schema の説明(「100% is 100」)から読み、`rate[step 1%]` の形にする。`.flow` に書いた `rate[step 0.01%]` も同じ形に直すので、型が合う。
- **モデルはバージョンで書く。** TypeSafe の文書は、確信度のしきい値を合わせたらバージョンを固定するよう勧める。確信度を使うタスク(下限か、確信度を受け取るフィールドがある)がエイリアス(`jev-latest`、`jev-preview`)を書いたら W032。
- **どのプラットフォームも同じ HTTP の呼び出しで、SDK は使わない。** Step Functions は HTTP Task で送り、キーは EventBridge の接続にヘッダ `Authorization` として置く(無ければ E050)。ほかのプラットフォームで dandori が書く実装は、`Transport` の `http` を通す(`HttpRequest` の `typesafe`)。既定の `Transport` は、`TYPESAFE_API_KEY` か `typesafe` のオプションからキーを足す。`Transport` に新しいメソッドを足さなかったのは、利用者が自分で書いた `Transport` がそのまま使えるようにするためである。答えを読むのは、Step Functions では Task の出力の JSONata、ほかでは `io.jev`。JSONata は型の違う値を大小で比べるとエラーになるので、数を比べる前に型を確かめる。エラーはステータスで宣言する(429 はレート制限、529 は過負荷)。ステータスの無いエラーは E007 で、宣言していないステータスは `failure`。`key`、`callback`、`effort`、案件に何かすることも E007。`url` は、同じ形の API を話すほかのサーバーに送るときに書く(この節の終わり)。
- **シナリオの Jev はスタブが答える。** シナリオは、Jev の呼び出しに API リファレンスの形の答えを返す。確信度は、下限ちょうど(境目の `<` と `<=` の違いが出る)と、下限をわずかに下回るもの。型に合わない答えも返す。確信度が足りない答えには、変数にありそうな値(列挙の最初の値、true)と違う値(最後の値、false)を入れ、試験用のフローは、そのあとの呼び出しで変数を送る。こうして、足りない答えを変数に入れてしまう誤りが食い違いとして出る。比較の `<` を `<=` に変える誤りと、Step Functions で Task の Assign に入れる誤りを入れてみて、どちらも突き合わせで捕まることを確かめた(2026-09-29)。数は、Jev が返すような小数第 4 位までにして、どの言語でも同じ double に読めるようにし、dandori も serde_json の `float_roundtrip` で読む(それが無いと、ほかの言語が書いた 0.20500000000000002 を隣の double に読んでいた)。
- **本物の Jev にも送る。** `TYPESAFE_API_KEY` があれば、例と試験用のフローの Jev のタスクを一つずつ、TypeScript と Python と Go の既定の `Transport` から TypeSafe に送り、答えがタスクの型に読めることを確かめる(`jev_tasks_answer_on_typesafe`。キーが無ければ SKIP。Go は 2026-10-02 から)。2026-09-29 には 18 のタスクを二つの言語から送り、36 回すべてが型に読めた。一回は 200〜500 ms で、入力は 400 トークン前後だった。同じリクエストでも答えは少しぶれる(審査の score は 0.87〜0.89、確信度は 0.78〜0.81)。実際の文でも試した。種類のはっきりした問い合わせは、英語でも日本語でも確信度 0.99 以上で正しい種類になった。「靴のサイズが合わないので、代金を返してほしいです」は請求 0.52・返品 0.48 で確信度 0.36(英語の同じ内容は returns で 0.96)で、`confidence 0.8` によってエージェントの読み取りに回る。審査の「半分はオーブン、半分は家族旅行」は却下 0.47・承認 0.38 に割れて確信度 0.00、score は 0.91 で保留になり、規則が人に回した。

#### 同じ種類の API のほかの送り先(2026-10-08)

Jev と同じ種類の API が二つ出た。

- **System One の API のほかのサーバー。** Ollama は 0.35 から、Jev の API と同じ形(System One の API)を `POST <サーバー>/v1/systemone` で受け、手元の判断のモデル(`nimble` の 9B、`tev1` の 4B と 0.8B)が答える。キーは要らない。リクエストと答えの形は Jev と同じで、質問は 64 個まで、score の段階は 2〜26、リクエストは画像なしで 64 KiB まで(Ollama の API の文書)。確信度は「分布の集中の度合い(1 − H(p) / ln N)で、正しさの確率ではない」と文書が書いている。
- **OpenAI の Decisions API。** `POST https://api.openai.com/v1/decisions`、キーは `Authorization: Bearer <キー>`。公開ベータで、モデルは `gpt-6-luna` だけ(バージョンを固定する名前は無い)。入力は `input`(文字列か、ユーザーのメッセージの配列)で、質問は名前(`name`)を付けた配列、答えも質問の順の配列で、それぞれが自分の名前を返す。質問の型は choice(`choices` は値と説明の組で 2〜255 個)、score(`levels` はラベルと説明の組)、predicate(はいの確率 `probability`。確信度は無い)。どの質問にも、答えの代わりに拒否(`{"type": "refusal", "name": …}`)が返りうる。課金は入力だけ(100 万トークンで $0.10)。上限とエラーのコードは文書に無い。

**決定**:キーワードは `jev` のままにし、`agent` のタスクと同じ作りで二つを足す。`url "<ベース URL>"` は System One の API を話すほかのサーバーに `<ベース URL>/systemone` で送り、`jev openai` は Decisions API に送る(`url` を書けば、Decisions API を話すほかのサーバーの `<ベース URL>/decisions` に)。ベース URL は、エージェントの `url` と同じく `/v1` までを書く。`jev typesafe "…"` は `jev "…"` と同じで、ほかの名前は E007。どの送り先も、答えの型から質問を作り、同じ式で読み、確信度の下限と確信度のフィールドもそのまま使える。フローの分岐は、これまでどおり答えの `match` と規則だけである(P1)。

```
task pick_kind(text: string) -> routing.kind
  jev "Which kind of inquiry is this?"
    returns "The customer wants to send an item back or exchange it"
    …
  model "nimble:9b-q4_K_M"
  url "http://ollama.internal:11434/v1"
  plaintext "The model server is reached only inside the cluster network, which the service mesh encrypts"
  confidence 0.8 else unsure

task read(text: string) -> Reading
  jev openai
    kind "Which kind of inquiry is this?"
      …
    urgency score "How urgent is it for the customer?"
      …
    person "Does the customer ask to talk to a person?"
    sure confidence of kind
  model "gpt-6-luna"
  confidence 0.6 else unsure
  refusal else declined
  connection "arn:aws:events:ap-northeast-1:123456789012:connection/openai/9c0d1e2f"
```

- **質問の対応。** 列挙の choice はどちらの API でも choice、`jev score` は score、bool は System One では noul、Decisions API では predicate。Decisions API の choice の `choices` は値と説明の組(説明を書かない値は `description` を省く)、score の `levels` は、列挙の値を `label` に、段階の意味を `description` にする。レコードの結果は、どちらも一回のリクエストで全部を尋ね、Decisions API では配列の `name` がフィールドの名前(結果がその値そのものなら `answer`)になる。Decisions API の predicate は、はいといいえの意味を受け取らないので、`true` と `false` の意味を書くと E007 にした(尋ねることの中に書く)。
- **入力。** System One の API には、いまどおり引数の名前をキーにしたオブジェクトを `state` に渡す。Decisions API には、引数を JSON にした文字列を `input` に渡す(エージェントの入力と同じ文字列。引数の順で、空白を入れない)。Go の `encoding/json` は map のキーを並べ替えるので、Go では引数の順に書く関数(`ddInputText`)を生成する。画像は入れない(7 章)。
- **Go も、宣言の順で送る。** System One の API の `state` と、質問の選択肢(`criteria` のキー)も、Go の map では名前の順になっていた(Jev を足したときから)。判断のモデルは選択肢を並んだ順に読むので、順が違うと答えも少し違う。実際、同じ問い合わせを Ollama の `tev1:0.8b` に尋ねると、TypeScript と Python から送ったものは確信度 0.81、Go から送ったものは 0.87 だった(2026-10-09)。そこで Go の生成コードは、`state` を引数の順の JSON の文字列に、質問を、ほかの言語と同じ JSON の文字列(`ddJevQuestions`)にして、そのまま送る(`json.RawMessage`)。ほかの HTTP の呼び出しのキーの順は、これまでどおり名前の順のままである(意味は同じ JSON で、読むのはプログラムなので)。
- **答えの読み方は同じ式。** choice は選んだ値、score は `floor(score + 0.5)` の段階、はいかいいえは確率が 0.5 を超えれば true で、その確信度は選んだ側の確率。Decisions API の答えは、配列の中を質問の名前で探す。はいの確率は、System One では `noul`、Decisions API では `probability` にある。答えが無い、知らない値、確率や確信度が 0〜1 でないときは null にし、結果の検査で `Dandori.BadResponse` になる。
- **拒否は、宣言したエラーにする。** `refusal else <エラー>` と書くと、Decisions API がどれかの質問を拒否したとき、呼び出しはそのエラーで失敗する。この行がエラーを宣言する(`confidence … else` と同じ)。書かなければ、呼び出しは `Dandori.Refused` で失敗し、`on failure` が受ける。拒否は確信度の下限より先に見る(拒否した質問には確信度が無い)。同じ入力で尋ね直しても拒否されることが多いので、確信度の下限と同じくリトライしない(`retry … on <拒否のエラー>` は E007。`Dandori.Refused` も、宣言したエラーと同じく、`on` の無い `retry` の受けるものから外す)。System One の API は拒否を返さないので、System One のタスクの `refusal` は E007。Step Functions では、Task の出力の JSONata が `refused` も返し、確信度より前の Choice で分ける。
- **確信度の意味は、プロバイダーとモデルごとに違う。** TypeSafe は確率の散らばりから計算した値、Ollama は分布の集中の度合い(正しさの確率ではないと文書が書く)で、OpenAI は文書に計算の仕方が無い。同じ文に同じ質問をしても、確信度は送り先で大きく違った(下の本物の答え)。だから、下限やしきい値は、タスクのプロバイダーとモデルに結び付いた値である。行動ごとのしきい値は規則(rulec の表)で決め、送り先かモデルを替えたら、規則の表を実際の答えで合わせ直すことを勧める(サイトの判断のモデルのページ)。
- **W032 は、送り先ごとの「バージョンが動く名前」で出す。** 確信度を使うタスク(下限か、確信度のフィールドがある)が、TypeSafe ではエイリアス(`jev-latest`、`jev-preview`)、`url` のサーバーではタグの無い名前と `:latest`(Ollama の名前の付け方。取り直すと新しいモデルに替わる)、Decisions API ではいつも(バージョンを固定する名前が無い)。Decisions API の W032 は、直す書き方が無いので、確信度の下限とフィールドを実際の答えでときどき確かめ直すよう書いた。
- **上限。** TypeSafe の score の段階の 2〜10 は、`url` の無い System One のタスクにだけ当てる(Ollama は 26 まで、Decisions API は文書に無い)。choice の選択肢の 255 個までと、choice と score の二つ以上は、どの送り先にも当てる(どちらの API の文書も 2 個以上と書く)。ほかの上限は、送るまで分からないので答えの検査に任せる。
- **キー。** 既定の `Transport` は、`HttpRequest` の `typesafe` に TypeSafe のキー(`TYPESAFE_API_KEY` か `typesafe` のオプション)を、`openai` に OpenAI のキー(`OPENAI_API_KEY` か `openai` のオプション)を足す。`url` のサーバーには、どちらのキーも送らず、鍵が要るなら `Transport` の `headers` で足す(エージェントの `url` と同じ)。`Transport` に新しいメソッドは足さない(Jev と同じ理由)。
- **Step Functions。** どの送り先も HTTP Task で送り、キーは EventBridge の接続に置く(`connection` が無ければ E050。鍵の要らないサーバーでも、接続には何かの値が要る)。HTTP Task は HTTPS の API しか呼べないので、`http://` の `url`(この機械の Ollama など)は E050。60 秒を超える `timeout` も E050。
- **E906 と W902。** 秘密の値を送る相手として、Decisions API は「OpenAI(the Decisions API)」、`url` のサーバーはこのマシンでなければそのホストを言う(エージェントの `url` と同じ)。`http://` の `url` がこのマシンでないホストに向くときは W902 で、タスクの下の `plaintext "<理由>"` で受け止める。
- **確かめ方。** `tests/flows/decisions.flow`(日本語の版は `decisions.ja.flow`)が、三つの送り先、choice・score・はいかいいえ、一部の値にだけ意味を書いた choice、三つの答えと確信度の率のレコード、確信度の下限、宣言した拒否と宣言しない拒否(呼び出しで処理するものとしないもの)、結果を読まない呼び出し、ステータスで宣言したエラーのリトライ、W032 の二つの形を、どのプラットフォームでも参照インタプリタと突き合わせる(それぞれ 20 本)。シナリオのスタブは、Decisions API の答えの形(配列、拒否を含む)も返す。TypeScript の `io.jev` から拒否の確かめを外す誤りと、Step Functions の Choice から拒否の分かれ目を外す誤りを入れると、どちらも突き合わせで食い違った(2026-10-09)。診断は `tests/fixtures/decisions.flow` と `decisions_targets.flow` にある。既定の `Transport` の確かめ(`tools/wire`)は、Decisions API に OpenAI のキーが付き、`url` のサーバーにはキーが付かないことも見る。
- **本物にも送る。** Ollama(`decision_tasks_answer_on_ollama`)は、`url` の System One のタスクを、この機械の Ollama の判断のモデル(`DANDORI_OLLAMA_DECISIONS`、無ければいちばん小さいもの。いまは `tev1:0.8b`)に送る。OpenAI(`decision_tasks_answer_on_openai`)は、`OPENAI_API_KEY` があれば、Decisions API のタスクを本物に送る(`url` のタスクも OpenAI に)。どちらも TypeScript・Python・Go の既定の `Transport` から一つずつ送り、答えがタスクの型に読めるか、宣言したエラー(下限か拒否)で失敗することを確かめる。Ollama が無いか判断のモデルが無ければ SKIP、キーが無ければ SKIP。CI にはキーを置かない。2026-10-08 の試験用のフローでは、OpenAI に 12 回送り、どれも型に読めた(一回は 220〜400 ms、最初の一回だけ 3.4 秒)。日本語の質問の名前(`種類`、`急ぎ`、`人`)も、そのまま名前として返った。
- **本物の答え(2026-10-09)。** 問い合わせの例の四つの種類(意味つき)を尋ねた。Ollama の `tev1:0.8b` は「My parcel arrived broken and I want my money back.」を returns 0.59・billing 0.31 で確信度 0.30、「The shoes don't fit, and I want my money back.」を returns 0.60 で 0.43、「靴のサイズが合わないので、代金を返してほしいです」を returns 0.68 で 0.48 とした(`confidence 0.8` では、どれもエージェントの読み取りに回る)。OpenAI の `gpt-6-luna` は、最初の文を delivery 0.51・billing 0.45 で確信度 0.35、日本語の文を billing 0.85 で 0.80 とした。同じ文でも、送り先とモデルで確信度がこれだけ違う。


### 1.12 記述から型を作る

`use proto` で読んだ `.proto` のメッセージと列挙は、`<API>.<名前>` と書けば、そのまま型になる。規則の列挙を `payment_intent.status` と書くのと同じ書き方である。書けるところは、ほかの型と同じ(レコードのフィールド、タスクの引数と結果、入力と出力、`let` の型、`list[…]` と `…?` の中、案件の型)。

```
use proto warehouse from "../specs/warehouse.proto"
  url "https://warehouse.example.com"

task reserve_stock(sku: string, quantity: int) -> warehouse.ReserveResponse
  connect warehouse "StockService/Reserve"
```

**決定**:記述から型を作るのに、記述との突き合わせ(1.10)と同じ表を使う。作った型でタスクを書けば、E016 は何も言わない。

| `.proto` | dandori |
|---|---|
| `string`、`bytes`(JSON では base64 の文字列) | `string` |
| `bool` | `bool` |
| 32 ビットの整数(`int32`・`uint32`・`sint32`・`fixed32`・`sfixed32`) | `int` |
| 64 ビットの整数(`int64`・`uint64`・`sint64`・`fixed64`・`sfixed64`) | `string`(JSON では文字列で来る) |
| `float`、`double` | `json`(dandori に小数の型は無い) |
| 列挙 | 列挙。値は `.proto` の値の名前そのまま |
| メッセージ | レコード。フィールドの名前は JSON の名前(`json_name`、無ければ lowerCamelCase) |
| `repeated T` | `list[T]` |
| `map<K, V>` | `json` |
| 値があるかを持つフィールド(`optional`、メッセージ、`oneof` の一つ) | `T?` |
| `google.protobuf.Timestamp` | `timestamp` |
| `Duration`、`FieldMask` | `string` |
| `Struct`・`Value`・`ListValue`・`Any`・`NullValue` | `json` |
| `Empty` | フィールドの無いレコード |
| ラッパー(`Int32Value` など) | 中の型。`Int64Value` は `string`、`DoubleValue` は `json` |

- メッセージのフィールドは値があるかを持つので、`Timestamp` のフィールドは `timestamp?`、ラッパーのフィールドは中の型の `?` になる。`json` は null も値に含むので、`json` になるフィールドには `?` を付けない。値が設定されていない `Value` は JSON から省かれるが、`json` のフィールドは無ければ null として読むので、結果の検査はそれを通す(1.2)。
- 名前は、`use proto` の名前のあとに、読んだファイルの package から見た名前を書く(`warehouse.ReserveResponse`、入れ子なら `warehouse.Order.Line`)。そのファイルが import した、ほかの package の型は、package から書く(`warehouse.common.v1.Money`)。
- 範囲:32 ビットの整数のフィールドは、Protovalidate のオプションの `gte`・`gt`・`lte`・`lt`・`const` を範囲として読む(`gt: 0` は `>=1`)。書き方は、`(buf.validate.field).int32 = {gte: 1, lte: 100}` のようにまとめても、`(buf.validate.field).int32.gte = 1` のように一つずつでもよい。`repeated` の要素の規則(`repeated.items.int32`)は、リストの中の数の範囲になる。rulec が契約を読むとき(rulec の設計書の 15.132)と同じく、読むのは数の範囲と `required` だけで、ほかの規則(文字列の長さ、リストの件数、CEL)は無いものとして読む。`ignore` が `IGNORE_IF_ZERO_VALUE`(と前の名前の `IGNORE_IF_UNPOPULATED`・`IGNORE_IF_DEFAULT_VALUE`)なら、0 も範囲に入れる。`gt` を `lt` より大きく書いた範囲(その外側)と、数が一つも入らない範囲は、一つの範囲にならないので読まない。64 ビットの整数の範囲も読むが、作る型は `string` なので範囲は付かない。使うのは、`int` の値をそのフィールドに送るタスクの突き合わせだけである。
- `required`:値があるかを持つフィールドに `(buf.validate.field).required = true` があれば、`T?` ではなく `T` にする。突き合わせでも、そのフィールドはレスポンスから省かれないものとして読む。
- 読んだ範囲は、突き合わせにも使う。OpenAPI の `minimum` と `maximum` と同じく、範囲のあるフィールドに範囲の分からない値を送るタスクは E016 になる。

**ゼロ値**:列挙の最初の値(ゼロ値)は、名前が「値が無い」ことを言うときだけ、作る列挙から落とす。列挙の名前から作る接頭辞(`Stock` なら `STOCK_`)を外すと `unspecified`(大文字と小文字は問わない)になる名前である。ほかの名前のゼロ値は、本当の値として残す。

理由。proto3 は、値があるかを持たないフィールドが省かれると、読む側にゼロ値を渡す。慣わしの `…_UNSPECIFIED` は、送る側が何も言わなかったという印で、業務の値ではない。作った型がそれを値に含めると、その列挙で `match` するフローはどれも、来てはいけない値のための分岐を書くことになる。落としておけば、その値が来たときは、型の合わないほかの値と同じく、境界で `Dandori.BadResponse`(入口なら `Dandori.BadInput`)になる(P5)。手で書いた列挙がゼロ値を持たないときのいまの扱い(1.10)と同じで、rulec が `import proto` で列挙を突き合わせるときの決まり(rulec の設計書の 15.59)とも同じである。名前が「値が無い」ことを言わないゼロ値(`STATUS_ACTIVE = 0`)は珍しいが実在し、黙って落とすのはツールがしてはいけないことなので、残す。値が無いこともフローで扱いたければ、ゼロ値を持つ列挙を自分で書けばよい。手で書いた型も、記述と形で突き合わせる(E016)。

**作るのは、フローが書いた型と、そこからたどれる型だけ**:`warehouse.ReserveResponse` と書かれたときに、そのメッセージと、フィールドがたどるメッセージと列挙を作り、ほかの型は作らない。記述の全体を型にしない 1.10 の読み方と同じで、大きな `.proto` でも、使うところだけの手間で済む。メッセージは、名前を先に入れてからフィールドの型を作るので、自分を含むメッセージ(`repeated Nested more` を持つ `Nested`)でも止まる。

**生成するコードでの名前**:規則の型(`urgency.carrier` は `urgency_carrier`)と同じく、`.` を `_` にする(`warehouse_ReserveResponse`、`warehouse_Order_Line`)。TypeScript でも Python でも同じである。その名前が、ファイルで宣言した型や規則の型の名前と重なるときは E006 にする。規則の型と宣言した型はいまも重なりうるので、この検査はすべての型にかける。`dandori doc` の表や診断では、`.flow` に書いたとおり `warehouse.ReserveResponse` と見せる。

**記述に無い名前**は E002 にし、記述のメッセージと列挙の名前を添える(規則の列挙の名前を添えるいまの E002 と同じ)。サービスの名前を型に書いたとき、OpenAPI や Smithy の記述の名前で型を書いたときも E002 で、それぞれそう言う(OpenAPI と Smithy から型を作ることは 7 章)。同じ名前を `use rule` と `use proto` の両方に付けると、`<名前>.<型>` がどちらを指すかが決まらないので E006 にする。値の名前が `none` の列挙も E006 にする。`match` は `none` を、値が無いことの分岐として読むので、その値では分けられないからである。手で書いた列挙の `none` と同じ扱いで、直すには、その列挙を自分で書く。

**自分を含むメッセージは、型にしない**:木のように、フィールド(リストや `T?` を通しても、ほかのメッセージを通してもよい)で自分を含むメッセージは、E003 にする。値は有限でも、シナリオの生成と Argo のジェネレーターは、自分を含む型を辿るところで終わらずにスタックを使い切り(手で書いた `record Node` に `next : Node?` を持たせて確かめた。`dandori scenarios` も `build --target argo` も落ちた)、Step Functions は、型を確かめる式を展開して書くので、自分を含む型は、ある深さから先を確かめない(実行で入ってくる値の型を境界で確かめるという P5 が、そこで崩れる)。型を作るのを止めるだけなら、名前を先に入れるので止まる。それでも、作った型はどのプラットフォームでも同じに運べなければならない。直し方は、自分を含むところを `json` にしたレコードを、自分で書くこと(手で書いた型も、記述と形で突き合わせる。`json` のフィールドはどの形にも合う)。手で書いたレコードの自己参照も、同じ理由で E003 にする(1.2)。

**案件の型**:メッセージから作ったレコードも、案件の型にできる。状態のフィールドは、規則の状態の列挙か、`.proto` から作った列挙で、値(ゼロ値を落としたあと)がちょうどステートマシンの状態であるものにする。`tests/flows/proto_types.flow` の案件は、`order_state.rule` の状態(received・paid・shipped・delivered・cancelled)を値に持つ `.proto` の列挙で状態を運ぶ。`.proto` の値は ASCII の識別子なので、日本語の状態を持つステートマシンの案件は、`.proto` の型では書けない。

**費用**:`.proto` を読むコードが、フィールドのオプションの値も読むようになる(サービスとメソッドのオプションも、1.14 で読む)。作った型は、宣言した型と同じ列挙とレコードなので、流れに沿った検査、ジェネレーター、`doc` は何も変わらない。

### 1.13 規則を Connect のサービスとして呼ぶ

rulec は規則を Connect のサービスとしても出す(rulec の設計書の 15.112)。`rulec gen` が、規則ごとの `.proto` と、その後ろに立つ Python のサービスを書く。

**決定**:`use rule` の下に、`lambda` と並ぶ呼び出し方として `connect "<URL>"` を書けるようにする。書いた規則は、どのプラットフォームでも、そのサービスを呼んで判断する。規則のコードを生成物に同梱する今の形と、規則を共有のサービスとして一か所に置く形を、規則ごとに選べる。rulec には何も足さない。

```
use rule urgency from "../rules/urgency.rule"
  connect "https://rules.example.com"
  connection "arn:aws:events:ap-northeast-1:123456789012:connection/rules/5e6f7a"
```

- 読むのは、規則の口が渡す Connect のサービスの形(`rulec api` の `connect` と同じもの)だけで、`rulec gen` が書く `.proto` は読まない(P2)。そこに、パス(`/rulec.urgency.v1.UrgencyService/Decide`)と、リクエストとレスポンスのフィールド(規則での名前、`.proto` のフィールドの名前、型、`T?` かどうか、列挙ならどの列挙か)と、リクエストとレスポンスに出る列挙ごとの、値の名前と番号がある。
- 送るのは `<URL><パス>` への POST で、ヘッダは `Connect-Protocol-Version: 1`、本文は JSON である。本文の名前はフィールドの JSON の名前(lowerCamelCase)で、数は十進の文字列(rulec は数をすべて `int64` にするので、protobuf の JSON では文字列で書く)、列挙は `.proto` の値の名前、真偽と文字列はそのまま。null の値は送らない(いまの dandori は規則の入力を `T?` にしないので、生成するコードがその場合を持つだけで、起きない)。null でない入力は、ゼロ値(false、0、空の文字列、列挙の 0 番の値)でも省かずに書く。rulec 0.22.0 が生成するサービスは、リクエストのフィールドをどれも `.proto` の `optional` にして、省いた入力をゼロ値と見分け、`invalid_argument` で受け付けないからである(下のエラーの項)。
- 列挙の値の `.proto` での名前は、口が渡す列挙ごとの名前から読む(`rulec api` では `connect.enums`)。フィールドが規則の列挙の名前を指し、その列挙の値の並びが、規則の値ごとに、`.proto` とやりとりする JSON での綴りと番号を言う。`unset` は、0 番が「設定されていない」を意味するときの 0 番の名前(`CARRIER_UNSPECIFIED`)で、このとき 0 番は規則の値ではない。0 番が規則の値の契約(`enum Status { ACTIVE = 0; CLOSED = 1; }` を取り込んだ規則)では、`unset` は null で、`values` に 0 番の値(`ACTIVE`)がある。規則の値のどれかに名前が無いとき、0 番の名前が分からないときは、E005 にする。読んだ名前が `rulec gen` の `.proto` と一致することは、テストで確かめる(5 章)。

  組み立てずに読むのは、組み立てが外れると、黙って別の値になるからである。はじめは、列挙の別名を大文字の snake case にしたもの(`Carrier` なら `CARRIER`、`MemberTier` なら `MEMBER_TIER`)と、値の別名を大文字にしたもの(`next_day` の別名は `NEXTDAY`)を `_` でつないで組み立てていた(`CARRIER_NEXTDAY`)。rulec 自身が宣言する列挙ではそれで合う。しかし `import proto` で取り込んだ列挙の名前は契約が決めるので、buf の lint が求める接頭辞を持たないことがある(`STATUS_ACTIVE` ではなく `ACTIVE`)。rulec 0.21.2 が生成するサービスは、protobuf の JSON を、知らないフィールドと知らない列挙の名前を捨てる設定で読むので、外れた名前はその列挙の 0 番の値として読まれる。0 番が `…_UNSPECIFIED` なら rulec が `invalid_argument` で受け付けないが、0 番が本当の値の契約では、外れた名前が 200 で受け取られ、0 番の値として判断される(2026-10-01 に、rulec 0.21.2 と connectrpc 0.12.1 のサービスで確かめた。0.22.0 のサービスは、同じ名前を 400 の `invalid_argument` で受け付けない。5.1)。
- **0 番の値**:サービスは、0 番の値の列挙を、ほかのゼロ値と同じくレスポンスの JSON から省く。dandori は、省かれた列挙を 0 番の名前で埋めてから読む。`unset` に名前がある列挙では、それは規則の値でないので、結果の検査が `Dandori.BadResponse` にする。`unset` が null の列挙では、0 番の値の名前(`ACTIVE`)で埋め、規則のその値(`有効`)として読む。はじめは、省かれた列挙をいつも `…_UNSPECIFIED` で埋めると決めていたので、0 番が本当の値の契約では、正しい結果を `Dandori.BadResponse` にしていた。送るときも、0 番の値を省かずに名前を書く(上の項)。シナリオは、0 番が規則の値の列挙を、その値のときにレスポンスから省いた形で返し、列挙を省いたレスポンス(拒否されるはずのもの)は、`unset` に名前がある列挙でだけ選ぶ(5 章)。
- **名前を言わない rulec**:前は、`connect.enums` を出さない rulec(0.21.2 まで)のために、rulec 自身が宣言する列挙の値の名前を組み立てて呼び(`CARRIER_NEXTDAY`)、取り込んだ列挙を持つ規則の `connect` を E005 でエラーにしていた。ritsu の中の dandori が読む rulec はいつもワークスペースの rulec で、口はいつも名前を言うので、この読み方は消した(0.3)。
- レスポンスは protobuf の JSON として読む。rulec のサービスは、ゼロ値のフィールドを JSON から省く(2026-10-01 に `rulec gen` のサービスを立てて確かめた。`urgent` が false のレスポンスは `{"carrier":"CARRIER_STANDARD","trace":[…]}` で、`urgent` が無い)。そこで `connect` のタスクと同じく、省かれたフィールドをゼロ値(1.10 で既定値と呼んだもの。ここでは false、`"0"`、`""`、列挙の 0 番の名前)で埋めてから、フィールドを規則の出力の名前に戻す。数は文字列から数に、列挙は規則の値の名前に戻す。規則に無い名前(`CARRIER_UNSPECIFIED` も)はそのまま残し、ほかの規則の結果と同じ検査(型と、rulec の出す範囲)が `Dandori.BadResponse` にする。列挙の表は、表が自分のキーとして持つ名前だけを引く。JSONata の `$lookup` は、オブジェクトが生まれつき持つ名前(`constructor`、`toString`、`__proto__`)まで見つけて、関数や空のオブジェクトを値にしてしまい、サービスのレスポンスは何でもありうるからである(作る途中で、四つの読み方を突き合わせるテストが見つけた。5 章)。十進の数でない文字列も、数に直さずに残して検査に拒否させる。直そうとして例外を投げると、その言語では検査の失敗が呼び出しの失敗になり、プラットフォームによって終わり方が変わるからである。数に直すのは、16 桁までの十進(`-` を付けてもよい)で、2^53 − 1(9,007,199,254,740,991)を超えないものだけにした。JavaScript と JSONata の数は倍精度で、それを超える整数は別の数に丸まる。Rust と Python は正確に読めるので、長い文字列まで直すと、同じレスポンスがプラットフォームによって別の数になる。そこで、これを超える文字列は数に直さず、結果の検査でエラーにする(6 章)。
- `trace`(当たった行)は読まない。一回の呼び出しの記録は、サービスの側が `--record` で残し、それを `rulec replay` と `rulec diff` が読む(rulec の設計書の 15.112)。dandori の実行履歴には、同梱の規則と同じく、規則の名前での入力と出力が残る。

**表のハッシュ**:rulec のサービスは、成功のレスポンスにヘッダ `rulec-source-sha256` を付けて、どのバージョンの表で決めたかを言う(失敗のレスポンスには付かない。同じ日に確かめた)。dandori はこれを読まず、口が渡す規則の SHA-256(検査したときの表。`rulec api` の `source_sha256` と同じもの)とも比べない。検査したのと違う表が立っていても、呼び出しは失敗にしない。

理由は三つ。一つ、規則を一か所のサービスに置くのは、表を直したときに、それを呼ぶワークフローを出し直さずに済ませるためである。バージョンが違えば失敗させると、表のどんな直し(説明の一語でも)も、それを呼ぶすべてのワークフローの走っている実行を止め、共有のサービスにする意味が無くなる。二つ、実行中の案件が途中から新しいバージョンの規則で判定されるのは、Lambda やアクティビティで呼ぶ規則でもいまと同じで(4.6)、そこでもバージョンは確かめていない。代わりに、結果の型と rulec の出す範囲を、結果ごとに確かめている(1.3、P5)。検査が頼る規則の性質(出力の列挙の値、範囲)は、これで結果ごとに守られる。三つ、形が互換かは rulec の契約の側で決まっている。package はメジャーバージョンを持ち(`rulec.urgency.v1`)、パスもそれを含む。同じメジャーの中で形が変わったかは `buf breaking` が言い(rulec の設計書の 15.112)、入力の名前を変えたり、入力を挟み込んだりすることも、そこで互換でない変更として止まる。dandori は、検査した規則のメジャーのパスを呼ぶ。

費用:表が変わったことは、ワークフローの実行履歴からは分からず、サービスの記録(`--record`)で知ることになる。Step Functions では、HTTP Task の結果にヘッダが入るので、実行の履歴にも残る。

**エラーとリトライ**:規則はエラーを宣言しない。サービスのレスポンスが 200 でないとき(Connect のエラーのコードが何であっても)と、送れなかったときは、`failure` にする。リトライは同梱の規則と同じ並び(どのエラーも 2 回、1 秒後と 2 秒後。Temporal ではタイムアウトも)にする。サービスが返すエラーは、宣言した範囲の外の入力(`invalid_argument`)、静的に証明できなかった重なりに実際に当たったとき(`internal`、rulec の W114)、止まっていたり混んでいたりするとき(`unavailable` など)である。前の二つは同じ入力なら何度でも同じになるので、リトライは 3 秒の無駄になるが、規則は純関数なので害は無い。コードでリトライするかを分けると、規則のリトライの並びが、呼び出し方で変わってしまう。検査は、規則の入力に rulec の範囲の外の値を渡させない(E014)ので、`invalid_argument` が来るのは、サービスが検査したのと違う表で動いているときである。

rulec 0.22.0 が生成するサービスは、知らないフィールド、知らない列挙の名前、省いた入力も、範囲の外の入力と同じく `invalid_argument`(HTTP の 400)で受け付けない(2026-10-01 に、公開前のビルドで確かめ、0.22.0 で確かめ直した。5.1)。rulec 0.21.2 のサービスは、知らないフィールドと名前を捨て、省いた入力をゼロ値として読んで、200 を返す。dandori は、口が言うフィールドに、null でない入力を全部、読んだ名前で送るので、検査した規則と同じ規則が立っていれば、どれも起きない。起きるのは、立っている規則の入力の名前や列挙の値が、検査した規則と違うときである。そのとき呼び出しは失敗し、黙ってゼロ値や 0 番の値で判断されることはない。0.21.2 のサービスでは、名前の変更を `buf breaking` が止めることに頼るしかない。

**プラットフォーム**:どのプラットフォームでも、`connect` のタスクの経路を使う(4 章)。Step Functions は HTTP Task で呼び、EventBridge の接続(`connection`)が要る(無ければ E050、HTTPS でなければ E050)。その規則の Lambda 関数のコードは出さない。Temporal、durable functions、Argo、pydantic-graph では、dandori が書く関数が `Transport` の `http` で送る。Temporal ではアクティビティの名前が、同梱の規則と同じ `rule_<名前>` のままなので、同梱からサービスに切り替えても、ワークフローのコードも、履歴に残る引数と結果も変わらず、走っている実行の再生は食い違わない。`local` と一緒に書くと、ワークフローのワーカーの中でサービスを呼ぶローカルアクティビティになる。`lambda` と `connect` は一緒に書けない(E007)。`connect` の無い規則の `connection` も E007 である。

**E040**:一回の呼び出しが実行履歴に残す件数は、同梱の規則を呼ぶときと変わらない。Step Functions では Lambda の Task が HTTP Task になり、結果を読む式は Task の中にあってステートは増えない。Temporal ではアクティビティ一つのまま、durable functions では invoke が step になり(どちらも操作一回)、Argo ではコンテナ一つのまま。だから見積もりは変えない。

**例**:注文の例の AWS 版(`examples/order/aws`)と、その日本語の版で使う。理由は三つ。急ぎの規則は、注文の例と引当と発送の例の両方が判断に使う規則で、一か所のサービスに置く意味がいちばんはっきりしている。AWS 版はどのプラットフォームでも走らせる版なので、例のシナリオで六つのプラットフォームを全部通る(Step Functions では、Lambda の代わりに HTTP Task と接続になる)。Temporal 版は、主なプラットフォームでの既定である同梱のままにして、両方の形を例に残す。`tests/flows/connect_rules.flow` は、日本語の規則を Connect で呼ぶ(5 章)。`tests/flows/connect_rules_contract.flow` は、契約から列挙を取り込んだ規則(`tests/fixtures/rules/account_fee.rule`。契約の値に接頭辞が無く、0 番の `ACTIVE` も状態の一つ)を呼び、省かれた 0 番の値をどのプラットフォームも同じに読むことを確かめる。例の規則には、取り込んだ列挙を持つものが無い。

**並びをたどる規則**:rulec の規則は、要素の並びをたどることがある(`elements 明細(lines)` と、一要素のフィールド)。dandori はその入力(要素のレコードのリスト)を型にしていないので、`use rule` が E005 でエラーにする(`tests/fixtures/rule_lists.flow`)。規則のコードを同梱しても、Connect で呼んでも同じで、Connect のリクエストに並びの要素を書くこと(`rulec api` の `element_fields`)も、同梱のコードに渡すことも、いまはしない。前の dandori は、並びの入力を文字列と読み違えていた(`rulec schema` の `array` を知らなかった)。リストを渡すと E003 になり、文字列を渡すと検査を通っていた。並びを渡すことは 7 章に残す。

**無いことがある入力と出力**:rulec の入力と出力は、`none` を取ることがある(`区分?`。rulec の設計書の 2.1)。dandori は規則に `none` を渡さず、生成するコードも、規則のコードとのあいだで null をやりとりしない(`Window | null` や Go の `*Window` という型を、名前として読み込んでしまう)。そこで、無いことがある入力か出力を持つ規則は、`use rule` が E005 でエラーにする。前の dandori は、その入力と出力を文字列と読み違えていた(`rulec schema` の `oneOf` を読まなかった)。null をやりとりすることは 7 章に残す。

**出力の無い規則**:rulec の `enums` のファイルには出力が無く、そこからは列挙の型のモジュールしか生成されない。`connect` が呼ぶサービスも無い。dandori は、出力の無い規則のサービスの形を読みに行かず、呼ぶところを、ほかの呼び方と同じ E005 で止める(10 章)。列挙は型として使える。

### 1.14 `.flow` が proto のサービスを実装する

ワークフローの入口(何を受け取って始まり、何を返して終わるか、途中で何を受け取るか)を、標準の proto3 のサービスとして書き、`.flow` がそれを実装する。E016 がタスクを呼び先の記述に合わせるのを、ワークフロー自身に向けたものである。

```
workflow fulfillment v1 implements shop.FulfillmentService
…
use proto shop from "../specs/fulfillment.proto"
```

```proto
service FulfillmentService {
  option (dandori.v1.workflow) = {name: "fulfillment", version: 1};

  rpc Fulfill(FulfillRequest) returns (FulfillResponse) {
    option (dandori.v1.start) = {fails: ["OutOfStock", "DeliveryFailed", "PackingLate"]};
  }

  rpc AnswerPacking(AnswerPackingRequest) returns (AnswerPackingResponse) {
    option (dandori.v1.answer) = {task: "wait_for_packing"};
  }
}
```

**決定**:

- `.proto` は標準の proto3 のままにし、dandori の印は、リポジトリに置く `proto/dandori/v1/options.proto` のカスタムのオプションで付ける。サービスには `(dandori.v1.workflow)`(ワークフローの名前とバージョン)を、メソッドにはそれぞれ次のどれか一つを付ける。`(dandori.v1.start)` は実行を始める(リクエストが入力、レスポンスが出力で、`fails` に `fail` の名前を並べる)。`(dandori.v1.event)` は `event` のタスクが待つ値を送る。`(dandori.v1.answer)` は `callback` のタスクに応答する。`(dandori.v1.status)` は、実行がいまどこにいるかを聞く。
- 状態のクエリが返す形は dandori が決めるので、`options.proto` に `message Status` として書く。いまのクエリ `dandori.status` が返すもの(`at`、`cases`、`events`)そのままの形で、`cases` の値は、始まっていない案件の null も書けるように `google.protobuf.Value` にした。protobuf の Python の `json_format`(protobuf 7.36.2)で、いまのクエリが返すものを `Status` として読めることを確かめた(2026-10-01。`at` と `cases` の null も読み、知らないフィールドは拒否した)。
- dandori は `options.proto` を、Google の well-known types と同じく、ファイルが無くても知っている(同じ一枚を `include_str!` で埋め込む)。protoc と buf にはファイルが要るので、利用者はリポジトリの `proto/dandori/v1/options.proto` を、自分の proto のルートに `dandori/v1/options.proto` としてコピーする。このファイルは buf の標準の lint(STANDARD)と protoc を通る(buf 1.54.0、libprotoc 35.1、2026-10-01)。拡張の番号は、protobuf が組織の中で使うために空けている範囲(50000〜99999)の 50700〜50704 で、protobuf の拡張の登録簿には載せていない(7 章)。
- `.flow` は、`workflow` の行に `implements <API>.<サービス>` と書いて、実装するサービスを一か所で指定する。一つのワークフローが実装するサービスは一つで、文法がそれを言う。`workflow` の行に置いたのは、サービスのオプションが繰り返すワークフローの名前とバージョンと、並べて読めるようにするためである。サービスの名前は、単純な名前(`FulfillmentService`)、package から見た名前、完全な名前のどれで書いてもよい。API の名前だけで、サービスを書かないものは構文の誤り(E001)にした。

**突き合わせ(E017)**:食い違いを一つずつ言う。

- サービスの `(dandori.v1.workflow)` の名前とバージョンが、`workflow <名前> v<版>` と同じであること。
- どのメソッドも、dandori の四つの印のどれか一つを持つ。ストリームのメソッドは実装できない。始めるメソッドはちょうど一つ。
- 始めるメソッドのリクエストのフィールドと `inputs` が、JSON の名前で一つずつ対応すること。入力の型は、そのフィールドの値を読めなければならない。型の比べ方は E016 と同じ(64 ビットの整数は `string`、列挙は記述の値を全部持つ。ゼロ値は 1.10 のとおり)。ただ、値があるかはワークフローが決める。値があるかを持つフィールド(メッセージのフィールドなど)を `T` の入力で受けてよく、そのフィールドの無いリクエストは、入口で `Dandori.BadInput` になる。`json` の入力(`Value` のフィールド)は、無ければ null として読み、拒否しない(1.2)。
- レスポンスのフィールドと `outputs` が一つずつ対応し、出力の値をそのフィールドに書けること(E016 の送る向きと同じ)。`T?` の出力は、値があるかを持つフィールドにしか書けない(持たないフィールドは、無いことを表せない)。
- `fails` の名前と、フローの `fail` の名前が、両方の向きで一致すること。proto にはエラーを書く場所が無いので、クライアントが失敗の名前を proto から知れるよう、ここに並べる。
- `event` のメソッドは `task` で `event` のタスクを指定し、そのタスクの値の型がリクエストを読めること。`answer` のメソッドは `callback` のタスクを指定し、そのタスクの応答の型がリクエストを読めること。値があるかは、入力と同じくワークフローが決める(要るフィールドが無い値は、結果の検査が `Dandori.BadResponse` にする)。どちらもレスポンスはフィールドを持たない(ワークフローは受け取っても何も返さない)。一つのタスクに、メソッドは一つまで。
- `status` のメソッドは多くとも一つで、リクエストはフィールドを持たず、レスポンスは `dandori.v1.Status` か、それと同じフィールドを持つメッセージ。
- dandori のオプションを使うファイルが、`dandori/v1/options.proto` を import していること(protoc と buf がそれを求める)。
- メソッドのメッセージが、読んだファイルにあること。読めなかった import にあるメッセージは形が分からないので、そう言い、どのファイルを読めなかったかを添える(1.10 の読めない import と同じ)。

食い違いを一つずつ言うために、印が二つ以上あるメソッドとストリームのメソッドは、その食い違いだけを言い、メッセージは比べない。始めるメソッドの数と状態のメソッドの数は、印の数にかかわらず、その印を持つメソッドで数える(`(dandori.v1.start)` と `(dandori.v1.status)` の両方を持つメソッドは、「始めるメソッドが無い」とは言わせない)。`T?` の出力が値があるかを持たないフィールドに書かれるときは、フィールドが `required` のとき(出力を `T` にする)と、`repeated` か `map` のとき(`optional` にできないので、出力を `T` にする)を、ほかと分けて言う。

値があるかをワークフローが決めるのは、proto3 が、フィールドが要ることを `buf.validate` のほかに書けないからである。メッセージのフィールドをいつも `T?` で受けさせると、どのフローも入口で `none` を分けることになる。E016 がタスクの結果に `T?` を求めるのは、記述が省いてよいと言うレスポンスを、フローが読めなければならないからで、ここではワークフローのほうが、自分の入口の決まりを言う側である。

入力と出力の型を `<API>.<メッセージ>` で書けば(1.12)、比べるまでもなく一致する。ただ、proto の型には単位が無いので、単位の付いた数を持つ値(引当と発送の注文の金額)は、`.flow` の型で書き、記述と形で突き合わせる。

**メッセージは形で比べる**:buf の標準の lint は、リクエストとレスポンスのメッセージを `<メソッド>Request` と `<メソッド>Response` と名付け、ほかのメソッドと共有しないことを求める(`RPC_REQUEST_STANDARD_NAME`、`RPC_RESPONSE_STANDARD_NAME`、`RPC_REQUEST_RESPONSE_UNIQUE`)。`google.protobuf.Empty` や `dandori.v1.Status` をそのまま使うと、この三つに当たる。dandori が `GetStatusRequest` と `GetStatusResponse` を用意しても、`RPC_REQUEST_RESPONSE_UNIQUE` はモジュール全体で数えるので、二つのサービスが使えば当たる(どれも buf 1.54.0 で確かめた)。そこで、メッセージは名前で決めず、フィールドで比べる。lint を通したければ、`GetStatusResponse` に `Status` と同じ三つのフィールドを書けばよく、lint を気にしなければ `dandori.v1.Status` をそのまま使ってよい。

**サービスに書かないイベントとコールバック**:フローが待つイベントとコールバックのうち、サービスに書くのは、そのサービスのクライアントが送るものだけでよい。インターフェースを実装するクラスがほかのメソッドを持ってよいのと同じで、書いたものは書いたとおりに実装する。一方、クライアントが受け取るもの(出力、失敗の名前、状態)は全部を書く。受け取る側に、知らない名前や形が来ないようにするためである。日本語の状態を持つ規則の案件を運ぶイベント(`tests/flows/events.flow` の配達の知らせ)は、`.proto` の列挙の値が ASCII の識別子なので、そもそも書けない。

**入口でゼロ値を埋める**:start の入力、サービスに書かれた `event` の値と `callback` の応答は、記述のリクエストのメッセージを protobuf の JSON として読む。protojson の書き手が省いたゼロ値を埋めてから、型を確かめる。埋める表は、`connect` のタスクの結果と同じ形で(1.10)、リクエストのメッセージから作って生成するコードに書き込む。六つのプラットフォームの全部で、入口で埋める(4 章。Step Functions は JSONata、Argo は式の中の `sprig.set`、ほかは `fill`)。

**出力**:出力は、いまと同じく値をそのまま書く(ゼロ値も省かない。protojson を読む側は、書いてあるゼロ値も受け付ける)。ただし、サービスを実装するフローの実行は、出力が無いときも `{}` で終える(ほかのフローは、いまどおり `null` で終える)。protobuf の JSON を読む側は、`null` からメッセージを読まない(`json_format` で確かめた)。

**ペイロード**:値は JSON で、形は protojson である。Temporal では `json/plain` のペイロードになる。ペイロードのコンバーターは使わない。dandori が生成するクライアント(`client.ts`・`client.py`・`client.go`)は、値を JSON のまま送る。protobuf のメッセージを持つほかのクライアントは、メッセージを protojson にし、それを JSON の値としてペイロードに入れればよい(Go なら `json.RawMessage(protojson.Marshal(m))`、Python なら `json_format.MessageToDict(m)`)。protojson を文字列のまま渡すと、ワークフローには文字列が届いて `Dandori.BadInput` になる。Temporal の Go の SDK は、proto のメッセージをそのまま渡すと `json/protobuf` のペイロードに書き、TypeScript の既定のコンバーターはそれを読まない(7 章)。

**Temporal での名前**:ワークフローの型はいまのまま `<名前>_v<版>` で、`event` は Update `dandori.event`、`answer` は Update `dandori.answer`、`status` はクエリ `dandori.status` である。生成する `client.ts`・`client.py`・`client.go` には、メソッドの名前の関数(`fulfill`、`answerPacking`。Python では `answer_packing`、Go では `Fulfill`、`AnswerPacking`)と、メッセージの名前の型(`FulfillRequest` は `WorkflowInput`)を足す。関数の名前がクライアントにもとからある関数と同じになるときは、同じことをするなら書かず(`Start` は `start`、`GetStatus` が返す `dandori.v1.Status` の型は `Status`)、違うことをするなら末尾に `Rpc`(Python では `_rpc`、Go では `RPC`)を付ける。`history encrypted` のフローでは、クライアントが暗号化のために持つ名前(1.18)も、もとからある名前に数える。イベントとコールバックの関数はリクエストの値だけを送り(`{ ok: 値 }`)、エラーの名前を送りたいときは、いまの `send` と `answer` を使う。提供側のスタブは、いまの生成物(ワークフローとワーカー)そのものである。ほかの言語のクライアントは、この `.proto` から protoc のプラグインで作れるが、そのプラグインはまだ作らない(7 章)。

**ほかのプラットフォーム**:start と `answer` は、どのプラットフォームでも同じ意味で作る(入口で埋める)。`event` は、いまと同じく Temporal のほかでは E050 である。`status` のメソッドを持つサービスも、Temporal のほかでは E050 にする。実行がいまどこにいるかを答えるクエリが、ほかのプラットフォームに無いからである(P6)。Step Functions と durable functions は、実行の中のコードに問い合わせる手段を持たない。Argo(ワークフローの出力のパラメータ)と pydantic-graph(`Deps` の状態)は答えられるはずだが、まだ書いていないので、診断もそう言う(7 章)。

**例**:引当と発送の三つの版は、同じ `shop.FulfillmentService` を実装する(`status` を持たないので、どのプラットフォームでも作れる)。日本語の版は、ワークフローの名前(`引当と発送`)も入力の名前も違うので、JSON の名前を日本語にした別の記述(`fulfillment.ja.proto`)のサービスを実装する。`json_name` には日本語を書ける(buf と protoc で確かめ、protobuf の Python の `json_format` も、日本語の名前で読み書きした。5 章)。`tests/flows/events.flow` は、Temporal だけのメソッド(`event` と `status`)を持つサービスを実装する。どのプラットフォームでも走る `tests/flows/service.flow` は、出力を持たないサービスを実装し、コールバックの応答もサービスから受け取る。引当と発送は `succeed` で出力を書いて終わり、`events.flow` は Temporal でしか走らないので、出力の無い実行が `{}` で終わることと、入口でゼロ値を埋めることを、六つのプラットフォームの全部で確かめる試験のフローを別に置いた。入力には、箱のリストの中の品のリスト(メッセージのリストの中のメッセージのリスト)、中のメッセージ、ゼロ値が本当の値でもある列挙を持たせ、ゼロ値をどの深さでも省く。

**費用**:`.proto` を読むコードが、サービスとメソッドのオプションの値も読むようになる。入口でゼロ値を埋めるコードが、六つのプラットフォームに一つずつ増える。Step Functions では、入力を埋める Pass が一つ増え、E040 の見積もりにその 2 件を足す。

### 1.15 規則の名前と、生成するコードの名前

**問題**:rulec が規則のために生成するコードは、規則の別名(`rule 出荷の急ぎ(urgency) v1` の `urgency`)で呼ばれる。TypeScript と Python では関数とモジュールの名前、Go ではパッケージの名前(別名から `_` を除いて小文字にしたもの)になる。規則が受け取る列挙も、別名から作った名前(`Room`、`parseRoom`)を持つ。dandori は、規則のコードがワークフローと一緒に行く出力先で、まわりのコードを書き、そのファイルに規則の名前を自分の名前と並べて読み込む。別名が、まわりのコードがもう使っている名前だと、生成物が壊れる。Temporal の Python の `rules.py` で別名が `activity` なら、`@activity.defn` が規則の関数を指して読み込みで止まり、`rules` なら終わりの行が名前を上書きして、アクティビティが規則の代わりにリストを呼ぶ。TypeScript の `rules.ts` で `rules` なら、同じ名前が二つになってコンパイルが通らない。ritsu の C の段階で見つけたもの(ritsu の PLAN 7.5)である。

**決定**:まわりのコードの名前を替えると、どの出力先の生成物も変わるので、替えない。代わりに、ぶつかる名前の規則を `check` が E006 でエラーにする。エラーにするのは、規則のコードがワークフローと一緒に行く規則(呼ばれていて、Connect のサービスで呼ばないもの)である。どの名前をエラーにするかは、生成するコードを読んで決め、それぞれの生成器の表(`AROUND_RULES`)に置いた。表の名前は、そのファイルが宣言するか、外から読む名前である。

| ファイル | 読み込む規則の名前 | エラーにする名前 |
|---|---|---|
| `rules.ts`(Temporal の TypeScript、Argo の caller) | 規則の関数(別名) | `rules`(宣言)、`args`・`out`(引数と変数)、`Boolean`・`String`・`BigInt`・`Number`(数や文字列にするのに使う) |
| `rules.py`(Temporal の Python、pydantic-graph) | 規則の関数と、受け取る列挙のクラス | `activity`・`Any`(読み込み)、`rules`(宣言)、`args`・`out`、`bool`・`str`・`int`・`dict`(型を変えるのと注釈に使う)、各規則のアクティビティ `rule_<規則>` |
| Lambda の関数(Step Functions、Lambda durable functions) | 規則の関数と、受け取る列挙のクラス | `handler`(宣言)、`event`・`context`(引数)、`out`、`bool`・`str`・`int` |
| `rules.go`(Temporal の Go) | 規則のパッケージ | `context`(読み込み)、`ctx`・`args`(引数)、`int64`・`any`・`string`・`error`・`nil`・`true`(使う組み込みの名前)、列挙を受け取る規則では `ok`(列挙の値を読み込む変数) |

`annotations`(`from __future__ import annotations` が名前を作るが、あとで使わない)、TypeScript の `Promise` と `Record`(型の名前としてだけ使い、関数の名前と別に引かれる。`tsc --strict` で確かめた)、Go の `out` と `err`(宣言の右側は、左の変数より先に読まれる)は、ぶつからないのでエラーにしない。`Any` で TypeScript が壊れないこと、`dict` と `Any` で Temporal の Python が壊れること(読み込みで止まる、アクティビティの引数を読めない)は、実際に走らせて確かめた(temporalio 1.33.0)。列挙のクラスは別名の頭を大文字にした名前なので、表のうち当たりうるのは `Any` だけである。

ほかに、次も E006 でエラーにする。

- 別名が同じ二つの規則(`rulec gen` が同じファイルに書くので、片方がもう片方を上書きする)。Go のパッケージだけが同じになるもの(`ship_fee` と `shipfee`)も。同じ規則のファイルを二つの名前で使うのは、モジュールが一つなので構わない。
- タスクと、規則のアクティビティ `rule_<規則>` が同じ名前になるもの(どの出力先でも、タスクと規則は同じ名前の並びのアクティビティやメソッドになる)。二つの規則のアクティビティが同じになるもの(`ident` で `_x` と `v_x` が同じになる)も。

**直したこと**:二つの規則のあいだで名前が重なるもの(受け取る列挙の名前が同じ二つの規則、`money[JPY, incl_tax]` を受け取る二つの規則)は、エラーにせず、生成するコードの側で避ける。`rules.ts` と `rules.py` は、前のモジュールがもう読み込んだ名前を、`<名前>_<モジュール>` の別の名前で読み込む(`import { parseRoom as parseRoom_rule }`、`from .rulec.python.rule import Room as Room_rule`)。単位の型の名前(`JPYInclTax`)は、rulec がどのモジュールにも同じ形(`bigint & { readonly __rulec: "JPYInclTax" }`)で書くので、一度だけ読み込む。前は、二つの規則が同じ単位の型を受け取ると、`rules.ts` が同じ名前を二度読み込んでコンパイルが通らず、同じ名前の列挙を受け取ると、`rules.py` が後のクラスで前の規則を呼んでいた。重なる名前の無いフローの生成物は一字も変わらない(例とテストのフローの全部の `build` を、替える前と比べた)。

**エラーにする範囲の外**:rulec の生成したコードと、出力先の言語そのもの(予約語、組み込みの名前、標準ライブラリのモジュール)がぶつかるものは、rulec の W121 が言う(警告。使わない出力先なら構わない)。dandori はそれをエラーにしない。たとえば、Go のパッケージが標準ライブラリのパス(`time`、`io`)と同じ規則は、rulec が W121 で言い、dandori の `rules.go` も読み込みを間違える。

**確かめ方**:`tests/names.rs` が、四つの表のどの名前の規則にも E006 が出ること、列挙の別名、Go の `ok`、`rule_<規則>`、別名の同じ二つの規則、タスクと規則のアクティビティを確かめ、表の名前がどれも、そのファイルに実際に書かれることも確かめる(使わなくなった名前が表に残らないように)。エラーにしない名前で全部の出力先の生成物が通ることは、`tests/flows/names.flow` で確かめる。このフローは、単位の型が同じ規則二つ、同じ名前の列挙を受け取る規則二つ、dandori の名前に近い別名の規則二つ(`rule` と `activities`。どちらも `tests/flows/rules/` に置いた)と、ステートマシンの規則 `order_state`(列挙の値の集合を行に書く規則。rulec の §15.171 で、その TypeScript が `tsc --strict` を通るようになった)を呼び、ほかのテストのフローと同じく、どのプラットフォームでも走らせて参照インタプリタと突き合わせ、TypeScript を `tsc --strict` で、Go を `go vet` で確かめ、規則のまわりのコードに rulec の例を全部答えさせる。

### 1.16 日付のファイルと帳簿(koyomi と chobo)

業務のワークフローは、判断のほかに、日を数えることと数を動かすことをする。支払期日まで在庫を押さえ、支払われたら出荷し、支払われなければ棚に戻す、といった形である。`.flow` は算術を持たない(P1)ので、どちらも自分ではしない。期日は koyomi の日付のファイル(`.cal`)に、数は chobo の帳簿(`.book`)に書き、`.flow` はそれを読む宣言と呼び方を持つ(ritsu の DESIGN 7.8)。`examples/invoice` が、どのプラットフォームでもそのまま動く例である。

```
use dates terms from "dates/payment_terms.cal"
  lambda "arn:aws:lambda:eu-west-2:123456789012:function:payment-terms"
use book stock from "books/stock.book"
  lambda "arn:aws:lambda:eu-west-2:123456789012:function:stock"

task reserve(order: string, sku: string, qty: int) -> stock.reserve
  book stock.reserve.hold
  starts stock.reserve
  errors out_of_stock

case hold : stock.reserve follows stock.reserve

flow
  hold <- reserve(order: order.id, sku: order.sku, qty: order.quantity)
    on out_of_stock => succeed outcome = out_of_stock, due = none
  let due = terms.payment(received: now)
  wait until due.at
  …
```

**決定**:規則と同じく、ritsu の口から読む。日付のファイルは日付の口(`ritsu_ports::Dates`、koyomi が答える)、帳簿は帳簿の口(`ritsu_ports::Books`、chobo が答える)の `facts` で、型の付いた事実を受け取る。dandori のクレートは koyomi と chobo のクレートに依存しない(P2)。口の答えは走らせる側が渡す(`dandori::cli::run_with_ports(引数, 規則の口, 日付の口, 帳簿の口, …)` と `dandori::sources::with_ports`)。口を渡さない走らせ方(dandori のクレートのバイナリ)では、規則と同じく E018 になる(`use rule`・`use dates`・`use book` のうち、読めないものの最初の宣言で一度だけ言い、`ritsu dandori` で走らせる形を注に書いて exit 2。0.3)。そのために、日付の口と帳簿の口にも `joined` を足し、読めない口(`NoDates`・`NoBooks`)は false を返す。テストは koyomi と chobo を dev-dependency にして、それぞれの口の答えをつなぐ。`ritsu dandori` と `ritsu check` は、三つの口をどれも渡す(`ritsu check` は `ports::Engine::checked_with` で)。

| 口の答え | dandori が読むもの |
|---|---|
| 日付の口の `facts` | ファイルの名前と ASCII の別名、バージョンと SHA-256、入力(名前、別名、日付か整数か、範囲)、日付ごとの名前と別名と読む入力と時刻(`at 09:00`、一日の終わり)、カレンダーの名前と UTC オフセット |
| 帳簿の口の `facts` | 帳簿の名前とバージョンと SHA-256、単位、勘定(引数、単位、外の勘定か、下限と上限とその理由)、振替(引数と単位、キー、仮押さえか、有効期限、移動、操作ごとの拒否されうる理由、仮押さえのステートマシン)、chobo が書くクライアントの名前(下) |

帳簿の口には、クライアントの名前(`BookFacts` の `typescript`・`python`・`go`、型は `BookClient` と `ClientTransfer`)を足した。dandori が書くコードは chobo のクライアントを呼ぶので、生成したクライアントを読まずに、振替のメンバーの名前(`book.reserve`、Go の `b.Reserve`)と引数の名前(Python のキーワード、Go のフィールド)が要る。chobo の口は、chobo 自身の名前の決め方(`chobo api` の `targets` と同じもの)から埋める。koyomi が生成するコードの名前(TypeScript と Python は別名、Go はパッケージの名前と外から見える名前)は、口に足さず、koyomi と同じ ritsu-emit の決め方で求める。

**`date` 型**:日付は新しい型 `date` で、値は `YYYY-MM-DD` の文字列である。入力、出力、レコードのフィールド、タスクの引数と結果に書け、値を埋め込んだ文字列にも入れられる。入ってくるところで形を確かめるのは、ほかの型と同じである(P5)。式は比べることも足すこともしない(P1)。日を数えるのは日付のファイルの役目で、dandori は日付を運ぶ。範囲を書いた日付は、入ってくるところで、その端と比べて確かめる(1.3、20 章)。rulec の `date` の列(規則の入力が日付のもの)も、文字列ではなく `date` として読むようになった。

**日付を呼ぶ**:`<ファイル>.<日付>(<入力>: …)` で、日付のファイルの一つの日付を、規則と同じく `let` で呼ぶ。返すのはレコード `{ day: date, at: timestamp }` で、`at` は、日付が時刻を言うとき(`at 09:00`)、その日のその時刻をカレンダーのオフセットで UTC にしたものである。時刻を言わない日付は `day` だけを返す。`at` はそのまま `wait until` に渡せる。

- 日付の入力には `date` を渡す。カレンダーが UTC オフセットを言うなら(`offset +09:00`)、`timestamp` も渡せ、時刻は、そのオフセットでその時刻にあたる日として読む(`2026-03-31T15:30:00Z` は、`+09:00` では 4 月 1 日)。オフセットを言わないカレンダーの日付に時刻を渡すと E003 で、注に、カレンダーにオフセットを書くか、日付を渡すよう書く。時刻をどの日と読むかは、日付のファイルのカレンダーが決めることで、`.flow` の側で決めると、ファイルとワークフローで読み方が分かれるからである。
- 整数の入力には `int` を渡す。範囲(ファイルが宣言した範囲)は、規則の入力と同じく確かめる(E014、W104)。
- 一つのファイルの日付は、それぞれ別に呼ぶ。日付ごとに、それが読む入力だけを渡す(日付の口が、日付ごとに読む入力を言う)。
- `use dates` の下の `lambda "<関数>"` は、Step Functions と Lambda durable functions で日付を計算する Lambda 関数、`local` は Temporal のローカルアクティビティ。規則のものと同じ意味である。

日付は、規則と同じくアクティビティ(各プラットフォームの呼び出し)にした。koyomi の生成したコードは純関数だが、カレンダーの祝日の表は毎年変わるので、ワークフローのコードの中で計算すると、表を入れ替えたワーカーで再生が食い違う(ritsu の DESIGN 7.8)。規則を普通のアクティビティにした理由(判定の記録が履歴に残る、直しても再生が食い違わない。4.2)も、そのまま当てはまる。生成するコードの中では、日付は規則の一つとして扱い、アクティビティの名前は `dates_<ファイル>_<日付>` にした(規則のアクティビティ `rule_<規則>` とぶつからない)。

**`now`**:その文を走らせた時刻を読む式。値は `timestamp`(UTC、秒まで)で、日付の入力、タスクの引数、`wait until`、文字列の中(`"受付 {now}"`)に書ける。ritsu の DESIGN 7.7 が、渡す日付が仮押さえを作った日であると言うために求めたものである。どのプラットフォームも、再生や再試行で同じ時刻を読み直せるように読み(4.6)、一つの文の `now` は一つの時刻である(呼び出しのリトライのあいだも。20 章)。参照インタプリタは、シナリオの `now` を読む。シナリオの `now` は一つの時刻か、期間を確かめる文が順に読む時刻の並び(下)で、無ければ `2026-03-31T15:30:00Z` である。期間を書かないワークフローでは、一つの実行のどの `now` も同じ時刻になる。この時刻は `+09:00` ではもう次の日なので、時刻をオフセットで日に読むところの誤りが、突き合わせに出る(5 章)。

**動く期間**:`workflow` の行の下に、`history encrypted` と並べて `now range >=2026-01-01 <=2027-12-31` と書くと(端の書き方は日付の範囲と同じで、`offset` も書ける。1.3)、ワークフローはその期間の中で動く(20 章)。走り始めた時刻が期間の外なら、何も呼ばずに `Dandori.OutOfPeriod` で失敗する(範囲の外の入力と同じ扱い)。本体で `now` を読む文は、その前で時計を一度読んで期間で確かめ、外なら、処理されなかったタスクのエラーと同じく `on failure` に入る。`on failure` と `on cancel` の `now` は確かめない。期間を書けば、本体の `now` は期間の中の時刻として、dandori の範囲の検査と ritsu の X6 で数えられる。

**帳簿の振替を呼ぶ**:タスクの呼び出し方の一つ、`book <帳簿>.<振替>.<操作>`。操作は chobo のものそのままで、すぐに確定する振替なら `do`、仮押さえにする振替なら `hold`・`post`・`void` である。振替に無い操作は E007。

- 引数は、振替の引数を名前のとおりに書く。`do` と `hold` は全部、`post` と `void` はキーの引数である。`post` に数の引数も書けば、押さえた数のうちその数だけを確定し、残りを戻す(一部の確定。数の引数は全部書くか、一つも書かないか)。数は `int` で、帳簿の単位で数えた整数のまま渡す。名前や型の違い、足りない引数、余る引数は E016。
- 結果は、`hold`・`post`・`void` なら仮押さえのレコード `<帳簿>.<振替>`(キーの引数のフィールドと、状態 `state`)で、`do` は何も返さない(`->` を書けば E016)。仮押さえのレコードは、帳簿を読んだときに作る。chobo のクライアントが返すのは、通ったか(`done`、同じ操作を前にしていれば `done_before`)と、拒否されたなら理由だけなので、結果は渡した引数と操作から作る(`hold` なら `held`、`post` なら `posted`)。
- 拒否された理由は、そのまま宣言したエラーの名前になる(`errors out_of_stock`、`errors expired`)。`= <ステータス>` は書けない(E007)。帳簿がその操作を拒否するときの理由になりえない名前は E016 で、理由になりうるのは、chobo の検査が言うもの、勘定の境界の理由、chobo が操作ごとに決めている理由(`key_conflict`、`no_such_hold`、`already_posted`、`expired` など)である。宣言しなかった理由で拒否されれば、`Dandori.Failure.<理由>` の `failure` になる。
- `key` は書けない(E007)。帳簿の振替は、帳簿に書いたキーで一度しか通らない。再試行しても二度は動かず、同じ操作の二度目は `done_before` で、通ったものとして読む。だから E030 と W030 にあたらない(chobo の DESIGN 5 章)。`refused as` も書けない(E007)。仮押さえは状態ごとに理由を付けて拒否されるので、理由を `errors` に書く(2.7)。
- `use book` の下の `lambda "<関数>"` は、Step Functions で帳簿の操作をする Lambda 関数である(4.1)。ほかのプラットフォームでは、dandori が書く実装が `Transport` の `book` で chobo のクライアントを呼ぶ(4.2)。

**名前**:規則・日付のファイル・帳簿は、`use rule`・`use dates`・`use book` の名前を共有する(同じ名前は E006)。`dates` と `book` は、`agent` や `jev` と同じく、置き場所(`use` のあと、タスクの項目の頭)でだけ読む語にした。ふつうの名前としてよく使う語だからである。`now` は式の語なので、サイトのハイライトでもキーワードにした。

**仮押さえを案件にする**:`case <名前> : <帳簿>.<振替> follows <帳簿>.<振替>` で、仮押さえを案件として追う(2.7)。

`tests/flows/dates_and_books.flow` は、日本語の名前で、すぐに確定する振替(`do`)、一部の確定、時刻を言わない二つの日付(一つは整数の入力も取る)をローカルアクティビティで呼ぶこと、`now` を日付の入力と文字列に書くこと、日付をタスクに渡すことと、タスクの結果の日付を日付に渡すことを、どのプラットフォームでも走らせる。

### 1.17 規則の前提を、走らせるときに確かめる

rulec の規則は、入力の形だけでは書けない前提を持つことがある。二つの入力の関係(`constraint asked <= paid`)と、日付の入力が koyomi の日付のとる日に限られること(`range from koyomi`)である(rulec の §15.116 と §15.174)。規則の生成したコードは、前提を破る呼び出しを入口で受け付けない。

`ritsu check` は、フローが規則を呼ぶところごとに、dandori が求めた値の範囲(1.3)で前提が保たれるかを決める(ritsu の DESIGN 7.4 の X2)。成り立つと示せれば何も言わず、破る例があれば E201、決められなければ W201 を出す。決められないのは、渡す値に範囲の無いところから来るものがあるとき(範囲を書いていないタスクの結果、期間を書いていないワークフローの `now` など)と、関係の前提で範囲の端が開いているときである。日の前提(`range from koyomi`)は、渡す日が koyomi の日付の日か、`.flow` が範囲を書いた日付なら、その日の全部で決まる(20 章)。

**決定**:決められなかった前提は、生成したコードが、ワークフローを走らせたときに確かめる。`ritsu dandori` は、ritsu-cross が決められなかった前提を、ritsu の口 `Undecided` で渡す(`dandori::cli::run_with_undecided`)。`build`・`run`・`scenarios`・`doc` は、それを確かめる文(`TK::Check`、`src/prechecks.rs`)をフローに入れてから、生成し、走らせ、描く。`check` は入れない(診断は変わらない)。dandori のクレートのバイナリは何も渡さないので、確かめる文は入らない(規則を使うフローは、そもそも E018 で止まる)。決められた前提(成り立つと示したもの、例があるもの)しか持たない呼び出しの生成物は、これまでと一字も変わらない。

**どこで確かめるか**:値ができたところで、すぐに確かめる。ただし、その呼び出しに必ず届く場所に限る。規則を呼ぶところから同じブロックの文を前へたどり、前提が読む値を変えず、次の文へ進むか実行を失敗させるかしかない文(ほかの変数への `let`、待ち、`pass`、ハンドラーがどれも `succeed` も `break` もしない呼び出し)を越えて、その前に置く。値を作る文(たいていは、その値を結果として返すタスク)か、ほかへ進むことがある文(`match`、ループ、実行を成功で終えるかループを抜けるハンドラーを持つ呼び出し)に当たれば、その直後で止まる。ブロックの頭まで来れば、ブロックの頭に置く。

```
  let asked = ask_amount(order: order)
  note_request(order: order)
  let decision = check(paid: paid, asked: asked)
  let day = pick_day(order: order)
  match decision.route
    at_once =>
      let run = settle(pay_day: day)
```

`tests/flows/preconditions.flow` の一部である。`asked <= paid` は、`ask_amount` が答えた直後、`note_request` を呼ぶ前に確かめる。`note_request` は `asked` を変えず、失敗すれば実行も失敗するので、越えてよい。日の前提(`settle` の `pay_day` は koyomi の支払日だけを取る)は、`pick_day` の直後ではなく、`at_once` の分岐の頭で確かめる。`review` に進む実行は、`settle` に日を渡さないからである。

これより前で確かめると、規則を呼ばない実行まで落とすことになる。上の例で `pick_day` の直後に確かめると、`review` に進む実行も、規則に渡さない日のせいで失敗する。逆に、規則を呼ぶ直前まで待つと、前提を破ると分かっている値のまま、そのあいだのタスクを呼ぶことになる。P5(外部のサービスが返したものは境界で確かめ、外れたらその場で失敗させる)に合わせて、値ができた直後に寄せた。越えるのは、失敗すれば実行も失敗する呼び出しだけなので、変わるのは、どのエラーで失敗するかと、そのタスクを呼ぶかどうかだけである。

**破ったとき**:実行は `Dandori.BrokenPrecondition` で失敗する。理由は `line <呼び出しの行>: the values given to the rule <規則> break its precondition <前提>` で、どのプラットフォームでも同じ文にした。`Dandori.BadResponse` と同じく、リトライせず、`on` でも `on failure` でも処理しない。値は何度呼んでも変わらず、タスクのハンドラーはそのタスクのエラーのために書くものだからである。並列のイテレーションの中では、`fail` と同じくそのイテレーションが終わり、ほかのイテレーションが終わってからループが失敗する。

前は、規則の生成したコードが入口で受け付けなかった。規則のアクティビティや Lambda の失敗になり、規則のリトライ(2 回、1 秒後と 2 秒後)を経て `failure` になって、`on failure` があればそれが走った。Connect で呼ぶ規則なら、サービスが `invalid_argument` で答えていた。規則の入口で受け付けないことは、そのまま残る。規則の生成したコードは、だれが呼ぶかを知らないからである。

**プラットフォームでの書き方**(4 章の各節にも書いた):
- Step Functions:条件を JSONata で書いた Choice と、破ったときに進む Fail。日は `in` と日の並びで比べる。
- Temporal(TypeScript・Python・Go)と durable functions:ワークフローのコードの `if` と `dd.fail`(Python は `raise dd.fail`、Go は `return nil, ddFail`)。日は `includes`、`in`、`switch` で比べる。
- Argo:値を計算するテンプレート一つ。成り立たなければ `dd_ctl` を `fail` にし、`dd_error` にエラーを入れる(4.4 の、グローバル出力パラメータを読み書きする置き方のまま)。
- pydantic-graph:確かめる節点一つ。成り立たなければ `dd.Failure` を投げる。

E040 の見積もりには、Step Functions で 4 件(Choice と Fail)、Argo で 4 ノード(値を計算するテンプレート)を足す。ほかのプラットフォームはワークフローのコードの中で確かめるので、履歴も操作も増えない。整数の比べと日の並びはどのプラットフォームでも書けるので、E050 にしたものは無い。

Temporal では、確かめる文が増えても減ってもコマンドは変わらないので、走っている実行の再生は食い違わない。ただし、範囲を足したときと同じく(1.3)、走っている実行のうち値が前提を破るものは、新しいコードでは失敗に変わる。範囲を書き足して ritsu が前提を決められるようになれば、確かめる文は消える。

**確かめ方**:参照インタプリタは、確かめる文で前提を計算し、破れば実行をそのエラーで終える(どのプラットフォームでも、呼び出しは何も出ず、終わり方だけが変わるので、`View` は変えていない)。シナリオの生成は、確かめる文ごとに、前提が保たれる実行と破れる実行を選ぶ。シナリオが作る数と日を決めずに残しておき、確かめる文が、範囲の中で、あとで作った値から書き換える。図(`dandori doc`)は、確かめる文を、その場所のノード(一行目に `check asked <= paid`、二行目にどの規則の前提か)として描き、そこで失敗した実行はそのノードで終わる。

`tests/flows/preconditions.flow`(英語)と `preconditions.ja.flow`(日本語)は、範囲の無いタスクの結果から来る額の関係と、koyomi の日(`tests/flows/rules/settlement.rule`、日本語は `精算.rule`)の二種類の前提を、タスクの直後、分岐の頭、順に回すループと並列のループのイテレーションの頭で確かめる。dandori のテストは、フローが呼ぶ規則の前提をすべて決められなかったものとして渡し(ritsu-cross も、この二つのフローの前提は一つも決められない)、どのプラットフォームでも参照インタプリタと突き合わせる。ritsu のテスト(`crates/ritsu/tests/dandori.rs`)は、`ritsu dandori build` が七つのプラットフォームで同じものを書くこと、決められる呼び出し(成り立つもの、E201 の例があるもの)の生成物が ritsu-cross を通さないときと一字も違わないことを確かめる。

あわせて、規則の日付の入力と出力を、つなぐコードで日の番号に直すようにした。rulec の生成したコードは、日付を 1970-01-01 からの日数で受け取り、返す。つなぐコード(`rules.ts`、`rules.py`、Lambda の Python、`rules.go`)は、それを数として読んでいた(Lambda の Python は `int("2026-02-10")` で止まり、`rules.ts` は `tsc --strict` を通らなかった)。日付の入力を持つ規則が例とテストに無かったので、表に出ていなかった。

### 1.18 秘密の値と、外へ出す通信(2026-10-06)

ritsu の言語としてのセキュリティの検査(ritsu の DESIGN 16 章)のうち、四つを dandori が受け持つ。どれもファイル一つで決まるので `dandori check` に置き、`ritsu check` は dandori の出力をそのまま並べる。

| コード | 重さ | 見ること | 意図を書く書き方 |
|---|---|---|---|
| W901 | 警告 | 鍵の形の値が `.flow` に書いてある | 同じ行のコメントに `ritsu: test secret` |
| W902 | 警告 | タスクが呼ぶ URL が、このマシンの外への `http://` である | URL を書いたところに `plaintext "<理由>"` |
| W904 | 警告 | 秘密の値が、プラットフォームの履歴に残る | `workflow` の下に `history encrypted` |
| E906 | エラー | 秘密の値を、プロジェクトの外の相手に送る | タスクの下に `discloses <引数>, … "<理由>"` |

**W901。** ritsu-base の `secrets` が、ファイルの全文(文字列もコメントも)から、プロバイダーが形を決めている鍵を探す(ritsu の DESIGN 16.3)。構文の誤りがあっても出す。診断には種類と接頭辞と長さだけを出す(`a Google API key is written here (AIza…, 39 characters)`)。dandori の診断は原文の行を引いて見せるので、どの診断でも、引いた行は ritsu-base の `secrets::mask` を通し、行の鍵を(テスト用の値も)`AIza…` の形にして見せる(`Diag::render`)。そうしないと、見出しで伏せた鍵や、同じ行に出たほかの診断の行から、鍵がそのまま CI のログに残る。

**W902。** 見る URL は、タスクとその呼び出し方が実際に送る先である。`http` のタスクの URL とエージェントの `url` はタスクの下に、規則のサービスは `use rule … connect` の下に、`use openapi` と `use proto` から呼ぶ操作は `use` の下(`url`、無ければ OpenAPI の文書の最初のサーバー)に書いてある。`plaintext` は URL を書いたところに書く。`use` から呼ぶタスクの下の `plaintext` は E007 で、その `use` の下に書くよう注で言う。`use` の URL の W902 は、それを呼ぶタスクがあるときだけ、`url` の行(無ければ `use` の行)に一つ出す。タスクごとに出すと、一つの直し方(`use` の下の `plaintext` か、`url` を https にすること)に、同じ警告がタスクの数だけ並ぶからである。このマシン(`localhost`、`.localhost`、`127.0.0.0/8`、`::1`)への `http://` は出さない(ritsu-base の `urls`)。Step Functions の E050(HTTPS でない送り先)はそのまま残り、`plaintext` を書いても Step Functions には出せない。

**秘密の値。** 次のところから読んだ値を秘密とする。

- `.flow` の `secret`:入力、出力、レコードのフィールド、タスクの引数と結果の型のあと(`range` と両方なら `range` のあと)に書く。予約語には足さず、書ける位置でだけ読むので、`secret` という名前はいまも使える。
- `.proto` の `debug_redact`:フィールドに直に書いたものと、`debug_redact` を付けた列挙の値をカスタムのオプションで付けたもの(`ritsu_proto::Protos::redaction`)。メッセージから作ったレコード(1.12)のフィールドと、`connect` のタスクの引数と結果(リクエストとレスポンスのフィールドと突き合わせるところ)に効く。
- OpenAPI のスキーマのプロパティの `x-data-classification`(`confidential` か `restricted`)、`x-sensitive-data`、`format: password`(ritsu-base の `marks`)。操作と突き合わせるタスクの引数と結果に効く。`writeOnly` だけのプロパティと `internal` は印にしない。

レコードは印の付いたフィールドの秘密を、リストは項目の秘密を、値を埋め込んだ文字列は埋め込んだ値の秘密を持つ。`json` の値は、秘密の値から作ったなら全体が秘密になる。規則の結果と、印の無いタスクの結果は秘密にしない。変数が何を持つかは、範囲(1.3)と同じく、変数に値を入れるすべての場所を合わせて決める。診断は、秘密の値を、フローがそれを読んだ書き方(`account.number`)で言い、印がどこに書いてあるかを注に出す。一つの値に印が二つあれば(入力の `secret` と、引数の `format: password`)、診断は一つにして、注に印を二つ並べる。

**W904。** 出すところは四つある。入力と出力(宣言の行。`succeed` が出力に書いた秘密の値は、その行)、フローが呼ぶタスクの結果(タスクの宣言の行。結果はフローが読まなくても履歴に残る)、タスク・規則・日付・帳簿・子のフローへの呼び出しの引数(呼び出しの行)、`fail` の理由(その行)。`history encrypted` を書いたフローには出さない。

```
warning[W904]: tests/fixtures/security/W904_payout_number.flow:39:1: the secret `account.number` is kept in the history of the workflow, as the argument `reference` of `pay`
    39 |   let paid = pay(account_id: account_id, amount: amount, reference: "Sales payout to {account.number}")
  = The mark is `debug_redact = true` at ../../../examples/payout/specs/payout.proto:34.
  = Temporal, Step Functions, Lambda durable functions and Argo Workflows keep the inputs and outputs of the workflow and of every call in the history, which whoever may read the executions can read.
  = Pass a reference instead (an ID, the name of a secret) and fetch the value inside the task. If the history is encrypted with a key you hold (Temporal: a payload codec; Step Functions and Lambda durable functions: a customer managed KMS key), write `history encrypted` under `workflow`.
```

**`history encrypted`。** 実行の履歴を、秘密を読んでよい人だけが持つ鍵で暗号化してある、という宣言で、`workflow` の行の下に書く。

- Temporal(TypeScript・Python・Go):ワーカーとクライアントを、ペイロードのコーデックを渡さないと作れない形にする。TypeScript は `WorkerConfig.codec` を要るフィールドにし、クライアントの関数は `encryptedClient(connection, codec)` だけが返す `EncryptedClient`(`Client` に、モジュールの外から作れない印を足した型)を受け取る。Python は `client.connect(target, codec)` だけが返す `EncryptedClient`(`NewType`)を、関数と `make_worker` が受け取る。Go は、このパッケージの外では作れない `EncryptedClient` インターフェースを `Dial(options, codec)` だけが返し、関数と `NewWorker` が受け取る。どれも、失敗のコンバーターを、失敗の文とスタックトレースを符号化して持つもの(TypeScript は `failure.ts` の `DefaultFailureConverter({ encodeCommonAttributes: true })`、Python は `DefaultFailureConverterWithEncodedAttributes`、Go は `temporal.NewDefaultFailureConverter(… EncodeCommonAttributes: true)`)にし、再生(`replay`)もコーデックを受け取る。宣言の無いフローの生成物は一バイトも変えない。
- Step Functions と Lambda durable functions:生成物を変えない。鍵はステートマシンと関数のリソースに設定するもの(カスタマー管理の KMS キー)で、dandori が書く ASL と関数のコードの外にあり、dandori はそれを確かめられない。
- Argo Workflows:E050。パラメーターを鍵で暗号化する手段が無いので、宣言を守れない(P6)。秘密は Kubernetes の Secret から環境変数かボリュームで渡す。
- pydantic-graph:履歴を残さないので、何もしない。

**クライアントが増やす名前。** `history encrypted` のとき、生成するクライアントが宣言し読み込む名前が増える。TypeScript の `client.ts` は `Connection`・`ClientOptions`・`PayloadCodec`・`encrypted`・`EncryptedClient`・`encryptedClient`、Python の `client.py` は `dataclasses`・`NewType`・`temporalio`・`DataConverter`・`DefaultFailureConverterWithEncodedAttributes`・`PayloadCodec`・`EncryptedClient`・`data_converter`・`connect`、Go のパッケージは `EncryptedClient`・`encryptedClient`・`CodecDataConverter`・`CodecFailureConverter`・`Dial` である。これらは、クライアントがもとから持つ名前と同じに扱う。実装するサービスのメソッドの関数とメッセージの型がこの名前になるときは、1.14 のとおり末尾に `Rpc`(Python は `_rpc`、Go は `RPC`)を付ける。Go のレコードと列挙の型は、どのパッケージにもある名前(`Workflow`、`Start` など)とぶつかるときと同じく、`_` を付けた名前にする(`record Dial` は `Dial_`)。E006 にはしない。どちらも dandori が付ける名前なので生成する側で避けられ、検査で止めると、`.proto` を変えられない利用者が暗号化を宣言できなくなる。TypeScript と Python のレコードと列挙は `types.ts`・`types.py` にあって `T.` を付けて読み、規則の別名は TypeScript と Python では `rules.ts`・`rules.py` にだけ読み込まれ、Go では小文字だけのパッケージの名前になるので、どれもこの名前とはぶつからない。表は各生成器の `ENCRYPTED_CLIENT`(Go は `ENCRYPTED`)に置いた。宣言の無いフローの検査と生成物は変わらない。

**E906。** 送る先は、モデルのプロバイダー(`url` の無いエージェントの OpenAI と Anthropic、このマシンでない `url` のサーバー)、判断のモデルの API(Jev(TypeSafe)、OpenAI の Decisions API、このマシンでない `url` のサーバー。1.11)、URL だけで書いた `http` の送り先(このマシンを除く)、AWS のサービスである。タスクが `discloses <引数> "<理由>"` で書いた引数は通す。`discloses` は E906 だけを通し、W904 は通さない(送ることと、履歴に残ることは別のことである)。

```
エラー[E906]: tests/fixtures/security/E906_payout_holder.flow:41:1: タスク `draft_notice` が、秘密の値 `account.holder` をプロジェクトの外の OpenAI に送ります
    41 |   let notice = draft_notice(amount: amount, payout_id: paid.payoutId, holder: account.holder)
  = 印は ../../../examples/payout/specs/payout.proto:35 の `debug_redact = true` です。
  = 参照か、相手に要るものだけを送ってください。そこへ送ることを意図しているなら、タスクの下に `discloses holder "<理由>"` と書いてください。
```

**プロジェクトの中へ送ること。** 秘密の値を、プロジェクトの中のファイル(OpenAPI の文書、`.proto`、Connect の規則、子の `.flow`、帳簿、日付のファイル)へ送る呼び出しは、ritsu の口 `Flows::sends`(`src/secrets.rs` の `sends`)で渡し、地図の上で送ってよいかを ritsu-cross が確かめる(ritsu の E905)。dandori は地図を読まない。パスは `RuleCall::rule` と同じく、dandori が届くパスである。

**例。** `examples/payout`(英語の `payout.flow` と日本語の `payout.ja.flow`、契約は `specs/payout.proto` と JSON の名前を日本語にした `payout.ja.proto`)は、口座の番号と名義に `debug_redact` を付けた銀行の契約を使い、口座の ID だけを運ぶので、どの検査も何も言わない。どのプラットフォームでも走る例で、シナリオを全部のプラットフォームで突き合わせる。`tests/fixtures/security` の変異が、番号を運ぶ形(W904)、名義をエージェントに読ませる形(E906)、`discloses` で通す形を作る。問い合わせの例の Temporal 版は、Open Responses のエンドポイント(この例では Ollama)を `http://ollama.internal:11434/v1` で呼ぶので、`plaintext "<理由>"` を書き足した。

**確かめ方。** `tests/fixtures/secrets.flow`(英語)と `secrets.ja.flow`(日本語)は、三つの印の出どころ、レコード、リスト、文字列への埋め込み、`for`、`some`、`json`、規則の結果(秘密にならない)、`writeOnly` だけのプロパティと `internal`(印にならない)、`discloses` を一つのフローで見せ、`check` の英語と日本語のテキストと JSON、`Flows::sends` の答えを golden にする。`tests/fixtures/security` に、四つのコードと E007・E001・E050 の変異を英語と日本語の対で置いた。`tests/secrets.rs` は、`history encrypted` の生成物が、コーデックを渡すプログラムでは `tsc --strict`・`mypy --strict`・`go vet` を通り、渡さないプログラムでは型の誤りになることと、`tests/encrypted/pay.flow` を Temporal の dev server で三つの SDK から、ペイロードを base64 にするコーデックで走らせ、コーデックの無いクライアントで読んだ履歴に、入力の値も失敗の文も平文で残らないことを確かめる。クライアントが増やす名前は、二つで確かめる。一つは、暗号化したときに増える名前が三つの表とちょうど同じであること(`tests/encrypted/pay.flow` と、そこから宣言を除いたフローの生成物を比べる)。もう一つは、レコード・列挙・メソッド・メッセージにその名前を付けた `tests/encrypted/names.flow`(英語)と `names.ja.flow`(日本語)で、生成物の名前が変わり、`tsc --strict` と `go vet` が通り、`mypy --strict` が名前の二重の定義を言わないこと(サービスを実装するフローの Python は、もとから、始めるメソッドが `start` に TypedDict を渡すところで `mypy --strict` を通らない)。名前を変えない生成器では、`tsc` が `Import declaration conflicts with local declaration of 'Connection'` を、`go vet` が `CodecDataConverter redeclared in this block` を言うことを、実際に走らせて確かめた。

### 1.19 手元の規則を呼ぶフローのシナリオ(2026-10-11)

**きっかけ**:`ritsu dandori scenarios` は、規則の呼び出しもタスクと同じく選ぶ場所として扱い、選んだ答えをシナリオの並び(`answers`)に書いていた。`ritsu run` は手元の規則(Connect のサービスでない `use rule … from "*.rule"`)を rulec で計算し、シナリオから答えを読まない(5 章の「規則と期日と帳簿を計算しながら流す」)。だから、規則を一度呼ぶたびに、後ろの答えが一つずつずれた。出来事を受け取るたびに規則を呼ぶフローでは、`ritsu dandori scenarios` が書いたシナリオのどれも、`ritsu run` で生成器が選んだ終わり方にならなかった。形の合わない答え(`{}`)で `Dandori.BadResponse` になるシナリオと、`Dandori.Test.Failure`(`scripted`)で終わるシナリオは、生成器が分岐を通すためにわざと選んだもので、ずれとは別である。

**決定**:

- **`ritsu dandori scenarios`(rulec の口がつながっているとき)は、手元の規則の呼び出しを口で計算し、答えをシナリオに書かない**(`scenarios::generate_computed`)。呼び出しが規則に渡す値は、決まっていないものをそこで選び(列挙、真偽、無いことのある値、リストの長さ)、口が返した出力で先へ進む。計算は `ritsu run` と同じもの(`computed::rule_answer`・`rule_outputs`)で、生成したコードが入口で受け付けない入力は、呼び出しの失敗になる。規則の答えを変えたいときは、規則の入力の出どころ(それより前のタスクの答え)を選ぶ。そういうシナリオには `"rules": "rulec"` を書く。
- **Connect のサービスとして呼ぶ規則は、これまでどおり答えを書く。** ネットワークを通る答えなので、シナリオが答える。
- **`dandori run` は、`"rules": "rulec"` のシナリオの規則の呼び出しを、口で計算する**(`computed::RulesComputed`)。だから `ritsu dandori run` は、そのシナリオで `ritsu run` と同じ実行になる。
- **`ritsu run` は、古い形のシナリオを、規則の呼び出しで止める。** 生成器が書いたシナリオ(`covers` がある)に `"rules"` が無ければ、規則の答えを並びに持っている。`ritsu run` はその規則の呼び出しに着いたところで、ずれて進まずに、「規則 step の呼び出しの答えを、シナリオが自分で持っています……`ritsu dandori scenarios` でシナリオを書き直すか、このシナリオは `ritsu dandori run` で流してください」と言って 2 で終わる(シナリオを流せないときの終了コード)。手で書いたシナリオ(`covers` が無い)は、これまでどおり読む。
- **ループが一歩の上限(`MAX_STEPS`)で切れた実行は、規則を口で計算するときは書かない。** 切れた所には終わりが無く、口が答える規則はここでの選択のとおりには回らないので、`ritsu run` ではそこで答えが尽きる。
- **dandori のクレートの単体のバイナリは変わらない。** 規則を読めない(E005)ので、`dandori scenarios` は口を持たず、`generate` はこれまでどおり全部の呼び出しに答えを書く。

**捨てたもの**:

- **`ritsu run` が、規則の答えをシナリオから読むこと。** `ritsu run` の役目は規則を rulec で計算して流すことで、読めば規則を確かめたことにならない。
- **計算した答えも、シナリオに書いておくこと。** `ritsu run` はそれを読み飛ばすことになり、二か所をそろえておくことになる。
- **古い形のシナリオを、読んだ時点で止めること。** 規則を呼ぶ前に終わるシナリオは、古い形でもそのまま流せる。

**正直な限界**:帳簿のタスクと日付の呼び出しの答えは、まだ口で計算しない。帳簿を使うフローのシナリオを `ritsu run` で流すと、帳簿はシナリオの `books`(生成器は書かない)から始まるので、生成器が選んだ終わり方にならないことがある(ritsu のテストのプロジェクト `invoice` では、英語の版と日本語の版の 30 本ずつが、どれも在庫切れで終わる)。規則と同じ形で、生成器に帳簿と日付の口を渡すのは、残した仕事である。

**確かめたこと**:ritsu のテストのプロジェクト `crates/ritsu/tests/projects/ticket_desk`(問い合わせの窓口。出来事ごとに規則が次の段階を決める。英語の版と日本語の版)で、`ritsu dandori scenarios` が書いた 10 本ずつのシナリオを `ritsu run` に通した。どれも `"rules": "rulec"` を持ち、終わりまで流れ、シナリオの答えを全部使い、規則の呼び出しは全部 rulec が答え、`ritsu dandori run` と同じ実行になった。終わり方には、解決と、放置・長すぎ・形の合わない答え・失敗がある。古い形に直した一本は、`ritsu run` が 2 で止まってそう言い、`ritsu dandori run` では同じ実行になる(`crates/ritsu/tests/run.rs`)。rulec の DESIGN の §15.209 にも同じことを書いた。

### 1.20 `.flow` の列挙の範囲(2026-10-19)

**きっかけ**:規則の、順序のある列挙の入力に書いた範囲(`campaign : tier  range >=basic <=gold`)は ritsu の規則の口を通り、dandori は、その入力に渡す値が範囲の外になりうれば E014 にする(1.3)。ところが `.flow` は、列挙に範囲を書けなかった。端に列挙の値を書くと構文の誤り(E001。端には整数が要った)、整数を書くと E003 である。列挙の値を書けるのも型の分かるところだけで、`let top = platinum` は E002 になる。だから、ワークフローが受け取った値をその入力に渡すには、`match` で分けて、値ごとに呼び出しを書くしかなかった。数なら、ワークフローの入力に `range` を書いて狭められる。

**原因**:範囲は数のものとして作ってあった。構文は端に整数と単位の付いた数だけを読み、型の検査は数でない型に書いた範囲を E003 にした。範囲を持つ場所の型(`Range`)も、それを確かめるところ(E014、参照インタプリタ、七つの生成先の入口と結果の確かめ、シナリオ)も、数の範囲だけを扱っていた。

**決定**:

- **書き方と場所。** 順序のある列挙の値の範囲を、規則と同じく端の値で書く(`range >=basic <=gold`。端は片方でもよい)。書ける場所は数と同じで、ワークフローの入力と出力、レコードのフィールド、タスクの引数と結果である。`T?` と `list[T]` に書いた範囲は、中の値の範囲になる。
- **書けない列挙。** `.flow` で宣言した列挙、`.proto` の列挙、規則が `|` で宣言した列挙には順序が無いので、範囲は E003 にし、文は順序が無いことを言う。列挙の値でない端、値の一つも入らない範囲(`>=gold <=silver`)、数の範囲に書いた列挙の値の端も E003 である。
- **持ち方。** 範囲を持つ場所の型 `Range` に、列挙の値の位置(列挙の順で 0 から)として持たせ、両端をいつも持つ(書かなかった端は、最初か最後の値)。どちらの範囲かは隣の型が決め、値で言うときは `Model::values_in` と `Model::range_text` を通す。範囲を運ぶ道(入力・出力・フィールド・タスクの範囲の表と、生成器の確かめる関数の引数)は数のものをそのまま使い、確かめるところは、型で分けて範囲に入る値の一覧を引く。言語をまたぐ検査に渡すもの(`crossings`)には、列挙の範囲を数の範囲として渡さない(範囲の分からない場所として渡す。前と同じ)。
- **E014。** 数と同じく、`.flow` の場所に書いた範囲でも確かめる。値がどこから来うるか(`Est`)は、範囲を書いた場所から来る値を範囲に入る値のどれかとして持ち、列挙の値の確かめ(`fit_values`)は、規則の入力にも `.flow` の場所にも使う。範囲が規則の入力の範囲に収まれば、そのまま渡せる。前の E014 の注(`.flow` の列挙には範囲を書けない)は、値の出どころに範囲を書くか、`match` で分けて範囲に入る値を書いて渡すかを言う注にした。書いた値と、書いた値だけのリストには、注を付けない(直すところは、その値そのものである)。規則の出力のように範囲を書けない出どころの値は、これまでどおり `match` で分ける。
- **入ってくる値。** 範囲を書いた入力・フィールド・タスクの結果(コールバックとイベントの値も)は、参照インタプリタと七つの生成先が型を確かめる式で確かめ、範囲の外なら `Dandori.BadInput` か `Dandori.BadResponse` で失敗する。式は、Step Functions の JSONata と Argo の式が値の一覧への `in`(Argo の式は、11 章で、文字列であることを先に確かめる形にした)、TypeScript(Temporal と durable functions)が `([…] as readonly unknown[]).includes(…)`、Python(Temporal と pydantic-graph)が `in (…)`、Go が `ddIsOneOf(x, …)` である。範囲の無い列挙の確かめは、この変更では変えていない。
- **`match`。** 範囲に収めた値の `match` は、範囲に入る値だけで分ける。範囲に入る値の分岐が無ければ E010、範囲の外の値の分岐は通らない分岐(E011)である。値がどこから来うるかは、E014 と同じく、変数に入れたすべての値を合わせて求める(`ranges::held`)。ただし、書いた値(`let x: tier = gold`)は列挙のどの値にもなりうるものとして数える。書いた値でも狭めると、範囲を書いていないフローの `match` まで E011 に変わりうるからである。範囲で狭めた値は、診断の中で列挙の順に並べる。
- **子の `.flow` と API の記述。** 数の範囲を比べるところで、列挙の範囲も比べる。子の入力の範囲に収まらない引数と、範囲の無い引数は E015 で、子の出力とタスクの結果のフィールドも同じである。API の記述とは、送る値を範囲に入る値だけとして比べ、記述が返しうる値が結果の範囲の外にあれば E016 にする。
- **エージェントと判断のモデル。** 結果の列挙に範囲があれば、エージェントの Schema の `enum` を範囲に入る値の一覧にする(OpenAI も Claude も受け付ける)。Jev には、choice も score も範囲に入る値の中から尋ね、範囲の外の値に意味を書けば E007 にする。範囲の外の値を尋ねれば、モデルがそれを選んだときに結果の検査で失敗するからである。
- **シナリオ。** 値を選ぶところ(列挙の値、オプショナルな値の中、リストの項目)は、範囲に入る値から選ぶ。結果の検査に落ちる結果として、範囲のすぐ外の列挙の値を返す結果も作る(数の範囲の外の結果と同じ扱い)。範囲を書いた列挙の入力(入力のレコードのフィールドも)があれば、その最初の一つを範囲のすぐ外の値にした入力の実行を一本足す(`input:outside`)。どのプラットフォームも、何も呼ばずに `Dandori.BadInput` で失敗する。生成したコードの入口が範囲の外の値を受け付けないことを、どのプラットフォームの突き合わせでも確かめるためである(9 章で、数の範囲を書いた入力にも広げ、最初の一つだけでなく、範囲を書いた場所の一つずつに一本ずつ足すようにした)。
- **図。** `dandori doc` の入力と出力の一覧で、範囲を書いた列挙の型に範囲を添える(`campaign: tiers.tier range >=basic <=gold`)。型の名前だけでは、列挙のどの値も受け取るように読めるからである(9 章で、数の範囲も添えるようにした)。

`tests/fixtures/enum_range_places.ja.flow` の検査の一部(英語の版は `enum_range_places.flow`):

```
エラー[E010]: tests/fixtures/enum_range_places.ja.flow:46:1: `match` に ゴールド の分岐がありません。ここで `選んだ段階` はその値を取りえます
    46 |   match 選んだ段階
エラー[E011]: tests/fixtures/enum_range_places.ja.flow:48:1: 分岐 `プラチナ` は通りません。ここで `選んだ段階` がとりうる値は シルバー・ゴールド だけです
    48 |     プラチナ => pass
エラー[E010]: tests/fixtures/enum_range_places.ja.flow:50:1: `match` に none の分岐がありません。ここで `無いかも` はその値を取りえます
    50 |   match 無いかも
```

**捨てたもの**:

- **`match` の腕の中で、その腕の値に狭めること。** `match campaign` の `basic, silver, gold` の腕の中で、`campaign` をその三つのどれかとして規則に渡せるようにする案である。数で分けた枝(規則の答えで分けたもの)の中でも数を狭めていないので、数と列挙の両方の枝の絞り込みとして決めることになる。出どころで範囲を言う形は、数と同じ考えに乗り、狭める仕組みを新しく持ち込まない。
- **範囲を、範囲に入る値の並び(`Vec<String>`)で持つこと。** 範囲を運ぶ道はどれも `Range` を受け取る。別の型にすると、数のための道と並べて、もう一本通すことになる。位置にすれば、変わるのは型で分けるところだけである。
- **範囲を型の一部にすること**(`tier[basic..gold]` のように、狭めた別の型を作る)。値を渡せるかの決まり(型が等しいか)と、生成するコードの型の名前が変わる。範囲は値を置く場所に付けるという 1.3 の決めのままにした。
- **シナリオを範囲の中の値だけで作ること。** それでは、生成したコードの入口の確かめを、どのプラットフォームでも範囲の外の値で試さない。

**変わること**:

- 範囲を書けるのは新しい書き方で、範囲を書いていない列挙の検査、生成物、シナリオ、図は変わらない(下の突き合わせ)。前は通っていたフローは、この変更で落ちない。
- 文が三つ変わる。E014 の列挙の注、数でないものに書いた範囲の E003(「範囲を書けるのは、数(`int` か単位の付いた数)と、順序のある列挙だけです」)、範囲の端の書き誤りの E001(「ここには整数か列挙の値が要ります」)である。
- 範囲を書いた列挙の入力を持つフローのシナリオには、範囲の外の入力の実行が一本増える。

**確かめたこと**:

- `tests/flows/lift.flow`(英語)と `lift.ja.flow`(日本語)は、範囲を書けなかったころの再現のフローに範囲を書いた形で、入力、入力のレコードのフィールド、タスクの結果、タスクの引数、出力の範囲と、範囲に収めた値の `match` を持つ。検査を通り、七つの生成先に組める。シナリオ(16 本ずつ。範囲の外の入力の実行と、範囲の外の結果の実行を含む)を、Step Functions(JSONata)、durable functions、pydantic-graph で参照インタプリタと突き合わせ、ritsu の Lean のモデル(`DandoriCore`)とも突き合わせた(ritsu-model のテスト。Lean のモデルには、範囲を書いた列挙の場所を、範囲に入る値の列挙として渡す)。Lean のモデルに範囲を渡さないと、この二本が食い違うことも確かめた。Temporal(TypeScript・Python・Go)と Argo の突き合わせは、この変更では走らせていない。代わりに、生成した TypeScript・Python・Go の入力の確かめ(`is_WorkflowInput`)を、範囲の中と外の六つの入力で直接評価し、どれも参照インタプリタと同じに受け付けた・受け付けなかった。
- 診断の材料(英語と日本語の対):`enum_range_syntax`(E003)、`enum_range_places`(E014・E010・E011)、`enum_range_child`(E015。子は `children/offer.flow` と `children/案内.flow`)、`enum_range_api`(E016。記述は `apis/offers.json` と、値を日本語で書いた `apis/案内.json`)、`enum_range_jev`(E007)。
- 替える前と後の突き合わせ:木の 213 本の `.flow`(足した 14 本を含む)を、替える前のバイナリと後のバイナリの `ritsu dandori` にかけた。`check`(英語・日本語のテキストと JSON の英日、終了コード)、`build`(七つの生成先。書いたファイルを名前と SHA-256 で)、`doc`(Markdown と HTML の英日)、`scenarios` の 16 の出力、3,408 である。変わったのは 17 本の 272 で、足した 14 本と、文の変わった 3 本(`enum_ranges.flow` と `enum_ranges.ja.flow` の E014 の注、`values.flow` の E003 の文)だけだった。3 本で変わったのは、その文と、それを中に持つ HTML の図だけである。

**見つけて直していないもの**:

- 数の範囲を書いた入力には、範囲の外の入力の実行が無い(前から)。数の入口の確かめは、どのプラットフォームでも範囲の外の値で試されていない(9 章で直した)。
- `doc` のページは、入力と出力の数の範囲を出さない(前から。9 章で直した)。

## 2. 案件とステートマシン

### 2.1 案件の状態を追う

案件は、外部のサービスが持つ実体で、rulec のステートマシンに従う。たとえば Stripe の PaymentIntent を、Stripe の文書から rulec に転記した `payment_intent.rule` に従わせる。

検査は、実行の各点で案件ごとに、状態の組を集めて持つ。組の一つ目は案件のレコードが言う状態で、ワークフローが最後に聞いた状態である(まだ始めていなければ無し)。二つ目は、そのときかそれより後に、外部のサービスで案件がいた状態である(始まっていなければ無し)。組ごとに、そこへ着く短い例も持つ。始まったか(はい・いいえ・どちらもありうる)は、レコードが状態を言うかで決まる。

二つの状態が分かれるのは、イベントを送る呼び出しが失敗したときである。外部のサービスではイベントが起きていても、ワークフローは答えを受け取っていないので、レコードは前の状態のままになる(2.3)。

`sends e` の呼び出しでは、組の二つ目の状態と、そこから外部のサービスのイベントで移れる状態(2.2)から、ステートマシンの表で `e` のあとの状態を求める。表の軸のうち、状態とイベントと held で決めたもの以外は、外部のサービスが決める値として全部の座標を試す。行が受け付ける座標は rulec の証明書にあるので、`policy first` の表で前の行に取られる行も数えない。

案件の状態で分かれる `match` が読むのは、案件のレコードである。だから `match` は組を一つ目の状態で絞り、二つ目の状態は絞らない。レコードを見て選んだ分岐の中でも、案件は外部のサービスで先へ進んでいることがある。分岐が受けられるのはレコードが言いうる状態だけで、そこでレコードが言うはずのない状態の分岐は、通らない分岐(E011)になる。E020(2.5)が見るのは二つ目の状態である。

ritsu の Lean のモデル(`proofs/DandoriCore/Check.lean`)も同じ組を持ち、組で調べる検査が通れば、検査が見る終わり方で終わるどの実行でも、引き渡す案件のほかは始まっていないか終わりの状態にいることを証明している(ritsu の DESIGN 11)。案件を始める呼び出しの失敗(2.3 の三つ目)と、拒否のエラー(2.3 の一つ目の前提)も、Lean の検査はこの検査と同じに読み、どちらも定理の前提にしている(ritsu の DESIGN 11.2 の 4)。

### 2.2 外部のサービスのイベント

`external` に書いたイベントは、ワークフローが何もしなくても起きる(客の認証、銀行の結果、与信の有効期限)。ワークフローが案件に次にタスクを呼ぶときと、ワークフローが終わるときには、組の二つ目の状態を、そのイベントで移れる状態まで広げてから考える。

これで、チェックアウトまで待つあいだに与信の有効期限が切れて `canceled` になり、capture が拒否される、という流れが検査に入る。

### 2.3 拒否と失敗

- ステートマシンがイベントを拒否する状態があれば、その呼び出しは `refused as` のエラーで返ってくる。拒否する状態が一つでもあるのに受けていなければ E022、どの状態でも拒否するなら E021。拒否されたとき、案件は拒否した状態のままで、レコードも呼ぶ前のままである。拒否だけを受ける `on <エラー>` は、案件が拒否する状態にいる組だけを受ける。`refused as` のエラーは拒否のときにしか返ってこない、というのが宣言の意味で、検査はそれを前提にしている。
- 拒否以外で失敗したときは、外部のサービスでイベントが起きたかどうかが分からない。検査は、起きた場合と起きなかった場合の両方を持って先へ進む。どちらの場合もレコードは呼ぶ前のままなので、起きた場合の組は、レコードが前の状態を言い、案件はイベントのあとの状態にいるものになる。
- 案件を始める呼び出し(`starts`)が失敗したときは、案件は始まっていないものとして先へ進む。外部のサービスで案件ができていても、レコードが無いので、ワークフローはその案件を特定できない。そのとき案件を見失わないための手当てが `key` で、無ければ W103 が勧める。

`tests/fixtures/capture_timeout.flow` は、確定がタイムアウトしたあと、レコードの `pending` を見て取消を送り、拒否されたら `succeed` で終わる。確定が外部のサービスでは済んでいた場合、案件は `processing` のまま残る。検査はそれを次のように言う(`--lang ja`)。

```
エラー[E020]: tests/fixtures/capture_timeout.flow:41:1: 案件 `p` が processing のまま、ここでワークフローが終わることがあります(終わりの状態は done・voided)
    41 |         on unexpected_state => succeed
  そうなる例:
      35  open_payment: p が pending で始まる
      36  capture_payment: p が pending → processing
      37  capture_payment が失敗する: on timeout
      39  match p.status: pending
      40  void_payment: void が拒否される(unexpected_state)
      41  succeed
```

拒否されたら `fail … leaving p` で引き渡せば通る(`tests/fixtures/capture_timeout_handed_over.flow`)。

### 2.4 まだ始まっていない案件を見る

始まっていない案件に `observes` のタスクを当てると、すでにある案件を読み込む意味になる。状態は、ステートマシンが始まりの状態からたどり着けるどれでもありうる。倉庫のシステムにある注文を読むときがこの形である。

### 2.5 終わり方

`succeed`・`fail`・flow の終わりで、始まった案件がどれも終わりの状態(rulec の `final`)にいるかを見る。外部のサービスのイベントで移れる先も含めて、終わりでない状態が残れば E020 とする。`fail … leaving c` は、終わっていない `c` を承知で引き渡す宣言である。

処理されなかったタスクのエラーで失敗する場合も数える。`on failure` があればそのブロックを検査し、無ければ案件ごとに W101 を一つ出す。

### 2.6 キャンセル

Temporal では、ワークフローに止まるよう頼める(キャンセル)。頼まれたワークフローは、後始末をしてから、キャンセルされたとして終わる。`on cancel` はその後始末を書くブロックである。

```
on cancel
  match 受注.状態
    none => pass
    受付, 入金済 =>
      受注 <- 取消を頼む(id: 受注.id)
        on 拒否された => pass
    出荷済 => fail ShippedAlready "もう出荷されています" leaving 受注
    配達済, 取消 => pass
```

- キャンセルは、呼び出しの結果を待つあいだか、待ち(`wait`、コールバック)のあいだに届く。`flow` の中でも `on failure` の中でも届く。流れはそこで止まり、`on cancel` が走り、ワークフローはキャンセルで終わる。`on cancel` の中の `fail` は、ワークフローを失敗で終える。`succeed` は書けない(E009)。
- `on cancel` の中にはキャンセルは届かない。後始末の呼び出しは、キャンセルに止められずに最後まで走る。`on cancel` の中で処理できなかったタスクのエラーは、`on failure` を通らずにワークフローを失敗させる。
- キャンセルはタスクのエラーではない。`retry` はリトライせず、`on failure =>` などでも処理されない。並列のイテレーションの中で届いたキャンセルは、すべてのイテレーションを止め、それより前のイテレーションが失敗していても、キャンセルのほうが決める。
- `on cancel` が無ければ、ワークフローはキャンセルが届いたところで終わる。Step Functions の `StopExecution` と同じく、案件は片付けられない。
- 検査は、キャンセルが届きうるところ(`on cancel` の外のすべての呼び出しと待ち)から `on cancel` に入る。呼び出しの途中で届いたキャンセルでは、外部のサービスでイベントが起きたかどうかが分からないので、失敗のときと同じく両方を持つ。`on cancel` の終わりで案件が終わりの状態にいなければ E020、後始末の呼び出しが失敗しうれば W101。
- Temporal のほかのプラットフォームでは、`on cancel` は E050。Step Functions と durable functions は、実行を止めるとその場で終え、あとに何も走らせない。Argo の `argo stop` は exit handler を走らせるが、`on cancel` をそれとして書くのはまだである。pydantic-graph も同じくまだである。

### 2.7 帳簿の仮押さえ

仮押さえの振替は、帳簿の口が渡すステートマシンを持つ。状態は `held`(押さえ中)・`posted`(確定)・`voided`(取消)・`expired`(期限切れ)、イベントは `post`・`void`・`expire`、始まりは `held`、終わりは残りの三つである。表の行は、拒否するなら理由(`reason`)を持つ。押さえ中でない仮押さえの確定は `already_voided` や `expired` で、取消は `already_posted` や `expired` で拒否され、確定した仮押さえをもう一度確定すると、前にしたとして通る。

- 仮押さえを作るタスク(`book … hold`)は `starts <帳簿>.<振替>`、確定は `sends post`、取消は `sends void` を書く。合わなければ E008 で、`do` は案件を動かさない。
- 有効期限のある振替(`pending expires after …`)では、`expire` が外で起きるイベントになる。検査は、`external expire` を書かなくても、確定や取消の前に期限が切れうることを数える(2.2)。期限の無い振替には `expire` が無い。`expire` のほかの `external` は E008。
- 拒否された理由は、行の `reason` そのもので、タスクの宣言したエラーとして返ってくる。理由ごとに、拒否されうるのに宣言していなければ E022、宣言していても `on` で受けていなければ E022、どの状態でも拒否するなら E021 である(2.3)。`refused when` は書けない(E008)。拒否するかどうかと理由は、帳簿が状態ごとに決めているからである。
- 案件のレコードは、仮押さえのレコード `<帳簿>.<振替>`(キーの引数と `state`)である。
- レコードの `state` は操作の結果から来る(`hold` は `held`、`post` は `posted`、`void` は `voided`)ので、レコードが `expired` を言うことはない。期限切れは、確定や取消が `expired` で拒否されて分かる。仮押さえの状態で分かれる `match` に `expired` の分岐を書くと、通らない分岐になる(E011。2.1)。
- 確定が失敗したときは、帳簿では確定していることがある(2.3)。そのあとレコードの `held` を見て取消を送ると、帳簿は `already_posted` で拒否することがある(E022)。

`tests/fixtures/book_refusals.flow` の検査の一部:

```
エラー[E022]: tests/fixtures/book_refusals.flow:41:1: ここでは帳簿が `post` を `expired` で拒否することがあります(`押さえ` が expired のとき)。`出荷する` の `errors` に `expired` を書き、`on expired =>` で処理してください
    41 |       押さえ <- 出荷する(注文: 受注.id, sku: 受注.sku)
  そうなる例:
      38  引き当てる: 押さえ が held で始まる
      40  match 受注.急ぎ: true
          外部のサービスで `expire` が起きる: 押さえ held → expired
```

chobo の DESIGN 5 章の案は、案件に `external expire` と `refused when refused = true` を書いていた。帳簿を読めば分かることをもう一度書かせると、書き忘れたときに検査が期限切れを数えず、理由も一つの `refused as` にまとまってしまう。そこで、`expire` は帳簿から足し、`refused when` はエラーにする(6 章)。

## 3. 検査の一覧

| コード | 内容 |
|---|---|
| E001 | 構文 |
| E002 | 名前が無い(型・変数・フィールド・規則・タスク・日付のファイルの日付・帳簿・帳簿の振替・`timeout until` が挙げるタスクの引数。単位の表に無い単位と次元、税区分の誤り、記述に無い型の名前、型を作れない記述の名前、`.proto` が import して読めなかったファイルにある型を作ろうとしたときも) |
| E003 | 型が合わない(オプショナルな値をそのまま使う、`none` の置き場所、リストのリスト、型の分からない `{…}` や `[]`、数でも順序のある列挙でもないものの範囲(順序の無い列挙の範囲も)、値の入らない範囲、型の次元に無い単位や整数にならない値で書いた範囲の端、列挙の値でない範囲の端、自分自身を含むレコードも。手で書いたものも、`.proto` から作ったものも。UTC オフセットを言わないカレンダーの日付に渡す時刻も。`timeout until` の引数が `timestamp` でないときも(16 章)) |
| E004 | 引数や出力の過不足、`{…}` のレコードのフィールドの不足 |
| E005 | 規則・日付のファイル・帳簿を読めなかった(rulec の検査を通らない規則、規則を読まない dandori のクレートのバイナリで読もうとした規則、要素の並びをたどる規則、無いことがある入力か出力(`T?`)を持つ規則も。`connect` で呼ぶ規則について、rulec が Connect のサービスを言わないとき、サービスが列挙の値を何と呼ぶかを言わないときも。koyomi の検査を通らない日付のファイル、chobo の検査を通らない帳簿、無いファイルも)。出力の無い規則(rulec の `enums` のファイル)を呼んだ(呼び方によらず、呼ぶところで。10 章) |
| E006 | 二度の宣言(生成するコードで同じ名前になる型、規則と API に付けた同じ名前、規則・日付のファイル・帳簿に付けた同じ名前も)。生成するコードでぶつかる規則の名前(規則の別名がまわりのコードの名前と同じ、別名が同じ二つの規則、タスクと規則のアクティビティが同じ名前。1.15) |
| E007 | タスクの項目の誤り(`flow` のタスクのほかの呼び出し方と `image`、OpenAPI の操作を呼ぶタスクの `form`、Connect のエラーコードの誤り、ステータスの付け忘れ、同じステータスの二つのエラー、呼び出し方の重なり、`aws` の `key` の引数、`callback` にできない呼び出し方、知らない AWS のサービス、子ワークフローの `key`、エージェントの `model` の書き忘れ・宣言したエラー・`key`・Schema に書けない応答・知らないプロバイダー・Claude で大文字と小文字だけが違う列挙の値、`event` のタスクの引数(`timeout until` の時刻のほか)・呼び出し方・`retry`・`key`・案件を始めることなど、`timeout until` を答えを待たないタスク・`aws` のコールバックに書く、`timeout until` のタスクの `retry` が `timeout` をリトライする、`timeout` を二つ書く(16 章)、エージェントでないタスクや Claude の `url`、http でも https でもない `url`、判断のモデル(`jev`)が答えられない結果の型・尋ねていないフィールド・意味の誤り(答えの列挙の範囲の外の値に書いた意味も)・下限の誤り・`confidence` のエラーのリトライ・ステータスの無いエラー・知らない API・System One のタスクの `refusal`・`refusal` のエラーのリトライ・Decisions API の predicate のはいといいえの意味・選択肢や段階が一つだけの列挙)。規則の項目の誤り(`lambda` と `connect` の重なり、`connect` の無い規則の `connection`、http でも https でもない `connect`)も。帳簿の操作のタスクの、振替に無い操作、`key`、`refused as`、エラーの `= …` も。`plaintext` を自分の URL を持たないタスク・`connect` の無い `use rule`・URL の無い `use` に書く、`discloses` がタスクに無い引数を書く、`plaintext` と `discloses` の理由が空の文字列(1.18)も |
| E008 | 案件の宣言や、案件に何をするかの誤り(`.proto` から作った列挙の値がステートマシンの状態と違う、案件の型も。帳簿の仮押さえでは、案件に合わない操作、`expire` のほかの `external`、`refused when` も) |
| E009 | 文の置き場所の誤り(`yield`、並列のイテレーションの中の `break`・`succeed`・案件の呼び出し・イベントの待ち、`on failure` と `on cancel` の中の `succeed`、中と外の両方で値を入れる変数、規則を `let` なしで呼ぶことも) |
| E010 | `match` のどの分岐にも当たらない値がある(範囲に収めた列挙の値なら、範囲に入る値のどれか) |
| E011 | 通らない分岐(範囲に収めた列挙の値の、範囲の外の値の分岐も) |
| E012 | 値を持っていないことがある変数を読んだ |
| E013 | 始まっていない案件にタスクを呼んだ、始まった案件をもう一度始めた |
| E014 | 範囲のあるところ(規則の入力、タスクの引数、書き出したレコードのフィールド、出力)に、範囲を外れることのある値を渡した。順序のある列挙の値の範囲(規則の入力と `.flow` の場所)では、範囲に入らない値になりうる値を渡した |
| E015 | ほかの `.flow` を走らせるタスクが、子と合わない(引数と入力、結果と出力、宣言したエラーと子の `fail`。子を読めない、子が検査を通らない、自分を走らせるフローも) |
| E016 | タスクが API の記述と合わない(無い操作、受け取らない引数、要る引数の不足、型・範囲・列挙の違い、結果が省くフィールドを `T?` にしていない、返さないステータス、無い例外、冪等トークンでない `key`、ストリームのメソッド。記述を読めないとき、`connect` で呼ぶ `.proto` に `url` が無いとき、メソッドやメッセージが読めなかった import の型を使っているときも)。帳簿の操作のタスクが振替と合わない(引数の名前と型、仮押さえでない結果、`do` の結果、帳簿の拒否の理由にならないエラー)も |
| E017 | ワークフローが、`implements` で指定したサービスと合わない(無いサービス、`.proto` でない記述のサービス、ワークフローの名前とバージョン、dandori の印の無いメソッドと印の二つあるメソッド、ストリーム、始めるメソッドの数、リクエストのフィールドと入力、レスポンスのフィールドと出力、`fails` とフローの `fail`、`event` と `answer` のメソッドのタスクと値、状態のメソッドの数と形、`dandori/v1/options.proto` の import、読めなかった import にあるメッセージ) |
| E018 | 規則・日付のファイル・帳簿を読めない dandori(このクレートのバイナリ)で、それらを使うフローを確かめた(読めないものの最初の宣言で一度だけ言い、exit 2) |
| E020 | 終わりでない状態の案件を残して終わる(`on cancel` が終わってキャンセルで終わるときも) |
| E021 | どの状態でも拒否されるイベントを送った(帳簿の仮押さえなら、帳簿がどの状態でも拒否する操作) |
| E022 | 拒否されることがあるイベントの拒否を処理していない(帳簿の仮押さえなら、帳簿が拒否しうる理由を宣言していない、宣言しても `on` で受けていない) |
| E030 | 外部のデータを変える呼び出しを、`key` なしでリトライする |
| E031 | Express でできないこと(五分を超える待ち、コールバック、ネストした実行、`key` なしで外部のデータを変える呼び出し) |
| E040 | プラットフォームで、一回の実行が大きくなりすぎうる(ビルドがプラットフォームごとに見る)。実行履歴が上限を超えうる(Step Functions は 25,000 件、Temporal は 51,200 件、Lambda durable functions は 3,000 操作)。Argo Workflows では、一回の実行のノードが目安の 10,000 個を超えうる |
| E050 | プラットフォームに出すのに要るものが無い、またはプラットフォームでできないこと(Step Functions では呼び出し方か `connection`(エージェントと判断のタスク、`connect` で呼ぶ規則にも要る)、ネストした実行の宣言したエラー、`http`・`agent`・`jev` の 60 秒を超える `timeout`、HTTPS でない送り先(規則のサービスも)。Temporal のほかでは `on cancel` と `event` のタスク、サービスの `status` のメソッド。Step Functions と durable functions では、呼ばれる規則の `lambda` か `connect` と、呼ばれる日付の `lambda`。Step Functions では、帳簿の `lambda`。durable functions では invoke する関数(`durable function` のタスク)の `timeout`。Argo では呼び出し方か `image`、`workflow template` の宣言したエラー、`callback` のタスクの `retry`、`history encrypted`(1.18)) |
| E906 | 秘密の値を、プロジェクトの外の相手(モデルのプロバイダー、判断のモデルの API(Jev、OpenAI の Decisions API、このマシンでない `url` のサーバー)、URL だけで書いた相手、AWS のサービス)へ送る(1.18) |
| W030 | 外部のデータを変えるかもしれない呼び出しを `key` なしでリトライする |
| W032 | 確信度を使う判断のタスクのモデルが、ここを変えなくても別のバージョンに移る(Jev のエイリアス、`url` のサーバーのタグの無い名前と `:latest`、バージョンを固定する名前の無い OpenAI の Decisions API。1.11) |
| W101 | 処理されないエラーで、案件を終わりでない状態に残して失敗することがある(`on failure` や `on cancel` の片付けの最中も) |
| W102 | 動くことのない `on <拒否>` |
| W103 | 案件を始めるタスクに `key` が無い |
| W104 | 範囲のあるところに、範囲の分からない値を渡した |
| W901 | 鍵の形の値が `.flow` に書いてある(1.18) |
| W902 | このマシンの外へ、暗号化しない HTTP で送る(1.18) |
| W904 | 秘密の値が、プラットフォームの履歴に残る(1.18) |

診断には、そうなる例(その点までの短い流れ)が付く。例は、状態・変数・`match` で絞った値ごとに持っているので、例の中で値が途中で変わることはない。

実行履歴の件数は、一回の呼び出し、再試行、Choice、Wait、並列のイテレーションなどごとに多めに見積もった件数を足して出す(`check.rs` の `ASL_COST`・`TEMPORAL_COST`・`DURABLE_COST`・`ARGO_COST`)。多めに見積もるので、上限の近くでは言い過ぎる側に倒れる。`for` は上限の数だけ回るとして数える。サービスを実装するフロー(1.14)は、Step Functions では入力を埋める Pass が一つ増えるので、その 2 件を足す。決められなかった規則の前提を確かめる文(1.17)は、Step Functions で 4 件(Choice と Fail)、Argo で 4 ノードを足す。`timeout until` のタスクの呼び出しは、durable functions で時計を読む step の 1 操作、Argo で秒数を計算するステップグループとテンプレートの 4 ノードを、試しごとに足す(16 章)。

E040 は `check` ではなく、プラットフォームごとのビルドで出す。上限はプラットフォームごとに違い、Temporal だけに出すワークフローが Step Functions の上限で拒否されることのないようにするためである。Temporal では、フローの一番外のループが履歴の長くなったところで新しい実行で続ける(4.2)ので、そのループは、続ける目安の 10,000 件にループの一回分を足した件数と、回り切ったときの件数の小さいほうで数える。それでも上限を超えるのは、ループの外の部分か、ループの一回が大きすぎるときで、診断はそう言う(`tests/fixtures/history_parallel.flow` と `history_round.flow`)。

pydantic-graph には実行の大きさの上限が無いので、見積もらない。

Argo には履歴の件数の上限が無い。限りは Workflow の大きさで、ノードの状態を含めて 1 MiB まで(`MAX_WORKFLOW_SIZE`。超えるとノードの状態を圧縮し、それでも超えれば、状態をデータベースに置く設定が無いかぎり保存できない)。`tests/flows/edges.flow` の実行では、ノード一つが JSON で 510〜612 バイト、圧縮して 60 バイトほどだった。60 バイトなら 15,000 個ほど入るが、テストより大きな値を運ぶワークフローでは減るので、目安を 10,000 個にした。WorkflowTemplate も、Workflow の中に二度(`storedTemplates` と `storedWorkflowTemplateSpec`)圧縮されずに入る。例でいちばん大きいホテルの予約では、それぞれ 122 KB と 119 KB で、ノードが 56 個の実行の Workflow は全体で 270 KB だった。テンプレートを小さくするため、値を計算する式は、同じ値を二度読むところを式の頭の `let` 一つにまとめ、呼び出しの終わり方(エラーの種類、結果の検査、受ける `on`)も出力ごとに `let` で一度だけ求める。

## 4. プラットフォーム

### 4.1 Step Functions

- `QueryLanguage` は JSONata。JSONPath は出さない。
- ワークフローの変数は全部、ステートマシンの変数にし、始まりの Pass で null にする。入力はそこで `$states.input` から入れ、次の Choice で型を確かめる。外れたら `Dandori.BadInput` で失敗する。実行の入力に無いフィールドは、null として入れる(`$exists` で確かめる)。何も返さない式は、そのステートを `States.QueryEvaluationError` で失敗させるからで、`T?` でも `json` でもない入力なら、次の Choice が拒否する。並列のイテレーションが持つ変数は、イテレーションの始まりで null にする(外の変数と同じ名前をイテレーションの中で入れることを Step Functions は許さない)。
- 出力は、終わりの Succeed の `Output` に書く。出力を持たないワークフローの終わりも、`Output` を JSONata の `null` にして、実行の出力を null にする(ほかのプラットフォームと同じ)。`Output` の無い Succeed は入力をそのまま出すので、最後の呼び出しの結果が実行の出力になってしまう(LocalStack の突き合わせで見つけた。5.1)。asl-validator は `null` をそのまま書くと拒否するので、式にする。
- 呼び出しは Task と、その結果を確かめる Choice の組にする。型が合わなければ `Dandori.BadResponse`、案件の状態が検査の言った集合の外なら `Dandori.UnexpectedState`。
- オプショナルなフィールドと `json` のフィールドを読む式は `($exists(x) ? x : null)` にして、式が undefined にならないようにする。入力とレコードの検査は、`json` のフィールドが無いことを拒否しない。
- `match` は Choice にし、どの分岐の条件も明示する。Default は `Dandori.UnexpectedValue` の Fail で、黙って最後の分岐へ流れることはない。`some y` の分岐は、Pass で `y` に値を入れてから本体へ進む。
- `retry` は Retry にする。`on` が無いときは、宣言したエラーを `MaxAttempts: 0` で止める retrier を先に置き、そのあとに `States.ALL` を置く。
- `on failure` は、各 Task の最後の Catch(`States.ALL`)から入る。エラーは変数 `dd_error` に入り、ブロックの終わりの Fail がそれで失敗し直す。
- 冪等キーは `実行名/呼び出しの場所/各ループの回数`。HTTP では `Idempotency-Key` ヘッダ、Lambda では `idempotency_key`、AWS の API では `key` に書いた引数で渡す。
- `callback` は、Lambda なら `lambda:invoke.waitForTaskToken` でトークンを `task_token` で渡し、SQS なら `sqs:sendMessage.waitForTaskToken` でメッセージの本文に入れる。`timeout until` のコールバックは、Task の `TimeoutSeconds` を JSONata の式 `$max([1, $ceil(($toMillis(<時刻>) - $toMillis(<now>)) / 1000)])` にする。`now` はステートに入った時刻なので、トークンを渡すあいだも待ちに入る(16 章)。
- `aws` は `arn:aws:states:::aws-sdk:<サービス>:<操作>`。引数とレスポンスは API のまま(PascalCase)で、型は利用者がタスクに書く(HTTP のタスクと同じ)。
- `agent` は HTTP Task で `https://api.openai.com/v1/responses` に送る。本文は `model`、`instructions`、`input`(引数を `$string` で JSON の文字列にしたもの)、`text.format`(`json_schema`、名前は `answer`、`strict`)。応答は、Responses API の出力のうち `message` の `output_text` を `$parse` で読み、`answer` を取り出す。モデルが拒否すると `output_text` が無いので、`$error` で Task を `States.QueryEvaluationError` で失敗させる。JSONata の式の失敗はその Task の Retry と Catch で捕捉できる(Step Functions の文書で確かめた)ので、拒否は `failure` としてリトライでき、`on failure` で処理できる。結果を `let` で受けない呼び出しでも、同じ式を Task の `Output` に置いて結果を読み、拒否を失敗にする。
- Claude の `agent` は HTTP Task で `https://api.anthropic.com/v1/messages` に送る。ヘッダは `anthropic-version`、本文は `model`、`max_tokens`、`system`(指示)、`messages`(user のメッセージ一つに、引数を `$string` で JSON の文字列にしたもの)、`output_config.format`(`json_schema`)。応答は、`content` のうち `text` のブロックをつないで `$parse` で読み、`answer` を取り出し、その中の列挙の値を大文字と小文字を区別せずに宣言した値に直す(直す式は応答の型から作る)。`stop_reason` が `end_turn` でなければ、`$error` で Task を失敗させる。あとは OpenAI のときと同じ。
- HTTP Task のリクエストは 60 秒で打ち切られるので、`http` と `agent` のタスクの `timeout` が 60 秒を超えれば E050 にする。
- `state machine` は `startExecution.sync:2` で、結果は子の出力(`Output`)。子の失敗は親には `States.TaskFailed` として届き、子の `fail` の名前は Cause の中にしかない。Cause の形は文書で確かめられなかったので、ネストした実行に宣言したエラーは E050 とし、`failure` で処理してもらう。
- `for` は、Pass と Choice で数えるループにする。要素の数は最初の Choice で確かめる。`yield` は `$append` で集める。
- `for … in parallel` は INLINE の Map にする。イテレーションの始まりの Pass が、ItemSelector で渡した要素と番号(`$states.context.Map.Item.Index` は ItemSelector でしか読めない)をイテレーションの変数に入れる。イテレーションの中の Task には、処理していないエラーを `{"fail": {Error, Cause, task: true}}` に変えてイテレーションを終える Catch を付け、`fail` や検査の失敗も同じ形でイテレーションを終える。Map のあと、結果の並びに失敗があれば最初のものを取り出し、タスクのエラーなら `on failure` へ、そうでなければ Fail へ進む。Fail の Cause は文字列でなければならないので、理由の無い `fail` は Cause を書かない Fail に分ける。
- 規則を呼ぶ Lambda 関数のコード(Python)も出す。rulec が生成した Python を import し、JSON を列挙と整数に直して呼び、結果を JSON に戻す。
- `connect` で呼ぶ規則(1.13)は HTTP Task にする。`ApiEndpoint` は `connect` の URL と `rulec api` のパス、ヘッダは `Connect-Protocol-Version: 1` で、本文は JSONata で組み立てる(フィールドの JSON の名前、数は `$string`、列挙は規則の値から `.proto` の名前への表)。結果は Task の出力の式で、ゼロ値を埋め、規則の出力の名前と値に戻してから、ほかの規則と同じ Choice で確かめる。Retry は規則のもの。`connection` が無ければ E050、HTTPS でなければ E050。その規則の Lambda 関数のコードは出さない。
- 規則と日付の Task(Lambda も `connect` の HTTP Task も)には `TimeoutSeconds` 10 を付ける。Retry は `States.ALL` を 1 秒の間で 2 回、倍率 2.0 で、時間切れ(`States.Timeout`)もそこに入る。値は `check.rs` の `RULE_SECONDS`・`RULE_RETRIES`・`RULE_RETRY_INTERVAL`・`RULE_RETRY_BACKOFF` から読む(16 章)。
- サービスを実装するフロー(1.14)では、最初のステートを、入力のゼロ値を埋める Pass(`Output` が埋めた入力)にし、始まりの Pass と入力を確かめる Choice はその出力を読む。サービスに書かれた `callback` のタスクは、Task の結果の式で応答を埋める。実行は、出力が無いときも `{}` で終える。`status` のメソッドを持つサービスは E050。
- **日付**(1.16)は、規則と同じく Lambda の Task にする。呼ばれる日付ごとに、koyomi が生成した Python を呼ぶ Lambda 関数のコード(`lambda/<ファイルの別名>_<日付の別名>_handler.py`)を出す。時刻の入力は、カレンダーのオフセットでその日に読み、日付と `at`(時刻を言う日付なら)を返す。
- **帳簿の操作**(1.16)は、`use book` の `lambda` の Lambda 関数を呼ぶ Task にする。payload は `{transfer, op, args, amounts?}`。帳簿ごとに、chobo の Python のクライアントで操作する Lambda 関数のコード(`lambda/book_<帳簿>_handler.py`)を出す。利用者は `handler = make_handler(lambda: tigerbeetle(ClientSync(…)))` のように、帳簿の値を作る関数を渡す。拒否されたら、理由を名前にした例外を投げる。Lambda はその名前を `errorType` に入れ、Step Functions はそれをエラーの名前として Catch に渡す(宣言したエラーの名前は、帳簿の理由そのもの)。通ったら、仮押さえのレコードを Task の Assign の JSONata で、渡した引数から作る。Retry は書いたとおりで、キーで冪等なので、二度目は `done_before` で通る。
- **`now`**(1.16)は、`($substring($states.context.State.EnteredTime, 0, 19) & "Z")`。そのステートに入った時刻を秒までにしたもので、Retry のあいだも変わらない。期間を書いたワークフローの本体では、確かめる Pass が入れた変数 `dd_now_<場所>` を読む(下)。
- **ワークフローが動く期間の確かめ**(20 章)は、時計を読む Pass(走り始めは `period at the start`、`now` を読む文の前は `<行> now`。ステートに入った時刻を変数 `dd_now_<場所>` に入れる)と、それを先頭の 19 文字で期間の端と比べる Choice(`period at the start check`、`<行> period`)。走り始めの外は `Dandori.OutOfPeriod` の Fail、読む文の前の外は、`dd_error` にエラーを入れる Pass から `on failure` の入口へ進む(`on failure` が無ければ Fail。並列のイテレーションの中では、タスクのエラーと同じく失敗をイテレーションの結果にして運ぶ)。
- **`history encrypted`**(1.18)は、生成物を変えない。実行の履歴を暗号化するのは、ステートマシンの `EncryptionConfiguration`(`Type: CUSTOMER_MANAGED_KMS_KEY` と `KmsKeyId`)に設定するカスタマー管理の KMS キーで、dandori が書く ASL の外にある。鍵を設定すると、`GetExecutionHistory` と `DescribeExecution` を呼ぶ人に `kms:Decrypt` が要る(2026-10-06 に AWS の文書の本文で確かめた)。
- **規則の前提の確かめ**(1.17)は、条件を JSONata で書いた Choice(日は `$v in ["2026-02-10", …]`)で、成り立たなければ `Dandori.BrokenPrecondition` の Fail へ進む。並列のイテレーションの中では、`fail` と同じく失敗をイテレーションの結果にして運ぶ。

### 4.2 Temporal

- `types.ts`(型と検査)、`activities.ts`(タスクの型、dandori が書くタスク、利用者が書く `OwnTasks`、それらを合わせる `makeActivities(own, transport)`)、`io.ts`(`Transport`)、`rules.ts`(規則を包むアクティビティ)、`runtime.ts`(共通の部品)、`workflow.ts`、`worker.ts`(ワーカー)、`client.ts`(始め方、コールバックへの応答の仕方、状態の聞き方)を出す。
- **ワークフローの型にバージョンを入れる。** 型は `<名前>_v<版>`(`hotel_stay_v1`)で、ワーカーのタスクキューも同じ名前にする。`.flow` のバージョンを上げると別のワークフローになり、古いバージョンの実行は、古いコードのワーカーが持つ古いキューで最後まで走る。同じバージョンのまま dandori を上げてコードが変わったときのために、`worker.ts` は Worker Deployment Versioning にも乗れる。`deployment` を渡すと、ワーカーはそのデプロイの一つのバージョンになり、ビルド ID は dandori が書いたコードのハッシュ(`BUILD_ID`)になる。ワークフローは始まったバージョンに留まる(PINNED)ので、書き直されたコードが古い実行を再生することはない。Worker Deployment Versioning を使わずに同じバージョンのコードを差し替えるなら、`client.ts` の `histories`(いま走っている実行の履歴)を `worker.ts` の `replay` にかけ、新しいコードが古い実行を再生できるかを先に確かめる。
- **`client.ts` の `start` は、ワークフロー ID を二度使わせない**(`REJECT_DUPLICATE`)。冪等キーはワークフロー ID から作るので、ID を使い回すと、新しい実行の呼び出しが前の実行の呼び出しと同じキーを持ち、外部のサービスに同じリクエストとして扱われる。前の実行が失敗していても同じである。
- **ワークフローがいまどこにいるかを、クエリ `dandori.status` で返す。** 返すのは、いま待っている呼び出しか待ちの行(`at`)と、案件ごとの状態(`cases`、始まっていなければ null)と、いま待っているイベント(`events`)。`start` に `searchAttributes: true` を渡すと、案件の状態を search attribute `DandoriCases`(キーワードのリスト、`"pi=requires_capture"` の形)にも出し、案件が動くたびに書き直す。これで「確定待ちの与信を持つ予約」を Temporal の一覧から探せる。`DandoriCases` は名前空間に前もって登録しておく必要がある(登録していない search attribute を書くと、ワークフロータスクが失敗し続けて止まる)ので、既定では書かない。オンにしたかどうかは memo(`dandori.cases`)で伝える(始めるときに空のリストを渡すと、属性が無いのと区別できない)。クエリには、実行の始めから答える。入力の確かめで失敗した実行にも、案件をすべて null として答える(TypeScript では、ワークフローの関数の最初で、案件をすべて null とするクエリを登録し、入力を確かめたあとで読み方を替える)。
- **再試行は Temporal にさせない。** アクティビティは `maximumAttempts: 1` で呼び、ワークフローのコードが ASL と同じ retrier の並びでリトライする。待ちは `sleep` で、間隔も Step Functions と同じ計算。どのプラットフォームでも、同じエラーを同じ回数だけリトライするためである。
- 宣言したエラーは、アクティビティが投げる `ApplicationFailure` の `type` で見分ける。`TimeoutFailure` は `timeout`、ほかは `failure`。dandori が書くアクティビティは、Lambda の `errorType`、HTTP のステータス、AWS の API の例外の名前を、宣言したエラーの名前に直して投げる。
- `queue` は、アクティビティと子ワークフローの `taskQueue` になる。別のワーカー(ほかの言語の SDK で書いたものでも)が持つアクティビティを、名前と引数の形が合えばそのまま呼べる。
- `workflow` は `executeChild` で呼ぶ。ワークフロー ID は `親の ID/呼び出しの場所/各ループの回数/何度目か`。子の失敗は `ChildWorkflowFailure` の `cause` に `ApplicationFailure` として届くので、宣言したエラーをその `type` で見分けられる。
- **`timeout` を書かないタスクには、プラットフォームが許す以上のタイムアウトを付けない。** Step Functions の Task に `TimeoutSeconds` を書かないのと同じにする。Temporal のアクティビティはタイムアウトを持たなければならないので、`http` と `agent` には HTTP Task と同じ 60 秒、`lambda` には Lambda が関数を動かせる 900 秒、ほかのタスクには一年を付ける。代わりに、ワークフローのワーカーが持つタスク(`queue` の無いもの)は、動いているあいだ 10 秒ごとにハートビートを送り、Temporal は 30 秒来なければそのアクティビティをタイムアウト(`timeout`)にする。ワーカーが落ちても 30 秒で分かり、`retry` があればリトライできる。`queue` のタスクは別のワーカーが持ち、ハートビートを送るとは限らないので、`timeout` が無ければ 60 秒にする。コールバックの ID を渡すアクティビティは、`timeout`(答えの待ちの長さ)にかかわらず 60 秒で、ほかの出力先の渡す部分と同じ上限である(`check.rs` の `CALLBACK_HAND_ON_SECONDS`。17 章)。
- **規則と日付のアクティビティは 10 秒で、タイムアウトもリトライする。** 規則は純関数で、すぐに終わる。タイムアウトは、動かしていたワーカーが落ちたことを意味するので、失敗と同じくリトライする。10 秒の上限とリトライの並びは、どのプラットフォームでも同じである(16 章)。上限は `startToCloseTimeout` で、ワーカーが受け取ってから数え、タスクキューで待つ時間は入らない。
- **`timeout until` の待ちは、ワークフローのタイマーにする。** `event` と `callback` の待ちを始めるときに、ワークフローの時計(`now` と同じ読み方。再生でも同じ値)から期限までの秒数を出し(切り上げ、1 秒より短くしない。`runtime` の `secondsUntil`、Python の `seconds_until`、Go の `ddSecondsUntil`)、待ちのタイムアウトにする。コールバックでは、ID を渡すアクティビティが返ってから数え始める(16 章)。
- **`callback` は Update で応答を受け取る。** 送る側のアクティビティに `callback_id`(ワークフロー ID と呼び出しの場所を JSON にしたもの)を渡し、ワークフローはその ID の応答が来るのを `condition` で待つ。結果は Update `dandori.answer` で受ける(`client.ts` の `answer`)。Update は、応答を受け取ったかどうかを、応答した側に返す。ワークフローが待っていない ID(渡していない ID や、タイムアウトで待つのをやめた ID)への応答と、二度目の応答は、validator が拒否する。シグナル `dandori.callback` でも受け取るが、待っていない ID への応答は捨てる。`timeout` を過ぎれば `timeout` になる。
- `for … in parallel` は、`dd.rounds` が `k` 個ずつイテレーションを走らせる。ワークフローのコードは決定的に動くので、Promise で並べても再生で食い違わない。イテレーションの変数はイテレーションの関数の中で宣言する。
- `on failure` は、処理されなかったタスクのエラー(`TaskError`)のときだけ走り、`fail` や結果の検査の失敗では走らない。Step Functions で Fail の状態に Catch が効かないのと同じにした。
- **`on cancel` は、キャンセルの届かない範囲で走らせる**(2.6)。TypeScript では、流れと `on failure` を `try` で包み、`isCancellation` で届いたキャンセルを見分けて、`CancellationScope.nonCancellable` の中で `on cancel` を走らせ、同じキャンセルを投げ直す。投げ直すと、ワークフローは Canceled で終わる。Python では `except BaseException` で受け、`temporalio.exceptions.is_cancelled_exception` で見分け、`on cancel` をそのまま走らせて `raise` する(Python の SDK はキャンセルを一度しか届けないので、受け取ったあとのアクティビティは止められない)。`dd.attempt` はキャンセルをタスクのエラーに直さずにそのまま通し、`dd.rounds` はキャンセルで止まったイテレーションがあれば、ほかのイテレーションの失敗よりそれを先に投げる。
- **フローの一番外のループは、履歴が長くなると新しい実行で続ける(Continue-As-New)。** 一番外の `repeat` と並列でない `for` は、イテレーションの始まりに履歴が 10,000 件に達していれば(`dd.CONTINUE_AT`)、またはサーバーが勧めれば、そのイテレーションを新しい実行で始める。新しい実行には、入力と一緒に、一番外の何番目のループの何回目からか、変数、`for` のリストとそれまでに `yield` した値を渡す。続きの実行は、そのループより前の文を飛ばし、変数を受け取ってそのイテレーションから回る。ワークフロー ID は変わらないので、冪等キー、子ワークフローの ID、コールバックの ID は、続けなかったときと同じになる。一つの実行の中で最初に回すイテレーションでは続けないので、どの実行も少なくとも一回は進む(入力が大きくてサーバーが初めから勧め続けても、止まらずに回る)。
  - サーバーの勧めは、dev server(Server 1.32.0)では、履歴が 4,096 件を超えたところで来た(4,020 件の時点ではまだで、4,123 件で来た)。10,000 件は、名前空間の設定で勧めが遅いか、来ないときの歯止めである。
  - 続きの実行は、Worker Deployment Versioning でも、始まったバージョンに留まる(サーバーの既定)。渡す値の形(何番目のループか、どの変数か)はビルドごとのものなので、`initialVersioningBehavior` を `AUTO_UPGRADE` にして新しいバージョンに移すことはしない。memo(`dandori.cases`)と search attribute は、サーバーが続きの実行に引き継ぐ。
  - 並列のイテレーションのループと、ループの中のループでは続けない。並列のイテレーションを途中で切ると、走っているイテレーションを新しい実行に渡せないからである。それらの大きさは E040 で見る。
- **`event` のタスクは、Update `dandori.event` を待つ。** アクティビティは呼ばない。`dd.awaitEvent` が、待っているイベントの名前を覚えたうえで、その名前の値が来るのを `condition` で待ち(タイムアウトはコールバックと同じ)、待ち終わったら名前を消す。Update の validator は、待っていない名前と、もう来た名前を拒否する。値は `{ event, ok }` か `{ event, error, message }` で、エラーの名前は宣言したエラーとして読む(コールバックと同じ)。`client.ts` は、イベントの名前の一覧(`EVENTS`)と、それを送る `send` を持つ。一番外のループのイテレーションで待つイベントは、新しい実行で続けた先でも同じ名前で待つので、送る側はワークフロー ID だけを知っていればよい。
- **エージェントは一つのアクティビティにする。** Temporal には OpenAI の Agents SDK との公式の統合があり、モデルやツールの呼び出しをそれぞれアクティビティにして、エージェントの実行そのものを durable にできる。何往復もしてツールを呼ぶエージェントのための形である。dandori のエージェントはツールを持たずに一往復で応答するので、タスクの `timeout` と `retry` がそのまま効く一つのアクティビティにした。
- 規則は普通のアクティビティとして呼ぶ。rulec の TypeScript を import し、`parse<列挙>` と `BigInt` で直して呼ぶ。規則は純関数なので、決定性のためだけならワークフローのコードで直接呼べるが、そうしない。理由は三つ。
  - 判定の入力と結果が、実行履歴に残る(ActivityTaskScheduled と ActivityTaskCompleted)。どの入力でどう判定したかを後から読め、rulec の `replay` と `diff` に渡す記録にもなる。
  - 規則を直してワーカーを出し直しても、実行中のワークフローの再生が履歴と食い違わない。再生では結果を履歴から読む。
  - 規則のアクティビティを別のワーカーに置けば、ワークフローに触れずに規則だけを出し直せる。Step Functions で Lambda を呼ぶのと同じ形になる。

  費用は、一回につきタスクキューの往復と履歴 6 件。業務のワークフローで判定が数回なら問題にならない。

  **`use rule` の下に `local` を書いた規則は、ローカルアクティビティで呼ぶ**(`proxyLocalActivities`、Python では `workflow.execute_local_activity`)。ワークフローを走らせているワーカーの中で走り、結果は履歴にマーカー一件で残る(ワークフロータスクの中で終われば、それだけ)。一回の判定が履歴 6 件から 1 件になり、タスクキューの往復も無い。判定の入力と結果は、マーカーとして履歴に残る。代わりに、規則はワークフローと同じワーカーに置くことになり、規則だけを出し直すことはできない。ループの中で何度も判定するワークフローのための形で、E040 の見積もりも、一回を 4 件(マーカーと、続くかもしれないワークフロータスク)、リトライを 9 件で数える(`tests/fixtures/history_local.flow` は、`local` なら通り、無ければ Temporal でも E040 になる)。タイムアウト(10 秒)とリトライは、ほかの規則と同じ。
  - ローカルアクティビティのエラーは、アクティビティのエラーに包まれずに届く。失敗は `ApplicationFailure`(Python では `ApplicationError`)のまま届き、タイムアウトは、初めて走るときは `ActivityFailure` に包まれた `TimeoutFailure` で、再生では包まれない `TimeoutFailure` で届く(TypeScript の SDK 1.24.0 と Python の SDK 1.33.0 の両方で、dev server の上で確かめた)。生成するコードは、包まれていてもいなくても同じ種類に読む。そうしないと、タイムアウトを処理する `on timeout` が、再生では失敗と読まれて別の道に進み、非決定的と言われる(`tests/flows/local_rules.flow` は、タイムアウトを処理してタスクを呼ぶので、これが起きれば再生で分かる。直す前は、実行が終わったあとのクエリの再生で `Nondeterminism error` になった)。
- **`connect` で呼ぶ規則は、dandori が書くアクティビティにする**(1.13)。名前は同梱の規則と同じ `rule_<名前>` で、`makeActivities(own, transport)` の中に書き、`Transport` の `http` で送る。ワークフローは同梱の規則と同じ proxy(10 秒、`local` ならローカルアクティビティ)で呼ぶので、ワークフローのコードも、履歴に残る引数と結果も、同梱のときと変わらない。`rules.ts` には同梱の規則だけを書く。
- **サービスを実装するフロー**(1.14)では、ワークフローのコードが、入力の型を確かめる前にゼロ値を埋め(`T.fill`)、サービスに書かれた `event` の値と `callback` の応答も、受け取ったあと型を確かめる前に埋める。表は記述のメッセージから作り、コードに書き込む。実行は、出力が無いときも `{}` で終える。`client.ts` には、サービスの名前(`SERVICE`)、メソッドの名前の関数(`fulfill`、`answerPacking` など。中身は `start`・`send`・`answer`・`status` を呼ぶだけ)、メッセージの名前の型(`FulfillRequest` は `T.WorkflowInput`)を足す。
- **日付**(1.16)は、規則と同じ proxy で呼ぶアクティビティ `dates_<ファイル>_<日付>` にする(`local` ならローカルアクティビティ)。`rules.ts` が、koyomi が生成した TypeScript(`koyomi/typescript/<別名>.ts`)の関数を、入力を位置の順に並べて呼ぶ。時刻の入力は、カレンダーのオフセットでその日に読む(`day`)。`at` は koyomi の `<日付>_at` が返す。
- **帳簿の操作**(1.16)は、dandori が書くアクティビティ(名前はタスクの名前)にし、`Transport` の `book` で chobo のクライアントを呼ぶ。`io.ts` は、フローが帳簿を使うときだけ、`BookCall`・`BookResult`・`book`、帳簿ごとの名前の表(振替のメンバーと数の引数)と、既定の `Transport` の実装を持つ。既定の実装は、利用者が渡す帳簿の値(`transport({ books: { stock: postgres(pool, { tenant }) } })`。chobo の TypeScript のクライアントが作るもの)のメンバーを呼び、数の引数を bigint にする。答えは `io.booked` が読み、拒否されたら理由の名前の宣言したエラー(無ければ `Dandori.Failure.<理由>`)として失敗させ、通れば仮押さえのレコードを引数から作る。
- **`now`**(1.16)は、ワークフローの `Date.now()`(再生でも同じ時刻を返す)を秒までにした `dd.now()`。`now` を読む呼び出しは、その前で一度読んで定数 `dd_now_<場所>` にし、引数はそれを読む。引数は、`dd.attempt` が試しのたびに走らせるクロージャの中で作り、リトライのあいだのタイマーで時計は進むので、そのままでは試しごとに違う時刻を渡す(20 章。Python 版と Go 版も同じ)。
- **ワークフローが動く期間の確かめ**(20 章)は、時計を一度読んで定数に入れ、先頭の 19 文字を期間の端と比べるワークフローのコードの `if`。走り始めの外は `throw dd.fail("Dandori.OutOfPeriod", …)`、`now` を読む文の前の外は `throw new dd.TaskError("Dandori.OutOfPeriod", …)` で、`on failure` を走らせる catch が捕まえる。アクティビティもタイマーも使わないので、履歴は増えない。
- **`history encrypted`**(1.18)のフローでは、`worker.ts` の `WorkerConfig.codec`(ペイロードのコーデック)を要るフィールドにし、ワーカーの `dataConverter` にそれと `failure.ts` の失敗のコンバーター(`encodeCommonAttributes`)を入れる。`client.ts` の関数は、`encryptedClient(connection, codec)` だけが返す `EncryptedClient` を受け取る。`replay` もコーデックを受け取る。Python では `client.py` の `connect(target, codec)` が返す `EncryptedClient`(`NewType`)を、関数と `make_worker` が受け取り、データコンバーターは `DefaultFailureConverterWithEncodedAttributes` を使う。Go では `client.go` の `Dial(options, codec)` だけが `EncryptedClient` を返し、関数と `NewWorker` が受け取る。宣言の無いフローの生成物は変わらない。
- **規則の前提の確かめ**(1.17)は、ワークフローのコードの `if (!(asked! <= paid)) throw dd.fail("Dandori.BrokenPrecondition", …)`(日は `[…].includes(day!)`)。アクティビティもタイマーも使わないので、履歴は増えない。

**Python SDK 向け**(`--target temporal-python`)も出す。ワークフローの名前のパッケージに、`types.py`、`activities.py`(`make_activities(own, transport)`)、`io.py`、`rules.py`、`runtime.py`、`workflow.py`(ワークフローのクラスと、ワーカーに渡す `workflows`)を置く。`worker.py`(`worker_options` と `make_worker`)と `client.py`(`start`・`answer`・`status`)も同じ形で出す。意味は TypeScript 版と同じで、線に乗る名前もそろえた。ワークフローの型、アクティビティの名前、コールバックの Update とシグナル、クエリ、search attribute、子ワークフローとコールバックの ID が同じなので、TypeScript のワーカーが持つアクティビティを Python のワークフローから呼べ、その逆もできる(5 章で、全部のシナリオを両方向に走らせて確かめている)。書き方が違うのは次のところ。

- Python にはラベルを付けた `break` が無い。`on` のある呼び出しは `try` にし、`except` でタスクのエラーを処理して、合う `on` が無ければ投げ直す。結果の検査は `else` に置く。ループの `break` は Python の `break` のまま使える。コードの中のループは `.flow` のループだけだからである。
- 並列のイテレーションは、イテレーションごとの `async def` を `asyncio.gather` で `k` 個ずつ走らせる。Temporal の Python SDK はワークフローの中の asyncio を決定的に動かす。
- 待ちは `asyncio.sleep`(SDK が durable なタイマーにする)、今の時刻は `workflow.now()`。
- 値は JSON のままの `dict` と `list` で運び、型は `TypedDict` と `Literal` の注釈にとどめる。フィールドの名前が日本語でも、そのまま鍵になる。流れの名前が Python の予約語や組み込みと重なるときは、後ろに `_` を付ける。
- 規則は、rulec の Python をパッケージの中の `rulec/python/` から読むアクティビティにする。Step Functions の Lambda と同じく、列挙はその型で、数は `int` で渡す。
- SDK は、空のエラーメッセージを受け取ると「Application error」と読む。ワークフローの中でエラーの理由を読むときは、届いた failure の `message` をそのまま読む。
- 新しい実行で続けるのは `workflow.continue_as_new` で、履歴の長さとサーバーの勧めは `workflow.info()` の `get_current_history_length()` と `is_continue_as_new_suggested()` で読む。続きの実行に渡すものは TypeScript 版と同じ形の `dict` で、`run` の二つ目の引数で受け取る。
- `connect` で呼ぶ規則は、`activities.py` の `make_activities` の中に、名前 `rule_<名前>` のアクティビティとして書く。サービスを実装するフローの `client.py` のメソッドの名前の関数は、snake case(`answer_packing`)にする。
- 日付は `rules.py` が `.koyomi.python.<別名>` から読み、`{"day": …, "at": …}` を返す。帳簿の操作は `io.py` の `book`(帳簿の値は `transport(books=…)`。chobo の Python のクライアントは同期なので `asyncio.to_thread` で呼ぶ)。`now` は `workflow.now()` で、`now` を読む呼び出しは、その前で一度読んで変数に入れる(試しのたびに走る `lambda` と `async def dd_call_<場所>` の外で)。
- 規則の前提の確かめ(1.17)は、`if not (…): raise dd.fail("Dandori.BrokenPrecondition", …)`(日は `in` と日のタプル)。ワークフローが動く期間の確かめ(20 章)は、走り始めの外が `raise dd.fail("Dandori.OutOfPeriod", …)`、`now` を読む文の前の外が `raise dd.TaskError("Dandori.OutOfPeriod", …)`。

**Go SDK 向け**(`--target temporal-go`)も出す(2026-10-02)。ワークフローの名前の Go のパッケージ一つに、`doc.go`(パッケージの説明と、前提にするモジュールのバージョン)、`types.go`、`values.go`(JSON の値を読み書きする部品)、`activities.go`(`OwnTasks` と `Activities(own, transport)`)、`io.go`(と、フローが使うときだけ `io_aws.go`・`io_openai.go`・`io_claude.go`)、`rules.go`、`runtime.go`、`workflow.go`(`Workflow`)、`client.go`(`Start`・`Answer`・`Send`・`Status`・`Histories`)、`worker.go`(`NewWorker`・`Replay`)を置く。線に乗る名前は TypeScript 版と同じなので、ワークフローとアクティビティを TypeScript や Python のワーカーと分け合える(5 章で、Go と TypeScript の組み合わせを両方向に走らせて確かめている)。書き方が違うのは次のところ。

- **値は JSON のまま運ぶ。** ワークフローの変数、タスクの引数と結果は、JSON の値(`map[string]any`、`[]any`、`float64`、`string`、`bool`、`nil`)のまま持ち、構造体には読まない。Python 版と同じ形である。構造体に読むと、無いフィールドと null の区別が消え、構造体が知らないフィールドが落ち、型に合わない結果は読む段階でエラーになる。シナリオは型に合わない結果(`{}` や `[]`)も返し、ワークフローがそれを `Dandori.BadResponse` で拒否すること、無いフィールドを無いまま次の呼び出しに渡すことを、参照インタプリタと比べる。型を確かめる関数(`Is<型>`)は JSON の値に対して書く。`types.go` には、レコードごとの構造体(JSON の名前をタグに持つ)と列挙ごとの文字列の型も書き、利用者は自分のタスクの中で `Decode` で読める。自分で書くタスク(`OwnTasks` のメソッド)は、引数を `map[string]any` で受け取り、SDK が JSON に書ける値を何でも返せる。
- **パッケージの名前は ASCII にする。** Go の import パスには ASCII しか使えない(golang.org/x/mod の `CheckImportPath`)ので、`宿泊` というディレクトリは import できない。ワークフローの名前が ASCII の識別子ならそれを、そうでなければ `.flow` のファイル名(`hotel.ja.flow` なら `hotel_ja`)を、それも使えなければ `workflow` を名前にする。Go の識別子は日本語を受け付けるので、変数の名前は日本語のまま書く。外から見える名前(型、構造体のフィールド、`OwnTasks` のメソッド)は先頭が大文字でなければならないので、日本語の名前には前に `X` を付ける(`X承認を求める`)。
- **`go.mod` は書かない。** TypeScript 版と Python 版と同じく、利用者のモジュールに置くファイルだけを書く。前提にするモジュールとバージョン(Temporal の Go SDK 1.49.0 など)は `doc.go` に書く。規則は、`rulec gen` が規則ごとに独立したモジュール(`rulec/go/holdamount`)として Go を書くので、利用者の `go.mod` でそれを require して replace する。rulec 自身のランナーと同じ形で、`rules.go` の先頭にその二行を書く。
- **一回の実行の状態は構造体に持つ。** TypeScript 版ではワークフローごとに分かれたモジュールの変数に、Python 版ではワークフローのクラスのインスタンスに持つもの(いま待っている行、案件の状態の読み方、待っているコールバックとイベント)を、Go 版では実行の始めに作る `ddRun` に持つ。Go のワーカーは一つのプロセスで多くの実行を動かすので、パッケージの変数には持てない。コールバックのシグナルは `workflow.Go` で動かすループが受け取り、並列のイテレーションは `workflow.Go` と `workflow.WaitGroup` で `k` 個ずつ走らせる。どちらも SDK が決まった順に動かす。map を回した順で結果が変わるところ(search attribute の案件の並び、クエリが返すイベントの並び)は、`.flow` の順か名前の順に並べ直す。
- **ラベル付きの `break` は使わない。** 呼び出しは `{ … }` のブロックにし、`on` のある呼び出しは、`ddAttempt` が返したエラーの種類で `if` を分ける。合う `on` が無ければ、エラーをそのまま返す。ループの `break` は Go の `break` のまま使える。コードの中のループは `.flow` のループだけで、`match` は `switch` でなく `if` の並びにしたからである。流れは関数 `ddFlow` にし、`succeed` と `fail` は `return` になる。`on failure` と `on cancel` は、流れを関数リテラルで包み、返ったエラーの種類で走らせる。`return` のあとに文を書かず(`go vet` が届かない文を言う)、すべての分岐が `return` で終わるところでは、関数の終わりの `return` も書かない。
- **`on cancel` は `workflow.NewDisconnectedContext` の上で走らせる。** キャンセルの届かないコンテキストに `ctx` を差し替えて `on cancel` を走らせ、最後に届いたキャンセルのエラーを返す。ワークフローは Canceled で終わる。
- **ローカルアクティビティは名前で呼ぶ。** `workflow.ExecuteLocalActivity` に名前を渡すと、ワーカーに登録したアクティビティを呼ぶ(SDK 1.49.0 のソースで確かめた)。引数はシリアライズされずにそのまま渡るので、ワークフローが書いた整数は `int` のまま届く。数を読む部品は `float64` と `int` の両方を受け取る。
- **none は JSON の null で送る。** Go の SDK は `nil` を `binary/null` のペイロードにし、TypeScript の SDK はそれを `undefined` と読む。`undefined` のフィールドは JSON に書かれないので、Go のアクティビティが返した null を TypeScript のワークフローが次の呼び出しに渡すと、そのフィールドが消える。生成したアクティビティとワークフローは、none を `json.RawMessage("null")`(`json/plain` の `null`)で返す。
- **既定の `Transport`。** HTTP は `net/http`、Lambda と AWS の API は AWS SDK for Go v2、OpenAI のエージェントは OpenAI の Go のクライアント(openai-go)、Claude は Anthropic の Go の SDK で呼ぶ。Go には import を使うときまで遅らせる仕組みが無いので、フローが使う SDK のファイル(`io_aws.go`・`io_openai.go`・`io_claude.go`)だけを書き、`init` で `io.go` に差し込む。`TransportOptions` のフィールドも、フローが使うものだけを書く。AWS の API は、Step Functions の名前(`sqs:sendMessage`)ごとに、SDK の入力の構造体を JSON から作って呼ぶ(構造体のフィールドの名前は API の名前と同じである)。結果は JSON にし、SDK のメタデータと、値の無いメンバー(構造体には全部ある)を除く。例外は、モデルの名前(`QueueDoesNotExist`)で返す。知っているサービスは TypeScript 版と同じ 13 で、ほかのサービスはエラーにする。
- **OpenAI のエージェントは Responses API で呼ぶ。** OpenAI は Go の Agents SDK を出していない(非公式のものはある)ので、Go 版は openai-go で Responses API に、Step Functions と同じリクエスト(モデル、指示、JSON の文字列にした入力、`text.format`、エフォート)を送る。ツールを持たず一往復で終わる dandori のエージェントには、これで足りる。Claude は、Go の SDK が system とメッセージをテキストのブロックのリストとして書くので、Step Functions と同じく文字列になるよう、送る前に置き換える(`option.WithJSONSet`)。どちらのクライアントもそれ自身ではリトライさせず、`TransportOptions.HTTPClient` を通す。
- **Go の map は順を持たない。** そのため Go 版は、オブジェクトの鍵も、クエリとフォームの組も、名前の順に書く。エージェントに渡す入力の JSON の文字列も同じである。JSON としても組としても同じだが、TypeScript 版と Python 版が `.flow` の引数の順や、受け取った JSON の順に書くのとは、文字列としては違う。モデルが読む文字列が鍵の順だけ違っても、答えに効くことはないと見ている。
- **gofmt のとおりに書く。** 構造体のフィールドと、一行に一つずつ書いた複合リテラルのキーと値を、gofmt の規則(キーが 40 バイトを超えるときの比の規則も)でそろえ、長くなる関数リテラルは初めから行を分ける。テストは、生成したコードが `go vet` を通り、`gofmt -l` が何も言わないことを確かめる。
- **Go のバージョン。** Temporal の Go SDK 1.49.0 は `go 1.26.0` を求めるので、生成したコードを使うには Go 1.26 が要る(手元の Go が古ければ、go コマンドが 1.26 のツールチェーンを取ってくる)。
- **日付と帳簿。** 日付は `rules.go` が koyomi の Go のパッケージ(`koyomi/go/<パッケージ>`。日付のファイルごとのモジュールで、規則と同じく利用者の `go.mod` で require して replace する)を呼ぶ。帳簿の操作は、フローが帳簿を使うときだけ書く `io_books.go` の `Book`(`BookTransport`)で、`TransportOptions.Books` の値(chobo の Go のクライアントの `*Book`)を呼ぶ。Go 版はクライアントの型を知らないまま書くので、振替のフィールド、操作のメソッド(`Do`・`Hold`・`Post`・`Void`)、引数の構造体を、chobo の口が渡す名前でリフレクションで引く。`now` は `workflow.Now(ctx)` で、`now` を読む呼び出しは、その前で一度読んで変数に入れる(`ddAttempt` に渡すクロージャの外で)。
- **規則の前提の確かめ**(1.17)は、`if !(ddNum(asked) <= ddNum(paid)) { return nil, ddFail("Dandori.BrokenPrecondition", …) }`。日は `switch ddStr(day) { case "2026-02-10", …: default: return nil, ddFail(…) }` で、gofmt のとおりに書く。ワークフローが動く期間の確かめ(20 章)は、走り始めの外が `return nil, ddFail("Dandori.OutOfPeriod", …)`、`now` を読む文の前の外が `return nil, &ddTaskError{Kind: "Dandori.OutOfPeriod", …}`。
- **ritsu の生成パッケージの中のフロー。** ritsu の `ritsu gen`(ritsu の DESIGN 9.3)は、プロジェクトの規則、日付、帳簿のクライアント、ワークフローを、言語ごとに一つのパッケージにする。フローは `flows/<名前>/` に置かれ、規則と日付と帳簿は、横にコピーしたコードではなく、同じパッケージの `rules/`・`dates/`・`books/` から読む。そのために、モデルに `package`(`InPackage`。Go のパッケージの import のパスを持つ)を置き、それがあるときだけ、三つの SDK のビルドが読み込む先を替える。TypeScript は `../../rules/<モジュール>` と `../../dates/<別名>`、Python は `...rules.<モジュール>` と `...dates.<別名>`、Go は `<import のパス>/rules/<パッケージ>` と `<import のパス>/dates/<パッケージ>` である(Go は gofmt の並びに並べる)。帳簿は、トランスポートが受け取るクライアントの型を、パッケージの `books/` のクライアントの型にした。TypeScript は `io.ts` の `Books`(帳簿の名前ごとに、`../../books/<帳簿>` の `Book`)、Python は `io.py` の `Books`(`TypedDict`)、Go は `io_books.go` の `Books` の構造体(帳簿ごとに `*<パッケージ>.Book` のフィールドと、`TransportOptions.Books` に渡す `Map()`)である。渡すクライアントが帳簿と違えば、型の検査が言う。`package` が無ければ(`dandori build`)、生成物は前と同じである。

### 4.3 Lambda durable functions

Temporal と同じく、コードを書き、チェックポイントと再生で続きから動かす形なので、Temporal のジェネレーターと骨組みを共有する(型、結果の検査、リトライの並び、エラーの種類、`on failure`、`io.ts`)。違うのは、タスク・待ち・コールバック・子の呼び出し方である。

- `types.ts`、`tasks.ts`(タスクの型、dandori が書くタスク、`OwnTasks`、`makeTasks`)、`io.ts`、`runtime.ts`、`workflow.ts` と、呼ばれる規則ごとの Lambda 関数(Step Functions 向けと同じ Python)を出す。`workflow.ts` は `makeHandler(own, transport)` を出し、利用者は `export const handler = makeHandler(自分のタスク)` と書く。
- **タスクは step の中で呼ぶ。** `lambda` のタスクも `context.invoke` にはせず、dandori が書く実装が step の中で AWS SDK から Lambda の Invoke を呼ぶ。`context.invoke` では、invoke の先で投げたエラーの名前が `InvokeError` の中で落ちる(SDK 2.4.0 のソースで、`ErrorType` を捨てて `ErrorMessage` と `ErrorData` だけを残していることを確かめた)。Invoke のレスポンスでは、関数が投げたエラーの型が `errorType` に残る。
- **`durable function` は `context.invoke` で呼ぶ。** 長く動く durable function を待つにはこれしかない。エラーの名前が落ちる問題は、`ErrorData` で渡して避ける。dandori が出す durable function は、`fail` のとき `ErrorData` に `{"error": <名前>}` を入れて失敗する(`Failure`、SDK の `DurableOperationError` を継ぐ)。呼ぶ側はそれを読んで宣言したエラーを見分ける。dandori 以外の関数も、同じ形で `ErrorData` を入れれば見分けられ、入れなければ `failure` になる。invoke にはタイムアウトの設定が無いので、`durable function` のタスクの `timeout` は E050。
- **規則と日付も step の中で呼ぶ。** `lambda` のタスクと同じく、dandori が書く実装(`tasks.ts` の `rule_<名前>` と `dates_<別名>_<日付>`)が、step の中で `Transport` の `lambda`(AWS SDK の Invoke)を呼び、step は呼び出しを `dd.within(…, 10)` で包む。SDK 2.4.0 の `context.invoke` の設定(`InvokeConfig`)にはタイムアウトが無く、一回の呼び出しに上限を付けられないからである(16 章)。規則は別の Lambda 関数のままなので、規則だけを出し直せることは変わらない。答えは step の結果としてチェックポイントに残る。invoke の操作と違って入力は履歴に残らないが、規則は副作用を持たず、入力は実行の状態から決まるので、再生には困らない。待つあいだ、durable の関数は動いたままになる(一回の呼び出しで 33 秒まで)。
- **再試行は SDK にさせない。** step には `retryStrategy: () => ({ shouldRetry: false })` を渡す(渡さないと SDK の既定のリトライが効く)。ワークフローのコードが、Step Functions と同じ retrier の並びでリトライ、あいだは `context.wait` で待つ。
- **外部のデータを変えて `key` の無いタスクは、「多くとも一回」の step にする**(`StepSemantics.AtMostOncePerRetry`)。既定の「少なくとも一回」では、結果を記録する前に落ちると同じ呼び出しがもう一度走る。「多くとも一回」の step は、中断されると結果の分からない失敗になり、検査はそれを「イベントが起きたか分からない失敗」として扱っている。
- **`callback`** は `context.createCallback` で ID を作り、別の step の中でタスクに `callback_id` を渡す。step は渡す呼び出しを `dd.within(…, 60)` で包み、60 秒を過ぎれば `Dandori.Timeout` で失敗して、タスクの時間切れ(`timeout`)になる(どの出力先でも、ID を渡すのは 60 秒まで。17 章)。呼び出し先は `SendDurableExecutionCallbackSuccess` で結果を JSON にして返す。タイムアウトは `CallbackTimeoutError` で、`timeout` になる。`timeout until` のコールバックは、`createCallback` の前に時計を読む step(`<行> <タスク> clock`)を置き、期限までの秒数(切り上げ、1 秒より短くしない。`runtime.ts` の `secondsUntil`)を `createCallback` の `timeout` に渡す。コールバックの時計は ID を作ったときから動くので、ID を渡す step の時間も待ちに入る(16 章)。
- **`for … in parallel`** は `context.map` にし、イテレーションの中のコードはイテレーションの子の context を通して呼ぶ。イテレーションは、yield した値か、どう失敗したかを JSON にして返す(チェックポイントに残すため)。`map` のあとで最初の失敗を投げ直す。
- **今の時刻は step の中で読む。** `wait until` は、step で読んだ時刻(チェックポイントに残る)から待つ秒数を出す。Temporal と違って、ワークフローの中の `Date.now()` は再生のたびに変わる。
- 冪等キーは `context.executionContext.durableExecutionArn` から作る。
- 一回の実行で使える操作は 3,000 回まで(引き上げられない)。E040 はこれも見積もる。
- `connect` で呼ぶ規則は、dandori が書く実装(`tasks.ts` の `rule_<名前>`)が、Lambda の代わりに規則のサービスに送る。step と 10 秒の上限は、Lambda で呼ぶ規則と同じである。`lambda` は要らない。サービスを実装するフローでは、ハンドラが入力の型を確かめる前にゼロ値を埋め、サービスに書かれたコールバックの応答も埋める。`status` のメソッドを持つサービスは E050。
- 日付は、規則と同じく step の中で Lambda を呼ぶ(Step Functions と同じ関数)。帳簿の操作は、dandori が書く実装(`tasks.ts`)を step の中で呼び、`Transport` の `book` で chobo のクライアントを呼ぶ(Temporal と同じ `io.ts`)。`now` は、その文の前の step(`<行> clock`)で読む。答えはチェックポイントに残るので、再生でも同じ時刻になる。step は操作を一回使うので、E040 の見積もりは `now` を読む文ごとに 1 を足す。
- 規則の前提の確かめ(1.17)は、Temporal と同じワークフローのコードの `if` で、step を使わないので操作は増えない。ワークフローが動く期間の確かめ(20 章)も Temporal と同じ `if` で、時計は確かめる文の前の step(`<行> clock`)で読み、`now` を読む文はその時刻を読む。走り始めの確かめの step の分だけ、操作が一回増える。
- `history encrypted`(1.18)は、生成物を変えない。durable の実行のデータを暗号化するのは、関数の `DurableConfig.KMSKeyArn` に設定するカスタマー管理の KMS キーで、dandori が書くコードの外にある。鍵を設定すると、実行のデータを読む `GetDurableExecution` と `GetDurableExecutionHistory`(`IncludeExecutionData=true`)を呼ぶ人に `kms:Decrypt` が要る。実行は、始まったときの鍵を最後まで使う(2026-10-06 に AWS の文書の本文で確かめた)。

### 4.4 Argo Workflows

- `<ワークフロー>.argo.yaml`(WorkflowTemplate)と、`caller/`(`lambda`・`http`・`aws`・`agent`・`jev` のタスクと規則をコンテナの中で動かすプログラム。`types.ts`・`tasks.ts`・`io.ts`・`transport.ts`・`rules.ts`・`call.ts`・`package.json`・`Dockerfile`)を出す。入力はパラメータ `input` に JSON で渡し、出力はグローバル出力パラメータ `dd_output` に残る。
- **タスクはどれもコンテナで動く。** `image` のタスクは利用者のイメージ、`lambda`・`http`・`aws`・`agent` のタスクと規則は `caller/` から作るイメージで動く(エージェントがあれば、`package.json` に `@openai/agents` か `@anthropic-ai/sdk` が入る)。中の実装は Temporal と durable functions 向けと同じもので、同じ `Transport` を通す。呼び出しは環境変数 `DANDORI_CALL` で渡る。結果は `/tmp/dandori/answer.json` に書き、宣言したエラーなら `/tmp/dandori/error.json` に `{"error", "message"}` を書いて終了コード 3 で終える。タイムアウトで止められた Pod(終了コード 143 か 137)は `timeout`、ほかの終わり方は `failure`。
- **Argo には変数が無いので、ワークフローの状態をグローバル出力パラメータに置く。** 変数ごとに `v<番号>`(値は JSON)、流れがどう進むかを表す `dd_ctl`(`next`・`break`・`succeed`・`fail`・`task`)、失敗の `dd_error`、出力の `dd_output`。並列のイテレーションが持つ変数は、イテレーションごとに接尾辞を付けた別のパラメータにする。YAML の頭のコメントに、どの変数がどのパラメータかを書く。
- **グローバル出力パラメータを読むのは、値を計算するテンプレートの出力の式だけにした。** Argo は、ステップの `when` と引数を、そのステップが動いているあいだ、ワークフローを見に来るたびに評価し直す。また、ステップを並べたテンプレートは、グローバル出力パラメータを、そのテンプレートに来たときの値で持っていて、自分のステップがそのあと入れた値は見えない。コントローラのソース(v4.1.4 の `executeSteps` と `executeStepGroup`)でこの二つを確かめ、kind の上でも起こした。そこで、ステップが一つで、それが決して走らないテンプレートを作り、値はその出力の式で計算する。Argo はこの式を、そのテンプレートに来たときに一度だけ、それまでに入った値をすべて見て評価する。ほかのステップはこの出力を読む。出力は一度入ると変わらない。
  - 呼び出しの引数、`match` の分岐、ループの回数は、その直前にこの形のテンプレートで計算する。
  - 流れを止めうる文(呼び出し、`match`、ループ、`fail`・`succeed`・`break`)は出力 `go` を持つ。ブロックの中でそのあとに続く文は、`go` が `true` のときだけ走る。走らなかった文の `go` は既定の `false` になるので、止まった流れはそのまま止まっていく。
- **エラーは Argo の失敗にしない。** 呼び出しのステップは `continueOn` で先へ進み、次のテンプレートが、終わり方(ステップの状態、終了コード、error.json)からエラーの種類を決めて、`dd_ctl` と `dd_error` に書く。受ける `on` があれば、その本体が走る。流れが失敗で終わると、最後のコンテナが `dd_error` の名前と理由を出して失敗し、ワークフローを失敗させる。
- **値を計算するテンプレートの式は、どんな値を読んでも評価が失敗しない形で書く。** 出力の式の評価が失敗すると、Argo はそのノードをエラーにし、`continueOn` があってもステップグループとワークフローを失敗させる。`dd_ctl` と `dd_error` は書かれず、最後のコンテナも走らないので、どのエラーで終わったかが残らない。だから、外から来る値(ワークフローの入力、タスク・規則・子のワークフロー・コールバックの結果、error.json、コールバックの応答)は、型を確かめてから読む。フィールドは map のときだけ読み、列挙の値とエラーの名前は、文字列であることを確かめてから値の一覧への `in` で比べる。Argo v4.1.4 の式(expr-lang 1.17.8)は、文字列の定数だけを並べた一覧への `in` を、その文字列を鍵にした map を引く形に置き換えるので、文字列でも `nil` でもない値を比べると評価が失敗する(11 章)。
- **タスクの `timeout` は Pod の `activeDeadlineSeconds` にする。** テンプレートの `timeout` は、Pod が始まる前に切れると、周りのステップグループを `continueOn` に関係なく失敗させる(取れないイメージの Pod で確かめた)。`activeDeadlineSeconds` なら、始まらなかった Pod も終了コード 137 の失敗になり、`timeout` として読める。
  - 期限は、Pod を kubelet が受け付けた時点から数えるので、コンテナが起動するまでの時間も入る。期限が過ぎるのと同時に終わった Pod は、Kubernetes が DeadlineExceeded で失敗にし、Argo はノードのメッセージを、コンテナのものではなくその文(`Pod was active on the node longer than the specified deadline`)にする。次の項の `retryStrategy` の式は、宣言したエラーの名前をメッセージから読むので、終了コードが 3 のままでも、そのエラーのリトライは起きない(呼び出しの結果の読み方は、出力の `error` を見るので変わらない)。短い `timeout` は Pod の起動の分を見込んで書く(サイトの platforms のページにも書いた)。テストでは、シナリオが `timeout` まで走らせないので、本物の Pod の回は期限に余裕を足してから走らせる(5.1)。
- **規則と日付の呼び出しの 10 秒は、Pod ではなく caller が数える。** `call.ts` は、規則と日付の呼び出しのときだけ 10 秒のタイマーを置き、過ぎれば `error.json` に `Dandori.Timeout` を書いて終了コード 143 で終える。期限で止められた Pod と同じ終わり方なので、ワークフローはそれを `timeout` として読み、規則の `retryStrategy`(`limit` 2、`backoff` 1 秒、倍率 2)に入る。Pod の `activeDeadlineSeconds` にしなかったのは、Pod をノードに置いて起動するまでの時間を入れないためで、Temporal の `startToCloseTimeout` がタスクキューで待つ時間を入れないのとそろえた(16 章)。
- **コールバックの ID を渡す 60 秒も、caller で渡すもの(`lambda` と `aws`)は caller が数える**(17 章)。`call.ts` は、コールバックのタスクのとき 60 秒のタイマーを置き、過ぎれば規則と同じく `error.json` に `Dandori.Timeout` を書いて終了コード 143 で終える。`image` のコールバックは利用者のイメージの中で渡すので、dandori はそこにタイマーを置けず、渡すテンプレートに `activeDeadlineSeconds: 60` を付ける。こちらは Pod の起動も 60 秒に入る。
- `retry` は `retryStrategy` にする。リトライするエラーは `expression` で、終了コードと、コンテナが終了メッセージに残した宣言したエラーの名前から選ぶ。回数は Step Functions と同じだが、Argo は次の回を、コントローラーがワークフローを見直したときに始める(既定では変化から 10 秒。テストのクラスタは 1 秒にしている)。間隔の `backoff` は、同じ秒数にはならない。
- `for` と `repeat` は、`withSequence` でイテレーションを並べ、`parallelism: 1` で一つずつ回す。再帰にしないのは、Argo がネストの深さを 100 までに限るからである。`break` のあとのイテレーションも並んでいるので、そのイテレーションは始まりのテンプレートで止まり、本体には入らない。`for … in parallel` は `parallelism` を `k` にする。各イテレーションは自分の `dd_ctl`・`dd_error`・変数を持ち、yield した値か、どう失敗したかを出力に残す。イテレーションがすべて終わってから、その並び(リストの順)で最初の失敗を探す。
- `callback` は、ID を渡すステップのあとの suspend のステップで待つ。タスクの `timeout` はこの待ちの長さで、渡すステップには上の項の 60 秒が付く。応答する側は `argo node set` で待ちのステップの出力 `answer` に `{"ok": 値}` か `{"error": 名前, "message": …}` を入れ、`argo resume` で進める。どちらも `inputs.parameters.callback_id.value=<ID>` でステップを選ぶ。タイムアウトすれば `answer` は既定の値になり、`timeout` として読む。Argo がリトライできるのは ID を渡すステップだけなので、`callback` のタスクの `retry` は E050。`timeout until` のコールバックは、ID を渡すステップのあとに期限までの秒数を計算するテンプレート(`s<場所>-until`)を置き、待ちのテンプレートの `suspend.duration` をその出力にする。式は `wait until` と同じものに切り上げと 1 秒の下限を足したもので(`let dd_s = …; string(int(dd_s > 1 ? dd_s : 1))`)、テンプレートに来たときに一度だけ評価される。ノードは 4 個増え、E040 の見積もりに足す(16 章)。
- `workflow template` は、そのテンプレートから Workflow を作る resource のステップにする(`action: create` で作り、成功と失敗の条件で終わりを待つ)。結果は子の `dd_output`。子の失敗は `failure` としてしか届かないので、宣言したエラーは E050 にした。子を作るには、ワークフローのサービスアカウントに Workflow を作って見る権限が要る。
- `wait until` は、待つ秒数を計算するテンプレートで一度だけ時計を読み、その秒数で suspend する。式は秒数を `let` で一度だけ計算してから下限と比べる(`let dd_s = …; string(dd_s > 0 ? dd_s : 0)`)。秒数の式を二度書くと、`now()` を二度読む。suspend の `duration` に `now()` を書くと、見に来るたびに計算し直される(`executeTemplate` は、動いているノードでもテンプレートの引数を埋め直す)。待ちのタイムアウトは、始まった時刻にその秒数を足して決まるので、タイムアウトの時刻が後ろへずれていく。
- 冪等キーは `ワークフロー名/呼び出しの場所/各ループの回数`。
- 一回の実行が作るノードの数は E040 で見積もる(3 章)。
- `connect` で呼ぶ規則も、ほかの規則と同じく caller のコンテナ(`node /app/call.ts rule_<名前>`)で動く。中身は `tasks.ts` の `makeTasks` に書く、`Transport` の `http` で送る実装で、`call.ts` は同梱の規則だけを `rules.ts` から読む。
- サービスを実装するフローでは、始まりのテンプレートの出力の式が、入力のゼロ値を `sprig.set` で埋めてから確かめる。表はビルドのときに分かるので、メッセージの入れ子とメッセージのリストの形に合わせて式を書く。`sprig.set` は map をその場で書き換えて返すので、式は「フィールドごとの `sprig.set` と、中のメッセージを埋める式と、リストの各項目を埋める `map` を並べたリストのあとに、値そのものを置き、その最後を読む」形にした(`[sprig.set(x, "k", x["k"] ?? 0), …, x][n]`)。中のメッセージは map のときだけ、リストは配列のときだけ埋め、無いものに鍵を足さない。`sprig.set` を入れ子に重ねる形では、無いメッセージにも `null` の鍵ができ、`json` のフィールドがあるかどうかの判定が参照インタプリタと変わる。リストの項目は `map` の `#` で、項目の中のリストはその内側の `map` の `#` で埋めるので、内側の `#` が外側の項目を指すことはない。この形が、null の項目を埋め、宣言していない鍵と、map でない値を残し、中のメッセージとリストの中のリストでも埋め、無いメッセージに鍵を足さないことを、kind の上の v4.1.4 で確かめた(2026-10-01)。サービスに書かれたコールバックの応答も、それを読む式で埋める。`status` のメソッドを持つサービスは E050。
- 日付は、ほかの規則と同じく caller のコンテナで、`caller/rules.ts` が koyomi の TypeScript を呼ぶ(読み込みは `.ts` を付けて書く)。帳簿の操作も caller のコンテナで、`transport.ts` で利用者が渡す帳簿の値を `Transport` の `book` が呼ぶ。`now` は、`now().UTC().Format("2006-01-02T15:04:05Z")` を出力の式にした、時計を読むテンプレート(`s<場所>-now`)で読む。`now` を読む文ごとにその前に一つ置き、出力をグローバルな出力パラメーター `dd_now_<場所>` にして、文の式はそれを読む。Argo は値を計算するテンプレートの出力の式を一つずつ評価するので、式ごとに `now()` を書くと、一つの文の引数やフィールドが別々の時刻になる(20 章)。ノードは 4 個で、E040 の見積もりに足す。
- 規則の前提の確かめ(1.17)は、値を計算するテンプレート一つ(`s<場所>-check`)で、出力の式が前提を計算し、成り立たなければ `dd_ctl` を `fail` に、`dd_error` を `Dandori.BrokenPrecondition` のエラーにし、`go` を `false` にする。成り立てば、どちらも元の値を書き戻す。ノードは 4 個で、E040 の見積もりに足す。
- ワークフローが動く期間の確かめ(20 章)は、時計を読むテンプレート(走り始めは `dd-period-start`、`now` を読む文の前は `s<場所>-now`)と、その時刻を期間と比べるテンプレート(`s<場所>-period`)の二つ。外なら、走り始めは `dd_ctl` を `fail` に、文の前は `task` にして(処理していないタスクのエラーと同じく `on failure` へ行く)、`dd_error` を `Dandori.OutOfPeriod` のエラーにする。文はその時刻(`dd_now_<場所>`)を読むので、時計を読むのは一度である。ノードは 4 個と 4 個で、E040 の見積もりに足す。
- `history encrypted`(1.18)は E050。Argo はテンプレートの入力と出力のパラメーターを Workflow のオブジェクトにそのまま残し、それを鍵で暗号化する手段が無い。

### 4.5 pydantic-graph

- ワークフローの名前のパッケージに、`graph.py`(`State`、`Deps`、節点、`graph`)、`types.py`、`tasks.py`(`make_tasks(own, transport)`)、`io.py`、`rules.py`、`runtime.py` を出す。呼び出し先は pydantic-graph 2.x(2.51.0 で確かめた)で、グラフは `GraphBuilder` で組み、節点は `BaseNode` で書く。
- **文ごとに節点を一つ置く。** 節点の `run` の返り値の型に、次に行ける節点を全部書く。pydantic-graph はこの型から辺を作るので、`graph.render()` がそのまま `.flow` の流れ図になる。`match` は分岐ごとの節点へ分かれ、`on` のある呼び出しは処理したエラーの本体の最初の節点へ、ループの本体の終わりは次のイテレーションの節点(`S<場所>_again`)へ進む。
- 変数は `State`(dataclass)のフィールドで、値は JSON のままの `dict` と `list`。ループの回数(冪等キーに入る)も `State` に置く。
- 外への呼び出しは `Deps` を通す。`tasks`(dandori が書く `lambda`・`http`・`aws` の実装と、利用者の関数)、`rules`、`clock`、`callbacks`、`run_id`(冪等キーとコールバックの ID に入る、実行の名前)。利用者の関数は、宣言したエラーを `TaskFailure(kind, message)` で投げる。
- 再試行、結果の検査、`on failure` の意味は Temporal と同じで、書き方も Python 版とそろえた(`dd.attempt`)。待ちとリトライの間隔は `Deps.clock` で眠り、タスクの `timeout` は `asyncio.wait_for` で測る。`connect` で呼ぶ規則は、サービスへの一回ごとに `dd.within(…, 10)` で包む。プロセスの中で計算する規則と日付(`Deps.rules`)には上限を付けない(16 章)。コールバックの ID を渡す呼び出しは `dd.within(…, 60)` で包む(17 章)。`timeout until` のコールバックは、ID を渡したあとで `Deps.clock` の時刻から期限までの秒数を出し(`dd.seconds_until`)、`await_callback` の待ちにする。
- **並列のイテレーションは、イテレーションの本体を別のグラフにする。** イテレーションごとに `State` のコピーを作ってそのグラフを走らせ、`k` 個ずつ並べる。イテレーションの中で処理していないエラーはイテレーションのグラフの外に出て、ループの節点が、全部のイテレーションが終わってから、リストの順で最初のものを扱う。イテレーションの本体を節点の並びのまま、イテレーションごとの変数のコピーで走らせられるので、この形にした。
- **実行はプロセスの中にしかない。** pydantic-graph 2.x は状態をどこにも残さない(1.x にあった `FileStatePersistence` などは無くなった)ので、プロセスが止まれば実行は失われる。何日もの `wait` やコールバックも書けるが、そのあいだプロセスが生きている必要がある。止まっても続く実行が要るなら、ほかのプラットフォームを使う。
- `connect` で呼ぶ規則は、`tasks.py` の `make_tasks(own, transport)` に書く関数(`rule_<名前>`)で、グラフは `Deps.tasks` から呼ぶ(`Deps.rules` は同梱の規則だけ)。サービスを実装するフローでは、グラフの最初の節点が入力のゼロ値を埋めてから確かめ、サービスに書かれたコールバックの応答も埋める。`status` のメソッドを持つサービスは E050。
- 日付は `rules.py` の関数が koyomi の Python を呼ぶ(Temporal の Python 版と同じ形)。帳簿の操作は `tasks.py` の関数が `Transport` の `book` を呼ぶ(Temporal の Python 版と同じ `io.py`)。`now` は `Deps.clock` から読む。`now` を読む節点は、`run` の初めで一度読んで `dd_now` に入れ、文の式はそれを読む。呼び出しの引数は試しのたびに作り直し、そのあいだの待ちで時計が進むので、式ごとに読むと、一つの文が別々の時刻を読む(20 章)。
- 規則の前提の確かめ(1.17)は、確かめる節点一つ(`S<場所>_check`)で、成り立たなければ `dd.Failure("Dandori.BrokenPrecondition", …)` を投げ、成り立てば次の節点へ進む。
- ワークフローが動く期間の確かめ(20 章)も節点一つ(`S<場所>_period`)で、時計を読んで `State` のフィールド `dd_now_<場所>` に入れ、期間と比べる。外なら、走り始めは `dd.Failure("Dandori.OutOfPeriod", …)` を投げ、`now` を読む文の前は `dd.TaskError` にして、`on` の無いタスクのエラーと同じく `on failure` へ進む。次の文はそのフィールドを読む。

### 4.6 五つのプラットフォームでそろえたこと

Temporal の列は、TypeScript 版・Python 版・Go 版のどれにも当てはまる(Go 版の書き方は 4.2)。

| こと | Step Functions | Temporal | Lambda durable functions | Argo Workflows | pydantic-graph |
|---|---|---|---|---|---|
| 再試行 | Retry | ワークフローのコード(同じ並び、同じ間隔) | ワークフローのコード(同じ並び、あいだは `context.wait`) | `retryStrategy`(`expression` でエラーを選ぶ) | グラフのコード(同じ並び、あいだは `Deps.clock`) |
| 結果の検査 | Choice と Fail | `is_<型>` と `dd.fail` | Temporal と同じ | 出力の式(`type()` と `matches`) | Temporal の Python 版と同じ |
| 冪等キー | `$states.context.Execution.Name` から | `workflowInfo().workflowId` から | `durableExecutionArn` から | `workflow.name` から | `Deps.run_id` から |
| `lambda`・`http`・`aws` のタスク | Task(Lambda、HTTP、AWS SDK 統合) | dandori が書くアクティビティ(`Transport` を通す) | dandori が書く実装を step の中で | dandori が書く実装をコンテナで | dandori が書く関数(`Transport` を通す) |
| `agent` のタスク | HTTP Task(Responses API か Messages API)、応答は `$parse` で読む | dandori が書くアクティビティ(`Transport` を通し、Agents SDK か Anthropic の SDK で。Go 版は openai-go の Responses API か Anthropic の Go の SDK で) | dandori が書く実装を step の中で | dandori が書く実装をコンテナで | dandori が書く関数(`Transport` を通し、Agents SDK か Anthropic の SDK で) |
| `jev` のタスク | HTTP Task(TypeSafe の API、`url` のサーバー、OpenAI の Decisions API)、答えは Task の出力の JSONata で読み、Choice で拒否と確信度を確かめてから変数に入れる | dandori が書くアクティビティ(`Transport` の `http` を通し、`io.jev` で読む) | dandori が書く実装を step の中で | dandori が書く実装をコンテナで | dandori が書く関数(`Transport` の `http` を通し、`io.jev` で読む) |
| それ以外のタスク | 作れない | 利用者のアクティビティ | 利用者の実装を step の中で | `image` のコンテナ(無ければ作れない) | 利用者の関数 |
| 子ワークフロー | ネストした実行 | `executeChild` | `context.invoke` | WorkflowTemplate から Workflow を作る | 利用者の関数 |
| コールバック | `.waitForTaskToken` | シグナル | `createCallback` | suspend と `argo node set` | `Deps.callbacks` |
| 並列のイテレーション | INLINE の Map、イテレーションの結果で失敗を運ぶ | `dd.rounds` | `context.map`、イテレーションの結果で失敗を運ぶ | `withSequence` と `parallelism`、イテレーションの出力で失敗を運ぶ | イテレーションごとのグラフを、状態のコピーで |
| 規則 | Lambda と、Python をつなぐコード | アクティビティ(`local` ならローカルアクティビティ)と、TypeScript・Python・Go のどれかをつなぐコード | step の中の Invoke(`Transport` の `lambda`)と、Step Functions と同じ Lambda | コンテナと、TypeScript をつなぐコード | 関数と、Python をつなぐコード |
| 規則と日付の一回の上限(16 章) | Task の `TimeoutSeconds` 10 | `startToCloseTimeout` 10 秒 | `dd.within(…, 10)` | caller のタイマー 10 秒(終了コード 143) | `connect` の規則は `dd.within(…, 10)`、プロセスの中の計算には無い |
| コールバックの ID を渡す一回の上限(17 章) | 別には無い(Task の `TimeoutSeconds` が、渡すことと待ちをまとめて限る) | 渡すアクティビティの `startToCloseTimeout` 60 秒 | submit の step の中の `dd.within(…, 60)` | caller のタイマー 60 秒(終了コード 143)、`image` は Pod の `activeDeadlineSeconds` 60 | `dd.within(…, 60)` |
| `timeout until` の待ち(16 章) | `.waitForTaskToken` の Task の `TimeoutSeconds` を JSONata の式で | ワークフローのタイマー(`condition`、`wait_condition`、`AwaitWithTimeout`) | 時計を読む step と `createCallback` の `timeout` | 秒数を計算するテンプレートと suspend の `duration` | `Deps.clock` から秒数を出して `await_callback` |
| on failure | Task の Catch | `TaskError` を捕捉する catch | Temporal と同じ | `dd_ctl` が `task` のときに走るステップ | `on failure` の本体の節点へ進む |
| イベント(`event`) | 作れない(E050) | Update `dandori.event` を `condition` で待つ | 作れない(E050) | 作れない(E050、まだ) | 作れない(E050、まだ) |
| `connect` で呼ぶ規則 | HTTP Task(`connection`)、結果は Task の出力の式で埋めて読む | dandori が書くアクティビティ `rule_<名前>`(`Transport` の `http`) | dandori が書く実装を step の中で | dandori が書く実装を caller のコンテナで | dandori が書く関数(`Deps.tasks`) |
| 無い `T?`・`json`(入力、レコードのフィールド)を null として読む | `($exists(x) ? x : null)` | TypeScript は `?? null`、Python は `.get`、Go は `ddGet` | Temporal と同じ | 式の `nil`(`toJson` で null) | `.get` |
| サービスの入口(ゼロ値を埋める) | 最初の Pass で入力を、Task の結果の式で応答を | ワークフローのコード(`T.fill`) | Temporal と同じ | 始まりのテンプレートの式(`sprig.set`) | グラフの最初の節点 |
| サービスの `status` | 作れない(E050) | クエリ `dandori.status` | 作れない(E050) | 作れない(E050、まだ) | 作れない(E050、まだ) |
| 日付(1.16) | Lambda と、koyomi の Python をつなぐコード | アクティビティ(`local` ならローカルアクティビティ)と、koyomi の TypeScript・Python・Go をつなぐコード | step の中の Invoke と、Step Functions と同じ Lambda | コンテナと、koyomi の TypeScript をつなぐコード | 関数と、koyomi の Python をつなぐコード |
| 帳簿の操作(1.16) | 帳簿の Lambda と、chobo の Python のクライアントで操作するコード。仮押さえは Task の Assign で引数から作る | dandori が書くアクティビティ(`Transport` の `book` で chobo のクライアントを呼ぶ。Go 版はリフレクションで) | dandori が書く実装を step の中で | dandori が書く実装を caller のコンテナで | dandori が書く関数(`Transport` の `book`) |
| `now`(1.16)。一つの文は一つの時刻を読む(20 章) | `$states.context.State.EnteredTime` を秒まで | ワークフローの時計(`Date.now()`、`workflow.now()`、`workflow.Now`)を、呼び出しの前で一度読む | 時計を読む step(チェックポイントに残る) | 文の前の、時計を読むテンプレート(`s<場所>-now`) | `Deps.clock` を、節点の初めで一度読む |
| 決められなかった規則の前提(1.17) | Choice と Fail(`Dandori.BrokenPrecondition`) | ワークフローのコードの `if` と `dd.fail` | Temporal と同じ | 値を計算するテンプレート(`dd_ctl` と `dd_error`) | 確かめる節点と `dd.Failure` |
| ワークフローが動く期間(20 章) | 時計を読む Pass と、比べる Choice。外は、走り始めなら Fail、文の前なら `dd_error` を入れる Pass から `on failure` へ | ワークフローのコードの `if`。走り始めは `dd.fail`、文の前は `TaskError`(Go 版は `ddFail` と `ddTaskError`) | 時計を読む step と、Temporal と同じ `if` | 時計を読むテンプレートと、比べるテンプレート(`dd_ctl` を `fail` か `task` に) | 確かめる節点。走り始めは `dd.Failure`、文の前は `dd.TaskError` |

実行中の案件と規則のバージョンの関係は、どのプラットフォームでも同じになる。規則は Lambda かアクティビティとして呼ぶので、規則を直すと、実行中の案件も途中から新しいバージョンで判定される。

### 4.7 説明の文を、生成するコードのコメントの外に出さない(2026-10-06)

ワークフローの `description` は、どのプラットフォームでも、生成するファイルの頭のコメントになる(TypeScript と Go は `//`、Python と Argo の YAML は `#`、Step Functions は ASL の `Comment`)。dandori の文字列は `\n` のエスケープを持てる。前は、一つのコメントの印のあとに説明をそのまま書いていたので、改行のあとが生成したコードの行になった。TypeScript では、その行はモジュールを読み込んだときに動く文になる。Argo の YAML では、`---` のあとに別の文書(たとえば Pod)を書けて、`kubectl apply -f` は WorkflowTemplate と一緒にそれも作った。Python と Go では、ファイルの先頭の `from __future__ import annotations` や `package` より前に文が来るので、構文の誤りで止まった。`dandori check` は、どれも通していた。

いまは、説明の各行を、それぞれ一行のコメントにする(ritsu-emit の `Comment::lines` と、`Model::titled`)。行は、`\n` のほかに、どこかの出力先が行の終わりと読む文字(`\r`、U+0085、U+2028、U+2029)でも切る。TypeScript は U+2028 と U+2029 で、Python は `\r` だけでも行を終えるので、エスケープを使わずにその文字を書いても、前は同じことが起きた。Go の `doc.go` の頭に書くファイルの名前は、エスケープにして一行に収める(`header::one_line`)。ASL の `Comment` と、Argo の注釈(`workflows.argoproj.io/description`)は JSON の文字列として書くので、前から外には出ない(YAML 1.1 の読み手は、引用符の中の U+0085・U+2028・U+2029 を空白に折りたたむことがあり、注釈の値が変わりうる。7 章)。

確かめ方は `tests/comments.rs`。説明に五つの改行(`\n` のエスケープ、生の `\r`、U+0085、U+2028、U+2029)と `---` を入れたフローを七つのプラットフォームでビルドし、説明を書いたどの行もコメントか JSON の文字列の中にあること、コメントの行が改行の文字を持たないことを見る。一か所でも前の書き方に戻すと落ちる。一行の説明の出力は前と同じである。例と `tests/flows` の 52 本を七つのプラットフォームでビルドした 2,149 のファイルが、0.23.0 のリリースの出力と一字も違わなかった。

## 5. 確かめ方

- **参照インタプリタ**(`dandori run`):シナリオ(入力と、呼び出しが受け取る結果の並び)に沿って型を付けた構文木を実行し、呼び出しをプラットフォームから見える形(Lambda の関数と payload、HTTP のメソッドと URL とヘッダと本文、AWS の API と引数、エージェントの名前・プロバイダー・モデル・指示・引数・Schema(Step Functions では Responses API か Messages API への HTTP のリクエスト)、ネストした実行・子ワークフロー・invoke の先と入力、Temporal のアクティビティと引数、durable functions の step と引数・コールバックの submit)で書き出す。並列のイテレーションは、リストの順に一つずつ回す。
- **規則と期日と帳簿を計算しながら流す**(ritsu の `ritsu run`。ritsu の DESIGN 7.9):参照インタプリタは、呼び出しの結果を、シナリオのほかに、走らせる側が渡すもの(`interp::Answers`)からも受け取れる。`ritsu run` が渡す `computed::Computed` は、規則の呼び出しには rulec の参照評価器の出力を、日付の呼び出しには koyomi のインタプリタが計算した日と時刻を、帳簿のタスクには chobo の参照インタプリタが操作した結果を返す。帳簿は、走らせる前にした操作(シナリオの `books`)から始まり、実行の中の操作を覚えていく。どの結果も、シナリオに書く形(`{"ok": …}`、`{"error": …, "cause": …}`)で返し、参照インタプリタはそれをシナリオの結果と同じに扱う(同じリトライ、同じエラーの名前、同じ結果の検査)。だから `ritsu run` の実行は、計算した結果を並びに入れたシナリオを `dandori run` で流したものと一字も違わない(`ritsu run --format json` の `replay` がそのシナリオで、`crates/ritsu/tests/run.rs` が確かめる)。
  - 言語に渡す値と、結果の読み方は、生成するコードと同じにした。日付の入力に渡す時刻はカレンダーのオフセットで日に読み(1.16)、`at` は koyomi の時刻の決め方(その日のその時刻をオフセットで UTC にしたもの)で書く。帳簿の操作は、振替の引数の名前で渡す(`render::book_call`)。Connect で呼ぶ規則の結果は、サービスが書く形にしてから読み直す。宣言していない理由で拒否された操作は、生成するコードと同じく `Dandori.Failure.<理由>` の失敗になる(Step Functions の見え方では、帳簿の Lambda 関数が理由の名前で失敗するので、理由そのもの)。規則が入力を受け付けなければ、生成したコードが入口で受け付けないのと同じく失敗になり、規則のほかの呼び出しと同じくリトライする。koyomi の日付の評価はファイルの入力を全部要るので、その日付が読まない入力には範囲のいちばん小さい値を渡す。koyomi の検査を通るファイルでは、範囲のどの入力でもどの日付も計算が止まらない(koyomi の E202〜E204)ので、結果は変わらない。
  - 時間:実行はシナリオの `now` に始まり、`wait`(`wait until` ならその時刻まで)、リトライの前の待ち、タイムアウトした呼び出しのタイムアウトの長さ(規則と日付は 10 秒、`timeout until` のタスクは、その時点から期限までの秒数を切り上げたもの。1 秒より短くはしない。16 章)だけ進む。結果が返った呼び出しには時間を数えない。シナリオはかかった長さを言わず、0 はありうるうちでいちばん短いからである。帳簿にも同じ時間がたち、有効期限の来た仮押さえは期限切れになる。`now` の値は、ほかの流し方と同じくシナリオの時刻のままにした(1.16)。待ちで時計が期間の終わりを過ぎても `now` は進まないので、期間の終わりを過ぎて `now` を読む実行は、シナリオの `now` を時刻の並び(走り始めと、それより後)にして書く(20 章)。並列のイテレーションは一つずつ回すので、その中の待ちは足し合わさる。
- **シナリオの自動生成**(`dandori scenarios`):選んだ結果の記録を持ってワークフローを実行し直す。選び方は、選択の場所ごとに、まだ使われていない選択肢を先に取る。何か新しい所に着いた実行を土台にして、その途中の選択を一つずつ変えた実行を試し、そこから先は同じ場所で同じ選択を繰り返す(ループが回り切る流れはこれで出る)。結果の検査に落ちる結果(型の合わない結果、ありえない状態、範囲の外の数や列挙の値を持つ結果)も入れる。範囲を書いた入力(数、順序のある列挙の値、日付、時刻の入力と、入力のレコードのフィールド)には、その一つずつに、そこだけを範囲のすぐ外の値にした入力の実行を一本ずつ入れる(9 章、20 章)。日付と時刻の入力には、範囲の端の値(最初の日と最後の日、最初の秒と、最後の秒に秒の端数を足したもの)にした実行も入れる。期間を書いたワークフローには、期間のすぐ外で走り始める実行と、期間の中で走り始めて、`now` を読む文の確かめで期間の外になる実行を入れる(20 章)。Connect のレスポンスには、既定値のフィールドを省いたレスポンス(レスポンスの中、中のメッセージ、メッセージのリストのどこでも)も入れ、どのプラットフォームも既定値を埋めて読むことを確かめる。`json` のフィールド(設定されていない `Value`)を省いたレスポンスも入れ、どのプラットフォームも null として読んで次の呼び出しに渡すことを確かめる。Claude のエージェントの応答には、列挙の値の大文字と小文字を変えた応答(どのリストにも項目が二つあり、オプショナルな値もある形)も入れる。`on cancel` のあるワークフローでは、`on cancel` の外のどの呼び出しにも、結果の代わりにキャンセル(`{"cancel": true}`)を選べる。並列のイテレーションの一つが失敗したあとに別のイテレーションでキャンセルが届く実行も、別のところとして数える。リストの長さは `for` が読むときに選び、空・一つ・二つ・上限を一つ超える長さを試す。オプショナルな値は `match` が読むときに、無い場合とある場合を選ぶ。二回以上回ったループと、一回だけのループを別のところとして数えるので、二つのイテレーションが済んでその先の終わりまで行く実行も残る。結果と入力の文字列と数は、一つの実行の中でどれも違う値にする(`id-5`、`id-6`、`1003` など)。同じ値ばかりだと、プラットフォームが二つの結果や並列のイテレーションの結果を取り違えても、突き合わせに出ない。範囲のある数は範囲の中から選ぶ(範囲が狭ければ、同じ数になることもある)。範囲を書いた列挙の値と、日付と時刻も、範囲に入る値から選ぶ。`ritsu dandori scenarios` は、手元の規則の呼び出しを選ぶ場所にせず、rulec の口で計算する(1.19)。
- **Step Functions の突き合わせ**:生成した ASL を `tools/asl-run.mjs` が JSONata 2.0.6 で実行し、参照インタプリタと同じ形で書き出す。Map のイテレーションは一つずつ回し、イテレーションの中で外の変数に値を入れたら失敗させる(Step Functions の規則)。全シナリオで一致を見る。定義は asl-validator 4.0.0 にも通す。呼び出し方を持たないタスクのあるワークフロー(Temporal と durable functions 向けのもの)は、E050 を表示して外す。JSONata を 2.0.6 に保つのは、Step Functions がこのバージョンの仕様で評価するからである(AWS の開発者ガイド「Transforming data with JSONata in Step Functions」)。2.0.6 には既知の脆弱性が五つあり(細工した式や入力を評価したとき。直した 2.2.x は `$string` の数の書き方などが変わる)、ritsu の監査は `osv-scanner.toml` に理由と期日を書いて通している(ritsu の DESIGN 3.6)。ここで評価するのは、dandori が書いた ASL の式と、テストのシナリオの入力だけである。
- **LocalStack の Step Functions の突き合わせ**:同じ ASL を、Docker で立てた LocalStack の Step Functions で `tools/localstack/run.mjs` が実行する。バージョンは 4.14.0 で、アカウントなしで立つコミュニティ版の最後のバージョンである(これより後のバージョンは認証トークンが無いと起動しない)。JSONata は dashjoin の Java 版(0.9.7)で、`tools/asl-run.mjs` の JavaScript 版とは別の実装になる。
  - 呼び出しの結果は、LocalStack のモックの仕組み(Step Functions Local と同じ形のモックの設定ファイル、`SFN_MOCK_CONFIG`)で返す。シナリオを一つのテストケースにし、Task ステートごとに、受け取る結果を順に並べる。どのステートがどの結果を受け取るかは、同じシナリオを `tools/asl-run.mjs` で流して決める。LocalStack が別の道を通れば、結果が足りなくなるか別の結果を受け取るので、食い違いとして出る。
  - 全部のフローの全部の実行を同時に流し、呼び出し・待ち・終わり方を、実行の履歴から読む。
  - 定義は次の点だけ変える。Map のイテレーションは一つずつ回す(MaxConcurrency を 1 にする。AWS の Step Functions Local の文書もモックではそう勧める)。Wait はすぐ進め(Seconds を 0 にする)、待つはずだった長さ(Seconds か Timestamp の値)を自分だけの変数に入れて、履歴から読む。Retry の待ちは本当に待ち、履歴の時刻で測る。測った待ちは、参照より短くなく(履歴の時計の 50 ms は除く)、長すぎる分が 0.9 秒か参照の四分の一の大きいほうより小さければ、同じと見る。負荷の高いマシンでは待ちが延び、倍率が一段ずれた待ち(倍か半分)は通らない。間隔そのものは、`tools/asl-run.mjs` が定義から秒まで読んで確かめる。呼び出しと終わり方が参照と同じで、Retry の待ちだけがそれより長かった実行は、新しいコンテナでもう一度だけ流す。テスト全体の始めはマシンが混み、LocalStack のスレッドが 1 秒遅れて起きることがある(Argo のランナーが Pod を走らせ直すのと同じ考え)。
  - モックの設定ファイルからは、名前が `States.` で始まるエラーを投げられない(LocalStack はモックのエラーをタスク自身のエラーとして作り、その名前は `States.` で始められない)。タスクのタイムアウト(`States.Timeout`)、HTTP Task のエラー(`States.Http.StatusCode.<状態>`)、エージェントの失敗(下)は、名前の前に `Dandori.` を付けて投げ、Retry と Catch に書く名前もそう替え、終わり方の名前は元に戻す。定義は `States.TaskFailed`(タイムアウトとほかのエラーを分ける、ただ一つの名前)を使わないので、Retry と Catch の捕捉の仕方は変わらない。
  - 4.14.0 は HTTP Task を知らず、それを持つ定義を拒否する。そのときは HTTP Task の resource を、引数を取らず結果に HTTP Task の鍵を持たない API の AWS SDK の統合(`arn:aws:states:::aws-sdk:sts:getCallerIdentity`)に替える。LocalStack はその引数を全部評価して履歴に残し、モックの結果もエラーもそのまま渡す。HTTP Task にいちばん近い API Gateway の統合は、どちらもしない(受け取らない引数を履歴から落とし、モックのエラーをどれも `ApiGateway.FailureEventException` にする)。
  - エージェントの失敗は、モデルの拒否が Step Functions でその Task を失敗させるエラー(`States.QueryEvaluationError`)として投げる。拒否を読む式は `$error` を起こし、Step Functions は式の評価の失敗を `States.QueryEvaluationError` にして Retry と Catch に渡す(4.1)。4.14.0 は、式の評価の失敗を(型のエラーでも)`States.Runtime` にして実行ごと失敗させ、Retry も Catch も捕捉しないので、拒否そのものはここでは演じない(`tools/asl-run.mjs` が演じる)。
  - Docker か、このイメージ(`docker pull localstack/localstack:4.14.0`)が無ければ SKIP にする。
- **Temporal の突き合わせ**:生成した TypeScript を `tools/temporal/run.mjs` が SDK 1.24.0 で、本物の Temporal サーバーの上で実行する。サーバーは、SDK のテスト用のパッケージが立てる Temporal CLI の dev server(1.9.1、Server 1.32.0)。
  - dandori が書くアクティビティは本物を走らせ、`Transport` だけを、送るものを書き出してシナリオどおりに応答するスタブ(`tools/transport.mjs`)に差し替える。だから Step Functions が送るものと一字ずつ比べられる。利用者のタスク、規則、子ワークフローは、シナリオどおりに応答するスタブを置く(子ワークフローは、結果をアクティビティに聞く小さなワークフロー)。
  - ワーカーは生成した `worker.ts` の `workerOptions` で作り(規則と子ワークフローだけ代わりに差し替える)、`queue` ごとにも立てる。実行は `client.ts` の `start` で search attribute をオンにして始め、コールバックには `answer`(Update)で応答する。応答するたびに、同じ応答をもう一度送って拒否されることを、初めの一度だけは、待っていない ID への応答が拒否されることも確かめる。実行が終わると、クエリ `dandori.status` の `cases` と search attribute `DandoriCases` を読み、参照インタプリタが終わりに持つ案件の状態と比べる。search attribute は案件が動くたびにサーバーが持つもので、クエリはワークフローの変数から答えるので、終わりには同じことを言う。クエリがそれと違うことを言ったときは、クエリを三回まで尋ね直し、前に言ったことを出力する(5.1)。
  - シナリオはどれも同時に流し、それぞれ ID `run-<n>` のワークフローにする。ID は冪等キーに入るので、参照インタプリタも同じ名前で走らせる。スタブは、自分を呼んだアクティビティのワークフロー ID から、どのイテレーションの呼び出しかを知る(子ワークフローの ID は親の ID で始まる)。
  - サーバーは本当の時間で動くので、走らせるコードのコピーでは待ちを縮める。タイマーの長さ(`dd.ms`)は 10 ms まで、アクティビティと子ワークフローのタイムアウトは 20 秒にする(はじめは 2 秒、次に 5 秒だったが、テスト全体を走らせてマシンが混むと足りなかった。結果を返すだけのアクティビティが 2 秒を超え、キャンセルを届ける実行と、子ワークフローを呼ぶ実行が 5 秒を超えた。キャンセルは、アクティビティの中からサーバーに頼み、ワークフローに着く前にアクティビティのタイムアウトが来ると、実行はキャンセルではなくタイムアウトの失敗で終わる。5.1)。タイムアウトのシナリオは、サーバーがタイムアウトさせるまでアクティビティをふさぐので、その分だけ(25 秒まで)長くなる。ただし、コールバックの ID を渡すアクティビティは、生成したとおりのタイムアウトのままにする。シナリオがそれをタイムアウトさせることは無く、そのスタブは、コールバックに応答して二度目の応答が拒否されるのを確かめてから返るので、ワークフロータスクを何度か待つ。dev server は 10 ms のタイマーでも 1 秒遅れて発火させるので、`history.timerProcessorMaxTimeShift` を 10 ms にする(ホテルの予約の、29 回呼び出す一本が 26 秒から 4 秒になった)。
  - タイムアウトのシナリオでは、スタブが応答せずにアクティビティをふさぎ、サーバーにタイムアウトさせる。コールバックの応答は、ID を渡すアクティビティが返る前に送り終える(返ったあとに送ると、短くした待ちが先に切れる)。コールバックのタイムアウトでは送らない。ふさいだアクティビティは、ハートビートの応えでタイムアウトやキャンセルを知って終わる(テストのワーカーはハートビートを 100 ms ごとに送る)。
  - キャンセルのシナリオでは、スタブがサーバーにそのワークフローのキャンセルを頼み、ハートビートを送りながら、自分のアクティビティがキャンセルされるまで返さない。コールバックでは、応答の代わりにキャンセルを頼む。
  - イベントは、ランナーが 20 ms ごとにクエリ `dandori.status` の `events` を見て、ワークフローが待っていれば、生成した `client` の `send` で送る(キャンセルのシナリオでは、送る代わりにキャンセルを頼む)。送る前に、ほかのイベントを送って拒否されることを確かめる。送ったイベントの結果は、送る前に取っておく(ワークフローが受け取ると、次の呼び出しがすぐ来る)。クエリは、終わったばかりの待ちや、新しい実行に続く直前の実行のことを返すことがあるので、拒否されたり、閉じていく実行に当たったりしたイベントは、結果を戻して送り直す。コピーでは、イベントの待ちを 5 秒までにする(10 ms では、ランナーが待ちを見る前にタイムアウトする)。タイムアウトのシナリオでは送らず、次の呼び出しが来たとき(または実行が終わったとき)に、そのイベントをタイムアウトとして書き出す。イベントのあるフローは、言語をまたぐアクティビティの確かめからは外す(イベントはワークフローに届くので、アクティビティの言語にかかわらない)。
  - 並列のイテレーションは一つずつ回す(`dd.atATime` を 1 に書き換える)。
  - `local` の規則を持つフローでは、履歴に、その規則のアクティビティのタスクが一つも無く、ローカルアクティビティのマーカー(`core_local_activity`)があることも確かめる。
  - ローカルアクティビティは、少なくとも一度走る。走らせたワークフロータスクが、結果をサーバーに記録する前に失敗するかタイムアウトすると、次のワークフロータスクが、同じ ID と試行の番号でもう一度走らせる。マシンが混んでワーカーが待たされると起き、GitHub の定時のジョブでは、規則を二度呼んだ実行が、二度目に次の呼び出しの応答を受け取って参照インタプリタと食い違った(2026-10-06 と 2026-10-08、`tests/flows/connect_rules.flow`)。そこで、三つの言語のランナーのスタブは、ワークフローの実行・アクティビティの ID・試行の番号が同じローカルアクティビティには、前に渡した応答をもう一度渡し、呼び出しを書き出し直さない。リトライは試行の番号が変わるので、次の応答を受け取る。ワーカーを 15 秒止めてワークフロータスクをタイムアウトさせると、直す前は TypeScript で食い違い、直したあとは TypeScript・Python・Go のどれも 22 本が参照と同じになった(2026-10-08)。
  - コピーでは、履歴を一件目から長いと数える(`dd.CONTINUE_AT` を 1 に書き換える)。フローの一番外のループは、一つの実行の最初のイテレーションを除くどのイテレーションも新しい実行で始めるので、イテレーションの境目のどこで続けても、呼び出しと終わり方が参照インタプリタと同じになることを確かめられる。実行が終わると、最初の実行から、続きの実行を一つずつたどって履歴を取る。
  - フローごとに dev server を立て、コアの数の 2 倍(`DANDORI_AT_ONCE` があればその 2 倍)までのフローを同時に流し、一つ終わるたびに次を始める。前は全部のフローを同時に流していて、GitHub の 4 コアのランナーでは、39 本のフローのワーカーと dev server が一度に立ち、呼び出しが一度も記録されないままアクティビティの StartToClose(20 秒)が切れる実行や、ランナーが送る前にイベントの待ち(5 秒)が切れる実行が出た(2026-10-06 と 2026-10-08 の定時の platforms)。一つのフローが立てるワーカーは一つなので、2 倍にすると、言語をまたぐアクティビティの確かめ(実行ごとに二つの言語のワーカーを立て、コアの数までを同時に流す)と同じ数のワーカーが同時に動く。手元の 14 コアでは、TypeScript の 853 本が 78 秒(全部を同時に流していたときは 100 秒)、`DANDORI_AT_ONCE=4`(GitHub のランナーと同じ 8 本ずつ)で 84 秒だった(2026-10-09)。Temporal を使うテストと durable functions の突き合わせ(こちらは全部のフローを同時に流す)は交代で走らせる。どれも多くのプロセスを立てるので、重なると互いを遅くし、横で走る Argo の突き合わせも遅くする。いちばん長い Argo の突き合わせは、交代を待たずに始める(待たせると、テスト全体が 20 秒ほど延びた)。
- **Temporal(Python)の突き合わせ**:生成したパッケージを `tools/temporal-python/run.py` が SDK 1.33.0 で、同じ dev server の上で実行する(ワークフローは SDK のサンドボックスの中で動く)。やり方は TypeScript 版と同じで、`Transport` の差し替え(`tools/transport.py`)も、利用者のタスク・規則・子ワークフローのスタブも、待ちの縮め方も、生成した `worker.py` と `client.py` を通すことも、クエリと search attribute を確かめることも、そろえてある。
- **Temporal(Go)の突き合わせ**:生成したパッケージを、`tools/temporal-go` のランナーが SDK 1.49.0 で、同じ dev server の上で実行する。ランナーは Go のプログラムで、生成したパッケージと一緒にコンパイルしなければならない。フローごとにビルドすると、Temporal の SDK を含むバイナリのリンクに時間とメモリがかかるので、`tools/temporal-go/build` が、テストの使うすべてのパッケージ(すべてのフロー、子の `.flow` の親と子、Worker Deployment Versioning の二つのビルド)を一つのバイナリに入れ、テストはそれをフローごとに起動する(`go_runner`)。パッケージはコピーを作り、Python のランナーと同じところを書き換える(タイマーを 10 ms まで、アクティビティと子ワークフローのタイムアウトを 20 秒に、`ddContinueAt` を 1 に、並列のイテレーションを一つずつに、イベントの待ちを 5 秒までに、規則をスタブに)。書き換える場所が見つからなければ、ビルドを止める。パッケージごとに型が別になるので、`Transport` は、ランナーのテンプレートがパッケージごとに書く小さな変換を通して差し替え、利用者のタスクは `OwnTasksBy`(タスクの名前を受け取る一つの関数)で差し替える。ほかは TypeScript 版、Python 版と同じで、`Transport` の差し替え(`tools/transport.py` を Go に移したもの)も、規則と子ワークフローのスタブも、生成した `worker.go` と `client.go` を通すことも、クエリと search attribute を確かめることも、そろえてある。
- **言語をまたぐアクティビティ**:同じフローの TypeScript 版と Python 版を作り、一方のワークフローを、もう一方のアクティビティで走らせる。Go 版と TypeScript 版でも、両方向に走らせる。この二組は順に流す。四通りをすべてのフローで一度に始めると、立つプロセスが一組のときの倍になり、テスト全体の中ではマシンが混んで実行がタイムアウトした(Go を足した最初のテスト全体で、dev server が三度とも 5 秒のうちに立たなかったランナーと、リトライするはずの呼び出しを一度しか記録せずに失敗で終わった実行が出た)。それぞれの組は、コアの数(`DANDORI_AT_ONCE`)までの実行を同時に流し、一つ終わるたびに次を始める(2026-10-06 にコアの数ずつ区切って流すようにし、2026-10-09 から空きができるたびに次を始める。手元の 14 コアで 2,156 本が 123 秒、区切っていたときは 147 秒)。ワークフローの側のランナーは、dev server を立て、ワークフローと子ワークフローのスタブだけを持つワーカーを動かす。アクティビティは、もう一方の言語のランナーが同じサーバーにつないで受け持つ(`--serve`。dandori が書くアクティビティ、利用者のタスクと規則と子ワークフローのスタブ、差し替えの `Transport`)。コールバックの応答とキャンセルの依頼も、アクティビティの側の言語の `client` から送る。呼び出しは、アクティビティの側が書き出したものを、終わってから受け取って参照インタプリタと比べる。`local` の規則を持つフローは外す(ローカルアクティビティはワークフローのワーカーの中で走るので、ワークフローと同じ言語になる)。
- **Temporal の再生**:ランナーは、実行が終わるたびにその履歴(続きの実行があればそれぞれの)を取り、生成した `replay` で同じコードにかけて、非決定的と言われないことを確かめる。生成した `histories` がワークフローの型で全部の実行(続きの実行も)を見つけることも確かめる。`tests/histories` には、ホテルの予約・引当と発送・審査・倉庫の出荷の Temporal 版(英語の版と日本語の版)と cancel の九つのフローについて、呼び出しのいちばん多い一本の履歴を TypeScript と Python と Go で一つずつ置き、いま生成したコードで再生する。倉庫の出荷の一本は三つの実行に、引当と発送の一本は二つの実行に続いたもので、どれも置く(倉庫の出荷のファイルは `run-45.json`・`run-45.2.json`・`run-45.3.json`)。履歴を置くときは、記録したマシンの名前とパス(ワーカーの名前、スティッキーなタスクキュー、スタックトレースの中のこのリポジトリと一時ディレクトリの場所)を中立の値(`@localhost`、`/work/dandori/`、`/tmp/`)に置き換える。再生はどれも読まない。ジェネレーターを直して、同じ `.flow` の走っている実行を再生できなくなると、ここで分かる(そうするしかない変更は `DANDORI_BLESS=1` で記録し直し、バージョンを上げるか Worker Deployment Versioning で出すよう書く)。
  - 引当と発送の子を `flow`(1.9)にしたときには、その一本が再生できなくなった。子ワークフローの型が `arrange_delivery` から子の `.flow` の名前とバージョンの `arrange_delivery_v1` に変わったからで、再生は `Child workflow type of scheduled event 'arrange_delivery' does not match child workflow type of command 'arrange_delivery_v1'` と言った。子の型やキューを変えるのも、走っている実行にとって互換でない変更なので、バージョンを上げるか Worker Deployment Versioning で出す。記録は取り直した。
  - 範囲(1.3)を足したときには、ホテルの予約の一本が再生できなくなった。記録した実行の入力の泊数(1002)が、例に書き足した範囲 `>=1 <=30` の外で、規則の結果の額(1004 円)も、rulec の範囲(12,000〜1,200,000 円)の外だったからである。新しいコードは入力を `Dandori.BadInput` で拒否し、履歴にあるアクティビティの予定と食い違う。後者はシナリオが作った結果で、本物の規則は返さない(rulec が範囲を証明している)ので、規則の結果を確かめるようにしたことは、範囲を書いていないフローの走っている実行を変えない。前者は本物の実行でも起こる。範囲を足したり狭めたりすると、走っている実行のうち値がその外にあるものは、再生で失敗に変わる。だから、その変更はバージョンを上げるか Worker Deployment Versioning で出す。記録は取り直した。
- **子の `.flow` の突き合わせ**:`tests/children` の受付(親)と査定(子)を、dandori が書いた二つのワークフローのまま、一つの dev server の上で走らせる(`tools/temporal/children.mjs`、`tools/temporal-python/children.py`、Go のランナーの `--children`)。親と子のワーカーは、生成した `worker` でそれぞれのタスクキューに立て、子のスタブは置かない。TypeScript どうし、Python どうし、TypeScript の親と Python の子、その逆、Go どうし、Go の親と TypeScript の子、その逆の七通りで、申込の区分ごとに一本ずつ走らせ、終わり方を参照インタプリタと比べる。参照インタプリタは、親が渡す入力で子を走らせて子の終わり方を求め、それを親の呼び出しの結果として親を走らせる。
- **Worker Deployment Versioning**:`tests/versions/approvals.flow`(一番外のループのイテレーションごとに承認を待ってから知らせる)から、通知の文面だけが違う二つのビルド A と B を作り、生成した `worker` で一つのデプロイのバージョンにして dev server で動かす。二回目のイテレーションは新しい実行で始まる(コピーの `CONTINUE_AT` を 1 にする)。A で始まって承認を待つあいだに B を current にしても、その実行は、新しい実行で始まった二回目のイテレーションも含めて A のコードで終わり、そのあと始めた実行は B のコードで走ることを確かめる。続けるときに `AUTO_UPGRADE` を付けると、二回目のイテレーションが B のコードで走り、このテストが落ちる(TypeScript と Python の両方で、埋めて確かめた)。Go でも同じことを確かめ、ランナーの二つのビルドの ID が、dandori が `worker.go` に書いた ID と同じであることも見る。ワーカーはワークフローをキャッシュしない(キャッシュにいたから A に残った、ということが無いように)。同じコードのビルド ID が同じで、違うコードのビルド ID が違うことも確かめる。
- **生成した TypeScript の型**:Temporal、durable functions、Argo の caller に出す TypeScript を、rulec が生成する規則のモジュールと一緒に、`tsc --strict`(TypeScript 7.0.2)に通す。既定の `Transport` が使うときに読み込むパッケージ(AWS SDK のクライアント、エージェントの SDK)は、何の型でもよいモジュールとして宣言する。
- **生成した Go**:すべてのフローの Go を、rulec が規則のために書く Go(規則ごとのモジュールを require して replace する、`rules.go` に書いたとおりの形)と一緒にモジュールにし、`go vet ./...` に通す。`gofmt -l` が何も言わないこと、つまり gofmt が書くとおりの形であることも確かめる(`generated_go_vets`)。英語の規則と日本語の規則は Go のパッケージの名前が同じ(`holdamount`)なので、二つのモジュールに分ける。
- **pydantic-graph の突き合わせ**:生成したパッケージを `tools/pydantic-graph/run.py` が pydantic-graph 2.51.0 で実行する。`Transport` の差し替えは Temporal の Python 版と同じもの(`tools/transport.py`)。利用者のタスクと規則はシナリオどおりに応答するスタブを `Deps` に渡し、時計は眠らずに時刻を進めるものに替える。コールバックの応答は `Deps.callbacks` に入れ、タイムアウトのシナリオでは入れずに、待ちを時計で切れさせる。タスクのタイムアウトは起こせないので、それを含むシナリオは外す。並列のイテレーションは一つずつ回す(`dd.at_a_time` を 1 に書き換える)。
- **Lambda durable functions の突き合わせ**:生成した TypeScript を `tools/durable/run.mjs` が SDK 2.4.0 のローカルのテストランナー(`@aws/durable-execution-sdk-js-testing` 1.1.4、時間を飛ばす)で実行する。`Transport` の差し替えは Temporal と同じ。規則は `registerFunction`、`durable function` の先は `registerDurableFunction` で登録したスタブで返し、後者は失敗するとき `ErrorData` を入れる。コールバックはその ID に応答を送って置き換える。このランナーは応答の来ないコールバックをタイムアウトさせないので、タイムアウトを含むシナリオは外す。ランナーの checkpoint のサーバーはコールバックの期限を Node の `setTimeout` で数え、Node は 2³¹−1 ミリ秒(24.8 日)を超える待ちをすぐに発火させる。そこで `run.mjs` は、生成したコードのコールバックの期限を 2,147,483 秒までに抑えてから走らせる。どの実行もコールバックをタイムアウトさせないので、答えは変わらない。抑える前は、`timeout until` の期限が 24.8 日より先のコールバック(review の例の run 7 など)が作ったすぐあとに期限切れになり、応答がそれより先に届いた実行だけが通っていた(2026-10-30 に CI のランナーで一度落ちた)。
- **Argo Workflows の突き合わせ**:生成した WorkflowTemplate を、kind の上の Argo Workflows v4.1.4(`tools/argo/setup.sh` が用意する)で `tools/argo/run.mjs` が実行する。コントローラーは本物を使い、Pod はランナーが演じる。
  - 演じる回の Pod は、クラスタに無いスケジューラーを待つように作らせる。
  - ランナーは、その Pod のコンテナと Argo の executor がするはずのことを代わりにする。Pod に書かれた呼び出しで、生成した `caller/call.ts`(差し替えの `Transport` を持たせる)か、`image` のタスク・規則・`workflow template` の先のスタブ(`tools/argo/stand-in.mjs`)をローカルで走らせる。テンプレートが宣言した出力は WorkflowTaskResult に書き、コンテナが残すはずの終了コードと終了メッセージで Pod を終わらせる。
  - `workflow template` の resource のステップは、子の Workflow を作って終わりを待つ。
  - コンテナを立てないので、一つのフローの全部の実行を同時に流せる。同時に流すフローは、マシンのコアの数まで(`DANDORI_AT_ONCE` で変えられる。多くて 14 本)で、空きができるたびに次を始める(12 章)。例の日本語の版を足して 21 本を一度に流すと、kind の API サーバーが WorkflowTemplate の適用をタイムアウトさせた。ランナーは、適用・読み取り・削除がタイムアウトで返ってくると、間をあけて数回送り直す(どれも二度送っても同じになる)。コールバックの応答(`argo node set` と `argo resume`)も、時間切れなら送り直す。`node set` は二度目に「もう設定されている」と言い、`resume` は待ちが終わっていれば通っているので、どちらも通ったかを見分ける。テストは、どれかのフローが食い違っても、ほかのランナーが終わって自分のワークフローを消すのを待ってから落ちる。一本目で落ちると、ほかのランナーが止められ、演じる回のワークフローがクラスタに残った。
  - Pod が終わって 1 分たっても、そのノードが終わっていなければ、ランナーは Pod にアノテーションを付ける。コントローラーはそれを Pod の変化として受け取り、Workflow を読み直してノードの終わりを書く(12 章)。演じた回の Pod でも本物の Pod の回の Pod でも、`workflow template` のステップが作った Workflow の Pod でも同じで、一つの Pod に 1 分おきに三回までである。テストは、そのことを `nudged the controller` と出力する。
  - 演じる回で、最後に演じた Pod のノードが、演じてから 5 分(`DANDORI_ARGO_STUCK_SECONDS`)たっても終わらないものは、名前を変えて二度まで演じ直す(5.1)。テストは、そのことを `played again` と出力する。
  - ランナーは、終わっていない実行のどれかが 10 分(`DANDORI_ARGO_MINUTES`)止まっていた(ノードの始まりも終わりも無く、その Pod を演じてもいない)ときか、全部で 30 分を超えたときにあきらめる。そのときは、終わっていない実行のノードと Pod と、最後に動いてからの秒数を書いて落ちる。試験は、フローごとに、ランナーが始めてから最後の実行が終わるまでの秒数を出す(12 章)。
  - 演じる回の Workflow は、テンプレートの中身をそのまま持つ。`workflowTemplateRef` だと、コントローラーは各 Workflow の最初の検証で、テンプレートを解決するたびに WorkflowTemplate を API サーバーから読み直す。大きなテンプレートで百本を同時に流すと、API サーバーが詰まった。
  - フローごとに一本(宣言したエラーを通るものがあればそれ)は、本物の Pod でも走らせる。`argo submit --from` と同じく WorkflowTemplate から作り、スタブのコンテナがクラスタの中のモック(`tools/argo/mock.mjs`)に結果を聞く。
  - コールバックの応答は `argo node set` と `argo resume` で渡し、タイムアウトのシナリオでは、タイムアウトしたときと同じ値を渡す。待ちは長さを 0 にしてすぐ進める。
  - タスクのタイムアウトは起こせないので、それを含むシナリオは外す。並列のイテレーションは一つずつ回す(`withSequence` を持つテンプレートの `parallelism` を 1 にする)。タイムアウトが起きないように、テンプレートの `activeDeadlineSeconds` には 120 秒を足して走らせる(本物の Pod の起動が期限に入るので。4.4 と 5.1。上限の試験では足さない)。
- **規則と日付の呼び出しの上限**(16 章):`tests/limits/limits.flow` は、規則を Lambda(`urgency`)と Connect のサービス(`urgency_service`)で一度ずつ呼び、どちらも `on timeout` で受ける。platforms の段の五つの試験(`rule_limits_hold_on_temporal`・`rule_limits_hold_on_localstack`・`rule_limits_hold_on_durable`・`rule_limits_hold_on_argo`・`rule_limits_hold_on_pydantic_graph`)が、規則の代わりに 15 秒答えない相手を置いて本当の時間で走らせ、どの試しも 10 秒で時間切れになること、試しが 10 秒とリトライの待ち(1 秒と 2 秒)の間隔で来ること、そのあと参照インタプリタのとおりに `on timeout` へ進むことを、呼ばれた回数と時刻と、実行がたどったステップで確かめる。ランナーは、環境変数 `DANDORI_LIMITS` があると、ふだん縮める待ちとタイムアウトを縮めず、呼ばれた時刻(`times`)を書く。durable functions と pydantic-graph の試験は Node か Python しか要らないが、本当の時間で一分ほど待つので、ほかの三つと同じく platforms の段で走らせる。
  - Temporal(TypeScript・Python・Go):dev server の履歴から、規則のアクティビティの START_TO_CLOSE のタイムアウト 3 回と、あいだの 1 秒と 2 秒のタイマーを読む。
  - Step Functions:LocalStack のモックは `States.*` を投げられず、答えるのに時間もかからない。そこで `tools/localstack/limits.mjs` が、LocalStack に本物の Lambda 関数(Node.js 22 で、15 秒待ってから答える)を作らせて走らせる。LocalStack は docker のソケットを借り、関数ごとにコンテナを立てる。LocalStack は HTTP Task を知らないので、Lambda で呼ぶ規則だけを試す。時間切れになった試しの遅れた答え(TaskSucceeded)は、次の試しの途中で履歴に入るので、試しの始まりから 14.5 秒より早い答えは、その試しのものと数えない。
  - durable functions:SDK のローカルの runner で、時計を進めずに(`skipTime` を false)、規則の Lambda と Connect のサービスの代わり(`Transport`)が 15 秒待ってから失敗する。
  - pydantic-graph:本当の時計で、Connect のサービスの代わりが 15 秒眠る。プロセスの中で計算する規則には上限を付けないので、試さない。
  - Argo:kind の上で、演じる回の Pod の中の `Transport` の代わり(`tools/argo/pod-transport.ts`)が答えず、ランナーがこの機械で走らせる caller の 10 秒のタイマーが、終了コード 143 で終える。モックの送り先は、呼ばれた時刻を書く。caller の中で計算する同梱の規則はタイマーで止められないので(16.7)、Connect のサービスで呼ぶ規則だけを試す。
  - CI の platforms のワークフローは、五つをほかの platforms の試験と同じく一つずつ回し、LocalStack の試験のために Lambda の Node.js 22 のイメージ(`public.ecr.aws/lambda/nodejs:22`)を先に取る。
- **コールバックの ID を渡す部分の上限**(17 章):`tests/limits/callbacks.flow` は、ID を渡すコールバック `pack`(`timeout 30 seconds`、`retry 1 times every 1 second`)を一度呼び、`on timeout` で受ける。`tests/limits/callbacks_argo.flow` は Argo のための版で(Argo はコールバックの `retry` を作れない)、caller で渡す Lambda のコールバックと、`image` のコールバックを一度ずつ呼ぶ。platforms の段の五つの試験(`callback_limits_hold_on_temporal`・`callback_limits_hold_on_localstack`・`callback_limits_hold_on_durable`・`callback_limits_hold_on_argo`・`callback_limits_hold_on_pydantic_graph`)が、本当の時間で走らせる。ランナーは `DANDORI_LIMITS` があると、シナリオがコールバックを時間切れにするところで、ID を渡す相手を 75 秒ふさぐ(渡し終えて答えないのではなく、渡し終えない)。どの試しも 60 秒で時間切れになり、1 秒のあとに試し直し、そのあと参照インタプリタのとおりに `on timeout` へ進むことを、呼ばれた回数と時刻と、実行がたどったステップで確かめる。
  - Temporal(三つの言語):履歴から、渡すアクティビティの 60 秒の START_TO_CLOSE のタイムアウト 2 回と、あいだの 1 秒のタイマーを読む。
  - Step Functions:`tools/localstack/limits.mjs` が、45 秒待ってから答える本物の Lambda 関数を渡す相手にする。渡す部分だけの上限は無く、Task の `TimeoutSeconds`(タスクの `timeout` の 30 秒)が渡すことと待ちをまとめて限るので、どの試しも 30 秒で `States.Timeout` になることを確かめる。
  - durable functions と pydantic-graph:渡す実装の代わりが 75 秒答えず、submit の step(pydantic-graph では渡す呼び出し)が 60 秒で時間切れになる。
  - Argo:Lambda のコールバックは演じる回で、caller の 60 秒のタイマーが終了コード 143 で終える。`image` のコールバックは本物の Pod で走らせ、スタブのコンテナが 90 秒答えず、Kubernetes が期限の 60 秒で Pod を止める(DeadlineExceeded。終了コードは 143 か、起動の前なら 137)ことを、ランナーが書き出す Pod の始まりと終わりの時刻(`DANDORI_ARGO_DUMP`)から確かめる。
  - CI の platforms のワークフローは、五つをほかの platforms の試験と同じく一つずつ回す。
- **エージェントの突き合わせ**:`Transport` の差し替えは、エージェントの呼び出しを `Transport` に渡された形(名前・モデル・指示・引数・Schema)のまま書き出し、シナリオの値を `{"answer": …}` に包んで返す。失敗は投げる(Agents SDK が拒否を投げるのと同じ)。ASL のランナーは、Responses API への HTTP Task に API のレスポンスの形(推論の項目と、`output_text` を持つメッセージ)で返し、失敗は、モデルが拒否した応答(`output_text` の無いメッセージ)で返す。Messages API への HTTP Task には、思考のブロックと `text` のブロックを持ち `stop_reason` が `end_turn` の応答で返し、失敗は `stop_reason` が `refusal` の応答で返す。ステートマシンがそれを読む式で Task が失敗し、Retry と Catch に渡るところまでを確かめる。ランナーは、Task の Assign と Output の式の失敗を、Step Functions と同じく Retry と Catch に渡す。
- **既定の `Transport` のエージェント**:TypeScript の既定の実装(Temporal・durable functions・Argo の caller が同じ `io.ts` を使う)と Python の既定の実装(Temporal と pydantic-graph が同じ `io.py` を使う)を、OpenAI の Agents SDK(TypeScript 0.18.0、Python 0.22.3)で走らせる(`tools/agents`)。モデルには、OpenAI の代わりに、応答を前もって決めた SDK のテスト用のモデル(`ScriptedModel`)を渡すので、OpenAI には何も送らない。シナリオのエージェントの呼び出しすべてについて、モデルが受け取ったもの(モデルの名前・指示・入力の文字列・応答の Schema)が Step Functions の同じ呼び出しで送るものと同じで、モデルの設定もツールも空であること、応答がそのまま返ること、拒否が `ModelRefusalError` になることを確かめる。また、`Transport` が作るクライアントをローカルの Responses API のモックに向け、500 が返ると一回だけ送って `InternalServerError` で失敗することを確かめる。Claude のエージェントは、Anthropic の SDK(TypeScript 0.128.0、Python 1.8.0)で、ローカル(127.0.0.1)に立てた Messages API のモックに送らせる。モックが受け取ったリクエスト(パス、`anthropic-version`、本文)が Step Functions の同じ呼び出しで送るものと同じで、一回だけであること、応答がそのまま返ること、拒否(`stop_reason: refusal`)で `AgentStopped` を投げること、500 が返るとリトライせずに `InternalServerError` で失敗することを確かめる。Anthropic には何も送らない。Open Responses のほかのサーバーのエージェント(`url`)は、送り先のパスはそのままに、ローカルに立てたそのサーバーのモックに送らせる。モックが受け取ったリクエスト(メソッド、パス、本文)が Step Functions の同じ呼び出しで送るものと同じで、一回だけであること、応答がそのまま返ること、拒否で `AgentStopped`、500 でリトライせずに `AgentHttpError` になることを確かめる。Go の既定の実装(`io.go`。Agents SDK が無いので openai-go と Anthropic の Go の SDK)は、Go のランナーの `--agents` が、`TransportOptions.HTTPClient` で送り先をローカルのモックに替えて走らせる。OpenAI のエージェントも、モックの Responses API が受け取ったリクエスト(メソッド、パス、本文)が Step Functions の同じ呼び出しで送るものと同じで一回だけであること、Claude のエージェントは TypeScript と Python と同じことを確かめる。拒否は `AgentStopped`、500 は SDK のエラー(`Error`)か、Open Responses なら `AgentHTTPError` で、どれもリトライしない。
- **本物の Open Responses のサーバー**:このマシンで Ollama が動いていれば(`DANDORI_OLLAMA`、既定は `http://127.0.0.1:11434`)、`url` のあるエージェントごとに、シナリオで応答のある最初の呼び出しを、モデルを Ollama にあるもの(`DANDORI_OLLAMA_MODEL`、無ければいちばん小さいもの)に替え、エフォートを外して(小さなモデルは推論しないことがあり、Ollama はそうしたモデルへのエフォートを拒否する。エフォートの形はモックのサーバーで Step Functions と突き合わせている)、TypeScript と Python と Go の既定の `Transport` から本当に送る。応答は、タスクの型に合わなければならない。Ollama が無ければ SKIP にする。テストの前に読み込まれていなかったモデルは、終わったら下ろす(Ollama は読み込んだモデルを 5 分置き、8B のモデルでは 8 GB を超える。テスト全体の途中でメモリが逼迫した)。
- **既定の `Transport`**:シナリオの呼び出しのうち `Transport` を通るもの(HTTP、Lambda、AWS の API)を、TypeScript の既定の実装(`fetch` と AWS SDK for JavaScript v3)と Python の既定の実装(標準ライブラリと boto3)から、ローカルのモックに送る(`tools/wire`)。ローカルのサーバーは、HTTP と Lambda の Invoke に、ランナーの差し替えの `Transport` がそのシナリオで返すものと同じレスポンスを返す。HTTP のタスクの URL は、スキームとホストをこのサーバーに置き換える。AWS の API は、AWS の API をローカルでまねる moto(5.2.3)に渡す。`tools/wire/requirements.txt` は、boto3 と moto の依存までバージョンとハッシュを固定したロックファイルで、`requirements.in` から `uv pip compile --generate-hashes` で作る(2026-10-06 から。前は二つの名前だけで、依存は入れるたびに新しいバージョンになり、脆弱性の検査も読めなかった。ritsu の DESIGN 3.6)。届いたもの(HTTP ならメソッド・パス・クエリ・ヘッダ・本文、Lambda なら関数と payload、SNS と SQS なら moto のキューに届いたメッセージ)が呼び出しと同じで、`Transport` が返すものが差し替えの `Transport` と同じで、TypeScript と Python が同じ文字列を送ることを確かめる。Go の既定の実装(`net/http` と AWS SDK for Go v2)も、Go のランナーの `--wire` が同じ呼び出しを送る。AWS SDK は `AWS_ENDPOINT_URL` でローカルのサーバーに向ける。Go の map は順を持たないので、Go はオブジェクトの鍵とクエリとフォームの組を名前の順に書く。そのため Go については、JSON の本文は値として、クエリとフォームは組の集まりとして比べ、文字列は比べない。AWS のエラーは、moto に起こさせられるもの(無いトピックへの `sns:publish` の `NotFoundException`、無いキューへの `sqs:sendMessage` の `QueueDoesNotExist`)だけを試す。何もローカルの外には出ない。
- **つなぐコード**:Lambda の Python、`rules.ts`、Python の二つのプラットフォームの `rules.py`、`rules.go` を、rulec が生成した Python と TypeScript と Go と一緒に動かし、`rulec vectors` の全件で rulec の期待値と比べる。例が呼ぶ規則はすべて比べ、Step Functions に出せない例の規則も外さない(審査の規則は Step Functions に出せない例にしか無く、前は外れていた)。出力が一つの規則は、rulec の関数がその値をそのまま返す(二つ以上なら `Output` のレコード)。つなぐコードはこれを知らずにフィールドを読んでいて、審査の規則を足したときに `tsc` がそれを見つけた。durable functions 向けに出す規則の Lambda が、Step Functions 向けのものと一字も違わないことも確かめる。
- **日付と帳簿の突き合わせ**(1.16):日付と帳簿の操作には、どのプラットフォームでも、規則やタスクと同じくシナリオが答える。日付には、一つの実行の中でどれも違う日付(と、時刻を言う日付なら時刻)で答える。帳簿の操作には、通った(`{"ok": {"result": "done"}}`)、前に通った(`done_before`)、帳簿の行の理由ごとの拒否(宣言したエラー)、失敗、タイムアウトで答える。帳簿は答えの形を決めているので、型の合わない答えやありえない状態の答えは選ばない(`done` と `done_before` のどちらでも、仮押さえは引数から作る)。ランナーは、日付のアクティビティと `Transport` の `book` をスタブにし、走らせるコードの時計を、その実行のシナリオの `now` に替える。期間を書いていないワークフローでは `2026-03-31T15:30:00Z` で、期間を書いたワークフローの時刻は 20 章。時刻の並びの実行では、走り始めに最初の時刻を、それより後に次の時刻を返す。Temporal の三つ・durable functions・pydantic-graph ではコードのコピーの時計の関数を書き換え、Step Functions では `tools/asl-run.mjs` がステートに入った時刻をその時刻にし、LocalStack の定義と、Argo の時計を読むテンプレート(`dd-period-start` と `s<場所>-now`)の `now()` は、その時刻の文字列に置き換える。参照インタプリタも、シナリオの `now` を読む。
- **日付のつなぐコード**(1.16):Lambda の Python、`rules.ts`、Python の二つのプラットフォームの `rules.py`、`rules.go` の日付のアクティビティを、koyomi が生成したコード(koyomi のライブラリで、テストのプロセスの中で作る)と一緒に動かし、koyomi の `vectors` の全件(入力の範囲のすべて)で、日付と `at` を koyomi の期待値と比べる。カレンダーがオフセットを言うファイルでは、七件に一件、日付の代わりに、その日の 0 時 30 分(オフセットで)を UTC にした時刻を渡し、同じ日として読むことも確かめる。
- **規則の前提の確かめ**(1.17):テストは、フローが呼ぶ規則の前提をすべて決められなかったものとして渡すので、前提を持つ規則を呼ぶ `tests/flows/preconditions.flow` と `preconditions.ja.flow` には、確かめる文が四つずつ入る。シナリオは、確かめる文ごとに、前提が保たれる実行と破れる実行を持ち、どのプラットフォームも、破れる実行を `Dandori.BrokenPrecondition` と同じ理由で、同じ場所で終えなければならない(タスクの直後で終わる実行は、次のタスクを呼ばない)。日の前提は、koyomi の日の並びを書き込んだコードで比べる。規則の日付の入力を日の番号に直すつなぐコードは、上の「つなぐコード」の確かめが、rulec の `vectors` の全件で確かめる。ritsu の側(`ritsu dandori build` が ritsu-cross の答えで同じものを書くこと)は、ritsu のテストが確かめる。
- **帳簿を本物のデータベースで**(1.16):例とテストのフローの実行が出す帳簿の操作の並び(参照インタプリタが Temporal に見せる形で、違う並びを一つずつ)を、dandori が書く `Transport` の `book`(TypeScript の `io.ts`、Python の `io.py`、Go の `io_books.go`)と、Step Functions 向けに書く Lambda 関数のコードから、chobo が書くクライアントに通し、PostgreSQL、TigerBeetle、MySQL の上で動かす(`books_run_on_postgres_tigerbeetle_and_mysql`)。サーバーは chobo のテストと同じく ritsu-testkit が立て、PostgreSQL には chobo が書く SQL を、MySQL には帳簿ごとに chobo が書くテーブル(`chobo build --target mysql`)を、テストのデータベースに入れる。MySQL では、TypeScript には mysql2 のプール(呼び出しごとに自分のトランザクション)を、Python には PyMySQL の autocommit の接続を、Go には go-sql-driver/mysql の `*sql.DB` を渡す。TiDB では回さない(TiDB は chobo のテストが確かめる)。クライアントの依存(`pg`、`tigerbeetle-node`、`mysql2`、psycopg、tigerbeetle、PyMySQL、pgx、tigerbeetle-go、go-sql-driver/mysql)は、chobo の `tools/runner` のものを読むだけで使う。並びごとに、空の帳簿で、帳簿の外から入れる振替(`receive`)で満たしてから、そのあと同じ帳簿でもう一度(どの操作も前にした)の三通りで流し、どの答えも、同じ操作に chobo の参照インタプリタ(帳簿の口の `open`)が返すものと同じでなければならない。durable functions と Argo の caller の `io.ts` が Temporal のものと、pydantic-graph の `io.py` が Temporal の Python 版のものと、頭の注のほかは同じであることも確かめるので、ほかのプラットフォームの実装もこれで確かめたことになる。サーバーかツールが無ければ SKIP にする。
- **サービスを実装するフローのシナリオ**(1.14):start の入力から、入力の型が受け取るゼロ値(空の文字列、範囲に入る 0、false、空のリスト、ゼロ値を持つ列挙のゼロ値)のフィールドを省いた実行を足す。中のメッセージはゼロ値のメッセージにし、メッセージのリストには、ゼロ値のメッセージを一つ入れて、どの深さでも省く。サービスに書かれた `event` の値と `callback` の応答にも、同じように省いたものを選べる。参照インタプリタは、省かれたものを埋めてから読むので、埋めないプラットフォームは食い違う。また、設定されているかどうかが分かるフィールド(メッセージ、`optional`、`oneof` の一つ)を `T?` でない入力で読むなら、その最初の一つを省いた入力の実行を一本足す。どのプラットフォームも、何も呼ばずに `Dandori.BadInput` で終えなければならない(`json` の入力は選ばない。無ければ null として読んで進むので、拒否の試験にならない)。型が受け取らないゼロ値(ゼロ値を持たない列挙)は省かない。省いても、埋めても埋めなくても型の検査で落ち、埋める誤りと区別がつかないからである。形の合わない値として選ぶのは、`connect` のタスクと同じく `[]` にした。`{}` は、ゼロ値をすべて省いたメッセージとして読め、埋めると形が合ってしまう(作る途中で、参照インタプリタがシナリオに無い次の呼び出しへ進んで分かった)。
- **入力が無い `json` のシナリオ**(1.2):`json` を入力に、または入力のレコードのフィールドに持つフローには、呼び出しがいちばん多い実行と同じ結果の並びで、その入力から無くてよいもの(`json` と、値の無い `T?`)を全部省いた実行を一本足す。どのプラットフォームも、それを null として読み、参照インタプリタと同じ呼び出しを、同じ引数(`json` は `null`)で出して、同じに終わらなければならない。サービスを実装するフローに限らない(Temporal のクライアントが、入力のフィールドを省いて始めることもある)。
- **Connect で呼ぶ規則の結果**(1.13):シナリオは、規則の出力を選んでから、rulec のサービスが書く形(protojson。ゼロ値のフィールドを省き、数を文字列、列挙を `.proto` の名前で書く。0 番が規則の値の列挙は、その値のときに省く)にして返す。ほかに、失敗(リトライを通る)、範囲の外の数、形の合わない結果(`[]`)、数と真偽と、0 番が規則の値の列挙をゼロ値にして省いた結果、列挙を省いた結果(0 番が「設定されていない」の列挙だけ。埋めると `…_UNSPECIFIED` になり、結果の検査が拒否する)を選べる。スタブの `Transport` は、`ok` を 200 の本文として、失敗を 500 として返す。
- **rulec gen と名前をそろえる**(1.13):例と `tests/flows` が使う規則ごとに `rulec gen` を走らせ、書かれた `.proto` を、dandori 自身の `.proto` を読むコードで読む。サービスとメソッドのパス、リクエストとレスポンスのフィールドの JSON の名前と型、列挙の値の名前と 0 番の名前、ゼロ値を埋める表が、dandori が規則の口から読んだものと同じであることを確かめる。例の規則に加えて、列挙を `.proto` から取り込む規則(`import proto`)を二つ、`tests/fixtures/rules` に置く。`delivery_fee.rule` の契約は buf の慣わしどおりで、値は接頭辞を持ち(`MEMBER_TIER_BASIC`)、0 番は `MEMBER_TIER_UNSPECIFIED` である。`account_fee.rule` の契約(`account.proto`)は、値に接頭辞が無く、0 番の `ACTIVE` が本当の値である。どちらも型は package 付き(`shop.v1.MemberTier`、`bank.v1.Status`)で、例の規則の列挙はどれも、この二つのどちらの形も持たない。契約のファイルは、`rulec gen` が package のディレクトリ(`proto/shop/v1/order.proto`)にコピーするので、dandori の読み手は、規則の `.proto` の import をそこから読む。レスポンスのフィールドは、値があるかを持たないこと(埋める決まりの前提)も確かめる。リクエストのフィールドは、いまの rulec ではどれも `optional` である。`rulec gen` が書くものは、rulec のライブラリで同じプロセスの中で作るので、ほかのツールは要らない。
- **規則のレスポンスの読み方をそろえる**(1.13):サービスのレスポンスは、参照インタプリタ(`render::rule_read`)、生成する TypeScript と Python と Go、ステートマシンの JSONata の五か所で規則のレコードに読まれる。`tests/flows/connect_rules.flow` の三つの規則と、`connect_rules_contract.flow` の規則(0 番が規則の値の列挙を持つ)について、サービスが書くレスポンスと、書かないはずのレスポンス(フィールドが無い、null、別の種類、十進でない数、桁数や 2^53 − 1 の境目の数、どの列挙にも無い名前、`constructor` や `__proto__` という名前、オブジェクトでない本文)を作り、五つの読み方が同じレコードを返すことを確かめる。シナリオにも、範囲の外の数や形の合わないレスポンスは入るが、決まった形のものだけで、境目の数(桁数、2^53 − 1)や変わった名前(`constructor`)までは出ないので、別に確かめる。
- **本物の規則のサービス**(1.13):`rulec gen` が書いた Python のサービスを、そのまま立てる(`--http`、標準ライブラリのサーバー)。サービスは connectrpc と、buf が書くスタブを import するので、`--http` でもそれが要る(2026-10-01 に確かめた。無ければ `ModuleNotFoundError: No module named 'connectrpc'` で立たない)。スタブは、手元のプラグイン(protoc-gen-py と protoc-gen-connectrpc)を指すテンプレートで `buf generate` が書くので、ネットワークは要らない。取り込んだ契約のスタブも同じく書かれ、サービスはそれを package のモジュール(`stubs.shop.v1`)から読む。`rulec vectors` の全件を、dandori が組み立てる本文(null でない入力を全部、ゼロ値も書いたもの)で送り、レスポンスを dandori の読み方で読んで、rulec の期待値と比べる。0 番が規則の値の列挙をサービスが省いたレスポンスも、その中にある。成功のレスポンスのヘッダ `rulec-source-sha256` が規則の SHA-256 と同じであることも確かめる。サービスが受け付けないはずのものも送る。範囲の外の入力、列挙に無い名前、知らないフィールド、入力を一つ省いたリクエストは、どれも 400 の `invalid_argument` にならなければならない(dandori は、200 でないものをどれも `failure` として読む。1.13)。比べるのはコードと HTTP の状態だけで、本文は比べない。後の二つの本文(`Failed to decode request body: …`)は connectrpc と protobuf が書くもので、その版で変わりうるからである。ツール(buf と、`tools/connect/.venv` の connectrpc 0.12.1・protoc-gen-py 0.6.0・protoc-gen-connectrpc 0.12.1)が無ければ SKIP にする。
- **テストが読む rulec**:規則は、rulec 自身が答える ritsu の規則の口(`rulec::ports::Engine`)から、同じプロセスの中で読む(ritsu の DESIGN 3.3。dandori のクレートは rulec を dev-dependency にだけ持つ)。テストが要る `rulec gen` と `rulec vectors` の出力も、rulec のライブラリ(`rulec::codegen::generate` と `rulec::vectors`)で作る。テストは rulec のバイナリを探さず、`RITSU_TEST_LEVEL=fast` でも規則を読む。前は rulec のバイナリを `DANDORI_RULEC` で受け取り、`rulec api` の `connect.enums` があるかで rulec を見分けて、無い rulec(0.21.2 まで)では、それが要るテストを飛ばしていた。ritsu の中では読む rulec がいつもワークスペースの rulec なので、見分けるのをやめ、名前を言わない rulec の E005 を記録した出力で確かめていたテスト(`tests/fixtures/unnamed_enums`)も消した。
- **dandori のクレートのバイナリ**:規則を使わないフローでは、`check`・`build`・`scenarios`・`doc` が、rulec の口を渡したコマンドと同じものを出し、同じものを書くこと。規則を使うフローでは、`use rule` ごとに E005 で `ritsu dandori` で走らせるよう言うこと(英語と日本語)。`ritsu dandori` そのものは、ritsu のクレートのテストが走らせる。
- **protobuf 自身の JSON**(1.14):サービスを実装するフローの `.proto` を protoc で記述子にし、protobuf の Python の `json_format` で確かめる。シナリオの入力が start のリクエストとして読めること(知らないフィールドは拒否される)。それを protobuf が書き直した JSON(ゼロ値を省いた形)を dandori が埋めると、シナリオの入力に戻ること。参照インタプリタの出力がレスポンスとして読めること。`event` の値と `callback` の応答が、それぞれのリクエストとして読めること。クエリ `dandori.status` が返すものが `dandori.v1.Status` として読めること。protoc か、Temporal の Python のランナーの venv(protobuf が入っている)が無ければ SKIP にする。
- **buf と protoc**(1.14):`proto/dandori/v1/options.proto` が buf の標準の lint を通り、buf で組み立てられることを確かめる。例とテストにあるサービスの `.proto`(`dandori/v1/options.proto` を import するもの)は、どれも protoc で組み立てられること(`proto/` を import のルートに足して)を確かめる。ディスクに無いファイルを import するもの(Protovalidate の規則を使う試験の `.proto` と、読めない import を試す `.proto`)は外す。例の `.proto` は `specs/` の直下に並べて置いているので、buf の lint の、パッケージとディレクトリをそろえる規則には合わず、lint は当てない。buf か protoc が無ければ、それぞれ SKIP にする。
- **記述から作る型**(1.12):`tests/protos.rs` が、`tests/fixtures/protos` の二つの記述(`types.proto` と、buf の形で import する `shop/v1/order.proto`)のメッセージと列挙の全部を、フローの型として名前を書いて作り、作った型が、同じメッセージ・列挙に、送る向き(そのレコードを引数に渡すタスク)でも、読む向き(そのレコードでレスポンスを読むタスク)でも合うことを確かめる(`apis::made_fits`)。型を作る表(`made_elem`)と突き合わせる表(`proto_shape`)は二か所に書いてあり、食い違えばここで落ちる。ほかに、作った型のフィールドと範囲とゼロ値、自分を含むメッセージと、手で書いた自分を含むレコード(`tests/fixtures/self_holding.flow`)がエラーになること、buf の形の import と、ファイルのディレクトリから探す import を確かめる(`src/proto.rs` と `src/apis.rs` の単体テストも)。`tests/fixtures/proto_types.flow`(E002・E003・E006・E008・E016)と `proto_ranges.flow`(E014・W104)は、診断を英語と日本語で固定する。E014 は、名前や型の誤りがあって作れなかったフローでは出ないので、別のファイルにした。`tests/flows/proto_types.flow` は、作った型で案件を運び、範囲のある数、文字列で来る 64 ビットの整数、日時、`optional` のフィールド、ゼロ値を省いたレスポンス、範囲を外れたレスポンスを、全プラットフォームで参照インタプリタと突き合わせる。
- **読めない import**(1.10):`tests/fixtures/protos/unread.proto` は、`google/api/annotations.proto`(オプションのため)と `google/type/money.proto`・`shop/v2/cancel.proto`(型のため)を import し、三つともディスクに無い。`tests/protos.rs` は、その記述を読めること、読めなかったファイルにある型に当たらない使い方(型を作る、メソッドを呼ぶ)は何も言われないこと、当たったとき(フィールドの型が読めなかった型のメッセージから型を作る、リクエストやレスポンスの型が読めなかった型のメソッドをタスクに合わせる、無い型やメソッドを書く)に E002 か E016 が、読めなかったファイルを添えて出ることを確かめる。`tests/fixtures/proto_unread.flow` が診断を英語と日本語で固定する。`proto.rs` の単体テストは、読めない import を飛ばして記録すること、すべて読めた記述で名前の誤りは今までどおりエラーになること、ファイルがあって `.proto` として読めないときもエラーになることを見る。
- **診断**:`tests/fixtures` の各ファイルの診断を、英語と日本語の両方で固定する(`DANDORI_BLESS=1` で書き直す)。
- **文書**:README とサイト(ワークスペースの根の `website/dandori/docs`、`website/dandori/docs-ja`)に載せたものが実物と食い違わないことを、`tests/docs.rs` で確かめる。診断の抜粋は、どれも `tests/fixtures` の golden ファイルにそのまま含まれていること。` ```flow ` のブロックの行は、どれも `examples` か `tests` の `.flow` にある行であること(`…` で省いた行は、残りの部分がその順で一つの行に含まれていること)。二つの言語の診断コードの一覧が、ソースにあるコードとちょうど一致し、コードの数を書いたページがその数を書いていること。サイトのハイライトに使う `KEYWORDS` が、`src/syntax.rs` のものと一字一句同じであること。エージェント向けのスキル(`skills/dandori`)も同じテストで確かめる。スキルに入れるサイトの英語のページのコピーは `skills/sync.sh` が作り、`tests/skill.rs` が、コピーがページと食い違っていないこと、スキルの中のリンクがスキルの外を指していないこと、`SKILL.md` の frontmatter が Agent Skills の形に合うことを確かめる。
- 突き合わせるのは `examples` の例と、言語のエッジケースを通すための `tests/flows` のフロー。例は、プラットフォームごとの版をディレクトリに分けて置く。主なプラットフォームである Temporal 向けの `temporal/`(HTTP は dandori が書くアクティビティ、ほかは利用者が書くアクティビティ、外からの通知は `event`、ほかのタスクキュー、`on cancel`、`local`)、Step Functions と Lambda durable functions に向けた `aws/`(Lambda、EventBridge の接続を通す HTTP、SNS と SQS を、Step Functions と同じに呼ぶ)、`pydantic-graph/`(プロセスの中の関数、規則もプロセスの中、コールバックも同じプロセスで返す)の三つで、審査には Argo Workflows 向けの `argo/`(タスクは利用者のイメージのコンテナ)もある。審査の `aws/` は、どのタスクも利用者が書くコードなので、Lambda durable functions だけが走らせる(Step Functions では E050)。例のディレクトリのすぐ下には、どのプラットフォームでもそのまま動くものだけを置く(引当と発送の三つの版が子として走らせる `arrange_delivery.flow`)。`rules/` と `specs/` は版が共に読む。
- どの版にも、同じディレクトリに日本語の版(`<名前>.ja.flow`。引当と発送の子は `arrange_delivery.ja.flow`)がある。流れも呼び出しも英語の版と同じで、シナリオの数も図の形も一致する。ワークフロー、タスク、エラー、案件、変数、説明、コメントは日本語で書き、英語のまま残すのは、API の記述が決めている名前(Stripe のフィールドと状態と経路の変数、倉庫の `.proto` のフィールドと値、SNS と SQS の API の引数)、Stripe の文書を転記した規則 `payment_intent.rule` の名前、どこでもこの綴りで使う `id`、倉庫の API の `expand` だけである。日本語の規則(宿泊の与信額・注文の状態・出荷の急ぎ・問い合わせの振り分け)は、英語の規則と同じ `rules/` に置き、`tests/flows` もこれを読む。日本語のサイトの例のページは、日本語の版から描く。 サービスの記述(1.14)はワークフローそのものを書くので、日本語の版は、JSON の名前(`json_name`)を日本語にした記述(`fulfillment.ja.proto`)のサービスを実装する。
- `temporal/`・`pydantic-graph/`・`argo/` の版は、そのプラットフォームのランナーだけで走らせる。`aws/` の版とすぐ下のフローは、どのプラットフォームでも走らせる。`aws/` の版の Lambda・HTTP・AWS の呼び出しは、dandori がどのプラットフォームにも書くので、ほかのプラットフォームでの確かめはこれで足りる。ただし `aws/` の日本語の版は、AWS の二つ(Step Functions と Lambda durable functions)だけで走らせる。その呼び出しをどのプラットフォームでも確かめる役は英語の版が受け持ち、どのプラットフォームにもそれ向けの日本語の版がある。どのプラットフォームでも走らせると、突き合わせるフローが一度に倍近くになり、テスト全体の途中でタイムアウトが起きた(Temporal のアクティビティの 5 秒の期限、kind の API サーバー)。例の日本語の版と `tests/flows` のフローは日本語の名前で書いてあるので、日本語の名前が五つのプラットフォームでそのまま識別子や鍵になることも、ほかと同じ突き合わせで確かめる(`timeouts.flow` は、HTTP のパスにも日本語の値を入れる)。

`Transport` の既定の実装は、突き合わせでは走らない(差し替える)。突き合わせが確かめているのは、dandori が書く実装が何を送り、結果をどう読むかで、既定の実装が本当に送るところは、上の既定の `Transport` の確かめで、ローカルのモックに向けて確かめる。本物の Stripe や AWS に向けて送るところは確かめていない。エージェントの既定の実装は、上のとおり OpenAI では Agents SDK の中まで、Claude では SDK が HTTP で送るところまで走らせるが、OpenAI と Anthropic の API が Schema を受け付けて返すところは確かめていない。

### 5.1 走らせた(2026-09-26)

- 例は五つで、それぞれ Temporal 向け・AWS 向け・pydantic-graph 向けの三通り(審査は Argo 向けも。5 章)。ホテルの予約(Stripe の PaymentIntent。Stripe の OpenAPI の記述に合わせる)、倉庫の注文の出荷(rulec の規則 `order_state`)、注文の引当と発送(リスト、並列のイテレーション、オプショナルな値、`json`、Connect で呼ぶ倉庫、Smithy のモデルに合わせた SNS と SQS、SQS のコールバック、子ワークフロー。子は dandori で書いた `arrange_delivery.flow` で、Temporal 版は、子が翌日便の車を取れなかったとき(子の `fail NoVan`)に通常便で頼み直す)、問い合わせの振り分け(Jev、エージェント二つ、規則一つ。種類は Jev が選び、Jev が確信を持てないときや呼べないときは、エージェントが読み取った種類を使う。読み取るのは OpenAI、下書きするのは Claude のエージェントで、読み取りに失敗すれば総合の窓口に起票し、下書きに失敗すれば下書きなしで起票する)、申し込みの審査(Jev が採点して確信度を率で返し、規則 `review_policy` がそのまま承認・却下するか人に回すかを決め、人は利用者のコールバックで承認する。Temporal 向けは採点とお知らせに `queue`、Argo 向けは承認とお知らせに `image`、AWS 向けは durable functions だけが走らせる)。ほかに `tests/flows/edges.flow` が、配列の `json` をリストに入れること、並列の中の並列、理由の無い `fail` を通り、`tests/flows/agents.flow` が、リスト・単位・時刻・オプショナルな値・範囲のある数を持つエージェントの応答、応答を読まないエージェントの呼び出し、並列のイテレーションの中のエージェントとそのリトライ、列挙の値を大文字と小文字を変えて返す Claude のエージェントを通り、`tests/flows/timeouts.flow` が、タイムアウトを処理する呼び出し、タイムアウトだけをリトライする呼び出し、`timeout` を書かないタスクのタイムアウトを通り、`tests/flows/local_rules.flow` が、`local` の規則(タイムアウトを処理してタスクを呼ぶ)と、同じ規則をふつうのアクティビティで呼ぶことを通り、`tests/flows/events.flow` が、`event` のタスク(承認のイベントとその拒否とタイムアウト、一番外のループのイテレーションごとに待つ、案件の状態を知らせる配達のイベント)を通る(Temporal だけ)。`tests/flows/jev.flow` は、Jev のすべての種類の質問(一部の値にだけ意味を書いた choice、score、意味を書いた noul、一回で四つに答えるレコードと確信度の率)、下限を切った答えを処理する呼び出しと処理しない呼び出し(`on failure` へ)、下限を切った答えのあとも前の値が残る変数、並列のイテレーションの中の Jev、結果を読まない呼び出し、ステータスで宣言したエラーのリトライを通る。`tests/flows/connect.flow` は、Connect のレスポンスが省く既定値(空の文字列、0、false、列挙の最初の値、空のリストとマップ、文字列の 64 ビットの整数)を、レスポンスの中、中のメッセージ、メッセージのリストで埋めて読み、次の呼び出しに渡す。
- シナリオは 50 本・38 本・20 本・13 本・10 本と、引当と発送の子の 9 本、connect の 7 本、edges の 12 本、agents の 17 本、timeouts の 7 本、local_rules の 12 本、events の 12 本。Step Functions では、審査と events を除く 185 本すべてが参照インタプリタと一致した(LocalStack の上でも同じ 185 本が一致した。下の項)。Temporal では、dev server の上で、`on cancel` を持つ cancel の 32 本、events の 12 本、例の Temporal 版の 161 本(ホテルの予約と倉庫の出荷は、`on cancel` のキャンセルのシナリオを加えて 62 本と 49 本、引当と発送は子の `NoVan` を処理するところを加えて 27 本)を加えた 400 本すべてが一致した(アクティビティのタイムアウトはサーバーがタイムアウトさせ、コールバックのタイムアウトは応答を送らないことで起こせるので、外したものは無い)。Temporal の Python 版でも、同じ 400 本すべてが一致した。どちらの言語でも、このうち 33 本(倉庫の出荷の 11 本と、その Temporal 版の 13 本、引当と発送とその Temporal 版の 1 本ずつ、edges と local_rules の 2 本ずつ、events の 3 本)が一番外のループのイテレーションで新しい実行に続き、続きの実行は合わせて 50 本だった(倉庫の出荷の 7 本と、その Temporal 版の 8 本と、events の 2 本は、三つの実行にまたがる)。Lambda durable functions では、タイムアウトを含む 17 本を除く 178 本すべてが一致した。pydantic-graph では、タスクのタイムアウトを含む 5 本を除く 321 本(例の pydantic-graph 版の 131 本を含む)すべてが一致した。agents の 17 本のうち 2 本は、Claude のエージェントが列挙の値の大文字と小文字を変えて返す(どちらも `Normal` と、`urgent` と `Low` のリスト)。どのプラットフォームも、それを宣言した値として読んだ。
- Argo Workflows(v4.1.4、kind の上)では、タスクのタイムアウトを含む 5 本を除く 190 本すべてが参照インタプリタと一致し、フローごとに本物の Pod で走らせ直した一本(十一本)も一致した。コールバックのタイムアウトは、待ちの出力にタイムアウトのときの値を入れて起こした。一回の実行のノードの数は、多いもので、ホテルの予約が 567 個(E040 の見積もりは 939 個)、倉庫の出荷が 239 個(446 個)、edges が 295 個(593 個)、引当と発送が 229 個(4,746 個)、問い合わせが 94 個(153 個)、agents が 115 個(177 個)、審査が 74 個(136 個)、timeouts が 49 個(87 個)、local_rules が 117 個(272 個)、引当と発送の子が 44 個(76 個)で、どれも見積もりの中に収まった。テスト全体は 2 分前後で終わる(118〜142 秒。範囲を足したあとの二回は 126 秒と 136 秒、子の `.flow` を足したあとは 108〜138 秒、API の記述を足したあとは 175 秒、Open Responses を足したあとは 221 秒(ほかのプロセスで、マシンの負荷の平均が 20 を超えていた)、LocalStack を足したあと全部が通った回は 131 秒。例の版を足す前は 96〜128 秒だった。Argo だけで 76〜88 秒、Temporal の突き合わせは TypeScript が 24 秒、Python が 17 秒、言語をまたぐアクティビティが 15 秒、durable functions が 16 秒。kind のノードは、何も走らせていなくても CPU を 3 割ほど使っていて、Argo の時間はそれに左右される)。
- 同じ日の昼、Argo の突き合わせがときどき食い違った。呼び出しは参照と同じで、終わり方だけが違い、その回の Pod が init の失敗(Error)や `Unknown (exit code 255)` で終わっていた。kind のノードの中の containerd(2.1.1)が SEGV で落ちては立ち上がり直していて、落ちた時刻が食い違いと重なった。落ちたときに動いていたコンテナは、どれもこう終わる。生成したワークフローは、その Pod の失敗を `failure` として正しく扱っていた。そこでランナーは、プラットフォームが動かせなかった Pod のあるイテレーションを、名前を変えて二度まで走らせ直し、そのことを出力するようにした。いまはこれが本物の Pod の回にだけかかわる(次の項)。クラスタを kind 0.33.0 のノード(Kubernetes 1.37.0、containerd 2.3.4)で作り直してからは、containerd は一度も落ちていない。テスト全体を通したときも、ホテルの予約の 49 本すべてを本物の Pod で同時に走らせたとき(375 秒)も SEGV は無く、本物の Pod の 49 本は演じた回と一本残らず一致した。
- 2026-10-01、テスト全体の負荷の中で、本物の Pod の回がまた食い違うようになった(審査の 3 本目と jev の 6 本目。段階 A から E の初めまでの全体の回の、ほとんどで出て、単独で回すと通った)。参照は `busy` や `混雑` を返されたあとリトライするが、本物の Pod の回はリトライせずに `failure` へ進み、あとの呼び出しの記録が足りなかった。ランナーに `DANDORI_ARGO_DUMP`(本物の Pod の回が終わるたびに、ノードと Pod の状態、終了コード、終了メッセージ、ログを書く)を足して調べると、Pod は呼び出しを終えていた。メインのコンテナは終了コード 3、終了メッセージ `busy`(スクリプトどおりの宣言したエラー)で終わり、`error.json` にも `busy` が入っていた。それでも Pod は `Failed`、理由は `DeadlineExceeded`(`Pod was active on the node longer than the specified deadline`)で、Argo は、Pod にメッセージがあるとそれをノードのメッセージにする(v4.1.4 の `inferFailedReason`)。`retryStrategy` の式は、宣言したエラーの名前をノードのメッセージ(ふつうは `main: Error (exit code 3): busy`)から読むので、期限の文に替わったメッセージには当たらず、リトライしなかった。期限は審査と jev の Jev のタスクの `timeout 10 seconds`(Pod の `activeDeadlineSeconds`)で、kubelet が Pod を受け付けた時点から数える。全体の負荷の中では、期限の付いた Pod 16 個で、Pod を作ってからメインのコンテナが終わるまでが 3 秒から 38 秒かかり(中央値は 7.5 秒)、10 秒以上のものが 4 個あった。生成したワークフローの誤りではなく、`timeout` が Pod の起動を含むという決まりによる(4.4)。そこでランナーは、試すテンプレートの `activeDeadlineSeconds` に 120 秒を足す。シナリオは `timeout` まで走らせない(外している)ので、試すものは減らない。直したあと、全体を十二回(`examples` だけの二回を含む)走らせ、期限の付いた Pod が `DeadlineExceeded` になった回も、あとの呼び出しの記録が足りなかった回も無かった(Argo のほかの揺れで落ちた回も含めて、そのときの本物の Pod の回は、どれも呼び出しがそろっていた)。
- 同じ負荷の中で、ほかに揺れが四つあった。
  - Argo の API サーバーが `argo resume` を時間切れにした(`service.flow` のコールバックの応答。そのときの Workflow は 88 KB あった)。ランナーは、コールバックの応答の `node set` と `resume` を送り直す(`node set` は二度目に「もう設定されている」と言い、`resume` は待ちが終わっていれば通っているので、どちらも通ったかを見分ける。はじめは二つをまとめて送り直して、通った `node set` を二度送り、別の誤りにした)。
  - Temporal の dev server が、SDK の待つ 5 秒で立たなかった(段階 A から E で四回)。ランナーは、まだ何も始めていないので、立て直す(三回まで)。
  - キャンセルを届ける回(注文の 11 本目)が、キャンセルでなく `on failure` の `fail` で終わった(段階 C と E で一回ずつ)。再現できなかった(CPU に負荷をかけて単独で走らせても出なかった)。キャンセルは、アクティビティが始まったあと、その中からサーバーに頼むので、頼みがサーバーに記録される前にアクティビティのタイムアウト(5 秒)が来ると、実行はキャンセルでなくタイムアウトの失敗で終わり、見えたとおりの終わり方になる。`DANDORI_TEMPORAL_DUMP`(キャンセルを届ける実行の履歴を書き出す)で、アクティビティが始まってからキャンセルの頼みがサーバーに記録されるまでを、全体の八回(3 回目から 10 回目)で測ると、アクティビティの中から頼んだ 441 本の中央値は 0.12 秒で、1 秒を超えたものが 36 本、3 秒を超えたものが 14 本、4 秒を超えたものが 4 本、最大は 4.9 秒だった。5 秒を超えたものは無かったが、5 秒との差は 0.1 秒しかない。失敗した回の履歴は取れていないので、タイムアウトが先に来たというのは、見えた終わり方と、この尾の長さからの説明である。そこで、キャンセルを届ける回のあるフローだけ、タイムアウトを 30 秒にした。そのあとの全体八回(3 回目から 10 回目)では、キャンセルを届ける回は食い違わなかった。その途中の全体の 7 回目に、言語をまたぐ Temporal の、Python のワークフローで引当と発送の AWS 版の 7 本目が、子ワークフローを呼ぶところで `on failure` に進み、子の呼び出しが記録されなかった。そのフローにはキャンセルを届ける回が無く、タイムアウトは 5 秒のままだったので、同じ 5 秒のタイムアウトと見ている(履歴は取っていない)。そこで、アクティビティと子ワークフローのタイムアウトを、どのフローでも 20 秒にした(5 章。測った遅れの最大 8.3 秒を超える)。そのあとの全体三回(8 回目から 10 回目)では、子ワークフローを呼ぶ回も、食い違わなかった。
  - Argo の演じる回が、演じた Pod は `Succeeded` なのに、ノードが `Pending` のまま 10 分の制限まで止まった(`connect_rules.flow` の 5 本目と 8 本目、`service.flow` の 1 本目。この段階の全体の七回のうち二回)。コントローラーが Pod の終わりを見落としたとしか言えず、原因は確かめていない。ランナーは、最後に演じた Pod のノードが 90 秒たっても終わらなければ、その回を名前を変えて演じ直し(二回まで)、テストはそのことを `played again` と出力する。10 分の制限に着いたときは、止まった回のノードと Pod の状態をエラーに書く。演じ直しは、入れたあとの全体四回(7 回目から 10 回目)のうち三回で出た(1 本、3 本、5 本。残る一回は出なかった)。出たのは、`dd-end` の Pod(終わりを知らせる、失敗で終わる Pod)か、リトライのある呼び出しの最初の Pod だった。WorkflowTaskResult を作ってから Pod の終わりを書くまでに 0.5 秒おく案を試したが、止まった回は減らず、やめた。Argo のコントローラーは、全体の途中で、終了コード 0 で自分から終わって立ち上がり直すことがあった(この機械で 10:28 と 14:43 の二回。止まった回との関係は確かめていない)。負荷でリーダーの選出を失ったためと見ているが、これも確かめていない。
  - 終わりのクエリが、案件の状態を一つ前のものや null で答える揺れが、全体の四回のうち一回出た(言語をまたぐ Temporal の、Python のワークフローで注文の AWS 版の 1 本目)。これも、負荷をかけて単独で走らせても出ず、原因は確かめていない。終わりの案件の状態は、クエリ(ワークフローの変数から答える)と search attribute(案件が動くたびにサーバーが持つ)が同じことを言うはずなので、違ったときは、クエリを三回まで尋ね直し、前に言ったことを出力する(`asked again`)。入れたあとの全体六回では、尋ね直した回は無かった。
- ジェネレーターにわざと誤りを入れると(ASL の冪等キーの区切りを一文字変える、`match` の条件から値を一つ落とす、durable functions の冪等キーの呼び出しの場所をずらす)、どれも一本目のシナリオで食い違いとして出た(2026-09-25)。Argo のジェネレーターでも、冪等キーの区切りを一文字変える、`match` の条件から値を一つ落とすと、どちらもホテルの予約の一本目で食い違いとして出た。Temporal の Python 版と pydantic-graph では、冪等キーの区切りを一文字変えると注文の引当と発送の四本目で、`match` の条件から値を一つ落とすとホテルの予約の一本目で出た(2026-09-26)。エージェントでも、ASL で応答を読まない呼び出しの `Output` を落とすと agents の二本目で、ASL で入力の引数の順を逆にすると問い合わせの一本目で、TypeScript で応答から `answer` を取り出さないと Temporal の問い合わせの一本目で、Python で別のタスクの Schema を渡すと pydantic-graph の問い合わせの一本目で、既定の `Transport` で Agents SDK に空の設定を渡さないと Agents SDK の確かめの一件目で、それぞれ食い違いとして出た(同日)。Claude のエージェントでは、応答の列挙の値を直す式を ASL から落とすと agents の七本目で、TypeScript の `io.fold` を呼ばないと Temporal の agents の七本目で、Python の `io.fold` で大文字と小文字を区別すると Temporal の Python 版の agents の七本目で、既定の `Transport` が送る `max_tokens` を一つ変えると確かめの四件目で、OpenAI のクライアントのリトライを切らないと確かめの三件目で、それぞれ食い違いとして出た(同日)。
- 範囲(1.3):シナリオには、範囲の外の数を持つ結果も入る(ホテルの予約では、与信額の規則が 11,999 円と返す 4 本目)。どのプラットフォームも、それを `Dandori.BadResponse` で拒否し、参照インタプリタと一致した。ジェネレーターの範囲の比べをわざと落とすと、JSONata では、その結果を通して次の呼び出し(結果の用意されていない `create_intent`)に進み、TypeScript(Temporal)、Python(Temporal と pydantic-graph)、Argo の式では、終わり方の食い違いとして、どれもホテルの予約の 4 本目で出た(2026-09-26)。エージェントの応答の Schema の範囲(OpenAI の `minimum` と `maximum`、Claude の説明)は、既定の `Transport` の確かめで、Agents SDK からも Anthropic の SDK からも、Step Functions と同じ形で送られた。
- Open Responses(1.7):既定の `Transport` の確かめでは、TypeScript と Python のどちらでも、Open Responses のエージェントの 24 件(問い合わせの Temporal 版の 11 件と、agents の 13 件)で、ローカルのモックのサーバーが受け取ったリクエストが Step Functions の本文と同じで、一回だけだった。拒否は `AgentStopped`、500 は `AgentHttpError` になった。ローカルの Ollama 0.32.13(Llama 3.1 Swallow 8B Instruct の 4 ビット、4.9 GB)に本物の呼び出しを送ると、二つのエージェント(問い合わせを読む、領収書を読む)の応答が、TypeScript と Python のどちらからも型に合った。領収書の結果には、時刻の正規表現、金額の範囲、列挙、null のことがある値が入る。`Transport` が送る指示に空白を一つ足すと(TypeScript)、結果を読むところをずらすと(Python)、どちらも agents の 8 件目で食い違いとして出た(2026-09-27)。
- LocalStack(4.14.0)の Step Functions では、185 本すべてが参照インタプリタと一致した(2026-09-27)。HTTP Task を持つ八つのフローの 26 の HTTP Task は、resource を替えて流した。テストは単独で 55 秒前後かかり、そのほとんどは Retry の本当の待ちである(いちばん長い実行は、問い合わせの 45 秒)。測った Retry の待ちは、単独で流すと参照より最大 0.37 秒長かった。テスト全体の中では、1 秒のはずが 2.04 秒、10 秒のはずが 11 秒になるなど、1 秒ほど長いものがあったので、待ちだけが長かった実行は流し直すことにした(5 章)。最後にテスト全体を流したとき(131 秒、メモリの空きは最も少なくて 28%)は、185 本のうち 11 本を流し直し、どれも一致した。幅の中で最も長かった待ちは、参照より 2.055 秒長かった(長い待ちでは、幅は待ちの四分の一になる)。はじめのころは、Open Responses のテストが Ollama に読み込ませたモデル(8.4 GB)が、使い終わってからも 5 分残ってほかのテストと重なり、メモリが逼迫していた。同じころ、Temporal と Argo の突き合わせも落ちた(Temporal ではキャンセルで終わるはずの一本が失敗で終わり、言語をまたぐ突き合わせでは dev server が 5 秒で立たず、Argo では一本が 10 分を超え、別のときは kind の etcd が時間切れになった)。Ollama のテストは、使い終わったモデルを下ろすようにした(5 章)。
  - 作る途中で、ジェネレーターの誤りが一つ見つかった。出力を持たないフローの ASL は、終わりの Succeed に `Output` が無く、最後の呼び出しの結果(local_rules では Lambda の `{"Payload": null, "StatusCode": 200}`)が実行の出力になっていた。参照インタプリタとほかのプラットフォームは null で終わる。`tools/asl-run.mjs` は、一番外の `Output` の無い Succeed を null で終えていたので、誤りが隠れていた。どちらも直した(4.1。ランナーは Step Functions と同じく入力を出す)。直したランナーは、直す前のジェネレーターを local_rules の一本目で食い違いとして捕まえる。
  - LocalStack 4.14.0 について分かったこと(どれも 5 章の扱いにした)。HTTP Task を知らない(定義が `Unknown service 'http'` で拒否される)。HTTP Task のエラーの名前(`States.Http.StatusCode.409`)も知らず、Retry と Catch に書くと定義が拒否される。モックの設定ファイルから `States.Timeout` を投げると、実行が `States.Runtime`(`Custom Error Names MUST NOT begin with the prefix 'States.'`)で終わる。API Gateway の統合は、モックのエラーを `ApiGateway.FailureEventException`(Cause は空)にする。JSONata の式の評価の失敗は、`$error` でも `1 + 'a'` の型のエラーでも `States.Runtime`(Cause は `JSONataException(('UNKNOWN', …))`)になる。Step Functions の文書は、型のエラーを `States.QueryEvaluationError` になる例に挙げている。
  - ジェネレーターにわざと誤りを入れると、LocalStack の突き合わせでは、冪等キーの区切りを一文字変えるとホテルの予約の一本目で、`match` の条件から値を一つ落とすと同じ一本目(`PaymentCanceled` で終わるはずが `Dandori.UnexpectedValue`)で、Task に書く Retry の BackoffRate を 2 から 3 にすると、待ちだけが長い七本を流し直したあと二本目(規則の呼び出しの二回目の待ちが、2 秒のはずが 3 秒)で、上の `Output` を戻すと local_rules の一本目で、どれも食い違いとして出た(2026-09-27)。
- API の記述(1.10):ホテルの予約の三つの版は、Stripe の OpenAPI の記述(spec3.json、API のバージョン 2026-09-30.endive)の PaymentIntent の五つの操作に、引当と発送は SNS と SQS の Smithy のモデルと倉庫の `.proto` に合い、`tests/fixtures/apis.flow` の食い違い(受け取らない引数、要る引数の不足、受け取らない列挙の値、返しうる値を持たない列挙、整数を `timestamp` で読む、null のことがあるフィールド、無い操作、無い例外、冪等トークンでない `key`、64 ビットの整数と小数、JSON の名前の違い、ストリームのメソッド)はどれも E016 になった。ジェネレーターから既定値の埋め方をわざと落とすと、TypeScript(Temporal)、Python(Temporal)、JSONata のどれでも、connect の 6 本目(既定値を省いたレスポンス)で食い違いとして出た(2026-09-26)。JSONata の埋め方は、はじめレスポンスの null の項目を消していて、ほかの言語(null を残し、既定値のあるものだけ埋める)と違ったので、そろえた。
- 子の `.flow`(1.9):`tests/children` の受付と査定は、TypeScript どうし、Python どうし、TypeScript の親と Python の子、その逆の四通りのどれでも、三本(個人・法人・団体の申込)すべてが参照インタプリタと一致した。法人の申込では、子の `fail 要確認` が、親の宣言したエラーとして `on 要確認` に届き、団体の申込では、親が宣言していない `対象外` が `failure` として届いた。親が子ワークフローの失敗から子の `fail` の名前を読むところを、TypeScript と Python で一つずつ落とすと、どちらも同じ言語どうしの二本目(法人)で食い違いとして出た(2026-09-26)。子の `arrange_delivery.flow` を例に足したあと、Argo の突き合わせが一度、子の本物の Pod の回で `failed to resolve {{workflow.labels.dandori-parent}}` と食い違った。Argo のテストはすべてのフローを一つのクラスタで同時に走らせる。引当と発送のランナーが子のスタブとして置く WorkflowTemplate と、子のフロー自身の WorkflowTemplate が同じ名前(`arrange-delivery`)で、先に置いたほうを上書きしていた。ランナーは子のスタブに自分の名前を付け、親のテンプレートの参照もそれに書き換えるようにした(生成したテンプレートは変えていない)。
- エフォート(1.7):`tests/flows/agents.flow` の三つのエージェント(OpenAI・Open Responses・Claude)と、問い合わせの三つの版の二つのエージェントにエフォートを書いた。どのプラットフォームも Step Functions と同じリクエストを送り、既定の `Transport` の確かめでも、TypeScript と Python の両方で、Agents SDK に渡す設定がエフォートだけになり、Messages API と Open Responses のモックが受け取った本文が Step Functions の本文と同じだった。TypeScript の既定の `Transport` が Agents SDK にエフォートを渡さないようにすると、agents の一件目で食い違いとして出た(2026-09-28)。
- 文書の確かめ(5 章):日本語のページの診断の一行を書き換える、英語の診断コードの一覧のコードを一つ別のものにする、ハイライトの `KEYWORDS` から一語を落とす、ページの `.flow` の一行の値を変える、README に書いたコードの数を変える、のどれでも `tests/docs.rs` が落ちた(2026-09-27)。
- `on cancel` を持つ `tests/flows/cancel.flow` の 32 本(うち 13 本でキャンセルが届く)は、Temporal の TypeScript と Python の dev server の上で、すべて参照インタプリタと一致した(2026-09-26)。ジェネレーターにわざと誤りを入れると、`dd.attempt` がキャンセルをタスクのエラーに直すと九本目で、`on cancel` を `CancellationScope.nonCancellable` の外で走らせると十一本目で、Python の `dd.attempt` がキャンセルをタスクのエラーに直すと九本目で、`dd.rounds` がキャンセルより前のイテレーションの失敗を先に投げると 23 本目で、食い違いとして出た。最後の誤りは、はじめ捕まらなかった。イテレーションが失敗したあとに別のイテレーションでキャンセルが届く実行がシナリオに無かったので、それを別のところとして数えるようにした。
- `event` の生成にわざと誤りを入れると、validator が待っていないイベントも受け取るようにした TypeScript と Python は、ランナーが送ったほかのイベントを受け取ったことで止まった。Python でイベントの待ちのタイムアウトを結果なしとして通すと events の一本目で、TypeScript でイベントが送ってきたエラーを値として読むと events の二本目で、食い違いとして出た(2026-09-26)。ランナーを作る途中では、イベントの待ちをコピーで 10 ms に縮めていてランナーが送る前にタイムアウトしたこと、同じ名前のイベントを次のイテレーションですぐに待つので「待ちが終わるまで待つ」ランナーが止まったこと、送ったイベントの結果を書き留める前に次のアクティビティがその結果を取ったことを、順に直した。
- Continue-As-New の生成にわざと誤りを入れると、TypeScript と Python のどちらでも、続きの実行がイテレーションを 0 から数え直すと倉庫の出荷の 15 本目で、ループより前の文を飛ばさないと同じ 15 本目で、変数を受け取らないと一本目で、クエリが返す案件の状態が参照と違うこととして出た。`yield` した値を渡さないと、edges の 10 本目で呼び出しの食い違いとして出た。続けるイテレーションを一つずらすと、残した倉庫の出荷の履歴の二つ目の実行が、再生で非決定的と言われた(2026-09-26)。
- Temporal の突き合わせは、生成した `worker` と `client` を通しても、TypeScript と Python の両方で 400 本すべてが一致し、どの実行でも、終わりのクエリ `dandori.status` と search attribute `DandoriCases` が参照インタプリタの案件の状態と同じだった。コールバックへの二度目の応答と、待っていない ID への応答は、Update がすべて拒否した(2026-09-26)。
- 既定の `Transport` では、シナリオの呼び出しのうち 456 件(HTTP が 408 件で、うち 146 件はエラーのステータス、Connect の呼び出しは 50 件でうち 17 件がエラー、Lambda が 45 件で、うち 18 件は関数のエラー、SNS と SQS が 3 件。例の三通りの版のものを含む)を、TypeScript と Python の両方からローカルのモックに送り、すべて呼び出しのとおりに届き、差し替えの `Transport` と同じものが返った(2026-09-26)。はじめ、Python の既定の実装が三か所で食い違った。
  - AWS のエラーを、線の上の符号(`NotFound`、`AWS.SimpleQueueService.NonExistentQueue`)で返していた。TypeScript の SDK と、タスクが宣言する名前は、API のモデルの名前(`NotFoundException`、`QueueDoesNotExist`)である。boto3 が投げる例外のクラスの名前がモデルの名前なので、それを返すようにした。
  - URL のパスに ASCII でない文字があると(例を英語にする前の倉庫の出荷の `/v1/orders/注文ID-1`。いまは `tests/flows/timeouts.flow` の `/v1/items/品番-2/check` で確かめる)、urllib が `UnicodeEncodeError` で送れなかった。fetch と同じく、UTF-8 でパーセントエンコードしてから送るようにした。
  - JSON の本文(HTTP の本文、Lambda の payload、SQS のメッセージ)を、`json.dumps` の既定のまま、空白を入れ、日本語を `\u` でエスケープして書いていた。値は同じだが、TypeScript と Step Functions の書き方(空白なし、文字はそのまま)にそろえた。
- 言語をまたぐアクティビティでは、TypeScript のワークフローと Python のアクティビティ、Python のワークフローと TypeScript のアクティビティの両方で、`local` の規則や `event` を持つフローを除く 252 本(例の Temporal 版の引当と発送と審査を含む)すべてが参照インタプリタと一致した(コールバックの応答、キャンセル、タイムアウト、エージェントの呼び出しも、もう一方の言語を通った)。Python のアクティビティの名前に一字足すと、TypeScript のワークフローの側だけが食い違いとして出た(2026-09-26)。
- Temporal の再生は、TypeScript と Python の両方で、400 本すべて(続きの実行も)が同じコードで再生でき、`tests/histories` の十六の履歴も再生できた。Worker Deployment Versioning では、A で始まった実行が B を current にしたあとも、新しい実行で始まった二回目のイテレーションまで A のコードで終わり、B で始めた実行は B のコードで走った(TypeScript と Python。ビルド ID は、TypeScript が `dandori-c98507328717f2f4` と `dandori-5fe7c15ba38debcf`)。生成するワーカーの既定の振る舞いを PINNED から AUTO_UPGRADE にすると、どちらの言語でも A の実行が B のコードで終わり、食い違いとして出た(2026-09-26)。
- TypeScript の実行の履歴を Python のコードで再生すると、cancel の 32 本のうち 27 本が再生でき、Python の履歴を TypeScript で再生すると 31 本が再生できた。再生できなかったのは、キャンセルやタイムアウトやコールバックの待ちを通る実行で、タイマーとアクティビティの順か番号が二つの言語で違っていた。同じ言語の中では決定的なので、どちらかの言語のワーカーだけで一つのワークフローを持つかぎり問題は無い。二つの言語のワーカーを同じキューに混ぜると、こうした実行で止まる。
- 生成した TypeScript を `tsc --strict` に通すと、走らせても出なかった誤りが三つ見つかった。出力を持つフローの `run` の終わりの `return null`(出力の型に合わない。検査がそこへ来ないと言うので、失敗を投げるようにした)、値を入れる前は null で宣言している変数の読み(`!` を付けた)、rulec の TypeScript の単位の付いた数(ブランドの付いた bigint で、`BigInt(…)` をそのまま渡せない。rulec 自身のランナーと同じく `as` で型を付けた)。直したあとは、25 のビルドすべてが通る。
- 並列のイテレーションの結果の並びを逆にする誤りは、はじめ、どのプラットフォームの突き合わせにも出なかった。シナリオの結果の値がどの呼び出しでも同じで、二回以上回ってその先まで行く実行も残っていなかったからである。値を一つずつ変え、二回以上回ったループを別に数えるようにすると、edges の十二本目で出た。この変更で、TypeScript の Temporal のランナーが、コールバックの応答のシグナルを、テスト環境が待ちの時間を飛ばしたあとに送ることがあると分かった。ジェネレーターではなくランナーの競争だったので、ランナーを直した(5 章)。
- Argo の突き合わせは、はじめ全部の実行を本物の Pod で走らせていて、テスト全体で 30 分余りかかった。次のように縮めた。
  - コントローラーは既定では、変化のあと 10 秒おいてワークフローを見直す(`DEFAULT_REQUEUE_TIME`。Argo 自身のテストは 1 秒にしている)。これを 1 秒にした。
  - それでも kind の上では、Pod を一つ立てて終わるまでに約 4 秒かかる。呼び出しが 29 回続くホテルの予約の一本は、単独でも 3 分余りかかった。そこで Pod をランナーが演じる形にした(5 章)。この一本は 48 秒になった。
  - 150 本を同時に流すと、まだ 2 分を超えた。API サーバーへのリクエストを数えると、ホテルの予約・倉庫の出荷・問い合わせの三つ(97 本)を流したあいだに、WorkflowTemplate の GET が 9,181 回あった(5 章の、最初の検証での読み直し)。演じる回の Workflow にテンプレートの中身を持たせると、七つのフローを流しても 500 回(本物の Pod の回の分)になった。
  - ほかに、Argo の Kubernetes のイベントを止め、Pod を作る速さと API を呼ぶ回数の上限を上げた(`setup.sh`)。
  - テスト全体が 85 秒になった(kind 0.33.0 のノードで作り直したあとは 71 秒)。コントローラーの待ちを 300 ms にすると、かえって遅くなった(ホテルの予約の一本で 48 秒が 57 秒)。
- 演じる形でも、ジェネレーターにわざと入れた誤りは捕まる。`match` の条件から値を一つ落とす誤りと、caller が宣言したエラーで終わるときの終了コードを 3 から 1 に変える誤りは、どちらもホテルの予約の九本目で食い違いとして出た(2026-09-26)。
- つなぐコードは、与信額の規則(`hold_amount`)のベクタ 24 件、急ぎの規則(`urgency`)のベクタ 16 件、振り分けの規則(`inquiry_routing`)のベクタ 8 件で、rulec の期待値と一致した。Python 版の `rules.py` も同じベクタで一致した。
- 既定の `Transport` のエージェントは、問い合わせの 16 件と agents の 44 件のどれでも、TypeScript と Python の両方で、Step Functions と同じものをモデルに渡した(Agents SDK は TypeScript 0.18.0 と Python 0.22.3、Anthropic の SDK は TypeScript 0.128.0 と Python 1.8.0)。Claude の件では、ローカルの Messages API のモックが受け取った本文が Step Functions の本文と同じだった。500 を返すと、どちらのプロバイダーも一回だけ送って失敗した。空の設定を渡さないと、どちらの Agents SDK も gpt-5.4-mini に `reasoning.effort: none` と `text.verbosity: low` を足す(走らせて確かめた)。OpenAI のクライアントのリトライを切らないと、500 に三回送った。
- 検査が例を書く途中で見つけたこと。チェックアウトまで待つあいだに与信の有効期限が切れると capture が拒否される。capture のあとは `processing` を経て `requires_payment_method` に戻ることがある。`requires_action` を見てから cancel するまでに、客が認証を済ませて有効期限が切れると、cancel も拒否される。入金がないので注文の取消を頼むあいだに、客が自分で取り消していると、倉庫はその取消依頼を拒否する。
- 記述から型を作る(1.12。2026-10-01):`tests/flows/proto_types.flow` の 26 本は、Step Functions(JSONata)、LocalStack、Temporal の TypeScript と Python、durable functions、pydantic-graph、Argo(三本目は本物の Pod でも)のすべてで参照インタプリタと一致した。Argo の一回の実行のノードは、多くて 158 個(E040 の見積もりは 315 個)。引当と発送の六つの版は、手で書いた `Stock` と `Reservation` を `warehouse.Stock` と `warehouse.ReserveResponse` に替えても、シナリオの本数(Temporal 版は 27 本、AWS 版は 20 本)も、記録した履歴(引当と発送のものを含む 30 本)の再生も、Argo のノードの数(229 個)も変わらなかった。`tests/flows/connect.flow` も、`場所` と `記録` を `棚.Location` と `棚.RecordResponse` に替えて、7 本のままだった。作る表と突き合わせる表が同じであることには、わざと誤りを入れて確かめた。64 ビットの整数を `int` に作ると、`required` を読まずに `T?` にすると、どちらも `made_types_fit_their_descriptions` が落ちた。
  - 作る途中で、ASL のジェネレーターの誤りが一つ見つかった。`match` の `none` の分岐は、案件の変数で分けるとき、どのフィールドで分けても `$案件 = null`(案件が始まっていない)で判定していた。案件の状態で分けるときはそれでよいが、案件のオプショナルなフィールド(`tests/flows/proto_types.flow` の `受注.note`)で分けると、値が無い実行が `Dandori.UnexpectedValue` になった。ほかのプラットフォームと参照インタプリタは値そのものが null かを見るので、一本目で食い違った。案件の状態で分けるときだけ、案件の null で判定するように直し、直す前のジェネレーターは、このフローの一本目で食い違いとして捕まる。
  - pydantic-graph の突き合わせ(`tests/examples.rs`)は、フローが `use rule` した規則を全部グラフの `Deps` に渡していたが、ジェネレーターは、呼ぶ規則が一つも無ければ `Deps` に規則を持たせない。`order_state` を案件のステートマシンにだけ使うこのフローで、`Deps.__init__() got an unexpected keyword argument 'rules'` と落ちたので、呼ぶ規則だけを渡すようにした。
  - 自分を含むメッセージを型にするのは、名前を先に入れるので止まる。しかし、できた型でシナリオを作ると `dandori scenarios` がスタックを使い切った(手で書いた `record Node` に `next : Node?` を持たせても同じで、`build --target argo` もそうなった)。そこで E003 にした(1.12)。
- 規則を Connect のサービスとして呼ぶ(1.13。2026-10-01):`tests/flows/connect_rules.flow` の 22 本と、注文の AWS 版(英語と日本語)の 39 本ずつは、Step Functions(JSONata)、LocalStack、Temporal の TypeScript と Python、durable functions、pydantic-graph、Argo のすべてで参照インタプリタと一致した。日本語の AWS 版は決まりで Step Functions、LocalStack、durable functions だけが走らせ、durable functions はタイムアウトを含む 1 本を除く 38 本を走らせた。Argo は、`connect_rules.flow` の二本目と注文の一本目を、本物の Pod でも走らせた。一回の実行のノードは、`connect_rules.flow` で多くて 178 個(E040 の見積もりは 365 個)、注文の AWS 版で 239 個(446 個)で、見積もりを超えなかった。LocalStack は HTTP Task を知らないので、`connect_rules.flow` の三つ(注文は五つ)の HTTP Task は resource を替えて流した。Retry の待ちは、参照より最大 0.34 秒(注文は 0.69 秒)長かった。
  - 既定の `Transport` の確かめは、いまは 1,044 件(HTTP が 927 件で 327 件が失敗、規則のサービスが 35 件で 6 件が失敗、Lambda が 76 件で 30 件が失敗、AWS が 6 件で 2 件が失敗。例の三通りの版を含む)で、TypeScript と Python の両方からローカルのモックに、すべて呼び出しのとおりに届いた。
  - 本物のサービス:`rulec gen` が書いた Python のサービスを、例の規則 11 本と、列挙を `.proto` から取り込む規則一本(`tests/fixtures/rules/delivery_fee.rule`)のそれぞれで立て、`rulec vectors` の 334 件を、dandori が組み立てる本文で送った。dandori がレスポンスから読んだ結果は 334 件すべてが rulec の期待値と同じで、範囲の外の入力 8 件は 400 の `invalid_argument` で受け付けられず、成功のレスポンスのヘッダ `rulec-source-sha256` は `rulec api` の `source_sha256` と同じだった(3 秒)。`rulec gen` の `.proto` との名前の突き合わせも、12 本すべてで一致した。どちらも rulec 0.21.2 で走らせ、`tests/protos.rs` の 9 本は 0.20.0 と 0.21.1 でも通った。
  - レスポンスの読み方:サービスのレスポンスを、参照インタプリタ、生成する TypeScript と Python、JSONata の四か所で読み比べるテストは、`connect_rules.flow` の三つの規則について、サービスが書くレスポンスと、フィールドが無い、null、種類が違う、十進でない数(`007`、`+5`、`1e3`、`5` の前後の改行、全角の数字)、桁数と 2^53 − 1 の境目の数、どの列挙にも無い名前を入れた 135 件を比べて、四つが同じレコードを返した。作る途中で、JSONata の誤りが一つ見つかった。列挙の表を `$lookup` で引くと、`constructor`、`toString`、`hasOwnProperty` はオブジェクトの関数を、`__proto__` は空のオブジェクトを値にしてしまい、フィールドが消えるか別の値になった(三つの読み方のうち JSONata だけが、12 件で食い違った)。表が自分のキーとして持つ名前だけを引くようにして直した(1.13)。
  - `rulec api` と `rulec gen` は、`import proto` の取り込み先を別のところから探す。`gen` は規則のファイルの隣から探して、型を `shop.v1.MemberTier` と書く。`api` は走らせたディレクトリから探し、見つからないと、package の付かない `MemberTier` と書く(0.20.0、0.21.1、0.21.2 で同じ)。dandori は型の最後の名前だけを使うので、どちらでも同じ値の名前を作る。両方の形を `src/rulec.rs` の単体テストで確かめる。(同じ日の夜:rulec 0.22.0 の `rulec api` は、どこで走らせても規則のファイルの隣から探し、`shop.v1.MemberTier` と書く。dandori は値の名前を `connect.enums` から読むようになり、型の最後の名前を使うのは、名前を言わない rulec で取り込んだ列挙を見分けるときだけになった。)
  - ジェネレーターにわざと誤りを入れると、`connect_rules.flow` で次のように出た。ASL では、数を `$string` に通さない、送る列挙を表に通さない、読む列挙を逆の表で引く、が一本目で、ゼロ値を埋めないのが 13 本目で、HTTP Task の Retry を外すのが 2 本目で出た。TypeScript では、数を文字列にしない、`Connect-Protocol-Version` を外すのが一本目、数を読み戻さないのが 2 本目、ゼロ値を埋めないのが 13 本目、Python では、数を文字列にしない、ヘッダを外すのが一本目、ゼロ値を埋めないのが 13 本目だった。リトライの回数を一回にすると、TypeScript と durable functions、Python と pydantic-graph、Argo のどれも 2 本目で出た。pydantic-graph が引数を送らないのは一本目で出た。参照インタプリタがレスポンスを読み戻さないのは一本目、ゼロ値を埋めないのは 13 本目で出た。桁数の境目は、読み比べるテストが出した(TypeScript の 17 桁と Python の 2^53 が一件ずつ、参照の 17 桁が三件)。
  - 出なかった誤りが三つあり、うち二つは直した。一つ、HTTP Task の接続(`InvocationConfig`)を外しても、どのランナーも出さなかった(asl-validator も、ランナーも、接続を見ない)。そこで、Step Functions のランナーのテストが、すべての HTTP Task が接続を持ち、その集まりがフローの指定した接続と同じことを確かめるようにした。二つ、列挙の値の名前を、型の最後の名前でなく型の全体から作る誤りは、例の規則の列挙が package を持たないので、出なかった。`import proto` で列挙を取り込む規則を足し、単体テストを足した。三つ、リトライの回数の定数を変えても出なかった。参照インタプリタも生成物も、同じ関数(`asl::rule_retriers`)から取るからで、生成物の側の変換を変えると出る。これは直さない。ほかに、参照インタプリタが null の引数も送る誤りと、durable functions の呼び出しに余計な引数を足す誤りも出なかった。規則の入力は `T?` にならないので前者は起きず、後者は、送るのが規則のフィールドだけなので送られない。
  - 規則のサービスの確かめで、Python のサービスは、`import proto` の契約の列挙を、その契約のモジュール(`order_pb`)から読む。buf が書くスタブはその名前では import できないので、スタブのディレクトリを `PYTHONPATH` に入れてサービスを立てる。(同じ日の夜:rulec 0.22.0 のサービスは、契約のスタブを package のモジュール(`stubs.shop.v1`)から読み、`rulec gen` が契約を `proto/shop/v1/order.proto` にコピーするので、`PYTHONPATH` も、テストが契約をコピーすることも外した。コピーしたままだと、buf が `symbol "shop.v1.MemberTier" already defined` で止まる。)
  - 検査の二つの変更(1.2、1.10):手で書いたレコードが自分を含む(直接、リストや `T?` を通して、ほかのレコードを通して)のは E003 になり、`tests/fixtures/self_holding.flow` が 4 件の E003 を出す。`tests/fixtures/agents.flow` の golden は、`乙 : 乙?` を持つ `甲` と `乙` の E003 が一件(26 行 8 桁)増えただけで、ほかは変わらず、E007 の確かめも残った。`.proto` が import して読めなかったファイルがあると、そこにある型を書いたとき、E002 と E016 に「そのファイルを読めなかったので、そこにある型は使えません」と添える(`tests/fixtures/proto_unread.flow` の 5 件)。
- サービスを実装する(1.14。2026-10-01):`tests/flows/service.flow` の 18 本は、Step Functions(JSONata)、LocalStack、Temporal の TypeScript と Python(アクティビティをもう一方の言語にしても)、durable functions(タイムアウトを含む 3 本を除く 15 本)、pydantic-graph、Argo(二本目は本物の Pod でも)のすべてで参照インタプリタと一致した。このフローは出力を持たず、どの終わり方でも `{}` を返す。ゼロ値を省いた入力(入れ子のメッセージ、メッセージのリスト、その中のリスト)の 3 本と、ゼロ値を省いたコールバックの応答の 2 本と、宛先(メッセージ)の無い入力の 1 本を通り、最後の一本は、どのプラットフォームでも何も呼ばずに `Dandori.BadInput` で終わる。引当と発送の六つの版は、サービスを実装して、シナリオが Temporal 版で 27 本から 29 本、AWS 版と pydantic-graph 版で 20 本から 23 本になり(ゼロ値を省いた入力、梱包の応答のゼロ値、注文の無い入力)、どのプラットフォームでも一致した。`tests/flows/events.flow` は承認のサービス(`窓口.ApprovalService`)を実装して 12 本から 14 本になり(ゼロ値を省いた入力と承認のイベントの値)、Temporal の TypeScript と Python で一致した。Argo の一回の実行のノードは、`service.flow` で多くて 156 個(E040 の見積もりは 431 個)、引当と発送の AWS 版で 247 個(4,746 個)。サービスを実装しないフロー 39 本を、この機能を入れる前の dandori と比べると、六つのプラットフォームの生成物で違うのは、下の二つの直し(ステートマシンの始まりの Pass と、Temporal の最初のクエリ)と、コードから計算する Worker のビルド ID だけで、シナリオ、`doc` の Markdown と HTML、診断は一字も変わらなかった。
  - protobuf で読む:`tests/protos.rs` は、五つのサービスの `.proto` を protoc で記述子にし(dandori のオプションは `proto/` のものを使う)、protobuf の Python の `json_format`(知らないフィールドを拒否する)に、シナリオの入力、参照インタプリタの出力、イベントの値とコールバックの応答、状態のクエリが返したものの 252 件を読ませた。すべて読め、protobuf が書き直した入力 182 件は、埋めるとシナリオの入力に戻った。日本語の `json_name` も、そのまま読めた。`options.proto` は buf の標準の lint を通り、buf で組み立てられた。
  - 作る途中で、ジェネレーターの誤りが二つ見つかった。どちらも宛先の無い入力のシナリオで出たもので、サービスに限らない。一つ、ステートマシンの始まりの Pass は、入力を `$states.input.<名前>` で変数に入れていたので、実行の入力にそのフィールドが無いと式が何も返さず、実行は `Dandori.BadInput` でなく `States.QueryEvaluationError` で終わった(JSONata のランナーでも LocalStack でも同じ)。`T?` の入力が実行の入力に無いときも、同じように失敗していた。無ければ null を入れるようにした(4.1)。二つ、TypeScript のワークフローは、入力を確かめたあとでクエリ `dandori.status` を登録していたので、入力で失敗した実行に聞くと `QueryNotRegisteredError` になった(Python は最初から答えていた)。ワークフローの関数の最初で登録するようにした(4.2)。
  - 二つめの誤りは、はじめ、生成した TypeScript から入力の埋めをわざと外したときに出た。Temporal のランナーがクエリの失敗で止まり、ほかの実行を待つワーカーが終わらないので、テストは 10 分を超えても終わらなかった。二つのランナーは、クエリの失敗を案件の状態の食い違いとして書き、ほかの実行を終わりまで流すようにした。同じ誤りは、いまは `service.flow` の二本目で、12 秒で出る。
  - ジェネレーターにわざと誤りを入れると、`service.flow` で次のように出た。入力のゼロ値を埋めないのは、TypeScript(Temporal と durable functions)、Python、pydantic-graph、Argo のどれでも二本目で(Step Functions も二本目)、無い入力をステートマシンが null にしないのは 18 本目で、コールバックの応答のゼロ値を埋めないのは六つのプラットフォームのどれでも 7 本目で、出力の無い終わりを `{}` でなく null にするのはどれでも一本目で出た。TypeScript がクエリを入力のあとで登録するのは、Temporal の一本目で、クエリの失敗として出た。
  - 食い違いを一つ直した(1.2):`json` の入力が実行の入力に無いとき、参照インタプリタと Argo は null として読み、TypeScript、Python、JSONata は拒否していた。レコードの `json` のフィールドが無いときは、参照インタプリタも六つのプラットフォームも拒否していた。protobuf の JSON は値が設定されていない `google.protobuf.Value` のフィールドを省くので、`json` になるフィールドは無いことがあり、正しいメッセージが `Dandori.BadInput` や `Dandori.BadResponse` になっていた。シナリオはいつも入力を全部書いていたので、出ていなかった。いまは、参照インタプリタ(`value_fits`)、JSONata(検査と読み)、TypeScript と Python(検査と読み)、Argo(検査)が、無い `json` を null として読む。TypeScript では、無い `T?` の入力も `?? null` で null にそろえた。静的な突き合わせ(`apis.rs` の `reads` と `sends`)は、レスポンスが省くことのあるフィールドを `T?` か `json` で受けるので、もとからこの決まりと同じで、変えていない。
  - シナリオを二つ足した(5 章)。`json` を入力か入力のレコードのフィールドに持つフローに、無くてよいものを全部省いた入力の実行を一本(`service.flow` は 19 本目、edges は 13 本目、子の `arrange_delivery` は 10 本目、引当と発送は Temporal 版が 30 本、AWS 版と pydantic-graph 版が 24 本)。`connect` のタスクには、`json` のフィールドを省いたレスポンス(`connect.flow` の 7 本目。`google.protobuf.Value` の `extra` を次の呼び出しの引数に渡す)。`service.flow` には `付帯 : json` の入力(`ShipRequest` の `Value`)を足した。どれも、全プラットフォームが参照インタプリタと同じ呼び出し(`json` の引数は `null`)で終わった。
  - わざと埋めを外すと、次のとおり出た。無いレコードのフィールドを拒否する誤りは、参照インタプリタ、JSONata、TypeScript、durable functions、Python、Argo のどれでも `connect.flow` の 7 本目(JSONata では `Dandori.BadResponse`。pydantic-graph は Python と同じ検査を使う)。無い入力を拒否する誤りは、参照インタプリタ、JSONata、TypeScript、Python で `service.flow` の 19 本目(durable functions はタイムアウトの 3 本を外すので 16 本目)。無いフィールドを null として読まない誤りは、JSONata(`States.QueryEvaluationError`)、TypeScript、pydantic-graph で `connect.flow` の 7 本目、TypeScript の入力を `?? null` で読まない誤りは `service.flow` の 19 本目。Python のフィールドの読みを `.get` から `[]` に戻すと、ワークフローの中で `KeyError` になる。Python の SDK は、それをワークフロータスクの失敗として終わりなく試し直すので、ランナーが終わらず、止めた。
- proto を契約にする三つの機能(1.12〜1.14)の全体(2026-10-01、最後のテスト全体。ここまでの項と重ならないところだけ):`cargo test --no-fail-fast` の全体は 4 分 49 秒で、lib 22・doc 5・docs 6・examples 21・playground 5・protos 12・skill 3 の 74 件が、SKIP なしで通った(出力の `SKIP` の行は 0)。examples が 269 秒で、マシンの負荷の平均が 100 前後になる回は、examples が 8〜10 分かかった。
  - 参照インタプリタと一致した実行の本数(例の全部の版と `tests/flows` のシナリオ。この段階で足したシナリオを含む)は、Step Functions の JSONata が 413、LocalStack が 413、Temporal の TypeScript と Python が 675 ずつ、言語をまたぐもの(TypeScript のワークフローと Python のアクティビティ、その逆)が 377 ずつ、durable functions が 403、pydantic-graph が 566、Argo が 303(ほかに、フローごとに一本、本物の Pod でも走らせた 17 本)。
  - ゼロ値や無い値を選ぶ実行は、全部で 1,109 本のシナリオのうち、入力のゼロ値を省いたものが 30 本(8 フロー)、必要な入力が無いものが 7 本(7 フロー)、`json` と値の無い `T?` を省いた入力が 10 本(10 フロー)、レスポンスとサービスが送る値のゼロ値を省いたものが 25 本(10 フロー)、`json` を省いた Connect のレスポンスが 1 本、規則のサービスのゼロ値を省いた結果が 7 本と列挙を省いた結果が 5 本(どちらも 3 フロー)。
  - ほかの確かめ:既定の `Transport` から 1,102 件の呼び出し(HTTP が 954 件、Lambda が 103 件、規則のサービスが 35 件、AWS が 10 件)、規則のレスポンスの読み比べが 135 件、`tsc --strict` が 87 のビルド、記録した履歴の再生が 30 本、図は 537 回の実行が光ってつながり、92 枚が Mermaid 11 と 12 で描け(Chrome で 6 本を確かめた)、ブラウザで試すページは 36 本のフローと 389 件の問い合わせがコマンドと同じだった(Chrome で 8 画面)。protoc は 5 本のサービスの `.proto` を組み立て、buf の lint と build は `options.proto` を通し、protobuf の `json_format` は 267 のメッセージと 189 の入力を読み、本物の規則のサービスは 12 規則の 334 件に答えて、範囲の外の 8 件を受け付けなかった。
  - わざと入れた誤りの一覧(どれもシナリオの食い違いとして出た。番号は、そのフローの何本目か)。無い `json` の読み(この段階)は、前の項に書いた。記述から型を作る(段階 B):作る表で 64 ビットの整数を `int` にする、`required` を読まずに `T?` にする(どちらも `made_types_fit_their_descriptions`)、ASL の `match` の `none` を案件の null で判定する(`proto_types.flow` の 1 本目)。規則のサービス(段階 C):数を文字列にしない、送る列挙を表に通さない、読む列挙を逆の表で引く、ヘッダを外す、参照インタプリタが読み戻さない(どれも `connect_rules.flow` の 1 本目)、数を読み戻さない、リトライを一回にする(2 本目)、ゼロ値を埋めない(13 本目)。サービスを実装する(段階 D):入力のゼロ値を埋めない(どのプラットフォームでも `service.flow` の 2 本目)、無い入力を null にしない(JSONata と LocalStack の 18 本目)、コールバックの応答のゼロ値を埋めない(どれも 7 本目)、出力の無い終わりを null にする(どれも 1 本目)、TypeScript のクエリを最初に登録しない(Temporal の 1 本目)。
- 列挙の値の名前を `rulec api` から読む(1.13。2026-10-01 夜):rulec の二つのビルドで走らせた。値の名前を言うもの(`connect.enums` を出す、公開前のビルド)と、言わないもの(公開されていた 0.21.2 と同じもの)で、どちらも `rulec --version` は `rulec 0.21.2` と言ったので、テストは `connect.enums` があるかで見分ける(5 章)。言うほうのビルドの出力は、そのあと rulec 0.22.0 として公開された。リポジトリの規則 14 本のすべてで、公開前のビルドと 0.22.0 とは、`rulec api` と `rulec schema` が一字も違わず、`rulec certificate`・`rulec doc`(英語と日本語)・`rulec gen` が書くものは、版の文字列だけが違った。以下の数は、特に書いていなければ公開前のビルドで取ったもので、0.22.0 で回し直した結果は、この項の最後に書く。
  - 名前を言う rulec での全体(`cargo test --no-fail-fast`、debug のビルド)は 5 分 28 秒で、lib 26・doc 5・docs 6・examples 22・playground 5・protos 12・skill 3 の 79 件が一回目で通り、SKIP は 0。参照インタプリタと一致した本数は、Step Functions の JSONata と LocalStack が 423、Temporal の TypeScript と Python が 685 ずつ、言語をまたぐものが 387 ずつ、durable functions が 413、pydantic-graph が 576、Argo が 313 で、どれも `tests/flows/connect_rules_contract.flow` の 10 本の分だけ増えた。その 10 本のうち、2 本目は状態が `ACTIVE` の結果を `{"fee":"3"}`(`nextState` が無い)で返し、7 本目と 10 本目は手数料も 0 の結果を `{}` で返す。どのプラットフォームも `有効` と読んで、次のタスクの引数に渡した。送る側では、どの呼び出しも `{"state":"ACTIVE", …}` と 0 番の値を書いた。Argo の一回の実行のノードは、多くて 120 個(E040 の見積もりは 162 個)。既定の `Transport` からの呼び出しは 1,115 件(規則のサービスが 42 件で 7 件が失敗)。
  - 本物のサービス:13 の規則(例の 11 と、`tests/fixtures/rules` の契約の列挙を持つ二つ)のベクタ 350 件を、入力を省かずに送り(114 件はゼロ値の入力を含む)、dandori が読んだ結果はすべて rulec の期待値と同じだった。サービスは、範囲を超える入力 9 件、列挙に無い名前 11 件、知らないフィールド 13 件、入力を一つ省いたリクエスト 13 件を、どれも 400 の `invalid_argument` で受け付けなかった。`rulec gen` の `.proto` との名前の突き合わせは 13 本すべてで一致した(契約は `rulec gen` が `proto/shop/v1/order.proto` と `proto/bank/v1/account.proto` にコピーし、テストはコピーしない)。読み比べは 4 規則の 200 件、図は 547 回の実行が光ってつながり、94 枚が Mermaid 11 と 12 で描けた。
  - 名前を言わない rulec(公開されていた 0.21.2 と同じ中身のビルド)では、lib(26 件)、`tests/protos.rs`(12 件)、`examples_pass_check`・`diagnostics_match_the_golden_files`・記録した出力での E005(3 件)、`DANDORI_FLOW=order/aws` と `DANDORI_FLOW=tests/flows/connect_rules` の突き合わせ、`dandori doc` のテスト(5 件)、プリセットのテストを走らせた。突き合わせは、注文の AWS 版が英語 39 本・日本語 39 本(durable functions は 38 本ずつ)、`connect_rules.flow` が 22 本で、どのプラットフォームでも一致した。`SKIP:` の行は、`account_fee.rule` と `delivery_fee.rule` の名前の突き合わせと本物のサービス(2 規則ずつ。ベクタは 332 件、受け付けられなかったのは範囲 8 件と名前 9 件)、知らないフィールドと省いた入力を受け付けないことの確かめ、`connect_rules_contract.flow`(突き合わせ、読み比べ、図)、プリセット。ほかに SKIP は無い。二つの `DANDORI_FLOW` の回は、`jev_tasks_answer_on_typesafe` と `open_responses_agents_answer_on_ollama` が「一件も送らなかった」で落ちた。絞ったフローに Jev のタスクも Open Responses のエージェントも無いためで、名前を言う rulec でも同じに落ち、絞らなければ通る。
  - わざと入れた誤り(どれも戻した)。0 番の値の埋めを一か所ずつ外すと、`connect_rules_contract.flow` の 2 本目で出た。JSONata では Step Functions と LocalStack、io.ts では Temporal の TypeScript、durable functions、Argo、TypeScript のアクティビティを使う Python のワークフロー、io.py では Temporal の Python、pydantic-graph、Python のアクティビティを使う TypeScript のワークフロー、参照インタプリタでは見たプラットフォームのすべてが食い違った。送るときに 0 番の値を省く誤り(参照インタプリタ)も 2 本目で出て、本物のサービスは `account_fee.rule` のベクタの一本目を 400(`状態: not set`)で受け付けなかった。突き合わせに出ない誤りが二つあった。埋める表(`RuleConnect.zeros`)から 0 番の値を落とすのと、`connect.enums` があっても名前を組み立てるのとで、どちらもすべてのプラットフォームが同じに誤るので食い違わない(後者では `RuleUnset` のシナリオが一本増えて 11 本になり、全部一致した)。どちらも `tests/protos.rs` が出した。前者は、`rulec gen` の `.proto` から作った埋める表との比べと、本物のサービスのベクタの一本目の読み(`{"fee":"110"}` を `次の状態` が null の結果として読んだ)で、後者は、`.proto` の値の名前との比べと、本物のサービスの 400(`cannot decode bank.v1.Status from JSON: STATUS_ACTIVE`)で出た。
  - 並びをたどる規則:前の dandori は、並びの入力 `明細` を `string` と読み、文字列を渡す `.flow` を検査に通していた(`rulec schema` の `array` を知らなかった)。リストを渡すと E003、`connect` で呼ぶと、型 `repeated Element` を読めないという E005 だった。いまは同梱でも `connect` でも `use rule` の同じ E005 で、`tests/fixtures/rule_lists.flow` が、どちらの rulec でも同じ golden になる。
  - Argo の演じ直しは 149 回(17 フロー)で、これまでの全体(1 回から 5 回)より多かった。この回は、コントローラーが全体の途中で一度、終了コード 0 で終わって立ち上がり直した。直前のログには、リーダーのリース(`leases/workflow-controller`)の更新が、クライアントのレート制限で 5 秒待たされたあと期限を過ぎて失敗したと出ていた(`Failed to update lease optimistically … context deadline exceeded`、`Error retrieving lease lock … client rate limiter Wait returned an error`)。リーダーを失ったコントローラーは終わるので(client-go のリーダー選出の決まり)、7 章の「自分から終わって立ち上がり直す」はこれと見ている。演じ直しの多くは、その立ち上がり直しのあいだに止まったノードの回と見ているが、時刻は突き合わせていない。テストは、演じ直しで全部通った。
  - rulec 0.22.0 での全体と、0.21.2 での回(2026-10-01 夜):rulec 0.22.0 が公開されたので、それで golden とプリセットを取り直し、全体を回した。取り直して変わったのは版の文字列(`rulec 0.21.2` が `rulec 0.22.0` に)だけで、`tests/doc` の golden 34 本(50 行)、サイトの例のページ 10 枚(各 1 行)、`presets.json`(37 行、50 か所)のどれも、版の文字列を戻すと取り直す前のものと一字も違わなかった。
  - 0.22.0 での全体(`cargo test --no-fail-fast`、debug のビルド)は二回回し、どちらも SKIP は 0 だった。一回目は 10 分 34 秒で、79 件のうち 78 件が通った。落ちたのは Argo の一件で、`arrange_delivery.ja.flow` の一回が、ノードが `Pending` のまま、ランナーの 10 分の制限に着いた(上の止まった回と同じ形。同じ回の英語版のフローでは、演じ直しが 7 回出た)。二回目は最後の状態で、5 分 6 秒で 77 件が通り、落ちたのは二件だった。`jev_tasks_answer_on_typesafe` は、本物の TypeSafe への送信が `read ECONNRESET` で失敗した。`temporal_activities_run_in_the_other_language` は、引当と発送の 10 本目で、Python のワークフローが TypeScript のアクティビティ `wait_for_packing` を呼んだところがタイムアウトし、`on timeout` の `fail PackingLate` で終わった(アクティビティの呼び出しは記録されなかった)。どちらも原因は確かめていない。全体の途中は、負荷の平均が 60 から 110 になる(`uptime` で見た)。同じ回に、Argo のコントローラーも、リースの更新が遅れて一度立ち上がり直した。落ちたものだけを回し直すと、Argo は 2 分 7 秒(313 本が一致、演じ直しは 0 回)、あとの二件は 52.65 秒で、どれも通った。
  - 参照インタプリタと一致した本数は、公開前のビルドでの値と同じだった。Step Functions の JSONata と LocalStack が 423、Temporal の TypeScript と Python が 685 ずつ、言語をまたぐものが 387 ずつ、durable functions が 413、pydantic-graph が 576、Argo が 313 である。本物のサービスは 13 規則のベクタ 350 件(ゼロ値の入力を含むものが 114 件)に答え、400 の `invalid_argument` で受け付けなかった数も、範囲の外 9、列挙に無い名前 11、知らないフィールド 13、入力を省いたリクエスト 13 で同じだった。
  - 0.21.2 では、lib(26 件)、`tests/protos.rs`(12 件)、`examples_pass_check`・`diagnostics_match_the_golden_files`・記録した出力での E005(3 件)が通った。`SKIP:` の行は `tests/protos.rs` の 5 行で、`account_fee.rule` と `delivery_fee.rule` の名前の突き合わせと本物のサービス(2 規則ずつ)と、知らないフィールドと省いた入力を受け付けないことの確かめである。本物のサービスには 11 規則のベクタ 332 件を送り、受け付けられなかったのは範囲の外 8 件と名前 9 件だった。`dandori doc` のテストは 5 件のうち 3 件が通り、規則のページに入る版の文字列が違う二件(golden とサイトの例のページ。`connect_rules_contract.flow` の SKIP が 2 行)が落ちた。ブラウザで試すページのテストは 5 件のうち 4 件が通り(プリセットは SKIP)、バンドルの答えとコマンドの出力を比べる一件が、同じ理由で落ちた。0.21.2 で `dandori doc` を走らせた出力は、golden の版の文字列を直したものと、英語と日本語の 5 例、Markdown と HTML の 20 通りで、一字も違わなかった。
  - 公開された 0.21.2 のサービス(`account_fee.rule`)に、知らないフィールド、入力を省いたリクエスト、列挙に無い名前 `STATUS_ACTIVE` を送ると、どれも 200 で通った(省いた入力は 0 として判断され、`fee` が `110` で返った)。0.22.0 のサービスは、三つとも 400 の `invalid_argument` で受け付けなかった。範囲の外の入力は、どちらも 400 だった。
  - プリセットを取り直すテストと同じ回に、`presets.json` を読む一件(バンドルの答えとコマンドの出力を比べるもの)が、書き直す前の記録を読んで落ちた。rulec を上げると、版の文字列が違うだけで、取り直しの一回目が失敗する。取り直す回はその一件を `SKIP:` の行を出して飛ばすことにした(5.3)。`dandori doc` のテストが、取り直す回に同じことをしているのと同じである。

- Temporal の Go SDK(4.2。2026-10-02):`--target temporal-go` の生成物を、Go の SDK 1.49.0(Go 1.26.0)で、Temporal CLI 1.9.1 の dev server の上で走らせた。
  - 例と `tests/flows` の 29 本のフローの 685 本のシナリオすべてが参照インタプリタと一致し、終わりのクエリと search attribute も参照の案件の状態と同じだった。TypeScript 版、Python 版と同じ本数である。全部を同時に流して 71 秒だった。
  - 言語をまたぐアクティビティは、Go のワークフローと TypeScript のアクティビティ、その逆の両方で、`local` の規則と `event` を持つフローを除く 20 本のフローのすべての実行が一致した(387 本ずつ。TypeScript と Python の組と順に流して、Go のランナーのビルドの 9 秒を含めて 108 秒)。ランナーを書いた段階では、Go と Python の組み合わせも両方向に試し、一致した。
  - 子の `.flow`:Go どうし、Go の親と TypeScript の子、その逆のどれでも、三本すべてが一致した。Worker Deployment Versioning:Go でも、A で始まった実行は新しい実行に引き継いだ二回目のイテレーションまで A のコードで終わり、B を current にしたあと始めた実行は B のコードで走った。ランナーの二つのビルドの ID は、dandori が `worker.go` に書いた `BuildID` と同じだった。
  - 再生:記録した九つのフローの Go の履歴 15 本(続きの実行を含む)が、いまのコードで再生できた(TypeScript と Python と合わせて 45 本)。はじめは、コールバックを持つ 10 本のフローの実行が、同じコードの再生で `[TMPRL1100] lookup failed for scheduledEventID to activityID` になった。Go の再生は、ワークフロー ID を渡さなければ `ReplayId` という ID で走る。ワークフローは冪等キー、子ワークフローの ID、コールバックの ID をワークフロー ID から作るので、コールバックの ID を引数に持つアクティビティの予定が、記録と食い違っていた。生成する `Replay` が `OriginalExecution` で元の ID を渡すようにした。
  - 既定の `Transport`:1,115 件の呼び出し(HTTP 954 件でうち 333 件が失敗、規則のサービス 42 件で 7 件、Lambda 109 件で 35 件、AWS の API 10 件で 2 件)を Go からもローカルのモックと moto に送り、すべて呼び出しのとおりに届いて、差し替えの `Transport` と同じものを返した。はじめは、SQS のメッセージの JSON の鍵の順が TypeScript と違うことで落ちた(Go の map は順を持たない)。JSON の値として比べるようにした。
  - エージェント:問い合わせの六つの版の 17 件ずつと agents の 44 件、計 146 件で、Go の既定の `Transport` は Step Functions と同じリクエストを一回だけ送り、拒否を `AgentStopped`、500 をリトライせずに SDK のエラーにした。
  - 規則のレスポンスの 200 通りを、参照インタプリタ、TypeScript、Python、Go、JSONata の五か所で同じに読んだ。`rules.go` は、例の規則のベクタ 600 件に rulec のとおりに答えた。生成した Go は、46 本のフローのすべてで、rulec の Go と一緒に `go vet` を通り、gofmt のとおりだった。
  - テストのランナーは、50 のパッケージを一つのバイナリにする。ビルドは 7〜9 秒(SDK のパッケージがビルドのキャッシュにあるとき)、バイナリは 102 MB で、ビルドの間のメモリは最大で 1.4 GB ほどだった。テスト全体の中では、ほかのテストと重なって 67 秒かかった。テストは、自分の一時ディレクトリ(`dandori-test-<プロセスの番号>-…`)を、あとで読めるように残して終わる。前はそれを誰も消さず、9/26 からの一週間で 26,000 個、6.8 GB たまっていた(Go のランナーは一つで 100 MB ある)。そこで、テストのプロセスは最初に、終わったプロセスのディレクトリを消す(`kill -0` がそのプロセスは無いと言うものだけ)。Go のビルドと、テストの `go vet`・`go run` には `-trimpath` を付ける。付けないと、Go のビルドキャッシュはパッケージをディレクトリごとに鍵にするので、毎回別の一時ディレクトリで作る同じパッケージがキャッシュに当たらず、Go の三つのテストを流すたびにキャッシュが 0.8 GB 増えた(Go 版を作るあいだに 8.6 GB)。`go run` は作った実行ファイルもキャッシュに入れるので、規則のつなぎを確かめるプログラムには、ベクタのファイルを絶対パスでなく名前で渡す。直したあとは、二回目からの増え方が 1 MB になった。
  - ランナーを書く中で分かった Go の SDK の振る舞い(ランナーの側で手当てした):ワークフローやアクティビティを登録していないワーカーでも、Go の SDK は両方の種類のタスクを取りに行き、自分のアクティビティを先に取ろうとする(eager execution)。言語をまたぐ実行で、ワークフローだけのワーカーには `LocalActivityWorkerOnly` を、アクティビティだけのワーカーには `DisableWorkflowWorker` を付けた。付ける前は `unable to find activityType=rule_policy. Supported types: []` で失敗し、クエリがアクティビティだけのワーカーに届いた。利用者が Go のワーカーを同じように分けるときも同じである。生成したワークフローが panic すると、既定ではワークフロータスクの失敗を繰り返して止まるので、ランナーのワーカーは `WorkflowPanicPolicy: FailWorkflow` にした。Go のワーカーのスティッキーなタスクキューは `<ホスト名>:<uuid>` と名付けられるので、履歴を記録するときにホスト名を `localhost` に替える。
- 日付と帳簿(1.16、2026-10-04):請求の例(英語の版と日本語の版、どちらも 18 本)と `tests/flows/dates_and_books.flow`(15 本)は、Step Functions(JSONata と LocalStack)、Temporal の TypeScript・Python・Go、Lambda durable functions、Argo Workflows、pydantic-graph のすべてで参照インタプリタと一致した。Argo では、フローごとに一本を本物の Pod でも走らせ、一回の実行のノードは、多いもので請求が 133 個(E040 の見積もりは 173 個)、`dates_and_books.flow` が 137 個(193 個)だった。言語をまたぐアクティビティでは、請求の 18 本が両方向で一致した(`dates_and_books.flow` は日付が `local` なので外れる)。日付のつなぐコードは、請求の日付で koyomi の 835 件(英語)と 831 件(日本語)、`dates_and_books.flow` の二つの日付でそれぞれ 728 件の例に、Temporal と pydantic-graph の `rules.py` が koyomi のとおりに答え、`rules.go` は規則と日付を合わせて 3,849 件に答えた。帳簿の操作は、三つのフローの並びを合わせた 170 件を、TypeScript・Python・Go の `Transport` と Step Functions の Lambda 関数のコードから、PostgreSQL と TigerBeetle の上で流し、どれも chobo の参照インタプリタと同じ答えだった。替える前(ff11288)と後で、例とテストのフローの全部(`check` を英語と日本語と JSON で、`scenarios`、`doc` を二つの形と二つの言語で、`build` を七つのプラットフォームで、939 回)の出力を比べると、違ったのは、新しい書き方を使うフロー(請求の二つの版、`dates_and_books.flow`、新しい四つの検査のテストのファイル)と、`tests/fixtures/values.flow` の `check`(型の一覧を言う注に `date` が増えた三つ)だけだった。

### 5.2 図にする(`dandori doc`)

書いたものが矛盾なく動くことは、検査とシナリオで機械が確かめる。書いた流れが意図どおりかは、人が確かめるしかない。エージェントが `.flow` を書く使い方では、とくにそうである。レビューする人にとって、分岐とエラーの行き先は本文より図のほうが早く読める。

**決定**:`dandori doc` を足す。既定は Markdown で、`--format html` なら HTML のページを一枚書く。どちらも検査を通したモデルから描くので、図はプラットフォームによって変わらず、何も走らせる前に手に入る。Temporal にはワークフローのコードを図にする画面が無く、Step Functions や Argo Workflows のグラフには、ビルドが足したステート(結果の確かめ、ループの回し方)が混ざる。

**何を載せるか**:ノードに名前だけを並べた図は、本文への索引にしかならない。rulec の `doc` のページで、値ひとつにノードひとつの図を捨てたのと同じ理由である。そこで、検査が知っていて本文には書かれていないことを載せる。

- 呼び出しのノードの二行目に呼び方、三行目に、流れには書かれていない宣言の中身(案件に何をするか、リトライ、タイムアウト)。
- 呼び出しの表:呼ぶもの、リトライ、タイムアウト、エラーごとの行き先(その場のハンドラ、`on failure`、ワークフローの失敗)、呼び出しのあとの案件の状態。
- 終わり方の表:`succeed`・`fail`・各ブロックの終わりのそれぞれで、案件がとりうる状態。E020 と同じく、外部のサービスで起きるイベントも含める。
- HTML では、ステップを選ぶと、そこに来たときの案件の状態も出る。

このために、流れに沿った検査が、各文の入口で案件がとりうる状態と、各終わりでの状態を残すようにした(`flow::Facts`)。検査が見たものをそのまま出すので、図と診断が食い違うことはない。

ワークフローの入力と出力は、Markdown では冒頭の文に、HTML では start の詳細に、型と、そこに書いた範囲とともに並べる(`quota: int range >=1 <=1000`、`campaign: tiers.tier range >=basic <=gold`、`visit: date range >=2026-01-01 <=2027-12-31`)。範囲が無いと、型のどの値も受け取るように読めるからである。ワークフローが動く期間(20 章)も、同じところに `now range …` として書き、`on failure` の説明には、期間の外で `now` を読むとそこへ入る行を添える。期間の確かめは図に描かず、そこで落ちる実行は、確かめる文の次の文を光らせる。数の範囲の端は、E014 の文と同じく、型の単位で数えた整数で書く(9 章)。

**Markdown**:Mermaid のフローチャートにする。GitHub がプルリクエストや issue の中でそのまま描くので、PR に貼る要約になる。rulec も、Markdown ではステートマシンを Mermaid で描いている。形は、タスクが四角、規則が両脇に線のある四角、外から値が届くタスク(`event`、`callback`)が斜めの四角、`match` が六角形、待ちが角の丸い四角、終わりが両端の丸い形で、ループは subgraph で囲み、次のイテレーションへ戻る辺と、`break` から外へ出る辺を描く。`match` をひし形にしないのは、文字を入れるとひし形が大きくなりすぎるからである。flow・`on failure`・`on cancel` は別々の図にする。

**HTML**:図は dandori が SVG で描き、レイアウトエンジン(dagre、Graphviz)は使わない。`.flow` には飛び越しが無く、分岐とループが入れ子になるだけなので、ブロックの入れ子どおりに並べられる。本線は真下に進み、`match` の分岐と呼び出しのハンドラは右に並んで、下で本線に戻る。`match` の真下には先へ進む最初の分岐を置き、そこで終わる分岐は右に出すので、ふだんの流れがまっすぐ下に読める。ループは本体を囲む枠で、左に次のイテレーションへ戻る線、右に `break` で抜ける線がある。文字は等幅なので、ブラウザが無くても幅を計算でき、同じ `.flow` からはいつも同じページができる(テストで全文を比べられる)。ページはネットワーク無しで開け、スクリプトが無くても図と表は見える。

**実行を光らせる**:`dandori scenarios` と同じシナリオを参照インタプリタで走らせ、通った文、選んだ分岐、入ったハンドラ、ループのイテレーション、呼び出しの結果を記録する(`interp::Visit`)。辺にはそれぞれ、その辺を通るために起きていなければならないこと(その分岐、そのハンドラ、呼び出しの結果が返った、ループのイテレーションがもう一つ走った、ループが回り切った)を持たせ、両端が光っていて、そのことが起きた辺を光らせる。診断のそうなる例は、各ステップが流れのどこで起きたか(`diag::At`)を持つようにして、同じように光らせる。ステップは呼び出しの返り方とループの回り方を言わないので、そこは、同じ例のほかのステップから読む。シナリオを走らせるときの見え方は Temporal にした。主なプラットフォームで、どの機能も持つからである。

**検査でエラーが見つかるワークフローも描く**:名前と型が解決していれば描き、終了コードは 1 にする。E020 のそうなる例を、図の上の一本の線として見せるためである。rulec の `doc` は検査を通らない規則を描かない。あちらは人が読むページで、壊れた規則を整ったページにすると、読む人を誤らせるからである。こちらはレビューと直しのためのものなので、逆にした。

**規則も見せる**:規則の呼び出しは図の上では四角一つで、何を決めるかは規則の中にある。レビューする人は、その表まで読めてはじめて流れを確かめられる。そこで `doc` は、ワークフローが使う規則を、`rulec doc` が描くページのとおりに見せる(ページの言葉を口に渡す)。Markdown では最後に規則の節を置き、規則ごとに `<details>` で畳む(ホテルの予約で 10 KB が 31 KB ほどになり、GitHub のコメントの上限 65,536 字には収まる)。HTML では、左に規則の一覧を、規則を呼ぶステップの欄にボタンを置き、`rulec doc --format html` のページを、このページの上いっぱいに開く(`#rule=<名前>` でも開き、Esc で閉じる)。開くページは sandbox の iframe(`allow-scripts` だけ)に入れ、そのスクリプトはこのページに触れない。新しいタブではなくページの上に開くのは、ブラウザで試すページの中(それ自体が sandbox の iframe で、別のウィンドウを開けない)でも同じに動かすためである。

表を dandori が描き直さないのは、規則を描くところを rulec の一か所に保つためである(捨てたもの)。そのかわり、rulec が描いたものには描いた rulec の版が入るので、rulec のバージョンが変わるたびに(ritsu の中では、ritsu のバージョンを上げるたびに)golden(`tests/doc`)、サイトの例のページ、ブラウザで試すページの記録を取り直す(テストが落ちて知らせる)。

**サービスと、Connect で呼ぶ規則**:サービスを実装するフロー(1.14)では、Markdown の冒頭の入力と出力のあとに、実装するサービスと記述のファイルを書き、メソッドごとの表(メソッド、何をするか、そのメソッドが指すタスク)を置く。HTML では、start の詳細に同じことを載せる。Connect で呼ぶ規則(1.13)は、呼び出しの表の「呼ぶもの」にサービスの URL を、図の二行目に `connect` を添える。

**確かめ方**(`tests/doc.rs`):

- Markdown が golden(`tests/doc`)と同じこと。例の Temporal 版、子の `.flow`、`tests/flows`、エラーのある下書き(`hotel_naive.flow`)を、英語と日本語で。
- サイトに置いた例のページ(`website/dandori/docs/doc` は英語の版、`website/dandori/docs-ja/doc` は日本語の版から描く)が、いまの出力と同じこと。サイトの Mermaid の図は `tests/docs.rs` が golden と比べる。
- 全シナリオで、光るところがつながっていること。光ったノードには光った辺が入る。通った分岐、ハンドラ、ループの出口には光った辺がある。ループの最初のステップを、そのループに入った回数より多く通ったなら、戻る辺が光る。ただし、外のループの本体の最後にある内のループの出口は、外のループの次のイテレーションへ戻る辺として描くので、外のループが回り切ったときは光らない(分岐の先が次のイテレーションへ戻る辺のときと同じ扱い)。`tests/flows/service.flow` の、箱のリストの中の品のリストのループで、この形が初めて試験に入った。
- Mermaid 11 と 12 で、golden の図がすべて描けること(headless Chrome の中で描かせる)。バージョンは 11.17.2 と 12.1.0 で、chobo と sakai の `tools/mermaid` と同じにした(2026-10-06。前の 12.0.0 は、依存の chevrotain が lodash-es 4.17.23 を指していて、それに既知の脆弱性が二つあった。ritsu の DESIGN 3.6)。
- Chrome でページを開くと、ページのデータどおりに光ること。`#rule=<名前>` で、その規則について `rulec doc` が描いたページが開くこと。

わざと入れた誤り(呼び出しの結果が返ったことを見ない、ループのイテレーションを記録しない、ループが回り切ったことを見ない、分岐の辺の行き先を変える、ハンドラを結果と取り違える、Mermaid の引用符をそのまま出す、サイトの図を一語変える)は、どれも捕まる。

### 5.3 ブラウザで試す

(2026-10-05 に、このページを ritsu のブラウザで試すページへの転送に替え、ここに書いたモジュールとテストを消した。0.3 の「ブラウザで試すページ」。以下は、それまでの形の記録である。)

インストールする前に、書いて検査するところまで試せれば、使うかどうかを決めやすい。rulec のサイトには、検査器を wasm32 にしてページの中で動かすプレイグラウンドがある。

**決定**:dandori も wasm32 にして、サイトのページの中で `check`・`build`・`doc` を走らせる。書いたフローは、ブラウザの外に出さない。

**ファイルと規則を読むところを一つにした**(`sources`):`.flow` が参照するもの(規則、API の記述、子の `.flow`)を読むところは、`check`・`apis`・`proto`・`rulec` に散っていた。これを一つの trait(ファイルを読む、同じファイルかを見分ける名前を返す、規則を読む口を返す)にまとめ、スレッドごとに差し替えられるようにした。コマンドはディスクと、走らせる側が渡した規則の口から読み(`Disk`)、ページは記録したバンドルから読む(`Playground`)。バンドルの中では、パスの `.` と `..` を字面のうえで畳んで引く。

**規則は、記録した口の答えから読む**:ページの中では rulec を動かせない。dandori の wasm のモジュールは、rulec を持たない(P2)。そこで、例の規則について rulec の口が答えたもの(規則の事実と、`rulec doc` が描いたページ)を、テストが記録してバンドル(`presets.json`)に入れる(`Recorder`)。事実は dandori の形の JSON(`src/record.rs`。単位は rulec の綴りで書き、読み直すと同じ単位になる)で持ち、ページは描いたまま持つ。ページの中では、記録から答える口(`Recorded`)が規則を読む。前は、三つのコマンド(`schema`・`certificate`・`api`)が出力した JSON をそのまま記録していた。ページでは規則を書き換えられない。規則を書くのは rulec のプレイグラウンドの役目である。

**規則を見るタブ**:「規則」のタブで、フローが呼ぶ規則ごとに、`.rule` の本文(読むだけ)と、`rulec doc --format html` のページを開くリンクを出す。`.rule` の本文は、表示のためだけに文字列として読み、読み解かない。バンドルには、規則の本文と、`rulec doc` が描いたもの(Markdown と HTML、そのフローを見せる言葉のもの)も入る。

**日付のファイルと帳簿**(1.16):ページの中では koyomi も chobo も動かせないので、例の日付のファイルと帳簿について口が答えた事実を、規則と同じくバンドルに記録する(`RecordedDates`、`RecordedBooks`。JSON の形は `src/record.rs`。単位は rulec の綴りで、名前だけの数の単位は `{"count": "pcs"}` と書く)。請求の例も開ける。「規則」のタブには、規則だけを出す(日付と帳簿には `rulec doc` のページが無い)。

**`.proto`**:フローが読む `.proto`(呼ぶ API の記述、型を作る記述、実装するサービスの記述)は、ほかのファイルと同じくバンドルに記録する。`dandori/v1/options.proto` は dandori に埋め込んであるので、バンドルには入れない。

**開けるフロー**:エラーのある下書き(`hotel_naive.flow`)と、例のすべての版。日本語のページも、日本語のサイトと同じく下書きはそのまま開き、例は日本語版を開く。版を開くと、ビルドの出力先はその版が書かれたプラットフォームになる(AWS 版は Step Functions。Step Functions で動かせないものを使う版は Lambda durable functions)。

**境目**:rulec のプレイグラウンドと同じく wasm-bindgen を使わず、境目を越えるバッファはどれも、先頭に自分の長さ(リトルエンディアンの u32)を持つ。ただしバンドルは最初に一度だけ渡し、モジュールが持っておく。rulec のモジュールは状態を持たないが、同じようにすると、キーを打つたびに 700 KB の JSON を読み直すことになる。答えは、コマンドが出力するものと一字ずつ同じにする。そのために、コマンドの出力の組み立て(診断の並べ方、検査を通ったときの行、出力先ごとのビルド)をライブラリに移し(`commands`)、コマンド(`cli`)とページが同じものを使う。

**図のページ**:`doc` のページはウィンドウ全体を使うので、別のタブで開く。スクリプトを持つページなので、サイトのオリジンでは動かさず、sandbox の iframe(`allow-scripts` だけ)に入れた小さなページとして開く。

**大きさと速さ**(2026-09-30):モジュールは 1.7 MB(gzip で 559 KB)、バンドルは 1.4 MB(gzip で 299 KB。規則のページを入れる前は 715 KB と 90 KB)。node の中で五回ずつ走らせた中央値で、バンドルを渡すのが 15 ms、下書きの `check` が 2 ms、ホテルの予約の Temporal 版の `check` が 11 ms、その `doc`(HTML と Markdown)が 38 ms、倉庫の出荷の Temporal 版の `check` が 1 ms、`doc` が 18 ms だった。

**確かめ方**(`tests/playground.rs`):

- `presets.json` が、いま例を検査して読むものと、いまの rulec の出力であること(`DANDORI_BLESS=1` で取り直す。取り直す回は、次の項の、コマンドとの突き合わせを、`SKIP:` の行を出して飛ばす。突き合わせが、書き直す前の記録を読むことがあり、rulec を上げたあとは、版の文字列が違うだけで落ちるからである)。
- 開けるすべてのフロー(英語と日本語で 36 本)で、ディスクから読んで rulec を動かすコマンドが出力し、書くものが、バンドルから読むページの答えと同じこと。`check`、七つの出力先への `build`(2026-10-02 に Go を足して七つ)、二つの形式の `doc` を比べる。「規則」のタブには対になるコマンドが無いので、同じ関数をディスクと rulec から読ませた答えと比べ、どの規則にもページがあることも確かめる。
- 置いてある `dandori.wasm` が、node の中で、どの問い合わせにもライブラリと同じ答えを返すこと(389 件)。例そのままのフローのほか、構文の誤り、ページに無い規則と子のフロー、自分を子として走らせるフロー、バンドルに無いファイル、直した下書きも試す。版もクレートと同じでなければならない。
- headless Chrome の中で、両方の言語のページが動き出し、下書きの `check` を表示し、フロー・タブ・出力先を指すリンクのとおりに開くこと。「規則」のタブが、規則ごとに本文とページへのリンクを出すこと。

わざと入れた誤り(ライブラリの文言を一語変え、wasm を作り直さない)は捕まる。

## 6. 捨てたもの

- **rulec に workflow の宣言を足すこと。** 0.2 のとおり。
- **JSONPath を出すこと。** 変数と JSONata があれば、ResultPath の組み替えなしにデータを運べる。
- **Temporal の再試行に任せること。** Temporal は、型を挙げて「これだけをリトライする」とは書けない(挙げられるのは、リトライしないものだけである)。Step Functions と同じ回数にならない。
- **`match` の最後の分岐を Default にすること。** 検査の外の値が、黙ってその分岐へ流れる。
- **条件式を dandori に入れること。** P1 のとおり。条件は rulec の表に書けば、網羅と重なりが rulec の証明書つきで閉じる。
- **値の組み立ても書けないこと。** はじめの P1 は、引数に変数・フィールド・リテラルしか書けず、レコードを組み立てるにも文字列に値を入れるにもタスクか規則が要った。判断にかかわらない操作まで外に出させる理由は無いので、組み立ては許した(0.1)。
- **Temporal で、規則をワークフローのコードで直接呼ぶこと。** 履歴は増えないが、判定の記録が残らず、規則を直すたびにワーカーのバージョンの固定(Worker Deployment Versioning の `PINNED`)が要る。固定しないと、実行中のワークフローの再生が新しい規則で判定し直し、結果が変わった分岐で止まる。
- **Temporal で、規則をいつもローカルアクティビティにすること。** 往復と履歴は減るが、規則をワークフローと同じワーカーに置くことになり、規則だけを出し直せなくなる。ループの中で何度も判定するワークフローのために、`use rule` の下の `local` で選べるようにした(4.2)。
- **Temporal で、コールバックをアクティビティの非同期完了で受け取ること。** はじめはこの形だった。応答する側がアクティビティのタスクトークンを持つ必要があり、待つあいだアクティビティが開いたままになる。シグナルなら、応答する側はワークフローを指す ID だけを持てばよく、待ちは `condition` のタイムアウトで表せ、テスト環境で時間を飛ばしてタイムアウトまで確かめられる。
- **durable functions で、`lambda` のタスクを `context.invoke` で呼ぶこと。** 4.3 のとおり、宣言したエラーを見分けられない。step の中で Invoke を呼ぶ実装を dandori が書けば、利用者のコードも要らない。
- **durable functions で、step の `retryStrategy` にリトライを任せること。** step だけなら関数一つで Step Functions と同じ並びを書けるが、`durable function` のタスクを呼ぶ `context.invoke` にはリトライの設定が無く、それはワークフローのコードでリトライするので、step もそれにそろえた。
- 規則と日付の呼び出しの上限の付け方と、`timeout until` の書き方で捨てた形は、16.4 にまとめた。
- **Step Functions で、ネストした実行の失敗を Cause から読み分けること。** 子の `fail` の名前は Cause の JSON の中にあると思われるが、その形を文書で確かめられなかった。確かめられない形に頼る代わりに、宣言したエラーを E050 にした。
- **並列のイテレーションの失敗を、先に起きた順で決めること。** プラットフォームごとに並べ方も速さも違うので、同じシナリオでも結果が変わる。リストの順なら、どのプラットフォームでも同じ失敗になる。
- **Argo で、ステップの `when` や引数からグローバル出力パラメータを読むこと。** はじめはこの形で、変数を読む式をそのまま `when` と引数に書いていた。4.4 のとおり、同じテンプレートの前のステップが入れた値が見えない。kind の上では、処理していないエラーのあとの文まで走った。動いているステップの `when` が途中で偽に変わると、そのステップが進まなくなることも、ソースで確かめた。
- **Argo で、変数を引数と出力で次のステップへ運ぶこと。** グローバル出力パラメータを使わずに済むが、すべての変数がすべてのノードの入力と出力に載り、Workflow の大きさ(1 MiB まで)を早く使い切る。グローバル出力パラメータなら、値は Workflow の出力に一つずつと、値を入れたノードにだけ載る。
- **Argo で、ループを再帰で回すこと。** イテレーションの数だけネストが深くなり、Argo の深さの上限 100 に当たる。
- **Argo で、タスクのタイムアウトをテンプレートの `timeout` にすること。** 4.4 のとおり、Pod が始まる前に切れると `continueOn` が効かない。
- **Go 版で、値を構造体に読むこと。** Go を書く人には自然な形で、タスクの引数も結果も型付きにできる。しかし、構造体に読むと、無いフィールドと null の区別が消え、構造体の知らないフィールドが落ち、型に合わない結果は読む段階でエラーになる。そうした結果をワークフローが自分で見て `Dandori.BadResponse` にすることと、受け取った値をそのまま次の呼び出しに渡すことが、ほかの言語と同じにできなくなる。JSON の値のまま運び、構造体は `types.go` に書いて、利用者が自分のタスクの中で `Decode` で読む形にした(4.2)。同じ理由で、`OwnTasks` のメソッドの引数も `map[string]any` にした。
- **Go 版に `go.mod` を付けること。** 生成したパッケージを独立したモジュールにすると、利用者のモジュールから使うには replace が要り、依存するモジュールの replace は利用者の側では効かない。TypeScript 版と Python 版と同じく、利用者のモジュールに置くファイルだけを書き、前提にするモジュールは `doc.go` に書いた。
- **Go 版で、OpenAI の Agents SDK の非公式な Go の移植を使うこと。** OpenAI の公式の Go の SDK は Responses API までで、Agents SDK は無い(2026-10-01 に Go のモジュールの proxy で確かめた)。非公式の移植(`github.com/nlpodyssey/openai-agents-go`、v0.1.0)はあるが、dandori のエージェントはツールを持たず一往復で終わるので、公式の openai-go で Step Functions と同じリクエストを送るほうが、送るものがはっきりする(4.2)。
- **Go のテストで、フローごとにランナーをビルドすること。** ランナーは生成したパッケージと一緒にコンパイルしなければならず、Temporal の SDK を含むバイナリのリンクは、一回で数百 MB のメモリを使う。フローを同時に走らせるテストで、フローの数だけビルドすると、メモリも時間も足りない。`tools/temporal-go/build` が、すべてのフローのパッケージを一つのバイナリに入れ、テストはそれをフローごとに起動する(5 章)。
- **エージェントをプラットフォームにすること**(流れそのものを Agents SDK や、OpenAI が走らせる Agents API に任せること)。次に何をするかをモデルが決めるので、流れを前もって決め、走らせる前に検査するという dandori の芯が消える。エージェントは流れの中の一手、つまりタスクの呼び出し方の一つにした(1.7)。
- **Claude のエージェントを、Claude Agent SDK で走らせること。** Agent SDK は、呼び出しのたびに Claude Code の CLI を子プロセスとして起動する。応答の形も、Messages API の構造化出力のように生成を Schema で縛るのではなく、あとから確かめて合わなければ言い直させる(上限まで合わなければ `error_max_structured_output_retries`)。一回の呼び出しでモデルを何度も呼ぶことがあり、Step Functions が一回で送るリクエストとはそろわない。Agent SDK の文書自身も、ツールを使わない一回きりの問い合わせには API を直接使うよう案内している。dandori のエージェントはツールを持たず一往復で応答するので、Anthropic の SDK で Messages API を呼ぶことにした。ツールを持たせて何手も動かすエージェントを足すときには、Agent SDK が候補になる。
- **Step Functions から、Claude を Bedrock の統合で呼ぶこと**(いまは)。IAM で認証でき、API キーを接続に置かずに済む。しかし Bedrock の構造化出力は、Claude の文書の注記では Opus 4.6・Sonnet 4.6・Sonnet 4.5・Opus 4.5・Haiku 4.5 までで、5 系のモデルが無い。
- **Step Functions などでも `on cancel` を通し、そこでは走らないとすること。** Step Functions の `StopExecution` は、Temporal のキャンセルではなく強制終了(terminate)にあたる。キャンセルが届かないプラットフォームでは、`on cancel` は決して走らないと言えなくもない。しかし、後始末を書いた人が、Step Functions で止めても片付くと思い込むおそれがある。プラットフォームごとに意味が変わるものは E050 でエラーにしてきたのにそろえた。
- **`on cancel` の無いワークフローにも、キャンセルのシナリオを入れること。** 呼び出しの数だけシナリオが増え、しかもキャンセルを演じられるのは Temporal だけで、ほかのプラットフォームでは外すことになる。キャンセルをタスクのエラーと取り違えない、という生成物の性質は、`on cancel` を持つフローで確かめられる。
- **Temporal のワークフローの型を、バージョンなしの名前にすること。** はじめはこの形だった。Worker Deployment Versioning に任せれば同じ名前のままでも古い実行は古いコードに留まるが、それにはサーバーの側の設定と、バージョンの切り替えの操作が要る。型とキューにバージョンを入れれば、何もしなくても新しいバージョンと古いバージョンが並んで動く。同じバージョンのままのコードの違いは、Worker Deployment Versioning か再生の確認で扱う(1 章のバージョンと 5 章)。
- **コールバックをシグナルだけで受け取ること。** はじめはこの形だった。シグナルは送りっぱなしで、応答した側は、ワークフローが受け取ったかも、もう待っていないかも分からない。Update なら、受け取ったかどうかが返り、知らない ID や二度目の応答をその場で拒否できる。
- **search attribute をオンにしたことを、始めるときに空のリストを渡して伝えること。** 空のリストは、属性が無いのと区別できなかった(走らせて確かめた)。memo で伝える。
- **Temporal の突き合わせを、時間を飛ばすテスト環境で走らせること。** はじめはこの形だった。待ちの時間を飛ばせるので速いが、アクティビティをタイムアウトさせられず(そのシナリオは外していた)、キャンセル、Update、Worker Versioning のような本物のサーバーの振る舞いも確かめられない。dev server の上に移すと、待ちを縮め、タイマーのずれを直したうえで、TypeScript と Python の両方とも十数秒で終わった。
- **Temporal で、`timeout` の無いタスクに一律 60 秒を付けること。** はじめはこの形だった。Step Functions ではタイムアウトの無いタスクが、Temporal では 60 秒で切れる。長い処理を書いた利用者が、Temporal でだけ `timeout` に出会う。ワーカーが落ちたことには、ハートビートで気づけばよい。
- **Temporal で、Agents SDK との公式の統合を使うこと**(いまは)。4.2 のとおり、ツールを持たない一往復のエージェントには、タスクの `timeout` と `retry` がそのまま効く一つのアクティビティのほうが合う。
- **Argo の突き合わせで、全部の実行を本物の Pod で走らせること。** テスト全体で 30 分余りかかった。kind のノードの containerd が負荷のもとで落ち、食い違いになることもあった。本物の Pod はフローごとに一本にし、ほかはランナーが Pod を演じる(5 章)。
- **Argo のコントローラーの待ちを 1 秒より短くすること。** 300 ms では、かえって遅くなった。Argo のソースにも、informer が追いつかないので 1 秒より短くしないよう書いてある。
- **Argo の突き合わせで、呼び出しを HTTP テンプレートやエグゼキュータのプラグインに置き換えること。** Pod を立てずに済むが、生成したワークフローが呼び出しの終わりを読む形(終了コードとファイルの出力)が変わり、確かめているものが生成物でなくなる。
- **イベントを、待つ前に来たものも受け取っておくこと。** 送る側は待ちの始まりを知らないので、先に来たイベントを貯めておけば、送り直さずに済む。しかし、貯めたイベントは新しい実行に続くときに運ばなければならず、待たれないまま残ることもある。二度目のイベントをどう扱うかも決めにくい。待っているあいだだけ受け取り、ほかは拒否すれば、送る側は拒否されたことを知り、いつ送り直すかを自分で決められる(`status` の `events` で待ちが分かる)。
- **イベントをシグナルで受け取ること。** シグナルは送りっぱなしで、ワークフローが受け取ったかどうかを送った側が知れない。待っていないイベントを拒否することもできない。
- **E040 を `check` で出すこと。** はじめはこの形だった。上限はプラットフォームごとに違うので、Temporal だけに出すワークフローが、Step Functions の上限で `check` を通らなかった。Temporal では一番外のループが新しい実行で続くので、同じワークフローでもプラットフォームによって上限に当たるかどうかが変わる。
- **エージェントの応答を、型を包まずにそのまま Schema にすること。** レコードの応答なら包まなくても書けるが、Structured Outputs は一番外にオブジェクトしか取らないので、列挙や文字列やリストの応答は包むしかない。包むかどうかを型で変えると、応答の取り出し方もプラットフォームごとに二通りになるので、いつも `{"answer": …}` に包むことにした。

- **HTML の図も、Mermaid や Graphviz に並べさせること。** ページがネットワークか外部のプログラムに頼り、スクリプトが無ければ何も見えなくなる。Mermaid が描いた SVG のノードの ID は版によって変わるので、実行を光らせるのに当てにできない。`.flow` の入れ子をそのまま使えば、自分で並べられる(5.2)。
- **ノードに名前だけを置く図。** 本文への索引にしかならない(5.2)。
- **ビルドしたもの(ASL、WorkflowTemplate)を図にすること。** プラットフォームごとに違う図になり、ビルドが足したステートが混ざる。Temporal には、図にするものが無い。
- **処理しないエラーを、`on failure` へ向かう辺で描くこと。** ほとんどの呼び出しから辺が出て、図が線で埋まる。呼び出しに印(`!`)を付け、`on failure` の説明に、どの行の呼び出しから来るかを書く。
- **Markdown でも実行を光らせること**(シナリオごとに図を並べる)。図の数がシナリオの数だけ増える。Markdown は PR に貼る要約とし、実行を光らせるのは HTML にした。
- **検査を通らないワークフローを、`doc` が描かないこと**(rulec の `doc` と同じにすること)。そうなる例を図の上で見せられなくなる(5.2)。
- **例の日本語の版を、別のディレクトリ(`examples/ja/…`)に置くこと。** 二つの版を見比べるには、同じディレクトリに並んでいるほうがよい。プラットフォームごとのディレクトリという置き方(例のすぐ下にはどこでも動くものだけ)も、そのまま使える。
- **例の日本語の版で、API の記述が決めている名前まで日本語にすること。** その名前は、送るリクエストと受け取るレスポンスそのものなので、変えると API と合わなくなる。検査もタスクを記述と突き合わせる(E016)。
- **ブラウザで試すページで、規則も書けるようにすること。** dandori の wasm のモジュールは rulec を持たず(P2)、規則は記録した口の答えから読む(5.3)。rulec と一つのモジュールにして規則も書けるようにするのは、ritsu のブラウザのページ(ritsu の PLAN の F.5)の役目である。ritsu のページで書けるようになり、このページはそこへの転送に替えた(0.3)。
- **規則の表を、dandori が描き直すこと**(`rulec certificate` の表から)。証明書の表は、行が受け付ける座標の番号で、書いた人のセルの文字が残らない。描くところが rulec と dandori の二か所になり、片方だけが直ることもある(5.2)。
- **`doc` のページから、規則のページを新しいタブで開くこと。** ブラウザで試すページでは、`doc` のページそのものが sandbox の iframe の中にあり、別のウィンドウを開けない。ページの上に重ねて開けば、ファイルから開いても、サイトでも、試すページの中でも同じに動く(5.2)。
- **バンドルに、rulec の CLI の出力をそのまま入れること。** 前はそうしていた(`schema`・`certificate`・`api` の JSON)。いまの記録は口の答えそのもの(事実と描いたページ)で、dandori が読むものと同じなので、dandori が読むところを増やしても、ページだけが違う答えを返すことはない。
- **dandori と rulec の機能も表せる、protobuf のスーパーセットの仕様記述言語を作り、そこからワークフローとクライアントのスタブ、サービスとクライアントのスタブ、規則のコードを生成すること。** 契約を proto に一本化するという向きはとり、その形は捨てた(1.12〜1.14)。理由は四つある。
  - proto のツール(protoc、buf の lint・breaking・generate、エディタの LSP、各言語のプラグイン)は、知らない構文で止まる。標準の `.proto` に変換して出すなら、外が読むのはその出力で、スーパーセットを読むのは自分のツールだけになる。それは proto のスーパーセットではなく、proto の構文を抱え込んだ自前の言語で、抱え込むのは proto の文法の全部(proto2・proto3・editions、オプション、拡張、`import public`、予約、well-known types)である。rulec も dandori も、いまは要るところだけを読む小さなコードを、依存なしで持っている。そこに三つ目として、文法の全部を読むコードを持つことになる。
  - proto の型には、二つの言語が守っている単位・税区分・率の刻み・範囲・丸めが無い。カスタムのオプションに書いても、実行時にはどの言語のスタブもそれを守らない。rulec は、自前のドメインオブジェクトの型体系を作らないことを譲らず、契約は作らずに突き合わせると決めている(rulec の設計書の 15.59 と 15.132)。スーパーセットは、proto のツールが分からない意味を、proto の中に書くことになり、その線を越える。引当と発送の例でも、注文の金額は `money[JPY, incl_tax]` のまま `.flow` に書き、`.proto` の `int32` とは形で突き合わせている(1.14)。
  - proto はインターフェースの言語で、振る舞いを書かない。`.flow` の本体と `.rule` の表は振る舞いである。proto の形の中に置いても proto のツールは何もしてくれず、検査・`doc`・ブラウザで試すページの土台になっている `.flow` の小ささだけが崩れる。
  - 先行する例は、どれも標準の proto にオプションで印を付けている。protoc-gen-go-temporal は、メソッドの `(temporal.v1.workflow)` に signal・query・update を結び、`(temporal.v1.activity)` にタイムアウトとリトライを書き、Go のクライアント、ワーカー、CLI、Nexus の補助を出す。そのファイルは protoc と buf がそのまま読む。Restate は、初めは gRPC の枠でサービスを定義させていたが、TypeScript の SDK の 0.9.0(2024-04-25)で gRPC を公開の API から外し、1.0(2024-06-07)で、コードでサービスを定義する API にした。いまの proto は、Go 向けの任意のコード生成(`protoc-gen-go-restate`)に残り、`(dev.restate.sdk.go.service_type) = WORKFLOW` でワークフローを印す(どれも 2026-10-01 に GitHub のリリースとソースで確かめた)。

  欲しいのは一つの言語ではなく、一つの契約である。契約は標準の proto に置き、`.flow` と `.rule` はそれを読む側と実装する側に回る。サービスとクライアントのスタブは、標準の `.proto` から `buf generate` が出す。規則のコードと、規則の Connect のサービスは `rulec gen` が出す。dandori が足すのは、記述から型を作ること(1.12)、規則をサービスとして呼ぶこと(1.13)、サービスを実装すること(1.14)の三つである。
- **規則のサービスの表のバージョンが、検査したときの表と違えば、呼び出しを失敗にすること。** 表のどんな直しも、それを呼ぶワークフローの走っている実行を止め、規則を一か所に置く意味が無くなる。バージョンが違っても、結果の型と範囲は結果ごとに確かめている(1.13)。
- **規則の Connect の `.proto`(`rulec gen` の出力)を、dandori が読むこと。** dandori が rulec を読むのは口からだけという P2 が崩れる。要るものは口の答えにあり(列挙の値の名前も)、読んだ名前が `.proto` と一致することはテストで確かめる(1.13、5 章)。
- **取り込んだ列挙の値の名前を、buf の慣わしから組み立てること。** 列挙の名前を大文字の snake case にして値の別名とつなぐ形は、rulec 自身の列挙と、慣わしどおりの契約でしか合わない。契約は、値に接頭辞を付けないことも、0 番に本当の値を置くこともできる。外れた名前は、rulec 0.21.2 のサービスでは 0 番の値として読まれ、黙って別の値で判断される。0.22.0 のサービスはエラーにするが、それでも呼び出しは失敗する。名前を決めているのは、契約と、それを規則の列挙と突き合わせた rulec なので、rulec が言う名前を読む(前は、名前を言わない rulec(0.21.2 まで)では、取り込んだ列挙を持つ規則を呼ばなかった。ritsu の中では、口がいつも名前を言う。1.13)。
- **契約の `.proto` を dandori が自分で読んで、列挙の値の名前を確かめること。** `import proto` が指すファイルを dandori も読めば、rulec に頼らずに名前を確かめられるように見える。しかし、ファイルをどこから探すかは rulec が決めていて、`rulec api` と `rulec gen` でさえ探す場所が違ったことがある(5.1)。規則の列挙と契約の値を突き合わせたのも rulec で(rulec の設計書の 15.59。値の集合がずれていれば E032)、その結果を dandori が読み直すと、二つの読み手が食い違ったときにどちらが正しいかを言えない。P2 のとおり、rulec が口で言う名前だけを読む。
- **規則のサービスのエラーを、Connect のコードでリトライするかどうか分けること。** 規則のリトライの並びが、同梱か Connect かで変わる。同じ入力には同じエラーが返るものをリトライしても、規則は純関数なので害は無い(1.13)。
- **記述の型を、`.proto` の全部から前もって作ること。** 使わない型まで型の一覧と生成物に入り、大きな記述で重くなる。フローが書いた型と、そこからたどれる型だけを作る(1.12)。
- **作る列挙から、ゼロ値をいつも落とすこと、またはいつも残すこと。** いつも落とすと、`STATUS_ACTIVE = 0` のような本当の値を黙って捨てる。いつも残すと、値が無いという印のために、どの `match` にも分岐が要る。名前が「値が無い」ことを言うときだけ落とす(1.12。rulec の設計書の 15.59 と同じ)。
- **64 ビットの整数を、入口や結果で数に直して読むこと。** protobuf の JSON を読む側は文字列も数も読むが、dandori の数は JSON の数(倍精度)で運ぶので、2^53 を超える ID を黙って丸める。記述の 64 ビットの整数は、突き合わせのとおり `string` で受ける(1.10、1.12)。規則の結果の数だけは数に直す。同梱の規則の結果も、いまから JSON の数で運んでいて(`rules.ts` の `Number(…)`)、Connect で呼んでも同じ値にするためである(1.13)。
- **サービスのメソッドのメッセージを名前で決めること**(リクエストに `google.protobuf.Empty`、状態のレスポンスに `dandori.v1.Status` だけを許すこと)。buf の標準の lint に三つの規則で当たり、dandori が名前を用意しても、二つのサービスが同じメッセージを使えば当たる(1.14)。形で比べる。
- **サービスに、フローが待つイベントとコールバックの全部を書かせること。** クライアントが送らないものまで契約に入り、日本語の状態を運ぶイベントのように、`.proto` で書けないものもある。書いたものは書いたとおりに実装させ、クライアントが受け取るもの(出力、失敗の名前、状態)は全部を書かせる(1.14)。
- **サービスのリクエストの、値があるかを持つフィールドを、いつも `T?` で受けさせること**(E016 と同じ読み方)。proto3 では、要るものをメッセージのフィールドで運ぶと、それだけで値があるかを持つ。どのフローも入口で `none` を分けるか、`buf.validate` の `required` のために Protovalidate への依存を足すことになる。サービスはワークフローの入口の決まりなので、値があるかはワークフローが決め、無ければ入口で拒否する(1.14)。
- **状態のクエリが返す `cases` を `map<string, string>` にし、始まっていない案件をそこから外すこと。** いまのクエリが返す形と、それを読む `client.ts` と `client.py` の型が変わる。`google.protobuf.Value` にすれば、いまクエリが返すもの(始まっていない案件は null)をそのまま protobuf の JSON として読める(1.14)。
- **キーワードを `provides` にすること、`implements` を独立した宣言の行にすること。** `provides` は、コンテナに何かを登録して差し出す意味によく使われ、実装がインターフェースに合うかを確かめる意味が出ない。`implements` は、TypeScript や Java で、クラスがインターフェースに合うことをコンパイラが確かめる語である。独立した行にすると、二つ書いたときの誤りを別に言うことになる。`workflow` の行に置けば、文法が一つだけを許し、サービスのオプションが繰り返す名前とバージョンとも並ぶ(1.14)。
- **Argo で、入口のゼロ値を `toPairs` と `fromPairs` で組み直して埋めること。** Argo v4.1.4 の式で `fromPairs` は使えるが、作る map の鍵が文字列の型にならず、`toJson` が書けなかった(`json: unsupported type: map[interface {}]interface {}`)。`sprig.set` で埋める式を並べる形にした(4.4)。
- **Argo で、入口のゼロ値を `sprig.set` を入れ子に重ねて埋めること。** 式は短くなるが、中のメッセージやリストが無いときにも、その鍵を `null` で足してしまう。`json` のフィールドは鍵があるかで確かめるので、無い値を受け取ったときの終わり方が、参照インタプリタとほかのプラットフォームと変わる。鍵が無いときは書かないように条件を付けると、重ねた式が条件の両側に現れ、フィールドの数だけ式が倍々に長くなる。`sprig.set` が map をその場で書き換えることを使い、埋める式を並べたリストの最後に値を置く形にした(4.4)。
- **レコードが自分自身を含むことを許し、再帰する型を全プラットフォームで扱えるようにすること。** 木のような値は書きやすくなる。しかし Step Functions の型の確かめは、展開した式なので、ある深さで打ち切るしかない。深さの先の値は確かめられず、実行で入ってくる値を境界で確かめる(P5)という決まりが、その型でだけ崩れる。シナリオの生成と Argo のジェネレーターも、再帰する型を扱うように書き直すことになる。含むところを `json` にしたレコードなら、どのプラットフォームでも同じに運べる(1.2)。
- **読めない import があるとき、`use proto` ごとエラーにすること。** 手元に無いファイルが一つあるだけで、使わない型を含む記述の全体が使えなくなる。オプションのためだけに import するファイルは多く、型には関わらない。飛ばして記録し、そこにある型に当たったところで言う(1.10)。
- **読めなかった import の型を、`json` として黙って運ぶこと。** タスクの引数やレスポンスが記述に合うかを確かめないまま通り、記述との突き合わせの意味が薄れる。分からない型は分からないと言い、`json` にするか、ファイルを足すかは書いた人が決める(1.10)。
- **規則のサービスのレスポンスの数を、どんな長さの十進の文字列からも数に直すこと。** JavaScript と JSONata の数は倍精度なので、2^53 を超える整数が別の数に丸まり、プラットフォームによって結果が変わる。16 桁までで、2^53 − 1 を超えないものだけを数に直し、ほかは残して結果の検査でエラーにする(1.13)。

- **日付をワークフローのコードの中で計算すること。** koyomi の生成したコードは純関数で、履歴も増えないが、カレンダーの祝日の表は毎年変わるので、表を入れ替えたワーカーで再生が食い違う(ritsu の DESIGN 7.8)。規則と同じくアクティビティにした(1.16)。
- **参照インタプリタで、帳簿の操作を chobo の参照インタプリタに答えさせること。** 帳簿の答えは、帳簿に入っているもの(在庫の量、前の操作)で決まり、それはワークフローの外にある。ワークフローを確かめるには、どの答えも起こりうるものとしてシナリオが選び、拒否される場合も通したい。chobo に答えさせると、初めの中身を決めないと答えが決まらず、拒否される場合を通るシナリオを作りにくい。帳簿の振る舞いそのものは chobo が確かめ、dandori は、書くコードが chobo のクライアントを正しく呼ぶことを、本物のデータベースで chobo の参照インタプリタと比べて確かめる(5 章)。
- **仮押さえのレコードを、操作のあとに帳簿から読むこと**(クライアントの `status`)。呼び出し一つにデータベースの往復が二つになり、そのあいだに期限が切れたかどうかで答えが揺れる。操作が通ったなら状態は操作で決まるので、渡した引数から作る(1.16)。
- **帳簿のタスクに `key` を書かせること。** 帳簿の操作は、帳簿に書いたキーで一度しか通らない。dandori の冪等キー(実行と呼び出しの場所から作るもの)を重ねても、帳簿は使わない(1.16)。
- **仮押さえの案件に `external expire` と `refused when refused = true` を書かせること**(chobo の DESIGN 5 章の案)。帳簿を読めば分かることをもう一度書かせると、書き忘れたときに検査が期限切れを数えない。拒否は理由ごとに違うので、一つの `refused as` にまとめると、どの理由で拒否されたかをフローが分けられない(2.7)。
- **`dates` と `book` をキーワードにすること。** どちらも名前としてよく使う語なので、`agent` と同じく置き場所でだけ読む語にした(1.16)。
- **`now` を、走らせる側が入力で渡すこと。** 再生でも同じ時刻になり、プラットフォームごとの読み方も要らないが、ワークフローの途中の時刻(待ったあと、リトライのあと)を読めず、仮押さえを作った日を言う(ritsu の DESIGN 7.7)には足りない。

- **案件ごとに、状態の集合を一つだけ持つこと。** はじめの検査は、案件のレコードが言う状態と、外部のサービスで案件がいる状態を分けず、一つの集合で持っていた。そのため、失敗した呼び出しのあとの `match` が、レコードで選んだ分岐の中で、案件がいる状態まで絞ってしまい、E020 を見落とした。2.3 の `capture_timeout.flow` を前の検査は ok とし、取消の拒否を受ける `on unexpected_state` は「ここでは動かない」とまで言っていた(W102)。ritsu の Lean のモデルが組の形でこの flow を通さなかったことから分かり、組の形に替えた(2.1)。替えると、例のうち三つが同じ見落としを持っていた。ホテルの予約は、確定がタイムアウトしたあと(Temporal 版では、キャンセルされたあとも)取消が拒否されると `pass` していた。注文の Temporal 版は、出荷を頼むあいだにキャンセルされ、取消が拒否されると `pass` していた。請求は、出荷の確定が失敗したあとの取消が `already_posted` で拒否されうるのに、それを受けていなかった。どれも、拒否されたら引き渡すか、拒否の理由を受けるように直した。前の検査がそろえるよう求めていた分岐のうち、レコードが言うはずのない状態の分岐(ホテルの予約の `on failure` と `on cancel` の `succeeded` と `canceled`、注文の `on cancel` の `cancelled`、請求の `on failure` の `posted`・`voided`・`expired`)も外した。テストのフローでは `tests/flows/dates_and_books.flow` が請求と同じ形だったので同じく直し、`tests/fixtures/book_refusals.flow` は新しい E022 と E011 をそのまま golden に入れた。

## 7. まだやっていないこと

- 案件を始める呼び出しが、外部のサービスで案件を作ったあとに失敗した場合を、E020 で数えること。いまは案件を始まっていないものとして扱い、W103 で `key` を勧めるだけである(2.3)。ritsu の Lean のモデルも、この読み方を定理の前提にしている(ritsu の DESIGN 11.2 の 4)。数えるようにするなら、始める呼び出しの失敗が `on failure` の `none => pass` に着くフロー(例のホテルの予約もこの形)を直し、Lean の前提も外す。
- 別々の本体を同時に走らせる Parallel。案件を二つの枝から同時に動かすことは、並行の問題になるのでエラーにする予定。
- API の記述(1.10)の、まだ読まないところ。OpenAPI の YAML と、記述の外を指す `$ref`、文字列の長さや `pattern`(Smithy の `@length` や `@pattern` も)、Stripe の `expand` で展開を頼んだときのレスポンス、protobuf のバイナリの符号化と、Connect の GET とストリーム、Connect のエラーの `details`。
- ネストした実行が宣言したエラーを、Step Functions で見分けること(AWS の上で Cause の形を確かめてから)。
- Argo の注釈の値(`workflows.argoproj.io/description`)の U+0085・U+2028・U+2029 を、YAML の `\u` のエスケープにすること。いまは JSON の書き方のまま文字そのものが入り、引用符の外には出ないが、YAML 1.1 の読み手は空白に折りたたむことがある(4.7)。
- 生成するコードのコメントに入るほかの文(`for … in …` の式の表示、規則の前提の文、`.proto` のファイルの名前など)に、改行を持てる文字列が届くかを確かめること。説明の文とファイルの名前のほかは、まだ確かめていない(4.7)。
- ワークフローが自分で持つ案件(rulec の生成した関数を呼んで次の状態を得る形)。
- 変数の範囲を、流れに沿って(値を入れた場所ごとに)求めること。いまは、変数に入れるすべての値の範囲を合わせる(1.3)。
- 子の `.flow`(1.9)を、本物の子と親で走らせる確かめを、Temporal のほかのプラットフォームでもすること(いまは子のスタブで突き合わせ、本物どうしは Temporal だけ)。pydantic-graph で、子のグラフを親のプロセスの中でそのまま走らせること(いまは利用者が書く関数)。
- 規則を ASL の中に JSONata として埋めること。rulec の側の新しいプラットフォームになり、数を 2^53 までに収める証明が要る。
- durable functions の突き合わせのシナリオでタイムアウトを起こすこと(規則と日付の上限の時間切れと、コールバックの ID を渡す部分の時間切れだけは、上限の試験が本当の時間で起こす。5 章)。
- durable functions の Python(`aws_durable_execution_sdk_python`)への出力。
- AWS の上(Step Functions の TestState と、durable functions の CloudDurableTestRunner)での確認と、`Transport` の既定の実装を本物の AWS と HTTP の本物の送信先に向ける確認(いまはローカルのモックと moto まで)。Temporal は dev server の上で確かめているが、本番の構成や Temporal Cloud の上では確かめていない。
- LocalStack の今のバージョンでの突き合わせ。今のバージョンのイメージ(2026.8.4)は HTTP Task の実装を持つので、resource を替えずに流せるかもしれないが、起動に認証トークン(無料の Hobby のプランでも、アカウント)が要る。4.14.0 の扱い(5 章)のうち、式の評価の失敗の名前とモックの `States.` のエラーが、今のバージョンで直っているかも見ていない。
- 実行中の案件が、規則やワークフローの改定でどうなるか(rulec の `diff` と `replay` を実行履歴に当てる)。
- `build` の JSON 出力。
- Argo Workflows(名前で再開する suspend のステップ)と pydantic-graph(`Deps` に名前で届く値)の `event`。
- 待ちの途中でも受け取るイベント(受け取ったら流れを変える、Temporal のシグナルやアップデートのハンドラにあたるもの)。いまの `event` は、流れの一か所で待つものだけである。
- Temporal で、一番外にないループ(ループの中のループ、`match` の中のループ)でも新しい実行で続けること。続きの実行に、外のループのイテレーションと、どの枝にいたかも渡すことになる。
- Argo で、タスクのタイムアウトを含むシナリオを起こすこと(Pod をタイムアウトで止める仕組みが要る。上限の試験だけは、規則と日付の呼び出しと、caller で渡すコールバックの時間切れを演じる回で、`image` のコールバックの期限を本物の Pod で起こす。5 章)。
- `event` と `callback` の期限を、変数に入れ直した時刻(`let t = due.at`)からも X5 が数えること(16.7)。
- Argo の `on cancel`(`argo stop` で走る exit handler として)と、pydantic-graph の `on cancel`(グラフを走らせるタスクのキャンセルを受け取って)。
- 待ち(`wait`)のあいだに届くキャンセルのシナリオ。いまのシナリオは、呼び出しのあいだに届くキャンセルだけを演じる(検査は待ちも数える)。
- Argo で、`workflow template` の子が宣言したエラーを見分けること。子の `dd_error` は子の Workflow の出力に残るので、resource のステップで読めるはずだが、失敗した resource のステップが出力を集めるかを確かめていない。いまは子の失敗の理由(`Cause`)も空で届き、参照インタプリタの Argo の見え方もそうしている(20.7)。
- `caller/` のイメージを、本物の Lambda・HTTP・AWS に向けて Argo から動かす確認。
- Argo の突き合わせのうち Pod を演じる回では、Argo の executor(init と wait のコンテナ、ファイルからの出力の読み取り)とコンテナの実行が走らない。それを通るのは、フローごとに一本の本物の Pod の回だけである。
- Argo で、`break` のあとのイテレーションもノードを作ること。イテレーションの数だけ Workflow が大きくなる。
- TypeScript と Python と Go の生成したワークフローが、どの実行でも同じコマンドを同じ順で出すこと(いまはキャンセルやタイムアウトを通る実行で違う。5.1)。そうなれば、別々の言語のワーカーを同じキューに混ぜられる。
- Go 版で、タスクごとに引数の構造体を書くこと(いまはレコードと列挙の型だけで、引数は `map[string]any` のまま受け取り、`Decode` で読む)。
- Go 版の既定の `Transport` で、いま知っている 13 のサービス(TypeScript 版と同じもの)のほかの AWS の API も呼べるようにすること。Go には import を使うときまで遅らせる仕組みが無いので、フローが呼ぶサービスの SDK のパッケージを、ビルドのときに書き足す形になる。
- pydantic-graph の実行を、止まったあとに続けること。2.x が状態を外に置く手段を持たないので、今はプロセスの中の実行だけにしている。
- エージェントを OpenAI と Anthropic の API に本当に向ける確認。構造化出力が生成した Schema を受け付けるか(Claude では、文法の大きさの上限に当たらないかも)、Step Functions の HTTP Task と EventBridge の接続で呼べるか、どちらも確かめていない。
- エージェントの拒否や API のエラー(429 など)を、名前の付いたエラーとして処理すること。いまは `failure` にまとめている。
- Step Functions の HTTP Task と EventBridge の接続から、本物の Jev と OpenAI の Decisions API を呼ぶ確認(既定の `Transport` からは、キーがあれば送っている)。日本語の入力での精度は、いくつかの文で試しただけである。
- 判断のモデルへの画像の入力。OpenAI の Decisions API はユーザーのメッセージの中の画像(base64 の data URL、128 枚まで)を、Ollama の System One の API は `images`(Clef と Clef Flash のモデル)を受けるが、dandori の型に画像が無いので、入力はいまどおり引数の文字列と値だけにした。Ollama の `keep_alive`(モデルを読み込んだままにする長さ)も送らない。
- Decisions API の `safety_identifier`(エンドユーザーの識別子)は送らない。dandori のタスクには、それを書く場所がまだ無い。
- Claude のエージェントを、Anthropic の API のほかの場所(ゲートウェイなど)に送ること。Open Responses の適合テスト(openresponses.org の acceptance tests)を、dandori が送るリクエストと読むレスポンスに当てること。
- OpenAI が走らせる Agents API(長く動くエージェント)を、コールバックで待つ呼び出し方。
- エージェントにツールを持たせること、指示をファイルから読むこと、エフォートのほかのモデルの設定(温度など)を `.flow` に書くこと。
- ブラウザで試すページ(いまは ritsu のページ。0.3)で、`dandori run` を走らせること。規則を書くことと、直したフローをリンクで人に渡すことは、ritsu のページでできるようになった。
- 案件のステートマシンそのものの図に、どのタスクがどの遷移を起こすか、どの遷移が外部のサービスで起きるかを重ねること。いまの `doc` は、呼び出しのあとの状態と終わり方の状態を表で出すだけである。
- `doc` のページで、子の `.flow` の図を親の図から開くこと。
- `doc` のページで分岐を選び、そこを通るシナリオを作ること。
- OpenAPI と Smithy の記述から型を作ること(1.12 と同じ `<API>.<名前>` の書き方で、OpenAPI のスキーマと Smithy の shape から)。いまは `.proto` からだけ作る。
- Protovalidate の、数の範囲と `required` のほかの規則(文字列の長さ、リストの件数、`in`、CEL)を読むこと。いまは 32 ビットの整数の範囲と `required` だけを読む(1.12)。
- ペイロードのコンバーター:TypeScript の SDK の `ProtobufJsonPayloadConverter` や、Go の SDK の既定のコンバーターが proto のメッセージを書く `json/protobuf` のペイロードを、ワークフローが読むこと。いまは `json/plain` の protojson だけを読む(1.14)。
- ほかの言語のクライアントを書く protoc のプラグイン(`dandori/v1/options.proto` の印を読み、Temporal のクライアントの関数を書く。protoc-gen-go-temporal と同じ形)。
- Temporal の Nexus のオペレーションとして、サービスを出すこと。
- サービスのメソッドを Connect や gRPC で受けて、実行を始めたりイベントを送ったりする入口(ゲートウェイ)。いまは、どのメソッドも Temporal のクライアントから呼ぶ。
- 結果を返す `event` のメソッド(Temporal の Update の結果)。いまのイベントは、受け取ったかどうかだけを返す。
- `dandori/v1/options.proto` の拡張の番号を、protobuf の拡張の登録簿に載せること。いまは組織の中で使う範囲の 50700〜50704 である。
- 規則のサービスが使った表のハッシュ(`rulec-source-sha256`)と `trace` を、実行の記録に残すこと(1.13)。
- 無いことがある入力と出力(`T?`)を持つ規則を呼ぶこと。いまは `use rule` が E005 にする(1.13)。生成するコードが、規則のコードに null を渡し、規則のコードが返す null を読めるようにして、`.flow` の型を `T?` にすることになる。
- 要素の並びをたどる規則(rulec の `elements`)を呼ぶこと。いまは `use rule` が E005 にする(1.13)。並びの入力を、規則の要素のフィールド(名前、型、範囲)から作るレコードのリストとして型にし、同梱のコードには rulec の関数が受け取る形で、Connect のサービスには `element_fields` の形で渡すことになる。並びの合計の範囲のような前提(`rulec api` の `preconditions`)を、渡す前に確かめることも要る。
- 規則のサービスを GET で呼ぶこと(`idempotency_level = NO_SIDE_EFFECTS` なので GET で呼べ、キャッシュにも載る)。いまは POST だけで呼ぶ。
- Argo と pydantic-graph で、実行がいまどこにいるかを答えること(サービスの `status`)。
- テスト全体の負荷の中でしか出ない揺れのうち、直していないもの(5.1)。
  - Argo で、演じた Pod のノードが 90 秒進まずに演じ直した回と、10 分の制限に着いた回(2026-10-01 まで)。12 章で突き止めた二つ(クラスタの遅さと、コントローラーが書き込みの失敗をやり直さないこと)のどちらかだった見込みが高いが、その回の記録が無く、確かめていない。
  - Argo のコントローラーが、全体の途中で自分から終わって立ち上がり直すこと。リーダーのリースの更新が、クライアントのレート制限や etcd の遅れで期限を過ぎ、リーダーを失って終わる(二回のログから分かった)。`tools/argo/setup.sh` でリーダー選出を止めるか、リースの期限やクライアントの上限を延ばせば防げるはずだが、試していない。
  - Temporal の終わりのクエリが、案件の状態を一つ前のものや null で答えること。そのテストだけを走らせても、負荷をかけても出なかったので、原因は確かめていない。ランナーは尋ね直す。
- Temporal のランナーに、一本ごとの上限を付けること。生成した Python のワークフローが例外を投げ続けると、SDK はワークフロータスクを終わりなく試し直すので、ランナーが終わらない(読みをわざと誤らせて確かめた)。TypeScript の SDK も決まりは同じだが、ランナーでは確かめていない。
- 言語をまたぐ検査の答え(ritsu の DESIGN 7.6・7.7。X4 の、数の範囲で絞った、拒否されうる理由と、X5 の、仮押さえが必ず期限切れになる・ならないという答え)を、dandori 自身の検査(E022)に返すこと。いまは ritsu-cross が `ritsu check` で言い(dandori は `crossings` で事実を渡す。0.3)、dandori の検査は、これまでどおり帳簿の検査が言う理由で数え、期限が切れうるものとして `expired` の処理を求める。
- 日付と時刻の範囲のうち、まだ書けないものと運ばないもの(20.7)。`.flow` の `range from koyomi`(koyomi の日付の日だけを受け取る入力)、`.proto` から作るレコードの日付と時刻の範囲、規則の日付の出力から来る日(rulec の口が日付の出力を数えない)。シナリオが、二つ目より後の `now` を読む文で期間を出る実行を作ること(いまの時刻の並びは、走り始めとそれより後の二つまで)。
- 帳簿の残高(クライアントの `balance`)と仮押さえの状態(`status`)を読むタスク、PostgreSQL の帳簿の期限切れを片付ける `expire()` をワークフローから呼ぶこと。
- 帳簿の操作を、生成したワークフローを通して(Temporal の dev server などの上で)本物の帳簿に送ること。いまは、既定の `Transport` と Step Functions の Lambda 関数のコードを、本物のデータベースの上のクライアントで確かめるところまでである(5 章)。
- 帳簿の Lambda 関数で、データベースにつなぐところ(`make_handler` に渡す関数)を dandori が書くこと。いまは利用者が書く。
- TypeScript の型で、`succeed` で省いた `T?` の出力を通すこと。いまの `run` の型は、省いた出力を `tsc --strict` が拒否する(請求の例は `due = none` と書いて避けた)。
- 生成する Python が、どのフローでも `mypy --strict` を通ること(0.3)。いまは、ネストした関数やラムダの中で読む `let` の変数(`T | None` で宣言する)と、サービスを実装するフローのクライアントの型(`TypedDict` の入力を `dict[str, Any]` を受け取る `start` に渡す)と、子のワークフローを名前の文字列で呼ぶところを、`mypy --strict` がエラーにする。変数の宣言の型を、流れに沿った検査が分かっていること(代入のあとでしか読まない)から決める必要がある。

## 8. 単位の記号を ritsu-units の字で読む(2026-10-21)

### 8.1 きっかけ

sekisho で、属性の型に `temperature[℃]` を書けないことがわかり(sekisho の DESIGN 17 章)、単位を書く言語の字句をそろえた。dandori は D.4 で、`℃` と `℉` を、型の `[` のすぐあとと範囲の端の単位として読めるようにしてあった(0.3)。全角の `%` は読まず、範囲の端(`<=50%`)と率の刻み(`rate[step 0.1%]`)が E001(「unexpected character `%`」)だった。rulec と kaname は `%` を `%` として読む。

### 8.2 原因

字句の読み手(`src/syntax.rs`)が、単位の記号を自分の決まり(`is_temperature` と `'%'`)で持っていて、rulec の単位の字と食い違っていた。

### 8.3 決定

- 単位の記号は ritsu-units の `chars` から読む(ritsu の DESIGN 5.1)。範囲の端の単位は、記号一つ(`%`、`%`、`℃`、`℉`)か、名前に使える字の続き(`kg`、`万円`)で、全角の `%` は `%` として読む。率の刻みの `%` も `%` として読み、型は表の綴りで書く(`rate[step 0.1%]` は `rate[step 0.1%]`)。
- `℃` と `℉` を型の `[` のすぐあとでだけ語にし、名前の一部にしないことは、前と同じである(`chars::is_word_sign`)。
- 率の型の `[` の中に `%` だけを書いた `rate[%]` は、前と同じく E002 で、注を、率の型には `rate[step 1%]` のように刻みを書くことにした。前は、型の書き方の一般の注(「型は int・string・…」)だった。rulec も `rate[%]` を読まない(★、sekisho の DESIGN 17.3)。

### 8.4 捨てたもの

- 範囲の端の、名前に使える字の続きを、rulec の字(`chars::is_unit_tail`)で切ること。いまは名前に使える字の続きを読むので、`mass[g]` の `>=5日` は E003(「the bound `5日` is not a value of `mass[g]`」)になる。rulec の字で切ると `日` が単位にならず、同じ書き誤りが E001(「a unit is expected after `5`」)という別の文になる。

### 8.5 変わること

- 範囲の端と率の刻みの全角の `%` が `%` として読める。前は E001 で止まった。
- `rate[%]` の E002 の注が変わる。ほかの型の書き誤りの注は変わらない。
- 前に検査を通ったフローは、検査も生成物も変わらない。

### 8.6 確かめたこと

- `tests/units.rs` に、℉ の型と範囲、全角の `%` の範囲と刻み、`rate[%]` の注を足した。
- `tests/fixtures/cold_chain.flow` と日本語の版 `cold_chain.ja.flow` は、荷室の温度を ℃ で、湿度を 1% 刻みで規則に渡し、検査を通って七つの生成先に組める(診断の golden は空)。`cold_chain_units.flow` と日本語の版は、℉ の温度と 0.1% 刻みの湿度を渡し、E003 を二つ言う。規則は `tests/fixtures/rules/cold_chain_departure.rule` と `出発の可否.rule`(sekisho の試験の材料と同じもの)で、規則を Connect のサービスとして呼ぶ `tests/protos.rs` の試験にも入る。
- ritsu の `tests/cross.rs`、`tests/gen.rs`、`tests/units.rs`(sekisho の DESIGN 17.6)。
- 直す前と後の突き合わせ。替える前のバイナリ(0.33.0 のまま組んだもの)と後のバイナリで、木の `.flow` の全部(193。足した 4 を含む)の `check`(英語と日本語と JSON)と、検査を通るものの `build` の七つの生成先(書いたファイルの中身まで)と `doc` を比べた。6,764 の出力が、一字も変わらない。足した 4 は `℃` と `℉` だけを使い、それは前から読めた。全角の `%` は `tests/units.rs` の材料にだけある。

### 8.7 見つけて直していないもの

- 温度の型を持つフローを、プラットフォームの上で走らせる材料(tests/flows のフロー)は無い。温度の値も、ほかの単位と同じく、型の単位で数えた JSON の整数で運ばれ、生成物は単位の綴りに依らない。

## 9. 範囲を書いた入力ごとの、範囲の外の実行と、図の数の範囲(2026-10-21)

### 9.1 きっかけ

1.20 で、順序のある列挙の範囲を書いた入力には、範囲のすぐ外の値にした入力の実行(`input:outside`)をシナリオに一本足した。生成したコードの入口の確かめを、どのプラットフォームでも範囲の外の値で試すためである。数の範囲を書いた入力には足しておらず、`dandori doc` の入力と出力の一覧も、列挙の範囲だけを添えていた(どちらも 7 章に残していた)。そのため、数の範囲の入口の確かめは、どのプラットフォームでも範囲の外の値で一度も試されていなかった。Step Functions の入口の確かめから、ワークフローの入力の数の範囲の比べを外しても、ASL の突き合わせの 633 本はすべて通った。図も、`quota: int` のように型だけを言い、ワークフローが入口で値を範囲に絞ることが読めなかった。

### 9.2 原因

- シナリオの生成は、範囲の外の入力を、列挙の値でしか作っていなかった(`outside_input` が見るのは列挙の型だけだった)。範囲を書いた場所が入力やフィールドにいくつあっても、作るのは最初の一つの分だけだった。
- 図は、入力と出力の型に添える範囲を、列挙の範囲に限っていた(`doc::typed`)。数の範囲は前から出しておらず、列挙の範囲を足したときもそのままにした。

### 9.3 決定

- **範囲を書いた場所の一つずつに、範囲の外の入力の実行を一本ずつ足す。** 場所は、数、順序のある列挙の値、日付、時刻(20 章)に範囲を書いた入力と、入力のレコードのフィールド(どの深さでも。リストは最初の項目、`T?` は中の値)で、入力とフィールドを宣言した順に並べる。実行の入力は、生成器が残した実行のうち、その場所まで値のある最初の実行(レコードがある、リストが空でない)の入力で、その場所だけを範囲のすぐ外の値にする。そういう実行が無い場所には足さない。いまの例とテストのフローには、そういう場所は無い。呼び出しの結果は持たず、どのプラットフォームも、何も呼ばずに `Dandori.BadInput` で失敗する。一つの実行で範囲の外にする場所を一か所にするのは、一か所でも範囲の外なら実行は入口で止まり、同じ実行で範囲の外にしたほかの場所の確かめは、どのプラットフォームでも試されないからである。
- **範囲のすぐ外の値は、数も列挙と同じ決め方で選ぶ。** 上の端のすぐ上(列挙なら上の端の次の値)を選び、上の端が無いか、列挙の範囲が最後の値までなら、下の端のすぐ下(列挙なら前の値)を選ぶ。片方の端だけの範囲は、書いた端の外になる(`>=1` なら 0、`<=5` なら 6)。日付は上の端の次の日(下の端なら前の日)、時刻は範囲のすぐあとの最初の秒(下の端なら前の最後の秒)にする(20 章)。範囲の外の結果は、これまでどおり、数なら下の端のすぐ下(下の端が無ければ上の端のすぐ上)を返す(`Range::beyond`)。両端のある数の範囲では、入力の実行が上の端の比べを、結果の実行が下の端の比べを試すことになる。
- **図の入力と出力の一覧に、数の範囲も添える。** 列挙の範囲と同じ形で、型のあとに `range` と範囲を書く(`quota: int range >=1 <=1000`)。端は E014 の文と同じく、型の単位で数えた整数で書く。`.flow` に `range >=0USD <=100000USD` と書いた `money[USD]` は `range >=0 <=100000` になる。Markdown の冒頭の文にも、HTML の start の詳細にも添える(5.2)。
- **検査と生成物は変えない。** 前は通っていたフローの検査の答えと、七つの生成先が書くものは変わらない。変わるのはシナリオと図だけである。

### 9.4 捨てたもの

- **数の範囲と列挙の範囲で、それぞれ最初の場所にだけ一本ずつ足すこと。** 1.20 の決め方を数に広げるだけで済むが、二つ目からあとの場所の入口の確かめは、試されないまま残る。入力そのものの確かめと、入力のレコードのフィールドの確かめ(レコードを確かめる関数)、リストの項目の確かめは、生成するコードの別々のところにある。
- **一つの場所の、範囲の両端の外をそれぞれ試すこと。** 実行が場所ごとに二本になる。上の端と下の端の比べは、入力の実行と範囲の外の結果の実行で、どのプラットフォームでも試される。
- **範囲の外の入力の数も、範囲の外の結果と同じく下の端のすぐ下から選ぶこと。** 入力の実行も結果の実行も、下の端の比べしか試さなくなる。
- **図の範囲の端を、`.flow` に書いた単位のまま書くこと**(`>=0USD`、`<=50%`)。E014 や W104 の文は範囲を型の単位で数えた整数で言うので、図だけ書き方が変わる。書いた単位を残すには、端を読んだときの書き方も範囲に持たせることになる。

### 9.5 変わること

- 範囲を書いた入力を持つフローのシナリオに、範囲を書いた場所の数だけ、範囲の外の入力の実行が増える。前からある実行は、中身も順も変わらない。足した実行は、生成器が残した実行(と、サービスの必要な入力の無い実行)のあと、`json` の入力を省いた実行の前に入るので、その実行の名前(`run <番号>`)が後ろへずれる。列挙の範囲を書いた入力を持つフローも、場所が二つ以上あれば増える(`tests/flows/lift.flow` は、範囲の外の入力の実行が一本から三本になった)。
- `dandori doc` の入力と出力の一覧に、数の範囲も出る。HTML のページには、足した実行が `Dandori.BadInput` で終わる実行として並ぶ。
- 検査の答えと生成物は変わらない。前は通っていたフローは、この変更で落ちない。
- platforms の段の突き合わせ(Temporal の三つの言語、Argo、LocalStack)にも、同じ実行が増える。

### 9.6 確かめたこと

- `tests/examples.rs` に `every_input_held_to_a_range_is_tried_just_outside_it` を足した。例と `tests/flows` のうち範囲を書いた入力を持つ 26 本のフローの、36 の場所のそれぞれに、範囲の外の入力の実行が場所の順に一本ずつある。その入力は、生成器が残した実行の入力の、その場所だけを範囲のすぐ外の値にしたもので、参照インタプリタは、どれも何も呼ばずに `Dandori.BadInput` で終える。数の場所を作らない、数を下の端から選ぶ、最初の場所にしか作らない、の三つの誤りをわざと入れると、どれも落ちる。
- tools の段の突き合わせ。Step Functions(JSONata)の 659 本(前は 633 本)、durable functions、pydantic-graph で、どの実行も参照インタプリタと同じに終わる。ritsu の Lean のモデル(`DandoriCore`)とも、61 本のフローの全部のシナリオで一致した。Step Functions の入口の確かめから、ワークフローの入力の数の範囲の比べをわざと外すと、前のシナリオでは突き合わせが通り、いまのシナリオでは `examples/payout/payout.flow` の範囲の外の実行で食い違う(入口を通って `pay` を呼ぶ)。
- Temporal(TypeScript・Python・Go)と Argo の突き合わせは、この変更では走らせていない。代わりに、生成した TypeScript・Python・Go の入力の確かめ(`is_WorkflowInput`)を、範囲を書いた入力を持つ例と `tests/flows`・`tests/children` の 28 本のフローの、全部のシナリオの入力(632)で直接評価した。どれも参照インタプリタと同じに受け付けた・受け付けなかった(受け付けなかったのは 44。範囲の外が 38、サービスの必要な入力の無いものが 6)。サービスを実装するフローの入力は、生成したワークフローと同じく、ゼロ値を埋めてから評価した。Argo の入口の確かめは、結果の確かめと同じ関数(`argo.rs` の `check`)で作るが、評価していない。
- 図。`tests/doc` の golden(`payout`・`preconditions`・`quota` の英日)と、サイトの例のページ(`hotel`・`fulfillment`・`invoice`・`payout` の英日)を取り直した。Mermaid 11 と 12 で全部の図が描け、Chrome で開いたホテルの予約のページは、データどおりに光る(選ぶ四つの実行のうち最後のものが、範囲の外の入力の実行になった)。
- 替える前と後の突き合わせ。木の 221 本の `.flow` を、替える前のバイナリ(この直しの前の木のまま組んだもの)と後のバイナリの `ritsu dandori` にかけた。`check`(英語・日本語のテキストと JSON の英日、終了コード)、`build`(七つの生成先。書いたファイルを名前と SHA-256 で)、`doc`(Markdown と HTML の英日)、`scenarios` の 16 の出力、3,536 である。変わったのは 58 本の 220 で、`check` と `build` は一字も変わらない。`scenarios` が変わったのは 50 本で、範囲の外の入力の実行が 66 本増え、前からの実行は中身も順も同じだった(`json` の入力を省いた実行の名前が一つ後ろへずれたフローが 10 本)。`doc` の Markdown が変わったのは、入力か出力に数の範囲を書いた 27 本(検査を通らない 8 本を含む)で、変わったのは冒頭の文の範囲だけだった。HTML は 58 本で、その一覧と、足した実行だけが変わった。

### 9.7 見つけて直していないもの

- 生成したコードが失敗に付ける理由(`the execution's input does not have the declared shape`、結果なら `the answer from … does not have the declared shape`)は、範囲の外の値を型の合わない値と区別しない。図の HTML の説明は、`Dandori.BadInput` を「宣言した型か範囲に合わない入力」(英語では an input not of the declared type or range)、`Dandori.BadResponse` を「宣言した型か範囲に合わない結果」と言う(20 章)。
- HTML のページの実行の一覧は、実行の入力を見せない。足した実行はどれも呼び出しが無いので、どの場所を範囲の外にした実行かは、`dandori scenarios` の出力を読まないと分からない。

## 10. 出力の無い規則(`enums` のファイル)を呼ぶフローを止める(2026-10-22)

### 10.1 きっかけ

rulec の `enums` のファイル(rulec の設計書の §15.212)からは、列挙の型のモジュールしか生成されない。関数も Connect のサービスも無い。rulec の口は 0.34.0 から、`enums` のファイルに Connect のサービスを答えない(§15.258)。そのため、`use rule … connect` で呼ぶフローは、`use rule` の行の E005(rulec がサービスを言わない)で止まるようになった。ところが、規則のコードを同梱する呼び方(`connect` を書かない `use rule`。`lambda`、`local` も同じ)では、`dandori check` が通り、`build` が、生成されない関数を読み込むコードを書いた。Temporal の TypeScript と Argo の `rules.ts` は `import { regions } from "./rulec/typescript/regions"`、Temporal の Python と pydantic-graph の `rules.py` は `from .rulec.python.regions import regions`、Go の `rules.go` は `regions.Regions(regions.Input{})`、Step Functions と durable functions の Lambda 関数は `from regions import regions` を書き、どれも組めない。`rulec gen` が `enums` のファイルから書くのは、列挙の型と、その値を読む関数(`parseRegion`)だけである。そのフローを子として走らせる親のフローも通っていた。

### 10.2 原因

rulec の口は、`enums` のファイルにも、ほかの規則と同じ形の呼び出し方(`RuleFacts` の `typescript`・`python`・`go`。モジュールと関数の名前)を答える。dandori は、規則を呼ぶところで、その規則に出力があるかを見ずに、口の呼び出し方どおりのコードを書いていた。出力の無い規則を呼んだ結果は、フィールドの無いレコード(`regions.outputs`)として型が付き、結果を読まなければ、どの検査にも当たらなかった。`connect` で呼ぶときだけ止まったのは、口が答えないサービスの形を読みに行ったからで、出力が無いことを見たからではない。

### 10.3 決定

- **出力の無い規則を呼ぶところを、E005 で止める。** 呼び方(規則のコードを同梱する、`lambda`、`local`、`connect`)によらず同じにする。`let` を付けない呼び出しも、案件への呼び出しも、E009 や E008 より先にこれで止まる。診断は呼んだ名前のところに出し、文は「規則 `regions` には出力が無いので、呼べません」、注は「出力の無い規則は `enums` のファイルで、中にあるのは規則が読む列挙だけです。rulec は、このファイルから呼べる関数を生成しません。列挙は `regions.region` のように型として使ってください」とする(`tests/fixtures/enums_files.flow`)。コードを E005 にしたのは、`connect` で呼んだときに出ていたコードにそろえるためである。台帳の E005 の見出しを「規則、日付のファイル、帳簿を読めないか、規則を呼べません」にし、いつ出るかと直し方にも足した。
- **出力の無い規則を `enums` のファイルとして扱う。** 口は規則のファイルの種類を言わない。しかし rulec の `rule` のファイルは出力を一つは宣言し(rulec の E009)、`contribution` のファイルは、足す先の規則と一つにして読まれて、その規則の出力を持つ。出力の無い規則は `enums` のファイルだけである。dandori は、口がすでに答えている出力の並びだけで決め、口には何も足さない。日付のファイルの日付と、帳簿の仮押さえは、規則の形で持っていても規則ではないので、当たらない(`RuleUse::answers_nothing` は rulec の規則だけを見る)。
- **列挙を型として使うだけなら通す。** `<規則>.<列挙>` を、レコードのフィールド、入力と出力、リストの要素、`T?`、タスクの引数と結果の型に書き、`match` で分けるフローは、前と同じに通る。生成したコードは、列挙を自分の型(`regions_region`)として書き、rulec が生成するものを何も読み込まない。どのプラットフォームでも、規則を呼ぶコードは、フローが呼ぶ規則についてだけ書くからである。kaname も同じ線引きで、規則を呼ぶところと、その出力を型に書くところだけを止める。sekisho は、ゲートの計算した値がいつも規則の出力を読む(`regions().region`)ので、出力の無い規則は、出力の名前のところの E101(「規則 `regions` に出力 `region` はありません」、注「出力は一つも宣言されていません。」)で止まる。
- **`connect` を書いた `use rule` も、`use rule` の行では止めない。** 出力の無い規則については、`connect` が呼ぶサービスの形を読みに行かない。呼べば、ほかの呼び方と同じく呼ぶところで E005 になり、呼ばなければ、型を使うだけのフローとして通る。`connect` の URL の形(http:// か https://)、`lambda` との重なり、`connection` の E007 は、前と同じに見る。
- **呼んだ結果のフィールドを読むところには、何も重ねない。** 前は、`let r = regions()` のあとの `r.region` が「`regions.outputs` にフィールド `region` はありません(あるのは )」(E002)になった。呼ぶところの E005 が誤りを言うので、出力の無い規則の結果のフィールドを読むところでは、診断を出さない。そうした結果を持つ変数はその呼び出しからしか生まれないので、E005 の無いまま検査を通ることは無い。

### 10.4 捨てたもの

- **`use rule` の行で止めること**(要素の並びをたどる規則や、`T?` を持つ規則と同じ形)。型を使うだけのフローも止まってしまう。`enums` のファイルを `use rule` で読んで列挙を型に使うのは正しい使い方で、生成したコードにも害が無い。
- **口に、規則のファイルの種類か、呼べるかどうかを足すこと。** 口は rulec のもので、`enums` のファイルに呼び出し方を答えないようにするかも、rulec の側で決めることである。出力の並びは、いまの口がすでに答えている。
- **`connect` のときだけ、`use rule` の行の E005 を残すこと。** 呼ぶフローには、一つの誤りに E005 が二つ出る。型を使うだけのフローが、呼び方の句のうち `connect` を書いたときだけ止まり、`lambda` や `local` を書いたときは通ることにもなる。
- **出力の無い規則の呼び出しを、`let` を付けない規則の呼び出しの E009 や、何も返さないタスクを `let` で受ける E003 に寄せること。** E009 の文(「`let` で結果を受け取ってください」)は、`enums` のファイルでは直し方にならない。E003 は値の型の誤りで、呼べるものが生成されないことを言わない。
- **呼ばない出力の無い規則に、呼び方の句(`lambda`・`local`・`connect`)を書いたことを警告すること。** 呼ばない普通の規則に書いた句にも、いまは何も言わない。

### 10.5 変わること

- 出力の無い規則を呼ぶフローは、前は通っていたものも、`check` が E005 で止まる(終了コード 1)。そうしたフローの生成物は、もともと組めなかった。そのフローを子として走らせる親のフローは、E015(子が検査を通らない。注は子の E005)で止まる。
- `connect` を書いた `use rule` で出力の無い規則を呼ぶフローの E005 は、`use rule` の行から呼ぶところに移り、文と注が変わる。呼ばずに列挙を型として使うだけのフローは、通るようになる。
- 出力の無い規則を `let` を付けずに呼ぶところは、E009 から E005 になる。呼んだ結果のフィールドを読むところの E002 は出なくなる。
- 台帳の E005 の見出し、いつ出るか、直し方が変わる。
- 出力のある規則を呼ぶフローと、`connect` を書かずに列挙を型として使うだけのフローの、検査と生成物は変わらない。

### 10.6 確かめたこと

- `tests/fixtures/enums_files.flow` と日本語の版 `enums_files.ja.flow` は、`enums` のファイル(`tests/fixtures/rules/regions.rule` と `配送の地域.rule`。ritsu のテストのプロジェクト `regions` の規則と同じもの)を、規則のコードを同梱して、`lambda` で、`local` で、`connect` で呼び、`let` を付けずにも呼んで、呼ぶところごとに E005 を一つずつ言う(五つ)。同梱で呼んだ結果のフィールドを読む `succeed` には、何も出ない。`enums_file_types.flow` と日本語の版は、同じ四つの `use rule` の列挙を、レコードのフィールド、入力と出力、リストの要素、`T?` の入力、タスクの引数と結果に書いて `match` で分け、検査を通って七つの生成先に組める(診断の golden は空)。生成したコードは rulec のものを何も読み込まず、TypeScript(Temporal、durable functions、Argo)は `tsc --strict` を、Go は `go build` と `go vet` を通り、Python(Temporal、pydantic-graph)は読み込める(手で一度確かめた)。
- `tests/protos.rs` で、サービスの形を `rulec gen` の `.proto` と比べる規則から、出力の無い規則を外した。呼ぶところが無く、サービスも無いからである。
- ritsu の `tests/enums_files.rs`。`connect` で呼ぶフローと同梱で呼ぶフロー(英日)が、呼ぶところの E005 一つで止まる。列挙を型として使うだけのフロー(英日)が検査を通って七つの生成先に組め、書いたファイルのどれも rulec のものを読まない。材料は `tests/projects/regions` の `connect.flow`・`bundled.flow`・`types.flow` と日本語の版。
- 替える前と後の突き合わせ。木の 231 本の `.flow`(足した 8 本を含む)を、替える前のバイナリ(この直しの前の木のまま組んだもの)と後のバイナリの `ritsu dandori` にかけた。`check`(英語・日本語のテキストと JSON の英日、終了コード)、`build`(七つの生成先。書いたファイルを名前と大きさと SHA-256 で)、`doc`(Markdown と HTML の英日)、`scenarios` の 16 の出力、3,696 である。変わったのは 8 本の 128 で、ほかの 223 本は一字も変わらない。変わったのは、足した材料のうち 6 本と、ritsu の材料の `connect.flow` と日本語の版である。足した `bundled.flow` と日本語の版は、前は通って組めないコードを書き、いまは E005 で止まる。`enums_files.flow` と日本語の版は、前は `use rule` の行の E005 と、`let` を付けない呼び出しの E009 と、結果のフィールドの E002 を言い、いまは呼ぶところの E005 を五つ言う。`enums_file_types.flow` と日本語の版は、前は `connect` を書いた `use rule` の行の E005 で止まり、いまは通って七つの生成先に組める。`connect.flow` と日本語の版は、E005 が `use rule` の行から呼ぶところに移った。足した ritsu の `types.flow` と日本語の版は、前も後も通り、出力は同じである。

### 10.7 見つけて直していないもの

- rulec の口は、`enums` のファイルにも、生成されない関数の呼び出し方(`Call`)を答える。dandori は出力の並びで止めるので、その答えを使わない。
- `dandori doc` の「規則」の節は、`use rule` で読んだ規則を全部「このワークフローが呼ぶ規則」として載せ、列挙を型として使うだけの `enums` のファイルも載せる(`rulec doc` のページは、列挙だけを描く)。型を使うだけの普通の規則も、前から同じに載っている。
- フィールドの無いレコードの、無いフィールドを読む E002 の文は、ほかのレコード(フィールドの無い `.proto` のメッセージから作ったものなど)では、いまも「(あるのは )」で終わる(英語は `()`)。

## 11. Argo の式を、形の合わない値で失敗させない(2026-10-23)

### 11.1 きっかけ

CI がスケジュールで回す platforms の段で、Argo の突き合わせ(`argo_runs_as_the_reference_says`)が、2026-10-21 の回から毎回、`tests/flows/lift.flow` の run 11 で食い違って落ちていた。その実行では、タスク `pick_campaign`(結果は `tiers.tier  range >=silver <=gold`)が、宣言した形に合わない結果 `{}` を返す(シナリオの `6:malformed`)。参照インタプリタは `Dandori.BadResponse`(`line 40: the answer from pick_campaign does not have the declared shape`)で失敗する。Argo のワークフローは、呼び出しの並びと結果は同じなのに、`dd_error` が `null` のままワークフローの失敗で終わり(`child '…' failed`)、ランナーはそれを `Argo.Failed` と読んだ。Step Functions(LocalStack)と Temporal の三つの言語の突き合わせは、同じ実行を参照インタプリタと同じに終えていた。

### 11.2 原因

- 呼び出しの結果を読むテンプレート(値を計算するテンプレート `s<場所>-after`)の出力の式は、列挙の値を `a in ["silver", "gold"]` で確かめていた。Argo v4.1.4 の式(expr-lang 1.17.8)は、文字列の定数だけを並べた一覧への `in` を、左の値の型を見ずに、その文字列をキーにした map で探す形に置き換える(optimizer の `inArray`)。探す値が文字列でも `nil` でもない(map、リスト、数、真偽値)と、キーの型に合わず、式の評価が失敗する(`reflect.Value.MapIndex: value of type map[string]interface {} is not assignable to type string`)。
- 出力の式の評価が失敗すると、Argo はそのノードをエラーにし、`continueOn` があってもステップグループと、その上のテンプレートとワークフローを失敗させる。`dd_ctl` と `dd_error` は書かれず、最後のコンテナ(`dd-end`)も走らない。kind の上で、lift.flow の run 11 のワークフローの after のノードに上の文が出ていることを確かめた。
- 列挙をこの形で確かめるのは前からで、1.20 で変えたのは、並べる値を範囲に入る値にしたことだけである。範囲の無い列挙でも同じに失敗する(11.6)。それまでの突き合わせに出なかったのは、結果そのものが列挙のタスクに `{}` を返す実行が無かったからである。Argo で走る例とテストのフローで、列挙を返すタスクは Jev のタスクだけで、その結果は、呼び出し側のプログラム(`caller/`)が Jev の応答から作る。レコードの結果では、`{}` にフィールドが無いので、列挙のフィールドの確かめまで進まない。lift.flow の `pick_campaign`(日本語の版では `キャンペーンを選ぶ`)が、Jev でも帳簿でもない、列挙を返す最初のタスクだった。
- 同じ仕組みの穴が、ほかに三つあった。どれも、外から来た値を、文字列か map だと決めてかかって読む式である。
  - 入口。ワークフローの入力の列挙(入力のレコードのフィールドやリストの項目も)に文字列でない値を渡すと、始まりのテンプレートの式が失敗し、`Dandori.BadInput` にならない。入口の確かめは、結果の確かめと同じ関数(`argo.rs` の `check`)で作るからである。
  - error.json。`image` のタスクが終了コード 3 で終わり、`/tmp/dandori/error.json` にオブジェクトでないもの(JSON の文字列、数、リスト)を書くと、`e.error` を読むところで式が失敗する。`error` が文字列でなければ、宣言したエラーの名前の一覧への `in` で失敗する。
  - コールバックの応答。`argo node set` で入れた応答がオブジェクトでない(JSON の文字列、数、リスト、JSON でない文字)か、`error` が文字列でないと、`"error" in wv`、`wv.ok`、宣言したエラーの名前の一覧への `in` のどれかで失敗する。

### 11.3 決定

- **値を確かめる式は、どんな JSON の値にも真か偽を返す形で書く。** 確かめが偽なら、ワークフローは参照インタプリタと同じエラーで終わる。式が失敗すると、`dd_error` も `on` も `on failure` も働かない。4.4 にもこの決まりを書いた。
- **列挙の値は、文字列であることを先に確かめる**(`(type(x) == "string" && x in […])`)。expr-lang の `&&` は、左が偽なら右を評価しないので、文字列でない値は、map で探す前に偽になる。入口の確かめも、タスク・規則・子のワークフロー・コールバックの結果の確かめもこの関数で作るので、レコードのフィールド、`T?`、リストの項目の列挙も、範囲のある列挙も無い列挙も、同じに直る。
- **error.json とコールバックの応答は、オブジェクトのときだけフィールドを読み、エラーの名前は文字列のときだけ、宣言した名前と比べる。** 形の合う値の読み方は変えない。失敗していた値は、形の合う値のうちいちばん近いものと同じに読む。
  - オブジェクトでない error.json と、`error` が文字列でない error.json は、宣言したエラーではない。宣言していない名前を書いたときと同じく `failure` で、理由(`Cause`)は、ファイルがオブジェクトなら `message`、でなければ空の文字列である。
  - オブジェクトでないコールバックの応答(JSON でない文字を含む)は、`error` も `ok` も持たない応答として読む。前から `{}` はそう読んでいて、結果が無いので、結果が `T?` でなければ `Dandori.BadResponse` になる。`error` が文字列でない応答は `failure` である。
- **ほかの六つの生成先は変えない。** TypeScript の `includes`、Python のタプルへの `in`、Go の `ddIsOneOf`、JSONata の `in` は、文字列でない値に偽を返す。

### 11.4 捨てたもの

- **列挙を、`in` でなく `==` を `||` でつないで確かめること**(`x == "silver" || x == "gold"`)。map で探す形にはならないが、値の多い列挙では式が長くなり、型の違う値どうしの `==` の振る舞いにも頼ることになる。型を先に確かめる形なら、文字列(`type(x) == "string"`)や数(`type(x) in ["int", "float"]`)の確かめと同じ書き方になる。
- **失敗した式を、ワークフローの終わり方の読み方で受けること**(失敗したノードのメッセージを読む、exit handler で `dd_error` を書く)。どの呼び出しのどの値で失敗したかは読めても、参照インタプリタのエラーの名前と理由は作れず、`on failure` も走らない。式が失敗しないようにすれば、ほかの生成先と同じ振る舞いになる。
- **入口、error.json、コールバックの応答の穴を、別の直しに回すこと。** 原因は同じ式の書き方で、どれもワークフローが、参照インタプリタの言うエラーではなく、ワークフローの失敗で終わる。

### 11.5 変わること

- Argo の WorkflowTemplate の、始まりのテンプレートと、呼び出しの結果を読むテンプレートの出力の式が変わる。形の合う値には、どの式も前と同じ値を出す(11.6)。形の合わない値では、前はワークフローが失敗していた(ランナーは `Argo.Failed` と読んだ)ところが、`Dandori.BadInput`、`Dandori.BadResponse`、`failure` のどれかで終わる。前は通っていた実行は、この変更で落ちない。
- YAML は、木の例とテストのフローで合わせて 2.8% 大きくなる。いちばん大きいのはホテルの予約で、`examples/hotel/aws/hotel.flow` が 133,277 バイトから 137,735 バイトに、日本語の版が 134,085 バイトから 138,536 バイトになる。ノードの数は変わらない(E040 の見積もりも同じ)。
- `caller/`、ほかの六つの生成先、検査、シナリオ、図は変わらない。
- ブラウザで試すページの `website/docs/playground/ritsu.wasm` を作り直した。ページの Argo の生成物が、コマンドの生成物と同じであるためである。

### 11.6 確かめたこと

- platforms の段の `argo_runs_as_the_reference_says` を、kind の上の Argo v4.1.4 で回した。直す前の木では、CI と同じに、lift.flow の手前の 21 本のフローの 375 本の実行が通り、lift.flow の run 11 で、参照インタプリタの `Dandori.BadResponse` と Argo の `Argo.Failed`(`child '…' failed`)が食い違って落ちる。直したあとは、31 本のフローの 547 本の実行と、本物の Pod で走らせ直す 31 本が、参照インタプリタと同じに終わる(SKIP の行なし)。lift.flow と lift.ja.flow は 18 本ずつである。どちらの回のあとも、kind のクラスタにワークフローは残っていない。
- 生成したテンプレートを、kind の上で、外から来る値を変えて直接評価した。テンプレートを、始まりのテンプレートと同じくグローバル出力パラメータを入れるステップのあとに置き、入力を与えて Workflow として走らせる。材料は、`.flow` で宣言した範囲の無い列挙を入力と結果に持ち、`image` のタスクと、`image` の `callback` のタスクが `Busy` を宣言するフローである。直す前と後の生成物に、同じ値を流した。
  - 入口:列挙の入力に `{}`・`5`・`["gold"]`・`true` を渡すと、直す前は式が失敗し、直したあとは `Dandori.BadInput` になる。列挙の値、列挙に無い文字列、入力そのものが `{}`・`[]`・`"x"` のときは、前も後も同じ(`next` か `Dandori.BadInput`)である。
  - `image` のタスクの結果:`{}`・`[]`・`5`・`true` で、直す前は失敗し、直したあとは `Dandori.BadResponse`。列挙の値と、列挙に無い文字列は前と同じである。
  - error.json(終了コード 3):`{"error": 5}`・`"oops"`・`[1]` で、直す前は失敗し、直したあとは `failure`(コールバックのタスクの ID を渡すステップでも、`"oops"` で同じだった)。`{"error": "Busy"}` は `Busy`、宣言していない名前と JSON でないファイルは `failure` で、前と同じ。終了コード 1 と 143 も前と同じ(`failure` と `timeout`)である。
  - コールバックの応答:`{"ok": {}}`・`"str"`・`5`・`[1]`・JSON でない文字と、待ちが成功しなかったとき(応答が空)で、直す前は失敗し、直したあとは `Dandori.BadResponse`。`{"error": 5}` は、直す前は失敗し、直したあとは `failure`。`{"ok": "gold"}`、`{"error": "Busy"}`、宣言していない名前、`{}`、期限切れ、ID を渡すステップの失敗は、前と同じである。
  - expr-lang の `in` も、それだけで確かめた。`a in ["silver", "gold"]` は、`a` が map・リスト・数・真偽値のとき失敗し、文字列と `null` では失敗しない。`type(a) == "string"` を先に置くと、どの値でも失敗しない。同じステップグループのステップは、一つ目の失敗で、`continueOn` があってもグループごと失敗し、ほかのステップは評価されない。
- 替える前と後の突き合わせ。木の 231 本の `.flow` を、替える前のバイナリ(この直しの前の木のまま組んだもの)と後のバイナリの `ritsu dandori` にかけた。`check`(英語・日本語のテキストと JSON の英日、終了コード)、`build`(七つの生成先。書いたファイルの中身まで)、`doc`(Markdown と HTML の英日)、`scenarios` の 16 の出力、3,696 である。変わったのは 97 本の Argo の生成物だけで、変わったファイルはどれも WorkflowTemplate の YAML だった。直したあとの YAML から、入れた確かめ(列挙の値の型 903 か所、error.json とエラーの名前の型 1,630 か所、理由を読むところ 352 か所、コールバックの応答 99 か所)を取り除くと、97 本とも前の YAML と一字も同じになる。
- tools の段の、Argo の生成物を読む試験。dandori の `tests/comments.rs` の二つ、`diagnostics_match_the_golden_files`、`generated_typescript_type_checks`、ritsu の `tests/dandori.rs` の五つ、`tests/enums_files.rs` の三つ、作り直した `ritsu.wasm` での `tests/playground.rs` の `the_page_answers_as_the_command_does` と `the_module_answers_as_the_library_does`。

### 11.7 見つけて直していないもの

- シナリオの形の合わない値は、結果の `{}`(Connect のタスクと、規則のサービスでは `[]`)だけである。入口に文字列でない値を渡す実行も、`[]` や数を返す結果の実行も無い。直した入口の確かめと、`{}` でない値の読み方は、kind の上で直接評価して確かめただけで、突き合わせの試験では試していない。シナリオに足すと、列挙の入力を持つすべてのフローの実行が増え、七つの生成先の突き合わせと図が変わる。
- error.json とコールバックの応答の形(Argo での受け渡しの形)は、参照インタプリタとシナリオには無く、ランナーはいつも形の合う値を書く。
- `{}` と、オブジェクトでないコールバックの応答は、`ok` を持たない応答として読むので、結果が `T?` のタスクでは `none` を受け取ったことになる(`{}` は前からそう読んでいた)。形の合わない応答として失敗させるかは、決めていない。

## 12. Argo の突き合わせを、混んだクラスタでも終わりまで走らせる(2026-10-24)

### 12.1 きっかけ

2026-10-23 に platforms の段を手で走らせた回で、`argo_runs_as_the_reference_says` が二回とも、ランナー(`tools/argo/run.mjs`)の 10 分の制限で落ちた(`the runs took more than 10 minutes`)。終わっていなかったのは、一回目が `tests/flows/preconditions.ja.flow` の 22 本目、二回目が `tests/flows/decisions.flow` の 19 本目と 20 本目と、本物の Pod で走らせ直した 6 本目である。エラーの文では、どの実行も最後のノード(19 本目は `try`、ほかは `end`)が `Pending` のままで、その Pod は終わっていた。演じた実行では、ランナーが Pod を `Failed` にしてあった。本物の Pod の実行では、コンテナが本当に走って `Failed` になっていた。同じ形は 2026-10-15 にも、`preconditions.flow` の 22 本目で一度出ている。ランナーには、最後に演じた Pod のノードが 90 秒たっても終わらない実行を、名前を変えて演じ直す仕組みがあった(5.1)。どの回でも、演じ直しは出なかった。

### 12.2 原因

同時に流す実行が多すぎて、kind の API サーバーが詰まっていた。そこから二つのことが起きる。どちらも、制限に着いたときのエラーの文は同じ形になる。

- **実行が遅くなる。** ノードを多く通る実行は、ランナーが始めてから 10 分のうちに終わらない。
- **コントローラーが、Pod の終わりを書き損ねたまま放っておく。** Workflow の書き込みが API サーバーで時間切れになると、Argo v4.1.4 のコントローラーは、その書き込みをやり直さない。書き込みに Pod の終わりが入っていて、そのあと Workflow の周りで何も変わらなければ、ノードは Pod が終わったまま `Pending`(か `Running`)に残る。コントローラーがその Workflow をもう一度見るのは、20 分ごとの見直しのときである。

#### 実行が遅くなる

- 終わっていなかった実行は、どれも、そのフローでいちばん時間のかかる実行である。手元の kind で、ノードのコンテナを CPU 2 つに絞って試験の全体を回すと、decisions.flow の 21 本の実行で時間のかかる上から三つは、本物の Pod の 6 本目(277 秒)、20 本目(251 秒)、19 本目(251 秒)だった。preconditions.ja.flow の 25 本でいちばん長いのは 22 本目(208 秒)だった。CPU 4 つでも、上位は同じ実行だった。
- 詰まるのは API サーバーである。CPU 2 つの回では、ノードの CPU のうち、API サーバーが 1.2 前後(0.9〜1.3)、コントローラーが 0.4 前後を使っていた。コントローラーが Workflow を書き換える PUT は 3,525 回あり、一回に平均 2.7 秒かかった。14 本のフローの実行(多いときは 220 本ほど)を同時に流すと、演じた Pod が終わってから、コントローラーがそのノードの終わりを書くまでに、中央値で 20 秒、長いもので 63 秒かかった(コントローラーのログを詳しく出した回で測った。CPU 4 つなら中央値 9 秒、長いもので 42 秒)。ノードを数十通る実行は、それを重ねて数分になる。CI のランナーは 4 コアで、クラスタと、ランナーが演じる Pod ごとに立てる Node のプロセスが、同じコアを分け合う。
- ノードを CPU 1 つに絞ると、直す前の木で、CI と同じに落ちた。`examples/fulfillment/aws/fulfillment.flow` の実行が 10 分で終わらず、エラーの文では、`end` のノードが `Pending` で `dd-end` の Pod が `Failed`、`try` のノードが `Pending` でその Pod が `Succeeded` だった。同じ回で、decisions.flow の本物の Pod の 6 本目は、作られてから 644 秒で終わった。

#### Pod の終わりを書き損ねる

- CPU 1 つで 14 本のフローを同時に流すと、コントローラーの Workflow の書き込みが、一回の試験で 3 回と 36 回、時間切れ(API サーバーの `Timeout: request did not complete within requested timeout` か、etcd の `request timed out`)で失敗した。4 本ずつ流した回では 0〜9 回、CPU 2 つと 4 つの三回では一度も無かった。
- Argo v4.1.4 のコントローラーは、Workflow の書き込みが衝突(Conflict)以外の理由で失敗すると、その回の変更を捨て、やり直しを予約しない(`persistUpdates`。衝突なら、最新の Workflow に変更を当て直す)。次にその Workflow を見るのは、Workflow や Pod や WorkflowTaskResult が変わったときか、その回の処理が予約した見直し(リトライの待ちの終わりなど)か、20 分ごとの見直し(`workflowResyncPeriod`)のときである。Pod の終わりを書くはずだった回が失敗すると、その Pod はもう変わらないので、ほかに何も変わらない実行(`dd-end` の Pod が最後に終わる実行など)は、20 分の見直しまで進まない。
- これを、kind の上で一つずつ確かめた。Pod が一つだけの Workflow を作り、Pod が終わるあいだだけ、コントローラーの Workflow の書き込みをアドミッションポリシーで断らせた。コントローラーの書き込みは一度だけ失敗し、ポリシーを外してから 3 分のあいだ、Workflow は `Running`、ノードは `Pending` のままで、書き込みは一度も無かった。Pod にアノテーションを付けると、2 秒でノードも Workflow も `Succeeded` になった。
- 試験のフローでも同じに起きる。直す前のランナーで、始めてから少しのあいだ書き込みを断らせて流すと、`tests/flows/decisions.flow` では、12 本目・19 本目・20 本目と本物の Pod の 6 本目が、Pod は終わったのにノードが終わらないまま、ランナーの制限に着いた(10 秒後から 30 秒のあいだ断らせ、制限は 6 分にした)。CI の二回目に終わっていなかった実行と、ほとんど同じである。`tests/flows/lift.flow` では、本物の Pod の実行が、`end` のノードが `Running` で `dd-end` の Pod が `Failed` のまま、制限(4 分)に着いた(20 秒後から 40 秒のあいだ断らせた)。
- CPU 1 つの回では、自然に起きた書き損ねも見た。`examples/order/aws/order.flow` の、演じ直した 8 本目で、`dd-end` の Pod が終わった 45 秒後に、コントローラーの書き込みが 34.8 秒待って時間切れになった。そのあと 4 分のあいだ、ノードは `Pending` のまま、Workflow は一度も書き換えられなかった。Workflow にアノテーションを付けると、1 秒後にコントローラーがワークフローを終えた。

#### エラーの文が言わないこと

- 制限に着いたときの文は、その瞬間のノードと Pod である。遅いクラスタでは、Pod が終わってからノードが進むまでのあいだが、どの実行にもある。書き損ねた実行も、同じ形に見える。文だけでは、遅かったのか、止まっていたのかが分からない。
- CI の回の演じた実行で演じ直しが出なかったのは、止まっていなかった(遅かった)か、止まってから制限までが 90 秒より短かったかのどちらかである。本物の Pod の実行(二回目の 6 本目)は、演じ直しの仕組みが見ていなかった。

### 12.3 決定

- **同時に流すフローを、マシンのコアの数までにする。** 14 本を上限にする。数は `DANDORI_AT_ONCE` で変えられる(Temporal の突き合わせと同じ変数)。CI の 4 コアでは 4 本、14 コアの手元では前と同じ 14 本になる。一つのフローの実行は、前と同じに全部同時に流す。同時に動く Workflow が減るので、一つの実行がノードごとに待つ時間が短くなる。
- **Pod の終わりを書き損ねたノードを、コントローラーに知らせる。** ランナーは 15 秒ごとに、終わったのに completed のラベルが付いていない Pod(コントローラーは、ノードの終わりを書いてからラベルを付ける)を拾い、終わってから 1 分たった Pod について、Workflow のノードを読む。ノードがまだ終わっていなければ、Pod にアノテーションを付ける。コントローラーは Pod の変化として受け取り、Workflow を読み直してノードの終わりを書く。演じた実行も本物の Pod の実行も、最後の Pod でなくても、`workflow template` のステップが作った Workflow の Pod も見る。一つの Pod に 1 分おきに三回までで、テストはそのことを `nudged the controller` と出力する。実行はそのまま突き合わせる。
- **演じ直しは最後の手段にする。** 最後に演じた Pod のノードが、演じてから 5 分(前は 90 秒)たっても終わらない実行だけを演じ直す。遅いだけの実行を初めからやり直させないためである。
- **ランナーは、実行が止まっている長さであきらめる。** 前は、ランナーが始めてから 10 分たったとき、終わっていない実行があれば落ちていた。いまは、終わっていない実行のどれかが、ノードの始まりも終わりも無く、その Pod を演じてもいないまま 10 分(`DANDORI_ARGO_MINUTES`)たったときに落ちる。動き続ける実行は、全部で 30 分(その 3 倍)まで待つ。`workflow template` のステップを演じているあいだは、実行は子の Workflow の中で進むので、止まっているとは数えない。
- **あきらめるときの文に、終わっていないそれぞれの実行が最後に動いてから何秒たったかを書く。** 止まっていたのか、遅かったのかが、文から読める。
- **試験の行に、ランナーが始めてから最後の実行が終わるまでの秒数を書く。** CI のランナーの遅さを、落ちる前に読める。

### 12.4 捨てたもの

- **10 分の制限を延ばすだけにすること。** 書き損ねたノードは、20 分の見直しまで進まない。CI のランナーの遅さは回によって違い、どこまで延ばせば足りるかも決められない。
- **演じ直しを広げるだけにすること**(最後の Pod でない Pod も見る、本物の Pod の実行も見る)。書き損ねたのはコントローラーの書き込みだけで、Pod の終わりは API サーバーにある。実行を初めからやり直すより、コントローラーに読み直させるほうが早く、遅いだけの実行を巻き込まない。CPU 1 つで 14 本を同時に流した回では、90 秒で演じ直す仕組みが 332 本の Workflow を作り直し、実行はさらに遅くなった。
- **Workflow にアノテーションを付けて知らせること。** Workflow の変化でもコントローラーは読み直す(12.2 の order.flow の実行はそれで進んだ)。しかし Workflow への書き込みは、コントローラーの書き込みと衝突し、当て直しの手間を増やす。Pod のアノテーションなら、仮にコントローラーがその Pod の変化を取りこぼしていても、いまの Pod がコントローラーに届く。
- **コントローラーの設定を変えること**(workers、qps、見直しの間隔)。詰まっているのは API サーバーで、コントローラーの CPU は 0.4 前後だった。20 分の見直しの間隔は、コントローラーの引数や環境変数では変えられない。
- **制限をなくすこと。** 生成したワークフローの誤りで、動き続けて終わらない実行があれば、試験は CI のジョブの制限まで終わらない。全部で 30 分は、その歯止めである。

### 12.5 変わること

- CI の 4 コアのランナーでは、同時に流すフローが 14 本から 4 本になる。
- ランナーは、終わったのにノードが進まない Pod に、アノテーション `dandori-nudged` を付けることがある。テストの出力には、そのたびに `nudged the controller` の行が出る。
- 演じ直すのは、最後に演じた Pod のノードが 5 分たっても終わらない実行になる(`DANDORI_ARGO_STUCK_SECONDS` の既定が 90 秒から 300 秒になる)。
- `DANDORI_ARGO_MINUTES` は、ランナーの全体の長さではなく、実行が止まっていてよい長さになる(既定は同じ 10 分)。全体の上限は、その 3 倍の 30 分である。
- あきらめるときのエラーの文は、止まった実行があれば `a run stood still for more than 10 minutes`、全体の上限なら `the runs took more than 30 minutes` で始まり、実行ごとに `last moved N seconds ago` が付く。
- 試験の行の終わりに `the last run ended N seconds after the runner started` が付く。
- 生成物、検査、シナリオ、図、ほかのプラットフォームの突き合わせは変わらない。

### 12.6 確かめたこと

- 手元の kind(Argo v4.1.4、ノードは kindest/node v1.37.0)で、ノードのコンテナの CPU を絞り、platforms の段の `argo_runs_as_the_reference_says` の全体を、CI と同じ debug のビルドで回した。コントローラーのログは、CI と同じ warn で、詳しく見た二回だけ info にした。試験の外から、10 秒ごとにコントローラーのメトリクスと、終わったのに completed のラベルの無い Pod を見張った(手元だけのスクリプトで、リポジトリには入れていない)。CI で同時に流すフローの数(4 コアで 4 本)は、`DANDORI_AT_ONCE=4` で与えた。

| 木 | ノードの CPU | 同時のフロー | 結果 | 試験の時間 | いちばん遅いフロー | 演じ直し | 知らせ | 書き込みの時間切れ |
|---|---|---|---|---|---|---|---|---|
| 直す前 | 4(ログは info) | 14 | 通った | 368 秒 | 217 秒 | 0 | — | 0 |
| 直す前 | 2(ログは info) | 14 | 通った | 611 秒 | 340 秒 | 0 | — | 0 |
| 直す前 | 2 | 14 | 通った | 507 秒 | 278 秒 | 0 | — | 0 |
| 直す前 | 1 | 14 | 10 分の制限で落ちた | 1,466 秒 | 646 秒より長い | 157 本 | — | 3 |
| 直したあと | 1 | 4 | 通った | 1,488 秒 | 428 秒 | 0 | 1 | 0 |
| 直したあと | 1 | 4 | 通った | 1,326 秒 | 467 秒 | 0 | 0 | 0 |
| 直したあと | 1 | 4 | 通った | 1,544 秒 | 319 秒 | 0 | 0 | 0 |
| 直したあと | 制限なし | 14 | 通った | 237 秒 | 149 秒 | 0 | 0 | 0 |

  「いちばん遅いフロー」は、フローの最初の Workflow ができてから、最後の実行が終わるまで(直したあとは、試験の行の秒数)である。直す前の CPU 1 つの回は、`fulfillment.flow` のランナーが 10 分で落ち、ほかのフローの実行も、演じ直した分は終わらずに消えた。直したあとの一回目の知らせは `examples/order/aws/order.flow` の 20 本目で、`end` のノードが、Pod が終わってから 69 秒 `Pending` だった(書き込みの失敗は無く、遅かっただけと見ている)。どの回のあとも、kind のクラスタにワークフローは残っていない。
- 途中の形でも測った。知らせを入れる前(演じ直しは 90 秒)に、制限を止まっている長さにし、同時のフローを 4 本にした木では、CPU 1 つで通ったが(1,781 秒)、`fulfillment.flow` の最後の実行は、ランナーが始めてから 651 秒で終わった。前の決まりなら制限に着いていた。書き込みの時間切れは 9 回、演じ直しは 2 本で、そのうち 1 本は時間切れのあとだった。同じ木で同時のフローを 14 本にすると、演じ直しが 332 本の Workflow を作り、decisions.flow の本物の Pod の実行は 1,328 秒かかって、全体の 30 分で落ちた。
- 書き込みを断らせて流した(12.2)。直したあとのランナーで `tests/flows/lift.flow` を、始めてから 5 秒後に 40 秒のあいだ断らせると、演じた実行の 9 本と本物の Pod の実行に一度ずつ知らせ、演じ直しは 0 本で、90 秒で通った。直す前のランナーでは、同じ断らせ方で 5 本を演じ直し、253 秒かかった。`tests/flows/decisions.flow` を、始めてから 10 秒後に 30 秒のあいだ断らせると、直す前のランナーは、12 本目・19 本目・20 本目と本物の Pod の 6 本目が終わらないまま制限(6 分にして流した)に着いた。演じた三本でも、演じ直しは出なかった(演じ直しは、最後に演じた Pod のノードしか見ない)。CI の二回目と同じ形である。直したあとのランナーは、6 本目・19 本目・20 本目と本物の Pod の実行に一度ずつ知らせ、106 秒で通った。
- 試験の行の秒数(`the last run ended N seconds after the runner started`)と知らせの行(`nudged the controller`)は、上の回の出力で確かめた。
- dandori の `tests/docs.rs` の 6 件(サイトのページの引用と、診断のコードの一覧)は通った。


### 12.7 見つけて直していないもの

- Argo のコントローラーが、失敗した Workflow の書き込みをやり直さないこと。ランナーは試験の中で補うだけで、生成したワークフローを本物のクラスタで動かすときは、混んだ API サーバーで同じことが起きうる。コントローラーの決まりで、dandori が書く WorkflowTemplate では避けられない。
- 本物の Pod の実行は、コントローラーの最初の検証で、テンプレートを解決するたびに WorkflowTemplate を API サーバーから読み直す(5 章)。CPU 4 つの回でも、コントローラーが decisions.flow の本物の Pod の実行を初めて処理するのに 53 秒かかり、一回の処理の上限(30 秒)を超えて `Deadline exceeded` を出した。本物の Pod の実行が、フローでいちばん遅くなることが多い理由の一つである。
- CPU 1 つの回では、コントローラーが一度、リーダーのリースを更新できずに立ち上がり直した(etcd の遅れ。7 章)。

## 13. 拒否のエラーだけを並べた `on` に、ほかの失敗のあとの状態を渡さない(2026-10-28)

### 13.1 きっかけ

帳簿の仮押さえを取り消すタスクは、取消を断られる理由を二つ宣言できる(`errors expired, already_posted`。期限が切れているとき、もう確定したとき)。`on failure` で `held` のまま残った仮押さえをこのタスクで取り消し、二つの拒否を一つの `on` で受けると(`on expired, already_posted => pass`)、`check` は、案件が `held` のままワークフローが失敗しうると言った(E020)。拒否ごとに `on` を分けて書くと(`on expired => pass` と `on already_posted => pass`)言わない。二つの書き方は同じ意味である。足した `tests/fixtures/book_refusals_joined.flow` を 0.36.0 で検査すると、49 行目の `on` について次の E020 が出る。

```text
error[E020]: tests/fixtures/book_refusals_joined.flow:45:1: the workflow can fail here with the case `goods` in held, which is not final (posted, voided, expired are); settle it first, or write `leaving goods` to hand it over as it is
    45 |   match goods.state
  the run that gets there:
      33  reserve: goods starts in held
      36  match urgent: true
      37  ship fails (timeout, failure)
      45  on failure
      47  match goods.state: held
      49  put_back fails: on expired, already_posted
      45  `on failure` ends and the workflow fails
```

### 13.2 原因

- 検査は、呼び出しのあとの `on` ごとに、その `on` に入るときの案件の状態を決める。拒否のエラーだけを受ける `on` は、呼び出しがその拒否で返ったときにしか動かないので、拒否が残す状態(`expired` で断られたなら期限切れ、`already_posted` なら確定済み)だけを受け取る。ほかの `on` は、拒否でないエラーで呼び出しが失敗したときの状態も受け取る。そこには、取消が届いたかどうか分からない失敗(タイムアウトなど)のあとの `held` も入る。
- Lean のモデル(`proofs/DandoriCore/Check.lean` の `handlerIn` と `takesOnly`。根の DESIGN 11.2)は、拒否をいくつ並べても、拒否だけを受ける `on` を拒否の状態だけで始める。`flow.rs` は、拒否を一つだけ書いた `on` しか、拒否だけを受ける `on` として扱っていなかった。二つ以上並べた `on` には、拒否でないエラーのあとの状態も渡し、その `pass` のあとに `held` が残った。

### 13.3 決定

- 拒否のエラーだけを並べた `on` は、並べた拒否のどれかで返ったときの状態だけを受け取る(拒否ごとの状態を合わせたもの)。並べた拒否のどれも返りえない呼び出しなら、その `on` には入らない。拒否を一つだけ書いた `on` の扱いは前と同じで、どちらも Lean のモデルと同じ決め方になる。
- 拒否と、拒否でないエラー(`timeout` や `failure`)を並べた `on` の扱いは変えない。`book_refusals_joined.flow` の 41 行目の `on expired, timeout => pass` は、取消がタイムアウトしたときに仮押さえが `held` のままかもしれないので、42 行目の `succeed` で E020 になる。golden の英日にはこの E020 だけが残る。

### 13.4 変わること

- 拒否のエラーを二つ以上並べた `on` のあとの、案件の状態についての診断(E020 など)が、拒否ごとに `on` を分けて書いたときと同じになる。
- 木の 231 本の `.flow` の `check` の出力(英日のテキストと JSON)は、一字も変わらない(14.5 の突き合わせ)。拒否を二つ以上並べた `on` を持つフローが、足したもののほかに無いからである。

### 13.5 確かめたこと

- `tests/fixtures/book_refusals_joined.flow`(帳簿は `tests/fixtures/books/stock.book`)を足し、`diagnostics_match_the_golden_files` の golden(英日)を書いた。0.36.0 では、上の 45 行目の E020 も出る。
- dandori の試験の全体(14.6)。

## 14. `http` のタスクの URL に入れる値を、`encodeURIComponent` と同じにエスケープする(2026-10-28)

### 14.1 きっかけ

`http` のタスクは、URL の `{id}` に引数の値をそのまま入れていた。値に `/`・`?`・`#`・`%`・空白があると、URL の形が変わる。足した `tests/flows/paths.flow` の最初の呼び出しで、0.36.0 の参照インタプリタ(`dandori run`)が出す URL は次のとおりで、生成したコードもこれをそのまま送る。

```text
https://items.example.com/v1/items/a/b c?d#e%f&g=h+i'(j)*!~é品/counts/1002/tags/
```

TypeScript の `fetch` と Python の urllib(生成した Transport の書き方のまま)は、`#` から後ろを送らず、パス `/v1/items/a/b%20c` とクエリ `d` を送る。パスの区切りが一つ増え、`/counts/1002/tags/` は消える。Go の `net/http` は、`%f&` を読めずに送らない(`invalid URL escape "%f&"`)。Step Functions の `ApiEndpoint` も、同じ文字列を作っていた。

### 14.2 原因

- 参照インタプリタ(`render.rs` の `fill_url`)と、URL を組み立てるどの生成先(TypeScript、Python、Go、JSONata)も、値を文字列にしてそのまま URL につないでいた。突き合わせの試験は、参照インタプリタの URL と生成したコードの URL を文字列で比べるので、どちらもエスケープしないことは見つからない。
- シナリオが作る文字列は `<名前>-<番号>` で、エスケープの要る文字は日本語の名前しか無かった(`tests/flows/timeouts.flow` の品番)。本物の HTTP で送る試験(`default_transports_send_what_the_calls_say`)は、届いたパスのエスケープを戻してから URL と比べていたので、`fetch` と urllib が送るときに日本語を UTF-8 でエスケープしても通っていた。
- 無い値(`T?` の none)を入れたときも、生成先によって違った。参照インタプリタと Python と Go は空の文字列を入れ、TypeScript(`String(args[…])`)と JSONata(`&` が `null` を `$string` で文字にする)は `null` という文字を入れていた。URL に `T?` の引数を入れるフローが木に無く、試験に出なかった。

### 14.3 決定

- URL に入れる値は、前と同じ決まりで文字列にしてから(文字列はそのまま、無い値は空の文字列、ほかは JSON)、JavaScript の `encodeURIComponent` と同じにエスケープする。英字・数字と `-_.!~*'()` はそのまま、ほかは UTF-8 のバイトごとに `%` と大文字の 16 進 2 桁にする。`/`・`?`・`#` を含む値も、その値を書いた区切りの中にとどまる。
- 生成先ごとの書き方は次のとおり。どれも、参照インタプリタ(`render::url_segment`)と同じ文字列を作る。

| 生成先 | 書き方 |
|---|---|
| Step Functions | JSONata の `$encodeUrlComponent`。文字列でない値は `$type` で見て、`null` を空の文字列に、ほかを `$string` で文字にしてから渡す。決まった値は dandori がエスケープして書く |
| Temporal(TypeScript)、Lambda durable functions、Argo Workflows | io.ts の `segment`(`encodeURIComponent`) |
| Temporal(Python)、pydantic-graph | io.py の `segment`(`urllib.parse.quote(…, safe="!*'()")`) |
| Temporal(Go) | io.go の `ddEscape`(前からクエリと URL エンコードの本文に使っていた関数) |

- 本物の HTTP で送る試験は、届いたパスが URL の文字列のとおりであることを確かめる。エスケープを戻してから比べるのをやめ、URL そのものに書いた日本語だけを、送る側が UTF-8 でエスケープしたものとして比べる。`%2F` が `/` に戻って届けば落ちる。

### 14.4 捨てたもの

- **エスケープしない書き方を足すこと**(OpenAPI の `allowReserved` のように、値の `/` を区切りとして送る)。値で区切りを増やすフローは例にもテストにも無く、URL に `{a}/{b}` と二つ書けば同じことができる。
- **Transport でエスケープすること。** Transport は組み立てた URL を受け取るので、どこまでが値か分からない。参照インタプリタの URL とも比べられなくなる。

### 14.5 変わること

- `{…}` を書いた URL に、英字・数字と `-_.!~*'()` のほかの文字を含む値を入れると、送る URL が変わる。それだけの値なら、前と同じ URL を送る。`T?` の none は、どの生成先も空の文字列を入れる。
- 生成するコード。木の 231 本の `.flow` を 0.36.0 と直したあとのバイナリの `ritsu dandori` にかけ、16 の出力(`check` の英日のテキストと JSON、七つの生成先の `build`、`doc` の英日の Markdown と HTML、`scenarios`)を比べた。11,913 ファイルのうち 1,120 が変わり、どれも `build` の出力だった。
  - URL に `{…}` を持つ 27 本の、タスクの URL の組み立て(Temporal の三つの言語の activities、durable functions と Argo の `tasks.ts`、pydantic-graph の `tasks.py`)と、Step Functions の 9 本の `ApiEndpoint`。
  - TypeScript と Python のすべての build の io(`segment` を足した)。
  - Temporal の 150 本の build の `BUILD_ID`。生成したコードから作る値なので、io が変われば変わる。ワーカーのバージョニングでは、新しいビルドとして扱われる。
- `dandori run` が出す呼び出しの URL(参照インタプリタ)。
- `check`、`doc`、`scenarios` の出力は変わらない。

### 14.6 確かめたこと

- `tests/flows/paths.flow`(英語)を足した。URL `https://items.example.com/v1/items/{item}/counts/{count}/tags/{tag}` に、決まった文字列(`a/b c?d#e%f&g=h+i'(j)*!~é品`)、値を埋めた文字列(`"{item}/{a} 100%"`)、数、`string?` を入れる。参照インタプリタの URL は `https://items.example.com/v1/items/a%2Fb%20c%3Fd%23e%25f%26g%3Dh%2Bi'(j)*!~%C3%A9%E5%93%81/counts/1002/tags/` になる。
- 5 本の実行が、どの生成先でも参照インタプリタと同じ URL を送った。Step Functions は `tools/asl-run.mjs`(JSONata 2.0.6)と LocalStack 4.14.0 の二つで確かめた。LocalStack は HTTP Task を知らないので、別のリソースに置き換えて引数を評価させる。ほかは、Temporal の三つの言語と、アクティビティをほかの言語で動かす四つの組、Lambda durable functions、Argo Workflows(kind の上の v4.1.4。本物の Pod で 2 本走らせ直す)、pydantic-graph である。本物の HTTP で送る試験では、TypeScript・Python・Go の既定の Transport が、6 件の呼び出しをエスケープしたパスのまま届けた。
- `render.rs` の単体の試験 `an_argument_stays_inside_its_part_of_the_url`。
- dandori の試験の全体を回した。`examples` の 27 件(言語をまたぐ一件は別に回した。358 秒と 127 秒)と、ほかの 13 ファイルである。SKIP の行は三つで、MySQL が無いことと、Ollama が答えないことの二つだった。どれも URL のエスケープとは関わらない。
- 0.36.0 の生成コードが送るものは、生成した Transport と同じ読み方で URL を読ませて確かめた(Node の `URL`、Python の `urllib.parse.quote` と `urlsplit`、Go の `url.Parse`)。JSONata の `"…" & $tag` は、`tag` が `null` のとき `…/tags/null` になる(jsonata 2.0.6)。

### 14.7 見つけて直していないもの

- 本物の Step Functions の HTTP Task が、`ApiEndpoint` のエスケープをそのまま送るかは確かめていない。AWS の文書は `QueryParameters` を URL エンコードすると書くが、`ApiEndpoint` については書いていない。LocalStack 4.14.0 は HTTP Task を動かさないので、Step Functions で確かめたのは、JSONata が作る URL の文字列までである。
- URL のホストの部分に `{…}` を書いても(`https://{tenant}.example.com/`)、同じにエスケープする。ホスト名に使える文字(英字・数字・`-`・`.`)だけの値なら変わらない。

## 15. 仮押さえの期限の検査(X5)で、イベントを待つタスクの `on timeout` を、待ちが終わってから数える(2026-10-28)

### 15.1 きっかけ

仮押さえを作ってから承認をイベントで 15 日待ち(`event`、`timeout 15 days`)、来なければ `on timeout` で取り消すフローを、14 日で切れる仮押さえに使うと、ritsu の X5(根の DESIGN 7.7)は W206(取り消すときに期限が切れているかを決められない)を言った。注は、取消までの長さを「0 秒から 15 日 1 分のあいだ」としていた。`on timeout` が動くのは 15 日たってからなので、取消はどの実行でも期限のあとに来る。E206 を言うべきところである。

### 15.2 原因

`crossings.rs` は、呼び出しのハンドラーを、呼び出しが終わったところから数えていた。最小は呼び出しのあとと同じで、呼び出しの最小は 0 秒である。ふつうのタスクでは、これで正しい。タイムアウトが、タスクの `timeout` より早く来ることがあるからである。Temporal では、ワークフローのワーカーが持つアクティビティはハートビートが 30 秒来なければタイムアウトし、コールバックのタスクは ID を渡すアクティビティ(60 秒)のタイムアウトでも `timeout` になる。イベントを待つタスクは何も呼ばず、ワークフローが自分のタイマー(`timeout`。書かなければ一日)で待つので、タイムアウトはタイマーが切れたときにしか来ない。

### 15.3 決定

- イベントを待つタスクの、`timeout` だけを書いたハンドラーは、呼び出しの始まりからタスクの `timeout`(書かなければ一日)がたってから始まるとし、仮押さえを作ってからの長さの最小にその長さを足す。最大は前と同じである。
- `timeout` とほかのエラーを並べたハンドラー(`on timeout, failure`)と、ほかのタスクのハンドラーは、前と同じに最小を足さない。
- 足したときは、注にその長さと理由を書く(`` `approval` (line 38) waits 15 days for its event before `on timeout` runs ``)。

### 15.4 捨てたもの

- **どのタスクでも、`on timeout` の最小に `timeout` を足すこと。** 15.2 のとおり、タイムアウトはそれより早く来ることがあるので、E206 を言いすぎる。

### 15.5 変わること

- イベントを待つタスクの `on timeout` だけのハンドラーの中で仮押さえを確定・取消するフローで、W206 が E206 になることがある。成り立つとしていたところ(何も言わない)は変わらない。
- E206 の説明(`ritsu explain E206` と `docs/codes.md`)に、イベントを待つタスクの `on timeout` を足した。
- W206 と E206 の日本語の注の「0 秒から8 日」のような長さに、空きを入れた(「0 秒から 8 日」)。

### 15.6 確かめたこと

- ritsu の `tests/cross.rs` の X5 の試験に、15 日のうちに来ない承認を待って `on timeout` で取り消す形を二つ足した。イベントで待つと E206、タスクで待つと W206 で、注は「0 秒から 15 日 1 分のあいだ」である。golden は `crates/ritsu/tests/golden/cross/x5-event-E206` と `x5-task-W206`(英日)で、cross の試験 12 件が通った。
- 木の 231 本の `.flow` の `ritsu dandori` の出力は、この直しでは変わらない(X5 は `ritsu check` が言う)。

## 16. 規則と日付の呼び出しの上限と、時刻までの答えの待ち(`timeout until`)(2026-10-29)

### 16.1 きっかけ

仮押さえを 7 日もたせ、承認を 3 営業日待つ返金のフローを考える。3 営業日は、年末年始をはさむと暦日で 11 日になる(12 月 24 日に受け付ければ 1 月 4 日)ので、仮押さえは承認を待つあいだに切れうる。ritsu の X5(根の DESIGN 7.7)がこれを見つけるはずだったが、二つの理由で見つけられなかった。

- **規則と日付の呼び出しの長さに上限が無かった。** 営業日の待ちは、koyomi の日付を呼んでから `wait until <日付>.at` と書く。X5 は `wait until` を koyomi の日数の幅で数えられるが、規則と日付の呼び出しを「上限なし」と数えたので、仮押さえからの長さの最大が決まらず、何営業日にしても W206(決められない)になった。W206 の見本(`ritsu explain W206`。仮押さえは 60 日)のタスクに `timeout 30 seconds` を書いても、替える前の ritsu は次のように言った。

```text
warning[ritsu W206]: invoice.flow:33:1: Whether the hold of the case `goods` has expired when `ship` posts it cannot be decided
    33 |   goods <- ship(order: order, sku: sku)
  = `ship` comes 17 days 9 hours or more after the hold is made (line 29), and nothing bounds how much more: nothing in the flow bounds how long the call of the date `terms.payment` (line 31) takes; the hold expires 60 days after it is made
```

- **答えの待ちの終わりを、時刻で書けなかった。** `event` と `callback` の `timeout` は、タスクの宣言に書く決まった長さ(`timeout 3 days`)だけで、「受け付けた日から 3 営業日後の 17 時まで承認を待つ」は書けなかった。

### 16.2 原因

- 規則は純関数ですぐ終わるものとして、呼び出しの長さを出力先に任せていた。リトライの並び(`check.rs` の `RULE_RETRIES`。2 回、1 秒と 2 秒の間)は出力先でそろえていたが、一回の上限は Temporal の生成器の定数(`temporal.rs` の `RULE_SECONDS`、`startToCloseTimeout` 10 秒)にしか無かった。Step Functions の Task には `TimeoutSeconds` が無く、durable functions は `context.invoke` で呼んでいて(SDK 2.4.0 の `InvokeConfig` にタイムアウトの設定が無い)、Argo の規則の Pod にも、pydantic-graph と durable functions の `connect` の規則の送信にも、上限が無かった。日付の呼び出しは規則と同じに作るので、同じだった。
- `timeout` の句は長さしか読まなかった(`syntax.rs` の `task_clause`)。

### 16.3 決定

**一、呼び出しの上限。** どの出力先でも、規則と日付の呼び出しの一回に 10 秒の上限を付け、時間切れは失敗と同じく 2 回までリトライする(1 秒と 2 秒の間)。リトライのあとは、ほかのタスクの時間切れと同じく、呼び出しの `on timeout`(無ければ `on failure` か、ワークフローの失敗)に入る。値は `check.rs` の `RULE_SECONDS`・`RULE_RETRIES`・`RULE_RETRY_INTERVAL`・`RULE_RETRY_BACKOFF` の一か所に置き、生成器、参照インタプリタ、X5、`doc` の表、試験がそこを読む(`temporal.rs` の `RULE_SECONDS` は、それを指す定数にした)。X5 は一回の呼び出しを、3 回の 10 秒と 1 秒と 2 秒の待ちで、最大 33 秒と数える(`check::rule_call_most`)。

| 出力先 | 上限の付け方 | 時間切れの届き方 | 前 |
|---|---|---|---|
| Step Functions | 規則と日付の Lambda の Task と、`connect` の HTTP Task に `TimeoutSeconds: 10` | `States.Timeout`(Retry の `States.ALL` と Catch に入る) | 無し |
| Temporal(TypeScript・Python・Go) | 規則と日付のアクティビティの `startToCloseTimeout` 10 秒 | アクティビティのタイムアウト | 同じ(変えていない) |
| Lambda durable functions | step の中で、`Transport` の `lambda`(Invoke)か、`connect` のサービスへの送信を、`dd.within(…, 10)` で包む | `Dandori.Timeout` | Lambda は `context.invoke` で無し、`connect` も無し |
| Argo Workflows | caller(`call.ts`)の 10 秒のタイマー。過ぎれば `error.json` に `Dandori.Timeout` を書き、終了コード 143 で終える | 期限で止められた Pod と同じく `timeout` | 無し |
| pydantic-graph | `connect` の規則は `dd.within(…, 10)`。プロセスの中で計算する規則と日付には付けない | `asyncio.TimeoutError` から `timeout` | 無し |

- **Temporal** は `startToCloseTimeout` のままにした。ワーカーが受け取ってから数え、タスクキューで待つ時間は入らない。`scheduleToCloseTimeout` にすれば入るが、混んだワーカーで今まで通っていた呼び出しが時間切れになる。
- **durable functions** は、規則と日付を `lambda` のタスクと同じ形(step の中で dandori が書く実装が Invoke を呼ぶ)に替えた。変わることが三つある。一つは、invoke の操作と違い、入力が履歴に残らないこと。規則と日付は副作用を持たず、入力は実行の状態から決まるので、再生には困らない。二つ目は、答えを待つあいだ durable の関数が動いたままになること(一回の呼び出しで 33 秒まで)。三つ目は、`dandori run --target durable` の呼び出しの形が、`{"invoke": <関数>, "payload": …}` から `{"lambda": <関数>, "payload": …}`(Step Functions と同じ)に変わること。Invoke のレスポンスには関数のエラーの型が残るので、invoke でエラーの名前が落ちる問題(4.3)にも当たらない。操作の数は一つのままで、E040 の見積もりは変わらない。
- **Argo** は、Pod ではなく caller の中の呼び出しに 10 秒を付けた。Temporal の `startToCloseTimeout` がタスクキューの待ちを数えないのとそろえ、Pod をノードに置いて起動するまでの時間は数えない。Pod の起動だけで 10 秒を超えることは、負荷の下では珍しくない(5.1 の記録では、期限の付いた Pod 16 個のうち 4 個で、作ってからメインのコンテナが終わるまでが 10 秒以上だった)。時間切れは期限で止められた Pod と同じ終了コード 143 で、同じように扱われる(`timeout` の名前、`retryStrategy`、`on timeout`)。Argo で前は通っていた実行が落ちるのは、呼び出しそのものが 10 秒を超えたときだけである。プラットフォームの待ち(Temporal のタスクキュー、Argo の Pod の置き場所と起動)に上限が無いことは、X5 の前提として根の DESIGN 7.7 の ★ に書く。
- **pydantic-graph** の、プロセスの中で計算する規則と日付(`Deps.rules`。rulec と koyomi の Python)には上限を付けない。入出力の無い計算で、規則の表と暦の範囲で必ず終わり、asyncio は同期のコードを途中で止められない(`asyncio.wait_for` は、計算が終わってからしか時間切れに気づかない)。X5 はここも 33 秒と数え、上からの見積もりとして成り立つ。

**二、時刻までの答えの待ち。** タスクの句に、長さの代わりに `timeout until <引数>` と書ける。引数はそのタスクの `timestamp` の引数で、呼ぶ側が時刻を渡す。

```text
task ask_for_approval(application_id: string, by: timestamp) -> Approval
  callback
  timeout until by

flow
  let due = approvals.due(received: now)
  let a = ask_for_approval(application_id: application.id, by: due.at)
    on timeout => …
```

- 書けるのは、答えを待つタスク(`event` と `callback`)だけ(ほかは E007)。仕事をするタスクを暦の時刻で切るのは、答えを待つことと違うからである。
- 答えの待ちは、渡した時刻に終わる。待つ秒数は秒に切り上げ、待ちを始めたときに 1 秒を切っていれば(過ぎていても)1 秒待つ。1 秒は Step Functions の `TimeoutSeconds` が取れるいちばん小さい値で、どの出力先も同じ下限にした。時間切れは `timeout` で、`on timeout` が受ける。
- `callback` のタスクでは、期限の引数もほかの引数と同じく ID と一緒に送る(答える人に期限を見せられる)。`aws` のタスクの引数は API の引数なので、`aws` のコールバックには書けない(E007)。期限を過ぎてからのリトライはすぐまた時間切れになるので、`timeout` をリトライする `retry`(`on` を書かないものも)も書けない(E007)。`retry … on failure` は書ける。
- `event` のタスクは、期限の引数のほかに引数を取らない(E007 の文を替えた)。
- 期限の引数がタスクに無ければ E002、`timestamp` でなければ E003(`timestamp?` も)。`timeout` を二つ書けば E007(前は、あとに書いたものが黙って使われた)。新しい番号は作っていない。

期限の秒数は、どの出力先でも `now` と同じ時計の読み方から出す。runner がすでに `now` をシナリオの時刻に替えているので、その置き換えが期限にもそのまま効く。

| 出力先 | `event` | `callback` | 秒数の出し方 |
|---|---|---|---|
| Temporal TypeScript | `dd.awaitEvent` の待ち | ID を渡すアクティビティのあとの `dd.awaitCallback` の待ち | `runtime.ts` の `secondsUntil`。ワークフローの時計から |
| Temporal Python | `await_event` の待ち | `await_callback` の待ち | `runtime.py` の `seconds_until`。`workflow.now()` から |
| Temporal Go | `awaitEvent` の待ち | `awaitCallback` の待ち | `runtime.go` の `ddSecondsUntil`。`workflow.Now` から |
| Step Functions | 作れない(E050。前と同じ) | `.waitForTaskToken` の Task の `TimeoutSeconds` を JSONata の式にする | `$max([1, $ceil(($toMillis(<時刻>) - $toMillis(<now>)) / 1000)])`。`now` はステートに入った時刻 |
| durable functions | 作れない(E050) | 時計を読む step のあとで `createCallback` の `timeout` に | `runtime.ts` の `secondsUntil` に、step で読んだ時刻を渡す(ワークフローの中の `Date.now()` は再生で変わる) |
| Argo Workflows | 作れない(E050) | ID を渡すステップのあとに秒数を計算するテンプレート(`s<場所>-until`)を置き、待ちの `suspend.duration` にする | `wait until` と同じ式に、切り上げと下限 1 を足したもの。テンプレートに来たときに一度だけ評価される |
| pydantic-graph | 作れない(E050) | ID を渡したあとの `await_callback` の待ち | `runtime.py` の `seconds_until`。`Deps.clock` から |

待ちの始まりは出力先で少し違う。Step Functions と durable functions は ID を渡す前から時計が動き(Task とコールバックの時間切れが、渡すことも含む)、ほかは渡したあとで秒数を出す。どちらも期限の時刻に終わる。違いが出るのは、ID を渡しているあいだに期限が過ぎたときだけで、前の二つはすぐ、ほかは 1 秒後に時間切れになる。渡すこと自体は、どの出力先でも 60 秒までである(17 章)。

- **参照インタプリタ**:答えが `timeout` なら、時間を期限まで進める(`interp::Answers` の `timeout_until`。`ritsu run` の `computed.rs` は、その時点から期限までの秒数を切り上げ、1 秒より短くはせずに時計と帳簿の時間を進め、`ritsu run` の行に「the timeout of the wait until <時刻>」と出す)。規則と日付の呼び出しの時間切れは、10 秒進める。答えがイベントの値か宣言したエラーなら、時間は進めない(前と同じ)。シナリオの探索は変えていない。
- **X5**(`crossings.rs`):期限の引数に渡した式が `x.at` で、`x` が「日付の入力に、仮押さえのあとで読んだ `now` を渡した koyomi の日付の答え」なら、期限を `wait until x.at` と同じく koyomi の日数の幅で数える。呼び出しの最大は、呼び出しを始めたときの最大と期限の最大の大きいほうに、切り上げの 1 秒を足したもの(コールバックでは、ID を渡す 60 秒と、失敗のあとのリトライの分も足す。17 章)。イベントのタスクの `on timeout` だけのハンドラーは、期限の最小より早くは動かない(15 章の、決まった `timeout` を足す扱いの代わり)。コールバックの `on timeout` の最小は、前と同じく呼び出しの始まりのままにした(ID を渡すほうが先に時間切れになりうる)。期限がほかの式(入力、タスクの答え、仮押さえの前に読んだ `now`、変数に入れ直した値)なら、最大は上限なしで、注がその期限を言う。koyomi の日数が最大を決めたときは、W206 の注に、最大になる日(ritsu-ports の `DaySpan::most_at`)と、そのときの期限の日と時刻を出す(ritsu-ports の `HoldSpan::most_why`)。
- **E040**:`timeout until` の呼び出しは、durable functions で時計を読む step の 1 操作、Argo で秒数を計算するステップグループとテンプレートの 4 ノードを足す。Temporal(タイマーは前と同じく一つ)と Step Functions(ステートは増えない)は変わらない。E050 に新しいものは無い。
- **`doc`**:規則と日付の行のリトライの列を「1 秒後と 2 秒後の 2 回(failure・timeout)」に、タイムアウトの列を 10 秒にした。`timeout until` のタスクは「`due.at` まで」と出す。
- **例**:`review` の四つの版(argo・aws・pydantic-graph・temporal)と日本語の版は、承認を、申し込みが来てから 3 営業日後の 17 時まで待つ(英語は `calendars/weekdays.cal` と `dates/approval_terms.cal`、日本語は `calendars/平日.cal` と `dates/承認の期日.cal`)。

### 16.4 捨てたもの

- **durable functions で、invoke と `context.wait` を `context.promise.race` で競わせること。** SDK の文書はタイムアウトの用途に挙げているが、負けたほうの操作(走り続ける invoke か、残る wait)を再生でどう扱うかを確かめていない。上限をそれに頼りたくない。
- **規則の Lambda 関数そのもののタイムアウトを 10 秒にしてもらうこと。** 生成物の外の設定に頼り、時間切れは `timeout` ではなく関数のエラー(`failure`)として届く。
- **Argo で、規則の Pod の `activeDeadlineSeconds` を 10 秒にすること。** Pod の起動が期限に入るので、混んだクラスタでは、呼び出しが起動だけで時間切れになり、2 回のリトライのあとで失敗しうる。**Argo だけ長い上限にすること**も、どの出力先でも同じ上限という決まりから外れ、X5 はいちばん長いもので数えることになるので捨てた。
- **Temporal で `scheduleToCloseTimeout` にすること。** 16.3 のとおり。
- **pydantic-graph で、プロセスの中の規則を `asyncio.to_thread` で別のスレッドにして時間切れにすること。** スレッドは止められずに走り続け、Temporal の Python 版と共有する `rules.py` の作りも替わる。
- **期限を呼ぶところに書くこと**(`let a = approval() until due.at`)。呼び出しの構文木(`TK::Call`)にフィールドが要り、それを分解している 11 ファイル 14 か所と、すべての生成器の呼び出しの引数が替わる。同じタスクを期限付きと無しで呼べると、宣言の `timeout` の意味が呼び出しごとに変わる。
- **期限の引数を送らないこと。** 宣言にある引数が黙って送られないのは分かりにくく、答える人に期限を見せたいときに同じ値をもう一つの引数で渡すことになる。
- **期限の長さを式で書くこと**(`timeout due.at - now`)。dandori に時間の長さの値が無く、X5 が期限の出どころを読めなくなる。
- **下限を置かないこと。** Temporal は 1 ミリ秒まで、Step Functions は 1 秒までなので、出力先で意味が 1 秒ずれる。期限を過ぎていたら Step Functions で Choice からすぐ `on timeout` に進める形も、ステートと Argo のノードが増えるので捨てた。

### 16.5 変わること

- **前は通っていた実行が落ちうる。** 10 秒を超える規則と日付の呼び出しは、Step Functions、durable functions、Argo(caller の中の呼び出しが 10 秒を超えたとき)、pydantic-graph の `connect` の規則で、時間切れになり、2 回リトライしたあと `on timeout`(無ければ `on failure` か、ワークフローの失敗)に進む。Temporal は前と同じである。
- **生成するコード。** 替える前(8b069303)と後のバイナリの `ritsu dandori` を、替える前の木の 233 本の `.flow` にかけ、15 の出力(`check` の英日のテキストと JSON、七つの生成先の `build`、`doc` の英日の Markdown と HTML、`scenarios`)と、`.flow` のある 52 のディレクトリの `ritsu check`(英日のテキストと JSON)を比べた。12,641 ファイルのうち 483 が変わった。
  - Step Functions:28 本の 50 の Task に `"TimeoutSeconds": 10` が足された。
  - durable functions:38 本の `workflow.ts` で、規則と日付の 60 の呼び出しが `context.step(…, async () => dd.within(tasks.rule_<名前>(…), 10), …)` の形になった(46 は `context.invoke` から、14 は `connect` の step に上限が付いた)。26 本の `tasks.ts` に、Lambda で呼ぶ規則と日付の実装(`LambdaRules`)が足され、その `workflow.ts` の `run` が受け取る `tasks` の型も `LambdaRules` を含むようになった。
  - Argo:40 本の `caller/call.ts` に、規則と日付の呼び出しの 10 秒のタイマーが足された。
  - pydantic-graph:13 本の `graph.py` で、`connect` の規則の 16 の呼び出しが `dd.within(…, 10)` で包まれた。
  - Temporal の TypeScript と Python の出力は、一字も変わらない(`BUILD_ID` も同じ)。Go も、次の import の順を直した二本のほかは変わらない。
  - Go の `rules.go` は、パッケージの外のフローが規則と日付の両方を呼ぶとき、import を一つの組にしてパスの順(gofmt の順)に並べる。前は規則のモジュールのあとに koyomi の日付のパッケージを並べたので、規則のモジュールの名前が `koyomi/go/…` より後ろに来ると、gofmt の形にならなかった。替える前の木では、ritsu の試験のプロジェクト `tests/projects/invoice` の二つの版(`invoice.flow` と `invoice.ja.flow`。規則のモジュールは `paymentmethod`)がそうで、その `rules.go` の import の順と、`worker.go` の `BuildID` が変わる(4 ファイル。ワーカーのバージョニングでは、新しいビルドとして扱われる)。dandori の例と試験のフローには無く、例 `review` が日付を呼ぶようになって、tools の段の `generated_go_vets` に出た。
  - `doc`:規則か日付を呼ぶ 79 本の英日の Markdown と HTML で、706 の行(規則と日付の行のリトライとタイムアウトの列)が変わった。
  - `check`、`scenarios`、`ritsu check`:`tests/fixtures/events.flow` の E007 の文(16.3)だけが変わった(その文を出す 18 のファイル)。ほかの診断、境目の答え、シナリオは同じである。
- `timeout until` を使う新しい形のフロー(`review` の八つの版、`tests/flows/deadlines.flow` と `deadline_event.flow`)は、替える前の dandori では E001 になる。`review` の Temporal の記録した履歴は、承認を待つ回(`run-3`)が無くなり、時間切れまで待つ回(`run-8`)を三つの言語で記録し直した。
- X5 は、規則と日付の呼び出しのあとの長さを数で言う。16.1 の見本は、替えたあとは成り立つ(何も言わない。境目は「確かめた 2 か所、決められない 1 か所」)。残る一つは、期間を書いていないワークフローの `now` を日付に渡す W205 で、ワークフローに動く期間を書けば決まる(20 章)。ritsu-cross の台帳の E206 と W206 の文、`docs/codes.md` と `docs/codes.ja.md` も合わせた。

### 16.6 確かめたこと

- **上限の試験**(5 章)を、出力先ごとに本当の時間で走らせた。Temporal の三つの言語では、規則のアクティビティが Lambda の規則でも Connect の規則でも 10.0 秒の START_TO_CLOSE のタイムアウトを 3 回起こし、あいだのタイマーは 1.0 秒と 2.0 秒だった。LocalStack では、Lambda の Task が 10.0 秒の `States.Timeout` を 3 回起こし、リトライの待ちは参照より長くて 0.010 秒で、遅れて届いた答えが履歴に 3 件あった。durable functions では、Lambda と Connect のどちらも試しが 11.0 秒と 12.0 秒の間隔で来て、最後の試しから 10.1 秒で終わった。pydantic-graph では、Connect の規則の試しが 11.0 秒と 12.0 秒の間隔で来て、10.0 秒で終わった。Argo(kind の上の v4.1.4、コントローラーの見直しは 1 秒)では、Connect の規則の Pod が三つとも終了コード 143 で終わり、試しは 11.2 秒と 12.2 秒の間隔で来て、最後から 11.2 秒で終わった。どの実行も参照インタプリタのとおり `on timeout` に進んだ。
- dandori の重い試験を、一つずつ全部回した(Temporal の突き合わせの三つの言語と、言語をまたぐアクティビティ、Worker Deployment Versioning、子の `.flow`、記録した履歴の再生、Argo、LocalStack、durable functions、pydantic-graph、上限の五つ)。どれも通った。Open Responses の試験は、Ollama が動いていないので SKIP になった。
- dandori の tools の段も全部回した(生成した TypeScript の型の検査、Go の `go vet` と gofmt、Step Functions の参照の実行、既定の `Transport`、Mermaid と Chrome の図、PostgreSQL と TigerBeetle の帳簿など)。帳簿の試験の MySQL の部分は、機械に MySQL が無いので SKIP になった。
- 年末年始:返金のフロー(仮押さえは 7 日、承認は受け付けた日から 3 営業日後の 17 時まで、`timeout until` で待つ。営業日は東京の祝日を入れたカレンダー)で `ritsu check` を回すと、確定と三つの取消が W206 になり、注は次のとおりだった。暦日の 3 日(`timeout 3 days`)のままなら、成り立つ(境目は 19 か所を確かめ、決められないものは無い)。

```text
warning[ritsu W206]: policy/返金.flow:120:1: Whether the hold of the case `押さえ` has expired when `確定する` posts it cannot be decided
   120 |   押さえ <- 確定する(申請: 申請番号)
  = `確定する` comes 0 seconds to 11 days 17 hours 7 minutes 37 seconds after the hold is made (line 93), and the hold expires 7 days after it is made
  = The longest is when the `now` given to `承認の期限` (line 107) falls on 2026-12-24: `承認の知らせ` (line 108) waits until `承認の期限.at`, 2027-01-04 at 17:00, 11 days after it
  = The book refuses a call on an expired hold with `expired`, which the flow handles here.
```

- ritsu の `tests/cross.rs` の X5 に、支払日の 09:00 まで承認を待つ形を四つ足した。イベントで待てば、取消は仮押さえから早くて 17 日 9 時間あとなので E206、コールバックで待てば W206 で、注は「いちばん長くなるのは、42 行目の `payment` に渡した `now` が 2026-07-21 のときです。43 行目の `approval` は、その日から 51 日後の 2026-09-10 の 09:00(`due.at`)まで待ちます」、仮押さえを 60 日にすれば成り立ち、入力の時刻まで待てば上限なしの W206 である。
- dandori の fast の段:`tests/fixtures`(`deadlines.flow` に E002・E003・E007 の六つ、`deadline_twice.flow`、英日の golden)、`tests/ports.rs`(期限のある `event` と `callback` の長さ、`most_why`、日付の呼び出しのあとの `wait until` の最大が数になること)、`doc` と例の golden、`tests/flows/deadlines.flow` と `deadline_event.flow` の突き合わせ。ritsu の `tests/run.rs` に、期限まで時計が進む `ritsu run` の golden(`until_timeout`)を足した。

### 16.7 見つけて直していないもの

- **コールバックの ID を渡す側の時間**は、17 章で、どの出力先でも一回 60 秒までにし、X5 が数えるようにした。残るところ(Step Functions と durable functions で X5 が多めに数えること、Argo の `image` のコールバックの Pod の起動)は 17.7 に書いた。
- **プラットフォームの待ち。** Temporal のタスクキューの待ちと、Argo の Pod がノードに置かれて起動するまでの待ちには、上限が無い。X5 の 33 秒はそれを数えない(根の DESIGN 7.7 の ★ の前提)。
- **Argo のリトライの間。** Argo は次の回を、コントローラーがワークフローを見直したときに始める。既定の見直し(変化から 10 秒)では、規則の呼び出しは 33 秒ではなく、3 回の 10 秒と二つの 10 秒ほどの間で 50 秒ほどまでかかりうる。X5 は 1 秒と 2 秒で数える。
- **Argo の caller の中で計算する規則と日付。** 同梱の規則(rulec と koyomi の TypeScript)は同期の計算なので、caller のタイマーはそれを途中で止められない。純関数で、規則の表と暦の範囲で必ず終わる。
- **pydantic-graph のプロセスの中の規則の `on timeout`。** 上限を付けないので、本番では時間切れにならないが、シナリオと参照インタプリタはその呼び出しの `timeout` を選び、ランナーは規則の代わりのスタブで演じる。
- **期限を変数に入れ直したもの**(`let t = due.at` のあとの `approval(by: t)`)。`wait until` と同じく、X5 は `x.at` の形だけを読み、ほかは上限なしと数える。
- **E031**(Express の 5 分)は待ちだけを数え、規則と日付の呼び出しの 33 秒を数えない(前と同じ)。
- **LocalStack の履歴。** LocalStack 4.14.0 は、時間切れになった試しの Lambda の答え(TaskSucceeded)を、時間切れのあと、次の試しの途中で履歴に書く。本物の Step Functions が同じことをするかは確かめていない。上限の試験は、その答えを試しの答えと数えない。

## 17. コールバックの ID を渡す部分を、どの出力先でも一回 60 秒までにする(2026-10-29)

### 17.1 きっかけ

コールバックで待つタスクの一回の試しは、ID を外へ渡す部分(Lambda の関数を呼ぶ、SQS にメッセージを送る、利用者のコードやイメージを動かす)と、答えの待ちでできている。ritsu の X5(根の DESIGN 7.7)は、一回の試しをタスクの `timeout` と数えていたが、`timeout` が限るのは答えの待ちで、渡す部分の長さは出力先ごとに違った。Temporal では、渡すアクティビティに `timeout` とは別に 60 秒の上限があり、一回の試しは `timeout` より最大で 60 秒長かった。durable functions の submit の step、Argo の渡す Pod、pydantic-graph の渡す呼び出しには、上限が無かった。

そのため X5 は、仮押さえと確定のあいだでコールバックを待つフローを、成り立つと言うことがあった。1 時間もつ仮押さえのあとで倉庫の確認を 59 分まで待ち(`timeout 59 minutes`)、それから確定するフローでは、押さえる呼び出しと確定の呼び出しの 10 秒ずつを足しても 59 分 20 秒なので、替える前の ritsu は確定について何も言わない(境目は「確かめた 1 か所、決められない 0 か所」)。Temporal で確認のアクティビティが ID を渡すのに 60 秒かかれば、確定は仮押さえから 1 時間 20 秒あとに来て、期限が切れている。替えたあとの ritsu は次のように言う(ritsu の `tests/cross.rs` の `x5-callback-W206`)。

```text
warning[ritsu W206]: invoice.flow:35:1: Whether the hold of the case `goods` has expired when `ship` posts it cannot be decided
    35 |   goods <- ship(order: order, sku: sku)
  = `ship` comes 0 seconds to 1 hour 20 seconds after the hold is made (line 32), and the hold expires 1 hour after it is made
  = The book refuses a call on an expired hold with `expired`, which the flow handles here.
```

同じところで、`timeout until` のコールバックの数え方(16.3)に漏れがあった。E007 は `timeout` をリトライする `retry` を止めるが、`retry … on failure` は書ける。答えが失敗なら、リトライは間を置いて ID を渡し直し、期限を過ぎていれば 1 秒待って終わる。16 章の X5 は、このリトライの分を数えていなかった。`ritsu explain E206` の見本で、承認を支払日の 09:00 まで待つコールバックに `retry 1 times every 5 seconds on failure` を付けると、取消と確定が来る最大は、替える前は 51 日 9 時間 1 分 1 秒、替えたあとは 51 日 9 時間 2 分 7 秒になる(5 秒の間と、渡し直しの 60 秒と、1 秒の分)。

### 17.2 原因

- `timeout` を答えの待ちの長さとして作り、ID を渡す部分の長さは出力先に任せていた。Temporal はアクティビティにタイムアウトが要るので、渡すアクティビティに 60 秒を付けた(4.2)。Step Functions は、`.waitForTaskToken` の Task の `TimeoutSeconds` が渡すことと待ちをまとめて限る。durable functions は、コールバックの時計が `createCallback` から動くので待ちのほうは渡す時間を含むが、submit の step そのものは止めない。Argo は、`timeout` を待ちの suspend にだけ使い、渡す Pod には期限を付けなかった。pydantic-graph は、`timeout` を `await_callback` の待ちにだけ使った。
- X5(`crossings.rs` の `duration`)は、コールバックの一回の試しを `timeout` と数えた。`timeout until` の呼び出し(`through`)は、`retry` を数えなかった。

### 17.3 決定

どの出力先でも、コールバックのタスクの一回の試しで、ID を渡す部分を 60 秒までにする。値は `check.rs` の `CALLBACK_HAND_ON_SECONDS`(`RULE_SECONDS` のそば)の一か所に置き、生成器、X5、`doc` の表、試験がそこを読む(Temporal の `activity_timeout` が返す 60 も、それを読むようにした)。60 秒を超えた試しは、答えの待ちが切れたときと同じくタスクの時間切れ(`timeout`)で、タスクの `retry` がリトライし、そのあと呼び出しの `on timeout`(無ければ `on failure` か、ワークフローの失敗)に入る。前からの Temporal と同じ扱いである。

| 出力先 | 渡す部分の上限 | 時間切れの届き方 | 前 |
|---|---|---|---|
| Temporal(TypeScript・Python・Go) | 渡すアクティビティの `startToCloseTimeout` 60 秒 | アクティビティのタイムアウト | 同じ(変えていない) |
| Step Functions | 別には付けない。`.waitForTaskToken` の Task の `TimeoutSeconds`(タスクの `timeout`)が、渡すことと答えの待ちをまとめて限る | `States.Timeout` | 同じ(変えていない) |
| Lambda durable functions | submit の step の中で、渡す呼び出しを `dd.within(…, 60)` で包む | `Dandori.Timeout` | 無し |
| Argo Workflows(`lambda` と `aws` のコールバック) | caller(`call.ts`)の 60 秒のタイマー。過ぎれば `error.json` に `Dandori.Timeout` を書き、終了コード 143 で終える | 期限で止められた Pod と同じく `timeout` | 無し |
| Argo Workflows(`image` のコールバック) | 渡す Pod の `activeDeadlineSeconds: 60` | Kubernetes が Pod を止め(DeadlineExceeded)、終了コード 143(起動の前なら 137)で `timeout` | 無し |
| pydantic-graph | 渡す呼び出し(dandori が書く関数か、利用者の関数)を `dd.within(…, 60)` で包む | `asyncio.TimeoutError` から `timeout` | 無し |

- **リトライはタスクの `retry` に従う**(Temporal にそろえた)。渡すことと答えの待ちを合わせた一回を、ほかの時間切れと同じく試し直す。渡す部分だけのリトライの決まりは作らない。`timeout until` のタスクは `retry … on failure` しか書けない(E007)ので、渡す部分の時間切れはそのまま `on timeout` に入る。Argo はコールバックの `retry` を作れない(E050)。
- **時間切れの届き方**は、規則と日付の上限(16 章)にならった。durable functions と Argo の caller は `Dandori.Timeout`、Argo は終了コード 143、pydantic-graph は `asyncio.TimeoutError`、Temporal と Step Functions はプラットフォームの時間切れで、どれも `timeout` として読む。
- **Step Functions は変えない。** トークンは `.waitForTaskToken` の Task の中でしか作れないので、渡す呼び出しだけを別のステートに分けられない。Task の時計はトークンを渡す前から動き、`TimeoutSeconds` が一回の試しの全部を限る(`timeout until` の `TimeoutSeconds` も、ステートに入ったときから期限までを数える)。一回の試しは `timeout` までで終わり、ほかの出力先より短い。`timeout` の無いコールバックは、どの出力先でも答えの待ちに上限が無く、X5 は上限なしと数える。
- **durable functions** も、コールバックの時計が ID を作ったときから動く(4.3)ので、一回の試しは、渡す部分の 60 秒と答えの待ちの `timeout` の長いほうまでで終わる。step は前と同じく `noRetry` で、60 秒を過ぎた step が `Dandori.Timeout` で失敗し、`dd.attempt` がそれを時間切れとして読む。規則と日付の 10 秒(16 章)と同じ形である。
- **Argo** は、caller で渡すコールバックには、規則と日付の 10 秒と同じく caller の中にタイマーを置き、Pod をノードに置いて起動するまでの時間を数えない。`image` のコールバックは利用者のイメージの中のコードが渡すので、dandori はそこにタイマーを置けない。そこで渡す Pod に `activeDeadlineSeconds: 60` を付けた。Kubernetes は期限を Pod を受け付けたときから数えるので、こちらはイメージを取ってコンテナを起動する時間も 60 秒に入る(`image` のタスクの `timeout` が前からそうであるのと同じ。4.4)。混んだクラスタでは渡す部分が 60 秒より早く時間切れになりうるが、60 秒より長くなることはないので、X5 の数え方はそのまま成り立つ。
- **X5**(`crossings.rs`):コールバックのタスクの一回の試しを、最大で 60 秒と `timeout` の和と数える。`retry` があれば、その回数だけの試しと、あいだの待ちを足す(リトライが k 回なら `(k + 1) × (60 + timeout)` にあいだの待ちの和)。`timeout until` のコールバックは、ID を渡し終えてから期限まで待ち(期限を過ぎていれば 1 秒)、試しは「呼び出しを始めたときの最大に 60 秒を足したもの」と期限の最大の遅いほうに、切り上げの 1 秒を足したときまでに終わる。失敗のあとのリトライは、間を置いて ID を渡し直し、期限を過ぎているので 1 秒待って終わるので、一回につき 61 秒とあいだの待ちを足す。最小は 0 秒のまま(答えも、渡す部分の時間切れも、すぐ来うる)。イベントのタスクは渡すものが無いので、前と同じである。X5 はフローをどの出力先に組むかを知らずに数えるので、いちばん長くなる Temporal・Argo・pydantic-graph の和で数え、Step Functions と durable functions では多めに数える(上からの見積もりとして成り立つ)。
- **参照インタプリタ**は変えない。シナリオの `timeout` は試しのどこで切れたかを言わないので、前と同じく答えの待ち(`timeout`、`timeout until` なら期限まで)として時間を進める。上限を数えるのは X5 である。
- **`doc`**:コールバックの行のタイムアウトの列を、「ID を渡すのに 1 分、そのあと 30 秒」(英語は「1 minute to hand the id on, then 30 seconds」)の形にした。`timeout until` なら「そのあと `due.at` まで」で、`timeout` が無ければ「ID を渡すのに 1 分」だけである。
- E040 の見積もり(durable functions の操作、Argo のノード)は変わらない。E050 に新しいものは無い。

### 17.4 捨てたもの

- **Step Functions に 60 秒を別に付けること。** 渡す呼び出しだけを別のステートに分けられない(17.3)。`HeartbeatSeconds` は、答える側がハートビートを送らなければ切れるので、人やほかのシステムが答えるコールバックを壊す。Lambda を `InvocationType: Event` で呼べば、関数のエラーを失い、関数の長さも限らない。`TimeoutSeconds` を 60 にすると、答えの待ちまで 60 秒になる。
- **X5 を出力先ごとに数えること**(Step Functions は `timeout` だけ、など)。`ritsu check` は、フローをどの出力先に組むかを知らない。
- **durable functions で、submit の step と `context.wait` を `context.promise.race` で競わせること。** 16.4 で規則に使わなかったのと同じ理由である(負けたほうの操作を再生でどう扱うかを確かめていない)。
- **Argo で、caller で渡すコールバックの Pod に `activeDeadlineSeconds` を付けること。** Pod の起動が期限に入るので、混んだクラスタでは、渡す前に時間切れになる(16.4 の規則のときと同じ)。
- **Argo の `image` のコールバックに、期限を付けないままにすること。** Pod の起動を数えずに済むが、どの出力先でも 60 秒という決まりから外れ、X5 は上限なしと数えることになる。利用者のイメージの中にタイマーを差し込む形(エントリポイントを包むプログラムを init コンテナで入れる、`command` を書き換える)は、イメージの作りに依る。
- **渡す部分だけのリトライの決まり**(規則のように 2 回、など)。Temporal はタスクの `retry` で一回ごとに試し直していて、それにそろえた。決まりを足すと、`doc` と X5 とシナリオの数え方が二通りになる。
- **参照インタプリタで、コールバックの時間切れのときに 60 秒を足して時計を進めること。** シナリオは、試しのどこで切れたかを言わない。答えの待ちが切れるのがふつうの走り方で、上限は X5 が数える。

### 17.5 変わること

- **前は通っていた実行が落ちうる。** durable functions、Argo、pydantic-graph で、ID を渡すのに 60 秒を超えるコールバックは時間切れになる(`retry` があればリトライし、そのあと `on timeout`、無ければ `on failure` かワークフローの失敗)。Argo の `image` のコールバックは、Pod の起動を含めて 60 秒である。Temporal と Step Functions は前と同じである。
- **生成するコード。** 替える前(8f52b2c6)と後のバイナリの `ritsu dandori` を、木の 238 本の `.flow` と、足した二本(`tests/limits/callbacks.flow` と `callbacks_argo.flow`)にかけ、16.5 と同じ 15 の出力を比べた。10,886 ファイルのうち 212 が変わった。
  - durable functions:17 本の `workflow.ts` で、18 の submit の step の中が `await dd.within(tasks.<タスク>({ …, "callback_id": dd_id }), 60)` になった。
  - pydantic-graph:31 本の `graph.py` で、32 の渡す呼び出しが `dd.within(…, 60)` で包まれた。
  - Argo:9 本の `caller/call.ts` に、渡すコールバックの名前の集合(`CALLBACKS`)と 60 秒のタイマーが足された(8 行ずつ)。`image` のコールバックを持つ 3 本の WorkflowTemplate(例 `review` の Argo の版と日本語の版、`callbacks_argo.flow`)で、渡すテンプレートに `activeDeadlineSeconds: 60` が足された。
  - Temporal の三つの言語と Step Functions の出力は、一字も変わらない。
  - `doc`:コールバックを呼ぶ 38 本の英日の Markdown(41 行ずつ)と HTML(79 行ずつ)で、タイムアウトの列が変わった。
  - `check` と `scenarios` は変わらない。
- **`ritsu check`。** X5 の最大は、仮押さえのあとでコールバックを待つ呼び出しごとに、試しの数だけ 60 秒(`timeout until` では、失敗のあとのリトライの分)長くなる。成り立つと言っていたものが W206 になり、W206 の注の最大が延びることがある。言語をまたぐファイルを持つ 65 のディレクトリの答え(英日のテキストと JSON)は、`tests/limits` に足した二本が「ok」と出るほかは変わらない。木の中には、この 60 秒で答えが変わるフローは無い。ritsu-cross の台帳の W206 の文、`docs/codes.md` と `docs/codes.ja.md` も合わせた。

### 17.6 確かめたこと

- **ID を渡す部分の上限の試験**(5 章)を、出力先ごとに本当の時間で走らせた。どの出力先でも、ID を渡す相手は 60 秒を過ぎても渡し終えない。
  - Temporal の三つの言語では、渡すアクティビティが 60.0 秒の START_TO_CLOSE のタイムアウトを 2 回起こし、あいだのタイマーは 1.0 秒だった。
  - durable functions では、試しが 61.0 秒の間隔で来て、最後の試しから 60.1 秒で終わった。pydantic-graph では、61.0 秒の間隔で来て、60.0 秒で終わった。
  - LocalStack では、45 秒かかる Lambda 関数を渡す相手にすると、どの試しも 30.0 秒(タスクの `timeout`)で `States.Timeout` になり、リトライの待ちは参照より長くて 0.004 秒だった。
  - Argo(kind の上の v4.1.4)では、Lambda のコールバックの caller が終了コード 143 で終え、実行は呼び出しから 61.0 秒で終わった。`image` のコールバックは本物の Pod で、Kubernetes が Pod を始まってから 60 秒で止め(DeadlineExceeded)、終了コードは 143 だった。
  - どの実行も、参照インタプリタのとおり `on timeout` に進んだ。
- dandori の `tests/ports.rs` に、二つの形を足した。`timeout 30 seconds` と `retry 1 times every 2 seconds` のコールバックのあとの確定の最大は、押さえる呼び出しの 10 秒、二回の 90 秒、2 秒の間、確定の 10 秒の和になる。`timeout until` と `retry 1 times every 5 seconds on failure` のコールバックのあとの最大は、期限の最大と 1 秒に、5 秒・60 秒・1 秒と確定の 10 秒を足したものになる。
- ritsu の `tests/cross.rs` の X5 に、17.1 の `x5-callback-W206` を足した。16 章のコールバックの四つの golden は変わらない(期限が呼び出しの始まりより十分あとなので、60 秒が最大を変えない)。
- `doc` の golden(`tests/doc` の 8 本の英日)と、サイトの図のページ(`fulfillment` と `review` の英日)を取り直した。
- platforms の段の全部(重い試験の 22 本。前からの 17 本と、足した五つ)を一つずつ回し、どれも通った。SKIP は Ollama の試験の一つだけで、手元で Ollama が動いていないからである。
- dandori の tools の段も全部回した(生成した TypeScript の型の検査、Go の `go vet` と gofmt、Step Functions・durable functions・pydantic-graph の参照との突き合わせ、既定の `Transport`、Mermaid と Chrome の図、PostgreSQL と TigerBeetle の帳簿など)。帳簿の試験の MySQL の部分は、機械に MySQL が無いので SKIP になった。

### 17.7 見つけて直していないもの

- **Step Functions と durable functions では、X5 が多めに数える。** 一回の試しは、Step Functions では `timeout` まで、durable functions では 60 秒と `timeout` の長いほうまでで終わるが、X5 は 60 秒と `timeout` の和で数える。この二つにしか組まないフローでは、成り立つ仮押さえを W206 と言うことがある。
- **Argo の `image` のコールバックの、Pod の起動。** 60 秒に入るので、イメージを取るのに時間がかかるクラスタでは、渡す処理が短くても時間切れになりうる。
- **プラットフォームの待ち**(16.7)。Temporal のタスクキューの待ちと、Argo の caller の Pod がノードに置かれて起動するまでの待ちは、60 秒に入らず、X5 も数えない(根の DESIGN 7.7 の ★ の前提)。

## 18. 振替の額に渡す規則の出力を、振替に至る実行で呼び出しが受け取りうる値で数える(ritsu の X4。2026-10-30)

### 18.1 きっかけ

ritsu の X4(根の DESIGN 7.6)は、規則の数の出力を chobo の振替の額に渡すところで、その額を chobo が受け取るか(0 から 2⁶³ − 1 か)を確かめる。dandori が口で渡していたのは、額がどこから来うるか(`Amount::from`:規則の出力、範囲の分かる数、範囲の分からないところ)だけだった。そのため X4 は、規則の出力を規則の入力の全部で数えていた。返却(席を 20 返す)を `match` の分岐で外してから振替に渡すフローでも、外したはずの返却を例に挙げて E203 と言っていた。`ritsu explain E203` の見本のフローの振替を、次のように分岐の中に置いたものである。

```text
flow
  let need = seats(event_kind: kind)
  match kind
    handback => pass
    workshop, talk => give_seats(event: event, count: need.needed)
```

替える前の `ritsu check` は、次のように言って 1 で終わる。

```text
error[ritsu E203]: booking.flow:20:1: The task `give_seats` can give `count` the amount -20, which chobo does not take
    20 |     workshop, talk => give_seats(event: event, count: need.needed)
  = `need.needed` is the output `needed` of the rule "seats.rule", which comes to -20 at event_kind = handback
  = chobo takes an amount from 0 to 9223372036854775807 in the unit's smallest step, and a call with any other fails rather than being refused.
  = Make the rule's amounts 0 or more (a negative one is a transfer the other way).
ritsu check: 3 files (rulec 1, chobo 1, dandori 1): 1 fail (1 error); borders between the languages: 2 checked, 0 undecided
```

替えたあとは「all pass; borders between the languages: 2 checked, 0 undecided」で 0 で終わる(ritsu の `tests/cross.rs` の `x4-branch-arm`)。数も同じで、`asked` を 50 以上で渡すフローでも、規則の `asked - holding` は −50 から数えられ、W203 だった。

### 18.2 原因

`crossings.rs` は、額の出どころを、フローのどこかでその変数に入れる値を全部合わせて求める(1.3 と同じ数え方)。どの実行が振替に至るか、その実行で規則の呼び出しが何を受け取るかは数えなかった。`match` の分岐の中で値を狭めないのは dandori の検査の決め(1.3)で、X4 の出どころもそれに従っていた。

### 18.3 決定

dandori は、振替の額ごとに、振替に至る実行でその額になりうる規則の呼び出しと、それぞれの呼び出しがその実行で規則に渡しうる値を、口で渡す(`Amount::calls`。ritsu-ports の `OutputCall`、`CallInput`、`Held`)。ritsu-cross は、それを rulec の新しい口 `output_values_over`(rulec の DESIGN §15.273)に聞いて、出力を数える。

- **たどり方**(`crossings.rs` の `Answers`):フローを文の順にたどり、各点で、そこに至る実行がした規則の呼び出しとその引数、規則の答えを入れた変数がどの呼び出しの答えを持ちうるか、を持つ。
  - 数の引数は、X2 で呼び出しに渡す範囲(`ranges::variables`)で、どの実行でも同じである。規則の入力が受け取る範囲より狭いときだけ渡す。
  - 列挙と真偽の引数は、値の来るところが入れる値(`ranges::held`。`match` の検査と同じ数え方)から、振替の前の `match` の分岐が外した値を除いたものである。値を書いて渡した引数(`seats(event_kind: workshop)`)は、その値になる。
  - `match` の分岐の中では、その変数かフィールドを受け取った呼び出しを、分岐の値に狭める。分岐の値のどれも受け取りえない呼び出しは、その分岐に至る実行ではされていない。案件の状態の `match` は、何も狭めない。
  - 呼び出しのあとでその変数に別の値を入れると(`let k = other`)、それより後の `match` は、その呼び出しを狭めない。
  - 分岐の終わりとハンドラーの終わりは合わせる。`fail`、`succeed`、`break` で終わる分岐は合わせない。ハンドラーは、呼び出しの前の状態から始める(失敗した呼び出しは変数に値を入れない)。
  - ループは一度たどり、ループの中で値を入れる変数は、ループの中でもループのあとでもたどらない。
  - `on failure` は、失敗で止まりうる点(呼び出しと待ち)を合わせた状態から始める。`on cancel` は、取消で止まりうる点(フローと `on failure` の、呼び出しと待ち)を合わせた状態から始める。`fail` と失敗した `check` は `on failure` を走らせない(3 章)ので、止まりうる点に入れない。
  - たどらない額(ループの中で値を入れた変数など)は `calls` を None にし、ritsu-cross は前と同じく、規則の入力の全部で数える。どの振替の額にも規則の出力が来ないフローは、たどらない。
- ★ **どの実行でも規則の出力にならない額は、X4 の境目にしない**(`calls` が空)。`let n = need.needed` のあとで `let n = 5` を入れて渡すフローの額は、前は規則の出力でもありうるとして X4 が数えたが、どの実行でも 5 である。額の範囲は dandori の E014 が見る。
- ★ **分ける勧めは、分けられる値にだけ言う。** E203 の例の入力のうち、規則の順で最初の列挙か真偽の入力が、呼び出しから振替までにほかの値を入れない変数かフィールドから来るとき(`CallInput::matchable`)、ritsu-cross は直し方に「振替の前に `kind` を `match` で分け、`handback` の分岐では出力を振替に渡さない」を足す。
- **dandori の検査は変えない。** `match` の分岐の中で値を狭めることは、検査(E014、E010、E011 など)ではしない(1.3)。たどるのは、X4 に渡すためだけである。`check`、`build`、`doc`、`scenarios` は `crossings` を読まないので、変わらない。

### 18.4 捨てたもの

- **dandori の検査でも、分岐の中で値を狭めること**(E014 を分岐で狭める)。数と列挙の両方の分岐の絞り込みとして決めることになり(9 章の捨てたもの)、生成するコードの型の名前にも関わる。X4 だけにした。
- **`Amount::from` の出どころを狭めること。** 出どころはフローのどこかで入れる値を全部合わせたもので、X2、X3、X6 も同じ数え方で読む。実行ごとのことは呼び出しごとに別に渡すほうが、ほかの境目を変えない。
- **ループを、答えが変わらなくなるまで回してたどること。** 木には、規則の出力を振替の額に渡すフローがそもそも無い。一度たどって、値を入れる変数をたどらない形にした。
- **`fail` で止まる点も、`on failure` の入口に入れること。** `fail` は `on failure` を走らせない(3 章)。入れると、`on failure` の振替に、そこへ至らない実行の値を数えることになる。

### 18.5 変わること

- 口の `Amount` に `calls` が増える。`from` は変わらない。
- `ritsu check` の X4 は、振替に至る実行で呼び出しが規則に渡しうる値で、規則の出力を数える。`match` で返却の分岐を外して渡すフロー、分岐の中で呼び出すフロー、返却の分岐を `fail` で止めるフロー、値を書いて渡すフローは、E203 でなく成り立つ。返却の分岐を残したフローは E203 のままで、注に呼び出しの行と渡しうる値が出て、直し方に分ける勧めが足される。
- 言語をまたぐファイルを持つ 162 のディレクトリの `ritsu check`(英日のテキストと JSON)では、X4 の答えは変わらない。木には、規則の出力を振替の額に渡すフローが無い(本を使うフローのうち規則も使う 15 本は、どれも振替の額にタスクの答えか入力を渡す)。
- dandori 自身の出力(`check`、`build`、`doc`、`scenarios`)は変わらない。

### 18.6 確かめたこと

- `tests/ports.rs` の `a_flow_says_what_it_crosses_into`:規則の出力がどの額にも来ないフローでは、`calls` が None であること。
- `tests/ports.rs` の `an_amount_says_the_calls_of_rules_it_can_come_from`:分けない形、分岐の中で渡す形、分岐の中で呼ぶ形、`fail` の分岐で止める形、分岐をまた合わせる形、呼び出しのあとで別の値を入れる形、別の値を入れてから渡す形、値を書いて渡す形、`let n = need.needed` を通して渡す形、二つの分岐で別々に呼ぶ形、ハンドラーの中の振替、ループの中で呼んでループのあとで渡す形、`on failure` の中の振替、真偽と数の入力(`asked` を `range >=0 <=40` で渡すと狭め、書いた値は一つの値)で、`calls` が言う呼び出しの行と渡しうる値。
- ritsu の `tests/cross.rs`、ritsu-model の `tests/cross.rs`、rulec の `tests/ports.rs`:ritsu と rulec の記録のとおり(根の DESIGN 7.6、rulec の DESIGN §15.273)。
- 替える前と後のバイナリの突き合わせ:162 のディレクトリの `ritsu check`(英日と JSON)、83 本の `.kn` の `ritsu kaname check`、983 本の `.rule` の `rulec check` と `certificate`。X4 の答えはどれも変わらない。

### 18.7 見つけて直していないもの

- **呼び出しに渡した変数を別の変数に入れてから、その変数で分けても、狭めない。** 狭めるのは、呼び出しに渡した変数かフィールドそのものの `match` だけである。
- **ループの中で値を入れる変数はたどらない。** ループの中の呼び出しの答えをループのあとで振替に渡すと、前と同じく規則の入力の全部で数える。
- **dandori の検査は分岐で狭めない**(1.3、18.4)。分岐の中で規則に渡す値が、規則の入力の範囲の外になりうるかは、分岐の外の値で数える。

## 19. `ritsu check` の JSON に渡す、フローのオブジェクト(2026-10-31)

`ritsu check --format json` の `files` の各項目は、`check` に、その言語の `check --format json` がそのファイルについて書くオブジェクトを持つ(ritsu の DESIGN 8.3)。`dandori check --format json` は、ファイルごとに `{file, diagnostics}` を書く(`cli.rs` の `load` が、その場でこの形を組む)。`dandori::ports::Engine::checked_with` は、診断と通ったファイルの行に加えて、同じ形のオブジェクトを、渡す診断の JSON から組んで `ritsu_ports::Checked::json` で渡す。二つのキーのための関数は作らなかった。ritsu は `file` と `diagnostics` を除くので、フローの `check` は `{}` になる。読めないファイルは `null` になる。dandori のコマンドの出力は変えていない。

確かめたこと:ritsu の `tests/check.rs` の `each_files_check_in_the_json_is_what_its_language_writes` で、テストのプロジェクトの二つのフローと、ritsu のテストのプロジェクト `cold_chain` と `regions`(検査を通らないフローを含む)、sekisho の例のフローの `check` が、`ritsu dandori check --format json` のオブジェクトから二つのキーを除いたもの(`{}`)と同じであること。check の JSON の golden(`crates/ritsu/tests/golden/check/`、`golden/modules/`)。

## 20. 日付と時刻の範囲と、ワークフローが動く期間(2026-10-31)

### 20.1 きっかけ

ritsu の X6(根の DESIGN 7.8)は、ワークフローが koyomi の日付の入力に渡す日が、その入力の範囲に収まるかを確かめる。dandori は日付を運ぶだけで(1.16)、どこから来た日かを言えるのは、ほかの koyomi の日付の日だけだった。請求の例は、`now` を支払条件の日付に渡すところで、英日のどちらも W205 になっていた。0.39.0 の `ritsu check` は次のように言う。

```console
warning[ritsu W205]: invoice.flow:58:1: Whether the day given to `received` of the koyomi date `terms.payment` stays inside the input's range cannot be decided
    58 |   let due = terms.payment(received: now)
  = `now` can be any day, and "dates/payment_terms.cal" takes 2026-01-01 to 2027-12-31 for `received`
  = koyomi's generated code refuses a day outside the range when the workflow runs.
…
ritsu check: 8 files (koyomi 4, chobo 2, dandori 2): all pass (8 warnings); borders between the languages: 8 checked, 8 undecided
```

木の W205 は、dandori の請求の例の英日、ritsu のテストのプロジェクト `invoice` の英日と `stockroom` の 5 件で、どれも `now` から来る日だった。dandori の試験のフローにも 4 件あり、3 件が `now`、1 件が範囲を書いていないタスクの結果のフィールドから来る。直すには、日がどこにあるかを `.flow` に書くしかないが、0.39.0 では書けなかった。

```console
error[E001]: a.flow:4:32: unexpected character `-`
     4 |   received : date  range >=2026-01-01 <=2026-12-31
error[E003]: b.flow:4:19: a range is for a number (`int` or a unit) or an ordered enum, and this is `timestamp`
     4 |   at : timestamp  range >=1 <=5
error[E001]: d.flow:2:3: only `history encrypted` can be written under `workflow`
     2 |   now range >=1
```

### 20.2 原因

- `date` は比べも足しもしない型として足した(1.16)。範囲は数と順序のある列挙にだけ書け(1.3)、日付の端を読む字句も無かった。
- 範囲の解析(`ranges.rs`)は日付と時刻の範囲を持たず、`now` はどの時刻にもなりうる値だった。ワークフローがいつ動くかを言う書き方も無かった。
- 口(ritsu-ports の `Origin`)には、`.flow` が書いた日の範囲と時刻の範囲を渡す形が無かった。kaname は日を日の番号にして `Origin::Range` で渡していた。

### 20.3 決定

**書き方**。`date` と `timestamp` に `range` を書ける。書けるところは数と同じで、ワークフローの入力と出力、レコードのフィールド、タスクの引数と結果である。端は rulec・koyomi・kaname と同じ `YYYY-MM-DD` で、`>=` と `<=`、片方だけでもよい。時刻の範囲は、端の日を UTC の日として読み、端のあとに `offset +09:00` と書けば、そのオフセットの日として読む。ワークフローが動く期間は、`workflow` の行の下に、`history encrypted` と並べて `now range >=2026-01-01 <=2027-12-31` と書く(`offset` も書ける)。走り始めた時刻と、本体で読むどの `now` も、その中になければならない。

```
workflow 請求 v1
  now range >=2026-01-01 <=2027-12-28 offset +09:00

record 来訪
  日     : date       range >=2026-01-01 <=2026-12-31
  時間帯 : timestamp  range >=2026-01-01 <=2026-12-31 offset +09:00
```

書き誤りは E001 と E003 にする(`tests/fixtures/date_range_syntax.flow` と `tests/fixtures/period/`)。暦に無い日(`2026-02-30`)、日の一つも入らない範囲、日付でない端(`>=1`、`>=today`)、数に書いた日付の端、日付に書いた `offset`、無いオフセット(`+25:00`、`+09:60`)、文字列に書いた範囲は E003、`now range` を二度書くこと、`offset` のあとの形の誤り(`offset 9`)、`workflow` の下のほかの行は E001 である。

```console
error[E003]: tests/fixtures/date_range_syntax.flow:14:54: `offset` is written only after a range of times and in `now range`: a date has no time of day for an offset to read
    14 |   date_offset : date       range >=2026-01-01 offset +09:00
error[E003]: tests/fixtures/date_range_syntax.flow:16:28: a range is for a number (`int` or a unit), an ordered enum, a date or a time, and this is `string`
    16 |   a_text      : string     range >=2026-01-01
```

**走らせたときの確かめ**。入ってくる値は、数と同じく、入ってくるところで確かめる(P5)。範囲の外の入力は `Dandori.BadInput`、範囲の外の結果は `Dandori.BadResponse` である。比べ方は、どのプラットフォームでも文字列の比べにした。日付は 10 文字全体を、範囲の最初の日と最後の日と `>=`・`<=` で比べる。時刻は先頭の 19 文字(秒まで)を、範囲の最初の秒と、範囲のすぐあとの最初の秒と `>=`・`<` で比べる。端は組むときに計算して書き込む(`render::time_tests`)。19 文字で比べるのは、時刻の形(`TIMESTAMP_RE`)が小数の秒を許すからで、`23:59:59.5Z` は、その秒の中の時刻として範囲に入る。参照インタプリタも同じ比べ方をする(`render::day_in`、`moment_in`)。式は、これまでどおり日付も時刻も比べず足さない(P1)。比べるのは、値が入ってくるところの確かめだけである。

エージェントの応答の Schema では、日付と時刻の範囲を、OpenAI にも Claude にも、`.flow` の書き方のまま説明(`description`)に書く(時刻はオフセットも)。JSON Schema には、文字列で書いた日付の範囲を言う語が無いからである。応答は、ほかの結果と同じく、来てから確かめる。

**動く期間の確かめ**。期間を書いたワークフローには、検査のあとで確かめる文(`TK::Period`、`src/periods.rs`)を入れる。`build`・`run`・`scenarios`・`doc` は、それを入れたフローから生成し、走らせ、描く(規則の前提の確かめと同じ置き方。1.17)。

- **走り始め**:フローの頭で時計を一度読み、期間で確かめる。外なら、何も呼ばずに `Dandori.OutOfPeriod` で失敗する。範囲の外の入力と同じ扱いで、`on failure` は走らない。理由は `the run starts at <時刻>, outside the period the workflow runs in, <期間>` である。
- **`now` を読む文ごと**:本体と、その中のハンドラー・分岐・ループで `now` を読む文の前で、時計を一度読んで確かめ、その文は確かめた時刻を読む(`TExpr::NowAt`)。確かめた時刻と使う時刻は同じで、一つの文は一つの時刻を読む。ループの `yield` が読む `now` は、イテレーションの終わりで確かめる。外なら、処理されなかったタスクのエラーと同じく `on failure` に入り、`on failure` が無ければワークフローが失敗する。リトライせず、タスクの `on` のハンドラーは受けない(タスクのエラーではない)。並列のイテレーションの中では、タスクのエラーと同じく、すべてのイテレーションが終わってから `on failure` に入る(1.8)。理由は `line <行>: now is <時刻>, outside the period the workflow runs in, <期間>` である。
- **`on failure` と `on cancel` の中の `now`** は確かめない。後始末を時計で止めないためである。dandori の範囲の検査と X6 では、その `now` を、期間の無い `now` として読む(W104、W205)。

走り始めで落とすのは、期間を過ぎて始まった実行が、仮押さえをしてから最初の `now` で落ちないようにするためである。読む文ごとの確かめを `on failure` に入れるのは、長い実行が期間の終わりを自然にまたぐからである。値が前提を破るなら実行はどのみち失敗するので、前提の確かめは `on failure` を走らせない(1.17)。期間は値ではなく時刻で、仮押さえのあとに期間を出た実行は、押さえを片付けて終わらなければならない。期間を書く前も、範囲の外の `now` を koyomi の日付に渡せば、日付の呼び出しがタスクのエラーとして失敗し、`on failure` が走っていた。それより悪くしない。

**案件の検査**(`flow.rs`)。期間を書いたフローでは、`now` を読む文の前の確かめを、処理されなかったタスクのエラーで `on failure` に入りうるところとして数える。すぐあとの文が、処理されないエラーで失敗しうる呼び出しなら、入りうるところは増えないので数えない。`on failure` の無いフローで案件が開いたままになりうれば、W101 が「`now` がワークフローの動く期間の外になると、案件 `…` が … のままワークフローが失敗します」と言う。請求の例では、確かめる文はどれも、すぐあとの呼び出し(日付の呼び出しと、帳簿の操作)がもともと失敗しうるところなので、W101 は増えない。`tests/fixtures/period_case.flow` は、案件を始めたあとの `let at = now` の前の確かめが、それだけで入りうるところになり、すぐあとが呼び出しの確かめは増えないことを確かめる(W101 の「2 か所」)。

**プラットフォームでの書き方**(4 章の各節にも書いた)。どのプラットフォームでも、時計を一度読み、その時刻を 19 文字で期間の端と比べる。

- Step Functions:時計を読む Pass(走り始めは `period at the start`、読む文の前は `<行> now`)が、ステートに入った時刻を変数 `dd_now_<場所>` に入れ、次の Choice(`period at the start check`、`<行> period`)が比べる。走り始めの外は Fail、読む文の前の外は、`dd_error` にエラーを入れる Pass から `on failure` の入口へ進む(`on failure` が無ければ Fail、並列のイテレーションの中では、失敗をイテレーションの結果にして運ぶ)。
- Temporal(TypeScript・Python・Go):ワークフローのコードの `if` で、走り始めの外は `dd.fail`(Python は `raise dd.fail`、Go は `return nil, ddFail`)、読む文の前の外は `dd.TaskError`(Go は `&ddTaskError{…}`)を投げ、`on failure` を走らせる catch がそれを捕まえる。
- durable functions:時計は、その文の前の step(`<行> clock`)で読む(4.3)。比べ方と投げ方は Temporal と同じである。
- Argo:時計を読むテンプレート(走り始めは `dd-period-start`、読む文の前は `s<場所>-now`)と、比べるテンプレート(`s<場所>-period`)。外なら `dd_ctl` を、走り始めは `fail`、読む文の前は `task` にし、`dd_error` にエラーを入れる(4.4 の置き方のまま)。
- pydantic-graph:確かめる節点(`S<場所>_period`)が時計を一度読み、状態のフィールド `dd_now_<場所>` に入れる。走り始めの外は `dd.Failure`、読む文の前の外は `dd.TaskError` で、処理されないタスクのエラーと同じく `on failure` の節点へ進む。

E040 の見積もりには、Step Functions で確かめる文ごとに 6 件(Pass、Choice、外のときに進むステート)、Argo で 4 ノード(比べるテンプレート)を足す。Argo では、`now` を読む文ごとに時計を読むテンプレートの 4 ノードも足す(期間を書かないフローでも。下の項)。durable functions は、時計を読む step が、`now` を読む文から確かめる文に移り、走り始めの確かめの分の 1 操作が増える。Temporal はワークフローのコードの中で確かめるので、履歴は増えない。

**一つの文の `now` は一つの時刻**。期間で確かめた時刻をその文が使うには、文が時計を一度だけ読む必要がある。生成するコードを読むと、期間を書かないフローでも、そうなっていないところがあった。どれも直した。

- pydantic-graph:節点の中の `now` のそれぞれが `dd.now(deps.clock)` で時計を読んでいた。節点の頭で一度読む(`one_now`)。
- Temporal(TypeScript・Python・Go):呼び出しの引数は、`dd.attempt` が試しのたびに走らせるクロージャ(Python は `lambda` と `async def dd_call_…`)の中で作る。リトライのあいだのタイマーでワークフローの時計は進むので、試しのたびに違う `now` を渡していた。`now` を読む呼び出しは、その前で時計を一度読んで変数(`dd_now_<場所>`)に入れる。
- Argo:テンプレートの式は、それぞれ別に評価され、`now()` もそれぞれ読み直される。`now` を読む文ごとに、時計を読むテンプレート(`s<場所>-now`)を前に置き、文はその出力をグローバル出力パラメータで読む。`wait until` の秒数と、`timeout until` のコールバックの秒数は、`let` で一度読んだ時計から計算する(前は二度読んでいた)。
- Step Functions(ステートに入った時刻で、Retry のあいだも変わらない)と durable functions(step で読み、チェックポイントに残る)は、もともと一つの時刻を読んでいた。

例と試験のフローには、一つの文が `now` を二度書くところが無かったので、`tests/fixtures/now_twice.flow`(リトライする呼び出しの引数、レコードのフィールド、文字列、出力に `now` を二度ずつ書く)を足した。

**範囲の解析**。dandori の範囲の解析は、日付を 1970-01-01 からの日数、時刻と期間を UTC の秒で、数と同じ `Range` に持つ。範囲を書いたところに日付か時刻を渡すと、数と同じく E014 と W104 を出す。文は日付と時刻で言い、時刻はオフセットのある範囲ならその書き方で言う。

```console
エラー[E014]: tests/fixtures/date_ranges.ja.flow:35:1: `訪問.時間帯` は `>=2026-01-01 <=2026-12-31 offset +09:00` で、`知らせる` の引数 `時刻` の範囲 `>=2026-01-01 <=2026-06-15` を外れることがあります
    35 |   知らせる(id: 訪問.id, 時刻: 訪問.時間帯)
警告[W104]: tests/fixtures/date_ranges.ja.flow:47:1: `now` の範囲が分かりません(`on failure` と `on cancel` の中の `now` に範囲がありません)。`知らせる` の引数 `時刻` が受け取るのは `>=2026-01-01 <=2026-06-15` です
    47 |   知らせる(id: 訪問.id, 時刻: now)
  = `on failure` と `on cancel` の中の `now` は、後始末を時計で止めないよう、動く期間で確かめません。どの時刻にもなりえます
```

- 期間を書いたワークフローの本体の `now` は、期間の中の時刻である。期間を書かないワークフローの `now` と、`on failure` と `on cancel` の `now` は、どの時刻にもなりうる(W104 の注が、期間の書き方か、確かめない理由を言う)。
- koyomi の日付の `day` は、koyomi が入力の範囲の全部で数えた日の最初から最後まで、`at` はその日のその時刻をカレンダーのオフセットで UTC にしたものとして読む。dandori 自身の E014・W104 のためで、規則の呼び出しに渡す日は、これまでどおり ritsu が koyomi の日で決める(X2、X3 の (a))。koyomi の答えを走らせたときに確かめることはしない。
- 子の `.flow` の入力の範囲(E015)は、日付と時刻の範囲も、子のタスクの引数に書いた範囲と比べ、日付で言う(`contract.rs`)。

**口と境目**。ritsu の口は、範囲のある日付を `Origin::Range`(日の番号。kaname が日を渡すときと同じ)で、範囲のある時刻と、期間を書いたワークフローの本体の `now` を、新しい `Origin::Moment`(UTC の秒と、`.flow` の書き方の範囲)で渡す。期間を書かないワークフローの `now` と、`on failure` と `on cancel` の `now` は、これまでどおり `Origin::Now` である。時刻を何日に読むかは、日付のファイルのカレンダーが決める(1.16)ので、ritsu-cross がそのオフセットで日に読む(`borders::moment_days`)。

- X6:範囲の日(時刻は、カレンダーのオフセットで読んだ日)がどれも koyomi の入力の範囲に収まれば何も言わず、はみ出す日があれば E205、範囲の無いところから来る日だけが W205 として残る。開いた端の範囲は、その先のどの日も来うるので E205 である。
- X3 の (a)(E202)と、X2 の日の前提(`range from koyomi`、E201)も、同じ範囲で決める。日付どうしの関係の前提(`check_in <= check_out`)も、日の番号で数と同じに決める。
- 境目の記録(`ritsu check --format json` の `borders`)の `detail` には、渡す日の出どころを一つずつ、`day`(koyomi の日付)、`range`(日付の範囲)、`moment`(時刻の範囲)、`now`(期間か、期間の無い `now`)、`unknown`、`output` の種類と、日の範囲で載せる。

```console
error[ritsu E205]: reminding.flow:17:1: The day given to `received` of the koyomi date `terms.payment` can be outside the input's range
    17 |   let due = terms.payment(received: now)
  = `now` is a moment of the period the workflow runs in, `>=2026-01-01 <=2026-12-20` (UTC), which is a day of `>=2026-01-01 <=2026-12-21` in the calendar of "terms.cal" (+09:00) and can be 2026-12-21, and "terms.cal" takes 2026-01-01 to 2026-12-20 for `received`
  = koyomi's generated code refuses a day outside the range, so this call fails only when the workflow runs.
  = A day from a date or a time with a range, or from `now` in a workflow that says the period it runs in, is checked over the whole range; a time and `now` are read as days at the offset of the dates file's calendar.
  = Widen the range of `received` in "terms.cal" (and the data of its calendar), or narrow the period the workflow runs in (with `offset`, it is written in the calendar's days).
```

ritsu の `tests/cross.rs` の `x6-period-E205` で、UTC で書いた期間が、東に 9 時間のカレンダーでは一日はみ出すところである。同じ期間を `offset +09:00` で書けば成り立つ(`x6-period-held`)。

**シナリオ**(5 章)。

- 範囲のある日付と時刻の値は、範囲の中から作る。範囲がいまの値を入れなければ、日付は下の端からの日、時刻は下の端の日の 10:00 である。
- 範囲を書いた入力の場所ごとの、範囲のすぐ外の実行(`input:outside`、9 章)を、日付と時刻にも作る。値は上の端の次の日か、範囲のすぐあとの最初の秒で、上の端が無ければ下の端の前の日か前の秒である。
- 範囲の端の実行(`input:at an end`)も足す。日付と時刻の範囲を書いた入力の場所の端ごとに、最初の実行の入力のその値だけを、日付なら最初の日か最後の日、時刻なら最初の秒か最後の秒のあとに小数の秒(`.5`)を付けたものにする。どのプラットフォームも受け付けなければならない。上の端を `<` で比べる誤りと、時刻の小数の秒を端と比べる誤りが、ここで出る。
- 実行の `now`:期間の中なら `2026-03-31T15:30:00Z` のまま。外なら、期間の中で、これにいちばん近い「月の最後の日の 15:30:00Z」にする(`+09:00` では次の月の 1 日になり、オフセットの読み違いが突き合わせに出る、という性質を保つ。1.16)。期間が月の終わりを含まなければ、期間の最初の秒である。
- 期間を書いたフローには、期間のすぐ外(終わりのすぐあとの最初の秒、終わりが無ければ始まりのすぐ前の秒)で走り始める実行(`now:outside`)を一つ作る。何も呼ばずに `Dandori.OutOfPeriod` で終わる。
- 読む文の確かめで落ちる実行のために、シナリオの `now` を、時刻の並び `[走り始め, それより後]` でも書けるようにした。参照インタプリタは、走り始めの確かめで最初の時刻を、それより後の確かめで次の時刻を読む。生成器は、`now` を読む文があるフローの実行で、最初の確かめが期間の外を選んだら、その実行の `now` を、期間の中の時刻と期間のすぐ外の時刻の並びにする(その確かめの場所の `<場所>:outside`)。

**ランナー**。七つのランナーは、`now` を読むコードを一つの時刻に書き換えていたところを、実行の時刻の並びを順に返すものにした。Temporal の三つ・durable functions・pydantic-graph はコードのコピーの時計の関数を、Step Functions は `tools/asl-run.mjs` がステートに入った時刻を、LocalStack は定義の時計の式を、Argo は時計を読むテンプレートの式(`dd-period-start` と `s<場所>-now`)を置き換える。Argo の本物の Pod の回は、一つのフローの実行がどれも同じ時刻の組を使う(違えばランナーが止まる)。`ritsu run` も `now` をシナリオの時刻(時刻の並びなら、走り始めとそれより後)で読み、待ちで時計が期間の終わりを過ぎても `now` は進めない。`dandori run` と同じ跡にするためである(5 章)。

**図**(5.2)。図の頭に「動く期間は `now range …` です。」を書き(HTML は start の詳細の「動く期間」)、終わり方に `Dandori.OutOfPeriod`(ワークフローが動く期間の外の `now`)を足した。読む文ごとの確かめはノードにせず、`on failure` の説明に「…行目の `now` がワークフローの動く期間 `…` の外のときにも走ります。」と、どの行から来るかを書く。HTML の、そこで落ちた実行の印は「now がその外」である。`Dandori.BadInput` と `Dandori.BadResponse` の説明は、範囲の外の値でも出るので、「宣言した型か範囲に合わない入力」「宣言した型か範囲に合わない結果」に直した(9.7 に残していたもの)。

**Lean**(ritsu の DESIGN 11.2)。DandoriCore の `fits` に日付と時刻の範囲を、生成するコードと同じ文字列の比べで足し、確かめる文の三つの実行(期間の中なら先へ進む、走り始めで外なら何も呼ばずに終わる、それより後で外ならタスクのエラーのあとと同じくブロックを出る。`Exec.periodIn`・`periodStart`・`periodOut`)を足した。E020 の検査のモデル(`chk`)は、後のほうの確かめを、処理されないエラーで出うるところとして数え、`exec_sound` と `chkFlow_sound` はそのまま成り立つ。RitsuCross の `daysGiven` は範囲の並びも受け取り、定理を足した(`daysGiven_fails_date`・`daysGiven_fails_range`、`rangeOutside_none`・`_outside`・`_in`、`momentDays_mem`、`dayNotIn_none`・`_some`)。ritsu-model の `tests/dandori.rs` は新しい試験のフローのシナリオを Lean と参照インタプリタで、`tests/cross.rs` は X6・X3 の (a)・X2 の答えを Lean と ritsu-cross で突き合わせる。

**見本の例**。請求の例の英語版に `now range >=2026-01-01 <=2027-12-31`、日本語版に `now range >=2026-01-01 <=2027-12-28 offset +09:00` を書いた。支払条件のファイルが受け付ける日と同じ期間で、日本語版のカレンダーは `+09:00` なので、期間もそのオフセットで書く。W205 は二つとも消え、`ritsu check` は次のように言う(残る 6 件は、仮押さえのタスクに `timeout` が無いことから来る W206)。

```console
ritsu check: 8 files (koyomi 4, chobo 2, dandori 2): all pass (6 warnings); borders between the languages: 8 checked, 6 undecided
```

試験のフロー `tests/flows/date_ranges.flow` と `date_ranges.ja.flow` は、入力、レコードのフィールド、タスクの引数と結果、出力、子の入力(`date_ranges_child.flow`)の日付と時刻の範囲、`offset` のある時刻、期間、待ちのあとの `now`、koyomi の日付に渡す `now` と、`on failure` の中の確かめない `now` を、どのプラットフォームでも走らせる。

### 20.4 捨てたもの

- **期間の外の `now` を、`Dandori.BrokenPrecondition` と同じく `on failure` で処理しないこと。** 期間の終わりをまたいで走る実行が、仮押さえのあとの `now` で落ちたとき、後始末をせずに終わる。案件の検査(E020・W101)も、前提の確かめと同じに数えれば、それを言わない。
- **読む文ごとの確かめだけにすること**(走り始めを確かめない)。期間を過ぎて始まった実行が、仮押さえをしてから最初の `now` で落ちる。
- **UTC の日だけで書くこと**(`offset` を書けない)。オフセットのあるカレンダーの日付のファイルと、期間をちょうど合わせられない。日本語版の請求の例は、期間の終わりを 2027-12-27 と書き、なぜ 28 日でないのかを注で言うことになる。
- **時刻の範囲の端を RFC 3339 の時刻で書くこと。** 秒まで書く業務の期間はまれで、端の比べに小数の秒の扱いが増える。
- **新しい語で期間を書くこと**(`period >=… <=…`、`runs from … to …`)と、一番外の行に書くこと。範囲の語をそのまま使えば、入力の範囲と同じく走らせたときに確かめるものと読める。実行のしかたを決める行は `workflow` の下にある(`history encrypted`)。
- **エラーの名前 `Dandori.BadNow`・`Dandori.NowOutOfRange`。** 前者は時計がおかしいように読め、後者は式の語と構文の語を混ぜる。宣言の言葉(動く期間)で言う `Dandori.OutOfPeriod` にした。
- **開いた端を、X4 の額と同じく決められない(W)にすること。** koyomi の入力にも koyomi の日の集合にも端があるので、開いた範囲には必ず外の日がある。dandori 自身の E014 も、開いた範囲を収まらないとする。
- **dandori が、カレンダーのオフセットで時刻を日にして、全部を `Origin::Range` で渡すこと。** 注で `now` と期間を言えなくなる。全部を新しい `Origin::Days` にすることは、kaname の側もそろえることになる。
- **X5 を期間で狭めること。** koyomi の口に、入力を狭めた日の数えを足すことになる。答えの正しさは変わらず、見本の例の W206 は `timeout` が無いことから来るので減らない。
- **読む文ごとの確かめを、図のノードにすること。** `now` を読む文ごとにノードが一つ増え、読む人が知りたいのは期間そのものである。
- **koyomi の日付の答えを、走らせたときに koyomi が数えた日で確かめること。** 日付を使うフローの生成物がどれも変わる。koyomi が数えた日は、dandori の検査でだけ使う。
- **Temporal で、試しのたびに時計を読み直すこと。** 期間で確かめた時刻と、二度目からの試しが渡す時刻が食い違い、試しごとに `now` が変わる。

### 20.5 変わること

- **前は通っていた実行が落ちる変更は無い。** 範囲も期間も書かないフローは、範囲の検査もシナリオも変わらない。範囲や期間を書いたフローは、その外の値と時刻で落ちる。新しい書き方のフローは、0.39.0 の dandori では E001 になる。
- **診断の文**。範囲を書けない型の E003(「数(`int` か単位)、順序のある列挙、日付か時刻」)と、`workflow` の下に書けるものの E001(「`history encrypted` と `now range`」)の文が変わる。golden では `enum_range_syntax`、`values`、`security/E001_workflow_の下` の英日が変わった。
- **期間を書かないフローの生成物**。替える前(0.39.0 の公式のバイナリ)と後の `ritsu dandori build` を、例と試験の全部の `.flow`、七つの生成先で比べた(数は 20.6)。
  - Temporal の TypeScript・Python・Go:`now` を読む呼び出しの前で時計を一度読む(`const dd_now_<場所> = dd.now();` など)。時計を読むのはワークフローの中の関数で、コマンドを出さないので、記録した履歴の再生は食い違わない。リトライの二度目からの試しが渡す `now` は、最初の試しの時刻になる。ワーカーのビルドの ID(`worker.*`)が変わるので、Worker Deployment Versioning では新しいビルドとして扱われる。
  - pydantic-graph:`now` を読む節点は、頭で一度時計を読む。
  - Argo:`now` を読む文ごとに、時計を読むテンプレート(`s<場所>-now`)が前に入る。`wait until` と `timeout until` の秒数の式は、`let` で一度読んだ時計から計算する。E040 の見積もりは、`now` を読む文ごとに 4 ノード増える。
  - Step Functions と durable functions は変わらない。
- **シナリオと `ritsu check`**。範囲も期間も書かないフローは変わらない。日付と時刻の範囲や期間を書くと、W205・W202・W201 だったものが、成り立つか、E205・E202・E201 になる。請求の例の要約は「all pass (6 warnings); borders between the languages: 8 checked, 6 undecided」になる。
- **図**。`Dandori.BadInput` と `Dandori.BadResponse` の説明が変わったので、それを出す例のページ(`fulfillment`・`hotel`・`inquiry`・`invoice`・`order`・`payout`・`review` の英日)と `tests/doc` の golden が変わる。請求の例は、図の頭の期間と、終わり方の `Dandori.OutOfPeriod`、`on failure` の説明も変わる。
- **ritsu-cross の台帳**。W205 の「いつ出るか」と直し方(範囲か期間を書けば決まる)、E205(範囲と期間から来る日の確かめ方)、E202・W202、W201 の文が変わる。

### 20.6 確かめたこと

- **書き方と診断**。`tests/fixtures` の `date_range_syntax`・`date_ranges`・`date_ranges_no_period`・`date_range_child`・`children/visit`・`period/`・`period_case`・`now_twice` の英日の golden(`diagnostics_match_the_golden_files`)。前からの golden で変わったのは、`enum_range_syntax`・`values`・`security/E001_workflow_の下` の E003 と E001 の文だけである。
- **シナリオ**(`tests/examples.rs`)。
  - `every_input_held_to_a_range_is_tried_just_outside_it`:範囲を書いた入力の 48 の場所(30 本のフロー。9.6 では 26 本の 36)に、範囲のすぐ外の実行が一本ずつあり、参照インタプリタはどれも何も呼ばずに `Dandori.BadInput` で終える。
  - `every_date_or_time_held_to_a_range_is_tried_at_its_ends`:4 本のフローの、範囲の端の実行 20 本を、参照インタプリタは受け付ける。
  - `a_workflow_that_runs_in_a_period_is_tried_outside_it`:期間を書いた 4 本のフローに、期間の外で走り始める実行が 4 本、走り始めのあとで期間を出る実行が 6 本ある。参照インタプリタは、前者を何も呼ばずに終え、後者を `on failure` に入れる。
  - `a_statement_reads_the_clock_once`:`now` を読む 16 本のフロー(例と試験のフローと `now_twice`)の生成したコードで、時計を読む 147 か所を数え、試しのたびに走るクロージャ(393)の中には一つも無く、pydantic-graph の節点と Argo のテンプレートには一つまでしか無い。
- **七つのプラットフォーム**。platforms の段の `tests/examples.rs` の 35 の試験が通った(外の API と本物のデータベースに送る五つは、この変更が触らないので外した)。新しい試験のフローは、`date_ranges` の英日が 24 本、`date_ranges_child` の英日が 9 本の実行で、Temporal の TypeScript・Python・Go(アクティビティをほかの言語で走らせる回も)、durable functions、pydantic-graph、LocalStack 4.14.0 の Step Functions、kind の上の Argo v4.1.4(演じる回と本物の Pod の回)のどれでも、参照インタプリタと同じに終わる。Step Functions の `tools/asl-run.mjs` の突き合わせは 738 本(9.6 では 659 本)である。Temporal の記録した履歴の再生(`temporal_replays_the_recorded_histories`)も通る。`now` を読む呼び出しの前で時計を読んでも、コマンドは出ない。
- **変異**(わざと誤りを入れて、試験が落ちるか)。
  - 日付の範囲の比べを、生成するコードから落とす:platforms の段の八つの突き合わせ(参照インタプリタとの突き合わせを、七つのプラットフォームとローカルの Step Functions で)がどれも落ちる。
  - 日付の上の端を `<` で比べる:八つとも、範囲の端の実行で落ちる。
  - `now` を読む文の前の確かめを、六つの生成器のどれでも、時計を読むだけにする:八つとも、時刻の並びの実行で落ちる。
  - 時刻を先頭の 19 文字でなく全体で比べる:落ちない。端を 19 文字で書くので、小数の秒を持つ時刻も同じ答えになる(同じに働く変異)。端を `Z` 付きで書いて全体で比べると、Step Functions・Temporal・durable functions の突き合わせが、範囲の端の実行で落ちる。
  - X6 で、時刻を日に読むオフセットの向きを逆にする:ritsu-cross の `x6_the_days_of_moments_at_a_calendars_offset` と、ritsu の `x6_a_day_from_a_range_or_the_period`(`+09:00` で一日はみ出す試験)が落ちる。
  - `days_given` が範囲を決められないことにする:ritsu-cross の `x6_a_day_given_to_a_koyomi_date`・`x3a_the_days_of_a_koyomi_date_against_a_rules_range` と、ritsu の `x6_a_day_from_a_range_or_the_period`・`x3a_a_date_held_to_a_range_given_to_a_rule` が落ちる。
  - 一つの文の `now` を一つにする直しを、Temporal の三つの言語、pydantic-graph、Argo の文ごとの時計のテンプレート、Argo の `wait until` の `let` のそれぞれから外す:どれも `a_statement_reads_the_clock_once` が落ちる。
  - `now` を読む文の前の確かめを、案件の検査で入りうるところとして数えない:`period_case` の golden(W101)が落ちる。
- **Lean**。`lake build` が通り、ritsu-model の `tests/dandori.rs`(試験のフローのシナリオの全部を、Lean のモデルと参照インタプリタで)、`tests/cross.rs`(X6・X3 の (a)・X2 の答えを、Lean と ritsu-cross で。10 の試験)、`tests/proofs.rs`(`sorry` が無く、どの宣言も Lean の三つの公理の上にある)が通る。
- **口と境目**。dandori の `tests/ports.rs`、ritsu-cross の `tests/borders.rs`、ritsu の `tests/cross.rs` の範囲と期間の試験(範囲が収まる、はみ出す、開いた端、期間が収まる、オフセットで一日はみ出す、期間の無い `now`、`on failure` の中の `now`、範囲のある日付を規則へ、日の前提へ)と、その golden。境目の記録の `detail` の出どころは `tests/golden/cross/records-x6.json` で確かめる。
- **`ritsu run`**。ritsu の `tests/run.rs` の `a_run_past_the_end_of_its_period_fails_where_it_reads_now` が、試験の中で作る小さなプロジェクトで、期間の中で走り始めて 3 日待ったあとに期間の外の `now` を読む実行が、そこで `Dandori.OutOfPeriod` になって `on failure` が仮押さえを取り消すことと、期間のあとで走り始める実行が何も呼ばないことを、英日の文で確かめる。
- **替える前と後**(20.5)。0.39.0 の公式のバイナリと、この木から組んだバイナリの `ritsu dandori` を、例と `tests/flows`・`tests/children`・`tests/fixtures` の 221 本の `.flow` にかけ、15 の出力(`check` の英日のテキストと JSON、七つの生成先の `build`、`doc` の英日の Markdown と HTML、`scenarios`)を比べた。0.39.0 で検査を通る 125 本では、`check`・`scenarios`・`doc` の Markdown は一字も変わらない。`doc` の HTML は 110 本の英日で変わり、変わったのは `Dandori.BadInput` と `Dandori.BadResponse` の説明だけだった。`build` は、Step Functions と durable functions では変わらない。変わるのは、Temporal の三つの言語で 13 本(ワークフローと、ワーカーのビルドの ID)、pydantic-graph で 12 本、Argo で 8 本で、どれも一つの文の `now` を一つにする直しである(新しいフィクスチャの `now_twice` を含む)。0.39.0 で検査を通らない 96 本のうち、出力が変わったのは、新しい書き方を使う 23 本(0.39.0 では E001)と、E003・E001 の文が変わった 4 本である。`ritsu check` を、`.flow` のある 33 のディレクトリ(dandori の例と試験のフロー、ritsu のテストのプロジェクト)で英日のテキストで比べると、変わったのは請求の例(W205 の 2 件が消え、要約は 20.3 のとおり)と `tests/flows`(新しい試験のフローが通り、確かめた境目が 2 か所増える)だけだった。

### 20.7 見つけて直していないもの

- **Argo の `workflow template` の子が失敗したときの理由は空である。** resource のテンプレートはエラーのファイルを持たないので、子の失敗は `Cause` の無い `failure` として親に届く。新しい試験のフローで、子が範囲の外の入力で落ちる実行が初めて試験に入り、参照インタプリタの Argo の見え方が `scripted` と言って食い違った。参照インタプリタの Argo の見え方も、子の失敗の理由を空にした(7 章の、子が宣言したエラーを見分けることと同じ所にある)。
- **シナリオの時刻は、走り始めと、それより後の二つまでである。** それより後のどの確かめも同じ時刻を読むので、二つ目の確かめのあいだに期間を出る実行(最初の読む文では中、次の読む文では外)は作らない。最初の読む文で外になる実行だけを作る。
- **Argo の本物の Pod の回は、一つのフローの実行がどれも同じ時刻の組を使う。** 時刻はワークフローのテンプレートに書き込むので、実行ごとに違う時刻を使うには、実行ごとにテンプレートを作り直すことになる。演じる回は、実行ごとの時刻で走らせる。
- **`on failure` の中の `now` の W205** は、期間を書かないワークフローの `now` と同じ文(「`now` はどの日にもなりえます」)で、期間が `on failure` の `now` を確かめないことを言わない。dandori の W104 の注は言う。
- **規則の日付の出力から来る日**は、W205 のままである。rulec の口が日付の出力を数えない(`output_values` は数でない出力に決められないと答える)ので、rulec に手を入れることになる。
- **`.proto` から作ったレコードの時刻と日付の範囲**は読まない。Protovalidate の `timestamp` の規則は秒の時刻と `lt_now` などで、日の範囲にならないものがある。
- **暦に無い日**(`2026-02-30`)は、値としては、これまでどおり形が合えば通り、koyomi が受け付けない。範囲の比べは、そのような日を範囲の中と読むことがある。範囲の端には書けない(E003)。
- **`.flow` の `range from koyomi`**(koyomi の日付の日だけを受け取る入力)は書けない。
- **kaname の W205** は変わらない。kaname は日を日の番号の範囲で渡すが、kaname の側で範囲を書く形は変えていない。
- **`ritsu run` の `now` は、待ちで進まない。** 期間の終わりを過ぎて `now` を読む実行は、シナリオの `now` を時刻の並びにして書く(20.3)。時計とともに `now` を進めるなら、`dandori run` の跡と、ritsu の `tests/run.rs` の、跡が同じであることの確かめを変えることになる。