コンテンツにスキップ

変更履歴

書式は Keep a Changelog、版は SemVer。識別子を振り直せない体系で何を破壊的変更と みなすかは方針にある。

未リリース

追加

  • 「ARK とは何か」に修飾子の使われ方を足した。/c3/s5.pdf(章・節の PDF)、 /f12(コマ)、/data/2020.csv(データセットの中のファイル)、.pdf(別形式)を 1 件の採番からどう解決するか、一部だけ register したときに長い一致が勝つこと、 行き先の末尾に / を付けると変種が別のパスになること、 行き先の無い対象では記述を返すこと、行き先を …&part= で終わらせて尾を リクエストパラメータとして渡す書き方(空の値と %-エンコードの扱いも)、書き方の 揺れ(旧ラベル・区切りの重複・ハイフン・クエリ)と揺れではないもの(%2F・ 検査桁)を並べた。表の答えはどれも実際にリゾルバへ投げて得たもの。

修正

  • 0.4.0 の取り下げが、手で書いた文書の 2 か所にまだ届いていなかった。 STATUS は purge を 0.3.0 の「RA の運用者だけ」のまま書いていた(同じ節の上の項目は 「やめた」と言っている)。データモデルの図には first_published_at と保留の列が無く、 published_at の注記と制約の表が「null なら削除できる」のままで、一度公開して 取り下げただけの ARK も軽く消せるように読めた。軽く消せるのは first_published_at が null のもの——before_delete が見るのもそちら——である。

0.14.0 — 2026-09-19

捨てるのを、採るのと同じ安さにした版。

予約でまとめて採るのは実際の使い方で、番号は審査中の対象にときに何年も預けられる。 その一括が取りやめになったとき、これまでは ARK ごとに 1 リクエスト必要で、それは 実際には誰も消さないということである。残るのは、予約という仕組みが避けようとして いた当の「放置された番号」。安い経路の範囲は、失われるもので決まる——誰にも 見られていない名前だけが通る。

追加

  • POST /api/delete/bulk(ark:delete)と、複数 ARK・標準入力を受ける arkhe ark delete。捨てるのは、採ったのと同じ安さでなければならない——予約で まとめて採るのは実際の使い方で、番号は審査中の対象にときに何年も預けられる。その 一括が取りやめになったとき、1 件ずつしか消せないなら実際には誰も消さない。 残るのは、予約という仕組みが避けようとしていた当の「放置された番号」である。

安くしてよい理由は、これらの名前が誰にも見られていないこと。 公開中、または 一度でも公開した行が 1 つでもあれば、新しい ARKHE-1504 でリクエスト全体が 落ちる——その名前は /api/delete を 1 件で通す(理由と ARK の打ち直し)。一部だけ 消えることはなく、名前は従来どおり二度と採られず、監査は 1 件にまとまる。

Python クライアントには delete_bulk() が入り、一括のメソッド名をエンドポイントに 揃えた——mint_bulk / import_bulk / update_bulk / delete_bulk(以前は *_many)。 検査で固定してある: /bulk のエンドポイントは _bulk で終わるメソッドが呼び、 それ以外は呼ばない。クライアントは未リリースなので、古い名前に依存するものは無い。

  • ほかの ARK ソフトウェアとの比較のページ(docs/project/comparison.md、日英)。 arklet / arklet-frick / EZID は読むだけでなく実際に入れて、DB を与えて HTTP で叩いた ——どれが実測かはページに明記してある。arkhe が独自に持つもの(shoulder 単位の採番委譲を 307 で返す・転送の保留・冪等鍵・3 段の到達範囲)と、後から着いたもの(公開ライフ サイクルは EZID の status モデルの言い換え、?info / ?json と suffix passthrough は arklet-frick が先)を両方書いた。

機能単位の対応表を載せた——解決/採番と書き込み/識別子の一生/認可と運用の 4 つの表 で arkhe・arklet・arklet-frick・EZID を並べ、動かして確かめたセルには剣印を付けた。 AMS・greens・arks-service・NOID 系・N2T はより粗い表に。見当たらなかっただけのものは ✗ ではなく「—」にしてある(無いことを確かめたわけではないため)。日英の README からも、 もともと arklet に触れていた「由来」の節から辿れるようにした。

修正

  • 「arklet は採番の衝突を更新に化けさせる」という記述を、時点つきに直した。 arkhe が 出発点にした版では事実だったが、本家では既に直っている。不変条件とデータモデルの ページが現在形で書いていた。

0.13.0 — 2026-09-19

クライアントを持った版。

API でできることが、自分で requests を書かなくても Python から届くようになった。 そして本題はそこではなく、どの呼び出しなら再送していいかを、呼ぶ人ごとに決め直さずに 済むようになったこと。識別子を採り直せない台帳で、その判断を人数ぶん持つ余裕は無い。

追加

  • Python クライアント(clients/python、arkhe-client)。minter と resolver の すべてのエンドポイントを持ち、API 文書には書けない判断を 3 つ行う。

採番は冪等鍵を載せる。 mint() は必ず request_id を送り(渡されなければ生成し)、 再送では同じ鍵を送る。応答が失われても、誰も持たない番号が消費されない。どちらが 起きたかは Ark.resent が言う。mint_bulk() は各行に鍵を置き、これが途中で切れた 一括採番をそのまま送り直せる理由。再送しても二重に効かない呼び出しだけを再送し、 それ以外は TransportError として判断を呼び出し側に残す。

別 minter への 307 を追わない。 追えば自組織の資格情報を他組織の口に送ることになり、 向こうで採られた ARK はこちらの台帳が知らない ARK になる。Delegated として上がり、 .minter と .about を持つ。

断りには ARKHE-xxxx が付く(ArkheError 配下の型付き例外)。コードを持たない 断り——domain が上げ、画面が項目の横に出す {項目: 何が悪いか} の形——も、素の 400 に せず中身を保つ。

生成ではなく手書き。生成が与えるのは網羅で、網羅は検査できるから—— clients/python/tests/test_contract.py がクライアントと公開 OpenAPI 文書を両方向で 突き合わせるので、サーバにエンドポイントが増えるとビルドが落ち、「クライアントも持つべきか」 を決める場面になる。スタブに対して 39 本、本物の一式に対して 14 本。

  • 通しの検査に「採番が委譲された shoulder」(99999/d9)を種として用意した。 単体 テストの中だけでなく、外から 307 を確かめられるようにするため。

変更

  • pytest が clients/python/tests も拾うようにし、英語のみの検査を clients/ にも 広げた。クライアントは、話し相手のサーバと同じ網で検査される。

0.12.1 — 2026-09-19

README の日英が揃っていなかったのを直した版。

英語は README.md、日本語は README.ja.md という分け方は前からのものだが、 片方にしか無い節があった——日本語版に「壊さないもの」とライセンスが無い。 入口の文書でずれているのがいちばん困る。

変更

  • README の日英を揃えた。 README.md は英語、README.ja.md は日本語という 分け方はそのままに、片方にしか無かった節を埋めた——日本語版に「壊さないもの」 (NR が何を守っているか)とライセンスが無かった。

古くなっていた数字も落とした(「403 件のテスト」)。検査の入口は scripts/check.sh 1 本だと書き、通しの検査と種蒔きの道具の走らせ方を足した。

0.12.0 — 2026-09-19

コードの言語を英語に揃えた版。

識別子・コメント・docstring・テスト名——src/ tests/ scripts/ alembic/ compose/ から日本語を無くした。日本語が残るのは、日本語が製品そのものである 3 か所だけ(多言語カタログ、errors.py の ja、文書サイトの日本語ナビゲーション) で、画面も CLI も日英の対はそのままである。

同じ版に、通しの検査を 13 本から 54 本に広げ、その台帳を作る道具を出した。 落ちない検査を「守っている」と数えない——再現しなかった競りの測定結果も、 測ったこととして残してある。

追加

  • コードをすべて英語で書き直した。 識別子・コメント・docstring・テスト名の すべてで、src/ tests/ scripts/ alembic/ compose/ から日本語を無くした ——テスト関数 493 本の改名を含む。

日本語が残るのは、日本語が製品そのものである 3 か所だけ: 多言語カタログ (api/i18n/・cli_i18n.py)、errors.py の ja(参照ページに出す説明)、 そして文書サイトの日本語ナビゲーション。画面も CLI も、日英の対はそのまま。

