Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 6 additions & 7 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,15 @@ jobs:
name: Test haskell codes
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: haskell/actions/setup@v1
- uses: actions/checkout@v4
- uses: haskell-actions/setup@v2
id: haskell-setup
with:
ghc-version: "8.8.3"
enable-stack: true
stack-version: "2.5.1"
- name: Restore caches
uses: actions/cache@v2
uses: actions/cache@v4
with:
path: |
${{steps.haskell-setup.outputs.stack-path}}
Expand All @@ -22,19 +22,18 @@ jobs:
key: ${{ runner.os }}-${{ hashFiles('**/package.yaml') }}-${{ hashFiles('**/onix.cabal') }}-${{ hashFiles('**/stack.yaml.lock') }}
restore-keys: |
${{ runner.os }}-
- run: ls -lah ${{steps.haskell-setup.outputs.stack-path}}
- run: make test
e2e:
name: Test e2e
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
- name: Setup Node.js environment
uses: actions/setup-node@v2.1.4
uses: actions/setup-node@v4
with:
node-version: 12.x
- name: Restore caches
uses: actions/cache@v2
uses: actions/cache@v4
with:
path: |
~/.npm
Expand Down
159 changes: 159 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
# AGENTS.md

このリポジトリで作業するエージェント/コントリビュータ向けのガイド。
まず「このリポジトリの価値」を読み、それに反する変更をしないこと。

## このリポジトリの価値

onix-codegen は EDItEUR が配布する ONIX for Books の XSD スキーマから、
各言語向けのクライアントコードを自動生成するジェネレータ。価値は次の 2 点に集約される。

1. **ONIX 仕様への追従を、なるべく後方互換性を保ったまま行うこと。**
スキーマの Issue 更新・リリース更新に追従しつつ、既に生成済みのコードを使っている
利用者のビルドを壊さない。互換性を壊す変更は、壊さない選択肢が本当に無いときだけ。
2. **コード生成の仕組みで、サポートできる言語をなるべく増やすこと。**
言語固有の知識はテンプレート (`template/<language>/<version>/*.mustache`) に閉じ込め、
スキーマ解析側 (`src/`) は言語非依存に保つ。新しい言語の追加が「テンプレートを書くだけ」に
近づくほど、この設計は正しい方向にある。

判断に迷ったら、この 2 つのどちらをより良くするかで選ぶ。

## 全体のパイプライン

```
EDItEUR の zip (WORKSPACE の http_archive)
└─ bazel genrule (BUILD.bazel: onix_v2 / onix_v3)
└─ schema/v2, schema/v3 … .gitignore 済み。生成物なのでコミットしない
└─ src/Xsd/*.hs … XSD を Haskell の AST へパース (言語非依存)
└─ src/{Model,Code,Mixed}.hs
… AST を「生成用の中間表現」へ変換 (言語非依存)
└─ src/Lib.hs + mustache テンプレート
└─ generated/<language>/<version>/{model,code,mixed,reader}.<ext>
```

中間表現は `Util.GenSchema` クラス (`readSchema :: Schema -> a`) と
`Text.Mustache.ToMustache` インスタンスで表現される。テンプレートから見えるキーは
各 `toMustache` 実装がすべて。テンプレートに新しい情報が必要になったら、
まず `toMustache` にキーを足す。

- `Model` … 要素・属性の木 (`shortname` / `xmlReferenceName` / `typeName` / `optional` / `iterable`)
- `Code` … コードリスト (列挙値と説明)
- `Mixed` … mixed content を持つ要素

## セットアップとよく使うコマンド

前提: `stack` (GHC 8.8.3 / lts-16.27)、`node` (bazelisk を npm 経由で実行)、`go`。
スキーマ取得で editeur.org への外部ネットワークアクセスが必要。

```sh
npm install # bazelisk などを入れる
make schema # EDItEUR の zip を取得して schema/v2, schema/v3 に展開
make build # schema + stack build --fast
make test # stack test (HUnit) のみ。ネットワーク不要
make generated/go/v3 # v3 の Go コードを再生成 (ターゲット名の末尾がスキーマ版)
make generated/ts/v2 # v2 の TypeScript コードを再生成
npx bazelisk test //e2e/go:snapshot_test # 生成済み Go クライアントの e2e スナップショット
make debug # プロファイル付きで v3/go を生成 (例外の発生箇所を追うとき)
```

