Skip to content

Latest commit

 

History

History
329 lines (247 loc) · 21.3 KB

AGENTS.md

File metadata and controls

329 lines (247 loc) · 21.3 KB

AGENTS.md

このリポジトリを改修するときの手引き。人にも、コーディングエージェントにも同じ ものを渡す——片方だけの決まりを作ると、もう片方が破る。

設計の意図そのものは不変条件と Contributing にある。ここに書くのは手順と、 実際に踏んだ罠。

今どこまで来ていて何が無いかは STATUS.md。 版・現在地・分かっている穴は そちらだけに書く(両方に書くと必ず片方が古くなる)。


1. 何をしているシステムか

ARK 識別子の払い出しと解決。一度配った名前が、別のものを指すようにならない ——設計の大半はこの 1 つの約束から出てくる拒否である。

だから、この基盤で最も避けたい失敗は「動かないこと」ではなく、間違ったまま 静かに動き続けること。以下の手順はほぼすべてそのためにある。

2. 手を動かす前に

uv sync --frozen --all-extras     # lock どおりに入れる
uv run pytest -q                  # 全部通ること
uv run ruff check src tests clients

--frozen は lock と pyproject.toml がずれていたら落ちる。依存を足したら uv lock を実行する(忘れると check.sh で落ちる。それが狙い)。

CI は無い。検査も公開も手元で走る

bash scripts/check.sh          # 検査ぜんぶ
bash scripts/deploy-docs.sh    # ドキュメントサイトを gh-pages へ
bash scripts/release.sh vX.Y.Z # 版を出す(--publish を付けたときだけ実際に出る)

系統は 1 つにする。 手元と CI の 2 系統があると「片方では通る」変更が生まれ、 やがて誰も片方を見なくなる。GitHub Actions は置いていない——.github/ に残して あるのは issue と PR のテンプレート、そして依存の更新(Dependabot)だけ。

3 本とも引数の扱いは同じ。 -h / --help で冒頭の注釈をそのまま出し、知らない 引数は exit 2 で止める。注釈の切り出しに行番号を使わない——sed -n '2,24p' は 見出しを 1 行足すたびにずれるので、「最初の # でない行まで」で切る:

-h|--help) awk 'NR > 1 && !/^#/ { exit } NR > 1' "$0"; exit 0;;

知らない引数で止めるのは deploy-docs.sh のためでもある。素通ししていた頃は --dryrun と打ち間違えると --dry-run として扱われず、何も言わずに gh-pages へ force push していた。止めたつもりが出ているのがいちばん困る。

check.sh は sync(--frozen)→ ruff → pytest → 使い捨ての PostgreSQL を立てて マイグレーションを往復(デモの DB には当たらない)→ 通しの検査(本番と同じ形に 建てて HTTP で叩く)→ OpenAPI のずれ → mkdocs --strict。 道具が無い項目は黙って通さず SKIP と出す——「入っていないから通った」がいちばん危ない。

手順をなぞるための覚え書き(.claude/skills/)

AGENTS.md が正で、あちらは実行に移すための覚え書きである。 手順が変わったら 先にここを直す——2 か所に書くと必ず片方が古くなる、というのはこの文書が冒頭で 言っているとおりである。

何のため
release 版を出す 6 手順と、踏んだ罠(節の差し込み先・日英・STATUS・OpenAPI の検査)
measure 測り方。クライアントの天井・ばらつき ±20%・時間で見えないものは数える
chaos kind の arkhe を壊して可用性を測る。期限切れの chaos は apply で再実行されない

書いてあるのは、間違えた記録である。 どれも実際にその形で誤ったことがあり、 次に同じところで止まらないために置いてある。

3. 地図

まずリポジトリ全体。どこに何があり、どれが生成物か。

src/arkhe/     実装。層の説明は下
tests/         ファイル名が対象を表す。`tests/e2e/` は**通しの検査**(`-m e2e`。docker が要る)
clients/python/ Python クライアント(`arkhe-client`)。**別パッケージだが同じ網で検査する**
               ——`test_contract.py` が公開 OpenAPI と両方向で突き合わせる
alembic/       マイグレーション。**PostgreSQL で検証する**
docs/          MkDocs。`page.md` が英語、`page.ja.md` が日本語
compose/oidc/  Keycloak つきの体験環境。**見本であって手本ではない**
scripts/       check.sh / deploy-docs.sh / release.sh(**CI の代わり**)、export_openapi.py、
               seed_e2e.py(**通しの検査の台帳**。`tests/e2e/` が呼ぶので腐らない)、
               bench.py(**check.sh からは呼ばない**——機械とその日に左右される値は緑と赤で語れない)
