Skip to content

ドキュメントの無いコードで生成器が落ちるのを直す - #75

Open
kogai wants to merge 2 commits into
mainfrom
claude/code-dedupe-constraint
Open

kogai wants to merge 2 commits into
mainfrom
claude/code-dedupe-constraint

Conversation

@kogai

@kogai kogai commented Sep 14, 2026 •

Copy link
Copy Markdown
Owner

base は main。#72 のレビューで -Wx-partial の 5 箇所を個別に見てもらった結果、2 箇所が実際に落ちうると分かったので、その修正です。

訂正あり。 初版の本文には不正確な記述が 2 件ありました(警告の観測元と、「触っていないもの」の中身)。レビューで指摘を受け、このコメントで訂正したうえで本文も直しています。あわせて回帰テストを`` 1 ケース追加しました(f5d2b7e)。

同じラムダが 4 回書かれていて、2 つだけガードが無い

src/Code.hs には \(X.Enumeration v docs) -> Code {...} という同じ変換が 4 回あります。

箇所 空の docs の扱い
constraintToCode(L196) 第 1 等式で Enumeration v [] を処理 全域
L108 if not (null docs_) then head docs_ else "" 全域
L129 ガード無し 落ちる
L178 ガード無し 落ちる

落ちる方はこうなっています。

in Code {value = v, codeDescription = head docs_, notes = last docs_}

<xs:enumeration value="02" /> のようにドキュメントを持たない列挙値が 1 つでもこの経路に来ると、生成器が Prelude.head: empty list で落ちます。

空の docs が実在する証拠は、同じファイルの中にあります。 constraintToCode がわざわざ第 1 等式で Enumeration v [] を書き分けているのは、それが起こるからです。実際の生成物にも generated/go/v3/code.go の Dir に空文字列の case が存在します。同じ行の last docs_ も同罪ですが、-Wx-partial は last を報告しないので警告には出ません。

修正

3 つのインラインのラムダを、すべて constraintToCode の呼び出しに置き換えます。

-    codes =
-      map
-        ( \(X.Enumeration v docs) ->
-            let docs_ = map (\(X.Documentation d) -> d) docs
-                codeDescription = if not (null docs_) then head docs_ else ""
-                notes = if not (null docs_) then last docs_ else ""
-             in Code {value = v, codeDescription = codeDescription, notes = notes}
-        )
-        constraints
+    codes = map constraintToCode constraints

同じ式は既にこのファイルの 2 箇所で使われています(L221 の map constraintToCode (typeConstraints t) と L225 の map constraintToCode simpleRestrictionConstraints)。新しい書き方を持ち込んでいるのではなく、既にある書き方に寄せるだけです。

型も既存の使用箇所が保証しています(typeConstraints :: X.Type -> [X.Constraint]、X.Constraint の構築子は Enumeration Text [Annotation] のみなので map constraintToCode は全域)。

生成結果は変わりません

置き換えた各箇所について、修正前の出力は「⊥(クラッシュ)」か「修正後と同じ値」のどちらかにしかなりません。

  • ガード済みの L108 と constraintToCode は完全に等価です。docs = [] ならどちらも ("", "")、1 要素なら head == last でどちらも (d, d)、複数要素ならどちらも (head docs_, last docs_)。
  • L129 / L178 の差分は ⊥ → ("", "") のみです。

generated/ は現に存在してビルドも通っているので、修正前の出力は ⊥ ではありません。したがって修正後と同じです。

回帰テスト

落ちうる 2 箇所の両方にテストを付けました。フィクスチャはいずれも既存のものをコピーして、"02" の列挙から <xs:annotation> を取り除いただけです。

経路 フィクスチャ
L178(topLevelElementToCode) test_code_undocumented{,_codelists}.xsd
L129(topLevelTypeToCode の ListType 分岐) test_code_territorycodelist_undocumented{,_codelists}.xsd
assertEqual "codes without documentation do not crash the generator" expected actual
assertEqual "codes without documentation do not crash the list-type branch" expected actual

期待値は元のテストと同じ CodeType で、2 番目の Code だけが Code "02" "" "" になります。修正前はどちらも Prelude.head: empty list で落ちます。

触っていないもの

