コンテンツにスキップ

立場ごとの使い方

同じツールでも、手元に何があり、何を出したいかで手順が違います。五つの立場に分けたので、自分に近いところから読んでください。

あなたは 手元にあるもの 出したいもの 読む節
公的なルール(法令、公開されている規約、料金表)を実装する 条文や規約の PDF 条文どおりに動き、改正に気づけるコード 1. 既存の公的なルールを実装する
社内の規程、自社サービスの規約や料金表、Excel など、すでに決まっている自分たちのルールを実装する。いま動いている実装がある場合も 規程や規約の文書、Excel、動いているコード 文書どおりに動き、いまの実装と同じ答えを返すコード 2. 既存の自分たちのルールを実装する
新しいルールを検討する(新しい公的ルール、社内ルール、EC サイトや自社サービスの決まり) 決めたい条件が、文章か頭の中に 抜けも矛盾も無い表と、人が確かめるための資料や公開する資料 3. 新しいルールを検討する
できあがったルールを元に実装する 検査を通った .rule 自分の言語で動く、表と同じ答えを返すコード 4. 新しいルールを元に実装する
API やメッセージの契約を持っていて(.proto、OpenAPI、JSON Schema)、それが業務のルールと食い違わないようにしたい 契約のファイルと、ルールの表 契約か規則が変わるたびに、食い違いを CI で知ること 5. API の契約とルールを突き合わせる

五つとも、真ん中の一歩は同じです。rulec check を通らない表からは何も出ません(何を証明するか)。使うコマンドは、この一覧にあるものだけです。

$ rulec --help

どの一歩も、エージェントに任せるもの、rulec がやるもの、人が決めるもののどれかです。条文や文書を表に転記する、Excel から下書きを起こす、いま動いている実装を包むアダプタの一行、生成したコードの組み込み。ここまではエージェントに任せられます。抜けと重なりの証明、出典の固定、いま動いている実装や過去の記録との突き合わせ、12 言語の一致検査は、rulec が機械的に返します。人に残るのは、条件を決めること、表を確かめること、食い違いをどちらの誤りとするかの判断、改正や差し替えのあとに出典を読み直すことです。各手順の見出しの下に、その一歩を誰がやるかを書いてあります。


1. 既存の公的なルールを実装する

法令や公開された規約を、そのとおりに動くコードにしたい場合です。ここでの主役は出典です。表のどの行がどの条文から来たかを書き、条文のコピーに縛り、改正が出たら気づけるようにします。

法令や公開された規約を、エージェントが表(.rule)に転記し、条文を @ で引用する。rulec が条文のコピーと照合し、抜けと重なりを証明して、12 言語に生成する。読む人は rulec doc の資料で条文と表を見比べる。改正が出れば rulec source outdated が知らせ、表を読み直す 法令や公開された規約を、エージェントが表(.rule)に転記し、条文を @ で引用する。rulec が条文のコピーと照合し、抜けと重なりを証明して、12 言語に生成する。読む人は rulec doc の資料で条文と表を見比べる。改正が出れば rulec source outdated が知らせ、表を読み直す

1-1. 条文を引用しながら表に転記する

担当: エージェント

転記するのはエージェントでも人でも構いません。転記するときは、表や行の行末に @出典 第○条 と書いて、どこから転記したかを残します。法令なら source に、e-Gov 法令検索の法令 ID と、いつの時点の条文かを書きます。

source 法 = law "342AC0000000023" asof 2026-04-01
source 措置法 = law "332AC0000000026" asof 2026-04-01

define 軽減期間(reduced) : bool = 作成日 <= 2027-03-31  @措置法 第91条

table 本則(base)  @法 別表第一
policy unique
         | 金額の記載あり | 契約金額         | -> 印紙税額(tax) : money[円] |
記載なし | false          | -                | 200円                        |
r3       | true           | >=1万円 <=10万円 | 200円                        |

書き方の一覧はルール(.rule)を書くにあります。附則を引用するなら @法 附則第3条、改正法の附則なら @法 附則(令和七年三月三一日法律第一三号)第3条 と書きます。

1-2. 条文のコピーを取って、ハッシュを書き込む

担当: エージェント

check は通信しません。条文のコピーを規則の隣に保存し、そのハッシュを規則に書き込んでおきます。コマンドは二つです。

$ rulec source fetch rules/印紙税.rule --lang ja
措置法: 第91条 を取りました(sha256:85faf53f6f6e8196)
$ rulec source pin rules/印紙税.rule --lang ja
措置法: 1 箇所のハッシュを固定しました

pin は source の下に 第91条 sha256:85faf53f6f6e8196 の一行を書きます。コピーは sources/law/<法令ID>@<日付>/ に入るので、git にコミットしてください。

