DESIGN.md@crates/koyomi · git:20261105.fbcb5b7 · 2026-11-05 · sha256 43ae44038d98421f

DESIGN.md@crates/koyomi git:20261105.fbcb5b7B

Immutable. This exact content never changes and is served at /api/v1/blob/43ae44038d98421f.

# koyomi 設計文書

締めと支払、営業日、月の足し算を書く小さな言語。ファイルは `.cal`、コマンドは `koyomi`。書いた条件を範囲のすべての日について総当たりで確かめ、条件を満たさない日付をそのまま出す。検査を通ったものだけを、TypeScript・Python・Go・Rust・SQL(PostgreSQL)のコードと、どのホストからも呼べる Wasm のモジュールにする。

名前は暦(こよみ)から取った。

この文書は設計の段階(A)で書き、言語の芯を作った段階(B)、出力先を作った段階(C)、人が読むページと文書を作った段階(D)で直した。B で作ったのは、字句と構文、名前と型、カレンダー、参照インタプリタ、検査、診断、`check`・`eval`・`explain`・`api` のコマンドである。C で作ったのは、五つの出力先の生成(`gen`)、`vectors`、生成したコードと参照インタプリタの突き合わせ、`source fetch`・`pin`・`outdated` である。D で作ったのは、`doc`(7 章)、例の仕上げと英語の版(10 章)、`docs/`、README.md と README.ja.md、エージェント向けのスキル(`skills/koyomi`)、THIRD_PARTY_NOTICES.md である。3〜10 章に載せた出力とコードは、その段階の koyomi が実際に出したものに差し替えた。そのあと ritsu に移ってから、例とテストの材料を英語を先にした(England and Wales の例、日本の暦の例の英語の版、英語の名前のテストの材料と変異を足し、日本語のものは日本語の版として残した。10.1)。

文書に出てくる日付と日数は、A の段階で、使い捨ての試作(Python)が内閣府と GOV.UK の祝日の表から計算したものである。B の参照インタプリタが同じ数を出すことを、`tests/check.rs` が確かめている(一つも食い違わなかった)。

## 0. 全体像

```
.cal ── 字句・構文 ── 名前と型 ── カレンダー(出典のコピーを読む。通信しない)
                                      │
                                      ├── 参照インタプリタ(意味を決める。計算の段を残す)
                                      │     ├── check:範囲のすべての入力で、条件・無い日・データの範囲・例を確かめる
                                      │     ├── eval:一つの入力の計算を一段ずつ見せる
                                      │     ├── vectors:範囲のすべての入力の結果
                                      │     └── doc:人が読むページ(月の表、言い直し、エッジケース)
                                      ├── api:関数・引数・カレンダー・データの範囲・出典のハッシュ(JSON)
                                      └── gen:TypeScript・Python・Go・Rust・SQL・Wasm(祝日の表を埋め込む)
```

### 0.1 芯にする主張

期日の決まりは、書いたあとで全部の日を試せる。100 年分でも 36,525 日で、3 万 7 千日に満たない。だから koyomi は、条件を式の上で解くことも、一部の日を選んで試すこともせず、宣言した範囲の日を一日ずつ全部計算する。条件を満たさない日があれば、その日付を全部挙げ、最初の日がどう計算されたかを一段ずつ見せる。

看板の言い方は、次を候補にする。rulec の看板(Write rules. Prove them. Compile them.)と同じ三つの句にした。真ん中を prove にしない理由は 3.4 にある。

- **Write due dates. Check every day. Compile them.**
- **期日を書く。全部の日で確かめる。コードにする。**

### 0.2 なぜ別の言語にするか

「20 日締め翌月 10 日払い、休みなら前の営業日」のような計算は、いまの rulec でも dandori でも書けない。

- rulec は日付を順序だけの型にしていて、足し算と引き算を E048 でエラーにする(rulec の DESIGN §2.1・§2.3・§15.84)。rulec の §0.1 は、時刻も在庫も引数で渡すと決めている。
- dandori は `wait until <timestamp>` で時刻を受け取るだけで、営業日も締めも計算しない(dandori の DESIGN §1.5)。

そのため、こうした計算はいま利用者が書くコードの中にある。そこには食い違いの元が三つある。月の足し算はライブラリごとに結果が違う(1.7 の表。2023-01-31 の 1 か月後は、2023-02-28 になるものと 2023-03-03 になるものがある)。祝日の表は法改正で変わり、毎年 2 月に翌年分が出る。フロントエンドとバックエンドで別々に書けば、支払期日がずれる。

期日の決まりは、rulec が対象に選んだ五つの性質(rulec の §0.1)を全部持っている。お金が動く(支払期日)。元の文書が外にある(契約書、約款、法令、祝日の表)。条件が絡み合う(締め、月の足し算、休みの扱い)。期日で改定される(祝日の表が毎年延び、法令が変わる)。書く人と決める人が違う(書くのは開発者、決めるのは経理や法務)。だから rulec の作り方がほぼそのまま使える。出典を固定して改正を知る、人が読むページ、ベクタ、多くの言語へのコード生成、参照インタプリタとの突き合わせ、である。

**決定**:rulec に足さず、別の言語にする。

理由は、確かめ方が違うことにある。rulec のセルは自分の列への単項テストなので、表の一行が区画になり、抜けと重なりを区画の代数で決められる(rulec の §6.2)。期日のほうは、有限の範囲の日を全部計算して確かめる。二つを一つの言語に入れれば、rulec の小ささ(だから証明できる)が崩れる。dandori を rulec の外に作ったのと同じ理由である(dandori の §0.2)。

**費用**:構文、検査、生成、確かめ方を、もう一揃い作ることになる。書き方は rulec と dandori にそろえ(0.4)、覚えることを減らす。

### 0.3 前提

- **P1**:意味を決めるのは参照インタプリタ一つ。check・eval・vectors・doc はどれもそれを通す。生成したコードは、範囲のすべての入力でそれと突き合わせる(6.4)。
- **P2**:検査は総当たりで、サンプリングはしない。範囲のすべての日(整数の入力があれば、その範囲のすべての値との組)を一つずつ計算する。予算を超えたら、確かめていないと言って止まる(3.2)。
- **P3**:無い日の扱い(1 月 31 日の 1 か月後をどうするか)は、書く人が書く。ツールは既定を持たない(1.7)。
- **P4**:祝日は表で持ち、計算しない。表は出典のファイルを sha256 で固定し、表が分かっている範囲(データの範囲)を宣言する。データの範囲の外の日が営業日かを問う計算は、検査でも生成したコードでも通さない(2.4)。
- **P5**:ループと一般の条件分岐を持たない。日付の関数は、操作を上から順に並べたものである。条件分岐は「休みなら〜する」の一つだけ(1.8)。
- **P6**:時刻は固定の UTC オフセットだけを扱う。夏時間のあるタイムゾーンは受け付けない(1.9)。
- **P7**:rulec と dandori は変えない。koyomi はどちらも読まず、どちらからも読まれない。将来 dandori が読めるよう、`koyomi api` で JSON を出しておく(8 章)。

### 0.4 rulec と dandori にそろえるもの

| | そろえるもの |
|---|---|
| 書き方 | キーワードは英語の一種類だけ。名前は日本語で書ける。公開する名前には ASCII の別名(rulec の §1.3)。範囲は `range >=… <=…`。例は `examples` のパイプの表で、出力の列は `->` |
| 出典 | `source <名前> = file "…" url "…" sha256:<16 桁>`、`koyomi source fetch \| pin \| outdated`。`check` は通信しない(rulec の §15.68・§15.76) |
| 診断 | dandori の形(`error[E301]: <ファイル>:<行>:<列>: …`、原文の行、そうなる例)。英語が既定で、`--lang ja` で日本語。台帳は一か所にあり、`koyomi explain` が引く |
| CLI | コマンドとフラグを一枚の表に置き、知らないフラグは exit 2(rulec の §12.1)。exit code は 0・1・2 |
| 生成物 | 依存の無い読めるコード。どの言語にも同じ手順の小さな関数を生成物の中に置き、言語の標準の日付の計算は使わない(1.7、6.1)。頭に元の `.cal` と出典のハッシュを書く |
| 確かめ方 | 参照インタプリタとの突き合わせ。ツールが無ければ `SKIP:` の行を出して通す |
| 語 | 「証明」は否定形でだけ使う(rulec の §15.47。3.4) |

そろえないもの:決定表、ヒットポリシー、単位と丸めの型。koyomi が扱う値は日付と整数だけである。

## 1. 言語

### 1.1 二種類のファイル

**決定**:`.cal` のファイルは二種類ある。一行目の語で分かれる。

- `calendar`:カレンダーを書く。どの日が休みか(休みの曜日、毎年の休み、祝日の表、特定の日)、例外の営業日、UTC オフセット。
- `dates`:日付の関数を書く。一つの日付(と整数)を受け取り、そこから決まる日付を返す。条件と例もここに書く。カレンダーは `use calendar "<ファイル>"` で読む。

例は英語の版を先に置き、日本語の版を並べる(10.1)。英語の版:

```
calendar tokyo_business_days v1
description "Saturdays, Sundays, Japan's national holidays and other days off, and 29 December to 3 January are closed. The English version of 東京の営業日.cal"
offset +09:00

source national_holidays = file "data/syukujitsu.csv" url "https://www8.cao.go.jp/chosei/shukujitsu/syukujitsu.csv" sha256:cec37a743c96995c
  format csv shift_jis
  covers 1955-01-01..2027-12-31

closed weekly sat, sun
closed national_holidays
closed every 12-29..01-03 "New Year holidays"
```

```
dates payment_20th_close_next_10th v1
description "Closes on the 20th; pays on the 10th of the next month, or on the business day before when that day is closed. The English version of payment_20th_close_next_10th.ja.cal. The claim within_60_days_of_receipt is this example's own, written as it reads; it does not say how any law is read"
use calendar "calendars/tokyo_business_days.cal"

inputs
  received : date  range >=2026-01-01 <=2027-11-20

date closing = received
  close day 20          # "closes on the 20th"

date payment = closing
  day 10 of month +1    # "pays on the 10th of the next month"
  roll preceding        # "on the business day before when that day is closed"
  at 09:00

claims
  paid_on_a_business_day      : payment is open
  within_60_days_of_receipt   : payment <= received + 60 days
  later_receipt_later_payment : payment is monotonic

examples
| received   | -> closing | -> payment |
| 2026-04-01 | 2026-04-20 | 2026-05-08 |
| 2026-12-21 | 2027-01-20 | 2027-02-10 |
```

日本語の版(名前を日本語で書き、ASCII の別名を付けたもの。計算と結果は英語の版と同じ):

```
calendar 東京の営業日(tokyo) v1
description "土日、国民の祝日と休日、12 月 29 日から 1 月 3 日までを休む"
offset +09:00

source 祝日 = file "data/syukujitsu.csv" url "https://www8.cao.go.jp/chosei/shukujitsu/syukujitsu.csv" sha256:cec37a743c96995c
  format csv shift_jis
  covers 1955-01-01..2027-12-31

closed weekly sat, sun
closed 祝日
closed every 12-29..01-03 "年末年始"
```

```
dates 支払条件(payment_terms) v1
description "20 日締め翌月 10 日払い。支払日が休みなら前の営業日。条件「受領から60日以内」は、この例の条件として文字どおりに書いたもので、法令の読み方を示すものではない"
use calendar "calendars/東京の営業日.cal"

inputs
  受領日(received) : date  range >=2026-01-01 <=2027-11-20

date 締め日(closing) = 受領日
  close day 20          # 「20 日締め」

date 支払日(payment) = 締め日
  day 10 of month +1    # 「翌月 10 日払い」
  roll preceding        # 「支払日が休みなら前の営業日」
  at 09:00

claims
  営業日に払う       : 支払日 is open
  受領から60日以内   : 支払日 <= 受領日 + 60 days
  遅い受領は遅い支払 : 支払日 is monotonic

examples
| 受領日     | -> 締め日  | -> 支払日  |
| 2026-04-01 | 2026-04-20 | 2026-05-08 |
| 2026-12-21 | 2027-01-20 | 2027-02-10 |
```

「受領から60日以内」は、この例の条件として文字どおりに書いたもので、何かの法令の読み方を示すものではない。

節の順序は決まっている。calendar のファイルは、見出し、`description`、`offset`、`use calendar`、`source`(表と法令)、`closed` と `open` の順。dates のファイルは、見出し、`description`、`use calendar`、`source`(法令だけ。表はカレンダーに書く)、`inputs`、`date`(いくつでも)、`claims`、`examples` の順(違えば E004)。`closed` と `open` は混ぜて書いてよい(どちらが先かに意味は無い。1.4)。`description`、`offset`、`use calendar`、`inputs`、`claims`、`examples` は一つのファイルに一度までで、二度あれば E004。表の出典に `format` か `covers` の行が無いときも E004 にした(どちらも省けないので)。字下げは、節の中身と、日付の操作と、出典の下の行を表す(スペースで書き、揃っていなければ E005。タブは使えない)。見出しの `v1` は人のための目印で、koyomi はそれを生成物の頭と `api` に書き出すだけである(rulec と同じ)。

受領日の範囲の終わり 2027-11-20 は、祝日の表(2027 年まで)から決まる。2027-11-21 に受領すると、締め日は 2027-12-20、支払日は 2028-01-10 になり、その日が休みかを表は知らない(4.3 の E203)。三つの条件は、受領日 2026-01-01〜2027-11-20 の 689 日のすべてで成り立つ。受領から支払までがいちばん長いのは 51 日(受領日 2026-07-21、2026-12-21、2027-07-21)、いちばん短いのは 18 日である。例の一行目は、翌月 10 日の 2026-05-10 が日曜で、土曜も休みなので、2026-05-08(金)に動く。

**理由**:カレンダーは会社に一つで、期日の決まりは取引先や契約ごとにある。同じカレンダーを多くの dates のファイルが読む。カレンダーだけのファイルは、それだけを総務や経理が読んで確かめられる(`koyomi doc` で月の表になる。7 章)。

**捨てたもの**:

- dates のファイルの中にカレンダーを書くこと。書ける場所が二つになり、同じカレンダーが何か所にもコピーされて食い違う。
- 一つの見出し(`calendar`)で両方を書くこと。「calendar 支払条件」は支払条件をカレンダーと呼ぶことになり、人が読むページで読み違える。

calendar のファイルも `use calendar` で別のカレンダーを読める。読んだカレンダーの休みに、自分の休みを足す(国の祝日のカレンダーに、会社の休業日を足す形)。決まりは 1.4 にある。

### 1.2 キーワードと名前

**決定**:キーワードは英語の一種類だけで、日本語の綴りや略語は置かない。名前(ファイル、入力、日付、条件、出典)は日本語で書ける。公開する名前(ファイルの見出しの名前、入力、日付)には、ASCII の別名を丸括弧で付ける。別名は `[a-z][a-z0-9_]*` で、生成したコードの識別子になる。条件と出典の名前は生成したコードに出ないので、別名は要らない。名前がもとから `[a-z][a-z0-9_]*` の形なら、別名は要らない(`dates net30 v1`、`invoice_date : date …`)。別名を求めるのは、漢字とかなに大文字が無く Go の公開する識別子になれないからで、`invoice_date` にはその問題が無い(rulec と同じ決まり)。

名前の頭が数字でも、日本語が続けば名前として読む(`40営業日以内`、`142条の満了日も単調`)。`30days` のように ASCII だけで数字から始まるものは、数と語のあいだの空白の書き忘れとして E001 にする。

語彙は次で全部である(B の段階で `src/kw.rs` の一枚の表にし、`explain` と予約語の検査はそこから引く)。

| 位置 | 語 |
|---|---|
| 行頭 | `calendar` `dates` `description` `offset` `source` `closed` `open` `use` `inputs` `date` `claims` `examples` |
| 出典 | `file` `url` `law` `asof` `sha256:` |
| 出典の下 | `format` `covers`、形式 `csv` `govuk`、文字コード `utf8` `shift_jis`、`listed years` |
| カレンダー | `weekly` `every`、曜日 `mon` `tue` `wed` `thu` `fri` `sat` `sun` |
| 型と範囲 | `date` `int` `range` |
| 操作 | `+` `-` `day` `days` `business` `month` `months` `year` `years` `of` `start` `end` `close` `roll` `following` `preceding` `modified` `if` `closed` `else` `at` |
| 無い日の扱い | `end_of_month` `start_of_next_month` `reject` |
| 条件 | `is` `open` `monotonic` `=` `<` `<=` `>` `>=` |
| 記号 | `->`(例の出力の列)、`..`(日の区間。両端を含む)、`@`(法令の引用)、`#`(行末までコメント) |

`day` と `days`、`month` と `months` は、英語の文法のための同じ語の形で、意味の違う二つの綴りではない(dandori の時間の書き方と同じ)。

**理由**:rulec の §1.1 と同じである。同じ意味の綴りが二つあると、grep と diff の両方で困る。キーワード(ASCII)と業務の語(日本語)が見た目で分かれる。

同じ意味の書き方が二つ残る場所が三つある。`day 31 of month +k else end_of_month` は `end of month +k` と同じ、`day 1 of month +k` は `start of month +k` と同じ、`close day 31 else end_of_month` は `close end of month` と同じである。rulec には書き方をそろえる `fmt` があるが koyomi にはまだ無いので、短いほうを勧める警告(W202)で一つにそろえる(`day 1 of month` は B の段階で足した。言い直しの文が `start of month` と同じ「翌月 1 日」になり、二つを書き分ける理由が無いため)。

日の区間を `..` で書くのは、日付には「以下」と「未満」の取り違えが起きないからである。`2026-08-13..2026-08-15` は 13 日と 15 日を含む三日で、ほかの読み方が無い。rulec が裸の `..` を E010 でエラーにするのは、重さや金額の境目で「以下」と「未満」が分かれるからである。入力の範囲だけは、rulec と dandori にそろえて `range >=… <=…` と書く。

### 1.3 入力と型

**決定**:型は `date` と `int` の二つ。dates のファイルの入力は、`date` がちょうど一つと、`int` がいくつか。どの入力にも `range` の両端を書く。

```
inputs
  受領日(received)    : date  range >=2026-01-01 <=2027-11-20
  締め日(closing_day) : int   range >=1 <=31
```

日付の宣言(`date`)は、どれも日付を返し、どれも公開する。生成した関数が受け取るのは、その日付がたどって使う入力だけである(締め日が受領日しか使わなければ、引数は受領日だけ)。

`range` は rulec と同じく三つの役を持つ。総当たりで計算する範囲、生成したコードの入口のガード(範囲の外の入力は例外)、そしてデータの範囲の外に出ないことを確かめる範囲(2.4)である。生成したコードが結果を返すのは、検査がすべての条件を確かめた入力だけ、ということになる。

**理由**:日付の入力を一つにしたのは、総当たりの大きさを抑えるためである。日付が一つなら 100 年でも 36,525 通りで、整数の入力を足しても百万通りほどに収まる(10 章の整数を三つ受け取る例で 871,596 通り)。日付が二つあると、100 年どうしで 13 億通りを超える。二つめの日付が要る計算(「検収は受領の N 日後」)は、日数を整数の入力として受け取れば書ける。

**捨てたもの**:

- 日付の入力を二つ以上にすること。上の理由。
- `range` を省けること。範囲が無ければ、総当たりの範囲も入口のガードも決まらない。
- 整数を返す関数(「二つの日付のあいだの営業日数」)。「何営業日以内」は条件の形で書ける(1.10)。返す型が二つになると、生成物、vectors、doc の形が全部二通りになる。要る例が出てから考える。

### 1.4 カレンダー

| 行 | 意味 |
|---|---|
| `closed weekly sat, sun` | 毎週その曜日を休む |
| `closed every 12-29..01-03 "年末年始"` | 毎年その月日を休む。年をまたいでよい。`closed every 05-01 "創立記念日"` のように一日でもよい |
| `closed 2026-08-13..2026-08-15 "夏季休業"` | その日を休む |
| `closed 祝日` | 出典の表に載っている日を休む(1.5) |
| `open 2026-12-28 "臨時営業"` | 休みの決まりに当たっても営業する日 |
| `offset +09:00` | 時刻を出すときの UTC オフセット(1.9) |
| `use calendar "<ファイル>"` | 別のカレンダーを読み、その上に足す |

ある日が営業日なのは、`open` に書いてあるか、どの `closed` にも当たらないときである。`open` はいつも勝つ。`use calendar` で読んだカレンダーの行と自分の行は、区別せずに一つにする(休みの決まりも例外の営業日も、両方の分を合わせる)。オフセットは一つのカレンダーに一つで、読んだ先と食い違えば E015。

**データの範囲**:祝日の表には、分かっている範囲がある(内閣府の表は 2027 年まで)。カレンダーのデータの範囲は、そのカレンダーが使う表のデータの範囲の共通部分である。表を使わないカレンダー(休みの曜日と毎年の休みだけ)は、扱える日付の全部(0001-01-01〜9999-12-31)がデータの範囲になる。データの範囲の外の日が営業日かは分からない(P4、2.4)。

営業日が一日も無いカレンダー(休みの曜日が七つ、毎年の休みが一年全部)は E108 でエラーにする。翌営業日を探す計算が終わらなくなるからである。

**捨てたもの**:

- 振替休日や春分の日を規則で計算すること。祝日は法律で変わり、春分の日と秋分の日は前の年に決まる。内閣府の表には振替休日も「休日」の行として載っている(1.5)。表を正とし、計算しない(P4)。
- 「第 2 土曜日」のような、月の何番目の曜日を休む決まり。会社の休みにはあるが、その日は CSV の表に並べれば書ける。要る例が出てから考える。
- `open` と `closed` の順序に意味を持たせること(後に書いたほうが勝つ)。`use calendar` をまたぐと、どちらが後かを読む人が追えない。

### 1.5 出典と祝日の表

**決定**:祝日の表は `source` で宣言する。rulec の `source = file` と同じ形で、手元に保存したコピーを SHA-256 の先頭 16 桁で固定する。下の行に、ファイルの形式とデータの範囲を書く。

```
source 祝日 = file "data/syukujitsu.csv" url "https://www8.cao.go.jp/chosei/shukujitsu/syukujitsu.csv" sha256:cec37a743c96995c
  format csv shift_jis
  covers 1955-01-01..2027-12-31
```

- `file`:コピーのパス(その `.cal` からの相対)。`url`:`source fetch` と `source outdated` が取りに行く場所。`sha256:`:コピーのバイト列の SHA-256 の先頭 16 桁(rulec と同じ長さ)。
- `format csv [utf8 | shift_jis]`:一行が `<日付>,<名前>` の CSV。日付は `2026-01-01` と `2026/1/1` のどちらの形でもよい。一行目の最初の値が日付として読めなければ、見出しとして飛ばす。名前は無くてもよい。値に `,` を含めるなら `"…"` で囲む。文字コードの既定は UTF-8。
- `format govuk "england-and-wales"`:GOV.UK の `bank-holidays.json`。三つの地域(`england-and-wales`、`scotland`、`northern-ireland`)から一つを選ぶ。
- `covers <日付>..<日付>`:この表が休みを全部載せている範囲。`covers listed years` と書くと、表に行のある最初の年の 1 月 1 日から最後の年の 12 月 31 日まで(あいだに行の無い年があれば E106)。

検査は、コピーが無ければ E101(`koyomi source fetch` へ)、固定が無ければ E102(書き足す `sha256:…` を示す。`koyomi source pin` でもよい)、コピーが固定と違えば E103、読めなければ E104(どのバイトか、どの行か)、行が `covers` の外にあれば E105 を出す。通信はしない。

`covers` を日付でも書けるようにしたのは、会社の年間休日のカレンダーが年度(4 月〜翌年 3 月)で作られることが多いからである。行のある年から範囲を推すと、2026 年度の表から 2026-01-01〜2027-12-31 を推し、2026 年 1〜3 月と 2027 年 4〜12 月を知っていることにしてしまう。だから範囲は推さず、書かせる。

#### 内閣府の CSV

2026-10-02 に取った `syukujitsu.csv` について確かめたこと:

- 21,538 バイト。`Last-Modified: Mon, 02 Feb 2026 00:30:17 GMT`。SHA-256 は `cec37a743c96995cdb9cb52b685c9003634682a9b0e1a640a6b9b96881fe964a`。
- Shift_JIS で、改行は CR LF。一行目は見出し `国民の祝日・休日月日,国民の祝日・休日名称`。
- 1,067 行。1955 年から 2027 年までの 73 年で、どの年にも行がある。日付は `1955/1/1` の形(ゼロで埋めない)で、昇順で、同じ日付は無い。
- 振替休日と国民の休日は、どちらも名前が「休日」の行として載っている(116 行)。だから振替休日を計算しなくてよい。
- 内閣府の「国民の祝日について」のページは、2028 年の祝日を 2027 年の 2 月に載せると書いている。表は毎年 2 月に一年ずつ延びる。
- 内閣府のサイトのコンテンツには、公共データ利用規約(第1.0版)が適用される(内閣府ホームページ利用規約、令和 7 年 3 月 25 日現在)。例とテストにコピーを置き、出典の書き方と条件は THIRD_PARTY_NOTICES.md に書いた。

#### 文字コード

**決定**:コピーは配られているバイト列のまま置き、koyomi が自分で Shift_JIS を読む。変換表は、WHATWG の Encoding Standard の `index-jis0208.txt`(Identifier `cbaa91f3deb7d0841faf5c33041fc15a285da0e87e64ab802c4bf04b7c4da861`、Date 2024-09-18、7,724 個の符号)から作って koyomi に埋める。Encoding Standard は CC BY 4.0 で、ソースコードに取り込んだ部分は BSD 3-Clause License になる(その文面と著作権の表示は THIRD_PARTY_NOTICES.md)。2026-10-06 に `Cargo.toml` の `license` を `(MIT OR Apache-2.0) AND BSD-3-Clause` にした。表は koyomi を含む `ritsu` のバイナリにも入るので、それを配るもの(リリースのアーカイブ、`.deb`・`.rpm`、Homebrew)には根の `THIRD_PARTY_NOTICES` が入る。THIRD_PARTY_NOTICES.md のこの表の節は、その中にそのまま載っている(ritsu の DESIGN 13.2。`crates/ritsu/tests/release.rs` が、一字も違わないことを確かめる)。

理由は、固定するハッシュを、誰がどの機械で取り直しても同じにするためである。`curl -s <url> | shasum -a 256` の先頭 16 桁が `.cal` の固定と同じなら、コピーが配られているファイルそのものだと誰でも確かめられる。取ってくるときに UTF-8 に変換し、そのコピーを固定する案では、固定するのは変換したもののハッシュになる。変換に使うツールの表(`〜` を U+301C にするか U+FF5E にするか、など)で値が変わりうるので、機械によって固定が合わなくなる。

WHATWG の表は、ブラウザが Shift_JIS のページを読むときに使う表で、版と識別子が付いて公開されている。表の 7,724 個の符号がどれも Python の `cp932` と同じ文字になることと、内閣府の CSV が `shift_jis` でも `cp932` でも同じ文字列になることを確かめた。読めないバイト列は置き換え文字にせず、E104 でそのバイトの位置を言う。埋める表は、符号の順に文字を並べた一つの文字列にする(11,104 字、空いている符号は U+FFFF。UTF-8 で 33,190 バイト)。

**捨てたもの**:

- 取ってくるときに `iconv` で UTF-8 に変換し、そのコピーを固定すること。上の理由。
- 名前を読まず、日付だけを読むこと。`,` と `/` と数字と CR LF は Shift_JIS の二バイトめに現れないので、日付は変換なしでも読める。しかし、月の表に祝日の名前を出せなくなる(7 章)。

#### GOV.UK の bank holidays

2026-10-02 に取った `https://www.gov.uk/bank-holidays.json` について確かめたこと:

- UTF-8 の JSON で、SHA-256 は `538b3482c28b85ecd2db606a0d5ae6ad17248900b6498700ce0a48d26a3ecde6`。三つの地域があり、`england-and-wales` は 2019 年から 2028 年までの 83 件。
- 土日に当たった祝日は、振り替えた平日の日付で載っている(`"notes": "Substitute day"`)。土日の日付は一つも無い。
- 古い年は落ちていく(取り直すと範囲の始まりが動く)。`covers listed years` と書けば、範囲は表に合わせて決まる。
- GOV.UK の内容は Open Government Licence v3.0 で使える(rulec のコーパスも同じ出典を使っている)。

JSON を読むので、依存に serde_json を使う(11 章)。

