GitHubのトレンドで tt-a1i/archify を見かけて、説明文の一語で手が止まりました。「beautiful, verifiable architecture … diagrams」。
作図ツールが「きれい」を名乗るのはよくあります。「検証可能」を名乗るのは珍しい。しかも売り文句は「エージェントが型付きJSON IRを出し、Archifyがそれを決定的にHTML/SVGへコンパイルする」。つまり、図の中身を書くのはLLMで、Archifyはその出力を受け取る側です。
私の仕事は検収とテスト設計なので、この構図を見ると反射的に一つのことを考えます。受け取る側は、どこまで嘘を弾いてくれるのか。 テストは目的ではなく手段で、ここで測りたい手段はひとつです。壊した入力を渡して、通るか落ちるかを数える。
というわけで、使い捨てコンテナを1つ立てて、正しいIRを12通りに壊してみました。
試した環境
Debian 12(bookworm)ベースの node:22-bookworm コンテナ、Node.js v22.23.1 / npm 10.9.8、x86_64。メモリ4GB・CPU2・pids 512の上限つきで、ホストには何も入れていません。対象は tt-a1i/archify の HEAD 9a50605(コミット日時 2026-08-27 14:49:23 +0800)、リポジトリ内の開発バージョン表記は v2.16.0-dev.0、直近リリースはCHANGELOG上 2.15.0(2026-08-17)です。
セットアップは驚くほど軽くて、ここは正直に感心しました。
# clone(--depth 1)
$ git clone --depth 1 https://github.com/tt-a1i/archify.git
clone_ms=2773 # 50MB / 471ファイル
# 依存インストール(archify/ サブディレクトリ側)
$ npm install
npm_install_ms=1527 # node_modules 31MB
$ ls node_modules
ajv entities fast-deep-equal fast-uri json-schema-traverse
parse5 require-from-string saxes simple-icons xmlchars
# セルフチェック
$ node bin/archify.mjs doctor
Archify doctor
[ok] Node.js v22.23.1 (requires >=18)
[ok] Core template
[ok] Example renderer
... (中略:全15項目)
[ok] lifecycle renderer, schema, and example
Archify is ready.
# 最初のレンダリング(パスを間違えた)
$ node bin/archify.mjs render architecture \
../examples/production-deployment.architecture.json /work/out/prod.html
Error: ENOENT: no such file or directory,
open '/work/archify/examples/production-deployment.architecture.json'
at Object.readFileSync (node:fs:440:20)
at loadDiagram (file:///work/archify/archify/renderers/shared/cli.mjs:22:33)
rc=1
# 正しいパスで
$ node bin/archify.mjs render architecture \
examples/production-deployment.architecture.json /work/out/prod.html
/work/out/prod.html
rc=0 render_ms=107 # 718,276 バイト
宣言されている開発依存は ajv / parse5 / saxes / simple-icons の4つだけ。APIキーもLLMも要りません。エージェント向けのツールなのに、エージェントが居なくても単体で全部動くというのは、検証する側からするとありがたい設計です。
途中でコケているのは私のミスです。リポジトリ直下にも examples/ があり、そちらにはレンダリング済みHTMLと一部のJSONしか入っていません。IRのフルセットは archify/examples/ の方でした。ただ、ここで返ってきたのが生のNodeスタックトレースだったのは意外で、この後さんざん見ることになる整形済みの診断メッセージと比べると落差があります。ここは好みが分かれるところですが、私はもう少し素っ気ない一行で良いと思いました。
正しいパスを指すと、production-deployment.architecture.json(5,476バイト・コンポーネント12・接続12・境界4)から 718,276バイトの自己完結HTMLが107ミリ秒で出ます。

上の画面がその成果物です。単一ファイルで、テーマ切替もガイドビューもズームも全部この中に入っています。
本題:IRを7通りに壊す
ここからが測定です。同梱の production-deployment.architecture.json を対照群(A)とし、1箇所だけ壊した変種を作って、validate と render の両方に通しました。1ケース1プロセスです。同じプロセスで複数ケースを回すと状態が持ち越されて交絡するので、ここは横着しません。
| ケース | 壊した内容 | validate | render | 出たメッセージの要点 |
|---|---|---|---|---|
| A 対照群 | 無改変 | rc=0 | rc=0 / 718,276バイト | 9 artifact checks; composition showcase: 0 errors, 0 warnings |
| B 宛先が存在しない辺 | ラベル無しの接続の to を postgress に | rc=1 | rc=1 | Cross-boundary connection “events->postgress” does not name its mechanism. |
| C id重複 | 同じコンポーネントをもう1件追加 | rc=1 | rc=1 | Component ids must be unique. |
| D 余計なフィールド | トップレベルに confidence、ノードに owner_email | rc=1 | rc=1 | must NOT have additional properties(2件・Fix提案つき) |
| E 幽霊フォーカス | meta.views[0].focus に居ないidを追加 | rc=1 | rc=1 | /meta/views/0/focus/5 references unknown semantic id “cache_layer” |
| F 幽霊を囲む境界 | 境界の wraps に居ないidを追加 | rc=1 | rc=1 | Boundary “AWS us-east-1 / production” wraps unknown component “shadow_node”. |
| G 座標欠落 | 1ノードから pos と size を削除 | rc=1 | rc=1 | non-finite pos/size ほか計11件(NaNpx viewBox の警告を含む) |
7戦7敗、というのがここでの合格点です。そして重要なのは、render が validate と同じ判定で落ちること。 検証を別コマンドに分けているツールだと「validateを飛ばして直接renderしたら通ってしまう」がありがちで、そこが一番危ない導線です。Archifyは renderers/shared/diagnostics.mjs を共通の投げ口にしていて、レンダリング経路からも同じ診断が飛んできました。壊れたIRからは、壊れた図が出るのではなく、何も出ません。
ただし、メッセージは原因を指してくれるとは限らない
ケースBだけ、腑に落ちませんでした。私が壊したのは接続の宛先で、postgress などというノードは存在しません。なのに出てきたのは「cross-boundary connection が mechanism を名乗っていない」。存在しないidの話が一言も出てこない。
そこで、変数を1つずつ振って切り分けました。
# B2: ラベル付きの接続(api_b -> postgres, label "SQL")の宛先を壊す
Architecture layout validation failed:
- Connection "SQL" references unknown target "postgress".
# B3: 同じ壊し方 + meta.engineering_profile を外す
- Connection "SQL" references unknown target "postgress".
# B4: ラベル無しの接続を壊す + meta.engineering_profile を外す
- Connection "postgress" references unknown target "postgress".
# B5: 送信元(from)側を壊す + profile 無し
- Connection "SQL" references unknown source "api_bb".
# B6: 対照群(無改変 + profile 無し)
ok architecture ... (9 artifact checks; composition showcase: 0 errors, 0 warnings)
つまり「存在しないidを指す辺」の検査はちゃんとあります(B2・B3・B5)。ケースBでそれが見えなかったのは、私が壊した接続がたまたまラベルを持っておらず、かつ engineering_profile: deployment-ownership が有効だったから。プロファイル側の規則が先に発火して、そこで打ち切られていただけでした。
ひとつ前のコードブロックのB4も面白くて、ラベルが無い接続は表示名として宛先idが使われるので、Connection "postgress" references unknown target "postgress" という自分で自分を説明するメッセージになります。間違ってはいないのですが、初見だと二度見します。
検証があることと、検証結果が原因を指していることは別物だ、というのは現場でよく言う話です。ここは実際にそうなっていました。とはいえ、落ちるべきものが落ちているので、優先度は高くありません。
ソースへの参照は「いま在るか」ではなく「あのコミットに在ったか」で見る
もうひとつの売り文句が「optionally open revision-verified source」でした。図のノードから実際のソースコードへ飛べる、しかもリビジョン検証済みで、と。
スキーマを見ると、コンポーネントに sources: [{ path, line, end_line, label }](最大3件)を付けられます。ここで最初、ソースだけ足して --repo-root を渡してレンダリングしたら、5ケース全部が同じ理由で落ちました。
Error: Repository evidence requires /meta/repository.
at verifyRepositoryEvidence (.../renderers/shared/repository-evidence.mjs:95:20)
ソース参照を1つでも書いたら、meta.repository でリポジトリURLとリビジョンをピン留めしろ、ということです。読んでいくと、要求はかなり厳格でした。リビジョンは40桁のフルSHA、URLは https://github.com/ の公開リポジトリ、--repo-root はGitのトップレベルそのもの、origin のURLは書いたURLと一致すること、そのコミットがローカルに存在すること。
そこでコンテナ内に小さなGitリポジトリを作り、origin を https://github.com/acme/demo-service に設定して、1つ目のコミット(52394e1…)をピン留め対象にしました。そのうえで7通りです。
| ケース | 与えたソース参照 | 結果 |
|---|---|---|
| R1 | src/gateway.js L3-5(実在・行も範囲内) | rc=0 / 718,748バイト |
| R2 | src/worker.js の42行目(実ファイルは1行) | rc=1 / requests line 42, but … has 1 lines |
| R3 | src/added-later.js(作業ツリーには在る/ピン留めコミットには無い) | rc=1 / does not identify a file at revision 52394e1… |
| R4 | ../../etc/passwd | rc=1 / must stay inside the repository |
| R5 | URLだけ別リポジトリに差し替え | rc=1 / origin … does not match … |
| R6 | リビジョンを短縮SHA(7桁)に | rc=1 / スキーマ段階で拒否 |
| R7 | .git/config | rc=1 / may not address .git |
この表でいちばん効いたのはR3です。 src/added-later.js は、そのとき私の作業ツリーに確かに存在していました。cat すれば読めます。それでもレンダリングは落ちます。検証がピン留めしたコミット基準(実装は git cat-file -t <rev>:<path> と git show の行数)で行われているからで、行番号のはみ出し(R2)も同じくそのリビジョン時点の行数と突き合わせています。
これは地味ですが、私は結構うなりました。図に貼られたソースリンクが腐る典型は「書いた時は正しかったが、その後ファイルが移動・削除された」です。リビジョン付きで固定して、しかもその時点の実在を確かめてからでないと出力させない、というのは、受け入れ基準としてかなり素直な設計だと思います。
通ったときにHTMLへ埋め込まれる証跡はこれでした。
{
"schemaVersion": 1,
"verified": true,
"repository": {
"url": "https://github.com/acme/demo-service",
"revision": "52394e1e53dd347f504df982a2181b93375bb448",
"shortRevision": "52394e1"
},
"referenceCount": 1,
"nodes": {
"gateway": [{
"path": "src/gateway.js", "line": 3, "endLine": 5, "label": "handle",
"href": "https://github.com/acme/demo-service/blob/52394e1e.../src/gateway.js#L3-L5"
}]
}
}
画面側では、ノードをクリックすると出る「SEMANTIC PASSPORT」に VERIFIED SOURCE の欄が生えて、acme/demo-service @ 52394… と handle L3-5 ↗ src/gateway.js が並びます。

上の画面では、右側の図が該当ノード中心に絞られて自動で122%までズームしています。ガイドビューの方も同じ挙動で、章を切り替えると対象5ノードだけが残って他が沈む。

check は何を見ていて、何を見ていないのか
最後に、ここは誤解しやすいと思ったので測っておきました。archify check <output.html> というコマンドがあります。名前だけ見ると「出来上がったHTMLが本物か」を見てくれそうに読めます。
実際に見ているのは9項目で、single_svg / finite_svg / orthogonal_arrows / label_route_clearance / relationship_crossings / relationship_corridors / container_border_runs / route_rhythm / legend_clearance。SVGが1つか、モーションが有限か、矢印が直交しているか、ラベルが線に被っていないか——描画の不変条件の検査です。
なので、こうなります。
# 出力HTMLのラベルを手で書き換えてから check
$ cp R1-verified.html tampered.html
$ sed -i "s|API Gateway|Payments Gateway|" tampered.html
$ node bin/archify.mjs check tampered.html
{ "ok": true, "file": "/work/out/tampered.html", "checks": [ ... ] }
check_rc=0
# visual-check は Chrome が要る
$ node bin/archify.mjs visual-check R1-verified.html
visual-check skipped: /work/out/R1-verified.html
containment skipped; captures skipped; visual review pending
receipt /work/out/R1-verified.visual-check.json
Chrome or Chromium is unavailable. Set ARCHIFY_CHROME to its executable path.
rc=2
改ざんしたHTMLが ok: true で通ります。これは不具合というより役割分担の話で、真正性の担保は「レンダリング時に検証してから出す」側に置かれていて、出来上がったファイルに署名や再検証の仕組みは(少なくとも check には)ありません。成果物を配ってしまえば後は編集し放題なので、レビューで信用するならHTMLではなくIRとリポジトリを見る、という運用になります。
ついでに書いておくと、visual-check の振る舞いは好印象でした。Chromeが無い環境で黙って成功を返さず、rc=2 と理由と次の一手(ARCHIFY_CHROME)を出して、しかも「スキップした」という受領証まで残す。できなかったことを、できなかったと記録するツールは信用しやすいです。
compare の受領証も同じ思想でした。2つのスナップショットを比べると 28/28 checks; completeness complete; authored; sha256 4ef69faff3e8 と出て、JSONには変更点の内訳(コンポーネント: 追加1・変更1・削除1・移動1、接続: 追加1・変更2・削除1・経路変更1、境界: 変更2)に加えて、こう書いてあります。
Authored Architecture IR only; no runtime impact, causality, risk, or mergeability is inferred.
自分が推論していないことを、自分で列挙している。proofLevel も、両側が40桁SHAでピン留めされていれば revision-pinned、そうでなければ今回のように authored と正直に下がります。
試してみての所感
12ケース回して、私が確認したかったことは確認できました。エージェントが吐いた図の定義を、Archifyは受け取ったまま描いたりはしません。存在しないノードを指した辺も、幽霊を囲んだ境界も、モデルが気を利かせて足した余計なフィールドも、全部レンダリング前に落ちます。ソース参照に至っては、コミット単位で実在を確かめるところまでやっていました。
一方で、動いたから品質OK、とは私は言いません。今回測ったのは「壊れた入力を弾くか」だけで、正しい形式のIRが、対象システムを正しく写しているかはまったく別の話です。ノードを1つ書き忘れた図は、スキーマ的にも幾何的にも完璧に妥当なので、当然そのまま通ります。そこはツールが埋められる穴ではなくて、人間のレビューが要る場所です。
自分の使い方としては、当面は「図そのもの」より「IRとレンダリングの決定性」を評価軸に置いて眺めるつもりです。差分が受領証つきで出る、というのは設計レビューの記録としてかなり使い出があるので、次はそこを実プロジェクトのスナップショットで試してみたいところ。逆に、成果物HTMLを信頼の起点にする使い方は、今回の check の挙動を見たあとでは選びません。