文書(この変更履歴・STATUS・docs/*.ja.md)は今までどおり日本語で保つ。 読む相手が違う——コードを読むのは実装に手を入れる人で、そこは 1 つの言語で 揃っていたほうがよい。

機械で見るようにした(test_the_code_is_written_in_english)。この種の決まりは 1 ファイルずつ崩れる——日本語のコメントの隣に日本語で書き足すのは、自然に見える。

移行の名前も英語にした(b5f83e2c9014_namespace_rules.py など)。revision id は 変えていないので、鎖も適用済みの履歴もそのままである。

  • 通しの検査の台帳を作る道具を置いた(scripts/seed_e2e.py)。NAAN・組織・ 主体と鍵・管理画面の合言葉を組み、--arks N で状態の混ざった ARK(公開・ 公開前・保留・墓碑・修飾子つき・取り下げ済み)を入れる。

tests/e2e/ がこれを呼ぶ。 検査の側に同じ組み立てを書くと片方が必ず古くなるし、 呼ばれているから、この道具だけが腐ることもない。手で確かめるときも同じ台帳から 始められる——鍵は刷ったその場で見せる。

組み立ては CLI を通す(運用者が実際に使う道でなければ検査にならない)。 途中まで在る台帳にも足せる——検査は落ちる途中で終わるので、「NAAN だけ在って 組織が無い」はふつうに起きる。

  • 通しの検査を 13 本から 54 本に増やした(tests/e2e/)。建てる形は同じ (docker の PostgreSQL + uvicorn で minter と resolver)で、叩く面を広げた:
見るもの
test_shape 役割の分かれ方、/healthz と /readyz、.well-known/ark、CLI と API が同じ数を返すこと
test_resolution 修飾子の継承、ラベルの大小、ハイフン、?info と ??、委譲した NAAN は委譲先へ・知らない NAAN は全体リゾルバへ、公開ページの CSP
test_lifecycle 公開前 → 公開 → 取り下げ → 再公開 → 削除、更新、一括、修飾子の明示登録、保留、墓碑
test_authz scope の外、他組織の ARK には届かない、止めた主体、1 日の上限、client_credentials
test_admin_ui 合言葉で入り、画面の form から取り下げて再公開する。断り方が ID の有無で分かれないこと
test_hardening ARKHE_ALLOWED_HOSTS、閉域のリゾルバ(公開前も解決する)
test_concurrency 同じ request_id を 32 本同時、32 本の同時採番、同時の取り下げ

効くことを確かめた。 create_app から ARKHE_ALLOWED_HOSTS の中間層を外すと test_a_host_that_was_not_allowed_is_refused が落ちる——この設定は 0.9.2 まで死んでいた(宣言 され、文書にも書いてあって、どこからも読まれていなかった)。単体の試験は app を 自前で組み立てるので、中間層が抜けていても気づけない。

  • 再現しなかったことも書いた。 request_id を揃えた再送を接続まで済ませてから 32 本同時に撃っても(送信の散らばり 1.3 ms)、_commit_or_replay の競りは起きない ——先頭が commit し終えるまで、後続が _replay に到達しない(worker 4 つでも同じ)。 直しを外しても落ちないことまで確かめ、競りの守りの検査として読まないよう 試験の側に書いた。落ちない検査を「守っている」と数えない。

修正

  • 残っていた count(*) を 3 か所なくした(採番の上限・部分参照の確認・ 管理画面の一覧)。数える列を名指しする——count(*) は行そのものを数えるので 索引だけでは答えられず、台帳が伸びるほど効く。0.6.0 で domain.stats を そう直したときに、この 3 か所が残っていた。

機械で見るようにした(test_every_count_names_its_column)。「今ある 3 か所を 直した」で終えると、次に書く人がまた func.count() と書く。

0.11.0 — 2026-09-19

測ってから書き、書いた手順を実際に走らせた版。

前の版で出した摘みを、推奨の値どおりに組んで測り直した。worker は 4 まで倍々、 そこから先は 1 割。プールは 30 倍に振っても変わらない。効いた摘みは ひとつだけで、それは rps では見えず、往復を数えて初めて決定的になった。

その過程で、自分がその日に書いた復元手順が動かないことと、/readyz が resolver の読まない DB を見ていたことに気づいた。どちらも検査は緑のまま通る。 走らせないと分からないものを、走らせて見つけた版である。そして、その走らせ方を 検査にした——本番と同じ形に建てて HTTP で叩く、通しの検査を足した。

追加

  • 通しの検査を足した(tests/e2e/、uv run pytest -m e2e)。本番と同じ形に 建てて、素の HTTP で叩く——docker で PostgreSQL を立て、uvicorn で minter と resolver を別々に建て、台帳を CLI で組み、その CLI が刷った鍵で採番して解決する。

ほかの試験は TestClient で app を直に呼び、SQLite の上で、認証を差し替えている ——速いし、認可の網はそちらのほうが細かい。そこを通り抜けるものだけを、ここで 捕まえる: app がファクトリであること(arkhe.app:app では起動しない)、minter に 解決の口が無く resolver に採番の口が無いこと、resolver が書き込み DB に 触れないこと。

resolver の書き込み側は、どこにも繋がらない先に向けてある。 触れば落ちる ——この版で直した /readyz の穴は、戻すと実際にこの検査が落ちることを確かめた。

check.sh の手順 5 になった。docker が無ければ SKIP と出す(黙って通さない)。

  • 推奨の値どおりに組んで測り直した結果を デプロイに載せた。 100 万件の台帳・resolver 4 worker・POOL_SIZE 3 + MAX_OVERFLOW 2 で、 解決は同時 8 で 938 rps(p50 6.5 ms)。

分かったことが 3 つ。同時 8 で頭打ちになり、それ以上は遅延だけが伸びる。 接続は 16 本しか開かない(上限 20、max_connections 200 に対し余裕 179) ——推奨のプールは絞りすぎていない。採番の p50 59.7 ms はほぼ Argon2で、 一括は 55 倍——速いのは DB ではなく、認証 1 回を 1000 件で割っているからである。

  • スケールアウトの推移と、詰まる順番を測って デプロイに書いた。

worker は 4 まで倍々(352 → 718 → 1,413 rps)で、そこから先は 1 割程度しか 動かない。プールは 30 倍に振っても rps が変わらない——変わるのは握る接続の数 だけで、解決は短い問い合わせ 1 回だから溜め込む意味が無い。次に詰まるのは PostgreSQL の CPU(アプリと約 1.4 : 1 で食う)。

測る前に、測っているのが本当にサーバかを確かめる節を先頭に置いた。 bench.py は 1 プロセス約 2,200 rps で頭打ちになる(GIL)ので、そこを超える値は こちらを見ている——プロセスを並べれば 4 つで 9,017 rps まで上がる。

同じ条件の測り直しで ±20% 振れることも書いた。この幅より小さい差は、意味の ある差ではない。

  • ARKHE_DB_PRE_PING——接続の生存確認を切れるようにした(既定は入れたまま)。

切ると、解決 1 件あたりの DB 往復がほぼ半分になる——実測で 1.60 → 0.82 (100 万件の台帳で xact_commit + xact_rollback を数えた)。解決はもともと 索引 1 回しか引かないので、確認の 1 往復が相対的に重い。

rps では見えなかった(ばらつきに埋もれる)。数えたら決定的だった。

  • DB とアプリの摘みを片端から振って、効かなかったものを デプロイに書いた。 uvloop / httptools、shared_buffers(128MB / 512MB / 2GB)、プール 30 倍、 採番の synchronous_commit=off——どれも差が出なかった。

shared_buffers は既定 128MB でキャッシュ命中 99.95%。 解決は索引 1 回と 行 1 本しか触らないので、一般の目安「メモリの 25%」はこの負荷では買えるものが 無い。synchronous_commit も同じで、採番の 60 ms のうち 53 ms は Argon2 ——安全を削っても速くならないなら、削る理由が無い。

  • レプリカを組んで測った結果を デプロイに書いた。

rps は増えない(1,433 → 1,307。同じ台の上なので、レプリカは CPU を増やさない)。 増えるのは主系の余裕で、読みの負荷が完全に外れる(CPU 136% → 0%) ——レプリカが買うのは速さではなく、読みが荒れても採番が巻き込まれないことである。 主系を止めても解決は続くことも確かめた(302、?info も 200)。

複製の遅れは、採番から解決できるまで中央値 3〜5 ms(書き込み負荷の最中でも replay_lag 1.3 ms)。50 回とも 1 回目の問い合わせで解決できた——監視の節に 書いた「採番直後に 404」は、この条件では 1 度も起きなかった。起きうる条件 (別の機械・レプリカが読みで詰まる・大量投入)を明示したうえで、監視は残した。

  • 手順をなぞるための覚え書きを 3 本置いた(.claude/skills/)。 release(版を出す 6 手順)・measure(測り方)・chaos(壊して測る)。

書いてあるのは、間違えた記録である。 版の節をリリース済みの節に足しかけた、 クライアントの天井を測っていた、期限切れの chaos を apply して「効かない」と 読んだ——どれも実際にやった。AGENTS.md が正で、あちらは実行に移すための 覚え書きである。

変更

  • AGENTS.md から、arkhe を組み込む側の手順を外した。 submodule の ポインタを進めるのは組み込んだ側の手順であって、この文書は arkhe の 手引きである。2 か所に書けば片方が必ず古くなる。

修正

  • 復元の手順に書いた起動コマンドが動かなかった。 uvicorn arkhe.app:app と 書いてあったが、この app はファクトリ(arkhe.app:create_app --factory)で、 そのままでは起動しない——直前に自分で書いた手順を実際に走らせて気づいた。 ほかの箇所(Quickstart・分散の手引き・Dockerfile・compose)は正しかった。

  • scripts/bench.py が -n より少なく送っていた。 n // concurrency の 切り捨てで、-n 3000 -c 16 は 2992 しか送らない。応答の内訳を -n と 見比べた人には、8 件落ちたように見える(実際に読み違えた)。端数を配って、 数えている数と送ると言った数を揃えた。

  • /readyz が、その役割の読まない DB を見ていた。 resolver は ARKHE_READ_DATABASE_URL(レプリカ)から読むのに、主系を叩いて確かめて いた——レプリカが落ちて解決が 500 を返しているのに、主系が生きていれば Ready と答え続ける。負荷分散はそのまま送り込む。

実際にレプリカを落として確かめた。読む側を見るようにした(resolver は 読み取り側、minter は書き込み側)。

0.10.0 — 2026-09-18

推奨の形が、既定のままでは動かないことに気づいた版。

resolver 2 台 × 4 worker × 15 接続 = 120 PostgreSQL の既定 max_connections = 100(予約 3 を除いて 97)

1 プロセスあたりの接続は worker 数と掛け算になる。推奨構成を書いておきながら そこで詰まることを書いておらず、しかもプールを調整する手段が無かった——運用者は worker を減らすしかなかった。

摘みを出し、測った値から必要なものだけを推奨として書いた。出典の無い推奨値は 書いていない。

追加

  • 接続プールの摘みを出した(ARKHE_DB_POOL_SIZE / ARKHE_DB_MAX_OVERFLOW / ARKHE_DB_POOL_RECYCLE)。既定のままだと、この手引きが勧める形が動かない:
resolver 2 台 × 4 worker × 15 接続 = 120
PostgreSQL の既定 max_connections = 100(予約 3 を除いて 97)

1 プロセスあたり pool_size 5 + max_overflow 10 = 15 は SQLAlchemy の既定で、 これが worker 数と掛け算になる。解決は短い問い合わせ 1 回(主キーの索引、 0.03 ms)なので、溜め込む必要は無い——worker あたり 2〜3 で足りることが多い。

既定値は変えていない(今までと同じ振る舞い)。接続を黙って切る前段の下では ARKHE_DB_POOL_RECYCLE を保持時間より短くする。pool_pre_ping が拾ってはいるが、 拾うたびに 1 往復を捨てている。

  • 推奨のミドルウェアの値を デプロイに書いた。 接続数を数える式、PostgreSQL(shared_buffers は索引が載る大きさに、 work_mem は既定のまま——解決は索引 1 回で、並べ替えも結合もほとんど無い)、 worker 数、前段の要求上限 1 MB(BULK_LIMIT が 1000 行なので、nginx の既定 client_max_body_size 1m が境目になる)。

測った値から必要なものだけを挙げた。 出典の無い推奨値は書いていない。

0.9.2 — 2026-09-18

守っているつもりで守っていなかった 2 つを直した版。 脆弱性を中心に全体を洗った 結果で、どちらも悪用はできない——が、どちらも「対処済み」と数えられる形をして いた。

ARKHE_ALLOWED_HOSTS はどこからも使われていなかった。設定として在り、参照 ページにも載っているのに、Host を検める仕掛けが無い——効かない安全の設定は、 無いより悪い。無ければ、少なくとも別の手を打つ。

行き先の検証は API のスキーマにだけあり、画面のフォームは素通りしていた。 転送とリンクは許可リストで守っているので実害は無いが、使う瞬間の守りと、入る 瞬間の守りは別のものである。

修正

  • ARKHE_ALLOWED_HOSTS が、どこからも使われていなかった。 設定として宣言され、 参照ページにも載っているのに、Host を検める仕掛けが入っていなかった ——絞ったつもりの運用者が、絞れていないまま「対処済み」と数えていたことになる。 効かない安全の設定は、無いより悪い。

絞ったときだけ弾くようにした(既定の * では何も挟まない——前段で終端している 構成では前段が見ているのが普通で、二重に弾くと切り分けが難しくなる)。

  • 画面のフォームが、行き先の検証を素通りしていた。 javascript: や data: を 拒む検証は API のスキーマにあり、画面は素の文字列を受けて採番していた ——「画面と API に差を作らない」という決まりが、検証の置き場所のせいで破れていた。

悪用はできない。 転送とリンクは許可リスト(http と https だけ)で 守っているので、入っていても転送されずリンクにもならない。だがそれは使う瞬間の 守りであって、入る瞬間の守りは別に要る——許可リストをいつか緩めたとき、台帳に 既に入っているものが効いてくる。ORM の層で拒むようにした(削除の守りと同じ形)。 画面には読める断りも出す。

0.9.1 — 2026-09-18

文書が実装より遅れていたのを直した版。 全体を機械的に洗って見つかった 3 件で、 どれも「今日入れた変更が文書に届いていない」類である。

いちばん重いのは、不変条件のページが 2 版遅れていたこと——この体系が何を 約束しているかを述べるページが、中心の不変条件について古かった。

手順に入れるだけでは足りないことが今日 2 度あったので、STATUS.md の版と移行 head については検査を付けた。

修正

  • 不変条件のページが 2 版遅れていた。「公開した ARK は削除しない」という節が、 0.3.0 の purge も 0.4.0 の公開の取り下げも知らないままだった ——この体系が何を約束しているかを述べるページが、中心の不変条件について 古かったことになる。

出口が 2 つあること(取り下げは戻せる/削除と破棄は戻せない)、ORM の守りが 「今 公開中か」ではなく「一度でも公開したか」を見ること、どちらを通っても NR は守られること(外に出て消された名前は永久に 404 で、別のものを指す ようにはならない)を書いた。同じ言い回しが Quickstart・分散の手引き・データ モデル・CLI の参照にも残っていたので、併せて直した。

  • 版の方針が、自分が 起きたことを知らなかった。 「公開した ARK が削除できるようになる」を MAJOR の 例として挙げていたが、それは 0.4.0 で実際に起きている。線を引き直した ——消せること自体ではなく、理由なしに・打ち直しなしに・届く範囲の外から 消せるようになることが弱体化である。

  • STATUS.md の移行 head が 1 本古かった。 alembic check はスキーマと模型を 見るが、文書は見ない。版と head が実際と一致することを見る検査を足した ——手順に入れるだけでは足りない、というのは今日 2 度目である。

0.9.0 — 2026-09-18

予約したまま忘れられたものが、見つかるようになった版。 公開前の ARK は 放っておくと溜まる——誰も指さない番号が、誰にも気づかれずに残る。

件数では警報を出せない。 10 件でも昨日採ったものなら普通で、1 件でも 3 年前なら 放置である。だから古さで見るようにした。

期限で自動的に消すことはしない。 予約した名前は既に誰かの手にあることが多く、 期限で消せばまだ使うつもりだった名前が黙って二度と採れなくなる ——取り返しがつかない操作を、時計に撃たせない。

これで STATUS.md が挙げていた arkhe 側の宿題は、すべて片付いた。残るのは運用の 作業だけである。

追加

  • 公開前のまま放置された ARK を見つけられるようになった。

arkhe stat にいちばん古い公開前が出る(日付と日数)。arkhe ark list には --older-than N が付き、--state reserved と重ねれば誰も公開しないまま残った 予約が拾える。

件数では警報を出せない。 10 件でも昨日採ったものなら普通で、1 件でも 3 年前 なら放置である——溜まっていることは、数ではなく古さに出る。

期限で自動的に消すことはしない。 予約した名前は既に誰かの手にあることが 多く(下書きの対象に先に刻むのが予約の目的そのもの)、期限で消せばまだ使う つもりだった名前が黙って二度と採れなくなる。取り返しがつかない操作を、 時計に撃たせない——消すかどうかは、その予約を握っている組織が決める。

--older-than は --state と直交させた。「3 年前に採って今も直していない 公開済みの ARK」も、探したい日が来る。

修正

  • arkhe stat が SQLite で落ちていた。 SQLite には時刻帯の型が無いので、 DateTime(timezone=True) と宣言していても naive な datetime が返る (PostgreSQL では aware)。engine によって型が変わり、片方でだけ引き算が TypeError になっていた。

ドメインの側で UTC を付けて返すようにした——CLI だけ直すと、API と画面に同じ 穴が残る。付けるのは推測ではない(台帳は utcnow() しか書かない)。

0.8.0 — 2026-09-18

委譲した先が落ちたことに、気づけるようにした版。 名前空間を委譲すると、その下の 識別子の生死は他人の手に移る——しかも arkhe はそれを知らない(転送を返すだけで、 行き先を取りに行っていない)。

見るべき一覧に、いちばん重いものが載っていなかった。 /.well-known/ark は採番の 委譲先だけを出していて、解決の委譲先が抜けていた。minter が死んでも止まるのは 採番だけだが、redirect が死ねばその下の ARK が 1 本残らず引けなくなる。

arkhe には叩かせない。 この台帳が外向きの通信を一切しないことは、障害の切り分けを 簡単にしている性質で、監視のために手放すには高すぎる——気づく手立ては外に、止める 手立て(保留)は中に。

追加

  • /.well-known/ark に、解決の委譲先(redirect)と shoulder の状態を足した。

以前は minter(採番の委譲先)と about だけが出ていた。壊れたときの重さが いちばん大きいものが載っていなかった——minter が死んでも止まるのは採番だけ だが、redirect が死ねばその下の ARK が 1 本残らず引けなくなる。外形監視で 真っ先に見るべきものが、載っていなければ見られない。

status=delegated だけを見る絞り込みもやめた。 redirect は status と 独立に設定できるので、自分で採りながら解決だけ外へ出している shoulder が一覧から 落ちていた。そこが落ちても、誰も気づけない。

  • 委譲先の外形監視をどう組むかを デプロイに書いた。

arkhe 自身には叩かせない。 この台帳は外向きの通信を一切しない(未知の NAAN にも 302 を返すだけで取りに行かない)——障害の切り分けを簡単にしている 性質で、監視のために手放すには高すぎる。代わりに見るべき一覧を出す。

気づく手立てが外に、止める手立て(保留)が中にある、という分担である。

0.7.0 — 2026-09-18

何を見ていれば約束が守られていると言えるか、を書いた版。 この台帳の約束は 1 つ ——配った名前が、別のものを指さない/引けなくならない——なので、監視もそこから逆に 引いてある。

捨てていた信号を 1 つ拾った。 採番の衝突は前から数えていたのに、呼び出し側が 全員捨てていた。名前空間の枯渇が静かに進む唯一の兆しなので、起きたときだけ 記録に出す。

レプリケーション遅延を、性能ではなく正しさの問題として書いた。 採番した ARK を 配った直後に引かれて 404 が返るなら、それは「少し待てば出る」では済まない。

追加

  • 採番が衝突したことを記録に残すようになった(mint_collision)。

mint() は前から衝突を数えて返しており、docstring も「名前空間の枯渇が静かに 進むのを検知できるようにするため」と言っていた。だが呼び出し側 3 か所すべてが ark, _ = で捨てていた——数えているのに、受け取る人がいなかった。

衝突は 1 回起きても実害は無い(採り直して通る)が、率が上がっていくのは桁を 増やす合図で、それは誰かが見ていないかぎり誰も気づかない。0 のときは出さない ——採番のたびに 1 行増やせば、読む人は速やかに読まなくなる。

  • 監視の設計をデプロイに 書いた。 何を見るかは何を約束したかで決まるので、約束が破れかけている兆しから 逆に引いてある。

レプリケーション遅延を、性能ではなく正しさの問題として書いた。 採番は主系・ 解決はレプリカという構成では、採番した ARK を配った直後に引かれると 404 に なる——ふつうの系なら「少し待てば出る」で済むが、ここでは「その識別子は存在 しない」と判断される。

メトリクスの口は作らない。 ログを取り込む仕掛けは既にあるはずで、2 つ目の 系統を増やすより 1 つに寄せるほうがよい。

0.6.0 — 2026-09-18

戻せることを、確かめられるようになった版。 バックアップから復元したあと、 件数ではなく中身で無傷を確認できる——件数が同じでも行き先が入れ替わっていれば、 識別子は全部壊れている。

これまで確認の手順は文書にベタ書きの SQL だった。貼り間違えても静かに「一致」と 読める種類の検査なので、コマンドにした。

あわせて、復元と PITR を実際に通し、手順・日次と月次のバックアップ・止まって いることに気づく方法をデプロイに 書いた。推奨構成と冗長化の判断も、裏を取ってから書いてある。

追加

  • arkhe fingerprint——復元できたことを、件数ではなく中身で確かめる。 バックアップから戻した台帳で出し、戻す前の値と突き合わせる。件数が同じでも 行き先が入れ替わっていれば、識別子は全部壊れている。
arks       c5e54e5af778420832800a7dc9104eee  53 rows
withdrawn  e3b0c44298fc1c149afbf4c8996fb924  0 rows

2 行に分けてあるのは、潰すと「どこが違うか」が消えるからである。 とくに withdrawn(二度と採らない名前)が落ちても採番は動き続けるので、合わせて 出さなければ黙って通る——NR を守る仕掛けの片側が欠けたまま、何事も無く動く。

保留は入れていない。 期限で勝手に変わるので、差が出ても「壊れた」と読めない ——鳴りっぱなしの警報は、誰も見なくなる。 題名や記述も入れない。失われれば 困るが、識別子が別のものを指すのとは重さが違う——混ぜると、重い差と軽い差が 同じ 1 つの値に潰れる。

これまでデプロイには md5(string_agg(...)) の SQL がベタ書きしてあった。貼り間違えても静かに 「一致」と読める種類の検査なので、コマンドにした。DB の方言にも依らない。

0.5.1 — 2026-09-18

冗長構成を検討していて踏んだ 1 件だけの版。 同じ request_id が同時に届くと 500 を返していた——負荷分散の再送や、応答を待てなくなった呼び出し側の投げ直しが、 別の minter に同時に当たる形で起きる。

台帳は壊れていなかった(控えの一意制約が効いていた)。壊れていたのは応答のほうで、 呼び出し側から見ると「採番できたか分からない」——そこで別の request_id で投げ直せば、 1 つの対象に 2 つの ARK が付く。

再送に同じ答えを返すという約束は、順次でも同時でも同じでなければならない。

修正

  • 同じ request_id が同時に届くと 500 を返していた。 再送の判定と控えの 書き込みのあいだに隙があり、負荷分散の再送や、応答を待てなくなった呼び出し側の 投げ直しが別の minter に同時に届くと、どちらも「まだ無い」と見てから、どちらも 書きにいく。守り自体は DB に在る(one_ark_per_request_id)ので台帳は壊れて いなかったが、負けたほうに 500 が返っていた(実測で 200 要求中 102)。

呼び出し側から見ると「採番できたか分からない」がいちばん困る応答で、 別の request_id で投げ直せば二重採番になる。負けたほうは、勝ったほうが 書いた ARK をそのまま返すようにした——再送に同じ答えを返すという約束は、 順次でも同時でも同じでなければならない。

一括採番(/api/mint/bulk)にも同じ隙があった。そちらは一度だけ組み直す ——勝ったほうの控えが見えるので、全件が再送ぶんとして揃う(採番は増えない)。 一括は 1 件でも落ちれば 1 件も作らないので、競合で落ちる理由が無い。

0.5.0 — 2026-09-18

台帳を数えられるようになった版。 何件あるか、どこに寄っているか、直近どれだけ 採番したか——運用の相談で最初に要る数字が、CLI・API・管理画面のどれからでも同じ形で 見られるようになった。

数えるのは 1 か所だけにしてある。 3 つの入口が別々に集計を書けば、同じ「件数」が 場所によって違うという、いちばん質の悪いずれが起きる。

合計も到達範囲で絞る。 組織は自分の shoulder、NAAN 管理者は自 NAAN—— 合計は在ることを漏らすからである。そして公開後に消した数は、合計から分けて 数える。あれはこの体系が守ると言っているものを破った回数で、埋めてよい数字ではない。

追加

  • 台帳の統計を見る口を足した(arkhe stat / GET /api/stats(ark:read)/ 管理画面の /admin/stats)。 ARK(公開/公開前)、取り下げた名前、shoulder の状態、組織、主体、今かかって いる保留、直近 24h / 7 日 / 30 日の採番、最初と最後の採番、そして shoulder ごとの内訳。CLI は --json で機械にも渡せる。

数えるのは 1 か所(domain/stats.py)にした。 画面・CLI・API が別々に 集計を書くと、同じ「件数」が場所によって違うという、いちばん質の悪いずれが 起きる。絞り込みを queries.py に置いたのと同じ理由で、3 つの入口が同じ数を 出すことはテストで縛ってある。

管理画面はダッシュボードにしていない。 この画面は台帳であって、見せるべきは 大きな数字ではない——図表を持たず、紙面を使ったのは公開後に消した数を合計から 分けて置くことのほうである。

合計も到達範囲で絞る。 組織は自分の shoulder、NAAN 管理者は自 NAAN、RA は 全部——合計は在ることを漏らすからである。他組織の ARK が何件あるかはその 組織の規模であり、こちらが教えてよい事実ではない。--naan と --org は一覧と 同じ絞り込みを重ねるだけで、範囲を広げる手段にはならない。

取り下げた名前は、公開後に消したものを分けて数える。 予約を引っ込めたのと、 世に出した名前を消したのとでは意味がまるで違う——後者はこの体系が守ると 言っているものを破った回数である。合計に埋めてよい数字ではない。

費用は行数に比例する。 一覧が数えずに済ませているのと違って、こちらは数 そのものが目的なので走査は避けようがない。避けようがないなら回数を減らす ——公開の別・最初と最後・3 つの窓・保留はどれも同じ絞り込みへの集計なので、 条件つき集計で 1 回の走査に畳んである(素朴に書くと ark を 7 回読む ところが 2 回)。速さのためだけではない——別々に数えると、その間に増えた 分だけ数どうしが食い違い、24h が 7d を上回るような見え方になる。

実測は 30 万件で約 110 ms。毎秒叩く口ではない。

0.4.0 — 2026-09-18

公開を取り下げ、出し直せるようになった版。 0.3.0 では published_at は一方通行で、 「公開を取り消す道は作らない」と決めていた。覆したのは、取り下げの判断が対象を 持っている組織のところにあるからである——公開してはならなかったものに気づくのは RA ではなく預けた側で、そこから RA に上げて戻ってくるまで出たままになる。 止められるのが遠すぎると、止まらない。

できるのは、それぞれの届く範囲の内側だけ——組織は自分の shoulder、NAAN 管理者は 自 NAAN、RA は全部。ここは緩めていない。緩めれば、ただの乗っ取りになる。

代わりに、儀式の重さを主体の位ではなく名前の履歴で決めるようにした。 一度も 公開していない予約の削除は今までどおり軽く、一度でも外に出した名前は理由と 打ち直しを要求する。危ないのは「誰が消すか」ではなく「何が消えるか」である。

NR は破れていない。 取り下げた名前は誰のものでもなく、別の対象に振り直す道は どこにも無い。残った参照は 404 になるだけである——壊れるのは「解決し続ける」ほうで、 「別のものを指さない」ほうではない。

追加

  • 公開を取り下げられるようになった。 0.3.0 では published_at は一方通行で、 「公開を取り消す道は作らない」と明記していた。覆したのは、取り下げの判断が 対象を持っている組織のところにあるからである——公開してはならなかったものに 気づくのは RA ではなく預けた側で、そこから RA に上げて戻ってくるまで出たまま になる。

  • POST /api/unpublish(ark:unpublish)——行は残したまま解決を止める

  • POST /api/publish ——取り下げたものを出し直すのも同じ口
  • arkhe ark unpublish、管理画面の ARK 詳細にも同じ操作

できるのは、それぞれの届く範囲の内側だけ。 組織の管理者は自分の shoulder、 NAAN 管理者は自 NAAN、RA は全部——更新や tombstone と同じ 3 段の判定 (authz.assert_may_touch)をそのまま通る。

  • ark:unpublish scope を新設した。 出せることと引っ込められることは別の判断 である。ark:delete とも分けてある——取り下げは戻せるが、削除は戻せない。 同じ鍵にすると、戻せるほうを使いたいだけの主体に、戻せないほうまで渡る。

変更

  • 儀式の重さを、主体の位ではなく名前の履歴で決めるようにした。 新しい列 ark.first_published_at(初めて公開した時刻)は片道で、取り下げても消えない ——外に出た名前は、引っ込めても「出ていなかったこと」にはならないからである。

一度も公開していない予約の削除は今までどおり軽い。一度でも公開した名前を 引っ込める・消すには、理由(reason)と、ARK を打ち直す confirm が要る (ARKHE-1017 / ARKHE-1018)。危ないのは「誰が消すか」ではなく 「何が消えるか」である——RA の運用者が誰も見ていない予約を消すのは軽く、 組織の管理者が 3 年前から引用されている名前を引っ込めるのは重い。位で決めると、 この 2 つが逆になる。

  • /api/purge の「RA の運用者だけ」をやめた(authority=system の要求を外し、 届く範囲と scope だけで縛る)。公開を取り下げられるようにした以上、 unpublish → delete の 2 手で同じ結果に届く——1 手だけを位で縛っても、 守っていることにはならない。理由・打ち直し・監査・名前を解放しないことは すべてそのままである。

  • 公開中の ARK への /api/delete の断り方が変わった(ARKHE-1501)。 「公開した ARK は削除できない」ではなく、「先に公開を取り下げるか、 /api/purge で一手に行う」になった。

  • 既存の ARK はすべて「一度公開した」ものとして移行する(published_at を first_published_at に写す)。今まで公開は取り消せなかったので、それが実態で ある——ここを埋めなければ、既存の公開済み ARK が「一度も出していない」ことに なり、理由も打ち直しも無しに消せてしまう。

BREAKING CHANGE: ark に first_published_at が増える。ark:unpublish scope が 新設され、ARKHE-1310(破棄は RA だけ)は無くなった。

0.3.0 — 2026-09-17

採った名前を、公開するまでは消せるようになった版。 これまで削除の口はどこにも 無かった——NR(再割当てしない)を宣言する体系だからである。だが NR が縛るのは、 外へ出した名前であって、採番した瞬間の名前ではない。下書きの対象に先に番号を振るのは 普通の運用で、その登録が取りやめになったとき、誰も指さない番号が永久に台帳に残る ほうが、約束を守っていることにはならない。

境目は Ark.published_at 1 つで、一方通行である。公開すれば、もう消せない ——RA の運用者が跡を残して破棄する POST /api/purge を除いて。既定は今までどおり 「採番と同時に公開」なので、既存の呼び出し側はそのままでよく、既存の ARK はすべて 公開済みとして移行する。

あわせて、alembic upgrade head が SQLite で流れなかったのを直した。 Quickstart に 書いてあるとおりの手順が 3 版のあいだ途中で止まっていた——PostgreSQL でしか往復させて いなかったためである。

追加

  • 公開前の ARK を削除できるようになった。 これまで削除の口はどこにも無かった ——NR(再割当てしない)を宣言する体系で、解決が止まることを許さないためである。 だが NR が縛るのは、外へ出した名前であって、採番した瞬間の名前ではない。 下書きの対象に先に番号を振るのは普通の運用で、その登録が取りやめになったとき、 誰も指さない番号が永久に台帳に残るほうが約束を守っていることにはならない。

そこで Ark.published_at を足した。null は「まだ公開していない」で、その ARK は 解決せず(リゾルバは知らない名前と同じに答える)、削除できる。

  • POST /api/mint に {"reserve": true} ——公開前として採る
  • POST /api/publish ——グローバルに公開する。ここから先は消せない
  • POST /api/delete ——公開前のものだけを取り下げる(公開済みは 409)
  • arkhe ark publish / arkhe ark delete、管理画面の ARK 詳細にも同じ操作
  • ARKHE_RESOLVE_UNPUBLISHED ——閉域のリゾルバは公開前の ARK も解決する

境目は一方通行である。 公開は取り消せない——取り消せるなら、公開前の削除に 意味が無い。既定は今までどおり「採番と同時に公開」なので、既存の呼び出し側は そのままでよい(既存の ARK はすべて公開済みとして移行する)。

解決するかどうかは、ARK の状態だけでは決まらない。 どのリゾルバが答えているかで 決まる——ARKHE_RESOLVE_UNPUBLISHED=1 で建てた閉域のリゾルバは、公開前の ARK も 解決する。閉じた網の中で採番した ARK を、その網のリゾルバが引けないなら配る意味が 無いからである(「閉じた対象にも同じ形の PID を配る」という目的そのものが壊れる)。 既定は解決しない側——?info と ?? は認証を要さない口なので、取り違えたときに 漏れる側を既定にしない。

取り下げた名前は解放しない。 withdrawn_name に移り、採番でも取り込みでも 二度と当たらない。予約した文字列は既に誰かの手にあることが多く(それが予約の 目的そのものである)、別の対象に振り直せば、外からは NR 違反と見分けが つかない。scope も ark:delete を新設して分けてある——採番できることと、 取り下げられることは別の判断である。

  • 公開した ARK を破棄する口を、1 本だけ開けた(POST /api/purge / arkhe ark purge、ark:purge)。 この基盤は「一度配った名前が別のものを指さない」ためにできていて、公開した ARK が 消えないことはその中心にある。それでも口を開けたのは、現実の運用に、識別子より 重い要求が来ることがあるからである——裁判所の削除命令、公開してはならなかった 個人情報、取り違えて一括投入した大量の行。逃げ道が無ければ、そのとき誰かが DB を 直接叩く。跡の残らない削除が、いちばん悪い削除である。

だから「できないこと」にはせず、通れる道を 1 本だけ作って、跡を残す:

  • RA の運用者だけ(ark:purge かつ authority=system)。NAAN 管理者にも 組織にも渡さない
  • 理由が要る(空では通らない)。残らない破棄は、無かったことと同じである
  • confirm に ARK を打ち直させる。 一覧を回すスクリプトが、意図せず全件 消すことがないように
  • 名前は返さない。 withdrawn_name に移り、二度と採られない
  • 監査に全件残る

最後の 2 つが効いている。行き先と記述は消えるが、その名前が別のものを指すことは 絶対にない——残っている参照は 404 になるだけで、NR は破らずに済む。破れるのは 「解決し続ける」のほうである。

ORM の側でも塞いである。公開した行は、そのセッションが「この ARK を破棄する」と 名指ししていないかぎり落とせない(models.sanctioned_purge)。大域の旗にすると 立てっぱなしが起き、気づくのは消えてはいけない行が消えた後になる。

変更

  • 不変条件の言い方を狭めた。「ARK は削除しない」は「公開した ARK は削除 しない」になった。守っているものは変わっていない(外へ出した名前は今までどおり 消えない)が、約束していた範囲を明示的に書き直したので、 版の方針の一覧も 併せて直してある。

修正

  • alembic upgrade head が SQLite で流れなかった。 Quickstart に書いてある とおりの手順(ARKHE_DATABASE_URL=sqlite:///… → alembic upgrade head)が、 3 本目の移行で NotImplementedError で止まっていた。SQLite には制約を 付け外しする ALTER が無いので、create_foreign_key と drop_constraint は batch(表を作り直して移し替える)を通す必要がある。部分索引の where 節も 素の文字列ではなく sa.text() で渡さないと落ちる。

0.0.9 から 3 版のあいだ入っていた。PostgreSQL でしか往復させていなかった から見えなかった——あちらを本番として検証する方針は変えないが、 文書に書いた手順が動くことは別に確かめる必要がある。tests/test_migrations.py が SQLite で upgrade → downgrade base → upgrade を流し、移行後のスキーマが モデルの宣言と同じ表と列を持つことまで見る。

PostgreSQL の挙動は変わらない(batch_alter_table は方言を見て素の ALTER を 出す)。列の幅を広げる移行だけは SQLite で何もしないままにしてある ——varchar の長さを見ない DB で、PK と外部キーを持つ表を作り直す危険を冒す 意味が無いため(alembic check を SQLite に対して打つと、その差分だけが出る)。

  • 文書の例示 ARK が、この台帳では採番できない識別子だった。 リファレンスと概念の 頁は ark:99999/x9abc を見せていた——blade が 3 文字、検査桁が無く、a は betanumeric に無い。実物を見せていたのは walkthrough だけ(ark:99999/c7w545sj4z5) で、頁どうしで名前の長さが食い違っていた。読者は例からその形を覚えるのに、 教えていた形は mint が作れない形だった。

すべての例を shoulder + betanumeric 8 文字 + 検査桁 1 文字に直し、検査桁は実装の noid_check_digit で計算した。walkthrough が実際に採番している ark:99999/x9tn1qkq2g7 をサイト全体の通しの例にし、転記ミスの例はその 1 文字違い (…g8)にしてある——ARKHE-1403 が「無い ARK」と別の答えである理由が、例その ものから読める。OpenAPI の説明文に出る例も同じ形にした。

検査を置いて固定した(test_docs)。文書と生成した OpenAPI に出る ark:99999/… は、betanumeric だけでできていて、検査桁が合わなければならない。 わざと合わない 2 つは理由とともに並べ、本当に合わないことと、文書にまだ在ること も見る——許可が腐って穴が開いたまま、に気づけなくならないように。

0.2.0 — 2026-09-07

閉じた台帳が、その名前を公開の台帳へ渡せるようになった版。 外から到達できない網の 中で採番した ARK を公開台帳に引き取り、同じ識別子のまま公開できる——これまでは 禁輸が明けたときに別の ARK を採り直すしかなく、閉じているあいだに配った参照がそこで 全部死んでいた。境界の両側で arkhe を動かすときの、いちばん大きな穴だった。

委譲まわりも同じ場面から見直した。minter が両立しない 2 つの意味を背負っていた ことと、それを背負わせていた制約を外してある。規模の見積もりは、主張ではなく実測に なった。

追加

  • 規模の見積もりを、当てずっぽうではなく実測で書いた。 デプロイに 解決と採番の処理量・遅延を、 データモデルに ARK 1 件あたりのバイト数と、自分の台帳で測る SQL を載せた。すべて 100 万件の台帳に 対して 1 台で取った値で、そう明記してある——考えるための形であって保証値ではない。

見積もる前に知っておく価値のある発見が 2 つ。解決は台帳の大きさを気にしない (索引 1 回。100 万件も 1000 件も同じで、処理量は worker 数に比例する)。そして 1 件ずつの採番を縛っているのは DB ではなく Argon2 である——82 ms のうち 53 ms が API キーの照合で、だから一括採番が 20 倍速く、規模のある投入では唯一の正気な選択肢 になる。

  • scripts/bench.py — その数字を取った負荷測定の道具。check.sh からは呼ばない ——負荷測定は機械とその日に左右されるので、緑と赤では語れない。測る前に温め、 文書には「まず /healthz を測れ」と書いた。サーバより先に飽和したクライアントは、 自分自身を測っているだけだからである。

  • 手引きの 10 節が、台帳 2 つを組むところから始まるようになった。 公開側と閉域側は 別々に運用する——DB も複製も同期も共有しない——ので、例もそこから始める: arkhe naan add / onboard / shoulder add と、外に「この名前空間は閉じている」と 伝える delegated + redirect の組。名前空間の割当は REST ではなく CLI の仕事 だということも書いた。

そのうえで両側に curl で採番する——公開 shoulder は普通の道、非公開 shoulder は 公開側の minter が 307 で委譲先を案内して断る——引き取りの前後で同じ名前を 両方のリゾルバに尋ねる。台帳 2 つ・プロセス 4 つ、1 回の実行から写した 1 本の記録。

  • 閉じたまま配って、あとで公開する例を 手引きの 10 節に足した。 閉域で採番し、公開のリゾルバが引き取り前後で何を答えるかを見て、記述だけで取り込み、 申請窓口へ、そして対象へと段を上げる。断られる例も。台帳 2 つ・プロセス 4 つで、 応答はすべて実際に動かした対から写した。

  • POST /api/import と /api/import/bulk で、外で採番された ARK を取り込める ようになった(1 本ずつでも、委譲した shoulder ごとまとめてでも)。公開側の下に閉じた arkhe を置く構成で、これが最大の穴だった——閉域で採った名前は公開台帳に載せられず、 禁輸が明けたときに別の ARK を採り直すしかなかった。閉じている間に配った参照が そこで全部死ぬ。これからは 行き先を 1 回書き換えるだけ で、同じ識別子のまま公開できる。

取り込みは採番ではないので scope も別(ark:import)。採番は「番号をもらう」 操作、取り込みは「この番号だと言い張る」操作である。検査は 3 つでどれも緩めない ——shoulder が委譲されていること、名前がその内側にあること、検査桁が合うこと (外から来た名前を信じる手立てはこれしかない)。さらにその NAAN の権威をこの台帳が 持っていることも要る。到達範囲はほかと同じ規則で上位が下位を覆い、一括は 1 件でも 落ちれば 1 件も作らない——入ってしまった名前は引っ込められないからである。

修正

  • 委譲に行き先を要求しないことにした。 minter と about を分けたことで 「どちらに書くか」は解けたが、そもそも間違った値を入れさせていた圧力は残って いた——初版から在る CHECK 制約が、委譲した shoulder にどこかを名指しさせて いたのである。閉域へ委譲した名前空間には名指せる先が無いので、制約を満たすには 嘘を書くしかない——内部ホスト名か、API を約束する欄に置いた人向けのページか。

委譲とは「この台帳では採番しない」と刻むことで、どこで採るかの公示はそれとは 別の、任意の事柄である。どちらも無い shoulder への採番要求は 403 ARKHE-1309 ——「採番は外で行われている」——と答え、URL を騙らない。about は在るときだけ detail に載る。

  • shoulder.minter が、両立しない 2 つの意味を背負っていた。 これは /.well-known/ark に公開され、採番要求に返す 307 の Location にもなる ——どちらも機械可読で「ここを叩けば採番できる」と言う。ところが閉域へ委譲した 名前空間には、外の人が叩ける口が存在しない。それで文書は「代わりに人向けの説明 ページを書け」と案内していた——それは契約を嘘にする。クライアントは 307 を 追ってウェブページに POST し、応答からは API とページを見分けられない。

人が読むページは shoulder.about に置くことにした。委譲にはどちらか一方が要る (CHECK 制約を minter <> '' から minter <> '' OR about <> '' に移した)。 採番要求への答えはこうなる。

  minter がある  →  307、Location: その minter        (従来どおり)
  about だけ     →  403、ARKHE-1309、本文に URL

403 の枝は元から例外ハンドラに在り、一度も通らなかった——委譲に minter を 必須にしていたからである。/.well-known/ark は 2 つを別々の鍵で出すので、読む側が 叩ける口とページを区別できる。既存の行は動かさない——どの minter が実は説明 ページだったかは、台帳には分からない。

  • 「打ち間違いも同じページに着く」は誤りだった。 検査桁は shoulder を見る前に 検証されるので、委譲した shoulder の下の綴りの壊れた識別子は説明ページではなく 404 ARKHE-1403 になる。実際に区別がつかないのは実在する名前と一度も採られて いない名前のほうで、意味があるのもそちらである——手引きをその形に直した。

  • arkhe shoulder add と arkhe onboard が、作った id を表示するようになった。 ほかの shoulder コマンド——status / redirect / hold add shoulder——はすべて その id を取るのに、作った当人だけが教えてくれなかった。知るには後から shoulder list を引くしかなく、文書の例もそれを黙ってやっていた。

  • 英語版の分散の手引きが、shoulder.redirect の CLI をまだ「無いもの」に挙げていた。 arkhe shoulder redirect は在り、コマンドの参照ページにも載っている——日本語版だけ 直っていて英語版が残っていた(済んだ項目は消す。取り消し線では残さない)。

  • 尾が行き先にそのまま連結されることを、手引きに書いた。 suffix passthrough は 名前より後ろを行き先に足すので、クエリを持つ行き先では尾がその中に潜り込む (…/view?id=1 + /page/3 → …/view?id=1/page/3)。挙動は変えていない ——書かれていなかっただけである。

  • 分散の手引きが、保留をまだ「無い」と書いていた。 「転送を一時的に止める手段」が 無いものの節に残っていた——0.0.9 で入っている(ARK / shoulder / NAAN 単位、 理由と期限は公開され、時計で戻る)。そのとき消し忘れただけの項目である。

0.1.0 — 2026-09-07

仕様との差を埋め、外に向かって日本語で話すのをやめた版。 draft-kunze-ark-42 に 対する既知の不適合——MUST 4 件と SHOULD 3 件——がすべて無くなり、arkhe が自分の台帳の 外に出すものは英語になった。OpenAPI と、符号を持つ誤りの本文(文面ではなく符号で 判定できる)である。管理画面と ?info のページは、代わりに画面の言語で答える。

公開している出力の形が変わる、最初の版でもある。 変わった形は下の「変更」と 「修正」に挙げてある。台帳そのものは触っておらず、マイグレーションは列幅の拡大だけ。

追加

  • PATCH /api/update は送った項目だけを書く。 同じパスの PUT は置き換えで、 title を省けば空になる——置き換えとしては正しいが、実際にいちばん多い「対象が 移った」には合わない。行き先を付け替えるのに記述を失わなくなった。空文字を送れば 今までどおり消えるので、値を消す手段も残っている(この 2 つは区別できなければ ならない)。

  • ?info は求められた媒体で答える。 §5.2 は「THUMP の応答の形は返す content type が示す」と書いている。既定は人が読むページ、Accept: application/json なら JSON、 Accept: text/plain なら ?? と同じ ANVL。中身はどれも同じ「記述+永続性宣言」 である(§5)。?json は残す——その JSON を名指しする別名で、?info に同じ Accept を送ったときと同じものを返す。

  • ?info のページを翻訳するようにした。 ここは公開の口で、ARK は世界中から 引かれるのに日本語しか話しておらず、識別子は届いたのに説明が読めない状態 だった。管理画面と同じ catalogue から語を採る。永続性の水準の表示名も同じで、 それは ?json の commitment_label にも出る。この口で ?lang= は使えない ——クエリ文字列そのものが inflection なので、言語は & の後ろ(?info&lang=en)か Accept-Language で渡す。

  • 識別子 1 本を端から端まで追う手引きを足した (curl で採番から解決まで)。 採番、解決、suffix passthrough、修飾子 1 点の登録、対象の移動、保留、墓碑、 誤りの符号、リゾルバが自分について答えること。載っている応答はすべて、 動いているインスタンスから写したもので、手で書いたものではない。

  • 口ごとに、要求する scope が仕様書に出るようになった。 これまでは {"oauth2": []}——「トークンで入れる」とは書いてあるが、何が要るかは書いていない ので、仕様書から起こしたクライアントは全部入りの scope を要求するしかなかった。

    POST /api/mint {"oauth2": ["ark:mint"]} PUT /api/tombstone {"oauth2": ["ark:tombstone"]} POST /api/query {"oauth2": ["ark:read"]}

scope を載せるのは oauth2 の要求だけ。bearer は type: http で、OpenAPI は oauth2 以外のスキームに scope を書くことを許さない(空配列でなければならない)。 宣言と本体の検査が一致することは、検査で固定してある。

  • トークンの取り方が OpenAPI に載るようになった。 securitySchemes に clientCredentials フロー(tokenUrl と scope の一覧)が無く、「これは OAuth 2.0 である」と機械可読な形では言っていなかった——エンドポイントの URL は README と 手引きにしか無く、仕様書からクライアントを起こすと認証の取得手順が落ちた。 Swagger UI の Authorize からもトークンを取れる。

ARKHE_AUTH に oauth2 を含む構成でだけ載せる。 口の無い構成で広告すると、 生成したクライアントが取りに行って 404 を踏む。scope の語彙は authz.SCOPES から、 その説明は管理画面と同じ語から起こしている。

  • トークンを保ち回して使う作法を認証の手引きに書いた。 expires_in が尽きる手前で取り直すもので、API 1 回ごとに取り直すものではない ——発行のたびに client_secret を Argon2 で照合するので、そうすると apikey と 同じ重さを払ったうえに往復が 1 回増える。

変更

  • /.well-known/ark は既定で text/plain を返すようになった(仕様どおり)。 draft-kunze-ark-42 §5.6 は ark を Well-Known URIs レジストリ(RFC 8615)に 登録し、応答を「リゾルバのルートパスを含む plain text、末尾は /」と定めている ——そこに compact ARK を継ぎ足すと解決の要求になる。arkhe は同じパスを先に独自の JSON 在庫に使っていたので、仕様どおりに読むクライアントからは「このホストに ARK リゾルバは無い」に見えていた。

    $ curl https://ark.example.ac.jp/.well-known/ark /

在庫——預かっている名前空間、委譲した shoulder とその minter、止めている名前空間 ——は同じ URL の JSON 表現になった。Accept: application/json を付けて求める。 Accept を付けずにその JSON を読んでいたものには影響がある(中身は resolver_path が増えただけで変わらない)。どちらの表現にも Vary: Accept が付く。

パスは ASGI の root_path から採るので、プレフィクス付きでマウントする構成は 設定すること(uvicorn --root-path /pid)——さもないと無い口を案内する。

修正

  • ARK の説明ページが短すぎ、行き先も示していなかった。 図が NAAN と name しか示しておらず、label・shoulder・blade・修飾子——何も引かずに文字列だけから 構造を読めるという要点を担う部分——が落ちていた。arks.org の定義に合わせて構成を書き直し(錠前の比喩も含めて)、このページは入口であって 体系そのものは arks.org にあると明記した。

  • クイックスタートの「手元で動かす」例では解決できなかった。 1 プロセスを起動して /ark:99999/… を叩いていたが、解決の口は ARKHE_RESOLVER=1 のときだけ生える ——1 プロセスでは 404 になる。分けてあること自体が設計なので、例も 2 つ起動する 形に直した。あわせて解析の誤りが二重に名乗るのをやめ (Not readable as an ARK: Not a valid ARK: missing name part)、 空の NAAN が「長すぎる」と言わないようにした。

  • 日本語版「壊さないもの」のページ内リンクが切れていた。 #ark が何も指して おらず、意図していた見出しは #ark-は削除しない。mkdocs はページ内アンカーの 切れを INFO で流すので --strict でも止まらず、見出しを書いた時点から静かに 死んでいた。check.sh と deploy-docs.sh で落とすようにした——読む側から見れば、 ほかのリンク切れと同じものだから。

  • 管理画面は言語を切り替えられるのに、断りだけ切り替わらなかった。 画面は ?lang= → cookie → Accept-Language で切り替わるが、断りの文面 15 か所—— 「この主体の範囲外」「NAAN 単位以上の権限が要る」、ログイン画面の「ID か パスワードが違います」——が直書きの日本語で、英語で使っていても いちばん困っているところで母語から落ちていた。画面と同じ catalogue から 採るようにしたので、片方の言語しか無ければ起動時に落ちる。

  • 起動時の設定エラーを英語にした。 Settings.check() は設定の欠けを配る前に 止めるもので、その文面は運用者の端末(コンテナのログ)に出る。arkhe がそこに 出す他の記録と言語を揃えた。

  • 誤りに符号が付き、本文が英語になった。 文面は変わる(分かりやすく直る、訳が 変わる)ので、文面で判定するクライアントは砂の上に立っていた。API と解決の口が 返す誤りは、すべてこの形になる。

    {"code": "ARKHE-1011", "message": "A request holds at most 1000 rows.", "detail": {"limit": 1000}}

detail には文面を埋めた値が構造化されたまま入る。数字を文から切り出させない ため。解決の口は text/plain なので符号を行頭に置く (ARKHE-1403 ark:99999/x9abcd — Check digit mismatch: …)。

一覧はエラーの参照ページ。 符号・状態符号・英語の文面・日本語の説明を 1 つの表(arkhe.errors)に持ち、 そこから起こしている——符号を足して書き忘れる、番号を再利用する、片方の言語 しか無い、のいずれも検査で落ちる。

/oauth/token だけ意図して形が違う。 RFC 6749 §5.2 に従い (error / error_description。OAuth のライブラリがそう読む)、符号は併記する。 管理画面は自前の catalogue のまま——あちらは画面の言語で答えるもので、 この API の言語とは別だから。

  • 公開している OpenAPI が日本語だった。 FastAPI と Pydantic は docstring を description にするので、日本語でコメントしているこのコードでは仕様書も日本語に なる——読者はこの台帳の外にいて、日本語が読めるとは限らないのに。

仕様書に出る文字列をすべて英語にした。概要、タグ、各エンドポイント、各応答符号、 リクエストのスキーマとその項目、2 つの認証スキーム。docstring は日本語のまま 残す——あれは実装を読む人のためのもので、読者が違う——ので、英語は description=(ルート)と model_config(モデル)で明示的に渡している。 FastAPI も Pydantic も、渡された側を docstring より優先する。

生成した仕様書を両方の役割で走査して CJK があれば落ちる検査を置いた。口を 足すたびに人が思い出す前提にしない——渡し忘れると docstring がそのまま 仕様書に漏れるので、漏れたことに気づく手立てをコードに持たせる。

  • 経路照合がラベルの大小を見ていた。 parse_ark は §3.2 手順3 のとおり最初から 大小非依存だったが、HTTP のルートは字面どおりだったので /ARK:/99999/x9abc は リゾルバに届かず 404 になっていた。経路照合の前にラベルの 5 文字だけを 小文字に直すようにした。名前の大小には触らない——そちらは識別子の一部である (手順5)。

  • THUMP の応答ヘッダが無かった。 §5.2 は inflection の応答に THUMP-Status と Link を示し、後者の役目まで書いている——inflection を知らない受信者に、この 応答が「修飾の付いていない ARK」を記述したものだと示すためのもの。識別子に ついてリゾルバ自身が答える応答(? ?? ?info ?json、記述、保留、404) すべてに付け、転送には付けない。

rel の綴りは仕様の応答例(<…> rel="describes";)ではなく <…>; rel="describes" にした。例のほうが RFC 8288 のリンク値として不正で、 そのままでは標準のパーサが読めない——同じことを、通じる形で言う。

  • ERC の where に転送先を入れていた。 §5.1.2 は where を「一時的な転送先では なく長期的な識別子」と定めているのに、arkhe は逆で、ARK は行き先が空のときの代替に していた。記述は「この識別子が何を指すか」を答えるものなので、行き先と一緒に動く 値では引用できない。where を compact ARK にし(付け替えても動かない)、今の 行き先は kernel の外の redirect として、あるときだけ出す。? ?? ?json と ?info の画面が変わる——where から行き先を読んでいたものは redirect を見ること。

  • 新しい ARK を旧形式の ark:/ で生成していた。 §2.2 は「新旧どちらのラベルも 永久に認識しなければならない。ただし新しい ARK は新形式で生成すべき」と 定めている。arkhe は受理は両方できていたが、出すものはすべて旧形式だった ——配った文字列がそのまま次の実装の入力になるので、これでは旧形式が減らない。

生成をすべて新形式(ark:99999/x9abc)にした。API 応答の ark、ERC の where と about、?json、404 の本文、他のリゾルバや N2T へ取り次ぐときの Location、 CLI の ark list、管理画面。表記を決める場所は 1 つにしてある (arkspec.naming.compact_ark)ので、また片方だけ残ることはない。

受理は変えていないし、狭めない。 ark: も ark:/ も裸の 99999/x9abc も 同じ識別子に当たる。これは検査で固定してある。

公開している出力の形が変わる——API 応答や CLI の行から ark:/… を切り出している ものは ark:… を受けられるようにすること。台帳は触らない(保存している鍵に ラベルは最初から入っていない)。

  • 列の幅が 2 つ、仕様が受け取る側に義務づける下限を割っていた。 NAAN は 10 オクテットを超えると拒否し、名前は varchar(100) に入れていたが、 draft-kunze-ark-42 は NAAN 16 オクテット(§2.3)と Base Name + Qualifier 255 オクテット(§3.1)への対応を要求している。

NAAN の 10 には理由があったが、ここには当てはまらない理由だった。arklet は NAAN を int() に通すので len(naan) > 10 で変換を守る必要があり、その定数が解析処理ごと 移ってきていた。arkhe は N2 で逆を決めている——NAAN は文字列として保持・比較し、 整数化しない(ark:/099999/… と ark:/99999/… を別物とするため)——ので、 守るべき変換がそもそも無かった。

naan.naan を varchar(16)、ark.assigned_name を varchar(255)、台帳の鍵 ark.ark を varchar(272)(16 + / + 255)にし、NAAN と ARK を入れる列を すべて揃えた。PostgreSQL ではメタデータだけの変更で、テーブルの書き換えも索引の 作り直しも起きない。255 を超える名前は DB のエラーではなく、上限を示した 400 で断る。

  • 名前のなかの %2F が、黙って成分の区切りにされていた。 ASGI はアプリに渡す前に 経路を復号するので、ark:/99999/x54%2Fc2 は x54/c2 として届いていた—— 別の識別子である。予約文字を %-エンコードしてよいのは、まさにその予約された 意味を隠すため(draft-kunze-ark-42 §3.2)で、%2F は「成分を区切らない /」を 意味する。復号すると、suffix passthrough が無関係なレコードの行き先を継ぎ、 グローバルリゾルバへは書き換わった ARKを渡していた。%7D も同じで、復号形の } は §3.1 の文字集合にそもそも入っていない——エンコードが唯一の合法な運び方で ある。仕様は名指しで禁じている: no %-encoded character should ever appear in an ARK in its decoded form.

解決は ASGI の raw_path からエンコードを保ったまま経路を読むようにし、16 進の 大小を揃える正規化(%2f → %2F。手順5)を解決側と登録側の両方に通した。 /a%2fb で登録した修飾子が /a%2Fb の要求で当たる。前段のプロキシもエンコードを 素通しにすること——nginx と Apache の設定は API リファレンスにある。

  • 一括採番で、同じ request_id が 1 回の要求に 2 行あると 500 で落ちていた。 控えは (client, request_id) で一意なので、控えを 2 度書こうとして IntegrityError に なり、1 件も採番されずに終わっていた。同じ request_id は「同じ 1 つの依頼」の 意味なので、1 件だけ採番して両方の行に同じ ARK を返すようにした——再送の扱いと 同じ約束を、塊の内側にも通す。

  • 応答の符号と媒体が仕様書で不足していた。 再送のときの 200(POST /api/mint と /api/mint/bulk)と、解決の 3xx / 400 / 404 が宣言されておらず、生成した クライアントがそれらを「知らない応答」として扱う形になっていた。解決の 200 も application/json しか宣言しておらず、ANVL(text/plain)と ?info の HTML が 漏れていた。転送は 301 / 302 / 303 / 307 の 4 通り(shoulder の委譲テンプレートが 先頭に符号を書ける)。宣言と実装がずれないよう、検査で突き合わせている。

  • 公開している API 仕様で、採番と解決の口だけ説明が空だった。 FastAPI は docstring を OpenAPI の description にするので、POST /api/mint POST /api/mint/bulk GET /ark:… の 3 ハンドラ(4 オペレーション)に説明が載っていなかった——他の 9 口は 揃っていたので、いちばん使われる 2 本だけが空という形になっていた。 再送の扱い(201 と 200 の区別)、一括採番の全か無か、解決が 302 以外を返す条件を書いた。

  • arkhe naan list と arkhe shoulder list に説明が無かった。 --help の一覧と CLI の参照ページで、この 2 行だけ説明欄が空白だった(他 22 コマンドは日英とも埋まって いる)。

  • ark:hold の語が日英とも欠けていて、管理画面に sc.ark:hold という生のキーが 出ていた。 0.0.9 で scope を足したときの取りこぼし。翻訳の抜けを見る既存の検査は 「言語間で対が揃っているか」を見るので、どちらの言語にも無い場合は素通りする。 語を足し、SCOPES の全項目に説明があることを検査するようにした。

  • 内部のつまみ read_only が、全エンドポイントのクエリパラメータとして外に出ていた。 FastAPI は依存の引数をクエリとして公開するので、get_session(*, read_only=…) を 依存に置いた結果、POST /api/mint?read_only=true のように採番の書き込みを外から 読み取りレプリカへ向けられた(ARKHE_READ_DATABASE_URL を設定した構成でのみ実害)。 引数を外し、接続先はプロセスの役割(ARKHE_RESOLVER)から決めるようにした。 役割はその app が実際に使っている設定から引く——get_settings()(環境変数の キャッシュ)を直に読むと、create_app(settings=…) に渡したものと食い違い、 ルータの出し分けと接続先が別々の設定を見ることになる。

あわせて ARKHE_READ_DATABASE_URL が初めて効くようになった。設定は読まれて いたが read_only=True を渡す箇所がどこにも無く、resolver は書き込みエンジンから 読んでいた——レプリカを立てても向いていなかった。

0.0.9 — 2026-08-31

行き先が信用できなくなったときに、識別子を殺さずに止められるようになった版。 委譲先のリゾルバが落ちた、間違った URL を配ってしまった、取り下げを求められた ——どれも急いで止めたいが、404 は嘘であり 503 は識別子が壊れて見える。止まるのは 転送だけで、解決も記述も続く。あわせて、複数の arkhe で分担する構成 (クローズド PID とオープン PID を含む)を文書に起こし、CI を廃して検査と公開を 手元のスクリプトに一本化した。

追加

  • 転送を期限つきで止められるようになった(arkhe hold add / arkhe hold list / arkhe hold release、PUT /api/hold と /api/hold/release、ark:hold、管理画面の「保留中の転送」)。

止めるのは転送だけで、解決は止めない——404 は嘘(その識別子は存在する)、 503 は識別子が壊れて見える。応答は 200 と記述である。粒度は 3 つ (ark / shoulder / naan)で、狭いほうが優先する。期限は必須 (上限は ARKHE_HOLD_MAX_DAYS)——一時的であることを人の記憶に頼らない。 止めた事実は ?json と /.well-known/ark に出る。

  • 複数の arkhe で分担する構成の手引き(分散して運用する)。 NAAN 単位で分ける・shoulder 単位で分ける・閉域に置いた arkhe を上位が名前空間だけ 把握する、の 3 通りと、繋がない選択を並べた。分けてよいのは名前空間であって、 1 つの名前空間の権威ではない——台帳を分けた瞬間に、コードが守っていた不変条件の いくつかが人の手に移るので、何が移るかを表にしてある。 委譲先も検査桁を作らなければならない理由(解決の判断順で検査桁が shoulder 委譲より 先にある)と、いま無いもの(外部で採番された ARK を取り込む口、shoulder.redirect の CLI、台帳をまたぐ一覧・監査)も明記した。図は Mermaid で 12 枚。

  • クローズド PID とオープン PID の実装イメージ(同じページ)。公開できない対象にも 識別子を先に配り、禁止期間が明けたら行き先を付け替えるだけで公開にする組み方を、 台帳の形とコマンドで書いた。分けるのは shoulder であって識別子の形は分けない ——形が変わる設計だと、閉じている間に配った参照が公開の瞬間に全部死ぬ。外から 見えるものを 3 段(見せない/記述だけ/申請窓口つき)に整理し、いちばん多い 「申請すれば使える」は url を申請フォームに向けるだけで作れることを示した。 あわせて、arkhe はアクセス制御をしない(閉じているのは行き先であって arkhe が 拒んでいるのではない)ことを明記した。

  • STATUS.md を置いた。 版・検査の状態・分かっている穴を 1 か所に集める。手順は AGENTS.md、現在地は STATUS.md と分けてあるのは、両方に書くと必ず片方が古くなるため。

  • 検査と公開を手元のスクリプトに一本化し、GitHub Actions を廃した。 .github/workflows/{ci,docs,release}.yml を消し、同じことを scripts/ の 3 本に 移してある——見ていたものは 1 つも減らしていない。

    • check.sh … sync(--frozen)→ ruff → pytest → 使い捨ての PostgreSQL を立てて マイグレーションを往復(+ alembic check)→ OpenAPI のずれ → mkdocs --strict
    • deploy-docs.sh … 検査 → ビルド → gh-pages へ公開
    • release.sh … 版の一致・変更履歴の節(日英)・check.sh・dist/ の作成。 --publish を付けたときだけタグと GitHub のリリースを作る

系統を 2 つ持たないため。 手元と CI に分かれると「片方では通る」変更が生まれ、 やがて誰も片方を見なくなる。道具が無い項目は黙って通さず SKIP と出す—— 「入っていないから通った」がいちばん危ない。deploy-docs.sh には、CI で checkout が 担っていた役(未コミットの変更があれば止める、HEAD が origin に無ければ警告する)を 入れてある——サイトに在るのにリポジトリで追えない内容を防ぐためで、手元から 出す以上ここが緩む。Dependabot の github-actions は監視する対象が無くなったので外した。

修正

  • コミット済みの OpenAPI が実装から遅れていた(docs/assets/openapi-*.json の version が 0.0.1 のまま)。公開されていた仕様は毎回作り直していたので影響は 無かったが、リポジトリの中身がずれていた。検査のスクリプトが初回の実行でこれを 見つけたので、以後はずれていたら落とすようにしてある。

  • トップページの「どこから読むか」のアイコンが出ていなかった。 :material-api: という文字列がそのまま公開されていた——アイコンを SVG に変える pymdownx.emoji が 設定されていなかったため。未知の短縮記法はただの文字列として通るので警告も出ず、 mkdocs build --strict でも気づけない。

  • README の ER 図へのリンクが移動前の場所(docs/data-model.md)を指していた。 参照ページは docs/reference/ にある。あわせてテスト件数の表記を現状に直した。

0.0.8 — 2026-08-29

認可サーバに寄せた構成でいちばん多い詰まりどころが、目に見えるようになった版。 client_id の綴りが 1 文字ずれると黙って 401 になっていたが、弾いた文字列を台帳が 持つようにしたので、打ち直さずに登録できる。あわせて、体験用の台帳から実在の機関名を 外した。

追加

  • 認可サーバから来た、登録の無い主体を画面に出す。 client_id の綴りが 1 文字 違うだけで 401 になり、しかも黙って落ちる——認可サーバに寄せた構成でいちばん多い 詰まりどころだった。弾いた瞬間に正しい文字列は手元にある(azp は署名検証を 通っている)ので、捨てずに残して一覧に出し、そこから打ち直さずに登録できるように した。認可サーバの資格情報は要らない。止めた主体は混ぜない——意図して止めた ものを「登録し忘れ」に並べると、消すために登録し直すことになる。

変更

  • デモの台帳から実在の機関名と NAAN を外した(seed_demo.py / realm-arkhe.json)。 本物の名前を置くと、その機関が arkhe を使っているように読める。利用者は ops / naan-admin / org-admin、組織は「例示大学」など、NAAN は 99999 (仕様が試験用に予約)と例示の 12345 / 54321 にした。

  • 管理画面の文言を画面ごとのファイルに分けた(api/i18n/)。288 件が 1 ファイルに並んでいて、直したい語を探すのに全体を読むことになっていた。 分ける単位は言語ではなく画面——日本語と英語を別ファイルにすると対が 離れ、片方だけ足したことが差分に出なくなる。分割前後で辞書が完全に一致する ことを確かめてある(画面の見え方は変わらない)。

0.0.7 — 2026-08-29

台帳にはあるのに端末からは見えなかったものと、読んだ人を間違った方に導いて いた記述。 どちらも識別子の振る舞いは変えていない。

追加

  • AGENTS.md。 改修の手順と、実際に踏んだ罠をまとめた。人にも コーディングエージェントにも同じものを渡す——片方だけの決まりを作ると、 もう片方が破る。
  • 参照ページが実装から遅れていないことを検査する(tests/test_docs.py)。 Contributing に「設定と CLI のページは生成している」と書いてあったのは誤りで、 そのせいで設定 2 つとコマンド 1 つが未記載のまま残っていた(ARKHE_TRUSTED_PROXIES、 ARKHE_LOG_LEVEL、arkhe manager policy)。記述を直し、埋め、二度開かないようにした。

  • arkhe ark list。 画面に ARK 一覧があるのに CLI に無かった。絞り込みは 画面と同じ式を通す(domain/queries.py)——到達範囲を 2 か所に書くと、 片方だけ直したときにずれる。既定で 50 件で打ち切り、打ち切ったことと続きの --offset を標準エラーに出す(出さなければ「これで全部」と読まれる)。

0.0.6 — 2026-08-29

中身は変えず、同じコミットから同じものができるようにした版。 src/ は 1 行も 触っていないので挙動は変わらない。取る理由は、これ以降あるタグから焼き直した イメージが、そのタグで試したものと同じになること。

変更

  • 依存を uv.lock で固定した。 宣言は下限しか書いていないので、これが無いと 同じコミットから別のものができる——同じ Dockerfile で焼き直すたびに イメージの中身が変わり、「いつから壊れたか」が追えない。CI もイメージのビルドも uv sync --frozen にした(lock と pyproject.toml がずれていたらそこで落ちる)。 上限は書かない——lock があれば要らないし、上限はライブラリとして使われる ときに他と共存しにくくする。更新は Dependabot が毎週まとめて出す ——固定は「新しいものを見なくてよい」という意味ではない。動かすのは lock だけで、 宣言の下限は触らせない(入れた版まで引き上げるのは「それより古いと動かない」と 確かめずに言うことになる)。

0.0.5 — 2026-08-29

公開面の脆弱性を 1 件塞ぎ、NR の宣言を検証できるようにした版。 どちらも 既に配ってしまった識別子に関わるので、区切って出す。

通しでコードを読み直したときに出たものが大半で、画面を実際に触って初めて 見えたものがその次に多い。

追加

  • 名前空間の決まりを NAAN 側に持たせた(入り方・自己登録・scope の上限)。 組織ごとにしか掛けられなかったが、組織が増えると 1 つずつ掛けるのは現実的で ない——800 機関に同じ制限を入れて回る運用は成立しない。原則を NAAN に置き、 組織ごとの設定はそこから狭めるだけ(広げられない)にした。重ねた結果は 1 か所で決め、発行・登録・認証の 3 か所ともそれを使う。
  • ARK の行き先が変わった記録(ark_change)。これが無いと以前どこを 指していたかを復元できず、NR を宣言しながら「変わらない」の検証手段が 利用者の側に無かった。監査ログとは別に持つ——監査は NAAN 単位以上の操作しか 残さないが、採番も付け替えも組織が行うので、監査だけでは肝心の変更が落ちる。
  • 発行した ARK の一覧。到達範囲でそのまま絞り、組織で絞り込め、詳細では ? と ?? で公開される記述を全項目見られる。件数は増える一方なので、 検索とページ送りを最初から入れてある。
  • 画面から利用者を登録し、鍵を発行・失効できる。 一覧の「開く」「利用者を 登録」も、存在しないルートを指していて 404 だった。人には鍵の発行の口を 出さない——人に鍵を配ると、その人が組織を離れても鍵が生き残るため。
  • arkhe client disable / enable と画面の同じ操作。認可サーバに寄せた構成では これが arkhe 側の唯一の止め方(鍵を持たないので revoke に効く相手が無い)。
  • 構造化ログと要求 ID(X-Request-Id)、/readyz。障害時に追う材料が DB を 直接見る以外に無く、/healthz と兼ねていたのでDB が落ちても Ready のまま だった。認証の失敗理由もサーバ側にだけ残す。
  • ログインとログアウトを監査に残す。 到達範囲で間引かない——失敗した ログインは成功したものより先に見たい記録である。
  • 採番の種別を候補から選べるように(DataCite の resourceTypeGeneral)。 縛りではない——ERC の what は語彙を定めないので、一覧に無いものは直接 入力できる。
  • ロゴ。ἀρχή の α をパスで描いた。favicon にも使う。

修正

  • 公開リゾルバの保存型 XSS。 転送先にスキームの制限が無く、javascript: を 採番できた——?info は認証を要さない公開ページなので、ark:mint を持つ主体が リゾルバのオリジンで動くスクリプトを他人に踏ませられた。登録は絞らない (ARK は物理オブジェクトにも他の識別子にも付けられるので urn: doi: ark: は 正当)。ブラウザに解釈させると危ないものだけ拒み、転送してよいかは別に判断 する。画面には CSP を付けた。
  • 一覧が使えなくなる規模に備えた。 組織管理の画面は毎回 ark 全体を集計して いた(30 万件で Seq Scan、実測 27.6ms → 読む行が 30 万から 7,500 件に)。 利用者一覧と監査ログにも検索とページ送りを入れた——監査は直近 200 件で頭打ち で、古いものを見る手段が無かった。
  • 押しても断られるだけのボタンや導線を出さない。 組織単位の管理者に監査ログの リンクが出て 403 になっていた。出ている導線を片端から開いて 1 つも断られない ことをテストで確かめている。
  • 認可サーバに寄せた構成で、正しく設定できている利用者が「未設定」に見えていた (鍵を持たないので「資格情報 0 有効」と出ていた)。列を入り方に変えた。 機構が無効な鍵は数えない——持っていても通らないので、あると言うと嘘になる。
  • その構成で使えない鍵を発行できてしまっていた。発行できる種別を、有効な機構 から決めるようにした。
  • 組織の制限が掛けられないように見えていた。約束の水準(組織自身の宣言)と 同じカードに並んでいて、誰が決めることなのかが読み取れなかった。
  • ログインの往復が失効したときに素のテキストを返していた(戻り道が無かった)。
  • 手のひらの幅で表がはみ出し、右側の列が読めなかった。ログアウトも サイドバーごと畳まれて押せなかった。
  • ログアウトが GET だった(SameSite=Lax はトップレベルの GET 遷移では Cookie を送る)。
  • 日本語の見出しのアンカーが _1 _2 … だった(深いリンクが使えなかった)。
  • 宣言だけで作られていなかった外部キー。use_alter は create_table の中では 後追いの ALTER にならない——alembic check が指していたのは誤検出ではなかった。

変更

  • api/admin.py 1,100 行を画面ごとに分割した(最大 316 行)。行を動かした だけで中身は変えていない。
  • 「できること」(scope)と組織の制限をチェックボックスにし、語彙を 1 か所から 出すようにした。自由入力だと、検査されない綴りを登録できてしまう。

0.0.4 — 2026-08-29

管理画面を実際に触って出てきたものだけ。押せるのに何も起きないボタンと、 止めたいのに止められない主体——どちらも、画面が言っていることと実装が 食い違っていた例である。

追加

  • 画面から利用者を登録し、鍵を発行・失効できるようになった。 一覧の「開く」と 「利用者を登録」も、存在しないルートを指していて 404 だった。平文は発行の応答に しか載せない(リダイレクトすると取り出せなくなる)。人には鍵の発行の口を 出さない——人に鍵を配ると、その人が組織を離れても鍵が生き残るため。組織単位の 管理者も自組織の利用者を扱える。
  • arkhe client disable と arkhe client enable、および管理画面の同じ操作。

変更

  • 認可サーバに寄せた構成の利用者画面に、その認可サーバの場所(issuer)を出す。 「秘密は認可サーバで作る」とだけ書いても、どの認可サーバのことか分からなかった。
  • ログイン画面と案内画面の言語切り替えを、管理画面と同じアイコンの形に統一した。 横並びのセグメントは、言語が増えると横に伸びて破綻する。
  • はじめて立ち上げるときの手引きに、認可サーバ側での鍵の作り方(Keycloak の コンソールと Admin API)と、入れ替え・停止をどちらでやるかの表を足した。

修正

  • 認可サーバに寄せた構成で、arkhe 側から利用者を止める手段が無かった。 oidc では資格情報を arkhe が持たないので revoke は効かず、Client.active を 落とす操作もどこにも無かった——認可サーバが出し続けるトークンで通り続ける。 「認証できることと、この名前空間を触ってよいことは別」と言いながら、その判断を 取り消せなかったことになる。去った組織の主体は戻せない(個別の復帰で離脱の 宣言を骨抜きにしない)。
  • 認可サーバに寄せた構成での「利用者を登録」が、何をする操作なのか画面に書いて いなかった。 鍵を出さないので何もしていないように見えるが、この登録が 認可サーバの主体と arkhe の到達範囲を結びつけている——登録が無ければ、正しい トークンを持っていても通らない。識別子として何を入れるか(機械は azp → client_id → sub、人は preferred_username → email → sub)も明記した。
  • その構成で使えない鍵を発行できてしまっていた。 authenticate は ARKHE_AUTH に挙がった機構しか試さないので、oauth2 を有効にしていない構成で client_secret を出しても、どこからも通らない(compose のデモがまさにこれ)。 発行できる種別を有効な機構から決めるようにした。
  • 押しても断られるだけのボタンや導線を出さないようにした。 組織単位の管理者に 監査ログのリンクが出ており、押すと 403 になっていた。採番の導線も ark:mint を 持たない主体に出ていた。判定はルートと同じ式を使う(別々に書くと、今度は逆に 「ボタンは出ないが URL は通る」穴になる)。出ている導線を片端から開いて、 1 つも断られないことをテストで確かめている。
  • ログインの往復が失効したときなどに、素のテキストを返していた。戻り道が無く、 利用者は URL を手で直すしかなかった。 ログイン画面と同じ外枠を共有する案内画面に し、「ログインし直す」を置いた。
  • 手のひらの幅で表がはみ出し、右側の列が読めなかった。カードに overflow: hidden があるためページは伸びず、代わりに切り取られていた。 640px 以下では 1 行を 1 枚に畳み、各値の前に見出しを出す。
  • 狭い画面ではログアウトが押せなかった。 サイドバーの底に名前の無いアイコンと して置いてあり、860px 以下ではそのサイドバーごと畳まれていた。ヘッダーに移した。
  • 日本語の見出しのアンカーが _1 _2 … になっていた。既定の slugify が非 ASCII を 落とすためで、日本語ページへの深いリンクが使えず、見出しを 1 つ足すと番号が ずれて既存のリンクが黙って別の場所を指していた。
  • 変更履歴を書いても公開が追随しなかった。docs ワークフローのパス条件に CHANGELOG*.md が入っていなかった(本文は --8<-- で取り込んでいる)。
  • バージョン方針のページに版番号を直書きしていて、0.0.1 のまま古くなっていた。 リリースのたびに古くなるものは文書に書かない。

0.0.3 — 2026-08-29

管理画面が、採番だけの画面から台帳を組める画面になった。0.0.2 と同じく、 出てきた欠陥はどれも実際に画面を触って見つけたもので、テストでは 1 つも 出ていない。

追加

  • 管理画面から台帳を組めるようになった。 一覧にあった「NAAN を登録」 「組織をオンボード」「shoulder を切り出す」「操作」の 4 つのボタンは、 いずれも存在しないルートを指していて 404 だった。実装し、NAAN と組織と shoulder の設定画面も足した。
  • 約束の水準を設定できるようになった(arkhe manager commitment、 arkhe onboard --commitment、管理画面)。? と ?? で公開されるのに設定する 手段が無く、全組織が既定の permanent-dynamic を名乗ったままだった。組織が 述べていない約束を組織の名前で名乗ることになる——宣言していないものを宣言と して出すのは、何も出さないより悪い。 --commitment を付けずに迎えた場合は その旨を stderr に出す。未知の水準は拒む。 その組織の管理者自身も変えられる——約束は組織自身のもので、述べる主体が 述べられなければ意味がない。
  • arkhe manager list。組織の id は他のコマンドの入力になるが、一覧する手段が 無かった。
  • set_quota。採番上限をオンボード時にしか決められなかった。自組織では 変えられない——課された側が外せる上限は上限ではない。
  • コマンドが日本語と英語を話す。 言語は起動時に環境から決まる(ARKHE_LANG、 次に LC_ALL / LC_MESSAGES / LANG を POSIX の順で見て、既定は管理画面と 同じ ja)。Typer が help を組み立てるのは import の時点なので、実行時の --lang は作れない。
  • 一から立ち上げるときの手引き(日英)。arkhe の外で起きる手順——NAAN の 申請と、NAAN registry への解決先 URL の登録——も含めて並べた。後者が抜けると n2t.net/ark:/99999/… は自分のところに届かない。
  • compose/oidc/lan.yml。LAN の別の端末からデモを見るための上書き。公開先を 0.0.0.0 にするだけでは足りず、issuer と redirect_uri はブラウザが実際に打つ URL でなければならないので、ARKHE_DEMO_HOST で受け取り、redirect は realm JSON に焼き込まず起動時に登録する。既定の待ち受けは 127.0.0.1 のまま——この一式は 秘密値を平文で持っているため。

変更

  • 画面の言葉を、台帳を初めて見る人に向けて書き直した。 一覧の名前は 「委譲の構造」から 組織管理 へ。ボタンは何が起きるかを述べる形にした (「組織をオンボード」→「組織を追加」、「shoulder を切り出す」→ 「名前空間を追加」)。組織を追加する画面には、同時に名前空間を渡すことを 最初に書いた。「主体と資格情報」は「利用者と鍵」に——ここに並ぶのは組織そのもの ではなく、組織のシステムと人だから。採番の欄は「shoulder」ではなく 「採番する名前空間」に(選ぶのは NAAN と shoulder の組であって shoulder だけ ではない)。約束の水準が生の値のまま出ることも無くなった。
  • 用語は捨てず、括弧で残す。「名前空間(shoulder)」「永続・内容は変わりうる (permanent-dynamic)」のように平易な言い方を先に置いて用語を添える。初めての 人はそのまま読め、用語を知っている人は仕様・CLI・API と対応が取れる。
  • 名前空間を渡す先を指す語を「機関」から 組織 に統一し、NAAN を 「組織番号(NAAN)」と呼ぶことにした(Name Assigning Authority Number で、 番号を持つのは組織のほう)。画面・コマンド・エラー文言・文書まで通して揃えた。
  • 画面の「利用者」と、API・CLI の「主体(principal)」が同じものであることを 文書に明記した。読み手が違うので呼び方は分けたままにしてある。対応表を 管理画面とコマンドの両方のページに置いた。

修正

  • ログアウトできなかった。 こちらのセッション Cookie は消していたが、認可 サーバのセッションはそのままだったので、次に画面を開くと何も訊かれずに戻って きていた。OIDC の構成では認可サーバ側も終わらせる(RP-Initiated Logout)。 id_token_hint は渡さない——渡すには ID トークンを Cookie に抱えることになり、 claim の多い環境で 4 KB を超えてブラウザに黙って捨てられ、今度はログイン できなくなる。end_session_endpoint を持たない認可サーバではこちら側だけで 終える。
  • NAA ポリシーを組織管理者が書き換えられた。 これは名前空間を配る側の宣言で NAAN 配下の全組織にかかるので、1 組織が他組織の分まで言い直せてはならない。 NAAN 単位以上を要求するようにした。組織が自分について述べるのは約束の水準の ほうで、そちらは組織管理者が変えられる——この分かれ方は権限表ではなく、 ARK の委譲の構造がそのまま出たもの。
  • NAAN の設定画面を組織管理者が開けてしまい、保存で 403 になっていた。編集できる ように見えて保存できないフォームは、ボタンが出ないのに URL は通るのと同じ欠陥 なので、開ける条件を保存できる条件に揃えた。

0.0.2 — 2026-08-28

すべて、0.0.1 を Kubernetes と compose に実際に載せて出たもの。テストでは 1 つも 出ていない——4 件とも「コードが正しい」と「デプロイできる」の間に落ちていた。

修正

  • /healthz をどのモードでも載せた。resolve ルータにしか無かったので、minter と admin は liveness probe に 404 を返し続け、繰り返し殺されていた。
  • resolver に認証設定を要求しないようにした。認証を通す口も管理画面も無いのに、 起動時に ARKHE_SESSION_SECRET と OIDC の設定を求めていた——使いもしない セッション署名鍵を、解決系の全ノードに配らせていたことになる。
  • shoulder を指定した主体が組織を継ぐようにした。--shoulder だけ渡すと manager が空のまま作られ、認可の入口で必ず弾かれていた。しかも「shoulder は 合っているのに通らない」という追いにくい形で。shoulder と食い違う manager を 明示した場合は、黙ってどちらかを優先せず拒む。
  • ラベルの一意性は、ラベルがあるときだけにした。(manager_id, label) の一意制約に 空文字が含まれていたため、1 組織にラベル無しの主体を 1 つしか置けなかった。 プロセスごとに鍵を分ける普通の構成(web-api / web-ui / worker)が通らず、 鍵を共有させる圧力になっていた。移行は 56e5e54db345。
  • compose のブラウザログインが invalid_scope: openid profile email で落ちていた。 realm の import で clientScopes を宣言すると Keycloak 組み込みの一式は追加では なく置き換えになるため、profile と email が realm に存在していなかった。

変更

  • デモ realm の defaultDefaultClientScopes から arkhe-api を外した。realm の 既定に入れていたので、後から作ったどんなクライアントでも arkhe 宛だと名乗れる トークンを取れた。audience は「このトークンがどの API 向けか」の宣言であって、 既定で配るものではない。
  • compose の resolver を独立したサービス(:8058)にした。デプロイ時と同じ形に なるうえ、Keycloak を止めると採番が 401 になり解決は 302 のままであることが、 そのまま見える。

0.0.1 — 2026-08-28

最初のタグ。プレリリースであり、版が 0 で始まる間は MINOR が破壊的変更を運ぶ。

追加

  • ARK の採番と解決。? / ?? / ?info / ?json の inflection、suffix passthrough、 検査桁、未知 NAAN のグローバルリゾルバへの取次。
  • 3 段の委譲(system / naan / manager)。ARK が名前空間を受け渡す構造の写しで、 配られた側が配った側より広く届くことはない。
  • API の認証 3 機構(apikey / oauth2 / oidc)。排他ではなく個別に有効化でき、 どれで認証しても Principal 1 つに集約される。
  • 管理画面への入口 4 種類(bearer / password / oidc / proxy)。
  • 操作ベースの管理画面(日英)。テーブルの行編集は持たない——行編集はドメインの 禁則を素通りするため。
  • 承継と離脱。どちらも既存の識別子は解決し続ける。
  • 冪等な採番。同じ request_id の再送には、前回と同じ ARK を返す。
  • ドキュメントサイト(日英、一部は実装から生成)。
  • 運用コマンド(arkhe)。画面と同じ domain を通る——arkhe check (設定の検証)、arkhe succeed / arkhe depart(承継と離脱)、 arkhe shoulder status(名前空間の状態)、arkhe client add / arkhe client key / arkhe client passwd / arkhe client revoke (主体と資格情報)、arkhe client breakglass(期限つきの緊急用の主体)。

備考

  • Django から FastAPI + SQLAlchemy 2.0 へ書き直した。仕様の層(arkspec/、 domain/resolution.py)は無改造で運べ、97 本のテストがそのまま通った。
  • arkspec/ の一部は Internet Archive の arklet(MIT)から派生。NOTICE を参照。