1-3. 検査する

担当: rulec。通るまで直すのはエージェント

$ rulec check rules/印紙税.rule --lang ja
ok rules/印紙税.rule

コピーが差し替わっていれば、check は E038 で止まり、読み直す定義を名指しします。

$ rulec check rules/印紙税.rule --lang ja
エラー[E038]: 出典 `措置法` の `第91条` が変わっています
  --> rules/印紙税.rule:7 出典 措置法
  |
7 |   第91条 sha256:85faf53f6f6e8196
  |   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 固定: sha256:85faf53f6f6e8196
  |
 いまのコピー: sha256:cac55089de86c8eb
 読み直す定義: 定義 軽減期間、表 軽減

ハッシュを書き忘れていれば E037、コピーが無ければ E039 です。どれも rulec explain E038 のように調べられます。

1-4. 人に見せる

担当: 人(条文と表を見比べる人)。資料は rulec が出します

条文と表を見比べるのは人の仕事です。rulec doc が、引用した条文の本文をコピーから抜き出して添えた資料を出します。

$ rulec doc rules/印紙税.rule --lang ja > 印紙税.md

資料の表の見出しの下には、こう出ます。

出典: 措置法 第91条(法令 332AC0000000026、2026-04-01 時点。2026-04-01 施行、令和8年法律第12号による改正後)

> 第九十一条
> 平成二十六年四月一日から令和九年三月三十一日までの間に作成される…

読む人は、表の行とこの本文を見比べるだけで済みます。資料の読み方は人に見せるにあります。

1-5. 生成して、突き合わせる

担当: エージェント。12 言語の一致は rulec が確かめます

$ rulec gen rules/印紙税.rule --out generated/
$ rulec test generated/
ok    stamp_duty_split (Python) ベクタ 296 件
ok    stamp_duty_split (SQL) ベクタ 296 件
…

生成したファイルのヘッダには、転記元の条文と、その時点と、コピーのハッシュが書かれます。コードを読む人が、どの条文から作られたかを確かめられます。

# Cites: 措置法 = law 332AC0000000026 asof 2026-04-01 (第91条 sha256:85faf53f6f6e8196)

すでに動いている実装があるなら、置き換える前に 2-3 と同じ手順で、表をその実装と突き合わせます。

1-6. 改正に気づく

担当: rulec(CI で週に一度)。読み直して決めるのは人

条文は改正されます。check はローカルにあるコピーしか見ないので、改正を知る手段はこのコマンドだけです。CI で週に一度走らせてください。

$ rulec source outdated rules/厚生年金保険料.rule --lang ja
厚生年金保険法: 2027-09-01 施行の改正で 第20条 が変わります。その日から効く版には、読み直した規則が要ります
  `source 厚生年金保険法_20270901 = law "329AC0000000115" asof 2027-09-01` を足し、その日以後に効く行をそこから転記してください
  第20条: - 六〇五、〇〇〇円以上
  第20条: + 六〇五、〇〇〇円以上六三五、〇〇〇円未満
  第20条: + 第三二級
  第20条: + 六五〇、〇〇〇円
  …

何が、いつから変わるかと、足す source の行まで出ます。改正が効く日は、日付の列の条件として行に書き(月分 >=2027-09-01)、古い行と新しい行を同じ表に置きます。日付に隙間や重なりがあれば check が止めます。

改正への備えは、日付を入力に取ること

法令の改正はたいてい「施行日以後に作成する文書について適用し、それより前のものは従前の例による」と書いてあります。つまり切り替わるのは処理した日ではなく、作成日や支払日や月分です。その日付を入力に取って行の条件に書けば、版ごとにファイルを分けなくて済みます。

CI の置き方はインストールにあります。outdated は改正があると exit 1 で止まるので、その出力をそのまま issue にできます。


2. 既存の自分たちのルールを実装する

社内の規程、自社サービスの規約や料金表、担当者の Excel、すでに動いているコード。公的ではないけれど、もう決まって動いているルールを、そのとおりに動くコードにしたい場合です。ここでの主役は突き合わせです。手元の文書は法令と違って取り直せないので、ファイルごとハッシュで固定します。動いている実装や過去の記録があるなら、表をそれらと突き合わせて、食い違いを行ごとに出します。

社内の規程や自社サービスの規約、Excel、動いているコードといった手元のものを、エージェントが表(.rule)に転記する。Excel からは rulec import で下書きを起こし、文書は @ で表ごとに引用し、コピーのハッシュに固定する。rulec が抜けと重なりを証明し、いま動いている実装や過去の記録と突き合わせて、食い違いをどの行で何件いくらかで返す。読む人は rulec doc の資料で文書と表を見比べる。通った表から12 言語に生成する 社内の規程や自社サービスの規約、Excel、動いているコードといった手元のものを、エージェントが表(.rule)に転記する。Excel からは rulec import で下書きを起こし、文書は @ で表ごとに引用し、コピーのハッシュに固定する。rulec が抜けと重なりを証明し、いま動いている実装や過去の記録と突き合わせて、食い違いをどの行で何件いくらかで返す。読む人は rulec doc の資料で文書と表を見比べる。通った表から12 言語に生成する