この PR の後、-Wx-partial の偽陽性として残るのは 2 箇所です(Code.hs:108 はこの PR で constraintToCode に置き換わって消えます)。

  • Code.hs:200 — constraintToCode 本体。第 1 等式が [] を処理しているが GHC には見通せない
  • Model.hs:150 — tail ys。findIndex p acc == Just i ⇒ i < length acc ⇒ splitAt i の ys は長さ ≥ 1

Util.unwrap(実質 fromJust)の無ガードな使用が Model.hs に 7 箇所・Code.hs に 2 箇所残っていますが、「無ガードの head / last を潰す」という単位が崩れるので別 PRにします。

警告の数について(観測元の注記)

-Wx-partial は GHC 9.8 で追加された警告です。main(この PR の base)は lts/16/27 = GHC 8.8.4 なので、この PR の CI ログには 1 行も出ません。

本文中の箇所の特定は、lts/23/25(GHC 9.8.4)に上げている #72 の CI ログで観測したものです(ログ 26 行 → 実際は 21 箇所、src/ が 5 / test/ が 16。src/ が倍になるのは stack.yaml の library-profiling: true で library が 2 way コンパイルされるため)。

この修正の妥当性は、その観測に依存していません(head が部分関数であることは警告の有無と無関係です)。

確認したこと

  • src/Xsd/Parser.hs の parseConstrains を読み、value 属性しか要求しないこと、annotation が無ければ parseAnnotations が [] を返すことを確認(したがって <xs:enumeration value="02" /> は Enumeration "02" [] になります)
  • フィクスチャ 4 本を xml.etree.ElementTree でパースして整形式であることを確認
  • 既存フィクスチャとの差分が schemaLocation の 1 行と "02" の annotation だけであることを diff で確認
  • CI で確認済み: 初版でテストケースが 33 → 34 に増えて全件通っています

🤖 Generated with Claude Code

https://claude.ai/code/session_01P8rZakwAiz1jpViUX34x1Z


Generated by Claude Code

src/Code.hs には `\(X.Enumeration v docs) -> Code {...}` という同じラムダが
4 回書かれていた。うち 1 つは `constraintToCode` という名前の付いた
トップレベル関数で、`Enumeration v []` を最初の等式で処理していて全域。
もう 1 つは `if not (null docs_)` でガードしてあり、これも全域。残る 2 つ
(Code.hs:129 と Code.hs:178) は **ガード無しで `head docs_` と
`last docs_` を呼んでいた**。

    in Code {value = v, codeDescription = head docs_, notes = last docs_}

`<xs:enumeration value="02" />` のようにドキュメントを持たない列挙値が
1 つでもこの経路に来ると、生成器が `Prelude.head: empty list` で落ちる。
空の docs が実在することは、同じファイルの `constraintToCode` がわざわざ
`Enumeration v []` を明示的に処理していることが示している。

3 つのインラインのラムダをすべて `constraintToCode` の呼び出しに置き換える。
同じ式は既にこのファイルの 2 箇所 (`map constraintToCode (typeConstraints t)`
と `map constraintToCode simpleRestrictionConstraints`) で使われている。

ガード済みのものと `constraintToCode` は完全に等価なので、現在パースできて
いるスキーマに対する生成結果は変わらない。変わるのは、これまで落ちていた
入力に対して `""` を返すようになる点だけ。

回帰テストを足す。fixtures/test_code_undocumented*.xsd は
test_code_description*.xsd のコピーで、"02" の列挙から `<xs:annotation>` を
取り除いただけ。この差分だけで、修正前は `head` が空リストで落ちる。
parseConstrains は `value` 属性しか要求せず、annotation が無ければ
parseAnnotations が `[]` を返すので、`Enumeration "02" []` になる。

-Wx-partial が src/ で報告する 5 箇所のうち、残る 3 箇所
(Code.hs:108 のガード済み、Code.hs:200 の `constraintToCode` 本体、
Model.hs:150 の `tail ys`) はいずれも偽陽性なので触っていない。

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

レビュー結果

マージを止める指摘(必須)はありません。 3 箇所の置き換えはいずれも等価で、回帰テストは修正前に確実に落ちることをコードを追って確認しました。以下は検証の内訳と、推奨/任意の指摘です。


