Skip to content

ADR-0008: Go の reader が ONIX コードを説明文に置き換えるのをやめる (要判断) - #73

Open
kogai wants to merge 2 commits into
mainfrom
claude/adr-go-code-descriptions
Open

kogai wants to merge 2 commits into
mainfrom
claude/adr-go-code-descriptions

Conversation

@kogai

@kogai kogai commented Sep 14, 2026

Copy link
Copy Markdown
Owner

ADR のみの PR です。コード変更はありません。 base は main

大幅に書き直しました(8cfbd26)。 初版には事実誤認が 5 件あり、レビューで全件指摘されました。訂正の詳細はこのコメントにあります。以下は書き直し後の内容です。

生成された Go クライアントの UnmarshalXML は、ONIX のコード値を人間可読な説明文に置き換えて返します。この挙動を続けるかどうかを決めるための ADR です。

実測した数字

generated/go/{v2,v3}/code.go を機械的に集計しました。

v2 v3
コード型の数 117 209
case 節(コード値)の総数 6,314 7,688
説明文が他と衝突する case 72(19 型) 254(33 型)
default:error を返す型 117 209
case を 1 つも持たない型 9 45
MarshalXML の実装 0 0

一番大きい問題: v3 の Go クライアントは ONIX 3.0 をそもそも読めない

case を 1 つも持たない型の UnmarshalXML はこうなります。

func (c *DtDotNonEmptyString) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
	var v string
	d.DecodeElement(&v, &start)
	switch v {
	default:
		return fmt.Errorf("undefined code for DtDotNonEmptyString has been passed, got [%s]", v)
	}
}

どんな値が来ても必ず error を返します。 これらはコードリストではなく XSD のデータ型(dt:NonEmptyString など)なので、列挙値が無く case が 0 個になります。

属性としてしか使われない型なら UnmarshalXMLAttr の方が呼ばれるので実害はありません。問題は要素として使われているものです。

case 0 個の型が要素として使われているフィールド数
v2 2ReligiousTextID ×1、IntermediaryAvailabilityCode ×1)
v3 225(21 型)

筆頭の DtDotNonEmptyString152 フィールドで、ONIX 3.0 の必須要素を含みます。

IDValue DtDotNonEmptyString `xml:"b244"`

<b244> を含む ONIX 3.0 ファイルは、値が何であれ必ずパースに失敗します。 README の「Schema Version 3 with Codes Issue52: OK」は成り立っていません。e2e テストが generated/go/v2 しか import していないため、検出されていませんでした。

変換が単射でない — 金額が復元できなくなる

コード ONIX 側の注記(生成物のコメント全文) Go が返す値
AFA "Afghanistan. DEPRECATED, replaced by AFN" Afghani
AFN "Afghanistan (prices normally quoted as integers)" Afghani

AFA→AFN は 2002 年のデノミで 1,000:1Price100000、通貨が Afghani という値からは、それが 100 AFN 相当なのか 1 億 AFA 相当なのか区別できませんRUB/RUR も同型(1998 年、1,000:1)。言語コードでも hrv/scrCroatianscc/srpSerbian に潰れます。

BYR/BYNv2 でのみ衝突します(v3 は (Old) Belarussian Ruble と書き分け)。版によって壊れ方が違うこと自体が、この設計の脆さを示しています。

未知のコードでドキュメント全体が落ちる — ただし v3 のみ

default: 節は全型で error を返し、Read() がそれをそのまま返すので、1 つでも未知のコードがあればファイル全体が読めません。ただし版で意味が違います。

  • v2: 同梱の Issue 36 は EDItEUR が ONIX 2.1 向けに出した最後のコードリストです(Issue 37 以降は 2.1 用リストを含まない)。古いのではなく、これ以上新しくならない
  • v3: 同梱 Issue 52 に対し現行は Issue 74(2026-07-21)。22 版ぶんのコード値が未知になります。

ONIX として書き戻せない