#### 法令の出典

**決定**:法令の出典も、rulec の `source = law`(rulec の §15.68)と同じ形で書ける。取れるのは e-Gov の法令だけである。

```
source 民法 = law "129AC0000000089" asof 2026-10-01
  第140条 sha256:e880059021fbb67d
  第141条 sha256:0575c131b9f08063
  第143条 sha256:6950bdfb988439b6

date 起算日(first_day) = 起点            @民法 第140条
  + 1 day
date 満了日(last_day) = 起算日           @民法 第141条, 第143条
  + 月数 months else start_of_next_month
  - 1 day
```

- コピーは `.cal` の隣の `sources/law/<法令ID>@<日付>/<要素>.xml`(条ごとに、e-Gov 法令 API v2 の `law_data` が返す XML をそのまま)と、版の ID を書いた `revision.txt`。rulec と同じ置き方である。固定はコピーのバイト列の SHA-256 の先頭 16 桁。
- 引けるのは本則の条・項・号(`第143条`、`第143条第2項`、`第143条第2項第1号`、`第20条の2`。漢数字でもよい)で、それぞれ e-Gov の要素(`MainProvision-Article_143-Paragraph_2`)に対応する。
- 引用は、`date` の宣言の行、操作の行、`at` の行、条件の行、カレンダーの `closed` と `open` の行の末尾に `@<出典> <条>` と書く(`#` のコメントより前)。条は `,` で区切っていくつでも書ける(`@民法 第141条, 第143条`。rulec と同じ)。`at` の行で引けるようにしたのは、141 条(その末日の終了をもって満了する)が `at end of day` に当たるからである。
- 検査は、引いた条が固定されていなければ E111(直し方は固定の行そのもの)、コピーが無ければ E101、固定と違えば E103、固定だけが残っていれば W102。通信しない。

koyomi は条文を読んで計算を決めるのではない。書いた人が条文に合わせて書いた計算の隣に、どの版の条文を見て書いたかを固定して並べる。人が読むページは、その日付の言い直しの隣に、コピーから引いた条文を載せる(7 章)。条文が改正されれば `koyomi source outdated` が施行日と一緒に言う(9 章)。

A の段階で、2026-10-01 時点の民法 140〜143 条を e-Gov 法令 API v2 から取り、`examples/sources/law/129AC0000000089@2026-10-01/` に置いた(版 `129AC0000000089_20260624_508AC0000000045`)。固定は 140 条 `e880059021fbb67d`、141 条 `0575c131b9f08063`、142 条 `fc8c35a0769d3b35`、143 条 `6950bdfb988439b6`。民法には 2026-10-01 より後に施行される版が五つ(2027-06-23、2027-12-05、2028-06-13、2028-12-23、2029-06-23)あり、どの版でも、この四つの条の XML は一バイトも変わらない(2026-10-02 に確かめた)。

**捨てたもの**:

- 法令の出典を入れず、コメントに条番号を書くだけにすること。それでは、ページに載せる条文がどの版のものかを言えず、改正を知る手段も無い。
- 附則と別表を引けるようにすること。期日の決まりで、そこを引く例を見ていない(rulec は附則と別表を引ける)。

### 1.6 日付の関数

**決定**:日付の宣言には、始まりの日付(入力か、ほかの日付)を書き、その下に操作を一行ずつ並べる。操作は上から順に一つずつ効く。

| 操作 | 意味 | カレンダー | 無い日の扱い |
|---|---|---|---|
| `+ 30 days`、`- 1 day` | 暦日で足す・引く | 要らない | 要らない |
| `+ 5 business days`、`- 2 business days` | 営業日で数える | 要る | 要らない |
| `+ 1 month else …`、`- 3 months else …` | 月を足す・引く | 要らない | 必ず書く |
| `+ 1 year else …` | 年を足す(12 か月と同じ) | 要らない | 必ず書く |
| `day 10 of month +1` | k か月後の N 日(`+0` は省ける。負の数で前の月) | 要らない | N が 29 以上になりうるときだけ書く |
| `start of month +1`、`end of month +2` | k か月後の月初・月末(`+0` は省ける) | 要らない | 要らない |
| `close day 20` | N 日締め:その日を含む期間の締め日 | 要らない | N が 29 以上になりうるときだけ書く |
| `close end of month` | 月末締め:その日を含む月の末日 | 要らない | 要らない |
| `roll following`、`roll preceding`、`roll modified following`、`roll modified preceding` | 休みなら営業日へ動かす(営業日の慣行) | 要る | 要らない |
| `if closed <操作>` | 休みのときだけ、その一つの操作をする | 要る | その操作による |

数(`30`、`5`、`1`、`10`、`20`)は整数の入力の名前でもよい(`close day 締め日 else end_of_month`、`day 支払の日 of month +支払の月 else end_of_month`)。そのときは、無い日の扱いが要るかを入力の範囲で決める。足す・引く数は 0 以上で、向きは `+` と `-` で書く。

日付の宣言の最後の行に `at 09:00` か `at end of day` を書くと、その日付を時刻でも出す(1.9)。

操作の定義は 2.2 にある。

**理由**:一行に一つの操作にすると、`eval` の一段、診断の「そうなる例」の一段、人が読むページの言い直しの一文が、どれも `.cal` の一行に対応する。読む人は、計算の段をそのまま原文の行と見比べられる。

**捨てたもの**:

- 式を入れ子にする書き方(`roll(day(10, month(+1, close(20, 受領日))), preceding)`)。読む順が計算の順と逆になり、読む人が追えない。
- 一行にパイプでつなぐ書き方(`受領日 |> close day 20 |> …`)。行が長くなり、変更の diff が一行に固まる。
- 「翌月 10 日」を `+ 1 month` と `day 10` の二つに分けること。1 月 31 日に `+ 1 month` をすると無い日に当たるが、そのあと 10 日にするので結果には効かない。それでも無い日の扱いを書かされる。だから「k か月後の N 日」を一つの操作にした。月初と月末も同じ。

### 1.7 無い日の扱い

月を足す、k か月後の N 日にする、N 日で締める。この三つは、その月に無い日(2 月 30 日、4 月 31 日)に当たることがある。

**決定**:無い日に当たりうる操作には、無い日の扱いを `else` で必ず書かせる。書かなければ E201。扱いは三つある。

| 書き方 | 意味 | 2026-01-31 の `+ 1 month` |
|---|---|---|
| `else end_of_month` | その月の末日に寄せる | 2026-02-28 |
| `else start_of_next_month` | 次の月の 1 日に送る | 2026-03-01 |
| `else reject` | エラーにする。範囲の中で一度でも当たれば、検査が E202 で止め、当たる入力を全部挙げる。生成したコードは例外を投げる | E202 |

当たりうるかは、書いたものから決める。月を足す操作は、いつも当たりうる。「k か月後の N 日」と「N 日締め」は、N が 29 以上になりうるとき(数が 29 以上か、整数の入力の範囲の上の端が 29 以上)だけ当たりうる。当たりえない操作に `else` を書くと W201。

範囲の中では一度も使われない `else` もある(20 日締めのあとの `+ 1 month`)。それは警告にしない。どの入力で使われたか、一度も使われなかったかは、人が読むページに出す(7 章)。

#### 民法の例

民法 143 条 2 項(e-Gov 法令 API v2 の版 `129AC0000000089_20260624_508AC0000000045` から取った):

> 週、月又は年の初めから期間を起算しないときは、その期間は、最後の週、月又は年においてその起算日に応当する日の前日に満了する。ただし、月又は年によって期間を定めた場合において、最後の月に応当する日がないときは、その月の末日に満了する。

140 条は「日、週、月又は年によって期間を定めたときは、期間の初日は、算入しない。ただし、その期間が午前零時から始まるときは、この限りでない。」である。

この二つを文字どおりに書くと、次になる。応当する日が無いときは、次の月の 1 日に送ってから前日にすると、ちょうどその月の末日になる。

英語の版(`civil_code_period_end.cal`)では次のとおり。

```
date first_day = origin
  + 1 day                                        # the first day is not counted
date last_day = first_day
  + month_count months else start_of_next_month  # the corresponding day of the last month; with none, the 1st of the month after
  - 1 day                                        # the day before it: the last day of the last month when it had no corresponding day
date last_day_142 = last_day
```

日本語の版(`civil_code_period_end.ja.cal`):

```
date 起算日(first_day) = 起点
  + 1 day                                 # 140 条:初日は算入しない
date 満了日(last_day) = 起算日
  + 月数 months else start_of_next_month  # 143 条 2 項:最後の月の応当する日。無ければ次の月の 1 日へ
  - 1 day                                 # その前日。応当する日が無かったときは、最後の月の末日になる
```

この書き方が条文どおりに計算することを、試作で確かめた。条文の場合分け(応当する日があればその前日、無ければその月の末日)をそのまま書いた関数と、起点 2000〜2027 年のすべての日と月数 1〜12 で一致した。

民法の期間は、無い日の扱いを法令の側が決めている例である。もし `else end_of_month` で書けば、条文と違う結果になる日がある。起点に月数を足して月末に寄せる書き方(`起点` から `+ 月数 months else end_of_month`)と比べると、起点 2026 年の 365 日と月数 1〜12 の 4,380 通りのうち 39 通りで結果が違う。最初の一つは起点 2026-02-28(土)、月数 1 で、条文どおりなら起算日は 2026-03-01、満了日は 2026-03-31。月数を足して寄せると 2026-03-28 になる。

#### ライブラリごとの違い

同じ「1 か月後」を、手元で走らせて比べた(2026-10-02)。

| 実装(版) | 2023-01-31 の 1 か月後 | 2024-01-31 の 1 か月後 | 2023-05-31 の 1 か月前 | 2023-03-31 に 1 か月を二回 | 2023-03-31 の 2 か月後 |
|---|---|---|---|---|---|
| Python の dateutil 2.9.0.post0(`relativedelta`) | 2023-02-28 | 2024-02-29 | 2023-04-30 | 2023-05-30 | 2023-05-31 |
| Java(OpenJDK 27、`LocalDate.plusMonths`) | 2023-02-28 | 2024-02-29 | 2023-04-30 | 2023-05-30 | 2023-05-31 |
| PostgreSQL 18.0(`date + interval '1 month'`) | 2023-02-28 | 2024-02-29 | 2023-04-30 | 2023-05-30 | 2023-05-31 |
| JavaScript の Temporal(polyfill 0.5.1、`PlainDate.add`) | 2023-02-28 | 2024-02-29 | 2023-04-30 | 2023-05-30 | 2023-05-31 |
| Rust の chrono 0.4.45(`checked_add_months`) | 2023-02-28 | 2024-02-29 | 2023-04-30 | 2023-05-30 | 2023-05-31 |
| macOS の `date -v+1m` | 2023-02-28 | 2024-02-29 | 2023-04-30 | 2023-05-30 | 2023-05-31 |
| Go 1.25.5(`time.AddDate`) | 2023-03-03 | 2024-03-02 | 2023-05-01 | 2023-06-01 | 2023-05-31 |
| JavaScript の `Date`(Node 23.11.0、`setUTCMonth`) | 2023-03-03 | 2024-03-02 | 2023-05-01 | 2023-06-01 | 2023-05-31 |

Temporal は `{ overflow: "reject" }` を渡すと、無い日で `RangeError` を投げる。Chrome 154 に組み込まれた Temporal も、試した五つの組で polyfill と同じ結果だった。Python の標準の `datetime` は月を足せない(`timedelta(months=1)` は `TypeError`)。

この表から分かることは三つある。

1. 多くは月末に寄せる(Catala の論文の切り捨て)。Go と JavaScript の `Date` は、余った日数を次の月へ繰り越す。そのため 2023-01-31 の 1 か月後(2023-03-03)が、2023-02-01 の 1 か月後(2023-03-01)より後になる。入力が遅いのに結果が早い(単調でない)。2023-05-31 の 1 か月前が 2023-05-01 になり、月が変わらないこともある。
2. どの実装でも、1 か月を二回足すのと 2 か月を一度に足すのとで結果が違う(2023-05-30 と 2023-05-31。Go と `Date` は 2023-06-01 と 2023-05-31)。月の足し算は結合しない。
3. 無い日をエラーにできるのは Temporal の `reject` だけで、それも既定ではない。

だから koyomi は、生成したコードの中で言語の日付のライブラリを使わず、通算日の整数で自分の手順を書く(6.1)。そうしないと、同じ `.cal` から生成した TypeScript と Go が違う日を返す。

#### Catala の論文から借りたもの

Monat、Fromherz、Merigoux の "Formalizing Date Arithmetic and Statically Detecting Ambiguities for the Law"(ESOP 2024、arXiv:2403.08935)は、月と年の足し算で無い日ができる計算を曖昧なものとし、丸め方を三つに分けた。切り捨て(その月の末日へ)、切り上げ(次の月の 1 日へ)、エラーである。koyomi の三つの扱いは、これと同じ意味にした。

- 年と月をまとめて足すときは、足し終えてから一度だけ丸める(論文の派生形 `rnd((e +y y) +m m) +d d`)。koyomi では一つの操作が一度だけ丸める。`+ 1 year else …` は `+ 12 months else …` と同じである(論文の Lemma 4)。一年と二か月を一度の丸めで足したいなら `+ 14 months else …` と書く。二行に分ければ、二回丸める。
- 切り捨てと切り上げは、どちらも単調で(論文の Theorem 3)、切り捨ての結果は切り上げの結果を超えない(Theorem 4)。試作で 1900〜2100 年のすべての日と月数 −24〜24 について確かめた。B の段階で、同じ総当たりを Rust のテストにする。
- Go と JavaScript の `Date` の繰り越しは、論文が coreutils について書いたのと同じもので、論文の意味では書けない。単調でないので、koyomi の扱いに入れない。
- 論文は、丸め方を替えると結果が変わるかどうか(rounding-insensitivity)を抽象解釈で判定した。koyomi は範囲を数え尽くせるので、同じ問いに総当たりで答える。扱いをほかの二つに替えると結果が変わる入力を全部数え、人が読むページに出す(7 章)。
- Catala は、丸めが要る計算で既定では実行時に例外を投げ、丸め方をスコープごとに書かせて、その根拠を法令か解釈で示すようにした。koyomi は、書かなければ検査で止める(P3)。

**捨てたもの**:

- 既定の扱いを置くこと(多くのライブラリのように月末に寄せる)。どの扱いが正しいかは契約や法令が決めることで、ツールが黙って決めれば、上の表のとおり誰かと食い違う。
- 余った日数を次の月へ繰り越す扱い(Go、JavaScript の `Date`)。単調でない。
- 年と月をまとめて書く操作(`+ 1 year 2 months`)。`+ 14 months` で足りる。二行で書けば二回丸めることを、別の書き方で隠すことにもなる。

### 1.8 営業日

**決定**:

- `+ n business days`:その日より**後**の営業日を数えて、n 番目の日。その日が休みでも、数え始めは次の日からである(土曜の 1 営業日後は月曜で、金曜の 1 営業日後と同じ)。`- n business days` は前へ同じように数える。
- `+ 0 business days` は、その日が営業日ならその日、休みなら翌営業日(`roll following` と同じ)。`- 0 business days` は、休みなら前営業日。どちらも、結果はいつも営業日になる。
- `roll following`:休みなら翌営業日。`roll preceding`:休みなら前営業日。`roll modified following`:翌営業日へ動かすが、月が変わるなら前営業日。`roll modified preceding`:前営業日へ動かすが、月が変わるなら翌営業日。営業日ならどれも動かさない。
- `if closed <操作>`:その日が休みのときだけ、一つの操作をする。`if closed + 1 day` は「休みなら翌日」で、翌日も休みでも、それ以上は動かさない。

休みの日から営業日を数えるときの結果は、ライブラリで分かれる。土曜(2026-10-03)の 1 営業日後を、手元で走らせて比べた。numpy 2.5.3 の `busday_offset` は、既定では休みの日を受け付けず(`ValueError`)、`roll='forward'` を渡すと先に月曜へ寄せてから数えて火曜(2026-10-06)を返す。QuantLib 1.43 の `Calendar.advance(土曜, 1, Days)` は次の日から数えて月曜(2026-10-05)を返し、`advance(土曜, 0, Days)` も月曜を返す(0 のときは翌営業日に寄せる)。

koyomi は QuantLib と同じ結果になるようにした。数え方が「その日より後の営業日を数える」の一通りで、受領日が休みでも営業日でも同じ言い方で済むからである。0 のときに寄せるのは、整数の入力で日数を受け取ると 0 が入りうるからで、そのときも「営業日で数えた日は営業日」が崩れないようにした。先に寄せてから数えたければ、`roll following` を一行足せば書ける。

**`if closed` を入れた理由**:P5 で一般の条件分岐は持たないとした。それでも「休みなら」だけは入れる。民法 142 条を文字どおりに書くには、これが要る。

> 期間の末日が日曜日、国民の祝日に関する法律(昭和二十三年法律第百七十八号)に規定する休日その他の休日に当たるときは、その日に取引をしない慣習がある場合に限り、期間は、その翌日に満了する。

条文の「その翌日」は、翌日も休みのときにさらに動くとは書いていない。これを `roll following`(休みが明けるまで動かす)で書けば、法令を一つの読み方で読んだことになる。koyomi は法令の読み方を決めない。文字どおりの「翌日」と「休みが明けた日」を二つの日付として書き、二つが等しいという条件を書けば、分かれる日を検査が全部挙げる。10 章の例では、起点 2026 年と月数 1〜12 の 4,380 通りのうち 121 通りで分かれる。最初の一つは起点 2026-01-22、月数 1 で、満了日 2026-02-22 が日曜、その翌日 2026-02-23 は天皇誕生日で、休みが明けるのは 2026-02-24 である。どちらを採るかは人が決める。

どちらの書き方にも、条文の「その日に取引をしない慣習がある場合に限り」は入らない。どの日を休みとするか(カレンダーを選ぶこと)が、その部分の読み方になる。それも人が決める。10 章の例のカレンダーは、条文が挙げる日(日曜日と、国民の祝日に関する法律に規定する休日)だけを休みにしたもので、「その他の休日」に何が入るかを決めていない。

`if closed` が見るのはカレンダーだけで、比較は書けない。続けられる操作は一つで、`if` を入れ子にはできない。だから、ループも任意の分岐も書けないままである。単調でなくなることはある(土日が休みなら、`if closed + 3 days` は日曜を水曜にし、翌日の月曜は月曜のままにする)が、それは条件の `is monotonic` が見つける。

**捨てたもの**:

- 比較で分ける一般の `if`(「受領が 25 日より後なら」)。締めの操作で書けない分岐が要る例を、まだ見ていない。
- `if open`。営業日なら何かをする決まりの例を見ていない。

### 1.9 時刻

**決定**:カレンダーに `offset +09:00` のような固定の UTC オフセットを書く。日付の宣言の最後に `at 09:00` と書くと、その日の 09:00(そのオフセットで)を時刻としても出す。`at end of day` は、その日が終わる時刻(次の日の 00:00)を出す。民法 141 条(前条の場合には、期間は、その末日の終了をもって満了する)の満了の時刻にあたる。

時刻は RFC 3339 の UTC で、`Z` で終わる形で出す(`2026-05-08T00:00:00Z`)。dandori の `timestamp` は `Z` で終わる形だけを受け付ける(dandori の `src/render.rs` の `TIMESTAMP_RE`)ので、そのまま `wait until` に渡せる。`eval` と人が読むページには、オフセット付きの形(`2026-05-08T09:00:00+09:00`)も並べる。

オフセットに `Asia/Tokyo` や `Europe/London` のようなタイムゾーンの名前を書くと、E107 でエラーにする。夏時間のあるタイムゾーンでは、同じ地域の同じ 09:00 でも季節でオフセットが変わり、固定のオフセットでは一年の半分で一時間ずれる。どのタイムゾーンに夏時間があるかを知るには tz データベースが要るので、どの名前も受け付けず、オフセットを数で書いてもらう。夏時間の無い地域なら、それで足りる。

`at` を書いたのに、カレンダーにオフセットが無ければ E110。

**捨てたもの**:tz データベースを埋め込むこと。夏時間の決まりは国が変え、データベースは年に何度も版が出る。祝日の表と同じ固定と更新の仕組みが要り、今回の範囲を超える。英語の例(England and Wales、夏時間がある)は日付だけを出し、時刻は出さない(10 章)。

### 1.10 条件

**決定**:`claims` の節に、名前と条件を一行ずつ書く。

```
claims
  paid_on_a_business_day      : payment is open
  within_60_days_of_receipt   : payment <= received + 60 days
  within_40_business_days     : payment <= received + 40 business days
  paid_after_closing          : payment > closing
  later_receipt_later_payment : payment is monotonic
  the_same_when_rewritten     : payment = payment_old
```

日本語の名前で書けば次のとおり。

```
claims
  営業日に払う       : 支払日 is open
  受領から60日以内   : 支払日 <= 受領日 + 60 days
  40営業日以内       : 支払日 <= 受領日 + 40 business days
  締めのあとに払う   : 支払日 > 締め日
  遅い受領は遅い支払 : 支払日 is monotonic
  書き直しても同じ   : 支払日 = 支払日_旧
```

- `X is open`:X が営業日である。
- `A <比較> B`:比較は `=`、`<`、`<=`、`>`、`>=`。A と B は日付の入力か日付の名前で、`+ n days`、`- n days`、`+ n business days`、`- n business days` を一つ付けてよい(n は操作と同じく、整数の入力の名前でもよい)。これで「ほかの日付との前後」「何日以内(暦日と営業日)」「二つの関数が一致する(書き直しの確かめ)」が書ける。
- `X is monotonic`:入力の日付が遅くなっても、X が早くならない。整数の入力があれば、その値ごとに日付について言う。

どの条件も、範囲のすべての入力について確かめる。単調性は、隣り合う二日(d と d+1)の組を全部比べれば、範囲全体について言える。≤ は推移するので、d < d' なら、あいだの隣り合う組をつないで X(d) ≤ X(d') が出る。

成り立たない入力が一つでもあれば、検査はエラーで止まる(E301。単調性は E302)。

**理由**:条件の形を比較と二つの述語に絞ったのは、どの条件も一つの入力ごとに真か偽が決まり、成り立たない入力を全部挙げられるようにするためである。

**捨てたもの**:

- `!=` と `is closed`。そう書きたい例を見ていない。
- 条件を一部の入力に限る書き方(`when 締め日 <= 28`)。条件の側に分岐が入る。限りたいなら、入力の範囲を狭めるか、ファイルを分ける。
- 成り立たなくてもエラーにしない「比べるだけ」の書き方。二つの読み方の違いを見たいときも、等しいという条件を書けば違う日が全部出る。それで足りるかは、例を書いて使ってみてから見直す(12 章)。

### 1.11 例

`examples` の表は、rulec と同じく、走らせて確かめる仕様である。入力の列と、`->` を付けた日付の列を書く。日付の列には、ファイルの日付を全部書く(足りなければ E304)。rulec の E111(例に出力の列が足りない)と同じ理由で、参照インタプリタと生成したコードが同じ誤りを持つと突き合わせは通ってしまい、それを破れるのは人が書いた値だけだからである。値が参照インタプリタと違えば E303。

## 2. 意味

### 2.1 日付の表し方

日付は先発グレゴリオ暦の暦日で、タイムゾーンを持たない。扱える範囲は 0001-01-01〜9999-12-31(Python の `datetime.date` と同じ)。内部では 1970-01-01 からの通算日の整数で持つ。rulec の `date` と同じ持ち方である。0001-01-01 は −719,162、9999-12-31 は 2,932,896 で、32 ビットの整数に収まる。年月日と通算日の変換には Howard Hinnant の `days_from_civil` と `civil_from_days` の手順を使う。範囲の 3,652,059 日のすべてで、変換して戻すと元に戻ることを試作で確かめた。

計算の途中で日付が範囲の外に出れば E204。

### 2.2 操作の定義

日付 z(通算日)の年月日を (y, m, d)、月 (y, m) の日数を len(y, m) と書く。(y, m) + k は k か月後の年月である。

- place(y, m, d, 扱い):d ≤ len(y, m) なら (y, m, d)。そうでなければ扱いに従う。`end_of_month` は (y, m, len(y, m))、`start_of_next_month` は (y, m) + 1 の 1 日、`reject` はエラー。
- `+ n days`:z + n。`- n days`:z − n。
- `+ n months else P`:(y', m') = (y, m) + n として place(y', m', d, P)。`- n months` は −n で同じ。`± n years` は ± 12n months。
- `day N of month +k else P`:(y', m') = (y, m) + k として place(y', m', N, P)。
- `start of month +k`:(y, m) + k の 1 日。`end of month +k`:(y, m) + k の末日。
- `close day N else P`:月ごとの締め日 c(y', m') = place(y', m', N, P) のうち、z 以後で最も早いもの。候補は (y, m) − 1、(y, m)、(y, m) + 1 の三つの月で足りる。(y, m) + 1 の締め日はその月の中か次の月の 1 日なので、必ず z より後にある。(y, m) − 1 が要るのは `start_of_next_month` のときで、前の月の締め日がこの月の 1 日になることがある。
- `close end of month`:(y, m) の末日。
- `roll following`:z 以後で最初の営業日。`roll preceding`:z 以前で最後の営業日。`roll modified following`:following の結果の月が z の月と違えば、preceding の結果。`roll modified preceding`:preceding の結果の月が違えば、following の結果。
- `+ n business days`:n ≥ 1 なら、z より後の営業日の n 番目。n = 0 なら `roll following` と同じ。`- n business days`:n ≥ 1 なら、z より前の営業日の n 番目。n = 0 なら `roll preceding` と同じ。
- `if closed S`:z が休みなら S(z)、営業日なら z。

カレンダーに問い合わせる(ある日が営業日かを見る)のは、`business days`、`roll`、`if closed` と、条件の `is open` と `business days` だけである。

### 2.3 成り立つ性質と、成り立たない性質

`if closed` を除くすべての操作は、日付について単調(z ≤ z' なら f(z) ≤ f(z'))である。`end_of_month` と `start_of_next_month` の月の足し算(Catala の Theorem 3)、締め、月初と月末、営業日の数え方、四つの慣行のどれもそうである。単調なものをつないでも単調なので、`if closed` を使わない日付はいつも単調になる。それでも `is monotonic` を書けるようにしたのは、読む人に向けた主張として残すためと、`if closed` を使った日付を確かめるためである。

成り立たない性質もある(Catala の論文の Non-Property 1・2 と同じ種類のもの。試作で確かめた)。

- 月の足し算は結合しない。2023-03-31 に `+ 1 month else end_of_month` を二回すると 2023-05-30、`+ 2 months else end_of_month` を一回すると 2023-05-31。
- 日と月の足し算は、順序を入れ替えられない。2023-01-30 に 1 日足してから 1 か月足すと(月末に寄せて)2023-02-28、1 か月足してから 1 日足すと 2023-03-01。
- 月を足して同じだけ引いても、元に戻らない。2023-03-31 に `+ 1 month else end_of_month` で 2023-04-30、そこから `- 1 month else end_of_month` で 2023-03-30。
- `start_of_next_month` で月を引くと、引いたのに同じ月になることがある。2023-03-31 の `- 1 month else start_of_next_month` は 2023-03-01(2 月 31 日が無いので、次の月の 1 日へ送る)。

書き直したら同じ結果になるはずだ、という思い込みは、この性質のどれかで外れる。`=` の条件(書き直しの確かめ)が役に立つのはそこである。

### 2.4 カレンダーへの問い合わせとデータの範囲

ある日が営業日かを問うとき、その日がカレンダーのデータの範囲の外ならエラーにする。検査では E203 で、どの入力で、どの操作が、どの日を問うたかを言う。生成したコードは例外を投げる。

データの範囲の外に出ても、問い合わせなければエラーにしない。`+ 30 days` だけの日付は、結果が 2028 年でも祝日の表を見ないので、結果は表に依らない。エラーにするのは、結果が表に依るときだけである。

入力の範囲がデータの範囲に収まっていても、計算の先が外に出ることがある(1.1 の例の 2027-11-21)。検査は範囲のすべての入力を計算するので、それを全部見つける。E203 の直し方には、外に出る最初の入力の前日で終わる `range` を書き換え後の形として示す。その前の入力はどれも外に出ないことを、検査がもう確かめている。

## 3. 検査

### 3.1 何を確かめるか

`koyomi check` は次の順に確かめる。前の段でエラーがあれば、後の段へは進まない。