.github/       issue と PR のテンプレート、Dependabot。**ワークフローは持たない**
AGENTS.md      これ。手順と罠
STATUS.md      現在地と、分かっている穴
CHANGELOG{,.ja}.md  版ごとの変更。**未リリースの節に足す**
site/ dist/ db.sqlite3   生成物。**すべて .gitignore 済み**

そして実装の層。

arkspec/    ARK 仕様を純粋な関数で。**stdlib しか import しない**
domain/     解決・認可・採番・管理・一覧の絞り込み。HTTP を知らない
db/         SQLAlchemy のモデル
auth/       3 つの機構、1 つの Principal
api/        FastAPI のルータ、管理画面、i18n
cli.py      運用コマンド。**画面と同じ domain を呼ぶ**

arkspec/ と domain/resolution.py が何にも依存しないのは偶然ではない ——フレームワークを丸ごと入れ替えたときに 97 件のテストが無傷で移れた理由が これ。仕様のロジックが DB を必要としたら、設計がどこか間違っている。

画面と CLI に差を作らない

cli.py の冒頭に書いてあるとおり、CLI にしかできないこと・画面にしかできない ことを作らない。実際に破れていた例が ark list で、画面にはあったが CLI に 無かった。絞り込みは domain/queries.py に置いて両方が同じ式を通る。

到達範囲の判定を 2 か所に書かない。 片方だけ直したときに、画面には出ない ものが CLI には出る——そして気づくのは、見えてはいけないものが見えた後になる。

文言は画面ごとのファイルに、日本語と英語を並べて足す

api/i18n/ は画面で分けてある(_shell _ledger _clients _arks _signin _audit)。言語で分けていないのは、対を離すと片方だけ足したことが差分に 出ないから——起動時の検査は最後の砦であって、最初の砦ではない。訳文に ` や ** を書かないこと(そのまま画面に出る。テストで縛ってある)。

4. 触る前に知っておくこと

到達範囲は 3 段(Authority)。上の段は下の段を含む。

SYSTEM   RA の運用者。全 NAAN に届く
NAAN     1 つの NAAN の配下すべて
MANAGER  1 組織ぶん(shoulder_id を併せると 1 shoulder に固定)

到達範囲はクライアント登録の属性で、要求本文やトークンの中身からは決して 来ない。トークンは scope を狭めることしかできない。ここを緩める変更は 差し戻される。

CLI は _root() でシステム管理者として動く。サーバのシェルに入れる時点で DB に 届くので権限で絞っても意味の在る防御にならない——代わりに操作は必ず監査に残る。

公開前の ARK は、解決の側から見ると「無い」

Ark.published_at が null のものは未登録の名前と同じに扱う(完全一致でも祖先 passthrough でも拾わない)。落とす場所は 2 か所ある——db/repository.py の問い合わせと domain/resolution.py の is_public。二重に見えるが、どちらも要る: 前者は リゾルバに要らない行を載せないため、後者は決定ロジックが「行があるか」ではなく 「外に出した名前か」で決めることを、差し替え可能な repo の向こう側でも守るため。

委譲まわりを触るなら、解決の順を先に読む

domain/resolution.py の判断順(完全一致 → 祖先 → 検査桁 → shoulder の redirect → 404)は、順序そのものが結論を決める。検査桁が shoulder 委譲より先にあるので、 委譲先が検査桁を作らないと上位経由の解決だけが 404 になる——下位に直接来た要求は 通るので、いちばん見つけにくい壊れ方をする。

複数の arkhe で分担する構成の前提と穴は分散して運用する。 台帳を分けた瞬間、コードが守っていた不変条件のいくつかが人の手に移る(同じ shoulder を 2 か所で採らない、権威を持つ台帳は NAAN あたり 1 つ)。

5. 実際に踏んだ罠

同じ穴に二度落ちないために書いてある。 どれも実際に起きた。

alembic check の指摘は本物 use_alter の FK は op.create_table が落とす。「宣言してあるのに作られない」が実際に起きた。SQLite は通す
SQLite にも一度は流す 制約の付け外し(create_foreign_key / drop_constraint)は SQLite に ALTER が無く、batch_alter_table を通さないと NotImplementedError で止まる。where 節も sa.text() にしないと落ちる。PostgreSQL だけ見ていた 3 版のあいだ、Quickstart のとおりに打つと途中で死んでいた。tests/test_migrations.py が頭まで流す
マイグレーションは PostgreSQL で見る SQLite は PostgreSQL が弾くスキーマを通す。開発中に 2 回、これがバグを隠した。upgrade → downgrade base → upgrade → check まで回す
CHANGELOG に s.index("### Added") を使わない ファイル先頭からの最初の一致を返すので、リリース済みの節に追記してしまう。2 回やった。「未リリース」の見出しを起点に探す
CSP の script-src 'none' は Swagger UI を白紙にする /api/docs /api/redoc だけ DOCS_CSP を当てている。触ったら実物を開いて見ること
/healthz は全モードに要る resolver / minter / admin のどれで起動しても要る。1 つに付け忘れて k8s に殺され続けた
/readyz に Depends(get_session) を使わない 依存がハンドラより先に落ちるので 503 でなく 500 になる。自前でセッションを開く
SQLAlchemy の default は None を代入しても効く 「値を入れていない」と「None を入れた」を区別しないので、Column(default=utcnow) に published_at=None を渡すと予約したはずの ARK が公開済みで入る。既定値を持たせず、domain/minting.py で決める
公開前の ARK を消すとき、指している行を先に落とす ArkChange と MintReceipt は ark.ark への FK を持つ。残すと外部キー違反で commit が落ちる
dataclasses.replace は代入しないと効かない replace(res, ...) の戻り値を捨てたまま次行で return していて、直したつもりが直っていなかった
i18n の訳文に ` や ** を書かない 画面にそのまま出る。何度もやったのでテストで縛ってある
部分ユニーク索引 (manager_id, label) の一意制約は空文字を弾いてしまう。ラベル無しの主体は 1 組織 1 つしか作れなくなる
参照ページは生成物ではない docs/reference/cli.md と configuration.md は手で書く。Contributing に「生成される」と書いてあったのは誤りで、そのせいで設定 2 つとコマンド 1 つが未記載のまま残っていた。今は tests/test_docs.py が落とす
uv lock は版を上げたら要る pyproject.toml の version を上げると lock とずれて --frozen が落ちる
Typer の help は import 時に確定する だから CLI の言語は環境変数から一度だけ決まる。--lang のような実行時切り替えは作れない