検証したこと

1. 等価性 — 3 箇所すべて OK

X.Constraint の構築子は Enumeration Text [Annotation] のみ(src/Xsd/Types.hs:236-238)なので、map constraintToCode は全域です。

L108 / L109(ガード付き版)vs constraintToCode

docs 旧 L108-109 constraintToCode
[] null docs_ が真 → ("", "") 第 1 等式 Enumeration v [] → ("", "")
[d] (head [d], last [d]) = (d, d) 第 2 等式 → (head docs_, last docs_) = (d, d)
[d1..dn] (n≥2) (d1, dn) (d1, dn)

docs_ = map (\(X.Documentation d) -> d) docs は長さを保つので、第 1 等式が外れた時点で docs_ は非空。よって head/last は安全です。1 要素のとき head と last が同じ値を返す点も両者で一致し、description と notes に同じ文字列が入るという既存の挙動がそのまま保たれます(これ自体は仕様として怪しいですが、この PR は挙動を変えていないので問題なし)。

strictness も一致します。旧 L108 は null docs_ で、旧 L129/L178 は head docs_ で docs を WHNF に強制し、constraintToCode も第 1 等式のパターンマッチで同じく WHNF に強制します。

L129 / L178(無ガード版): docs ≠ [] では旧新とも (head docs_, last docs_) で完全一致。docs = [] では旧が ⊥、新が ("", "")。差分は ⊥ → ("", "") のみです。

2. 「生成結果は変わらない」 — 結論は正しく、本文より強い論証が立ちます

本文は「現在パースできているスキーマではこの経路に空の docs が来ていない」という経験的な前提に寄りかかっていますが、そこに依存する必要はありません。上の表から、任意のスキーマ S について

before-output(S) は ⊥ であるか、さもなくば after-output(S) と等しい

が成り立ちます。つまり 「これまで生成できていた入力の出力は、この PR では絶対に変わらない」 は前提抜きで言えます。変わりうるのは「これまで生成器が落ちていた入力」だけで、それは定義上 generated/ を生んでいません。

遅延評価の抜け穴(Code のフィールドは lazy なので head [] のサンクが forced されなければ落ちない)も塞がっています。template/go/{v2,v3}/code.mustache:60,62,79,82,85 は全コードについて {{&notes}} と {{description}} を必ず展開するので、空 docs がこの経路に届けば必ず forced されて必ずクラッシュします。加えて readSchema の uniq = S.toList . S.fromList(src/Util.hs:58-59)が Ord CodeType 経由で全 Text フィールドを強制します。

補足として、注釈なし enumeration が実在することの裏付けは generated/ の中にあります。 generated/go/v3/code.go:33049-33063 の Dir は

  // 
  case "ltr":
		*c = ``

となっており、fixtures/test_code_attribute_group_ref.xsd の <xs:enumeration value="ltr" />(annotation なし)がそのまま出力に現れています。ただしこれは既に constraintToCode を通る属性経路なので、L129/L178 に空 docs が届いていないこととは矛盾しません。("", "") というフォールバックがこのリポジトリの既存の出力と整合していることの証拠にもなっています。

→ git diff --exit-code generated/ を必須にはしません(下の「任意 1」に回します)。

3. 回帰テストは L178 を確かに通ります

fixtures/test_code_undocumented.xsd → topLevelElementToCode の経路を追いました。

  1. collectCodes(Code.hs:238-253)— AddresseeIDType は SimpleContentExtension で base="List44"、T.isPrefixOf "List" が真 → 通過。要素は 1 つなので head も安全。
  2. keyOfType(Code.hs:147-155)→ Just List44。Code.hs:164 で QName Nothing "List44" に正規化。
  3. ty(Code.hs:165)— include 先の codelists には targetNamespace が無いので simpleType name="List44" も QName Nothing "List44"。lookup 成功。
  4. typeConstraints t(Code.hs:86)— TypeSimple (AtomicType SimpleRestriction{..} _) なので [Enumeration "01" [Documentation ×2], Enumeration "02" []]。
  5. Code.hs:171-181 の codes_ に入り、Code.hs:178 の head docs_ が Enumeration "02" [] に対して呼ばれる。

