# Spec 62: 判断特化のエージェント(問いの集合と、人が書く規則) - 状態: **Done**(2026-09-28。P4 = 実機検収 6 件すべて観測。記録は末尾の「P4 実機記録」。P4 の実機から 有効/無効のトグルと編集の鉛筆を足した — D2 の追補)。以下は P1 時点の記述 — **rev4 承認 → P0 完了 → P1 完了**(2026-09-27。P1 の記録は末尾の「P1 実装記録」。rev3 = 査読 2 系統 29 点を反映(表は Notes 5)→ 査読 2 系統とも「P0 へ」→ P0 の測定を反映して rev4 → 未決 0 を利用者が裁定 → `data_contract.yaml` の `judge_contract` と `entities.JudgeSpec` を凍結) - 起点: 利用者(2026-09-27)—「Jev のような判断特化型モデル(System One)のエージェントを置けるように したい。判断させるパターン(Choice、Score、Noul)を決めて、次の行動を書けるようにしたい」 - ルナ → 判断役 → 結果によってイクス・ザリなど渡す相手を決め、ルナの命令 + 判断結果を渡す - ルナ → 判断役 → 結果を出力 - 利用者裁定(同日): 1. **左ペインのサーヴァントの下に「判断特化」の見出しを作り、LLM のサーヴァントとは分けて登録する。 ただし繋ぐことはできる** 2. **2026-08-22 の「動的ルーティングは採らない」を覆す** —「やはりエージェントにすべて任せることは 現段階の技術では難しい」 3. **問いと規則はユーザーが書く** —「System One Model は応用が効く if 文だと思っている。それを ユーザーが書ければより柔軟性が増す」。この条件なら次のエージェントにこれを渡す / 渡さない、を設定する 4. **形式は TOML**(YAML ではなく)・**条件は文字列の式**・**ファイルの置き場は任せる** ## rev2 からの変更(査読の反映。詳細は Notes 5) - **複数への撒きは `execute_wave` を呼ばない**(D6)。あちらは波ペインの `plan_id` とイベント・`via="plan"` の 直書き・`verify_bundle` を持つ。並列配送と束ねの純粋な部分を関数に切り出し、`plan` と判断役で共有する - **`connected_agents` を 2 つに振り分ける**(D7)。今は `turn.rs:988` 付近が全要素を `HandoffTools` へ流すので、 そのままだと判断役に `ask_*` / `transfer_to_*` が生え、`plan` の宛先にも混ざる - **`from_persisted` と `validate_connections` が判断役を知る**(D1)。今は既知のサーヴァント以外への線と座標を 読み込みで捨てるので、新版でも再起動のたびに判断役への線が消える - **文法と検査を締めた**(D3・D4)— 予約語 / Choice は 2 択以上 / 確率の定義域 / Score の `in` / `[otherwise]` の スキーマ / 空の `to` / `note` の上限 / NUMBER の形 - **有効の述語を 1 つに**(D8)/ **Score の番号の正規化**・**`state` の中身**・**入力の上限**(D5)/ **`ToolContext` にワークスペースのパス**(D8)/ **判断役を選んだときの画面**(D10)/ **`toolActor`**(D7) - **TOML は `toml_edit` で読む**(D8)。`toml` 1.1.3 は `tauri-build` の build 依存で、コアのツリーには居ない ## P0 実測(2026-09-27。**こちらが正**) 実物の口(Cloudflare Workers AI `POST /accounts/{id}/ai/run`・`model = "typesafe/jev"`・`jev.rs` と同じ)へ撃った。 鍵は利用者の環境変数 `CLOUDFLARE_ACCOUNT_ID` / `CLOUDFLARE_API_TOKEN`。スクリプトは scratchpad の使い捨て (Spec 34 と同じ扱い。数字はここが正)。**予測を先に書いてから撃ち、3 つとも当たった**(Choice / Score は SDK と 同じ形で 200 / Score の `score` は 0 始まりの期待値で小数 / 3 型を 1 回に混ぜられる)。 ### 要求の形は SDK と同じ(`@typesafe-ai/sdk` 0.6.0・MIT の `index.d.mts` を読んだ) ```json { "model": "typesafe/jev", "input": { "state": { "message": "…" }, "questions": { "kind": { "type": "choice", "instructions": "…", "criteria": { "research": "…", "implement": "…", "other": "…" } }, "size": { "type": "score", "instructions": "…", "criteria": ["…", "…", "…"] }, "risky": { "type": "noul", "instructions": "…", "criteria": { "true": "…", "false": "…" } } } } } ``` - **Choice の `criteria` は「鍵 → 説明文」の map、Score の `criteria` は説明文の配列(0 始まり)**。SDK は向きを 取り違えると投げる(「Score criteria must be a list … not a map」) - **3 型を 1 回に混ぜて 200**(`input_tokens` 593 / `output_tokens` 69 / 0.27 秒)。D5 の「1 回で送る」は成立 - `state` は `{ "message": … }` でも素の文字列でも同じ答え(Choice research 1.00 / 0.99・Score 1.28 / 1.26) ### 答えの形 ```json "kind": { "type": "choice", "choice": "implement", "probabilities": { "other": 0, "research": 0, "implement": 1 }, "confidence": 1 }, "size": { "type": "score", "score": 1.89, "legend": { "0": "…", "1": "…", "2": "…" }, "probabilities": { "0": 0.01, "1": 0.09, "2": 0.9 }, "confidence": 0.83 }, "risky": { "type": "noul", "noul": 0.08 } ``` - **Score の `score` は期待値**(SDK の型注釈「Expected score, which may fall between integer rubric levels」)。 **0 始まり**で、`probabilities` の鍵も `"0"` 始まり。**rev3 の D4 は「選ばれた段階の番号(整数)」と書いていたが、 Jev はそれを返さない** — 例の `size >= 3` は、期待値 1.89(0 始まり)= 2.89(1 始まり)で**偽**になる。確率の 90% が最上段に載っているのに。rev4 で D4 の Score の値を定義し直した(未決 0) - **確率は小数第 2 位で丸めて返る**(0.01 刻み)。`.margin` の分解能も 0.01 - **`confidence` は 1 位の確率と別の値**: Choice は 1 位 0.81 に対して 0.73、Score は 1 位 0.70 に対して 0.55・ 0.36〜0.68 と散る。**定義は SDK にも無い**(「Reported confidence」だけ)ので、rev4 でも条件には書けない - `usage` は `input_tokens` と `output_tokens` の両方を返す。`output_tokens` は出力が無料でも数として返る ### 振れ幅(同じ入力を 8 回) | 依頼 | Choice | Score(0 始まりの期待値) | |---|---|---| | 明らかな調査の依頼 | 8/8 で `research`・1 位 1.00・margin 1.00 | 1.19〜1.27(幅 0.08)・段階 1 の確率 0.71〜0.79 | | 境目の依頼(「調べて、足りなければ直して」) | 8/8 で `implement`・1 位 0.79〜0.82・**margin 0.59〜0.65(幅 0.06)** | 1.58〜1.63(幅 0.05)・段階 2 の確率 0.62〜0.67 | - **選ばれる選択肢は 16 回とも変わらなかった。** 境目の依頼でも 1 位は 0.8 前後で安定し、揺れたのは確率の 小数第 2 位だけ。Spec 59 の段落採点(最大 0.11)より小さい - **規則に書く `.margin` のしきい値は 0.1 刻みで考えるのが目安**(幅 0.06 の揺れで規則が入れ替わらないように)。 境目の依頼でも margin は 0.6 あったので、D3 の例の `margin >= 0.2` はこの依頼では常に真。**本当に「迷っている」 依頼を作れていない** — 迷う依頼の例は P4 の実機で拾う ### 上限 | 撃ったもの | 結果 | |---|---| | 約 27 万字の `message` | **HTTP 400**・`code 7003`・本文に `"error_type":"max_tokens_exceeded"` → D5 の `too_large` はこの形で分類できる | | Choice 1 択 | **200**(`probabilities: {"only": 1}`)— Jev は受ける。D3 の下限 2 は本 Spec の規則(`.margin` を常に定義するため) | | Score 1 段階 | 400「expected array to have >=2 items」 | | Score 11 段階 | 400「Too many score levels. Must have at most 10 levels.」 — D3 の 2〜10 と一致 | ### 依存 `toml_edit` 0.25 の `serde` feature は `serde_core` / `toml_datetime/serde` / `serde_spanned` を引く。**`serde_spanned` 1.1.1 は既に `Cargo.lock` に居る**(`toml` 1.1.3 = `tauri-build` の build 依存が使っている)ので、ロックファイルに 新しい crate は増えない。コアの通常の依存としてコンパイルされる crate は 1 つ増えうる(`serde_spanned`・小さい)。 D8 は `toml_edit` の `serde` で確定。 ### 旧版で開いたとき(コードで確定・実機は P4) `World::from_persisted`(`world.rs:539` 付近)の `known` はサーヴァントの ID だけなので、v0.3.7 は **サーヴァント → 判断役の線と判断役の座標を読み込みで捨て**、保存でそのまま消える。`judges` 配列は `UnknownFields` で書き戻る。D1 の見込みどおり。 ## 覆すもの **2026-08-22 の裁定「動的ルーティングは採らない」**(CLAUDE.md「スイッチ」の節)。あの裁定が退けたのは 「LLM が毎回自由に判断して配送を決める」形で、採ったのは「LLM が書いた判定コードを走らせ、走るときに LLM は居ない」形だった。本 Spec は**走るときに判断モデルが居る**ので、その裁定の外に出る。 覆す理由は利用者の言葉がそのまま正 — **宛先を選ぶ判断を、自由に文章を書くモデル(ルナ)に全部任せる のが一番信用できない**。本 Spec は宛先の選択を次の形へ移す: | | 今(ルナが選ぶ) | 本 Spec(判断役が選ぶ) | |---|---|---| | 選択肢 | ルナに生えている `ask_*` 全部 | **人が書いた規則の `to`**(D6) | | 問いの文面 | ルナが毎ターン自分で考える | **人が書いて固定** | | 分岐の規則 | ルナの頭の中 | **人が書いた if 文**(D4・D5) | | 判断モデルの出力 | 自由文 + ツール呼び出し | **型付きの答え**(文章を書かない) | | 決定性 | 無い | **規則は決定的・答えは確率的**(下の「代償」) | ルーターの分類(CLAUDE.md「スイッチ」の節と「外部研究の受領(2026-08-30)」)へ **5 つ目**を足す — 人が書くコード(LangGraph)/ LLM(この村の現状)/ LLM が書いたコード(スイッチ案)/ 出力の統計量 (semantic entropy)/ **人が書いた規則に、判断専用モデルの型付きの答えを入れる**(本 Spec)。 **代償: 答えは決定的ではない。** Jev は同じ入力を 8 回投げると 16 問中 11 問の値が振れ、最大 0.11 動いた (Spec 59 の実測)。本 Spec は規則の評価を決定的にし(D5 — 同じ答えなら必ず同じ規則)、揺らぐのを Jev の答えだけに閉じる。境目をどう扱うかは人が規則に書く(`.margin`・D4)。 ## Goal 1. 判断役を、サーヴァントとは別の一覧(左ペインの「判断特化」)に登録できる 2. 判断役ごとに、**問いの集合**(Choice / Score / Noul を混ぜてよい)と**規則**を TOML で書ける 3. サーヴァントから判断役へ線を引くと、そのサーヴァントに「判断役に判定させる」ツールが生える 4. 判断役は、サーヴァントが渡した文脈を全部の問いで判定し、規則を上から評価して、当たった規則に従って **サーヴァントへ中継する / 複数へ撒いて束ねる / 呼び出し元へ返す** 5. 中継した先の答えは呼び出し元(ルナ)へ戻る。利用者へは流れない 6. 判断役自身は文章を書かない。中継する本文は呼び出し元の本文そのまま + 判定 1 行(+ 規則の `note`) **範囲外**: 判断役から判断役への連鎖(`to` に書けるのはサーヴァントだけ)/ `plan` の波の中での判断役 / スケジュールの配送への判断役 / Jev 以外の判断モデル(D11 で trait にしておくので、足すのは後)/ GUI のリストで規則を組み立てる画面(条件を構造で持たないので、作るなら別 Spec) ## Design ### D1. 判断役は別の型。ID の名前空間だけ共有する `AgentSpec` の欄(`model_template_id` / `work_dir` / `enabled_tools` / `max_tool_iterations` / `rag_sources` / `Construct.md` / `Memory.md` / 会話履歴)は判断役にほとんど当てはまらない。 同じ型に入れると、意味の無い欄を抱えた個体が生まれる。 - `World` に `judges: Vec` を足す。`JudgeSpec` は `{ id, name, order }` だけ — **問いと規則は ファイル**(D8) - **ID は `AgentId` 型をそのまま使い、同じ名前空間に置く。** 検証は既存の `AgentId::is_safe`(`agents//` のディレクトリ名として安全かの判定。`model.rs:44`)を**同じ関数のまま**判断役にも掛ける。 `judges//` のパスはこの検証を通った ID からしか組まない(トラバーサルは既存の述語で塞がる) - **衝突の検査は 2 つの一覧をまたぐ。** `AgentList.vue:154` の `deriveId` は `state.agents` の中でしか見て いないので、判断役の一覧も見る。**コアでも拒否する** — 既存の `DUPLICATE_AGENT` / `DUPLICATE_AGENT_NAME` を 2 つの一覧をまたいで使う(新しい variant を作らない。衝突の意味は同じ)。表示名の一意性も 2 つの一覧をまたぐ - **線は向きで置き場が違う。** `connected_agents` が持つのは**サーヴァント → 判断役**だけ。**判断役 → サーヴァントは保存しない**(正本は `judge.toml` の `to`。描画時に合成 — D9)。`topologyPositions` は 判断役の ID も鍵に持つ - **コアの 2 箇所が判断役を知る必要がある**(今は既知のサーヴァント以外を捨てる): - `World::from_persisted`(`world.rs:539` 付近)の `known` に `judges` の ID を含める。含めないと、新版でも 再起動のたびに**サーヴァント → 判断役の線**と**判断役の座標**が黙って消える - `validate_connections`(`world.rs:952`)が判断役の ID を接続先として受け付ける - **旧版(v0.3.7)で開くと、サーヴァント → 判断役の線と判断役の座標は消える。** 旧版の `from_persisted` が 既知のサーヴァント以外を捨てるため(上と同じ行)。`judges` 配列そのものは `UnknownFields`(v0.2.4 から)で 保持されて書き戻るので、判断役と `judges/` のファイルは残る。**旧版で開いて保存した村を新版で開き直すと、 判断役は在るが線が無い**状態になる — 直せないので、配布のノートの「利用者が負う条件」に書く。P0 で実測する ### D2. 左ペインの「判断特化」 - サーヴァントの一覧の**下**に見出し「判断特化」。その下に判断役を並べ、作成もそこから - **判断役には起動がない。** 受信箱もターンも持たない(D5)ので、次のものを出さない / 対象から外す: 起動のトグル / 一括起動(見出しの ▶ と Spec 51 のグループの ▶)/ 状態の輪 / コンテキスト使用率の輪 / Alt + ↑↓ の選択 / 会話の宛先の候補 / `@@` の候補 - **代わりに出すもの**: 有効かどうかと、無効ならその理由(D8 の述語) - **追補(P4 の実機・利用者裁定)**: 行に**有効/無効のトグル**と**編集の鉛筆**を置く(サーヴァントのカードと同じ形)。 トグルは `JudgeSpec.enabled`(`world.json`・欄が無ければ有効)で、D8 の述語が最初に見る。止めてもファイル・線・ 座標は残る。**行そのものは押しても開かない** — 開くのは鉛筆だけ(判断役は選択状態を持たないので、行のクリックに 割り当てる動作が無い)。地図のノードのクリックで開くのは D10 のまま - Spec 51 のグループには入れない(グループはサーヴァントの区分け) - 並びは `order`。サーヴァントの並びとは独立 ### D3. `judge.toml` の形 ```toml # {workspace}/judges//judge.toml [questions.kind] type = "choice" ask = "依頼 `message` の主な作業の種類を選んでください。" options = { research = "外部情報の調査・比較", implement = "コードの変更・実装", other = "上記のいずれにも当てはまらない" } [questions.risky] type = "noul" ask = "`message` は取り消せない操作を依頼していますか。" true_if = "削除・公開・送信など取り消せない操作を依頼している" false_if = "読み取り・調査・下書きだけを依頼している" [questions.size] type = "score" ask = "`message` の作業量を評価してください。" levels = ["1 回の検索で済む", "数件の資料を比べる", "複数の工程に分かれる"] # 上から順に評価し、最初に当たった規則だけを実行する [[rules]] when = "risky >= 0.7" do = "return" note = "取り消せない操作を含むので振り分けませんでした" [[rules]] when = "kind == other" do = "return" note = "どの作業にも当てはまらないので振り分けませんでした" [[rules]] when = "kind == research and size >= 3" to = ["agent_3", "agent_10"] [[rules]] when = "kind == research and kind.margin >= 0.2" to = ["agent_3"] [[rules]] when = "kind == implement" to = ["agent_10"] [otherwise] do = "return" ``` - **問い**(`[questions.<名前>]`) - 名前は `[a-z][a-z0-9_]*`。**予約語 `and` / `or` / `not` / `in` は拒否**(式が読めなくなる) - `choice`: `options` = `{ 鍵 = 説明文 }`。**2〜255 択**(下限が無いと `.margin` の 2 位が存在しない)。 鍵は名前と同じ規則(`[a-z][a-z0-9_]*`・予約語は拒否。ハイフンは不可 — 式の中で引き算と区別できないため) - `score`: `levels` = 説明文の配列(**低い順**。2〜10 段階)。規則では **1 始まりの段階の番号**で書く - `noul`: `true_if` / `false_if` = 真と偽それぞれ一文(`jev.rs` の `NoulCriteria` の形。Kataribe の実測で 付けたほうが精度が上がる)。規則では**確率(0〜1)**で書く - **規則**(`[[rules]]`): `when`(D4)と、**`to` か `do = "return"` のどちらか 1 つ**。任意で `note` - **`[otherwise]`**: どの規則にも当たらなかったとき。**スキーマは規則から `when` を除いたもの** — `to` か `do = "return"` のどちらか 1 つ + 任意の `note`。**無いと保存を拒否する** - **`to`**: サーヴァントの ID の配列。**1 体以上**(空の配列は拒否 — `do = "return"` と同義になって紛らわしい)。 重複は拒否。判断役の ID は書けない - **`note`**: 200 字まで(**文字数 = Unicode のコードポイント数**。バイト数ではない — `MAX_*_CHARS` を `len()` で数えると日本語では枠の 1/3 で発火する、の規律)。ツール結果と中継する本文に載るので、呼び出し元のコンテキストを汚さない長さに抑える - **Choice に「その他」を置き、`otherwise` より前に `kind == other` の規則を置くことを勧める**(上の例)。 「その他」が選ばれるときは `.margin` も低くなりがちで、`.margin` の規則に任せると意図と違う規則へ 落ちうる。強制はしない(書き手が選ぶ)。新規作成の雛形はこの形にする ### D4. 条件式は閉じた小さな文法(利用者裁定 4 — 文字列の式) ```text expr := or or := and ("or" and)* and := not ("and" not)* not := "not" not | atom atom := "(" expr ")" | cmp cmp := value op literal | value "in" "[" literal ("," literal)* "]" value := NAME | NAME ".p" | NAME ".margin" op := "==" | "!=" | ">=" | "<=" | ">" | "<" literal := NUMBER | NAME # NAME は Choice の鍵 NUMBER := [0-9]+ ("." [0-9]+)? # 負の数・先頭の "." は書けない(どの値も 0 以上) ``` | 書き方 | Choice | Score | Noul | |---|---|---|---| | `q` | 選ばれた鍵。`==` / `!=` / `in` のみ | **確率が最も高い段階の番号**(1 始まりの整数。コアが `probabilities` から求める)。比較すべてと `in` | 確率 0〜1。比較のみ(`in` は不可) | | `q.p` | 選ばれた鍵の確率。比較のみ | その段階の確率。比較のみ | 書けない(`q` がすでに確率) | | `q.margin` | 1 位と 2 位の確率の差。比較のみ | 1 位と 2 位の段階の確率の差。比較のみ | 書けない | | `q.mean` | 書けない | **Jev が返す期待値**(1 始まりへ直した小数。1〜段階数)。比較のみ | 書けない | - **Score の `q` は Jev の `score` ではない**(P0 実測 — Jev の `score` は 0 始まりの期待値の小数)。規則は 「どの段階か」で書くほうが if 文として読みやすく、`in` も意味を持つので、`q` はコアが確率の最大から求める 段階の番号にし、期待値は `.mean` で別に出す。**同じ確率の段階が並んだら低いほうを選ぶ**(`margin` は 0) — この定義は未決 0 で利用者が裁定した - **保存時に型と定義域を検査し、合わなければ拒否する**: - 型の合わない比較(`kind >= 3` / `risky == research` / `risky.margin` / Noul の `in`) - **`in` の左辺は Choice か Score の `q` だけ**(`kind.p in [...]` は文法上は書けるが拒否する) - Choice の存在しない鍵 / Score の段階の外の番号(`size >= 4` で 3 段階など)・整数でない番号 - 確率(Noul の `q`・`.p`・`.margin`)に 0〜1 の外の数(`risky >= 1.5`) - 実行時に「偽」として黙って通すと、書き手の意図と違う枝へ進んでも気づけない - **`.margin` は常に定義される**: Choice と Score は 2 択 / 2 段階以上(D3)なので 2 位が必ずある。全部が 同じ確率なら 0 - **四則演算・関数・変数・文字列の加工は持たない。** if 文より大きな言語にすると台本を書く言語になり、 `run` が 3 rev かけて退けた「汎用インタプリタ」と同じ種類の穴が開く。足りなければ問いを増やす - `.confidence`(応答にある欄)は **rev3 では書けない**。1 位の確率とどう違うかが分かっていない(Notes 3) - パーサーは自前で書く(小さい。依存を足さない) ### D5. 評価 — 呼び出し元のツール呼び出しの中で、同期的に 判断役は受信箱を持たない。**呼び出し元(ルナ)のツール呼び出しの中で Jev を呼び、その場で規則を評価する。** ```text ルナの turn └ judge_x(message) ├ Jev(全部の問い + state)→ 全部の答え │ └ Jev の失敗・タイムアウト・入力の上限超え・どれかの問いが未回答 │ → 規則を評価せず「判定できなかった(理由)」を返す(otherwise にも行かない) ├ 答えを正規化(Score の番号を 1 始まりへ — 下) ├ rules を上から評価 → 最初に当たった 1 つ(無ければ otherwise) ├ to = [S] → 1 体へ中継(D6)→ S の答えがルナのツール結果 ├ to = [S1, S2…] → 撒いて束ねる(D6)→ 束ねがルナのツール結果 └ do = "return" → 判定 1 行 + note がルナのツール結果 ``` - **規則の評価は決定的。** 同じ答えなら必ず同じ規則に当たる。揺らぐのは Jev の答えだけ - **Jev が失敗したら `otherwise` には流さない。** ツール結果は「判定できなかった(理由)」で、配送は起きない。 `otherwise` に宛先が書いてあると、判定できなかった依頼がそこへ流れ込むため(fail-closed。Spec 46 の `error / timeout / unapproved` を再依頼に回さないのと同じ規律)。**`otherwise` は「判定はできたが、 どの規則にも当たらなかった」ときだけ** - **未回答の問いを 0 に潰さない。** 規則が参照していない問いでも、1 つでも未回答なら判定できなかった扱い (Kataribe `consistency.rs` の「答えが返らなかったことと『違反なし』は別」と同じ規律) - **Score の番号の正規化**: ファイルと規則は常に 1 始まり。**Jev は 0 始まりで返す**(P0 実測 — `score` も `probabilities` の鍵も 0 から)ので、コアが規則を評価する前に段階の番号へ 1 を足し、`.mean` は `score + 1`。 書き手が Jev の番号の形を知る必要は無い - **`state` の中身は `message` だけ**(ツールの引数 = 呼び出し元が書いた文)。会話の履歴・広場ログ・黒板・ RAG の結果は**送らない**。判定の材料を増やしたければ、呼び出し元がそれを `message` に書く。 送るものが 1 つに決まっていることが PRIVACY の書き方を決める(D13) - **問いは 1 回の呼び出しで送る**(Jev は問いを並列・独立に評価する)。**3 型を混ぜて送れることは P0 で確かめた** (593 トークン・0.27 秒) - **入力の上限**: Jev の公式の上限(state + 最長の質問で約 32k トークン・合計約 64k)を超えると判定できない。 **判定は Jev の応答で行う** — HTTP 400 の本文に `"error_type":"max_tokens_exceeded"`(`code 7003`)があれば `reason=too_large`(P0 実測)。**送る前の字数の事前検査は持たない** — 字数とトークンの比は文字種で変わり、 閾値を置くと送れたはずの入力を落とすか、落とすべき入力を通すかのどちらかになる。400 は 0.7 秒で返り、課金も 起きない形なので、撃って分類するほうが正確で安い - **hop**: 中継の `next_hop` は呼び出し元の hop + 1(`ask_*` と同じ)。判断役を挟んでも hop は 1 つしか 増えない — 判断役はターンを持たないので ### D6. 行き先 - **`to` が 1 体**: `deliver_and_wait(from = ルナ, to = S, 本文 = message + 判定 1 行 (+ note), waiting = ルナの連鎖, via = "judge")`。今の `ask_*` と同じ経路 - **`to` が 2 体以上**: **`execute_wave` は呼ばない。** あちらは波ペインの `plan_id` と `PlanTaskResolved` / `PlanWaveFinished` のイベントを出し、`deliver_and_wait` の `via` に `"plan"` を直書きし (`delegation.rs:588` 付近)、束ねの後に `verify_bundle`(Spec 53 の検証役)を呼ぶ。判断役から呼ぶと、 通常のツール呼び出しの中で波ペインに波が現れ、ログは `via=plan` に化け、村の既定の検証役が同期の ツール呼び出しの裏で走る。そこで: - **並列配送と束ねの純粋な部分を関数に切り出す**(仮 `fanout_and_bundle(…, via)`)。中身は `deliver_and_wait` を宛先ごとに並列で呼び、入力順に `## id(表示名)\n本文` で連結するだけ (今の束ねの組み立てと同じ形。`delegation.rs:660` 付近) - `execute_wave` はこの関数を呼び、その外側で波の記録・イベント・検証役を今までどおり担う(**`plan` の 挙動はバイト等価**)。判断役はこの関数を直接呼ぶ(`via = "judge"`・波の記録なし・検証役なし) - 束ねはルナのツール結果として返る。**要約の LLM は足さない** — ルナがその束ねを読んで答えを書くので、 LlamaIndex の `TreeSummarize` に当たる役は既にルナにある(Notes 4) - 打ち切りの伝播(`parent` の子トークン)と予算(同じ `Arc`)は `deliver_and_wait` が運ぶので、 切り出した関数でもそのまま効く。**配送できなかった宛先は、理由の本文をそのまま束ねの中に入れる** — 予算切れの「配送していません」も、待ちの輪(`circular`)の拒否文も、`deliver_and_wait` が返した本文を その宛先の見出しの下に置く(宛先を束ねから落とさない。落とすと、ルナは誰の答えが欠けたのか読めない) - **`do = "return"`**: 誰にも渡さず、判定 1 行と `note` をルナへ返す。利用者の「渡さない」はこれ - **答えは構造上ルナへ戻る。** 中継の待ちはルナのツール呼び出しの中で起きるので、`reply_to` の継承を 新しく書く必要が無い。答えが利用者へ流れる #96 の形は起きない - **委譲の輪の検出(Spec 44)・hop・予算・打ち切りがそのまま効く。** `deliver_and_wait` は `from` を `Endpoint` で受け、待ちの連鎖に `from` を足して入口で判定している(`delegation.rs:126`) - `to` にルナ自身を書いても、連鎖にルナが居るので `circular` で拒否される - **サーヴァント → 判断役 → サーヴァント → 同じ判断役 … の連鎖も詰まらない。** 判断役は受信箱を持たない 関数なので、同じ判断役が同時に何度呼ばれても待ち合わない。各段は別のサーヴァントのターンで、待っている サーヴァントは全員連鎖に入っているので、待っている誰かへ戻る配送は `circular` で拒否される。 戻らずに伸び続ける連鎖は `max_hops` と予算が切る(D5 の hop) - **送り手はルナ。** 封筒は `【送り手: ルナ】` のまま、本文の末尾に判定 1 行を添える (例: `[判断: 振り分け役 → kind=research(0.82) size=3]`)。判断役を送り手として名乗らせると、 受け手は返事の宛先を取り違えうる。判断役の名前は判定の行にだけ出す - **判定の行と `note` は封筒の寄せ(Spec 26 の defuse)を通す。** 鍵と `note` は人が書く自由文 - **判断役は文章を書かない**ので、ルナの本文は 1 字も変わらない ### D7. 呼び出し元に生えるツール - **`connected_agents` を 2 つに振り分ける。** 今は `turn.rs:988` 付近で全要素を `HandoffTools::build` へ 流しており、そのままだと判断役に `ask_*` と `transfer_to_*` が生え、接続が 2 体以上に数えられて `plan` が 生え、`plan` の宛先にも判断役が混ざる。**判断役の ID は `HandoffTools`(`ask_*` / `transfer_to_*` / `plan`) から外し、`judge_*` の合成へ送る**。`plan` が生える条件(接続 2 体以上)もサーヴァントだけで数える。 顔ぶれ(Spec 06)に判断役は載せない(会話の相手ではない) - ツール名は `ask_*` とは別の族(仮 `judge_*`)— 答えるのは判断役ではなく判断役が選んだ相手か判定結果なので、 同じ族にするとモデルが「判断役に質問した」と読む - 引数は `message`(判定の材料 = 文脈)**だけ**。問いと規則はファイルから引く - 説明文には判断役の名前と、**行き先になりうる相手**(全規則と `otherwise` の `to` の和の表示名)と 「振り分けずに判定だけ返すことがある」を書く。**問いの文面と規則は説明文に書かない**(毎ターンのトークンに なるうえ、モデルが規則を先読みして材料を寄せる) - **生やすのは有効な判断役だけ**(D8 の述語) - 会話ペイン: ツール名の表示(`lib/toolLabel.ts`)に「{名前}に判定させる」を足す。**`ChatPanel.vue:321` の `toolActor` はサーヴァントの一覧しか引かない**ので、判断役の名前を引けるようにする(ツール行の主語は 呼び出し元のサーヴァントだが、ツール名の表示で判断役の名前を解決する経路が要る) ### D8. ファイル・有効の述語・囲い(利用者裁定 4 — 置き場は任せる) - **置き場は `{workspace}/judges//judge.toml`。** 村の内容物なので村と一緒に配られる。サーヴァントの 個体ごとのファイル(`agents//mcp.json` / `run.json`)と同じ形の棚 - **読み書きの IPC は判断役の ID で受け、パスを受けない**(`ConfigFileKind` の「任意のファイル名を IPC で 受け取らない」と同じ規律) - **保存時に検査し、落ちたら保存を拒否する**(`mcp.json` / `run.json` と同じ): TOML の構文 / D3 の形と上限・ 名前と鍵の規則 / D4 の文法・型・定義域 / 参照している問いが定義されているか / `to` のサーヴァントが実在するか - **有効の述語は 1 つ**(`JudgeStatus`)。有効 = **ファイルが在る ∧ TOML が読める ∧ D3・D4 の検査に通る ∧ `to` の ID が全部実在するサーヴァント ∧ Jev の設定が在る**。1 つでも欠けたら無効で、無効の理由(閉じた列挙)を 持つ。**ツールを生やすか(D7)・左ペインの表示(D2)・地図の線(D9)はこの 1 つを読む** — 別々に判定すると、 ツールは生えないのに線は描かれる、のような食い違いが生まれる - **いつ評価するか**: ~~保存時・起動時・サーヴァントの削除時・Jev の設定の変更時~~ → **P1 で確定: ファイルの読み込みと 検査は起動時と保存時だけ**で、検査を通った形をメモリに持ち、呼び出しのたびには読まない。**有効かどうかは読むたびに その時点の村と判断モデルの有無から求める**(状態として持たないので、サーヴァントの削除や Jev の設定の変更の後に 古いまま残る形が無い)。**サーヴァントを消したとき、そのサーヴァントを `to` に書いている判断役を名指しで 知らせる**(黙って無効にしない)。起動時に無効なら WARN を 1 行 - **囲い**: サーヴァントの作業フォルダがワークスペースを含む村では、`file` / `sd` / `yq` の書き込み系で `judges/` を書き換えられる。黒板の囲い(`is_under_blackboard`)は作業フォルダ基準の判定なのでそのままでは 効かない。**`ToolContext`(`tool.rs:34`)には今ワークスペースのパスが無い**ので、`judges_dir: Option` を 足し、正規化後のパスがその配下かで判定する囲い(仮 `JudgesFence`)を `BlackboardFence` と同じ 3 本の書き込み系に 配線する。書けるのは人だけ(画面の保存) - **TOML は `toml_edit` で読む**(コアの直接依存・0.25)。`toml` 1.1.3 は `tauri-build` の build 依存で、コアの ツリーには居ない(査読で確認)。`toml_edit` の `serde` feature でデシリアライズする — `Cargo.lock` に新しい crate は増えない(P0 実測) ### D9. 地図 - 判断役のノードは**サーヴァントと形を変える**(例: 菱形・アバター無し)。判断役は会話の相手ではない - ホバーのパネルには問いの名前と型、規則の数、有効かどうかを出す(問いの文面は出さない — 長い) - **判断役 → サーヴァントの線は `judge.toml` の `to` の和から合成して描き、破線にする**(保存しない — D1)。 線とファイルの 2 か所に書くと、片方だけ直したときに「画面の行き先」と「実際の配送先」がずれる(Spec 20 で 踏んだ提示集合と配送経路の食い違い)。人が地図で判断役から直接線を引く操作は作らない。破線にするのは、色が 既に通常の辺を意味しているから(2026-08-22 の Switch の材料と同じ理由)。無効な判断役からは線を描かない(D8) - サーヴァント → 判断役の線は今の線と同じ引き方(カードを drop / 設定のチェック) - **判断役のノードをクリックすると編集ダイアログが開く**(D10) ### D10. 画面 — 編集ダイアログと「試す」 - **判断役を選ぶと、中央ペインではなく編集ダイアログが開く**(左ペインでも地図でも)。`selectedAgentId` は **変えない** — `ChatPanel` と `useOrchestrator` はそれがサーヴァントの ID であることを前提にしており (`useOrchestrator.ts:422` は一覧に無い ID を先頭のサーヴァントへ戻す)、判断役の ID を入れると会話ペインが 壊れる。判断役は会話の相手ではないので、選択状態を持たない - ダイアログは既存の `CodeEditor`(Ctrl+S で保存・検査に落ちたら理由と位置を出す)と、**「試す」の欄**: サンプルの `message` を書く入力欄 + 「試す」ボタン。押すと Jev の全部の答え(値・`.p`・`.margin`)と、 **どの規則が当たったか**を出す。**配送はしない。** 問いの文面の一文で確度が 0.36 動く(Notes 1)ので、 書き換えたらその場で確かめられないと調整できない。押したときだけ外へ出る(Spec 59 の「接続を確かめる」と同じ) - 「試す」は**保存前の編集中の内容**で走らせる(検査に通らなければ走らせず理由を出す) - 新規作成時の雛形は D3 の例を縮めたもの(Choice 1 問(`other` 入り)+ `kind == other` の規則 + 規則 1 本 + `otherwise`) ### D11. コアは trait、Jev は 1 実装 Spec 59 D3 と同じ形。コアは `Judge` の trait(問いの集合と `message` を受けて答えの集合を返す)だけを知り、 Jev は GUI 側が設定から組んで差し込む(`set_paragraph_scorer` と同じ入口の形)。鍵と接続先は Spec 59 の `{app_data_dir}/jev.json` と資格情報ストアの鍵を**共有する**(判断役ごとに鍵を持たない)。 Jev の設定が無い村では判断役は全部無効(D8)。 `jev.rs` は今 **Noul しか送れない**(`NoulQuestion` のみ・`decode` は `type == "noul"` しか拾わない)。 Choice / Score の要求と応答の型を足す(応答の欄は Notes 3)。**Kataribe の `jev.rs` も Noul のみ**で、 Choice / Score を送った記録はどちらのリポジトリにも無い(2026-09-27 に確認。Kataribe `consistency.rs` の `score` 欄は Noul の確率に付けた名前で、Jev の Score 型ではない)。 ### D12. 費用と計器 - **計器 `judge:` を 1 行**: `judge: caller=… judge=… rule=<番号|otherwise|none> outcome= routed|fanned|returned|undecided answers=kind:research/0.82/0.31,size:3/0.60,risky:0.12 to=… input_tokens=… output_tokens=…`(`usage` は両方を返す — P0 実測。出力は無料でも数は出す)。**出すのは問いの名前・鍵・数値だけ。問いの文面・`note`・ `message` は出さない**(#71 の規律)。鍵は人が設定に書いた識別子で、`run:` 行がコマンド名を出すのと同じ扱い - 判定できなかったときは `outcome=undecided reason=jev_error|timeout|too_large|unanswered` - 中継した先の配送は既存の `turn start:` / `reply:` にそのまま出る。**`via=judge` が載るのは待ちの輪の拒否の行 (`ask refused: … via=judge reason=circular`)だけ** — `turn start:` は `via` を持たないので、判断役を経た配送かは 直前の `judge:` 行と `turn start:` の `from=`(呼び出し元)で読む(P1d で実装を読んで訂正。rev4 までは 「`turn start:` を `via=judge` で見分ける」と書いていたが、そういう欄は無い)。撒いたときも `plan wave:` / `plan bundle:` は**出ない**(波ではないので — D6)。束ねの大きさは `judge:` 行に `bundle_chars=` - **Jev のトークンは予算に入れない**(Spec 59 と同じ扱い。$0.042/MTok・出力無料で、中継する先の LLM の ターンに比べて桁が小さい)。ただし**払ったことは `judge:` 行に必ず出す**(#103 — 払っているのにどこにも 出ない形を新しい経路で作らない)。統計画面へ写すかは未決 1 ### D13. 外へ送るもの(PRIVACY) 判断役を使うと、**呼び出し元のサーヴァントが `message` に書いた文(依頼の本文を含みうる)と、問いの文面・ 選択肢と段階の説明文・成立条件が Cloudflare へ送られる**。会話の履歴・広場ログ・黒板・RAG の結果は送らない (D5)。PRIVACY 日英の 4-4 は今「ツール結果の圧縮」だけを書いているので、判断役の節を足す。 既定は Spec 59 と同じく**判断役を 1 つも作らなければ 1 バイトも出ない**。「試す」は押したときだけ。 ### D14. 台帳へ書く覆し - CLAUDE.md「スイッチ」の節の「動的ルーティングは採らない」に、本 Spec で覆したことを取り消し線つきで - 同じく「LangGraph の実読」の「落とし込むなら 1」(「前判定」止まり)にも - ルーターの分類に 5 つ目を足す(上の「覆すもの」) ## 採らなかった形 - **判断役を `AgentSpec` に同居させる**(`kind` の欄で分ける)— 意味の無い欄を大量に抱え、起動・ 一括起動・ツール・作業フォルダの全経路に「判断役なら飛ばす」の分岐が要る - **判断役を受信箱を持つ個体にする** — 答えを呼び出し元へ戻す経路(`reply_to` の継承)を新しく書く ことになり、待ちの連鎖に判断役を入れる規則も要る。同期の関数なら既存の `deliver_and_wait` に乗る - **判断役を送り手として名乗らせる** — 受け手が返事の宛先を取り違えうる(D6) - **判断役 → サーヴァントの線を保存し、規則と別に持つ** — 真実が 2 つになる(D9) - **複数への撒きで `execute_wave` を呼ぶ** — 波ペイン・`via=plan`・検証役が付いてくる(D6) - **判断役の呼び出しの深さを数えるカウンタ**(査読の提案)— 詰まる経路が無い(D6 の連鎖の項)。 伸び続ける連鎖は既存の `max_hops` と予算が切る。数える機構を 2 つ持つと、どちらで止まったかが読めなくなる - **判断役を選んだら中央ペインを差し替える** — `selectedAgentId` がサーヴァントの ID である前提が崩れる(D10) - **YAML** — このワークスペースに YAML を読む依存が無く(`Cargo.toml:138` に理由つきで「YAML は 対応しない」)、代表格の `serde_yaml` は作者が保守を終えてアーカイブしている。TOML は既存の依存で読め、 コメントが書け、`[[rules]]` の並びが自然に書ける。JSON は `mcp.json` / `run.json` と揃うがコメントが 書けない — 規則に「なぜこの条件か」を残せないのはこのファイルでは痛い - **条件を構造で書く**(`{ all = [{ q = "kind", is = "research" }] }`)— パーサーが要らず GUI のリスト編集に そのまま載るが、人が書くには長い。利用者裁定で文字列の式 - **汎用の式言語を埋め込む**(Lua / Rhai など)— 四則演算・関数・変数が入った時点で台本の言語になる(D4) - **Choice の上位 k 件やしきい値超えの全件へ撒く** — Choice の確率は「どれか 1 つが正しい」前提の迷い方で、 「イクス 0.45 / ザリ 0.40」は「分からない」であって「両方」ではない。複数へ撒きたいなら、宛先ごとに 独立した Noul を立てて規則で `to` を並べる(Notes 4) - **LLM のサーヴァントに判断させる(振り分け役を 1 体置く)** — それは今でもできる。本 Spec の価値は、 分岐の規則を人が書き、判断モデルの出力が型付きであることにある ## Tasks ### P0 — 測ってから凍結する(**測定は 2026-09-27 に完了。上の「P0 実測」が正。凍結は未決 0 の裁定の後**) - [x] Jev の **Choice** と **Score** を Cloudflare Workers AI の REST で実際に送る — SDK と同じ形で 200 - [x] Score の `score` が 1 始まりか 0 始まりか、`legend` が何を返すか — **0 始まりの期待値(小数)**。`legend` は `{"0": 説明文, …}`。D4 を定義し直した(未決 0) - [x] **Choice / Score / Noul を 1 回の呼び出しに混ぜて送れるか** — 送れる - [x] `confidence` と 1 位の確率の関係 — 別の値で定義は不明。条件には書けないまま - [x] 入力の上限 — 400・`code 7003`・`max_tokens_exceeded`。事前検査は持たない(D5) - [x] 同じ入力を 8 回 — 選ばれる選択肢は 16/16 で不変・margin の幅 0.06・Score の期待値の幅 0.05〜0.08 - [x] `toml_edit` の `serde` feature — `Cargo.lock` に新しい crate は増えない - [x] 旧版で開いたとき — コードで確定(線と座標は消え、`judges` は残る)。実機は P4 - [x] `data_contract.yaml` へ `judge_contract` と `entities.JudgeSpec`(未決 0 の裁定の後・2026-09-27)(凍結: 別の型・`AgentId` と `is_safe` の共有と衝突の拒否 / 線の 置き場は向きで違う / 起動を持たない / ファイルの置き場と IPC は ID で受ける / 文法・型・定義域の検査 / 規則は上から最初の 1 つ・`otherwise` 必須でスキーマは規則と同じ / Jev の失敗は規則を評価しない / `state` は `message` だけ / 行き先 3 種と撒きは `execute_wave` を通らない / 有効の述語は 1 つ / 同期の中継・ 送り手は呼び出し元 / `connected_agents` の振り分け / 計器は本文を出さない / 囲い) ### P1 — コア - [x] `JudgeSpec` と `World.judges`・ID と表示名の衝突の拒否・`from_persisted` と `validate_connections` - [x] `judge.toml` の読み・検査(条件式のパーサーと型・定義域の検査)と `JudgeStatus` - [x] `jev.rs` へ Choice / Score の型 + `Judge` trait と Jev 実装 - [x] ~~`fanout_and_bundle`~~ `fanout_and_wait` + `bundle_answers` の切り出し(`execute_wave` はバイト等価のまま — 既存の `plan` のテストが緑のままで証明) - [x] `connected_agents` の振り分けと `judge_*` ツールの合成・実行(1 体の中継・撒く・返す・判定できない枝・計器) - [x] `ToolContext.judges_dir` と `judges/` の囲い(`file` / `sd` / `yq` の書き込み系) - [x] 単体: パーサー(文法・予約語・型・定義域の拒否)/ 規則の評価(最初の 1 つ・otherwise・未回答・Score の正規化) - [x] 結合: 中継の答えが呼び出し元へ戻る / 2 体へ撒いて束ねが返り、波の記録もイベントも出ない / `to` にルナ自身 → circular / Jev の失敗で otherwise に流れない / Jev の設定が無ければツールが生えない / 判断役に `ask_*`・ `transfer_to_*` が生えず `plan` の宛先にも入らない / 再起動しても判断役への線と座標が残る / 衝突する ID を 拒否 / `to` のサーヴァントを消すと判断役が無効になり名指しされる / 囲い ### P2 — GUI **進捗(2026-09-27 のセッション終わり)**: Rust 側だけ着地(`commands.rs` に IPC 7 本 = `list_judges` / `create_judge` / `update_judge` / `delete_judge` / `read_judge_file` / `save_judge_file` / `try_judge`、`lib.rs` に登録、 `jev_settings::apply` が**鍵があれば判断モデルを差し込む**(圧縮のチェックとは独立。`can_enable` で判定))。 GUI crate のテスト 33 本が緑。**→ 2026-09-28 に画面側も着地**(下の「P2 実装記録」が正。**実機は未確認**)。 - [x] 左ペインの「判断特化」と作成(雛形)・削除 - [x] 編集ダイアログ(`CodeEditor` + 「試す」の入力欄と結果)。`selectedAgentId` を変えない - [x] 地図のノードの形・クリックでダイアログ・破線の辺(有効な判断役の `to` から合成) - [x] `deriveId` の衝突検査を 2 つの一覧へ・`toolActor` と `toolLabel` - [x] D2 の対象外の配線(走査テストで留める) - [x] 辞書 ja / en ### P3 — 台帳 - [x] DETAIL 日英 / README 3 言語の 1 行 / PRIVACY 日英の 4-4 / CLAUDE.md の覆し(D14)(2026-09-28。下の「P3 台帳記録」) ### P4 — 実機 - [x] ルナ → 判断役 → イクス / ザリ の振り分けで、`judge:` 行と、その直後の宛先の `turn start: … from=agent` (呼び出し元)と、答えがルナへ戻ったこと(ルナの `tool:` 行の `body_chars`) - [x] `to` が 2 体の規則で、`judge: … outcome=fanned bundle_chars=…` が出て、束ねがルナへ戻り、**波ペインに波が出ない** - [x] `do = "return"` の規則で、配送が起きず判定 1 行と `note` がルナのツール結果になる - [x] `.margin` の規則が境目の依頼で当たらず、次の規則か `otherwise` へ進む - [x] Jev の鍵を外した状態で、判断役のツールが生えず、左ペインに理由が出る - [x] 判断役をクリックしても会話ペインの宛先が変わらない ## 未決 0. ~~**Score の `q` を「確率が最も高い段階の番号」にし、Jev の期待値は `.mean` で別に出す形でよいか**~~ → **決着(2026-09-27 利用者裁定): 推し案を採用。** 理由は 4 つ — if 文の書きやすさ(期待値だと確率 90% でも `size >= 3` が偽)/ Choice の `q` との対称性 / `in` が整数でこそ効く / `.mean` があるので表現力を失わない。 以下は裁定前の記述。推しはこの形。代案は `q` を期待値(小数)そのものにする形 — Jev の `score` と同じ値で説明は要らないが、 `size >= 3` のような段階の規則が「確率がほぼ全部最上段に載ったとき」しか真にならず(P0 の例で 90% でも偽)、 `in` も使えなくなる。**`judge_contract` はこの裁定の後に凍結する** 1. **Jev のトークンを統計画面へ写すか**(D12) 2. **`.confidence` を条件に書けるようにするか**(P0 で定義が分かった後) 3. **1 つの判断役に置ける問いの数の上限**(Jev の入力は字数で効くので、P0 で問いを増やして確かめる) ## Notes ### 1. Lorekeel(Kataribe Spec 32)の実測で、判断役にそのまま効くもの - **問いの文面の一文で確度が 0.36 動いた**(0.87 → 0.51、Kataribe `failures.md` #104)。問いは固定し、 変えたら測り直す — D10 の「試す」はこのため - **1 問 1 論点**。複合した問いは 0.28、分けると 0.95。**規則ファイルの形がこの実測に合っている** — 論点を問いに分け、`and` / `or` でつなぐのは規則の側になる - **`criteria` を付ける**(真偽それぞれに一文)。Noul の `true_if` / `false_if` はこの形 - **`state` は構造化 JSON で渡す**(人が読む 1 枚のテキストでは照合の問いが 0.28)。ただしこれは 照合の問いの話で、Fuseforks Spec 59 は依頼文を素の文章で渡して話題の問いはよく分かれた。rev3 は `message` の文をそのまま渡す(D5) - 公式に明記された弱点: **敵対的な入力に弱い**。判断役の材料はサーヴァントが書いた文で、その中には web や外部 MCP から取った本文が混ざりうる。判定を誤らせる文を仕込まれる経路がある(行き先は人が 書いた `to` に限られるので、誤っても届くのは村の中の誰か) ### 2. Jev に向く問い(Fuseforks Spec 60 の実測) 同じ 20 要素で、**話題を問うと 16/20、属性(誰が書いたか)を問うと 1/20**。宛先の振り分け (「調査か実装か」)は話題の問いなので向く側。「成果物は十分か」「成功したか」は判断の問いで弱い側 (Kataribe Spec 32 でも照合 > 判断の順)。 ### 3. Choice / Score の形(Qiita @ynakayama 2026-09-25 の記事。利用者の提示・2026-09-27 に読んだ) https://qiita.com/ynakayama/items/b69f6d458e834b53d2c4 。SDK `@typesafe-ai/sdk` の `TypeSafeClient.systemOne({ state, questions })` で、複数の問いを 1 回の呼び出しに束ねて同じ `state` に当てる。 | 型 | 問いの形 | 答えの欄 | |---|---|---| | Choice | 問い + `{ 鍵: 説明文, … }` | `choice` / `probabilities`(選択肢ごと)/ `confidence` | | Score | 問い + 説明文の配列(順序つき) | `score` / `legend` / `probabilities`(段階ごと)/ `confidence` | | Noul | 問いのみ | `noul`(0〜1。`confidence` は無い) | - 記事の Choice の例は**問い合わせを担当キューへ振り分ける**もので、本 Spec の使い道そのもの。`other` を 選択肢に置いている - 使い分けは「順序のない有限候補から 1 つ選ぶ」なら Choice、「順序付きの尺度で評価する」なら Score、 「ある命題が成立する確率を得る」なら Noul - **書かれていないもの**: REST の要求の形 / 数値の例 / `confidence` の定義 / 選択肢の数と段階の数の上限 (上限は Kataribe Spec 32 が公式 docs から写した「Choice 最大 255 択 / Score 2〜10 段階」が正)/ 精度と 振れ幅。→ **2026-09-27 に実物へ撃って埋めた**(上の「P0 実測」が正) ### 4. 他の実装の束ね方(2026-09-27 に確かめた) | 系統 | 実装 | 束ね方 | 裏付け | |---|---|---|---| | 1 つしか選ばない | Haystack `ConditionalRouter` + `BranchJoiner` | 流れるのは 1 本なので、`BranchJoiner` は枝を 1 つの出口へ戻すだけ | 公式 docs | | 複数選び、**LLM が要約** | LlamaIndex `RouterQueryEngine` + `LLMMultiSelector` | 答えが 2 つ以上なら `combine_responses` が `TreeSummarize`(既定・LLM)で 1 つにする | `router_query_engine.py` のソース | | 複数選び、**コードで合流** | LangGraph の条件付きの辺が一覧を返す / `Send` | 状態の欄ごとのリデューサで合流。リデューサの無い欄に同じ周で 2 本書くと `InvalidUpdateError` | コードの実読(2026-08-19) | | 同上 | Haystack `DocumentJoiner` | `concatenate` / `merge` / `reciprocal_rank_fusion` / `distribution_based_rank_fusion`。合流させるのは文書の一覧で、答えの文章ではない | 公式 docs | **この村の `plan` はコードの合流と LLM の読解の二段** — 各個体の答えを機械的に連結し(`delegation.rs:660` 付近)、それを進行役がツール結果として読む。本 Spec の複数の `to` はこの連結だけを切り出して使う(D6)。 ### 5. rev3 の査読の反映(2026-09-27。2 系統 29 点) 査読 1(文面から)と査読 2(実コードと照合)。**査読 2 のコードの事実の指摘は全部を実物で確かめ、一致した** (`execute_wave` の `via="plan"` 直書きとイベントと `verify_bundle` / `turn.rs:988` の無条件の流し込み / `from_persisted` の `known` による線と座標の除去 / `ToolContext` にワークスペースのパスが無い / `toml` 1.1.3 は `tauri-build` の build 依存 / `toolActor` と `selectedAgentId` の前提)。 | 指摘 | 判定 | 反映先 | |---|---|---| | 査読 2 (1) 撒きで `execute_wave` を呼ぶと波ペイン・`via=plan`・検証役が付く | **採用**(最重) | D6 — 純粋な部分を切り出す | | 査読 2 (2) `connected_agents` が全部 `HandoffTools` へ流れる | **採用**(最重) | D7 | | 査読 2 (3) 新版でも再起動で判断役への線と座標が消える | **採用** | D1 | | 査読 1-1 ID をファイルパスに使うとトラバーサル | **前提を訂正して採用** — 正規表現を新設せず、既存の `AgentId::is_safe` を同じ関数のまま掛ける(2 つ目の検証を書かない) | D1 | | 査読 1-2 / 査読 2 (6) `otherwise` のスキーマと Jev の失敗時 | 採用 | D3・D5 | | 査読 1-3 / 査読 2 (4) 予約語・Choice の鍵の命名 | 採用(予約語は文法の 4 語だけ。`return` / `otherwise` は式に現れないので不要) | D3 | | 査読 1-4 D1 の旧版互換の記述が逆 | **前提を訂正して採用** — rev2 の記述はサーヴァント → 判断役の線の話で向きは正しかった(査読 2 (3) がコードで裏付けた)。ただし「`connected_agents` はサーヴァント → 判断役だけ」を明記した | D1 | | 査読 1-5 Score の 1 始まりの正規化 | 採用 | D5 | | 査読 1-6 `.margin` が 1 択で未定義 | **別の形で採用** — 1 択のときの値を決めるのではなく、Choice の下限を 2 にして 1 択を作れなくした(査読 2 (5))。全部同じ確率なら 0 | D3・D4 | | 査読 1-7 S → J → S → J の再入でデッドロック | **反証** — 判断役は受信箱を持たない関数なので待ち合わない。各段は別のターンで、待っているサーヴァントは全員が待ちの連鎖に入っており、戻る配送は `circular` で拒否される。伸びる連鎖は `max_hops` と予算が切る。カウンタは足さない | D6・採らなかった形 | | 査読 1-8 有効の述語を 1 つに | 採用 | D8 | | 査読 1-9 1 回の呼び出しの前提が未測のまま凍結 | 採用 — 凍結は P0 の後と明記。混ぜられなければ型ごとに並列(問いはもともと独立に評価されるので一貫性は変わらない) | D5・P0 | | 査読 1-10 `state` の中身 | 採用 — `message` だけ | D5・D13 | | 査読 1-11 旧版の保存で `judges` 配列が消える | **前提を訂正して採用** — `UnknownFields` は v0.2.4 から読み込みで保持して**書き戻す**ので、`judges` 配列は残る。消えるのは線と座標(査読 2 (3)) | D1・P0 | | 査読 1-C NUMBER / 空の `to` / `note` の上限 / 「試す」の入力欄 / `other` の規則の例 | 採用 | D3・D4・D10 | | 査読 1-C 計器に入力トークンを出す | **確認済み** — rev2 の D12 に `tokens=` はあった。Jev は出力が無料なので `input_tokens=` へ名前を正した | D12 | | 査読 2 (5) Choice の下限 2 | 採用 | D3 | | 査読 2 (7) Score の `in`・確率の定義域 | 採用 — Score に `in` を許す・確率の 0〜1 の外は拒否 | D4 | | 査読 2 (8) `ToolContext` にワークスペースのパスが無い | 採用 | D8 | | 査読 2 (9) `too_large` の基準 | 採用 — 字数の事前検査 + Jev の 400 の分類。閾値は P0 | D5 | | 査読 2 (10) `toml` 1.1.3 は build 依存 | **採用**(rev2 の見立ては外れていた)— `toml_edit` で読む | D8 | | 査読 2 (11) 判断役を選んだときの画面と `selectedAgentId` | 採用 — 編集ダイアログを開き、選択状態を持たない | D10 | | 査読 2 (12) `toolActor` が判断役を引けない | 採用 | D7 | **rev2 の見立てが外れていたもの 2 つ**: - 「`toml` 1.1.3 は既に依存ツリーに居るので直接依存にしても増えない」— 居たのは GUI の build 依存で、コアの ランタイムのツリーではなかった。依存を数えるときは**どの依存ツリーか**まで見る - 「複数へ撒くときは `execute_wave` に乗せればそのまま使える」— 関数の名前と束ねの行だけを見て、その関数が 波ペインと検証役まで持っていることを数えていなかった。**機構を再利用すると書く前に、その関数が外へ出す 副作用(イベント・計器の値・後段の呼び出し)を数える** ## P1 実装記録(2026-09-27) コミットは 6 段(P1a `244b149` 検査と評価 / P1b `77b0c65` 村の状態 / P1c `e2e9b5f` 判断モデルの口 / P1d `a5a45d5` 並列配送の切り出し / P1e + P1f 本体と囲い)。コア全体で **50 バイナリ・1,148 本が緑**(P0 前は 48・1,123)・clippy 0・ GUI の crate もビルドが通る。**プロンプトとツール提示の golden は 1 バイトも動いていない**(判断役の無い村はバイト等価)。 ### 実装で決まったこと 1. **判断役のツールは `HandoffTools` に相乗りさせた**(別の引数を足すと `build_prompt` / `present_tools` / `run_turn` / `CallRunner` の署名が全部変わる)。**`is_empty` / `offers_plan` / 名簿は判断役を数えない**ので、`plan` の生える条件と 宛先はサーヴァントだけのまま。判断役のツールは `use_handoff_tools` ではなく**モデルがツールを扱えるか**で出す (判断役にだけ繋いだ個体 = サーヴァントの接続 0 でも生える) 2. **囲いは `BlackboardFence` に相乗りさせた**(書き込み系 6 か所の配線がもう 1 組要らない)。判定 `is_under_judges` は 黒板と同じ形で、基準はワークスペース(`judges_dir` の親)。**`judges/` がまだ無い村でもその下へ作る書き込みを塞ぐ** 3. **`via` が載るのは待ちの輪の拒否の行だけ**(D12 を訂正。`turn start:` に `via` は無い)。`plan` の撒きで `via` を `judge` に変える変異で落ちるテストは無かった — wave の `via` はテストの網の外(`ask refused` の行にしか出ない) 4. **判断モデルの HTTP は段落の採点と共有する `post` 1 本**。失敗の 400 は本文から `max_tokens_exceeded` の印だけを 探して `TooLarge` へ分類し、**本文はログにもエラーにも載せない**(#71) 5. **`INVALID_JUDGE_FILE` を足した**(契約の「新しい variant を作らない」は ID と表示名の衝突の話で、ファイルの検査の 失敗は GUI が理由を出すために独立した code が要る)。**`data_contract` の `ErrorCode` の一覧は 16 個ずれていた** (#129 と同じ形)ので、本 Spec の分だけ足して残りは別タスクへ切った 6. **有効かどうかは状態として持たない**(上の D8 の訂正)。ファイルの検査結果だけをメモリに持ち、有効の述語は読むたびに 求める 7. **サーヴァントを消したときの知らせは System 行**(役職の変更と同じ経路・記録のみで配送しない)+ 計器 `judge disabled:` 8. 撒いた束ねの見出しは `bundle_answers` の既存の形(`## id(表示名)`)。**配送できなかった宛先も落とさない** (`deliver_and_wait` が返した理由の本文をその見出しの下に置く) ### テストと変異(**一発で緑になったものは全部、機構を壊して赤を確かめた**) | 置き場 | 本数 | 変異(予測どおり狙った 1 本だけ赤) | |---|---|---| | `judge.rs` 単体 | 24 | 同率を高い段階へ / 規則を下から / Score の範囲検査を外す / 未回答を読み飛ばす | | `world.rs` 単体 | 7 | `known` から判断役を外す(**査読 2 (3) の回帰**)/ 表示名の検査から判断役を外す | | `jev.rs` 単体 + 実鍵 | 4 + 1 | 400 の分類からステータスの条件を外す | | `tests/judge_agents.rs` | 9 | 宛先の振り分けを外す / 有効の述語を外す / 削除の知らせを外す | | `tests/judge_log.rs` | 1 | `judge:` 行に本文を足す(#71) | | `tools/file.rs` 単体(囲い) | 1 | `is_under_judges` を常に偽 | **実鍵の live**: 3 型を混ぜた 1 回が Rust の口を通って全部の答えになり、既存の段落の採点の live も同じ走行で通った。 ### 作業で踏んだもの - **`LNK1104`(`libucrt.lib` を開けない)で全テストが 0 本になった回がある**(#132 と同じ形 — 集計が 0 のまま緑に 見えかけた)。再実行で 50 バイナリがリンクされ、コードの問題ではなかった。**全体の結果は本数で読む** - **`ToolContext` のリテラルへ欄を足す機械置換が `Shared` の欄宣言にも当たった**(`run_approval:` の行を目印にしたので、 同じ名前の欄を持つ構造体の定義にも入った)。ビルドの前に差分で見つけて外した。Spec 61 の「戻り型の `{` に当たる」と 同じ系統 — **目印の字面は構文上の位置を区別しない** - 変異の復元に 1 度 `git checkout --` を使った(未コミットの切り出しごと戻るので、切り出しのスクリプトを当て直して SHA が一致することを確かめた)。以後は可逆な置換だけにした ## P2 実装記録(2026-09-28。画面側) コミットは 2 段(画面 1 `8d72879` = 型・IPC・一覧・編集ダイアログ / 画面 2 = 地図・線・ツール名・除外の走査)。 vitest 702 → **733**・vue-tsc 0・build 緑。**実機は未確認**(P4)。 ### 実装で決まったこと - **id の導出は `lib/judges.ts` の `deriveId(name, taken, fallback)` の 1 実装**。`AgentList.vue` の関数をここへ移し、 サーヴァントの作成と判断役の作成が同じ関数に**両方の一覧の id を渡す**。判断役の土台は `judge`(日本語だけの名前は 英数字が残らない) - **状態の表示は `JudgeStatus` の `kind` を写すだけ**(`judgeStatusText`)。有効かどうかを画面で判定し直さない — 一覧・ダイアログ・地図のホバーが同じ関数を読む - **地図の破線は `judgeEdges`**(有効な判断役の `targets` から合成・同じ相手へは 1 本)。行き先が隠れたグループに 居れば描かない。**地図からは切れない**(`edge:click` は判断役の線を無視 — 正本は `judge.toml`)。破線は静止 (`dasharray` 4)で、稼働中の辺の「動く破線」とは動きで分ける - **サーヴァント → 判断役の線の入口は 2 つ**(D9 どおり): カードを判断役のノードへ drop(`tieAddition` に `receivers` を 1 引数足した。判断役は受け側にだけなれる)/ サーヴァントの設定ダイアログの接続先チェック (判断役を「(判断特化)」の印つきで並べる) - **編集ダイアログは `App.vue` に 1 つだけ**置き、開いている id は `composables/useJudgeDialog.ts` のモジュール 変数に持つ(入口が一覧と地図の 2 つ)。`selectedAgentId` は触らない - **`CodeEditor` に `plain`(色付けなし)を足した**。TOML の構文モードは依存に無く、足さない — 検査はコアが 保存時に行い、落ちた場所を `INVALID_JUDGE_FILE` のまま出す - **「試す」は編集中の本文で走る**(`try_judge(text, message)`。未保存でも可・配送しない)。結果は当たった規則 / otherwise / 判定できなかった理由 → 行き先(表示名 + id)と、問いごとの値(`key p= margin=` の文字列はコアが組む) - 改名はダイアログの見出しの入力欄(Enter か blur で `update_judge`)。削除は見出しのボタン(確認つき) ### ホバーの概形(同日に利用者裁定で広げた) - 初版は `JUDGE_PROFILE` に名前・id・状態・行き先の数だけで、D9 の「問いの名前と型・規則の数」を外していた (`JudgeView` に無かった)。利用者の「ホバーの情報も広げて」で **`JudgeView.outline: Option`** を 足した — `questions: [{name, kind}]`(名前の順)と `rules`(`[[rules]]` の本数。`[otherwise]` は数えない) - **`None` = ファイルが無いか検査に落ちている。** 空の配列と 0 で表すと「読めない」と「規則が 0 本」が同じ値に畳まれる。 画面は `outline` があるときだけ `RUL` と `[QUESTIONS]` を出す(走査テストが留める) - **問いの文面・選択肢・規則の中身は運ばない**(単体テストがワイヤに文面が載らないことを留める)。型の語 (`choice` / `score` / `noul`)は `judge.toml` の `type` のまま訳さない — HUD の略号と同じく識別子 - 既存の `Question::kind() -> &'static str`(エラー文用・非公開)と名前が衝突したので、新しい方は `question_kind()` - 変異(`outline` を常に `None`)で結合テストの 1 本だけが赤。Rust 1,182 本・vitest 734 本 ### テストと変異 - 新設 `lib/judges.test.ts`(id の導出・並び・状態の表示・破線の合成)/ `lib/judgeExclusions.test.ts`(D2 の除外 = 起動・一括・グループ・選択・宛先・`@@` の規則のファイルが `judges` を読まない / 行を押すと編集ダイアログ / 地図のクリックの分岐 / 選択の正規化はサーヴァントからだけ / 破線の合成と地図から切らない / drop の受け側 / id の導出が 2 つの一覧をまたぐ)/ `editorSaveWiring.test.ts` に `JudgeDialog.vue` を足した - **変異 4 回、どれも予測した 1 本が赤**: 保存の門(`canSave` を外す)/ drop の受け側(`receivers` を渡さない)/ 地図のクリック(常に選択)/ 破線の絞り込み(無効な判断役も描く) - 起動テストの IPC のモック 11 ファイルに `listJudges` を足した(`fetchAndAssign` が 1 本増えたので、足さないと 起動テストが落ちる — 足す前に落ちることは確かめていない) ### 作業で踏んだもの - **変異の復元に `git checkout --` をまた使った**(P1 の記録に「以後は可逆な置換だけにした」と書いた直後)。 `AgentList.vue` の未コミットの変更(`receivers` の 1 行)ごと HEAD へ戻り、後の 2 つの変異の結果に 1 本余計な赤が 混ざったことで気づいた。当て直して、変異前に取った SHA-1 と一致することを確かめた。**自分が書いた規律は 自分を守らない**(CLAUDE.md にも P1 の記録にも書いてあった)— 以後、変異を戻すのは sed の逆置換だけ - Bash の heredoc で Python を渡すと、本文のバッククォート入りの文字列で bash の構文解析が落ちた (`unexpected EOF while looking for matching`)。スクリプトはファイルへ書いてから実行する ## P3 台帳記録(2026-09-28) **数えたのはファイル単位**(#51 (b))。書いたのは 10 ファイル: | ファイル | 書いたこと | |---|---| | `DETAIL.md` / `DETAIL_en.md` | 新しい節「判断役 — 人が書いた規則で宛先を決める」(層 2 の直前)/ ディレクトリ木(`judge.rs` / `judging.rs` / `lib/judges.ts` / `useJudgeDialog.ts` / `JudgeList.vue` / `JudgeDialog.vue`、`jev.rs` と `jev_settings.rs` の説明)/ 画面の構成表(左ペインの「判断特化」・編集ダイアログの行)/ 絆の張り方(判断役へも同じ 2 つで引ける・破線は地図で引けず切れない)/ ワークスペースの木(`judges/{judge_id}/judge.toml`)/ 診断ログ(`judge:` 行の欄)/ システム設定の表(Jev の鍵は判断役と共有) | | `DETAIL_zh.md` | 抄訳の節とディレクトリ木の `judging.rs`(zh は抄訳なので、画面の表と診断ログの欄までは写していない — Spec 59 のときと同じ粒度) | | `README.md` / `README_jp.md` / `README_zh.md` | 圧縮の行の直後に「判断役」の 1 行。170 / 170 / 166 行(上限 160 を超えた記録は CLAUDE.md の冒頭に足した) | | `PRIVACY.md` / `PRIVACY_en.md` | 4-4 を「判断専用モデル Jev(ツール結果の圧縮・判断役)」へ改め、小節「判断役」— 送る 2 つ(`message` と問い)と送らないもの(規則・行き先・`note`・履歴・広場ログ・黒板・検索結果・村の情報)。保存の表に `judges//judge.toml`、`jev.json` と資格情報の行に「共有」。最終更新 2026-09-28 | | `CLAUDE.md` | D14 の覆し 3 箇所(スイッチの節の裁定に取り消し線 / LangGraph の「落とし込むなら 1」/ 外部研究の受領の分類)+ ルーターの分類の 5 つ目 + README の行数 + Spec の状態 | | `data_contract.yaml` | 追従漏れの回収 1 件(下) | **PRIVACY の「送るもの」は実装を読んで書いた**(`jev.rs` の `encode_judge`)— `input.state` は `message` だけで、 問いは名前・`instructions`・`criteria`(選択肢の鍵と説明文 / 段階の説明文 / 真偽の条件)。規則と `note` は `judge.rs` の評価側にしか無く、送る形に現れない。 **追従の grep で回収したもの 2 件**: - `data_contract.yaml` の `judge_contract` が並列配送を **`fanout_and_bundle`**(起票時の仮名)と書いたまま。P1d で `fanout_and_wait` + `bundle_answers` に割ったのに、凍結側が古い名前を指していた - `CLAUDE.md` の Spec の状態の行が「2 体以上は `execute_wave` で撒いて」のまま — rev3 の D6 で**通らない**と決めた 関数を名指ししていた(起票時の文が rev を 3 回越えて残った。#51 (b) の「腐るのはその機能の節ではなく隣の節」の形) **grep 網の外**: ランディングページは固有の機能を列挙しておらず、「LLM 以外の外向き通信は単価表だけ」は Jev が 利用者の鍵で設定するモデルの口なので今も正しい(Spec 59 と同じ判断)。Qiita の記事は判断役に触れていない。 ## P4 実機記録(2026-09-27) 検収用の `judge.toml` は、`route`(記号 I / Z / F / R / なし の Choice)と `kind`(research / implement / other)の 2 問。 規則は `route == ret` → return / `fan` → `["agent_10","agent"]` / `ix` → イクス / `za` → ザリ / `kind == research and kind.margin >= 0.6` → ジェミー / `otherwise` → return。判断役の id は `judge`(名前が日本語なので `deriveId` の既定へ落ちた)、ツール名は `judge_judge`。呼び出し元はルナ(`agent_8`・gpt-6-sol)。 | 記号 | `judge:` 行 | その後 | 判定 | |---|---|---|---| | I | `rule=3 outcome=routed route:ix/1.00/1.00 to=agent_10` | 4 ms 後に `turn start: agent=agent_10 hop=1 from=agent_8` → `reply: agent=agent_10 to=agent_8 chars=193` → ルナの `tool: … body_chars=193` | 答えがそのままルナへ戻った | | Z | `rule=4 outcome=routed route:za/1.00/1.00 to=agent` | `turn start: agent=agent hop=1 from=agent_8` → `reply: … to=agent_8 chars=546` → `body_chars=546` | 同上 | | F | `rule=2 outcome=fanned to=agent_10,agent bundle_chars=1121` | 2 体が同じ ms で `turn start: … from=agent_8`、答え 375 + 708 字を束ねて `body_chars=1121` | 束ねがルナへ戻った | | R | `rule=1 outcome=returned route:ret/0.99/0.98` | 他の個体の `turn start:` は 0 本。ルナの `body_chars=107`(判定の 1 行 + `note`) | 配送なし | - **記号の問いは 4 回とも確率 0.99〜1.00・margin 0.98〜1.00** — 閉じた記号の照合は Jev の強い側(Kataribe の「照合型は強い」と同じ)。 `kind` は 4 回とも implement 0.53〜0.56・margin 0.06〜0.12 で、「README の誤字を直す案」は調査と実装の境目に居る - **Jev の払いは問い 2 つで入力 534 / 出力 87 トークン**(1 問のときは 396 / 38) - **F で波ペインに出た波は判断役のものではない** — `plan wave: agent=agent wave=1 tasks=2 to=[agent_2,agent_5]` は、 束ねの相手として呼ばれたザリが自分の `plan` でロボットくんへ撒いたもの。判断役は `plan wave:` 行を 1 本も出していない (D6 = `fanout_and_wait` は波の記録を通らない) - **撒く経路では `judge:` 行が束ねの後に出る**(F は `turn start:` 23:22:26 に対し `judge:` 23:32:23)。1 体へ渡す経路は 配送の前に出る(I は `judge:` の 4 ms 後に `turn start:`)。`bundle_chars` は束ねた後でしか分からないため。代償は、 撒いている最中(今回はイクスが 21 周・10 分)に**どの判断役がどの規則で撒いたかがログから読めない**こと。宛先の `turn start: … from=agent_8` は出るので、呼び出し元までは分かる。頻度を見てから(撒いた時点で 1 行を足すかは未決) - 左のカードの直近ツールは「judge に判定させる」(id)、会話ペインは「振り分け1 に判定させる」(名前)。カードは 相手の名前を引けない既存の規則どおり - **地図の破線は 3 本出た**(振り分け1 → ジェミー / イクス / ザリ = `to` の和)。ルナ → 判断役は実線(保存された絆) - **見出しの「絆」が判断役の破線まで数えていたので直した**(実機で「サーヴァント 10 / 絆 10」= 保存された絆 7 + 破線 3)。「サーヴァント N」は判断役を数えていないので、同じ見出しの中で数え方が割れていた。`tieCount` は `judge` の印の無い辺だけを数える(`judgeExclusions.test.ts` に 1 本・変異で赤を確認) - **静止した破線(判断役)と、稼働中の個体どうしの動く破線は、止まった画面では同じに見える**(同じスクリーンショットで ザリ → ロボットくん 5 体も破線 — 両端が稼働中なので流れていた)。D9 は動きで分けると決めており、スクリーンショットの 上では区別が付かない。見分けは起点のノードの形(菱形)で付く。頻度を見てから - **検収 5(Jev の鍵を外す)を観測** — 左ペインに「判断モデル(Jev)が未設定です(システム設定 > 外部連携 > Jev)」、 地図のノードは無効の見た目(点線の菱形)、判断役からの破線は消えた。**ツールが生えないことはこの走行では ターンが無く、ログに出ていない** — 結合テスト `without_a_judge_model_no_judge_tool_is_offered` へ預けた (述語は左ペイン・破線・ツールで 1 つ) - **利用者裁定 2 点(検収 5 の画面から)** — (a) 有効/無効は左ペインの判断役の行で切り替えたい(Jev の鍵の有無とは 別の、人の意思のスイッチ)(b) 編集はサーヴァントと同じ鉛筆で開く。→ `JudgeSpec.enabled`(既定は有効・欄が 無ければ有効で読む)+ `JudgeStatus::Disabled`(述語が最初に見る)+ 一覧の行にトグルと鉛筆・行のクリックは外した。 結合 1 本(無効でツールが生えず、ファイルと行き先は残り、戻すと有効)+ 旧形の読み込み 1 本 + 走査 2 本。 変異 2 回(述語の `if !enabled` を殺す → 単体と結合が 1 本ずつ赤 / トグルが反転しない → 走査 1 本が赤) - **検収 6(判断役をクリックしても会話ペインの宛先が変わらない)を観測**(利用者の確認)。地図の菱形のクリックは 編集ダイアログを開くだけで、`selectedAgentId` はルナのまま(D10) - **検収 4(`.margin`)は半分だけ** — 境目のつもりの文(「宇宙の法則を元にラグジュアリー哲学的な野獣先輩が LICENSE の 正しさを検証を比較して調査しない」)は `kind:other/0.69/0.39` で `rule=otherwise outcome=returned`。 **落ちたのは `kind == research` の側で、`.margin` の比較には届いていない**(kind が research でないので)。 対照の「Rust の TOML ライブラリを 3 つ比べて」は `kind:research/1.00/1.00` で `rule=5 outcome=routed to=agent_3`、 ジェミーの 1,305 字がルナの `body_chars=1305` に戻った(規則 5 そのものが当たる側は観測)。 **`.margin` が当たらない側を切り出すには、kind が research で margin が 0.6 未満の文が要る** - 配送された本文の末尾に判定の 1 行(`[判断: 振り分け1 → kind=research(1.00) route=none(1.00)]`)が付き、 ジェミーの画面にも出ている(D6 —「送り手はルナ・本文の末尾に判定 1 行」のとおり) - **検収 4 を閉じた — 文を固定し、閾値だけを動かした。** 「試す」で差が 0.6 未満になる research の文は 見つからなかった(research と判定したときの差は 1.00 / 1.00 / 0.98)。そこで同じ文(68 字)を 2 回撃ち、 間で規則 5 の閾値だけを 0.6 → 0.99 に変えた: ```text 03:02:52 judge: rule=5 outcome=routed answers=kind:research/0.99/0.98,… to=agent_3 (閾値 0.6) 03:05:23 judge: rule=otherwise outcome=returned answers=kind:research/0.99/0.98,… (閾値 0.99) ``` **Jev の答えは 2 回とも 1 桁も違わず**、変えたのは閾値だけなので、`otherwise` へ落ちたのは `kind == research` を満たしたうえで `kind.margin >= 0.99` が偽になったから、としか読めない。 1 回目はジェミーの 4,223 字がルナの `body_chars=4223` に戻り、2 回目は他の個体の `turn start:` が 0 本で `body_chars=106`(判定の 1 行) - **Jev の margin は二極に寄る**(この走行の 9 回)。記号の問いは 0.98〜1.00、research は 0.98〜1.00、 境目の文は implement で 0.06〜0.12 / other で 0.39。**「差が中くらい」の答えがほとんど出ない**ので、 `.margin` の閾値を 0.2 と 0.6 のどちらにしても当たり方がほぼ変わらない。`.margin` が実際に効くのは implement 側(差が小さい)で、閾値を書くなら「当てたい側の差の分布」を「試す」で先に見る - **システム設定のページ名を「Jev(ツール結果の圧縮)」から「判断特化モデル(Jev)」へ**(利用者裁定。英語は `Judge model (Jev)`)。鍵を使うのが圧縮だけではなくなったので、ページは鍵の側で名付け、圧縮は小見出し 「ツール結果の圧縮」の下へ。候補の「System One モデル」は、画面に 3 つ目の呼び方を増やし System 1 を知らない人に 通じにくいので採らず、左ペインの見出し「判断特化」と語を揃えた。無効の理由の文言(`noJudgeModel`)と DETAIL 日英の 道順も追従。**鍵・`jev.json`・IPC・計器の名前は据え置き**(表示名の改名 — 型の grep 網の外)