Skip to content

Latest commit

 

History

History
500 lines (389 loc) · 35.8 KB

File metadata and controls

500 lines (389 loc) · 35.8 KB

STATUS

2026-09-19 時点の arkhe の現在地。 作業を止めて再開するときに、まずここを読む。

設計の意図は不変条件、手順と踏んだ罠は AGENTS.md、変更の履歴は CHANGELOG.ja.md にある。 ここに書くのは「今どこまで来ていて、何が無いか」だけ——重複させると必ず片方が古くなる。

一言でいうと

単一台帳で動く ARK 基盤としては一通り揃っている。 採番・解決・委譲・承継離脱・ 3 つの認証機構・管理画面・監査まで実装され、緑で、文書がある。まだ 0.x なのは、 運用実績が無いことと、複数拠点で分担する構成(分散して運用する)に まだ穴があるため。

0.14.0(2026-09-19 リリース)。main は clean、タグと pyproject.toml は一致
テスト すべて greenuv run pytest -q)。通しの検査は別枠uv run pytest -m e2e——docker で PostgreSQL を立て、minter と resolver を uvicorn で建てて HTTP で叩く。check.sh の手順 5)
静的検査 ruff check src tests clients 通過(E/F/I/UP/B、line-length 100)
文書 mkdocs build --strict 警告 0。日英 2 言語
マイグレーション head は単一(b9d4a17c3e85)。scripts/check.sh が PostgreSQL 17 で up→down→up→check を回し、tests/test_migrations.py が SQLite で頭まで流す
Python 3.12 以上。本体の依存は optionalarkspecdomain.resolution は何も入れずに import できる)

何が動くか

すべて単一台帳(1 つの arkhe + 1 つの DB)を前提にした状態。

領域 状態 場所
ARK 仕様の純関数層 NOID 生成、検査桁、shoulder 分割、正規化・インフレクション arkspec/
解決 完全一致 → 祖先 passthrough → 検査桁 → shoulder 委譲 → 404/取次。? ?? ?info ?json domain/resolution.py
採番 衝突は握りつぶさず数えて採り直す。冪等鍵(request_id)、一括採番、quota domain/minting.py
公開と取り下げ 公開前として採れ、公開は取り下げて出し直せる。 未公開なら一括で捨てられる/api/delete/bulk——採るのと同じ安さでなければ放置された番号が残る。一度でも公開した名前が 1 つでも混じれば全体を断る)。 削除は戻せない。公開前を解決するかはリゾルバの置き場所で決まる(閉域は解決する)。取り下げた名前は二度と採らない domain/admin_ops.py
取り込み 外で採番された ARK を台帳に載せる/api/import)。名前は first-digit 規約でshoulder を 1 つだけ選び、総当たりで「入る shoulder」を探さない。scope は ark:import api/mint.py
委譲 shoulder の 4 状態、delegated307 で行き先を返す(プロキシしない domain/admin_ops.py
転送の保留 ARK / shoulder / NAAN を期限つきで止める。解決は止めない(200 と記述) domain/resolution.py
承継・離脱 arkhe succeed / arkhe depart --resolverArk の行には触れない domain/admin_ops.py
認証 apikey / oauth2(client_credentials のみ)/ oidc併用できる auth/
管理画面への入口 bearer / password / oidc / proxy の 4 つ auth/login.py
認可 3 段の到達範囲。判断は 1 か所、リクエストで広がらない domain/authz.py
管理画面 台帳・主体・ARK 一覧・監査・未登録主体。日英切替、画面ごとの i18n api/admin/, api/i18n/
記録 AuditEvent(NAAN 以上の操作)と ArkChange(行き先の変更は全件) db/models.py
復元の確認 arkhe fingerprint——件数ではなく中身で確かめる。arkswithdrawn分けて出す(潰すと「どこが違うか」が消える) domain/stats.py
統計 数えるのは 1 か所domain/stats.py)。arkhe statGET /api/stats / 管理画面が同じ数を見る。合計も到達範囲で絞る(合計は在ることを漏らす)。同じ絞り込みへの集計は 1 回の走査に畳む——ark を読むのは 2 回(30 万件で約 110 ms) domain/stats.py
運用コマンド 画面と同じ domain を通る cli.py
観測性 /healthz /readyz、構造化ログ、/.well-known/ark衝突の記録mint_collision)。メトリクスの口は持たない——ログの系統を 2 つにしない observability.py, domain/minting.py
体験環境 Keycloak + PostgreSQL + minter/resolver の compose compose/oidc/
Python クライアント 全エンドポイント+採番の冪等鍵・307 を追わない・ARKHE-xxxx 付きの例外公開 OpenAPI と両方向で突き合わせ、本物の一式に対しても 14 本流す clients/python/