CI (`.github/workflows/test.yml`) は `make test` と `//e2e/go:snapshot_test` の 2 ジョブ。
この 2 つがローカルで通ることを、push 前に確認する。どちらもネットワークを必要としない。

`make schema` は現在 editeur.org から取得できない (202 が返る)。生成系を動かすには
手元に zip を用意する必要がある。事情と手順は `docs/adr/0003-editeur-schema-acquisition.md`。

既知の古さ: Makefile の `json` ターゲットは存在しない `run` に依存し、
import path も `go/helper` と古い (実体は `e2e/go`)。スナップショットの更新は
`//e2e/go:snapshot` の出力を `fixtures/20201200.json` に反映する形で行う。

## 後方互換性の守り方

生成コードの「公開 API」は、型名・フィールド名・XML/JSON タグ。ここが実質的な互換性境界。

- **生成物の diff を必ず読む。** `make generated/go/v2` 等を実行し、`git diff generated/` を確認する。
既存の型・フィールドの **削除やリネーム** が出ていたら、それは破壊的変更。意図した場合のみ、
PR 本文に理由と影響範囲を書く。追加のみの diff は基本的に安全。
- **スキーマの版はディレクトリで分ける。** 新しい Issue / リリースに対応するときは、
既存の `v2` / `v3` の出力を置き換えるのではなく、必要なら新しいバージョンとして足す。
- **未対応は黙って落とさない。** 扱えない構造に遭遇したら `Util.unimplemented`、
起こり得ないはずの分岐は `Util.unreachable` で、理由の文字列を付けて明示する。
暗黙にフィールドを落とすと、後方互換性の問題が静かに発生する。
- **`generated/` を手で編集しない。** 差分は必ずテンプレートか `src/` を直して再生成する。
- **`fixtures/20201200.json` は e2e のスナップショット。** ここが変わる = 生成物の
ランタイム挙動が変わっている。変更する場合は、それが意図した互換性変更か確認する。

### ONIX スキーマの版を上げる手順

1. `WORKSPACE` の `http_archive` (`org_editeur_v2` / `org_editeur_v3`) に URL と `sha256` を設定する。
既存の版を差し替えるのではなく、追従先を増やす方向を先に検討する。
2. `org_editeur_*.bazel` の `glob` と `BUILD.bazel` の `filegroup` (`onix2p1` / `onix3p0p7`) に
ファイル名を反映する。
3. `src/Lib.hs` の `schemaRoot` にあるルート XSD のパスを確認する。
4. `make test` と生成物の diff で、既存の型が消えていないことを確認する。

## 新しい言語を足す手順

言語非依存な `src/` には手を入れないのが理想。触る必要が出たら、それは中間表現に
情報が足りていないサインなので、言語別分岐ではなく中間表現の拡張として実装する。

1. `src/Lib.hs`
- `data Language` にコンストラクタを追加
- `ext` … 出力拡張子
- `template` … `template/<language>/<version>` のパス
- `generateTo` … `generated/<language>/<version>` のパス
2. `app/Main.hs` の `run` に `--language <name>` のパターンを追加する。
未対応の組み合わせは `unimplemented` を返す (v3 × typescript が既にその例)。
3. `template/<language>/<version>/` に 4 つの mustache を置く。
`model` / `code` / `mixed` / `reader` の 4 つは `Renderer` と 1 対 1 で、すべて必須。
`reader` だけはスキーマを受け取らない (`substitute t ()`) ので、静的なテンプレートでよい。
4. `generated/<language>/<version>/` を作り、生成物をコミットする。
5. `Makefile` に生成ターゲットを追加する (`generated/go/%` を参考に)。
6. 可能なら e2e を足す。`e2e/go` が手本: 生成クライアントで `fixtures/20201200.onix` を読み、
JSON にして `fixtures/20201200.json` と突き合わせる。言語をまたいで同じ
スナップショットに一致することが、生成器の正しさの一番強い証拠になる。
7. `README.md` の Current Status 表を更新する。