2-1. 手元にあるものから表を起こす

担当: エージェント。「推定」の印を確かめるのは人

Excel や CSV があるなら、そこから下書きを起こします。推定した箇所には全部「推定」と印が付くので、そこだけ確かめれば済みます。

$ rulec import xlsx 運賃表.xlsx --sheet 本則 --name 運賃 --lang ja > rules/運賃.rule
rule 運賃(imported) v1
description "運賃表.xlsx(シート 本則) から rulec import が起こした下書き。「推定」と書いた行は全部、人が確かめること"

enum あて先の値(c1_kind) = 北海道(v1) | 沖縄県(v2) | 東京都(v3)  # 推定: この列に現れた値をそのまま列挙にした。値が足りなければ足し、別名は付け直すこと
enum 重量の値(c2_kind) = <=2000g(v1) | >2000g(v2)  # 推定: この列に現れた値をそのまま列挙にした。値が足りなければ足し、別名は付け直すこと

inputs
  あて先(c1) : あて先の値
  重量(c2) : 重量の値

outputs
  送料(o1) : money[円, incl_tax]  round down(1円)  # 推定: 丸めの向きと刻みは出典で決めること。無ければ仮置きだと書き残すこと。税込か税抜かは出典で確かめること

table 表(t)  # 出典: 運賃表.xlsx(シート 本則)(推定: 出典の文書名と日付に書き換えること)
policy unique
| あて先 | 重量    | -> 送料(o1) : money[円, incl_tax] |
| 北海道 | <=2000g | 1200円                            |
| 北海道 | >2000g  | 1800円                            |
…

規程や規約の文書なら、1-1 と同じように転記します。動いているコードしか無いなら、そのコードをエージェントに渡して表に書き起こしてもらい、2-3 で元のコードと突き合わせます。動いているコードには触りません。

2-2. 文書を固定し、転記した表に縛る

担当: エージェント。差し替えと転記の誤りに気づくのは rulec

法令と違って取り直せないので、転記元の文書そのものを規則の隣に置き、ファイル丸ごとのハッシュを規則に書き込みます。引用は @規約 だけでもよく、文書の何番目の表を転記したかまで言うなら @規約 表1 と書きます。

source 規約 = file "配送規約.md"

table 基本送料(base_fee)  @規約 表1
policy unique
| 届け先      | 重量    | -> 基本送料 : money[円, incl_tax] |
| 遠隔地      | <=2000g | 1200円                            |
| 遠隔地      | >2000g  | 1800円                            |
| not: 遠隔地 | <=2000g | 800円                             |
| not: 遠隔地 | >2000g  | 1100円                            |

表を引くと、rulec source fetch がその表を文書から取り出して隣に置きます。Excel(.xlsx)はシートが表、Word(.docx)は文書の中の表、Markdown と CSV はそのままです。PDF やスキャンは中身を読めないので、--via で抽出器(docling など)を渡すか、@規約 と丸ごと引きます。

$ rulec source fetch rules/送料.rule --lang ja
規約: 表1 を取り出しました(3 行 × 3 列、sha256:c846fef7727dd6e0)
$ rulec source pin rules/送料.rule --lang ja
規約: sha256:d1156fa90a72194c を固定しました
規約: 1 箇所のハッシュを固定しました

ここから先が、転記の誤りの歯止めです。表の金額は、取り出したコピーに出てくる値でなければ通りません。

$ rulec source pin rules/送料.rule --lang ja
規約: sha256:a4b42e3e6c346e56 を固定しました

文書が差し替わると、check が E038 で止まり、その文書を引用している表を名指しします。差し替わったかどうかは、ハッシュの違いでその場で分かります。文書に url "…" を書いておけば、rulec source outdated が元の場所に問い合わせて、引いている表が変わったのか、それとも規則が転記していないところが変わっただけなのかまで言います。

$ rulec check rules/送料.rule --lang ja
エラー[E038]: 出典 `規約` のコピーが変わっています
  --> rules/送料.rule:6 出典 規約
  |
6 | source 規約 = file "配送規約.txt" sha256:a4b42e3e6c346e56
  | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 固定: sha256:a4b42e3e6c346e56
  |
 いまのコピー: sha256:a4b1a4052a111009
 読み直す定義: 表 基本送料、表 負担判定
 元の文書を読み直し、転記した行がまだ正しければ、次の行に書き換えて固定し直してください。