1. 字句と構文(E001〜E006)。
2. 名前と型(E007〜E015、E304)。`use calendar` の先も読み、読んだカレンダーの診断もここで出す。例の表に入力か日付の列が足りないこと(E304)も、書いたものだけから決まるのでここで見る。エラーが無ければ、出力先の綴りで一つの識別子になる別名と、出力先が使えない名前になる別名も見る(W001、15 章)。警告なので、後の段へ進む。
3. カレンダーと出典(E101〜E111、W101、W102)。コピーを読み、固定と比べ、表を読む。通信はしない。
4. 書いたものから決まること(E201、W201、W202)。無い日の扱いが要るか。
5. 総当たり(E202〜E204、E301〜E303、E305)。入力の組み合わせの数が予算を超えれば、計算を始めずに E305 で止まる。超えなければ、範囲のすべての入力について、すべての日付と、条件が要る日を計算する。計算が止まる入力(E202〜E204)が一つでもあれば、それを言って終わり、条件と例は確かめない。範囲の一部の入力でしか計算できない決まりについて、条件が「すべての入力で」成り立つとは言えないからである。計算が止まらなければ、条件(E301、E302)と例(E303)を確かめる。

一つめの段の前に、ファイルの全文から鍵の形の値を探す(W901、4.2 の表の下)。警告なので、どの段も止めない。

通れば、何を確かめたかを一行で言う(B の段階の実際の出力)。

```
$ koyomi check examples/close_20th_pay_10th.cal
examples/close_20th_pay_10th.cal: ok — 3 claims hold on all 1,055 days of received (2026-01-01..2028-11-20); 2 examples match
$ koyomi check examples/payment_20th_close_next_10th.cal
examples/payment_20th_close_next_10th.cal: ok — 3 claims hold on all 689 days of received (2026-01-01..2027-11-20); 2 examples match
```

```
$ koyomi check examples/payment_20th_close_next_10th.ja.cal
examples/payment_20th_close_next_10th.ja.cal: ok — 3 claims hold on all 689 days of 受領日 (2026-01-01..2027-11-20); 2 examples match
$ koyomi check examples/payment_20th_close_next_10th.ja.cal --lang ja
examples/payment_20th_close_next_10th.ja.cal: ok — 3 つの条件が、受領日 2026-01-01〜2027-11-20 の 689 日のすべてで成り立ちます。例 2 行も合っています
```

### 3.2 総当たりと予算

入力の組み合わせの数は、日付の範囲の日数と、整数の入力それぞれの範囲の大きさの積である。1.1 の例は 689 通り、10 章の整数を三つ受け取る例は 639 日 × 31 × 2 × 22 で 871,596 通りで、koyomi の検査は 0.07 秒で終わる(A の段階の Python の試作では 6 秒ほどだった)。組み合わせごとに、すべての日付と条件を計算する。計算の段は、報告に使う入力(成り立たない最初の入力など)についてだけ、あとでもう一度計算して残す。

予算は入力の組み合わせの数で数える。既定は 10⁸(100,000,000)にした。B の段階で、手元のマシン(macOS、Apple シリコン。検査は一つのスレッドで走る)で次を測った。

| ファイル | 組み合わせ | 時間 |
|---|---|---|
| `closing_and_payment_days_as_inputs.ja.cal`(10 章) | 871,596 | 0.07 秒 |
| 100 年(1926〜2025 年)の日付 × 締め日 31 × 支払の月 12。締め、翌月の N 日、`roll preceding`、条件三つ | 13,587,300 | 0.92 秒 |
| 同じものに「支払日 <= 受領日 + 60 business days」の条件を足したもの | 13,587,300 | 3.5 秒 |

既定の予算いっぱいまで検査すると、表の二行目の重さの計算なら 7 秒ほど、三行目なら 26 秒ほどかかる。A の段階の目安(10⁷)は Python の試作の速さから見積もったもので、Rust の実装は試作より二けたほど速いので、一けた多い 10⁸ にした。超えたら E305 で止まり、確かめていないと言う。サンプリングして続けることはしない(P2。rulec の §6.3 と同じく、確かめていないのに緑を返すことは、このツールがある理由を否定する)。E305 は、どの入力の範囲が組み合わせの数を大きくしているかを添える。`--budget` で変えられる。

**捨てたもの**:

- 抽象解釈(Catala が使った Mopsa の方法)。範囲が有限で小さいので、総当たりのほうが厳密で、成り立たない日を全部挙げられる。抽象解釈で言えるのは「成り立たないかもしれない」までで、反例は手がかりにとどまる。
- SMT ソルバ。同じ理由に加え、依存を足さないため。
- サンプリング。上のとおり。

### 3.3 報告の形

成り立たない入力が多いとき(10 章の月末締め翌々月末払いは、669 日のうち 648 日)、全部を一行ずつ出すと読めない。そこで次の形にする。

- 続いている日はまとめる(`2026-01-01〜2026-01-29(29 日)`)。文面には最初の六か所までを書き、全部で何か所かを言う。`--format json` は全部を出す。ただし一つの診断につき 100 万か所までで、それより多いときは残りの数だけを言う。10⁸ 通りの入力で一日おきに成り立たない条件があると、続いている日のまとまりが 5,000 万か所になり、覚えておくだけでメモリが足りなくなるからである。
- 整数の入力があれば、その値の組み合わせごとにまとめ、組み合わせごとの数も注に並べる(`月数 1 で 11、月数 2 で 10、…`)。
- 最初の入力の計算を一段ずつ見せる。
- 比較の条件なら、いちばん外れる入力を一つ添える。

### 3.4 「証明」という語

koyomi の検査は、宣言した範囲のすべての入力を計算するので、サンプリングで試すテストとは違う。言えるのは、**書いた決まりが、宣言した範囲のすべての入力で条件を満たす**ことである。言えないことが三つある。範囲の外の入力。書いた決まりが元の文書(契約、約款、法令)と合っているか。祝日の表が現実と合っているか(コピーが配られたファイルと同じであることまでは、固定で言える)。

rulec の §15.47 にそろえ、ツールの出力では「証明」を肯定形で使わない。通ったことは「確かめた」と書く。生成したコードには「検査を通った決まりから生成した」と書き、コードそのものを「検証済み」とは書かない。生成したコードが参照インタプリタと同じ結果を返すことは、範囲のすべての入力で突き合わせたテストである(6.4)。看板の真ん中を prove にせず check にしたのも、同じ理由による。

## 4. 診断

### 4.1 形

dandori の形にそろえる。

```
error[E301]: <ファイル>:<行>:<列>: <一行の見出し>
    <行> | <原文の行>
  = <注>
  <そうなる例の見出し>:
      <計算の段>
```

一行目は業務の言葉で完結させる(rulec の §11 の原則 1)。どの診断にも、そこに至る具体的な入力か計算を添える(原則 2)。直し方は、書き換え後の形まで書く(原則 3)。書き換え後の行は `--format json` の `fix` に入れ、文面では、注がまだ言っていなければ `= 直した行: …` の一行で出す(E102、E103、E105、E111、E303)。

文面の作りで決めたことが二つある。英語の文は大文字で始める(文面の部品を組み合わせても、そうなるよう描くときにそろえる)。日本語の文では、ASCII の名前と日本語のあいだに空白を入れる(`invoice_date は`)。名前は書いた人のものなので、日本語の文に英字の名前が混ざることがあるからである。数字は対象にしない(`第140条` を崩さないため)。

文面は英語が既定で、`--lang ja` か環境変数 `KOYOMI_LANG=ja` で日本語にする。システムのロケールは見ない(rulec の §11 の原則 7)。日本語と英語は `tr!("…", "…")` で隣に書き、片方だけの文面を作れないようにする(rulec の `src/i18n.rs` と同じ考え)。ただし rulec の `tr!` がプロセス全体で一つの言語を読むのと違い、koyomi の `tr!` は二つの文を組にして返し、言語は描くときに渡す(dandori の `src/diag.rs` が文を二つ持つのと同じ)。テストが同じプロセスの中で、英語と日本語の golden を並行して描けるようにするためである。

`--format json` は、ファイルごとに一行で `{"file", "ok", "summary", "diagnostics": [{"code", "severity", "file", "line", "col", "message", "notes", "inputs", "steps", "fails", "fix"}]}` を出す。診断ごとの `file` は、`use calendar` で読んだカレンダーの診断のとき、そのカレンダーのパスになる。`inputs` は例の入力(そのまま `koyomi eval` に渡せる形)、`steps` は計算の段、`fails` は成り立たない入力の全部(続いている日をまとめた `{"from", "to", "params"}` の並び)、`fix` は `.cal` にそのまま貼れる書き換え後の形(無ければ null)。キーは `--lang` に依らず英語。

exit code は 0(エラーなし。警告はあってよい)、1(エラーあり)、2(引数の誤り、読めないファイル、koyomi 自身の不具合)。

### 4.2 台帳

番号と意味はこの表で決め、B の段階の `src/codes.rs` に移す。各コードは、いつ出るか、どう直すか、最小の再現(走る `.cal`)を持ち、`koyomi explain <コード>` が引く。テストが全コードの再現を走らせ、そのコードが出ることを確かめる(rulec の §11 と同じ)。

| コード | いつ出るか |
|---|---|
| E001 | 読めない字句(知らない文字、閉じていない文字列、形の崩れたリテラル) |
| E002 | この位置に書けない語 |
| E003 | ファイルが `calendar` か `dates` の行で始まらない |
| E004 | 節の順序や数が違う(同じ節が二度、calendar のファイルに `inputs` がある、表の出典に `format` か `covers` の行が無い、など) |
| E005 | 字下げが合わない(タブ、揃っていない) |
| E006 | 無い日付・月日・時刻のリテラル(`2026-02-30`、`13-01`、`25:00`) |
| E007 | 同じ名前を二度宣言している |
| E008 | 宣言されていない名前 |
| E009 | 名前や別名が予約語と同じ(koyomi のキーワード、出力先の言語の予約語)か、どの出力先の生成物でも壊れる(`<別名>_at` とぶつかる日付、カレンダーを読み入力が日付だけのファイルの日付 `is_open`) |
| E010 | 公開する名前に ASCII の別名が無い |
| E011 | 型が合わない(日付のところに整数、など) |
| E012 | `date` の入力がちょうど一つではない |
| E013 | 範囲の誤り(両端が無い、空、`>` や `<` で書いた、足す数に負の数が入りうる、日が 1〜31 の外) |
| E014 | 日付どうしが循環している |
| E015 | `use calendar` の先が使えない(読めない、calendar のファイルでない、循環、オフセットが食い違う) |
| E016 | `gen` が書く出力先を W001 が挙げる。`gen` は何も書かずに 1 で終わる(15 章) |
| W001 | 二つの別名が、ある出力先の綴りで一つの識別子になる。または、別名が、ある出力先では使えない名前になる(15 章) |
| E101 | 出典のコピーが無い |
| E102 | 出典が固定されていない(`sha256:` が無い) |
| E103 | コピーが固定と違う |
| E104 | コピーが読めない(文字コード、形式、行の形、同じ日付が二度) |
| E105 | 表の行が `covers` の外にある |
| E106 | `covers listed years` で、あいだに行の無い年がある |
| E107 | オフセットが `±HH:MM` の形でない(タイムゾーンの名前は、夏時間を扱わないので理由を添えてエラーにする。`UTC` は `+00:00` と書くよう言う) |
| E108 | 営業日が一日も無いカレンダー |
| E109 | カレンダーが要る操作か条件なのに、`use calendar` が無い |
| E110 | `at` を書いたのに、カレンダーにオフセットが無い |
| E111 | 法令の引用が使えない(引いた条が固定されていない、条や引用の書き方が読めない、宣言の無い出典や法令でない出典を引いている) |
| W101 | `open` に書いた日が、もともと営業日 |
| W102 | 固定した法令の条が、どこからも引かれていない |
| E201 | 無い日に当たりうる操作に、無い日の扱いが書かれていない |
| E202 | `else reject` の操作が、範囲の中の入力で無い日に当たる |
| E203 | データの範囲の外の日が営業日かを問う計算がある |
| E204 | 計算した日付が 0001-01-01〜9999-12-31 の外に出る |
| W201 | 無い日に当たりえない操作に `else` が書かれている(日が 28 以下、または `else` を取らない操作) |
| W202 | 同じ意味の短い書き方がある(`end of month`、`start of month`、`close end of month`) |
| E301 | 条件が成り立たない入力がある |
| E302 | 単調でない(隣り合う日で、後の日のほうが早い結果になる) |
| E303 | 例の値が違う(例の入力が範囲の外のときも) |
| E304 | 例に入力か日付の列が足りない |
| E305 | 検査の予算を超えた(確かめていない) |
| W901 | 鍵の形の値がファイルに書いてある(文字列でもコメントでも) |

番台は、構文と名前(E0xx)、カレンダーと出典(E1xx)、日付の計算(E2xx)、条件と例(E3xx)で分けた。警告も同じ番台に置く(名前の W001、カレンダーの W101、日付の計算の W201)。増やすときは番台の末尾に足し、番台をまたがない。9xx の番台は、ritsu のどの言語でも同じ検査を指すセキュリティの検査に使う(ritsu の DESIGN 16 章)。

**W901(2026-10-06)**:鍵の形の値(AWS のアクセスキー ID、GitHub・Slack・Stripe・OpenAI・Anthropic・Google の鍵やトークン、Slack の Incoming Webhook の URL、PEM の秘密鍵)が、ファイル(文字列でもコメントでも)に書いてある。調べ方は ritsu-base の `secrets` で、ritsu のどの言語も同じ決まりで調べる(ritsu の DESIGN 16.3)。診断には鍵の種類と接頭辞と長さだけを出し、鍵そのものは出さない。鍵のある行も引用しない。テスト用の値は、同じ行のコメントに `ritsu: test secret` と書けば言わない。

### 4.3 診断の例

10 章の月末締め翌々月末払いは、わざと「受領から60日以内」を破る例である。ファイルは次のとおり。

英語の版(`eom_close_two_months_later.cal`):

```
 1  dates eom_close_two_months_later v1
 2  description "Closes at the end of the month; pays at the end of the month two months later, or on the business day before when that day is closed. The English version of eom_close_two_months_later.ja.cal. It breaks the claim within_60_days_of_receipt on purpose: the claim is this example's own, written as it reads, and does not say how any law is read"
 3  use calendar "calendars/tokyo_business_days.cal"
 4
 5  inputs
 6    received : date  range >=2026-01-01 <=2027-10-31
 7
 8  date closing = received
 9    close end of month    # "closes at the end of the month"
10
11  date payment = closing
12    end of month +2       # "pays at the end of the month two months later"
13    roll preceding        # "on the business day before when that day is closed"
14
15  claims
16    within_60_days_of_receipt : payment <= received + 60 days
```

```
$ koyomi check examples/eom_close_two_months_later.cal
error[E301]: examples/eom_close_two_months_later.cal:16:3: The claim within_60_days_of_receipt fails for 648 of the 669 days of received
    16 |   within_60_days_of_receipt : payment <= received + 60 days
  = It fails on 2026-01-01..2026-01-29 (29 days), 2026-02-01..2026-03-29 (57 days), 2026-04-01..2026-08-30 (152 days), 2026-09-01..2026-10-28 (58 days), 2026-11-01..2026-11-29 (29 days), 2026-12-01..2026-12-27 (27 days), and 4 more runs; --format json lists them all
  = The farthest is received 2026-05-01, where payment 2026-07-31 is 91 days after received
  the first input it fails on:
      received  2026-01-01 Thu
      closing   2026-01-31 Sat  close end of month
      payment   2026-03-31 Tue  end of month +2
                2026-03-31 Tue  roll preceding: a business day, stays
                payment is 89 days after received, and the claim allows at most 60 days after
```

日本語の版(`eom_close_two_months_later.ja.cal`):

```
 1  dates 月末締め翌々月末払い(eom_two_months) v1
 2  description "月末締め翌々月末払い。支払日が休みなら前の営業日。条件「受領から60日以内」をわざと破る例で、この条件は例のために文字どおりに書いたもの。法令の読み方を示すものではない"
 3  use calendar "calendars/東京の営業日.cal"
 4
 5  inputs
 6    受領日(received) : date  range >=2026-01-01 <=2027-10-31
 7
 8  date 締め日(closing) = 受領日
 9    close end of month    # 「月末締め」
10
11  date 支払日(payment) = 締め日
12    end of month +2       # 「翌々月末払い」
13    roll preceding        # 「支払日が休みなら前の営業日」
14
15  claims
16    受領から60日以内 : 支払日 <= 受領日 + 60 days
```

検査すると、次のようになる(B の段階の実際の出力)。

```
$ koyomi check examples/eom_close_two_months_later.ja.cal
error[E301]: examples/eom_close_two_months_later.ja.cal:16:3: The claim 受領から60日以内 fails for 648 of the 669 days of 受領日
    16 |   受領から60日以内 : 支払日 <= 受領日 + 60 days
  = It fails on 2026-01-01..2026-01-29 (29 days), 2026-02-01..2026-03-29 (57 days), 2026-04-01..2026-08-30 (152 days), 2026-09-01..2026-10-28 (58 days), 2026-11-01..2026-11-29 (29 days), 2026-12-01..2026-12-27 (27 days), and 4 more runs; --format json lists them all
  = The farthest is 受領日 2026-05-01, where 支払日 2026-07-31 is 91 days after 受領日
  the first input it fails on:
      受領日  2026-01-01 Thu
      締め日  2026-01-31 Sat  close end of month
      支払日  2026-03-31 Tue  end of month +2
              2026-03-31 Tue  roll preceding: a business day, stays
              支払日 is 89 days after 受領日, and the claim allows at most 60 days after
```

```
$ koyomi check examples/eom_close_two_months_later.ja.cal --lang ja
エラー[E301]: examples/eom_close_two_months_later.ja.cal:16:3: 条件「受領から60日以内」が、受領日 669 日のうち 648 日で成り立ちません
    16 |   受領から60日以内 : 支払日 <= 受領日 + 60 days
  = 成り立たない日: 2026-01-01〜2026-01-29(29 日)、2026-02-01〜2026-03-29(57 日)、2026-04-01〜2026-08-30(152 日)、2026-09-01〜2026-10-28(58 日)、2026-11-01〜2026-11-29(29 日)、2026-12-01〜2026-12-27(27 日)、ほか 4 か所。全部は --format json で出ます
  = いちばん外れるのは受領日 2026-05-01 のときで、支払日 2026-07-31 は受領日の 91 日後
  そうなる例(最初の入力):
      受領日  2026-01-01(木)
      締め日  2026-01-31(土)  月末締め
      支払日  2026-03-31(火)  2 か月後の月末
              2026-03-31(火)  休みなら前営業日: 営業日なので動かない
              支払日は受領日の 89 日後で、条件は 60 日後まで
```

England and Wales の例の受領日の範囲を、GOV.UK の表の終わり 2028-12-31 まで広げると(`tests/mutants/E203_past_the_table.cal`)、次になる。

```
$ koyomi check tests/mutants/E203_past_the_table.cal
error[E203]: tests/mutants/E203_past_the_table.cal:6:3: The computation for received 2028-11-21 asks whether 2029-01-10 is a business day, and the table bank_holidays knows only 2019-01-01..2028-12-31
     6 |   received : date  range >=2026-01-01 <=2028-12-31
  = The same happens for 41 days of received: 2028-11-21..2028-12-31
  = To fix it: make the range `range >=2026-01-01 <=2028-11-20`, or take the copy again once a newer table is out (koyomi source fetch)
  the first input it happens on:
      received  2028-11-21 Tue
      closing   2028-12-20 Wed  close day 20
      payment   2029-01-10 Wed  day 10 of month +1
                                roll preceding: whether 2029-01-10 is a business day is outside what the calendar knows
```

1.1 の例の受領日の範囲を 2027-12-31 まで広げると(`tests/mutants/E203_表の外.cal`)、次になる。

```
$ koyomi check tests/mutants/E203_表の外.cal --lang ja
エラー[E203]: tests/mutants/E203_表の外.cal:6:3: 受領日 2027-11-21 の計算が、2028-01-10 が営業日かを問いますが、祝日の表「祝日」が知っているのは 1955-01-01〜2027-12-31 です
     6 |   受領日(received) : date  range >=2026-01-01 <=2027-12-31
  = 同じことが起きる受領日は 41 日(2027-11-21〜2027-12-31)
  = 直し方: 範囲を `range >=2026-01-01 <=2027-11-20` にしてください。または、新しい表が出てからコピーを取り直してください(koyomi source fetch)
  そうなる例(最初の入力):
      受領日  2027-11-21(日)
      締め日  2027-12-20(月)  20 日締め
      支払日  2028-01-10(月)  翌月 10 日
                                休みなら前営業日: 2028-01-10 が営業日かは、表の外なので分からない
```

無い日の扱いを書き忘れた英語のファイル(`tests/mutants/E201_no_way_for_a_missing_day.cal`)

```
1  dates month_later_example v1
2
3  inputs
4    received : date  range >=2026-01-01 <=2026-12-31
5
6  date month_later = received
7    + 1 month
```

では、次になる。

```
$ koyomi check tests/mutants/E201_no_way_for_a_missing_day.cal
error[E201]: tests/mutants/E201_no_way_for_a_missing_day.cal:7:3: `+ 1 month` can land on a day the month does not have, and the line does not say what to do then
     7 |   + 1 month
  = For received 2026-01-29 it would be 2026-02-29
  = To fix it, write one of: `+ 1 month else end_of_month` (giving 2026-02-28), `+ 1 month else start_of_next_month` (giving 2026-03-01), `+ 1 month else reject` (the check makes sure it never happens in the range)
  the input that gets there:
      received     2026-01-29 Thu
      month_later                  + 1 month: 2026-02-29 does not exist
```

日本語の名前で書いた同じファイルでは、

```
1  dates 一か月後の例(month_later_example) v1
2
3  inputs
4    受領日(received) : date  range >=2026-01-01 <=2026-12-31
5
6  date 一か月後(month_later) = 受領日
7    + 1 month
```

次になる(`tests/mutants/E201_無い日の扱いが無い.cal`)。例の入力は、範囲の中で最初に無い日に当たる受領日である(2026 年はうるう年でないので、1 月 29 日から当たる)。

```
$ koyomi check tests/mutants/E201_無い日の扱いが無い.cal --lang ja
エラー[E201]: tests/mutants/E201_無い日の扱いが無い.cal:7:3: `+ 1 month` はその月に無い日に当たることがありますが、そのときどうするかが書かれていません
     7 |   + 1 month
  = 受領日 2026-01-29 なら 2026-02-29 になります
  = 直し方: 次のどれかを書いてください。`+ 1 month else end_of_month`(2026-02-28 にする)、`+ 1 month else start_of_next_month`(2026-03-01 にする)、`+ 1 month else reject`(範囲の中で起きないことを検査が確かめる)
  そうなる例:
      受領日    2026-01-29(木)
      一か月後                    1 か月後の同じ日: 2026-02-29 は無い
```

例の入力を探すため、E201 のときも、その操作の前までを範囲の入力で順に計算する。範囲の中で一度も無い日に当たらない操作でも、E201 は出す(書かなければエラーにする、という決まりは、書いたものだけから決める)。そのときの注は「範囲の中では起きませんが、`else reject` と書けば、起きないことを検査が確かめます」になる。

## 5. コマンド

| コマンド | すること |
|---|---|
| `koyomi check <file.cal>... [--format json] [--budget <n>]` | 検査(3 章)。ディレクトリを渡すと、下の `.cal` を全部(パスの順に) |
| `koyomi eval <file.cal> <名前>=<値>... [--format json]` | 一つの入力ですべての日付を計算し、計算の段を見せる。入力は名前でも別名でも渡せる。条件も、その入力で成り立つかを一つずつ言う。calendar のファイルなら `koyomi eval <file.cal> <日付>` で、営業日か休みか(休みの理由つき) |
| `koyomi diff <旧> <新> [--format text\|markdown\|json] [--budget <n>]` | 二つの版の答えを比べる。カレンダーは両方が知っている日の全部で、日付のファイルは両方が受け取る入力の全部で比べる。それぞれの側は、パス、`<パス>@<git の版>`、`@<名前>/<パス>`、`@<名前>@<版>/<パス>` のどれか(ritsu の DESIGN 17.9.3)。違いがあれば 1、比べられなければ 2 |
| `koyomi gen <file.cal>... --target typescript\|python\|go\|rust\|sql\|wasm... [--extra runner] [--out <dir>] [--check]` | 指定した出力先のコードと、`--extra runner` を付ければランナーを生成する(6 章、16 章)。検査を通らないファイルからは生成しない。書く出力先を W001 が挙げるファイルがあれば、何も書かずに 1 で終わる(E016、15 章)。`--target` は何度でも書け、無ければ何も書かずに 2 で終わる。`--out` の既定は `generated`。`--check` は、書き出す代わりに、同じ引数で書くはずの生成物が最新かを確かめる |
| `koyomi vectors <file.cal>` | 範囲のすべての入力の結果を JSON Lines で出す(6.3) |
| `koyomi doc <file.cal> [--format markdown\|html] [--months <YYYY-MM>..<YYYY-MM>]` | 人が読むページ(7 章)を標準出力に。既定は Markdown。条件が成り立たなくてもページを出して 0、ページを作れないエラーなら診断を標準エラーに出して 1 |
| `koyomi api <file.cal>` | 関数・引数・カレンダー・データの範囲・出典のハッシュを JSON で出す(8 章) |
| `koyomi source fetch\|pin\|outdated <file.cal>` | 出典を取ってきて保存する・固定する・元が変わったかを問う(9 章) |
| `koyomi explain <コード>`、`koyomi explain --all [--format markdown]` | 診断のコードを引く |

`--lang ja|en` はどのコマンドにも付けられる(無ければ `KOYOMI_LANG`、次に ritsu のどの言語も読む `RITSU_LANG`、どちらも無ければ英語)。`koyomi --help`、`koyomi <コマンド> --help`、`koyomi --version`。

`eval` の例(1.1 のファイル。B の段階の実際の出力):

```
$ koyomi eval examples/payment_20th_close_next_10th.cal received=2026-04-01
received  2026-04-01 Wed
closing   2026-04-20 Mon  close day 20: closes the period 2026-03-21..2026-04-20
payment   2026-05-10 Sun  day 10 of month +1
          2026-05-08 Fri  roll preceding: 2026-05-10 (Sunday) and 2026-05-09 (Saturday) are closed
          time 2026-05-08T09:00:00+09:00 (2026-05-08T00:00:00Z in UTC)

claim paid_on_a_business_day: holds
claim within_60_days_of_receipt: holds (payment is 37 days after received, and the claim allows at most 60 days after)
claim later_receipt_later_payment: holds (for the day before, 2026-03-31, payment is 2026-05-08)
```

日本語の版を `--lang ja` で:

```
$ koyomi eval examples/payment_20th_close_next_10th.ja.cal 受領日=2026-04-01 --lang ja
受領日  2026-04-01(水)
締め日  2026-04-20(月)  20 日締め: 2026-03-21〜2026-04-20 の期間の締め日
支払日  2026-05-10(日)  翌月 10 日
        2026-05-08(金)  休みなら前営業日: 2026-05-10 は日曜、2026-05-09 は土曜で休み
        時刻 2026-05-08T09:00:00+09:00(UTC で 2026-05-08T00:00:00Z)

条件「営業日に払う」は成り立つ
条件「受領から60日以内」は成り立つ(支払日は受領日の 37 日後で、条件は 60 日後まで)
条件「遅い受領は遅い支払」は成り立つ(前の日 2026-03-31 なら支払日は 2026-05-08)
```

計算の段は A の段階のスケッチと同じになった。条件の行は B の段階で足した。`is monotonic` は隣り合う二日を比べる条件なので、`eval` はその前の日も計算して比べる。範囲の外の入力は計算せず、生成したコードの入口のガードと同じく受け付けない(exit 1)。

カレンダーには日付を一つ渡す。

```
$ koyomi eval examples/calendars/england_and_wales.cal 2026-05-04
2026-05-04 Mon  closed: Early May bank holiday
```

```
$ koyomi eval examples/calendars/東京の営業日.cal 2026-05-04 --lang ja
2026-05-04(月)  休み: みどりの日
```

**決定**:コマンドとフラグの定義を `src/cli.rs` の一枚の表に置き、`--help` の表示と引数の読み取りが同じ表を引く(rulec の §12.1)。知らないフラグ、閉じた集合の外の値(`--target java`)、値の無いフラグ、二度書いたフラグは exit 2 で止める。黙って無視すると、エージェントはフラグが効いたと信じて次に進むからである。短いフラグは `-h` と `-V` だけ。