## テストの書き方

`test/` は HUnit。`test/Spec.hs` が `TestModel` / `TestParser` / `TestCode` / `TestMixed` を束ねる。

新しい XSD の構文や生成パターンに対応するときは、**まず `fixtures/test_*.xsd` に
最小の XSD を足す**。既存の fixture は 1 ファイル 1 論点で、命名は
`test_<領域>_<論点>.xsd` (`test_code_space_separated.xsd`、`test_model_iterable_choice.xsd` など)。
テスト側では `getSchema "./fixtures/test_xxx.xsd"` で読み、期待値を AST リテラルで書いて
`assertEqual` する。コードリストを参照する fixture は `*_codelists.xsd` を隣に置く慣習。

**fixture から `fixtures/` の外を include しないこと。** `Xsd.getSchema` は `xs:include` を
再帰的にたどってローカルパスを解決するので、`../schema/` を参照した瞬間、テストは
`make schema` のダウンロードに依存する。実際に `test_mixed_html.xsd` がそうなっていて、
テスト全体が editeur.org の可用性に縛られていた (ADR-0002)。必要な定義は
`test_mixed_html_xhtml_subset.xsd` のように、代替物を `fixtures/` 内に置く。

## コーディング規約

- Haskell は ormolu 相当の整形。既存ファイルのスタイル (import の並び、レコード記法) に合わせる。
- `package.yaml` の `library` は `-Wall -fwarn-incomplete-patterns -fwarn-incomplete-uni-patterns`。
警告を増やさない。パターンマッチは網羅するか、`unreachable` / `unimplemented` で明示的に落とす。
- 依存は `package.yaml` でバージョン固定。追加したら `stack.yaml.lock` の更新も確認する。
- モジュールの役割を混ぜない。XSD の形の話は `src/Xsd/`、中間表現は `Model`/`Code`/`Mixed`、
出力先や言語の対応表は `Lib`、CLI は `app/Main.hs`。

## 設計判断は ADR に残す

「なぜそうなっているか」は `docs/adr/` に ADR として記録する。この AGENTS.md は
「今どうすべきか」を書く場所、ADR は「どういう制約のもとにそう決めたか」を残す場所。

次のいずれかに影響する判断をしたら ADR を足す (`docs/adr/0000-template.md` が雛形)。

- **後方互換性**: 生成コードの公開 API に影響する判断、スキーマの版の扱い方
- **サポート言語の増やしやすさ**: 中間表現とテンプレートの責務分担、言語追加の手順
- **ビルドと CI の前提**: 外部依存 (EDItEUR の配布物、ツールチェーンのバージョン) の扱い方

判断が変わったときは既存の ADR を書き換えず、新しい ADR を書いて古いものを
`Superseded` にする。決定の履歴が消えないことが重要。

## 触らない / コミットしないもの

- `schema/` … `make schema` の生成物 (`.gitignore` 済み)
- `.stack-work/`, `bazel-*` … ビルド成果物
- `generated/` を手編集したもの … 必ず再生成した結果をコミットする
12 changes: 10 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
TS_FILES := $(shell find ./ -type f -name '*.ts' | grep -v 'node_modules')
HS_FILES := $(shell find ./ -type f -name '*.hs' | grep -v '.stack-work')
BZL := npx bazelisk
BZL_BIN := $(shell npx bazel info bazel-bin)
# Deferred on purpose: `:=` would run bazel on every make invocation, including
# `make test`, which runs in a job with no node_modules and no need for bazel.
BZL_BIN = $(shell $(BZL) info bazel-bin)

generated/go/%: build
stack exec onix-exe -- --schemaVersion $(@F) --language go
Expand All @@ -12,8 +14,14 @@ generated/ts/%: build
debug: build
stack exec --trace -- onix-exe +RTS -xc --RTS --schemaVersion v3 --language go