転記の誤りはこう出ます。1100円 を 1000円 と書いてしまった規則は、丸めの刻みにも載っていて、抜けも重なりもありません。ほかの検査は全部緑のまま、この二つだけが落とします。

$ rulec check rules/送料.rule --lang ja
エラー[E116]: 行4 の値が、引いた出典のコピーにありません
  --> rules/送料.rule:24 表 基本送料 行4
   |
24 | | not: 遠隔地 | >2000g  | 1000円      |
   |                           ^^^^^^ コピーに無い: 1000円
   |
 引いた出典のコピー: 規約 表1
 値は転記するときに書き換わらないので、これは転記の誤りか、その値が別のところから来たかのどちらかです。別のところから来たのなら、この行の引用を外し、どこから来たかを行末のコメントに書いてください。

警告[W120]: 表1 のコピーにある値を、どの行も使っていません
 どの行にも出てこない値: 1100円

一桁の打ち間違いは、この二つが同時に出て両側を名指しします。W120 のほうは行を一本転記し忘れたときにも出ます——落ちた行の入力は残った行のどれかに当てはまってしまうので、完全性の検査には出ないからです。

人が読む資料には、表の見出しの下に転記元の表がそのまま引用され、確かめたことの一覧に一行増えます。

出典: 規約 表1(配送規約.md、sha256:d1156fa90a72194c)

> | 届け先 | 2kg まで | 2kg 超 |
> |---|---|---|
> | 北海道・沖縄県 | 1200円 | 1800円 |
> | その他 | 800円 | 1100円 |

…

- この表の金額は、引いた出典のコピー(規約 表1)に出てくる値です(E116)

上に転記元の表、下に規則の表、間にこの一行。読む人の仕事が「表全体をもう一度読む」から「この行とあのセルを見比べる」に変わります。

2-3. 動いているコードと突き合わせる

担当: エージェント(アダプタの一行と verify)。食い違いをどちらの誤りとするかは人

いま動いている実装があるなら、置き換える前に表と突き合わせます。rulec を知らないコードでも、rulec が前に生成したものでも構いません。20 行ほどのアダプタで包み、規則の境界から作ったケースを両方に流します。アダプタのテンプレートは rulec が出すので、書くのはその実装を呼ぶ一行だけです。実装には触りません。

$ rulec adapter rules/送料.rule --template python --lang ja > adapter.py
# rulec のアダプタのテンプレート(規則 送料)。
# 標準入出力で JSON Lines をやりとりするだけ。いま動いている実装をこの中から呼ぶ。
import json, sys

sys.stdin.readline()  # ハンドシェイク
print(json.dumps({"ok": True, "impl": "legacy@REPLACE_ME"}), flush=True)

for line in sys.stdin:
    line = line.strip()
    if not line:
        continue
    req = json.loads(line)
    d = req["in"]  # 入力は 届け先, 重量, 注文金額, 会員

    # ここでいまの実装を呼ぶ。
    got = 0  # TODO: legacy.compute(d)

    print(json.dumps({"id": req["id"], "out": {"送料": got}}, ensure_ascii=False), flush=True)
$ rulec verify rules/送料.rule --adapter python3 adapter.py --lang ja
照合 70 件 / 一致 60 (85.714%)
相手: legacy@2024-03

影響 10 件 (14.286%)  差の合計 +2,550
  表 基本送料 行2 / 表 負担判定 行2                                3 件  差 +150 一様  合計 +450
    例: 会員=プラチナ, 届け先=北海道, 注文金額=0, 重量=2001 → 規則 送料=900 / 現行 送料=750
  表 基本送料 行2 / 表 負担判定 行3                                7 件  差 +300 一様  合計 +2,100
    例: 会員=一般, 届け先=北海道, 注文金額=0, 重量=2001 → 規則 送料=1800 / 現行 送料=1500

食い違いは、当てはまった行ごとにまとまって出ます。この例では、遠隔地の 2kg 超の行だけがずれています。いまの実装の誤りか、表の転記の誤りか、丸めの違いかは、その行と例を見て決めます。どちらが正しいかを rulec が決めるわけではありません。

2-4. 過去の記録と突き合わせる

担当: エージェント。食い違いの判断は人

動いているコードが無くても、過去の処理の記録(入力と、そのとき出た値)が残っているなら、規則を記録に当てます。記録は一件一行の JSON で、まず形を検査してから再生します。

$ rulec fixtures lint 記録.jsonl rules/送料.rule --lang ja
記録.jsonl: 記録 70 件(そのまま 70、補った分 0)
形式の問題はありません。
$ rulec replay rules/送料.rule --fixtures 記録.jsonl --lang ja --terse
照合 70 件 / 一致 60 (85.714%)
相手: 記録.jsonl