テストの内訳:

test_admin_forms.py  画面のフォーム        test_authz.py       到達範囲(負の場合を厚く)
test_admin.py        管理画面             test_resolution.py  解決とインフレクション
test_arkspec.py      仕様の純関数層        test_api.py         API
test_auth.py         3 機構の認証          test_models.py      不変条件(削除拒否ほか)
test_succession.py   承継と離脱            test_cli.py         運用コマンド
test_cli_i18n.py     訳の抜け             test_docs.py        参照ページの追随
test_hold.py         転送の保留            test_publication.py 公開と、公開前の取り下げ
test_migrations.py   移行が頭まで流れること(`test_publication.py` は purge も見る)
test_stats.py        台帳の統計(**届かないものが混ざらないこと**)

clients/python/tests/ はクライアント側(スタブに対する判断 39 本+OpenAPI との突き合わせ)。 pytesttestpaths に入れてあるので、同じ 1 本の uv run pytest -q で一緒に回る

分かっている穴

「無い」と分かっているものを、探す前に書いておく。

分散構成(分散して運用する で扱った)

  • 台帳をまたぐ一覧・監査・quota が無い。 /.well-known/ark が出すのは名前空間の 割当まで。
  • 委譲先の健全性を arkhe は見ない(見ないと決めた)。見るべき一覧は出している ——/.well-known/ark に委譲先と解決の委譲先が並ぶので、外形監視はそれを入力にする。 arkhe 自身には叩かせない——この台帳は外向きの通信を一切しない性質を持っており、 障害の切り分けを簡単にしているそれを、監視のために手放すのは高すぎる。 気づく手立てが外に、止める手立て(保留)が中にある、という分担。
  • redirect の循環を検知しない。 A→B→A は無限ループになる。運用で禁じるしかない。

実装の細部

  • ARKHE_RAW_URI_HEADER だけ Settings の外にある(api/resolve.pyos.environ を直読み)。したがって .env から効かず、arkhe check の検査対象にも、 tests/test_docs.py の網にも入らない。参照ページに載っているのは手で書いたから
  • Naan.minter を設定できるのは管理画面だけnaan add に対応する選択肢が無い)。
  • README.ja.md の例に実在の機関名が 1 か所残っている(arkhe naan add 99999 "国立情報学研究所")。 0.0.8 でデモ台帳からは外したが、README は対象外にしてある——arkhe の開発元自身の名前 なので誤読は生まないという判断。揃えるなら直す。

まだ確かめていないこと

  • 本番運用の実績が無い。 性能は測った(100 万件の台帳に負荷をかけた実測が デプロイにあり、道具は scripts/bench.py)が、 測ったのと運用したのは別である。
  • 本番の機械で復元を試していない。 手元では通した(下記)が、実際の運用系で、 実際のバックアップから戻したことは無い。取得が止まっていた・置き場が同じ機械 だった、の類はそこでしか出ない。

次の一手の候補

決まっていない。 選ぶときの材料として並べる。

  1. 本番の機械でバックアップと復旧を組む。手元では復元も PITR も通し、確認は arkhe fingerprint で機械化した(下記)。残るのは運用の側——取得が止まって いることに気づく仕掛け(pg_stat_archiver の古さ)、置き場を別の機械にすること、 毎月の復元検証を仕事にすること、本番規模での所要時間と担当。 手立てはデプロイに書いた。組んでいないだけである。

やってみて分かったこと