# The fixtures under fixtures/ are self-contained, so the tests deliberately do
# not depend on the `schema` target: `make schema` downloads the EDItEUR
# archives over the network, and requiring it here made the test suite
# unrunnable whenever editeur.org was unreachable. Keep fixtures from including
# anything outside fixtures/, or this dependency comes back.
# See docs/adr/0002-decouple-unit-tests-from-the-vendored-schema.md
.PHONY: test
test: schema
test:
stack test --trace --fast

.stack-work: $(HS_FILES) package.yaml stack.yaml
Expand Down
26 changes: 26 additions & 0 deletions docs/adr/0000-template.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# ADR-0000: タイトル

- **ステータス**: Proposed | Accepted | Superseded by ADR-XXXX
- **日付**: YYYY-MM-DD

## 背景

どういう状況で、何が問題になっているか。観測された事実 (エラーメッセージ、計測値、
外部サービスの挙動) を具体的に書く。

## 決定

何を決めたか。

## 理由

なぜその選択肢を選んだか。

## 検討した他の選択肢

- **案 A**: 内容と、採らなかった理由
- **案 B**: 内容と、採らなかった理由

## 結果

この決定によって何が変わるか。新しく発生する制約や、将来見直すべき条件も書く。
52 changes: 52 additions & 0 deletions docs/adr/0001-record-architecture-decisions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# ADR-0001: 設計判断を ADR として記録する

- **ステータス**: Accepted
- **日付**: 2026-09-13

## 背景

onix-codegen には、コードを読むだけでは意図が復元できない判断がいくつもある。例えば次のようなもの。

- スキーマの版を `v2` / `v3` というディレクトリで分け、既存の出力を置き換えない
- 言語固有の知識をテンプレートに閉じ込め、`src/` を言語非依存に保つ
- 対応していない XSD 構造は暗黙に無視せず `unimplemented` / `unreachable` で落とす

これらはいずれも「後方互換性を保ちながら ONIX 仕様に追従する」「サポート言語を増やしやすくする」という
このリポジトリの価値 (AGENTS.md) から導かれているが、根拠がコミットログに散っているため、
後から来た人が「なぜこうなっているのか」を再構成しづらい。結果として、良かれと思った変更が
その判断を静かに壊すことがある。

外部依存の扱いも同じ問題を抱えている。EDItEUR の配布物、Bazel、Stackage のスナップショットは
どれも外部の都合で壊れるが、「なぜこのバージョンに固定しているのか」が残っていないと、
更新のたびに同じ調査をやり直すことになる。

## 決定

設計判断を `docs/adr/` 配下の ADR として記録する。フォーマットは MADR を簡略化したもので、
`0000-template.md` を雛形とする。連番は一度振ったら変えず、判断が変わったときは既存の ADR を
書き換えるのではなく、新しい ADR を書いて古いものを `Superseded` にする。

AGENTS.md には、どういう判断を ADR にすべきかの基準を書き、両者を相互に参照させる。

## 理由

ADR は「決定そのもの」ではなく「決定に至った制約」を残せる点が、コメントやコミットメッセージより優れている。
このリポジトリで重要なのは、まさにその制約 (EDItEUR の配布形態、生成物の互換性、テンプレートの責務分担) が
時間とともに変わることなので、当時の前提ごと記録に残せる形式が要る。

Markdown をリポジトリ内に置くのは、スキーマや生成物と同じコミットで変更履歴が追えるため。
外部の Wiki やイシューに書くと、コードとの対応が切れる。

## 検討した他の選択肢

- **AGENTS.md / README.md に全部書く**: 判断の履歴が積み上がると読みものとして破綻する。
AGENTS.md は「今どうすべきか」を書く場所として保ち、「なぜそうなったか」は ADR に分ける。
- **GitHub Issues / Discussions に残す**: 検索性はあるが、リポジトリを clone しただけでは読めず、
コードとの対応も切れる。オフラインで完結しない。
- **記録しない (現状維持)**: 調査のやり直しと、意図しない互換性破壊が続く。

## 結果

- 新しい設計判断、および後から言語化された既知の判断は ADR に書く。
- ADR を追加したら `docs/adr/README.md` の一覧も更新する。
- ADR はレビュー対象になる。判断に異論があれば、実装ではなく ADR に対してコメントできる。
Loading
Loading