影響 10 件 (14.286%)  差の合計 -1,700
  表 基本送料 行2 / 表 負担判定 行2                                3 件  差 -100 一様  合計 -300
  表 基本送料 行2 / 表 負担判定 行3                                7 件  差 -200 一様  合計 -1,400

どちらの突き合わせも突き合わせと再生に詳しく書いてあります。

2-5. 確かめて、生成する

担当: 確かめるのは人、生成はエージェント

確かめる人には rulec doc の資料を渡します。文書の引用が表の見出しの下に出るので、見比べるところは 1-4 と同じです。生成と12 言語の突き合わせは 4. 新しいルールを元に実装する と同じです。


3. 新しいルールを検討する

送料やクーポンの条件、返品の可否、社内の判定基準、新しい公的なルール。まだ決めている途中のものを、表にして固めたい場合です。ここでの主役は検査です。書いた表の抜けと矛盾が、それを起こす具体的な入力つきで返ってくるので、まだ決めていない組み合わせが、決める段階で分かります。

決めたいこと(送料、クーポン、返品の可否、社内の判定基準)を、検討する人が表(.rule)に書く。rulec check が抜けと重なりを、それを起こす入力つきで返し、通るまで直す。通った表からは人が読む資料、お客向けの案内、改定の影響が出る 決めたいこと(送料、クーポン、返品の可否、社内の判定基準)を、検討する人が表(.rule)に書く。rulec check が抜けと重なりを、それを起こす入力つきで返し、通るまで直す。通った表からは人が読む資料、お客向けの案内、改定の影響が出る

3-1. まず表に書く

担当: 条件を決めるのは人。表に書くのはエージェントでも人でも

条件を列に、答えを右端の列にして書きます。文章で「北海道と沖縄は 2kg まで 1,200 円」と言っていたことが、一行になります。

rule 送料(shipping_fee) v1
description "通常便の送料"

import std/都道府県
group 遠隔地 = 北海道, 沖縄県

inputs
  届け先(dest) : 都道府県
  重量(weight) : mass[g]  range >=1g <=40kg

outputs
  送料(fee) : money[円, incl_tax]  round up(10円)

table 基本送料(base)
policy unique
| 届け先      | 重量    | -> 送料(fee) : money[円, incl_tax] |
| 遠隔地      | <=2000g | 1200円                             |
| 遠隔地      | >2000g  | 1800円                             |
| not: 遠隔地 | <=2000g | 800円                              |
| not: 遠隔地 | >2000g  | 1100円                             |

すでに Excel や CSV があるなら、そこから下書きを起こせます。推定した箇所には全部「推定」と印が付くので、そこだけ確かめれば済みます。

$ rulec import csv 運賃.csv --name 運賃 --lang ja > rules/運賃.rule

自分のルールが表で書けるかは、自分のルールが入るかで先に確かめられます。

3-2. 検査にかける

担当: rulec。返ってきた入力にどう答えるかは人

$ rulec check rules/送料.rule --lang ja

抜けがあれば、それを起こす入力と、足す行の形が返ります。金額だけは人が決めます。

エラー[E101]: 完全性の欠落: どの行にも当てはまらない入力があります
  --> rules/運賃.rule:34 表 運賃表
   |
34 | table 運賃表(fee_table)
   |       ^^^^^^ 起こりうる入力を網羅していません
   |
 当てはまらない例: あて先 = 山梨県, サイズ = S60
 ヒント: この入力に当てはまる行を足してください。
 足す行の形: `| 山梨県 | S60 | 820円 |`。出力の値は表の一行目からコピーした仮の値で、正しい値とは限りません。

矛盾があれば、両方に当てはまる入力つきで止まります。

エラー[E105]: 行の重なり: 同じ入力が 行8 と 行22 の両方に当てはまります
  --> rules/運賃.rule:58 表 運賃表
 両方に当てはまる例: あて先 = 青森県, サイズ = S60
 `policy unique` では重なりは許されません。どちらが正しいか決めるか、順序に意味を持たせるなら `policy first` を宣言してください。

通るまで直します。直すのは表であって、コードではありません。七つの検査の中身は何を証明するかにあります。

3-3. 例を書く

担当: 人(決めた人が例を出します)。ファイルに書くのはエージェントでも

決めた答えを examples に書いておくと、check のたびに実行されます。仕様書の「例えば」が、消えないテストになります。

examples
| 届け先 | 重量  | -> 送料 |
| 沖縄県 | 2500g | 1800円  |
| 東京都 | 1999g | 800円   |

3-4. 人が読む資料と公開の資料を出す

担当: rulec(rulec doc)。読むのは規則を確かめる人とお客

