前回は、小さなメモAPIを実装し、Archifyの構成図をコードと実行結果に照らして確認しました。次は処理を一つ増やし、図の矢印や説明が古いまま残らないかを追いたいと書きました。
今回は、メモを保存したときに履歴を1行残す処理を追加します。私が気になったのは、箱を増やす必要がない変更でも、図から読み取れる内容を更新できるかです。
メモを保存した直後に、履歴も書き込む
前回のAPIは、Python標準ライブラリとSQLiteだけで動きます。POST /notesでメモを保存し、GET /notes/{id}で取得する小さな実装です。
比較できるように、前回の実装は変更せずに残しました。今回はexamples/archify-notes-api-auditにコピーし、変更前のソースもbaseline/に保存しています。
追加したのは、note_eventsテーブルへの書き込みです。メモのIDとcreatedを記録します。
CREATE TABLE IF NOT EXISTS note_events (
id INTEGER PRIMARY KEY AUTOINCREMENT,
note_id INTEGER NOT NULL,
kind TEXT NOT NULL
);
今回は処理順を追うことが目的なので、履歴を読むAPIや外部通知は作りません。操作者や日時も持たない、検証用の最小限の履歴です。改ざん防止を備えた監査ログではありません。
ここで先に決めたのが、履歴だけ保存できなかった場合の扱いです。メモだけが残ると、今回確認したい関係が曖昧になります。そのため、メモと履歴を一緒に確定し、履歴が書けなければメモも戻す形にしました。
INSERTの後と、コミットの後は分けて考える
変更した中心はnotes_api/store.pyのNoteStore.insert()です。要点だけ抜き出すと、次の順番になります。
with connection:
connection.execute("BEGIN")
cursor = connection.execute(
"INSERT INTO notes(text) VALUES (?)", (text,)
)
note_id = cursor.lastrowid
connection.execute(
"INSERT INTO note_events(note_id, kind) VALUES (?, ?)",
(note_id, "created"),
)
# 正常に抜けた後で、メモと履歴の両方がコミット済み
同じ接続でトランザクションを開始し、二つのINSERTを行います。Pythonのsqlite3.Connectionは、コンテキストマネージャーを正常に抜けるとコミットし、例外時にはロールバックします。接続を閉じる処理は別なので、実装では外側にclosing()を残しています。Python公式ドキュメント
履歴のINSERTで起きたSQLiteの例外は、with connection:の外で受け取ります。StorageErrorに変換し、HTTP側で500を返すようにしました。成功時の応答は、前回と同じidとtextです。
保存の後に履歴を追加、と書くだけでは足りないと感じました。今回の履歴記録は、メモのINSERT後ですが、コミット前です。コミットしてから別接続で履歴を書く実装とは、失敗したときの残り方が変わります。
古い構成図を検証しても、変更漏れでは止まらなかった
先に実行環境を分けておきます。今回もCodexでコードを読み、ローカルのArchify
2.17.0-dev.1を使用しました。Claude Codeで実行した記録ではありません。Archifyは、エージェントが作った仕様JSONから図を描画・検証するスキルです。今回はコードを読んでJSONを編集しています。CLIがソース変更を見つけて、自動で図を更新したわけではありません。Archify公式リポジトリ
ここで、あえて前回の構成図JSONをそのまま検証しました。ブログ用プロジェクトのルートから実行しています。
node .claude/skills/archify/bin/archify.mjs validate architecture \
examples/archify-notes-api-audit/baseline/architecture.json \
--quality showcase --json
結果は、9項目とも合格し、エラー・警告は0件でした。履歴保存の説明がない古い図でも通ります。
これはArchifyがコード変更を見落としたという結果ではありません。このコマンドに渡したのは図のJSONだけで、変更したPythonコードではないためです。図の検証が通ることを、コードとの一致確認に使ってはいけないと改めて感じました。
箱は増えず、説明と矢印のラベルが変わった
前回の構成は、次の5要素でした。
HTTPクライアント → Handler → NoteService → NoteStore → SQLiteファイル
今回はテーブルが一つ増えますが、同じSQLiteファイルを使います。プロセスや外部サービスも増えていません。そこで、この構成図では箱と接続先を変えず、説明を更新しました。

画像をクリックすると元のサイズで開きます。
更新箇所を表にすると、少ない変更でした。
| 場所 | 変更前 | 変更後 |
|---|---|---|
| NoteStoreの説明 | SQL実行・コミット | メモと履歴を同時確定 |
| SQLiteの説明 | notesテーブル | notes / note_events |
| DBへの矢印 | INSERT / SELECT | 2表へ書込 / メモ読取 |
| 補足説明 | 入力不正は保存前に400 | 履歴保存失敗時はメモも戻して500、も追加 |
GET /notes/{id}は引き続きメモだけを読みます。矢印を履歴保存だけの説明に変えると、今度は既存の取得処理が消えてしまうため、メモ読取の意味を残しました。
編集した二つのJSONは、Archifyのcompare architectureでも比較しました。
node .claude/skills/archify/bin/archify.mjs compare architecture \
examples/archify-notes-api-audit/baseline/architecture.json \
articles/diagrams/20260909-archify-audit-after.architecture.json \
articles/diagrams/20260909-archify-audit-comparison.html \
--quality showcase --json
記録された差分は、コンポーネントの変更が2件、接続の変更が1件でした。追加・削除はどちらも0件です。タイトルや補足カードの変更はpresentationChangedにも記録されました。
ただし、これも編集したJSON同士の差分です。必要な更新をJSONに書き忘れれば、その変更は比較結果にも出ません。コードを読む工程の代わりにはなりませんでした。
処理順の図では、コミット前の矢印が一本増える
構成図の箱が変わらないと、履歴をいつ書いているかは分かりにくいままです。そこで、POST /notesの正常系をシーケンス図にもしました。
変更前のAPIも今回あらためて動かし、コードの呼び出しとトレースを確認しています。変更後は、メモのINSERTとコミットの間に履歴のINSERTを置きました。