MarshalXML はどこにもありません。置き換えをやめるだけでは直りきりません。 スペース区切りのコードリスト型(v2 で 5 型、v3 で 6 型)は []string が土台なので、go1.24.7 で実測すると:

CountryCodeList{"GB", "US"}  ->  <b090><b091>GB</b091><b091>US</b091></b090>

繰り返し要素になり、スペース区切りには戻りません。これらには独自の MarshalXML が要ります。

決定(案)

コード値をそのまま保持し、未知のコードはエラーにせず通す。説明文は Description() メソッドとして別途生成する(案 D)。

AGENTS.md の「既存の出力を置き換えず新しい版として足す」方針に沿う案 E(既存の generated/go は据え置き、新しい出力先を足す)も選択肢に挙げています。既存利用者を一切壊さない代わりに、壊れた成果物を配布し続けることになります。

なぜ実装を含めないか

generated/go/{v2,v3} は配布している成果物なので、返る値が変わるのは利用者にとって破壊的変更です。ADR-0003 と同じく、受理の判断はメンテナに委ねます。

確認したこと

  • 表の数字は generated/go/{v2,v3}/code.go を python でパースして実測(レビュアーが独立実装のパーサで再集計し、5 項目すべて一致を確認済み)
  • 「225 フィールド」は model.go の struct タグを ,attr の有無で分類して集計
  • MarshalXML の不在は generated/go/ 全体への grep
  • スペース区切り型の marshal 結果は go1.24.7 で実際に実行して確認
  • Issue 36 が ONIX 2.1 向け最後のコードリストである点は EDItEUR の告知で確認
  • デノミの比率は外部知識で、リポジトリ内の情報から導いたものではありません

🤖 Generated with Claude Code

https://claude.ai/code/session_01P8rZakwAiz1jpViUX34x1Z


Generated by Claude Code

生成された Go クライアントの UnmarshalXML は ONIX のコード値を説明文に
置き換えて返す。生成物を実測したところ、これが 3 つの問題を起こしている。

変換が単射でない。v2 では 117 型 6,314 個の case のうち 72 個、v3 では
209 型 7,688 個のうち 254 個が、他の case と同じ説明文を返す。最悪なのは
CurrencyCode で、BYR と BYN がともに `Belarussian Ruble`、AFA と AFN が
ともに `Afghani` になる。BYR/BYN は 2016 年のデノミで 10,000:1、AFA/AFN は
2002 年のデノミで 1,000:1 の関係にあるので、金額が復元できない。

未知のコードでドキュメント全体が落ちる。default 節が全型で error を返し、
Read() がそれをそのまま返す。同梱コードリストは v2 が Issue 36、v3 が
Issue 52 で、それより新しい Issue のコードを含む正当な ONIX ファイルは
読めない。「後方互換性を保って ONIX に追従する」という価値と衝突する。

ONIX として書き戻せない。MarshalXML は v2/v3 のどこにも実装がないので、
デコードした構造体を encoding/xml で書き出すと要素の中身が説明文になる。
e2e テストは JSON にしか書き出していないためこれを検出しない。

あわせて、TypeScript は同じジェネレータから生成されながらこの変換をせず、
同じファイルから Go は `Afghani`、TypeScript は `AFN` を返す。

この ADR は方針だけで実装を含まない。generated/go は配布物なので、返る値が
変わるのは利用者にとって破壊的変更であり、受理の判断はメンテナに委ねる。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P8rZakwAiz1jpViUX34x1Z

kogai commented Sep 14, 2026

Copy link
Copy Markdown
Owner Author

レビュー: ADR-0008

ADR と PR 本文の事実主張を、生成物の再パース(python)・実際の Go 実行(go1.24.7)・外部情報の確認で一つずつ突き合わせました。

結論から: 表の数字は全部合っています。 独立に書いたパーサで完全一致しました。一方で、表以外の本文に事実として誤っている記述が 5 つあり、うち 2 つは ADR の主張の柱(BYR/BYN の例と、同梱コードリストが古いという論拠)に関わるので、そのままでは受理判断の材料になりません。


