DESIGN.md@crates/rulec · git:20261031.76f9a81 · 2026-10-31 · sha256 4e66205ab7242a61
DESIGN.md@crates/rulec git:20261031.76f9a81C
Immutable. This exact content never changes and is served at /api/v1/blob/4e66205ab7242a61.
# rulec 設計文書
業務ルールを決定表として記述し、静的に検査し、Python と Go の両方へ読めるコードを生成する道具の設計を決める。
実装はまだ書かない。
本文の構文例と生成コード例は、決定を説明するためのスケッチである。
合意済みの前提は再論せず、以下の記号で参照する。
- **P1**:最初の対象領域は送料。次がクーポンとポイント、引当は最後
- **P2**:記述の中心は決定表。業務担当者が読めること
- **P3**:ルールは純関数。時刻も在庫も会員区分も入力として渡される
- **P4**:型で守るのは、単位、丸め、完全性、優先順位と重なりの四つ
- **P5**:出力は Python と Go への依存ゼロの読めるコード生成。実行時ランタイムは持たない
- **P6**:道具本体はデータ不要、旧実装があれば等価検証、過去データがあれば影響算出、と段階的に価値が出ること
- **P7**:汎用言語にしない。ループも再帰も状態も GUI も持たない
本書は v0 草案を、公開規約からの転記実測を受けて改訂した v1 である。
実測とは、公開情報(日本郵便ゆうパック基本運賃 東京発、ヤマト宅急便のサイズ区分、楽天市場の送料無料ライン、39 キャンペーン、複数クーポンの計算方法とクーポン利用条件、Yahoo!ショッピングのクーポン利用条件)を 2 本の `.rule` に転記した計測を指す。
規模はゆうパック運賃が 84 行(表 55 行、列 2、群 6)、クーポン割引が表 15 行、列 6 である。
本文で「実測」と言うときはこの転記を指し、転記そのものは回帰コーパスとして §13 に収録する。
実測が支持した決定(二方式の共存、単項セルの制限、線形パイプライン、群、表引き出力への丸め宣言、完全性の常時必須)は各所に一文で記す。
覆った決定は本文を書き換え、v0 が持っていた検査の抜け道は塞いだ経緯ごと残す(§5.3)。
## 0. 全体像
rulec は一本のパイプラインである。
```
.rule ── 構文解析 ──> AST ── 型付け ──> TIR(型付き表 IR)
├──> 領域 IR(圧縮座標上の区画の集合)──> 検査 E101/E102/E105
├──> 参照評価器 ──> 例の検証・ベクタ期待値・verify/replay/diff
└──> コード生成 ──> generated/python/… generated/go/…
```
単位の誤り(E103)と丸めの漏れ(E104)は型付けの段で出る。
完全性(E101)、当てはまらない行(E102)、重なり(E105)は領域 IR の段で出る。
意味論の原本は TIR 上の**参照評価器**一つに置く。
例の検証も、テストベクタの期待値も、過去再生も、すべて同じ評価器を通す。
生成された Python と Go は、共通ベクタでこの評価器との一致を機械的に確かめる(§9)。
参照評価器は rulec の内部にだけ存在し、本番で実行されるのは生成コードのみである。
P5 の「実行時ルールエンジンにしない」は、この線引き(評価器は検証系専用、出荷物は生成コード)で守る。
この全体像に、境界の宣言を一つ足しておく。
**この道具の本当の境界はヒットポリシーではなく、セルの言語である。**
rulec は DMN の読み手でも部分集合でもない。
借りるのは決定表の骨(単一ヒットの二方式と、完全性・重なりという解析の問題設定)だけで、セルに書けるのは自列への単項テストに限る(§3)。
公開されている実在の DMN 資産には、セルに任意の FEEL 述語(`some … satisfies …` や `count(…)`)を書くものがあり、我々はそれをヒットポリシー以前にセルの言語で受け付けない(§4 の調査で開いた伊 ANAC の調達規則がその実例)。
この狭さは妥協ではなく購入である。
セルが単項だから行が区画になり、完全性も重なりも厳密に決定できる(§6)。
入れ子のデータへの述語も同じ線の上にある。
入れ子は境界で平たい入力に潰してから渡し、セルには入れない(§3、§15-6)。
「FEEL なら書けるのに」という圧力は実務で必ず来る。
曲げてよいのは境界の外(入力の作り方。§15-6)だけで、セルの言語を曲げた瞬間に、この道具の検査は成り立たなくなる。
### 0.1 対象の範囲
**決定**:対象は「**一件の取引を、決まった数の平たい値から一度で判定して、金額・可否・区分・順序のいずれかを返す規則**」で、かつ「**承認する人がいて、期日で改定されるもの**」である。運賃、割引とクーポン、料率と手数料、適用可否と期間区分がその形で、転記した 7 本はすべてここに入る。入力は 1〜10 個、出力は上の四種。
範囲を業種ではなく**判定の形**で書くのは、境界を決めているのがセルの言語だからである(§0)。セルが自列への単項テストだから行が区画になり、完全性と重なりが厳密に決まる。だから「送料の道具」でも「EC の道具」でもなく、上の形をした判定の道具である。
この領域を選んだのは、五つの性質が同時に成り立ち、道具の機能がその一つずつに対応して生えるからである。
1. **お金が動く** → 単位と税区分が型で(§2.1)、数値出力の `round` が必須で(§7.2)、書き忘れには円差を見せてから訊く(§11 E104)。
2. **原本が外にある** → 仕事は設計ではなく転記であり、最初の価値が「転記した瞬間に穴が出る」になる(§13)。`doc` が原本か検査結果に遡れない文を書かないのも同じ理由である(§1.6)。
3. **条件が絡み合う** → `if` 一本では書けず、網羅を目視で確かめられない。決定表と完全性検査に意味が出る(§4、§6)。
4. **期日で改定される** → 版、`--diff-base`、過去再生と差分(§10.4、§12)。
5. **書く人と決める人が違う** → `doc`(§1.6)と、決められないことを、入力を添えた問いに変える文面設計(§11 原則 2)。
五番目が満たされない規則も技術的には書けるが、この道具の半分(描画、差分、承認の通り道)は無用になる。
**捨てたもの**:
- **汎用のルールエンジンを名乗ること**:ループも状態も持たない以上、名乗れば期待を裏切る。P7 の言い換えでもある。
- **ワークフローとステートマシン**:複数段階と状態は P3(ルールは純関数)と正面から衝突する。時刻も在庫も引数で渡すという決定が、過去再生を意味あるものにしている(§10.3)。**§15.148 で開き直した**:状態を呼び出し側が持つ形に限って、ステートマシンの一歩は書ける。生成する関数は純関数のままである。ワークフローを動かすこと(段階そのもの、段階のあいだの待ち、外への副作用)は、いまも捨てたままである。
- **集合・集約の判定**:「明細のいずれかが冷蔵品」「カート全体で 3 点以上」。境界で平たくして渡す(§0、§15-6)。DMN の Collect を採らない判断(§4)と同じ線の上にある。
- **文字列のパターン一致**:`string` は等値と集合一致だけ。前方一致や正規表現はセルの言語を壊す(§2.1、§15-1 で実データを見てから再考する)。
- **スコアリング・最適化・機械学習**:証明できるのは「どの行に当たるか」であって、重みの妥当性ではない。検査の主張が成り立たない領域には行かない。
**按分(2026-09-13 に測った)**:粒度の原則(§5.4)は**配り方による**。「順に上限まで充てる」形は一明細ぶんの純関数として書け、反復は呼び出し側に残り、合計は構成上ぴったり合う(`tests/corpus/値引の充当.rule`。2,000 通りの明細で確かめた)。「定価の比で割り付ける」形は書けない — `明細 / 合計` が §7.4 の除算制限に当たる。比率を呼び出し側で作って率で渡す迂回も、率の刻みが実装上 1% 単位までしか書けないので実用にならない(下の「測って分かった欠陥」)。§15-7 はこの結果で閉じる。
## 1. 表面構文
### 1.1 ファイル形式
**決定**:行指向の独自テキスト形式。拡張子は `.rule`。エンコーディングは UTF-8(BOM なし)で、識別子は NFC に正規化する。表はパイプ区切りで、1 行が決定表の 1 ルール行に対応する。セル内改行は存在しない。
git diff の意味単位を業務ルールの行に一致させることが、この形式の唯一の設計目標である。
パイプ表は開発者も業務担当者も PR 上で読み慣れており、新しい読み方を教える必要がない。
キーワードは英語一種類だけを正とし、同義の綴り(日本語のキーワード、略語)は用意しない。
同じ意味の綴りが二つあると、grep と diff の両方で敵になる。
語彙は次の表で全部である(実装は `src/kw.rs` の一枚の表で、README のキーワード表と E009 の予約語判定はそこから引く)。
| 位置 | 語 |
|---|---|
| 行頭 | `rule` `description` `import` `enum` `group` `inputs` `outputs` `derive` `define` `table` `policy` `result` `examples` |
| 宣言の修飾 | `range` `round` `contract_only` `default` `step` |
| 方式 | `unique` `first` |
| セルと式 | `-` `not:` `none` `true` `false` |
| 型 | `money` `mass` `length` `area` `volume` `duration` `temperature` `sound` `rate` `number` `bool` `date` `string`。金額の属性は `incl_tax` `excl_tax` |
| 丸め | `up` `down` `half_up` `half_even`(向きの語は Java の RoundingMode と同じ) |
| 結果の関数 | `min` `max` |
| 組み込み | `import std/都道府県` |
| 記号 | `->`(出力列の境目)、`,`(集合の区切り)、`-`(任意の値)。`→` `・` `、` `,` は読めるが、`rulec fmt` が `->` と `,` に正準化する |
英語にするのは道具の語だけである。
識別子(規則名、入力、群、表の名前)とセルの中身は業務の語であり、日本語のまま書く(§1.3)。
この線引きで、字面の上でも道具の語(ASCII)と業務の語(日本語)が見分けられ、キーワードと名前の衝突(E009)は実質的に起きなくなる。
単位(`円` `銭` `万` `億` `g` `kg` `cm` `m` `%`)は語彙ではなく数値の表記なので、そのまま残す。
**覆した決定**:2026-09-13 まではキーワードも日本語(規則、取込、型、群、入力、出力、導出、定義、表、方式、結果、例、範囲、丸め、以外、無し)だった。
「一種類だけ」の原則は変えず、その一種類を英語に切り替えた。
理由は上の線引きと、日本語を読まない開発者にも構造が読めること。
移行は機械的で、コーパス、変異、golden、README の例をすべて書き換えた。
**捨てたもの**:
- **YAML / TOML への埋め込み**:二次元の表をネストで書くと列の対応が視覚から消え、P2 が死ぬ。
- **CSV**:型宣言とメタデータの置き場がなく、引用符の規則が業務担当者に説明できない。
- **DMN XML**:diff 不能。借りるのは意味論だけで、交換形式は借りない(前提のとおり)。
- **Markdown 文書内埋め込み(literate 形式)**:地の文と表の境界の規則が増えるだけで、表自体の可読性は上がらない。
### 1.2 ファイルの構造
一つの `.rule` ファイルが一つの規則(一つの生成関数)である。
構造は上から、ヘッダ、取込、型と群、入力、出力、定義と表の列、結果、例、の順で固定する。
全体のスケッチを示す。以降の節はすべてこの例を参照する。
```
rule 送料(shipping_fee) v4
description "通常便の送料。2026-04 の運賃改定を反映"
import std/都道府県
enum 会員区分(member_kind) = 一般(basic) default | ゴールド(gold) default | プラチナ(platinum)
group 遠隔地 = 北海道, 沖縄
inputs
届け先(dest) : 都道府県
重量(weight) : mass[g] range >=1g <=40kg
注文金額(total) : money[円, incl_tax] range >=0円 <=1000万円
会員(member) : 会員区分
outputs
送料(fee) : money[円, incl_tax] round up(10円)
define 大口 : bool = 注文金額 >= 3万円
table 基本送料
policy unique
| 届け先 | 重量 | -> 基本送料 : money[円, incl_tax] |
| 遠隔地 | <=2000g | 1200円 |
| 遠隔地 | >2000g | 1800円 |
| not: 遠隔地 | <=2000g | 800円 |
| not: 遠隔地 | >2000g | 1100円 |
table 負担判定
policy first
| 大口 | 会員 | -> 負担率 : rate |
| true | - | 0% |
| - | プラチナ | 50% |
| - | - | 100% |
result 送料 = 基本送料 × 負担率
examples
| 届け先 | 重量 | 注文金額 | 会員 | -> 送料 |
| 沖縄 | 2500g | 40000円 | 一般 | 0円 |
| 東京都 | 1999g | 12000円 | プラチナ | 400円 |
```
`examples` は実行される仕様である。
`rulec check` が参照評価器で全行を検証し、外れたら発火行つきでエラーを出す(E107)。
例の期待値は全出力について必須で、列の欠けはエラーにする(空セルと同じ理屈で、書き忘れを沈黙にしない)。
例は実装間の一致の盲点を破る唯一の楔でもある。
評価器と生成器が同じ誤りを共有すると一致は緑のまま通すが(複数出力の丸めが揃って落ちていた実測)、人が書いた期待値だけがそれと食い違える。
出力と同名の中間値が存在するなら、その出力の `result` は省略できる(表が一つだけの規則が典型で、複数出力でも出力ごとに効く)。
### 1.3 日本語識別子と ASCII 別名
**決定**:識別子は日本語を許す。ASCII 別名(丸括弧で併記)が必須なのは、公開 API に出る名前だけとする。すなわち規則名、入力、出力、そして入出力の型に現れる列挙型とその値である。内部に閉じる名前(群、表、定義、導出、内部でしか使わない型)の別名は任意で、省略すると生成コードも日本語識別子をそのまま使う。表の中身(セル)と例は常に日本語だけで書く。
Go の公開識別子は先頭が Unicode の大文字である必要があり、漢字とかなは大文字を持たないため公開名になれない[^go-export]。
つまり公開面には ASCII 名が必須で、その名前は宣言時に人間が一度だけ決めるのが、レビュー可能性ともっとも安い。
一方、非公開の位置には大文字要件がなく、漢字かなの識別子は Go でも Python でも合法である。
内部名にまで別名を要求する理由は、生成の都合には存在しない。
実測がこの線引きを与えた。
ゆうパック運賃 84 行に別名は 23 個要り、うち公開面に出るのは 5 個(規則名 1、入力 3、出力 1。都道府県は標準取込が別名込み)だけだった。
残り 18 個はヘッダを埋めるだけで、v0 の「全宣言で必須」はこの実測で捨てた。
公開面に別名が欠けているときは、エラー(E011)が不足している名前の一覧と書き足す位置を示す。
**捨てたもの**:
- **自動ローマ字化**:漢字の読みは辞書依存で、「届け先」が `todokesaki` になるか `delivery` になるかを機械が決めた結果は誰もレビューしない。
- **識別子を英語に限定**:セルまで英語になり、P2 の「業務担当者が読める」が崩れる。
- **全宣言での別名必須(v0 案)**:実測で 8 割が公開されない内部名だった。書かせる理由が測って消えた。
[^go-export]: Go 言語仕様の exported identifiers の定義による。先頭文字が Unicode uppercase letter(Lu)であることが公開の条件で、CJK 文字は Lo に属する。非公開識別子に大文字要件はないため、内部の関数名や型名は漢字かなのままで合法である。
### 1.4 Excel と CSV からの取り込み
**決定**:構文レベルでは考慮しない。`rulec import xlsx` と `rulec import csv` が一方向に `.rule` の草稿を生成し、人間がレビューして確定する。`.rule` から Excel への逆方向は作らない。
双方向同期を始めた瞬間に Excel が正本になり、GUI モデラへの一本道が始まる(P7)。
既存の Excel ルールは移行の入口としてだけ扱う。
### 1.5 git diff の読みやすさ
**決定**:`rulec fmt` を唯一の整形器とし、CI で `rulec fmt --check` を強制する(gofmt と同じ運用)。列は表ごとに内容最大幅へ揃え、文字幅は East Asian Width の W と F を 2、それ以外を 1 として数える(℃ と ℉ も 2。§15.134)。字句がそれだけで記号として読む全角の記号は ASCII へ直す。比較記号 ≦ ≧ < > は `<= >= < >` へ、足し算の + は `+` へ、出力列の印 `→` は `->` へ、セルの集合区切り `・` `、` `,` は `,` へ。字句が語を読めないところ(語の頭)の全角数字は半角へ直す。名前とセルの値以外に IME を要らなくするためである。整形は、字句が読む語(名前、演算子、数)を変えない。名前の中の字(`松+竹` の `+`、`第1案` の `1`)は名前の字なので触らず、`#` コメントと文字列の中も触らない(§15.257)。
既知のコストとして、列の最大幅が変わる編集は表全体を再整列させ、diff が表全体に及ぶ。
再整列は表の中に閉じるので許容し、PR レビュー向けには `rulec changes --base <rev>` が空白を無視した行単位の構造差分(追加行、削除行、変更行、新たに生じた隠れ)を出す。
列幅はヘッダを含む全行の内容最大幅で取る。
出力列のヘッダは型注記を含めて長く(`-> サイズ(size) : サイズ区分` で 28 桁)、値が数桁しかない列がヘッダ幅まで膨らむ。
M0 の実装で見えたこの不格好さは、承知の上で受け入れる。
ヘッダを幅から外す案と、型注記だけ溢れさせる案は、パイプの位置が行ごとに変わって本節の唯一の目標を壊すので捨てた。
そしてヘッダが幅の天井になることには実利がある。
日常の編集(値の修正、行の追加)はヘッダ幅を超えない限り再整列を起こさず、diff が 1 行に閉じる。
余白は、値の編集を局所 diff に保つための買い物である。
転記の現場で実害が出たら、型注記をヘッダの外の行に出す構文変更が指定の逃げ道で、実害が測れるまでは動かさない。
`#` コメントの正準位置もここで凍結する(実装で決めた規則の追認)。
表の行では、字句がコメントとして読むもの(字句が読む最後の `|` の後ろの、文字列の外の `#` から行末まで)を行末コメントとして切り離し、整形後の行に空白二つを挟んで付け直す。
コメントと文字列の中の `|` は、セルを区切らない(§15.267)。
表以外の行は、コメントを含めてそのまま残す(行末の空白だけ落とす)。
表の行の途中に `#` を書くと行の残りがコメントになって列数の不一致になり、構文エラーとして捕まる(静かに欠けることはない)。
数値リテラルは `3万円` `1000万円` の万・億接尾辞と `10_000` の下線区切りを許す。
桁区切りのカンマは、セルの集合区切りと衝突するので許さない。
版と履歴は git に任せる。
ヘッダの `v4` は人間向けの目印で、rulec は生成物ヘッダへの転記だけに使う。
CLI の `送料@v3` という参照は、git タグ `rules/送料/v3` を引く糖衣とする。
タグが無ければ `v3` をそのまま git のリビジョンとして引き、`rules/送料.rule@origin/main` はそのリビジョンにあるそのパスを読む(§15.42)。
### 1.6 検査済みの描画(rulec doc)
**決定**:`rulec doc` は、check を通った規則を markdown に描画する読み取り専用の副コマンドとする。恒等変換ではなく、検査器が知っていて字面に現れない事実を人間の言葉で添えることが仕事である。逆方向(markdown から `.rule`)は作らない。§1.4 と同じ強さの一方向原則で、ここに先に書いておく。
読み手は、コードが実現すべきものを理解し、確かめるために読む人である。業務の担当者、経理や法務、運用する人、PR をレビューする開発者がそうである(GitHub は `.md` を表として描画し、`.rule` はしない)。
チームで規則を承認する決まりがあるなら、その承認にもこの描画が使える。
規則を書く人は `.rule` を直接読むので対象でない。
§11 の五文面が書き手のための面だとすれば、これは読む人のための最初の面になる。
描画する事実は次のとおり。
群の展開と「この分割は全体を過不足なく覆う」という検査済みの言明(`近畿圏` の一語が 6 県を隠し、47 県の被覆は目視では確かめられない。検査器は知っている)。
表の依存(どの出力がどの列へ流れるか)。
隠れの三分類と既定行、W114 とガードの存在。
単位・税区分・丸め格子の列見出しへの畳み込み。
検証済みの例。
導出の式と到達範囲。
`contract_only` と `default` の宣言意図。
そして宣言行の行末コメント。丸めの仮置きの出典(§7.2)がここで読む人の目に浮上する。§16 第三(仮置きの権威化)への防波堤でもある。
検査器の出力に無い文章は書かない。
すべての行が、原本か検査結果に遡れること。
生成物としては扱わない。
コミットせず、CI が生成して PR に貼る(replay の markdown と同じ通り道)。
古い描画が正の顔をして残るのが最大の危険なので、長生きする成果物を作らない。
ヘッダに原本のパスとハッシュと rulec の版を刻み、貼られた先でも古さを検査できるようにする。
入力は check を通った規則に限り、通らない規則は描画しない(壊れた規則の綺麗な描画は嘘になる)。
描画の細目も固定する。
表のセルは AST から組み直さず原本の行から取り、描かれた全セルが原本に字面として現れることをテストが確かめる(創作を仕込むと落ちる腐敗検査つき)。
出力列の見出しだけは畳む(`→ 運賃(金額[円, incl_tax] / up(10円))`)。
これは §1.6 の「畳み込み」の指定どおりだが、字面一致のゲートの対象外になるので、代わりのゲートを持つ。
畳んだ見出しに現れてよいトークンは宣言に由来するもの(名前、単位、税区分、丸めのモードと格子)だけで、これもテストで縛る。
範囲と入力例は 万・億 で書き戻す(書き手が `100万円` と書いたものに `1000000円` と返すと、突き合わせに読み替えが要る)。
`not: 群` の補集合は展開せず、件数を添える(「以外: 遠隔地(残り 45 県)」)。
41 県の壁は目視の確認に耐えず、読む人の一次の問いは基数で答えられ、内訳は群の節の展開から引き算できる。
長い表は畳まない。
47 行の表は 47 行のまま出し、原本との行の対応を要約より優先する。
`rulec changes`(§1.5、残件台帳)とは別物である。
あちらは版と版の構造差分、こちらは今の状態の描画で、PR という行き先が同じでも成果物の種類が違う。
統合しない。
## 2. 型システム
### 2.1 型の一覧
- **列挙**:閉じた有限集合。`enum 会員区分(member_kind) = 一般(basic) | …`。開いた列挙は存在しない。値が増えたら完全性検査が既存の表を割る。これが P4 の完全性の動力源になる。値には `default` を付けられ、「専用の行を持たず、既定の行に落ちるのが意図」を宣言する(付ければ、その値がどのセルにも書かれていなくても、W111 は警告しなくなる。§11)。§1.2 のスケッチでは、プラチナだけが特別で残り二値は既定、という業務の形がこの印で読める。
- **群**:列挙の名前つき部分集合。`group 遠隔地 = 北海道, 沖縄`。セルで値と同格に使える。検査時には常に展開される。実測では 47 都道府県 × 7 サイズ = 329 通りが群で 42 行に縮み、さらに群の分割が 47 県の完全な分割になっているかを完全性検査が保証した。転記の作業そのものが検査される。
- **導出**:入力だけの線形結合(整数定数倍と加減のみ)で定める名前つきの数量・金額。`derive 残余 : money[円, incl_tax] = 商品合計 - 適用済割引 - 額面 range …`。`range` 宣言必須。数量のまま表の列に置ける唯一の中間値で、本体は §5.2 と §6.2。
- **数量**:次元と単位を持つ整数。`mass[g]`。同一次元内の単位換算はリテラルの厳密な整数換算だけ(`2kg` は `2000g` の糖衣)で、実行時値は宣言単位の整数一本である。次元は質量・長さ・面積・体積・時間の五つ(§15.83)。面積と体積は長さの積ではなく独立した次元で、`縦 × 横` は E103 になる。時間(`duration`)は期間の量で、暦日の `date` とは別物である。温度と音量は**順序だけの次元**で、比較と範囲には使えるが演算には使えない(§15.84)。
- **金額**:`money[円, incl_tax]`。通貨と税区分の二重 brand。税込と税抜の演算は型エラーで、変換演算子は用意しない。税率とその丸めは業務ルールそのものなので、必要なら表として書かせる。円と銭は別単位で、混ぜるには明示の丸めが要る。
- **率**:無次元。リテラル `3%` `0.5%` は正確な有理数として扱う。入力にする場合は `rate[step 0.1%]` と分解能を宣言させ、実行時表現を整数に保つ。
- **真偽**:`true` と `false`。
- **文字列**:等値と集合一致だけ。パターン一致は持たない(§15-1)。
- **日付**:タイムゾーンを持たない暦日。比較と範囲だけで、加減算は持たない(§15.84 で E048 として本当に止まるようにした)。JST 前提などの解釈は呼び出し側の契約に置く。内部表現は元期からの通算日とする。後者関数が +1 と一致するので、境界の ±1、区間の空判定、隣接の合流が整数の機構のまま正しい。`y×10⁴ + m×10² + d` の形は順序こそ保つが、月末と年末に実在しない整数の隙間を作り、`<=2026-03-31` と `>=2026-04-01` で敷き詰めた自然な表に偽の完全性エラーを出す。入力例とベクタの ±1 は 1 日を意味する。
- **optional**:`T?`。セルの `none` でだけ消費でき、式には現れない。式に来る前に表で場合分けさせることで、null を算術から締め出す。範囲と刻みは包む型のもので、値が在るときの値に掛かる(§15.201)。出力の列では、セルの `none` が値の無いことを答え、値のあるセルのリテラルと丸めの刻みは包む型で読む。数の optional の出力にも丸めの宣言が要る(§15.249)。
- **未丸め**:数量と金額の演算が作る、静的スケールつきの中間型。ユーザが書く構文は無く、推論でだけ現れる(§2.3、§7)。
完全な次元解析はやらない。
次元の掛け合わせ(円×g のような合成次元)が業務ルールに現れたら、それはモデリングの誤りとしてエラーにする。
必要なのは brand の混同防止までで、そこで止める(前提のとおり)。
これは掛け算にも割り算にも効く。`縦 × 横` も `重さ ÷ 2m` も E103 で、積そのものが答えになる面積や体積は、入力として受け取るか表で引く(§15.83)。
### 2.2 宣言場所と範囲宣言
**決定**:型はファイル内宣言と `import` の二段だけ。`import` はパスを引数に、型、群、単位の宣言だけを取り込む。表と定義は取り込めない。組み込みの `std/` 名前空間に、都道府県(47 値、ASCII 別名込み)、質量(g、kg)、長さ(cm、m)、金額(円、銭)、率、日付を同梱する。都道府県の別名はヘボン式で長音を落とす表記(Hokkaido、Oita)で凍結した。訓令式にしないのは、自治体の英語表記の慣行に合わせるためである。2026-10-04 に、ほかの十二か国の一段目の区分(`std/us/states` など)と、都道府県の英語の名前 `std/jp/prefectures` を足した(§15.182)。
数量、金額、率、日付の入力と、すべての導出には `range` 宣言を必須とする。
入力の範囲は三役を兼ねる。
オーバーフロー証明の前提(§7)、完全性検査の全体集合(40kg 超の行を書かなくてよい根拠)、生成コードの入口ガード(§8)である。
上限のない金額入力は業務にも存在しないので、書かせることが仕様の発掘になる。
導出の範囲は検査の軸の全体集合になり、健全性のための包含条件を負う(§6.2)。
**捨てたもの**:**中央型レジストリ**(リポジトリ横断の型サーバのようなもの)。ファイルと取込で足りる規模の道具に、名前解決のインフラを持ち込まない。
### 2.3 演算規則
| 式 | 結果の型 | 備考 |
|---|---|---|
| 金額 ± 金額(単位・税区分が同一) | 金額。丸め済+丸め済は丸め済、未丸めが混ざれば未丸め | 税込と税抜の混合は E103 |
| 金額 × 整数定数 | 左と同じ丸め状態 | 厳密 |
| 金額 × 率 | 未丸め金額 | スケールが積になる |
| 金額 ÷ 正整数定数 | 未丸め金額 | |
| 金額 ÷ 金額定数(同単位・同税区分) | 未丸め無次元数 | 「100円につき1点」用 |
| 数量 ± 数量(同次元) | 数量 | 単位換算はリテラル位置のみ |
| 数量 × 率、数量 ÷ 定数 | 未丸め数量 | |
| 数量 ÷ 数量定数(同次元) | 未丸め無次元数 | 「100gごとに」用 |
| 率 ± 率、率 × 率 | 率 | 厳密な有理数 |
| 比較 `< <= > >= =`(同型) | 真偽 | 未丸め同士も厳密比較(共通スケールへ静的に揃える) |
| 最小(a, b)、最大(a, b)(同型) | 同型 | |
| 丸めモード(式, 格子) | 丸め済 | 未丸めから戻る唯一の道 |
禁止するもの:金額×金額、税込±税抜、異次元の混合、**変数による除算**、日付の加減算。
変数除算を禁じるのは、スケール(§7)を静的に決めるためである。
割る数が業務データなら、それは率か定数表のどちらかとして表に現れるはずで、実際 P1 から P2 の領域(送料、クーポン、ポイント)では、変数除算が要る計算は一つも挙がらなかった。
挙がったら §15 で扱う。
### 2.4 丸め済と未丸めを別型にする案の是非
**決定**:採用する。ただしユーザに未丸め型を書かせない。演算が値を自動で未丸めへ持ち上げ、丸め関数だけが丸め済へ戻す。
「丸め前の値が出力に漏れない」(P4)は規約では守れず、型でしか守れない。
一方で型を二重にする複雑さは実在するので、その支払いを書き手ではなく検査器に寄せる。
書き手が未丸め型に出会うのは、丸めを書き忘れたときのエラーメッセージの中だけである(E104、§11)。
**捨てたもの**:**丸めを暗黙適用して型を一本化する案**。表の出力セルに `805円` と書いたとき、出力宣言が `up(10円)` だからと黙って 810 円にする道具は、差分を見る道具として自殺である。リテラルが格子に載っていなければエラーにする(E106)。
## 3. セルに書けるもの
**決定**:セルの単項テストは次の七種で全部である。空セルは構文エラー。テストの対象になる列は、入力、導出、真偽と列挙の中間値である(§5.3)。
1. `-`:don't-care。任意の値に一致する。don't-care の表記はこれ一つだけで、空欄を don't-care と解釈しない(書き忘れと区別がつかなくなる)。
2. リテラル一致:列挙値、`true` `false`、`2000g`、`1200円`、`2026-04-01`、`"WELCOME10"`、`none`。数量と金額のリテラルには単位が必須で、裸の `2000` はエラー(「2000g ですか」と聞き返す)。
3. 集合:`北海道, 沖縄`。いずれかに一致。要素はリテラルか群名。
4. 補集合:`not: 北海道, 沖縄`。集合にも群にも付けられ、意味は宣言された全体集合に対する補集合で、行単体で閉じている。
5. 比較:`<=2000g` `>2000g` `>=2025-04-01` など四種の演算子。全角の ≦ ≧ < > も受け、fmt が正規化する。
6. 区間=比較の並記:`>=1000円 <20000円`。空白区切りは連言。空区間は型エラー。
7. optional 列に対する `none`。比較、リテラル、集合は値が在るときだけ一致し、`none` には `-` と `none` と `not:` が一致する(`not:` は宣言された全体から挙げた値を除いたもので、値の無いこともその全体に入る。§15.201)。
### 3.1 境界の開閉の表記
**決定**:範囲は必ず比較演算子で書く。`..` を使った範囲記法は、片側でも両側でも構文エラーにする。
当初スケッチの `..2000g` と `2000g..` は、この道具がいちばん守りたい場所に曖昧さを残す。
「2000g まで」は日本語では両端含みに読めるが、`..2000g` を含みに解釈すると隣の `2000g..` と 2000g ちょうどで重なり、排他に解釈すると業務の読みと 1g ずれる。
数学の `[0, 2000)` は開閉が正確だが、括弧の向きの規則を業務担当者に教えることになる。
比較演算子は全員が既に読めて、開閉の解釈が一意で、教えることが何もない。
隣接する行を `<=2000g` と `>2000g` で書けば敷き詰めは目で追える。
`<=2000g` と `>=2000g` と書き間違えれば、重なり検査が「重量=2000g が両方に当たる」という入力を添えて捕まえる(E105)。
境界の曖昧さを構文で禁じ、貼り合わせの誤りを検査で拾う、という分担である。
**捨てたもの**:
- **半開区間を既定にする案**(`a..b` を a 以上 b 未満と定める):敷き詰めは美しいが、「まで」の直感と静かに 1 単位ずれる。静かにずれる仕様は、この道具では最悪の性質である。
- **セル内の区間の合併**(`<500g, >30kg` のような書き方):行を二つに分ければ書ける。実測(運賃表とクーポンの転記)で必要になった箇所は 0 件で、v1 から外すことが決着した(旧 §15-1)。
- **列をまたぐ条件をセルに書くこと**:セルは自列への単項テストに限る。複合条件は `define` で名前を付けて真偽列にするか、入力の線形結合なら `derive` にする(§5.2)。この制限が検査の決定可能性の土台になる(§6)。実測でいちばんきつい試金石は、ヤマトの「3 辺計と実重量を比較し、大きい方のサイズの扱いになる」規約だったが、列ごとの単項テストと `first` でそのまま書けた。50cm/8kg の荷は 60 サイズ行を重量で外れ、80 も外れ、100 で当たる。
- **正規表現・前方一致**:クーポンコードの実形式を見るまで入れない(§15-1)。実測の公開規約では、文字列そのものが列に一度も現れなかった。
- **セル内のパス式・量化子**(`注文.配送先.都道府県`、`some … satisfies …`):実在の DMN 業務資産(伊 ANAC の調達規則)はセルが任意の FEEL 述語で、我々はヒットポリシー以前にセルの言語で受け付けない。それでよい(§0)。入れ子は境界で平たい入力に潰してから渡し、セルには入れない。射影の宣言と生成という拡張候補は §15-6。
### 3.2 出力セルに書けるもの
**決定**:出力セルにはリテラルのほかに、名前(入力、導出、定義)を書ける。式は書けない。
optional の列(`T?`)には `none` も書け、値の無いことを答える。`T?` でない列に書いた `none` は E103 である。`T?` の列には、`T` の値の名前も、`T?` の値の名前も書ける(§15.249)。
リテラル限定では書けない規則が実在する。
楽天の複数クーポンは「%引きクーポンを先に適用し、その後円引きクーポンを適用する。パーセンテージ割引は商品の元価に対して計算される」という規約で、クーポン種別によって割引の基準額が変わる。
率引き行の出力は「元価 × 割引率」という計算値であり、リテラルでは書けない。
リテラル限定のまま逃げるなら率クーポン規則と額クーポン規則にファイルを割ることになるが、どちらを呼ぶかの判断がホスト言語へ漏れる。
式そのものをセルに許さないのは、表を眺めて桁が読める性質を保つためである。
計算には `define` で名前を付けさせ、セルからは一往復で辿れるところに置く(§5.4 のスケッチにこの形がある)。
名前の丸め状態は列に合流し(どれかが未丸めなら列は未丸め)、E106 の静的な格子検査はリテラルにだけ適用する(§7.2)。
領域解析は入力側の列だけで行われるので、この拡張は完全性にも重なりにも影響しない。
名前の先の `define` がさらに前の定義を参照する連鎖は、構文上は無制限である。
それでも機械的な深さ制限は置かない。
コーパスの実測は深さ 1 で、根拠なく選んだ上限は恣意にしかならないからである。
深い連鎖が実表に現れたら、そのとき件数を数えて W 系の lint として決める。
**捨てたもの**:
- **出力セル内の式**:表が計算式の置き場になり、P2 の可読性が崩れる。名前を一つ挟む往復が、複雑さの上限として働く。
- **表出力の名前参照**:`result` で書けることと重複する。拡張は実測が要求した最小に留めた。
## 4. ヒットポリシー
**決定**:採用は二つ。`policy unique`(DMN の Unique)と `policy first`(DMN の First)。既定は `unique`。単一ヒットの二方式だけを採るので、完全性は全表で常に必須とし、宣言項目にしない(この不変が単一ヒットの性質であることは本節末尾に記す)。
`unique` は解析がもっとも強く効く方式で、行の重なりはすべてエラーになる(E105)。
順序に意味がないので、行の並べ替えが意味を変えない。
既定を `unique` にするのは、順序依存を書き手に意識して選ばせるためである。
実測でも二方式は同一ファイルの中で共存した。
ヤマトのサイズ判定は階段なので `first` が合い(`unique` で書くと `>60cm <=80cm` の区間並記になり、同じ境界を二度書くことになる)、運賃表は重なりようがないので `unique` である。
`first` は「例外を先に、一般則を後に」という業務の書き方をそのまま受けるために要る。
最後に全部 `-` の行を置く書き方が既定行になる。
`first` でも完全性と、先行行に完全に覆われて決して当てはまらない行(E102)はエラーである。
部分的な隠れの扱いは、M0 の実装が教えた事実で設計を直した。
v1 は「隠れはすべて入力を添えた注記(W105)」としていたが、階段表ではそれが信号にならない。
ゆうパックのサイズ判定は 7 行の階段で 21 対すべてが重なり、しかも階段は出力が全部違うのが正常なので、「出力が同じ重なりは冗長」という文言の区別も効かない。
`--diff-base` も、階段への 1 行追加で良性の新規対が 6〜7 個まとめて出る。
そこで `first` の隠れ対を三つに分類する。
- **階段**:後の行の領域が前の行の領域を包含する(交わりが前の行の領域そのもの)。階段と既定行がこれで、`first` を選んだ時点で書き手が意図している構造である。既定では件数だけ報告する。
- **同じ答え**:包含でない部分交差だが、両行の出力が同じ。「〜なら不可」を並べる守り行が互いに交わる形で、どちらが勝っても値は変わらない。既定では件数だけ報告する。
- **要確認**:包含でない部分交差で、出力が異なる。順序が円を変える唯一の形で、これだけを入力を添えて一覧する(W105)。合意した五つ目の文面「意図通りですか」の仕事は、この分類が引き継ぐ。
この分類で、ゆうパックの 21 対は全部が階段、クーポンの適用可否の 10 対は階段 4 と同じ答え 6 になり、どちらも一覧はゼロになる。
例(§1.2)の負担判定の 行1×行2(0% と 50%)は要確認として残る。
黙る二分類には安全網が残っている。
階段の並びの事故は包含の向きが逆転して当てはまらない行になるから、注記ではなく E102(エラー)が捕まえる。
同じ答えの対は、片方の出力を後で変えた瞬間に要確認へ遷移して一覧に現れる。
全対は `--show-shadow` で一覧でき、既定の出力は「隠れ 21 対(階段 21、同じ答え 0、要確認 0)」という件数の注記になる。
CI の `rulec check --diff-base <rev>` は要確認の対の集合で差分を取り、新たに生じた分(同じ答えからの遷移を含む)だけを浮かせる。
対の同一性は行番号ではなく、セルの正規形から作った鍵で照合する。
行を挿入しても fmt が再整列しても鍵は変わらず、既知として伏せた対があるときは伏せた件数を言う。
隠れの既知集合をファイルに書かせる案(行番号での承認リスト)は、行の挿入で番号が静かにずれる罠なので捨てたままである。
状態は git の履歴に置き、rulec 自身は無状態を保つ。
逆向きの検査(重なりが一つもない `first` に `unique` を提案する W110)は、実装の初回から有用だった。
当初スケッチにあった `完全性 = 必須` という宣言は落とした。
宣言できるなら宣言し忘れが生まれ、既定 OFF の完全性検査は P4 に反する。
穴を許したい表は書けない。それがこの道具である。
公開の実使用も測った(2026-08、GitHub のコード検索。`.dmn` 17,536 ファイル、数字は一致ファイル数)。
FIRST 4,568、UNIQUE 2,384(宣言なしの約 8,000 は既定の UNIQUE なので、実質シェアは 6 割前後)、COLLECT 1,720、PRIORITY 518、ANY 326、RULE_ORDER 2、OUTPUT_ORDER 0。
採用した二方式が実使用の一位と二位で、捨てた側では COLLECT だけが突出する(PRIORITY と ANY の合計の 2 倍)。
ただしこの数字には限界が三つある。
一ファイルに複数の表が入りうること、コード検索の順位が非ランダムなこと、そして企業の DMN は SaaS の中にあって公開リポジトリに出てこないこと。
一番知りたい母集団が、最も薄い。
**捨てたもの**:
- **A(Any、326 件)**:出力が同じなら重なりを許す方式。今日同じでも明日の編集で片方だけ変わる。重なりは冗長でも検出したい対象(Calvanese らの言う redundancy)で、表現力も増えない。重なりの出力が一致している表は、行をそのまま `first` の表として書けば同じ関数になる。
- **P(Priority、518 件)**:優先順位が出力値の順序という別の場所に書かれる。優先は紙面の上下で見えるべきで(P4)、行を優先順に並べ替えれば `first` がそのまま P である。表現力は増えない。
- **C(Collect)と集約**[^dmn-multi]:実在する(上の調査で 1,720 件)。ただし出所を深いページで 200 件掘ると 51% がエンジンやツール由来で、業務モデルの候補は 26%、うち 6 割は伊 ANAC(反汚職庁)の一つの調達規則集だった。その実物と英 HMCTS のタスク設定が C で行っているのは「当てはまる項目を全部集める」=検証であって、金額の集約ではない。つまり C の採否はヒットポリシーの選択ではなく、検証という別の仕事を範囲に入れるかどうかの決断である。単一の値を計算する道具でいる限り、要らない。前回ここに書いた「実測のクーポン転記でも集約は要らなかった」は撤回する。書けるように書いた自選の二表から、必要性の不在は導けない(測り直しは §15-8)。クーポンの重ね掛けが粒度の構成で書けること自体(§5.4)は、構成を示した事実として残る。
C を入れる日に何を失うかも、ここに記録しておく。
労働基準法の割増賃金は「重なると足す」の教科書例に見える。
時間外 25%、法定休日 35%、深夜 25% で、厚労省の計算例には休日深夜 1.60(1.35 + 0.25)が明記されている。
ところが法定休日労働には時間外という概念がなく、8 時間を超えても 35% のままなので、素朴な合計は休日+時間外に 60% という違法な答えを出す。
正しく書くには時間外の行に「法定休日 = 偽」という排他を書き込むしかなく、それを書いた瞬間、表は単一ヒットと同じ仕事をしている。
違いは一点だけで、単一ヒットならその排他の正しさを完全性と重なりの検査が確かめるのに対し、合計方式では重なりが仕様そのものになるため、どちらの検査も意味を失う。
この道具の検査能力は、単一ヒットの上に立っている。
検証の形の需要も、表現力だけなら今日の道具で書ける。
検査項目ごとに真偽の出力を持たせれば表になり、当たった項目を集めるのはホストの一行で、各項目は網羅と重なりの検査を受ける。
C が買うのは簡潔さであって表現力ではなく、支払うのは排他と網羅の保証である。
この取引を受けるかどうかが、上の「範囲の決断」の中身になる。
本節の「完全性は常に必須」と「表は値を一つ返す」は、単一ヒット方式の性質として述べている。
C を入れる日は、この二つの不変を検証の意味論のために作り直す日で、ヒットポリシーを一つ足す変更では済まない。
[^dmn-multi]: DMN の multiple hit にはほかに R(Rule order)と O(Output order)があるが、公開の `.dmn` 17,536 件で R は 2 件、O は 0 件だった。標準に載っていても誰も使わないものは実在する。DMN を全部実装しないという方針の、実測の裏づけでもある。
## 5. 表の合成と中間値
### 5.1 線形パイプライン
**決定**:合成はファイル内で完結する線形パイプラインとする。`derive`、`define`、`table` は上から順に名前を導入し、後のものは前の名前だけを参照できる(define-before-use)。最後の `result` が出力を束ねる。DRD に相当する図は作らない。
前方参照を許さないので循環は構文的に存在せず、ファイルを上から読めば依存が読める。
ファイルの並び順そのものが DRD である。
規則が図を要するほど育ったら、それは規則の分割が要る徴候として扱う。
実測では運賃が実際に 3 段の連鎖(県から地帯へ、地帯とサイズから運賃へ)を要求した。
発地と着地の対応は静的な部分集合である群では表せず、表の連鎖でだけ書ける。
最大の表は 144 行、列 3 で、§6.3 の設計目標の内側の規模だった。
表は複数の出力列を持てる。
意味はタプルで、各出力が独立の名前と型を持つ。
`->` は入力と出力の境目を一度だけ示す印で、最初の出力列に付ける。
境目より後ろの列は、印の有無にかかわらずすべて出力である(パーサは余分な `->` も受け、fmt が正準形に畳む)。
列ごとに印を要求する v0 の形は、二列目の印の書き忘れが列を黙って入力へ化けさせた実測(M1 の例の検査)で捨てた。
例も同じ綴りで、出力が複数なら期待値の列を出力の数だけ並べる。
規則から規則を呼ぶことはできない。
上流の規則の答えが要るなら、それは入力として宣言し、呼び出し側が渡す。
これは P3 の「外から来るものはすべて入力」の合成への一般化で、二つの利益がある。
規則単位で過去再生が閉じる(送料の replay にクーポンの実装が要らない)ことと、規則間の版の組合せ爆発を呼び出し側の責任に置けることである。
### 5.2 導出
**決定**:「導出」を入力、定義、表出力に並ぶ第四の名前つき値として一級にする。右辺は入力だけの線形結合(整数定数倍と加減のみ)で、上流の表の出力も定義も使えない。`range` 宣言必須。数量・金額のまま表の列に置ける。
```
derive 残余(margin) : money[円, incl_tax] = 商品合計 - 適用済割引 - 額面
range >=-110万円 <=100万円
```
なぜ要るか。
実測で、値引き後の金額で判定する規約が二件見つかった。
楽天は送料無料ラインをクーポン利用前の購入合計で判定する一方、39 キャンペーンのポイント条件はクーポン適用後の金額で判定する。
同一プラットフォームの中で、特典ごとに判定の前後が違う。
Yahoo には「値引き額が商品合計を上回るか同額ならクーポンは適用されない」という判定がある。
どちらも「入力 − 入力」の線形結合で、これを列に書けないと、値引き後という一番間違えやすい引き算が呼び出し側の裸の一行になり、単位も税区分も丸めも検査されない。
OSS の EC 実装にも同じ形がある。
Saleor はカタログ割引を先に計算し、その後の価格にオーダー割引を適用する。
値引き後の金額を次の判定と計算が使う形の、これで三例目である。
なぜ線形結合までか。
実測で列に要求されたのは線形結合だけで、率 × 金額のような非線形は `result` と `define` にしか現れなかった。
そして線形結合なら、検査の決定可能性を区画の代数のまま保てる(§6.2)。
整数定数倍と加減は丸め済のまま閉じて厳密な int64 で計算できるので、丸めの新しい場所も生まれない(§7.1)。
範囲宣言は検査の軸としての全体集合になる。
宣言は、入力範囲から区間演算で計算した到達区間を含まなければならない(E112)。
含まないと完全性検査が実際に到達する値を見ず、不健全な緑が出るからである。
到達区間より広い分は実現不能領域としてふるい落とされるので害はない(§6.2)。
### 5.3 列に使える値と真偽定義の原子
**決定**:表の列に使えるのは、入力、導出、そして真偽と列挙の中間値(定義と表出力)である。数量と金額の中間値(表出力と一般式の `define`)は列に使えず、`result` と式の中でだけ使える。真偽の `define` の原子は二形。セルの単項テスト(§3)を入力か導出に適用したものと、導出で差の取れない型(日付、列挙)に限った同型の入力・導出どうしの比較(`A期限 <= B期限`。日付は四種の比較、列挙は等値。型の一致は §2.3 に従う)である。数量・金額・率どうしの直接比較は E113 のままとし、差を導出として宣言してから定数と比べさせる。そのほうが厳密に解析できるからで、精度の規律(下記)が数値には導出という正しい家を既に持っている。連言、選言、否定を許す。名前どうしの比較はセルには書けない(セルは単項のまま)。
v0 草案にはここに抜け道があった。
数量の定義(`適用後金額 = 注文金額 − 割引額`)を真偽の定義(`適用後金額 >= 3980円`)が参照でき、その真偽が列に置けたので、`x − y >= c` という半空間が列に入り込み、§6.2 の「定義は DNF 展開で区画の和になる」が原子の形の前提ごと崩れていた。
原子を入力と導出に制限することでこの穴を塞ぐ。
線形結合が要るなら導出として宣言させ(E113 が書き換えを提示する)、表出力との比較が要るなら、その表に真偽の出力列を足すのが正しい書き方である。
名前どうしの比較の原子は、適用順序の規則が要求した。
§5.4 の「期限が近い順」は二つの日付入力の比較そのもので、日付に減算が無い以上、導出(差を取って定数と比べる形)では書けない。
これを許しても検査は壊れない。
真偽の定義は解析では自由な真偽軸として扱われ(§6.2)、原子の形が何であれ扱いは変わらないからである。
一方、数量の定義への参照を禁じる E113 は残る。
自由軸として健全に扱えるかどうかの問題ではなく、導出にすれば厳密に解析できるものを、怠けて不透明にさせないという精度の規律である。
### 5.4 規則の粒度
**決定**:規則の粒度は「一回の判定」に固定する。可変個数のものを規則の中で畳み込まない。リスト型も集約も導入しない。
クーポンは可変個数で、併用の順序があり、枚数の制約がある。
これを解くのはリスト型ではなく粒度である。
規則は「このクーポン 1 枚は適用できるか、いくら引くか」を答える純関数で、重ね掛けの順序と反復は呼び出し側が持つ。
順序が呼び出し側にあるのは欠点ではない。
順序こそが業務判断であり、規則の中に隠すべきではないからである。
適用順序そのものも規則として書ける。
楽天の「%引きが先、円引きは有効期限が近い順」は、`(A種別, B種別, A期限, B期限) → 先/後` という二引数の純関数になり、呼び出し側はこれを比較関数としてソートに渡す。
反復はホスト、判定(比較を含む)は規則、という分担が一貫する(期限の比較は、真偽定義の名前どうしの比較の原子で書く。§5.3)。
有効期限の比較が要るので、日付型は v1 に入る(旧 §15-7 は逆向きに決着した)。
併用の制約も入力で書ける。
「対象商品指定クーポンは同一商品に 2 枚以上使えない」は、`同商品適用済 : bool` を入力にすれば表になる。
状態はホストが持ち、P3 のままである。
クーポン 1 枚の規則のスケッチを示す。
導出(§5.2)と出力セルの名前(§3.2)もここに現れる。
```
rule クーポン一枚(coupon_step) v1
description "クーポン1枚の適用可否と素割引。重ね掛けの順序と反復は呼び出し側が持つ"
enum クーポン種別(coupon_kind) = 率引き(percent) | 額引き(fixed) | 送料無料(free_ship)
inputs
商品合計(subtotal) : money[円, incl_tax] range >=0円 <=100万円
適用済割引(applied) : money[円, incl_tax] range >=0円 <=100万円
種別(kind) : クーポン種別
割引率(rate) : rate[step 1%] range >=0% <=100%
額面(face) : money[円, incl_tax] range >=0円 <=10万円
同商品適用済(dup) : bool
outputs
可否(ok) : bool
素割引(raw) : money[円, incl_tax] round down(1円) # 端数処理は公開規約に記載なし。切り捨てに仮置き
derive 残余(margin) : money[円, incl_tax] = 商品合計 - 適用済割引 - 額面
range >=-110万円 <=100万円
define 率割引 : money[円, incl_tax] = 商品合計 × 割引率
table 適用可否
policy first
| 種別 | 残余 | 同商品適用済 | -> 可否 : bool |
| - | - | true | false |
| 額引き | <=0円 | - | false |
| - | - | - | true |
table 素の割引
policy unique
| 可否 | 種別 | -> 素割引 : money[円, incl_tax] |
| false | - | 0円 |
| true | 率引き | 率割引 |
| true | 額引き | 額面 |
| true | 送料無料 | 0円 |
```
二つの出力はどちらも同名の中間値を持つので `result` は省略されている(§1.2 の糖衣)。
`素割引` 列は、リテラル(丸め済)と `率割引`(未丸め)が合流するので列として未丸めになり、出力宣言の `down(1円)` が一度だけ掛かる(§7.2)。
畳み込みそのものを宣言にする案(順序規則、一段の規則、アキュムレータを指定し、参照評価器が畳み込みまで実行する形)は、ここで検討して v1 では見送った。
宣言にすれば検査の網(実装間の一致、過去再生)が畳み込みまで届き、手書きの fold のバグはこの網のどれにも掛からないから、魅力は本物である。
見送りの決め手は、単一アキュムレータの畳み込みが、動機になった当のクーポンをまだ表せないことにある。
併用制約の入力 `同商品適用済` は「これまでに適用したクーポンの対象商品の集合」から来る状態で、残額一本のアキュムレータからは導けない。
塞ぐ道は二つしかない。
段ごとにホストが状態を注入する形は、畳み込みが閉じなくなり、評価器が実行できるという肝心の利点そのものが消える。
アキュムレータに集合を許す形は、コレクションが型システムに入り、P7 への圧力になる。
どちらも、実畳み込みの根拠が楽天の一件だけの今に払う理由がない(採否の測定は §15-5)。
それまでの緩和として、**連鎖の例**の慣行を置く。
連続する `examples` 行で一続きの場面を書き、行 k の `適用済割引` に行 k−1 までの結果を手で入れる。
評価器が各段を検査し、ホスト両実装の端から端までのテストが同じ表を fixtures として共有できる。
新しい構文はゼロで、畳み込みの「データとしての仕様」だけが規則側に残る。
**捨てたもの**:
- **DMN DRD 相当の規則間依存グラフ**:GUI モデラの引力の中心。ファイル内線形で書けない規則は分割の合図とみなす。
- **規則間 `import`(表の再利用)**:共有したくなるのは型と群で、実際に共有できるのもそこまで。表の共有は「どの版を共有しているのか」問題を持ち込む。
- **リスト型と規則内の畳み込み**:可変個数は粒度で規則の外に出す(§5.4)。値引きの明細行への按分は、逐次充当型ならこの粒度で書ける(§15-7 で測定済み)。比率按分が書けないのは粒度ではなく除算の制限による(§7.4)。
## 6. 検査の実装方針
### 6.1 何を検査するか
先に、全検査に共通する保証の向きを固定する。
**見逃さない**。
緩めるときは常に過剰報告の方向へ緩め、不健全な緑(実際には成り立たない「検査済み」)はどの検査も出さない。
この改訂で入った変更(導出の軸化、起きうるかのふるい、実現可能性未検証の札)はすべてこの向きに倒してあり、以降の各所で個別に確認する。
- **E101 完全性**:どの行にも当たらない入力領域の検出。導出が入っても健全なまま(§6.2)。
- **E102 当てはまらない行**:どの入力にも到達されない行の検出(`first` では先行行への完全被覆、`unique` では到達不能な上流値も含む)。
- **E105 / W114 / W105 重なり**:同じ入力に複数行が当たる状況の検出。`unique` では実在を確認できた重なりだけが E105(エラー)で、確認も反証もできなかった重なりは W114(警告)に落ち、生成コードのガードが引き受ける(§6.2、§8.1)。`first` は隠れ注記(W105)。出力が異なる重なり(矛盾の恐れ)と同じ重なり(冗長)は文言を分ける。Calvanese らの contradiction と redundancy の区別[^calvanese]に対応する。偽陽性が出うるのは導出が絡む場合に限る(§6.2)。
[^calvanese]: Calvanese, Dumas, Laurson, Maggi, Montali, Teinemaa. "Semantics and Analysis of DMN Decision Tables" (BPM 2016, arXiv:1603.07466)。欠落ルールと重複ルールの検出を決定表の幾何として定式化している。検出対象の分類はこの論文に合わせ、実装は下記のとおり独自に決める。
### 6.2 区間の格子で足りるか
**決定**:足りる。SMT ソルバは v1 では使わない。検査の空間を「各入力と各導出を独立の軸とする圧縮座標」に取り、すべてを**区画(直積領域)の有限和**の代数として実装する。導出の従属関係(残余 = 商品合計 − 適用済割引 − 額面 という等式)は空間には入れず、**起きうるかのふるい**として発見の報告段で回収する。
まず区画の和に閉じる根拠は、§3 と §5.3 の構文制限がちょうど決定可能性の境界に置いてあることによる。
- セルは自列への単項テストしか書けないので、行は「軸ごとの部分集合の直積」、すなわち区画の有限和になる(`not` と集合は座標の合併)。真偽の `define` は自由な真偽軸として扱い、原子の中身は領域解析に入れない。当初の設計は DNF 展開で代入する予定だったが、M0 の実装は自由軸で出荷され、これを設計に昇格する。真も偽も到達可能とみなすのは過剰報告の向きで、完全性の見逃しは生じず、E102 は相関起因の死を黙って見送るだけで偽の「決して当たりません」を出さない。行が直積のままなので、隠れの包含判定の速い形(§6.3)の前提も保たれる。
- 列に使える中間値は真偽と列挙に限られるので、中間値の各値の逆像(軸空間でその値になる領域)も帰納的に区画の有限和である。上から方式の「行 i が勝つ」も先行行の補集合との交差で、圧縮座標上では補集合も区画の和に閉じる。
閉性自体は自明(圧縮格子の任意の部分集合は区画の和)で、問題は大きさと、導出を独立軸にしたことで空間に混ざる「実在しない点」(等式を満たさない座標)である。
後者をふるいが引き受ける。
- **ふるい**:区画 B と導出 d = f(入力) について、f を B の入力部分の区間に区間演算で適用した結果が B の d 区間と交わらなければ、B は実現不能。線形な f の区間評価は厳密で、判定は導出 1 本あたり定数時間。ソルバは要らない。
- **完全性(E101)は健全なまま**:軸の全体集合は導出の宣言範囲で、E112 が到達区間の包含を強制するので、実空間の穴は必ず軸空間の穴として現れる。ふるいは「実現不能と証明できた穴」だけを報告から外す。したがって偽の緑は出ない。証明しきれない実現不能の穴が報告に残ることはある(過剰報告の向き)。この議論は、軸が列の値のとる刻みで切られていることを前提にしている。分数の定数を掛ける導出(`amount × 10%`)では §15.190 までそうなっておらず、整数の境目のあいだの値に座標が無かった。
- **重なり(E105/W114/W105)**:交わりの区画にも同じふるいを掛け、実現不能と証明できた発見は出さない。残った発見は、その入力を実際に組み立てて評価器で確認できたかどうかで重大度を分ける。構成できた重なりは実在が確認された矛盾で、`unique` では E105(エラー)。構成できず、実現不能の証明もない重なりは、`unique` でも W114(警告)に落とす。未証明の存在で CI を止めないためである。ただし警告に落とすだけでは足りない。行の排他は生成器が分岐の並びを正当化する前提でもあり、未検証のまま何もしなければ、その重なりが万一実在したとき `unique` が宣言なしの `first` に静かに化ける。そこで生成コードに**ガード**(当該行対の実行時の排他検査)を入れる(§8.1)。両方の行の条件を評価し、同時に当たる入力が来たら黙って先の行を選ばずエラーを返す。静的に閉じなかった証明義務を実行時検査が引き受ける分担は、入力範囲の入口ガードと同じである。出力が構文的に同一の行対は、どちらが選ばれても値が変わらないのでガードを省く。`first` の隠れは宣言された順序が解決するので、従来どおり W105 の注記のままでガードも要らない。導出が複数あって入力を共有すると、導出ごとに独立な区間のふるいは従属を見ないため W114 が出やすくなるが、外れる向きは常に過剰報告で、見逃しは生じない。
- **当てはまらない行(E102)**:残りの区画にもふるいを掛け、証明できたときだけ報告する(未証明の「決して当たりません」を断定しない)。残余が空でなくてもすべての区画の実現不能が証明できたなら、「この行に当たる実入力は存在しない」の文面変種で報告する。
- **定義軸が絡むとき**:真偽定義(名前比較の原子を含む)が絡む発見で出す入力は、実際に作って定義を評価器で計算し、軸の値と一致することを確かめてから出す。構成できない `unique` の重なりは導出と同じく W114 に落ち、ガードが引き受ける(ガードの条件は原子を実行時に評価するだけなので、そのまま生成できる)。精度が足りないときの増強も決めてある。単項原子の DNF 代入(当初設計)で、発動条件は自由軸起因の過剰報告が実表で邪魔になること(§15-9)。M0 のふるいは導出軸までで、定義軸での入力の確認と W114 降格はガードの生成と対で M1 が実装する。それまでの正直化として、定義列が交差に絡む E105 の文面には「この例は定義の中身との整合を確認していません」の一行を足し、証明済みの顔を避ける。
保証の非対称を一行で言えば、**完全性は健全なまま、重なりと当てはまらない行には偽陽性が出うる。ただし偽陽性はエラーではなく W114 とガードの形で現れ、エラーで止めるのは実在を確認できたものだけ**、である。
導出が 1 本だけの表ではふるいはほぼ厳密に効く(残るのは整数の粒度による過大評価だけ)。
一方、導出が入力を共有する形は仮定の話ではない。
コーパスのクーポン規則が最初から含んでいる(`最低差` と `支払差` が `現在残額` を共有する。tests/corpus/クーポン割引.rule)。
当の表は `first` なので W114 の実害はまだ出ていないが、`unique` で同じ形が書かれる前提は既に現実である。
W114 が実表に積もるか、ガードに本番で当たりが出たら、そのときの増強も決めてある。
区画の線形制約系への Fourier–Motzkin 消去で、有理数演算のまま厳密、決定的、外部依存なしに実現可能性が正確になる。
v1 には入れない(§15-4)。
数量の中間値(表出力や一般式の `define`)を列に許すと、行の領域に任意の従属が現れてふるいの形にも収まらない。
§5.3 の制限はこのためにあり、v0 ではこの制限に抜け道が実際に開いていた(同節)。
導出は、その表現力のうち実測が要求した部分(入力の線形結合)だけを、ふるいで回収できる形に限って取り込み直したものである。
非線形の列条件は実測で一度も現れず、旧 §15-3 の SMT 導入案は当面不要と決着した。
### 6.3 アルゴリズムと計算量
- **正規化**:群を展開し、`not` を補集合化する。真偽の `define` は自由な真偽軸として加える(§6.2)。導出は独立の軸として加える。上流表の出力(真偽・列挙の中間値)も独立の軸とし、逆像の展開はせず、座標を上流が出力セルで実際に出す値に制限する(M0 の実装判断。値の到達までを見て、組合せの相関は追わない。外れは過剰報告の向きで、E102 の上流値変種は空になった行マスクとして当てはまらない行の検出にそのまま乗る)。各軸の座標を、その軸に現れる全境界値で圧縮する(列挙は値ごと、数値は境界で区切った区間ごとに 1 座標、真偽は 2 座標)。隣接する境界の差が刻み(日付は 1 日、数量・金額は 1、率は宣言の刻み)ちょうどのときは、あいだの開区間の座標を作らない。値を一つも含まない座標は幻の穴になる。通算日への変更だけでは足りないことは、M0 の両端含みの敷き詰めが実測で示した。
- **起きうるかのふるい**:発見(穴、交差、残余)の区画の数に比例し、1 区画あたり導出本数 × 定数時間。探索予算には効かない。
- **隠れの三分類**(§4):包含の意味は「前の行の領域 − 後の行の領域 = ∅」である。いまの言語では行は軸ごとの部分集合の直積(一つの区画)なので、この判定は軸ごとの部分集合検査と厳密に一致し、実装はその速い形でよい。M0 で領域減算の実装が合成ベンチを落とし(減算は軸数に指数的)、直積の性質に置き換えて三分類の件数の不変を確認した。行が区画の和になる拡張(真偽定義の代入や表出力の逆像を分類に持ち込むとき)では軸ごとの検査は近似になるので、減算へ戻す。ふるいは掛けず、実現できない部分のせいで包含が崩れる対は要確認側へ倒れる。外れる向きはここでも過剰報告で、見逃しは生じない。
- **重なりと隠れ**:行対ごとの区画の交わり判定。行数 r、列数 c、行あたり区画の数 b として O(r²·c·b²)。r が数百、c が十数なら瞬時である。出す入力は交わりの領域から、列挙は宣言順の最小値、数値は境界値を優先して選ぶ。
- **完全性と当てはまらない行**:列を圧縮座標数の昇順に並べた再帰分割で、全体集合から行の区画を引いていく。残り列がすべて don't-care の行が生き残っている部分木は被覆確定として枝を刈る。訪問数の当初の見込み(行数 × 列数に線形)は実測で外れ、行数に対して二乗で伸びる。速度は約 6 万ノード/ms(下の予算の段)。見つかった穴は隣接領域を貪欲に合流させ、点ではなく「届け先 ∈ {離島}、重量は任意」という区画の形で報告する。出す入力と穴の報告は表の列順で書き、探索の列順(圧縮座標数の昇順)を読み手に見せない。
- **DNF と逆像の膨張**:`define` の DNF 展開と表合成の逆像展開には上限(例:1 行あたり区画 64 個)を置き、超えたらエラーで表の分割か群化を促す。
爆発時の逃げ道は一つだけ用意する。
訪問ノード予算(既定 5×10⁷、`--budget` で変更)を超えたら、検査は**未証明**としてエラーで止まる。
サンプリングによる近似検査へ静かに縮退することはしない。
不健全な緑は、この道具が存在する理由の否定だからである。
エラーには「どの列の境界数が支配的か」を添え、群化と表分割という現実的な回避先を示す。
実用限界は M0 の合成ベンチで実測した(§15-2 は決着)。
設計目標(列 12、行 500、数値列 4)は 1,004 万ノード、164 ms で収まり、行数を 1000 に倍加すると二乗則どおり約 4 倍(4,075 万ノード、622 ms)だった。
既定予算 5×10⁷ は、設計目標に 5 倍の余裕を取り、使い切っても 1 秒弱で止まる値として実測から決めた。
回帰コーパスの実測は 522、267、7 ノードで、予算の五桁下にいる。
**捨てたもの**:
- **SMT ソルバ(Z3 等)**:v1 で書ける条件には過剰で、出す入力が決まっていること(同じ規則には同じ入力)と単一バイナリ配布(§12)を損なう。列の線形条件は導出とふるいで取り込み、精度が足りなくなったときの増強も Fourier–Motzkin 消去まで(§6.2)。SMT が要る形(非線形の列条件)は実測で 0 件だった。
- **BDD / MDD**:座標圧縮後の問題規模なら区画の代数の素朴な実装で足り、変数順序のチューニングという新しい闇を持ち込むほどではない。ベンチで裏切られたら再訪する。
## 7. 丸めと単位の意味論
### 7.1 実行時表現
**決定**:すべての数値の実行時表現は int64 一本とする。値は r × s(r が実行時整数、s が型に付く静的な正の有理スケール)で、丸め済の型は s = 1 単位、未丸めの型は演算が合成したスケールを持つ。
浮動小数点も任意精度も decimal も生成コードに入れない。
言語間の一致(P5 の等価性)は、両言語が同じ int64 演算を実行することで機械的に成立させる。
`基本送料 × 負担率` は、円の整数 × パーセント整数で「0.01 円」単位の整数になり、スケール 1/100 円が型に記録される。
導出は整数定数倍と加減だけなので丸め済のまま閉じ、スケールを持たない(常に単位の整数一本で、丸めの新しい場所を作らない)。
### 7.2 いつ丸めるか
**決定**:丸めは明示した場所でだけ起きる。場所は三つ。(1) 出力宣言の `round`(数値出力には必須)、(2) 表の出力列ヘッダの `round`、(3) 式中の丸め関数 `up(式, 10円)` など。出力宣言の丸めは、結果式が未丸めのときに一度だけ適用される。結果式が既に丸め済なら再丸めせず、格子との整合だけを検査する。
「各表で丸めるか、最終出力で丸めるか」への答えは、**既定では最終出力の一回だけ、中間で丸めたければ書け**、である。
中間丸めの有無は円が動く業務仕様であって、道具が決めてよいことではない。
だから既定をもっとも情報を失わない側(丸めない)に置き、丸めるなら紙面に見える形で書かせる。
旧実装が中間で丸めているなら、等価検証がその差を「丸め差異」クラスタとして出し(§10)、書き手が中間丸めを明示して一致させる。
この往復こそが「差分の主要因になる」ことへの設計上の答えで、丸め位置の違いはすべて `.rule` の diff に現れる。
丸めの計算は、スケール s = p/q と格子 g に対して r·p を q·g' で割る整数除算一回と余りの検査で厳密に実行できる(g' は基準単位に直した格子)。
したがって格子とスケールの整除性のような制約は存在しない。
唯一の制約はオーバーフローで、`range` 宣言からの区間演算で int64 に収まることを規則ごとに静的に証明する。
証明できなければエラー(E108)で、中間丸めの挿入か刻みの粗直しを促す。
このエラーは円が動く業務判断を要求するので、文面は §11 の設計対象に含める。
出力セルに名前が書ける(§3.2)ことに伴う規則も、ここで固定する。
出力列の丸め状態は全セルの結合で決まる(どれかが未丸めなら列は未丸め。リテラルは常に丸め済)。
E106 の静的な格子検査はリテラルにだけ適用する。
名前の値が格子に載っているかは静的に分からないためで、列や出力の丸め宣言は名前の値にも宣言どおり一度掛かる。
宣言は紙面にあるので、これは「黙って変える」ことにはならない。
実測でこの検査がまず効いたのは計算のない表引きの運賃表で、丸め宣言が `1451円` のような転記タイポを桁の検査として落とした。
設計時は計算のための規約と考えていたが、実際には転記の検査として先に働く。
公開規約の転記は、丸めについてもう一つのことを教えた。
複数クーポンの端数処理は、楽天にも Yahoo にも書かれていない。
割引上限が素割引に掛かるのか最終額に掛かるのかも書かれていない。
つまり E104 と丸め宣言の必須化が要求する決定は、規約の側に存在しないことがある。
このとき書き手の仮置きが仕様の顔をして生成コードに入るので、規約に根拠のない丸めには出典と仮置きの旨をコメントで残す慣行を置き(§5.4 のスケッチの `# 端数処理は公開規約に記載なし。切り捨てに仮置き`)、過去データが来たら replay が仮置きを裁定する(§10)。
丸めを書けと迫ること自体の価値は、公開規約の欠落という形で、データを見る前に実証された。
### 7.3 丸めモードの定義
**決定**:モードは四つ。負の値に対する向きまで仕様で固定する。(2026-09-16 に五つめの `half_down` を足した。§15.39)
- **切り上げ**:0 から遠ざける(-4.2 円 → -5 円)
- **切り捨て**:0 へ寄せる(-4.8 円 → -4 円)
- **四捨五入**:半分ちょうどは 0 から遠ざける
- **銀行家丸め**:半分ちょうどは偶数へ
負の値まで固定するのは、Python の `//` が −∞ 方向、Go の整数除算が 0 方向と、言語既定の除算が食い違っているからである。
生成コードが言語の素の除算を使うと、同じ表から違う円が出る。
そこで両言語に同一アルゴリズムの丸めヘルパ(十数行、生成ファイル内に自己完結)を吐き、負値と半分ちょうどを含む共通ベクタで一致を検証する(§9)。
値引きや返金で負の金額は現実に通るので、この固定は飾りではない。
### 7.4 除算と比率
除算は正の整数定数と、同型の定数(`注文金額 / 100円`、`重量 / 100g`)に限る(§2.3)。
結果は未丸めの無次元数で、丸めモードを明示しないと整数にならない。
「100 円につき 1 点」は `down(注文金額 / 100円)` と書き、切り捨てという業務仕様が紙面に残る。
## 8. 生成コードの形
### 8.1 可読性の基準
生成物の可読性は次の五つで定義し、golden テストで固定する。
1. 表の 1 行が分岐 1 本に対応する。分岐条件は行の全セルを省略せずに書き(先行分岐で真とわかる条件も消さない)、コメントに元の行を転記する。
2. 使う抽象は標準ライブラリの列挙と整数まで。テーブル駆動、クラス階層、ジェネリクスを使わない。
3. `.rule` の 1 行の編集が、生成物でも局所的な diff になる(条件の共通化や並べ替えという「賢い」最適化をしない)。
4. 長さは行数に線形で、1 ルール行あたり 2〜4 行を目安とする。
5. gofmt と PEP 8 に素で適合し、出力はバイト決定的である。
依存ゼロは「第三者パッケージゼロ」の意味で、標準ライブラリ(Python の enum、Go の fmt)は使う。
識別子は公開と内部で流儀を分ける(§1.3)。
公開 API(関数名、入力、出力、その型と列挙値)は ASCII 別名を使い、内部(表の関数、群の集合、内部の型、導出、定義)は別名が無ければ日本語識別子をそのまま使う。
漢字かなの識別子は非公開の位置なら両言語で合法で、gofmt も black も通り、行とコードの目視対応(基準 1)には原文のままの内部名がむしろ効く。
出力セルの名前(§3.2)は、その行の分岐での変数参照になる(`raw = 率割引`)。
1 行 1 分岐の対応は変わらない。
未検証の重なり(W114。§6.2)を持つ `unique` の表には、ガードを生成する。
```python
# ガード: W114(表 適用判定 行1 × 行2)。重ならないことを静的に証明できなかった行の対
if rest_a <= 1000 and rest_b >= 3980:
raise RuleContradictionError("表 適用判定: 行1 と 行2 が同時に当てはまりました")
```
Go では同じ条件でゼロ値と error を返す。
Python の例外は入力契約違反の `RuleInputError` と区別して `RuleContradictionError` とする(規則そのものの矛盾であって、呼び出し側の誤りではないため)。
ガードは未検証の行対にだけ生成され、対象の行対をコメントに刻む。
この分岐を踏む入力はベクタには存在しない(構成できたなら W114 ではなく E105 になっている)ので、生成されるが通常は実行されないコードであることをファイルヘッダの注記にも書く。
細目も約束しておく。
ガードを入れる対は、生成時点の解析が出す W114 の対そのもの(`unique` の表で、出力が構文的に異なるもの)で、「構文的に同一」の判定は `--diff-base` と同じセル正規形の鍵で行う(同一性の定義を二つ作らない)。
複数の対は表の行順に並べ、メッセージは「表名: 行i と 行j が同時に当たりました」の形に固定する(運用が grep する面なので安定させる)。
導出の範囲にはガードを置かない。
E112 が宣言範囲 ⊇ 到達区間を静的に保証し、入口ガードが入力を範囲内に保つので、導出が宣言範囲を出る実行は存在しない。
### 8.2 Python
```python
# Code generated by rulec 0.1.0. DO NOT EDIT.
# 原本: rules/送料.rule (規則 送料 v4, sha256:9f2c1a…)
from __future__ import annotations
import enum
from typing import NewType
YenInclTax = NewType("YenInclTax", int) # 金額[円, 税込]
class MemberKind(enum.Enum):
BASIC = "一般"
GOLD = "ゴールド"
PLATINUM = "プラチナ"
class RuleInputError(ValueError):
"""宣言された入力域の外。呼び出し側の契約違反。"""
def shipping_fee(dest: Prefecture, weight_g: int,
total: YenInclTax, member: MemberKind) -> YenInclTax:
"""規則 送料 v4。分岐は rules/送料.rule の行と 1:1 に対応する。"""
if not 1 <= weight_g <= 40_000:
raise RuleInputError(f"重量 {weight_g}g は範囲 1g..40kg の外")
# 表 基本送料(方式 一意)
if dest in _遠隔地 and weight_g <= 2000: # 行1: 遠隔地 | <=2000g | 1200円
base = 1200
elif dest in _遠隔地 and weight_g > 2000: # 行2: 遠隔地 | >2000g | 1800円
base = 1800
elif dest not in _遠隔地 and weight_g <= 2000: # 行3: 以外:遠隔地 | <=2000g | 800円
base = 800
elif dest not in _遠隔地 and weight_g > 2000: # 行4: 以外:遠隔地 | >2000g | 1100円
base = 1100
else:
raise AssertionError("到達不能: 完全性は rulec が静的に検査済み")
...
raw = base * pay_rate # 単位: 0.01円(円 × %)
return YenInclTax(_ceil_10yen(raw))
```
金額の brand は `NewType` で表す。
mypy と pyright には効き、実行時コストはない。
列挙は stdlib の `Enum` で、値に和名文字列を持たせてログとワイヤ形式(§10)に使う。
完全性は rulec が証明済みなので、pyright の完全性検査に依存しない。
ただし Python は動的で列挙外の値が渡り得るため、入口で列挙と範囲を検証して `RuleInputError` を投げ、最終 else は「到達不能」の assert として残す。
証明の前提(入力が宣言域内)を実行時に守るのが入口ガード、証明そのものを信じるのが最終 assert、という分担である。
### 8.3 Go
```go
// Code generated by rulec 0.1.0. DO NOT EDIT.
// 原本: rules/送料.rule (規則 送料 v4, sha256:9f2c1a…)
package shippingfee
type YenInclTax int64 // 金額[円, 税込]
type MemberKind int
const (
MemberBasic MemberKind = iota // 一般
MemberGold // ゴールド
MemberPlatinum // プラチナ
)
func (m MemberKind) Valid() bool { … }
func (m MemberKind) String() string { … } // 和名を返す
type Input struct {
Dest Prefecture
WeightG int64 // 質量[g] 範囲 1g..40kg
Total YenInclTax // 範囲 0円..1000万円
Member MemberKind
}
func ShippingFee(in Input) (YenInclTax, error) { … }
```
金額の brand は defined type(alias ではない)で表す。
`YenInclTax + YenExclTax` は Go コンパイラが弾き、キャストは grep で見つかる。
Python の brand が型検査器頼みなのに対し Go は言語本体で守れるので、この点の非対称は Go 有利に倒れる。
sum type の不在への答えは「頼らない」である。
完全性の証明は rulec が持ち、生成される switch は証明済みの全ケースと、列挙外の値を受ける default(error を返す)で構成する。
したがって `nishanths/exhaustive` 等の linter にも依存しない。
生成関数は `(値, error)` を返し、error は入力契約違反(列挙外、範囲外)だけを表す。
error か panic かは呼び出し側の流儀に合わせたく、実際に組み込む Go の慣行を見て最終決定する(§15-3)。
入力は 4 個を超えると同型の int が並んで危ないので、フィールド名に単位を含む Input 構造体で受ける。
### 8.4 生成物の置き場所
**決定**:生成物は git にコミットする。CI の `rulec gen --check` が、コミット済み生成物と再生成結果の一致を検証する。
理由は三つ。
表の変更 PR に生成コードの diff が並ぶことがレビューの実体であること(P5 の「人間がレビューできる」)。
利用側リポジトリが rulec なしで生成物を vendoring できること。
そして生成物の手編集が `--check` で即座に露見することである。
ヘッダには rulec の版と原本のハッシュを刻み、Go の生成コード規約(`^// Code generated .* DO NOT EDIT\.$`)に従う。ritsu に取り込んでからは、版は ritsu のバージョンで、原本はファイルのパス(`rulec gen` ではファイルの名前)とハッシュで書く(§15.174)。
### 8.5 M1 の生成細目
設計文書に書かれていなかった前提を、M1 の着手前にここで固定する。
- **配置と命名**:`generated/python/<規則別名>.py`、`generated/go/<規則別名>/<規則別名>.go`。Go のパッケージ名は規則別名から `_` を落とした小文字。
- **決定性**:同じ `.rule` と同じ rulec からは、実行環境によらずバイト同一の生成物。内部の反復はすべて宣言順か整列済みで、ハッシュ順の混入は不合格。改行は LF。
- **複数出力**:Python は `typing.NamedTuple`、Go は名前付きの Output struct を返す。単出力は素の値のまま。
- **入口ガードの範囲**:列挙の妥当性、入力の `range`(率は刻み単位の整数で検査)、optional の `none` の扱い。導出には置かない(§8.1)。**日付もガードを持つ(2026-09-13 に追加)** — 日付は通算日の整数で、その `range` は完全性証明が使った全体集合そのものである。ガードが無いと、宣言範囲の外の日付がたまたま当たった行に落ちて、呼び出し側は静かに間違った答えを受け取る。§8.6 の照合(一覧が言う範囲が生成物のガードの字面にあること)で見つかった抜けで、**この照合を入れた初回に実在の欠陥を一つ見つけた**という点で W111 のときと同じ形をしている。
- **実装間の一致の判定**:宣言域内のベクタで、正準 JSON に直列化した出力のバイト一致。域外の挙動(Python の RuleInputError、Go の error)は言語ごとのユニットテストで域外 1 点ずつ確かめる。評価器は域外を受けないので、比較の対象は域内に限る。
- **丸めヘルパの単体ベクタ**:§7.3 の四モードについて、負値と半分ちょうどを含む共通ベクタを表のベクタとは別に出し、両言語のヘルパ単体で回す。表レベルの一致だけでは、端数の出ない表でヘルパの誤りが隠れる。
- **定義軸のふるい**:定義軸の入力の検証(点を構成して定義を評価器で確かめる)と、未確認の `unique` の重なりの W114 降格は、ガードの生成と対で M1 が実装する(§6.2)。依存の共有だけで機械的に Unknown へ落とす形は採らない。検証を先に試み、実在する矛盾を W114 に逃がして CI を素通りさせない。
- **ベクタの原子境界**:真偽定義の原子の境界の抽出(§9.1)も M1 の実装範囲。
- **整形の内蔵**:gofmt 相当の整形は生成器が内蔵し、後段で環境の gofmt を走らせない(環境の版に依存した瞬間、決定性が壊れる)。`gofmt -l` が空であることは検証として残す。列の整列は `text/tabwriter` と同じルーン数で数える。表示幅の全角 2 ではない(和名の識別子が二行並んで初めて出た実測)。`.rule` の fmt は東アジア幅、生成 Go は Go の流儀と、それぞれの生態系の正準に合わせる。
- **PEP 8 の検証**:`ruff check --select E,W --ignore E501` が空であることを固定する。行長(E501)は意図した逸脱である。§8.1 の「表の 1 行を分岐 1 本に、原本のセルをコメントで添える」は 88 桁に収まらず、行長より 1 行 1 分岐の対応が優先する。
- **組み込みの隠れ**:生成 Python は言語の組み込みを裸で呼ばない。入力の別名が `min` だっただけで生成物が壊れた実害から、`_min` `_max` などの内部名を生成側が持ち、衝突の族ごと消す。
### 8.6 生成物の呼び方の一覧(rulec api)
**決定(2026-09-13)**:`rulec api <file.rule>` が、生成物を**読まずに呼べるだけの情報**を JSON で出す。Python の module・function・署名・引数(和名、別名、型、単位、範囲、optional)・戻り値・出力(丸めつき)・列挙の値(その言語での綴り)・送出しうる例外。Go の package・func・署名・`Input` と `Output` の型とフィールド・列挙の値。言語に依らない(全部が生成物で実際に使われている名前か数値である)。
置き場所は `src/codegen.rs` の中、生成器の隣にした。一覧は生成物の**二番目の説明**であり、離れた場所に置いた瞬間に、名前を一つ変えた日から食い違いはじめる。同じ `pub_name`・`pascal`・`py_ty`・`go_ty` から組むこと、そして**テストが生成物そのものと突き合わせること**の二つで縛る。突き合わせは三段ある。(1) 一覧が言う署名・ガードの範囲・列挙の綴り・例外クラスが、生成されたファイルの字面にあること。(2) Python は生成物を import して `inspect.signature` と比べること。(3) Go は**一覧だけから呼び出しプログラムを組み立てて** `go vet` に通すこと — 名前が一つでも違えばコンパイルが通らない。
範囲は「生成物のガードが実際に使っている数字」をそのまま出す。一覧が別の計算をすると、一覧どおりに送った値をコードが拒む、という最悪の食い違いが生まれる。
**捨てたもの**:
- **人の手で書いた呼び出し規約の文書だけで済ませる**:`docs/generated-code.md` は残すが、名前の正は一覧に置いた。手書きの文書は、生成器が名前を変えた日から静かに嘘になる。
- **`--format` で python だけ / go だけを出す**:一つの JSON に両方入れる。呼ぶ側が両言語を同時に見たい場面(同じ表が二か所で使われている確認)が主で、片方だけ欲しければ `jq .python` で足りる。
- **一覧に「使用例のコード」を入れる**:例は文書(§8.6 の `docs/generated-code.md`)の仕事で、一覧は事実だけを持つ。例を一覧に入れると、例が腐ったかどうかを一覧のテストが見なければならなくなる。
## 9. 境界からのテストケース生成
### 9.1 値の候補
列ごとに候補値を決める。
- 列挙と真偽:その列のセル群が誘導する同じ答え類(圧縮座標)ごとに代表 1 値。47 都道府県を全部舐めるのではなく、表が区別している区分だけを踏む。
- 数値(数量、金額、日付):表に現れる各境界 b について {b − 最小刻み, b, b + 最小刻み}、および `range` の両端。
- 導出:境界は導出の値なので、その値になる入力を求めてベクタにする(残りの入力は行の他セルを満たす値に固定する)。求め方は契約だけを決める。構成した点は必ず評価器で再評価し、目標に当たった点だけを採る。当たらなければ次の入力で試み、どうしても埋まらない義務は被覆判定器が赤で報告する。この二重の網(採用前の再評価と、義務の事後判定)の下では、解き方は実装の自由である。M1 の実装は記号解法ではなく数値の傾き(seed で評価し、一刻み動かして傾きを取り、目標へ跳んで、再評価で確かめる)で、上流の表や定義を挟んでも同じ一つの機構で通り、区分線形の継ぎ目や整除の外れは再評価が自然に落とす。解が入力範囲の外になる側の点は実現不能なので捨て、境界両側被覆の定義は実現可能な側に限る。
- 真偽定義の原子:解析は自由軸(§6.2)なので原子の境界を見ない。ベクタは定義から原子の境界を直接取り出して踏む(`注文金額 >= 3万円` なら 3万円 の両側、名前どうしの比較なら同着 A = B とその両側を、導出と同じ要領で、その値になる入力を求める)。解析に見えない境界こそ、ベクタが踏まないと誰も踏まない。
- optional:`none` と、在る側の上記候補。
### 9.2 被覆基準
**決定**:三つの被覆を定義し、`rulec vectors` はこれを満たす最小に近い集合を決定的に生成する。
- **行被覆**:各行につき、その行が発火する(`first` では勝つ)ベクタが 1 つ以上。
- **境界両側被覆**:数値列の各セル境界につき、他列をその行に当たる値へ固定したまま境界の両側を踏むベクタ対。行の削除、境界値の ±1、開閉の付け間違いは、この対の期待値を必ず変える。
- **隠れ対被覆**:`first` の隠れ対 (i, j) ごとに、交差の内側(i が勝つ点)と j∖i(j が発火する点)の 2 点。隣接行の入れ替えという変異を殺す。
三基準は義務の集合として実装する。
義務は規則から静的に列挙し、生成器はそれを狙い、被覆判定器は各義務が実際に片づいたか(そのベクタで行が勝つか、境界の両側を対で踏んだか)を評価器で確かめ、片づけたベクタだけを残す。
発火したトレースから義務を作る形は禁じる。
踏めなかったものが「必要なかった」ことになるからである。
義務の列挙そのものは一箇所でよい。
同じ書き手による二重実装は盲点を共有しやすく、維持費だけが残る。
代わりに三つの網を張る。
隠れ対の義務は解析器から取る(既に独立)。
境界の義務は、式の木から境界リテラルを数えるだけの素朴な収集器と件数で照合する。
規則ごとの義務の件数を回帰に固定し、言語に列の種類や原子の形を足すときは、該当カテゴリの義務が増えることを受け入れ条件にする。
増えなければ、列挙器がその機能を見ていない。
三基準を満たした上で、列間の相互作用の取りこぼしに保険を掛けるため、候補値のペアワイズ(全 2 因子網羅)を貪欲法で足す。
全直積は候補 10 値 × 6 列で 10⁶ に爆発するが、ペアワイズなら百数十件で収まり、行標的と合わせて規則あたり数百件が目安になる。
生成は決定的(同じ `.rule` から同じベクタ列)で、期待値は参照評価器が付け、発火行のトレースも記録する。
```
{"in":{"届け先":"沖縄","重量":2001,"注文金額":40000,"会員":"一般"},
"out":{"送料":0},
"trace":{"基本送料":2,"負担判定":1},
"why":"境界両側: 基本送料 行2 重量>2000g の内側"}
```
### 9.3 ベクタの三つの使い道
1. **言語間一致テスト**:同じベクタを生成した各言語に流し、参照評価器との一致を確かめる。データも旧実装も不要で、M1 の主力の保証になる(P6)。
2. **等価検証の入力**:旧実装アダプタ(§10)に同じベクタを流す。セル境界から作っているので、過去データが薄い境界近傍も踏む。
3. **golden**:生成された各言語のテストファイルにベクタを埋め込み、利用側 CI でも回るようにする。
## 10. 等価検証と過去再生のインタフェース
### 10.1 旧実装の呼び方
**決定**:アダプタプロセス方式。旧実装の言語で 20〜30 行のアダプタを書いてもらい、rulec が子プロセスとして起動して JSON Lines を stdin/stdout で流す。FFI もネットワークも使わない。
言語をまたぐ最小の共通面が「プロセスと行指向 JSON」で、Python でも Go でも Perl の遺物でも同じ書き方になる。
ワイヤ上の名前と値は `.rule` の正準名(和名)と正準単位の整数で、`rulec schema` が入出力の JSON Schema を吐き、テンプレートは `rulec adapter --template python|go` が印字する。
アダプタが「答えられない」と言った件は一致率の**分母から外す**。そうしないと、難しい件に「答えられない」と返すだけで一致率が上がる。
**形式の定義そのものは [`docs/formats.md`](docs/formats.md) に移した(2026-09-13)。** ここに残すのは決定とその理由で、握手の順序やフィールドの名前は文書の側にある。二か所に書けば必ず片方が腐り、腐るのは決まって説明のほうだからである。
### 10.2 記録形式
過去データのスナップショットは 1 件 1 行の JSONL に固定する(フィールドの定義は [`docs/formats.md`](docs/formats.md))。
本番ログからこの形への抽出(ETL)は利用者の仕事と割り切り、rulec は `rulec fixtures lint` で型と範囲の検証だけを引き受ける。
注文金額を含むので fixtures はリポジトリに入れず、CI にはアーティファクトか保護ストレージで渡す。
**数値は正準単位の整数で、小数は受け付けない。** この禁止をどこで守るかは 2026-09-13 に動かした。以前は JSON の読み手(`src/json.rs`)が小数を見た瞬間に行ごとエラーにしていたが、rulec 自身が小数を書くようになったので(`verify`/`replay`/`diff` の一致率)、読み手は小数を `Frac` として読み、**`as_int` が否と言う**形に変えた。禁止は fixtures の側に残り、文面は「`observed.運賃`: 正準単位の整数 を期待しましたが 小数 でした」と、**どのフィールドが壊れているかを示す**ぶん良くなった。小数は浮動小数点にせず書かれた桁のまま持つ(§2.1 の「浮動小数点はどこにも現れない」を読み書きの経路でも守る)。指数表記は引き続きエラーにする — 転記された記録が `1e3` と書く読み方は無い。
**`observed` の小数は受け付ける(2026-10-08、§15.199)。** そのとき出た値は、旧実装が出力の刻みより細かく計算していれば小数になる。それは記録の形の誤りではなく、規則と旧実装の食い違い(多くは丸め方の違い)なので、書かれた桁のまま正確に読んで照合する。入力は、これまでどおり正準単位の整数だけを受け付ける。
### 10.3 入力が完全に復元できない場合
**決定**:モードは二つだけ。フィールドごとに (1) その記録を除外する、(2) 既定値で補完し「補完系」の札を付ける。逆推定はしない。
補完の既定値は、規則の言語には入れない。
規則は純関数で、補完は特定の再生実験の判断だからである。
同じ規則を「会員は一般で埋める」「ゴールドで埋めて感度を見る」と別々に再生することが正当にありうるし、規則の宣言に書けば、意味に関与しない注記が規則のハッシュを変えて gen --check が無意味な再生成を要求する。
置き場所は版管理される再生マニフェスト(fixtures の隣に置く小さな JSON。既定値と対象のフィールドだけで機微な数字を含まないので、リポジトリに入る)とし、CLI の `--fill` は感度分析のための一時上書きとして許す。
どちらから来た値でも、レポートが「使った既定値」と「補完した件数」をフィールドごとに必ず刻む。
根拠は指定の場所ではなく、数字の出る場所(PR に貼られるレポート)に現れることで担保する。
なお、補完が見出しを膨らませる誘惑は階段に存在しない。
見出しの一致率は実測系だけから計算するので、補完は二次集計にしか効かない。
fixtures のフィールドの欠けは、鍵の不在で表す。
optional の `none` は値であり、`null` として明示的に書く。
「値が無いと分かっている」(無し)と「値が分からない」(欠け)は別物で、前者は補完の対象にならない。
レポートは実測系と補完系を必ず分けて集計し、見出しの一致率は実測系だけから計算する。
「982 件」の説得力は再現の正直さそのものなので、補完を混ぜた一致率を大きく見せる誘惑を、形式のレベルで断つ。
記録を使う `diff` は、記録を旧版の読み方で読むので、既定値も旧版の型で読む。
新しい版だけが取る入力は旧版の記録に無いので、その既定値は新しい版の型で読み、補った記録として数える(§15.197)。
### 10.4 差分の帰属
replay と diff の不一致は、参照評価器の発火行トレースを鍵にクラスタする。
鍵は発火行の組だけ(diff は新旧両側、replay と verify は規則側のみ)で、金額は鍵に入れない。
表引きの行なら行の組が金額の組を一意に決めるので、金額で分ける意味がない。
計算の絡む出力(出力セルの名前、`result` の式)で金額を鍵に入れると、金額の数だけクラスタが割れて要約が死ぬ。
代わりに、クラスタごとに件数、Δ の合計、最小と最大、入力例を必ず出し、Δ が全件同じ答えならそれを一行に畳む。
下の例の一行は、この一様な場合である。
既知の限界を一つ記す。
表そのものが増減した版の diff は、対応の取れない発火行を(旧のみ)(新のみ)と記すだけで、遷移としては読ませない。
構造の組み替えに意味のある対応は取れないからで、実表で頻発するようなら、そのとき設計する。
```
$ rulec diff 送料@v3 送料@v4 --fixtures replay/2025-08.jsonl
影響 3,412 件 (0.27%) 金額 +182,400 円
クラスタ 1: 基本送料 行2→行5(離島新設) 2,900 件 +196,800 円
例: 届け先=沖縄, 重量=2400g, … v3: 1800円 → v4: 2000円
クラスタ 2: |Δ| < 10円 の端数のみ 512 件 -14,400 円
丸め差異の疑い(出力格子 10円 未満の一様なずれ)
```
**記録は旧版の読み方で読む(§15.197)。**
記録は旧版のシステムが残したものなので、`diff` は記録の入力を旧版の名前と型(`--read-as` を付ければ、その版の刻みと単位)で読み、`observed` と `trace` は読まない。
旧版でも受け付けない入力の記録は、形式の食い違いとして外す。
新しい版が受け付けない入力の記録は、外さずに照合した記録に数え、答えが動いた記録とは別に、理由ごとに出す。
**二つの答えは値として比べる(§15.199)。**
`verify`、`replay`、`diff`(記録を使うときも使わないときも)は、二つの答えを、どちらかの刻みに丸めずに値として比べる。金額と数量は、単位が違えば換算して比べる。
差は規則(`diff` では新しい版)の受け渡す単位で、丸めずに書く。どちらかの答えが規則の刻みより細かければ、差は小数になる。
`replay` と `verify` は、規則の取りうる範囲の外の答えも照合して不一致に数え、その件数を見出しの下に書く。
出力格子未満のずれを「丸め差異の疑い」として自動で括るのは、§7 の規約が差分の主要因になるという予測を運用に折り返したものである。タグの条件は「クラスタの全件が、ゼロでなく格子未満のずれ」で、格子ちょうどのずれは丸めではなく値の食い違いなので付けない。複数出力では、食い違っている出力のすべてが格子未満であることを求める。
## 11. エラーメッセージ
設計原則を先に決める。
1. 一行目は業務の言葉だけで完結させる。型や IR の語彙を一行目に出さない。
2. 必ず具体の入力例を付ける。値は列挙の宣言順と境界値を優先して選ぶ。導出が絡むときは入力として組み立てて評価器で確かめ、構成できなければ「実現可能性未検証」と明記する(証明していないことを証明済みの顔で出さない)。定義や導出の軸が絡んで実在を確認できた発見は、軸の座標(なぜ重なるか)を残したまま、それを作る入力を一行添える。座標を入力だけに差し替えると「なぜ」が消えるからである。
3. ヒントは「書き換え後の形」まで書く。
4. 位置はファイル:行に加え、表名、行番号、列名で示す。関与する行が複数なら全部にスパンを張る。
5. エラーコードと JSON 構造は安定 API とし、文言は改善してよい。
6. `--format json` が GitHub annotations にそのまま流せる形を吐く。exit code は 0(注記のみ)、1(エラー)、2(内部異常)。
**JSON v2(2026-09-13)。** 診断は同じ発見を二通りで言う — 人が読む文面と、プログラムが動く**データ**である。v1 のフィールド(severity, code, file, line, column, title, notes)は全部そのまま残り、`"v": 2` が書き手を名乗る。足したのは `where`(file, line, column, table, row)、`spans`(位置とラベル)、`witness`、`rows`(関与する表と行)、`fix`(kind と、貼れる書き換え後の形)、`key`(`--diff-base` の鍵)である。**構造は発行箇所で入れる。** 文面を後から解析して取り出さない — 原則 5 が文面の改善を明示的に許している以上、文面を解析する読み手は次の改善で壊れる。
**入力例は「値の割り当て」だけを運ぶ。** `witness.inputs`(具体の入力)、`witness.outputs`(その入力から規則が実際に出す値)、`witness.expected`(例が言っていた値。E107 だけが持つ)。到達区間や「丸め方で動く幅」のような**区間や差は割り当てではないので入れない** — 文面に残し、行動できるものは `fix.text` に置く(E112 の広げた `range` がそれ)。こうすると入力例はいつも一つの形になり、受け取った側はそのままベクタや fixtures として投げ返せる(§10.2 のワイヤと同じ形)。**捨てたもの**:出す入力に区間や統計を混ぜて型を増やす案(読み手が値と区間を場合分けすることになり、「そのまま投げ返せる」性質が消える)。
**`fix.text` は文面を含まない。** 言語に依らないバイト列で、`.rule` にそのまま貼れる形だけを持つ(`round down(1円)`、`range >=-110万円 <=100万円`、`| 山梨県 | S60 | 820円 |`)。丸めの向きも格子も、足す行の金額も**業務の判断**であって道具が決めてよいものではないから、その注意は言語に依る `notes` の側に置く。テストが E101 と E104 の `fix.text` を機械的に貼って、そのコードが本当に消えることを確かめている。嘘をつく fix は、fix が無いより悪い — 指示の顔をしていて、間違いだと分かるまでに一往復かかる。
**`--terse`。** text 出力を「見出し・位置・入力例」の三行に絞り、走査の最後に一度だけ `details: rulec explain <code>` と出す。原則 3 は消えていない — 一件ごとに繰り返す代わりに、道案内として一度出る。`--format json` との併用はエラーにする(JSON は既に機械向けの形で、どちらを削るかの判断が要らない)。
7. 文面は日本語と英語の二言語を持つ。`--lang ja|en`、無ければ環境変数 `RULEC_LANG`、次に `RITSU_LANG`(§15.168)、無ければ**英語**。システムロケールは見ない。生成物は `gen --check` で照合され、CI のログは diff されるので、走らせた機械で出力が変わってはならない。コードは言語に依らない(原則 5)。実装は各文面を `tr!("日本語", "English")` で隣り合わせに書き(`src/i18n.rs`)、片方だけの文面を作れないようにしている。
**覆した決定(2026-09-13)**:既定は日本語だった。第一の利用者が AI エージェントであると決めたので、既定を英語にした。理由は二つ。英語のほうがトークンが少なく済み、モデルの読み書きが安定すること。そして `--help` と診断と JSON だけで手順を回させる以上、既定で出る言葉が道具の顔になること。日本語は失われていない — `--lang ja` か `RULEC_LANG=ja` の一つの設定で全面が戻り、承認者に貼る PR コメントを日本語にする CI はその一行を書く。**捨てたもの**:システムロケールを見て切り替える案(走らせた機械で出力が変わる。原則 7 の本文と衝突する)と、診断だけ英語・レポートだけ日本語のような面ごとの既定(どの面がどの言語かを覚える必要が生まれ、`tr!` の対一つという実装の単純さも壊れる)。
以下、合意済みの五つの文面。表示は Rust 風の枠を借りる。
```
error[E101]: 完全性の欠落: どの行にも当たらない入力があります
--> rules/送料.rule:23 表 基本送料
|
23 | table 基本送料
| ^^^^^^^^ 入力空間を覆いきっていません
|
当たらない例: 届け先 = 離島, 重量 = 500g
覆われていない範囲: 届け先 ∈ {離島}, 重量は任意
原因: 型 都道府県 に「離島」が追加されています(共通型.rule:3)が、
この表の行はどれも「離島」を含みません。
ヒント: 行を追加してください。例: | 離島 | - | 2400円 |
```
```
error[E102]: 行3 はどの入力にも当てはまりません
--> rules/送料.rule:31 表 基本送料
|
29 | | 北海道, 沖縄 | <=2000g | 1200円 | 行1
30 | | 北海道, 沖縄 | >2000g | 1800円 | 行2
31 | | 北海道 | - | 1500円 | 行3: ここに到達する入力はありません
|
原因: 方式 上から のため、北海道の全域(重量も全域)を行1 と行2 が先に取ります。
ヒント: 行3 が新しい仕様なら行1 より上へ移してください。
不要なら削除してください。出力 1500円 は現在どの入力にも効いていません。
```
```
error[E103]: 単位の混同: money[円, incl_tax] に 質量[g] を足しています
--> rules/送料.rule:40 結果
|
40 | result 送料 = 基本送料 + 重量
| ^^^^^^^^ ^^^^ 質量[g](入力 重量, 12行目)
| 金額[円, incl_tax](表 基本送料 の出力, 23行目)
|
ヒント: 重量に応じた加算料金なら、それは表で書きます。
例: 表 重量加算 | 重量 | -> 加算額 : money[円, incl_tax] |
```
税込と税抜の混同も E103 で、二行目が「金額[円, 税込] と 金額[円, 税抜] を足しています。税の変換は変換式ではなく表として書いてください」に変わる。
```
error[E104]: 丸めていない値が出力に到達します
--> rules/送料.rule:8 出力 送料
|
8 | outputs 送料(fee) : money[円, incl_tax]
| ^^^^^^^^^^^^^ round の宣言がありません
41 | result 送料 = 基本送料 × (100% - 割引率)
| ^^^^^^^^^^^^^^^^^^^^^^^^^ ここで 1円未満の端数が生まれます
| (割引率 は 刻み 0.1% なので最小 0.001円)
|
例: 基本送料 = 800円, 割引率 = 12.3% のとき計算値は 701.60円 です。
down(1円) なら 701円, half_up(1円) なら 702円, up(10円) なら 710円 と、
丸め方で最大 9円 動きます。
ヒント: 出力の宣言に丸めを書いてください。例: round up(10円)
```
E104 の文面は二形ある。
上は式が端数を生む場合(率の乗算や除算が経路にある場合)で、具体の円差を添える。
出力が表引きだけで端数が生じ得ない場合は、一行目を「出力に丸めの宣言がありません」に替え、円差の例は出さない。
代わりに「丸め宣言は、リテラルが格子に載っているかの検査(E106)に使われます。1451円 のような転記のタイポを桁で落とすのが、この宣言の第二の働きです」と言う(§7.2)。
どちらの形でも E104 の意味は「数値出力に丸め宣言が無い」で一つである。
```
warning[W105]: 行の重なり: 同じ入力が 行1 と 行2 の両方に当たります
--> rules/送料.rule:35 表 負担判定(方式 上から)
|
34 | | true | - | 0% | 行1
35 | | - | プラチナ | 50% | 行2
|
両方に当たる例: 大口 = true, 会員 = プラチナ
policy first のため 行1(0%)が勝ちます。意図通りですか。
意図通りならこのままで構いません。CI の check --diff-base は
新たに生じた重なりだけを報告します。
行2 を優先したいなら、行2 を行1 より上へ移してください。
```
W105 で一覧されるのは要確認の対(包含でない部分交差で出力が異なるもの)だけである(分類は §4)。
階段と同じ答えの対は「隠れ 21 対(階段 21、同じ答え 0、要確認 0)」という件数の注記に畳まれ、`--show-shadow` で全対を一覧できる。
`policy unique` での重なりは同型の文面で error[E105] になり、末尾が「出力が異なります(1200円 と 800円)。どちらが正しいか決めるか、順序に意味を持たせるなら 方式 上から を宣言してください」に変わる。
出力まで同じ重なりには「出力まで同じなら、片方を削ります」を出す。
E105 になるのは、その入力を作れた(実在を確認できた)重なりに限る。
構成できず、実現不能の証明もない重なりは warning[W114] に落ち、生成コードのガードが引き受ける(§6.2、§8.1)。
「同時には起きない」を書き手にそのまま言っても伝わらないので、文面は「同時に起きる注文が存在するかどうかを判定できなかった」の形に固定する。
```
warning[W114]: 未確認の重なり: 行2 と 行4 の両方に当たる入力が有り得ます
--> rules/クーポン併用.rule:31 表 適用判定(方式 一意)
|
29 | | <=1000円 | - | false | 行2
31 | | - | >=3980円 | true | 行4
|
重なる条件: 残高A <= 1000円 かつ 残高B >= 3980円
二つの導出(残高A = 合計 - 割引A、残高B = 合計 - 割引A - 割引B)が入力を共有しているため、
この条件を同時に満たす注文が存在するかどうかを、検査は判定できませんでした。
存在するなら: 行を直してください。出力が異なる(偽 と 真)ので、当たれば矛盾です。
存在しないなら: このままで構いません。生成コードには、万一この条件に当たる入力が来たとき
黙って行2 を選ばずエラーを返すガードが入ります。
この警告は check --diff-base では新規分だけ表示されます。
```
なお、この例の重なりは実際には実現不能である(割引B は 0円 以上なので、残高B は定義から必ず 残高A 以下。残高A が 1000円 以下のとき 残高B は 3980円 に届かない)。
導出ごとに独立な区間のふるいはこの従属を見ないので、判定できずに W114 になる。
Fourier–Motzkin 消去なら証明できる形で、W114 が実表に積もることが、まさに §15-4 の増強の発動条件になる。
台帳をここで凍結する。
番号と意味は安定 API で、文言だけ改善してよい。
**台帳の本体は `src/codes.rs` に移した(2026-09-13)。** 各コードの「いつ出るか」「どう直すか(書き換え後の形まで)」「最小の再現 `.rule`」「関係するコード」はそちらが持ち、`rulec explain <CODE>` が引き、`docs/codes.md`(英)と `docs/codes.ja.md`(日)は `rulec explain --all --format markdown` の出力そのものである(手で編集しない。テストがチェックイン済みの中身といまの出力の同一性を検査する)。本節に残すのは番号と意味の凍結、そして番号を決めたときの理由だけである。同じ台帳を二か所に書けば必ず片方が腐るからで、腐るのは決まって説明のほうである。**空き番は無くなった**(E009 と E110 は M1 で、E111 は例の出力列の検査で埋まった。増分は末尾から足し、番台をまたがない)。
台帳の項目には**走る最小の再現**が必ず付く。文面だけの台帳は、検査器が変わった日から静かに嘘になる — テストが全項目の再現を `check_source` に掛け、そのコードが出ることを毎回確かめている。
構文と名前(E001〜E013):
- E001 文字列が閉じていない
- E002 読めない文字がある
- E003 ファイルが `rule` の行で始まらない
- E004 行の先頭に語がない
- E005 この位置で知らない語
- E006 宣言に `=` がない
- E007 知らない方式
- E008 空のセル
- E009 宣言の名前が予約語と衝突している(`結果(verdict)` や `enum range` を、行指向のパーサがセクションの始まりと読んで黙って捨てる事故の再発防止。構文解析の時点で弾く。M1 で空き番から埋まった)
- E010 裸の `..`(「<=2000g と >2000g のどちらの意味か」を聞き返す)
- E011 公開面の別名欠落(不足している名前の一覧と書き足す位置を示す。§1.3)
- E012 宣言されていない名前
- E013 取込先が無い
表の検査(E101〜E113。E101〜E105 は上の五文面):
- E106 丸め格子に載らない出力リテラル(対象はリテラルのみ。§7.2)
- E107 例の不一致(発火行つき)
- E108 オーバーフロー未証明(範囲宣言からの区間演算。§7.2)
- E109 検査予算超過(未証明として失敗)
- E110 型を解析できない列があるのに、表の検査が黙って飛ばされようとした(内部の防波堤。日付と optional で二度起きた「検査の静かな素通り」を、個別修正ではなく類として塞ぐ。これが出たら rulec 自身のバグである。M1 で空き番から埋まった)
- E111 例の期待値に出力の列が無い(宣言した出力のうち、`examples` が書いていないものがある。実装間の一致は全部が同じ誤りを共有すると緑のままで、それを破れるのは人の書いた期待値だけなので、欠けた列はエラーにする。§1.2)
- E112 導出の範囲が到達区間を含まない(下に文面)
- E113 真偽定義の原子が許された二形(入力・導出への単項テスト、導出で差の取れない型どうしの同型比較)のどちらでもない。数値どうしの直接比較には「差を導出として宣言してから定数と比べる。そのほうが厳密に解析できる」を、表出力との比較には真偽出力列の追加を提示する(§5.3)
警告(W105、W110、W111、W114):
- W105 要確認の隠れの一覧(分類は §4。階段と同じ答えは件数の注記に畳む)
- W110 重なりのない `first`(`unique` を提案)
- W111 使われない宣言(入力、導出、群、どのセルにも書かれない列挙値)。入力が `range` の入口検査としてだけ効いている場合は、書き忘れの徴候と正当な契約の両方がありうる。実測ではゆうパックの重量がこれで、どの表にも現れず 25kg の上限としてだけ働く。正当なら宣言に `contract_only` を付けて黙らせ、付けない限り警告は残る(ヤマトでは同じ重量が実際に列として使われるので、既定を沈黙にはできない)。実装の初回に、道具はコーパスの重量をこの形のとおり独立に再検出した。列挙値がセルに書かれているかを見る検査には、限定が二つある。第一に、対象はそのファイルで宣言された型だけで、取込した型は対象外とする。都道府県を `not: 沖縄` の形で使う規則は 45 値がどのセルにも書かれないのが正常で、ここで鳴らせば警告の通り道ごと死ぬ(共有型の語彙の生死は、ディレクトリ単位の検査として将来の候補に留める)。第二に、正当な既定落ちは値の宣言に `default` を付けて黙らせる(catch-all を持つ表では、値を書かないことが正しいモデリングでありうる。§2.1)。この検査も実装の初回に実在の欠陥を一つ見つけた。同じ概念が二つの型に重複していた値(クーポンの `送料`)で、どのセルにも書かれていないことがその徴候だった。ヒントは両方の出口を示す。意図した既定落ちなら `default` を、二重の語彙なら値の削除を。
- W114 未検証の重なり(`unique` のみ。上に文面。生成コードのガードと対になる。§6.2、§8.1)
**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)。診断には鍵の種類と接頭辞と長さだけを出し、鍵そのものは出さない。行の枠も出さない(その行に鍵があるため)。場所は `-->` の行と、JSON の `line`・`column`・`where` が言う(`spans` は空)。三つの段の前に全文から探し、どの段も止めない。テスト用の値は、同じ行のコメントに `ritsu: test secret` と書けば言わない。9xx の番号は、ritsu のどの言語でも同じ検査を指す(ritsu の DESIGN 16.2)。番台をまたがない決まり(上)の外にある番号で、台帳の末尾に置いた。ほかの診断の枠が鍵のある行を見せるときは、その行の鍵を ritsu-base の `secrets::mask` で接頭辞と `…` に替え、`^` もその見せ方で数える。
E112 は導出のときに出す入力の形を決めるので、文面を示す。
```
error[E112]: 導出の範囲が、実際に到達しうる値を含んでいません
--> rules/クーポン一枚.rule:16 導出 残余
|
15 | derive 残余(margin) : money[円, incl_tax] = 商品合計 - 適用済割引 - 額面
16 | range >=0円 <=100万円
| ^^^^^ 到達区間は >=-110万円 <=100万円 です
|
例: 商品合計 = 0円, 適用済割引 = 100万円, 額面 = 10万円 のとき 残余 = -110万円
原因: 範囲が狭いと、完全性の検査が実際に起きる値を見ないまま「完全」と答えます。
ヒント: range >=-110万円 <=100万円 に広げてください。到達しない分まで広げても、
実現不能な領域として検査がふるいうので害はありません。
```
## 12. 実装言語と配布形態
**決定**:rulec は Rust で書く。配布は単一静的バイナリ(macOS arm64/x64、Linux x64/arm64 は musl 静的リンク)を GitHub Releases に置き、CI では sha256 を固定してダウンロードする。同じバイナリを Homebrew と、`.deb`・`.rpm` からも入れられるようにする。crates.io は用意だけして保留している(§15.158)。
理由。
この道具の本体はコンパイラ(AST、型付け、領域代数、正確な有理数演算、決まった順で入力を作ること)であり、直和型とパターンマッチが実装の正しさに直接効く。
三候補のうちそれを言語本体で持つのは Rust だけである。
検査は 10⁶ ノード規模の探索なので Python は速度で外れ、Go は書けるが IR の直和が interface の運用規約になり、コンパイラを書くには一段落ちる。
Rust なら診断表示や有理数の枯れたクレートも使える(rulec 自身の依存はよい。ゼロにする対象は生成物である)。
生成物の行き先が Python と Go である事実は認めた上で、こう整理する。
毎日触られるのは `.rule` と生成された Python と Go であり、rulec 本体を触る人はコンパイラを書く人に限られる。
道具の保守者の裾野より、道具の正しさと配布の単純さ(ランタイム不要の 1 ファイル)を取る。
CI への組み込みは四行で完結する。
```yaml
- run: rulec fmt --check rules/
- run: rulec check rules/ --diff-base origin/main
- run: rulec gen rules/ -o generated/ --check
- run: rulec test generated/ # python3 と go の toolchain を使う唯一の段
```
replay は fixtures を持つ環境だけの別ジョブとし、`rulec replay` が Markdown を吐いて `gh pr comment` で貼る。
`--lang` はどのコマンドにも付けられる。CI では言語を明示する(環境に `RULEC_LANG` を置くか、各行に `--lang`)。生成物と PR コメントの文面が機械で変わらないためである(§11 原則 7)。
レポート整形まで rulec が持ち、投稿は CI の一行に任せて CI ベンダ非依存を保つ。
配布は ritsu のリリースが担う(ritsu の DESIGN 13.2、§15.186)。ritsu のリリースは、`ritsu` と、言語ごとにそれを指すリンクを入れたアーカイブ四つ(macOS の arm64 と x64、Linux の x64 と arm64。Linux は musl で静的にリンク)と、`.deb`・`.rpm` を、`SHA256SUMS` と一緒に GitHub Releases に置き、formula を tap に置く(`i2y/tap/ritsu`)。`rulec` のリンクから呼ぶと、`ritsu` は rulec として動く。CI では、ritsu の根の `action.yml` が、そのリリースのアーカイブを `SHA256SUMS` と突き合わせて runner の PATH に置く(`uses: i2y/ritsu@v0.23.0`)。0.22.1 までは、このクレートの `.github/workflows/release.yml` と `action.yml` が rulec だけのリリースを作っていた(§15.42、§15.158)。
**捨てたもの**:
- **Go 実装**:書ける人の裾野は最大だが、上記のとおりコンパイラの内側で支払いが続く。
- **Python 実装**:検査の速度と、CI への配布(venv の管理)で二重に不利。
- **pip / npm ラッパ配布**:単一バイナリに対する追加価値がない。要望が出てから考える。
### 12.1 CLI は契約である
**決定(2026-09-13)**:コマンドとフラグの定義を `src/main.rs` の一枚の表に置き、`--help` の描画と引数の解析が同じ表を引く。`rulec <cmd> --help`(`rulec help <cmd>` も同じ画面)は、目的・引数・フラグ(値の型と既定)・exit code の意味・走らせられる例を二つ・出しうる診断コードを出す。`rulec --help` は一覧、`rulec --version` は版の一行だけ。
**知らないフラグは exit 2 で止める。** 同じ扱いを、値が閉じた集合の外にあるとき(`--format jsn`)、値を取るフラグに値が無いとき、繰り返せないフラグが二度書かれたときにも広げた。理由は第一の利用者が変わったことにある。人間なら出力を見て「あれ、効いていない」と気づくが、エージェントは `--shwo-shadow` と打って exit 0 を受け取ると、要求が通ったと信じて次に進む。**黙って無視することは、嘘の緑を返すことと同じ**である(§0 の「不健全な緑を出さない」と同じ原則を、CLI の面に広げたもの)。
**捨てたもの**:
- **clap などの引数パーサ**:依存を足さない方針(§12)の外にあるうえ、この表が持つべき情報(exit code の意味、例、関係する診断コード)はどのパーサも持たない。
- **未知のフラグを警告にして続行**:警告は読まれない。CI のログに一行増えるだけで、誤った引数のまま緑が出る。
- **短いフラグ(`-f` `-o`)**:綴りが二通りになる。§1.1 で語彙に同義語を置かないと決めたのと同じ理由で、CLI にも置かない(`-h` と `-V` だけは、慣習として `--help` `--version` の別名に残した)。
- **`rulec` を引数なしで叩いたときに exit 0**:何もしていないのだから 0 ではない。全体の `--help` を標準エラーに出して 2 で止める。
## 13. MVP の切り方
**決定**:四段。M0 と M1 は、データも旧実装も本番のデータも一切使わずに完成する(P6 の第一段)。受け入れ条件の中心には、実測の転記を格上げした回帰コーパスを置く。
**回帰コーパス**は、公開情報だけから作った「通る `.rule`」の集合で、`tests/corpus/` として git に入れ、仕様の一部として扱う。
収録するパターンは実測で現れたものすべてである。
- ゆうパック基本運賃:47 都道府県 × 7 サイズ、群 6、3 段の連鎖(§5.1)
- ヤマトのサイズ判定:3 辺計と実重量の大きい方(単項セルと `first`。§3)
- 楽天の送料無料ライン:クーポン利用前の金額で判定(素の入力列)
- 39 キャンペーン:クーポン適用後の金額で判定(導出。§5.2)
- 複数クーポンの素割引:種別で基準額が変わる(出力セルの名前。§3.2)と割引上限(`result` の 最小)
- 併用制約:`同商品適用済` を入力に(§5.4)
- 適用順序:期限の日付比較の二引数規則(日付型。§5.4)
- 相関する導出:入力を共有する導出二本が作る、ふるいに見えない実現不能の重なり(W114 の実演。クーポン併用)
- 設計文書 §1.2 の送料スケッチ:真偽の `define` を列に使う正常系、率の出力、`default`、要確認の隠れ 1 対の実演
コーパスはパターン単位で最低一つの行使ファイルを持つ。
検査の意味論として新しくない転記は、独立のファイルを必須としない(ヤマトの大きい方は単項セルの多列 `first`、送料無料ラインは入力列の閾値、39 キャンペーンの判定は導出列の閾値で、いずれも既存ファイルが行使している)。
逆に、どのファイルもまだ行使していないパターン(適用順序の日付比較、真偽定義の列)は、M0 の宣言前にファイルを足す。
日付型はこのコーパスが要求するので、v1 の型付け(M0)と生成(M1)に含める。
- **M0 検査器**:fmt、parse、型付け、§11 の台帳の全検査(E001〜E013、E101〜E109、E112、E113、W105・W110・W111・W114 と起きうるかのふるい。ふるいは導出軸まで、定義軸の入力の検証は M1。§6.2)、例の実行。完成の定義は五つ。(1) コーパス全部が check を通る。(2) 五種の誤りを仕込んだ変異ファイルが §11 の五つの文面をそのまま出す。コードの一致だけでなく、描画された文面をスナップショットで固定する。(3) fmt が冪等で、§1.5 の正規化を行い、`--check` で CI に組める。(4) `--format json` と exit code が §11 の原則 6 のとおりに出る。(5) `--diff-base` が要確認の隠れと W114 の新規分を差分で出せる。検査予算の既定値を §15-2 のベンチで決めることも、M0 の出口に含める。この時点で「手元の Excel や公開規約を転記して check に掛ける」という使い方が成立し、実測が示したとおり、転記の過程そのものが穴と重なりを見つけ始める。
- **M1 生成器**:Python と Go の生成、ベクタ生成、三者一致テスト(評価器、生成 Py、生成 Go)。完成の定義は、コーパス全部が両言語に生成でき、全ベクタで三者の出力がバイト一致し、生成物が gofmt と PEP 8 に素で通ること(PEP 8 は行長 E501 を除く。§8.5)。ここまでで「単一記述から両言語へ、ズレの証明つき」が成立する。
- **M2 等価検証**:アダプタプロトコル、テンプレート印字、verify レポート。旧実装へのアクセスが要る最初の段。レポートは §10 の設計どおり、発火行クラスタ、件数と金額差、入力例、そして出力格子未満のずれへの「丸め差異の疑い」タグを含む。一致率の分母から外した件数は、外した理由ごとに明示する。
- **M3 過去再生**:fixtures lint、replay、diff、PR コメント用 Markdown。データが要る唯一の段。
完成の記録。M0 は宣言条件込みで完成。M1 と M2 も完成した(コーパス 7 本、テスト 68 本、被覆判定器と三つの網、定義軸のふるい、ガードまで。コミット 5acaf2e、569c104)。
追補が一つある。
生成テストファイル(§9.3 の三つ目の使い道)と `rulec test`(§12 の CI 四行目)は、どの段の完成定義にも割り付けられていなかったことが M3 の着手時に見つかり、M1 の取りこぼしとして M3 期間に実装された。
M3 も完成した(fixtures lint、replay、diff、markdown、`rulec test`。テスト 80 本、m3 の受け入れ 9 本。コミット eb83e28、26a62e5。実データはゼロで、ベクタから合成した fixtures による行使。切り分けの有効性を実データで確かめる仕事は、§16 第二のとおり、記録を持つ側に残る)。生成物が外部の依存を持たないことは、`rulec test` が `GOPROXY=off` で走らせることで仮定ではなく検査された性質になっている。
残件の台帳も置く。
`rulec import`(§1.4)と `rulec changes`(§1.5)は、どの段にも割り付けられていない。(`import csv` は 2026-09-16 に実装した。§15.41。`import xlsx` は 2026-09-17 に実装した。§15.50。`changes` はまだである。)
`rulec doc`(§1.6)は M3 後の最初の増分として実装され、二条件(畳んだ見出しのゲート、`not` の件数)込みで完成した(コミット 80fdf0d +条件の commit)。
どちらも導入期の便宜(Excel からの移行の入口、PR レビュー用の構造差分)で、正しさの機能ではない。
実際の導入が始まるとき、要否ごと決める。
ここに書くのは、黙って消えた顔をさせないためである。
2026-09-13 の増分。キーワードを日本語から英語に切り替え(§1.1)、ソースコードのコメントを英語にし、文面を日英二言語にした(§11 原則 7、`--lang`)。日本語の出力は一文字も変えていないので、golden と受け入れテストは書き換えた語彙の分だけが動いた。
同じ日に、出力列の印と集合の区切りも ASCII(`->` と `,`)を正とし、`→` `・` `、` は読んだ上で `rulec fmt` が正準化する形にした(§1.5)。名前とセルの値以外に IME が要らなくなった。残るは M3 で、実装は合成 fixtures(ベクタと、欠陥を仕込んだアダプタ)で全部行使でき、実データでの検証だけが残る(§16 第二)。
「動くもの」の宣言は M1 で行う。
M0 だけでも道具として立つが、依頼の核(単一記述から両言語)を示すのは M1 だからである。
## 14. 名前
**決定**:`rulec` を正式名として採用する。
rule compiler がそのまま読め、小文字 5 字で打ちやすく、既存の著名ツールと衝突しない。
検討した代替は、`dtc`(decision table compiler。device-tree-compiler と衝突して即死)、`tablec` と `decc`(何の表か、何の dec かが読めない)、和名(掟、定め。CLI に打つ名前としてローマ字化の揺れが出る)。
どれも rulec を上回らなかった。
公開前に crates.io と Homebrew と PyPI の名前空間を一度だけ確認する。
## 15. 未決事項と測定計画
実測で決着した v0 の未決を先に記録する。
セル内の区間の合併は 0 件で v1 から除外(§3)。
列の線形条件は導出として取り込み、SMT は当面不要(§5.2、§6.2)。
日付型は v1 入りが確定(§13)。
クーポンの重ね掛けは、粒度の構成で書けることを示した(§5.4)。
ただし前回そこから「集約は不要」と閉じたのは測り方の誤りだった。
書けるように書いた自選の二表から、必要性の不在は導けない。
撤回して §15-8 に開き直す(経緯は §4)。
残るものと、新たに開いたもの。
ここから先の大半は、動く検査器がないと測れない。
机上調査はここで打ち止めにし、以降の測定は M0 の実装と実地の転記で行う。
1. **文字列のパターン一致**:公開規約の転記では文字列が列に一度も現れなかったが、実運用のクーポンコード体系は公開情報では確認できない。実データ(コード形式の一覧)を見て、完全一致で足りるか前方一致が要るかを数えてから決める。
2. **検査予算の既定値(決着)**:M0 で実測した。設計目標(列 12、行 500)は 1,004 万ノード、164 ms で予算内。訪問数は行数に二乗、速度は約 6 万ノード/ms。既定は 5×10⁷(§6.3)。
3. **Go の入力契約違反の流儀**(error か panic か):組み込み先の Go サービス 2〜3 本の慣行を見て合わせる。生成器のフラグにはしない(生成物の形は一つに保つ)。
4. **相関する導出と Fourier–Motzkin 消去**:導出が入力を共有する形は、コーパスのクーポン規則が最初から含んでいる(`最低差` と `支払差` が `現在残額` を共有)。ただし当の表は `first` で、W114 を生む `unique` との組合せはまだ出ていない。コーパスを広げるたびに、導出の本数の分布と W114 の件数を数える。発動条件は二つのどちらか。W114 が実表に出続けること、またはガード(§8.1)に本番で当たりが出ること。どちらも起きるまでは足さない。効果の物差しも用意できた。コーパスのクーポン併用にある W114 一件(区間のふるいには見えない実現不能の重なり)が、FM を入れた日に消える。
5. **畳み込みの宣言化**:`畳み込み 順序=適用順序 一回=クーポン割引 累積=残額 ← 残額 − 割引額` の形で反復を宣言にし、参照評価器が畳み込みまで実行できるようにする案。見送りの理由と暫定の緩和(連鎖の例)は §5.4 に書いた。採否は次の三つで測る。(1) 二本目以降の実畳み込み(ポイント付与、按分、Yahoo の重ね掛け)を転記し、「比較規則によるソート → 段ごとの純関数適用」の形に収まるかを確かめる。(2) 各段の入力を「アキュムレータと当該要素から導ける」「畳み込み全体の定数」「外部状態」に分類して数える。外部状態が 0 で、アキュムレータがスカラーか固定形のレコードに収まる実例が 2 件以上そろい、かつコーパスのクーポン規則も同じ形に書き直せるなら入れる。(3) クーポン重ね掛けの呼び出し側を両言語で試作し、fold 単体ではなく、規則を呼ぶために書くホストコードの総量(射影、反復、前後処理)を行数と分岐数で数える。二重実装は畳み込み、射影、前後処理に分かれて出るので、別々に見ていると合計がいつ許容を超えたか分からない。fold が定型 30 行程度に収まるなら宣言化の利得は薄いままとし、膨らむか二実装が食い違うなら圧力が上がる。入れると決まった日のための設計メモも残す。順序規則には全順序性の検査が要る(全域・反対称・推移。二引数規則は積空間上の表なので、反対称は行対の突き合わせ、推移は圧縮同じ答え類上の三重ループで有限に検査できる)。累積には初期値と各段の出力範囲からの帰納で範囲不変条件を検査する。丸めは各段の出力宣言が既に持つ。生成形は固定の短いループ骨格と一段関数の呼び出しで、1 行 1 分岐の基準は表側に残る(P7 が禁じるのは DSL 表面のループであって、生成物のループではない)。
6. **入力の射影と由来の宣言**:入れ子のドメインオブジェクトへの述語(`注文.配送先.都道府県`、`明細のいずれかが冷蔵品`、`count(明細) > 3`)は現実に普通にあり、平たい入力への射影は今はホストの手書きである。コーパスの入力は 13 個(クーポン 10、運賃 3)で、素朴に見積もれば射影だけで両言語 26 行の未検査グルーになり、規則を増やすたびに線形に増える(これは見積もりで、実数は §15-5 の試作で数える)。拡張候補は、入力宣言への由来の付記(`届け先(dest) : 都道府県 ← 注文.配送先.都道府県`、`冷蔵品あり : bool ← 注文.明細 のいずれかが 区分 = 冷蔵`)と、既存スキーマの借用である。rulec は独自のドメインオブジェクト型システムを作らない(非交渉)。呼び出し側に既にある JSON Schema / protobuf / OpenAPI を読んでパスの存在と型を検査し、両言語の射影関数を生成する。受け入れの根拠は、検査に一切触れないこと。射影の出力はスカラーの入力で、領域解析も区画の代数も無傷、言語機能ではなくコード生成機能である。先に置く設計上の注意が三つ。(1) 由来は check に不要な注記とし、スキーマが無くても検査は完結する(照合と生成は gen 側)。(2) 生成物ヘッダに原本ハッシュと並べてスキーマの版も刻む。スキーマと本番の乖離は、新しい偽の緑の通り道になるからである。(3) 射影の言語は「一つのコレクションへの いずれか / すべて / 件数 と、要素フィールドへの単項述語」で打ち止めにし、結合や入れ子の量化は実例を数えてから。セルにパス式や量化子を入れる形は採らない(§0)。優先度は畳み込み(§15-5)より上に置く。量が規則ごとではなく入力ごとに増えることに加え、畳み込みは動機の実例を表せずに見送られたのに対し、射影は実例を既に表せて検査にも触れず、採用の障壁が低い。着手は §15-5 の試作で総量を測ってから。
7. **按分**(2026-09-13 に測定、決着):配り方で割れる。**逐次充当型**(順に上限まで充てる)は「一回の判定」の粒度にそのまま収まり、`tests/corpus/値引の充当.rule` として収録した。呼び出し側は明細を回して残額を引くだけで、合計は構成上ぴったり合う(配りすぎも配り残しも起きない。2,000 通りの合成明細で確認)。**比率按分**(定価の比で割り付け、端数を再配分する)は書けない。`明細 / 合計` が §7.4 の「除算は正の整数定数と同型の定数に限る」に当たり、比率を呼び出し側で作って率の入力で渡す迂回も、率の刻みが実装上 1% 単位までしか書けないため実用にならない。粒度原則の例外に格上げはしない — 逐次充当が書ける以上、原則が破れたのではなく、比率按分が**除算の制限**に当たっただけである。比率按分を入れる日に動かすのは §5.4 ではなく §7.4 と率の実装になる。
8. **集約(C)の需要**:「実測のクーポン転記でも集約は要らなかった」という前回の記述を撤回して開き直す(経緯は §4)。公開調査で分かったのは、実需要の主形が検証(当てはまる項目の全収集)であることまでで、不在の証明ではない。以降は机上ではなく実地で数える。M0 の後、実表を転記するたびに「単一ヒットで書けなかった」「書けたが排他の書き込みが苦痛だった」事例を、検証の形と集約の形に分けて記録する。採ると決まったら、§4 の記録(排他と網羅の検査が意味を失う取引、範囲の決断)を前提に設計し直す。
9. **単項原子の DNF 代入**:真偽定義は自由軸で出荷した(§6.2)。代入すれば定義の中の閾値(`注文金額 >= 3万円` の 3万円)まで領域解析が見るようになり、実現不能な組合せ起因の過剰報告が減る。発動条件は、その過剰報告(穴や W114)が実表で邪魔になること。それまでは、ベクタが原子の境界を直接踏むこと(§9.1)で検査の穴を塞ぐ。§15-4 の増強(FM 消去)とは独立に発動できる。
### 15.9 按分を測ったときに出た実装の欠陥(2026-09-13)
§15-7 を測る過程で、設計どおりに動いていない箇所が三つ出た。いずれも**この道具の検査機構そのものが見つけた**ものではあるが、直すのは別作業として記録する。
1. **率の入力の入口ガードが誤っている。** `rate[step 1%]` の入力に `range >=0% <=100%` を宣言すると、生成コードのガードが `0 <= r <= 1` になる。ワイヤ表現は刻みの個数(0〜100)なので、1% を超える値がすべて弾かれる。さらに、その規則の生成物は参照評価器と食い違い、`rulec test` が赤になる(生成 0 / 期待 1)。範囲の Rat から分子だけを取っていることに由来する(§8.6 の `range_json` に同じ注記がある)。**率の入力を持つ規則は、いま正しく生成されない。**
2. **小数のリテラルが字句解析に通らない。** §2.1 は `0.5%` と `rate[step 0.1%]` を仕様として書いているが、実装は `.` を E002(読めない文字)でエラーにする。率の分解能が実質 1% 単位に限られ、比率按分の迂回路が塞がっている原因でもある。
3. **使われない表の出力列があると、生成 Go がコンパイルできない。** Go は未使用のローカル変数をコンパイルエラーにするので、どこからも読まれない表の出力があると `declared and not used` で落ちる。Python は通るので、`rulec test` の Go 側だけが落ちる。W111(使われない宣言)は入力・導出・群・列挙値を見ており、表の出力列は見ていない。
三つとも、コーパスの 7 本では一度も踏まなかった。**コーパスが行使していない構造は検査されていない**という、当たり前だが忘れやすい事実の実例である。
### 15.10 三つを直したときに出たもの(2026-09-13)
§15.9 の三つを直した。直す過程で、同じ根から出ている欠陥がさらに四つ出たので、合わせて記録する。
1. **ワイヤ表現の変換点が散らばっていた。** 率は「格納する値」と「運ぶ値」が違う唯一の型で、その境界を越える場所が十箇所あり、変換していたのは一部だった。JSON Schema の上下限、診断が出す入力、記録の読み込み、verify/replay/diff の比較と差額、入口ガード、表の出力の代入。`Checked::wire_scale` と `wire_int` / `from_wire` の一点に集め、全部をそこに通した。**捨てたもの**:値の側に単位を持たせて自分で変換させる案。`Val` は評価器の内側の型で、そこに表現の都合を持ち込むと評価器が表現を知ることになる。
2. **ベクタの重複判定が値を整数に切り詰めていた。** `vectors::show` は表示であると同時に重複判定の鍵でもあり、100% 未満の率が全部 "0" になっていた。率の列は軸まるごと一点に潰れ、行被覆も境界被覆も満たせない。`coverage` はそれを正しく「欠け」と言い続けていたのに、こちらが読んでいなかった。表示と鍵を分け、鍵は正確な値にした。
3. **出力のセルが最初の語だけを採っていた。** §3.2 は出力のセルに式を許さないが、実装は残りを黙って捨てていた。`| - | 商品合計 × 割引率 |` から、掛け算の無い生成コードが出る。**E014** を足した。
4. **刻みの間の値が黙って刻みに寄せられていた。** `rate[step 0.1%]` の列に `<=0.05%` と書くと、生成コードの比較は `<= 1`(0.1%)になる。紙面で読める境界と動く境界が食い違う。**E114** を足した。出す入力についても同じで、開いた区間では中点を出していたが、中点は型が持てる値とはかぎらない(`(10%, 15%)` の中点は 12.5%)。閉じている側から一刻み入った点に変えた。日付だけは最初からそうしていた。
**決定**:コーパスに `決済手数料.rule`(刻み 0.1% の率)を足した。§15.9 の教訓そのままで、行使されていない構造は検査されない。
### 15.11 単位のない数の型(2026-09-13)
**決定**:無次元の整数型 `number` を入れる。件数・日数・点数のように単位を持たない数を、偽の単位をでっち上げずに書けるようにする。範囲宣言は他の数値型と同じく必須、出力なら `round` も必須(§7.2 を弱めない)。演算では率と同じ無次元数として振る舞い、`金額 × number` は金額、`金額 ÷ 同通貨の金額定数` は `number` になる。最後のものは §2.3 が最初から「100円につき1点」用として書いていたのに、結果を入れる型が無かったために書けなかった規則である。
同時に、除算の実装を直した。`x ÷ k`(k は定数)は**整数を割らない。スケールを k 倍する**だけでよく、それが値を厳密に保つ唯一の形である。実装は `//`(Python の床除算)を出していたので、参照評価器の厳密有理数と食い違っていた。コーパスに除算を使う規則が一本も無かったので、実装間の一致が踏まずに残っていた。`ポイント付与.rule` を足した。
**捨てたもの**:
- **`count[件]` のようにラベル付きの単位を持たせる案。** 件数と日数を型で分けられるが、ラベルは brand にしかならず、業務の側に「この数の単位は何か」を考えさせる手間が増えるだけだった。単位が無いことこそがこの型の意味である。
- **生成物で `Number` という brand を作る案。** 件数も日数も同じ brand になるので取り違えは防げず、呼ぶ側に整数を包む手間だけが残る。Python は `int`、Go は `int64` のまま出す。
### 15.12 無次元の値と、変数による除算(2026-09-13)
「複数の評価項目を合算し、満点に対する達成率でランクを付けられるか」を測った。答えは**書ける**で、`tests/corpus/評価ランク.rule` に収録した。重み付きの合計は `derive`、達成率は `define … : rate = 合計点 ÷ 満点定数`、ランクは達成率の列を持つ表になる。**実行時に割り算は起きない** — 満点が定数なので `>=90%` は生成コードで `合計点 >= 45` に畳まれる。
測る過程で三つ出た。
**決定**:**無次元の値は、宣言のほうが名前を決める。** `rate` と `number` はどちらも無次元で、実行時の表現も同じ(刻みつきの整数)なので、`derive`/`define` の宣言が `rate` なら率、`number` なら数として受ける。`達成率 : rate = 合計点 ÷ 50` は割合として `>=90%` と書けて、`基本点 : number = 税込金額 ÷ 100円` は点数として `>=10` と書ける。**捨てたもの**:`unifies` そのものを緩めて率と数を混ぜられるようにする案。それだと `品質 + 10%` まで通ってしまう。緩めるのは宣言の突き合わせ(`check_same`)だけに留めた。
**欠陥(直した)**:**変数で割れてしまっていた。** §2.3 と §7.4 は除算を定数に限っているが、実装は検査を持っていなかった。`合計点 ÷ 満点`(満点は入力)が `check` を通り、生成 Python は `//`(床除算)を出し、参照評価器の厳密有理数と食い違い、生成 Go はコンパイルさえできなかった。**E115** を足した。コーパスに除算を使う規則が一本も無かったので、実装間の一致が踏まずに残っていたもので、§15.10-2 と同じ形の見落としである。
**欠陥(直した)**:**ベクタ生成器が入力を一つしか動かせなかった。** 導出の目標値に届かせるために動かす入力を一つだけ選び、その入力の範囲で足りなければ諦めていた。`品質 × 2 + 納期 + 価格 + 対応`(各 0〜10)で合計 45 を狙うには複数を動かす必要があり、行カバーが 2/4 に落ちていた。各入力を自分の範囲いっぱいまで動かし、残りを次の入力に回す形にした(各手は評価で確認するので、非線形な式は誤って置かれるのではなく置けずに失敗する)。4/4 と 6/6 に戻った。
**残した課題**:それでも、境界の外側の点が「二手先」にしかない配置には届かない。合計 64 から 63 へ、重み 3 の項を 1 下げて(61)重み 1 の項を 2 上げる、という手順が要る場合である。最初の一手が目標から遠ざかるので、いまの貪欲法では見えない。重みを 2:1:1:1 にすれば一手で届くため、コーパスの例はそちらにした。整数計画に踏み込むかどうかは、実表でこの形が出てから決める。
### 15.13 生成対象の言語(2026-09-13、TypeScript を実施)
**決定**:TypeScript を三つめの生成対象にした。残りは Swift・Kotlin・Java・SQL を候補として順に置く。
一本通してみた実コストは **codegen.rs に約 700 行、そのほか runtest・api・テスト・文書で約 280 行**。生成器はいま「Python 風のテキストを一度組み立て、言語ごとに後段で綴りを直す」形(`go_expr` / `ts_expr`)で、C 系の言語はこの道をそのまま使える。三つめを足した時点で、言語ごとの差分をトレイトに切り出す設計変更は**まだ割に合わない** — 差分は型名・ゼロ値・分岐の括弧・ランナーの四箇所に集中していて、`Lang` 列挙一つで足りた。五つめあたりで再評価する。
**決定(TypeScript)**:数値は `bigint` にする。E108 の証明は int64 に対してであり、JavaScript の `number` は 2^53 までしか正確でないので、`number` を使うと 9000 兆を超えたところで黙って間違う。**黙って間違わないことがこの道具の存在理由**なので、ここで妥協しない。単位は branded bigint(`bigint & { __rulec: "YenInclTax" }`)で、実行時の費用はゼロ。
**捨てたもの**:`enum` を使う案。TypeScript の `enum` は消去可能構文ではなく、`node file.ts` の型ストリップで動かない。凍結オブジェクトと合併型にしたことで、**ビルド手順も tsconfig も要らずに `node` だけで走る**生成物になった。`rulec test` が node だけで TypeScript 側を回せるのはこのため。
**残りの候補**(着手順は未定)。
| 言語 | 要るもの | 形の注意 |
|---|---|---|
| Java | JDK | 単一ファイル実行(`java Main.java`)でランナーがビルドツール無しに書ける。`long` は int64 そのもの |
| Kotlin | kotlinc | JVM。コンパイルが遅く、CI の時間が目立つ |
| Swift | swiftc | 動かせる環境が限られる。`Int64` はそのまま |
| SQL | 2026-09-16 に実施(§15.46) | 形が違う。行が分岐ではなく `CASE` の枝になり、標準入出力の JSON ランナーが作れないので、一致検査の取り方から設計が要る。方言(SQLite / PostgreSQL)、出す形(スカラー関数 / CASE 式 / ビュー)、整数除算の方言差が論点 |
**原則**:新しい言語は、**一致検査に乗らないなら入れない**。参照評価器とバイト単位で突き合わせられない生成物は、証明済みという看板の外側にあることになる。
### 15.14 ASCII 別名は、書いた場所で効いていなかった(2026-09-13)
「`table 運賃表(fee_table)` の括弧は意味があるのか」という問いから、§1.3 と実装が二つの方向でずれていたことが分かった。
**欠陥(直した)その一:全部 ASCII で書いた規則が通らなかった。** §1.3 が別名を要求する理由は「漢字は大文字を持てず、Go の公開識別子になれない」ことなのに、実装は**名前が ASCII かどうかを見ずに**括弧の有無だけを見ていた。`rule shipping_fee v1` が E011 で止まり、通すには `rule shipping_fee(shipping_fee) v1` と書くしかなかった。名前がもとから ASCII なら別名は要らない、に直した。**この道具は日本語で書くことを想定しているが、英語だけで書くことを妨げる理由は一つも無い。**
**欠陥(直した)その二:内部名の別名が捨てられていた。** §1.3 は「内部に閉じる名前の別名は任意で、省略すると生成コードも日本語識別子をそのまま使う」と書いている。書けば使う、の意味である。実装には `pub_name()` があるのに、内部名には一度も呼ばれていなかった。`derive 残余(margin)` と書いても生成コードは `残余` のままで、**別名は解析されてから黙って捨てられていた**。識別子を決める場所を `Gen::ident` 一つにまとめ、群・導出・定義・表の出力列すべてをそこに通した。
**決定**:`table` の別名だけは行き先が無い(表は一つの関数にインライン展開されるので、表そのものの識別子が生成されない)。構文としては残し、**SQL 生成でビューや CASE に名前が要るときのために取っておく**。いまは使われないことを文法リファレンスに明記した。
直す過程で出たもの三つ。
- **Python も Go も、宣言した群の集合/述語を使わずに毎回メンバーを展開していた。** `_遠隔地 = frozenset(...)` や `func is遠隔地(...)` は宣言されるだけで、セルは `dest in {…47 県のうち 6…}` を書いていた。死んだ宣言であり、読みにくくもある。一つの群を名前で書いたセルは、その宣言を呼ぶようにした(TypeScript は最初からそうしていた)。
- **丸め前の一時変数が `raw` 固定だった。** 出力の別名が `raw` の規則(`クーポン割引`)で衝突し、Python は黙って再束縛、TypeScript は構文エラーになった。規則が使っていない名前を選ぶ `Gen::temp` にした。
- **読まれない `derive` / `define` があると生成 Go がコンパイルできなかった。** §15.10-3 で表の出力列について直したのと同じ形で、導出と定義には効いていなかった。`Gen::go_unread` に一本化した。
### 15.15 公開する日本語から「箱」と「証人」を外す(2026-09-14)
**決定**:README・ドキュメントサイト・診断の文面から「箱」と「証人」を外す。一行が覆う領域は **区画**、それを起こす具体的な入力は **それを起こす入力**(短い札が要る場所では「その入力」、表の列見出しでは「入力例」)と呼ぶ。英語側は box / witness のまま触らない。形式手法で定着した語であり、英語では浮かないからである。
理由は単純で、どちらも英語をそのまま訳したせいで日本語として浮いている。「箱」はアルゴリズムの比喩が表に出てきたもので、「証人」は法廷の言葉であって数の呼び名ではない。この道具の日本語は業務担当者が読む前提に立っているので、そこで浮く語は落とす。「区画」を選んだのは、一行が受け持つのは各列の幅を掛け合わせた長方形であり、日常の日本語でその形をした持ち場を指す語だからで、列が三つ以上でも言い方が壊れない。
**捨てたもの**:「反例」。意味としては正しく、形式手法の日本語としても定着している。捨てたのは、これも漢字熟語であることと、E105 に当てはまらないことによる。両方の行に当てはまる入力は何かの反例ではなく、矛盾そのものの実例である。呼び名を一つに保てないなら、呼び名を置かずに毎回「それを起こす入力」と書くほうが短く読める。
**残すもの**:DESIGN.md の「箱代数」「箱和」「交差箱」「残余箱」。ここは実装の設計記録で、読者は決定可能性の議論を追っている。公開文書と設計文書で語彙が割れることは承知のうえで、そのままにする。**この一段は §15.123 で取り消した。**
### 15.16 出力が二つ以上のときの返り値(2026-09-14)
三段の表をつないだ規則を書いて測ったところ、出力が二つ以上ある経路がまとめて壊れていた。見つかった順に四つ。
1. **`result` が生成コードから消える。** 評価器は `result` を一つ目の出力に当てる(§1.2)。生成側は出力を「同じ名前の束縛」から取るので、`result` の式をどこにも書かない。出力が一つのときは両者が一致するので露見しない。`rulec check` は `ok` を出し、`rulec gen` は束縛のない名前を返す Python・TypeScript・Rust・Go を書いた。
2. **刻みの割り戻しが無い。** 一つ出力の経路は、値の保持スケールが線上のスケールと違うとき `raw` に束ねて `/ (scale / out_scale)` で戻す。複数出力の経路はこの割り戻しを持っていなかった。**コーパスの `クーポン一枚` がこれに当たっていて、`素割引` が 100 倍で返っていた。**
3. **`result <名前>` の名前が効かない。** 型検査は名前を書いた出力の型で見るのに、評価は一つ目に入れる。`number` が `money` の枠に入って E103 が出ない。
4. **`result` を二本書くと二本目が一本目を黙って上書きする。** `RuleFile::result` が `Option` 一つで、診断が無い。
**決定**:生成側は、出力ごとに同じ三手(出どころ → 線上のスケールへ戻す → 宣言された丸めを一度だけ)を踏む。一つ出力と複数出力で別々に計算していたのをやめ、四つの言語それぞれで一本のループにした。出どころは、一つ目の出力だけ `result` の式、あとは同じ名前の束縛である。`result` は**一つ目の出力のための糖衣**のままとし、それ以外の出力を指したら E015、二本目を書いたら E016 で止める。
**捨てたもの**:`result <名前>` で、書いた名前の出力に値を入れる案。書き手の期待には近いが、出力ごとの値は「同じ名前の束縛」で既に一般に取れる(`define 乙 = …` が二つ目の出力になる)ので、`result` を一般化すると同じことをする道が二本になる。糖衣のままにして、糖衣の効く場所を診断で固定するほうを採った。
**なぜ通っていたか。** ここでも §15.9 と同じで、行使されていない構造は検査されない。`result` を持つコーパスの規則は六本あって全部 outputs が一本、複数出力の規則は `クーポン一枚` 一本きりだった。しかもその一本は、境界から作ったベクタ 94 件が**すべて `素割引 = 0`** で、100 倍のずれが一件も現れない。突き合わせは 0 と 0 を比べて緑だった。`examples` は参照評価器しか走らせないので、こちらも黙っていた。コーパスに `会員特典.rule`(`result` + 出力二つ + 表の列から来る率)を足して固定する。
**残っている穴**:ベクタが出力の値を的にしていないこと。行・境界・隠れ対は的にしているが、「数値出力が全ベクタで同じ値」という状態を誰も咎めない。`rulec coverage` の第四の基準にするのが素直だが、これは別に決める。
### 15.17 英語圏のコーパスと、単位の表(2026-09-14)
**測ったこと。** 英語のサイトも図も、例が日本の地域と円で書かれている。これは文書の問題ではなく、**言語の問題**だった。
- `5USD` は書けない。`src/lex.rs` の `is_unit_char` が通すのは `円 銭 g k c m % %` の 7 文字だけなので、`5` と `USD` の二語に割れて E014 で止まる。`lb` `oz` `in` `mi` も同じく無い。
- `src/types.rs` の `unit_info` も同じ 7 単位きりである。
- 一方で `Ty::Money { cur: String }` は通貨を自由な文字列として持っているので、**型のほうは既に用意ができている**。足りないのは字句と単位表だけである。
- 英語の生成物にも円が残っている。丸めヘルパの doc コメントが "Toward zero: -4.8 yen → -4 yen" と書く(`src/codegen.rs` に 6 箇所)。
**決めていないこと。** 単位表を (a) 組み込みで増やす(USD/cent、lb/oz、in/mi …)か、(b) 規則の側で宣言できるようにするか。(b) は「単位は型の一部」という §2.1 の骨格に触るので安易には採らない。通貨は主単位と補助単位の比が要る点で 円/銭 と同じ形をしているから、増やすなら既存の対がそのままテンプレートになる。
**順序。** 公開のあと。単位が先で、次に英語圏の規則を一本コーパスへ、最後に英語版の図の描き直し(`overview.rule` は名前だけ英語で、金額は円のままである)。コーパスの既存 12 本は**実在する公開規約の転記**なので、円のまま残す。転記を書き換えたら転記ではなくなる。
### 15.18 単位の表を開く(2026-09-14)
**決定**:§15.17 で測った穴を塞ぐ。三つある。
**1. 通貨はそれぞれが独立した次元である。** いままで `unit_info` は `円` と `銭` を `money` という一つの次元に入れていた。同じ次元に `USD` を足すと、`money[円]` の列に書いた `100USD` が黙って換算される。**為替レートはこの道具の中に無いし、あってはならない。** だから次元を通貨そのものにする。`円` と `銭` は同じ通貨の主単位と補助単位、`JPY` は `円` の別綴り。通貨の違う値を混ぜれば E103 で止まる。
**2. 主単位と、その 1/100 の補助単位。** ISO 4217 のコードに `c` を付けたものを補助単位とする(`USD` と `USDc`、`EUR` と `EURc`)。`money[USD]` は整数ドル、`money[USDc]` は整数セントで、リテラルは両者のあいだで換算される(`5USD` は `money[USDc]` の列で 500、`599USDc` は `money[USD]` の列では 5.99 なので拒まれる)。`mass[kg]` と `mass[g]` の関係そのままである。この整理で既存の欠陥も一つ消えた — 旧実装は宣言側の係数を割っていなかったので、`money[銭]` の列に書いた `5円` が 5 になっていた(正しくは 500)。
**3. 質量と長さにヤードポンド法を足す。** `lb`(453.59237 g)、`oz`、`in`(2.54 cm)、`ft`、`yd`、`mi`、それと `mg` `t` `mm` `km`。換算係数はすべて厳密な有理数なので `Rat`(i128/i128)のままで誤差は出ない。`1lb` は `mass[g]` の列では 453.59237 で整数でないから拒まれる。これは正しい — ポンドの列にグラムは書けない。
**捨てたもの**:通貨を自由な識別子にして、宣言と綴りが一致するかだけ見る案。表が要らなくなるかわりに、`100lbs` が「lbs という通貨」として通る。閉じた集合であることがこの道具の骨格なので、通貨も閉じた表にする。
**字句解析**:`is_unit_char` は 7 文字の白名簿だった(`円 銭 g k c m % %`)。ASCII の英字と `円 銭 % %` に広げる。`5USD` が `5` と `USD` の二語に割れていたのはこれが原因である。
### 15.19 予定していない言語へ生成する(2026-09-14)
**きっかけ**:「私たちが予定していない未知の言語や、ワークフローシステムの式言語に組み込みたい場合がありうる」という問い。調べてみたら、**やり方はもうあって、書いていなかっただけ**だった。`rulec api` が呼び出し方を出し、`rulec schema` がやりとりする JSON の形を出し、`rulec adapter` がテンプレートを出し、`rulec verify --adapter <cmd>` が**どんなプロセスでも**立ち上げて生成ベクタ全件で突き合わせる。`verify` の一行説明が「旧実装を立ち上げる」としか言っていないので、新しい対象を確かめる手段だとは誰も読み取れない。それだけのことだった。
**決定**:やり方が二つあることを `docs/backends.md` に書く。**rulec の外で生成するほう**(自分のジェネレータで書き出し、adapter で包み、`verify` で押さえる/rulec は無改造)を先に、**rulec に組み込むほう**(`src/codegen.rs` にバックエンドを足す/§15.13 の手順とコスト)を後に置く。順番には意味がある。外で生成するほうは今日から使えて、対象を選ばず、証拠の強さは組み込みと変わらないからである。
**決定**:**CI で走らせられない対象は、禁じるのではなく、生成物に「確かめていない」と書かせる。** ライセンスの無いエンジン、スクリプトから叩けない表計算、他人のサービスの上で動く DSL — adapter が書けない以上、突き合わせる相手がいない。
理由は、禁じても要る人は要るからである。禁じられたら手で書き直すことになる。**手で書き直すことこそ、この道具が防ぐために存在している失敗**で、しかもそちらには何の印も残らない。「確かめていない」と自分で名乗る生成物のほうが、黙って正しい顔をしている手書きよりそのぶん安全である。ただし看板の外に置く — 証明済みとは呼ばないし、`verify` を通った対象と並べて一覧しない。
**§15.13 の原則を広げたのであって、置き換えたのではない。**「一致検査に乗らないなら入れない」は**組み込みバックエンド**を縛る規則として、そのまま残る。外で生成したものはこのリポジトリの中には無く、利用者のものである。その二つを分けているのが、この注記である。
**捨てたもの**:rulec 側に「確かめていない」と書かせる案。外での生成は他人のジェネレータの中で起きるので、rulec には書きようがない。ジェネレータを書く人への決まりごととして文書に書くのが、実際に効く唯一の形である。
**捨てたもの**:文書だけ書いて、例は載せない案。このページの主張は「証拠を手放さずに、予定していない相手へ生成できる」であり、走らせたことのない手順書ではその証拠にならない。だから SQL(SQLite。§15.13 の第一候補で、ランナーを持てない「形の違う」対象)で実際に一周して、ec261 の 88 件全件一致と、閾値を一つ壊したときの不一致報告の両方をページに載せた。`tests/backends.rs` が文書から SQL と adapter を取り出して走らせ、両方の出力を固定する。
### 15.20 五つめの生成対象に Ruby(2026-09-15)
**決定**:Ruby を五つめの生成対象にする。§15.13 の候補表(Java・Kotlin・Swift・SQL)より先に置く。
理由は三つ。**当たりが近い** — この道具が狙うのは運賃表・手数料・割引・可否判定で、その種のコードが実際に住んでいるのは Rails のアプリである。Swift や Kotlin(モバイル)より業務判断からの距離が短い。**一致検査に何も足さずに乗る** — `json` が標準添付なので、ランナーに gem が一つも要らない。`node` だけで走る TypeScript、`rustc` だけで通る Rust と同じ性質で、§15.13 の関門をそのまま通る。**形が既知** — Ruby の `/` は Integer に対して負の無限大方向へ丸め、Python の `//` と同じである。§15.11 で一度刺さった差なので、生成器に処理もテストもすでにある。
**決定(単位)**:**素の `Integer` にして、単位は署名とコメントで言う。** Ruby には実行時費用ゼロの別名型が無い。この道具の保証は**証明**であって host 言語の型検査ではなく、Python がすでにその前例である(`NewType` は mypy が見るだけで、実行時には何も起きない)。`.rbs` を隣に出して `steep` に見せる案は**足せる形にしておくが、いまは出さない** — 生成物が二つになり、型検査器を持たない読み手には使われないまま腐る側に回る。
**決定(整数の幅)**:Ruby の `Integer` は多倍長なので、E108 が証明する int64 の範囲を**破ることはないし、強制もしない**。JavaScript とは事情が逆で、2^53 を超えて黙って間違うことが無い。だから `bigint` に相当する措置は要らない。証明が守っているのは値であって、器ではない。
**決定(3.x と 4.x)**:生成物に 4.x 固有の機能を使わない。モジュール、メソッド、凍結した定数、`Integer`、`Struct` だけで書けるので、3.x と 4.x の両方で動く。`Data.define`(3.2 以降)は使わず `Struct.new` にするのはこのため。
**実装上の制約が一つ**:Ruby の定数は**大文字の ASCII で始まらなければならない**。モジュール直下に `遠隔地 = [...]` と書くと定数ではなくローカル変数になり、メソッドから見えない(実測で `NameError`)。だからグループは `GROUP_遠隔地` の形にする。列挙の値名は別名から大文字 ASCII になるので問題ないが、別名が無い場合に備えて同じ前置を通す。
**捨てたもの**:キーワード引数。Ruby では三つ以上の同型引数にキーワードを使うのが作法だが、ほかの四言語は位置引数で、`rulec api` の一覧も署名を言語ごとに並べて比較できることに意味がある。位置引数に揃える。
**再評価(§15.13 が「五つめあたりで」と書いたもの)**:**§15.13 の見立ては誤りだった。** 実測は、生成コード本体が 540 行、それとは別に**手で編集したファイルが 22 個**である。
「差分は四箇所に集中していて `Lang` 列挙一つで足りる」は、`guards()` の内側だけの話だった。その外に**手書きの一覧が七つ**ある — `main.rs` の生成物、`runtest.rs`(五箇所)、`api()`、`tests/threeway.rs`、`tests/shapes.rs`、`tests/api.rs`、`tests/formats.rs`。さらに図の生成器が二つ、文書が九ファイル約二十箇所。どれも「言語を一つ足したら手で直す」ものである。
言語ごとの綴りを `Backend` トレイトに集め、登録簿を一つにして生成・実行・`api`・テストをそこから回す整理を、**次にやる**。これは §15.9 と同じ形の教訓でもある — 手書きの一覧が七つあることは、七つとも手で直すまで見えなかった。
**唯一、止めてくれたのは図の `--verify` だった。** 流れ図が「rulec gen が書く言語」を実際のツール出力と突き合わせていて、Ruby を足した瞬間に落ちた。同じ検査が文書の側には無いので、そちらは黙って古いままになる。整理ではそこも閉じる。
### 15.21 対象言語の登録簿(2026-09-15)
**決定**:どの言語を生成するかを `src/backend.rs` の一箇所に持たせ、生成・実行・`api`・テストはそこを回す。§15.20 が数えた「手書きの一覧が七つ」を一つにする。
登録簿の一行が持つのは、安定した id、人が読む名前、PATH に要る道具、書き出すファイル、走らせ方(`Plan`=作業場所・任意のビルド・コマンド)である。**データで言えるものは登録簿に、言えないもの(生成器そのもの)は `codegen.rs` に**、という線で切った。
`guards()` にあった三つの `match lang` は消えた。言語ごとの綴り(コメント記号、`and` か `&&` か、`if` の頭、ブロックの閉じ方)は `Lang::spelling()` の一行になり、セル描画は `Gen::cell()` 一箇所になった。
**決定(文書も検査に乗せる)**:対象言語を三つ以上並べている行は、登録簿の全部を並べていなければ落とす。理由は §15.20 の実測で、Ruby を足したとき**私を止めたのは図の `--verify` だけ**だったからである。図には検査があり、文章には無かった。
**その検査が書いた直後に三箇所を見つけた。** `docs/generated-code.md` の冒頭(Ruby が抜けていた)、同 179 行(部分列挙)、そして `src/codes.rs` の E107 の説明 — そこには「**Three-way agreement (evaluator, Python, Go)**」「all three」が残っていた。TypeScript より前の文面であり、退役したはずの「三者一致」でもある。**Ruby とは無関係に、ずっと前から腐っていた。** §15.9 の「行使されていない構造は検査されない」が、文章についても成り立つことの実例である。
**捨てたもの**:`Backend` をトレイトにして各言語を型で実装させる案。生成器の本体は `Gen` の借用と密に絡んでいて、トレイト化すると寿命の記述が増えるばかりで、減る重複が無い。関数ポインタを持つ構造体の表で足りている。
### 15.22 生成 Python は mypy を通っていなかった(2026-09-15)
**きっかけ**:「Python の生成コードに型注釈は適切に付いているか」という問い。注釈は付いていた。**一度も型検査器に通していなかった**のが問題だった。`mypy --strict` を当てると、コーパス 13 本のうち **8 本が落ちた**。
**原因**:`NewType` に算術をすると素の型に戻る。`YenInclTax * int` は `int` であり、丸めヘルパから出てきた値も `int` である。それをブランド付きの出力として返していたので、**ブランドが捕まえるために存在している取り違えを、生成物自身がやっていた**。
**決定**:出力の値をブランドで包み直す(`return YenInclTax(_round_up(fee, 10))`)。`NewType.__call__` は引数をそのまま返すので、実行時にはほぼ何も起きない。中間の `raw` は包まない — あれは出力の単位ではない別の刻みの値であり、包めば嘘になる。
**決定**:ランナーも同じ規律に従わせる。`m.Kilometer(int(d["距離"]))` のように引数を構築する。これは**呼び出し側が実際にやらねばならないこと**なので、ランナーが呼び出し方の手本として正しくなる。`_ord` と `_wire` にも注釈を付けた。
**決定**:`mypy --strict` をテストに乗せる(`生成pythonはmypy_strictを通る`)。mypy が無ければ飛ばして報告する、既存の `ruff` と同じ流儀。
**これも §15.9 である。** README と `docs/generated-code.md` は「ブランドは mypy と pyright で効く」と書いていた。**書いてあるだけで、それを行使する検査が無かった。** 行使されない主張は、行使されない構造と同じように腐る。
**残っていること**:`pyright` では試していない。`NewType` の扱いは両者でほぼ同じだが、確かめてはいない。
### 15.23 Ruby に署名を添える(2026-09-15)
**決定**:`rulec gen` が `ruby/sig/<規則>.rbs` も書く。§15.20 で「足せる形にしておくが、いまは出さない」と書いたものを出す。
**まず測った。** RBS で単位を分けられるかを `steep` で実験した。`type yen = Integer` と `type gram = Integer` を宣言して、gram を渡す呼び出しを書いた — **通る。** RBS にも newtype は無く、型別名は同じ型である。捕まったのは `String` を渡したときだけだった。**だから .rbs は §15.20 で諦めた単位安全性を取り戻さない。** そこは正直に書く。
**それでも出す価値がある。** 実験で効くと確かめたものが三つある。引数の数、引数の種別(`Integer` と `String` と `bool`)、そして**列挙**である。列挙は値そのものの合併(`"率引き" | "額引き" | "送料無料"`)として書けるので、**型のレベルで閉じる**。生成した Ruby 側では列挙の値は実行時まで単なる文字列で、入口ガードが弾くまで分からない。ここは .rbs のほうが強い。
**決定(検査の当て方)**:`.rbs` を**モジュール自身に当てる**。呼び出し側だけを見るのでは、署名が取りこぼしたメソッドが見つからない。実際、私有の丸めヘルパを宣言するまで `steep` は警告を出し、`Output.new` を宣言するまでエラーを出した。どちらも「呼び出し側だけ」の検査では出ない。
**決定(噛むことも検査する)**:`rbsは誤った呼び出しを拒む` が、誤った三つの呼び出し(種別違い・列挙外・引数不足)が拒まれ、正しい一つが拒まれないことを確かめる。**一度も失敗したことのない検査は、効いていることが示されていない。**
**捨てたもの**:実行時のラッパクラスで単位を分ける案。それなら単位安全性は戻るが、§15.20 の「実行時費用ゼロ」を捨てることになり、生成物が普通の Ruby でなくなる。単位は証明が守っていて、host 言語の型検査はそれをなぞっているに過ぎない。
### 15.24 六つめの生成対象に Swift(2026-09-15)
**決定**:Swift を六つめの生成対象にする。§15.13 の候補表に残っていた三つ(Java・Kotlin・SQL)より先に置く。
理由は三つ。**当たりが近い** — 狙いは「同じ業務ルールを、バックエンドとクライアントで共有する」ことで、iOS 側が Swift なら表が一枚で済む。§15.20 が Ruby を Swift より先に置いた根拠は「業務判断からの距離」だったが、距離が短いのは *サーバ側の* Swift ではなく、**サーバとクライアントが同じ答えを返す必要**のほうである。**単位が型に載る** — 値ひとつの struct は Swift ではその整数と同じ配置になるので、Rust の newtype と同じ取引(実行時費用ゼロで取り違えを弾く)がそのまま成り立つ。§15.20 で Ruby に諦めさせたものが、ここでは戻る。**一致検査に何も足さずに乗る** — `swiftc` だけで通り、SwiftPM も `Package.swift` も要らない。整数除算は 0 方向で Go・Rust と同じなので、丸めヘルパはそちらと同じ書き方で済む。
**§15.21 の登録簿は効いたか。** 効いた。実測は、生成器に約 760 行、`src/backend.rs` に 1 行、**`src/` で他に手で直したファイルは無い**。§15.20 の Ruby は本体 540 行に加えて手編集 22 ファイルだったので、後半がまるごと消えたことになる。唯一の例外は `--help` の二文で、そこは対象言語を書き出していたのを登録簿から引くように直した — つまり**手書きの一覧が一つ残っていた**のを、Swift が見つけた。
**綴りは Swift のものにした。** 識別子は lowerCamelCase(`shipping_fee` → `shippingFee`)で、Go に `pascal` を書かせたのと同じ理由である — 生成物は他人の Swift のコードベースの中に住み、そこでは snake_case をリンタが叩く。§1.3 の「作者の書いた別名をそのまま使う」から外れる二つめの言語になるが、外れた綴りは `rulec api` が申告し、一覧から組んだ呼び出しを `swiftc` に通すテストがそれを押さえる。予約語に当たる別名はバッククォートで囲む。**呼び出し側の引数ラベルだけは規則が逆で**、キーワードはそのまま置く(囲むと「囲む必要は無い」と警告される)。例外は `inout` ひとつだけ。
**`_min` / `_max` は自前のままにした。** 最初、Swift の標準ライブラリの `min` / `max` に寄せた。`クーポン割引.rule` が `最低購入額(min)` という入力を宣言していて、パラメータがグローバルを隠すので、`min(素割引, 上限額)` が**呼び出し側の渡した金額**に解決してコンパイルが落ちた。README の「組み込みをそのまま呼ばない」がそのまま刺さった形で、他の五つが最初から自前を持っていたのはこのためである。
**読まれない列には `_ = x` が要った** — Go と同じ device である。Swift は読まれない束縛を警告し、生成物は DO NOT EDIT なので後から黙らせられない。`is_read()` はすでにあったので、呼ぶ場所を足しただけで済んだ。
**ランナーは `main.swift` ではなく `@main` にした。** トップレベルのコードは `main.swift` という名前のファイルにしか書けず、規則の本体をその隣に置きたいので、ランナー側が `@main` を持つ。一方 `_round_test.swift` は単独でコンパイルされる=それ自体が main ファイルになるので、逆にトップレベルのまま書く。
**ついでに見つかったもの**:予約語に当たる別名(`重量(where)` など)で、**Rust の生成物がコンパイルできない**。`r#where` が要る。Swift とは無関係に前からある穴で、直すのは別の変更である。`tests/shapes.rs` の新しいテストに、Rust をそこだけ検査していない理由として書いた。
**捨てたもの**:`Int` を使う案。Swift の `Int` は語長で、いまの対象では 64 ビットだが、E108 の証明が述べられているのは int64 である。証明の言葉と生成物の型を一致させるほうを採った。
### 15.25 式の中のリテラルは自分の単位で読む(2026-09-15)
**きっかけ**:Swift を足す途中で「真偽値の `define` を表の列にするとベクタが 0 件になる」規則に当たった。最初はそう報告したが、**原因も範囲もそれとは違った**。`送料.rule` も `define 大口` を列にしていて、そちらは動いている。
**原因**:`eval.rs` の `Expr::Lit` が、式の中のリテラルを **円・率・無単位の三つでしか試していなかった**。`500g` はどれにも当たらず `None` を返し、比較全体が `None`、`define` に値が付かず、表が一行も発火しない。「列が `define` だから」ではなく「リテラルの単位が円でないから」だった。
**実測**:15 単位系のうち通っていたのは 3 つ(円・`%`・無単位)だけ。mass の 5 つ、length の 4 つ、EUR と USD は**値が出ない**。`銭` はもっと悪く、`500銭` が 円 の枝に当たって 5 と読まれ、**百分の一の値で通っていた** — `499銭 >= 500銭` が真になり、オラクルが誤答する。`derive 超過 = 重量 - 2000g` も同じ経路で壊れていた。
**コーパスが全部 円 で式を書いていたから、ずっと見えなかった。** ec261 は EUR だが、金額が表のセルにしか出てこない(セルは列の型で読まれるので別経路)。§15.9 の「行使されていない構造は検査されない」がまた当たっている。
**決定**:`Expr::Lit` は `lit_ty_pub`、つまり**リテラル自身の型**で読む。`codegen.rs` は最初からそうしていて、ずれていたのは評価器だけだった。これが唯一の正しい読みだと言えるのは **E103 が式の中の単位混在を禁じている**からで、`重量(mass[g]) - 2kg` も `額(money[銭]) >= 5円` も検査で止まる。したがってリテラルが出会う相手の単位は必ず自分と同じである。
**壊れていたのはオラクルのほうで、生成コードは六言語とも正しかった。** `rulec test` は言語間の一致を見るので落ちて気づけるが、`replay` と E107 は評価器だけを見る。E107 は「例が成り立ちません」と言うので、**正しい規則を書いた人が、存在しない欠陥を出典に探しに行く**ことになる。そこが一番の害だった。
**検査を足した**(`tests/units.rs`)。単位表を全部歩いて、式の中のリテラルが評価でき、両方の行に当たるベクタが出ることを見る。`銭` は別に一本、境界の下の値が 1 行目に当たらないことを確かめる。
**ついでに見つけたもの**:TypeScript と Swift のランナーが規則を裸の名前で呼び、同じ場所で `d` という局所変数を使っていた。`rule d` が JSON を `d` に束ね、呼び出しがそちらに解決していた。Python・Ruby・Rust・Go は呼び出しを修飾しているので無事。衝突する名前だけを改名する(`runner_local`)ので、他の規則の生成物は 1 バイトも動かない。
### 15.26 ベクタ生成器が「一度に一つの入力」で詰まる(2026-09-15)
**きっかけ**:`policy first` の表で、到達可能な行にベクタが一つも当たらず、`coverage` が exit 1 のまま CI が緑にならない事例に当たった。`examples` でその行を明示的に固定しても変わらない(`examples` はベクタ集合に入らない)ので、書き手に打つ手がない。
**原因は二つあった。** どちらも「一度に動かす入力は一つ」という縛りの副作用である。
1. **`win_row` が、対象行が `-` にしている列しか動かさなかった。** 「対象行が条件を書いている列を動かすと対象行自身が壊れる」という理由づけだが、それが真なのは**セルが一点のときだけ**。`a >= 1` の行が `a = 1` の先行行を外すには `a = 2` があり、両立する。**全列に条件を書いた行は、どれだけ到達可能でも永久に勝てなかった。**
2. **`reach_row` が、後の列を直すときに前の列を壊した。** 導出 `d = b - a` の `d >= 0` を直すのに `place` は宣言順で最初に効く入力=`a` を動かし、二列左の `a >= 2` を取り消す。二パス回す作りなので次のパスが戻し、その次がまた下げる — **振動して収束しない。**
**決定**:`win_row` は「動かしてよい列か」ではなく「動かしたあと対象行が保てるか」で判断し、受理条件を `violate_cell` に渡す(先に見つかる外し方は対象行も外すことが多く、使えるものはその先にある)。動かしたあとは行を組み直し、組み直しが先行行を戻したら捨てる。`place`・`satisfy_cell`・`reach_row` には**凍結集合**を通し、その行が既に固定した入力には手を出させない。凍結で行き止まりなら凍結なしで再試行するので、探索が狭まることはない。
**実測**:コーパス 13 本の**ベクタは一件も変わらない**(件数も中身も)。直るのは、これまで落ちていた形だけである。
**残っている(別件)**:`region` は導出列を、宣言された `range` だけに縛られた**自由な軸**として扱う。そのため `a = 1` と `d < 0` を同時に満たす点が在ると判断し、`policy unique` なら **E105 を出して正しい規則を弾く**。示される入力例も再現できない(導出値が入力の顔で並び、それを決める入力が出てこない)。ここは近似の設計そのものなので、切り分けて扱う。
### 15.27 導出軸の重なりを、証拠のあるものだけ報告する(2026-09-15)
**きっかけ**:§15.26 の調査中に、`policy unique` の表で **E105 が正しい規則を弾く**のを見つけた。示された入力例は `a = 1` と `d < 0` を同時に主張するが、`d = b - a` で `b >= 1` なら `a = 1` のとき `d >= 0` である。**そんな入力は存在しない。**
**原因**:§6 は一列を独立した軸にし、一行を区画にする。これが「完全性と重なりが厳密に決定可能」の土台である。ところが導出列にも独立した軸が与えられ、宣言された `range` にしか縛られない。区画の積には、どの入力からも到達できない点が混じる。ふるい(`feasible`)はそれを落とすためにあるが、**見ているのは軸ごと**で、「同じ点で別の軸が固定されているとき、この導出は何を取りうるか」は問わない。今回の矛盾は軸と軸の関係にあるので、構造的に落ちない。
**機構はすでにあった。** 定義軸については「区画が自由に置いた座標を鵜呑みにせず、実際の入力を構成し、**構成できたものだけ**を本物の矛盾として扱う」(`pair_witness`)。導出軸がその扉を通っていなかっただけである。
**決定**:同じ扱いを導出軸にも広げる。構成できなければ `Unknown` に落とす — 証明できていないものを証明済みとして出さない(§6.2)。`policy first` では、構成できなかった対を**影として数えない**(入力例を付けず、§9.2 の隠れ対の義務も課さない)。`first` では先行行が勝つだけで守るものが無いので、降格の代償が無い。
**副産物**:定義軸しか通っていなかった実証が導出軸にも効くので、W105 の「この例を作る入力」が出る場面が増えた。**区画由来の作り話ではなく、評価器が実際に構成した入力**が並ぶ。golden はその分だけ増えた。
**残っている**:構成できなかった対は W114 になり、生成コードに実行時ガードが入る。それが**起こり得ない**場合、警告も守りも空振りである。消すには導出を入力に結び直す必要があり、それは §15-9 の将来項目(真偽定義の代入)と同じ話で、発動条件も同じ — **その過剰報告が実表で邪魔になったとき**である。今回それが実表で起きたので、材料は揃った。
### 15.28 出力が、自分のしたことを正しく言っていなかった(2026-09-15)
**きっかけ**:出力の文面を、実態を知らないつもりで読み直した。§15.25〜15.27 はそこで出た大きい二つで、ここは残りをまとめる。**どれも致命的ではない。**共通しているのは「**道具が、自分が何をしたかを正しく言っていない**」ことである。
- **`number` に `%` が付いていた。** 領域の軸が単位を持ち、空の単位が「日付」の目印だったので、単位を持たない唯一の数値型がその隣の率の枝に落ちていた。「JSON は正しいので表示だけ」で済む話に見えるが、**そのまま貼れるはずの `fix.text` も `| 1% | 0% | true |` になっていて**、貼ると E103 になる。手順どおり動くエージェントが無効な行を貼る。
- **走らなかった実行を「食い違い」と呼んでいた。** toolchain が取れない、コンパイルできない、落ちた — 全部が `参照評価器と食い違います` の見出しで出ていた。比較が行われて結果が悪かった、と読める。行われていない。見出しを分け、JSON に `ran` を足した。
- **飛ばした言語があっても緑だった。** 中心的な主張は「参照評価器と**生成した全言語**がバイト単位で一致する」なのに、toolchain が無い言語を飛ばして `All 2 matched.` / exit 0 と出る。主張の範囲が環境で黙って縮む。要約に範囲を書き、`--require-all` を足して CI の手順書をそちらに変えた。既定は寛容なままにした — 六つの toolchain を全部持っている機械ばかりではない。
- **生成 `go.mod` が `go 1.25` を要求していた。** 1.21 以降その行は toolchain の切替指示でもあるので、古い `go` は新しい版を取りに行き、通信の無い環境で失敗する。生成コードが使うのは `fmt` などだけで、要求する根拠が無かった。**上の「食い違い」の見出しは、たいていこれが引き金だった。**
- **`examples` がベクタ集合に入っていなかった。** 実行される仕様と謳いながら、E107 が参照評価器に当てて終わりで、**人が書いた唯一の事例がどの言語にも届いていなかった**。カバー義務にも数えないので、生成器の穴を例で塞ぐこともできない。入力の全部に値を書いた行をベクタにした。ec261 は 88 件から 94 件になった。
**教訓**:五つのうち四つは「出力の言葉が実態とずれている」類で、実装の誤りではない。**文面は、実態を知らない読み手にしか試されない** — 書いた本人は実態のほうを知っているので、足りない言葉を補って読んでしまう。§15.9 の「行使されていない構造は検査されない」の、文面版だと思う。
### 15.29 導出を入力に結び直す(2026-09-15)
**きっかけ**:§15.27 は「構成できた重なりだけ報告する」で誤りを止めたが、**起こり得ない対が W114 として残り**、生成コード六言語に絶対に発火しない実行時ガードが入り、`rulec doc` は承認者に存在しない場合を訊いていた。§15-9 の将来項目(9 番)は、この着手条件を「過剰報告が実表で邪魔になったとき」と書いていた。**実表で起きたので着手した。**
**決定**:軸ごとのふるいに加えて、**交差の区画の上で導出の式を区間演算する**。`derive` は §5 で「入力の線形結合」に限られているので、`+` `-` 定数倍の区間演算で足りる。`a = 1` が区画に固定されていて `d = b - a`、`b ∈ [1,10]` なら `d ∈ [0,9]`。区画が `d < 0` を主張していれば、その交差は空である。
**一点ではなく区画で見る。** 既存のふるいは交差の**一点**(両方のマスクが立つ最初の座標)を見ていた。一点が不可能でも交差が空とは限らないので、そこを厳しくするのは危うい。各軸について両方が選ぶ座標の**包**を取り、その上で式を評価する。両側とも包=外側に広いので、「交わらない」と言えたときは本当に空である。
**開区間を刻みで閉じる必要があった。** `< 0` の上端を 0 と読むと `0 < 0` が偽になって判定が落ちる。座標は軸の刻みの上にあるので、`Open` は `[a+刻み, b-刻み]` に閉じる。ここが効かないと、直しが一切発火しない。
**完全性の穴の探索には手を入れていない。** あちら(§6.1 の `hole_within`)が `feasible` に渡すのはパディングされた一点で、その一点が不可能でも別の埋め方が本物の穴かもしれない。厳しくすると**本物の E101 を落とす**——この道具が唯一やってはいけない向きである。重なり側は交差の区画全体を見るので、この危険が無い。
**逆向きを落とさないことを検査に乗せた**(`tests/region.rs`)。一文字だけ違う二つの規則を並べる。`b >= 1` なら `a = 1` のとき `d >= 0` で対は不可能、`b >= 0` なら `a = 1, b = 0` で `d = -1` となり対は**実在する**。前者は一切報告せず、後者は必ず報告する。締めすぎれば後者が落ち、緩ければ前者が残る。
**実測**:コーパス 13 本と mutants の診断は**一文字も変わらない**。消えるのは、これまで過剰に出ていた分だけである。
**残っている**:上流の表との相関は、まだ軸ごとである。`b → x` の表の下流で `a ∧ b` を条件に書く行は実際には死んでいるが、E102 はそれを拾わない(`coverage` は「その行を勝たせるベクタが作れない」と正しく言う)。同じ近似の、同じ場所の話である。
### 15.30 上流の表との相関を見る(2026-09-15)
**きっかけ**:§15.29 の残りとして挙げた最後の一つ。下流の表が「上流の出す値」と「それを決める入力」を同時に条件に書く行は、**片方ずつなら起こるのに同時には起こらない**ことがある。`reachable` は列ごとに「上流が出しうる値の集合」しか持たないので、その相関が見えない。実表で二行出た。
**決定**:E102 の判定に、上流の表を一段だけ読む検査を足す。下流の行が列 `k` に値 `v` を求めるとき、上流で `v` を出す行のうち、**この行の他のセルと両立し、かつ自分より前の行一つに丸ごと覆われていない**ものがあるかを見る。一つも無ければ、その行はどの入力でも勝てない。
**緩い側に倒してある。** 先行行の**和**は引かない(一行ずつしか見ない)し、上流のさらに上流までは追わない。だから「出せる」と言い過ぎることはあっても、「出せない」と言い過ぎることはない。**E102 はエラーなので、締めすぎれば正しい規則を弾く** — これまでの直し(報告を減らすだけ)と逆向きで、そこが唯一の危うさだった。検査は一値違いの二規則で押さえた。`黒` は `種 = 甲` からしか出ないので `黒 × 乙` は死に、`白 × 乙` は生きる。
**入れた直後に、道具が自分と矛盾した。** E102 が「この行は死んでいる、消せ」と言い、消すと E101 が「その組み合わせが覆われていない、足せ」と言う。完全性の探索(`find_hole`)が上流相関を知らないからで、**作者に正反対のことを二つ言う**状態になった。点に対しても同じ判定ができるようにして、そちらにも通した。
**その瞬間、既存の変異が一つ軽くなった。** `m_e102b` は「上流が出さない値を下流が書いた」変異で、E102 三件のほかに **E101 を一件出していた** — 「決して出ない `可否 = true` に行を足せ」という要求である。同じ実行の中で E102 がその行を死んでいると言っているのだから、前から矛盾していた。`tests/m0.rs` の固定値を一件減らし、理由をその場に書いた。
**実測**:コーパス 13 本の診断は変わらない。mutants で変わったのはいま書いた一件だけである。
**残っている**:二段以上の連鎖(上流の上流)と、先行行の和による被覆は見ていない。どちらも「見落とす」側なので、正しい規則を弾く危険は増えない。
### 15.31 リテラルの尺度は一か所で決める(2026-09-15)
**きっかけ**:処理の流れを図解する作業で `codegen.rs` の §7.1 を読み直していて見つけた。§15.25 の**裏返し**である — あちらは評価器が間違っていて生成コードが正しかった。こちらは評価器が正しくて生成コードが間違っていた。
**原因**:リテラルの尺度を**二か所が別々に計算していた**。`types.rs` は「小数桁の 10 の冪 × 単位の分母」で決め、それを名前の尺度として記録する(`3.6%` なら 1000)。`codegen.rs` は**約分後の分母**を使っていた(`3.6%` は 9/250 なので 250)。`define` は前者の尺度で読み直されるので、250 で書いて 1000 で読む。黙って 4 倍ずれる。
**実測**:`3.6%` は 1/4、`8%` も 1/4、`50%` は 1/50 になる。`7%` と `1%` は正しい — 100 と約分できないからである。**100 と公約数を持つ率はほぼ全部壊れていて、抜き取りで確かめると助かっている側を引きうる。**
**コーパスが一度も踏まなかった。** コーパスの率はすべて**列**なので、生成器には名前として届く。名前は `types` が記録した尺度をそのまま持つので、ずれようがない。リテラルが式の中に出るのは `define` / `derive` の本体だけで、コーパスにその形が一本も無い。セルの中の `%` は列の型で読まれる別経路である。§15.9 の「行使されていない構造は検査されない」がまた当たっている。
**普段の網が二つとも効かない形だった。** `check` は通る — 評価器は別経路で、しかも正しいから。六言語も互いに一致する — 生成器のテキストを共有しているから。食い違うのは**生成コードと評価器のあいだだけ**で、それを見るのは `rulec test` しかない。
**決定**:`types::lit_scale` を一つ置いて、両側がそれを呼ぶ。約分後の分母は使わない。`3.6%` は `36`@1000、`8%` は `8`@100 として保管される。二度と離れないように、定義を一つにしたのが要点である。
**実測**:コーパス 13 本の生成物は **1 バイトも動かない**。踏んでいなかったのだから当然だが、確かめた。
**検査を足した**(`tests/wire.rs`)。率のリテラルを `define` に置いた規則を生成して、保管される整数と戻しの除数が名前の尺度と揃っていることを見る。このファイルは「率だけが保管値と転送値で違う型だから、変換を忘れた場所を捕まえる」ためにあり、これもその一件である。
**ついでに直したもの**:`÷` の右が変数のときのコメントが E103 と書いていたが、実際に出るのは E115 だった。
### 15.32 表と行に出典を書く(2026-09-16)
**決定**:表の出典は `table` 行の行末コメントに書き、その表の出典と違うところから来た行は、行の行末コメントに書く。`rulec doc` は前者を表の見出しの直下に、後者を「注記」の列として出す(例の行も同じ)。`description` は一行の要約に戻す。文法は変えず、check は関与しない。
理由。§16 第三の心配(仮置きが権威になる)は丸めについて書いたが、金額も同じ形をしている。出典の無い 42 行を承認する人は「表全体をもう一度読む」ことになり、出典のある行なら「この行とそのセルを見比べる」ことになる。出典が改定されたとき読み直す行も、その出典を引いている行に絞れる。書くのは転記している当人で、そのとき出典のページは開いている。
実装で分かったこと。宣言行の行末コメントは doc の注記に出ていたが、表の行のコメントは原本から行を取るときに落としていて、**書けるのに承認者に届いていなかった**。表に一つ書けるようにしたのは、42 行の表に同じ出典を 42 回書かせないためである。コメントの無い表は一字も変わらない(注記の列は、どれかの行にコメントがあるときだけ出す)。
**捨てたもの**:`source` という構文を足す案。コメントで足りるものに構文を足すと、fmt・parser・reference・fixtures の全部が一語ぶん増える。出典ファイルのダイジェストを持たせて、変化を check が指摘する案。ダイジェストの変化から言えるのは「この出典を引く行は読み直し」までで、どのセルかは分からない。それは表の性質ではなくレビューの引き金なので、check の診断にはしない。出典がスプレッドシートでセル参照まで書けるなら読み取り専用で突き合わせられるが、それは `import`(§1.4)と一緒に決める。
### 15.33 生成コードが、当てはまった行を返す(2026-09-16)
**決定**:生成する関数を二つにする。呼び出し側が使う関数は署名を変えず、隣に `_traced`(Go と Swift は `Traced`)を置く。同じ入力を取り、出力と一緒に当てはまった行を返す。行は表ごとに一つ、順に、表の名前と 1 から数えた行番号で、`doc` の `#` 列、verify と replay のクラスタの鍵と同じ番号である。分岐は `_traced` に一度だけ書き、素の関数はそれを呼んで行を捨てる。`rulec api` は `traced` と `traced_signature` で名前と署名を出す。
理由は三つ。第一に問い合わせ対応。「なぜこの送料か」に、ログの (表, 行) から承認済みの資料の行へ直接たどって答えられる。第二に一致検査が強くなる。expected.jsonl は `out` だけを持っていたので、「値は合っているが違う行で合った」は通っていた。vectors はもう行を持っているから、runner が行も出せば突き合わせは行の単位になる。§15.16 の「全ベクタが 0 だったので百倍のずれが見えなかった」と同じ種類の盲点が一つ減る。第三に本番の記録に行が乗る。本番が当てた行と規則が当てる行を組にすれば、金額が同じで行が違う食い違いが見える(記録の側は次の増分)。
**捨てたもの**:`--with-trace` という生成のフラグ。§15 の 3 番で「生成物の形は一つに保つ」と決めていて、フラグは形を二つにする。出力の構造体に行を足す案は、出力が一つのとき値そのものを返しているので置き場所が無い。省略可能な引数で行を受け取る案は、Go と Swift に自然な形が無く、Rust では署名が変わる。
**代償と、出たもの**:行ごとに一行増える(§8.1 の基準 4、1 行あたり 2〜4 行に収まる)。生成物の抜粋を載せている文書は全部動いた。Swift の runner は、文字列を `+` でつなぐ一行が長くなりすぎて型検査が「妥当な時間で型検査できない」と諦めたので、補間に変えた。生成物にも起きる実在の失敗として記しておく。生成物の中の一時変数の名前(`trace`、`raw`)は、別名の無い宣言名とも衝突しないように `temp` を直した。それまでは、別名を持つ名前としか比べていなかった。
### 15.34 表の列の範囲は、セルから決める(2026-09-16)
**きっかけ**:生成コードの記録関数(§15.35)を入れるとき、gen が書く期待値のファイルを fixtures として `fixtures lint` に通す検査を足した。`会員特典` で、出力 `付与点` の値 11999 が「宣言範囲 0..10000 の外」と報告された。規則は正しく、範囲のほうが間違っていた。
**原因**:式の区間を求める `interval` が、範囲の無い率の名前を **0%〜100% と仮定**していた。`付与点 = 基本点 × 倍率` の `倍率` は表の出力列で、セルには 100%・120%・150% が書いてある。仮定は「保守的」のつもりで書かれていたが、100% を超える率の列には保守的でなく、`rulec api` はこの出力の範囲を 0..10000 と公表し、int64 に収まることの証明(E108)も同じ誤った上限で進んでいた。しかもこの仮定は式の中だけのもので、範囲の無い率の**入力**には入口のガードが出ていなかった。証明が仮定したことを、生成コードは守らせていなかった。
**決定**:二つ。(1) 表の数値の出力列に、セルから範囲を付ける。リテラルの最小と最大、セルが名前ならその名前の範囲で広げる。範囲の無い名前を指すセルが一つでもあれば、列に範囲を付けない。(2) 範囲の無い率の入力は 0%〜100% とし、それを**宣言の場所で記録する**。区間の計算、入口ガード、`rulec api`、ベクタの絞り込みが同じ一つの範囲を読む。100% を超えうる率の入力は、ほかの数と同じく範囲を書く。式の中の仮定は消し、範囲の無い名前は区間を持たない。**分からないものは分からないままにする**ほうが、誤った数を出すより良い。
**検査**:`rulec api` が `付与点` の範囲を 0..15000 と言うこと。刻み 100% の率の列に 200% を置いた規則で、9×10¹⁸ 円の入力を掛けると E108 が出ること(旧仮定では 9×10¹⁸ が int64 に収まり、黙っていた)。
**開いたままのこと**:範囲の無い数値の入力は、文書では「必須」だが、検査器は専用の診断を出さない。完全性は E101 で、溢れは E108 の沈黙で、それぞれ穴になる。診断を足すかは別に決める。
### 15.35 生成コードが、記録を自分で書く(2026-09-16)
**決定**:生成する関数を三つめまで増やす。`_record`(Go と Swift は `Record`)は、入力と、返した出力と、当てはまった行と、tag を受け取り、fixtures の形(docs/formats.md)の一行を返す。`in` はワイヤ形式(列挙は名前、日付は `YYYY-MM-DD`、数は宣言した単位の整数)、`observed` は出力、`trace` は行。tag が空なら書かない。JSON は各言語で自前に組み、日付の変換も自前に持つので、どの言語にも import は増えない。
理由。§10.2 は本番ログからの抽出を利用者の仕事と割り切っていたが、その中身は「入力と出力をワイヤ形式に落とす」ことで、生成コードが呼ばれた瞬間に全部そろっている。列挙の値に和名を持たせた理由(§8.2)も、ログとワイヤ形式に使うためだった。生成コードが記録を書けば、移行のあとの改定で diff に食わせる記録が、抽出の仕事なしに毎日たまる。fixtures の `trace` はこの関数が書き、lint は表と行が実在することだけ確かめる。replay は `trace` のある記録を行の単位でも突き合わせる。鍵は記録の行から規則の行への遷移で、diff の `Fired::Moved` と同じ形なので、動いた行は `行2→行5` と読める。値が合っていて行だけ違う記録は、不一致とは別に「行の移動」として件数と例つきで出す。見出しの一致率は値の話のままにし、行の付け直しが一致率を崩さないようにするためで、`--format json` では `moved` に同じ形のクラスタで出る。
**確かめ方**:runner が出力を自分で JSON にするのをやめ、モジュールの `_record` を呼んで印字する。expected.jsonl も同じ fixtures の形にした。三者一致の突き合わせが、値と行だけでなく、**入力のワイヤ形式(日付を含む)まで六言語で確かめる**ことになる。expected.jsonl は有効な fixtures なので、`fixtures lint` が通り、`replay` で全件一致することも検査に入れた。この検査を入れた初回に、出力の範囲の欠陥が一つ見つかった(§15.34)。
**捨てたもの**:`_record` が自分で規則を評価する案。呼び出し側は出力を自分でも使うので二度評価になり、失敗したときに何を記録するかも決めなければならない。呼び出し側が持っているものを渡すほうが素直である。各言語の標準ライブラリの JSON を使う案。Rust に無く、TypeScript の JSON は bigint を扱えず、Swift は Foundation が要る。三つが自前になるなら六つとも自前のほうが、生成物の説明(「何も import しない」)が動かない。
### 15.36 七つめの生成対象に JavaScript(2026-09-16)
**きっかけ**:承認する人が自分の件を試せる一枚の HTML(§15.37)を出すのに、ブラウザで走るコードが要る。TypeScript の生成物は型を消せる構文だけで書いてある(§8.3)ので、型を消せばそのまま JavaScript である。
**決定**:JavaScript を七つめの生成対象にする。emitter は増やさず、TypeScript の生成物から型注釈・型の別名・`interface`・`as` のキャストを機械的に外す変換一つで作る。`.mjs` で出すのは、node が package.json なしにモジュールとして読み、ブラウザがそのまま受け取るためである。runner と丸めの単体ベクタも同じ変換で作り、`rulec test` が七つめとして同じベクタで突き合わせる。`rulec api` は `javascript` の項を持ち、型のフィールドは実行時の型(数と日付は bigint、列挙と文字列は string、真偽は boolean)を言う。
**理由**:二つの emitter が同じコードを書けば必ず離れる。変換なら TypeScript 側を直した日に JavaScript も直り、離れれば一致検査が落ちる。変換が壊れる形(新しい型構文を emitter が使い始める)も、同じ検査で見つかる。
**代償**:単位を載せる型が無い。Ruby と同じ位置で、生成物の説明はそう書く。
**捨てたもの**:HTML の中だけで使う隠れた生成物にする案。一致検査に乗らない言語は入れない、という §15.13 の線をそのまま守るなら、乗せて対象に数えるほうが正直である。
### 15.37 承認する人が自分の件を試せるページ(2026-09-16)
**決定**:`rulec doc --format html` を足す。markdown と同じ資料を一枚の HTML にし、表の行に表の名前と行番号を持たせ、入力欄と、生成した JavaScript(§15.36)を同じページに積む。入力を入れると `_traced` が走って当てはまった行に色が付き、出力が出て、`_record` が書く一行がそのまま見える。例のボタンは、検証済みの `examples` をワイヤ形式で欄に入れて走らせる。
理由。§1.6 の描画は「表を読む」ための面だった。承認する人が一番強く読めるのは、自分の件を入れて答えと当たった行を見ることで、それは表の説明では代替できない。ページの中で動くのは生成コードそのものなので、§1.6 の禁止(検査器の出力に無い文章を書かない)は破れない。ページが言うことは、規則の原本、検査の結果、生成コードのどれかに遡れる。
**古さの扱い**:markdown と同じ。CI が変更ごとに作り、コミットしない。ヘッダの刻印もそのまま入る。
**件はリンクになる**:入れた件はページの住所に `?dest=大阪府&weight=…` の形で乗り、`?example=2` は二つめの例を開いた状態で始まる。「この件を見てほしい」がリンク一つで済む。
**作り**:markdown を HTML にする変換は自前で、`render` が使う部分集合(見出し、段落、箇条書き、パイプの表)だけを受ける。表を AST から組み直す第二の描画器を書けば §1.6 の「字面が原本にある」検査が二重になる。行の名前と番号は、`## 表 X` の見出しと `#` 列から付ける。フォームの欄は、規則の記述(入力の種類・単位・範囲・列挙の値・率の刻み)を JSON で積み、script が組む。入力の欄は二度書かない。
**検査**:行の属性、試すフィールド、生成 JavaScript、例のワイヤ形式が入っていること。markdown の全セルが HTML にもあること。ページの script を node が構文として読めること。ブラウザが手元に無かったので、DOM の代わりの最小の代役を置いて script を node で走らせ、例のボタンで当たった行と結果が規則の `examples` どおりに出ることを確かめた。この確認はテストには入れていない。
### 15.38 MCP サーバは、コマンドの表のもう一つの描画(2026-09-16)
**決定**:`rulec mcp` を足す。stdio で JSON-RPC を一行ずつ読み書きし、コマンド一つをツール一つ、フラグ一つを引数一つにして出す。ツールの一覧は §12.1 の表から機械的に作る。目的、値の閉じた集合、exit code の意味、例が表にあるので、説明もそこから出す。呼び出しは同じバイナリを同じ引数で走らせ、印字したものと exit code を返す。コマンドの実装は二つにならない。手順書(AGENTS.md)とリファレンス四つはビルド時に埋め込んでリソースとして出し、互いへのリンクはリソースの URI に書き換える。
理由。スキルは Claude Code の外にも届き始めているが、シェルの無い環境(チャットの画面、MCP で話す IDE)には届かない。そこにも同じ手順と同じコマンドを届ける安い方法が、表のもう一つの描画である。届けるものの中心は手順であってツールではないので、AGENTS.md をリソースとして出し、`initialize` の指示文で最初に読めと言う。
**決めたこと**:発見(exit 1)は誤り(`isError`)にしない。誤りは exit 2 だけである。表が知らない引数は、走らせる前に JSON-RPC のエラーとして返す。CLI が知らないフラグを exit 2 で止めるのと同じ線で、黙って落とすことは嘘の緑と同じだからである(§12.1)。
**捨てたもの**:コマンドをプロセス内で呼ぶ案。各コマンドが標準出力へ直接書いているので、文字列を返す形に全部書き換えることになり、同じ結果を二通りの経路で出すことになる。同じバイナリを子プロセスで走らせれば経路は一つで、バージョンもずれない。依存を入れて MCP のライブラリを使う案。依存ゼロ(§12)で、要るのは一行の JSON の読み書きだけである。
### 15.39 五つめの丸め、half_down(2026-09-16)
**きっかけ**:社会保険料の表を転記した。協会けんぽと日本年金機構の保険料額表は、被保険者負担分(折半額)に円未満の端数が出たとき、給与から控除するなら「50銭以下の場合は切り捨て、50銭を超える場合は切り上げ」、被保険者が現金で納めるなら「50銭未満の場合は切り捨て、50銭以上の場合は切り上げ」と書いている。後者は `half_up` で書ける。前者は四つのモードのどれでもない。半分ちょうどが 0 へ寄る丸めである。
**決定**:`half_down` を足す。半分ちょうどは 0 へ寄せ、負の値はほかのモードと同じく 0 について対称にする(4.5 → 4、−4.5 → −4、4.6 → 5)。語は Java の `RoundingMode.HALF_DOWN` と同じで、四つの語を選んだときの基準(§1.2)は変えない。評価器、六言語の生成物とその丸めの単位ベクタ、`explain E104` の文面、文法と `tour` の表に同時に入れた。
**捨てたもの**:規則の側で `折半額 − 50銭` を切り上げる、のような細工。50銭ちょうどのときだけ答えが違う規則を、式の形に隠すことになる。丸めの向きは宣言に見える形で書かせる、という §7.2 の原則に反する。
### 15.40 公開された規則を転記して、表と突き合わせる(2026-09-16)
**決定**:公開されている規則を五本、コーパスに足す。国税庁タックスアンサーの所得税の速算表(No.2260)、印紙税額の一覧表(No.7140、No.7141)と軽減措置(No.7108)、協会けんぽの保険料額表(東京支部、令和8年3月分から)、日本年金機構の厚生年金保険料額表(令和8年度版)。どれも取ってきた原本から転記し、`table` 行の `# 出典:` に文書名と時点を書く。保険料の二本は、印刷された折半額を転記した記録(`tests/oracle/`)に replay を当てて全件一致することを `tests/library.rs` で固定する。記録を作る段で 標準報酬月額 × 料率 ÷ 2 と突き合わせるので、転記の誤りはそこで止まる。「表が現実と合っているか」に道具は答えられない(README の証明していないことの一番目)が、原本の数字をもう一度別の形で転記して当てることはできる。
**転記して出たこと**:
- 丸めが一つ足りなかった(§15.39)。
- 定義の中の日付リテラルが、六言語すべてで 0 に生成されていた。`define 軽減期間 = 作成日 <= 2027-03-31` が `made <= 0` になる。参照評価器は正しく読むので、`rulec test` の突き合わせで出た。セルの日付は生成していたが、式の中の日付は行使されていなかった。§15.9 と同じ形である。
- 銭を持つ出力は書けない。`money[円]` と `money[銭]` は別の型で、値は換算されない(換算されるのはリテラルだけ。§15.18)。保険料額表の折半額は 6,599.5 円のように銭を持つが、出力は円の整数に丸めるしかない。転記した規則は円の出力を二つ(給与控除額と現金納付額)返し、銭の付いた折半額は記録の側に残した。銭の出力を持たせるなら、単位の変換をどう書かせるかを §15.18 の延長で決める。
- 定義の中で列挙の値と比べる形(`種別 == 率引き`)は、診断なしに定義が消え、その列が E012 で落ちる。今回の規則には要らなかったので触っていない。
- 速算表の段の上端「1,949,000円まで」は、整数の全体で完全性を言うには「1,950,000円未満」と書き直す必要があった。課税所得が 1,000円 単位だから同じことだが、その理由は規則の中にコメントで書いた。
- 印紙税の「記載金額のないもの」は、金額の入力とは別の真偽の入力(金額の記載あり)にした。金額を 0 円で代用すると、1 万円未満の非課税の行と区別できない。
- 料率は入力にした。都道府県ごと、年度ごとに変わるもの(協会けんぽ)と、基金ごとに違うもの(厚生年金)を規則に焼き込むと、改定のたびに表が書き換わる。P3 のとおりである。
- ベクタが 1,215 件になった。50 等級の表に入力が三つ加わり、pairwise の場合分けが 1,065 件を占める。既存の最大は 242 件で、コーパスの検査は 1 本 600 件未満を折り畳みの故障の目印にしていた。故障ではなく規模なので、目印は 2,000 件に上げた。行数と入力の候補の積で増える形は、規則が大きくなれば再び当たる。
**取り下げたもの**:ゆうパックの二重転記。日本郵便の運賃のページは取れなかった(403)。
### 15.41 CSV からの下書き(2026-09-16)
**決定**:§1.4 の `rulec import csv` を実装する。見出し行のある CSV を読み、列を入力に、末尾の列(`--outputs` で本数を変えられる)を出力にして、`.rule` の下書きを標準出力に書く。列の型は値から推定する。全部が同じ単位の数なら `money`・`rate`・`mass`・`length`・`number` のどれかで、範囲は見た値の最小と最大。全部が日付なら `date`。それ以外は現れた値をそのまま列挙にする。数値の出力には `round down(1)` を置く。**推定した箇所には全部 `# 推定` を付け**、表の行には `# 出典:` の列を用意する。数値の列のセルは書いてあった値との等値として転記し、閾値なら比較に書き換えるよう表の下に注記する。
理由。最初の利用者はエージェントで、シートを読んで下書きを書くことは既にできる。それでも importer を置くのは、決定的な道具が「何を推定したか」を漏れなく印として残せるからである。人が手で試す最初の五分も、これで表の形は整う。`rulec doc` は宣言行のコメントを承認者に見せる(§1.6、§15.32)ので、推定の印はそのまま承認者の目に届く。
**やらないこと**:範囲も丸めも単位も決めない。数値の列を閾値に読み替えない。check を通すことを目標にしない(表引きの完全な CSV なら通るが、値の抜けた表は E101 で穴が出るのが正しい)。xlsx は読まない。zip と XML の読み手を自前で持つか最初の依存を入れるかは、実際の移行の案件が来てから決める(§1.4、§13 の台帳のとおり)。
### 15.42 配布と、PR に貼るまで(2026-09-16)
**決定**:四つ。(1) `v*` のタグで静的バイナリ四つ(macOS arm64/x64、Linux x64/arm64、Linux は musl)を作り、`SHA256SUMS` と一緒に GitHub Releases に置くワークフローを足す。タグが Cargo.toml の版と違えば作らない。(2) リポジトリ直下に `action.yml` を置く。`uses: i2y/rulec@v0.1.0` で、その ref と同じリリースのアーカイブを取り、`SHA256SUMS` と突き合わせて PATH に置く。`with: sha256:` で一つのハッシュに固定できる。それ以外は何もしない。(3) `replay` と `diff` の `@` を広げる。`送料@v3` はこれまでどおり git タグ `rules/送料/v3` を引き、タグが無ければ `v3` をリビジョンとして引く。`rules/送料.rule@origin/main` は、そのリビジョンのそのパスを読む。(4) `replay` と `diff` に `--terse` を足す。入力例の列(markdown)と `例:` の行(テキスト)を出さない。`--format json` とは一緒に書けない(check と同じ)。
理由。§12 は静的バイナリの配布を決めていたが、ワークフローは無く、入れる手段は `cargo install --path .` だけだった。Rust の toolchain 無しでは入らない道具は、CI にも人の手元にも置かれない。PR に「何件がいくら動くか」を貼る部品(`check --format json`、`gen --check`、`diff --format markdown`)は揃っていて、足りなかったのは配布と、PR の形に合わせる三つの小さな差だった。一つ、PR が比べたいのは base ブランチの版で、タグではない。二つ、`diff` の exit 1 は「影響がある」であって失敗ではないので、ワークフローでは `|| [ $? -eq 1 ]` と読み替える。三つ、入力例の列には本番の記録の値がそのまま出る。PR のコメントはリポジトリを読める全員に見えて残るので、貼るものからは外せなければならない。
**捨てたもの**:ゲート五行と投稿まで抱えた action。§12 の分担(整形は rulec、投稿は CI の一行)を action の中に隠すことになり、五行を読めば分かるものが読めなくなる。`diff` に exit 0 で返すフラグを足す案。exit code は契約(§11 原則 6、§12.1)で、読み替えはワークフロー側の一行で済む。`--terse` で入力例の列を空にして残す案。列が残れば読む人は「なぜ空か」を考える。無いほうが表として正しい。Windows のバイナリ。§12 の範囲の外で、要望が出てから。
### 15.43 入口のガードは、整数かどうかも見る(2026-09-16)
**決定**:Python・Ruby・TypeScript・JavaScript の入口のガードに、範囲より前に「整数か」の検査を足す。Python は `int` であって `bool` でないこと、Ruby は `Integer`、TypeScript と JavaScript は `typeof v === "bigint"`。静的に型のある三言語には要らない。
理由。生成した Python に率 18.3 を渡すと、範囲 0..300 の中に収まるので通り、0.1% 刻みの 183 のつもりの値が 1.83% として計算され、2379.0 が黙って返った。型検査を通した呼び出し側では起きないが、JSON から呼ぶ相手(§15.44 のツール、HTTP の受け口)に型検査は無い。黙って間違わないことがこの道具の存在理由である(§15.13)。
**捨てたもの**:小数を刻みに丸めて受ける案。18.3 が 183 の書き間違いなのか 18 の書き間違いなのかは分からず、どちらに倒しても黙って別の答えになる。エラーにして、呼び出し側に言い直させるほうが正しい。
### 15.44 規則を呼ぶエージェントには、規則そのものをツールにして渡す(2026-09-16)
**決定**:`gen` が `<alias>_mcp.py`(Python)と `<alias>_mcp.ts`、その型を消した `<alias>_mcp.mjs` を書く。stdio の MCP サーバで、ツールは一つ、名前は規則の別名。`inputSchema` は `rulec schema` の `in` そのもの、結果は `_record` の一行を text と `structuredContent` の両方で返す。契約の外の呼び出しは、`isError` と引数の名前を返してエラーにする。`--record <file>` で、答えた呼び出しを fixtures として追記する。`rulec test` はサーバを client として起動し、initialize・tools/list・tools/call をベクタの件数だけ回して、runner と同じ期待値に一字一句突き合わせる(結果の `via` が `mcp`)。`rulec api` は `mcp` にファイル名を出す。`schema` の description は単位と、率なら刻みを言う(「整数。率を 0.1% 刻みの個数で書く(100% なら 1000)」)。
理由。エージェントに「この注文は返品できるか」を毎回判断させると境界のあたりで揺れ、監査もできない。判断を表に落として、エージェントには決定的な関数を呼ばせる。書くのもエージェント、呼ぶのもエージェントで、あいだに証明が挟まる。いまの位置づけ(書くエージェントのハーネス)の残り半分である。
ここで P5 が効く。rulec が自分の評価器で判定を返し始めれば、捨てたはずのランタイムエンジンになり、rulec のバイナリが本番の依存になる。だから rulec はサーバに**ならず**、サーバを**生成する**。承認者向けの HTML(§15.37)が rulec ではなく生成した JavaScript を動かすのと同じ線で、サーバも生成物だから一致検査に乗せられる。
ツールの形をワイヤに揃えたのは、呼び出しがそのまま記録になるからである。エージェントの尋ねたことが、次の改定の `diff` の分母になる。LLM の入力は雑なので、完全性が証明済みで「当てはまる行が無い」と返さないことと、契約の外は名前の付いたエラーで止まることが、この相手にはそのまま効く。
**捨てたもの**:`rulec mcp` に規則を評価するツールを足す案(上記、P5)。率を `"18.3%"` の文字列で受ける案。ワイヤが二つになり、fixtures・schema・adapter が揃えている整数のワイヤに二つめの形が乗らなくなる。説明で刻みを言い、整数でなければエラーにする。複数の規則を一つのサーバで出す案。生成は規則ごとで、集めるのは呼び出し側の数行で済み、要るなら後で足せる。ビルド無しで立てられない言語のサーバ。要望が出てから。
### 15.45 入口の検査は、schema が元(2026-09-16)
**決定**:`rulec schema --keys alias` を足す。プロパティのキーを規則の名前ではなく ASCII の別名にし、元の名前を `title` に入れる。既定は名前のまま。zod や pydantic や OpenAPI の生成器は足さない。サイトの「生成して呼ぶ」に、schema を入口の検査の元にする節を置き、JSON Schema からの変換器(datamodel-codegen、json-schema-to-zod)を指す。
理由。生成コードの入口のガードと同じ情報が `rulec schema` に既に出ている。足りなかったのは、HTTP の受け口やフォームが欲しがる別名のキーと、「これが入口のスキーマです」と書いた一ページだけだった。JSON Schema を読む生成器は言語ごとに枯れたものがあり、rulec が抱えれば古びる一方で、抱えなくても同じものが出る。
**捨てたもの**:`rulec gen` に zod・pydantic のファイルを足す案(上記)。ワイヤそのものを別名にする案。fixtures・adapter・ツール(§15.44)は名前で揃っていて、別名は API の受け口のためのもう一つの綴りに留める。
### 15.46 八つめの生成対象に SQL(2026-09-16)
**決定**:SQL を八つめの生成対象にする。出すのは関数ではなく、入力の関係 `<alias>_input`(一行が一件。`_id` と入力の列)に対する一つの問い合わせ `sql/<alias>.sql`。CTE の連鎖で、入力を BIGINT に揃え、定義を一段ずつ列に足し、表ごとに「当てはまった行の番号」の `CASE` と、その番号で引く出力の `CASE` を置き、最後に出力ごとの丸めを算術で当てる。返るのは `_id`、入力、出力、表ごとの `<表の別名>_row`、そして `_input_error`。問い合わせは止まれないので、入口のガードは列にする。宣言の中なら NULL、外なら他言語が投げるのと同じ文(無い、整数でない、範囲の外、列挙に無い)で、整数の検査は BIGINT に揃える前の値で行う(18.3 を 18 に切って通さない、§15.43)。W114 の対があれば `_contradiction` 列に文を出す。runner はどちらでも止まる。方言は PostgreSQL 向けに書き、SQLite でも同じに動く範囲(`CASE`・`ABS`・`%`・0 方向の整数除算・`IN`・`TRUE`)に留める。`min`/`max` は `LEAST`/`GREATEST` で、runner が SQLite に登録する。一致検査は `sql/<alias>_runner.py` が python3 の sqlite3 でベクタ全件を流して行い、丸めの五モードも `sql/_round_test.py` が SQLite で 270 件回す。`rulec api` は `sql` の項目(file・input・id・columns・outputs・rows)を出す。式は `Gen::expr` の書く Python の綴りを小さな構文解析で読み直して SQL にする(名前は引用符付きの列、`//` は `/`、丸めの呼び出しは引数を列に束縛してから算術に展開)。
理由。料金の再計算、締めのバッチ、分析の集計は SQL で書かれ、アプリ側とずれるのが定番の事故で、同じ表から出た SQL とアプリのコードが一致することを、既存の一致検査でそのまま言える。それだけではなく、SQL は実行の仕方が違う。一件ずつの関数では百万行の再計算はできないが、関係に対する一つの問い合わせなら一文で済み、`diff` を fixtures を抜き出さずにデータベースの中で全件に当てられる。
§15.13 の論点はこう決めた。方言は PostgreSQL に絞るが、検査を SQLite で回せる部分集合に留める。一致検査に乗らない言語は入れない、という原則を守れる唯一の手で、int4 の溢れは入口で BIGINT に揃えて避ける。形はパラメータ付きの文でも関数でもなく、関係に対する問い合わせ。ビューにできるし、パラメータの綴りは方言で違うが関係なら同じ。整数除算は両方言とも 0 方向で、`down` の丸めはそれそのもの。Python の `//` が出る唯一の場(刻みで割り切れない定数除算)は両者で負の向きが違いうるが、コーパスには無く、出れば三者一致が捕まえる。コーパス 18 本、3,039 ベクタが初回から全件一致した。
**捨てたもの**:`CREATE FUNCTION` で丸めを関数にする案。SQLite で検査できなくなり、丸めこそ間違いが隠れる場所である。パラメータ付きの一件用の文。方言ごとに綴りが違い、一括の再計算ができない。SQLite を第一の方言にする案。置き場としては PostgreSQL のほうが多く、SQLite は検査の道具に留める。日付を `DATE` 型で受ける案。日数の整数なら方言差が無く、呼び出し側は `date - DATE '1970-01-01'` の一行で済む。
### 15.47 「証明」という語(2026-09-16)
**決定**:「証明」は、静的に示す五つ(完全性、重なり、当てはまらない行、単位、int64 に収まること)にだけ使う。意味は「書かれた表について、宣言した範囲のどの入力でも成り立つことを、しらみつぶしに調べて示した」で、相対的なものが二つある。宣言した範囲と、書かれた表そのもの(転記の誤りはそのまま通る)。そして検査器自身の正しさ(その証拠は tests/mutants であって、証明ではない)。だから「証明」と書く場所には、何についての証明かと、証明していないこと(表と現実、生成コードとの一致、W114 の対)を隣に置く。ツールの出力は、肯定形では「証明」を使わない。生成コードのコメントは「静的に検査済み」、承認する人のページは「rulec check が確かめたこと」で、「証明」が出るのは否定形だけ(「証明できませんでした」「存在しないことの証明ではありません」)。「証明済み」をコードに付けない。証明済みなのは表で、コードは証明済みの表から生成したもの、表との一致はテストである。流れの矢印は「通ったら」で、「証明できたら」とは書かない。検査は通るものであって、証明は「できたら」するものではない。
理由。区画の検査はサンプリングではない。入力を、答えの変わらない区画に分けて全部を調べ、予算を超えれば E109 で止まり、両方に当てはまる入力を構成できない対は W114 で挙げる。モデル検査や静的解析が prove と言うのと同じ基準であり、境界から作ったケースを流す一致検査(テスト)とは別物である。この二つを同じ語にしないことがこの道具の骨で、「これは証明ではなくテスト」という一文は、「証明」という語があってはじめて意味を持つ。
**足したもの(§15.95、2026-09-20)**:モデル検査器を通した生成物を、どう言うか。「Kani が通った」は**生成物についての証明**である——この Rust が、宣言した範囲のどの入力でも穴に落ちず、ガードに当たらず、i64 をあふれさせない。**表についての証明ではない**し、検査器が正しいことの証明でもない。区画の検査と Kani は一行もコードを共有しないので、二つが同じことを言うのは証拠として重い(食い違えばどちらかにバグがある)。ただし重いのは証拠であって、語は分ける。生成物のコメントも `rulec test` の行も「証明」を肯定形で使わない線は動かさない(ハーネスの名前は `_answers` と `_rows` で、検査に通ったことは `ok` と書く)。
**捨てたもの**:全部を「確認」にする案。網羅した検査とサンプルのテストが同じ語になり、正直さが増すのではなく減る。「検証」に言い換える案。`rulec verify` が旧実装との突き合わせを指しているので、静的な証明を検証と呼ぶと二つが混ざる。生成コードに「証明済み」を付ける書き方。サイトの流れ図の「証明済み・依存ゼロ」と一枚目の図の「証明できたら」がそれで、「証明済みの表から・依存ゼロ」と「通ったら」に直した。
### 15.48 ブラウザで試せる面(2026-09-17)
**決定**:検査器を `wasm32-unknown-unknown` に載せ、ドキュメントサイトに `playground` のページを両言語で置く。境界を渡るのは六つの関数(`rulec_alloc`・`rulec_free`・`rulec_version`・`rulec_check`・`rulec_gen`・`rulec_doc`)と一つの約束だけ。**どのバッファも先頭四バイトに自分の長さを持つ**(little-endian の u32)。bindgen も JS の道具立ても入れない(§12 の依存ゼロは、生成物だけでなく道具の側にも効く)。ページが受け取るのは `check` の文面と診断の JSON、`gen` が書く全ファイル、`doc --format html` のページで、最後の二つは規則が通ったときだけ返る(§1.6)。動かすのはブラウザの中だけで、打ち込んだ表はどこにも送らない。
`website/docs/playground/rulec.wasm` は**リポジトリに入れる**。図と screenshot と同じ扱いで、そうしないとサイトのビルドに Rust の toolchain が要る(`website/README.md` の「SVG を commit するのは、サイトのビルドに Python を要らなくするため」と同じ線)。入れたものは古くなりうるので、`tests/wasm.rs` が node で動かして**バイナリの出力と一字一句突き合わせる**。`check` の文面を両言語で、`gen` の全ファイルを、`doc` のページを、そして `rulec_version` が Cargo.toml の版と同じことを。古い wasm はテストの失敗になり、ページが黙って古い答えを返すことは起きない。作り直しは `website/tools/make_wasm.sh`(`opt-level=z` で 900 KB、gzip で 315 KB。まだ何も install していない読み手に届く大きさを、人が打った表の検査の速さより優先した)。
理由。最初の接触が「表を貼って、穴が出るまで 60 秒」になるかどうかが、この道具が使われるかを決める。install の前に置いたのはそのためで、nav でもホームの次に入れた。`check` の答えは表そのものだけで出る(データも旧実装も要らない)ので、サーバの無いページで成立する唯一の機能でもある。
**決めたこと**:文面はライブラリ側に切り出して共有する。`findings_text` と `check_tail` を `lib.rs` に置き、`main.rs` の `check` もそれを印字する。ページと端末が違う答えを返す余地を、実装の側で無くした(§15.38 で「同じ結果を二通りの経路で出さない」と決めたのと同じ)。ページが開くのは**最後の行を落とした表**で、これはホームの一枚目の絵と同じ表である。最初に見えるのが「完全性の欠落」と、すり抜ける具体的な入力になる。
**捨てたもの**:wasm を CI で作ってサイトに載せる案。docs の workflow が Rust に依存し、`website/README.md` が二度書いている「ビルドに toolchain は要らない」が崩れる。bindgen を入れる案(依存が増え、生成される glue を読まずに信じることになる)。`rulec mcp` のように wasm を別クレートにする案(`Cargo.toml` の `[lib]` に cdylib を足すと host のビルドでも dylib が出る。`cargo rustc --crate-type cdylib` で足りる)。ページに `verify`・`replay`・`diff` を出す案。プロセスとファイルが要る。出せない機能を出さないことは、出せる機能の信用になる。
### 15.49 gen は自分で検査する(2026-09-17)
**決定**:`rulec gen` は生成の前に `report` を走らせ、エラーが一つでもあれば何も書かずに exit 1 で止まる。
**きっかけ**:playground(§15.48)の生成タブが規則をエラーにし、コマンドが同じ表から生成したので、テストが食い違いとして落ちた。調べると `gen` は `prepare`(構文と型)しか通していなかった。表そのものの検査——完全性、重なり、当てはまらない行、オーバーフロー、例——を走らせないまま生成していた。**穴のある表から八言語ぶんのコードが出て、exit 0 が返っていた。**
理由。README の「証明が先に終わっている」も、AGENTS.md の "It refuses to generate from a rule that does not pass check" も、サイトの「検査を通らない規則からは、何も生成されません」も、この一行を前提にしている。CI では `check` を先に走らせるので露見しなかったが、`gen` を単体で呼ぶ相手——エージェントがまさにそれである——には、約束が守られていなかった。`doc` は §1.6 のために同じ検査を自分で走らせており、`gen` だけが素通りしていた。
**捨てたもの**:`check` を先に走らせるよう文書で促す案。約束は文書ではなくコマンドが守るものである。`--force` のような抜け道を足す案。穴のある表から出たコードは、穴をそのままコードにしただけで、置いておく値打ちが無い。
### 15.50 xlsx をそのまま読む(2026-09-17)
**決定**:`rulec import xlsx <file.xlsx>` を足す。`--sheet <名前>` でシートを選び、既定は最初のシート。読むのは自前で、ZIP の目次と DEFLATE と、要素と属性を辿るだけの XML を `src/xlsx.rs` に置く(依存ゼロは §12 の決定で、道具の側にも効く)。下書きを起こす手続きは csv と共有する(`import::draft_rows`)。読み終えた先は同じ表なので、**同じ表なら csv から起こしても xlsx から起こしても一字一句同じ下書きになる**。これはテストで固定した。
表計算が CSV と違うのは三か所だけで、そこだけを読む。**日付は数**(1899-12-30 起点の通し番号。1904 年起点のブックもある)、**率は分数**(18.3% はセルに 0.183 と入っている)、**単位は書式にある**(セルは 990 で、`#,##0"円"` が円を書いている)。三つとも `xl/styles.xml` の書式が決めるので、書式を読んで値に戻す。率の百倍は文字列のまま桁をずらす。float を通すと 0.1% 刻みが壊れるからである。
何も入っていない行と列は落とす。表が C3 から始まっていても表として読める。読んだシートの名前は出典に書く(「tariff.xlsx」だけでは、どの表かを言っていない)。
理由。業務から届くのは `.xlsx` である。「CSV に書き出して送ってください」と頼むと、転記の前に人の手が一つ増える。そこで止まる。§13 の「最初の価値は転記した瞬間に穴が出ること」は、転記が始まらなければ何も起きない。
**決めたこと**:単位は、ブックが**書いているとき**だけ採る。引用符の中の文字(`"円"`)と ¥ 記号である。`$` は採らない。USD にも CAD にも AUD にも読めて、どれかを決めるのは importer の仕事ではない(§1.4 の「額を決めない」と同じ線)。1900-02-29 は実在しない日で、Excel が互換のために持っている通し番号 60 である。日付にはせず、数のまま残す。ふりがな(`rPh`)は値ではないので混ぜない。数式は `<f>` を読まず、キャッシュされた値(`<v>`)を読む。シートが見せているものが表である。
**捨てたもの**:crate を入れる案(zip・calamine。§12)。`.rule` から xlsx を書く向き(§1.4。始めた瞬間に Excel が正本になる)。セル参照を出典として `.rule` に書く案(§15.32 で退けたのと同じ理由で、出典の照合は check の仕事ではない)。ZIP64 と暗号化されたブック。読めない理由を名前で返す(「ZIP64 の xlsx は読めません」)ほうが、半分読めたふりをするより良い。
### 15.51 規則のサーバは、URL でも呼べる(2026-09-17)
**決定**:生成する MCP サーバ(§15.44)に、MCP の Streamable HTTP を足す。`--http [<host>:]<port>` で待ち、外の Origin は、`--origin` に書いたものだけを通す。stdio と HTTP は**同じ `handle()` を通る**。メッセージ一つを受けてメッセージ一つを返す関数で、運びだけが違う。だから `rulec test` は**両方の運びでベクタを全件流す**(結果の `via` が `mcp` と `mcp-http`)。運び方で答えが変わればそれは食い違いである。生成する Python のサーバは、レスポンスのヘッダーと本文を分けて書くので、ハンドラーに `disable_nagle_algorithm = True` を付けて、すぐに送らせる(2026-10-23。付けないと、ACK を遅らせる Linux の呼び手には、呼び出しごとに 40 ミリ秒ほど遅れて届いた)。`rulec test` のほうも、リクエストをヘッダーと本文に分けずに一度で書き、ソケットの Nagle アルゴリズムを切る(2026-10-23)。
理由。規則を呼ぶ側にいる相手——チャットのカスタムコネクタ、エージェントを組み立てる画面、ワークフロー製品の MCP ノード——は、**URL しか受け付けない**。stdio で届くのは同じ機械の上のエージェントだけで、そこから先に一歩も出ない。§15.44 で「呼ぶエージェントには規則そのものを渡す」と決めたのに、渡せる相手が手元だけに限られていた。
P5 はここでも効く。rulec 自身はサーバにならず、サーバを**生成する**。生成物だから一致検査に乗り、rulec のバイナリが本番の依存になることもない。
**決めたこと**:TLS と認証は**入れない**。前に置くものである。中途半端な認証は、無いことより悪い(あると思わせる)。だから既定で待つのは `127.0.0.1` だけにし、`Origin` が手元でない要求は受け付けない。ブラウザで開いているページが、その人の機械で動いているサーバを叩けないようにするための検査で、これは仕様が要求している。id の無いメッセージには 202 だけを返す。`GET` は 405——こちらから送るものが無いので、開く流れが無い。セッション id は initialize で渡し、以後は照合して、知らない id には 404(クライアントに「やり直せ」と言う形)。まとめ送り(batch)はこの版の仕様に無いので 400 を返す。半分読むより良い。
型を落とす器(`strip_types`)も直した。引数の型に `Record<string, string>` のようなコンマを含む総称が来ると、コンマで引数を切ってしまっていた。括弧と山括弧の深さを見て、**外側のコンマだけ**で切る。
**捨てたもの**:SSE で流す案。押し出すものが無い。認証を足す案(上記)。既定で `0.0.0.0` に待つ案。手元で試すつもりの一行が、そのまま社内ネットに口を開ける。サーバをもう一つ生成する案。ツールの実装が二つになり、片方だけ直る日が来る。
### 15.52 ツールの答えの隣に、表そのものを出す(2026-09-17)
**決定**:MCP Apps(SEP-1865、2026-01-26 に stable)に乗る。`gen` が、サーバの隣に `<alias>_page.html` を書く。中身は `rulec doc --format html` が描くページ**そのもの**(同じ描画器、同じ生成 JavaScript、一字一句同じ)である。サーバはそれを `ui://<alias>/table`、mimeType `text/html;profile=mcp-app` の資源として出し、ツールの `_meta.ui.resourceUri` がそれを指す。ホストは `resources/read` で取り、砂場の枠で描く。
ページの側には橋を足した。枠の中にいるとき(`window.parent !== window`)だけ、`ui/initialize` を送り、返事が来たら `ui/notifications/initialized` を出し、`ui/notifications/tool-result` が来たらその `structuredContent.in` でフィールドを埋めて計算する。**いま訊かれた件が入った状態でページが開く**。ファイルとして開いたときは、この一段は動かない。ページは一つで、住む場所が二つになる。
理由。ツールの答えは一行の記録で、正しいが、チャットを読む人には数字に見える。同じ呼び出しの隣に表が出て、当てはまった行に色が付いていれば、**どの表のどの行がそう言ったか**が読める。§15.37 で承認者向けのページを作ったときの動機(表の説明では代替できない、自分の件で読む)が、そのままチャットの中に届く。
P5 もここで守られる。rulec は描画しない。ページは生成物で、動いているのは生成した JavaScript で、rulec のバイナリはどこにもいない。
**決めたこと**:view は、**描けると言ったホストにだけ**出す(`initialize` の capabilities に `io.modelcontextprotocol/ui` があるときだけ `_meta.ui` を付ける)。仕様が「UI は増分であって要件ではない」と言っており、描けない相手に view を約束するのは嘘である。ページは**ファイル**として隣に置く。三つの言語の生成物にそれぞれ同じ HTML が入るが、各言語のディレクトリが自足するという既存の作りに合わせた。文字列として生成物に埋め込む案は、三言語ぶんの引用符の逃がし方を抱えることになる。CSP は宣言しない。外に一つも繋がないので、ホストの既定(`default-src 'none'`、inline の script と style だけ許す)で足りる。
**確かめ方**:`tests/tool.rs` が、資源が `doc --format html` と一字一句同じであること、描けないと言った相手には `_meta` を付けないこと、知らない URI を、名前を挙げてエラーにすることを見る。ブラウザの側(握手と、結果を受けてフィールドが埋まること)は、ホストを模したページを headless Chrome で走らせて手で確かめた(screenshot と同じ扱い)。送料の規則で、`基本送料#1` と `負担判定#3` に色が付き、`送料 = 1200円` が出ることまで見ている。
**捨てたもの**:ホストのテーマ(明暗)に合わせる案。ページの色は承認者向けの紙の色で、いまは一つでよい。`ui/message` や `ui/update-model-context` を使ってページから会話に書き戻す案。表を読む面であって、書く面ではない。view の中から `tools/call` を呼んで再計算する案。ページはすでに生成コードを持っていて、手元で計算できる。往復する理由が無い。
### 15.53 値を誰が決めたかは、記録の `by` に置く(2026-09-17)
**決定**:fixtures の一行に、省略可能な最上位のキー `by` を足す。入力の名前で引き、`{"src":…, "ver":…, "conf":…}` を持つ。`src` は決めたものの短い識別子(`jev`・`agent`・`ops`)、`ver` はその版、`conf` は確信度を **0.1% 刻みの個数の整数**で書く(934 が 93.4%)。系からそのまま読んだ入力には項目を置かない。**いま rulec はこれを読まない。** lint も見ないし、`replay` の一致率にも入らない。
**きっかけ**:型のついた決定だけを返すモデル(TypeSafe の Jev)が出た。列挙から一つ選ぶ、真偽を確率で返す、といった形で、規則の入力そのものの形をしている。規則は純関数のままでよく、モデルの判定は**値として引数に入る**(P3。時刻や在庫と同じ扱い)。そこまでは既存の作りで足りていて、足りないのは「その値を誰が決めて、どのくらい自信があったか」だけだった。それが無いと、一月後に「モデルが 90% 未満だった件だけ集めて人の判断と突き合わせる」ができない。
**決めたこと**:`tag` には入れない。`tag` は記録の見出しで、入力例の行にそのまま出る(`order:1234567`)。そこに塊を入れると、報告が読めなくなる。`conf` を小数にしない。§10.2 のワイヤ(正準単位の整数)が二つになる。率と同じ刻みの個数なら、読む側の規則が増えない。`in` の中に値と並べて置かない。`in` は規則が知っているフィールドだけで閉じていて、知らないフィールドは誤りである(その検査があるから書き間違いが「既定値で埋めた」に化けない)。だから出所は `in` の外に出す。
**行使されていない構造は検査されない**(§15.9)。だから書式だけを決めて、検査は持たない。必要になったら lint に足す(キーが規則の入力であること、`conf` が 0〜1000 の整数であること)。それまでは、**書いてよい場所が決まっているだけ**である。ここを曖昧にしたまま「rulec が出所を追跡する」と読める書き方をすると、追跡していないものを追跡していると言うことになる。
**捨てたもの**:`in` の値を `{"value":…, "conf":…}` の形にする案(ワイヤが二つになり、生成コードの入口も fixtures も adapter も全部が二つの形を受けることになる)。出所ごとに別のファイルに出す案(記録と突き合わせるのに join が要る。一行で完結することが §10.2 の値打ちである)。`conf` を必須にする案。人が決めた値に確信度は無い。
### 15.54 「モデルに決めさせるな」ではなく「決まっているものを毎回考え直させるな」(2026-09-17)
**決定**:文書の主張を二つ直す。(1) `checks.md`(両言語)と README に、**証明と型検査は別のことを言っている**という段を置く。型が言うのは値の形で、形が正しいことは答えが正しいことを何も言わない。証明しているのは表の性質で、宣言した範囲を標本ではなく総当たりで調べている。証明していない三つ(表と現実、生成コードとの一致、W114 の対)はその隣に置いたままにする。(2) `generate.md`(両言語)の「モデルが毎回考え直す答えではなく表を渡す」を、**もう決まっている判断**(規約に書いてある、料金表に載っている、承認を経ている)を毎回考え直させない話に限る。まだ決まっていない判断は人かモデルの仕事で、その答えは値として渡す。(3) `fit.md`(両言語)の「向いていないもの」に**入力そのものを決めること**を足す。曖昧な状態から値を決めるのはこの道具の仕事ではない。確信度で分けるかどうかは業務の判断なので、それ自体は表に書ける。
**きっかけ**:型のついた決定だけを返すモデルが出た(§15.53)。「型が合っている・型エラーを起こさない」は強くて短い言い方で、読む人の頭の中で「形が正しい=安全」が既定になると、この道具の説明はもう一段むずかしくなる。そして「AI に判断させるな」という言い方は、そういうモデルがある世界では弱い。元から正しかったのは `fit.md` の言い方——表になるものとならないものの線——のほうで、そちらに寄せる。
**決めたこと**:ついでに、**文章の中の言語の数**を検査に乗せる。`tests/website.rs` に二つ足した。(1) 「七つの言語」「seven languages」のような数え上げの語を、登録簿の数と突き合わせる(サイトの書き下ろしのページと、リポジトリの文書の両方)。(2) `tests/docs.rs` がリポジトリの文書に掛けている「三つ以上並べて一つ落としたら失敗」を、サイトのページにも掛ける。閾値だけ四にした。サイトの文は「三つめの TypeScript は 700 行だった」のように言語を**例として**挙げることがあり、三では誤検出になる。四つ以上並ぶ段落は一覧である。
入れた時点で古さが四か所出た。ホームの「七つの言語」、`tour` の「出力が複数のとき何になるか」と「整数除算の向き」(どちらも JavaScript と SQL が落ちていた)、`generate` の「単位を置ける型が無い言語」(SQL が落ちていた。README では直っていた)と日本語版の冒頭の一覧(SQL が無かった)。**目で見つけた古さは、次も目で見落とす**(§15.9、§15.20)。
**捨てたもの**:特定の製品名を文書に書く案。道具の名前は古くなり、比較として読まれる。書くべきなのは「型のついた値を返す API やモデルが増えた」という形のほうである。「証明」という語をやめる案(§15.47 で決めたことを、外の言葉づかいに合わせて曲げることになる)。数え上げの語を文章から消して「登録簿を見よ」にする案。読む人は数を知りたいのであって、参照を辿りたいのではない。
### 15.55 起きない組み合わせを、宣言できるようにする(constraint、2026-09-17)
**決定**:`constraint <入力> <比較> <入力>` を足す。比較は `<=` `<` `>=` `>` の四つ、両側とも入力で、順序のある型(金額・数量・率・number・日付)に限る。複数行はすべて成り立つ。`A = B` は二行(`A <= B` と `A >= B`)で書く。宣言は何も計算しない。**どの組み合わせが起きるか**だけを言う。
効くのは四か所で、全部同じ一つの事実から出てくる。
1. **完全性の検査が、起きない組み合わせに行を要求しなくなる。** 実装は region の起きうるかのふるい(`feasible`)に一つ穴を開けるだけで済んだ。導出列を区間演算で潰しているのと同じ場所に、制約の区間演算を並べる。完全性の穴も行の重なりも、同じふるいを通っているので、両方に同時に効く
2. **出す入力が、起きうる入力になる。** ふるいが落とした組み合わせからは入力を出さない
3. **ベクタが制約を満たすものだけになる。** そうしないと `rulec test` が、生成コードが受け付けない入力を流すことになる
4. **生成コードが入口で受け付けない。** 八言語すべて。証明が仮定したことを実行時に守らせる、という §15.43・§15.45 と同じ線である。受け付けてしまうと、行の無い組み合わせに落ちて「到達しないはず」の分岐に当たる
**きっかけ**:表の `-` が二つの意味を持っていた。「この列はここでは関係ない」と「この組み合わせは起きない」である。前者は表の性質だが、後者は表の外の事実で、書く場所が無かった。書けないので `-` で埋めるしかなく、読む人には区別が付かない。§1.6 の「検査器が知っていて字面に現れない事実を人の言葉で添える」の裏返しで、**人が知っていて検査器に伝える場所が無かった**。
**決めたこと**:例が制約を破っていたら誤りにする(E019)。例は「この入力にこう答える」という主張で、生成コードが受け付けない入力について、答えを主張することはできない。承認者向けの資料に「起きない組み合わせ」の節を出す。検査がそれを信じて行を要求しなかった、という事実は、承認する人が同意すべきことである。制約だけに現れる入力を W111(使われていない宣言)にしない。空間を狭める仕事をしている。
**捨てたもの**:一般の論理式(`and` `or` `not`、含意)を許す案。区間演算で決まる形を超えると、ふるいが「分からない」を返す場面が増え、そのぶん W114 の側に流れる。一行一関係なら、読むほうも検査するほうも単純である。列挙どうしの関係(`X = a のとき Y は b ではない`)。同じ理由で v1 から外した。必要なら、その関係は数で書ける場合が多い。`assume` という語(何を仮定したかではなく、何が成り立つかを書く宣言なので)。
### 15.56 列を歩くことを、宣言できるようにする(elements と fold、2026-09-17)
**決定**:`elements` と `fold` を足す。`elements <名前>(<別名>)` は、規則が歩く列の**一要素ぶんのフィールド**を `inputs` と同じ書き方で宣言する。表はそのフィールドを列に使える。`fold <判定の列> over <列の名前>` は、要素ごとの判定を一つの答えに畳む。腕は六つ。`next`(次へ)、`stop`(終わり)、`stop with <値>`、`take_unique <値>`、`take_first <値>`、`keep_max <値> by <鍵>`。これに加えて `empty`(要素ゼロ件のときの答え)と `exhausted`(最後まで見終えたときの答え。`held` が保持中の値)を**必須**にする。
**なぜ検査が壊れないか。** rulec が検査できるのは「セルは自分の列しか見ない → 一行は入力空間の区画 → 隙間と重なりが算術で決まる」からで、任意のループを入れるとこれが崩れる。畳み込みは崩さない。**表が完全かつ一意なら、どの要素もちょうど一つの判定に落ちる。** 判定の型は有限の列挙だから、畳み込みは「有限アルファベット上の列に対する簡約」=小さなオートマトンであり、要素数が実行時に決まっても、列としての振る舞いは静的に決まる。要素ごとの表の証明(完全性・重なり・当てはまらない行・単位・オーバーフロー)は**一要素について、いままでどおりそのまま**成り立つ。
**増える検査は四つで、どれも人が手で書いた走査が落とすところである。**
1. **空のときの答え**が宣言されていない(E022)。空の列は必ず来る。手書きの走査がいちばんよく落とすのがここで、たいていは先頭要素をそのまま読んで落ちる
2. **最後まで到達したときの答え**が宣言されていない(E023)。保持していた暫定を返すつもりでも、それは書いて初めて決まる
3. **腕の無い判定がある**(E024)。表が出す値に歩き方が無い。表の完全性を、畳み込みの側に当てたもの。裏返し(どの要素も辿り着けない腕)は W115
4. **`take_unique` か `take_first` か**。`take` とだけ書くことはできない(E021)。二件目が誤りなのか、無視して先頭が勝つのかは、最初に書くときに選ぶことである。これは E104 が丸めを宣言させているのと同じ形で、**宣言があれば、書くときに選ばされる**
**歩きの意味(2026-09-17 の続き)。** 参照評価器を書いた。`take` と `keep_max` は**別の枠**に持つ。確定は definite、持ち越しは provisional で、暫定を持っていることは `take_unique` が一意である対象ではない。二つの**確定**が矛盾で、いくつ持ち越したあとの確定も答えである。この区別は、網羅の欠けが教えてくれた(「持ち越しのあと確定」に義務が立ったのに、評価器が矛盾と答えていた)。`exhausted` の `held` は、確定 → 持ち越し → `empty` の順に落ちる。だから `exhausted -> held` は全域で意味を持つ。
**ワイヤは列を持つ。** `in` の中で、`elements` の名前が**オブジェクトの配列**になる。要素のフィールドはそれぞれ正準単位の整数で、数の読み方は増えない(§10.2 のまま)。
**網羅の基準が五つになった。** 「畳み込みの遷移カバー」。義務は、要素ゼロ件・判定ごとの一件・**判定の順序対**すべて。列の長さを覆うのではない。歩きはオートマトンで、できることは「どの判定のあとにどの判定が来るか」で決まるからである。生成器と監査器は同じ義務を見る(§15.39 と同じ作り)。一要素の列は**一要素の場合**として扱い、行カバーと境界カバーはそこに効く。
**覆えていないものが一つあり、報告に出る。** `take_unique` が二度当たる並びは、生成コードが矛盾として落ちるので答えが無い。期待値の記録に「誤り」の形がまだ無いので、ベクタからは外し、**網羅の側で欠けとして挙げる**(20/21 のように出る)。黙って満点にはしない。
**(2026-09-17 の続き)受け付けないことを期待値にした。** 上の「覆えていない一つ」を、記録の形を広げずに閉じた。**答えの無い入力は、無い入力ではなく、受け付けないことが答えの入力である。** だからベクタとは別の三本目のファイル `vectors/<別名>.refused.jsonl` に置く(中身は `in` だけなので、生成されたランナーがそのまま読める)。`rulec test` はこれを**一件ずつ**入れて、答えを返さずに止まることを要求する(生成コードは最初の一件で落ちるので、まとめて入れると一件しか試せない)。MCP サーバにも同じことを聞き、`isError` を求める。網羅はこの入力も義務を果たすものとして数えるので、`fold_transition` は 21/21 になる。満点にしたのではなく、**確かめる手段を足した**——手を入れて受け付けるようにした言語があれば、テストがその言語の名前を出して、その場で落ちる(tests/fold.rs)。
`rulec verify` はこのファイルを使わない。外の実装に「答え」を求める道具で、ここには比べる答えが無いからである。
**生成は七言語に入った(2026-09-17 の続き)。** Python・TypeScript・JavaScript・Rust・Ruby・Go・Swift が歩きを書く。形はどの言語でも一つである。`empty` の値で始めた答え、終わったかどうかの印、確定の枠、持ち越しの枠、鍵の最大。そして列を一周するループ。ループの中身は**一要素ぶんの入り口検査と、規則の項目そのまま**で、腕は判定ごとの `if` が一つずつ。ループを抜けたところで `held` を確定 → 持ち越し → `empty` の順に落とし、`exhausted` の値を答えに入れ、最後に**出力の名前**に束ねる。こう畳むと、単位も丸めも戻り値も**既にある道をそのまま通る**ので、畳み込みのために書き換える場所が無い。要素のフィールドは入力と同じ入り口検査を受ける(§15.43)。ワイヤは列なので、ランナーは配列を読み、記録は配列を書く。一覧(`rulec api`)も列を最後のフィールドとして載せ、要素のフィールドをその下に並べる——一覧を読んで呼ぶ側が引数を一つ落とさないため、そして一覧の署名が生成物の署名と一字一句同じであることは検査してある。
**試用フィールドは列を編集できる。** ページ(§15.52)は入力ごとに一つのフィールドを作っていたので、畳み込みのある規則では列が渡らず、開いて押した瞬間に落ちる。落ちるページを出すのは、ページを出さないより悪い。だからフィールドの作り方を一つの関数にまとめ、列のほうは**行の表**にした(足す・消す、要素のフィールドはそのまま同じ部品)。行をゼロにすれば `empty` の答えが出る。これは説明でなく、**走らせて確かめてある**(tests/fold.rs が最小の DOM を置いて、生成されたページの本体をそのまま node で動かす)。リンクが運ぶのは入力だけで、列はアドレスに載せない。
**SQL だけは生成しない。** 一つの問い合わせには、行から行へ値を持ち越し、途中で打ち切る場所が無い。窓関数や再帰 CTE で似た形は書けるが、それは**生成された問い合わせの枝と、規則の行との 1:1**(§8.2)を捨てることであり、SQL を出している理由そのものと引き換えになる。だから `gen` は SQL だけを生成せず、七言語を書く。「まだ」ではなく、そういう決定である。登録簿の `folds` のフィールドがこの一行を持つ。
**(2026-09-17 の続き)例が書けるようになった。** 「例の一行はセルの並びなので、列を一つのセルに書けない」が理由でエラーにしていた。**ならばセルに書かなければよい。** `sequence <名前>(<別名>)` で並びを一度書き、例のセルにはその名前を書く:
```rule
sequence 近い一件(near)
| 行ゾーン | 閾値 | 行運賃 |
| 近畿圏 | 500円 | 800円 |
| 近畿圏 | 2000円 | 1500円 |
examples
| あて先 | 注文金額 | 運賃行 | -> 運賃 |
| 近畿圏 | 5000円 | 近い一件 | 800円 |
```
セルが「宣言済みの何かを名前で指す」のは、列挙の値やグループと同じ形である。並びは名前で何度でも使え、**行ゼロ本の `sequence` は要素ゼロ件の例**になる——手で書いた走査がいちばんよく落とす場合を、人が書いて残せるようになった。フィールドは `elements` のフィールドとそろっていなければならず、セルは値だけ(範囲や `-` は表の書き方で、ここは実際に渡す一件である)。例は他の例と同じく `check` で参照評価器に通り(E107)、ベクタにもそのまま入る。
**E025 の意味を差し替えた。** 「畳み込みのある規則には例を書けません」という条件はもう存在しない。番号に穴を空けるより、同じ番号で**いまある条件**——並びを歩く規則の例に、その並びの列が無い——を指すことにした。ここに書いておく。新しいのは E026(`sequence` の書き方)、E027(指した実例が無い)、W116(どの例も使っていない実例)。
**ついでに二つ直した。** `doc` が `elements` と `fold` を一切描いていなかった(承認者が読む面に、規則の半分が無かった)。それと、腕の中だけで読まれるフィールドが W111 で「どの表も使っていない」と言われていた——`take_unique 行運賃` は読んでいる。
**残っているもの。** いまのところ無し。
**捨てたもの**:二つ以上の列を歩く案(入れ子の走査になり、判定の列のオートマトンで説明できなくなる)。畳み込みを二段にする案(同じ理由)。`Val` に列の変種を足して先に評価器だけ入れる案(ワイヤ・fixtures・入力例・ベクタの全部が列を知ることになり、走らせる先が無いまま面だけ広がる)。腕を式で書けるようにする案(`verdict` ごとに一つの動作、という形を保つほうが、オートマトンとして読める)。
### 15.57 count は fold の糖衣にする(2026-09-17)
**決定**:`count`(`derive 会社名一致数 : count of 納入先 where 会社名一致` と `buckets ゼロ件 | 一件 | 複数件`)を**独立の機能としては作らない**。数える畳み込みは、要素を「該当/非該当」の判定に落とす表と、数える畳み込みと、バケット化の表に分解できる。つまり fold の特殊形であり、fold が生成まで入ったあとに、その上の糖衣として足す。
**いま何ができるか。** 件数を `number` の入力として受け、バケット化は**表に書く**(`0` / `1` / `>=2` の三行)。これで少なくとも、バケットの境界は完全性の検査に掛かる。さらに §15.55 の `constraint` があるので、「全条件一致数 ≤ 会社名一致数」のような入力どうしの関係も宣言でき、論理的にありえない組み合わせに行を要求されることもない。手で書く部分は「数えること」だけに縮む。
**理由**:先に `count` を入れると、同じことを二通りに書けるようになる。数える畳み込みが `fold` でも `count` でも書けて、片方だけが検査を持つ、という形になりやすい(§15.44 で「ワイヤが二つになる」案を捨てたのと同じ嫌い方)。順番は、畳み込みを最後まで入れてから、その言い換えとして足す。
### 15.58 count を入れる(数える畳み込み、2026-09-18)
**決定**:§15.57 の順番どおり、畳み込みが生成まで入ったので `count` を足す。ただし**糖衣ではない**。`fold` の腕は `next` / `stop` / `take_unique` / `take_first` / `keep_max` の五つで、**どれも値を貯めない**ので、いまの畳み込みの上に数え上げは書けなかった。形は宣言一つ。
```rule
count 一致数(hits) over 候補 where 照合結果 = 一致 range >=0 <=50
```
**`fold` が並びを一つの答えにするのに対し、`count` は並びを一つの数にして、そこから先をふつうの規則に戻す。** これが §15.57 の言っていた分解そのもので、**数を判定に変えるのがふつうの表なら、その境界も完全性の検査に掛かる**(`0` / `1` / `>=2` の穴と重なりが、いつもどおり止まる)。数えることと、数から決めることが、別々に検査される。
**`where` が指すのは要素ごとに決まる列**——要素のフィールドか、要素ごとの表が出した列——で、値が有限の集合(真偽か列挙)であること。列挙なら `= <値>`、真偽なら省ける(E029)。入力や導出は一件の呼び出しに一つしかないので、数えても 0 か 1 にしかならない。だからエラーにする。
**二相になる。** 規則に `elements` があるとき、**要素ごとに走る項目と、一度だけ走る項目**に分かれる。どちらかは宣言させず**導出する**——「要素しか持たない値を読む項目は、要素ごと」。表は列で決まり、導出と定義は式で決まる。だから分類の表は自動的に歩きの中、数を読む表は歩きの外になり、書く人は順番どおりに書くだけでよい。評価器・七言語の生成器・ベクタ生成は同じ判定(`types::element_scoped`)を見る。
**`range` を必須にした(E030)。** 二つの意味を持つ。数えた結果を列に使ったときに完全性の検査が見る全体集合(無いと「一致数 = −1 の行が無い」と言われる)と、**並びの長さの上限**。生成コードは上限より長い並びを入口で受け付けず、参照評価器も答えない。証明が置いた前提の外に、実行時に出ていかないためである。上限は宣言のうち**いちばん小さいもの**——数は並びの長さまで届きうるので。
**`fold` と `count` は排他(E031)。** どちらも同じ並びの終わり方で、`fold` は `stop` で打ち切れる。止まった歩きの数え上げが何を意味するかは決めていないので、決めていないことを黙って動かすよりエラーにする。
**ベクタは長さで覆う。** 畳み込みの義務が「判定の順序対」だったのに対し、数え上げの義務は**表がその数について引いた境界**なので、`count_pool` は「条件に当てはまる要素を n 件並べた」ケースを、セルに書いた最大値の一つ先まで作る。要素そのものは畳み込みと同じく候補を一件ずつ。行カバーと境界カバーがそのまま効く(新しい基準は増やしていない)。
**SQL には引き続き生成しない。** 一つの問い合わせで数えること自体はできるが、生成しないと決めたのは並びを取ること全体(§15.56)で、`gen` の条件を `fold` から `elements` に広げた。
**残っているもの。** `constraint` は入力どうしの関係しか書けないので、「全条件一致数 ≤ 会社名一致数」のように**数え上げどうしの関係**は宣言できない。起こりえない組み合わせの行を書かされる場面が残る。
### 15.59 列挙の出どころを `.proto` に結ぶ(2026-09-18)
**決定**:`import proto "<ファイル>" <列挙> -> <この規則の列挙>` を足す。読むのは `.proto` の `enum` ブロックだけで、メッセージの中にあっても読む。値の別名は接頭辞(`MemberTier` なら `MEMBER_TIER_`)を落として小文字にした綴りで突き合わせる。ゼロ値は名前が `_UNSPECIFIED` のときだけ「未設定」として落とす。集合がずれていれば **E032**、そろっていて行にも現れず `default` も付いていない値があれば **E033**。読み手は自前で書く(依存ゼロは §12 の決定で、ここでも効く)。パスは規則ファイルのある場所からたどる。ファイルを読むのは型検査の次の一段だけで、検査器そのものは「テキストを読んで診断を返す関数」のまま残す(playground の wasm では読めないと言う)。
**理由**:列挙に値を足すのは**ワイヤの上では互換な変更**で、契約を守る道具はそれを通す。通ったその変更が、既定の行のある表を黙って通り抜ける。新しい区分が、誰も額を決めていないまま既定の額を受け取る。proto の側からは見えない — 答えが proto に無いからである。
**まず測った。** 実装の前に、proto から転記した規則を二つ書いて、proto に値を一つ足した。**専用の行がある表**では W111 と E101 が出て exit 1、当てはまらない入力も足す行の形も出た。**既定の行がある表**では W111 だけで `ok`、**exit 0**。文面は事故そのものを正確に述べていて、止めない。現実に多いのは後者である。
**二段で止める理由**。E032 が落ちるのは **`.proto` を変えた側**で、E033 が落ちるのは「値は足したが表をまだ触っていない」ところである。E032 だけだと、いちばん安い直し方(規則の `enum` に一語足す)で緑になり、事故が一歩後ろにずれるだけになる。W111 と見た目の同じ状態をエラーにしているのは、**中身が同じ状態ではないから**である。自分で書いた値に行が無いのは書き忘れで、契約から来た値に行が無いのは、規則の外で起きた変更がまだ読まれていないということである。
**捨てたもの**:`.proto` を**生成する**向き。生成物に runtime が要らないのが看板で、サービスを出すとそことぶつかる。やりとりの形は `schema` と MCP が持っている。**proto から列挙を作る案**(`import std/都道府県` と同じ形で値を持ってくる)。表のセルが `gold` になり、業務の言葉で書く表でなくなる。proto は日本語を持っていないので、名前は人が書くしかない。だから作るのではなく**突き合わせる**。**ゼロ値をいつでも落とす案**。`STATUS_ACTIVE = 0` は珍しいが実在し、黙って落とすのは道具がやってはいけない側である。**`--deny-warnings` で W111 をエラーに上げるフラグ**。フラグは設定で、設定はいつか外される。出どころが宣言に書いてあるなら、厳しさもそこから出るべきである。
**台帳に一つ足りなかったもの**。E032 の最小の再現は二ファイルになる。台帳は「例は一本の `.rule`」を前提にしていたので、項目に**隣に置くファイル**を持たせ、テストがそれを一時ディレクトリに書いてからそこで走らせるようにした。`explain` はそのファイルも一緒に出す。例が腐らないという性質(§11)は、二ファイルになっても落とさない。
**副作用として引き受けたこと**:既存の規則を proto に結ぶと、値ごとに行か `default` かを一度は書くことになる。一度きりの手間で、そこに付いた `default` が「額を決めた人がいる」という印になる。
**実測**:`src/proto.rs` が 約 300 行、ほかは `parse`・`types`・`doc`・`codes`・`lib`・`main` に合わせて 100 行ほど。文書は文法・AGENTS・台帳の再生成。§15.21 の登録簿は生成対象の話なので、ここでは効かない — 効いたのは §11 の台帳のほうで、コードを二つ足すのに触る先を全部教えてくれたのはテストだった(README の件数、スキルの description の範囲まで落ちた)。
### 15.60 JSON Schema と OpenAPI からも取り込む(2026-09-18)
**決定**:`import jsonschema "<ファイル>" "<ポインタ>" -> <この規則の列挙>` を足す。読むのは JSON Schema の列挙で、OpenAPI の中のスキーマも同じものとして読む。文書には列挙がいくつも入っているので、名前ではなく **JSON ポインタ**で指す。ポインタはスキーマそのものでも、その `enum` の配列でもよく、先頭の `#` と `/` はどちらも省ける。値は**そのまま別名**にする。診断は §15.59 と同じ E013・E032・E033 で、新しいコードは足さない。読み手は `src/jsonschema.rs`、突き合わせは `src/enums.rs` に出して、`src/proto.rs` は形式の読み手だけにした。
**理由**:契約が `.proto` にあるとは限らない。HTTP の API なら型のある契約は OpenAPI にあり、そこにも `enum` がある。§15.59 が守ろうとしたのは「値の集合が規則の外にあり、規則が知らないうちに増える」という状況であって、proto という形式ではない。
**なぜ変換しないか**。`.proto` では接頭辞を落として小文字にする(`MEMBER_TIER_GOLD` が `gold`)。あれは `buf lint` が守らせている慣習があるから成り立つ変換で、スキーマにその慣習は無い。こちらで大文字小文字を畳んだり接頭辞を落としたりすると、**そろって見えるのに違う文字列を送る規則**ができる。だから値はそのまま別名として突き合わせ、名前にできない値(数や null の列挙)は名前を挙げてエラーにする。
**YAML を読まない。** OpenAPI はたいてい YAML で、ここが判断の分かれ目だった。手元のファイルで使われている書き方にだけ合わせた読み手は書ける。ただしそれは、次のファイルで黙って間違える読み手である。値の集合が黙って違うことは、この道具がいちばんやってはいけないことなので、読まないと名乗って JSON への直し方を言うほうを取った(§15.50 の ZIP64 と同じエラーの出し方)。**これは仮の判断で、実際に YAML しか無い相手が出てきたら考え直す。** そのときも、部分的に読む案より、JSON に直す手順を文書に書く案のほうが先である。
**捨てたもの**:形式ごとに新しい診断コードを足す案。E032 と E033 の意味は形式に依らない(集合がずれた、値がまだ決まっていない)ので、コードを増やすと台帳が形式の数だけ太る。`import schema` のように一語にまとめて拡張子で見分ける案。`rulec schema` という既存のコマンドと語がぶつかり、ファイル名から形式を当てるのは当たらないことがある。
**実測**:読み手 `src/jsonschema.rs` が約 120 行、`src/enums.rs` への切り出しが約 200 行(proto から移したもの)、テスト 9 本。**新しい診断コードはゼロ**で、文面は三つとも「二つある」を「三つある」に直しただけで済んだ。§15.59 で突き合わせの側を形式から切り離しておいたぶんが、そのまま効いた。
### 15.61 顧客に見せる描画(2026-09-19)
**決定**:`rulec doc` に `--audience customer` を足す。出力は markdown だけで、`--format html` とは併用できない。承認者向けの描画(§1.6)から、別名、宣言範囲、診断コード、「`rulec check` が確かめたこと」の箇条書きを省く。代わりに四つを足す。入力の説明(列挙は値を並べ、量は単位つきの範囲、真偽は「はい/いいえ」)、セルの言い換え(`-` は「どれでも」、`not:` は「以外」、日本語では比較を以下・以上・未満・より大きいに直す)、丸めの説明文(「端数は 10円単位で切り上げます」)、そして境目の例である。境目の例には §9.2 の境界両側ベクタをそのまま使う。同じ入力を境界の内側と外側に一つずつ動かし、ほかの入力を固定した二件を一行にまとめる(「三辺合計が 60cm のとき 運賃 1410円、61cm のとき 運賃 1710円」)。`--out` では `<alias>.customer.md` に書く。
**理由**:サポートの答えは、人が出すにせよモデルが出すにせよ、ヘルプセンターの記事を読んで作られる。記事を表から生成すれば、記事は表と同じだけ完全で、版がそろい、境目が文字で書かれる。承認者向けの描画は「検査器が知っていて表には出ない事実」を足すためのもので、読み手は表を承認する人である。それを顧客にそのまま見せると、別名や範囲、診断コードが邪魔になる。読み手が違えば、省くものも足すものも変わる。
**測ったこと**:ゆうパック運賃の境界ベクタ 18 件を、思考なしのモデルに四通りの文脈で答えさせた(`experiments/llm-verify/`、§15.62)。規則と一致した件数は、文脈なしで 0、日本郵便の運賃ページ本文で 12、`.rule` そのもので 14、この描画で 17 だった。外れは全部サイズ境界の外側(61cm、81cm など)で、一つ下のサイズの運賃を答えている。`<=80cm` の行を見て 81cm をその行に入れる誤りで、公式ページでも `.rule` でも同じ場所で起きる。ただし境目の例はこの 18 件を文字どおり含むので、描画の 17 件は割り引いて読む。全件での比較は §15.62 に書いた。
**捨てたもの**:顧客向けの HTML。承認者向けの HTML は生成した JavaScript で自分の件を計算するための画面だが、顧客向けは検索にかかる記事が目的なので markdown で足りる。`rulec article` という別コマンド。同じ描画器の読み手が違うだけで、別の道具ではない。表を文に書き下す案(「60cm 以下なら S60」と一行ずつ)。表のほうが読みやすく、文に直すと表と二重になる。境目の例を省いて表だけにする案。61cm が S80 だと読み取れない読み手のために作る描画なので、それを省けば承認者向けの描画から診断を消しただけになる。
### 15.62 verify の相手はモデルでもよい(2026-09-19)
**決定**:`rulec verify` のアダプタは旧実装のために作ったが、相手がモデルでも同じ規約で動く。`experiments/llm-verify/` に、規則のベクタをモデルに問う `ask.py`(答えを全部ファイルに残す)、そのファイルから `verify` に答える `adapter.py`、集計の `report.py`、全条件を回す `run_all.sh` を置いた。問いは一度で済み、比較は何度でもやり直せる。既定では一件を k 回引き、全部同じなら答え、割れたら `err` を返す。割れた件は `verify` の「答えられなかった」に数えられ、分母から外れて別に表示されるので、揺れの大きさがそのまま件数になる。
**理由**:サポートのボットが規約を読んで金額を答える構図を、この道具の語彙で測れる。外れが当たった行ごとにまとまって出るので、「境界の外側だけ外す」がそのまま図になる。
**直したもの**:この実験で `verify` の見出しが「影響 195 件 (1083%)」と出た。答えられなかった記録を分母からは外していたのに、分子には入れていた。分子からも外した(`src/report.rs` の `impact`)。
**コーパスに足したもの**:Claude API の料金表(`Claude利用料`。モデルと種類ごとの百万トークンあたりの単価に、バッチと米国内推論の倍率とトークン数を掛け、セント単位に四捨五入する。enum にモデルを一つ足すと E101 が「| Opus6 | 入力 | 10USD |」の形で行を求める)と、τ²-bench の航空規約から三本(`預け荷物料金`、`予約取消可否`、`補償証明書`)。取消の規約は乗客に理由を三択で訊けと言い、別の節では保険が効くのは健康か天候の理由だけだと言う。表にするには理由の列に健康と天候が要り、航空会社が取り消したかどうかは乗客の申告ではなく便の状態から取る。転記で決めたのはこの二つで、規則の description に書いた。四本を足した一致検査で生成の欠陥が二つ見つかった。Go では集合のセルが行の唯一の条件のとき `if (a || b) {` と括弧が残り、gofmt が直したがる(`unparen` を通した)。Ruby では `[A, B].include?(x)` を steep が `Array[A | B]` と型付けして `x` を通さない(値ごとの `==` を並べる形にした)。どちらも既存のコーパスには無い形だった。
**τ²-bench の環境**:`experiments/tau2-airline/` に、航空の道具区画に三本の生成 Python を足した領域 `airline_rulec` を置いた(ツールは `baggage_fee`・`cancellation_verdict`・`compensation_amount` で、答えと当たった行を返す。規約には道具の名前を挙げる一節を足し、データベースと課題は元のまま)。同じ課題を道具の有無だけ変えて走らせれば、判断を表に落とした効果が pass^k で出る。`compare.sh` がその二回を回し、`summarize.py` が並べる。手元の Ollama でも回るが、推論モデルは思考を切る必要がある(答えを全部思考に入れて本文を空にし、τ²-bench がそれを拒む)。
**測ったこと(qwen3.6:35b-a3b、思考なし、取消と補償が絡む 15 課題、一回ずつ)**:道具なしの平均報酬 0.800、道具ありで 0.867。道具ありで直ったのは 43 と 47 で、どちらも道具なしでは取り消してはいけない予約を取り消していた。47 は「保険が効くのは健康か天候の理由だけ」の件で、エージェントは `cancellation_verdict` を呼んで `refused` を受け取り、取り消さなかった。逆に 41 は道具ありで落ちたが、道具は呼ばれておらず、予約の照会を繰り返して手数の上限に当たった揺れである(39 はどちらでも同じ形で落ちる)。呼ばれた道具は `cancellation_verdict` だけで 6 課題、預け荷物と補償の道具はこの 15 課題では出番が無かった。一回ずつでは揺れと効果を分けられない。三回ずつは同じ `compare.sh` で二時間半ほどかかるので、まだ回していない。
**全件の比較(手元のモデル)**:`run_all.sh` が、二つの規則(ゆうパック運賃 209 件、会員特典 90 件)を、文脈なし・公式ページ・`.rule`・承認者向け描画・顧客向け描画の五条件で、モデルごとに k=3 で問う。API の鍵が無かったので、Ollama で動く二つのモデル(qwen3.6:35b-a3b と Llama-3.1-Swallow-8B、どちらも思考なし、温度 0.7)で全件を回した。顧客向け描画は境目の例と examples を文字どおり含むので、比べるのは残りの 187 件(pairwise と行ごとの狙い撃ち)の一致率にした。
| 文脈 | ゆうパック qwen | ゆうパック Swallow | 会員特典 qwen | 会員特典 Swallow |
|---|---|---|---|---|
| なし | 0.5% | 0.0% | — | — |
| 公式ページ | 67.9% | 32.1% | — | — |
| `.rule` | 71.1% | 44.4% | 22.5% | 6.2% |
| 承認者向け描画 | 67.4% | 27.8% | 25.0% | 12.5% |
| 顧客向け描画 | 79.1% | 58.8% | 38.8% | 22.5% |
読み取れることは三つある。平らな表でも三割は外し、外れは境界の外側と地域の行に集まる(東京都を近距離圏の行で引く、81cm を 80 サイズで引く)。表が二段重なって丸めが入る会員特典は、どの文脈でも大きく崩れ、`.rule` を渡しても二割台にとどまる。これは「モデルに計算させない」の根拠になる。顧客向け描画は引用のない件でも一番よく、承認者向け描画より十ポイント以上高い。承認者向けの描画は範囲や診断の事実が邪魔をして、公式ページより低い。三回引いて答えが割れた件(揺れ)は、qwen の `.rule` で 209 件中 36 件、Swallow で 68 件だった。上位のモデルでの比較は鍵が要り、同じ `run_all.sh` で回せる。
### 15.63 生成した Rust は Wasm の中でも同じ答えを返す(2026-09-19)
**決定**:`rulec test` に Wasm の段を足す。Rust の runner を `--target wasm32-wasip1` でコンパイルし、wasmtime でベクタを流して同じ期待値と比べる(結果の `via` は `wasm`)。wasmtime か wasm32-wasip1 の標準ライブラリが無ければ飛ばし、飛ばしたことを報告する。言語を飛ばしたのとは別に数え、まとめの「n 言語中 m 言語を飛ばしました」には入れない。`src/backend.rs` の表に `wasm` のフィールドを足し、いまは Rust だけが持つ。
**理由**:Shopify Functions、Extism、Cloudflare Workers のような実行環境は、依存のない小さな Wasm を求める。生成する Rust は依存がゼロで、外部とのやり取りは標準入出力だけなので、一行も変えずにコンパイルできる。測ってみると、Claude利用料の runner は 0.23 秒でコンパイルでき、85 件のベクタの出力がネイティブと一字一句同じだった。「一致検査に乗らない言語は入れない」(§15.46)を実行環境にも当てはめた形で、環境側の設定(モジュールの大きさ、`panic` の扱い)は環境側の crate の仕事として残す。
**捨てたもの**:九つ目の生成対象として Wasm component(WIT)を出す案。いまの Rust がそのまま WASI で動くので、増やす物が無い。component model が必要な相手が出てきたら、そのとき考える。**この判断は同じ日に §15.64 で覆した。**Shopify の関数の形そのものを生成する案。カートの GraphQL を平らな入力に直す層は呼び出し側の仕事で(§15-6)、そこを生成するとこの道具が Shopify の入力の形を追いかけることになる。層の書き方は docs/backends.md に置いた。
### 15.64 Wasm を九つ目の生成対象にする(2026-09-19)
**決定**:`wasm/` を生成対象に足す。中身は五つ。Rust と同じモジュール `<alias>.rs`、それを canonical ABI の `call: func(input: string) -> string` で包む crate root `<alias>_wasm.rs`、component model 向けの `<alias>.wit`、`rulec test` が回す Node の runner `<alias>_runner.mjs`、丸めヘルパの単体検査(`_round.rs` と `_round_test.mjs`)。`rustc` だけで組める(`-C opt-level=s -C lto -C panic=abort -C strip=symbols --target wasm32-unknown-unknown --crate-type cdylib`。送料で 40 KB。`-O` だけだと 1.4 MB で、lto が効いて 110 KB、panic=abort と strip で 20 KB 台になる)。入出力は JSON のワイヤで、答えは各言語の `_record` が書く行と同じ、契約の外は `{"error":…}`。列挙に無い値も panic ではなく error で返す。`.wit` は同じ関数を world の export として書くだけなので、`wasm-tools component embed` と `component new` でモジュールに手を入れずに component になり、wasmtime の `--invoke` で呼べる。`rulec api` に `wasm` の項、`rulec test` に `(Wasm)` の行が出る。node か wasm32-unknown-unknown の標準ライブラリが無ければ、言語が無いときと同じ形で飛ばして言う。登録簿には `ready` のフィールドを足し、`tool` 一つでは言い尽くせない前提をそこに書く。
**理由**:§15.63 では「Rust がそのまま WASI で動くので増やす物が無い」として component を退けた。だが WASI の runner は標準入出力を読む一本のプログラムで、関数を呼びたい相手(JavaScript のホスト、Extism、component が要る場所)には合わない。関数として呼べるモジュールと、その interface を書いた `.wit` が無いと、Wasm への対応は「テストの一段」で止まり、機能として名前が付かない。八言語と並ぶ生成対象にすることで、`gen`・`test`・`api`・文書のすべてに載る。
**WIT を `string -> string` にした理由**:入力を record、列挙を enum に対応させた型付きの interface のほうが読みやすい。だがそれには canonical ABI の record・list・string の配置を生成コードが自前で実装する必要があり、wit-bindgen を依存に入れずにやると数百行の glue になる。JSON はこの道具のワイヤそのもの(fixtures・adapter・MCP・vectors がみな同じ形)なので、component も同じワイヤで話す。型付きの interface が要る相手が出てきたら、`.wit` を足す形で広げられる。
**捨てたもの**:`wasm32-wasip1` を対象にする案。WASI の import が付き、ブラウザや Extism では動かない。WASI の段(§15.63)はそのまま残す。Shopify Functions のように標準入出力で話す置き場の証拠になる。**この最後の前提は §15.73 で崩れた。Shopify は標準入出力をやめている。**cargo のプロジェクトを生成する案。`rustc` 一発で組めるのが Rust 側と同じ約束で、cargo が要るなら「依存ゼロ」が崩れる。
### 15.65 名前を出す出力列が、下流の行を殺していた(2026-09-19)
**きっかけ**:準用(「第X条の規定は…について準用する。この場合において『A』とあるのは『B』と読み替える」)を、呼び出しなしで書けるかを試した。読替えを上流の表にして、出力セルに入力の名前を書く形である。下流の表の**全行**が E102 で落ちた。同じ形でセルをリテラルに替えると通る。
**原因**:`region.rs` の `upstream_blocked` が、出力セルの裸の語を無条件に列挙の値として読んでいた。パーサはそこに二つの読みを入れている(列挙の値か、宣言された名前か。§3.2)。`勤続年数` が `Val::Enum("勤続年数")` になり、下流の `<10` と照合されて外れ、「この値を出す行が上流に無い」と判定される。関数自身が doc コメントで「E102 is never handed a row that is alive」と不変条件を書いていて、それが破れていた。
**評価器は同じ二義を正しく解いていた。** `eval.rs` はまず束縛を引き、無ければ列挙の語として読む。`types.rs` も型で分けていて、数値の列では名前を変数として読んで列の値域を広げ(§15.34)、「上流が出す値の集合」は列挙と真偽のときだけ集めている(§5.3)。**`region.rs` だけが型で分けていなかった。**
**決定**:`c.syms` に載っている名前なら、静的に値が決まらないので、すぐ下の安全側の枝へ落とす。列挙の値は `syms` に無いので、そちらの相関検査は動いたままである。一箇所に条件を一つ足すだけで済んだ。
**害の形は §15.25 と同じ**である。E102 は誤りなので exit 1 で、正しい規則が拒まれる。しかも文面が「上流の表がこの行の書いている値を出さない」と言うので、読替えを書いた人は上流の表を疑いに行く。**正しく書いた人が、存在しない欠陥を探しに行く。**
**実測**:コーパス 24 本の `check` と `coverage` の出力は、直しの前後で一字も変わらない。`cargo test` は 332 通る(新しいテスト 1 本を含む。`tests/region.rs` に置き、直しを外すと落ちることを確かめた)。読替えの規則を `gen` して `test` にかけると、九つの生成対象が 19 ベクタで参照評価器と一致した。
**これで変わること**:**同一ファイルの中の準用は、呼び出しなしで書ける。** §5.4 が捨てた規則間の呼び出しが要るのは、準用元が別ファイルの独立した承認対象であるときだけに絞られた。
### 15.162 ritsu の土台に移る(2026-10-03)
**きっかけ**:rulec は ritsu(七つの小さな言語を一つにまとめる処理系)に取り込まれた。ritsu は、言語ごとに書いていた同じ役目のコード(SHA-256、JSON、法令のコピー、生成物が名前に使えない語の表、テストの共通の部分)を、土台のクレート(ritsu-base、ritsu-emit、ritsu-testkit)に一つにする。rulec は「合うところだけ」移す(ritsu の PLAN の C.11)。診断(`src/diag.rs`)、コマンドの表(`src/main.rs`)、`tr!` とプロセスで一つの言語(`src/i18n.rs`)は rulec のまま残した。
**決定**:
- **SHA-256** は ritsu-base の `sha256` を使う。`src/sha256.rs` は消し、`rulec::sha256` はそれを指す。値は同じである(FIPS 180-4 の既知の値で確かめてある)。
- **JSON** は ritsu-base の `json` を使う。もともとこのファイル(`src/json.rs`)から作ったもので、読み方(指数を読まない、小数を書いた桁のまま持つ、同じキーが二度あれば止める、入れ子は 256 段まで)は同じである。rulec に残したのは、読めないときの rulec の文(土台は何が悪いかを種類で返し、rulec が自分の文を選ぶ。位置も前と同じ数)、値の種類の名前(「整数」「a fraction」)、オブジェクトをキーの順に並べて読み書きすること(土台のオブジェクトは書いた順を保つ。rulec は前と同じく並べ替える)、`--format json` を組み立てる `Obj` である。
- **法令のコピー**は ritsu-base の `sources` を使う。引用から要素の名前を作ること(`第143条第2項` → `MainProvision-Article_143-Paragraph_2`)、コピーの場所、コピーの本文、固定の行の書き換え、curl と e-Gov と eCFR と GitHub への問い合わせ、base64 である。rulec に残したのは、何をどこから引いているか、コピーと固定の検査(E037〜E039、W119)、コピーと行の突き合わせ(E116、E119、W120)、`source fetch | pin | outdated` そのものである。
- **生成物が名前に使えない語の表**(12 の出力先の予約語、トップレベルの名前、標準ライブラリのモジュール)は、ritsu-emit の `copies` から読む。表の中身は前と同じである。標準の表(Rust の `Self`、Go の `complex64` と `complex128` を含む)にそろえるかは、ritsu の段階 E で一つの生成パッケージを作るときに決める。
- **テストの SKIP の行**は `SKIP: rulec: <理由>` にした(前は `注意:` と `skip:` の二通り)。ritsu-testkit の `skip` が出し、`RITSU_SKIP_LOG` にも書く。外のツールが要るテストは、先に `RITSU_TEST_LEVEL` を見る。`fast` なら、python3・node・go・swiftc・PostgreSQL・Lean などが要るテストは、段で外したと言って通る。理由の文は前のまま(日本語)である。
**変わったこと**:コーパスの全部の規則について、`check`(英語・日本語・JSON)、`certificate`、`api`、`schema`、`graph`、`vectors`、`coverage`、`adapter`、`fmt --check`、`doc`(Markdown と HTML、英語と日本語)、`gen` の全部の出力先と、変異の全部の `check`、壊れた JSON Lines を渡した `fixtures lint` の出力を、移す前と後で比べ、一字も違わなかった(1,227 回)。テストと golden と証明書も変わらない。違うのは、どの規則も踏まない次の場合だけである。
- 法令の条の番号を、漢数字で百と千の位まで読む(`第百四十三条`)。前は九十九までで、それより大きい漢数字の番号は「引用箇所の書き方が読めません」(E037)だった。番号 0(`第0条`)は、どの法令にも無いので読まない(前は `Article_0` としてコピーを探した)。
- JSON として読まないものが三つ増えた。文字列の中にそのまま書いた制御文字、小数点のあとに数字の無い数(`1.`)、上位サロゲートのあとの下位サロゲートでない `\u`(前は、デバッグビルドでは止まり、リリースビルドでは別の文字になった)。どれも RFC 8259 の JSON ではない。文は rulec の言い方で足した。
- JSON の文字列の中の後退(U+0008)と改ページ(U+000C)を、`\u0008` と `\u000c` ではなく `\b` と `\f` と書く。serde_json と同じ書き方で、ritsu のどの言語も同じになる。読む側にとっては同じ文字である。
- `source fetch` と `source outdated` の問い合わせ:`file://` の URL は一度だけ試す(前は三度。待っても変わらない)。curl が無ければ、三度試さずにすぐそう言う。e-Gov の `law_full_text` は標準の base64(パディングあり)として読む(前は URL 用の文字やパディングの無い形も読んだ。e-Gov は標準の形で返す)。改正の施行日と eCFR の改正日は `YYYY-MM-DD` の形のものだけを読む。失敗を言う文は土台のものになった(「e-Gov の答えが JSON として読めません」は「<URL> のレスポンスが JSON として読めません」、「三度試しました」は「3 回試しました」など)。
**確かめたこと**:rulec のテストは、全部が前と同じ件数で通った(727 件、ignored 1)。`RITSU_TEST_LEVEL=fast` で、外のツールを呼ぶと記録を残して失敗するコマンドを PATH の先頭に置いて回し、どのテストもツールを呼ばずに通ることも確かめた。e-Gov と eCFR への問い合わせは、移す前と後のバイナリで `source outdated` を一度ずつ走らせて比べた(ritsu の PLAN の C.11)。
### 15.163 立てたプロセスは、どこから戻っても止める(2026-10-03)
**きっかけ**:`rulec test` は、生成した MCP サーバを HTTP でも立て(`--http 127.0.0.1:0`)、ベクタを一件ずつ `tools/call` で送って答えを確かめる(§15.51)。答えが食い違うと、`?` や `return Err(…)` でそのまま戻っていた。Rust の `std::process::Child` は、値を捨てても子プロセスを止めない。標準入出力のサーバは、標準入力が閉じれば自分で終わるので残らない。HTTP のサーバは、止められるまで待ち受け続ける。rulec が終わったあとも、親のいないプロセスとして残った。`tests/fold.rs` の「受け付けない入力は生成コードでもエラーになる」は、わざと壊した生成コードで `rulec test` を落とすので、テストを全部回すたびに一つ増えた。開発に使っている機械には、9 月 14 日からの分が 515 個(約 1.2 GB)たまっていた。利用者が、確かめの落ちるプロジェクトで `rulec test` を走らせても、同じことが起きる。
**決定**:
- **子プロセスを包む型を置いた(`src/child.rs` の `Owned`)。** 値が捨てられるときに子がまだ終わっていなければ、止めて(SIGKILL)、終わるのを待つ。終わっていれば何もしない。パイプ、`wait`、`try_wait`、`id` は `Child` のまま使える。`?` で戻っても、`return Err(…)` で戻っても、包みが捨てられるところで止まる。
- **rulec が立てて、動いているあいだにやりとりするプロセスは、全部これで包む。** 四か所ある。`rulec test` の MCP サーバ(`src/runtest.rs`)、`verify` のアダプタ(`src/verify.rs`)、`source fetch` の抽出器(`src/extract.rs`、§15.82)、`rulec mcp` が呼び出しごとに立てる rulec(`src/mcp.rs`)である。終わりまで走らせて出力を受け取るだけのコマンド(`output` と `status`)は、もともと終わるのを待つので包まない。
- **ほかの三か所にも、同じ戻り方があった。**
- アダプタ:ハンドシェイクを受け付けなかったとき、JSON でない行を返したとき、違う件の `id` で答えたときなどに、止めずに戻っていた。標準入力が閉じれば終わるアダプタなら残らないが、終わらないアダプタは rulec のあとも走り続けた。
- 抽出器:UTF-8 でない行を読むと、止めず、待たずに戻っていた。
- `rulec mcp`:上限を過ぎた呼び出しは、前からグループごと止めている(§15.156)。止めずに戻るのは、終わったかを聞く `try_wait` が失敗したときだけだが、ここも包みに入れた。グループごと止める処理は包みに移した(`spawn_group` と `stop`)。出力を書かせる二つの一時ファイルは、rulec を立てられなかったときにも消す。
- **普通に終わるときの振る舞いは変えない。** アダプタと抽出器は、今までどおり、標準入力を閉じて(抽出器は最後まで読んで)終わるのを待つ。包みが止めるのは、終わっていないまま戻るときだけである。コマンド、フラグ、終了コード、診断、`--format json`、生成物は変わらない。
**捨てたもの**:
- **戻るところごとに `kill` と `wait` を書くこと。** `via_mcp` だけで、子を立てたあとに戻るところが 26 か所ある。一つ書き忘れれば同じことが起き、書き忘れたかどうかはテストでは見えにくい。
- **子をいつも自分のプロセスグループで立てること。** 端末の Ctrl-C はフォアグラウンドのグループにしか届かない。別のグループの子には届かず、rulec だけが止まって子が残る。`rulec mcp` が呼び出しを自分のグループで立てるのは、上限を過ぎた呼び出しを、それが立てたものごと rulec 自身が止めるためである(§15.156)。
- **止める前に少し待つこと。** HTTP のサーバは、待っても自分では終わらない。
**テストが後に残すもの**:同じときに、テストのほうも直した。OS の一時ディレクトリに作っていたディレクトリ(`rulec-<用途>-<プロセス ID>`。消さないテストと、落ちたときに消さないテストがあり、9 月 18 日からの分が 14,199 個たまっていた)を、ritsu-testkit の `TempDir` に替えた。テストが終われば、落ちても消える。ritsu-testkit には、落ちたテストのディレクトリを残す仕組みが無いので、ほかのクレートと同じく残さない。名前は `ritsu-test-rulec-<プロセス ID>-<番号>-<用途>` になり、パスはシンボリックリンクをたどった形(macOS では `/private/var/…`)になった。`gen --check` が書き込まないことを見るテストは、まだ無いディレクトリを渡す形を保った。`tests/tool.rs` が HTTP で立てる MCP サーバも包みで立て、アサーションが落ちたときにも止まるようにした。続けて(2026-10-04)、テストが `rulec test` と swiftc を走らせるときは、子の TMPDIR をそのテストの一時ディレクトリの下に向けるようにした(ritsu-testkit の `tmp::tmpdir_in`)。swiftc は `--version` に答えると、ほとんど毎回、空の `TemporaryDirectory.*` を TMPDIR に残し、`rulec test` はツールチェーンの全部に版を聞くからである。rulec のテストを一度回すと 51 個残っていた。`rulec test` が swiftc に渡す環境は変えていない。
**確かめたこと**:テストを三本足した。`tests/child.rs` の二本は、包みそのものを見る。途中で戻る関数の中で立てた `sleep 30` が、戻ったときにはもう無いこと(`kill -0` で見つからない。止めても待たなければ、終わったプロセスとして見つかる)と、自分のグループで立てた `sh` が、それが立てた `sleep` ごと止まることである。包みの `Drop` を空にすると、二本とも落ちる。`tests/fold.rs` の一本は、生成した Python を壊して `rulec test` を途中で落とし、rulec を自分のプロセスグループで立てて、終わったあとにそのグループに何も残っていないことを見る。テストの一時ディレクトリを片づける前に見て、残っていれば止めてから落ちる。`src/runtest.rs` の直しを戻すと、`python3 -B freight_mcp.py --http 127.0.0.1:0` が残って落ちる。直す前のコードでは、`tests/fold.rs` の「受け付けない入力は生成コードでもエラーになる」を一度回すと、親のいないそのサーバが一つ残った。直したあとは、rulec のテストを全部(730 件が通り、ignored 1、SKIP 0)回した前と後で、親のいない MCP のサーバは 0 個と 0 個、OS の一時ディレクトリの `rulec-*` は 0 個と 0 個だった(ritsu の PLAN の 7.5 の後ろ)。
### 15.164 単位の表と有理数を、ritsu の単位の型に移す(2026-10-04)
**きっかけ**:ritsu では、rulec、dandori、chobo が同じ単位を扱う。単位の表(通貨、量の単位とその係数、℉ のずれ)と有理数は、rulec の `src/types.rs` と `src/num.rs` にあった。dandori は単位を文字列のまま比べ(`money[JPY, incl_tax]` と `money[円, incl_tax]` を別の単位としてエラーにする)、率の刻みを JSON Schema の説明の文から読んでいた。表を一つにして、どの言語も同じ表を読むようにする(ritsu の DESIGN 5 章)。
**決定**:
- 有理数 `Rat` と表(`CURRENCIES`、`money_unit`、`unit_info`、`unit_offset`)を、ritsu の単位の型のクレートに移した(`ritsu_units::Rat`、`ritsu_units::table`)。rulec の `num::Rat` はそれを指し、`types.rs` の `unit_info` と `unit_offset` は表を引くだけになった。表の中身は一つも変えていない。
- 丸めの五つの仕方(`RoundMode`)と `round_to` は rulec に残した。§7.3 が決める、この言語の意味だからである。`round_to` は、rulec が `Rat` に足すトレイト `RoundTo` のメソッドになった。
- 型 `Ty` は、書いたとおりの綴りを持ち続ける(`money[JPY, incl_tax]` の `JPY` も)。証明書の `types`、診断、生成物は、書いたとおりに出すからである。単位の意味(次元、係数、ずれ)は、どれも表から引く。型を ritsu の単位の型にするのは `Ty::unit`(率には、受け渡す整数が数える刻みを添える)で、ritsu の口(§15.167)はこれで入力と出力の単位を渡す。表に無い綴りには単位が無い(None)。
- 税の語を `incl_tax` と `excl_tax` のほかに書いても(`money[円, foo]`)、rulec はいまも黙って通す。この決定では変えなかった(§15.169 で E103 にした)。
**確かめたこと**:コーパスの 50 本、変異の 109 本、ほかの 16 本の規則について、`check`(英語、日本語、JSON)、`certificate`、`api`、`schema`、`graph` を、コーパスの 50 本についてはさらに `fmt --check`、`vectors`、`coverage`(テキストと JSON)、`doc`(Markdown と HTML、英語と日本語、顧客向け)、`gen`(全部の出力先)、`adapter` を、移す前と後のバイナリで出した。1,782 回とも一字も違わなかった。テストを二本足した(`tests/units.rs`)。単位を挙げる文(`types::units`)が表のすべての綴りを挙げること、コーパスのどの数の型も表で書けること(206 の数)である。
### 15.165 言語をスレッドごとに持つ(2026-10-04)
**きっかけ**:rulec の `tr!` は、プロセスで一つの言語(`i18n` の `AtomicU8`)を読む。ritsu では、dandori と yuen が rulec を同じプロセスの中でライブラリとして呼び、ほかのテストと並んで、違う言語で呼ぶ。dandori は、英語のページにも日本語のページにも rulec のページを埋め込む。プロセスで一つの言語のままでは、片方の言語がもう片方に混ざる。
**決定**:`i18n::with(言語, || …)` を足した。その呼び出しの中で作る文は、そのスレッドではその言語になる。抜ければ(パニックで抜けても)、そのスレッドは前の言語に戻る。スレッドの言語はプロセスの言語に勝つ。CLI はこれまでどおり `set` でプロセスの言語を決めるので、振る舞いは変わらない。rulec は自分でスレッドを立てないので、`with` の中で作る文は、どれもそのスレッドで作られる。2,800 か所の `tr!` は書き換えていない(すべての文を二つの言語の組(ritsu の `Text`)にすることは、ritsu の DESIGN 15 章に残した)。ritsu のほかの言語が読む `RITSU_LANG` は、rulec はまだ読まない。CLI の振る舞いを変えない決定だからである(ritsu の PLAN 7.6。§15.168 で読むようにした)。
**確かめたこと**:`tests/lang.rs` に一本足した。`送料.rule` のページを、Markdown と HTML、英語と日本語の四つのスレッドで同時に四回ずつ描き、どれも `rulec doc --lang …` が出すものと一字も同じであること、`with` を抜けたスレッドの言語が元に戻ることを見る。スレッドの言語を読まないようにすると、このテストは落ちる。§15.164 の 1,782 回の出力も、替える前と一字も違わなかった。
### 15.166 契約の .proto を ritsu の読み手で読み、読めないファイルは途中まで読まない(2026-10-04)
**きっかけ**:rulec は契約の `.proto` を自分の読み手(`src/proto.rs`、1,358 行)で読み、dandori と sakai も、それぞれ自分の読み手を持っていた。ritsu では読み手を一つにした(`ritsu_proto`、ritsu の DESIGN 4.10)。同じ `.proto` を、rulec の契約の突き合わせ、dandori の `connect` と `implements`、sakai の境界の検査が、同じに読むためである。rulec の読み手と ritsu の読み手の違いは二つだった。rulec の読み手は、行の頭にしか `package` を見つけない(`syntax = "proto3"; package a.v1;` と一行に書くと何も返さない)。そして、読めない文があると、そこを飛ばして、読めたところまでを返す。
**決定**:
- 読むのは ritsu の読み手にした。`proto::read(パス, 中身)` は、ritsu の読み手が読んだものから、規則が契約に尋ねるもの(package、import、列挙とその値、メッセージと、`shape` のパスがたどるフィールド、Protovalidate の規則)を取って返す(`proto::File`)。列挙の値から別名を作ること(`Enum::named` など)、`upper_snake`、`int_bounds` は rulec に残した。Protovalidate の規則の型(`Rules` など)は、ritsu の読み手のものをそのまま使う。
- **読めないファイルは、途中まで読まない。** `import proto` と `shape … = proto` の指すファイルが `.proto` として読めなければ、E013(「`order.proto` を読めません」)で止め、どこで何が読めないかを注に書く(「`.proto` として読めません(14 行目の 21 文字目): `}` が要るところにファイルの終わりがあります」)。前は、最後の `}` が欠けたファイルでも、そこまでに読めた値で規則と突き合わせて通していた。値の行の `=` が抜けていれば、その行を飛ばして、契約からその値が消えたと E032 で言っていた。読めない契約に規則を合わせても、その契約から組んだサービスとは話せない。直す先はファイルのほうだと言うのが正しい。
- `shape` の契約が読めないときの E013 の題は「`…` を読めません」にした。前は、読めた中に指すメッセージが無いときと同じ「`…` の中に `…` はありません」だった。JSON Schema が JSON として読めないときの題は、前のままにした。
- 生成(`gen` の Connect の部分)は、`check` を通った規則についてだけ走り、契約そのものは `check` が読み切っている。契約が import する先のファイルが読めないときは、そこから何も取らない(buf がそのファイルの名前を出して止まる)。
- 一行に書いた `package` も読むようになった。コーパスにも変異にもテストにも、そう書いた契約は無く、出力は変わらない。
**確かめたこと**:§15.164 と同じ 1,782 回の出力が、替える前と一字も違わなかった(コーパスと変異の契約の突き合わせを含む)。読み手の単体テスト 14 本は、ritsu の読み手で読む形にしてそのまま通る。テストを二本足した。`tests/proto.rs` の「読めない契約は途中まで読まずに場所を言う」(最後の `}` を落とした契約は前は通り、値の行の `=` を落とした契約は前は E032 になった)と、`tests/projection.rs` の「読めない proto の契約は読めないと言う」である。ritsu の読み手のテスト(`ritsu-proto` の `tests/readers.rs`)は、rulec の古い読み手と生のまま比べるのをやめ、rulec が取るものを書いた golden と比べる形だけを残した。
### 15.167 ritsu の口から規則を渡す(2026-10-04)
**きっかけ**:dandori は、規則ごとに rulec を三回(`schema`、`certificate`、`api`)子プロセスで走らせ、JSON を読んで規則の事実を組み立てている。率の刻みは JSON Schema の説明の文から読む。ritsu では、規則の事実を型の付いたまま渡す口を置いた(ritsu の DESIGN 3.2、`ritsu_ports::Rules`)。その口に rulec が答える。
**決定**:
- `src/ports.rs` の `Engine` が口に答える。`facts` は規則の事実(入力と出力の名前と別名、型と単位と範囲、率の刻み、列挙、ステートマシン、前提、Connect のサービスの形、生成したコードの TypeScript・Python・Go での呼び方)、`doc` は `rulec doc` のページ(言語は §15.165 の `with` で選び、ファイルの名前は呼ぶ側が渡す)、`eval` は参照評価器の答え(生成したコードが入口で受け付けない入力は、受け付けない)である。読んだ規則はパスと中身のハッシュで覚えておき、同じ規則を何度尋ねられても、検査は一度で済ませる。
- 事実は、`api`、`certificate`、`schema` が JSON を書くのと同じ関数から取る。列挙の一覧(`codegen` の `enum_list`)は、`api` の JSON もそこから書くようにした。ステートマシンの遷移は、証明書の `machine` と同じ決まりで表の行から読み(`cert::machine_facts`)、読めないときは事実を返さずに理由を言う。
- 前提の問い(`preconditions_hold`)は、二つの入力の関係(`constraint`)を、範囲の角で決める。範囲は箱なので、いちばん厳しい角で成り立てばどの組でも成り立ち、成り立たなければその角が例になる。並びの合計と長さの上限は、問いが並びの長さの範囲を持たないので決めず、決められない理由を言う。入力を日付の集合に限って表を確かめる問い(`checked_over`)には、まだ答えない(決められない、と言う)。
- 規則のファイルが持つもの(`Items`)は、入力、出力、列挙とその値、表、節、`define`、`derive`、ステートマシン、出典である。それぞれの定義の文は、そのものの行を `rulec fmt` が書く形にしたもの(表なら見出しから最後の行まで)で、表を揃え直しても変わらない。ファイルが参照する外のもの(`References`)は、`import proto` と `import jsonschema` の先、`shape` の契約、`apply` の規則、`source … file` のコピーである。
- CLI の振る舞いは変わらない。`doc.rs` には、ページの頭に書くファイルの名前を、読む場所と別に渡す `render_named` を足した(`render` はそれを同じ名前で呼ぶ)。
**確かめたこと**:`tests/ports.rs` の六本。コーパスの 50 本の全部で、口の事実が `api`、`certificate`、`schema` の JSON を読んだものと同じこと(率は、型の刻みが説明の文の刻みと同じこと)。50 本の 4,894 のベクタのどれにも、口の評価が `rulec vectors` と同じ答えを返すこと。口が描くページが `rulec doc` と一字も同じこと(三本の規則、二つの言語、Markdown と HTML)。前提の問い。検査を通らない規則には、事実の代わりに検査の診断が両方の言語で返ること。ファイルが持つものと参照するもの。§15.164 の 1,782 回の出力も一字も違わなかった。
### 15.168 ritsu の言語の変数 `RITSU_LANG` も読む(2026-10-04)
**きっかけ**:ritsu のほかの六つの言語は、`--lang`、自分の変数(`KOYOMI_LANG` など)、`RITSU_LANG`、英語の順に言語を選ぶ(ritsu の DESIGN 4.1)。`RITSU_LANG` を一つ置けば、どの言語も同じ言語で出す。rulec だけが `RITSU_LANG` を読まず、§15.165 では CLI の振る舞いを変えないために、そのまま残していた。
**決定**:言語を `--lang`、`RULEC_LANG`、`RITSU_LANG`、英語の順に選ぶ。変数の値がどの言語でもないときは、その変数は無いものとして次を見る。ほかの六つと同じく、ritsu-base の `Lang::pick` で読む(`i18n::from_env`)。システムのロケールは、これまでどおり見ない(§11 原則 7)。
**変わること**:`--lang` も `RULEC_LANG` も無く、`RITSU_LANG` がある環境でだけ、出力の言語が変わる。ほかの環境では前と同じである。一つだけ細かい違いがある。`RULEC_LANG` にどの言語でもない値を置き、`RITSU_LANG` も置いた環境では、前は英語、いまは `RITSU_LANG` の言語になる。`--help` の `--lang` の説明と最後の行、サイトの入れ方のページの優先順位の文に、`RITSU_LANG` を足した。
**確かめたこと**:`tests/lang.rs` に一本足した(`ritsu_lang_comes_after_rulec_lang`)。`RITSU_LANG` だけのとき、`RULEC_LANG` と両方あるとき、`--lang` も付けたとき、どの言語でもない値のときに、どの言語で出るかを見る。このファイルのテストが rulec を走らせるときは、`RULEC_LANG` と `RITSU_LANG` の両方を外してから要るものを置く。`RULEC_LANG` と `RITSU_LANG` を外した環境での rulec の出力(コーパスと変異とほかの規則の 1,725 回)は、替える前と一字も違わなかった。
### 15.169 税区分の語の誤りを E103 で止める(2026-10-04)
**きっかけ**:お金の型の二つ目に `incl_tax` と `excl_tax` のほかの語を書いた型(`money[円, foo]`)を、rulec は黙って通していた。その語をどの表にも無い税区分の名前として持ち、`money[円, incl_tax]` とは別の型として扱う。税込と税抜を混ぜることは型で止めている(§2.3)のに、どちらでもない金額が検査を通り、ritsu の単位の型にもならなかった(§15.164 の `Ty::unit` が None を返す)。単位の表を ritsu に移したときに見つかった。
**決定**:型を書くところ(入力、出力、並びの要素のフィールド、`derive`、`define`、表の出力の列の見出し)のどれでも、お金の型の二つ目の語が `incl_tax` でも `excl_tax` でもなければ、E103 で止める。題は「税区分 `foo` は incl_tax でも excl_tax でもありません」で、型に印を付け、何を書くかを注で言う。新しいコードを作らずに E103 にしたのは、E103 が型の書き方の誤り(型の刻みが読めない `rate[step 1g]`、率の入力に刻みが無い)も受け持つコードだからである。台帳の E103 の「いつ出るか」と「直し方」に税区分の誤りを足し、`docs/codes.md` と `docs/codes.ja.md` を取り直した。
**変わること**:税区分を誤った規則は、前は検査を通り、いまは E103 で止まる(exit 1)。税区分を書かない型(`money[円]`)と、`incl_tax`・`excl_tax` を書いた型は前と同じである。コーパス、変異、ほかの規則には税区分を誤った型が無く、出力(1,725 回)は一字も変わらない。golden も変わらない。
**確かめたこと**:`tests/units.rs` に一本足した(「税区分はincl_taxかexcl_taxだけ」)。入力、出力、`define`、表の列の見出しの四か所に `money[円, foo]` を書いた規則が、それぞれの行で E103 を出すこと(日本語と英語の題)、`incl_tax`、`excl_tax`、税区分なしの三つでは何も出さないことを見る。
### 15.170 `gen` の本体をライブラリに置く(2026-10-04)
**きっかけ**:ritsu の dandori は、規則を rulec のバイナリではなく、同じプロセスの中で rulec の口から読むようになった(ritsu の PLAN の D.3)。dandori のテストは、生成したワークフローを、規則のために `rulec gen` が書くコード(TypeScript・Python・Go のモジュール、規則の `.proto` とサービス)と一緒に型を確かめ、走らせる。そのために rulec のバイナリを `DANDORI_RULEC` で受け取っていたが、それをやめる。`gen` の本体は `src/main.rs` にあり、ライブラリからは呼べなかった。
**決定**:`gen` の本体を、ライブラリの `codegen::generate(ファイル, 出力先, --check か, JSON か, 標準出力, 標準エラー)` に移した。返すのは終了コードで、コマンドはそれを呼ぶだけになった。処理は変えていない(書く先を渡された出力先にし、書いたファイルと古いファイルを数える閉包を関数にしただけ)。dandori のテストは、これを呼んで `rulec gen` と同じものを書く。ritsu の一つの生成パッケージ(ritsu の DESIGN 9.3)も、同じ関数から書ける。
**変わること**:無い。コーパスの 50 本の `gen`(書いたファイルと、標準出力と標準エラー)を含む rulec の出力(1,725 回)は、移す前と一字も違わなかった。
### 15.171 生成した TypeScript を `tsc --strict` で確かめ、列挙の値の集合を列の型に広げる(2026-10-04)
**きっかけ**:ritsu の dandori のテストは、ワークフローのコードと一緒に、規則のために rulec が生成した TypeScript を `tsc --strict`(TypeScript 7)で型検査する。そこで、ステートマシンの例の `order_state`(コーパスの `注文の状態.rule`)の TypeScript が通らなかった(ritsu の PLAN の D.3)。表の行のセルに列挙の値の集合(`出荷, 配達`)を書くと、生成した TypeScript は `[Event.SHIP, Event.DELIVER].includes(event)` と書く。列挙のオブジェクトは `as const` なので、配列の型はその二つの値の型(`"出荷" | "配達"`)だけになり、`includes` は列挙全体の型の `event` を受け取らない(TS2345)。rulec のテストは、生成した TypeScript を node が型を外して走らせ、参照評価器と突き合わせるだけで、型は確かめていなかった。生成 Python には `mypy --strict` をかけている(§15.22)のに、TypeScript には何もかけていなかったので、コーパスの 50 本のうち 6 本(`order_state`、`payment_intent`、`cancel_verdict`、`compensation`、`retirement_allowance`、`return_eligibility`)の 17 か所が通らないことが見えていなかった。
**決定**:
- 列挙の列で、セルに値の集合を書いた行(`出荷, 配達`、`not 入金`)は、配列を列の型の配列に広げてから `includes` を呼ぶ:`([Event.SHIP, Event.DELIVER] as Array<Event>).includes(event)`。無いことがある列なら `Array<Window | null>` である。数と文字列の集合は、リテラルの配列がもともと広い型(`bigint[]`、`string[]`)になるので変えない。グループ(`_遠隔地.has(…)`)は `ReadonlySet<string>` なので変えない。
- 型を外す変換(§15.36)は `as Array<…>` をほかの型と同じく外すので、JavaScript は括弧が一組増えるだけである(`([Event.SHIP, Event.DELIVER]).includes(event)`)。
- `tests/threeway.rs` に、コーパスの全部の規則の TypeScript(モジュール、呼び出しの手本になるランナー、MCP サーバー)を `tsc --strict --erasableSyntaxOnly` にかけるテストを足した。tsc は koyomi と同じ版(TypeScript 7.0.2 と `@types/node`)を `tools/package.json` と `tools/package-lock.json` に固定し、`npm ci --prefix tools` で入れる(`RITSU_TSC` か `RULEC_TSC` でほかの場所のものも使える。無ければ SKIP)。ritsu の CI の tools のジョブでも、rulec の組で入れる。入れ方の文書(サイトの入れ方のページ、`docs/backends.md`)にも、生成 TypeScript が `tsc --strict` を通ることを書いた。
**変わること**:列挙の列に値の集合を書いた規則の、TypeScript と JavaScript の生成物と、それを中に入れたページ。コーパスでは 8 本で、上の 6 本と、もともと通っていた `shipment_fee`(無いことがある列挙の列)と `part_time_allowance` である。それぞれ `typescript/<別名>.ts`、`javascript/<別名>.mjs`、三つの `<別名>_page.html`(JavaScript を中に持つ)と、`rulec doc --format html` の出力(英語と日本語)が変わった。ほかの出力(`check`、`certificate`、`api`、`schema`、`graph`、`vectors`、`coverage`、`fmt`、`adapter`、Markdown の `doc`、ほかの十の言語の生成物)は、コーパスと変異とほかの規則の 1,675 回の出力で一字も変わらない。ritsu の dandori の承認のページ(HTML)は規則のページを中に入れるので、dandori のサイトの例のページ(hotel と order の、英語と日本語)も括弧のぶんだけ変わった。
**確かめたこと**:足したテストは、直す前の生成器では上の 6 本で TS2345 を出して落ち、直したあとは 50 本の 150 のファイルが通る。全言語の突き合わせ(`評価器と生成コードが全言語で一致する`)とほかのテストも通る。dandori の `tests/flows/names.flow` は、`order_state` を関数として呼ぶ形にして、Temporal(TypeScript)を含む全部のプラットフォームで走らせた。
### 15.172 ritsu の口 `Sources` に答える(2026-10-04)
**きっかけ**:ritsu の yuen は、規則がコピーを保存して固定している出典を借りて要件の出どころにし(yuen の DESIGN 1.4、3.3)、要件のコピーと規則のコピーの本文を比べる(E107)。取り込む前の yuen は `rulec api` の `sources` を読むつもりだった。ritsu では、それを型で渡す口(ritsu の DESIGN 3.2、`ritsu_ports::Sources`)を置いた。
**決定**:`src/ports.rs` の `Engine` が `Sources` に答える。規則が宣言する出典ごとに、名前、行、法令ならデータベースと ID と時点と引用箇所ごとの固定、ファイルならパス(規則のファイルからの相対)と url と固定を渡す。答えるのは検査を通る規則だけで、通らない規則には検査の診断を返す(`facts` と同じ)。読んだ規則は `facts` と同じく覚えておき、検査は一度で済ませる。CLI の振る舞いは変わらない。
**確かめたこと**:yuen の `tests/suite.rs` が、`印紙税の本則と軽減.rule`(`法 別表第一 0ba69792e960021e`、`措置法 第91条 85faf53f6f6e8196`)と `osha_extinguisher.rule`(`"§1910.157" c2a9ce966c7e2269`)の固定を口から読み、借りた出典と E107 で使う。§15.164 の rulec の出力の突き合わせ(1,675 回)も、§15.171 で直したあとの出力と一字も違わなかった。
### 15.173 コマンドをライブラリの関数にし、`ritsu check` に `check` の結果を渡す(2026-10-04)
**きっかけ**:ritsu の入口(ritsu の DESIGN 8.1)は、七つの言語のコマンドを `ritsu <言語>` で走らせ、`ritsu check` でプロジェクトのファイルを各言語の `check` で確かめる(ritsu の PLAN の E.2)。rulec のコマンドは `src/main.rs` にあり、ほかのクレートからは呼べなかった。また `ritsu check` は、rulec の `check` が印字するものを、ほかの言語の診断と並べ、見出しにツール名を足して(`error[rulec E032]`)、JSON にも入れる。そのために、rulec の文を読み直すのではなく、診断を一つずつ受け取る必要がある。
**決定**:
- コマンドの本体を、`src/main.rs` からライブラリの `cli::run(引数)` に移した(`src/cli.rs`。`mcp` は `cli` の下のモジュールのまま、ファイルは `src/mcp.rs` に置いた)。クレートのバイナリは、SIGPIPE を戻してこれを呼ぶだけで、`ritsu rulec` も同じ関数を呼ぶ。ブラウザで試すページの wasm には入れない。`src/` のファイルが一つ増え、README のモジュールの数を 52 にした。
- `ports::Engine::checked(ルート, ファイル, 言語)` が、`rulec check` がファイルごとに印字するもの(診断一つずつの `findings_text` と `--format json` の行、そのあとの `check_tail`)を返す。印字するのはコマンドと同じ関数で、`rulec check` の出力と一字も違わない(`tests/ports.rs` がコーパスの 50 本で確かめる)。読めないファイルは、そのファイルだけを確かめられなかったものとして返す(コマンドは、そこで 2 で止まる)。
- `Engine` は、`check` の報告をファイル(リンクをたどり、`..` を畳んだパス)と中身で覚える。dandori や sakai が同じ規則の事実を尋ねたら、報告から検査を通ったかを読み、規則を検査し直さない。報告の文は、作ったときの言語とパスのまま覚え、同じ言語とパスで頼まれたときだけ使い回す。`Engine::checks` が、規則を丸ごと検査した回数を言う(`tests/ports.rs` が、`check` と事実を尋ねて 50 本で 50 回であることを確かめる)。
**変わること**:無い。コマンドの振る舞い(コマンド、フラグ、終了コード、出力)は変えていない。コーパスの出力の突き合わせで確かめた(ritsu の PLAN の E.2)。
### 15.174 入力の範囲を koyomi の日付がとる日にする(`range from koyomi`、2026-10-04)
**きっかけ**:日付を入力に取る規則は、いままで `range >=2026-02-10 <=2027-12-10` のように両端を書き、そのあいだのすべての日で表が完全でなければならなかった。ところが、支払日のように別の計算が決める日付は、とる日がずっと少ない。koyomi は日付を入力の範囲のすべてで計算するので、とる日の集合が正確にわかる(koyomi の例 `支払_20日締め翌月10日払い.cal` の支払日は、689 日の受領日に対して 23 日)。rulec は日付の足し算を持たない(E048)ので、自分で支払日を計算することはできないが、koyomi が数えた集合を範囲として受け取れば、その日の上で表を確かめられる。ritsu で、言語のあいだで型の付いたまま渡す口ができた(ritsu の DESIGN 3.2 の `Dates`、7.5 の (b))。
**決定**:
- 書き方は `<入力> : date range from koyomi "<ファイル>" date <日付の名前>`。`from` の後ろは ritsu の参照(ritsu の DESIGN 6.2)と同じ形で、ファイルは規則のディレクトリからのパス。`from` は射影(§15.125)の語でもあるので、`range` のすぐあとの `from` は射影の始まりとして読まない。
- 集合は ritsu の口 `ritsu_ports::Dates` から読む(`src/days.rs`)。口は実行のあいだスレッドに置く(`days::with`)。`ritsu rulec` と `ritsu check` と、規則を読むほかの言語(dandori、yuen、sakai)が使う `ports::Engine::with_dates` が koyomi をつなぐ。rulec のクレートのバイナリとブラウザで試すページはつながない。
- 入力の範囲は集合の両端(範囲を書いた入力と同じく、生成コードの入口の範囲の検査にもなる)。領域(§6)の日付の軸は、集合の日を一つも含まない座標を、`constraint` のふるい(§15.55)と同じところで「起きない」と読む。完全性の穴、重なり、当てはまらない行(E102。集合の日を一つも取らない行)、例(E019)、ベクタ、証明書の点が、どれも集合の日だけを見る。例やベクタの日付は、境界のそばの集合の日から選ぶ。網羅(`rulec coverage`、§9.2)の境界の対は、どちらかの側が集合の日でなければ、実現できない側として義務にしない(§9.1。`constraint` が外す側と同じ扱い)。
- 生成コードは、集合の外の日を入口で受け付けない(`constraint` の入口と同じ関数 `constraint_guards` が、続けて `day_guards` を書く。続いた日は、一つの範囲の比較にまとめる。SQL は自分の `CASE`)。エラーの文には、どの言語の文字列にもそのまま入るよう、引用符を入れない。
- 証明書は、集合のある規則にだけ上位の `days` を書く(入力ごとに、出どころ `from`:ツール、ファイル、日付の名前、ファイルの SHA-256、日の並び)。覆いの葉に `{"days_axis": 軸}` を足した。軸が分けず、集合の日が分ける行の対(両方がとる日付の座標に、集合の日が一つも無い)は、表ごとの `days_apart` に軸と一緒に書く(集合のある表にだけ書く)。`tools/recheck.py` と Lean の再検査(`proofs/`)は、軸の座標に集合の日が無いことを確かめて葉と対を認め、到達の点の値が集合の日であることも確かめる。Lean では `Sieve` に `days` を足し、`Sieve.asked` に「日付の軸の値は集合の日」を加え、`daysRulesOut` と `not_asked_of_days`(点)、`daysPart` と `not_asked_of_daysPart`(対)を証明した(`RulecCert/Sieve.lean`、`Certified.lean` の `pairsApart`)。集合そのものは、範囲と同じく証明書の言うことで、koyomi のファイルとは SHA-256 で結ぶ。
- 前提(§15.116)に `days` の種類を足した(`rulec api` の `preconditions` の `{"kind": "days", "input": …, "from": …, "days": [日付…]}`。ritsu の口では `Precondition::Days`)。承認する人のページは、入力の注記に出どころと日の並びを書く。ritsu の口 `References` は、規則が参照する外のものとして `koyomi "<ファイル>" date <名前>` を出す(参照の仕方は `range from koyomi`。sakai と yuen が読む)。
- 集合を読めないときは、そのあいだのすべての日で確かめ直さずに止める。koyomi がつながっていなければ E129(ファイルを確かめられなかったものとし、終了コード 2。dandori の E018、yuen の E206 と同じ扱い)。ファイルが無い、koyomi の検査を通らない、その名前の日付が無い、koyomi が数えられない、とる日が無い、のどれかなら E130(koyomi が言うことを注に書く)。`range from koyomi` を日付でない入力、出力、導出、並びの要素に書いたとき、形が違うときは E065。
- ritsu の口 `Rules::checked_over`(入力を与えた日の集合に限ったときに、表が完全で、重ならず、どの行も当たるか)も、同じ仕組み(`days::over`)で答える。自分の検査を通る規則が対象なので、集合に限って新しく出るのは、どの日も当たらない行(E102)である。
**捨てたもの**:
- **集合を規則のファイルに転記すること**(`range in 2026-02-10, 2026-03-10, …`):koyomi のファイルを直したときに、転記した集合が古くなっても誰も気づかない。§15.59 で列挙の値を `.proto` から読むのと同じく、値の集合を決めるのは別の成果物にし、規則はそれを読んで突き合わせる。
- **集合の両端だけを範囲にし、あいだの日も入口で通すこと**:表が確かめていない日が入口を通り、生成コードの「到達しない」分岐に当たる(§15.55 の 4 と同じ理由)。
- **koyomi を読めないとき、範囲全体で確かめ直すこと**:集合の上で書いた表は、すべての日の上ではたいてい完全でなく、確かめ直しは偽の E101 を出すだけである。ritsu の DESIGN 7.5 で決めたとおり、止める。
- **rulec が koyomi のクレートに依存すること**:言語のクレートはほかの言語のクレートに依存しない(ritsu の DESIGN 3.1)。口を通す。
**確かめたこと**:`tests/days.rs`(英語の材料 `tests/days/payment_terms.cal` と `settlement.rule`)が、通る規則、二つの行が支払日の無い日(2026-07-01〜07-05)でだけ重なる規則(通る。証明書の `days_apart`)、集合の日を取らない行(E102)、集合の日に穴のある表(E101、例は 2026-07-10)、集合の外の例(E019)、集合と同じ両端を書いた普通の範囲(E101、例は 2026-07-01)、形の誤り(E065)、その日付を持たないファイルと、置かれていないファイル(E130)、koyomi がつながっていないとき(E129)を英語と日本語の golden で確かめる。クレートのバイナリが終了コード 2 で終わること、証明書の `days`・覆いの葉・`days_apart`、`tools/recheck.py` と Lean の再検査が通ること、手元にツールのある全部の言語の生成コードがベクタに答えること(`rulec test`。この機械では十二の言語の全部)、生成した Python が集合の外の日を受け付けないこと、ベクタが支払日だけをとり、網羅が満たされること(行 3/3、境界の対は義務なし)、口の `checked_over`・前提の `days`・`References` の koyomi の参照も確かめる。台帳に E065、E129、E130 を足した。koyomi の例 `支払_20日締め翌月10日払い.cal` の支払日を範囲にした規則では、集合が 23 日になり、きっかけに書いた数と合う。コマンドの出力を替える前のバイナリと突き合わせると、コーパスとテストの規則の 1,682 回のうち違ったのは、新しい材料 `tests/days/settlement.rule` の 5 回(前は E013、後は E129)だけで、`ritsu rulec` で走らせても、違ったのは同じ材料の 7 回(後は koyomi をつないで通る)だけだった。
### 15.175 生成物の頭を ritsu の形にそろえる(2026-10-04)
**きっかけ**:ritsu の `ritsu gen`(ritsu の DESIGN 9.3)は、規則、日付のファイル、帳簿のクライアント、ワークフローを、言語ごとに一つのパッケージにする。四つの言語の生成物の頭がそれぞれの形だと、一つのパッケージの中で、どのバージョンで、どのファイルから作ったかを、ファイルごとに違う読み方で読むことになる。rulec の頭は `Code generated by rulec <rulec のバージョン>. DO NOT EDIT.` と `Source: <規則の名前> (rule <規則の名前> v<版>, sha256:<12 桁>)` で、規則の名前を二度書き、ファイルを書いていなかった。
**決定**:頭の二行は ritsu の土台(ritsu-emit の `header`)が書く形にした(ritsu の DESIGN 9.2)。koyomi、chobo、dandori の生成物も同じ形である。
- 一行目は `Code generated by rulec <ritsu のバージョン>. DO NOT EDIT.`。ritsu のワークスペースのバージョンは一つにそろえるので(ritsu の DESIGN 13.1)、そろえたあとは rulec のバージョンと同じ数になる。そろえる前のいまは ritsu のバージョン(0.1.0)で、rulec のクレートのバージョン(0.22.1)ではない。
- 二行目は `Source: <ファイル> (rule <名前> v<版>, sha256:<16 桁>)`。ファイルは、`rulec gen` ではファイルの名前(生成した機械のパスは書かない)、`ritsu gen` ではプロジェクトのルートからのパスである(`Gen::shown`)。ハッシュは、ritsu の固定と同じ 16 桁にした(12 桁から延びた)。日本語は `もと: <ファイル>(rule <名前> v<版>、sha256:<16 桁>)` で、種類の語は `.rule` に書く語の `rule` にした(前は `規則`)。
- 準用(`Applies:`)と出典(`Cites:`)の行は、前と同じく三行目から下に続く。
- Python の生成物は、ファイルの頭のコメントの行のあいだに空行を入れない。`pep8_blanks` が、トップレベルの行のあいだに空行を足すので、頭の行のあいだにも空行が入っていた。
**変わること**:全部の言語の生成物の頭の二行(Python は頭の行のあいだの空行が無くなる)。規則のページ(HTML)は生成した JavaScript を中に持つので、そのページの頭の二行も変わる。ブラウザで試すページの `rulec.wasm` は作り直した(`website/tools/make_wasm.sh` は、ワークスペースでは `crates/rulec/target` を探して止まるので、同じフラグの `cargo rustc` で作り、ritsu のビルドの置き場所からコピーした)。ritsu の dandori のサイトの例のページとブラウザで試すページの記録も、規則のページを中に持つので変わった。ほかの出力は変わらない。
**確かめたこと**:rulec のテスト(全言語の突き合わせと、`rulec.wasm` の突き合わせを含む)が、745 件通る(ignored 1)。
### 15.176 一つの生成パッケージのためのモジュール(2026-10-04)
**きっかけ**:`ritsu gen` のパッケージに入れるのは、規則のライブラリ(TypeScript、Python、Go の一つずつ)だけで、ランナー、MCP のサーバー、ページ、`.proto`、ベクタは入れない。`codegen::generate` は全部の言語の全部のファイルを書くので、ritsu はそこから選ぶことになる。
**決定**:`codegen::package_module(ソース, パス, ルートからのパス, 言語)` が、言語(`typescript`・`python`・`go`)のモジュールを、パッケージの `rules/` の下のパス(`<別名>.ts`、`<別名>.py`、`<パッケージ>/<別名>.go`)と中身で返す。中身は `rulec gen` が書くモジュールと、頭の二行目のファイルのほかは同じである。検査を通らない規則からは作らない。
**変わること**:無い。`rulec gen` の出力は変えていない。
### 15.177 コーパスの規則に、英語の名前で書いた双子を足す(2026-10-04)
**きっかけ**:コーパス 50 本のうち 37 本は、名前もセルの値も日本語で書いてあった(日本の運賃表、法令、保険料の表の転記と、日本語で書いた例)。例とテストの材料、サンプル、golden、文書の例は、英語を先にして、日本語は横に置く版か、よくて対等にする(ritsu の PLAN)。日本語の規則は消さず、中身も変えない。そこで、日本語の名前の規則一本ごとに、同じ規則を英語の名前で書いたもの(双子と呼ぶ)を足した。コーパスは 87 本になり、74 本が 37 組の対である。
**決定**:
- **置き場所と名前。** 双子は同じ `tests/corpus/` に、英語のファイル名で置く。ファイル名は規則の名前で、生成物の別名でもある。`gen` は別名の同じ規則が二つあっても何も言わず、後のものが先のものを上書きする。コーパスを一つのディレクトリへ生成するテスト(`threeway.rs`、Kani の件数を数える `website.rs`)では、片方を二度走らせることになる。だから双子の名前は、元の別名に言葉を足したものにした(`送料(shipping_fee)` の双子は `member_shipping_fee`)。対は `tests/corpus/twins.tsv` に書き、`tests/twins.rs` が、日本語の名前の規則が全部載っていること、コーパスの別名が重ならないことを確かめる。
- **置き換えで作る。** 名前は、元の規則が別名に使っていた英語に替えた(別名の無い名前には英語の名前を付けた)。`円` は `JPY`、`1000万円` は `10_000_000JPY` と書き、`×` `÷` は ASCII にした。表、セル、例はコピーしたもので、打ち直していない。説明とコメントは訳した。別名のままでは困る所だけ替えた:予約語(`min`、`none`)、入力と同じ名前になる列挙の型(`kind : kind` と読めてしまうので、型には `coupon_kind` のような名前を付けた)、暗号のような別名(`m`、`wc`、`s`、`r`)。
- **日本語のまま残るもの。** `std/都道府県` の値(組み込みの名前空間は、規則の中では日本語でしか書けない)、法令と表計算の引用箇所の名前(`別表第一`、`第91条`、`表1`)、出典の文書のファイル名。どれも文書のほうの言葉である。
- **文字列を返す規則。** `保存基準` の出力の文字列は、双子では英語にした。
- **同じ規則であることを、五つで確かめる**(`tests/twins.rs`。ほかに、対の名前の決まりを確かめるテストがある)。名前は対のあいだで違うので、名前を比べるものは一つも無い。
- `check`:同じ警告と注(影の対の数)が出る。
- `coverage`:七つの基準の義務の数と、満たした数が同じである。
- `vectors`:ケースの数が同じで、数と真偽が同じ場所に同じ値で並び、文字列は対の中で一つの対応になり(片方の `一般` は、もう片方でいつも `basic`)、働く行と、ケースの理由の文が同じである。4,360 ケースを比べた。
- `certificate`:表ごとに、主張を名前抜きで比べる(行ごとの検査と、軸ごとに受け入れる点の数、行どうしの分け方、尋ねられない行、到達の点、覆い・`linear`・`above` の木の形)。
- `api`:呼び手への約束を名前抜きで比べる(入力どうしの関係としての前提、入力と出力の単位・範囲・丸め、列挙の値の数、準用・出典・契約・状態の有無)。`円` と `JPY` は同じ通貨なので、単位は同じものとして比べる。
- **名前の順が結果の順に出る所は、数と形に落とす。** 証明書の鍵と、`fold` の遷移を数え上げるケースは、名前の順に並ぶので、対のあいだで並びが違う。`fold` の規則(`全国運賃`)は、ケースの数と網羅だけを比べる(遷移は、日本語の判定を別名に直して並べ替えれば同じ集合になることを、走らせて確かめた)。ケースの中の値(並びの要素に入れる数)も、名前の順で選ぶ候補が変わるので、同じではない。
- **ほかのテストに載せた。** 双子は `tests/corpus/` の中にあるので、ディレクトリを読むテスト(証明書の再検査、グラフ、口、`check` の言語、`vdiff`、`robustness`、`connect`、`units`)にはそのまま入る。一覧を持つテストには足した:`threeway.rs` の一致(12 言語。87 本)、`coverage.rs` の固定値(日本語の規則と同じ値)、`doc.rs` の「セルはもとの規則までたどれる」、`vdiff.rs` の「決められない」規則(`checked_bag_fee`)。golden は、W105 の二つに双子の分を足した(`W105-b-twin`、`W105-c-twin`。日本語と英語の文面、JSON)。
- **文書。** 例のページの英語は双子を見せ(`website/tools/make_examples.py` が `twins.tsv` から)、日本語のページは元の規則のままである。README、AGENTS.md(とそれから作るスキル)、リファレンス、案内、確かめ方、生成、道案内の規則の抜粋と実演は、英語の名前にした。出力の実演は、双子に走らせた実物を貼った(`verify`、`diff`、`replay`、`fixtures lint`、E101、E116、E122、E126、E038)。突き合わせのページの `verify` と `diff` の実演は、英語のページを双子の出力に、日本語のページを元の規則の出力に結ぶように、`tests/verify.rs` と `tests/vdiff.rs` を直した(`verify` は、旧実装のふりをするアダプタも英語の名前で足した)。
- **Kani の記録を取り直した。** `experiments/kani/report.txt` を、87 本で取り直した(207 本のハーネスが通り、落ちるものは 0。766 秒)。ページの数(ハーネスの本数と秒数)もそれに合わせた。
**捨てたもの**:
- **日本語の規則の別名を替えて、双子と名前をそろえること。** 日本語のものは中身も名前も変えない。
- **日本語の名前の規則を、英語の名前の規則に置き換えること。** 日本語でしか書けないこと(組み込みの都道府県、法令の引用箇所)を確かめる規則が無くなる。
- **双子の名前を、別名と同じにして、別のディレクトリに置くこと。** `gen` は、ディレクトリを渡されると中を全部たどる(`tests/corpus/` の下の子ディレクトリも)ので、別名のぶつかりを防げない。
- **`std/都道府県` に、英語のつづりで書ける別名を足すこと。** 言語の変更で、この段階の範囲を超える。三つの規則(`送料`、`ゆうパック運賃`、`送料のただし書`)の双子では、都道府県の値だけが日本語で残る。
**見つけたこと**(直したもの):`tests/website.rs` の `例のページは両言語で同じ規則を同じ順に並べる` は、規則のブロックを取り出す条件が合っておらず、空の列を二つ比べて、いつも通っていた。ブロックを取り出す条件を直し、英語のページの双子を日本語のページの元の規則へ戻して比べる。
**見つけたこと**(直したもの、もう一つ):生成のページ(`website/docs/generate.md`)の生成コードの抜粋は、古い版の日本語の規則から手で転記したままで、いまの生成器の出力(Go の `Fired` は三つの項を持つ)と食い違っていた。README の例(テストが生成物に照らして守っている)の実物に替えた。
**見つけたこと**(直したもの、§15.179):E043 の英語の文は、引数の並びが違った(`For example years = 0 is outside range retirement_pay.rule of `years` in `>=1 <=40`.` と、規則のファイル名と範囲が入れ替わる)。日本語の文は正しい。E103 の英語の文の例に `round down(1円)` が出た(`円` は `JPY` と同じ通貨である)。
**確かめたこと**:双子 37 組の全部で、上の五つが一致する。コーパス全体の `threeway`(12 言語、87 本)、証明書の再検査、網羅の固定値、`rulec test` の Kani の段(上のとおり)が通る。双子の表のセルの金額を一つ書き換えると、`twins` のテストが落ちる(実際に書き換えて確かめた)。
**残したこと**:フォーマットの文書(`docs/formats.md`)、生成コードの文書(`docs/generated-code.md`)、`docs/backends.md` の例と、案内のページ(`scenarios.md`)の CSV と表計算の取り込みの実演、確かめ方と入口のページの画面写真(日本語の規則の画面)は、日本語の規則に走らせた実物のままである。生成のページの W114 の防壁の抜粋は、コーパスに W114 を出す規則がもう無く(`tests/doc.rs` の注を見よ)、双子でも撮り直せないので、日本語の名前のまま残した。フォーマットと生成コードの文書の例は、名前と別名の違い(日本語の名前にだけ別名がある)や、日本語の名前の値が配線に出ること、日本語の関数名を表計算に書く例を見せるもので、双子では見せたいことが見えなくなるか、画面や生成物を撮り直すところから要る。`rulec explain` の台帳の再現例は、英語の出力で英語の名前の例を見せるようにした(§15.179)。
### 15.178 英語のページに残っていた日本語の例を、英語の規則の実物に替える(2026-10-04)
**きっかけ**:§15.177 で README、AGENTS.md、リファレンス、サイトの多くのページを双子に結んだが、フォーマットの文書(`docs/formats.md`)、生成コードの文書(`docs/generated-code.md`)、`docs/backends.md` の表計算の例、案内・確かめ方・道案内・生成のページの実演、画面写真、図は、日本語の規則に走らせた出力のままだった。英語のページに日本語で書いた所があれば英語にする。ただし、日本語であること自体が要点の所(日本語の名前が書けること、`std/都道府県` の値、e-Gov の条文、出典のコピーにある日本語の閾値の言い方を読むこと、全角の記号)は日本語のままでよい。
**決定**:
- **例は、双子か、英語で書いた規則の実物にする。出力は走らせて貼る。** 作って貼らず、手で直さない。`formats.md` の JSON の例は、長い出力を途中で省いてあるので、省き方は前のままにして(`…` が値、配列の残り、オブジェクトの残り、文字列の後ろを表す)、残った鍵と値の全部が実物の出力にあることを、`tests/formats.rs` の `formatsの例は実物の一部である` が確かめる。確かめるのは、`check`(`tests/mutants/m_e101en.rule`)、`coverage`(`member_shipping_fee`)、`api`(`single_coupon`、`order_lifecycle` の状態機械、`nationwide_freight` の並びの要素)、`graph`(`cart_shipping_fee`)、`certificate`(`coupon_stacking`)、`vectors`(`order_period`)の八つである。走らせる準備が要る例(`verify`、`diff`、`replay`、`fixtures lint`、アダプタの対話)は、下のとおり実物を貼ってあるが、テストには載せていない。
- **突き合わせの例は、都道府県の無い英語の規則(`parcel_rate`)に替えた。** 旧実装のふりは、`rulec gen` が `parcel_rate` をコピーした表(`north_america | large | >160oz` の 30USD を 31USD にしたもの)から書いた Python のランナーを包む 20 行で、アダプタの対話の記録もそれで取った(`verify` の 96 件のうち 9 件の食い違い)。`diff` の領域の例は、同じ規則の `v3` と `v4` を git のタグで比べた。`fixtures lint` の例は、`dest` に `mars` を入れた一件を足した記録である。
- **画面写真は、英語のページには `member_shipping_fee` の画面を撮り直した**(`try-*-en*.png` の六枚)。日本語のページの画像(`try-*-ja*.png`)は元のまま。`website/tools/shots.sh` は言語ごとに規則を替え(日本語は `送料.rule`、英語は `member_shipping_fee.rule`)、第二引数で撮る言語を絞れる。画面には `dest` の値として `東京都` が出る(`std/都道府県` の値は日本語でしか書けない)。
- **W114 の防壁の抜粋は、テストの材料の小さな英語の規則(`tests/pages/half_bound.rule`)から取る。** コーパスには W114 を出す規則がもう無く、`tests/mutants/` に足すと、確かめ方のページと図に書いた本数が変わり、日本語のページの数字まで替わる。`tests/docs.rs` の `w114の防壁の抜粋は実物と一致する` が、`docs/generated-code.md` と `website/docs/generate.md` の抜粋を `gen` の出力に照らす。
- **英語のページでは、文書の表を `table1` と書く。** `表1` と `table1` は同じ名前なので、`表1` は「日本語では同じ名前」と書く所にだけ残した。`rulec import` の下書きの印は英語では `# guess:` なので、AGENTS.md の `# 推定` を直した。E119 の例は、`tests/mutants/m_e119.rule` の `Under 18` と `<=18` にした。
- **生成コードの文書と生成のページは、`member_shipping_fee` と `single_coupon` の生成物の名前にした。** 型の名前は `JPYInclTax`、`rulec test` の実演は双子の実物(70 ベクタ、全部で 36 件が一致)、Wasm のモジュールの大きさは測った値(43,729 バイト)の 44 kB にした(前は 40 kB)。`tests/api.rs` の `生成物の文書が実物の名前を使っている` は、`single_coupon.rule` の `api` に結び直した。
- **日本語のままにしたもの**:`std/都道府県` と、その値を入力や例に使う所(双子 `member_shipping_fee`、`yupack_base_fee`、`shipping_fee_proviso` を見せる所)。e-Gov の引用箇所の名前(`第91条`、`別表第一`、`附則…`、漢数字の条)と、それを例にした `rulec source outdated` の出力。出典のコピーにある日本語の見出しと閾値の言い方(`関東`、`一般の事業`、`60cm以下`、`円以上`、`1,000分の5`)が読めること。名前と別名の例(`届け先(dest)`、`会員区分(member_kind)`)、単位と桁の言葉(`円`、`銭`、`坪`、`万`、`億`、`兆`)、全角の記号(`→ ・ 、 ≦`)。`rulec schema --keys alias` の例の `送料.rule`(名前と別名が違う規則でしか見せられない)。README の日本語のページへの案内。例のページの双子の説明に出る日本語のファイル名(`所得税.rule`)。
**捨てたもの**:
- **`std/都道府県` の値を避けるために、英語で書いた規則だけに例を寄せること。** 画面写真と、案内・生成のページの話の筋が変わる。都道府県の値を英語のつづりで書ける別名を足せば足りるが、言語の変更になる(§15.177)。
- **`tests/mutants/` に W114 の英語の規則を足すこと。** 上のとおり、本数が日本語のページと図に出る。
- **`flow.svg` の質問の `山梨県` を英語にすること。** 図の URL の刻印は `flow` の中身のハッシュで、日本語の `index.md` の URL も同じ刻印を持つので、日本語のページを触ることになる。
- **ほかの `.rule` の例を、`formats.md` の `by` の例のために作ること。** `by` は rulec が読まない(出典の記録の置き場)ので、走らせて取れる出力が無い。英語の説明のための例を手で書いた。同じく、抽出の約束(`--via`)の対話の例は、説明のための例で、走らせていない。
**見つけたこと**(直していないもの):
- `rulec verify --help`、`replay --help` などの英語の `--help` の例に、日本語の規則名(`rules/送料.rule`、`送料@v3`)と `<フィールド=値>` が出る(`src/cli.rs`)。`--help` の出力が変わるので、直すなら golden と、文書に貼った `--help` を取り直す。
- `rulec import` は、`JPY` の列も `money[円, incl_tax]` と書く(`src/import.rs` の `num_type`)。英語の表を円で取り込むと、下書きに `円` が出る。英語のページの例は、USD の表で取った。
- E119 の英語の文に `60cmまで` が出る(`src/sources.rs`)。
- `examples.md` を作る `tools/make_examples.py` の、三つの双子の説明(`所得税.rule` など)は、日本語の元のファイル名を書いている。
**確かめたこと**:英語のページ(18 枚)の日本語の連なりは、729 から 300 に(文字では 1,941 から 808 に)減った。`formats.md` は 230 から 0、`backends.md` は 23 から 0 になった。`formatsの例は実物の一部である`(例を一つ書き換えると落ちることも確かめた)、`w114の防壁の抜粋は実物と一致する`、`生成物の文書が実物の名前を使っている`、`資料の抜粋は実物と一致する`(`checks.md` の `## Groups`)、`サイトが丸ごと見せる規則は検査を通る`、`スキルはいまの文書から組み立てたものと同じ`、`rulec doc` と `verify` と `vdiff` の文書のテストが通る。
**§15.177 の「残したこと」**は、`formats.md`、`generated-code.md`、`backends.md`、案内の実演、画面写真、W114 の防壁の抜粋の全部について、ここで替えた。残っているのは、上の「日本語のままにしたもの」だけである。
### 15.179 `rulec explain` の再現を、英語の名前で書いた版でも持つ(2026-10-04)
**きっかけ**:`rulec explain` の台帳の再現は、112 項のうち 61 項が日本語の名前・文字列・単位(`円`)を使っていた。英語の出力(`rulec explain <コード>`、`--all --format markdown`、`docs/codes.md`、サイトの英語の台帳のページ)でも、再現の規則は日本語の名前のままだった(§15.177 の「残したこと」)。英語のページで日本語を使っているところは英語にする。koyomi・yuen・sakai の台帳は、英語の再現と日本語の再現を二つ持つ形(ritsu-base の `Entry::english`)で済んでいる。
**決定**:
- **台帳の項が、英語の再現を二つ目として持てる。** `Entry::english(example)`(隣に置くファイルが違うときは `english_with_files(example, files)`)で、同じ規則を英語の名前で書いたものを足す。`Entry::repro(lang)` が、英語の出力では英語の版を、日本語の出力(`--lang ja`)では元の再現をそのまま返す。持たない項は、これまでどおり、どちらの言語でも同じ再現を見せる。rulec は自分の形の台帳を持つので、フィールドは `example`・`files`(元の再現。英語の版が無いときは両方の言語)と `english: Option<Repro>` にした(`Repro` は `example` と `files`)。予算(`--budget`)は両方で同じ。`explain --format json` の `example` と `files` も言語に従う(鍵は変えない)。日本語の出力は、テキスト、Markdown、JSON のどれも一字も変わらない(替える前と後の出力を突き合わせた)。
- **英語の版を持たせた項は 55。** 再現が日本語の名前・文字列・単位を使っていた 61 項から、次の 6 項を除いたもの。
- 日本語であること自体が要点の 5 項は、日本語のまま残した。E011:ASCII でない名前には別名が要る、という項で、説明も `重量(weight)` を見せる。E037・E038・E039・W119:e-Gov の法令を引くので、条の名前(`第1条`)と条文のコピー(`第一条`、`甲は、乙とする。`)が日本語である。
- E115 は日本語を含まない(非 ASCII の文字は除算の記号 `÷` だけで、言語も説明も使う)。
- **作り方。** 名前は英語にし、別名は残した(`呼(c)` の英語の版は `call(c)`。生成コードの名前は変わらない)。`円` は `JPY`、`1万円` は `10_000JPY` のように桁(5 桁以上は `_` で区切る)、`×` は `*`(コーパスの双子と同じ。§15.177)。表の列は `rulec fmt` でそろえた。隣に置くファイルも英語にした:`callee.rule`(行ラベルは `small`、`large`)、`enum_callee.rule`(値は `low`、`mid`、`high`)、`broken_callee.rule`、`tariff.md` と `tariff.md.fragments/table1.tsv`(`Kinki`、`Kanto`)、`sizes.md` と `sizes.md.fragments/table1.tsv`。出典のコピーは英語の文書の言い方(`up to 60cm`、`over 60cm up to 80cm`)で書き、固定の digest(`sha256:…`)は取り直した。
- **同じ診断を同じ行に出すことを、テストで守る**(`tests/codes.rs`)。英語の版は、日本語の版と同じコードを同じ行に出す。作るときに 55 項の全部で確かめた(名前が予約語や生成先の語とぶつかって警告が増える、単位が別に読まれて診断が変わる、といったことが無い)。
- **英語の版に日本語が残る項は、テストの一覧 `KEPT`(5 項と理由)で決める。** 一覧に無い項の英語の版に日本語(かな、漢字、全角)が入るか、一覧の項から日本語が無くなると、テストが落ちる。英語の版を持たない項(日本語の名前を使わない 51 項と E115)も、同じ検査に通る。
- **英語の説明(`when` と `fix`)も、28 項で英語にした。** 説明が `1451円` と言い、再現が `1451JPY` と言うのでは、読む人が迷う。再現の名前を引いている所(`円` は `JPY`、`関東` は `Kanto`、`倍` は `twice`、`60cm以下` は `up to 60cm`、`料金表.md.fragments/表3.tsv` は `tariff.md.fragments/table3.tsv`)に加えて、説明の中の例で日本語の名前が要らないもの(E002 の `六十(60)` は `sixty(60)`、E003 の `規則名` は `<name>`、E009 の `範囲区分`、E014 の `率割引`、E015 の `付与点`、E020 の `運賃行`、E053 の `返金額`、E057 の `都道府県` は `<enum>`、E103 の `重量加算`)を英語の名前にした。日本語の説明は変えていない。
- **診断の英語の文を二つ直した。** E043:英語の文は引数を日本語と同じ並び(入力、呼び先のファイル、入力、範囲)で受け取るのに、並びが違い、ファイル名と範囲が入れ替わっていた。位置で取るようにして、`For example years = 0 is outside range >=1 <=40 of `years` in `retirement_pay.rule`.` になった。E103(丸めの刻みが列の値ではないとき):英語の文の例が `round down(1円)` の固定だったのを、列の単位から作るようにした(`money[JPY]` なら `round down(1JPY)`、`mass[g]` なら `round down(1g)`、`円` の列なら `round down(1円)`。E104 の fix が使う `unit_one`)。日本語の文は替えていない。
**捨てたもの**:
- **英語の版を、日本語の版から実行時に作ること。** 置き換えの規則が `explain` の中に入り、読む人が何を見ているのか分からなくなる。再現は、書いたとおりに出る(テストも書いたとおりを走らせる)。
- **E011 の英語の版を、ASCII でない別の名前(`größe` など)で作ること。** 日本語の名前であることが要点で、説明の例(`重量(weight)`)とずれる。
- **E037〜E039・W119 の英語の版を、条の名前とコピーだけ日本語にして、ほかを英語にすること。** 半分だけ日本語の例は、日本語が取り残されたように読める。条の名前を英語の別名で書く言語の変更は、この段階の範囲を超える(§15.177 の `std/都道府県` と同じ)。
- **E103 の英語の文を `round down(1JPY)` に固定すること。** `mass[g]` の列に円の例が出る欠点は、日本語の文と同じで、列の単位から作るほうが手間が同じで正しい。
**見つけたこと**(直したもの):E043 と E103 の英語の文(上)。英語の説明のうち、日本語の例を使っていたのは 40 か所(28 項)。
**見つけたこと**(直していないもの):
- 英語の診断の文に、日本語の例がまだ 30 か所ある(`src/*.rs` の `tr!` の英語の側)。E118 の `{name}(<値>, 10円)`、`range >=0円 …`、`円×円`、`気温`、`届け先(dest)`、`>0円` など。`tests/golden_en.rs` の `assert_english` は `円 銭 万 億 兆` と `届け先` を許している。
- 大きい金額の書き戻し(`fmt_big`)は、英語の出力でも `万`・`億`・`兆` を使う。英語の版の E112 は `the reachable interval is >=-100万JPY <=100万JPY` と言う。E112 の説明の `range >=-110万円 <=100万円` は、その形を見せているので、日本語のまま残した。
- E037 の説明の `表3`(`table3` の日本語の書き方)、E116 の `1,000分の5`、E119 の `円以上` は、言語が日本語の書き方も読むことを言っているので、日本語で残した。
**確かめたこと**:英語の版 55 本の全部が、日本語の版と同じ診断(コードと行)を出す。`tests/codes.rs` の 15 件が通る(10 件から、再現の確かめを両方の言語に広げ、五つ足した)。テストの守りが働くことは、英語の版を一つ壊す(E014 の英語の版の `p * r` を値に替える)ことと、英語の版に日本語の名前を一つ入れることで確かめ、戻した(再現が自分のコードを出さない、同じ診断を同じ行に出さない、日本語が残る、`docs/codes.md` が古い、の四つのテストが落ちた)。E043 と E103 の文を守る二つのテストは、直す前の `src/apply.rs` と `src/types.rs` に戻すと落ちる。`docs/codes.md` は `rulec explain --all --format markdown --lang en` の出力そのもので、`docs/codes.ja.md` は替える前と一字も違わない。golden は `tests/golden/en/E043.txt` の一行だけが変わった(`golden`、`golden_en`、`json_v2` は通る)。rulec の全体(`cargo test -p rulec --no-fail-fast`、PostgreSQL つき)は、64 の試験バイナリで 767 件が通り(英語を先にした担当の 762 件に、この担当の 5 件)、落ちたものは 0、ignored 1(前からのもの)だった。SKIP の行は 15 で、どれも Lean の層(`proofs/`)が build されていないための「Lean の再検査は走らせない」である。
**残したこと**:上の「見つけたこと(直していないもの)」。
### 15.180 英語の出力から、日本語の例と万・億・兆を外す(2026-10-04)
**きっかけ**:§15.179 で `explain` の再現を英語にしたあと、英語の出力に日本語がまだ出ていた。診断の文(`src/*.rs` の `tr!` の英語の側)の例が 30 か所、大きい金額の書き戻し(`fmt_big`)が英語の出力でも `万`・`億`・`兆`、`doc` の範囲が英語でも `〜`、`--help` の例が英語でも `rules/送料.rule`、引数の名前が英語でも `<フィールド=値>` だった。`tests/golden_en.rs` の `assert_english` は `円 銭 万 億 兆` と `届け先` を許していて、全角の約物は日本語と数えていなかった。
**決定**:
- **英語の文の例を英語にした(16 か所)。** `円` は `JPY`(E118 の `10JPY`、`range >=0JPY …`、`range >=1JPY …`、`>0JPY`、`JPY×JPY`)、`気温` は `temp`、`運賃行` は `fee_rows`、`サイズ60(size_60)` は `sixty(size_60)`、`送料@v3` は `shipping_fee@v3`、`60cmまで` は `up to 60cm`、`表3` は `table3`、`@法`・`@郵便` は `@act`・`@postal`。日本語の文は替えていない。
- **日本語のまま残した 14 か所と、その理由。**
- e-Gov の条の名前 `第20条`、`第20条の2`、`別表第一`、`附則…`(`parse.rs`、`sources.rs`):法令の引用の書き方そのものが日本語で、英語の別名は無い。
- 組み込みの `std/都道府県`(`types.rs`、E013 の説明):規則の中では日本語でしか書けない。
- 単位の一覧(`types.rs` の `units()`)の `坪`・`円`・`銭`:言語が受け付ける綴りである。英語では `坪 (tsubo)`、`円 (yen, the same as JPY)`、`銭 (a hundredth of 円)` と添えた。
- ASCII でない名前の例 `届け先(dest)`(`types.rs` の E011 の hint、`cli.rs` の `--keys` の説明)、E011 の説明の `重量(weight)`:その項の要点が、ASCII でない名前に別名が要ることである。
- `表3`(`table3` の日本語の書き方。`sources.rs`、E037)、`1,000分の5`(E116)、`円以上`(E119):言語が日本語の書き方も読むことを言う文で、日本語の書き方を挙げている。
- **大きい金額の書き戻しは、英語では桁を `_` で区切る。** 5 桁以上の整数を 3 桁ずつ `_` でつなぐ(`1_000_000JPY`、`-12_345JPY`)。日本語は `万`・`億`・`兆` のまま。決め方:rulec の英語の出力で数を書くところの中で、桁を区切っているのは `lex.rs` の E049 の「桁を区切りたいときは `_`」(`,` は集合の区切りなので書けない)だけで、コーパスの双子も 5 桁以上を `_` で書く。出力のほかの数(格納される整数、証人)は区切らずに書くが、`fmt_big` が書くのは、規則に貼り戻される範囲と金額(E112 の hint、E108 の区間、`doc` の範囲)なので、規則が読む書き方にそろえた。`,` は規則が読めず、区切らない書き方は 8 桁を超えると読めない。`fmt_big` に言語を渡し(`fmt_big(v, ja)`)、`fmt_big_pub` と `fmt_val` が現在の言語を渡す。
- **JSON の `fix.text` は、金額を今までどおり `万` で書く。** `fix` は `witness` と同じデータで、言語で変わらないと `tests/json_v2.rs`(`証人と直し方は言語で変わらない`)が決めている。`fmt_val_fixed` が、言語に関わらず日本語の書き方を返す。
- **`doc` の範囲の区切りは、英語では `to`(日本語は `〜`)。** `fmt_big` の出力と同じ行にあり、金額だけ英語で区切りが日本語では半端になるため。顧客向けの範囲(`customer_input`)も同じ。
- **英語の `--help` の例と引数の名前を英語にした。** 例は `tests/corpus/twins.tsv` の英語の名前にそろえた(`rules/送料.rule` は `rules/member_shipping_fee.rule`、`印紙税` は `japan_stamp_duty`、`運賃` は `yupack_base_fee`、`運賃表.xlsx` は `yupack_fares.xlsx`、`--sheet 本則` は `--sheet main`、`--name 料率` は `--name premium_rate`、`--fill 重量=1000` は `--fill weight=1000`)。`doc` の例の `--lang ja` は、英語では外した。引数の名前は `<フィールド=値>` が `<field=value>`、`<名前>` が `<name>`。日本語の `--help` は替えていない(34 の例と 5 の引数の名前を `tr!` の対にした)。
- **`assert_english` が許す語は `KEPT`(`届け先` だけ、理由つき)にした。** `円 銭 万 億 兆 万円 億円 兆円` は外した。判定は、かな・漢字に加えて、日本語の約物(`〜`、`「」`)と全角(`()`)も日本語と数える。英語の `--help` の全ページ(`rulec --help` と全コマンド)も同じ `KEPT` で調べる。
**捨てたもの**:
- **`fix.text` も英語では `_` の桁にすること。** 日本語の JSON の `fix.text`(`範囲 >=-100万円 …`)が変わる。日本語の出力は替えない。
- **`fmt_big` を、5 桁以上のうち、日本語が略していた 1 万の倍数だけ区切ること。** 同じ出力に `30_000JPY` と `25000g` が並び、英語の読み手には根拠の無い区切りに見える。
- **英語の金額を `,` で区切ること**(`1,000,000JPY`)。規則の中では E049 になり、貼り戻せない。
- **顧客向けのページの金額だけ `,` にすること。** 顧客向けのページは、英語では単位を数に付けて書く(`990JPY`)ので、区切りだけ変えても揃わない。顧客向けの書き方は別に決めるのがよい。
- **`doc` の全角のかっこ(`(default)`、`(row 1)`)を英語で ASCII にすること。** `tests/doc.rs` が `店内全商品(default)` を確かめていて、このパッチの範囲を超える。直すなら `src/doc.rs` の 371 行と 859 行を `tr!` にする。
**見つけたこと**(直したもの):`assert_english` は、全角の約物(`(`、`〜`、`「`)を日本語と数えていなかった。
**見つけたこと**(直していないもの):`rulec import` は `JPY` の列にも `money[円, incl_tax]` と書く(`src/import.rs` の `num_type`。直すなら、書いてあった単位を残す)。出力の振る舞いの変更なので、直すかはまだ決めていない。
取り込むときに直したもの:英語の `doc` の全角のかっこ(列挙の値の `(default)`、状態の遷移の `(row 1)`)と、優先する表を囲む `「」` は、`src/doc.rs` の三か所を `tr!` の対にして、英語では `shop (default)`、`(row 1)`、`"table"` と書くようにした(日本語は変わらない)。英語のページの `rulec import` の実演は、§15.178 で英語の表に替わった。ブラウザで試すページの `rulec.wasm`(いまは `website/rulec/docs/playground/`)は作り直した。
**確かめたこと**:規則とコーパスと変異(196 ファイル)に、`check`(テキスト、JSON、`--show-shadow`)、`doc`(承認者向けと顧客向け)、`coverage`、`vectors`、`api`、`graph`、`certificate`、全コマンドの `--help` を、英語と日本語で走らせ(3,962 回)、替える前と後を突き合わせた。**日本語は 1,981 回の全部で一字も変わらない。** 英語は 239 回が変わり、どれも意図したもの(金額の桁、範囲の `to`、文の例、`--help` の例)である。`doc` の差は、数字の並びと `〜` を除いて同じ行であることを、行ごとに確かめた。変わらなかった残りの日本語(英語の出力、規則に由来しないもの)を同じ道具で数え直すと、`doc` の全角のかっこ・`「」`、`届け先` だけが残る。
**残したこと**:上の「見つけたこと(直していないもの)」。
### 15.181 サイトを ritsu のサイトの `website/rulec/` に移し、英語のページの日本語の残りを三つ替える(2026-10-04)
**きっかけ**:配るのは ritsu だけにする(ritsu の DESIGN 13.2)。rulec のサイトの中身も、dandori のサイトと同じく ritsu のサイトに移す。切り替え(公開、URL の付け替え、元のサイトを止めること)はまだしない。あわせて、§15.178 で残した英語のページの日本語のうち、画面写真、突き合わせの実演、`flow.svg` の質問の三つを替える。
**決定**:
- **サイトは ritsu の根の `website/rulec/` に置く**(`git mv` で移した)。設定は元のままで、`site_url` を `https://i2y.github.io/ritsu/rulec/`(日本語は `/ritsu/rulec/ja/`)、`repo_url` を ritsu のリポジトリ、`edit_uri` を `edit/main/website/rulec/docs`、言語の切り替えを `/ritsu/rulec/…` にした。組むのは ritsu の `website/build.sh`(`sites=(rulec dandori)`)で、`website/rulec/build.sh` が `website/.venv` の Zensical で `website/rulec/build` に組み、根がそれを `website/build/rulec` にコピーする。`serve.sh` は消した(根の `website/serve.sh` が `/ritsu/` の下で全部を配る)。
- **文書と例はクレートに残す。** `website/rulec/sync.sh` は、`AGENTS.md` と `docs/` を `crates/rulec` からコピーする。`tools/make_examples.py`・`make_stack.py`・`make_assurance.py`・`shots.sh` はコーパスを `crates/rulec/tests/corpus` から読み、`tools/make_wasm.sh` はワークスペースの根で `cargo rustc --package rulec` を走らせ、ビルドの置き場所を `cargo metadata` に尋ねる。テストもクレートに置いたまま(`tests/website.rs`・`docs.rs`・`skill.rs`・`m0.rs`・`verify.rs`・`vdiff.rs`・`doc.rs`・`wasm.rs`)、サイトの道だけを、クレートの二つ上のワークスペースの根の `website/rulec/` に向けた。`skills/sync.sh` も例のページをそこからコピーする。前のブラウザで試すページ(`rulec.wasm` と `playground/`)は `website/rulec/docs/` にそのまま残した。
- **ページの中の元のリポジトリへのリンクは ritsu の `crates/rulec/…` に替え、入れ方のページは README の入れ方に合わせた。** Homebrew、`.deb` と `.rpm`、リリースのアーカイブ、`uses: i2y/rulec@v0.22.1` は、いまの rulec のリリースのものとして残し(ダウンロードの URL は `github.com/i2y/rulec/releases` のまま)、ritsu の最初のリリース(0.23.0)が引き継ぐと書いた。ソースからは `cargo install --git https://github.com/i2y/ritsu --locked rulec`。README、スキル、例のページから公開中のサイト(`i2y.github.io/rulec`)へのリンクと、`Cargo.toml` の `homepage` は、切り替えるまで公開中のほうを指す。
- **英語の画面写真は `parcel_rate` の画面にした**(`try-*-en*.png` の六枚、例 2:5lb、40in、canada、署名なし → 13USD。ドックは `base_rate`)。`member_shipping_fee` の画面では `dest` に `東京都` が出ていた。日本語の画像は元のまま。写真の説明と、`checks.md` のコマンドとアドレスの例も `parcel_rate` に合わせた。
- **突き合わせの実演は、英語のページでは `parcel_rate` の実物にした。** `compare.md` の `verify`(旧実装のふりは、海外あてに 1USD 多く取る。`tests/verify.rs` の `RULE_EN` とアダプタ)、`fixtures lint` と `replay`(`dest` に `mars` を入れた一件を足した記録)、二つの版の `diff`(北米の燃料の率を 5% から 6% にした `v3` と `v4`。切り上げで、ほかの行では同じドルになって消えることを領域が見せる。`tests/vdiff.rs`)、記録の上の `diff`(封筒を 22in から 24in にした `v2`。決まる行が `row 2→row 1` と動く)、JSON の例。`scenarios.md` の 2-3 と 2-4 と改訂の節も同じ規則にした(旧実装 `legacy@2024-03` は海外の小さい荷物を 36USD としたコピーから `rulec gen` で作ったもの)。どれも走らせた出力を貼った。英語の `compare.md` の日本語は 7 から 0 に、`scenarios.md` は 51 から 47 になった。
- **英語の `flow.svg` の質問を英語にした**(「What is the fee for a small parcel going overseas?」。案内の文はもうそう書いていた)。図の URL の刻印は四つの図のハッシュなので `55d3351c` から `778336e2` に変わり、四枚のページ(英日の `index.md` と `tour.md`)の刻印だけを替えた。日本語の文と日本語の図は変わらない。
- §15.178 の「画面写真は `member_shipping_fee` の画面」と、「捨てたもの」の `flow.svg` の項は、これで替わった。
**捨てたもの**:テストを ritsu のクレートに移すこと(ページを守るのは言語のテストで、言語の出力と突き合わせる。dandori と同じ形)。前のブラウザで試すページを ritsu のページに替えること(この節では替えなかった。2026-10-05 に、ritsu のページへの転送に替えた。§15.185)。
**確かめたこと**:ritsu の `website/build.sh` で根・rulec・dandori の英日を組み(rulec のサイトの警告は 0)、rulec の英語と日本語のページを Chrome で開いた。`website/rulec/tools/make_wasm.sh` は新しい場所から走る。`website/rulec/sync.sh` のあとの `cargo test -p rulec -p ritsu` の全体で、852 件が通り、落ちたものは 0 だった。ritsu の `tests/website.rs` の二つの新しい確かめ(`build.sh` が rulec のサイトを組むこと、言語のリポジトリへリンクしないこと)は、わざと壊して落ちることを見た。
### 15.182 十三か国の一段目の区分を組み込みにし、都道府県を英語の綴りでも書けるようにする(2026-10-04)
**きっかけ**:組み込みの名前空間は `std/都道府県` だけで、値は規則の中では日本語でしか書けなかった。英語の双子(`member_shipping_fee`、`yupack_base_fee`、`shipping_fee_proviso`)も、英語のページに貼った出力も、都道府県のところだけ日本語のまま残っていた(§15.177、§15.178)。英語の規則を書く人にとっては、自分の国の州や地域圏を使えないことのほうが大きい。そこで、各国の「都道府県にあたるもの」を入れると決めた。
**決定**:
- **段はどの国も一段目の区分で、国は十三。** 米国 56(50 州、コロンビア特別区、五つの準州)、英国 4(イングランド、スコットランド、ウェールズ、北アイルランド)、中国 33、台湾 22、韓国 17、インド 36(28 州と 8 つの連邦直轄領)、フランス 18(地域圏)、スペイン 19(17 の自治州とセウタ・メリリャ)、イタリア 20、ドイツ 16、オーストラリア 8(6 州と 2 つの準州)、ブラジル 27(26 州と連邦直轄区)、日本 47。中国の 33 には台湾を入れない。ISO 3166-2 の中国の一覧には `CN-TW` があるが、台湾は台湾の区分(`std/tw/divisions`)として持つ。米国の `US-UM`(合衆国領有小離島)は、州・特別区・準州のどれでもないので外した。
- **名前空間の名前は `std/<ISO 3166-1 の 2 文字(小文字)>/<区分の種類>`。** `std/us/states`、`std/gb/nations`、`std/cn/provinces`、`std/tw/divisions`、`std/kr/provinces`、`std/in/states`、`std/fr/regions`、`std/es/communities`、`std/it/regions`、`std/de/states`、`std/au/states`、`std/br/states`、`std/jp/prefectures`。`std/都道府県` は名前も値もそのまま残る。
- **規則の中の列挙の名前は `<国>_<種類の単数>`。** `us_state`、`gb_nation`、`cn_province`、`tw_division`、`kr_province`、`in_state`、`fr_region`、`es_community`、`it_region`、`de_state`、`au_state`、`br_state`、`jp_prefecture`。いままでの `std/都道府県` は、パスの最後の語(`都道府県`)を列挙の名前にしていた。同じ形を英語に移すと `states` が五か国でぶつかり、二か国を取り込む規則で名前が決まらない。単数にしたのは、列挙の名前が一つの値の型を言うからである(`dest : us_state`。双子の `member_kind` と同じ)。
- **値は英語の名前を ASCII にしたもの。** CLDR の英語の名前から、ダイアクリティカルマークを落とし(`São Paulo` は `Sao_Paulo`、`Île-de-France` は `Ile_de_France`)、略語の点を消し(`U.S. Virgin Islands` は `US_Virgin_Islands`)、空白・ハイフン・アポストロフィ・かっこを `_` にする。名前の中に `-` と空白は置けない(`lex.rs` の `is_delim`。`-` は演算子と「問わない」のセルである)。大文字小文字は英語の名前のまま(`New_York`、`Rio_de_Janeiro` の `de` は小文字)にした。凍結してある都道府県の ASCII 名(`Hokkaido`、`Tokyo`)が固有名詞の大文字で始まり、ほかの国もそれにそろえる。双子の英語の値(`basic`、`north_america`)は小文字だが、あれは業務の語で、地名ではない。値は配線(生成コードの文字列、JSON、SQL のガード)にもそのまま出るので、どの生成先でも書ける ASCII にした。
- **規則の中では、現地の綴りと ISO 3166-2 の符号でも書ける。** 現地の綴りは CLDR のその国の言語の名前(中国語の簡体字、繁体字の台湾、韓国語、ヒンディー語、フランス語、スペイン語、イタリア語、ドイツ語、ポルトガル語)で、空白とハイフンを `_` にする(`Bayern`、`北京市`、`서울특별시`、`Île_de_France`、`São_Paulo`、`महाराष्ट्र`)。加えて、よく使う短い形と公用の別の言語の名前を足した。中国の省・市・自治区の接尾辞を落とした形(`北京`、`广西`)、香港とマカオの正式の名、台湾の `臺` を `台` にした形(`台北市`)、韓国の略称(`서울`、`경기`、`충북`)と 2023 年・2024 年に改まった正式の名(`강원특별자치도`、`전북특별자치도`)、スペインのカタルーニャ語・バスク語・ガリシア語の名(`Catalunya`、`Euskadi`)、イタリアの `Trentino_Südtirol` と `Vallée_d’Aoste`、ウェールズの `Cymru`、ドイツ語の ü を `ue` にした形(`Baden_Wuerttemberg`)、アクセントを落とした現地の綴り(`Andalucia`)。符号は国の接頭辞を除いた部分(`US-CA` の `CA`)で、文字で始まるものだけを書ける。数字で始まる符号(日本の `JP-13`、韓国の `KR-11`、イタリアの `IT-21`、フランスの `FR-20R` と海外の `FR-971` など)は、名前として読めない(`60cm` と同じく数として読まれる)ので外した。日本の都道府県は、日本語の値と凍結した ASCII の名前の二つで書ける。
- **同じ区分を指す綴りは、同じ値。** パーサのあと、`apply` の展開のあとに、取り込んだ区分のほかの綴りを、その値に書き換える(`prelude::normalize`)。完全性も重なりもベクタも証明書も生成コードも、一つの区分を一つの値として見る。書き換えないのは、ファイルの中のどれかの列挙の値である語と、宣言した名前である。規則が自分で `enum region = CA | NY` を宣言していれば、その `CA` はその列挙の値のままである。取り込んだ二つの国が同じ綴りを持つ語(`WA` はワシントン州と西オーストラリア州、`SH` はシュレースヴィヒ=ホルシュタイン州と上海)は E012 にして、値の名前で書いてもらう。列の型を見て決める案は捨てた(下)。
- **`std/都道府県` と `std/jp/prefectures` は同じ 47 の区分で、綴りだけが違う。** `std/都道府県` は値を日本語で(`東京都`)、`std/jp/prefectures` は英語で(`Tokyo`)持ち、どちらで取り込んでも両方の綴りを書ける。生成コードはどちらも `Prefecture` で、値の名前も同じ(`Tokyo`)。違うのは、規則の中の列挙の名前(`都道府県` と `jp_prefecture`)と、出力と配線に出る値の綴りだけである。日本語の規則の出力が一字も変わらないのはこのためで、英語の双子が英語の値を出せるのもこのためである。一つの規則で二つとも取り込むと、一つの生成の型に二つの列挙ができるので、E013 にした。`apply` の呼び先がもう一方の名前で取り込んでいれば、呼ぶ側の名前に寄せる。呼び先の取り込みを足さず、入力の読み替えは区分どうしで対応づけ(`with` を書かなくても `東京都` は `Tokyo` を読み替える)、呼び先のセルは呼ぶ側の綴りに書き換わる。読み替えの値と `with` の両側も、どの綴りで書いてもよい。
- **生成コードの型と値の名前は、`Prefecture` と同じく凍結して `prelude.rs` に持つ。** 型は `UsState`、`GbNation`、`CnProvince`、`TwDivision`、`KrProvince`、`InState`、`FrRegion`、`EsCommunity`、`ItRegion`、`DeState`、`AuState`、`BrState`(日本は `Prefecture` のまま)。値は、値の名前の `_` を落として語の頭を大文字にしたもの(`New_York` は `NewYork`、`Ile_de_France` は `IleDeFrance`)で、規則が宣言した列挙の値と同じ作り方である。各生成先はそこから自分の書き方を作る(Python の `NEWYORK`、Swift の `newYork`、Go の `UsStateNewYork`、proto の `US_STATE_NEWYORK`)。生成先の全部(12)で、`Prefecture` と同じ道を通る(`codegen.rs` の `enum_names` と `value_names`、Rust の `enum_members`)。
- **名前のもとは Unicode CLDR 48.2 の地方の区分の名前で、各国の一覧と突き合わせた。** `common/subdivisions/` の英語とその国の言語、どの区分がどの国に属するかは `common/supplemental/subdivisions.xml`。ライセンスは Unicode License V3 で、文をクレートの `THIRD_PARTY_NOTICES` に置き、`Cargo.toml` の `include` に足した。2026-10-06 に `Cargo.toml` の `license` を `(MIT OR Apache-2.0) AND Unicode-3.0` にした。rulec を含む `ritsu` のバイナリを配るもの(リリースのアーカイブ、`.deb`・`.rpm`、Homebrew)には根の `THIRD_PARTY_NOTICES` が入り、このファイルの文は、その CLDR の節にそのまま載っている(ritsu の DESIGN 13.2。`crates/ritsu/tests/release.rs` が、一字も違わないことを確かめる)。取ったものは表として `prelude.rs` に書き、実行時には何も読まない。テストもネットワークに頼らない。突き合わせで CLDR から離れたところは次のとおり。
- 米国の五つの準州(`AS`、`GU`、`MP`、`PR`、`VI`)は、CLDR の区分の英語の名前に無い(国・地域の名前として持っている)ので、`common/main/en.xml` の地域の名前を使った。
- 台湾の `CYI` と `CYQ`:CLDR は `CYI` を嘉義県、`CYQ` を嘉義市にしているが、ISO 3166-2:TW はいま `CYI` が嘉義市、`CYQ` が嘉義県である(嘉義県の符号が `CYI` から `CYQ` に替わった)。ISO に合わせた。市と県が同じ名の二組は、英語の名前に `City` と `County` を付けて分けた(`Chiayi_City`、`Chiayi_County`、`Hsinchu_City`、`Hsinchu_County`)。片方だけの `Hsinchu` は、どちらとも読めるので綴りに入れない。
- 区分の種類だけを言う語が、その国の一覧でほかの名前に付いていないものは外した(`Gwangju City` は `Gwangju`、`Murcia Region` は `Murcia`、`Madrid Autonomous Community` は `Madrid`、`Navarra Chartered Community` は `Navarra`、`Guyane (française)` は `Guyane`)。CLDR の長い形は綴りとして残した。
- CLDR の名前に付いている重複の印 `²`(`Île-de-France²`、`La Rioja²`)は落とした。
- 韓国の `강원도` と `전라북도` は CLDR では旧称で、いまの正式の名を綴りに足した。符号(`KR-42`、`KR-45`)は数字なので、どちらにしても書けない。
- 日本は、CLDR の英語の名前のダイアクリティカルマークを落とすと、凍結した 47 の ASCII 名と一つ残らず一致し、CLDR の日本語の名前も 47 の値と一致した。
- **検査の案内。** 取り込んだ区分の値ではない語がセルや群にあると、E012 の注が、似た綴り(`Calfornia` には `California`)か、その語を値に持つ別の国の名前空間(`std/us/states` だけを取り込んだ規則の `Bavaria` には `std/de/states`)を言う。両方の国を取り込んでいれば、別の列挙の値として E103 になる(いままでの形)。無い名前空間を取り込むと、E013 の注が近い名前(`std/us/state` には `std/us/states`)と、組み込みの名前空間の一覧を言う。`explain` の E012 と E013 の説明を直し、E013 には英語の再現(`import std/us/state`)を足した。日本語の再現は `import std/都道府`(近い名前に `std/都道府県` を挙げる)にした。
- **英語の双子は英語の綴りにした。** 三つの双子の取り込みを `std/jp/prefectures` に、型を `jp_prefecture` に、値を `Tokyo`、`Hokkaido`、`Okinawa` などにした(日本語の規則は変えていない)。双子の対の確かめ(`tests/twins.rs`)は、値の文字列が対の中で一つの対応になることを見るので、そのまま通る。双子の W105 の golden(`W105-b-twin`、`W105-twin` の JSON)は、証人の `dest` が英語になった。
- **英語のページの都道府県を英語にした。** 例のページ(`make_examples.py` が双子から作る)、案内(`tour.md`)、確かめ方(`checks.md`)、生成のページ(`generate.md`)、生成コードの文書(`docs/generated-code.md`)、リファレンス(`docs/reference.md`)に貼った双子の出力と抜粋を、双子に走らせ直して貼り直した。リファレンスの組み込みの節は、十三の名前空間と綴りの決まりを書いた。
**捨てたもの**:
- **英語の値を CLDR の名前のまま(空白・ハイフン・アクセントつき)にして、規則では引用符で書くこと。** 列挙の値は名前で、引用符の値は文字列である。文字列の列は前方一致しか持たない(§15.101)。
- **値を小文字のスネークケース(`new_york`)にすること。** 凍結した都道府県の名前(`Tokyo`)と並ばない。
- **列の型を見て綴りを書き換えること。** 書き換えを型検査の中に入れることになり、群・例・`apply` の読み替え・`fold` の腕・状態機械の行のそれぞれで、列の型が決まる前に書き換えが要る。二つの国を取り込んで、しかも同じ符号を書く規則だけが困る形にして、名前で書いてもらうほうが、規則を読む人にも分かりやすい。
- **`std/jp/prefectures` の値も日本語にして、`std/都道府県` の完全な別名にすること。** 英語の双子は英語の綴りで書けても、出力(ベクタ、`doc`、配線)は日本語のままになり、英語のページの日本語が減らない。
- **短い形を自動で作ること(接尾辞を落とす規則を、どの国にも当てる)。** 韓国の `충청북도` から `도` を落とすと `충청북` になり、略称の `충북` にならない。短い形は国ごとに、使われているものだけを表に書いた。
- **インドの綴りをヒンディー語だけにすること、または入れないこと。** インドは英語も公用語で、英語の名前がそのまま現地の名前でもある。ヒンディー語の名前(デーヴァナーガリー)は、名前の最初の字が文字で、続く字が区切りでなければ名前として読める(`lex.rs`)ので、入れた。
**正直な限界**:
- 綴りは規則の中でだけ効く。生成コードと `verify`・`replay`・`fixtures lint` が受け取る入力は、値の綴り(英語、`std/都道府県` なら日本語)だけである。配線の値を一つにしておくためである。
- 区分は 2026-10-04 時点のもので、CLDR と各国の一覧が改まれば、表を取り直す。値を足すことは、§2.1 の閉じた列挙の約束どおり、既存の表を完全性検査で割る。
**見つけたこと**(直したもの):準用の呼び先の記録(`Callee.enums`)は、呼び先が宣言した列挙だけを持ち、取り込んだ列挙を持っていなかった。呼び先の入力が `std/都道府県` の値をとると、呼ぶ側の同じ型の入力で読み替えても、47 の値が全部「呼び先の値にあたらない」として E042 になり、組み込みの列挙を入力にとる規則は準用できなかった(替える前のバイナリで確かめた)。検査器が持つ列挙の全部(取り込んだものを含む)を記録するようにした。宣言した列挙だけの呼び先(コーパスの `退職手当` と `retirement_pay`)では、記録は前と同じである。
**確かめたこと**:`tests/regions.rs` の 8 件。名前空間ごとの値の数(決定に書いた数。検査器が持つ列挙の値の数も同じ)。一つの綴りが一つの区分を指し、名前として読めること。英語の名前・符号と現地の綴りを混ぜて書いた規則と、現地の綴りだけで書いた規則が、値の名前で書いた規則と同じベクタを出すこと(十四の取り込みの全部)。生成先の全部(12 と Connect の `.proto`)の型と値の名前が凍結した表のとおりであること。別の国の区分(`Bavaria`、`北京`)、両方の国を取り込んだときの別の列挙の値、似た綴り、二つの国の同じ符号(`WA`)、規則が自分で宣言した `CA`、一つの国の二つの名前、近いパスの診断。都道府県をどちらの名前で取り込んでも同じケースで綴りだけが違い、生成コードも同じ `Prefecture` であること。準用で同じ国の別の名前を区分で読み替えることと、同じ取り込みどうしの準用が通ること。`THIRD_PARTY_NOTICES` と本節があること。替える前と後のバイナリで、コーパスと変異の規則 196 本に 11 のコマンド(`check`、`check --format json`、`check --show-shadow`、`doc`、顧客向けの `doc`、`coverage`、`vectors`、`api`、`schema`、`graph`、`certificate`)を英語と日本語で走らせ、コーパス 87 本を `gen` した出力(4,486 ファイル)を突き合わせた。日本語の規則は、変異二本のほかは一字も変わらない。変わったのは `m_e013.rule`(E013 の注。直すと決めた文)と `m_e012group.rule`(群の `大阪` に「`大阪府` のことですか。」の一行)だけである。英語の双子三本の 246 ファイルは、108 が同じ、81 が綴り(値、列挙の名前、取り込みのパス、原本のハッシュ)だけの違い、54 が列挙の並び(列挙の名前の順なので、`jp_prefecture` が `member_kind` と `size_class` の前に来る)だけの違い、3 つの `.proto` は綴りと、値が値の名前と同じになったので消えた注(`// 北海道`)だけの違いだった。英語のページ 19 枚の都道府県の日本語の連なりは 109 から 6 に(日本語の連なり全体は 321 から 219 に)減った。残る 6 は、日本語の名前空間 `std/都道府県` の名前と、その値の綴りを説明する所である。全体(`cargo test -p rulec -p ritsu -p dandori`、dandori の重い十二を除く。PostgreSQL と Lean の層つき)は、99 の試験バイナリで 955 件が通り、落ちたものは 0、ignored 2(前からのもの)、SKIP の行は無かった。そのあと準用の二つ(区分での読み替えと、呼び先の取り込んだ列挙の記録)を足し、関わる `regions`・`apply`・`twins`・`golden`・`golden_en`・`codes` と、作り直した二つの wasm を読む試験(rulec の `wasm`・`website`、ritsu の `playground`・`website`)を回し直して通った。替える前と後の突き合わせも、そのあとのバイナリで取り直し、変わらなかった。
### 15.183 ページを読む人の言い方と、出力の中の節の番号(2026-10-04)
**きっかけ**:`rulec doc` のページを読むのは、承認する人に限らない。コードが実現すべきものを理解し、確かめるために読む人(事業を回す人、経理や法務、運用する人、コードを見る開発者)である。ritsu の根の README と DESIGN はもうそう書いていて、rulec の文書と出力は「承認する人」「approver」と言っていた。あわせて、利用者に見える出力に設計の文書の節の番号(`§15.59` など)があり、E032 の注の日本語も不自然だった(ブラウザで試すページで E032 を開くと、どちらも目に付いた)。
**決定**:
- **ページは、英語では "the page for people"、日本語では「人が読むページ」(Markdown は「人が読む資料」)と言う。** 読む人は、英語では "readers"、日本語では「読む人」「確かめる人」。`--help`、W1xx の直し方の文、MCP のツールの生成物(隣のページの説明とコメント)、AGENTS.md とスキル、README、docs、§1.6、サイトのページとシナリオの図(「Approver」の札は「Reader」「読む人」、「and signs off」「承認する」は「and checks it」「確かめる」)、トップの流れと全体の図(「approves the rule」「ルールを承認する」は「checks the rule」「ルールを確かめる」。図の URL の刻印は `778336e2` から `cbbcdb93` に変わった)、コードのコメントを直した。チームで規則を承認する決まりがあるなら、その承認にもページが使える(§1.6)。
- **替えなかったもの。** CLI の値 `--audience approver`(利用者のスクリプトが使う名前。`--help` は「既定は approver で、人が読む資料になる」と説明する)。規則を決める人の役割としての承認(AGENTS.md と README の「someone approves the table」、§0.1 の「承認する人がいて」、サイトの「Does a person approve the answer」)。`source pin` でコピーの変更を受け入れること(「この内容で承認するなら」)と、E037・E043 の承認。例の中の業務の承認(Stripe の `approved` など)。日付の入った記録(この 15 章)と、テストの関数の名前。
- **利用者に見える出力には、設計の文書の節の番号を書かない。** 診断の文と注、`explain` の台帳、`--help`、`doc` のページ、生成したコードのコメントと文字列から外した(286 か所。番号を外すと意味が欠ける所は、中身を一言で言った:丸めの補助の見出しは「丸めの五つのモード」、関数の呼び出しの直し方は「以前の版では」、`diff` が入力を作れなかった組み合わせの説明は、英語を日本語の言い方にそろえた)。この文書、README、docs、コードとテストのコメントの中の番号は、そのまま。法令の節(`§1910.157`)は引用なので、そのまま。
- **E032 の注は、列挙の書き方に合わせる。** 値を ASCII の名前で書く列挙(`received | paid | cancelled_in_ordering(cancelled)` のように、別名の付いた値が混じっていてもよい)には「この列挙に `returned` を足します」、値の名前を日本語などで書いて別名を付ける列挙には「`<名前>(returned)` の形で足します。名前は自分で決めます」。前は、どちらにも別名の形と「日本語はそのファイルに入っていません」を出していた。最後の注は「契約(order.proto)に値が増えました。この規則がその値をどう扱うかは、まだ決まっていません」で、増えた値に金額を付けると決めてかからない。契約にだけある値があるときだけ出す(前は、契約から値が消えただけのときにも出ていた)。E033 の台帳の文も、「既定の額」を「既定の行の答え」、「額を決めた人」を「扱いを決めた人」にした。
**捨てたもの**:`--audience approver` の値を替えて、前の値を別名として残すこと。値は利用者のスクリプトとの約束で、言い方の問題は `--help` の説明で足りる。
### 15.184 「断る」と「写す」を、何が起きるかで言う(2026-10-05)
**きっかけ**:rulec の日本語は、生成コードやツールが入力を受け付けないことを「断る」と言い、出典を手元に保存したものを「写し」と呼び、それを取ってきて置くことと、人が文書を読んで数字を規則に書くことの両方を「写す」と言っていた。日本語では、プログラムが入力を「断る」とも、ファイルを「写す」とも、普通は言わない。そのうえ「写す」は、保存(fetch)と転記(transcribe)という別々のことを一語で言っていた。
**決定**:何が起きるかで言う。
| 前 | 後 |
|---|---|
| 写し(出典を手元に保存したもの。英語の copy) | コピー(文から出典だと分からないときは「出典のコピー」。E116・E119 の見出しは「引いた出典のコピー」) |
| 写す(出典を取ってきて手元に置く) | 保存する |
| 写す(文書の数字や文を規則に書く。英語の transcribe)、写し間違い、写し元 | 転記する、転記の誤り、転記元 |
| 写す(ファイルや値を複製する) | コピーする |
| 断る(生成コードが、範囲・制約・列挙の外の入力を) | 受け付けない |
| 断る(ツールや外のプログラムが、読めないものや誤った引数を) | エラーにする(`invalid_argument` を返す、400 を返す、のように返すものが決まっているときはそれを書く) |
| 断り書き | 注記 |
| 書き写す | 転記する、書く、書き直す(文脈で選ぶ) |
診断の文と注(E019、E037〜E039、E116、E119、E122、W120 ほか)、`explain` の台帳と `docs/codes.ja.md`、`source` の `--help`、`doc` のページ、生成コードのコメント(Java の `RuleInputError`、Connect のサービス)、`coverage` と `test` のまとめの行(「受け付けない入力」)、`replay` の「途中でエラーになる」、この文書、実験の文書、サイトの日本語のページと図、テストの名前と材料のコメントを直した。コーパスの `予約取消可否.rule` の説明は「予約を取り消せるか、取り消せないか、担当者に回すか」にした(判定の値 取消可・取消不可・担当者へ と同じ並び)。英語は一字も変えていない。
**残したもの**:実行の記録(`experiments/point-in-time/reports/*.log`)。そのときの出力の記録なので、書き換えない。§15.82 の「誤写」は、二つの語の形には当たらないが、同じ転記の誤りのことなので「転記の誤り」にそろえた。
**捨てたもの**:生成コードの入口の「断る」を「拒否する」にすること。拒否は、一式の中では chobo が操作を受け付けないこと(理由の名前が付く)に使う語なので、混ぜない。
### 15.185 前のブラウザで試すページを、ritsu のページへの転送に替える(2026-10-05)
**きっかけ**:ritsu のリポジトリを公開し、サイトを `https://i2y.github.io/ritsu/` に切り替えた。ritsu のブラウザで試すページは、このサイトのページが開いた五つの規則を英語と日本語で開き、規則を一つだけ試すための空のプロジェクトも持つ(ritsu の DESIGN 8.7)。前のページは、ritsu のページに例を全部持たせたうえで、切り替えのときに ritsu のページへの転送に替えると決めていた(ritsu の DESIGN 13.2)。
**決定**:
- **`website/rulec/docs/playground.md` と `docs-ja/playground.md` は、ritsu のページへ送るだけのページにした。** 英語のページは `/ritsu/playground/` へ、日本語のページは `/ritsu/ja/playground/` へ送る。前のページは URL を読まず、共有の形も持たなかったので、来たリンクにかかわらず、前のページが開いていた一行足りない表(`#project=rulec/gap`、日本語は `#project=rulec/gap.ja`)を開く。ただし `=` を含むリンク(ritsu のページが読む形)で来たときは、それをそのまま渡す。ページは、移ったことを言う一文と ritsu のページへのリンク(`id="moved"`)と、そのリンクの先へ `location.replace` で移るスクリプトだけを持つ。スクリプトが動かなくても、リンクから行ける。リンクはサイトの中の相対のパスで、Markdown のリンクと同じくページのファイルから読む形(`../playground/#…`)で書く。Zensical は Markdown の中の HTML の `href` も同じように読み、一つ深いディレクトリに出したページでは `../../playground/#…` に書き直す。
- **ナビの「ブラウザで試す」とトップページのボタンは、ritsu のページを直接指す**(転送のページを通さない)。ナビのリンクは Zensical がサイトのルートからたどってページごとに書き直し、ページの中のリンクはページのファイルからたどる。転送のページはナビから外した。
- **前のページだけが使っていたものを消した。** `src/wasm.rs`(ページとの境目の六つの関数。ritsu の wasm も、使わない `rulec_*` の関数を出していた)、`website/rulec/docs/playground/` の `playground.js`・`playground.css`・`rulec.wasm`、`website/rulec/tools/make_wasm.sh`、`sync.sh` が日本語の木へコピーしていたところと、その gitignore の行。`src/` のモジュールは 52 になった(README の数も直した)。
- **確かめていたことは、ritsu のテストに移した。** `tests/wasm.rs` と、`tests/website.rs` の二つ(ページの表が絵の表と同じこと、サンプルがコーパスの規則と同じこと)を消した。ritsu の `tests/playground.rs` が、前のページのファイルではなく、もとのファイル(`website/rulec/tools/overview.rule`・`overview-ja.rule` とコーパス)に対して、ritsu のページの五つの規則を英日で確かめる。`tests/wasm.rs` がわざと選んでいた規則(消去で答えが決まる `クーポン併用` とその英語の双子、決まらない `m_w114`、℃ より後ろに出る指摘。`^` の位置が ℃ の幅の数え方で変わる)は、ritsu のページの答えをバイナリと、ritsu の wasm をライブラリと突き合わせる編集に入れた。`tests/website.rs` は、書き下ろしのページから `playground.md` を外し、サイトの外(ritsu のサイト)へ出るリンクは、公開先の URL でたどって ritsu のページがあることを確かめる。転送のページは、ritsu の `tests/website.rs`(リンクとスクリプトとナビ、組んだ木)と `tests/playground.rs`(Chrome で、送られた先が同じ中身を開くこと)が確かめる。
- **E129 の注と台帳から「ブラウザで試すページ」を外した。** koyomi をつないでいない rulec は、いまは rulec のクレートのバイナリだけで、ritsu のページは `ritsu check` と同じく koyomi をつなぐ(§15.174)。`docs/codes.md`・`codes.ja.md`、`docs/reference.md` とスキルのコピー、E129 の golden を直した。
- ライブラリのモジュールを公開する理由(`lib.rs` と `docs/compatibility.md`)の「プレイグラウンド」は、「ritsu(コマンドとブラウザで試すページ)」にした。
**捨てたもの**:
- **前のページを残すこと。** 例も、規則を書いて試すことも ritsu のページにそろったので、残すと、二つのページと二つの wasm を同じに保つ手間だけが残る。
- **`src/wasm.rs` をフィーチャーの陰に置くこと。** 使うものが無い。要るようになれば、履歴から戻せる。
- **ナビとボタンを転送のページに向けること。** 読む人が毎回、移ったことを告げるページを通ることになる。
**確かめたこと**:`cargo test -p ritsu -p rulec -p dandori`(dandori のプラットフォームのテストを除く)の全体で 959 件が通った(rulec 774、ritsu 96、dandori 89。落ちたもの 0、ignored 2、12 分 55 秒)。SKIP の 15 行は、どれも rulec の Lean の確かめ直しで、`proofs/` を組まずに回したためだった。`proofs/` で `lake build` してから `tests/lean.rs`・`days.rs`・`machine.rs` を回し直し、42 件が SKIP なしで通った。`rulec explain --all` から `docs/codes.md` と `codes.ja.md` を作り直すと、変わるのは E129 の一行ずつだった。ritsu の `website/build.sh` で組んだサイトを `/ritsu/` の下で配り、英語と日本語の転送のページを headless の Chrome で開いて、ハッシュ無しのリンクが一行足りない表を、ritsu のページの共有のリンク(`#project=rulec/full&view=gen`)がそのままの中身を開くことを見た。
### 15.186 配り方を ritsu のリリースに移し、rulec だけのリリースのファイルを消す(2026-10-05)
**きっかけ**:ritsu の最初のリリース(0.23.0)が出た。アーカイブ、`.deb`・`.rpm`、formula(`i2y/tap/ritsu`)、action(`uses: i2y/ritsu@v0.23.0`)のどれもが、`rulec` を `ritsu` へのリンクとして持つ。tap は `Formula/rulec.rb` を消し、`formula_renames.json` で `rulec` を `ritsu` に付け替えた(ritsu の DESIGN 13.2)。一方で README と入れ方のページは、Homebrew、`.deb`・`.rpm`、アーカイブ、action を rulec 自身のリリースのものとして案内し、ritsu の最初のリリースが引き継ぐと書いていた(§15.181)。このクレートには、rulec だけのリリースを作るファイル(`release.yml`、`ci.yml`、`packaging/`、`action.yml`)も残っていた。
**決定**:
- **入れ方の案内を ritsu のリリースにした。** README、入れ方のページ(英日)、互換の約束(`docs/compatibility.md`・`compatibility.ja.md` とスキルのコピー)を直した。Homebrew は `brew install i2y/tap/ritsu`、パッケージは `ritsu_<版>-1_<arch>.deb` と `ritsu-<版>-1.<arch>.rpm`(`rulec` のパッケージを置き換える)、アーカイブは `ritsu-v<版>-<target>.tar.gz`(`ritsu`、リンク七つ、ライセンス二つ。PATH にあるディレクトリに `tar -xzf … --exclude 'LICENSE-*'` で展開する)、CI は `uses: i2y/ritsu@v0.23.0` である。入れ方のページには「rulec 自身のリリースから移る」の節を足した。Homebrew の移り方(`brew install i2y/tap/ritsu` のあと `brew migrate ritsu` と `brew upgrade ritsu`、または `brew trust --formula i2y/tap/ritsu` のあと `brew upgrade`)、パッケージとアーカイブで古い `rulec` がどう置き換わるか、`uses: i2y/rulec@v0.22.1` が rulec のリポジトリが残るあいだは動くこと、を書いた。互換の約束の 7 の名前と CI の例も、`i2y/tap/ritsu` と `uses: i2y/ritsu@v1.0.0` にした。ページから rulec のリポジトリのリリースへのリンクは無くなり、ritsu の `tests/website.rs` は、言語のリポジトリのリリースへのリンクも落とすようになった。
- **貼った出力は、0.23.0 のリリースの実物で取った。** アーカイブ(macOS の arm64)は、ページのコマンドでダウンロードし、`SHA256SUMS` と突き合わせ、使い捨てのディレクトリに展開して `rulec --version` を走らせた。`.deb`・`.rpm` は、ダウンロードと、Debian のコンテナの GNU の `sha256sum -c` での突き合わせまでを走らせた(macOS の `sha256sum` は、標準入力から `-c` の一覧を読めない)。`apt install` と `dnf install` は走らせていないので、出力を貼っていない。Homebrew は、作者の Homebrew とは別の場所に置いた使い捨ての Homebrew 7.0.6(`HOME` も別)で、三つの場合を走らせた。何も入っていないところへの `brew install i2y/tap/ritsu`。tap を rulec 0.22.1 の時点に戻して rulec を入れ、tap を今の状態に進めたあとの `brew install i2y/tap/ritsu`・`brew migrate ritsu`・`brew upgrade ritsu`。同じ状態からの `brew trust --formula i2y/tap/ritsu`・`brew upgrade`。どれも最後の `rulec --version` が `rulec 0.23.0` を返した。二つ目の `brew install` は formula を信頼し、rulec がすでに入っていて移していないと警告して、`brew migrate ritsu` を案内する。古い名前の `brew install i2y/tap/rulec` は、formula を信頼する前は `Refusing to load formula i2y/tap/ritsu from untrusted tap i2y/tap` と言って止まり(名前を変えた先の formula は、古い名前で指しても自動では信頼されない)、`brew trust --formula i2y/tap/ritsu` のあとなら ritsu を入れる。ページの Homebrew の節に、そう書いた。古い `rulec` のファイルがある場所にアーカイブを展開すると、リンクに置き換わることは、macOS の tar と GNU tar の両方で確かめた。
- **rulec だけのリリースのファイルを消した。** `.github/workflows/release.yml`、`.github/workflows/ci.yml`、`packaging/`(`homebrew.sh`、`linux.sh`、`nfpm.yaml`)、`action.yml` である。`ci.yml` は丸ごと消した。GitHub はクレートの中のワークフローを走らせず、中身は ritsu の根の `tools`・`proofs`・`kani`・`packages` に移してある(ritsu の DESIGN 10.5)。もう無い `./website/sync.sh` を呼んでいたので、走らせても落ちる。テストと文書でこれらを読むものは無かった。`experiments/kani/README.md` が挙げていた `ci.yml` の段だけを、ritsu の `kani.yml` の同じ段に替えた。
- **`Cargo.toml` の `[package.metadata.binstall]` を外した。** `pkg-url` は `{ repo }`(いまは ritsu のリポジトリ)の `rulec-v<版>-<target>.tar.gz` を指していて、ritsu のリリースにその名前のアーカイブは無い。ritsu のアーカイブの `rulec` は `ritsu` へのリンクなので、`rulec` だけを取り出しても動かない。crates.io にも出さない(ritsu の DESIGN 13.2。このクレートは、出していない ritsu の中のクレートに依存する)。入れ方のページと README のコメントの中の `cargo binstall` も消した。
- **`experiments/library/` の CI の見本を `uses: i2y/ritsu@v0.23.0` にした。** 規則のライブラリを外のリポジトリに出したときに走らせる見本で、ritsu の CI ではない。rulec のリリースのたびに README と一緒に版を上げてきた行で、新しい rulec はもう ritsu のリリースにしか入らない。`uses: i2y/rulec@v0.22.1` のままでも動くが、ライブラリが 0.22.1 から上がれなくなる。リリースの rulec 0.23.0 が、ライブラリの五つの規則で `fmt --check` と `check` を通すことを確かめた。`check.yml` の頭のコメント(rulec の `action.yml`、0.5.0 のあとのリリース)も直した。
- §15.181 の「Homebrew、`.deb` と `.rpm`、リリースのアーカイブ、`uses: i2y/rulec@v0.22.1` は、いまの rulec のリリースのものとして残し」は、これで替わった。§12 の配布の段落も、ritsu のリリースの形に書き直した。§15.42 と §15.158 は、0.22.1 までの形の記録として残す。
**捨てたもの**:
- **`ci.yml` の `packages` のジョブだけを消し、ほかのジョブを残すこと。** 残るジョブも GitHub は走らせず、同じことを根のワークフローがしている。残すと、読む人がどちらを正とするかで迷う。
- **binstall の `pkg-url` を ritsu のアーカイブに向け直すこと。** `rulec` の名前の中身はリンクで、`ritsu` がそばに無ければ動かない。binstall で入れられるようにするなら、crates.io に出すと決めたときに、`ritsu` のクレートの側で決める。
- **ページに rulec のリポジトリのリリースへのリンクを残すこと。** ページは、これから入れる人のためのもので、0.22.1 を取りに行く人はそのリポジトリを知っている。
**確かめたこと**:`cargo test -p rulec -p ritsu`(`proofs/` を組み、PostgreSQL を立てて)の全体で、872 件のうち 871 件が通った。落ちた 1 件は `tests/website.rs` の `言語の数を書いた文は登録簿と合っている` で、入れ方のページに書いた「seven languages」(ritsu の言語の数)を、生成先の言語の数を書いた文として読んだ。ページを言語の数を言わない文に直し、ページと DESIGN を読む六つのテスト(rulec の `website`・`docs`・`skill`、ritsu の `website`・`readme`・`skill`)を回し直して、全部通った。SKIP の行は 0 だった。
**入れ方の出力(0.38.0 で取り直した。2026-10-30)**:README、`AGENTS.md`(とスキルのコピー)、入れ方のページ(英日)に貼った出力と版と、`experiments/library/` の CI の見本の版を、0.38.0 のリリースの実物で取り直した。THIRD_PARTY_NOTICES が 0.24.0 から入ったことを言う文と、0.23.0 の歴史を言う文は残した。
- アーカイブ(macOS の arm64)は、ページのコマンドのとおりにダウンロードして `SHA256SUMS` と突き合わせ(`ritsu-v0.38.0-aarch64-apple-darwin.tar.gz: OK`)、使い捨ての HOME の `~/.local/bin` に展開して、`rulec --version` が `rulec 0.38.0` を返した。中身は、`ritsu`、リンク九つ、ライセンス二つ、THIRD_PARTY_NOTICES である。ページの英日のリンクの並びに、0.32.0 から入っている `kaname` が抜けていたので足した。
- `.deb` は Debian 13(amd64)の、`.rpm` は Fedora 43(x86_64)のコンテナで、ページのコマンドのとおりにダウンロードし、GNU の `sha256sum -c`(coreutils 9.7)で突き合わせ、`apt install` と `dnf install` で入れた。どちらも `rulec --version` が `rulec 0.38.0` を返し、入ったのは `/usr/bin/ritsu` とリンク九つ、`/usr/share/doc/ritsu` のライセンス二つと THIRD_PARTY_NOTICES だった。`.deb` は `rulec` を `Replaces`・`Provides`・`Conflicts` に、`.rpm` は `Obsoletes`・`Provides`・`Conflicts` に持つ。
- Homebrew は、2026-10-30 の時点で最新の 7.0.11 で、四つの場合を、場合ごとに新しい Homebrew で走らせた。何も入っていないところへの `brew install i2y/tap/ritsu`。古い名前の `brew install i2y/tap/rulec` を、formula を信頼する前と、`brew trust --formula i2y/tap/ritsu` のあと。tap を rulec 0.22.1 の時点に戻して rulec を入れ、tap を今の状態に進めてからの `brew install i2y/tap/ritsu`(rulec が入っていて移していないと警告する)・`brew migrate ritsu`・`brew upgrade ritsu`。同じ状態からの `brew trust --formula i2y/tap/ritsu`・`brew upgrade`(移すことと上げることを一度にする)。どれも最後の `rulec --version` が `rulec 0.38.0` を返した。信頼する前の古い名前は、`Refusing to load formula i2y/tap/ritsu from untrusted tap i2y/tap.` を Ruby の backtrace つきで出して止まり、`i2y/tap/rulec resolves to i2y/tap/ritsu` と言って `brew trust --formula i2y/tap/ritsu` を勧める。ページの文のとおりなので、ページは版だけを直した。
- 四つの場合を走らせたのは Linux(Debian 13 の arm64 のコンテナ、既定の prefix の `/home/linuxbrew/.linuxbrew`)である。取り直した macOS 27 には Xcode 26.2 が入っていて、7.0.11 は tap の formula を入れる段で `Your Xcode (26.2) … is too outdated.` と言って止まった(Command Line Tools も同じ)。入れる前の段(信頼する前の古い名前で止まること、tap ごと名前を書くと formula を信頼すること)は、Linux と同じだった。macOS で入れることは、リリースのワークフローの `homebrew` の段(macOS と Linux)が、0.38.0 の formula を `brew install` で入れ、九つの名前の `--version` が版と合うことを確かめて通っている。使い捨ての Homebrew を macOS に置くときは `/tmp` の外にする。`/private/tmp` の下では、入れる段で `Inherited sandbox permits writes to …/bin/brew` と言って止まる(7.0.8 で見た)。
- `experiments/library/` の CI の見本の三つのワークフローを `uses: i2y/ritsu@v0.38.0` にした。リリースの rulec 0.38.0 が、ライブラリの五つの規則で `fmt --check`・`check`・`coverage` を通すことを確かめた。
- 文書を読むテスト(rulec の `docs`・`mcp`・`readme`・`skill`・`website`・`library`、ritsu の `release`・`readme`・`sekisho`・`skill`・`website`)が全部通った。SKIP の行は 0 だった。
### 15.187 生成物の頭とページのスクリプトの外に、規則の文字列を出さない(2026-10-06)
**きっかけ**:生成物の頭のコメントには、規則のファイルの名前、出典のパスと URL(`source … = file "…" url "…"`)、準用する規則のパスが入る。どれも規則に書いた文字列で、rulec の文字列は `"` のほかにどの文字でも持てる。行を終える文字を書けば、コメントはそこで終わり、続きは生成したコードの行になる。U+2028 は TypeScript と JavaScript の行を、`\r` は Python の行を終える。`rulec check` は、そういう規則も通していた。もう一つ、`rulec gen` が書くページ(`<別名>_page.html`)は、生成した JavaScript を頭ごと `<script type="module">` に埋め込む。URL に `</script>` があると要素がそこで閉じ、続き(`<img src=x onerror=…>` など)が HTML として読まれて、ページを開いた人のブラウザでスクリプトが動いた。
**決定**:
- **頭の一行に書く文字列は、一行に収める。** ファイルの名前、出典のパスと URL、準用のパスにある、行を終える五つの文字(`\n`、`\r`、U+0085、U+2028、U+2029)を、`U+XXXX` の形(`U+000A`、`U+2028` など)にして書く。`\` も `\u` も含まない形にしたのは、Java と Scala 2 がコメントの中でも `\uXXXX` を読むからである(下)。ritsu-emit の `header::one_line` を使う。`Source:` の行は ritsu-emit の `Source::line` が、`Cites:` と `Applies:` の行は `codegen.rs` の `header` が通す(ritsu の DESIGN 9.2)。koyomi・chobo・dandori の頭も同じ関数を通る。
- **Java の頭は、文字列のバックスラッシュを二つにする。** Java は、コメントの中でも `\uXXXX` を、ほかのどの処理より先に読む(JLS 3.3。Scala 2 も同じ)。だから、出典の URL に六文字の `\u000a` と書くと、javac はそこを改行として読み、頭のコメントがそこで終わって、続きが Java の文になる。以前は、行を終える文字を `\u{2028}` と書いていたので、`\u` のあとが `{` で、javac が「不正な Unicode エスケープ」で止まりもした。`\` は、前に偶数個の `\` があるときだけエスケープを始める決まりなので、`ritsu_emit::header::for_unicode_comment` が `\` を二つにして、どの `u` の前の `\` も奇数個にする。行を終える文字はもう `U+XXXX`(`\` を含まない)なので、二つにするのは文字列がもともと持つ `\` だけである。rulec の `codegen/java.rs` が、頭をこの関数に通す。rulec のほかの出力先(Python、Go、TypeScript、Rust、Ruby、Swift、SQL、PHP、NumPy)と、koyomi・chobo・dandori の出力先は、どれもコメントの中で `\uXXXX` を読まないので、通さない。
- **Java の本文も同じにする。** rulec の字句は、名前に `\` を、文字列に `"` のほかのどの文字も入れられる。表の名前、行のラベル、列挙の値、`starts_with` の文字列は本文のコメント(分岐の上の `// 表 …`、分岐の行の `// 行 1 …`、列挙と規則と要素の `/** … */`)に入り、六文字の `\u000a` でコメントが終わって、続きが Java の文になった。`codegen/java.rs` の `jc` が、規則の文字列を入れるコメントの全部を `one_line` と `for_unicode_comment` に通す。文字列リテラルは、前から `java_str` が `\` を `\\` に、`"` を `\"` にしていたので、javac が `\u0022` を読んでリテラルを閉じることは無かった。記録の JSON のキー(`"\"<名前>\":"`)だけは名前をそのまま入れていたので、名前を JSON の文字列にしてから `java_str` に通す(`json_key`)。これで、名前に `\` があっても、記録は名前をそのまま返す。
- **ページのスクリプトは、中身をまとめてから ritsu-base の `docpage::script_text` に通す。** `</script`(大文字と小文字を問わない)を `<\/script` に、`<!--` を `<\!--` にする。JavaScript のコメント、文字列、正規表現の中では、どちらも元と同じ意味になる。ページの本文(出典を書いた `<p>` など)は、前から HTML のエスケープをしていた。chobo と dandori のページは、埋め込む JSON の `</` を前から `<\/` にしていた。
- **確かめ方**:`tests/heads.rs`。URL に U+2028、`\r`、U+0085 を入れた規則を `rulec gen` し、URL を書いたどの行もコメントで、行を終える文字を持たないこと(12 の出力先の頭のすべて)。Java は、ファイルの名前と URL に U+2028 と六文字の `\u000a` を入れた規則を生成して、生成物を javac にかけ、警告も誤りも無く通り、頭のコメントの外に何も出ないこと。本文は、表の名前、行のラベル、列挙の値、`starts_with` の文字列と出力の文字列に、六文字の `\u000a` と `\u0022`、`//`、U+2028 を入れた規則を生成し、ファイルのどの `\u` も前の `\` が奇数個であること、javac(`-Xlint:all`)で警告も誤りも無く通ること、ランナーに入力を渡すと、出力の文字列と、当てはまった表と行のラベルが、規則に書いたとおりに返ること(javac と java が無ければ飛ばす)。`</script>` と `<!--` を入れた URL で、ページのスクリプトが最後まで一つの要素であること。直しを外すと、どれも落ちる。
- **ふつうの規則の出力は変わらない。** コーパスの 87 の規則を英語と日本語で `rulec gen` した 10,140 のファイルが、0.23.0 のリリースの出力と一字も違わなかった。頭の書き方を `U+XXXX` に替え、Java の頭と本文のコメントの `\` を二つにし、記録のキーを JSON にしてからリテラルにしたあとも、ふつうの規則(行を終える文字も `\` も持たない名前・パス・URL・文字列)の出力は、替える前と一字も変わらない(コーパスの 87 の規則を英語と日本語で、替える前と後の版で生成した 10,140 のファイルを突き合わせた)。
**捨てたもの**:
- **行を終える文字を、字句解析で文字列に入れさせない(新しい診断にする)。** 危ない文字は出力先ごとに違い(JavaScript は U+2028、Python は `\r`)、問題は書き出すときに起きる。書き出す側の一か所で収めれば、これから足す文字列もそこを通る。
- **URL の形を検査して、使える文字を絞る。** パスも同じ行に入るので、URL だけを絞っても足りない。
### 15.188 入力を区間に限ったときに出力がとる値を、ritsu の口に答える(2026-10-06)
**きっかけ**:sekisho(ritsu の八つ目の言語。認可を書き、Cedar を生成する)は、規則の答えを Cedar のリクエストの context に入れ、許すか拒むかを全部の組み合わせで数える(sekisho の DESIGN 3.2)。数えるには、規則の出力がとりうる値を知る要がある。規則に渡す値がポリシーの条件にも出てくると(返金の額を 50 ポンドと比べる)、条件が切る区間ごとに、出力がとる値が変わりうる。sekisho の例の規則は `excess = amount - limit` を 0 と比べるので、上限が 100 ポンド以上なら、50 ポンドまでの返金は `within_limit` にしかならない。この問い(ritsu の口 `Rules::outputs_over`)に、rulec の解析で答える。
**決定**:
- 問いは、列挙か真偽の出力の名前と、入力ごとの区間(受け渡す整数で、日付は日の番号。両端を含み、None の端は宣言した範囲のまま)である。答えは、出力がとる値の一つ一つに、そこに届く入力を添えたもの(値を宣言した順)か、決められない理由である。列挙の値は公開名(規則が括弧の中に書いた別名、無ければ名前。`上限まで(within_limit)` なら `within_limit`)で返す。sekisho が Cedar に入れる文字列だからである。添える入力は `eval` がそのまま受け取る形(列挙の値は名前)にする。区間の中に入力が一つも無ければ、空の並びを返す。数の出力、並びを歩く規則、数でも日付でもない入力を区間に限る問いには、決められないと答える。
- 規則を、入力を区間に限って読み直す(`src/over.rs` の `with`)。型の検査が入力の範囲を区間との共通部分にし、導出と define の届く区間もその範囲から計算し直される。領域(§6)の入力の軸も同じ範囲で切る。`checked_over` が日付の入力を日の集合に限る(§15.174)のと同じ仕組みで、問いに答えるあいだだけ、そのスレッドで効く。`rulec check` の答えは変わらない。
- **上から**:出力を決める表の行のうち、ある入力が届きうるものを求める(`region::live_rows`)。行の升のうち、優先する行に覆われない点が一つでも、完全性の証明のふるい(koyomi の日、`constraint`、導出の届く区間、上の表、一次のモデル)に落とされずに残れば、その行は届きうる。数の列の升が、その列の届く区間(`Checked::ranges`:入力の範囲、導出と define の区間の計算、上の表が書く値)の外にあるときも落とす。開いた升は端を含まないので、届く区間が 0 で終わるとき `> 0` の升は外にある(ふるいは端を含めて読み、導出だけを見る)。E102 は行の升の集合と優先する行しか見ないので、E102 が通した行が、ここでは落ちることがある。上の表の列挙と真偽の出力は、表の順に、届きうる行が書く値だけに絞ってから、下の表を読む(`Checked::out_values`)。減るのは届かない行の値だけなので、下の表が受け取りうる値はどれも残る。
- **下から**:届きうる行が書く値の一つ一つに、区間の上の規則のベクタ(§9)から、そこに届く入力を探し、評価器で確かめる。
- 出力を決める表は、出力そのものの表か、`result` が名前一つで渡す表か、名前一つの define が渡す表(準用の出力は、展開のあとでそういう define になる。§15.69)である。式で計算する出力は、値の全部を候補にする。準用する規則を区間に限るときも、準用される規則は区間に限らずに検査する(`apply.rs` の `over::without`)。限るのは、展開してこの規則の表になったあとの行である。
- `src/over.rs` を足したので、`src/` のモジュールは 53 になった(README の数も直した)。
- 口の事実の列挙の値(`ritsu_ports::EnumValue`)に、公開名 `public` を足した(`codegen::public_value`)。これまでの `alias` は生成したコードの名前(`WithinLimit`)で、変えていない。dandori は値の名前を、yuen は名前と `alias` を読むので、どちらの答えも変わらない。
- 二つがそろえば、答えは正確である。届きうる行の値に届く入力が見つからなければ、その値と行を言って、決められないと答える。評価器が、解析の落とした行の値に届いたときは、食い違いとして何も決めない(テストでは一度も起きていない)。
**捨てたもの**:
- **区間ごとに範囲を書き換えた規則のテキストを作り、`check` を走らせる。** 範囲の端を元の単位の書き方に戻す要があり(率の刻み、万と億)、戻し方を誤れば別の規則を確かめることになる。値のまま型の検査と領域に渡せば、戻す要が無い。
- **E102 を、導出の届く区間とふるいで強める。** `rulec check` の答え(E102 の数と golden)が変わる。区間に限った問いでだけ使う。
**確かめたこと**:`tests/ports.rs` の六つのテスト。
- コーパスと、`tests/over/` と `tests/apply_fixtures/` の規則(並びを歩かず、文字列の入力の無いもの)の数と日付の入力を、ベクタの値のまわりの区間に限った。全部の入力を限る区間(幅は ±3 から始め、組み合わせが 2,000 通りを超えない幅まで狭める)と、一つの入力だけを ±40 に広げてほかを一点に置く区間である。区間の中の全部の入力(列挙と真偽の入力は全部の値。`constraint` を守るもの)を参照の評価器に流した答えと比べると、323 の問いのうち 322 が正確に答えられ、総当たりと同じ値になり、どの値にも区間の中の入力が添えてあった。そのうち 197 は、出力の値の全部より少ない値の答えだった。決められないと答えた一つは、下の `parity.rule` の、和が 3 になりうる区間である。文字列の入力を持つ二つの規則は、全部の入力を歩けないので比べず、組み合わせが多すぎる四つの区間も歩かなかった。
- sekisho の例の規則(`tests/over/refund_limit.rule` と日本語の版):額を 1〜50 か 51〜10,000 に限ると両方の値、上限を 100 以上にすると `within_limit` だけ、上限を 0 にすると `over_limit` だけ、`excess` がちょうど 0 になる組では `within_limit` だけになる。`tests/over/reach.rule` の、導出が届かない値だけをとる行は、`rulec check` を通る(E102 は出ない)が、問いでは落ちる。
- `tests/over/parity.rule`:同じ入力を読む二つの導出に、整数では同時にとれない値(和が 3 で差が 0)を求める行は、ふるいも一次のモデルも落とせず、届く入力も無い。問いは行を言って、決められないと答える。区間を和が 3 にならないところに限ると、正確に答える。
- `tests/over/apply_band.rule`:同じ名前の入力 `amount` を持つ規則(`size_band.rule`)を準用する規則。額を 1〜50 に限ると偽だけ、51〜100 なら真だけ、50〜51 なら両方になる。準用される規則まで区間の下で検査し直すと、その表の行が届かなくなって準用が通らず、問いは決められないと答える(`over::without` を外すと、このテストが落ちる)。
- 公開名:例の規則の英語の版と日本語の版で、`返金の区分` の値は名前が `上限まで` と `上限超え`、公開名が `within_limit` と `over_limit`、生成したコードの名前が `WithinLimit` と `OverLimit` になり、二つの版の `outputs_over` の答えは同じ公開名になる。コーパスでは、公開名が名前と違う値は、どれも規則の文に `<名前>(<公開名>)` と書いてある。
**列挙と真偽の入力も限る(2026-10-07)**:sekisho のゲートが、ポリシーも読む列挙の属性(注文の状態)や、列挙の定数(イベント `cancel`)を規則に渡すと、この問いは列挙を限れず、規則の出力を全部の状態とイベントの上で答えていた。sekisho はそれを多めに数え、規則の答えに頼る期待を決められないとしていた(ritsu のブラウザで試すページの通販のゲートで、出荷した注文はだれも取り消さないという期待。sekisho の DESIGN 3.2)。そこで、列挙と真偽の入力も限れるようにした。
- 問いの区間に、列挙の入力は値の位置(規則が宣言した順に 0 から。口の `RuleFacts::enums` と同じ順)を、真偽の入力は偽を 0、真を 1 として書く。同じ入力を二度以上書けば、それぞれの値を合わせたものに限る(ゲートの列挙が規則の列挙より値が少ないとき、位置が続かないことがある)。値が一つも無ければ、空の並びを返す。文字列の入力を限る問いには、これまでどおり決められないと答える(文は「数でも日付でも列挙でも真偽でもない入力なので、範囲に限れません」に替えた)。
- 領域では、限った入力の軸の、ほかの値の座標を全部の行から外す(`region.rs` の `reachable`。上の表が出さない値を外すのと同じ形)。その値だけを指す行は届かなくなる。ベクタも、限った入力にはその値だけを候補にする(`vectors.rs` の `candidates`)。どちらも問いに答えるあいだだけ効くので、`rulec check` と `rulec vectors` の答えは変わらない。
- 出力のセルが入力の名前を書く行(`| paid | pay | state |`。状態をそのまま返す)は、これまで、上からは出力の値の全部を候補にしていた。その入力の値のうち、行がその列のセルに書いた値(と、限った値)だけを候補にした(`over.rs` の `passed_on`)。下からは、ベクタが、セルが見分けない値の組(値のグループ `unconfirmed` が二つの状態をまとめるなど)からは代表の一つの値しか試さない(§9.1)ので、組のほかの値に届く入力が見つからないことがある。そうした値は、ベクタの、そのまま返す入力をその値に替えて、評価器で確かめる。
**確かめたこと**:`tests/ports.rs` に二つのテストを足し、一つを直した。
- 状態とイベントと急ぎかどうかを入力に持つ規則(`tests/over/order_state.rule` と日本語の版 `注文の状態.rule`):出荷済とイベント `cancel` に限ると、受け付けるか(`accepted`)は偽だけ、受付なら真だけ、入金済なら急ぎかどうかで分かれる。そこで急ぎを真に限ると偽だけ、偽に限ると真だけになる。受付と入金済にイベント `pay` を渡すと、次の状態(`next_state`)は入金済だけで、位置を一つの区間(0〜1)で書いても、二つの区間で書いても同じになる。受付と出荷済(位置が続かない)なら、入金済と出荷済。値の無い位置に限ると、空の並びになる。どの値にも、限った値の中の入力が添えてある。
- コーパスと `tests/over/`・`tests/apply_fixtures/` の、列挙か真偽の入力を持つ 15 の規則:ベクタの値に全部の入力を限った問いと、そこから列挙と真偽の入力を一つずつ自由にした問い(358)を、限った入力の全部を参照の評価器に流した答えと比べた。346 が正確に答えられ、総当たりと同じ値になり、どの値にも限った値の中の入力が添えてあった。決められないと答えた 12 は、どれも二つの日付の入力を比べる規則(`coupon_order.rule` と日本語の版 `適用順序.rule`)で、二つの日付を一点に限ったときに、行が比べる二つの日付の関係を、領域のふるいが読めないものである。§15.195 で、真偽の `define` がつねにとる値を `live_rows` が読むようにして、12 も決まるようになった。そのまま返す入力の値を替えて確かめる前は、`payment_intent.rule` の問いの一つも、値のグループのために決められないと答えていた。
- 答えられないときに理由を言うテストは、列挙の入力を限れないことを確かめていた。文字列の入力を限れないことを確かめるように替えた。
- 数と日付の入力を区間に限るこれまでのテストは、足した二つの規則の分を含めて 327 の問いのうち 326 が正確で(決められない一つは `parity.rule`)、出力の値の全部より少ない値の答えは 197 のままだった。
- 直す前と後で、コーパスの全部の規則の `rulec check`(英語と日本語)と `rulec vectors` が一字も変わらないことを確かめた。木にある `.gate` の全部(143)の sekisho の出力も変わらなかった(sekisho の DESIGN 3.2)。
### 15.189 導出が取りえない値だけを求める行を、E102 にする(2026-10-06)
**きっかけ**:E102(どの入力にも当てはまらない行)が見つけていたのは二つの形だけだった。先行する行(`overrides` で優先する行を含む)にすべて覆われている行と、上流の表が決して出さない値を指している行である。表の列が `derive` で、その導出が入力の範囲から実際に取りうる値の外だけを求める行は、通っていた。`amount` が 1〜100 ポンド、`limit` が 0〜100 ポンドなら、`excess = amount - limit` は −99〜100 ポンドにしかならない。それでも、範囲を `range >=-1000GBP <=1000GBP` と広く書いた導出に `>500GBP` の行を書くと、`rulec check` は通った(`tests/over/reach.rule`)。完全性の検査(E101)は同じ導出の取りうる値をふるいとして読み、その外の値に行を求めない。証明書もその行を `unreachable`(ふるいがすべての点を落とす行)として挙げていた。§15.188 で `outputs_over` のために `region::live_rows` を足したとき、E102 を同じふるいで強める案は、`rulec check` の答えが変わるので捨てた。互換のページ(`docs/compatibility.md` の「1.x で変わりうるもの」)は、落ちるべきだったのに通っていた規則を 1.x のあいだでも直して落とすとしており、これはその直しに当たる。
**決定**:
- **三つ目の形を足す。** 一つ目と二つ目の形が通した行について、行の区画のうち優先する行に覆われない点が、ふるいで一つも残らないときに E102 にする。ふるいは完全性の検査(`find_hole`)と同じものである。koyomi の日、`constraint`、導出の取りうる値(`feasible` と同じく、座標の端を含めて読む)、上の表(`upstream_dead_at`)、一次のモデル(Fourier–Motzkin の消去)の五つを読む。ただし、報告するのは導出が加わったときだけにする(`OutOfReach::by_a_derive`)。導出の取りうる値が点を落としたときと、モデルの反駁が導出の式か導出の範囲を使ったときである。`constraint` だけ、上の表だけ、koyomi の日だけで死ぬ行は、これまでどおり通す(証明書の `unreachable` には出る)。
- **確かに届かないと言えるときだけ止める。** 区間の計算が正確に出せるのは、入力の足し算・引き算・定数倍だけでできた導出である。そこで、三つ目の形が読む名前を絞る(`reach_names`)。入力(範囲の外の値は、生成コードが入口で受け付けない)、並びを数えた `count` と足した `sum`(同じく入口で受け付けない)、そしてそれらの一次式でできた導出だけである。`define`、表の出力(当たった行によって値が変わる)、それらを読む導出、名前どうしを掛ける導出の取りうる値は、三つ目の形では読まない。一次のモデルも、それらの名前の式と範囲を外して緩めたもの(`Reach::model`)で反駁し、さらに元のモデルでも反駁できるときだけ点を落とす。§15.195 で、E101 のふるいも `define` の取りうる値を読むようにしたので、この絞り込みは外した。取りうる値を区間で必ず含む側に出せるもの(`define`、名前どうしの積、丸め、上の表の出力を読む導出)は、どれも読む。
- **E101 と食い違わない。** 三つ目の形が落とす点は、どれも E101 のふるいも落とす点である。読む検査が E101 の検査の一部で、モデルについては元のモデルでの反駁も求めるからである。したがって、E102 が消すように言った行を消しても、E101 がその行を求め直すことはない(`tests/reach.rs` が、行を消した規則が通ることを確かめる)。§15.30 で起きた、E102 と E101 が書き手に逆のことを言う事態は、ここでは起きない。
- **軸の刻みが値の刻みより粗い表は読まない。** 導出に分数の定数(`amount × 10%`)があると、値は 0.1 ポンド刻みなのに、軸は金額の刻み(1 ポンド)で切られる。境目が 1 ポンド離れた二つのあいだには座標が作られないので(下の「見つけたこと」)、その軸では行が実際より少ない値にしか当たらないように見える。表と上の表のどれかの数の軸で、刻みが値の刻み(`Checked::scales`)より粗ければ、三つ目の形はその表を読まない(`TableRegion::steps_exact`)。§15.190 で軸を値の刻みで切るようにしたので、この制限は外した。
- **文が原因を書き分ける。** 導出の式の上に、その導出が実際に取りうる値を `>=-99GBP <=100GBP` の形で示す(E112 と同じ言い方)。注では、導出が何を取りうるかと、それを何から計算したか(入力の範囲と、取りうる値を狭める `constraint`)を言い、行の値がその中に無いのか、その中の部分を優先する行が先に取るのかを言う。取りうる値は、モデルからその導出のほかの名前を消して求め、値の刻みへ内側に丸める。導出が二つ以上加わるとき(同じ入力を読む二つの導出の組み合わせなど)は「値の組」と言う。導出に関わらない `constraint` や上の表が点を落としたときは、それを別の文で言う。ヒントは、条件を導出の取りうる値の中に書き直すか、行を削除するかで、導出の `range` を広げても取りうる値は変わらないことも言う。
- **予算**:三つ目の形の探索は、表ごとに `--budget` の分の予算を持つ。使い切ったら、残りの行はこれまでどおり通す(E109 にはしない。止めるのは確かに届かないときだけである)。三つ目の形が読む導出の列を持たない表では、探索をしない。
- **準用で持ち込まれた行**:三つ目の形で死ぬ行も、ほかの形と同じく、E102 は出さずに死んだ行として数える(網羅の義務、W118、承認する人のページに効く)。コーパスの準用の規則では、これで変わるものは無かった。
- **台帳**:E102 の「いつ出るか」と「直し方」を三つの形にし、三つ目の形の再現を英語と日本語で足した。台帳の項が、最初の再現が見せない形の再現を持てるようにした(`Entry::also`。`explain` のテキストと Markdown では見出しの下に、JSON では `also` に出す。持つのは E102 だけ)。関係するコードに E112 を足した。
**捨てたもの**:
- **`live_rows` をそのまま使う。** `live_rows` は数の列の座標を、`define` や表の出力の取りうる値(`Checked::ranges`)とも比べ、開いた座標は端を除いて読む(`beyond_reach`)。`outputs_over` は上からの答えを下からの答え(評価器で見つけた入力)と突き合わせるので、上からの答えが誤っても食い違いとして何も決めずに済む。E102 には突き合わせる相手が無い。また、E101 は `define` の取りうる値を読まないので(下の「見つけたこと」)、E102 だけが読むと、E102 が消すように言った行を E101 が足すように言う。
- **`constraint` だけ、上の表だけで死ぬ行も E102 にする。** どれも確かに届かない行だが、導出の取りうる値で強めるという今回の範囲を超える。残した(下)。
**確かめたこと**:
- コーパスの 87 の規則(英語と日本語)で、替える前と後の `rulec check` の出力(英語、日本語、JSON の英語と日本語。`--show-shadow` つきで、終了コードも)が一字も変わらない。木にあるほかの 204 の `.rule`(変異、テストの材料、例、サイトとプレイグラウンドの規則、ほかの言語のテストの材料)で変わったのは二つで、`tests/over/reach.rule` と変異 `m_e019.rule` である。`m_e019` は `constraint 商品合計 <= 値引` を足した変異で、`支払額` が 0 円以下にしかならないので、`>=3000円` の行に当たる入力は無い。新しく出る E102 は正しく、`tests/m0.rs` の固定に足した。
- `tests/reach/` の英語の規則四つと、その日本語の版四つ(名前は `E102_…`)。導出の取りうる値の外の行、`policy first` で残りを上の行が取る受け皿行、`constraint` で狭まる導出、同じ入力を読む二つの導出の組である。`tests/reach.rs` の八つのテストが次を確かめる。英語と日本語の出力の golden(`tests/golden/reach/`)。英語と日本語の版が、同じ行に同じ診断を出すこと。範囲の中の全部の入力(`constraint` を守るもの)を参照の評価器に流しても、その行が一度も当たらないこと。行を消すと規則が通ること。優先する表と、関わらない `constraint` と、koyomi の日(koyomi をつないで確かめる)の言い方。三つ目の形が読まないもの(分数の定数を掛ける導出、`define` の列、`constraint` だけで死ぬ行、表の出力を読む導出)がこれまでどおり通ること(どれも、その行は実際には当たらない。分数の定数を掛ける導出は、§15.190 から読む)。JSON の場所の情報が英語と日本語で同じこと。
- `tests/ports.rs`:`reach.rule` は E102 になり、ritsu の口は答えない。区間に限った問いでだけ行が落ちる例は、`define` の列で書いた `tests/over/define_reach.rule` に移した(三つ目の形は `define` の取りうる値を読まないので、`rulec check` は通る)。全部の入力と突き合わせる問いは数が変わらず、323 の問いのうち 322 が正確に答えられた。
- 証明書、`rulec doc`(英語と日本語)、`rulec coverage`:コーパスと `tests/over`、`tests/apply_fixtures` の 96 の規則のうち、`reach.rule`(検査を通らなくなったので出ない)のほかの 95 で、替える前と一字も変わらない。95 の証明書は、どれも `tools/recheck.py` を通る。準用の規則で、死んだ行として数える行も変わっていない。
- 証明書と再検査:証明書は検査を通る規則にしか出ないので、形は変えない。`unreachable` に残るのは、三つ目の形が報告しない行(`constraint` だけで死ぬ行など)である。`tools/recheck.py` と `proofs/` の Lean は、`unreachable` の行を「述べただけ」として数えるだけで、E102 の判定を読まない。どちらも、コメントのほかは変えていない。
- rulec の全体(`cargo test -p rulec --no-fail-fast`、PostgreSQL つき)は、68 の試験バイナリで 796 件が通り、落ちたものは 0、ignored 1(前からのもの)だった。Lean の層(`proofs/`)を build したあと、Lean の再検査を飛ばしていた三つ(`lean`、`days`、`machine`)を回し直し、通った。ワークスペースの全体(`cargo xtask test --level tools`)も、313 の試験バイナリで 2,228 件が通った(SKIP の 13 行は、どれも platforms の段のテスト)。koyomi の日の言い方のテストは、そのあとに足して回した。
- 時間:導出を三つ持つ 301 行の `policy first` の表(乱数で作ったもの)で、検査の時間は替える前と変わらない(デバッグのビルドで、替える前 61.5 秒、後 53.0 秒。ほとんどはこれまでの検査の時間)。
**見つけたこと**(直していないもの):
- **分数の定数を掛ける導出の軸で、E101 が穴を見逃す。** `derive tenth : money[GBP] = amount * 10%` の値は 0.1 ポンド刻みだが、軸は 1 ポンド刻みで切られる。`<=1GBP` と `>=2GBP` の二行の表は `rulec check` を通り、`amount = 15GBP`(`tenth = 1.5GBP`)を渡すと、生成した Python は `unreachable` の assert で止まる。同じ理由で、`>1GBP <2GBP` の行は当たる値があるのに、一つ目の形の E102 になる。軸の刻みを値の刻みにすれば直るが、コーパスにも分数の定数を掛ける導出がある(`uk_income_tax` の `tapered`)ので、出力が変わるかを測ってから決める。三つ目の形は、この軸を持つ表を読まない。§15.190 で直した。
- **`define` の列で、E101 が届かない値に行を求める。** `define ratio : rate = total / 30` を列にすると、`ratio` は 0〜100% にしかならないのに、E101 は `ratio = 310/3%` を例に行を求める。過剰報告の向きなので不健全な緑にはならないが、三つ目の形が `define` の取りうる値を読まない理由になっている。直すなら、E101 のふるいと、証明書の二つの再検査も `define` の取りうる値を読むようにする。§15.195 で直した。
**残したこと**:上の二つ。`constraint` だけ、上の表だけ、koyomi の日だけで死ぬ行(証明書の `unreachable` には出る)。上の表の出力と導出の組み合わせ(`amount <= 50GBP` でしか `low` にならない列 `k` の下で、`low` と `excess > 50GBP` を同時に求める行)は、上の表の列と導出の入力が region でつながっていないので、どのふるいも落とせない。この組み合わせと、`define` の列の二つは、§15.195 で直した。
### 15.190 計算した値の列を、値が実際にとる刻みで切る(2026-10-06)
**きっかけ**:§15.189 の「見つけたこと」の一つ目。`derive tenth : money[GBP] = amount * 10%` の値は 0.1 ポンド刻みなのに、完全性の検査(E101)の軸は 1 ポンド刻みで切られていた。`<=1GBP` と `>=2GBP` の二行の表は `rulec check` を通り、生成した Python は `amount = 15GBP`(`tenth = 1.5GBP`)を渡すと `unreachable` の assert で止まった。どの入力にも当てはまる行があると言いながら、そうなっていなかった。
原因は、軸を切る刻みにある。軸は、境目の値を点の座標にし、隣り合う二つの境目のあいだに開いた区間の座標を置く。ただし二つの差が刻み以下なら、あいだに値は無いとして区間を置かない(§6.2。`<=2026-03-31` と `>=2026-04-01` のように端を含めて敷き詰めた表に、偽の E101 を出さないため)。その刻みが、率のほかは 1 に決め打ちされていた。金額、量、`number` の入力とセルは整数なので、入力の列ではそれで正しい。規則が計算する値(導出、定義、表の出力)は、整数とは限らない。同じ刻み(`coverage::quantum`)で、境界の対のベクタ(§9)の外側の点も置いていた。
同じところを調べて、生成したコードが計算した値を違う刻みで読み戻す不具合を二つ見つけた。`Checked::scales` は、生成したコードがその名前の値を持つ刻みで、表のセルもこの刻みに直して比べる。
- `min` と `max` の刻みが、一つ目の引数の刻みになっていた。生成コードは二つの引数の刻みの最小公倍数で値を持つので、`min(amount, amount * 10%)` は百分の一ポンドを 1 とする整数で持たれ、表はそれを 1 ポンドを 1 とするセルの値とそのまま比べた。`amount` が 1GBP から 10GBP のとき、参照の評価器は `<=1GBP` の行を、生成した Python は `>1GBP` の行を選んだ。
- 範囲の無い入力から計算した値は、区間が計算できないので、刻みも記録されなかった。生成コードはその名前を刻み 1 として読み、`amount * 10%` を 100 倍の値で比べた。
どちらも `check` は通り、生成したコードは参照の評価器と違う答えを返した。ベクタがその違いを含むので `rulec test` では落ちるが、`check` と `gen` は止めなかった。
**決定**:
- **軸を、値が実際にとる刻みで切る(分数の軸を正確に扱う)。** 計算した値の刻みを、式の葉から順に求める(`Checked::lattice`)。名前はその名前の刻み、リテラルはその値、和と差は両辺が共有する刻み(有理数の最大公約数)、積は二つの刻みの積、整数の定数での商は左辺の刻みをその定数で割ったもの、`min` と `max` は両辺が共有する刻み、丸めはその格子、`allocate` は 1 とする。`amount * 10%` は 0.1、`12570GBP − above_100k × 50%` は 0.5、`min(amount, amount * 10%)` は 0.1 になる。これに当てはまらない形は、値を持つ刻み(`1/scale`)を使う(`Checked::value_step`)。値はかならずこの刻みの整数倍で、この刻みは値を持つ刻みの整数倍でもある。軸の刻みは、この刻みと整数の単位が共有する刻みにする(`coverage::quantum`)。セルは整数の単位でしか書けないので(E103)、境目は整数か範囲の端で、どれもこの刻みに載る。だから「差が刻み以下なら、あいだに値は無い」が正しくなる。境界の対のベクタも、同じ刻みで外側の点を置く。率の軸は、これまでどおり値を持つ刻みで切る。
- **「決められない」として通さない形は取らなかった。** 刻みは式から正確に求まる。分数の軸を持つ表を E109 のように止めると、`<=1GBP` と `>1GBP` のように正しく敷き詰めた表まで落ちる。正確に扱えば、増えるのは境目のあいだの座標だけで、検査の時間もほとんど変わらない。
- **刻みの記録を直す。** `min` と `max` の刻みを、生成コードと同じく二つの最小公倍数にする(`Checked::scale`)。計算した値の刻みは、区間が出なくても記録する(`Checked::record_value`)。
- **例を、値がとる数で書く。** 開いた座標の中の例は、値がとる刻みに載る最初の値にする(`amount * 30%` は 0.3 刻みなので、`>1GBP` のすぐ上は 1.1 ではなく 1.2)。E101 の文は計算した値をそのまま書く(`commission = 1.1GBP`)。JSON の `witness` は数をワイヤーの整数で書くが、その整数で表せない計算した値はワイヤーで渡ることがないので、その数を文字列で書く(`"1.1"`)。これまでは整数に切られて 1 になり、それは `<=1GBP` の行に当たる値だった。`fix.text` の足す行は、例の値をセルに書けないので(セルは整数の単位)、その値がある座標の両端で書く(`| >1GBP <2GBP | false |`)。端が整数でなければ、足す行は出さない。
- **例を作る入力を添える。** 計算した列の穴の例は、呼び出す側が渡せない値でできている。そこで表の線形のモデルを、例の値(導出の列も入力の列も)に固定して解き、ほかの入力は、例にあればその値、無ければとりうる最初の値にする。その入力が範囲と刻みと `constraint` を守り、参照の評価器が表の列にちょうど例の値を出し、どの行もそれに当たらないときだけ、「この例を作る入力: amount = 11」を添える(E105 と同じ言い方)。確かめられないときは何も言わない。生成したコードが `unreachable` で止まるのは、この入力である。
- **三つ目の形(§15.189)の制限を外す。** 軸の刻みが値の刻みより粗い表を読まないための `TableRegion::steps_exact` は、軸を値の刻みで切るようになったので要らない。分数の定数を掛ける導出の列を持つ表も、三つ目の形が読む。
- **証明書と二つの再検査を合わせる。** 証明書は軸ごとに `step` を書くが、どちらの再検査もそれを確かめていなかったので、1 ポンドで切った軸の証明書も通っていた。証明書に `scales`(数の名前ごとの、値を持つ刻み)を足し(形式の版は 1 のまま)、`tools/recheck.py` と Lean の `rulec-recheck`(`checkSteps`)は、`values` の式と `scales` から rulec と同じ規則で列の値の刻みを求め、軸の `step` がそれを割り切ること、座標の端がどれも `step` に載ることを確かめる。`scales` の無い証明書(これより前の rulec のもの)では、求められない刻みを「述べただけ」として数える。
**互換のページ**:どれも、ページが 1.x でも変えてよいとしているものに当たり、ページ自体は直さない。穴を見逃して通っていた規則が E101 で落ちること(と、三つ目の形が分数の定数を掛ける導出の列も読むようになり、届かない行が E102 で落ちること)は「落ちるべきだったのに通っていた規則」に、生成したコードの刻みの直しは「参照の評価器と食い違った答え」に当たる。証明書の `scales` は、形式が足してよいフィールドである。`witness` の値が整数から文字列になるのは、計算した値が整数に載らない例だけで、これまで書いていた整数は例の値ではなかった。どれもリリースノートで、影響する規則の形と一緒に言う。
**捨てたもの**:
- **軸の刻みを、値を持つ刻み(`1/scale`)にする。** 健全ではあるが、値を持つ刻みは値がとる刻みより細かいことがある(`amount * 10%` は、`10%` の刻みを連れてきて百分の一で持たれる)。開いた座標の例が 1.01 のように導出のとらない値になり、境界の対のベクタも、作れない点を求めるようになる。
- **整数の単位をそのまま軸の刻みにし、分数の軸を持つ表を通さない。** 上に書いた。
**確かめたこと**:
- 再現:英語の変異 `tests/mutants/m_e101frac.rule` と、その日本語の版 `m_e101fracja.rule`(金額の 10% の手数料を、`<=1GBP` と `>=2GBP` の二行で分ける。日本語の版は円)。コーパスに分数の値をとる導出を列にする表が無いので、`make-mutants.sh` からは作れず、`m_e101c` と同じく手で書いて `tests/m0.rs` の `BY_HAND` に置いた。どちらも E101 を一つだけ出す(`tests/m0.rs`)。文面は `tests/golden/E101-frac.txt` と `tests/golden/en/E101-frac.txt`。
- `tests/steps.rs`:例の値が JSON で `"1.1"`、例を作る入力が `amount = 11`、足す行が `| >1GBP <2GBP | false |` で、英語と日本語の版が同じ行に出すこと。検査を通さずに生成した Python が、その入力で `unreachable` の assert で止まり、参照の評価器もどの行にも当てないこと。足す行を入れると規則が通り、生成した Python が答えること。1GBP と 2GBP のあいだの行は、11GBP から 19GBP の金額で当たること(これまでは一つ目の形の E102 だった)。`min(amount, amount * 10%)` と、範囲の無い入力からの `amount * 10%` で、生成した Python と参照の評価器が 1GBP から 100GBP の全部の金額で同じ答えを返すこと。証明書が手数料の軸を `1/10` で切り、`scales` を書くこと。
- 替える前の rulec(patch 1 と 2 を当てた木)は、二つの変異とも `ok` を返し、そこから生成した Python は `amount` が 11、15、19 のとき `unreachable` で止まる。
- E102 の三つ目の形:`tests/reach/` に、金額の一割と固定額の和の導出が届かない行の規則を、英語と日本語の版の対で足した(`E102_past_a_tenth_of_the_amount.rule`、`E102_金額の一割が届かない行.rule`)。`tests/reach.rs` は、全部の入力(600 通り)でその行が当たらないこと、行を消すと規則が通ることも確かめる。三つ目の形が読まないものを並べたテストから、分数の定数を掛ける導出を外した。
- 証明書と再検査:`tests/cert.rs` と `tests/lean.rs` に一つずつ。手数料の軸の `step` を `1` にした証明書は、`tools/recheck.py` も Lean の `rulec-recheck` も落とし(「its values sit on a step of 1/10」)、`scales` を外した証明書は「述べただけ」の一行で通る。替える前の rulec が `<=1GBP` と `>=2GBP` の表に出した証明書そのもの(軸は 1 ポンド刻みで、1GBP の次の座標が 2GBP)は、`scales` が無いので「述べただけ」で通り、いまの rulec が書く `scales` を足すと、どちらも落とす。
- 替える前と後の突き合わせ:木にある 304 の `.rule` を、`check`(英語、日本語、JSON の英語と日本語、`--show-shadow`。終了コードも)、証明書、`doc`(英語と日本語)、`coverage`、`vectors`、`gen`(全部の言語の生成物)で比べた。足した四つの規則のほかで変わったのは、sekisho のテストの材料 `crates/sekisho/tests/mutants/fixtures/broken.rule`(E101 の注に「An input producing this example: amount = 1, limit = 0」が一行増えた。どのコマンドも検査の失敗を出すので全部に出る)と、証明書の `scales` だけだった。証明書は 185 の規則で、`scales` を除けば JSON として等しい。生成したコードは、どの規則でも一字も変わらない(`min` と `max` の引数の刻みが違う導出も、範囲の無い入力から計算した導出も、木には無い)。`uk_income_tax` の `tapered`(0.5 刻み)は表の列ではないので、軸は変わらない。新しく落ちる規則は、木の中には無い。
- 替えたあとの 185 の証明書は、どれも `tools/recheck.py` と Lean の `rulec-recheck` を通り、刻みを「述べただけ」とする行は出ない。`--rule` で規則の文と突き合わせると、準用で表を持ち込む二つ(`tests/apply_fixtures/規程.rule` と変異 `m_w118.rule`)がセルの読み直しで落ちるが、刻みとは関わらない(`recheck.py` は替える前のものでも同じように落ちる)。
- rulec の全体(`cargo test -p rulec --no-fail-fast`、PostgreSQL と Lean の層つき)は、69 の試験バイナリで 806 件のうち 803 件が通り、落ちた 3 件は README とサイトの「確かめ方」が言う変異と golden の件数(109 本から 111 本へ、golden は 55 と 44 から 56 と 45 へ)だった。件数と `assurance` の図を直して、その二つの試験バイナリを回し直して通った。rulec を読むほかの言語(dandori、ritsu、sekisho、sakai、yuen)の関係するテストも通り、ワークスペースの全体(`cargo xtask test --level tools`)は、314 の試験バイナリで 2,238 件が通った(落ちたものは 0。SKIP の 13 行は、どれも platforms の段のテスト)。
**残したこと**:
- 線形でない導出(`min` を含む式など)の穴では、例を作る入力を添えないことがある。モデルがその導出と入力を結ばないため。例の値そのものは正しい(`min(amount, amount * 10%)` の 1.1GBP は `amount = 11` で起きる)。§15.194 で、入力の組み合わせが 16,384 通りまでの規則では、入力を総当たりで探して添えるようにした。
- 単位の無い小数のリテラル(`amount * 0.4`)は、`number` の値として読めない(`number` は整数)ので、参照の評価器は値を出さず、生成したコードは 0 を掛けていた。`check` はこれを止めない。刻みとは別の不具合なので、ここでは直していない(§15.191 で E103 にした)。
- §15.189 の「見つけたこと」の二つ目(`define` の列で、E101 が届かない値に行を求める)は、そのまま残る。§15.195 で直した。
### 15.191 式の中の、値にならない数を E103 にする(2026-10-06)
**きっかけ**:§15.190 を調べていて見つけた。`amount * 0.4` の `0.4` には単位が無いので `number` で、`number` は整数なので、値として読めない(`lit_value_in` が値を返さない)。それでも型の検査は `number` という型を決めるだけで通した。参照の評価器はその式に答えを出さず(ベクタが一つもできない)、生成したコードは値の代わりに 0 を使い、`amount * 0` を計算した。`check` は何も言わなかった。金額や量の端数(`amount + 0.5GBP`。金額は 1GBP 単位の整数)も同じだった。セル、範囲、例、出力のセル、丸めの宣言では、値にならないリテラルはすでに E103 だった。抜けていたのは式の中だけである。
**決定**:
- **式(導出、定義、`result`)の中の数のリテラルを、書いた単位で読み、値にならなければ E103 にする**(`Checked::literal_values`)。参照の評価器と生成器が読むのと同じ読み方である。
- **単位の無い小数には、それが表す率を勧める。** 見出しは「単位の無い小数 `0.4` は書けません」、印は「率なら `40%`」、`fix.text` は `40%`(`rewrite_literal`)。rulec は量に単位を、率に `%` を書かせる言語なので、小数を率として読む読み方を足すより、書き方を一つにする。
- **金額と量の端数には `fix.text` を出さない。** 細かい単位の型で書く(`mass[g]` の `1500g`)ほかに、機械的に書き換える形が無いため。
- **整数に書ける小数(`2.0`、`100.0g`)は、これまでどおり通す。** 値があり(2)、参照の評価器と生成したコードが同じ値を使う。セルの `2.0` も同じく通る。止めるのは値の無いリテラルだけで、書き方の好みは止めない。
- **割る数は E115 に任せる。** `amount / 2.5` には「割る数が正の整数の定数でない」と言う E115 が出るので、重ねて E103 は出さない。
**確かめたこと**:
- 再現:コーパスの双子 `retirement_pay.rule` と `退職手当.rule` の `80%` を `0.8` にした変異 `m_e103bare.rule`(英語)と `m_e103bareja.rule`(日本語の版)を、`make-mutants.sh` で作る。どちらも E103 を一つだけ出す。替える前は、例の期待値が成り立たない E107(「came out as reduced」)だけで、原因の小数は指されなかった。例の無い規則なら `check` を通り、生成したコードは `* 0` になっていた。文面は `tests/golden/E103-bare.txt` と `tests/golden/en/E103-bare.txt`。
- `tests/positions.rs`:式の中(導出、定義、`result`、式の中の丸めの格子)とセル、節、出力のセル、例の期待値で、値の無いリテラルが E103 になること。`100.0g` と `10.0円` は通ること。二つの変異の `fix.text` が `80%` で、印の位置に入れるとコーパスの規則に戻ること。
- 木にある 306 の `.rule` で、`check` の出力(英語、日本語、JSON の英語と日本語、`--show-shadow`。終了コードも)が変わったのは、足した二つの変異だけだった。式に値の無いリテラルを書いた規則は木に無く、0 を掛けるコードを生成していた規則も無い。
- rulec の全体(`cargo test -p rulec --no-fail-fast`、PostgreSQL と Lean の層つき)は、69 の試験バイナリで 810 件が通り、落ちたものは 0、ignored 1(前からのもの)で、SKIP の行は無かった。rulec を読むほかの言語の関係するテスト(dandori の規則の口、sekisho の全体、ritsu の `check`)も通った。
**互換のページ**:「落ちるべきだったのに通っていた規則」に当たる。そういう規則の生成コードは、参照の評価器と違う答え(0 を掛けた値)を返していた。
### 15.192 「名指し」を、参照・指す・挙げるで言う(2026-10-06)
**きっかけ**:rulec の日本語は、セルや行や例が値・列・群を名前で書くこと、診断や出力が行・対・ファイルを挙げることを、どれも「名指す」「名指し」と言っていた。ritsu の参照の書き方(`<ツール> "<パス>" [<種類> <名前>]...`)も「名指し」、その頭の語を「ツールの語」と呼んでいた。日本語の技術の文では、どちらも普通は言わない。
**決定**:何が起きるかで言う。
| 前 | 後 |
|---|---|
| 名指し(ritsu の参照の書き方) | 参照 |
| 名指す(ファイルが外のものを、参照の書き方で) | 参照する、指す |
| ツールの語(参照の頭の `rulec`・`koyomi` など) | ツール名 |
| 名指す(セル・行・例が、値・列・群・並びを) | 書く、条件に書く、使う(並び) |
| 名指す(診断・出力・テストが、行・対・ファイル・コードを) | 挙げる、示す、指摘する(文脈で選ぶ) |
| 名指す(`overrides`・`implements` などが、表やサービスを) | 指す、指定する |
| 名指しで生成しない(SQL、NumPy) | 生成しない(「SQL だけは生成しない」) |
診断の文と注(E051、E102、W116)、`explain` の台帳(E015、E032、E035、E041、E052、E130、W116)と `docs/codes.ja.md`、`fmt` の `--help`、`coverage` のまとめの一文、`certificate` の証明しないものの一行、`doc` の W111 の一文、生成する MCP サーバーの Origin のコメント、この文書、サイトの日本語のページ、テストの名前と変異の説明を直した。英語は一字も変えていない。
**残したもの**:なし。
### 15.193 出典から引く部分を「引用箇所」と言う(2026-10-06)
**きっかけ**:この文書は、規則が出典から引く部分(法令の条や別表、文書の表)を、診断と出力(E037 の「引用箇所 `{frag}` の書き方が読めません」、`source` の `--help`)とは別の、直訳の語で呼んでいた。同じ語を、言語が書ける範囲のことと、文書に貼った規則の一部のことにも使っていた。
**決定**:出典から引く部分は、出力と同じく「引用箇所」と言う(`<引用箇所> sha256:…` の固定行、`@<出典> <引用箇所>`、`<文書のパス>.fragments/<引用箇所>.tsv`)。言語が書ける範囲のことは「いまの言語」「v1 で書ける条件」、領域の一部は「部分」、文書に貼った規則や表は、ほかの節と同じく「抜粋」と言う。この文書の 30 か所、テストの名前(`tests/sources.rs` の `ページは引用箇所の本文を載せる`)、変異の説明(`tests/m0.rs` の三つ)、実験の文書(`experiments/state-machine/README.md`)の二か所を直した。英語は変えていない。ファイルの名前(`.fragments/`)とコードの名前は英語のままである。
**残したもの**:なし。
### 15.194 線形のモデルでは入力を求められない列の穴にも、例を作る入力を添える(2026-10-07)
**きっかけ**:§15.190 から、E101 は計算した列の穴の例に、それを作る入力を添える(「この例を作る入力: amount = 11」)。入力は、表の線形のモデルを例の値に固定して解いて組み立てる。線形のモデルが持つのは、同じ型の名前の足し算と引き算、定数を掛ける形と定数で割る形の導出だけである(`fourier::linear`)。`min`・`max`・丸め・`allocate` を使う導出や、名前どうしを掛ける導出は持たず、`define` と、上の表が出す値も持たない。そういう列の穴では、モデルが列と入力の関係を持たないので、組み立てた入力は例の値を作らず、E101 は入力を添えなかった。`min(amount, amount * 10%)` の 1.1GBP の穴は `amount = 11` で起きるが、それを言えなかった(§15.190 の「残したこと」)。
もう一つある。入力を探すのは、表の列に導出か真偽の `define` があるときだけだった(`define` の列を見分ける `TableRegion::is_define` のフラグは、真偽の `define` の列にしか立たない)。数を計算する `define`(`rating_grade.rule` の `ratio = total / 50`)の列や、上の表が出す値の列(ゆうパック運賃の `サイズ`、`parcel_rate` の `size`)の穴は、呼び出す側が渡せない値の穴なのに、入力を探しもしなかった。
**決定**:
- **線形のモデルで組み立てた入力が例を作らないときは、入力を総当たりで探す**(`grid::exhaust_input`)。動かすのは、表の計算した列が読む入力(導出と `define` の式から、上の表もたどって行き着く入力)と、`constraint` でそれらにつながる入力である。表が列としてそのまま読む入力は、例の値に置いたまま動かさない。ほかの入力は、線形の場合と同じく、とりうる最初の値に置く。組み合わせが 2^14(16,384 通り)を超えるとき、範囲の無い入力や文字列の入力が関わるとき、並びを歩く規則では探さない。§15.153 の総当たり(`exhaust_tie` と `exhaust_pair`)と同じ上限、同じ条件で、同じ `Domain` を使う。
- **添える前の確かめは、線形の場合と同じにする。** 参照の評価器で、表の列がちょうど例の値になり、どの行も当たらない入力だけを添える。その入力が範囲と刻み、`constraint`、koyomi の日を守ることも確かめる(`vectors::no_row_takes`)。見つからなければ何も添えない。
- **探す順は、ほかの総当たりと同じにする**(`Domain::each`)。入力を規則が宣言した順に並べ、最初の入力をいちばん速く動かし、どの入力も範囲の下の端から上へ動かして、最初に確かめを通った入力を添える。結果は実行のたびに同じで、英語の規則と日本語の版も同じ値を言う。
- **線形のモデルで組み立てた入力が例を作るときは、これまでどおりその入力を添える。** 総当たりは、その後ろにだけ置いた。これまで添えていた入力は一つも変わらない。
- **入力を探すのは、表の列に、入力でない列が一つでもあるときにした。** 導出、`define`(型を問わない)、上の表が出す値の列である。どれも呼び出す側が渡せない値で、穴の例だけでは、どの入力を渡せばその例になるかが分からない。
- **出力のセルに書いた列挙の値を、動かす入力に数えない。** 表の出力が何を読むかを答える `coverage::reads_of` は、出力のセルに書いた語も読む名前に入れる。そのため `small` のような列挙の値が動かす入力の集まりに入り、総当たりは「調べられない」と答えていた(`parcel_rate` の `size` の穴は、これを外すまで入力が見つからなかった)。型の無い語は外から入ってくる値ではないので、総当たりの側で外した。`reads_of` そのものは変えず、網羅の検査の総当たりと行の固定の判定でも、同じく外した(下の「見つけて直したこと」)。
- **注の文、`fix.text`、JSON の `witness` は、線形の場合と同じ形にする。** 注は「この例を作る入力: …」で、JSON では `notes` の一つになる。台帳の E101 の「いつ出るか」に、入力の求め方を一文足した。
**捨てたもの**:
- **総当たりを先にする。** 線形のモデルで組み立てられる穴にも最大で 16,384 回の評価をすることになり、これまで添えていた入力が、総当たりで最初に見つかる入力に替わりうる。線形のモデルを先にすれば、変わるのは、これまで何も添えなかった穴だけである。
- **探す数の上限を `--budget` に結ぶ。** `--budget` は検査が訪ねるノードの数の上限で、評価の回数とは単位が違う。上限は §15.153 と同じ 2^14 にした。超えても入力を添えないだけで、E101 そのものは変わらない。
- **例の値ではなく、同じ穴(同じ座標)に入る入力を探す。** 見つかりやすくはなるが、注は「この例を作る入力」と言うので、例の値そのものを作る入力に限った。
**確かめたこと**:
- 再現:コーパスの双子 `rating_grade.rule` と `評価ランク.rule` の、いちばん下の受け皿の行 `-` を `<50%` にした変異 `m_e101ratio.rule`(英語)と `m_e101ratioja.rule`(日本語の版)を、`make-mutants.sh` で作る。列は `define` の達成率(合計点 ÷ 50)で、50% から 58% がどの行にも入らない。どちらも E101 を一つだけ出す(`tests/m0.rs`)。替える前は「当てはまらない例: 達成率 = 50%」だけだった。替えたあとは四つの評価の組み合わせ(14,641 通り)を探して、「この例を作る入力: 価格 = 0, 品質 = 10, 対応 = 0, 納期 = 5」を添える。英語の版も同じ値を言う。文面は `tests/golden/E101-ratio.txt` と `tests/golden/en/E101-ratio.txt`。
- `tests/behind.rs` の十のテスト:
- `min(amount, amount * 10%)` の穴で、例の値が JSON で `"1.1"`、例を作る入力が `amount = 11`、足す行が `| >1GBP <2GBP | true |` になり、英語の規則と日本語の版が同じ行に出すこと(線形の場合を確かめる `tests/steps.rs` と同じ形)。その入力で、参照の評価器は導出を 1.1 にしてどの行にも当てず、検査を通さずに生成した Python は `unreachable` の assert で止まること。
- 変異の二つで、添えた入力が達成率をちょうど 50% にし、どの行にも当たらないこと。その入力が探す順で最初に来るものであり、三回の実行で変わらないこと。生成した Python がその入力で止まり、納期を一つ下げた入力と五つ上げた入力では答えること。
- 上の表が出す値の列の穴で、英語の `m_e101en`(`parcel_rate`)と日本語の `m_e101`(ゆうパック運賃)に例を作る入力が付き、その入力で参照の評価器が列を例の値(`size = small`、`サイズ = S60`)にして、どの行も当てないこと。検査を通さずに生成した Python がその入力で止まり、girth が 22 と 61 では答えること。
- 表が列として読む入力(`tier`)は例の値のままであること。`constraint` を破る組み合わせを飛ばすこと(`amount <= cap` の下で `amount = 11, cap = 11` を言う)。上限ちょうどの 128 × 128 通りは探し、128 × 129 通りでは何も添えないこと。どの入力も作らない例(§15.189 の「見つけたこと」の二つ目、`define` が取りえない 310/3%)には何も添えないこと。§15.195 で E101 が `define` の取りうる値を読むようになり、この規則は通るようになった(このテストは、取りうる値の中の穴に入力を添えることを確かめる形にした)。
- 替える前と後の突き合わせ:木にある 306 の `.rule`(コーパス 87、変異 113、ほか 106)と足した二つの変異を、`check`(英語、日本語、JSON の英語と日本語、`--show-shadow`。終了コードも)、証明書、`doc`(英語と日本語)、`coverage`(英語と日本語)、`vectors`、`gen`(全部の言語の生成物)で比べた。変わったのは、上の表が出す値の列に穴がある変異の四本(`m_e101.rule`、`m_e101e.rule`、`m_e101en.rule`、`m_e105.rule`)と、足した二つの変異だけで、どれも E101 の注に、例を作る入力の行が一つ増えただけだった(四本では 12 種類の出力の 48 か所。JSON では `notes` の二つ目に入り、ほかのキーは変わらない)。四本は検査を通らない規則なので、どのコマンドも E101 を出し、全部の出力に出る。木の E101 のほかの七本は変わらない。入力の列の穴(`m_e101c`、`m_e101d`、yuen の二つ)は例がそのまま入力なので何も添えず、線形の導出の列の穴(`m_e101frac`、`m_e101fracja`、sekisho の `broken.rule`)はこれまでと同じ入力を添える。コーパスの規則は一字も変わらない。
- README、サイトの「何を証明するか」(英語と日本語)、日本語の「立場ごとの使い方」、`docs/formats.md`(とスキルのコピー)の E101 の抜粋に、実物の注の行を一つずつ足した(`parcel_rate` には「An input producing this example: dest = overseas, girth = 23, signature = true, weight = 1」、ゆうパック運賃には「この例を作る入力: あて先 = 山梨県, 三辺合計 = 1, 重量 = 1」)。golden は `E101.txt`、`en/E101.txt`、`json/E101.json`、`json/en/E101.json` を取り直した。変わったのは、どれも注の一行である。
- 時間:何も見つからずに 14,641 通りを最後まで探す規則で、`rulec check` は 0.14 秒(デバッグのビルド)。
- rulec の全体(`cargo test -p rulec --no-fail-fast`、PostgreSQL と Lean の層つき)は、69 の試験バイナリで 822 件が通り、落ちたものは 0、ignored 1(前からのもの)で、SKIP の行は無かった。
**見つけて直したこと**:
- **網羅の検査の総当たり(`exhaust_tie`、`exhaust_pair`)も、出力のセルに書いた語を、動かす入力の集まりに入れていた。** `reads_of` が表の出力の読む名前にセルの語(列挙の値と `true`・`false`)を含めるので、そういう表を通る名前では、総当たりはいつも「調べられない」と答えていた。答えは決められないほうへ倒れるだけで、見逃しにはならない。呼び出す側は、網羅の義務を残すか、ベクタをほかの探し方で作る。どの入力も届かないと誤って言う道は無い(語を外すと、歩ける総当たりが増えるだけである)。語を外したときの答えもその場で計算する計測用のビルドで、木の 308 の `.rule` に `coverage`・`vectors`・`doc` をかけると、語が入る呼び出しは 34 本の規則で 112 件(`exhaust_pair` 94、`exhaust_tie` 18)だった。語を外すと答えが変わるのは `exhaust_pair` の 40 件(20 本の規則)で、どれも出力の値が一つに決まるかの判定(`value_duties` の `output_fixed`)からの呼び出しだった。「調べられない」が「対が見つかる」に変わるだけで、この判定は「どの入力も届かない」のときにしか効かないので、答えは変わらない。`value_duties` の、行が計算のもとを全部決めているかの判定に語が入る場面は、木に無かった。`exhaust_input` と同じく型の無い語を外すビルドでも、`check`・`doc`・`coverage`・`vectors`・`gen` の出力は 308 本とも一字も変わらなかった。そこで、`exhaust_tie` と `exhaust_pair` の動かす入力の集まりと、行の固定の判定からも、型の無い語を外した(`c.ty_of` の無い名前を外す、`exhaust_input` と同じ書き方)。木の 308 本で、`check`(英語、日本語、JSON の英語と日本語、`--show-shadow`)、証明書、`doc`、`coverage`、`vectors`、`gen` の出力は一字も変わらない。木の外の、列挙を出す表を通る出力を持つ小さな規則では、総当たりが歩けるようになり、満たせない網羅の義務が立たなくなったり、手がかりの探索が見つけない対がベクタに入ったりする。
**残したこと**:
- 組み合わせが上限を超える規則(入力の範囲が広い規則)の、線形でない列の穴には、これまでどおり入力を添えない。
### 15.195 `define` の取りうる値と、上の表の行と導出の組み合わせを、完全性の検査と E102 が読む(2026-10-07)
**きっかけ**:完全性の検査(E101)は、表の点のうち入力から届かない点を、ふるい(koyomi の日、`constraint`、導出の取りうる値、上の表が出さない値、一次のモデル)で落としてから数える。このふるいには、届かない点を落とせない形が二つ残っていて、E101 はどの入力でも起きない穴に行を求めていた。
- 一つは、数を計算する `define` の列である。`define ratio : rate = total / 30`(`total` は 0〜30)は 0%〜100% にしかならないのに、E101 は `ratio = 310/3%` を例にして行を求めた(§15.189 の「見つけたこと」の二つ目、§15.194 の「確かめたこと」)。導出の列なら、一次のモデルが導出の取りうる値を二つの事実として持つので、その外の座標は落ちるが、`define` の式はモデルに入らない。
- もう一つは、上の表が出す値の列と導出の列を両方持つ表である。上の表が `low` を出すのが `amount <= 50GBP` のときだけなら、`low` の行の下で `excess = amount - limit` は 50GBP を超えない。region は上の表の列と導出の入力を結んでいなかったので、どのふるいもこの組み合わせを落とせなかった(§15.189 の「残したこと」)。
どちらも穴を見逃す向きではなく、言いすぎの側である。一つ目は、E102 の三つ目の形が `define` の取りうる値を読まない理由にもなっていた(E102 だけが読むと、E102 が消せと言った行を E101 が足せと言う。§15.189 の決めの 2)。
**決定**:
- **`define` の取りうる値をふるいに入れる。** 数を計算する `define` の列の取りうる値(`TableRegion::define_reach`)は、式を区間で計算したもの(`Checked::ranges`)である。`Checked::interval` が、入力の範囲、導出と別の `define` の取りうる値、上の表が書く値から計算する。掛け算、割り算、丸め、`min`、`max`、`allocate` も、必ず含む側に出す。座標がその外にあれば、その点にはどの入力も届かない。開いた座標は端を含まないので、取りうる値が 100% で終わるとき `>100%` の座標は外にある。導出の座標は端を含めて読み、一次のモデルが導出の取りうる値を二つの事実として持って、開いた端の残りを落とす。`define` の式はモデルに入らないので、ここで読む。E101 の穴の探索、被覆、証明書の到達点、E102 の三つ目の形が読む。重なり(E105、W105、W114)は読まない(`feasible` は変えていない)。
- **上の表の行と導出の組み合わせを落とす**(`TableRegion::above_rows_out`)。表が上の表の出力の列を持つとき、点がその列にとる値を書く上の表の行の一つ一つについて、その行がこの点で当たらないことを示す。示し方は二つある。二つの表がともに持つ列挙か真偽の列で、点の座標がその行の受け付ける値に無いとき。または、この表の一次のモデル(導出の式、範囲、`constraint`。型ごとの系で、モデルの無い型はこの表の数の列だけ)に、点の座標の両端と、その行の箱がこの表の数える名前(数の列とモデルの名前)にとる両端を足して、解が無いとき(Fourier–Motzkin の反駁と乗数を残す)。その値を書く行の全部が当たらなければ、その点にはどの入力も届かない。上の表が `policy first` でも、行の箱だけを読む。行が実際に受け持つのは箱の一部なので、読むのは本当より広く、落とす点は確かに届かない。上の表の行が値ではなく名前を書くとき(どこでもその値を書きうる)は読まない。E101 は、一次のモデルを読むところ(穴の探索と被覆)で読む。被覆の点ごとの葉(`ByPoints`)は再検査がふるいだけで歩き直すので、そこでは読まない。
- **E102 の三つ目の形も同じものを読む。** `define` の列の取りうる値の外だけを求める行と、上の表の行が当たるときには導出が取りえない値を求める行を、三つ目の形として止める。§15.189 の決めの 2 で読む名前を一次式の導出に絞っていたのも外し、取りうる値を区間で必ず含む側に出せる導出(名前どうしの積、丸め、`min`、上の表の出力を読む導出)も読む。区間は必ず含む側なので、その外の点は確かに届かない。報告するのは、導出か `define` の値が加わったときだけである(`OutOfReach::by_a_derive`)。上の表の組み合わせでは、反駁が導出の式、導出か `define` の取りうる値、それらの列の座標の端を使ったときである。上の表だけで死ぬ行と、上の表と `constraint` だけで死ぬ行は、これまでどおり通して、証明書の `unreachable` に出す。三つ目の形が落とす点は、どれも E101 のふるいも落とす点で、§15.189 の決めの 3 は保たれる。
- **文**:E102 の三つ目の形の注は、`define` の場合も導出と同じ言い方で取りうる値を言う。式が上の表の出力を読むなら「入力の範囲と、上流の表が出す値から計算すると」と言う。上の表との組み合わせでは、どの行がその値を書くかを言い、一行なら、その行の条件と、その行が当たるときに導出が取りうる値も言う(「`返金の大きさ` が 少額 になるのは、表 大きさ表 の 行1(金額 <= 50円)が当たるときだけです。そのとき `超過額` が取りうる値は >=-99円 <=50円 で、この行の条件に当てはまる値はその中にありません。」)。`define` だけが加わったときのヒントは、導出の `range` の一文を言わない(`define` には範囲が無い)。台帳の E101 と E102 の「いつ出るか」と「直し方」を直し、`Entry::also` に二つの形の再現(英語と日本語)を足した。
- **E101 の例の値**:穴の例の値は、計算した列(導出と `define`)では、その列の取りうる値の中の値にした(`TableRegion::reach_values`)。座標が取りうる値の端をまたぐとき、座標の最初の値は列がとらない値のことがある(`share` が 0%〜100% のとき、`>-10% <10%` の座標の最初の値 -9% ではなく 0% を言う)。証明書の到達点の値も同じにした。
- **証明書**:葉を二つ足した(形式の版は 1 のまま)。`{"define_axis": <軸>}` は、`define` の座標が取りうる値の外にある箱である。`{"above_rows": {"axis", "column", "value", "rows": [{"row", "clash" | "farkas"}]}}` は、上の表がその値を書く行の一つ一つと、その行が当たらない理由である。反駁の不等式に、上の表の行の端 `{"cond": {"name", "hi", "at", "open"}}` を足した。上の表の行を数えるのは再検査で、証明書は行の番号と理由だけを書く。到達点が、被覆が上の表の行に頼る値をとるときは、その値を書く行のどれかが当たる値を到達点の値にする(`TableRegion::above_values`)。上の表が出さない組み合わせの葉(`ByUpstream`)で、小さく書ける理由が無いときも、この葉で書けるなら書く。
- **二つの再検査**:`tools/recheck.py` と Lean の `rulec-recheck` は、`define` の取りうる値を `values` の式から自分で計算し直し(導出と同じ計算)、`define_axis` の葉の座標と、到達点の値を、それに照らす。開いた座標は端を含めずに読む。導出の列も同じ読み方にした。この読み方は、取りうる値の外と認める座標を増やすだけなので、これまで通っていた葉は、この読み方のためには落ちない。一次のモデルの範囲の事実も、規則が計算する値については、証明書の `ranges` ではなく計算し直した区間から作る。`above_rows` の葉は、上の表の証明書の `produces` からその値を書く行を自分で数え(書く値を読めない行が一つでもあれば拒む)、葉の行の集まりがそれと同じであること、行ごとの理由が成り立つことを確かめる。`clash` は二つの表の同じ名前の列の値で確かめる。`farkas` は乗数つきの不等式の和で確かめ、上の表の行の端は、その行がその列にとる座標の全部がその端の内側にあることで確かめる。
- **Lean の意味と定理**:`Semantics.lean` の「尋ねられる点」は `Sieve.asked` が決める。軸ごとの取りうる値 `reach` を、導出の列だけでなく `define` の列(計算し直した区間)にも入れた。`Sieve.above`(上の表がその値を書く行の一つ一つが、この表の数える値にとる座標と、列挙の列で受け付けるこの表の座標)を足して、「その値をとる点では、その値を書く行のどれかが、その点の値で当たる」を `asked` の条件に足した。この条件を、意味の側を直してから足し、定理を書いた。`not_asked_of_aboveRuledOut`(その下の `rowOut_sound` は `farkas_sound` の上に立つ)と、開いた座標の端を含めない読み方に直した `not_asked_of_derived` が、二つの葉の確かめが主張を決めることを示す。`witnessOk` は上の表の条件も確かめ、`asked_of_witnessOk` も直した。`RitsuCross/Days.lean` の `AskedBut` にも同じ条件を足した。
**捨てたもの**:
- **`define` の式を一次のモデルに入れる。** 一次の `define` なら、範囲の事実と式の事実でモデルが落とせる。ただ、モデルは重なりの判定(`refutes_pair`)、到達点の値、例を作る入力にも使うので、E105 と W114 の答えが変わる。`min` や丸めの `define` は入らない。証明書の `linear` も、`define` の列を持つ表で変わる。区間で読めば、どの形の式も同じ一つの読み方で済む。
- **上の表の行の条件を、表の一次のモデルに初めから足す。** モデルが大きくなり、消去が上限(`CAP`)を超えて、これまで落としていた点を落とせなくなることがある。重なりの判定と到達点も変わる。行ごとに系を作れば、変わるのは、これまで落とせなかった点だけである。
- **真偽の `define` のとる値を、完全性の検査と E102 も読む。** `outputs_over` の 12 の問いは、真偽の `define`(`a_due <= b_due`)が、入力を一点に限ると一つの値しかとらないことを読めば決まる。`check` の範囲(入力の宣言した範囲)では、真偽の `define` が一つの値しかとらない規則は木に無く、読むなら二つの再検査にも比べる式の真偽の計算を足す要がある。ここで直す範囲(数を計算する `define`)を超えるので、`outputs_over`(`live_rows`)だけで読む。
**`outputs_over`**:`live_rows` は、上の表の行と導出の組み合わせと、真偽の `define` がつねにとる値(`TableRegion::truth_of`。比べる二つの辺の区間から、つねに真かつねに偽かを決める)を読むようにした。`define` の取りうる値は前から読んでいた(`beyond_reach`)。§15.188 の 2026-10-07 の段落で決められなかった 12 の問い(`coupon_order.rule` と `適用順序.rule` の、二つの日付を一点に限った問い)は決まるようになり、値に限った問いは 358 の全部が正確に答えられる。区間に限った問いは 327 のうち 326 のまま(`parity.rule`)。
**確かめたこと**:
- 再現(E101):`tests/sieve/` に、英語の規則と日本語の版の二組(`E101_past_a_defines_reach.rule` と `E101_定義の届かない値.rule`、`E101_past_what_a_table_above_allows.rule` と `E101_上の表が許さない値.rule`)。どれも替える前は E101 で、どの入力も届かない値(`share` が 100% を超える値、`small` で `excess` が 50GBP を超える値)に行を求めていた。替えたあとは通る。`tests/sieve.rs` の五つのテストが、範囲の中の全部の入力でその値に届かないこと、証明書がその葉を持つこと、行を一つ抜くと、本当の穴を取りうる値の中の例とそれを作る入力で言うこと、座標が取りうる値の端をまたぐとき例が取りうる値の中の値になることを確かめる。
- 再現(E102):`tests/reach/` に二組(`E102_past_a_defines_reach.rule` と `E102_定義の届かない行.rule`、`E102_past_what_a_table_above_allows.rule` と `E102_上の表が許さない行.rule`)。`tests/reach.rs` で、ほかの組と同じこと(英語と日本語の golden、同じ行に同じ診断、どの入力もその行に当たらないこと、行を消すと規則が通ること)を確かめる。三つ目の形が読まないものを並べたテストは、`constraint` だけで死ぬ行と、上の表と `constraint` だけで死ぬ行にした。新しく読むもの(`define` の列、上の表の出力を読む導出、定数倍の導出)が E102 になることは、新しいテストで確かめる。
- §15.194 の 310/3% の規則:替えたあとは E101 が出ず、どの `total` も 100% を超える割合を作らない。行を `<50%` にすると、穴は `ratio = 50%`、それを作る入力は `total = 15` と言う(`tests/behind.rs`)。
- 証明書と再検査:`tests/cert.rs` と `tests/lean.rs` に一つずつ。`define_axis` の葉を届く座標に置いたもの、証明書の `ranges` で `define` の取りうる値を狭く書いてその葉を置いたもの、`above_rows` の葉の行を別の行にしたもの、乗数を変えたもの、行の端を動かしたもの、上の表のほかの行も同じ値を書くようにしたもの、到達点の値を上の表の行の外に置いたものを、二つの再検査がどれも拒む。Lean の公理のテストに `not_asked_of_derived` と `not_asked_of_aboveRuledOut` を足した。
- `tests/ports.rs`:`tests/over/define_reach.rule` は E102 になり、口は答えない。区間に限った問いでだけ行が落ちる例は、`constraint` だけで死ぬ行の `tests/over/constraint_reach.rule` に移した。新しいテストが、`coupon_order.rule` と `適用順序.rule` で、二つの日付を一日ずつに限った問い(額引きどうしで期限が先・同じ日・後、率引きと額引き、種別を限らないもの)が正確に答えられ、どの値にもその日の入力が添えてあることを確かめる。真偽の `define` を読まないようにすると、このテストは「決められない」の答えで落ちる(試して確かめた)。
- 替える前と後の突き合わせ:木にある 312 の `.rule`(コーパス 87、変異 115、ほか 110)を、`check`(英語、日本語、JSON の英語と日本語、`--show-shadow`。終了コードも)、証明書、`doc`(英語と日本語)、`coverage`(英語と日本語)、`vectors`、`gen`(全部の言語の生成物)で比べた。変わったのは `tests/over/define_reach.rule` だけで、`define excess = amount - limit` が −99GBP〜100GBP にしかならないのに `>500GBP` を求める行3 が、E102 の三つ目の形になった(検査を通らないので、どのコマンドもその E102 を出す)。コーパスの規則は一字も変わらない。足した九つの規則では、`E102_…` の四つが、替える前は通り、替えたあとは E102 になる。`E101_…` の四つは、替える前は E101、替えたあとは通る。`constraint_reach.rule` は変わらない。証明書の新しい葉と `cond` を持つのは `E101_…` の四つだけで、木の規則の証明書は、`define_reach.rule` のほかは一字も変わらない。替えたあとの 188 の証明書と足した五つは、新しい二つの再検査を通る。`--rule` つきで前から落ちる二つ(`tests/apply_fixtures/規程.rule` の Python と、変異 `m_w118.rule` の両方。どちらもセルの読み直しで、ここで変えたものとは関わらない)は、前と同じ理由で落ちる。替える前の二つの再検査は、`E101_…` の四つの証明書を、知らない葉として拒む。替える前の rulec が出した 189 の証明書を新しい再検査に通すと、違うのは `define_reach.rule` だけで、行3 の到達点 `excess = 501` を、式が届かない値として拒む。sekisho の木の 147 の `.gate`(`check` の英語・日本語・JSON の英語と日本語、`doc` の英語と日本語、`vectors`、`api`、生成した Cedar)は一字も変わらない。`outputs_over` の総当たりは、区間に限った問いが 327 のうち 326 で正確(出力の値の全部より少ない値の答えは 197)のまま、値に限った問いは、正確な答えが 358 のうち 346 から 358 になった。
- 全体のテスト:ワークスペースの全体(`cargo xtask test --level tools`。先に `lake build`、PostgreSQL の層つき)は、312 の試験バイナリと 19 の doc テストで 2,356 件が通り、落ちたものは 0、ignored は 1(前からの `予算の分布を測る`)だった。SKIP の行は 13 で、どれも platforms の層のもの(Temporal、Argo、LocalStack、Ollama、TypeSafe、kani、ネットワーク)である。替えたあとのコードで作り直した `ritsu.wasm` で、ブラウザで試すページのテスト(`crates/ritsu/tests/playground.rs`)の 7 件も通った。そのあと台帳の E101 と E102 の文(`constraint` を読むのは導出だけであること)を直し、関わる `codes`・`docs`・`skill` と ritsu の `codes`・`links` を回し直して通った。
**残したこと**:
- 上の表の行の条件が、この表の数える名前(数の列と一次のモデルの名前)に無い名前を見るとき、その条件は読まない。落とせない点が残る、言いすぎの側である。
- 上の表がその値を書く行のうち、名前を書く行(値が実行時に決まる)があれば、その値は読まない。
- 到達点の値が上の表の行に合っていることを確かめるのは、被覆が上の表の行に頼る値だけである。そうでない値をとる到達点の値は、モデルだけで解いたもので、上の表の行が当たる値とは限らない。
- 真偽の `define` のとる値は、`outputs_over` でだけ読み、`check` は読まない。§15.196 で `check` も読むようにした。
- 証明書の軸の `kind` は、数を計算する `define` の列も `upstream` と書く(前から。`define` と書くのは真偽の `define` だけ)。直すと、そういう列を持つ規則の証明書が変わる。二つの再検査は、`kind` ではなく、計算し直した区間があるかどうかで取りうる値を読む。§15.196 で `define` と書くようにし、二つの再検査も `kind` を自分で求めて照らし合わせる。
- 上の表の出力の取りうる値(`ranges`)は、証明書の言うとおりに読む(入力の範囲と同じく、文書の言い分である)。その出力を読む `define` の取りうる値は、それを使って計算し直す。§15.196 で、上の表の行が書く値を証明書に載せ、二つの再検査が自分で計算するようにした。
- 重なりの判定(`feasible`)は `define` の取りうる値を読まず、重なりの検査は上の表の行と導出の組み合わせも読まない。§15.196 で、E101 と同じふるいを読むようにした。
### 15.196 §15.195 の残りを閉じる:列の種類、上の表が数の列に書く値、重なりの検査、ファイルを添えた再検査、真偽の `define` のとる値(2026-10-07)
**きっかけ**:§15.195 は、完全性の検査(E101)と E102 の三つ目の形が、数を計算する `define` の取りうる値と、上の表の行と導出の組み合わせを読むようにした。そのとき、範囲の外として五つを残した。
- 証明書の軸の `kind` が、数を計算する `define` の列を `upstream` と書いていた。二つの再検査は `kind` を自分で求めていなかったので、この書き違いを見逃した。`kind` は到達点と軸の敷き詰めの読み方を決める(入力の軸の到達点は呼び出し側が送れる値で、軸は宣言した範囲を敷き詰める)ので、書き違いは見逃してよいものではない。
- 二つの再検査は、上の表が数の列に書く値の範囲(証明書の `ranges`)を、証明書の言うとおりに受け取っていた。数の答えのセルの値が証明書に無かったからである。§15.195 は、`define` の取りうる値をこの範囲の上で計算して点を落とすので、頼る場面が増えた。
- 重なりの判定(`feasible`)は `define` の取りうる値を読まず、重なりの検査(E105、W105、W114)は上の表の行と導出の組み合わせも読まなかった。E101 が届かないと見る点で、重なりを言うことがあった(言いすぎの側)。
- `--rule` を付けた再検査が、`tests/apply_fixtures/規程.rule`(Python)と変異 `m_w118.rule`(両方)で、セルの読み戻しで落ちていた。
- 真偽の `define` がつねにとる値は、`outputs_over` だけが読み、`check` は読まなかった。E101 は、いつも true になる真偽の `define` の列にも、false の行を求めた。§15.195 で直した言いすぎと同じ種類である。
**決定**:
- **列の種類。** `kind` は、`define` の列なら型によらず `define` にする。証明書は、規則の入力(`inputs`)、歩きが残す値(`walks`。`count` と `sum`)、`values` の一つ一つが何か(`of`。`derive`、`define`、`result`)を新しく書き、二つの再検査は、それらと表の `decides` から `kind` を自分で求めて照らし合わせ、違えば落とす。要素のフィールド(`elements`)の列は、前からの `upstream` のまま残した(入力と呼ぶと、到達点の主張が強くなる。弱い側のままにした)。`inputs` の無い前の証明書では、種類は証明書の言い分として述べる。
- **上の表が数の列に書く値。** 表が数の列を決めるとき、証明書の行は `writes` に、その列に書く値を、`values` と同じ木の形で、セルの場所と一緒に持つ。答えのセルはリテラルか名前だけである(§3.2)。二つの再検査は、行ごとに区間を計算してその包(hull)をとり、それを列の取りうる値として使う。計算する値の区間と、一次のモデルの範囲の事実の両方である。証明書の `ranges` は、その包を含むことを確かめる。rulec の `ranges` も同じ計算(リテラルは一点、名前はその値の区間、行を合わせた包)なので、木の証明書では一致する。行の箱で名前の区間を狭めることはしない。rulec の `ranges` と違う読み方になり、rulec の到達点が再検査の狭い範囲の外に出て、正しい証明書が落ちるからである。`--rule` では、答えのセルも読み戻す。名前はその字を、リテラルは字と、字が表す数を確かめる(`%` は百分の一、通貨の単位と単位の無い数はそのまま。ほかの単位は数を確かめず、述べる)。計算する値と上の表の列は互いに頼る(`define` が列を読み、表が `define` を書く)ので、両方を一緒に、動かなくなるまで計算し直す。Lean は、`Values.lean` に `hullOf` と `writesWithin`、定理 `eval_mem_hull` と `eval_mem_stated` を足した。意味の側:Lean の「尋ねられる点」は、上の表の列の範囲の事実と、それを読む `define` の取りうる値を、証明書の `ranges` から作っていた。いまは包から作る(答えのセルから決まる形)。証明書の `ranges` は、`eval_mem_stated` で、セルが書く値を必ず含むことが示される。
- **重なりの検査。** `feasible` に、`define` の取りうる値と、真偽の `define` がとらない値を入れた。重なりの歩き(`first_reachable_pair`)の点ごとの確かめには、上の表の行と導出の組み合わせ(`above_rows_out`)も入れた。E101 と重なりの検査が、同じ点を届かないと見る。`unique` の表の証明書は、上の表の行で分かれる対を `above_apart` で言う。その列の、二つの行がともにとる値の一つ一つに、cover の `above_rows` の葉と同じ形の理由を付ける。二つの再検査は、行を自分で数えて理由を確かめる。Lean は、`abovePart` と定理 `not_asked_of_abovePart` を足し、`pairsApart` と `disjointAsked_of_pairsApart` に入れた。`define` の取りうる値と真偽の `define` だけで分かれる対の言い方は足していない。区間のセルなら、二つの行がそれぞれ届く点を持つとき、その列の交わりにも届く点がある(三つの区間が二つずつ交われば、三つとも交わる)。数の集合のセルでは起こりうるが、そういう対は前と同じく `undecided`(述べただけ)に入る。
- **ファイルを添えた再検査。** Python の `parse_cell` は `not:` しか読まず、`not 前二つ`(コロン無し。rulec はどちらも読む)を一つのリテラルと読んでいた。直した。二つの再検査は、「どの列にもどこかの行のセルがある」(§15.99 の 4)を、準用が持ち込んだ行の列にも求めていた。そういう列のセルは準用した規則のファイルにあり、その規則の証明書で読み戻す。準用した行がその列に `any` でない条件を持つなら、その列にはここで書いたセルを求めない。準用した行も持たない列は、これまでどおり落とす。木の全部の `.rule` の証明書を、二つの再検査に `--rule` を付けて通すテストを足した。数秒で済むので、絞っていない。
- **真偽の `define` のとる値。** 比べ方一つ(`<=`、`<`、`>=`、`>`)の真偽の `define` で、二辺の区間(日付のリテラルは日の番号)が比べ方を決めるとき、その値を、E101 の穴の探索、被覆、証明書の到達点、E102 の三つ目の形、重なりの検査が読む(`TableRegion::define_truth`)。§15.195 で `outputs_over` に足した読み方を、`check` に広げた。`=` は読まない。二つの再検査が四つの比べ方だけを読むのに揃えた。証明書は、とらない値の箱を新しい葉 `{"truth_axis": 軸}` で言い、日付のリテラルには日の番号(`value`)を書く。二つの再検査は、比べ方から真偽を計算し直して確かめる。Lean は、`Sieve` に `truth`(比べ方、二辺の区間、true の座標)を足し、「尋ねられる点」に「真偽の `define` の座標は、二辺の区間の中の値の組を比べた結果と同じ」を足してから、定理 `not_asked_of_truth`(`Truth.always_sound` と `Truth.never_sound` の上に立つ)と、到達点の確かめ(`Truth.can`、`Truth.exists_of_can`、`asked_of_witnessOk`)を書いた。`RitsuCross/Days.lean` の `AskedBut` にも同じ条件を足した。
- **文。** E102 の三つ目の形は、`define` の行に「入力の範囲では、いつも true です」と印を付け、注で「入力の範囲では、`軽減期間` はいつも true です。この行は false を求めています。」と言う。ヒントは「この行の `軽減期間` の条件を、それがいつもとる値に書き直すか、この行を削除してください。」。台帳の E101、E102、E105、W105、W114 の「いつ出るか」と直し方を直し、E102 の別の形の再現(英語と日本語)を一つ足した。
**捨てたもの**:
- 種類を `types` と `values` だけから求める形。`values` は導出と `define` を区別せず、`types` は入力と要素のフィールドと上の表の列を区別しないので、`of`、`inputs`、`walks` を足した。
- 包を、行の箱で名前の区間を狭めて計算する形(上の理由)。
- 重なりの証明書に、cover と同じ木(分ける軸と葉)を持たせる形。対の箱は軸ごとの座標の集まりで、cover の葉(葉までに決めた座標と、残りの軸の全部)とは形が違う。上の表の行で分かれる対は、一つの軸の値ごとに理由を言えば足りる。
- `=` の真偽を読む形(上)。
**確かめたこと**:
- 材料(英語の規則と日本語の版):`tests/sieve/` に、E101(`E101_a_define_that_is_always_true` と `E101_いつも真になる定義`。前は `reduced = false` を例に E101、いまは通る)、E105(`constraint` と上の表の行で交わりが届かない。前は `refund_size = small, limit = 61GBP` を例に E105)、W105(`policy first`。前は W105)、W114(導出と上の表の行。前は W114)。`tests/reach/` に E102(`E102_asks_what_a_define_never_is` と `E102_定義がとらない真偽を求める行`。前は通り、いまは行2 が E102)。範囲の中の全部の入力(日付は全部の日)で、その値や交わりに届く入力が無いこと、証明書の葉と `above_apart`、条件を少し動かすと本当の穴と重なりを前と同じに言うこと、英語と日本語の版が同じ行に同じ診断を出すこと、golden を確かめる。
- 偽造した証明書:`tests/cert.rs` と `tests/lean.rs` に四つずつ。列の種類(`define` を `input`、`upstream`、`derived` と書く、入力を `inputs` から落とす)、上の表が書く数の範囲(範囲を狭く言う、答えのセルの値を書き換える(ファイルを添えると落ちる)、字を書き換える)、真偽の葉(true の側に置く、比べる日を動かす、入力の範囲を延ばす)、上の表の行で分かれる対(別の値の理由、乗数、別の対)を、二つの再検査がどれも拒む。Lean の公理のテストに `not_asked_of_truth`、`not_asked_of_abovePart`、`asked_of_witnessOk`、`eval_mem_hull`、`eval_mem_stated` を足した。
- 替える前と後の突き合わせ:替える前は §15.195 のあとの木である。木の 321 の `.rule`(コーパス 87、変異 115、ほか 119。§15.195 で足した 9 を含む)を、`check`(英語、日本語、JSON の英語と日本語、`--show-shadow`。終了コードも)、証明書、`doc`(英語と日本語)、`coverage`(英語と日本語)、`vectors`、`gen`(全部の言語の生成物)で比べた。変わったのは証明書だけで、193 の証明書のどれも、足したもの(`inputs`、`walks`、`values[].of`、日付のリテラルの `value`、数の列を書く表を持つ 129 の証明書の `rows[].writes`)と、数を計算する `define` の列の `kind`(`rating_grade.rule`、`評価ランク.rule` と §15.195 の材料二つの 4 本で、`upstream` から `define`)のほかは、JSON として等しい。`check`、`doc`、`coverage`、`vectors`、`gen` は一字も変わらない。木に、新しく届かないと分かる点で穴や重なりを言っていた規則は無かった。足した 10 の規則では、E101 の二つが E101 から通るに、E102 の二つが通るから E102 に、E105 の二つが E105 から通るに、W105 の二つが W105 から W110 だけに、W114 の二つが W114 から通るに変わる。W114 の二つは生成コードのガードが無くなり、W105 の二つは生成した確かめのページが変わる。替えたあとの 193 の証明書と足した 8 は、新しい二つの再検査を、`--rule` つきでも無しでも通る(前から `--rule` つきで落ちていた `規程.rule` と `m_w118.rule` も通る)。替える前の 193 も新しい再検査を通り、列の種類と上の表が書く値の範囲は述べたものとして言う。古い二つの再検査は、替えたあとの 193 を前と同じに通し(`--rule` つきの二つは前と同じに落ちる)、足した材料のうち新しい葉と対を持つ 6 本を拒む。sekisho の木の 147 の `.gate`(`check` の英語・日本語・JSON の英語と日本語、`doc` の英語と日本語、`vectors`、`api`、生成した Cedar)は一字も変わらない。`outputs_over` の総当たりは、区間に限った 327 のうち 326、値に限った 358 のうち 358 で変わらない。ブラウザで試すページのテストは、作り直した `ritsu.wasm` で 7 件が通る。
- 全体のテスト:rulec の全体は 849 件が通った(一度目は `SKILL.md` が 500 行になって一件落ち、`AGENTS.md` の一行を詰めて回し直した)。rulec を読むほかの言語の関わるもの(sekisho の全部、dandori の規則の口、ritsu の `check`・`cross`・`playground` ほか、sakai、yuen)は 421 件が通った。ワークスペースの全体(`cargo xtask test --level tools`。先に `lake build`、PostgreSQL の層つき)は、312 の試験バイナリと 19 の doc テストで 2,371 件が通り、落ちたものは 0、ignored は 1(前からの `予算の分布を測る`)、SKIP の行は 13 で、どれも platforms の層のもの(Temporal、Argo、LocalStack、Ollama、TypeSafe、kani、ネットワーク)だった。
**残したこと**:
- 要素のフィールドの軸の `kind` は `upstream` のまま(上)。
- `define` の取りうる値と真偽の `define` だけで分かれる対(数の集合のセルでだけ起こりうる)は、`undecided` として述べる。
- 答えのセルの数を確かめるのは、`%`、通貨、単位の無い数だけである。ほかの単位は、字だけを確かめ、数は述べる。
### 15.197 記録を使う `diff` は、記録を旧版の読み方で読む(2026-10-08)
**きっかけ**:`rulec diff <旧> <新> --fixtures <記録>`(`ritsu rulec diff` も同じ)は、過去の記録の入力を二つの版で評価し直し、答えが変わる記録を数える。ところが、記録を新しい版の型で読んでいた。そのため、記録の `observed` が新しい版の出力の範囲に照らされ、外れた記録は「形式が宣言と食い違う」として外されていた。`diff` は `observed` を一度も使わないので、外す理由は無い。改正で答えが新しい範囲の外へ動いた記録、つまりいちばん比べたい記録が落ちていた。同じ記録を、旧版に対する `fixtures lint` は通す。
コーパスの `非常勤退職手当.rule` で、準用する `退職手当.rule` の支給表の短期を 5 か月から 6 か月にすると、新しい版の出力の範囲は 60 万円からになる。`gen` が書くいまの期待値(記録の形の 14 件)は、どれも答えが報酬月額の 1 か月分だけ増えるのに、照合は 8 件で、旧版の答えが 60 万円に届かない 6 件は外れていた。一件だけの記録なら「照合できた記録はありません」で終わり、刻みや単位の読み方(`--read-as`)を案内していた。木のテストの材料にも同じ形があった。`注文の状態.rule` を、入金で出荷済まで進めるように変えた版(`tests/machine.rs`)では、取消で 5,000 円を返す旧版の答えが新しい版の返金額の範囲の外にあり、その記録が外れていた。
入力の側にも同じ根がある。新しい版が入力を一つ消すと、記録に残ったその入力が「規則が知らないフィールド」になり、全部の記録が外れていた。新しい版が入力の範囲を狭めると、新しい範囲の外の入力の記録は、差として数えられずに外れていた。
**決定**:
1. **記録は、それを書いた旧版の読み方で読む。** 記録は旧版のシステムが残したものなので、`diff --fixtures` は記録を旧版の名前と型(刻み、単位、範囲、列挙)で読む。`--read-as <file.rule[@rev]>` を付ければ、これまでどおり、数をその版の刻みと単位で読み、旧版の単位に直す(§15.145 の直す先を、新しい版から旧版にした)。旧版でも受け付けない入力の記録は、これまでどおり形式の食い違いとして外す。報告の文は「形式が旧版の宣言と食い違う記録を 1 件外しました」として、どの版で読んだかを書く。JSON の `excluded` の鍵は `bad_format` のままである。
2. **`diff` は `observed` と `trace` を読まない。** 二つの版の答えを比べるのに、そのとき出た値は関わらない。旧版の型で `observed` を検めることもできるが、それで外れるのは、記録を書いたシステムの答えが旧版の取りうる範囲の外にある記録で、二つの版の差とは関係がない。記録の形を版に照らすのは `fixtures lint` の役目で、記録を書いた版に対して走らせる。`diff` は `observed` の無い記録も読む。`fixtures lint` と `replay` は、これまでどおり全部を読む。
3. **新しい版が受け付けない入力は、外さずに差として数える。** 旧版の読み方で読んだ入力を一つずつ新しい版の型に移し(金額と数量は、次元が同じなら単位を換算する)、新しい版の宣言に照らす。照らすのは、生成したコードが入口で拒むものと同じで、範囲、列挙の値、型(none を取らない入力、種類や通貨の違う型)、刻みと単位(新しい版の刻みの整数にならない値)、日付の範囲と koyomi の日、`constraint` である。外れた記録は照合した記録に数え、一致には数えない。報告では、答えが動いた記録とは別に、理由ごとにまとめて出す。テキストは「新しい版が受け付けない入力 6 件 (42.857%)」の見出しと理由の行、Markdown は表の行と内訳の表、JSON は `refused` である。理由には、記録ごとに違う値を入れない。範囲の外の金額が何通りあっても一行にするためである。列挙の値だけは、数が少なく、値ごとに理由が違うので入れる。例には、記録の入力と旧版の答えを出す。終了コードは、答えが動いた記録があるときと同じく 1 にする。
4. **JSON の `refused`** は、理由ごとに `kind`、`field`、`count`、`witness`(`in` と `theirs`)、`records`(`line` と `tag`)、`what` を持つ。`kind` は `input_range`、`enum_value`、`input_type`、`input_step`、`input_day`、`constraint` のどれかで、記録を使わない `diff` の `domain` と同じ語を使えるところは同じにした。`field` は理由になった入力で、二つの入力にまたがる `constraint` では付けない。`verify` と `replay` の JSON にも空の `refused` が出る。三つは同じ形だからである。
5. **片方の版にしか無い入力。** 新しい版だけが取る入力は、旧版の記録に無い。マニフェストと `--fill` は、旧版の入力なら旧版の型で、新しい版だけの入力なら新しい版の型で読む。既定値で補った記録は、これまでどおり補った記録として別に数え、既定値の無い記録はフィールドが欠けた記録として外す。旧版だけにある入力は、記録から読み、新しい版には渡さない。
6. **例の入力は、記録にあるとおりに出す。** 記録を読んだときに、旧版の型で、人が読む形とやりとりする整数の形を作っておく(新しい版だけの入力の既定値は、新しい版の型で)。新しい版が単位を変えた入力や、新しい版に無い入力も、記録の値のまま出る。
7. **ステートマシンの記録(案件)も同じ読み方にした。** 案件の呼び出しを一つずつ新しい版に渡すとき、記録一件と同じく型を移し、新しい版の宣言に照らす。受け付けない呼び出しがあれば、その案件はそこで「途中でエラーになる」(JSON の `cases.refused`)。旧版が読めない記録は、これまでどおり、その行で案件を止める。新しい版だけが取る入力が記録に無く、既定値も無い記録は、案件から外す(フィールドが欠けた記録と同じ)。
8. **`fixtures lint` と `replay` も、生成したコードが入口で拒む入力を、記録の誤りとして報告する。** 範囲の外の数は、もともと記録の形の誤りだった。日付の範囲、koyomi の日、`constraint` も入口で拒むものなので、記録を読むときに照らし、外れた記録を `bad_input` として報告する(`constraint` は二つの入力にまたがるので、`field` を付けない)。これをしないと、旧版でも受け付けない組み合わせを新しい版の差として数え、`fixtures lint` が通した記録を `diff` が外すことになる。
**捨てたもの**:
- **旧版の型で `observed` も検める。** `diff` が照合する記録が、旧版に対する `fixtures lint` が通す記録とちょうど同じになる。ただ、`diff` が使わない値で記録を外し、分母を減らす。記録を書いたシステムが旧版の範囲の外の値を出していたこと(旧実装の誤り、転記の誤り)は、`replay` と `fixtures lint` が見つけることで、二つの版の差ではない。
- **新しい版が受け付けない記録を、答えられなかった記録(`unanswered`。一致率の分母から外す)として数える。** 受け付けないことは、新しい版がその入力に返すものそのものである。分母から外すと、入口を狭めるだけで一致率が上がる。
- **受け付けない記録を、答えが動いた記録の塊(`clusters`)に混ぜる。** 塊の鍵は当てはまった行の組で、新しい版はどの行にも当たらない。金額の差も無く、差の合計に入らない。別の並びにして、見出しも分けた。
- **記録に、書いた版を書き込む。** §15.145 で捨てた理由と同じである。記録を書く側が十数か所あり、どれも同じ記録を出すことをテストが見張っている。
**確かめたこと**:
- 再現の材料の二つ:一件の記録は、替える前は照合 0 件(外した 1 件)で、いまは照合 1 件。14 件は、替える前は照合 8 件(外した 6 件)で、いまは 14 件とも照合し、14 件とも動く(差の合計 +4,200,000)。
- `tests/diff_records.rs` の八つ。どれも英語の規則(コーパスの `part_time_retirement_pay.rule` と `retirement_pay.rule`、英語で書いた小さな規則)を先に、日本語の版(`非常勤退職手当.rule` と `退職手当.rule` ほか)を横に置いて、同じことを確かめる。
- 答えが新しい範囲の外へ動いた記録が照合に入る(一件と 14 件)。旧版に対する `fixtures lint` は通り、新しい版に対する `fixtures lint` は `observed` を範囲の外と言う。
- 新しい版が報酬月額を 40 万円までに狭めると、14 件のうち 6 件が `input_range` として理由ごとに出て、例は記録の値のまま出る。上限だけを変えた版でも、終了コードは 1 になる。
- 旧版が読めない記録は外れ、`observed` の無い記録は照合に入る。
- `--read-as` がこれまでどおり効く。キログラムで書いた記録を、グラムで数える旧版で読む。
- 新しい版だけが取る入力を `--fill` とマニフェストで補い、旧版だけにある入力は新しい版に渡さない。
- 理由の四つ(列挙の値、刻み、範囲、`constraint`)が、それぞれの `kind` で出る。旧版にもある `constraint` を破る記録は、`fixtures lint` が報告し、`diff` は外す。
- ステートマシンの記録で、新しい版が支払額の範囲を狭めると、その案件は最初の呼び出しで `cases.refused` になる。
- 型が移るかどうか。旧版がキログラムで数える重量は、グラムで数える新しい版に移って照合に入る。optional でなくなった入力の none と、列挙から真偽になった入力の値は、どちらも `input_type` として出る。
- ritsu の `tests/rulec_diff.rs`。koyomi の日は、koyomi をつないだ `ritsu rulec` でしか読めないので、`ritsu rulec diff` で確かめる。払う日を 10 日から 15 日にした版で、10 日の記録は `input_range` と `input_day` として出る。旧版の払う日でもない記録は外れ、`fixtures lint` がそれを報告する。英語(`settlement.rule`)と日本語の版(`精算.rule`)。
- 替える前と後の突き合わせ(240 回)。木のテストで `diff --fixtures` を使う材料(`tests/m3.rs`、`tests/formats.rs`、`tests/machine.rs`、`tests/vdiff.rs` のコーパスの全部)と、サイトの四つの例(`compare.md` と `scenarios.md` の英日)を、テキスト、`--terse`、Markdown、JSON で、英語と日本語で走らせた。同じ材料の `fixtures lint` と `replay` も比べた。変わったのは三つだけだった。JSON に空の `refused` が増えた(92 回)。外した記録の文が「形式が旧版の宣言と食い違う」になった(18 回)。`tests/machine.rs` の入金で出荷済まで進める版で、旧版の答えが新しい範囲の外にある記録が照合に入った(10 回。照合 4 件が 5 件に、影響 3 件が 4 件になった)。`fixtures lint` と `replay` の出力は一字も変わらなかった。
- 文書:サイトの `compare.md`(英日)の、記録の上の `diff` の節に、記録を旧版の読み方で読むこと、新しい版が受け付けない入力を差として数えることを書き足した。英語の例は、外した記録の文が変わるので、組み直した材料で取り直した(替える前の rulec で、貼ってあった出力がそのまま出ることを確かめてから)。`docs/formats.md`(とスキルのコピー)、`AGENTS.md`(とスキルの `SKILL.md`)、§10.3 と §10.4 を直した。
**見つけて直していないもの**:
- **出力の刻みを粗くした版では、記録の `diff` が答えの動きを見落とす。** 旧版の答えを新しい版の刻みの整数に直してから比べるので(`replay::diff` の `wire(new.1, …)`)、12.3% を返していた出力を 1% 刻みにして 12% を返す版でも「不一致はありません」になる。記録を使わない `diff` は、それぞれの版の整数で比べて、違うと言う。記録の読み方ではなく答えの比べ方の問題なので、ここでは直さなかった。値で比べるか、それぞれの版の整数で比べるかと、差の大きさをどの刻みで書くかを決める要がある。§15.199 で直した(二つの答えを値として比べ、差は新しい版の単位で丸めずに書く)。
- **日付どうしの `constraint` を、ベクタが守らない。** `vectors::allowed` は数どうしの比べしか見ないので、`constraint check_in <= check_out` の規則では、17 件のベクタのうち 6 件が制約を破る。生成したコードは入口でそれを拒み、`rulec test` は 17 件が食い違うと言う。記録を読む側(`fixtures::holds`)は日付も比べる。`allowed` は E019、網羅の総当たり、ステートマシンの解析、ritsu の口でも使われるので、ここでは直さなかった。木の規則には、日付どうしの `constraint` は無い。§15.198 で直した。
- **`replay` は、記録の `observed` を規則の出力の範囲に照らして外す。** 出力の範囲は宣言ではなく計算したものなのに、文は「宣言した範囲」と言う。旧実装が規則の取りえない値を返していた記録は、不一致ではなく形の誤りとして外れ、一件も照合できなかったときは `--read-as` を案内する。`非常勤退職手当.rule` で、旧実装の答えが 499,999 円(規則の範囲の下の端より 1 円少ない)の記録も外れる。旧実装と規則の食い違いを探す `replay` で、食い違いが分母から消えることになる。`diff` の直しの外なので、ここでは直さなかった。§15.199 で直した(`replay` は範囲の外の `observed` も照合して不一致に数え、`fixtures lint` はそれを形式の問題とせず別に出す)。
### 15.198 二つの日付のあいだの `constraint` を、入力を組み立てる所でも日の番号で比べる(2026-10-08)
**きっかけ**:§15.197 の「見つけて直していないもの」の二つ目。`vectors::allowed` は、`constraint` の二辺が数どうしのときだけ比べ、ほかの組は成り立つとしていた。二辺が日付だと、どの組も通る。テストの材料に足した `hotel_stay.rule`(`constraint check_in <= check_out`)では、`rulec vectors` の 146 件のうち 66 件が、チェックアウト日をチェックイン日より前にしていた。`rulec gen` のあとの `rulec test` では、この規則の 23 の実行がどれも食い違った。22 は生成したコードが入口で止まり、NumPy は答えが食い違った。
`allowed` を使う所は、どこも同じ穴を持っていた。チェックアウト日がチェックイン日より前の例(E019 の形)は、検査を通った。ritsu の口の `outputs_over` は、二つの日付の範囲を交差させて聞くと、`low` の例に 2026-06-25 と 2026-06-20 を出した。`allowed` と同じ比べ方を自分で持っていた所も同じだった。`diff`(`vdiff::constraints_hold`)は、`stay_pair` の差の例に 2026-07-15 と 2026-01-01 を出した。ritsu の口の `eval` は、チェックアウト日がチェックイン日より前の入力にも、範囲の外の日付にも、koyomi の日でない日(§15.174)にも答えた。どれも、生成したコードが入口で拒む入力である。
**決定**:
1. **一つの `constraint` が成り立つかは、一つの関数で決める**(`vectors::constraint_holds`)。二辺は E018 で同じ順序のある型に限られるので、数は数として、日付は日の番号(§2.1)として比べる。生成したコードの入口と、記録を読む側(`fixtures::holds`、§15.197)と同じ読み方である。`allowed`、E019 が破った制約を挙げる所、`diff` の入力の組み合わせ(`vdiff::constraints_hold`)、ritsu の口の `eval` がこれを使う。`allowed` を通る所(ベクタ、E019、`grid.rs` の総当たり、ステートマシンの解析、`outputs_over`、E101 の例を作る入力、重なりの例)は、これで日付どうしの制約も守る。
2. **`constraint` に書ける形は、数どうしと日付どうしの二つだけである。** 構文は `<入力> <比べ方> <入力>` で(E017)、二辺は入力の名前(E018。定数や式は書けない)、型は金額・数量・率・`number`・日付で、二辺は同じ型である。日付と日付の定数も、日付の足し算も、`constraint` には書けない。点の上で `constraint` を比べる所は、ほかに無かった。区間で読む所(region のふるい、`grid.rs` の区画、`fourier.rs` の消去、網羅の境界の側、証明書と二つの再検査)は、もともと日付を日の番号の区間として読んでいた。
3. **`diff` は、制約が結ぶ二つの入力を、導出が結ぶ入力と同じに扱う。** `diff` とステートマシンの解析は、入力の組み合わせ(座標の区画)を一つの点で代表させて歩く。制約が日付も読むようになると、代表の点が制約の外にある組み合わせが、「入力を作れず、無いことも示せなかった」組み合わせになった。`hotel_booking.rule` では、ステートマシンの主張の W127 が三つから五つに増えた。数どうしの制約でも、前から同じだった。そこで、組み合わせの名前の二つを `constraint` が結ぶときは、その組み合わせに入力が無いことを消去で示し(`linearly_empty`)、代表の点が制約の外なら、組み合わせの中の点を消去で探す(`solved_point`)ことにした。W127 は二つになった。消去が置いた日付は、日付として入力に戻す。これまでは日の番号のまま数として入れていたので、日付を一点で書いたセルに当たらず、`diff` の例にも `issued=20454` と書いていた。
4. **ritsu の口の `eval` は、生成したコードが入口で拒むものを、日付でも拒む。** ritsu の DESIGN の口の表は、`eval` が「型と列挙と範囲と入力どうしの関係を破るもの」を受け付けないと言う。数の範囲と数どうしの制約しか見ていなかったので、日付の範囲、koyomi の日、日付どうしの制約も見るようにした。`preconditions_hold` が、日付どうしの関係を破る角を例に出すときは、日付として出す(これまでは日の番号の整数だった)。
5. **E019 の台帳に、二つの日付のあいだの制約を破る例も、これまで検査を通っていたことを書いた。** これで新しく E019 で止まる規則が出るのは、この形だけである。
6. **ステートマシンの主張が決まらなかったとき、`rulec doc` のページはそう書く。** ページは、終わりの状態から出ないこと(E124)と、どの状態からも終わりの状態へ行けること(E125)を、全部が成り立つか、そうでないかの二つでしか書いていなかった。決まらなかった主張(W127)まで「終わりの状態に着けなくなる状態があります(E125)」と書き、`check` が言っていない誤りをページが言っていた。`hotel_booking.rule` のページで見つかった。`never` と `once` の行と同じく、決まらなかったときは W127 と書く。木の規則には、主張が決まらないステートマシンが無いので、これで変わるページは無い。
**捨てたもの**:
- **記録を読む側の `fixtures::holds` を呼ぶ形。** 記録の読み方は同じ時期に別に直していて、その関数は記録の型に沿って書かれている。同じ読み方の関数を `vectors` に置いた。二つを一つにするのは次の機会にした。
- **`diff` の組み合わせの代表の点を、制約を満たすように選び直す形。** 代表の点を変えると、制約の無い規則の例まで変わる。消去は、制約が組み合わせの二つの名前を結ぶときにだけ走らせた。
**確かめたこと**:
- `tests/date_constraint.rs` の十。英語の規則(`hotel_stay.rule`、ステートマシンの `hotel_booking.rule`、テストの中に書いた三つ)を先に、日本語の版(`宿泊料金.rule`、`宿泊の予約.rule` ほか)を横に置いて、同じことを確かめる。
- ベクタのどれも制約を守り、網羅の基準はどれも満たされる。`hotel_stay.rule` のベクタは 146 件から 80 件になり、行は 5/5、境界の対は 9/9 のまま。
- 二つの日付を比べる `define`(デイユース)の同着の点のベクタも、制約を守る。
- チェックアウト日がチェックイン日より前の例は E019 になる。
- E101 が計算する列の穴に添える入力も、制約を守る。これは替える前から通っていた(線形のモデルが、もともと日付どうしの制約を読んでいたため)。押さえとして残す。
- `outputs_over` の例は、範囲を限らないとき、チェックイン日を夏に限るとき、二つの日付の範囲を交差させるとき、どれも制約を守り、範囲の中にある。
- ritsu の口の `eval` は、チェックアウト日がチェックイン日より前、範囲の外の日付(前と後ろ)、koyomi の支払日でない日を拒む。`preconditions_hold` は、破る角を `2026-08-31` と `2026-06-01` で出す。
- `diff` は、`stay_pair`(一行の金額を変えた版)と `coupon_window`(入力を共有する二つの導出と、発行日と使用日)で、例が制約を守り、日付を日付で書き、入力も、無いことの証明も無い組み合わせが 0 になり、報告した区域の外は同じだと言い切る。
- ステートマシンの例(`scenario`)の呼び出しが制約を破ると E019 になり、入力を作れない呼び出しの組み合わせが残らない。決まらずに残る主張は、ページも W127 と書く。
- 生成したコードの全部の言語(Java と、PostgreSQL の関数の側を含む。道具の無いものは SKIP の行)が、ベクタと手順の例に一致する。
- 替える前のコードでは、十のうち九が落ちる(E101 の一つだけが通る)。
- 証明書と二つの再検査は、到達点の値と覆いの葉の制約を、もともと日の番号の数として確かめていた。`tests/cert.rs` と `tests/lean.rs` に、到達点のチェックアウト日をチェックイン日より前にした証明書と、同じ日に始まり終わる区画を制約で落とした証明書を、両方の再検査が拒むことを確かめるテストを足した(英語と日本語の材料)。
- 替える前と後の突き合わせ。木の 331 の `.rule` で、`check`(英語、日本語、JSON の英語と日本語、`--show-shadow`。終了コードも)、証明書、`doc`(英語と日本語)、`coverage`(英語と日本語)、`vectors`、`gen`(全部の言語の生成物)を比べ、一字も変わらなかった(木に日付どうしの `constraint` は無い)。sekisho の木の 149 の `.gate`(`check` の英日と JSON、`doc` の英日、`vectors`、`api`、生成した Cedar)も変わらない。`diff` は、`constraint` を持つ木の 22 の規則のうち、`examples` を外して表の金額を一つ動かした版を作れた 8 本で比べた。5 本(`express_delivery_quote.rule`、`速達の見積.rule`、`pro_rata_allocation.rule`、`比例配分.rule`、変異の `m_w124.rule`)で、入力を作れなかった組み合わせ(140、140、90、90、180)が 0 になり、報告した区域の外は同じだと言うようになった。違う組み合わせの数と例は変わらない。残りの 3 本は変わらない。足した材料では、`hotel_stay.rule` と `宿泊料金.rule` のベクタ(146 件から 80 件)と網羅の件数と生成物が、`hotel_booking.rule` と `宿泊の予約.rule` では、それに加えて `check` の W127(三つから二つ)とページが変わった。証明書はどれも変わらない。
**見つけて直していないもの**:
- **NumPy の生成物は、`constraint` を入口で見ない。** 計画(`<別名>.json`)に制約が無く、実行時(`rulec_np.py`)も見ない。数どうしの制約でも同じで、`express_delivery_quote.rule` の NumPy は、`declared` が 200,000 円で `cover` が 100,000 円の入力に 1,300 円と答える。ほかの言語は入口で拒む。ベクタは制約を守るので、`rulec test` はこれを見つけない。直すには、計画に制約を載せ、実行時が行ごとに確かめる要がある。実行時のファイルはどの規則の生成物にも入るので、全部の規則の `gen` の出力が変わる。§15.200 で直した。
- **ステートマシンで、二辺とも `held` の制約が、持ち越す入力の結びつきに数えられる。** `coupled_inputs` は、`constraint` に出る `held` の入力を、呼び出しごとに変わる入力と結ぶものとして扱う。二辺とも `held` なら、制約は世界を狭めるだけで、一つの世界の中の値はどれも同じ呼び出しを許す。それでも「終わりの状態に着ける」が決まらず、`hotel_booking.rule` に W127 が二つ残る。数どうしの制約でも同じである。
- **証明書の到達点は、二つの名前を比べる真偽の `define` を自由な軸として扱う**(§5.3、§6.2)。その名前が制約で線形のモデルに入ると、到達点が置く値が `define` と食い違うことがある。`tests/date_constraint.rs` の `day_use`(`define day_use : bool = check_out <= check_in`)では、行 2 の到達点が `day_use = false` なのに、チェックイン日とチェックアウト日をどちらも 2026-01-01 に置く。二つの再検査も `define` を自由な軸として読むので、これを通す。日付に限った話ではない。
### 15.199 二つの答えは値として比べる。`replay` は規則の取りえない答えも照合する(2026-10-08)
**きっかけ**:§15.197 の「見つけて直していないもの」の二つ。どちらも、答えの食い違いを黙って見落とす向きの誤りである。
- 記録を使う `diff` は、旧版の答えを新しい版の受け渡す整数に直して(`wire(new, …)`)、新しい版の答えの整数と比べていた。`rate[step 0.1%]` で 12.3% を返す規則を、`rate[step 1%]` で 12% を返す版にすると、12.3% は新しい版の刻みで 12 になり、「不一致はありません」と答えた。単位は換算しなかったので、円を銭にした版では、額を変えていない 500 円も 500 と 50,000 の差(+49,500)になった。記録を使わない `diff` は、それぞれの版の整数を文字として比べていた。12.3% → 12% は「違う」と言えたが、刻みや単位を変えた版では、値の変わらない答えまで動いたと言った(2% が `rate: 20 → 2`、500 円が `手数料: 500 → 50000`)。
- `replay` は、記録の `observed` を規則の出力の取りうる範囲(宣言ではなく計算したもの)に照らし、外れた記録を「形式が宣言と食い違う」として外していた。`非常勤退職手当.rule` に、旧実装の答えが 499,999 円の記録(規則の範囲の下の端は 50 万円)を流すと、照合 0 件、外した 1 件で終わり、`--read-as` を案内した。旧実装が規則の取りえない値を返したことは、規則と旧実装の食い違いそのもので、`replay` がいちばん見つけたいものである。`fixtures lint` は同じ記録を「宣言した範囲の外」と報告していたが、この範囲は宣言したものではない。
- 小数も同じ向きだった。記録の `observed` の小数(旧実装が 1 円より細かく計算した 820.5 円)は、「整数を期待しましたが小数でした」として外れた。`verify` は旧実装の答えを文字として比べていたので、Python の `float` で計算して `11.0` と書くアダプタは、規則と同じ値を返しても 96 件のうち一件も一致しなかった。差は整数でしか数えなかったので、`22.5` と答えた件は、差も「丸め方の違いの疑い」も出なかった。
**決定**:
1. **二つの答えは、どちらかの刻みに丸めずに、値として比べる。** 金額と数量は量として比べ、単位が違えば換算する(円と銭、キログラムとグラム、℃ と ℉。℉ は 32 度のずれも含めて換算する)。率と数はそのまま比べる。`replay` と `verify` は規則の答えと記録や旧実装の答えを、`diff` は二つの版の答えを、記録を使うときも使わないときも同じ関数(`report::same_answer`)で比べる。記録を使わない `diff` は、セルを歩くときも、同じセルのほかの入力を試すときも、ステートマシンの道を探すときも、この関数で比べる。刻みや単位だけを変えた版は、どの答えも動かないので「同じ」になる。ただ、生成したコードの呼び出し側が受け取る整数は変わる。そこで記録を使わない `diff` は、出力の型か刻みが変わったことを `domain` に出すようにした(`output_type` と `output_step`。これまで出力について出していたのは、増えた、消えた、丸めが変わった、の三つだった)。`domain` に何かあれば、これまでどおり 1 で終わる。
2. **差は、規則(`diff` では新しい版)の受け渡す単位で、丸めずに書く。** 二つの答えが規則の刻みに載っていれば、差はその刻みの整数で、これまでと同じである。旧版の刻みのほうが細かいとき、記録や旧実装の答えが刻みより細かいときは、小数になる(1% 刻みの版の 12.3% → 12% は −0.3、820 円に対する 820.5 円は −0.5)。合計、最小、最大も同じで、テキストは三桁ごとに区切り、JSON は数として書く。小数で書けない差(42℉ を返していた旧版と 6℃ を返す版の 4/9 度など)だけは、分数の文字列(`"4/9"`)にする。新しい版の単位で書くのは、二つの理由からである。記録を使わない `diff` も、新しい版の答えを新しい版の整数で書く。そして、記録ごとの差を足して合計にするには、単位が一つでなければならない。
3. **例の二つの答えは、それぞれの側の刻みと単位で、丸めずに書く。** テキストと Markdown は人の読む形で書く(「規則 料率=12% / 旧版 料率=12.3%」)。二つの版の単位が違うときは、数に単位を添える(「規則 手数料=200050銭 / 旧版 手数料=2000円」)。数だけでは、同じ単位に読めてしまうからである。記録を使わない `diff` のテキストも、整数ではなく同じ形で書く(「料率: 12.3% → 12%」)。整数のままだと、刻みの違う二つの版で `123 → 12` と、桁の違う数が並ぶ。JSON の `witness` は、`ours` を規則の受け渡す整数で書き、`theirs` を相手の書き方で書く。`diff` は旧版の受け渡す整数(`123`。記録を使わない `diff` の `outputs[].old` と同じ)、`replay` は規則の受け渡す単位の値(刻みより細かければ `123.4`)、`verify` はアダプタが返したとおりである。記録を使わない `diff` の JSON の `outputs[].old` と `new` は、これまでどおりそれぞれの版の整数である。
4. **`replay` は、`observed` の形だけを検める。** 型、列挙の値か、数であることを宣言に照らし、計算した取りうる範囲には照らさない。範囲の外の値の記録も照合し、不一致として数える。範囲の外の値を持つ記録の数は、見出しの下に出力ごとに書き(「観測の値が規則の取りうる範囲の外にある記録 2 件(料率 2 件)。不一致に数えています」)、JSON の `out_of_reach`(出力の名前から件数へ)にも入れる。同じ所に `--read-as` を案内する。刻みや単位を変えた後の規則で、変える前に書いた記録を読むと、たいてい範囲の外の値になるからである(§15.145 の 4 を、照合できた記録がある場合にも広げた)。範囲の外の値が、旧実装の誤りなのか、刻みを変える前の記録なのかは、記録だけからは決められない。だから外さずに照合し、読み方を添える。
5. ★ **刻みに載らない `observed` は、旧実装が刻みより細かく計算していた不一致として照合する。** `observed` は小数を受け付け、書かれた桁のまま正確に読む(0.1% 刻みの出力の 12.34% は `123.4`)。差が出力の丸めの刻みより小さければ、これまでどおり「丸め方の違いの疑い」が付く(§10.4)。`--read-as` は案内しない。刻みや単位を変える前の版の記録は、その版の刻みの整数で書かれているので、小数にはならないからである(変える前の記録は、いまの刻みの整数か、範囲の外の値として現れ、後者は 4 で案内する)。入力は、これまでどおり受け渡す単位の整数だけを受け付ける。入力の小数は、生成したコードが入口で受け付けない値で、規則がそれに何を答えるかが決まらないからである。
6. **`fixtures lint` の形式の問題は、`replay` が外す記録とちょうど同じにする。** `lint` が通した記録を `replay` が外すことも、その逆も無い。範囲の外の `observed` は、形式の問題とせず、「observed の値が規則の取りうる範囲の外にある記録(形式の問題ではありません)」として出力ごとにまとめて別に出す。件数と例に、`replay` が不一致として数えることと、`--read-as` の読み方を添える。JSON は `out_of_reach`(`field`、`count`、`example`、`what`、`hint`)である。終了コードは形式の問題だけで決まる。文は「宣言した範囲」をやめて「規則が取りうる範囲」と書く。入力の範囲は宣言したものなので、入力の文は「宣言した範囲」のままである。
7. **`verify` も、答えを同じ形で読む。** アダプタの答えを `observed` と同じ関数(`fixtures::observed_as`)で値にし、値として比べる。`11.0` は 11 と一致し、`22.5` は −0.5 の差になる。規則の取りうる範囲の外の答えの数は、記録と同じく見出しの下と `out_of_reach` に出す(「現行の答えが規則の取りうる範囲の外だったもの 3 件(運賃 3 件)。不一致に数えています」)。`verify` に `--read-as` は無く、アダプタはいまの規則の単位で答えるので、読み方は案内しない。
8. **JSON の `out_of_reach`** は、`verify`、`replay`、`diff` の三つに出る(`diff` では空)。三つは同じ形だからである(§15.197 の `refused` と同じ)。
**捨てたもの**:
- **新しい版の刻みに直して比べる(これまでの記録を使う `diff`)。** 粗くした刻みで丸められる動きが消える。
- **それぞれの版の受け渡す整数を比べる(これまでの記録を使わない `diff`)。** 刻みや単位だけを変えた版で、動いていない答えを動いたと言う。
- **細かいほうの刻みに直して比べる。** 記録や旧実装の答えは、どちらの刻みにも載らないことがある。どの刻みに載せても、どこかで丸める。値で比べれば丸めは無い。
- **差を旧版の単位で書く。** `replay` と `verify` の差は規則の単位で書くので、`diff` だけが相手の単位で書くことになる。記録を使わない `diff` も、新しい版の答えは新しい版の整数で書く。
- **刻みに載らない `observed` を、形式の誤りとして外す(これまで)か、`--read-as` を案内する。** 外せば、旧実装が刻みより細かく計算していたこと、つまり丸め方の違いが分母から消える。§10.4 の「丸め方の違いの疑い」は、これを見つけるためにある。`--read-as` は、刻みや単位の違う整数の読み方で、小数には当てはまらない。
- **範囲の外の `observed` を、`--read-as` の手がかりとして外す。** 刻みを変える前の記録か、旧実装の誤りかは、記録からは区別できない。外せば後者を見落とすので、照合したうえで前者の読み方を案内する。
- **`fixtures lint` で、範囲の外の `observed` を形式の問題のまま残す。** `lint` が 1 で止めた記録を、`replay` が照合することになる。
**確かめたこと**:
- `tests/compare_values.rs` の六つ。どれも英語の規則を先に、日本語の版を横に置く。
- 0.1% 刻みの率を 1% 刻みにした版で、記録を使う `diff` は 12.3% → 12% を差(−0.3、丸め方の違いの疑い)として出し、2% は同じとする。記録を使わない `diff` も、同じ入力を「違う」と言い、2% を「同じ」と言い、出力の刻みが変わったこと(`output_step`)を出す。例は「規則 料率=12% / 旧版 料率=12.3%」と「料率: 12.3% → 12%」である。
- 円を銭に(ドルをセントに)した版で、額を変えなかった答えは同じ、50 銭を足した答えだけが 50 動く。二つの `diff` が同じことを言い、例は「規則 手数料=200050銭 / 旧版 手数料=2000円」と、数に単位を添えて書く。記録を使わない `diff` は、出力の型が変わったこと(`output_type`)を出す。
- ℉ を ℃ にした版で、42℉ を返していた旧版と 6℃ を返す版の差は 4/9 度になり、JSON は `"4/9"`、例は「規則 上限温度=6℃ / 旧版 上限温度=42℉」である。5℉ と −15℃ は同じ温度なので動かない。記録を使わない `diff` も同じ入力を「違う」と言い(「上限温度: 42℉ → 6℃」)、出力の型が変わったことを出す。
- `非常勤退職手当.rule`(英語は `part_time_retirement_pay.rule`)で、範囲の外の 499,999 円、1 円より細かい 500,000.5 円、範囲の中の 600,000 円を照合し、三つとも不一致になる(差は +1、−0.5、−100,000、合計 −99,999.5)。範囲の外の件数と `--read-as` の案内が出る。列挙に無い値の記録は、`replay` と `fixtures lint` が同じ一件を外す。`fixtures lint` は範囲の外の値を形式の問題とせず、別に出す。
- 0.1% 刻みの規則で、12.34% の記録は照合されて −0.4 の差(丸め方の違いの疑い)になる。`fixtures lint` は通る。1% 刻みの版が書いた記録は、範囲の外の値として照合され、`--read-as` の案内が出る。`--read-as` で読めば、2% は一致し、12% は 12.3% との差になる。
- `verify` で、`123.4` と答えるアダプタと、範囲の外の値を答えるアダプタ。
- `tests/m3.rs` の汚れた記録の一件は、`observed` の小数(820.5)から入力の小数(`重量` 1000.5)に替えた。`observed` の小数は形式の誤りではなくなったので、小数の報告は入力で確かめる。
- `tests/diff_records.rs`:新しい版に対する `fixtures lint` は、範囲の外の `observed` を形式の問題とせず、`out_of_reach` に出して 0 で終わる。
- 替える前と後の突き合わせ(706 回)。木のテストで `diff --fixtures`、`replay`、`fixtures lint`、`verify` を使う材料(`tests/m3.rs`、`tests/formats.rs`、`tests/machine.rs`、`tests/vdiff.rs` のコーパスの全部、`tests/verify.rs` の二つの偽の旧実装)と、サイトの例(`compare.md` と `scenarios.md` の英日)を、テキスト、`--terse`、Markdown、JSON で、英語と日本語で走らせた。同じだったのが 474 回、JSON に空の `out_of_reach` が増えただけが 204 回で、違ったのは 28 回、どれも直した所だった。`observed` に小数を持つ汚れた記録が照合に入った(`fixtures lint` の問題は 6 件から 5 件になり、`replay` は −0.5 の差を丸め方の違いの疑いとして出す。8 回)。1% 刻みで書いた記録を 0.1% 刻みの規則で読むと、範囲の外の値として照合され、`fixtures lint` は 1 ではなく 0 で終わる(8 回)。刻みだけを変えた版を、記録を使わない `diff` が「違う 2」ではなく「同じ 2」と言い、出力の刻みが変わったことを出す(4 回)。出力を円から銭にして表の金額を同じに書いた版は、これまで「答えを決めるものは何も変わっていません」で 0 だったが、出力の型が変わったことを出して 1 で終わる(4 回)。日本語の偽の旧実装に対する `verify` に、範囲の外の答え 3 件の行と `out_of_reach` が出る(4 回)。
- 文書:`docs/formats.md`(とスキルのコピー)の `verify`・`replay`・`diff` の JSON の例は、古い偽の旧実装の出力のままだったので、いまの偽の旧実装の実物で取り直し、`tests/verify.rs` が、その頭と差と末尾を実物と照らすようにした。同じ文書に、`out_of_reach`、差と例の答えの書き方、`fixtures lint` の `out_of_reach`、記録の小数と範囲の外の値、アダプタの答えの読み方、記録を使わない `diff` の比べ方を書いた。サイトの `compare.md`(英日)に、値として比べることと範囲の外の答えの扱いを書き、日本語の `verify` の例を取り直した(三行目と、JSON の `out_of_reach`)。§10.2 と §10.4 に段落を足した。`AGENTS.md` には、この節で正しくなくなった文が無かったので、触っていない。
**見つけて直していないもの**:
- **`Rat` の表示(`ritsu_units` の `Display`)は、−1 と 0 のあいだの値の符号を落とす。** −1/2 を `0.5` と書く。報告の中の答えと差は、符号を自分で付ける関数で書くので(`report::shown`、`report::exact_text`)、この節で足した表示には出ない。ほかの所(入力の例を書く `vectors::show` など)は `Display` を使うので、−1% と 0% のあいだの率の入力があれば、符号が落ちる。`ritsu_units` は rulec の外で、ほかの言語も使うので、ここでは直さなかった。2026-10-08 に直した。`Rat` の表示は符号を先に書き、絶対値をその後に書く(128 ビットに収まらない小数は、あふれさせずに分数で書く)。直す前は、`check` の E101 が、どの行にも当てはまらない −0.5% の入力を「change = 0.5%」と書き、足す行の形(JSON の `fix` も)を `| 0.5% | 10USD |` と、表がもう覆っている入力の行にしていた。`fixtures lint` も、−0.1% を返しうる出力の範囲を `0.1%..0.1%` と書いていた(`tests/negative_fraction.rs`、英語の規則が先で日本語の版が横)。この節の自前の表示(`report::shown`、`report::exact_text` と `decimals`)は `Rat` の表示に戻した。出力は一字も変わらない。差の三桁区切り(`report::group_rat`)だけは、`Rat` の表示の整数の部分に区切りを入れる形で残した。木のコーパスと材料には −1 と 0 のあいだの負の値が無く、木の出力は変わらなかった。
- **率の差は、テキストでも規則の刻みの数で書く。** 0.1% 刻みの出力で 0.3% の差は「+3」である。これまでと同じで、ここでは変えなかった。1% 刻みなら、差の数と百分率の数が一致する。
### 15.200 NumPy の計画にも、ほかの言語と同じ入口を持たせる(2026-10-08)
**きっかけ**:§15.198 の「見つけて直していないもの」の一つ目。NumPy の生成物(計画 `<別名>.json` と、どの規則にも共通の実行時 `rulec_np.py`)は、入口で `constraint` を確かめなかった。`express_delivery_quote.rule` の NumPy は、`declared` が 200,000 円で `cover` が 100,000 円の入力に 1,300 円と答え、ほかの十一の言語は同じ入力を入口で拒んだ。ベクタは制約を守るので、`rulec test` はこれを見ない。どの言語でも同じ入力に同じ答えを返し、規則が受け付けない入力は拒む、という生成コードの約束が、NumPy で破れていた。
ほかの言語の入口(`Gen::constraint_guards`、SQL の `_input_error`)と並べると、NumPy に無いものはほかに三つあった。koyomi の日(§15.174)を見ず、最初の支払日から最後の支払日までのどの日にも答えた。整数でない数を見ず、`astype(np.int64)` が 1000.5 を 1000 に切り捨て、`True` も文字列の `"1000"` も通した。W114 のガードが無く、二行が当たる要素では黙って前の行を選んだ。入口の文も、生成した言語の文ではなかった。実行時に日本語の決まった文で書いてあり、英語で生成しても「範囲の外です」と言った。
**決定**:
1. **入口を計画に載せる。** 制約は、二辺の名前と比べ方、二辺を共通の刻みにそろえる倍率(§15.139)、文。koyomi の日は、続く日の連なりと文。W114 の対は、表の中の二行の位置と文。実行時はそれを読み、ほかの言語と同じ順に確かめる。列ごとの型・列挙・範囲、制約、koyomi の日、表ごとの W114 である。日付は日の番号で比べる(§15.198)。
2. **止め方は範囲の外と同じ形にする。** 入力は `RuleInputError`(`ValueError`)で、文と値と要素の位置を持つ。W114 は `RuleContradictionError`(`AssertionError`)で、文と要素の位置を持つ。名前は Python の生成物の二つの例外と同じで、文もほかの言語と同じ文に、位置を後ろに足す(`the constraint does not hold: declared <= cover (row 1)`)。どの要素かは `row` で取れるので、呼び出し側は文を読み返さなくてよい。
3. **文は計画が運ぶ。** 実行時のファイルは全部の規則に共通なので、生成した言語の文を計画に書く。制約と日と W114 の文だけでなく、範囲・列挙・欠けた列・長さの揃わない列の文も、計画が運ぶ(ほかの言語にある文は同じ文にした)。日本語で生成すれば `重さ が範囲の外です: 50000 (行 0)` と言う。
4. **整数でない数。** Python の入口は小数を全部拒むが、NumPy の列は小数の型で来ることが多い。SQL の入口と同じく、値が整数かどうかを見る。1000.0 は 1000 として読み、1000.5、真偽、文字列は拒む。日付として読めない文字は「日付ではありません」で拒む(これまでは Python の `ValueError` がそのまま出ていた)。`optional` の列の不在の値は、これまでどおり型の確かめの外に置いた。
**捨てたもの**:
- **NumPy の入口を、行ごとに Python の生成物を呼ぶ形。** 列をまとめて決める、という NumPy の意味が無くなる。
- **入口の文を実行時に英語と日本語の二通り持つ形。** 生成した言語は計画を書くときに決まっているので、実行時が言語を選ぶ理由が無い。ほかの言語の生成物と同じく、生成するときに文を決める。
**確かめたこと**:
- `tests/door.rs` の六つ。英語の規則(コーパスの `express_delivery_quote.rule`、§15.198 の `hotel_stay.rule`)を先に、日本語の版(`速達の見積.rule`、`宿泊料金.rule`)を横に置いて、同じことを確かめる。
- 全部の言語(十二)の runner が、制約を破る入力(二つの金額と二つの日付)で止まり、同じ文を言う。英語で生成すれば英語の文、日本語で生成すれば日本語の文である。
- 同じ入力を `rulec test` の受け付けない入力のファイル(§15.56)に書くと、どの規則でも 24 通りの実行が拒み、ベクタの答えは一致したままである。十二の言語の runner のほかに、Python の MCP(stdio と HTTP)と Connect(ASGI と WSGI の、POST と GET)、TypeScript と JavaScript の MCP(stdio と HTTP)、Rust の WASI のモジュール、PostgreSQL の関数である(PostgreSQL の関数は、PostgreSQL を立てたワークスペースの全体で確かめた)。
- NumPy は、拒んだ要素の位置を言う(`the constraint does not hold: declared <= cover (row 1)`、`制約が成り立ちません: 申告額 <= 補償額 (行 0)`、`check_in is not a date: '2026-13-01' (row 0)`)。
- 整数でない数(1000.5、`True`、`"1000"`)を拒み、1000.0 は読む。
- koyomi の支払日でない日(2026-02-16)を、NumPy も全部の言語も拒む。
- W114 の二行が当たる要素で、NumPy は Python の生成物と同じ文の `RuleContradictionError` で止まる(計画と Python の両方の `doubled` を金額そのものに書き換え、5 円で二行が当たるようにした)。
- 替える前の NumPy では、六つとも落ちる。
- 替える前と後の突き合わせ。木の 335 の `.rule` で、`check`(英語、日本語、JSON の英語と日本語、`--show-shadow`)、証明書、`doc`(英語と日本語)、`coverage`(英語と日本語)、`vectors` は一字も変わらない。`gen` は英語と日本語で 670 回走らせ、生成できた 410 回の 23,302 のファイルが同じだった(残りの 260 回は検査が通らない 130 の規則で、どちらも同じ終了コードで止まる)。変わったのは NumPy の実行時(388 回、どれも同じ一つのファイル)と計画(388 回)だけで、計画から入口の鍵(`row`、`constraints`、`days`、入力ごとの `say`、表ごとの `guards`)を除くと、どれも前の計画と同じだった。制約の鍵を持つ計画は 36(18 の規則の英日)、W114 の対を持つ計画は 4(二つの規則の英日)。koyomi の日をとる三つの規則は、koyomi を結んだ `ritsu rulec gen` でも英日で比べた。変わったのは同じく NumPy の二つのファイルだけで、計画には日の連なり(どれも 12)が載った。sekisho の木の 149 の `.gate` も変わらない。ブラウザで試すページの `ritsu.wasm` を作り直し、ritsu の `playground` のテストが通る。
**見つけて直していないもの**:
- **`rulec test` が入口を確かめるのは、受け付けない入力のファイルにある入力だけである。** `gen` がそこに書くのは、参照評価器が矛盾で止まる入力(§15.56)だけである。だから NumPy の入口が制約を見なくても、`rulec test` は通っていた。制約を破る入力、koyomi の日でない日、範囲の外の値を一つずつ書けば、どの言語の入口も `rulec test` が確かめる。ただし、制約や範囲を持つ全部の規則の生成物と、`rulec test` の件数が変わる。§15.204 で直した。
- **`optional` の数と日付の入力で、検査が表の穴を見落とす。** `due : date? range >=2026-01-01 <=2026-12-31` に `<2026-07-01` の一行だけを書くと、`check` は ok と言う。生成した Python は 2026-08-01 で `unreachable: completeness was statically checked by rulec` の `AssertionError` になり、不在の値(`null`)では `TypeError` になり、範囲の外の 2025-01-01 には答える(入口が範囲を見ない)。`bonus : number?` に `<5` の一行だけでも同じく ok になる。逆に `none`・`<5`・`>=5` の三行を書くと、E105(`bonus = 4` で行 2 と行 3 が当たる)と E102(行 1)になる。`?` を外して `none` の行を消すと、どれも正しく E101(`due = 2026-07-01`、`bonus = 5`)か ok になる。替える前の rulec でも同じだった。リファレンスは、どの型にも `?` を付けられると言う。NumPy の実行時も、不在の値を数の列に入れられない(`astype(np.int64)` で落ちる)。検査の不具合で、生成器の外なので、ここでは直していない。§15.201 で直した。
- **真偽の入力に真偽でない値が来ると、言語ごとに答えが割れる。** NumPy は `astype(bool)` で何でも真偽にし、`express` が文字列の `"false"` の入力に 1,200 円(真として)と答える。同じ入力を `rulec test` の受け付けない入力のファイルに書くと、拒んだのは MCP の口(Python、TypeScript、JavaScript の stdio と HTTP)だけだった。真として 1,200 円と答えたのは Python(runner と Connect)、Ruby、PHP、NumPy、SQL、偽として 700 円と答えたのは TypeScript、JavaScript、Rust(runner と WASI)、Go、Swift、Java、Wasm である(PostgreSQL の関数は走らせていない)。NumPy と Python は、替える前の rulec でも同じ答えだった。値が型に合うかを runner と入口のどちらで確かめるかは、全部の言語にまたがる決めなので、ここでは直さず、NumPy は Python と同じ読み方のままにした。§15.203 で直した。
- **`rulec api` の NumPy の項目は、上げる例外を挙げない。** Python、TypeScript、JavaScript、Rust、Ruby、PHP、Swift、Java の項目は `errors` を持つ(Python なら `RuleInputError` と `RuleContradictionError`)が、NumPy の項目には無い。NumPy の実行時も同じ二つを上げるようになったので、`errors` を足せば、呼び出し側は生成物を読まずにそれを知れる。足すと全部の規則の `rulec api` の出力に鍵が一つ増える(互換のページの、1.x が足してよい形)ので、ここでは足していない。§15.202 で足した(NumPy と Go の `errors`、十の項目の `error_types`)。
### 15.201 optional の数・日付・真偽の入力を、値の無いことと値の範囲の両方で検査する(2026-10-08)
**きっかけ**:§15.200 の「見つけて直していないもの」の二つ目。`due : date? range >=2026-01-01 <=2026-12-31` に `<2026-07-01` の一行だけを書くと、`check` は ok と言い、生成した Python は 2026-08-01 で `unreachable` の `AssertionError` に落ちた。`bonus : number?` に `<5` の一行だけでも同じだった。逆に `none`・`<5`・`>=5` の三行は、重ならない行を重なり(E105、`bonus = 4`)と言い、届く行を届かない行(E102)と言った。どの入力にもちょうど一つの答えがある、という約束が、緑のまま破れていた。
**原因**:optional の列を「列挙に値が一つ多いもの」として読む作り(§15.87)が、列挙のところにしか無かった。
- 領域(`region.rs`)は、列挙の軸の頭にだけ `none` を置いていた。数と日付の軸には値の無いことの座標が無く、真偽の軸は `true` と `false` の二つだけだった。しかも列のセルを `T?` の型で読んだので、数も日付もリテラルとして読めず、比べ方はどの座標も残し、`none` はどの座標も取らなかった。だから一行の表が全体を覆って見え、三行の表は重なりと届かない行に見えた。
- 型の検査(`types.rs`)は、optional の入力の範囲と刻みも `T?` の型で読んだ。`number?` と `money[JPY]?` の範囲は E103 になり、`date?` の範囲は黙って落ちた。`rate[step 1%]?` の刻みも E103 になり、刻みの無い `rate?` は E103 にならずに通った。範囲が無いので、生成したコードの入口もその範囲を確かめなかった。
- 参照評価器(`eval.rs`)も数のリテラルを `T?` の型で読んだので、`<5` はどの数にも当たらなかった(日付のリテラルは型を見ずに読むので当たっていた)。
- 生成したコードは、比べ方を値の無いことにも当てていた。替える前の生成物で走らせると、Python は `TypeError` で落ち、Rust と Swift はコンパイルできず、JavaScript と PHP は値の無い日付と重さに答えを返した(`null < 20635` が真になる)。真偽の `false` の行は、Python、JavaScript、Ruby、PHP で値の無いことにも当たった。Ruby の runner は値のある日付を文字列のまま渡し、PHP と Python の Connect の runner は、日付が optional の入力にしか無い規則で日付を読む関数を書かなかった。Python の runner は、optional の重さしか持たない規則で `Gram` が無いと言った。NumPy は値の無いことを数の列に入れられなかった。
**決定**:
1. **値の無いことを、軸の座標にする。** 数と日付の軸の頭に、値の無いことの座標を置く。列挙の `none` と同じ位置である。真偽の optional は、`none`・`true`・`false` の三つの値の軸にする。完全性(E101)は値の無いことと、値があるときの宣言した範囲の全部の両方に行を求め、重なり(E105・W105・W114)と届かない行(E102)も同じ軸で読む。値の無いことは、例では `none` と書く(列挙の `none` と同じ)。
2. **セルは包む型で読む。** 比べ方、リテラル、値の集合は、値があるときだけに当たる。`-` と `not:` は値の無いことにも当たる。`not:` は宣言された全体から挙げた値を除いたもので、値の無いことはその全体に入るからである。列挙の optional では、検査、参照評価器、全部の生成先が、もともとこう読んでいた。§3 の 7 は「`none` には `-` と `none` だけが一致する」と書いていたので、この読み方に書き直した。参照評価器と網羅の監査も包む型でリテラルを読み、網羅は optional の数と日付の境目の対も数える。
3. **範囲と刻みは包む型で読む。** optional の入力の範囲、刻み、率の既定の範囲(0% から 100%)、刻みの無い率の E103 は、包む型と同じにした。ritsu の口が値や日を与えた optional の入力は、値の無いことをとらない(値を与えた列挙の入力が `none` をとらないのと同じ)。`range from koyomi` は、これまでどおり optional でない日付だけに書ける(E065)。
4. **入口は、値があるときに包む型と同じだけ確かめる。** 全部の生成先で、optional の入力に値があれば、型、列挙、整数、範囲を、包む型の入力と同じ文で確かめる。値が無ければ確かめない。2025-01-01 は、2026 年を宣言した日付の入力では拒まれる。列挙の optional も、値があるときの列挙の確かめが Python、TypeScript、JavaScript、Ruby で増える(型を持つ言語では、もともと型が確かめていた)。
5. **生成したコードのセルは、値の無いことを避けて比べる。** 比べ方は値があることを先に確かめ(Python は `is not None and`、TypeScript と JavaScript と PHP は `!== null &&`、Ruby は `!x.nil? &&`、Go は nil の確かめ、Rust は `matches!(x, Some(__v) if …)`、Swift は `x.map { … } ?? false`、Java は `!= null &&`)、真偽は `is True`・`=== true`・`== true`・`Boolean.TRUE.equals(…)` のように値と比べる。Java は値の集合も `null` を先に確かめる(`List.of(…).contains(null)` は例外を投げる)。SQL の `not:` は `IS NULL OR … NOT IN (…)` にする(`NULL NOT IN (…)` は NULL で、どの `WHEN` もとらない)。NumPy は、値の無い要素を数の列の外に持ち、`none` と `not:` だけがそこを取る。枠の型(`JPY`、`Gram`)は、optional の入力にしか無くても宣言する。Ruby の runner は値のある optional の日付を日の番号にし、PHP と Python の Connect の runner は、optional の日付があれば日付を読む関数を書く。
6. **証明書は値の無いことを書き、二つの再検査がそれを確かめる。** 値の無いことの座標は、`coords` の頭の `none` で、`bounds` は null である。`tools/recheck.py` と `proofs/` の Lean は、optional の入力の軸がその座標で始まることを確かめ、残りの座標が宣言した範囲を敷き詰めることを確かめる。比べ方と数の集合はその座標を取らず、`not:` は取る。Lean には、セルが値の無いことを取るかどうかの意味(`CellTest.takesAbsent`)と、軸の頭の座標が区画に入ることとそれが同値だという定理(`mem_boxOf_absent_iff`)を足した。宣言した範囲の両端が点の座標で終わることも、二つが確かめるようにした。区間の座標は端を含まないので、最後の点を落とした証明書が、手前の区間で範囲の端に届いて見え、通っていた。
7. **値の無いことの書き方をそろえる。** JSON Schema(と MCP の `inputSchema`)は、optional の数と日付を、包む型と同じ形(範囲、単位の説明、日付の形)に null を並べて書く(これまでは数も日付も `integer` だった)。ページは optional の範囲を包む型で書き(日付を日の番号でなく日付で)、案内のページは「…、または無し」と書く。
**捨てたもの**:
- **optional の数と日付を、E110(まだ検査できない型)で止めること。** 参照評価器と生成コードは optional の数と日付を扱い、リファレンスはどの型にも `?` を付けられると言う。止めれば、すでに書かれた規則を通さなくなるだけで、何も確かめない。
- **値があるかどうかを、別の真偽の軸にすること。** 値が無いときに数の軸の全部の座標を掛け合わせることになり、同じ穴を何度も数え、証明書の区画も読みにくくなる。列挙の `none` と同じく、一つの座標にした。
- **`not:` が値の無いことを取らない読み方。** §3 の 7 の元の文はそう読めるが、列挙の optional では、検査、参照評価器、全部の生成先が、もう取る読み方をしていた。数と日付だけを逆にすれば、同じ `not:` の意味が型で変わる。
**確かめたこと**:
- `tests/optional.rs` の八つ。材料は `tests/optional` で、英語の規則を先に、日本語の版を横に置いた。
- 変異の八つ(英日で十六)が、それぞれ一つの診断を言う。値の無い行が無い日付の一行の表(E101、`due = none`)、同じく数(E101、`bonus = none`)、値の無い行のほかが足りない日付と数(E101、`due = 2026-07-01` と `bonus = 5`)、`none` の行と `-` の行(E105、`due = none`)、`none` の行と `not: 5` の行(E105、`bonus = none`)、`policy first` で `-` や `not: 2026-07-01` のあとに置いた `none` の行(E102、行 2)。英日の出力は golden(`tests/golden/optional`)と同じで、日本語の版は同じ行に同じ診断を言う。替える前の rulec では、一行の表の二つは ok だった。
- 値の無い行と範囲の全部を書いた規則(`parcel_cover.rule` と `小包の補償.rule`。金額、数、日付、真偽の optional を一つずつ持ち、値の無い行を表の頭、真ん中、最後に置く)は診断を一つも出さない。替える前は E105 と E102 になった `none`・`<5`・`>=5` の形も、日付と率で同じ形も通る。
- 参照評価器で、値の無いこと、個数の全部、年のすべての日、金額の境目とその両隣が、どの表でもちょうど一つの行に当たる。
- 全部の言語(十二)の runner と、Python の MCP と Connect、TypeScript と JavaScript の MCP、Rust の WASI のモジュール、PostgreSQL の関数(PostgreSQL を立てたワークスペースの全体で)が、ベクタに参照評価器と同じ答えを返し、値があって範囲の外にある六つの入力(金額、個数、日付のそれぞれ両側)を拒む。runner は範囲の外の金額で、英語なら `declared is out of range`、日本語なら `申告額 が範囲の外です` と同じ文で止まり、全部の入力の値が無いときは答える。optional の重さと率しか持たない規則(枠の型の宣言と率の刻み)と、並びの要素の optional の数を読む規則でも、全部の言語が答えをそろえて範囲の外を拒む。
- 網羅の監査が、optional の数と日付の境目の対を、包む型のときと同じ数(6)だけ求める。
- 証明書の optional の軸は `none` で始まり、二つの再検査が通す。値の無いことの座標を落とした証明書、範囲の最後の点を落とした証明書、比べ方の行の区画に値の無いことを足した証明書は、二つとも落とす。
- 替える前と後の突き合わせ。木の 335 の `.rule` で、`check`(英語、日本語、JSON の英語と日本語、`--show-shadow`)、証明書、`doc`(英語と日本語)、`coverage`(英語と日本語)、`vectors`、`api`、`schema` は一字も変わらない。検査が新しく落とす規則も、E105・E102 が消える規則も無かった(木の optional の入力は列挙だけである)。`gen` は英語と日本語で 670 回走らせ、生成できた 410 回で 23,550 のファイルが同じだった。変わったのは、NumPy の実行時(388 回、どれも同じ一つのファイル)と、optional の列挙の入力を持つ八つの規則の英日(140 のファイル)だけである。後者は、値があるときの列挙の確かめが Python、TypeScript、JavaScript(と、それを埋め込んだページ)、Ruby、Go に増えたことと、Java の値の集合の行が `null` を先に確かめるようになったことだけだった。sekisho の木の 149 の `.gate` も変わらない。証明書を書ける 205 の規則の証明書は、どれも新しい二つの再検査を通る。ブラウザで試すページの `ritsu.wasm` を作り直し、ritsu の `playground` のテストが通る。
**見つけて直していないもの**:
- **optional の出力。** `money[JPY]?` のような出力の型は、この節では見ていない。§15.249 で直した。
- **`range from koyomi` の optional の日付。** E065 のままで、値があるときに koyomi の日をとる形は書けない。
- **規則の名前と同じ名前の列挙。** 規則 `match_kind` に `enum match_kind` を書くと、`check` は通るが、Go と Java の生成物が同じ名前の型を二つ宣言してコンパイルできない。並びの要素の optional を確かめる途中で見つけた。
### 15.202 `rulec api` の言語の項目に、上げる例外の種類と、それが持つ値を挙げる(2026-10-08)
**きっかけ**:§15.200 の「見つけて直していないもの」の四つ目。NumPy の項目は、上げる例外を挙げていなかった。NumPy の実行時は、Python の生成物と同じ二つ(`RuleInputError` と `RuleContradictionError`)を上げ、拒んだ要素の位置を持つ(§15.200)。それでも呼び出し側は、生成物を読まないとそれを知れなかった。ほかの言語の項目を調べると、`errors` を持つのは Python・TypeScript・JavaScript・Rust・Ruby・PHP・Swift・Java の八つで、どれも名前の並びだけだった。Go の生成物も型のある二つ(`*RuleInputError` と `*RuleContradictionError`)を返すのに、Go の項目には `errors` が無かった。`docs/generated-code.md` も「どちらも `error` で届き、文がどちらかを言う」と書いていた。どの項目も、どちらが呼び出し側の誤りか、例外から何を読めるか(文と、拒んだ値と、NumPy では要素の位置)を言わなかった。
**決定**:
1. **NumPy と Go の項目にも `errors` を足す。** 形はほかの八つと同じで、名前の並び(入力の誤りが先、矛盾が後)である。NumPy は `RuleInputError` と `RuleContradictionError`、Go は `*RuleInputError` と `*RuleContradictionError` と書く。Go の書き方は、`errors.As` に渡す形である。
2. ★ **十の項目の全部に `error_types` を足す。** 例外ごとに、`kind`(`input` は入口が拒んだ入力、`contradiction` は W114 のガード)、`name`(`errors` と同じ綴り)、`fields`(呼び出し側が読める値の名前)を持つ。NumPy の `RuleInputError` は `what`・`value`・`row`・`word`、`RuleContradictionError` は `what`・`row`・`word` である。Go は `What`・`Value`・`HasValue` と `What`、ほかの八つは `what`・`value` と `what` である。いつ上げるかは `kind` が言い、文ではなく決まった語にした。`api` の出力は、`--lang` で変わらない名前と数でできているからである。
3. ★ **`errors` の形は変えない。** 名前の並びを、種類と名前と値の名前の組の並びに替えれば、鍵は一つで済む。ただ、それはすでにある鍵の形を変えることになる。互換のページは、1.x が鍵を足すことは許し、ある鍵の形を変えることは許さない。そこで名前は `errors` に残し、組は新しい鍵に置いた。
4. **Connect・SQL・Wasm の項目には足さない。** この三つの生成物は、自分の例外の型を持たないからである。Connect は `invalid_argument` と `internal` で失敗する。SQL の問い合わせは `guard` の列で答え、関数は `raises` の SQLSTATE を上げる。Wasm のモジュールは `{"error":…}` を返す。どれも、それぞれの項目か文書がすでに言っている(足りない所は、下の「見つけて直していないもの」)。
**捨てたもの**:
- **`errors` を組の並びに替える。** 3 のとおり、ある鍵の形を変えることになる。
- **いつ上げるかを文で書く。** `--lang` で変わる文は `api` に置かない(NumPy の項目の `wire` は、前からある例外である)。
- **NumPy の項目にだけ組を足す。** ほかの言語の項目と形がそろわない。呼び出し側は、言語ごとに読み方を変えることになる。
**確かめたこと**:
- `tests/api_errors.rs` の三つ。英語の規則(コーパスの `express_delivery_quote.rule`)を先に、日本語の版(`速達の見積.rule`)を横に置く。
- 十の項目が `errors` と `error_types` を持ち、種類は入力、矛盾の順で、名前は二つの鍵で同じである。どの名前も、どの値の名前も、その言語の生成物に宣言がある(Python と NumPy は `self.<名前> =`、TypeScript は `readonly`、JavaScript は `this.<名前> =`、Ruby は `attr_reader`、PHP は `$<名前>`、Java は `public final`、Go は構造体のフィールド、Rust は構造体のヴァリアントのフィールド、Swift は関連値のラベル)。Connect・SQL・Wasm の項目には無い。
- Python と NumPy は、二つの例外を生成物のとおりに作り、挙げた値を全部読める。NumPy の実行時は、範囲の外の要素を持つ列を `RuleInputError` で拒み、`row` は 1、`what` はその列の名前で始まり、`word` は英語なら `row`、日本語なら `行` である。
- 目録だけから組んだ Go の呼び出しが、`errors.As` で二つを見分け、挙げたフィールドを全部読んで、`go vet` を通る。フィールドの名前を一つ違えると通らないことも確かめた。
- `tests/api.rs` の `生成物の文書が実物の名前を使っている` に、`error_types` を足した。
- 替える前と後の突き合わせ。木の 335 の `.rule` を英語と日本語で、`api` と `gen`(全部の生成先)の 670 回ずつ走らせた。`gen` は 670 回とも同じで、生成できた 410 回の 24,078 のファイルが一字も変わらない。`api` は、答えた 410 回の全部で、足した鍵(十の項目の `error_types`、NumPy と Go の `errors`)を除くと一字も変わらない。検査が通らない規則の 260 回は、前と同じ終了コードと同じ文で止まる。生成できる 205 の規則で、それぞれの rulec が生成したものに `rulec test` をかけた結果も、前と後で同じだった(PostgreSQL を立てて、関数の側も走らせた)。
- 文書:`docs/formats.md`(とスキルのコピー)の `api` の例を、実物で取り直した(Python と Go の `error_types`、NumPy の項目)。`errors` と `error_types` の段落を足した。`docs/generated-code.md`(とスキルのコピー)では、Go の節(二つの型と `errors.As`)、入口のガードの節(`rulec api` が二つの例外を挙げること)、NumPy の段落(`row`)を直した。サイトの生成のページ(英日)にも書いた。互換のページは変えていない。足したのは鍵だけで、1.x が足してよい範囲である。
**見つけて直していないもの**:
- **SQL の項目は、矛盾の側を言わない。** 規則が W114 の対を持つとき、問い合わせは `_contradiction` の列を持ち、関数は SQLSTATE `P0001` を上げる。それでも `sql` の項目は、`guard`(`_input_error`)と `raises`(`22023`)しか挙げない。
- **Wasm の項目は、拒んだときの答えの形(`{"error":…}`)を言わない。** 生成した Wasm は、入力の誤りも矛盾も、同じ形で返す。§15.204 で、答えは runner と同じ拒む行(`{"refused":…,"error":…}`)になり、`refused` が入力の誤りと矛盾を分ける。`api` の項目は、まだその形を言わない。§15.206 で直した。
- **Connect の項目は、失敗の符号(`invalid_argument` と `internal`)を挙げない。**
- この三つは、例外の型ではない形で拒む。挙げるなら、鍵の形をそれぞれに決める要がある。
### 15.203 真偽の入力は、どの入口でも JSON の true と false だけを受け付ける(2026-10-08)
**きっかけ**:§15.200 の「見つけて直していないもの」の三つ目。真偽の入力に JSON の真偽でない値が来ると、言語ごとに答えが割れた。文字列の `"false"` は、Python・Ruby・PHP・NumPy・SQL が真と読み、TypeScript・JavaScript・Rust・Go・Swift・Java・Wasm が偽と読み、拒んだのは MCP のサーバだけだった。数の `1` は、Python・NumPy・Ruby・PHP・Swift・SQL が真、TypeScript・JavaScript・Rust・Go・Java が偽と読み、Wasm だけが拒んだ。`null` は、SQL(「がありません」)と Wasm のほかは偽として答えた。どれも、規則が書いていない値に答えを返している。
**決定**:
1. **真偽は JSON の `true` と `false` だけである。** 文字列の `"true"`・`"false"`、数の `0`・`1`、ほかの型の値は、整数でない数と同じ形で拒む。モジュールの `RuleInputError` で、文は「{入力} が真偽ではありません」(英語なら `{input} is not a boolean`)。文は生成した言語で一つに決まり、十二の言語と MCP、Connect、Wasm、SQL、NumPy のあいだで一字も違わない。`optional` の真偽は、`null` を値の無いことと読み(§15.201)、ほかの値は同じに確かめる。文字列の `"null"` は値の無いことではない。
2. **確かめるのは、値が入ってくる所の全部である。** 呼び出し側が何でも渡せる言語のモジュールの入口(Python、TypeScript、JavaScript、Ruby。並びの要素のフィールドも)と、JSON を読む口の全部(十二の言語の runner、MCP のサーバ、Python の Connect の runner、Wasm の入口、SQL の二つの runner、NumPy の計画)である。Python・Ruby・TypeScript・JavaScript の runner は値を受け取ったまま渡し、モジュールの入口が拒む(§15.204 で、この四つの runner も読むところで拒むようにした)。型を持つ言語(Rust、Go、Swift、Java)と PHP の runner、Wasm の入口、SQL の runner、NumPy の計画は、読むところで拒む。
3. **Rust・Java・Wasm の読み手は、文字列に印を残す。** この三つは JSON を自前で読み、文字列の値を引用符を外して持っていたので、文字列の `"true"` と真偽の `true` を区別できなかった。文字列の値は開きの引用符を一つ残して持ち、文字列として読む所でそれを外す。`optional` の `null` も印の無い四文字で見るので、文字列の `"null"` は値の無いことにならない。Swift は、Foundation が数と真偽を同じ `NSNumber` で返すので、型の符号(真偽は `c`)で見分ける。
4. **射影の関数は、呼び出し側のオブジェクトの値を受け取ったまま入口へ渡す**(§15.125)。Python の `bool(…)`、TypeScript と JavaScript の `Boolean(…)`、Ruby の `? true : false` は、文字列の `"false"` を真にしていた。PHP は、射影の中で同じ文で拒む(PHP の `(bool)` も真にしていた)。
5. **NumPy の計画は、列をいつも一次元に持つ。** 要素に配列(`[]`)のある列を `np.asarray` が二次元にし、拒むべき要素が見えなくなって、`optional` の真偽の列で落ちていた。
**捨てたもの**:
- **runner で文字列の `"true"`・`"false"` を真偽に読み替える形。** 型の違う値を言語ごとの読み替えで救えば、救い方がまた割れる。JSON の真偽でない値は、どの言語でも同じに拒む。
- **PHP のモジュールの引数を `mixed` にして、入口で確かめる形。** PHP のモジュールは引数に型を宣言していて、`declare(strict_types=1)` のある呼び出し側(生成した runner と射影の関数はそう)からは、真偽でない値が型の誤り(`TypeError`)になる。宣言の無い呼び出し側では PHP が値を読み替えるが、それは PHP の呼び出しの規則で、整数の引数でも同じである。型を外せば、モジュールを読む人の手がかりが減る。
**確かめたこと**:
- `tests/boolean.rs` の四つ。英語の規則(コーパスの `express_delivery_quote.rule`、§15.201 の `parcel_cover.rule`)を先に、日本語の版(`速達の見積.rule`、`小包の補償.rule`)を横に置いた。
- `"false"`、`"true"`、`1`、`0`、`[]`、`null` の六つを受け付けない入力のファイルに書くと、`rulec test` の全部の実行(十二の言語の runner、Rust の WASI、Python の MCP と Connect の四つ、TypeScript と JavaScript の MCP)が六つとも拒み、ベクタの答えは変わらない。PostgreSQL の関数は、PostgreSQL を立てたワークスペースの全体で確かめた。
- `optional` の真偽(`signature`、`署名`)は、`null` を値の無いことと読み(ベクタが通る)、`"true"`、`0`、`"null"` を拒む。
- 全部の言語の runner が、`"false"`、`"true"`、`1`、`0`、`[]` に同じ文で止まる。英語で生成すれば `express is not a boolean`、日本語で生成すれば `速達 が真偽ではありません` で、`false` には答える。
- 射影の関数(Python、TypeScript、JavaScript、Ruby、PHP)が、`.proto` の契約の `express` に文字列の `"false"` と数の `1` を入れたオブジェクトを同じ文で拒み、`false` には答える。
- 替える前と後の突き合わせ。木の 353 の `.rule` で、`gen` を英語と日本語で 706 回走らせ、生成できた 414 回のうち、17,626 のファイルが同じで、6,688 のファイルが変わった。変わったのは、真偽の読み方と入口だけである。どの規則でも変わったのは、真偽を読む部分を持つ Rust の runner と Wasm の入口(文字列の印、各 414)、Java の runner(414)、Swift の runner(414)、SQL の二つの runner(各 396)、Python・TypeScript・JavaScript の MCP(文、各 414)、NumPy の実行時(392、どれも同じ一つのファイル)である。真偽の入力を持つ 87 の規則の英日(174 回)では、ほかに Python・TypeScript・JavaScript・Ruby のモジュールの入口と runner、ページ、Go・PHP の runner、Python の Connect の runner、NumPy の計画(168)、射影を持つ三つの規則の PHP のモジュール(6)が変わった。`rulec api` と `rulec schema` は、英日の 1,412 回のどれも変わらない。`rulec test` は、生成できた 207 の規則で替える前と後の結果が同じだった(23 の通り道で 7,205 の実行、通らない 3 は替える前から Rust がコンパイルできない変異 `m_w121.rule` の三つ。PostgreSQL の関数は立てずに走らせた)。
### 15.204 受け付けない入力のファイルに入口が拒むものを全部書き、`rulec test` が拒む理由まで確かめる(2026-10-08)
**きっかけ**:§15.200 の「見つけて直していないもの」の一つ目。`gen` が受け付けない入力のファイル(§15.56)に書いたのは、参照評価器が矛盾で答えを持たない並びだけで、`rulec test` はほかの入力を拒むことを一つも確かめなかった。NumPy の入口が制約を見なかったこと(§15.200)、`optional` の範囲(§15.201)、真偽の値(§15.203)は、どれも `rulec test` が緑のまま残っていた。確かめていたのも、止まったこと(終了コードが 0 でないこと)だけで、止まった理由は見ていなかった。
入口が受け付けないものを一つずつ全部の runner に渡すと、ほかにも割れていた。数を一つ持つ規則で、その入力を渡さないと、Rust・Ruby・PHP・Go・Swift・Java の runner は 0 として答えた。数に文字列の `"1"` を渡すと、Python・TypeScript・JavaScript・Rust・Ruby・PHP・Java・Wasm が 1 として、Go と Swift が 0 として答えた。`1.5` は、Python・Ruby・PHP・Go・Swift が 1、Rust と Java が 0 として答えた。列挙に無い `"z"` は、Go が列挙の最初の値として答え、ほかは Python の `ValueError`、Rust の panic、Swift の強制アンラップのように、言語ごとの例外で止まった。暦に無い `2026-13-01` は、TypeScript と JavaScript が次の年の日にずらして「範囲の外」と言った。拒む文も、列挙は Python・TypeScript・JavaScript・Ruby が型の名前(`Kind`)、Go が「the enum」と言い、MCP のサーバ、Connect のサービス、Wasm の入口は、それぞれ別の文で言っていた。
**決定**:
1. **受け付けない入力のファイルに、入口が拒む理由ごとに入力を書く。** どれもベクタの一件を一か所だけ変えたもので、入口が言うことは一つになる。入力ごとに、渡さない入力と型の違う値を一つずつ。数は、整数でない数(率なら刻みに載らない値)と、範囲の両端の一つ外(`optional` の入力も、値があるときの範囲の外)。日付は、暦に無い日(2026-02-30)、範囲の初日の前の日と終わりの次の日、koyomi の日付がとらない日。列挙は、列挙に無い語。真偽は、文字列の `"false"` と数の `1`。`constraint` ごとに、両辺が範囲の中で制約を破る組み合わせ。並びをたどる規則は、並びそのものを渡さないこと、配列でない値、オブジェクトでない要素と、一つの要素のフィールドごとに同じもの、`count` の上限より一つ多い要素、要素のフィールドの `sum` が範囲の上端を超える要素の並び。参照評価器が受け付ける入力(型が端を閉じている範囲など)は書かない。
2. **書く数は、入力ごとに決める。** 入口の確かめは言語ごとに入力ごとの行で書かれ、§15.201 の穴も一つの入力の範囲だった。規則ごとに理由を一つずつにすると、確かめない入力が残る。両端も、片方の比べ方だけを誤る入口を見つけるために両方書く。上限を超える並びは、要素が 50 を超えるなら書かない(ファイルが一行で数千字になる)。木の規則で書けるもの(英語で生成できる 207 の規則)では、一つの規則に 3 から 42 行、真ん中で 11 行、合わせて 2,470 行と 410 KB になる。理由ごとには、範囲の外が 638、渡さない入力が 603、整数でない数(刻みに載らない値と型の違う値を含む)が 578、列挙に無い値が 310、真偽でない値が 232、日付でない値が 64、制約が 20、並びが 9、オブジェクトでない要素が 9、並びの矛盾が 3、文字列でない値が 2、数え上げの上限が 2 行だった(koyomi の日は、koyomi を結んだ `ritsu rulec gen` でだけ書ける)。
3. **参照評価器が理由を言う。** `src/door.rs` に、入口が言う文を一つの関数ずつ置き、生成器はそれを全部の言語に書き、参照評価器は受け付けない入力にそれを言う。参照評価器の入口は、入力を宣言の順に、あること、型、範囲の順に読み、並びと要素、`constraint`、koyomi の日、並びの矛盾の順に見る。ファイルの一行は、`in`、拒む理由の種類(`refused`:呼び出し側の契約違反の `input` か、規則の矛盾の `contradiction`)、参照評価器の文(`error`)、なぜファイルにあるか(`why`、文面)を持つ。
4. **runner は拒んだ入力に、記録の代わりに拒む行を書いて、次の行へ進む。** `{"refused":"input","error":"weight is out of range"}`。`rulec test` は受け付けない入力を一度の実行で全部渡し、行ごとに種類と文が参照評価器と同じことを確かめる(言語が文のあとに `: ` で足す値は比べない)。MCP のサーバは `isError` の本文が `RuleInputError: <文>…` であること、Connect は `invalid_argument` と `internal` の符号と本文、Wasm の入口は自分で拒む行を返し、SQL の runner は `_input_error` と `_contradiction` の列、PostgreSQL の関数は SQLSTATE 22023 と P0001 を一時の関数で受けて、それぞれ同じ行にする。NumPy の runner は、まとめて決めて拒まれたら、一行ずつ決め直す。
5. **全部の口が、線の上の値を同じに読む。** 渡されていない入力(キーが無い、`optional` でない入力の `null`)は「{入力} がありません」。型の違う値はその型の文で、「{入力} が整数ではありません」「{入力} が日付ではありません」「{入力} が列挙 {列挙} の値ではありません」(列挙は規則が宣言した名前で)「{入力} が真偽ではありません」「{入力} が文字列ではありません」、並びは「{並び} が並びではありません」と「{並び} の要素がオブジェクトではありません」。文字列の数は数ではなく、1000.5 は整数ではなく、暦に無い日は日付ではない。Go の runner は数を書かれた桁のまま読み(`UseNumber`)、Rust・Java・Wasm の読み手は印の付いた文字列と桁だけの整数を見分け、Swift は `NSNumber` の型の符号を見る。Python・Ruby・TypeScript・JavaScript の runner も、モジュールの入口に渡す前に同じ文で読む(§15.203 では受け取ったまま渡していた)。渡さない入力を言語のキーの誤り(Python の `KeyError` など)で落とさず、並びと要素も入口と同じ文で拒むためである。
6. **MCP のサーバと Connect のサービスも同じ文で言う。** MCP は値を文のあとに足す(`RuleInputError: weight is out of range: 0`)。足りない引数は一つ目を言う。Connect のサービスは、省いたフィールドを「{入力} がありません」と言い(要素なら位置を括弧で後ろに足す)、列挙と日付も同じ文にした。
7. **Ruby のモジュールの並びの要素の入口も、列挙のフィールドを確かめる。** Python・TypeScript・JavaScript・Go の要素の入口は確かめていて、Ruby だけが見ていなかった(ライブラリとして呼ぶ側が列挙に無い値を渡すと、どの行にも当たらずに「unreachable」で止まった)。
8. **コーパスの全部の規則で、全部の言語の runner に受け付けない入力を通す。** `tests/threeway.rs` の一致の試験に足した。
**捨てたもの**:
- **受け付けない入力を一件ずつ別の実行で確かめる形(これまでの形)。** 一つの規則で二十前後の入力を、二十三の通り道の全部で一件ずつ走らせると、`rulec test` が一つの規則で数十秒延びる。runner が拒んだあとも読み進めれば、一度の実行で済み、拒んだ理由まで比べられる。
- **止まったこと(終了コード)だけを見る形。** 別の理由で落ちた runner(キーが無くて `KeyError`)も、通ってしまう。
- **言語が文のあとに書く値まで比べる形。** 値の書き方は言語ごとに違う(`'false'`、`"false"`、`false`)。文は一字も違わず、値は呼び出し側が属性で読む(§8.5)。
- **`coverage` の `refused` に入口の入力を数える形。** 網羅の義務を果たすのはベクタと並びの矛盾で、入口の入力はどの義務も果たさない。数えれば、`coverage` の出力と golden が全部の規則で変わり、意味も混ざる。
**確かめたこと**:
- `tests/refused.rs` の四つ。英語の規則を先に、日本語の版を横に置いた七つずつで、二つの金額のあいだの制約と真偽、二つの日付のあいだの制約、要素に列挙のある並びと矛盾、数え上げ、合計、文字列、どの型も持つ `optional` の入力で、理由を全部そろえた。
- `gen` が、規則ごとに、その入口が持つ理由の文を全部書く(英語なら `weight is missing`、`express is not a boolean`、`the constraint does not hold: declared <= cover`、`candidates has too many elements (at most 50)`、`total is out of range`、`sku is not a string`、`signature is not a boolean`。日本語なら `重さ がありません`、`制約が成り立ちません: 申告額 <= 補償額`、`候補 の要素が多すぎます(上限 50)` ほか)。並びの矛盾はファイルの頭に残る。
- 英語の七つと日本語の七つを一つずつ生成した木で、`rulec test` の全部の実行(十二の言語の runner、Rust の WASI、Python の MCP と Connect の四つ、TypeScript と JavaScript の MCP)が、ファイルの全部の入力を、ファイルの種類と文で拒む。
- koyomi を結んで生成した `settlement.rule` のファイルに、koyomi の日付がとらない日(`pay_day is not a day payment of payment_terms.cal comes to`)があり、全部の実行が拒む。
- 生成した Python のモジュールから `weight` の範囲の確かめを抜くと、`rulec test` は Python の runner が「参照評価器が受け付けない入力に答えを返した」と言って落ち、Ruby のモジュールの範囲の文を変えると、Ruby の runner の拒む行と期待した行を並べて落ちる。ほかの言語は通ったまま。
- `tests/threeway.rs` の一致の試験に、受け付けない入力を足した。コーパスの 87 の規則の 1,276 件を、十二の言語の runner が、どれもファイルの種類と文で拒む(301 秒。替える前のこの試験は 251 秒)。
- これまでのテストのうち、runner が止まることを確かめていたもの(`tests/door.rs`、`tests/optional.rs`、`tests/boolean.rs`、`tests/api.rs`)は拒む行を、受け付けない入力が一件だと数えていた `tests/fold.rs` は頭の矛盾と全部の件数を、Wasm の入口の答えを読む `tests/wasm_target.rs` は拒む行と文を、Connect のサービスの文を読む `tests/connect.rs` は新しい文を確かめるようにした。
- PostgreSQL の関数も、一時の関数で受けた拒む行を、ほかの言語と同じ種類と文で返す(探りの規則の 26 件と `settlement.rule` の 6 件で走らせ、ワークスペースの全体の `rulec test` でも走った)。
- 替える前と後の突き合わせ。木の 353 の `.rule` で、`gen` を英語と日本語で 706 回走らせ、生成できた 414 回のうち 14,076 のファイルが同じで、10,238 が変わった。受け付けない入力のファイルは、204 の規則の英日で新しく書かれ、並びの矛盾を持つ 3 の規則の英日で行が増えた。どの規則でも、runner、MCP のサーバ、Connect のサービスと runner、Wasm の入口と `.wit`、SQL の二つの runner、NumPy の runner と実行時が変わった。モジュールで変わったのは、列挙の入力を持つ規則の入口の列挙の文(Python・TypeScript・JavaScript・Ruby・Go と、それを埋め込んだページ)と、Ruby の要素の入口の列挙の確かめだけだった。`api` と `schema` は、英日の 1,412 回のどれも変わらない。`rulec test` は、生成できた 207 の規則の 23 の通り道(7,205 の実行)で、どの実行の結果も、受け付けない入力の件数のほかは替える前と同じだった(通る 7,202、通らない 3 は替える前から Rust がコンパイルできない変異 `m_w121.rule`)。受け付けない入力は全部で 56,540 件を拒み、全部の規則の `rulec test` は 6 分 26 秒から 8 分 13 秒になった(六つずつ並べて走らせ、一つの規則の真ん中は 9.9 秒から 13.2 秒)。koyomi の日をとる三つの規則は、koyomi を結んだ `ritsu rulec gen` で比べ、同じ種類のファイルと、6 行の受け付けない入力のファイル(koyomi の日を含む)が増え、`rulec test` の 35 の実行がどれも 6 件を拒んだ。PostgreSQL の関数は、この突き合わせでは立てずに走らせた。
**見つけて直していないもの**:
- **`rulec api` の `wasi` の `wire` は、拒む行を言わない**(「one fixtures record per line on stdout」)。`api` を書く所はこの直しの外で、言い直すと全部の規則の `api` の出力が変わる。§15.206 で直した。
- **Connect のサービスに JSON で直に送られた型の違う値**(真偽に文字列、数に配列など)は、サービスの入口より前に protobuf の JSON の読み手が、その読み手の文で拒む(`invalid_argument`)。生成したコードの外の文なので、そろえていない。Connect の runner は、送る前に同じ文で拒む。
- **射影の関数(§15.125)は、数と日付を契約の形どおりに読み替える。** protojson は int64 を文字列で書くので、文字列の数も数として読む。型の違う値は契約の検査(JSON Schema、Protovalidate)が拒むことを前提にしていて、受け付けない入力のファイルでは確かめていない(真偽だけは §15.203 で入口に渡すようにした)。
- **何でも渡せる言語のモジュールの入口は、文字列の入力(`string`)の型を見ない。** runner と MCP は見るので `rulec test` は通るが、ライブラリとして呼ぶ側が文字列でない値を渡すと、セルの比べ方がそのまま走る。
- **`1000.0`(小数点を書いた整数)は、NumPy と SQL が 1000 と読み(§15.200)、ほかの口は整数でないと拒む。** 参照評価器も拒む。受け付けない入力のファイルには書いていない。§15.205 で直した。
- **合計(`sum`)が表の出力の値を足すときは、上限を超える並びを書かない。** 参照評価器の入口が、要素ごとの表を引かずに足せる、要素のフィールドの合計だけを見るため。
### 15.205 値が整数の数は、どの入口でも整数として受け付ける(2026-10-08)
**きっかけ**:§15.204 の「見つけて直していないもの」の五つ目。JSON は整数と小数を分けず、Python の `float` を `json.dumps` に渡せば `1000.0` と書く。値が整数の数を「整数ではありません」で拒む理由は無いのに、入口ごとに扱いが割れていた。替える前の生成物で、送料の規則のベクタの整数を全部 `1000.0`・`1e3`・`-0.0` の形に書き直して `rulec test` に通すと、TypeScript と JavaScript の runner(`JSON.parse` が `1000.0` と `1000` を同じ数にする)だけが同じ答えを返した。NumPy の runner は計算には使ったが、記録の `in` に `1.0` と書いて、ほかの言語の記録と一字違った。ほかの十四の通り道(Python の runner と Connect の四つ、Rust と WASI、Ruby、PHP、Go、Swift、Java、SQL の runner、Wasm)は拒んだ。参照評価器も、`fixtures` の入口(§15.197)も拒んだ。SQL の問い合わせは、直に呼ばれれば `CAST(… AS BIGINT)` で 1000 と読む。
**決定**:
1. **整数で渡す入力(数、金額、量、率の刻みの数)に値が整数の JSON の数が来たら、その整数として受け付ける。** `1000.0`、`1e3`、`1.5e3`、`-0.0` は、1000、1000、1500、0 である。整数に直したあとは、範囲と刻みをいままでどおり確かめる。値が整数でない数(`1000.5`)は、いままでどおり「整数ではありません」で拒み、文字列の `"1000"` もいままでどおり拒む。日付は文字列で渡すので、この読み方とは関わらない。
2. **参照評価器も同じに読む。** `src/door.rs` の `whole` が、小数点を書いた整数(`Json::Frac` の小数部が全部 0)を整数にし、受け付けない入力のファイルを書く入口と `fixtures` の入口が使う。rulec 自身の JSON の読み手(ritsu-base の `json`、ritsu の DESIGN 4.9)は指数の書き方を読まないので、`1e3` は参照評価器の入口に来る前に、読み手が行の誤りにする。★
3. **全部の口を同じ読み方にした。**
- 数を書かれた桁のまま持つ読み手は、桁と指数から値を正確に求め、64 ビットに収まる整数だけを受け付ける。Go の runner(`json.Number`)、Rust の runner と Wasm の入口の自前の読み手、Java の runner の自前の読み手(`BigDecimal.longValueExact`)である。
- 数を言語の数(倍精度)で持つ読み手は、値が整数の小数を整数に直す。Python の runner・MCP のサーバ・Connect の runner、Ruby と PHP と Swift の runner、SQL の二つの runner、NumPy の計画である。PHP と Swift は、64 ビットに収まらない値をいままでどおり拒む。
- TypeScript と JavaScript の runner と MCP のサーバは、もとから受け付けていた。
- 呼び出し側が何でも渡せる言語のモジュールの入口(Python、TypeScript、JavaScript、Ruby)も、値が整数の小数(TypeScript と JavaScript では値が整数の `number`)を受け付け、その整数(TypeScript と JavaScript では `bigint`)として計算する。並びの要素のフィールドは、整数に直した値を持つ要素を新しく作り、たどる続きはそれで計算する(呼び出し側の要素は書き換えない)。
- ★ PHP のモジュールの引数は型の宣言のまま(§15.203)なので、`declare(strict_types=1)` の呼び出し側からの `float` は、PHP が型の誤りにする。runner は整数に直してから渡す。
- 記録の `in` には、整数として書く。NumPy の runner と SQL の二つの runner は、読んだ値をそのまま書いていたので、直した値を書くようにした。
4. **倍精度で持つ読み手では、2^53 を超える値を小数の形で書くと、最も近い倍精度の値として読む。** 整数の形で書いた値は、いままでどおりに読む。宣言する範囲はふつうこの大きさに届かないので、読み手を変えていない。
5. **受け付けない入力のファイルは、整数でない数の行をいままでどおり持つ。** 値が整数の小数の形を受け付けることは、`tests/whole_numbers.rs` が、整数を別の形に書き直したベクタを全部の通り道に流して確かめる。
**捨てたもの**:
- **`rulec test` が全部の規則で、ベクタを小数の形に書き直してもう一度流す形。** 全部の規則の `rulec test` の時間が倍近くになる。読み方は規則によらず生成器の読み手で決まるので、数の種類のそろった規則で一度確かめれば足りる。
- **rulec の JSON の読み手に指数を読ませる形。** 読み手は ritsu の全部の言語が使う ritsu-base のもので、指数を読まないのはそこの決めである(ritsu の DESIGN 4.9)。
- **文字列の `"1000"` も数として受け付ける形。** 文字列は数ではない(§15.204)。protojson が int64 を文字列で書くことは、射影の関数が契約の形として読む(§15.125)。
**確かめたこと**:
- `tests/whole_numbers.rs` の四つ。英語の規則を先に、日本語の版を横に置いた五つずつ(金額と重さの `member_shipping_fee` と `送料`、0.01% 刻みの率の `health_insurance_premium` と `健康保険料`、`optional` の数と制約の `parcel_cover` と `小包の補償`、並びの要素の金額の `nationwide_freight` と `全国運賃`、並びの合計の `cart_shipping_fee` と `買物かごの送料`)。
- ベクタの入力の整数を全部、小数点、指数、小数点と指数(`1.5e3`)、`-0.0` のどれかの形に書き直して `rulec test` に通すと、全部の通り道(十二の言語の runner、Rust の WASI、Python の Connect の四つ、PostgreSQL の関数)が、整数の形と一字も違わない記録を返す。英語の五つと日本語の五つで、それぞれ 4,186 の数を書き直した。MCP のサーバは、`rulec test` がベクタではなく期待値の `in` から呼ぶので、次の試験で確かめる。
- Python と TypeScript と JavaScript の MCP のサーバが、`2500.0` と `1.2e4`、`25e2` と `-0.0` を整数と同じに答え、`2500.5` を「整数ではありません」で拒む。
- Python と JavaScript と Ruby のモジュールの入口が、入力と並びの要素で、値が整数の小数を整数と同じに答え、`2500.5` を拒む。
- `fixtures lint` が、`2500.0` と `-0.0` の記録を受け付け、`2500.5` を「小数」と言って拒む。受け付けない入力のファイルには、整数でない数の行がいままでどおりある。
- 替える前と後の突き合わせ。木の 353 の `.rule` で `gen` を英語と日本語で 706 回走らせ、生成できた 414 回のうち、17,060 のファイルが同じで、7,662 のファイルが変わった。変わったのは数の読み手と入口だけである(差分の行を種類ごとに数えた)。どの規則でも変わったのは、Python の runner と Connect の runner と MCP のサーバ、Ruby・PHP・Go・Rust・Swift・Java の runner、Wasm の入口(各 414)、SQL の二つの runner(各 396)、NumPy の runner(392)である。数の入力を持つ規則の英日(334 回)では、ほかに Python・TypeScript・JavaScript・Ruby のモジュールと、それを埋め込んだページが変わった。`rulec api` と `rulec schema` は、§15.206 の二つのほか変わらない。`rulec test` は、生成できた 207 の規則で、替える前と後の結果(23 の通り道、7,205 の実行)が全く同じだった(通らない 3 は替える前から通らない変異 `m_w121.rule`。PostgreSQL の関数は立てずに走らせた)。
**見つけて直していないもの**:
- **射影の関数(§15.125)は、整数でない数を言語ごとに違って読む。** 値が整数の小数はどの言語でも整数になるが、`1000.5` は、Python(`int(…)`)と Ruby(`.to_i`)が 1000 に切り捨て、TypeScript と JavaScript(`BigInt(…)`)は `RangeError` で止まる。射影は、契約の検査(JSON Schema、Protovalidate)を通ったオブジェクトを読む前提で、数を契約の形どおりに読み替える(§15.204 の「見つけて直していないもの」の三つ目)。入口の文で拒むには、射影の読み方を言語ごとに変える要がある。
### 15.206 `rulec api` の Wasm の項目に、答えの二つの形を書く(2026-10-08)
**きっかけ**:§15.204 の「見つけて直していないもの」の一つ目と、§15.202 の「見つけて直していないもの」の Wasm の項目。Wasm のモジュールは、受け付けない入力に記録の代わりに拒む行(`{"refused":"input","error":"…"}`)を返す(§15.204)。それなのに `rulec api` の Wasm の項目はその形を言わず、Rust の項目の `wasi` の `wire` も「標準出力に記録を一行ずつ」としか言わなかった。ホストは、生成物を読まないと、答えが記録か拒む行かを見分ける鍵を知れなかった。
**決定**:
1. **Wasm の項目に `answers` を足す。** `call` が返す二つの形を、この順に並べる。一つ目は記録で、`kind` が `record`、`keys` が `in`・`observed`・`trace`(ほかの言語の記録の関数が書く鍵)。二つ目は拒む行で、`kind` が `refusal`、`keys` が `refused`・`error`、`refused` がとる値が `input`・`contradiction`。`refused` の値は、§15.202 の `error_types` の `kind` と同じ意味である(`input` は入口が拒んだ呼び出し側の入力、`contradiction` は W114 の守り)。`error` は文で、モジュールが数として持つ値があれば、文のあとに `: ` で足す(§15.204)。
2. **Rust の項目の `wasi` の `wire` に、拒む行を書く。** 「標準出力に記録を一行ずつ、拒んだ行には記録の代わりに `{"refused":…,"error":…}`」。`wire` は文面で、鍵の形は変わらない。
3. **鍵を足すだけにする。** ほかの鍵の名前と形は変えない。互換のページの「機械が読む形に鍵を足してよい」の範囲である。
4. **Connect と SQL の項目には足さない(§15.202 のまま)。** Connect の呼び出しはプロトコルの符号(`invalid_argument`、`internal`)で失敗し、SQL は問い合わせの `guard` の列と、関数の SQLSTATE(`raises`)で拒む。どちらも、拒み方を決めているのはプロトコルと PostgreSQL で、項目はもうその名前を持つ。Wasm のモジュールは例外を上げず、答えの文字列だけが拒んだことを言う。その文字列の形は rulec が決めたもので、外のどの決まりも言わないので、項目が言う。
**捨てたもの**:
- **`error_types` を Wasm にも足す形。** `error_types` は、言語の例外の型の名前と、呼び出し側が読むフィールドの一覧である。Wasm の答えには型の名前が無く、`error_types` があると「捕まえる例外がある」と読める。
- **全部の言語の項目に、runner の拒む行を書く形。** runner は `rulec test` が生成物を確かめるための道具で、呼び出し側が組み込む口ではない。WASI の runner だけは、Fastly Compute や Spin の上でそのまま動かす形として項目に載っている(§15.63)ので、`wire` に書いた。
**確かめたこと**:
- `tests/api_errors.rs` の `the_wasm_entry_says_what_its_module_answers`。英語の `nationwide_freight` を先に、日本語の版 `全国運賃` を横に置いた。`answers` が記録と拒む行の二つで、鍵と `refused` の値が上のとおりであること、`wasi` の `wire` が拒む行を言うこと。項目の `build` の行でモジュールを組み、項目の名前(`call`・`realloc`・`memory`・`post_return`)だけで書いたホストから呼ぶと、要素一つの並びには記録の鍵で、範囲の外の要素には `input` と文(`row_fee is out of range: 200000`、`行運賃 が範囲の外です: 200000`)で、二つの要素が `take_unique` に当たる並びには `contradiction` と文で答える。
- 替える前と後の突き合わせ:全部の規則の `api`(英日の 706 回)で、生成できる 207 の規則の 414 回は Wasm の項目の `answers` と `wasi` の `wire` だけが変わり、残りの 292 回(`check` を通らない規則)と、`schema` の 706 回は同じだった。
### 15.207 Python のエラーの値を repr の文字列で持ち、組み込み関数は別の名前で呼ぶ(2026-10-11)
**きっかけ**:生成した Python のモジュールを、型を付けた Python の部分集合を静的にコンパイルする処理系が、そのまま取り込めるか。コーパスの規則から生成できたモジュール 87 本をそのような処理系の検査にかけると、`RuleInputError` の `_NOVALUE = object()` が 87 本全部で止められた。`object` は「どんな値でも入る」型で、`object()` の番兵は、ほかのどの値とも違う印として、モジュールを読み込むときに一度だけ作る。型を実行の前にすべて決め、読み込みのときの文を走らせない処理系は、どちらも持てない。検査が止めた書き方はほかにもある(`NewType`、型の別名、組み込み関数の別名、`frozenset` の定数など)が、どれも型を付けた Python としては普通の書き方なので、処理系の側が受け付けるものとして、この節では扱わない。この節の問いは二つある。一つは、静的にコンパイルする処理系が生成した Python を取り込めるようにするには、エラーの値をどう持つか。もう一つは、そのエラーに値を渡す行が `repr()` を呼ぶので、生成した関数が組み込み関数を呼ぶ行をどう書くかである。モジュールの先頭のコメントは「組み込み関数を素の名前で呼ばない」と言っていたが、素の名前で呼んでいたのは `isinstance` のほかにもあった。`float(x)`、`int(x)`、`len(items)` がそうで、別名が `int` や `float` の入力を持つ規則は、正しい入力でも `TypeError` になった(確かめた)。
**決定**:
- **`value` は、拒んだ値の `repr()` の文字列で持つ。** 型は `str | None` で、値が一つに決まらない拒否(二つの入力のあいだの `constraint`、足りない入力)では `None` にした。`_NOVALUE` は無くした。`repr()` は拒む場所で取る。入口のガード、要素のガード、並びの長さと合計の上限は `raise RuleInputError("…", _repr(x))`(`_repr` は組み込みの `repr` に付けた名前。下の決定)、runner・Connect の runner・MCP のサーバーがワイヤを読む関数は `raise RuleInputError("…", repr(x))` の形に揃えた。
- **メッセージは一字も変えない。** `__str__` は `what` と `value` を `: ` でつなぐだけになった。値の `repr()` を、エラーを表示するときではなく拒むときに取る違いだけなので、できあがる文は同じである。
- **見える面は変えない。** `rulec api` の `error_types` は、`fields` に名前(`what`、`value`)だけを挙げるので、出力は同じである。`docs/formats.md` には、値が無いことを表すものが Python では `None` になった、と書いた。NumPy の `rulec_np.RuleInputError` は、numpy のスカラーを値に持つこともある別のクラスなので、この直しの外にした。
- **規則の名前を持つ関数は、組み込み関数を、モジュールが付けた名前で呼ぶ。** 規則の別名が名前になる関数、その `_traced`、`_record`、`_from` には、入力・定義・表の出力・並びの名前が入る。それが `int` や `len` のような名前だと、関数の中の `int(x)` や `len(items)` は、組み込み関数ではなくその入力を呼んでしまう。そこでモジュールは、関数の前に、使う組み込み関数ごとに一行ずつ書く。`_isinstance = isinstance`、`_int = int`、`_float = float`、`_bool = bool`、`_str = str`、`_repr = repr`、`_len = len`、`_any = any`、`_all = all`、`_sum = sum` の十個のうち、そのモジュールの関数が使うものだけである。関数は `_int(x)` と書き、`isinstance` の型の引数も `_isinstance(x, _int)` と書く。補助関数(丸め、`_json_str`、`_proto`、`_dig`、`_days`)には規則の名前が入らないので、組み込み関数をそのまま呼ぶ。規則の言語の `min` と `max` は、前から `_min`、`_max` の補助関数として書いてある。
- **先頭のコメントを、実際に合わせた。** 「規則の名前を持つ関数は、組み込み関数を、ここで付けた名前か下の補助関数でしか呼ばない」と書く。コメントが一行ごとに空行で切れて出ていたのは、`pep8_blanks` がトップレベルの行のあいだにいつも空行を足すためだった。コメントはその下の行に付けて続け、組み込み関数に別の名前を付ける行(`_int = int`)も続けて書くようにした。グループの定数は、`_remote` のようにグループの名前の前に下線を付けた名前なので、グループの名前が `int` だと `_int` になり、組み込み関数に付けた名前と重なる。重なるときは下線を一つ足す(`_int_`)。補助関数の名前と重なるときも同じで、グループの名前が `json_str` だと、前から `_json_str` の定義に上書きされて壊れていた。
- **ほかの `object` と `Any` を調べた。** 生成する Python を全部読んだ(モジュール、`_traced`、`_record`、ステートマシン、並びの歩き・数え上げ・合計、`apply`、`shape` の取り出し関数、runner、Connect の runner、MCP のサーバー)。モジュールで `object` を使っていたのは `RuleInputError` だけだった。`Any` は、呼び出し側のオブジェクトから入力を取り出す関数(`<別名>_from`、`_proto`、`_dig`)にだけあり、木の規則 192 本のうち 12 本のモジュールに出る。呼び出し側のオブジェクトは任意の JSON なので、静的な型にするには、`shape` ごとに `TypedDict` を生成するか、JSON の再帰的な型と絞り込みの関数を足すことになる。どちらも取り出し関数の形が大きく変わるので、この直しでは変えない。型の付いた値で規則を呼ぶアプリは、この関数を使わない。runner・Connect の runner・MCP のサーバーは、JSON を読む実行ファイルで、`Any` と `object` を JSON の値の型に使う。取り込まれるものでもないので、変えない。
**捨てたもの**:
- **その処理系向けに、Python の別の出力を足すこと。** Python の出力が二つになる。ネイティブの側が正しいかを確かめるために、ritsu のテストがその処理系のエンジンのビルドを抱えることにもなる。生成する Python を静的な型だけで書けば、出力は一つのままで済む。
- **`value: object | None = None`。** 番兵は要らなくなるが、拒んだ値が `None` の場合(`x is not a boolean: None`)と、値が一つに決まらない拒否が区別できなくなる。`repr()` の文字列で持てば、`None` を「値が無い」に使える。
- **`value` を `int | None` にすること。** 拒まれる値は整数だけではない。文字列、小数、列挙のメンバー、`None` も来る。
- **値を元のオブジェクトのまま持ち、`__str__` に `!r` を残すこと。** `value` の型が `object` に戻る。
- **補助関数の組み込み関数も、別の名前で呼ぶこと。** 補助関数には規則の名前が入らず、守るものが無い。規則の別名や列挙の別名が `abs` のような組み込み関数の名前なら、ファイルの一番外側でそれを隠すが、それは W121 が警告する(入力や表の出力の別名は関数の中だけの名前で、W121 も関数も困らない)。
- **`import builtins` して `builtins.int(x)` と書くこと。** 一行で済むが、取り込む処理系が受け付ける書き方が、モジュールの import と属性の参照でもう二つ増え、前からある `_isinstance = isinstance` の形とも揃わない。
- **入力の名前を、組み込み関数と重ならないように変えて出すこと。** 公開する関数の引数の名前が変わり、名前で渡している呼び出し側が壊れる。
**変わること**:`e.value` が、元の値ではなく、その `repr()` の文字列になる。`2500.5` は `'2500.5'`、文字列 `'1000'` は `"'1000'"`、列挙のメンバーは `<Kind.PERCENT: 'percent'>` である。元の値が要る呼び出し側は、渡した引数のほうを見る。`str(e)` と `e.what` は変わらない。0.x のあいだの小さな非互換である。生成したモジュールの先頭に、使う組み込み関数ごとの一行(`_int = int` など)が増え、関数の中の `isinstance`・`int`・`float`・`bool`・`repr`・`len` などが、その名前に替わる。公開する名前と、関数が答えるものは変わらない。
**正直な限界**:
- 補助関数(丸めの `abs`、`_json_str` の `ord`、`_proto` の `enumerate`・`isinstance`・`len`・`dict`、`_days` の `int`)は、組み込み関数をそのままの名前で呼ぶ。規則の別名や列挙の別名がその名前だと、モジュールの一番外側で隠して補助関数を壊す。それは W121 が警告する。
- 二つのエラーのクラスの `super()` は、別の名前に付け替えられないので、そのまま呼ぶ。グループと最終状態の `frozenset(...)` は、規則の名前が入る前の、読み込みのときの一行である。`AssertionError` のような例外のクラスは、組み込み関数ではないので、そのまま書く。
- fold の局所変数の型注釈(`answer: int = …`)は、`int` や `bool` をそのまま書く。`int` という名前の入力や並びがあると、実行は正しいが、`mypy` はそれを型として読めない。
- 取り込む処理系が受け付けなければならないのは、`_isinstance = isinstance` と同じ形の、組み込み関数に別の名前を付ける一行(`_int = int`、`_repr = repr` など)と、その名前での呼び出しである。型の引数に書く名前(`_isinstance(x, _int)`)も同じ名前である。
**確かめたこと**:木の `.rule` 232 本のうち検査を通る 192 本で、前と後の生成物を比べた。変わったのは Python の 960 ファイル(モジュール、runner、Connect の runner、MCP のサーバー、丸めの試験。どれも 192 本)だけで、ほかの言語は一字も変わらない。前と後のモジュールに同じ引数(正しいもの、範囲の外、型違い、巨大な数、`None`)を 744,000 回渡し、戻り値、例外の型、`str(e)`、`what`、`args` が同じで、前の `value` の `repr()` が後の `value` であることを比べた。MCP のサーバーにも同じ引数のオブジェクトを 558,000 回渡して、JSON-RPC の答えを比べた。どれも全部一致した(比べたのは 186 本。組み込み関数の名前の規則 4 本とグループの名前の規則 2 本は、前のモジュールでは呼ぶと `TypeError` になる)。`rulec test` をコーパスで回すと、1,997 件が全部一致した(Python の分は、runner・MCP・Connect の 610 件)。型を付けた Python の部分集合をコンパイルする処理系の検査にかけると、この直しの前は、コーパスの 87 本全部で `_NOVALUE = object()` の行が止められた。直したあとは止められない(かわりに、`_repr = repr` などの一行が、`_isinstance = isinstance` と同じ理由で止められる)。コーパスの 87 本が使う別名は、`_repr` と `_isinstance` が 87 本、`_bool` が 86 本、`_int` が 81 本、`_float` が 77 本、`_len` が 6 本、`_any` が 4 本である。
`tests/error_value.rs` に四本、`tests/builtin_names.rs` に四本足した。前者は、モジュールが `object` と番兵を持たず、`Any` は取り出し関数のあるモジュールにしかなく、エラーに渡すものがどのファイルでも `repr` であることを見るのが一本、Python で拒否のメッセージと `value` を確かめるのが一本、MCP のサーバーでメッセージを確かめるのが一本、引数を取り出す関数の試験が一本である。後者は、入力・表の出力・出力・並び・取り出しの形の名前を `int`・`float`・`bool`・`repr`・`len`・`str`・`list`・`dict`・`any` などにした規則(`tests/builtins/`)が、参照評価器と同じ答えを返し、同じ文で拒むこと、規則の名前を持つ関数が組み込み関数を素の名前で読まないこと(コーパスの十対の規則の、三十四の関数を symtable で見る。前の生成器の出力では落ちる。木の 192 本の生成物の全部にも同じ見方をかけて、読むものは無かった)、コメントが一まとまりで出ること、グループの定数が別名を取らないことを見る。英語の規則と日本語の規則の両方で走る。
### 15.208 並びを歩く規則の三つの段と、出力のセルの語の読み方(2026-10-11)
**きっかけ**:並びを歩く規則を書いて走らせると、四つの形で答えが出なかった。fold の `empty` に規則の中で計算する値(入力から作る `define`)を書くと、値が出ない。fold の答えを後ろの表が読むと、その表の値が出ない。要素ごとの定義が、要素によらない表(配達の方法から係数を決める表)の値を読むと、検査は通り、評価器は合計に 0 を足した。列挙を答える fold は、空の並びでも、何も持たずに終わったときも、値を出さなかった。どの規則も、生成したコードは動かなかった(Python は `UnboundLocalError`、Go と Rust は組めない)。
四つの原因は一つだった。並びを歩く規則の項目は、本当は三つに分かれる。要素も歩きの答えも読まない項目は、歩く前に一度。要素の値を読む項目は、要素ごとに。歩きの答えを読む項目は、歩いたあとに一度。評価器と生成器は、これを分けていなかった。`run_fold` は要素ごとに全部の項目を走らせ、答えを最初の出力にだけ入れて、後ろの表を走らせなかった。`empty` と `exhausted` の値は、入力と `held` だけの環境で評価した。`run_counted` は、要素によらない表を歩いたあとに回し、要素の値が無いときは 0 として足した。生成器は、答えの変数をループの前に置き、項目を全部ループの中に書いた(`count`・`sum` では、要素によらない項目をループの後ろに書いた)。型付けは、fold の答えの語を答えの型で読まず、列挙の値を名前として扱った。
同じ規則を書く中で、小さな食い違いが六つ見つかった。
1. 上の表が入力をそのまま渡すセル(`| c | x |`、`x` は同じ列挙の入力)を、下の表が読むと、E102(上の表は `a` を出さない)と E101(`a` の行が無い)が同時に出て、どちらにも直せなかった。上の表が出す値の集め方が二つあった。型付けの `out_values` は、セルの語をそのまま値として集めて `x` を値の名前と読み、完全性の `upstream_blocked` は、宣言した名前を「何でもありうる」と読んだ(§15.65 の直し)。
2. 出力と同じ名前の `count`・`sum`・`derive` に W111(使われていない)が出た。出力はその値を返している。文は `sum` も Count と呼んだ。
3. E044 の英語の文で、表と出力の名前が入れ替わった。日本語の文の順に、英語にも引数を渡していた。
4. `exhausted -> held` の `held` が何を返すかを、文法の表が書いていなかった。評価器と生成器は「採った値、無ければ持ち越した値、それも無ければ `empty` の答え」で一致している。
5. 出力のセルに書いた列挙の値が、同じ綴りの、別の型の宣言(数の出力)として読まれて、E103 になった。パーサは出力のセルの裸の語を名前として持ち、型付けは「宣言した名前なら名前」を先に選んで(§3.2)、型が合わなくても値として読み直さなかった。例の期待値のセルも同じだった。
6. 同じ出力を決める節どうしの重なりの E105 が、節を「表」と呼び、`policy` の後ろに `overrides` を書けと言った。節には `policy` が無い。重なりを E105 にすること自体は正しい(下の「捨てたもの」)。
**決定**:
- **項目を三つの段に分ける関数を一つ置く**(`types::stages`)。§15.58 の「要素しか持たない値を読む項目は要素ごと」に、「歩きの答えを読む項目は後」「どちらも読まない項目は前」を足した。歩きの答えは、fold の答え(最初の出力)と、`count`・`sum` の名前である。読むものは、ほかの項目を通しても数え、同じ出力を書く表は、読むものを合わせて一つの段にする。`count` と `sum` そのものは要素ごとに足すので、要素ごとの段にいる。前の項目が後の項目を読むことは、定義が上から順なので起きない。`element_scoped` は、この関数の要素ごとの段を返すだけになった。評価器、八つの生成器、検査はこの一つを見る。ベクタと網羅は評価器を通して同じものを見る。証明書は段を読まない。要素ごとの表は、一件の要素を一つの入力として、ほかの表と同じに証明する(§15.56 のまま)。
- **評価器は段の順に走らせる**(`eval::run_walking`)。前の項目を一度走らせてから歩き、要素ごとの項目には前の値を渡す。`empty`・`exhausted`・`stop with` は、前の値と `held` の上で評価する。歩いたあとに、答えを最初の出力に入れ、後の項目を走らせ、出力を全部読む。`count` と `sum` で、要素の値が無い要素があれば、答えを「値が無い」にする。0 として足さない。
- **生成器も同じ形にする。** 前の項目をループの上に、要素ごとの項目をループの中に、後の項目をループの下に書く。答えの変数の初期値(`empty` の答え)は、前の項目のあとに置く。Python、TypeScript、Rust、Ruby、Swift、Go の歩きと、`codegen/java.rs`・`codegen/php.rs` の歩き(JavaScript は TypeScript から、Wasm は Rust から作る)の、fold と `count`・`sum` の全部を、この形にそろえた。前の項目の値は普通の局所変数で、Python の番兵は使わない(§15.207)。fold の局所変数は、使うものだけを宣言する。
- **fold の答えは、最初の出力の型で読む**(`types::read_words`)。その列挙の値の語は値(`empty -> unavailable`)、`true` と `false` は真偽の値である。型の合わない値は E103、値でも宣言した名前でもない語は E012 にした。答えが、それを決める所に無いものを読めば E021 にする。`empty`・`exhausted` が要素の値を読むとき、`held` を `exhausted` の外に書くとき、答えや `keep_max` の比べる値が歩いたあとの項目を読むときである。
- **要素の値と歩きの答えの両方を読む項目は、E031 にする。** 答えは並びを最後まで見ないと決まらず、要素の値はその要素を見ているあいだにしかない。両方を読むには、歩き終えてから、もう一度並びを歩くことになる。それを書く形を入れるまでは、黙ってどちらかの段に置かない。E031 の題と注は、この形と、`fold` と `count`・`sum` を一緒に書く形の二つを言えるように書き直した。要素ごとの値を出力にすること、`result` の行が要素の値を読むことも、同じ理由で E031 にした。
- **入力をそのまま渡すセルが書く値を、一つの関数にする**(`Checked::row_writes`)。リテラルならその値。宣言した名前で、その名前が同じ表の入力の列にあるなら、その行のその列のセルが許す値(`| c | x |` なら `c` だけ)。そうでなければ、その名前の型の値の全部(安全側)。型付けの `out_values` と、完全性の `upstream_blocked` が、これを使う。`a` の行がある版は E102 だけ、無い版は通る。§15.65 が直した向き(正しい規則を E102 で拒まない)は保つ。
- **出力と同じ名前の宣言は、使われているものとして数える**(W111)。`count`、`sum`、`derive`、`define`、表の列のどれも。題は、数え上げと合計を言い分ける。
- **E044 の文は、名前を付けた引数(`{table}`、`{output}`)にした。** 二つの言語の文が、それぞれの順で読む。
- **`held` を、文書とページに書く。** 文法の表の `exhausted` の行を「採った値(`take_unique`・`take_first`)、無ければ持ち越した値(`keep_max`)、それも無ければ `empty` の答え」にし、`generated-code.md` と、`held` で答える fold のページ(承認する人の向けと、客に見せる記事)に同じ文を足した。振る舞いは変えない。
- **出力のセルの語は、列の型で読む**(`types::read_words`、新しい診断 E075)。列の列挙に値があり、同じ綴りの宣言の型がその列挙でないなら、値として読む。両方が当てはまる(同じ列挙の入力 `current` があり、列挙にも値 `current` がある)なら、どちらの意味かを書き手に決めさせる E075「`current` は、列挙 plan の値でも、入力の名前でもあります」にする。fold の答えも同じに読む。例の期待値のセルは値しか書けないので、いつも値として読む。評価器と生成器は、型付けが決めた読み方を使い、名前を引き直さない。E075 の語は、W111 の「どの行にも現れない値」に入れない。
- **節どうしの重なりの E105 の注は、種類で言い分ける。** 節なら「優先する節の下に `overrides <相手>` の行を書いてください」、表なら今までどおり `policy` の後ろに書く。二つが同じ答えを返すときは、注を一つ足す:「二つは同じ答えを返します。どちらが決めてもよいなら、一つの `policy first` の表の行にするか、`overrides` で順を書いてください」。
- **生成する Python のモジュールの条件は、真偽の値だけにする。** 型を付けた Python の部分集合をコンパイルする処理系の中には、`if` や `and` の条件に真偽の値でないものを書くことを受け付けないものがある。生成器が真偽の値でないものをそのまま条件にしていたのは五か所だった。丸めの補助関数 `_round_up` の余り(`if r` を `if r != 0` に)、`_record` の行のラベルと呼び手の付けた印(`if f.label` を `if f.label != ""` に、印も同じ)、並びを歩く `_traced` の並び(`if items and not stopped` を `if _len(items) > 0 and not stopped` に。組み込み関数は §15.207 の別名で呼ぶ)、`_record` が書く、値の無いことがある真偽の入力(`"true" if x else "false"` を `"true" if x is True else "false"` に)である。どれも答えは変わらない。明示の比較は、生成したコードとしても読みやすい。どのモジュールにも三か所(`_round_up` に一つ、`_record` に二つ)、fold を持つモジュールに一か所、値の無いことがある真偽の入力ごとに一か所増える。呼び手のオブジェクトから入力を取り出す関数(`<別名>_from`)は、任意の JSON の値を読むので(§15.207)、この直しの外にした。runner、Connect の runner、MCP のサーバーも、取り込まれるものではないので変えない。
**捨てたもの**:
- **段を書き手に宣言させること。** 読むものから決まり、§15.58 もそうしている。宣言させれば、宣言と中身の食い違いを別に確かめることになる。
- **歩いたあとに、規則の項目を全部もう一度走らせること。** 要素ごとの項目を要素の外で評価することになり、要素の値が無いまま答えを作る。
- **両方を読む項目を、どちらかの段に黙って置くこと。** 前に置けば要素の値が無く、後に置けば最後の要素の値を読む。どちらも答えが黙って違う。
- **曖昧な語を、名前を先に読む順(§3.2)で決めること。** 同じ列挙の名前と値が同じ綴りなら、どちらを書いたつもりかは書き手にしか分からない。黙って一つを選べば、取り違えは検査を通る。
- **同じ答えを返す節どうしの重なりを E105 にしないこと**(DMN の Any)。§4 で捨てた形である。今日同じ答えでも、明日の編集で片方だけが変わる。
- **無い値を 0 として足すこと。** 黙って 0 にするのは、それだけで別の誤りである。
- **Python の条件の直しを、その処理系の側に任せること。** 真偽の値でない条件を受け付けるかどうかは、処理系ごとに違う。比較を書けば、どの処理系でも同じに読める。
**変わること**:並びを歩く規則で、答えが出なかった入力に答えが出るようになり、要素の値が無い要素を含む合計は 0 ではなく「値が無い」になる。これまで検査を通った規則が、E075(どちらにも読める語)、E031(両方を読む項目、要素ごとの出力)、fold の答えの E012・E103・E021 で止まることがある。どれも、黙って誤った答えを出していたか、どちらの意味か決まっていなかった規則である。出力と同じ名前の宣言への W111 は出なくなる。生成するコードは、並びを歩く規則で項目の位置が変わり、Python のモジュールは条件が明示の比較になる。
**正直な限界**:
- 要素の値と歩きの答えの両方を読む項目は、書けない(E031)。並びを二度歩く形を入れるまでの限りである。
- 条件のセル(入力の列)の語は、前から列の型で読んでいたので変えていない。E075 は出力のセルと fold の答えにだけ出る。
- `<別名>_from` の条件は、取り出した JSON の値をそのまま真偽として読む所が残る(`_e["chilled"]` のような形)。任意の JSON を読む関数なので、§15.207 と同じく、この直しの外にした。
**確かめたこと**:
- 直す前の 0.26.0 では、`tests/stages` の三つの規則(英語の版と日本語の版)が、どれも例で落ちた(`best should be … but no value came out`、`charge should be 17 but came out as 0`、`shipped should be unavailable but no value came out`)。直したあとは、六本とも何も言わずに通る。生成したコードは、`rulec test` で六本とも 25 件が参照の評価器と一致した(Java は `javac` が、Connect の段は `protoc-gen-py` が、この機械に無いので飛ばした。SQL と NumPy は並びを歩く規則を生成しない)。新しいコードを含む生成した Python の三本は、`pyright --strict` で、前からある補助関数と import の未使用のほかに、何も言われなかった。
- `tests/fold.rs` に六本足した。三つの段の規則が何も言われずに通り、ベクタが網羅の七つの基準を全部満たすこと(合計が境目に来る並びはベクタの探し方では作れないので、例に書いた)、生成した Python で前・ループ・後の順になること、六本の生成コードが参照の評価器と一致すること、fold の答えの E021・E012・E103 の五つの形(英語と日本語)、`result` の行の E031 と、通るときに歩いたあとで足されること、ページが `held` を言い、`held` を使わない fold では言わないこと、である。`tests/count.rs` に二本(出力と同じ名前の `count`・`derive` に W111 が出ないこと、要素ごとの出力と両方を読む定義の E031 の文と印)、`tests/region.rs` に一本(入力をそのまま渡すセル)、`tests/values.rs` に二本(出力のセルと例の値の読み方と生成した Python の列挙のメンバー、出力のセルと fold の答えの E075)、`tests/defset.rs` に一本(節の E105 の注)、`tests/apply.rs` に一本(E044 の文)。変異は、E075、E031 の新しい形、W128(§15.209)を英語と日本語の対で足し(手で書いたもの。コーパスに無い形なので)、`m_e031` の種は fold の答えを答えの型の値にした(前の種は列挙を答える fold に `false` を書いていて、答えの型を読むようになると、その誤りも出た)。golden は E075・E031 の新しい形・W128 の三つを、日本語と英語で足した。
- 木の `.rule` 377 本を、0.26.0 と直したあとで `ritsu rulec check` にかけ、診断の符号と行を比べた。357 本は同じだった。違ったのは 20 本で、16 本はこの節と §15.209 で足した材料、残る 4 本は §15.209 の W128 である。
- 生成する Python の条件:生成器で直したのは五か所。コーパスの 87 本から生成したモジュールでは 263 か所(どのモジュールにも三か所、fold を持つ二本に一か所ずつ)が明示の比較になった。値の無いことがある真偽の入力は、コーパスには無い(`tests/optional` の規則で、`"true" if signature is True else "false"` になることを見た)。コーパスと `tests/stages` の 93 本のモジュールの、`if`・`elif`・条件式・`and`・`or`・`not` の条件を Python の構文木で全部読み、真偽の値(比較、`isinstance`、真偽しか入らない名前)でないものが無いことを確かめた。
### 15.209 計算した列の線形の等式を消去に入れ、決められない行を W128 で言う。`ritsu check --budget` と、規則を呼ぶフローのシナリオ(2026-10-11)
**きっかけ**:量に加算をして、上限を超えた分だけ減らす表を書いた(一般の名前で書き直したものが `tests/reach/E102_past_a_defines_equation.rule`)。上限 `limit`、加算後の量 `added`、超えた分 `excess = added - limit` の三つを `define` で計算し、表の列にした。行 4(`| >=1 | >=1 | >=1 | <=1 | 0 |`、上限が 1 以上、超えた分が 1 以上、加算後が 1 以下)には、どの入力も当たらない。上限が 1 以上で超えた分が 1 以上なら、加算後は 2 以上だからである。0.26.0 の `check` は E102 も警告も出さず、`coverage` は「No vector produces an input on which this row wins. A row nothing can reach is named by E102 instead, so this is usually the generator not getting there.」と言った。証明書を取ると、行 4 の到達の点は `base = 1, limit = 1, excess = 1, added = 0` で、`excess = added - limit` を満たさない点だった。
これは誤りではなく、いまの意味論の限りである。rulec の「訊かれる点」は、列のそれぞれが自分の取りうる範囲に入っていることで、計算した列どうしの関係は入っていない(§6.2、`proofs/RulecCert/README.md`)。E102 は「示せたときだけ」出す約束なので、言わないこと自体は約束に反しない。証明書も、その意味論の上では正しい。ただ、`check` の `ok` を見た人は、どの行にも当たる入力があると読む。黙っているのはよくない。
同じ時期に、二つの抜けが見つかった。
- `ritsu rulec check` には `--budget` があるが、`ritsu check` には無かった。予算を超えた規則(E109、ステートマシンなら E128)は、`ritsu check` では越えられなかった。
- `ritsu dandori scenarios` は、手元の規則の呼び出しも、タスクと同じく選ぶ場所として扱い、その答えをシナリオの並びに書いた。`ritsu run` は手元の規則を rulec で計算し、シナリオから答えを読まない。だから、規則を一度呼ぶたびに、後ろの答えが一つずつずれた。出来事を受け取るたびに規則を呼ぶフローは、生成したシナリオのどれも、生成器が選んだ終わり方で終わらなかった。
**決定**:
- **計算した列の線形の等式を、消去に入れる。** 数を計算する `define` の式が、足し算・引き算・定数倍だけなら、その等式を Fourier–Motzkin の消去(`fourier.rs`、§15.126)に足す。`derive` の等式と同じ扱いである。等式はどの評価でも成り立つので、足しても健全である。この行 4 は E102 の三つめの形になり、注は「`added = base + bonus` と `excess = added - limit` は、どの入力でも成り立つ式です。この式も合わせると、この行の条件をすべて満たす値はありません」と言い、直し方も式どうしの関係で言う。完全性の検査(E101)も、同じ等式で閉じる穴の行を求めない。
- **証明書は、等式を `define` の事実として運ぶ。** `linear.facts` の `{"define": 名前, "le": true}` と `"le": false` が、等式の二つの半分である。再検査は、等式を証明書の言葉から取らず、`values` にある定義の式から作り直す。`tools/recheck.py` の `model_facts` と、Lean の `RulecMain` の `factOf` が、`derive` と同じく式から `linOf` で組み立てる。事実が `derive` と言うのに、その名前が `define` なら、受け付けない。
- **Lean の「訊かれる点」は、`Semantics.lean` を変えずに広がる。** `Semantics.lean` の `asked` は、述語として表に持たせているだけで、中身を決めるのは `Sieve.lean` の篩(制約と線形のモデルの事実を満たす値があること)である。等式は、その事実の列(`Sieve.facts`)に入る。`Sieve.lean` の説明は、等式も事実に入ると書き直した。`lake build` は通り、定理が立つ公理は変わらない。
- **到達を入力で確かめられない行を、W128 で言う。** 計算した列(`derive`、`define`)にセルを書いた行で、規則の例にも、ベクタを作るときの探し方にも、入力の候補の値の組み合わせ(4,096 通りまで)にも、当たる入力が見つからず、どの入力も当たらないことも示せなかった行に、W128「行 N に当たる入力を見つけられず、当たらないことも示せませんでした」を出す。どの列が関わったかを添える。入力と列挙だけの行は、点がそのまま入力なので、この探しをしない。要素ごとの表と、歩きの答えを読む表は、並びを作らないと入力にならないので、この探しの外にした(§15.208 の段で、前の段の表だけを探す)。注は、当たる入力があるなら例に書けば消えること、無いなら行を削除すること、削除して E101 が出るなら、その組み合わせが起きないことも rulec は示せていないので行は残してよいこと、を言う。
- **`coverage` の文を言い分ける。** 行のカバーが欠けたとき、「たいていは生成器のせい」ではなく、「check が W128 で言った行」か、「check は E102 とも W128 とも言っていない。ベクタが入力を見つけられなかった行」かを言う。
- **`ritsu check` に `--budget <n>` を足す。** 正の整数で、rulec の検査(区画とステートマシン)が訪れるノード数と、sekisho の一つの action の組み合わせの数に、同じ数を渡す(`Joined::with_budget`)。既定はそれぞれの言語の既定(rulec は 50,000,000、sekisho は 100,000,000)のままである。正の整数でない値は、そう言って 2 で終わる。E109 と E128 の直し方は、`rulec check` にも `ritsu check` にも `--budget` があると言う。
- **`ritsu dandori scenarios` は、手元の規則の呼び出しを、口で計算する。** 答えはシナリオに書かず、シナリオに `"rules": "rulec"` を書く。`ritsu run` と同じ計算(`computed::rule_answer`)である。規則の答えを変えたいときは、規則の入力の出どころ(タスクの答え)を選ぶ。Connect のサービスとして呼ぶ規則は、これまでどおり答えを書く。dandori のクレートの単体のバイナリは規則を読めない(E005)ので変わらない。くわしくは dandori の DESIGN の 1.19。
**捨てたもの**:
- **A08 を不具合として、到達の点の意味を「入力が作る点」に変えること。** 訊かれる点を入力から定義し直すと、`derive` と `define` の式を全部、証明の側で評価することになり、§6.2 の「列を独立の軸にする」から離れる。線形の等式を足すだけなら、今の意味論のまま、等式で決まる分だけ広がる。
- **等式を、証明書の言葉で運ぶこと。** 再検査が証明書の言う式を信じることになる。式は規則の `values` にあり、そこから作り直せる。
- **W128 を出さずに、`coverage` だけで言うこと。** `check` の `ok` が「どの行にも当たる入力がある」と読まれるのは `check` の出力で、`coverage` を走らせない人には届かない。
- **W128 をエラーにすること。** 当たる入力があるかどうかを、rulec は決められていない。示せないことを誤りとは言えない。
- **`ritsu run` が、規則の答えをシナリオから読むこと。** `ritsu run` の役目は、規則を rulec で計算して流すことである。読めば、規則を確かめたことにならない。
- **計算した答えも、シナリオに書いておくこと。** `ritsu run` はそれを読み飛ばすことになり、二つの所をそろえておくことになる。
- **古い形のシナリオを、読んだ時点で止めること。** 規則を呼ぶ前に終わるシナリオは、古い形でもそのまま流せる。止めるのは、規則の呼び出しに着いた所にした。
**正直な限界**:
- 等式に入るのは、線形の式の `define` だけである。`min`・丸め・かけ算の `define` の関係は入らず、そういう行は今までどおり区間で読み、決められなければ W128 になる。
- W128 の探し方は、入力の候補の値の組み合わせを 4,096 通りまでしか見ない。候補に無い値でしか当たらない行(二つの数の積が 997 になる行)は、当たる入力があっても W128 になる。その入力を例に書けば消える。
- `ritsu dandori scenarios` は、帳簿のタスクと日付の呼び出しの答えを、まだ口で計算しない。帳簿を使うフローのシナリオを `ritsu run` で流すと、帳簿は空から始まり、生成器が選んだ終わり方にならないことがある(ritsu のテストのプロジェクト `invoice` では、英語の版と日本語の版の 30 本ずつが、どれも在庫切れで終わる)。規則と同じ形で口を渡すのは、残した仕事である。
- `ritsu check --budget` が渡すのは、rulec と sekisho だけである。koyomi の検査も入力の組み合わせの予算(`koyomi check --budget`、E305)を持つが、`ritsu check` からは既定のままになる。koyomi の口は予算を持たないので、渡すには口を変えることになる。
**確かめたこと**:
- この表を一般の名前で書いた `tests/reach/E102_past_a_defines_equation.rule` と日本語の版で、行 4 が E102 の三つめの形になり、取りうる値の印と、等式の注が出る(golden を英語と日本語で足した)。範囲の中の 756 通りの入力を参照の評価器で全部流し、行 4 がどれにも当たらないことを見た。行 4 を消した版(`tests/sieve/E101_closed_by_a_defines_equation.rule` と日本語の版)は、0.26.0 では E101 だったが、いまは何も言われずに通り、証明書の被覆は、等式の乗数(`{"fact":0,"y":"1"}`、事実 0 は `{"define":"excess","le":true}`)で箱を閉じる。この証明書は `tools/recheck.py` と Lean の `rulec-recheck` の両方を通り、等式の向きを入れ替えたもの、別の定義の等式を指したもの、定義の等式を導出の等式と呼んだものは、どちらでも落ちる。`define` の式を `added + limit` に変えると、穴に入力が届き、E101 がその入力を挙げる。E102 の行を残した規則は検査を通らないので、証明書を出さない。
- コーパスの 87 本(`define` を持つ 48 本を含む)で、等式を入れる前と後の、検査の結果と、区画の検査が訪れたノードの数を比べた。どれも同じだった(ノードは合わせて 14,198)。
- W128 は、木の `.rule` 377 本のうち、ここで足した材料のほかに 4 本に出た。整数でしか当たらない行(二つの導出の和が 3 で差が 0、`tests/over/parity.rule` と sekisho の `halves.rule`)と、二つの真偽の定義がどちらも偽になる行(`m_w114` と `tests/pages/half_bound.rule` の行 3。どの入力も当たらないが、それを示せず、消すと E101 が求める)である。どれも、当たるかどうかを rulec が決められない行で、前は何も言っていなかった。`m_w114` の変異の固定に W128 を足した。`tests/region.rs` に一本足した(積が 997 になる行が W128 になり、例を書くと消えること、入力の列の行には出ないこと)。`tests/coverage.rs` に一本足した(W128 の行と、ベクタを抜いた届く行で、注が違うこと)。
- `ritsu check --budget`:`crates/ritsu/tests/check.rs` に一本足した。`--help` に旗と例が出ること、台帳の E109 の例の規則が `--budget 1` で `[rulec E109]` になり、旗が無ければ通ること、sekisho の返金の例が `--budget 20` で `[sekisho E307]` になること、`0`・`-3`・`many` を英語と日本語で 2 で止めること、である。
- 規則を呼ぶフロー:`crates/ritsu/tests/projects/ticket_desk`(問い合わせの窓口。出来事ごとに規則が次の段階を決める。英語の版と日本語の版)で、`ritsu dandori scenarios` が書いた 10 本ずつのシナリオを `ritsu run` に通した。どれも終わりまで流れ、シナリオの答えを全部使い、規則の呼び出しは全部 rulec が答え、`ritsu dandori run` と同じ実行になった。終わり方には、解決と、放置・長すぎ・形の合わない答え・失敗の分岐がある。古い形(規則の答えを並びに持ち、`"rules"` の無いもの)は、`ritsu run` が規則の呼び出しで 2 で止め、そう言い、`ritsu dandori run` では同じ実行になる(`crates/ritsu/tests/run.rs`)。
### 15.210 順序のある列挙と比べるセル。生成先の予約語と同じ名前(2026-10-11)
**きっかけ**:会員の段階(一般 < シルバー < ゴールド < プラチナ)のように値に順がある列挙を表で分けるとき、「ゴールドから上」は `ゴールド, プラチナ` と値を並べて書くしかなかった。段階を一つ足すと、並べた行を全部直すことになる。足し忘れても、その値がどこかの行に入っていれば完全性の検査は何も言わない(上の段階の行に入れ忘れても、下の段階の行に入っていれば通る)。順を宣言に一度書き、セルに「金から上」と書ければ、段階を足したときに直すのは宣言だけになる。
同じ時期に、0.26.0 からある問題が見つかった。入力や出力の名前が生成先の予約語と同じ(別名を付けていない `class` など)とき、検査は通るのに、生成した Python や TypeScript が読み込めなかった。W121 は別名しか見ていなかった。
**決定**:
- **値を `<` で区切った列挙は、書いた順に並ぶ**(低い方から)。`enum tier = basic < silver < gold < platinum`。一つの宣言に `<` と `|` を混ぜると E069。
- **比べるセル**(`<v`、`<=v`、`>v`、`>=v`、二つ並べた `>=silver <platinum`)は、順のある列挙の列にだけ書ける。順の無い列挙に書くと E069。検査は、比べるセルを `read_words` の段で `Cell::Ranked` に直し、比べと、それが覆う値の集合を持たせる。区画、完全性、重なり、ベクタ、カバレッジは、覆う値の集合として、列挙の軸の上の箱で読む。だから検査は、これまでの列挙と同じく正確である。
- **式**では、同じ順のある列挙の二つの値の比べ(`campaign > member`)と、`max`・`min`(後の方、前の方)が書ける。真偽の `define` の原子の二つめの形(差を導出できない型の同型の比べ、§5.3)に当たり、列挙ではこれまで等値だけだった比べに、順のある列挙では大小が加わる。列挙の値そのものは式に書けない(E012)。一つの値と比べるのはセルの仕事である。順の無い列挙をこう使うと E069。
- **順のある列挙の入力は、値で範囲を書ける**(`range >=basic <=gold`)。範囲の外の値は、数の範囲と同じく入口で受け付けず、表には範囲の中の値だけを求める(その入力の軸を範囲の中の値に縮める)。ベクタには、範囲のすぐ外の値を受け付けない入力として足す。範囲に入る値が無いとき、要素のフィールドの列挙に範囲を書いたときは E069。ritsu の口は、範囲に入る値の並びを入力の型に付けて渡し、口の評価器も範囲の外の値を受け付けない。`rulec schema`・`rulec api`・`rulec graph`、生成した MCP のツールと注釈とページも、数の範囲と同じく範囲を言い、E122、記録の確かめ(`fixtures lint`・`replay`・記録つきの `diff`)、記録の無い `diff` も範囲で確かめる(§15.251)。この入力を持つ規則を準用する規則は、渡す値が範囲に入ることを E043 で確かめる(§15.252)。
- **生成コードは、比べるセルを順位の比べとして、規則の同じ行の分岐に書く。** Python は `tier_rank`、TypeScript と JavaScript は `tierRank`、Rust は `rank()`(列挙は `PartialOrd`・`Ord` を導く)、Ruby と PHP は順位の関数、Go は宣言の順に並んだ定数そのもの、Swift は `rank`、Java は `ordinal()` と `compareTo`、SQL は `CASE`、NumPy は計画の `rank` のセルである。`max`・`min` は列挙ごとの補助の関数一つずつになり、使う規則にだけ出る。
- **証明書**は、順のある列挙を `ordered` に挙げ、比べるセルを `{"cell":"rank","tests":[{"op":">=","value":"gold"}]}` と書く。範囲を持つ入力は `enum_ranges` にその値を挙げる。どれも、その規則が持つときだけ書くので、ほかの証明書は 1 バイトも変わらない。`tools/recheck.py` は順から箱を組み直し、加えて、列挙の入力の軸が列挙の値(範囲があればその中の値)と一致することを確かめる(列挙の軸は、これまでどちらの再検査も列挙の値と突き合わせていなかった)。Lean は `Cells.lean` の `CellTest.rank` で箱を組み直し、補題 `mem_boxOf_rank_iff`(比べるセルの箱に入る座標は、順の中の位置が比べを全部満たす値の座標)で読み、`Read.lean` は順を証明書の `ordered` と `enums` から取る。`enum_ranges` は Lean の定理が無いので「述べただけ」として名前を挙げる(設計の拡張の 14 章の 10)。
- **生成先の予約語と同じ名前は、どの生成先でも後ろに `_` を付けて書く。** 入力、出力、`derive`・`define`・まとめ、表の出力の列、並びとその要素のフィールドの名前で、別名が無く、名前をそのまま識別子にする生成先(Python、TypeScript、JavaScript、Rust、Go、Ruby、Wasm)のどれかの予約語なら、全部の生成先で `class_` と書く(`codegen::escape_keywords`、`prepare` の最後。§15.260 で、`codegen::assign_identifiers` が、準用の持ち込む名前と一緒に決めるようにした)。別名は、書いた人が選んだ識別子なので、そのままにし、受け付けない生成先は W121 がこれまでどおり言う。`rulec api` はその名前を言う。入力の JSON のキーと記録の名前は宣言した名前のままにする。W121 は、どの生成先の予約語かを言い、別名を付ければ生成コードでの名前を自分で決められると言う。Connect のサービスの Python は、protoc の Python の生成器がその名前に付ける `_` に合わせる。列挙の値と表の名前は付け替えない(値は言語ごとの大文字・小文字の形で書き、表の名前は trace の文字列である)。
**捨てたもの**:
- **順を数で持たせる**(`enum tier = basic(1) …`)。数を付けると、算術ができるように読める。要るのは順だけである。
- **式に列挙の値を書けるようにする**(`member >= gold`)。一つの値との比べはセルで書ける。式の中で列挙の値を読むと、名前と値の読み分け(§15.208)がもう一か所増える。
- **壊れる生成先でだけ名前を付け替える、または止める。** 一つのアプリが二つの言語で同じ規則を呼ぶとき、名前が言語ごとに違うと読みにくく、`rulec api` の読み手も言語ごとに確かめることになる。止めるのは、検査を通った規則から生成できない言語を残すことになる。
- **予約語と同じ名前を E009 にする。** 0.26.0 で通った規則が通らなくなり、互換の約束(compatibility.md、予約語は 1.0 で固定)にも反する。
- **別名も付け替える。** 0.26.0 で動いていた生成コードが変わる。Swift は、予約語の別名(`where`)をバッククォートで書いて、そのままの名前で呼べていた(`tests/shapes.rs`)。
**正直な限界**:
- 比べるセルが比べるのは、その列の列挙の一つの値とだけである。二つの列どうし(`member` と `campaign`)は、`define` で真偽にしてから列にする。
- 付け替えた名前は、名前付きで渡す呼び出しにも届く。0.26.0 の Swift で `fare(class: …)`、PHP で名前付き引数 `class:` と書いていた呼び出しは、`class_` に書き直すことになる(0.26.0 の Python と TypeScript では、そもそも読み込めなかった)。
- Lean は、列挙の入力の軸が列挙の値であることを、まだ確かめない(範囲のある軸は「述べただけ」と言う)。`tools/recheck.py` は確かめる(§15.232 で Lean も確かめるようにした)。
**確かめたこと**:
- `tests/corpus/member_tier_shipping.rule` と日本語の版 `会員段階の送料.rule`(`>=gold` のセル、`max`、二つの段階の比べ、範囲のある入力)。12 の生成先と PostgreSQL の関数で、64 件のベクタと 12 件の受け付けない入力が参照の評価器と一致した(`rulec test --require-all`)。証明書は `tools/recheck.py` と Lean の `rulec-recheck` の両方を通り、比べるセルの値を `gold` から `silver` に書き換えた証明書は、どちらでも落ちる。
- 変異:`m_e069`(段階を `|` で区切る。範囲・`max`・比べ・セルの四か所の E069。セルが指すはずだった値を、W111 が使われていないと重ねて言わないようにした)、`m_e069mixed`(`<` の並びに `|` を一つ混ぜる。E069 が一つ)と、それぞれの日本語の版。
- 予約語と同じ名前:`class`・`type`・`def` を入力にした規則で、W121 が三つ出て、`rulec api` の Python の署名が `def fare(class_: Seat, type_: int, def_: bool | None) -> int:` になり、`rulec test` で全部の生成先が一致した(`tests/enums.rs`)。Connect のサービスも一致した。木の `.rule` 379 本では、新しい形の W121 はほかに出なかった(列挙の値の `void` に一度出たので、値を付け替えの外にした)。
**Rust とモデル検査器**:Rust の生成コードは、順序のある列挙の比べるセルと `max`・`min` を、派生した `PartialOrd` ではなく、宣言の順位(`rank()`)の整数の比較で書く。比べるときの `partial_cmp` の道を、モデル検査器が扱えないからである。補助関数(`_rank_<型>`、`_max_<型>`、`_min_<型>`)は、丸めの補助関数と同じく `pub(crate)` にして、証明のハーネスから見えるようにした。範囲のある順序つきの列挙の入力は、ハーネスでも範囲の中の値だけを取る(入口が範囲の外を拒むのは、数と同じ)。どれも、コーパスの規則を Kani にかけて見つかった(ハーネスがコンパイルできない、答えを返さない入力を作る)。
### 15.211 列挙の値の表示名(2026-10-11)
**きっかけ**:列挙の値の名前は、生成コードの識別子とワイヤの値を兼ねる。だから一語で、記号を含まない。画面や領収書に出す名前(「Rest of the country」「食品・飲料」)は、アプリの側で値ごとに書き足していた。規則で値を足すと、その名前も別の所で足すことになり、足し忘れは画面で初めて見つかる。
**決定**:
- **値の後ろに文字列を書くと、その値の表示名になる**(`national "Rest of the country"`。`default` の印はその後ろ)。空、改行などの制御文字を含むもの、同じ列挙の二つの値で同じものは E069。表示名の無い値は、宣言した名前で示す。
- **表示名が出る所**:承認者向けのページ(値の横)、顧客向けのページ(セルと入力。順のある列挙の比べは「より前」「まで」「より後」「から」)、`rulec api`(値ごとの `label` と、表示名を返す関数の名前)、生成コードの関数かメソッド(Python は `zone_label`、TypeScript と JavaScript は `zoneLabel`、Rust・Swift・Java は `label`、Go は `Label()`、Ruby と PHP は関数、SQL は関数のファイルの `"<規則>_<列挙>_label"(v text)`、NumPy は計画と `rule.label(列挙, 値)`)。表示名を返す関数は、表示名を持つ列挙にだけ出す。
- **ワイヤと記録には載せない。** 値の名前のままである。Wasm の境目は JSON で値しか運ばないので、Wasm は表示名を持たない。
- **どの言語でも、書いたとおりの文字列を返す。** 生成先の文字列が特別に読む文字(`$`、`${…}`、`#{…}`、`(` や `u` の前のバックスラッシュ、`%`、`'`)を、言語ごとの書き方で文字列にする。`rulec gen` は `vectors/<規則>.labels.tsv` と `.labels.expected` を書き、`rulec test` は各言語のランナーに `--labels` で全部の表示名を聞いて比べる。
- **ページでは、表示名を文字として出す。** Markdown が別の意味に読む文字(強調、コード、リンク、タグ、実体参照、取り消し線、数式に読まれるドル記号)の前にバックスラッシュを書く。HTML のページへの変換も、バックスラッシュの後の記号を文字として読み、表の一行は、バックスラッシュの付いた `|` では切らない。
**捨てたもの**:
- **表示名を言語ごとに複数持つ。** 規則の文字列は、規則を書く言葉で一つ書く。訳はアプリの側の仕事である。
- **表示名をワイヤに載せる。** 値の名前と二つになり、表示名を直しただけで記録の比べが変わる。
- **表示名の関数を、全部の列挙に出す。** 表示名を持たない列挙では、宣言した名前を返すだけの関数が増える。
**正直な限界**:
- 文字列にエスケープが無いので、表示名に `"` は書けない。
- 表示名は、完全性などの検査の対象ではない。
**確かめたこと**:
- `tests/corpus/plan_receipt.rule` と日本語の版 `プランの領収書.rule`:表示名に `$`、`${district}`、`#{campus}`、`\(term)`、`\users`、`%`、`'`、`{}`、`<>`、`|` を含めた。Python・NumPy・TypeScript・JavaScript・Rust・Ruby・PHP・Go・Swift・Java のランナーと、PostgreSQL の関数が、どれも書いたとおりに返した(`rulec test`、`tests/enums.rs`)。承認者向けのページは Markdown と HTML のどちらでも文字のまま出し、顧客向けのページの表は `|` で切れない。
### 15.212 規則どうしが分け合う列挙:`enums` のファイルと `import enum`(2026-10-11)
**きっかけ**:二つの規則が同じ列挙(商品の種類、配達の地域)を使うと、規則ごとに宣言を書き、生成コードでは規則ごとに別の型になった(`catalog_shipping.Category` と `catalog_returns.Category`)。静的な型で書くアプリは、二つを同じ値として渡せない。値を足すときも、全部の宣言を直すことになる。JSON Schema や `.proto` に置いて `import` すれば値のずれは E032 で止まるが、宣言は規則ごとに書き、型は分かれたままだった。
**決定**:
- **見出しが `enums <名前>(<別名>) v<版>` のファイルは、列挙だけを持つ。** 書けるのは `description`・`import`・`enum`・`group` で、ほかの行(表、入力、`import enum` など)は E066。何も決めず、関数を生成しない。
- **規則は `import enum <列挙> from "<ファイル>" [-> <名前>]` で読む。** 値、順、表示名、`default` の印と、その列挙のグループが来て、規則の側では書き直さない。パスは規則のディレクトリからたどり、毎回の `check` で読み、ハッシュで固定しない(`import proto` と同じく、パスが二つを結ぶ。値を足したら、読む全部の規則で検査が走るのが目的である)。読めないファイルと、そのファイルに無い列挙は E013。`enums` でないファイルと、検査を通らない `enums` のファイルは E066。読んだ列挙やグループが、規則の宣言するものや別の `import enum` が読んだものと同じ名前なら E034(表、節、出典の名前と同じ扱い)。読んだ列挙の値とグループは、規則が使わなくても W111 にしない。分け合うファイルの値は、ほかの規則のためにある。
- **生成**:`enums` のファイルは、言語ごとに型だけのモジュールになる(Python、TypeScript、JavaScript、Rust、Ruby と `.rbs`、PHP、Go、Swift、Java)。読む規則のモジュールは、型をそこから読み、自分では宣言しない。NumPy、SQL、Wasm は列挙を値で持つので、モジュールを作らない。`rulec gen <規則>` は、読んだ `enums` のファイルが渡されていなくても、そのモジュールを同じ出力に書く。何本の規則が読んでも一度だけ書き、二つの別のファイルが同じモジュールを作るなら止める。
- **言語ごとの読み方**:Python は `rulec gen` の出力で `from catalog import Category`(絶対)、`ritsu gen` のパッケージで `from .catalog import Category`(相対)。`rulec test` のランナーは出力のディレクトリで走るので絶対で読め、パッケージはその中から読む。TypeScript は `./catalog.ts`(パッケージでは拡張子なし)から読み、型を `export` し直す。JavaScript は `./catalog.mjs`。Rust は `pub use crate::catalog::{…};` で、ランナーと証明の crate は `#[path]` でモジュールを並べる。Go は持ち主のパッケージを import し、`type Category = catalog.Category` と定数と `ParseCategory` を同じ名前で置く(`rulec gen` では規則の `go.mod` の `replace`、`ritsu gen` では `--module` のパス)。Ruby は `require_relative` と定数の別名、`.rbs` も同じ別名。PHP は `require_once` と `use`。Swift は同じモジュールで、規則と一緒に組む(`rulec gen` が `vectors/<規則>.owners.json` を書き、`rulec test` が `swiftc` に足す)。Java は同じパッケージの `Catalog` の中の enum(`Catalog.Category`)。
- **`rulec api`**:`enums` のファイルには関数が無いので、`rule` の代わりに `enums` を持ち、言語ごとのモジュールと、その中の列挙を言う。規則の `api` の列挙は、読んだものなら `from`(ファイル)と `module` を持つ。
- **何も決めないファイルに意味の無いコマンドは、理由を言って 2 で止める**:`schema`、`adapter`、`verify`、`replay`、`fixtures lint`、`diff`、`doc` の HTML と顧客向け。`check`、`fmt`、`doc`、`api`、`certificate`(表の無い証明書で、二つの再検査を通る)、`vectors` と `coverage`(0 件)、`graph` はそのまま動く。
- **ritsu**:`ritsu check` と `ritsu gen` は `enums` のファイルを規則と同じに扱い、パッケージでは `rules/` にモジュールとして置く。rulec の references の口は、`import enum` の行を、そのファイルの列挙への参照として返す。
- **`open` の印**:列挙の行の、最後の値の後の `open` を読んで持つ。外から値を足す仕組みはこの版には無く、意味は寄与のファイルの節が決める(§15.228 で、寄与が `extend` で値を足せるようにした)。
- **`enums` と `open` は予約語にしない。** `enums` はファイルの一行目、`open` は列挙の行の最後の値の後でだけ読み、どちらも名前の立つ位置ではない。予約語の一覧は 1.0 で固定する約束で(compatibility.md、`tests/compat.rs`)、足すと、その語を名前にした規則が E009 になる。値の名前の `open` も、その位置のほかでは値として読む。
**捨てたもの**:
- **出力のある規則を、列挙の持ち主にする。** 列挙が一つの判断に縛られ、その規則を直すたびに、読むだけの規則も巻き込む。
- **JSON Schema を分け合う持ち主にする。** 値を規則ごとに書き直すことが残り、型も分かれたままである。
- **`enums` のファイルをハッシュで固定する。** 値を足したことが、読む規則の検査に届かなくなる。
- **Wasm にも型のモジュールを分ける。** Wasm は規則ごとに一つの crate で、境目は JSON である。型は中に書く。
- **`enums` のファイルを渡されたときだけモジュールを書く。** 渡し忘れると、規則のモジュールの import が宙に浮く。
**正直な限界**:
- `enums` のファイルが、別の `enums` のファイルの列挙を `import enum` で読むことはできない(E066)。
- `diff` は `enums` のファイルを比べない。値を足した影響は、読む規則の `check` が言う。
- 列挙の名前が量の型の名前(`area` など)と同じとき、0.26.0 からある読み違い(列が量の型として読まれて E103)が、`-> area` で読んだときにも起きる。
- 型を付けた Python の部分集合をコンパイルする処理系で、`rulec gen` の出力(並んだモジュール)を読むアプリを調べると、分け合う列挙のモジュールと、そこからの import には何も言われなかった(止められたのは、生成コードにもともとある六種類の書き方だけ)。`ritsu gen` のパッケージの中の import は、その処理系がまだたどらない。
**確かめたこと**:
- `tests/corpus/shared/catalog.rule` を `catalog_shipping.rule` と `catalog_returns.rule` が、日本語の版 `shared/品目.rule` を `品目の送料.rule` と `品目の返品.rule` が読む。12 の生成先と PostgreSQL の関数で、34 件と 12 件のベクタ、11 件と 9 件の受け付けない入力が参照の評価器と一致した。`enums` のファイルを `tests/corpus` の下のディレクトリに置いたので、コーパスを一つずつ見る試験は、それを読む規則だけを見る。
- 一つのアプリが、二つの規則に同じ値を渡して答えを得て、`mypy --strict` を通る(`tests/enums.rs`、英語と日本語の版)。`ritsu gen` のパッケージでも、Python は同じ値で二つの規則を呼び、TypeScript は `tsc --strict`、Go は `go build` と `go vet` を通った。
- 変異:`m_e066`(`enums` のファイルに表)、`m_e013enum`(無いファイル)と、それぞれの日本語の版。名前の重なり(E034)、`-> <名前>` での読み替え、ファイルに無い列挙(E013)、何も決めないファイルに意味の無いコマンドは、`tests/enums.rs` が確かめる。
### 15.213 丸めの中で値で割る。行のセルが狭める列の範囲と、行を除いた穴から決める W128(2026-10-11)
**きっかけ**:平均(合計 ÷ 件数)、単価、利用率、月の日数で割る日割りは、どれも値で割る。対戦の勝ち負けを決める規則でも、攻める強さと守る強さの比、重みの割合が同じ形である。これまでは、割る数が定数でなければ E115 で止めた。割る数の刻みが静的に決まらないと、生成コードが言語の割り算に頼ることになり、Python は −∞ へ、Go は 0 へ丸めて答えが食い違うからである(§2.3、§7.4)。止めること自体は正しいが、業務でよく書く形が書けなかった。
**決定**:
- **五つの丸めの関数の、第一引数のいちばん外側の割り算に限って、値で割れる。** `half_up(合計 / max(件数, 1), 1円)`、`half_even(attack * 1000 / (attack + defense), 1)`。一つの丸めに一つである。`down(料金 * (使った日数 / 月の日数), 1円)` のように、いちばん外側でない割り算は E115 のまま。ほかの場所も E115 のままで、その文に「値で割れるのは丸めの引数のいちばん外側の割り算だけ」というヒントを足した(割る数が定数でないときだけ)。割る数が数のリテラルなら、これまでの定数の割り算である。
- **型は §2.3 の割り算の規則。** 同じ単位どうしは単位の無い比で、刻みが数なら number、率なら rate になる(`half_up(a / b, 0.1%)` は rate)。number か率で割れば元の型のまま。刻みは商の単位で書く(合わなければ E103)。
- **E132「割る数が 0 になりえます」。** 割る数の区間が 0 を含めば止める。区間は、入力の範囲、導出と `define` の範囲、上の表が書く値から求める。範囲の決まらない値を読む割る数も、0 にならないとは言えないので E132 である。注の先頭に、割る数を 0 にする入力を添える(`lib::enrich_arith`。読む入力が少なければ全部の組み合わせを、多ければ範囲の端と 0 を試し、`constraint` を満たす入力だけを使う)。直し方は三つ:範囲を狭める、`max(件数, 1)` で割る、0 のときに別の値を選ぶ表を置いてその出力で割る。割る数は負でもよい。0 を含まない区間なら、どちらか一方にある。
- **答えの区間は、四隅の商の外側を、刻みで外へ丸めたもの。** 計算した列として表の列に使える(§15.195)。
- **E108。** 生成コードは、割られる数と割る数をそれぞれの刻みから丸めの刻みにそろえた二つの整数(`types::division_factors`)を割る。二つとも int64 に収まることを区間で示す。
- **評価器は、正確な有理数で割り、書いた丸め方で刻みに乗せる。** 負の値は丸め方の定義どおり(§7.3、0 からの向きで対称)。
- **12 の生成先は、丸め方ごとにヘルパを一つ持つ。** Python・Ruby・PHP は `_div_round_half(n, d)`、Go と Java は `divRoundHalf`、TypeScript・JavaScript・Swift は `_divRoundHalf`、Rust は `quot_half`(`divround_half` は既存の名前とぶつかった)。割る数が負なら両方の符号を変えてから割る。使う丸め方のヘルパだけを書く。SQL は ABS と `%` と CASE で同じ計算をし、割る数を `NULLIF(…, 0)` で包む。入口が拒む行で割る数が 0 になっても、問い合わせ全体が止まらず、その行が NULL になる。NumPy は int64 の配列の `floor_divide` と余りで、Wasm は Rust のヘルパを外へ出す。丸めのヘルパの単体のベクタ(`_round_test.*`)に、割られる数 17 個と割る数 9 個の組(負の分子、負の分母、ちょうど半分、割り切れる場合、int64 の両端)を、五つの丸め方ごとに足した。12 の言語で同じ答えになることを `rulec test` が確かめる。単体のベクタのファイルは、同じディレクトリに書き出した規則のすべてが使い、ヘルパのコピーを自分で持つ。はじめは規則が使うヘルパの分だけを書いたので、最後に書き出した規則が、ほかの規則のヘルパを何で確かめるかを決めていた(`mod` を使う規則のあとに使わない規則を書き出すと、`mod` のベクタが消えた)。いまは、どの規則でも全部のヘルパとそのベクタを書く(`Uses::every`)。Go の単体のベクタだけはパッケージごとに書き、そのパッケージが持つヘルパの分を書く。
- **証明書と再検査。** 値の式では、丸めの呼び出しの中に `/` の節点がある(`{"call":"half_up","args":[{"op":"/",…},…]}`)。`tools/recheck.py` は四隅で区間を組み直し、割る数の区間が 0 を含めば区間を出さない。Lean の `Values.lean` に、値で割る丸めの節点 `divRound` を足し、型の規則、評価、区間の規則(`div_box`:正か負の区間で割るときの四隅)と、`eval_mem_interval` の場合を証明した。
**行のセルが狭める、列の範囲**:E132 の直し方の三つめ、「0 のときに別の値を選ぶ表」は、そのままでは効かなかった。`| 0 | 1 |` と `| >=1 | 件数 |` の二行の表が書く列の範囲を、行が書く名前の範囲(件数の 0 から 1000)の和で求めていたので、列は 0 を含み、その列で割ると E132 になった。行が自分の表の列をそのまま書くなら、書く値はその行のセルがその列に通す値だけである。そこで列の範囲を、行ごとに、書く名前の範囲をその行のセルで狭めた区間の和にした(`Checked::row_written`。`>` と `<` の端は、その列の格納の刻みで内へ寄せる)。列の範囲が変わるときだけ、その列を狭めたと覚え(`Checked::narrowed`)、証明書の行の `writes` に `inside`(その行が書く値の区間)を書く。範囲が変わらない規則の証明書は、一字も変わらない。`tools/recheck.py` は、行の `tests` と、列の値が乗る刻み(`column_grid`)から区間を組み直し、それで列の範囲を求め、`inside` がそれを含むことを確かめる。Lean のプログラムには、行のセルを行が書く値に結ぶ定理がまだ無い。`inside` のある列の範囲は証明書の言うとおりに取り、終わりの行で「the range of per: a row's own cell narrows what it writes there」と、述べただけのものとして名を挙げる。
**W128 を、行を除いた穴から決める**:§15.209 の W128 は、計算した列にセルを書いた行に当たる入力が、例にも、ベクタの探し方にも、候補の値の組み合わせにも見つからず、当たらないことも示せないときに出る。ところが、完全性に要る行は、その行を除くと穴が開く。穴の入力はほかのどの行にも当たらないので、その行に当たるはずである。そこで W128 の候補の行ごとに、その行を除いて完全性を確かめ(`region::input_by_gap`)、穴が見つかれば、その穴の入力を参照の評価器に通して、その行が当たることを確かめてから、行の入力として使う。ベクタもその入力を取る(理由は「the input behind the gap … closes」)。予算の中で穴が見つからないか、穴の入力がその行に当たらなければ、W128 のまま。手間は、W128 の候補の行の数だけ完全性を一度ずつ確かめることである。`mod` と `isqrt` は、消去が逆に解けない計算した列を作るので、この探しが効く。`tail = mod(ticket * 3 + 1, 1000)` の列に `777` の行を書いた表では、行を除いた穴の入力 `ticket = 592` がその行に当たり、W128 は出ない。`tests/mutants/m_w114` と `tests/pages/half_bound.rule` の行 3 は、この探しでも W128 のままである。行 3 を除くと出る E101 の穴は、二つの真偽の定義がどちらも偽になる組み合わせで、どの入力もそれを作らない(倍にした額が 5 円より小さく、かつ大きい)。行 3 にも、どの入力も当たらない。W128 のままが正しい。W128 の台帳の文と、`coverage` の行の注を合わせた。
**捨てたもの**:
- **丸めの外でも割れるようにして、出力の丸めに任せること。** 途中の値の刻みが実行時の値で決まり、軸を刻みで切れない。E115 の理由がそのまま残る。
- **割り算に刻みだけを書くこと(`(a / b) step 0.1%`)。** 刻みだけでは、端数をどちらへ寄せるかが決まらない。
- **E132 で `constraint` を読むこと。** `constraint a < b` のもとでの `b - a` は 0 にならないが、それを示すには、区間ではなく消去で割る数の符号を示し、その証明を証明書に運び、二つの再検査で確かめることになる。いまは区間だけで決め、文に `max(b - a, 1)` と書く直し方を挙げた(その制約のもとでは同じ値である)。
- **0 のときに別の値を選ぶ表の代わりに、`define` を表の行で遅らせて評価すること。** `define` は表の前に一度評価するので、0 の行が読まない `define` でも割り算は走る。評価の順を行で変えると、生成コードの形が言語ごとに変わる。列の範囲を行のセルで狭めるほうが、どの言語の生成コードも変えずに済む。
- **`inside` を、範囲が変わらない行にも書くこと。** 書けば正しいが、範囲が変わらない規則の証明書まで変わり、Lean のプログラムが「述べただけ」と言う規則が増える。
**正直な限界**:
- 列の範囲を狭めるのは、行が自分の表の列の名前をそのまま書くときだけである。別の列のセルで間接に狭まる値(`| 件数 >=1 | … | 件数 + 1 |` のような関係)は、名前のセルしか書けない(§3.2)ので起きないが、`define` の値をその `define` が読む入力のセルで狭めることもしない。
- Lean の側では、`inside` のある列の範囲は述べただけである。行のセルの意味(`mem_boxOf_cmp_iff`)と、行が書く値を結ぶ定理は、ext-lean の担当に残した(§15.232 で証明した)。
- 行を除いた穴の探しは、表がほかにも穴を持つときは、別の穴を先に見つけ、その入力がこの行に当たらなければ W128 のままになる。その表には、もともと E101 が出ている。
**確かめたこと**:
- コーパスに、平均単価の二つの版(`max(units, 1)` で割る `average_unit_price.rule` と、割る数を表で選ぶ `average_unit_price_split.rule`)と日割り(`daily_proration.rule`)を、英語と日本語の版で足した(`twins.tsv` に三組)。平均単価の二つの版は、`1001円 ÷ 2` が `half_up` で 501円になる例を持つ。表で選ぶ版の証明書は、列 `per` の範囲を `["1","1000"]` と言い、行 2 の `writes` に `"inside":["1","1000"]` を持つ。`tools/recheck.py` はこれを通し、`inside` を `["2","1000"]` に狭めたもの、列の範囲を `["2","1000"]` に狭めたもの、`inside` を消したものを、どれも落とす。Lean の `rulec-recheck` は、範囲を述べただけとして名を挙げて通し、`inside` を消したものは落とす。
- 変異を英語と日本語の対で足した:割る数から `max` を外した E132(`m_e132`、注は「An input that makes the divisor zero: units = 0」)と、日割りの月のうちの割合を先に括弧で割った E115(`m_e115div`)。文面の golden を両方の言語で足した。
- `tests/arith.rs`:五つの丸め方で、割られる数と割る数の 9 組(負、ちょうど半分、割り切れる場合)の商を、正と負の割る数の範囲の両方で評価器に求め、手で書いた答えと比べる。`attack * 1000 / (attack + defense)` の E132 の注が `attack = 0, defense = 0` を挙げること、割る数がずっと負なら通ること、いちばん外側でない割り算と丸めの外の割り算が E115 で、その注が値で割れる場所を言うこと、刻みが商の型を決めること、E108 も確かめる。
- 表で選ぶ版の行のセルを `-` にして `policy first` にすると、列は 0 を含み、割り算は E132 になる。
- 12 の言語と PostgreSQL:`rulec test --require-all` で、足したコーパスの規則と、丸めのヘルパの単体のベクタ(12 の言語)が、どれも参照の評価器と一致した(数は §15.216 の最後にまとめた)。生成する Python は `pyright --strict` で、足したヘルパの行に新しい指摘が無い(出るのは、どの規則にもあるもの、使わない `_min`・`_round_*` と `enum`・`NewType` の読み込みだけ)。生成する Ruby は、新しいヘルパを `.rbs` に宣言していなかったので `steep` が型の誤りにした。宣言を足し、通るようにした。
- 崩した規則(`tests/robustness.rs` と同じ崩し方)を、足した 14 本で 1,519 通り作って `check` に通した。割る数の範囲を逆さまに書いた 4 通り(`range >=28 <=0` など)で、区間の割り算が 0 の端で `Rat::div` に入って止まった。区間の割り算は、割る数の両端が 0 の同じ側にあるときだけにした(`tools/recheck.py` も同じ)。直したあとは、1,519 通りとも止まらずに答える。
- 行を除いた穴:`tail = mod(ticket * 3 + 1, 1000)` の `777` の行は W128 にならず、ベクタが `ticket = 592` でその行に当たり、行のカバーが満たされる(`tests/arith.rs`)。`m_w114` の行 3 は W128 のまま(変異の固定は変わらない)。
### 15.214 余り `mod`(2026-10-11)
**きっかけ**:曜日や当番のように、通し番号をある数で割った余りで決まるものがある。ターンごとに回る時刻も、`modulo(現在 + ターン − 始めのターン, 時刻の数)` の形である。余りが無いので、書けなかった。
**決定**:
- **`mod(<number の式>, <正の整数の定数>)`。** 答えは 0 から定数 − 1 まで。負の数を割っても負にならない、床の余りである(`mod(-1, 3)` は 2)。引数は number だけを取り、お金、量、率、日付は E103、割り算で端数の出る値も E103(「`mod` が取るのは整数だけです」、先に `down(x / 2, 1)` のように丸める)。割る数が正の整数の定数でなければ E115 で、文は `mod` の割る数のことを言う。
- **区間。** 引数の区間が一つの周期の中にあれば、その区間を周期の倍数だけずらしたもの。そうでなければ 0 から定数 − 1 まで。計算した列として表の列に使える。
- **E108。** 引数の格納の整数と、同じ刻みにした割る数が int64 に収まること。
- **予約語にしない。** `mod` と `isqrt` は、呼び出しを書いた所でだけ関数として読む。名前は呼び出せないので、そこに名前は来ない。予約語は 1.0 の 65 語で固定する(§15.157、`tests/compat.rs`)ので、足さない。入力を `mod` と名づけた規則も、`mod(mod + isqrt, 3)` と書けて、そのまま通る。
- **12 の生成先。** Python・Ruby・NumPy は言語の `%`(正の除数では床の余り)をヘルパ `_mod` で、Rust は `rem_euclid`(`mod_floor`)、Java は `Math.floorMod`、Go・TypeScript・JavaScript・Swift・PHP は `((x % n) + n) % n` を `modFloor`・`_mod` で、SQL は CASE で負を直した `%` で書く。単体のベクタに、負の数と 0 と int64 の両端の余りを足した。
- **ページ**:式は書いたとおりに出す。
- **証明書と Lean。** 値の式の呼び出し `{"call":"mod",…}`。再検査は区間を同じ規則で組み直す。`Values.lean` に `modR` と、答えが 0 から n − 1 にある補題(`modR_bounds`)、一つの周期の中ならずらした区間に入る補題(`modR_same`)、`eval_mem_interval` の場合を足した。
**捨てたもの**:`%` の演算子(`7%` の率の書き方とぶつかる)。値で割った余り(§15.213 と同じ理由で刻みが決まらず、言語によって負の値の扱いが違う)。
**正直な限界**:余りの列に書いた行の境目の対(`coverage` の境目の対)は、消去が余りを逆に解けないので、引数の入力が少ないときだけ、全部を試して見つける。入力の範囲が広い規則では、境目の対が欠けたまま `coverage` がそう言うことがある(行のカバーは §15.213 の穴の探しで満たす)。
**確かめたこと**:
- コーパスに、当番の持ち回り(`duty_rotation.rule`、三つの班を `mod(week - first_week, 3)` で決める)を英語と日本語の版で足した。例は、回し始める前の週(`9` と `10`、`1` と `53`)を含む。ベクタは `week = 1, first_week = 2` のように負の引数の余りを含み、12 の言語の答えが評価器と一致した。
- 変異:割る数を入力 `teams` にした E115(`m_e115mod`、英語と日本語)。golden を両方の言語で足した。
- `tests/arith.rs`:`mod(x, 3)` の値(`-7` は 2、`-1` は 2、`100` は 1)、引数の範囲が一つの周期に入るとき(13 から 17)と入らないとき(13 から 23)の表の完全性、割る数が入力・0・負・小数のときの E115、お金の引数の E103。
### 15.215 整数の平方根 `isqrt`(2026-10-11)
**きっかけ**:経済的発注量(√(2 × 年間の需要 × 一回の発注の費用 ÷ 一個の保管の費用))や、人口が増えるのに要る量の `floor((人口 − 1)^1.5)` は、平方根が無いと、人口の値ごとに表へ展開するしかなかった。
**決定**:
- **`isqrt(<number の式>)`。** 二乗がその値を超えない最大の整数。分数なら、整数の部分の平方根と同じである。number だけを取る(お金、量、率、日付は E103)。引数の区間が負を含めば E139「`isqrt` の引数が負になりえます」で、注に引数を負にする入力を添える。直し方は、範囲を狭めるか `isqrt(max(x, 0))`。冪は掛け算で書く(`isqrt(n * n * n)` が n の 1.5 乗の切り捨て)。小数の桁が要るなら `isqrt(x * 10000)` で百分の一を数える。
- **区間は、両端の平方根。** 単調だからである。
- **12 の生成先は、言語の関数を使わず、同じ二分探索のヘルパ `_isqrt` を書く**(Go は `isqrtInt`、Java は `isqrtLong`、Rust は `isqrt_i64`)。上の端は `3_037_000_499`(二乗が int64 に収まる最大の整数)。Python の `math.isqrt` を使わないので、生成する Python は `enum` と `typing` のほかを読まないままである。SQL は一つの式に探索が入らないので、関数 `rulec_isqrt` を呼ぶ。関数の形のファイル(`_function.sql`)の先頭に PL/pgSQL で定義し、問い合わせのファイルの頭に、PostgreSQL では先にそれを作ることを書く。SQLite で問い合わせを確かめるランナーは、同じ探索を同じ名前で登録する(`LEAST` と同じ)。NumPy は int64 の配列で同じ二分探索を回す(`rulec_np._isqrt`)。単体のベクタに、平方数とその前後、int64 の上の方の値を足した。
- **証明書と Lean。** 値の式の呼び出し `{"call":"isqrt",…}`。`Values.lean` に `isqrtR`(core の `Nat.sqrt` を使う)と単調性の補題(`isqrtR_monotone`)、`eval_mem_interval` の場合を足した。
**捨てたもの**:率を返す `sqrt`(刻みが決まらない)。有理数の指数の `pow`。
**正直な限界**:`mod` と同じく、根の列に書いた行の境目の対は、入力が少ないときだけ全部を試して見つける。
**確かめたこと**:
- コーパスに、経済的発注量(`economic_order_quantity.rule`。根の中を `down(2 * 年間の需要 * 発注の費用 / 保管の費用, 1)` で整数にしてから `isqrt` に渡す)を英語と日本語の版で足した。例は、`1000, 5000円, 100円` で 316、`100万, 10万円, 1円` で 447213(根の中が 2,000 億)。
- 変異:根の中から年間の需要を引いた E139(`m_e139`、注は「yearly_demand = 1, order_cost = 1, holding_cost = 100000」)。golden を両方の言語で足した。
- `tests/arith.rs`:0 から 10^12 までの平方数とその前後、負になりうる引数の E139 と `max(a - b, 0)` で消えること、お金の引数の E103。
### 15.216 `constraint` の片側の和(2026-10-11)
**きっかけ**:「使った量 + 申し込む量 <= 上限」「値引き + ポイント <= 小計」「乱数 < 挑む側の強さ + 守る側の強さ」は、二つの入力の関係ではなく、和と一つの入力の関係である。`constraint` は入力どうしの関係しか書けなかったので、書けなかった。書けないと、和が上限を超えない組み合わせにも行を求められ、片方の入力は範囲を確かめられるだけになって W111 が出た。
**決定**:
- **片側を、`+` でつないだ入力の和にできる。** `constraint used + requested <= quota`、`constraint roll < challenger_strength + holder_strength`。両側とも和(E017)、和の中の定数や係数(E017、`a + 3 <= b`、`2 * a <= b`)、入力でない名前(E018)は書けない。和の入力は、もう片側と同じ型(E103)で、日付は足さない(E048)。E017 の文に、和を書けることを足した。`ast::Constraint` は、両側を名前の並びで持つ(`left: Vec<String>`、`right: Vec<String>`)。
- **ふるい。** 区画の各入力の区間の和で、成り立たないことが示せた区画を落とす(`region::settle_sums`)。和も一つの線形の不等式として、Fourier–Motzkin の消去に入る(`fourier::constraint_form`)。`quota_request` の表は、残りが負の行を持たないが、和の制約から完全である。
- **例とベクタ。** 例を作る入力とベクタの入力は、制約を満たす点を探して作り、最後に全部の制約に当てる(`vectors::constraint_holds`)。和を破る例は E019。受け付けない入力のファイルには、範囲の中で和を破る組み合わせを書く(§15.204)。
- **入口。** 12 の言語の入口が、和を破る入力を拒む。値は、和に入る入力の刻みをそろえてから足す。そろえた刻みで和が int64 に収まることを、区間で示す(E108)。
- **契約。** 入力を読む契約(§15.140)は、規則が前提にする和を約束しなければならない(E123)。契約が三つのフィールドのうち二つの関係しか言わないと、和を破る要求が通る。E123 は和に入るフィールドを全部挙げ(英語では、三つ以上をコンマと and でつなぐ)、破る要求の例と、和を約束する CEL(`this.used + this.requested <= this.quota`)を直し方に書く。
- **`rulec api` と ritsu の口。** `preconditions` の `constraint` では、和の側が名前のリストになる(`"left":["used","requested"]`)。ritsu の口は `Precondition::SumRelation` を持つ。ritsu-cross の X2 は、和の入力をそれぞれの範囲の端(関係をいちばん厳しく試す側)に置いて、和をもう片側と比べる。成り立たなければ E201 が、和の入力のそれぞれの範囲と、破る値を挙げる。範囲の分からない値があれば W201 である。dandori は、ritsu が決められなかった和の前提を、生成するワークフローのコードで確かめる(Temporal の TypeScript・Python・Go、Step Functions、durable、Argo、pydantic-graph)。Go は gofmt の書き方(`ddNum(a)+ddNum(b) <= ddNum(c)`)で書く。シナリオを作るとき、前提が成り立つ実行では、規則がその範囲で受け付ける値を先に選ぶ(和では、最後に作った値を動かすと負になることがある)。
- **証明書と Lean。** `constraints` の和の側は名前のリストになる。`tools/recheck.py` と Lean の `Sieve.lean` は、和の区間(`sideSum`、`sideSpan`、`side_bounds`)と、線形のモデルの事実としての和を組み直す。Lean の定理はそろっている。
**捨てたもの**:係数と定数を持つ一般の線形の制約。読む人がそのまま読めて、いま要る形は和だけだった。
**正直な限界**:ritsu-model(ritsu-cross の判断を Lean のモデルと突き合わせる試験)は、二つの入力の関係だけを突き合わせ、和の X2 の判断はまだ突き合わせていない。
**確かめたこと**:
- コーパスに、割り当ての申し込み(`quota_request.rule`、残りが負の行を持たず、和の制約で完全)と、乱数で決める一回(`contested_roll.rule`、`roll < challenger_strength + holder_strength`)を、英語と日本語の版で足した。
- `tests/arith.rs`:和を書ける形と書けない形(両側の和、和の中の定数の E017、入力でない名前の E018)。制約を外すと E101 になり、その例の入力(`quota = 1, requested = 1, used = 1`)が和を破ること。和を破る例が E019 になること。ベクタが和を守り、受け付けない入力のファイルに範囲の中で和を破る組み合わせがあること。`contested_roll` から制約を外すと W111 が戻ること。`rulec api` の `preconditions` に和の側がリストで出ること。和を約束しない契約が E123 になり、三つのフィールドと CEL を挙げること。証明書では、二つの再検査が、和の制約に頼る被覆の葉を確かめ、和から入力を一つ落とした証明書を落とす。
- ritsu:`ritsu check` の X2 に、和の前提の三つ(保たれる、E201、W201)を足した(`crates/ritsu/tests/cross.rs`、golden を英語と日本語で)。E201 は「`used` is `>=0 <=10000`, `asked` is `>=0 <=10000`, `quota` is `>=0 <=10000`, and at used = 10000, requested = 10000, quota = 0 `used + requested <= quota` does not hold」と言う。
- dandori:`tests/flows/quota.flow`(和の前提を持つ規則を、範囲の無いタスクの答えで呼ぶ)を足した。参照の解釈器と同じ実行になることを、Step Functions(手元の JSONata の実行)、durable、pydantic-graph、Temporal の TypeScript・Python・Go で、10 本ずつ確かめた(Go は gofmt の書き方に直してから)。生成した TypeScript は `tsc --strict`、Go は `go vet` と gofmt を通る。`ritsu dandori build` が七つの生成先で dandori の書くものと同じになる(`crates/ritsu/tests/dandori.rs`)。LocalStack と Argo では走らせていない(手元の Docker が動いていなかった)。
- まとめ(2026-10-11 の実行):足したコーパスの 14 本は、`rulec test --require-all` で、24 の生成先(12 の言語に、MCP・Connect・WASI・PostgreSQL の関数の入口を足したもの)で参照の評価器と一致した。丸めのヘルパの単体のベクタも 12 の言語で一致し、合わせて 348 とも一致した。`tests/threeway.rs` は、コーパスの 101 本、ベクタ 9,580 件、受け付けない入力 1,462 件で 12 の言語が一致し、`tsc --strict` は 303 のファイルを、`steep` は生成した Ruby を通した。コーパスの証明書は全部、`tools/recheck.py` と Lean の `rulec-recheck` を通る。直す前(38c706d4)と後で、既存の 379 本の `.rule` の `check`(英語・日本語・JSON)・`certificate`・`vectors`・`coverage`・`doc` を比べると、変わったのは三つだけだった。`m_e017` の E017 の注(和を書けること)、`m_e115` の E115 のヒント(値で割れる場所)、W128 の行を持つ 6 本の `coverage` の注(行を除いた穴の探し)である。
### 15.217 状態のほかに、範囲のある数と真偽と列挙を持ち越す(2026-10-11)
**きっかけ**:§15.148 の機械は、一つの列挙だけを状態として持ち越す。数を持ち越さないのは、足し算で持ち越すとカウンタになり、届くかどうかが一般には決まらないからだった(§15.148 の 5)。けれども業務の手続きには、状態と一緒に動く値がある。引き落としを試した回数、猶予の残り日数、無料体験を使ったかの印である。いまの書き方では、呼ぶ側がその値を持って入力で渡し、規則は一回ぶんだけを決める。主張は列挙の状態しか見ないので、「猶予のあいだは残りが 1 日以上」も「あきらめたときは三回試している」も、検査の外に残る。印を状態に混ぜれば、状態は印の組み合わせの数だけ増え、それを手で書くことになる。数の範囲に両端があれば、案件のとりうる値は有限個で、主張は決まるはずである。どう書き、どう調べ、値を一つずつ調べきれない大きさではどうするか。
**決定**:
1. **二行目からの `carry` で、状態のほかの値を持ち越す。** 持ち越せるのは、範囲の両端を宣言した数(`number`、金額、量、率)、真偽、別の列挙である。入力と出力は同じ型にする(E051)。どの値にも、`initial <入力> <値>` で始まりの値を書く。始まりの値が無い、二つある、型や範囲に合わない、状態や持ち越さない入力を書いた、のどれも E072 にした。一語の `initial`・`final`・`never` は、これまでどおり最初の `carry`(状態)について言う。行は、持ち越す値をほかの出力と同じに書く(値、入力そのもの、計算した名前)。
2. **数え下げの専用の書き方は足さない。** 残りを一つ減らすのは、`define one_less : number = days_left - 1` を行に書けば済む。専用の書き方を足すと、残りが 0 のときにどうなるかを黙って決めてしまう。書かせれば、そこに行が要る。下の「走らせた」の売り手と市場の関係の規則で、残り 0 で猶予が続く行が W126 になったのは、その行が書いてあったからである。
3. **案件のいる場所は、状態と持ち越す値の組にする。** `final`、`never`、`once`、終われること、着かない状態(W125)、使われない行(W126)は、組の上でこれまでと同じ意味になる。主張は二つ増やした。
- **`always <入力> <セル> [and …] [when <状態>, …]`**:`when` の後の状態にいるあいだ(`when` が無ければどの状態でも)、持ち越す値がセルに当てはまる。破れれば E135 で、いちばん短い呼び出しの並びを付ける。`always` に書けるのは持ち越す値だけで、状態そのものを書けば E072 にした。状態について言うことは `never` と `final` で言える。
- **持ち越す数の次の値は、入力の範囲に収まる。** 案件が着ける場所からの呼び出しが範囲の外を答えれば E134 にした。次の呼び出しにその値を渡せないので、案件はそこで止まる。範囲を出るのが、どの案件も持たない値からだけなら、W126 に「その行が要る値では着かない」と注記する。範囲を出た場所は E134 だけが言い、`always`(E135)と終われること(E125)は、その場所を重ねて言わない。三つ出しても、直す所は一つだからである。
4. **小さければ値を一つずつ、大きければ区間で調べる。** 持ち越す数の値を全部、区画の座標にしても区画の数が予算(`--budget`、区画一つに 50)に収まるなら、そう切って歩く。区画の一つずつが案件のいられる場所の一つになり、主張の結果は正確である。収まらなければ、数を、表のセルの境目、行が出力に書く値、`always` のセルの境目、始まりの値で区間に切る。区間からの呼び出しの行き先は、次の値の式を区間の上で計算し、重なる区間の全部に辺を引く。実際より多い辺の上で、悪い場所に着かないという主張(`final` から出ない、`never`、`once`、`always`、範囲)が成り立てば、本当にも成り立つ。
5. **破れる並びは、本当に起きる呼び出しで作り直す。** 区間の上で主張が破れたら、始まりの値から規則を本当に呼んでたどり(区画で見た入力と、区画の両端の入力で)、同じ破れを探す。見つかればそれを反例にする(E124、E126、E127、E134、E135)。どの呼び出しも、規則が受け付けて答える入力である。見つからなければ W127 にして、どこまでたどったかを注記に書く。たどるのは、2 万の場所か 20 万の呼び出しまでである。終われること(着けるどこからでも終わりの状態に行けること)は、多い辺の上では示せないので、区間で調べたときは W127 にする。値を一つずつ調べたときも、次の値が区画の中でほかの入力とともに動く(`balance + amount`)なら、区画の両端と見た入力の答えしか試していない。それに掛かる破れも、作り直せなければ W127 になる。
6. **生成する関数は変えない。** 持ち越す値も引数で受け、次の値を返し、何も覚えない。足したのは始まりの値の定数で、ほかの面と合わせて §15.218 に書く。
**走らせた**:
- コーパスに、書き下ろしの規則を三つ、英語の版と日本語の版で足した(六本)。引き落としを試した回数(`retry_attempts.rule`、`再試行の回数.rule`)、猶予の残り日数(`grace_period.rule`、`猶予の残り日数.rule`)、一度だけの特典の印(`one_time_perk.rule`、`一度だけの特典.rule`)である。どれも値を一つずつ調べる大きさで、主張は全部成り立つ。猶予の規則のページの「状態ごとに持ち越す値」は、`active` が 0、`in_grace` が 1..7、`suspended` が 0 になった。
- 試した回数の規則には、はじめ `always attempts <=3` を書いた。入力の範囲が 0〜3 なので、これは E134 と同じことを言う(範囲を出れば、E134 が先に言う)。`always attempts >=1 when collected` と `always attempts 3 when given_up` に替えた。
- 状態と一緒に数や印を持つ二つの規則で確かめた(`tests/machine.rs`)。一つは、売り手と市場の関係の規則(`seller_standing`。六つの状態と 0〜16 の残り日数で 102 の場所)である。値を一つずつ調べると、残り 0 で猶予が続く行(行 15)が W126 になり、注記は「猶予には着くが、この行が要る残り日数では着かない。着くのは 1..16」と言う。猶予はいつも 16 日から始まるからである。`always days_left 0 when dispute` を足すと、三回の呼び出しで破れる。
```
error[E135]: A sequence of calls breaks `always days_left 0 when dispute`
Calls (from fresh):
1. at fresh (days_left = 0), event = first_sale → dispute (next_days_left = 0, table step row 1)
2. at dispute (days_left = 0), event = warn → grace (next_days_left = 16, table step row 3)
3. at grace (days_left = 16), event = revoke → dispute (next_days_left = 16, table step row 10)
At the end of this sequence the case is in dispute with days_left = 16. The line says the value fits its cell whenever a case is in dispute.
```
猶予を取り消して争いに戻すとき、表は残りを 16 にしたまま戻す。数を持ち越すと、こういう「意味の無くなった残り」が呼び出しの並びとして出る。
- もう一つは、二つの印を真偽で持ち越すキャンペーンの規則(`campaign`)である。`once reaction welcome` と `once reaction reminder` が成り立つ。印を見ずに訪問のたびに歓迎する行にすると、E127 が二回の呼び出しで出る。`always welcomed false and reminded false when running` は、一回目の訪問で破れる(E135)。
- 大きな範囲:0〜10,000,000 から 2 ずつ減らす規則(`even_count`)は、値を一つずつでは予算を超えるので、区間で調べた。`never`、`always left >=0`、範囲の主張は、区間の上で成り立つ。`always left not: 1` は区間の上では破れるが、偶数から 2 ずつ減らしても 1 にはならないので、本当に起きる呼び出しでは作り直せず、W127 になる(「20001 の場所までたどった」と注記する)。デバッグのビルドで 0.92 秒、137 MB だった。
- 変異を英語と日本語の対で四つ足した(E072、E134、E135、W127)。三回目の拒否でもあきらめずに数え続けた変異は、四回目の呼び出しで E134 になる。はじめは同じ場所について、E134 と一緒に E135(`always attempts <=3`)と E125(範囲を出た案件が終われない)も出ていた。上の 3 のとおり、範囲を出た場所は E134 だけが言うようにした。
- 金額(範囲は −5,000〜100,000 円で、始まりは −5,000 円)、率、別の列挙、真偽を一度に持ち越す規則(`account`)を `rulec test --require-all` に通した。ベクタ 89 件、受け付けない入力 27 件、手順の中の呼び出し 36 件が、12 の言語と、MCP(stdio と HTTP)、Connect(ASGI と WSGI、GET も)、WASI、PostgreSQL の関数の全部で一致した。この規則は値を一つずつでは予算を超え、区間で調べるので、終われることは W127 になる。
**直した欠陥**:
- 終わりの状態の無い機械(`final` を書かない機械)では、生成した Ruby が `FINAL = [].freeze` になり、`steep` が要素の型の無い空の配列を警告して、自分の `.rbs` での検査が通らなかった。一度だけの特典の規則を足して見つかった。空のときは `Array.new(0).freeze` を書く。
**見つけて直していないもの**:
- 区間で調べたときの終われることは、W127 のままである。多い辺の上の道を、本当に起きる呼び出しで作り直す手立てが要る。
- 偶数の規則のように、区間では表せない性質(偶奇)に掛かる主張は、本当に起きる呼び出しを 2 万の場所までたどっても決まらず、W127 が残る。
- 区間で調べた数のページの範囲は、数の区切りを入れずに書く(`0..10000000`)。
**捨てたもの**:
- **範囲の無い数を持ち越すこと。** §15.148 の 5 のとおり、届くかが決まらない。
- **日付、文字列、省略できる値を持ち越すこと。** 前の呼び出しの日を持ち越すのは暦を持つことで、「14 日以内」は経過日数を入力で受ける(§15.148 の 5)。文字列は有限にならず、省略できる値は「値が無い」ことを持ち越す意味が決まらない。
- **数え下げの書き方。** 上の 2 のとおり。
- **区間の上で破れた並びを、そのまま反例にすること。** 実際より多い辺の上の並びは、呼べない並びでありうる。反例は、どの呼び出しも規則が受け付けて答える入力でなければならない(§15.148 の 6)。
- **範囲を出た値を範囲の端に丸めて続けること。** 案件は先へ進めないのに、進むように見える。
### 15.218 持ち越す値を、生成物・手順のベクタ・網羅・ページ・`diff`・証明書に載せる(2026-10-11)
**きっかけ**:§15.217 の値は呼ぶ側が持って渡すので、生成する関数の形は変わらない。けれども、どの値から始めるかは呼ぶ側に伝えなければならない。手順のベクタは、持ち越す全部の値を渡して比べなければ、受け渡しの誤りを見逃す。ページ、網羅、記録を使う `replay` と `diff`、証明書も、状態だけを見ていた。
**決定**:
1. **始まりの値を定数にする。** モジュールを持つ九つの言語に、持ち越す値ごとに一つ足した。名前は言語の慣習に合わせ、Python・TypeScript・JavaScript・Rust・Ruby・PHP・Java は `INITIAL_DAYS_LEFT`、Go は `InitialDaysLeft`、Swift は `initialDaysLeft` にした。型は入力の型で、金額なら金額の型である(Python は `INITIAL_BALANCE: JPY = JPY(-5000)`)。Ruby の `.rbs` にも宣言を足した。`INITIAL` はこれまでどおり状態の始まりである。NumPy の計画は `machine.more` にデータとして持つ。SQL と Wasm は入口が一つなので定数を置かず(§15.148 の 8)、`rulec api` の `machine.carried` が、持ち越す値ごとの入力と出力、型、始まりの値(`initial`、ワイヤの形)、言語ごとの名前(`constants`)を言う。`machine.always` は `always` の行を言う。持ち越す値の無い機械には、どちらの鍵も無い。
2. **手順のベクタは、持ち越す全部の値を渡す。** 手順のファイルの一行目に `more`(入力と出力の組)を足し、`"step":"start"` に `carried`(始まりの値)を付けた。各言語の走らせ役は、前の呼び出しがその言語の値のまま答えた状態と値を、次の呼び出しに渡す。並びは、本当に起きる呼び出し(§15.217 の 5)から作り、遷移の一つずつと、続けて起きる二つずつに、いちばん短い並びを当てる。`scenario` には、これまでどおり持ち越す値の列を書かない(E055)。一行目は始まりの値から、二行目からは一つ前の答えから始まる。
3. **網羅の六つ目の基準は、持ち越す値を含む場所の上で数える。** 二つの遷移が続くのは、一つ目が答えた値で二つ目が呼ばれるときだけである。区間で調べた規則で、本当に起きる呼び出しが着かない遷移は、そう注記する。
4. **ページ。** 状態の図は列挙の状態で描き、状態ごとに持ち越す値の範囲を添える(mermaid の注記、SVG、「状態ごとに持ち越す値」の表)。区間で調べたときは、範囲が実際より広いことがあると書く。持ち越す値の表(入力と出力、型、範囲、始まり)、`always` の行とその結果、範囲の主張を、検査が示したことに載せる。HTML の「続ける」ボタンは、答えた状態と一緒に持ち越す値もフォームに戻す。顧客向けの記事にも一文を足した。
5. **`graph`。** `carry_more` に、持ち越す値の組を `carry` と同じ形で足した。持ち越しは、これまでどおり辺にしない。
6. **記録を使う `replay` と `diff`。** 同じ `tag` の記録を一つの案件として通し直すとき、その版が答えた持ち越す値も次の記録に渡す。記録が別の値を書いていれば、そこで案件が分かれたと言う。
7. **証明書。** `machine.carried` に、持ち越す値ごとの座標(値を一つずつ調べたなら全部の値、区間なら区間)、行ごとにその値へ何を書くか(値、そのまま、計算した名前)、案件がいられる場所(状態と座標の組)の閉じた集合、主張ごとの閉じた集合を載せる。行き先は行の箱から読む。実際の呼び出しより広く、悪い場所に着かない主張には安全な向きである。計算した名前の行き先は、`values` の式をその場所の区間で計算したもので、Rust と Python は同じ区間の算術を使う。`policy first` の表では、場所の全部を一つ上の行がそれだけで覆う行を外す。終われることは載せない(多い辺では示せない。§15.233 で、値を一つずつ調べた機械には本当に起きる呼び出しの並びを載せた)。`tools/recheck.py` は座標の並び、行の書くものとファイルのセル、場所の集合が閉じていること、主張を全部確かめる。Lean の `rulec-recheck` は節を読み、主張を「述べただけ」として挙げる。持ち越す値の主張の Lean の定理は、まだ無い。
**走らせた**:
- 猶予の規則の証明書は、Python の再検査で「carried values over step: 9 places a case can be, 6 claims laid on the rows」になり、Lean の再検査は「the claims over the values the machine carries besides its state (9 places): stated, not proved」と言う。偽った五通り(場所を一つ落とす、行が書く値、`always` の境、始まりの場所、範囲)は、Python の再検査が受け付けない。試した回数の規則は 7 の場所と 6 の主張、特典の規則は 5 の場所と 4 の主張である。
- コーパスの新しい六本を、それぞれ `rulec test --require-all` に通した。どれも 36 の組み合わせ(12 の言語、MCP、Connect、WASI、PostgreSQL の関数、丸めの部品)で一致した。試した回数の規則は、ベクタ 16 件、受け付けない入力 11 件、手順の中の呼び出し 16 件である。
- コーパスの 93 本で、参照の評価器と 12 の言語が、ベクタ 9,384 件と受け付けない入力 1,338 件で一致した(`tests/threeway.rs`)。生成した Python は mypy の strict、TypeScript は tsc の strict、Ruby は自分の `.rbs` での steep を通る。
- 前からあるコーパスの 87 本で、替える前のビルドと比べた。`gen`(全部の言語の生成物とベクタと手順)、証明書、`api`、`graph`、`doc`(Markdown と HTML)、`check` と `coverage` の JSON は、一字も変わらない。状態だけを持ち越す機械(`注文の状態.rule` と英語の版)も同じである。
**見つけて直していないもの**:
- 区間で調べたときの模倣の定理(多い辺の上で閉じた集合は、本当の呼び出しでも閉じている)と、持ち越す値の主張の Lean の定理。それまでは「述べただけ」である(§15.232 で証明した)。
- 区間で調べた規則の手順のベクタは、本当に起きる呼び出しをたどれた所までしか作らない。
### 15.219 一つの規則に並びと集計を何本でも書き、歩きの順を読むものから決める(2026-10-14)
**きっかけ**:規則がたどれる並びは一つで(E020)、fold は一つ、fold と `count`・`sum` は一緒に書けず(E031)、要素の値と歩きの答えを一つの項目で読むこともできなかった(E031、§15.208)。そのため、手で書けばループを一つ足すだけの決まりが書けなかった。注文の明細と支払いの二つの並び(明細の合計から支払い済みの額を引く)。単価がいちばん高い明細だけに値引きを当てること(いちばん高い単価は明細を全部見るまで決まらないので、明細をもう一度たどる)。一つの歩きで、最初に当たった要素を採る fold と、全部を数える `count` を並べること。
**決定**:
1. **`elements` を何本でも書ける。** 名前は要る(無ければ E020)。どの行も `over` で指さない並びも E020 にした:「並び xs をたどるものがありません」。たどらない並びのフィールドを読む項目は、どの要素の値を読むのかが決まらない(前の版は、たどる行の無い規則で要素のフィールドを読む `define` を、何も言わずに通していた)。入力・並びの名前・要素のフィールドは一つの名前空間で、二つの並びのフィールドどうしも、同じ名前なら E034 にした。
2. **`count`・`sum`・`max`・`min`・`fold` を何本でも書ける。** どれも、たどる並びを `over` で言う。`max` と `min` は §15.220。
3. **fold の答えに名前を付ける**:`fold <判定> over <並び> -> <名前> [: <型>]`。出力の名前なら、答えはその出力で、型は出力の型である。新しい名前なら型を書く(`-> any_flagged : bool`)。`->` が無ければ、これまでどおり最初の出力に答える。腕の値は、この型で読む(§15.208 の「最初の出力の型で読む」を広げた)。E021 にするのは、答えの名前が入力・要素のフィールド・上で宣言した値と同じとき、出力でない名前に型が無いとき、出力の名前に違う型を書いたとき、fold とほかの項目が同じ名前を決めるとき(「`subtotal` を決めるところが二つあります」)である。何も読まない新しい名前の答えは W111 にした。
4. **歩きの順は宣言せず、読むものから決める**(`types::stages`)。§15.208 の三つの段を、歩きの番号に広げた。`Stage::Once(k)` は要素によらない項目で、k はそれが読む集計の答えのうちいちばん後の歩き(読まなければ 0、歩く前)である。`Stage::Each { seq, walk }` は並び seq の要素ごとの項目で、読む集計の答えが全部そろった次の歩きに入る。集計はその列を求める歩きで終わり、答えはその歩きのあとの一つの値になる。一回目の歩きの集計を読む要素ごとの項目は、二回目の歩きに入る。決め方は不動点で、名前の歩きは増えるだけ、歩きの数は集計の数で上が決まるので、並びの長さによらず止まる。それでも、回す回数に上限を置いた(項目の数と集計の数から決まる。検査が止まることは rulec の芯である)。
5. **同じ名前を二つの項目が決めるときは、先の項目がその名前の歩きを決める。** fold の答えと `sum` が同じ名前を決める規則で、二つが交互に自分の歩きを書き込み、`check` が戻らなかった(作るあいだに見つけた)。止まったうえで E021 が言う。
6. **自分の答えを待つ fold は E021 にする。** 判定が、その fold の答えから計算する値を読むと、答えを出す前に答えが要る。歩きが集計の数を越えたところで止め、「畳み込みが、自分の答えから計算する review を読んでいます」と言う。答えを使う判定は、別の fold にすれば二回目の歩きで読める。
7. **`stop` は、その fold の読み方だけを終える。** 同じ歩きのほかの集計は、全部の要素を見る。生成コードは、一つの歩きに fold が一つだけなら、これまでどおり `break` でループを抜ける。ほかの集計と同じ歩きなら、その fold の腕を飛ばす印(`done`)を立て、ループは続ける。これで、前の E031 の理由(止まった歩きの合計が何を意味するか決まっていない)は無くなる。
8. **E031 の意味を替える。** 一つの項目(表・節・導出・定義・畳み込み)が二つの並びの要素を読むときである。片方の並びの要素ごとにもう片方を最初からたどることになり(並びの中で並びをたどる)、歩きの数が並びの長さにつれて増え、検査が終わらない。要素ごとの値の出力と、`result` の行が要素の値を読むことは、これまでどおり E031 である。§15.56 で E025 にしたのと同じく、番号はいまある条件を指すように意味を替えた(1.0 の前。`compatibility.md` の 6 に書いた)。E020 も、「二本目の `elements`」から「たどるものの無い並び」に替わった。
9. **後ろの項目が読む fold の答えは、範囲を持つ。** 答えうる値(`empty`、`held` でない `exhausted`、`stop with`、取った値と持ち越した値。`held` はそのどれか)の区間の外側である。`define balance = subtotal - discount - paid` は、前は `discount` に区間が無く、E108 の確かめが黙って飛んでいた(`overflow` は区間が無いと何もしない)。読まれない答え(最初の出力に答えて何も読まない、これまでの fold)は範囲を持たないので、その証明書は変わらない。範囲は証明書の `fold_answers` に載り、再検査が確かめる(§15.220)。
10. **入口は並びごと。** `count` の上限は、その並びを数える `count` の宣言のうち最小である。`sum` が宣言の範囲を越えたら、参照評価器も答えを出さない。前は評価器が答え、生成コードは入口で拒んでいて、二つ以上の集計を持つ規則のベクタで食い違った。
**捨てたもの**:
- **段を宣言させること**(`pass 2` のような書き方)。§15.58 が読むものから決めたことを崩す。宣言させれば、宣言と中身の食い違いを別に確かめることになる。
- **fold に値を貯める腕**(`acc + x`)。カウンタになり、歩きが有限のオートマトンだという説明(§15.56)が崩れる。数は `sum` で足りる。
- **並びの中で並びをたどること。** 区画の算術に収まらず、歩きの数が並びの長さで増える。
- **平均の集計。** 丸めの中の割り算(§15.213)で `half_up(total / max(count, 1), 1円)` と書ける。
- **E031 を引退させて新しい番号を振ること。** 前の条件はもう起きないので、番号を残しても指すものが無い。
- **同じ名前を決める二つの項目の歩きを、毎回書き換えて合わせること。** 合わないまま回り続けた。
**変わること**:
- E020 と E031 の意味(上の 8)。これまで E031 で止まっていた形(fold と `count` が一つの規則にある、要素の値と歩きの答えを一つの項目で読む)が通る。前者は、fold の答えが表の決める出力と重なれば E021 になる。
- E005 の注記の、行頭に書ける語の一覧に `max`・`min` が入る(この注記を持つ変異は四本。サイトの語の表も足す)。
- fold の答えを後ろの表が読む規則(`tests/stages/best_offer.rule` と日本語の版)は、答えに範囲 0〜1100 が付き、MCP の出力のスキーマ、HTML のページ、proto の注記、証明書の `ranges` と `fold_answers`、`api`、`graph` が変わる。生成するコードとベクタは変わらない。
- `rulec doc` の集計の節(§15.221)。
- `rulec diff` は、集計の行と並びのフィールドも比べる。前は `count` の `where` の値を変えても「答えを決めるものは何も変わっていない」と言った(`supplier_matching` で `where kind = same` を `diff` にすると、前の版は何も変わらないと言い、直したあとは 10 の区画のうち 8 で答えが違うと言う)。二つ以上の並び、`max`・`min`、二回目の歩きのある規則は、区画に分けて比べない(§15.220)。
**正直な限界**:
- 二回目の歩きは、要素の値を一回目と同じに計算し直し、入口の確かめもやり直す。生成コードは要素の値を歩きのあいだで持ち越さない。記録の trace には、二回目の歩きが当てた行が要素ごとに並ぶ。
- SQL と NumPy は、これまでどおり並びを歩く規則を書かない(§15.56)。
### 15.220 最大と最小の集計。証明書が述べる、計算した範囲(2026-10-14)
**きっかけ**:正の増分のうちいちばん大きなものと、負の増分のうちいちばん小さなものを一回目の歩きで出し、二回目の歩きで使う決まり。未完了の作業のうちいちばん早い期日。どちらも fold の `keep_max` では書けなかった。`keep_max` が持つ値は一つで、合計と同じ規則に置けなかった(§15.219 の前)。
**決定**:
1. **`sum` と同じ形の `max` と `min`**:`max <名前>(<別名>) over <並び> of <列> [where <列> [= <値>]] empty <値> [range …]`。答えは、`where` を満たす要素の列のうち最大(最小)で、一つも無ければ `empty` の値である。同じ値の要素が二つあっても、答えは値なので決まる。どの要素かが要るときは fold の `keep_max` を使う。
2. **列は要素ごとの数・お金・量・率・日付・順序つきの列挙。** そうでなければ E029 で、題は「この列はまとめられません」にした(数え上げの題「この列は数えられません」は `count` と `sum` のまま)。`where` は `count` と同じに読み、真偽の列なら `= <値>` は要らない。
3. **`empty` は必ず書く**(E022)。並びが空のときと、`where` に当てはまる要素が無いときの答えで、業務の判断である。
4. **範囲は書かなくてよい。** 列の範囲と `empty` の値の外側を計算する(`types::extreme_decl`)。書けば、それを含むこと(E112。含む範囲を直しとして書く)で、含むなら書いた範囲が全体になる(宣言した範囲と同じ)。列挙の `max`・`min` には範囲を書けない(E069)。
5. **生成**:答えを `empty` の値で始め、`<名前>_set` の印を持つ。`where` を通った最初の要素が置き換え、それからは大きい(小さい)ときだけ置き換える。順序つきの列挙は `_rank_<型>` で比べる(Rust は `rank()`)。
6. **証明書**:`max`・`min` の名前は `walks` に入り(軸の `kind` は `walk`)、範囲は `ranges` にある。`count`・`sum` の範囲は書いた範囲だが、`max`・`min` と、後ろの項目が読む fold の答え(§15.219 の 9)の範囲は rulec が計算したものなので、再検査が確かめられるように二つの項目を足した。`extremes` は `max`・`min` ごとに、名前、種類、並び、列、`where` の列と値、`empty` の値(`ranges` と同じ目盛り。日付は日の番号、列挙は語)を持つ。`fold_answers` は答えごとに、名前、並び、答えうる値の式を持つ。どちらも、その形がある規則にだけ書くので、ほかの証明書は一字も変わらない。`carried_json` と、その再検査(`check_carried`)には触れていない。
7. **`tools/recheck.py`**(`check_extremes`、`check_fold_answers`):`max`・`min` の範囲が、列の範囲(書いた範囲か、要素ごとの値なら式から計算した区間)と `empty` の値を含むことを確かめる。列挙なら `empty` がその値の一つで、範囲を言っていないこと。fold の答えの範囲が、答えうる値の式の区間を全部含むこと。
8. **Lean の `rulec-recheck`** は二つの項目を読み、範囲を「述べただけ」として名前を挙げる(「the range of top_price, the max of price or its `empty` value」「the range of discount, which the fold over lines answers」)。定理はまだ無い(後の担当に回した。§15.233 で証明した)。
9. **`rulec diff`**:二つ以上の並び、`max`・`min`、二回目の歩きのある規則は、区画に分けて比べない。区画は、集計がその座標に来る並びを作って確かめるもので、作れるのは一つの並びの `count` と `sum`(要素を足すと増えていく)だけである。`max` は要素を足しても増えず、二つ目の並びは作るものが一つ増え、二回目の歩きは一回目の答えを読む。答えを決めるもの(行、式、並びとフィールド、集計の行)が何も変わっていなければ「同じ」と言い、そうでなければ理由を言って止める。
**捨てたもの**:
- **範囲を必ず書かせること。** 列の範囲と `empty` から計算でき、書かせれば、書き間違いを E112 で拾う仕事が増えるだけである。
- **`empty` を省いたら列の最小値(最大値)にすること。** 業務の判断を道具が決めることになる。
- **証明書の `ranges` に計算した範囲を黙って載せたままにすること。** 再検査は `ranges` を宣言として信じるので、計算の誤りが証明書を通ってしまう。
### 15.221 二回目の歩きを、評価器・ベクタ・網羅・ページ・文書に載せる(2026-10-14)
**決定**:
1. **評価器**(`eval::walk_all`):歩きの番号と並びの順に、その歩きの集計と、その歩きに入る要素ごとの項目を走らせる。二回目の歩きは、一回目までの答えと、要素ごとに計算し直した値の上で走る。止まった fold は印で読むのをやめ、ほかの集計は続ける。要素ごとの場合(`ElementCase`:入力、その歩きまでの答え、要素の値)を、網羅のために集める。
2. **ベクタ**:これまでの形(並びが一つ、歩きが一回、fold だけか、`count`・`sum` だけ)は、前の作り方(`fold_pool`・`count_pool`)をそのまま使う。前からある規則のベクタは一字も変わらない。ほかの形は `walk_pool` で作る。並びごとに、要素の候補を組み合わせた要素、境界の値、空の並び、上限の一つ先である。二回目の歩きには `second_walk_cases` を足した。二つの要素の並びで、一回目の答えがどちらの要素にもよるようにし、数を一刻み、二刻み動かした組も入れる。二回目の歩きの表の境界には、一回目の答えがその境界に来る並びで届く。生成器と網羅の監査は、互いを見ずに規則だけからこれを作る。
3. **網羅**:基準の数は増やさない。fold の遷移は fold ごとに数える。二回目の歩きの fold の判定の順序は、`second_walk_cases` が届くものに絞った(いちばん高い単価の行が二つ続く順序のように、一回目の答えから届かない順序がある)。二回目の歩きの表の境界・隠れ・値の対は、要素ごとの場合で判定する。
4. **ページと文書**:並びごとに「順に見ていく並び」の節を書く。「並びをたどって求める値」(前は「数え上げ」)の表で、集計ごとに一文で何を求めるかを言い(「明細 の 単価 の最大。無ければ 0USD」)、歩きが二つ以上なら、歩きの順を一文で言う(「明細 を一回目にたどって 小計、最高単価 を求め、支払い を一回目にたどって 支払済 を求め、明細 を二回目にたどって、前の歩きで求めた値を読みながら 値引き を求めます」)。表ごとの列の「出どころ」は、数え上げ・合計・最大・最小・畳み込みの答えを言い分ける(前は合計も「数え上げ」と書き、合計の説明は数え上げの文だった)。HTML の試す欄は並びごとに編集の表を持ち、例を選べば両方を組み直す。並びが一つか無い規則のスクリプトは一字も変えず、二つ以上のときだけ、一つの表の部分を並びごとの表に差し替える。
5. **呼び出し側に見えるもの**:要素の型の名前は、並びが一つなら `Element` のまま、二つ以上なら並びの別名のパスカル表記に `Element` を付ける(`LinesElement`、`PaymentsElement`)。引数は宣言の順に並ぶ。`rulec api` の connect は、二つ以上のとき `sequences` に並びごとのメッセージとフィールドを挙げる(`element_fields` は一つ目のもの)。ritsu のポートの `RuleFacts.elements` と `Connect.elements` は並びのリストになった(dandori と sekisho の読み手も合わせた)。
**正直な限界**:
- 二回目の歩きの網羅は、二つの要素の並びで作る。三つ以上の要素で初めて起きる組み合わせはベクタでは探さないので、要るなら例に書く。
- 二回目の歩きの fold の順序で、`second_walk_cases` が届かないものは、義務にも未達にもしない。届かないことを証明してはいない。
- Kani の証明のハーネスは、`count` の上限までの長さの並びを全部調べる(§15.95 のまま)。要素ごとに表が当たる歩きが二つあると、上限とともに時間が急に伸びる。料率の調整は、上限 5 で 105 秒だったが、10 では 10 分を過ぎても終わらず、CBMC が 12 GB を使った。コーパスの規則は上限を 5 にした。
**確かめたこと**(§15.219〜§15.221):
- コーパスに六本を足した(英語の三本と、その日本語の版)。注文の精算は二つの並びと二回目の歩き、最も早い期日は日付の `min` と `count`、料率の調整は一回目の歩きの `max`・`min` を二回目の歩きの表が読む形である。どれも `rulec test --require-all` で 31 の組み合わせ(SQL と NumPy を除く十の言語と、MCP、Connect、WASI、丸めの部品)が一致した。ベクタは、注文の精算が 11 件と受け付けない入力 23 件、最も早い期日が 16 件と 23 件、料率の調整が 18 件と 14 件である。網羅の七つの基準を全部満たす。料率の調整の境界の対は 14 件あり、二つの要素の並びのベクタを抜くと、監査が二回目の歩きの表の境界の対が残ると言う(`tests/walks.rs`)。
- 一つの歩きに二つの fold と `count` を置き、順序つきの列挙の `max` と、fold の答えを読む表を持つ規則(`tests/walks.rs` の `MIXED`)は、ベクタ 22 件で fold の遷移 20 件を満たし、全部の言語で参照の評価器と一致した。
- 前からある規則(`tests/corpus` の 117 本と `tests/stages` の 6 本)を、替える前のビルドと比べた。`gen` の生成コードとベクタは一字も変わらない。変わったのは、HTML のページ(`count`・`sum` を持つ七本の集計の節)、fold の答えを表が読む `tests/stages` の二本の MCP のスキーマと proto の注記とページ、`tests/stages/parcel_charge.rule` と日本語の版の Go(下の gofmt)である。`api`・証明書・`coverage`・`doc`・`graph` の 615 回の実行で違ったのは 13 回で、`doc` の七本と、その二本の `api`・証明書・`graph` である。木の `.rule` 437 本の `check --format json` で違ったのは 7 本で、E005 の注記の語の一覧(四本)と、意味を替えた E031 の変異の三本である。
- 生成した Go は `gofmt -l` に何も言われない。足した規則で、二つのことが見つかった。歩きのループの中で続けて書く注記の付いた行(`subtotal += line_total // subtotal` のような)をそろえていなかった。替える前から、`tests/stages/parcel_charge.rule` の Go は `gofmt` に直される形だった(コーパスに無かったので、`tests/threeway.rs` が見ていなかった)。もう一つは、`gofmt` が演算子の空白を落とす深さで、二つ以上の引数の呼び出しを演算子の項に置いた形(`(roundHalfDown((line_total * 10), 100) / 100)`)を、前の決め方(`tighten_nested_products`)は読めなかった。注記は、そろえる印(`CELL`)を歩きの行にも付け、そろえる範囲を同じ字下げの行に限った(`gofmt` もそうする)。空白は、`go/printer` の深さの規則(引数が二つ以上の呼び出しと、ほかの優先度の演算子の項で一つ深くなり、括弧で一つ浅くなる)を式から読んで決める(`gofmt_spacing`)。読めない形は前の決め方に残す。コーパスと `tests/` の下の全部の規則の Go が `gofmt -l` を通り、コーパスの Go は一字も変わらない。
- 証明書:コーパスと木の規則の証明書は、`tools/recheck.py` と Lean の `rulec-recheck` を通る(`tests/cert.rs`、`tests/lean.rs`)。偽った三通り(`max` の範囲を狭める、`empty` の値を動かす、fold の答えの範囲を狭める)は、Python の再検査が受け付けない。
- Kani:新しい六本の生成した Rust の証明のハーネスは、どれも検査を通った(kani 0.68.0。注文の精算は 2 秒と 3 秒、最も早い期日は 40 秒と 67 秒、料率の調整は 105 秒と 106 秒)。二つの並びの規則のハーネスは、並びごとに要素の配列を作って関数に渡す。
- `cargo test -p rulec`(`RITSU_TEST_LEVEL=tools`、PostgreSQL を立てて):999 件が通った。落ちた 10 件は、件数と文書のコピーとサイトを実物に合わせる試験(`codes.md`、サイトの行頭の語の表と色付けの語、README とサイトの件数、スキルのコピー、Kani の記録)で、合わせる作業は別に行う。SKIP は 2 件で、どちらも platforms の段の試験(ネットワーク、Kani)である。足した試験は `tests/walks.rs` の 13 本で、`tests/count.rs`・`tests/fold.rs` の前の E031 と E020 の試験は、新しい意味に書き直した。
### 15.222 列挙の値どうしの前提のつながり:`prerequisites`(2026-10-14)
**きっかけ**:科目の履修、資格の段階、製品のモジュールの依存、機能フラグ、技術の研究のように、ある値を取る前に別の値を持っていなければならない決まりがある。これまでは、前提を表の行(値と前提の二つの列挙の列と、真偽の出力)に開いて書くか、別の言語の認可の役割(sekisho の `includes`)に読み替えるしかなかった。表に開くと「どれか一つあればよい前提」が書きにくく、「前提を全部満たしているか」は呼ぶ側が書き直すことになる。役割に読み替えると、循環と名前の誤りは捕まるが、それは別の言語の意味で、最初の状態からたどり着けるか、余分な前提が無いか、学年や時代の順と食い違わないかは、どこも確かめていなかった。値の数は有限で、前提のつながりは有限のグラフなので、これらは全部決まる。
**決定**:
1. **書き方。** `prerequisites <名前>(<別名>) over <列挙> [open]` の塊の下に、`start <値>, …`、値ごとに一行の `<値> requires <前提>, …`(`,` で区切ったグループは全部が要り、グループの中の `or` はどれか一つでよい。`a, b or c` は「a と、b か c」)、`<値> requires nothing "<理由>"`、`ordered by <表>` を書く。列挙のどの値も、`start` か自分の行のどちらか一か所にだけ書く。`start` の行は必ず書く。値を足して前提を書き忘れたら、何も要らない値として通らずに E071 で止まる。塊は規則にも `enums` のファイルにも置け、`ordered by` は表を読むので規則にだけ書ける。塊の名前は生成する関数の名前になるので、ASCII でなければ別名が要り(E011)、一つのファイルの二つの塊は別の名前にする(E034)。`open` は読んで持つだけで、何も変えない。何を開くかは、ほかのファイルから値と行を足す仕組みの節が決める(§15.229 で、寄与が自分の足した値の行を足せるようにした)。
2. **予約語にしない。** `prerequisites` は行の頭の語(`kw::LINE_HEAD`)に足したが、予約語の一覧(`kw::RESERVED`、1.0 の 65 語)には足していない。どのブロックにも入っていない行の頭でだけ見出しとして読み、`inputs` の下の `prerequisites(p) : number` は宣言、`apply` の下の `prerequisites = …` は読み替えとして読む(`parse::named_like_head`。予約語ならこれまでどおり E009)。塊の中の `start`・`requires`・`or`・`nothing`・`ordered` も位置で読む。二語目が `requires` の行は、一語目が何であれその値の行なので、`start requires …` は値 `start` の行である。`nothing` は、`requires` のすぐ後で文字列が続くときだけ語として読み、ほかでは値の名前である。拡張の設計の 13.1 はこれらを予約すると書いていたが、§15.157 の約束(予約語は 1.0 で固定)と、列挙の担当(§15.212)、算術の担当(§15.214)の読み方にそろえた。
3. **検査**(`src/prereq.rs`)。グラフを一度か二度たどるだけで決まり、答えは正確である。
- E071:形の誤り。`over` の後が列挙でない、列挙の値でない名前、値を二か所に書いた(`start` と自分の行、二つの行、一つの行の中で二度)、`start` にも自分の行にも無い値、`start` の行が無い、読めない行。`ordered by` の表が無い、塊より下にある(前方参照は無い)、列挙の列一つを条件にし、行ごとに順序のある列挙か `number` の値をそのまま書く表でない、`enums` のファイルに書いた。読めない行は構文の段で、ほかは型の段で言うので、どちらもこれより後の段を止める。
- E136:循環。`or` の中の値もたどる。互いにたどれる値の組(強連結成分)ごとに一つ出し、組の最初の値から戻るいちばん短い輪と、組の値を挙げる。`or` の別の値から輪に入れる場合もエラーにした。前提の読み違いのもとで、証明書が「循環が無い」をトポロジカルな順で示せなくなるからである。
- E137:`start` からたどり着けない値。`start` から「どのグループにも持っている値がある値」を足していく閉包に入らない値である。循環が無ければ、入らないのは手に入らない値(`requires nothing`)を待つ値だけなので、手に入らない値だけのグループを持つ値、つまり待ちの始まりにだけ出し、その後ろで待つ値は注記に挙げる。循環があるときは E136 だけを言う。一つの原因を何本にも言わないためである(§15.217 の 3 と同じ考え)。
- E138:`ordered by` の食い違い。辺ごとに、値の答えが前提の答えより前なら出す。循環の上の辺(E136 が言う)と、手に入らない値への辺(順に意味が無い)は見ない。値の答えは、その値を取る最初の行の答えで、表に E1xx の誤りがあるあいだは見ない(表が完全で重ならないことは、表の検査が言う)。
- W130:余分な前提。一つの値だけのグループの前提が、同じ値のほかの一つの値だけのグループの前提が(一つの値だけのグループをたどって)要るものの中にあるとき。`or` の中の値は、どちらの側でも見ない(拡張の設計のとおり)。
- W111:塊が `over` に挙げる列挙で、どの行も値を挙げないものは塊のものなので、言わない。`ordered by` が読む表の列も、使われている。
4. **生成。** モジュールを持つ九つの言語の、塊を持つファイルのモジュール(規則か `enums` のファイル)に二つの関数を足した。`<名前>_requires(value)` はグループのリストで、何も要らない値は `[]`、手に入らない値は値の無いグループ一つ `[[]]` を返す。こうすると、二つ目の関数は手に入らない値のための分岐を持たずに済む。`<名前>_available(value, held)` は、`held` にまだ無く、どのグループにも `held` の値があるかを返す。前提は値ごとに一つの分岐で、列挙の順に書く(最後の値は最後の分岐。Rust・PHP・Swift は全部の値を挙げた `match` と `switch`)。辞書の定数にしなかったのは、型を付けた Python をネイティブにコンパイルする処理系が受け付けやすいからである。名前は言語の慣習に従い、Python・Rust・Ruby・PHP は `enrollment_requires`、TypeScript・JavaScript・Swift・Java は `enrollmentRequires`、Go は `EnrollmentRequires` にした。Java の型は `java.util.List` と書き、`enums` のファイルのクラスに import を足さずに済むようにした。NumPy は計画の `prerequisites` にグループをデータとして持ち、`rulec_np.Rule` に `requires(name, value)` と `available(name, value, held)` を足した(どの規則の NumPy の出力でも `rulec_np.py` が変わる)。`enums` のファイルには NumPy の計画が無いので、その塊は NumPy には出ない。SQL と Wasm は入口が一つで、二つ目の関数を置く場所が無いので書かず、`rulec gen` がそう言い、`rulec api` が `without` に挙げ、`rulec test` も注記して JSON の `without` に挙げる(拡張の設計の 0.7)。`rulec test` の `skipped` に入れなかったのは、`--require-all` が道具の欠けを言うためのもので、作りとして無い関数で落ちると、塊を持つ規則がどれも `--require-all` を通らなくなるからである。Rust の Wasm の crate にも書かない。登録簿(`backend.rs`)に `prereqs` を足した。
5. **ベクタと `rulec test`。** `rulec gen` は `vectors/<別名>.prereq.tsv` と `.prereq.expected` を書く。値ごとに、何も持っていないとき、各グループの値を一つずつ持つとき、そこから一つのグループの値を外したとき、`or` のほかの値に替えたとき、値そのものも持つとき、ほかの全部を持つとき、の答えを参照(`prereq::Graph`)から出す。各言語の走らせ役は `--prerequisites` で答え、`rulec test` が比べる。`enums` のファイルの塊を確かめるため、そのファイルにも各言語の走らせ役を書き、`rulec test` は `.jsonl` の無い別名を `.prereq.tsv` から見つけて、この比べだけをする(丸めの部品の比べは規則があるときだけ)。
6. **`rulec api`、証明書、再検査、ページ、`fmt`。** `rulec api` は最上位の `prerequisites` に、塊のグラフと、言語ごとの二つの関数の名前と型、関数の無い生成先の理由を書く。証明書は `prerequisites` に、グラフと、検査が見つけた二つの順(トポロジカルな順と、`start` からたどり着く順)と、`ordered by` の表の値ごとの答えを書く。`tools/recheck.py` は、値が一か所ずつにあること、二つの順がグラフに合うこと、表の答えが証明書の `tables` のその表の行(その値を取る最初の行の箱と答え)から出ること、手に入らない値への辺を除くどの辺でも順が保たれることを確かめる。Lean の `rulec-recheck` は節を読み、定理がまだ無いので「述べただけ」として名前を挙げる(§15.233 で証明した)。承認者向けのページは、塊ごとに値と要るものの表、つながりの図(Markdown は Mermaid、HTML は SVG。列は `ordered by` の表の答えの順、無ければ `start` から何段でたどり着くか)、検査が示したことを書く。顧客向けの記事には、何の後に何を取れるかの箇条書きを足した。`rulec fmt` は塊の値と `start` を最長の幅にそろえ、`requires` と `start` の値を一つの列に並べる。
**走らせた**:
- コーパスに、書き下ろしの規則を英語の版と日本語の版で六本足した。科目の履修(`course_catalog.rule`、`科目の履修.rule`。14 の科目、`or` の二行、手に入らない科目一つ、`ordered by level_of`)、資格の段階の `enums` のファイル(`shared/certifications.rule`、`shared/資格.rule`。七つの資格)、それを `import enum` で読む受験料(`exam_fee.rule`、`受験料.rule`)である。どれも何も言われずに検査を通る。
- 二つの組をそれぞれ `rulec gen` して、PostgreSQL を立てて `rulec test --require-all` に通した。どちらも 79 の比べが全部一致した。科目の履修は前提の 60 件が十の言語(Python・NumPy・TypeScript・JavaScript・Rust・Ruby・PHP・Go・Swift・Java)で、資格の `enums` のファイルは 29 件が九つの言語で一致し、規則の関数(ベクタ 7 件と 11 件、受け付けない入力 3 件と 6 件)は 12 の言語と MCP、Connect、WASI、PostgreSQL の関数の全部で一致した。SQL と Wasm には四行の注記が出る(`note: SQL: course_catalog's prerequisites have no functions in this target, which has one entry, so none were compared`)。ほかのファイルの `enums` から読んだ列挙の上の塊(`tests/prereq.rs` の技術ツリー。八つの値、`or` が一行、`ordered by` は三つの時代)も、十の言語が前提の 34 件で参照と同じに答える。
- コーパスの 119 本で、参照の評価器と 12 の言語が、ベクタ 10,024 件と受け付けない入力 1,622 件で一致し、塊を持つ科目の履修の二本では、前提の 120 件(60 件ずつ)が十の言語で一致した(`tests/threeway.rs`。前提の比べはこの節で足した。PostgreSQL を立てて回し、飛ばしたものは無い)。
- 証明書:科目の履修の証明書を `tools/recheck.py` は「prerequisites enrollment over course: 14 values and 15 edges with no cycle, 13 reached from start and 1 no case can take, and level_of keeps its order along 15 edges」と確かめ、Lean の `rulec-recheck` は「the prerequisites enrollment over course (14 values): no cycle, every value reached from `start` but those no case can take, and the order of level_of kept along its edges」を述べただけとして挙げる。偽った四通り(値を二か所に書く、前提より前に来る順、前提を持つ前にたどり着く順、表が書かない答え)は、Python の再検査が受け付けない。`proofs/` の `lake build` は通る。
- Kani:新しい四本の規則の Rust(`course_catalog` と `course_enrollment` が 3 本ずつ、`exam_fee` と `exam_charge` が 2 本ずつのハーネス)が、どれも 0 件の失敗で通る。
- 前からある 437 本の `.rule`(コーパス、変異、ほかの crate の規則)で、替える前のビルドと出力を比べた(6,164 回の実行と、コーパスの 115 本の `gen` と HTML のページ)。違ったのは、E004・E005・E066 の注記が書ける語に `prerequisites` を挙げること(rulec の四本と sekisho の二本の変異で、診断を出す六つのコマンドの全部。E066 の注記は、塊の関数は出るので「関数は生成しません」を「規則の関数も生成しません」に替えた)と、NumPy の `rulec_np.py` に二つのメソッドが入ること(NumPy を出す 106 本)だけで、ほかの生成物、証明書、`api`、`graph`、`doc`、`check` と `coverage` の JSON は一字も変わらない。
- CI がコーパスに当てる手順も、新しい規則で回した。`rulec fmt --check` と `rulec check` が通り、証明書を規則の本文と一緒に渡すと、Python の再検査は「every claim this program states was proved」で終わり、Lean の再検査は前提のつながりを述べただけとして挙げて `OK` で終わる。`ritsu gen` のパッケージにも、規則と `enums` のファイルのモジュールに二つの関数が入り、Python は `mypy --strict`、TypeScript は `tsc --strict`、Go は `go vet` と `gofmt -l` を通って、呼ぶプログラムが期待どおりに答える。
- 変異を英語と日本語の対で五つ足した(E071、E136、E137、E138、W130)。
**直した欠陥**:
- 取り込んだ列挙の Go の `Parse<名前>` は、`gofmt` が一行に残す長さ(見出しと一つの文で 100 バイト)を超えても一行で書いていた。日本語の受験料の規則(`exam_charge`、取り込む列挙が `Certification` でパッケージが `certificationpath`)を足して、`gofmt -l` がファイルを挙げて見つかった。長いときは本体を三行に書く。前からある規則の Go の出力は変わらない。
**捨てたもの**:
- **予約語にする。** 上の 2 のとおり。1.0 で通っていた規則が E009 になる。
- **E137 を、たどり着けない値の全部に出す。** 手に入らない値一つが、それを待つ値の数だけのエラーになる。
- **`or` の別の値から入れる輪を通す。** 輪は読み違いのもとで、証明書のトポロジカルな順が書けなくなる。
- **網羅(`rulec coverage`)に八つ目の基準を足す。** 前提のベクタは塊のグループから作るので、どのグループも一度ずつ欠け、どの `or` の値も一度ずつ一人でグループを満たす。基準はいつも満たされて何も測らず、全部の規則の網羅の出力に一行増えるだけである。
- **前提を定数の辞書で出す。** 上の 4 のとおり。
- **走らせ役の答えを JSON にする。** `[a][b|c]` で足り、どの言語でも短く書ける。値の名前は識別子なので、`[`、`]`、`|`、`,`、タブを含まない。
- **ritsu の索引(`Items`)に塊を載せる。** ritsu の参照の書き方(`ritsu_base::naming` の rulec の種類)に `prerequisites` が無く、足すと ritsu 全体の参照の書き方と、種類の語を名前に使わせない sakai の決まりに響く。この節では足さず、参照の書き方の側で決める。
**正直な限界**:
- `or` という名前の値は、前提の並びには書けない(区切りとして読む)。`start` の行と自分の行には書ける。
- E138 が見るのは表の答えの順だけである。
- 生成した関数は、列挙の値でないものを渡されたときの扱いを型に任せる(順位と表示名の関数と同じ)。型を持たない呼び方で別の文字列を渡すと、最後の値の答えになる。
- 証明書の主張(循環が無い、たどり着ける、順が保たれる)には Lean の定理がまだ無い。`tools/recheck.py` は全部を確かめる(§15.233 で証明した。数で順を決める塊を `tools/recheck.py` が受け付けなかった欠陥も、そこで直した)。
- `rulec diff` は塊の変化を言わない。
- `enums` のファイルの HTML のページは、これまでどおり作らない(Markdown のページには塊が出る)。
### 15.223 呼び出し側のオブジェクトから、並びの合計・最大・最小と、並びそのものを取る(2026-10-14)
**きっかけ**:§15.125 の `from` は、一つのコレクションについて `any`・`all`・`count` までで止めていた。並びを `elements` の出どころにする案は「実例を数えてから」として捨てていた。呼び出し側が判断ごとに組み立てて渡すオブジェクト(注文、申込、ある時点の状況をまとめたもの)から規則が読みたいのは、ある・全部・数のほかに、合計と最大と最小、それに要素ごとの判断のための並びそのものだった。注文の JSON から明細の金額の合計を出し、明細を並びとして規則に渡すつなぎのコードは、いまは手で書かれていて、何も確かめていない。契約がフィールドの名前を変えても、実行するまで誰も気づかない。
**決定**:
1. **`from sum|max|min <shape>.<並び> of <フィールド> [where …] [empty <値>]`。** `sum` は `where` に当てはまる要素のフィールドの合計で、当てはまる要素が無ければ 0 である。`max` と `min` は最大と最小で、見る要素が無いときの値を `empty` で書く(必須、E074)。`of` は要素のフィールドを一つ指し、`where` と同じく要素からの一語である。受ける入力は、`sum` なら数・お金・量・率、`max` と `min` ならそれに日付を加えたもので、どれも省略できる入力(`T?`)にはならない(E120)。`empty` は入力の値として入力の単位で書き(`empty 0USD`、`empty 2030-12-31`)、入力の型の値で、入力の範囲に入っていなければならない(E074)。範囲の外なら、見る要素が無いときに、規則が入口で受け付けない値を取り出し関数が渡すことになるからである。
2. **`elements <名前> from <shape>.<並び>` と、要素のフィールドの `from <要素からのパス>`。** 並びは契約の配列から要素を全部取り、`where` で絞ることはしない。並びを契約から取るときは、どのフィールドも要素の中のどこから読むかを `from` で書く(`from qty`、`from unit.kind`)。書かないフィールド、契約から取らない並びのフィールドの `from`、パスでない `from`(`from count qty` のような)は E074 にした。`elements <名前>` の後ろに書いた語は、これまで黙って捨てられていた。`from` 以外を書けば、いまは E074 である。
3. **検査には触れない。** §15.125 と同じく、取り出した値はふつうの入力で、取り出した要素はふつうの要素である。完全性、重なり、歩き、ベクタは、`from` が無いときと同じに決まる。証明書も Lean も変わらない。
4. **パスと型(E121、E120)。** `of` のフィールドが要素に無ければ E121、その型が入力と合わなければ E120 である。並びのパスと、要素のフィールドのパスも同じに確かめる。要素のフィールドは、要素の契約を一つの契約として読み直して確かめる(`Contract::element`。JSON Schema なら `items` の指す場所を JSON Pointer として求め、`.proto` なら要素のメッセージから読み始める)。メッセージは、要素の中のパスを `order.lines[].qty` のように、並びの後ろに `[]` を付けて示す。
5. **契約の検証との食い違い(E122)。** `sum` は、契約が通すフィールドの値の範囲と、要素の数の範囲から、合計の届く先を求めて入力の範囲と比べる。十件まで、一件 100 までなら合計は 1000 まで届き、500 までしか受け付けない入力は E122 で、`fix.text` は合計が範囲に収まる件数(`"maxItems": 5`)にした。件数にも値にも上限が無いときは、一つのキーワードでは合計を抑えられないので `fix.kind` を `none` にした。`max` と `min` は、フィールドの値をそのまま入力の範囲と比べる。契約から取る並びでは、要素のフィールドを入力と同じに比べ(`required`、null、範囲、列挙の値、proto3 の既定値)、並びの件数を `count` が決める上限と、列の合計を `sum` の上限と比べる。後者は、列のいちばん大きい値に、契約と上限が通すいちばん長い並びの件数を掛けたものである。要素のフィールドは入力ではないので、その E122 には `witness` の `inputs` を付けない。付ければ、呼び出し側はそれを入力の例として読む。
6. **生成は五つの言語のまま**(Python、TypeScript、JavaScript、Ruby、PHP)。`sum` は各言語の合計(`_sum`、`reduce`、`sum`、`array_sum`)で、`max` と `min` は、値のリストと `empty` を受け取る補助関数(`_largest`、`_smallest`。Ruby は `max || <empty>`)で書く。補助関数は、規則がそれを使うときだけ出る。日付は日の番号として比べる。並びは、取り出し関数の中で、要素の型(Python の `NamedTuple`、TypeScript のインタフェースのオブジェクト、Ruby の `Struct`、PHP のクラス)を要素ごとに作って組み立てる。`.proto` の契約からは、ほかの射影と同じく protojson の形で読む(JSON 名か `.proto` の名前、無いフィールドは既定値、int64 は文字列か数)。契約から取る並びは取り出し関数の引数から外れ、引数は、オブジェクト、`from` の無い入力、`from` の無い並びの順になる。ほかの七つの生成先には、これまでどおり取り出し関数が無い。設計の「書けない生成先は名前を挙げて飛ばす」に合わせて、`rulec gen` はその七つを、理由を一文で添えて名前で言うようにした(前は何も言わなかった)。理由は、Go・Swift・Java・Rust では呼び出し側のオブジェクトを型で持つこと、SQL・NumPy・Wasm では取り出す相手のオブジェクトが無いことである(`backend.rs` の `Backend.projects`)。
7. **`rulec api` の `projection` に `elements` と `without` を足した。** `elements` は、契約から取る並びごとに、名前、別名、どこから取るか、フィールドごとの要素からのパスを挙げる。無いときは空のリストで、キーは必ずある。`without` は、取り出し関数の無い七つの生成先と、その理由である。五つの言語の署名は、取り出し関数そのものと同じ組み立てから作るようにした。前は、署名が並びの引数を一つも挙げていなかった(並びと `shape` を両方持つ規則がコーパスに無く、誰も見ていなかった)。
8. **ページ。** 入力の注記を、取り出し方の言葉にした(「`order.lines` の `amount_usd` の合計」「`order.lines` のうち `fragile = true` の要素の `amount_usd` の最大。無ければ 0USD」)。`any`・`all`・`count` の入力も同じ言い方にそろえ(前は `from` の式をそのまま載せていた)、フィールドを一つ読むだけの入力はパスのままにした。並びの節には、どこから要素を取り、各フィールドをどのパスから読むかを書く。取り出し関数の説明の文は、並びを取る規則でだけ「入力と並びを取り出して」に替え、ほかの規則の生成物は一字も変えない。
9. **ritsu の口(`RuleFacts`、`Connect`)には載せない。** 口が述べるのは規則の関数の呼び方で、ワークフローは規則の関数を呼ぶ。入力の `from` も口に載っていない。並びが契約から来ることは `rulec api` の `projection.elements` が述べる。
10. **予約語は増やさない。** 使った語(`sum`、`max`、`min`、`of`、`empty`)はもう予約語で、`from` は決まった位置でだけ読む。
**捨てたもの**:
- **`sum` にも `empty` を書かせる。** 何も無いときの合計は 0 で、業務が決めることが無い。
- **`empty` を省いたら、フィールドの最小値(最大値)にする。** 業務の判断を道具が決めることになる(§15.220 と同じ)。
- **`empty` を契約の生の値で書く。** `where` の値は契約の値と比べるので生の値だが、`empty` は入力の値で、契約の値とは比べない。入力の単位で書けば、目盛りのある数でも読み違えない。
- **並びを `where` で絞って取る。** 絞った並びの件数の上限を契約に書けず(§15.132)、要素の判断は規則の表で書ける。
- **要素のフィールドの `from` を省いたら、同じ名前のフィールドを読む。** 規則の名前は日本語のことが多く、別名と契約の名前もたいてい違う。どこから読むかを書かせれば、パスの検査がそのまま効く。
- **順序つきの列挙の `max` と `min`。** 契約が運ぶのは値の名前の文字列で、順序は規則のものである。取り出し関数に順位の関数を持ち込むことになり、要るとはっきりしてから考える。
- **フィールドどうしの比較(`where owner = me`)、結合、入れ子の量化、グラフの探索。** §15.125 のとおり。隣の隣や経路のような事実は、呼び出し側がオブジェクトに並びとして作って渡す。
**変わること**:
- `rulec api` の `projection` に `elements` と `without` のキーが増える(`from` を持つ規則の全部。`elements` はたいてい空のリスト)。
- `from` を持つ規則の `rulec gen` が、取り出し関数の無い七つの生成先を一行ずつ言う。
- ページの入力の注記で、`any`・`all`・`count` から読む入力の言い方が替わる(`shape` を持つコーパスの規則のうち、それを使う規則)。
- W122 の注記が「入力も並びも無い」になる。
- `elements <名前>` の後ろに語を書いた規則が E074 になる(木の `.rule` には一本も無い)。
**走らせて見つけて直したもの**:
- `rulec api` の `projection` の五つの言語の署名は、並びの引数を一つも挙げていなかった(上の 7)。
- 最初に書いたコーパスの規則は、パレットの数と、合計と最大を一つの表の三つの列で読んでいた。網羅の監査(`rulec coverage`)は、その表の行の、合計と最大の境界の対のうち二つで、内側の点にベクタが届いていないと言った。パレットの数(`count`)の条件を持つ行の境界の対は、その件数になる並びと、境界の値の入力を同時に要るが、ベクタの作り方は、入力の境界の対を要素一件の並び(歩きの多い規則では、どの並びも一件)で運んでいて、`>=2` のような行には届かなかった。規則は、入力だけで決める表とパレットの数だけで決める表に分け、二つの答えを足す形にした。ベクタの作り方も直した(`vectors::held_pairs`)。数え上げ・合計・歩きの答えのセルが `-` でない行では、入力の境界の対を、行のほかの列をそろえ、並びをそのセルに当てはまるもの(件数ならその長さ、合計ならその合計、ほかの答えならそこに届く並び)にして、もう一度作る。一回の歩きの数え上げ(`count_pool`)と歩きの多い規則(`walk_pool`)の両方に入れた。コーパスの 143 本と `shared/` の 6 本のベクタと網羅の結果は、直す前と一字も変わらなかった。`tests/count.rs` と `tests/walks.rs` に、この形の規則で境界の対の二点がそろうことを確かめる試験を一本ずつ足した。
**確かめたこと**:
- コーパスに二本(英語の `order_handling_fee` と、日本語の版の `注文の取扱手数料`)と契約 `contracts/order_lines.schema.json` を足した。どちらも `check` と `fmt --check` を通り、網羅の七つの基準を全部満たす(行 8/8、境界の対 12/12、値の対 1/1)。`rulec gen` して PostgreSQL を立て、`rulec test --require-all` で 31 の組み合わせが全部一致した(ベクタ 61 件、受け付けない入力 24 件)。
- 取り出し関数を五つの言語で実際に呼んだ(`tests/projection.rs`)。コーパスの二本は、例の並びと同じ明細を持つ注文の JSON を五通り、`.proto` の規則は、protojson の形(lowerCamelCase の名前、`.proto` に書いた名前、文字列の int64 と数の int64、省かれた `false` と列挙の番号 0、並びの無い注文)で六通り渡し、どれも五つの言語が規則を直に呼んだ答えと同じに答えた。
- 生成した Python は、`pyright` の strict(basedpyright 1.39.8、`typeCheckingMode: strict`)で、足したコードに何も言われない(言われたのは、前からある丸めの補助関数が使われていないことと、`.proto` の `_proto` の補助関数の型だけ)。TypeScript は `tsc --strict`(7.0.2、`tests/threeway.rs` と同じオプション)を通る。`tests/threeway.rs` の `mypy --strict`、steep、ruff、`gofmt -l` と、参照の評価器と全部の言語の一致も通る。
- 証明書は変わらない。新しい二本の証明書を `tools/recheck.py` は「every claim this program states was proved」と確かめ、Lean の `rulec-recheck` は `OK` で終わる。Kani 0.68.0 の証明のハーネスは、二本とも 497 の確かめの失敗が 0 件だった(35.7 秒と 38.8 秒)。
- 前からある規則を、替える前(e3f48327)のビルドと比べた。木の `.rule` 385 本の `check --format json`(英日)と、コーパスの 125 本の `api`(英日)、`doc` の Markdown と HTML(英日)、証明書、`coverage`、`graph`、`gen` の全部のファイルの、2,020 回の実行で違ったのは 34 回だった。W122 の注記(変異 `m_w122` の英日)、`api` の `elements` と `without`(射影を持つ六本の英日)、`any`・`all`・`count` から読む入力の注記(四本の Markdown と HTML の英日、`gen` が書くページ)である。生成したコード、ベクタ、証明書、`coverage`、`graph` は一字も変わらない。古い三つの形の `from` の E013 も、前と同じに出る。
- 変異を英語と日本語の対で四本足した(`m_e074` と `m_e074ja`:割れ物の最高額から `empty` を消した。`m_e121seq` と `m_e121seqja`:明細の並びを契約に無い配列から取った)。golden を四つ足した(日英の `E074` と `E121-seq`)。
- 試験:`tests/projection.rs` に五本(コーパスの二本を五つの言語で、`.proto` の合計・最大・最小と並びを五つの言語で、並びを取る規則の `api` の署名、E074・E120・E121 の形、取り出し関数の無い生成先を名前で言うこと)、`tests/contract.rs` に五本(合計の届く先、最大と最小、合計の読むフィールドの `required` と null、契約から取る並びの件数と合計と要素、`.proto` の並びと合計)を足した。
**正直な限界**:
- `max` と `min` は数と日付だけで、順序つきの列挙は読まない(上のとおり)。
- 列の合計を `sum` の上限と比べるとき、計算した列の値は規則の宣言から求めた区間を使う。契約がその列のもとのフィールドをもっと狭く抑えていても、そこまでは読まないので、要らない E122 が出ることはある(見逃すことはない)。
- `.proto` の列挙のフィールドは、要素のフィールドとしても規則の列挙には読めない(§15.133 の限界のまま。`where` では比べられる)。
- `rulec test` は取り出し関数を走らせない。ベクタは規則自身の平たい入力だからで(§15.125 のまま)、取り出し関数を五つの言語で走らせるのは `tests/projection.rs` である。
- 取り出し関数は呼び出し側のオブジェクトを `dict[str, Any]` で受ける(§15.207 が残したとおり)。足したコードは、その上で `pyright --strict` に何も言われない形で書いた。
### 15.224 乱数の入力と確率の主張:`random`、`chances`、区分ごとに点を数える検査(2026-10-14)
**きっかけ**:抜き取り検査、くじ、無作為の割り当て、一回の乱数で決まる勝負のように、入力の一つを呼び出し側が乱数で引く規則がある。乱数の値を入力にすれば一回の答えは書けたが、答えごとの確率は言えなかった。くじの景品の確率は、ポスターに刷る値と実装が別々に書かれ、抜き取りの割合は、監査の計画と表が食い違っても誰も気づかない。表の行は区画なので、一様に引く入力の点を数えれば、確率は正確な有理数で出る。
**決定**:
1. **`random`。** 入力の行の最後に書く印で、呼び出し側が宣言の範囲から一様に、ほかの入力と独立に引いて渡すことを言う。引けるのは、範囲の両端を書いた数(金額・量・率も、刻みの整数倍ずつ)、列挙(範囲が外す値は除く)、真偽である。生成する規則の関数は変わらない。引くのは呼び出し側で、乱数の種も呼び出し側のものなので、同じ入力から同じ答えが出る。`constraint` が乱数の入力を挙げれば、一様なのは制約を満たす点の中である(`constraint roll < challenger_strength + holder_strength` なら、`roll` は 0 から二つの強さの和より一つ小さい値まで)。完全性と重なりは、乱数の入力も含めていつもどおりに見る。machine の主張(§15.148)は、乱数の入力をどの値もとりうる入力として扱う。起こりうるかどうかの主張なので、それで正しい。
2. **引けないものは E070。** 範囲の端の無い数、値の無いことがある入力(`?`)、日付、文字列、machine が持ち越す入力、要素のフィールド(要素ごとに引くことになり、点の数が並びの長さで変わる)、出力。`from` で呼び出し側のオブジェクトから取り出す入力(§15.125、§15.223)も E070 にした。取り出した値は呼び出し側の値で、引いた値ではない。`from` は行の終わりまでを読むので、`from sum order.lines of amount_usd random` の `random` は、`from` の残りとして読み飛ばされて黙って消えていた。`from` の後ろの最後の語が `random` で、`from` の語(`.` や `of`・`where`・`empty` の後ろ、比べの値)でなければ印として読み、E070 にする。乱数の入力の点を掛け合わせた数が 2^62 を越える規則は E108 で止める。確率の通りの数は、どの言語でも int64 で数えるからである。
3. **`chances` の表。** 見出しは、乱数でない入力の全部の列(並びを歩く規則なら並びの列も。セルは `examples` と同じく `sequence` の名前)、`->`、主張に要る出力の列、最後に `chance` の列である。乱数の入力の列は書かない。数えるのはその点だからである。出力の列には表と同じセル(`>=50`、`-`)を書き、書かない出力はどの値でもよい。`chance` には正確な有理数(`5/14`、`0`、`1`)、率(`35%`、`0.1%`)、それとの比べ(`<=1/100`、`>=1/4 <=1/2`)を書く。行は、「入力のセルに当てはまる乱数でない入力の**どの**組み合わせでも、乱数の入力を一様に引いたとき、出力が出力のセルに当てはまる確率がその値である(比べなら、それを満たす)」と主張する。形の誤り(`chances` の行に続く語、表が無い、`->` が無い、最後の列が `chance` でない、出力の列が無い、行が無い、乱数でない入力の列の抜け、乱数の入力や入力でない名前を左に書いた、出力でない名前を右に書いた、確率として読めないセル、乱数の入力が無い規則の `chances`)は E070 である。
4. **数える検査**(`src/chances.rs`)。乱数でない入力と乱数の入力の軸を、規則自身の境目で区分に切る(`diff` と machine と同じ `vdiff::Walker`)。乱数の入力と乱数でない入力を一緒に読む計算が無く、両方を挙げる `constraint` も無い規則では、歩きの区画は、乱数でない入力の区分と乱数の軸の区画を掛け合わせたものになる。答えが区画の上で一つに決まれば、区画の点の数は区間の長さの掛け算なので、範囲がどれだけ広くても、点を一つずつ見ずに正確に数える。答えが区画の中で乱数の入力とともに動くところでは点を一つずつ評価する。動くのが乱数の入力だけなら区分の代表の組み合わせ一つで、ほかの入力とともに動くなら区分の全部の組み合わせで評価する。両方を一緒に読む規則(`derive margin = roll - challenger_strength` と和の `constraint`)では、行のセルが許す乱数でない入力の値を全部挙げ、組み合わせごとに分布を数える。点を一つずつ評価する予算は、主張一つに `--budget` の 50 分の 1 で(machine の区画と同じ一点の重さ。既定で 100 万点)、組み合わせの数と乱数の点の数の積が予算を越えると前もって分かれば、一点も回さない。
5. **E133 と W129。** E133 は、成り立たない組み合わせと、そこでの確率を有理数と百分率で言い(「With campaign = false, the chance of prize = third is 1/20 (5.0%).」)、その組み合わせを、それを起こす入力として JSON の `witness` に載せる。乱数でない入力の無い規則では確率だけを言う。W129 は、決められなかった主張に理由(「the 10000 combinations of the inputs that are not random, times the 200 points of the random inputs, go past the budget (1000000 points)」)を添える。決められない主張は、成り立つとも成り立たないとも言わない。証明できない緑を出さないためである(§1)。
6. **予約語は増やさない。** `chances` は行の頭の語(`kw::LINE_HEAD`)に足したが、予約語(`kw::RESERVED`、1.0 の 65 語)には足していない。`random` は入力の行の最後でだけ印として読み、名前の `random` は名前である。拡張の設計の 13.1 はこれらを予約すると書いていたが、§15.157 の約束にそろえた(§15.222 の 2 と同じ)。その結果、E004・E005 の注記が行の頭に書ける語に `chances` を、E047 の注記が宣言の行に置ける語に `random` を挙げる。
**捨てたもの**:
- **正規分布や指数分布の入力を宣言する。** 確率が正確な有理数にならない。一様な入力と表で、離散の分布は書ける。
- **machine の並び全体の確率**(勝つまでの回数を含む吸収の確率)。状態の数と分母が大きくなり、生成コードが任意精度の整数を要る。何回もの勝負は、呼び出し側が一回ごとの確率の関数を組んで計算する。
- **実行時に乱数を引く関数を生成する。** 種と引き方は呼び出し側のもので、同じ入力から同じ答えが出ることを保つ。
- **点を一つずつ評価する予算を固定の数にする。** `diff` と machine と同じく `--budget` に比べて伸ばす。
- **予算を越えた主張を、回した点の範囲で成り立つとみなす。** 証明していない緑になる。
- **`chances` を予約語にする。** 上の 6 のとおり。
**正直な限界**:
- 点を一つずつ評価した区分は、証明書に点ごとの答えを載せず、区分の確率(一つに決まればその値、そうでなければいちばん低い値と高い値)だけを載せる。`tools/recheck.py` はその値が主張を満たすことを確かめ、数え方は「述べただけ」として挙げる(§15.225 の 4)。
- 両方を一緒に読む規則の主張は、行のセルが許す乱数でない入力の値を全部回す。範囲の広い入力に `-` を書いた主張は W129 になりやすい(変異 `m_w129`:強さの全部の組 10,000 通り × 200 点)。
- ページは既定の予算で数える。`--budget` を上げてはじめて決まる主張は、ページでは「予算の中で決まらない(W129)」と出る。
- 数えられるのは宣言した範囲の整数の点だけで、分布は一様だけである(上の「捨てたもの」)。
### 15.225 確率の関数、ベクタと `rulec test`、`rulec api`、証明書、ページ(2026-10-14)
**決定**:
1. **`<規則>_chances(<乱数でない入力>, <並び>)`。** モジュールを持つ九つの言語に、乱数の入力を持つ規則ごとに一つ足す。乱数の入力の点を全部回して規則自身の関数を呼び、答えごとに数え、答えごとの引き方の数と、引き方の全部の数を返す。確率は数を全部の数で割った値で、正確な有理数にするか百分率にするかは呼び出し側が決める。規則自身の関数を回すので、数が規則と食い違うことはない。入口が拒む点(`constraint`、重みの合計を越える値)は点に数えず、一つも残らなければ最後の拒みを投げる。答えの順は最初に出会った順(最初の乱数の入力がいちばん外側で、それぞれ低い値から。列挙は宣言の順、真偽は `false` から)。名前は言語の慣習に従う(`prize_lottery_chances`、`prizeLotteryChances`、`PrizeLotteryChances`)。Python は `Chances(NamedTuple)`(`total: int`、`ways: list[tuple[<出力の型>, int]]`)で、§15.207 の静的な型だけで書く。TypeScript と JavaScript は数を `bigint` で、Rust・Go・Swift・Java は 64 ビットの整数で持つ。Go は規則の `Input` を受け取り、乱数のフィールドは読まない。Go の規則の関数は `Input` を取るので、乱数でない入力だけの型をもう一つ作るより、同じ値を渡せるほうが呼びやすい。NumPy は `Rule.chances(**fixed)` で、乱数の軸を一つの列として全部の点を一度に評価する(`rulec_np.py` に足したので、NumPy を出す規則の全部でこのファイルが変わる)。
2. **SQL と Wasm は名前を挙げて生成しない。** SQL は、点を回す問い合わせにすると、問い合わせの枝と規則の行の 1:1 が無くなる。Wasm は入口が一つ(`call`)で、二つ目の関数を置く場所が無い(§15.148 が定数を置かなかったのと同じ)。`Backend` に `chances` のフィールドを足し(`projects` の次)、`rulec gen` は §15.223 の `proj_skipped` と同じ形で生成先ごとに一行で言い(「SQL: the function that counts the chances of the answers (`prize_lottery_chances`) is not generated for this target: …」)、`rulec api` の `chances.without` が同じ理由(`codegen::chances_why`)を挙げ、`rulec test` は比べなかったと注記する。一回の呼び出しの手間は点の数で、`rulec api` の `chances.runs` が言う。100 万を越えれば W131(「The chances function runs the rule 10000000 times on every call」)。
3. **ベクタと `rulec test`。** `rulec gen` は `vectors/<別名>.chances.jsonl` と `.chances.expected` を書く。一つ目は、ベクタの入力の行を、乱数でない入力の組み合わせごとに一行(24 行まで、参照の評価器が数える点の合計が 200 万まで)、二つ目は行ごとに `{"total":N}` と、答えごとに数とタブと、記録の `observed` の形の出力である。走らせ役は `--chances` で答え、`rulec test` が比べる(`via` は `chances`、表示は「ok prize_lottery (Python, chances) 2 chance cases」)。`tests/threeway.rs` も、確率の関数を持つ十の生成先の全部で同じ比べをする。
4. **証明書の `chances`。** 乱数の入力の点の数と範囲(`random`)、区分に切った軸(`axes`)、主張ごとに、入力と出力のセル、確率、判定、区分(`classes`)を書く。区分は、乱数の軸を敷き詰める区画の並び(区画ごとの座標、区間の長さの積の点の数、行の答え、主張に当たるか)と区分の点の数と当たりの数、または点を一つずつ評価した数(`run`)とその確率(一つに決まれば `chance`、いつも `low` と `high`)、または入力の無い区分である。`tools/recheck.py` は、`random` を規則の範囲と、乱数の軸の座標がその範囲を敷き詰めて点の数が合うことを、区画の点の数を区間の長さの積と、区画の答えが主張の出力のセルに当たるかを、区分の区画が乱数の軸を敷き詰めて点の数と当たりの数が合うことを、当たりの数 ÷ 点の数(評価した区分は `low` と `high`)が主張を満たすことを確かめる。区画の答えが表の行のとおりであることと、評価した区分の確率は「述べただけ」として挙げる。Lean の `rulec-recheck` は節を受け付け、主張ごとに「chances N row R: the chance of its outputs over the random inputs」を述べただけとして名前を挙げる。定理(敷き詰めた区画の点の数の和が全体の点の数になること、行が区画なので区分の中で当たる行が一つであること、主張の算術)は、作例のあとの Lean の担当が `Chances.lean` に書く。
5. **`rulec api` の `chances`。** 乱数の入力と引く値(数は刻みの整数の両端、列挙と真偽は値)、点の数、数え方(`points` と `runs`、または `weights` と `draw`)、言語ごとの関数の名前・署名・答えの型、関数の無い生成先と理由である。乱数の入力を持たない規則には、キーが無い。
6. **ページ。** 入力の表の注記に、引く値を書く(「`random`: 0 〜 13 の整数 14 通りから一様に選ばれます」、制約に挙がる入力は「(制約を満たす点の中で)」)。「確率の主張(検証済み)」の節に、主張の表と、検査の結果の列(「成り立つ: 5/14(35.7%)」、組み合わせで確率が変わる主張は「成り立つ: 3/5(60.0%)〜 7/10(70.0%)」、決まらなければ「予算の中で決まらない(W129)」)、数え方の一文(区画ごとに数えたか、点を一つずつ評価したか、重みから数えたかを、実際に数えたとおりに)、確率の関数の名前を書く。顧客向けのページは「答えごとの確率」に、主張の表と確率の百分率を書く。HTML のページの「試してみる」は、乱数でない入力を入れると、生成した JavaScript の確率の関数で分布を出す(答えごとに既約の有理数と百分率。「third | 1/20 (5.0%)」)。乱数を持たない規則のページは一字も変わらない。
**捨てたもの**:
- **確率の関数を、規則の関数と別に表から組み立てる。** 規則の関数と食い違いうる。規則の関数を回せば、一行も食い違わない。
- **確率を浮動小数で返す。** 正確な有理数が数から出るのに、丸めの誤差を持ち込む。
- **SQL で `generate_series` を使って点を回す。** 上の 2 のとおり。
- **点ごとの答えを証明書に載せる。** 範囲が広いと証明書が点の数だけ大きくなる。区分の確率の両端は、主張が区間(比べの積)なので、両端が満たせばあいだも満たす。
### 15.226 重みで選ぶ:`take_weighted` と `draw`(2026-10-14)
**きっかけ**:担当の割り当て、候補からの抽選、重みのある選択肢のように、並びの要素から一つを、その重みに比例した確率で選ぶ規則がある。呼び出し側が累積の重みを作って乱数と比べるコードは、境目の一つずれや、重みが 0 のときの扱いを間違えやすく、何も確かめていない。
**決定**:
1. **腕 `take_weighted <値> by <重み>` と行 `draw -> <乱数の入力>`。** 判定に落ちた要素の重みを順に足していき、引いた値が足した合計を初めて下回った要素の値を採る。要素が採られる確率は、重み ÷ 重みの合計である。`draw` は `fold` の下で、`->` の後ろに名前が一つだけあり、それが腕の語でないときだけ行として読む。`draw -> next` は、これまでどおり `draw` という判定の腕である。
2. **暗黙の制約:引いた値 < 重みの合計。** 生成コードは歩き終えたあとで確かめ、破れば入力の誤り(`roll is not below the total of the weights`)にする。参照の評価器も答えを持たず、受け付けない入力のベクタになる。一様なのはこの制約を満たす点の中である。重みの合計が 0 なら、答えは `exhausted` の答えで、`held` は採った値が無いので `empty` の答えになる。
3. **E073。** `take_weighted` に `by` が無い、`draw` の行が二つある、`take_weighted` の無い fold に `draw` の行がある、重みで選ぶ fold が一つの規則に二つある、その腕に `next` と `take_weighted` のほかの腕がある、重みが `number` でないか負になりうる、`draw` の行が無い、`draw` が指す値が `random` を付けて範囲が 0 から始まる `number` の入力でない。重みで選ぶのが一つの規則に一回なのは、通りの数を重みで数えられるようにするためで、二つ要るなら規則を二つに分ける。止めたり別の値を採ったりする腕を禁じたのは、どの要素の重みも合計に入ってから一つに決まらないと、重みの割合が選ばれる確率にならないからである。
4. **確率は式で出す。** 乱数の入力が一つで、`draw` の行だけが読む規則(`chances::Method::Weighted`)では、確率の関数は点を回さない。歩きを一度(引いた値 0 で)回して累積の重みを得て、重みの区切りごとに、その要素を採る最初の値で規則を一度呼び、数をその重みにする。このため生成コードは歩きを非公開の関数(Python・Rust・Ruby・PHP は `_<別名>_walked`、TypeScript・JavaScript・Swift・Java は `_<別名>Walked`、Go は `<別名>Walked`)に移し、出力と当たった行に累積の重みを添えて返す。公開の関数は前と同じである。検査も同じく、区切りごとに一度だけ評価する。W131 も出ない。
5. **ベクタ。** 要素の分の最初と最後の値、重みの合計を引いた値に置くベクタ(`roll = 99 (an edge of the weights)`、32 件まで)と、合計を越える値の受け付けない入力を足した。
**捨てたもの**:
- **呼び出し側が累積の重みとロールを渡す。** 表と生成コードの外に、確かめないコードが残る。
- **重みにお金や率をそのまま使う。** 重みは選ばれる割合を決める整数で、単位を持たない。お金なら `down(額 / 1JPY, 1)` のように整数にしてから使う。
- **重みで選ぶ fold を一つの規則にいくつも置く。** 上の 3 のとおり。
**確かめたこと**:
- コーパスに書き下ろしの規則を英語と日本語の版で八本足した。伝票の抜き取り(`invoice_sampling.rule`、`伝票の抜き取り.rule`。四つの種類と金額の境目で、全部・10 分の 1・4 分の 1・5%・50 分の 1)、くじの景品(`prize_lottery.rule`、`くじの景品.rule`。ポスターに刷る六つの確率)、案件の振り分け(`case_routing.rule`、`案件の振り分け.rule`。`take_weighted` で、三人のチームの 1/2・3/10・1/5 と、一人目が不在のときの 3/5)、抽選の特典(`reward_box.rule`、`抽選の特典.rule`。机の上の確かめの B08 の形を一般の名前で書き直したもので、`5/14`・`3/14`・`1/7`・`0` の主張と、`>=5/14 <=4/7`、ポイントが 50 以上の `2/7`)。一回の乱数で決める勝負(`contested_roll.rule`、`乱数で決める一回.rule`)は、乱数を `random` にして、和の `constraint` の下の確率(`1/2`、`3/5`、`2/5`、`100/101`、`>1/2`)を主張した。十本とも何も言われずに検査を通り、`fmt --check` を通り、網羅の七つの基準を全部満たす。
- 十本を `rulec gen` して PostgreSQL を立て、`rulec test --require-all` に通した。重みで選ぶ二本は 40、ほかの八本は 46 の比べが全部一致した。そのうち確率の関数は、十の言語(Python・NumPy・TypeScript・JavaScript・Rust・Ruby・PHP・Go・Swift・Java。重みで選ぶ規則は歩く規則なので NumPy を除く九つ)で、それぞれ 24・2・11・20・17 件の組み合わせの分布が参照と一致した。
- コーパスの 135 本で、参照の評価器と 12 の言語が、ベクタ 10,866 件と受け付けない入力 1,928 件で一致し、乱数を持つ十本の確率の関数は、組み合わせ 148 件(英日で 74 件ずつ)で、それぞれ確率の関数を持つ生成先(十。重みで選ぶ二本は NumPy を除く九)と一致した(`tests/threeway.rs`。確率の比べはこの節で足した。PostgreSQL を立てて回し、飛ばしたものは無い)。
- 証明書:十本の証明書を `tools/recheck.py` と Lean の `rulec-recheck` が通す。くじは「chances: 6 claims, 8 classes counted box by box and added up again, 0 empty, 0 points run one by one」、抽選の特典は 450 の区分、伝票の抜き取りは 40 の区分を区画ごとに足し直し、勝負は 25,000 点、案件の振り分けは 20 回の評価を「述べただけ」として挙げる。偽った証明書(区画の点を一つ増やす、当たりを裏返す、主張の確率を替える、乱数の点を一つ増やす、評価した区分のいちばん低い確率を主張の外にする)は、どれも `tools/recheck.py` が拒む(`tests/chances.rs`)。
- 生成した Python は `pyright` の strict(1.1.408)で、足したコードに何も言われない(言われたのは、前からある使われない丸めの補助関数と `NewType` の import だけ)。`tests/threeway.rs` の `mypy --strict`、steep、ruff、`gofmt -l`、`tsc --strict` も通る。
- HTML のページの「試してみる」を、node で生成したスクリプトを動かして確かめた(`tests/chances.rs`)。くじで、キャンペーン外なら「first 1/1000 (0.1%)、second 9/1000 (0.9%)、third 1/20 (5.0%)、no_prize 47/50 (94.0%)」、キャンペーン中なら三等が「9/100 (9.0%)」、はずれが「9/10 (90.0%)」と出る。
- 前からある規則を、替える前(`stage4`、80ff7a65)のビルドと比べた。木の `.rule` 466 本の `check --format json`(英日)と、コーパスの 125 本の `api`(英日)、`doc` の Markdown(英日)と HTML と顧客向け(英日)、証明書、`coverage`、`graph`、`gen` の全部のファイルの、2,307 回の実行で違ったのは、E004・E005・E047 の注記(rulec の三本と sekisho の二本の変異の英日)と、NumPy の `rulec_np.py`(NumPy を出す 108 本)だけだった。ほかの生成コード、ベクタ、証明書、`api`、`graph`、`doc`、`coverage` は一字も変わらない。
- 変異を英語と日本語の対で六つ足した(`m_e070`:伝票ごとに引く数から範囲を外した、`m_e070from`:注文の合計を取り出したまま乱数にした、`m_e073`:`draw` の行を消した、`m_e133`:ポスターの三等を 6% と書いた、`m_w129`:強さの全部の組についての主張、`m_w131`:引く数を一千万通りに広げた)。golden を英日で六つずつ足した。
- 試験:`tests/chances.rs` に十五本(コーパスが何も言われないこと、四つの語を位置で読むこと、E070 の十八の形、取り出した値の `random`、E073 の十一の形、E133 が言う組み合わせと確率、W129 と予算、W131 と E108、重みの合計を越える値の拒みと重みの区切りのベクタ、`gen` と `api`、言語の確率の関数、証明書の再検査と偽り、ページ、「試してみる」、`fmt`)。
**走らせて見つけて直したもの**:
- `from` の後ろに書いた `random` が、`from` の残りとして黙って読み飛ばされていた(§15.224 の 2)。§15.223 の `from sum` と出会う所で、`order_handling_fee` の合計に `random` を付けて見つけた。
- 確率の関数の Python の走らせ役が `'}'` を一重引用符で書いていて、`ruff format` が直したがった(`tests/threeway.rs` の整形器の試験)。`"}"` にした。
- 網羅の試験の素朴な境界の数え上げ(`tests/coverage.rs`)は、数の列の集合のセル(`8, 9`)と `not:` を数えていなかった。網羅の監査そのものは数えている。抽選の特典が、コーパスで初めて数の列に集合のセルを持つ規則だった。試験の数え上げに足した。
- ページの数え方の一文は、どの規則にも「区分の点の数を区間の長さの掛け算で数えた」と書いていた。点を一つずつ評価しただけの勝負や、重みから数えた振り分けでは、実際に数えたとおりに書くようにした。
**正直な限界**:
- 重みの合計が 0 のときは、引いた値に関係なく `exhausted` の答えなので、確率の関数は、乱数の入力の全部の点をその答えに数える。
- `draw` が指す乱数の入力の上限は、重みの合計がとりうる値より下でもよい(並びの長さに上限が無いので、比べられない)。上限より大きな合計になると、上限を越えた分の要素は採られない。
- `rulec diff` は、確率の変化を言わない。
### 15.227 開いた値と寄与のファイル:`open … combine`、`contribution`、`to`、`after`、`into`(2026-10-14)
**きっかけ**:規則の値を、元のファイルを書き換えずに、別のファイルから足したいことがある。キャンペーンが値引きを足す、商品ラインが商品の種類とその上乗せと税率を足す、方針が節に優先する。これまでは、`apply` で元の規則を読み、足す分を書いた一覧のファイルを手で作るしかなかった(`tests/merging/*/list.rule` の形)。足すたびに一覧を書き直し、合わせ方(足すのか、いちばん大きいものだけか、掛けるのか)も一覧を書く人に任されていた。元の規則の作り手が、どこに足せるかと、どう合わせるかを書き、足す側はその場所に足すだけにする。
**決定**:
1. **開いた値。** `open <名前>(<別名>) : <型> combine sum|product|max|min [empty <値>] range >=… <=…` の行と、その下に一行に一つずつの項の名前を書く。項は上で決まる値(表や節の出力、`derive`、`define`、集計、fold の答え、ほかの開いた値、準用が持ち込んだ名前)で、無くてもよい。`sum` は 0 に全部の項を足したもの、`product` は 1(率なら 100%)に全部を掛けたもの、`max` と `min` は `empty` の値と全部の項のうち、いちばん大きいもの、小さいものである。四つとも項の順によらないので、寄与を合わせる順は答えを変えない。開いた値は、書いた位置の `define` に下ろす(`link::place_opens`)。出力と同じ名前の開いた値は、その出力の値になる。
2. ★ **`max` と `min` の `empty` は、いつも合わせに加わる。** 項が無いときの値であり、項があるときは `max` の下限、`min` の上限になる。拡張の設計の 5.3 の「項の値が要らない行は中立の値を書く(最大なら `empty` の値以下)」が、この読み方でだけ成り立つからである。注文の値引きの上限は `combine min empty 50USD` で、「キャンペーンが決めたうちでいちばん低いもの。決めていなければ 50 ドル」になる。四つの合わせ方は、どれも出発の値に項を順に合わせる形でそろい(`sum` は 0、`product` は 1、`max` と `min` は `empty` から)、どの寄与も当てはまらないときは、寄与の無いときと同じく `empty` になる。この `empty` は、並びをたどる `max`・`min`(§15.220)、fold、`from max`・`from min`(§15.223)の `empty` とは違う。そちらは見る要素が無いときの答えで、要素があるときは合わせに加わらない。設計の 5.3 の例のうち、上限を `combine max empty 1000%` で「能力の上限のうちいちばん高いもの」とする行は、この読み方では値がいつも 1000% 以上になり、合わない。項が無いときだけ `empty` を使う読み方なら合うが、そのときは中立の値の文が成り立たない。
3. **型と形**(E068)。`sum` は数・お金・量・率、`product` は数と率、`max` と `min` はそれに温度と音量を足したもの。オプショナルの型は開けない。合わせ方、範囲、`max` と `min` の `empty` の抜けも E068 である。項の刻みが値の刻みより細かいときも E068 にした。開いた値は宣言した刻みで持つので、細かい項は黙って切られるからである。
4. **範囲は必ず書き、検査はその範囲の上で行う。** 開いた値は、検査の中では計算せず、宣言した範囲の全体を動く軸として扱う(`Checked.opens`。型に `step` を書いたときは、その刻みで切る)。下の表は範囲の全体で検査されるので、元の規則だけの検査は、どの寄与が来ても成り立つ。項を合わせた値の区間(項ごとの区間から、区間の算術で)が範囲を出れば E131 で、項ごとの区間と出どころ(この規則か、どの寄与か)と、範囲の外にする入力(`enrich_arith` が見つける)を言う。フーリエ・モツキンの消去には、開いた値の等式を入れない。入れると、範囲の全体を動くという約束が崩れる。網羅の刻みも宣言した刻みを使い、証明書では軸の種類を `open` とする。
5. **寄与のファイル。** 一行目が `contribution <名前>(<別名>) v<版>` のファイルで、`to "<ファイル>" signature:<16 桁>`(一行だけ、必ず)、`after "<寄与のファイル>" signature:<16 桁>`(何行でも)、`inputs`、表・節・`derive`・`define`・集計・fold・`open`、`into <開いた値>` とその下の項の名前、`extend`(§15.228)、`prerequisites <塊>` と `requires` の行(§15.229)、`sequence` と `examples` を書ける。出力、`result`、`apply`、機械、たどる並び、列挙、取り込み、出典、制約は書けない(E067)。関数の形は元の規則が決めるからである。`to` の先は、規則、`enums` のファイル、ほかの寄与のどれかで、寄与の先なら、その寄与が足す規則に足す。寄与が開いた値に、ほかの寄与が足せる。
6. **名前。** 寄与が宣言した名前は、合わせた規則では `<寄与>:<名前>` になり、別名は `<寄与の別名>_<別名>` になる。二つの寄与が同じ名前を使っても、元の規則の名前とも、ぶつからない。寄与は元の名前をそのまま読み、`after` の寄与の名前は `<寄与>:<名前>` で読む。名前は、自分の名前、足す先の寄与の名前、書いたままの名前の順に解く。寄与の入力は名前で一つになり、型か範囲が違えば E042。
7. **置き場所と順。** 寄与の表や定義は、読むものより後、それを読むものより前に置く(`link::sink`)。開いた出力の表や節は、その出力の最後の定義のすぐ後に置く。定義の集まりは、最後の定義の位置で決まるからである。そこより下で決まる値を読めば E067。寄与を合わせる順は `after` の順で、残りはパスの順。`after` の輪は E067。
8. **予約語にしない。** `open` は行の頭の語(`kw::LINE_HEAD`)に足したが、予約語(1.0 の 65 語)には足していない。どのブロックにも入っていない行の頭でだけ開いた値の行として読み、`inputs` の下の `open(o) : bool` は宣言として読む(`parse::named_like_head`)。`combine` と `product` は `open` の行の中で、`contribution` は一行目で、`to`・`into`・`extend` は寄与のファイルの行の頭で(`kw::CONTRIBUTION_HEAD`。`after` も)読む。リポジトリの 488 本の `.rule` では、コメントと文字列を除くと、`open`・`combine`・`product`・`contribution`・`to`・`into`・`extend`・`meaning` を名前に使う行は無く、`signature` は入力の名前として 4 本(10 行)にある。固定は `signature:` に 16 桁以上の 16 進が続くときだけ読むので、その 4 本はこれまでどおり通る。`open` を行の頭の語に足したので、E004 と E005 の注記が挙げる「書ける語」に `open` が増える(§15.222 の `prerequisites` と同じ)。
9. **合わせた値を変えない項は W132。** 項がどの値を取っても合わせた値を動かせないとき(`max` で項の区間が全部 `empty` 以下、`min` で全部 `empty` 以上、`sum` でいつも 0、`product` でいつも 1)、その項を警告で言う。拡張の設計の 5.3 の上限の例のように、上限を `max` で書くと、項は黙って効かなくなるからである。項の区間は大きめに見積もるので、言った項は本当にどの答えも変えない。前からある警告に、項が答えを変えないことを言うものは無いので、新しい番号にした。台帳と、英日の変異の対(どの注文にもある 50 ドルより高い上限を足すキャンペーン)を足した。
**捨てたもの**:
- **読み込む寄与の一覧を書くファイル。** 順が要るのは上書きだけで、それは `overrides` と `after` で、寄与の側に書いて検査できる。
- **`max`・`min` の `empty` を、項が無いときだけの値にする。** 上の 2 のとおり。
- **開いた値を、検査の中でも項から計算する。** 寄与が来るたびに、元の規則の検査の答えが変わる。
- **寄与を別の関数として生成し、実行のときに合わせる。** 拡張の設計の 0.6 のとおり。
**正直な限界**:
- `max` と `min` の開いた値に、順序のある列挙と日付は使えない(E068)。拡張の設計の 5.3 は許すと書いたが、区間の算術と軸の作り方が数の型を前提にしている。要る例が出たら足す。
- 合わせ方は四つで、足し算と掛け算が書いた順に交互に来る並びは書けない(拡張の設計の 5.12)。
- 項の無い開いた値と、どこからも読まれない開いた値に、警告は無い。
### 15.228 列挙に値を足す:`extend`(2026-10-14)
**きっかけ**:新しい商品の種類を足すとき、料金・税・配送のどの表がその種類について何も決めていないかを、全部知りたい。列挙の `open` の印は §15.212 で、読んで持つだけの構文として入れてあった。
**決定**:
1. `extend <列挙> with <値>(<別名>) ["<表示名>"] [after <値>] [default]`。`open` の付いた列挙にだけ足せる(付いていなければ E067 で、注に開いた列挙を挙げる)。`enums` のファイルの列挙には、そのファイルに `to` で足す。規則が取り込んだ列挙に、その規則への寄与から足すことはできない(E067)。順序のある列挙では `after` が要り(E067)、値はその位置に入る。元の列挙にある値を足せば E067。
2. `enums` のファイルに足した値は、その列挙を読む全部の規則に入る。規則は、取り込むファイルを、組の中の寄与を合わせて読む(`enums::read_imports` が組の中で `prepare` を呼ぶ)。
3. 足した値に行の無い表は、これまでどおり E101 で、例の値が、どの寄与が足した値かを注で言う(`link::attribute`)。
4. **E033 の広げ方。** 意味は同じで、外から来た値に、まだ誰も行を決めていないことを言う。足した値が、開いていない表の `-` の行、その値を外さない `not:` の行、その値まで覆うようになった比べるセルの行に黙って落ちるなら、行ごとに E033 にする。`extend` の行に `default` を付ければ通る(`enum` の行の `default` と同じ語で、同じ意味)。開いた表なら、寄与が行を足せる。★ 台帳の E033 の見出しを「外から来た列挙の値に、行も `default` もありません」に広げた。
5. W111 は、寄与が足した値については言わない。E033 と E101 が受け持つ。
6. ページは、足した値に、どの寄与が足したかを添える(「garden "Garden supplies" (added by contribution garden_kind)」)。生成物では、足した値は列挙のモジュールの値になり、そのモジュールの頭に寄与を書く。
### 15.229 開いた定義と前提の塊に足す:`table … open`、`clause … open`、`prerequisites … open`(2026-10-14)
**決定**:
1. 表の名前の後ろか、節の見出しの最後に `open` を書くと、その出力を決める定義の集まり(§15.66)に、寄与が表や節を足せる。寄与の定義は集まりに入り、いつもの検査(完全性は和の上で、重なりは `overrides` で順を書かなければ E105、覆われた行は E102)を受ける。元の定義にも、`after` の寄与の節(`strict_policy:strict_decay`)にも、`overrides` で優先できる。開いていない出力に定義を足せば E067。
2. 準用で持ち込んだ項目のうち、上書きされる出力を読むものは、その出力の、規則の全体での最後の定義の後に移す(`apply::reorder_after_overrides` を書き直した)。寄与の節の上に、さらに別の寄与の節を重ねたとき、読む項目が二つ目の節より前に残っていた。
3. 前提の塊(§15.222)に `open` を付けると、寄与が、自分が足した値の `<値> requires …` の行を足せる。足した値に行を足さなければ、合わせたグラフで E071(行の無い値)になる。元の値の行は変えられない(E067)。`start` と `ordered by` は塊のファイルのものである。合わせたグラフで、E136・E137・E138・W130 がそのまま走る。
### 15.230 固定:`signature:` と `meaning:`。準用の広げ方(2026-10-14)
**きっかけ**:`apply` の固定は元のファイルのバイト列の SHA-256 で、コメントを一つ直しても E040 で落ちた。寄与には、元の行が変わっても落ちず、頼っている形が変わったら落ちる固定が要る。準用は、準用している規則、並びをたどる規則、自分の列挙を出す規則を読めず(E044)、元の途中の値も読めなかった。
**決定**:
1. **`signature:<16 桁>`**(寄与の `to` と `after`)。足す先の、寄与から見える形の SHA-256 の先頭 16 桁である。形は行の並びで、名前、入力(型と範囲)、出力(型と丸め)、開いた値の行、開いた出力、開いた列挙(順序があれば値も)、開いた塊、寄与が足す値、寄与が読む名前の宣言が入る。コメント、`description`、表の行、寄与が読まない定義は入らない。元の行を直しても寄与の固定は落ちない。合わせた答えは動くが、それは元の規則の承認の範囲である。形が変われば E040 で、リポジトリの履歴(そのファイルの最後の 60 の版)に固定したときの版があれば、何が変わったかを言う(「`open stackable : money[USD] combine sum range >=0USD <=100USD` is now `… <=80USD`」)。無ければ、形に入るものを言う。`rulec source pin <寄与のファイル>` が書き直す。
2. **`meaning:<16 桁>`**(`apply`)。元の規則の行を字句に分けて一つの空白でつなぎ、コメントだけの行、`description` の行、出典の引用(`@` から行末まで)を除いたものの SHA-256 の先頭 16 桁である。行が変われば落ち、コメントや空白では落ちない。E040 は、履歴に固定したときの版があれば、動いた行を書いたとおりに言う。いまある `sha256:` は前のとおりバイト列として読み、`rulec source pin` はいつも `meaning:` を書く(`sha256:` の固定も `meaning:` に書き直す)。コーパスの準用の規則二本(`part_time_retirement_pay.rule`、`非常勤退職手当.rule`)を `meaning:` に取り直した。そこから作る準用の変異(`m_e040`〜`m_e043`、`m_w118`)は `make-mutants.sh` で作り直した。`m_e044` だけは `m_e101.rule` のバイト列の `sha256:` のままにした(シェルで計算できる固定で、前の形が読めることも確かめる)。
3. **字句。** `signature:` と `meaning:` は、`sha256:` と同じく 16 桁以上の 16 進が続く一続きでだけ読み(`lex::Kind::Signature`、`Kind::Meaning`)、予約語にはしない。
4. **準用の準用。** 元の規則は、自分の検査と同じに展開される。持ち込んだ名前は、両方の準用の名前の下に入る(`outer:inner:step_of`)。展開中の規則の積み(`EXPANDING`)と、パスだけを読む `ring_below` で輪を見つけ、E044 で輪のファイルを全部挙げる(「the applies go round (a → b → a)」)。輪は固定より先に言う。輪のどのファイルの意味も次のファイルの固定を含むので、固定がそろうことは無いからである。
5. **並びをたどる元の規則。** 元の並びを、この規則の並びで読み替える(`<元の並び> = <この規則の並び>`)。要素のフィールドは名前で対応し、違う名前は `with <この規則のフィールド> -> <元のフィールド>` で書く。元の集計と fold は、`<準用>:<名前>` としてこの規則の歩きに入る。答えの名前の無い fold は、元の最初の出力として答える。フィールドの型と範囲は E042 と E043 で確かめる。並びをたどらない元の規則の入力を、この規則の要素のフィールドで読み替えると、元の規則は要素ごとに当てはまる(`kg = weight`)。
6. **元の列挙。** 元が宣言した列挙は `<準用>:<列挙>`(別名は `<元の別名>_<列挙の別名>`)として入り、値の綴りは元のままである。持ち込んだ項目の型が使う列挙だけを入れる。元と同じ `enums` のファイルから読む列挙は、この規則の列挙と同じものとして扱う。★ 元の列挙の出力をこの規則の列挙で読むときは、出力の行に `with <元の値> -> <この規則の値>, …` を書き、対応の表 `<準用>:<出力>_as_<名前>` を作る。入力の `with` は「この規則の値 -> 元の値」なので、どちらも値の流れる向きに書くことになる。
7. **`<準用>:<名前>`** を、式と、節の `when` の列に書ける。字句では、コロンの両側に空白なしで続く二つの名前を一つの名前として読む。使われない元の名前は消すが、この規則が `<準用>:<名前>` で読むものは残す。
8. E044 は、準用の輪と、元の規則が読めないこと、`check` を通らないことだけを言う。準用している規則、並びをたどる規則、自分の列挙を出す規則は準用できるようになった。★ 台帳の E040 の見出しを「固定した元のファイルが、書いてあるハッシュと違います」に広げた。
### 15.231 合わせた規則を、検査・生成・ページ・証明書・ritsu に載せる(2026-10-14)
**決定**:
1. **組。** どの寄与を合わせるかは、コマンドに渡したファイルで決まる(ディレクトリは中の `.rule` を全部。`link::with_set`)。`gen`・`doc`・`api`・`certificate`・`coverage`・`vectors`・`graph`・`schema`・`adapter` は、渡した寄与を、それが行き着く規則か `enums` のファイルに置き換えて答える(`link::roots`)。`diff` と `replay` は、二つの版や記録を比べるものなので置き換えない。
2. **`check`。** 寄与が届く規則は、二度検査する。規則だけ(その例と一緒に)と、合わせた規則(例は走らせない。どの例も、それを書いたファイルの組についてのものだから)である。見つけたことは、両方を合わせて一度ずつ言う(`finding_key`)。寄与のファイルは、行き着く規則に自分と `after` の寄与だけを合わせて、自分の例と一緒に検査する。一つの実行の中で、前のファイルで言ったことは繰り返さず、エラーが全部上で言われたファイルには「note …: its errors are said above」を一行出す。`ok` の行は、合わせた寄与を言う(`ok rules/product_price.rule (2 contributions: rules/contrib/garden_price.rule, rules/contrib/garden_kind.rule)`、`ok rules/contrib/garden_price.rule (contributes to rules/product_price.rule)`)。規則が取り込む `enums` のファイルの列挙に値を足した寄与も挙げる。
3. **ほかのファイルの行についての診断。** ほかのファイルを読むときは、字句の位置にそのファイル(見せるパス、行、SHA-256)を持たせ(`diag::in_file`、`Span.file`)、枠はそのファイルの行で描き、JSON の `file` と `where.file` と、その span の `file` に、そのファイルを書く。
4. **生成。** 合わせた規則の関数一つを、12 の生成先がいまの形のまま書く。引数は、元の入力に、寄与の入力が合わせた順で続く。頭に `Contributions: <名前> = <ファイル> (signature:…, sha256:…)` を寄与ごとに一行書く(ファイルは規則の場所から)。`rulec api` は `contributions`(名前、ファイル、signature、SHA-256、足した入力)と `opens`(開いた値ごとに型、合わせ方、`empty`、範囲、項とその出どころ)を書き、`applies` の項目に `meaning` を足した。
5. **ページ。** 開いた値ごとに「この値を決める寄与」の節(項と、この規則のものか、どの寄与のものか、そのファイル)を、寄与ごとに「寄与 <名前>: <ファイル>」の節を置き、寄与の表と節はその下に寄与のファイルから描く。足した列挙の値には、どの寄与が足したかを添える。依存の図(`rulec graph`)は、開いた値を種類 `open` の節として描く。
6. **証明書。** 寄与が足した行に `file`(規則の場所からのパス、SHA-256、寄与の名前)を持たせ、`--rule` の読み戻しは、その行のセルを寄与のファイルから、その SHA-256 に固定して読む。`tools/recheck.py` と Lean の読み手(`RulecCert/Read.lean` の `ReadRow`、`RulecMain.lean` の `checkSpans`)を、行ごとに読むファイルを選ぶように広げた。開いた値は `opens` に書く。`tools/recheck.py` は、項を合わせた区間が、その値の `ranges` の範囲に入ることを確かめ、Lean のプログラムはそれを「述べただけ」として名前を挙げる(新しい定理は書いていない。後の Lean の担当に回す)。
7. **ritsu。** `ritsu check` と `ritsu gen` は、プロジェクトの寄与のファイルを組にする(`Engine.set_project`、`package.rs`)。寄与のファイルにはモジュールを作らない。★ `ritsu run` に、繰り返し書ける `--with <パス>` を足した。フローが呼ぶ規則に合わせる寄与のファイルかディレクトリで、プロジェクトを読まない `ritsu run` でも、どの寄与で走らせるかを選べる。ritsu の README に、寄与の一文と例(テストのプロジェクト `crates/ritsu/tests/projects/product_line` の `ritsu check`)を足した。
8. **寄与と確率**(§15.224〜§15.226)。乱数の入力と確率の主張は、足す先の規則のものとした。寄与のファイルの `inputs` の `random` と、寄与のファイルの `chances` は E067 である。何を引くかは、呼ぶ側のすべてが従う決まりで、主張は、それについての規則の作り手の約束だからである。規則の主張は、寄与を合わせた規則でも確かめる。寄与が足した入力は、主張の表に `-` の列として入り(足した入力がどの値でも主張は成り立つ、という読み方)、主張が書く並びは、合わせた規則にも残す。寄与が答えの確率を変えれば E133 で、主張する出力を決める値に関わる、寄与の表と節を挙げる。規則だけでも同じ確率で破れる主張は、規則の E133 として一度だけ言う。確率を変えない寄与なら、合わせた規則の確率の関数は、寄与が足した入力も引数に取る(`prize_lottery_chances(campaign, holiday)`)。
9. **寄与と取り出し**(§15.223)。寄与の入力は、足す先の規則が宣言した `shape` から `from` で読める。合わせた規則の取り出す関数が、その入力もオブジェクトから読み、`from` の無い寄与の入力は、オブジェクトの後ろの引数になる(`handling_fee_from(order, rush)`)。寄与のファイルは `shape` を書けない(E067)。パスの誤りは、寄与のファイルの行で言う(E121)。
10. **重みで選ぶ fold と寄与**(§15.226)。開いた表や節に寄与が足した行は、重みで選ぶ fold の判定にも、ふつうの表と同じに入る。評価、ベクタ、確率の数え方は、合わせた規則の上でそのまま行う。
**走らせた**(§15.227〜§15.231):
- これまで `apply` と一覧のファイルで書くしかなかった五つの形を、一般の名前で書いた組(`tests/merging/`。足し算、種類ごとの和と積、最大と和、上書きの上の上書き、歩く規則への要素ごとの寄与)で、合わせた規則の答えが、元の一覧のファイル(`apply` と手で書いた式)の答えと、合わせた規則の全部のベクタ(15、26、40、31、19 件、計 131 件)で同じだった(`tests/contrib.rs`)。
- コーパスに、注文の値引き(重ねる値引きの `sum`、重ねない値引きの `max`、上限の `min`)と地域のキャンペーンの寄与、商品の種類(`enums` のファイル)と料金・税・配送の三つの規則と、種類を足す商品ラインの三つの寄与を、英語と日本語の版で足した。どれも検査を通る。商品ラインから料金と税の寄与を外すと、二つの開いた表が E101 で園芸の行を求め、どの寄与が足した値かを「garden is a value garden_kind adds」と言う。`default` を外すと、開いていない配送の表の `-` の行で E033 になる。
- 合わせた十の組(注文の値引きと商品ラインの英日、`tests/merging` の五つ、くじと番号を入れ替える寄与)を `rulec gen` し、PostgreSQL を立てて `rulec test --require-all` に通した。522 の比べ(くじの確率の関数の比べを含む)が全部一致し、飛ばしたものは無い(歩く二つの組では、NumPy と SQL は歩く規則を生成しないと注記する)。
- 合わせた 14 の規則(くじを含む)の証明書が、`tools/recheck.py` と Lean の `rulec-recheck` の両方を通る。Lean は開いた値の範囲を述べただけとして挙げる(「the range of stackable, which its 3 terms put together (sum) stay inside」)。寄与のファイルの行を書き換えると、両方が「that file is another text than the certificate's」で受け付けない。
- 変異を英語と日本語の対で七つ足した(E067、E068、E131、W132、E033 の足した値、E040 の形、E044 の輪)。
- 寄与と確率:くじの規則(表を開いたもの)に、休日に 60〜99 番も三等にする寄与を合わせると、規則だけでは成り立つ二つの主張が E133 になり、寄与の節を挙げる(「With campaign = false, holiday = true, the chance of prize = third is 9/100 (9.0%).」)。三等の番号を入れ替えるだけの寄与なら主張は成り立ち、合わせた規則の確率の関数が休日の入力を取って、`rulec test` で参照と一致する。重みで選ぶ担当の振り分け(判定の表を開いたもの)に、監査の週に担当 1 を外す節を足すと、三つの主張が E133 になり、寄与の例(引いた値 0 と 29 で担当 2、30 で担当 3)は成り立つ。
- 寄与と取り出し:注文の契約から合計を読む規則に、数量の合計を `from sum order.lines of quantity` で読む寄与を合わせると、取り出す関数が `handling_fee_from(order, rush)` になる。パスを `quantty` と書くと、E121 が寄与のファイルの 5 行目で言う。
- 足す前のビルド(§15.226 までの rulec)と、出力を比べた。変えていない 481 本の `.rule` の `check`(英語、日本語、JSON)の 1,443 回で、違ったのは E004 と E005 の注記が `open` を挙げることだけ(rulec の二本と sekisho の二本の変異の 12 回)だった。変えていないコーパスの 133 本の `api`・`certificate`・`doc`(Markdown と HTML)・`graph`・`coverage`・`vectors`・`schema`・`check` の JSON と `gen` の出力の 1,330 回は、一字も変わらない。
- ritsu:テストのプロジェクト `product_line`(料金の規則、種類の `enums` のファイル、商品ラインの二つの寄与、料金を求めるフロー)で、`ritsu check` が規則を寄与と合わせて検査し、`rulec check` に規則のファイルを一度に渡したときと同じことを言う。上乗せの寄与を外すと E101 になり、フローは規則を読めなくなる。`ritsu gen` の Python のパッケージで、園芸の 20 ドルの品が 23 ドルになる。`ritsu run --with rules/contrib` で園芸の品は 23 ドルになり、`--with` が無ければ入力が受け付けられない。
**捨てたもの**:
- **`check` で、合わせた規則の例も走らせる。** 元の規則の例は寄与の無いときの主張で、寄与の例はその寄与と `after` の寄与についての主張である。全部を合わせた規則の上では、どちらも当たるとは限らない。
- **`diff` で寄与を規則に置き換える。** `rulec diff old.rule old.rule` のような、同じファイルを二度渡す比べが一つにまとまってしまう。
**正直な限界**:
- `rulec diff` と `rulec replay` は、合わせた規則の版を比べない。
- 証明書の開いた値の主張(項を合わせた値が範囲に入る)には、Lean の定理が無い。
### 15.232 述べただけだった四つの主張を Lean で証明する:行のセルが狭める範囲、列挙の入力の軸、持ち越す値、区間にまとめた場所(2026-10-14)
**きっかけ**:0.27.0 で足した主張のうち四つを、Lean の `rulec-recheck` は「述べただけ」として名前を挙げて通していた。行のセルが狭める列の範囲(§15.213 の `inside`)、順のある列挙の入力の範囲(§15.210 の `enum_ranges`)、状態のほかに持ち越す値の主張(§15.217・§15.218)、そして持ち越す数を区間に分けて調べたとき、多い辺の上で閉じた場所の集まりが本当の呼び出しでも閉じていること、の四つである。どれも `tools/recheck.py` は確かめていたが、検査が通れば主張が成り立つという定理が無かった。あわせて、列挙の入力の軸が列挙の値と一致することを、Lean のプログラムは前から確かめていなかった。
**決定**:
1. **行のセルが狭める範囲は、Lean のプログラムも行のセルから組み直す。** 自分の表の列を書く行(`| >=1 | units |`)が答えるのは、その行の箱が取る点だけである。その列での行の箱は、行のセルが描く箱である(`checkBoxes` が組み直して確かめている)。だから、行が書く値はセルを満たし、セルが通す区間の中にある。区間は、比べるセルならその端(`>` と `<` は、その名前の格納の刻み `1/scale` の次の点に寄せる。Rust の `row_written` と同じ)、値の集合ならその両端である。`Cells.lean` の `cut_of_box` がこれを言い、`Values.lean` の `eval_mem_hull` と `eval_mem_stated` は、行ごとの区間をこの区間で狭めた形(`Writes` の `cut`)に広げた。`Cells.lean` の `written_mem_hull` は、二つをつないで「表がその列に書く値は、行が答えるどの点でも、組み直した範囲の中にある」と言う。プログラムは証明書の `inside` を、自分で組み直した区間を含むかどうかでだけ読み、含まなければ落とす(`tools/recheck.py` と同じ)。前提は、名前の値がその格納の刻みの整数倍であること(§7.1。`scales` は証明書の言い分である)。
2. **列挙の入力の軸を、列挙の値と範囲に突き合わせる。** 入力が列挙なら、その軸は列挙の値(順のある列挙が範囲を持てば、範囲が通す値)を宣言の順に並べたもので、オプショナルなら先頭に `none` が来る。プログラムは、どの表のどの入力の軸もこの並び(`Cells.lean` の `enumAxis`)と一致することを確かめる。定理 `enumAxis_coords` は、そう並んだ軸には入力がとりうる値の全部に座標があり、どの座標も入口が通す値である、と言う。範囲の中の値に座標が無ければ、どの行にも求めなかった値が残り、範囲の外の値に座標があれば、入口が拒む値で行が当たると言ってしまう。範囲を持つ入力が表の軸でないとき(`member_tier_shipping` の `campaign`)は、軸について言うことが無い。`enum_ranges` を「述べただけ」と挙げる行は無くした。
3. **持ち越す値の主張は、新しい `Carried.lean` で証明する。** 案件のいる所を、状態と持ち越す値の組(数はその値、真偽は 0 と 1、列挙は値の位置)とし、本当に起きる呼び出しを次のように定めた。行の箱がその状態の点を取り、`first` なら前の行の箱はその点を取らない。点の裏には名前の値があり、数の軸の座標はその列の値を含み、入力はどれも宣言した範囲の中にあり、計算する値はどれもその式の値である(式の中の丸めは `RoundsWell` を満たす)。状態は行が書くとおりに動き、持ち越す値は、行が書く値(入力そのもの、行に書いた値、計算した名前の値)を出力が宣言した丸めで丸めた値になる。丸めは、刻みの点で、切り下げと切り上げのあいだにある、とだけ置いた(`roundsTo`)。証明書の場所(状態と、持ち越す値ごとの座標。範囲の外は `-1`)は、値がどれもその座標の中にある組を表す。
- **模倣の定理(`Model.simulates`)**:場所の上の組から本当に起きる呼び出しは、検査がその場所から数える呼び出しのどれかで、行き先の組は数えた行き先の場所の上にある。計算した値は、場所の座標と入力の範囲から計算した区間に入る(`eval_mem_interval`)。丸めた値は、その区間の両端を外へ丸めた区間に入り、丸めの刻みの点である。丸めの刻みは座標の刻みの整数倍で、座標は刻みの上で範囲を隙間なく敷き詰めている(`tilesB`)。だから丸めた値は、どれかの座標に乗るか、範囲の外に出る。行の箱がその場所の値をとりうるか、`first` で前の行がその場所で後の行の全部を覆うかも、この読み方で組み直す。
- **閉じた集合(`Sys.reaches_mem`)**:始まりの場所を含み、数えた呼び出しで外へ出ない場所の集まりは、案件が着ける組を全部含む。値を一つずつ調べたときは場所と組が一つずつ対応し、区間に分けたときは場所が実際より多くを含む。どちらも同じ定理で言える。
- これに乗せて、`Machine.lean` の `final_stays`・`never_after`・`once_below_two` を組の上に広げた(`Sys.final_stays`、`Sys.never_after`、`Sys.once_below_two`)。`always` は「その状態の場所の座標がどれもセルを満たせば、その状態の組の値はセルを満たす」として読む(`Model.always_holds`)。範囲の外の値を持つ組は範囲の主張が言う(E134 と同じ切り分け)。範囲の主張は「`-1` の座標を持つ場所が無ければ、持ち越す数は範囲の中にある」(`Model.range_holds`)。始まりの場所に組が一つも無ければ、どの主張も中身の無いまま成り立ってしまう。プログラムは、始まりの場所が持ち越す値ごとに一つの座標を持ち、どれもその値の座標であることを確かめ(`Model.startOk`)、`Model.start_inhabited` は、そのとき始まりの場所に組が少なくとも一つあると言う。
- プログラムは、証明書の `carried` を `Carried.Model` に読む。座標の敷き詰め、出力の丸めの刻みが座標の刻みの整数倍であること、どの行にもどの値にも書くものがあること、書くものが表の `writes` と `produces`、ファイルのセルと一致すること、を確かめる。場所の集まりと主張ごとの集まりは、定理が言う検査の関数そのもので確かめる。状態だけの節で行に載らなかった主張(`final`、`never`、`once`)は、場所の上に載れば「述べただけ」と挙げない。持ち越す値の項目で、`tools/recheck.py` と同じく、行が書くもののファイルの字を読み戻す。
4. **証明書の持ち越す数の項目に、出力の丸めの刻み `round` を足した。** 次の呼び出しに渡るのは出力の値で、出力は宣言した丸めで丸める(§7.2。数の出力は丸めを必ず持つ。E104)。模倣の定理は、丸めた値がどの座標に乗るかを丸めの刻みで決めるので、刻みが証明書に要る。`round` は証明書の言い分に加わる(`scales` と同じ)。木の規則で証明書が出る 263 本のうち、変わるのは数を持ち越す 6 本(コーパスの `retry_attempts`・`grace_period` と日本語の版、変異の `m_w127` の英日)で、`"round":"1"` が一つ増えるだけである。場所の集まりは一つも変わらない。`round` の無い証明書(0.27.0)では、Lean のプログラムは持ち越す値の主張を「述べただけ」と挙げる。`tools/recheck.py` は値を書かれたまま座標に落とし、そのことを「述べただけ」と挙げる。
**直した欠陥**:
- **証明書の場所の集まりが、出力の丸めを見ていなかった。** Rust の `carried_json` と `tools/recheck.py` は、行が書く値の区間を、丸めずに座標に落としていた。丸めで値が動く規則では、場所の集まりが本当の呼び出しで閉じていなかった。数を毎回半分にして `round down(1)` で丸める規則(`define half : number = units * 50%`、始まりは 10)では、証明書の場所は 10 と 5 の二つだけだった。5 から計算した 2.5 はどの座標にも乗らず、行き先が無いことになっていたが、本当は 2、1、0 と進む。`check`(`machine.rs`)は丸めた値で調べるので、`always units >=5 when running` を足せば E135 になり、証明書は出ない。主張の無い規則では証明書が出て、`tools/recheck.py` はそれを通していた。Rust と Python も、行が書く値(入力そのもの、行に書いた値、計算した値の区間)を丸めの刻みで外へ丸めてから座標に落とすようにした。この規則の証明書は 12 の場所を持ち、二つの再検査を通る(丸め方で 2.5 が 2 か 3 かは決めないので、本当には着かない場所も含む)。丸めの刻みと座標の刻みが同じで、値がいつも刻みに乗る規則(木の全部)では、場所の集まりは変わらない。
**捨てたもの**:
- **五つの丸め方を、Lean で一つずつ書くこと。** 丸めは「刻みの点で、切り下げと切り上げのあいだ」とだけ置いた。証明書も丸め方を書かずに、両端を外へ丸める(`cert_interval` と同じ)。丸め方を見ないので場所は増えることがあるが、減ることは無い。
- **座標を、値の刻みで内へ寄せて読むこと(`closed_at`)を Lean でも使うこと。** 寄せるには、軸の列の値がその刻みに乗っていることを、式から求める刻みの定理として持たなければならない。Lean のプログラムは、軸の座標をその両端を含む区間として読む。行の箱が持ち越す値の座標をとりうるかと、`first` の覆い方では、場所の端が軸の刻みに乗っていれば、読み方の差は出ない。行の箱で計算した値を狭める所では、Lean の区間のほうが広くなることがある。広く数えたときは「閉じていない」と言って落ちる側で、通してしまう側ではない。木の規則では、どちらの差も出なかった。
- **行のセルの区間の刻みを、式から求める刻み(`tools/recheck.py` の `column_grid`)で取ること。** 式から求める刻みが値を刻むことは、定理にしていない。格納の刻み(Rust の `row_written` と同じ)にした。
- **持ち越す値のある機械で、終われることを証明すること。** 終われることは、本当に起きる呼び出しで終わりの状態まで行けるという主張で、場所が実際より多くを含むことは、この主張の助けにならない。値を一つずつ調べたときに、呼び出しの並びを点と値ごとに証明書に載せれば示せる。ただ、計算した値の丸めの向きが決まらないと、本当に起きる値を Lean のプログラムが組み直せない。二つの再検査とも、これまでどおり「述べただけ」と挙げる。
**正直な限界**:
- 持ち越す値のある機械で終われることは、上のとおり述べただけのままである(§15.233 で、値を一つずつ調べた機械について証明した)。
- `once` の行が呼び出しを数えるかは、行が書く値(証明書の `once[].rows[].value`)から決める。その値は、二つの再検査ともファイルの字と突き合わせていない(`tools/recheck.py` が突き合わせるのは字だけである)。状態だけの `once` も同じで、前からある抜けである(§15.233 で、その行の答えのセルの字と突き合わせた)。
- 本当に起きる呼び出しは、`constraint` を満たすことを求めていない(入口が `constraint` を破る入力を拒むことは、定理に入れていない)。数える呼び出しが増えるだけで、閉じた集合の主張には安全な向きである。
- 始まりの場所の座標(`start`)は、状態の `initial` と同じく証明書の言い分で、規則の `initial` の行とは突き合わせていない(§15.233 で突き合わせた)。
**確かめたこと**:
- 木の `.rule` で証明書が出る 263 本が、`tools/recheck.py` と `rulec-recheck` の両方を通る。「述べただけ」の行が消えたのは 12 本である。行のセルが狭める範囲が 2 本(`average_unit_price_split`、`平均単価の場合分け`)、列挙の範囲が 2 本(`member_tier_shipping`、`会員段階の送料`)、持ち越す値の主張が 8 本(コーパスの 6 本と `m_w127` の英日)。一度だけの特典の英日は、状態の行に載らなかった `once` の行も場所の上で証明され、「every claim this program states was proved」になった。持ち越す数のある 6 本に残るのは、終われることの一行だけである。
- 一か所ずつ崩した証明書は、Lean のプログラムが落とす(`tests/lean.rs`)。`inside` を狭める、列の範囲を狭める、`inside` を消す。`enum_ranges` に範囲の外の値を足す、`enum_ranges` を消す、列挙の軸の値を並べ替える。場所を一つ落とす、行が書く値の座標、`always` の境、始まりの場所、範囲、`round` を 2 にする、座標の刻み、`never` の組を一つ落とす、`-1` の場所を足す、始まりの座標を空にする、座標の無い場所を足す、行が書く値のファイルの字、`once` の組の数え方。`round` を消した証明書は、落とさずに「述べただけ」と挙げる。
- `tests/machine.rs` の規則も通した。売り手と市場の関係の規則(場所 53)、0〜10,000,000 から 2 ずつ減らす規則(区間に分けた場所 10。`always left not: 1` は区間の上で破れるので、これまでどおり「述べただけ」)、金額・率・列挙・真偽を一度に持ち越す `policy first` の規則(場所 31)、二つの真偽と二つの `once` の行を持つ規則(場所 12)。どれも Lean のプログラムで 0.01 秒だった。
- `#print axioms` で、足した定理(`cut_of_box`、`written_mem_hull`、`enumAxis_coords`、`Carried.Model.simulates`、`Carried.Sys.reaches_mem` ほか)が立つのは `propext`・`Classical.choice`・`Quot.sound` だけである(`tests/lean.rs`)。Lean の層の宣言 6,307 個(定理 2,525 個)も同じだった(`crates/ritsu-model/tests/proofs.rs`)。足した Lean は、`Carried.lean` が 1,569 行、`Cells.lean` と `Values.lean` が合わせて 395 行(置き換えた 34 行を含まない)、`RulecMain.lean` の読み手と検査が 428 行(66 行を置き換えた)である。
### 15.233 第 3 段で述べただけだった主張を Lean で証明する:最大と最小と fold の答えの範囲、前提のつながり、持ち越す値のある機械で終われること。始まりと答えのセルの読み戻し、範囲が外す列挙の値と W111(2026-10-14)
**きっかけ**:第 3 段で足した主張のうち三つを、Lean の `rulec-recheck` は「述べただけ」として名前を挙げて通していた。`max`・`min` の集計と、後ろの項目が読む fold の答えの範囲(§15.220)、前提のつながりの塊の三つの主張(§15.222)である。第 2 段から残っていたものも二つある。持ち越す値のある機械で終われること(§15.232 で捨てた)と、`once` の行の値と始まりの値をファイルと突き合わせていないこと(§15.232 の正直な限界)である。終われることのほかは `tools/recheck.py` が確かめていたが、検査が通れば主張が成り立つという定理が無かった。あわせて、順のある列挙の入力に `range` を書くと、範囲の外の値が W111 で「どの行にも現れない」と言われていた。その値はどの呼び出しでも来ないので、行に書く必要は無い。
**決定**:
1. **最大と最小の集計の範囲**(`Values.lean`)。`max` の答えは、`empty` の値から始め、`where` を通った最初の要素の値で置き換え、それからは勝つ要素の値だけで置き換えた値である(`extremeOf`。生成コードと同じ手順で、どちらが勝つかは比べ方に任せた)。答えは `empty` の値か、通った要素のどれかの値なので、列の区間と `empty` の値の外側に入る(`extreme_mem_hull`)。プログラムは、証明書が `ranges` に書く範囲がその外側を含むことを確かめ(`extremeOk`)、`extreme_mem_stated` がそれで何が言えるかを言う。列の区間は、ほかの名前と同じく、要素ごとに計算する値なら式から、表が書く列なら行が書くものの外側から、要素のフィールドなら宣言した範囲から取る(`Worked.rangesOf`)。順のある列挙の `max`・`min` は、`empty` がその列挙の値であることと、範囲を書いていないことを確かめる(`tools/recheck.py` と同じ)。
2. **fold の答えの範囲**(`Values.lean`)。fold は要素ごとに、何もしない(`next`、取らなかった `take_first`・`keep_max`)、式の値を持つ(取った `take_first`・`take_last`・`keep_max`)、止まる(`stop with` の値か、使い切ったときの答えで)のどれかをする(`FoldStep`)。要素が無ければ `empty` の値、使い切れば持っている値(何も持っていなければ `empty` の値)か `exhausted` の値になる(`foldAnswer`)。だから答えは、証明書の `answers` のどれかの式の、呼び出しかどれかの要素での値である。`foldAnswer_mem_stated` は、範囲がどの式の区間も含めば(`writesWithin`。表が書く列と同じ検査)、どの答えも範囲に入ると言う。日付を書いた答え(`empty -> 2027-12-31`)は日の番号として読む。あわせて、答えを軸に読む表の刻みは、答えの式が共有する刻みにした(最大と最小は、列の刻みと `empty` の値が共有する刻み)。これまでは `scales` に答えの名前が無く、刻みを「述べただけ」と挙げていた(`tests/stages/best_offer.rule` と日本語の版)。`tools/recheck.py` の `column_grid` も同じにした。
3. **前提のつながり**(新しい `Prereq.lean`)。塊は、`start` の値と、値ごとの `requires` の行(グループのリスト。グループは全部要り、グループの中はどれか一つでよい)と、手に入らない値である(`Graph`)。案件は `start` の値を最初から持ち、まだ持っていない値のうち、どのグループにも持っている値があるものを一つずつ取れる(`Graph.available` は生成する `<名前>_available` と同じで、一つずつ取っていくことを `Graph.Holds` で表す)。手に入らない値は、何も入らない空のグループを一つ持つ(生成する `<名前>_requires` が返す `[[]]` と同じ)。
- (a) `topological` が、どの値もそれが要るもの全部(`or` の中も)より後に置けば(`topoOk`)、要るものをどこまでたどっても自分に戻らない(`no_cycle`)。
- (b) `reached` が、どの値も一度だけ並べ、`start` の値か、各グループの値を `start` か自分より前に持つ値だけを並べていれば(`reachedOk`)、`start` から始めてその順に取っていく案件は、その全部を持つ(`reached_holds`)。プログラムは、`reached` と `never` が列挙の値をちょうど一度ずつ並べることも確かめるので、手に入らない値のほかは全部取れる。手に入らない値は、どの案件も持たない(`never_held`)。
- (c) `ordered by` の表の答えは、プログラムが表の行から読む。値の座標を取る最初の行が書くもの(順のある列挙なら `produces` の語の順位、数なら `writes` の数)である。証明書の `values` がそれと一致し、`never` でない値への辺のどれでも答えが下がらなければ(`orderOk`)、要るものをどこまでたどっても答えは下がらない(`order_along`)。表がその値に答えるのがその行であることは、表自身の証明書(`unique` と完全性)が言う。
4. **持ち越す値のある機械で終われること**(`Carried.lean`)。§15.232 でこれを捨てた理由は、計算した値の丸めの向きが決まらないと、本当に起きる値を組み直せないことだった。丸めがどれも、すでに刻みに乗った値に当たるなら、どの方式もその値を動かさない(`rounds_onGrid`)。`evalExact` はそういう評価で、刻みから外れた値の丸めに当たれば答えない。`evalExact_sound` は、答えたときの値が、`RoundsWell` を満たすどの丸めでも `eval` の値であることを言う。これで、本当に起きる呼び出しを定めた(`Model.fires`)。点は規則が尋ねられる点で、その状態で行の箱が取り、`first` なら前の行は取らない。値を持つのは入力と規則が計算する値だけで、表の列はどれも入力か、その点で数を表す計算した値である。数の座標は列の値を含み、入力は宣言した範囲と `constraint` の行を守る(入口が通すものである)。計算した値は、どの丸めでも式の値である。持ち越す値は、行が書く値そのものに移る(出力が丸める刻みに乗っているので、どの方式でも動かない)。
- 証明書の `carried` に `finish` を足した。値を一つずつ調べた機械(場所の一つが組の一つを表す)に終わりの状態があるとき、終わりの状態でない場所ごとに、終わりの状態までの呼び出しの並びを書く。呼び出しは、行と点と、数の入力の値(日付は日の番号)である。プログラムは呼び出しを一つずつ走らせ(`Model.callOk`。計算した値を入力から組み、上のことを全部確かめ、持ち越す値の次の値を出す)、`Model.fires_of_callOk` がそれを本当に起きる呼び出しにする。`Model.finishes_of_pathOk` は、確かめた並びが終わりの状態に着くことを、`Model.reachable_finishes` は、閉じた場所の集まりと合わせて、案件が着けるどの組からも終わりの状態に行けることを言う。`Model.call_of_fires` は、本当に起きる呼び出しが、場所の集まりが数える呼び出しの一つであることを言う。
- rulec は、機械の分析が見つけた本当の呼び出し(全部の入力を持つ)から並びを作る。並びのどれかの呼び出しが、刻みから外れた値を丸めるか、持ち越す数を出力の刻みの外の値にするか、表が入力でも計算した数でもない列を読むときは、並びを載せず、これまでどおり `uncertified` に挙げる。区間で調べた機械(`m_w127`)も、場所が多くの組を表すので載せない。そこでは `check` 自身も終われるかを決めていない(W127)。
5. **始まりと答えのセルを、ファイルから読み戻す**(`RulecMain.lean`、`tools/recheck.py`)。
- 機械に `initial_source`(`initial` の行が状態を書く場所)を、持ち越す値の項目に `initial_source`(その値の `initial` の行の値の場所)を足した。再検査は、そこの字が状態の名前(持ち越す値の値)であることと、その行が `initial`(と持ち越す入力の名前)で始まり、値の後ろには注釈しか無いことを確かめる。持ち越す値なら、書いた値が始まりの場所の座標に入ることも確かめる。
- 機械の行き先、持ち越す値の行き先、`once` の行が読む値の `source` は、これまで字だけを比べていた。同じ字の別のセル(ほかの行の同じ答え)を指しても通った。いまは、その行を書いたファイルの行の、その出力の列の答えのセルであることを確かめる。`once` の行が読む値は、そこの字と一致すること(真偽と語は字そのもの、数は字の数に `test.scale` を掛けた整数)も確かめる。
- 表の行が `produces` に書く語(列挙の値と真偽)は、どちらの再検査も読み戻していなかった。行き先、`once`、`ordered by` の答え、上の表の事実は、これに寄りかかっている。いまは、一か所に書いた表(いくつもの表をまとめた集まりでないもの)で、語を書く列の答えのセルがその語であることを確かめる。
- `once` の行の `test` の `words` は、列挙の値(真偽なら `false` と `true`)のそれぞれにセルを当てて、セルが受け付ける語を全部書くようにした。これまでは、語一つと語の集合と順の比べだけを書き、`not:` の語は空の集まりになって、再検査がどの行も数えなかった。数のセルが比べでないとき(`100円`、`100円, 200円`)は区間で書けないので、その `once` の行は表の行に載せず、`uncertified` に挙げる。数の比べには `scale`(やりとりする整数のいくつで一になるか)を足した。
6. **範囲が外す値と W111**(`types.rs`)。順のある列挙の値で、その型を持つ名前がどれも、その値を `range` で外した入力であるものは、W111 の「どの行にも現れない値」に数えない。どの呼び出しもその値を持ってこないからである。その型の名前に、範囲の無い入力、要素のフィールド、出力、計算する値が一つでもあれば、その値は来うるので、これまでどおり言う。範囲の外の値を表のセルに書いたときの扱いは変えていない。範囲の外の値だけを書いたセルの行は、どの入力も当たらないので E102 である。範囲の中の値と並べて書いた集合(`basic, silver`)は、範囲の中の値の集合として何も言わずに通る。数の範囲の外の値を集合に並べたとき(`range <=100` に `1, 150`)と同じである。台帳の W111 の「いつ出るか」に一文を足した。
**直した欠陥**:
- `tools/recheck.py` の `check_prerequisites` は、`ordered by` の表の答えを、数を書く表でも `produces` から読んでいた。数を書く表の `produces` は `null` なので、数で順を決める塊の証明書を受け付けなかった(`FAILED prerequisites research: the answers of tier_of are not the ones its rows write`)。コーパスには数で順を決める塊が無く、見つからなかった。いまは `writes` の数を読む。答えの無い値への辺も、黙って飛ばさずに落とす。
- 上の 5 の三つ。行き先と `once` の値の字だけの比べ、`produces` の読み戻しの抜け、`once` の `words` の書き方である。
**捨てたもの**:
- **丸めの五つの方式を Lean に書くこと**(§15.232 と同じ理由)。どの方式でも同じ値になる呼び出しに限り、方式によって値が変わる並びは載せない。
- **区間で調べた機械の終われること。** 場所の一つが多くの組を表し、組ごとの並びは書ききれない。`check` 自身も決めていない(W127)。
- **前提のつながりの塊の行(`start`・`requires`・`requires nothing`)と、`once` の行のセルを、ファイルから読み戻すこと。** 行の字句を読むことになり、再検査が rulec と同じ読み方を持つことになる。式や `constraint` と同じく、証明書の言い分のままにした。
- **表が入力でも計算した数でもない列を読む機械で、並びを載せること。** 上の表が決める列、計算した真偽、歩きの答えの値は、呼び出しの入力からは組めない。
- **範囲の外の値を書いたセルがあっても、W111 で言わないこと。** 範囲の外だけのセルは E102 が言う。
**正直な限界**:
- 終われることは、値を一つずつ調べた機械で、並びの呼び出しがどれも刻みに乗った値だけを丸めるときにだけ証明する。それ以外(区間で調べた機械、半分にして丸める規則)は、これまでどおり「述べただけ」と挙げる。
- 本当に起きる呼び出しは、表の列が入力と計算した数だけのときについて定めた(`Model.fires` がそれを求める)。
- `extremes` の列と `where`、`fold_answers` の式、前提のつながりのグラフ、`once` の行のセルは、式と同じく証明書の言い分である。
- `produces` の読み戻しは、一か所に書いた表だけである。いくつもの表をまとめた集まりでは、行の答えのセルが `decides` の順に並ぶとは限らない。
**確かめたこと**:
- 木の `.rule` 462 本のうち証明書が出る 277 本が、`tools/recheck.py` と `rulec-recheck` の両方を通る。「述べただけ」の行が消えたのは 18 本で、どれも「every claim this program states was proved」になった。最大と最小が 6 本(`order_settlement`・`rate_adjustment`・`earliest_due` と日本語の版)、fold の答えが 4 本(`order_settlement`・`tests/stages/best_offer.rule` と日本語の版。答えを読む表の刻みの行も消えた)、前提のつながりが 6 本(`course_catalog`・`shared/certifications.rule`・変異の `m_w130` と日本語の版)、終われることが 4 本(`grace_period`・`retry_attempts` と日本語の版)である。持ち越す値のある機械に残るのは、区間で調べた `m_w127` の英日の、終われることの一行だけである。宿泊の予約の英日に残る終われることの行は、`held` の入力をほかの列も読む機械のもので(§15.149)、この節の外である。
- 一か所ずつ崩した証明書は、二つの再検査が両方とも落とす(`tests/lean.rs`、`tests/walks.rs`)。最大の範囲を狭める、`empty` の値を動かす、fold の答えの範囲を狭める、読めない答えを足す、`max` を `walks` から外す。値を二か所に書く、前提より前に来るトポロジカルな順、前提の輪、前提を持つ前に取る順(二通り)、表が書かない答え、表の行が書く語、数で順を決める表の答えと、行が書く数。並びの呼び出しの行、入力の値、座標の外の値、終わりに着かない並び、場所の並びを落とす、範囲の外の入力、`first` で前の行が取る点。始まりの行と状態、`once` が読む値、持ち越す値の始まり、`once` の値と行き先を別の行のセルで読む、行が書く語。
- 計算した数を列に読む `policy first` の表で、列に無い入力から計算する機械(`tests/lean.rs` の `LOAN`)も、二つの再検査とも終われることを証明した。半分にして切り下げる機械(`HALVING`)は、5 の半分の 2.5 が方式で分かれるので並びを載せず、二つとも「述べただけ」と挙げる。
- 証明書が変わる規則は、木の 277 本のうち 20 本で、どれも機械の規則である。`initial_source` が 20 本、持ち越す値の `initial_source` が 8 本、`once` の数の比べの `scale` が 8 本に増え、値を一つずつ調べた 4 本には `finish` が増えて `uncertified` の終われることの一行が消えた。`once` の `words` の書き方を変えても、木の証明書は一字も変わらない(木の `once` の語のセルは一語だけである)。`check --format json` の出力は、木の 462 本のどれも変わらない。
- W111:範囲が外す値には言わず、範囲の無い同じ型の入力があれば言い、範囲の外だけのセルの行は E102、範囲の中と外を並べた集合は何も言わない(`tests/enums.rs`)。
- `#print axioms` で、足した定理(`extreme_mem_stated`、`foldAnswer_mem_stated`、`rounds_onGrid`、`evalExact_sound`、`Prereq.no_cycle`、`Prereq.reached_holds`、`Prereq.never_held`、`Prereq.order_along`、`Carried.Model.call_of_fires`、`Carried.Model.fires_of_callOk`、`Carried.Model.finishes_of_pathOk`、`Carried.Model.reachable_finishes`)が立つのは `propext`・`Classical.choice`・`Quot.sound` だけである(`tests/lean.rs`)。Lean の層の宣言 6,654 個(定理 2,687 個)も同じだった(`crates/ritsu-model/tests/proofs.rs`)。足した Lean は、`Values.lean` が 464 行、`Carried.lean` が 593 行、新しい `Prereq.lean` が 289 行、`Read.lean` が 28 行(17 行を置き換えた)、`RulecMain.lean` の読み手と検査が 456 行(39 行を置き換えた)である。
### 15.234 呼び出し側のオブジェクトから読む値を、規則が受け取る型どおりに確かめる(2026-10-15)
**きっかけ**:作例のゲームの規則を、生成した取り出し関数で呼ぶと、オブジェクトの値の型の誤りが、答えになって返ってきた。`<規則>_from` は、オブジェクトから読んだ値を、言語の読み方のまま規則の型に直していた。替える前のビルドで、コーパスの `order_handling_fee` の `order_handling_fee_from` に、明細の `amount_usd` を `1.5`・`true`・`"7"` で渡すと、Python は `_int(…)` がそれぞれ 1・1・7 にして、規則は答え(5USD)を返した。規則の関数に `1.5` を直に渡せば `RuleInputError: total is not an integer: 1.5` で止まる。Ruby の `.to_i` と PHP の `(int)` は `"abc"` を 0 にし、TypeScript と JavaScript の `BigInt(…)` は文字列と真偽を数にした(`1.5` は `RangeError` で止まった)。Python は、`where` が読む真偽を `if _e["fragile"]` と書いて、真偽の判定のまま読んでいた。`where` の日付は文字列のまま比べていたので、`ships_on` が `"zzz"` の明細も `>= 2026-01-01` に当てはまった。`.proto` の列挙のフィールドを番号で書いた明細(protojson の書き手は番号でも書ける)は、`where` に当てはまらないものとして読まれ、壊れ物の明細の送料が普通の明細の送料になった。§15.205 は、取り出し関数の数の読み方が言語ごとに違うことを「見つけて直していないもの」に挙げていた。
**決定**:
1. **読む値の一つ一つを、規則がその値を受け取る型どおりに確かめる。** 入力に読む値、`sum`・`max`・`min` が読む要素の値、`elements … from` の要素のフィールドは、規則の型で確かめる。数(お金・量・率の刻みも)は JSON の整数か、値が整数の小数で、§15.205 の入口と同じである。`.proto` の契約からは、protojson が整数を書く 10 進の文字列も受け付ける(protojson は 64 ビットの整数を文字列で書き、どの整数も文字列で読める)。真偽は JSON の `true` か `false`、文字列は文字列、日付は暦にある日の `YYYY-MM-DD` の文字列、列挙は列挙の値の名前である。`where` が読む値は、契約がそこに置く型で確かめる(真偽、整数、数、文字列。日付を比べるセルなら日付)。日付は、確かめたうえで日の番号にして比べる。
2. **合わない値は、規則の関数と同じ種類のエラーで止め、文はパスを規則の書き方で言う。** Python なら `RuleInputError("order.lines[].amount_usd is not an integer", _repr(v))` で、表示は `order.lines[].amount_usd is not an integer: 1.5`、日本語で生成すれば `order.lines[].amount_usd が整数ではありません` である。文は、入口が言う文(`door.rs`)の名前の所にパスを入れたもので、五つの言語で同じである。値は、言語のエラーが値を持てる所で添える(Python は `repr()` の文字列、TypeScript・JavaScript・Ruby は値そのもの)。PHP の `RuleInputError` は値に整数しか持てないので、§15.204 の runner と同じく添えない。途中の場所がオブジェクトでない(`order.lines[] is not an object`)、値が無いか `null`(`order.lines[].fragile is missing`)、コレクションが配列でない(`order.lines is not a sequence`)ときも、同じに止める。`door.rs` に、オブジェクトでない場所の文と、数でない値の文を足した。
3. **`.proto` の契約は、protojson の形のとおりに読む。** 無いフィールドと `null` は、protojson の仕様のとおり既定値として読む(`_proto` は前からそう読んでいた。並びの要素のフィールドを読む `_pget` も同じにした)。途中の場所が、無いのでも `null` でもなくオブジェクトでもなければ、パスを言って止める。前の `_proto` は、それも無いものとして既定値を読んでいた。列挙のフィールドは値の名前の文字列で読み、番号は受け付けない(下の「正直な限界」)。
4. **五つの言語で、同じ順に読む。** 入力は宣言の順に、次に並びを読む。コレクションは要素ごとに、`where` が読む値を読み、当てはまれば、その要素の、合計・最大・最小が読む値を読む。Python の内包表記がこの順で、TypeScript と JavaScript は `flatMap`、Ruby は `flat_map`、PHP は `array_map` と `array_merge` で同じ順にした。前は、Python だけが要素ごとに読み、ほかの四つは、`where` を全部の要素で読んでから値を読んでいた。`any` と `all` は、答えが決まったあとも全部の要素を読む(Python は `_any([…])` と `_all([…])`、ほかは `reduce`)。同じオブジェクトは、どの言語でも同じ値で止まる。
5. **読む手続きは、規則が使うものだけを補助関数として書く。** Python では、`_at`(オブジェクトのキーの値)、`_int_at`・`_bool_at`・`_str_at`・`_date_at`・`_num_at`・`_seq_at`(キーの値を型どおりに)、すでに読んだ値を確かめる `_of` の形、`_int_pb`(`.proto` の整数)、`_enum_of`、`_opt`(省略できる入力。前の `_dig`)、`_pget`(`.proto` の並びの要素のフィールド)である。TypeScript と JavaScript は同じものを camelCase で、Ruby はモジュールのメソッドとして(`.rbs` に署名を足した)、PHP は名前空間の関数として書く。補助関数には規則の名前が入らないので、組み込み関数はそのまま呼ぶ(§15.207)が、拒む値は、モジュールのほかの拒否と同じく `_repr(…)` で渡す。Python の補助関数は、オブジェクトを確かめる前に `cast` で `dict[str, Any]` と `list[Any]` として見るので、`mypy --strict` にも `pyright` の strict にも何も言われない。前からある `_proto` も同じ形にした。
**捨てたもの**:
- **読む値を規則の関数にそのまま渡し、規則の入口に確かめさせる**(§15.203 の 4 の真偽の形を、全部の型に広げる形)。合計・最大・最小が読む要素の値は、規則の入口に届かない。入口が見るのは合計だけである。入口の文は入力の名前を言い、オブジェクトのどこにあった値かを言わない。
- **要素の番号をパスに入れる**(`order.lines[2].amount_usd`)。五つの言語で番号を数えて回す手間に見合わない。`[]` のパスと添えた値で、どの要素かは分かる。契約の検査の文(E120〜E122)も、要素の中のパスを `[]` で書いている。
- **`where` の値を、セルの値の種類だけで確かめる**(整数を書いたセルなら整数)。`number` の契約のフィールドを整数のセルで比べると、`2.5` を整数でないとして止めてしまう。契約がそこに置く型を読んで決める(`projection::where_kinds`)。
- **契約から `TypedDict` を作って、取り出し関数の引数の型にする。** 領域のモデルは作らない(§15-6)。取り出し関数の引数は、これまでどおり `dict[str, Any]` である。
- **`any` と `all` を、答えが決まった所で止める。** 止まる所が言語ごとにずれ、後ろの要素の型の誤りを、ある言語は通し、ある言語は止める。
**変わること**:
- `from` を持つ規則の、五つの言語の生成物の取り出し関数と、その上に書く補助関数。ほかの規則の生成物は変わらない。
- 前は答えを返していた値が、`RuleInputError` で止まる。整数でない数、JSON Schema の契約から来る文字列の数、真偽でない値、無いフィールド、日付でない文字列、`.proto` の列挙の番号、オブジェクトであるべき場所のオブジェクトでない値である。TypeScript と JavaScript で `RangeError` だった値も `RuleInputError` になる。
- 入力に読む真偽は、入口の文(`express is not a boolean`)ではなく、パスの文(`quote.express is not a boolean`)で止まる。§15.203 の 4 を、この節で置き換えた。
- Python のモジュールの `from typing import` に `cast` が、列挙を読む取り出し関数を持つ規則では `Callable` と `TypeVar` も増える。
**確かめたこと**:
- `tests/projection.rs` に二本を足し、一本の名前を変えた。
- 「読む値は型どおりに確かめ_五つの言語で同じ文で止まる」:コーパスの英語の `order_handling_fee` と、日本語の版 `注文の取扱手数料` に、12 通りずつの注文を渡す。例の注文、値が整数の小数で書いた注文、金額が `1.5`・`true`・`"7"` の明細、`fragile` が数の明細と無い明細、個数が数でない明細、明細が配列でない注文と無い注文、明細がオブジェクトでない注文、型の違う値が二つある注文である。五つの言語が、同じ答えを返すか、同じ文で止まる。読む順を確かめる規則(`where` の真偽で絞った最大と、日付の `where`)にも 4 通りを渡す。
- 「protoの値も型どおりに確かめる」:`.proto` の規則(`出荷の送料`)に 7 通りの要求を渡す。例の要求、数でない文字列と真偽の int64、規則に無い列挙の値、文字列でない列挙のフィールド、メッセージの場所の文字列、`null` の任意のフィールドである。
- 「whereの日付は文字列として比べる」を「whereの日付は暦の日として比べる」に改めた。
- 前からある射影の試験 15 本(五つの言語で同じ答え、protojson の形、日付の射影の 264 件など)も通る。
- `tests/boolean.rs` の射影の真偽の試験を、パスの文に合わせた。`tests/error_value.rs` は、モジュールの中の拒否が、補助関数のものも含めて `_repr(…)` を渡すことを確かめる。
- 型と整形:`tests/threeway.rs` の `mypy --strict`、`tsc --strict`(143 本の規則の 429 のファイル、7.0.2)、steep、ruff と gofmt の整形が通る。`from` を持つコーパスの 8 本のモジュールを `pyright` の strict(1.1.414)にかけると、替える前の 74 件が 50 件になった。残った 50 件は前からある使われない丸めの補助関数で、消えた 24 件は `_proto` と `_dig` の型である。
- 替える前(1e7d646e)のビルドと比べた。木の `.rule` 544 本の `check --format json`(英日)と、コーパスの 143 本の `api`、`doc`(Markdown と HTML)、証明書、`gen` の全部のファイル(英日)を、両方のビルドで 5,036 回走らせた。書いた 22,580 のファイルのうち、違ったのは 268 だった。内訳は、`from` を持つ 8 本の五つの言語の生成物と、それを埋め込んだページ(この節)、率の範囲を書く 10 本のページ(§15.241)、fold の答えを `walks` に足した 2 本の証明書(§15.236)、W121 の変異 2 本の `check`(§15.239)である。`rulec api` と、Go・Swift・Java・Rust・SQL・NumPy・Wasm の生成物は、一字も変わらない。
**正直な限界**:
- 確かめるのは型で、範囲ではない。入力の範囲は規則の入口が確かめ、要素のフィールドの範囲も、要素の入口が確かめる。合計・最大・最小が読む要素の値の範囲は確かめない。それは契約の検査(JSON Schema、Protovalidate)が呼び出し側で確かめることで、`check` は、契約の範囲と入力の範囲の食い違いを E122 で言う。
- `.proto` の列挙のフィールドの番号は受け付けない。protojson の読み手は番号も読むが、取り出し関数は値の名前で比べる。番号から名前への表を持ち込むことになり、protojson の書き手の既定は名前なので、要るとはっきりしてから考える。
- PHP は JSON を連想配列で読むので、空のオブジェクト `{}` と空の配列 `[]` を区別できない。空のオブジェクトを並びとして読み、空の配列をオブジェクトとして読む(そのあとのキーが無いので「がありません」で止まる)。
- 取り出し関数は、読む値だけを確かめる。規則が読まないフィールドの誤りは、契約の検査の仕事である。
### 15.235 寄与の検査では、元の規則が引くファイルを元の規則の場所から引く(2026-10-15)
**きっかけ**:寄与のファイルを元の規則と一緒に検査すると、元の規則は通り(`ok rules/fee.rule (1 contribution: mods/m/extra.rule)`)、寄与のファイルの検査が、元の規則の `shape` のパスを寄与のファイルのあるディレクトリから引いて、E013 で止まった。同じ位置の `import enum … from` は正しく引けていた。直すときに確かめると、元の規則の `import proto` も(E013)、元の規則の入力の `range from koyomi` も(E130)同じに止まった。寄与の検査(§15.231)は、元の規則に寄与を合わせた規則を、寄与のパスのまま検査する。元の規則が引くファイルのパスは、元の規則のある場所からのものなのに、寄与の場所から引いていた。寄与が足す入力の `range from koyomi` は逆で、元の規則の側の検査では、寄与のパスを元の規則の場所から引いていた。
**決定**:
1. **引くファイルの宣言に、そのパスを引く元のファイルを持たせる。** `shape`、`import proto` と `import jsonschema`、`range from koyomi` の宣言(`ShapeDecl`、`EnumImport`、`DaysFrom`)に `base` を足した。`source` の宣言は、`apply` が持ち込む宣言のために、前から持っている。無いときは、検査しているファイルから引く。
2. **寄与の検査では、元の規則の宣言の `base` を元の規則にする**(`link::scope` の `cites_from`)。`shape`、引く列挙、入力と要素のフィールドの `range from koyomi`、`source` の全部である。`source` は、`apply` が持ち込むものと同じく、出典のコピーとの突き合わせを寄与の検査から外す。突き合わせるのは元の規則の検査で、寄与の検査は元の規則が通ることを前提にしている(通らなければ E067)。
3. **寄与が足す入力の `range from koyomi` は、寄与のファイルから引く**(`link::merge_into`)。元の規則を寄与と合わせて検査するときも同じである。
4. 読む側は、どれも `base` から引く(`projection::check`、`proto_paths`、`contract_certificates`、`enums::check`、`types` の koyomi の読み込み)。
**捨てたもの**:
- **寄与の検査のパスを、元の規則のパスにする。** 寄与の行についての診断の場所も、元の規則のものになってしまう。
- **元の規則のパスを、寄与の場所からの相対パスに書き直す。** 診断の文に、書いていないパスが出る。
**確かめたこと**:
- `tests/contrib.rs` に一本を足した。元の規則が `shape` と `import proto` を引く寄与を CLI で、koyomi の日付を引く寄与を koyomi の口で検査する。元の規則の入力の範囲を引く向きと、寄与が足す入力の範囲を引く向きの両方である。替える前のビルドでは、`shape` と `import proto` が E013、koyomi の範囲が E130 で止まった。
- 再現(元の規則が `shape` を引き、寄与が別のディレクトリにあるもの)は、`ok mods/m/extra.rule (contributes to rules/fee.rule)` になる。
### 15.236 歩きの答えを読む表は、その答えの範囲の上で検査する(2026-10-15)
**きっかけ**:作例のゲームの勝ち負けの規則で、名前を付けた fold の答え(0 か、1 から 4)を読む表に、−1 の行が求められた(E101、`first = -1`)。reference §6.2 は「後ろの項目が読む数の答えは、fold が答える値の外側を範囲にする」と書いている。型の検査は、その範囲を求めて `ranges` に入れ、証明書もそれを述べ、Lean の `foldAnswer_mem_stated` が証明している(§15.233)。ところが表の完全性の検査(`region`)は、数の列の軸を、入力の宣言した範囲、開いた値の範囲、`count`・`sum`・`max`・`min` の範囲からしか取らず、fold の答えの軸は数の全体だった。日付の列では、`max` と `min` の範囲も使っていなかった(明細の出荷日が 2026 年の中に限られ、`empty` も 2026-01-01 なのに、最も遅い出荷日を読む表に `latest = 2025-12-31` の行を求めた)。§6.3 の「答えを読む表はその範囲の上で検査される」も、日付では成り立っていなかった。
**決定**:
1. **reference が正しく、実装を直す。** 答えが fold の答える値の外側に入ることは、証明してある。範囲の外の値のための行を書かせても、その行はどの呼び出しでも使われない。書かせる理由が無い。
2. **数と日付の列の軸は、歩きの答えの範囲を取る**(`region::walk_answer_range`)。`count` と `sum` は宣言した範囲、`max` と `min` は列の範囲と `empty` の値の外側、fold の答えは答える値の外側である。日付の `max`・`min` と、日付を答える fold も同じにした。
3. **証明書は、範囲を持つ fold の答えも `walks` に挙げる**(`cert::walked_names`)。その列の軸は `walk` になる。前は `upstream` だった。
4. **二つの再検査は、`walk` の軸を `ranges` の範囲に合わせて確かめる。** 入力の軸を宣言した範囲に合わせて確かめるのと同じで、両端が範囲の両端の点であることも見る(`tools/recheck.py` の `check_axis`、Lean の `checkAxes`)。前は、歩きの答えの軸の両端を何とも突き合わせていなかった。証明書が範囲を `[0, 1000000]` と述べ、軸を `[0, 2000000]` まで切っても、どちらの再検査も通していた。範囲そのものは、前からのとおり、`extremes`(`max`・`min`)と `fold_answers` が答えの届く先と突き合わせ、`count` と `sum` は宣言した範囲である。
**捨てたもの**:
- **reference を実装に合わせて、範囲の外の行を書かせる。** 届かない値のための行は、規則を読む人に、その値が来ると思わせる。
**変わること**:
- 前は `check` を通らなかった形の規則が通る。名前を付けた fold の答えや、日付の `max`・`min` を読む表に、答えの範囲の外の行は要らない。
- 範囲を持つ fold の答えのある規則の証明書の `walks` に、その答えが増え、それを読む表の軸の `kind` が `walk` になる。
**確かめたこと**:
- `tests/walks.rs` に一本を足した。英語の `first_member`(fold の答えが 0 か 1〜4)と日本語の版、日付の `max` を読む `latest_line` の三本が `check` を通り、証明書の `walks` に答えが入り、その列の軸が `walk` になり、範囲が `["0","4"]`(日付は日の番号の 20454〜20818)と述べられる。範囲の端を一つ広げた証明書を、二つの再検査がどちらも止める。前からある Lean の試験の、`walks` を書き換える改ざんの文字列を、増えた名前に合わせた(`["subtotal","top_price","paid","discount"]`)。替える前のビルドでは、`first_member` は E101(`first = -1`)、`latest_line` は E101(`latest = 2025-12-31`)で止まった。
- コーパスの 157 本の証明書を、二つの再検査で確かめ直した。149 本はそのまま通る。残りの 8 本は寄与のファイルで、証明書は足す先の規則についてのものなので、`--rule` に足す先の規則を渡すと、8 本とも両方を通る(寄与のファイルそのものを渡すと、替える前のビルドでも同じ 8 本が「別の文の証明書」と言って止まる)。`lake build` は 94 のジョブで通る。
- 証明書が変わるのは、コーパスでは `order_settlement` と `注文の精算` の 2 本だけで、`walks` に fold の答え(`discount`、`値引き`)が増えた。その答えを読む表は無いので、軸は変わらない。
**正直な限界**:
- 行の重なり(E105)と、どの入力も届かない行(E102)の検査は、歩きの答えの範囲を使わない。`count` の列で `<=-1` の行と `<=4` の行を書くと、来ない −1 で重なると言われる(前からのもの)。答えの範囲の外だけを取る行も、E102 にならない。
- fold の答えの範囲は、答える値の外側で、答える値そのものの集まりではない。`take_first 3` のように飛び飛びの値しか答えない fold の答えを、`>=1` と `<=0` の行で読むと、`check` は通るが、網羅の監査(`rulec coverage`)は、境目 1 と 0 の対の片方に届くベクタが無いと言う(境界の対 2 / 4)。答えが 1 にも 2 にもならないからである。前は、この形の規則が `check` を通らなかった(E101)。
### 15.237 準用する規則に合わさった寄与を、準用する側の規則でも言う(2026-10-15)
**きっかけ**:`apply` で読む規則に寄与が合わさると、準用する側の生成コードは、その寄与の節を含んでいた(`elig:m_elig:storm_weight`)。それなのに、`check` の `ok` の行にも、生成物の頭の `Contributions:` にも、寄与の名前が出なかった。reference §6.7 と generated-code.md は、頭がどの寄与と合わせて作ったかを言うと書いている。
**決定**:
1. **準用する規則は、準用した規則に合わさった寄与を持つ**(`apply::expand`)。寄与の記録(`Merged`)に、どの `apply` を通ったかを足した(`via`。`apply` の中の `apply` なら `a:b`)。
2. **`check` の `ok` の行は、そのファイルを挙げる**(`ok rules/pick.rule (2 contributions: mods/m/elig.rule, mods/m/content.rule)`)。並びは、規則そのものの寄与、準用した規則の寄与、`enums` のファイルに値を足した寄与の順で、同じファイルは一度だけ書く。
3. **生成物の頭と `rulec api` は、`apply` の名前を付けて言う**(`Contributions: elig:m_elig = ../mods/m/elig.rule (signature:…, sha256:…)`)。準用が持ち込む名前の書き方に合わせた。`rulec api` の `contributions` では、その寄与の `inputs` は空である。寄与が準用した規則に足した入力は、`apply` が読み替えるので、準用する規則の関数の引数にはならない。
4. ページの表と節は変えない。準用した規則の節が、その規則に合わさった寄与の節を、前から含んでいる。HTML のページは生成物を埋め込むので、その頭の一行が増える。
**確かめたこと**:
- `tests/contrib.rs` に一本を足した(`ok` の行、生成物の頭、`rulec api` の `contributions`)。
- 再現(`rules/pick.rule` が `rules/elig.rule` を準用し、`mods/m/elig.rule` が `elig.rule` に寄与する)で、`check` の行と生成物の頭が決定の 2 と 3 のとおりになる。Markdown のページは替える前と一字も変わらず、HTML のページは、埋め込んだ生成物の頭の一行(`// Contributions: elig:m_elig = …`)だけが増える。
### 15.238 machine の runner の三つの不具合(2026-10-15)
**きっかけ**:作例のゲームの外交と一度だけのイベントの規則で、`rulec test --require-all` の比べ方が五つ走らなかった。どれも生成したモジュールではなく、`rulec test` が走らせる runner の不具合だった。
**決定**:
1. **NumPy の runner は、machine の規則でも `--labels` に答える。** machine の runner は別のテンプレートから作っていて、表示名の分岐を入れていなかった。表示名を持つ列挙の規則では、表示名の TSV を JSON として読もうとして止まっていた。
2. **TypeScript と JavaScript の runner は、`enums` のファイルから読む状態の列挙の解析の関数を、そのファイルのモジュールから一度だけ import する。** 前は、規則のモジュールと `enums` のファイルのモジュールの両方から `parse<列挙>` を import して、名前がぶつかって読み込めなかった。規則のモジュールは、その関数を書き出していない。
3. **PHP の runner は、状態の列挙を、それを宣言するファイルの名前空間で引く**(`php_enum_fqn`)。前は規則の名前空間で引いて、クラスが見つからなかった。
**確かめたこと**:
- `tests/machine.rs` に一本を足した。表示名のある `enums` のファイルから状態を読む機械(`door`)、自分の列挙に表示名を持つ機械(`lamp`)、表示名の無い `enums` のファイルから状態を読む機械(`gate`)を、道具のある全部の runner で走らせ、どれも通る。
- 再現の二本(`door` と `lamp`)を、PostgreSQL を立てて `rulec test --require-all` に通すと、82 の比べ方が全部一致する。替える前は、`door` と `lamp` の NumPy の表示名、`door` の TypeScript・JavaScript・PHP・Go、Go の丸めの補助関数の七つが走らなかった(Go の二つは §15.239 の名前のぶつかりによる)。
### 15.239 規則と列挙が同じ名前のとき:Go は持ち主のパッケージで型を書き、書き分けられない形は `check` が言う(2026-10-15)
**きっかけ**:規則の名前と、`enums` のファイルから読む列挙の名前が同じ(`rule size` と `enum size`)と、Go のパッケージが、規則の関数 `func Size` と列挙の別名 `type Size = sizes.Size` を並べて書き、ビルドできなかった。`check` は何も言わなかった。ほかの生成先は、関数と型の大文字小文字が違うか(Python、TypeScript、Rust、Swift)、列挙を持ち主のクラスの名前で書く(Java の `Sizes.Size`)ので、ぶつからない。規則が自分で宣言する列挙の名前が規則と同じだと、Go と Java の両方でぶつかる(Java は、規則のクラス `Size` の中に列挙 `Size` を置く)。
**決定**:
1. **Go のパッケージは、`enums` のファイルの列挙の型の名前が、規則のための名前とぶつかるときは、その型を別名にせず、持ち主のパッケージの名前で書く**(`sizes.Size`。`codegen::go_enum_ty`)。規則のための名前は、規則の関数、`<規則>Traced`、`<規則>Record`、確率の関数、`Input`、`Output`、`Fired`、要素の型、二つのエラー、`Chances`、`Way`、machine の `Initial`・`Final`・`IsFinal` である。Java が持ち主のクラスの名前で書くのに合わせた。定数と `Parse<列挙>` は、ほかの列挙と同じく規則のパッケージにも置く。runner も、状態や前提の塊の列挙をその名前で書き、持ち主のパッケージを import する。`rulec api` の Go の入力の型も `sizes.Size` になる。
2. **規則が自分で宣言する列挙の型が、規則の名前とぶつかるときは、`check` が言う**(W121)。Go では規則の関数(とその仲間)と、Java では規則のクラスと同じ名前になる場合で、注記は生成先ごとに何とぶつかるかを言う。どちらの名前も書き手が付けた別名で、道具が付け替えることはしない。直し方は、規則か列挙の別名を変えるか、列挙を `enums` のファイルに置いて読むことである。台帳の W121 の「いつ出るか」に一文を足した。変異は `m_w121rule` と `m_w121ruleja` の二本である。
**捨てたもの**:
- **規則の関数の名前を付け替える**(`SizeRule`)。呼ぶ側が使う名前が、Go だけほかと違ってしまう。
- **列挙の型の名前に `_` を付ける**(§15.210 の予約語の扱い)。型の名前は呼ぶ側が書くもので、ぶつかる場合にだけ名前が変わると、呼ぶ側のコードが規則の名前しだいで変わる。
**変わること**:Go の生成物は、`enums` のファイルの列挙の型の名前が規則のための名前とぶつかる規則でだけ変わる(コーパスには無い)。規則と同じ名前の列挙を宣言する規則は W121 になる(木の規則では、足した変異の 2 本だけ)。
**確かめたこと**:
- `tests/enums.rs` に一本を足した。`enums` のファイルの `enum size` を読む `rule size` で、`rulec api` の Go の入力の型が `sizes.Size` になり、Go の比べ方が通る。自分で `enum size` を宣言する `rule size` は、Go と Java を挙げた W121 になる。
- 変異を英語と日本語の対で二本足した(`m_w121rule`:会員の段階の送料の規則 `member_tier_shipping` の名前を、規則が宣言する列挙と同じ `tier` にした。`m_w121ruleja`:日本語の版の別名を `tier` にした)。
- 再現(`rule size` と、`shared/` の `enums` のファイルの `enum size`)を、PostgreSQL を立てて `rulec test --require-all` に通すと、36 の比べ方が全部一致し、`gofmt -l` は何も言わない。
### 15.240 `enums` のファイルへの寄与も、決めごとを問うコマンドには渡せない(2026-10-15)
**きっかけ**:`rulec doc rules/content.rule --format html` は、`enums` のファイルを、事例を試すページが無いとして 2 で止める(§15.212)。ところが、そのファイルへの寄与を渡すと、寄与は足す先のファイルを表すので、`enums` のファイルの HTML のページが、事例を試すところつきで出た。
**決定**:決めごとを問うコマンド(`schema`、`adapter`、`verify`、`replay`、`fixtures`、`diff`、HTML と顧客向けの `doc`)は、`enums` のファイルへの寄与も、そのファイルと同じに、理由を言って 2 で止める。文は、寄与と足す先のファイルの両方を挙げる(`` `mods/m/content.rule` adds to `rules/content.rule`, an enums file that decides nothing, … ``)。Markdown の `doc` は、これまでどおり `enums` のファイルのページを書く。
**捨てたもの**:
- **事例を試すところの無い HTML のページを、どちらの渡し方でも出す。** そのページは Markdown のページと同じことしか言わない。§15.212 の「何も決めないファイルに、決めごとのページは無い」を崩す理由が無い。
**変わること**:`enums` のファイルへの寄与を含むディレクトリを、HTML や顧客向けの `doc` に渡すと、2 で止まる。`enums` のファイルを含むディレクトリを渡したときと同じである(こちらは前から 2 で止まる)。ページの要る規則と、それに寄与するファイルを並べて渡す。
**確かめたこと**:`tests/enums.rs` に一本を足した(HTML と顧客向けの `doc` と `schema` が、寄与を渡しても 2 で止まり、ページを書かないこと。Markdown の `doc` は足した値を書くこと)。再現は、直接渡しても寄与で渡しても同じ文で止まる。
### 15.241 人向けのページで、率の範囲を百分率で書く(2026-10-15)
**きっかけ**:率の列を `sum` した値の範囲を、人向けのページが `0 to 1` と書いた(宣言は `>=0% <=100%`)。直すときに見ると、率の入力と要素のフィールドの範囲も `0 to 0.2` のように、内部で持つ分数のまま書いていた。
**決定**:ページが範囲を書く所(入力、要素のフィールド、導出、集計、顧客向けの入力)は、率を百分率で書く(`0% to 20%`、`0.5% to 50%`)。値の書き方は、診断が率の値を書くときと同じである(`Val::show`)。
**確かめたこと**:
- `tests/doc.rs` に一本を足した(要素のフィールド、入力、`sum` の合計の範囲。英語の規則と日本語の版)。
- 替える前と比べて、コーパスで変わるのは率の範囲を書く 10 本(英語の 5 本と日本語の版)のページだけである。Markdown のページは、範囲の列の文字だけが変わる(`0 to 0.2` が `0% to 20%`、導出の `0 to 0.25` が `0% to 25%`)。dandori の `doc` の golden 6 本と、dandori のサイトの 2 ページ(判断のモデルの確信度の範囲。`0 to 1` が `0% to 100%`)を取り直した。
### 15.242 小さな三つ:`ritsu check` の要約の数え方、`rulec api` の行の数、表が書く数を読む表(2026-10-15)
1. **`ritsu check` の最後の行は、各ファイルの指摘を、そのファイルの結果の側で数える。** 前は、通らないファイルがあると、通るファイルの警告も「通らない」側の括弧に入れていた。通らないファイルが無ければ、同じ警告を `all pass (2 warnings)` と数える。いまは、通るファイルの警告は、通らないファイルがあっても通る側に数える。警告が二つある規則と、行の抜けた規則のプロジェクトで、前は `1 fail (1 error, 2 warnings)`、いまは `1 fail (1 error), 1 pass (2 warnings)` である。どのファイルのものでもない指摘は、通らない側に数える。どれも通るときの書き方と、通るファイルに指摘が無いときの書き方は変えていない。ritsu の DESIGN の W905 の例(地図が通らず、フローは通って警告を持つ)は、`1 fail (1 error), 5 pass (1 warning)` になる。
2. **`rulec api` は、規則のファイル一つにオブジェクト一つを、一行に書く。** 寄与のディレクトリを渡すと、寄与が足す先の規則ごとに一行を書く。これは実装のとおりで、formats.md の「One object.」が、ファイルが一つのときのことしか言っていなかった。formats.md を直した(寄与は足す先のファイルを表し、同じファイルは一度だけ書く)。
3. **表が書く数を別の表が読むとき、読む側は、書かれた値に限られない数の全体の上で検査される。** reference の §4 の `range` と §6.7 に書いた。`>=1` と `0` ではなく、`>=1` と `<=0` の行が要る。開いた表でも同じで、寄与の行がどの値を書いても、読む表は規則だけの検査で完全である。列挙の値と真偽は、行が書く値だけを読む。
**確かめたこと**:
- ritsu の `tests/check.rs` に一本を足した(上の 1 の、英語と日本語の要約の行)。言語をまたぐ試験の golden で、通らないファイルの括弧に警告を数えているもの(X3a、X5、X6 など)は、警告が通らないファイルのものなので、変わらない。
- ritsu の DESIGN の W905 の例は、台帳の再現を走らせると `1 fail (1 error), 5 pass (1 warning)`(日本語は「検査を通らないもの 1 個(エラー 1 件)、通るもの 5 個(警告 1 件)」)になる。
### 15.243 第 4 段で述べただけだった主張を Lean で証明する:確率の主張、開いた値の範囲、数の軸の敷き詰め。寄与のファイルと語のセルの読み戻し(2026-10-15)
**きっかけ**:第 4 段で足した主張のうち二つを、Lean の `rulec-recheck` は「述べただけ」として名前を挙げて通していた。`chances` の主張(§15.224〜§15.226)と、開いた値の範囲(§15.227)である。木の `.rule` 544 本のうち証明書が出る 335 本で、確率の主張は 12 本の 70 か所、開いた値の範囲は 16 本の 36 か所だった。`tools/recheck.py` も、区画の答えは証明書が書くとおりに読み、一つずつ走らせた区分の確率は「述べただけ」として挙げていた(それぞれ 6 本)。あわせて、数の軸の敷き詰めの検査に、通れば何が言えるかの定理が無かった。そのため、歩きの答えを軸に読む表(§15.236)が、答えがどの値になっても行で答えることも、定理になっていなかった。
**決定**:
1. **数の軸の敷き詰め**(新しい `Axes.lean`)。検査を `axisTilesB` として書き直した。座標の端は軸の刻みに乗り、隣どうしは、刻み一つ離れた二つの点か、点とそこから始まる(そこで終わる)区間である。最初と最後の座標は、範囲の端の点か、外へ続く区間である。これが通れば、範囲の中で刻みに乗った値はどれも、ちょうど一つの座標に乗る(`axisTilesB_cover`、`axisTilesB_unique`)。
- 書き直すときに、これまでの検査(Lean と `tools/recheck.py` の両方)が通していた形が二つ見つかった。端が重なる二つの区間(`(0, 5)` の次に `(5, 10)`。5 がどちらにも入らない)と、同じ点の二度書き(一つの値が二つの座標に乗る)である。どちらも落とすようにした。
- 入力、歩きの答え、開いた値の軸は、範囲の両端を点として持つ。ほかの軸(計算する値、`define`、上の表が決める列)は両端が開いていなければならない。その値がとりうるのは数の全体だからである。
- 歩きの答えを読む表:`extreme_mem_stated` と `foldAnswer_mem_stated` が答えを証明書の範囲に入れ、軸はその範囲を敷き詰めるので、答えは軸のどれかの座標に乗る(`extreme_on_axis`、`foldAnswer_on_axis`)。どの列の値も座標に乗れば、その座標の組は表の空間の点で、表の証明書の完全性から、どれかの行がその点をとる(`Certified.answers_at`)。これで、下の表が答えを読むどの呼び出しにも表が答える、までがつながった。
2. **開いた値の範囲**(`Values.lean`)。開いた値は、決まった値(`sum` は 0、`product` は 1、`max`・`min` は `empty` の値)から始めて、項を一つずつ合わせた値である(`openValue`)。`max`・`min` の `empty` の値は、項があっても合わせる値に入る(§15.227 のとおり)。項がそれぞれの区間にあれば、合わせた値は、区間を同じように合わせた区間に入る(`openValue_mem`)。プログラムは、宣言した範囲がその区間を含むことを確かめ(`openOk`)、`openValue_mem_stated` が、それで値が範囲に入ることを言う。項の区間は、ほかの名前と同じく、式・行が書く値・宣言した範囲から求める(`Worked.rangesOf`)。開いた値を読む表は、宣言した範囲の上で軸を切る(上の 1)。
3. **確率の主張**(新しい `Chances.lean`)。主張の意味を `Holds` として書いた。乱数でない入力が、行の入力のセルが通す値をとるどの場合でも、乱数の入力ごとに引く値を一つずつ選んだ組の全部のうち、出力が行の出力のセルを通る組の割合が、主張する確率である(比べなら、それを満たす)。出力の丸めは `RoundsWell` を満たすどの方式でもよい。検査の三つの数え方のそれぞれで、通れば `Holds` が成り立つことを証明した。
- **区画ごとに数える**(`Claim.holds`)。乱数の軸の座標は、入力を引く値を、最初の値から最後の値まで、漏れも重なりも無く並べる(`chain_flatMap`、`tiles_flatMap`)。だから区画の点の数は座標の長さの掛け算で、引いたどの組もちょうど一つの区画に入る(`countP_cart_flatMap`)。区画の中では、区画が乗る表の軸の座標のどれでも行が同じ値を答えるので(`nearOf_sound`、`cellOf_sound`)、どの組の答えも、プログラムが表の行から求めた区画の答えと同じになる(`Claim.box_const`)。区分の当たりの数と点の数は、その区分の値での割合そのものである(`Claim.count_share`)。乱数でない入力の軸は、その列がとる値を覆う(`FAx.cover`)。
- **一つずつ走らせる**(`RunClaim.holds`)。答えが区画の中で動く主張では、行のセルが通す乱数でない入力のどの組み合わせでも、引く値の全部を走らせる。計算する値は、どの丸めでも同じ値になるときだけ求め(`evalExact`。`envExact_sound`)、表の答えは上と同じく読む(`RunClaim.pointHit_sound`)。`constraint` が乱数の入力を挙げれば、数えるのは制約を通る点の中である。
- **重みで選ぶ**(`WeightedClaim.holds`)。`take_weighted` の fold は、要素ごとに表から判定を読み、その判定の分岐が要素を取るなら重みを累計に足し、引いた値が累計より初めて小さくなったところの要素の値を答える。重みの合計が 0 なら `exhausted` の値(`held` なら持っている値か `empty` の値)、要素が無ければ `empty` の値である。引いた値は重みの合計より小さくなければならない(入口が通さない)。プログラムはこの歩きを、引くどの値でも走らせる(`WeightedClaim.pointHit_sound`)。手順は評価器(`eval.rs`)と同じにした。
- 証明書の `chances` に三つ足した。主張ごとの `source`(入力のセル、出力のセル、確率のセルがファイルのどこにあるか)、`rounds`(主張が読む数の出力の丸めの刻み)、重みで選ぶ規則の `weighted`(歩く並び、判定の列、引く入力、答える列、分岐ごとの判定と取る値と重み、`empty`、`exhausted`、主張が読む `sequence` の要素とそのセルの場所)である。二つの再検査は、主張のセルと確率を `chances` の表の行から、並びのセルをファイルから読み戻す。これで、証明した主張がファイルに書いた主張になる。`rounds` は、行が書く数がその刻みに乗っていれば丸めても動かず、規則の返す値そのものだと言うために要る(`Claim.roundVal_keeps`)。
- 区画ごとに数える主張で、乱数でない入力の軸を切る刻みは、その列の値が乗る刻み(§15.190 と同じ求め方)を割り切らなければならない。`Holds` はその刻みに乗った値について言うので、刻みを粗く書いた証明書は、列がとる値より少ない値の上で数えたことになる。これまではどちらの再検査も確かめていなかった。
4. **寄与のファイルと、語のセルの読み戻し**(`cert.rs`、`RulecMain.lean`、`tools/recheck.py`)。
- 合わせた規則の証明書に、上の階層の `contributions` を足した。寄与ごとの名前、規則のディレクトリからのファイル、その SHA-256 である。二つの再検査は、どのファイルもそのハッシュであること、`rows[].file` と開いた値の項の `from` がそのどれかであることを確かめる。これまで、行を足す寄与のファイルは行のハッシュで縛られていたが、行を足さない寄与(列挙に値を足すだけ、開いた値に項を足すだけのもの)は、どのハッシュにも縛られていなかった。
- いくつもの場所に書かれた表(一つの列を決める表と節の集まりで、寄与の行を含むもの)の行に、`answers` を足した。行が語を書く答えのセルの場所である。そうした表では答えのセルが `decides` の順に並ぶとは限らないので、§15.233 では語の読み戻しを一か所に書いた表に限っていた。いまは二つの再検査とも、語を `answers` のセルから、行を書いたファイルで読み戻す。
- 寄与の証明書は、その寄与が合わさる規則の証明書なので、`--rule` にはその規則を渡す。`docs/formats.md` と `proofs/RulecCert/README.md` にそう書いた。
**直した欠陥**:
- 上の 1 の二つ(端が重なる二つの区間、同じ点の二度書き)と、上の 3 の乱数でない入力の軸の刻み。どれも rulec が書く証明書には現れない形で、木の証明書で落ちるものは無い(表の数の軸 314 本と、確率の乱数でない数の軸 2 本を調べた)。
- `tools/recheck.py` は、範囲を書いた順のある列挙を乱数の入力にした規則の証明書を、正しくても受け付けなかった(`the boxes of a class do not tile the random axes (2 of 4)`)。範囲が外す値の区画は点が無いので証明書に載らないのに、区画の数を列挙の値の数と比べていた。木には無い形で、試しに書いた規則で見つかった。
- 上の 4 の二つ(行を足さない寄与のファイル、いくつもの場所に書かれた表の語)。
**捨てたもの**:
- **丸めの五つの方式を Lean に書くこと**(§15.232・§15.233 と同じ理由)。一つずつ走らせる主張と重みで選ぶ主張は、計算する値と出力がどの方式でも同じ値になる点でだけ数える。方式で答えが分かれる点がある主張は、「述べただけ」と挙げる。
- **W129 の主張を証明すること。** 検査自身が成り立つかを決めていない(§15.224)。
- **区画ごとの数え方で、文字列の列を切ること。** 文字列の軸は接頭辞で切るので、乱数でない入力のとる値を並べられない。
**正直な限界**:
- `Holds` は、乱数でない入力の値を、検査が軸を切った刻みの上で考える。その刻みが列の値の刻みを割り切ることは確かめる(上の 3)ので、列がとる値は全部入る。
- 重みで選ぶ fold の意味は、評価器の手順を Lean に書いたもので、それ自体が仕様である。分岐、取る値と重みの式、`rounds` は、式と同じく証明書の言い分である。
- Lean のプログラムが走らせる点は、主張一つにつき 4,000,000 までで、越える主張は「述べただけ」と挙げる。計算する値が、証明書の `values` で後に挙げた値を読むときも同じである(`values` の順に求めるため)。どちらも木には無い。
- 木で残る「述べただけ」は、確率の主張では `m_w129` の英日の行 5(W129)だけである。
**確かめたこと**:
- 木の `.rule` 544 本のうち証明書が出る 335 本が、`tools/recheck.py` と `rulec-recheck` の両方を通る。「every claim this program states was proved」になる本は、Lean で 255 本から 281 本に、`tools/recheck.py` で 262 本から 272 本に増えた。確率の主張は 12 本の 70 か所から 2 本の 2 か所(上の W129)に、開いた値の範囲は 16 本の 36 か所から 0 になった。区画ごとに数えたのは `prize_lottery`・`reward_box`・`invoice_sampling` と日本語の版(`reward_box` は区分 450)、一つずつ走らせたのは `contested_roll`(25,000 点)と日本語の版、重みで選ぶのは `case_routing` と日本語の版(引く値 70,000)で、どれも Lean で 1 秒かからない。
- 一か所ずつ崩した証明書は、二つの再検査が両方とも落とす(`tests/lean.rs`)。区画の点の数、乱数の軸の座標の抜け、区分の当たりの数、主張の確率、区画の答え、主張のセルの場所、乱数でない入力の軸の刻み、一つずつ走らせた点の数と確率の範囲、並びの要素の値、重みで選ぶ主張の確率、開いた値の範囲、寄与の行のセル、寄与のファイルのハッシュ、寄与の行が書く語、開いた値の軸が範囲に届かないこと、である。
- 証明書が変わる規則は、木の 335 本のうち 32 本である。`contributions` が付くのが寄与の証明書の 20 本、`chances` の `source` と `rounds` が付くのが 12 本で、そのうち `case_routing` と日本語の版には `weighted` も付く。`answers` が付く証明書は木に無い(いくつもの場所に書かれた表で語を書く行が無い)。試しの規則(寄与が開いた表に語を書く節を足すもの)で、語のセルを読み戻すことを確かめ、`tests/lean.rs` に入れた。
- `#print axioms` で、足した定理(`axisTilesB_cover`、`axisTilesB_unique`、`extreme_on_axis`、`foldAnswer_on_axis`、`Certified.answers_at`、`openValue_mem_stated`、`Chances.countP_cart_flatMap`、`Chances.chain_flatMap`、`Chances.tiles_flatMap`、`Chances.mem_grid_num`、`Chances.holds_iff_inRun`、`Chances.nearOf_sound`、`Chances.cellOf_sound`、`Chances.CCell.inside_iff`、`Chances.FAx.cover`、`Chances.Claim.box_const`、`Chances.Claim.count_share`、`Chances.Claim.holds`、`Chances.envExact_sound`、`Chances.RunClaim.pointHit_sound`、`Chances.RunClaim.holds`、`Chances.WeightedClaim.pointHit_sound`、`Chances.WeightedClaim.holds`)が立つのは `propext`・`Classical.choice`・`Quot.sound` だけである(`tests/lean.rs`)。Lean の層の宣言 8,095 個(定理 3,371 個)も同じだった(`crates/ritsu-model/tests/proofs.rs`)。足した Lean は、新しい `Axes.lean` が 354 行、新しい `Chances.lean` が 2,863 行、`Values.lean` が 89 行、`Read.lean` が 10 行(1 行を置き換えた)、`RulecMain.lean` の読み手と検査が 795 行(66 行を置き換えた)である。
### 15.244 生成する Python のモジュールの直下に、型変数を置かない(2026-10-15)
**きっかけ**:§15.234 で、取り出し関数が列挙の値を読むための補助関数 `_enum_of(make: Callable[[str], _E], v: Any, what: str) -> _E` を書き、その型変数 `_E = TypeVar("_E")` をモジュールの直下に置いた。型を付けた Python の部分集合を静的にコンパイルする処理系は、モジュールの直下の文を翻訳し、モジュールが動くときに計算される値の代入を受け付けない(§15.207)。`_E = TypeVar("_E")` もその一つで、アプリケーションが取り出し関数を呼ばなくても、規則のモジュールを import するだけで止まる。0.29.0 の生成物のモジュールの直下には型変数が無く、その処理系は、コーパスの 143 本と寄与を合わせた 8 本の全部を受け付けていた。0.29.1 の生成物では、列挙を読む取り出し関数を持つコーパスの 4 本(`order_shipping_fee`、`order_shipping`、`shipment_delivery_fee`、`shipment_fee`)が、それぞれその一行で止まる。作例のゲームの生成物でも、列挙を読む取り出し関数を持つ 5 本のモジュールが止まった。
**決定**:
1. **列挙を読む補助関数を、列挙の型ごとに一つ書く。** `def _shipping_zone_of(v: Any, what: str) -> ShippingZone:` のように、型の名前を snake case にして `_` と `_of` で挟み、答えをその型にする。ほかの型を読む `_int_of`・`_bool_of`・`_str_of` と同じ形で、型の名前から関数の名前を作るのは、公開の `shipping_zone_rank`・`shipping_zone_label`(§15.210、§15.211)と同じである。中身は前の `_enum_of` と同じで、文字列でない値と、列挙のどの値でもない値を、同じ文の `RuleInputError` で止める。書くのは取り出し関数が読む型の分だけで、`shipment_fee` は `_region_of` と `_window_of` の二つになる。型変数が要らないので、`typing` から `Callable` と `TypeVar` を取り込まない。
2. **名前がほかとぶつかるときは、下線を一つずつ足す。** 組み込み関数に付けた名前、補助関数の名前、グループの定数、取り出し関数の引数、先に名前を決めた列挙の補助関数のどれかと同じなら、ぶつからなくなるまで後ろに `_` を足す。グループの定数の付け方(§15.207)と同じである。列挙 `seq` の補助関数は、並びを読む `_seq_of` があるので `_seq_of_` になり、グループ `level_of` を持つ規則の列挙 `level` の補助関数は、そのグループの定数 `_level_of` があるので `_level_of_` になる。
3. **補助関数の名前の一覧(`PY_HELPERS`)には、`_enum_of` と `_E` を残す。** グループの定数は、この一覧の名前を避けて名前を決める。外すと、`enum_of` という名前のグループの定数が `_enum_of_` から `_enum_of` に変わる。前に書いていた `_dig` も、同じく一覧に残っている。
4. **TypeScript・JavaScript・Ruby・PHP は変えない。** TypeScript と JavaScript の `_enumOf` は列挙の値の配列を受け取り、答えには呼ぶ所で `as` で型を付ける。Ruby は値の配列を、PHP はクラスの名前を受け取る。どれもモジュールの直下に型変数を置かない。
5. **モジュールの直下に置く文は、0.29.0 が書いた種類に限り、試験で確かめる。** 種類は下の表のとおりである。試験は、ほかの種類の文と、モジュールのどこかにある `TypeVar` を、場所を言って止める。
**モジュールの直下に置く文の種類**:0.29.0(1e7d646e のビルド)で、コーパスの 143 本と `shared/` の `enums` のファイル 6 本を一度の `rulec gen` に、寄与を合わせた 4 組(値引きの英語と日本語、商品ラインの英語と日本語。`tests/contrib.rs` と同じ組)を一組ずつ `rulec gen` に渡した。`rulec api` が挙げるモジュール 159 本(規則のモジュール 151、`enums` のファイルのモジュール 8)の直下の文を Python の `ast` で数えると、次の 17 の種類だけだった。
| 種類 | 例 | 文 | モジュール |
|---|---|---|---|
| `__future__` からの import | `from __future__ import annotations` | 159 | 159 |
| `typing` からの import | `from typing import NamedTuple, NewType` | 151 | 151 |
| `enum` の import | `import enum` | 110 | 110 |
| `enums` のファイルのモジュールからの import | `from goods_catalog import Category as Category, Zone as Zone, zone_rank` | 18 | 18 |
| `enum.Enum` のクラス | `class SizeClass(enum.Enum):` | 165 | 110 |
| `NamedTuple` のクラス | `class Fired(NamedTuple):` | 221 | 151 |
| `ValueError` のクラス | `class RuleInputError(ValueError):` | 151 | 151 |
| `AssertionError` のクラス | `class RuleContradictionError(AssertionError):` | 151 | 151 |
| 関数 | `def _min(a: int, b: int) -> int:` | 1,750 | 159 |
| ブランド | `Yen = NewType("Yen", int)` | 147 | 107 |
| 組み込み関数に付けた名前 | `_isinstance = isinstance` | 726 | 151 |
| 一致した行のリストの型の別名 | `_Trace = list[Fired]` | 151 | 151 |
| グループの定数 | `_fragile = frozenset({Category.ELECTRONICS, Category.FURNITURE})` | 26 | 14 |
| machine の始まりの状態 | `INITIAL: Subscription = Subscription.ACTIVE` | 9 | 9 |
| machine の終わりの状態 | `FINAL: frozenset[Subscription] = frozenset({Subscription.SUSPENDED})` | 9 | 9 |
| 持ち越す数の始まり | `INITIAL_DAYS_LEFT: int = 0` | 4 | 4 |
| 持ち越す真偽の始まり | `INITIAL_TRIAL_TAKEN: bool = False` | 2 | 2 |
- クラスは、基底が表の四つのどれか一つで、デコレータもキーワードも型引数も無いもの。関数は、`async` でなく、デコレータも型引数も無いもの。代入は、名前一つへの代入で、右辺が表の形のもの(`NewType` の呼び出し、組み込み関数の名前、`list[…]`、列挙の値を並べた `frozenset`、列挙の値、整数と真偽のリテラル)である。
- 0.29.1 の生成物には、このほかに `_E = TypeVar("_E")` が 4 本にあった。この節のあとの生成物も 17 の種類だけで、関数のほかは数も 0.29.0 と同じである。関数は 1,750 から 1,796 に増える。§15.234 が足した補助関数の 44 と、列挙を二つ読む 2 本で補助関数が一つずつ増えた分である。
**捨てたもの**:
- **静的にコンパイルする処理系の側で、モジュールの直下の `TypeVar(...)` を、型の宣言として受け付ける。** 生成する側で型変数を使わずに済み、そうしても型の確かさは失わない。モジュールの直下で値を計算しない形にとどめておけば、同じように前もってコンパイルするほかの処理系にも、受け付けを広げてもらわずに済む。
- **`_enum_of` の答えを `Any` にする**(`make: Callable[[str], Any]`、`-> Any`)。型変数は消えるが、取り出し関数が規則の関数に渡す列挙の値の型を、型検査器が見なくなる。`Any` は増やさない。`object` を答えにする形も、§15.207 のとおり使わない。
- **PEP 695 の書き方(`def _enum_of[E](…)`)にする。** 型変数はモジュールの直下に出ないが、Python 3.12 からの書き方である。生成物は 3.10 から動かす(`requires-python` が 3.10 以上)。
**変わること**:
- 列挙を読む取り出し関数を持つ規則の、Python のモジュールだけが変わる。コーパスでは 4 本で、`from typing import` から `Callable` と `TypeVar` が消え、`_E = TypeVar("_E")` の行が消え、`_enum_of(Zone, …)` が `_zone_of(…)` になる。取り出し関数の答えと、止まるときの文は変わらない。§15.234 の決定 5 の `_enum_of` と、変わることの `Callable` と `TypeVar` を、この節で置き換えた。
- ほかの言語の生成物、runner、ページ、ベクタ、`rulec api` は変わらない。
- runner(`<alias>_runner.py`)、MCP のサーバー(`<alias>_mcp.py`)、Connect のサービスとその runner(`<alias>_service.py`、`<alias>_connect_runner.py`)は、前からモジュールの直下に `E = TypeVar("E", bound=enum.Enum)` を置いている。どれも Python がそのまま走らせるプログラムで、アプリケーションが import するモジュールではないので、変えず、試験にも入れない。
**確かめたこと**:
- `tests/threeway.rs` に一本を足した。「生成pythonのモジュールの直下には決まった種類の文だけを置く」:コーパスと `enums` のファイル、寄与を合わせた 4 組を生成し、`rulec api` が挙げるモジュール 159 本の直下の文が表の 17 の種類のどれかで、`TypeVar` がどこにも無いことを確かめる。試験の Python のスクリプトを、そのまま 0.29.0 と 0.29.1 の生成物にかけると、0.29.0 は通り、0.29.1 は 4 本の `_E = TypeVar("_E")` と、`typing` の import と型変数を作る所の `TypeVar` で止まる。
- `tests/projection.rs` に二本を足し、一本に二つの場合を足した。「列挙を読む関数はほかの名前とぶつからない」:列挙 `seq` と `level`、グループ `level_of`、並びの数を読む規則で、補助関数が `_seq_of_` と `_level_of_` になり、五つの言語が同じ答えを返し、文字列でない値と列挙に無い値を同じ文で止める。下線を足さないビルドでは、この試験が止まる。「ほかのファイルの列挙もその型の関数で読む」:コーパスの `shared/catalog.rule` の列挙を二つ読む規則で、モジュールが `from catalog import Category as Category, Zone as Zone, zone_rank` と `_category_of`・`_zone_of` を持ち、五つの言語が同じ答えと同じ文を返す。`enums` のファイルの列挙を取り出し関数で読む規則は、コーパスに無い。「protoの値も型どおりに確かめる」には、二つめの列挙(`時間帯`)に無い値と、数で書いた列挙の値を足した。
- 前からある射影の試験 18 本、`tests/threeway.rs` の全部(`mypy --strict`、`tsc --strict`、steep、整形器、決定性、全部の言語の一致)、`tests/builtin_names.rs`、`tests/error_value.rs`、`tests/boolean.rs` が通る。
- pyright の strict(1.1.414):`from` を持つコーパスの 8 本のモジュールは、替える前も替えた後も 50 件で、全部が前からある使われない補助関数(丸めの五つ、`_min`、`_max`、`_civil`)である。
- 替える前のビルド(db494dff)と比べた。コーパスと寄与を合わせた 4 組の `gen` の全部のファイルと、`from` を持つ 8 本の `api`・`doc`(Markdown と HTML)・証明書・`gen`(英語と日本語)で、違うのは列挙を読む 4 本の Python のモジュールだけだった。
- 型を付けた Python の部分集合を静的にコンパイルする処理系で、この節の生成物を確かめた。コーパスの 143 本と寄与を合わせた 8 本の全部で、拒否が 0 になり、CPython の答えがベクタ 11,044 件と一致し、ネイティブのビルドと CPython の印字 44,618 行が一致した。同じ 4 本を 0.29.1 の生成物にすると、それぞれ `_E = TypeVar("_E")` の一行で止まる。この処理系が翻訳するのは、モジュールの直下の文と、アプリケーションが呼ぶ関数である。取り出し関数は `dict[str, Any]` を受け取り、この処理系は `Any` を受け付けないので、確かめたアプリケーションは取り出し関数を呼ばず、列挙を読む補助関数も翻訳の対象にならない。
- `ritsu gen` で作った `shop` のプロジェクトの Python のパッケージでも、`rules/shipment_fee.py` は `_region_of` と `_window_of` を持ち、`TypeVar` が無く、`mypy --strict` を通る。
**正直な限界**:
- 表の種類は、コーパスと寄与を合わせた規則が書くものから決めた。生成器は、コーパスのどの規則も書かない形を一つ書く。お金・量・率を持ち越す machine の始まりの値(`INITIAL_BALANCE: USD = USD(20)`)で、0.29.0 から同じである。静的にコンパイルする処理系は、これも、モジュールが動くときに計算される値として受け付けない(再現の規則で確かめた)。そういう規則をコーパスに足すと、この節の試験が止まる。
- 試験が見るのは `rulec gen` の生成物である。`ritsu gen` のパッケージのモジュールは、`enums` のファイルのモジュールを相対の import(`from .goods_catalog import …`)で読み、試験の表にはその形が無い。
### 15.245 HTML のページで、説明の `<` とコードスパンを Markdown のとおりに見せる(2026-10-16)
**きっかけ**:規則の説明(`description`)にある、HTML として読まれうる `<` は、Markdown のページで `<` と書く(ritsu の DESIGN 16 章の頭)。`--format html` のページはその Markdown を `md_to_html` で HTML にするので、`inline_html` がこの `<` の `&` をもう一度 `&` にしていた。`description "A <img src=https://example.invalid/a.png> test"` は、HTML のページで `A <img src=https://example.invalid/a.png> test` と見えた。タグとして読まれることは無いが、書いた文のとおりには見えない。同じ説明の ``a`<i>`b`` も、`inline_html` がバッククォートを一つずつ開け閉めするので、空のコードスパン二つと `<i>` だけのコードスパンに割れ、バッククォートが消えていた。
**決定**:
1. **`inline_html` は、コードスパンの外の `<` を `<` のまま書く。** `md_prose` が書いた `<` は文の `<` で、HTML でも一度だけエスケープした形になる。説明に `<` と書いたときも `<` と見えるが、その Markdown をサイトが見せるときも同じである。ほかの文字参照(説明に書いた `&` など)は、前のとおり字のまま書く。
2. **コードスパンは、開けたのと同じ数のバッククォートの並びで閉じる。** `md_prose` と CommonMark の読み方である。閉じる並びが無いバッククォートは字のまま書き、前のように残りを全部コードにはしない。中の改行は空白にし、両端が空白で、空白だけではない中身からは、両端の空白を一つずつ外す(``` `` `x` `` ``` の中身は `` `x` `` になる)。
**確かめたこと**:
- `tests/doc.rs` の「説明のタグは文字のまま書く」に、HTML のページを足した。同じ説明が `<p>Sizes <=60cm, a > b, R&D. <img src=x onerror=alert(1)> … <code><b></code> and <code>a`<i>`b</code> and <code>`x`</code> and a ` alone</p>` になる。直す前のコードでは、`&lt;img` と空のコードスパンが出て、この試験が止まる。
- コーパスの 143 本を、0.29.2 のリリースのバイナリと直したビルドで `doc --format html` にかけると、英語と日本語の 286 ページが一字も変わらない。コーパスの説明には HTML として読まれうる `<` が無く、rulec が自分で書く Markdown のコードスパンは、どれもバッククォート一つで開け閉めするからである。規則のページを埋め込む dandori のサイトの例のページも、dandori の `tests/doc.rs` のとおり変わらない。
- playground の人が読むページは、作り直した `website/docs/playground/ritsu.wasm` で同じ直しになる。
**正直な限界**:
- HTML のページは Markdown から組むので、説明に書いた `<` と `md_prose` が書いた `<` を区別できず、どちらも `<` と見える。ほかの文字参照は、Markdown をサイトが見せるときは解かれ(`&` が `&`)、HTML のページでは字のまま見える。
- `inline_html` が読むのは、rulec が書く Markdown の部分集合(コードスパン、太字、バックスラッシュのエスケープ)のままである。説明に書いた `*強調*` やリンクは、HTML のページでは字のまま見える。
### 15.246 人が読むページを、狭い枠でも読めるようにする(2026-10-16)
**きっかけ**:playground は、人が読むページを右半分の枠に出す。ウィンドウが 1400px のとき枠は 761px で、左のペインは `clamp(15rem, 34%, 26rem)` の 34% にあたる 259px になり、入力と出力の表の見出しが「名/前」「範/囲」「注/記」と一字ずつ折り返した。日本語は字と字のあいだのどこででも行を折るので、表が列をいちばん狭くまで縮めると、見出しや名前が一字ずつになる。日本語の運賃の規則で 8 個のセル、クーポン割引の規則で 37 個のセルがそうなり、クーポン割引はウィンドウいっぱい(1400px)でも 17 個がそうだった。枠が 554px だと左のペインが下限の 240px で止まって図に 308px しか残らず、カードの表の右の列が切れる。スマホの幅(390px)では図が 144px になる。「試してみる」も、入力の多い規則では、入力がフォームの右へはみ出していた。
**決定**:
1. **狭い枠(719px 以下)では、左右に並べず一列にする。** 順は読む人がたどる順で、「試してみる」、それが色を付ける図とその説明、残りの文書である。二つのペインを `display: contents` でほどいて `order` で並べるので、DOM は動かさない(スクリプトは広い枠と同じに動く)。図はウィンドウの 70% の高さの枠で、その中で縦にも横にもスクロールする。境目のドラッグは出さない。
2. **左右に並ぶときの左のペインの下限を、15rem から 20rem に上げる。** 761px の枠で左のペインは 320px、図に 435px が残り、運賃のカードが切れずに入る。720px の枠でも図に 394px が残る。
3. **左のペインの表は、語のあいだでだけ折る。** 見出しは折らず(`white-space: nowrap`)、セルは `word-break: keep-all` で日本語の字のあいだでは折らない。最後の列の注記は文なので、ふつうに折る。それでも入らない表は、その表だけが横にスクロールし、ペインは動かない。
4. **「試してみる」の中身の幅が 20rem 以下のときは、入力の名前を上、入力を下に置く**(コンテナクエリ)。入力はフォームの幅を越えない(`min-width: 0` と `max-width: 100%`)。
**確かめたこと**:
- ritsu の `tests/playground.rs` に、日本語の運賃とクーポン割引の規則のページを Chrome で 1400・761・390px で開く試験(`the_page_for_people_reads_in_a_narrow_frame`)を足した。どの幅でも一字ずつ折れたセルが無く、入力がフォームからはみ出さず、文書がウィンドウより広くならないこと、761px 以上では左のペインが 320px 以上あること、390px では「試してみる」、図、残りの文書の順に一列になることを確かめる。
- 規則のページを埋め込む dandori のサイトの例のページ(英日 10 枚)は、スタイルの一行だけが変わるので取り直した。playground の `website/docs/playground/ritsu.wasm` も作り直した。
**正直な限界**:
- 一列のとき、図はページの中の小さなスクロールの領域なので、スマホでは図の中とページのスクロールが入れ子になる。
- 左右に並ぶときの 320px のペインでは、注記の長い表は横にスクロールする。隠れた列があることは、表の右の端が切れて見えることでしか分からない。
- 境目の 720px は、左のペインの 320px と図に要る 400px ほどを足した幅で、規則の大きさによっては変えていない。
### 15.247 見出しに書く型は、その名前がもう持っている型と同じにする(2026-10-17)
**きっかけ**:出力を `運賃(fee) : money[円, excl_tax]` と宣言し、それを決める表の見出しが `-> 運賃(fee) : money[円, incl_tax]` のままの規則が、`check` を何も言われずに通った。表の出力の列に書いた型は、その列のセルを読む型になるだけでなく、その表から後ろでは、その名前の型を置き換えていた。出力の宣言と比べるところが無かった。`rulec api` で見ると、生成する Python は `def fee(dest: Zone, weight: Gram) -> YenInclTax:` で、宣言の税抜が消えている。見出しを `mass[g]` にし、セルを `800g` などにしても通り、出力はグラムになって、`round up(10円)` の丸めも `api` から消えた。一つの名前を二つの表が決めるときも同じで、上の表が `money[JPY, incl_tax]`、下の表が `money[JPY, excl_tax]` と書くと、名前は下の表の型になった。それを読む後ろの表が下の表と同じ型なら、上の表の税込の金額が税抜として黙って通った。例の見出しに書いた型は、だれも読んでいなかった。畳み込みの答えには、出力の名前に違う型を書けば止める検査が §15.219 からあり、表と節と例の見出しにだけ無かった。
**決定**:
1. **見出しに型を書くなら、その名前がもう持っている型と同じでなければ E103 にする。** 名前の型は、出力なら出力の宣言の型、上の表や節が決める名前ならその型である。比べるのは型そのもの(通貨、税区分、量の単位、列挙、真偽、値が無いことがあるか)で、`unifies` のように税区分の無い金額をどちらとも合うとはしない。見出しの `money[円]` は、`money[円, incl_tax]` の出力から税区分を落とすからである。
2. **同じ型をもう一度書くのは、これまでどおり通す。** リポジトリの 544 本の規則のうち 223 本が、出力の名前の見出し 243 か所に型を書いていて、どれも出力の宣言と同じ型である。コーパスの書き方で、台帳の最小の再現も 137 のうち 76 がそう書く。
3. **診断は見出しの型に印を付け、名前の型が決まったところ(出力の宣言か、上の表や節の見出し)にも印を付ける。** その表のセルはこれまでどおり見出しに書いた型で読み、名前は前の型のままにする。名前を読む後ろの表に、同じ食い違いをもう一度言わせないためである。
4. **どちらを直すかは書く人が決める。** 注記は、ここで決める値が出力の値になることを言い、出力の宣言か、見出しの型のどちらかを直すよう求める。見出しの型を消せば名前の型で読むことも言う。税込の料金表を転記した表なら、直すのは出力の宣言のほうかもしれないので、`fix.text` は付けない。
5. **例の見出しも同じに比べる。** 例の値は前から出力の型で読んでいて、見出しの型は使われていなかった。違う型を書けば E103 にする。
6. **準用で入る表の見出しは比べない。** その型は `apply` が元の規則の宣言から引き継いだもので、元の規則はそれ自身の検査で比べられる。寄与のファイルの表と節は、規則の表と同じに比べる。
7. **新しいコードは作らない。** E103 は、単位・通貨・税区分の食い違いと、型の書き方の誤りを受け持つ。畳み込みの答えの E021(§15.219)は変えない。
**捨てたもの**:
- **出力の名前の見出しには型を書かせないこと。** 223 本の規則を書き直すことになり、表だけを読む人には型が見えなくなる。
- **警告にとどめること。** 見出しの型が出力の型になっていたので、生成するコードの型が変わる。通してよい場合が無い。
- **出力の宣言のほうを黙って勝たせること。** セルを税込で書いた表なら、税込の値が税抜として出る。どちらが正しいかは、表を転記した人にしか分からない。
- **率の刻みまで比べること。** rulec の型は刻みを持たず、率の出力が呼び出し側へ渡る刻みは出力の宣言が決める(§15.144)。表の見出しに書いた刻みは、新しい名前の見出しでも列の刻みを決めない。列の刻みはセルの値から決まる(§15.16)。
**変わること**:
- 宣言と違う型を見出しに書いた規則が、E103 で止まる。リポジトリの 544 本の規則では、E103 の数がどの規則でも変わらない(0.29.1 のバイナリと比べた)。型を書いた節の見出しは 3 か所で、どれも新しい名前か同じ型である。型を書いた例の見出しは無い。
- 台帳の E103 の「いつ出るか」と「直し方」に一文ずつ足し、この形の最小の再現を足した(`docs/codes.md`・`docs/codes.ja.md`)。リファレンスの表・節・例の節と、AGENTS.md の E103 の項にも書いた。
- playground の日本語の最初の例(`website/rulec/tools/overview-ja.rule`)の見出しから型を消し、英語の版(`-> fee`)にそろえた。日本語のトップページの抜粋も同じにした。トップページの二つの図は変わらない(`make_overview.py --verify`、`make_checks.py --verify`)。playground の `projects.json` と `ritsu.wasm` を作り直した。
**確かめたこと**:
- `tests/positions.rs` に、型を書ける見出しの表を足した(出力の宣言を変えたとき、表の見出し、ほかの単位の見出し、上の表と違う型の節の見出し、例の見出し、畳み込みの答え)。診断が見出しの行に出ることまで求める。前のコードでは、出力の宣言を変えたときと例の見出しは何も言われず、表と節の見出しは、名前を読む後ろのセルが E103 を出すことがあるだけだった。
- 変異を二本足した。ゆうパックの運賃の出力を税抜にして、表の見出しの税込を直し忘れた規則(`m_e103col`)と、その英語の版(`m_e103colen`)で、前のコードはどちらも通していた。文面は golden で固定した。
**正直な限界**:
- 率の見出しの刻みは比べない(上)。`rate[step 1%]` の出力を `rate[step 0.1%]` の見出しの表が決めても何も言わず、刻みに載らないセルは、丸めの格子の E106 が言う。
- 寄与のファイルの見出しが規則の名前と違う型を書いたとき、二つ目の印は規則のファイルの行を指すが、端末に出す文面は、その行がどのファイルの行かを書かない(JSON の `spans` には `file` がある)。
### 15.248 ritsu のパッケージの中では、お金と量のブランドをパッケージの単位のモジュールから読む(2026-10-18)
**きっかけ**:`ritsu gen` のパッケージ(ritsu の DESIGN 9.3)で、規則の出力を chobo の帳簿の振替に、ワークフローを通さずに手で書いたコードで渡すと、単位がどこでも確かめられなかった。規則のモジュールは額をブランドの型(TypeScript の `bigint & { readonly __rulec: "USDc" }`、Python の `NewType`、Go の `type USDc int64`)で返すが、帳簿のクライアントは額を素の整数(`bigint`、`int`、`int64`)で受け取る。ドルで数える規則の手数料を、セントで数える帳簿に渡しても、tsc も mypy も何も言わなかった。Go では `int64(...)` と書いて外すことになり、外せば何も言わない。dandori のフローに書けば dandori の E003 で止まる取り違えである。規則のあいだでも同じで、ブランドは規則のモジュールごとに宣言していたので、Python と Go では、同じ単位が規則ごとに別の型だった。TypeScript でも、`円` と `JPY` で書いた二つの規則はブランドの文字列が違い(`YenInclTax` と `JPYInclTax`)、`円` の規則が返す額を `JPY` の規則に渡すと、三つの言語のどれもが型の違いとして止めた。
**決定**:
1. **パッケージの中では、お金と量のブランドを、パッケージの単位のモジュールのものにする。** 規則のモジュールはブランドを宣言せず、単位のモジュールから同じ名前で読んで公開する。TypeScript は `import type { USDc } from "../units"` と `export type { USDc }`、Python は `from ..units import USDc as USDc`、Go は `type USDc = units.USDc` である。呼ぶ側から見た名前は、`rulec gen` のモジュールと変わらない。
2. **単位のモジュールは `units_module` が書き、ritsu がパッケージに置く。** 型の表し方は §8.2 のブランドのままで、一つの単位に一つの型を置く。名前は、表がその単位を最初に書く綴りから作る(`JPY` の額は `円` の型 `YenInclTax`、`mL` の量は `cm3` の型)。パッケージのファイルが別の綴りで書いた単位は、その綴りの名前を別名として置く。どの単位を置くかは、規則のモジュール(`package_parts` が返す)と帳簿のクライアント(chobo の DESIGN 4.3)が使う単位の和である。
3. **率は規則のモジュールに残す。** 率の整数が数える刻みは入力の宣言にあり、`Ty::Rate` は刻みを持たない(ritsu の DESIGN 5.2)。二つの規則の `Rate` が同じ値を表すとは限らないので、共通の型にしない。数(`number`)には、これまでどおりブランドを付けない。
4. **ブランドの名前の表を ritsu-units に移す。** `brand_of` が持っていた量の単位の英語の名前と、`円`・`銭` の名前を、`ritsu_units::table::type_name` にした。chobo のクライアントと ritsu の単位のモジュールが、規則と同じ名前で単位の型を呼ぶためである。`brand_of` の答えは変わらない。
5. **`rulec gen` の出力は変えない。** パッケージの外のモジュールは、これまでどおりブランドを自分で宣言する。
**捨てたもの**:
- **TypeScript だけ、ブランドの文字列を単位ごとに一つにすること。** TypeScript のブランドは構造で比べるので、文字列をそろえればモジュールのあいだで渡せる。Python の `NewType` と Go の定義した型は名前で比べるので、宣言が一か所でなければならない。三つの言語を一つの形にした。
- **規則のモジュールが公開する名前を、最初の綴りの名前に変えること。** `JPY` で書いた規則の `JPYInclTax` が `YenInclTax` になると、`rulec gen` とパッケージで呼び方が変わる。単位のモジュールに別名を置けば、名前は書いたとおりのまま、型は一つになる。
- **帳簿のクライアントが、規則のモジュールの型を読むこと。** どの規則とも関わらない帳簿があり、規則の無いパッケージもある。
**変わること**:
- パッケージの規則のモジュールが、お金と量のブランドを宣言する代わりに、単位のモジュールを読む。お金も量も使わない規則のモジュールは変わらない。Python のモジュールは、自分で宣言するブランドが無ければ `NewType` を読まない。
- `package_parts`(`package_module_in` の答えに、単位のモジュールに置く単位を添える)と `units_module` を足した。
- sekisho のテストが組み立てるパッケージ(`tests/typescript.rs`、`tests/python.rs`、`tests/go.rs`)に、単位のモジュールを置くようにした。例の規則(`refund_limit`、`approval_limit`)が金額を扱うからである。
**確かめたこと**:
- リポジトリの 546 本の規則で、`rulec gen` の 12 の言語の出力が、変える前のバイナリの出力と一字も同じである。
- ritsu の `tests/gen.rs`:`円` で書いた規則の値段を `JPY` で書いた規則に渡すコードが、三つの言語の型の検査を通って走る(変える前は三つとも止めた)。テストのプロジェクト `marketplace` の決済のコードが、規則の額を帳簿の振替に渡して三つの型の検査を通り、規則をドルで数える版では三つとも止まる。stockroom と sekisho の例のパッケージも型の検査を通り、sekisho の三つの言語のテストは、生成したコードが参照の評価と同じに答えることを確かめる。
**正直な限界**:
- 単位の型が守るのは、型のまま受け渡す額だけである。手で書いたコードが額を足し引きすれば、TypeScript と Python では答えがブランドを失い(`bigint`、`int`)、呼ぶ側が型を書き直す(`as units.USDc`、`units.USDc(...)`)。そこに別の単位を書けば、型の検査は止めない。Go の定義した型は、足し引きしても型が残る。
- 単位のモジュールを置くのは `ritsu gen` だけである。`rulec gen` で別々に作ったモジュールのあいだでは、これまでどおり、TypeScript でブランドの文字列が同じときにだけ渡せる。
### 15.249 optional の出力を、セルの `none`、包む型で読む値と丸め、上の表から来る値の無いことまで通す(2026-10-18)
**きっかけ**:§15.201 の「見つけて直していないもの」の一つ目。出力の列の型を `T?` にすると、型の検査だけが取り残されていた。
- 出力のセルの `none` が、どの型でも E012(`none` は値の名前としても、宣言された名前としても見つかりません)になった。例の期待値の `none` も同じだった。金額、数、列挙、真偽、日付、文字列の六つの型で同じだった。
- 数の optional の出力は、一つも書けなかった。丸めの刻みもセルの数のリテラルも `T?` の型のまま読んだので、`round down(1USDc)` も `439USDc` も E103 になった。E103 の注は、`money[USDc]?` には `round down(1)` と書くよう勧めていた。
- 数の optional の出力に `round` が無くても、検査が通った。同じ規則から `?` を外すと E104 になった。
- optional の入力を出力にそのまま渡す規則は、検査を通った。ところが生成したコードは、六つの生成先で動かなかった。Rust、Go、Swift と、Rust を使う WASI と Wasm はコンパイルできず、NumPy は値の無いことを `"none"` という語で返した。TypeScript は、列挙の入力を `bigint` と宣言した変数に入れていて、tsc --strict を通らなかった(Node は型を読まずに走らせるので、答えは合っていた)。
- optional の列を下の表が読むと、上の表が渡す値の無いことに、下の表は行を求められなかった。上の表が optional の入力を渡す規則では、下の表に値の無いときの行が無くても通り、生成したコードは「到達不能」で止まった。値の無いときの行を書くと、どの入力も当たらない行(E102)と言われた。
**原因**:
- パーサは出力のセルの `none` を名前として読み、型の検査は、宣言された名前でも列挙の値でもない語として E012 にした。参照評価器は同じ語を値の無いことと読んだので、`none` の行は、型の検査さえ通れば正しく答えた。
- 数のリテラルを値に読む `lit_value_in` が、`T?` を包む型として読まなかった。§15.201 は入力の範囲と刻みとセルを、呼ぶ側で包む型に直していたが、出力のセル、丸めの刻み、出力の範囲と保存の刻み、出力の刻み(率)は直していなかった。生成器も丸めの刻みを同じ関数で読み、読めないときは 1 を使っていた(検査が E103 で止めていたので、表には出なかった)。
- E104 は `Ty::is_numeric` で数の出力かを見ていた。`is_numeric` は `T?` の中を見ない。
- 生成器は、表の出力の列の変数を、`T?` に合わない型で宣言していた(Rust は `i64`、TypeScript は `bigint`、Swift は `Int64`)。Go は列を `*USDc` で宣言しながら、返すところで `*USDc(price)` と書いていた。丸めと単位の型を付ける式も、値が無いことを考えずに組んでいた。
- 下の表が読む列について、上の表の行が書く値(`Checked::row_writes`)に、値の無いことが入っていなかった。値の無いことのある名前を渡す行は、包む型の値だけを書くものとして読まれ、`none` のセルは何も書かないものとして読まれた。
**決定**:
1. **`none` を書けるところ。** 出力の列の型が `T?` なら、セルに `none` と書けて、値が無いことを答える(構文木の `OutCell::Nothing`)。例の期待値の `none` も、値が無いことを言う。`T?` でない列(規則の出力、表が自分で決める列、例の期待値)に書いた `none` は E103 にする。注は、その列が optional でないことと、`T?` にする直し方を言う。直すところは、規則の出力なら `outputs` の宣言(型を書いた見出しもそろえる)、表が自分で決める列ならその見出しである。どちらを直すかは書く人が決めるので、`fix.text` は付けない。値の無いことのある名前(optional の入力など)を `T?` でない列に書いたときの E103 にも、同じ直し方の注を付けた。`T` の値の名前は、`T?` の列に書ける。値があるときの答えである。
2. **包む型で読むもの。** `lit_value_in` が、`T?` を包む型として読む。optional の出力の、セルのリテラル、例の期待値、丸めの刻み、率の刻み(`out_scales`)、列の範囲と保存の刻み(`ranges`、`scales`)を、包む型で読む。§15.201 の決定 3 の入力と同じ形である。丸めの刻みの E103 の注は、包む型の単位で書く(`round down(1USDc)`)。値を示す文(E106 の注、例の値、ページの値)も、包む型の単位で書く。証明書の `ranges` に書く optional の数の範囲は、値を書く行の値から決まり、二つの再検査も、その行だけから確かめる。`none` の行は数を書かない(その語は、ファイルの答えのセルから読み戻す)。前は recheck.py がこの形の証明書を全部落とし(`none` の行を、読めない値を書く行とした)、Lean はこの列を見ていなかった。
3. **丸めの宣言。** 数の optional の出力にも `round` を求める(E104)。E104 の注に添える「端数が出る入力」は、optional の入力に包む型の値を入れて探す。値の無いことを入れると端数が出ず、注が付かなかった。
4. **上の表から来る値の無いこと。** 下の表が読む optional の列(列挙と真偽、§5.3)は、上の表が `none` を書く行と、値の無いことのある名前を渡す行で、値の無いことをとる(`row_writes`、`out_values`、`outputs_over` の絞り込み)。完全性(E101)はその値に行を求め、その値の行を E102 と言わない。証明書は、上の表の行が書く値(`produces`)に `none` を書き、二つの再検査はそれをファイルから読み戻す。下の表が、上の表のどの行も書かない値(`express`)を書かずにすむという事実(`above` の `never`)も、`none` を書く行のある表について言える。前は `none` の行を「読めない値を書く行」としたので、事実を言えず、二つの再検査が証明書を落としていた。
5. **生成したコード。** `none` のセルでは、その生成先が optional の値の無いことに使っている形を返す。Python は `None`、TypeScript と JavaScript と PHP と Java は `null`、Rust は `None`、Go と Swift と Ruby は `nil`、SQL は `NULL`、NumPy は値の無い要素が `None` のオブジェクトの配列である。MCP とレコードは JSON の `null`、Connect は値を入れないフィールドである(`.proto` の注は「無い場合は入らない」と書く)。表の列の変数は、その言語が optional の入力に与えている型にした(Rust は `Option<USDc>`、Go は `*USDc`、Swift は `USDc?`、Java は `Long`、TypeScript は `bigint | null`、Python は `int | None` の注釈)。だから optional の入力は、そのまま渡せる。丸め、刻みの換算、単位の型は、値があるときだけ掛ける(Python の `None if price is None else USDc(_round_down(price, 1))`、Rust の `price.map(|__v| USDc(round_down(__v.0, 1)))`、Go は値があるときだけ新しいポインタを作る)。NumPy は、値の無いところを列の外に持ち(§15.201 の入力と同じ)、出力ではそこを `None` にする。
6. **ritsu の口。** `RuleFacts` の出力の型(`ColumnType::Opt`)、`eval` の答えの `Value::None`、`outputs_over` の `Value::None` は、今までと同じ形で値の無いことを伝える。`output_values` は、数の optional の出力には、これまでどおり答えを決めない(`Found::Undecided`)。答えは数の範囲で、値の無いことを入れる場所が無いからである。理由の文は「`price` には値が無いことがあるので、とる数を範囲で答えられません」にした(前は「数の出力ではありません」と言っていた)。
7. **機械の `once`。** optional の出力の `once` の行を、証明書の行に載せる。行の語(`words`)には、行のセルが値の無いことをとるとき `none` が入り、`none` のセルの行はその語を読む。前は optional の出力のどの行の値も読まず、`once` は「行に載せていない」として述べるだけだった。これを直す途中で、名前を渡すセルが、その名前の綴りを語として読まれていたことが分かった(`| guest | - | member | flag |` の `flag`)。どの `once` のセルもその語をとらないので、その行は数えない行になり、本当は数えうる行が数えられていなかった。検査そのもの(E127)は正しく数えていたので、誤った答えは出ていない。ただ、証明書は数える行を誤って書き、二つの再検査はそれを確かめていなかった。名前を渡すセルは、計算する値として扱い(`value` は null、どこでも数えうる)、二つの再検査は、語が出力の値(optional なら `none` も)であることを確かめる。名前の綴りを語として書いた証明書は、二つとも落とす。
8. **W111。** 値をそのまま運ぶ名前のセル(`| false | requested |`)が書く列挙の値は、その列挙をどの表も列で確かめていないとき、「どの行にも現れない値」に数えない。どの表もその値の行を持ちようがなく、`default` の印は「既定の行に落ちる」という誤った意味になるからである。列挙をどこかの表が列で確かめていれば、その表が `-` で吸っている値を、これまでどおり言う。optional の入力に限らず、ふつうの入力を渡す規則も同じである。
9. **そのほかの口。** `rulec doc` の案内のページは optional の出力を「〜、または無し」と書く。`rulec api` の NumPy の項は、optional の出力をオブジェクトの配列と言う。`rulec adapter` のテンプレートは、最初の出力が optional なら、値が無いときに返す形(`None`、`nil`)を書き添える。NumPy の runner は、日付の出力を YYYY-MM-DD で書く。前は日の番号で書いていて、optional でない日付の出力でも参照評価器と食い違っていた。網羅の監査は、optional の入力の値を、値があるときの数として読む。前は数として読まなかったので、optional の金額を渡す出力が、届かない丸めの境目の対を求められていた。
10. **式の中の値の無いこと。** §2.1 は「`T?` は式には現れない」と決めていたが、型の検査はそれを確かめていなかった。式の中の名前の型が `T?` でも、演算の型を `T?` のまま運んだので、`define d : money[USDc]? = quoted * 2` が通り、生成したコードは値の無いことに 2 を掛けた。optional の出力が書けるようになると、この定義を出力にそのまま渡す規則が通ってしまう。演算と関数の引数に値の無いことのある名前を書いたら E103 にする(「`quoted` は値が無いことがあるので、式には書けません」)。名前だけの式(`define d : service? = requested`)は演算をしないので通し、値の無いことをそのまま渡す。
11. **準用。** 準用した規則の出力は、この規則の定義になり、元の規則の丸めが掛かる(§15.69)。値の無いことのある数の出力は、丸めを掛けた定義にできないので、E044(準用できません)にして、その出力の名前と理由を言う。数の optional の出力には必ず丸めがある(3)。値の無いことのある列挙・真偽・日付・文字列の出力は、名前だけの定義になり、値の無いことをそのまま渡す。`with` で読み替える列挙の出力では、読み替えの表に `none` から `none` への行を足す。
12. **ケースを試すページ。** ページのスクリプトは、optional の入力と出力を、包む型の欄として受け取る(`optional` の印が付く)。入力は、選ぶ欄(列挙と真偽)なら先頭の `none` が、書き込む欄(数、日付、文字列)なら空のままが、値の無いことである。例の `none` は `null` として入り、URL には空の値として残る。値の無い答えは `none` と出し、値のある答えは包む型の単位で出す。前は optional の入力も出力も文字列の欄で、値の無い答えは `null` と出た。生成したコードは、欄に書いた語を金額、日付、真偽として受け取らなかった(`declared is not an integer: 50000`)。数と日付の optional の入力を持つ規則の例は、リテラルが読めずにページから落ちていた。決定 2 でリテラルが読めるようになると、その例が「例」のボタンに出て、押すと同じ文で止まる。だから入力の欄もここで直した。文字列の optional の入力には、空の文字列を値として入れられない(空の欄は値の無いことになる)。
**捨てたもの**:
- **`none` を名前のまま読み、使うところごとに `none` という語を見分けること。** 出力のセルを網羅的に読むところだけで 21 か所あり、名前のセルだけを見るところはさらに多い。どこか一つが見分け忘れると、`none` が列挙の値か入力の名前として読まれる。`none` は予約語で、名前にも列挙の値にもならないので、構文木で一つのものにした。網羅的に読むところは、コンパイラがそれぞれを挙げる。
- **`T?` でない列の `none` を、E012 のままにすること。** E012 は宣言されていない名前の診断で、`none` はどの規則でも宣言されない予約語である。`T?` でない列の `none` は、列の型が持たない値を書いたことなので、E103 の受け持ちである(E103 の台帳は、列の型にない値を出力のセルに書いたときを挙げている)。
- **値が無いときに、0 や空の文字列や型の既定の値を返すこと。** 呼ぶ側が、値の無いことと 0 円を見分けられない。
- **`output_values` が、値があるときの数の範囲を答えること。** 答えには値の無いことを書く場所が無く、ritsu-cross は、値の無いことのある額を、いつも額があるものとして突き合わせることになる。
- **W111 で、運ばれる値をいつも使われたものに数えること。** 同じ列挙を別の表が列で確かめ、`-` で値を吸っているとき、その警告が消える。
- **値の無いことのある数を準用するとき、丸めを掛けずに名前だけの定義にすること。** 元の規則の列の値が丸めの刻みに載っている保証は、リテラルにしかない。入力をそのまま渡す行の値は、刻みより細かいことがある。
- **ページで、optional の入力の欄の横に、値の無いことを選ぶ印を置くこと。** 入力の欄も並びの行の欄も、値一つを要素一つで読む。印を足すと、読むところが全部、要素を二つ見ることになる。選ぶ欄の先頭の `none` と空の欄で、同じことが言える。
- **表の行が列の値を確かめていれば、その列の値の無いことのある名前を `T` の列に書けるようにすること**(`| >=0USDc | quoted |`)。値のあることを表で確かめてから式に使う形になるが、この節の範囲を越える。書けないことは「見つけて直していないもの」に挙げた。
**変わること**:
- 数の optional の出力で `round` の無い規則が、E104 で止まる。前は通っていた。直し方は、ふつうの数の出力と同じく、丸めを宣言することである。
- `T?` でない列に書いた `none` は、E012 から E103 になる。
- 値の無いことのある名前を演算や関数の引数に書いた規則が、E103 で止まる。定義の型を `T?` にしていれば、前は通っていた。`T` にしていれば前も E103 だったが、文は「型が合いません」だった。
- 値の無いことのある数を出力に持つ規則の準用は、E044 で止まる。前は、元の規則が値の無いことのある入力を丸めずにそのまま渡す形なら、元の規則も準用する規則も検査を通っていた。この版では元の規則が E104 になり(準用する規則は、その E104 を挙げる E044 になる)、丸めを宣言すると、準用する規則が E044 になる。元の規則が丸めを宣言していた形は、前も元の規則が E103 で止まっていた。
- `none` を書く行のある上の表を下の表が読むとき、`never` の事実を証明書に書けるようになる。optional の出力の `once` の行が、証明書の行に載る。名前を渡すセルを持つ出力の `once` の行は、証明書でその行の値が null になる。前の rulec が書いたその形の証明書は、新しい二つの再検査で落ちる。
- 値をそのまま運ぶ名前が書く列挙の値に、どの表もその列挙を列で確かめていなければ、W111 が出なくなる。
- NumPy の runner は日付の出力を YYYY-MM-DD で書く。
- ケースを試すページのスクリプトが変わるので、どの規則のページも変わる。optional の入力を持つ規則のページは、その入力を包む型の欄で書き、`none` を書いた例も「例」のボタンに出す。
- 二つの再検査は、optional の数の出力の範囲が、値を書く行の値を全部含むことを確かめる。Lean の再検査は、前はこの列を見ていなかった。
**確かめたこと**:
- `tests/optional.rs` に 15 件を足した(§15.201 の 8 件と合わせて 23 件)。材料は `tests/optional` で、英語の規則を先に、日本語の版を横に置いた。
- 変異の七つ(英日で十四)が、それぞれ一つの診断を言う。optional でない出力、例の期待値、表が決める列に書いた `none`(E103。注は `?` を付ける所を言う)、値の無いことのある入力を optional でない出力に渡す行(E103)、丸めの無い optional の金額(E104)、上の表が渡す値の無いことに行の無い下の表(E101、`service = none`)、値の無いことを期待して値を答える例(E107)。英日の出力は golden と同じで、日本語の版は同じ行に同じ診断を言う。
- `parcel_answers.rule`(金額、数、列挙、真偽、日付、文字列の六つの optional の出力に、`none` と値を書く)と `carried_answers.rule`(optional の入力を optional の出力にそのまま渡し、下の表がその列を読む)と、それぞれの日本語の版は、診断を一つも出さず、網羅の監査の七つの基準を満たす。替える前は、`none` のセルが E012、`439USDc` と `round down(1USDc)` が E103、下の表の `none` の行が E102 だった。
- 参照評価器が `none` と値を答え、全部の生成先が同じに答える。runner、MCP、Connect、WASI、Wasm、PostgreSQL の関数、NumPy の 24 の口に、どの optional の出力にも値の無いことを求めるベクタを流した。生成した Python は mypy --strict を、TypeScript は tsc --strict を、Ruby は steep を通る。替える前は、入力をそのまま渡す規則で、Rust、Go、Swift、Wasm がコンパイルできず、NumPy が語の `none` を返した。
- 二つの再検査が、`none` を書く行のある証明書を通し、偽造を落とす。上の表が書かない値の事実(`never`)を足したものと消したもの、`none` を書く行で別の語を読むもの、`once` の行の語を出力の値でない語にしたもの、optional の金額の範囲を狭めたもの、金額を書く行を `none` と偽ったものである。
- ritsu の口は値の無いことを今までと同じ形で伝える。過去の答えを読む口(`fixtures lint`、`replay`、`verify`、`diff`)は `none` を読む。ページは値の無い答えを `none` と出し、optional の入力を持つ規則のケースを、全部の例で試せる。値の無いことのある名前を式に書いた定義は E103 になり、名前だけの定義は通る。準用は、列挙と `with` の読み替えで値の無いことを渡し、数の出力では E044 と言う。
- 替える前と後の突き合わせ。木の 547 本の規則の `check`(英日と JSON、`--show-shadow`、終了コード)、証明書、`doc`、`coverage`、`vectors`、`api`、`schema`、`graph`、`adapter` は一字も変わらない。`doc --format html` と `gen` が置くページ(325 本)は、ケースを試すスクリプトだけが変わり、optional の入力を持つ 10 本では規則の説明も変わる。`gen` の NumPy の実行時は、一つのファイルとして変わる。166 本の `.gate` と 195 本の `.flow` は一字も変わらない。ブラウザで試すページの答え 1,154 件は、ページのスクリプトと NumPy の実行時のほかは変わらない。木の証明書 336 本は、新しい二つの再検査を通る。
- rulec の全体(tools の段)の 1,138 件、ritsu の `check`・`cross`・`gen`・`run`・`playground`・`skill`・`website`・`release` の 102 件、ritsu-cross と ritsu-project の 30 件、dandori と sekisho(fast の段)の 236 件が通った。ritsu の `playground` は、作り直した `ritsu.wasm` で、ページが開く例とリンクを Chrome で確かめる。
**見つけて直していないもの**:
- **畳み込みの答えの値の無いこと。** `fold` が optional の出力を答えるとき、`empty -> none` は E012 になり、要素の値(`take_first amount`)は E103(`money[USD]? is given money[USD]`)になる。表のセルと同じ読み方を畳み込みの答えに入れるには、生成先ごとに畳み込みが持つ値の型を変えることになり、この節の範囲を越える。
- **表の行が値を確かめた optional の名前を、`T` の列に書くこと**(`| >=0USDc | quoted |`)。E103 のままである(捨てたものの最後の項)。
- **名前のセルが運ぶ列挙の値のベクタ。** ベクタは、名前のセルが下の表へ運ぶ列挙の入力の値を一つしか試さない。`carried_answers.rule` から例を外すと、下の表の `library` と `express` の行に届くベクタが無く、`rulec coverage` は 8 行のうち 6 行で止まる。optional でない列挙を渡す規則でも同じで、替える前の rulec でも同じだった。
- **準用で、値の無いことのある数の出力を受け取ること。** E044 のままにした(決定 11)。
### 15.250 PostgreSQL の関数の一致検査は、実行ごとのスキーマで走らせる(2026-10-18)
**きっかけ**:`sql/<alias>_function_runner.py`(§15.80)は、関数を接続先の検索パスの先頭のスキーマ(ふつうは `public`)に作り、走り終えたら落としていた。同じデータベースで二つの実行が重なると、片方が落ちる。`_function.sql` の頭の `DO` ブロックは同じ名前の関数を全部落としてから作るので、二つの `CREATE FUNCTION` が同時に来れば一意制約の違反(`pg_proc_proname_args_nsp_index`)になり、少しずれれば相手が作った関数を落として、相手の呼び出しが「関数が無い」で落ちる。ritsu の CI では、`tests/formats.rs` の二つの試験が同じ規則の `rulec test` を同時に走らせ、ときどき落ちていた。手元で変える前のランナーを四つ同時に走らせると、5 回とも二つか三つが落ちた。二つのブランチの CI が一つのデータベースを使えば、使う人の手元でも起きる。一つで走らせても、データベースにもともとある同じ名前の関数(`_function.sql` を入れたデータベースのもの)を頭の `DO` ブロックが落とし、終わりの `DROP` が作り直した関数も落とすので、走り終えるとその関数が無くなっていた。
**決定**:ランナーは実行ごとに名前の重ならないスキーマ(`rulec_run_` と 16 桁の 16 進)を使う。psql のセッションの頭でそのスキーマを作って `search_path` を向け、セッションが終わるたびに `DROP SCHEMA … CASCADE` で落とす。規則の関数も列挙の表示名の関数もそこに作られ、呼び出しもそこを引く。`_function.sql` は変えない。使う人がデータベースに入れるファイルに、スキーマは書かない。
**捨てたもの**:
- **関数を作るところを、勧告的ロック(`pg_advisory_lock`)で順番にすること。** 重なりは無くなるが、データベースにもともとある同じ名前の関数を落とすことは変わらない。
- **`PGOPTIONS` で `search_path` を渡すこと。** セッションの頭の `SET` で足り、使う人が `PGOPTIONS` に書いたものと混ざらない。
- **一時スキーマ(`pg_temp`)に作ること。** PostgreSQL は関数の名前を一時スキーマから探さない。
**変わること**:
- 一致検査に使うロールに、データベースへの `CREATE` の権限が要る。スキーマを作るためで、データベースの所有者は持っている。これまでは `public` に作る権限が要った(PostgreSQL 15 からは、所有者でなければ持たない)。
- 生成する `sql/<alias>_function_runner.py` が変わる。`sql/<alias>_function.sql` とほかのファイルは変わらない。
**確かめたこと**:
- `tests/sql.rs` の `同じデータベースで走る関数は互いにもデータベースの関数にも触れない`:同じ規則のランナーを四つ同時に走らせると、四つとも同じ記録を返し、データベースにもともとある同じ名前の関数が残る。変える前のランナーでは落ちる。
- 手元の PostgreSQL 18 で、直したランナーを四つ同時に 5 回走らせると、5 回とも四つ全部が通り、スキーマも残らなかった。
**正直な限界**:
- ランナーがセッションの途中で殺されると、そのスキーマが残る(前は関数が `public` に残った)。名前が `rulec_run_` で始まるので、見つけて落とせる。
### 15.251 列挙の入力の範囲を、数の範囲が通るところに通す(2026-10-18)
**きっかけ**:順序のある列挙の入力に書いた範囲(§15.210)は、規則の検査と生成したコードの入口では数の範囲と同じに働くが、そこから外へは出ていなかった。
- ritsu の口(`ports::Engine`)は列挙の列を `ColumnType::Enum(<列挙の名前>)` で渡し、範囲を渡さなかった。口を読む dandori は、`campaign : tier range >=basic <=gold` の入力に、範囲の無い列挙の入力をそのまま渡すフローを通した。数の入力なら、同じ形で E014 と言う。生成したワークフローは、走らせたときに、規則の生成コードの `RuleInputError`(`campaign is out of range`)で止まる。sekisho も、プラチナまである列挙の属性を渡すゲートを通した。
- 口の `eval` も範囲を確かめず、`campaign = platinum` に答えた。生成したコードの入口はこの値を受け付けないので、参照の答えと生成したコードの答えが割れた。口の `eval` を、コーパスの全部の規則について、生成したコードの入口が受け付けない入力(`rulec gen` が書く入口の試験、§15.204)にかけると、同じ形の食い違いがほかに二つあった。暦に無い日(`2026-02-30`)を日付として読んで答えていたことと、並びの要素のフィールドを範囲で確かめていなかったことである。口が運べる 1,724 件のうち 66 件に答えた(列挙の範囲の外が 2、暦に無い日が 12、範囲の外か暦に無い日の要素のフィールドが 52)。口の評価器の試験は、`rulec vectors` のベクタ(受け付ける入力)しか渡していなかったので、どれも見つからなかった。
- `outputs_over` は、範囲を書いた列挙の入力を範囲の外の位置だけで尋ねられると、表が出力を決める規則では空(範囲の中に入力が無い)と答えたが、define が決める規則(`member_tier_shipping.rule` の `lifted`)では決められないと答えた。
- 規則が呼び手に言うところも、列挙の範囲を言わなかった。`rulec schema` は `campaign` の `enum` に四つの値を挙げ(生成した MCP のツールの `inputSchema` も同じ)、`rulec api` の引数には範囲が無かった(数の引数には `range` がある)。`rulec graph` の入力のノード、`.proto`、SQL の見出し、Go の `Input` のフィールドの注釈も数の範囲だけを書き、ページの「試してみる」は platinum も選ばせた。schema で形を確かめる呼び手は、platinum を通す形を見る。
- 範囲で確かめるところも、列挙の範囲を見なかった。E122 は、範囲を書いた列挙の入力を列挙の全部の値と比べたので、platinum を通す契約に黙った。`fixtures lint` は platinum の記録を形の合うものとして通し、`replay` は、入口が受け付けない入力への参照の答えをその記録と比べ、記録つきの `diff` は、新しい版の範囲の外の値を受け付けない入力として数えずに比べた。記録の無い `diff` は、範囲を足した版を、受け付ける入力の変わらない版として比べ、「どの入力でも同じ答え」と言って 0 で終わった。
**決定**:数の範囲が通るところには、列挙の範囲も通す。
1. **口が範囲を運ぶ。** 列挙の列の型を `ColumnType::Enum { name, range }` にした。`range` は、入力の範囲に入る値の、規則での名前の並び(列挙の順)で、`Checked::enum_ranges` と証明書の `enum_ranges` と同じものである。範囲の無い入力、出力、並びの要素のフィールド(列挙に範囲を書けない。E069)では None にする。両端でなく値の並びにした理由は、ritsu の DESIGN 3.2 に書いた。
2. **口の `eval` は、生成したコードの入口が受け付けない入力を受け付けない。** 範囲に入らない列挙の値は、数の範囲の外と同じ `RuleError::Input`(「入力 `campaign` が範囲の外です」)にする。日付は入口と同じ読み方(`door::civil`)で読み、暦に無い日は、型の合わない値と同じ文で受け付けない。並びの要素のフィールドも、入力と同じく範囲で確かめる(「要素のフィールド `amount` が範囲の外です」)。範囲の確かめ方は一つの関数(`outside_range`)にまとめ、入力と要素のフィールドが同じものを使う。
3. **`outputs_over`** は、範囲を書いた列挙の入力を、尋ねられた位置のうち範囲に入る値だけに限る。数の入力が宣言した範囲と交わる(`meet`)のと同じである。範囲の外の位置だけなら、範囲の中に入力が無いので、答えは空になる。
4. **口を読む言語**:dandori の E014 と sekisho の E206 が、数の範囲と同じ考えで列挙の範囲を確かめる(dandori の DESIGN 1.3、sekisho の DESIGN 3.2)。ritsu-cross の検査は、規則の入力の範囲を自分では見ないので、直すところが無かった(ritsu の DESIGN 3.2)。
5. **規則が呼び手に言うところでは、数の範囲を言うところで列挙の範囲も言う。**
- `rulec schema` は、範囲を書いた列挙の入力の `enum` を、範囲に入る値だけにする(`--keys alias` でも同じ)。生成した MCP のツールの `inputSchema` は schema の `in` なので、同じになる。出力と、範囲の無い入力は、列挙の全部の値のままである。
- `rulec api` は、各言語の `params`(Go は `input_fields`、NumPy と SQL は `columns`)にあるその入力に、数の範囲と同じ鍵の `range` を足し、`min` と `max` に両端の値の名前を書く(`"range":{"min":"basic","max":"gold"}`)。名前はワイヤが運ぶ規則での名前で、各言語での綴りは、同じ言語の `enums` が値ごとに言う。間に入る値は、`enums` の順で決まる。`rulec graph` の入力のノードにも、同じ形の `range` を付ける。
- 生成物の注釈は、数の範囲と同じところに両端を書く。`.proto` は proto の値の名前で(`// campaign. TIER_BASIC to TIER_GOLD`)、Go の `Input` のフィールドは入口が比べる定数で(`// campaign range TierBasic..TierGold`)書く。SQL の見出しは範囲に入る値だけを挙げ(`one of basic, silver, gold`)、ページの「試してみる」は範囲に入る値だけを選ばせる。
6. **範囲で確かめるところでは、列挙の範囲で確かめる。**
- E122 は、範囲を書いた列挙の入力を、範囲に入る値と比べる。契約が通すのに範囲の外の値があれば「契約は `order.campaign` に "platinum" を通しますが、規則はそれを受け付けません」と言い、`fix.text` は範囲に入る値だけを挙げた契約の注釈(`"enum": ["basic", "silver", "gold"]`、`[(buf.validate.field).string = {in: [...]}]`)にする。規則の側の直しは、範囲の外の値なら「規則の範囲を広げて、その値の答えを決めてください」で、列挙に無い値なら、これまでどおり列挙に値を足す。契約の通す値が一つも範囲に入らなければ、狭めた注釈は書けないので、直しは出さずにそう言う。値の名前と別名が同じ綴りの列挙でも、それを別名の綴りの違いとは言わない。
- `fixtures lint` と `replay` は、範囲の外の列挙の値を持つ記録を、数の範囲の外と同じ `bad_input`(「`in.campaign`: `platinum` は宣言した範囲 basic..gold の外です」)にして外す。記録つきの `diff` は、新しい版の範囲の外の値を、数と同じ `input_range`(「campaign が新しい版の範囲 basic..gold の外です」)として数える。
- 記録の無い `diff` は、受け付ける入力の変わりめ(`domain`)に、列挙の範囲の変わりめを数と同じ `input_range` として挙げる。`old` と `new` は両端の値で、範囲の無い側は列挙の全部である(`basic..platinum` から `basic..gold`)。数の範囲と同じく、両方の版が受け付ける入力だけを比べ、終了コードは 1 になる。
**捨てたもの**:
- **範囲を両端(`Option<String>` の組)で口に運ぶこと。** 読む側が `RuleFacts::enums` の順を引いて、位置で比べることになる。
- **口の `eval` を、入口の関数(`door::refusal`)にそのまま回すこと。** 入力を JSON に組み直して入口と同じ関数で確かめれば、受け付けない理由は作りの上でそろう。ただ、口の文(「入力 `x` が範囲の外です」)が入口の文(`x is out of range`)に変わり、dandori の `ritsu run` とシナリオが書く失敗の文と、口の評価器の試験の文が変わる。足すのは三つの確かめで足り、入口の試験の全部を口の評価器にかける試験(下)が、この先のずれも見つける。
- **schema の `enum` を四つのままにして、範囲を `$comment` と `api` の前提(`preconditions`)に書くこと。** 範囲に入る値の集まりは JSON Schema の形で書ける。形で書けるものを注記に回すと、schema で確かめる呼び手は platinum を通したままになる。前提の一覧は、形では書けないもの(入力どうしの関係、並びの合計と件数)のためにある(§15.116)。
- **`api` の `range` に、範囲に入る値の並びを書くこと。** 数の `range` と鍵が変わり、`range` を読む呼び手が数と列挙で読み方を分けることになる。値の並びは `enums` の順と両端から決まる。
**変わること**:
- 口の事実で、範囲を書いた列挙の入力の型に、範囲に入る値の並びが付く。口の型が変わるので、口を読むクレート(dandori、sekisho)のコードが変わる。
- 口の `eval` は、範囲に入らない列挙の値、暦に無い日、範囲の外の要素のフィールドを `RuleError::Input` で受け付けない。前は答えていた。dandori の `ritsu run` とシナリオでは、規則の入口が受け付けない呼び出しとして、生成したコードと同じく失敗になる。
- `outputs_over` は、範囲の外の位置だけの問いに空を答える。前に決められないと答えていた形も、正確な答えになる。
- dandori と sekisho で、範囲を書いた列挙の入力に、範囲の外になりうる値を渡すフローとゲートが、E014 と E206 で落ちる。
- 範囲を書いた列挙の入力を持つ規則では、`rulec schema` の `enum` が狭まり、`rulec api` のその入力に `range` が付く。schema で確かめる呼び手は、範囲の外の値をそこで断られる。前も生成したコードの入口で断られていたので、答えていた呼び出しが答えなくなることは無い。`range` の `min` と `max` が文字列になるのは、この入力だけである。
- 同じ規則の `rulec graph` の入力のノードに `range` が付く。記録の無い `diff` は、範囲を足すか変えた版との比べで、`input_range` を挙げて 1 で終わる。前は「答えを決めるものは何も変わっていません」と言って 0 で終わった。
- 同じ規則の生成物では、MCP のツールの `inputSchema`、`.proto`・SQL・Go の注釈、ページの選択肢が変わる。生成したコードの答えは変わらないが、字面が変わるので、`rulec gen --check` は生成し直すまで落ちる。
- `from` で契約から読む、範囲を書いた列挙の入力は、契約が範囲の外の値を通せば E122 で落ちる。前は通った。
- `fixtures lint`・`replay`・記録つきの `diff` は、範囲の外の列挙の値を持つ記録を、上のとおり外すか、受け付けない入力として数える。前は比べていた。
- ほかの規則では、`rulec` のコマンドの答えも生成物も変わらない(下の突き合わせ)。
**確かめたこと**:
- `tests/ports.rs` に二件を足した。
- `the_range_of_an_enum_input_crosses_the_port`:コーパスの `member_tier_shipping.rule` と日本語の版 `会員段階の送料.rule` で、`campaign` の型が範囲に入る三つの値を持ち、`member` と出力は持たない。口の `eval` は gold のキャンペーンに答え、platinum を英日の文で受け付けない(前は答えた)。`outputs_over` は、platinum の位置だけなら空を、0〜3 の位置なら 0〜2 と同じ値を答える(前は決められないと答えた)。
- `the_port_refuses_every_input_the_door_refuses`:コーパスの全部の規則で、`rulec gen` が書く入口の試験のうち口が運べる 1,724 件を、口の `eval` が一つ残らず受け付けない(前は 66 件に答えた)。
- `口の事実はapiと証明書とschemaが言うことと同じ` は、列挙の列の範囲を証明書の `enum_ranges` と突き合わせ、schema の `enum` をその範囲(無ければ列挙の全部の値)と突き合わせる。
- `tests/api.rs` の `the_range_of_an_enum_input_is_said_wherever_the_inputs_are`:同じ二つの規則で、schema の `enum`(名前と別名の二通りの鍵)、`api` の 11 か所と `rulec graph` の `range`、`api` だけから組んだ Python の入口の文、Python・TypeScript・JavaScript の MCP のツール、`.proto`・SQL・Go の注釈、ページの選択肢が範囲を言い、範囲の無い入力と出力は列挙の全部の値を持つ。
- `tests/contract.rs` の `an_enum_input_with_a_range_is_held_to_the_values_inside_it`:英語の規則と JSON Schema、日本語の版と `.proto` で、platinum を通す契約は E122(直しは範囲に入る値の注釈)で、範囲の値だけを通す契約は何も言わない。platinum だけを通す契約は、直しを出さずに範囲に入る値が無いと言う。範囲の無い入力は、列挙の全部の値を通す契約で何も言わない。
- `tests/diff_records.rs` の `an_enum_value_outside_the_new_range_is_turned_away_as_a_number_is`:範囲の無い旧版と、コーパスの規則(新しい版)で、platinum の記録を、`diff` は `input_range` として数え、新しい版での `fixtures lint` は宣言した範囲の外と言い、`replay` は外す。旧版での `lint` は全部を通す。英日。
- `tests/vdiff.rs` の `a_range_put_on_an_enum_input_is_a_change_of_what_the_version_takes`:範囲の無い版とコーパスの規則を両方の向きで比べ、`domain` は `input_range` 一つ(`basic..platinum` と `basic..gold`、日本語の版は `一般..プラチナ` と `一般..ゴールド`)、答えの差は無く、終了コードは 1。
- `tests/twins.rs` の `a_twin_makes_the_same_promises_as_its_original` は、英語の規則と日本語の版の `api` を名前を除いて比べるので、列挙の範囲は、両端が列挙の順の何番目かで比べるようにした。
- dandori(`tests/fixtures/enum_ranges.flow` とその日本語の版、範囲の中だけを渡す `enum_ranges_held.flow` とその日本語の版)、sekisho(`tests/walk/ranges/` の二つのゲートと、E206 の変異の英日)、ritsu(`tests/cross.rs` の、フローとゲートと規則のプロジェクト)の試験。
- 替える前と後の突き合わせ。木の 565 本の規則の `check`(英日と、JSON の英日、終了コード)、195 本の `.flow` の dandori の `check`(同じ四つ)と 7 の生成先の `build`、166 本の `.gate` の sekisho の `check`(同じ四つ)と 4 の生成先の `gen`、10 のプロジェクト(ritsu の試験のプロジェクトの 8 と、ブラウザで試すページの通販の英日)の `ritsu check`(英日と JSON)は、5,763 の出力のどれも一字も変わらない。木の全体の `ritsu check .` で変わったのは、足した試験の材料(dandori の `enum_ranges.flow` とその日本語の版が E014 に、sekisho の二つの変異が E301 と E303 から E206 に)と、要約の行だけである。同じ 565 本の `schema`(名前と別名の二通りの鍵)、`api`、`graph`、自分との `diff`、`doc --format html`、`gen`(書いたファイルの全部を、名前と大きさと SHA-256 で)の 3,955 の出力で変わったのは、範囲を書いた列挙の入力を持ち検査を通る 4 本(コーパスの 2 本と `m_w121rule` の英日)の 24 だけで、どれも上の決定の 5 のとおりの変わり方である(`gen` は規則ごとに 10 のファイル。MCP のツールとページが三つの言語の分、SQL の二つ、`.proto`、Go)。木で範囲を書いた列挙の入力を持つ規則は 8 本(コーパスの 2 本と、変異の 6 本。検査を通るのは 4 本)で、それを使う `.flow` と `.gate`、契約から読む入力、`apply` で読む規則、記録は無かった。
**見つけて直していないもの**:
- **`apply` の E043 は、元の規則の、範囲を書いた列挙の入力に渡す値を確かめない。** 元の規則の入力が数か日付なら、渡す値の取りうる範囲がその範囲の中にあるかを E043 が確かめる(範囲の外の値には、元の規則の完全性が証明されていない)。列挙の範囲は両端が数でないので読まずに飛ばし(`apply::bounds_of`)、`campaign : tier range >=basic <=gold` に、platinum にもなるこの規則の入力を渡しても通る。元の規則の表に範囲の外の値に当たる行が無ければ、この規則の検査が E101(`c = platinum` に当たる行が無い)で止めるが、`>=silver` のように範囲の外まで覆う行があれば、元の規則が定めていない値にその行で答える。直すには、渡す値がとりうる列挙の値(入力ならその範囲か列挙の全部、リテラルならその値、表の出力なら行が書く値、式なら列挙の全部)を `with` の対応で元の規則の値に読み替え、範囲に入らない値があれば E043 にする。表の出力と式の値の集まりをどう取るかに決めることが残るので、この節では直していない。木の `apply` に、元の規則が範囲を書いた列挙の入力を持つものは無い。§15.252 で、E043 がこの値を確かめるようにした。
- **二つの順序のある列挙の入力を比べる define の答えを、`outputs_over` が決められない。** `define lifted : bool = campaign > member` と `define effective : tier = max(member, campaign)` は、二つの入力をどちらも一つの値に限っても「決められない」になる。範囲を書かない規則でも同じである。sekisho がこの出力を読むと、期待が W303 になる(sekisho の試験の規則を、真偽を表で決める形にしたのはこのため)。
### 15.252 準用の E043 を、元の規則の列挙の入力の範囲にも効かせる(2026-10-19)
**きっかけ**:順のある列挙の入力の範囲(§15.210)を、数の範囲が通るところに通したとき、準用の E043 だけが列挙の範囲を見ないまま残った(§15.251 の見つけて直していないもの)。元の規則が `campaign : tier range >=basic <=gold` を持つとき、範囲を書かない `campaign_tier : tier` をそこへ渡す規則は、次のどちらかになった。
- 元の規則の表に、`>=silver` のように範囲の外まで覆う行があれば、検査を通った。展開した規則の生成コードは、`campaign_tier = platinum` にその行で答える(Python で 5USD)。元の規則の生成コードは、同じ platinum を入口で受け付けない(`campaign is out of range`)。同じ規則を準用したのに、答えが割れる。
- 範囲の中の値ごとに一行の表なら、検査は E101(`campaign_tier = platinum` に当たる行が無い)で止まった。直し方は「この入力に当たる行を足す」で、行を足す先は元の規則の表になる。この規則からは足せず、元の規則に行を足すのは別の承認の話である。
**原因**:E043 の確かめ(`apply::check`)は、元の規則の入力の `range` の両端を数か日付として読み(`apply::bounds_of`)、読めなければ確かめを飛ばした。列挙の範囲の両端は値の名前なので、いつも飛ばされた。元の規則を展開する時点(`apply::expand`)では、範囲に入る値(元の規則の `Checked::enum_ranges`)が手元にあったが、この規則に持ち込む元の規則の入力の記録(`CalleeInput`)は、型と、書いたとおりの `range` しか持たなかった。
**決定**:
1. **元の規則の入力の記録に、範囲に入る値を持たせる。** `CalleeInput.enum_range` は、元の規則の `Checked::enum_ranges` のその入力の値の並び(元の規則の順と綴り)。範囲を書かない入力と、並びの要素のフィールド(列挙に範囲を書けない。E069)では None。
2. **E043 は、渡す値が取りうる列挙の値を `with` で元の規則の値に読み替え、範囲に入らない値があれば出る。** 読み替えは展開と同じである。`with` に書いた値はその先の値になり、書いていない値は同じ綴りの値になる。ただし、`with` の先に書いた値は、同じ綴りの値からは読まない。
3. **取りうる値は、数の範囲を名前ごとに求めるとき(`types::check`)と同じ考えで取る。**
- 入力は、その範囲に入る値。範囲が無ければ列挙の全部。
- リテラルは、その値。
- 表の出力は、その出力を決める表の、全部の行が書く値。行が値を書けばその値、名前を書けば、その名前が取りうる値を、その行のその名前の列のセルで絞ったもの。`Checked::row_writes` と同じ読み方で、名前の値を「その型のどの値も」とする代わりに、その名前が取りうる値を使う。`>=silver` の行が、範囲を書いた入力をそのまま書く表は、その入力の範囲の値だけを書く。
- 名前を一つ書いただけの定義(`define repeat_tier : tier = campaign_tier`)は、その名前が取りうる値。ほかの準用の出力は、この形の定義になる。
- 並びの要素のフィールドは、列挙の全部(範囲を書けない)。
- そのほかの式(`max`・`min` を含む `define`、導出、fold の答え)は、列挙の全部。check は式がとる列挙の値を求めないので、数の E043 が範囲の分からない値を落とすのと同じく、安全側に取る。
4. **文は、数の E043 と同じ形で、列挙の値で言う。** 取りうる値に範囲の外の値があれば、数と同じ見出し(「`campaign` に渡す値が、元の規則の範囲の外に出ます」)にする。印には渡す値の取りうる値を書き(「渡す値の取りうる値: basic, silver, gold, platinum」。一つなら「渡す値: platinum」)、注の例には、範囲の外の値のうち元の規則の順で最初のものを挙げる(「例: campaign = platinum は、`campaign_credit.rule` の `campaign` の range >=basic <=gold の外です。」)。直し方の注は、数の「渡す側の範囲を狭めるか」を、列挙では「渡す値を範囲の中の値に限るか」にした。範囲の外の値が、式の値を列挙の全部とした安全側の取り方からだけ来るときは、数の範囲が分からないときと同じ形で「`campaign` に渡す値の取りうる値が分かりません」と言う。注では、どの定義が式で決まるか、どの値にもなりうるとして読んだか(「`lift` は式で決まる値で、…platinum にもなりうるとして読みます」)と、直し方(範囲の中の値だけを書く表で値を決めて渡す、同じ範囲を書いた入力を渡す)を言う。`fix.text` は、数と同じく付けない。
5. 台帳の E043 の「いつ出るか」と直し方に列挙の範囲のことを書き、列挙の範囲の再現を別の形として足した。元の規則を隣に置く別の形は、E040 の寄与の形や E044 の輪の形と同じく、英語の名前だけで書く(`Entry::also_with`)。
**捨てたもの**:
- **式の値を、その中の名前が取りうる値から求める**(`max(a, b)` を、`a` と `b` の取りうる値を合わせたものとして読む)。`max` と `min` の答えはいつも引数のどちらかなので、安全側には正しい。ただ、E043 のためだけに、列挙の値を求める式の読み方を持つことになる。式の値を列挙の全部とするのは、数の E043 が範囲の分からない値を落とすのと同じ取り方で、直し方もある(範囲の中の値だけを書く表で決める、範囲を書いた入力を渡す)。§15.256 で、`max` と `min` を引数の取りうる値から読むようにした。
- **式で決まる値も「範囲の外に出ます」と言い、範囲の外の値を例に挙げる。** 例を列挙の全部から取ると、範囲の中の二つの段階の `max` に「platinum になる」と言うことになる。本当は platinum にならない。取りうる値が分からない、と言う形にした。
- **`Checked::row_writes` そのものを、範囲を書いた入力の名前を書く行で、その範囲に絞る。** E101・E102・W111 の読み方が変わり、ほかの規則の診断が動く。E043 の取り方の中でだけ絞った。
**変わること**:
- 範囲を書いた列挙の入力を持つ規則を準用し、その入力に範囲の外になりうる値を渡す規則は、E043 で落ちる。前は、元の規則の行が範囲の外まで覆えば検査を通って、展開した規則の生成コードが、元の規則の受け付けない値に答えた。覆わなければ E101 で止まり、元の規則の表に行を足せと言った。
- 前は通っていた規則が、新しく E043 で落ちうる。範囲を書いた列挙の入力に、範囲の無い入力、範囲の外のリテラル、表の出力、式の値を渡している準用である。表の出力は、行が範囲の外の値か、範囲の外になりうる名前を書けば落ちる。式(`max`・`min`)の値は、範囲の中の値にしかならなくても、取りうる値が分からないとして落ちる。
- 範囲の中の値だけを渡す準用は、これまでどおり通る。同じ範囲を書いた入力、範囲の中のリテラル、範囲の中の値だけを書く表の出力、どの値も範囲の中の値に読み替える `with`、それらを指すだけの定義(ほかの準用の出力を含む)である。
- 範囲を書いた列挙の入力を持たない規則の準用では、何も変わらない。
**確かめたこと**:
- `tests/apply_enum.rs` と、材料の `tests/apply_enum/`(英語を先に、日本語の版を横に)。元の規則は二つで、`campaign_credit.rule`(`>=silver` の行が platinum にも当たる)と `campaign_points.rule`(範囲の中の値ごとに一行)。
- 変異の六つ。範囲を書かない入力を渡す(`E043_a_tier_with_no_range`)、同じ入力を一行ずつの元の規則に渡す(`E043_no_row_past_the_range`)、リテラルの platinum(`E043_a_literal_past_the_range`)、platinum を書く行のある表の出力(`E043_a_table_that_writes_platinum`)、範囲の中の二つの段階の `max`(`E043_the_later_of_two_tiers`。取りうる値が分からない形)、`with` で elite を platinum に読み替える入力(`E043_with_reads_a_level_as_platinum`)。どれもエラーは E043 一つで、英語の報告が範囲の外の値を例に挙げるか、取りうる値が分からないと言い、英日の文は `tests/golden/apply_enum/` の golden と同じである。日本語の版は同じ診断を同じ行に出す。どれも `prepare` が受け付けないので、生成にも ritsu の口にも渡らない。替える前のバイナリでは、二つ目は E101 で止まり、ほかの五つは検査を通った。一つ目は、展開した規則の生成コード(Python)が platinum に 5USD を答え、元の規則の生成コードは platinum を受け付けなかった。
- 通る規則 `order_credits.rule` とその日本語の版。同じ範囲を書いた入力、範囲の中のリテラル、範囲の中の値だけを書く表の出力、どの値も範囲の中の値に読み替える `with`、範囲を書いた入力を指すだけの定義を、それぞれ準用に渡し、診断は一つも出ず、例も成り立つ。証明書は、規則のファイルを添えて二つの再検査を通る。表の出力は、範囲の値を全部書き、範囲の値を一行ずつ持つ `campaign_points.rule` に渡す(見つけて直していないものの二つ目を踏まないため)。
- ほかの準用の出力を渡す形。その出力を決める元の規則の表が gold までしか書かなければ通り、platinum を書けば platinum を例に E043 になる。
- 台帳:`tests/codes.rs` の、別の形の再現の試験に E043 を足した。再現は E043 を出し、見出しは英日とも「範囲の外に出ます」の文である。`docs/codes.md` と `docs/codes.ja.md` を `rulec explain --all --format markdown` で書き直した。
- 替える前と後の突き合わせ:木の 581 本と足した 18 本の規則の、`rulec check`(英日と、JSON の英日)と `rulec certificate` の 2,995 の出力(標準出力、標準エラー、終了コード)を、替える前のバイナリと比べた。変わったのは 60 で、どれも足した変異の 12 本の五つずつである。木の 581 本の出力と、足した元の規則と通る規則の 6 本の出力は、一字も変わらない。
**見つけて直していないもの**:
- **値の無いことのある入力(`tier?`)を、値の無いことの無い元の規則の入力に渡すと、`none` が E101 になる。** 元の規則の表に `none` の行が無いからで、直し方の注は「この入力に当たる行を足す」と、元の規則の表に足すことを言う。数でも同じである(`number?` を `number` の入力に渡す)。範囲の外の値の話ではないので、この節では直していない。§15.255 で、E043 が言うようにした。
- **上の表から来る列挙の列で、上の表が書かない値まで覆う行の箱を、証明書が狭めて書き、二つの再検査がそれを受け付けない。** 区画の検査は、上の表が書かない値の座標を、下の表の行の箱から除く(`region.rs` の `reachable`)。証明書はその箱を行の `accepts` に書くが、二つの再検査(`tools/recheck.py` と Lean の `rulec-recheck`)は、行のセルから組み直した箱と同じであることを求める。上の表が列挙の一部の値しか書かず、下の表の行のセルが書かれない値も覆うとき(`-`、`>=silver`、値の並び)、検査は通るのに、証明書は二つの再検査で落ちる(「the box it states is not the one its cell describes」)。準用の無い規則でも起きる(上の表が silver と gold だけを書き、下の表は `>=silver` の一行)。準用した表では、上の表が書かない値だけに当たる行も E102 にならずに残るので、起きやすい。替える前のバイナリでも同じで、この節の前から木にある規則の証明書には、上の表が書かない値の事実(`never`)を持つ表が一つも無かった。直すなら、証明書が狭めない箱を書くか、再検査が `never` の事実で狭めた箱を受け付けるかを決めることになる。この節では直していない。§15.253 で、二つの再検査がこの箱を受け付けるようにした。
### 15.253 上の表が書かない値を除いた箱を、二つの再検査が受け付ける(2026-10-21)
**きっかけ**:上の表が列挙の一部の値しか書かず、下の表の行のセル(`-`、`>=silver`、`not: basic`、`<=gold`、値の並び)がほかの値も覆うと、`rulec check` は通るのに、その証明書を二つの再検査(`tools/recheck.py` と Lean の `rulec-recheck`)が落とした。Python は「row 1's box on lift is not what its cell `>= silver` describes」、Lean は「the box it states is not the one its cell describes」と言う(§15.252 の見つけて直していないもの)。準用の無い規則でも起き、0.32.0 でも同じである。上の表から来る列が表の一つ目の軸になる形では(軸は座標の少ない順に並ぶ。§6.3)、Lean の再検査は覆いも「the cover does not tile the space」で落とした。
**原因**:
- 区画の検査は、上の表から来る列挙と真偽の列で、上の表が書かない値の座標を、どの行の箱からも除く(`TableRegion::build` の `reachable`。§15.115)。どの入力もその列をその値にしないので、その座標にはどの点も来ない。覆い、行どうしの交わり、届く点、上の表の行を読む理由は、どれもこの除いた箱で組み立てる。証明書の `accepts` もこの箱である。二つの再検査は、行の箱をセル(`tests`)から組み直し、`accepts` と一字も違わないことを求めていた。
- 証明書が上の表の事実 `above.never` を書くのは、覆いの葉が上の表に頼るところ(`Cover::ByUpstream` の `Apart::Never`)だけで、その葉は、全部の軸を決めた点でしか出なかった。上の表から来る列が一つ目の軸だと、上の表が書かない値の下は「点ごとに外れる」葉(`every_point_ruled_out`)になり、事実は一つも書かれなかった。Lean の再検査は点ごとの葉で上の表の事実を読むが(`pointRuledOut`)、読む事実が無かった。Python の再検査は、点ごとの葉で上の表の事実を読まなかった。
**決定**:
1. **再検査は、セルから組み直した箱から、その表の `never` の事実が指す座標を除いたものを、`accepts` と比べる。** 除いてよいのは事実の座標だけで、事実はこれまでどおり、その列を決める表の行を読んで確かめる(どの行も値の語を書き、どれもその値ではない)。上の表が書く値まで除いた箱も、事実の座標を残した箱も、組み直した箱と合わないので落ちる。交わり、届く点、覆い、上の表の行を読む理由は、これまでどおり `accepts` で確かめる。
2. **Lean は、除いた箱について言った主張を、セルの箱の表に移す。** `dropNever` は、箱の軸ごとに事実の座標を除く。`Certified.statesCells` は、証明書の行がセルの行から事実の座標を除いたものであることで、`RulecMain.checkBoxes` が軸ごとに確かめる(`dropNeverAt`)。規則が尋ねられる点は事実の座標に無いので(`Sieve.asked` の定義に入っている)、その点では二つの箱が同じ点をとる(`inBox_dropNever`)。そこから、完全性、一意、尋ねられる点での交わりの無さ、届く点(値を添えない弱い読み方も)が、セルの箱の表についても成り立つ(`Certified.cells_complete`、`cells_unique`、`cells_disjointAsked`、`cells_reached`、`cells_notExcluded`)。
3. **証明書は、上の表が書かない値の座標を全部、`never` の事実として書く。** 覆いの葉が頼る座標だけでなく、箱から除いた座標の全部である。書けるのは、その列を決める表のどの行も値の語を書くときだけである(§15.115 と同じ。行が名前を書くと、その行が何を書くかを、再検査は証明書から読めない)。覆いの葉から集めた事実を先に、残りをそのあとに書くので、前から事実を書いていた証明書の並びは変わらない。
4. **覆いは、上の表が書かない値に列を決めた箱を、ほかの軸を決める前に、その事実一つで外れる `upstream` の葉にする。** 全部の軸を決めた点では、これまでどおり同じ事実が先に見つかる。
5. **Python の再検査は、点ごとの葉で、上の表の事実(`never` と `apart`)も読む。** Lean の `pointRuledOut` と同じにした。ほかの理由と組み合わせて点ごとに外れる箱が、上の表が書かない値の点を含むときのためである。
**捨てたもの**:
- **証明書が狭めない箱(セルの箱)を `accepts` に書き、交わりと覆いを `never` の事実で言い直すこと。** 覆いと届く点はそのままで通る。ただ、除いた箱でしか分かれない行の対(`>=gold` と `silver, platinum` は、platinum が来なければ交わらない)を言う新しい形が要り、二つの再検査と Lean の証明にもそれを足すことになる。それだけでは済まない。下の表が上の表の行の箱を読む理由(覆いの葉の `above_rows`、`above.apart` の範囲、`above_apart`)は、rulec が上の表の除いた箱で組み立てている。上の表の行の `accepts` をセルの箱にすると、再検査が組み直す範囲が広がり、rulec が書いた範囲を超えて落ちうる(`apart` は、書いた範囲が組み直した範囲を含むことを求める)。上の表そのものがさらに上の表から読む、三段の表で起きる。それらを全部、中とは別の箱で組み直すことになる。再検査の側で受け付ければ、rulec の中の箱と証明書の箱は同じままで、証明書の形も変わらない。
- **セルの箱と、そこから事実の座標を除いた箱のあいだにある箱を、どれでも受け付けること。** それでも主張は移るが、`accepts` の意味が一つに決まらない。rulec が書くのは除いた箱なので、その一つに決めた。
- **上の表が書かない値の下を、点ごとの葉のままにすること。** 事実を全部書けば二つの再検査とも通るが、事実一つで外れる箱を、点の数だけの問いにする。
**変わること**:
- 上の表が書かない値をセルが覆う行のある表で、証明書が二つの再検査を通る。前は、`check` を通るのに、二つとも落とした。
- 同じ表で、上の表が書かない値の `never` の事実が全部書かれる。上の表から来る列が一つ目の軸なら、覆いのその値の下が一つの `upstream` の葉になる。前は点ごとの葉で、事実は無かった。証明書のキーと読み方は変わらず、`accepts` の説明(`docs/formats.md`)が「セルの箱から、その軸の `never` の事実の座標を除いたもの」になった。
- 二つの再検査の出力に、事実の座標を除いて読んだ箱の数が出る(Python は「1 boxes read back from their cells (1 without the values the tables above never write)」、Lean は「1 boxes read back from the cells they were written as, 1 of them less the values no table above writes」)。そうした箱の無い証明書では、出力は変わらない。
- ほかの規則の証明書と、二つの再検査がそれについて言うことは変わらない(下の突き合わせ)。
**確かめたこと**:
- `tests/above_never.rs` と、材料の `tests/above_never/`(英語を先に、日本語の版を横に、7 組)。どれも、上の表が会員の段階を silver か gold に引き上げ、下の表がそれを読む。再現の形(`plain_lift.rule`)、セルの形ごと(`-`、`>=silver`、`not: basic`、`<=gold`、値の並び)と、platinum でだけ交わる二つの行(`lift_cells.rule`)、引き上げた段階が二つの軸の一つ目になる形(`lift_and_area.rule`)、上の表が真しか書かない真偽の列と `first` の表(`flag_first.rule`)、basic と platinum に当たる行を持つ規則の準用(`lifted_credit.rule` と元の規則 `tier_credit.rule`)、値の無いことのある段階(`optional_lift.rule`)。
- どれも `check` を通り、証明書は規則のファイルを添えて二つの再検査を通る。上の表から読む 12 本は、どれも事実の座標を除いて読んだ箱を持つ。替える前のバイナリと二つの再検査では、その 12 本の証明書を、どちらの再検査も落とした。替える前の証明書でも、一軸の表だけの 10 本は、直した二つの再検査を通る(前から事実を書いていた)。二軸の形の 2 本は、事実が無いので通らない。
- 二軸の形では、覆いの basic と platinum の下がそれぞれ一つの `upstream` の葉で、証明書は二つの事実を書く。
- 日本語の版の証明書は、どの表でも、事実、行の箱、覆いの形が英語の版と同じである。
- 偽った証明書:`>=silver` の箱から gold(上の表が書く値)を除いたもの、platinum の事実を消したもの、platinum を箱に残したもの、silver を上の表が書かない値と言って箱から除いたものを、二つの再検査がどれも落とす。前の三つは箱の確かめで落ちる。
- Lean:`lake build` が通る。`tests/lean.rs` の `#print axioms` に、新しい定理(`inBox_dropNever` と `Certified.cells_*` の五つ)を足した。
- 替える前と後の突き合わせ:木の 601 本の規則の `rulec check`(英日と JSON の英日)と `rulec certificate` の 3,005 の出力(標準出力、標準エラー、終了コード)は、一字も変わらない。木の 364 本の証明書を二つの再検査にかけた 728 の出力も、一字も変わらない。木で上の表が書かない値の座標を持つ表は、準用の材料の 2 本(`tests/apply_enum/order_credits.rule` と日本語の版)だけで、どの行のセルもその値を覆わず、事実は前から書いていた。
**見つけて直していないもの**:
- **上の表の行が値でなく名前を書くと(`| >=gold | member |`)、上の表が書かない値の事実を書けず、覆いの葉が理由の無い `upstream` になって、二つの再検査が落とす。** 下の表の箱が狭まらない形でも起きる(下の表が `>=silver` の一行で、basic の下が理由の無い葉になる)。替える前のバイナリでも同じである。証明書は、名前を書く行の `produces` を null にし、その行が書きうる値を言わないので、再検査はどの行もその値を書かないことを確かめられない。直すなら、名前を書く行が書きうる値(その行のその名前の列のセルが通す値。`Checked::row_writes`)を、再検査が行の箱から組み直せる形で証明書に載せ、二つの再検査の `never` の確かめと、上の表の行を数えるところ(Python の `writers`、Lean の `aboveRowsOf`)を、それで読むことになる。§15.261 で、行がどの列を渡すかを証明書に書き、`never` の確かめをそれで読むようにした。
### 15.254 並びの先へ続く `from` のパスを、E121 が並びとして言う(2026-10-21)
**きっかけ**:JSON Schema で関係が `type: array` になっているところを通る `from` のパス(`line.ofProduct.entityType`)に、E121 は「It resolved as far as `line.ofProduct`, which has no fields under it.」と言った。`ofProduct` の要素(`Product`)には `entityType` があるので、この文は事実と合わない。本当の理由は、そこが並びで、パスが一つの値を読めないことである。業務のモデルから書き出した契約は関係を配列で書くことが多く、そうした入力がどれもこの文で止まった。`.proto` の `repeated` のフィールドの先へ続くパスも、同じ文で止まった。
**原因**:パスの解決(`Contract::at`)は、JSON Schema では、並びの節に `properties` が無いので次の段が見つからないところで止まり、そこにある名前を空と言った(`Stuck.had`)。`.proto` では、`repeated` のフィールドの先に段が残ると、名前を挙げずに止めた。どちらも、止まったところが並びであることを記録していなかった。
**決定**:
1. **パスの解決は、並びで止まったことを記録する**(`Stuck.collection`)。JSON Schema では止まった節が `array` のとき、`.proto` では `repeated` のフィールドの先に段が残るときである。
2. **そのときの E121 は、見出しで並びの先へ続くことを言い**(「パス `line.ofProduct.entityType` は、並び `line.ofProduct` の先へ続いています」)、印に、契約での姿(`array`、`.proto` なら `repeated` のフィールド)を書く。注は次のとおり。
- パスの上の並びを、要素の契約(`Contract::element`)をたどって全部見つけ、二つ目からの並びを、要素の中を `[]` の後ろに示して挙げる(`line.inQuote[].forDeal`)。
- 並びが一つで、残りのパスが要素の中の値を読むとき、理由を言う(パスが読むのは一つの値で、要素はそれぞれ `entityType` を持ち、入力がどの要素のものを受け取るかを言っていない)。入力の値を読むパスなら、書き方も言う。残りが要素のフィールド一つなら、要素を調べる `any`・`all`・`count` と、要素を並びとしてたどる `elements … from`。数なら `sum` と `max` も。残りが二段以上なら `elements … from` だけを言い、`where` と `of` が指せるのは要素のフィールド一つなので量化では読めないと添える。
- 並びをたどる書き方(`count` などの並びのパス、`elements … from` の並びのパス)なら、たどる並びは一つで、そこまでのパスは別の並びを通れないと言う。
- 残りが要素に無ければ、要素の中でどこまで届いたかと、そこにあるフィールドを言う。
- 終わりに、要素がいつも一つなら、契約でその並び(通る並びの全部)を一つのオブジェクト(`repeated` でないフィールド)にするよう言う。要素がフィールドを持たない並び(文字列の配列)では、要素がフィールドを持たないとだけ言う。
3. **台帳の E121** の「いつ出るか」と直し方に並びのことを書き、並びの先へ続くパスの再現を別の形として足した(`Entry::also_with`、英語の名前だけ。§15.252 と同じ)。
**捨てたもの**:
- **並びを通るパスを読めるようにすること**(一つ目の要素を読む、など)。どの要素の値を渡すかは規則の中身で、ツールが決めてかかれない。結合と入れ子の量化を受け付けない §3.3 の境界のとおり、パスが読むのは一つの値のままにした。
- **E120(型が合わない)にすること。** パスの終わりの型の話ではなく、パスの途中で一つの値が無くなる話で、「パスが契約に無い」の E121 に近い。コードを変えると、`check --diff-base` と `fix` を読む呼び手が変わる。
- **書き方を、入力の型で選ぶこと**(列挙の入力に `any` を勧めない、など)。`any` は真偽を返すので列挙の入力の値そのものにはならないが、規則の書き方を変える糸口として並べるほうが役に立つ。数を読むときだけ、`sum` と `max` を足した。
**変わること**:
- 並びの先へ続く `from` のパスの E121 の見出しと注が変わる(英日)。コードは E121 のままで、止まる規則の顔ぶれは変わらない。`check --format json` では `title`、`notes`、`spans` の `label`(前は空)が変わる。
- ほかの E121(綴りの違うフィールド、無い `shape`、要素に無い `where` や `of` のフィールド)は変わらない。
**確かめたこと**:
- `tests/collection_path.rs` と、材料の `tests/collection_path/`(英語を先に、日本語の版を横に)。注文とその明細の契約が二つ(JSON Schema と `.proto`)、変異が 9 組、通る規則が 1 組。
- 変異は、明細のフィールド一つ、明細の数、明細の中の二段のパス、明細の中のもう一つの並び、明細に無いフィールド、`.proto` の `repeated` のフィールド、`count` がたどる並びのパス、`elements … from` の並びのパス、要素のフィールドのパスが要素の中の並びを通るもの。どれもエラーは E121 一つで、英語の報告は場合ごとの文を言い、英日の文は `tests/golden/collection_path/` の golden と同じである。日本語の版は同じ診断を同じ行に出す。
- 通る規則 `order_lines_read.rule` とその日本語の版は、注が挙げる書き方(`any`、`all`、`count … where`、`sum … of`、`max … of … empty`、要素のフィールドが二段のパスを読む `elements … from`)で明細を読み、診断は一つも出ない。
- 台帳:`tests/codes.rs` の、別の形の再現の試験に E121 を足した。再現は E121 を出し、見出しは英日とも並びの先へ続くことを言う。`docs/codes.md` と `docs/codes.ja.md` を `rulec explain --all --format markdown` で書き直した。
- 再現:机の上の確かめの規則(`dealer_discount.rule`)の三つの E121 が、どれも並びの文になった。一つ目(`line.ofProduct.entityType`)は量化と並びの書き方を、二つ目と三つ目(`line.inQuote.forDeal.withDealer.…`)は、要素の中の並び二つと、三つの並びを一つのオブジェクトにする直し方を言う。同じところの 182 本の規則のうち、`check` の出力が変わったのはこの一本だけである。
- 替える前と後の突き合わせ:木の 601 本の規則の `rulec check`(英日と JSON の英日)と `rulec certificate` の 3,005 の出力は、一字も変わらない(木に、並びの先へ続くパスを書いた規則は無い)。
### 15.255 値の無いことがある値を準用で渡すと、E043 が言う。`with none` で読み替える(2026-10-21)
**きっかけ**:値の無いことがある入力(`member : tier?`)を、準用(`apply`)で元の規則の値の無いことの無い入力(`tier : tier`)に渡すと、展開した元の規則の表に `none` の行が無いので、E101(`member = none` に当たる行が無い)で止まった(§15.252 の見つけて直していないもの)。直し方の注は「この入力に当てはまる行を足す」と、足す行の形(`| none | 0USD |`)まで示したが、行を足す先は元の規則の表で、この規則からは足せない。元の規則に行を足すのは別の承認の話である(§15.252、§7 の準用)。数でも同じだった(`number?` を `number` に)。並びをたどる元の規則に、要素のフィールドが `T?` の並びを渡しても同じだった。
**原因**:準用の型の確かめ(`apply::check`)は、両側の `?` を外して比べるので、`tier?` を `tier` に渡しても E042 にならない。範囲の確かめ(E043)も、値の無いことを見なかった。値の無いことは展開した表の点(`none` の座標)として残り、完全性の検査がそれに行を求めた。
**決定**:
1. **値の無いことがある値を、`?` の無い元の規則の入力に渡すのは E043 にする。** 元の規則の完全性はその入力に値があるところでだけ証明されていて、値の無いときの答えが無い。範囲の外の値と同じ考えである。値の無いことがあるかは、型だけでなく、どこから来るかで決める(`absent_from`)。入力と要素のフィールドは、`T?` と宣言していればある。表の出力は、行が `none` を書くか、値の無いことがある名前を書くときだけある。`T?` の出力でも、`none` を書かない表の出力は、これまでどおり通る(上の表が書かない値として、どの点も来ない)。名前を一つ書いただけの定義は、指す名前と同じ。式は値の無いことを持たない(§2.1)。並びをたどる元の規則の要素のフィールドも同じに確かめる。
2. **見出しは「`tier` に渡す値には値の無いことがありますが、元の規則の `tier` は値の無いことを受け付けません」。** 印は、値の無いことの出どころ(「`member` は tier? です」「表 lift_by_member の 行1 が none を書きます」「`repeat_tier` は `member`(tier?)を指します」)。注は、元の規則の完全性が値のあるところでだけ証明されていることと、この規則の側で書けることを言う。列挙なら、`with none -> <値>` で値の無いことを元の規則の値に読み替える、値がいつもあるなら名前を `?` の無い型にする(表の出力なら、`none` を書く行が値を書くようにする)、`none` にも答える表をこの規則に書いて値を決めて渡す、の三つ。数と要素のフィールドには `with` が無いので、`?` の無い型にするか、元の規則が値の無いことを受け付ける(`T?` と `none` の行)かで、後のほうは元の規則の変更だと言う。
3. **`with none -> <値>` を書けるようにする。** 準用の `with` が、この規則の値の無いことを元の規則の値として読む。読み替える先の値には、同じ綴りのこの規則の値もそのまま対応する(ほかの `with` の行と違い、先の値を同じ綴りの値から取り上げない)。展開では、元の規則のセルがその値を取るなら、この規則のセルは値の無いことも取る。セルの言葉は `none` を値と並べて書けないので(§3.2)、値の無いことと値の幾つかを取るセルは `not:` に残りの値を書いた形にする(`not:` は値の無いことにも当たる)。値の無いことだけなら `none`、全部なら `-`。元の規則の `not:` のセルは、値の無いことをその値に読み替えるなら、残りの値の並びにする。書けるのは、この規則の値に値の無いことがあり、元の規則の入力が値の無いことを受け付けないときだけで、ほかは E042(「`member` は tier で、値の無いことがありません」「元の規則の `tier` は値の無いことをそのまま受け付けるので、`with` で読み替えません」)。
4. **W111 は、`with none` で読む列の、準用した表の `not:` のセルが取る値を、行に現れた値として数える。** 元の規則の行は値を名前で書いていて、`not:` は展開が書いた形だからである。
5. 台帳の E043 と E042 の「いつ出るか」と直し方に書き、E043 の別の形として値の無いことの再現を足した(`also_with`、英語の名前だけ)。
**捨てたもの**:
- **E042(型が合わない)にすること。** `tier?` と `tier` は値の集まりの違いで、範囲の外の値と同じく、元の規則が証明していないところへ値を渡す話である。E042 は、値の種類(単位、列挙と数)の食い違いのためにある。
- **型だけで決めること**(`T?` の名前は、いつも値の無いことがあるとする)。`none` を書かない表の出力(`-> lift : tier?`)を渡す規則は通っていて、展開した表も完全である。それを落とすことになる。
- **展開で、値の無いことと値を取る行を二行に分けること**(値のセルの行と `none` の行)。セルは名前で値を書けたままになるが、行の番号がずれ、ラベルのある行は二度現れて E034 になり、`overrides` の行が片方しか指さなくなる。
- **セルの言葉に、`none` を値と並べて書く形(`none, basic`)を足すこと。** 値の並びは値の無いことに当たらない、という §15.201 の読み方を変えることになる。`not:` で書ける。
- **数にも `with none -> <値>` を書けるようにすること。** 数の `with` は無く、どの数として読むかは規則の中身である。数を決める表では、値の無いことのある名前をそのまま書けない(E103)。
**変わること**:
- 値の無いことがある値を `?` の無い元の規則の入力やフィールドに渡す規則は、E101 ではなく E043 で止まる。止まる規則の顔ぶれは変わらず、コードと文が変わる。`with none -> <値>` を書いた準用は、前は展開した表の E012 で止まったが、いまは通る。
- `with none` を書いた準用では、展開した表のセルが `not:` になる。生成したコードの答えは、`with` が読み替えた値の答えである。
- W111 は、`with none` で読む列の `not:` のセルが取る値を数える。ほかの W111 は変わらない。
- ほかの規則の出力は変わらない(下の突き合わせ)。
**確かめたこと**:
- `tests/apply_absent.rs` と、材料の `tests/apply_absent/`(英語を先に、日本語の版を横に)。元の規則が四つ(いつも値のある段階と数量、値の無いこともある段階、明細の段階をたどる並び)。
- E043 の変異が五つ(`T?` の入力、`number?` の入力、`none` を書く行のある表の出力、`T?` の入力を指す定義、`T?` の要素のフィールド)と、E042 の変異が二つ(いつも値のある名前の `with none`、値の無いことを受け付ける元の規則への `with none`)。どれもエラーはそのコード一つで、英日の文は `tests/golden/apply_absent/` の golden と同じである。替える前のバイナリでは、E043 の五つは E101 で、E042 の二つは E012 と W111 と E042 だった。どれも `prepare` が受け付けない。
- 通る規則 `absent_tiers_read.rule` とその日本語の版は、値の無いことを `with none -> basic` で読み替える準用、`none` にも答える表で値を決めて渡す準用、`none` を書かない `tier?` の表の出力を渡す準用、値の無いことを受け付ける元の規則への準用、値がいつもある数量の準用を持ち、診断は一つも出ず、例も成り立つ(値の無い会員は basic の値引き)。生成したコード(Python、NumPy、TypeScript、JavaScript、Rust、Ruby、PHP、Go、Swift、Java、SQL、Wasm の 23 の走らせ方)は、値の無い会員を含むベクタのどれでも参照の評価器と同じ答えを返す。
- 台帳:`tests/codes.rs` の、別の形の再現の試験に E043 の二つ目の形を足した。`docs/codes.md` と `docs/codes.ja.md` を書き直した。
**見つけて直していないもの**:
- **準用で持ち込んだ名前の ASCII の別名が、この規則の名前とぶつかると、生成したコードが同じ変数を二度宣言する。** 持ち込んだ名前の別名は `<準用の名前>_<元の規則の名前>` で、`apply read` の `credit` は `read_credit` になる。この規則に出力 `read_credit` があると(`credit -> read_credit`)、TypeScript・JavaScript・Go・Swift・Java のコードが組めない(「Identifier 'read_credit' has already been declared」)。`check` も `gen` も何も言わない。替える前のバイナリでも同じで、木の `tests/apply_enum/order_credits.rule`(`apply campaign` と出力 `campaign_credit`)も、生成したコードがこの五つの言語で組めない。直すなら、持ち込んだ名前の別名がこの規則の名前とぶつからないことを E034 と同じく確かめるか、ぶつからない別名を選ぶことになる。§15.260 で、ぶつからない識別子を選ぶようにした。
- **日本語の名前に別名を書いた元の規則を二度以上準用すると、持ち込んだ名前の ASCII の別名どうしがぶつかる。** 別名の頭は元の規則の別名(`段階の値引き(tier_credit)` なら `tier_credit`)で、準用の名前ではないので、三度準用すると、三つの出力がどれも `tier_credit_値引き` になる。英語の元の規則(別名を書かない)では、頭は準用の名前になる。替える前のバイナリでも同じで、木の `tests/apply_enum/注文ごとの値引き.rule` の生成したコードも組めない。`tests/apply_absent.rs` の生成したコードの試験が英語の規則だけなのは、このためである。§15.260 で、頭を準用の名前にした。
### 15.256 準用の E043 で、列挙の `max` と `min` の値を引数の取りうる値から読む(2026-10-21)
**きっかけ**:準用の E043 は、元の規則の順のある列挙の入力の範囲に渡す値が取りうる値を求めるとき、式で決まる値を列挙の全部とした(§15.252 の決定の 3、捨てたものの一つ目)。範囲の中の二つの段階の `max`(`define lift : tier = max(campaign_tier, member_tier)`、どちらも `range >=basic <=gold`)は、platinum にならないのに「取りうる値が分かりません」で落ちた(変異 `E043_the_later_of_two_tiers`)。`max` と `min` の答えはいつも二つの引数のどちらかで、引数の取りうる値は求められる。
**決定**:
1. **二つの値の `max` と `min` は、二つが取りうる値のうち、答えになりうるものを取る**(`Holds::of_expr`)。端の取り方は、数の区間の `max` と `min`(§15.195、`Checked::interval`)と同じである。`max` は、二つの一番前の値のうち後のほうより前にならず、二つの一番後の値のうち後のほうより後にならない。`min` は、一番前の値のうち前のほうより前にならず、一番後の値のうち前のほうより後にならない。その間にある、どちらかの引数が取りうる値が答えになりうる(二つが別々に動くなら、どれも実際に答えになる)。引数は名前と入れ子の `max` と `min` で、名前は §15.252 と同じに読む(式に列挙の値の語は書けない。E012)。引数が分かった値なら答えも分かった値で、`fold` の答えのように列挙の全部としか読めない引数があれば、そこから来る値は分からない値のままにする。
2. **取りうる値が分からないと言う注は、`fold` の答えか式で決まる値、と言う。** `max` と `min` を読むようにしたので、列挙の値が分からないのは、`fold` の答えなど、check が値を求めないものになった。
3. **数は直すところが無かった。** 数の E043 は、名前の区間(`Checked::ranges`)を使い、`max` と `min` の区間はもともと引数の区間から求めている(`max(a, b)` は、`a` が 0〜50、`b` が 10〜60 なら 10〜60)。列挙の読み方を、この端の取り方にそろえた。
**捨てたもの**:
- **二つが取りうる値を、そのまま合わせたものとして読むこと。** 安全側には正しいが、答えになりえない値まで範囲の外に出ると言うことがある。`min(campaign_tier, member_tier)` で `member_tier` に範囲が無いと、合わせた値には platinum があるが、`min` はキャンペーンの段階より後にならないので、platinum にならない。範囲の外に出ると言えば、その例は誤りになる。
- **ほかの式も、中の名前の取りうる値から読むこと。** 列挙の値を答える式は、名前、`max`、`min` のほかに無い。`fold` の答えは、走らせるまで分からない。
**変わること**:
- 準用で、範囲を書いた列挙の入力に、二つの値の `max` か `min` を渡す規則は、答えになりうる値がどれも範囲の中なら通る。前は、どれでも「取りうる値が分かりません」で落ちた。答えになりうる値に範囲の外の値があれば、「元の規則の範囲の外に出ます」と、その値を例に言う。前は「取りうる値が分かりません」だった。
- `fold` の答えを渡す規則は、これまでどおり「取りうる値が分かりません」で、注の文が「`fold` の答えか式で決まる値」になる。
- ほかの規則の出力は変わらない(下の突き合わせ)。
**確かめたこと**:
- `tests/apply_enum.rs` と、材料の `tests/apply_enum/`(英語を先に、日本語の版を横に)。
- 前の変異 `E043_the_later_of_two_tiers` とその日本語の版は、通る規則になった(名前はそのままにし、注を直した。golden の四つは消した)。キャンペーンの段階と範囲の無い会員の段階の `min`(`earlier_of_a_tier_and_any_member.rule`)も通る。
- 新しい変異が二つ。同じ二つの段階の `max`(`E043_the_later_of_a_tier_and_any_member`)は、platinum を例に「範囲の外に出ます」。`fold` の答え(`E043_the_tier_a_fold_answers`)は「取りうる値が分かりません」。どちらも E043 だけで、英日の文は golden と同じである。替える前のバイナリでは、どちらも「取りうる値が分かりません」で、`min` の規則も同じだった。
- 数:`max_and_min_of_numbers_are_read_from_their_arguments`。0〜50 と 10〜60 の `max` は通り、0〜50 と 0〜200 の `max` は 200 を例に E043、`min` は通る(替える前と同じ)。
- 台帳の E043 の「いつ出るか」と、reference の 7 章の段を直し、`docs/codes.md`・`codes.ja.md` を書き直した。
- 替える前と後の突き合わせ:木の 601 本の規則の `rulec check`(英日と JSON の英日)と `rulec certificate` の 3,005 の出力で、変わったのは `tests/apply_enum/` の前の変異の 2 本の 10 で、どちらも終了コード 1(E043)から 0 になった。ほかは一字も変わらない。
### 15.257 `fmt` は、字句が読む語を変えない(2026-10-21)
**きっかけ**:道具がファイルに書く名前を、あとで同じ言語が読めるかを見回した(yuen の DESIGN 21 章、sakai の DESIGN 21 章)。`rulec fmt` は、名前の中の `+` と全角の数字も書き換えていた。列挙の値 `松+竹` は `松+竹` になり、列挙の行が E058、その値を書いた表のセルが E014 で止まる。`第1案` は `第1案` という別の名前になる。整形する前は通っていた規則が、整形すると通らなくなった。
**原因**:整形は、全角の数字と全角の記号を、文字列とコメントの外ならどこでも ASCII に直していた(`fmt::normalize`)。字句は、`+` を語の頭でだけ足し算の記号として読み、名前の途中では名前の字として読む(語を切る字の `is_delim` に、`+` はあるが `+` は無い)。全角の数字は、語の頭では読めず(E002)、名前の途中では名前の字である。整形と字句が、どの字を記号とするかを別々に決めていて、名前の中で食い違った。
**決定**:
1. **整形は、字句が読む語(名前、演算子、数)を変えない。** 字句の決まり(`+` を名前の途中では名前の字にし、語の頭では記号にすること)は変えない。
2. **全角の記号を ASCII に直すのは、字句がそれだけで記号として読んだところだけにする。** 対象は `≦ ≧ < > + → ・ 、 ,` で、どこが記号かは字句そのものに聞く(`lex::read_through`。字句が一行をどう読むかを、読めない字のあとも読み進めて返す)。名前の中の `+` と、桁区切りを書いた数(E049)の中の `,` は、語の一部なので残す。
3. **全角の数字を ASCII に直すのは、字句が語を読めないところ(語の頭。E002)だけにする。** 一字直すごとに行を読み直すので、`10万円` は数の `10万円` になる。名前の中の全角の数字(`第1案`)は名前の字なので残す。
4. **ASCII に直すと隣の字と一つの語になる記号は、空白で離す。** `->`(字句は `-` と `>` の二つの語を読む)を `->` にすると出力列の印に、`<=`(`<` と `=`)を `<=` にすると一つの比較記号になる。記号だけ、前に空白、後ろに空白、両方、の順に試し、字句が直す前と同じ語を読む最初の書き方にする(`- >`、`< =`)。どれでも読めなければ、全角のまま残す。
5. **表の行を列に切るのは、文字列の外の `|` だけにする。** 字句は `starts_with "a|b"` の `"a|b"` を一つの文字列として読むが、整形は文字列の中の `|` でも切り、`"a | b"` に書き換えていた。同じ見回しで見つけた。
6. 表の見出しで、二つ目からの出力列の `->` を外すこと(§5.1)は変えない。構文解析は印のある形と無い形を同じ規則として読み、整形が一つの形にそろえる。整形が語を消すのは、ここだけである。
**捨てたもの**:
- **字句の決まりを変え、`+` をいつも足し算の記号にすること。** `松+竹` のような名前が書けなくなる。全角の `+` は、名前の中では名前の字として書かれ、読まれてきた。
- **整形の側にも、どの字が名前の字かの決まりを書くこと。** 二つ目の決まりは、字句と食い違いうる。今回の原因がそれである。
- **全角の数字をどこでも残すこと。** 全角で打った数は字句が読めない(E002)。名前とセルの値のほかに IME を要らなくするのが整形の役目で(§1.5)、語の頭の全角の数字は直す。
- **隣の字と一つの語になる記号を、空白を試さずに全角のまま残すこと。** `->` の多くは `->` の打ち間違いで、全角のまま残すと、どこが違うのかが見て分かりにくい。空白で離せば `- >` と二つの記号に見え、続く `check` の誤りと結びつく。
**変わること**:
- 名前に `+` か全角の数字があるファイルは、整形しても名前が変わらない。前は、整形すると E058 や E014 で止まった。
- 隣の字と一つの語になる全角の記号(`->`、`<=`、`>=`、`-≧`)は、空白で離した ASCII になる。前は、別の語(`->`、`<=`、`>=`、`->=`)になった。
- 桁区切りを書いた数の `,`(`1,000`)は、整形しても残る。前は `1, 000` になり、E049(桁区切りは書けない)が言われなくなって、字句は `1` と `000` の二つの数を読んだ。
- 表のセルの文字列の中の `|` は、整形しても変わらない。
- `Items` の口が答える定義の文(整形した行)も、同じに変わる。
- ほかのファイルの整形の答えは変わらない(下の突き合わせ)。
**確かめたこと**:
- `tests/fmt.rs`(新しい)の六つ。
- 木の全部の `.rule`(681 本。例、テストの材料、実験、サイトの規則)を整形し、字句が読み切る行は同じ語を同じ順に読み、読めない字のある行も、読めない字の直後でない名前を同じ順に読む。
- 同じ 681 本を IME で打ったとおりに書き換え(記号は字句が同じ語を読むところだけ全角に、数と日付の数字は全角に)、整形すると、元のファイルと同じ語を読む。
- 材料の `tests/fmt/`(英語を先に、日本語の版を横に)。記号だけを全角で打った `symbols_typed_with_an_ime.rule` と、日本語の版の `IMEで打った記号.rule`(名前は `第1案`、`第2案`、`松+竹`)は、書いたままで check を通り、整形すると同じ語を ASCII で読み、check を通る。数字も全角で打った `digits_typed_with_an_ime.rule` と `IMEで打った数字.rule` は、書いたままでは E002 で、整形すると check を通り、名前の全角の字は残る。
- 一つの語のままか二つの語のままかを、`->`、`<=`、`>=`、`-≧`、`松+竹`、`a+b`、`1,000`、`第1案`、`a + b`、`≦10`、`1 ,000`、`東、西` で確かめる。
- 文字列の中の `|` とコメントは変わらない。
- `fmt.rs` を替える前に戻すと、六つとも落ちる。
- 替える前と後の突き合わせ:木の 677 本と材料の 4 本の `rulec fmt`(書き換えたファイルと出力)で、違ったのは材料の日本語の版の 2 本だけである。替える前は、どちらも名前を書き換えた。
**見つけて直していないもの**:
- コメントに `|` を書いた表の行(`| a | # x|y`)は、コメントの中の `|` を最後の区切りと取り違え、コメントをセルとして並べ直す(`| a | # x | y |`)。字句が読む語は変わらないが、コメントは書いたとおりに残らない。替える前から同じで、表の行のコメントは最後の `|` の後ろにある、という §1.5 の決まりの中での食い違いなので、この変更には入れていない(§15.267 で、表の行を字句が読む `|` と `#` で切るようにして直した)。
### 15.258 口は、生成されるものだけを答える。`enums` のファイルには Connect のサービスを答えない(2026-10-21)
**きっかけ**:sakai で、`published language rulec.…` の塊に `enums` のファイルを並べ、`open host service` に生成されないサービスの名前(地域の `enums` のファイルなら `RegionsService`)を書いても、E302 にならなかった(sakai の DESIGN 20 章の「見つけて直していないもの」)。同じ口を読む dandori でも、`enums` のファイルを `use rule … connect` で呼ぶワークフローが通った。
**原因**:口(`ports::Engine::facts_of`)が、`enums` のファイルにも、ほかの規則と同じ形の Connect のサービス(`/rulec.<別名>.v<版>.<名前>Service/Decide`。要求と応答のフィールドは空)を答えていた。`enums` のファイルからは型のモジュールだけが生成され(§15.212)、`rulec gen` はサービスを書かない。`rulec api` も、`enums` のファイルには `connect` を書かない。口だけが、生成されないものを答えていた。
**決定**:
1. **口は、生成されるものだけを答える。`enums` のファイルには Connect のサービスを答えない**(`RuleFacts::connect` が None)。列挙は型のモジュールとして生成されるので、これまでどおり答える。`enums` のファイルに足す寄与のファイルにも、口は足す先の `enums` のファイルを答えるので(§15.240)、同じくサービスを答えない。`ritsu_ports::RuleFacts::connect` の説明にも書いた。
2. **口を読むほかの言語は、それぞれの診断で止める。** sakai は、公表された言語の `rulec "…"` のファイルに rulec がサービスを答えないと、その行を E302 に、その下の `open host service` を一つずつ E301 にする(sakai の DESIGN 23 章)。dandori は、`connect` で呼ぶ規則のサービスを rulec が答えないので、E005 で止める(注の「rulec から規則の Connect のサービスの情報を得られないので、`connect` で呼べません」は前からある)。sekisho と kaname は口のサービスを読まない。`enums` のファイルの出力を読む書き方は、前から E101(出力が無い)で止まる。
**捨てたもの**:
- **sakai が、入力も出力も無いことで `enums` のファイルを見分けること**(口は変えない)。口が生成されないサービスを答え続け、dandori の `connect` のように口を読むほかのところも、同じ答えを読む。
- **`enums` のファイルの事実を、口がエラーで答えること。** sakai は地図の rulec の成果物の全部に事実を尋ね、答えのないファイルを E105 にする。`enums` のファイルを持つ地図が、どれも落ちる。列挙の値と順は、`enums` のファイルにもある事実である。
- **生成したコードの呼び出し方(`typescript`・`python`・`go` の `Call`)も、`enums` のファイルには答えないこと。** 下の「見つけて直していないもの」の一つ目に書いた。`Call` は Option ではなく、変えると口を読む全部の言語に及ぶ。
**変わること**:
- 口は、`enums` のファイル(とそれに足す寄与のファイル)に Connect のサービスを答えない。ほかの規則の答えは変わらない(下の突き合わせ)。
- `enums` のファイルを公表された言語に並べた sakai の地図は、E302(`open host service` を書いていれば E301 も)で落ちる。`enums` のファイルを `use rule … connect` で呼ぶ dandori のワークフローは、E005 で落ちる。どちらも前は通っていた。
**確かめたこと**:
- `tests/ports.rs` の `an_enums_file_has_no_connect_service`:コーパスの `enums` のファイル(英語の `shared/catalog.rule` と、日本語の版の `shared/品目.rule`)の事実は、列挙を持ち、入力も出力も Connect のサービスも無い。`rulec api` も `connect` を書かない。それを読む規則(`catalog_shipping.rule`、`品目の送料.rule`)は、サービスを答える。
- sakai の変異 `E302_an_enums_file_as_a_published_language` と、日本語の版の `E302_公表された言語に並べた列挙のファイル`:E302 と E301 で、英日の文は golden と同じである。替える前は、どちらも通った。
- ritsu の `tests/enums_files.rs` と、材料の `tests/projects/regions/`(英語を先に、日本語の版を横に):`enums` のファイルを `connect` で呼ぶワークフローは E005 になる(替える前は通った)。ゲートとモジュールが `enums` のファイルの出力を読むと、E101 になる(替える前と同じ)。
- 替える前と後の突き合わせ:
- 口の答え(`facts`、`items`、`references`)を、木の全部の `.rule`(681 本)について書き出して比べた。違ったのは 15 本である。13 本は、`enums` のファイル(10 本)とそれに足す寄与のファイル(3 本)の Connect のサービスで、答えていたサービスを答えなくなった。2 本は §15.257 の材料で、`items` の定義の文が名前を書き換えなくなった。
- sakai の地図(例 4、テストの地図 7、ritsu のテストのプロジェクト 2、根の `ritsu.ctx`)の `check`(英日と JSON の英日)と `api`、変異 178 の `check`(英日と JSON の英日。`command` のある 4 本はそのコマンドの英日)、木の全部の `.flow`(223)・`.gate`(184)・`.kn`(32)の `check`(英日)、ritsu のテストのプロジェクト 11 の `ritsu check`(英日と JSON の英日)の 1,696 の出力で、違ったのは新しい材料の 16 だけだった。sakai の変異二つの 8 と、dandori の `connect` の二つの 4 と、それを持つプロジェクトの `ritsu check` の 4 である。
**見つけて直していないもの**:
- **`enums` のファイルを、`connect` を書かずに規則として呼ぶ書き方は止まらない。** 口は、生成したコードの呼び出し方(`Call`)に、`enums` のファイルにも関数(地域の `enums` のファイルなら `regions`)を答える。`enums` のファイルからは関数が生成されないのに、dandori のワークフローが規則のコードを同梱して呼ぶと(`use rule` に `connect` も `lambda` も書かない)、`check` が通り、`build --target temporal` が、型のモジュールに無い関数を読み込むコード(`import { regions } from "./rulec/typescript/regions"`)を書く。kaname のモジュールも、出力を読まずに呼ぶだけ(`let r = regions()`)なら通る。替える前から同じである。直すなら、口の `Call` が `enums` のファイルに関数を答えないようにするか(`Call` を Option にすると、口を読む全部の言語に及ぶ)、dandori と kaname が出力の無い規則の `use rule` を止めることになる。
- kaname の E101 の注は、出力の無い規則のフィールドを空のまま並べる(「regions.outputs のフィールドは です。」、「The fields of regions.outputs are .」)。kaname の文なので、この変更には入れていない。
### 15.259 `import` は、名前として読めない語に仮の名前を付ける(2026-10-21)
**きっかけ**:§15.257 と同じ見回しで、`rulec import` が表の見出しとセルの語をそのまま名前にしていることが分かった。見出し `Weight (kg)` の列は入力の行 `Weight (kg)(c1) : mass[kg]` になって E057、セルの値 `re-quote` は列挙の行の `re-quote(v2)` になって E058 で、書いた下書きを `check` が読めなかった。閾値を値として書いたシート(`<=2000g` と `>2000g`)も同じで、サイトの使い方の例(シナリオの 2-1)が、この読めない下書きを見せていた。
**原因**:`import` は、名前に別名を添えるかを、ASCII の識別子かどうか(`ascii_ok`)だけで決め、ASCII でない語は `<語>(<別名>)` と書いた。語が名前として読めるか(空白、`(`、`-`、`.`、`/`、`・` などで切れないか、字か `_` で始まるか)は見ていなかった。
**決定**:
1. **名前として読めない語は、別名を名前にし、元の語を `# 推定` の注に残す。** 見出しなら列の別名(入力は `c1`、`c2` …、出力は `o1` …)を、セルの値なら値の別名(`v1`、`v2` …)を名前にする。注は、見出しなら「見出しの「Weight (kg)」は名前として読めないので、仮に c1 と名付けた」(空の見出しなら「見出しが空なので」)をその列を宣言する行に、値なら「名前として読めない値には仮の名前を付けた(v2 は「re-quote」)」を列挙の行に書く。規則の名前(`--name` かファイルの名前)も同じに読み、読めなければ `imported` にして、`rule` の行に注を書く。
2. **読めるかどうかは、rulec の字句に聞く**(`lex::is_bare_word`。`rulec source pin` が引用箇所を書くときにも使う)。読める語は、これまでどおりそのまま名前にする(ASCII でなければ別名を添え、予約語なら `_` を足す)。
3. **仮の名前が、シートがほかの列や値に付けた名前と同じなら、`_` を足して離す**(値が `v2.0`、`v1` の順に現れる列では、`v2.0` の仮の名前 `v1` が値の `v1` と重なるので、`v1_` にする)。`check` は同じ名前の入力や同じ値を二つ書いても止めないので(下の「見つけて直していないもの」)、重なった名前は黙って通ってしまう。§15.266 で、`_` ではなく番号で離すようにした(`v1_2`)。`_` は PascalCase と `.proto` の JSON の名前で消え、`v1_` と `v1` は一つの識別子になる。
4. **値の列に数や日付が混じると、表のセルも値の名前を書く。** 前は、列挙の値の列でも、数と日付のセルをそのまま書いた(`1200JPY`、`2026-04-01`)。
5. **注と文字列に書くシートの語は、一行にする。** 改行などの制御文字は空白にする(改行のある見出し)。説明の文字列(`description`)に書くファイルの名前の `"` は `'` にする。そのままでは、文字列がそこで終わる。
**捨てたもの**:
- **名前を引用符で書くこと。** rulec の名前は引用符で書けない。字句は `"…"` を文字列として読む。
- **読めない字を `_` に置き換えて名前を作ること**(`Weight (kg)` を `Weight__kg_` に)。読める名前に見えるが、どの字を置き換えたかは注を読まないと分からず、置き換えた二つの見出しが同じ名前になることもある。仮の名前(`c1`)なら、人が付け直す名前だと一目で分かる。
- **読めない語のある列を飛ばすこと。** 列が欠け、下書きが元の表と食い違う。
**変わること**:
- 名前として読めない見出しやセルの値のあるシートの下書きは、その語を仮の名前にし、`check` がそのまま読める。前は E004、E005、E006、E057、E058、E063 で止まった。
- 読める語だけのシートの下書きは、一字も変わらない(下の突き合わせ)。
- reference の `rulec import` の段、AGENTS.md と、そこから組むスキルの SKILL.md、サイトの使い方の例(シナリオの 2-1、英日)を合わせた。サイトの例は、閾値を値にした `<=2000g` と `>2000g` が `v1` と `v2` になった、いまの実物にした。
**確かめたこと**:
- `tests/import.rs` の `a_draft_is_read_back_by_check_whatever_the_sheet_writes` と、材料の `tests/import/`(英語を先に、日本語の版を横に。九つの種類で 18 本)。空白と括弧のある見出し、区切り(`/`、`・`)のある見出し、数字で始まる見出しと値、`-` と `.` のある値、空白のある値、語の中の数と日付、比べる値(`<=2000g`)、シートの名前と仮の名前が重なるもの、空の見出しと改行のある見出し。英日のどちらの下書きでも、読めない語は仮の名前になって元の語が注に残り、`check` は何も言わずに通り、`fmt --check` も通る。
- `a_rule_name_not_read_as_a_name_is_named_for_now`:ファイルの名前(`fees "2026".csv`)と `--name "parcel fee"` は `imported` になり、説明の文字列には `fees '2026'.csv` と書く。
- `import.rs` を替える前に戻すと、新しい二つが落ちる。
- 替える前と後の突き合わせ:上の材料 18 本、`tests/import.rs` の CSV 8 本、`tests/xlsx.rs` のブック 1 冊、コーパスのブック 1 冊、サイトの例のブック 2 冊の `rulec import`(英日の 60)と、書いた下書きの `check`(JSON)を比べた。違ったのは 40 で、材料の 36 とサイトの例の 4 である。どれも、替える前は `check` が下書きを読めず、替えた後は読める。ほかの 20(読める語だけのシートと、表の形でないシート)は一字も変わらない。
- §15.257 から §15.259 の三つを入れたあと、木の全部の `.rule`(685 本)の `rulec check`(英日と JSON の英日。終了コードを含めて 2,740 の出力)は、替える前と一字も変わらない。三つとも、検査の答えは変えない。
**見つけて直していないもの**:
- **`check` は、同じ名前の入力を二つ宣言しても、列挙に同じ値を二度書いても止めない**(`inputs` に `c1 : bool` を二行、`enum e(e) = v1 | v1`)。どちらも通り、`gen` もコードを書く。替える前から同じで、この変更では、`import` が重なる名前を書かないようにしただけである。§15.262 で、どちらも E034 で止めるようにした。
### 15.260 準用が持ち込む名前を、どの生成先でもほかの識別子とぶつからない識別子で書く(2026-10-21)
**きっかけ**:§15.255 の見つけて直していないもの二つ。準用で持ち込んだ名前の生成コードでの識別子は、`<元の規則の別名>_<名前の別名>`(元の規則に別名が無ければ `<準用の名前>_<…>`)だった。これが二つの形でほかの識別子とぶつかった。一つは、この規則の名前とぶつかる形である。`apply read` が持ち込む `credit` は `read_credit` になり、`credit -> read_credit` が付けるこの規則の出力の名前と同じになる。もう一つは、持ち込んだ名前どうしがぶつかる形である。日本語の名前に別名を書いた元の規則(`段階の値引き(tier_credit)`)を三度準用すると、三つの準用の値引きがどれも `tier_credit_値引き` になる。どちらも `check` と `gen` は何も言わず、生成したコードは TypeScript・JavaScript・Go・Swift・Java で組めなかった(同じ名前を二度宣言する)。日本語の版では、SQL の問い合わせも答えを違えた。木では `tests/apply_enum/order_credits.rule` と `注文ごとの値引き.rule`、`tests/apply_absent/値の無い段階を読む規則.rule` が当たっていた。
**原因**:識別子は、準用の展開(`apply.rs` の `Renamer::name`)と生成器(`Gen::new` の `idents`)がそれぞれ作り、規則の中で識別子が一つずつ違うことを確かめるところが無かった。展開は、持ち込む名前ごとに識別子を作るだけで、この規則のほかの名前を見ない。頭に元の規則の別名を使うので、同じ元の規則の準用は二つとも同じ頭になる。§15.71 の (1) は、`->` で付け替えた出力の識別子を名前そのものにして、持ち込んだ出力列とのぶつかりを一つ避けたが、ほかの形は残っていた。生成器は名前ごとに別名を引くだけで、二つの名前が同じ識別子になっても気づかない。Python・Rust・Ruby・PHP は同じ名前に代入し直すだけなので、そこでは黙って通った。
今までの試験が見逃したのは、生成したコードを組んで走らせる試験が、手で選んだ規則しか走らせていなかったからである。CI の「the generated code runs in every language」は `ゆうパック運賃.rule` の一本だけである。`tests/apply.rs` は束の規則(一つの元の規則を一度ずつ準用し、名前はぶつからない)と `適用.rule`、`tests/apply_absent.rs` は英語の規則だけを走らせていた(日本語の版は、この穴のため外した)。`tests/apply_enum.rs` は `check` しか走らせず、`order_credits.rule` の生成したコードは、どの試験も組んでいなかった。`tests/apply.rs` の識別子の試験(§15.71)は、一つの束の規則の項目の識別子が違うことしか確かめず、入力と出力も、生成先ごとの綴り(camelCase、大文字と小文字)も見ていなかった。
**決定**:
1. **持ち込む名前の識別子は、準用の識別子(別名、無ければ名前)に、元の規則の生成コードがその名前に使う識別子をつないだものにする**(`codegen::apply_prefix`、`codegen::Idents`)。`apply キャンペーン(campaign)` が持ち込む `値引き(credit)` は `campaign_credit` である。英語の元の規則(別名を書かない)では前と同じになる。別名を書いた元の規則(日本語の名前の元の規則はどれもそう)では、頭が元の規則の別名から準用の別名になり、後ろが元の規則のコードでの識別子(表の出力列なら、出力の宣言の別名)になる。準用の名前が大文字で始まるなら、その一字を小文字にする(Ruby は大文字で始まる名前を定数として読み、関数の中で代入できない)。持ち込む列挙と、表と節の名前も同じ作り方にする。
2. **規則の中の識別子を、生成の前に一か所で決める**(`codegen::assign_identifiers`。`prepare` の最後で、§15.210 の予約語の `_` を付けていたところ)。この規則が宣言する名前の識別子(別名、無ければ名前そのもの)は、書いた人が選んだものとして先に取り、変えない。そのあと、rulec が作る識別子を宣言の順に決める。予約語の名前に付ける `_`(§15.210)と、準用と寄与(§15.227。寄与は `<寄与>:<名前>` で持ち込む)が持ち込む名前である。作る識別子は、どの生成先の綴りでも、すでに取った識別子と同じにならず、どの言語の予約語でもないものにする。同じになるなら、後ろに `_2`、`_3`… を付けて、最初に何ともぶつからないものを取る(予約語の `_` の後ろなら `class_2`)。規則を書く人には名前を替えさせない。
3. **ぶつかるかは、生成先の綴りごとに比べる**(`value_spellings`)。比べる綴りは次のとおり。そのまま(Python、TypeScript、JavaScript、Rust、Ruby、Go の局所変数、SQL、NumPy、Wasm)。英字の大文字と小文字を区別しないもの(SQLite は引用符で囲んだ `"Credit"` と `"credit"` を同じ列として読み、Wasm のコンポーネントの WIT は名前を小文字にする)。camelCase(Swift)と、それに Java が予約語に付ける `_` を足したもの。PascalCase(Go のフィールド)。PHP の二つの綴り。PostgreSQL が残す先頭の 63 バイト(それより長い名前は切られ、そこまで同じ二つは一つの名前になる)。番号を付けた名前が 63 バイトを超えるなら、頭を切り、番号が 63 バイトの中に残るようにする。
4. **識別子の種類は三つで、種類ごとに比べる。** 値(入力、出力、並びとその要素のフィールド、群、`shape`、項目が名付ける値)、表と節の名前(SQL の `<表>_row` の列と、Rust の証明の `rows_<表>`)、列挙の型(PascalCase で、大文字と小文字を区別せずにも比べる。規則の関数と型の名前、組み込みの区分の型、`enums` のファイルの型も、取った識別子に入れる)である。表の SQL の行番号の列は、値の列と同じ問い合わせの中に並ぶので、値と一緒に比べる。
5. **W121 は、予約語の名前の識別子に番号が付くときは、その識別子を言い、注で理由を言う**(「`class_` は、この規則のほかの名前の識別子です」)。`check` も同じ関数(`codegen::made_identifiers`)で識別子を求める。準用が持ち込む名前は規則を書く人の名前ではないので、番号が付いても何も言わない。台帳の W121 の「いつ出るか」に、別名の無い予約語の名前のことを足した。
**捨てたもの**:
- **ぶつかったら E034 にして、規則を書く人に名前を替えさせること。** 出力に `read_credit` と名付けるのは自然で、替えさせる理由は識別子の作り方の都合でしかない。元の規則の名前は別の承認の単位で、この規則からは替えられない。
- **ぶつかる生成先でだけ名前を替えること。** §15.210 と同じ理由で捨てた。一つのアプリが二つの言語で同じ規則を呼ぶとき、言語ごとに名前が違うと読みにくい。
- **頭に元の規則の別名を使ったまま、ぶつかったら番号を付けること。** 同じ元の規則の準用はいつもぶつかり、どの準用の値かが識別子から読めない。準用の名前は規則の中で一つずつ違うので、それを頭にすれば、ぶつかるのは名前がたまたま重なるときだけになる。
- **頭と名前の間を `__` にするなど、ぶつかりにくい形にすること。** camelCase と PascalCase は `_` を消すので、`read__credit` も `readCredit` になる。ぶつからないことは、比べて確かめるしかない。
- **書いた人の名前どうしのぶつかりも、この手順で直すこと。** 書いた人が二つの名前に同じ別名を付けたときなどは、どちらかを替えると、書いた人が選んだ識別子が変わる。この節が決めるのは、rulec が作る識別子である(見つけて直していないもの)。
**変わること**:
- 準用のある規則の生成したコードで、持ち込んだ名前の識別子が変わる。別名を書いた元の規則を準用する規則では、`retirement_allowance_手当` が `retirement_allowance` のようになる(`非常勤退職手当.rule`)。別名を書かない英語の元の規則を準用する規則では、ぶつかるところにだけ番号が付く(`order_credits.rule` の `campaign_credit_2`)。どれも関数の中の局所変数、持ち込んだ列挙の型、SQL の行番号の列で、関数の引数と戻り値、`rulec api`、記録は変わらない。前は組めなかった五つの言語のコードが組め、SQL の答えが参照の評価器と同じになる。
- 予約語の名前の識別子 `class_` が、ほかの名前の識別子でもあるときは `class_2` になる。前は二つの名前が同じ識別子になり、組めなかった。入力なら、生成した関数の引数の名前と、`rulec api` が言う名前が変わる。
- `check` の出力と証明書は変わらない。W121 は、番号が付くときだけ文が変わる。
**確かめたこと**:
- `tests/apply_names.rs` と、材料の `tests/apply_names/`(英語を先に、日本語の版を横に)。元の規則が三つと、ぶつかる形ごとの規則が七つある。準用が持ち込む名前と同じ名前の出力、一つの元の規則の三度の準用、二つの準用の名前が重なるもの(`member` が持ち込む `tier_credit` と、`member_tier` が持ち込む `credit`)、Swift と Java が同じに綴る名前(`readCredit`)と SQLite が同じ列として読む名前(`read_CREDIT`)、自分の列挙を持ち込む元の規則の二度の準用(一つはこの規則の値の違う列挙と同じ型の名前になる)、PostgreSQL が 63 バイトで切ると同じになる長い名前、予約語の `_` がほかの名前と同じになる入力である。
- どれも `check` を通る。予約語の規則は W121 だけで、英日の文は `tests/golden/apply_names/` の golden と同じである。持ち込んだ名前の識別子は試験の表に書いたとおりになり、rulec が作った識別子は、どの綴りでもほかの識別子とぶつからない。
- 生成したコードは、12 の言語の 36 の走らせ方(PostgreSQL の関数を含む)のどれでも、参照の評価器と同じ答えを返す。替える前のバイナリでは、十四本のうち九本が組めないか、答えられなかった。英語の六本(出力の付け替え、名前の重なり、綴りの重なり、列挙の二度の準用、長い名前(PostgreSQL の関数だけ)、予約語)と、日本語の三本(三度の準用、列挙の二度の準用、予約語)である。日本語の残りの四本が通ったのは、頭が元の規則の別名で、たまたまぶつからなかったからである。
- 木の全部の規則のうち `prepare` を通るもので、rulec が作った識別子がほかとぶつからないこと(`no_identifier_rulec_makes_meets_another_in_the_tree`)。木の準用のある規則のうち `check` を通る 24 本を、全部の言語で走らせること(`every_rule_of_the_tree_that_applies_another_runs_alike`)。替える前は、このうち 3 本(`order_credits.rule`、`注文ごとの値引き.rule`、`値の無い段階を読む規則.rule`)が組めなかった。
- 替える前と後の突き合わせ:木の 677 本の規則(追跡している `.rule` の全部)の `rulec check`(英日と JSON の英日)・`rulec certificate`・`rulec gen` の 4,062 の出力のうち、変わったのは 16 本の生成物だけで、`check` と証明書は一字も変わらない。16 本はどれも準用のある規則である。十五本は、別名を書いた元の規則を準用するもの(頭が元の規則の別名から準用の名前に、後ろが元の規則のコードでの識別子に変わった。`非常勤退職手当.rule`、`merging/` の四本、`注文ごとの値引き.rule` ほか)で、残りの一本は、英語の名前がぶつかる `order_credits.rule`(`campaign_credit_2`、`launch_credit_2`、`repeat_credit_2`)である。変わったのは関数の中の局所変数、持ち込んだ列挙の型、SQL の行番号の列と、それを含む人向けのページの JavaScript で、関数の引数と戻り値、`rulec api` は変わらない。足した 20 本では、予約語の規則の二本の `check` の出力(W121 の文)と、十三本の生成物が変わった。
**見つけて直していないもの**:
- 無い。書いた人の名前どうしが一つの識別子になるときは、別名が同じなら §15.262 の E034 で、生成先の綴りでだけ重なるなら §15.263 の W121(どの生成先でも重なるなら E034)で、`check` が言う。大文字で始まる名前を Ruby が定数として読むことも、§15.263 の W121 が言う。準用が持ち込む名前は、この節の決定の 1 で小文字で始まる。
### 15.261 上の表の行が名前を渡すときも、上の表が書かない値を事実として書く(2026-10-21)
**きっかけ**:§15.253 の見つけて直していないもの。上の表の行が値ではなく名前を書くと(`| >=gold | member |`)、`rulec check` は通るのに、証明書を二つの再検査(`tools/recheck.py` と Lean の `rulec-recheck`)が落とした。再現の `named_lift.rule` では、上の表は basic を書かず、下の表は `>=silver` の一行である。証明書の覆いの basic の下は理由の無い `upstream` の葉で、上の表が書かない値の事実(`above.never`)は一つも無かった。下の表の行のセルが basic も取る形(`-` など)では、行の箱からも basic が除かれていて、二つの再検査は箱の確かめでも落とした。
**原因**:区画の検査は、上の表が書く値(`Checked::out_values`)を、行ごとに `Checked::row_writes` で読む。名前を書く行は、その名前がこの表の列なら、行のその列のセルが通す値を書くと読み、ほかの名前ならその型のどの値も書くと読む。下の表の箱は、そうして求めた値の外の座標を除いて組む(`TableRegion::build` の `reachable`)。一方、証明書が書ける事実は、上の表のどの行も値の語を書いていて、そのどれもその値でないときだけだった(`never_written`)。名前を書く行の `produces` は null で、二つの再検査には、その行が何を書くかを証明書から読む手段が無かった。区画の検査は、証明書に書けない事実で箱を狭めていた。
**決定**:
1. **行が自分の表の列を渡すときは、証明書がどの列を渡すかを書く**(行の `passes`。決める列ごとに、渡す列の軸の番号、ほかは null)。そういう行は、発火した点でのその列の値を書き、行が発火するのは自分の箱の中だけである。だから、値がその軸での行の箱の外にあれば、その行はその値を書かない。`passes` は、そういう行にだけ書くので、ほかの証明書は変わらない。
2. **上の表が書かない値の事実は、値の語を書く行と、列を渡す行から決める**(`region.rs` の `settled_writes`、`never_writes`)。列を渡す行は、区画の検査と同じく、行のその列のセルが通す値(`Checked::row_writes`)を書くと読む。自分の表の列ではない名前を渡す行は、その型のどの値も書きうるので、その表について事実は書かない。
3. **箱を狭めるのは、証明書に書ける事実だけにする。** 下の表の箱から除く座標を、`out_values` からではなく、事実と同じ `never_writes` から求める(`reachable`)。区画の検査が箱を狭めるのに使った事実は、どれも証明書に書いてある。前と比べて狭め方は変わらない。`out_values` も、自分の表の列ではない名前にはその型の値を全部入れていたからである。
4. ★ **二つの再検査は、事実を確かめるとき、列を渡す行を、その行の箱で読む**(Python の `passed_axis` と `never_written`、Lean の `passedAxis` と `checkAbove`)。`passes` が指す軸は、数の軸でも接頭辞の軸でもない、値の語の軸に限る。値がその軸の座標にあり、その座標が行の箱(`accepts`)に入っていれば、その行はその値を書くので、事実は成り立たない。行の箱は、これまでどおりセルから組み直して確かめる(§15.253)。規則のファイルを添えたときは、答えのセルが渡す列の名前であることも読み戻す(Python の `answer_words`、Lean の `answerWords`。一つの場所に書いた表ではその行の答えのセル、何か所かに書いた表では `answers` が指すセル)。Lean の定理は変えない。`Sieve.asked` は事実の座標の外の点を読み、事実そのものは `checkAbove` が上の表の行を読んで確かめる、という分け方は §15.253 のままで、変わったのは、行を読むところの数行である。
5. ★ **名前が取りうる値は、区画の検査が読むとおりに読む。** 準用の E043 の `Holds`(§15.252、§15.256)は、範囲を書いた入力や名前を指すだけの定義から、もっと狭く値を求める。区画の検査はそう読まず、自分の表の列ではない名前にはその型の値を全部とるので、そこから事実は生まれない。再検査も、その名前の値を証明書から組み直す手段を持たない。
6. ★ **二つの再検査は、行が書くと証明書が言う語が、その列の型の値で、規則の名前でないことを確かめる**(Python の `check_words`、Lean の `checkWords`)。二つの再検査には、前からの穴があった。名前を渡す行を、その名前を値の語として書く行だと偽った証明書(`"produces":["member"]`)を、二つの再検査がどちらも通した。規則のファイルを添えても、ファイルはそこに `member` と書いているので、読み戻しが通る。その偽りの上では、上の表が実際に書く値(platinum)を書かないと言え、下の表の箱と覆いからその値を除けた。下の表に platinum の行が無い規則(`check` が E101 で止める)でも、完全だと言う証明書が通った。答えのセルの語は、規則が同じ綴りの名前を宣言していれば名前で(§3.2)、そのときの行はその名前の値を書く。だから、値の語は列の型の値に限り、規則の名前と同じ綴りの語は値の語と認めない。
**捨てたもの**:
- **名前を書く行が書きうる値を、証明書に並べること**(`produces` を値の並びにする)。再検査は、並べた値を信じるか、どこからか組み直すことになる。行の箱は、その列で行が取る値をすでに持ち、再検査はそれをセルから確かめている。
- **名前を書く行があれば、箱を狭めないこと。** `named_lift.rule` のように完全な規則が、basic の点に行が無いと言われて E101 で止まる。
- **E043 の `Holds` と同じに、ほかの名前の値も読むこと。** 上の決定の 5 のとおり、区画の検査がその読み方で狭めていないので、書く事実が無い。読むようにするなら、区画の検査と再検査の両方に、名前の値を組み直す読み方を足すことになる。
- **Lean の証明を変えること。** 事実が成り立つことを確かめるのは、証明書の行を読むプログラムの側で、主張の意味(`Semantics.lean`)と、それを箱に移す定理(`Certified.cells_*`)は変わらない。
**変わること**:
- 自分の表の列を渡す行のある規則の証明書は、その行に `passes` を書く。木では 24 本で、どれもステートマシンが状態や運ぶ値を据え置く行(`| … | state |` の形)である。その 24 本は、事実も箱も覆いも変わらず、二つの再検査が読み戻す答えのセルが、列を渡す行ごとに一つ増える。
- 列を渡す行が、下の表の列の値を書かないときは、証明書がその値を事実として書き、二つの再検査がその証明書を通す。前は、`check` が通るのに、どちらの再検査も落とした。
- 二つの再検査は、行が書くと言う語が列の型の値でないか、規則の名前と同じ綴りなら、証明書を落とす。rulec が書く証明書では、そういう語は出ない(木の 404 本の証明書で、落とすものは増えない)。
- `check`、生成したコード、`rulec api` は変わらない。
**確かめたこと**:
- `tests/above_names.rs` と、材料の `tests/above_names/`(英語を先に、日本語の版を横に)。名前を渡す上の表の形ごとに六つ。再現の形(`passed_member.rule`)、引き上げた段階が下の表の二つの軸の一つ目で、ある行の箱から値が除かれる形(`passed_member_and_area.rule`)、間の表が上の表の決めた列を渡す形(`passed_lift_on.rule`)、真偽の印を渡す形(`passed_flag.rule`)、値の無いことのある段階を渡す形(`passed_optional.rule`)、自分の表の列ではない名前を渡す形(`passed_other_name.rule`)である。
- どれも `check` を通り、何も言わない。証明書は、表ごとに決めた事実と `passes` を書き、規則のファイルを添えて二つの再検査を通る。替える前のバイナリと二つの再検査では、はじめの五つの証明書を、どちらの再検査も落とした。表の外の名前を渡す形は、前も後も事実を書かず、通る。
- 日本語の版の証明書は、どの表でも、事実、行の箱、`passes` が英語の版と同じである。
- 表の外の名前を渡す形で、下の表から basic の行を外すと、前と同じく E101 で止まる(そこから箱は狭まらない)。
- 下の表を `silver, gold` の一行にした規則(`check` は E101 で止まり、「当てはまらない例: lift = platinum」「この例を作る入力: member = platinum」と言う)に合わせて、名前を渡す行が `member` という語を書くと偽り、platinum を上の表が書かない値と言って箱と覆いから除いた証明書を、替える前の二つの再検査はどちらも通した(規則のファイルを添えて、セルを 8 つ読み戻したうえで)。いまは二つとも落とす(`a_gap_hidden_behind_a_name_said_to_be_a_word_is_refused`)。
- 偽った証明書:列を渡す行が書く platinum を、上の表が書かない値と言って箱から除いたもの、行の `passes` を消したもの、`>=silver` の箱から gold を除いたもの、`member` を渡す行が `area` を渡すと言ったもの、`member` を渡す行がその名前を値の語として書くと言い、事実と箱と覆いもそれに合わせたもの、規則が `gold` という名前を宣言すると言ったものを、二つの再検査がどれも落とす。替える前の二つの再検査は、名前を値の語と偽った証明書を通した。
- Lean:`lake build` が通り、警告は替える前と同じ六つ(どれも ChoboModel・DandoriCore・RulecMain の前からの非推奨の名前)。
- 替える前と後の突き合わせ:木の 677 本の規則の `rulec check`(英日と JSON の英日)・`rulec certificate`・`rulec gen` の 4,062 の出力のうち、この節で変わったのは 24 本の証明書だけで、どれも `passes` が足されただけである(事実、箱、覆いは変わらない)。`check` と生成物は変わらない。木の 404 本の証明書を二つの再検査にかけた 808 の出力のうち、再検査が落とすのは前も後も同じ 40 で(合わせる規則の部分のファイル 18 本と `m_w132` の二本を、その部分のファイルで確かめたもの)、変わったのは `passes` のある 24 本の 48 だけで、読み戻した答えのセルの数が増えた。足した 12 本では、列を渡す 10 本の証明書が変わり、二つの再検査が落としていたその 20 の出力が、どれも通るようになった。
**見つけて直していないもの**:
- **名前を書く行のある上の表では、`above_rows` の葉(§15.195)と `apart` の事実(§15.115)を、rulec も二つの再検査も組まない。** どちらも、値を書く行を `produces` だけで数え、名前を書く行があればやめる(rulec の `span_of_value`・`produces_where` ほか、Python の `writers`・`produced_where`、Lean の `producedWhere`)。どちらの側も何も主張しないので、証明書が通らなくなることはない。そういう葉や事実が要る規則では、覆いがほかの理由を探すことになる。
### 15.262 一つの名前は一度だけ宣言する(2026-10-22)
**きっかけ**:§15.259 の見つけて直していないものに、`check` は同じ名前の入力を二度宣言しても、列挙に同じ値を二度書いても止めない、と書いた。`inputs` に `weight : number range >=0 <=9` と `weight : number range >=0 <=99` を書くと `ok` で、検査は二行目を読み、書いた人の `<=9` は消えて、規則は 0 から 99 を受け付けた。`rulec schema` の JSON には `weight` が二度出て、`"required":["weight","weight"]` になった。`number` と `bool` の二度書きでは、あとの `bool` が勝ち、表のセルに「この列は bool です」と、書いた人の意図と違う誤りが出た。`enum e(e) = v1 | v1` も通った。§15.260 の見つけて直していないものの一つ目(書いた人の二つの名前に同じ別名を書くと、生成したコードが組めないか答えを違える)も、同じ形の穴である。
**見回し**:名前の並びごとに、同じ名前を二度書いたときの `check` の答え、`schema`・`api`・`gen` の出力、生成したコードを全部の言語で走らせた答え(`rulec test`)を、替える前のバイナリで確かめた。
- 黙って片方が勝つか、生成したコードが組めないか答えを違えたもの:
- 値。入力の二度書き(範囲が違うもの、型が違うもの、同じもの)、出力の二度書き、`derive`・`define`・`count`・`sum`・`max`・`min`・`open` の二度書き、それらと入力の重なり、表や節の出力と定義の重なり、入力と同じ名前の出力(入力をそのまま返すつもりの出力も、生成したコードが答えを返さなかった)、一つの表の見出しに同じ出力の列を二つ書いたもの。
- 識別子。同じ並びの二つの名前に同じ別名を書いたもの(入力、出力、定義、表の出力、畳み込みの答え、`open`、たどる並び、要素のフィールド、表、列挙、列挙の値、グループ、`shape`、前提のつながり)、一方の別名がほかの名前と同じで、その名前に別名が無いもの、一つの名前(出力とそれを決める定義、一つの名前を決める二つの表)に違う別名を書いたもの。
- そのほか。同じ名前の列挙(あとの宣言が前の宣言を置き換えた)、列挙の値の二度書き、同じ名前のグループ(あとのグループが勝った)、グループに書いた値の二度書き(Go の生成コードが組めない)、列挙の値と同じ名前のグループ(セルが二通りに読まれた)、`enums` のファイルで列挙の名前がほかの列挙の別名と同じもの(`import enum` が、別名を持つほうの列挙を読んだ)、同じ名前の `shape`(あとの宣言の契約から読んだ)、表と例と手順の例と名前付きの並びの見出しに同じ列を二度書いたもの(表では二つの列が別々の入力として数えられ、どの入力も当たらない行が E102 にならなかった。例と手順の例と並びでは、後ろの列が勝った)、畳み込みの答えとたどる並びの重なり、準用の出力の名前と要素のフィールドの重なり(E029 で止まった)、一つの準用で同じ出力に名前を二度付けたもの(二つ目の名前が黙って捨てられ、それを読む列が「宣言されていません」の E012 になった)、同じ名前の二つの準用(書いていない名前の「表か節の名前 `c1:t1` が二度あります」で止まった)。
- すでに止まっていたもの:行ラベル、表と節の名前、出典の名前、入力とたどる並びと要素のフィールドの重なり、前提のつながりの名前、`import enum` で読んだ列挙(E034)、二つの手順の例(E055)、二つの名前付きの並び(E026)、二つの `machine`(E050)、`held` の二度書き(E056)、`carry` の二度書き(E072)、二つの寄与(E067)、`chances` の見出しの列の二度書き(E070)、節の `when` の列の二度書き(E046)、準用の入力の二度の読み替えと、準用の出力の名前と入力や定義の重なり(E041)、畳み込みの答えと入力や上の値の重なり(E021)。
- 黙って通るが、読み方が一つに決まるもの:`a(x)` と `x(y)` のように、ある名前の別名がほかの名前と同じで、その名前が別名を持つもの。規則の中の語は名前で、生成したコードは別名で書くので、二つの値は二つの名前と二つの識別子を持ち、全部の言語で参照の評価器と同じ答えを返した。`enums` のファイルの列挙だけは、`import enum` が名前か別名で列挙を探すので、読み方が二通りになる。
**原因**:名前の宣言は、名前を鍵にした表に宣言の順に入る(型の検査の `syms`・`enums`・`groups`、生成器の `Idents`)。同じ鍵の二つ目は、一つ目を黙って置き換える。重なりを言うところは、穴が見つかった並びごとに足してきた(行ラベル §15.66、出典 §15.68、`import enum` §15.212、たどる並びとフィールド §15.219、前提のつながり §15.222)。ほかの並びには無かった。`rulec schema`・`api`・`gen` は宣言の並びをそのまま書くので、二つとも書いた。別名は、§15.260 が rulec の作る識別子だけをほかと比べ、書いた人の識別子どうしは比べていなかった。見出しは、構文解析が列を並べるだけで、同じ名前の列を比べるところが無かった。
**決定**:
1. ★ **ファイルの行が宣言する名前は、一つの名前に一度だけにする。二度目を E034 で止め、一度目に印を付ける。** ほかのところが名前を読む前に、ファイルの行だけを見て確かめる(`names::check`。`check` の段の初めと、`schema`・`api`・`gen` が読む `prepare` の初め)。見つかれば、構文の誤りと同じく、その先の検査はしない。その先の答えは、どちらの宣言を読んでも、書いた人の意図の片方にしかならないからである。
2. **並びは、名前の種類ごとに一つとする。** 値(入力、たどる並びとその要素のフィールド、出力、`derive`・`define`・`count`・`sum`・`max`・`min`・`open` と表や節の出力の列)、表と節、列挙、一つの列挙の値、グループと一つのグループに書いた値、`shape`、出典、準用、前提のつながり、である。★ 値は一つの並びとする。出力は自分を決めるものと同じ名前になり、一つの名前をいくつかの表や節で決めるのは一つの定義の組(§15.66)である。それ以外の重なりは、入力と同じ名前の出力も含めて止める。入力をそのまま返すなら、出力に別の名前を付けて `define` で返す(注にそう書く)。前からある E034(表と節、出典、たどる並びとフィールド、前提のつながり)はここに移した。表と節、出典、前提のつながりは文をそのままにし(前提のつながりにだけ一度目の印を足した)、たどる並びとフィールドは値の並びの文にした。
3. **識別子も同じにする。** 生成したコードが書いた人の識別子(別名、無ければ名前)をそのまま書く並び(値、表と節、列挙、一つの列挙の値、グループ、`shape`、前提のつながり)で、二つの名前が一つの識別子になるとき(一方の別名が、他方の別名か名前と同じ)と、一つの名前に二つの別名を書いたときを、E034 で止める。準用の別名は、持ち込む名前の識別子の頭になり、ぶつかれば rulec が番号を付けて離す(§15.260)ので比べない。手順の例と名前付きの並びの別名は、生成したコードのどこにも書かれないので比べない。予約語の名前に rulec が付ける `_`(§15.210)は rulec の識別子なので比べない。生成先の綴り(camelCase、大文字と小文字、PostgreSQL が残す 63 バイト)でだけ重なる名前は、この節に入れない(§15.260 の見つけて直していないもの)。
4. **名前と別名の重なり(`a(x)` と `x(y)`)は止めない。** 値では、規則が名前で読み、生成したコードが別名で読むので、読み方は一つに決まる(見回しで、生成したコードが全部の言語で同じ答えを返した)。`enums` のファイルの列挙だけは、`import enum` が名前か別名で探すので、列挙の名前がほかの列挙の別名と同じなら E034 にする。その列挙に別名が無ければ、名前がそのまま識別子なので、3 の識別子の重なりとして一度だけ言う。
5. ★ **見出しは、一つの名前を一列だけにする。** 表と例の見出しは E034、手順の例は E055、名前付きの並びは E026 にした。手順の例と並びは、その形の誤りを言うコードにそろえた(`chances` の見出しは前から E070、節の `when` は E046)。構文解析が見出しを読んだところで言う。
6. ★ **列挙の値と同じ名前のグループは E034 にする。** セルはグループも値も同じ語で書くので、二通りに読める。どの列挙もそろう型の検査で言う(`import enum` で読んだ列挙と、組み込みの区分の値とも比べる)。その値はどの行にも無い、とは言わない(W111)。セルの語が、その値のつもりだったかもしれないからである。
7. ★ **寄与が足す入力が、足す先の規則のほかの値と同じ名前なら E034 にする**(`link::merge`)。同じ名前の入力が同じ入力であることは変えない。
8. **畳み込みの答えの名前は E021、準用の出力に付けた名前は E041 のままにする。** どちらも、その行のほかの誤りと一緒に、行の形として重なりを前から言っていた。E021 は、答えがたどる並びと同じ名前のときも言い、一度目に印を付け、「決めるところが二つあります」で同じ重なりを二度言わないようにした。E041 は、たどる並び、要素のフィールド、開いた値とも比べ、一度目に印を付け、英語の文の二つの名前の順を直した(付けた名前と元の規則の出力を、逆に言っていた)。一つの出力に名前を二度付けたときも E041 で止める。入力の二度の読み替えを E041 にしているのと同じ形である。
9. ★ **台帳の E034 の見出しを「同じ名前が二度あります(A name appears twice)」にした。** 前の「行ラベルが二度あります」は、E034 の一つの形しか言っていなかった。コードは変えない。最小の再現は入力の二度の宣言にし、列挙の値、同じ識別子の二つの名前、表の見出しの列、行ラベルの四つを、別の形として並べた。
**捨てたもの**:
- **新しいコードを足すこと。** E034 は、前から「一つのものを指す名前はどれも同じ」と言い、行ラベル、表と節、出典、たどる並びとフィールド、前提のつながり、`import enum` の重なりを言っていた。同じ誤りの残りの並びも、E034 にそろえた。
- **型の検査の中で言い、その先の検査を続けること。** あとの宣言で読んだ型の誤り(「この列は bool です」)、置き換えられた列挙の値の E012、グループの E105 と E101 が、E034 の横に並ぶ。どれも書いた人の誤りではない。
- **一度目を読み、二度目を警告にすること。** 書いた人が `<=9` と `<=99` のどちらのつもりだったかは、規則からは分からない。
- **同じ型と範囲の二つの宣言を、黙って一つにすること。** 二行あれば、いつか片方だけが直される。
- **値の名前と別名の重なり(`a(x)` と `x(y)`)も止めること。** 読み方は一つで、生成したコードも組める。止めれば、通っていた規則が理由なく通らなくなる。
- **畳み込みの答えと準用の出力の名前も E034 にすること。** E021 と E041 は、その行のほかの誤りと一緒に、行の形として言っている。
- **見出しの二度書きを全部 E034 にすること。** 手順の例と名前付きの並びには、その形の誤りを言うコード(E055、E026)があり、`chances` の見出しの二度書きは前から E070 である。
**変わること**:
- 同じ名前を二度宣言したファイル、二つの名前に一つの識別子を書いたファイル、見出しに同じ列を二度書いたファイルは、`check` が E034(手順の例は E055、名前付きの並びは E026、畳み込みの答えは E021、準用の出力の名前は E041)で止まる。前は通るか、別の誤りで止まった。`schema`・`api`・`gen` も、そのファイルからは何も書かない。
- 前からある E034 は、ほかの検査より先に言い、そこで止まる。表と節、出典、前提のつながりの文は変わらず、前提のつながりは一度目にも印が付く。たどる並びとフィールドの重なりは、値の並びの文(印に宣言の種類が付く)になる。
- E021 は、答えが上の値と同じ名前のとき、「すでに…です」の一つだけを言い、一度目に印が付く。E041 は、一度目に印が付き、英語の文の名前の順が正しくなる。一つの出力に名前を二度付けた準用は E041 で止まる(前は、二つ目の名前を読むところが E012 で止まった)。
- 台帳の E034 の見出しと「いつ出るか」と「直し方」と再現、E021・E026・E041・E055 の「いつ出るか」、reference(名前と別名の節、表の別名、畳み込みの答え、準用の出力)、サイトの案内(英日)、スキルの reference のコピーを合わせた。reference とサイトの案内の「表の別名は受け付けるだけで使われない」は、§15.260 からは事実でない(SQL の問い合わせの行の番号の列と、Rust の証明の名前になる)ので直した。変異と golden とモジュールが増えたので、README のツリーの数と、サイトの「確かめ方」と「何を確かめるか」のページ(英日)の変異の数と、確かめ方の図も合わせた。
- 木の規則の `check` の答えは変わらない(下の突き合わせ)。
**確かめたこと**:
- `tests/names.rs`(新しい、七つ)と、材料の `tests/names/`(英語を先に、日本語の版を横に)。
- 止まる形が 29 組ある。値(入力の二度の宣言、型の違う二つの入力、入力と同じ名前の定義、定義と同じ名前を決める表、入力と同じ名前の出力、二つの出力、二つの集計、定義と同じ名前の開いた値)、識別子(同じ別名の二つの入力、ほかの名前と同じ別名、一つの名前の二つの別名)、ほかの並び(列挙の値、二つの列挙、二つのグループ、グループの値、値と同じ名前のグループ、二つの `shape`、二つの準用、同じ別名の二つの表)、`enums` のファイル(ほかの列挙の別名と同じ名前、同じ別名の二つの前提のつながり)、見出し(表の入力の列、表の出力の列、例、手順の例、名前付きの並び)、ほかのコード(たどる並びと同じ名前の答え、要素のフィールドと同じ名前の準用の出力、同じ出力に二度名前を付けた準用)である。どれも決めたコードを一つだけ言い、英日の文は `tests/golden/names/` の golden と同じで、二つ目の行で言い、一つ目に印がある。`schema`・`api`・`gen` は何も書かずに止まる。
- 通る形が 3 組ある(別名を持つほかの名前と同じ別名、一つの出力を決める二つの表、準用される規則)。どれも `check` を通り、生成したコードは全部の言語で参照の評価器と同じ答えを返す。
- 寄与(英日)。足す先の規則の表の出力と同じ名前の入力は、規則と寄与を一緒に検査しても、寄与だけを検査しても、E034 を一つ言う。
- `enums` のファイルで名前と別名が重なると、それを `import enum` で読む規則は E066 で止まる。
- 木の全部の規則で、ここの材料と E034 の変異のほかに、名前を二度書いたものは無い。
- 変異 `m_e034input`・`m_e034value`・`m_e034alias` と、それぞれの日本語の版(英語のゆうパックの規則と、その日本語の版から作る)。どれも E034 を一つだけ出し、英日の文は golden と同じである。
- 台帳。E034 の最小の再現と四つの別の形が、英日のどちらでも E034 を出し、それぞれの見出しの文を言う。
- 替える前と後の突き合わせ:木の全部の `.rule`(追跡している 717 本と、足した 70 本)の `rulec check`(英日と JSON の英日、終了コードつき)の 3,148 の出力で、変わったのは、足した 64 本(変異 6 本と、止まる材料 58 本)の 256 だけだった。追跡していた 717 本と、通る材料 6 本の出力は一字も変わらない。足した 64 本は、替える前は 44 本が `ok`、2 本が W111 だけで通り、残りの 18 本は別の誤り(E012、E029、E103、E105 と E101、E106、E107、書いていない名前の E034)で止まった。
**見つけて直していないもの**:
- **セルの集合に同じ値を二度書くと(`| p, p |`)、Go の生成コードを `go vet` が「redundant or」で止める**(`rulec test` の Go の丸めの補助の試験が落ちる)。宣言ではなく、意味も一つに決まるので、この節には入れていない。直すなら、Go の生成器が同じ値を一度だけ比べるか、セルを読むところで止めることになる。
- **`open` の下に同じ項を二度書くと、`combine sum` では二度足される。** 参照の評価器も生成したコードも同じに足すので答えは食い違わないが、書いた人の意図とは違いうる。名前の宣言ではないので入れていない。
- **`final` や `never` の行に同じ状態を二度書いても止まらない。** 集合として読まれ、答えは変わらない。
- **E034 の JSON の位置(`line` と `column`)は、一つ目の印、つまり一度目の宣言の行になる。** 文の `-->` は二度目の行を言う。前からの E034 と同じで、JSON の位置を最初の印から取る決まり(E105 なども同じ)の中での食い違いである。
### 15.263 書いた名前どうしが生成先の綴りで一つの識別子になるとき、`check` が言う(2026-10-23)
**きっかけ**:§15.260 の見つけて直していないもの。書いた人の名前どうしが、ある生成先で同じ識別子になると、生成したコードが組めないか答えを違えるのに、`check` は何も言わなかった。二つの名前に同じ別名を書いたときは、§15.262 で E034 にした。残っていたのは、識別子は違うのに生成先の綴りでだけ重なる名前と、生成先が使えない名前である。`define base_fee` と `define baseFee` は、Swift と Java がどちらも `baseFee` と書いて組めなかった。`afee` と `aFee` は、SQLite が一つの列として読み、問い合わせが片方の値で答えた。先頭の 63 バイトが同じ二つの長い名前は、PostgreSQL が「column reference is ambiguous」で止まった。出力 `READ_CREDIT` は、Ruby が定数として読み(「dynamic constant assignment」)、生成した Ruby のファイルが読み込めなかった。
**見回し**:生成先ごとに、書いた名前の種類ごとの識別子を表にした。名前の種類ごとに綴りを変えた名前を置いた規則を生成して読み、重なる組を一つずつ規則にして、替える前のバイナリの `rulec test` で確かめた。Python・TypeScript・JavaScript・Rust・Wasm・Ruby・NumPy は値の名前をそのまま書くので、そこで重なるのは書いたとおりに同じ識別子(§15.262 の E034)だけである。
| 名前の種類 | 生成先と綴り | 重なる組と、壊れる生成先 |
|---|---|---|
| 値(入力、並び、出力、導出、定義、まとめ、畳み込みの答え、表と節の出力、開いた値) | 一つの関数の引数と変数。Swift と Java は lowerCamelCase(Java は予約語に `_`)、PHP は `$` と名前(`$this` とスーパーグローバルと同じ名前には `_`)、SQL の問い合わせと関数は引用符で囲んだ列(SQLite は大文字と小文字を区別せず、PostgreSQL は先頭の 63 バイトだけを残す)。Go は入力と並びを `in.<PascalCase>` で読み、ほかの値は名前のまま | `base_fee` と `baseFee`(Swift・Java)、`record` と `record_`(Java)、`_GET` と `_GET_`(PHP)、`afee` と `aFee`(SQLite)、先頭の 63 バイトが同じ二つ(PostgreSQL) |
| 入力と並び | Go の `Input` のフィールド(PascalCase)、`.proto` の `DecideRequest` のフィールド(JSON の名前は、`_` を除いて次の字を大文字にしたもの) | `fee` と `fee_`、入力 `lines_` と並び `lines`(Go・Connect) |
| 出力 | Go の `Output` のフィールド(PascalCase、出力が二つ以上のとき)、`.proto` の `DecideResponse` のフィールド、Python の `NamedTuple` の `Output` のフィールド(出力が二つ以上のとき) | `list` と `list_`(Go・Connect)、`_` で始まる出力(Python) |
| 要素のフィールド | Go の構造体のフィールド(PascalCase)、Swift と Java の型のフィールド(lowerCamelCase)、PHP のクラスのプロパティ(変数と同じ綴り)、`.proto` のメッセージのフィールド、Python の `NamedTuple` のフィールド。Ruby は `Struct` のメンバで、大文字で始まっても読める | `unit_fee` と `unitFee`(Go・Swift・Java・Connect)、`_` で始まるフィールド(Python) |
| 列挙の型 | どの言語でも PascalCase(SQL と NumPy は型を書かない)。PHP はクラスと列挙の名前の大文字と小文字を区別せず、Java は列挙を規則のクラスの中に置いて `<クラス>$<型>.class` に書く | `tier_level` と `tierLevel`(型を書く全部)、`Tierlevel` と `TierLevel`(PHP、大文字と小文字を区別しないファイルシステムの Java) |
| 列挙の値 | PascalCase から作る。Rust と Wasm はそのまま、Go は型の名前の後ろにつなぐ。Python・TypeScript・JavaScript・Ruby・PHP・Java と `.proto` は大文字、Swift は lowerCamelCase | `basic` と `Basic`(値を名前で書く全部)、`gold_plus` と `goldplus`(大文字で書く七つ)、`LOW` と `low`(大文字の七つと Swift)、列挙 `plan` の `gold_plus` と列挙 `plan_gold` の `plus`(Go) |
| グループ | Go は `is<PascalCase>`、Swift は `_<lowerCamelCase>`、PHP は `GROUP_<名前>`(予約語に `_`)、ほかは名前のまま | `far_zone` と `farZone`(Go・Swift) |
| 表と節 | SQL の問い合わせと関数の列 `<識別子>_row`。値の列と並ぶ | `by_qty` と `By_qty`、表 `fee_by_qty` と値 `fee_by_qty_ROW`(SQLite)、行の番号の列が 63 バイトより長い表(PostgreSQL) |
| 前提のつながり | 関数 `<snake_case>_requires` と `_available`。TypeScript・JavaScript・Swift・Java は lowerCamelCase、Go は PascalCase | `core_path` と `corePath`(関数を書く全部)、`core__path` と `corePath`(TypeScript・JavaScript・Go・Swift・Java) |
| 並びが二つ以上の規則の要素の型 | `<PascalCase>Element` | `order_lines` と `orderLines`(型を書く全部。TypeScript は二つを一つの型にまとめる) |
| `shape` | 呼ぶ側のオブジェクトから入力を読む関数(`<規則>_from`)の引数。契約から読まない入力と並びと並ぶ。この関数を書くのは Python・TypeScript・JavaScript・Ruby・PHP | `shape rush` と入力 `rush`(この関数を書く全部)、大文字で始まる `shape`(Ruby) |
| 名前の頭と長さ | Ruby は大文字で始まる名前を定数として読み、引数と変数にできない。Python の `NamedTuple` は `_` で始まるフィールドを受け付けない。PostgreSQL は、問い合わせと関数が返す列の名前も先頭の 63 バイトに切る | `READ_CREDIT`(Ruby)、63 バイトより長い出力(PostgreSQL) |
見回しで、生成器の食い違いが二つ見つかった。PHP の `Output` と要素のクラスは、プロパティを変数の綴り(`$list`)で宣言し、関数の綴り(`->list_`)で読んでいた。出力が二つ以上の規則で、出力か要素のフィールドが PHP の予約語と同じ名前だと(`list`、`float` など)、PHP のコードは値を読めずに答えを違えた。木の `tests/builtins/group_names.rule` と日本語の版(出力 `float`)もそうで、どの試験も PHP では走らせていなかった。もう一つは、呼ぶ側のオブジェクトから入力を読む関数で、Python・TypeScript・JavaScript・Ruby の `shape` の引数が、値の名前から別名を引いていた(`Gen::ident`)ことである。`shape` の名前が値の名前と同じで、その値に別名があると、引数がその別名になった。`shape rush` と入力 `rush(r)` なら、二つの引数がどちらも `r` になった。
**原因**:§15.260 は、rulec が作る識別子を、生成先の綴りごとにほかの識別子と比べて離した(`value_spellings`)。書いた人の識別子は書いたとおりに残し、§15.262 は、それどうしを書いたとおりに比べた(同じ別名は E034)。生成先の綴りで比べるところは、書いた人の識別子どうしには無かった。生成器はそれぞれ名前を自分の言語の綴りで書くだけで、二つが一つになっても気づかない。言語が受け付けない形の識別子(Ruby の大文字、Python の `NamedTuple` の `_`、PostgreSQL の 63 バイト)を確かめるところも無かった。W121 は、予約語とすでにある名前だけを見ていた。
**決定**:
1. **書いた人の識別子どうしを、§15.260 と同じく、生成先の綴りごとに比べる**(`codegen::spellings`)。比べるのは、生成したコードで同じ名前空間に入るものどうしで、どの綴りで比べるかは名前の種類で決まる(上の表)。書いた人の識別子は、別名、無ければ名前である。rulec が作る識別子(予約語の `_`、準用と寄与が持ち込む名前)は、§15.260 がほかの全部から離しているので比べない。`enums` のファイルから読む列挙の値とグループは、そのファイルの検査が言う。
2. ★ **ある生成先でだけ一つになる二つの名前は W121 にする。** 見出しは、二つの名前と、一つになる生成先を言う(「`base_fee` と `baseFee` は、Swift・Java の生成コードで同じ識別子になります」)。注は、生成先ごとに、何という識別子になり、なぜそうなるか(「値の名前を lowerCamelCase で書くので、どちらも `baseFee` です」)と、どう壊れるかを言う。最後の注は、使わない生成先ならそのままでよいことと、どちらかに別名を付ける直し方である。名前は付け替えない。§15.260 の捨てたものと同じ理由である。
3. ★ **生成先が使えない識別子も W121 にする。** 大文字で始まる値と `shape`(Ruby が定数として読む。要素のフィールドは `Struct` のメンバなので、大文字で始まっても読める)、`_` で始まる出力(出力が二つ以上のとき)と要素のフィールド(Python の `NamedTuple`)、63 バイトより長い出力と、行の番号の列が 63 バイトより長い表(PostgreSQL が切り、問い合わせと関数が返す列の名前が変わる)である。入力が 63 バイトより長くても、PostgreSQL は書いた名前も読む名前も同じに切るので、壊れない。
4. ★ **どの生成先でも一つになる二つの名前は E034 にする**(`names::check`。§15.262 の識別子の確かめの続き)。どの言語も一つの綴りから識別子を作る種類で、その綴りが同じときである。二つの列挙の型と一つの列挙の二つの値(PascalCase)、二つの前提のつながりの関数(snake_case)、並びが二つ以上の規則の要素の型(PascalCase と `Element`)が当たる。型と値を書かない SQL と NumPy では組めるが、§15.262 で同じ別名の二つの列挙を E034 にしたのと同じく、生成したコードの識別子が一つなので止める。寄与が `extend` で足す値が、列挙のほかの値と同じ名前になるとき(`link::merge`)と、この規則の列挙の型が `import enum` で読む列挙の型と同じ名前になるとき(`enums::read_imports`。Java だけは読んだ列挙をそのファイルのクラスの名前で書くので組める)も E034 にする。
5. ★ **`shape` と、契約から読まない入力や並びが同じ識別子なら E034 にする。** どちらも `<規則>_from` の引数になり、この関数を書く五つの言語で組めない。§15.262 は、`shape` と値を別の並びとして比べていなかった。
6. **Go のパッケージで、列挙の値の定数がほかの列挙の値や型、値を読む関数(`Parse<型>`)と同じ名前になるなら、W121(Go)にする。** 定数の名前は型の名前と値の名前をつないだもので(`PlanGoldPlus`)、別の列挙の組からも同じ名前ができる。
7. ★ **生成器の二つの食い違いを直す。** PHP のプロパティは、宣言と同じ綴りで読む(`php::php_prop`)。PHP は予約語をプロパティの名前に使えるので、予約語にも `_` を付けず、`$this` とスーパーグローバルにだけ付ける。`shape` の引数は、`shape` の識別子のまま書く(値の名前から別名を引かない)。どちらも、ほかの綴りの比べ方が前から前提にしていた形である。
8. ★ **`gen` は、W121 があっても全部の生成先を書く**(これまでの W121 と同じ)。使わない生成先なら、そのままで構わないからである。E034 のときは、§15.262 と同じく何も書かない。
9. ★ **台帳の W121 の見出しを「生成先の言語で、識別子がぶつかるか使えません(An identifier collides, or cannot be used, in a target language)」にした。** 前の「別名が生成先の言葉とぶつかります」は、別名の形しか言っていなかった。コードは変えない。
**捨てたもの**:
- **ぶつかる生成先でだけ、書いた名前を付け替えること。** §15.210 と §15.260 と同じ理由で捨てた。一つのアプリが二つの言語で同じ規則を呼ぶとき、言語ごとに名前が違うと読みにくく、`rulec api` が一つの名前で言えなくなる。
- **どの言語でも、ぶつかる名前に番号を付けること。** 書いた人が選んだ識別子を、rulec が変えることになる。
- **全部 E034 にすること。** 使わない生成先のために、通っていた規則が止まる。W121 の「使わない生成先なら、このままで構いません」と同じ考えである。
- **全部 W121 にすること。** 列挙の型のように、型を書く全部の言語で組めないものまで、警告にとどめることになる。§15.262 は同じ別名の二つの列挙を止めている。
- **Ruby と Python と PostgreSQL の制限を、E011 のように誤りにすること。** 使わない生成先のために止めることになる。
- **PHP のプロパティを関数の綴り(予約語に `_`)で宣言すること。** 読む側に合わせる直し方もあったが、予約語のプロパティを避ける理由が PHP には無く、`rulec api` が言う名前とも離れる。
**変わること**:
- `check` が、新しい W121 と E034 を言う。木の 789 本の規則では、どちらも出ない(下の突き合わせ)。
- W121 は警告なので、`check` の終了コードは変わらない。警告も止める使い方(`--format json` の `severity` を見る CI など)では、新しい W121 で止まる規則がありうる。
- E034 の新しい形は、`check` と、`schema`・`api`・`gen` を止める。前は通り、生成したコードが組めなかった。
- 出力が二つ以上の規則で、PHP の予約語と同じ名前の出力(`list`、`float` など)と要素のフィールドを、PHP のコードが読めるようになる。木で生成したものが変わるのは、`tests/builtins/group_names.rule` と日本語の版の PHP の一行だけである(`$out->float_` が `$out->float`)。`shape` の名前が値の名前と同じ規則は木に無く、生成したものは変わらない。
- 台帳の W121 の見出しと「いつ出るか」と直し方、別の形の再現四つ(Swift と Java、SQLite、PostgreSQL、Ruby)、E034 の「いつ出るか」と別の形の再現一つ(型の名前が一つになる二つの列挙)、reference の名前と別名の節、サイトのツアーのページ(英日)、スキルの reference のコピー。変異が 10 本増えたので、README のツリーの数、サイトの「確かめ方」と「何を確かめるか」のページ(英日)の数と、確かめ方の図も合わせた。
**確かめたこと**:
- `tests/spellings.rs`(新しい、七つ)と、材料の `tests/spellings/`(24 の形、英語を先に、日本語の版を横に)。
- どの形も W121 を一つだけ言い、英日の文は `tests/golden/spellings/` の golden と同じで、英語の見出しが決めた生成先を挙げる。
- 生成したコードは、W121 が挙げない全部の生成先で、参照の評価器と同じ答えを返す。`.proto` は一つのディレクトリの規則が一つの buf のモジュールになるので、Connect を挙げる規則があるディレクトリでは、Connect の答えを数えない。
- 二つ目の名前(使えない名前なら、その名前)に別名を付けると、`check` は何も言わず、全部の生成先の 36 の走らせ方で参照の評価器と同じ答えを返す。`tests/names` の E034 の新しい形五つも同じで、PHP の予約語の出力 `list` と、入力と同じ名前の `shape` は、生成器の二つの直しが無いと、別名を付けても PHP と、Python・TypeScript・JavaScript・Ruby で答えなかった。
- `import enum` で読む列挙と、寄与が足す値は、英日で E034 を言い、別名を付けると通る。どちらも、別名を付けた規則の生成したコードは全部の言語で同じ答えを返した(手で一度走らせた)。
- 木の全部の規則で、材料と変異のほかに、この節の W121 は出ない。
- `tests/names.rs` の止まる形に五組(英日)を足した。どれも E034 を一つだけ言い、英日の文は golden と同じで、`schema`・`api`・`gen` は何も書かない。
- 替える前のバイナリで生成した `tests/spellings` の 48 本は、W121 が挙げる生成先でだけ組めないか答えを違え、挙げない生成先では答えが一致した(このマシンは macOS で、大文字と小文字だけ違う列挙の型は、Java でも落ちた)。ただし `outputs_with_a_trailing_underscore.rule` と日本語の版は、PHP でも落ちた。上の生成器の食い違いのためで、替えた後は PHP の答えが一致する。`tests/names` の E034 の 10 本は、型や値を書く全部の言語で落ちた(前提のつながりは、関数を書く九つの言語。Python と Ruby は答えを違え、ほかは組めなかった)。
- 変異 `m_w121camel`・`m_w121case`・`m_w121long`・`m_w121ruby`・`m_e034member` と、それぞれの日本語の版(`single_coupon.rule` と `クーポン一枚.rule` から作る)。どれも決めたコードを一つだけ出す。替える前のバイナリの `rulec test` では、`m_w121camel` は Swift と Java、`m_w121case` は SQL(SQLite)、`m_w121long` は SQL(PostgreSQL)、`m_w121ruby` は Ruby でだけ落ち、`m_e034member` は型や値を書く全部の言語で落ちた。注のとおりに別名を付けた 10 本は、替えた後のバイナリで `check` が何も言わず、12 の言語の `rulec test` が全部通った(SQL は PostgreSQL の関数も)。
- 台帳。W121 の別の形の再現四つと E034 の別の形の再現一つが、英日のどちらでもそのコードを出し、それぞれの見出しの文を言う。
- 替える前と後の突き合わせ:木の 789 本の `.rule` の `rulec check --format json`(英日)の 1,578 の出力は、一字も変わらない。同じ 789 本の `rulec gen` では、生成できた 450 本のうち、生成したものが変わったのは上の 2 本の PHP の一行だけである。
**見つけて直していないもの**:
- **`enums` のファイルから読む列挙の値とグループの、綴りでだけ重なる形は、読む規則の `check` では言わない。** そのファイルの `check` が言うからで、読む規則が言うと、同じものを読む規則の数だけ言うことになる(§15.265)。
### 15.264 式の真偽の値を、言語ごとの語で書く。名前の中の `True` と `False` を書き換えない(2026-10-23)
**きっかけ**:§15.263 の見つけて直していないもの。名前に `True` か `False` を含む値を、いくつかの生成器が式の中で書き換えた。`define isTrueFee` は、宣言では `isTrueFee`、表のセルが読むところでは `istrueFee` と書かれ、TypeScript・JavaScript・Rust・Wasm・Ruby・PHP・Swift・Java で組めないか答えを違えた。式の中で読むと Go も落ちた。予約語なので rulec が `_` を付ける `define True` も、`True_` が `true_` になり、TypeScript・JavaScript・Rust・Wasm・Ruby・PHP で落ちた。`check` は何も言わなかった。
**原因**:式は `Gen::expr` が一つの文法で文字列に組み、真偽の値を Python の `True` と `False` で書く。ほかの言語は、その文字列を自分の綴りに直す関数(`go_expr`、`ts_expr`、`rs_expr`、`rb_expr`、`sw_expr`、`java_expr`、`php_expr`)で、文字列の中の `True` と `False` を全部 `true` と `false` に置き換えていた。識別子は、`Gen::expr` が組むときに、言語ごとの綴りで文字列に入っている。置き換えは識別子の中の `True` と `False` にも及んだ。表のセルが名前を書くところも、同じ関数を通る。SQL は文字列を字句に分けて読むので、識別子の中は書き換えなかったが、識別子がちょうど `True` なら真の値 `TRUE` として読んだ。
**決定**:
1. **式の真偽の値は、識別子に現れない印で書く**(`codegen::TRUE_MARK` と `FALSE_MARK`。制御文字 U+001D で囲んだ語)。言語ごとの関数が文字列を自分の綴りに直すときに、印をその言語の語にする(`codegen::truths`)。Python は `True` と `False`、SQL は `TRUE` と `FALSE`、ほかの言語は `true` と `false` である。識別子の中の字は、どの言語でも書き換えない。
2. 真偽の値が式に入るのは、準用の引数に `true` か `false` を書いたときである(`特例 = false`)。表のセルや畳み込みの真偽の値は、言語ごとの生成器が自分の語で書いていて、この節では変わらない。
**捨てたもの**:
- **置き換えを字句の単位にすること**(前後が識別子の字でない `True` だけを置き換える)。識別子の中の `True` は守れるが、識別子がちょうど `True` か `False` のとき(別名 `three(True)`)は、真偽の値と区別できない。TypeScript・JavaScript・Rust・Wasm と Go の局所変数では、その値を読むところが真偽の値になる。
- **式を言語ごとに組むこと。** 式の算術(尺度のそろえ方、丸め、割り算)を一か所で組み、全部の言語に同じものを渡すために、`Gen::expr` は一つにしてある。言語で違うのは真偽の値の語だけなので、そこだけを印にした。
**変わること**:
- 名前に `True` か `False` を含む値の生成したコードが、どの言語でも組めて、参照の評価器と同じ答えを返す。
- `define True`(rulec は `True_` と書く)は、Ruby のほかのどの言語でも組める。Ruby は `True_` を定数として読み、`check` が W121 でそう言う(§15.265)。
- 別名がちょうど `True` の値を、SQL の問い合わせが、真の値 `TRUE` ではなく列 `"True"` として書く。PostgreSQL はその列を読み、SQLite は読めない(見つけて直していないもの)。
- 木の規則の生成したものは変わらない(下の突き合わせ)。
**確かめたこと**:
- `tests/truths.rs`(新しい、三つ)と、材料の `tests/truths/`(英語を先に、日本語の版を横に)。名前に `True` と `False` を含む三つの定義(`isTrueFee`、`hasFalseStart`、`feeIfTrue`)と、真偽の値をそのまま書く二つの準用(`special = true` と `special = false`)を持つ規則と、それが準用する規則である。
- どの言語の生成したファイルにも、三つの名前がそのまま書かれ、真偽の値の印は残らない。
- 生成したコードは、どの言語でも参照の評価器と同じ答えを返す。替える前のバイナリでは、英日どちらの版も、TypeScript・JavaScript・Rust・Wasm・Ruby・PHP・Go・Swift・Java の九つの言語の 15 の走らせ方が、組めないか答えを違えた。
- 再現の二つ(表のセルが `isTrueFee` を読む規則と、`define True` の規則)。前者は、替える前は八つの言語(TypeScript・JavaScript・Rust・Wasm・Ruby・PHP・Swift・Java)で落ち、替えた後はどの言語でも答えが一致する。後者は、替える前は六つの言語(TypeScript・JavaScript・Rust・Wasm・Ruby・PHP)で落ち、替えた後は Ruby でだけ落ちる。
- 替える前と後の突き合わせ:木の 857 本の `.rule`(追跡している全部)の `rulec check --format json`(英日、終了コードつき)は一字も変わらない。同じ 857 本の `rulec gen` では、生成できた 506 本の生成物が一字も変わらない(351 本はどちらのバイナリでも生成しない)。
**見つけて直していないもの**:
- **SQLite は、問い合わせの式の別名 `AS "True"` を列の名前にしない。** `true` と `false` は、大文字と小文字を問わず、引用符で囲んでも、列の名前にならず(`column2` のような名前になる)、`"True"` と書いて読むと文字列になる。別名がちょうど `True` か `False` の値は、SQL(SQLite)で答えを違える。PostgreSQL は列として読む。`check` は、SQL(SQLite)が使えない名前として W121 で言う(§15.265)が、生成器は直していない。
### 15.265 書いた名前が、生成したコードが自分で使う名前と同じになるとき、`check` が言う(2026-10-23)
**きっかけ**:§15.263 の見つけて直していないもの。生成器は、規則の名前のほかに、自分の名前を書く。当てはまった行と答えと要素の型、誤りの型、単位の型、丸めや日付の補助関数、局所変数、`.proto` のメッセージとフィールド、列挙の値の `<型>_UNSPECIFIED` である。書いた名前がそれと同じになると、生成したコードが組めないか答えを違えるのに、`check` は何も言わなかった。出力 `trace` は、`.proto` の `DecideResponse` が当てはまった行を運ぶフィールド `trace` と同じ名前になり、buf が受け付けなかった。やりとりする列挙の値 `unspecified` は `<型>_UNSPECIFIED` と同じになり、出力が二つある規則の列挙 `output` は、十の生成先の型 `Output` と同じになった。ほかに二つあった。ASCII でなく別名も無い並びと要素のフィールドは、Go の構造体でも `.proto` でも書けないのに、E011 は規則と入力と出力だけを見ていた。`.proto` の列挙の値の名前が二つの列挙のあいだで同じになる形は、確かめていなかった。
**見回し**:木の規則の生成したもの(全部の生成先)から識別子を集め、書いた名前にもなりうる 1,092 個を選んだ。それを、名前の種類(定義、入力、出力、要素のフィールド、並び、列挙の型、列挙の値、グループ、表、規則の名前)ごとに、六つの規則(表、グループ、順序のある列挙、並びと畳み込み、日付、単位、乱数を分けて持つ)へ入れて生成し、替える前のバイナリの `rulec test` と `tsc --strict`、`mypy --strict` で確かめた(733 回、911 の名前)。生成先ごとに、生成したコードが自分で使う名前と、それに当たった書いた名前は次のとおりである(`codegen::own` が一覧を持つ)。
| 生成先 | 生成したコードが自分で使う名前 | 当たった書いた名前と、壊れ方 |
|---|---|---|
| Python | 型 `Fired`・`RuleInputError`・`RuleContradictionError`・`Output`(出力が二つ以上)・要素の型・`Chances`・単位の型。読み込む `annotations`・`NamedTuple`・`NewType`・`enum`・`Any`・`cast`。組み込みを使う名前 `_isinstance`・`_int`・`_float`・`_bool`・`_repr`・`_len` と、補助関数 `_json_str`・`_civil`・丸めの五つ・`_rank_<型>`・`_max_<型>`・`_min_<型>`・`<型>_rank`。ふつうの関数の局所変数 `out`。補助関数が名前で読む組み込みの `abs`・`ord`・`super`(呼ぶ側のオブジェクトから読む関数があれば `isinstance` や `int` も)。関数のどこかで代入した名前は、その関数の全体で局所変数になる | 値が型や補助関数を隠す(定義 `Fired`、入力 `_int`、断る呼び出しで読む `RuleInputError` と `_repr`)、入力 `out`(mypy)、`NamedTuple` のあとのフィールドの型と同じ名前の出力やフィールド(出力 `bool` のあとの真偽の出力。mypy)、列挙 `output`、規則 `annotations`(mypy)、規則 `ord`(`_json_str` が呼ぶ) |
| TypeScript・JavaScript | `RuleInputError`、`JSON`・`String`・`Object`・`Number`・`BigInt`・`Error`、丸めの `_roundDown` など、`_min`・`_max`、`_<グループ>` の定数、`_civil`、型 `Fired`・`Output`・要素の型(TypeScript)。値は関数の定数で、宣言より前では読めない | 値 `RuleInputError`(断る呼び出し)、値 `BigInt`(数を `number` で渡す呼び出し)、グループ `roundDown`(`_roundDown`)、列挙 `output`(TypeScript) |
| Rust・Wasm | `Ok`・`Err`・`Some`・`None`、単位の型(一つのフィールドの構造体で、パターンにもなる)、丸めの `round_down` など、`min_i64`・`max_i64`、`is_<グループ>`、`json_str`・`civil`、型 `Fired`・`RuleError`・`Output`。局所変数は、宣言したあとだけを隠す | 定義 `round_down`(出力の丸め)、値 `Ok`、列挙 `output` |
| Go | 引数 `in`、型 `Fired`・`Input`・`Output`・`RuleInputError`・`RuleContradictionError`、`append`・`panic`・`len`・`nil`・`int64`・`bool`・`string`・`strings`、丸めの `roundDown` など、`is<グループ>`。表の出力は分岐の前に型つきで宣言し(`var fee int64`)、答えを返すところで型に直す(`bool(ok)`)。断る呼び出しは、ループの中と、範囲を持つまとめのあとでも答えを返す。パッケージは規則の名前 | 値 `in`、まとめ `RuleInputError`(ループの中の確かめ)、出力 `bool`(あとの真偽の出力の宣言)、規則 `main`(パッケージ)、規則 `fired`(型 `Fired`)、列挙 `output` |
| Swift | 局所変数 `trace`・`head`・`ins`・`obs`・`rows`・`out`・`tag`、畳み込みの `answer`・`stopped`・`done`・`taken`・`kept`・`best` など(`Gen::fold_names`。腕によって)、`<まとめ>Set`、`_<グループ>`、丸めの `_roundDown` など、型 `Fired`・`RuleError`・`Output`。値は lowerCamelCase で、`Gen::temp` は局所変数を書いたとおりの名前からしか離さない。規則のファイルは `<規則>.swift` | 定義 `Trace`、定義 `top_price_set`(`topPriceSet`)、規則 `main`(`main.swift`)、列挙 `output` |
| Java | 局所変数(Swift と同じものと `elem`・`held`)、型 `Fired`・`Traced`・`RuleInputError`・`RuleContradictionError`・`Output`・`Walked`(規則のクラスの中)、走らせる側の `Str`・`Pairs`・`E` | 定義 `top_price_set`、規則 `fired`(クラス `Fired`)、列挙 `output` |
| Ruby・PHP | 型 `Fired`・`RuleInputError`・`RuleContradictionError`・`Output`。PHP は規則の名前空間の中で組み込みの関数を呼び(`abs`・`intdiv`・`ord`・`sprintf`・`strlen`・`implode` など)、同じ名前の関数があればそちらを呼ぶ | 列挙 `output`、規則 `ord`(PHP) |
| SQL | 列 `_id`・`_input_error`・`_raw_<出力>`・`_r<番号>`(丸めと割り算が、何度も読む値を置く列。数は rulec が書く問い合わせに聞く) | 定義 `_r1` |
| Connect(`.proto` とサービス) | メッセージ `Fired`・`DecideRequest`・`DecideResponse`・要素の型、サービス `<規則>Service`、`DecideResponse` のフィールド `trace`、列挙ごとの `<型>_UNSPECIFIED`。サービスのモジュールは `Code`・`ConnectError`・`Any`・`TypeVar` などを読み込み、規則のサービスをクラス `<規則の PascalCase>` と `<…>Sync` に書く。Python のモジュールを動かすので、その壊れ方も受け継ぐ | 出力 `trace`、列挙の値 `unspecified`、列挙 `decide_request`、規則 `code`(クラス `Code`)、`_` で始まる入力(下の生成器の食い違い) |
`_` だけの名前は、Go(空白の識別子)・Java(予約語)・Rust・Swift・Wasm(値を捨てるパターン)の変数にもフィールドにもならない。PascalCase にすると何も残らない列挙の型と値(`_` だけ)は、型を書く全部の生成先で書けない。
見回しで、生成器の食い違いが一つ見つかった。Connect のサービスと走らせる側は、`.proto` のフィールドを、protobuf の Python のプラグイン(protoc-gen-py)が stub に書く名前で読む。rulec はその名前を、Python の予約語に `_` を付けることでしか合わせていなかった。プラグインは、`_` か `pb_` で始まる名前の前に `pb_` を付け、メッセージが自分で使う名前(`has_field`・`to_json`・`desc`・`self` など)の後ろに `_` を付け、`ext_` で始まる名前と、`int_`・`list_` のように `_` で終わる名前も書き換える(`escape_message_attr`)。モジュールのパスの部分も、`_pb` で終わるものの後ろに `_` を付ける(`escape_proto_module_component`。規則 `int_pb` の stub は `rulec.int_pb_.v1.int_pb__pb`)。入力 `_rate`、出力 `list_`、規則 `order_pb` で、Connect の四つの走らせ方が落ちた。
**原因**:§15.103 と §15.239 は、書いた名前を、言語の予約語とすでにある名前、規則と列挙の名前に比べた。§15.263 は、書いた名前どうしを生成先の綴りで比べた。生成器が自分で付ける名前に比べるところは無かった。生成器は、局所変数を `Gen::temp` で書いた名前から離すが、書いたとおりの綴りでしか比べず、Swift と Java の lowerCamelCase で一つになる名前は離れなかった。E011 は、公開される名前を規則と入力と出力だけとしていた。
**決定**:
1. **書いた名前を、生成先ごとに、生成したコードが自分で使う名前に比べる**(`codegen::own` が名前を挙げ、`codegen::spellings::own_warnings` が比べる)。比べるのは、同じ名前空間に入るものどうしである。値は、規則の関数が読む名前(補助関数、型、組み込みの名前、引数、局所変数、問い合わせの列)に、その言語の綴りで比べる。Go と Rust は、局所変数が宣言より後だけを隠すので、関数を書く順(`Gen::walk_steps`)で、読むところが後にあるときだけ当たりとする。読むところがその規則に無い名前(丸めを使わない規則の丸めの補助関数、出力が一つの規則の `Output`)は比べない。列挙の型はモジュールが宣言する型に、やりとりする列挙は `.proto` のメッセージとサービスに、その値は `<型>_UNSPECIFIED` に、出力は `DecideResponse` の `trace` に、規則の名前は、その関数とクラスとパッケージが並ぶ名前(モジュールの型と読み込む名前、Go の型と go コマンドが取ってある名前、Swift の `main.swift`、Rust の補助関数、サービスのクラス、補助関数が名前で読む組み込み)に比べる。グループは、TypeScript と JavaScript の `_<グループ>` を補助関数に比べる。
2. ★ **生成したコードが自分で使う名前と同じになる名前は、W121 にする。** 見出しは、名前と、当たる生成先を言う(「`trace` は、Connect の生成コードが自分で使う名前と同じになります」)。注は、生成先ごとに、何に当たり、どう壊れるかを言い、最後の注は §15.263 と同じ直し方である。名前は付け替えず、生成器が付ける名前も規則ごとに変えない。
3. ★ **E034 にする形は無い。** 名前を書くどの生成先でも当たる名前が無いからである。`Output` は JavaScript が型を書かず、`Fired` は JavaScript に無く、`RuleInputError` は Rust・Swift・Wasm が `RuleError` と書き、`.proto` の名前は Connect だけのものである。§15.263 の分け方のとおり、どれかの生成先で組めるなら止めない。
4. ★ **言語の語は、§15.103 と §15.149 が別名に当てる一覧のまま、別名の無い規則の名前と列挙の名前にも当てる。** 別名が無ければ、名前そのものが識別子だからである。比べるのは、その言語が書く綴り(Go は PascalCase、Java はクラスの名前、Swift は lowerCamelCase)で、大文字と小文字を区別しないのは PHP だけである。§15.103 の一覧は言語が持つ名前で、生成したコードが読む名前ではないので、生成したコードが壊れなくても言うことがある(Python の規則 `len` は、モジュールが先に `_len` と読んでおくので組める)。別名と同じ一覧に当てるので、そのままにした。生成したコードが読む組み込み(Python の `abs`・`ord`、PHP の `ord`・`intdiv` など)は、一覧に無くても、1. のとおりに言う。Ruby は列挙のモジュールを規則のモジュールの中に置くので、列挙の名前は、言語のクラスの全部ではなく、そのモジュールのコードが読むクラス(`Integer`、`String`、`Struct` など)にだけ比べる(列挙 `signal` の `Signal` は組める)。
5. **Connect は、Python のモジュールの壊れ方を受け継ぐ。** サービスは Python のモジュールを動かすので、モジュールが読み込めない形(`_` で始まる `NamedTuple` のフィールド、大文字にすると重なる、やりとりしない列挙の値)と、走らせると答えを違える形には、Connect も挙げる。mypy だけが読む形(注釈、`annotations`)は、Python だけを挙げる。§15.263 の `_` で始まる出力とフィールドの W121 も、Python と Connect を挙げるようにした。
6. **`_` だけの名前は、使えない生成先を挙げて W121 にする。** 値は Go・Java・Rust・Swift・Wasm で、入力と並びは Python も(ふつうの関数が二つ目の答えを `_` に捨てて、引数を書き換える。mypy)、出力が二つ以上の規則の出力と要素のフィールドは Python と Connect も(`NamedTuple`)である。`_` だけの列挙の型と値は、型を書く全部の生成先である。§15.263 の `_` で始まる名前の W121 は、`_` だけの名前を言わず、こちらが一つにまとめて言う。
7. **予約語なので rulec が `_` を付ける大文字で始まる名前(`True_`、`None_`、`Self_`)は、Ruby が使えないと W121 で言う。** 要素のフィールドは `Struct` のメンバなので言わない。SQLite が列の名前にしない `true` と `false`(§15.264)も、値の識別子が大文字と小文字を問わずそれなら、SQL(SQLite)が使えないと言う。
8. ★ **ASCII でなく別名も無い並びと要素のフィールドは、E011 にする**(E011 を広げる)。並びは呼ぶ側が渡す入力で、要素のフィールドは呼ぶ側が埋めるので、入力と同じく公開される名前である。生成先ごとに確かめると、Go は構造体のフィールドとして公開できず(漢字とかなは大文字を持たない)、`.proto` はフィールドの名前に ASCII しか取らず、Connect の stub が作れない。どちらも入力が E011 で止まる理由と同じである。W121 にすると、同じ理由で壊れる入力は止まり、並びは通ることになる。木の規則に当たるものは無い(下の突き合わせ)。
9. ★ **ASCII でなく別名も無い列挙の型と値は、やりとりする列挙なら W121(Connect)にする。** `.proto` は名前に ASCII しか取らない。Go は列挙を規則のパッケージの中で書き、ほかの言語もその名前で組めるので、E011 のように止めない。
10. **`.proto` の列挙の値が二つの列挙のあいだで同じになる形は、確かめた。** 値の名前は `<型の UPPER_SNAKE>_<値の PascalCase を大文字にしたもの>` で、値の側に `_` は無い。だから二つの列挙の値が同じになるのは、二つの型の頭が同じときだけである。頭が同じ二つの列挙(`http_code` と `HTTPCode`)は、`<頭>_UNSPECIFIED` が二度になり、buf が受け付けない(替える前のバイナリで Connect の四つの走らせ方が落ちた)。§15.263 の、大文字と小文字だけ違う列挙の型の W121(PHP・Java)に、二つともやりとりする列挙で頭が同じなら、Connect も挙げる。
11. **`enums` のファイルから読む値とグループは、そのままにする。** そのファイルの `check` が言う。読む規則が言うと、同じものを読む規則の数だけ言うことになる。
12. ★ **生成器の食い違いを直す。** Connect のサービスと走らせる側は、フィールドとモジュールのパスを、protoc-gen-py が書くとおりに書く(`connect::pb_attr` と `pb_module`)。プラグインの付け方にそのまま合わせたので、書いた名前は変わらず、`.proto` も変わらない。
13. **何を読むかは、生成器に聞く。** 畳み込みの局所変数は `Gen::fold_names` と腕の種類から、まとめのフラグは `Gen::ident` から、問い合わせが値を置く列の数は rulec が書く問い合わせ(`Gen::sql_bound`)から取る。そのため、型の検査で止まる規則では、この節の比べ方をしない。生成したコードを書けないからで、止まる規則はどのみち生成しない。
**捨てたもの**:
- **生成器が付ける名前を、ぶつかる規則でだけ変えること。** 生成したコードの形が規則ごとに変わり、`Output` や `DecideResponse` の `trace` を読む呼ぶ側のコードが、規則によって読めなくなる。
- **書いた名前を付け替えること。** §15.210、§15.260、§15.263 と同じ理由である。
- **§15.103 の一覧を、生成したコードが読む名前だけに絞ること。** 別名と、別名の無い名前とで言うことが変わり、呼ぶ側のコードがモジュールを読む場面(標準のライブラリと同じ名前のモジュール)を落とす。
- **並びと要素のフィールドを W121 にすること。** 上の 8. のとおり、入力と同じ理由で壊れるものを、入力より軽く扱うことになる。
- **PHP の組み込みの関数の全部を一覧に入れること。** PHP は名前空間の中の関数に組み込みと同じ名前を許すので、壊れるのは生成したコードが呼ぶ関数だけである。
**変わること**:
- `check` が、新しい W121 を言う。木の 857 本の規則では、材料と変異のほかに出ない(下の突き合わせ)。
- `_` で始まる出力と要素のフィールドの W121(§15.263)が、Python だけでなく Connect も挙げる。見出しの文と注が変わる。
- 大文字と小文字だけ違う、やりとりする二つの列挙の型の W121(§15.263)が、二つの頭が UPPER_SNAKE で同じなら Connect も挙げる。
- E011 が、ASCII でなく別名も無い並びと要素のフィールドで止まる。前は通り、Go と Connect が組めなかった。木の規則で止まるものは無い。
- Connect のサービスと走らせる側が、`_` で始まる名前、`has_field` などメッセージが使う名前、`_pb` で終わる規則の名前でも動く。木で生成したものが変わるのは、§15.263 の材料のうち、`_` で始まる出力と要素のフィールド、`_` で終わる出力の 6 本(英日)の、Connect のサービスと走らせる側の行だけである。
- 台帳の W121 の「いつ出るか」と直し方、別の形の再現二つ(生成したコードが自分で使う名前、`_` だけの名前)、E011 の「いつ出るか」と別の形の再現一つ(別名の無い並び)、reference の名前と別名の節、サイトのツアーのページ(英日)、スキルの reference のコピー。変異が 6 本増えたので、README のツリーの数、サイトの「確かめ方」と「何を確かめるか」のページ(英日)の数と、確かめ方の図も合わせた。
**確かめたこと**:
- `tests/own_names.rs`(新しい、七つ)と、材料の `tests/own_names/`(16 の形と、E011 の二つの形と、プラグインが別に書く名前の一つ。英語を先に、日本語の版を横に)。
- どの形も、この節の W121 を一つだけ言い、英語の見出しが決めた生成先を挙げる。英日の文は `tests/golden/own_names/` の golden と同じである。`define True` は、§15.103 の `True_` と書くという W121 と並ぶ。
- E011 の形は、英日で E011 を一つだけ言い、注が並びとフィールドを挙げる。
- 生成したコードは、W121 が挙げない全部の生成先で、参照の評価器と同じ答えを返す。プラグインが別に書く名前の規則は、何も言われず、全部の生成先で答えが一致する。
- 注のとおりに別名を付けると、`check` は何も言わず、全部の生成先で参照の評価器と同じ答えを返す(E011 の形も)。
- mypy だけが読む二つの形(入力 `out`、出力 `bool`)は、`mypy --strict` が生成したモジュールを受け付けず、別名を付けると受け付ける。
- 木の全部の規則で、材料と変異のほかに、この節の W121 も、並びとフィールドの E011 も出ない。
- `codegen::own` の単体の試験。コーパスの規則(検査を通るもの全部)を全部の生成先で生成し、一覧が挙げる補助関数・型・定数・局所変数・列・引数・読み込む名前が、その生成先の書いたものにあることを確かめる。
- `tests/spellings.rs` に、頭が UPPER_SNAKE で同じ二つの列挙の形(英日)を足し、`_` で始まる出力とフィールドの二つの形が挙げる生成先を、Python と Connect にした。
- 替える前のバイナリで、材料の 16 の形(英語)は、W121 が挙げる生成先で組めないか答えを違えた(`tsc` と `mypy` を含む。規則 `code` の Python は、§15.149 の標準のライブラリのモジュールとして挙げ、走らせると答えは一致した。出力 `bool` の Go は、見回しのあとで足した形)。挙げない生成先では答えが一致した。E011 の形は Go と Connect、頭が同じ二つの列挙は PHP・Java・Connect、プラグインが別に書く名前は Connect で落ちた。`define True` は、§15.264 の直しの前は六つの言語で落ち、替えた後は Ruby でだけ落ちる。
- 見回しの 733 回を、替えた後のバイナリの `check` と突き合わせた。`check` が挙げずに落ちた生成先は 9 回で、どれも `_` で始まる入力と規則 `int_pb` の Connect(上の生成器の食い違い)だった。その形を替えた後のバイナリで走らせ直すと(46 回)、`check` が挙げずに落ちる生成先は無かった。挙げたのに落ちなかった生成先は 25 回で、18 回は §15.103 と §15.149 の一覧による規則の名前(4.。`len`・`input`・`json` など 16 の名前)、7 回は確かめに使った規則が通らない呼び方だった。後者は、数を `number` で渡す JavaScript の呼び出し(走らせる側は `bigint` で渡す。`number` で渡すと `ReferenceError` で落ちるのを、別に確かめた)、関数に届く前に断られる呼び出し、mypy を通さなかったサービスのモジュール、`tsc` が替える前から通らなかった規則である。
- 変異 `m_w121own`・`m_w121owntrace` と日本語の版(`single_coupon.rule` と `クーポン一枚.rule` から作る)、`m_e011seq`・`m_e011seqfield`(`買物かごの送料.rule` から作る)。どれも決めたコードを一つだけ出す。
- 台帳。W121 の別の形の再現二つと E011 の別の形の再現一つが、英日のどちらでもそのコードを出し、それぞれの見出しの文を言う。
- 替える前と後の突き合わせ:木の 857 本の `.rule`(追跡している全部)の `rulec check --format json`(英日、終了コードつき)は、§15.263 の材料の `_` で始まる出力とフィールドの 4 本(英日の 8 の出力)の見出しと注のほかは、一字も変わらない。同じ 857 本の `rulec gen` では、生成できた 506 本のうち、生成したものが変わったのは、§15.263 の材料の 6 本(`_` で始まる出力とフィールド、`_` で終わる出力の英日)の、Connect のサービスと走らせる側の一行ずつだけである(351 本はどちらのバイナリでも生成しない)。
**見つけて直していないもの**:
- **PHP の規則の関数は、生成したコードが呼ばない組み込みの関数と同じ名前でも組めるが、呼ぶ側のコードが同じ名前空間からその組み込みを呼ぶと、規則の関数を呼ぶ。** `check` は、生成したコードが呼ぶものだけを言う。
- **§15.103 の別名の W121 は、Python の予約語を Connect に挙げない。** 別名 `True` の値は Python のモジュールが組めず、Connect も落ちる。§15.103 の注は backend の一覧の名前を挙げるので、Connect の名前が無い。
- **NumPy は、定数だけで決める出力を組めない。** 見回しで見つけた、名前と関わらない誤りである(`define <出力> : number = 1` だけで決める規則の NumPy の走らせ方が落ちる)。
### 15.266 `import` は、生成先が書いたままでは使えない名前に、`check` が何も言わない綴りを付ける(2026-10-23)
**きっかけ**:§15.263 で、書いた名前どうしが生成先の綴りで一つになる形と、生成先が使えない名前を、W121 と E034 が言うようになった。すると CI で `tests/import.rs` の二つ(`a_draft_is_read_back_by_check_whatever_the_sheet_writes` と `a_rule_name_not_read_as_a_name_is_named_for_now`)が落ちた。`rulec import` は、字句が名前として読む見出しと値を、そのまま名前にする。英語のシートの見出しは大文字で始まることが多く(`Zone`、`Weight`、`Fee`)、下書きの `check` は「`Zone` は大文字で始まるので、Ruby の生成コードでは使えません」と W121 を言った。W121 の誤りではない。その下書きから生成した Ruby は `def self.fees(Zone, Weight)` になり、`ruby -c` が「formal argument cannot be a constant」で落ちる。§15.263 の前から、こういう下書きの Ruby は読み込めなかった。§15.259 の約束(シートが何を書いても、下書きは `check` を黙って通る)は、この形の名前で破れていた。
**見回し**:替える前のバイナリで、W121 と E034 の形ごとにシートを作って下書きを起こし、`check` の答えと、生成したコードを `rulec test` の全部の走らせ方(12 の言語。PostgreSQL の関数を含む)で走らせた答えを確かめた。
- W121 が出る形:大文字で始まる見出し(Ruby)。生成先の予約語と同じ見出し(`type`、`class`、`in`。生成コードが `_` を付けて書くという §15.260 の W121)。rulec の予約語の見出しで、別名にした綴りが生成先の予約語と同じもの(`import`、`not`、`default`、`range` など。下書きの `import_(import)` の別名に §15.103 の W121 が出る)。`_` で始まる出力(出力が二つ以上のときの Python の `NamedTuple`)。63 バイトより長い出力(PostgreSQL)。綴りで一つになる二つの見出し(`ship_zone` と `shipZone` は Go・Swift・Java・Connect、`Zone` と `zone` は Go・Swift・Java と SQLite)。どれも、W121 が挙げる生成先でだけ組めないか答えを違え、ほかの生成先は参照の評価器と同じ答えを返した。生成先の予約語と同じ見出しは、生成コードが `type_` と書くので組めた。`import` の別名は、Python・TypeScript・JavaScript で組めなかった。
- E034 で止まる形:同じ見出しの二つの列(同じ名前の二度の宣言)。一つの列で PascalCase が同じになる二つの値(`north` と `North`)。英語の下書きで列挙の型が同じになる二つの見出し(`Zone` と `zone` の列挙 `Zone_values` と `zone_values` は、どちらも `ZoneValues`)。§15.259 の仮の名前の離し方(`v2.0` の仮の名前 `v1_` と値の `v1` は、PascalCase でどちらも `V1`。仮の名前の列 `c2_` の列挙 `c2__values` と、列 `c2` の列挙 `c2_values` は、どちらも `C2Values`)。`gen` は何も書かなかった。`tests/import/` の `a_name_an_alias_would_take` と `別名と同じ名前` は §15.263 からこの E034 で止まっていたが、試験はその前の材料の W121 で落ちていて、見えていなかった。
- 規則の名前:`--name sum` の下書き `rule sum_(sum)` は、別名 `sum` が Python の組み込みの `sum` を隠すので W121。見出し `Zone` のシートを `--name Zone_values` で起こすと、規則と列挙 `Zone_values` が Go と Java で同じ型の名前になり、§15.239 の W121。
**原因**:import が名前を決めるときに見ていたのは、字句が名前として読むか(§15.259)と、rulec の予約語か(E009)だけだった。生成先の綴りで識別子を比べるのは `check` の W121 と E034(`codegen::spellings`、`names::check`、`types` の予約語の確かめ)で、import はそれに聞いていなかった。§15.259 の仮の名前を `_` で離す決め方も、書いたとおりの綴りでしか比べていなかった。
**決定**:
1. ★ **import は下書きを `check` にかけ、W121 か E034 が印を付けた名前の綴りを変えて、`check` がどちらも言わなくなるまで繰り返す**(`import::draft_rows` の `said` と `Names::respell`)。`check` は `rulec check` と同じもの(`crate::report`)にした。診断の印の位置(行と列)を、下書きを構文解析して求めた宣言の位置(規則の名前、入力と出力の行と表の見出しの出力、列挙の名前と値、表の名前)に対応づける。生成先ごとの決まりは import に書かない。W121 と E034 が広がれば(生成器が自分で付ける名前とのぶつかりなど)、import はそのまま従う。この口が `check` に求めるのは、W121 と E034 が、言う名前の宣言に印を付けることである。いまの W121 と E034 は、どれもそうしている。二つの名前の印の順(後のほうを後に付ける)は、次の決定の 2 がどちらを変えるかを決めるときに読む。
2. ★ **どの名前を変えるか**:一つの名前に付いた印(大文字、`_`、63 バイト、予約語)を先に、二つの名前の印を後に扱う。二つのうち、import が選んだ識別子を持つほう(仮の名前、別名、import が付ける列挙と表の名前)を変え、どちらもシートの語なら後のほうを変える。同じ回にどちらかの綴りを変えたら、その印はその回は見送り、次の `check` に任せる。こうして、シートの語はできるだけそのまま残る(`Zone` と `zone` では、大文字の `Zone` だけに別名が付く)。
3. ★ **別名の綴りは、次の順に試す**。シートの語なら、先頭の大文字の並びを小文字にしたもの(`Zone` → `zone`、`URLPath` → `urlPath`。W121 の注が勧める「小文字で始まる別名」で、Swift の生成器の `lower_lead` と同じ綴り)。その後ろに `_` を付けたもの(`type` → `type_`。生成先の予約語と同じ名前に生成コードが付けるのと同じ綴り)。種類の別名(列なら `c1`、`o1`、値なら `v1`、列挙なら `c1_kind`、規則なら `imported`、表なら `t`)。それに 2 からの番号を付けたもの(`c1_2`、`c1_3` …)。rulec の予約語の見出し(`import_(import)`)は、別名を外して名前そのもの(`import_`)、種類の別名の順にした。ASCII でない語(`重量(c2)`)と仮の名前(`c1`)は、種類の別名に番号を付ける。同じ種類のほかの名前か別名と、書いたとおりに同じ綴りは使わない。`a(x)` と `x(y)` は `check` が通しても、読む人がどちらの `x` か迷うからである。どの綴りを生成先が使えるかは、その綴りで書いた下書きの `check` が言う。
4. ★ **仮の名前は、`_` ではなく番号で離す**(§15.259 の決定の 3 を替えた)。`v1_` と `v1`、`c2_` と `c2` は、PascalCase と `.proto` の JSON の名前で一つになる。番号は、§15.260 が rulec の作る識別子に付けるのと同じ形にした(`v1_2`、`c2_2`)。
5. **同じ見出しの二つ目の列は、種類の別名を名前にする**(`c2`)。注には「見出しの「zone」はほかの列の見出しと同じなので、仮に c2 と名付けた」と書く。名前が同じ二つの宣言は E034 で、別名では離せないからである。最初の列は見出しのままにする。
6. **綴りを変えた名前には、ほかの推定と同じ形の注を書く**。列なら「見出しの「Zone」は、生成先の言語によってはそのままでは使えないか、ほかの名前と同じ識別子になるので、生成コードでは仮に zone と名付けた」(英語は "the code generated for some languages cannot take the header "Zone" as it is, or makes it one identifier with another name, so the generated code names the column zone for now")。値なら列挙の行に「…値には、生成コードでの仮の名前を付けた(North は v2)」。規則、列挙、表の名前も同じ形にした。どの生成先がなぜ使えないかは注に書かない。別名を外して `check` にかければ、W121 か E034 がそれを言う。
**捨てたもの**:
- **生成先ごとの決まりを import にも書くこと**(大文字で始めない、`_` で始めない、63 バイトに収める、PascalCase で比べる…)。W121 と E034 が広がるたびに二か所を直すことになり、片方だけが直れば、下書きがまた黙らなくなる。
- **W121 と E034 の比べ方を `codegen::spellings` から関数として出し、import が呼ぶこと。** 比べ方は名前の種類と生成先ごとに分かれていて(値、入力、出力、要素のフィールド、列挙の型と値、表の行の番号の列…)、どれを呼ぶかを import が決めること自体が、決まりをもう一つ持つことになる。予約語の確かめ(`types`)や規則と列挙のぶつかり(§15.239)のように、`spellings` の外で言う W121 もある。
- **シートの語を、名前ごと書き換えること**(`Zone` を `zone` に)。名前はシートの語のままにして(表の見出しとセルが読むのはそれ)、生成コードの識別子だけを別名で変える。§15.259 が、読めない語にだけ仮の名前を使ったのと同じ考えである。
- **全部の名前に別名を付けること。** 生成先が使える名前にまで別名が付き、下書きが読みにくくなる。
- **下書きでは W121 を言わないこと。** 下書きから生成したコードは、実際に組めない。
**変わること**:
- 生成先が書いたままでは使えないか、ほかの名前と同じ識別子になる名前のあるシートの下書きは、その名前に別名か仮の名前が付き、`check` が何も言わずに読み、生成したコードは全部の言語で組める。前は W121 か E034 が出た。英語のシートでは、大文字で始まる見出しの全部に別名が付く(`Zone(zone)`、`Fee(fee)`)。
- 仮の名前を離す綴りが、`_` から番号になる(`c2_` が `c2_2`、`v1_` が `v1_2`)。
- 綴りを変える名前の無いシートの下書きは、一字も変わらない。
- 規則の `check` の答えと、規則から生成したコードは変わらない。import だけの変更である(ほかは、`codegen::lower_lead` をクレートの中から呼べるようにしただけ)。
- reference の `rulec import` の段、AGENTS.md と、そこから組むスキルの SKILL.md と reference、サイトの使い方の例(シナリオの 2-1、英日)の説明を合わせた。サイトに貼った下書きは変わらない(見出しが小文字か日本語のシートで、替えた後のバイナリの下書きと一字も違わない)。
- `rulec import --help` の終了コード 0 の文を、「下書きを書き出した。名前のほかは、まだ check を通していない」(英語は "the draft was written; but for its names, it has not been through check")にした。前の「check はまだ通していない」は、名前を `check` にかけるいまの姿と合わない。
**確かめたこと**:
- `tests/import.rs` の `a_name_the_generated_code_cannot_take_as_written_is_spelled_so_that_it_can` と、材料の `tests/import/`(英語を先に、日本語の版を横に、11 本)。大文字で始まる見出し、生成先の予約語と同じ見出し、`_` で始まる二つの出力、63 バイトより長い出力、綴りで一つになる二つの見出し、大文字と小文字だけ違う二つの見出し、綴りで一つになる値、同じ見出しの二つの列である。英日の下書きのどちらでも、語ごとに決めた宣言(`Zone(zone)`、`import_`、`North(v2)`、`c2`)になり、注に語と生成コードでの名前が残り、`check` は何も言わず、`fmt --check` も通る。
- 日本語のシートで当たる形は三つ(ASCII の見出し `SKU`、大文字と小文字だけ違う値 `S60` と `s60`、同じ見出しの二つの列)で、日本語の版はそれを材料にした。ほかの形は、日本語の語には起きない。日本語の見出しと値には ASCII の別名(`c1`、`o1`、`v1`)が付き、その別名は小文字で始まる短い語なので、大文字、`_`、63 バイト、綴りのぶつかりのどれにも当たらない。日本語のシートが当たるのは ASCII の見出しを書いたときで、英語の材料と同じ形になる。
- `a_rule_name_not_read_as_a_name_is_named_for_now` に、`--name sum`(`rule sum_ v1` と注)と `--name Zone_values`(列挙が `Zone_values(c1_kind)`)を足した。どちらも `check` は何も言わない。
- `the_code_generated_from_every_draft_answers_alike_in_every_language`:`tests/import/` の 29 本を英日で下書きにした 58 本を生成し、`rulec test` の全部の走らせ方(12 の言語。PostgreSQL の関数を含む)で、参照の評価器と同じ答えを返す。替える前のバイナリでは、E034 の 15 本は何も生成されなかった。W121 の下書きは、W121 が挙げる生成先で落ちた。大文字の見出しは Ruby、別名 `import` は Python・TypeScript・JavaScript、`_` で始まる出力は Python、長い出力は PostgreSQL の関数、`ship_zone` と `shipZone` は Go・Swift・Java・Connect、`Zone` と `zone` は Go・Swift・Java・Ruby・SQLite である。替える前の大文字の見出しの下書きの Ruby は `ruby -c` で落ち、替えた後は通る。
- `tests/import.rs` の `UNREAD` の 18 本は、仮の名前の綴りが `c2_2` と `v1_2` になったほかは、前と同じことを確かめる。
- 替える前と後の突き合わせ:木の中で `rulec import` にかけるもの全部を、英日で import した。`tests/import/` の 29 本、`tests/import.rs` が書く CSV の 13 通り、`tests/cli.rs` の 2 通り、`tests/xlsx.rs` のブックと CSV の 6 通り、コーパスのブックとそのコピーの 2 通り、サイトの例のブック 2 冊(ページの下書きから作り直したもの)で、108 の下書きと、その `check --format json` を比べた。変わったのは 52 で、どれも替える前は W121 か E034 が出ていて、替えた後は `check` が何も言わない。ほかの 56 は一字も変わらない。そのうち 10 は表の形でないシートで、前も後も同じ文で止まる。`E101` が出る下書きは、前も後も同じ `E101` である。
- 木の追跡している 857 本の `.rule` の `rulec check --format json`(英日、終了コードつき)の 1,714 の出力は、替える前と一字も変わらない。
- 見出しと値に紛らわしい語(`Zone`、`zone_`、`c1` と `C1`、`type`、`import`、`North` と `north`、`_fee`、63 バイトより長い語、`trace` など)を選んで組んだ、表の形のシート 1,000 枚を英日で下書きにした(試験には入れていない)。替えた後の 2,000 本は、どれも `check` が何も言わず、`fmt --check` も通る。替える前のバイナリでは、1,756 本に W121 か E034 が出た。
**見つけて直していないもの**:
- **入力の見出しの `from`**(`from,to,fee` のような、どこからどこへの表):下書きの入力の行 `from : from_values` を、構文解析が `from <shape>.<field>` の始まりと読み、E013 で止まる。入力の行から `from` を探すところ(`parse::var_block`)が、行の頭の名前まで見るためで、手で `inputs` に `from : bool` と書いた規則も同じく止まる。出力と値の `from` は読める。直すなら、型の後ろから探すことになり、構文の誤りだった規則が通るようになるので、`check` の答えが変わる。
- **W121 がまだ言わない形は、下書きにも残る。** 生成器が自分で付ける名前とのぶつかり(§15.263 の見つけて直していないもの)で、出力の見出し `trace` の下書きは、`check` が何も言わないまま Connect の `.proto` が組めない。W121 がそれを言うようになれば、import は別名を付ける。
### 15.267 `fmt` は、表の行と `machine` の行を、字句が読むとおりに切る(2026-10-24)
**きっかけ**:§15.257 の見回しで見つけて残したもの。コメントに `|` を書いた表の行(`| a | # x|y`)を、`rulec fmt` は `| a | # x | y |` に書き直していた。字句が読む語は変わらないが、コメントは書いたとおりに残らない。
**見回し**:替える前のバイナリで、表のセルのあいだにコメント・文字列・引用が入る書き方を一つずつ整形し、字句が読む語とコメントを整形の前と後で比べた。
- 表の行(見出しの行、ラベルのある行、`examples` と `scenario` の行)のコメントの中の `|`:そこで行を切り、コメントをセルとして並べ直した。見出しの行のコメント `# old|-> c` は、二つ目からの出力の列の印と読まれて `->` まで外れ、`# old | c |` になった。引用(`@…`)のあとのコメントも同じだった。
- 引用箇所を引用符で書いた行:`@osha "x|y"` の `|` を最後の区切りと取り違え、行の終わりに `|` を足した(字句が読む `|` が一つ増える)。`@osha "x#y"` の `#` をコメントの始まりと取り違え、文字列を `"x #y"` に書き換えた。
- 行の頭の語のすぐあとに、空白を挟まずに `|` を含む文字列かコメントが続く行:`description"a|b"` を、ラベル `description"a` の表の行と読み、`description"a | b" |` に書き換えた(文字列が変わり、`|` が一つ増える)。`examples#x|y` は `examples#x | y |` になった。
- 最後の `|` と引用のあいだの語:行を閉じ忘れた `| a | b @x` の `b` を捨てた(`| a | @x`)。
- `machine` の行:`once` と `always` の文字列の中の `#` をコメントの始まりと取り違え、`starts_with "paid #"` を `starts_with "paid #"` に書き換えた。文字列の中の ` ,` も前の字に詰めた(`"paid ,1"` が `"paid,1"`)。`once` の文字列が変わると、検査の答えも変わる。記録が `paid #` で始まる呼び出しが二回ある規則は E127 で止まるが、整形したあとは何も言われなくなった。文字列の外の `1 ,000` も、詰めると桁区切りの一つの数(E049)になった。
- `clause` の `when`・`then`・`overrides`、表の `overrides` の行、`prerequisites`、`apply`、`enum` と `define` の行は、文字列の外の `#` からをコメントとして扱っていて、食い違いは無かった。
**原因**:整形は、表の行を字句に聞かずに、字で切っていた(`is_row`、`split_label`、`split_comment`、`split_cite_row`)。行かどうかとラベルは最初の `|` の字で、コメントは最後の `|` の字の後ろの `#` から、引用は最後の `|` の字の後ろの `@` から決めていた。字句は、文字列の外の `#` から行末までをコメントとして読み、その中の `|` と `@` を読まない。文字列の中の `|`・`@`・`#` も、文字列の字として読む。`machine` の行でも、整形は `#` の字をコメントの始まりとし、` ,` の字を詰めていた(`machine_body`)。§15.257 は、セルを切る `|` を文字列の外のものにしたが、行の頭とコメントと引用は、字で決めたままだった。
**決定**:
1. **表の行は、字句が読むとおりに切る**(`fmt::split_row`)。行かどうかとラベルは、字句が `|` として読む最初の `|` で決める(`is_row`)。引用は、字句が読む最後の `|` の後ろで字句が読む最初の `@` から、コメントは、字句が読む最後の語の後ろの `#` から行末まで(`comment_at`)。字句が一行をどう読むかは、§15.257 と同じく `lex::read_through` に聞く。
2. **セルは、§15.257 のとおり、文字列の外の `|` で切る。** コメントと引用を先に切り離すので、その中の `|` でセルを切ることはない。
3. **表の行の途中の `#` からも、字句どおりにコメントとして書く。** `| a | # b | c |` は `| a | # b | c |` になる。前は `| a | # b | c |` のまま並べた。字句はどちらも `| a |` とコメントを読み、列の数が合わない誤り(§1.5)も同じに言われる。
4. **`machine` の行のコメントは、字句が読むところから始める**(`comment_at`)。カンマを前の語に詰めるのは、字句がカンマとして読むところで、詰めても字句が同じ語を読むときだけにする(`commas_against`)。`1 ,000` は書いたままにする。
5. §1.5 の「表の行では、最後の `|` より後ろにあるものを行末コメントとして切り離す」を、字句が読む最後の `|` と、文字列の外の `#` で書き直した。reference の 11 章も同じにした。
**捨てたもの**:
- **コメントと引用の見分け方を、整形の側に字で書くこと**(`#` のあとの `|` は数えない、引用符の中の `@` は数えない、など)。字句の決まりをもう一つ書くことになり、文字列の中の `#` のように二つの決まりが食い違う。§15.257 で捨てたのと同じ理由である。
- **字句の決まりを変え、表の行のコメントを最後の `|` の字の後ろからにすること。** 字句は行を左から読み、`#` から先を読まない。`| a | # b | c |` がセルとして読まれるようになり、構文の誤りだった規則が通るようになる。
- **セルを切る `|` も、字句が読む `|` にすること。** 閉じていない文字列(E001)のある行では、字句はそこから行末までを読まないので、その中の `|` が無いことになり、並べ直すと `|` が一つ増える。文字列の外の `|` で切れば、そういう行も前と同じに並ぶ。
**変わること**:
- コメントに `|` を書いた表の行(見出しの行、ラベルのある行、`examples` と `scenario` の行)は、整形してもコメントが書いたとおりに残る。前は、コメントがセルとして並べ直された。
- 引用箇所を引用符で書いた行(`@osha "x|y"`、`@osha "x#y"`)は、整形しても字句が読む語が変わらない。前は `|` が足されるか、文字列が書き換えられた。
- 最初の `|` が文字列かコメントの中にある行(`description"a|b"`、`examples#x|y`)は、表の行として並べず、書いたまま残す。
- 最後の `|` と引用のあいだの語は捨てない。引用の無い行と同じく、行を `|` で閉じる(`| a | b | @x`)。
- 表の行の途中の `#` から行末は、コメントとして空白二つのあとに書く(`| a | # b | c |`)。
- `machine` の行の文字列の中の `#` と ` ,` は、整形しても変わらない。前は文字列が書き換えられ、`once` と `always` の検査の答えが変わることがあった。`1 ,000` も書いたまま残る。
- 木の規則の整形は変わらない(下の突き合わせ)。
**確かめたこと**:
- `tests/fmt.rs` に三つ足し、二つを強めた。
- `a_comment_and_a_string_stay_as_written`:材料 `tests/fmt/comments_and_strings_kept_as_written.rule` と、日本語の版 `書いたまま残るコメントと文字列.rule`。表の見出しの行とラベルのある行と `examples` の行のコメントに `|` と引用符と `->` を書き、セルの文字列に `|` と `#` とカンマの前の空白を、`machine` の `once` の文字列に `#` とカンマの前の空白を、そのコメントに `|` を書いた。どちらも check を通り、整形しても一字も変わらない。行の空白を詰めたものを整形すると材料に戻り、check の答えも同じである。
- `a_row_is_cut_where_the_lexer_cuts_it`:見回しで試した 13 の行(表の行と、最初の `|` が文字列かコメントの中にある行)と、引用の前の語。13 の行は、どれも字句が読む語とコメントが変わらず、二度整形しても同じである。
- `a_line_under_machine_keeps_its_strings`:`machine` の行の四つ(文字列の中の `#` とカンマ、文字列の外のカンマ、`1 ,000`)。
- 木の全部の `.rule` を整形する二つの試験(`every_rule_of_the_tree_reads_the_same_formatted` と、IME で打った版)は、字句が読む語のほかに、行ごとのコメントも同じであることを確かめるようにした。
- `fmt.rs` を替える前に戻すと、足した三つと、木の全部を整形する二つ(足した材料で)が落ちる。
- 替える前と後の突き合わせ:木の `.rule` の全部(911 本。足した 4 本を含む)をコピーし、`fmt --check`(英語と日本語、JSON)、`fmt` が書き換えた中身とその出力、書き換えたあとの `fmt --check` を比べた。5,466 の出力のうち、変わったのは足した二つの材料の 10 だけで、替える前は二つとも「整形されていない」と言い、書き換えるとコメントとセルと `machine` の文字列が変わった。替える前も後も、整形したものにもう一度かけると何も変えない。
- 再現の `| a | # x|y` は、替えた後の整形では一字も変わらない。二度整形しても同じで、check の答え(規則の行が無いので E003)も整形の前と後で同じである。
- `once note starts_with "paid #"` を持ち、記録に `paid #1` と `paid #2` を出す規則は、記録がそのセルに当てはまる呼び出しが二回あるので E127 で止まる。替える前の整形のあとは何も言われず、替えた後の整形のあとも E127 で止まる。
**見つけて直していないもの**:
- `source` の下の固定行にコメントを書くと(` 第1条 sha256:… # 注`)、整形はそれを固定行と見ず(語が二つの行だけを固定行とする `is_pin`)、その行から下の固定行をそろえない。語とコメントは変わらない。替える前から同じで、字句との食い違いではないので、この変更には入れていない。
### 15.268 出力の行の `range` を確かめてほかの言語へ渡し、`result` が決める出力にもとる値の範囲を付ける(2026-10-28)
**きっかけ**:二つある。
- 出力の行に書いた `range` を、パーサーが黙って読み飛ばしていた。`配分 : money[円, incl_tax] range >=0円 <=10円 round down(1円)` と書いた規則は、例が 7,333 円を答えても `check` を通った。生成したコードも schema も、範囲を書かない同じ規則と、ファイル名とハッシュのほかは一字も違わなかった。書いた人は答えの範囲を約束したつもりでいて、ほかの言語はそれを知らない。
- 証明書の `ranges` には、入力、導出、表が決める出力が載るが、`result` が決める出力は載らなかった。kaname と dandori と ritsu-cross は、規則の数の出力の範囲を ritsu の口 `Rules` の `output_values` で読み、それはこの範囲から来る。そのため `result` の出力は、上限も下限も無い値として読まれた。按分の定番の形(同じ金額を手前までとここまでの累計で `allocate` し、`result` で引く。§15.102)を kaname の関数から明細ごとに呼び、配った額を税率ごとに `sum` で足すと、足した値が int64 に収まるかを決められず(kaname の W302)、関数のエフェクトに `overflow` が付いた。出力に `range` を書いて渡そうとしても、一つ目のとおり読まれなかった。
**見回し**:替える前のバイナリで、宣言の行の種類ごとに、その行が読まない語を書いた規則を 13 本作り、`check` にかけた。読む語だけを書いた規則 4 本も並べて比べた。
- 黙って通ったものが 12 本あった。出力の行の `range` と `contract_only`、入力の行の `round`(`from count` を持つ行も)、要素のフィールドの行の `round`、`derive` の行の `round`・`contract_only`・`random`、`count` の行の `contract_only`、`sum` の行の `round`、入力の `from` のパスの後ろの `range`、`shape` の行の後ろの `range` である。どの語も、検査にも生成したコードにも効いていなかった。
- `from max … empty 0` の後ろの `contract_only` は、E074(`empty` の値の後ろには何も置けない)で止まった。
- E047 が出るのは、四つの語(`range`・`round`・`contract_only`・`random`)のどれでもない語だけだった。四つの語は、どの種類の行に書いても、行が読む語として読み飛ばされていた。
**原因**:
- パーサーは宣言の行の後ろを一つの決まり(`parse::tail_junk`)で読み、四つの語を、どの種類の行にも置ける語として扱っていた。行の種類ごとの読み手(`tail_range` など)は自分の欲しい語だけを探すので、ほかの種類の行の語は、だれにも読まれずに残った。出力の行を読むところ(`out_block`)は型と丸めだけを読み、`OutDecl` には範囲を持つところが無かった。
- `result` が決める出力の区間は、int64 の確かめ(E108)のために求めていた(`Checked::interval`)が、`Checked::ranges` にも証明書にも入れていなかった。表が決める出力は、行が書く値の区間を `ranges` に入れていた(§15.196)。
**決定**:
1. ★ **出力の行の `range` を読み、出力のとる値を含むかを確かめる。含まなければ E140 にする**(`Checked::output_ranges`)。とる値は、出力を決めるもの(表の行のセルの値と式、同じ名前の `define`、`result` の式)が、入力の範囲と `constraint` のもとでとる値を、丸めの刻みへ外向きに広げたものである。丸めの向きによらず、下の端は刻みへ切り下げ、上の端は切り上げる。丸めた値の区間を求めるときと同じ広げ方である。E140 の文は、広げる先の範囲を言い、`fix.text` にも `range >=0円 <=1000円` の形で入れる(`FixKind::WidenRange`)。
E112 にしなかったのは、守るものと勧める直し方が違うからである。E112 は、導出の範囲が狭いと完全性の検査が起きる値を見ないことを言い、起こりえない値まで広げても害は無いと勧める。出力の範囲が狭いと、規則が返す値を見落とすのはほかの言語の範囲の検査で、広げすぎると、ほかの言語が示せることが減る。下の 2 の確かめられない範囲は、E112 には無い場合である。番号は台帳の次の番号にした。
2. ★ **確かめられない範囲も E140 にする。** 出力が範囲の無い入力を読むとき(rulec がとる値を求められない)と、数・お金・量・率でない出力に書いたときである。書いた範囲をそのまま信じて渡すと、規則が返す値の一部を外した範囲を、ほかの言語が読むことがある。文は、読む入力に範囲を書くか、範囲を外すことを勧める。範囲の無い数の出力は、ほかの言語が型の全体として読む(kaname の DESIGN 32 章)。ほかのエラーがあるときは、数の出力の確かめられない範囲を言わない。とる値を求められない理由が、そのエラーのことがあるからである(E117 になる `allocate` には区間が無い)。
3. **確かめた範囲を、出力の範囲にする**(`Checked::ranges`)。片側だけ書いた範囲は、書かない端を、とる値の端で埋める。`max` と `min` に書いた範囲と同じである(§15.219)。この範囲は、入力と導出の範囲が出るところの全部に出る。証明書の `ranges`、`rulec schema`(`properties.out` の `minimum` と `maximum`)、`rulec api`(言語ごとの `outputs` の `range`)、MCP のツールの `outputSchema`、`.proto` と SQL の出力のコメント、`doc` と人が読むページの出力の表(注の列の「範囲 0円 〜 100万円」)、ritsu の口の `output_values` である。`check` が示した範囲なので、生成したコードは走るときに確かめない。空の範囲は E061、型の値でない端は E103 で、入力の範囲と同じに言う。E061 の注は、出力のどの値もその範囲に入らないことを言う。入力の範囲の注にある、完全性の証明と入口の話は、出力には当たらないからである。
4. ★ **範囲を書かない `result` の出力にも、とる値の範囲を付ける。載せるのは証明書の `ranges` と口の `output_values` だけにする**(`Checked::result_range`、`Checked::output_range`)。表が決める出力と同じく、ほかの言語が読めるようにする。`Checked::ranges` には入れない。そこを読む生成器、schema、`api`、規則の事実は変わらない。数・お金・量・率の出力で、optional でないものに限る。
5. ★ **按分の差を、按分どうしの順で読む**(`Checked::shares_in_order`)。同じ金額と同じ全体の `allocate` を二つの累計で呼び、`constraint`(の連なり)か同じ名前が一つ目の累計を二つ目以下に保つとき、二つ目から一つ目を引いた差は 0 を下回らない。`floor(T × C ÷ S)` は C について増えるからである。金額の範囲の下の端が 0 以上で、全体の下の端が 0 より大きいときに限る。区間だけで引くと −T から T になる差を、0 から T と読む。按分は、呼び出しのまま書いても、それを計算する `derive` か `define` の名前で書いてもよい(`Checked::defs`)。証明書の再検査も同じに読む。Python の `tools/recheck.py` は `shares_in_order`、Lean は `RulecCert/Values.lean` の `sharesInOrder` と、按分の値の順の定理 `sharesInOrder_le`、それを使う `eval_mem_interval` の引き算の場合、`RulecMain.lean` の `expandShares` である。
6. **宣言の行の種類ごとに読む語を決め、ほかの語を E047 にする**(`parse::Tail`)。入力の行は `range`・`contract_only`・`random`、要素のフィールドの行は `range` と `contract_only`、出力の行は `range` と `round`、`derive` と `count`・`sum`・`max`・`min` の行は `range` を読む。`shape` の行と、入力の `from` のパスの後ろは何も読まない。E047 の注は、その行が読む語を挙げる。出力・要素のフィールド・`from` の後ろの `random` は、これまでどおり E070 が理由を添えて言う。出力の `range from koyomi` も、これまでどおり E065 である。
7. **`rulec diff` は、出力の範囲が動いたことを、規則が答えるものの変更として言う**(`domain` の `output_range`)。どちらかの版が出力の行に範囲を書いているときに比べる。
**捨てたもの**:
- **出力の範囲を、生成したコードが走るときに確かめること。** 範囲は `check` が示しているので、確かめて外れる値が無い。示せない範囲は E140 で止める。
- **書いた範囲をそのまま信じて渡すこと。** 確かめられない範囲を通すと、ほかの言語の範囲の検査が、規則の答えの一部を見ないまま「示した」と言う。
- **`result` の出力のとる値を `Checked::ranges` に入れること。** 生成器、schema、`api`、規則の事実がそこを読むので、範囲を書いていない規則の生成物が変わる(木の規則では 46 本)。ほかの言語が読むのは証明書と口だけである。
- **二つの値の差の下の端を、`constraint` の関係から一般に求めること。** `floor` を通った値の順が累計の順と同じだと言えるのは、同じ金額と同じ全体の按分だけである。按分の形に絞った。
- **範囲の無い数の出力を、口で型の全体として渡すこと。** rulec は分からない範囲を分からないまま渡し、型の全体として読むのは読む側にした(kaname の DESIGN 32 章)。出力が返る型は、生成したコードの出力先ごとの型で、読む側が知っている。
**変わること**:
- 出力の行に、とる値を含まない範囲を書いた規則は E140 で止まる。確かめられない範囲を書いた規則も E140 で止まる。前は黙って通った。
- 宣言の行に、その種類の行が読まない語を書いた規則は E047 で止まる。前は黙って通った(見回しの 12 本のうち、出力の行の `range` のほかの 11 本)。
- 範囲を書かない `result` の出力は、証明書の `ranges` と口の `output_values` に、とる値の範囲が載る。木の規則では、46 本の証明書の `ranges` に一つずつ増えた。
- 按分の差と、それを読む値の区間は、−T 〜 T から 0 〜 T になる。木の規則では、4 本の証明書の `values` と、2 本の証明書の `ranges` の二つずつが変わった(`tests/corpus/pro_rata_allocation.rule` の `gap` と `allocated`、`比例配分.rule` の `差分` と `配る`)。前の版の再検査は、この 4 本の新しい証明書を通さない。前の `tools/recheck.py` は、`pro_rata_allocation.rule` に `FAILED ranges: allocated: its rows write values from -1000000 to 1000000, and the certificate states the range [0, 1000000]` と `FAILED int64` を言う。
- 生成したコード、schema、`api` は、出力の行に範囲を書いた規則のほかは変わらない。範囲を書いた規則で変わるのは、決定の 3 に挙げたところだけである。
- ほかの言語は、`result` の出力の範囲を読む。kaname では、規則の答えを足すところが int64 に収まると示せるようになり、関数のエフェクトから `overflow` が消えることがある(kaname の DESIGN 32 章)。ワークフローが `result` の出力を chobo の振替の額に渡すところ(ritsu の X4)は、前は範囲に端が無いので W203 と W204 だった。いまは、範囲が 0 以上 2⁶³ − 1 以下なら成り立ち、外れれば E203 で止まる。`result needed = asked - holding`(`asked` は 0 〜 100、`holding` は 0 〜 50)を振替に渡すワークフローは、替える前は W203 と W204 で通り、替えた後は E203(「タスク `give_seats` が `count` に渡す額が -50 になることがあり、chobo はその額を受け取りません」)で止まり、注に規則のベクタの入力 `asked = 0, holding = 50` を挙げる。
- E140 を台帳と `docs/codes.md`・`codes.ja.md` に足し、E047 の文を、行の種類ごとに読む語を言う形に書き直した。reference の 4 章に「`range` on an output」の段を、8 章の `allocate` に按分の差の段を足し、formats の `ranges`・`api`・`diff` の段を合わせた。コーパスの按分の規則(`pro_rata_allocation.rule` と `比例配分.rule`)の出力の行に `range >=0JPY <=1_000_000JPY`(`range >=0円 <=100万円`)を書き、例のページ(`tools/make_examples.py`)に、その範囲を検査が確かめることを足した。AGENTS.md とスキル、サイトの checks・generate・index・assurance(英日)も合わせた。
**確かめたこと**:
- `tests/output_range.rs` の 8 つ。出力の行の範囲がとる値を含むか(`result` と表、英日、端の `>` と `<`、片側だけの範囲、`fix.text`)、確かめられない範囲(範囲の無い入力、列挙の出力)、空の範囲(E061)と型の値でない端(E103)と `range from koyomi`(E065)と `random`(E070)と、按分の累計の `constraint` を外した規則の E117 が、それぞれのコードだけで言われること、行の種類ごとの E047(出力の行の税区分と `contract_only`、`derive` の `round`、`from` の後ろの `range`、`shape` の後ろの語)、書いた範囲が証明書・口(事実と `output_values`)・schema・`doc` に出ること、範囲を書かない `result` の出力が証明書と `output_values` にだけ範囲を持ち、schema と口の事実が変わらないこと、按分の差(コーパスの按分の規則と、kaname の discount_split が呼ぶ規則。累計の順が無いとき、全体が違うとき、金額が違うときは、前と同じく 0 を下回る。差の範囲は出力の行に書ける)、`diff` が出力の範囲の動きを言うこと。
- 台帳の試験(`tests/codes.rs`):E140 の例(英日と、確かめられない範囲の英日)が E140 を言う。mutant の `m_e140.rule` と `m_e140ja.rule`(`tests/make-mutants.sh` が作る)と golden の `E140.txt`(英日)。
- 見回しの 17 本:替えた後は、黙って通った 12 本のうち 11 本が E047、出力の行の `range` が E140 で止まる。E074 の 1 本と、読む語だけの 4 本は前と同じである。
- 替える前と後の突き合わせ:木の `.rule` の全部(911 本)を、替える前と後のバイナリで `check`(英日、JSON)、`certificate`、`schema`、`api`、`gen`(全部の出力先の全部のファイルのハッシュ)にかけて比べた。変わったのは 47 本である。46 本は証明書の `ranges` に `result` の出力が一つ増えたもの(そのうち 4 本は按分の差で `values` の区間も狭まり、2 本は `ranges` の二つも狭まった)、1 本は E047 の文が変わった mutant の `m_e047.rule`(`check` の英日と JSON、`api` の文)である。`check` の答えのコードと行は 911 本とも変わらない。出力の行に範囲を書いた按分の規則 2 本は、`schema` と `api` と `gen` も変わる。`gen` で変わるのは、MCP のツール(TypeScript・JavaScript・Python)の `outputSchema`、人が読むページ(三つ)の出力の表とスクリプトが読む規則の形、`.proto` と SQL の二つのコメントの 9 つのファイルである。ほかの 909 本の生成したコードと schema は一字も変わらない。
- 再現:出力の行に `range >=0円 <=10円` を書いた規則は E140 で止まり、とる値として `>=0円 <=1_000_000円` を言う。按分の差の `result` の出力は、証明書の `ranges` に `0` 〜 `1000000` で載る。
**見つけて直していないもの**:
- **証明書の再検査は、出力の行に書いた範囲がとる値を含むことを確かめ直さなかった。** `result` か `define` が決める出力に書いた範囲は、`check` の E140 だけが確かめていた。§15.269 で、二つの再検査が、範囲のある数の出力のどれについても、範囲が出力を決めるものを丸めの刻みへ広げたものを含むことを確かめるようにした。
- **按分の差の読み方は、按分の三つの引数が名前のときだけである。** `allocate(返金額, 手前まで + 1円, 合計)` のように式を渡した按分や、按分を `min` や丸めで包んだ値の差は、前と同じく区間で引く。
- **二つの入力を結ぶ `constraint` は、出力の区間を狭めない。** `constraint holding <= asked` のもとの `result needed = asked - holding` は負にならないが、区間は −50 〜 100 になる。ritsu の X4 は、外れる額になる入力が規則のベクタにあるときだけ E203 にする(ritsu の DESIGN 7.6)。口の例には、ベクタがとるいちばん小さい値といちばん大きい値も入る(§15.270)。ベクタは `constraint` を守るので、この出力のベクタは 0 〜 100 で、ワークフローがこれを振替に渡すと W203(決められない)になる。同じ差から 5 を引いた `asked - holding - 5` は、ベクタが −5 になるので E203 で止まる。W203 の出力が成り立つと言うには、二つの値の差を `constraint` で狭めることが要るが、按分の差(決定の 5)のほかには無い。
- **`rulec diff` は、範囲を書かない出力のとる値が動いても言わなかった。** §15.271 で、数の出力のどれも、口が渡す範囲で比べるようにした。
### 15.269 二つの再検査でも、出力の範囲が丸めた答えを含むことを確かめる(2026-10-29)
**きっかけ**:§15.268 の見つけて直していないものの一つ目。数の出力の範囲は、ほかの言語が規則の答えの範囲として、ritsu の口の `output_values` から読む。ところが証明書の二つの再検査(`tools/recheck.py` と Lean の `rulec-recheck`)はそれを確かめ直さず、出力の行に書いた範囲は `check` の E140 だけが確かめていた。
**見回し**:再検査に足す確かめを、出力の範囲が、出力を決めるもの(`result` と `define` の式、表の行が書く値)を丸めの刻みへ外向きに広げたものを含むこと、とした。規則が答えるのは、出力を決めるものを刻みに丸めた値だからである(§7.2)。証明書に刻みを書き足しただけの rulec(出力の範囲は前と同じ `ranges` の範囲)で木の規則の全部(913 本。証明書の出るものは 554 本)の証明書を作り、この確かめにかけると、7 本を二つの再検査がどちらも通さなかった。どれも、範囲を書かず `define` が決める出力である。
| 規則 | 出力と丸め | 証明書の範囲 | 刻みへ広げた範囲 | 口が渡した範囲 |
|---|---|---|---|---|
| 手数料の 4 本(kaname の例 `payout` の `card_fee.rule` と `カード手数料.rule`、ritsu の試験のプロジェクト `settlement` の `card_fee.rule`、コーパスの `paypal_fee.rule`) | `fee`、`round half_up(1USDc)` | 49.0349 〜 4,990,049 | 49 〜 4,990,049 | 49 〜 4,990,049 |
| `experiments/library/rules/協会けんぽ健康保険料.rule` | `支援金給与控除額`(`round half_down(1円)`)と `支援金現金納付額`(`round half_up(1円)`) | 0 〜 1,598.5 | 0 〜 1,599 | 0 〜 1,598 |
| コーパスの `japan_income_tax.rule` と `所得税.rule` | `surtax` と `復興特別所得税`、`round down(1JPY)` | −100,714.95 〜 94,500,000 | −100,715 〜 94,500,000 | −100,714 〜 94,500,000 |
口は、証明書と同じ範囲を、wire の整数に 0 の向きへ切り捨てて渡していた。`支援金現金納付額` は rulec のベクタでも 1,599 円を答えるのに、口は上の端を 1,598 と渡していた。
**原因**:
- 再検査は、計算する値の区間を `values` から自分で求め、`ranges` が言う範囲を読まない(§15.195)。表が決める列は、`ranges` の範囲が行の書く値の外包を含むかを確かめる(§15.196)が、丸める前の値についてである。出力を何の刻みに丸めるかは、証明書に無かった。
- 証明書と口の出力の範囲(`Checked::output_range`)は、出力の行に書いた範囲(E140 が、刻みへ広げたとる値で確かめる)と、`result` が決める出力の区間(§15.268 で刻みへ広げた `Checked::result_range`)のほかは、`Checked::ranges` の範囲をそのまま渡していた。`define` と表が決める出力では、それは出力を決めるもの、つまり丸める前の値の範囲で、刻みの上に無い端は、丸めた答えより内側にあることがある。
**決定**:
1. ★ **証明書に `rounds` と `output_ranges` を足す。** `rounds` は、数の出力ごとに、丸める刻みを `ranges` と同じ尺度の有理数で書く(`"rounds":{"fee":"10"}`)。数の出力は、どれも丸めを宣言している(§7.2、E104)。`output_ranges` は、数の出力ごとに、ritsu の口がほかの言語へ渡す範囲を書く(`"output_ranges":{"fee":["0","1010"]}`)。
2. ★ **二つの再検査は、`output_ranges` の範囲のどれについても、その範囲が、出力を決めるものを丸めの刻みへ外向きに広げたもの(下の端は刻みへ切り下げ、上の端は切り上げる)を含むことを確かめる。含まなければ証明書を通さない。** 出力の行に書いた範囲にも、rulec が数えて載せた範囲にも当てる。ほかの言語は、どちらも口の `output_values` で同じに読むからである。出力を決めるものは、再検査がほかの確かめのために数えているとおりに数え、新しい数え方は作らない。`result` と `define` は式の区間(按分の差の `sharesInOrder` を含む `interval`)、表は行が書く値の外包(`hullOf`)である。たどる並びが残す値(`count`、`sum`、`max`、`min`、畳み込みの答え)と開いた値は、値が `ranges` の言う範囲に収まることを、宣言かそれぞれの確かめが言っている(formats の `walks` と `opens`)。そこでその範囲を出力を決めるものとして読む。丸めの向きは見ない。どの向きでも、答えは刻みへ切り下げた値と切り上げた値のあいだにある。E140 も、向きによらず同じに広げる(§15.268 の決定の 1)。
3. ★ **行に範囲を書かない出力の、`output_ranges` と口の範囲を、丸めの刻みへ外向きに広げる**(`Checked::output_range`、`types::round_out`)。見回しの 7 本は、こうしないと新しい再検査を通らない。**`ranges` は前のまま、出力を決めるものの範囲にする**(`Checked::stated_range`)。証明書のほかの主張が、`ranges` を出力を決めるものの範囲として読むからである。たどる並びと開いた値の軸はその範囲の上を走り、再検査は、まだ数えていない名前を読む式の区間を、その範囲で抑える(捨てたもの)。`Checked::ranges` も変えない。§15.268 の決定の 4 と同じく、生成器、`schema`、`api`、規則の事実はそこを読むので、変わらない。
4. ★ **たどる並びが残す値か開いた値の出力で、`ranges` の範囲が `output_ranges` の範囲と同じで、その端が刻みの上にないものは、通さないのではなく、確かめられない主張として一行で言う**(`what total answers once rounded: the range it declares, [0, 1050], does not sit on the grid it rounds to, 100`)。出力の行に範囲を書くと、それが並びの範囲にもなる(§15.219)ので、再検査にはそれより狭い範囲が無い。`check` の E140 は、並びの宣言した範囲を刻みへ広げたもので確かめていて、その規則を通している。行に範囲を書かなければ、`output_ranges` は決定の 3 で刻みに載るので、この一行は出ない。
5. **Lean では、出力の主張と、確かめと、定理を `RulecCert/Outputs.lean` に置く。** 主張は、範囲のある名前がどれも範囲に入り、`constraint` がどれも成り立つところでは、丸めがどの向きでも(`RoundsWell`)、出力を決める式の値を刻みに丸めた答えが、証明書の言う範囲に入ること(`OutputHolds`)である。表が決める出力は、行が書く値のうち、その行のセルが切る値のところのものについて言う(`WrittenOutputHolds`)。確かめは `roundedWithin`(刻みが 0 より大きく、範囲の下の端が区間の下の端を切り下げたもの以下で、上の端が区間の上の端を切り上げたもの以上)で、通れば主張が成り立つ(`output_mem_stated`、`written_output_mem_stated`)。どちらも、区間に入る値を丸めた答えは範囲に入るという `rounded_mem_stated` から出る。たどる並びが残す値と開いた値は、`rounded_mem_stated` に、それぞれの定理(`extreme_mem_stated`、`foldAnswer_mem_stated`、`openValue_mem_stated`)が言う範囲を渡したものになる。`rulec-recheck` の `checkRounds` と `tools/recheck.py` の `check_rounds` が、`output_ranges` に載った出力ごとにこの確かめを走らせる。新しい算術は無い(`floorTo` と `ceilTo` は、丸めた値の区間を求めるところで使っている)。定理は、出力一つと、`rounds` がそれに言う刻みについて言い、`rounds` も `output_ranges` も読まない。どの出力を確かめるかは、二つのプログラムが決める。`output_ranges` のどの出力にも当たる、という形の定理にするには、証明書の JSON のどの名前を回るかを Lean の論理に入れることになる。`RulecMain.lean` のほかの確かめも、回るところはプログラムで、定理は一件ごとである(`checkOpens` と `openValue_mem_stated` など)。そこで定理はこのままにし、回るところを決定の 7 で決めた。
6. **刻みは、証明書の言うとおりに読む。** 再検査は規則の出力の行を読まない。型、範囲、式、尺度、運ぶ値の丸め(§15.217)と同じく、証明書の言い分である。
7. **再検査は `output_ranges` の側から回り、そこに載った出力の刻みを `rounds` が言わなければ、二つとも同じ文で通さない**(`fee: the certificate states a range for it but not the grid it rounds to`)。`output_ranges` があって `rounds` の無い証明書も同じである。範囲が縛るのは、出力を決めるものを刻みに丸めた答えなので、刻みが無ければ、範囲が答えを含むかを言えない。rulec は数の出力のどれにも刻みを書く(E104。0 以下の刻みは E060)ので、rulec が書いた証明書ではこうならず、証明書を書き換えたときにだけなる。
8. **`rounds` も `output_ranges` も無い証明書は通さないのではなく、確かめられない主張として一行で言う**(`what each output answers once rounded: the certificate does not say what grid its outputs round to`)。§15.268 までの rulec が書いた証明書がそれにあたる。§15.190 の `scales` と同じ扱いである。範囲の端が開いている出力と、出力を決めるものを再検査が数えない出力も、同じに一行で言う(木の規則には、どちらも無い)。
**捨てたもの**:
- **範囲を書いた出力だけを確かめること。** 口は、範囲を書いた出力も書かない出力も同じに渡す。書かない出力の範囲が答えを外していても、ほかの言語の検査はそれを見ずに「示した」と言う。見回しの 7 本がそうだった。
- **丸めの向きごとに答えの端を求めること**(`half_down` の 1,598.5 は 1,598)。範囲が刻み一つ狭くなることがあるだけで、確かめに向きごとの場合分けが要る。Lean の丸めは、向きを決めずに言っている。
- **証明書の範囲はそのままにして、再検査の側で刻みへ広げてから比べること。** 再検査は通るが、口が渡す範囲は、規則の答えを外したまま残る。
- **`ranges` の出力の範囲を刻みへ広げること**(`output_ranges` を足さずに、`ranges` を口の範囲にする)。初めはこうしたが、`check` を通る規則の証明書を、二つの再検査が通さなくなるところが二つあった(木の規則には無い)。一つは、範囲を書かない出力をたどる並びが決め、並びの範囲の端が刻みの上にないときである。1,050 セントまでの `sum` を `round down(100USDc)` で答える出力で、表の軸は並びの範囲の 1,050 セントまで走るのに、`ranges` は広げた 1,100 セントを言い、軸の確かめが落ちた(`FAILED big_of: total runs from 0 to 1050, and its range is 0 to 1100`)。もう一つは、`define` の出力を、それより上に書いた `define` が読むときである。再検査は、まだ数えていない名前の区間を `ranges` から読むので、読む側の区間が証明書の言う区間より広くなり、int64 の確かめが落ちた(`FAILED int64: total: the interval here is [50, 1084], wider than the stated [500349/10000, 10839/10]`)。どちらも、替える前の rulec の証明書は通っていた。
- **`Checked::ranges` を刻みへ広げること。** 生成器、`schema`、`api`、規則の事実がそこを読むので、範囲を書かない出力を持つ規則の生成物が変わる。決めることが一つ増えるので、別にした(見つけて直していないもの)。
- **`rounds` の側から回ること。** 刻みの無い出力の範囲は、確かめず、確かめられない主張にも挙げない。ある出力の刻みを `rounds` から外し、同じ出力の `output_ranges` の範囲を狭めた証明書は、二つの再検査が `OK: every claim this program states was proved` と言って通し、`rounds` を丸ごと外した証明書も、確かめられない主張の一行だけで通っていた。
**変わること**:
- 数の出力を持つ規則の証明書に、`rounds` と `output_ranges` が増える。木の規則では、証明書の出る 554 本のどれも、この二つが増えたほかは前と同じである。`output_ranges` の範囲が `ranges` の範囲と違うのは、見回しの 7 本の 8 つの出力である。
- 口の `output_values` が渡す範囲は、7 本のうち 3 本で変わる。`協会けんぽ健康保険料.rule` の支援金の二つは、上の端が 1,598 から 1,599 に、`japan_income_tax.rule` の `surtax` と `所得税.rule` の `復興特別所得税` は、下の端が −100,714 から −100,715 になる。手数料の 4 本は、前と同じ 49 〜 4,990,049 である(49.0349 を切り捨てた 49 が、刻みへ切り下げた端と同じ)。変わる 3 本は、rulec の試験と文書だけが読む規則である。
- 二つの再検査は、出力の範囲を確かめた数を一行で言う(`outputs: 3 ranges hold what their output answers, put on the grid it rounds to`)。範囲が丸めた答えを含まない証明書と、刻みの無い範囲を言う証明書は、二つとも同じ文で通さない(`tools/recheck.py` は `FAILED outputs: ` に、Lean は `FAILED: ` に続ける)。
```
FAILED outputs: fee: what decides it runs from 0 to 1005; rounded to 10, it answers from 0 to 1010, and the certificate states the range [0, 1005] for it
FAILED outputs: fee: the certificate states a range for it but not the grid it rounds to
```
- 替える前の rulec が書いた証明書(`rounds` も `output_ranges` も無い)は、新しい再検査も通すが、確かめられない主張の一行を言う。Lean の `rulec-recheck` の最後の行は、`OK: every claim this program states was proved, by the theorems of RulecCert.` から `OK, and one thing is stated rather than proved: what each output answers once rounded: the certificate does not say what grid its outputs round to.` に変わる。終わりのコードは 0 のままである。替える前の再検査は、新しい証明書を前と同じに通す(`rounds` と `output_ranges` を読まない)。
- `rulec diff` の `domain` の `output_range` は、範囲を書かない版の出力の範囲を、刻みへ広げたもので比べる(決定の 3)。範囲を書かない版と、刻みへ広げた範囲をそのまま書いた版を比べると、前は `an output changed range: fee 49.0349..83.9 → 49..84` と言って 1 で終わったが、いまは何も変わらないと言って 0 で終わる。
- `check` の答え、生成したコード、`schema`、`api` は変わらない。
- formats の `ranges` の段を直し、`rounds` と `output_ranges` の段を足した。formats とサイトの頭のページ(英日)の再検査の出力を、いまの出力に取り直した。`proofs/README.md`、`proofs/RulecCert/README.md`、サイトの assurance(英日)、スキルのコピーも合わせた。
**確かめたこと**:
- `tests/lean.rs` の `出力の範囲は丸めた答えまで二つの再検査で確かめられる`:0 〜 1,005 円の値を `round half_up(10JPY)` で答える出力を、範囲を書く、`result`、`define`、表で決める四つの規則で、`output_ranges` が 0 〜 1,010 円、`rounds` が `{"fee":"10"}` であること。`ranges` は、範囲を書く規則と `result` の規則では 0 〜 1,010 円、`define` と表の規則では出力を決めるものの 0 〜 1,005 円のままであること。二つの再検査が通り、出力の範囲を一つ確かめたと言うこと。`output_ranges` を 0 〜 1,005 円(出力を決めるものの範囲)と 0 〜 1,000 円に狭めた証明書を、二つとも同じ文で通さないこと。`output_ranges` を 0 〜 1,000 円に狭め、`fee` の刻みを `rounds` から外した証明書と、`rounds` を丸ごと外した証明書も、二つとも同じ文(決定の 7)で通さないこと。`rounds` と `output_ranges` の両方を外した証明書は通り、確かめられない主張の一行を言うこと。
- `tests/lean.rs` の `出力の範囲を丸めの刻みへ広げても証明書のほかの主張は変わらない`:捨てたものの二つの規則で、`ranges` が出力を決めるもの(並びの 1,050 セントと、手数料の 49.0349 〜 83.9 セント)のまま、`output_ranges` が刻みへ広げたもの(1,100 セントと、49 〜 84 セント)になり、二つの再検査が通って出力の範囲を一つ確かめたと言うこと。出力の行に刻みの上にない範囲(1,050 セント)を書いた並びの規則は、二つとも通し、決定の 4 の一行を言うこと。
- `tests/lean.rs` の `木の規則の証明書は証明付きの検査器でもファイルを添えて通る`:木の規則の証明書の全部が、Lean の検査器と `tools/recheck.py` の両方を通り、二つが確かめた出力の範囲の数が同じこと(どちらも 513)。
- `tests/output_range.rs` の `an_output_with_no_range_written_carries_what_decides_it_put_out_to_its_grid`:1 〜 1,000 セントの 3.49% と 49 セントの手数料(`round half_up(1USDc)`)を `define` と表で決める二つの規則で、`output_ranges` と口の範囲が 49 〜 84 セントで、`ranges` が出力を決めるもの(`define` では 49.0349 〜 83.9、表では 49 〜 83.9)のままであること。前は証明書が `ranges` の範囲を出力の範囲として言い、口が 49 〜 83 を渡して、例の 1,000 セントの答え 84 セントを外していた。
- 替える前と後の突き合わせ:木の規則の全部(913 本)の証明書を、替える前と後の rulec で作り(どちらも 554 本)、替える前と後の二つの再検査にかけた。後の証明書は、554 本とも、`rounds` と `output_ranges` が増えたほかは前と同じだった。
| 証明書 | 再検査 | 通った数(Python、Lean) | 出力の範囲を確かめた数 | 確かめられない主張の一行 |
|---|---|---|---|---|
| 後 | 後 | 554、554 | 513、513(`define` 154、表 288、`result` 55、たどる並び 14、開いた値 2) | 無い |
| 後 | 前 | 554、554 | — | 無い |
| 前 | 後 | 554、554 | 0 | 554 本とも |
| 前 | 前 | 554、554 | — | 無い |
後の証明書の `output_ranges` を `ranges` の範囲に戻すと(決定の 3 の前の形)、後の再検査が 7 本を通さなかった(見回し)。
- `lake build` と、ritsu-model の `tests/proofs.rs`(どの宣言も Lean の三つの公理のほかに立たない。名前を挙げて確かめる定理に `rounded_mem_stated`・`output_mem_stated`・`written_output_mem_stated` を足した)。
**見つけて直していないもの**:
- **`rulec schema` と `api` は、範囲を書かない `define` か表の出力に、出力を決めるものの範囲を、wire の整数に 0 の向きへ切り捨てて書いていた。** `協会けんぽ健康保険料.rule` の `支援金現金納付額` は rulec のベクタで 1,599 円を答えるのに、schema の `maximum` と `api` の `range` の `max` は 1598 だった。§15.271 で、口と同じ範囲を書くようにした。
### 15.270 規則のベクタがとるいちばん小さい値といちばん大きい値も、口の例にする(2026-10-30)
**きっかけ**:ritsu の X4(ritsu の DESIGN 7.6)は、規則の数の出力を chobo の振替の額に渡すところで、外れる額を、口の `output_values` が添える例から取る。例は、出力の範囲の両端と、出力を決める行が書く数のそれぞれについて、その値になる最初のベクタの入力だった。rulec は出力の範囲を上から数える(§15.268)ので、端は規則の答えにならないことがあり、`result` と `define` が決める出力には行の数も無い。端にも行の数にも届かないが、ベクタが 0 より下へ行く出力では、外れる入力がベクタにあるのに、X4 は W203(決められない)と言っていた。
**見回し**:証明書の出る 613 本の規則の数の出力 557 のそれぞれで、`rulec vectors` のベクタがとる値のいちばん小さいものといちばん大きいものを、範囲の端と比べた。範囲が 0 〜 2⁶³ − 1 に収まらない出力は 17 で、そのうち 4 つで、ベクタが 0 より下へ行く。コーパスの `order_settlement.rule` の `balance`(範囲 −27,500 〜 25,000、ベクタ −100 〜 449)とその日本語版 `注文の精算.rule` の `残額`、`rate_adjustment.rule` の `rate`(範囲 −50 〜 150、ベクタ −25 〜 54)とその日本語版 `料率の調整.rule` の `料率` である。どれも `define` が決め、端に届かず、行の数も無いので、例が無かった。ほかの 13 は、ベクタが 0 〜 2⁶³ − 1 に収まる。木には、ワークフローがこの 4 つを振替に渡すところは無い。
**原因**:`output_values` は、範囲の端と行の数のほかに例を探さなかった。ベクタは例を探すために同じ関数の中で作っていたが、ベクタがとる値そのものは例にしていなかった。
**決定**:
1. ★ **`output_values` は、前の例(範囲の下の端、上の端、行が書く数)のあとに、ベクタで出力がとるいちばん小さい値といちばん大きい値を足す。** それぞれ、出力を wire の整数にした値がその値になる最初のベクタの入力を添え、前に並んだ値と同じ値は足さない。前の例は中身も並びも変わらない。行がどれも数を書く出力では、ベクタの値はどれも行の数なので、何も増えない。ベクタは前と同じもの(入力の範囲と `constraint` を守る)で、作り直さない。
2. ★ **X4 の選び方は変えない。** 0 より小さい例のいちばん小さいもの、無ければ 2⁶³ − 1 を超える例のいちばん大きいもの、同じ額なら先のものである(ritsu の DESIGN 7.6)。ベクタの最小はどの例以下で、最大はどの例以上なので、前の E203 の例が範囲の端なら、額も入力も前と同じになる。前の例が行の数か反対側の端で、端にも行の数にも当たらないベクタがそれより外へ行くときだけ、例がより外へ動く。木には無い形で、例が外へ動くことは受け入れる。
3. **例の並びは、範囲の端、行の数、ベクタの最小、最大の順にする。** 前の例を動かさないためで、X4 の答えには効かない(X4 は額の大小で選び、同じ値の例は無い)。
4. **Lean の `RitsuCross.amountFits` の定義と定理は変えない。** 例はデータとして受け取り、選び方も変わらない。`Borders.lean` の注を、例にベクタの最小と最大が入る形に書き直した。
**捨てたもの**:
- **前の選び方を先にする二段の選び方**(端と行の数の例を先に見て、無ければベクタの最小と最大)。例に出どころの区別を付け、X4 と Lean のモデルを二段にすることになる。木には例が外へ動く規則が無く、ベクタのいちばん外の額のほうが、例として分かりやすい。
- **ベクタの値の全部を例にすること。** X4 が要るのは、いちばん外の値だけである。口の答えが、ベクタの数だけ大きくなる。
- **出力の区間を、二つの入力を結ぶ `constraint` で狭めること**(§15.268 の見つけて直していないもの)。この節では扱わない。`constraint` が 0 以上に保つ差は、ベクタも 0 以上なので、前と同じく W203 になる。
**変わること**:
- 口の `output_values` の例に、ベクタの最小と最大が増える(同じ値が前に無いとき)。ritsu-cross の X4(`amount_fits`、`amounts_given`)と ritsu-model の突き合わせが、それを読む。
- 範囲の端にも行の数にも届かないが、ベクタが 0 より下(2⁶³ − 1 より上)へ行く出力をワークフローが振替に渡すと、W203 ではなく E203 になり、注がその値になる入力を言う。`constraint holding <= asked` のもとの `result needed = asked - holding - 5`(範囲 −55 〜 95。下の端には `constraint` を破る入力でしか届かない)は、`asked = 0, holding = 0` の −5 で E203 になる。`asked - holding` は、ベクタが 0 〜 100 で W203 のままである。
- 木では、`ritsu check` の答えは変わらない(確かめたこと)。
**確かめたこと**:
- `tests/ports.rs` の `the_examples_of_an_output_are_its_ends_its_rows_and_the_fewest_and_most_of_its_vectors`:`asked - holding - 5` の例が、上の端の 95(`asked = 100, holding = 0`)とベクタの最小の −5(`asked = 0, holding = 0`)の順であること(下の端の −55 には、どのベクタも届かない)。行が −20、10、30 を書き、−20 の行に `constraint` の外でしか届かない表では、例が 30 と 10 だけであること。
- ritsu-cross の `tests/borders.rs`:同じ規則で、`amount_fits` が −5 とその入力で外れ、`amount_unshown` が何も言わないこと。例の並びが、`asked - holding - 5` で 95、−5、`asked - holding` で 100、0、行の数に届かない規則で 30、10 であること。前から決められなかった三つ(`asked - holding` ほか)が、決められないままであること。
- ritsu の `tests/cross.rs` の `x4-E203-vectors`(英日の golden):`x4-W203-unshown` と同じフローで、規則だけを `asked - holding - 5` にすると E203 で、注が `asked = 0, holding = 0` の −5 を言うこと。`x4-W203-unshown` は W203 のままであること。
- ritsu-model の `tests/cross.rs`:書いた規則に `asked - holding - 5` を足し、X4 の額の判定が Lean のモデルと同じ答えになること。`lake build`。
- 替える前と後の突き合わせ(§15.271 と同じもの):言語をまたぐファイルを持つ 162 のディレクトリの `ritsu check`(英日と JSON)で、X4 の答えの変わったものは無い。
**見つけて直していないもの**:
- **X4 は、規則の入力の範囲で作ったベクタから例を取り、ワークフローが規則に渡す値の範囲を見ない**(ritsu の DESIGN 7.6)。`x4-E203-vectors` のフローは `asked` を 50 以上で渡すが、注に挙げる入力は `asked = 0, holding = 0` である。この −5 にはフローが渡す `asked = 50, holding = 50` でもなるので E203 は正しいが、フローの範囲では負にならない出力でも、規則のベクタが負になれば E203 になる。範囲の端と行の数の例でも、前から同じことが起きていた。
- **二つの入力を結ぶ `constraint` は、出力の区間を狭めない**(§15.268)。
### 15.271 `schema`・`api`・生成物・規則の事実も、数の出力に、口が渡す範囲を言う(2026-10-30)
**きっかけ**:§15.269 の見つけて直していないもの。§15.268 と §15.269 で、数の出力の範囲は、出力の行に書いた範囲か、書かなければ出力を決めるものを丸めの刻みへ外向きに広げたもの(`Checked::output_range`)になり、証明書の `output_ranges` と ritsu の口の `output_values` がそれを言う。ところが、ほかに出力の範囲を外へ言うところは、出力を決めるものの範囲(`Checked::ranges`)を wire の整数に 0 の向きへ切り捨てて言い、`result` が決める出力には範囲を言わなかった。
**見回し**:出力について `Checked::ranges` を読むところを全部見て、外へ言うものと中で使うものに分けた。外へ言うものは八つあった。
| | 場所 | どこに出るか |
|---|---|---|
| 1 | `verify.rs` の `prop_body` | `rulec schema` の `out`。MCP のツールの `outputSchema` の `observed`(生成した MCP サーバー三つが持つ同じ JSON) |
| 2 | `codegen.rs` の `range_json` | `rulec api` の出力の `range`(python、typescript、javascript、rust、ruby、php、swift、java、go、numpy、sql の節) |
| 3 | `codegen/connect.rs` の `proto_note` | 生成した `.proto` のレスポンスのフィールドのコメント |
| 4 | `codegen/sql.rs` の `sql_col_doc` | 生成した SQL の二つのファイルの頭のコメント |
| 5 | `doc.rs` の `rule_json` | `rulec doc --format html` と生成したページ三つのスクリプトの `RULE` |
| 6 | `ports.rs` の `column_type` | 規則の事実(dandori の型と規則の答えの確かめ、`rulec api` と同じ並びの列) |
| 7 | `graph.rs` | `rulec graph` の出力のノード |
| 8 | `fixtures.rs` の `beyond_reach` | `replay`・`fixtures`・`verify` の `out_of_reach`(規則が出せない値) |
`rulec diff` の `domain` の `output_range` は、どちらかの版が出力の行に範囲を書くときだけ比べていた(§15.268 の見つけて直していないもの)。中で使うもの(証明書の `ranges` と二つの再検査、検査そのもの、ベクタ、生成コードの計算の上限と入口の確かめ、Kani のハーネス)は、出力を決めるものの範囲が要る(§15.269 の決定の 3)。
証明書の出る規則のうち、外へ言う範囲が口と違ったのは 67 本の 68 出力だった。64 本は `result` が決める出力(一本に一つ)に範囲が無く、3 本の 4 出力は端が刻み一つ内側にあった(`協会けんぽ健康保険料.rule` の支援金の二つは上の端が 1,598 で、口は 1,599。`japan_income_tax.rule` の `surtax` と `所得税.rule` の `復興特別所得税` は下の端が −100,714 で、口は −100,715)。dandori は規則の答えを事実の範囲で確かめる(dandori の DESIGN の範囲の段)ので、`協会けんぽ健康保険料.rule` を呼ぶワークフローは、`支援金現金納付額` が答える 1,599 円(§15.269)を `Dandori.BadResponse` にしていた。木のフローはこの規則を呼ばない。
**原因**:§15.269 は、証明書と口の出力の範囲だけを `Checked::output_range` にし、`Checked::ranges` を読むほかのところは、生成物を変えないために残した(§15.269 の捨てたもの)。ほかの言語の多くは口の `output_values` を読むので、0.37.0 から広がった範囲を読んでいた。dandori は規則の事実(`ColumnType::Num` の `min` と `max`)を読むので、読んでいなかった。
**決定**:
1. ★ **外へ出力の範囲を言うところは、どれも `Checked::wire_range` に聞く。** `Checked::output_range` を、その値の wire の尺度(`Checked::wire_scale`)で wire の整数にしたものである。見回しの 1〜8 と、口の `output_values` の端がそれを呼ぶ。八つを別々に直して食い違わないように、一つの関数にした。入力では `Checked::ranges` と同じである(丸めと `result` の範囲は、出力のものだからである)。
2. **wire の整数に直しても、端は内側に入らない。** 書いた範囲の端は単位の整数(E103)か率の刻みの整数倍(E114)で、広げた範囲の端は刻みに載る。刻みは単位の整数か率の宣言した刻みの整数倍なので、0 の向きへの切り捨てが何も切らない。証明書の出る規則の数の出力 557 に、そうでないものは無かった。
3. ★ **`rulec diff` は、数の出力のどれも `output_range` で比べる。** schema と `api` が範囲を言うので、比べなければ、外に見える形が変わったのに 0 で終わる。答えが一つも動かない版でも、範囲が動けば `domain` に `output_range` が出て、1 で終わる(`amount * 10% + amount - amount` を `amount * 10%` に書き直すと、区間が 0 〜 1,000 から −10,000 〜 11,000 に動く)。
4. ★ **宣言の変化が出力のものだけなら、`rulec diff` の終わりの文は、受け付ける入力の違いを言わない**(`VDiff::accepts_alike`)。前は宣言の変化が一つでもあれば、「両方の版が受け付ける入力のうち、ここに挙げたほかは同じ答えを返します」と言い、`--format markdown` ではそのあとに「受け付ける入力そのものの違いは上のとおりです」と続けた。出力の範囲・型・刻み・丸めと出力の有無は、受け付ける入力を変えないので、この文は誤りで、決定の 3 でよく出るようになる。いまは「ここに挙げた入力のほかでは、二つの版は同じ答えを返します」と言う。答えを決めるものが何も変わらないときの文も同じに分ける。
5. ★ **`rulec graph` の出力のノードと、`replay`・`fixtures`・`verify` の規則が出せない値も、この範囲にする。** どちらも出力の範囲を外へ言っている。刻みへ広げた端の値(支援金の 1,599 円)は出せない値でなくなり、`result` が決める出力の範囲の外の値は出せない値になる。
6. **`doc` の Markdown の出力の表には、書いていない範囲を出さない。** 表は書き手が書いた範囲を言い、書いた範囲では口の範囲と同じである。出すと、数の出力を持つほとんどの規則の `doc` が変わる。ページのスクリプトの `RULE` は範囲を持つが、ページは出力の範囲を見せない(スクリプトが範囲を使うのは、試す値を入力の範囲に収めるところだけである)。
7. **`Checked::ranges` と証明書の `ranges`、二つの再検査、生成コードの計算は変えない**(§15.269 の決定の 3)。生成コードは出力を範囲で確かめない。規則の答えは範囲に入る(§15.269 の定理)からである。
8. **`api` の JSON の形は変えない。** `range` は前から、範囲を言える出力にだけ付くキーである。`result` が決める出力にもそれが付き、ほかの出力では値が変わるだけで、新しいキーも並びの変化も無い。
**捨てたもの**:
- **`Checked::ranges` を刻みへ広げること。** 証明書のほかの主張と軸が、出力を決めるものの範囲として読む(§15.269 の捨てたもの)。
- **八つのところを別々に直すこと。** 外へ言うところを一つ足したときに、また食い違う。
- **`rulec diff` は、書いた範囲だけを比べたままにすること。** `ritsu mod diff` は `api` の範囲の動きを、使う側を壊しうる差の候補として言うのに、`rulec diff` は同じ二つの版で 0 で終わることになる。
- **生成したコードが、出力を範囲で確かめること。** 規則の答えは範囲に入るので、何も止めない。
**変わること**:
- 範囲を書かず `result` が決める数の出力に、`rulec schema` と MCP のツールの `outputSchema` の `minimum` と `maximum`、`api` の `range`、`.proto` と SQL のコメント、ページの `RULE`、規則の事実、`rulec graph` の範囲が付く。範囲を書かず `define` か表が決める出力で、出力を決めるものの端が刻みに載らないものは、端が刻み一つ外へ動く。
- 替える前と後の突き合わせ(確かめたこと)では、木の 983 本の規則のうち 78 本が変わる。証明書の出る 67 本は、`schema`、`api`(英日)、`graph`、`doc --format html`(英日)、`gen` が変わり、変わるのは範囲だけである。`gen` で変わるのは 591 のファイルで、MCP のツール 201、ページ 201、`.proto` 67、SQL 122 である。生成したコードの計算と入口は一字も変わらない。検査を通らない変異の 11 本は、`schema` と `graph` だけが変わる。`check`、`certificate`、`vectors`、`doc` の Markdown は、983 本とも変わらない。
- dandori:規則の事実を読むので、`result` が決める出力の範囲を知るようになる。
- 生成したコードは、規則の答えを範囲で確かめる。木では 258 本のフローのうち 4 本の `build`(Temporal の三つの言語、Argo、pydantic-graph。Step Functions と durable functions は、この 4 本では規則に `lambda` が無いので組まない)が変わり、`is_total_outputs` に `0 <= total <= 2410000` の比べが足され、Worker Deployment Versioning のビルド ID が変わる。
- 検査では、その出力を範囲のあるところ(規則の入力、タスクの引数、レコードのフィールド、ワークフローの出力)に渡すと、前は範囲が分からないとして W104 だったものが、収まれば何も言わず、収まらなければ E014 になる。答えが 0 〜 1,000 円の手数料を `<=500` の出力に渡すフローは、替える前は W104 で通り、替えた後は E014 で止まる。前の範囲より刻み一つ広がった 3 本の 4 出力でも、渡す先の端が前の端と同じなら E014 になる。木の 258 本のフローの `check` は変わらない。
- 図の HTML は、規則の答えが範囲の外になって `Dandori.BadResponse` で終わるシナリオを足す。上の 4 本のページはシナリオが一つ増え、それを子のフローとして呼ぶ 2 本のページは、載せた規則のページの `RULE` だけが変わる。`scenarios`(規則を走らせて答えを得る)は 258 本とも変わらない。
- `ritsu check` の X2(規則の前提):`result` が決める出力を別の規則の前提に渡すところは、前は範囲が分からないとして W201 だったものが、範囲で示せれば何も言わず、破る組み合わせがあれば E201 になる。木の 162 のディレクトリの `ritsu check` は変わらない。
- kaname:`<規則>.outputs` の範囲は前から口の `output_values` から読んでいて、83 本の `.kn` の `check`、`api`、`doc`、`vectors` は変わらない。`ritsu gen` は、上の 4 本のフローを持つ 8 つのディレクトリで、フローのコード(範囲の比べとビルド ID)だけが変わる。
- `ritsu mod diff`:新しい版で `result` が決める出力の範囲が動くと、規則のファイルの `api` の差に、`/{python,…,sql}/outputs/<出力>/range/min` と `max`(Go は `/go/output_fields/<出力>/range/…`)の「変わった」が、使う側を壊しうる差の候補として出る。前は何も言わなかった。
- `rulec diff`:範囲を書かない出力の範囲が動いた二つの版で `an output changed range` を言い、1 で終わる。答えが同じでもそうなる。出力の宣言だけが変わった版の終わりの文が変わる(決定の 4)。ritsu の `ritsu mod diff` の golden(`diff-pricing-2.0.0`、`report-break`)は、出力の名前を替えた版の文が変わった。
- `rulec graph` のノードの範囲が口の範囲になる。規則が出せない値(`verify` と `replay` の見出しの下の行と JSON の `out_of_reach`、`fixtures lint` が形式の問題と分けて挙げる値)には、`result` が決める出力の範囲の外の答えが入り、刻みへ広げた端の答えは入らなくなる。照合の数、一致の数、終了コードは変わらない(範囲の外の答えは、もとから規則の答えと違うので不一致である)。サイトの実演では、英語の比べ方のページの `verify`(海外あてに 1USD 多く取る旧実装のふりは、署名つきで海外へ送る大きい荷物に 73USD を答え、規則のいちばん高い 72USD を超える。3 件)と、日本語のシナリオのページの記録(遠隔地あての 2kg 超の送料が 2,000 円の 7 件。規則は 1,800 円まで)に、その行と一覧が出るようになった。
**確かめたこと**:
- `tests/output_range.rs`:外へ範囲を言うところの全部(証明書の `output_ranges`、規則の事実、`output_values`、schema、MCP のツールの `outputSchema`、`api` の 11 の節、`.proto` と SQL のコメント、ページ三つの `RULE`、`graph`)が同じ範囲を言うことを、書いた範囲(0 〜 2,000)、`result` が決める出力(0 〜 1,000)、`define` と表が決めるカードの手数料(49 〜 84 セント)で確かめる。`replay` が、範囲の端の答えを出せる値に、外の答えを出せない値に数えること(カードの手数料の 84 と 85、`result` の 1,000 と 1,001)。`rulec diff` が、範囲を書かない出力の範囲の動きを言い、答えの同じ版(`amount * 10% + amount - amount`)でも 1 で終わること。
- `tests/ports.rs` の `口の事実はapiと証明書とschemaが言うことと同じ`:コーパスと試験の規則の全部で、数の列のどれも、`api` の 11 の言語の範囲が事実の範囲と同じで、出力では証明書の `output_ranges` を wire の整数にしたものと、口の `output_values` の端とも同じであること。
- `tests/vdiff.rs`:予算を超えた比べで、出力の範囲を動かさない変更は 0 で終わり、動かす変更(送料の 1,800 円を 2,000 円に)は、領域を出さずに `output_range` を言って 1 で終わること。サイトの比べ方のページ(日本語)の実演に、出力の範囲の行を足した。
- `tests/verify.rs` の `文書に載せた実演は_いまの出力と一致する`:英語の比べ方のページと `docs/formats.md` の `verify` の実演に、範囲の外の答えの行と `"out_of_reach":{"fee":3}` を足した。日本語のシナリオのページの `fixtures lint` と `replay` の実演は試験が守っていないので、記録(`送料.rule` の遠隔地の 2kg 超を 2,000 円にした版のベクタとその答え)を作り直し、替える前のバイナリがページと同じ出力を出すのを見てから、替えた後の出力を貼った。英語のシナリオのページの実演(`parcel_rate` の記録)は、二つのバイナリで同じだった。
- ritsu の `tests/modules_diff.rs` の `a_result_that_comes_to_more_changes_the_range_of_the_interface`(英日):`result total = subtotal + 395` を `+ 495` にした版で、振る舞いの差が `output_range` の 395..100395 → 495..100495 を言い、インターフェースの差が 11 の言語の `range/min` と `range/max` の 22 件で、どれも壊しうる差の候補であること。ritsu の `doc-order_total.html` と `report-moved`(英日)の golden の範囲が変わった。
- ritsu の `tests/cross.rs` の `x2_a_rules_preconditions_where_a_flow_calls_it` に `x2-result-E201`(英日)を足した。ほかの規則の `result`(範囲を書かない `price * items`)の答えを、前提 `asked <= paid` を持つ規則に渡すフローが、`result` の範囲 0 〜 9,000 で E201 になり、dandori の W104 は出ない。替える前のバイナリでは、同じフローが W104 二つと W201 だった。手数料(答えが 0 〜 1,000 円)を `<=500` の出力に渡すフローが、替える前は W104、替えた後は E014 になることも、二つのバイナリで確かめた。
- 替える前と後の突き合わせ:この変更の前の木で組んだバイナリと、替えた後のバイナリを、木の全部の `.rule`(983 本:`check` の JSON と日本語、`certificate`、`vectors`、`schema`、`api` の英日、`graph`、`doc` の Markdown と HTML の英日、`gen` の全部のファイル)、`.flow`(258 本:`ritsu dandori` の `check` の JSON と日本語、`scenarios`、`doc` の Markdown と HTML、七つの出力先の `build` の全部のファイル)、`.kn`(83 本:`ritsu kaname` の `check` の JSON と日本語、`api`、`doc`、`vectors`)、フローか `.kn` か `ritsu.mod` を持つ 162 のディレクトリ(`ritsu check` の JSON と日本語、`ritsu gen` の全部のファイル)にかけて比べた。変わったものは上の「変わること」のとおりで、ほかには無い。`verify`・`replay`・`fixtures lint` は相手(アダプタか記録)が要るので、この突き合わせには入れず、サイトの実演で確かめた(上)。
**見つけて直していないもの**:
- **`rulec diff --fixtures` は宣言の変化を言わない。** 記録の答えを比べるだけなので、範囲だけが動いた版は、記録の答えが同じなら 0 で終わる。範囲の動きは、記録の無い比べか `ritsu mod diff` が言う。
### 15.272 `api` の Connect のフィールドに、`.proto` での番号を出す(2026-10-30)
**きっかけ**:ritsu の `mod diff` は、二つの版の `api` の JSON を比べ、使う側の検査によらず呼び出しを壊す差を E310 として言う(ritsu の DESIGN 17.9.5)。列挙の値は `enums` に番号があるので比べられたが、`request_fields`、`response_fields`、`element_fields`、`sequences` のフィールドには名前と型しか無かった。rulec は Connect の `.proto` で、フィールドに書いた順の番号を振る。リクエストは入力が 1 からで、並びがそのあとに続き、レスポンスは出力が 1 から、要素のフィールドも 1 からである(§15.160、§15.219)。途中に入力を足すと、後ろの入力と並びの番号がずれる。protobuf のバイナリの形はフィールドを番号で送るので、前の版から生成したコードが書いたメッセージを、新しい版は別のフィールドとして読むか、読み飛ばす。`api` の JSON の差には、それが出なかった。
**決定**:
1. **四つの段のフィールドのどれにも `number` を足す。** 値は規則が Connect で話す `.proto` での番号で、`field` のすぐ後ろに置く(`.proto` の行 `int64 subtotal = 1;` と同じ並び)。規則から生成する `.proto` なら書いた順の番号で、契約のサービスを実装する規則(§15.274)なら契約の番号である。リクエストの並びのフィールドにも付ける。`sequences` の項は、その並びを運ぶリクエストのフィールドの番号を `request_fields` で引けるので、重ねて書かない。
2. **生成する `.proto` の番号の振り方は変えない。** 並びは入力のあとに続くので、並びをたどる規則に入力を足すと、どこに足しても並びの番号がずれる。並びの番号を入力から離せば(たとえば 1000 から振れば)これからは動かないが、いま並びをたどる規則の番号が一度に動き、それ自体が呼び出しを壊す。番号を動かさずに値を足すには、番号を契約に持たせる(`implements`、§15.274)。契約を持たない規則で順を変えるか消すなら、版を上げて別のサービスにする。ritsu の E310 がそう言う。
**変わること**:
- `rulec api` の JSON の Connect のフィールドに `number` が増える。`check` の答え、生成したコード、`.proto` は変わらない。
- formats の `connect` の段に `number` と番号の振り方を書き、例に番号を足した。スキルの中のコピーも合わせた。
**確かめたこと**:
- `tests/connect.rs` の `connect_のフィールドの番号は生成した_proto_と同じ`:並びをたどらない規則(`送料.rule`)、一つたどる規則(`全国運賃.rule`)、二つたどる規則(`order_settlement.rule`)で、リクエスト、レスポンス、要素のフィールドの番号が、生成した `.proto` を ritsu-proto で読んだ番号と同じであること。レスポンスの `trace`(100)は比べない。
### 15.273 口に、入力を限ったときの数の出力を聞く `output_values_over` を足す(2026-10-30)
**きっかけ**:ritsu の X4(ritsu の DESIGN 7.6)は、規則の数の出力を chobo の振替の額に渡すところで、口の `output_values` が答える出力の範囲と例で、額を chobo が受け取るかを決める。`output_values` は規則の入力の全部で数えるので、ワークフローが `match` で返却の分岐を外してから額を渡しても、返却の行(−20)が例になって E203 だった。dandori が、振替に至る実行で呼び出しが規則に渡しうる値を口で渡すようにした(dandori の DESIGN 18 章)ので、rulec には、その値の中で数の出力が何になるかを答える口が要る。
**原因**:入力を限って読み直す仕組み(`over.rs` の `outputs_over`)は、列挙と真偽の出力にしか答えなかった。
**決定**:
1. **口に `output_values_over(rule, output, ranges)` を足す。** `ranges` は `outputs_over` と同じに読む(wire の整数で、日付は日の番号、列挙は宣言の順の値の位置で入力に書いた `range` が入れるものだけ(§15.251)、真偽は 0 と 1、同じ入力を二度書けば両方の値)。答えは `output_values` と同じ形(最小、最大、行の数、例)である。`output_values` に引数を足さなかったのは、いまの呼び出し(kaname、sekisho の試験、dandori の `NoRules`、試験)を変えないためである。ritsu-ports の既定は、決められないと答える。
2. ★ **宣言より狭く限った入力が無ければ、`output_values` の答えにする。** 狭く限った入力があれば、規則を限って読み直し、出力を決めるものを読む。出力の行に書いた `range` は読まない。書いた範囲は規則が入力の全部について約束するもので、一部の入力については何も言わないからである。そのため、入力を一つも狭めない呼び出しは書いた範囲を答え、狭める呼び出しは出力を決めるものを答える(返却の規則の出力に `range >=-500 <=500` と書けば、入力の全部では −500〜500、`charge` に限れば 300〜350)。
3. ★ **最小と最大は、二つを重ねたものにする。**
- 限って読み直した検査が数える、出力を決めるもの(表の行、名前の `define`、`result`)の届く範囲。§15.268 の `output_ranges` が、書いた範囲で上書きする前の値(丸めの刻みへ外向きに広げたもの)を `Checked::reaches` に残すようにした。
- 出力を決める表の行のうち、限った入力のどれかが届きうる行(`region::live_rows`)が書く値の外包。行ごとの値は、型の検査が列の値を求める関数(`Checked::row_bound`。型の検査もこれを使うようにした)で求め、丸めの刻みへ外向きに広げる。
どちらも無ければ、入力の全部での範囲(`Checked::wire_range`)にする。重ねて空になれば、決められないと答える。
4. **数は、届きうる行がどれも数を書くとき、その数にする**(`row_numbers`。`output_values` も同じ関数で、全部の行を数える)。
5. **例は、`output_values` と同じ取り方(§15.270、`examples_of`)で、範囲の中のベクタから取る。** 範囲の端、行の数、ベクタの最小と最大の順である。ベクタは限って読み直した規則で作り、範囲の中のもの(`Held::takes`)だけを使う。範囲の中のベクタが最小と最大の外になれば、解析と評価器が食い違うので、決められないと答える。
6. ★ **並びをたどる規則と、限ると検査が通らない規則は、入力の全部での最小と最大を答え、例だけを範囲の中のベクタから取る。**
7. **決められないと答えるのは**、数でない出力、`T?` の出力(§15.249)、規則の入力でない名前、範囲の中に規則の入力が無いときである。
8. **`outputs_over` と `output_values` の答えは変えない。** 限り方を `held_of`、`Held::takes`、`decided_by` に分けて `values_over` と分け合い、`output_values` を `output_values_of`、`row_numbers`、`wire_outputs`、`examples_of` に分けた。
**捨てたもの**:
- **`output_values` に引数を足すこと。** 呼び出しが全部変わる。
- **狭めたときも、書いた範囲を読むこと。** 書いた範囲が 0 より下へ届けば、限っても X4 が決まらない(`charge` に限った返却の規則が −500〜500 のままで、W203 になる)。
- **狭めないときも読み直すこと。** 出力の行に範囲を書いた規則の答えが変わり、X4 の前の答え(§15.270 の例を含む)も動く。
- **例を、範囲の中で新しく探すこと**(ベクタに無い入力を作って評価する)。ベクタは入力の範囲と `constraint` を守り、評価器を通った入力で、例はそこから取る(§15.270)。範囲の中にベクタが無ければ例は無く、X4 は W203 になる。
**変わること**:
- 口に `output_values_over` が増え、ritsu-cross の X4 が、dandori のたどった呼び出しごとに聞く。
- `Checked` に `reaches` が増える。証明書と生成物には出ない。
- 替える前と後のバイナリで、木の 983 本の `.rule` の `check`(JSON)と `certificate`、83 本の `.kn` の `ritsu kaname check`(JSON と日本語)、言語をまたぐファイルを持つ 162 のディレクトリの `ritsu check`(JSON と英日)を比べた。変わったのは、`crates/dandori/tests/flows` の `ritsu check` の日本語の W201 の注の 4 行(名前の後ろの空白。ritsu-cross の直し)だけだった。型の検査が `row_bound` を使うようにしたことで、変わった診断は無い。
**確かめたこと**:
- `tests/ports.rs` の `a_numeric_output_over_ranges_is_what_decides_it_there`:`handback`(−20)、`workshop`(30)、`talk`(80)で席の数を決める表を、`workshop` と `talk` に限ると 30〜80 で、数も例もその二つであること。`handback` と `talk` を別々に挙げると −20〜80 であること。全部の値か、値の外まで含む範囲に限ると、`output_values` と同じ答えであること。書いた範囲 −500〜500 の返却の規則を `charge` に限ると 300〜350、さらに `late` を true に限ると 350 であること。`asked - holding` を `asked` 50〜100 に限ると 0〜100、0〜50 に限ると −50〜50 であること。−20 の行に `constraint` の外でしか届かない表は、入力の全部では −20〜30 で、どれかの入力を宣言より狭めると 10〜30 であること。決められないと答える三つ(数でない出力、入力でない名前、範囲の中に入力が無い)。
- `tests/ports.rs` の `a_numeric_output_over_ranges_holds_what_every_input_inside_them_comes_to`:コーパスと試験の規則の数の出力を、列挙と真偽の入力は値一つずつに、両端のある数と日付の入力は範囲の下半分と上半分に、数と日付の入力はベクタのまわりの数歩に限って聞く。歩ける大きさの 579 の限り方(97 本の規則で 647 の問い)で、限った入力の全部を評価器に流し、評価器の答えがどれも最小と最大の中にあり、数を言うときはその中にあり、例の入力が範囲の中にあって言う額になることを確かめた。決められないと答えた問いは無く、385 の問いで入力の全部での範囲より狭かった。歩くには広すぎる 793 の限り方は聞いていない。
- ritsu-cross の `tests/borders.rs`、ritsu の `tests/cross.rs`、ritsu-model の `tests/cross.rs`:根の DESIGN 7.6 の記録のとおり。sekisho の `walk` と `diff`(`outputs_over` を使う)、kaname の試験(`output_values` を使う)も通った。
**見つけて直していないもの**:
- **二つの入力を結ぶ `constraint` は、限っても出力の区間を狭めない**(§15.268)。`asked - holding` を、`asked` と `holding` に同じ数(0〜50)を渡して限ると −50〜50 で、ベクタは `constraint` を守って 0 以上なので、X4 は W203 のままである。
### 15.274 規則が、契約の Connect のサービスを実装する(`implements proto`)(2026-10-30)
**きっかけ**:§15.272 で `api` の Connect のフィールドに番号を出し、ritsu の `mod diff` が、番号の動いたフィールドを E310 として言うようになった。規則から生成する `.proto` は、規則に書いた順に番号を振る(リクエストは入力が 1 からで、並びがそのあとに続く。レスポンスは出力が 1 から)。途中に入力を足すと後ろの番号がずれ、並びをたどる規則では、入力をどこに足しても並びの番号がずれる。フィールドの番号を動かさずに値を足す道は無く、直し方は、版を上げて別のサービスにすることだった(列挙の値の番号だけは、`import proto` で契約から読めば動かない。§15.59)。protobuf の契約は、本来番号を持ち続ける側である。フィールドを足せばまだ使っていない番号を付け、消せば番号を `reserved` にする。dandori のワークフローは `implements` で契約のサービスを実装できる(dandori の DESIGN 1.14)が、規則にはその道が無かった。
**原因**:規則が Connect で話す名前と番号を決めるのは、規則から生成する `.proto` だけだった。
**決定**:
1. ★ **一行で書く。** `implements proto "<ファイル>" <サービス>`。パスは規則ファイルのある場所からたどり、サービスは名前か、ファイルの package を付けた名前で書く(`shape` のメッセージと同じ)。行頭の語で、どのブロックの外にも書け、置く場所は検査しない。予約語にはしない。`implements : bool` は、`inputs` の下では入力の宣言である(`open`、`prerequisites` と同じ)。一つの規則に一行で、二行あれば E013 である。`enums` のファイル(E066)と寄与のファイル(E067)には書けない。
2. ★ **契約のサービスは、規則から生成する `.proto` と同じ形でなければならない。** 違ってよいのは、package、サービス・メソッド・メッセージの名前、フィールドの番号と並び、コメント・`reserved`・ほかのオプション、メッセージを置くファイル(契約が import するもの)と入れ子である。フィールドは別名で引き当てる。型は、真偽が `bool`、日付と文字列が `string`、ほかの数が `int64` で、列挙は規則が `import proto` で読んだ列挙そのもの(同じファイルの同じ完全名)である。リクエストと要素のフィールドはいつも `optional`、答えのフィールドは `T?` の出力のときだけ `optional` である。答えには `repeated <メッセージ> trace` が要り、そのメッセージは `string table`・`int32 row`・`string label` の三つだけを持つ(名前と番号は自由)。メソッドは一つで、単項で、`option idempotency_level = NO_SIDE_EFFECTS;` を持つ。契約は proto3 で、`json_name`・`oneof`・`map` のフィールドは読まない。
- 型の対応は、生成する `.proto` が書く型と一つの関数(`implements::Kind`)にした。生成と突き合わせが別々に型を決めると、食い違ったときに、検査を通った規則が契約と違う型で話す。
- ★ 五つの厳しさ(`NO_SIDE_EFFECTS`、メソッドは一つ、整数は `int64` だけ、`trace` が要る、proto3 だけ)は緩めない。生成するサーバー、`rulec test` のランナー、dandori が、それぞれ頼りにしている。`int32` を受け付けると、protobuf の JSON の形で 64 ビットの整数だけが文字列になることに乗っている、`api` の `json_int64: string` と dandori の読み方が崩れる。
3. ★ **合わないところは E076 で、一つずつ言う。** 規則の名前のこと(契約に無い入力・並び・要素のフィールド・出力、型の違い、`optional` の違い、契約から読んでいない列挙)は、その宣言の行で言う。契約にしか無いもの(契約にしか無いフィールド、`trace` の無い答えか形の違う `trace`、規則のサービスの形でないサービス、proto3 でない契約)は、`implements` の行で言う。注は、契約のファイルの何行目に何があるかと、契約に足す行(`optional bool express = <番号>;`)を言う。
- 契約にしか無いフィールドも、警告ではなくエラーにする。警告では、契約が先に育つあいだ、サービスは送られた値を黙って捨て、呼ぶ側は書かれない値を 0 と読む。受け取るだけで使わない値は、規則に `T?` の `contract_only` の入力として書くか、契約から消して番号を `reserved` にする。
- 契約から読んでいない列挙は、列挙ごとに一度だけ、それを運ぶ最初の値で言う。同じ列挙の入力と出力で何度も言うと、直すことは一つなのに数が増える。
- proto3 でないファイルは、ファイルごとに一度だけ言う。
4. ★ **契約のファイルを読めないこと、サービスが無いこと、行の形が違うことは E013 にする。** E013 の台帳の文を「規則が外から読むものが無いか、読めない」に広げ、外から読む六つの行(組み込みの名前空間、`import proto`、`import jsonschema`、`import enum`、`shape` と `from`、`implements`)を挙げた。前の文は `import` の三つしか言わず、`import enum` と `shape` の E013 が文に無かった。無いサービスを指せば、注にファイルのサービスを並べる。
5. **型の検査を通らない規則は、契約と比べない。** 値が通信で何の型になるかが決まらないからで、E076 は最初の誤りを言い直すだけになる。型の誤りを直すと比べる。
6. ★ **`gen` は、契約を生成する `.proto` の代わりにする。** `proto/rulec/<別名>/v<版>/` を書かない。契約とそれが import するファイルを、`import proto` の契約と同じに(§15.160)、package が決めるパスへ一字も変えずにコピーする。import は `check` が契約を読んだのと同じたどり方(`implements::Imports`。モジュールの根から、ritsu のモジュールなら依存の根から)でたどるので、`check` が規則と比べたファイルが、そのままコピーするファイルになる。契約が BSR のモジュールのファイルを import していれば、`proto/buf.yaml` と `buf.lock` に依存を書くのも `import proto` の契約と同じである(§15.161)。
- サービスは契約のパス(`/<package>.<サービス>/<メソッド>`)に立ち、Python のサービスとランナーは、protobuf-py と Connect のプラグインが契約から書く stub を読む。サービスのファイルの stub を自分の `.proto` のときと同じ `pb` で読み込み、ほかのファイルの stub(別のファイルのメッセージ)は平たい名前で読み込む。クラスとメソッドの名前は、プラグインと同じ付け方(`escape_class_name`、`escape_public_identifier`、メソッドの snake case)で綴る。入れ子のメッセージは親のクラスの中にある(`pb.CartRequest.Line`)。
- stub のモジュールのパスは、ファイルの名前の英数字と `_` のほかの文字を `_` にする(`member-shipping.proto` は `member_shipping_pb`)。プラグインがそうするからである。規則の別名は英数字と `_` だけなので、規則の `.proto` の stub の名前は変わらない。`import proto` で読む契約のファイルの名前にほかの文字があると、前は Python のサービスが `from stubs.shop.v1 import member-tier_pb` と書き、構文の誤りで Connect の四つの段のどれも立たなかった(0.38.0 で再現した)。いまはプラグインの書く `member_tier_pb` を読む。
- `rulec test` の Connect の段は、ランナーがあるかで走らせる(`connect_runner_of`)。stub は `proto/` の全体から作るので、`.proto` のパスを綴る要が無くなった。
- `gen` は出力先のファイルを消さない。生成した `.proto` をコピーして同じパスへ移った規則では、契約のコピーがそのパスに書かれる。別のパスの契約へ移った規則では、前の `gen` が書いた規則の `.proto` が残るので、`gen` がそれを、もう生成しないファイルとして挙げる(§15.275)。
7. ★ **通信に乗る列挙は、`import proto` で読んだものだけにする。** 規則が自分で宣言した列挙、`import jsonschema` と `import enum` の列挙は E076 である。値の番号を契約が決めるためで、規則が自分の `.proto` を書かない以上、ほかの列挙には番号を付ける場所が無い。
8. ★ **`rule … v2` の版と契約の package は比べない。** `rule` の版は規則の振る舞いの版で、通信の版は契約の package が持つ。
9. ★ **`api` の `connect` は契約の名前と番号を言い、`contract` を足す。** `proto`、`package`、`service`、`method`、`path`、`request`、`response`、並びの `message`、フィールドの `number`、`python` の `client` が契約のものになる。`contract` は、規則が書いたファイル(`file`)と `gen` のコピー先(`proto`)で、`import proto` の列挙の `contract` と同じ形にした。`implements` を書かない規則には出さない。口の `Connect`(`connect_facts`)の `path` も契約のものになり、dandori、sekisho、sakai はそれを読む。
- 型の名前は、実装するサービスの package からの名前にする(同じ package の列挙は `Tier`、ほかの package のものは完全名)。生成する `.proto` を書く規則では、これまでどおり完全名である。
10. ★ **規則の口の `References` に `implements` を足す。** 指す先は契約のファイルとサービス(`proto "<パス>" service <サービス>`)で、dandori の `implements` と同じ形である。これが無いと、契約だけを使う依存に W301(どのファイルも使っていない依存)が出る。yuen の `trace` と `affected` も、契約から規則へたどれる。
11. ★ **承認者のページに一行足す。** 入力と出力が契約のサービスに合わせてあり、Connect で呼ぶときの名前と番号はその契約のもので、`rulec check` が一致を確かめる(ずれていれば E076)、と書く。`import proto` の列挙の持ち主を書く一行と同じ扱いである。
12. ★ **入れ子の列挙を `import proto` で読むと、生成した `.proto` が buf を通らなかったのを直す**(0.38.0 で再現した)。`message Member { enum Tier {…} }` の `Tier` を、生成した `.proto` は親のメッセージを抜いて `shop.v1.Tier` と書き、`buf build` が型を知らないと言って止まった。Python のサービスも `shop_v1_member_pb.Tier` と引き、protobuf-py が列挙を置く親のクラスの中(`Member.Tier`)を見ていなかった。いまはファイルの package からの名前(`Member.Tier`)を持ち、`.proto` は `shop.v1.Member.Tier`、Python は `shop_v1_member_pb.Member.Tier` と書く。契約の型との突き合わせは、入れ子の名前を正しく引かないと成り立たないので、ここで直した。
13. ★ **`import proto` の契約のコピー先が、規則から生成する `.proto` と同じパスで、中身が違えば、`gen` を止める**(0.38.0 で再現した)。前は片方が黙ってもう片方を上書きした。§15.160 は、中身の違う二つの契約が同じパスに来ると止めたが、契約と規則から生成する `.proto` の重なりは見ていなかった。生成した `.proto` をコピーして契約にし、`import proto` で列挙を読んで、`implements` をまだ書いていない途中の状態が、ちょうどこれになる。`gen` は、コピー先が規則から生成する `.proto` と同じパスで二つの中身が違うと言い、`implements` を書くよう案内して 1 で終わる(契約が依存のファイルなら 2。ritsu の DESIGN 17.6)。中身が一字も違わなければ止めない。一つの `gen` に二つの規則を渡し、一方のコピーする契約と、もう一方の生成する `.proto` が重なるときも同じである。
14. **台帳の再現に `.proto` のファイルを置ける。** E076 の再現は契約と規則の二つのファイルで、`docs/codes*.md` は `.proto` のファイルを ```` ```proto ```` で囲む。
**捨てたもの**:
- **規則の `.proto`(`rulec.<別名>.v<n>`)を書いたまま、番号だけを契約から取ること。** パスが契約と違うので、契約から作ったクライアントが届かない。
- **`rule` の行に書くこと(`rule x v1 implements …`)。** dandori は `use proto` でファイルに別名を付け、`workflow` の行でその別名からサービスを指す。rulec の `rule` の行は名前と版だけで、ファイルを書く場所が無く、ファイルに別名を付ける仕組みも無い。dandori の形にすると、その仕組みを rulec に持ち込むことになる。
- **契約にしか無いフィールドを警告にすること**(決定の 3)。
- **規則の自前の列挙を、契約の同じ名前の列挙と黙って合わせること。** `import proto` の E032 と E033(値ごとに、規則の行が答えるか)を飛ばすことになる。
- **`trace` を省けるようにすること。** `rulec test` のランナーが、答えた行を評価器と比べられなくなる。rulec が生成するものは、どの呼び方でも答えた行を返す(§15.112)。
- **`int32` などの整数を受け付けること**(決定の 2)。
- **メソッドを選べるようにすること(`<サービス>.<メソッド>`)。** 生成するサーバーは、ほかのメソッドを実装できない。connect-python のアプリケーションは、サービスの全部のメソッドを持つ実装を求める。
- **rulec の `proto::read` を広げること。** ritsu-proto がすでにサービス、メソッド、番号、入れ子、import の解決を読む。同じ読み手を使えば、dandori・sekisho と読み方が割れない。
**変わること**:
- `implements` を書かない規則の `check`、`gen`、`api` は変わらない(確かめたことの最後)。宣言の語で始まらない行(E004)と、その位置に書けない語(E005)の注が並べる、書ける語に `implements` が増える。
- 台帳に E076 が増え(139 件)、E013 の文が広がった。
- 入れ子の列挙を `import proto` で読む規則の、生成した `.proto` と Python のサービスが変わる(決定の 12)。名前に英数字と `_` のほかの文字がある契約から `import proto` で読む規則の、Python のサービスが変わる(決定の 6)。下の突き合わせの規則には、どちらの規則も無かった。
- `import proto` の契約のコピー先が規則の `.proto` と同じパスで、中身が違うと、前は通った `gen` が止まる(決定の 13)。リリースノートの「前は通っていた実行が落ちうる変更」に書く。
- ritsu の `mod diff` と `mod report` は、変わっていない規則も、モジュールの `.proto` が変わったときに比べ、E310 の注を契約の番号に合わせて言い分ける(ritsu の DESIGN 17.9.5)。ritsu-cross の E310 の台帳の文を、いまの姿に書き直した。
- sakai の E302 の注が、`implements` の規則の package は契約のものだと言う。sakai は規則の `implements` も、ワークフローの `implements` と同じ E208 で確かめる。実装するサービスは、規則を持つコンテキストの公表された言語の `open host service` に無ければならない。一つの package の二つのサービスを二つの規則が実装するときは、契約の `.proto` を公表された言語にし、E004 と E006 の注がこの形を案内する(sakai の DESIGN 26 章)。
- 文書:`docs/reference.md` に §3.4 を足し、`formats.md` の `connect` の段、`generated-code.md` の Connect の節、互換のページ(英日。1.x が残す書き方と、予約語にしない語)、サイトの rulec の `tour`・`generate`・`checks`・`assurance`・`index`(英日)と、変異の本数を描く `assurance` の図、根のサイトの `modules`(英日)、プレイグラウンドの `words.json`、サイトのレクサの行頭の語を合わせた。`AGENTS.md` には一行を足し、詳しいことは §3.4 に任せた。スキルの `SKILL.md`(`AGENTS.md` から組み立てる)は 500 行の上限の手前(499 行)だったので、同じ行数に収めるため、`AGENTS.md` の 6 章の一つの段落を、語を変えずに折り返し直した。スキルの説明が挙げるコードの範囲を E001-E076 にし、スキルのコピーを作り直した。
**確かめたこと**:
- `tests/implements.rs`(14 件):
- コーパスの規則は何も言われない(`implements` を書かない規則の `check` は変わらない)。
- 番号を違えて振った契約(リクエストのフィールドの並びと番号が違い、あいだに `reserved` があり、答えのフィールドも別の番号と並びで、`trace` は 15、列挙の値の一つは 7、同じファイルにもう一つのサービスがある)を実装する規則が、英日で通る。サービスを package を付けた名前で書いても通る。
- `gen` が `proto/rulec/` を書かず、契約を package のパスへ一字も変えずにコピーし、サービスとランナーが契約のサービス・メソッド・メッセージを使う。
- `api` の `connect` のパス・package・サービス・メソッド・メッセージが契約のもので、どの `number` も、契約を ritsu-proto で読んだ番号と同じである。`contract` がファイルを言う。
- 並びが入れ子のメッセージで、`trace` の行が別の package の別のファイルのメッセージである契約(`cart.proto` と `rows.proto`):二つのファイルがそれぞれの package のパスへコピーされ、番号はメッセージごとのものになる。
- 規則から生成した `.proto` をコピーして契約にし、`implements` を書く移り方:途中の状態(列挙だけをコピーから読む)で `gen` が英日の文で止まり、行を書くと `api` は `contract`(と列挙の `contract`)のほかは前と同じで、`gen` は同じ `.proto` をコピーする。
- E013 の六つ(ファイルが無い、サービスが無い、行の形の三つ、二行)と、E076 の 11 の形(入力が無い、契約にしか無いフィールド、型、`optional` の二つ、列挙、`trace` が無い、`trace` の行の形、サービスの形、`json_name`、proto2)を英日で、言う行と注とともに確かめる。どの形も E076 はちょうど一つである。
- 型の通らない規則は契約と比べない。`enums` のファイルは E066、寄与のファイルは E067。`implements` という名前の入力が通る。`fmt` が行をそのまま残す。承認者のページの一行(英日)。口の `References` の `implements` と、口の `Connect` のパスが契約のものであること。
- `tests/connect.rs` の `契約を実装した規則は契約の番号で話す`(tools の段。buf、二つのプラグイン、connectrpc、uvicorn、mypy が要る):上の二つの契約を実装する規則を `gen` し、`rulec test` が規則ごとに Connect の四つの段(ASGI と WSGI、POST と GET)を走らせて評価器と一致する。生成した四つの Python のファイルが `mypy --strict` を通る。元の契約のディレクトリから別に作ったクライアントが、protobuf のバイナリの形でベクタの全部を送り、答えが評価器と同じである。要求のバイトは契約の番号を持つ(`member` が platinum(7)だけの要求は `10 07`)。
- `tests/proto.rs` の `入れ子の列挙はメッセージの名前で引く`:決定の 12。生成した `.proto` の型、import、Python のサービスの引き方、`api` の型。`契約のファイルの名前の記号はstubの名前で下線になる`:決定の 6 の stub の名前。`member-tier.proto` の列挙を読む規則のサービスが `member_tier_pb` を読む。同じ規則を 0.38.0 と替えた後のバイナリで `gen` し、生成物の Python と `.proto` とベクタで `rulec test` を走らせると、前は Connect の四つの段が `SyntaxError` で落ち、後は Python の八つの段が一致した。
- golden:E076 と、サービスの無い E013(英日)。変異:`m_e076`、`m_e076ja`、`m_e013svc`、`m_e013svcja`(`tests/mutants/contracts/member_shipping.proto` を読む)。
- dandori の `tests/protos.rs`:`use rule … connect` で規則を読むフローを dandori が検査し、呼ぶ URL と組む名前を rulec の生成した `.proto` と突き合わせる試験(`rulec_gen_writes_the_names_dandori_builds`)に、`implements` の規則(`tests/fixtures/rules/member_shipping_quote.rule` と、番号を振り直した契約 `member_shipping.proto`)を足した。dandori が呼ぶ URL が契約のパス(`/shop.v1.MemberShippingService/Quote`)で、組む名前が契約にあることを確かめる。tools の段の `rule_services_answer_as_dandori_reads_them` が、生成したサービスを立ててこの規則のベクタを dandori の形で送り、答えを規則のものとして読む。
- ritsu の `tests/modules_diff.rs` の `a_rule_that_implements_a_contract_keeps_the_contracts_numbers` と `tests/modules_report.rs` の `a_contract_that_moves_its_numbers_is_a_new_error`(英日):契約に新しい番号で足した入力は E310 にならない。契約が番号を動かすと、規則のファイルが変わっていなくても E310 になる。規則から生成した `.proto` をコピーして移った版は、`contract` が増えるほかは番号もパスも変わらない。
- 替える前と後の突き合わせ:0.38.0 のバイナリと替えた後のバイナリで、木の 983 本の `.rule`(`implements` を書く 7 本のほかの全部)の `rulec gen`(全部のファイル、出力、終了コード)と `rulec api`(JSON と出力)を比べた。違ったのは、E004 か E005 で止まる 4 本(`m_e004`、`m_e005`、sekisho の試験の二つ)で、書ける語を並べる注の終わりに `implements` が増えたことだけだった。`range from koyomi` を持つ 5 本は、どちらも `ritsu` を `rulec` という名前のリンクで呼んで比べた(rulec のクレートのバイナリは koyomi をつながず、E129 で止まる。§15.174)。
**見つけて直していないもの**:
- **`import enum` の列挙は通信に乗せられない。** `implements` の規則が送れる列挙は `import proto` で読んだものだけで、`enums` のファイルの列挙を契約にする道は無い。
- **editions と proto2 の契約。** フィールドが省かれたかどうかの決まりが proto3 と違う。
- **メソッドが二つ以上のサービス。** 一つの規則は一つのメソッドで、生成するサーバーはほかのメソッドを実装できない。
- **well-known types**(`google.type.Date`、ラッパー型)を値の型として読むこと。
- **Protovalidate の範囲と規則の `range` の突き合わせ。** `shape` の契約とは突き合わせる(E122)が、`implements` の契約とはまだしない。
- **空いている番号の候補を言うこと。** ritsu-proto が `reserved` を読まないので、E076 の注は番号を書かずに「まだ使ったことのない番号」と言う。
- **Python 以外の言語の Connect のサーバー**(前から無い)。
### 15.275 `implements` に移った規則の、前の `gen` が書いた `.proto` を挙げる(2026-10-31)
**きっかけ**:§15.274 で、`implements` を書いた規則では、`gen` が規則の `.proto`(`proto/rulec/<別名>/v<版>/<別名>.proto`)を書かずに、契約をコピーするようにした。`gen` は出力先のファイルを消さないので、生成した `.proto` をコピーせずに別のパスの契約へ移った規則では、行を書く前の `gen` が書いた `.proto` が出力先に残る。0.38.0 の公式のバイナリで、`tests/implements` の規則から `implements` の行と契約の列挙を読む `import proto` の行を除いたものを `gen` すると、60 のファイルを書く。二つの行を足して 0.39.0 で `gen` しても 60 のファイルである。違うのは、0.38.0 だけが書く `proto/rulec/member_tier_shipping/v1/member_tier_shipping.proto` と、0.39.0 だけが書く契約のコピー `proto/shop/v1/member_shipping.proto` の二つで、ほかの 59 のファイルはパスが同じで、中身が書き換わる。行の無い規則では、0.38.0 と 0.39.0 の書くファイルは同じである。Python のスタブは `gen` が書かない。生成した `buf.gen.yaml` で `buf generate` が `python/stubs/` の下に書き、頭の行は protoc-gen-py と protoc-gen-connectrpc のものである。
残った `.proto` は、`proto/` の buf のモジュールに入ったままになる。契約が規則と別の package なら `buf build` は通るが、`buf generate` は、もう使わないサービスのスタブ(`python/stubs/rulec/member_tier_shipping/v1/`)も作り続ける。契約が規則と同じ package(`rulec.member_tier_shipping.v1`)にあり、ファイルの名前だけが違えば、同じ名前の型が二つになって、`buf build` が 100 で止まる(buf 1.54.0。最初の行は `rulec/member_tier_shipping/v1/contract.proto:8:6:symbol "rulec.member_tier_shipping.v1.Tier" already defined at rulec/member_tier_shipping/v1/member_tier_shipping.proto:8:6` で、同じ形の行が全部で 24 行)。
**原因**:`gen` は、その実行で書くファイルのほかを見ない。規則が `implements` に移ったとき、前の実行が規則のために書いたファイルが残っているかを確かめるところが無かった。
**決定**:
1. **「`gen` は出力先のファイルを消さない」は保つ。** 出力先には、`gen` の書くもののほかに、buf のスタブや、人が置いたファイルも並ぶ。
2. **`implements` を書いた規則ごとに、規則自身の `.proto` のパスを見る。** パスは、規則のいまの別名と版の大きい数で決まる `<出力先>/proto/rulec/<別名>/v<版>/<別名>.proto`(`Gen::proto_path`。`implements` を書いても変わらない)で、行を書く前の `gen` が書いたはずのところである。このパスがその実行の書き出し先のどれでもなく、ファイルがあり、一行目が rulec の頭の行(`// Code generated by rulec ` で始まり `DO NOT EDIT.` で終わる。版は問わない)なら、残っているファイルとして挙げる。
- 生成した `.proto` をコピーして同じパスへ移った規則では、契約のコピーがそのパスに書かれるので、挙がらない。`--check` で書かないときも、書き出し先として数える。
- 人が置いたファイルと、ほかの生成器が書いたファイルは、一行目が違うので挙がらない。一行目が空行のファイルも挙がらない。
3. **文の出力は、生成の行のあとに、ファイルごとに二行を標準出力に書く。** 一行目は ritsu の `gen` と同じ言い方の「generated no more, and still there: <パス>」「もう生成しないファイルが残っています: <パス>」で、二行目の注記が、規則が契約のサービスを実装するので規則の `.proto` はもう生成しないこと、`gen` は出力先のファイルを消さないので、このファイルは消してかまわないことを言う。
4. **JSON は、`written`・`stale`・`missing` のあとに `left`(パスの配列)を足す。** ほかの三つと同じく、無くても空の配列で、いつも出す。ほかのフィールドは変えない。名前は、ritsu の `gen --format json` の `state` の `left`(もう生成しないファイルが残っている)にそろえた。`docs/formats.md` に書いた。
5. ★ **`gen --check` でも同じように挙げ、終了コードは変えない。** 生成物をコミットする使い方では、残ったファイルもコミットされていて、それが見えるのは CI の `gen --check` だからである。残ったファイルは、`--check` の終了コードに数えない。
6. **診断のコードにはせず、終了コードも変えない。** 残っているかどうかは出力先で決まり、出力先を知らない `check` には言えない。
**捨てたもの**:
- **残ったファイルを `gen` が消すこと。** ritsu の `gen` は、パッケージの出力先を自分のものとして、もう生成しないファイルを消す。rulec の出力先には buf のスタブが並び、人がファイルを置くこともあるので、消すかどうかは使う人が決める(決定の 1)。
- **`proto/rulec/` の全部を見て、その実行のどの規則も書かない rulec の `.proto` を挙げること。** `gen` には、規則を一つずつ、あるいは一部だけ渡すことがある。全部を見ると、その実行に渡さなかった規則の `.proto` まで、もう生成しないと言ってしまう。
- **`.proto` から作ったスタブも挙げること。** スタブは buf が書いたファイルで、rulec の頭の行を持たない。挙げるのは rulec が書いたファイルだけにする。buf はスタブを消さないので、reference の §3.4 に、`.proto` と一緒に消すよう書いた。
- **`--check` を落とすこと。** 0.39.0 で通っていた `gen --check` が、規則も生成物も変えずに落ちるようになる。
**変わること**:
- `implements` を書いた規則の `gen` は、行を書く前の `gen` が書いた規則の `.proto` が残っていれば、それを挙げる。`--check` でも挙げる。終了コードは変わらない。
- `gen --format json` の答えに `left` が増える(いつも出る)。知らないキーを受け付けない読み手には、前は無かったキーになる。リリースノートの「前は通っていた実行が落ちうる変更」に書く。
- 文書:`docs/reference.md` の §3.4 の終わりの、`gen` が消さないと言っていた文を、残ったファイルを挙げることに書き直し、実物の二行と、スタブのことを書いた。`formats.md` の `gen` に `left` を、`generated-code.md` の Connect の節と、サイトの rulec の `generate`(英日)に一文ずつを足した。
**確かめたこと**:
- `tests/implements.rs` に 3 件を足した(17 件)。
- `別のパスの契約へ移った規則では前の_proto_が挙がる`:行の無い規則を `gen` してから行を足して `gen` すると、文の出力の終わりが上の二行で、ファイルは書き換わらずに残り、JSON の `left` がそのパスだけで、`written` は空である。`--check` でも文と `left` で挙がって 0 で終わり、生成物を一つ書き換えると、`left` を挙げたまま `stale` が一つで 1 で終わる。日本語の文も確かめる。ファイルを消すと、もう挙がらない。
- `生成した_proto_をコピーして同じパスへ移った規則では何も挙がらない`:英日の文と、`--check` の有無の JSON で、何も挙がらない。
- `頭の行の無いファイルは挙がらない`:同じパスに置いた、頭の行の無いファイル、`DO NOT EDIT.` で終わるが rulec のものでない二つの頭の行、空行のあとに rulec の頭の行があるファイルは挙がらず、一行目が rulec の頭の行(0.38.0)なら挙がる。
- `tests/formats.rs`:`gen --format json` のキーが四つで、`left` が空であること。
- 実物:行を書く前の規則を `gen` した出力先で、行を書いてから替えた後のバイナリで `gen` すると、文の出力は、書き換えた 27 のファイルの `generated: …` の行のあとに、次の二行で終わる。
```text
generated no more, and still there: generated/proto/rulec/member_tier_shipping/v1/member_tier_shipping.proto
note: `rules/member_tier_shipping.rule` implements a contract's service (`implements`), so gen writes no `.proto` of the rule's own; gen deletes nothing in the output directory, and this file can be deleted
```
`gen --check --format json` は `{"written":[],"stale":[],"missing":[],"left":["generated/proto/rulec/member_tier_shipping/v1/member_tier_shipping.proto"]}` を出して 0 で終わる。その出力先で buf 1.54.0 の `buf generate`(ローカルのプラグインに替えたもの)は、契約のスタブと並べて `python/stubs/rulec/member_tier_shipping/v1/` の二つのスタブを書き、`.proto` をどけてもう一度走らせても、その二つは残った。
- 替える前と後の突き合わせ:0.39.0 の公式のバイナリと替えた後のバイナリ(どちらも `ritsu` を `rulec` という名前のリンクで呼ぶ)で、木の 990 本の `.rule`(`implements` を書く 7 本を含む全部)の `gen` を、規則ごとに新しい出力先で比べた。文の出力と終了コード、続けて走らせた `gen --check --format json`、書いたファイルの全部、別の新しい出力先での `gen --format json` とそのファイルである。違ったのは、JSON の終わりに `"left":[]` が増えたことだけで、それを除くと 990 本とも一字も同じだった(終了コードは 0 が 616 本、1 が 374 本)。新しい出力先には前の `.proto` が無いので、`implements` を書く 7 本でも何も挙がらない。
**見つけて直していないもの**:
- **別名か版の大きい数を、`implements` に移るのと同時に変えた規則。** 見るのは、いまの別名と版のパスだけなので、前のパスに残った `.proto` は挙がらない。
- **buf が前の `.proto` から作ったスタブ。** buf の書いたファイルなので挙げない(捨てたもの)。`buf generate` はスタブを消さないので、`.proto` を消しても `python/stubs/rulec/<別名>/v<版>/` は残る。
### 15.276 `ritsu check` の JSON の、規則の `check` は `null`(2026-10-31)
`ritsu check --format json` の `files` の各項目は、`check` に、その言語の `check --format json` がそのファイルについて書くオブジェクトを持つ(ritsu の DESIGN 8.3)。`rulec check --format json` は診断一つにつき一行を書き(JSON Lines)、ファイルのオブジェクトを書かない。テキストの `ok …` の行にあたるものも JSON には無い。そこで `ports::Engine::checked` は、単位のオブジェクトに `Json::Null` を渡し(`ritsu_ports::Checked::json`)、規則の `check` は `null` になる。rulec のコマンドの出力は変えていない。
確かめたこと:ritsu の `tests/check.rs` の `each_files_check_in_the_json_is_what_its_language_writes` で、どのプロジェクトでも規則の `check` が `null` であること。check の JSON の golden(`crates/ritsu/tests/golden/check/`、`golden/modules/`)。
見つけて直していないもの:規則について `check` が言うこと(`ok …` の行、警告の数)を、ほかの言語のように JSON のオブジェクトで書く形は無い。足すなら `rulec check --format json` の形を変えることになり、JSON Lines を読む側に関わるので、ここでは足していない。
## 16. この設計で最も危うい点
第一に、**rulec が消したかった二重実装が、一段上で小さく再発する**。
表現力の崖は実際にクーポンで来たが、導出と粒度の二手で、崖ではなく段差として越えられた(§5.2、§5.4)。
その代償が、重ね掛けの畳み込み(ソート、逐次適用、状態の運搬)を Python と Go の手書きに一つずつ残すという構図である。
「二重実装は必ずズレる」という、この道具が存在する理由そのものが、規則の外側で小さく再現される。
順序の判定まで規則に入れて畳み込みを自明な形に追い込む設計はその緩和だが、本当に自明になるかは試作で数えるまで分からない(§15-5)。
畳み込みを宣言にして検査の網を届かせる案は検討済みで、単一アキュムレータが当のクーポン(併用制約の状態)を表せないため見送った(§5.4)。
それまでは連鎖の例が、畳み込みの仕様をデータとして規則側に留める。
按分は 2026-09-13 に測って決着した。逐次充当型は粒度原則にそのまま収まり、比率按分は粒度ではなく除算の制限に当たる(§15-7)。粒度原則の未検証領域は、この一件ぶん狭まった。
第二に、**99.9% の先の 0.1% が道具の信用を決める**。
不一致 1,107 件の身元(旧実装のバグか、転記ミスか、fixtures の汚れか、丸め差異か)を報告書が自力で切り分けられなければ、「982 件」は証拠ではなくノイズになり、一致率を上げる過程が仕様の発掘ではなく徒労になる。
発火行クラスタ、丸め差異の自動タグ、実測系と補完系の分離はすべてこのための設計だが、その有効性は実データに当てるまで仮説にとどまる。
実測は道具の前半(記述、検査、生成)を現実の規約に当てたが、後半(M2 と M3)は依然として一件の実データにも触れていない。
検証済みと未検証の境界は、いまここにある。
M3 は機能を足す段ではなく、最初の実表 1 本でレポートの切り分け精度を磨き込む段として扱うべきである。
第三に、**丸めの決定が、規約に存在しないまま `.rule` に書かれて権威になる**。
実測は、公開規約が複数クーポンの端数処理も上限の掛かり先も書いていないことを示した(§7.2)。
E104 が丸めを書けと迫るのは設計どおりだが、迫られた転記者が書けるのは仮置きであり、生成コードに入った仮置きは仕様の顔をする。
出典コメントの慣行と replay での裁定は緩和であって解決ではなく、データが来るまで仮置きは仮置きのまま積み上がる。
オーバーフロー証明が要求する中間丸め(E108)も同じ圧力点で、円が動く業務判断が型エラーの顔をして業務担当者に届く。
E104 と E108 の文面がそれを業務の判断として提示し直せているかは、業務担当者に文面を読ませる実地の検証(誤読率を見る)まで含めて M0 の合否に入れないと、P2 の「読める」がここから崩れる。
### 15.66 一つの出力を、優先の順序を書いた複数の表で定める(2026-09-19)
**決定**:DESIGN-draft.md 第二稿の増分 1 を入れた。行の**ラベル**(最初の `|` の前の一語)、表の `policy` の次に置く **`overrides`** 行、そして検査・評価・生成・網羅・描画の単位を「表」から「一つの出力を定める定義の集合」(`src/defset.rs`)に移すこと。同じ出力を定める表が二つ以上あれば、列の和集合を軸に、無い列を `-` で埋めた合成表を作って `check_table` に渡す。行は評価の順(表を宣言の逆順に、表の中は上から下)に並び、優先の関係 `beats` は書いた辺と `first` の位置の順の推移閉包である。辺は上の表にしか向けられないので閉路検査は要らない。表が一つなら合成表はその表そのもので、診断の文面は一字も変わらない(golden で固定した)。
**新しい診断**:E034(行ラベルの重複)、E035(`overrides` の指す先が無い・後ろにある・行にラベルが無い・形が読めない)、E036(相手が同じ出力を定めていない)、E045(出力を共有する表に出力列が二つ以上)、W117(交わる行の無い効かない例外)。E105 と W114 は「順序の無い交わり」に広がり、末尾の文が「優先する表に `overrides` を」に変わる。E102 は「優先する定義の和に覆われた行」に広がり、E101 は定義集合の和で探す(合成表には `fix.text` を付けない。行をどの表に足すかは人が決める)。台帳は 54 から 59 になった。
**記録と trace**:発火の記録は勝った定義ごとに一つで、`{"table":…,"row":N}` の `N` は書かれた位置(`Row.index`。持っていたが読まれていなかった)。ラベルのある行は `"label"` を足し、九つの生成対象の `Fired` に第三のフィールドが入った。`replay` と `diff` は、両側にラベルがあればラベルで、無ければ番号で行の対応を取る。SQL は表ごとの行の列を保ち、先に試した表が当たっていれば後の表の列は NULL、runner はその列を飛ばす。
**測った**:`tests/corpus/印紙税の本則と軽減.rule`。印紙税法別表第一の本則の表と、租税特別措置法第91条の軽減の表に分け、軽減が本則に `overrides` する。`check` は通り、`coverage` は行 23 / 23、隠れ対 10 / 10、九言語のベクタ一致は全件通った。`doc` は各表を自分の出典の下に描き、「印紙税額 の決まり方」の節で「表 軽減 は 表 本則 に優先する。交わる 10 対のすべてで、表 軽減 の行は相手の行に収まる(例外)」と言う。一表で `軽減期間` の列と `-` の行に畳んでいた形と比べ、承認者が二つの原文とそれぞれ見比べられるようになった。
**実装を読んで直したもの**:`Row.index` を trace の番号の正として使うようにした。`check` の `--help` が挙げるコードを台帳と揃えた。変異ファイルを作り直したところ、コーパスの出典コメントの移動(§15.32)を反映していなかった 13 本が動き、E101 の golden と README の抜粋が「`table 運賃表(fee_table) # 出典: …`」の行ぶんだけ変わった。生成物ヘッダの `sha256:` が FNV-1a である件と、識別子の NFC は、それぞれ増分 3 と別の決定に送る。
**捨てたもの**:行が自分の `overrides` を書く場所(行が優先したいなら節として書く。増分 2)。二出力の表を定義集合に入れること(同じ行の条件を二度書くことになる。実例を数えてから)。第三の `policy` の語(`unique` の意味が「辺の無い交わりは誤り」に広がるだけで足りる)。
### 15.67 節:一行の定義を文で書く(2026-09-19)
**決定**:DESIGN-draft.md 第二稿の増分 2 を入れた。`clause <名前>(<別名>) -> <出力>` の下に `when <列> <セル> and …`(条件が無ければ `when always`)、`then <値>`、任意の `overrides` を書く。節は**一行の表**である。`when` に書いた列だけを列に持ち、書かなかった列は `-`、`then` は出力セルと同じもの(リテラルか名前)を持つ。構文解析が `Table { clause: true, rows: [一行] }` に置き換えるので、型付け・定義集合・検査・評価・生成・網羅・記録は増分 1 の機構にそのまま乗り、trace には `{"table":"無料","row":1}` と出る。新しく書いたのは、構文と、節を「節」と呼ぶ文面と、承認者向けのページの節の描画(条件と値を原本の行そのままに一行の表として出す)と、`fmt` の本体の正規化(二字下げ、語間一つ)である。
**新しい診断**:E046(`clause` の形が読めない。見出しの欠け、`when` か `then` の欠けと重複、`when` の列の欠けと重複)。`when` を省くことはできない。空セルを don't-care と読まないのと同じ理由である。表と節の名前は一つの名前空間で、重複は E034 に足した。台帳は 60。
**測った**:`tests/corpus/印紙税の本則と軽減.rule` の非課税(記載された契約金額が1万円未満)を、本則の行から節に移した。別表第一では課税物件表の行ではなく非課税物件の欄に書かれているので、原文の置き場に合う。節は本則と交わらないので `overrides` は要らず、辺の無い分割として通る。`check` は通り、網羅は行 23 / 23、境界 41 / 41、隠れ対 10 / 10 のまま、九言語の一致も全件通った。もう一本、第二稿 §2 のスケッチを `tests/corpus/送料のただし書.rule` に置いた。運賃表が基本運賃を決め、節 通常(`when always`、`then 基本運賃`)と節 無料(`overrides 通常`)が送料を定める。ページは「送料 を定めるのは 節 通常、節 無料 で、節 無料、節 通常 の順に試す」「節 無料 は 節 通常 に優先する。交わる 1 対のすべてで、節 無料 の行は相手の行に収まる(例外)」と言う。
**ついでに見つけて直したもの**:別名の無い群(`group 遠隔地 = …`)を Ruby に生成すると、定数 `GROUP_遠隔地` は Ruby では合法だが RBS の字句には通らず、steep が落ちる。コーパスの群はすべて別名を持っていたので気づかなかった。Ruby と RBS では、別名の無い群を宣言順の `GROUP_g1` のように書くことにした(§8.1 の「内部は日本語のまま」は、RBS だけがこの例外になる)。`送料のただし書.rule` の群は別名を持たないまま置き、この道が試験に入るようにした。
**捨てたもの**:一つの節に `then` を二つ書くこと(二つの値を変えるただし書は節を二つ書く。実例を数えてから)。`when -`(`always` の代わり。読めるが書き間違いに見える)。節の条件に列名どうしの比較を書くこと(真偽の `define` に名前を付けてから列に置く。§5.3 のまま)。
### 15.68 出典を契約にする(2026-09-19)
**決定**:DESIGN-draft.md 第二稿の増分 3 を入れた。`source <名> = law "<法令ID>" asof <日付>` の下に、引用箇所ごとの固定行 ` <引用箇所> sha256:<16桁>` が並ぶ。`source <名> = file "<ファイル>" sha256:…` は、引用箇所に分けずに文書を丸ごと固定する。表・節・行・導出・定義は行末の `@<出典> <引用箇所>` で引く(`#` のコメントより前)。引用箇所の名前は原文の呼び方そのまま(`第91条`、`第20条の2`、`第20条第2項`、`第20条第2項第3号`、`別表第一`)で、e-Gov の `elm`(`MainProvision-Article_91`、`AppdxTable[1]`)に対応する。コピーは規則の隣の `sources/law/<法令ID>@<日付>/<要素>.xml` に引用箇所ごとに保存し、git に入れる。
**§15.32 を覆す**。あちらは `source` 構文とダイジェストを「表の性質ではなくレビューの引き金」として捨てた。相手が法令になり、単位を引用箇所にすると、変化から言えることは「第91条を引く三つの定義を読み直す」まで縮み、引いている定義は挙げられる。E032 が `.proto` の変更で落ちるのと同じ場所で、保存した条文の変更でも落とす。文書全体のハッシュ一つの案は、無関係の改正でも全部の規則を落とし、人が更新を読まずに通すようになるので採らない。
**検査**は `enums::check` の隣の一段 `sources::check` で、ファイルを読む段がもう一つ増えた。ネットワークは見ない。引用箇所が固定されていなければ E037(`fix.text` は固定行そのもの。宣言・引用・引用箇所の書き方が読めないときも E037)、固定とコピーが違えば E038(引いている表・節・行を挙げ、コピーの差分を読んでから貼れと言う)、コピーが無ければ E039(`rulec source fetch` へ)、固定だけ残れば W119、宣言の無い出典を引けば E012。台帳は 64。
**コマンド**は `rulec source fetch | pin | outdated`。`fetch` と `outdated` だけがネットワークを見て、`curl` を子プロセスで呼ぶ(`git` と同じ扱い。HTTPS を自前で書かず依存も足さない)。e-Gov 法令 API v2 の `law_data` は JSON の包みに base64 の XML を入れて返すので、`json.rs` と自前の base64 で足りた。`pin` は固定行だけを書き換える。`outdated` は `law_revisions` で `asof` より後に施行された版を引き、その日の引用箇所を取ってコピーと比べ、変わる引用箇所があれば施行日と一緒に言って exit 1 で止まる。`asof` で固定している限り `check` は改正を知りようがないので、改正を知る手段はこれ一つで、CI の定期実行に置く。
**SHA-256 は自前**(`sha256.rs`。FIPS 180-4 の既知の値で固定)。生成物ヘッダの `sha256:` は FNV-1a を名乗り違えていたので、この機会に本物にした。名乗りと中身が一致した。
**ページ**は、引いた定義の見出しの下に「出典: 法 別表第一(法令 342AC0000000023、2026-04-01 時点)」と書き、コピーから引用箇所の本文をタグを落として引用する。行の引用は注記の列に、導出と定義の引用も注記に出る。HTML は引用を `<blockquote>` にした。
**測った**:`印紙税の本則と軽減.rule` に印紙税法(342AC0000000023)の別表第一と、租税特別措置法(332AC0000000026)の第91条を e-Gov から取って固定した。`check` は通り、固定行を消せば E037、ハッシュを変えれば E038、コピーの無い第92条を引けば E039、余った固定は W119 が出る。`pin` は消した固定行を戻し余った行を消して、ファイルのほかの部分は一字も変えない。`outdated` は両方の法令の後の改正を全部(印紙税法 10 版、措置法 21 版)引いて、引用箇所はどれも変わらない、と答えた。租税特別措置法第91条の軽減は令和9年3月31日までと条文自身が言っているので、2027-04-01 の改正がこの条を変えないのは正しい。
**変えたもの**:`@` を字句に足した(今までは E002 だった。E002 の台帳の例は `%` に替えた)。`sha256:<16進>` は一つの字句にした(数として読むと単位つきの値に化ける)。E002 以外の既存の文面は変わっていない。
**捨てたもの**:法令 ID を裸で書くこと(`342AC…` は数として読まれるので `"…"` で囲む。第二稿の書き方を直した)。コピーを持たずにネットワークを見る `check`(オフラインで決定的、という §15.59 の線を守る)。法令を丸ごと保存する案(措置法は数 MB になる。引用箇所だけを置く)。別表を表題で探す案(`別表第一` は文書順の一番目で、`AppdxTable[1]` がそのまま返す)。附則と号の細分(実例を数えてから)。
### 15.69 準用をそのまま書ける呼び出し(2026-09-19)
**決定**:DESIGN-draft.md 第二稿の増分 4 を入れた。`apply <名前>(<別名>) = "<規則ファイル>" sha256:<ハッシュ>` の下に、呼び先の入力を一つずつ束縛する `<呼び先の入力> = <値>`(列挙は `with <この規則の値> -> <呼び先の値>, …` を続ける。同じ綴りは書かなくてよい)、外す定義の `except <表か節か 表:行>, …`、出力に付ける名前の `<呼び先の出力> -> <名前>` を書く。呼び出しは `derive`・`define`・`table` と同じ列に並ぶ項目で、後ろの項目がその出力を使える。
**展開**(`src/apply.rs`)は構文解析の直後、型付けの前に走る。呼び先を規則ファイルの隣から読み(`--diff-base` と `path@rev` のときは `src/vfs.rs` を通して同じ git リビジョンから読む)、見出しのハッシュと比べ(E040。`fix.text` は新しい見出し、`rulec source pin` も書く)、呼び先を自分の場所で丸ごと `check` に掛け(通らなければ E044。自身が `apply` を持つ、列を歩く、呼び先自身の列挙を出す表や節を持つ場合も E044)、束縛の形を見て(E041、`except` の指す先は E035)、呼び先の項目をこの規則の項目に取り込む。取り込むときに、束縛した名前で列を付け替え、リテラルに束縛した列は部分評価して消し(合わない行は落ちる)、列挙のセルはこの規則の値に綴り直し(対応の無い値だけの行は落ちる)、群は要素に開き、呼び先の名前はすべて `<呼び出し>:<名前>`(別名は `<呼び先の別名>_<別名>`)に改める。呼び先の出力は `<この規則での名前> = <丸め>(<呼び出し>:<出力>)` の定義になり、呼び先の丸めが先に、この規則の出力の丸めがその上に一度だけ掛かる。外した節しか読まない呼び先の定義は消す。呼び先の `source` は `<呼び出し>:<名>` の名前で引き継ぎ、コピーは呼び先の隣のまま(`SourceDecl.base`)で、この規則の `sources::check` は引き継いだ出典を検査しない(呼び先の検査で済んでいる)。取り込んだ項目の位置はすべて `apply` の見出しにする。呼び先の行番号はこの規則の中では何も指さない。
**型付けの後**(`apply::check`)に、束縛の型(E042。列挙は対応の全域性も)と、束縛した値の到達区間が呼び先の `range` に収まること、呼び先の `constraint` がこの規則の `constraint` か区間から導けること(E043。入力を添える、`fix.text` 無し)を見る。
**定義集合への接続**:展開した表は `Table.applied = Some(呼び出し名)` を持つ。`overrides 退職手当:支給表` のような相手は、全体が表の名前ならそれ、でなければ `表:行`。節や表の見出しの `-> 退職手当:手当` は、コロンが両側の語に接していれば一つの名前と読む(空いていれば型の区切り)。この規則の定義が呼び先の表に優先すると、その表の出力を読む呼び先の項目(定義、後の表、出力の定義)は、優先する定義の直後に移す。定義集合は最後の要素の位置で評価されるので、間に置いたままでは読む側が早すぎる。
**到達しない行**:展開した表の行が死んでいても E102 は出さず、網羅の義務からも外す。全行が死んだときだけ W118(展開の時点で行が全部落ちたときも同じ)。承認者向けのページは、準用の段に「規則 退職手当 v1 を次の読替えで適用する」と束縛・対応・`except`・出力の名前を書き、「この準用で使われない行: 表 支給表: 中期、長期」を挙げ、その下に呼び先の表と節を呼び先のファイルと出典から一段下の見出しで描く。お客向けの案内も同じ形で、読替えを「勤続年数 は 在職期間 のこと」と言う。生成物のヘッダは `Applies: 退職手当 = 退職手当.rule (sha256:…)` を刻み、`rulec api` は `applies` に並べる。trace は `{"table":"退職手当:支給表","row":1,"label":"短期"}`。
**ついでに見つけて直したもの**:数値の軸は、宣言した範囲の外にセルが置いた境界も座標にしていた。範囲 `>=1 <=3` の列に `<=3` と `>=10` の行を書くと、(3, 10) の区間が完全性の欠落として要求され、`>=10` の行は到達できるように見えて `coverage` が永遠に満たせない義務を積んでいた。境界を範囲に切り詰めた(`num_bounds`)。既存のコーパス・変異・golden は一つも動かなかった。もう一つ、直していない隙:表 甲 の出力を `define` が読み、その後の節が `overrides 甲` で同じ出力を定めると、集合は節の位置で評価されるので `define` は値を得られず、例があれば E107 で、無ければ黙って空になる。呼び出しでは上の並べ替えで避けたが、手で書いた規則には並べ替えも診断も無い。位置の検査を足すべきで、別の決定に送る。
**測った**:第二稿 §5 の例を `tests/corpus/退職手当.rule`(本則。支給表と、自己都合の減額の節)と `tests/corpus/非常勤退職手当.rule`(在職期間 1〜3 年、任期終了事由を `with` で読み替え、`except 減額`、`手当 -> 非常勤手当`)に置いた。`check` は通り、`coverage` は行 2 / 2(支給表の中期・長期は使われない行として義務から外れる)、九言語のベクタ一致は全件通った。在職期間の範囲を `>=0` にすると E043 が「在職期間 = 0 は、退職手当.rule の 勤続年数 の range >=1 <=40 の外です」と言い、束縛を一つ消せば E041、対応から 辞職 を落とせば E042、check を通らない規則を呼べば E044、支給表の全行に優先する節を足せば W118 が出る。改正の流れは、二ファイルを git に入れ、支給表の短期を 5 か月から 6 か月に直して測った。`check` は E040 で止まり、`rulec diff 非常勤退職手当.rule@HEAD 非常勤退職手当.rule` が呼び先を HEAD から読んで「14 件が +100,000〜+500,000 円動く」と言い、`rulec source pin` が見出しを書き換えて `check` が通った。台帳は 70。
**捨てたもの**:呼び先を関数として生成すること(展開で九言語とも変更なしに通った。最適化として後から選べる)。条番号と定数の読替え(§5.5 のまま)。準用の準用(一段。実例が要求したら。§15.230 で、上書きを重ねる例を受けて入れた)。呼び先の列挙を出す表の展開(その値の綴りがこの規則に無い。入力の列挙は `with` で対応づけられるので、出力にも同じ形を足すのは実例を数えてから。§15.230 で、`<準用>:<列挙>` と出力の `with` として入れた)。
### 15.70 草案を畳む(2026-09-19)
**決定**:`DESIGN-draft.md` を消した。四つの増分(§15.66〜§15.69)が全部入り、決定と測定はそれぞれの節に書いてあるので、草案は役目を終えた。第一稿と第二稿は git の履歴に残る。ここには、四つの節に書いていなかった草案の中身だけを置く。
**中核の考え**。節は行であり、表は行の束であり、順序は出力ごとの厳密な半順序である。rulec の行は自列への単項テストの連言、つまり入力空間の区画で、Catala の定義は条件つきの定義とその上の優先順位だった。条件を区画に限った Catala の定義は rulec の行そのものであり、違いは、行を表に並べて位置で順序を付けるか、定義に名前を付けて例外の関係を書くかだけである。だから意味論は一本にできる。プログラムは、ラベル付きの定義(区画と、一つの出力と、その値)の集合と、出力ごとの厳密な半順序である。`policy unique` は「同じ表の行どうしに辺が無く、交われば誤り」、`policy first` は「同じ表の行どうしに位置の全順序がある」という特別な場合に戻る。順序が「出力ごと」なのは、二出力の行の一方だけを節が上書きしたとき、もう一方の値の出どころを決めるためである。
**幾何が壊れない理由**。完全性は出力ごとに区画の和が宇宙を覆うかで、半順序は関与しない。決定性は「同じ出力を定めて交わる二つの定義は比較可能」で、成り立てば点に当てはまる定義の集合は鎖になり最小が一つ決まる。判定は区画の交わりの総当たりで、重なりの検査と同じ計算量である。これは「一意の極小元がある」より強い十分条件で、交わって比較不能な二つを第三の定義が丸ごと覆う配置も弾く。そう読める条文が無いので、十分条件をそのまま定義にした。到達性は、自分に優先する定義の和に覆われていないかで、`first` の残余領域の計算の引く相手を替えるだけである。辺は後の宣言から前の宣言へ向くか、同じ `first` の表の中で上から下へ向くので、閉路は作れず、循環検査は要らない。
**版は言語に入れない**。施行期間はファイルの属性とし、適用日から版を選ぶのは生成されたディスパッチャの仕事にする。時点を列にすると、全部の表の幾何に時間軸が増え、完全性が日付を量化することになる。ただし「どの日を適用日にするか」(施行前の行為はなお従前の例による、など)は条文が決めているので、各版が何の日を基準に選ばれるかは承認の紙に一行出す必要がある。その宣言(`effective`)はディスパッチャの生成と一緒に決める。まだ入れていない。
**捨てたもの**。条件の任意式(六つの検査が死ぬ)。日付の演算(年齢と期間の計算は曖昧さの本体で、全部アプリ側に置く。外に出したものは承認の紙に書く)。実行時の empty と conflict(全部コンパイル時に決める)。自然言語ごとの字句解析器。識別子の大文字小文字の同一視と NFC 正規化は、実装がそうなっておらず、依存ゼロで Unicode の表も持たないので、約束しない(§1.1 の「NFC に正規化する」は実装に合わせて読む。正規化を入れるかは、表を持ち込む費用と一緒に別に決める)。
**書ける範囲**。形式的な表現力はほとんど変わらない。半順序は線形拡大で全順序に直せ、`policy first` で同じ関数が書ける。変わるのは、検査が弾くもの(`first` では階段の件数に紛れて黙っていた重なりが、辺で正当化されていなければ止まる)と、承認を通せる範囲(原文と見比べられる形で書けるかどうか)である。本当に範囲が広がるのは準用だけで、セルが区画であるという境界は動かさないので、期間の按分、明細をまたぐ累積、条番号の読替え、包括読替え、一回の判定でない手続きは外のままである。
**まだ測っていないもの**。実在の準用の条文で、読替えが「語の付け替え」「定数の付け替え」「条番号の付け替え」のどれかを数えること。定数の読替え(`param`)を入れるかは、この数を見てから決める。
**変えたもの**。`DESIGN-draft §n` を指していたソースとテストのコメントは、§15.66〜§15.69 を指すようにした。コーパスの三本の説明文も同じで、`退職手当.rule` の中身が変わったので `非常勤退職手当.rule` のハッシュを固定し直した。
### 15.71 版に施行日を持たせない(2026-09-19)
**決定**:版を言語の単位にしない。規則が「いつから効くか」は、条文が挙げる日付(印紙税なら作成日、源泉徴収なら支払日、年末調整なら年分、保険料なら月分)を入力に取り、その入力への行の条件として書く。切り替えは表の一列であり、「版が時間軸を隙間なく重なりなく敷き詰めている」ことは、その列に対する E101(隙間)・E105(重なり)・E102(もう当たらない版)そのもので、新しい検査も診断コードも要らない。書き方は二つあり、どちらも今の言語で書ける。**畳む**(一つのファイルに期間ごとの表を置く。`overrides` は要らず、日付の列が交わりを切る)を既定にし、**束ねる**(期間ごとのファイルを `apply` で両方呼び、日付の表が出力を選ぶ)は、列挙の値が入れ替わる改正と数百行の別表のときに使う。§15.70 が残した `effective` 宣言と、処理日から版を選ぶ生成ディスパッチャは入れない。数えた切り替え 30 件のうち、処理日で切り替わるものは 0 件だった。git は「いつ書いたか」であって「いつ効くか」ではなく、`送料@v3` はそのまま残す。
**数えた**:五法令(印紙税法、租税特別措置法、所得税法、健康保険法、厚生年金保険法)を e-Gov 法令 API v2 で辿った。改正 423 版、施行日 305 日(履歴は 2017-04-01 から、予定分は 2030 年まで)。コーパスが保存する引用箇所と、給与計算が保存することになる引用箇所の 23 本を施行日ごとの時点で取り、隣り合う時点で比べた(1,213 回)。バイトで違ったのは 103 件、うち 38 件は XML の属性だけで本文は同じ。本文が違う 65 件は、語句と参照だけ(セルが動かない)35、値(金額・日付・行の増減)19、形(列・入力・出力・列挙の値)11。コーパスの表そのものは動いていない。別表第一の第1号の税額の欄は 9 年間で 0 件、所得税法第89条の税率表 0 件、健康保険法第40条の等級表 0 件、厚生年金保険法第20条は 0 件で予定が 3 件(2027・2028・2029 年 9 月に上限の行が足される。2020 年 9 月の 65 万円は政令なので法律の改正履歴に出ない)。措置法第91条は 5 件のうち 4 件が期限の日付だけで、額は 2014 年から変わらない。2024 年の一件は第1項を消して第3項を第2項に繰り上げてもいて、`第91条第2項` の指す先が不動産から建設工事に変わった(引用箇所の名前は条の単位が安全である)。値と形の 30 件の起点は、作成日 5、年分 10、支払日 6、月分 3、施行日の資格の得喪 6。附則に書かれているのが 23、本文が 4(措置法は「…までの間に作成される」と本文に期間を持つ)、施行期日だけが 3。仮説「切り替えが要るのは形が変わるときだけ」は違った。形の 11 件のうち 10 件は畳める。定額が本人の合計所得金額の段階表になった 3 件(配偶者控除・配偶者特別控除 2018、基礎控除 2020)と被扶養者の国内居住要件(2020)は列を足して古い期間の行を `-` に、条件が消えた 5 件(短時間労働者の一年以上要件 2022、賃金要件 2026、業種要件 2029。健康保険法と厚生年金保険法で同じ改正)は列を残して新しい期間の行を `-` にし、特定親族特別控除の新設(2025)は年分を取る新しいファイルが古い年分に 0 円を返す。残る 1 件(2019 年の商法改正で第9号の物件名が貨物引換証から複合運送証券に付け替わった)は、畳むと「貨物引換証 × 2019-04-01 以後」という起きない組み合わせに行が要り、`constraint` は数の比較しか書けない(§15.55)ので束ねる。五法令の附則 1,391 本のうち「従前の例による」は 428 本、起点を取引の日付で書くのは 173 本、施行期日だけが 960 本。取った条文と数えた道具は `experiments/point-in-time/` に置いた。
**走らせた**:畳む形を厚生年金の上限の三段で書いた(`月分 : date`、共通の行と期間ごとの上限の行、15 行。`experiments/point-in-time/rules/厚生年金の上限改定.rule`)。`check` は通り、ベクタ 307 件、行 15 / 15、境界 46 / 46、九言語の一致は全件通った。2028 年の期間の行を消すと E101 が「月分 = 2028-09-01, 報酬月額 = 605000」を出して止まり、月分の範囲を 2029-08-31 で切ると最後の期間の 5 行が E102、期間の始まりを一日ずらすと E105 が行 3 と行 6 を挙げて「月分 = 2028-08-31」を出す。版の敷き詰めの証明はこれで全部である。束ねる形も書いた(同じ場所の `束.rule`)。旧と新の二ファイルを `apply` で両方呼び、`作成日` の二行の表が `旧税額` と `新税額` を選ぶ。`check` は通り、行 6 / 6、お客向けのページは「旧(旧税額.rule の決まりを使う)」と読める。`rulec source outdated` はコーパスの印紙税に対して後の改正 27 版(法 9、措置法 18)を引き、引用箇所はどれも変わらないと答えた。
**走らせて見つけて直したもの**:四つ。(1) 束ねる形の生成物が TypeScript・JavaScript・Go・Swift で通らなかった。`apply` の展開が、`->` で付け替えた出力に別名が無いとき、呼び先の別名に呼び出しの接頭辞を付けた別名を与えていて、それは呼び先の表の出力列が出力と同じ名前のとき(一表の規則の普通の形)にその列の識別子そのものだった(`const old_tax_tax` が二度、Go は `:=` に新しい変数が無い)。Python・Rust・Ruby は再束縛を許すので黙って通っていた。§1.3 のとおり、別名が無ければ名前そのものを識別子にした。`tests/apply.rs` が同じ束を作って九言語で回す。(2) 承認者向けのページが日付の範囲を通算日で出していた(`作成日 | date | 16161 〜 22279`)。日付で出す。(3) `source fetch` と `source outdated` がバイトで比べていて、e-Gov が改正の無い条文の属性や改行を書き換えた版(103 件中 38 件)を改正と言った。本文(`xml_text`)で比べ、本文が同じなら `fetch` はコピーを書き換えない(書き換えると固定が外れて次の `check` が E038 で止まる)。 (4) お客向けのページで、二つの境界を持つセルの言い換えが壊れていた(「10万円 <=50万円より大きい」)。下限から上限へ「10万円より大きく 50万円以下」と、条文の言い方に直した。
**同じ日に入れたもの**:出典の引用箇所としての附則。`@法 附則第3条`(法令自身の附則)と `@法 附則(令和七年三月三一日法律第一三号)第3条`(改正法の附則。番号は e-Gov の附則の見出し `AmendLawNum` どおり)で、条を書かなければ附則を丸ごと引く。e-Gov は附則を文書順の `SupplProvision[k]` でしか取れず、番号で選ぶ書き方は 400 のエラーになる。しかも `k` は時点で数個ずれる(令和7年法律第13号の附則は所得税法の 2025-12-01 時点で 349 番目、2027-04-01 時点で 350 番目)。`k` が要るのは通信する `fetch` と `outdated` だけなので、コピーの名前は番号のまま `SupplProvision_<番号>-Article_3.xml` にし、`fetch` は本文全体を一度読んで `k` を引き(所得税法で 16 MB、15 秒)、`outdated` は後の時点では前の `k` の近くを一件ずつ小さく探ってから、外れたときだけ全文を読む。E037・E038・E039・W119 と `pin` と承認者向けの引用はそのまま効く。所得税法の附則(令和七年三月三一日法律第一三号)第三条(給与所得控除の経過措置)を引く規則で `fetch`・`pin`・`check` が通り、ページが「新所得税法第二十八条の規定は、令和七年分以後の所得税について適用し…」を引用し、`outdated` は後の 12 版でどれも変わらないと答えた(`experiments/point-in-time/reports/suppl.log`)。切り替えの起点 30 件のうち 23 件が附則にあるので、§15.68 で「実例を数えてから」と外したものを入れた。`source outdated` は四つ変えた。同じ法令を二つ以上の時点で引く規則ではいちばん新しい時点だけを問う(古い時点は設計どおり古い)。後の版はコピーではなく一つ前の版と比べ、改正はその施行日に一度だけ言う(コピーと比べると、一度の改正が後の全部の日に繰り返し出た)。変わる引用箇所は本文の差分(消えた行と現れた行、各 8 行まで)を出し、着地の一行を「`source 法_20241118 = law "342AC0000000023" asof 2024-11-18` を足し、作成日 >=2024-11-18 の行をそこから転記してください」の形で言う(日付の入力が一つならその名前で、無いか二つ以上なら「その日以後に効く行」)。`curl` は三度まで試し、JSON でない答えも二度は取り直す(取得が続くと e-Gov はそういう答えを返す)。印紙税の規則の二つの `asof` を 2024-01-01 に戻して問うと、別表第一が 2024-11-18(貯留権)・2025-10-01(公証人法の条番号)・2026-04-01(公益信託)に、第91条が 2024-04-01 に変わると、差分つきで一度ずつ出る(同じ場所の `outdated-2024.log`)。生成物の共有ヘッダに `出典:`(英語は `Cites:`)の一行が入り、法令は `law <ID> asof <日付> (<引用箇所> sha256:…)`、ファイルは `file <パス> sha256:…` で、九言語同じ場所に同じ行が出る。`rulec api` は `applies` の隣に `sources` を出す。承認者向けのページの出典の行に、コピーの `revision.txt` から施行日と改正法を添える(「2026-04-01 時点。2026-04-01 施行、令和8年法律第12号による改正後」。改正法の番号は ID から起こす)。
**まだ入れていないもの**:お客向けの境目の例に日付の原子の境界を入れること(いまはベクタが両側を踏んでいるのに例に出ない。境界の義務が表のセルの境界しか数えていないので、§9.2 の義務を広げる話になる)。同じ法令を二つの時点で引くとき、どの行がどちらを引くかをページに出すこと(行の日付のセルの範囲から言える。二つの時点を引く規則がコーパスに入ってから)。九言語の規則の中身の生成は変えない。日付は通算日の整数で入口の範囲検査があり、SQL は `_input_error`、Wasm は `{"error":…}` で、期間の外の日付には答えない。
**測り方**:コーパスに `厚生年金の上限改定.rule` を入れ、上の数字が再現し、`source outdated` が第20条について 2027-09-01・2028-09-01・2029-09-01 を言い、所得税法第89条のマークアップだけの三つの版を改正と言わないこと。第28条の 2020・2025・2026 の三版を年分で分けて附則を引く `給与所得控除.rule` が `fetch`・`pin`・`check` を通り、ページが附則の本文を引用すること。別表第二を二つの時点で畳んだときの規模(345 行 × 8 列 × 2 期間)が §15.40 の目印 2,000 件と検査予算 5×10⁷ に収まるかを数え、収まらなければ「数百行の別表は束ねる」を規則から既定に格上げすること。
**捨てたもの**:`effective` 宣言と処理日のディスパッチャ(処理日で切り替わる改正は 30 件中 0 件で、要る日付は規則が入力に持っている)。git のタグ `rules/<名前>/<版>` から版を読んで束ねる形(このリポジトリにそのタグは一つも無く、生成物が git の状態に依存すれば §8.5 の決定性が崩れ、過誤払いの精算に要る「いつ効くか」はタグではなく行にある)。複数ファイルをまとめて検査する新しい段(束ねる形は `apply` と日付の表で、証明は E101・E105 のまま)。`source` の行に効く期間を書くこと(読んだ時点と効く期間が同じ行に並ぶのが取り違えの元。期間は行のセルに置けば証明が見る)。版を型や列挙として組み込むこと(`期間区分.rule` のとおり、日付の範囲の表が列挙を出す形で足りる)。コピーの時点と行の期間の始まりの整合の検査(措置法第91条の 2014 年から効く行を 2026-04-01 時点のコピーから転記するのは正しいので、健全に書けない)。新しい期間の列を optional にすること(古い記録に無い入力は §10.3 の補完で扱い、型は変えない)。版ごとに関数を分けて出すこと(`stamp_duty_v3`、SQL の `_version` の列。条文が入力で決めている切り替えを呼び出し側にもう一度書くことで、二重実装の再発(§16 第一)である)。
### 15.72 配る種(2026-09-19、保留)
**決定**:毎年書き換わる給与計算の規則を `.rule` と九言語の生成物で配るライブラリを、道具とは別のリポジトリで育てる。種は `experiments/library/` に置いた(五本の規則、出典のコピー、三本の workflow、免責を書いた README)。走らせて確かめたことは三つ。47 支部の料率は表で `apply` に渡せるので、定数の読替え `param` は要らない。有効期間の失効は日付の入力の `range` の上端で、上端を過ぎた日付は九言語とも入口で受け付けないので、新しい仕組みは要らない。令和8年4月分からの子ども・子育て支援金 0.23% は §15.71 の「形が変わる改正」の実物で、月分の表で畳めた。準用を実データで回して生成器の欠陥が五つ出た(丸めた定義の尺度、SQL の丸め関数名、四言語の `half_down`、丸めた値の区間、裸のリテラルの定義の型)。全部直し、`tests/apply.rs` の束の試験で固定した。
**保留**:ここでいったん止める。残りは `experiments/library/README.md` の「保留中(TODO)」に置いた。rulec 側に足すのは `source = file` の `url` と `outdated` の file 対応、それらを含む次のリリース。種の側で決めるのは、リポジトリの名前とライセンス、雇用保険の端数の出典、47 支部の保険料額表を持つかどうか、事業主負担の額を出すかどうか。
### 15.73 Shopify は標準入出力をやめていた(2026-09-19)
**きっかけ**:`.rule` を Shopify Functions のような置き場へ配るところまでを道具にするか、という問いから、相手の現状を確かめた。
**分かったこと**:Shopify Functions は `wasm32-unknown-unknown` に移っていた。extension target ごとに引数も戻り値も無い関数を export し、入力の読み出しと operations の書き出しはホストの import 関数で行う(Shopify Wasm API。`shopify_function` crate 1.0 以上と `@shopify/shopify_function` 2.0 以上が包む)。Wasm ファイルは 256 kB 未満、一回の実行は 1,100 万命令まで、入力の形を決める `input_query` は 3,000 バイトまで。§15.64 の「WASI の段は Shopify のような置き場の証拠になる」は、もう成り立たない。移った先は §15.64 で足したほうの形である。
**決定**:古くなった記述を五箇所直す。`docs/backends.md` の節(二つの形の説明と、Shopify がどちらを取るか)、`docs/generated-code.md` の WASI の節、`src/runtest.rs` の註、サイトの `generate.md` の英日。WASI の段そのものは残し、例に挙げる相手を Fastly Compute・Spin・サンドボックスの中のバッチに替える。ついでに、飛ばした Wasm の段が `--require-all` では落ちることを文書に書く(`out.skipped` に積んでいるので前からそうだが、「言語の数には入れない」だけを読むと逆に取れる)。境界の crate から呼ぶのは `wasm/` の JSON の ABI ではなく `rust/<alias>.rs` の型の付いた関数にする、と書き足す。JSON の往復は 1,100 万命令の予算に対して無駄で、ここでは何も買わない。
**理由**:文書が事実と違うと、読んだ人は動かないものを作る。しかも今回は「対応していない」ではなく「**していた対応の説明が古い**」形なので、走らせるまで気付けない。事実の寿命が短い相手のことは、書くなら日付と数字で書くしかない。
**捨てたもの**:Shopify の関数の形を生成する案を、もう一度捨てる。§15.63 で捨てたときの理由(相手の入力の形を追いかけることになる)に、この移行そのものが証拠を足した。対象も扉も入力の形も動く。動くものは相手の crate の中に置く。`rulec deploy` の案。Partner の組織・アプリ・開発ストア・OAuth が要り、通信して状態を持つのに、証拠を一つも生まない。`check` が固定したコピーだけを読み、通信するのは `test` だけ、という性質と引き換えにするものが無い。組み上げるところまでが道具の仕事で、配るのは相手の CLI である。
### 15.74 二つの Wasm に別々の名前を付ける(2026-09-19)
**きっかけ**:§15.73 で古い記述を直したあと、なぜ古いまま残ったのかを見た。`rulec test` の出力に `ok … (Rust, Wasm)` と `ok … (Wasm)` が並んでいる。前者は runner を `wasm32-wasip1` で組んだ標準入出力のプログラム、後者は `wasm32-unknown-unknown` の呼ばれるモジュールで、まったく違う形なのに、名前が同じである。**混同は書き手の不注意ではなく、名前が一つしか無かったことの結果だった。**
**決定**:形ごとに名前を分ける。`Backend::wasm` のフィールドを `wasi` に改名し、`--format json` の `via` の値 `wasm` を `wasi` に、行に出る `(Rust, Wasm)` を `(Rust, WASI)` に、飛ばしの文言を「WASI 側」にする。これで `wasm` は言語の名前、`wasi` は言語への行き方の名前になり、両方を一行に並べても読める。
**`--format json` の非互換な変更である。** `via` の値は `docs/formats.md` に載っていて `tests/formats.rs` が集合を固定している、つまり安定した API として扱ってきたものである。それでも変える。値は診断コードではなく説明で、しかも「混同が実際に欠陥を生んだ」証拠がある。1.0 より前に一度で直すほうが、間違った名前を約束として抱えるより安い。
**`rulec api` に `wasi` の項を足す**。`rust` の項の中に入れる。登録簿がこれをバックエンドの性質として持っているからで、二つ目の言語がフィールドを持ったら同じ鍵がその言語の項に出る。中身は `source`・`module`・`build`・`run`・`wire`・`needs`。build 行は `codegen::WASI_RUSTC_FLAGS` から組み、`backend.rs` の `wasi_rustc` も同じ定数を使う——`api` と `test` が二通りに組むことが構造的に起きないようにした(`WASM_RUSTC_FLAGS` と同じやり方)。`tests/api.rs` に一本足して、api の build 行で実際に組み、api の run 行で走らせ、ネイティブの runner の出力と一字一句比べる。手元に wasmtime と wasm32-wasip1 があれば本当に走る。
**フィールドは一つで閉じる、と書く。** WASI のフィールドは*形*を証明するためにあり、Rust が一つ証明すれば主張は満ちる。TinyGo・Javy・ruby.wasm を並べても `rulec test` の前提が増えるだけで、証明する事実は増えない。八つの `None` が「未対応の一覧」ではなく決定だと読めるように、フィールドの doc コメントと `docs/generated-code.md` に書いた。
**SQL の方言を書く。** 生成する SQL は Postgres 向けで、一致検査は SQLite で回している。この二つは「整数どうしの `/` はゼロ方向に切り捨てる」で一致するので、後者で証明することが前者について何かを言う。BigQuery の `/` は常に `FLOAT64` を返し(整数の割り算は `DIV`)、Snowflake は scale の付いた `NUMBER` を返す。生成物に実際に出ている `(ABS("_raw_fee") / 20 + 1) * 20` は、そこへ貼った瞬間に丸めでなくなる。**証明が見張っているまさにその場所で、静かに**。どちらにも手元にエンジンが無いので、移すなら意図してやり、`rulec verify` で本物に当てる、と `docs/generated-code.md` に書いた。
**Google Apps Script のレシピを `docs/backends.md` に置く。** `import xlsx` に出口が無かった。承認する人が自分の表から出ずに済むのは、この道具の役割分担(表を承認するのは人)にそのまま乗る。技術的には生成する JavaScript がそのまま動き、要るのは二つだけ——Apps Script に ES module が無いので `export` を落とす(手ではなく `sed` の一行で。`gen --check` を壊さないため)、そして生成物の整数は全部 `bigint` なのでセルとの境目で変換する。2^53 の話は円では当たらないが、書かないと読んだ人が迷う。
**捨てたもの**:GAS を十番目の生成対象にする案。走らせられない `.gs` は `rulec test` に乗らず、比較の外の生成物は主張の外である(§15.46)。BigQuery と Snowflake の UDF を対象にする案。手元にエンジンが無いので同じ理由で落ちる。加えて用途のほうも、「過去の全件に新しい表を当てる」は `replay` と `diff` がすでに答える仕事で、倉庫の側には行のトレースも承認者向けの描画も無い。`rulec deploy`(§15.73 で捨てたもの)と同じ線の引き方をした。
### 15.75 Rego の算術を測った(2026-09-19)
**きっかけ**:十番目の対象を考えたとき、BigQuery と Snowflake は手元にエンジンが無いので落ちた(§15.74)。OPA は違う。`opa eval` は単体のバイナリで、オフラインで、決定的で、JSON を読んで JSON を返す。**比較の外ではなく中に置ける唯一の候補**だった。だが規則は「全部整数、丸めは宣言したものだけ」で持っていて、Rego の数は JSON の数、`/` は有理数を返す。ここが通るかどうかは文書からは分からないので、測った。`experiments/opa/` がその全部で、`run.sh` 一本で再現できる。
**分かったこと(OPA 1.20.2 / Rego v1)**:
- **Rego の数は float64 ではなく厳密な有理数。**`floor((2^53+1)/1)` が 9007199254740993 のまま返り、`123456789 * 987654321` が 121932631112635269、int64 の最大値が一字一句往復する。いちばん怖かった「料金の下の桁が静かに落ちる」は起きない。
- **五つの丸めは無損失で Rego に書ける。**`gen` が書く丸めの単体ベクタ 270 件が全一致。同じ参照実装から作った厳しい版 27,005 件(|x| は int64 max まで、刻みは 1〜1,000,000,007、両符号)も全一致。0.4 秒。Rego では本体が外れたケースは誤りではなく「無い」なので、一致数とは別に**答えが出た数**を数えている。
- **負の向きの食い違いは、書き方で消える。**Rego の `floor` は −∞ 方向、規則の `round down` はゼロ方向。だが絶対値で計算して符号を戻す——生成するヘルパがすでに取っている形——にすれば差は出ない。
- **規則一本が丸ごと一致する。**ec261(表三段、50% 刻みの率、尺度つきの丸め)の 94 件のベクタが全一致。入口の番人も、値が無いのではなく error のオブジェクトを返せた。
- **`/` を裸で出してはいけない。**生成する SQL は尺度の割り戻しを `((raw / 2) * 2) / 2` と書く。これは整数除算が切り捨てることに依存していて、Rego では切り捨てない(`(7/2)*2` は 7)。**そして ec261 の 94 件のベクタはこの欠陥を捕まえない。**あの規則では積がたまたま常に偶数だからで、素朴に書き直した版は 94 件全部を通る。尺度 100・値 12345 で `123.45` が出て初めて見える。捕まえるのは丸めの単体ベクタのほうで、**言語ごとに丸めの検査を別ファイルで出すという判断(§15.46 の周り)が、そのまま効いた。**
- **値が無いことは静かである。**`_base("xl")` は誤りではなく「値が無い」で、呼び出し側では拒否と区別が付かない。これは OPA の現場の事故そのもの(列挙に値が増えて誰も行を足さず、`default allow = false` が黙って拒否する)で、生成する Rego は必ずオブジェクトを返すべきだと決まる。Wasm の対象が `{"error":…}` を返すのと同じ約束である。
**決定**:測った結果を `experiments/opa/` に残す。バックエンドにするかはまだ決めない。**算術はもう障害ではない**ので、次に見るのは文法のほう(`fold` と `count` の歩き方、`overrides` の順序、列挙の入口)である。作るときは §15.46 のとおり `gen`・`test`・`api`・文書の全部に載せる。
**捨てたもの**:測らずに「Rego は JSON の数だから危ない」で見送る案。危ないのは事実だが、危ないのは `/` だけで、しかも emitter 側の規則一つで閉じる。測らなければ、正しい相手を間違った理由で落としていた。
### 15.76 出典のファイルに、どこから来たかを持たせる(2026-09-19)
**きっかけ**:file の出典は sha256 でコピーに縛られていて、`check` も `pin` も効く。効かないのは二つだった。**どこから来たかが書けない**ので取り直せず、**もとが変わったかを問い合わせられない**。§15.72 の TODO に挙げたまま止まっていた場所である。
**決定**:`source 名 = file "…" url "…" sha256:…` の `url` を足す。`source fetch` は url があればそこから取り直し、`source outdated` は file も見るようにする。`check` は前と同じで、通信しない。
**一番の判断は「コミットは file にとっての `asof` である」**。法令に `asof <日付>` が付いているのは、e-Gov が「ある時点の姿」を出すからだった。ファイルにも同じ役が要る。そして git のコミットは日付よりきれいにその役をやる——`raw.githubusercontent.com/<owner>/<repo>/<commit>/<path>` は構造的に不変で、来年取り直しても同じバイトが返る。
**その帰結として、コミットで固定した URL のバイトを比べても意味がない。**永遠に一致するからである。だから問いを相手のリポジトリに向けた。`compare/<rev>...HEAD` の `files` にそのパスがあるかで「変わったか」を決め、あれば `commits?path=` で「何がいつ変えたか」を並べ、最後に**次に固定すべき URL** を書く。法令の側が「{date} 施行の改正で {引用箇所} が変わります」と言い、次に書く `source` の行まで出しているのと、同じ精度・同じ形にした。
**新しい種類は作らなかった。**`git "o/r" path "…" rev <sha>` のほうが `doc` で読みやすいのは確かだが、種類を増やすと parse・doc・`Cites:`・`api`・fetch の全部に分岐が増える。そしてよく見ると、**raw の URL はすでにコミットを含んでいる**。だから、どこまで細かく言えるかを文法ではなく `outdated` の中で決めることにした。相手が GitHub でなくても壊れず、GitHub でコミットなら自動的に細かくなる。`law` が `file` より細かく言えるのと同じ構造である。
**診断コードも足さなかった。**ブランチを指す URL は「取り直しても同じ内容が返る保証がない」ので `check` で警告したくなるが、W を一つ足すと codes.md・codes.ja.md・`explain`・その試験まで増える。`fetch` と `outdated` の出力で一行言う形にした。通信するときにしか関係しない事実なので、通信するコマンドが言えばいい。
**動かして確かめた**。OPA のリポジトリの README を 2024 年初頭のコミットで固定して `fetch`・`pin`・`check` を通し、`outdated` に聞いたら、そのパスを触った五つのコミットを日付と件名つきで並べ、次に固定する URL を出して exit 1 になった。最新のコミットで固定し直すと「変わっていません」で exit 0。ブランチ(`/main/`)に変えるとバイトの比較に落ち、「コミットを指す URL なら何がいつ変えたかまで言える」と註が出る。
**正直な限界**:PDF と xlsx は中身の差分が取れないので「変わった」までしか言えず、`outdated` はそう註記する。未認証の API は一時間 60 回なので、定期実行なら `GITHUB_TOKEN` か `GH_TOKEN` を渡す(渡されていれば使う)。非公開のリポジトリは鍵が要る——法令の側は鍵なしで済んでいたので、ここで初めて認証が前提に入る。
**捨てたもの**:url を必須にする案。人から受け取った文書には住所が無い。`check` で URL を取りに行く案。`check` が通信しないことは、この道具の性質そのものである。
### 15.77 十番目の生成対象に PHP(2026-09-20)
**決定**:PHP を十番目の生成対象にする。§15.20 が Ruby を五つめに置いたときの三つの理由が、そのまま全部立つ。
**当たりが近い**——この道具が狙うのは運賃表・手数料・割引・可否判定で、その種のコードが日本の商売で実際に住んでいるのは WooCommerce と EC-CUBE と Laravel である。Rails より母数が大きい。**一致検査に何も足さずに乗る**——`php` は一本のバイナリで、`ext/json` は 8.0 以降コンパイル時に組み込まれていて外せないので、ランナーに composer が一つも要らない。`node` だけで走る TypeScript、`rustc` だけで通る Rust と同じ性質で、§15.13 の関門をそのまま通る。**形が既知**——綴りは C 系(`{}`・`&&`・`!`)で TypeScript と同じ一行で済み、割り算はゼロ方向の切り捨てで Go・Rust・Swift と同じ側である。
**決定(型を書く)**:引数の型を宣言し、`declare(strict_types=1)` を付ける。だから入口の番人は Go・Rust・Swift と同じ「範囲だけ」であって、Python・TypeScript・Ruby と同じ「整数かどうかも見る」ではない。載せられないのは単位だけで、そこは Ruby と同じく署名の註と `rulec api` で言う。列挙は 8.1 からの native な backed enum にして、裏の値を原本の名前——ワイヤに出る綴りそのもの——にした。`CouponKind::from($s)` が変換の全部になり、表を二度持たない。
**決定(`/` を一つも書かない)**:割り算はすべて `intdiv`。PHP の `/` は割り切れなければ float を返し、float が正確なのは 2^53 までで、E108 が証明しているのは int64 である。**証明が見張っているまさにその場所で、静かに下の桁が落ちる。** §15.75 で Rego について測った `(7/2)*2` が 7 になる欠陥と、同じ形の罠である。生成器の式は `//` を常に自分の括弧の中に書くので、その括弧ごと `intdiv(a, b)` に書き換える。
**決定(下限は 8.2)**:使う構文は 8.1 までのもの(enum、`readonly`、コンストラクタの引数昇格)だけ。それでも下限を 8.2 と書くのは、8.1 のセキュリティ対応が 2025-12 に終わったからである。`rulec test` は PATH にある `php` で走らせ、`-n` で php.ini を読まない——Python の `-B` と同じで、**走らせる物が誰かの手元の設定に依存しないため**。
**走らせて見つけたもの**:`(a // b)` の書き換えで、括弧が無い場合を諦めていた。式そのものが一つの割り算のとき `unparen` が外側の括弧を外すので、演算子が最上位に来る。生成物には `… // 20000;` が残り、PHP はそれを行コメントとして読むので、文の終端が無くなって構文エラーになる。**コーパスでは起きず、`tests/apply.rs` の束(呼び先の出力を尺度から戻す形)だけが踏んだ。** 括弧が無ければ operands は文字列の両端、と直した。
**測った**:コーパス 28 本すべてで、参照評価器と生成 PHP がベクタ全件で一字一句一致した。丸めの単体ベクタ 270 件も全一致。畳み込み(`fold`)・数え上げ(`count`)・日付・複数出力・準用のどれも通る。**下限は仮定ではなく走らせて確かめた**——8.2.33 でも 8.5.10 でも、送料と全国運賃のベクタが期待値と一字一句一致し、丸めの 270 件も通る。
**整形について**:生成物は PSR-12 の形(クラスと関数の `{` は次の行、制御構造は同じ行)で書く。ただし Go の `gofmt -l` や Python の `ruff` のように**突き合わせる相手が処理系に付いてこない**ので、§8.5 の「自分で整形し、あとで外の道具に頼らない」の後半——整形器に掛けて確かめる——はここでは効かない。効くのは `php -l` と、読み込みが通ることで、`tests/shapes.rs` がその二つを見る。
**捨てたもの**:`json_encode` で記録の行を書く案。既定では `/` を `\/` に、非 ASCII を `\uXXXX` に、タブを `\t` にするので、ほかの十の対象とバイトが揃わない。ほかの言語と同じく、記録は自前で組み立てる。MCP サーバと承認者向けのページを PHP でも出す案。扉は「ただで付いてくる言語」に出すもので(いまは Python・TypeScript・JavaScript)、PHP に出しても証明する事実は増えない(§15.74 の「フィールドは一つで閉じる」と同じ線)。
### 15.78 十一番目の生成対象に Java(2026-09-20)
**決定**:Java を十一番目の生成対象にする。README が §15.13 以来「対応予定」と書いてきた約束の回収であり、届く先はほかの十が届かない場所——保険料率表・手数料表・可否判定が動いている JVM で、この道具が語彙を借りた DMN の実装(Camunda、Drools)が住んでいるところである。
**唯一の山は JSON だった**。Java の標準ライブラリには JSON が無く、入れる提案(JEP 540)は現在の LTS の次以降に向けた incubator である。だが**この勘定は一度払ってある**——Rust のバックエンドはクレートを取らない約束なので、そのランナーはワイヤを手書きの読み取りで読む(168 行)。それを Java の綴りに移した。ほかに要るものは無い。`javac` と `java` はどの JDK にも入っていて、Maven も Gradle も依存も無い。
**決定(`long` が int64)**:E108 が証明している範囲と機械語がここでは同じものなので、`bigint` に相当する措置も入口の検査も要らない。あふれたときは Go や Rust と同じく回り込む。
**決定(下限は 17、最新の LTS ではない)**:生成物について決めるのは**どこまで古い処理系で走るか**であって、いま最新の版ではない(§15.20 で Ruby 3.x と 4.x の両方で動かすと決めたのと同じ形)。`javac --release 17` を `rulec test` が渡すので、下限は仮定ではなく**毎回証明される**。使うのは enum・record・`List.of` だけ。**手元の三つの JDK(21.0.10、25.0.3、26.0.2.1)で、組んで走らせて確かめた**——21 と 25 でも送料のベクタ 70 件が期待値と一字一句一致する。Kotlin と Scala を対象にしないのはこのためで、生成した class をそのまま呼べる。
**決定(機械に依存する二つを固定する)**:`-encoding UTF-8`——18 より前の JDK はソースをプラットフォームの文字集合で読むので、日本語の識別子が化ける。`String.format` にはすべて `Locale.ROOT`——数字を自前で持つロケールでは `%04d` がほかの十とは違うバイトを書く。どちらも「事実がよそに依存している場所」を潰す措置で、§8.5 の「生成物が手元の道具の版に依存してはいけない」と同じ線である。
**決定(全部を一つの public class に入れ子にする)**:列挙も、二つの例外も、`Fired` も `Output` も静的メソッドも、規則の名前を持つ class の中に置く。トップレベルに出すと、同じディレクトリに生成した二本の規則がどちらも `RuleInputError.java` を名乗ることになる。その帰結として**ファイル名が別名ではなく class 名になる**(`shipping_fee` ではなく `ShippingFee.java`)。
**走らせて見つけたもの**:二つ。(1) `rulec test` が Java の規則の段を**黙って飛ばしていた**。`runtest.rs` が「その言語のディレクトリに別名で始まるファイルがあるか」で生成の有無を判定していて、Java だけは別名で始まらないからである。丸めヘルパの段は名前が固定なので通り、**規則の段だけが消えて、それでも緑だった**。登録簿に「そのバックエンドがファイルをどう名づけるか」のフィールド(`stem`)を足し、判定をそこから引くようにした。§15.21 の「データで言えるものは登録簿に」に、一つ足りていなかった項目である。(2) 記録を組み立てる `String.join` の末尾に空文字を置いていて、出る行に余分なコンマが入った。要素が無いときに `String.join(",")` と書けることを使い、空文字をやめた。
**直した検査**:文書の言語一覧を登録簿と突き合わせる `tests/docs.rs` の検査が、部分文字列で見ていた。**`JavaScript` は `Java` を含む**ので、JavaScript の名前を出す段落はすべて Java の名前も出していることになり、**Java を落とした段落だけをちょうど見逃す**。語として(前後が英数字でない)見るように直した。サイト側の同じ検査も同じ。
**測った**:コーパス 28 本すべてで、参照評価器と生成 Java がベクタ全件で一字一句一致した。丸めの単体ベクタ 270 件も全一致。畳み込み・数え上げ・日付・複数出力・準用のどれも通る。`javac -Xlint:all` は警告を一つも出さない(`tests/shapes.rs` が警告を失敗として扱う。Swift と同じ理由で、DO NOT EDIT と書いてある物の警告は誰にも消せない)。
**実コスト**:PHP が 1,098 行、Java が 1,293 行で、どちらも `src/codegen/` の下の自分のファイルに置いた(SQL と同じ形)。TypeScript の 700 行、Ruby の 500 行より大きいが、増えた分のほとんどは「なぜそう決めたか」を書いた doc コメントである。**そのほかは二言語あわせて 55 ファイルに +848 / −207** で、内訳は登録簿の二行、`Lang` の二つ、`cell()` と `api()` の二箇所、試験四本、文書と図と skills のコピー。§15.20 が Ruby のときに数えた「手で直したファイル 22 個」との差が、§15.21 の登録簿が買ったものである。
**捨てたもの**:Kotlin を対象にする案。§15.13 が「コンパイルが遅く CI の時間が目立つ」と書いたとおりで、しかも JVM では生成した Java をそのまま呼べる。**README の「Java / Kotlin は対応予定」から Kotlin を落とした**——守る気のない約束を看板に残さない。`java Runner.java` の単一ファイル実行を使う案。複数ファイルのソース実行は 22 以降なので、それに寄せると 17 と 21 を捨てることになる。`javac` はどの JDK にも入っているので、失うものは無い。C# を同時に入れる案。`dotnet run app.cs` と `System.Text.Json` で Java より安い可能性はあるが、手元で測っていない。**測っていない相手は、候補のまま置く**(§15.75 で OPA についてそうしたのと同じ)。
### 15.79 生成物を「プラグイン」として配る案(2026-09-20、却下)
**きっかけ**:「出力先言語それぞれのメジャーなプラグインシステムに乗ったプラグインとして出力できるモードもあるといいのでは」という問い。
**分かったこと**:この案は**二つの別物**が混ざっている。分けると答えが逆になる。
**(a) パッケージのマニフェストを出す**(npm・gem・composer・Maven・cargo・pyproject)。**これは設定である。** パッケージ名、版、ライセンス、名前空間、組織のスコープ——どれも `.rule` からは出てこない。出すなら発明するか、新しいフラグか設定ファイルを増やすことになり、「ランタイムも設定も無い」という看板と正面衝突する。しかも**一致検査に乗らない部分**でもある。`verify` が押さえられるのは答えであって、マニフェストの中身ではない。
すでに引かれている線がそれを説明する——**ファイルが単体で走らないときだけマニフェストを出す**。`go.mod` が出ているのはそれが無いと `go run` できないからで、cargo のプロジェクトを出す案は §15.64 で捨てた。この線で見ると、十番目と十一番目を足しても**新しく要るものは無い**(PHP は `require_once` 一行、Java は `javac` 一発で走る)。配って使われる形にしたいものは規則ごとではなく**ライブラリごとに一度**あればよく、それは §15.72 の「配る種」の側の仕事である。
**(b) ホストのプラグインの形で出す**(WordPress、Shopify Functions、Rails engine、Java の SPI)。**これは §15.63 と §15.73 で二度却下していて、理由は今も有効である。** 相手の入力の形を追いかけることになり、その形は動く——Shopify は一年のうちに `wasm32-wasip1` から `wasm32-unknown-unknown` へ、標準入出力から host import へ移った。さらに悪いことに、**生成した骨組みは、古くなった文書より危ない**。文書なら「古い」と読めるが、生成物は権威の顔をする。
**決定**:モードは作らない。代わりに、この道具が既に出している「扉」の条件を言葉にして置く——**すでに話しているワイヤ(JSON)で、SDK 無しで、`rulec test` がベクタで回せること**。MCP サーバ(§15.44)と `_page.html` と Wasm の canonical ABI(§15.64)はこの三つを満たしたから出ている。満たさないものはレシピとして `docs/backends.md` に置く(Shopify と Google Apps Script が既にその形である)。
**残る候補は二つだけ**。この物差しを当てると、プラグインと呼べる相手で関門を通るのは **OPA のバンドル**(`opa eval` は単体バイナリ・オフライン・決定的。算術は §15.75 で測り済みで、残るのは文法)と **Extism**(標準の ABI 一つでホスト言語十数種に一度に届く。ただし PDK を依存にできないので host import を生成 Rust に手書きする必要がある)である。どちらも「モード」ではなく登録簿の一行、すなわちバックエンドとして入れる。なお **Extism の価値は、生成しない言語への最短路であることにある**——PHP と Java が入った分、その価値は下がった。二つの問いはここで繋がっていて、片方を選ぶと片方が小さくなる。
**捨てたもの**:`rulec gen --package name@version` のように、足りない設定をフラグで受け取る案。マニフェストはリポジトリに一度書けば二度と触らないもので、フラグは一度足せば永久に抱えるものである(§12.1「CLI は契約である」)。割に合わない。
### 15.80 同じ問い合わせを関数としても出す(2026-09-20、実施)
**きっかけ**:「PostgreSQL。SQL 関数としてそのまま、あるいは pgrx で Rust 出力を拡張として。Supabase・PostgREST・Hasura なら同じ関数が RPC として外から呼べる」という問い。§15.46 は `CREATE FUNCTION` を一度捨てているが、あれは**丸めを関数にする案**に対する却下で(SQLite で検査できなくなる。丸めこそ間違いの隠れる場所である)、規則そのものを関数にする案には当たっていなかった。
**測ったこと**(node の PGlite と、docker の PostgreSQL 18.6 で):
- 生成済みの `sql/<alias>.sql` を**初めて本物の PostgreSQL に流した**。コーパス 26 本・3,630 ベクタが全件一致。`rulec api` の `runs_on: ["postgresql", "sqlite"]` は、ここまで SQLite でしか裏を取っていなかった。
- 関数の中身は、生成した問い合わせ**そのまま**でよい。引数を一行だけの `<alias>_input` にする CTE を頭に足せば、`FROM "<alias>_input"` がそれを読む(`WITH` の名前が表を隠す)。**生成器が二つに分かれない**のが、この形を選んだ一番の理由である。
- **純 SQL の関数は危ない。** 範囲外の入力に対して、`_input_error` と一緒にもっともらしい数を返す(重量 0 で `fee 1200`)。その列を見ない相手には、間違った金額が 200 で返る。ほかの十一の対象は投げる。
- plpgsql にすれば同じ文で投げる(SQLSTATE 22023。PostgREST はこれを 400 にする)。`#variable_conflict use_column` が要る。引数と列は作りからして同じ名前になり、`LANGUAGE sql` の関数には「列を優先する」規則があるが、plpgsql は曖昧だと言って止まるからである。
- `RETURN QUERY` は宣言した型ちょうどを求める。record の列は式が作った型(int4)で宣言は bigint なので、明示のキャストが要る。**これはコーパスを流して初めて出た。**
- 別名が組み込み関数と衝突する規則がコーパスにあった(評価ランク → `rank`)。位置で呼ぶと `rank(VARIADIC "any")` と曖昧になる。**名前付き引数なら解ける**(可変長の組み込みは名前付きを受けない)ので、runner も文書も名前で呼ぶ。PostgREST の呼び方もこれである。
- **PostgREST(Supabase が動かしているものそのもの)に載せて確かめた。** 生成した一枚を流し込むだけで `POST /rpc/shipping_fee` が `[{"fee":1200,…}]` を返し、宣言の外の入力は **400** に `{"code":"22023","message":"重量 が範囲の外です"}` が乗る。`IMMUTABLE` なので `GET` でも呼べ、`Accept: application/vnd.pgrst.object+json` なら一件だけ返る。**`rank` も通った**——PostgREST は名前付きで呼ぶからである。
**決定**:`sql` バックエンドに二枚目のファイル `sql/<alias>_function.sql` を足す。新しい対象言語としてではなく、同じ問い合わせのもう一つの出し方として。中身は plpgsql の関数一つで、引数は入力、返るのは出力と表ごとに当たった行、`IMMUTABLE PARALLEL SAFE`。宣言の外の入力は 22023、W114 の矛盾は P0001 で投げる。頭に `DO` ブロックを置き、同じ名前の関数を引数に関わらず全部落としてから作る(`CREATE OR REPLACE` は同じ引数のときだけ置き換えるので、入力が変わった規則は古い多重定義を残し、名前で呼ぶ相手はそれを見分けられない)。一致検査は `sql/<alias>_function_runner.py` が `psql` で本物の PostgreSQL に流し、`rulec test` の一段として同じ記録と一字一句突き合わせる。繋ぎ先は libpq の環境変数で、`psql` が無いか繋がらないときは toolchain が無いときと同じように言って飛ばす(`--require-all` はそれを失敗にする)。走り終えたら関数を落とすので、人のデータベースに残らない(§15.250 で、実行ごとのスキーマに作り、スキーマごと落とすようにした)。`rulec api` は `sql.function`(file・name・signature・language・runs_on・raises)を出す。引数は `columns`、返る列は `outputs` と `rows` なので、一覧を二度は書かない。
**理由**:§15.79 が言葉にした「扉」の条件——すでに話しているワイヤで、SDK 無しで、`rulec test` がベクタで回せること——を三つとも満たす。値の綴りは問い合わせと同じ、`psql` は PostgreSQL に標準で付く client、検査は本物の PostgreSQL で全件回る。§15.46 の周りで BigQuery と Snowflake を落とした理由(手元にエンジンが無い)は、ここには当たらない。得るものも大きい。PostgREST と Supabase は、公開スキーマに置いた関数をそのまま HTTP から呼べるようにするので、サーバのコードが一行も要らない。§15.46 が「関数ではなく関係」と決めたのは今も正しい。これは置き換えではなく、隣に足す一枚である。一括の再計算は関係のほうがやる。
**捨てたもの**:**pgrx で Rust 出力を拡張にする案。** 狙っている置き場(Supabase・RDS・Cloud SQL)は、第三者の拡張を入れられない。中身は整数の算術なので速くする対象も無く、PostgreSQL のメジャー版ごとにビルドが要る。**関数を二枚出す案**(投げない `<alias>_row` と投げる `<alias>`)。投げないほうの形は関係に対する問い合わせが既に持っていて、名前も一つで済む。**Hasura を看板に載せる案**。Hasura が追えるのは `SETOF <追跡済みのテーブル>` だけなので、同じ形のテーブルかビューを先に追跡させる手当てが要る。できはするが、こちらが出す一枚では終わらない。文書には条件付きで書いた。
### 15.81 ClickHouse(2026-09-20、測って保留)
**きっかけ**:§15.80 と同じ問いの後半——「ClickHouse の executable UDF なら、別プロセスの実行ファイルを UDF として登録できるので、ネイティブバイナリを何も変えずに乗せられる」。
**測ったこと**(`clickhouse local` 26.10、172 MB の単体バイナリ。サーバは要らない):
- **生成済みの `.sql` は ClickHouse でエラーなく走り、26 本中 5 本が黙って小数を返した**(463 件)。`利用料 0.00055`(正 0)、`請求額 3809`(正 3800)、`給与控除額 14.5`(正 14)。`/` が Float64 だからで、整数除算は `intDiv`。通った 21 本は積がたまたま偶数だっただけで、§15.75 で Rego について書いた罠(「`/` を裸で出してはいけない」)がそのまま出た。**エラーにならないのが一番悪い。**
- executable UDF は動く。生成した Python をそのまま繋いで 70/70 一致。`TabSeparated` でも `JSONEachRow` + `return_name` でもよく、Cloud でも console から登録できる(public beta)。
- ただし「バイナリを何も変えずに」とはいかない。ClickHouse が渡すのは生の列(`{"dest":…}`)で、rulec の runner が読むのは `{"in":{…}}` である。十数行の橋渡しか、runner のフラグ一つが要る。
**決定**:入れない。**測った結果を残して、候補のまま置く**(§15.75 で OPA についてそうしたのと同じ)。方言の罠のほうは待てないので、`docs/generated-code.md` の「方言は主張の一部である」に BigQuery・Snowflake と並べて、測った数字ごと書いた。
**理由**:executable UDF は「別プロセスで既存の生成物を呼ぶ」以上のことを言っていない。登録簿の一行にすると `rulec test` に 172 MB の toolchain と linux 向けのビルドが増えるのに、証明される事実は一つも増えない。**やるなら方言のほう**(`intDiv` で ClickHouse 向けの問い合わせを出す)で、そちらは `clickhouse local` 一本で全件検査できるから関門は通る。代わりに生成器へ方言の分岐が入る。「一つの規則、一つの問い合わせ」という今の形に、最初の枝を入れることでもある。要望が出てから測り直す。
### 15.82 転記した表を、転記元に突き合わせる(2026-09-20、四段とも実施)
**きっかけ**:「既存の文書から `.rule` を取り出すのはモデルがやる。そこをもっと強く補助できないか。docling などと繋げないか」という問い。
**見立て**:穴は二つある。一つは**表が取り出せない**こと——PDF や Word をモデルに直接読ませるのは非決定的で、同じ表が二度同じ形で出てこないし、トークンも食う。docling・marker・MinerU・Textract の領分である。もう一つは**転記を誰も検査していない**ことで、こちらは README の「証明していないこと」の一番目そのものである。`check` は表の内部整合を見て、`test` は生成物と表の一致を見る。**表と原本のあいだには何もない。**
一つめだけを直すと、便利さだけが増えて、緑のままの転記の誤りは残る。二つめは、抽出結果を**固定したコピーとして規則の隣に置き、行に引用を持たせた**瞬間に手が届く。§15.68 が法令に対してやったことを、法令以外の文書に広げるだけでよい。だから順番は「抽出を良くする」ではなく「**抽出結果を出典の契約に入れる**」から始める。
**決定**:四段に分ける。順番が大事で、**抽出を良くする段(3・4)より、抽出結果を契約に入れる段(1・2)が先である**。逆にすると、便利な抽出器が付いたぶんだけ、緑のまま転記の誤りが通る場面が増える。四段とも同じ日に入った。
1. **`file` の出典にも引用箇所を持たせる**。コピーは文書の隣に置き、固定は法令と同じ機械(E037・E038・E039・W119)で効かせる。抽出は rulec が自分で読める形式(csv・md・xlsx)から。
2. **突き合わせの検査**。引用のある行の**出力の値**が、引用箇所に無ければ落とす(E116)。逆向きに、引用箇所の値のセルのうち、どの行も使っていないものを警告する(W120)。完全性検査が構造的に捕まえられない誤り(原本の行を一本落とした)は、これでしか出ない。
3. **docx を自前で読む**。zip と XML の読み手は §15.50 で xlsx のために書いたものがそのまま使える。社内規程は Word である。
4. **抽出アダプタ**(`extract/1`)。PDF とスキャンはここ。`verify` のアダプタ(§10.1)の一方向版で、最初の行に `impl` を名乗らせる。
**第一段でやったこと**:
- **`src/extract.rs`**(新)。文書から表を取り出す一段で、読めるのは csv・md(パイプの表)・xlsx。出てくるのは文書順の表の並びで、コピーは TSV。`rulec doc` に見せるための markdown の表もここが書く。
- **`src/xlsx.rs` を `book` と `sheet` に割った**。`grids` がシートを文書順に全部返す。**ブックの引用箇所はシートである。**空のシートも数に残す——`表2` が指すのは「見て二枚目のシート」であってほしいからで、説明だけのシートが一枚目にあるのはむしろ普通である。
- **`sources::check`** に file の引用箇所の段を足した。**診断は一つも増えていない**:E037(固定が無い、引用箇所の書き方が読めない)・E038(コピーが変わった)・E039(コピーが無い)・W119(引用の消えた固定)を法令の側からそのまま使う。W119 は二か所で同じ文だったので一つの関数にした。
- **`fetch`** が引いている表を文書から取り出して `<文書のパス>.fragments/<引用箇所>.tsv` に置く。**`url` の無い出典でも取り出す**——人から受け取った文書は住所を持たないが、手元にはあるからである。
- **`pin`** が引用箇所の固定行を `source` の下に書く。文書そのもののハッシュは上の行に残る。
- **`doc`** が引用箇所を**表として**載せる(markdown はエスケープせず、HTML は blockquote の中に `<table>` を置く)。承認する人の画面で、規則の表の真上に転記元の表が並ぶ。
- **`Cites:` のヘッダと `rulec api`** が file の固定も運ぶ(`file tariff.md url … sha256:… (表1 sha256:…)`)。
- **`outdated` の精度**。file の出典は今まで「バイトが違う」までしか言えなかった。引いている表があるなら、新しいほうから取り出して比べ、**どの表が動いたか**、あるいは**引いている表は動いていない**(=この規則が転記していないところが動いた)と言う。§15.76 が法令に対して持たせた精度が、file にも来た。
**理由**:
- **docling を依存にしない。** 依存ゼロのバイナリ一本、という線は動かさない。抽出器は子プロセスで、`verify` のアダプタと同じ扱いにする。`curl` と `git` を子プロセスで呼ぶ線(§15.68)の上にある。
- **抽出は `fetch` の中だけ。** `check` が通信も子プロセスも持たないことは、この道具の性質そのものである。
- **非決定的な抽出器でよい。** 出力は一度人が見て固定され、以後の検査は**固定に対して**行われる。docling の ML でも、モデルそのものでも構わない(§15.62 で verify の相手をモデルにしてよいと決めた線の、抽出側への適用)。抽出 API の比較記事が LLM 抽出の弱点として挙げる非決定性は、固定すれば消える。
- **アダプタ方式だと、文書を外に出すかどうかを利用者が選べる。** 社内規程を誰に読ませるかは、道具が決めることではない。
**引用箇所の呼び方**:`表<n>`(文書順、1 から)と `table<n>`。見出しで指す形(`"基本運賃"/表1`)は採らなかった。字句の上で識別子は `/` も `"` も含めないので構文が要るのと、順番がずれた場合も固定が落ちて(E038)黙っては間違わないからである。実例を数えてから足す(§15.68 が号の細分を見送ったのと同じ)。
**コピーの置き場**:`<文書のパス>.fragments/<引用箇所>.tsv`(`料金表.md.fragments/表1.tsv`)。**拡張子まで入った名前にする**——落とすと、同じ名前で形式の違う二つの文書(`料金表.pdf` と `料金表.md`)が一つのディレクトリを共有して、黙って互いの表を持つ。法令のコピーが `sources/law/<法令ID>@<日付>/` にあるのは、e-Gov の文書に手元のパスが無いからである。file の出典にはパスがあるので隣に置けば、PR で文書とコピーが並ぶ。形式は TSV——セルに `|` が出ても壊れず、表の一行が diff の一行のまま出る。
**測った**(markdown の料金表一枚と、xlsx 一冊で):
- 端から端まで通った。`check` が E037(文書のハッシュが固定されていない)と E039(表1 のコピーが無い)→ `fetch` が「表1 を取り出しました(3 行 × 3 列)」→ `pin` → `check` が ok → `doc` が規則の表の上に転記元の表を出す。
- **文書の中の一つの金額を書き換えると、E038 が二つ出る**。文書そのものと、その表と。挙げられるのは引いている `表 運賃表` で、コピーの diff は一行である。
- `表5` を引くと `fetch` が「この文書にある表は 2 個です」と言い、`check` は E039。`別紙1` を引くと E037 が「文書から引けるのは表なので、引用箇所は `表3` か `table3` と書きます」と言う。**`check` は文書の中身までは見ない**ので、何個あるかを言えるのは `fetch` のほうだけである。
- ブックのほうは、説明だけのシートを一枚目に持つ実物の fixture で確かめた。`表2` が運賃の表で、日付も率も書式の中の単位も xlsx の読み手が読んだとおりにコピーに入る。
- `.pdf` を引くと、`check` は「`.pdf` からは表を取り出せません。読めるのは csv, md, xlsx です。抽出器を繋げるまでは、この文書は `@料金表` と丸ごと引いてください」と言う。**拡張子は文書を読まなくても分かる**ので、これは `check` が言ってよい。`fetch` のほうは一行報告して次の出典へ行く(一つの PDF が、ほかの出典のコピーを取りに行くのを止めない)。
- `outdated` は、上流の前書きだけが変わったときに「引いている表は変わっていません。動いたのは、この規則が転記していないところです」、表の金額が変わったときに「引いている表のうち 表1 が変わります」と言った。
**判断したこと**:**文書そのもののハッシュは残す。**法令では「文書全体のハッシュ一つ」を捨てた(無関係の改正で全部の規則が落ちる)が、ここには当たらない。文書は手元にあって git の diff が読め、落ちるのはその文書を引いている規則だけで、直すのは `rulec source pin` 一回である。そして**前書きが変わったことにも意味がある**——「四月から一割増し」と書いた注が付いた文書は、表が一字も動かなくても別の文書である。だから一回の改訂で E038 が二つ出るのは、うるささではなく二つの事実である。
**第二段でやったこと**(突き合わせの検査、2026-09-20):
新しい診断は二つ、**E116** と **W120**。向きが逆で、**どこまで寛容に読むかも、わざと逆にしてある**。
- **E116「行の金額が、引いた出典のコピーにありません」**。引いた出典のコピーのどこにも出てこない出力の値を落とす。コピーの側は**どこに出てくる数でも数える**(`990円(税込)` の中でも `60cm以下` の中でも)——文書が現に見せている値を「無い」と言わないため。
- **W120「出典のコピーにある値を、どの行も使っていません」**。コピーのセルのうち**数だけでできているもの**が、表のどこかに出ているかを見る。出方は二つ、行がその値を書いているか、どれかのセルの比較がその値を含むか(`<=3kg` の一行が、コピーの `1kg`・`2kg`・`3kg` をまとめて転記している)。セル全体が数のものしか見ないので、`2026年4月1日改定` は金額に数えない。
- **表が丸ごと引いているときだけ W120 は出る。**行の引用は「この行はここから来た」としか言わない。だから引用した表の一部だけを転記した表は、引用を `table` の行から各行の末尾へ移せば、残りを問われずに E116 は残る。**逃げ道が検査を弱めない**形になった。
- **条件の数はコピーに突き合わせない。**閾値は転記するときに書き換わる(`1,949,000円まで` → `<=1949000円`。§15.40 に実例がある)のに対し、金額は書き換わらない。日付も比べない——文書が「令和8年4月1日」と書いていたら、`2026-04-01` は無いことになってしまう。
- 値の読みは**規則自身の字句解析器**を使う(`lex::number` を公開した)。`1,210円` の `,` を落として読めば、コピーの `1,210円` と規則の `1210円` は同じ額である。単位は次元まで畳むので `0.6m` と `60cm` も同じで、**単位の無い数はどの次元とも合う**——文書は単位を見出しに置くからである。
**測った**:**本物で測った。**`experiments/library` にある日本年金機構の保険料額表(`R08ryougaku.xlsx`、実際に取ってきた本物)と、そこから転記した `厚生年金保険料.rule` の等級表。取り出すと 64 行 × 7 列で、見出しの行も脚注の行も入ったまま出てくる。この検査を一度も見たことのない転記に当てて、**通った**(32 等級ぶんの標準報酬月額が全部コピーにある)。そのうえで 26 行目の `470000円` を `480000円` に変えると、**ほかの検査が全部緑のまま E116 だけが落ちた**——刻みにも載っていて、完全で、重なりも無い額だからである。これがこの二つの診断の全部である。
**承認する人の画面にも一行増えた**:「この表の金額は、引いた出典のコピー(料金表 表1)に出てくる値です(E116)」。`doc` は check を通った規則しか出さないので、この行は言い切れる。転記元の表はもともと見出しの下に引用されているので、**上に転記元の表、下に規則の表、間にこの一行**という形になった。顧客向けのページには出さない(診断コードを出さないため。§15.61)。
**正直な限界**:**W120 がよく効くのは一件ずつ並ぶ表で、数直線を区切る表には何も言わない。**年金の等級表は `報酬月額` の行の範囲が金額の線を覆い尽くすので、コピーの `全額`・`折半額`・等級の番号まで「どれかの行の範囲の中」に入ってしまい、転記したことになってしまう。コピーの列と表の列を対応づけていないからで、対応づけるにはコピーの側の意味を読まねばならない。一件ずつ並ぶ表(あて先 × サイズ → 額)なら、落とした行がそのまま出る(`m_w120.rule` がその形で、**完全性検査は緑のまま W120 だけが「1100円 を誰も使っていない」と言う**)。
**第三段でやったこと**(docx、2026-09-20):
- **zip と XML の読み手を `src/ooxml.rs` に出した。**xlsx のために書いたもの(ZIP の目次、DEFLATE、要素と属性と文字を歩く XML)を**一行も変えずに**移しただけで、docx 側から読めるようになった。§15.50 の「最初の依存を入れるか自前で持つか」の判断が、二つ目の形式で報われた形である。`src/docx.rs` は 90 行で、中身は Word の表の形(`w:tbl` / `w:tr` / `w:tc`)だけである。
- **結合セルは埋めない。**二列にまたがるセルは二列目を空のまま残し、縦の結合の続きも文書のとおり空にする。**コピーは証拠だから、文書が言っていないことを書き足さない。**列の対応は空を置いても崩れない。名前空間の接頭辞(`w:`)は書き手の選択なので、要素名は接頭辞を落として見る。
- セルの中の改行・タブ・段落の切れ目は空白一つにする(`990円` と `(税込)` がくっつかないように)。セルの中の表は、それを含むセルの文に流し込む——引用箇所は文書の表を数えたもので、人が目で数えるときもそう数える。
**測った**:fixture は `tests/docxbuild.py`(python3 の標準ライブラリだけ)で、本物の ZIP と、本物の `w:` 付きのマークアップを書く。加えて**実物のライブラリが書いた .docx でも読めることを確かめた**(`uvx --from python-docx`。結合した見出しセルを含む表二つ)。そのとき気づいたこと:**同じ表を .md で書いたときとコピーのハッシュが一字一句同じになった**(`a583ec8586bbf596`)。コピーは表の中身だけなので**形式に依らない**。文書が Word で配り直されても、表が同じなら固定は動かない。
**第四段でやったこと**(抽出アダプタ、2026-09-20):
`rulec source fetch <規則> --via <コマンド>`。子プロセスに文書のパスを渡し、標準出力の JSON Lines を読む。**一方向・一往復**——抽出は一つの問いだから、`verify` のような会話は要らない。
```
rulec → ./extract.py 料金表.pdf
← {"rulec":"extract/1","impl":"docling 2.4.0"}
← {"block":"table","page":12,"grid":[["あて先","運賃"],["近畿","990円"]]}
← {"done":true}
```
- **最初の行で名乗らせる。**`impl` は必須で、コピーの隣の `extractor.txt` に残り、`doc` が承認する人に見せる(「出典: 料金表 表1(料金表.pdf、sha256:…、docling 2.4.0 が読んだ)」)。**モデルがスキャンから読んだ表と、もともと表の形で配られていた表は、証拠として別の種類である。**法令のコピーが `revision.txt` を持つのと同じ場所である。
- **`done` を必須にした。**途中で死んだ抽出器の出した分をコピーとして保存すると、`表3` が黙って別の表になる。名乗りが無い・`done` が無い・終了コードが 0 でない、の三つはどれもコマンドを止める(exit 2)。**そのとき既にあるコピーは一文字も触らない**ので、承認済みのコピーに対する `check` は生き続ける。
- **表の番号は rulec が数える。**抽出器が `id` を付けても採らない。「文書順に n 番目の表」という `表n` の意味を、誰が読んでも一つに保つため。`page` は報告に出すだけ。表以外の block(見出しなど)は読んで捨てるので、多く言う抽出器に旗を足させない。
- **アダプタを使うのは、自分で読めない形式だけ。**csv・md・xlsx・docx は毎回同じに読めるので、`--via` がそれらのコピーを黙って書き換えると、固定が「誰が走らせたか」に依存してしまう。
- `rulec adapter <規則> --template docling` がテンプレートを出す。**その規則の出典のうち抽出器が要る文書を挙げる**ので、引数の規則ファイルは飾りではない。
**測った**:偽の抽出器(python3 十行)で全部の道を通した。取り出し→固定→`check`→`doc`(コピーの表が引用され、誰が読んだかが出る)→金額を一桁変えると E116。失敗の三つも、既にあるコピーが無傷のまま exit 2 になることを確かめた。**docling そのものは走らせていない**——テンプレートは docling の API に合わせて書いたが、この機械には入れていない。プロトコルの側は偽の抽出器で全部行使されている。
**捨てたもの**:docling を依存にする案、Python を抱える案。`check` が抽出する案。新しい出典の種類(`pdf`、`docx`)を足す案(§15.76 と同じ理由。種類を増やすと parse・doc・`Cites:`・`api`・fetch の全部に枝が増える。どこまで細かく言えるかは文法ではなく抽出器が決める)。markdown-it を依存にする案(欲しいのは表と見出しだけで CommonMark 全部ではない。忠実度が要る人は十行のアダプタで包める)。文書の一般モデル(読み順、図、数式、RAG のためのチャンク)——引用に要る三種(見出し・段落・表)で打ち止めにする。html の表(rowspan と入れ子が泥沼で、第一段には要らない)。
### 15.83 面積・体積・時間を足し、合成次元の穴を塞ぐ(2026-09-20)
**きっかけ**:単位の表が質量と長さで止まっている。面積・体積・距離・速度・明るさ・温度・気温・音量のうち、どれを足すべきか。
**見立て**:この七つは、いまの機構への乗り方で三つに分かれる。`unit_info` は単位を `(次元, 基準単位への係数)` の対で持っていて、換算は掛け算一発である。
- **そのまま乗る**:面積・体積・速度・照度。0 が「無い」を意味し、倍にすることに意味がある量。
- **原点がずれていて乗らない**:温度。℃ と ℉ は係数ではなく一次式で結ばれるし、`気温 × 2` に意味が無い。
- **対数なので乗らない**:音量の dB。足すと掛け算になる。
距離は `length` そのもので、足すものは無い(`ec261.rule` が `length[km]` で書いている)。後ろの二つは §15.84 で扱う。
**先に実測した穴が二つ。** どちらも面積を足せば真っ先に踏む。
1. **数量どうしの掛け算が通っていた。** §2.1 は「次元の掛け合わせはモデリングの誤りとしてエラーにする」と書いているのに、`Mul` が拾っていたのは金額×金額だけだった。`derive 面積 : length[m] = 縦 × 横` は `length[m]` として通り、`10m × 10m` が `100m` になって `>=50m` の行に当たる。`重さ × 長さ` も左辺の型 `mass[kg]` のまま通った。割り算はもっと悪い:割る数は**書いてある単位のまま**読まれるので、`重さ(mass[g]) ÷ 2kg` は 2000 ではなく 2 で割り、型も `mass[g]` のまま残っていた。`重さ ÷ 2m` も同じ経路である。
2. **宣言の行の余りが黙って捨てられていた。** 宣言を読む関数はどれも「欲しい語を探して残りを踏み越える」歩き方をする。`range >=0m2 <=100m2` は、単位に数字が書けないせいで `0m` と `2` に割れ、`>=0m <=100m` として通っていた——**範囲が百分の一のまま完全性が証明される**。`range >=0m <=100m 999 ゴミ` も通った。
**決定**:
1. `Mul` に「単位を持つ値どうし」の腕を足し、金額×金額の特別扱いをそこへ畳んだ。`Div` は、割る数が単位を持つなら左辺と同じ単位であることを求める(同じなら無次元数に消える、従来どおり)。次元が同じで単位が違うときは、換算していないことをはっきり言う別の文にする。
2. **E047**(宣言の後ろに余分な語がある)を足す。`range`・`round`・`contract_only` のどれにも属さない語で止まる。引用(`@…`)はそこで打ち切る。
3. 字句解析器で、**単位の二文字目以降に数字を許す**(`m2`、`m3`)。一文字目には許さない——数字は数のほうが先に食うので、許せば `万` の後ろを単位が飲み込む。
4. 次元を三つ足す。**面積**(基準は m²。`mm2 cm2 m2 a ha km2 坪 in2 ft2 yd2 mi2 ac`)、**体積**(基準は L。`mm3 cm3 mL L m3 kL`)、**時間**(基準は秒。`ms s min h d w`)。係数はすべて厳密な有理数で、坪は 400/121 m² である(一間は六尺、一尺は 10/33 m)。分が `min` なのは `m` がメートルだからで、これは選んだのではなく決まっていた。
5. `ec261.rule` の `delay` を `number` から `duration[h]` にする。条文が「二時間」「三時間」「四時間」と書くものを素の数で持っていたのは、入れる型が無かったからである。
**捨てたもの**:
- **ガロン。** 米国の 3.785411784 L と英国の 4.54609 L があって綴りは同じである。二つの大きさを持つ単位は、この表が持ってはいけない唯一のものである。
- **`length × length` を面積にする案**(次元解析の第一歩)。入れれば `縦 × 横` は通るが、`速度 = 距離 ÷ 時間` まで引きずられ、指数の代数と、それを生成コードの整数スケールへどう対応させるかが要る。§2.1 の前提は brand の混同防止までで、そこを越えない。積そのものが答えなら、入力として受けるか表で引く。
- **`mass[g] ÷ 2kg` を換算して通す案。** §2.3 の表は「数量 ÷ 数量定数(同次元)」と書き、実装と E115 の文面は「同じ単位」と言っている。換算するには `const_value` が左辺の型を知る必要があり、評価器と生成器を揃えて直すことになる。今回は**黙って 2 で割るのをやめる**ところまでにして、換算は別の決定に残す。
- **速度。** `km/h` の `/` は演算子なので `100km/h` は割り算に割れる。綴りを `kph` にするか字句解析器に手を入れるかを決める必要があり、道交法の速度超過のほかに使いどころが薄い。
- **`㎡` `㎥` `ℓ` を規則に書けるようにする案。** 表計算の書式にはこれらが出るので、**取り込みの側**(`import.rs`)でだけ `m2` `m3` `L` に畳む。規則の中の綴りは一つにする。
### 15.84 順序だけの次元——温度と音量(2026-09-20)
**きっかけ**:§15.83 で分けた三つのうち、残した二つ。温度は原点がずれていて、音量は対数で、どちらも `(次元, 係数)` の対には素直に乗らない。
**見立て**:乗らないのは**演算**であって、**順序**ではない。規則の中でこの二つがどう現れるかを数えると、出てくるのは閾値だけである——食品衛生法の保存基準(10℃以下、−15℃以下)、騒音規制法の区域と時間帯ごとの規制基準、安衛法の 85dB。差を取る規則も、二倍にする規則も無い。だから足すべきなのは新しい次元二つではなく、**次元に一ビット**である。
そしてその一ビットは、既にあるべきだったところに無かった。§2.1 は日付を「比較と範囲だけで、加減算は持たない」と書き、§2.3 の禁止事項にも「日付の加減算」と書いてある。**検査していなかった。** `derive 差 : date = 甲 - 乙` は型検査を通り、文句を言うのは範囲の E112 だけだった。
**決定**:
1. **E048**(この型には足し算も掛け算もありません)。`+ - × ÷` の前で、順序だけの型を弾く。対象は `date`、`temperature`、`sound` の三つ。日付にも同じ機構が効く——というより、日付のために書かれるべきだった機構である。
2. **温度**:`℃`(基準)と `℉`。`sound` は `dB` 一つ。
3. **℉ の換算は一次式**にする。`unit_offset` を足し、基準単位への換算を `v × f + o` にした。℉ は `(5/9, −160/9)` で、41℉ はちょうど 5℃、−40 は両方で同じ数である。ほかの単位は全部 `o = 0` なので、式はいままでの掛け算に戻る。**これが安全なのは演算が無いからで**、差には効かない(9℉ の上昇は 5℃ の上昇であって、−27.2℃ の上昇ではない)。E048 が差の作られないことを保証しているあいだだけ正しい、という関係になっている。
4. 字句解析器で `℃` `℉` を**識別子の先頭にも許す**。Unicode はこの二つを記号に分類するので、`temperature[℃]` の括弧の中が読めなかった。`円` と `銭` は漢字なので、この問題は起きていなかった。
**捨てたもの**:
- **℃ と ℉ を別々の次元にする案**(通貨と同じ扱い)。`unit_offset` が要らなくなるかわりに、`41℉` を `temperature[℃]` の列に書けなくなる。通貨を分けたのは**為替レートがこの道具の中にあってはならない**からで、℉→℃ は相場ではなく厳密な有理数である。理由が違うものを同じ形にはしない。
- **0.1℃ 刻み。** 感染症や学校保健の「37.5℃」は、いまの `temperature[℃]` には書けない。答えの候補は二つある——`mg`/`g` のように細かい単位を足す(`m℃`。誰もそう書かない)か、`rate[step 0.1%]` のような刻みを数量にも許すか。後者のほうが筋が良さそうだが、数量の刻みは実行時表現に触るので、別の決定に残す。**いまは度の整数だけである。**
- **dBA を dB と別に持つ案。** A 特性の補正は dB とは換算できないので、持つなら通貨と同じく別の次元になる。騒音規制法の条文は「デシベル」としか書かないので、いまは分けない。
### 15.85 例のセルを、列の型に照らす(2026-09-20)
**きっかけ**:§15.83 の作業中に、`mass[g]` の列に `1lb` と書いた例が「どの表も発火しませんでした」としか言わないのに気づいた。表のセルなら、同じ字を E103 が指摘する。
**測ってみたら、もっと広かった。** `examples` は表の形をしているが `Item::Table` ではないので、`c.table(...)` に一度も渡されていない。**セルを見る検査が一つも走っていなかった。**
| 書いたもの | それまでの答え |
|---|---|
| `mass[g]` の列に `1lb` | E107「値が出ませんでした」(読めなかったセルではなく、出力の名前を言う) |
| 単位の要る列に裸の `500` | 同上 |
| 列挙に無い値 `幽霊` | **通る。** `-` の行に当たって、例は何も証明しないまま緑になる |
| `money[円]` の期待値に `800kg` | **通る。** 800円 と等しいと判定される |
| 宣言されていない名前の見出し | **通る。**(無視される) |
最後の二つが重い。§1.2 は `examples` を「実行できる仕様」と呼び、AGENTS §1 は「評価器と全生成言語が揃って間違えている誤りを捕まえられる唯一のもの」と書いている。その検査が、**一度も読んでいない単位に対して「はい」と答えていた**。
**決定**:`example_cells` を足し、項目を全部処理したあと(表の出力や `define` を名前として引けるようになってから)に走らせる。見るのは四つ——入力の列の名前、入力のセル(表と同じ `cell`)、出力の列の名前、出力のセル。**診断は一つも増やしていない**:E012 と E103 を、表の側からそのまま使う。並びをたどる規則の `sequence` のフィールドは `sequence` の名前であって入力ではないので、E025・E027 に任せて踏み越える。
**一つ落とし穴があった**:`cell` は読んだ名前を「使われた」に印を付ける。例をそのまま通すと、例にしか出てこない列挙の値まで使用済みになり、W111 が黙る。W111 が訊いているのは「**表**が使っているか」で、例は表ではない——専用の行を持たない値は、例に書いてあっても専用の行を持たない。だからこの一段は、終わったときに二つの集合を元に戻す。
**捨てたもの**:`examples` をそのまま `c.table(...)` に渡す案。`table` は出力のフィールドを `syms` に `TableOut` として入れ直し、W110 や列の値集合の収集まで走らせる。`t.name.is_some()` で守られている段もあるが、全部ではない。例は表ではないので、例のための一段を書く。
**ついでに**:`rulec import csv` が英語のとき、自分で作った下書きが E009 で落ちていた。表の名前が `table` になるからで(`named("表"/"table", "t")` は ASCII なら別名を付けないので、予約語がそのまま名前になる)、`decision` に変えた。ついでに、`count` や `source` を見出しに持つファイルも同じ事故を起こすので、予約語は `count_(count)` の形にして、綴りは別名として残す。**テストが全部日本語で書かれていた**(`.cargo/config.toml` が `RULEC_LANG=ja` を固定している)ので、英語の下書きは作られてから一度も検査されていなかった。言語を指定する走らせ方をテストに足した。
### 15.86 値が宣言された型に出会う位置を、全部数える(2026-09-20)
**きっかけ**:§15.85 の「例のセルを検査していなかった」を、v0.9.0 のバイナリを別に建てて裏を取った。本当だった。**では、ほかの位置はどうなのか。**
**やり方**:値が宣言された型に出会う位置を、文法から全部数え上げる。位置ごとに、無傷の規則を一本用意して、**一か所だけ**違う単位のリテラル(あるいは存在しない値、存在しない列名)に置き換え、診断が出るかを見る。`tests/mutants` が一本の規則に一つの誤りを蒔くのと同じ作法を、位置の表に対して回す。
**測った結果、六つが素通りだった。** すべて v0.9.0 で再現する。
| 位置 | v0.9.0 | 何が起きていたか |
|---|---|---|
| 例のセル(入力・期待値・見出し) | 素通り | §15.85。期待値が読めないと**比較ごと飛ぶ** |
| 出力の丸めの格子 | 素通り | `lit_value_in` が No を返すと**記録しない**。生成器は 1 単位に落ちる。`round up(10銭)` も `round up(10)` も 1円 に丸めた |
| 畳み込みの答え(`empty`・`exhausted`・`stop with`・`take_*`・`keep_max`) | 素通り | `money[円]` の出力に `empty -> 999銭` が通り、生成コードは **99900円** を返した |
| 型の刻み(`rate[step …]`) | E105(別件) | 読めない刻みは 1 に落ち、`0%〜100%` の範囲が生成コードの入口ガードで `0〜1` になった |
| 要素のフィールドの範囲 | 素通り | 入力と同じ `bounds_of` を呼びながら、**診断を捨てていた**(`let (b, _) = …`) |
| 群の一員 | 素通り | 値でない綴りは黙って外れる。47 県の群が一つ打ち間違いで 46 県になり、その県は既定の行に落ちる |
**どれも同じ一つの誤りである**:リテラルが宣言された型に出会い、誰も突き合わせなかった。表のセルではずっと E103 になっていたものが、隣の位置では何も言われない。
**決定**:六つとも塞ぐ。**診断は一つも増やしていない**——E103 と E012 を、表の側からそのまま使う。丸めの格子と刻みは、読めなかったときに黙って既定値へ落ちる経路を消して、読めないと言わせる。要素のフィールドは捨てていた診断を拾う。群は一員ごとに列挙の値かを見る。
**テストの形が本題である。** 六つを個別に直しても、次に足す位置がまた抜ける。だから `tests/positions.rs` を置いて、**位置の表そのもの**を回す。土台の規則が無傷であることを確かめ、一か所だけ置換し、決めたコードが出ることを求める。位置を足して行を足さなければ、その位置は誰も押さえていない。
さらに、六つのうち実物の規則に蒔けるものは `tests/mutants` にも入れた(`m_e103ex` `m_e103round` `m_e103fold` `m_e103elem` `m_e103step` `m_e012group`)。位置の表は「検査が走るか」を見るが、変異は「**実在する料金表で、いくら動くか**」を見る。
**なぜ 0.9.0 まで残ったか。** コーパス 28 本は全部**正しい**規則である。正しい規則では、検査が走らない位置と、走って通る位置が、見分けられない。緑であることが、検査が動いている証拠になっていなかった。変異が一本もその位置を踏んでいなかったのも同じ理由で、変異はコーパスから作るからである。**この二つを足して初めて、緑が意味を持つ。**
**捨てたもの**:畳み込みの腕を `expr_ty` で全面的に型検査する案。`held` や要素のフィールドなど、名前の側は既にほかの経路で見ている。いま塞いだのは**リテラル**で、測った穴もそこだった。名前の側まで広げるなら、偽陽性をコーパスで測ってからにする。
### 15.87 optional の不在の値は、参照器の中に無かった(2026-09-20)
**きっかけ**:§15.88 でコーパスに無い語を一つずつ走らせる途中、`T?` の規則が動かなかった。生成側を疑ったが、**参照評価器のほうが先に壊れていた。**
**測ったこと**:
- `eval.rs` の `Cell::Nothing` が `false` を返す。つまり `| none |` の行は**どの値にも当たらない**。領域の側(`region.rs`)は最初から不在の値を「列挙の値がもう一つある」ものとして持っているのに、評価器にはそれが無かった。
- 当たらないので、その行を狙うベクタが一件も作れない。`rulec coverage` は正しく「行 1 に届くベクタが無い」と報告していた——**道具は言っていた。誰も `optional` の規則を書いていなかっただけである。**
- 例の `none` のセルも何も束縛しない。だから E107 は「出力に値が出ませんでした」と言う。読めなかったセルではなく、出力の名前を言う。
- wire の表現も食い違っていた。`rulec schema` と生成コードの `_record` は JSON の `null` を書き、ベクタだけが `none` という語を書いていた。しかも `in_object` には同じ処理が二度書かれていて、**後ろのほうが動いていた**ので、前のほうを直しても何も変わらなかった。
**決定**:`Cell::Nothing` を「不在の値に当たる」にする。例の `none` は不在の値として束縛する。ベクタの wire は `null` にし、二重になっていた実装を一本に寄せる。
**これで初めて**、`optional` の生成コードが走るようになり、七つの壊れが出た(§15.88)。**参照側が正しくないうちは、生成側の誤りは見えない。**
### 15.88 生成物を、コーパスに無い形でも走らせる(2026-09-20)
**きっかけ**:§15.86 で位置を数えたが、それは**検査器**の話である。生成器のほうはどうか。行カバレッジは `codegen.rs` で 94.5%、全体 86.7% と高い。だが**行が通ることと、答えが合っていることは別**である。
**数え方**:文法の語を `tests/corpus/*.rule` に対して数える。コーパスは `tests/threeway.rs` が十二言語で走らせる唯一の材料なので、**コーパスに無い語は、生成コードが一度も実行されていない語**である。出てきたのは八つ——`not` のセル、`none` と `T?`、`string` の出力、`round half_even`、`constraint`、`take_first`、それと面積・体積・温度・音量。一つずつ最小の規則を書いて `gen` から `rulec test` まで通した。
**二つが壊れていた。**
**1. `string` の出力は、全言語で定数 0 を返していた。** 出力セルの生成に文字列の腕が無く、`_ => "0"` に落ちる。行の注記は正しい値を書いているのに、その隣の代入が 0 である。`check` は ok、参照評価器も正しい。生成コードだけが違う。バックエンドごとに一か所ずつ、計八か所。Rust だけは所有が要るので `.to_string()` を付ける。ついでに、出力が二つ以上あって片方が文字列の規則は Rust で**コンパイルも通らなかった**(`String` は `Copy` ではないのに `Output` が `Copy` を derive していた)。
**2. `optional` はどの言語でも動かなかった。** しかも**参照側も壊れていた**——`eval.rs` の `Cell::Nothing` がどの値にも当たらないので `| none |` の行を狙うベクタが一件も作れず、例の `none` のセルは何も束縛しなかった(§15.87)。そちらを直して初めて、生成側の壊れが出た。出たものは七つ:wire の表現が `"none"`(schema と `_record` は `null`)、Python/Ruby/PHP の runner が `null` を数として読む、TypeScript が `import type { Kind | null }`(構文として不正)、Rust と Swift と Wasm が型名をそのまま構築子として貼る、Go と Rust の**本体**が `*Kind`/`Option<Kind>` を中身と比べる。
**ついでに出たもの。** 出力セルは `Lit::Num` しか読んでいなかった。`money[円]` の列に `2026-04-01` と書くと、通算日がそのまま額になって **20544円** を生成する。入力セルの文字列は、どの腕にも当たらず `_ => {}` に落ちて、E102「到達しない行」として出ていた——真だが理由ではない。三か所(表・例・畳み込み)に一つの `out_lit` を通した。
**それと、検査器が落ちる規則があった。** `税込金額 ÷ 税込金額` のように変数で割ると、E115 が出る前に導出の区間計算が走り、0 を含む除数で `Rat::div` が assert して `rulec check` ごと落ちる。**診断より先に走るものが、診断を追い越してはいけない。** 0 を跨ぐ除数では区間を作らないことにした。
**決定**:
1. 八つの壊れを全部塞ぐ。`rulec test` で十言語一致を確認した(Java と NumPy は手元に道具が無い)。
2. **`tests/shapes.rs` に `agrees_everywhere` を足す。** いままでこのファイルは「生成物がコンパイルできる」だけを見ていた。コンパイルと一致は別物で、その差がちょうど今回の二つである。
3. **コーパスに無い語だけを集めた規則**(`SURFACE`)を置き、コンパイルと全言語一致の両方を回す。新しい次元の規則も同じく回す。
4. **変異を 36 本から 54 本に増やす。** 畳み込みと数えには変異が一本も無く、その診断は台帳の最小例だけで支えられていた。位置の表(§15.86)は「検査が走るか」を見るが、変異は「**実在する料金表で、いくら動くか**」を見る。
5. `tests/positions.rs` に**誤りの種類の軸**を足す。単位だけでなく、日付・文字列・真偽・刻みに載らない値を、位置ごとに。
**測り直し**:`cargo llvm-cov` で全体 86.7%(未到達 6,308 行)。低いのは `sources.rs` 65.7%(未到達 1,074 行、関数の 41% が未実行)、`vfs.rs` 43.9%、`ooxml.rs` 76.5%。`sources.rs` は網に出る経路を含むので、測れないのか書かれていないのかは別に見る。**そして今回の二つは、どちらも 94.5% の「通った側」に入っていた。** 行は通り、答えが違っていた。カバレッジは、この種の誤りを見つけない。
### 15.89 飛ばした二言語と、渡されないフラグ(2026-09-20)
**きっかけ**:§15.88 で「全言語一致」と書いたが、走っていたのは十言語だった。`rulec test` は道具の無い側を正しく飛ばし、飛ばした数も報告する。**報告を読んでいなかったのはこちらである。**
**測ったこと**:
- `/usr/bin/javac` は macOS のスタブで、JDK 本体が入っていない。`rulec test` の `ready` が `java` を見にいくのはこのためで(`backend.rs`)、判定は正しかった。numpy も未導入。
- **JDK と numpy をこの機械に入れた途端、`optional` で二件落ちた。** Java の runner が `long` を enum の引数に渡してコンパイルできず、NumPy は wire の `null` を `astype(str)` で `"None"` にしてから列挙の値でないとエラーにしていた。計画(`<alias>.json`)は `none` という語で不在を試すのに、**列が optional であることを計画が言っていなかった**。
- CLI のフラグを `main.rs` の `flag(...)` の宣言から数えると 35 個。うち二つ(`--budget`、`--outputs`)はどのテストも渡していなかった。`--budget` は **E109 に意図的に到達する唯一の手段**である。
**決定**:
1. Java の runner と、NumPy の計画+実行器に、不在の値の腕を足す。計画は列に `optional` を持たせ、列挙の値に不在の値を加える。
2. `--budget` と `--outputs` のテストを書く。さらに、**宣言したフラグが全部どこかのテストから渡されていること**を検査する。台帳・色づけの語彙と同じ形で、増やして放置すると落ちる。
3. `website/docs/playground/rulec.wasm` を焼き直す。生成器を変えたので、コミットされている wasm が古くなっていた——`tests/wasm.rs` がそれを捕まえた。**この検査は効いている。**
**測り直し**:行カバレッジは 87.7% から **89.6%** へ。関数 88.9%。低いまま残るのは `sources.rs` 65.8%(未到達 451 行、関数 59.0%)、`apply.rs` 71.9%(未到達 241 行、関数 69.1%)、`vfs.rs` 52.0%。**`apply.rs` は前回 `codegen.rs` を除いて測ったせいで見えていなかった。**
**教訓が一つある。** 道具が「飛ばしました」と言っているのに、こちらは「一致した」とだけ読んだ。`rulec test --require-all` は CI のためにあるが、**手元で走らせるときにも同じ規律が要る**。
### 15.90 準用と出典の、測れていなかったところ(2026-09-20)
**きっかけ**:§15.89 の測り直しで、`apply.rs` 71.9%(未到達 241 行、関数 69.1%)と `sources.rs` 65.8%(未到達 451 行、関数 59.0%)が残った。どちらも大きい。ネットワークに出ない `apply` から。
**`apply.rs` の空白は、形が一つ足りないことだった。** コーパスの準用は一本(`非常勤退職手当` → `退職手当`)で、その呼び先は `derive` も `constraint` も `not` のセルも、日付や真偽の入力も持たない。束縛はすべて名前である。空白はその裏返しで、展開の分岐がそのまま残っていた。
**そして、リテラルでの束縛がそもそも動かなかった。** 呼び先の入力に定数を渡す——「『A』とあるのは『B』と読み替える」そのもの——が、E041「そんな名前はこの規則にありません」で落ちる。構文解析は型を知らないので識別子を全部 `BindValue::Name` にし、検査の型表には `(Lit::Word, Ty::Bool)` の腕が、展開の側には定数で列を畳む部分評価が、**どちらも最初からあって、到達する道が無かった**。
**決定**:語が呼び出し側のどの宣言でもなく、呼び先の型の値であるなら、リテラルとして読む。名前が先に勝つ(検査も展開も同じ順)。どちらでもない語は E041 のままで、打ち間違いを飲み込まない。検査と展開の二か所で同じ判断をするので、片方だけ直すと食い違う。
呼び先に `derive`・`constraint`・`not` のセル・日付と真偽の入力を持たせた対を `tests/apply_fixtures/` に置き、展開の形(定数で束縛した列が消え、その列で当たらない行が落ち、呼び先の `derive` と `constraint` が呼び出し側の名前で入る)を押さえ、**十二言語で評価器と突き合わせる**ところまで回した。71.9% → 78.3%。
**`sources.rs` の 451 行は二つに割れる。**
- **ネットワークが要るもの(約 130 行)**:`outdated`(115)、`curl_json`、`fetch_json`、`fetch` と `fetch_fragment` の一部。e-Gov の改正一覧と、`url` の先を読む。ここは測らない。
- **ネットワークの要らないもの(約 320 行)**:`check`(121)、`transcription`(71)、`pin`(65)、`fragments`(42)、`accounted`(36)。**こちらは単に書かれていなかった。**
その一つが形式だった。文書は csv・md・xlsx・docx の四つを読むのに、`fetch` → `pin` → `check` の流れを通っていたのは md と docx だけで、csv と xlsx は `extract::tables` の単体までしか来ていない。**同じ表を四つの容器に入れて、コピーも固定も一つになること**を押さえた——コピーは表であって、ファイルではないからである。
**残り**:`check` の未到達は、引用先の無い出典・`url` を持つ出典・空の引用箇所・引用箇所のコピーの欠けと digest の食い違いといった、ネットワークの要らない誤りの経路である。次はここ。
### 15.91 出典の残りは、ほとんどネットワークだった(2026-09-20)
**きっかけ**:§15.90 で `sources.rs` の未到達 451 行を「ネットワークが要るのが約 130、要らないのが約 320」と見積もった。**この見積もりは古い測定に基づいていて、間違っていた。**
**測り直した内訳**(3 行以上の塊、計 319 行):
| | 行 | |
|---|---|---|
| `outdated` | 124 | e-Gov の改正一覧を引く |
| `fetch` | 36 | 法令と `url` の取得 |
| `suppl_index` + `suppl_index_near` | 29 | 附則の索引。どちらも `fetch_json` を呼ぶ |
| `moved_fragments` | 20 | `outdated` の下請け |
| `curl_json`・`fetch_json`・`curl`・`curl_once`・`fetch_fragment` | 58 | ネットワークそのもの |
| `check` | 18 | **ネットワークは要らない** |
| `pin` | 8 | **ネットワークは要らない** |
**つまり約 250 行はネットワークである。** 残りは `check` の誤りの経路と `pin` の書き換えで、こちらは書いた。
- **コピーが無い**(E039。探した先のパスを言う)
- **引用箇所が固定されていない**(E037。貼れるハッシュと、`fix` に固定の行そのもの)
- **引用の形が読めない**(E037。文書は表で引くので `表3` か `table3` と言う。法令は条で引くので別の文面になる)
- **`pin` が変わったコピーの固定を書き換える**(行が二本にならない)
66.8% へ。**残りを埋めるにはネットワークに出るしかない。** 出るなら記録した応答を再生する形(`replay` の出典版)になるが、それは別の決定である。
**見積もりを二度直したことのほうが教訓である。** 一度目は `codegen.rs` を除いて測ったので `apply.rs` が見えず、二度目は古い profdata を読んだ。**測り直すたびに前の数字を捨てる。**
### 15.92 コーパスの両側を数える(2026-09-20)
**きっかけ**:「正しい方も間違っている方も十分に整備したか」。数えていなかったので数えた。
**正しい側(コーパス 28 本)。** 全部が `threeway`(十二言語で評価器と突き合わせ)と `doc`(承認者向けの描画)を通る。`threeway` には「コーパスが全部載っているか」の守りもある。**穴は二つあった。**
1. **`coverage.rs` が 19 本しか監査していなかった。** ベクタ集合を五基準に照らす検査で、九本が台帳から漏れていた。しかも `threeway` にある守りがこちらには無かったので、漏れても誰も気づかない。九本とも足したら**全部が五基準を満たした** — 監査していなかっただけである。守りも足した。
2. **文法の十四語をコーパスが一度も使っていない**(`constraint`、`not` のセル、`none` と `T?`、`string`、`round half_even`、`take_first`、面積・体積・温度・音量、`import proto`/`jsonschema`、`url`)。§15.88 で `tests/shapes.rs` に置いて十二言語で走らせたが、**コーパスではないので `coverage` と `doc` は通らない**。コーパスは「実物からの転記」という性質を持つので、合成の規則をそこに入れるのは筋が違う。第三の置き場を作るかどうかは残す。
**間違っている側(変異)。** 36 本・33 コードから **68 本・59 コード**へ。今日足した内訳は、値が型に出会う位置が六本、畳み込みと数えが九本、式と `result` が三本、語と日付と刻みが六本、最後の五本、そして契約の二本。
**残る十五は、置き場が台帳のほうが正しい。** 構文の十二(E001〜E007、E013、E017〜E020)は、文字が一つずれただけで**動く金額が無い**。E109 は `--budget` が要る(規則の中身ではなく規模の話で、`tests/cli.rs` が渡す)。E110 は列の型とそのセルを同時に変えないと出ない(「一つの誤り」にならない)。W114 は導出どうしが入力を共有する形で、転記には無い。
**そして、変異そのものが古くなる道が開いていた。** `tests/make-mutants.sh` はコーパスから変異を焼くが、**それを走らせるテストも CI も無かった**。十四のテストファイルが `tests/mutants/` を読むのに、そこにあるファイルが「そのコーパス規則に一つ誤りを蒔いたもの」であり続ける保証が無い。`m0` は通り続ける——ディスクにあるファイルの診断コードを見るだけだからである。今日 `ec261.rule` を直したときも同じ危険があった。
**決定**:`make-mutants.sh` に出力先の引数を足し、`変異はコーパスから作り直せる` が一時ディレクトリへ焼き直して一字一句突き合わせる。手で置いたもの(コピーが要る E116・W120、契約が要る E032・E033)は名前を書くので、「生成しない」が決定として残る。**コミットされた `rulec.wasm` と同じ形で、そちらには最初から検査があった。**
### 15.93 構文の変異と、コーパスの届いていなかった語(2026-09-20)
**きっかけ**:§15.92 で「構文の十二は動く金額が無いので台帳の最小例で足りる」と書いた。**これは判断の誤りだった。** 変異の目的は §13 が「決めたコードと**文面**」と書いている通りで、文面には位置が含まれる。台帳の規則は四行で、六十行の表の正しい行を指せるかは見せられない。
**十二本とも蒔けた。** 文字列を閉じない、識別子を始められない文字、`rule` で始まらない一行目、行頭に語が無い、その位置に書けない語、`=` の無い宣言、無い方式、無い取込先、関係の形をしていない `constraint`、片側が表の出力の `constraint`、制約の外にある例、名前の無い `elements`。**いくつかは連鎖するが、連鎖も固定する** — 字句の誤りは実際に残りの行を頭無しにするので、しないふりのほうが嘘である。E110 も、型とセルの二か所を同時に変えれば蒔けた。**残るのは E109(`--budget` が要る。規模の話)と W114(導出どうしが入力を共有する形)の二つだけ。** 変異は 79 本・72 コードになった。
**そして測定がもう一度間違っていた。** 「コーパスが使っていない十四語」のうち `not` は**使われていた** — `| not: 遠隔地 |` の形で二本。正規表現が `not ` しか見ていなかった。数えるたびに正規表現を疑うこと。
**残った語を、コーパスに届かせた。** これらは文書の `reference.md` や `tour.md` には出てくるのに、**走っているのは `codes.md` の最小例と `examples.md` のコーパスだけ**だった。手書きのページの規則は誰も検査していない。
二本を書いてコーパスに入れた。
- **`保存基準.rule`** — 食品の保存基準のスケッチ。`temperature[℃]`、`volume[mL]`、`区分?` と `none` のセル、`string` の出力。
- **`事務所の衛生基準.rule`** — 事務所衛生基準規則と騒音障害防止のスケッチ。`area[m2]`、`sound[dB]`、`duration[h]`、`constraint`、`round half_even`。
どちらも十二言語で評価器と全件一致し、五基準を満たし、`doc` が描き、`examples.md` に載る。
**コーパスは「実物からの転記」と名乗ってきたので、この二本はその外に置いた。** 数え方を「規則 30 本(うち 28 本は実物からの転記、2 本は言語の残りに届かせるために書いたもの)」に直した。README とサイトと `tests/readme.rs` の三か所。**名乗りを守るために数を分ける。**
**まだ届いていない四つ**:`take_first`(`tests/fold.rs` が走らせている)、`import proto` と `import jsonschema`(隣にファイルが要る。`tests/proto.rs` と、§15.92 で足した変異 `m_e032`・`m_e033` が走らせている)、`source` の `url`(網に出る)。**どれも別のテストが持っていて、コーパスに無いのは理由がある。**
### 15.94 CI を足したら、四つ落ちた(2026-09-20)
**きっかけ**:押したあと CI の結果を見にいって、**そもそもテストを走らせる workflow が無かった**。`docs` はサイトを建て、`release` はタグでバイナリを作る。どちらもテストを通さない。**配っているバイナリは、一度も検査を通っていなかった。**
**決めたこと**:`ci.yml` を足す。十二の生成先が要る処理系を全部入れ、**飛ばしたら落とす**。`rulec test --require-all` が言語を数え、PostgreSQL の関数側はその旗が数えないので警告の行を読む(psql が無いときに `--require-all` が 0 で終わることを測って確かめた)。PostgreSQL は service で立てる。
**そして四つ落ちた。手元では全部緑だったものである。**
1. **`verify` の四つのテストが一つのディレクトリを共有して競合していた。** `setup` が fixture をプロセス名で呼び、同じファイルの四つが同じプロセスにいる。片方の `remove_dir_all` が、もう片方が `adapter.py` を読んでいる最中に走り、`verify` が 1 ではなく 2(読めない)で終わる。**laptop の上ではタイミングが持つ限り緑だった。** ほかに同じ形は無い——ディレクトリ名を作る補助関数はどれも一度しか呼ばれていないことを機械的に確かめた。
2. **`website/docs-ja/` の半分は `sync.sh` が書き、gitignore されている。** 日本語のページは英語の図を共有していて、二つ目のコピーを持たない。リンクの検査はそのページを歩くので、**新しい checkout では無い木を見にいく**。`docs.yml` は `build.sh` 経由で `sync.sh` を走らせていた。この job も走らせる。
3. **`fmt --check` がコーパスの三本しか見ていなかった。** 名前を手で並べていたので、残りは一度も見られず、`会員特典.rule` が未整形のまま居座っていた。ディレクトリを渡す形に直した。**列挙ではなくディレクトリが、この種の検査を正直に保つ。**
4. 整形したら**二つのものが古い形を読んでいた**。図を描く `make_stack.py` が、セルごとに `->` を探していたので、二つ目の出力を入力と読んだ(正準形は二つ目以降の `->` を落とす)。そして私が §15.92 で書いた E045 の種が、**空白の並びに当たる形**だった——`make-mutants.sh` の冒頭が「空白に依存するな、`fmt` が桁幅を変えた瞬間に黙って外れる」と書いているのに、そのとおりに外れた。
整形が答えを変えていないことは、生成コードを前後で突き合わせて確かめた(差はソースの digest だけ)。
**四つのうち三つは「一つの機械でしか緑を見ていなかった」ことそのものである。** 一つ(3 番)は、列挙で書いた検査が伸びなかった話で、今日 `coverage.rs` で見たのと同じ形だった。**同じ誤りが一日に二度出るなら、それは癖である。**
### 15.95 生成した Rust を、モデル検査器に読ませる(2026-09-20)
**決定**:三つ入れた。(1) エラーは、SQL 以外の十一言語で**文と値を別々に持つ**(SQL のエラーは `_input_error` のフィールドであってオブジェクトではない。Wasm は Rust の生成物をそのまま持つ)。Rust は `RuleError::Input { what: &'static str, value: Option<i64> }` と `Contradiction { what }` にし、文の組み立ては `Display` に移す。文面は一字も変えない。(2) `rulec gen` が `rust/<別名>_proof.rs` を書く。[Kani](https://model-checking.github.io/kani/)(rustc の MIR を CBMC に落とすモデル検査器)の証明ハーネスで、中身は全部 `#[cfg(kani)]` の下なので rustc は読まない。`rulec test --proofs` を付けたときだけ走り、`kani` が無ければ飛ばしたと言う(`Backend` の `proof` 列。WASI と PostgreSQL の検査と同じ形だが、**既定では走らせない**——後述)。`rulec api` は `rust.proof` とハーネスの名前を出し、CI はコーパス 30 本を回す。(3) §15.47 に一行足す——「Kani が通った」は**生成物についての証明**であって、表についての証明ではない。
**きっかけ**:五つの静的な証明のうち、生成コードと表の一致は**テスト**のままで(README の穴 2)、W114 の行対は**静的に閉じていない**(穴 3)。検査器と一行も共有しない道具で、この二つを閉じられるか。
**測った**:
- **生成物は、五つのうち三つを既に assert として持っていた。** 表の末尾の `unreachable!` が完全性、W114 のガードが未証明の行対、i64 の演算そのものが、あふれないという主張にあたる。Kani は panic の到達可能性と算術のあふれを既定で見るので、ハーネスは「宣言した範囲を仮定して関数を呼ぶ」だけでよい。
- **唯一の壁は `format!` だった。** 3 行の `クーポン併用` でも CBMC が 10 分で終わらない。切り分けると、trace の `Vec` は 0.15 秒、規則の論理は 1 秒未満だった。**`format!("...: {}", 記号的な i64)` だけが、10 分を超えても終わらない**(二度)。値を埋め込むのをやめた同じ生成物は 0.80 秒で通った。決定 (1) はこの測定である。エラーを作るところで文字列を組み立てなくなるのは、ついでに正しい。
- **重なり(E105)は生成物からは再検査できない。** if / else if の連鎖が先に順序で決めてしまうので、`unique` の重なりは、ガードを置いた対のほかどこにも現れない。だから行の条件を別に取り出して数える(`rows_<表>`。`unique` はちょうど一つ、`first` は一つ以上)。最初は生成した Rust から正規表現で取り出して測り(40 行、コーパスの表 45 個・行 355 本が取れた)、そのうえで生成器が IR から直接書くようにした。
- **コーパス 30 本、ハーネス 73 本、全部検証、合計 155 秒**(Kani 0.68.0 / CBMC 6.11.0)。最遅は `健康保険料` 30 秒(率のかけ算)、`納入先照合` 27 秒(要素 50 件の歩き)、`Claude利用料` 19 秒。**畳み込みは「上限つき」ではなく完全**である——要素数の上限が入口のガードにあるので、`unwind(上限+2)` で宣言した範囲を丸ごと覆い、unwinding assertion も通る。
- **空振りではない。** `印紙税` の行を一つ消すと `契約金額 = 804,896円 / 作成日 = 2026-10-11 / 記載あり` で当たる行が無いと言い、行 3 の上限を 10万円から 50万円に広げると `108,304円 / 2030-12-25` で二行に当たると言う。どちらも具体的な入力つきで、`--concrete-playback` はそれをテストとして書き出す。
- 測定の全部は `experiments/kani/`(`run.sh`、`report.txt`、取り出しの試作 `extract.py`)。
- **既定にしなかった理由も測った。** `納入先照合` と `クーポン併用` の二本で、`rulec test` は **7 秒、`--proofs` を付けると 33 秒**。差の大半は要素 50 件を歩く一本で、単体で 27 秒かかる。コーパス 30 本なら +155 秒。ベクタは秒、証明は分である。既定は速い側に置き、遅い側は `--proofs` を付けたときだけ走らせる。CI は一本の規則でそれを通し(`--require-all` と同じところで)、コーパス 30 本は `kani` を直に回す。
- **この節は一度、測り損ないの上に書いた。** 最初は「証明を既定にしたら一式が 5 分から 20 分に伸びた」と書いていた。伸ばしていたのは証明ではなかった。同じ機械で `cargo test` が 11 個同時に走り、互いの lock を待っていたのである。測り直すと、テストが使う規則は小さいので、証明を既定にしても一式は 20 秒対 21 秒でほぼ動かない。`--proofs` にした根拠は一式ではなく、自分の規則を持っている人の `rulec test` が、黙って数分になることである。
**走らせて見つけて直したもの**:五つ。(1) `constraint`(§15.55)を仮定に入れていなかった。`事務所の衛生基準` のハーネスが落ち、入口のガードが受け付けない入力を自分で渡していた。**宣言した範囲は `range` と `constraint` の両方である**。(2) 列を歩く規則には、一要素ぶんのフィールドを列にした表と、数えた値を読む表がある。どちらも歩きの外では再現できないので、`rows_` は書かず、生成物のハーネスだけにした。(3) TypeScript から型を落とす処理(`strip_types`)が、引数の既定値を注釈と一緒に落としていた——`value: unknown = NO_VALUE` が JavaScript では `value` になり、既定値が消えて `undefined` になっていた。クラスのプロパティを宣言する行も知らなかったので、落とすようにした。(4) `rulec api` の Swift のテストが、`api` の出す名前から `RuleError.input("")` を組んでいた。ラベルが付いたので、名前だけを参照する形にした(関連値を持つ case は、その値を取る関数でもある)。(5) README とサイトの `generate.md` の抜粋(Python と Go)。
**捨てたもの**:生成物そのものに証明を混ぜる案(読む人の邪魔になる。`_runner.rs` と `_round_test.rs` の隣に置く形はもうあった)。Java に JBMC を足す案(`proof` の列は `wasi` と同じく**一つで閉じる**。形が成り立つことを示すのが目的で、二つ目は道具立てを増やして何も広げない。二つが食い違う規則が見つかったら再訪する)。SMT-LIB を吐いて z3 に解かせる案(表をもう一度別の形に書き直して解くだけで、**出荷する生成物**には触らない。§6.2 が SMT を道具の中に入れないと決めたのはそのままでよく、外に吐くだけなら入力の決まり方も単一バイナリも崩れないが、Kani より弱い)。行の条件を、生成した Rust の字面から取り出し続ける案(生成器が IR から書けるので、正規表現に頼る必要が無い)。
**まだやっていないもの**:生成コードと表の一致そのもの(穴 2)。表を data として出し、手書きの参照評価器と生成関数が同じ答えを返すことを Kani に証明させる形になる。ただし、どちらも同じ frontend から出るので、転記の誤りはそのまま抜ける。畳み込みの中の表の行も、まだ数えていない。証明書(検査の出力に、独立した小さい検査器が読める証拠を付ける形)は別の決定に送る。
### 15.96 検査の証拠を、渡せる形にする(2026-09-20)
**決定**:`rulec certificate <file.rule>` を足した。**五つの証明すべて**について、その根拠を JSON で出す。(1) **完全性**——§6.3 の探索の木をそのまま書く。節点は、その深さの軸の座標の数だけ子を持つ(だから「子が軸を敷き詰める」は主張ではなく形で決まる)。葉は、その下を丸ごと取る行か、「どの入力も来ない」理由(`constraint` が成り立たない/導出の座標が宣言範囲の外/上流の表が出せない値)である。(2) **重なり**——`unique` の行の対ごとに、座標が交わらない軸を一つ。(3) **到達**——行ごとに、そこに当たる点。(4) **int64**——値ごとに、式の木・宣言範囲・刻み。(5) **単位**——値ごとに宣言した型と、葉(名前と literal)の型。決められなかった対(W114)は `undecided` に、`apply` が持ち込んで使われない行(§15.69)は `unused` に、上流由来の葉は「述べただけ」と分かる形で書く。再検査するのは `tools/recheck.py`——一ファイル、依存ゼロ、rulec と一行も共有しない Python である。検査を通らない規則は証明書を出さない。
**渡すのは「述べたもの」ではなく「組み直せるもの」にした。** 行の区画(`accepts`)は証明書に載っているが、検査する側はそれを信じず、**セル(`tests`)と軸の座標から組み直して突き合わせる**。式の型も同じで、節点の型は載せず、葉の型から規則で導いて宣言と照らす。だから「区画だけ広げる」「型だけ書き換える」細工は落ちる。
**きっかけ**:§15.95 で Kani が入り、生成物についての独立した意見は得られた。ただしそれは相手にも 2 GB の道具立てと 155 秒を要求するもので、**渡せない**。区画の検査そのものは、いまも「この実装を信じてください」のままである。渡せる証拠にできるのはどれか。
**なぜ証拠が小さいか**:肯定の主張なのに、証拠が小さいからである。二つの行が交わらないなら**どこか一つの軸で**交わらず、その軸を挙げれば、読む側の仕事は集合の交わり一回になる。行に当たる入力があるなら**その点**を書けばよい。完全性は探索そのものだが、書き下ろせば読む側は探索しなくてよい——節点の子の数を見て、葉を一つずつ確かめるだけである。int64 と単位も、式と宣言があれば区間演算と型付け規則をやり直せる。どれも読む側に探索をさせない。そこが、証拠と「同じプログラムをもう一度走らせること」の違いである。
**測った**:
- **形は既に `region.rs` の中にあった。** `hole_rec` の刈り込み(「残りの軸を全部覆う行がある」)はそのまま葉であり、重なりの判定が見つけた軸もそのまま使える。新しい理屈は足していない。
- **大きさ**。予算を絞って E109 が出る境目を二分探索して、規則ごとの訪問ノード数を測った。コーパス 30 本で合計 1,324、最大 337(`ゆうパック運賃`)、平均 44。木はこの規模で、JSON にして数 KB に収まる。重なりは `unique` の行 300 本・対 3,608 組、到達は点 300 個である。
- **コーパス 30 本が再検査を通る。** CI も同じことを毎回やる。
- **偽った証明書は落ちる。** 十の細工で確かめた。対を落とす、実際には交わる軸を挙げる、行の点を一つ上の行に付け替える、点を区画の外に置く、覆う行を別の行に偽る、木の枝を一つ落とす、int64 の区間を狭く言う、刻みを偽る、**区画をセルに合わないよう広げる**、**型を偽る**。制約の葉は向きを逆にした証明書も落ち、`--rule` で別のファイルを指しても落ちる。`tests/cert.rs` の七本が毎回やる。
- **起きない葉が二つある。** 導出のふるいによる葉は、いまの実装では出ない。軸の座標は宣言範囲に切り詰められる(§15.69 で直した `num_bounds`)ので、「座標が宣言範囲の外」が起こらないからである。上流由来の葉もコーパスでは 0 件だった。どちらも形は残してあり、出たときは前者が再検査され、後者は「この道具は再検査しない」と数えて言う。
**走らせて見つけたもの**:四つ。(1) `非常勤退職手当` が落ちた——準用が持ち込んだ行 2・3 は、この規則の束縛では当たらないが §15.69 で E102 の対象から外してあるので、黙って落とすと「行に点が無い」に見える。`unused` として挙げることにした。(2) 丸めの区間で、検査する側が「一刻み外へ」と緩く書いていたため `非常勤手当` の区間が広くなりすぎた。rulec と同じ規則(下端は零へ、上端は零から離れる向きに格子へ乗せる)に直した。(3) 単位で、`達成率 : rate = 合計点 ÷ 50` と `付与点 : number = 基本点 × 倍率` が落ちた。**宣言のときだけ rate と number を通す**という §2.1 の線(`check_same` のコメントがそう書いている)を検査する側にも入れた。足し算では通さないので、規則としては厳しいままである。(4) 群(`= 遠隔地`)を開くには群の中身が要るので、証明書に `groups` を載せた。
**「証明」と言えるようになるか**:まだである。証明書が動かすのは README の穴 4(検査器そのもの)で、信じる対象が 42,000 行の Rust から**数百行の検査する側**に移る。それは大きいが、移っただけで消えてはいない。遠慮なく言えるようになるには、(a) 規則の意味論を形式に書き(いまは §15.70 の散文)、(b) 証明書の条件がその意味論のもとで主張を含意することを機械で確かめ、(c) 「この .rule のセル=証明書のセル」の一段を閉じる必要がある。(c) はいま digest までで、その先は構文解析器の検証になる——そして**検査する側が rulec と同じ読み方で規則を読むなら、それは独立ではない**ので、閉じ方そのものに設計が要る。(a)(b) は数百行が相手なので Lean や F* に載る大きさで、そこが今回の一番の収穫である。穴 1(表と現実)は、どこまでいっても `@source` と承認の紙の仕事で、§15.47 の「何についての証明か」を隣に置く決まりは動かない。——この三つは §15.97 でやった。
**判断を一度まちがえた**:最初、単位(五つ目)は「型付けの導出を検査し直すのは、ほぼ型推論のやり直しだから割に合わない」として外した。誤りである。式の木はすでに int64 のために載っており、**節点の型を載せずに葉の型だけ載せれば、検査する側は規則を一段ずつ当てるだけ**で済む。推論のやり直しではない。実装は 60 行だった。
**捨てたもの**:節点の型を証明書に書くこと(書けば「導ける」が「述べた」に落ちる。葉だけにした)。証明書に規則の構文解析器を持たせる案(検査する側が parser を持てば「独立」が薄まる)。検査する側を Rust で書く案(同じ言語・同じ作者では独立が見えない。Python 一ファイルにして、後で Lean に載せ替える道を残す)。上流由来の葉を再検査できるようにすること(上流の表の区画ごと証明書に入れる必要があり、いまは 0 件なので、実例が出てから)。
### 15.97 意味論を書き、検査が主張を含意することを証明する(2026-09-20)
**決定**:`proofs/` に Lean 4 の開発を置いた。四つの部分からなる。(1) **表の意味**——点・区画・行・方針と、`unique` が主張すること(訊かれるどの点も、ちょうど一つの行が取る)、完全性、到達、単位、int64。(2) **検査**——証明書が通らなければならない条件を、決定可能な関数として書く。(3) **定理**——どの検査も、`true` を返したならそれに対応する主張が成り立つ。(4) **実行ファイル**——証明書の JSON を読み、(2) の関数**そのもの**を走らせる。`lake build` が (3) を確かめ、そこから出てくるのが (4) である。だからその出力は「もう一つの意見」ではなく、定理を一つの文書に当てたものである。`tools/recheck.py` は依存ゼロで誰でも読める版として残し、両方が毎回コーパスを通ることをテストで縛る。
**きっかけ**:§15.96 は「遠慮なく『証明』と言うには、(a) 規則の意味論を形式に書き、(b) 証明書の条件がその意味論のもとで主張を含意することを機械で確かめ、(c) 『この .rule のセル=証明書のセル』の一段を閉じる必要がある」と自分で書いて終わっていた。三つとも相手は数百行である。先送りする理由が無い。
**「訊かれる」を定義した。** 主張はすべて「訊かれる点について」である。その意味は、**座標の裏に値があって、宣言した `constraint` を全部満たし、導出の列がそれぞれ自分の式の届く区間に入っていること**。これは rulec が実際に決めていることと同じで、それ以上ではない。ある導出値を本当に作る入力があるかどうかは、ここでも rulec でも決めていない(§6.2 が「安全側に緩い読み」と書いたところ)。広く取れば完全性の主張は強くなり、到達の主張は弱くなる。狭く取れば逆で、rulec には出せない証拠を要求することになる。意味論に書いたのは、その線である。
**到達には、強い読みと弱い読みがある。** 行に当たる点は、点の座標だけでなく**その裏の値**(`at_values`)を証明書に載せる。値が座標に収まり、制約を満たし、導出の届く先に入っていれば、強いほう——「この行は訊かれる点を持つ」——が言える。値が無い証明書には弱いほう——「ふるいが排除しない点を持つ」——を当て、**どちらを言ったのかを出力に書く**。コーパス 30 本はすべて強いほうで通った。
**セルとファイルの一段を閉じた((c))。** 証明書は行ごと・列ごとに、そのセルが**ファイルのどこにあるか**(行・バイト位置・長さ)と、そこに何が書いてあるかを載せる。`--rule` を付けると、再検査する側はファイルを開いて digest を取り、各セルをその位置から読み戻して突き合わせる。書かれていないセルは黙って飛ばさず `null` と書く——準用が持ち込んだ行(別のファイルに書かれている)、節の `when` が触れない列、併合した表の片方に無い列。`origin`(どの表に書かれた行か)も載せる。同じ表に書かれた行はどれも同じ列にセルがあり、別の表の行と行番号の並びが入り混じることもない。そこも検査する。
**その一段に定理が要る。** バイト列を突き合わせても、「セル=区画」が残る。区画は座標の集合で、数値の列の座標は開いた区間だから、「この座標を取る」と「この座標のどの値も条件を満たす」が同じになるのは、**セルが比べる値がその列の境目になっている**ときだけである。それは §6.2 の作り方そのものだが、作り方は証明書には書けない。そこで `axisSplits`——「どのセルが比べる値も、どの座標の内側にも落ちない」——を検査に足し、その上で `mem_boxOf_cmp_iff`(座標を取ることと、条件を満たすことは同値)を証明した。これで五つの主張が向いている先は、圧縮した影から**書かれた表**そのものに変わる。
**測った**:
- Lean 2,652 行、11 ファイル、定理 66 本。`sorry` も `axiom` も `native_decide` も無く、依存しているのは `propext`・`Classical.choice`・`Quot.sound` だけである(`tests/lean.rs` が毎回そこを確かめる)。
- `lake build` は、まっさらから 5.3 秒。証明書一本の再検査は 20 ミリ秒。CI は別のジョブで、`lake build` とコーパス 30 本をやる。
- コーパス 30 本・表 48。完全性は 48 表すべて通り、到達は全部強いほうで通り、`unique` の対はすべて分かれた(未決の対があるのは 4 表)。値 37 個は型も int64 も通り、区間を諦めた値は 0 だった。セルから組み直した区画 788、ファイルから読み戻したセル 766。
- 読み戻せないものは挙げる。準用が持ち込んだ行 3 本、節が触れない列、そして境目に一致しない literal 2 個(宣言範囲の外の値を比べるセル。座標に無いので数を突き合わせられない)。
**走らせて見つけたもの**:五つ。
1. **完全性の検査が健全でなかった**(§15.98)。証明を書く前に、証明の形を書こうとしたときに出た。
2. **丸めの区間が、負の値で狭すぎた**(`tools/recheck.py`)。「零へ寄せる/零から離す」と書いていたが、正しくは「下端は floor、上端は ceil」である。−7 を刻み 5 で丸めると −10 になりうるのに、零へ寄せる読みは下端を −5 と言っていた。計算した区間が実際より狭ければ、「述べた区間からはみ出していないか」という検査は素通りする。通してはいけないものを通す向きの誤りである。rulec 本体は正しかった。
3. **行番号が重なった証明書を落としていなかった。** 以下の検査はどれも行を番号で引く。同じ番号の行が二つあると、その対は「どちらも先ではない」ので重なりの検査を素通りする。`rowsDistinct` を足した。定理を書こうとすると、この仮定が無いと進まない。
4. **到達の点を、ふるいに掛けていなかった。** 「点が行の区画の中にある」しか見ていなかったので、制約が禁じる点でも通った。上の `at_values` はこれを閉じるために足した。
5. **型の書き方を狭く読んでいた。** `money[…]` と `qty[…]` しか知らなかったので、`area[m2]` を持つ規則が落ちた。`種類[単位]` 一般に直した。
**「証明」と言えるようになるか**:一段だけ近づいた。証明書が通ったときに何が言えるかは、いまは `Semantics.lean` に書いてある。以前は散文だった。信じる対象は 42,000 行の Rust から、**健全性を証明した数百行の検査**に移った。残るのは「rulec が正しい証明書を**出す**か」で、そこはいまも証拠である——コーパス、変異 80 本、Kani の第二の意見。README の穴 1(表と現実)は動かない。
**捨てたもの**:
- Lean に規則の構文解析器を持たせること。§15.96 と同じ理由で、検査する側が rulec と同じ読み方をするなら独立ではない。バイト位置で引用を突き合わせ、区画は構文解析済みの形から組み直す、という二段に分けた。セルの本文を読んで演算子と数を突き合わせるのは `tools/recheck.py` の側だけがやる。
- 丸めのモードを意味論に入れること。どちら向きに端数を寄せるかは区間の主張と関係がない。丸めは「格子の上に乗り、floor と ceil の間にある関数」として置き、その性質だけを仮定にした。
- 「訊かれる」を「本当に入力がそれを作る」にすること。導出の列については決められず、rulec も決めていない。決められないことを意味論に書けば、定理は強く見えて中身が無くなる。
- sha256 を書かずに digest を Python 側に任せること。Lean だけでは「どのファイルの話か」が言えなくなる。70 行だった。
- 掛け算の区間を「片側が定数のときだけ」に狭めること。`商品合計 × 税率` があるので、積が四つの端どうしの積の間に入ることを、単調性を二段重ねて証明した。割り算は §2.3(E115)が定数に限っているので、そのぶん軽い。
### 15.98 ふるいは、区画に訊く。角に訊いていた(2026-09-20)
**欠陥(直した)**:**完全性の検査が、穴を見逃していた。** `hole_rec` は、どの行も取らない枝に来ると、経路を 0 で埋めて**一点**を作り、その点をふるいに掛けていた。ふるいがその点を「起こらない」と言えば、**部分木ぜんぶ**を穴なしとして畳んでいた。埋めた角がたまたま `constraint` に禁じられていれば、その隣の実在する穴は報告されない。
最小の例(`tests/mutants/m_e101c.rule`)。
```rule
inputs
甲(a) : number range >=0 <=1
乙(b) : number range >=0 <=1
constraint 甲 <= 乙
table 判定(t)
policy unique
| 甲 | 乙 | -> 答(r) : number |
| 0 | - | 1 |
```
`甲 = 1` の枝に行は無い。0 で埋めた点 (1, 0) は `甲 <= 乙` が禁じるのでふるいが落とし、検査は **ok と言った**。しかし (1, 1) は制約を満たし、どの行にも当たらない。生成した Python を呼ぶと `unreachable: completeness was statically checked by rulec` で落ちる。**検査器が緑を出し、その緑から出たコードが自分の assert を破る**——§6.1 が「不健全な緑はどの検査も出さない」と書いた、その一点である。
**直し方**:ふるいに**区画を訊く**。`span_of_name` は、経路が固定していない軸については宣言範囲を返すので、**埋めずに**短い経路のまま訊けば、判定は「この区画のどの点でも成り立たない」になる。それで落ちなければ、点を一つずつ見て、ふるいが落とさない最初の点を出す(`first_reachable`)。全部落ちたなら、その区画に届く入力は無いので穴ではない。予算は同じものを使うので、探索が伸びれば E109 になり、黙って通ることはない。
**出す入力も直った。** 前は 0 で埋めた点をそのまま出していたので、報告される穴が制約の禁じる点になりえた(§11 原則 2「未検証のものを「これがその入力です」という顔で出さない」に反する)。いま出す入力の**座標**は必ずふるいを通っている。——座標の裏の**値**まで制約に沿うようになったのは §15.99 である。ここで直ったのは半分だった。
**測った**:コーパス 30 本は全部 ok のまま、訪問ノード数も変わらない(`ゆうパック運賃` 337、`健康保険料` 102)。`事務所の衛生基準` は 2 から 1 に減った(制約のある規則で、区画ごと落とせるようになったぶん探索が浅くなる)。変異は 79 本から 80 本になった。
**どこで見つけたか**:証明書の意味論を Lean で書き(§15.97)、「`impossible` の葉は部分木ぜんぶについて何を言っているのか」を形式に落とそうとしたときである。定理の形を書こうとすると、実装が点について言っていることと、主張が区画について言っていることの食い違いが残せなくなる。**証明を書く前に、証明を書こうとしたことが見つけた。**
### 15.99 敵の目で読み直す(2026-09-21)
**決定**:§15.97 を書き終えたところで、Lean の開発と実装の両方を、**偽の証明書を通そうとする側から**読み直した。出てきたものを三つに分けて扱う。偽物を通していたものは塞ぐ。正しいものを落としていた・止まっていたものは直す。言い過ぎていた文は、主張どおりに狭める。塞げないものは「どこまでがファイルに縛られ、どこからが証明書の言い分か」として `docs/formats.md` に並べる。
**きっかけ**:§15.97 が言えるようになったのは「証明書が通ったとき何が言えるか」までで、「**通らないはずのものが通らないか**」は測っていなかった。証明書は肯定の主張を小さく渡す仕組みなので、渡した側の善意に寄りかかった検査は、渡された側から見ると何も言っていないのと同じである。
**偽物を通していた(塞いだ)**:
1. **引用が場所ではなく字だけの検査だった。** 行 3 のセルの位置を、同じ字が書かれた行 1 に向けても通った。行ごとに `line` を載せ、セルはその行の `|` 区切りそのもの・全部で `outputs` 個余る・行番号の順に並ぶ・別の表の行と混ざらない、を検査する。
2. **長さ 0 の引用。** 空文字は「何も書いていないセル」と読めたので、条件つきのセルを消せた。
3. **`source: null` で行ごと逃げられた。** 「準用が持ち込んだ行」の顔をして読み戻しを飛ばせた。同じ表の行は、全部この場に書かれているか全部よそかのどちらかである。セルの無い行は、パイプの無い行にしか書けない。
4. **セルを `any` と宣言して隠せた。** 同じ表の行はどの列にセルを持つかで揃う、どの軸にもどこかの行のセルがある、を足した。
5. **`unused` が生きた行にも付けられた。** 準用が持ち込んだ行(`source` が無い行)に限った。
6. **境目でない数が自由変数だった。** ファイルに `>=3000円` と書いてあっても、証明書が「2000」と読めば「pin できない」と数えて通した。軸自身の単位で書かれた literal は、境目であるか軸の外に出ているかのどちらかでなければ落とす。外の単位(グラムの軸に `2kg`)だけを数える。
7. **軸が敷き詰めていることを見ていなかった。** 座標を一つ消せば、その下の隙間は誰も覆わない。`step` を載せ、宣言範囲の端から端まで、隣と接するか一刻み先かで並ぶことを検査する。座標ゼロの軸も落とす。
8. **点の裏の値が無いとふるいを素通りした。** 値の無い点は弱い読みに落とし、そう言う。
9. **逃げ道に言い訳が要らなかった。** 覆いを `null` にする、葉を全部 `upstream` にする、対を全部 `undecided` にする——どれも「ok」で終わった。覆いが無いのは覆いではない。上流の葉は、上流由来の列がある表にしか置けない。そして**述べただけのものは最後に並べ、素の「ok」は出さない**。
10. **Lean 側が、値つきの検査に落ちると黙って弱い読みに落ちていた。** 値を渡していない証明書だけが弱い読みになる。
11. **宣言範囲が、計算した区間より優先されていた。** `eval_mem_interval` の前提(名前を縛る区間が本当にその値を縛る)を、プログラム自身が用意していなかった。計算した区間を使い、宣言範囲がそれを含むこと(E112)も検査する。
**正しいものを落としていた/止まっていた(直した)**:
12. **到達の探索に予算も枝刈りも無かった。** 12 列・11 座標の表で `rulec certificate` が返らない(11¹¹ 点を全部底まで見る)。前置きで刈り、§6.3 と同じ予算に付けた。
13. **制約でしか死なない行が、黙って落ちていた。** E102 はふるいを見ないので `check` は通り、証明書だけが点の無い行を抱えて、再検査が「行に点が無い」と落ちる。`unreachable` として挙げる。
14. **短い経路の `constraint` の葉で、再検査が例外を投げた。** 経路が固定していない列は宣言範囲で読む——道具のふるいと同じ読み方に揃えた。
15. **全角の比較記号と `、`/`,`/`・` の区切りを、セルの構文解析が知らなかった。**
16. **`result` と、束縛できない値が証明書に出ていなかった。** `check` は `result` にも E103・E108 を当てているのに、証明書は `derive` と `define` しか載せていなかった。比較演算子も `?` に潰さずに出す。コーパスで型の検査が 37 本から 49 本に増えた。
**出す入力が、規則の制約を破りうる(直した)**:
17. これがいちばん大きい。E101 が出す入力も証明書の点の値も、**軸ごとに独立に**選んでいた。`constraint 甲 <= 乙` があると、選んだ組が制約を破ることがある。`rulec check` が「当てはまらない例: 乙 = 1, 甲 = 4」と言い、その入力は制約が禁じている——§11 原則 2 の「未検証のものを「これがその入力です」という顔で出さない」に正面から反する。同じ理由で、証明書の点の値が制約を破り、再検査が正しい証明書を落とした。制約に沿って上下の境を押し合わせ(Bellman-Ford、名前の数だけ回す)、低いほうの端を取り、**最後に全部の制約に当ててから**渡す。解が見つからなければ値を渡さず、弱い読みだと言う。§15.98 が「出す入力は必ずふるいを通った点である」と書いたのは、座標については本当で、値については半分だった。
**測った**:
- コーパス 30 本は両方の再検査を通る。セルの読み戻し 766、敷き詰めを確かめた数値軸 33、型 49(前は 37)、int64 44、区間を述べない値 5(真偽値と列挙)、単位が読めない literal 2(グラムの軸の `2kg`)、準用の行 3。
- 偽った証明書は `tests/cert.rs` で二十三通り、`tests/lean.rs` で九通り試して、全部落ちる。うち二本は**それまで別の理由で落ちていた**——行に当たる入力を落とす細工は「点が区画の外」で、ふるいの細工は「座標が違う」で落ちていて、名前どおりの検査は一度も走っていなかった。試験が緑なのと、試験が何かを試しているのとは別である。
- Lean は 2,964 行・定理 66 本。`sorry` も `axiom` も `native_decide` も無い。
**言い過ぎを狭めた**:「すべてのセルをファイルから読み戻す」は、引用の場所まで縛って初めて本当になった。「圧縮した影ではなく書かれた表そのものについての主張になる」は取り下げる——区画とセルの対応は定理で閉じたが、軸・宣言範囲・型・制約・式・刻みは証明書の言い分のままで、そこへ踏み込むには規則の構文解析器が要る(§15.96 でそれを持たせないと決めている)。「信じる対象が数百行に移った」には、証明していない読み取り 300 行が含まれることを書き足した。
**捨てたもの**:
- 証明書の宣言範囲や型をファイルから検証すること。構文解析器が要る。代わりに、何がファイルに縛られ何がそうでないかを一覧にした。
- 弱い読みを黙って使うこと。使ってよいが、使ったと言わなければならない。
- 「pin できない literal」を数えるだけで済ませること。単位が読めないときだけ数える。
- 制約の解を完全に解くこと。差分制約の押し合いは連鎖と循環には効くが、一般の連立には効かない。効かなかったときは値を渡さず、弱い読みだと言う。
### 15.100 合計を足す(並びをまとめる二つ目、2026-09-21)
**決定**:`sum` を足した。`count` とまったく同じ形で、同じ場所に入る。
```rule
elements 明細(lines)
金額(amount) : money[円] range >=0円 <=100000円
sum 合計(total) over 明細 of 金額 range >=0円 <=1000000円
```
`count` が並びを一つの**数**にするのに対し、`sum` は並びを一つの**額**にする。そこから先はふつうの列で、`>=5000円` は軸の境目になり、完全性も重なりもいつもどおり止まる。要素ごとに走る項目と一度だけ走る項目の振り分け(§15.58 の二相)も、`types::element_scoped` がそのまま導出する。宣言は `Item::Count` から `Item::Agg` に一般化し、種別(`count` / `sum`)を持たせた——分かれるのは「一件につき 1 を足すか、列の値を足すか」だけだからである。
**きっかけ**:「合計 5,000 円以上で送料無料」が書けない、という報告。§6.3 の文書は「合計も平均も無い。それを出すのは呼び出す手前の仕事」と書いてあり、それは決めであって不足ではなかった。ただし `count` を足した §15.58 で、その線は一度越えている。**越え方まで決めてあるなら、二つ目に同じ越え方をさせない理由が無い。**
**足し算は決定的なので、§7.1 の悩みが無い。** 刻み済みの整数の和は順序にも言語にも依らない。割り算の丸めが言語ごとに違う(Python は −∞ 方向、Go は 0 方向)という E115 の理由が、ここには当てはまらない。
**長さの上限ではなく、走っている途中の合計を見張る。** `count` の宣言範囲は上限がそのまま並びの長さの上限になるが、合計の上限は長さを縛らない(金額が 0 でありうるなら何件でも足せる)。かわりに、**合計そのものが宣言範囲を出た時点で入口がエラーにする**。これは「範囲の外の数を受け付けない」のと同じ形で、しかも累算器は「上限 + 一要素」より大きくならないので、E108 の int64 の主張が長さの分からない並びについても本当になる。
**その代わり、合計する列は非負でなければならない(E029)。** 負が混じると走っている途中の合計が上下し、「出た時点でエラーにする」が成り立たない。差を取りたいなら、非負の列を二つ合計して引く。制限に見えるが、これが無いと上の見張りが嘘になる。
**測った**:
- 生成は 11 言語すべて。並びを歩かない SQL と NumPy は、これまでどおり生成しない。コーパスに `買物かごの送料` を足して、ベクタは 11 件・両側カバーまで満たす。
- ベクタの生成に一段要った。`count` は「当たる要素を n 個並べる」で n に届くが、合計は**総額**で届く。狙う額を一要素に載せ、載りきらなければ何要素かに割る、を足した(`total_seq`)。行ごとの境界の掃き出しも同じ形に直した。
- 証明書の軸の種別に `walk` を足した。`count` と `sum` が残した列は入力でも上流でもない。前は `upstream` と書いていて、点の出どころについて嘘を言っていた。
**捨てたもの**:
- `elements` に長さの上限を持たせること。合計そのものを見張るほうが、宣言が一つ少なくて、エラーにする理由もはっきりする。
- 負の列を合計できるようにすること。累算器の上限が静的に出せなくなる。要るという例が出てきたら、そのときは長さの上限を宣言させる。
- `average`。合計と数の割り算で、割る数が定数ではない(§2.3、E115)。按分と同じ理由で、要るなら専用の宣言になる。
- `Item::Count` を残したまま `Item::Sum` を足すこと。39 か所の分岐が倍になる。種別を持つ一つの宣言にした。
### 15.101 文字列を前置きで切る(2026-09-21)
**決定**:`string` の列に `starts_with "CH-"` を書けるようにした。書けるのはそれだけで、等号も集合も書けない(E110)。
```rule
| 品番 | 区画の数 | -> 扱い |
| starts_with "FZ-" | - | 冷凍便 |
| starts_with "CH-", "CL-" | - | 冷蔵便 |
| - | >=20 | 常温便 |
```
**きっかけ**:SKU やカテゴリの前方一致が書けず、enum にするしかない、という報告。SKU は数え上げられないので、それは「書けない」と同じである。
**前置きの集合は、有限に切れる。** 文字列の全体は無限だが、**ある列のセルに書かれた前置きの集合**は有限で、そこから作られる分割も有限である。座標は「この前置きの下で、どの長い前置きの下でもない」が一つずつと、「どれの下でもない」が一つ。§6.2 が列に求めているのは有限に切れることだけなので、**完全性も重なりも、列挙の列とまったく同じに動く**。穴を起こす入力は貼り付けられる文字列になる(前置きそのもの、または「どれの下でもない」なら、どの前置きも始まらない一文字)。
**定理を一つ増やした。** 数の列で `admits_iff` が要ったのと同じ仮定が、ここにも要る。「座標を取る」と「その座標のどの文字列も条件を満たす」が同値になるのは、**セルに書かれた前置きが、その軸自身の座標である**ときだけである。それは軸の作り方そのものだが、作り方は証明書には書けないので、`axisCovers` として検査に足し、そのうえで `mem_boxOf_prefix_iff` を証明した(Lean では文字列を `List Char` として扱い、`List.prefix_of_prefix_length_le` に乗せた)。
**バイトで比べる。** 大文字小文字も Unicode 正規化もしない。そうしないと 11 言語が同じ答えを返さない。部分一致(`contains`)と正規表現は入れない——原子の数え上げと入力の組み立てが急に難しくなるので、要るという例が出てから考える。
**測った**:
- 生成は 11 言語。`startswith` / `HasPrefix` / `start_with?` / `hasPrefix` / `str_starts_with` / `LIKE 'ABC%' ESCAPE` を一つずつ。NumPy だけは、並びのときと同じ理由で生成しない——ここの実行時は整数の列を読むので、文字列の列は別の表現が要る。登録簿に `texts` のフィールドを足した。
- コーパスに `品番の扱い` を足して、両方の再検査を通る。ベクタは前置きそのもの・その下の一件・どれの下でもない一件で、29 件が全言語一致。
- 診断が二つ増えた。数の列に前方一致を書くと E103、文字列の列に等号や集合を書くと E110。どちらも前は E102「この行には誰も来ません」——症状であって、悪いところではなかった。
**捨てたもの**:
- 文字列の等号(`= "ABC"`)。原子は増やせるが、前方一致があれば書けるし、値が数えられるなら `enum` が正しい。
- `string?` の列。「無い」を一つの座標として足せばよいが、`none` と前置きが並んだときの読みを決めていないので、E110 のままにした。
- 前置きの一覧を証明書に書かずに、座標の表示名から読み取ること。表示名は人が読むためのもので、そこに機械が寄りかかると引用符の付け方が意味を持ってしまう。`prefixes` を別に載せた。
### 15.102 比で配る(2026-09-21)
**決定**:`allocate(配る額, 累計, 全体)` を足した。値は `配る額 × 累計 ÷ 全体` を下に丸めたもので、**一行ぶんの配分は、ここまでの配分から直前までの配分を引いた差として書く**。
```rule
constraint 直前までの定価 <= ここまでの定価
constraint ここまでの定価 <= 定価合計
derive 直前までの配分(to_before) : money[円] = allocate(値引き総額, 直前までの定価, 定価合計) range >=0円 <=100万円
derive ここまでの配分(to_upto) : money[円] = allocate(値引き総額, ここまでの定価, 定価合計) range >=0円 <=100万円
result 配分額 = ここまでの配分 - 直前までの配分
```
**きっかけ**:値引きやポイントを、明細に定価の比で割り付けられない。§2.3 が変数で割ることを禁じている(E115)ので、比そのものが書けなかった。問いは「除算を一つも開放せずに、比で配ることが書けるか」である。
**差にすると、端数の行き先が決まる。** 累計の配分を二つ引き算する形は最大剰余法そのもので、隣り合う行の間で同じ値が足されて引かれるから、**配った合計はかならず総額に一致する**。余りをどこかの行に足して回る必要がなく、一行ぶんの判定が一行の情報だけで決まる。端数は最後の行に寄る。これは選んだ側面で、行の大きい順に配る書き方もあるが、それは行の順序を一行の判定に持ち込むことになる。
**この一致を、定理にした。** `proofs/RulecCert/Values.lean` に `runTotal_telescopes`(隣どうしが打ち消し合う)と `runTotal_exact`(切れ目が 0 から全体まで並ぶなら、配分の和は総額そのもの)を置いた。加えて `shareOf_nonneg`(累計は上がる一方だから、負の配分は出ない)と `eval_share`(規則が実際に書く `ここまでの配分 - 直前までの配分` が、その `shareOf` に評価される)。最後の一つが無いと、定理は似た形の算術についての話で終わる。
**§7.1 の悩みは、非負を要求することで消える。** 下に丸めるのと零へ丸めるのが食い違うのは負のときだけなので、配る額と累計が負になれないなら、12 言語の整数除算がそのまま `floor` になる。Python と NumPy は `//`、他は `/`、PHP は `intdiv`、SQL は `/` で、同じ答えを返す。
**区間は、範囲ではなく制約に乗っている。** 上限が「配る額そのもの」なのは、累計が全体を超えないからである。宣言された範囲だけを読むと `T×C÷S` は二つの範囲の積になり、E108 の主張が無意味に大きくなる。そこで `constraint 累計 <= 全体` を必須にし(E117)、区間はそれを読んで出す。**制約は鎖でつながる**——「直前まで <= ここまで」と「ここまで <= 合計」の二行が、「直前まで <= 合計」を言う——ので、検査も再検査も推移閉包を取る。
**証明書に、規則の制約を一度だけ載せた。** これまで制約は表ごとにしか出ておらず、表を持たない規則や、その列を軸に持たない表しかない規則では、再検査が区間を出せなかった。上位に `constraints` を足し、`tools/recheck.py` と Lean の検査器の両方が同じ閉包を取る。Lean 側では区間の関数が `within` という関係を受け取り、定理はそれを「呼び出し側が本当にその順序を守る」という仮定として使う——§15.55 が入口の番人に守らせているものと、同じ仮定である。
**割る前の積も int64 に収める。** 答えは配る額以下だが、途中の `T × C` はそうではない。E108 を積そのものについても出すようにした。
**測った**:
- 生成は 12 言語すべて。コーパスに `比例配分` を足して、ベクタ 35 件が全言語一致。行カバーは 2/2。
- 再検査は両方通る。証明書を偽る三つ——制約を消す、鎖を真ん中で切る、区間を狭く言う——は、いずれも、合わない箇所を挙げて落ちる。鎖を切ったときに落ちるのが二つの導出のうち片方だけであることが、推移閉包が効いている証拠になっている。
- 生成器の穴が一つ見つかった。NumPy の式の組み立てには `allocate` の枝が無く、**黙って 0 を出していた**。`check` が受け付ける呼び出しは決まっているので、知らない呼び出しに落ちたら、それは 0 ではなく生成器の欠落である。両方の組み立てで、その落ち先を止まるようにした(§15.99 と同じ筋)。
- **止めてみたら、検査のほうに穴があった。** `foo(a, b)` も `min(a, b, c)` も `check` を素通りしていて、生成器で初めて行き止まりになる。呼び出せる関数は閉じた集合で、それぞれ引数の数も決まっているのだから、そこでエラーにするのが正しい。E118 を足した——無い関数と、数の合わない引数。多い引数は黙って捨てられ、少なければ答えが決まらない。
- **モデル検査器は、この形について返ってこない。** 一行ぶんの配分は変数で割る式が二つで、その差に主張が乗る。CBMC はどちらもビットに展開するので、範囲をすべて千円に切り詰めた `比例配分` で二つのハーネスが 20 分、コーパスのままの範囲では 25 分待っても返ってこなかった。`allocate` が一つだけで範囲が 100 万・1000 万の規則なら 1 秒である。**返ってこないハーネスは、書かないハーネスより悪い。** `gen` は `allocate` を使う規則にハーネスを出さず、ファイルにその理由を書く——`string` の入力について前からそうしているのと同じ形である。モデル検査器からの二つ目の意見は、この規則には届かない。
**捨てたもの**:
- 明細の並びを受け取って、全部の配分を返す形。出力が並びになり、どの表にも載らない。呼び出し側が既に一行ずつ回しているなら、要るのは一行ぶんだけである。
- 四引数の `allocate(配る額, 直前, ここまで, 全体)`。引き算で書けるものを宣言に取り込むと、上の定理が道具の中に隠れる。二つの導出とその差なら、規則の文面がそのまま telescoping の形になる。
- 端数を大きい順に配る配り方。一行の判定が、他の行の余りに依存する。
- 範囲だけから区間を出すこと。制約が要ることを明示するほうが、E108 の主張が締まる。
- 汎用の除算。`average` を捨てた §15.100 と同じ理由で、要る形が出てくるたびに、その形を宣言にする。
- 範囲を小さくしたコーパス規則にして、モデル検査器を通すこと。通るのは規則が小さいからで、`allocate` を使う人の規則が通ることにはならない。やらない理由を書くほうが正直である。
### 15.103 別名が生成先の言葉とぶつかる(2026-09-21)
**決定**:ASCII の別名が生成先の言葉と同じとき、W121 で指摘する。登録簿 `src/backend.rs` に列を二つ足した——`reserved`(その言語の予約語)と `globals`(その言語がすでに使っている名前)。
**きっかけ**:規則の別名に `allocate` が使われていて、それが関数の名前と同じに見える、という指摘。別名は式の中では読めない(E012)ので道具としては安全だが、**別名は生成コードの識別子そのもの**である。そこで問いが二つに割れた。「読み手が混乱するか」と「生成コードが壊れるか」。後者を測った。
**測った**:入力の別名を `type`、規則の別名を `sum` にした規則を 12 言語に生成して走らせた。
- **Rust だけが落ちる。** `pub fn sum(type: i64, list: i64)` と書かれ、`type` は予約語なのでコンパイルが通らない。Wasm と WASI も同じ Rust なので落ちる。
- **Go は落ちない。** 入力は構造体のフィールドになり、`Type` と大文字で書かれる。PHP も落ちない——変数に `$` が付く。
- **Python は通るが、`def sum(...)` がモジュールの外から見える組み込みを隠している。** 黙って通るぶん、こちらのほうが質が悪い。
**位置で危険度が変わるので、列を二つに分けた。** 予約語はどの別名についても見る(少なくとも一つの生成先がそのまま書く)。すでにある名前は、**ファイルの一番外側に出る二つ——規則の別名(関数の名前)と列挙の別名(型の名前)——だけ**に当てる。引数やローカル変数は本体の外までは隠さないからで、実際この区別をするまではコーパスが 7 件も警告を出していた(`list` `cap` `min` `dup` `input`、どれも引数か局所)。分けたあとはゼロになる。
**SQL には一語も載せない。** 生成する問い合わせは識別子を全部引用符で囲む——`"on"` も `"select"` も、関数の名前も、名前渡しの引数も。予約語であることの代償は、その関係を手で読む人が引用符を打つことだけで、生成コードは何も壊れない。NumPy も空で、こちらは名前が JSON のデータであって識別子ではないからである。
**大文字小文字は区別しない。** 別名は生成先によって `PascalCase` にも `UPPER_CASE` にもなる。`ok` が Rust の `Ok` にぶつかると言われるような行き過ぎは、値の別名を対象から外して消した(列挙の値は `判定::Ok` と修飾して書かれる)。
**警告であって誤りではない。** 使わない生成先の予約語は実害が無い。**一覧は網羅でなくてよい**——載っていない語は「出なかった警告」であって、「誤った警告」ではない。
**捨てたもの**:
- 別名をこちらで書き換えること(Rust なら `r#type`)。生成コードの識別子は利用者が決めたものなので、黙って変えない。
- 別名にも E009 を当てて誤りにすること。別名は生成コードの都合で、`.rule` の中では名前として読まれない。使わない言語のために書き直させるのは行き過ぎである。
- 一つの一覧で済ませること。位置を見ないと、引数の `min` と関数の `sum` が同じ重さになる。コーパスが 7 件も鳴った時点で、それは使われない警告になると分かった。
### 15.104 確かめ方の見取り図を一枚にする(2026-09-21)
**決定**:サイトに「どうやって確かめているか」(`assurance.md`、英語は "How it is checked")を両言語で足した。層は九つで、それぞれに「何が決まるか」「どこで止まるか」「走らせ方」を書く。最後に「どこにも届いていないこと」を置く。
**きっかけ**:確からしさのために積んである仕掛けが、README・`checks.md`・`formats.md`・`proofs/README.md`・そして DESIGN の §15.47 / §15.95〜§15.99 に散っていて、**全体でどこまでやっているのかを一覧できる場所が無い**。個々の説明はどれも正確だが、読み手が「で、結局どこまで信じていいのか」を自分で組み立てる必要があった。
**見取り図であって、説明の焼き直しではない。** 各層は数段落で、詳しいことは元のページへ送る。同じことを二箇所に書くと、いつか片方が腐る。書くのは「その層が届く範囲」と「止まる場所」で、後者のほうを丁寧に書いた——どの層も、止まる場所こそが次の層の存在理由になっているからである。
**載せる数字は、テストが固定できるものだけにした。** 規則 33 本・変異 83 本・診断 77 件は `tests/website.rs` が実物と突き合わせる。ベクタの本数や網羅の義務の数は、規則を一つ直すたびに動くので載せない(「規則ごとに一式」「五基準」と書く)。Lean は行数や定理の数ではなく、`sorry` も `axiom` も無いことと `#print axioms` に三つしか出ないことを書く——こちらは動かないし、テストが見張っている。
**「証明」の語も、ここで一度きちんと定義した。** トップページの冒頭にも一段落足して、**宣言した範囲の入力を一つ残らず調べて示す**という意味だと言い切る。標本を取るテストでもなく、人が紙に書く数学の証明でもない。§15.47 で語の使い方は決めてあったが、**最初に読む人がその決めを知らない**のが問題だった。
**絵にした(同日)。** 表は「走る順番」には向いているが、**それぞれの層が何と何のあいだを見張っているか**は表では出せない。描いてみると形がはっきりした——背骨は四つのもの(現実の業務 → 転記元の文書 → 書かれた表 → 生成コード)で、どれも一つ上のものを書き表したものである。検査の層はたいていその**継ぎ目**にいて、二つを突き合わせている。例外は二つだけで、表そのものを見る 1・2・3 と、道具のほうを向いている 7・8 である。そして**いちばん左の継ぎ目には層が一つもない**。現実を読んで文書にしたのは人で、その読みを確かめる手立ては道具の中に無い。絵の左半分が空いていることが、そのまま主張になっている。
`website/tools/make_assurance.py`。`--verify` は、図が番号を振っている層がページの表の行と一致するかを両言語で確かめる——**説明している文章から図がずれるのは、図が無いより悪い**。`?v=` のハッシュは前からある四つ(トップページの図)だけに掛け、この図には掛けない。トップに載っていない図は、初めて見る人がどのみち取りに行くからである。
**捨てたもの**:
- `checks.md` を膨らませること。あれは `rulec check` のページで、モデル検査器や証明書の話まで入れると主題がぼやける。
- README に全部書くこと。README は既に長く、しかも GitHub で読む人は「何ができるか」を先に知りたい。一行のリンクにした。
- 数字を多く載せること。ベクタ 4,211 本・網羅の義務 904 件は測ってあるが、載せれば規則を直すたびに嘘になる。腐る数字は書かない。
### 15.105 CI だけが見ていたもの(2026-09-21)
**決定**:生成 Python の要素の組み立てから、余計なカンマを外した。あわせて、それを見つけられなかったテストを直した。`ruff format --check --diff` に `--color never` を渡す。
**きっかけ**:0.13.0 を push したら CI が一つ落ちた。手元では全部通っていた。落ちたのは「生成物は両言語の整形器に素で通る」で、指摘はこの一行である。
```python
rows = [m.Element(m.Yen(int(e["金額"])),) for e in d["明細"]]
```
**カンマは、組(タプル)だったころの名残だった。** 一要素の組は `(x,)` と書かないと `x` になってしまうので、フィールドが一つのときだけカンマを足していた。いまここが組み立てているのは `Element(...)` という**呼び出し**で、呼び出しの引数末尾のカンマは意味を持たない。しかも `ruff format` はそれを「わざと置いた印」と読んで、一行を四行に展開しようとする。フィールドが一つの並びをコーパスに入れたのは §15.100(`sum` の `買物かごの送料`)が初めてで、それまでこの枝を誰も通っていなかった。
**テストのほうが重い。** 手元で通っていたのは、`ruff` が**パイプに向かっても色を付ける**からである。差分の各行が `-` ではなくエスケープで始まり、「消される行」を数える filter が何も拾わなかった。**何も見ていないから緑だった**。CI では pip で入れた `ruff` が色を落としていたので、そちらだけが本物を見ていた。`--color never` を明示して、手元と CI が同じものを見るようにした。
**測った**:直したあと、コーパス 33 本の生成 Python に対する整形器の指摘は、88 桁を超える行だけになった(0 件)。この形の欠陥——**テストが空を見て緑になる**——は §15.92(コーパス九本が監査から漏れていた)と同じで、二度目である。緑の理由を疑う習慣のほうが、個々の欠陥より大事だと思う。
**捨てたもの**:
- 整形器を生成のあとに走らせて済ませること。§8.5 の決定的な出力が環境依存になる。出す時点で整った形にするのが決めである。
- `--color never` ではなく `NO_COLOR=1` を環境に置くこと。テストが渡すコマンドの中に書いてあるほうが、読んだときに理由が分かる。
### 15.106 README がサイトを二度言っていた(2026-09-21)
**決定**:README から、ドキュメントサイトに同じものがある節を落とした。516 行から 309 行になった。残したのは、GitHub を離れる前に要るもの——何をする道具か、通し例ひとつとその生成物、入れ方、よく使う命令、CI、リポジトリの中身、リンク——だけである。
**きっかけ**:README を読み返すと、`index.md` の「Where to start」、`tour.md` の言語の解説、`generate.md` の生成コード、`install.md` の CI が、そのまま二枚目として並んでいた。80 字を超える段落 57 個のうち 20 個がサイトのどこかと酷似し、うち 12 個は事実上同じ文字列だった。
**二枚目は、片方だけが古くなる。** 実際に六か所が食い違っていて、どれも README のほうが古い。「Where to start」の一行目はサイトだけが法令に触れ、対応言語の表は README だけが「畳み込みのある規則には SQL を生成しない」を落とし、Kani の段落は主語が違っていた。`docs/` をサイトとスキルへ複製するのに `sync.sh` とテストを使っているのに、README だけがその外にいた。
**数字も同じだった。** 「リポジトリの中身」のツリーが数える五つのうち四つが実物と違っていた——コーパス 32(実物 33)、変異 33(実物 83)、golden 35(実物 37 と 35)、モジュール 38(実物 42 と 5)、DESIGN 1,483 行(実物 3,168 行)。**テストが留めていた一文だけが正しかった。** だからツリーの数もディレクトリに留めた(`readmeのツリーが言う件数は実物と合っている`)。`assurance.md` の件数を留めているのと同じやり方である。
**捨てたもの**:
- 例を README から消すこと。例の無い README は読まれない。通し例ひとつと生成 Python の抜粋は残し、テストが実物と突き合わせている。
- 「証明しないこと四つ」を短くすること。この道具の性格そのもので、GitHub に来た人が最初に読むべきものだと思う。
- 見出しを手がかりに例を探すテストを残すこと。節を組み替えたら「何も検査していないのに緑」になる。`rule` で始まる塊を探す形に変えた。
### 15.107 英語の規則を足したら、率の出力の丸めが 0 になっていた(2026-09-21)
**決定**:出力列の目盛りを、行のリテラルだけでなく**宣言した丸めの刻み**からも決めるようにした(`src/types.rs`)。あわせてコーパスに英語の規則を五本足した——作り物二本(ポンドとインチの運賃表、返品の可否)と転記三本(英国の最低賃金、英国の所得税、米国の連邦所得税)。`ec261` の description と注釈も英語に直した。
**きっかけ**:コーパス 33 本のうち英語で書かれているのは `ec261` 一本だけで、その一本さえ description と注釈が日本語だった。英語ページに載っている規則の塊 162 個のうち 128 個に日本語が入っていた。英語で読む人は、言語を見に来て漢字の表しか見ない。
**率の出力を持つ規則が、一本も無かった。** 英国の所得税は税率帯を返すので、出力に `rate[step 1%] round down(1%)` と書いた最初の規則になった。生成 Python は `_round_down(rate, 0)` を書き、Python は ZeroDivisionError で落ち、NumPy は黙って 0 を返した。**参照評価器は有理数で丸めるので落ちない。一致検査だけが見つけられる形の欠陥である。**
**目盛りが、リテラルの約分後の分母から決まっていた。** 列にあるのは 0%・20%・40%・45% で、約分した分母は 1・5・5・20、その最小公倍数は 20。刻みの 1% は 1/100 なので 20 分の 1 の目盛りには乗らず、`g.num * scale / g.den` が 0 に落ちた。§7.1 は「約分後の分母ではない」と決めているのに、この一か所だけがそうなっていた。直しは、丸めの刻みの分母も最小公倍数に混ぜること。目盛りは広がる方向にしか動かないので、いまある規則の生成物は一行も変わらない。
**ヤード・ポンド法も、誰も通っていなかった。** `oz` `lb` `in` `ft` `yd` `mi` は文法にあるのに、コーパスのどの規則も使っていない。運賃表をポンドとインチで書いたのはそのためで、§15.9(行使されていない構造は検査されない)がまた当たった。
**測った**:38 本・12 言語で一致。Kani は 87 本のハーネスが 162 秒で通る。
**捨てたもの**:
- 約分後の分母をやめて `lit_scale` に統一すること。銭を使う規則の目盛りが 2 から 100 へ動き、生成物が広く変わる。筋としてはそちらだが、この欠陥に要る変更ではない。
- 生成器の側で刻み 0 を 1 に読み替えること。参照評価器と食い違ったまま隠れる。
- 英語の例を作り物で増やすこと。日本語側の厚みは転記から来ている。作り物を並べても同じにはならない。
### 15.108 法令データベースを二つめにする(2026-09-21)
**決定**:`law` の後に**データベースの語**を書けるようにした。`law ecfr "29 CFR 1910" asof 2026-01-01` は米国の連邦規則集(eCFR)を読む。語を省くと e-Gov で、これまでに書いた規則は一字も変わらない。語として読めない引用箇所は `"` で囲む(`@osha "§1910.157"`)。コーパスに `osha_extinguisher`(消火器までの距離、29 CFR 1910.157(d))を足した。
**きっかけ**:出典の仕組みのうち、**日付を指定して条文を取り、コピーを留め、改正が来たら読み直す行を挙げる**という一番おいしいところが、日本の法令にしか効かなかった。英語圏の規則は `file` の出典でコピーを置くことしかできず、それでは「原文が動いた」を知る術がない。
**データベースの語は予約語にしない。** `law` の次に来られるのは引用符つきの id だけなので、そこに裸の語があればデータベース名だと決まる。予約語を増やすと `kw.rs`・文法表・playground の色づけ・`rulec.wasm` まで波及し、`ecfr` や `egov` という名前を規則の側が使えなくなる。**文法を広げずに済むなら、広げない。**
**eCFR は、e-Gov と同じ形をしていた。** 日付つきで一つの section の XML を返す端点があり、versions の端点が section ごとの改正日と、それが **substantive かどうか**を返す。後者は e-Gov の「XML の付け替えだけの改版は改正と数えない」とまったく同じ判断で、`outdated` の設計がそのまま乗った。違うのは id の形(title と part)、箇所の形(section)、コピーの置き場所の名前だけである。コピーのディレクトリ名は空白を `-` にする——`29-CFR-1910@2026-01-01`——ので、e-Gov の id は今までと同じ場所に落ちる。
**`--compressed` は最適化ではない。** eCFR は圧縮を受け取れないリクエストに 406 を返す。curl の引数に足した。
**項(`(d)(2)`)は引けないままにした。** eCFR が section 単位でしか渡さないので、引くにはこちらでコピーを切ることになる。**自分で切ったコピーは、政府が出したままのものより証拠として弱い。** §15.82 で文書の表を切り出したときと同じ問いだが、あちらは切る前の文書も digest で留まっている。ここは留まらない。
**測った**:コーパス 39 本・12 言語で一致。`fetch`・`pin`・`check`・`outdated`・`doc`・`api` を一周させ、2016 年時点で留めた規則に対して 2017-01-01 の改正が挙がることを確かめた。
**捨てたもの**:
- `cfr` という別のキーワードを立てること。`law` と並ぶ三つめの宣言になり、共通の仕組みが二つに割れる。データベースは `law` の**属性**である。
- EUR-Lex を同時に入れること。CELEX 番号と条の形が別に要るので、一つずつ入れて、一つずつ動かす。次はこれだと思う。
- 箇所を裸で書けるように字句解析を広げること。`§` を識別子の文字にすると、名前に `§` が使えることになる。引用符のほうが、例外の置き場所として狭い。
### 15.109 英語圏の転記を四本、そしてブロックの途中のコメント(2026-09-21)
**決定**:転記を四本足した。英国の印紙税(GOV.UK の SDLT)、29 CFR の二本(1910.95 の騒音の表、1926.652 の掘削の防護)、PayPal の米国手数料。あわせて、**コメントだけの行がブロックを切らない**ようにした。
**きっかけ**:出典の仕組みが二つの法令データベースとファイルの両方で動くようになったのに、英語で書いた転記は五本しかなかった。日本語側は二十七本である。仕組みが揃ったあとに要るのは、同じ手順を回す作業だけになる。
**「本則と軽減」は日本の法令に固有の形ではなかった。** SDLT の初回購入者向け軽減は £500,000 で切れる。覆わない範囲では本則がそのまま顔を出す——`overrides` が想定していたのはまさにこれで、`印紙税の本則と軽減` と同じ二枚の表になった。国が違っても、条文の作り方は似る。
**出典が印刷していない数は書かない、が三度出た。** IRS の第一段("10% of the taxable income")、SDLT の最初の帯("Zero")、PayPal の合計率(3.49% + 1.5% = 4.99%)。どれも行から引用を外し、なぜ外したかを注に書いた。**行ごとに引用を付けられる**という §15.82 の判断が、そのたびに効いている。
**取得は地域で答えが変わる。** eBay と Stripe は、米国の料金を見に行ったつもりで日本語のページを返してきた。コピーに URL と取得日を書いてあるのは、こういう取り違えを後から見つけるためでもある。PayPal は URL に `/us/` が入っていて、それで英語のページが返った。
**コメント行は、空行と見分けが付かなくなっていた。** `inputs` の途中に `# …` だけの行を置くと、次の宣言が E005 で落ちる。字句解析を通るとどちらも空のトークン列になり、空行がブロックを終わらせるからである。**日本語側の規則は注を必ずブロックの上に書いていたので、三十九本を通して一度も踏まれていなかった。** 英語で書き始めて二日で二度踏んだので直した。空行がブロックを切ることは変えていない。
**測った**:43 本・12 言語で一致。Kani は 96 本のハーネスが 163 秒で通る。コーパスと変異のどの規則も、コメント行の次の行は新しい宣言だったので、この変更で意味の変わる規則は一本も無い。
**捨てたもの**:
- eBay を出典にすること。図表の数字は二度取って一致したが、返ってきたページが日本語で、**英語の原文を保存したことにならない**。コピーは原文の言葉でなければコピーではない。
- E005 の文面だけ直して「コメント行はブロックを切ります」と書くこと。切れないほうが正しい。
- コメント行を空行と同じに扱ったまま `fmt` で書き換えさせること。整形器が意味を変える道具になる。
### 15.110 README の例が日本語のままだった(2026-09-21)
**決定**:README の通し例を英語で書き直した(`fee_demo`、ポンドとインチ、USD)。生成コードの抜粋も、見せている診断も、命令の例のファイル名も英語にした。診断のほうはコーパスの `parcel_rate` から英語の変異を焼いて、そこから出る本物を貼っている(`m_e101en`)。
**きっかけ**:サイトの英語ページを英語の例に直したのに、**GitHub の入り口だけが日本語の表のまま**だった。README は英語で書いてあるのに、最初に出てくるコード片が漢字の表である。英語で読む人が最初に見るのはこのページで、サイトより手前にある。
**例そのものは README のもので構わない。** 以前の `送料例` もコーパスには入っておらず、`tests/readme.rs` が毎回検査を通していた。だから今回も、コーパスの規則を貼るのではなく、同じ形(表を二段、群、単位、丸め、例)を英語で書いた。長さも前と同じ 37 行である。**コーパスの規則をそのまま貼ると 63 行になり、短くしたばかりの README がまた伸びる。**
**診断だけは作り物にできない。** 「穴が空いたらその入力が返ってくる」は、この道具の一番の売りである。だから英語の変異を一つ足して、`rulec check` が実際に出した文字列を貼った。`tests/readme.rs` はその変異の出力と README を突き合わせ続ける。変異は 84 本になった。
**捨てたもの**:
- 日本語の例を残して英語の例を足すこと。README が長くなり、しかも「どちらが本物か」がぼやける。
- 診断の例だけ日本語のままにすること。読めない witness は witness ではない。
- `not: north_america` で書くこと。宣言した列挙のうち `overseas` がどの行にも名前で出てこなくなり、W111 が出る。**警告の出る例を入り口に置かない。**
### 15.111 丸めの問いが、円で書かれていた(2026-09-21)
**決定**:E104 の文面を、その規則自身の単位で書くようにした。ドル建ての規則には「35.5USD になる入力があります。`up(10USD)` なら 40USD」と出る。金額なら通貨コード、量なら宣言した単位、率なら `%`、`number` なら単位なし。
**きっかけ**:英語の Home ページに貼る本物の出力を採ろうとして、`money[USD]` の出力から丸めの宣言を外したら、**「some input computes to 35.5 yen … e.g. `round up(10円)`」**と返ってきた。英語の文面の中に、その規則が持っていない通貨が二度出てくる。
**この道具でいちばん人に近い問いが、これである。** 「端数をどちらに倒すか」は道具が決めないと決めてあり(§7.1)、その問いを人に渡すのが E104 の仕事である。**渡す相手に通じない単位で書いた問いは、問いになっていない。** 円で書いてあれば、ドルの規則を書いている人は自分の数字と結び付けられない。
**踏まれていなかったのは、またしてもコーパスの形のせいである。** 丸めの宣言が無い規則は変異にしか無く、その変異はどれも円建ての規則から焼いていた。**英語の規則を書き始めるまで、この枝は一度も英語で読まれていなかった**(§15.9 が三度目)。
**測った**:golden の二本が動いた。英語の文面で `0.12 yen` が `0.12円` になる——単位はリテラルの書き方で書く、という一つの決めに寄せたためで、`down(1円)` と同じ綴りになった。
**捨てたもの**:
- 英語のときだけ単位を落として「computes to 35.5」と書くこと。単位の無い金額は、この言語がいちばん嫌うものである。
- 出力ごとに単位を引き当てること。E104 の例の値は最初の出力を評価して作っているので、単位もそこから採る。二つ目以降の出力に別の単位があるときは、例の値のほうが先にずれる。
### 15.112 規則を、エージェントでない呼び手に渡す(2026-09-21)
**決定**:規則を Connect のサービスとして出す。`gen` が書くのは五つ——`proto/rulec/<別名>/v<版>/<別名>.proto`(契約)、`proto/buf.yaml` と `proto/buf.gen.yaml`(出たディレクトリがそのまま buf のモジュールで、stub の作り方は connect-py の文書がすすめるとおり)、`python/<別名>_service.py`(その後ろに立つ実装。ASGI と WSGI の両方)、`python/<別名>_connect_runner.py`(同じベクタをワイヤ越しに通す runner)。`rulec test` は全ベクタを、二つのアプリそれぞれに POST と GET で四回通し、記録をバイトで突き合わせる。runner が uvicorn に渡すソケットは、プロトコルに `IPPROTO_TCP` を書いて作る(2026-10-23。asyncio が Nagle アルゴリズムを切るのはそう作ったソケットだけで、`socket.socket()` のままでは、Linux で呼び出しごとに 40 ミリ秒ほど待った)。逆向きに、いま動いているほうが Connect のサービスなら `rulec adapter --template connect-python` がアダプタのテンプレートを出す。
**きっかけ**:§15.44 で規則を MCP のツールにしたとき、呼び手として想定したのはエージェントだった。呼び手はもう一つある。別のリポジトリにある、たいてい別の言語で書かれた、すでに何かの呼び出し規約を持っているコードである。そちらに渡す口を、この道具は持っていなかった。`rulec api` を読んで自分で包むか、SQL の関数(§15.80)を PostgREST の後ろに置くかの二つしかない。
**なぜ Connect か。** 生成物の側に持ち込む重さがいちばん軽いからである。単項の呼び出しは、`POST /<package>.<Service>/<Method>` に JSON を一つ送って JSON が一つ返るだけで、curl で叩ける。gRPC のスタックも、独自のフレーミングも要らない。そして `.proto` は、この道具がもともと**読んで**いた形式でもある(§15.59)。読める形式を書けるようにするのだから、往復が閉じる。
**決めたことが十ある。**
1. **`.proto` は言語ごとではなく一枚。** 契約が十二部あるのは契約ではない。だから言語のディレクトリの外、`proto/` に置き、レジストリ(§15.20)ではなく `gen` 本体から書く。
2. **置き場所が package と同じ形。** buf の `PACKAGE_DIRECTORY_MATCH` が求めるとおりにしてあるので、生成物をそのまま buf のモジュールに落とせる。`buf lint` は何も言わない(STANDARD、コーパス五本で確認)。サービス名の接尾辞、列挙の値の接頭辞、ゼロ値の `_UNSPECIFIED` も同じ理由でそう書く。
3. **package が持つのは major の版だけ。** package はワイヤの同一性であって、規則の版そのものではない。形が変わったかどうかを言うのは `buf breaking` の仕事である。
4. **手続きは一つ、`Decide`。** 名前は規則ごとに変えない。サービス名のほうが規則を名乗っているので、`ShippingFeeService.ShippingFee` は同じことを二度言うだけになる。
5. **`idempotency_level = NO_SIDE_EFFECTS` を書く。** ここではこれは願いではなく、すでに証明されていることである。規則は純関数で(P3)、同じ入力なら同じ答えが返ることは表の一つの版について検査済みだから、GET で呼べてキャッシュに載る。`rulec test` が POST と GET の両方を通すのは、MCP で stdio と HTTP の両方を通すのと同じ理由である(§15.51)。運び方で答えが変わったら、それこそ捕まえたいものである。
6. **答えに `trace` を載せ、番号を 100 にする。** 記録の形(§10.3)とそろえてあるので、呼び出し一件が fixtures の記録一件になる。番号を離してあるのは、あとで出力が一つ増えたときに次の小さい番号をそちらに渡し、trace を動かさないためである。
7. **ワイヤに出る列挙だけを宣言し、取り込んだ列挙は宣言し直さない。** `import proto` で契約を引用している規則は、生成する `.proto` でもその契約を `import` し、フィールドの型は向こうの型(`shop.v1.MemberTier`)にする。同じ集合の二枚目を作らないというのは §15.59 の立場そのままで、ここで折れたら引用した意味がなくなる。
8. **`.proto` が取る名前とぶつかる別名は、check で言う。** 出力の別名が `trace`、列挙の別名が `fired`・`element`・`decide_request`・`decide_response` のとき、`.proto` は同じ名前を二度書く。どれもほかの言語では予約語でないので、W121 の既存の表(§15.103)には引っかからない。protoc がエラーにするのでは遅すぎるし、その規則を書いた人が頼んでもいないファイルの話になる。
9. **ASGI と WSGI の両方を出し、stub は buf に作らせる。** どちらを使うかは呼び出し側の事情で、こちらが決めることではない。規則は純関数で待つものが無いので、`async def` の側も同期の側も同じ一本を呼ぶだけで済む——二つのクラスは同じ本体に付いた二つの扉である。stub の作り方も、その相手の文書がすすめるやり方(`buf generate` と遠隔のプラグイン二つ)にそのまま合わせる。合わせない理由が無いところで独自の手順を書くと、使う人はどちらが正しいのか確かめる仕事を増やされる。`rulec test` の中だけは、網の無い機械でも走るように、同じ二つのプラグインを手元の名前で指すテンプレートを `--template` に直接渡す(手元のプラグインは buf が既定でディレクトリごとに呼ぶので、package の印を書く側には `strategy: all` を付ける。遠隔のプラグインにこの指定は書けず、必要も無い)。stub は `python/stubs/` という自分の package に置く——プラグインは出力先の根に `__init__.py` を書くので、隣にある平たいモジュールの上にそれを落とすと、そこが package になってしまう。protoc の道は無い。メッセージは protobuf-py のもので、そのプラグインに行き着くのは buf だからである。なお `buf.yaml` と `buf.gen.yaml` はディレクトリのもので規則のものではないので、ヘッダに規則名と原本ハッシュを書かない。書けば、最後に生成した規則によって中身が変わってしまう。
10. **依存はサービスの一枚に閉じる。** `connectrpc` を import するのは `_service.py` だけで、判断しているモジュールは相変わらず何も import しない。生成物に依存が入るのはこれが初めてなので、線をどこに引いたかを書いておく——消しても規則は残る。
**誤りの割り当て。** 宣言した入力の外は `invalid_argument`(400)で、本文は規則自身の名前で引数を書く(`重量 is out of range: 0`)。W114 のガードが実際に当たったときは `internal`(500)である。どちらの行を採るかは表が決めることで、呼び出し側には直しようがない。静的に証明できなかった唯一の場所(§8.1)なのだから、サービス自身の失敗として数えられるところに出すのが正しい。返す見出しには `rulec-source-sha256` を入れ、どの版の表が答えたかを呼び出し側が持てるようにした。
**連携先の名前を一度間違えた。** 最初に入れたのは PyPI の `connect-python` で、これは古い名前である。いまの配布物は `connectrpc`(リポジトリは connectrpc/connect-py、プラグインは `protoc-gen-connectrpc` と `protoc-gen-py`)で、古いほうの最後の版は半年前で止まっている。古い版に合わせて書いたコードは、`_pb2` の名前、`HasField`、`ctx.response_headers()` の呼び方——どれも今の版では違っていた。**依存の名前は、入れた物ではなくいまの文書で確かめる。** 遠隔のプラグインと手元の連携先が食い違って見えたのも、実は古い名前のほうを入れていたからで、いまの `buf.build/connectrpc/py` は `connectrpc` 0.12.1 と一致した(生成物をバイトで突き合わせた)。
**測った**:コーパス 43 本を端から端まで通した。4,545 ベクタを、ASGI と WSGI のそれぞれに POST と GET で、実際にソケットを通してサービスに投げ、172 回とも参照評価器の記録とバイトで一致した。47 値の列挙を持つ規則も、日付を受け取って列挙を返す規則も、並びを歩く規則も、文字列の列を持つ規則も、出力が二つの規則も同じである。参照評価器が答えを持たない入力は、ワイヤの向こうでもエラーになった(`internal`)。`buf lint`(STANDARD)は 43 本の `.proto` に何も言わず、`buf build` と `buf breaking` も生成したモジュールをそのまま読み、生成した service と runner はコーパス全体で `ruff` の二つの検査(E/W と整形)を、ほかの生成 Python と同じ基準で通り、stub と依存のある機械では `mypy --strict` も通る(そちらは専用のテストで見る。stub が無い所では型検査器に見えないファイルなので、`tests/threeway.rs` の一括検査からは外した)。`rulec verify` を生成したサービス自身に向けると 96/96 一致し、相手は `connect@http://127.0.0.1:8123` と名乗った。
**標準ライブラリのサーバで一度詰まった。** 誤りを返す経路で、要求も答えも止まる。原因は WSGI の入力にあった。PEP 3333 はサーバに「本文の長さで終わる入力」を渡すよう勧めているが、標準ライブラリのサーバはソケットをそのまま渡す。相手が本文の先を読もうとすると、呼び出し側が次を送るまで待つ。呼び出し側は答えを待っているので、何も来ない。本番用の WSGI サーバはどれも長さで切っているので当たらない。生成する `server()` に長さで切る包みを一枚入れた——回避ではなく、サーバがやるべきことをやらせただけである。
**捨てたもの**:
- **列挙を宣言し直して `.proto` を自己完結させること。** いつでもコンパイルは通るが、契約が二枚になる。`import` が解決しないときは buf がその行を示して止まるので、直せる失敗である。黙って二枚目ができるほうが悪い。
- **単位をカスタムオプションにすること**(`(rulec.v1.unit) = "円"`)。BSR に publish して `.rule` を読まない人が呼ぶなら価値が出るが、いまはフィールドのコメントと `rulec api` の `request_fields` で足りる。足りなくなったと言えるまで足さない。
- **ASGI だけにすること、WSGI だけにすること。** 最初は WSGI だけで書いた。`wsgiref` で立つので試すのに何も要らないからである。だが呼び出し側がどちらのサーバを使っているかは、こちらの都合で決まらない。両方出すことにして、標準ライブラリのサーバは「何も入れずに試す道」として WSGI の側に残した。`rulec test` の ASGI 側だけは uvicorn が要るので、無ければそこだけ飛ばす。
- **rulec 自身が Connect を喋って `verify` すること。** 単項の JSON なら書けるし、HTTP の口はすでに持っている(§15.51 の試験側)。それでもアダプタを外のプロセスに置いたのは、TLS と認証と再試行をそちら側に置けるからである。`source fetch` が curl を呼ぶのと同じ線の引き方である(§12)。
### 15.113 入口で言語だと名乗っていなかった(2026-09-21)
**決定**:看板の一文を「表で書ける業務ルールのための小さな言語と、それをエージェントがコードにするためのハーネス」に替えた。同じ一文を十か所に置いた——README、GitHub の About、両言語の `site_description`、両言語のヒーロー、「これは何か」の見出し二つ、SNS プレビューの画像、そして `Cargo.toml` の `description`。画像は `website/tools/social.sh` が作り直すが、GitHub には上げる API が無いので、設定画面から手で差し替える。
**きっかけ**:`.rule` には文法があり、README は `docs/reference.md` を「the complete grammar」と呼び、サイトには「どんなルールのための言語か」「言語が目指しているもの」という節がある。それなのに入口の一文だけが「エージェントのためのハーネス」で止まっていて、言語という名詞がどこにも出てこなかった。**下のほうでは自分で言っているのに、看板だけが言っていない。** 初めて来た人は `.rule` の塊を見せられても、それが文法のある言語なのか、道具の設定ファイルなのかを判断できない。
**ハーネスは落とさなかった。** 言語だけを名乗ると、この道具が何のために作られたか——転記をエージェントにやらせること——が入口から消える。一文に名詞を二つ置いて、どちらも残した。
**捨てたもの**:
- **「プログラムが表である言語」の一本に寄せること。** 言語としては一番強い名乗りだが、エージェント向けという柱が入口から消える。
- **二文に割ること。** 「業務ルールを表として書く、小さな言語。その表をエージェントがコードにするためのハーネス。」は読みやすいが、二つが別の道具に見える。一つの道具の二つの面である。
- **`Cargo.toml` の `description` を空のままにすること。** crates.io に出すときに要るうえ、「これは何か」を書く場所がもう一つ空いているだけだった。
### 15.114 上流どうしの相関(2026-09-22)
**きっかけ**:この道具でワークフローは書けない(§0.1 の非目標)が、その相手と組ませるなら、`(いまの状態 × 事象) -> 次の状態` という遷移表はこちらに置けるのではないか、という問い。書いてみると通る。完全性も重なりもそのまま当たり、生成コードは純関数のままである。ところが**遷移のガードを足すと崩れた**。ガードは「上で作った二つの区分」の形になりがちで、そこが §15.30 が「残っている」と書いた場所だった。
**測ったら三つ出た。** 下流の表が条件に書く列が、どちらも上の表の出力のときである。
1. **E101 が、起こりえない組み合わせに行を要求する。** 上流が `<=10kg` を `小`、別の上流が `>20kg` を `重` とするなら `小 かつ 重` は起こらない。それでも行を求めてくる。
2. **その行を書いても、E102 は黙っている。** 要求されて書いた行が、決して発火しないことを誰も言わない。
3. **E105 が、起こりえない入力で重なりを報告する。** これがいちばん重い。E105 は**エラー**なので、正しい表が弾かれる。しかも導出列と入力列の組では、**同じ実行の中で** E102 がその行を死んでいると言いながら E105 が入力を出していた。§15.55 の 2 が「ふるいが落とした組み合わせからは入力を出さない」と約束した場所である。
**原因は二つで、別々だった。** 一つ、`upstream_blocked` は列を一本ずつ見る。列 `k` を調べるとき、その行の**ほかのセル**は上流の軸に読み直すが、ほかのセルが**別の上流の出力**だと、その軸の名前がこの表に無いので `project` が全許可を返す。二つ、§15.30 はふるいを `find_hole` と E102 に繋いだが、**重なりの検査には繋いでいなかった**。重なりのときに出す入力は交わりの「最初の角」で、角は軸が始まる場所であって、起こりうる点とは限らない。§15.98 が完全性についてこれを直したときと同じ誤りが、重なりの側に残っていた。
**決定**:
1. **兄弟の列で狭める**(`narrow_by_siblings`)。列 `k` の可能性を調べる前に、同じ行がほかの上流列を固定している分だけ、`k` の上流の軸を狭める。狭め方は `produces_where` — その値を出す行の区画を軸ごとに和で集めたもので、**真の領域の上位集合**である(先行行を引かない、軸ごとの和は区画の和より広い)。上位集合だから、狭めて落ちるのは「その列が確かにその値でない」入力だけで、**「起こりえない」と言い過ぎることはない**。E101 と E105 は起こりえないと言い過ぎれば不健全になる側なので、ここは緩さではなく健全性で守る。
2. **軸をまたぐ転送**(`carry`)。二つの表は同じ列を自分の閾値で切るので、座標は一対一に対応しない。`project` が一点についてやっている読み(区間が交われば許す)を集合に広げた。これも上位集合の側に倒してある。
3. **重なりのときに出す入力を、交わりの中の「ふるいも上流も落とさない点」にする**(`first_reachable_pair`)。`first_reachable` と同じ歩き方を、両方の行が選ぶ座標だけに絞ったものである。全点が落ちれば重なりは無いので報告しない。予算を使い切ったときは角を採って報告する — 証明が尽きたときに倒れる向きは、過剰報告の側である(§6.1)。
**証明書の側は、強くなった分を運べない。** 対の非交差は「軸のどれかが二つの区画を分ける」という主張で、再検査器はそれを一回の集合演算で確かめられる。上の表が分けている、という理由はそこに書けない。だからふるいが分けた対は `undecided` に落ち、`recheck.py` は「証明されたのではなく述べられている」側に積む。**これは元からそうで**、`overrides` で順序が付いた対も `constraint` が落とした対も前から同じ場所にいた。変えたのは札のほうで、「W114 が決められなかった対」と言い切っていたのを「軸が分けていない対」に直した。W114 はその一つの理由にすぎない。嘘の札は、無い札より悪い。
**測った**:コーパス 43 本の診断と証明書の数字は**1 バイトも動かなかった**。既存のどの規則も、上流の出力を二つ同時に条件に書く表を持っていない。動かないということは**新しい経路が一度も踏まれないということ**でもあるので(§15.9)、コーパスに `二つの区分.rule` を足した。同じ重量から二つの区分を切り、起こりえない組の行を書かない表である。この規則は**変更前は E105 で通らなかった**。12 言語で 31 件とも一致し(psql と protoc-gen-py が無い機械なので、PostgreSQL の関数と Connect の側は飛ばした)、証明書は `recheck.py` を exit 0 で通る。変異を二つ、生成器から作れる形で足した。`m_e102c` は起こりえない組を書いた行で E102。`m_e101e` は**起こりうる**組を覆わずに残したもので E101 と E107 二件 — ふるいが刈りすぎていないことの裏で、これが黙ったら生成コードは自分の `unreachable!` に落ちる。
**捨てたもの**:
- **`constraint` を導出列にも書けるようにすること。** 検査器が計算できる事実を人に宣言させることになる。§15.55 が列挙どうしの関係を外したのは「数で書ける場合が多い」からだったが、ここは数ですらなく、**上の表を読めば分かる**。
- **上流の上流まで追うこと。** 一段のまま残した。§15.30 が「見落とす側なので危険は増えない」と書いた判断はそのままで、実例が出てから測る。
- **ふるいが分けた対を、証明書で `disjoint` と名乗ること。** 再検査器が確かめられない主張を、確かめられる主張と同じフィールドに置くことになる。§15.96 の分け方そのものが崩れる。
- **準用の準用を、ここで一緒に開けること。** 遷移表を書いていて二段が要るとは一度も言えなかった。§15.69 が「一段。実例が要求したら」と書いたときのトリガは、まだ引かれていない。
### 15.115 上流由来の葉を、証明の中に入れる(2026-09-22)
**きっかけ**:§15.114 でふるいを強くしたとき、証明書のほうが追いつかないことに気づいた。強くなった分は全部「述べただけ」のフィールドに落ちる。そのフィールドがどれくらい広いのかを、誰も測っていなかった。
**測ったら、記録のほうが先に見つけていた。** §15.99 の 9 にそのまま書いてある——「葉を全部 `upstream` にする」は当時の抜け道の一つで、塞いだところまで含めて記録がある。上流の葉は上流由来の列がある表にしか置けず、述べただけのものは最後に並び、素の「ok」は出さない。実測でもそのとおりだった。それでも二つ足りなかった。**テストが一つも無い**(`tests/cert.rs` と `tests/lean.rs` の十六の細工に、この葉のものが無い)。**コーパスが一度も踏まない**(§15.96 が「0 件だった」と書いたまま 0 件)。塞いだ箇所が、毎コミット一度も通らない。§15.9 がまた当たっている。
**そして裸の葉は、本物の穴を隠せた。** 行が実際に取っている組み合わせの覆いを `{"upstream": ...}` に差し替えると、両方の再検査器が exit 0 で通す。増えるのは最後の一行の数だけである。
**決定**:塞ぐ。葉が寄りかかっている事実を証明書に載せ、両方の再検査器に**組み直させる**。
**証拠は二つの形で足りた。** 一つ、**その列を決める表が、その値をどの行でも書かない**。二つ、**二つの座標が同時に立たない**——共有している入力の上で、それぞれが残す範囲が交わらない。コーパスと、こちらで作れた形の全部で、証拠の書けない葉は 0 件だった。だから**証拠を必須にできた**。裸の葉は落ちる。
**証明書に足したのは三つ。** 表ごとの `decides`(その表が決める列)、行ごとの `produces`(その行が書く値)、表ごとの `above`(`never` と `apart`、`apart` には共有する入力と二つの範囲)。前の二つが、葉から「その列を決める表のどの行が、その値を書くか」へ辿る道になる。証拠が小さいのは §15.96 の線のままで、読む側の仕事は行の走査一回と区間の交わり一回である。
**事実は「座標の対」にした。** 最初は区間のまま `asked` に入れようとして、そこで詰まった。`asked` の値の枠は軸ぶんしかなく、共有する入力が下流の表の軸でないとき——実際そうなる——枠が無い。枠を足すと、その行に当たる入力(軸ぶんの値しか無い)が通らなくなる。だから**区間の算術は検査器に置き、`asked` には「同時に立てない座標の対」として入れた**。証明されるのは**対から組み合わせへの一歩**で、対そのものは両方の再検査器が表の行から組み直す。組み直しは証明されていない——ここが今の線である。
**Lean から前提が一つ消えた。** `Certified.coverChecks` は `!C.cover.leansOnUpstream` を前提に置いていた。事実が `Sieve` に入ったので、`{"upstream": ...}` の葉は `.impossible` として読まれ、`boxRuledOut` が決める。`Sound` の既存の定理がそのまま届くので、定理は一本も足していない。代わりに `coverOk` の `.upstream` を `true` から `false` にした——自由通行券だったものを、通行券でなくした。
**`policy first` で一度つまずいた。** 区画は発火域ではない。表の末尾の受け皿行は軸を丸ごと区画に持ちながら、実際に発火するのは残りだけである。区画のまま範囲を作ると受け皿行が全域になり、証拠が出ない。先行行が区画を丸ごと取るときだけ落とす読み(§15.30 と同じ、安全側)にして出るようになった。
**座標の読みで一度食い違った。** 証明書の `bounds` は座標の**開いた**端を書き、rulec の内部は閉じた端で数える。`(10, 30)` が刻み 1 なら `[11, 29]` である。`step` は証明書に載っているので、再検査器の側を閉じた読みに揃えた。揃える前は「書いた範囲が組み直した範囲より狭い」で落ちていた——**その検査が最初に捕まえたのが自分たちの食い違いだった。**
**測った**:コーパス 44 本が両方の再検査器を通る。偽造四種——葉だけ書いて事実を消す、起こる組を離れていると言う、範囲を狭く書いて離す、`never` をでっち上げる——は全部 exit 1 で落ちる。`tests/cert.rs` と `tests/lean.rs` に一本ずつ、同じ四種を回すテストを置いた。`二つの区分.rule` に表をもう一枚足して、一本の規則が重なりの側(ふるいが分けた対)と完全性の側(上流由来の葉)を同時に持つようにしてある。
**残っている**:対そのものの組み直しは、両方の再検査器が同じ算術でやっていて、証明はされていない。証明するには「上流由来の列の値は、同じ入力でその表が書く値である」という関係を `Semantics.lean` に入れることになり、一枚の表の意味論が複数枚の意味論になる。**「証明した」の指す先が変わる**ので、要求が出てから測り直す。
**捨てたもの**:
- **区間を `asked` に入れること。** 値の枠が足りず、足すとその行に当たる入力が通らなくなる。証明の深さと、既にある検査を壊さないことを較べて、後者を採った。
- **事実を葉ごとに書くこと。** 同じ事実が木の中に何度も出る。表の単位で一度書き、葉は座標で引く。
- **裸の葉を許し続けること。** どんな「これは許す」の形も、偽造する側から見れば通り道である。許さないと決められたのは、証拠の書けない葉が実測で 0 件だったからで、0 件でなければこの決定はできなかった。
- **述べたものが一つでもあれば exit 1 にすること。** CI の関門が「述べたものがゼロ」になり、読む人に見せるための行が通す/通さないの判定に使われる。行のほうが歪む。
### 15.116 形だけでは足りない前提を、呼び手に渡す(2026-09-22)
**きっかけ**:`elements` に渡る並びは、durable execution の上では**副作用を持つステップ(activity、step)が取ってきた結果**である。そこから「ではステップは、自分の返り値が受け取ってもらえるかを、返す前に知れるか」を測った。
**測ったら、渡している契約が、守らせている契約より弱かった。**
`rulec schema` が段に渡せるのは形だけである。`買物かごの送料` なら `明細: array of {金額: 0..100000}`。ところが規則はもう一つ宣言している——`sum 合計 over 明細 of 金額 range <=100万円`。生成コードはこれでエラーにする(11 件 × 10万円 を入れて `RuleInputError: 合計 is out of range: 1100000`)。**JSON Schema に「要素の合計が N 以下」は書けない。** `rulec api` にも載っていなかった(`合計` の語が一度も出てこない)。
同じ形が三つあった。**入力どうしの関係**(`constraint`、§15.55。生成コードは `the constraint does not hold: …` のエラーにする)、**並びの合計の上限**(§15.100)、**並びの長さの上限**(`count` の範囲、§15.58。`… has too many elements (at most 50)`)。どれも入口でエラーになり、どれも `schema` にも `api` にも無い。
**落ちる場所が悪い。** 段が返し、workflow の本体が判断を呼び、そこで落ちる。値は**すでに history に記録されている**ので、再生しても毎回同じところで落ちる。段の境界で分かるべきことが、判断の境界まで来ないと分からない。
**決定**:二つ足す。`rulec api` に `preconditions`——`kind` が `constraint`・`sum`・`length` の構造化された一覧で、**文ではない**(エラーの文面は `--lang` で動くが、これは動かない。§12.1)。そして `rulec schema` に、前提があるときだけ `$comment` を一行——「形だけでは足りない」と、**実際にある種類だけ**を挙げる。無い種類まで並べる注記は、次の一行も読み飛ばすことを教えてしまう。
**`$comment` を選んだ理由**:JSON Schema の語彙で、検証器が必ず無視すると決まっている唯一のフィールドである。ここに書けば「検証器が何かしてくれる」と誤解されない。通ってしまう文書が、自分について「通っても足りない」と言う場所は、ここしかない。
**一覧は空でも出す。** 前提が無い規則は `[]` で、フィールドごと消さない。読む側は「無い」と「この道具は言わない」を区別できなければならない。
**確かめ方は「飾りでないこと」にした。** 一覧に並べた前提ごとに、**schema には合っていてその前提だけを破る入力**を作り、生成コードが実際にエラーにすることを見る。境目の内側の入力は通ることも一緒に見る。一覧と生成コードの入口検査は別々に書かれているので、これは二か所の突き合わせである(§15.31 の教訓の形)。
**測った**:`比例配分`(関係 2 件)、`買物かごの送料`(合計 1 件)、`納入先照合`(長さ 1 件)、`送料`(0 件)。コーパス 44 本で `cargo test` 492 通過。
**残っている**:段が前提を**確かめる**手段は、まだ「判断を呼ぶ」しかない。生成コードの入口検査は `*_traced` の中にべた書きで、切り出されていない。呼び出しても副作用は無い(P3)ので実害は小さいが、段が自分の返り値だけを見て判定したい場面はある。切り出すかどうかは、要求が出てから測る。
**捨てたもの**:
- **エラーの文面を `preconditions` に載せること。** 文面は `tr!` を通るので `--lang` で動く。動くものを突き合わせの鍵にすると、言語を変えた側が壊れる。構造だけ載せて、文面は生成コードに置いたままにした。
- **JSON Schema のキーワードで表そうとすること。** 関係は `allOf` と `not` で書けなくはないが、合計は書けない。一部だけ書けると、**書けなかった残りが見えなくなる**のがいちばん悪い。全部 `$comment` の外に出し、`api` に一本化した。
- **前提があったら `schema` を出さないこと。** 形は形として正しく、要る人がいる。足りないと言えば済む。
### 15.117 規則を一枚のグラフにする(2026-09-22)
**きっかけ**:表を積み、`elements` を使うと、規則は値の網になる。字面は表ごとに切れているので、網としては読めない。**絵が要る**——が、何を描けばいいかは、まず出してみないと決まらない。だから配置には手を付けず、**辺だけを出す**ところから始めた。
**決定**:`rulec graph <file.rule>`。**節点は値、辺は「その値を決めるときに読む」。** 表は節点ではなく、値に乗る(`by`)。
理由が二つ。読む人の語彙は**値の名前**であって、表は「決め方」だからである。そしてもう一つ、一つの値に決め手が二つ付く形(節が表に優先する、§15.66)が、節点の重複ではなく `by` の二要素としてそのまま書ける。表を節点にすると、同じ値を決める二つの節点が並ぶことになり、読む人の側に「これは同じものです」を足させることになる。
**新しい知識は一つも無い。** `doc` はもう、表ごとに「列|出どころ」を出している。これはそれを一箇所に集めただけである。だから**二つを突き合わせるテストを置いた**——`doc` が「表 T は列 c を読む」と言うなら、グラフに `c --reads--> … via T` が無ければならない。同じ事実の二度目の描画は、片方が腐ったときに気づけることが取り柄で、突き合わせないならその取り柄が無い。
**時間の図ではない。ただし境界には時間がある。** 辺はどれも「この値を決めるときにあの値を読む」で、全部が一回の呼び出しの中で起きる。間に何かが起きる隙間は無い。起きるのは**外**で、§15.116 のとおり `elements` に渡る並びは段が取ってきた結果である。だから境目の番人(型と範囲)を節点に載せ、形では言えない前提を `preconditions` として一緒に運ぶ。規則の内側で止まる絵は、呼ぶ側が合わせるべき半分を隠す。
**囲いは三つある。** 要素の枠(`per_element`)、準用の枠(`from_apply`)、規則そのもの。このうち**要素の枠は字面に無い**——`elements` が繰り返しの宣言で、`sum`・`count`・`fold` がそこから出る三つの口だ、と知っていないと、どの値が一件ごとに決まるのか分からない。だから計算して節点に載せた。
**測った**:コーパス 44 本で **6〜15 節点、5〜14 辺**。小さい。だから配置に本格的な機構は要らない見込みが立った。そして**ダイヤは実在した**——クーポン割引 2、us_income_tax 4、保存基準 2、会員特典・クーポン一枚・比例配分 各 1、二つの区分 2。**§15.114 が二回のコミットを費やした形が、コーパスの三分の一近くにある。** 決め手が二つ付く値は退職手当の `手当`、要素の枠は納入先照合と全国運賃に出た。
**書いていて二つ出た。** 一つ、**出力セルが名前で値を指す読みが、辺になっていなかった**。節が `define` の答えをそのまま渡す形(`then 満額`)で、鎖が切れていた。表の列(`->` の左)しか見ていなかったからである。二つ、要素の中にいる値に印が無かった——`採用` は一件ごとに決まるのに、`行ゾーン` と違って `element` ではないので、どちらの側か分からなかった。
**`check` より手前で出す。** 完全性の穴がある規則にもグラフを出す。辺は行の穴に左右されないし、**絵がいちばん欲しいのは規則を直している最中**だからである。名前か型が解決しないときだけ出さない——そのときは辺を引く相手がいない。`doc` が check を待つのとは違う線で、違う理由がある。
**ついでに見つけたもの(直していない)**:`certificate`・`api`・`coverage`・`vectors` は、**完全性の穴がある規則に exit 0 を返す**。`certificate` のコマンド表は「1: 規則が検査を通らない」と書いているので、文書と食い違っている。`cover` が `null` になるので再検査器は落とすが、終了コードは嘘である。`doc` だけが 1 を返す。別の決定に送る。
**捨てたもの**:
- **節点を項目(表・導出・定義)にすること。** 上の理由。
- **配置まで rulec が持つこと**(この回では)。§15.81 と同じで、**形を見てから決める**。見た結果、節点は 15 個までと分かったので、次は素朴な層で足りる。
- **mermaid を出すこと。** サイトでもリポジトリでも一度も使っていない。入れるなら描画器を一つ増やす判断が要るので、`--format json` だけにした。
### 15.118 承認者のページを、カードの並ぶ一画面にする(2026-09-22)
**きっかけ**:§15.117 で辺を出し、コーパス 44 本で形を見た。節点は 6〜15 個、ダイヤは規則の三分の一にある。では、その辺を承認者のページにどう出すのか。
最初は素直に描いた——値ひとつに節点ひとつ、「決めるときに読む」ひとつに矢印ひとつ、上下の位置は呼び手からの距離。**それは読めなかった。** 節点の中には名前しか無く、承認者が見たいもの——どの行が当てはまってこの値になったのか——は、図から遠く離れた本文の表にある。図と表が別の場所にあるかぎり、図は索引にしかならない。索引なら文章で足りる。
**決定**:図を足すのではなく、**ページ自体をその画面にする**。`doc --format html` は左右二枚のペインになり、下にもう一枚付く。
- **左**:「試してみる」のフィールド、入力、出力、型。承認者が触るものと、語彙の定義。
- **真ん中**:縦にも横にもスクロールする一枚のキャンバスに、カードが依存の深さで左から右に並ぶ。
- **下のペイン**:カードを選ぶと開く。その表の「列|出どころ」と「`rulec check` が確かめたこと」。
- 縦のボーダーも横のボーダーも**ドラッグで動かせる**。規則ごとに、見たいものの大きさが違う。
**カード一枚は値ではなく、値を決めている表や式である。** これが図を捨てた核心である。節点が値なら中に書けるのは名前だけだが、節点が**それを決めている表**(あるいは `derive`・`define`)なら、中に入るのは**本物の決定表そのもの**である。当てはまった行がその表の中で光り、カードの見出しに行番号が出る。承認者は視線を動かさずに「この表の、この行が、この値を出した」を読む。**図が表の索引になるのをやめて、表が図の中身になった。**
**選んでいないカードはモノクロ。** 色は二か所だけ——選んだカードと、当てはまった行。色が意味を持つのは、色の付いていないものがあるときだけである。行の色は `#ffe9a8` のままにした。二つ目の色を作れば、二つの意味があるように見える。
**時間の図ではない、と形で言う。** 矢印は順番ではない。全部が一回の呼び出しの中で決まる。だから説明文の一文でそう言い、**時間を表す語(次に、それから)を一つも使わない**。§0.1 がワークフローを非目標に置いているのに、フローチャートの形をした図を出せば、道具が自分で引いた線を図が裏切る。起きるのはこの画面の外で、§15.116 のとおり `elements` に渡る並びはステップが取ってきた結果である。
**表は見出しではなく行から引く。** カードが自分の表を探すとき、見出しを手がかりにすると二つの理由で外す——ページの言語が二つあり(§12.1)、準用が持ち込んだ表は名前が `{準用}:{表}` に変わる。行が持つ `data-t` は trace が使う名前そのもので(§15.69)、どちらにも左右されない。これを直すとき、**準用の表には印が一つも付いていなかった**ことが分かった。`md_to_html` が節を括るのは `##` だけで、準用の表は `###` だったからである。行の印も節の印も付かず、したがって trace が光らせる行も光っていなかった。テストが先に落ちて教えた。
**JavaScript が無ければ、ページは元の文書のままである。** この画面は本文から要素を**移して**組む。作るのではなく移すので、二つの描画が食い違う余地が無い。
**ウィンドウを前提にすると、埋め込む選択肢が消える。** この配置はウィンドウいっぱいを前提にしている。すると、**小さい枠に埋め込むという選択肢が無くなる**。プレイグラウンドは承認者のページを 32em の枠に出していたが、左のペインだけで幅の六割を食った。枠を大きくするのではなく、**別のタブで開く**ようにした。押した瞬間に開くポップアップではなくリンクにしてある——スクリプトからの `window.open` は塞がれることがあり、リンクなら普通の遷移で、中クリックも効く。リンクの先は sandbox 付きの iframe を一つ持つだけの一行の文書で、生成コードは前と同じく opener に手の届かないオリジンで動く。あわせて左のペインの幅を、ウィンドウの 34% に下限と上限を付けたもの(いまは `clamp(20rem, 34%, 26rem)`。15.246)にした。固定幅は、狭いウィンドウ(MCP ホストのパネル、資料の中の枠)でカードの居場所を奪う。左右に並ばないほど狭い枠では一列にする(15.246)。
**`result` の出力に節点が無かった。** スクリーンショットを撮ろうとして気づいた。`result 送料 = 基本送料 × 負担率` は `items` に現れないので、`items` を歩いて作るグラフは**呼び手が訊いた当の値に届かない**。`rulec graph` にも画面にも、答えを出したものだけが無かった。コーパス 44 本のうち **10 本**がこれだった。宣言した出力がすべて節点になることをテストにした。
**URL はカードまで指す。** `#t-基本送料` は表の見出しを指すリンクだったが、見出しを下のペインへ移したので行き先を失っていた。いまはその表を持つカードを選ぶようにし、選んだカードは URL に残る。「この件のこの表を見てください」がリンク一つになる。同じ順番の穴がもう一つあり、`?example=2` で開いたときはカードに行番号も答えも入っていなかった——ページの本体はカードより先に走るので、一回目の結果はカードができてから置き直す。
**測った**:コーパス 44 本。カードは最大 15 枚(クーポン割引)、準用を含む `非常勤退職手当` で借りた表もカードに入る。headless Chrome を CDP で動かし、例を押して**当てはまった二行が光ること**、カードを選んで下のペインが開き「列|出どころ」と「確かめたこと」が入ること、ボーダーが二本とも動くこと(横 433→609、縦 324→490)、`?example=2#t-基本送料` がカードを選んだ状態で開くこと、プレイグラウンドのリンクが blob からページをウィンドウいっぱいに出すことを確かめた(§15.52 と同じ確かめ方)。サイトのスクリーンショットは撮り直した——高さ 2600px で描いて 1560px の位置を切り出す旧来のスクリプトは、縦に積まれた文書が前提で、この配置には当たらない。
**捨てたもの**:
- **値ひとつに節点ひとつの図。** 読めなかった。節点に名前しか入らないなら、図は本文への索引でしかない。
- **右のペイン。** 表の詳細を右に置くと、承認者は図と右を往復する。カードの中と下のペインに分けたら要らなくなった。
- **大きい表を横スクロールに畳むこと。** 畳んだ列に何があるかは、畳んだまま分からない。キャンバスごと縦横に動かすほうが、承認する人の目の動きに合う。
- **資料の中の枠にこのページを埋め込むこと。** プレイグラウンドの 32em の枠では、左のペインだけで幅の六割になる。ウィンドウを前提にした配置には、ウィンドウを与えるしかない。
- **別の色で光らせること。** 行とカードで色が違うと、二つの意味があるように見える。
- **markdown にも図を出すこと。** mermaid を入れる判断が別に要る(§15.117)。いまは HTML だけ。
### 15.119 プレイグラウンドに、一枚の表で終わらない規則を置く(2026-09-22)
**きっかけ**:プレイグラウンドが見せていたのは一枚の表だけだった。rulec がこの一年で足したもの——表が表を読む形、値が合流する形、並びを歩く形——は、どれも触れる場所が無かった。
**決定**:サンプルを三つ足す。**どれもコーパスの規則そのもの**にし、`tests/website.rs` が一字一句で突き合わせる。ページの中に規則を書き下ろせば、それは「毎コミット検査も生成も実行もされない規則」が一本、いちばん人の目に触れる場所に置かれることになる。
| ボタン | 日本語 | 英語 | 見せる形 |
|---|---|---|---|
| 表をつなぐ | 送料 | parcel_rate | 表が決めた値を次の表が読む。`define` と `result` も |
| 大きい規則 | クーポン割引 | Claude利用料 | 同じ入力から出た二本が下で合流する(節点 16・辺 18) |
| 並びを歩く | 買物かごの送料 | shipment_surcharge | `elements` と `sum`。入力欄が「行を足す」になる |
**英語に、並びを歩く規則が一本も無かった。** コーパス 44 本のうち `elements` を使うのは 3 本で、全部日本語だった。だからサンプルを選ぶだけでは英語側が埋まらず、`shipment_surcharge` を書いた。**書いた瞬間に生成器の不具合が出た。**
**`elements` のフィールドにしか出ない単位は、型が生成されていなかった。** `mass[lb]` のフィールドを持つ要素を生成すると、Python は `m.Pound` を呼び、Rust は `pub weight: Pound` と書く——どちらも定義が無い。銘柄(brand)を集めるループが `inputs` と `outputs` しか歩いていなかったからである。**要素のフィールドは、入力と同じだけ呼び手が渡す値**で、しかも専用の構造体に書き出されるのに、そこだけ数えられていなかった。既存の 3 本が踏まなかったのは、要素のフィールドが全部 `money` で、その銘柄は出力が先に引き込んでいたからである。§15.9 が単位について言ったこと——**使われていない単位は検査されていない単位**——が、言語についてもそのまま成り立っていた。五つの生成器で同じ一行を直した。
**測った**:新しい規則は 12 言語 27 件すべて一致、証明書の再検査も両方通る。プレイグラウンドは日英とも五つのボタンが並び、三つのサンプルが検査を通り、承認者向けのページも開く(headless Chrome を CDP で確認)。コーパスは 45 本になり、サイトが数を書いている三箇所を直した。
**捨てたもの**:
- **サンプルをページの中に書き下ろすこと。** 検査も生成も実行もされない規則が、いちばん目に付く場所に置かれる。
- **サンプルを取ってくること。** ページは三つのファイルで完結している。四つ目を足せば、オフラインで開けなくなる。
- **`elements` のサンプルを日本語だけにすること。** 片方の言語にしか無い形は、片方の言語でしか検査されていない形である。実際にそうだった。
### 15.120 終了コードの表を、実際にそうする(2026-09-22)
**きっかけ**:§15.117 で `graph` を足したとき、「検査を通らない規則には出さない」というテストを書いたら**落ちた**。出ていたからである。調べると、同じ形が六つあった。
**`prepare` は検査の半分しか走らない。** 名前と型が解決するところまでで、§6 の領域の検査——完全性、重なり、どの入力にも当たらない行——はその後に来る。ところが `certificate`・`api`・`vectors`・`coverage`・`schema`・`adapter` は `prepare` だけを見ていた。**自分の終了コードの表には「1: 規則が検査を通らない」と書いてあるのに、完全性に穴のある規則に 0 を返していた。**
出ていたものも嘘だった。証明書は `cover` が `null`(再検査器は落とすので、証明として通ることはない——嘘なのは終了コードのほう)。`api` は生成されないコードの呼び方。`vectors` は何も settle していない表のベクタ。`gen` と `doc` だけが `report` を通していて、正しく 1 を返していた。
**決定**:`checked()` を一つ置く。`report` を走らせ、誤りがあれば**その診断を `check` と同じ体裁で標準エラーに出して** 1 で止め、無ければ `prepare` の結果を返す。標準出力ではなく標準エラーなのは、これらのコマンドの標準出力が機械の読む JSON だからで、途中に散文が混ざれば読む側が壊れる。
**線は「証明について」か「形について」かで引いた。**
- **証明の結果を出すもの**——`certificate`・`api`・`vectors`・`coverage`・`gen`・`doc`——は待つ。
- **規則の形を出すもの**——`graph`・`schema`・`adapter`——は待たない。名前と型が解決すれば出す。表にもそう書き直した(「1: 名前か型が解決しない」)。
**後者の線は、テストに教えられた。** 最初は `schema` と `adapter` も関門に通した。`tests/extract.rs` が落ちた——**`adapter --template docling` は抽出器のテンプレートを出すコマンドで、その抽出器が無いと PDF のコピーが取れず、コピーが無いから規則は E037/E039 で通らない。** 通らないとテンプレートが出ないなら、入り口が無い。`graph` を `check` より手前にしたのと同じ理由が、こちらにもあった(§15.117)。**絵もテンプレートも、規則を直している最中にいちばん要る。**
**`coverage` の表は書き足した。** そこの「1」は元々「義務が欠けている」で、意味が一つ増えることになる。どちらも「きれいに終わらなかった」であり、CI の関門としての読み方は変わらない。文面が理由を言い分ける。
**`verify` は触っていない。** そこの「1」は「食い違いがある」で、規則が通らないことと混ぜると CI が取り違える。穴のある規則に対しては、そもそもアダプタを立てる前に別の理由で止まる(実測で 2)。要求が出てから決める。
**確かめ方**:穴だけがある規則(名前も型も解決する)を一つ置き、**七つのコマンドが 1 を返し、三つが 0 を返すこと**を一本のテストで固定した。エラーが何が悪いかを言うこと(`E101` が出る)と、エラーにしたのに標準出力へ何も書かないことも見る。
**捨てたもの**:
- **`prepare` 自身に領域の検査を足すこと。** `prepare` は `diff` と `graph` と `schema` が使う「名前と型まで」の関門でもあり、そこに足すと後者の入り口が塞がる。関門を二つに分けて、コマンドがどちらを要るか選ぶ形にした。
- **エラーを標準出力に出すこと。** `gen` と `doc` はそうしているが、あちらの標準出力は人が読む。JSON を出すコマンドで同じことをすると、読む側が壊れる。
### 15.121 式で決まる値は、その式をカードに出す(2026-09-22)
**きっかけ**:`define` と `derive` のカードを見ても、その値がどう決まるのかが読めない。出ているのは「定義」という語と、読んでいる名前の一覧だけで、**名前が既に言っていること以外は何も言っていなかった**。
**決定**:`rulec graph` の `by` に `expr` を足し、カードに出す。文面は `doc` が「導出と定義」の表に出しているものと同じ——同じ関数(`doc::expr_src`)が、宣言範囲と出典を落とした「著者が書いた行」を返す。式は読んでいる名前を全部含むので、`← 注文金額` の行に**足す**のではなく**置き換える**。
**二つの向きでテストした。** グラフが持つ式はすべて `doc` が出しているものであり、`doc` が出している式はすべて対応する節点に乗っている。片方だけなら、式を一つも持たないグラフが通る。
**準用が持ち込んだ値は式を持たない。** 行番号は相手のファイルを指しているので、こちらの行を読めば**まったく別の行が出る**(実際、`退職手当:満額` の式として `"退職手当.rule" sha256:…` が出た。テストが捕まえた)。相手のファイルを開けば読めるが、`build` は `(f, c, src)` の純関数で、**プレイグラウンドにはファイルシステムが無い**。読める場所と読めない場所で中身が変わるほうが悪い。だから準用の値は、読んでいる名前の行のままにした。`apply` が束縛する出力(`手当 -> 非常勤手当`)も同じで、そもそも `=` で書かれていない。
**まだ出ていないもの:中間の値そのもの。** 表で決まる値は、光った行の `→` 列に答えが出ている。出力はカードの見出しに出る。しかし `define`・`derive`・`sum` の**いまの値**はどこにも出ない——生成コードが返すのは出力と当てはまった行だけで、途中の値は関数の中の局所変数である。出すには生成コードの返り値を変えることになり、それは 12 言語すべてと `record` の形(`replay` と `diff` が読む)に関わる。ページだけで計算すれば、**規則の評価がページの中にもう一つできる**——この設計がいちばん避けているものである。だから今回は出していない。
### 15.122 記録の無い差分——二つの版が違う答えを返す入力の全体(2026-09-22)
**きっかけ**:`rulec diff` は `--fixtures` を要る。答えるのは「手元の記録のうち何件が動くか」で、**見たことのない件については何も言えない**。承認する人が本当に知りたいのは「そもそもどの入力なら動くのか、そして動かないと言い切れるのはどこか」のほうである。二つの版はどちらも宣言した空間の全域関数で、その空間は完全性の証明がすでに区画に切っている。ならば差は決定可能ではないか。
**決められるのは、規則が全域関数で、その空間が有限の区画に切れているからである。**任意のコードについて「二つの版が同じ答えを返すか」は決められない。ここで決められるのは §3 と §5.3 の構文制限がちょうどその境界に置いてあるからで、§6.2 が区間の格子で足りると判断したのと同じ理由による。
**分かったこと(試作で測った)**:
- **組み合わせの数は小さい。**コーパス 45 本の、規則ひとつぶんの列の積は最大 11,664、中央値 63、100 万を超えるものは無い。総当たりで足りる。区画の代数は差を**探す**ためではなく、**まとめて見せる**ためだけに要る。
- **証明書には答えが載っていない。**出力セルが平値でない(`produces` が `null`)のが 516 中 404、78%。証明書は五つの証明のための文書なので、金額はそもそも無い。差分エンジンは証明書だけからは作れない。
- **入力の空間に置くと、静かに間違える。**最初の試作は入力の軸だけを切った。`事務所の衛生基準` の `余裕`(`床面積 - 占有面積`)の閾値を `<10m2` から `<15m2` に動かすと、実際には 6,336 件中 240 件で答えが変わる(`適合/0円` が `要改善/3000円` と `使用停止/10000円` になる)のに、**試作は「違う入力は無い」と答えた**。`余裕 >= 10m2` は入力の平面の斜めの帯で、軸をどれだけ細かく切っても区画にならないからである。§15.81 で ClickHouse について書いたのと同じ形——**エラーにならないのが一番悪い**。
- **列の空間に置くと、区画になる。**同じ差は `余裕 >=10m2 <=14m2 かつ 騒音 <=84dB かつ 残業 <=45h` で、これは厳密で、しかも規則の書き方そのままである。畳み込みの要約も同じで、`合計 >=3000円 <5000円` とは書けるが「どの買物かごか」は書けないし、書く必要もない。
**決定**:`rulec diff <旧> <新>` の `--fixtures` を任意にする。付けなければ、二つの版を**入力空間の全体**で比べ、答えが違う入力の領域と、**その外では同じだという主張**を出す。新しいコマンドは作らない。同じ問いを規則に訊くか記録に訊くかの違いだからで、報告の形(当てはまった行でまとめる塊)も既存の `diff` のものをそのまま使う。
**空間は入力ではなく列に取る。** 軸になるのは、宣言した入力すべてと、**どこかの表が列として試している** `derive`・`define`・畳み込みの要約だけ。式から読まれるだけの導出は計算されるので軸を持たない——ここを外すと空間が桁で膨れ、そのほとんどが誰も送れない入力になる(この一段を入れた時点で測ると、外についてまで言い切れる規則が 25 本から 38 本に増えた。当時はまだ下の先回りが無く、自分自身との比較でも全部を当たっていた)。宣言した出力も軸にしない。答えは座標ではない。
**組み合わせは一つずつ三通りに決める。**
1. `Same`——証明した。二つの版が**同じ計算を走らせた**(同じ行が当たり、その出力セルも式も丸めも字面まで同じ)か、両版の答えがその範囲のどこでも一つの値で、かつ等しいか。後者が要るのは、変更が下流で消える場合があるからである。送料の例では `基本送料` を 1800 円から 2000 円に上げても、大口は負担率 0% なので `×0` が変更を消す。行の出力セルは違うのに答えは同じ、という組み合わせがここに落ちる。
2. `Differs`——証明した。違う答えを返す入力を添えて出す。
3. `Unknown`——決められなかった。数え、領域として出し、**外についての主張を取り下げる**。
**入力を作れなかった組み合わせも、決められなかったに数える。** 座標の組が矛盾していないのに実現する入力を組めなかったなら、それは比べていない。比べていないものを「同じ」に混ぜないのが §15.99 の線である。コーパスでこれが残るのは**入力を共有する二つの導出**を持つ規則だけで、`クーポン併用` が自分のコメントで「Fourier-Motzkin なら証明できる形」と書いている、**W114 とまったく同じ盲点**である。実際に出てくる領域も `残高A <=1000円 かつ 残高B >=3980円` で、規則が書いているその組み合わせそのものだった。**差分の主張が引き下がる場所と、重なりの証明が引き下がる場所が一致している。**
**斜めの帯は解く。** その組み合わせの導出の座標に当たる入力は、探すのではなく解く。式はその範囲では線形なので(§5)、入力ひとつぶんの当たりを取れば係数が出て、割り算一回で動かす先が決まる。届かなければ次の入力が残りを引き受ける——`達成率` は四つの点数の各四分の一なので、90% に届かせるには四つとも動かす必要がある。入力を共有する導出があるので、全部が同時に成り立つまで通す。
**受け付ける入力の変化は、答えの差とは別に出す。** 入力の増減、型、範囲、列挙の値、出力の丸め。扉が広がったことは違う答えではない。混ぜると読めなくなる。**そして軸は、両方の版が受け付ける入力に絞る**——範囲は狭いほうの端、列挙は両方にある値だけ。絞らないと、旧版が入口で受け付けない入力について「同じ答えを返します」と言うことになる(参照評価器は入口のガードを持たないので、黙ってそう言えてしまう)。入力そのものが増減したときは、両方に渡せる入力が無いので比べられないと言い、扉の違いだけを出す。
**並びを畳む規則は、比べられないと言う。** 答えが並び全体で決まるので、有限個の列の関数ではない。要約の上に領域を書くと、**書いてあるように見えることを言わない領域**になる。歩く側(一件ぶんの表、畳み方の各腕)が一つも変わっていなければ `Same` と言えるので、そこまでは言う。
**答えを決めるものが何も変わっていないなら、組み合わせは当たらない。** 同じ行が当たり、同じ値を書き、同じ式と同じ丸めなら、二つはどの入力でも同じ関数である——一つの組み合わせについて `identical_run` が言うことを、全部について一度に言っただけである。これが無いと、`--diff-base` のような使い方でも毎回全部を当たることになる。**綴りは見ない。** リテラルは値で比べる(`1800円` と `1_800円` は同じ額)。見ていた頃は、書式を直しただけの版に領域が出た。
**この先回りが、一度この命令に嘘をつかせた。** 最初の版は行を**答え**で見分けていた。`<=2000g` を `<=1500g` に動かすと、出力セルは一つも動かない——だから先回りが当たり、答えが変わる規則について「どの入力でも同じ答えです」と出た。これは §15.81 で ClickHouse について書いた害と同じ形で、しかも**この命令がいちばん出してはいけない一文**である。行の指紋に**何を試すか**(各列のセル、方針、優先の順序)を入れ、さらに行の外で答えを決めるもの——**群の中身**(`= 遠隔地` の指す先が変われば、行はそのままで当たる入力が変わる)と `constraint`——も足した。セルは順序を揃えてから比べるので、集合や連言の書き順を入れ替えただけの版では先回りが当たったままである。`tests/vdiff.rs` に、閾値と群のそれぞれについて「先回りしてはいけない」と「先回りしてよい」を置いた。**一箇所にまとめて書いてある**のは、この一覧を落とすことがこの命令の唯一の嘘の付き方だからである(`decides_alike`)。
**同じ答えが返ってきても、一つの点だけで決めない。** 最初に作る入力は閉じた端から一目盛り内側で(§11 原則 2)、そこは境界のずれが出る場所だが、**率のずれが出る場所ではない**。百万トークンあたり 10USD と 17USD は、1 トークンではどちらも 0 である。だから同じ答えが返ったら、その範囲の他の端でも訊く。ただし入力を一つ動かすと導出がその範囲の外へ出ることがあるので(`商品合計` を上げると `支払額` も動く)、動かした点を種にして**解き直す**。この一段で、会員特典の「一般会員の倍率を 100% から 200% にする」が、決められない 15 通りから、違う 6 通り+決められない 9 通りになった。
**測った**:コーパス 45 本。自分自身と比べるとどれもここで片がついて「どの入力でも同じ」(0.14 秒で 45 本)。金額を一つ動かして実際に全部を当たるのは 14 本で、そのうち **12 本が「ここに挙げた入力のほかでは同じ」まで言い切る**。言い切れない 2 本は、`クーポン割引`(ふるいの盲点、9,072 通り)と `預け荷物料金`(無料個数が一人あたりなので答えがその範囲で一つの値にならず、試した点では全部一致したが証明にはならない 1 通り)である。送料の例は起きうる 3,525 通りのうち 24 通りが違い、区画は 2 つ、0.05 秒。同じ二版を、まとめて作った 504 件の記録に当てた `diff --fixtures` は「影響 54 件」と言い、その範囲が含む組み合わせは 2×3×3×3 = **54 通り**。一致する。コーパスで一番広い `クーポン割引` は 437,400 通りで 6 秒——**導出まわりの座標だけで決まる計算を覚える**ことで 20 秒から落とした(組み合わせごとに解き直していたものが、座標の小さな組に対する一度の問いになる)。
**動いた記録は、全部挙げる。** `diff --fixtures --format json` はクラスタごとに件数と例を一件しか出していなかった。数えるだけでは、記録に対して何かをする側——古い版を残す、見積もりを出し直す——が動けない。クラスタに `records` を足し、`line`(fixtures の行。`verify` では流した順)と `tag` で一件ずつ示す。`fixtures lint` が問題の場所を示すのと同じ対で、記録が `tag` を持たなくても行で見つかる。`replay` と `verify` も同じ報告の形なので、三つとも同時に付いた。**`--terse` は効かない。**あれは PR のコメントに本番の値を貼らないための旗で、`--format json` とは元から一緒に書けない。
**`tests/vdiff.rs` が毎回三つを縛る。** (1) 動いた記録が領域の外に落ちていないこと。(2) **領域が出した入力が、本当に違うこと**——一件の記録として突き返し、記録側の `diff` が不一致と言うまで確かめる。(3) 決められない規則の顔ぶれが上の 2 本のままであること。**記録が動いたのに「外でも同じ」と言う**のがこの命令の最悪の壊れ方なので、そこだけは別に書いてある。
**`--fixtures` のときだけ意味のある旗はエラーにする。** `--manifest` と `--fill` は記録に補完を当てる話で、`--budget` はその逆。知っている旗でも、そのモードで意味が無ければ exit 2 で止める——未知の旗を黙って落とさない線(§12.1)を、既知の旗の置き場所にも当てた。
**exit は証明した差だけで 1 にする。** 決められなかった領域は 1 にしない。`check` が W114 を報告しても落とさないのと同じ線で、厳しい関門が要る相手は JSON の `total` を読む。
**公開の文面には造語を出さない。** §15.15 が「箱」と「証人」を落としたのと同じ理由で、出力は「入力の組み合わせ 7,050 通り」「ここに挙げた入力のほかでは」と書く。
**捨てたもの**:**入力の空間に領域を書く案**。上のとおり、静かに嘘をつく。**生成コードを評価器に使う案**(試作はこれで、だから斜めの帯に届かなかった。導出の座標を指定して走らせることができない)。**ふるいを厳しくして、入力を作れなかった組み合わせを「起こらない」に倒す案**。それは差を取り逃す向きの近似で、安全な向きの逆である。**新しいコマンドを足す案**。同じ問いであり、`--fixtures` の有無で分かれるほうが、二つの答えを突き合わせる習慣に繋がる。
### 15.123 設計記録のほうも、造語をやめる(2026-09-22)
**きっかけ**:§15.122 を書いたあと、この記録の日本語が読めるかを見直した。「升目」「短絡」「掃き出し」「空を歩く」——どれも英語の言い方をそのまま漢字にしたもので、書いた本人以外には通じない。そして同じ目で見ると、§15.15 が公開文書から落としたはずの「箱」と「証人」が、この記録には 85 と 65 のこっていた。
**§15.15 の「残すもの」を取り消す。** あのときは「ここは実装の設計記録で、読者は決定可能性の議論を追っている」から割れてよいとした。割れてよくない。設計記録も日本語で、あとで読むのは同じ人である。しかも語彙が二重だと、公開の文面を書くたびに訳し直すことになり、実際 §15.122 の出力は「升目」を素通しで出していた。
**置き換え**:
| 前 | 後 |
|---|---|
| 箱(箱代数・箱和・交差箱・残余箱を含む) | 区画(§15.15 が公開の文面に選んだ語に揃える。複合語は「区画の代数」「区画の和」「交わりの区画」「残りの区画」) |
| 篩 | ふるい |
| 証人 | その入力/出す入力/入力例(§15.15 の呼び方) |
| 升目 | 組み合わせ/通り |
**語そのものを論じている §15.15 の中と、`公証人法` は元のまま。** 一括置換は後者を `公その入力法` に変えた。法令の名前が壊れるのは、置換で起きる害の典型である。**変わった 129 行は一行ずつ読み直した**——「定義軸の証人」が「定義軸のその入力」になるような、機械が作った意味の通らない言い回しが二十箇所ほどあった。
**英語側は触らない。** box / cell / witness / sieve は形式手法で定着した語で、英語では浮かない。§15.15 がそう決めていて、その部分は今も正しい。
### 15.124 境界の側を、出典のコピーに突き合わせる(2026-09-22)
**きっかけ**:§15.82 は「比べるのは金額だけ」とし、理由をソースのコメントにこう書いた——「閾値は転記するときに書き換わる(`1,949,000円まで` は `<=1949000円` になる)。金額は書き換わらない」。この理由は、書き換わったあとには**何も残らない**と言っているように読める。残る。どちらの書き方に変えても、境界の値そのものが上下どちらの区分に入るかは動かない。
**見立て**:「60cmまで」を `<60cm` と転記する誤りは、いまの検査を全部すり抜ける。60 という数は使っているので W120 は黙り、金額ではないので E116 は見ない。境界を分け合う二つの行を**両方**動かせば、完全性も重なりも緑のままである。動くのはちょうど 60cm の一点だけで、そこだけ金額が変わる。承認する人のページとお客向けのページは両側を書き出すが(§15.61 の境界の対)、それが効くのは**二枚を見比べる人**にだけで、検査ではない。§15.82 が四段で塞いだ穴の、残っていた一つである。
**決定**:比べるのは演算子ではなく**側**——境界の値そのものが上下どちらの区分に入るか——にする。この形で比べられるのは、**否定が向きを入れ替えて側を保つ**からである。`<=60` と `>60` は一つの境界の両側で、どちらも「60 は下に入る」と言う。だから規則がどちらの端から書いていても、コピーの言い方と突き合わせられる。読む場所は二つ。
1. **数のとなりの語**。うしろは日本語と英語(`60cm以下`、`60cmを超え`、`21 and over`)、まえは英語だけ(`Up to £12,570`、`Not over $11,925`、`Under 18`)。同じ `over` が位置で意味を変える(`over £125,140` は下、`21 and over` は上)ので、まえとうしろは別の表で読む。
2. **フィールドの見出しの語**。`円以上` と `円未満` を見出しに置いて下は裸の数字にするのが日本の料額表の普通の形で、セルだけ読む版ではここに何も見えない。見出しと数えるのは「境界の語と単位だけでできているセル」に限る——語を取り除いた残りが空か、道具の知っている単位であること。`超過額` は `過額` が残るので読まない。
語が無いとき(`18 to 20`、`60〜80`)と、同じ数に両方の側が付くときは、何も言わない。`X to Y` の `to` は、X では始まりで Y では終わりで、同じ語が位置で意味を変える——読み違えれば正しい規則を落とすので、黙るほうを採る。診断は **E119**、fix は `flip_bound`(`<` と `<=` の入れ替えだけ。向きは表の幾何であって、コピーが決めることではない)。通貨は記号も読む(`$`・`£`・`€`)。`$11,925` と `1192500USDc` を同じ値として突き合わせるのにこれが要る。`¥` は読まない——日本語の文書は数のうしろに `円` と書き、字句解析器がすでに読んでいるし、`¥` は一つの通貨を指さない。
**測った**:コーパス 45 本の表の閾値を、境界ごとに**対で**——その境界を分け合う行を全部同時に——反対側へ動かした。199 通り。いまの検査で捕まるのは 35 通りだけで(例がちょうど境界に乗っていれば E107、片側だけずらせば E101・E105・W105)、**164 通りは何も言われない**。E119 はそのうち 43 を捕まえ、**43 とも E119 だけが出る**。手を入れていないコーパスには 0 件で、誤検出は無い。
内訳。出典の表を引いている 5 本に 51 通りあり、43 が捕まる。残る 8 通りのうち 6 通りは `X to Y` の言い方(`£12,571 to £50,270`、`(£125,001 to £250,000)`、`18 to 20`)で、2 通りは引いた表の外から来た境界である(`allowance_of` はそもそも引用が無く、見習いの 19 歳は表でなく地の文にある)。引用の無い 40 本の 148 通りには何も言わない——突き合わせる相手が無い。
一番効いたのは日本年金機構の保険料額表(`R08ryougaku.xlsx`、§15.82 で使った本物)で、**31 通り中 31**。この表が見出しに `円以上`・`円未満` を置く形で、セルだけ読む版では 0 通りだった。見出しを読む一段がこの差の全部である。
**走らせた**:変異を二本、生成器から作った。`m_e119.rule` は英語でセルに語がある形(GOV.UK の `Under 18`)、`m_e119col.rule` は日本語で見出しに語がある形(保険料額表の `円以上`・`円未満`)。どちらも**出るのは E119 だけ**で、境界を分け合う二行の両方に出る。文面は golden に固定した。
**承認する人のページにも一行増えた**:「この表の境界 31 個は、出典のコピーが書いている側と同じです(E119)」。E116 の一行と同じく、`doc` は check を通った規則しか出さないので言い切れる。コピーが境界を一つも言っていない表には出さない——言っていないことを「確かめた」と書く行は、いちばん危ない行である。
**正直な限界**:三つ。**`X to Y` は読まない**(上のとおり)。**見出しを読んでも、コピーのフィールドと規則の列を対応づけてはいない**。見出しの語を同じフィールドの数に配るだけで、§15.82 が W120 について書いた限界——コピーの列の意味を読んでいない——はそのままである。**引用が無ければ何も言えない**。いまも何も言われない 121 通りのうち 114 は、出典を引いていない表のものである。
**捨てたもの**:警告にする案。E116 と同じく落とす。コピーと食い違う転記を緑にしてよい理由は無く、逃げ道(その行の引用を外し、どこから来たかを行末のコメントに書く)は E116 のものがそのまま使える。`up to` を曖昧として落とす案。税と運賃の文書では「〜まで」の意味でしか使われておらず、GOV.UK の `Up to £12,570` がその実例で、落とすと英語側の半分が読めなくなる。`fix.text` に向きまで書く案。コピーから決まるのは側だけで、向きまで書けば決まっていないことを書くことになる(§11 の原則 3 が `fix.text` に散文を持たせない理由と同じ線)。
### 15.125 呼び出し側のオブジェクトから入力を取り出す(2026-09-22)
**きっかけ**:§15 の未決 6 番「入力の射影と由来の宣言」は、着手の条件を「§15-5 の試作で総量を測ってから」と書いていた。その §15-5(畳み込み)は §15.56 で出荷済みで、条件は満たされている。残っていたのは、あの項目が自分で書いた採用の根拠——「検査に一切触れないこと」——がいまも成り立つかを確かめて、書くことだけだった。成り立つ。
**見立て**:規則の入力は平たいスカラーで、呼び出し側が持っているのは入れ子のオブジェクトである。そのあいだをつなぐコードは、いまはホストの手書きで、**規則ごとではなく入力ごとに増える**。しかも検査は一行も見ていない。`check` は表の中を見て、`test` は生成物と表の一致を見て、§15.82 は表と転記元を見る。**入力と呼び出し側のオブジェクトのあいだには、また何も無い。**
埋める材料は既にある。呼び出し側は JSON Schema か `.proto` で注文の形を決めていることが多い。借りればよい。**自前のドメインオブジェクトの型体系は作らない**(§15-6 が非交渉と書いたとおり)。
**決定**:宣言を二つ足す。`shape` が契約を指し、入力の行末の `from` がその中のどこから来るかを言う。
```rule
shape 注文(order) = jsonschema "contracts/order.schema.json" "#/$defs/Order"
inputs
あて先(zone) : 地域 from 注文.shipping.zone
冷蔵あり(cold) : bool from any 注文.lines where chilled = true
明細数(lines) : number range >=1 <=50 from count 注文.lines
```
**検査には触れない。**射影から出てくるのはただのスカラーの入力で、領域解析はそれが射影されたことを知らない。完全性も重なりも単位も溢れも、`from` が無いときとまったく同じに決まる。買うのは二つだけ——**つなぐコードが生成されること**と、**契約がフィールドの名前を変えたら生成の前に止まること**。
**言語は打ち止めにする**:一つのコレクション、`any`・`all`・`count`、要素のフィールドへの単項テスト。結合も入れ子の量化も入れない。セルにパス式を入れる形は §0 のとおり採らない——セルの言語がこの道具の境界だからで、そこを曲げれば区画の代数が立たなくなる。`where` の値は契約が運ぶ**生の値**と比べるので、単位は書けない(E120)。契約に単位は無く、目盛りの違う数を黙って比べるのは、この道具がしてはならないことである。
**§15-6 の一点を覆した。** あの項目は「照合と生成は gen 側」と書いていた。契約は毎回の `check` で読む。§15.59 が `.proto` を `check` に入れたときと同じ理由で——契約が動いたのは誰も読んでいない変更であり、それを生成のときまで黙らせる理由が無い。固定(digest)は付けない。これも `import proto` と揃えた。契約を縛るのは digest ではなく**パス**である。
診断は三つ。**E120**(`from` の返すものが入力の型と合わない、道の先の型が合わない、並びでないものを歩こうとしている、`where` の値がフィールドと合わない)、**E121**(パスが契約に無い。どこまで届いたかと、そこにあったフィールドを出す)、**W122**(宣言したのにどの入力も射影していない `shape`)。読めない契約は `import proto` と同じ E013 である。
**生成は 12 のうち 5**:Python・TypeScript・JavaScript・Ruby・PHP。境界は「**呼び出し側のオブジェクトを型の名前で書く必要があるか**」で引いた。この五つはただの連想配列なので名前が要らない。Go・Swift・Java・Rust は型で持っていて、その型を指すやり方は二つしかない——生成する(ドメインオブジェクトの型体系。作らない)か、相手の型を追いかける(§15.63 と §15.73 で二度却下した)。SQL は平たい列の関係を、NumPy は列を、Wasm の ABI は規則自身の入力の JSON 一つを取るので、射影する相手のオブジェクトがそもそも無い。**パスの検査は 12 全部に等しく効く**——`check` の中で起きるからである。
**走らせた**:コーパスに `注文の送料.rule` と契約 `contracts/order.schema.json` を足した。三つの形を全部使う(フィールド・`any … where`・`count`)。`check` は通り、ベクタ 28 件、行 7 / 7、境界 2 / 2。**五つの射影関数を実際に呼ぶ試験を置いた**(`tests/projection.rs`)。契約に合わせて手で組んだ注文を五言語に通し、規則を直に呼んだときの答えと突き合わせる。`rulec test` のベクタは規則自身の平たい入力なので、射影関数はそれまで「組めるが一度も走らない」ものだった——走らない生成物は主張の外である(§15.46)。JSON Schema の側は `$ref` を追い、`.proto` の側は `repeated` を含むフィールドを読む(`map` と `oneof` は読まずに飛ばす。形を勝手に決めるほうが危ない)。承認する人のページには入力ごとに「呼び出し側の `注文.shipping.zone` から取ります」の一行と、引いた契約の名前が出る。`rulec api` は `projection` の下に、契約・入力ごとのパス・五言語の関数名と署名を並べる。
**変異を三本**、生成器から作った。`m_e120`(件数を bool で受ける)、`m_e121`(契約が持っていないフィールドを射影する——フィールドの名前が変わったときの姿)、`m_w122`(契約を宣言して誰も使わない)。E120 と E121 の文面は golden に固定した。
**正直な限界**:四つ。**生成は 5 / 12**(上のとおり)。**`map` と `oneof` は読まない**ので、その中へのパスは通らずに E121 になる。**`$ref` は文書の中だけ**追う。外へ出る `$ref` は取りに行かず、そこで止まる。そして一番大きいもの——**パスが契約に通っても、呼び出し側が実際にその契約に従っている保証はここには無い**。契約と現実のあいだには層が一つも無く、それは §15.47 の「何についての証明か」と同じ線の上にある。この一段が言えるのは「規則の入力は、契約がそう言っている場所から来ている」までである。
**捨てたもの**:契約に digest を付ける案(`import proto` と揃えた。上のとおり)。契約から型を生成する案(§15-6 の非交渉)。結合と入れ子の量化(実例を数えてから。§15-6 の三つ目の注意そのまま)。`from` を注記だけにして照合を gen に置く案(覆した。上のとおり)。射影を `elements` の並びの出どころにも使う案——並びは呼び出し側が組み立てて渡すもので、`fold` の要素は規則の型を持つ。契約の配列をそこへ流し込むには要素のフィールドごとの射影が要り、それは「一つのコレクション」の打ち止めを一段深くする。実例を数えてから。
### 15.126 入力を共有する導出どうしの重なりを、消去で決める(2026-09-22)
**きっかけ**:§15 の未決 4 番「相関する導出と Fourier–Motzkin 消去」。発動条件は「W114 が実表に出続けること、またはガードに本番で当たりが出ること」と書いてあった。どちらも起きていない——コーパスの W114 は 1 件で、それは**この現象を見せるために書いた規則**である。条件を待たずに入れる理由のほうを書いておく。**W114 はこの道具で唯一、静的な証明が無い場所である**(README の「証明していないこと」の 3 番、生成コードの実行時ガード)。その穴が、算術で決まる形については塞げると分かっているのに開いたままなのは、条件の問題ではない。
**見立て**:領域解析は導出ごとに独立した軸を与え、その軸はその値が到達しうる区間でしか縛られない。**健全だが粗い**。`残高A = 合計 − 割引A` と `残高B = 合計 − 割引A − 割引B` は定義から `残高B <= 残高A` なのに、検査からは自由に動く二本の軸に見える。だから `残高A <= 1000円` の行と `残高B >= 3980円` の行が重なって見え、入力も構成できず、W114 になる。
導出は入力の線形結合で(§5)、セルは単項の比較で、範囲は区間で、`constraint` は二つの入力の比較である。だから「この二行が同時に当たる入力はあるか」は線形不等式の連立で、有理数上では変数を一つずつ消して決められる。
**決定**:`src/fourier.rs` に Fourier–Motzkin 消去を置き、入力を構成できなかった対について、W114 に落とす前に「そもそも存在しうるか」を尋ねる。連立に入れるのは、両方の行のセル、導出の定義式、宣言した範囲、`constraint` の四つ。**読めないものは落とす**——落とすと系は緩むだけなので、緩めた系が充足不能なら元も充足不能である。
**一方向にしか使わない。** 言えるのは「解が無い」ことだけで、有理数の解が見つかっても整数点があるとは限らないから、そのときは何も主張せず W114 はそのまま残る。厳密不等号は厳密のまま扱い、整数性で締めることはしない(`x < 1` を `x <= 0` に強めるのは、変数が整数だと分かっているときしか正しくない)。単位の変換はしないので、連立に入る名前は全部同じ型でなければ諦める。不等式の本数の上限は 400 で、超えたら「証明できなかった」と答える——失うのはもともと出るはずだった W114 一つだけである。
**走らせた**:コーパスの `クーポン併用.rule` の W114 が消えた。§15 の 4 番が効果の物差しとして書いていた一件そのものである。コーパス全体の W114 は 0 になった。
**危ない向きのほうを測った。** 決められないのを決められないと言うのは安全側だが、**本当に重なる対を「起きない」と言えば、エラーが沈黙に変わる**。これがこの道具のしてはいけないことなので、答えの分かる組み合わせを総当たりで当てた(`tests/fourier.rs`)。同じ二つの導出に閾値 `a` と `b` を振り、`残高B <= 残高A` から答えは「`b <= a` のときだけ重なる」と決まっている。25 通りのうち 10 通り以上が両側とも実際に通り、**起きる重なりが黙って消えた例は 0**、起きない重なりが残った例も 0 だった。
**走らせて見つけたもの**:**`constraint` を破る入力が、E105 の「両方に当てはまる例」として出ていた。** 表の列が導出のとき、ふるいは `constraint` を当てられない——制約は入力について書かれていて、その入力は列ではないからである。そして `vectors::pair_witness` は制約を見ずに入力を組んでいた。`constraint a <= b` のある規則で `a = 5, b = 0` を「両方に当てはまる例」として出す。§6.1 は「挙げる入力はどれも、誰かが実際に送りうる場合になる」と約束しているので、これは約束が破れていた場所である。`pair_witness` に `allowed` を通した。コーパスの診断は変わらない(制約を持つ規則の列が導出でなかったため)。
**正直な限界**:三つ。**真偽の `define` には届かない**。定義の本体は連立に入らないので、同じ入力から出た二つの定義(`上位 = 点数 >= 8` と `下位 = 点数 <= 2`)が同時に真に見える形は W114 のまま残る。§15 の 9 番「単項原子の DNF 代入」がその項目である。**整数性だけが妨げている形**も残る。**単位をまたぐ連立**は諦める。
**W114 の題材を差し替えた。** これまでコーパスの `クーポン併用.rule` が W114 の見本で、台帳の最小再現も同じ形だった。どちらも消去が決めてしまうので、残る形——同じ入力から出た二つの真偽の定義——に置き換えた。手で置いた変異 `m_w114.rule` がそれで、`BY_HAND` に名前を書いた(コーパスは正しい規則でできているので、この形は無い)。`クーポン併用.rule` は残す。今度は**消去が働くことの証拠**である。
**捨てたもの**:SMT に載せる案。§15 の頭が「列の線形条件は導出として取り込み、SMT は当面不要」と書いたとおりで、消去は必ず終わるから検査は「決定」のままでいられる。SMT を呼べば「予算内に反例が見つからなかった」が普通の答えになり、検査の性格が変わる。整数計画に進む案(Omega テスト、切除平面)。いま残っている限界は整数性ではなく `define` の本体なので、先に効くのは 9 番のほうである。`rulec diff` の「入力を作れなかった組み合わせ」(§15.99)に同じ消去を当てる案。あそこが見ているのは行の対ではなく一つの組み合わせで、形が違う。同じ盲点であることは記録に残っているので、次に手を入れるときに一緒に見る。
### 15.127 定義の中の閾値も、消去に入れる(2026-09-22)
**きっかけ**:§15 の未決 9 番「単項原子の DNF 代入」。発動条件は「過剰報告が実表で邪魔になること」だったが、§15.126 を入れた直後に別のことが分かった。**W114 に残るのが、真偽の `define` の中身だけになった。** 唯一の静的な証明の無い場所が一つの形まで狭まったのなら、その形を見るのが順番である。
**見立て**:§6.2 は真偽の定義を**自由な軸**として出荷した。`上位 = 点数 >= 8` と `下位 = 点数 <= 2` は同じ入力から出ているのに、検査からは独立に動く二本の軸に見える。だから「上位 かつ 下位」という起きない組み合わせが重なりとして残り、入力も構成できず、W114 になる。
**決定**:**DNF に均す必要は無かった。** E113 が定義の本体を「一つの名前と定数の比較」に限っている(それが完全性と重なりの検査を有限に保っている条件そのものである)ので、代入する対象は常に一本の比較である。9 番が想定していた展開の段は、言語の側が先に閉じていた。
やることは一つだけになる。**重なりがその定義を一つの真偽値に決めているとき**、本体は「そこで成り立たねばならない比較」か「そこで成り立ってはならない比較」のどちらかで、どちらも線形の条件である。§15.126 の連立にそれを足す。否定は向きの入れ替えで書ける——`not(<=)` は `>`、`not(<)` は `>=`。`=` は、真なら二本の不等式に、偽なら選言になるので、偽の側だけ入れない。重なりが両方の真偽値にまたがっているときは、その対は定義に依存していないので何も足さない。
**日付も連立に入れた。** 日付は通算日で持っているので(§2.1)、線形の系にそのまま座る。`前半 = 注文日 <= 2026-06-30` と `後半 = 注文日 >= 2026-07-01` が同時に真になれないことは、これで決まる。金額や数量と同じ系には入れない——型が違えば諦める、という §15.126 の線をそのまま使う。
**走らせた**:`上位 = 点数 >= 8` と `下位 = 点数 <= 2` の対が決まるようになった。逆向きも測っている——`下位 = 点数 <= 95` に変えると本当に重なるので、そのときは黙って消さずに残る(`tests/fourier.rs`)。日付の対も決まる。
**W114 の題材を三度目に差し替えた。** §15.126 で「導出が入力を共有する形」から「真偽の定義ごし」へ移し、その定義ごしがここで決まるようになった。残る形は、**消去が有理数の上で解いていること**そのものである:`倍額 = 支払額 + 支払額` は必ず偶数なので `5円` ちょうどにはならないが、有理数には `2.5円` があり、消去はそこで止まる。`m_w114.rule` がそれで、台帳の最小再現も同じ形にした。整数であることまでは見ていない、と本文に書いた。
**正直な限界**:四つ。**有理数の上で解いている**(上のとおり。整数計画へ進めば閉じるが、いまの実表にその形は無い)。**予算**(400 本を超えた系は「証明できなかった」)。**単位をまたぐ系**は諦める。そして **`=` が偽に決まる形**——選言になるので、一つの連立には入らない。どれも「証明できなかった」であって「起こりうる」ではなく、W114 の文面がそう言う。
**捨てたもの**:本体を DNF に展開する案(E113 のおかげで要らなかった。9 番の見出しはそのまま残すが、やったことは代入だけである)。整数計画(Omega テスト、切除平面)へ進む案——閉じるのは上の一つ目だけで、実表にその形が出てから測る。定義の軸そのものを消して入力の軸に畳む案。軸を消すと挙げる入力の書き方も承認者向けの描画も変わり、`define` が「表に置ける名前」であることの意味が薄くなる。足したのは連立の側だけで、表の見え方は何も動かない。
### 15.128 並びの別名が、生成物の局所変数とぶつかっていた(2026-09-22)
**きっかけ**:§15.125 の射影と `elements` を同じ規則で使ったらどうなるかを確かめた。組み合わせは通ったが、その途中で別のものが出た。**`elements 候補(rows)` と書くと、生成した TypeScript と JavaScript が構文として読めない。**
```
SyntaxError: Identifier 'rows' has already been declared
const rows = trace.map(...)
```
**見立て**:生成器には局所変数の名前を規則の名前とぶつけないための `temp` がある(§15.33)。それが見ていたのは入力・出力・グループ・導出・定義・集約・表の出力列で、**`elements` の名前とそのフィールドを見ていなかった**。並びは生成された全部の関数の引数で、一要素のフィールドは繰り返しの中の局所変数なので、どちらも取ってはいけない名前である。
コーパスの並びは `明細(lines)`・`運賃行(fee_rows)`・`候補(candidates)` で、`rows` と名付けたものが一つも無い。だから**誰も走らせたことがなかった**。§15.88 で `optional` が半分の言語で組めなかったのと同じ形——コーパスに無い形は、誰も走らせていない形である。
**決定**:`temp` が避ける名前に `elements` の名前とフィールド、それと `shape` の名前(射影関数の引数。§15.125)を足す。
**測った**:生成器が `temp` に頼む 22 の名前を、一つずつ並びの別名にして当てた(`tests/shapes.rs`)。`rows` は `rulec test` で十二言語まで通し、残りは TypeScript と JavaScript のランナーが走ることを見る——ぶつかると「読めないモジュール」になるので、走れば十分である。直す前に同じ試験を当てると落ちる。
**直したあとに残る形**:`temp` が見る一覧は手で書いてある。生成器が新しい局所変数を足したとき、その名前を一覧に足し忘れれば同じことが起きる。掃く側の名前の一覧を試験の中に手で持ったのはそのためで、生成器から読み出すと「名乗らなくなった瞬間に緑になる」試験になる。
### 15.129 記録の無い差分で、入力を作れなかった組み合わせ(2026-09-22)
**きっかけ**:§15.126 の「捨てたもの」に「`rulec diff` の入力を作れなかった組み合わせに同じ消去を当てる案。見ているのが行の対ではなく一つの組み合わせで、形が違う」と書いた。形が違うのは確かだが、**連立は同じ**である。行の対なら「両方の行のセル」が条件で、組み合わせなら「各列の座標」が条件で、残りは全部同じ——導出の定義式、宣言した範囲、`constraint`。
**見立て**:見えていなかったものが二つあった。一つめは §15.126 とまったく同じで、入力を共有する二つの導出が独立に動いて見えるため、**誰も送れない組み合わせ**が「答えを出せなかった組み合わせ」として残る。二つめは**点を組み立てる側**で、`realize` は導出ごとに目標の真ん中へ向かって入力を一つずつ動かすので、**二つの目標の交わりにしか点が無い**ときに届かない。`残高A > 1000円` かつ `残高B < 3980円` は `合計` が (1000円, 3980円) のときだけ成り立つが、`残高A` の真ん中は 500500円、`残高B` の真ん中は −98010円 で、どちらを狙っても外れる。
どちらも「入力を作れなかった組み合わせ」に落ち、その数が 0 でないと `total`——**「報告した範囲の外では、二つの版は同じ答えを返す」という主張**——が取り下げられる。承認する人が一番見たい一行が、そこで消える。
**決定**:二つとも消去で決める。
1. **起きない組み合わせを落とす**。座標を条件として連立に入れ、充足不能なら「誰も送れない」として、形で弾かれたものと同じように数えず通り過ぎる。**両方の版で起きないときだけ**である——片方でしか起きないなら、それは受け付ける入力が変わったということで、`domain` が別に言う。
2. **点を解いて出す**。消去は解も出せる。変数を順に消しながら消す前の系を積んでおき、逆順に値を入れていく——その時点で残りの変数には値が入っているので、いま入れる変数の上と下はただの数になる。**整数を優先する**(この道具が量化する値は、どれも自分の単位で整数である)。`realize` は真ん中を狙って外したとき、ここへ落ちる。
**解いて出した点は信用しない。** 返ってきた点は、真ん中を狙って当てた答えとまったく同じように、全部の座標へ突き合わせる(`finish`)。合わなければ「入力を作れなかった組み合わせ」に戻るだけで、算術を根拠に何かを通すことはしない。
**測った**:`クーポン併用.rule`(入力を共有する二つの導出)の出力を一つ動かして差分を取った。
| | 前 | 後 |
|---|---|---|
| 入力を作れなかった組み合わせ | 38 | **0** |
| 起きうる組み合わせ | 61 | 63 |
| 同じ | 34 | 36 |
| 違う | 27 | 27 |
| `total`(外について言い切れるか) | **false** | **true** |
38 のうち 36 は誰も送れない組み合わせで(消去がそう決めた)、2 は本物だった——真ん中を狙って届かなかっただけで、解いたら点があり、二つの版はそこで同じ答えを返した。**報告する差は一つも動いていない**(違う 27 はそのまま)。動いたのは、言い切れるかどうかだけである。
**共有した部品**:§15.126 で `region.rs` の中に書いた「規則が名前について言っていること」の組み立て(導出の定義式・範囲・`constraint`)を `fourier.rs` の `ground` に出した。行の対も組み合わせも、そこに自分の条件を足すだけになる。真偽の定義を一つの値に決めたときの読み方(§15.127)も `pinned` として共有している。二つの呼び出し側が別々のコピーを持つと、片方だけ直る日が来る。
**正直な限界**:§15.126 と同じ三つ(有理数の上で解く、不等式 400 本の上限、単位をまたぐ連立)に、一つ増える。**解が有理数にしかない組み合わせ**は、整数の点を返せないので入力を作れないままである。そこは「点が無い」ではなく「点を見つけられなかった」なので、組み合わせは消さずに残す——数えるほうへ倒している。
### 15.130 言い換えの表を、残りの三語に広げる(2026-09-22)
**きっかけ**:§15.123 が置き換えたのは四語(箱・篩・証人・升目)で、同じ理由で浮く語がほかにも残っていた。そして §15.124〜§15.129 を書くあいだに、四語のうち三つが戻り、`道` が新しく一つ増えた。表が四行で止まっていたことと、書く側が表を見ないことの、両方が原因である。
**戻していた分**:`升目`(11 箇所)、`篩`、`証人`。どれも §15.123 の表にあるものをそのまま書いた。`rulec diff` の出力はもともと「入力の組み合わせ 7,050 通り」「入力を作れず 0」と書いていたので、設計記録だけが別の語彙になっていた——§15.123 が「語彙が二重だと、公開の文面を書くたびに訳し直すことになる」と書いた、その状態である。
**新しく作っていた分**:`道`(12 箇所)。オブジェクトの中の path のことで、診断の文面、台帳、`rulec api` の説明、生成コードの doc コメント、日本語のページ、変異のコメントまで届いていた。`糊`(glue)も同じで、しかも診断の一つは「手書きのグルー」と書いていて、同じものが二つの呼び方をしていた。
**表を三行足す**:
| 前 | 後 |
|---|---|
| 道(object の中の path) | パス |
| 目録(`rulec api` の inventory) | 一覧(ZIP のものは目次) |
| 雛形(`--template` が出すもの) | テンプレート |
| 欄(record・message・schema・要素の field) | フィールド |
| 糊(glue) | つなぐコード |
**`欄` は 109 箇所あって、一律には置き換えられなかった。** 英語側を見ると `field` と `column` の両方に当たっていて、`examples` の見出しやコピーの列は `column` である。そこは `列` にした。そして**日本語として自然に立っている用法は残した**——`空欄`、`入力欄`(フォーム)、`税額の欄`(法令の別表の言い方)、ページの `試す欄`。置き換えた 110 行は、一行ずつ読み直した(§15.123 が一括置換で壊した経験のとおり、`(2) 列を歩く規則には、フィールドが一件ぶんの表` のような意味の通らない言い回しが出る)。
**同じ語が一文に二つの意味で出ていたもの**も直した。`from` の形を説明する診断が「形は `from <形>.<欄>…` です」で、前の「形」は行の書き方、`<形>` は `shape` で宣言した名前だった。`<shape の名前>` にした。`目録` を `一覧` にしたら、今度は「生成・実行・一覧・テスト」の `一覧` と「手書きの一覧が七つ」の `一覧` がぶつかったので、前者は `api` と書いた。
**残る問題**:表は手で書いてあり、書く側がそれを見ないと同じことが起きる。§15.123 のあと三語が戻ったのがその実例である。機械で止める案(禁止語の一覧を持つテスト)は採らない——語そのものを論じている §15.15・§15.123・この節が必ず引っかかり、例外の一覧を持った瞬間に、例外を足すほうが直すより楽になる。
### 15.131 同梱の wasm が古いまま通っていた(2026-09-23)
**きっかけ**:押す前に、版を上げる支度として配る物を一通り見た。`website/docs/playground/rulec.wasm` は committed の build product で、ファイルの先頭にこう書いてある——「古いものはページが黙って昔の答えを返すのではなく、落ちる試験になる」。**その日の変更で古くなっていて、試験は通っていた。**
同梱の wasm は `クーポン併用.rule` に W114 を出し、生成する Python に実行時のガードを書いていた。バイナリのほうは §15.126 の消去で `ok` と答える。**一日ぶん、ページだけが昔の検査を持っていた。**
**見立て**:`tests/wasm.rs` が流していたのは 3 本(overview の英日と `送料.rule`)で、どれもこの日に変わった検査を踏まない。**踏まない題材は、古い物と新しい物を区別しない。** 版の文字列は見ていたが、版は上げていなかったので一致していた。コーパスに無い形は誰も走らせていない(§15.88・§15.128)のと同じことが、ここでは「題材に無い形」で起きた。
**決定**:題材に二本足す。`クーポン併用.rule`(消去が決める。§15.126)と `m_w114.rule`(決まらずに残る。§15.127)。どちらも**隣にファイルが要らない**——ページには読む先が無いので、そこが選べる条件である。E119・E120・E121 はコピーや契約が要るので、ここでは踏めない。
足した状態で古い wasm に当てると、`check` の文面と生成した Python の両方が落ちる。作り直して通した。
**残る限界**:この試験が見ているのは、題材が踏む範囲だけである。踏まない検査を変えた日には、また同じことが起きうる。コーパス全部を流せば閉じるが、`check`・`gen`・`doc` を 46 本 × 2 言語ぶん文字列で突き合わせることになる。**版を上げる前に作り直す**を手順に書くほうが先で、それは §15.48 が「build product は作り直して固定する」と書いたことの繰り返しである。
### 15.132 契約が通す値を、入力の宣言と突き合わせる(2026-09-23)
**きっかけ**:§15.125 で、入力が呼び出し側のオブジェクトのどこから来るかを書けるようになった。パスは契約に照らしているが、契約はもう一つのことを言っている——そこから**どんな値が来るか**である。Protovalidate の `gte`・`lte`、JSON Schema の `minimum`・`maxItems`・`enum`・`required`。入力の側も、`range` と列挙で同じことを宣言している。二つは別々の人が別々のファイルに書いていて、食い違っても誰も何も言わない。食い違えば、API 自身が受け付けた要求を、生成コードが入口でエラーにする。
**まず測った**:コーパスの `注文の送料.rule` がちょうどその形だった。規則は明細数を 1〜50 件と宣言し、契約の `lines` には件数の制限が無い。`rulec check` は `ok` を返し、生成した Python に明細 0 件の注文を渡すと `RuleInputError: 明細数 is out of range: 0`、51 件でも同じだった。契約の上では正しい注文である。
**決定**:`check` が、`from` で読む値について、契約の検証が通す値と入力の宣言を突き合わせる。診断は二つ。
- **E122**:契約は通すのに、入力が受け付けない値がある。比べるのは数の範囲、並びの件数、列挙の値、JSON Schema の `required`。JSON の `witness` にはその値を入れ、`fix.text` には契約に書く注釈そのもの(Protovalidate のオプションか JSON Schema のキーワード)を入れる。`fix.kind` に `narrow_contract` を足した——`.rule` の外を直す、初めての種類である。
- **W123**:行のセルが `from` で読む入力を試していて、そのセルが受け付ける値を、契約が一つも通さない。その行に当たる要求やメッセージは来ない。
表の検査は相変わらず一つも動かない。比べているのは契約と入力の宣言であって、表ではない。§15.125 の「検査には触れない」は、表の検査についての約束としてそのまま残る。
**向きで扱いを分けた。** 契約のほうが広いずれは、実行時に落ちる。規則のほうが広いずれは、それだけでは何も起きない。規則の範囲が契約より広いのは、契約が広がる日に備えて残していることもある。だから後者は入力ごとには言わず、**広い部分でしか当たらない行**があるときだけ W123 で言う。
**読まない規則は、無いものとして読む。** CEL の式、predefined の規則、文字列の `pattern`。読まなかった規則は値を狭めることしかできないので、無いものとして読むと、契約は実際より広く見える。要らないところで E122 を言うことはあっても、黙って通すことはない。W123 も同じ向きに倒れる——広く読んだ契約でも当たらない行は、本当の契約でも当たらない。読まなかった規則は名前を E122 の注に出す。
**proto3 のゼロ値。** `optional` の付かない数のフィールドは、入れ忘れると 0 として届き、Protovalidate は既定でその 0 にも規則を当てる。だから規則の無い数のフィールドは 0 を通し、`gte: 1` のあるフィールドは通さない。`IGNORE_IF_ZERO_VALUE`(と前の二つの名前)があれば、0 は規則と無関係に通る。いちばん多く引っかかるのはここだと見ている。Protovalidate が `gt` を `lt` より大きく書いた範囲を「その外側」と読む決まりも、そのとおりに読む。
**省略できるフィールドは `T?` で受ける。** 生成した取り出し関数は、オブジェクトに添字でじかに入っていたので、契約が省略を許すフィールドを受け取る手段が無かった。入力が `T?` のときだけ、無いフィールドを none として読むように五言語を変えた(Python は `_dig`、TypeScript と JavaScript は `?.`、Ruby は `dig`、PHP は `??`)。途中のオブジェクトが無いときも none になることは、五言語を実際に走らせて確かめた(`tests/projection.rs`)。必須の入力が省略できるフィールドを読むのは E122 で、ヒントは「契約で `required` にするか、入力を `T?` にするか」の二つを出す。
**走らせて見つけて直したもの**:三つ。(1) 文法の説明どおりの `shape order(order) = proto "…" shop.v1.Order` が E047 でエラーになっていた。パーサがメッセージ名を一語しか読まず、`.v1.Order` を余りの語とみなしていた。`.proto` の `shape` を通す試験が一本も無く、文書の例が一度も走っていなかった。(2) `.proto` の読み手は、フィールドのオプションの中に `[1, 2]` のようなリストがあると、内側の `]` でオプションが終わったとみなし、続く `}` でメッセージまで閉じていた。その下のフィールドが全部消える。括弧の深さで対応を取るように直した。(3) 読み手は文字列の中身を捨てていたので、`in: ["honshu"]` が読めなかった。文字列は番号に置き換えて取っておき、オプションを読むときに戻す。
**確かめたこと**:`.proto` と JSON Schema の両方で、注釈の二通りの書き方、入れ子のリスト、`;` を含む文字列、`IGNORE_IF_ZERO_VALUE`、`cel` の読み残し、`exclusiveMinimum`、`where` で絞った件数、要素の `where` のフィールドの `required`。コーパスの契約には `minItems: 1, maxItems: 50` を足した(変異のコピーも)。変異を二本足した。`m_e122` は入力を 40 件までに狭め、`m_w123` は入力を 80 件まで広げて 60 件を超える行を書いたものである。
**捨てたもの**:
- 契約の検証を、規則の**宣言した範囲として取り込む**案。§15.125 の約束を破るうえ、契約を書き換えた人が、知らないうちに完全性の証明の前提を動かせることになる。突き合わせなら、どちらが動いても CI で止まる。
- 規則のほうが広いことを、入力ごとに警告する案。契約が広がる日に備えた範囲はよくあり、毎回出る警告は読み飛ばされるようになる。
- 日付の範囲の突き合わせ。JSON Schema には日付の上下限を書く標準のキーワードが無い。規則の日付の範囲はたいてい規則の効く期間で、契約が決めることでもない。
- 入力どうしの関係(`constraint`)の突き合わせ。Protovalidate ならメッセージの CEL で書けるが、CEL は読まない。JSON Schema では書けない。
- 生成する Connect の `.proto` に `buf.validate` の注釈を書くこと。規則の前提が契約に載る代わりに、生成したモジュールが `buf.build/bufbuild/protovalidate` に依存し、`rulec test` がそれを取りに通信することになる。§15.112 の「依存はサービスの一枚に閉じる」とぶつかるので、要るとはっきりするまで入れない。
**正直な限界**:`.proto` の側は、フィールドが省略されうるか(`optional` を付けたフィールド、メッセージのフィールド)を見ていない。そもそも protojson は既定値のフィールドを出さず、既定のフィールド名も lowerCamelCase だが、取り出し関数は `.proto` に書いた名前でじかに読む。`.proto` の `shape` は、呼び出し側が `.proto` の名前のまま、既定値も出した JSON を渡す前提で動いている。これは §15.125 からの前提で、今回は変えていない。率の入力のうち、契約が小数(`double`・`number`)で運ぶものは比べていない。
### 15.133 `.proto` の形を、protojson のとおりに読む(2026-09-23)
**きっかけ**:§15.132 の限界に残した前提がある。`.proto` の `shape` から作る取り出し関数は、`.proto` に書いたフィールド名でじかに読み、既定値のフィールドも JSON に入っている前提で動いていた。protojson の既定の出力はそうなっていない。名前は lowerCamelCase(`zone_code` は `zoneCode`)で、既定値のフィールドは出さない(0 の数、空の文字列、false、空の `repeated`、セットされていないメッセージと `optional`)。int64 は文字列で書く。Connect の JSON も、Kafka に JSON で流すメッセージも、たいていこの形で来る。そのまま渡すと、名前に `_` を含むフィールドが一つでもあれば KeyError、既定値のフィールドが一つでもあればまた KeyError だった。
**決定**:取り出し関数は、`.proto` の形を protojson の JSON として読む。
- **名前**:各段を JSON 名(`json_name` があればそれ、無ければ lowerCamelCase)で探し、無ければ `.proto` に書いた名前で探す。protojson の読み手も両方を受け付けるので、`UseProtoNames` で書く送り手の JSON もそのまま読める。
- **無いフィールド**:proto が読むとおりに読む。数は 0、文字列は ""、`bool` は false、`repeated` は 0 件、列挙は番号 0 の値、メッセージは中のフィールドが全部省かれたもの。`T?` の入力は、無いことと既定値を区別できるフィールド(メッセージのフィールドと `optional` のフィールド)が無ければ none として読む。区別できないフィールドは、無いことが既定値そのものなので既定値のまま読む。
- **int64**:文字列で届いたものを数として読む。`where` の比較でも同じで、TypeScript と JavaScript では BigInt で比べる(`Number` にすると 2^53 を超えたところで丸まる)。
- **列挙**:`where` は値の名前の文字列として比べる。番号で書いた `where` は E120 になる。`bytes` とメッセージのフィールドも E120 で、比べられる値ではない。
- 五言語とも、段ごとの二つの名前と、無いときに読む値を、`_proto` という一つの補助関数に渡す。
**既定値も、契約を通った値として突き合わせる。** 既定値が届くようになったので、§15.132 の突き合わせにそれを入れた。`required` の無いメッセージのフィールドと、`optional` のフィールドは省略でき、そのとき Protovalidate は中を何も検証しない。だから中の値は、フィールドの規則にかかわらず既定値のまま届く。入力がそれを受け付けなければ E122 で、`fix.text` は `[(buf.validate.field).required = true]`。フィールドの規則が 0 を通すことと、メッセージが省略できることは、直す場所が違うので別々の E122 にした。`where` で絞った件数は、もともと 0 件がありうる(§15.132)。メッセージを `required` にしても変わらないので、重ねては言わない。
**日付は "" を見る。** §15.132 は日付の突き合わせを捨てたが、捨てた理由は範囲の話(契約に日付の上下限を書く標準が無い)だった。"" は範囲の外にあるのではなく、日付でない値である。しかも protojson は空の文字列を出さないので、日付を運ぶ proto3 の文字列フィールドは、送り手が入れ忘れると "" として届き、取り出し関数はそこで落ちる。そこで `.proto` の文字列から読む日付は、"" を通すかどうかだけを見る。"" を除くのは、無いことと既定値を区別できないフィールドの `required`、`min_len`・`len`(とバイトで数える二つ)、`in`・`const`・`not_in`。`optional` のフィールドの `required` は「セットされていること」しか求めず、"" をセットすれば通るので、そこでの `fix.text` は `string.min_len = 1` にした。
**入力にならない型は、型の名前を出す。** `.proto` の列挙や `bytes` のフィールドを入力へじかに読むと、パスの終わりで型が分からず、E121 の「その先にフィールドはありません」になっていた。パスは届いているので、これは型の食い違いである。E120 にして、契約の側の型(`enum Zone`、`bytes`)を出すようにした。列挙のときは、値の名前で運ばれることと、`where` なら比べられることを注に出す。
**走らせて見つけて直したもの**:三つ。(1) `where` に書いた日付が、引用符なしで生成されていた。`2026-01-01` は Python では構文エラー(整数に先頭の 0 を書けない)、ほかの言語では引き算になる。§15.125 から、`where` に日付を書いた規則の取り出し関数は一度も走っていなかった。(2) `where` が `bytes` やメッセージのフィールドを見ていても検査を通り、それをそのまま比べるコードが生成されていた。(3) 列挙の値の名前が契約の値と一つも重ならないとき、E122 の `fix.text` が `in: []` だった。空の `in` は規則が無いのと同じで、直しにならない。`fix.kind` を `none` にし、契約が列挙の別名(`honshu`)で綴っているときは、運ばれるのは値の名前(`本州`)のほうだと注で言う。
**確かめたこと**:五言語を実際に走らせる試験を三本置いた(`tests/projection.rs`)。protojson の既定の出力、`.proto` の名前で既定値も書き出した JSON、全部が既定値の注文、要素の `false` が省かれた明細、数で書いた int64、`where` での int64 の比較と列挙の既定値と空の並びへの `all`、`where` の日付。契約の側は `tests/contract.rs` に置いた。`.proto` の側の E122 には、それまで試験が一本も無かった。台帳の例と変異は JSON Schema の側だけを通っていた。
**捨てたもの**:
- JSON 名だけ、または `.proto` の名前だけで読む案。どちらか一方にすると、既定の書き手か `UseProtoNames` の書き手のどちらかで落ちる。protojson の読み手と同じく、両方を受け付ける。
- 必須の入力でも、無いフィールドを none として読む案。無いことと既定値を区別できないフィールドでは、二つは同じものである。none にすると、0 を送ったメッセージと送らなかったメッセージで答えが変わる。
- `T?` の日付や列挙で、"" を none とみなす案。"" を送ったのか省いたのかは区別できない。区別したいなら `.proto` で `optional` を付けるのが proto のやり方で、ヒントもそれを勧める。
- `pattern` を読むこと。正規表現が "" に当たるかを決めるには、正規表現を実装することになる。読まない規則は無いものとして読むので、`pattern` だけで "" を除いている契約には要らない E122 が出る。`required = true` を足せば消える。
**正直な限界**:列挙のフィールドは `where` でしか読めない。protojson が運ぶのは `.proto` の値の名前(`ZONE_HONSHU`)で、規則の列挙の値とは綴りが違う。その対応は `import proto`(§15.59)が別名で決めているが、それを取り出し関数に持ち込むのは別の仕事として残した。列挙を番号で書く送り手(`UseEnumNumbers`)は読まない。`where` で日付を比べるとき、`.proto` の文字列フィールドが省かれていれば "" と比べることになり、"" はどの日付よりも前に並ぶ。`google.protobuf.Timestamp` のような、ファイルに無い型は読まない。
### 15.134 日本語の入った表の縦をそろえる(2026-09-23)
**きっかけ**:`rulec fmt` がそろえたはずの表が、日本語が入るとずれて見える。サイトに載せた `.rule` でも、`.rule` ファイルでも、列を区切る `|` の縦が行ごとに違う。
**見立て**:ずれの出どころは三つあった。
- **サイトのフォント。** テーマはコードを JetBrains Mono で組み、JetBrains Mono は日本語の字形を持たない。ブラウザは日本語だけを別のフォントから借り、その字は 1em の幅、JetBrains Mono の英数字は 0.6em なので、日本語 1 文字は英数字 1.67 文字ぶんにしかならない。`rulec fmt` は日本語を 2 桁と数えてそろえているので、日本語のセルがあるたびに、その先の `|` が左へずれる。さらにテーマは本文の字間を 0.01em 詰めていて、コードもそれを受け継いでいた。字間は 1 文字ごとに同じだけ詰まるので、日本語 1 文字は英数字 2 文字より 0.15px 広くなる。フォントを替えたあとも 1px ほどのずれが残ったのは、これだった。
- **手でそろえた文書の抜粋。** 文書の `rule` ブロックの表のいくつかは、表示幅ではなく文字数でそろえてあった。台帳の最小再現(`rulec explain` が出し、`docs/codes.md` に載る)はそもそも整えていなかった。`rulec import` が書く下書きと、変異の生成器が足した行も同じだった。
- **℃。** Unicode ではこの字の幅は「曖昧」で、`rulec fmt` は 1 桁と数えていた。しかし手元で確かめた等幅フォントのうち ℃ を持つものは、どれも 2 桁の幅で描く(欧文の Noto Sans Mono まで)。1 桁と数えてそろえた表は、どのフォントで読んでもずれる。
**決定**:
- `rulec fmt` は ℃ と ℉ を 2 桁と数える。ほかの「曖昧」な字は 1 桁のまま。コーパスの表の中に出てくる曖昧な字は ℃ だけだった。
- サイトはコードを M PLUS 1 Code で組む。英数字が 0.5em、日本語が 1em で、ちょうど 1 対 2 になる。M PLUS 1 Code に無い字(℃、全角の句読点)は BIZ UDGothic から借りる。こちらも 1 対 2 である。コードの字間は 0 に戻した。プレイグラウンドの入力欄も同じフォントになる。
- 文書の抜粋と台帳の最小再現を `rulec fmt` のとおりにそろえ、`tests/docs.rs` が、文書の `rule` ブロックの表を `rulec fmt` の出力と突き合わせる。`rulec import` の下書きは `fmt` を通してから書く。変異も、生成器の最後で `fmt` を通す。
**確かめたこと**:組み上げたサイトをヘッドレスの Chrome で開き、表の行ごとに `|` の位置を測った。例のページは 578 行でずれが 0、ツアーと文法と台帳の表も 0 だった。前は日本語 1 文字ごとに、英数字 0.2 文字ぶんずつずれていた。
**捨てたもの**:
- 曖昧な字をすべて 2 桁と数える案。日本語の端末の設定ではそう描かれることもあるが、欧文の等幅フォントは `×` や `§` や罫線を 1 桁で描き、M PLUS 1 Code も同じである。どのフォントも 2 桁で描くのは ℃ と ℉ だけだった。
- 表に日本語が含まれるかどうかで、曖昧な字の幅を変える案(Unicode が勧める「文脈で決める」)。同じ字の幅がファイルによって変わると、人が `fmt` の出力を予想できない。
- 日本語の字を JavaScript で 2 桁の箱に入れ、どんなフォントでもそろえる案。入力欄(`textarea`)の中では使えず、プレイグラウンドだけがずれたまま残る。
**正直な限界**:GitHub の画面と、日本語が英数字のちょうど 2 倍にならないフォントのエディタでは、ずれは直らない。GitHub はコードを自前のフォントの並びで組み、そこに日本語は無いので、借りた字で描く。ファイルの側でできることは無い。エディタでは、日本語が英数字の 2 倍になるフォント(BIZ UDGothic、M PLUS 1 Code、UDEV Gothic、HackGen など)を使えばそろう。曖昧な字を 1 桁で描く端末では、℃ のある行だけが 1 桁ずれる。
### 15.135 確かめ方の図の件数を、描くときに数える(2026-09-23)
**きっかけ**:サイトの「確かめ方」の図は、わざと壊した規則を 83 本、全言語で走る規則を 33 本と書いていた。実物は 94 本と 46 本である。ページの表の件数は `tests/website.rs` が実物に突き合わせていたが、図の件数は手で書いた文字列で、誰も見ていなかった。
**決定**:`website/tools/make_assurance.py` が、描くときに `tests/mutants` と `tests/corpus` の `.rule` を数える。変異を足しても図を描き直さなければ、図が古いという既存の試験が落ちる。`--verify` は、図の件数とページの表(8 の行)の件数が同じであることも確かめる。英語のページが書き下ろしの規則を 9 本と書いていた(実物は 10 本)ので、それも直して試験に入れた。
### 15.136 射影を、例と説明に載せる(2026-09-23)
**きっかけ**:`shape` と `from` を説明していたのは、文法(§3.3)と生成物の説明と台帳だけだった。サイトの自前のページにあったのは、ツアーの表の一行と、検査のページの二段落だけである。例のページにも、射影を使う規則は一本も無かった。契約と規則を突き合わせるのはこの道具の中心の使い方の一つなのに、読む人はその書き方にたどり着けなかった。
**決定**:
- 例のページに二例を足した。コーパスの `注文の送料.rule`(JSON Schema)と、新しく書いた `出荷の送料.rule`(`.proto` と Protovalidate)である。どちらも、規則の下に、規則が読む契約のファイルをそのまま載せる。ページはスクリプトが契約のファイルを読んで作るので、ページに載る契約と `check` が読む契約はずれない。
- ツアーに「入力を、呼び出し側のオブジェクトから取る」の節を、生成のページに「呼び出し側のオブジェクトのまま呼ぶ」の節を足した。載せた呼び出し例は、先に走らせて答えを確かめた。
- `出荷の送料.rule` は、コーパスで初めての `.proto` の射影である。`required` の入れ子のメッセージ、`repeated` の件数、`optional` の文字列を `T?` で受ける入力、列挙のフィールドへの `where` を、一本で通る。`tests/projection.rs` がその取り出し関数を protojson の要求で五言語走らせ、規則自身の例と同じ答えになることを確かめる。コーパスは 47 本(書き下ろし 11 本)になった。
**走らせて見つけて直したもの**:生成した Ruby の型定義(`.rbs`)に、§15.133 で足した補助関数 `_proto` が宣言されていなかった。コーパスに `.proto` の射影が無かったので、steep で型を確かめる試験(`tests/threeway.rs`)は、一度もそこを通っていなかった。宣言を足し、空の配列は `[]` ではなく `Array.new` と書くようにした。steep は注釈の無い空の配列を警告し、試験は警告も通さない。
**Kani の記録を取り直した**(`experiments/kani/report.txt`)。ハーネスは新しい規則の 5 本を足して 111 本、総計 175 秒だった。
### 15.137 立場ごとの使い方に、契約の場面を足す(2026-09-23)
**きっかけ**:契約との突き合わせ(§15.132、§15.133)は、立場ごとの使い方のどの場面にも出てこなかった。四つの場面はどれも、ルールを書き、証明し、コードにする流れで、すでにある API の契約の側を、規則に照らして検査する使い方が無かった。そこでは契約が主役で、コードは生成しなくてよい。
**決定**:五つめの場面「API の契約とルールを突き合わせる」を、図つきで足した。要求が規則の答えにたどり着くまでのつなぎ目を三つ(要求と契約、契約と規則の入力、規則の入力と表の行)に分け、rulec が受け持つのは下の二つで、上の一つはいまある検証の道具(Protovalidate、OpenAPI の検証)のままだと書いた。`buf breaking` がワイヤの互換を見るのに対して、こちらは契約の変更で業務の判断が壊れないかを見る。図は他の場面と同じ一文の形で、CI の一場面だけを描き、実行時の検証とは矢印でつながない。載せた E122 の出力は、載せるとおりのコマンドで取った。
### 15.138 先行例を挙げ、新しいと言う範囲を二つに絞る(2026-09-23)
**きっかけ**:サイトの「似たものは何があるか」は、ルールエンジンを「実行時に評価する」とだけ書き、Catala の行には静的な検査が一つも出てこなかった。README の「About the design」は、DMN に無いもの(単位、丸め、複数言語への生成、過去の記録への再生)を、この道具の違いとして並べていた。契約の場面(§15.137)は、契約の変更で業務の判断が壊れないかを見る道具は「ありません」と言い切っていた。どれも、隣を調べずに書いた文である。
**調べたこと**(どれも公開資料から):
- 表に抜けと重なりが無いことの証明は、1990 年代からある。SCR(米海軍研究所)の検査器は条件表の欠けと重なりを、それを起こす状態つきで出す。PVS は表ごとに二つの証明の義務を立て、成り立たない表には意味を与えない。
- いまの製品も、編集しているそばで見る。IBM ODM、Pega、SAP BRFplus、Corticon、Drools の DMN。BRFplus は、通らない表を有効にさせない設定ができる。IBM ODM は列の定義域を XML Schema から取れるので、そこに足された列挙の値は、エンジンの中で表の抜けとして見える。
- Catala は Z3 のプラグインで、定義が空になる場合と例外が重なる場合を静的に調べ、それを起こす場合を返す。結果は警告で、ビルドは止めない。
- LF-ET(商用)は、完全性・冗長・矛盾を検査した決定表から多くの言語のコードを出し、実行時にエンジンは要らない。条件は対象の言語で書いた式に名前を付けたもので、検査はその真偽の組み合わせについてである。
- 改定が過去の記録をどう動かすかは、ルールエンジンのシミュレーション(IBM ODM、Pega)が前から出している。
- 見つからなかったものが二つある。一つは、検査が証拠を渡し、コードを共有しない別のプログラムが再検査し、その検査が主張を含意することまで証明したもの。結果と一緒に証拠を出すアルゴリズム(certifying algorithm)の考え方はあり、SAT の証明を検証済みの検査器で確かめる形がそれにあたる。Cedar はポリシーの分析器そのものを Lean で確かめていて、こちらは証拠を渡す形ではない。決定表に当てたものが無かった。もう一つは、API の契約の変更を、その検証が書く範囲・件数・必須・列挙の値まで含めて、契約の側の CI で、検査済みの表の入力の宣言に照らして判定する道具。`buf breaking` はカスタムオプションを意図して対象外にし、列挙に値が増えることを壊れる変更として扱わない。oasdiff も、要求の列挙に値が増えることを壊れない変更とする。ODM のように定義域を XML Schema から取るエンジンは列挙の追加を見るが、エンジンの中での話で、範囲や必須には及ばない。
**決定**:新しいと言うのは、この二つだけにする。ほかは組み合わせと書き、先行例を挙げる。サイトの比較の表には、形式手法の表、LF-ET、API の契約の検査の三行を足し、ルールエンジンと Catala の行に静的な検査を書き足した。README の「About the design」も同じ線にそろえた。契約の場面は「ありません」をやめ、`buf breaking` と oasdiff が何を見ないかを書いたうえで、調べた範囲では見つからなかったと書いた。
**ついでに直したもの**:
- 規則集の数え方。README とサイトは「47 本のうち 36 本は実物からの転記、11 本は書き下ろし」と言っていたが、11 は §15.93 以降に言語の隅のために足した分しか数えておらず、それより前のスケッチと例(設計文書のスケッチ、§15.67・§15.69 のスケッチ、会員特典や決済手数料のような例)を転記に入れていた。規則の description が転記元の出典を挙げているものを転記と数え直すと 21 本、残りの 26 本がスケッチと例である(τ²-bench の航空規約から転記した 3 本は、公開されている文書からの転記として数えた)。`tests/readme.rs` と `tests/website.rs` の定数も 26 にした。英語のスケッチは二本ではなく三本だった。
- `--format json` の範囲。「どのコマンドにも」と書いていたが、`source` と `import` には無い。指摘を返すコマンドに限って言い、二つを挙げた。
- リリースのバイナリ。「macOS も Linux も静的」と書いていたが、macOS 版は libSystem に動的にリンクしている(macOS ではそれしかできない)。Linux 版が静的で、macOS 版がリンクするのはシステムのライブラリだけ、と書き直した。
- 「全言語で実行」。NumPy と SQL は、設計どおり生成しない規則がある。「生成できるすべての言語で」にした。
- README の `cargo test` の件数は、何にも縛られないまま古くなっていたので消した。
**捨てたもの**:
- 比較の表を消す案。隣を正直に並べること自体に意味がある。言い過ぎたのは表があったからではなく、行が足りなかったからである。
- 「唯一」と書く案。言えるのは調べた範囲までで、表の冒頭にもともとそう書いてある。
- LF-ET の検査の読みを断定する案。公開資料から読んだものなので、表の冒頭の注記の下に置いた。
### 15.139 消去が反駁を渡し、桁あふれで答えを誤らない。刻みの違う制約の門(2026-09-23)
**きっかけ**:消去で決めた行の組を、証明書に載せたい(§15.141)。載せられるのは結論ではなく根拠で、消去の根拠は「どの不等式にいくつを掛けて足したら矛盾したか」という乗数の組である。いまの `fourier::unsat` は真偽しか返さない。乗数を返すように手を入れる前に、消去とそのまわりを読み直すと、欠陥が六つ見つかった。
**決定**:
1. **`refute` が乗数を返す。** 消去で作る不等式はどれも、手元の二本をそれぞれ正の数倍して足したものなので、その倍率を持ち回れば、最初に偽になった行の乗数がそのまま反駁になる。返す前に `farkas_holds` に通す。やることは足し算だけで、乗数がどれも 0 以上か、変数が全部消えるか、残る定数が偽か(正か、0 で厳密な不等式が加わっているか)を見る。通らない乗数は返さない。`unsat` はこの上に乗るので、呼び出し側は変わらない。不等式には由来(どの行のどのセル、どの範囲、どの `derive`、どの `constraint`、どの定義)を持たせた。
2. **128 ビットに収まらない計算は、答えではなく降参にする。** `Rat` の四則は i128 のままで、release ビルドではあふれを黙って巻き戻す。消去は係数を掛け合わせ続けるので、巻き戻った積が「充足不能」を作れば、本物に見える誤りになる。消去と解の組み立ては `checked_*` で計算し、収まらなければ「決められなかった」と答える。一行ごとに、係数が整数のまま最も小さくなる正の数で割る(乗数も同じ数で割る)ので、数はめったに育たない。
3. **系を数の型ごとに分ける。** §15.126 は、列の型が混ざると問いごと諦め、線形でない `derive` が一つあっても諦めていた。どちらも要らなかった。型ごとに作った系は、どれも元の問いの条件を一部落としたものなので、一つでも充足不能なら元も充足不能である。線形でない `derive` は式だけを落とし、名前は範囲の中で自由に動かす。日付の列が一本入っただけで W114 に戻っていた表が、これでまた決まる(`tests/fourier.rs`)。
4. **`solve` が厳密さを落としていた。** 同じ値の上限が二本あって片方だけ厳密なとき、先に見た厳密でないほうを残していた(`x <= 5` と `x < 5` から `x = 5` を出す)。値が等しいときは厳密なほうを取る。下限の側には、等しいときの扱いがそもそも無かった。記録の無い差分(§15.129)は点を必ず確かめ直しているので実害は出ていなかったが、§15.140 で契約の反例を作るのに使う。
5. **`constraint` の門が、刻みの違う二つを整数のまま比べていた。** 両辺は同じ型でも、同じ刻みとは限らない。`rate[step 1%]` と `rate[step 0.1%]` では 50% が 50 と 500 になり、生成コードは 50 ≤ 100 を通して、50% ≤ 10% を受け付けていた。十二の言語と SQL と Kani のハーネスで、両辺を共通の刻みにそろえてから比べる。そろえるのは掛け算なので、その積が int64 に収まることを E108 で証明する。
6. **率の入力の刻みを必須にした(E103)。** 刻みの無い率の入力は 100% を単位に数えていたので、範囲の `10%` は門で `1` になり、1000 が「10% 以下」として通った。リファレンスは「刻みは列のリテラルから決まる」と書いていたが、入力ではそうできない。行を一つ足すだけで、呼び出し側が渡す整数の意味が変わってしまう。計算で出す率は、これまでどおりリテラルから決まる。E103 の説明には、§15.86 で入れた読めない刻みの場合もあわせて書いた。
**走らせた**:`tests/fourier.rs` に型の違う列の一本、`src/fourier.rs` に反駁の乗数、厳密さの運ばれ方、桁あふれ、厳密な上限の四本、`tests/constraint.rs` に門と刻みの無い率の二本を足した。門の一本は、生成した Python に (50, 100) を渡すとエラーになること、(5, 200) と (30, 300) を通すことを確かめる。刻みの無い率の入力は、コーパスと変異には無く、テストに一本あった(`tests/units.rs` の単位の一覧)。そこには `rate[step 1%]` と書いた。記録の無い差分(§15.129)で外について言い切れない規則は、2 本から 1 本になった。抜けたのは `クーポン割引` で、二つの導出列が入力を共有するふるいの盲点に、率の列があるせいで消去が届いていなかった。残るのは `預け荷物料金` だけである。
**見つけて直していないもの**:制約が列でない入力を経て連なるとき(`a <= b` と `b <= x` で、`b` が列でない)、ふるいは制約を一本ずつしか見ないので、制約を破る入力を例に挙げて E105 と E101 を出す。黙って通す向きではなく、誤った誤りのほうである。§15.141 で扱う。
**捨てたもの**:`Rat` の四則をすべて検査つきにする案。消去の外で扱う数は宣言した範囲と刻みから出る小さな数で、型検査の区間計算まで `Option` に変えると差分が大きい割に効かない。桁が実際に育つのは消去だけである。刻みの無い率の入力の刻みを、範囲とセルのリテラルの最小公倍数から決める案(6 の理由)。
### 15.140 契約がフィールドのあいだに置く条件を読む(2026-09-23)
**きっかけ**:§15.132 は、契約と入力を一つのフィールドずつ比べていた。ところが契約は、フィールドどうしの関係も言っている。メッセージに付けた CEL の `this.min_weight <= this.max_weight`、一つしか設定できない `oneof`、JSON Schema の `if`/`then` で書く「速達なら 5kg まで」がそうである。規則の側も、`constraint` と表の行で入力どうしを関係づけている。二つの関係がずれていても、一つずつの比較には見えない。どこまで読めば、見逃さずに言えるか。
**決定**:
1. **契約の条件を一つの式として持つ(`src/relation.rs`)。** 原子は三種類で、整数の一次式と 0 との比較(`≤`、`<`、`=`、`≠`)、文字列がある集合に入るか入らないか、真偽値の値である。それを `and` と `or` で組む。否定は組むときに原子まで下ろし、読めない部分は「読めない」という原子のまま運ぶ。真にするのは式ができあがってからである。先に真にすると、あとの否定がそれを偽に変え、契約を実際より狭く読んでしまう。問いは三つある。一つのフィールドが取りうる値(場合ごとに消去で上下限を出し、和をとる)、条件と行が両立するか(場合ごとの反駁。§15.139 の乗数がそのまま根拠になる)、制約を破る点があるか(整数の点を、分けて解き直して探す)。場合は 256 までで、超えたらフィールドのあいだについては何も言わない。広く読む向きである。
2. **CEL は、読める部分を読む(`src/cel.rs`)。** 読むのは、整数の一次式(定数との積まで)、六つの比較、リテラルの一覧に対する `in`、繰り返しのフィールドの `size()`、`has()`、`!`、`&&`、`||`、`? :` と、文字列を返す規則(`""` で通る)である。除算、`%`、文字列の関数、`all` のようなマクロ、double は読まない。`has()` は、セットしたかどうかを既定値と区別できないフィールドでは「既定値でない」と読む。`optional` や `oneof` の一員のように区別できるフィールドでは、「セットされていなければ既定値」とだけ読む。セットされているかどうかは値に現れないので、値について確かに言えることだけを取った形である。
3. **`.proto` の条件は、Protovalidate の読み方どおりに組む。** 組むのは次のとおり。
- メッセージの CEL(`cel`、`cel_expression`)
- フィールドの規則(範囲、`in`、`not_in`、`const`、`min_len`、`required`)とフィールドの CEL
- `oneof` と `(buf.validate.message).oneof`(どれか一つを除いて既定値)
- 型が保証する範囲(`int32` の上下限など)
セットされていないメッセージの中は検証されないので、そのメッセージの条件には「中が全部既定値」の場合を `or` で足す。`optional` のフィールドと `oneof` の一員の規則にも、同じく既定値の場合を足す。`IGNORE_IF_ZERO_VALUE` と `IGNORE_ALWAYS` もこの形で読む。Protovalidate が廃止した `(buf.validate.message).disabled` は、古い版が「何も検証しない」と読むので、そのメッセージの規則を全部捨てる。`oneof` の一員は、これまで読み飛ばしていたが、いまはメッセージのフィールドとして読む。`optional` のフィールドと同じく、セットしたかどうかを既定値と区別できるものとして扱う。
4. **JSON Schema の条件は、必ずあるフィールドについてだけ組む。** `properties` に書いたキーワードは、そのプロパティが無いときは何を書いても成り立つ。だから、根から `required` がつながっていないフィールドのキーワードは、値の条件として読まない(読めない原子にする)。組み合わせは、`allOf` を `and`、`anyOf` を `or`、`not` を否定、`if`/`then`/`else` を二つの場合として組む。`oneOf` は「ちょうど一つ」だが、`anyOf` として広く読む。`$ref` の横のキーワードは、2019-09 以降と OpenAPI 3.1 の文書でだけ足す(`$schema` と `openapi` で見分ける)。型に null があるフィールドは、値の種類を決めない。
5. **三つの使い道を持たせた。** 一つめ。ほかのフィールドの規則が届いて狭まる分だけ、E122 の比べる範囲が狭まる。フィールドの CEL の `this <= 100` も、メッセージの CEL の `this.w >= 1 && this.w <= 100` も範囲になる。二つめ。同じ契約から読む二つの入力のあいだの `constraint` を、契約が守っていなければ **E123** である。入力の範囲に収まり、制約を破り、契約を通る整数の点を例に出す。`.proto` なら、約束させる `(buf.validate.message).cel` が `fix.text` である。JSON Schema には二つのフィールドを比べる書き方がないので、契約の側の直しは無い。三つめ。二つ以上の入力を試す行が、契約の条件のもとでは一つも通らなければ **W124** である。W123 が出た行、規則自身の範囲と制約で届かない行は除く。ほかの列のセルは見ないので、届かないと言いすぎることはない。
6. **パスの読み方を三つ直した。** JSON Schema の `allOf`・`anyOf`・`oneOf`・`if` の中にだけ書いたプロパティも、パスとして読む(これまでは E121 だった)。`allOf` の中の `required` も、省略できないことの根拠に数える(これまでは要らない E122 が出た)。型に null があるのに入力が省略できないときは E122 にした。生成コードは、省略できる入力なら null を none として読み(五つの言語で確かめてある)、そうでなければ落ちる。
7. **直した欠陥が三つある。**
- `optional` のフィールドに付けた `required` を「0 を通さない」と読んでいた。§15.133 に書いたとおり、このときの `required` は「セットされていること」しか求めず、0 をセットすれば通る。日付の "" を見るところ(`empty_passes`)はそう読んでいたが、数の範囲を出すところは読んでいなかった。契約を狭く読む向きの誤りで、E122 を見逃していた。
- 表の検査が `constraint` を区間の端で比べるとき、端が開いているかを見ていなかった。`>5万円` と `<=5万円` のもとでの `申告額 <= 補償額` は、どの組も満たさないのに、箱を落とせずに E101 を出した。`<=` と `>=` で端が等しいとき、どちらかの端が開いていれば落とす。有理数の上で成り立つ判定なので、再検査(`tools/recheck.py`)と Lean の `constraintRulesOut` を同じ形にし、`not_asked_of_constraint` の証明に二つの場合を足した。刻みの上の閉じた区間で読む案は、Lean の意味論(座標の後ろの値は有理数)で正当化できない箱まで落とすので捨てた。
- ベクタの監査(§9.2)が、`constraint` のせいで作れない境界の点まで義務に数えていた。行のほかのセルと範囲と制約のもとで、その点に入力が無いことを消去で示せたときだけ、義務から外す。有理数の上の反駁なので、作れる義務を落とすことはない。いまのコーパスの件数は一つも動かなかった。
8. **コーパスに「速達の見積」を足した。** 契約(`contracts/quote.proto`)は CEL で二つのことを約束している。速達は 5kg まで、申告額は補償額を超えない、の二つである。規則はその約束に乗っていて、補償料の表は制約が無いと穴が開く形にしてある。変異は二本で、制約を `<` にしたもの(E123)と、5kg を超える速達の行を足したもの(W124)である。
**走らせた**:`src/relation.rs` に七本、`src/cel.rs` に六本、`src/proto.rs` にメッセージの規則と `oneof` の二本、`src/fourier.rs` に上下限の一本を足した。`tests/contract.rs` には次の九本を足した。
- CEL による範囲と、列挙の値
- E123(`.proto` と JSON Schema)
- W124(CEL、`oneof`、`if`/`then`)
- 組み合わせの中のパス
- null
- `optional` の `required`
変異の隣の契約がコーパスのコピーと同じであることも、テストにした。新しい規則の証明書は、再検査と Lean の検査器の両方を通る(補償の表で、制約による空の箱が 6 つ)。Kani のハーネスは 114 本になった。
**見つけて直していないもの**:
- `where` で絞った件数と、並びの要素の数の関係(前者は後者を超えない)は、条件に入れていない。
- JSON Schema で、必ずあるとは言えないフィールドの条件は読まない。存在を真偽値として持てば読めるが、場合が倍になる。
- double のフィールドと `number` の値は読まない。浮動小数の比較を有理数で読むと、境界で答えが変わりうるからである。
- 制約が列でない入力を経て連なるときの誤った E105・E101(§15.139)は、まだ残っている。
**捨てたもの**:
- `oneOf` を「ちょうど一つ」として読む案。否定が要り、枝の中の読めない部分と組むと場合が増える。`anyOf` として広く読んでも、見逃しにはならない。
- CEL の評価器を丸ごと持つ案。読めるのは決定できる部分だけで、それより外は真にするほうが、言うことが確かである。
- 整数の点が見つからないときに E123 を出さない案。消去が決めきれないのは「契約が約束している」ことの証明ではないので、見逃しになる。例の無い E123 として出す。
### 15.141 消去の乗数を証明書に載せ、Lean で確かめる。制約の連鎖(2026-09-23)
**きっかけ**:§15.126 から、消去は行の組が重ならないことを決めてきた。ところが証明書は、その組を「決めていない」と書くしかなかった。消去が示したことを、検査器が足し算だけで確かめられる形で渡せるか。あわせて、§15.139 で残した誤りがある。制約が列でない入力を経て連なるとき、E105 と E101 が誤って出る。これも同じ仕組みで直せるか。
**決定**:
1. **表ごとに線形のモデルを持つ。** 表の数値の列から始めて、次のものを名前でつながる限り集める(数の型ごとに一つの系)。
- 線形の `derive` の等式
- 宣言した範囲
- `constraint`
`grounds` が制約もたどるようにした。これまでは導出の式だけをたどっていたので、列でない入力を経る制約の連鎖が系に入らなかった。範囲のほかに何も言わない表では、モデルを空にする。
2. **ふるいが通した点を、もう一度モデルに照らす。** 穴を探すとき、ふるいが通した点をモデルで反駁できれば、そこは穴ではない。重なりは、まず組の箱全体を反駁し、だめなら点ごとに反駁する。これで §15.139 の誤った E105・E101 が消えた。
3. **証明書は根拠を出どころで運ぶ。** 被覆の空の箱に `farkas` の葉を足した。軸で分かれない組のうちモデルが分けるものは、`undecided` から `refuted` に移した。どちらも、乗数と、不等式の出どころを書く。出どころは、モデルの事実の番号か、箱の座標の端(その値と、開いているか)である。事実そのものは書かない。`linear.facts` に、どこから来たか(導出、範囲の端、制約)だけを書き、検査器は規則の `values`・`ranges`・`constraints` から組み立て直す。導出の線形化は、rulec と同じ形だけを受け付ける。同じ型の名前、リテラル、和と差、数か率による積と商である。座標の端は値を証明書に書き、検査器は、箱のどの座標もその内側にあることを確かめる。最小の端を検査器に計算させるより、証明が短く済む。
4. **到達の例は、モデルを解いた値にした。** モデルのある表では、点の座標とモデルを消去で解く。列でない名前の値は `extra_values` に書く。読む人向けの `values` も同じ点にそろえた。以前は座標の中ほどの値で、検査用の値と食い違いうる。
5. **Lean で確かめる。** 足したものは次のとおり。
- `RulecCert.Linear`:線形の不等式と、乗数の検査(`farkasOk`)。検査が通れば、全部を同時に満たす値は無いという定理(`farkas_sound`)
- `Sieve.asked`:モデルの事実を条件に足した。値の並びは、軸のあとに列でない名前が続く
- 出どころから不等式を組み立て直す関数と、乗数が通れば箱の中に問われる点は無いという定理(`not_asked_of_farkas`)
これを、被覆の空の判定(`Certified.ruledOut`)と、問われる点に限った重なりの主張(`disjointAskedHolds`、`pairsApart`)につないだ。`Certified.unique` は、この新しい主張から導く。事実は `Main` が証明書の式から組み立てる(`linOf`)。
6. **`#print axioms` のテストを足した。** 文書は「`#print axioms` に出るのは三つだけで、テストが見張っている」と書いていた。ところがテストは、`sorry` と `axiom` の文字列を探すだけだった。いまは、主張が立つ十三の定理について、Lean に尋ねて確かめる。
**直した欠陥**:
- `solve` が、等式で一点に決まる値を、整数でないときに返さなかった。`between` が整数を探して見つからず、幅 0 の区間で中点を諦めていた。
- 再検査で、座標の端の「開いているか」を受ける変数が、厳密さの累積と同じ名前で、累積を上書きしていた。書いてすぐ、テストで見つかった。
**走らせた**:
- クーポン併用の行1と行2は、これまで証明書で未決だった。いまはモデルの乗数つきで離れていると示せて、再検査も Lean も通る。W114 の変異体の組は未決のまま。
- 制約の連鎖の規則を二つ足した(`tests/fourier.rs`)。一つは連鎖があって初めて完全になる表、もう一つは連鎖があって初めて重ならない表で、どちらも `check` を通る。連鎖の一本を外すと、E101 と E105 が戻る。証明書は両方の検査器を通り、乗数を別の事実に向けると、両方が受け付けない(`tests/cert.rs`、`tests/lean.rs`)。
- コーパスの証明書は、全部が両方の検査器を通る。
**見つけて直していないもの**:
- 真偽の定義を一つの値に決める条件(§15.127)は、モデルに入れていない。検査の側の消去はこれも使うが、証明書の意味論が定義の本体を知らない。これで決まる組は、証明書では未決のまま。
- 整数であることは見ていない(W114 の変異体)。
- 事実の意味、つまり導出の式が規則のものであることは、ほかの値の主張と同じく、証明書の言い分である。`--rule` を渡しても、セルのようには本文に照らさない。
**捨てたもの**:
- 証明書に不等式そのものを書く案。検査器が出どころから組み立て直せば、書いた不等式が規則と合っているかを別に確かめなくて済む。
- 導出の軸の届く範囲を、別の出どころにする案。rulec の届く範囲は宣言した範囲そのものなので、範囲の事実で足りる。
### 15.142 契約と入口の関係を証明書に載せ、Lean で確かめる(2026-09-23)
**きっかけ**:§15.140 から、契約が通す値を規則の入口が受け取るかを `check` が決めている。けれども、その答えは rulec を信じるしかない。表の主張と同じく、別のプログラムが足し算だけで確かめられる形で渡せるか。もう一つ、契約の条件は整数の上の話なのに、再検査器の反駁は有理数の上の足し算である。`weight_g > 0` の契約が `>=1g` の範囲を守ることは、整数の上でしか言えない。この差をどう埋めるか。
**決定**:
1. **主張は「含まれる」である。** 契約が通す値は、どれも入口が受け取る。入口が求めるのは次の三つである。
- 数の入力が、宣言した範囲に入ること(契約が運ぶ整数の刻みで)
- そのうち二つの入力のあいだの `constraint` が成り立つこと
- 列挙の入力が、その列挙の値であること
含まれていれば、呼び出し側の検証を通った要求が入口でエラーになることはない。表は入口の内側で完全なので、答えの無い要求も無い。
2. **証明書に `contracts` を足した。** 契約ごとに、条件を原子と場合で書き、入口の求めることごと・場合ごとに証明を付ける。証明は三種類ある。
- `farkas`:場合の不等式と、求めることの否定を足し合わせると矛盾になる乗数
- `clash`:場合の文字列か真偽値が両立しない
- `within`:場合が許す文字列が、どれも列挙の値である
入口そのものは書かない。再検査器が、規則の `ranges`・`constraints`・`types`・`enums` から組み立て直す。証明書が求めることを一つ書き落としても、検査器はそれを「示されていない」と言う。§15.141 で事実を出どころから組み立て直したのと同じ考え方である。列挙の値は、これまで証明書に無かったので `enums` として足した。
3. **数の原子を整数の形に直す。** 契約の条件に出る値は、どれも整数である(`int64` などのフィールドと、並びの要素の数)。そこで原子を場合に開く前に、係数を互いに素な整数にし、定数を不等式の向きに丸める。`2x < 5` は `x ≤ 2`、`2x = 5` は偽、`x ≠ 5` は `x ≤ 4` か `x ≥ 6` になる。整数の上では同じ意味で、有理数の上では狭くなる。これで、整数の上でしか成り立たない境界も、有理数の足し算で示せる。この直しは、場合への開き方と同じく、契約の読みの一部である。
4. **契約の本文を digest で縛る。** `sha256` に契約のファイルの digest を書く。再検査器は、`--rule` を渡されると、規則の隣から契約を読んで照らす。これが無いと、契約を書き換えたあとも古い証明書が黙って通る。
5. **Lean で確かめる(`RulecCert.Contract`)。** 値は数・文字列・真偽の三つの並びで、原子と場合と入口をその上に書いた。検査は `includedOk`、定理は `included_sound` である。検査が通れば、条件のどれかの場合を満たす値は、入口の求めることを全部満たす。`Main` は証明書を読み、入口を規則から組み立て直して `includedOk` を走らせる。`#print axioms` のテストに `included_sound` を足した。
6. **示せないことは名前で言う。** 二つの再検査器は、証明の無い求めることを一つずつ「示されていない」と言い、それがあれば「OK」だけでは終えない。読めない部分を真とした契約(`unread`)と、場合が多すぎて開けない契約も同じである。
**直した欠陥**:
- Lean の検査器の最後の一行が、証明は `RulecCert.Sound` にあると言っていた。§15.141 から乗数の定理は `Linear` と `Sieve` にあり、いまは `Contract` にもあるので、`RulecCert` の定理と言うようにした。
- `proofs/README.md` のファイルの表に、§15.141 の `Linear.lean` が無かった。
**走らせた**:
- コーパスの契約三つの証明書が、両方の検査器を通る。速達の見積は 7 つの求めることを 2 つの場合で、出荷の送料は 5 つを 2 つで、注文の送料は 3 つを 1 つで示した。注文の送料の契約には読めない部分があり、両方の検査器がそれを言う。
- 偽りを 9 通り試した(`tests/cert.rs`、`tests/lean.rs`)。乗数を変える、契約を広く読む、別の原子を指す、場合の証明を一つ欠く、食い違わない場合を食い違うと言う、値を渡す入力を値の一覧から外す、別の契約の本文、の 7 通りは、両方の検査器が受け付けない。求めることを一つ落とす、証明を空にする、の 2 通りは、通したうえで示されていないと言う。
- 列挙に無い値を通す契約は、両方が受け付けない。食い違う場合を手で足した証明書は、両方が通す。
- `included_sound` が立つ公理は、Lean 自身の三つだけである。
**見つけて直していないもの**:
- 省略できる入力(`時間帯?`)と、`any` や `where` 付きの `count` で読む入力は、契約の節に入らない。セットされていないときの読みや、並びの中の条件が要るからで、一つずつの比較(E122)が受け持つ。
- 食い違う場合は、場合に開くときに落とす。だから rulec 自身の証明書に `clash` はまず出ず、落とし方は読みの一部になる。
- 契約の読み、つまり原子と場合が契約の本文の言うとおりであることは、証明書の言い分である。再検査器は CEL もスキーマも読まない。契約がどの入力に値を渡すか(`from`)と、その刻みも、ほかの宣言と同じく証明書の言い分で、証明書がある入力を節から外せば、その入力について入口が求めることは確かめられない。
**捨てたもの**:
- 入口の条件を証明書に書く案。書いた入口が規則と合っているかを、別に確かめなければならなくなる。
- 整数の点を分けて解き直した木(§15.140)を、そのまま証明にする案。証明が大きくなる。整数の形に直すだけで、コーパスの求めることは全部示せた。
- 食い違う場合を落とさずに、`clash` の証明を付ける案。場合が増えて、256 の上限に早く届く。
### 15.143 文書を転記するときに踏む欠陥を直す(2026-09-23)
**きっかけ**:文書から規則を起こすとき、まず踏むのは数字の書き方と、行の取り違えである。文書は `1,000円` と書く。料金表の額を転記するときに起こりやすいのは、額そのものの打ち間違いより、隣の行の額を転記してしまうことである。どちらも、コピーと突き合わせる検査が止めるべき誤りなのに、止めていなかった。文書から始める流れの入口にあたる `import` と `source` は、MCP から呼べなかった。転記の作業で踏むものを、どこまで止められるか。
**決定**:
1. **桁区切りのカンマを止める(E049)。** §1.5 で「桁区切りのカンマは許さない」と決めていたのに、実装はエラーにしていなかった。`,` はセルの中で集合を区切るので、数の列の `<=1,000` は `<=1` として検査を通り、金額の列の `1,000円` は「`1` に単位が無い」という的外れな E103 になっていた。字句解析で、1〜3 桁のあとに `,` と 3 桁の組が続く形を止め、`fix.text` に区切りを外したリテラルを出す(`fix.kind` に `rewrite_literal` を足した)。行は読み捨てず、区切りを外した数として読み進める。読み捨てると表がそこで切れ、下の行が全部 E004 になる。あわせて、字句の誤りにも `ファイル:行` を付けた。
2. **E116 は、行の見出しの下で額を探す。** 行のセルの語(`関東`)と同じ見出しがコピーにあれば、その見出しの行と列の中だけを探す。一列目は、語で始まる見出しも同じ見出しとみなす(`一般の事業` と `一般`)。ほかのセルは、空白と `・` を除いて一字一句同じものだけである。結合された見出しの下や横の空欄も、その見出しの行と列に含める。探す範囲は見出しの数だけ広がるだけなので、正しい見出しさえ読めていれば誤った E116 にはならない。額がコピーの別の見出しの下にあるときは、その見出しを言う。見出しの無いコピーは、これまでどおり表全体で探す。
3. **コピーの千分率を率として読む。** `5/1,000`、`8.5/1,000`、`1,000分の5` を 0.5% などとして読む。分母は 100 以上の 10 のべきに限る。`4/1` や `12/31` を分数と読まないためである。
4. **MCP のサブコマンド。** 使い方の行が `a|b` の選択肢で始まるコマンドは、`subcommand` を取る。値はその語の `enum` で、`file` の前に渡る。選択肢に無い値は、呼ぶ前にエラーにする。
5. **`check --help` と `gen --help` の一覧を台帳から作る。** 手書きの一覧が台帳に遅れ、E116・E119・W120・E120〜E123・W122〜W124・E047・E048 が抜けていた。台帳のコードが help に載っているかという逆向きのテストを足した。
6. **docling のテンプレート。** シバンを一行目に戻した。`--via ./extract.py` で動かすとシェルが読んでいた。`export_to_dataframe` に文書を渡し、見出しの行が見つからなかった表の `0 1 2 …` の行は書かない。
7. **E104 は率を百分率で言う。** 率の値は分数で持っているのに、そのまま `%` を付けて書いていた(12% が `0.12%`)。端数かどうかは、整数かではなく、出力を格納する刻みで判定する。ヒントの刻みも率の刻みにした。
**直した欠陥**(上の七つを直す途中で見つけたもの):
- 数の列の集合(`100, 200`)が、どの座標にも当たらなかった。検査は集合を列挙の列でしか読んでおらず、行は E102 になり、表はその行の値を欠いていた。評価器と生成コードは集合として読んでいた。座標、箱、境目のベクタ、カバレッジの監査で、集合の値を点として読む。証明書は `{"cell":"in","values":[…]}` と `not_in` で渡し、二つの再検査器が箱を組み直す。Lean に `CellTest.inVals` と `notInVals`、補題 `mem_boxOf_in_iff` と `mem_boxOf_notIn_iff` を足した。
- `source pin` は、固定が変わらない出典について何も言わず、そのため「出典の宣言がありません」と言っていた。
- `api` の丸めの刻みを、率では分数を整数に切り捨てて書いていた。コーパスの `round down(1%)` が `"grid":0` になっていた。範囲と同じく、値が運ばれる整数で書く。
**走らせた**:
- 七つの欠陥は、直す前にどれも手元で再現した。直したあと、それぞれにテストがある(`tests/units.rs`、`tests/sources.rs`、`tests/extract.rs`、`tests/mcp.rs`、`tests/codes.rs`)。
- docling の代わりに、同じ呼び出しを持つ小さなパッケージを置き、テンプレートを使い方どおりに動かした。
- 厚生労働省の雇用保険料率の PDF を docling で取り出したコピーで、`5/1,000` を読めるようになった。令和8年度の一般の事業の行に前年度の 0.55% を書くと、E116 が「(令和7年度)の行にある」と言う。
- コーパス、ライブラリ、変異の規則で、E116 と W120 が新たに出たものは無い。
- 数の集合の規則を 12 の言語で生成し、境目のベクタが全部一致した(`tests/threeway.rs`)。証明書は両方の検査器を通り、広げた箱はどちらも受け付けない。
- E049 の変異を足した(`送料.rule` の閾値を `<=2,000g` と転記したもの)。
**見つけて直していないもの**:
- 出力の率の格納の刻みは、宣言した `step` ではなく、表の値と丸めから決まる。`rate[step 0.1%]` の出力でも、0.5% と 12% だけの列は 0.5% 刻みで運ばれる。行を足すと整数の意味が変わるので、入力に刻みを宣言させた理由は出力にも当てはまる。
- 見出しの照らし方は、上の二つの形だけである。規則の語が見出しの言い換えのとき(見出しは `東京都・神奈川県`、規則の語は `関東`)は照らせず、表全体で探す。
- 漢数字の千分率(`千分の九十一`)は読まない。
- 同じ行の中で列を取り違えた額は、列の見出しが行の語と合うときにしか見分けない。
**捨てたもの**:
- 見出しを部分一致で照らす案。注記のセルに語が入っているだけで、額の無い行に絞ってしまう。
- 見出しごとの範囲の交わり(行と列の重なるセル)で探す案。一つの見出しを読み違えると正しいセルが外れ、誤った E116 になる。
- 数の列の集合を、型の検査でエラーにする案。評価器と生成コードは集合として読んでいて、読めていなかったのは検査の側だけだった。
### 15.144 出力の率は宣言した刻みで渡す。照合できなかった記録(2026-09-23)
**きっかけ**:生成コードは率を整数でやりとりする。入力なら、その整数が何 % を 1 と数えるかは宣言した刻みで決まり、行を足しても変わらない。出力は違った。宣言した刻みではなく、表の値と丸めと、出力の列に名前で出す値の刻みから決まっていた。そのため、宣言が黙って無視される(`rate[step 0.1%]` の 1% が `1` で渡る)。細かい刻みの値を名前で出す行を足すと、ほかの行の答えの整数まで変わる。`result` で組む出力は 1% 刻みに落ち、12.3% を `12` と切り捨てていた。呼び出し側が受け取る整数の意味を、規則の宣言だけで決められるか。あわせて、刻みが変わったあとの過去の記録を `replay` にかけると、一件も照合されないのに「不一致はありません」で終わっていた。
**決定**:
1. **率の出力は、宣言した刻みで渡す。** 宣言が無ければ、丸めの刻みで渡す。数の出力は丸めの宣言が必須(E104)なので、答えは必ず丸めの刻みに載る。どの行を足しても、呼び出し側の整数の意味は変わらない。
2. **丸めの刻みが、宣言した刻みの整数倍でなければ E114 にした。** 答えは丸めの刻みに載るのに、呼び出し側へは宣言した刻みの整数で渡すので、表せない答えが出る。E114 の見出しは「値が宣言した刻みに載っていません」に広げた。
3. **生成コードは、出力の値を細かい刻みで持ち、丸めてから一度だけ割って渡す。** 細かい刻みとは、渡す刻みと丸めの刻みがどちらも割り切れる刻みのことである。12 の言語とも同じ形で、`result` の式もそこまで広げてから丸める。
4. **一件も照合できなかった実行は、通さない。** `replay`、`verify`、記録を使う `diff` は、比べた記録が一件も無いとき(全件が形の誤りで外れた、答えられなかった)、「照合できた記録はありません」と書いて 1 で終わる。
5. **記録の値が宣言範囲の外のときの文面を、規則の書き方にそろえた。** 率の範囲は `0.01..0.03` ではなく `1%..3%` と書き、整数にはそれが表す値を添える(`1(0.1%)`)。
**走らせた**:
- 規則三つで確かめた。宣言 0.1% に丸め 1% のもの、細かい刻みの値を名前で出す行を足したもの、`result` で組むものである。どれも、生成コード・検算器・API がそろって宣言どおりの整数を出し、12 の言語で境目のベクタが一致した(`tests/threeway.rs`)。
- コーパスの率の出力は三つとも `rate[step 1%] round down(1%)` で、整数は変わらない。コーパス全体の全言語の突き合わせも、前と同じく通る。
- 刻みを変えた規則で、前の刻みの記録を `replay` にかけると、全件が外れて 1 で終わる。
**直した欠陥**:
- PostgreSQL の関数を呼ぶ走らせ役(`rulec test`)が、出力が一つで表を持たない規則で落ちていた。列が一つの関数は `FROM` の中でその列の型になり、`row_to_json` に行として渡せない。副問い合わせを挟み、列の数によらず行にした。手元には psql が無くてこの側を飛ばしていたので、上の規則を足した CI で初めて見つかった。
**見つけて直していないもの**:
- 記録は率を整数で持つだけで、その刻みを書かない。刻みが変わった規則で過去の記録を読むと、形の誤りとして外れる。今回それを 1 で止めたが、前の刻みで読み直す手立ては無い。
- 出力でない計算の率は、いまも表の値から刻みが決まる。呼び出し側には出ないので、整数の意味は問題にならない。
**捨てたもの**:
- 出力の刻みを表の値から決めたまま、変わったときに警告する案。宣言があるのに、それと違う整数を渡すこと自体が誤りである。
### 15.145 刻みや単位を変える前の記録を、書いた版で読む(2026-09-23)
**きっかけ**:記録は数を整数で持つだけで、何を 1 と数えるか(刻みと単位)を書かない。規則の宣言した刻みや単位を変えると、それより前の記録は、もとの意味で読めなくなる(§15.144 では、全件が外れた `replay` を 1 で止めるところまでにした)。記録の形を変えずに、過去の記録を読み直せるか。
**決定**:
1. `fixtures lint`、`replay`、記録を使う `diff` に `--read-as <file.rule[@rev]>` を足した。記録の数を、その版の規則の刻みと単位で読み、いまの規則の刻みと単位に直す。版は、`diff` と同じく、ファイルか git のリビジョンで書く。
2. 単位は、同じ次元なら換算する(円と銭、g と kg)。通貨が違うときや、率と金額のように種類が違うときは、その記録を形の誤りとして外す。
3. 別の規則を渡したら、引数の誤りとしてエラーにする。記録を使わない `diff` に渡したときもエラーにする。
4. 一件も照合できず、形の誤りで外れた記録があったときは、この引数を案内する。
**走らせた**:
- 1% 刻みで書いた記録を、0.1% 刻みにした規則で読んだ。引数が無いと全件が外れて 1 で終わり、引数の案内が出る。前の版を渡すと、全件を照合して一致する。
- 円で書いた記録を、銭で数える規則で読んだ。前の版を渡すと一致する。
- 前の版を git のリビジョン(`rate.rule@HEAD~1`)で渡しても同じく読める(`tests/m3.rs`)。
**見つけて直していないもの**:
- どの版で書いた記録かは、人が知っている必要がある。記録は、版も刻みも書かない。
- 記録を書いたあとで入力や出力の名前を変えた版に対しては、読めない。
**捨てたもの**:
- 記録に刻みと単位を書き込む案。記録が自分の読み方を持つので、版を知らなくても読める。ただ、記録を書く側が十数か所あり(12 の言語の記録関数、`rulec test` の走らせ方、MCP サーバーなど)、どれも同じ記録を出すことをテストが見張っている。規則の刻みや単位を変えることはまれなので、読む側の引数で足りる。
### 15.146 承認する人のページを暗い配色でも出す(2026-09-25)
**きっかけ**:承認する人のページ(`rulec doc --format html`。MCP のツールの view とプレイグラウンドの枠も同じページ)は、明るい配色しか持っていなかった。暗い配色のブラウザやホストの中では白く浮く。サイトの「何を証明するか」に載せたこのページのスクリーンショットも、暗い配色のサイトではそこだけが白い四角になる。
**決定**:
1. 色はすべて `:root` の変数(`--rc-…`)にまとめ、明るい側は前と同じ値にした。暗い側は `@media (prefers-color-scheme: dark)` と `:root[data-theme="dark"]` の二か所に置く。`color-scheme: light dark` を宣言したので、入力欄やボタンもブラウザが暗く描く。
2. 盤面の線と矢印は、スクリプトに直に書いていた `#b0b0b0` をやめて `currentColor` にし、色は CSS の `#wires` が決める。
3. MCP Apps のホストの中では、ホストのテーマに従う。`ui/initialize` の応答の `hostContext.theme` と、あとから来る `ui/notifications/host-context-changed` の `theme` が `light` か `dark` なら、それを `data-theme` に入れる。ファイルとして開いたときは、読む人の設定に従う。
4. `tests/graph.rs` に試験を一つ足した。暗い側の宣言、ホストのテーマを読むこと、変数を通さない色がページに残っていないことを確かめる。
5. スクリーンショットは明暗の二枚を撮る。`website/tools/shots.sh` は `--blink-settings=preferredColorScheme` で配色をはっきり指定する。指定しないと、ヘッドレスの Chrome は撮る機械の設定に従うので、暗い設定のデスクトップでは明るい版まで暗く出る。サイトは `#only-dark` と `#only-light` で出し分ける。
6. ページが変わったので、プレイグラウンドの `rulec.wasm` を作り直した(`tests/wasm.rs` がページを一字一句で比べている)。
**明るい側で変わったこと**:`color-scheme` の宣言で、入力欄のプルダウンの矢印の描き方がわずかに変わった。画素で比べて、矢印のまわりの 274 画素だけである。
**捨てたもの**:
- **ページに明暗の切り替えボタンを置くこと。** 読む人の設定か、ホストの指示に従えば足りる。承認する人の画面に、判断と関係の無い部品を増やさない。
- **ホストが渡す CSS 変数(`styles.variables`)まで取り込むこと。** 当たった行の琥珀色は判断を読むための印で、ホストの配色に溶かすと見分けにくくなる。いまは明暗だけに従う。
### 15.147 入口で「小さな言語」と名乗る(2026-09-25)
**きっかけ**:トップの最初の画面で、何の道具かを一度で言えているか。§15.113 のあとも、機能を足すたびに冒頭の文と「rulec がやること」に一行ずつ足してきた。英語のトップは 538 行になり、12 の言語の名前を全部並べた箇所が本文だけで三つ、同じ E101 が三つ、自己紹介が形を変えて四つあり、「rulec がやること」は九つの項目を同じ扱いで並べていた。9 月 22 日に「一つのことは一度だけ言ってリンクする」直しを入れたが、削る向きの直しでは追いつかなかった。
名詞も合っていなかった。看板は「表を書く」、一文目は「表で書ける業務ルール」、ツアーの名前も「表(.rule)を書く」だった。表は条件を書く形で、書けるのは表だけではない。計算(`derive`・`define`)、本則に優先する特例(`overrides`)、ただし書(`clause`)、準用(`apply`)、明細の並び(`elements`)、按分(`allocate`)がある。入口がそろって「表」と言うと、表しか書けない道具に見える。
**決定**:
1. 看板を「ルールを書く。証明する。コードにする。」、英語は "Write rules. Prove them. Compile them." にした。
2. 一文目は「業務ルールのための小さな言語」にし、表は「条件は表に書く」と、書き方として出す。二文目で、小さいのはわざとで、だから証明できると言う。再帰も状態も持たず、セルは自分の列しか見ない。AGENTS.md の §4 が検査の終わるための代償と呼んでいるものを、入口では証明できる理由として言う。Bentley の言う little language(領域を絞り、ふつうの言語が持つ機能の多くを持たない言語。SQL や正規表現がそうである)と同じ形である。
3. 三文目で、書く作業をエージェントに任せられることを言う。エージェントが返してくるのはコードではなく、人が読めるルールである。
4. トップは、ヒーロー、図、「rulec がやること」の九つの行、手元にあるものから始める、誰が書くか、何が証明されて何がされないか、書けることと書けないこと、次に読むもの、に絞った。英語は 538 行から 224 行、日本語は 429 行から 189 行になった。「なぜこの形を狙うのか」「言語としての狙い」「似たものは何があるか」は「自分のルールが入るか」へ移した。ほかの節は、同じことを書いたページがすでにあるので削った(単位と丸めは「何を証明するか」、表を重ねる話はツアー、生成物に付いてくるものは「生成して呼ぶ」、Kani と証明書と Lean は「どうやって確かめているか」)。
5. 書けるものが表だけでない理由は、親が二つあることで説明する。条件を表に書く形は DMN から、本則と特例、ただし書は Catala から来ている(§15.70)。
6. README も同じ順に並べ直した。Kani の本数と、src のモジュールの一覧は外した。`tests/website.rs` は README の Kani の本数を見なくなった(本数は「生成して呼ぶ」が持つ)。
7. 同じ一文を、README、両言語のヒーローと `site_description`、SNS プレビューの画像、`Cargo.toml` の `description` に置いた。ツアーの名前は「ルール(.rule)を書く」にした。GitHub の About と SNS プレビューの画像は、設定画面から手で差し替える。
8. 承認する人のページは、トップでは行の半分の幅に収まるので、`website/tools/shots.sh` に幅 1100 の窓で撮る一枚(`try-top-*.png`)を足した。入力欄と色の付いた行と答えが、その幅でも読める。
9. トップの流れの図も同じ名詞に直した。エージェントが書くもの、人が承認するもの、コードの出どころを「表」から「ルール」にし、真ん中の紙の「1 規則 = 1 表」を「表とただし書」(英語は "tables, provisos")にした。
**見つけて直したもの**:狭い画面で、「rulec がやること」の行がコードブロックの幅に押し広げられ、本文が画面の右にはみ出していた。一列にするときの `1fr` は、中身の最小の幅より狭くならない。`minmax(0, 1fr)` にして、コードブロックは枠の中で横にスクロールするようにした。
**「rulec がやること」の並び**:先頭は大前提の「表を土台にするから、証明できます」にした。セルは自分の列しか見ないので一行が一つの区画になり、隙間と重なりを計算で決められる。同じ表を人は仕様として読んで承認し、生成したコードでは一行が一つの分岐になる。見せる表は冒頭の図の運賃表(一行足りない)で、二つめの「抜けと重なりは、走らせる前に落ちます」が、その表の本物の E101 を見せる。あとは、単位と税区分が型で丸めは宣言が要ること(E103 と E104 の本物の出力)、表を重ねて書けること(ゆうパックのサイズ判定と運賃表、英語は小包のサイズと基本料金)、特例やただし書もまとめて検査すること、証明を rulec の外で確かめ直すこと(証明書、依存の無い再検査器、Lean の定理、生成した Rust への Kani)、出典を引用してコピーを固定すること(`source` の宣言と `@` の引用が見える `.rule` を載せる。日本語は日本年金機構の保険料額表のブック、英語は GOV.UK の SDLT)、承認する人に渡すページ(`rulec doc --format html` のスクリーンショット。明暗の二枚、§15.146)、12 の言語、の順。各行に載せるのは、コーパスの規則の抜粋か、走らせた出力か、ページを撮った画像で、手で作った例は載せない。
一度は先頭を「出典の書き方のまま書けます」にして、条文の引用とハッシュの固定を見せた。表だけではないことを見せるつもりだったが、先頭に来たのは出典の話で、芯ではない。そもそも「書く」は使う人がすることで、rulec がすることではない。出典は外さずに、五つめの行として、引用とコピーの固定を rulec のする仕事として書いた。
**入口の主張の大きさ**:生成したコードを「検証済み」とは書かない。生成コードが表と同じ答えを返すことはテストであって証明ではないと、README の「証明していないこと」の二番目に書いてある。入口でそう書けば、同じページの下で取り消すことになる。「`source` と `import` を除く」「有理数の上で解くので」のような細かい注記は消さずに、主張を全部述べるページへ置いた。
**捨てたもの**:
- **判断の互換(契約との突き合わせ)を入口の中心に置くこと。** §15.138 のとおり新しいところではあるが、rulec が何の道具かを一文で言うものではない。使い道の一つとして、立場ごとの使い方と「似たものは何があるか」に置いたままにした。
- **看板の目的語を「表」のままにすること。** 表は検査の仕組みの芯だが、入口の名詞にすると、表しか書けないと読まれる。
- **一文目に「ハーネス」を残すこと。** §15.113 の判断を一部戻した。一文に名詞を二つ並べると、入口が二つのことを言う。そのうえハーネスはエージェントを組む人の言葉で、業務ルールの側の読み手には通じない。エージェントの話は三文目に普通の言葉で置いたので、入口から消えてはいない。
- **比較の表のために新しいページを立てること。** 「自分のルールが入るか」は、この道具が自分のルールに向いているかを確かめるページなので、隣の道具との違いもそこで読める。ナビの項目も増えない。
### 15.148 ステートマシンの一歩を書く。§0.1 を開き直す(2026-09-25)
**きっかけ**:業務ルールには、一回の判定で終わらず、続いていく何かの一歩ぶんのものがある。注文は入金され、出荷され、配達されるか、取り消される。§0.1 は「ワークフローとステートマシン」を捨てたが、理由は P3(ルールは純関数)との衝突だった。状態を呼び出し側が持つなら、生成する関数は純関数のままで、呼び出しの並びについての主張(取り消した注文は出荷されない、返金は一回まで)を証明できるのではないか。できるなら、別の言語を作るのか、宣言を一つ足すのか。何を足して、何を捨てるか。
**決定**:
1. **別の言語ではなく、宣言を一つ足す。** `machine <名前>(<別名>) over <表>` の節に、`carry <入力> -> <出力>`、`initial`、`final`、`never <状態…> after <状態…>`、`once <出力> <セル>` を書く。意味として新しいのは `carry` だけで、「この出力を、次の呼び出しではこの入力として渡す」と言う。表はふつうの表で、完全性・重なり・単位の検査はそのまま効く。「出荷のあとに取消依頼が来たら」の行が無ければ、これまでどおり E101 である。
2. **生成する関数は変えない。** 状態は引数で受け、次の状態を返し、何も覚えない。状態は呼び出し側(注文の行など)が持つ。P3 も、過去再生の意味(§10.3)も崩れない。
3. **主張は読める形に限る。** 一般の時相論理(LTL・CTL)は採らない。`constraint` で一般の式を採らなかったのと同じ理由で、承認する人が読めない主張は承認されない。言えるのは、終わりの状態(`final`)、あとに来てはいけない状態(`never … after …`)、一つの案件で一回までの答え(`once`)の三つと、宣言しなくても確かめる「たどり着けるどの状態からも終われる」である。`final` があるので、終わらない不具合が一歩の検査になる。
4. **決まる理由。** 状態は有限の列挙で、ほかの入力は表自身の境目で有限の区画に切れる(`diff` の区画、§15.122)。区画ごとに、そこを実現する入力を一つ探し、その答えを遷移とする。主張は有限のグラフの探索になる。実現する入力も、実現しないことの証明も見つからない区画が主張を左右するときは W127 とし、成り立つとは言わない。区画が予算を超えたら E128 である(`--budget` の単位で、区画一つに 50)。`once` の数える出力が入力や計算から決まるときは、その値の境目をセルの境目でも切り、区画の中で答えが一つに決まるようにする。
5. **線を引いたもの。** 呼び出しをまたいで数を持ち越さない(残高や回数は、これまでどおり呼び出し側が数えて渡す。足し算で持ち越すとカウンタになり、到達できるかが一般には決まらない。比較だけなら決まることは Felli・de Leoni・Montali(Fundamenta Informaticae、2021)が示しているが、今回は入れない。範囲の両端のある数は、§15.217 で持ち越せるようにした)。一つの規則に機械は一つ、持ち越す状態も一つ(並行とメッセージの順序は TLA+ や P の領分である)。暦は持たない(「14 日以内」は経過日数を入力で受けて定数と比べる)。並びを畳む規則は機械の一歩になれない(E054。答えが並び全体で決まり、区画に切れない)。
6. **反例は最短の呼び出しの並び。** 崩れた主張には、始まりの状態から崩すまでの最短の並びを付ける(JSON は `witness.trace`、一回ずつが規則の受け付ける入力と、答えと、当たった行)。一行ずつ読むともっともらしい表でも、並びにすると誤りが見える。
7. **`scenario` は何回かの呼び出しにわたる例。** 持ち越す入力の列は書かせない。一行目は `initial` から、二行目からは一つ前の答えから始まる。列に書くと、どちらを信じるかが決まらないからである。食い違いは E107 で、それまでの呼び出しの並びを付ける。
8. **生成物に足すのは、始まりと終わりの定数だけ。** モジュールを持つ九つの言語に、始まりの状態、終わりの状態の集合、終わったかどうかの判定を足す。名前は言語の慣習に合わせ、`final` が予約語の PHP は `FINAL_STATES`、Swift は `initialState` と `finalStates` にした。NumPy の plan はデータとして持つ。SQL の問い合わせと Wasm のモジュールは入口が一つで、定数を置く場所が無いので足さない。`rulec api` の `machine.constants` が、言語ごとの綴りを全部言う。
9. **ベクタに呼び出しの並びを足す。** 案件がたどれる遷移の一つずつと、続けて起きうる二つずつについて、始まりの状態からの最短の並びを作り、`scenario` も加える。各言語の走らせ役は、前の呼び出しが返した状態を、その言語が持っている値のまま次の呼び出しに渡す。JSON から読み直さないので、呼び出し側がする受け渡しそのものが比べられる。最初の一行で定数を出させ、それも比べる。
10. **網羅の基準を一つ足した(六つ目)。** 遷移の一つずつと、続けて起きうる二つずつに、ベクタが届いているか。二つずつにしたのは、一つずつでは、二回の呼び出しのあいだで状態を渡し損ねる誤りを踏まないからである。
11. **`doc` に状態の移り方を描く。** Markdown には mermaid の図と、状態ごとの行き先の表と、検査が確かめたこと。HTML には SVG の図を描き、答えた状態をフォームに戻して次の出来事を入れるボタンを置く。顧客向けの記事には「状態の移り方」の節を足した。
12. **`diff` は、呼び出しの並びと、処理中の案件で言う。** 二つの版が違う答えを返す最短の並びと、改定で案件がどうなるか(その状態が無くなる、そこから終われなくなる、終わりでなくなる、終わりになる)を言う。記録を使う `replay` と `diff` は、`tag` の同じ記録を一つの案件の呼び出しとして、ファイルの順に、その版が答えた状態を渡しながら通し直し、途中で答えが分かれた案件、途中でエラーになった案件、取り残された案件を数える。
13. **証明書と Lean。** 遷移は、行の箱が状態の軸で取る座標から読む。実際に起きる呼び出しより広いが、「悪い状態に着かない」主張(到達、`final`、`never`、`once`)は広い側で閉じていれば成り立つ。「終われる」主張だけは、実際に起きる呼び出しが要るので、行の箱の中の点を値つきで渡す(`reach` と同じ扱い)。`tools/recheck.py` と `proofs/RulecCert/Machine.lean` が確かめ、Lean には `reaches_mem`、`final_stays`、`never_after`、`once_below_two`、`reachable_finishes` の定理がある。
14. **すべての面に載せた。** 診断は形の誤り六つ(E050〜E055)、主張の崩れ四つと予算切れ一つ(E124〜E128)、警告三つ(W125〜W127)。コーパスに書き下ろしの `注文の状態.rule` を足し、変異を十本足した。トップの「rulec がやること」には、特例とただし書の行のあとに「ステートマシンも書けます。呼び出しの並びまで検査します」の行を足した(§15.147 の並びの六つ目)。載せるのは、コーパスの規則の `machine` の節と、取消のあとの入金の欠陥を入れたときの E126 の実物の出力で、そのあとの行は左右の向きを一つずつ入れ替えた。ヒーローの一文目にも、README と同じく「状態が移っていく手続きの一歩」を足した。
**走らせた**:
- `注文の状態.rule` は 12 の言語で、単一の呼び出し 42 件と、並び 40 回ぶんの呼び出しが一致した。MCP(stdio と HTTP)、Connect(ASGI と WSGI、GET も)、WASI も同じ並びで一致した。網羅は六基準とも満たし、ステートマシンの遷移は 28 件である。Kani のハーネス 2 本が通る。
- 取消のあとの入金で注文を戻す行を入れると、E124・E126・E127 と、手順の例の E107 がそろって出る。一行ずつ読めば、どの行ももっともらしい。
- 証明書は二つの再検査器を通り、偽った六通り(到達集合から状態を落とす、行の行き先、終わりの状態、`never` と `once` の組、終わりへの呼び出しの行)はどちらも受け付けない。
- 出力が状態だけの機械では、関数が構造体ではなく状態そのものを返す。十の走らせ役が、無いフィールドを読んでいた。直して、`tests/machine.rs` に試験を置いた。
- `machine` の無い規則の生成物は、前と一字も変わらない。例外は、どの規則にも共通の部品だけである。SQL と Wasm の走らせ役(持ち越しの定数が `None` と `null` で入る)、承認する人のページ(図の CSS と続けるボタンのスクリプト)、NumPy の実行時(plan の `machine` を読む一行)。
**直した欠陥**(作る途中で見つけたもの):
- 例の期待値を、例の見出しの順ではなく出力の宣言の順で読んでいた。見出しを `-> y | x` と書くと、正しい例が E107 になっていた。
- TypeScript から JavaScript を作るときに型注釈を外す処理が、`export` の付いた宣言を素通りしていた(`export const INITIAL: State` が JavaScript に残る)。
**見つけて直していないもの**:
- 案件のあいだ変わらない入力(注文の金額など)を、宣言で分ける手立てが無い。検査は呼び出しごとに入力が自由に変わる世界で行うので、実際には起きない並びを反例に出すことがある(E127 の例で、`支払額` が 0円 と 1円 に分かれる)。§15.149 の `held` で直した。
- 二つ目の `examples` の節が、一つ目を黙って置き換える。一つ目の外れた例が報告されない。§15.149 で直した。
- `inputs` の型の無い行(`b(b)`)が、何も言わずに捨てられる。§15.149 で直した(E057)。
- 規則の別名が Python の標準ライブラリの名前(`signal`)だと、生成した Python のモジュールがそのライブラリを隠す。W121 はキーワードしか見ていない。§15.149 で直した(Go・Ruby・Java も)。
- プレイグラウンドの `rulec.wasm` が 633 KB から 735 KB(gzip)に増えた(§15.149 のあとは 750 KB)。
**捨てたもの**:
- **状態遷移の別の言語を作ること。** 検査・生成・差分・過去再生を、もう一揃い作ることになる。二つの言語のあいだの対応も検査しなければならない。表の検査がそのまま効くのは、遷移を表の行として書くからである。
- **一般の時相論理。** 上の 3 のとおり。
- **状態に数を持たせること。** 上の 5 のとおり。範囲の両端のある数と真偽と別の列挙を、状態と一緒に持ち越すことは §15.217 で入れた。
- **状態を持つオブジェクトを生成すること**(`step()` を持つクラス)。P3 と衝突し、過去再生の意味が崩れる。
- **遷移を一つずつ通すだけのベクタ。** 上の 10 のとおり。
- **SQL と Wasm に定数を置くこと。** 入口が一つなので、置けば入口の意味が変わる。綴りは `rulec api` が持つ。
- **証明書の遷移を、実現した点から数えること。** 区画の探索を再検査器にもう一度させることになる。行の箱から読めば、再検査は集合の閉包を見るだけで済む。
### 15.149 黙って落ちていた三つと、案件のあいだ変わらない入力(2026-09-25)
**きっかけ**:§15.148 を作る途中で、ステートマシンとは別の既存の欠陥が三つと、ステートマシンの検査の限界が一つ見つかった。どれも、書いた人の書いたものが黙って落ちるか、起きない反例が出るという形をしている。それぞれどこまで直すか。
**決定**:
1. **`examples` の節を二つ以上書ける。** 二つ目の節が一つ目を黙って置き換え、一つ目の外れた例が報告されなかった。`inputs` と `outputs` の節は、重ねて書けば足し合わされる。`examples` も同じ読み方にした。節ごとに見出しを持ち、どれも実行される。公開された計算例を一つの節に、人が足した例を別の節に書き分けられる。
2. **型の無い宣言は E057。** `inputs`・`outputs`・`elements` の行に名前だけがあると、その行は黙って捨てられていた。規則は、書いた人の思うより入力が一つ少ないまま検査を通った。語で始まらない行は、ファイルの一番外側と同じく E004 にした。
3. **規則の別名は、標準ライブラリのモジュールの名前とも比べる(W121)。** 規則の別名は、関数の名前であるだけでなく、Python ではファイルの、Go ではパッケージの、Ruby ではモジュールの名前になる。`time` を別名にした規則では、Python は生成した `time.py` を標準の `time` の代わりに読み、Go は `import "time"` がどちらを指すか決められず、Ruby は `module Time` を定義できなかった。`signal` では、Connect の ASGI の側だけが uvicorn の中で落ちた。Java では、生成したファイルが `import java.util.List` を書くので、`list` を別名にするとコンパイルが通らなかった。どれも生成して走らせて確かめた。
- 登録簿の各バックエンドに、標準ライブラリの名前の一覧と、別名からモジュールの名前を作る関数を足した。一覧は、Python は `sys.stdlib_module_names`(3.14、非公開のものを除く)に 3.12 と 3.13 で消えたもの、Go は `go list std` の先頭の要素、Ruby は `date` と `json` を読み込んだあとの最上位のクラスとモジュールである。
- Java は、生成したファイルが読み込むクラスと、参照する `java.lang` のクラスを、既存の「最上位で使われている名前」に足した。
- 列挙の別名はモジュールの中の型の名前になるだけなので、モジュールの名前とは比べない。Ruby で列挙の別名を `date` にしても壊れないことを確かめた。
4. **`held` で、案件のあいだ変わらない入力を宣言する。** 検査は、呼び出しごとに入力が自由に変わる世界で行っていた。そのため、注文の金額を途中で変える並びのような、実際には起きない並びが反例に出た。`machine` の節に `held <入力>, …` の行を足した。
- 語は、はじめ `fixed` にした。すると、英語で `enum 種別 = percent | fixed` と書いた既存の試験が E009 で止まった。値引きの種類を「率」と「定額」で分けるのはありふれた書き方で、その値の名前を予約語で取り上げることになる。`held` はもともと予約語(`fold` の `exhausted -> held`、ここまで持ってきた値)で、新しく取り上げる名前が無い。意味も「一歩をまたいで持ち続けるもの」で通じている。
- 検査は、固定した入力をその入力の列の座標で分け、座標ごとの**世界**を別々に歩く。一つの案件の呼び出しは、同じ世界の中だけで続く。
- 世界ごとに、最初に見つかった呼び出しの値を代表にし、その世界のほかの呼び出しも同じ値で作り直す。答えが変わらなければ、作り直したほうを使う。固定した入力をほかに読むものが無ければ、答えは必ず変わらない。だから反例の並びも手順のベクタも、固定した入力を一つの値のまま渡す。
- 計算した列や制約も固定した入力を読むときは、ある値が許す呼び出しを、同じ座標の別の値が許さないことがある。悪い状態に着かないという主張は、座標の中のどの値の呼び出しも含めて歩いているので成り立つ。終わりに着けるという主張は W127 にし、途中で値を変えないと通れない反例も W127 にする。
- `scenario` が固定した入力を途中で変えれば E055 にした。過去の記録が変えていれば、その案件を `replay` と `diff` でエラーにする。`held` の形の誤り(入力でない名前、持ち越す入力、同じ名前を二度)は E056 にした。
- 証明書は、遷移を決める表が列として読む固定した入力で世界を分け、主張を世界ごとに行へ載せる。世界は、それらの列の座標のすべての組み合わせを一つずつ。二つの再検査器は、世界がすべてそろっていることを確かめてから、世界ごとに、その座標を取らない行を外して調べる。Lean では、世界を行を外した機械として定義し、世界の案件の呼び出しと状態がその機械の呼び出しと状態であることを証明した(`stepIn_world`、`reachesIn_world`)。表が入力でない列を読むか、制約が固定した入力を指すときは、終わりに着く呼び出しの並びを証明書に載せず、再検査器はそれを言う。
- 書き下ろしの `注文の状態.rule` に `held 支払額` を足した。二度返金する反例は、同じ支払額のまま四回呼ぶ並びになった。
**走らせた**:
- 小口の申込はすぐ承認し、承認のあとの変更で金額が閾値を超えれば審査に回す規則で確かめた。`held` が無ければ E124 と E126 が出る。`held 申込額` を書けば通り、変更の行が W126 になる。注記は「その行が当てはまる状態には着くが、その行が要る金額のままでは着かない」と言う。
- この規則の証明書は 5 つの世界を持ち、Python と Lean の両方の再検査を通る。偽った三通り(世界を別の世界と重ねる、到達集合から状態を落とす、道の呼び出しを別の世界の金額で作る)は、どちらも受け付けない。
- `held` の無い規則の検査結果、手順のベクタ、網羅の義務、差分は、前と変わらない。
- 別名を `time`・`types`・`list`・`json`・`signal` にした規則を生成して `rulec test` で走らせ、どの言語が壊れるかを確かめてから一覧を足した。
**ついでに直したもの**:
- 参照文書の予約語の節が「名前にも別名にも使えない」と書いていた。E009 が見るのは名前だけで、別名は見ない。コーパスには `額引き(fixed)` があり、`fixed` を予約語にしても別名なら通る(上のとおり、結局 `fixed` は予約語にしなかった)。別名は W121 が見ると書き直した。
- §15.148 の欠陥:`final` の行が列挙の順と違う順で状態を並べると、手順のベクタの期待値は行の順で終わりの状態を並べ、各言語の走らせ役は列挙の順で並べたので、全言語で食い違った。コーパスの規則は列挙の順に書いてあったので見つからなかった。期待値も列挙の順にした。上の申込の審査の規則を全言語で走らせて見つけ、試験に残した。
**見つけて直していないもの**:
- W121 は大文字と小文字を区別せずに比べるので、Ruby の一覧は `io` のような別名も言う。Ruby の定数 `Io` と `IO` は別物である。
- 固定した入力が遷移を決める表の列でないときは、証明書は世界を分けない。その表の遷移は値によらないので、主張は分けなくても成り立つ。ただし `once` の数える値が固定した入力から計算されるときは、どの値でも数えるものとして読む。
**捨てたもの**:
- **二つ目の `examples` をエラーにすること。** 見出しの違う例は、一つの表には書けない。
- **固定した入力を、座標ではなく値ごとに歩くこと。** 座標の中の値は無数にある。計算した列が読むときに要るのは、値ごとに許される呼び出しを求める量化子の消去で、今回は入れない。
- **型の無い宣言を `bool` などと読むこと。** 黙って型を決めるのは、黙って捨てるのと同じ種類の誤りである。
### 15.150 PaymentIntent を転記する。同じ名前の値と、予約語の値(2026-09-25)
**きっかけ**:§15.148 のステートマシンは、書き下ろしの規則でしか試していなかった。運賃表や法令では、公開されている表を転記するたびに、言語の足りないところが見つかってきた。状態遷移でも、公開されている仕様を一つ転記すると、仕様の穴が見つかるか、言語の足りないところが見つかるか。題材には、状態と遷移の条件が文書になっている Stripe の PaymentIntent を選んだ。読んだのは、ライフサイクルのページ、状態の確かめ方のページ、confirm・cancel・capture の API リファレンス、与信の保留のページ、3D セキュアのページである(2026-09-25 に読んだ)。
**転記したもの**:`tests/corpus/payment_intent.rule`。
- 状態は Stripe の七つ、出来事も七つ(支払い方法を付ける、confirm、顧客の認証、あとで届く結果、capture、cancel、与信の期限切れ)。遷移を決めるのは一枚の表で、行は 28。出力は、次の状態のほかに、資金の動き(`untouched | held | captured | released`)と、その状態がその出来事を受け付けないこと(`refused`)。
- `held capture_method, confirmation_method`。どちらも作ったときに決まり、一つの案件のあいだ変わらない。
- 主張は、`succeeded` と `canceled` で終わること、`canceled` のあとに `succeeded` に着かないこと、`funds captured` が一回までであること、どの状態からでも終われること。どれも成り立つ。
- 文書に書かれている流れを六つの `scenario` にした。3D セキュアを通るカード、ホテルの与信と確定、与信の期限切れ、口座振替が失敗してカードで払い直す、手動の confirm、confirm の上限である。
**文書について見つかったこと**(Stripe に聞かないと決まらないもの):
1. **認証のあとに `processing` を通るか。** 3D セキュアのページは、認証が通ると Stripe が課金を試み、PaymentIntent は `processing` に移ると書く。カードでもそう書いている。ライフサイクルのページと状態のページは、`processing` を「あとで結果が分かる支払い方法」のときだけの状態としている。3D セキュアのページの読みを `clause` にして足すと E105 が三つ出て、うち二つは答えが違う(手動の確定なら `requires_capture` と `processing`、自動なら `succeeded` と `processing`)。規則はライフサイクルのページに従い、表の上の注記に残した。
2. **どのページも決めていない組み合わせ。** 表の完全性が、状態と出来事の組み合わせを全部埋めさせる。三つはどのページにも書かれておらず、仮に決めて行末に書いた。`requires_confirmation` で支払い方法を付け直すと同じ状態にとどまる、`requires_action` で支払い方法を付け直すのと confirm はエラーになる、とした。confirm の上限を超えたとき、呼び出しが誤りを返すかどうかも書かれていない。
3. **capture で `processing` に移る支払い方法。** ライフサイクルのページは「支払い方法によって `processing` か `succeeded`」と書くが、どの方法かは書いていない。与信の保留のページは、口座振替では与信の保留ができないと書く。表は「あとで結果が分かる方法なら `processing`」と読んだ。
4. **`processing` での cancel。** cancel の API リファレンスは「まれに」と書き、ライフサイクルのページは口座振替の六種と、取り消せる時間枠が限られていることを書く。食い違いではないが、どちらからも条件は決まらない。受け付けられたかどうかを入力にした。
5. **`capture_method=automatic_delayed`。** 与信の保留のページにだけ書かれていて(非公開のプレビュー)、オブジェクトの列挙には無い。転記しなかった。
**rulec について見つかって直したもの**:
1. **二つの列挙に同じ名前の値を書けなかった。** Stripe の `capture_method` と `confirmation_method` は、どちらも `automatic` と `manual` を持つ。検査は値の名前だけで列挙を引き、ハッシュ表の順で最初に見つかった列挙の値と読んだ。そのため、もう一方の列のセルに誤った E103 が出た。生成器も値の名前だけで引く表を持っていて、後から宣言した列挙の型で値を書いた。検査が通る形にしても、Python は `cap == Confirmation.AUTOMATIC` と書き、どの行にも当たらなかった。
- 値は、書かれた場所の列挙で読む。セルなら列の、出力のセルなら出力の、`machine` の行なら持ち越す列挙の値である。場所が列挙を決めないときは、宣言順で最初にその値を持つ列挙を取る。検査は列挙の宣言順を持つようにした。
- 生成器の値の表は「列挙と値」の組で引く。値を書く関数(`py_value` など八つ)は型を受け取り、呼び出しはどれも文脈の型を渡す。
- W111 は、使われた値を「列挙と値」の組で数える。片方の列挙の `automatic` を書いても、もう片方の `automatic` は使われたことにならない。
- 群は、一員をすべて値に持つ列挙に属する。そういう列挙が二つあるときは、最初に使われた列の列挙に属し、もう一方の列挙の列で使えば E103 にした。Java は群をその列挙の型のリストとして出して `contains` で調べるので、取り違えるとコンパイルは通るのに、黙って当たらなくなる。
2. **出力のセルに別の列挙の値を書いても通っていた。** 入力のセルは列の列挙で調べていたが、出力のセルは「どれかの列挙の値か」しか見ていなかった。`size` の列に `red` と書いても検査を通り、生成コードは `Color.RED` を返した。E103 にした。群に二つの列挙の値を混ぜたときも、最初の一員の列挙として黙って読んでいたので、E103 にした。
3. **予約語を列挙の値にできなかった。** 資金の状態の `held`(与信で押さえた資金)は Stripe の文書の言い方そのままだが、`held` は予約語である。ステートマシンでは、`initial`・`final`・`next` のような語が状態の名前に来やすい。E009 の理由は、行の頭に来る名前が節の始まりと読まれることで、値は行の頭に来ない。値については、値の位置で別の意味に読まれる 11 語だけを受け付けないことにした。セルの語(`not` `none` `true` `false` `starts_with`)、`enum` の行で値のあとに付く `default`、`never` の行を分ける `after`、`fold` の行の頭に来る `empty` `exhausted` と、行き先を分ける `by` `with` である。名前では今までどおり、予約語をどれも受け付けない。
4. **`held` の世界をまたぐ遷移の対が、手順のベクタに入らなかった。** 手順のベクタは、遷移ごとに代表の呼び出しを一つ選び、対の二つ目をその代表の世界の中でしか探していなかった。エラーになった confirm はどの世界でも起きるが、そのあとの与信の保留は `manual` の世界でしか起きない。代表が `automatic` の世界のものだと、その対の手順は作られない。網羅の義務はすべての世界で対を数えるので、`rulec coverage` が 204 件中 11 件足りないと言って落ちた。対ごとに、二つとも起きる世界を探すようにした。代表の世界を先に見るので、これまで作れていた手順は変わらない。
**書けなかったもの**(直していない。今の線の内側にあるもの):
- **起きない組み合わせにも行が要る。** `settle` は `processing` でしか届かないが、ほかの状態にも行を書いて `refused` を立てた。`constraint`(§15.55)は順序のある型の二つの入力の関係しか言えず、列挙の組み合わせを「起きない」とは言えない。
- **一回の試行のあいだだけ変わらない入力。** あとで結果が分かる支払い方法かどうかは、試行ごとに変えてよいが、一回の試行の中(confirm から認証まで)では変わらない。`held` は案件の全体にしか言えないので、試行の途中で方法を変える並びも検査は歩く。今回の主張には効かなかったが、起きない並びを通って「終われる」と言ってしまう余地はある。
- **出力が三つある遷移の表に、別の出典の節を足せない(E045)。** 同じ出力を二つの定義で書くには、表の出力が一つでなければならない。ステートマシンの表は、次の状態のほかにその遷移の効果も出すのが普通で、別のページの読みを `clause` で足して比べるには、表を出力ごとに分けることになる。上の 1 の E105 は、出力を一つにしたコピーで確かめた。
**走らせた**:
- 規則は、`rulec test --require-all` の 36 通りがすべて一致した(ベクタ 92 件と、手順の呼び出し 382 回)。網羅の義務は、行が 28/28、遷移とその対が 204/204。証明書は 6 つの世界を持ち、二つの再検査器を通る。Kani のハーネス 2 本も通る(5 秒)。
- 同じ名前の値を持つ規則と、予約語を値にした規則(`initial | held | review | final` と `next | stop | over | where | result | table`)を生成し、どちらも `rulec test --require-all` の 36 通りがすべて一致した。Swift は `case final` をそのまま受け、PHP はもともと `FINAL_` と書いていた。
- `tests/values.rs` に、同じ名前の値、出力のセルの E103、列挙ごとの W111、群の二通り、予約語の値と、11 語の E009 を置いた。`tests/machine.rs` に、片方の世界でしか続かない対の試験を足した。直す前は落ちることを確かめた。
**捨てたもの**:
- **同じ名前の値をエラーにすること。** 値の名前は呼び出し側が送るものである。名前を変えさせると、Stripe の `automatic` を規則の中では別の名前で書き、呼び出し側で訳すことになる。
- **群に列挙を書かせること**(`group g : 列挙 = …`)。使った列で決まるなら、書かせる必要がない。
- **名前も予約語にしてよくすること。** 名前は、式、列の見出し、`fold` と `count` の行など、語で区切る場所に値より広く出てくる。一つずつ確かめる手間に見合わないので、今回は値だけにした。出力を `next` と名づけると、今までどおり E009 になる。
### 15.151 計算した値を二通りで試す。丸めの同着の義務を規則から立てる。adapter の答えを JSON として読む(2026-09-25)
**きっかけ**:ステートマシンの呼び出しの並びを、状態を自分で持つ実装(注文の行をデータベースに持つアプリ)に流し、観測を fixtures に書いて `replay` で突き合わせると、判定はそのままできる。ところが、取消済みの注文に取消依頼が来るともう一度返金する欠陥がすり抜けた。支払額をそのまま返金する行は、単一のベクタでも支払額 0 円の一件でしか試されていない。一度も返金しない実装を adapter にして `verify` にかけると 100% 一致し、`coverage` は六基準とも満たすと言う。ステートマシンに限らない。条件の列が二つあり、金額から計算する行が先頭でない三行の規則でも、常に 0 円を返す実装が 11/11 で一致した。行カバーは一点で満たされ、生成器がその行に着く点はどの範囲も下端にある。同じ規則で、丸めの同着の義務は 0/1 ではなく 0/0 だった。§9.2 は「義務は規則から静的に列挙する。踏めなかったものが必要なかったことになるから」と決めていたが、同着だけは生成器が baseline から探して見つけたときにしか義務にならなかった。もう一つ、`verify` は adapter の答えを JSON として読まず、`"名前":` を文字列として探していた。Python の `json.dumps` の既定の書き方(`\u` のエスケープ)で答えると、正しい実装が 0% になる。検査の側が「ここまで試した」と言う中身を、どう正しくするか。
**決定**:
1. **七つ目の基準「計算値の対カバー」(JSON では `value_pair`)を足す。** 義務は、出力のセルに名前を書いた行ごとに一つ。ただし、その名前の計算のもとを、行が自分のセルで全部一つの値に決めているなら義務にしない。`| 受付 | 出荷 | 状態 |` は状態を返すが、同じ行が状態を受付と決めている。満たすのは、その行に当たり、入力が一つだけ違い、値も違う二つのベクタである。定数を返す実装も、その入力を無視する実装も、二つのどちらかで答えを外す。
2. **値は、実装と突き合わせるときの形で比べる。** 規則の出力なら丸めたあと、途中の列なら、その列と規則の出力のどれかがともに動くこと。丸める前の値で比べると、3.49% の 1 セントと 2 セント(丸めればどちらも 0 セント)の対で満たしてしまう。`クーポン割引` の `min(素割引, 上限額)` のように、途中の値が動いても上限額 0 円で出力が動かない対も、同じ理由で数えない。
3. **表を通らない出力も義務を持つ。** `define` や `result` で計算する出力は、どの行にも無い。`手数料 = 金額 × 3.49%` だけの規則は義務が一つも立たず、ベクタが 0 件だった。`test` も `verify` も何も比べずに通っていた。出力ごとに一つの義務にし、対はどこにあってもよい。
4. **丸めの同着の義務を規則から立てる。** 丸めを宣言した出力には、どれも義務が立つ。立てないのは、どの入力も同着に届かないと規則から示せたときだけである。示し方は刻みの計算である。値を行ごと・定義ごとに `Σ aᵥ·v + k`(各 v は自分の刻みの倍数)に展開すると、取りうる値は k に、刻みと各 `aᵥ·刻みᵥ` の最大公約数の倍数を足したものに限られる。同着(刻みの半分ずれた値)がそこに無ければ届かない。表の出力の列は行のセルごとに分け、`min`・`max` は両側に分ける。`allocate` と丸めの関数は、その刻みに載る新しい変数に置く。名前どうしの積は、二つの刻みの積を刻みとし、両端の積を範囲とする変数に置く。変数が一つの形は、宣言した範囲も入れて合同式を解き、範囲の中に解があるかで決める。範囲を捨て、積の届く値を広めに読むので、届かないと言ったものは本当に届かない。示せず、見つけてもいない同着は義務として残り、欠けとして出る。
5. **`result` の値を監査の束縛に加える。** `result` で書いた最初の出力は束縛に無く、以前の探索は同着を探すことすらしていなかった(`送料` など 4 本)。いまは、その 4 本とも届かないと示せる(基準の送料 × 負担率は必ず 10 円の倍数)。
6. **生成器は、その義務を狙う。** 計算値の対は、行の点、その点の数を範囲の上端や真ん中に寄せた点、それまでに作った候補、の順に入力を一つ動かして探し、見つからなければ行の点から二つ動かす。同着は、baseline からの傾き(前と同じ探し方なので、前に見つかっていたベクタは変わらない)、一次式に直せる出力では合同式を解いた値、行ごとの点からの傾き、数の入力を一つ動かした点からの傾き、の順に探す。`amount × 3.49% + 49USDc` が半セントになる金額は一万通りに一つで、傾きをたどっても着かない。
7. **`verify` は adapter の行を JSON として読む。** 握手、答え、`err`、`id` を JSON から取り、出力の値は `report::wire` と同じ形に直して比べる。任意の出力の `null` は `none` として比べる(ベクタがそう書いている)。JSON でない行、別の件の `id` を持つ答え、`out` も `err` も無い答えは、何件目かを示して exit 2 で止める。要求の側も、ベクタの JSON から `"in":` を文字列で切り出すのをやめ、入力のオブジェクトをそのまま書く。
8. **生成する Rust の JSON の読み取りを直す。** 走らせ役と Wasm の入口が共有する読み取りは、`\u5165` を `u5165` と読み、`\r` を `r` と読んでいた。JSON のエスケープを全部解く(サロゲートペアも)。Wasm の入口は、無い入力、整数でない数、true でも false でもない値、`YYYY-MM-DD` でない日付を `{"error":…}` で返す。以前は、数なら 0、真偽なら false として計算し、日付ではトラップしていた。単価と個数の積の規則に、Python の既定の `json.dumps` の書き方で `{"個数": 3, "単価": 1000}` を渡すと、キーが見つからず、`{"in":{"個数":0,"単価":0},"observed":{"金額":0}}` が黙って返っていた。0 が範囲の外にある入力(送料の重量)では、範囲の外としてエラーになっていただけである。走らせ役は rulec 自身が書いたベクタしか読まないので、エスケープの直しだけにした。
**走らせた**:
- コーパス 50 本すべてで七基準を満たした(`coverage` を全部回して 2.6 秒)。計算値の対の義務は 28 本に計 45 件、丸めの同着の義務は 14 件。ベクタの総数は 4,913 件から 4,894 件で、増えずに少し減った。新しい義務を果たすベクタ(数を範囲の上端に寄せた点など)が、ペアワイズの足していた組み合わせも埋めるからである。
- 一度も返金しない実装は、`verify` で 41 件中 1 件が食い違う(支払額 100 万円の取消で返金額 +1,000,000)。常に 0 円の手数料は 11 件中 2 件。
- 前は黙って落ちていて、新しく立って満たした同着の義務は 6 本。`クーポン一枚`(公開規約に記載がなく切り捨てに仮置きした丸めが、これまで一度も試されていなかった)、`会員特典`、`parcel_rate`、`paypal_fee`(合同式を解いて見つけた)、`uk_income_tax`、`決済手数料` である。届かないと示せて義務にならないのは、`厚生年金保険料`(等級は 2,000 円の倍数、料率は 0.1% の倍数)と、`result` で書いた 4 本。
- `\u` でエスケープして答える adapter は、ゆうパック運賃で 100% 一致する(前は 0%)。任意の出力を `null` で返す正しい adapter は 4 件中 4 件一致する(前は 0 件)。Wasm の入口に Python の既定の書き方で渡した入力は、書き出した入力と同じ答えになる。
- 送料の規則の Wasm モジュールは、入力の読み取りを厳しくして 41 KB から 44 KB になった。プレイグラウンドの `rulec.wasm` は gzip で 756 KB から 777 KB。
- verify と Wasm の入口の新しいテストは、直す前の版で落ちることを確かめた。coverage の側は直す前の版に無い基準を使うので、同じ規則を直す前の版の `coverage` と `verify` にかけて確かめた(同着 0/0、常に 0 円の実装が 11/11)。
**見つけて直していないもの**:
- 手順のベクタ(呼び出しの並び)は、遷移とその対を覆うが、状態×出来事の組み合わせは覆わず、`held` の入力は世界ごとの代表の一つ(多くは 0 円)でしか試さない。生成コードの受け渡しを確かめる目的には足りるが、状態を自分で持つ実装を並びで試すには足りない。
- 変数が二つ以上の形は、範囲を入れずに同着に届くかを決める。範囲の外にしか同着が無い出力は、義務が残って欠けとして出うる。コーパスには無い。§15.152 で直した。
**捨てたもの**:
- **同着の義務を、見つかったときだけ立てる形。** §9.2 の決め事に反する。見つけにくい同着ほど、試されないまま緑になる。
- **計算値の対を、行と、値が依存する入力の組ごとに立てること。** 対は倍に増える。定数を返す実装を捕まえる力は、一つの対で足りる。入力ごとの依存は、境界の両側とペアワイズがすでに踏んでいる。
- **Wasm の入口で、無い任意の入力を `none` と読むこと。** 綴りの誤りと区別がつかない。`null` と書けば `none` になる。
### 15.152 同着と計算値の義務を、範囲と行の条件の中で決める(2026-09-25)
**きっかけ**:§15.151 は、変数が二つ以上ある形の同着を、範囲を入れずに最大公約数だけで決めていた。二つの金額に 3.49% と 1.5% を掛けて足す規則で、どちらの金額も 10 セントまでなら、手数料は 0.499 セントを超えず、同着(0.5 セント)には届かない。それでも義務が立ち、欠けとして出た。同じ規則で、計算値の対も欠けた。手数料は丸めるといつも 0 セントなので、値の違う対は作れない。どちらも「試していないのに緑」ではなく誤った赤だが、書く人には直しようがない。丸めの宣言は外せず(E104)、同着に載る例も書けない。行の条件でも同じことが起きる。10 セント以下の行だけが 3.49% を返す規則では、宣言した範囲(1 万セントまで)なら同着に届くが、その行が当たるところでは届かない。範囲と行の条件を、どこまで判定に入れるか。
**決定**:
1. **変数が二つ以上の形も、範囲を入れて決める。** 一つを除いた変数を範囲の中で歩き、残りの一つを合同式で解いて、範囲の中に解があるかを見る。歩くのは、その変数の倍数が刻みの新しい余りを出すあいだだけで(その先は同じ余りの繰り返しになる)、解くのは範囲がいちばん広い変数にする。組み合わせが 2^20 を超えるときだけ、これまでどおり最大公約数で決める。
2. **行の条件で範囲を狭める。** 表の出力の列を行ごとに展開するとき、その行が入力の列に書いた数の条件(`<=10USDc`、`>=63000円 <73000円` など)で、その行の形の変数の範囲を狭める。二つの行から来た形を足すときは、同じ変数の範囲の共通部分を取る。導出の列の条件と、`policy first` で前の行に取られる分は入れない。入れなければ値を広めに読むだけなので、届かないと言ったものは本当に届かない。
3. **一つの値にしかならない計算値には、対の義務を立てない。** 値の取りうる区間を形から求め、規則の出力なら丸めた両端が同じとき、途中の列なら一点のとき、義務にしない。規則の出力がどれも丸めると一つの値にしかならないなら、どの計算値の対も出力を動かせないので、一つも立てない。
4. **生成器は、判定が見つけた点を使う。** 範囲の中で同着に届く組み合わせを判定が見つけたら、その点をそのまま試す。3.49% と 1.5% で 1〜120 セントなら、同着に届くのは国内 50 セント・国外 117 セント(1.745 + 1.755)の一通りだけで、傾きをたどっても、入力を一つだけ解いても着かない。
**走らせた**:
- 上の三つの形(二つの率、行で狭い、率を掛けた積の範囲)は、前の版ではどれも欠けとして出た。いまはどれも義務が立たず、`coverage` は緑になる。一通りだけの同着は、前の版では見つからずに欠けだったが、いまは見つかる。
- コーパス 50 本は、義務の件数もベクタも一つも変わらない(この形を持つ規則が無かった)。
- 新しいテストは、前の版で落ちることを確かめた。
**見つけて直していないもの**:
- 導出の列の条件(`三辺合計 <= 60cm` など)と、`policy first` で前の行に取られる分は、範囲に入れていない。そこで初めて届かなくなる同着や、一つの値になる計算値は、まだ欠けとして出うる。§15.153 で直した。
- 組み合わせが 2^20 を超える形は、最大公約数で決める。入力が少なければ、§15.153 の総当たりで決まる。
**捨てたもの**:
- **決めきれない同着を、警告に落として緑にすること。** §9.2 の決め事に反する。届くか分からないうちは欠けとして見せ、判定の届く形を広げる。
- **書く人に「届かない」と宣言させること。** 確かめられない主張を増やすことになる。確かめられる形を広げるほうを取った。
### 15.153 導出の列、前の行、`constraint` を判定に入れる。決めきれなければ総当たり(2026-09-25)
**きっかけ**:§15.152 は、同着と計算値の義務を行ごとの範囲の中で決めるようにしたが、範囲に入れたのは入力の列に書いた数の条件だけだった。二つの金額の合計が 60 円以下の行だけが 1.5% を掛ける規則では、合計 60 円までに同着は無い(1.5% が半円になるのは合計 100 円から)。それでも、入力それぞれの範囲(1,000 円まで)で判定して義務を立てていた。`policy first` で前の行が 10 セントを超える分を取る規則でも、後の行を宣言した範囲全体で見ていた。`constraint 国内 <= 国外` で国内が 10 セントまでに収まる規則でも同じである。どれも誤った赤で、書く人には直しようがない。入力の組み合わせで範囲が切られる場合を、どう判定に入れるか。
**決定**:
1. **導出の列を一つの名前として扱う。** 表が条件に使う導出の列のうち、式が入力の一次式のものは、式の項が載る刻み(各項の係数×刻みと定数の最大公約数)と、取りうる範囲を持つ一つの変数にする。その列に行が書いた条件は、入力の列の条件と同じようにこの変数の範囲を狭める。
2. **導出の列と `constraint` で、範囲を行き来させる。** 導出の列の範囲からその式の各項の範囲を逆算し、各項の範囲から列の範囲を求める。`a <= b` は a の上端を b の上端で、b の下端を a の下端で抑える。これを二巡する。消去(§15.126)を一段で止めたもので、名前ごとの範囲しか持たない箱にはこれで足りる。桁があふれた絞り込みは飛ばす。空の箱と取り違えると、行を捨ててしまうからである。
3. **`policy first` で前の行に取られる分を除く。** 前の行のセルのうち、いまの箱が満たしきっていないものが数の条件一つだけなら、その条件の区間の外がこの行の当たる範囲で、箱は一つか二つに分かれる。いまの箱が前の行のセルを全部満たすなら、その箱ではこの行は当たらない。満たしきっていないセルが二つ以上あるときは、その前の行は範囲に入れない。入れなければ値を広く読むだけなので、届かないと言ったものは本当に届かない。箱が 64 を超えたら、残りの前の行は入れない。
4. **決めきれなければ、入力が少ないときは全部を評価する。** 計算で示せず、出力が読む入力の組み合わせが 2^14 以下なら、全部を参照評価器にかけて、同着に載る入力があるか、入力が一つだけ違い値も違う対があるかを確かめる。見つからなければ義務は立たず、見つかればその点を生成器が使う。前の行が二つの列で条件を持つ規則のように、計算では範囲を切れない形もこれで決まる。
5. **格子の計算を `coverage.rs` から `grid.rs` に移した。** 規則ごとに一度作る `Grid` が、導出の列と、行ごとの箱を覚える。
**走らせた**:
- 導出の列の条件、`policy first`(金額 100 万セントまでで、総当たりは使えない)、`constraint`、前の行が二つの列で条件を持つ規則(6,001 × 2 通りを総当たり)の四つは、前の版ではどれも欠けとして出た。いまはどれも義務が立たず、緑になる。前の行が取るのが 1 万セントを超える分だけなら、5,000 セントの同着は後の行に残り、義務が立って満たされる。
- コーパス 50 本は、義務の件数もベクタも変わらない。
- 新しいテスト 4 件は、前の版で落ちることを確かめた。
**見つけて直していないもの**:
- 導出の列の式が一次でないとき(名前どうしの積など)は、その列の条件を範囲に入れない。(この項は不正確だった。§15.154 の訂正を見よ。)
- 箱は名前ごとの範囲しか持たない。導出の列の条件が入力を斜めに切るとき、箱に入っても斜めの外にある点は、届くものとして数える(義務が残る側)。導出の列を一つの名前にした値ならこれは起きないが、同じ式が値の中に入力のまま現れると、その相関は捨てる。入力の組み合わせが 2^14 を超えると、総当たりにも回せない。
**捨てたもの**:
- **消去(§15.126)で、行の当たる領域を多面体のまま持つこと。** 同着の判定は格子の点があるかどうかで、有理数の上の消去では決まらない(整数の変数を足しても、緩めた問題はいつも解ける)。名前ごとの範囲に落として、格子の上を歩くほうを取った。
- **総当たりの上限を大きくすること。** `coverage` と `gen` のたびに走る。2^14 なら一つの問いは 0.4 秒ほどで、前の行が二つの列で条件を持つ規則(12,002 通りを最後まで評価する)の `coverage` は、生成と監査が同じ問いを二度ずつして 1.7 秒だった。
### 15.154 真偽の定義の条件を、比べている数の範囲として読む(2026-09-25)
**きっかけ**:§15.153 の残りを確かめるうちに、同じ種類の穴がもう一つ見つかった。表の列に使える真偽の `define`(`大口 = 金額 >= 11USDc`)のセルを、判定が範囲として読んでいなかった。大口でない行だけが 3.49% を掛ける規則では、その行の金額は 10 セントまでで、手数料は丸めると必ず 0 セントになる。それなのに同着と計算値の対の義務を立て、欠けとして出していた。`policy first` で大口の行を前に置いた規則でも同じである。真偽の定義は `送料.rule` の `大口` のように実際の規則でよく使われるので、§15.153 で残した形より起きやすい。
**決定**:
1. **真偽の定義のセルを、比べている数の範囲に直す。** 表が使う真偽の定義のうち、一つの数(入力か、一つの名前として読む導出の列)を定数と比べるものが対象である。行のセルが true なら比較そのもの、false ならその否定で、`=` の否定は二つの箱に分ける。
2. **`policy first` の前の行がその列に持つセルも、同じように直す。** 数の範囲にして、数の条件と同じ扱いで除く。どの真偽も通さないセルを持つ行は、当たらない行とする。
3. **もう一つの形は読まない。** 差を導出にできない二つの値の比較(日付どうしなど)である。金額は日付から計算されないので、同着にも計算値にも関わらない。
**走らせた**:上の二つの規則は、前の版ではどちらも欠けとして出た。いまはどちらも義務が立たない。大口の線を 6,000 セントに上げると、5,000 セントの同着が行に残り、義務が立って満たされる。コーパス 50 本の義務の件数とベクタは変わらない。新しいテストは、前の版で落ちることを確かめた。
**§15.153 の訂正**:§15.153 の「見つけて直していないもの」に、導出の列の式が一次でないとき(名前どうしの積など)を挙げたが、導出の列の右辺は文法で入力の一次結合に限られる(§5.2)。一次でないのは `allocate` だけである。
**見つけて直していないもの**:
- 斜めに切る条件は、名前ごとの範囲でしか見ない。二つの金額の合計がちょうど 101 セントの行で、それぞれに別の率を掛ける規則では、線の上に同着は無く、入力を一つ動かすと行から外れるので計算値の対も作れない。それでも宣言した範囲が広いと総当たりに回せず、欠けとして出る。こういう形は業務の規則ではまれなので、残した。
### 15.155 突き合わせのページの verify の実演を、いまの出力に縛る(2026-09-25)
**きっかけ**:突き合わせのページ(英日)の verify の実演が「照合 207 件」のままだった。ゆうパック運賃のベクタはいま 209 件で、最初のクラスタのあとにもクラスタが続く。実演の前の `rulec adapter` と `rulec schema` は `送料.rule` を指していて、verify する規則と合っていなかった(送料のテンプレートでは、ゆうパック運賃の入力を読めない)。JSON の実演も、コマンドは `送料.rule` なのに出力はゆうパック運賃のもので、`records`・`moved`・`cases` が無かった。同じページの diff の実演は `tests/vdiff.rs` がいまの出力に縛っているのに(§15.73)、verify の実演を縛るテストは無かった。
**決定**:実演をいまの出力で取り直し(`tests/verify.rs` の偽の旧実装に `BUG=1`)、コマンドはどれもゆうパック運賃にそろえた。報告は最初のクラスタまで載せて、続きを「…」で示す。JSON は長い配列を「…」で省く。`tests/verify.rs` に、報告の頭の六行と、JSON をページの行の切れ目で分けた五つの部分が、英日のページにそのまま載っていることを確かめるテストを足した。直す前のページでは落ちる。
**直していないもの**:`fixtures lint` と記録つきの `diff` の実演は、リポジトリに無い記録のファイル(`replay/2025-08.jsonl`)の出力なので、取り直せない。そこに出る件数は記録の数で、ベクタの数とは関係がない。
### 15.156 壊れた規則にも、落ちず止まらずに答える(2026-09-26)
**きっかけ**:1.0 の前に、打ちかけや書き損じの規則を渡したとき、道具がどう振る舞うかを確かめた。コーパスの規則を行単位でランダムに崩して `check` に約 7 万件流すと、三つの形で道具が倒れた。崩れた `derive` の行は、同じ行を読み直し続けて終わらない(`derive` の一語だけでも)。`÷ 0` は、E115 を出したあと、報告を組み立てる段の評価器が 0 で割ってパニックする。`from any … where <フィールド>` で比べる値を書かないと、パーサが空のセルの先頭を読んでパニックする。プレイグラウンドは入力が 300 ミリ秒止まるたびにメインスレッドで検査を走らせるので、`derive` と打った時点でタブが固まる。MCP サーバは子プロセスを時間切れなしで待つので、一度止まると、そのあとの呼び出しにも答えなくなる。直す場所を読むうちに、同じ根のものがほかにも見つかった。読めない宣言の行を、何も言わずに捨てていたことである。
**決定**:
1. **パーサは必ず次の行へ進む。** 本体のループで、どの枝も行を読み進めなかったら一行進める。`derive` の読み手は、読めたかどうかに関係なく、最初に行を進める(`fold` の読み手には、前から同じ注意書きがあった)。
2. **宣言の語で始まる行を読めなかったら、必ず何か言う。** 行を読んだ結果、規則に何も加わらず、診断も一つも出ていなければ、E058(`<語>` の行を読めません)を出す。`define`・`derive`・`result` に名前が無ければ E058、`=` が無ければ E006、型が無ければ E057 にした。E006 の台帳の説明は前から `derive`・`define`・`result` を挙げていたのに、実装は黙って捨てていた。名前の無い `enum`・`group`・`sequence`、文字列の無い `description`、名前の無い `rule` の行(ファイルごと、中身の無い規則として通っていた)も止める。
3. **式は最後の語まで読む。** 読めない語が残れば E059(式を読めません)を出す。いままでは読めたところまでを式にして、残りを捨てていた。演算子を書き忘れた `金額 税` は `金額` と読まれて検査を通り、`doc` は書いたとおりの `金額 税` を見せていた。`金額 -100円` は、符号が数字にくっついて一つの負の数になり、同じく `金額` と読まれていた。この形には、間を空ければ引き算になると案内する。
4. **生成コードが 0 で割る道を塞ぐ。** 丸めの刻み(`round up(0円)`)、型の刻み(`rate[step 0%]`)、式の中の丸めの刻み(`down(x, 0円)`)が 0 以下なら E060。`round up(0円)` は検査を通り、生成した Python が実行時に ZeroDivisionError で止まっていた。刻み 0 の型は、黙って刻み 1 として読まれていた。丸めの呼び出しの刻みに名前を書くのは E118 にした(例が無ければ通り、その値が 0 になった呼び出しで割っていた)。`range >=10円 <=0円` のような空の範囲は E061 にした。完全性の証明は空の集合について「漏れなし」と答え、入口の検査はどの呼び出しも受け付けていなかった。`allocate` の全体の範囲が逆さだと、区間の計算が上端の 0 で割っていたので、上端も見る。
5. **E115 の見出しを「割る数が正の定数ではありません」にした。** 0・負の数・小数も前から E115 で止まっていたが、見出しが「変数では割れません」だったので、読む人は変数を探していた。
6. **一文字のフラグを、何かする前に止める。** `-o` や `-c` はファイル名として読まれ、前のファイルを処理してから「読めない」と言っていた。`rulec gen x.rule -o out` は既定の `generated/` に書き、`rulec fmt x.rule -c` は見るだけのつもりのファイルを書き換えていた。§12.1 のとおり、`-h` は `--help` として読む。知らないフラグには、一番近いフラグが一つに決まるときだけそれを添える(`-o` なら `--out`、`--outt` も `--out`)。
7. **MCP サーバの呼び出しに上限を付ける。** `rulec mcp --timeout <秒>`(既定 600 秒)。超えたら子プロセスをそのグループごと止め、失敗として答えて、次の呼び出しに進む。0 と 1 以外の終了(パニックの 101、シグナル)も失敗として返す。いままでは 2 のときだけだった。子の出力は一時ファイルに書かせる。`rulec test` が起こしたコンパイラが親より長く生きると、パイプではそれを待ってしまうからである。
8. **JSON の入れ子を 256 段までにする。** 2 万段ほどでスタックがあふれてプロセスごと落ち、MCP サーバ本体も、要求一つで落ちていた。
9. **同じ崩し方をテストにした**(`tests/robustness.rs`)。コーパスの宣言の行(表の行は除く)を、語の区切りごとに切る、消す、二重にする、数字の並びを一つずつ 0 にする、の 6,779 通りを CPU の数だけのスレッドで検査にかけ、パニックせず 60 秒以内に答えることを求める。デバッグビルドで 5 秒ほどかかる。書いた日に、上の `allocate` の 0 割りを見つけた。見つけた形は、それぞれのコードで止まることも一つずつ確かめる。
**走らせた**:最初に見つけた再現例は、どれも誤りを返して 0.01 秒で終わる。プレイグラウンドの wasm を作り直し、Node で `derive` だけの行を渡すと 4 ミリ秒で答える。MCP で、答えない偽の実装(`sleep 30`)を `verify` に渡すと、`--timeout 1` で止まって次の呼び出しに答え、孫プロセスも残らない。コーパス 50 本の診断と生成物は変わらない。変異は二本が変わった。`m_e002` は、`policy` の行が字句の誤りで空になり、表が見出しだけで消えていたのを、E058 が言うようになった。`m_e110` は、生成スクリプトの awk が表のセルだけでなく `enum` の宣言の行まで値を引用符で囲んでいて、その行が黙って捨てられていたと分かった。スクリプトを表の行だけに当てるように直した。
**生成したコードまで走らせた**:崩した規則のうち検査を通ったもの 342 本について、コードを生成し、Python の走らせ役に自分のベクタを流した。検査を通ったのに、生成コードが落ちるか、参照評価器と違う答えを返したものが六本あり、原因は三つだった。直したあと別のシードで 600 本走らせると、もう二本出た(四つ目)。
- **名前の無い表。** `table` の後の名前は省略できる作りで(名前の無い表は、例や手順の表のために内部で使う)、`table -` の `-` も黙って捨てられていた。その表から生成したコードは出力の刻みを落とし、0.1% 刻みの率を掛けた手数料が 1000 倍で返った。文法は表に名前を求めているので、名前が無ければ E058、名前の後に語があっても E058 にした。行は読み進めてから止めるので、下の行が一行ずつ報告されることはない。
- **ありえない日付。** `2026-01-99` は形だけを見て読まれ、検査器は 1 月 1 日から数えて 4 月 9 日として扱い、生成した Python は実行時に止まった。JavaScript の `Date` なら黙って繰り越すので、言語ごとに答えが割れる。E062(ありえない日付です)にした。字句の誤りだが、行の残りは読めるように柔らかい誤りにした。
- **範囲の外の例。** 例の入力(並びの要素のフィールドも)が宣言した範囲の外でも、検査を通っていた。その例から作ったベクタを、生成コードは入口で受け付けない。例が制約を破るときの E019 と同じ理由なので、E019 を「例が、生成コードが入口で受け付けない入力です」に広げた。
- **表の行の書き損じ。** 別のシードでもう一度走らせると、表の行で二つ出た。最後の `|` を書き忘れた行は、最後のセルが黙って捨てられた。比較の後ろの語(`<=0円 + false`)も捨てられた。どちらも参照評価器と生成コードが行を別々に読み、答えが割れた。調べると、表の読み手には同じ形がほかにもあった。値の後ろの語(`a b` は `a`)、集合や `not:` の中の値でない語、読めないセルを飛ばしたせいで後ろのセルが一つずつ左の列へずれること、セルの数が見出しと違う行、名前の無い見出しの列である。セルを読み切れなければ E063(セルを読めません)、行が見出しと合わなければ E064(行が見出しと合いません)にした。読めないセルや空のセルのところには印を置いて、列がずれないようにした。例の表では、見出しに出力の列が無いか行の期待値が足りなければ E111、並びの列が無ければ E025 が、どの列かを示す。その二つが先に言えるように、例の表の行の数は、型付けの後で見出しが揃っていると分かってから見る。変異の `m_e026` は、並びの見出しから列を消して行には値を残していたので、E064 が先に出て E026 まで進まなくなった。行からも同じ列を消すように生成スクリプトを直した。
**捨てたもの**:
- 読めない行を警告にして続ける案。読めない行を飛ばして検査を続けると、書いた規則とは別の規則を検査することになる。
- MCP の上限を呼び出しの種類ごとに変える案。長くかかるのは `test` と `verify` だけだが、どちらも規則の数や相手の実装しだいで、決まった値が無い。上限を一つにして、変えられるようにした。
**見つけて直していないもの**:
- プレイグラウンドは、いまもページのメインスレッドで検査を走らせる。止まらない道は塞いだが、また入れば、タブが固まる。
- `fmt` は、構文の壊れたファイルも整えて 0 を返す。`check` が誤りを出すので、黙って通ることはない。
- 二つ目の `description` は、一つ目を黙って置き換える。
- 崩し方はコーパスの規則を行ごとにいじる形に限る。xlsx・docx・`.proto`・JSON Schema の読み手には、同じことをしていない。
- 崩した規則から生成したコードを走らせる確かめは、手元のスクリプトで Python についてだけ行い、テストには入れていない。一本ごとに 12 の言語をコンパイルすると重いからである。コーパスの規則の生成コードは、これまでどおり 12 の言語すべてをベクタに当てている(`tests/threeway.rs`、CI では `rulec test --require-all`)。
### 15.157 1.0 で壊さないものを書く(2026-09-26)
**きっかけ**:1.0.0 は、壊さないことを約束する版である。では何を壊さないのか。いままで書いてあったのは「診断コードと JSON の形は安定」(§11 原則 5)だけで、言語、生成コードの答えと呼び方、CLI、データの形式、MCP、Action については、どこにも書いていなかった。書き出してみると、約束と食い違う運用が三つあった。予約語は E009 で名前にも使えないので、語を足すたびに、その語を名前にしていた規則が落ちる(0.20.0 だけで八語増えた)。E025 は、番号に穴を空けないために意味を差し替えた(§15.56)。証明書は形式の版を持たないので、形を変えたとき、rulec と無関係の再検査器は古い形のつもりで読んでしまう。
**決定**:`docs/compatibility.md` と日本語の `docs/compatibility.ja.md` に書き、サイト・README・AGENTS.md・MCP のリソースからたどれるようにした。
1. **変えないもの**:`.rule` の言語(ステートマシンを含む)、名前に使える語、生成コードの答え、生成コードの呼び方(`rulec api` が説明するものすべて)、CLI(コマンド・フラグ・値・終了コード)、診断コード、機械が読む形式(`--format json` の出力、ベクタ、fixtures、マニフェスト、`adapter/1`・`extract/1`、証明書、MCP のツールと引数とリソース、Action の入力、リリースのアーカイブの名前)。
2. **予約語は 1.0 の 65 語で固定する。** 後の版で言語に語は増やせるが、この一覧には足さない。新しい語は、名前が来ない位置でだけ読むか、宣言された同じ綴りの名前に譲る。
3. **コードの番号は使い回さない。** 廃止したコードは、廃止したと書いて台帳に残す。
4. **形式はフィールドを足すだけにする。** 読む側は知らないキーを読み飛ばす。`v` は形の版で、無ければ 1。証明書に `"v":1` を足し、`tools/recheck.py` と Lean のプログラムは、知らない `v` の証明書を受け付けないようにした(`v` が無ければ 1 として読む)。
5. **変わりうるもの**:文面。生成コードの本文(ヘッダに版が入るので、上げたら作り直す。CI は版を固定し、`@v1` のような動くタグは出さない)。落ちるべきだったのに通っていた規則(緑のままにはせず、リリースノートに書く)。参照評価器と食い違っていた答え。検査にかかる時間(既定の予算の中で終わっていた規則は、その後も終わる)。
6. **約束の外**:`src/lib.rs` の Rust の API、プレイグラウンドとサイト、`experiments/`。
7. **テストで見張る**(`tests/compat.rs`)。予約語の一覧(65 語と、値に使えない 11 語)は、増えても減っても落ちる。1.0 の診断コード 109 個と、20 のコマンドとそのフラグは、消えたら落ちる。
**捨てたもの**:
- 1.x で予約語を足すことを、約束の例外として書く案。E009 が語と直し方を言うので壊れ方は目に見えるが、1.0 で通った規則が 1.x で落ちることに変わりはない。
- 規則のファイルに言語の版を書かせる案(Rust の edition のようなもの)。新しい語を名前とぶつけない決まりで足りるうちは、書く人の手間を増やさない。
- `@v1` のような動くタグ。生成物のヘッダに版が入るので、タグが動けば、こちらが何も変えていなくても `gen --check` が落ちる。
- 証明書以外の出力すべてに `v` を足す案。「無ければ 1」と書けば足りる。証明書だけは、読むのが rulec と無関係のプログラムなので、知らない形をエラーにできるように持たせた。
**見つけて直していないもの**:
- リリースノートはまだ定型文だけで、「答えが変わる修正」と「緑が赤になる修正」を書く場所が無い。1.0 までに要る。
- サイトは main への push で配信されるので、1.0 のあとは、まだ出ていない機能の説明がサイトと SKILL.md に先に載る。
- 記録は、書いた規則の版を持たない(§15.145)。読む側が知らないトップレベルのキーを無視するので、1.x で足しても互換である。
### 15.158 パッケージマネージャから入れる(2026-09-26)
**きっかけ**:入れる手段は三つだった。リリースのアーカイブを手で取って `SHA256SUMS` と突き合わせる、Rust の toolchain で `cargo install --path .` する、CI で Action を使う。どれも正しいが、macOS で Homebrew、Linux で apt や dnf を使う人にとっては、ふだんの入れ方ではない。更新も削除も、そのパッケージマネージャに任せられない。§12 は pip と npm のラッパを、§15.42 は Windows を「要望が出てから」とした。いまのリリースの形(四つの静的バイナリと `SHA256SUMS`)を変えずに、同じバイナリを同じ確かさで届けられるのは、どのパッケージマネージャか。
**決定**:Homebrew と、`.deb`・`.rpm` の二つを足し、crates.io は用意だけして保留した。どれもリリースのアーカイブと同じバイナリを届け、アーカイブの名前は変えない。
1. **Homebrew。** 作者の tap(`i2y/homebrew-tap`)に formula を置く。入れ方は `brew install i2y/tap/rulec`。formula はビルドせず、その環境向けのアーカイブを取って `sha256` で固定するだけである。macOS と Linux、arm64 と x64 の四つを `on_macos`・`on_linux`・`on_arm`・`on_intel` で分ける。`packaging/homebrew.sh` が、公開した `SHA256SUMS` から formula を書く。release.yml は、書いた formula を macOS と Linux の runner で `brew audit --strict --online`・`brew install`・`brew test` にかけ、両方が通ってから tap へ push する。push には、tap のリポジトリにだけ書ける deploy key を使う(secret は `HOMEBREW_TAP_KEY`)。formula のテストは、`--version` を見たあと、二つの値を持つ enum の規則が検査を通ることと、その行を一つ消した規則に `check` が穴の入力(`dest = overseas`)を返すことを見る。
2. **.deb と .rpm。** Linux の二つのバイナリを nfpm で `.deb` と `.rpm` にして、アーカイブと並べて置き、同じ `SHA256SUMS` に載せる。入るのは `/usr/bin/rulec` とライセンスの二つだけで、依存は無い。`packaging/linux.sh` がパッケージを作り、Debian と Fedora のコンテナで、入れる・`--version`・コーパスの検査・消す、を通してから残す。このスクリプトは、release.yml の Linux の二つのジョブと、ci.yml が push ごとに走らせる `packages` ジョブの両方で走る。nfpm はイメージのダイジェストで固定した。
3. **crates.io(保留)。** クレート `rulec` として出す用意をした。送るのは `src/`、`rulec mcp` が埋め込む文書六つ、README とライセンスだけで(`include`)、テスト・サイト・DESIGN.md は送らない。ci.yml の `packages` ジョブが push ごとに `cargo package` を走らせ、送る中身だけでビルドできることを確かめる。保留の間もこれは続け、出すときに中身がずれていないようにする。`[package.metadata.binstall]` を書いたので、出せば `cargo binstall rulec` はビルドせずにリリースのアーカイブを取る。Linux には musl の静的バイナリしか無いので、glibc の target も musl のアーカイブに向けた。`src/lib.rs` の API が約束の外であること(§15.157)は、クレートの説明にも書いた。公開は release.yml の `crates` ジョブが trusted publishing で行い、トークンをどこにも置かない形に書いた。その版がもうあれば、ジョブは何もしない。ただし crates.io は最初の版を手で出すことを求め、それはこの決定の範囲に入れていない。そこでジョブをコメントにして止め、README と install のページの Cargo の節、互換の約束に書いたクレートの名前もコメントにして残し、スキルの冒頭からは `cargo install rulec` を外した。出すときは、最初の版を手で出し、crates.io に trusted publisher(i2y/rulec の release.yml)を登録してから、これらを戻す。
互換の約束(§15.157)の「リリースのアーカイブの名前」に、パッケージの名前(`i2y/tap/rulec`、`.deb` と `.rpm`)を足した。クレートの `rulec` は、出すまでコメントにしてある。
理由。三つとも、いまのリリースをそのまま材料にできる。新しいバイナリを作らず、アーカイブの名前も変えないので、Action、手で取る手順、互換の約束に書いたものは何も動かない。formula はアーカイブを `sha256` で固定するので、Homebrew から入れても、`SHA256SUMS` と突き合わせたのと同じ確かさになる。Homebrew は macOS と Linux の両方に効き、`.deb` と `.rpm` は Debian 系と Red Hat 系をまかない、crates.io は Rust の toolchain があればどこでも使える。
どの形も、公開か push の前に、そのパッケージマネージャで実際に入れて走らせる。書いたとおりに入るかどうかは、その道具に通すまで分からないからである。書いている間にも二つ見つかった。`brew audit` は、URL から読める版を `version` で重ねて書くと落とす。Debian の `sh`(dash)の `command -v` は、消したあとのコマンドも覚えていた場所から答えるので、消えたかどうかはファイルで確かめる。
**走らせた**:v0.21.0 の実物で確かめた。formula は手元の使い捨ての tap に置き、macOS(arm64)と Linux(arm64、`homebrew/brew` のコンテナ)で audit・install・test が通った。入ったバイナリは、アーカイブの中のものとバイト単位で同じである。`.deb` と `.rpm` は arm64 と x64(x64 はエミュレーション)で Debian と Fedora に入れ、コーパスを検査して、消した。binstall は `--manifest-path` で、Debian(glibc)と Alpine(musl)の arm64 に v0.21.0 のアーカイブを入れた。`cargo package` は 70 ファイル、圧縮して 966 KiB で、送る中身だけからビルドできた。
**捨てたもの**:
- **Homebrew の本家(homebrew-core)**。本家はソースからのビルドと、知られていること(GitHub のフォーク 30・ウォッチ 30・スター 75 のどれか、作者自身が出すならその三倍)を求める。いまのスターは 1 である。届いたら、`cargo install` でビルドする formula を出せばよい。依存が無いのでビルドは短い。
- **rulec だけの tap(`i2y/homebrew-rulec`)**。作者の tap がすでにあり、一人の作者に一つの tap がふつうの形である。
- **tap を手で更新する**。手で書いた formula には、公開した `SHA256SUMS` と食い違う余地が残る。ワークフローが書き、brew が通したものだけを置く。
- **tap の側から新しいリリースを定期的に見にいく**(鍵が要らない)。反映が遅れるうえ、GitHub は動きの無いリポジトリの定期実行を止める。
- **個人アクセストークン**。deploy key なら書けるのは tap のリポジトリだけで、作るのも `gh` で済む。
- **apt や dnf のリポジトリ**(Pages に置く、COPR、PPA、外のホスティング)。署名の鍵か外のアカウントが要る。ファイルを置くだけでも、パッケージマネージャで入れて消せる。更新は入れ直しになるので、install のページにそう書いた。
- **パッケージへの署名**。鍵の置き場が要るうえ、確かさは `SHA256SUMS` と変わらない。
- **Alpine の .apk、Arch の AUR、Nix、Snap**。Alpine では静的バイナリがそのまま動く。AUR と Snap は外のアカウントが要る。Nix は flake をリポジトリに置けば済むが、この機械に Nix が無く、確かめが CI だけになる。要望が出てから。
- **pip と npm のラッパ**(§12 のまま)。MCP のクライアントに `uvx rulec mcp` や `npx rulec mcp` と書ける利点はあるが、アカウントと包み方の保守が二つ増える。要望が出てから。
- **Windows**(§15.42 のまま)。Scoop も winget も Windows のバイナリが要る。`cargo install` が Windows で通るかは確かめていない。
- **binstall 向けの署名**。binstall は minisign の署名を確かめられるが、鍵の置き場が要る。install のページの Cargo の節(いまはコメント)には、binstall は `SHA256SUMS` と突き合わせないと書いた。
**見つけて直していないもの**:
- tap への push には、tap のリポジトリの deploy key と、rulec の側の secret が要る。無ければ tap のジョブだけが落ち、リリースそのものは出る。
- サイトは main への push で配信されるので、`.deb`・`.rpm` の節は、それらを出すリリースと続けて push する。
### 15.159 本体の日本語を、サイトの語に揃える(2026-09-28)
**きっかけ**:日本語のページの地の文を、日本語の技術文書がカタカナで書く語に直した(イベント、リクエスト、ツール、ジェネレーター、ネスト、ローカル、マシン、レスポンス)。rulec の出力はそのままだったので、ページに引用した出力とその下の地の文とで、同じものが二つの呼び方をしていた。E122 の出力が「API なら要求が誤りとして返り」と言い、四行下の地の文が「API ならリクエストが誤りとして返ります」と言う。§15.123 が避けようとした、語彙が二重の状態である。
**決定**:出力のほうをページに合わせる。対象は診断、`rulec explain` とその台帳、CLI の説明、生成コードのコメントである。
| 前 | 後 |
|---|---|
| 道具 | ツール |
| 要求(HTTP や RPC のリクエスト) | リクエスト |
| 生成器 | ジェネレーター |
| 入れ子 | ネスト |
| 手元(ローカルのマシン) | ローカル |
| 答え(HTTP のレスポンス) | レスポンス |
**残したもの**:require の意味の `要求する`(「行を要求していません」)、`機械向けの JSON`、rulec の語としての `答え`(規則の答え)。例の規則の中の語(`出来事`、`出荷の要求`)は規則を書く人の語なので、変えていない。台帳と golden を焼き直し、`rulec.wasm` も作り直した(§15.131)。ページに引用した E122 の出力と、`docs/compatibility.ja.md` の `道具` も合わせた。
### 15.160 Connect のサービスを、生成したまま立て、省いた入力と知らない名前を受け付けない(2026-10-01)
**きっかけ**:Connect のサービス(§15.112)を呼ぶのは、stub を使う呼び手だけではない。`rulec api` を読んで JSON を手で書く呼び手もいる。そのつもりで `connect` の節とサービスを読み直すと、呼び手が要るものを出しておらず、手で書いた JSON の誤りもエラーにせずに答えていた。生成したサービスがそのままでは立たない場合もあった。見つけたのは七つである。
1. 列挙の値がワイヤでどう綴られるか(`CARRIER_NEXTDAY`)が、`rulec api` のどこにも無かった。呼び手は型の名前と値の別名から組み立てるしかなく、接頭辞の慣わしに従わない契約では組み立てが外れる。接頭辞の無い `NEXTDAY = 2` も check は通る。
2. `rulec api` は、`import proto` の契約を作業ディレクトリから探していた。`Gen` に規則のパスを渡していなかったからで、見つからないと package の無い型の名前を黙って書いた。同じ抜けが `doc --format html` にもあり、`.proto` の `shape` を持つ規則では、承認者のページに入る取り出し関数が、走らせた場所によって protojson で読まない古い形になった。
3. 取り込んだ列挙を持つ規則のサービスは、生成したままでは立たなかった。契約の stub を最上位の `order_pb` として import していたが、プラグインはそれを `stubs/` の下に書く。試験は import の文字列を確かめるだけで、このサービスを一度も起動していなかった。契約は モジュール に入っていないので、`rulec test` の Connect の段も、文書の `cd proto && buf generate` も、import を解決できずに落ちた。
4. サービスの説明は、`--http` を「標準ライブラリだけ」と書いていた。サーバは標準ライブラリのものだが、connectrpc と stub は要る。§15.112 の「何も入れずに試す道」も同じ誤りである。
5. Connect の JSON の読み手は、既定で知らないフィールドを捨てる。綴りを誤った入力はゼロとして判断された。知らない列挙の名前も 0 番として読まれる。契約が 0 番に本物の値を置いていると(§15.59 で落とさないと決めた形)、誤った名前のリクエストに 200 が返り、その値として判断された。
6. 省いた入力も、proto3 では 0、false、0 番の値として読まれ、そのまま判断されていた。同じ規則の MCP のサーバーは、知らない引数も足りない引数もエラーにしている。エラーにしないのは Connect のサービスだけだった。
7. `.proto` の列挙の名前の読み方が、buf と二か所ずれていた。
- `HTTPMethod` を `H_T_T_P_METHOD` と切っていた。buf lint が求める接頭辞は `HTTP_METHOD_` なので、lint を通る契約を取り込むと E032 になり、注は未設定の 0 番を本物の値として足すよう勧めた。規則自身の列挙の別名がこの形だと、生成した `.proto` は buf lint に落ち、サービスは `AttributeError` で立たなかった。
- 接頭辞の後が数字の値(`SIZE_60`)には、別名 `60` を求めていた。ところが数字の別名は括弧ごと黙って読み飛ばされていた(`六十(60)` は別名の無い値になる)ので、E032 を満たす書き方が無かった。
**決定**:
- **`connect` の節に、ワイヤを手で書くのに要るものを全部出す。** `enums` は、ワイヤを渡る列挙ごとに、規則の値の名前、ワイヤでの名前(`alias`)、番号を並べる。`unset` は、0 番が「未設定」を意味するときのその名前である。0 番が規則の値なら null で、その値が番号 0 として並ぶ。省かれた出力を読むときに、何で埋めればよいかがこれで決まる。`contract` は、取り込んだ契約のファイル(規則からのパス)と、モジュール の中のコピーの場所である。規則自身の列挙なら null。フィールドには `optional`(規則の `T?`)と `enum`(規則の列挙の名前)を足した。`enums` は規則の列挙の名前で引く。一つの契約の列挙は二つの規則の列挙に取り込めるので、型の名前では引けない。並びを歩く規則は、要素のフィールドを `element_fields` に出す。
- **`Gen::new` が規則のパスを引数で受け取る。** 設定のメソッド(`.at()`)だったので、渡し忘れが二か所あった。引数なら、忘れたものはコンパイルされない。
- **`gen` が、取り込んだ契約を モジュール にコピーする。** コピーする先は、契約の package をディレクトリにしたパスである(`package shop.v1` なら `proto/shop/v1/order.proto`)。buf の `PACKAGE_DIRECTORY_MATCH` が置く場所で、生成する `.proto` の import もこのパスにした。コピーするのは `rulec check` が読んだファイルそのもので、一字も変えない。契約が import するファイルも、import のパスどおりの場所に見つかれば一緒にコピーする。well-known types は buf が持っている。二つの規則が同じ契約を取り込むのはよいが、違う中身の二つのファイルが同じパスに来るときは、`gen` を止める。サービスは契約の stub を `from stubs.shop.v1 import order_pb as shop_v1_order_pb` で読む。規則自身の stub はそれを相対で import しているので、二つは同じモジュールになる。
- **説明は事実に合わせる。** `--http` は「標準ライブラリのサーバで試す」ためのもので、connectrpc と stub は要る、と書いた。
- **知らないフィールドと知らない列挙の名前を受け付けない。** JSON の読み手を `proto_json_codec(ignore_unknown_fields=False)` にして、ASGI と WSGI の両方に渡す。知らないフィールドも知らない列挙の名前も `invalid_argument` になり、本文は読み手が書くもので、知らなかった名前を挙げる。binary はフィールドを番号で運ぶので、名前の取り違えは起きない。
- **リクエストの全フィールドに `optional` を付け、省いた入力を受け付けない。** 列挙のフィールドも含め、要素のフィールドも同じにした。規則が要る入力が無ければ、規則の名前で `会員: not set`、要素なら `明細[2].数量: not set` と言って `invalid_argument` を返す。`T?` の入力は、今までどおり、省けば無いものとして読む。レスポンスは変えない。出力はいつもあるので、0 の出力が JSON から省かれるのは今までどおりである。
- **名前は buf と同じに切る。** 大文字は、前が小文字か後ろが小文字のときに語の始まりとする(`HTTPMethod` は `HTTP_METHOD`、`Tier2` は `TIER2`)。接頭辞を落とすのは、残りが英字で始まり、ほかの値の名前と同じにならないときだけにする(`SIZE_60` は `size_60`)。括弧の中が名前でない別名は E002 にした。サービスの列挙の対応表は、protobuf-py が値に付ける属性名ではなく、番号で引く(`shop_v1_order_pb.MemberTier(2)`)。属性名は数字や名前の重なりで接頭辞が残ることがあり、番号はワイヤと契約が合意している値だからである。
**捨てたもの**:
- **契約をコピーせず、呼び手に置いてもらうこと。** これまでの形である。文書にその手順は無く、`rulec test` もそれを前提にしていなかった。生成したものがそのまま組めないのは、§15.112 の「出たディレクトリがそのまま buf のモジュール」に反する。
- **コピーする先を、規則から契約へのパスにすること。** `../proto/shop/v1/order.proto` のようなパスでは import が壊れる。package と食い違う場所にコピーすれば、契約のほうが buf lint に落ちる。
- **`--http` を connectrpc 無しで動かすこと。** protojson の読み書き(int64 を文字列で送ること、二通りのフィールド名、列挙の名前)を生成コードの中に作り直すことになり、§15.112 の 9 と 10(stub に対して書く、依存はサービスのファイルに閉じる)とぶつかる。何も入れずに HTTP で呼ぶなら、`_mcp.py --http` がある。
- **列挙のフィールドだけは `optional` にしないこと。** `_UNSPECIFIED` でエラーにできるからだが、0 番が本物の値の契約では、省いた列挙がその値として判断される。
- **出力も `optional` にすること。** 出力はいつもあるので、区別するものが無い。stub を使う呼び手は、出力を Optional の型で受け取ることになる。
**ワイヤの互換**:入力に `optional` を付けるのは、`buf breaking` が止める変更である。0 や false を省いて送っていた呼び手は、エラーを受け取るようになる。互換の約束(`docs/compatibility.md` の 4)は `rulec api` が書くものを 1.x のあいだ守るので、1.0 の前に入れた。生成する `.proto` の import のパスと、`HTTPMethod` のような列挙の値の名前も、同じ理由でいま変えた。
**正直な限界**:契約が モジュール の外のファイルを import していると、契約をコピーした モジュール は組めない。protovalidate の `buf/validate/validate.proto` のような BSR の モジュール がそれに当たる。`buf.yaml` はディレクトリのもので規則のものではない(§15.112)ので、規則ごとに依存を書き足すことはできない。buf がその import を示して止まり、`rulec test` の Connect の段もそこで落ちる。
**確かめたこと**:`tests/connect.rs` に試験を五本足した。取り込んだ列挙を持つ規則のサービスを、PYTHONPATH を付けずに生成したまま立て、正しいリクエスト、0 や false を明示したリクエスト、入力を一つずつ省いたもの、知らないフィールド、知らない列挙の名前、`_UNSPECIFIED` を送る。0 番が本物の値の契約で、省いた列挙が `not set` のエラーになることもここで見る。同じ規則で `rulec test` の Connect の段が通ることも確かめる。ほかの四本は次のとおり。`connect` の目録を、生成した `.proto` を rulec の読み手で読み返したものと突き合わせる。承認者のページが、`doc` をどこから走らせても `gen` の書くページと同じになる。buf lint を通る `HTTPMethod` と `SIZE_60` の契約が E032 にならない。違う契約が同じパスに来ると `gen` が止まる。取り込んだ列挙と要素を持つ規則の生成物は、`mypy --strict`(rulec の書いたファイル)と ruff の二つの検査を通った。
### 15.161 契約が BSR の モジュール に依存していても、生成した モジュール を組めるようにする(2026-10-01)
**きっかけ**:§15.160 で、取り込んだ契約を生成した モジュール にコピーするようにした。そこに正直な限界として残したのが、契約が モジュール の外のファイルを import している場合である。protovalidate の注釈を書いた契約は `buf/validate/validate.proto` を import する。そのファイルは BSR の モジュール にあり、手元には無い。コピーした モジュール は組めず、`rulec test` の Connect の段もそこで落ちた。protovalidate の注釈は、契約によくある形である。
**まず buf に確かめた。**
- `deps` を書いても `buf.lock` が無ければ、buf は依存を解決せず、import が見つからないと言って止まる。固定する commit が要る。
- v2 の `buf.lock` は b5 の digest しか読まない。v1 の lock の shake256 は持ち込めず、digest の無い行もエラーになる。
- 宣言したまま使わない依存も、`buf.lock` の余分な行も、誤りにはならない。
- 依存のファイルの stub は、既定では書かれない。契約の stub はそれを import するので、`include_imports` が要る。
**決定**:
- **依存とピンは、契約の利用者の側から引き継ぐ。** 契約の上でいちばん近い `buf.yaml`(buf が使うのと同じもの)の `deps` と、そのそばの `buf.lock` を読む。生成する `proto/buf.yaml` には import の元の モジュール を宣言し、`proto/buf.lock` には lock のピンを全部コピーする。モジュール 自身の依存もそこに固定されているからである。どの モジュール から来たかは、よくある import のパスで絞る(`buf/validate/` なら protovalidate、`google/type/` なら googleapis)。表に無いパスなら、その `buf.yaml` の依存を全部宣言する。
- **`gen` は何も取りに行かない。** 依存を取りに行くのは、組むときの buf である。`gen` はこれまでどおり、規則のそばのファイルだけから出力を決める。
- **固定できないときは、言って任せる。** そばに `buf.yaml` が無い、`buf.lock` が無い、lock が v1 の形、のどれかなら、宣言できる モジュール だけを宣言して `buf.lock` は書かない。そして `proto/` で `buf dep update` を走らせるよう言う。そうして利用者が作った `buf.lock` は `gen` の書くものではないので、次の `gen` でも残り、`gen --check` も緑のままである。
- **二つの lock が一つの モジュール を違う commit に固定していれば、止める。** 一つの モジュール に置けるのは一つである。
- **`buf.yaml` と `buf.lock` は、一回の `gen` で一度だけ書く。** どちらもディレクトリのもので、規則のものではない(§15.112)。規則ごとに書けば、最後の規則の依存だけが残る。全部の規則の依存を合わせて、ループのあとに書く。依存の無い規則だけなら、`buf.yaml` は今までと同じで、`buf.lock` は書かない。
- **stub は、依存のファイルの分も書く。** `buf.gen.yaml` の py のプラグインと `rulec test` のテンプレートに `include_imports: true` を足した。well-known types は含まれないが、protobuf-py が自分で持っている。
- **`rulec api` の `connect` に `deps` を足す。** stub を自分のテンプレートで作る呼び手は、`include_imports` が要ることをここで知る。
**捨てたもの**:
- **依存のファイルを生成物にコピーすること。** ファイルを手に入れるには、`gen` が buf を呼んで BSR に取りに行くことになる。CI の `gen --check` が、ネットワークと buf を要するようになる。
- **依存を宣言するだけで、lock を書かないこと。** buf は lock の無い依存を解決しない。
- **`gen` が `buf dep update` を走らせること。** 同じ理由で、`gen` がネットワークに出る。
- **よく使われる モジュール の commit を rulec に持たせること。** すぐに古くなる。
- **v1 の lock の digest を b5 に直すこと。** digest は モジュール の中身から計算するもので、中身が手元に無ければ計算できない。
**`rulec test` とネットワーク**:依存のある契約を持つ規則では、Connect の段で buf が依存を取りに行く(キャッシュに無ければ)。ネットワークにつながらない機械では、buf がそう言って落ちる。§15.112 で `rulec test` をネットワークなしで走るようにしたのは、プラグインの話である。依存は契約の性質なので、ここは避けられない。
**確かめたこと**:`tests/connect.rs` に三本足した。一本目は、protovalidate と googleapis を宣言する v2 の ワークスペース から、protovalidate だけが宣言され、ピンが全部コピーされ、`include_imports` が付き、`rulec api` の `deps` に出ることを見る。二本目は、`buf.yaml` が無いとき、lock が v1 のときに警告が出て `buf.lock` を書かないこと、二つの lock が食い違うと止まることを見る。三本目だけがネットワークに出る。その モジュール を buf で組み、`rulec test` の Connect の段を通し、`validate_pb.py` が書かれたことを見る。BSR に届かなければ、そう言って飛ばす。手では、`buf.yaml` の無い契約で警告どおり `buf dep update` を走らせると組め、そのあとの `gen --check` も緑のままであることを確かめた。