通信するのは `source fetch` と `source outdated` の二つだけである。`check` を含め、ほかのコマンドは通信しない。

## 6. 生成するコード

### 6.1 共通の形

**決定**:`koyomi gen` は、検査を通ったファイルから、`--target` で指定した出力先ごとに依存の無いファイルを一つ(Go は一つのパッケージ)書き、`--extra runner` を付ければそのランナーも書く(16 章)。検査を通らないファイルからは書かない(診断を出し、ほかのファイルは書いて、exit 1)。書く出力先を W001 が挙げるファイルがあれば、E016 を出し、どのファイルも書かずに exit 1 にする(15 章)。

| 出力先 | 生成物 | ランナー(`--extra runner`) |
|---|---|---|
| TypeScript | `typescript/<別名>.ts` | `typescript/<別名>_runner.ts` |
| Python | `python/<別名>.py` | `python/<別名>_runner.py` |
| Go | `go/<パッケージ名>/<パッケージ名>.go` | `go/<パッケージ名>/<パッケージ名>_runner_test.go` |
| Rust | `rust/<別名>.rs` | `rust/<別名>_runner.rs` |
| SQL(PostgreSQL) | `sql/<別名>.sql` | `sql/<別名>_runner.sql` |
| Wasm | `wasm/<別名>_wasm.rs`(crate root)と、それが読む `wasm/<別名>.rs`(Rust と同じモジュール)、`wasm/<別名>.wit` | `wasm/<別名>_runner.mjs` |

書き出す先は `--out`(既定は `generated`)。calendar のファイルからは `is_open` だけの生成物を、dates のファイルからは日付ごとの関数と、カレンダーを読んでいれば `is_open` を書く。中身が変わらないファイルは書き直さない。`--check` は書かずに比べ、違うファイルと無いファイルを挙げて exit 1 にする(CI 用)。二つの `.cal` が同じ別名で同じ場所に書こうとしたら、何も書かずに exit 2 で止める。

頭には、どの `.cal` のどの版から作ったかを書く。`koyomi gen examples/payment_20th_close_next_10th.cal --target typescript` の頭(実際の出力):

```ts
// Code generated by koyomi 0.42.0. DO NOT EDIT.
// Source: payment_20th_close_next_10th.cal (dates payment_20th_close_next_10th v1, sha256:9973542043dbee17)
// Calendar: calendars/tokyo_business_days.cal (calendar tokyo_business_days v1, sha256:37af228cf6ba7b95)
// Cites: national_holidays = file calendars/data/syukujitsu.csv sha256:cec37a743c96995c (https://www8.cao.go.jp/chosei/shukujitsu/syukujitsu.csv), covers 1955-01-01..2027-12-31
```

日本語の版 `koyomi gen examples/payment_20th_close_next_10th.ja.cal --target typescript` では次のとおり。

```ts
// Code generated by koyomi 0.42.0. DO NOT EDIT.
// Source: payment_20th_close_next_10th.ja.cal (dates 支払条件 v1, sha256:75482b2e796019d0)
// Calendar: calendars/東京の営業日.cal (calendar 東京の営業日 v1, sha256:d7b6134e23a8cb9f)
// Cites: 祝日 = file calendars/data/syukujitsu.csv sha256:cec37a743c96995c (https://www8.cao.go.jp/chosei/shukujitsu/syukujitsu.csv), covers 1955-01-01..2027-12-31
```

一行目は Go の決まり(`^// Code generated .* DO NOT EDIT\.$`)の形にし、どの言語でも英語のまま書く。二行目から下は `--lang ja` で日本語(`もと:`、`カレンダー:`、`出典:`)になる。ハッシュはどれも SHA-256 の先頭 16 桁で、`source` の固定と同じ長さである。パスは生成のもとにした `.cal` のディレクトリから数え、生成した機械のパスは書かない。読んだカレンダーがさらに別のカレンダーを読んでいれば、その分も `Calendar:` の行に並ぶ。法令を引いていれば、`Cites: 民法 = law 129AC0000000089 asof 2026-10-01 (第140条 sha256:e880059021fbb67d, …), revision 129AC0000000089_20260624_508AC0000000045` の形の行が足される。

頭の一行目と二行目は、ritsu の土台(ritsu-emit の `header`)が書く形で、rulec・chobo・dandori の生成物も同じ形にそろえた(ritsu の DESIGN 9.2)。一行目のバージョンは ritsu のバージョンで、ワークスペースの一つのバージョンである(いまは 0.1.0 で、koyomi のクレートのバージョンと同じ数なので、`koyomi gen` の出力は変わらない)。ritsu の `ritsu gen`(ritsu の DESIGN 9.3)がパッケージに入れるときは、`.cal` のパスと、`Calendar:` と `Cites:` の行のパスを、プロジェクトのルートから数えて書く(`codegen::unit_shown`)。`ritsu gen` が `crates/ritsu/tests/projects/stockroom` から書いた TypeScript の頭(koyomi の例から作ったものではないので、`tests/design.rs` が生成物と照らし合わせる `ts` の塊にはしていない):

```
// Code generated by koyomi 0.42.0. DO NOT EDIT.
// Source: dates/payment_terms.cal (dates payment_terms v1, sha256:20eacc1719046107)
// Calendar: calendars/weekdays.cal (calendar weekdays v1, sha256:dbf95c1f04262520)
```

日付の計算は、通算日(1970-01-01 からの日数)の整数の上で、生成物の中に書いた小さな関数で行う。どの関数も `src/date.rs` と `src/calendar.rs` の同じ名前の手順を移植したもので、TypeScript・Python・Go・Rust・SQL の五つの言語に同じ手順を書く(Wasm は Rust のモジュールをそのまま持つ)。言語の日付のライブラリは使わない(1.7 の表のとおり、言語ごとに結果が違うから)。言語の日付の型に触れるのは、入力を受け取るときと結果を返すときだけである。Python の `date` は年・月・日の値を読み書きするだけで、SQL の `date` は `d - DATE '1970-01-01'` と `DATE '1970-01-01' + z` で日数に直すだけである。

小さな関数の名前は `_` で始め、言語の書き方に合わせる(TypeScript と Go は `_closeDay`、ほかは `_close_day`)。書くのは、そのファイルの操作が使う関数と、それが呼ぶ関数だけである(`net30.cal` の生成物に `_closeDay` は無い)。無い日の扱いと営業日の慣行は、`.cal` の語を文字列のまま渡す(`"end_of_month"`、`"preceding"`)。無い日に当たりえない操作(日が 28 以下の `day N of month` と `close day N`、月初と月末)には `"none"` を渡す。`"none"` で無い日に当たるのは koyomi の不具合なので、`KoyomiError` ではない例外(Go と Rust は panic、SQL は SQLSTATE XX000)にする。

公開する関数は、日付ごとに一つ(名前は別名。Go はパスカルケース)と、`at` のある日付の時刻の関数(`<別名>_at`)と、カレンダーを読んでいれば `is_open` である。名前と署名は `src/naming.rs` が決めていて、`koyomi api` が出すものと一字一句同じになる(生成物の中に api の署名がそのままあることを `tests/api.rs` が確かめる)。日付の関数は、始まりの日付から自分までの操作を順に並べ、ほかの日付の関数を呼ばない。`<別名>_at` だけは、自分の日付の関数を呼んでから時刻にする。関数の中の変数は `day` 一つで、これは koyomi のキーワードなので、どの別名ともぶつからない。Wasm のモジュールが export する関数は `call` 一つで、日付の関数と `is_open` の結果を JSON で返す(13 章)。

同じファイルの TypeScript から、payment の二つの関数(実際の出力):

```ts
/**
 * Gives payment.
 * It takes received 2026-01-01..2027-11-20, the range koyomi check checked every input of; outside it, the error is a KoyomiError of kind range.
 */
export function payment(received: string): string {
  let day = _inputDate(received, "received", 20454, 21142); // range >=2026-01-01 <=2027-11-20
  day = _closeDay(day, 20, "none"); // close day 20  (closing)
  day = _dayOfMonth(day, 10, 1, "none"); // day 10 of month +1
  day = _roll(day, "preceding"); // roll preceding
  return _formatDate(day);
}

/** Gives the time of payment's day (at 09:00, offset +09:00) in RFC 3339, in UTC, ending in Z. */
export function payment_at(received: string): string {
  return _at(payment(received), 540, 540);
}
```

日本語の版の、支払日の二つの関数:

```ts
/**
 * Gives 支払日 (payment).
 * It takes 受領日 2026-01-01..2027-11-20, the range koyomi check checked every input of; outside it, the error is a KoyomiError of kind range.
 */
export function payment(received: string): string {
  let day = _inputDate(received, "受領日", 20454, 21142); // range >=2026-01-01 <=2027-11-20
  day = _closeDay(day, 20, "none"); // close day 20  (締め日)
  day = _dayOfMonth(day, 10, 1, "none"); // day 10 of month +1
  day = _roll(day, "preceding"); // roll preceding
  return _formatDate(day);
}

/** Gives the time of payment's day (at 09:00, offset +09:00) in RFC 3339, in UTC, ending in Z. */
export function payment_at(received: string): string {
  return _at(payment(received), 540, 540);
}
```

行末のコメントは、その操作の `.cal` の行である。始まりの日付の操作には、一行目にその日付の名前を添える(`(締め日)`)。

**エラー**:どの言語でも、エラーから種類が取れる(TypeScript・Python・Rust は `kind`、Go は `Kind`、SQL はメッセージの頭の `koyomi <種類>:`、Wasm は答えの `refused`)。種類は四つある。

| 種類 | いつ起きるか | 参照インタプリタでは |
|---|---|---|
| `range` | 入力が `range` の外(入口のガード) | 範囲の外の入力 |
| `data` | カレンダーが知らない日が営業日かを問うた | `OpFail::Outside`(E203) |
| `reject` | `else reject` の操作が無い日に当たった | `OpFail::Missing`(E202) |
| `date` | 日付が 0001-01-01〜9999-12-31 の外に出た。または、入力が日付として読めない(TypeScript の文字列、Go と Rust の `Date` の値、Wasm に渡す JSON の文字列) | `OpFail::OutOfRange`(E204) |

検査を通ったファイルでは、範囲の中のどの入力でも `data`・`reject`・`date` は起きない。検査がすべての入力で確かめたからである。したがって公開する関数から実際に出るのは、範囲の外の入力の `range`、`is_open` にデータの範囲の外の日を渡したときの `data`、読めない日付の `date` の三つで、ほかは起きないはずのことへの備えである。Wasm のモジュールは、このほかに、渡された JSON が入力を持たないとき(入力が無い、日付が文字列でない、整数が整数の数でない、一つの JSON のオブジェクトでない)に種類 `input` で拒否する(13 章)。

**生成物が使う名前**:koyomi のキーワードと出力先の言語の予約語(PostgreSQL の予約語と PL/pgSQL の予約語の `begin`・`declare`・`execute`・`foreach`、ファイルの別名の `json`・`public` なども)と同じ別名は、生成する前に E009 でエラーにする(`src/reserved.rs`。予約語の表そのものは C.10 から ritsu-emit のもの。11.1)。生成物が置く名前と読み込む名前とぶつかる別名は、どの出力先が壊れるかで分ける(`src/spellings.rs`。15 章、17 章)。どの出力先でも壊れるもの(`<別名>_at` とぶつかる日付、カレンダーを読み入力が日付だけのファイルの日付 `is_open`)は E009 で、一部の出力先だけが壊れるものは `check` で W001 の警告にし、その出力先を書く `gen` を E016 で止める。後者は、Go の生成物が置く `Date`・`KoyomiError`・`ParseDate` を PascalCase の綴りに持つ別名、Go の関数が使う `err`(入力の別名のとき)、Python の生成物が呼ぶ `str`・`tuple`・`isinstance`(整数の入力を取る関数があるとき)、Python の生成物が読み込む標準ライブラリのモジュールと同じファイルの別名、PostgreSQL が自分のものにするスキーマの名前になるファイルの別名(`pg_` で始まる名前、`information_schema`)である。出力先が別名を綴り直してから同じになる名前(Go の PascalCase とパッケージの名前、PostgreSQL の 63 バイト)と、rustc のリントが警告する名前(`_` が二つ続く別名)も同じである(15 章)。Wasm の出力先で増えるのは入力 `__proto__` だけである(17 章)。crate root が置く名前は、canonical ABI の三つ(`cabi_realloc`、`call`、`cabi_post_call`)とモジュールの `k` のほかは `_` で始まり、別名はモジュールの中の名前(`k::payment`)にしかならない。world の名前は別名から作り、WIT が読めない形にならないようにする(13.2)。

`--lang ja` を付けると、頭の二行目から下、関数の説明、エラーのメッセージが日本語になる。

**捨てたもの**:

- 小さな関数を、使わないものも含めて全部の生成物に書くこと。どの生成物も同じ形になるが、読む人は使われない関数まで読むことになる。どの関数もどこかの生成物で使われ、突き合わせで確かめられていることは、6.4 の helpers のファイルが受け持つ。
- Go のエラーを panic で運び、公開する関数の `defer` で `error` に直すこと。本体は短くなるが、名前付きの戻り値が要るので `koyomi api` の署名(`(Date, error)`)と合わなくなり、Go を読む人にも見慣れない形になる。
- 無い日の扱いと慣行を列挙の定数にすること。TypeScript の `enum` は型を剥がすだけでは走らない(6.2)。文字列なら、どの言語でも `.cal` の語がそのまま読める。

### 6.2 出力先ごとの形

| 出力先 | 日付の型 | エラー | 確かめるツール(テスト) |
|---|---|---|---|
| TypeScript | `"YYYY-MM-DD"` の `string` | `KoyomiError`(`Error` の子、`kind`) | `node`(型を剥がして走らせる)、`tsc --strict --erasableSyntaxOnly` |
| Python | `datetime.date` | `KoyomiError`(`ValueError` の子、`kind`) | `python3`、`mypy --strict` |
| Go | パッケージの `Date{Year, Month, Day int}`、`ParseDate`、`String()` | `error` を返す(中身は `*KoyomiError`、`Kind`) | `gofmt -l` が何も出さない、`go vet`、`go test -c` |
| Rust | モジュールの `Date { year, month, day }`(`FromStr`、`Display`、`Copy`、`Ord`) | `Result<_, KoyomiError>`(`kind`) | `rustc -D warnings` をエディション 2021 と 2024 で |
| SQL(PostgreSQL 14 以降) | `date` | `RAISE EXCEPTION 'koyomi <種類>: …' USING ERRCODE = '22023'` | 使い捨てのクラスタの PostgreSQL |
| Wasm | JSON の `"YYYY-MM-DD"` の文字列 | 例外を上げず、答えに拒否の行 `{"refused":"<種類>","error":"…"}` を返す | `rustc --target wasm32-unknown-unknown -D warnings` をエディション 2021 と 2024 で、Node、wasm-tools と wasmtime |

- TypeScript は Node が型を剥がすだけで走る形にする(`enum`、実行時のコードを含む `namespace`、コンストラクタの引数でのプロパティ宣言を書かない。dandori の生成物と同じ制約)。テストは `tsc --erasableSyntaxOnly` で、その形から外れていないことも確かめる。日付を `string` にしたのは、日付だけの型が標準に無く(`Date` はタイムゾーンを持ち、Temporal は Node 23 に無い)、JSON でそのまま運べるからである。ランナーは生成物を名前空間として読み込む(`import * as m`)ので、日付の別名がランナーの名前とぶつからない。
- Python は標準の `datetime.date` で受け取り、返す。範囲は Python の `date` と同じなので、変換で溢れない。どの関数にも型注釈を付け、`mypy --strict` を通す。
- Go と Rust は日付だけの型が標準に無いので、生成物の中に小さな `Date` の型を置く。`time.Time` を使う案は、タイムゾーンの取り違え(JST の時刻を UTC に直してから日付を取ると前の日になる)を呼び込むので採らなかった。
- Go の関数は、失敗しうるものは小さな関数も含めて `(int, error)` を返し、呼ぶたびに `err` を見る。gofmt が揃える行末のコメント(祝日の表)は、gofmt と同じ揃え方(いちばん長い行の一つ後ろ。幅は文字数)で書く。ランナーは同じパッケージのテストのファイル(`TestMain`)にした。環境変数 `KOYOMI_RUNNER` があれば、テストを走らせる代わりに vectors を読む。`go test -c` で作ったプログラムを走らせる。同じパッケージなので import のパスが要らず、生成したディレクトリをどのモジュールに置いても動く。
- Rust の生成物はモジュール(`main` を持たない)で、エディション 2021 でも 2024 でも通る書き方にする(let チェーンを使わない、など)。利用者のクレートのエディションに依らないようにするためである。小さな関数は `_` で始まるので、使わないものがあっても警告にならない。ランナーは `#[path]` でモジュールを読み、`#[allow(dead_code)]` をその読み込みにだけ付ける。標準のライブラリに JSON が無いので、ランナーは vectors の `in` を自分で読む(名前にエスケープは無く、値は引用符の中の日付か数)。
- SQL は、ファイルの別名のスキーマ(`CREATE SCHEMA IF NOT EXISTS <別名>`)に関数を作る。公開する関数と、ループや分岐のある小さな関数は PL/pgSQL で、式一つで書ける小さな関数(曜日、うるう年、月の日数、表の日か、切り捨ての割り算、年月日の文字列)は SQL 標準の関数本体(`RETURN 式`、PostgreSQL 14 以降)にした。後者には `STRICT` を付けない。`STRICT` の関数の本体に `CASE` や `AND` があると、PostgreSQL はその関数を呼ぶ側の式に展開しないからである。展開されるようにしたことで、SQL の突き合わせ(この出力先だけを走らせたとき)は 33.7 秒から 22.5 秒になった。祝日の表は `_is_holiday` の中の `= ANY (ARRAY[…])` に一行一日で埋める。年・月・日のように値を二つ以上返す関数は、整数の配列を返す。公開する関数は `STRICT` で、`NULL` を渡すと `NULL` が返る。スキーマで分けたのは、一つのファイルが関数をいくつも持つので、名前を `<別名>_<日付>` とつなぐと別のファイルの名前とぶつかりうるからである(`a_b` の `c` と `a` の `b_c`)。ランナーは psql のスクリプトで、vectors を `\copy … FROM pstdin` で一時テーブルに入れ、一行ずつ `pg_temp` の関数で計算して `COPY … TO STDOUT` で出す。その関数は SQLSTATE 22023 を受け止めて `error <種類>` にする。
- Wasm は、Rust のモジュールを canonical ABI の関数一つ(`call: func(input: string) -> string`)の後ろに置き、`rustc` だけで依存の無いモジュールに組む。受け渡しは rulec の Wasm と同じく、JSON のオブジェクトを一つ渡して一つ受け取る形で、答えは koyomi vectors の行と同じ形になる。決めたことと理由は 13 章にある。

**捨てたもの**:

- 言語の日付のライブラリ(dateutil、`time.AddDate`、`interval`、Temporal)を呼ぶこと。1.7 の表。
- 実行時のライブラリを別に配ること。rulec の P5 と同じく、生成物は一つで読めて依存が無いほうがよい。小さな関数を生成物ごとに書く重複は、テストで同じ結果になることを確かめる(6.4)。
- 日付を通算日の整数で受け渡すこと(rulec の生成物の `date` はそうしている)。期日を使う側のコードは日付で考えるので、言語ごとの日付の形にした。通算日は生成物の内側だけで使う。
- Go のランナーを別のパッケージ(`package main`)にすること。生成物のパッケージを import するパスが、置かれるモジュールで変わる。go.mod を二つ書いて `replace` でつなぐ形(rulec の Go のランナー)もあるが、生成物に go.mod を書かない(dandori の Go の生成物と同じ)ほうを選んだ。

### 6.3 vectors

`koyomi vectors <file.cal>` は、範囲のすべての入力について、一行に一つの JSON を出す。キーは名前(rulec の vectors と同じ)。`koyomi vectors examples/payment_20th_close_next_10th.cal` の 91 行目(実際の出力):

```jsonl
{"in":{"received":"2026-04-01"},"out":{"closing":"2026-04-20","payment":"2026-05-08","payment.at":"2026-05-08T00:00:00Z"}}
```

日本語の版 `koyomi vectors examples/payment_20th_close_next_10th.ja.cal` の 91 行目:

```jsonl
{"in":{"受領日":"2026-04-01"},"out":{"締め日":"2026-04-20","支払日":"2026-05-08","支払日.at":"2026-05-08T00:00:00Z"}}
```

`out` には、ファイルの日付を宣言の順に、`at` のある日付ならその直後に時刻(`<名前>.at`、`Z` で終わる UTC)を置く。行は検査が回す順(`check::Inputs`。整数の入力が外側で、最後に宣言したものがいちばん速く回り、日付が内側)に出すので、n 行目は検査が n 番目に計算した入力である。

範囲の行のあとに、範囲のすぐ外の入力を足し、そこでは `"error":"range"` を期待する。日付の範囲の最初の日の前日と最後の日の翌日(整数の入力は最小の値)、続いて整数の入力ごとに、最小の値の一つ下と最大の値の一つ上(日付は範囲の最初の日)である。どの日付の関数も受け取らない入力(条件だけが使う整数)は、生成したコードにガードが無いので、外の行を出さない。0001-01-01 の前日のように日付にならない値も出さない。

```jsonl
{"in":{"received":"2025-12-31"},"error":"range"}
{"in":{"received":"2027-11-21"},"error":"range"}
```

```jsonl
{"in":{"受領日":"2025-12-31"},"error":"range"}
{"in":{"受領日":"2027-11-21"},"error":"range"}
```

calendar のファイルの vectors は、データの範囲のすべての日について `{"in":{"date":"…"},"out":{"open":true}}` を出し、データの範囲の前日と翌日で `"error":"data"` を期待する。表の無いカレンダーはどの日のことも分かるので、1900〜2100 年(73,414 日)に限って出し、外の行は無い。

行の数(PLAN C.8 の数と同じ)は、`england_and_wales` 3,655、`net30` 1,066、`close_20th_pay_10th` 1,057、`close_and_pay_on_given_days` 872,968、`period_of_months` 4,384、日本の暦の例は英語の版と日本語の版で同じで、`payment_20th_close_next_10th` 691、`civil_code_period_end` 4,384、`closing_and_payment_days_as_inputs` 871,604、`tokyo_business_days`(`東京の営業日`)と `civil_code_142_days`(`民法142条の休日`)26,665 である(`tests/vectors.rs`)。検査を通らないファイルには出さない(診断を出して exit 1。`gen` と同じ)。

### 6.4 突き合わせ

**決定**:生成したコードを、範囲のすべての入力で参照インタプリタと突き合わせる(P1)。

- ランナーは vectors の行を標準入力から読み、一行ごとに生成した関数を呼んで、結果を一行に書く。日付の値と時刻を `out` と同じ順に空白で区切って並べ(calendar なら `true` か `false`)、例外が出たら、最初に出た例外の種類で `error <種類>` と書く。
- `tests/targets.rs` は、出力先ごとに、`examples/` の検査を通るファイル全部(英語を先にした段階から、calendar の五つと dates の十。England and Wales の例、日本の暦の例の英語の版と日本語の版)と `tests/fixtures/` の十四のファイル(英語の七つと、同じものを日本語で書いた七つ)を生成し、その出力先のツールで生成物を確かめてから(6.2 の表の右の列)、ランナーに vectors の全部の行を流して一行ずつ比べる。vectors はテストの中で作りながらパイプで流し、ディスクには書かない。合わない行があれば、最初の五行の入力と、参照インタプリタの結果と、生成したコードの結果を出して落ちる。合えば `compared <出力先> <ファイル>: <行数> lines` を一行出す。ツールが無ければ `SKIP: <理由>` の行を出して通す。
- `--lang ja` の生成物も、同じツールで確かめる(型の検査、gofmt と go vet、二つのエディションの rustc、PostgreSQL への読み込み、wasm32-unknown-unknown への rustc)。日本語の生成物が英語と違うのはコメントとエラーのメッセージだけだが、メッセージの文字列がその言語の構文を壊していないことと、エラーのときに実際に組み立てられることを確かめるため、`payment_20th_close_next_10th.ja.cal` と英語の版 `payment_20th_close_next_10th.cal`(範囲の外の行で `range`)、`東京の営業日.cal` と `england_and_wales.cal`(データの範囲の外の行で `data`)は、日本語の生成物でも全部の行を突き合わせる。
- Wasm のモジュールは、ランナーの突き合わせのほかに、`tests/wasm.rs` が JSON の答えそのものを確かめる。全部のファイルについて(大きなファイルは最初の 2,000 行と範囲のすぐ外の行、カレンダーを読む日付のファイルはカレンダーの行も)、答えが koyomi vectors の行と一字一句同じであること、契約の外の入力に拒否の行で答えること、`.wit` から作った component を wasmtime が呼べることである(13.5)。
- `tests/fixtures/` のファイルは、表の無いカレンダー `calendars/休みの書き方を全部使う.cal`(休みの曜日、年をまたぐ毎年の休み、一日だけの毎年の休み、2 月 29 日の毎年の休み、特定の日、例外の営業日を全部使い、オフセットは +05:45)と、それを使う、または使わない六つの dates のファイルである。月の足し算と引き算をそれぞれ月数 0〜24 で(`helpers_月を足す`、`helpers_月を引く`、各 1,835,350 通り)、k か月後の N 日・N 日締め・月初と月末・年の足し引きを日 27〜31 で(`helpers_月の日と締め`、367,070 通り)、営業日の数え方・四つの慣行・`if closed`・`at 09:00` と `at end of day` を日数 0〜3 で(`helpers_営業日`、293,656 通り)、どれも 1900〜2100 年のすべての日に使う(英語を先にした段階で、同じものを英語の名前で書いた `calendars/every_way_to_close.cal`、`helpers_add_months`、`helpers_subtract_months`、`helpers_days_of_months`、`helpers_business_days`、`helpers_reject`、`helpers_no_dates` を足した。vectors は名前のほかは一行も違わない)。`else reject` は 2 月 29 日を含む範囲では検査が止めるので、無い日に当たらない 2001-03-01〜2003-12-31 で使う(`helpers_エラーにする`)。`date` を一つも宣言しないファイル(`helpers_日付が一つも無い`)も検査を通るので、生成したコードが動くことを確かめる。PLAN C.8 は helpers を Python のランナーで一つ 60 秒以内に収まる大きさに分けることにしていたが、いちばん大きいファイルでも 11 秒ほどだった。

D の段階の終わりに、この機械(macOS、Apple シリコン、14 コア)で `cargo test -- --nocapture` を走らせた結果(C の段階の終わりから、英語の版の例 `payment_20th_close_next_10th.cal` の 691 行が増えた):

| 出力先 | ツール | 突き合わせた行 | 突き合わせの時間 |
|---|---|---|---|
| TypeScript | Node v23.11.0、tsc 7.0.2 | 5,341,318 | 6.6 秒 |
| Python | Python 3.14.6、mypy 2.4.0(Python 3.13.11 の venv) | 5,341,318 | 16.3 秒 |
| Go | go 1.25.5 | 5,341,318 | 7.3 秒 |
| Rust | rustc 1.94.1 | 5,341,318 | 7.7 秒 |
| SQL | PostgreSQL 18.0 | 5,341,318 | 26.3 秒 |

五つの出力先のテストは並んで走り、一つの出力先の中ではランナーを四つずつ同時に走らせる(SQL は一つのクラスタに四つの psql)。時間は、その出力先の生成物を確かめ終えてから、全部のファイルを突き合わせ終えるまでで、ほかの出力先と同時に走っているときのものである。`cargo test` 全体は 99 のテストで 40 秒だった(C の段階の終わりは 82 のテストで 41 秒。走らせるたびに数秒は変わる)。どの出力先でも vectors の全部の行を突き合わせ、行を間引いていない。

英語を先にした段階の終わりに、同じ機械で `cargo test -p koyomi -p chobo -- --nocapture` を走らせた結果(突き合わせるファイルが 29 になり、行は倍を超えた):

| 出力先 | 突き合わせた行 | 突き合わせの時間 |
|---|---|---|
| TypeScript | 11,554,942 | 13.7 秒 |
| Python | 11,554,942 | 28.8 秒 |
| Go | 11,554,942 | 14.5 秒 |
| Rust | 11,554,942 | 14.9 秒 |
| SQL | 11,554,942 | 46.9 秒 |