同じ表から、読む相手ごとの資料が出ます。規則を確かめる人には、表には出ない事実(何を確かめたか、どの行がどの行に隠れているか、どの丸めが仮置きか)を添えた資料が出ます。お客には、別名や診断コードを省いて、条件の境目の両側で答えがどう変わるかの例を添えた案内が出ます。

$ rulec doc rules/送料.rule --lang ja > 送料.md
$ rulec doc rules/送料.rule --lang ja --audience customer > 送料の案内.md
## 境目の例

- 重量が 2000g のとき 送料 1200円、2001g のとき 送料 1800円(届け先: 北海道)

3-5. 改定の影響を、入れる前に知る

担当: rulec(rulec diff)。入れるかどうかは人

ルールを改定するとき、何が起きるかを先に出せます。二通りあって、先のほうは二つの版だけで足ります。

記録が無くても、rulec diff はどの入力で答えが変わるかを、規則の書き方のまま出します。そして、ここに挙げたほかでは変わらない、と言い切ります。

$ rulec diff 送料.rule 送料_新.rule --lang ja
規則 送料 v3 → v4
入力の組み合わせ 7050 通り。うち起きうるのは 3525 通りで、同じ 3501 / 違う 24 / 決められず 0 / 入力を作れず 0

  会員 not プラチナ  かつ  重量 >=2001g <=40000g  かつ  注文金額 >=0円 <=29999円  かつ  届け先 = 遠隔地
    送料: 1800 → 2000
    当たる行: 表 基本送料 行2, 表 負担判定 行3
    例: 会員=一般, 届け先=北海道, 注文金額=0, 重量=2001

  会員 = プラチナ  かつ  重量 >=2001g <=40000g  かつ  注文金額 >=0円 <=29999円  かつ  届け先 = 遠隔地
    送料: 900 → 1000
    当たる行: 表 基本送料 行2, 表 負担判定 行2
    例: 会員=プラチナ, 届け先=北海道, 注文金額=0, 重量=2001

ここに挙げた入力のほかでは、二つの版は同じ答えを返します。

注文金額がひとりでに出てきます。3万円以上の注文は基本送料の 0% を払うので、値上げが掛け算で消えるからです。変えた行ではなく、規則の全体が決めていることです。

過去の記録があれば(一件一行の JSON)、旧と新の二つの版を同じ記録に当てて、手元の何件が動くかを名前で出します。

$ rulec fixtures lint 記録.jsonl rules/送料.rule --lang ja
記録.jsonl: 記録 70 件(そのまま 70、補った分 0)
形式の問題はありません。
$ rulec diff 送料.rule 送料_新.rule --fixtures 記録.jsonl --lang ja --terse
照合 70 件 / 一致 60 (85.714%)
相手: 送料.rule → 送料_新.rule

影響 10 件 (14.286%)  差の合計 +1,700
  表 基本送料 行2 / 表 負担判定 行2                                3 件  差 +100 一様  合計 +300
  表 基本送料 行2 / 表 負担判定 行3                                7 件  差 +200 一様  合計 +1,400

先に前者を走らせて何が動きうるかを知り、次に後者で手元の記録のうち何件がそこに入るかを見ます。--format json は動いた記録を行と tag で一件ずつ名指しします。すでに動いている実装があるなら、置き換える前に 2-3 の手順で表と突き合わせられます。どれも突き合わせと再生にあります。


4. 新しいルールを元に実装する

検査を通った .rule を受け取って、自分の言語のアプリやバッチに組み込みたい場合です。ここでの主役は生成物で、自分で書くのは呼び出す側だけです。

検査を通った表(.rule)から rulec gen が12 言語のコードを出し、rulec test が参照評価器と突き合わせる。実装する人は rulec api で呼び方を読み、関数・SQL・Wasm・MCP サーバのどれかを自分のアプリやバッチやエージェントに組み込む。生成物は編集せず、表が変われば CI の gen --check が止める 検査を通った表(.rule)から rulec gen が12 言語のコードを出し、rulec test が参照評価器と突き合わせる。実装する人は rulec api で呼び方を読み、関数・SQL・Wasm・MCP サーバのどれかを自分のアプリやバッチやエージェントに組み込む。生成物は編集せず、表が変われば CI の gen --check が止める

4-1. 生成する

担当: エージェント

$ rulec gen rules/送料.rule --out generated/

generated/ の下に、言語ごとのディレクトリができます。Python・TypeScript・JavaScript・Rust・Ruby・PHP・Go・Swift・Java は関数、NumPy は規則そのものと固定の評価器、SQL は入力の関係に対する一つの問い合わせと、同じ中身の関数、Wasm は一つのモジュールです。ランタイムも依存もありません。

4-2. 呼び方を読む

