<!-- このファイルは KrileWorks が公開している Claude Code 用スキルです。
     出典: https://krileworks.com/agent-skills
     手元の .claude/skills/ に置くと、エージェントが Apex Stem の API を
     記憶ではなくこの記述に従って書くようになります。 -->

---
name: apex-access-mode
description: >
  Use this skill when deciding or reviewing the security execution context of Apex (sharing
  keyword × FLS/CRUD AccessLevel), and when migrating to ApexEloquent v3 / the Summer'26 (API v67)
  "user-mode by default" security flip. Covers: when to use without sharing vs with/inherited
  sharing, when to add .systemMode() / .userMode(), the 2-axis truth table (and the one impossible
  combo), the trigger/flow cascade migration recipe, and the runAs test strategy that audits each
  escalation before bumping API version. 発火する典型: systemMode / userMode / USER_MODE / SYSTEM_MODE /
  without sharing / inherited sharing / AccessLevel / FLS / 共有 / INSUFFICIENT_ACCESS_OR_READONLY /
  fields being inaccessible / No such column / runAs / 制限ユーザーテスト / v67 / Summer'26 / secure-by-default /
  ApexEloquent v3 移行 / 既存ユーザー新規ユーザーで方針が変わる /
  Apex 管理共有 / __Share / RowCause / Manual / sharingReasons / fieldPermissions / 部分プロファイル /
  公開グループ / 共有ルール / sharingRules / UserRecordAccess / GroupMember / トリガでスタンプ / 標準活動をカスタム化。
---

# Apex 実行モード (sharing × AccessLevel) 判断ガイド

Apex の「どの権限文脈で動くか」は **独立した2軸** で決まる。ここを混同すると「動くが漏れる」「直したつもりで次の hop が落ちる」事故になる。
ApexEloquent v3 / Summer'26 (API v67) の **user-mode 既定化 (secure-by-default)** を境に、既存処理の見直しが要る部分。

関連: Eloquent/IEloquent の DI・`userMode()`/`systemMode()` の API 仕様・v1〜v3 バージョン差分は Skill ツールで `apex-eloquent` をロード。

📘 **基礎編**: 「そもそも FLS / OWD / 共有ルール / `__Share` は何を制御しているのか」「エラーメッセージから壊れた層を特定する」は
**`references/access-control-3layers.md`** を読む。下記の2軸を **表 (CRUD) / 列 (FLS) / 行 (共有)** の3層に開いて、
`UserRecordAccess` での実測手順つきで整理してある。**新規カスタム項目に FLS が自動付与されない罠**
(`Field does not exist` なのに org には項目がある) もそこ。v3 設計に入る前の土台として先に読むとよい。

---

## 🧭 2軸モデル (これが全ての土台)

| 軸 | 決めるもの | 制御方法 |
|---|---|---|
| **共有 (record 可視性)** | レコード単位の見える/見えない | クラスの `with` / `without` / `inherited sharing` キーワード |
| **FLS / CRUD** | 項目・オブジェクト権限 | DML/SOQL の `AccessLevel` (`USER_MODE` / `SYSTEM_MODE`) |

### 真理値表 (実現できる組み合わせ)

| 欲しい挙動 | 可能? | やり方 |
|---|---|---|
| FLS 効く + 共有 効く | ✅ | `USER_MODE` (クラスの sharing キーワードに**関係なく** USER_MODE が共有も強制) |
| FLS 無視 + 共有 効く | ✅ | `SYSTEM_MODE` + `with` / `inherited(from with) sharing` |
| **FLS 無視 + 共有 無視** | ✅ | **`SYSTEM_MODE` + `without sharing`** ← system プロセスの定番 |
| **FLS 効く + 共有だけ無視** | ❌ | AccessLevel 単独では不可 (下記「例外」参照) |

### 恒久知識 (落とし穴)
- **`USER_MODE` は {FLS + CRUD + 共有} を「束」で強制し、`without sharing` を上書きする。**
  → `without sharing` クラスでも USER_MODE DML だと共有が再強制され `INSUFFICIENT_ACCESS_OR_READONLY`。
  共有を外すには `SYSTEM_MODE` に降りるしかなく、**降りると FLS も一緒に落ちる**。
- **`Modify All Data` は CRUD と record 共有は貫通するが、FLS は貫通しない。**
  → システム管理者ですら、ある項目の Edit FLS を持たなければ USER_MODE DML は `fields being inaccessible` で落ちる
  (実例: `InstalledStore__c.Age__c` が管理者プロファイルでも Edit=false だった)。