koyomi のテストは 156 で、テストのバイナリの時間を足して 77 秒だった。

テストが違いを見つけることは、手で一度確かめた。TypeScript の生成器の `_monthLen` で 11 月を 31 日にすると、`closing_and_payment_days_as_inputs.ja.cal` で 4,637 行、`helpers_月の日と締め.cal` で 66,531 行が違うと言って落ちた。

どれも、範囲のすべての入力で一致することを確かめるテストであって、一致の証明ではない(3.4)。

## 7. 人が読むページ(doc)

**決定**:`koyomi doc` は、コードが実現すべきものを理解し、確かめる人(経理、法務、会社のカレンダーを決める人、コードをレビューする開発者)が読むページを、Markdown か、一枚の HTML で出す。書くのは `.cal` と検査の結果から言えることだけで、どの文も `.cal` の行か検査の結果に遡れる(rulec の §1.6)。操作を言い直す文は、7.1 の表(`src/paraphrase.rs`)からだけ作る。

ページは一度だけ、見出し・段落・表・引用・月の表のブロックの並び(`src/doc/mod.rs` の `Page`)として組み、Markdown(`src/doc/markdown.rs`)と HTML(`src/doc/html.rs`)は、それをそれぞれの書き方で出すだけにした。二つの形のあいだで中身が食い違わないようにするためである。

カレンダーのファイルの HTML のページには、もう一つ節がある(2026-11-05)。日付を一つ入れると、その日が休みか、なぜかを、ページの中で答える。カレンダーの行を JSON にしてページに埋め、`src/doc/try_it.js` の `tryCalendar` が `Calendar::is_open` と同じ順に読む。これがこのページの持つただ一つのスクリプトで、読み込むものは今も無い。スクリプトが動かなければ節は隠れたままで、残りのページは同じに読める。Markdown には出さない。`tests/doc.rs` が、九つのカレンダーと祝日のモジュールの全部の日について、ページのスクリプトの答えを Node で走らせ、koyomi の答えと比べる。

dates のファイルのページは、次の順に並ぶ。

1. 見出し:名前、バージョン(`v1`)、description。もとにしたファイル(`.cal`、カレンダー、表、法令の条)とハッシュ、koyomi のバージョン。ハッシュが今のファイルと違えばページは古い、と一文で添える。
2. 判定:通ったファイルなら `koyomi check` が出す一行(「3 つの条件が、受領日 2026-01-01〜2027-11-20 の 689 日のすべてで成り立ちます」)。成り立たない条件や違う例があれば、その診断の見出しを並べる。
3. 計算のしかた:日付ごとに、操作を一行ずつ普通の言葉で言い直し、後ろに `.cal` の行をコメントごと置く。行末のコメントは書いた人が `.cal` に残したもので、契約や条文のどの言葉にあたるかを書いておけば、そのままページに出る(10 章)。`roll` と `if closed` の行には、範囲の中で休みに当たって効いた入力の数と、`roll` ならいちばん大きく動いた日数を添える。法令を引いた行には、コピーから引いた条文と、何年何月何日時点のどの版かを添える。
4. 確かめたこと:入力の範囲と組み合わせの数、条件ごとの結果。比較の条件は、成り立てば余裕がいちばん少ない入力を、成り立たなければいちばん外れる入力を書く。成り立たない条件ごとに、成り立たない入力の全部(整数の入力があれば、その値ごと)と、診断と同じ最初の入力の計算を載せる。
5. 無い日の扱い:`else` ごとに、範囲の中で使われた入力の数と最初の例、ほかの二つの扱いに替えたら日付が変わる入力の数(`reject` なら計算が止まる入力の数)。無い日に当たりうる操作が無ければ、そう書く。
6. エッジケース(7.3 の選び方)。
7. 例:`examples` に書いた値と、計算した値と合っているか。
8. カレンダー:読んでいるカレンダーと description、休みの決まり(ファイルごとに `.cal` の行つき)、出典とデータの範囲、入力と計算した日付が入る範囲でいちばん長い連休、月の表。

calendar のファイルのページは、見出し、休みの決まり、出典とデータの範囲、営業日の数(表の月を含む年のうち、カレンダーが一年まるごと知っている年)、表の月でいちばん長い連休、月の表である。東京の営業日の 2026 年は 240 日が営業日で、2026-01〜2027-12 でいちばん長い連休は 2026-12-29〜2027-01-03 の 6 日になる(`tests/doc.rs`)。

D の段階の実際の出力から、`koyomi doc examples/eom_close_two_months_later.ja.cal --lang ja` の一部を引く。

```markdown
> [!WARNING]
> 条件「受領から60日以内」が、受領日 669 日のうち 648 日で成り立ちません。
>
> 成り立たない入力の日は、下の月の表で太字にしてあります。
…
1. 2 か月後の月末 `end of month +2  # 「翌々月末払い」`
2. 休みなら前営業日 `roll preceding  # 「支払日が休みなら前の営業日」`

   受領日 669 日のうち 247 日で休みに当たり、日付が動きます(いちばん大きく動くのは 3 日)。
…
| 2026-10-01(木) | 2026-10-31(土) | 2026-12-28(月) | 支払日が年末年始に当たり、前営業日に動く。支払日がいちばん大きく動く(3 日) |
…
**2026 年 5 月**

| 月 | 火 | 水 | 木 | 金 | 土 | 日 |
|---:|---:|---:|---:|---:|---:|---:|
|  |  |  |  | **1**◆ | **(2)** | **(3)** |
| **(4)** | **(5)** | **(6)** | **7** | **8** | **(9)** | **(10)** |
```

### 7.1 言い直しの表

| 操作 | 日本語 | 英語 |
|---|---|---|
| `close day 20` | 20 日締め:前の月の 21 日からその月の 20 日までの期間の、締め日 | the closing day of its period, closing on the 20th (the 21st to the 20th) |
| `close end of month` | 月末締め:その日を含む月の末日 | the end of its month |
| `day 10 of month +1` | 翌月 10 日(`+2` は「2 か月後の 10 日」、`-1` は「前月 10 日」、`+0` は「同じ月の 10 日」) | the 10th of the next month |
| `end of month +2` | 2 か月後の月末 | the end of the month two months later |
| `start of month +1` | 翌月 1 日 | the first of the next month |
| `+ 30 days`、`- 1 day` | 30 日後、前日 | 30 days later, the day before |
| `+ 5 business days` | 5 営業日後 | 5 business days later |
| `+ 1 month else end_of_month` | 1 か月後の同じ日。その日が無ければ、その月の末日 | the same day a month later; the end of that month when it has no such day |
| `+ 1 month else start_of_next_month` | 1 か月後の同じ日。その日が無ければ、その次の月の 1 日 | …; the first of the following month when it has no such day |
| `+ 1 month else reject` | 1 か月後の同じ日(範囲のどの日でも、その日があることを確かめた) | the same day a month later (it exists for every day of the range, as checked) |
| `roll following` | 休みなら翌営業日 | if it is closed, the next business day |
| `roll preceding` | 休みなら前営業日 | if it is closed, the business day before |
| `roll modified following` | 休みなら翌営業日。月が変わるなら前営業日 | if it is closed, the next business day, unless that is in the next month; then the business day before |
| `roll modified preceding` | 休みなら前営業日。月が変わるなら翌営業日 | the mirror of the above |
| `if closed + 1 day` | 休みなら翌日(翌日が休みでも、それ以上は動かさない) | if it is closed, the day after (once, whatever that day is) |
| `at 09:00` | 時刻はその日の 09:00(+09:00) | at 09:00 (+09:00) on that day |

言い直しの文は、この表(B の段階で `src/paraphrase.rs`)からだけ作る。表に無い文をページに書かない。

### 7.2 月の表

**決定**:月の表は月曜から始まる七列にする。Markdown では、休みの日を( )で囲み、条件が成り立たない入力の日を太字にし、エッジケースの入力の日に ◆ を、カレンダーが知らない日に ? を付ける。表の下に、名前のある休み(祝日、毎年の休み、特定の日)と、`open` で営業する日を並べる。HTML では、休みの日に色を付けて祝日の名前をその日のところに書き、成り立たない入力の日を枠と色で目立たせる。日付にマウスを載せると、休みの理由、成り立たない条件(整数の入力の値つき)、エッジケースに選んだ理由が出る(`title`)。

出す月は、dates のファイルなら、入力の範囲と計算した日付が入る月の全部である。24 か月を超えれば、成り立たない入力のある月と、エッジケースの入力と計算した日付のある月だけにし、省いた月の数を書く。`net30.cal` は 2026-01〜2028-12 の 36 か月のうち 8 か月を載せる。エッジケースの計算した日付の月も入れたのは、支払日が Good Friday から動いた例で、その Good Friday の月が表に無いと、なぜ動いたかが表から読めないからである。calendar のファイルなら、データの範囲の最後の 24 か月にする。表を読まないカレンダーはどの日のことも知っているので月を選びようがなく、`--months` を書いたときだけ月の表を出す。どちらも `--months 2026-04..2027-03` で替えられる。今日の日付は使わない。走らせた日によってページが変わらないようにするためである。

**捨てたもの**:

- 日曜から始まる月の表。日本の壁掛けのカレンダーには日曜始まりが多いが、土日を並べて置くと休みの続き方が見やすく、ISO 8601 と koyomi の曜日の番号(月曜を 0)にもそろう。
- Markdown の月の表で、日付の隣に祝日の名前を書くこと。列が広がって表が読めなくなるので、表の下に並べた。
- 月の表に、計算した日付(支払日)にも印を付けること。締めや翌月の N 日のように多くの入力が同じ日に集まる決まりなら読めるが、Net 30 のように入力ごとに違う日になる決まりでは、ほとんどの営業日に印が付いて何も読めない。

### 7.3 エッジケースの選び方

**決定**:ページを作るときに範囲のすべての入力をもう一度計算し(`src/doc/edges.rs`)、次に当たる入力を、検査が計算する順で最初のものを選ぶ。

- 条件ごとに、成り立たない最初の入力か、成り立つ中でいちばん余裕の無い入力(比較の条件で、`=` でないもの)。
- `else` ごとに、無い日に当たって使われた最初の入力。`if closed` ごとに、休みに当たって効いた最初の入力。
- ほかの日付の始まりにならない日付(結果)ごとに、入力から結果までの日数がいちばん多い入力と少ない入力。同じ入力で同じ日数になる日付はまとめて言う。日数がいつも同じ日付(`+ 30 days` だけの日付など)は選ばない。
- `roll` ごとに、日付を動かした休みの理由(曜日、祝日の名前、毎年の休み、特定の日)ごとの最初の入力(六つまで)と、いちばん大きく動いた入力(2 日以上動くときだけ)。
- 入力の日が、休みの日、毎年の休みの日、祝日の前日と翌日(入力の日が営業日のとき)、月末(31 日、30 日、うるう年でない年の 2 月 28 日、2 月 29 日)である最初の入力。

同じ入力は一行にまとめる。16 行を超えるときは、上に挙げた順に残し(条件、`else` と `if closed`、日数、`roll`、入力の日の順)、残った行を検査が計算する順に並べる。どの行にも、何に当たったかを書く。

**理由**:読む人が見たいのは、決まりが効く日である。月末や祝日の前後は、人が手で確かめるときにまず試す日で、koyomi は範囲を全部計算しているので、その中から最初のものを選べば足りる。日数のいちばん多い入力と少ない入力を結果の日付に限ったのは、締め日のような途中の日付まで並べると、同じ入力が同じ理由で何度も出て表が読めなくなったからである(A の段階の計画では日付ごとにしていた)。

**ほかの扱いに替えたときの数**:`else` のある操作ごとに、範囲のすべての入力で、その操作だけをほかの扱いに替えて日付を計算し直し、元の日付と比べる。無い日に当たった入力だけを計算し直せば足りるように見えるが、`close day` は違う。`close day 31 else start_of_next_month` で 2026-05-01 を締めると、4 月の締め日が 5 月 1 日に送られるのでその日に締まり(無い日に当たる)、`else end_of_month` なら 4 月の締め日は 4 月 30 日で、5 月 1 日は 5 月 31 日に締まる(無い日に当たらない)。扱いを替えると、無い日に当たるかどうか自体が変わる。だから全部の入力を計算し直す。`closing_and_payment_days_as_inputs.ja.cal` の 871,596 通りでも、ページ全体が 0.3 秒ほどで出る(release のビルド)。

### 7.4 検査を通らないファイル

構文、名前、出典、無い日の扱い、日付の計算、予算(E0xx、E1xx、E201〜E204、E305)でエラーがあれば、ページを作らない。壊れた決まりを整ったページにすれば、読む人を誤らせる。条件と例が成り立たないこと(E301〜E303)は決まりについての事実なので、ページを作り、成り立たない日に印を付ける。ここが rulec と違う。rulec は検査を通らない規則のページを作らないが、koyomi の条件は「決まりが満たすべきこと」で、満たさない日を読む人に見せることが、このページのいちばんの役目だからである。

**決定**:ページを出せば、条件が成り立たなくても exit code は 0 にする。ページを作れないときは 1 で、診断は標準エラーに出す。標準出力はページだけにしたので、`koyomi doc x.cal > x.md` で診断がページのファイルに混ざることはない。成り立たないことを CI で止めたいなら、`koyomi check` がその役をする。

### 7.5 HTML

**決定**:HTML は一枚で、script も、外のファイル(フォント、画像、スタイルシート)も読まない。メールで送っても、印刷しても、そのまま読める。明るい配色と暗い配色を CSS の変数で持ち、`prefers-color-scheme` に従う。`<html data-theme="light|dark">` と書けば、どちらかに決められる(rulec のページと同じ)。計算の段は、Markdown では `koyomi check` と同じ字下げのテキストにし、HTML では表にした。等幅のフォントでも、日本語の幅は二文字分にそろわないことが多いからである。

`tests/doc.rs` は、Chrome(`KOYOMI_CHROME`、無ければ macOS の Google Chrome、それも無ければ PATH の `google-chrome` か `chromium`)で、わざと破る二つの例のページのスクリーンショットを撮る。ページの先頭は明るい配色で、月の表は暗い配色で撮る。ヘッドレスの Chrome は OS の外観に従い、`--force-dark-mode` でも `--blink-settings` でも `prefers-color-scheme` が替わらなかったので、ページをコピーし、その `<html>` に `data-theme` を書いてから撮る。スクリーンショットは `KOYOMI_BLESS=1` のときに `docs/images/` に置き、README が載せる。

**捨てたもの**:

- HTML に script を入れて、入力を変えて計算を試せるようにすること(rulec のページの「試してみる」)。生成した TypeScript を埋め込めばできるが、一つの入力を試すなら `koyomi eval` がある。何も読まない一枚のほうが、読む人の手元で確実に開ける。
- 法令の条文を、ページの最後にまとめて一度だけ載せること。読む人は日付ごとに読むので、引いた場所ごとに載せる(142 条のように二度載ることがある)。条文は、項ごとに一行にし、項の番号と文とただし書を同じ行に置いて、法令の書き方に近づけた(`sources::article_lines`)。`source outdated` が比べる `xml_text` は文ごとに一行のままである。

## 8. api

**決定**:`koyomi api <file.cal>` は、生成したコードを読まずに呼べるだけのことを JSON で出す。検査を通らないファイルには出さない(exit 1)。`gen` が生成しないファイルの関数を、`api` だけが言うことになるからである。形は rulec の `api` にそろえ、出力先ごとの項を持つ。dandori が rulec の `api` を読むのと同じ読み方(dandori の `src/rulec.rs`。出力先の項の `params` から順序と別名を取る)で、将来 dandori が koyomi を読めるようにする。dandori は今回変えない。

```json
{
  "koyomi": "0.1.0",
  "kind": "dates",
  "name": "支払条件",
  "alias": "payment_terms",
  "version": "1",
  "source_sha256": "75482b2e796019d0…",
  "description": "20 日締め翌月 10 日払い。…",
  "calendar": {
    "name": "東京の営業日",
    "alias": "tokyo",
    "version": "1",
    "file": "calendars/東京の営業日.cal",
    "source_sha256": "d7b6134e23a8cb9f…",
    "offset": "+09:00",
    "uses": [],
    "closed": [{"weekly": ["sat", "sun"]}, {"table": "祝日"}, {"every": "12-29..01-03", "name": "年末年始"}],
    "open": [],
    "sources": [
      {
        "name": "祝日",
        "kind": "file",
        "file": "calendars/data/syukujitsu.csv",
        "url": "https://www8.cao.go.jp/chosei/shukujitsu/syukujitsu.csv",
        "sha256": "cec37a743c96995c",
        "format": "csv",
        "encoding": "shift_jis",
        "covers": {"from": "1955-01-01", "to": "2027-12-31"},
        "rows": 1067
      }
    ],
    "data_range": {"from": "1955-01-01", "to": "2027-12-31"}
  },
  "sources": [],
  "inputs": [{"name": "受領日", "alias": "received", "type": "date", "range": {"min": "2026-01-01", "max": "2027-11-20"}}],
  "dates": [
    {
      "name": "締め日",
      "alias": "closing",
      "params": ["受領日"]
    },
    {
      "name": "支払日",
      "alias": "payment",
      "params": ["受領日"],
      "at": {"time": "09:00", "offset": "+09:00"}
    }
  ],
  "claims": [
    {"name": "営業日に払う", "text": "支払日 is open"},
    {"name": "受領から60日以内", "text": "支払日 <= 受領日 + 60 days"},
    {"name": "遅い受領は遅い支払", "text": "支払日 is monotonic"}
  ],
  "wire": {
    "date": "YYYY-MM-DD",
    "int": "a JSON number",
    "at": "RFC 3339 in UTC, ending in Z, as dandori's timestamp",
    "errors": ["range", "data", "reject", "date"]
  },
  "typescript": {
    "file": "typescript/payment_terms.ts",
    "module": "payment_terms",
    "date_type": "string",
    "functions": [
      {
        "name": "支払日",
        "function": "payment",
        "signature": "export function payment(received: string): string",
        "at_function": "payment_at",
        "at_signature": "export function payment_at(received: string): string",
        "params": [{"name": "受領日", "alias": "received", "type": "string"}]
      }
    ],
    "is_open": "export function is_open(day: string): boolean",
    "errors": ["KoyomiError"]
  },
  "python": {"…": "typescript と同じ形"},
  "go": {"…": "typescript と同じ形"},
  "rust": {"…": "typescript と同じ形"},
  "sql": {"…": "typescript と同じ形"}
}
```

上は B の段階の実際の出力を、ハッシュと、TypeScript 以外の出力先と、締め日の関数を省いて載せたもの(日本語の版。英語の版 `payment_20th_close_next_10th.cal` の api は、名前が英語で、別名が `payment_20th_close_next_10th` になるほかは同じ形)。出力先の項の名前と署名は `src/naming.rs` が決め、C の段階の生成器も同じ関数を使う(署名が生成物の中にそのままあることを `tests/api.rs` が確かめる。6.1)。`is_open` の引数は `day` にした(Python の `date` 型の名前とぶつからないように)。`wire.errors` は、生成したコードのエラーの種類(入力が範囲の外、データの範囲の外、無い日、0001〜9999 の外)。

法令の出典は、dates のファイルの `sources` に `{"name": "民法", "kind": "law", "db": "egov", "id": "129AC0000000089", "asof": "2026-10-01", "revision": "129AC0000000089_20260624_508AC0000000045", "pins": [{"fragment": "第140条", "sha256": "e880059021fbb67d"}, …]}` の形で出る(rulec の `api` の `sources` と同じ形に、コピーの版を足したもの)。

calendar のファイルの `api` は、`calendar` の項と出力先ごとの `is_open` だけを出す。

Wasm の項は形が違う。rulec の `api` の Wasm の項と同じ鍵(`build`、`wit`、`package`、`world`、`call`、`call_signature`、`post_return`、`realloc`、`memory`、`runner`、`component`)と、ほかの出力先の項の鍵のうち意味を持つもの(`file`、`module`、`date_type`)を持つ。関数の署名の代わりに、日付ごとに `out` の鍵(`key`、時刻があれば `at_key`)と引数を書き、カレンダーを読むファイルなら `is_open` に問い方(`{"in": "date", "out": "open"}`)を、`answers` に答えの二つの形を書く。`errors` は持たない(13.2)。

将来 dandori がこれを読むなら、dandori の Temporal のワークフローの中で、koyomi が生成した関数を(純関数なので)アクティビティにせずに呼び、`wait until` の時刻を決められる。その形は dandori の側で決めることで、今回は `api` に必要なこと(関数、引数の型と範囲、`at` の関数と時刻の形)を出しておくところまでにする。

## 9. 出典のコマンド

**決定**:rulec と同じく三つ。通信するのは `fetch` と `outdated` の二つだけで、`curl` を子プロセスで呼ぶ(HTTPS を自前で書かず、依存も足さない)。HTTP と HTTPS の URL は三度まで試し(二度めの前に 2 秒、三度めの前に 4 秒待つ)、`file://` の URL は一度だけ読む。どのコマンドも `.cal` を構文の段まで読み、検査は通さない。コピーが無い(E101)、固定が無い(E102)、固定と違う(E103)は、どれもこのコマンドで直すものだからである。

- `koyomi source fetch <file.cal>`:`url` のある表を取り、`file` に書く。配られているバイト列をそのまま書き、文字コードを変えない(1.5)。書いたあと、固定と合うかを言う。法令の出典は、固定している条と引いている条ごとに、e-Gov 法令 API v2 の `law_data/<法令ID>?asof=<日付>&elm=<要素>&law_full_text_format=xml` を引き、返ってきた XML(`law_full_text` の base64 を戻したもの)をコピーのファイルに、`revision_info.law_revision_id` を `revision.txt` に書く。本文(タグを落としたもの)が前のコピーと同じなら、コピーを書き換えない(rulec の §15.71 と同じ。e-Gov は改正の無い条の属性や改行を書き換えることがある)。
- `koyomi source pin <file.cal>`:コピーの SHA-256 の先頭 16 桁を、`source` の行の `sha256:`(法令なら条の固定の行)に書く。変えるのはその 16 桁だけで、ほかの文字は、行の終わりの CR LF も含めて一字も変えない。固定の無い `source` の行には、コメントの前に ` sha256:…` を足す。引いているのに固定の行が無い条には、その出典の最後の固定の行のあとに一行を足す。表なら、行の数と `covers` も言う。

```
$ koyomi source pin examples/calendars/england_and_wales.cal
bank_holidays: already pinned at sha256:538b3482c28b85ec; the table has 83 rows, covers listed years = 2019-01-01..2028-12-31
$ koyomi source pin examples/civil_code_period_end.cal
civil_code: all 4 articles already pinned
$ koyomi source pin examples/calendars/東京の営業日.cal
祝日: already pinned at sha256:cec37a743c96995c; the table has 1,067 rows, covers 1955-01-01..2027-12-31
$ koyomi source pin examples/civil_code_period_end.ja.cal --lang ja
民法: 4 条とも固定済みです
```

- `koyomi source outdated <file.cal>`:表は、`url` から取ったもののハッシュを固定と比べる。同じなら「変わっていません」で exit 0。違えば、コピーと取ったものの両方を読み、増えた日、消えた日、名前の変わった日をそれぞれ六件まで並べて全部の数を言い、`covers` の書き換え案(`covers listed years` なら範囲の変わり方)を言って、exit 1。法令は、`law_revisions/<法令ID>` で `asof` より後に施行される版を引き、その施行日を `asof` にして条を取り、本文を比べる。比べる相手は一つ前の施行日の本文(最初はコピー)なので、改正は施行の日に一度だけ言う。同じ日に施行される版が二つ以上あっても、その日の本文は一つなので一度だけ問う。変わる条があれば、施行日と版と、変わる文を `-` と `+` で並べて exit 1。

`tests/fetch.rs` は、内閣府の表のコピーから 2027 年の 17 行を除いた表を固定し、元のコピーを指す `file://` の URL を `url` に書いた `.cal` で `outdated` を走らせる。URL が変わったと言う一行め(テストの場所のパスを含むので省く)のあとは、次のとおり(実際の出力。exit 1):

```
  2027 年の 17 日が増えます: 2027-01-01 元日、2027-01-11 成人の日、2027-02-11 建国記念の日、2027-02-23 天皇誕生日、2027-03-21 春分の日、2027-03-22 休日、ほか 11 日
  表が延びるので、covers も延ばします: `covers 1955-01-01..2027-12-31`(いまは 1955-01-01..2026-12-31)
  読んでから koyomi source fetch で取り直し、koyomi source pin で固定します
```

本物の内閣府、GOV.UK、e-Gov には、C の段階で一度だけ問い合わせた(`KOYOMI_NET=1 cargo test --test fetch the_real_sources_when_asked`)。2026-10-01 より後に施行される五つの版のどれでも、140〜143 条の本文は変わらなかった(`outdated` は本文を比べる。1.5 に書いた、XML が一バイトも変わらないという事実は A の段階で確かめたもの)。

```
# 2026-10-03 01:49(日本時間)に走らせた実際の出力。通信する
$ koyomi source outdated examples/calendars/東京の営業日.cal
祝日: unchanged (https://www8.cao.go.jp/chosei/shukujitsu/syukujitsu.csv is sha256:cec37a743c96995c, as pinned)
$ koyomi source outdated examples/calendars/england_and_wales.cal
bank_holidays: unchanged (https://www.gov.uk/bank-holidays.json is sha256:538b3482c28b85ec, as pinned)
$ koyomi source outdated examples/civil_code_period_end.ja.cal
民法: the revision in force from 2027-06-23 (129AC0000000089_20270623_508AC0000000045) leaves the cited articles as they are
民法: the revision in force from 2027-12-05 (129AC0000000089_20271205_507AC0000000057) leaves the cited articles as they are
民法: the revision in force from 2028-06-13 (129AC0000000089_20280613_505AC0000000053) leaves the cited articles as they are
民法: the revision in force from 2028-12-23 (129AC0000000089_20281223_508AC0000000045) leaves the cited articles as they are
民法: the revision in force from 2029-06-23 (129AC0000000089_20290623_508AC0000000045) leaves the cited articles as they are
```

exit code は rulec と同じにした。`fetch` と `pin` は済めば 0、`outdated` は元が変わっていれば 1、`.cal` が構文の段で読めなければ 1、引数の誤り・読めないファイル・`curl` の失敗は 2 である。

`check` は通信しないので、改正を知る手段は `outdated` だけである。CI の定期実行に置く(rulec の §15.68 と同じ)。`check`・`eval`・`api` が通信しないことは、テストが PATH を空にして走らせて確かめる。

テストは通信しない。表の `url` は `file://` にする(curl は `file://` を読める)。e-Gov の API の場所は環境変数 `KOYOMI_EGOV` で替えられるようにし、テストは、用意したレスポンスを返す小さな HTTP サーバーをテストの中に立てて(`127.0.0.1` の空いたポート。テストの終わりに止める)、その場所を渡す。用意するのは民法 140〜143 条(例のコピーを base64 にしたもの)と後の版が五つある `law_revisions` で、ある施行日から 143 条の本文を一文字変えたレスポンスや、属性だけを変えたレスポンスも返せる。本物の内閣府、GOV.UK、e-Gov に取りに行くテストは、環境変数 `KOYOMI_NET=1` のときだけ走らせる。走らせないときは `SKIP:` ではなく `not asked:` の行を出す。ツールが欠けているのではなく、通信しないことを既定にしているからである。

## 10. 例

`examples/` に置いた。どれも公開されている出典(GOV.UK の bank holidays、内閣府の祝日の表、e-Gov の民法)だけを使う。英語の例を先に置く。England and Wales のカレンダーで書いた英語の例と、日本の暦で書いた例の英語の版と日本語の版である。日本の暦の例の数は A の段階の試作で、England and Wales の例の数は英語を先にした段階の試作(Python の `datetime` と GOV.UK の表で書いた使い捨てのもの)で計算し、参照インタプリタが同じ数を出すことを `tests/check.rs` が確かめている(どちらも一つも食い違わなかった)。

カレンダー(`examples/calendars/`):