Python クライアントを書いた(2026-09-19・生成ではなく手書きにした)

生成が与えるのは網羅で、網羅は検査できる——clients/python/tests/test_contract.py が クライアントと公開 OpenAPI 文書を両方向で突き合わせる(エンドポイントに対応する メソッドがあるか、メソッドの指すエンドポイントがまだ在るか、Ark の項目が ArkOut と 一致するか)。壊して落ちることを確かめた——Ark から項目を 1 つ落とすと落ち、 OpenAPI に架空の POST /api/rename を足すと落ちる。

生成が与えられないのは 3 つ。採番の冪等鍵(request_id を必ず載せ、再送で同じものを 送る)、307 を追わないこと(追えば自組織の資格情報を他組織の口に送る)、断りに ARKHE-xxxx を載せること。これがクライアントを書いた理由で、残りは API に Python の 名前を付けただけ。

書いている途中で 1 つ分かった: コードを持たない断りがある。domain が Invalid({"until": "...", "reason": "..."}) の形で上げるもの(画面が項目の横に出す形)で、 最初のクライアントはこれを素の HTTP 400 にしていた。本物のサーバに対して流して気づいた もので、スタブだけなら気づかないまま通っていた

コードの言語を英語に揃えた(2026-09-19)

識別子・コメント・docstring・テスト名(493 本の改名を含む)を英語にした。対象は src/ tests/ scripts/ alembic/ compose/、シェルと設定とテンプレートも含む。

日本語が残るのは、日本語が製品そのものである 3 か所だけ:

残した場所 なぜ
多言語カタログ(api/i18n/cli_i18n.py 日本語の画面そのもの。日英の対が揃っていることを起動時に検査している
errors.pyja 参照ページに出す日本語の説明。英語の message と対で持つ
mkdocs.yml の日本語ナビゲーション 文書サイトの日本語版の見出し

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

機械で見るようにしたtest_the_code_is_written_in_english)。日本語を 1 行 戻すと落ちることを確かめてある。この種の決まりは 1 ファイルずつ崩れる ——日本語のコメントの隣に日本語で書き足すのは、自然に見える

移行ファイルの名前も英語にしたが、revision id は変えていないので、鎖も 適用済みの履歴もそのままである。

通しの検査を足した(2026-09-19・3 つの穴に効くことを確かめた)

この版までに見つけた失敗は、どれも試験ではなく「動かしたこと」で見つかっている。 試験は全部 green のまま、復元手順は起動せず、/readyz は読まない DB を見ていた。 同じ形の見落としが、次も試験をすり抜ける。

そこで tests/e2e/ を足した。本番と同じ形に建てて、素の HTTP で叩く——docker で PostgreSQL、uvicorn で minter と resolver を別々に、台帳は CLI で組み、その CLI が 刷った鍵で採番して解決する。

効くことを確かめた(戻して、落ちるところまで見た):

見ているもの 戻すとどうなるか
app がファクトリであること arkhe.app:app では起動せず、fixture が uvicorn の出力ごと落とす
/readyz がその役割の読む DB を見ること read_only=s.resolver を戻すと実際に落ちた
ARKHE_ALLOWED_HOSTS が効いていること 中間層を外すと実際に落ちた——0.9.2 まで死んでいた設定
resolver が書き込み DB に触れないこと resolver の書き込み側はどこにも繋がらない先に向けてある

組み立てでも 1 つ見つかった。 組織に結び付けずに作った主体は採番できない (ARKHE-1303)——client add --manager が要る。CLI だけを追っていては気づかない

既定の pytest からは外してある(-m 'not e2e')。混ぜると誰も普段は流さなくなる ——check.sh が手順 5 として呼び、docker が無ければ SKIP と出す

通しの検査を 54 本に広げた(2026-09-19)

叩く面を、解決の規則・一生・認証と到達範囲・管理画面・締める設定・同時まで広げた (tests/e2e/ の 7 本。内訳は変更履歴にある)。建てる形は変えていない。

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

復元リハーサル(2026-09-18・通った)