画像をクリックすると元のサイズで開きます。
追加した矢印だけでなく、コミットの説明を両方を確定に変え、戻り値を返す位置も確認しました。履歴の記録は同期処理です。キューへ送って先に201を返す実装にはしていません。
構成図と変更前後のシーケンス図は、いずれも9項目の検証を通りました。生成HTMLも4種類の画面サイズで検査し、はみ出しはありませんでした。日本語で書いた説明とは別に、Viewerの固定UIとhtml langは英語になります。
履歴だけを失敗させ、メモが残らないことを確かめる
正常系だけでは、コミットの位置を確認しきれないと思いました。そこで、一時DBにトリガーを作り、履歴のINSERT時に意図的にエラーを起こします。
CREATE TRIGGER fail_audit
BEFORE INSERT ON note_events
BEGIN
SELECT RAISE(ABORT, 'injected audit failure');
END;
トリガー内のRAISE(ABORT, ...)で履歴のINSERTを中断します。その例外を受けて、Python側のコンテキストマネージャーがトランザクションをロールバックする流れです。SQLite公式ドキュメント
モックでストアを差し替えず、実際のSQLiteで失敗させました。先に成功したメモを1件保存しておき、失敗した新しいメモだけが戻るかも確認します。

evidence/runtime.jsonから作成した結果の図です。400・500・404は、この検証で想定した応答です。画像をクリックすると元のサイズで開きます。
履歴の保存を失敗させたときのトレースは、次の順番でした。
service.validate
db.transaction.begin
db.note.inserted
db.audit.insert.attempt
db.transaction.failed
http.500
冒頭のHTTPルート判定とJSON解析の記録は省略しています。db.note.insertedはありますが、db.transaction.committedはありません。
このログだけを見ると、メモが一度保存されたようにも読めます。別のSQLite接続から両方の表をSELECTすると、失敗前と同じ1行ずつでした。直前にINSERTしたメモは残らず、最初に成功したメモと履歴はそのまま残っています。
さらに、失敗したメモのIDをGETすると404でした。トリガーを削除して再度POSTすると201になり、別のサーバープロセスで起動し直した後も取得できました。
変えていない経路と、まだ確認していないこと
実装ディレクトリでは、次のコマンドで再現できます。
python3 -m unittest discover -s tests -v
python3 scripts/verify_runtime.py
前回から引き継いだ10件に5件を追加し、15件のHTTP結合テストが通りました。別スクリプトでは、2つの実プロセスを使って6ケースを確認しています。Pythonは3.13.5、SQLiteは3.51.2でした。
最初のテスト実行は、今回も作業環境のソケット制限で止まりました。ローカルHTTPの待ち受けが許可された環境で再実行しています。これはAPIの入力処理やロールバックの失敗ではありません。
入力不正は引き続き保存前に400で止まります。GET /healthはDBを読みません。既存DBに対しては起動時に履歴テーブルを追加しますが、過去のメモに履歴を後付けしないこともテストしました。
一方で、ディスク障害、ロールバック自体の失敗、並行書き込みは試していません。今回の500応答も、POSTのトランザクション内で起きたSQLiteの例外が対象です。DB接続開始やGETでの障害まで、すべて同じ応答になる実装ではありません。
図の差分を見るとき、先に処理の境目を決めておきたい
今回は一つの処理を増やしても、構成図の箱は増えませんでした。変わったのは保存対象と確定の意味で、処理順の図ではコミット前の矢印として表れました。
私には、追加した処理だけを探すより、成功を返す前に何が終わっているべきかを先に決めるほうが追いやすく感じられました。図の説明、トレース、DBに残った行を同じ条件で見られるためです。
ただ、自作した小さなAPIを同じエージェントで実装・照合した記録です。未知のコードへの追従精度や、図の自動更新を評価したものではありません。
次は、保存後の処理を別プロセスに渡した場合も確認してみたいです。今回のように一つのトランザクションで戻せない構成で、成功応答と処理完了の境目がどう変わるかを追うところから始めたいと思います。
ここまで読んでいただき、ありがとうございます。もしこの記事の技術や考え方に少しでも興味を持っていただけたら、ネクストのエンジニアと気軽に話してみませんか。
- 選考ではありません
- 履歴書不要
- 技術の話が中心
- 所要時間30分程度
- オンラインOK