- `england_and_wales.cal`:土日と GOV.UK の表(`england-and-wales`、`covers listed years`、2019〜2028 年)。オフセットは書かない(夏時間があるため。1.9)。2026 年の営業日は 253 日。
- `tokyo_business_days.cal` と `東京の営業日.cal`:土日、内閣府の表、年末年始(12-29..01-03)。データの範囲 1955-01-01〜2027-12-31。2026 年の営業日は 240 日。前者が英語の版、後者が日本語の版で、同じ表を読み、どの日も同じ結果になる。
- `civil_code_142_days.cal` と `民法142条の休日.cal`:142 条が挙げる日(日曜日と、国民の祝日に関する法律に規定する休日)だけを休みにしたカレンダー。「その他の休日」に何が入るかは、この例では決めない(description にそう書く)。前者が英語の版。
- `data/bank-holidays.json`、`data/syukujitsu.csv`:配られているバイト列のままのコピー。

法令のコピー:`examples/sources/law/129AC0000000089@2026-10-01/`(民法 140〜143 条の XML と `revision.txt`。1.5)。

England and Wales の例(英語):

| ファイル | 書いてあること | 検査の結果 |
|---|---|---|
| `close_20th_pay_10th.cal` | 20 日締め、翌月 10 日、休みなら前営業日。条件は営業日・受領から 60 日以内・単調 | 受領日 2026-01-01〜2028-11-20 の 1,055 日すべてで成り立つ。いちばん長いのは 51 日(受領日 2026-07-21、2026-12-21、2027-07-21、2027-12-21)、いちばん短いのは 18 日。休みなら前営業日で動く支払日は 9 日 |
| `close_eom_pay_two_months_on.cal` | わざと破る例。月末締め、2 か月後の月末、休みなら前営業日。条件は受領から 60 日以内 | 受領日 2026-01-01〜2028-10-31 の 1,035 日のうち 1,008 日で成り立たない(15 か所)。いちばん外れるのは受領日 2026-05-01 の 91 日。2027-05-31(月、Spring bank holiday)の支払は 2027-05-28(金)に動く |
| `net30.cal` | 請求日の 30 日後、休みなら翌営業日。条件は営業日・30 日以上後・単調 | 請求日 2026-01-01〜2028-11-29 の 1,064 日すべてで成り立つ。いちばん長いのは 34 日で、30 日後が Good Friday かクリスマスの前後に当たる請求日(六日)である |
| `close_and_pay_on_given_days.cal` | 締め日(1〜31)、支払の月(1〜2)、支払の日(10〜31)を整数で受け取る。条件は営業日・締めのあとに払う・単調 | 受領日 2027-01-01〜2028-10-01 と三つの整数の 872,960 通りのすべてで成り立つ(受領日は 2027 年からにした。日本の暦の版と同じくらいの数にして、生成したコードと突き合わせる時間を抑えるため) |
| `period_of_months.cal` | 月で数える期間。初日を数えず、最後の月の応当する日の前日に満了し(無ければ月末)、休みに当たれば翌日に動かす。この例の読み方として書いたもので、法令の読み方ではない | 起点 2026 年の 4,380 通りのすべてで成り立つ。結果は 2027-12-31 までで、データの範囲に収まる |
| `period_of_months_two_readings.cal` | わざと破る例。休みに当たった満了日を動かす先の二つの読み方(`if closed + 1 day` と `roll following`)が等しいという条件と、応当する日の前日と月数を足して月末に寄せる書き方が等しいという条件 | 前者は 4,380 通りのうち 709 通り(土曜に当たる満了日と、Good Friday から Easter Monday まで)、後者は 39 通りで成り立たない |

`net30.cal` の請求日の範囲の終わり 2028-11-29 も、表(2028 年まで)から決まる。2028-11-30 の 30 日後は 2028-12-30(土)で、翌営業日を探して 2029-01-01 を問うと表の外になる。同じ例に `roll modified following` の日付を足すと、following と 21 日で結果が分かれる(月末をまたぐ日)。

日本の暦の例(英語の版と日本語の版):

| 英語の版 / 日本語の版 | 書いてあること | 検査の結果 |
|---|---|---|
| `payment_20th_close_next_10th.cal` / `.ja.cal` | 1.1 の例。20 日締め、翌月 10 日、休みなら前営業日、09:00。条件は営業日・受領から 60 日以内・単調 | 受領日 2026-01-01〜2027-11-20 の 689 日すべてで成り立つ。いちばん長いのは 51 日 |
| `eom_close_two_months_later.cal` / `.ja.cal` | わざと破る例。月末締め、2 か月後の月末、休みなら前営業日。条件は受領から 60 日以内 | 受領日 2026-01-01〜2027-10-31 の 669 日のうち 648 日で成り立たない(4.3)。いちばん外れるのは受領日 2026-05-01 の 91 日。2026-12-31(木)の支払は年末年始で 2026-12-28(月)に動く |
| `civil_code_period_end.cal` / `.ja.cal` | 140 条・143 条を文字どおりに書き(1.7)、142 条を文字どおり「休みならその翌日」(`if closed + 1 day`)で書く。どの日付も条文を引く(英語の版は `@civil_code 第140条`、日本語の版は `@民法 第140条`。1.5 の法令の出典)。入力は起点と月数(1〜12)。条件は単調、満了日が起点より後 | 起点 2026 年の 4,380 通りのすべてで成り立つ。結果は 2027-12-31 までで、データの範囲に収まる |
| `civil_code_two_readings.cal` / `.ja.cal` | わざと破る例。142 条の二つの読み方(`if closed + 1 day` と `roll following`)が等しいという条件と、143 条と月数を足して月末に寄せる書き方が等しいという条件 | 前者は 4,380 通りのうち 121 通り、後者は 39 通りで成り立たない(1.7、1.8) |
| `closing_and_payment_days_as_inputs.cal` / `.ja.cal` | 締め日(1〜31)、支払の月(1〜2)、支払の日(10〜31)を整数で受け取る。締め日を規則から受け取る形。条件は営業日・締めのあとに払う・単調 | 受領日 2026-01-01〜2027-10-01 と三つの整数の 871,596 通りのすべてで成り立つ |

英語の版と日本語の版は、名前のほかは同じ決まりで、同じ結果になる。vectors は、名前を読み替えれば一行も違わない(`tests/vectors.rs`)。名前が計算に効かないことのテストにもなる。

「受領から60日以内」は、例の条件として文字どおりに書いたもので、法令の読み方を示すものではない。期間の例も、決まりや条文を文字どおりに書いたもので、読み方を一つに決めるものではない。二つの読み方を並べた例は、読み方が分かれる日を見せるためにある。どの例の description にも、そう書く。

わざと破る例は六つ(England and Wales の二つ、日本の暦の二つの英語の版と日本語の版)あり、`koyomi check examples/` は exit 1 で終わる。README にそう書き、`tests/check.rs` がその六つだけが落ちることを確かめる。診断は README と人が読むページに載せ、`tests/docs.rs` と `tests/doc.rs` が実際の出力と同じかを確かめる。

**決定**(D の段階):操作の行末に、その行が何にあたるかをコメントで書いた。支払の例は、支払条件の言葉のどれにあたるか(`# "closes on the 20th"`、`# 「20 日締め」`)。民法の例は、条文の言葉か、二つの読み方のどちらにあたるか(`# the first reading: the day after is the very next day`、`# 読み方の一つめ: 「その翌日」を、文字どおり翌日とする`)で、どちらの読み方が正しいかは書かない。コメントは、人が読むページにその行と一緒に出る(7 章)。

### 10.1 英語の版と日本語の版の名前

**決定**(英語を先にした段階):例、テストの材料、文書は英語を先にする。日本語のものは消さず、中身も変えず、日本語の版として残す。英語のものは足して先に見せる。英語の版と日本語の版の対は、名前で分かるようにする。決まりは次の三つで、chobo と同じにした。

- **例(`examples/`)**:dandori と同じ形にする。英語の版を `<英語の名前>.cal`、日本語の版を `<英語の名前>.ja.cal` とする。もとからあった日本語の例は、中身を一字も変えずに `<英語の名前>.ja.cal` へ名前だけを替え(`git mv`)、英語に訳した版を足した。英語の名前は、日本語の版の別名と違うものにする(`payment_20th_close_next_10th` と `payment_terms`)。テストは全部の例を一つのディレクトリに生成するので、別名が同じだと生成物がぶつかる。
- **カレンダー(`examples/calendars/`)**:`use calendar` で読まれるので、日本語の版は名前を替えない(`東京の営業日.cal`)。替えれば、それを読む日本語の版の `use calendar` の行を書き換えることになる。dandori の日本語の規則(`宿泊の与信額.rule`)も名前を替えていない。英語の版は英語の名前で足し(`tokyo_business_days.cal`)、description の最後に「The English version of 東京の営業日.cal」と書いて対を示す。
- **テストの材料、変異、golden(`tests/`)**:日本語の名前のものは、名前も中身もそのまま残す。日本語の名前で日本語の版だと分かるからである。同じ振る舞いを確かめる英語のものを、英語の名前で足す。変異の対は、診断のコードを頭に付けた名前でそろえる(`E001_閉じていない文字列.cal` と `E001_unclosed_string.cal`)。どちらも同じ診断のコードと終了コードを出すことを、`tests/mutants.rs` の `every_japanese_mutant_has_an_english_one` が確かめる。テストの材料は、英語の名前の版を足す(`helpers_月を足す.cal` と `helpers_add_months.cal`、`calendars/土日.cal` と `calendars/weekends.cal`)。名前が英語で中身が日本語のデータ(`data/holidays.csv`)には、`.en` を挟んだ英語の版を足す(`data/holidays.en.csv`)。例の golden は、例の名前に合わせて名前を替えた(中身は変えていない)。

- **`explain` の再現(台帳)**:台帳の項は、再現を英語と日本語の二つ持てる(ritsu-base の `Entry::english`)。`koyomi explain` は、英語の出力では英語の名前の再現を、日本語の出力では日本語の名前の再現を見せる。決めて変えたことで、日本語の出力は変えていない。英語の出力そのものである `docs/codes.md` の再現が英語の例になり、`docs/codes.ja.md` は変わらない。英語の再現を足したのは 14 項(E001、E009、E010、E101〜E106、E110、W101、E203、E301、E302)で、持たないのは、e-Gov の条を引く再現の E111 と W102 だけである(koyomi が固定できる法令は e-Gov のものだけで、英語の対が書けない)。この二つは、どちらの出力でも同じ(日本語の名前の)例を見せる。`tests/codes.rs` が、持っている再現の全部を走らせ、そのコードが出ることを確かめる。

日本語でしか確かめられない振る舞い(内閣府の Shift_JIS の CSV、e-Gov の条文、漢数字の条、全角の幅、数字から始まり日本語が続く名前)のテストは、日本語の材料のまま持つ。そのうえで、同じ振る舞いを英語の材料でも確かめるテストを足した。英語の名前の版でも e-Gov の条は `第143条` と書く(引けるのは e-Gov の法令だけで、条の名前は e-Gov のものだから)。Shift_JIS の読み方に当たる英語の材料は無いので、UTF-8 として読めない表(Windows-1252 で保存された表)で、読めないバイトの場所を言う振る舞いを確かめる。テストは英語と日本語の両方を回す。

**捨てたもの**:

- 英語の README に、日本語の名前の例を載せること。英語を読む人には名前が読めない。
- 日本語の例やテストの材料を、英語に訳して置き換えること。日本語の版が無くなる。日本語で読む人にも、同じものが要る。
- `tests/` の下の日本語の名前のファイルにも `.ja` を付けること。日本語の名前で日本語の版だと分かり、名前を替えると、そのファイルの名前を書いているテストや文書を全部直すことになる。
- 英語の例を日本の暦だけで書くこと。英語を読む人の多くにとって身近な暦(GOV.UK の bank holidays)を先に見せる。米国の連邦の祝日は、そのまま読める公開の表(CSV か GOV.UK の形)が見つからず、手で書いた表は出典を固定できないので、まだ置かない。

## 11. 実装

- Rust(edition 2024、手元の stable 1.94.1 で通ること)。依存は serde_json だけ(GOV.UK の JSON、`api`、vectors、`--format json`)。`preserve_order` の機能を使い、`api` と `--format json` のキーをこの文書の順に出す。
- テストのビルドは `[profile.test] opt-level = 2` にした。1900〜2100 年のすべての日を月数や締め日ごとに何度も回すテスト(`tests/date.rs`)が、最適化なしでは遅すぎるため。ritsu に取り込んでからは、cargo がワークスペースの根のプロファイルしか読まないので、根の `Cargo.toml` の `[profile.test.package.koyomi]` に置いている。
- SHA-256 は ritsu-base のものを使う(依存を足さずに書いたもので、FIPS 180-4 の既知の値でテストしている)。
- Shift_JIS は自前で読む。変換表は WHATWG の `index-jis0208.txt` から作った `src/sjis_table.rs`(作る手順は `tools/sjis/`。表の頭に、元の索引の Identifier、日付、SHA-256 を書く)。テストは、python3 があれば、全符号を Python の `cp932` と比べる(無ければ SKIP)。
- 通信は `curl` を子プロセスで呼ぶ(`source fetch` と `source outdated` だけ。`src/fetch.rs` が、ritsu-base の `sources` の e-Gov への問い合わせを呼ぶ)。
- 生成器は `src/codegen/`(共通の部分と、出力先ごとに一つのファイル。Wasm の `wasm.rs` は、Rust のモジュールを `rust.rs` のものそのままで書く)。PLAN は `src/gen/` としていたが、`gen` は Rust 2024 の予約語なので、rulec と同じ `codegen` にした。
- 突き合わせのテストが使う型の検査器は、版を固定して `tools/` に置く。TypeScript は `tools/package.json` と `tools/package-lock.json`(`npm ci --prefix tools`。`tools/node_modules` は git に入れない)、mypy は `tools/requirements.txt`(`uv venv --python 3.13 tools/.venv` に入れる。venv は git に入れない。2026-10-06 から、`tools/requirements.in` の mypy 2.4.0 から `uv pip compile --generate-hashes` で作った、依存までバージョンとハッシュを固定したロックファイルにした。ritsu の脆弱性の検査が、入れるバージョンのとおりに読めるように。ritsu の DESIGN 3.6)。テストは環境変数 `KOYOMI_TSC` と `KOYOMI_MYPY` でほかの場所のものも使える。Wasm のテストは、rustc の wasm32-unknown-unknown の標準ライブラリ(`rustup target add wasm32-unknown-unknown`)、Node、wasm-tools、wasmtime を PATH から探す。PostgreSQL は `KOYOMI_PG_BIN`(`initdb`・`pg_ctl`・`psql` のあるディレクトリ)と `KOYOMI_PG_SOCKET_DIR`(ソケットのディレクトリ。パスは 103 バイトまで)で受け取る。
- 診断の文面は `tr!` で英語と日本語を隣に書く。二つの文の引数が違うときは `;` で分ける(`tr!("{}曜", "{}", ja; en)`)。`tr!` と、診断の共通の部分、台帳の書き出し、コマンドの表の読み方は ritsu-base のもので、台帳の中身(`src/codes.rs`)とコマンドの表(`src/cli.rs`)と計算の段(`src/diag.rs` の `Example`)が koyomi のものである(11.1)。
- 人が読むページは `src/doc/`。`mod.rs` がページを組み、`edges.rs` が範囲をもう一度計算してエッジケースと無い日の扱いの数を集め、`months.rs` が月の表を作り、`markdown.rs` と `html.rs` がそれぞれの形で出す(7 章)。
- 文書は `docs/`(英語の `reference.md` と `generated-code.md`、`koyomi explain --all --format markdown` の出力そのものの `codes.md` と `codes.ja.md`)、README.md、README.ja.md、`skills/koyomi`(`SKILL.md` は手で書き、ほかは `skills/sync.sh` が `docs/` からコピーする)。`tests/docs.rs` が、README と `docs/` とスキルに載せた `.cal` の行、コマンドの出力、診断、生成したコード、vectors の行、比べた行の数が、実際のものと同じかを確かめ、`tests/skill.rs` が、スキルにコピーしたページとリンクと frontmatter を確かめる。
- テストの共通の部分(自分を消す一時ディレクトリ、ツールの探し方、時間を区切って走らせること、golden、SKIP、使い捨ての PostgreSQL、Chrome、テストの中の HTTP サーバー)は ritsu-testkit のものを使う。Chrome は `RITSU_CHROME` か `KOYOMI_CHROME` で受け取り、無ければ macOS が Google Chrome を入れる場所、それも無ければ PATH の `google-chrome` か `chromium` を探す(dandori と chobo と同じ順)。ヘッドレスの Chrome はスクリーンショットを書き出したあとも終わらないことがあるので、ritsu-testkit がそのファイルの大きさが変わらなくなったところで止める。

モジュールの分け方と、各段階の作業は PLAN.md にある。

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

koyomi は ritsu(七つの言語を一つにまとめる処理系)に取り込まれ、ほかの言語と重なっていたコードを、ritsu の土台のクレート(ritsu-base と ritsu-testkit)のものに替えた(ritsu の PLAN の C.4)。替えたのは、SHA-256、二つの言語の文(`tr!`、`Text`、`Lang`)、診断の共通の部分、台帳の書き出しと再現の走らせ方、コマンドの表の読み方と `--help` の組み立て、法令のコピーの扱い(引用から要素の名前を作ること、コピーの場所と本文、固定の行の書き換え、e-Gov への問い合わせ)、人が読むページの HTML の頭と配色の変数、テストの共通の部分である。koyomi に残したのは、台帳の中身、コマンドの表の中身、計算の段(`Example`)、祝日の表、法令のコピーのうち koyomi が引くもの(本則の条・項・号だけ)、ページの中身と koyomi だけの色である。

出力は、次のものを除いて一字も変えていない(ritsu の土台で決めたこと)。

- 言語の選び方に `RITSU_LANG` が入った。`--lang`、`KOYOMI_LANG`、`RITSU_LANG`、英語の順に読む。ritsu のどの言語も同じ変数を読むので、一つの設定で全部の言語の文面を日本語にできる。`--lang` の説明と `koyomi --help` の最後の行が、この順を書くようになった。
- HTML のページの配色の変数のうち、ほかの言語と共通の役割(背景、文字、線、パネル、コードの背景、成り立つ・注意の色)は、ritsu-base の値(GitHub の配色。chobo と dandori の値)になった。koyomi だけの色(休み、成り立たない日、エッジケース、カレンダーが知らない日、法令の引用)は値を変えていない。変数の並びは一行にまとめて書く。README の画像(`docs/images/`)は取り直した。
- 行の無い診断(UTF-8 でないファイルの E001)の `--format json` は、`line` と `col` を `0` でなく `null` で書く。行が無いことを、行番号の 0 で表さないためである(ritsu の診断の共通の形)。
- `source fetch` と `source outdated` の curl に `--compressed` が付いた(eCFR は付けないと 406 を返す。ritsu-base で rulec が見つけたこと)。e-Gov が JSON でないページを返したときは、二度まで取り直してから止める。curl が無いときは、三度試さずにすぐそう言う。
- コピーの本文の取り出し(`source outdated` が比べて差分に出すもの)は、rulec と yuen の形になった。表の行と列も一行ずつ・空白で区切って読む。koyomi が引く本則の条には表が無いので、いまの例では変わらない。固定の行を書き換えるとき、文字列の中の `\"` を文字列の終わりと読まなくなった。
- テストの SKIP の行は、ritsu のどのクレートとも同じ `SKIP: koyomi: <理由>` の形になった。

続く C.10 で、生成先の言語の表面にかかわるところを、ritsu の生成器が共に使うクレート(ritsu-emit)のものに替えた。五つの出力先の予約語の表(E009 でエラーにする語。ECMAScript 2025、Python 3.14.6、Go 1.25、Rust 1.94、PostgreSQL 18.0 のもの)、Go の名前の作り方(`PaymentAt`、`paymentterms`)、文字列のリテラル(四つの出力先は JSON の文字列、SQL は `'…'`)、生成物の頭の一行(`Code generated by koyomi 0.1.0. DO NOT EDIT.`)とそれを書くコメントである。koyomi に残したのは、生成物が使う名前(`GENERATED`、`MODULES`)と、各出力先の生成器の中身である。例の全部の `.cal` を五つの出力先に生成したファイル(80 個)が一バイトも変わらないこと、五つの出力先の突き合わせのテストと golden が通ることを確かめた。

段階 D の最初の部分で、ritsu の口(ritsu の DESIGN 3.2)に答える `src/ports.rs` を足した。`ritsu_ports::Dates` の事実(関数、入力とその範囲、カレンダーとデータの範囲、`at` の時刻と UTC オフセット、条件の名前と文)は、`koyomi api` と同じものを型で渡す。日付がとりうる値の集合と、日付の入力からの日数の最小と最大は、`koyomi check` と同じく範囲のすべての入力で計算して渡す。入力の組み合わせが `check` の確かめる数を超えるとき、途中で計算が止まる入力があるときは、決められないと言う。評価は参照インタプリタのものである。`.cal` が持つもの(入力、日付、条件、出典)の定義の文は、日付なら `date … =` の塊の行(操作の行と `at` を含む)、条件ならその行で、どの行もコメントと前後の空白を除く。参照する外のものは、`use calendar` の先と、出典のコピーのファイルである。ritsu の DESIGN 7.5 の例(`payment_20th_close_next_10th.ja.cal` の `支払日` は 23 通り、受領日から 18〜51 日)は、口から数えても同じだった(`tests/ports.rs`)。コマンドの振る舞いは変えていない(例と fixture の全部のコマンドの 352 回の出力が一字も違わない)。

段階 D の最後の部分で、`ritsu_ports::Sources` にも答えるようにした(ritsu の DESIGN 3.2)。yuen が、日付のファイルとカレンダーのファイルが保存して固定している出典を借り、要件のコピーと比べるためである(yuen の DESIGN 1.4、3.3)。出典ごとに、名前、行、法令ならデータベースと ID と時点と条ごとの固定、ファイルならパス(そのファイルからの相対)と url と固定を渡す。答えるのは `koyomi check` を通るファイルだけで、通らないファイルには検査の診断を返す。コマンドの振る舞いは変えていない。

段階 E の言語をまたぐ検査(ritsu の DESIGN 7.5〜7.8)のために、日付の口の問いを二つ足した。`span` は、日付の入力から日付までの日数の最小と最大を、それぞれに初めてなる日付の入力の日とともに返す(ritsu の X5 が、仮押さえが必ず期限切れになる例に、その日を書く)。`input_for` は、日付がある日になる最初の入力を返す(X3 の (a) と X6 が、範囲を外れる日の例に、koyomi のどの入力でその日になるかを書く)。どちらも `check` と同じく範囲のすべての入力を計算し、入力の組み合わせが確かめる数を超えるときは答えない。コマンドの振る舞いは変えていない。

段階 E の最初の部分で、コマンドの本体を `src/main.rs` からライブラリの `koyomi::run::run` に移した。クレートのバイナリと ritsu の `ritsu koyomi`(ritsu の DESIGN 8.2)が、同じ関数を呼ぶ。`ritsu check` のために、`koyomi::ports::Engine::checked` が、`koyomi check` が印字するもの(診断一つずつのテキストと `--format json` のオブジェクト、通ったファイルの `ok —` の行)を、コマンドと同じ関数で作って渡す(ritsu の DESIGN 8.3)。コマンドの振る舞いは変えていない。

2026-10-06、八つ目の言語 sekisho(認可の言語)のために、日付の口の問いを二つ足した。`calendar` は、カレンダーのファイルの名前と別名、データの範囲、UTC オフセットと、データの範囲の中で休みの日の全部(`koyomi vectors` が `"open": false` と書く日)を返す(sekisho の `today is open in <カレンダー>` が、範囲の日ごとの真偽を数える)。`doc` は、`koyomi doc` と同じページ(Markdown か HTML、英語か日本語)を返し、頭の「ファイル」の行だけを、呼ぶ側の書き方にする(sekisho のページが埋め込む)。どちらも `check` を通るファイルにだけ答える。`tests/ports.rs` が、`koyomi api` と `vectors` と golden のページと `koyomi doc --format html` の出力に突き合わせ、イングランドとウェールズのカレンダーでは、GOV.UK の表のコピーと曜日から別に数えた休みの日(2019-01-01〜2028-12-31)とも突き合わせる。コマンドの振る舞いは変えていない。

同じ日に、口が同じファイルを何度尋ねられても、検査を一度で済ませるようにした。sekisho は日付の関数を、渡す値ごとに `Dates::eval` で呼ぶ(例の `.gate` では返金の期限を、注文の `paid_on` がとる 1,004 日について一回ずつ)。これまでの口は、尋ねられるたびにファイルを検査し直していた。いまの口は、検査の結果を、呼ぶ側が書いたパスごとに覚え、その検査が読んだファイルの全部を、読んだときの中身と一緒に持つ。読んだファイルとは、そのファイル、使うカレンダー、カレンダーが読む祝日の表、法令のコピーと `revision.txt` である。次に同じファイルを尋ねられたら、読んだファイルを読み直し、どれも同じなら覚えた結果で答え、一つでも違えば検査し直す。

- 読んだファイルは、検査を `ritsu_base::fs::with` で包み、検査が `ritsu_base::fs` に頼んだ読みをそのまま記録して知る(`ports.rs` の `Recorder`)。結果の型から読んだファイルを集める形は、取らなかった。読みが増えたときに集め忘れると、変わったファイルを見落とすからである。外側に `with` があれば(ブラウザで試すページが渡すメモリのファイル)、記録しながら読みはそこへ渡すので、覚えた結果も、読み直しも、そのスレッドが読むものの上にある。土台に、外側の `with` を返す `ritsu_base::fs::current` を足した。
- 覚えるのはスレッドごとで(rulec が規則の日を読む口 `days::with` と同じ置き方)、256 件を超えたら覚え直す。`Engine` は値を持たない型のままにした。ritsu の三十を超える所が `koyomi::ports::Engine` と値で書いているからである。`Engine::checks` が、そのスレッドで検査した回数を数える。
- `ritsu check` が印字する `koyomi check`(`Engine::checked`)は、前のとおり、ファイルごとに検査する。

確かめたこと:`tests/ports.rs` の二つのテスト。Net 30 の例の三つのファイルを一時のディレクトリに置き、200 回の `eval`、`values`、`days`、`doc` を尋ねても、検査は一回だけである。答えは、何も覚えていないスレッドが検査して答えるものと同じになる。カレンダーに休みの日を一つ足すと検査し直し、期日が後ろにずれる。祝日の表のコピーを一字変えると、固定した中身と合わずにエラーになる。メモリのファイルは、中身が替われば検査し直し、同じパスをディスクで尋ねると、メモリの結果では答えない。koyomi のテストの全部(160 件)、yuen・sakai・ritsu-cross・ritsu-model・ritsu-project の全部、dandori と rulec と ritsu の koyomi を通るテスト(`RITSU_TEST_LEVEL=tools`、563 件)が通る。

速さ(test の構成、この機械で測った実物):

| | 前 | 後 |
|---|---|---|
| 例の `refunds.gate` の検査(sekisho の `check_file`。三回) | 1.771〜1.809 秒 | 0.460〜0.464 秒 |
| 例の `refunds.ja.gate` の検査(三回) | 1.785〜1.799 秒 | 0.461〜0.467 秒 |
| 例の `dates/refund_terms.cal` に、入力の範囲の 1,060 日の全部で `Dates::eval` | 1.460 秒(1 回 1.377 ミリ秒) | 0.036 秒(1 回 0.034 ミリ秒。最初の検査を含む) |
| `cargo test -p koyomi --test ports` | 14.11 秒 | 0.63 秒 |

## 12. まだやらないこと

どれも、作る理由が見えたら作る。ここに書くのは、黙って消えたように見せないためである。

コマンド:

- `koyomi test <dir>`:利用者の CI で、生成したコードを vectors と突き合わせる(rulec の `test`)。ランナーは `gen` が書くので、要れば足せる。
- `koyomi fmt`:書き方をそろえる。それまでは W202 で代える。

言語:

- 週の締め(`close weekly fri`)、月の何番目の曜日、年度の締め(`close every 03-31`)。
- 日付の入力を二つ持つこと(1.3)。整数を返す関数(1.3)。
- 比較で分ける一般の `if`、`if open`、条件の `!=` と `is closed`(1.8、1.10)。成り立たなくてもエラーにしない比較(1.10)。
- tz データベースと夏時間(1.9)。
- 法令の附則と別表を引くこと(1.5)。

生成するコード:

- Wasm の型付きの WIT(日付ごとの関数と、`result` のエラー)と、Rust のランナーを WASI のコマンドにすること(13.3)。

ページと文書:

- 人が読むページの中で、入力を変えて計算を試すこと(7.5)。
- 日本語の `docs/reference.md` と `docs/generated-code.md`。今は英語だけで、日本語は README.ja.md と `docs/codes.ja.md` にある。
- ドキュメントのサイト、MCP サーバー、ブラウザで試すページ(rulec と dandori にはある)。

公開:

- リリースとバイナリ、crates.io。今の入れ方は、リポジトリを clone して `cargo install --path .` である。
- dandori が `koyomi api` を読んで、ワークフローの中で koyomi が生成した関数を呼ぶこと(8 章)。dandori の側で決めることなので、koyomi は `api` を出すところまでにした。

## 13. 出力先の Wasm(2026-10-24)

### 13.1 きっかけ

ritsu の生成物に、Rust と Wasm の出力先をそろえる。koyomi、kaname、chobo、`ritsu gen` の順に足し、この章はその最初の段である。Wasm のモジュールは、ブラウザでも、Node でも、エッジの実行環境でも、Wasm のプラグインを読むホストでも、ホストの言語を問わずに動く。rulec はすでに規則を Wasm のモジュールにしていて(rulec の DESIGN §15.64)、ホストは JSON のオブジェクトを一つ渡して一つ受け取る。期日も同じ口で呼べれば、一つのホストのコードで規則と日付の両方を呼べる。koyomi の Rust の生成物は標準ライブラリの外を何も読まないので(6.2)、そのまま wasm32-unknown-unknown に組める。

### 13.2 決定

1. **出力先 `wasm` を足す。** `koyomi gen --target wasm` は、`wasm/` に四つのファイルを書く。`--target` が無ければ、ほかの五つと一緒に書く。
   - `<別名>_wasm.rs`:crate root。canonical ABI のアロケーター(`cabi_realloc`)、`call`、`cabi_post_call` と、JSON のオブジェクトを一つ読む小さな読み手と、入力を読んでモジュールの関数を呼び、答えを書く入口を持つ。
   - `<別名>.rs`:`rust/` に書くものと一字一句同じモジュール。
   - `<別名>.wit`:`call` を export する world。
   - `<別名>_runner.mjs`:Node で走るランナー。

   組むのは `rustc` だけで、cargo も Cargo.toml も要らない。`rustc --edition 2021 -C opt-level=s -C lto -C panic=abort -C strip=symbols --target wasm32-unknown-unknown --crate-type cdylib <別名>_wasm.rs -o <別名>.wasm` の一行で、フラグは rulec と同じである。できるモジュールは何も import しない(WASI も読まない)。例のモジュールは 32〜48 KB で、`payment_20th_close_next_10th.wasm` は祝日の表を入れて 45 KB だった。crate root は、エディション 2021 でも 2024 でも `-D warnings` で通るように書く(`#[unsafe(no_mangle)]` と書き、unsafe の関数の中でも `unsafe` のブロックを使う)。そのため rustc 1.82 以降が要る。

2. **口は `call: func(input: string) -> string` の一つにする(★)。** 日付のファイルは日付の関数をいくつも持ち、カレンダーを読むなら `is_open` も持つが、export するのは `call` だけにした。理由は二つある。
   - rulec の Wasm と同じ口になる。canonical ABI で文字列を渡して受け取る二十行ほどのホストのコードで、規則のモジュールも日付のモジュールも呼べる。
   - 関数ごとに export すると、日付の別名が export の名前と WIT の名前になる。WIT には予約語(`result`、`list` など)と名前の形の決まりがあり、モジュールの export には `memory` もある。それに当たる別名を、新しく E009 で止めることになる。`call` 一つなら、別名はモジュールの中の名前(`k::payment`)にしかならず、Rust の予約語の検査(6.1)がそのまま効く。

   `call` は日付を全部計算して返す。日付を一つだけ使うホストは、`out` から一つ読めばよい。日付の計算は一つの入力で数マイクロ秒なので(13.5。871,604 行を、Node から呼んで 3.3 秒)、全部を計算しても困らない。

   どの関数を呼ぶかは、渡すオブジェクトの形で決まる。入力の名前のオブジェクトなら日付で、`{"date": "YYYY-MM-DD"}` ならカレンダー(`is_open`)である。`date` は koyomi のキーワードなので入力の名前にはなれず、二つを取り違えることはない。カレンダーのファイルの vectors の行も `{"in":{"date":…}}` なので、カレンダーのファイルのモジュールも、そのカレンダーを読む日付のファイルのモジュールも、同じオブジェクトに同じ答えを返す。

3. **入力と答えは koyomi vectors の形にする(★)。** 入力は、入力の名前(別名ではない)をキーにした JSON のオブジェクトで、日付は `"YYYY-MM-DD"` の文字列、整数は JSON の数で書く。koyomi vectors の行のように `"in"` の下に入力を置いたものも同じに読むので、vectors の行をそのまま渡せる。答えは、koyomi vectors がその入力に書く行と同じ `{"in":…,"out":…}` で、`out` には日付を宣言の順に置き、`at` のある日付ならその後ろに時刻を置く。`in` に書くのは、日付が受け取る入力だけである。モジュールはそれ以外の入力を読まない(条件だけが使う整数は、ほかの出力先でも関数の引数にならない)。

   どの JSON のエンコーダーが書いたものも読めるようにした。キーの `\u` のエスケープ(Python の `json.dumps` は、既定で日本語の名前をそう書く)、空白、整数の値のいろいろな書き方(`20`、`20.0`、`2e1`。rulec の §15.205 と同じ)を読む。同じキーが二度あれば、後の値を読む(JavaScript の `JSON.parse` と同じ)。

4. **拒否の行は rulec の形にする(★)。** 契約の外の入力には、日付の代わりに `{"refused":"<種類>","error":"<文>"}` を返す。トラップはしない。
   - 種類は、ほかの出力先のエラーの四つ(`range`、`data`、`reject`、`date`)に、Wasm の入口が拒む `input` を足した五つである。四つの種類の文は、ほかの出力先のエラーのメッセージと同じで、`--lang ja` なら日本語になる。
   - `input` は、渡されたものが入力を持たないときの種類である。一つの JSON のオブジェクトでない、UTF-8 でない、入力が無い(`null` も無いものとみなす)、日付の入力が文字列でない、整数の入力が整数の数でない(文字列の `"20"`、`20.5`、`true`、64 ビットを超える数)のどれかである。文は rulec の入口と同じ言い方にした(`received is missing`、`received がありません`)。
   - 文字列だが日付でない値(`2026-02-30`、`2026-4-1`)は `date` にした。TypeScript の出力先が、日付でない文字列を `date` のエラーにするのと同じである。
   - rulec の `refused` の値は `input` と `contradiction` で、koyomi の値とは違う。どちらも「何を拒んだか」を言う鍵で、行の形は同じなので、ホストは `refused` があるかで答えを分けられる。

5. **world と package の名前。** package は `koyomi:<名前>@<バージョン>`、world はその `<名前>` で、`<名前>` は別名の語を `-` でつないだものにする(`payment_20th_close_next_10th` は `payment-20th-close-next-10th`)。WIT の決まりに合わせて、二つのことをする。`_` が続くところと端の `_` は、一つの `-` にまとめるか、落とす(`a__b` は `a-b`。WIT は `-` の両側に文字か数字を求める)。WIT の予約語になる名前には `%` を付ける(`result` は `%result`)。予約語の表は、WIT.md の予約語を wasm-tools 1.245.1 が読むとおりに転記した四十三語である。バージョンは `v1` なら `1.0.0` で、semver の数(64 ビット)に収まらなければ書かない。これで、検査を通るどの別名からも、wasm-tools が読める world ができる。Wasm でだけ使えない別名は無いので、新しい診断は足していない(13.5)。world の doc コメントには、送るもの(入力の名前、型、範囲)と返るもの(`out` の鍵)を書く。

6. **`koyomi api` の `wasm` の項。** rulec の `api` の Wasm の項と同じ鍵に、ほかの出力先の項の `file`・`module`・`date_type` を足した(8 章)。Wasm には関数の署名が無いので、日付ごとに `out` の鍵を書く。`errors` は持たない。rulec と同じく、モジュールは例外を上げず、捕まえるものが無いからである。拒否の形は `answers` が言う。

7. **ランナーは Node の `.mjs` にする。** ほかのランナーと同じく、vectors の行を標準入力から読み、答えを一行ずつ書く(`out` の値を空白で区切り、拒否なら `error <種類>`)。rulec も Wasm を Node で走らせて確かめている。

### 13.3 捨てたもの

- 日付ごとに関数を export すること。JSON の口を関数ごとに置く形も、型付きの関数も、13.2 の 2 の理由で採らなかった。
- 型付きの WIT(`payment: func(received: string) -> result<string, koyomi-error>`)。読みやすいが、canonical ABI の `result` や record の配置を、生成するコードが自前で書くことになる。rulec が JSON の口を選んだ理由(rulec の §15.64)と同じである。
- 答えから `in` を外すこと。答えを koyomi vectors の行と同じ形にしておけば、ホストは答えを vectors と同じに読め、テストも答えを vectors の行と一字一句比べられる。
- 拒否を、koyomi vectors の誤りの行(`{"in":…,"error":"range"}`)の形にすること。rulec の Wasm の拒否の行と形が分かれ、両方を呼ぶホストが二通りに読むことになる。`error` の鍵に、rulec では文が、koyomi では種類が入ることにもなる。
- `wasm32-wasip1` を対象にすること。WASI の import が付き、ブラウザや WASI を持たないホストでは動かない。Rust のランナーを WASI のコマンドにすること(rulec の §15.63)もしていない。日付のモジュールを標準入出力で呼ぶ実行環境の例が、まだ無いからである。
- cargo のプロジェクトを書くこと。`rustc` 一つで組めることは、Rust の出力先と同じ約束である(rulec の §15.64 と同じ)。
- Wasm のモジュールのコードを全部ほかの言語と分け合うこと。組み立てのフラグ、`call` の署名、WIT の予約語の表、world と package の名前の作り方(13.2 の 5)、ABI の三つの関数、Node のランナーが `call` を呼ぶ関数は、ritsu の生成器が共に使う ritsu-emit の `wasm` にあり、rulec の規則のモジュールと kaname のパッケージの Wasm も同じものを使う(kaname の DESIGN 36.12、rulec の DESIGN §15.278)。ここへ移す前と後で、木の全部の `.cal` の `gen`(英語と日本語)と `api` の出力を比べ、0.40.0 の後に入った E016(15 章)で止まるファイルのほかは、一字も変わらないことを確かめた(kaname の DESIGN 36.15)。JSON の読み手、整数の読み方、拒否の行は koyomi の中に残した。kaname の入口とは、読む深さ、整数でない値への文、拒否の種類が違い、一つにすると、どちらかの生成物の文字が変わる。rulec も、JSON の読み手と拒否の行を自分のものに残している。

### 13.4 変わること

- `koyomi gen` は、`--target` が無ければ `wasm/` の四つのファイルも書く。前のバージョンで生成したディレクトリに `koyomi gen --check` をかけると、`wasm/` のファイルが無い(`missing:`)と言って 1 で終わる。`koyomi gen` を一度かけ直すか、`--target` で出力先を選べばよい。
- 二つのファイルの別名が、Wasm の出力先でだけ同じパスを書くことがある(別名 `same` の crate root と、別名 `same_wasm` のモジュールは、どちらも `wasm/same_wasm.rs`)。ほかの出力先で同じパスを書くときと同じく、`koyomi gen` は何も書かずに 2 で止まる。
- `--target` が `wasm` を受け付ける。`koyomi --help` と `koyomi gen --help` の文が変わる(`TypeScript, Python, Go, Rust, SQL and Wasm`、`every target without it`、例の `--target wasm`)。
- `koyomi api` の出力に `wasm` の項が足される。ほかの鍵は変わらない。
- E010(別名の形が違う)の注は、「五つの出力先のどれでも使える形」が「どの出力先でも使える形」になる(英語は `all five targets accept` が `every target accepts`)。
- ほかの出力先の生成物と、`check`・`doc`・`vectors` の出力は変わらない(13.5)。

### 13.5 確かめたこと

- `tests/targets.rs` の `wasm`。突き合わせの 29 のファイル(6.4)の crate root を、`koyomi api` の組み立ての行(エディション 2021)と、エディション 2024 の型の検査で、どちらも `-D warnings` で通す。Node のランナーに vectors の全部の行(11,554,942 行)を流し、一行ずつ参照インタプリタの結果と比べる。`--lang ja` の生成物も、6.4 の四つのファイルで全部の行を比べる。この機械で、Wasm だけを走らせると、突き合わせは 13.4 秒(ランナーを四つずつ同時に)、試験は組み立てを含めて 19 秒だった。koyomi の試験の全部(167 件、SKIP なし。`cargo xtask test --level tools -p koyomi`)と並べて走らせたときは、突き合わせが 14.8 秒だった(README の表)。
- `tests/wasm.rs` の五つのテスト。
  - `gen` が四つのファイルを書き、`wasm/` のモジュールが `rust/` のものと一字一句同じこと。world に、送るものと返るものが英語と日本語で書かれること。
  - 29 のファイルの答えが、koyomi vectors の行と一字一句同じこと(`in` は、日付が受け取る入力だけにして比べる)。行が 100,000 に満たないファイルは全部の行、大きなファイルは最初の 2,000 行と範囲のすぐ外の行、カレンダーを読む日付のファイルはカレンダーの最初の 400 日とデータの範囲のすぐ外の日で、合わせて 303,558 行。ホストはモジュールの import が空であることも確かめる。
  - 契約の外の入力への答え(英語と日本語、整数を受け取るファイル、カレンダーのファイル)。範囲の外、日付でない文字列、文字列でない日付、無い入力と `null`、整数でない値、`\u` のエスケープのキー、同じキーが二度、`"in"` の下、JSON でないもの、UTF-8 でないもの、百段の入れ子。どれもトラップせずに、決まった行で答える。同じ入力で二万回呼んでも、メモリが大きくならない(答えを解放している)。
  - `koyomi api` の `wasm.component` の二行で component を作り、`wasm-tools validate` が通り、world が `call` を export し、wasmtime が `--invoke` で呼ぶと、答えが同じ行になること(英語の日付、日本語の範囲の外、カレンダー)。
  - WIT の予約語の四十三語と、数字の語、`__`、端の `_` を別名にしたファイル。ほかの出力先が別名として受け付けない 17 語(E009)を除いた 32 の別名で、日付のファイルとカレンダーのファイルの world を wasm-tools が読み、名前と package のバージョン(`v007` は `7.0.0`、21 桁のバージョンは書かない)が上のとおりであること。
- `tests/cli.rs`。`gen` が書くファイルの一覧に `wasm/` の四つが入ること、`--target wasm` が四つだけを書くこと、Wasm の出力先でだけ同じパスになる二つの別名を `gen` が止めること。
- `tests/api.rs`。例の api の golden を取り直し、差分が `wasm` の項を足しただけであること(15 のファイルで 999 行の追加、削除は 0)。`wasm` の項の `file` が `gen` の書く crate root で、`call_signature` が `.wit` にあり、`out` の鍵がどれも crate root にあること。
- `tests/docs.rs` と `tests/design.rs`。ページの ```wit と ```js のコードも、`gen` の書くファイルから切り出したものであること(コードの塊は、出力先ではなくファイルの拡張子で照らし合わせるようにした)。README には、比べた行の数を六つの出力先のそれぞれについて書く(`tests/docs.rs` が数える)。
- 替える前と後の突き合わせ。替える前のバイナリ(ブランチの起点 830e32df のまま組んだもの)と後のバイナリで、木の `.cal` の全部(228。koyomi の 137、yuen の 51、ritsu の 13、dandori の 11、sakai の 6、ブラウザで試すページの 4、sekisho の 3、kaname の 2、rulec の 1)について、`check`(英語、日本語、JSON)、`doc`(英語、日本語)、`vectors`、`api`、`gen` の五つの出力先(英語と日本語)と `--target` なしの `gen` を比べた。4,104 回の出力は、`api` の `wasm` の項と、`--target` なしの `gen` が書く `wasm/` のファイル(検査を通る 103 のファイルで 412)のほかは一字も変わらない。五つの出力先の生成物 2,060 個も一バイトも変わらない。変わったのは `koyomi --help` と `koyomi gen --help` の文(英語と日本語)だけである。

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

- rulec の `.wit` は、規則の名前によっては wasm-tools が読めない形になる。rulec の `wit_name` は別名の `_` を `-` に替えるだけなので、`__` を含む名前(`a__b` は `a--b`)、`_` で終わる名前(`trailing_` は `trailing-`)、WIT の予約語の名前(`list`、`record`、`world`、`type`、`use` など。四十三語のうち、rulec の検査は 39 語を規則の名前として受け付け、その 39 の `.wit` はどれも wasm-tools が読めなかった)では、`wasm-tools component embed` が `.wit` を読めずに止まる。モジュールそのもの(`call`)は動き、component にするところだけが止まる。直すには、13.2 の 5 の作り方がそのまま使える。

## 14. `ritsu check` の JSON に渡す、ファイルのオブジェクト(2026-10-31)

`ritsu check --format json` の `files` の各項目は、`check` に、その言語の `check --format json` がそのファイルについて書くオブジェクトを持つ(ritsu の DESIGN 8.3)。`koyomi::ports::Engine::checked` は、診断と `ok —` の行に加えて、`koyomi check --format json` がファイルごとに書くオブジェクト(`{file, ok, summary, diagnostics}`)を `ritsu_ports::Checked::json` で渡す。作るのはコマンドと同じ `check::to_json` で、その値を書き出してから ritsu-base の JSON に読み直すので、キーの順もコマンドと同じである。ritsu は `file` と `diagnostics` を除いた `{ok, summary}` を入れる。読めないファイルは、コマンドがオブジェクトを書かないので `null` になる。koyomi のコマンドの出力は変えていない。

確かめたこと:ritsu の `tests/check.rs` の `each_files_check_in_the_json_is_what_its_language_writes` で、テストのプロジェクトの三つの日付のファイルと sekisho の例の三つの日付のファイルの `check` が、`ritsu koyomi check --format json` のオブジェクトから二つのキーを除いたものと同じであること。check の JSON の golden(`crates/ritsu/tests/golden/check/`、`golden/modules/holidays.json`)。

## 15. 出力先の綴りで一つの識別子になる名前(2026-11-01)

**きっかけ**:rulec は、書いた人の名前どうしが生成先の綴りで一つの識別子になるときと、生成先が使えない識別子になるときを、`check` で言う(rulec の DESIGN 15.263、15.265)。koyomi の出力先でも同じことが起きるかを、出力先ごとに名前の綴りを表にして確かめた。koyomi は、書いたままの別名を予約語と生成物が使う名前の表と比べるだけで(E009、6.1)、出力先が綴り直したあとの名前は比べていなかった。`date net_30 = …` と `date net30 = …` を書いたファイルは `check` を通り、`gen` が書いた Go のコードは `Net30 redeclared in this block` で組めなかった。先頭の 63 バイトが同じ二つの長い別名の日付は、PostgreSQL で一つの関数になり、前の日付を呼ぶと後の日付の答えが返った。

**原因**:別名は、どの出力先でも書いたままの識別子になる、という前提で E009 を作った。別名は ASCII の小文字と数字と `_` だけなので(`ast::is_alias`)、別名をそのまま書く出力先では、別名が違えば識別子も違い、PascalCase で宣言する生成物の名前(`Date`、`KoyomiError`)ともぶつからない。別名を綴り直すのは Go と PostgreSQL だけである。Rust は綴り直さないが、rustc のリント `non_snake_case` が、`_` が二つ続く名前を警告する。koyomi は Rust と Wasm のコードを `rustc -D warnings` で組めるように書いているので(6.2 の表)、そうした名前の関数、引数、モジュールは、警告をエラーにする組み立てで止まる。Python も綴り直さないが、ファイルの別名はモジュールの名前になり、Python は同じ名前のモジュールを一つしか読み込まない。標準ライブラリに同じ名前のモジュールがあれば、どちらかがもう一方の代わりに読み込まれる。

| 出力先 | 日付の関数(`at` のある日付は、時刻の関数 `<別名>_at` も) | 入力の引数 | ファイルの別名 | 生成物が宣言する名前 |
|---|---|---|---|---|
| TypeScript | 別名のまま | 別名のまま | ファイル `<別名>.ts` | `KoyomiError`、`KoyomiErrorKind`、カレンダーを読むなら `is_open` |
| Python | 別名のまま | 別名のまま | モジュール `<別名>.py`。標準ライブラリの同じ名前のモジュールとは、先に読み込んだほうだけが残る | `KoyomiError`、`is_open`。読み込む `date`・`datetime`・`timedelta` |
| Go | PascalCase(`net_30` は `Net30`、`cutoff_at` は `CutoffAt`) | 別名のまま | パッケージ。別名から `_` を除く(`payment_terms` は `paymentterms`) | `Date`、`ParseDate`、`KoyomiError`、カレンダーを読むなら `IsOpen`。ランナーの `TestMain` |
| Rust | 別名のまま。`_` が二つ続くと、rustc のリント `non_snake_case` が警告する | 別名のまま。同じリント | モジュール `<別名>.rs`。`mod <別名>;` で宣言し、同じリント | `Date`、`KoyomiError`、`is_open` |
| Wasm | Rust のコードの関数(`k::<別名>`)。同じリント。export は `call` だけ | Rust のコードの引数(同じリント)。JSON のキーは入力の名前 | world。`_` を `-` にする。Rust のコードは `mod k` で宣言する | `cabi_realloc`、`call`、`cabi_post_call`、`k` |
| SQL | スキーマの中の関数。PostgreSQL は名前の先頭の 63 バイトだけを残す | 同じく先頭の 63 バイト | スキーマ | `_` で始まる関数 |

そのため、次の名前が一部の出力先を壊す。0.40.0 の `check` はどれにも何も言わず、`gen` はそのまま書いた。

- Go で、`_` の位置だけが違う二つの別名(`net_30` と `net30`)。時刻の関数と、`_at` に `_` が付いた日付の別名も同じで、日付 `cutoff` の時刻の関数 `cutoff_at` と日付 `cutoff_at_` は、どちらも `CutoffAt` になる。一つのパッケージが同じ名前を二度宣言するので、組めない。
- Go で、PascalCase にすると生成物が宣言する名前になる別名。`date_` は `Date`、`test_main` はランナーの `TestMain`、`parse_date_` は `ParseDate`、`koyomi_error_` は `KoyomiError`、カレンダーを読むファイルの `is_open_` は `IsOpen` になる。
- Go で、パッケージの名前が Go のキーワードか `main` になるファイルの別名。`go_to_` は `goto` になり、組めない。`main_` は `main` になり、Go はプログラムとして組むので、ほかのパッケージから読み込めない。カレンダーのファイルの別名も同じである。
- PostgreSQL で、先頭の 63 バイトが同じ二つの別名。二つの日付の関数が同じ引数を取るなら、`CREATE OR REPLACE FUNCTION` が前の関数を後の関数で置き換え、残った関数が両方に答える。返す型が違えば(日付の関数と時刻の関数)`cannot change return type of existing function` で、引数の型が同じで名前が違えば `cannot change name of input parameter` で、SQL のファイルの読み込みが止まる。一つの関数が取る二つの入力の引数なら、`parameter name used more than once` で止まる。
- Rust と Wasm で、`_` が二つ続く別名(端の `_` は数えない。`ship__by`)。rustc のリント `non_snake_case` が、その名前の関数と引数を警告するので、`-D warnings` で組むと止まる。警告をエラーにしなければ、警告が出て組める。`_` で終わる別名の日付に時刻があると、時刻の関数が `cutoff__at` のように `_` が二つ続く名前になる。ファイルの別名は、Rust ではモジュールを宣言する `mod <別名>;`(`gen` が横に書くランナーと、コードを使うクレート)で警告される。Wasm はコードを `mod k` で宣言するので、ファイルの別名では止まらない。
- Python で、標準ライブラリのモジュールと同じ名前のファイルの別名(`abc`、`site`、`secrets`)。`abc` と `site` は Python が起動するときに読み込むので、`import abc` は標準ライブラリのモジュールを返し、生成した関数は呼べない(ランナーは `has no attribute` で止まる)。起動のときに読み込まない `secrets` では、生成したモジュールを先に読み込むと、プログラムのどの `import secrets` も生成したモジュールを返し、標準ライブラリの関数(`secrets.token_hex`)が無くなる。`ritsu gen` のパッケージは、日付のモジュールを `<パッケージ>.dates.<別名>` として持つので、この形では壊れない。

**決定**:

1. **出力先が綴り直したあとの名前を、生成したコードで同じ名前空間に入るものどうしで比べる**(`src/spellings.rs`)。Go では、パッケージの中の日付の関数と時刻の関数と、生成物が宣言する名前を比べ、パッケージの名前も見る。PostgreSQL では、スキーマの中で同じ型の引数を取る関数どうしと、一つの関数の引数どうしを比べる。綴りは生成器と同じ関数(ritsu-emit の `pascal` と `go_package`)で作る。Rust と Wasm では、関数と引数とモジュールの名前になる別名を、rustc の `non_snake_case` と同じ決まりで見る(`spellings::rust_snake`:端の `_` を除いて、`_` が二つ続かない。別名に大文字は無いので、ほかの決まりは当たらない)。Python では、ファイルの別名を標準ライブラリのモジュールの名前と比べる。名前の表は、rulec が規則のモジュールの名前に使う ritsu-emit の `copies::rulec::PY_STDLIB` である(3.14 の `sys.stdlib_module_names` から内部のモジュールを除き、3.12 と 3.13 で消えたモジュールを足した 219 の名前)。生成したコードとランナーが自分で読み込む `datetime`、`json`、`sys`、`typing` は、前から E009 で止めている(`naming::MODULES`)。
2. ★ **`check` は W001 の警告にし、`gen` は W001 が挙げる出力先を書くときに E016 で止まる。** chobo の W108 と E064 と同じ分け方である(chobo の DESIGN 16 章)。壊れるのはいつも一部の出力先(一つ、または Rust と、同じ Rust のコードから組む Wasm)で、ほかの出力先は組めて答えも合う。そのため `check` は通し、`ritsu check` も止めない。けれども、W001 が挙げる出力先のコードをそのまま書くと、組めないか、読み込めないか、黙って別の日付の答えを返すので、`gen` はその出力先を書かない。名前は付け替えない。どの出力先でも壊れる名前は、これまでどおり E009 で止める。番号は、W001 を名前の段の警告に、E016 を名前の段のエラーの続きにした(4.2)。
3. **E016 は、W001 をエラーにしたものである。** 場所、見出し、注は W001 と同じで、最後の注だけが違う。W001 の最後の注は、使わない出力先ならそのままでよいことと、別名の付け方(`月末から30日(<別名>)`)を言う。E016 の最後の注は、別名の付け方と、使わない出力先なら `--target` で使う出力先を挙げることを言う。台帳の再現は、W001 の再現と同じファイルを置いたディレクトリで `gen t.cal --target go` を回す。
4. ★ **止めるのは、`koyomi gen` と `ritsu gen` の両方が通る、出力先ごとの生成の入口である。** `codegen::build`(`koyomi gen` が出力先ごとに呼ぶ)と `codegen::package_module`(`ritsu gen` がパッケージの日付のモジュールごとに呼ぶ)が、コードを作る前に `spellings::stops` を見て、E016 を返す。chobo の `target::build_in` が `check_in` を呼ぶのと同じ形である。
   - `koyomi gen` は、書く出力先のどれか一つでも E016 になれば、E016 を標準出力に、止まった出力先を標準エラーに一行で言い(`` error: `t.cal` cannot be made into the code of `rust`, `wasm`, so nothing is generated ``)、どのファイルも書かずに 1 で終わる。一つの指摘が Rust と Wasm の両方を壊しても、E016 は一度だけ言う。`--target` を付けたときは、その出力先を W001 が挙げるときだけ止まる。`--check` も同じく止まる。
   - E016 は、二つのファイルが同じ場所に書こうとするとき(6.1。こちらは exit 2)と同じく、実行の全体を止め、一緒に渡したほかのファイルも書かない。検査を通らないファイルは、これまでどおりそのファイルだけを書かず、ほかのファイルは書く。
   - `ritsu gen` は、E016 を受けて、パッケージを書かずに 1 で終わる(ritsu の DESIGN 9.3)。ritsu の側で変えたのは、止まった診断を受けて止まるところだけである。