1.0 前の筆頭に挙げていた項目。 PostgreSQL 17 で、体験用の台帳を作って pg_dump -FcDROP DATABASEpg_restore を通した。手順と確かめ方は デプロイに書いた。

見たもの 結果
alembic_versionalembic check 一致・差分なし
ARK と行き先と公開状態の md5 指紋 完全一致
監査・資格情報の件数 一致
resolver 役で実際に解決 302 で同じ行き先
復元後に採番できるか(連番) できたlast_value も戻っていた)

素の pg_dump -Fc / pg_restore は、この台帳を無傷で往復させる。 特別な 手当ては要らなかった。分かったのは手順のほうである:

  • --no-owner --no-privileges が要る。 戻す先の役割名は元と違うのが普通
  • 確認は resolver 役で行う。 既定の起動は minter で解決の口を持たない ——リハーサル中、実際にここで 404 を見て「復元に失敗した」と思った
  • 件数ではなく指紋で見る。 件数が合っていても行き先が入れ替わっていれば 識別子は全部壊れている。指紋のずれと連番のずれは、黙って通る

PITR リハーサル(2026-09-18・通った)

wal_level=replicaarchive_command で WAL を保存し、pg_basebackup の後に 採番を挟んでから rm -rf でデータを消し、基礎バックアップ + WAL 再生で戻した。

結果
基礎バックアップ時点 53 件
その後に採番(A、5 本) 5/5 戻った——日次ダンプなら失っていたぶん
復旧目標より後に採番(B、5 本) 0/5(指定どおり切り捨て)
alembic check 差分なし
復旧後の採番 できた(昇格済み)

PITR は効く。 夜間ダンプでは消える「その日ぶんの識別子」が、実際に戻った。

そのうえで、この体系に固有の危険が見つかった——時点を戻すと、台帳は 「何を失ったか」を知らない。B の 5 本について、arkmint_receiptark_change も監査もすべて一緒に巻き戻りwithdrawn_name にも入らない。 つまり配った名前が永久に 404 になり、しかも「一度も使われていない」扱いに 戻る——NR を守る仕掛けのどちらにも載らないからである。tombstone も出せない (出す対象が台帳に無い)。

だから**既定は「WAL の終わりまで」**にし、時点を選ぶのは取り消したい操作がある ときだけにする。戻したなら、その間に配った名前は台帳の外から突き合わせ、 採り直すのではなく取り込むPOST /api/import)。手順は デプロイに書いた。

解決の検査は、組み直して通した(2026-09-18)。1 回目は前のドリルの ARK を 使ったので 404 になり、検査の側が誤っていた。組み直して、基礎バックアップ より後に採番した ARK——つまり WAL 再生でしか戻らないもの——を引いた:

解決:              302 https://repo.example.ac.jp/records/PITR-PROOF
記述(?info):      200
検査桁(1 文字違い): 404

WAL からしか復元されない識別子が、行き先まで無傷で解決する。 検査桁も効いて いるので、復元されたのは行だけでなく名前の規則ごとである。

resolver は本当に読み取り専用か(2026-09-18・確かめた)

「resolver をレプリカに向けられる」は構成の要なのに、裏が取られていなかった。 SELECT 権限だけのロールに向けて全経路を叩いた——? ?? ?info ?json、 検査桁、未知 NAAN の取次、/.well-known/ark/healthz /readyzすべて通る。 保留の期限切れを解決のたびに時計で判定している(バッチで戻さない)ので、 戻すための書き込みが無いのが効いている。

認証の設定を一切持たずに起動することも確かめたARKHE_AUTHARKHE_OIDC_ISSUER も不要)。管理画面も採番の口も載っておらず、どちらも 404認可サーバと同じ障害で倒れない。

落ちたとき何が続くかの表をデプロイに置いた。

冗長構成の検討(2026-09-18・1 件直した)

resolver と minter で答えが逆になる。 resolver は状態を持たず書かないので 好きなだけ並べてよい。minter と主系 DB は自動フェイルオーバを入れない ——split-brain は同じ名前を別の対象に割り当てうる、この体系が唯一守ると 言っていることの違反である。採番は止めてよいのだから、天秤は傾いている。

