Skip to content

Move to lts-23.25 (GHC 9.8.4) and stop pinning dependency versions - #65

Open
kogai wants to merge 2 commits into
claude/drop-unused-haskell-depsfrom
claude/haskell-resolver-lts23
Open

kogai wants to merge 2 commits into
claude/drop-unused-haskell-depsfrom
claude/haskell-resolver-lts23

Conversation

@kogai

@kogai kogai commented Sep 13, 2026

Copy link
Copy Markdown
Owner

Haskell 側のツールチェーンは 2020 年の lts-16.27 / GHC 8.8.3 で止まっていました。base は #60(未使用依存の削除)。

変更前 変更後
resolver lts-16.27 lts-23.25
GHC 8.8.3 9.8.4
stack (CI) 2.5.1 latest

固定バージョンをやめます

全依存に == x.y.z の完全固定が付いていましたが、これはスナップショットと喧嘩します。更新のたびに手で書き直す必要があり、スナップショットと食い違えばそれを回避する仕掛けが要ります(実際 allow-newer: true が入っていました)。固定を外し、スナップショットに任せます。base の範囲指定だけ残しています。

lts-23.25 に必要なパッケージが揃っていることは確認済みです。

mustache 2.4.3.1 / yaml 0.11.11.2 / vector 0.13.2.0 / xml-conduit 1.9.1.4
http-client 0.7.19 / http-client-tls 0.3.6.4 / network-uri 2.6.4.2
optparse-applicative 0.18.1.0 / HUnit 1.6.2.0

text parsec containers transformers bytestring filepath は GHC 同梱です。

ついでに落ちるもの

extra-deps が丸ごと不要になりました。

  • HUnit-1.6.1.0 — スナップショットが 1.6.2.0 を提供します
  • typeable/xsd-parser の git 依存 — どこからも使われていません。 どの build-depends にも現れず、このリポジトリは自前の src/Xsd/ を持っています。つまり stack は、ビルドされることのないパッケージのために 4.8 MB のツリーを解決していました

allow-newer: true も削除。 バージョン境界のチェックを全体で無効化する設定で、5 年前のスナップショットに完全固定を組み合わせていた事情なら分かりますが、スナップショットに任せる以上、境界違反を隠すのは逆効果です。

stack.yaml.lock を手で書いている点

この環境では stack を実行できません(haskell.org が egress ポリシーでブロック)。そこで手法を先に検証しました。既存 lock の lts-16.27 のエントリを自分で再計算し、コミット済みの値と完全一致することを確認してから、新しい値を求めています。

lts-16.27  size=533252  sha256=c2aaae52beeacf6a5727c1010f50e89d03869abfab6d2c2658ade9da8ed50c73
committed: size=533252  sha256=c2aaae52beeacf6a5727c1010f50e89d03869abfab6d2c2658ade9da8ed50c73   ← 一致
lts-23.25  size=684278  sha256=a3501d1b61a539371d85305fe73e1134fc08e7c97498a42952455f011fd97ecf