修正前は assertEqual の == が Code "02" "" "" と比較する際に、value の一致を確認したあと codeDescription を強制するので、Prelude.head: empty list が HUnit の Error として上がります。L108 の経路ではなく、無ガードだった L178 を通っています。

この経路が実際に動くことは、同一構造の既存ケース(fixtures/test_code_description.xsd、期待値 description = "Name code type"、コード 2 件)が通っていることで裏付けられます。

4. フィクスチャの妥当性 — OK

  • src/Xsd/Parser.hs:219-225 の parseConstrains は theAttribute "value" しか要求せず、parseAnnotations e を無条件に呼びます。parseAnnotations(:483-493)は annotation 軸が空なら [] を返します。→ <xs:enumeration value="02" /> は Enumeration "02" [] で確定。
  • AGENTS.md の「fixture から fixtures/ の外を include しないこと」— schemaLocation="test_code_undocumented_codelists.xsd" の相対 include 1 本のみ。規約遵守。
  • 命名 test_<領域>_<論点>.xsd と *_codelists.xsd を隣に置く慣習も守られています。「1 ファイル 1 論点」も OK。

5. 残した 3 箇所は本当に偽陽性 — OK

  • Code.hs:200(constraintToCode 本体)— 第 1 等式が [] を捌いた後なので docs は非空、map は長さを保つので docs_ も非空。安全。
  • Model.hs:150 の tail ys — dropDuplicate(Model.hs:137-153)で findIndex p acc == Just i ⇒ 0 ≤ i < length acc。splitAt i acc は length ys == length acc - i ≥ 1 を保証するので ys は非空。安全。
  • Code.hs:108 — この PR で消えます(下の「推奨 3」参照)。

6. 他の無ガードな部分関数

src/ 全体を head / last / tail / init / fromJust / !! / read / error / undefined / maximum / minimum / foldr1 / foldl1 で洗いました。この PR が拾い損ねた無ガードは Prelude 由来には残っていません(src/ に残るのは上の 2 箇所の偽陽性のみ)。→「任意 2」に別クラスの残件を挙げます。


推奨

推奨 1. L129(ListType 経路)に回帰テストが無い

この PR は落ちる 2 箇所のうち 1 箇所(L178)にしか回帰テストを付けていません。Code.hs:129 は topLevelTypeToCode の ListType 分岐にあり、topLevelElementToCode とは別経路です。

しかも この経路は既にテストで踏まれています — fixtures/test_code_territorycodelist.xsd の <xs:simpleType name="TerritoryCodeList"><xs:list itemType="List1" /></xs:simpleType> が test/TestCode.hs:43 の topLevelTypeToCode scm . head . collectTypes から L129 に到達します。つまり test_code_territorycodelist_codelists.xsd の undocumented 版(test_code_undocumented_list.xsd + _codelists.xsd)を足して 1 ケース書けば、もう片方の落ちる箇所も「修正前に落ちる」ことで担保できます。今回のテストは L129 については何も言っていません。

推奨 2. 本文の -Wx-partial の記述が、この PR の base では成り立たない

箇所数の内訳自体は正しいことを確認しました。src/ に head 4 + tail 1 = 5、test/ に head/tail 計 16(TestCode.hs 7 + TestModel.hs 8 + TestParser.hs 1)で 21。stack.yaml の build.library-profiling: true で library が 2 way コンパイルされ、src の 5 箇所が二重計上されて 21 + 5 = 26 —— 算数は合っています。2 way コンパイルされること自体も、この PR の CI ログで [2 of 9] … [9 of 9] が 2 回出ていることから確認できます。

問題は base が main であることです。main の stack.yaml は lts/16/27(GHC 8.8.4)で、-Wx-partial は GHC 9.8 で追加された警告なので存在しません。実際、この PR の CI ログ(run 34818204905, job 103893469477)には警告が 1 行も出ていません(Installing library in .../8.8.4/lib/x86_64-linux-ghc-8.8.4/...)。21 箇所/26 行が観測されるのは lts/23/25 に上げている #71 → #72 の側です。