- **症状の読み方**:
  - `No such column 'xxx__c'` / `fields being inaccessible` → **FLS 軸** (→ `.systemMode()` で解決)
  - `INSUFFICIENT_ACCESS_OR_READONLY` (レコード編集権) → **共有軸** (→ `without sharing` で解決)
  - v3 は DML 例外を `ApexEloquentException` でラップするので、**スタックの doUpdate 位置と元例外**で軸を判別する。

### 例外: 「FLS は効かせたいが共有だけ外したい」(レア)
AccessLevel enum では表現不能。`SYSTEM_MODE`(`without sharing`)で動かしつつ FLS を手動で:
- 読み: SOQL に `WITH USER_MODE`
- 書き: `Security.stripInaccessible(AccessType.UPDATABLE, records)` で不可視項目を落としてから DML

---

## 🎯 判断ルール: 「呼び出し元」ではなく「操作の意図」で決める

トリガ=systemMode / フロー=userMode のような **呼び出し元での二分は誤り**(反例: Flow から呼ばれる設置店同期 `SyncAccountToStore` は system 処理 = `without sharing` + systemMode)。

**既定はセキュア (user mode + inherited sharing)。必要な箇所だけ理由コメント付きで昇格する。**

| 操作の性質 | 推奨 | 例 |
|---|---|---|
| **system プロセス** (集計 / 焼付 / 移行 / 非正規化。誰が起こしても整合性のため完遂すべき) | `without sharing` + 全 Eloquent `.systemMode()` | RecalcAccount, MigrateAccountToStore, SyncAccountToStore, PushbackStoreToAccount, StampEventOffice |
| **ユーザー代行** (本人が見える/触れる範囲で動くべき) | `inherited`/`with sharing` + user mode (systemMode なし) | ユーザーが自レコードを編集する LWC/Flow アクション |
| **DML/SOQL を持たない** (before-insert の in-memory 計算) | どちらも不要 | BakeAccountArea |

判断の所在: 「`without sharing` + `.systemMode()`」は **共有と FLS の両方を外す**。`with sharing` のクラスに `.systemMode()` だけ付けても **共有は残る**(FLS だけ外れる)ので、低権限ユーザー起点の親レコード書込は直らない。両軸セットで考える。

> **🪝 トリガ連鎖の罠**: v3 USER_MODE 化は **1 usecase で終わらない**。1つ直して書込が成功すると、その DML が **次のトリガを発火** → 次の hop の USER_MODE 処理が FLS/共有で落ちる。
> (例: RecalcAccount の Account 更新成功 → AccountTrigger 発火 → MigrateAccountToStore の USER_MODE 書込が InstalledStore FLS で落ちる)。
> **「親オブジェクト/関連オブジェクトへ書き込む処理」「トリガが連鎖する処理」は hop ごとに書込先のアクセスを点検**する。

---

## 🔁 v3 / v67 移行レシピ (cascade を揃える)

1. **対象を洗い出す**: トリガ/フロー/バッチから到達し `new Eloquent()` を使う usecase 全部 (`grep -rl "new Eloquent()" Usecases FlowHandlers BatchHandlers`)。
2. **各 usecase を意図で分類** (上表)。system プロセスは `without sharing` + 全 Eloquent `.systemMode()` に。**昇格には必ず理由コメント**を残す。
3. **DML/SOQL を持たない usecase は触らない**(secure 既定のまま)。
4. **runAs テストで escalation を監査** (下記)。
5. **デプロイは連鎖単位**で。1つだけ直すと次の hop で赤になるので、関連トリガ配下を一括で。
6. ローカル独自 hotfix (例: `SystemModeDml`) は v3 systemMode に置換できたら **撤去**(repo + org の destructive delete)。

---

## 🧪 runAs テスト戦略 (v67 昇格前の必須ガードレール)

公式ガイド「クラスを新 API version に上げる前に、制限付きユーザーとして実行するテストを足してデータ可視性が正しいことを検証せよ」への回答。
**runAs テストは単なる動作確認ではなく「各 escalation 判断が必要十分か」を縛る監査装置**。

### USER_MODE が新たに強制する5次元 (テストで突く面)
1. FLS read (SOQL) — `No such column`
2. FLS edit (DML) — `fields inaccessible` (※管理者でも欠けうる)
3. オブジェクト CRUD — `sObject type 'X__c' is not supported`
4. record 共有 (read) — 行が減る
5. record 共有 (edit) — `INSUFFICIENT_ACCESS_OR_READONLY`