担当: エージェント

生成物を読まなくても、呼び方は rulec api の出力で分かります。

$ rulec api rules/送料.rule | jq -r .python.signature
def shipping_fee(dest: Prefecture, weight: Gram, total: YenInclTax, member: MemberKind) -> YenInclTax:
$ rulec api rules/送料.rule | jq -r .go.signature
func ShippingFee(in Input) (YenInclTax, error)

値は宣言した単位の整数で渡します(1,999g なら 1999、率は刻みの数)。列挙の値は、rulec api が出す綴りのとおりに渡します。入口で範囲と列挙を検査するので、宣言の外の値は黙って計算されず、例外かエラーで返ります。詳しくは生成して呼ぶにあります。

4-3. 突き合わせる

担当: rulec(rulec test)

生成した全言語が、参照評価器と同じ答えを返すことを確かめます。表の境界から作ったテストケースを全言語に流し、答えをバイト単位で比べます。

$ rulec test generated/
ok    shipping_fee (Python) ベクタ 68 件
ok    shipping_fee (TypeScript) ベクタ 68 件
ok    shipping_fee (Go) ベクタ 68 件
ok    shipping_fee (SQL) ベクタ 68 件
ok    shipping_fee (Wasm) ベクタ 68 件
…

ツールチェーンが入っていない言語は飛ばし、飛ばしたことを表示します。

4-4. 組み込む

担当: エージェント

組み込み先で形を選びます。

組み込み先 使うもの 読むところ
アプリのコード 生成した関数 生成して呼ぶ
DB の再計算、締めのバッチ SQL の問い合わせ(入力の関係に対して一文) SQL には問い合わせと関数の両方が出ます
自前のサーバを書かずに HTTP から呼びたい 同じ問い合わせを関数にしたもの(PostgREST や Supabase の RPC) SQL には問い合わせと関数の両方が出ます
ブラウザや、どの実行環境でも Wasm のモジュール Wasm
エージェントの中で使う場面 生成した MCP サーバ(規則が一つのツールになる) 規則をエージェントのツールにする
フォームや API の入口 rulec schema が出す JSON Schema 入口の検査も同じ表から

生成物は編集しません。直したいことは表の側にあるので、表を直せば全言語が一緒に変わります。

4-5. 表が変わったときに、ずれないようにする

担当: CI(rulec gen --check)

git には生成物もコミットしておき、CI で --check を付けて生成し直します。表が変わったのに生成物が古いままなら、そこで止まります。

$ rulec gen rules/ --out generated/ --check

CI のジョブ全体はインストールにあります。改定の影響を PR に貼る二行も、そこにあります。


5. API の契約とルールを突き合わせる

API のリクエストやメッセージの形と、そこに入ってよい値は、たいてい契約で決まっています。.proto と Protovalidate の注釈、OpenAPI や JSON Schema です。業務のルールも、どの値を受け付けるかを決めています。二つは別々の人が別々のファイルに書くので、食い違っても誰も気づきません。

契約を変えたとき、それがワイヤの上で互換かどうかは buf breaking のようなツールが見ます。けれど、その変更で業務の判断が壊れないかは、意図して見ていません。buf breaking は Protovalidate の注釈のようなカスタムオプションを読みませんし、リクエストの列挙に値が一つ増えても、buf breaking も oasdiff も壊れる変更とは扱いません。そこを見るツールは、調べた範囲では見つかりませんでした。rulec はそこを、ルールの側から見ます。ここでの主役は契約で、コードを生成しなくても使えます。

API の契約(.proto と Protovalidate、OpenAPI や JSON Schema)と規則の表(.rule)を、エージェントが対応づける。入力ごとに shape と from で契約のどこから来るかを書き、列挙は import で契約の列挙と対応づける。契約か規則が変わるたびに、CI の rulec check が二つを突き合わせ、名前が変わったフィールド、増えた列挙の値、契約は通すのに規則が受け付けない値を、その値つきで返す。契約と規則のどちらを直すかは、人が決める API の契約(.proto と Protovalidate、OpenAPI や JSON Schema)と規則の表(.rule)を、エージェントが対応づける。入力ごとに shape と from で契約のどこから来るかを書き、列挙は import で契約の列挙と対応づける。契約か規則が変わるたびに、CI の rulec check が二つを突き合わせ、名前が変わったフィールド、増えた列挙の値、契約は通すのに規則が受け付けない値を、その値つきで返す。契約と規則のどちらを直すかは、人が決める

リクエストが規則にたどり着くまでに、つなぎ目は三つあります。

