Conversation
生成された 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
レビュー: ADR-0008ADR と PR 本文の事実主張を、生成物の再パース(python)・実際の Go 実行(go1.24.7)・外部情報の確認で一つずつ突き合わせました。 結論から: 表の数字は全部合っています。 独立に書いたパーサで完全一致しました。一方で、表以外の本文に事実として誤っている記述が 5 つあり、うち 2 つは ADR の主張の柱(BYR/BYN の例と、同梱コードリストが古いという論拠)に関わるので、そのままでは受理判断の材料になりません。 必須1. 「v2 が Issue 36 … いずれも実際の最新より古い」は v2 について誤り
→ 「いずれも」を落として 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 で違います。
v3 の Issue 52 では EDItEUR が
→ 両版で成立する 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":
4. 「置き換えをやめれば
|
| 内訳 | 件数 | 中身 |
|---|---|---|
| 説明文が空文字列のもの | 38 | Align / Dir / Scope / Shape / TFrame / TRules / Type / Valign(XHTML 属性型) |
| 地域コードの別綴り | 136 | PrizeRegion / RegionCode の CN-BJ と CN-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:
ReligiousTextID(b376),IntermediaryAvailabilityCode(j348) - v3:
DtDotNonEmptyString(152 個の要素フィールド、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.Body102 /*c1 = 108 型、v3 が append 6 /c.Body145 /*c13 = 164 型。残る 9 型 / 45 型がcaseゼロの型(→ 推奨 7)で、合計 117 / 209 になります。代入行を見つけられなかったcaseは 0 件でした。 - 二重計上もありません。 同一型内で重複する
case値は 0 件(Go のコンパイラが弾くので当然ですが確認しました)。 MarshalXMLはgenerated/全体の grep で 0 件。
その他
hrv/scr→Croatian、scc/srp→Serbian: 正しい。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.go・model.goで 0 件。ProductFormDescription等の接尾辞付きは多数ありますが衝突しません。案 D の前提は成立しています。- デノミの比率: 両方正しい。BYR → BYN は 2016-07-01 に 10,000:1、AFA → AFN は 2002 年に 1,000:1(北部同盟発行分のみ 2,000:1 という例外あり、公称は 1,000:1)。
- 「
Price100000 が 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
|
必須 5 件はすべてそのとおりでした。 一つずつ裏を取ったうえで ADR を書き直しました( 必須 1: 「v2 の Issue 36 も最新より古い」は誤りEDItEUR の告知を確認しました。
Issue 36 は ONIX 2.1 向けの最後のコードリストで、Issue 37 以降は 2.1 用のリストを含みません。v2 側は「古い」のではなくこれ以上新しくならない。後方互換性の議論が成り立つのは v3 だけ(Issue 52 に対して現行 74)です。ADR を版ごとに書き分けました。 必須 2: BYR/BYN が衝突するのは v2 だけ確認しました。
ADR の目玉に据えていた例が v3 で成立していませんでした。 ご提案どおり、両版で衝突する 必須 3: 引用が truncate されていたそのとおりです。 必須 4: スペース区切り型は置き換えをやめても書き戻せないgo1.24.7 で実測しました。 繰り返し要素になり、スペース区切りには戻りません。 「置き換えをやめれば 必須 5: ADR-0007 は未マージ、main の reader.ts は数値変換する
const parsed = xml.parse(file.toString());と fast-xml-parser の既定設定で呼んでいることを確認しました。「TypeScript は値をそのまま返す」は main では成立しません。 推奨:
|
| 版 | case 0 個の型が要素として使われているフィールド数 |
|---|---|
| v2 | 2(ReligiousTextID ×1、IntermediaryAvailabilityCode ×1) |
| v3 | 225(21 型) |
筆頭は DtDotNonEmptyString の 152 フィールドで、その中には
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
ADR のみの PR です。コード変更はありません。 base は
main。生成された Go クライアントの
UnmarshalXMLは、ONIX のコード値を人間可読な説明文に置き換えて返します。この挙動を続けるかどうかを決めるための ADR です。実測した数字
generated/go/{v2,v3}/code.goを機械的に集計しました。case節(コード値)の総数casedefault:でerrorを返す型caseを 1 つも持たない型MarshalXMLの実装一番大きい問題: v3 の Go クライアントは ONIX 3.0 をそもそも読めない
caseを 1 つも持たない型のUnmarshalXMLはこうなります。どんな値が来ても必ず
errorを返します。 これらはコードリストではなく XSD のデータ型(dt:NonEmptyStringなど)なので、列挙値が無くcaseが 0 個になります。属性としてしか使われない型なら
UnmarshalXMLAttrの方が呼ばれるので実害はありません。問題は要素として使われているものです。case0 個の型が要素として使われているフィールド数ReligiousTextID×1、IntermediaryAvailabilityCode×1)筆頭の
DtDotNonEmptyStringは 152 フィールドで、ONIX 3.0 の必須要素を含みます。<b244>を含む ONIX 3.0 ファイルは、値が何であれ必ずパースに失敗します。 README の「Schema Version 3 with Codes Issue52: OK」は成り立っていません。e2e テストがgenerated/go/v2しか import していないため、検出されていませんでした。変換が単射でない — 金額が復元できなくなる
AFAAfghaniAFNAfghaniAFA→AFN は 2002 年のデノミで 1,000:1。
Priceが100000、通貨がAfghaniという値からは、それが 100 AFN 相当なのか 1 億 AFA 相当なのか区別できません。RUB/RURも同型(1998 年、1,000:1)。言語コードでもhrv/scrがCroatian、scc/srpがSerbianに潰れます。BYR/BYNは v2 でのみ衝突します(v3 は(Old) Belarussian Rubleと書き分け)。版によって壊れ方が違うこと自体が、この設計の脆さを示しています。未知のコードでドキュメント全体が落ちる — ただし v3 のみ
default:節は全型でerrorを返し、Read()がそれをそのまま返すので、1 つでも未知のコードがあればファイル全体が読めません。ただし版で意味が違います。ONIX として書き戻せない
MarshalXMLはどこにもありません。置き換えをやめるだけでは直りきりません。 スペース区切りのコードリスト型(v2 で 5 型、v3 で 6 型)は[]stringが土台なので、go1.24.7 で実測すると:繰り返し要素になり、スペース区切りには戻りません。これらには独自の
MarshalXMLが要ります。決定(案)
コード値をそのまま保持し、未知のコードはエラーにせず通す。説明文は
Description()メソッドとして別途生成する(案 D)。AGENTS.md の「既存の出力を置き換えず新しい版として足す」方針に沿う案 E(既存の
generated/goは据え置き、新しい出力先を足す)も選択肢に挙げています。既存利用者を一切壊さない代わりに、壊れた成果物を配布し続けることになります。なぜ実装を含めないか
generated/go/{v2,v3}は配布している成果物なので、返る値が変わるのは利用者にとって破壊的変更です。ADR-0003 と同じく、受理の判断はメンテナに委ねます。確認したこと
generated/go/{v2,v3}/code.goを python でパースして実測(レビュアーが独立実装のパーサで再集計し、5 項目すべて一致を確認済み)model.goの struct タグを,attrの有無で分類して集計MarshalXMLの不在はgenerated/go/全体への grep🤖 Generated with Claude Code
https://claude.ai/code/session_01P8rZakwAiz1jpViUX34x1Z
Generated by Claude Code