6. テスト

新しい挙動には、それが無ければ落ちるテストを付ける。認可に触るなら 通らないほうの場合も書く——面白いのは正しい主体が通ったことではなく、 間違った主体が通らなかったこと。

テストは文になっている(test_m3_another_organisations_ark_cannot_be_updated)。 落ちたときに、例外を投げた関数ではなく壊れた決まりが名指しされるように。

コードは英語で書く——識別子・コメント・docstring とも。日本語が残るのは 多言語カタログ(api/i18n/・cli_i18n.py・errors.py の ja)だけで、 あれは日本語の画面そのものである。文書(この文書・STATUS・docs/*.ja.md)は 今までどおり日本語。

API を足したらクライアントも落ちる。 clients/python/tests/test_contract.py が 公開 OpenAPI 文書と両方向で突き合わせているので、エンドポイントを足すと 「クライアントも持つべきか」を決めるまで緑にならない。決めずに素通りしないための仕掛け。

どのファイルが何を見ているかは STATUS.md の「テストの内訳」。 ここに並べ直すと必ず片方が古くなる——実際、この一覧は 5 本ぶん遅れていた。 ファイル名が対象を表すので、迷ったら対象の名前で探す。

7. 文書

MkDocs Material、接尾辞で 2 言語(page.md が英語、page.ja.md が日本語)。 図は Mermaid のコードブロック——再生成する画像ファイルを持たない。

uv run mkdocs build --strict     # 警告 0 を保つ
uv run python scripts/export_openapi.py   # API 仕様はここだけ生成物

設定を足したら docs/reference/configuration.{md,ja.md} に、コマンドを足したら cli.{md,ja.md} に、両方の言語で行を足す。 忘れるとテストが落ちる。

ページを 1 枚足すときにやること(どれか 1 つ抜けると片方の言語で消える):

  1. docs/…/name.md と docs/…/name.ja.md を対で作る
  2. mkdocs.yml の nav に英語の題で足す
  3. 同じ題を nav_translations に日本語で足す——ここを忘れると日本語版に英語の項目が並ぶ
  4. 日本語の見出しに深いリンクを張るなら {#anchor} を自分で書く(attr_list が効く)。 自動生成のアンカーは見出しを 1 つ足すとずれる

8. コミット、ブランチ、リリース

コミットメッセージは変更ではなく理由を書く。将来の読者が知りたいのは、 何を知っていたからそれが正解だったのか——特に答えが奇妙に見えるところで。

ブランチは Gitflow に沿う

ブランチ 切る元 戻す先 何を載せるか
main — — リリースした版だけ。 タグはここに打つ
develop main — 次の版に入るもの
feature/<名前> develop develop 機能・文書・手順の変更
bugfix/<名前> develop develop 次の版で直すもの
release/X.Y.Z develop main と develop 版を上げ、CHANGELOG と STATUS を揃える。機能は足さない
hotfix/X.Y.Z main main と develop 出した版への急ぎの修正

main と develop には直接コミットしない。 変更は PR を通し、--no-ff で マージする——ブランチの単位が履歴に残り、何をひとまとまりとして入れたかを後から 辿れる。Dependabot の PR も develop に向ける(.github/dependabot.yml)。

0.14.0 までは main へ直接コミットしていた。初期は速かったからで、方針では なかった。 この節が以前「main へ直接コミットするこの体系」と書いていたので、 方針と読まれた。develop は 2026-10-04 に main から作った。

リリース

リリースも手元で回る。既定は「出さない」——確かめるのは安く何度でもできるが、 タグと GitHub のリリースはそうではない。

bash scripts/release.sh v0.0.9              # 検査と dist の作成だけ(どのブランチでも)
bash scripts/release.sh v0.0.9 --publish    # タグ → push → GitHub のリリース(main でだけ)

手順そのものも検査する——版の一致(pyproject とタグ)、CHANGELOG にその版の節と リンク定義が日英ともあること、「未リリース」の比較リンクが新しい版を指していること。 どれも実際に間違えたことのある場所である。--publish は main の上でしか動かない ——タグが release/X.Y.Z の上に打たれると、main に無いコミットが版を名乗る。

リリースノートは CHANGELOG.md のその版の節から起こす(本文は英語、日本語は 在処を指すだけ)。gh --generate-notes は使わない。main へ直接コミットしていた頃は、 あれが並べるのは PR なので拾うものが無く、比較リンク 1 行だけの空の本文になった (v0.0.9 と v0.2.0)。PR を通すようになっても使わない——PR の題を並べたものは、 同じ変更を別の言葉で書いたもう 1 か所で、CHANGELOG と必ずずれる。

版は pyproject.toml だけで決まる。ずれていれば release.sh が落とす ——「v0.0.2 と名乗る 0.0.1」を世に出さないため。手順:

  1. git switch -c release/X.Y.Z develop
  2. pyproject.toml の version を上げる → uv lock
  3. CHANGELOG の「未リリース / Unreleased」の下に版の節を作る(リリース済みの 節に追記しない)。末尾のリンク定義も 2 言語ぶん、「未リリース」の比較リンクも 新しい版に置き換える
  4. STATUS.md の版と日付を合わせる(同じコミットに入れる——0.1.0 と 0.2.0 では触らずに出したので、「版 0.0.9」のまま 2 版ぶん据え置きになった。 リリースで動く数字を、リリースの手が触らないならそうなる)
  5. コミットし、bash scripts/release.sh vX.Y.Z が緑になるまで release/X.Y.Z で直す
  6. main に --no-ff でマージ → main の上で bash scripts/release.sh vX.Y.Z --publish
  7. main を develop に --no-ff でマージして push し、release/X.Y.Z を消す ——戻し忘れると、次の版の develop に版上げと CHANGELOG の節が無い
  8. main の上で bash scripts/deploy-docs.sh(変更履歴のページを追随させる。 main 以外からは出さない——サイトに未リリースの記述が載る)

急ぎの修正は hotfix/X.Y.Z を main から切り、2〜8 を同じにたどる。

arkhe を submodule として組み込んでいる側があるなら、そこのポインタも進める。 それは組み込んだ側の手順なので、ここには書かない——この文書は arkhe の手引きで あって、arkhe を使う人の手引きではない。

0.x のあいだはプレリリースとして出る。

デモの台帳に実在するものを置かない

seed_demo.py と realm-arkhe.json に置くのは例示用の名前と番号だけ。 本物の機関名を置くと、その機関が arkhe を使っているように読める——画面をその まま見せる資料や録画に載ったときに、こちらの意図と関係なく既成事実になる。 NAAN も同じで、割り当て済みの番号は使わない(99999 は仕様が試験用に予約して いる番号、12345 / 54321 は例示のための値)。

9. 動かして確かめる

画面に関わる変更は、実物を開いて見る。 括弧書きの用語も、スマホ幅での はみ出しも、テストでは落ちなかった。

cd compose/oidc && docker compose up -d      # arkhe :8057 / resolver :8058 / Keycloak :8080
docker compose exec -T arkhe arkhe ark list

LAN から見せるときは lan.yml を重ねて ARKHE_DEMO_HOST を指定する。

イメージを焼き直したら古いコンテナを残さない——別ポートに古い版が生きて いて、見ている画面が違う、が実際に起きた。

10. 迷ったとき

識別子は振り直せない。 迷う変更のほとんどは、「間違えたときに取り返せるか」 で決まる。取り返せないものは、緩めるより拒否するほうを既定にする。