diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index 6805e4bd..12e74705 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -98,8 +98,10 @@ updates:
# Microsoft.EntityFrameworkCore.SqlServer へ同じ変更セットで上げる
# (この ignore は残したままでよい)。
#
- # 解決版と EF Core の宣言版のメジャー一致・床値の後退・この保留の消失や効きすぎは、
- # いずれも EfCorePackageAlignmentTests が機械的に検出する。
+ # この保留と対になる検査は EfCorePackageAlignmentTests にある。
+ # 【何を検出し、何が漏れるか】は CLAUDE.md §3 の同名の節を唯一の一覧とする
+ # (ここに書き写すと検査を足したとき片方だけ古くなる。実際 SqlClientPin_StaysADirectReference
+ # を足したときに、この行が 1 件少ないまま取り残された)。
- dependency-name: "Microsoft.Data.SqlClient"
update-types: ["version-update:semver-major"]
diff --git a/CLAUDE.md b/CLAUDE.md
index 48082b17..d5156b29 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -113,6 +113,7 @@ catch (DbUpdateConcurrencyException) { TempData["Warning"] = "..."; return ...;
### 注意点(固有の不変条件)
- **マイグレーションは起動時 `Database.Migrate()` で自動適用** — モデル変更は同一変更セットでマイグレーションを追加する。
+- **ロックファイルは全プロジェクト分を同時に再生成する** — `packages.lock.json` は `src/IncidentInsight.Web` と `tests/IncidentInsight.Tests` の 2 つある。テストプロジェクトは web を `ProjectReference` で参照しているため、**web の依存が動くとテスト側の解決結果も動く**。片方だけ更新すると CI の `dotnet restore --locked-mode` が `NU1004: The project references incidentinsight.web whose dependencies has changed` で落ちる。**とくに Dependabot の nuget PR は `src/` 側しか更新しないので、そのままではマージできない**(実例: PR #180 → #181 で手当て)。取り込むときは `dotnet restore` を流して 2 ファイルとも再生成し、同一コミットに含める。ロックファイルを持つプロジェクトを増やしたときも同じ(`RestorePackagesWithLockFile` を宣言し忘れたプロジェクトは locked-mode restore も `EfCorePackageAlignmentTests` の検査もすり抜けるため、同テストが宣言漏れ自体を検出する)。
- **`HasAtLeastOneValidMeasure` をバイパスしない**(インシデントは予防策が最低 1 件ないと登録不可)。
- **重症度/部署/インシデント種別の enum は `Incident` クラス上**(DB ではない)。値追加は static 辞書/配列+それを回す View を更新(マイグレーション不要)。enum の日本語ラベルと Bootstrap カラーは `Models/Enums/EnumLabels.cs` に集約し、`EnumLabels.Japanese()` と `EnumLabels.Color()` の両方を更新。`IncidentTypeMapping.cs` が日本語↔DB 文字列の双方向変換を担う。
- **再発ロジックは `IRecurrenceService`**(`Services/RecurrenceService.cs`)に集約。`HomeController.Index`(90 日窓 `FindRecurrenceAlertsAsync`)と `IncidentsController.Details`(時間無制限 `FindRecurrencesForIncidentAsync`)が委譲。マッチャ `RecurrenceDetector.FindSimilar` は純粋関数。ルール変更時はサービスとそのテストを更新。
@@ -120,7 +121,7 @@ catch (DbUpdateConcurrencyException) { TempData["Warning"] = "..."; return ...;
- **PHI を `AuditLog.ChangesJson` に平文で残さない** — 自由記述・個人名カラムには `[Sensitive(Mask.Redact)]` か `[Sensitive(Mask.Hash)]`(`Models/Auditing/SensitiveAttribute.cs`)を付与。インターセプタが `[REDACTED]` か HMAC-SHA256 擬似匿名化(鍵 = `Audit:HashSalt`)に置換。**新しい自由記述/個人名カラムは必ず annotate**。本番で `Audit:HashSalt` 空は起動失敗。salt ローテーションは過去ハッシュとの相関を失うので runbook に記録。
- **時刻は常に `IClock`**(`Services/IClock.cs`、JST)。`DateTime.Now/Today/UtcNow` を直接呼ばない。永続業務タイムスタンプと `AuditLog.ChangedAt` は運用ローカル時刻(JST)で保存。新規 View では `@inject IClock Clock` + `Clock.Today` を使う。
- **EF Core 本体とプロバイダ実装はメジャー版を全プロジェクトで揃える** — プロバイダ実装(Sqlite / SqlServer / Npgsql)は EF Core の内部 API に結び付くため、本体と同じメジャー版でしか動作保証がない。一方 `Microsoft.AspNetCore.Identity.EntityFrameworkCore` と `Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore` は公開 API しか使わず、メジャー版は .NET のリリース(`net8.0` → `net9.0`)に追随する(9.0.0 は `net9.0` 専用)ため、この規則の対象外にしている(束ねると EF Core 9 への更新が復元不能になる)。一部だけ上げてもビルドもテストも通る(テストは InMemory / SQLite しか触らない)ため、**PostgreSQL 配備でだけ実行時に壊れる**。Dependabot は `.github/dependabot.yml` の `nuget-ef-core`(通常の版更新)と `nuget-ef-core-security`(セキュリティ更新。`applies-to` の既定は version-updates なので別グループが要る)が major も含めて 1 本の PR に束ね、版ズレ自体は `EfCorePackageAlignmentTests` が検出する。検査は `packages.lock.json` の解決済み版を読む(壊れる主役は推移依存の `Relational` で、どの csproj にも直接書かれていないため)。「EF Core 系とは何か」の定義は dependabot.yml の `patterns` が唯一の真実の源で、名前に `EntityFrameworkCore` **または** `EFCore`(`EFCore.NamingConventions` 等)を含むパッケージがパターンから漏れていないことも同テストが固定する(上記 .NET リリース列の 2 つだけは理由付きで明示除外)。**プロバイダを足すときは 3 箇所(上記 2 グループの `patterns` と `nuget-minor-and-patch.exclude-patterns`)すべてに反映する。** 2 グループの `patterns` は集合として一致している必要があるが、`exclude-patterns` 側は書き方を問わず「EF Core 系を 1 つも拾わないこと」だけを求める。判定は文字列の一致ではなく、解決済みのパッケージ ID と `patterns` が表す範囲の代表値の両方が拾われないことで行う(`patterns` 側が前方一致 `"Foo*"` のときは、除外もその前置詞で始まるものをまるごと覆う必要があり、`"Foo"` と `"Foo.*"` のようにドット区切りの形を列挙する書き方では `FooBar` 等の同族が漏れるため足りない。完全一致のパターンは 1 つの ID しか指さないので、その ID を覆えていれば足りる)。そのため`"Npgsql*"` のように広く書くのは可、逆に `"Microsoft.EntityFrameworkCore*"` から末尾 `*` を落とすと(`.Sqlite` / `.SqlServer` / `.Relational` など既存のプロバイダ実装がその時点で漏れるため)通らない。広げすぎて上記 .NET リリース列の 2 つまで minor / patch の束ねから外すのも同テストが検出する。
-- **SQL Server の ADO.NET ドライバ(`Microsoft.Data.SqlClient`)のメジャー版は EF Core の SqlServer プロバイダに合わせる** — このドライバは `Microsoft.EntityFrameworkCore.SqlServer` の実体で、動作保証があるのはプロバイダが宣言したメジャー版に対してだけ。ところが名前に `EntityFrameworkCore` / `EFCore` を含まないため上記の `nuget-ef-core` グループには入らず、メジャー更新が単独 PR として現れる(実際 PR #178 が 5.1.7 → 7.0.2 で出ていた)。**壊れ方は EF Core 本体とプロバイダの版ズレとまったく同じ**で、テストは InMemory / SQLite しか触らないため**ビルドも全テストも緑のまま通り、SQL Server 配備でだけ実行時に壊れる**。`.github/dependabot.yml` の nuget エコシステムの `ignore` が major 更新だけを保留し(minor / patch は csproj の「床値」を上げる更新なので止めない)、`EfCorePackageAlignmentTests` が 4 つを検出する: 解決版と EF Core 宣言版のメジャー不一致 / 床値の後退 / 保留の消失 / 保留の効きすぎ(`update-types` の欠落・`versions` の追加・重複エントリ)。**期待値はテストに書かず EF Core の宣言側(`packages.lock.json` の `dependencies`)から読む**ので、EF Core を上げれば検査が自動で追随する。ドライバのメジャーを上げたいときは、それを宣言する版の `Microsoft.EntityFrameworkCore.SqlServer` へ同じ変更セットで上げる。
+- **SQL Server の ADO.NET ドライバ(`Microsoft.Data.SqlClient`)のメジャー版は EF Core の SqlServer プロバイダに合わせる** — このドライバは `Microsoft.EntityFrameworkCore.SqlServer` の実体で、動作保証があるのはプロバイダが宣言したメジャー版に対してだけ。ところが名前に `EntityFrameworkCore` / `EFCore` を含まないため上記の `nuget-ef-core` グループには入らず、メジャー更新が単独 PR として現れる(実際 PR #178 が 5.1.7 → 7.0.2 で出ていた)。**壊れ方は EF Core 本体とプロバイダの版ズレとまったく同じ**で、テストは InMemory / SQLite しか触らないため**ビルドも全テストも緑のまま通り、SQL Server 配備でだけ実行時に壊れる**。`.github/dependabot.yml` の nuget エコシステムの `ignore` が major 更新だけを保留し(minor / patch は csproj の「床値」を上げる更新なので止めない)、`EfCorePackageAlignmentTests` が 5 つを検出する: 解決版と EF Core 宣言版のメジャー不一致 / 床値の後退 / **ピンが直接参照でなくなること**(`packages.lock.json` の `type` が `Direct` から外れる = csproj のピン削除や推移依存への差し戻し)/ 保留の消失 / 保留の効きすぎ(`update-types` の欠落・`versions` の追加・重複エントリ)。**期待値はテストに書かず EF Core の宣言側(`packages.lock.json` の `dependencies`)から読む**ので、EF Core を上げれば検査が自動で追随する。**ただし「床値の後退」の比較相手はあくまで EF Core の宣言(現在 5.1.6)であって、csproj に書かれたピンの値ではない**——ピンを削除した場合は上記の `Direct` 検査が捕まえるが、**ピンの値を「EF Core の宣言版以上・現在のピン未満」の範囲(今なら 5.1.6 以上 5.2.3 未満、たとえば Dependabot の更新を revert して 5.1.7 に戻す変更)へ下げると 5 つすべてを素通りする**。床値検査が比べるのは EF Core の宣言であって直前のピンではないため、`5.1.7 >= 5.1.6` が成立してしまう。期待値を書かない方針と引き換えに残している穴なので、**ピンの値が下がる差分は(下げ幅の大小によらず)レビューで理由を確認する**。ドライバのメジャーを上げたいときは、それを宣言する版の `Microsoft.EntityFrameworkCore.SqlServer` へ同じ変更セットで上げる。
- **`SameDepartmentHandler`(`Authorization/SameDepartmentHandler.cs`)は `Incident` の eager-load が前提**。`PreventiveMeasure` / `CauseAnalysis` で `AuthorizeAsync` する前に `.Include(x => x.Incident)`。**fail-closed**(`Incident` が null なら拒否)。
### 新規 POST アクション追加時のチェックリスト
diff --git a/src/IncidentInsight.Web/IncidentInsight.Web.csproj b/src/IncidentInsight.Web/IncidentInsight.Web.csproj
index bb6a9904..1c4cc00b 100644
--- a/src/IncidentInsight.Web/IncidentInsight.Web.csproj
+++ b/src/IncidentInsight.Web/IncidentInsight.Web.csproj
@@ -19,16 +19,31 @@
-
+ EfCorePackageAlignmentTests がメジャー一致と床値の後退を検出する。
+ 【この行を消さない】床値検査が比べる相手は EF Core の宣言(現在 5.1.6)であって
+ ここに書かれた値ではないため、この行を「冗長だから」と消しても床値検査は素通りする
+ (解決版が 5.1.6 まで落ちても 5.1.6 >= 5.1.6 は成立する)。削除だけは
+ SqlClientPin_StaysADirectReference がロックファイルの type で捕まえる。
+ 残る穴は「この値を 5.1.6 以上・現在のピン未満へ下げる」変更(例: この更新を revert して
+ 5.1.7 に戻す)で、5.1.7 >= 5.1.6 が成立するため検査は全て緑のまま通る。
+ 下げ幅の大小によらずレビューでしか止まらない。 -->
+
diff --git a/src/IncidentInsight.Web/packages.lock.json b/src/IncidentInsight.Web/packages.lock.json
index ac516021..3720ff75 100644
--- a/src/IncidentInsight.Web/packages.lock.json
+++ b/src/IncidentInsight.Web/packages.lock.json
@@ -14,23 +14,18 @@
},
"Microsoft.Data.SqlClient": {
"type": "Direct",
- "requested": "[5.1.7, )",
- "resolved": "5.1.7",
- "contentHash": "awBwR6pCRyiFqB5z1iu+eMaFmt986JWgaA1+LR+vsdIRBgeBI5X8f3u+ZPnTqlHUwfTugl6ptIObzalWeAPugQ==",
+ "requested": "[5.2.3, )",
+ "resolved": "5.2.3",
+ "contentHash": "KtBQ2ZmPrGwIe8W8n3urW8hyDrPQgyNg7sE7T70zi5+Tdre5r5e7aAYMsJTRIKq8Y+wZB4UAfAAnUlK728xWOg==",
"dependencies": {
"Azure.Identity": "1.11.4",
- "Microsoft.Data.SqlClient.SNI.runtime": "5.1.2",
+ "Microsoft.Data.SqlClient.SNI.runtime": "5.2.0",
"Microsoft.Identity.Client": "4.61.3",
"Microsoft.IdentityModel.JsonWebTokens": "6.35.0",
"Microsoft.IdentityModel.Protocols.OpenIdConnect": "6.35.0",
"Microsoft.SqlServer.Server": "1.0.0",
- "System.Configuration.ConfigurationManager": "6.0.1",
- "System.Diagnostics.DiagnosticSource": "6.0.1",
- "System.Runtime.Caching": "6.0.0",
- "System.Security.Cryptography.Cng": "5.0.0",
- "System.Security.Principal.Windows": "5.0.0",
- "System.Text.Encoding.CodePages": "6.0.0",
- "System.Text.Encodings.Web": "6.0.1"
+ "System.Configuration.ConfigurationManager": "8.0.0",
+ "System.Runtime.Caching": "8.0.0"
}
},
"Microsoft.EntityFrameworkCore.Design": {
@@ -235,8 +230,8 @@
},
"Microsoft.Data.SqlClient.SNI.runtime": {
"type": "Transitive",
- "resolved": "5.1.2",
- "contentHash": "Q3mjL/oG7rYKDI1D34HLxf0FvhEAwOGzbiDfwv9/HaGP9f9yNV8KeXAS6ehxOaoBNqBRM6sTA19f9XVtf8rvLA=="
+ "resolved": "5.2.0",
+ "contentHash": "po1jhvFd+8pbfvJR/puh+fkHi0GRanAdvayh/0e47yaM6CXWZ6opUjCMFuYlAnD2LcbyvQE7fPJKvogmaUcN+w=="
},
"Microsoft.Data.Sqlite.Core": {
"type": "Transitive",
@@ -526,11 +521,6 @@
"resolved": "1.0.0",
"contentHash": "N4KeF3cpcm1PUHym1RmakkzfkEv3GRMyofVv40uXsQhCQeglr2OHNcUk2WOG51AKpGO8ynGpo9M/kFXSzghwug=="
},
- "Microsoft.Win32.SystemEvents": {
- "type": "Transitive",
- "resolved": "6.0.0",
- "contentHash": "hqTM5628jSsQiv+HGpiq3WKBl2c8v1KZfby2J6Pr7pEPlK9waPdgEO6b8A/+/xn/yZ9ulv8HuqK71ONy2tg67A=="
- },
"Mono.TextTemplating": {
"type": "Transitive",
"resolved": "3.0.0",
@@ -646,11 +636,11 @@
},
"System.Configuration.ConfigurationManager": {
"type": "Transitive",
- "resolved": "6.0.1",
- "contentHash": "jXw9MlUu/kRfEU0WyTptAVueupqIeE3/rl0EZDMlf8pcvJnitQ8HeVEp69rZdaStXwTV72boi/Bhw8lOeO+U2w==",
+ "resolved": "8.0.0",
+ "contentHash": "JlYi9XVvIREURRUlGMr1F6vOFLk7YSY4p1vHo4kX3tQ0AGrjqlRWHDi66ImHhy6qwXBG3BJ6Y1QlYQ+Qz6Xgww==",
"dependencies": {
- "System.Security.Cryptography.ProtectedData": "6.0.0",
- "System.Security.Permissions": "6.0.0"
+ "System.Diagnostics.EventLog": "8.0.0",
+ "System.Security.Cryptography.ProtectedData": "8.0.0"
}
},
"System.Diagnostics.DiagnosticSource": {
@@ -658,13 +648,10 @@
"resolved": "9.0.19",
"contentHash": "UW+FCKPoM15boJwYTsWueNxTgUaqipnZx3z68khaWutVnbtjNpgl9WO7tCcmt7/x3eEYoXn6TZSoHyLwryJUAA=="
},
- "System.Drawing.Common": {
+ "System.Diagnostics.EventLog": {
"type": "Transitive",
- "resolved": "6.0.0",
- "contentHash": "NfuoKUiP2nUWwKZN6twGqXioIe1zVD0RIj2t976A+czLHr2nY454RwwXs6JU9Htc6mwqL6Dn/nEL3dpVf2jOhg==",
- "dependencies": {
- "Microsoft.Win32.SystemEvents": "6.0.0"
- }
+ "resolved": "8.0.0",
+ "contentHash": "fdYxcRjQqTTacKId/2IECojlDSFvp7LP5N78+0z/xH7v/Tuw5ZAxu23Y6PTCRinqyu2ePx+Gn1098NC6jM6d+A=="
},
"System.Formats.Asn1": {
"type": "Transitive",
@@ -723,10 +710,10 @@
},
"System.Runtime.Caching": {
"type": "Transitive",
- "resolved": "6.0.0",
- "contentHash": "E0e03kUp5X2k+UAoVl6efmI7uU7JRBWi5EIdlQ7cr0NpBGjHG4fWII35PgsBY9T4fJQ8E4QPsL0rKksU9gcL5A==",
+ "resolved": "8.0.0",
+ "contentHash": "4TmlmvGp4kzZomm7J2HJn6IIx0UUrQyhBDyb5O1XiunZlQImXW+B8b7W/sTPcXhSf9rp5NR5aDtQllwbB5elOQ==",
"dependencies": {
- "System.Configuration.ConfigurationManager": "6.0.0"
+ "System.Configuration.ConfigurationManager": "8.0.0"
}
},
"System.Runtime.CompilerServices.Unsafe": {
@@ -734,37 +721,15 @@
"resolved": "6.0.0",
"contentHash": "/iUeP3tq1S0XdNNoMz5C9twLSrM/TH+qElHkXWaPvuNOt+99G75NrV0OS2EqHx5wMN7popYjpc8oTjC1y16DLg=="
},
- "System.Security.AccessControl": {
- "type": "Transitive",
- "resolved": "6.0.0",
- "contentHash": "AUADIc0LIEQe7MzC+I0cl0rAT8RrTAKFHl53yHjEUzNVIaUlhFY11vc2ebiVJzVBuOzun6F7FBA+8KAbGTTedQ=="
- },
"System.Security.Cryptography.Cng": {
"type": "Transitive",
- "resolved": "5.0.0",
- "contentHash": "jIMXsKn94T9JY7PvPq/tMfqa6GAaHpElRDpmG+SuL+D3+sTw2M8VhnibKnN8Tq+4JqbPJ/f+BwtLeDMEnzAvRg==",
- "dependencies": {
- "System.Formats.Asn1": "5.0.0"
- }
+ "resolved": "4.5.0",
+ "contentHash": "WG3r7EyjUe9CMPFSs6bty5doUqT+q9pbI80hlNzo2SkPkZ4VTuZkGWjpp77JB8+uaL4DFPRdBsAY+DX3dBK92A=="
},
"System.Security.Cryptography.ProtectedData": {
"type": "Transitive",
- "resolved": "6.0.0",
- "contentHash": "rp1gMNEZpvx9vP0JW0oHLxlf8oSiQgtno77Y4PLUBjSiDYoD77Y8uXHr1Ea5XG4/pIKhqAdxZ8v8OTUtqo9PeQ=="
- },
- "System.Security.Permissions": {
- "type": "Transitive",
- "resolved": "6.0.0",
- "contentHash": "T/uuc7AklkDoxmcJ7LGkyX1CcSviZuLCa4jg3PekfJ7SU0niF0IVTXwUiNVP9DSpzou2PpxJ+eNY2IfDM90ZCg==",
- "dependencies": {
- "System.Security.AccessControl": "6.0.0",
- "System.Windows.Extensions": "6.0.0"
- }
- },
- "System.Security.Principal.Windows": {
- "type": "Transitive",
- "resolved": "5.0.0",
- "contentHash": "t0MGLukB5WAVU9bO3MGzvlGnyJPgUlcwerXn1kzBRjwLKixT96XV0Uza41W49gVd8zEMFu9vQEFlv0IOrytICA=="
+ "resolved": "8.0.0",
+ "contentHash": "+TUFINV2q2ifyXauQXRwy4CiBhqvDEDZeVJU7qfxya4aRYOKzVBpN+4acx25VcPB9ywUN6C0n8drWl110PhZEg=="
},
"System.Text.Encoding": {
"type": "Transitive",
@@ -776,14 +741,6 @@
"System.Runtime": "4.3.0"
}
},
- "System.Text.Encoding.CodePages": {
- "type": "Transitive",
- "resolved": "6.0.0",
- "contentHash": "ZFCILZuOvtKPauZ/j/swhvw68ZRi9ATCfvGbk1QfydmcXBkIWecWKn/250UH7rahZ5OoDBaiAudJtPvLwzw85A==",
- "dependencies": {
- "System.Runtime.CompilerServices.Unsafe": "6.0.0"
- }
- },
"System.Text.Encodings.Web": {
"type": "Transitive",
"resolved": "9.0.19",
@@ -807,14 +764,6 @@
"type": "Transitive",
"resolved": "4.5.4",
"contentHash": "zteT+G8xuGu6mS+mzDzYXbzS7rd3K6Fjb9RiZlYlJPam2/hU7JCBZBVEcywNuR+oZ1ncTvc/cq0faRr3P01OVg=="
- },
- "System.Windows.Extensions": {
- "type": "Transitive",
- "resolved": "6.0.0",
- "contentHash": "IXoJOXIqc39AIe+CIR7koBtRGMiCt/LPM3lI+PELtDIy9XdyeSrwXFdWV9dzJ2Awl0paLWUaknLxFQ5HpHZUog==",
- "dependencies": {
- "System.Drawing.Common": "6.0.0"
- }
}
}
}
diff --git a/tests/IncidentInsight.Tests/Packaging/EfCorePackageAlignmentTests.cs b/tests/IncidentInsight.Tests/Packaging/EfCorePackageAlignmentTests.cs
index fb7f219f..cf7f08ca 100644
--- a/tests/IncidentInsight.Tests/Packaging/EfCorePackageAlignmentTests.cs
+++ b/tests/IncidentInsight.Tests/Packaging/EfCorePackageAlignmentTests.cs
@@ -170,6 +170,10 @@ public class EfCorePackageAlignmentTests
// ロックファイルで、実際に解決された版を表す JSON キー
private const string ResolvedKey = "resolved";
+ // ロックファイルの type が「csproj に直接書かれた参照」を表すときの値
+ // (推移依存なら "Transitive"、ProjectReference なら "Project" になる)
+ private const string DirectPackageKind = "Direct";
+
// Dependabot 設定ファイルのリポジトリルートからの位置
private static readonly string DependabotConfigPath = Path.Combine(".github", "dependabot.yml");
@@ -574,6 +578,41 @@ public void SqlClientPin_StaysWithinEfCoreDeclaredMajor()
+ "セキュリティ更新を後退させない床値として置かれています。下げないでください。");
}
+ [Fact]
+ public void SqlClientPin_StaysADirectReference()
+ {
+ // 全ロックファイルから、このドライバの記録(どのプロジェクトが・直接か推移か)を集める
+ var entries = LockEntriesFor(SqlClientPackageId);
+
+ // 1 件も無いのは、参照そのものが消えたか検出網が劣化したかのどちらか(fail-closed)
+ Assert.True(entries.Count > 0,
+ $"{LockFileName} に {SqlClientPackageId} の記録がありません。参照が外れた可能性があります。");
+
+ // csproj に直接書かれた参照として記録しているプロジェクトを取り出す
+ var direct = entries
+ .Where(package => string.Equals(package.Kind, DirectPackageKind, StringComparison.OrdinalIgnoreCase))
+ .ToList();
+
+ // 【なぜ「直接参照であること」まで固定するのか】
+ // 上の SqlClientPin_StaysWithinEfCoreDeclaredMajor が床値の比較相手にするのは
+ // EF Core SqlServer の宣言(現在 5.1.6)であって、csproj に書かれたピンの値ではない。
+ // そのためピンを「冗長だから」と削除すると、解決版が EF Core の宣言どおりに落ちても
+ // 5.1.6 >= 5.1.6 で床値検査を素通りし、ビルドもテストも緑のまま SQL Server 配備だけが
+ // 古いドライバに戻る(このリポジトリで最も起きやすい形の無言の後退)。
+ // ここで type が Direct であることを見ておけば、削除も「推移依存に戻す」リファクタも
+ // 検出できる。期待する版をテストに書かずに済むので、期待値を宣言側から読む方針とも両立する
+ Assert.True(direct.Count > 0,
+ $"{SqlClientPackageId} が {LockFileName} のどこにも {DirectPackageKind} として記録されていません。"
+ + "csproj の直接参照(床値のピン)が外れた可能性があります。\n"
+ + "このピンは、EF Core SqlServer が推移的に引く版が古いサービシングパッチであるために"
+ + "置かれています。外すと解決版はその古い版まで落ちますが、床値検査の比較相手が"
+ + "EF Core の宣言そのものであるため検査は素通りし、SQL Server 配備でだけ古いドライバが"
+ + "読み込まれます。\n"
+ + "意図して外すのであれば、EF Core SqlServer の宣言版が十分新しくなったことを確認し、"
+ + "csproj のコメントと本テストを同じ変更セットで畳んでください。\n"
+ + "現在の記録:\n" + Describe(entries));
+ }
+
[Fact]
public void DependabotConfig_HoldsSqlClientMajorUpdates()
{
@@ -966,8 +1005,7 @@ private static string ReadDeclaredDependencyVersion(string dependentId, string d
private static string ReadResolvedVersion(string packageId)
{
// 全ロックファイルから、その ID で解決されている版を集める
- var versions = ResolvedPackages.Value
- .Where(package => string.Equals(package.Id, packageId, StringComparison.OrdinalIgnoreCase))
+ var versions = LockEntriesFor(packageId)
.Select(package => package.Version)
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToList();
@@ -983,6 +1021,16 @@ private static string ReadResolvedVersion(string packageId)
return versions[0];
}
+ // 指定パッケージが全ロックファイルでどう記録されているかを集める。
+ // 【なぜ切り出すか】「ID で照合して該当行を集める」は解決版の読み出しと
+ // 直接参照の検査の 2 箇所で必要になる。照合規則(大文字小文字を無視する等)を
+ // 書き写すと、片方だけ直したときにもう片方の検査が静かに意味を変える(§6 DRY)
+ private static IReadOnlyList LockEntriesFor(string packageId) =>
+ // ID が一致する行だけを残して一覧にする
+ ResolvedPackages.Value
+ .Where(package => string.Equals(package.Id, packageId, StringComparison.OrdinalIgnoreCase))
+ .ToList();
+
// 版文字列からメジャー版(最初のドットまで)を取り出す
private static int MajorOf(string version)
{
diff --git a/tests/IncidentInsight.Tests/packages.lock.json b/tests/IncidentInsight.Tests/packages.lock.json
index 8f48cf17..a5316b1c 100644
--- a/tests/IncidentInsight.Tests/packages.lock.json
+++ b/tests/IncidentInsight.Tests/packages.lock.json
@@ -155,28 +155,23 @@
},
"Microsoft.Data.SqlClient": {
"type": "Transitive",
- "resolved": "5.1.7",
- "contentHash": "awBwR6pCRyiFqB5z1iu+eMaFmt986JWgaA1+LR+vsdIRBgeBI5X8f3u+ZPnTqlHUwfTugl6ptIObzalWeAPugQ==",
+ "resolved": "5.2.3",
+ "contentHash": "KtBQ2ZmPrGwIe8W8n3urW8hyDrPQgyNg7sE7T70zi5+Tdre5r5e7aAYMsJTRIKq8Y+wZB4UAfAAnUlK728xWOg==",
"dependencies": {
"Azure.Identity": "1.11.4",
- "Microsoft.Data.SqlClient.SNI.runtime": "5.1.2",
+ "Microsoft.Data.SqlClient.SNI.runtime": "5.2.0",
"Microsoft.Identity.Client": "4.61.3",
"Microsoft.IdentityModel.JsonWebTokens": "6.35.0",
"Microsoft.IdentityModel.Protocols.OpenIdConnect": "6.35.0",
"Microsoft.SqlServer.Server": "1.0.0",
- "System.Configuration.ConfigurationManager": "6.0.1",
- "System.Diagnostics.DiagnosticSource": "6.0.1",
- "System.Runtime.Caching": "6.0.0",
- "System.Security.Cryptography.Cng": "5.0.0",
- "System.Security.Principal.Windows": "5.0.0",
- "System.Text.Encoding.CodePages": "6.0.0",
- "System.Text.Encodings.Web": "6.0.1"
+ "System.Configuration.ConfigurationManager": "8.0.0",
+ "System.Runtime.Caching": "8.0.0"
}
},
"Microsoft.Data.SqlClient.SNI.runtime": {
"type": "Transitive",
- "resolved": "5.1.2",
- "contentHash": "Q3mjL/oG7rYKDI1D34HLxf0FvhEAwOGzbiDfwv9/HaGP9f9yNV8KeXAS6ehxOaoBNqBRM6sTA19f9XVtf8rvLA=="
+ "resolved": "5.2.0",
+ "contentHash": "po1jhvFd+8pbfvJR/puh+fkHi0GRanAdvayh/0e47yaM6CXWZ6opUjCMFuYlAnD2LcbyvQE7fPJKvogmaUcN+w=="
},
"Microsoft.Data.Sqlite.Core": {
"type": "Transitive",
@@ -699,11 +694,6 @@
"Microsoft.TestPlatform.ObjectModel": "18.9.0"
}
},
- "Microsoft.Win32.SystemEvents": {
- "type": "Transitive",
- "resolved": "6.0.0",
- "contentHash": "hqTM5628jSsQiv+HGpiq3WKBl2c8v1KZfby2J6Pr7pEPlK9waPdgEO6b8A/+/xn/yZ9ulv8HuqK71ONy2tg67A=="
- },
"Npgsql": {
"type": "Transitive",
"resolved": "9.0.3",
@@ -763,11 +753,11 @@
},
"System.Configuration.ConfigurationManager": {
"type": "Transitive",
- "resolved": "6.0.1",
- "contentHash": "jXw9MlUu/kRfEU0WyTptAVueupqIeE3/rl0EZDMlf8pcvJnitQ8HeVEp69rZdaStXwTV72boi/Bhw8lOeO+U2w==",
+ "resolved": "8.0.0",
+ "contentHash": "JlYi9XVvIREURRUlGMr1F6vOFLk7YSY4p1vHo4kX3tQ0AGrjqlRWHDi66ImHhy6qwXBG3BJ6Y1QlYQ+Qz6Xgww==",
"dependencies": {
- "System.Security.Cryptography.ProtectedData": "6.0.0",
- "System.Security.Permissions": "6.0.0"
+ "System.Diagnostics.EventLog": "8.0.0",
+ "System.Security.Cryptography.ProtectedData": "8.0.0"
}
},
"System.Diagnostics.DiagnosticSource": {
@@ -780,14 +770,6 @@
"resolved": "8.0.1",
"contentHash": "n1ZP7NM2Gkn/MgD8+eOT5MulMj6wfeQMNS2Pizvq5GHCZfjlFMXV2irQlQmJhwA2VABC57M0auudO89Iu2uRLg=="
},
- "System.Drawing.Common": {
- "type": "Transitive",
- "resolved": "6.0.0",
- "contentHash": "NfuoKUiP2nUWwKZN6twGqXioIe1zVD0RIj2t976A+czLHr2nY454RwwXs6JU9Htc6mwqL6Dn/nEL3dpVf2jOhg==",
- "dependencies": {
- "Microsoft.Win32.SystemEvents": "6.0.0"
- }
- },
"System.Formats.Asn1": {
"type": "Transitive",
"resolved": "9.0.19",
@@ -837,48 +819,21 @@
},
"System.Runtime.Caching": {
"type": "Transitive",
- "resolved": "6.0.0",
- "contentHash": "E0e03kUp5X2k+UAoVl6efmI7uU7JRBWi5EIdlQ7cr0NpBGjHG4fWII35PgsBY9T4fJQ8E4QPsL0rKksU9gcL5A==",
+ "resolved": "8.0.0",
+ "contentHash": "4TmlmvGp4kzZomm7J2HJn6IIx0UUrQyhBDyb5O1XiunZlQImXW+B8b7W/sTPcXhSf9rp5NR5aDtQllwbB5elOQ==",
"dependencies": {
- "System.Configuration.ConfigurationManager": "6.0.0"
+ "System.Configuration.ConfigurationManager": "8.0.0"
}
},
- "System.Runtime.CompilerServices.Unsafe": {
- "type": "Transitive",
- "resolved": "6.0.0",
- "contentHash": "/iUeP3tq1S0XdNNoMz5C9twLSrM/TH+qElHkXWaPvuNOt+99G75NrV0OS2EqHx5wMN7popYjpc8oTjC1y16DLg=="
- },
- "System.Security.AccessControl": {
- "type": "Transitive",
- "resolved": "6.0.0",
- "contentHash": "AUADIc0LIEQe7MzC+I0cl0rAT8RrTAKFHl53yHjEUzNVIaUlhFY11vc2ebiVJzVBuOzun6F7FBA+8KAbGTTedQ=="
- },
"System.Security.Cryptography.Cng": {
"type": "Transitive",
- "resolved": "5.0.0",
- "contentHash": "jIMXsKn94T9JY7PvPq/tMfqa6GAaHpElRDpmG+SuL+D3+sTw2M8VhnibKnN8Tq+4JqbPJ/f+BwtLeDMEnzAvRg==",
- "dependencies": {
- "System.Formats.Asn1": "5.0.0"
- }
+ "resolved": "4.5.0",
+ "contentHash": "WG3r7EyjUe9CMPFSs6bty5doUqT+q9pbI80hlNzo2SkPkZ4VTuZkGWjpp77JB8+uaL4DFPRdBsAY+DX3dBK92A=="
},
"System.Security.Cryptography.ProtectedData": {
"type": "Transitive",
- "resolved": "6.0.0",
- "contentHash": "rp1gMNEZpvx9vP0JW0oHLxlf8oSiQgtno77Y4PLUBjSiDYoD77Y8uXHr1Ea5XG4/pIKhqAdxZ8v8OTUtqo9PeQ=="
- },
- "System.Security.Permissions": {
- "type": "Transitive",
- "resolved": "6.0.0",
- "contentHash": "T/uuc7AklkDoxmcJ7LGkyX1CcSviZuLCa4jg3PekfJ7SU0niF0IVTXwUiNVP9DSpzou2PpxJ+eNY2IfDM90ZCg==",
- "dependencies": {
- "System.Security.AccessControl": "6.0.0",
- "System.Windows.Extensions": "6.0.0"
- }
- },
- "System.Security.Principal.Windows": {
- "type": "Transitive",
- "resolved": "5.0.0",
- "contentHash": "t0MGLukB5WAVU9bO3MGzvlGnyJPgUlcwerXn1kzBRjwLKixT96XV0Uza41W49gVd8zEMFu9vQEFlv0IOrytICA=="
+ "resolved": "8.0.0",
+ "contentHash": "+TUFINV2q2ifyXauQXRwy4CiBhqvDEDZeVJU7qfxya4aRYOKzVBpN+4acx25VcPB9ywUN6C0n8drWl110PhZEg=="
},
"System.Text.Encoding": {
"type": "Transitive",
@@ -890,14 +845,6 @@
"System.Runtime": "4.3.0"
}
},
- "System.Text.Encoding.CodePages": {
- "type": "Transitive",
- "resolved": "6.0.0",
- "contentHash": "ZFCILZuOvtKPauZ/j/swhvw68ZRi9ATCfvGbk1QfydmcXBkIWecWKn/250UH7rahZ5OoDBaiAudJtPvLwzw85A==",
- "dependencies": {
- "System.Runtime.CompilerServices.Unsafe": "6.0.0"
- }
- },
"System.Text.Encodings.Web": {
"type": "Transitive",
"resolved": "9.0.19",
@@ -917,14 +864,6 @@
"resolved": "4.5.4",
"contentHash": "zteT+G8xuGu6mS+mzDzYXbzS7rd3K6Fjb9RiZlYlJPam2/hU7JCBZBVEcywNuR+oZ1ncTvc/cq0faRr3P01OVg=="
},
- "System.Windows.Extensions": {
- "type": "Transitive",
- "resolved": "6.0.0",
- "contentHash": "IXoJOXIqc39AIe+CIR7koBtRGMiCt/LPM3lI+PELtDIy9XdyeSrwXFdWV9dzJ2Awl0paLWUaknLxFQ5HpHZUog==",
- "dependencies": {
- "System.Drawing.Common": "6.0.0"
- }
- },
"xunit.abstractions": {
"type": "Transitive",
"resolved": "2.0.3",
@@ -969,7 +908,7 @@
"type": "Project",
"dependencies": {
"Microsoft.AspNetCore.Identity.EntityFrameworkCore": "[8.0.30, )",
- "Microsoft.Data.SqlClient": "[5.1.7, )",
+ "Microsoft.Data.SqlClient": "[5.2.3, )",
"Microsoft.EntityFrameworkCore.SqlServer": "[9.0.19, )",
"Microsoft.EntityFrameworkCore.Sqlite": "[9.0.19, )",
"Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore": "[8.0.30, )",