AI 編集 API・CRDT op・参照整合性
Strand のコードは物理ファイルではなく content-addressable CRDT graph に格納される。AI エージェントはテキストファイルを編集するのではなく、構造化された編集オペレーション(op) を発行する。
これにより:
- ファイル単位のマージ衝突が原理的に発生しない
- 編集の影響範囲が静的に計算できる
- リネームで参照が壊れない(hash 不変)
- 編集失敗時に自動修復ループが回せる
9.1 全体像
┌──────────────────────────────────────────────┐
│ CRDT graph store │
│ (definition の集合、各々が content-addressable) │
└──────────────────────────────────────────────┘
↑ ↓
│ │ strand view
│ strand op apply │
│ ↓
┌──────────────┐ ┌──────────────────────┐
│ AI エージェント │ ←───────│ projection (text) │
└──────────────┘ edit op └──────────────────────┘AI が見るのは graph からの projection(テキスト断面)。AI が出力するのは op(テキスト diff ではない)。
9.2 strand CLI
9.2.1 読み取り系
strand view <selector> # 定義をテキスト化して出力
strand view slot.todos # 単一定義
strand view 'slot.*' # ワイルドカード
strand view --with-deps reducer.add # 関連定義もまとめて出力
strand view --hash slot.todos # content-hash を表示
strand view --history slot.todos # この定義の編集履歴
strand view --refs slot.todos # この定義への参照元一覧
strand list <layer> # レイヤ内の全定義名
strand list # 全定義名(layer prefix 付き)9.2.2 書き込み系
strand add <layer> <name> <body> # 新規定義追加
strand replace <layer>.<name> <body> # 定義差し替え
strand edit <layer>.<name> <patch> # 部分編集(reducer の do= 内など)
strand rename <layer>.<old> <new> # リネーム(hash 不変)
strand remove <layer>.<name> # 削除(参照があれば失敗)
strand patch apply <file> # CRDT op バンドルを適用
strand patch revert <op-id> # 特定 op を取り消し9.2.3 検証系
strand check # 型・参照・effect 全部
strand check --types # 型のみ
strand check --refs # 参照整合性のみ
strand check --effects # capability・policy 整合性のみ
strand check --a11y # アクセシビリティ規約9.2.4 修正補助
strand fix --auto-patch <error-id> # エラーを自動修正する CRDT op を提案
strand fix --apply # 提案をそのまま適用
strand fix --interactive # 提案を 1 つずつ確認しながら適用9.3 CRDT op の形式
9.3.1 op の種類
| op | 意味 |
|---|---|
add | 新規定義追加 |
replace | 定義本体を差し替え |
edit | 定義の一部編集(field 更新、reducer の do= 内文の追加削除など) |
rename | 名前変更(hash 不変、参照は別 op で更新) |
remove | 定義削除(dependent ops 自動生成) |
link | 参照追加(明示) |
unlink | 参照削除(明示) |
9.3.2 wire format
{
"op": "add",
"layer": "slot",
"name": "todos",
"body": "Map(TodoId, Todo) = {}",
"author": "agent:claude-1",
"ts": 1779884546123,
"op-id": "op_01JC...",
"parent-ops": ["op_01JB..."],
"depends-on": ["type:TodoId@h:9ab3...", "type:Todo@h:7cde..."]
}| フィールド | 意味 |
|---|---|
op | op 種別 |
layer | 対象レイヤ |
name | 対象名 |
body | 新本体(add/replace で必須) |
author | 発行エージェント |
ts | 発行時刻(UNIX ms) |
op-id | op の ULID |
parent-ops | この op が依拠する直前 op の id(CRDT 順序保証) |
depends-on | 本体が参照する他定義の hash(参照整合性検証用) |
9.3.3 op の収束保証
Strand graph は Add-Wins LWW-Map(最終書き込み勝ち + 削除より追加優先)。
- 同名 add が複数エージェントから来た場合:
op-idの辞書順で勝者決定 - add と remove が交差: add 勝ち(dangling reference になるくらいなら残す)
- replace 同士: ts 新しい方が勝つ
- rename と remove: rename 勝ち
これらは数学的に収束保証される。が、意味的整合性は別途検査が必要(次節)。
9.4 参照整合性の強制
CRDT が構文収束を保証しても、意味的衝突は別問題:
- A:
strand remove slot.draft - B:
strand add tile.NewForm input(bind=draft)
両方が CRDT として収束したあと、tile.NewForm から slot.draft への参照が dangling になる。
Strand はこれを 2 段階で防ぐ:
9.4.1 op 発行時の事前検査
strand remove slot.draft
# Error: cannot remove slot.draft (referenced by 3 tiles, 2 reducers)
# tile.NewForm:1
# tile.Compose:4
# tile.SearchBox:1
# reducer.submitNew:2
# reducer.clearDraft:1
# Use --cascade to remove all dependents, or --force to leave dangling--cascade で依存先も同一 op バンドルに含めて remove する。--force は dangling 許容(warning 出力)。
9.4.2 op 適用時の事後検査
複数エージェントの op が同時に着信した場合、graph store はトランザクション境界で参照検査を実行:
transaction begin
apply op_A (remove slot.draft)
apply op_B (add tile.NewForm with ref to draft)
check refs
-> dangling: tile.NewForm -> slot.draft
resolve:
policy=strict: rollback both ops, mark as conflict
policy=heal: add slot.draft back with default value, log conflict
policy=warn: apply both, mark warning, emit notification
transaction commitresolve policy は strand config conflict-policy <strict|heal|warn> で設定。デフォルト strict。
9.5 hash 計算と参照解決
9.5.1 hash 計算
canonical(body) = AST正規化 (識別子は型hash+位置に置換、フィールド名アルファベット順、空白除去)
hash(def) = blake3(canonical(def.body) ⊕ hash(dep1) ⊕ hash(dep2) ⊕ ...)9.5.2 参照解決
ソーステキストの users のような名前参照は graph store 内では slot:hash:9ab3c1... として記録される。
- 名前 → hash 解決はコンパイル時 / op 適用時に行う
- 同名でも依存先が変われば別 hash
- リネームは
(rename, name-old, name-new)op のみ。hash は不変
9.5.3 表示時の名前
strand view で取り出すと hash は人間可読名に戻される(ラベル)。
9.6 エラーコードと自動修復
すべてのエラーは構造化されている:
{
"code": "E0103",
"kind": "undef-ref",
"location": "tile.TodoRow.body:2",
"message": "Reference to undefined slot 'usres'",
"suggestion": {
"kind": "did-you-mean",
"name": "users",
"similarity": 0.92
},
"auto-patch": {
"op": "edit",
"layer": "tile",
"name": "TodoRow",
"patch": {"body:2": "replace 'usres' -> 'users'"}
}
}9.6.1 主なエラーコード
| code | 種類 |
|---|---|
E0101 | 未定義型 |
E0102 | 未定義 reducer |
E0103 | 未定義 slot |
E0104 | 未定義 effect |
E0105 | 未定義 tile |
E0106 | 未定義 fn |
E0201 | 型不一致 |
E0202 | refinement 違反 |
E0203 | union 網羅性不足 |
E0204 | nominal 型混同 |
E0301 | capability 不足 |
E0302 | effect 直接呼び出し |
E0303 | reducer 外での slot 書き込み |
E0304 | tile 内 effect emit |
E0305 | fn 内 slot 読み書き / effect emit |
E0306 | event selector が tile 名ではない |
E0401 | 直接再帰 |
E0402 | ラムダ使用 |
E0403 | null 使用 |
E0404 | 任意述語 |
E0501 | 参照整合性違反 (dangling) |
E0502 | 循環依存 |
E0601 | 同 slot への複数書き込み |
E0701 | a11y 警告(label/alt 等) |
9.6.2 自動修復ループ
# AI agent script
while true; do
errors=$(strand check --json)
if [ -z "$errors" ]; then break; fi
for err in $errors; do
if has_auto_patch "$err"; then
strand patch apply <(echo "$err" | jq .auto-patch)
else
# AI に修正を委ねる
echo "$err" | ai-fix
fi
done
donestrand fix --auto-patch <code> で auto-patch があるエラーは構造的に解決される。auto-patch がないエラーだけ AI のコンテキストに乗せて修正させる。
9.7 MCP サーバ
Strand は Model Context Protocol サーバとして起動でき、AI エージェントから直接 tool 呼び出しできる:
strand mcp serve --store ./project.strand-store提供される tools:
| tool name | 引数 | 戻り値 |
|---|---|---|
strand_view | selector: string, with_deps?: bool | 定義テキスト |
strand_list | layer?: string | 定義名リスト |
strand_add | layer, name, body | op-id |
strand_replace | qname, body | op-id |
strand_edit | qname, patch | op-id |
strand_rename | qname, new_name | op-id |
strand_remove | qname, cascade?: bool | op-id |
strand_check | scope?: string | error list (JSON) |
strand_fix | error_code, apply?: bool | patch (JSON) |
strand_refs | qname | 参照元リスト |
strand_history | qname | op 履歴 |
strand_episode | episode_id | episode log |
AI からはファイル操作の代わりにこれらを呼ぶ。
9.8 エージェント並列開発プロトコル
複数エージェントが同時に編集する際の協調:
9.8.1 同時性
- 各エージェントは ローカル graph store のスナップショットを持って作業
- 出力は op バンドル
- マスター graph store に op を push → CRDT で収束
9.8.2 ロックなし
graph store はロックを取らない。op はいつでも push 可能。ただし:
- 参照整合性で reject される可能性あり
- reject されたエージェントはマスターの最新を pull して再試行
9.8.3 タスク境界
複数エージェントが同じ定義を編集することは避けたい。タスク分割の単位を「定義名のドメイン」で行う:
agent-1: slot.todos*, reducer.todo-*, tile.Todo*
agent-2: slot.user*, reducer.user-*, tile.User*
agent-3: slot.route, reducer.route-*これは規約だが、Strand コンパイラに ownership lock(オプション)を追加できる:
strand lock agent-1 'slot.todos*,reducer.todo-*'同名空間に他エージェントが op を出すと reject される。
9.9 episode と op の関係
実行時の episode log はビルド成果物に対して記録される。op は ソース graph の編集履歴。両者は分離されている:
| op log | episode log | |
|---|---|---|
| 対象 | ソース定義の変更 | 実行時の状態変化 |
| 永続化先 | graph store | episode store |
| 用途 | 並列開発・回帰検査 | デバッグ・replay test |
| 単位 | CRDT op | reducer 実行 + effect 結果 |
→ episode log は ./runtime.md。
9.10 ファイルシステムとの互換層
実装初期は、graph store を ディレクトリ内のファイル群として projection することもできる:
project.strand/
├── types/
│ ├── User.strand
│ └── TodoId.strand
├── slots/
│ └── todos.strand
├── effects/
│ └── loadTodo.strand
├── reducers/
│ └── add.strand
├── tiles/
│ ├── TodoRow.strand
│ └── App.strand
├── fns/
│ └── matchFilter.strand
└── .strand/
├── store.crdt ← CRDT graph 本体(バイナリ)
├── op-log.jsonl
└── episode-log.jsonlstrand sync で双方向同期:ファイル編集 → op に変換 → store に適用、または store の変更 → ファイルに反映。
これにより既存の Git ベースの workflow とも共存可能。ただし真の互換性は graph store 側にある。
9.11 設計上の判断記録
| 判断 | 理由 |
|---|---|
| 編集はファイル diff ではなく構造化 op | 並列マージで意味的に安全 |
| 参照整合性は op 発行時と適用時の 2 段階 | CRDT の意味的衝突を構造で防ぐ |
| 自動修復ループ | AI のデバッグサイクルを構造で短縮 |
| MCP サーバ提供 | AI エージェントから直接使える |
| ownership lock オプション | 並列開発の規約を機械化 |
| ファイル投影との互換 | 既存ツール (Git/エディタ) と共存 |
9.12 次
- ランタイム実装の詳細 → ./runtime.md
- 完全例 → examples/