複数 minter は安全(8 並列 × 50 本 = 400 要求で重複 0・失敗 0)。

直したもの——同時の再送が 500 になっていた。 同じ request_id が同時に 届くと、再送の判定と控えの書き込みの隙で両方が書きにいき、負けたほうが 500(200 要求中 102)。台帳は壊れていない(控えの一意制約が効いている)が、 呼び出し側から見ると「採番できたか分からない」——別の request_id で投げ直せば 二重採番になる。負けたほうに勝ったほうの ARK を返すようにした。一括も同じ隙が あり、そちらは組み直して全件を再送ぶんとして返す。

負荷分散の死活検査は /healthz(DB を見ない)。/readyz を使うとDB の障害が 「全台不健全」になり、残っていたはずの読み取りまで止まる

公開前の放置の回収(2026-09-18・自動では消さないと決めた)

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

代わりに、気づく材料を出す:

arkhe stat いちばん古い公開前 を日付と日数で出す
ark list --state reserved --older-than N 拾って、手で始末する

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

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

この検討で 1 件直した。 arkhe statSQLite で落ちていた——SQLite には 時刻帯の型が無いので DateTime(timezone=True) でも naive な値が返り、 PostgreSQL では aware。engine によって型が変わり、片方でだけ引き算が TypeError になっていた。ドメインの側で UTC を付けて返すようにした (CLI だけ直すと、API と画面に同じ穴が残る)。

委譲先の外形監視(2026-09-18・arkhe は見ないと決めた)

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

代わりに、見るべき一覧を出す。 /.well-known/ark解決の委譲先 (redirect)を足した。以前は minterabout だけが出ており、壊れたときの 重さがいちばん大きいものが載っていなかった——minter が死んでも止まるのは採番 だけだが、redirect が死ねばその下の ARK が 1 本残らず引けなくなる

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

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

監視設計(2026-09-18・1 件直した)

何を見るかは、何を約束したかで決まる。 この台帳の約束は 1 つ(配った名前が 別のものを指さない/引けなくならない)なので、そこから逆に引いた表を デプロイに置いた。

直したもの——衝突回数を捨てていた。 mint() は衝突を数えて返し、docstring は 「名前空間の枯渇が静かに進むのを検知できるようにするため」と言っているのに、 呼び出し側 3 か所すべてが ark, _ = で捨てていた。数えているのに誰も受け取って いない。衝突したときだけ mint_collision を記録に出すようにした(0 のときは出さない ——採番のたびに 1 行増やせば、読む人は速やかに読まなくなる)。

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

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

測って、やらないと決めたこと

やらない判断も、理由と数字ごと残す。 残さないと、同じ実験をやり直すことになる。

Ark に整数の代理キーを入れる(2026-09-18・入れない)

統計の count() を速くする案として出た。PostgreSQL 17 で 100 万 ARK + 200 万行の 履歴を両方の形で作って測った:

今の形(ark が主キー) 代理キー(idark に一意索引)
ark 表 + 索引 173 MB 202 MB(+17%
履歴表 + 索引 330 MB 281 MB(−15%)
合計 503 MB 484 MB(−4%)
解決(ark で 1 本) 0.077 ms 0.059 ms
100 万件を数える 27.2 ms 27.7 ms
  • 数えるのは速くならない。 count(ark) 27.2ms / count(id) 27.7ms / count(*) 24.3ms は全部誤差で、行を読む費用が支配的である。列の幅は効かない
  • ark 表はむしろ 17% 太る。 解決は ARK 文字列で引くので、代理キーを入れても ark の一意索引は消せない——索引が 1 本増えるだけである
  • 得をするのは履歴側の FK(200 バイトの文字列 → bigint)だけで、差し引き 4%

数字より設計の理由が重い。 ark が主キーであることはこの台帳の主張そのもの で、「識別子が行の同一性」だから NR を ORM の削除フックで守れている。代理キーを 入れると「同じ ARK の行が 2 つある」がスキーマ上は表現できてしまい、一意制約と いう別の見張りに頼ることになる。

後から入れられる。 履歴が桁で増えて FK 側が効いてきたら、そこで判断すればよい。 統計が遅くて困るなら、先に効く手がある——概算(pg_class.reltuples)か、 集計の保持(shoulder ごとの件数を別表に持ち、採番時に更新)。どちらも代理キーより 効く。

入っているもの: 転送の保留(hold)

委譲先のリゾルバが落ちた、間違った行き先を配ってしまった、機密が漏れて取り下げを 求められた、データが移動中——どれも急いで止めたいが、識別子を殺したくない

額面どおりに「解決を止める」形では作っていない。 404 は嘘(その識別子は 存在する)、503 は識別子が壊れて見える。止めるのは転送だけで、応答は 200 と 記述——D6(url が空なら記述を返す)と tombstone が既に通っている経路に乗せてある。

tombstone hold
意味 対象が失われた 対象は在るが、今は行き先を出せない
期間 恒久 期限つき(必須。上限 ARKHE_HOLD_MAX_DAYS
元の行き先 捨てる 残す。 期限が切れれば戻る
scope ark:tombstone ark:hold

決めたこと:

  • 粒度は 3 つark / shoulder / naan)。同じ 3 列を持たせ、狭いほうが 優先する(1 件を止めた理由のほうが具体的だから)。shoulder と NAAN の保留は 配下の ARK の行を触らない——触らないから戻せる
  • 期限切れをバッチで戻さない。 解決のたびに時計を見る(resolution.hold_of)ので、 止め忘れは残っても戻し忘れは残らない
  • inflection は止めない。 ?info?? も答え続ける——とくに永続性宣言を 引っ込めるのは、約束そのものを取り下げることになる。理由と期限はそこに載せて公開する。
  • 理由は必須。 公開の口に出るし、外す判断にも要る(書かれていない保留は、 掛けた本人以外に外せない)。
  • 止めた事実を公開する。 ?json/.well-known/arkheld に出る ——分散構成では、上位が止めたことを下位が機械的に確かめられる必要がある。
  • 保留の判定で問い合わせを増やさない。 ARK → shoulder → NAAN は同じ 1 本の 問い合わせに載せている(repository._WITH_HOLD_CHAIN)。解決はいちばん回る経路。

まだ決めていないこと:

  • クローラにどう伝えるか。 200 を返す以上 Retry-After は使えない。人には 説明が届くが、機械には「今は行き先が無い」としか伝わらない。
  • 委譲先の NAAN を上位が止められるべきか。 今は自 NAAN に届く主体だけが止められる。

入っているもの: 公開前の削除

NR が縛るのは、外へ出した名前である。 採番した瞬間から縛られるわけではない ——下書きの対象に先に番号を振るのは普通の運用で、その登録が取りやめになったとき、 誰も指さない番号が永久に残るほうが約束を守っていることにはならない。

列は 2 つある。published_at は行ったり来たりし、first_published_at は片道 ——外に出た名前は、引っ込めても「出ていなかったこと」にはならない。

公開していない 公開中
解決 公開のリゾルバでは しない(知らない名前と同じ 404)。閉域のリゾルバは するARKHE_RESOLVE_UNPUBLISHED する
公開 できる/api/publish)——一度出したものを出し直すのも同じ口
取り下げ できる/api/unpublishark:unpublish)。行は残るので戻せる
削除 できる/api/delete できない(409。先に取り下げるか /api/purge で一手に)
名前 消せば withdrawn_name へ移り、二度と採られない

儀式の重さは、主体の位ではなく名前の履歴で決まる。 一度も公開していない予約の 削除は軽く、一度でも公開した名前を引っ込める・消すには理由と打ち直しが要る。 届く範囲は今までどおり 3 段で、組織は自分の shoulder、NAAN 管理者は自 NAAN、 RA は全部