本文の「-Wx-partial が CI ログに 26 行出る」「-Wx-partial が src/ で報告する残り 3 箇所」は、この PR の CI では再現できません。メンテナが main の CI ログを見に行って「そんな警告は出ていない」となる導線なので、「これらの警告は lts-23 に上げた #71/#72 系列でのみ観測されるもので、base の main(GHC 8.8.4) では出ない」と一言添えることを推奨します。修正の妥当性自体は警告の有無に依存しないので、コードの変更は不要です。

推奨 3. 「触っていないもの」に Code.hs:108 が入っているのは自己矛盾

本文の「触っていないもの」節に Code.hs:108 が挙がっていますが、同じ行の説明が「この PR で constraintToCode に置き換わり、警告ごと消えます」となっており、実際には触っています。この PR 後に残るのは Code.hs:200(constraintToCode 本体の head)と Model.hs:150(tail)の 2 箇所です。リストから外すか、「消える 1 件 / 残る 2 件」と書き分けてください。


任意

任意 1. generated/ の再生成確認

上の「検証 2」のとおり論証は airtight なので必須にはしませんが、AGENTS.md は「生成物の diff を必ず読む」を明文で求めています。ローカルに EDItEUR の zip がある場合に限り、make generated/go/v2 / make generated/go/v3 / make generated/ts/v2 を回して git diff --exit-code generated/ が空であることを確認しておくと、記録として完璧になります。make schema は ADR-0003 のとおり editeur.org から取得できない(202)ので、手元に zip が無ければスキップで構いません。CI は make test(stack test --trace --fast)だけで生成器を走らせていないので、この確認は CI では代替できない点は本文に明記しておくと親切です。

任意 2. 同じクラスのバグが Util.unwrap 側に残っている

Prelude の部分関数は片付きましたが、このリポジトリ独自の unwrap :: Maybe a -> a(src/Util.hs:48-50、実質 fromJust。理由文字列なしの Unreachable を投げる)が同じクラスの「生成器が落ちる」を作ります。src/Model.hs:165,199,200,231,265,357,358 と src/Code.hs:118,183 の計 9 箇所。

特に Code.hs:118 は今回直した L129 の 11 行上、同じ ListType 分岐の中にあります。

t = (unwrap . M.lookup key . X.schemaTypes) scm   -- Code.hs:118 : 見つからなければ落ちる

対して兄弟関数 topLevelElementToCode は同じ lookup を Maybe のまま扱って Nothing -> [] で流しています(Code.hs:161-166, 171-182)。同じ「List 型の解決」が片方は落ち、片方は落ちない、という非対称です。Code.hs:183 の refname = unwrap $ findFixedOf "refname" ... も同様に、fixed="refname" を持たない要素で落ちます。AGENTS.md が求める「unreachable に理由の文字列を付けて明示する」の観点からも、別 PR で拾う価値があります。この PR のスコープ外です。

任意 3. description のフォールバックを "" のままにする判断は正しい

value を流用する案もありえますが、generated/go/v3/code.go の Dir が既に *c = ``` を出している以上、"" が既存の出力と整合します。valueにするとgenerated/が変わり、本文の「生成結果は変わりません」が崩れます。**現状維持が正解**です。念のため、生成コードのランタイム挙動としては「コードは受理するが空文字列を返す」になり、コード値そのものが失われる点だけ既知の性質として認識しておくとよいと思います(既存のDir` と同じなので新規の劣化ではありません)。

任意 4. L108 の経路には enumeration ありのテストが無い

topLevelTypeToCode の AtomicType 分岐(L92-112、置き換え対象の L108 を含む)を踏むテストは fixtures/test_code_dot_email.xsd の 1 件だけで、期待値が codes = V.fromList [] です。つまり map constraintToCode [] しか通っておらず、doc 抽出そのものは未カバーです。等価性は上で証明済みなので実害はありませんが、推奨 1 のフィクスチャを足すついでにここも埋まると安心感が増します。


まとめ

  • 必須: なし。 等価性・回帰テストの有効性・フィクスチャの妥当性・偽陽性判定は、いずれもコードを追って裏が取れました。
  • 推奨: 3 件。 L129 の回帰テスト欠落(1)、本文の -Wx-partial 記述が base=main では再現しないこと(2)、「触っていないもの」の自己矛盾(3)。
  • 任意: 4 件。