必須

1. 「v2 が Issue 36 … いずれも実際の最新より古い」は v2 について誤り

WORKSPACE の同梱物は確かに v2 = Issue 36 / v3 = Issue 52 です。しかし:

  • Issue 36 は ONIX 2.1 に対応する最後のコードリストです。 Issue 37 以降は 2.1 専用リスト(7, 10, 78 など)を含まず、EDItEUR 自身が "ONIX 2.1 remains fully usable using Issue 36 of the codelists or earlier" と書いています。つまり v2 について「より新しい Issue のコードを含む正当な ONIX ファイル」は原理的に存在しません。ここは論拠として成立していません。
  • v3 側の主張は正しいです。 現行は Issue 74(2026-07-21 リリース)。コードリストは四半期改訂なので、Issue 52 は 22 版・約 5 年半遅れです。ADR が言う「版が上がるたびに壊れる」は v3 では現実の問題です。

→ 「いずれも」を落として v3 に限定してください。v2 について同じ懸念を残したいなら、「共有リスト(通貨 96 / 言語 74 / 国 91 など)に Issue 36 より後に追加された ISO コードを、2.1 送信者が実務上使ってくる」という別の書き方が要ります(これは主張として成り立ちますが、書かれている内容とは別物です)。

出典: Issue 74 / Issue 36 / Issue 38

2. BYR/BYN が衝突するのは v2 だけ。v3 では衝突しません

ADR と PR 本文の目玉の例ですが、生成物を直接読むと v2 と v3 で違います。

v2 code.go v3 code.go
BYR `Belarussian Ruble` `(Old) Belarussian Ruble`
BYN `Belarussian Ruble` `Belarussian Ruble`
衝突するか する しない

v3 の Issue 52 では EDItEUR が (Old) を付けて曖昧性を解消済みです。ADR / PR 本文は v2 / v3 を区別せずに書いているので、v3 についての事実誤認になります。

CurrencyCode の衝突組を全部出すとこうです(DefaultCurrencyCode も同一内容)。

  • v2(7 組 / 14 case): AFA/AFN, BGL/BGN, BYR/BYN, CSD/RSD, GHC/GHS, RUB/RUR, ZWD/ZWL
  • v3(7 組 / 14 case): AFA/AFN, BGL/BGN, CSD/RSD, GHC/GHS, RUB/RUR, VEB/VEF, ZWD/ZWL

→ 両版で成立する AFA/AFN を主例に。デノミの桁落ちを言いたいなら RUB/RUR(1998 年、1,000:1) も両版で衝突します。BYR/BYN を残すなら「v2 では」と明記してください。

3. PR 本文「ONIX 側の注記(生成物のコメントそのまま)」は、そのままではありません

BYR の実際のコメントは先頭が落ちています。

// v2
// Belarus (prices normally quoted as integers). Now replaced by new Belarussian Ruble (BYN): use only for historical prices that pre-date the introduction of the new Belarussian Ruble
case "BYR":

// v3
// Belarus (prices normally quoted as integers). Deprecated – now replaced by new Belarussian Ruble (BYN): use only for historical prices that pre-date the introduction of the new Belarussian Ruble
case "BYR":

Belarus (prices normally quoted as integers). が落ち、v3 の Deprecated – も入っていません。AFA / AFN / BYN の 3 行は正確でした。「そのまま」と書くなら本当にそのままにするか、「抜粋」と書き換えてください。

4. 「置き換えをやめれば encoding/xml の既定の挙動で正しく書き出せる」は spaceSeparatable 型で成立しません

実際に走らせました(go1.24.7)。

var ccl onix.CountryCodeList          // type CountryCodeList []string
xml.Unmarshal([]byte(`<b090>GB US</b090>`), &ccl)
xml.Marshal(&struct{ X onix.CountryCodeList `xml:"b090,omitempty"` }{ccl})
// => <b090>United Kingdom</b090><b090>United States</b090>