onix.cabal も同様に手で更新し、hpack のハッシュを再計算しています(#60 で検証済みの方法)。

確認

  • lts-23.25 に全依存が存在することをスナップショット YAML で確認
  • lock の生成方法を既存値の再現で検証
  • onix.cabal のハッシュ自己整合性を確認
  • ビルドは未検証。 GHC 8.8.3 → 9.8.4 は 5 世代分の跳躍で、ソース側の非互換が出る可能性があります。この PR の CI が唯一の検証手段で、落ちたら追って直します

🤖 Generated with Claude Code

https://claude.ai/code/session_01P8rZakwAiz1jpViUX34x1Z


Generated by Claude Code

The snapshot was lts-16.27, GHC 8.8.3, from 2020.

Every dependency also carried an exact `== x.y.z` pin, which fights the
snapshot: the versions have to be restated by hand on every bump, and any
disagreement with the snapshot has to be papered over. Drop them and let
the snapshot decide. base keeps its range. Checked that every non-boot
dependency this package uses is in lts-23.25: mustache 2.4.3.1,
yaml 0.11.11.2, vector 0.13.2.0, xml-conduit 1.9.1.4,
http-client 0.7.19, http-client-tls 0.3.6.4, network-uri 2.6.4.2,
optparse-applicative 0.18.1.0, HUnit 1.6.2.0. text, parsec, containers,
transformers, bytestring and filepath ship with GHC.

That makes two other things in stack.yaml unnecessary:

- extra-deps pinned HUnit-1.6.1.0, which the snapshot now supplies, and a
  git checkout of typeable/xsd-parser that nothing depends on — it is
  absent from every build-depends list, so stack was resolving a 4.8 MB
  tree for a package that is never built. This repository has its own
  src/Xsd.
- allow-newer: true, which disables version-bound checking globally.
  Pinned versions against a five-year-old snapshot probably needed it;
  with the snapshot in charge, hiding bound violations is the opposite of
  what we want.

CI moves to GHC 9.8.4 to match, with stack "latest" — the snapshot is
what pins the build, so pinning the tool as well only adds a number to
maintain.

stack.yaml.lock is written by hand rather than regenerated, since stack
cannot run here (haskell.org is blocked by egress policy). The method was
checked first by recomputing the existing lock's snapshot entry from
lts-16.27 and getting byte-identical values (size 533252,
sha256 c2aaae52…) before computing the new one.

onix.cabal is hand-updated to match, hash refreshed the same way as
before.

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

総評

移行そのものは妥当で、検証可能な主張はすべて独立に再現して一致しました(lock の size/sha256、hpack ハッシュ、lts-23.25 のパッケージ在庫、xsd-parser が本当に未使用であること)。== 固定を外した判断も、この構成では 再現性を落としていません(後述)。一方で緑の CI が保証しているのは「GHC 9.8.4 でコンパイルが通ること」と「XSD パース層の 33 ケースが通ること」までです。コード生成そのもの(stack exec onix-exe)はどちらのジョブでも一度も走っていません — Test e2e は //generated/go/v2:go(コミット済みの生成物)に対して Go バイナリを組むだけで、Haskell 側には触れていません(34 秒で完了)。そしてテストスイートは Text.Mustache も Lib も import していないため、テンプレート置換の経路は完全に未テストです。mustache 2.3.1 → 2.4.3.1 と xml-conduit 1.9.0.0 → 1.9.1.4 はまさにその未検証の経路にあるので、マージ前に一度生成して generated/ を diff することだけは踏んでほしい、というのが結論です。それ以外は運用上の推奨に留まります。

== 固定を外した件についての見解(所見・修正要求ではありません)

失うものはありません。むしろ再現性は上がっています。 根拠:

  1. 外した 13 個のうち 6 個(text containers bytestring filepath parsec transformers)は GHC の boot ライブラリです。lts-23.25 の YAML はこれらを一切列挙しておらず、GHC 9.8.4 が供給します。つまり text == 1.2.4.0 を残すことは lts-23.25 では成立しません。バージョンはスナップショットの resolver.compiler: ghc-9.8.4 経由で一意に決まります(stackage の global-hints で確認: base 4.19.2.0 / text 2.1.1 / containers 0.6.8 / bytestring 0.12.1.0 / filepath 1.4.301.0 / parsec 3.1.17.0 / transformers 0.6.1.0)。
  2. 残り 7 個はスナップショットに入っており、スナップショットは hackage: mustache-2.4.3.1@sha256:...,<size> の形で バージョンだけでなく cabal ファイルのリビジョンまで固定します。== 2.3.1 はリビジョンを固定しないので、固定としては固定を外した今のほうが厳密です。
  3. lock がそのスナップショットを内容ハッシュで固定しています。したがって「resolver + lockfile」で pin としては閉じています。

失うものがあるとすれば、cabal ファイルが「テスト済みのバージョン範囲」を一切記録しなくなった点だけです(後述の任意 7)。ただしこのリポジトリは Hackage に上げず、Makefile も CI も stack 経由でしかビルドしないので、実害はありません。

allow-newer: true の削除も問題ありません。境界を持つ依存は base >=4.7 && <5 のみで base-4.19.2.0 は適合、他の build-depends は全て無境界、スナップショット内のパッケージ群は Stackage の構成上相互に整合しています。境界違反が起きうる箇所は存在しません。むしろ境界チェックが復活するので改善です。


必須

1. xml-conduit 1.9.1 で entity 展開の上限がデフォルトで有効になった — src/Xsd/Parser.hs:539

parseFile path = parse defaultConfig <$> Text.XML.readFile def {psRetainNamespaces = True} path

def を使っているため、xml-conduit の ParseSettings のデフォルト変更をそのまま被ります。1.9.0.0 → 1.9.1.4 の間に入った変更:

  • 1.9.1: ParseSettings に psEntityExpansionSizeLimit が追加され、デフォルト 8192 文字。entity 展開ループの検出も追加。
  • 1.9.1.1: パラメータ entity 宣言は無視されるようになり、不正な entity 宣言はパースエラーになるように変更。

なぜ問題か: getSchema が読むのは EDItEUR の実スキーマで、これは schema/ 配下に bazel が実行時に取得するもので リポジトリには入っていません。fixtures/ 配下の XSD には <!ENTITY が 1 つも含まれていないため(git grep -c ENTITY = 0 件)、実スキーマが内部 entity を使っていてもテストスイートは検知できません。そして生成パスは CI で走りません。パースは通る/通らないの二値なので、通らなければ make generated/go/v3 が例外で落ちます。

修正: 下の 2 と同じ手当てで済みます。実スキーマで一度パースを通し、もし entity が原因で落ちるなら def {psEntityExpansionSizeLimit = ...} を明示してください。なお parseLazyByteString(src/Xsd/Parser.hs:544)も同じ def を使っています。

(egress ポリシーで editeur.org に到達できないため、実スキーマが entity を使っているかどうかは私の側では確認できませんでした。)

2. generated/ の再生成と差分確認がどこでも行われていない

  • .github/workflows/test.yml の test ジョブ: make test → stack test --trace --fast。生成はしません。
  • e2e ジョブ: //e2e/go:snapshot_test は e2e/go/BUILD.bazel を見るとおり //generated/go/v2:go(コミット済みの Go コード)を deps にした helper を走らせ、その JSON 出力を fixtures/20201200.json と比較するだけです。Haskell のジェネレータは呼びません。
  • テストスイートは Text.Mustache / Lib を import していません(git grep -nE 'Mustache|substitute|import Lib' -- test/ は 0 件)。つまり mustache 2.3.1 → 2.4.3.1 の挙動差はどこにも当たっていません。

なぜ問題か: このリポジトリの成果物は generated/ です。GHC 9.8.4 でコンパイルが通ることと、同じ入力から同じ generated/ が出ることは別問題で、後者を確認する仕組みがありません。差分が出た場合、次に誰かが再生成したときに無関係な巨大 diff として表面化します。

修正: マージ前にローカルで一度

make schema && make generated/go/v2 && make generated/go/v3
git diff --exit-code generated/

を通し、結果を PR に書いてください。恒久対策としては CI の test ジョブの末尾に git diff --exit-code を足すのが確実ですが、make schema が editeur.org に依存する(ADR 0002 の経緯)ので、そこは別 PR でもよいと思います。


推奨

3. stack-version: "latest" — .github/workflows/test.yml:14

この PR は「resolver と lock で固定する」方向の変更なのに、それらを解釈するツール自体の固定を外しています。resolver がカバーしないのはまさにこの次元です。

具体的に効いてくる箇所:

  • stack は毎ビルド hpack を走らせ、lock ファイルの読み書きと再生成判定も行います(下の 4 と任意 5)。この挙動はバージョンで変わります。
  • キャッシュキーが ${{ runner.os }}-<package.yaml>-<onix.cabal>-<stack.yaml.lock> で、stack / GHC のバージョンを含んでいません。加えて restore-keys: Linux- で任意の過去キャッシュにフォールバックするので、別バージョンの stack が書いた ~/.stack を新しい stack が読む構図になります。実際この run のキャッシュは 544 MB あります。
  • stack 3.x は resolver: を受け付けますが正式表記は snapshot: に移っています(任意 6)。将来の major で落ちれば、リポジトリ側を一切変えていないのに CI が壊れます。

修正: この run が実際に使ったバージョンに固定してください(例 stack-version: "3.3.1")。renovate は config:base のみなので stack のバージョンは自動追従しません。手動更新になりますが、年 1 回程度で済みます。合わせてキャッシュキーに GHC / stack バージョンを含めると安全です。

4. hpack のバージョンずれが onix.cabal を静かに書き換えうる — onix.cabal:3

onix.cabal は今も -- This file has been generated from package.yaml by hpack version 0.33.0. のままですが、CI は stack latest(3.x、同梱 hpack はおよそ 0.38)になりました。stack はプロジェクトに package.yaml があると毎回 hpack を NoForce で実行します。hpack 0.33.0 の src/Hpack.hs:158-166 の mkStatus を読むと判定は次の通りです:

(Just oldVersion, Just hash)
  | old == new       -> OutputUnchanged
  | v < oldVersion   -> AlreadyGeneratedByNewerHpack
  | sha256 (unlines old) /= hash -> ExistingCabalFileWasModifiedManually
  | otherwise        -> Generated        -- ← 上書きする

ハッシュは一致する(下で再計算済み)ので「手で編集された」判定にはならず、oldVersion 0.33.0 < 0.38 なので、新しい hpack のレンダリング結果が 1 文字でも違えば Generated になり onix.cabal が上書きされます。CI は作業ツリーの汚れを一切チェックしていないので、書き換わっても緑のままです。

なぜ問題か: コミット済みの onix.cabal が「ビルドで実際に使われているもの」と乖離し得ます。キャッシュキーに hashFiles('**/onix.cabal') が入っているので、乖離するとキャッシュの当たり方も不安定になります。

修正: 同梱 hpack で onix.cabal を再生成してコミットし直すのが最短です。恒久的には 2 と同じ git diff --exit-code が効きます。

(0.38 が 0.33.0 と実際に違う出力を出すかは、この環境に Haskell ツールチェーンが無く、CI ログの先頭部分も blob ストレージが egress で塞がれていて取得できなかったため、確認できていません。仕組みとして起こりうる、というところまでです。)


任意

5. lock ファイルのキー順とヘッダ URL — stack.yaml.lock:4, 9-11

  • completed 内のキー順が size / url / sha256 になっていますが、これは lts-16.27 のエントリ(stack 2.5.1 + aeson 1.x 時代)を踏襲したものです。現行 stack が書き出す順とは違う可能性があります。ただし実害はありません: Stack.Lock.lockCachedWanted の書き戻し判定は when (newLocked /= locked) で、テキストではなく パース後の Locked 値を比較しています。値は一致するので stack はこのファイルを書き換えません。
  • ヘッダの参照 URL が https://docs.haskellstack.org/en/stable/lock_files のままです。stack 3.3.1 は .../en/stable/topics/lock_files を書きます。将来何かの拍子に再生成されるとこの行も変わります。

6. resolver: → snapshot: — stack.yaml:20

stack 3.3.1 の Stack/Types/ProjectAndConfigMonoid.hs:36 は o ...: ["snapshot", "resolver"] なので resolver: は今も通ります。ただし現行の正式表記は snapshot: です。resolver を触る PR なので、ついでに改名しておくと 3 の将来リスクが 1 つ減ります。

7. cabal 側の上限境界が完全に消えた — onix.cabal:44-57, 87-93

残っているのは base >=4.7 && <5 だけになりました。stack 経由なら何も問題ありませんが、cabal build 単体だと完全に無制約になります。またこの base >=4.7 は 8.8.3 時代から据え置きで、実際にテストされている下限(4.19.2.0)を表していません。stack 専用運用を前提にするなら、package.yaml にその旨をコメントで 1 行残しておくと、次に固定を戻そうとする人の手間が省けます。


検証したこと

lock の snapshot エントリ(一致)

$ curl -sSL -o lts2325.yaml https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/23/25.yaml
$ wc -c lts2325.yaml && sha256sum lts2325.yaml
684278 lts2325.yaml
a3501d1b61a539371d85305fe73e1134fc08e7c97498a42952455f011fd97ecf  lts2325.yaml

コミット値(stack.yaml.lock:9-11)と完全一致。末尾の resolver: compiler: ghc-9.8.4 も確認しました。

packages: [] が正しいこと

Stack.Lock の pkgImmutableLocations = lockLocations (pliCompleted <> prjCompleted)。extra-deps がゼロで、packages: [.] はローカルの mutable パッケージなので immutable location は 1 件も生まれません。よって [] が正解です。上の任意 5 の通り、stack はこのファイルを書き戻しません。

hpack ハッシュの自己整合性(一致)

Hpack.hs:184 の sha256 withoutHeader、withoutHeader = cabalVersion ++ body、header は 6 行 = 3〜8 行目。つまり 1〜2 行目 + 9 行目以降。

origin/claude/haskell-resolver-lts23   declared 07b4003d...ffd5  computed 07b4003d...ffd5   ← 一致
origin/claude/drop-unused-haskell-deps declared ccd13791...605c  computed ccd13791...605c   ← 一致(手法の裏取り)

package.yaml の依存リストと onix.cabal の build-depends(base 追加 + アルファベット順)も突き合わせて一致を確認しました。

依存の在庫

lts-23.25 の YAML に HUnit 1.6.2.0 / mustache 2.4.3.1 / yaml 0.11.11.2 / vector 0.13.2.0 / xml-conduit 1.9.1.4 / http-client 0.7.19 / http-client-tls 0.3.6.4 / network-uri 2.6.4.2 / optparse-applicative 0.18.1.0 を確認。text containers bytestring filepath parsec transformers は列挙なし = boot ライブラリ、で PR の記述通りです。

xsd-parser が未使用であること

  • head で git grep -i xsd-parser → 0 件。base ブランチでは stack.yaml と stack.yaml.lock にしか現れず、どの build-depends にも入っていません。
  • src/, app/, test/ の全 import を列挙して確認。Xsd.Parser / Xsd.Types / Xsd は onix.cabal の exposed-modules にある自前モジュールです。

Ord Text / Map の順序 — 変化なし(問題なし)

生成物の並び順は M.toList に依存します(src/Code.hs:241,258,271 / src/Mixed.hs:55,66 / src/Model.hs:100 / src/Xsd/Parser.hs:698。キーはいずれも X.QName か Text.XML.Name で、どちらも Text 上の derived Ord)。Util.uniq = S.toList . S.fromList(src/Util.hs:59)も同様です。text 1.2.4.0 の compareText(Data/Text.hs:449、iter でコードポイントに復号して比較)と text 2.1.1 の compareText(src/Data/Text.hs:464、UTF-8 バイト列比較 ≡ コードポイント順)を読み比べた結果、両者は同じ順序です。UTF-16 → UTF-8 移行による並び替えは起きません。

mustache の Null 挙動変更 — 影響なし(問題なし)

mustache 2.4.2 の "Also treat Null as falsey in inverted sections" が該当しないか確認しました。{{^...}} で使われているキーは hasElements spaceSeparatable is_tag iterable optional の 5 つですが、すべて Bool を束ねています(src/Code.hs:64,67 / src/Model.hs:73-75)。唯一の Maybe フィールドである Model.typeName は Nothing のときキー自体を object から外す実装(src/Model.hs:67-69)で、Null を作りません。したがってこの変更で出力が変わることはありません。

src/Util.hs の例外まわりと trace' — 問題なし

Empty の deriving (Show, Typeable)(src/Util.hs:35)は GHC 7.10 以降 Typeable が自動導出なので冗長ですが 9.8 でも受理されます。throw(pure exception)と Debug.Trace.trace(src/Util.hs:53)の意味論は base 4.19 で変わっていません。CI ログ末尾の *** Exception (reporting due to +RTS -xc): (THUNK_STATIC), stack trace: Main.main は test/Spec.hs の exitSuccess が --trace の -xc で表示されているだけで、33 ケース全通過後の出力です。回帰ではありません。

なお T.toTitle(src/Util.hs:65-67,70,78)は text 1.2.4.0 → 2.1.1 で case mapping テーブルが Unicode 13.0 相当に再生成されていますが、ASCII の型名に対する結果は不変です。実スキーマの型名が ASCII でない場合のみ影響しますが、これは必須 2 の再生成 diff で一発で分かります。

その他

  • リポジトリ内に 8.8.3 / lts-16 / 2.5.1 の取り残しはありません。
  • renovate.json は config:base のみで、そもそも stack/cabal の依存は管理対象外です。固定を外したことで renovate の追従が失われる、ということはありません。

検証できなかったこと

  • 実際のビルド・生成の実行: この環境に GHC / stack が無く、haskell.org も egress でブロックされています。必須 1・2 はここが理由で私の側では潰せません。
  • EDItEUR 実スキーマの中身: editeur.org がブロックされており、schema/ はリポジトリに含まれていません。そのため実スキーマが XML entity を使っているか(必須 1)、型名が ASCII のみか(T.toTitle)を確認できていません。
  • CI ログの先頭部分: 実際にインストールされた stack のバージョンと hpack の警告有無を見たかったのですが、GitHub の Actions ログ本体(blob ストレージ)が egress ポリシーで拒否され、末尾 120 行しか取得できませんでした。推奨 3・4 で「この run が使ったバージョン」を具体的に書けていないのはこのためです。

Generated by Claude Code

Review's point: this PR pins the package set and then leaves the tool
that resolves it floating. stack "latest" also decides which hpack runs,
and stack regenerates onix.cabal on every build, so a stack release could
silently rewrite a committed file with nothing checking for a dirty tree.
Pin 3.11.1 (which bundles hpack 0.39.6).

The cache key had the same shape of problem from the other side: it
hashed package.yaml, onix.cabal and stack.yaml.lock but not GHC or stack,
and fell back on a bare Linux- prefix — so a GHC 8.8.3 store could be
restored into a 9.8.4 build. Put the toolchain in the key and in
restore-keys.

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

非常に密度の高いレビューをありがとうございます。推奨 2 件を反映し (310ef7e)、必須 2 件については 1 つを実測で潰し、もう 1 つは実行できない旨をお伝えします。

必須 1: xml-conduit の psEntityExpansionSizeLimit — 2.1 については実測で否定できました

src/Xsd/Parser.hs:539 の指摘を受けて、実物の ONIX 2.1 スキーマを手元で検査しました(#70 の make schema-from-history で git 履歴から復元できます)。

$ grep -c '<!ENTITY' schema/v2/*.xsd
none in any file

$ grep -oE '&[A-Za-z#][A-Za-z0-9]*;' schema/v2/*.xsd | sed 's/.*://' | sort | uniq -c
    389 &lt;
    389 &gt;
      6 &amp;

<!ENTITY 宣言は 1 件もなく、使われているのは XML 定義済み実体のみです。これらは内部実体の展開量を制限する psEntityExpansionSizeLimit(billion laughs 対策)の対象外なので、2.1 に関してはこの経路のリスクは実質ゼロです。

ただし 3.0 / 3.1 については確認できていません(アーカイブを取得できないため)。スキーマを入手した時点で同じ検査をすべき項目として残ります。

必須 2: generated/ が再生成・差分確認されていない — これは私には実行できません

指摘は完全に正しいです。Test e2e はコミット済みの //generated/go/v2:go をビルドしているだけで Haskell の生成器を呼ばず、テストスイートは Text.Mustache も Lib も import していません。したがって mustache 2.3.1 → 2.4.3.1 は全くテストされていません。

ご提案の「マージ前にローカルで再生成して git diff --exit-code generated/」が正しい手順ですが、この作業環境では実行できません。GHC / stack が無く(haskell.org が egress ポリシーでブロック)、v3 のスキーマも取得できません(editeur.org の CAPTCHA)。

マージ前にメンテナ側で実行していただく必要があります。 手順は:

make schema-from-history                  # 2.1 のみ (#70)
# 3.x は zip を third_party/distdir/ に置く
stack build --fast
stack exec onix-exe -- --schemaVersion v2 --language go
git diff --exit-code generated/

推奨 3・4: stack-version: "latest" とキャッシュキー

反映しました。「パッケージ集合は固定したのに、それを解決するツールを浮かせている」というのは的確な指摘でした。

  • stack-version を 3.11.1 に固定(hpack 0.39.6 同梱)。これは推奨 4 の「hpack 0.33.0 表記のまま stack latest が静かに上書きしうる」リスクも同時に下げます
  • キャッシュキーに GHC と stack を入れました。従来は package.yaml / onix.cabal / stack.yaml.lock のハッシュのみで、restore-keys が Linux- だったため、GHC 8.8.3 のストアを 9.8.4 のビルドに復元しうる状態でした。restore-keys も同じ接頭辞まで絞っています

検証について

packages: [] が正しいことを Stack.Lock の pkgImmutableLocations から確認し、さらに書き込みガードが when (newLocked /= locked) でテキストではなくパース値の比較だから stack がファイルを書き戻さない、というところまで追っていただけたのは、こちらでは到達できなかった深さです。

Ord Text が UTF-16 → UTF-8 移行を跨いで不変(compareText を両バージョンで確認)ゆえ M.toList の順序が安全、という否定側の検証も、生成物の決定性に直結するので特に有用でした。

任意の 3 件(lockfile のキー順、resolver: → snapshot:、base >=4.7 の陳腐化)は、別途まとめて対応します。


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.

1 participant