src/Code.hs が 25 行減って 3 行増える(git diff --numstat で確認)差分は、既に同ファイル内で 2 回使われている書き方に寄せるだけで、新しい概念を持ち込んでいません。良い変更だと思います。


Generated by Claude Code

レビュー指摘。落ちうる 2 箇所のうち、先の回帰テストは L178
(topLevelElementToCode の経路) しか通っていなかった。L129
(topLevelTypeToCode の ListType 分岐) は無防備なまま。

fixtures/test_code_territorycodelist*.xsd が既にその経路を踏んでいるので、
その undocumented 版を足して 1 ケースで塞ぐ。差分は "02" の列挙から
<xs:annotation> を取り除いただけ。

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

レビューありがとうございます。推奨 3 件すべて対応しました。

推奨 1: L129 側の回帰テストが無い → 足しました(f5d2b7e)

そのとおりでした。落ちうる 2 箇所のうち、先のテストは L178(topLevelElementToCode の経路)しか通っていませんでした。L129(topLevelTypeToCode の ListType 分岐)は無防備なままです。

ご提案どおり、fixtures/test_code_territorycodelist*.xsd が既にその経路を踏んでいるので、その undocumented 版を足しました。差分は同じく「"02" の列挙から <xs:annotation> を取り除いただけ」です。

scm <- getSchema "./fixtures/test_code_territorycodelist_undocumented.xsd"
let actual = (topLevelTypeToCode scm . head . collectTypes) scm
    ...
assertEqual "codes without documentation do not crash the list-type branch" expected actual

これで落ちうる 2 箇所の両方にテストが付きました。

推奨 2: 本文の -Wx-partial の記述が base=main では再現しない → そのとおり

確認しました。

ブランチ resolver GHC
main(この PR の base) lts/16/27 8.8.4
claude/test-warnings(#72 系列) lts/23/25 9.8.4

-Wx-partial は GHC 9.8 で追加された警告なので、この PR の CI ログには 1 行も出ていません。 本文に書いた「26 行 / 21 箇所」「library-profiling で src が二重計上」という内訳は、#72 の CI ログ(lts-23.25)で観測したものです。数字自体は正しいものの、どこで観測したかを書かずに載せたのは不正確でした。本文を直します。

なお、この PR の修正の妥当性はその観測に依存していません(head が部分関数であることは GHC の警告の有無と無関係)。

推奨 3: 「触っていないもの」に Code.hs:108 があるのは自己矛盾 → そのとおり

Code.hs:108 はこの PR で constraintToCode に置き換わって消えます。「触っていない」リストに入れたのは誤りでした。PR 後に残る偽陽性は Code.hs:200(constraintToCode 本体)と Model.hs:150(tail ys)の 2 箇所です。本文を直します。

任意 2: Util.unwrap(実質 fromJust)

これは別途対応します。 ご指摘の非対称が特に効いています。

  • Code.hs:118 は今回直した L129 の 11 行上、同じ分岐の中
  • 兄弟関数 topLevelElementToCode は同じ lookup を Maybe で流している

同じクラスのバグが Model.hs に 7 箇所・Code.hs に 2 箇所残るとのことなので、この PR に混ぜず(「無ガードの head / last を潰す」という単位が崩れるため)、課題として登録しました。unwrap は head と違って -Wx-partial が何も言わないので、洗い出しは手作業になります。

検証していただいた点について

等価性 3 箇所(1 要素で head == last になる点まで)、回帰テストが L178 に到達する経路、Model.hs:150 の findIndex p acc == Just i ⇒ i < length acc ⇒ ys が長さ ≥ 1、src/ に他の無ガード部分関数が無いこと — いずれも独立に裏を取っていただけて助かりました。

特に 「生成結果は変わらない」の論証を『before-output は ⊥ か after-output のどちらか』に整理していただいたのは、私の書き方(「現在パースできているスキーマでは空の docs が来ていない」という経験的前提)より強い形になっています。本文をその形に書き換えます。generated/go/v3/code.go:33049 の Dir に *c = ``` `` が実在するという指摘も、注釈なし enumeration が現実に存在することと ("", "") が既存出力と整合することの両方の裏付けになっていて有用でした。


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