[]string フィールドは繰り返し要素として書かれます。ONIX はスペース区切りの単一要素を期待するので、説明文の置き換えをやめても(<b090>GB</b090><b090>US</b090> になるだけで)round-trip しません。該当は v2 で 5 型、v3 で 6 型(tmpeCodes 分岐)。

一方 hasElements な struct 型については主張どおりでした。属性は保たれ、本文だけが壊れています。

xml.Unmarshal(`<j152 textcase="02">AFN</j152>`) → Marshal
// => <j152 textcase="02">Afghani</j152>

→ 「結果」節の 「置き換えをやめれば encoding/xml の既定の挙動で正しく書き出せるようになる」 を、「struct 型は書き出せるようになるが、spaceSeparatable な型は MarshalXML が別途必要」に直してください。

5. ADR-0007 が main に存在せず、TypeScript reader の記述も main 時点では誤り

この PR の base は main で、maindocs/adr/ は 0001 / 0002 / 0003 だけです。ADR-0007 は未マージの claude/ts-reader-no-value-coercion にあります。ADR-0008 は 0007 を 2 箇所で参照しているので、このままマージするとリンク切れになります。

さらに中身の問題として、ADR-0008 の 「reader は値をそのまま返す」main では成立しません。maingenerated/typescript/v2/reader.tsfast-xml-parser 3.17.6 を既定設定で使っており、ADR-0007 自身が実測を残しています。

既定                  {"NotificationType":1,"ProductIDType":2,"IDValue":62124983,"Price":1200.5}
parseTagValue:false   {"NotificationType":"01",...,"IDValue":"062124983","Price":"1200.50"}

値を潰していないのは code.ts の型宣言のほう(116 型すべて = string — ここは確認できました)で、reader ではありません。"AFN" は数値に見えないので PR 本文の Go / TS 対比の例自体は成立しますが、一般化した記述は要修正です。

→ 0007 への参照を外して自己完結させるか、この PR を 0007 の後ろに積み直すか、どちらかを選んでください。


推奨

6. v3 の「254」は、意味のある衝突としては過大

254 という数字自体は再現しました(私の集計も 254 / 33 型)。ただし内訳を見ると質が揃っていません。

内訳 件数 中身
説明文が空文字列のもの 38 Align / Dir / Scope / Shape / TFrame / TRules / Type / Valign(XHTML 属性型)
地域コードの別綴り 136 PrizeRegion / RegionCodeCN-BJCN-11 など、同一地域の 2 通りの表記
実質的な衝突 約 80 通貨・言語・Proprietary な ID タイプなど

しかも上の 8 型は v3 の model.go / mixed.go / code.go のどこからもフィールドとして参照されていないデッドコードで、UnmarshalXML が呼ばれることはありません(8 型すべて、フィールド宣言としての出現が 0 件)。

なお v2 の 72 はすべて非空で、内訳も素直でした(通貨 14+14、言語 4×5、Proprietary 2×8 ほか)。

→ 「説明文が空文字列に潰れる型がある」のは (1) より強い欠陥なので、むしろ分けて書いたほうが主張が強くなります。今の書き方だと「254 のうち半分以上は中国の省コードの別名じゃないか」と返されて終わります。

7. もっと強い実例が生成物にあります。ADR がそれを使っていません

case が 1 つも無く default だけの型が v2 に 9 型、v3 に 45 型あります。そのうち要素として使われている型は、どんな値でも必ずエラーを返します

xml.Unmarshal([]byte(`<b376>01</b376>`), &r)
// => undefined code for ReligiousTextID has been passed, got [01]

xml.Unmarshal([]byte(`<b014>Paperback</b014>`), &d3)
// => undefined code for DtDotNonEmptyString has been passed, got [Paperback]
  • v2: ReligiousTextIDb376), IntermediaryAvailabilityCodej348
  • v3: DtDotNonEmptyString152 個の要素フィールドb014 / b029 / b030 / b031 / b036 / b045 …), DtDotStrictPositiveInteger(18), DtDotPositiveDecimal(8), DtDotEmailString(7), DtDotPositiveInteger(7), DtDotNonEmptyURI(4), URI(4) ほか計 21 型

