コマンド層という設計
oniyanma には「AI 用の API」がありません。UI が使うものと同じものを AI が使います。
5 つの経路が 1 つの層を通る
UI ボタン ─┐
⌘K パレット ─┤
ショートカット ─┼─→ executeCommand(ctx, name, args, actor) ─→ Viewer / Nav / Measure …
AI コンソール ─┤
MCP / 中継 ─┘新しい入口が増えても、通る場所は変わりません。 これは実装の都合ではなく、次の 4 つを 1 箇所で決めるためです。
コマンドは自分の性質を申告する
各コマンドは、名前・説明・引数の JSON Schema に加えて、 読み取り専用か(readonly)と破壊的か(dryRun)を自分で持ちます。
{
name: 'queryElevation',
description: '標高(実Z, m)分布を取得: {min,max,mean,count,bins[]}…',
input_schema: { type: 'object', properties: { bins: { type: 'number', minimum: 2, maximum: 128 } } },
readonly: true,
run: (c, a) => c.viewer.queryElevation(…),
}この印だけで、次の 4 つが同時に決まります。
1. ツール定義。 toolDefs() がレジストリからそのまま生成します。 Anthropic の tool-use 互換なので、AI に渡す定義を手で書く場所がありません。 定義と実装がズレる余地が構造的に無い、ということです。
2. 権限。 「コマンドごとの ACL 表」を別に持たず、 サーフェス(どの鍵で入ってきたか)で決まります。
3. 確認ゲート。 dryRun を持つコマンドを機械が confirm 無しで呼ぶと、 実行されずに影響件数だけが返ります。→ 確認ゲート
4. ドキュメント。 コマンド一覧の引数表は、 このドキュメントのビルド時に同じ定義から生成されています。
コマンドを 1 つ足す人が印を付けるだけで、公開面・拒否・テスト・ドキュメントが同時に追随します。
サーフェス
読み書きの分離は、コマンド単位ではなく入口単位です。
| サーフェス | 実行できるもの |
|---|---|
all(既定) | 全 59 コマンド。UI / ⌘K / AI コンソールがこちら |
read | readonly の 18 コマンドだけ |
読み取り専用の入口を組むと、そこからは書きコマンドを実行できません。 ツール定義を絞るだけでは「一覧に出ないが名前を知っていれば通る」ので、 実行時にも弾きます。拒否のメッセージには使えるコマンドの一覧を添えてあり、 AI が「権限が無い」と理解して代替(読み取りで答える / 人間に依頼する)へ切り替えられます。
発注者・測量会社・コンサルへ渡す入口をこの面に縛れば、 権限設計が「コマンドごとの ACL を作る」から「どのサーフェスを開けるか」に縮みます。 読み取りだけなら結果はキャッシュでき、読み専用トークンで MCP を外に出しても壊せません。
発行者(actor)
サーフェスが「どの鍵で入ってきたか」なのに対して、発行者は「誰が」です。 ディスパッチのたびに注入され、編集コマンドは Command ログに焼き込みます。
kind | 由来 | 確認ゲート |
|---|---|---|
human | UI / ⌘K / ショートカット | 通らない(自分の結果を見ているため) |
ai | AI コンソール(モデル名と指示文の冒頭も記録) | 通る |
api | window.oniyanma / MCP 経由 | 通る |
「AI が触った範囲」を後から分離できるので、成果物の監査ができます。
テストが 5 方向を守る
コマンド層のテスト(引数バリデータ・ディスパッチ・ツール定義の妥当性・actor 注入)は、 5 つの入口すべてを同時に守ります。入口ごとにテストを書く必要がありません。
その上に、自然言語からコマンド列への変換を測る評価セットが乗っています。