### 不変レシピ (シナリオ共通の骨格)
```
1. persona = System.runAs(self){ insert User(本番最小プロファイル) }   // mixed DML 回避
2. データ = SOrchestrator で admin 所有で作成 + Share で最小アクセスだけ付与 (敵対的)
3. System.runAs(persona){ Test.startTest(); 起動; Test.stopTest(); }
4. assert を「意図」で差し替え:
     system プロセス → 副作用が完遂したか (焼けた/移行した)
     ユーザー代行    → 漏れてない/スコープされた/graceful に拒否されたか
     可視性          → runAs(persona){ クエリ } で見える範囲が正しいか
```

### シナリオ・アーキタイプ (昇格サイトに必要なものを1本ずつ)

| # | 種別 | 突く次元 | 何を assert | どこに要る |
|---|---|---|---|---|
| **S-A** | system / 共有 | 5 | 他人所有・Read共有のみのレコードへ system 処理が**完遂して書ける** | `without sharing` 昇格サイト |
| **S-B** | system / FLS | 1,2 | 制限ユーザーが項目FLSを持たなくても**焼き付く**。かつ本人は `WITH USER_MODE` で読めない(=systemMode が必要だった裏付け) | `.systemMode()` 昇格サイト |
| **S-C** | ユーザー代行 / **負** | 2,4 | user-mode 据え置きアクションを権限不足ユーザーが叩くと**漏れず拒否・不変** | systemMode を**付けなかった**サイト(過剰露出ガード) |
| **S-D** | 可視性事後条件 | 3,4 | system 処理で作られたレコードが、起点ユーザーの権限を超えて**見えるようになっていない** | 連鎖して別オブジェクトを生成/更新するサイト |

> **S-C の対象が無い** = その機能群が全部 system 処理である証拠。**ユーザー代行機能を新設したら S-C を必ず添える**。

### 実装 gotcha
- **admin で書かない**。本番最小ペルソナ(実在プロファイル)で。理想化した独自プロファイルは本番とズレ、FLS 借金を炙り出せない。
- **mixed DML 回避**: `System.runAs(new User(Id=UserInfo.getUserId())){ insert user; }` で setup オブジェクト(User/Profile)を先に commit。
- **FLS は USER_MODE のコードでしか強制されない**。runAs は共有/プロファイル文脈を切り替えるが、FLS は v3 の USER_MODE 経路で初めて効く → **このテスト群は v3 上でこそ意味を持つ**。
- **可視性クエリは `WITH USER_MODE`** で(runAs だけでは inline SOQL に共有/FLS が効かない)。ただし**項目 FLS 起因の誤検出を避けるため `SELECT Id` / `count()` 主体**に(Id は常にアクセス可)。
- **オブジェクトレベル CRUD が record レベルより先に効く**ことがある。`SELECT ... WITH USER_MODE` が `sObject type 'X__c' is not supported` で落ちたら、それはオブジェクト参照権が無い証拠(=より強い「漏洩なし」)。`try/catch(QueryException)` で「遮断されたこと」を assert する形にする。
- テストの説明は `Trace.of('正常系/異常系/エッジケース: ...')`、`Assert` 使用、ブランチ名/チケット番号は書かない(プロジェクト共通ルール)。

### 最小到達点
> 昇格サイトに **S-A / S-B** を1本ずつ(必要だった証明)＋ user-mode 据え置きサイトに **S-C**(漏れてない証明)＋ 連鎖サイトに **S-D**(過剰露出していない証明)。
> 骨格は共通で、**最後の Assert ブロックだけ意図に応じて差し替える**。他プロジェクトの v3 移行でも同じ型で再利用できる。

---

## 🔐 トリガ駆動の項目スタンプ＆レコード共有 (実案件の知見)

「活動レコード作成/更新時に親や User の値を子へスタンプし、関係者にレコード共有する」系で踏んだ罠と型。

### 標準活動(Event/Task)の共有は制御できない → カスタムオブジェクト化
標準活動(Event)の共有は **owner / WhatId に固定**で「特定 User(アポインター等)にだけ共有」ができない。細粒度共有が要件なら **カスタムオブジェクト + OWD=Private + Apex 管理共有** にするの Event→SalesActivity__c 逆移行の動機)。

### 項目スタンプ (before-insert/update)
- 親(取引先)や作成者(`OwnerId`=作成時は作成者 / `CreatedById`)の値を子へ焼くのは **before-trigger で in-memory セット**(DML 不要)。
- 親/User を **クエリして読む**ので、実行ユーザーの FLS/可視性に依存しないよう `without sharing` + `Eloquent.systemMode()`。
- **仕様が「作成/更新時」なら beforeInsert と beforeUpdate 両方**にフック。update 専用ロジックが将来入っても壊れないよう **結合テストも両経路**書く。