つまり v3 の Go クライアントは「コードリストが古くて新しい値が読めない」以前に、<b014> を含む ONIX 3.0 をそもそも読めません。 (2) は「未知のコード」の問題として書かれていますが、実態は既知の正当な値でも落ちるです。原因はテンプレートの {{#hasCodes}} 分岐が、コードリストを持たない単純型(xs:string 系)にまで「switch して default でエラー」な UnmarshalXML を生成していることに見えます。

これを背景に入れると、メンテナが「これは直すべきだ」と判断するための材料として格段に強くなります。また (2) の直し方が「未知コードを通す」だけで足りるのかの検討にも直結します。

8. AGENTS.md が明示している回避策が「検討した他の選択肢」に入っていません

AGENTS.md「後方互換性の守り方」にこうあります。

スキーマの版はディレクトリで分ける。 新しい Issue / リリースに対応するときは、既存の v2 / v3 の出力を置き換えるのではなく、必要なら新しいバージョンとして足す。

この ADR は自分で「破壊的変更である」「メンテナ判断を要する」と言いながら、既存 generated/go/v2 を残して新しい出力先を並置する案を検討していません。リポジトリのガイドが真っ先に挙げている回避策なので、少なくとも案として立てて、採らない理由(生成物が 2 倍、テンプレート分岐が増える、など)を書くべきです。今の選択肢一覧で一番大きな欠落だと思います。

併せて挙げる価値がある案:

  • 案 E: (1) の直しと (2) の直しを分離する。 「未知のコードを通す」だけなら返る値は一切変わらず、エラーが消えるだけなので破壊的変更ではありません。これだけ先に実装 PR を出せば (2) は今日直ります。ADR 全体がメンテナ判断待ちでブロックされる必要があるのか、という問い自体を立て直せます。
  • 案 F: 説明文を map[string]string として生成する(案 D の変種)。 生成行数が減り、利用側から全件列挙もでき、Description() はそのマップを引くだけになります。

9. 「決定」が決め切れていません

決定節は「説明文は値を潰さない形で別の経路で提供する」とだけ書き、案 C / 案 D のどちらかを選んでいません(案 D に「現時点ではこれが有力」とあるだけ)。README は ADR を 「その時点で、どういう制約のもとに、なぜそう決めたか」を残すもの と定義しているので、

  • (a) 案 D に決めて、案 C を却下理由付きで落とす、か
  • (b) 決定の範囲を「置き換えをやめる/未知コードを通す」に限定し、説明文の提供方法は別 ADR に切る

のどちらかにするのがきれいです。

10. 案 C のコスト見積もりが不正確

生成物のサイズが説明文の分だけ増える(v2 の 30,342 行が更に伸びる)

説明文は既に case 本体に入っています(30,342 行という数字自体は正しいです)。案 C で増えるのは struct のフィールド 1 行 × 型数と、case あたり代入 1 行(v2 なら概ね +6,300 行)。「説明文の分」という言い方は誤りです。


任意

11. (2) の「ファイル全体が読めない」は正確だが、余地の書き方を足すと選択肢が見える

Read() の API としては主張どおりでした(実測)。ただし encoding/xml 自体は Decode 失敗時も先頭部分を格納して返します。

// 3 件中 2 件目が未知コード
xml.Unmarshal(doc, &msg)
// err = undefined code for ProductForm ... / len(msg.Products) == 1

捨てているのは Read()return nil, err です。「回復の余地がまったく無い」ではなく「Read() が捨てている」と書くと、strict / lenient を選べるようにする案が視野に入って良いと思います。

12. 番号 0008 は妥当です

0004(claude/drop-rules-nodejs)/ 0005(claude/fast-xml-parser-v5)/ 0006(claude/bzlmod-migration)/ 0007(claude/ts-reader-no-value-coercion)がいずれも未マージブランチ上に実在するので、README の 「番号は一度振ったら変えない」 に従えば 0008 で正しく、飛び番は問題ありません。ただしマージ順によっては main の索引が 0003 → 0008 と飛ぶので、索引に「0004–0007 は別 PR で進行中」と一行足しておくと親切です。

13. ステータス欄の書き方

Proposed(要判断 — 生成コードの公開 API を壊す変更を伴う) はテンプレートの Proposed | Accepted | Superseded by ADR-XXXX に括弧書きを足した形です。ADR-0003 も Accepted (2026-09-13, #58 にて承認) としているので前例はあります。そのままで構いません。

14. ADR にすべき判断か / 実装を切り離した判断について

どちらも妥当だと思います。

  • README の基準「後方互換性: 生成コードの公開 API に影響する判断」に該当します。AGENTS.md の「公開 API = 型名・フィールド名・XML/JSON タグ」の定義に返り値は厳密には入りませんが、同じ AGENTS.md が fixtures/20201200.json … ここが変わる = 生成物のランタイム挙動が変わっている」 と書いているので、ADR 化は筋が通っています。
  • 実装を含めずメンテナ判断に委ねたのも ADR-0003 と同じ運び方で妥当です。ただし 8-E のとおり「未知コードを通す」部分は非破壊なので、先行実装という選択肢は提示しておくと親切です。

検証できて、問題が無かった主張

ここは再現できました、と明記しておきます。

表の数字 — 全項目一致

独立に書いた python パーサ(func (c *T) UnmarshalXML ごとに関数本体を切り出し、case "..." とその直後の代入行を対応付ける方式)で、PR 本文・ADR の表と完全に一致しました。

v2 v3
コード型の数 117 209
case 節の総数 6,314 7,688
説明文が衝突する case 72(19 型) 254(33 型)
default: で error を返す型 117 209
MarshalXML 0 0

ご懸念だった取りこぼし/二重計上について:

  • 3 分岐すべて拾えています。 tmpeCodes = append(...)spaceSeparatable)と c.Body = hasElements)に加えて、*c = spaceSeparatable でも hasElements でもない分岐) も存在します。代入先の内訳は v2 が append 5 / c.Body 102 / *c 1 = 108 型、v3 が append 6 / c.Body 145 / *c 13 = 164 型。残る 9 型 / 45 型が case ゼロの型(→ 推奨 7)で、合計 117 / 209 になります。代入行を見つけられなかった case は 0 件でした。
  • 二重計上もありません。 同一型内で重複する case 値は 0 件(Go のコンパイラが弾くので当然ですが確認しました)。
  • MarshalXMLgenerated/ 全体の grep で 0 件。