5. ★ **Python の標準ライブラリと同じ名前のモジュールでは、`ritsu gen` は止まらない。** パッケージは日付のモジュールを `<パッケージ>.dates.<別名>` として持つので、標準ライブラリのモジュールとぶつからない。ほかの形は、パッケージの中でも同じところで壊れるので止まる(`spellings::Found::in_package`)。W001 と E016 の注も、このことを言う。
6. **見出しは二つの名前(一つなら、その名前)と出力先を言い、注は出力先の綴りと壊れ方を言う。** 見出しは「`net_30`(7 行目)と `net30` は、生成した Go のコードで同じ識別子になります」。一つめの注は「Go: 関数の名前は PascalCase で書くので、日付「受領から30日」の関数 `net_30` と日付「月末から30日」の関数 `net30` は、どちらも `Net30` です。同じ名前を一つのパッケージに二度宣言することになり、コンパイルできません。」のように、何という識別子になり、なぜそうなり、どう壊れるかを言う。
7. **カレンダーのファイルも、パッケージとモジュールの名前を見る。** カレンダーのファイルも Go のパッケージと、Rust と Python のモジュールになるからである(`calendar for_ v1` は `for`、`calendar cal__x v1` は `mod cal__x;`、`calendar secrets v1` は `secrets.py`)。日付の関数は無いので、見るのはファイルの別名だけである。
8. **生成器は変えない。** 前から名前を付け替えておらず、書いたままの別名で書いている。書く出力先のコードは、0.40.0 と同じバイト列である。
9. ★ **rustc のリントの警告も、出力先が使えない名前に数える。** `-D warnings` を付けなければ組めるが、koyomi は Rust と Wasm のコードを `rustc -D warnings` で組めるように書くと決めていて(6.2)、`tests/targets.rs` もそれで確かめる。警告をエラーにする組み立てで止まるので、そう組む人には壊れたのと同じである。Rust の注は、警告であることと、`-D warnings` で止まることの両方を言う。
10. **Rust と Wasm では、一つの日付を一度だけ言う。** 別名に `_` が二つ続けば、日付の関数も時刻の関数も警告されるが、直すのは別名の一つなので、日付の関数を一度言う。別名が `_` で終わり、時刻の関数だけが `_` の二つ続く名前になるときは、時刻の関数を言い、注は `_` で終わらない別名を勧める。入力は、取る関数がいくつあっても一度だけ言う。

**捨てたもの**:

- **壊れる出力先でだけ、別名を付け替えること**(Go で二つ目を `Net30_2` にする、PostgreSQL で長い名前を縮める、など)。呼ぶ側の名前が出力先で違い、`koyomi api` が一つの名前で言えなくなる。rulec の DESIGN 15.263 と同じ理由である。
- **W001 をエラーにすること。** `check` と `ritsu check` が、使わない出力先のために、通っていたファイルを止める。止めるのは、その出力先を書くときだけで足りる。
- **`gen` が、W001 の挙げる出力先もそのまま書くこと**(rulec の W121 の扱い)。組めないか、読み込めないか、PostgreSQL では黙って別の日付の答えを返すコードが書かれる。`check` の注が言っても、`gen` だけを回す CI では気づかれないまま使われる。
- **`gen` が、W001 の挙げる出力先を黙って飛ばすこと。** `gen` は出力先を挙げなければ六つを書くので、一つが黙って無いと、使う人が気づかない。止めて、`--target` を案内する。
- **Rust と Wasm のコードに `#![allow(non_snake_case)]` を書くこと**(rulec と chobo の Rust のコードはそうしている)。生成したファイルの中に書く内側の `allow` は、モジュールの名前の警告も消す。モジュールの先頭に `#![allow(non_snake_case)]` を足すと、ランナーの `mod rust__module;` は `-D warnings` で通る(rustc 1.94.1 で確かめた。chobo のクライアントの `mod my__shop;` も同じである)。けれども、どの `.cal` の Rust と Wasm のコードもバイト列が変わり、生成器を変えないという決定(8)に反する。

**変わること**:

- `check` が、新しい W001 を言う。警告なので、終了コードは変わらない。警告でも止める使い方(`--format json` の `severity` を見る CI など)では、新しい W001 で止まるファイルがありうる。`ritsu check` は koyomi の診断をそのまま言うので、W001 も言う。
- `koyomi gen` は、書く出力先を W001 が挙げるファイルがあると、E016 を言い、何も書かずに 1 で終わる。0.40.0 は全部の出力先を書いて 0 で終わった。W001 の挙げない出力先を `--target` で挙げれば、0.40.0 と同じものを書く。
- `ritsu gen` は、パッケージの出力先を W001 が挙げる日付のファイルがあると、E016 を言い、パッケージを書かずに 1 で終わる(Python の標準ライブラリと同じ名前のモジュールを除く)。
- 書く出力先のコードは変わらない。
- 木の `.cal` のうち、W001 のある 34 本を除く 313 本では、`check`(英語、日本語、JSON)の出力も `gen` が書くものも、0.40.0 と同じである(下の突き合わせ)。
- 台帳の W001 と E016(`koyomi explain`)、`docs/codes.md` と `docs/codes.ja.md`、reference の名前の節とコマンドの表、生成したコードのページの名前と書き方の段落、スキルのコピーと `SKILL.md` の生成の手順とコードの表、`koyomi gen --help` の終了コード 1 の文。

**確かめたこと**:

- `tests/spellings.rs`(九つ)と、材料の `tests/spellings/`(15 の形。英語を先に、日本語の版を横に置いて 30 本)。
  - どの形も W001 を一つだけ言い、挙げる出力先はファイルの名前が言う出力先(`go_` は Go、`postgresql_` は PostgreSQL、`python_` は Python、`rust_` は Rust と Wasm、`rust_module` は Rust だけ)で、英日の文は `tests/golden/spellings/` の golden と同じである。
  - `codegen::build` は、W001 が挙げる出力先でだけ E016 を一つ返す。場所、見出し、最後を除く注は W001 と同じで、最後の注は `--target` を言う。挙げない出力先では書き、別名は書いたまま TypeScript に `export function <別名>(` として入る。`codegen::package_module` は、Python の形を除いて、挙げる出力先(パッケージの TypeScript、Python、Go のうち)で止まる。
  - `koyomi gen` は、どの形でも E016 を一度だけ言い、止まった出力先を標準エラーに言って、何も書かずに 1 で終わる。`--check` も同じである。W001 の挙げない出力先を `--target` で挙げると書き、挙げる出力先を挙げると止まる。止まるファイルと一緒に渡した、注のとおりに別名を付けたファイルも書かない。日本語の文も確かめる。
  - 注のとおりに別名を付けた版(`tests/spellings/renamed/`、30 本)は、何も言われない。
  - `tests/spellings/` に形の無い名前は、試験の中に書いたファイルで確かめる。`ParseDate`、`KoyomiError`、カレンダーを読むときだけの `IsOpen`、カレンダーのファイルの別名 `for_`・`cal__x`・`secrets`、標準ライブラリの `collections`・`random`・`queue`・`secrets` は、W001 を一つ言う。`datetime`・`json`・`sys`・`typing` は E009 のままで、`collections_`・`my_site`・`secret` は何も言われない。`rust_snake` が端の `_` を数えないこと、時刻のある日付が一度だけ言われること(別名 `cut__off` なら日付の関数、`cutoff_` なら時刻の関数 `cutoff__at`)も確かめる。
  - JSON の W001 は `severity` が `warning` で、ファイルの `ok` は `true` である。
- `tests/targets.rs` で、形ごとに出力先の道具に通した。
  - W001 が挙げない出力先では、どの形も組めて、範囲の全部の入力で参照の評価器と同じ答えを返す。
  - 挙げる出力先では、Go の形は `go vet` が `common::SPELLING_FILES` の言う行で止まる(`Net30 redeclared in this block`、`expected 'IDENT', found 'goto'`、`main` のパッケージは、読み込むパッケージを足すと `is a program, not an importable package`)。PostgreSQL の形は、読み込みが止まるか(`cannot change return type of existing function` など)、読み込めて、置き換えられた関数の 31 行が参照の評価器と違う(`postgresql_functions.cal`)。Rust の形は、ランナー(モジュールを別名で宣言する)を `-D warnings` なしで組むと警告が出て通り、`-D warnings` で組むと `should have a snake case name` で止まる。Wasm の形(モジュールの名前の形を除く 6 本)は、crate root をエディション 2024 で `-D warnings` で検査すると、同じところで止まる。Python の形は、ランナーが `has no attribute` で止まる。
  - 注のとおりに別名を付けた 30 本は、六つの出力先のどれでも組めて、参照の評価器と同じ答えを返す。
- 台帳(`tests/codes.rs`)の E016 の再現は E016 を言う。台帳のコードは 42 である。変異の `tests/mutants/E016_rust_and_wasm.cal` と日本語の版は、`gen` の見出しが E016 を言う(`tests/mutants.rs`)。
- ritsu の `tests/gen.rs` の `a_dates_file_whose_names_break_the_target_stops_the_package`。Go の形の日付のファイルがあるプロジェクトの `ritsu gen --target go` は、E016 を言い、何も書かずに 1 で終わる(JSON では、診断の `tool` が `koyomi`、`code` が `E016`)。同じプロジェクトの `--target typescript` と `--target python` は書く。Python の形(別名 `abc`)のモジュールは、パッケージの中で `import abc` のあとでも答える。
- 木の 347 本の `.cal` を、0.40.0 といまのバイナリで `check`(英語、日本語、JSON)し、`gen` で全部の出力先を書いて比べた。違うのは、W001 のある 34 本(`tests/spellings/` の 30 本と、W001 と E016 の変異の 4 本)の `check`(W001 を言う)と `gen`(E016 で止まり、何も書かない)だけで、ほかの 313 本は一字も変わらない。34 本で W001 の挙げない出力先を `--target` で一つずつ書くと、0.40.0 の `gen` が書くものと同じバイト列である。

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

- E009 が一部の出力先でしか壊れない名前まで止めていたこと、入力 `__proto__`、WIT の package と PostgreSQL のスキーマで一緒に渡したファイルが一つになる形は、17 章で W001 と E016 にした。
- ritsu の playground(ritsu-wasm)は、この章の変更では触っていない。E016 で止まるようになったのは、16 章で `codegen::build` を呼ぶようにしてからである(根の DESIGN 8.7)。

## 16. `gen` は指定した出力先とランナーだけを書く(2026-11-01)

**きっかけ**:0.40.0 の `koyomi gen` は、`--target` が無ければ六つの出力先を全部書き、どの出力先にもランナーを付けた。`examples/net30.cal` からは 14 のファイルを書き、ほかのコードが読むのはそのうち 8 である(Wasm は crate root、モジュール、`.wit` の三つ)。`--target` は一つしか書けなかった。書く組がバイナリの版で決まるので、出力先を足すたびに(13 章の Wasm のように)、変えていないファイルの `gen --check` が新しい出力先のファイルを「無い」と言って落ちる。ritsu の生成のコマンドは、どれも指定したものだけを書く形にそろえた(ritsu の DESIGN 9.6)。

**原因**:`gen_cmd` は `--target` が無ければ `TARGETS` の全部を回し、`codegen::files` はモジュールとランナーを一緒に返していた。

**決定**:

1. **出力先は `--target typescript|python|go|rust|sql|wasm` で、何度でも書く。** 同じ出力先を二度書けば exit 2 で止まる(`` `--target go` is given twice``)。書く順はフラグの順ではなく `TARGETS` の順(typescript、python、go、rust、sql、wasm)で、「生成しました」の行もその順に並ぶ。
2. **`--target` が無ければ、何も書かずに exit 2 で止まる。** 文は、使える名前と、0.40.0 と同じものを書くコマンドを言う。コマンドは、その人のコマンドラインに、六つの `--target` と `--extra runner` を足したものである(シェルが分けて読む語は単一引用符でくくり、`--lang` も残す。生成したコードのコメントがその言語になるからである)。足す引数は `cli::UP_TO_0_40` に 0.40.0 のまま固定し、これから足す出力先は入れない。`.cal` のファイルが無いときの止まり方が先で、どちらもファイルを読む前に止まる。

   ```text
   error: `koyomi gen` writes only the targets it is given: give one or more `--target typescript|python|go|rust|sql|wasm`, and `--extra runner` for the runners
   note: up to 0.40.0, `koyomi gen` with no `--target` wrote every target with its runner; this writes the same files:
     koyomi gen examples/net30.cal --target typescript --target python --target go --target rust --target sql --target wasm --extra runner
   ```

   ```text
   エラー: `koyomi gen` が書くのは、指定した出力先だけです。`--target typescript|python|go|rust|sql|wasm` を一つ以上付けてください。ランナーは `--extra runner` で指定します
   注記: 0.40.0 までの `koyomi gen` は、`--target` が無ければ、六つの出力先をランナーつきで書きました。同じファイルを書くのは次のコマンドです
     koyomi gen examples/net30.cal --lang ja --target typescript --target python --target go --target rust --target sql --target wasm --extra runner
   ```

3. **ランナーは付属物で、`--extra runner` で頼んだときだけ書く。** 付属物はこの一つなので、`--extra` は一度だけ書くフラグにした(二度書けば exit 2)。Go のランナー(`<p>_runner_test.go`)をモジュールのパッケージのディレクトリに書くのは変えない。`codegen::library` がモジュール(Wasm は crate root、モジュール、`.wit`)を、`codegen::runner` がランナーを返す。`codegen::files` は二つを合わせたもの(0.40.0 がその出力先に書いたもの)として残し、`codegen::build` はランナーを書くかを受け取る。モジュールはランナーを読まない(ランナーがモジュールを読む)。
4. **頼まなかったランナーが残っていれば、挙げる。** その実行のファイルと出力先について、ランナーを書くはずのパスに、頭の行が koyomi のもの(版を問わない)のファイルがあれば、「もう生成しないファイルが残っています」と一行ずつ言い、最後に注記を一度出す。rulec の §15.277 と同じ形で、koyomi には JSON が無いので文だけである。消さず、終了コードも変えない(`--check` でも同じ行を出し、終了コードには数えない)。Go のランナーはモジュールのパッケージの中にあり、古いまま `go test` に組み込まれるからである。頭の行の無いファイル(人が置いたもの)と、指定しなかった出力先のディレクトリは見ない。0.40.0 が書いたディレクトリに `--target python --target go` で書くと、次のとおりになる。

   ```text
   generated no more, and still there: generated/python/net30_runner.py
   generated no more, and still there: generated/go/net30/net30_runner_test.go
   note: `--extra runner` writes these files, and this run does not ask for it; gen deletes nothing in the output directory, so they can be deleted if they are not wanted
   ```

5. **`--check` は、指定したものだけを比べる。** 文はいまのまま。
6. **E016 の決まり(15 章)は、指定した出力先ごとに保つ。** 指定した出力先のどれか一つでも W001 が挙げれば、何も書かずに 1 で終わる。E016 の最後の注と、E016・W001 の直し方の英語は、`--target` で一つを選ぶ言い方("pick the one you use")を、使う出力先を挙げる言い方("name the ones you use")にした。日本語の「`--target` で使う出力先を挙げてください」は、もとから数を言わない。

**捨てたもの**:

- **ランナーを別の出力先にすること**(`--target typescript-runner` のような名前)。ランナーは出力先ごとのもので、出力先の名前を二重にする。
- **`--target all`。** 「全部」の中身はバイナリの版で変わり、なくしたい問題をそのまま戻す(rulec の §15.277 と同じ)。
- **頼まなかったランナーを消すこと。** `gen` は出力先のファイルを消さない(rulec の §15.275 と同じ)。

**変わること**:

- `koyomi gen`(と `--check`)は `--target` が要る。ランナーは `--extra runner`。5 章の表と 6.1 の決定。
- dandori の試験の `koyomi_gen` は、glue が読むモジュールだけを書く(`codegen::library`)。ritsu の試験は、0.40.0 と同じものを読むところで `cli::up_to_0_40` の引数を渡す。ブラウザで試すページ(ritsu-wasm)は、選んだ一つの出力先のモジュールだけを書く(`gen --target <出力先>` と同じ。`codegen::build` を呼ぶので E016 で止まる。根の DESIGN 8.7)。
- 文書:`docs/reference.md`、`docs/generated-code.md`、README(英日)、スキル(`skills/koyomi/`)。

**確かめたこと**:

- 0.40.0 の実物で、ランナーを消した写しのモジュールが単独で使えること(Python と TypeScript の import、Rust の lib、`go.mod` を置いた Go の vet、Wasm の cdylib)。
- 木の 347 本の `.cal`(`git ls-files '*.cal'` の全部)を、英語と日本語(`--lang ja`)で、0.40.0 の `gen <file> --out <dir>` と、替えた後の `gen <file> --out <dir>` に `cli::UP_TO_0_40` の引数を足したもので比べた。比べたのは、終了コード、標準出力、標準エラー、続けて走らせた `gen --check` の三つ、書いたファイルの全部で、同じディレクトリで順に走らせたので、文の中のパスも読み替えていない。694 回のうち 626 回が一字も違わない。違った 68 回は、15 章の 34 本(`tests/spellings/` の 30 本と、W001 と E016 の変異の 4 本)の英日で、替えた後は E016 で止まって何も書かずに 1 で終わる。0.40.0 には E016 が無く、全部を書いていた。終了コードは、`gen` と `--check` の組で (0, 0) が 352 回、(1, 1) が 342 回。替えた後が書いたファイルは合わせて 4,928。替えた後のバイナリも版が 0.40.0 なので、頭の行も読み替えていない。
- 0.40.0 が書く 210 本について、出力先を一つずつ、`--extra runner` の有る無しで書いた(2,520 回)。`--extra runner` を付ければ 0.40.0 がその出力先のディレクトリに書いたものと一字も違わず、付けなければ、そこからランナーを除いたものと同じだった。84 回は 15 章の 34 本で W001 が挙げる出力先で、E016 で止まり何も書かなかった。
- `tests/cli.rs`:止まる文の英日(パスに空白があるとき、`--extra runner` を落とすこと、示すコマンドが 0.40.0 のファイルを書くこと)、二つの出力先をフラグと違う順で書いても `TARGETS` の順に書くこと、`--target` の名前の並びが `TARGETS` と同じこと、残ったランナーの挙げ方(人が置いたファイルと、指定しなかった出力先のランナーは挙げない)、`--check` が指定した出力先だけを比べること、exit 2 の引数(`--target` が無い、`--extra runner` だけ、同じ出力先を二度、知らない付属物、`--extra` を二度)。

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

- `--check` の文(「生成し直すと変わります」、"differs from what gen writes")は、rulec と `ritsu gen` の言い方("generated file is stale or hand-edited")と違う。そろえるのは別にする。

## 17. 一部の出力先だけで壊れる名前を E009 から分け、一緒に渡したファイルも比べる(2026-11-04)

**きっかけ**:15 章で直さずに残した四つの形である。E009 は、Go でしか壊れない `koyomi_error` や Python でしか壊れないファイルの別名 `datetime` まで、どの出力先でも止めていた。入力 `__proto__` は、JavaScript のオブジェクトのリテラルで作った入力では Wasm のモジュールに届かなかった。一緒に渡した二つのファイルが WIT の一つの package や PostgreSQL の一つのスキーマになることを、`check` も `gen` も言わなかった。

**原因**:`naming::GENERATED` と `naming::MODULES` は、どの出力先の何とぶつかるのかを持たない一つの表だった。C の段階で、生成物を書いてみて動かなくなった名前を足してできた表で、六つの出力先で壊れる名前と、一つの出力先だけで壊れる名前を区別していなかった。W001 が比べるのは一つのファイルの中だけで、`gen` がファイルどうしを比べるのは同じパスに書くときだけだった。

**決定**:

1. **二つの表を消し、名前を一つずつ分ける。** 生成物を読み、六つの出力先の道具に通して、どの出力先の何とぶつかるかを確かめた。どの出力先でも壊れるものは E009 のまま、一部の出力先でだけ壊れるものは W001 と E016 にし、どこでも壊れないものは何も言わない。同じ名前でも、書く場所とファイルの形で答えが違う。

   | 名前 | 書く場所 | いま | ぶつかるもの |
   |---|---|---|---|
   | `is_open` | カレンダーを読み、入力が日付だけのファイルの日付 | E009 | 六つの全部。カレンダーの関数と同じ引数の、同じ名前の関数になる(PostgreSQL は `cannot change return type of existing function`) |
   | `is_open` | カレンダーを読み、整数の入力もあるファイルの日付 | W001(TypeScript、Python、Go、Rust、Wasm) | カレンダーの関数。PostgreSQL は引数の違う別の関数として持てる |
   | `is_open` | カレンダーを読まないファイル | 何も言わない | どの出力先も宣言しない |
   | `koyomi_error`、`parse_date` | 日付 | W001(Go) | Go の `KoyomiError`、`ParseDate` |
   | `err` | 入力 | W001(Go) | Go の関数の中の `err` |
   | `err` | 日付 | 何も言わない | 関数は `Err` になる |
   | `str` | 日付 | W001(Python) | 生成物が呼ぶ `str(...)` を隠す |
   | `tuple` | 日付 | W001(Python) | 注釈の `tuple[...]` を隠す。動くが、`mypy --strict` を通らない |
   | `isinstance` | 日付 | W001(Python)。整数の入力を取る関数があるときだけ | 整数の入力を調べる `isinstance(...)` を隠す |
   | `frozenset`、`datetime`、`timedelta` | 日付、入力 | 何も言わない | モジュールの頭で読むので、関数があとから置いても壊れない |
   | `datetime`、`sys`、`typing` | ファイル | W001(Python) | 生成物が読み込む標準ライブラリのモジュール |
   | `std`、`core`、`alloc` | ファイル | 何も言わない | Rust のモジュールの中の別の経路である |
   | `pg_` で始まる名前、`information_schema` | ファイル | W001(SQL) | PostgreSQL が自分のものにするスキーマの名前(`unacceptable schema name`) |

   koyomi のキーワードと、出力先の言語の予約語(`date`、`int`、`range`、`bool`、`super`、ファイルの別名の `json`、`public` など)は E009 のままで、`<別名>_at` とぶつかる日付も E009 のままである。

2. **入力 `__proto__` は Wasm の W001 にする。** 入力を JSON のオブジェクトで受ける出力先は、六つのうち Wasm だけである(`call` が受け取る文字列のキーが入力の名前になる)。TypeScript、Python、Go、Rust、SQL は引数を位置で受け取るので、入力の名前はキーにならない。`gen --target wasm` は E016 で止まる。
3. **一緒に渡したファイルを比べる。** `koyomi check`、`koyomi gen`、`ritsu check` は、渡された順にファイルを比べ、次の形を後のファイルの W001 にして、注で前のファイルのパスを言う。`gen` は、挙げた出力先を書くなら、後のファイルの場所の E016 で止まる。

   | 形 | 例 | 挙げる出力先 |
   |---|---|---|
   | Go のパッケージの名前が同じ | `pay_days` と `paydays` | Go |
   | WIT の package の名前が、版も含めて同じ | `pay_days` と `pay_days_`(同じ版) | Wasm と Go |
   | PostgreSQL のスキーマの名前の先頭の 63 バイトが同じ | 先頭の 63 バイトが同じ別名の二つのファイル | SQL |

   別名が同じ二つのファイルは比べない。どの出力先でも同じ名前なので、`gen` は同じパスに書くと言って止まる。`check` には一つのファイルの版を並べて渡せる。エラーのあるファイルも比べない。そのエラーが先で、そのファイルからは何も生成しないからである。カレンダーのファイルも、別名がパッケージとスキーマになるので比べる。`ritsu gen` は、同じ Go のパッケージになる二つのファイルを、前からの `both write go/dates/paydays/paydays.go of the package` で exit 2 にする。
4. **生成物は一字も変えない。** W001 の挙げない出力先の生成物は、この章の前と同じバイト列である。Rust と Wasm のコードに `allow` を足して W001 を減らすことはしない。Wasm のモジュールのバイト列が変わるからである。

**捨てたもの**:

- 表を出力先ごとに分けて残すこと。上の表のとおり、同じ名前でも書く場所とファイルの形で答えが変わるので、名前の一覧には書けない。
- E009 のまま残すこと(15 章はエラーを警告に緩めるのを避けて変えなかった)。`gen` が E016 で止めるので、緩めても壊れたコードは書かない。Go を使わない人の `koyomi_error` まで止める理由が無い。
- ファイルをまたぐ形を前のファイルに付けること。前のファイルは、一つだけ読めば何もおかしくない。`koyomi check a.cal` と `koyomi check a.cal b.cal` で、`a.cal` について言うことが同じほうが読みやすい。
- ファイルをまたぐ形をエラーにすること。挙げない出力先では組めるので、それらの出力先を使う人を止めない。

**変わること**:

- これまで E009 で止まったファイルのうち、上の表で W001 か「何も言わない」になるものは `check` を通る。`gen` は、W001 が挙げる出力先を書くときだけ止まる。
- 新しく W001 と E016 になるのは、`pg_` で始まる別名(これまで E009 だった `pg_catalog` などのほかは、何も言わずに書いていた)、入力 `__proto__`、一緒に渡した三つの形である。どれも、書いても組めないか、片方のファイルの関数がもう片方を黙って置き換えるものなので、動いていた生成物は無くならない。入力 `__proto__` の Wasm だけは、JSON の文字列でキーを書くホストなら動いていたので、その人は別名を付ける。
- E009 の文と注から、消した表の名前の一覧を外した。

**確かめたこと**:

- `tests/spellings.rs`(13)と、材料の `tests/spellings/`(25 の形。英語を先に、日本語の版を横に置いて 50 本)。どの形も W001 を一つだけ言い、挙げる出力先は `common::SPELLING_FILES` のとおりで、英日の文は golden と同じである。挙げる出力先の `gen` は E016 で何も書かず、挙げない出力先は書く。注のとおりに別名を付けた 50 本(`tests/spellings/renamed/`)は何も言われない。
- `tests/spellings/across/` の六つの組(英語の三つと日本語の三つ)。後のファイルにだけ W001 が付き、前のファイルについて言うことは一つだけ読んだときと同じで、順を逆にすると付く先も逆になる。`gen` は挙げる出力先で E016 で止まって何も書かず、挙げない出力先は書く。注のとおりに別名を付けたファイルとなら、六つの出力先を一度に書ける。
- `tests/targets.rs`(ツールの段)で、挙げる出力先が壊れるところを確かめた。TypeScript は Node が止まり(`has already been declared`)、Python はランナーが止まるか(`cannot import name`、`missing 1 required positional argument`、`has no attribute 'year'`)`mypy --strict` が止まる(`is not valid as a type`)。Go は `go vet` が止まり(`KoyomiError redeclared in this block`、`ParseDate redeclared in this block`、`value of interface type error`)、PostgreSQL は読み込みが止まる(`unacceptable schema name`)。Rust と Wasm は rustc が止まる(`defined multiple times`)。注のとおりに別名を付けた版と、挙げない出力先は、六つのどれでも組めて、参照の評価器と同じ答えを返す。二つのファイルを一つのデータベースに読むと、後のファイルの関数が前のファイルの同じ名前の関数を置き換えることも、ここで確かめる(`across_sql`)。
- `tests/wasm.rs`。入力 `__proto__` のモジュールは、JSON の文字列でキーを書けば答え、JavaScript のオブジェクトのリテラルから作ると `__proto__ is missing` を返す。別名を付けた版はどちらでも答える。同じ package の二つの `.wit` は、`wasm-tools` が一緒に読めず、別名を付けた版は読める。
- `tests/ports.rs`。`ritsu check` も、一緒に渡したファイルの W001 を言う。
- この章の前のバイナリといまのバイナリで、木の `.cal`(403 本)の `check`(英語、日本語、JSON)と六つの出力先の生成物を比べた。違うのは材料の 20 本(`tests/spellings/` の、この章で足した 10 の形の英語と日本語の版)だけで、ほかの 383 本は一字も変わらない。

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

- 出力先の言語の予約語の E009 も、同じ形である(`bool` は Go だけ、`super` は TypeScript と Rust だけの言葉で、`json` と `public` は PostgreSQL だけの言葉だが、どの出力先も止める)。この章は `naming.rs` の表を分けたもので、`reserved.rs` は変えていない。分けるなら、予約語の表が言語ごとの列を持つことになる。
- 別々の `koyomi gen` で同じ `--out` に書くときは比べない。比べるのは一度に渡したファイルだけである。
- Python のモジュールの形は、15 章のとおり `ritsu gen` のパッケージを止めない。