つなぎ目 見るもの いつ 見るツール
リクエストと契約 リクエストが契約の検証に合うか 実行時(サービスの入口) Protovalidate、OpenAPI の検証
契約と規則の入力 パスがあるか、列挙の値の集合がそろうか、契約が通す値を入力の宣言がすべて受け付けるか CI rulec check
規則の入力と表の行 宣言した入力を、表の行がちょうど一つずつ覆うか CI rulec check

rulec が見るのは下の二つです。上の一つは、いまある検証のツールがそのまま受け持ちます。

5-1. 入力が契約のどこから来るかを書く

担当: エージェント

入力ごとに、契約のどこから来るかを from で書きます。契約は shape で名指します。

shape 出荷(shipment) = proto "contracts/shipment.proto" shop.v1.CreateShipmentRequest

inputs
  あて先(region)      : 地域  from 出荷.destination.region
  割れ物あり(fragile) : bool  from any 出荷.parcels where handling = HANDLING_FRAGILE
  個数(parcels)       : number  range >=1 <=20  from count 出荷.parcels

契約が列挙を持っているなら、import proto "<ファイル>" <列挙> -> <この規則の列挙>(JSON Schema なら import jsonschema)で、規則の列挙を契約の列挙と対応づけます。書き方はルール(.rule)を書くの「取り込み」と「入力を、呼び出し側のオブジェクトから取る」に、契約と並べた例は例で見るにあります。

5-2. 突き合わせる

担当: rulec(rulec check)

rulec check は走るたびに契約のファイルを読んで、規則と突き合わせます。見る食い違いは次のとおりです。

食い違い 診断
規則が名指すフィールドが契約に無い(名前が変わった、消えた) E121
規則が受ける型と、契約の型が合わない E120
契約の列挙と規則の列挙で、値の集合が違う E032
契約から来た値に、行も default も無い E033
契約は通すのに、規則の入力が受け付けない値がある E122
契約が通さない値でしか当たらない行がある W123
二つの入力のあいだの constraint を、契約が守っていない E123
契約が通さない組み合わせでしか当たらない行がある W124

いちばん多く出るのは E122 です。たとえば、契約が注文の明細の数に上限を書いていないと、こうなります。

$ rulec check rules/注文の送料.rule --lang ja
エラー[E122]: 契約は `注文.lines` が 51 件でも通しますが、規則はそれを受け付けません
  --> rules/注文の送料.rule:13
   |
13 |   明細数(lines)  : number  range >=1 <=50  from count 注文.lines
   |                                            ^^^^^^^^^^^^^^^^^^^^^ 契約では 1 件以上、入力 明細数 の範囲は >=1 <=50
   |
 契約の検証を通っても、この値は生成コードが入口で受け付けません。API ならリクエストが誤りとして返り、Kafka の消費側なら処理が止まるか DLQ に回ります。
 ヒント: その値が来ないはずなら、契約に "minItems": 1, "maxItems": 50 と書いて狭めてください。来るのなら、規則の範囲を広げて、その件数のときの答えを決めてください。どちらにするかは人が決めることです。

契約の検証を通った 51 件の注文を、規則は受け付けません。API ならリクエストが誤りとして返ります。生成したコードを使っていなくても、規則はその件数の答えを持っていません。fix.text は、契約に書き足すキーワードそのもの(ここでは "minItems": 1, "maxItems": 50)です。

5-3. どちらを直すかを決める

担当: 人

食い違いは、契約の側の誤りのことも、規則の側の誤りのこともあります。51 件の注文が本当に来ないなら契約を狭め、来るなら規則の範囲を広げて、その件数のときの答えを決めます。rulec は両方の直し方を示しますが、どちらにするかは、契約と規則を持つ人が決めます。

5-4. 契約が変わるたびに止める

担当: CI(rulec check)

契約のファイルが規則から見える場所にあれば、rulec check を CI に置くだけです。契約の変更も規則の変更も、同じジョブで突き合わされます。

$ rulec check rules/ --diff-base origin/main

--diff-base を付けると、そのブランチで新しく出たものだけを報告します。契約が別のリポジトリにあるなら、CI で規則の横に取ってきてから走らせます。

5-5. 生成するかどうかを決める

担当: 人

ここまでに、生成したコードは一度も出てきていません。いま動いているサービスの実装はそのままにして、規則は契約を検査するためだけに持っていても構いません。実装も規則に縛りたくなったら、2-3の rulec verify で、いまの実装を表と突き合わせられます。生成するなら、rulec gen が契約の形のままリクエストを受け取る関数も書きます(生成して呼ぶ)。

Kafka のようなメッセージの契約でも同じです。契約の検証を通ったメッセージを規則が受け付けなければ、消費側は処理を止めるか DLQ に回すことになります。それを、メッセージが流れる前に CI で見つけます。


次に読むもの

ルール(.rule)を書く 何を証明するか 生成して呼ぶ 突き合わせと再生