oksskolten

Activity, over time

Daily observations from the public event archive.

Attention

Daily stars & forks

Loading attention chart

Star accumulation

Cumulative stars

Loading star accumulation chart

Pull requests

Daily opens & merges

Loading pull requests chart

Conversations

Daily issues & comments

Loading conversations chart

Community

Daily accounts & reviews

Loading community chart

Releases

Daily releases

Loading releases chart

Mentions

1post · 30d
Nov 31 post
  1. Zenn · · @babarot

    ADRをregoでlintする AIが書くADRのフォーマットをどう揃えるか 自分のリポジトリでは、設計の決定を ADR(Architecture Decision Records)として docs/adr/ に残している。最近は、その ADR の多くを Claude Code に書いてもらっている。決定を相談して、「これを ADR にしておいて」と頼むと書いてくれる。 ただ、AI に書かせた ADR は、少しずつ自由になっていく。テンプレートを置いて書き方を指示していても、ずれるときはずれる。たとえば、 Decision に、画面のメニュー項目の一覧や定数、構造体の定義まで書いてある。仕様書に近づいて、コードを変えるたびに古くなる Status が Proposed のまま、実装は別の形で進んでいる 決定を変えたときに、古い ADR を書き換えたり、新しい ADR が古い ADR の内容を何の印もなく覆したりしている みたいなこと。自分の ADR を持つリポジトリ7つを読み直したところ、コードとずれていたのはどれも、ADR に書かなくてよい種類の内容だった。決定そのものや、守るべき保証は古くなっていなかった。 このうち2つ目と3つ目は、Status の値や ADR 同士の参照といった構造の問題で、見ればすぐに分かる。厄介なのは1つ目で、節も Status も正しいのに、意味のうえでは仕様書になっている。構造を見ても分からず、読んで意味を判断するしかない。 なので、ADR に何を書き、何を書かないかをかっちり決めて、構造だけでなく意味についても守らせたいと思った。 ガイドラインではなくポリシーで縛りたい AI が書く ADR の質をどう保つかは、ほかの会社でも話題になっている。 以下の記事は、雛形だけで AI に ADR を書かせると読みづらい ADR が頻出したので、過去のレビューの指摘から規約をまとめたガイドラインを作った話。ガイドラインを、ADR を書くスキル、Claude Code の rule、CI の AI レビューから参照させている。記載ルールがレビュー用のプロンプトや過去の指摘に分散していた、という問題意識もおもしろかった。 zenn.dev 以下の記事は、ADR を書く Claude Code の skill を用意し、テンプレートとガイドラインに沿って書かせている。 zenn.dev どちらも参考になるが、ガイドラインやスキルは結局のところ AI へのお願いになる。自分もルールを文章にして、Claude Code が ADR を触るときに読む rule に置いているが、守られることが多くても、守られる保証はない。AI レビューで指摘させるのも、指摘するのがまた LLM なので、見逃すこともあれば、同じ ADR に違うことを言うこともある。 そこで、ルールをガイドラインではなくポリシーとして書くことにした。以下では、ADR が守るべき決まりを「ルール」、それを rego で書いて検査できるようにしたものを「ポリシー」と呼ぶ。 構造のルール(節の有無と順序、Status の値、ADR 同士の参照、リンク先が存在するか)は、ポリシーで機械的に判定し、違反があれば CI を落とす 意味のルール(仕様書になっていないか)は、ポリシーの中から LLM に質問して判定させ、結果を PR に注意として出す 意味の判定には LLM が要るが、何を判定させるかはポリシーに書いて、構造のルールと一緒に管理する。LLM の答えはぶれるので CI は落とさず、注意に留める。こうすれば、書き手が人でも AI でも、構造は同じ基準で弾き、意味のずれも PR の上で気づける。 ルールをregoで書くためにCLIを作った conftest(OPA)は、構造化されたデータをポリシーで検査するためのツールで、こういう検査はまさに専門なので、lint のルールは独自の DSL や YAML の設定で書くより、rego で書くのがよいと思っている。 ポリシーがコードなので、opa test でテストを書ける。条件の組み合わせを設定のキーで表すより、素直に書ける 「置き換えの参照が循環していないか」のような、集合やグラフの規則が書きやすい 実際、前に書いた conftestでMarkdownの構造をバリデーションする でも、remark と conftest で Markdown の構造を検査していた。 ただし conftest は Markdown を読めない。前の記事では、remark で AST の JSON にし、jq でファイル名を足してから conftest に渡すシェルスクリプトを書いていた。このつなぎの部分が弱く、変換に失敗したファイルを見逃して、残りのファイルだけで検査が通ってしまうことがあった。リンク先が存在するかのように、ファイルを見ないとわからないことも、rego からは調べられない。 なので、Markdown の解析と OPA での評価を1つのバイナリにまとめた CLI を作った。 babarot/regomark regomark がやっていることは次のとおり。 Markdown を解析し、最上位のブロックとリンクを行番号付きの JSON にする リンクをリポジトリに対して解決し、リンク先が存在するかを JSON に入れる ポリシーが意味について質問していれば、その質問と対象の原文を LLM に渡して答えをもらう 組み込んだ OPA で、rego のポリシーを評価する。構造の検査だけでなく、LLM の答えを使って ADR の内容をルールと照らし合わせる意味の検査もできる(後述) 違反を行番号付きで出し、deny があれば終了コードで CI を落とす regomark 自体には ADR についての実装はなく、ADR のルールも、LLM に何を判定させるかも、すべて rego のポリシーの側にある。解析から評価までを1つのバイナリで行うので、Node の依存も、conftest につなぐシェルスクリプトも要らなくなった。 ルールを構造と意味に分ける ADR の書き方のルールは、まず文章として .claude/rules/adr.md にまとめ、paths: docs/adr/** で Claude Code が ADR を触るときに読ませている。そのうえで、各項目を rego のポリシーにして regomark で検査している。 ルールの項目と、検査のしかたの対応はこうなっている。 ルール 種類 検査のしかた ADR は意思決定の文書で、仕様書ではない。画面の細部、定数、コード片は書かない 意味 仕様書になっていないかを LLM で判定し、warn にする 作業の手順は書かない 意味 手順書になっていないかを LLM で判定し、warn にする 節は Status、Context、Decision、Consequences の4つ 構造 節の有無と順序を deny で検査する Status の値は Accepted、Rejected、Deprecated、Superseded by ADR-NNN のどれか 構造 値を deny で検査する Status の最初の段落には、値と Partly replaced by ADR-NNN の行だけを書く 構造 段落の形を deny で検査する 決定を変えるときは新しい ADR を書き、古い方に Superseded by か Partly replaced by を付ける 構造 参照先が存在するか、向きが正しいか、循環していないかを deny で検査する 決定を守るテストやパッケージは、ファイルへの相対リンクで名指しする 構造 リンク先のファイルが存在するかを deny で検査する 意味のルールは、ADR がその文書としてふさわしいかを問うもので、ふつうは機械的に検出できない。「ADR が仕様書や手順書になっていないか」はいちばん分かりやすい例で、上で紹介した記事のガイドラインでも、Decision が実装の詳細を抱え込む ADR が問題として挙がっている。こうしたルールは LLM を通して判定する。 一方、構造で判定できるように、ルールの側も機械が読める形に寄せた。regomark は Status の最初の段落を値と参照として読み、理由まで同じ段落に書かれるとどこまでが値なのかが決められなくなるので、その段落には値と参照だけを置く。名指しをリンクにするのも同じで、ADR の本文から関数名を推測して探すより、リンク先が存在するかを見るほうが確実に判定できる。 regomarkの使い方 コマンドは3つ。 regomark check [--config .regomark.yaml] [--format text|github|json] [--judge | --no-judge | --judgments FILE] regomark dump # ポリシーに渡す入力の JSON を出す(テストを書くため) regomark questions # ポリシーが LLM に判定させる質問を出す 設定はリポジトリの直下の .regomark.yaml に書く。 documents: docs/adr # 検査する Markdown のディレクトリ policy: .regomark/policy # rego のポリシーのディレクトリ entrypoint: adr # deny と warn を集めるパッケージ(data.adr) skip_rules: [] # 違反を捨てるルール ID exclude: [] # 検査しない文書(docs/adr/000-*.md など) judge: # ポリシーの質問を LLM でどう判定するか(任意) command: [sh, -c, 'claude -p --output-format json --model sonnet --tools "" --json-schema "$REGOMARK_ANSWER_SCHEMA"'] max_questions: 200 # 1回の実行で聞く質問の上限 regomark check は、違反を path:line: [rule] message の形で出す。ルールを決める前に書かれていた gh-infra の ADR に当てると、こうなる。 $ regomark check docs/adr/002-import-action-selector.md:5: [status-value] Status "Proposed" must be Accepted, Rejected, Deprecated or "Superseded by ADR-NNN" docs/adr/003-nullable-deletion-markers.md:1: [required-sections] missing "## Consequences" docs/adr/003-nullable-deletion-markers.md:1: [required-sections] missing "## Decision" docs/adr/004-deletable-deletion-markers.md:1: [required-sections] missing "## Consequences" docs/adr/004-deletable-deletion-markers.md:1: [required-sections] missing "## Decision" $ echo $? 1 --judge を付けると、ポリシーの質問を LLM で判定し、意味の注意も warning: として並ぶ(後述)。--format github にすると、どちらも GitHub Actions の注釈として PR の差分に付く。終了コードは、deny がなければ 0、あれば 1、設定やポリシーの誤りで検査そのものができなければ 2。warn は終了コードを変えない。 構造のルール ポリシーに渡る入力は regomark dump で見られる。goldmark で取り出した最上位のブロックが、文書の順に、始まりと終わりの行番号付きで並ぶ。 $ regomark dump | jq -c '.documents[1].blocks[:4]' [{"kind":"heading","level":1,"line":1,"end_line":1,"text":"ADR-002: Import action selector for interactive diff viewer","lang":""}, {"kind":"heading","level":2,"line":3,"end_line":3,"text":"Status","lang":""}, {"kind":"paragraph","level":0,"line":5,"end_line":5,"text":"Proposed","lang":""}, {"kind":"heading","level":2,"line":7,"end_line":7,"text":"Context","lang":""}] リンクは regomark がリポジトリに対して解決し、kind(file、dir、missing、outside、external)まで入れて渡す。rego はファイルを読めないので、リンク先が存在するかは regomark の側で調べておく。 Status の値を検査するポリシーはこうなっている。 deny contains {"rule": "status-value", "path": doc.path, "line": adrlib.status_paragraph(doc).line, "message": msg} if { some doc in input.documents s := adrlib.status(doc) not s in allowed_status not regex.match(^Superseded by ADR-\d{3}\.?$, s) msg := $"Status \"{s}\" must be Accepted, Rejected, Deprecated or \"Superseded by ADR-NNN\"" } 違反は rule、path、line、message を持つオブジェクトで返す。行番号は入力のブロックから取るので、違反は Status の値が書かれた行に付く。adrlib はポリシー側に置いたライブラリで、「ある見出しの節に含まれるブロック」を返す関数などを持っている。 置き換えの循環は graph.reachable で書ける。 deny contains {"rule": "replacement-cycle", "path": doc.path, "line": r.line, "message": msg} if { msg := "this ADR is in a cycle of replacements" some doc in input.documents some r in adrlib.refs(doc) some d in numbered d.number == r.target doc.path in graph.reachable(replacement_graph, {d.path}) } 構造のルールは今のところ14ある。 ファイル名、H1、必須の節とその順序 Status の形と、置き換えの参照の置き場所 参照先の ADR が存在するか、参照の向きが正しいか 番号の重複、欠番、000 番 置き換えの循環 Accepted の ADR から、リポジトリ内の存在しないファイルへの相対リンク regomark は独自の組み込み関数を足していない。なので、ポリシーは素の opa test、opa check --strict、Regal でそのまま検査できる。 意味のルール ここからが、普通の lint にはできないところ。「この ADR は仕様書になっていないか」「1本の ADR に2つの決定が入っていないか」のような意味の判定は、rego でも Go でも書けない。LLM なら判定できる。 regomark では、ポリシーが LLM に質問できる。何を聞くかも、答えをどう扱うかも、構造のルールと同じ rego で書く。 質問をポリシーに書く ポリシーに questions を書くと、regomark がそれを LLM に判定させる。質問は次の項目を持つオブジェクトで、「どの文書の、どのブロックについて、何を聞くか」を表す。 id: 質問の種類 path と line: 対象の文書と、その場所 blocks: LLM に見せるブロック。入力の blocks から選ぶ ask: yes か no で答えられる質問 たとえば「Decision の節に、関数名や定数が書かれているか」を聞いて、yes なら注意を出すポリシーはこう書ける。 questions contains {"id": "spec-detail", "path": doc.path, "line": h.line, "blocks": [doc.blocks[i + 1]], "ask": "Decision の節に、関数名や定数が書かれているか"} if { some doc in input.documents some i, h in doc.blocks h.kind == "heading" h.text == "Decision" } warn contains {"rule": "spec-detail", "path": j.path, "line": j.evidence_line, "message": j.reason} if { some j in input.judgments j.id == "spec-detail" j.status == "ok" j.verdict == "yes" } regomark は、質問と、選んだブロックの原文を行番号付きで judge.command(上の設定では claude -p)に渡す。LLM は yes か no と、その理由と、根拠にした行番号で答える。yes の根拠は、渡した行のどれかでなければならない。答えは input.judgments としてポリシーに戻り、上の warn が拾う。 手元で regomark check --judge を実行すると、こうなる。 regomark check --judge 最初に挙げた「Decision に定数や構造体の定義まで書いてある」ADR が、こうして根拠の行と理由つきで PR に出る。節も Status も正しいので、構造のルールだけでは通ってしまっていたものだ。 答えには status が付く。答えが返れば ok、聞かなかったら not_run、コマンドが失敗したら provider_failed、答えの形が崩れていたら invalid_answer。聞けなかったときにどうするか(たとえば provider_failed なら注意を出し、not_run なら何も言わない)も、ポリシーで決める。 LLM の答えを読めるのは warn だけで、答えによって CI が落ちることはない(後述)。 ローカルで意味検査をする(claude -p) 意味検査をするかどうかは、使う人が決める。リポジトリの .regomark.yaml に書けるのは「どう判定させるか」(judge.command)までで、実際に意味検査をするのは --judge を付けたときか、REGOMARK_JUDGE=on のときだけ。人のリポジトリで regomark check を打っても、そこに書かれたコマンドが勝手に実行されることはない。 LLM を呼ぶ回数と実行コストにも気を配っている。 答えは、送った中身とコマンドをキーにキャッシュする。ADR とポリシーを変えなければ、2回目からは LLM を呼ばない 1回の実行で聞く質問が max_questions を超えたら、聞かずに止める コマンドは並列で動かし、時間切れを設ける LLM には ADR の本文を渡すので、本文に指示が書かれていればそれに従うかもしれない。なので、LLM にはリポジトリを変更する手段を持たせず、上の設定でも claude -p に --tools "" を付けている。 CIで意味検査をする(claude-code-action) CI では、claude-code-action のように、ワークフローの1つのステップとして動く LLM に答えさせたい。これは質問ごとに呼べるコマンドではないので、質問をまとめてファイルで渡す。 regomark questions で、質問と LLM に渡すプロンプトをファイルに書き出す 別のジョブで、claude-code-action にすべての質問に答えさせ、答えをファイルに書かせる もう1つのジョブで、チェックアウトし直して regomark check --judgments で答えを読む LLM が検査するチェックアウトに触れると、たとえば skip_rules にルールの ID を足して deny を消せてしまうので、LLM は検査と別のジョブで動かす。答えは質問の中身から作ったキーで照らすので、ADR やポリシーが変わったあとの古い答えは、どの質問にも当たらない。fork からの PR のように secrets がなく LLM のジョブが動かないときは、答えなしで構造のルールだけを検査する。ワークフローの全体は README に置いている。 検査の流れ 1回の regomark check は、LLM で判定する段階と、ポリシーを評価する段階に分かれている。 ポリシーの questions を評価して、LLM に判定させる質問を決める。これは意味検査をするかどうかに関係なく、毎回行う 質問に答える。judge.command で LLM に判定させるか、答えのファイルを読むか、何もしない(すべて not_run) ポリシーを2回評価する。deny は LLM の答えなしの入力で、warn は答えありの入力で評価する 上で書いたとおり LLM の答えは毎回同じとは限らず、同じ ADR がある日は通って次の日は落ちる CI にはしたくないので、LLM の答えは warn にしか渡さず、そのために評価を2回に分けている。答えがあるときとないときで deny の結果が変わるポリシー、つまり deny が LLM の答えを読んでいるポリシーは、終了コード 2 で止める。 $ regomark check regomark: data.docs.deny depends on input.judgments: it reports different violations with them (1) than without them (0); judgments may only feed warn これで、CI が落ちるかどうかは構造のルールだけで決まり、意味検査をやめても、LLM が失敗しても変わらない。意味の判定は、PR の上の注意として人が読む。 ポリシーの書き間違いをどう見つけるか lint でいちばん困るのは、違反があるのに 0 で終わること。CI が緑なら、誰もその ADR を見直さない。rego は、書き間違えてもエラーにならずに「何も見つからなかった」になりやすいので、regomark は次のようなポリシーを、終了コード 2 で止めるようにしている。 パッケージ名の誤り: package adr.links と書くと、違反は data.adr.links.deny に入り、誰も集めない。lib/ の外のモジュールは entrypoint のパッケージ(package adr)でなければならない deny の打ち間違い: deny を denies と書いたモジュールは、違反を1つも出さない。lib/ の外のモジュールは deny、warn、questions のどれかを必ず定義する 関数として書いた deny: OPA はパッケージの値に関数を含めないので、評価の結果から消える どのモジュールも定義していない data の参照: rego では未定義になり、それを使う deny や warn が何も出さない 入力の項目名の打ち間違い: 入力は Go の構造体から生成した JSON Schema で、評価の前に検証する skip_rules の打ち間違い: ポリシーに rule_ids で ID を宣言してもらい、それと照らす 形の崩れた質問: 項目が足りない、入力にないブロックを指している、といった質問は、LLM に渡す前に止める どれも rego としては正しいので、opa check は通ってしまう。regomark のテストでは、Markdown から終了コードまでを通し、「違反があるのに 0 で終わらない」ことを確かめている。 ルールとポリシーを各リポジトリに配る ルールの文章、ポリシー、設定、ワークフローは、自作の gh-infra で各リポジトリに配っている。gh-infra は GitHub のリポジトリの設定やファイルを YAML で宣言して管理するツールで、FileSet を使うと、同じファイルを複数のリポジトリに PR として配れる。 rego のポリシーもただのファイルなので、ディレクトリごと FileSet で同期すれば、足したポリシーは各リポジトリに届き、消したポリシーは消える。ルールの文章とポリシーを同じ PR で変えられて、ルールを足すのに regomark のリリースは要らない。ポリシーを書く regomark と、それを配る gh-infra を組み合わせると、複数のリポジトリの ADR を1か所のルールで揃えられる。 gh-infra については以下の記事で紹介している。 zenn.dev CI では、mise でバージョンを固定して実行している。 - name: Check the ADRs run: mise x github:babarot/regomark@0.2.0 -- regomark check --format github env: mise-action はトークンを渡さないので、共有のランナーでは API のレート制限に当たる MISE_GITHUB_TOKEN: ${{ github.token }} ADR の中のリンクは、テストのファイルを改名するような、ADR を触らない PR でも切れるので、ワークフローはパスで絞らず、すべての PR で動かしている。 まとめ ADR のルールは、Claude Code が読む文章と、regomark が検査する rego のポリシーの2つで持つ形になった。構造のずれは deny で CI が落とし、仕様書に近づいていないかのような意味のずれは、ポリシーから LLM に判定させて warn として PR に出す。何を検査するかも、LLM に何を判定させるかも同じ rego に書いて、同じ PR で変えて配れる。 実際に regomark を使っている例として、自分のリポジトリを2つ挙げておく。どちらも CI では mise で regomark を取ってきて、.regomark/policy/ に置いた rego のポリシーで docs/adr/ を検査している。 リポジトリ ワークフロー ポリシー babarot/oksskolten regomark.yaml .regomark/policy babarot/claude-recall regomark.yaml .regomark/policy oksskolten は前の記事のリポジトリで、ADR の検査を conftest から regomark に移した。ポリシーは構造、Status、参照、リンクのファイルに分けていて、それぞれに *_test.rego のテストがある。 regomark は ADR 専用のツールではなく、内部に ADR についての実装もないので、ほかの Markdown の検査にも使えるはず。作ったばかりで、自分のリポジトリで回している程度だが便利だったので、Markdown を rego で検査したい人は試してみてください。 babarot/regomark

Latest stargazers

The last 10 distinct accounts we observed this year.

Most active participants

PR, issue, comment & review events in the past year.

Packages

3
PackageVersionDownloads · 30dTrend · 30d
———
———
———

README badges

Embed a badge in your README or website.

Live mentions · last 30 days
babarot/oksskolten mentions in the last 30 days