その他

  • hrv/scrCroatianscc/srpSerbian: 正しい。v2 は LanguageCode / LanguageOfText / OriginalLanguage / DefaultLanguageOfText / LanguageList74 の 5 型、v3 は FromLanguage / ToLanguage を含む 5 型。
  • 未知コードのエラー伝播: 正しい。UnmarshalXML の error → decoder.Decode()Read()return nil, err まで、ネスト 2 段(ONIXMessage[]Product*ProductForm)を越えて伝播することを実測しました。encoding/xml に自動回復はありません。
  • Description の名前衝突なし: 正しい。^type Description\b / フィールド名 Description 単体 / func (...) Description( のいずれも v2・v3 の code.gomodel.go で 0 件。ProductFormDescription 等の接尾辞付きは多数ありますが衝突しません。案 D の前提は成立しています。
  • デノミの比率: 両方正しい。BYR → BYN は 2016-07-01 に 10,000:1、AFA → AFN は 2002 年に 1,000:1(北部同盟発行分のみ 2,000:1 という例外あり、公称は 1,000:1)。
  • Price 100000 が 10 BYN か 10 億 BYR か区別できない」: 算数も正しい(100000 BYR = 10 BYN、100000 BYN = 10 億 BYR、差は 10,000 倍)。ただし必須 2 のとおり v2 限定です。
  • TypeScript code.ts が全型 = string: 正しい。116 型すべて。
  • generated/go/v2/code.go が 30,342 行: 正しい。
  • d.DecodeElement(&v, &start) の戻り値が捨てられている(テンプレート 2 箇所 / 生成物は全コード型): 正しい。
  • テンプレート準拠: 0000-template.md の 背景 / 決定 / 理由 / 検討した他の選択肢 / 結果 の 5 節が揃っており、準拠しています。

問題の立て方(説明文への置換が単射でない、未知コードで全体が落ちる、書き戻せない)は的確で、生成物を実際に測って書いている点も良いと思います。必須の 5 点、特に 1(v2 の Issue 36 は最新)2(BYR/BYN は v2 限定) は ADR の論拠そのものなので、修正してから Accepted の判断に回すのが良さそうです。推奨 7 の「既知の値でも必ず落ちる型が v3 に 21 ある」は、むしろ ADR の主張を強くする材料だと思います。


Generated by Claude Code

レビューで 5 件の事実誤認を指摘され、いずれも裏を取って確認した。

1. 「v2 の Issue 36 も最新より古い」は誤り。Issue 36 は EDItEUR が ONIX 2.1
   向けに出した最後のコードリストで、Issue 37 以降は 2.1 用のリストを含まない。
   v2 側は古いのではなく、これ以上新しくならない。後方互換性の議論は v3
   (Issue 52 に対して現行 74) にのみ当てはまる。

2. BYR/BYN が衝突するのは v2 だけ。v3 は `(Old) Belarussian Ruble` と
   `Belarussian Ruble` に書き分けられている。ADR の目玉に据えていた例が v3 で
   成立しなかったので、両版で成立する AFA/AFN と RUB/RUR に差し替える。

3. 「生成物のコメントそのまま」と書いた引用が truncate されていた。全文に直す。

4. 「置き換えをやめれば encoding/xml の既定で正しく書き出せる」は、スペース
   区切りのコードリスト型 (v2 で 5 型、v3 で 6 型) では成立しない。go1.24.7 で
   実測したところ `CountryCodeList{"GB","US"}` は
   `<b090><b091>GB</b091><b091>US</b091></b090>` になる。独自の MarshalXML が要る。

5. ADR-0007 は未マージで main に無く、main の reader.ts は fast-xml-parser の
   既定で数値変換する。「TypeScript は値をそのまま返す」は成立しない。
   code.ts が全 116 型 `= string` である点だけが正しい。

あわせて、レビューが見つけたより強い実例を本文の先頭に据える。case を 1 つも
持たない型 (v2 に 9、v3 に 45) の UnmarshalXML は、どんな値でも必ず error を
返す。属性としてしか使われない型なら UnmarshalXMLAttr が呼ばれるので実害は
ないが、要素として使われているものは v3 に 225 フィールドある。筆頭の
DtDotNonEmptyString は 152 フィールドで、ONIX 3.0 の必須要素 <b244> を含む。
つまり v3 の Go クライアントは実在する ONIX 3.0 ファイルを読めない。
e2e が v2 しか通していないため検出されていなかった。

決定も具体化する。案 D (Description() メソッド) を選び、AGENTS.md の
「既存の出力を置き換えず新しい版として足す」に沿う案 E も選択肢に加えた。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P8rZakwAiz1jpViUX34x1Z

kogai commented Sep 14, 2026

Copy link
Copy Markdown
Owner Author

必須 5 件はすべてそのとおりでした。 一つずつ裏を取ったうえで ADR を書き直しました(8cfbd26)。実測の再現までしていただいて助かりました。

必須 1: 「v2 の Issue 36 も最新より古い」は誤り

EDItEUR の告知を確認しました。

Issue 36 of the Codelists will be the last issue compatible with 2.1, and future issues will contain only the lists for use with ONIX 3.0.

Issue 36 は ONIX 2.1 向けの最後のコードリストで、Issue 37 以降は 2.1 用のリストを含みません。v2 側は「古い」のではなくこれ以上新しくならない。後方互換性の議論が成り立つのは v3 だけ(Issue 52 に対して現行 74)です。ADR を版ごとに書き分けました。

必須 2: BYR/BYN が衝突するのは v2 だけ

確認しました。

v2 v3
BYR Belarussian Ruble (Old) Belarussian Ruble
BYN Belarussian Ruble Belarussian Ruble

ADR の目玉に据えていた例が v3 で成立していませんでした。 ご提案どおり、両版で衝突する AFA/AFN(2002 年、1,000:1)と RUB/RUR(1998 年、1,000:1)に差し替えました。BYR/BYN は「版によって壊れ方が違う」例として残しています。

必須 3: 引用が truncate されていた

そのとおりです。Belarus (prices normally quoted as integers). を落としたまま「生成物のコメントそのまま」と書いていました。差し替えた表では全文を載せています。

必須 4: スペース区切り型は置き換えをやめても書き戻せない

go1.24.7 で実測しました。

CountryCodeList{"GB", "US"}  ->  <b090><b091>GB</b091><b091>US</b091></b090>

繰り返し要素になり、スペース区切りには戻りません。 「置き換えをやめれば encoding/xml の既定で正しく書き出せる」は成立しないので、「これらの型には独自の MarshalXML が要る」に直しました(v2 で 5 型、v3 で 6 型)。

必須 5: ADR-0007 は未マージ、main の reader.ts は数値変換する

git ls-tree main docs/adr/ で 0000–0003 しか無いこと、mainreader.ts

const parsed = xml.parse(file.toString());

と fast-xml-parser の既定設定で呼んでいることを確認しました。「TypeScript は値をそのまま返す」は main では成立しません。 code.ts が全 116 型 = string である点だけが正しいので、そこだけを主張し、数値変換は ADR-0007 / #64 の話として切り離した旨を注記しました。

推奨: case ゼロの型 — これを本文の先頭に据えました

これが一番強い実例だというご指摘はそのとおりで、しかも実測したら想定より大きかったです。

case を 1 つも持たない型の UnmarshalXML は、どんな値でも必ず error を返します。属性としてしか使われない型なら UnmarshalXMLAttr が呼ばれるので実害はありませんが、要素として使われているものを数えました。

case 0 個の型が要素として使われているフィールド数
v2 2ReligiousTextID ×1、IntermediaryAvailabilityCode ×1)
v3 225(21 型)

