背景
Issue #663 の代表フロー自動走査中、PATCH /api/tasks/{id} の 500 調査で発見。直接原因は UpdateTaskUseCase の手組み JSON が LocalDate を引用符なし出力したこと(応急修正 PR #703)。だが調査の結果、根本原因は AuditLogPort.record が String detail を要求し、全 12 呼び出しが detail を文字列手組みしていることと判明した。当初の 3 ファイルは LocalDate 等で顕在化したサブセットにすぎない。
根本原因(監査の全 12 呼び出しが手組み JSON)
| caller |
現在の detail 生成 |
リスク |
UpdateTaskUseCase |
buildAuditDetail(ヘルパ) |
LocalDate で 500(顕在) |
UpdateTenantUseCase |
buildDiffDetail(ヘルパ) |
フィールド追加で同 500(潜在) |
ListTenantsUseCase |
buildListDetail(ヘルパ) |
同設計 |
CreateTaskUseCase / DeleteTaskUseCase / GetTenantUseCase |
"{\"taskId\":" + id + "}" |
連結手組み |
AddStakeholderUseCase / RemoveStakeholderUseCase |
"{\"taskId\":...,\"userId\":...}" |
連結手組み |
ChangeVisibilityUseCase(×2) |
"{\"taskId\":...,\"from\":\"" + ... |
文字列値の素連結=破壊リスク |
UpdateTenantStatusUseCase |
"{\"tenantId\":...,\"newStatus\":\"" + ... |
同上 |
GetPlatformMetricsUseCase |
"{}" |
固定値 |
CrossTenantViolationAuditService |
detail 組み立て |
同様 |
方針(2026-06-20 確定)
AuditLogPort.record の signature を record(AuditEventType, @Nullable Long tenantId, @Nullable Long userId, @Nullable Object detail) に変更。
- シリアライズを
audit.adapter.persistence.AuditLogPersistenceAdapter に集約。Spring 自動構成の ObjectMapper(JavaTimeModule 登録済・LocalDate→"2026-08-01")を注入し writeValueAsString。JsonProcessingException は unchecked に包んで fail-closed(監査整合性優先)。null detail の扱い("{}" 既定)を定める。
- 全 12 呼び出しを構造体渡し(
Map / 小型 record)に置換し、手組み JSON・buildAuditDetail・buildDiffDetail・buildListDetail・toJsonValue・escapeJsonString を全削除。
- detail キーは後方互換を維持(
taskId/tenantId/old/new 等)。diff 系(TASK_UPDATED/TENANT_UPDATED)は old/new を保つため小型 record AuditFieldChange(String field, @JsonProperty("old") @Nullable Object oldValue, @JsonProperty("new") @Nullable Object newValue) を usecase/audit 層に置いて詰め替える。domain の FieldChange は POJO のまま(設計規約 §1.2.3 遵守)。Map.of は null 値を弾くため不可。
- 再発防止として、コーディング規約に「監査 detail は構造体(
Map/record)で渡し、シリアライズは AuditLogPersistenceAdapter に集約する。usecase での JSON 手組み禁止」を 1 項追記(設計規約 §1.2.3 に整合、ADR は不要)。
やること
受入条件
背景
Issue #663 の代表フロー自動走査中、
PATCH /api/tasks/{id}の 500 調査で発見。直接原因はUpdateTaskUseCaseの手組み JSON がLocalDateを引用符なし出力したこと(応急修正 PR #703)。だが調査の結果、根本原因はAuditLogPort.recordがString detailを要求し、全 12 呼び出しが detail を文字列手組みしていることと判明した。当初の 3 ファイルはLocalDate等で顕在化したサブセットにすぎない。根本原因(監査の全 12 呼び出しが手組み JSON)
UpdateTaskUseCasebuildAuditDetail(ヘルパ)LocalDateで 500(顕在)UpdateTenantUseCasebuildDiffDetail(ヘルパ)ListTenantsUseCasebuildListDetail(ヘルパ)CreateTaskUseCase/DeleteTaskUseCase/GetTenantUseCase"{\"taskId\":" + id + "}"AddStakeholderUseCase/RemoveStakeholderUseCase"{\"taskId\":...,\"userId\":...}"ChangeVisibilityUseCase(×2)"{\"taskId\":...,\"from\":\"" + ...UpdateTenantStatusUseCase"{\"tenantId\":...,\"newStatus\":\"" + ...GetPlatformMetricsUseCase"{}"CrossTenantViolationAuditServicedetail組み立て方針(2026-06-20 確定)
AuditLogPort.recordの signature をrecord(AuditEventType, @Nullable Long tenantId, @Nullable Long userId, @Nullable Object detail)に変更。audit.adapter.persistence.AuditLogPersistenceAdapterに集約。Spring 自動構成のObjectMapper(JavaTimeModule登録済・LocalDate→"2026-08-01")を注入しwriteValueAsString。JsonProcessingExceptionは unchecked に包んで fail-closed(監査整合性優先)。nulldetail の扱い("{}"既定)を定める。Map/ 小型 record)に置換し、手組み JSON・buildAuditDetail・buildDiffDetail・buildListDetail・toJsonValue・escapeJsonStringを全削除。taskId/tenantId/old/new等)。diff 系(TASK_UPDATED/TENANT_UPDATED)はold/newを保つため小型 recordAuditFieldChange(String field, @JsonProperty("old") @Nullable Object oldValue, @JsonProperty("new") @Nullable Object newValue)を usecase/audit 層に置いて詰め替える。domain のFieldChangeは POJO のまま(設計規約 §1.2.3 遵守)。Map.ofは null 値を弾くため不可。Map/record)で渡し、シリアライズはAuditLogPersistenceAdapterに集約する。usecase での JSON 手組み禁止」を 1 項追記(設計規約 §1.2.3 に整合、ADR は不要)。やること
AuditLogPort.recordをObject detailに変更AuditLogPersistenceAdapterにObjectMapper注入 +writeValueAsString+ 例外 fail-closed +null規定AuditFieldChange(@JsonProperty・null 許容)を新設UpdateTaskUseCaseTestにLocalDate回帰テスト(detail が valid JSON かつ"2026-08-01"クォート、PR fix(audit): LocalDate を audit_logs.detail に引用符なし出力して 500 になるバグを応急修正 #703 review 指摘)AuditLogPortを mock するテスト・detail をアサートするテスト(TenantAdminAuditIT等)をキー維持で更新受入条件
AuditLogPersistenceAdapterの 1 か所AuditLogPort.recordの detail がObject型PATCH /api/tasks/{id}の期限日変更でaudit_logs.detailが valid JSON、既存キー名維持LocalDate回帰テストが存在