### Apex 管理共有 (`X__Share`) レシピ
- **前提 OWD=Private**(or ReadOnly)。ReadWrite だと __Share は無意味/作れない。
- after-insert/update の system プロセス: `without sharing` + `new Eloquent().systemMode().doInsert(shares)`。
- 項目: `ParentId` / `UserOrGroupId` / `AccessLevel`('Edit'|'Read') / `RowCause`。
- **RowCause**: カスタム Apex 共有理由が堅牢だが **sharingReasons メタが deploy で通らないことがある**(`'Xxx' does not resolve to a valid sObject type`)→ 詰まったら `RowCause='Manual'` にフォールバック(機能同等)。
- **オーナー自身へは共有不可**(SF が拒否)→ `UserOrGroupId == OwnerId` はスキップ(作成者=オーナーはオーナー権で可視)。
- **重複排除**: 同一 (record×user) で複数役割になると重複行で失敗 → Map でまとめ Edit>Read を優先。
- **更新追従**: AP/CL 変更時は当レコードの該当共有を **delete→recreate**。← この分岐は insert テストでは通らない(削除0件)ので **update の結合テスト必須**。

### 全レコードを公開グループへ = 共有ルール(宣言的)
「グループ(責任者等)が全件閲覧」は Apex でなく **owner-based 共有ルール**(`<sharedFrom><allInternalUsers></allInternalUsers></sharedFrom>` → `<sharedTo><group>Xxx</group></sharedTo>`, Read)で担保。Apex 共有は per-record の AP/CL/作成者だけ。

### ⚠️ FLS は項目追加だけでは付かない (最大の罠)
**メタデータ deploy したカスタム項目は、どのプロファイル(システム管理者すら)にも FLS が自動付与されない。** → トリガが `systemMode` でスタンプしても **ユーザーに見えない / 通常クエリで `No such column`**。「セットされてない」ように見えて実は FLS 欠如。
- 対処: 必要プロファイルに `fieldPermissions` 付与。
- **全プロファイル deploy は既存の無効タブ設定(Devops* 等)で失敗しがち** → **FLS だけの部分プロファイル**を deploy(加算的・他設定に触れない)。

## 🧪 共有/FLS の結合テスト レシピ (追加)
- **可視性は `UserRecordAccess` で assert**(runAs 不要・確実): `SELECT RecordId, HasReadAccess, HasEditAccess FROM UserRecordAccess WHERE RecordId=:id AND UserId=:u`。**SELECT に書けるのは RecordId / Has*Access / MaxAccessLevel のみ(UserId は WHERE 専用・RecordId 必須)**。
- **FLS の red→green**: `System.runAs(user){ [SELECT F__c FROM X WHERE Id=:id WITH USER_MODE] }`。FLS 無しなら `QueryException(No such column)` で落ちる→付与で緑。
- **共有ルール(公開グループ)可視**: `GroupMember`(group=公開グループ, user) を `runAs(self)` で insert(mixed DML 回避)→ レコード作成を `Test.startTest/stopTest` で挟み非同期共有再計算を flush → `UserRecordAccess.HasReadAccess` を assert。
- **共有付け替え**: 親 AP 変更→子 update→ 新 AP の `X__Share`(AccessLevel='Edit')が在り、旧 AP は `HasReadAccess=false` を assert。
- **可視性は両面ペアで**: 「見えるべき人(グループ/AP/CL/オーナー)が見える」＋「部外者が見えない」。
- **判定ロジック(不可逆/共有の副作用)は実トリガを通す結合テストで end-to-end 検証する**。単体テスト(MockEloquent/stub)は *実装の誤りに合わせて緑になりうる* —で `canApply` を誤実装したまま単体は全緑、実 DML(トリガ→recalc)では後退ブロックが効かない不具合が出た。`SOrchestrator` で実レコードを insert し、取引先ステータスが期待通り維持/変化することを Assert するケースを必ず1本。

### ⚠️ テストクラスは `.cls-meta.xml` 必須 (沈黙スキップの罠)
`.cls` だけで `-meta.xml` を作り忘れると **sfdx は黙ってそのクラスをデプロイ対象から外す**。同名の org 限定レガシークラスがあると上書きされず動き続け、「自分のテストのはずが別物が走る」混乱になる。新規 Apex は必ず .cls + -meta.xml セットで。