決めたこと:

  • 既定は「採番と同時に公開」。 公開の一手間を既存の呼び出し側に課すと、足すのを 忘れた側で「採番できているのに解決しない」が静かに積もる。
  • 公開は取り消せる(0.4.0 で 0.3.0 の決定を覆した)。出してよいかの判断は 対象を持っている側にある——公開してはならなかったものに気づくのは RA ではなく 預けた側で、そこから上げて戻ってくるまで出たままになる。戻せるのは公開だけで、 削除は戻せないので、2 つを 1 手にまとめていない。
  • 名前は解放しない。 予約した文字列は既に誰かの手にあることが多い(それが予約の 目的そのもの)ので、振り直せば外からは NR 違反と見分けがつかない。
  • scope を 3 つに分けたark:mint / ark:unpublish / ark:delete)。 戻せる操作と戻せない操作に同じ鍵を渡さない——渡すと、戻せるほうを使いたい だけの主体に、戻せないほうまで渡る。
  • purge の「RA だけ」をやめた(0.4.0)。unpublishdelete の 2 手で同じ 結果に届く以上、1 手だけを位で縛っても守っていることにはならない。縛るのは 届く範囲と、理由・打ち直し・監査のほうである。
  • 修飾子付きの子がいる名前は取り下げられない。 親だけ消すと、継ぐ先の無い部分 参照が残る。
  • 公開前を解決するかは、リゾルバの置き場所で決めるARKHE_RESOLVE_UNPUBLISHED)。 閉域で採番した ARK をその網のリゾルバが引けないなら配る意味が無い一方、?info は 認証を要さないので、既定は漏れない側にしてある。
  • 閉域で引けていた ARK を取り下げると、その網では引けなくなる。 それでも名前が 別のものを指すことは無い(404 になるだけ)——NR が守るのはそこである。
  • 逃げ道は塞がず、1 本に絞って跡を残すpurge)。裁判所の削除命令や、公開して はならなかった個人情報には、識別子より重い要求が来る。逃げ道が無ければ誰かが DB を 直接叩く——跡の残らない削除がいちばん悪い。RA の運用者だけ(ark:purge かつ authority=system)、理由必須、confirm に ARK を打ち直し、名前は解放しない。 ORM 側でも、そのセッションが名指しした 1 本以外は落とせない(sanctioned_purge)。 使えば約束を破ったことになるので、破ったことが必ず残る形にしてある。

まだ決めていないこと:

  • 公開前のまま放置されたものを、arkhe は自分で消さない(消さないと決めた)。 気づく材料は出す——arkhe statいちばん古い公開前ark list --state reserved --older-than N溜まっていることは数ではなく古さに 出る(10 件でも昨日なら普通、1 件でも 3 年前なら放置)。消すかどうかは、 その予約を握っている組織が決める。
  • 一括の公開・取り下げが無い。 1 本ずつ。投入の規模で必要になったら足す。

環境まわりのメモ

  • ローカルの dist/(過去のビルド成果物)、db.sqlite3site/.venv/すべて .gitignore 済みで追跡されていない。消して困るものは無い。
  • compose/oidc見本であって手本ではない——秘密値が平文、Keycloak は dev モード。
  • CI は無い。 検査も公開も scripts/ の 3 本で、走らせるのは手元である ——check.sh(検査ぜんぶ)、deploy-docs.sh(gh-pages へ)、release.sh (版を出す。--publish のときだけ実際に出る)。系統を 2 つ持たないため。 3 本とも引数の扱いは同じ——-h で冒頭の注釈を出し(行数ではなく「最初の # でない行まで」で切るので、書き足してもずれない)、知らない引数は exit 2 で止める。deploy-docs.sh だけ素通ししていて、--dryrun と打ち間違えると 黙って gh-pages へ出ていた
  • リリースノートは CHANGELOG.md のその版の節から起こすrelease.sh)。 gh --generate-notes が並べるのは PR なので、main へ直接コミットするこの 体系では比較リンク 1 行だけになる——v0.0.9 と v0.2.0 が実際そうなっている。
  • ドキュメントサイトは gh-pages ブランチを配信している(Settings → Pages → Deploy from a branch)。書き手は deploy-docs.sh だけで、gh-pages は手で触らない
  • .github/ に残っているのは issue と PR のテンプレート、Dependabot(uv lock を毎週)。