筆頭は DtDotNonEmptyString152 フィールドで、その中には

IDValue DtDotNonEmptyString `xml:"b244"`

という ONIX 3.0 の必須要素が含まれます。<b244> を含むファイルは値が何であれ必ず落ちるので、v3 の Go クライアントは実在する ONIX 3.0 ファイルを読めません。 README の「Schema Version 3 with Codes Issue52: OK」は成り立っていません。

e2e が generated/go/v2 しか import していないため検出されていませんでした。「e2e に v3 を通すケースを足す」を「結果」節の対処項目に加えました。

推奨: 「決定」が案 C / D を決め切っていない、案 E が無い

どちらも直しました。案 D(Description() メソッド)を選択として明記し、AGENTS.md の「既存の出力を置き換えず新しい版として足す」方針に沿う案 E を選択肢に追加しました。案 E は既存利用者を一切壊さない代わりに、壊れた成果物を配布し続けることになるというトレードオフを併記しています。

推奨: v3 の 254 の内訳が不均質

ご指摘のとおりです(38 件はデッドコード型、136 件は中国省コードの別綴り、実質 80 件程度)。表の数字は残しつつ、議論の重心を衝突から case ゼロの方に移したので、この不均質さが結論を左右しなくなりました。


5 件のうち 3 件(1・2・5)は、別の版や別のブランチの状態を現在の main の状態だと思い込んでいたのが原因でした。v2 で確認したことを v3 でも確認する、未マージの自分の PR の内容を main の事実として書かない、という 2 点を以後徹底します。


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants