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

---
name: apex-eloquent
description: Use this skill when working with the ApexEloquent ORM in Salesforce Apex — building SOQL via the Scribe query builder (Scribe.of / .field() / .whereEqual() / parentField / withChildren / aggregate), executing queries/DML through the IEloquent DI abstraction (Eloquent in prod, MockEloquent in tests), and wrapping records with IEntry / Entry / MockEntry. Fires on keywords: Scribe.of, .field(), .whereEqual(), .whereIn(), .ignoreWhen(), getAsSObject / firstAsSObject, IEntry, Entry, MockEntry, IEloquent, Eloquent, MockEloquent, label() / attach() / failOn*, userMode() / systemMode(), ApexEloquent v3, SOQL/DML の DI. ApexEloquent を使った Usecase 実装・単体テスト作成時の実用リファレンス (v1〜v3 のバージョン差分早見付き)。
---

# ApexEloquent 実用リファレンス

ApexEloquent は「クエリ構築 (`Scribe`) と実行 (`IEloquent`) の分離」が中核 (Query Delegation Pattern)。Usecase 側でクエリを組み立て、実行は内蔵 Repository (`IEloquent`) に委譲する。これにより Selector の肥大化を避けつつ、`MockEloquent` 差し替えで DB なし単体テストが書ける。

3 つのコア:
- **`Scribe`** — immutable な SOQL ビルダー。各メソッドが新インスタンスを返す
- **`IEloquent`** (`Eloquent` / `MockEloquent`) — SOQL/DML 実行の DI 抽象
- **`IEntry`** (`Entry` / `MockEntry`) — SObject / AggregateResult 共通のレコードラッパー

---

## 🔢 まずバージョン確認 (プロジェクトごとに v1〜v3 が混在)

ApexEloquent は submodule 導入のため **プロジェクトごとにバージョンが違う**。コードを書く前に必ず確認する:

```bash
git -C force-app/main/default/classes/ApexEloquent describe --tags
```

基本形 (Scribe / IEntry / MockEntry / MockEloquent / `firstOrFail`) は全バージョン共通。**上位バージョンの機能は下位に存在しない** — 書く前に下表で確認:

> 🔁 **ただし「v3 の機能 = v3 必須」ではない。破壊的でない機能は v2 系にもバックポートされる。**
> 例: `ignoreWhen()` / `fetchedBy()` / `failSave()` / `firstOrFail(Scribe, Exception)` は v3 が初出だが **v2.2.0 で v2 系にも入っている**。
> v3 専用なのは **破壊的変更そのもの (secure-by-default = `inherited sharing` + デフォルト `USER_MODE`)** だけ。
> 「この API を使いたいから v3 へ上げる」と判断する前に、**必ず submodule の実タグを見る** — v2 系の最新で足りることが多い:
> ```bash
> git -C force-app/main/default/classes/ApexEloquent fetch --tags
> git -C force-app/main/default/classes/ApexEloquent tag --sort=v:refname | grep '^v2'
> git -C force-app/main/default/classes/ApexEloquent grep -n "public .*ignoreWhen" v2.3.0 -- '*.cls'
> ```
> v2 系のプロジェクトは、**v3 へ上げずに v2 系の最新へ上げるのが第一選択** (破壊的変更を飲まずに新機能が入る)。

| バージョン | そのバージョンで増えた機能 (累積) |
|---|---|
| **v1.2.2** | 起点が **`Scribe.source(Account.SObjectType)`** (`Scribe.of(Type.class)` は無い)。`failOn*` / MockEntry `alias` も無い |
| **v1.2.2+15** | + `Scribe.of()` / `MockEntry.of()` / `failOn*` / MockEntry `alias`・`set` 拡充・エラーハンドリング (`ApexEloquentException`) |
| **v2.0.0** | + `whereIn` が Set/全プリミティブ直渡し対応 (それ以前は List 詰め替えが要る)、`MockEntry.asAggregateResult()`、`rawSoql()` |
| **v2.1.0** | + `label()` / `attach()` / `whenLabel()` / `upsertedRecordsAt()` / strict mode、`doUpsertByExternalId`、`doUpdate(..., allOrNone)`、MockEntry の typo 即例外検知 |
| **v3.0.0** | + **secure-by-default**: `Eloquent` が `inherited sharing` 化・デフォルト `AccessLevel.USER_MODE`。`userMode()` / `systemMode()`、`Scribe.ignoreWhen()`、`getThrough`/`getChildren` の名前解決統合 |
| **v2.1.1 / v3.0.1** | 🐛 patch (2026-07-30): `MockEntry.setParent(field, null)` 後の `getParent` が NPE → **null を返す** よう修正 (`Entry.getParent` と挙動一致、`getThrough` 経由も同時修正)。**「親がいない」分岐の単体テストが書けるようになった** — `setParent(field, null)` で親なしをモックしてよい。v2 系は保守ブランチ `v2.1.x` から v2.1.1 を取る (v3 破壊的変更を飲まずに済む) |
| **v2.2.0** | ✨ (2026-08-02) **v3 の非破壊機能をまとめて v2 へバックポート**: `ignoreWhen()` / `orCondition()` no-op / `MockEntry.fetchedBy(Scribe)` / `MockEloquent.failSave()` / `firstOrFail(Scribe, Exception)` / `MockEntry.getParent` の null 修正。⚠️ **未 attach ラベルへの読み取りが例外になる (テスト時 auto-strict)** も同時に入る — 上げた瞬間に「attach 漏れで空振りしていたテスト」が赤くなりうる (下記 v3.2.0 の項と同じ挙動) |
| **v2.3.0** | 🐛 (2026-08-06) `MockEloquent` のラベル別 FIFO エラーキュー / `attach`・`failSave`・`whenLabel` がコピーを返すよう immutability 統一 / `copy()` が `saveErrorsById` を落とすバグ修正 |
| **v3.0.2** | 🐛 patch (2026-07-30): `orCondition().where...().ignoreWhen(true)` で **OR-only モードが残留するバグを修正**。v3.0.1 以前では OR 条件を ignore した後に素の `where...()` が例外になり、回避に `orCondition()` を足すと **AND のつもりが黙って OR になる** — この組み合わせを書くなら v3.0.2 必須 |
| **v3.1.0** | ✨ (2026-07-30): **条件が1つも無い時の `orCondition()` は no-op** に (旧: 先頭 OR は例外)。`.whereIn('X', a).ignoreWhen(a.isEmpty()).orCondition().whereIn('Y', b).ignoreWhen(b.isEmpty())` が**完全に分岐なし**で書ける — 両方あれば OR / 片方消えれば残りが単独条件 / 両方消えれば WHERE なし |
| **v3.2.0** | ⚠️ (2026-07-31): **未 attach ラベルへの get/first/firstOrFail が例外に** (テスト実行時自動 strict、エラーに attach 済みラベル一覧)。0件経路は `.attach(label, new List<IEntry>())` の明示 attach で宣言 (従来どおり有効)。DML・ラベルなしレガシーモードは無変更。**昇格時に赤くなるテストは「attach 漏れで何も検証せず緑だった」テスト** — 機械的に空 attach を足さず、本来注入すべきデータを確認する |
| **v3.3.0** | ✨ (2026-08-02): **`MockEntry.fetchedBy(scribe)`** (直接注入エントリに SELECT 契約を1ステップで焼く — `new MockEntry(sobj, fieldStructure)` 手組みの後継) / **`MockEloquent.failSave(id, msg)`** (allOrNone の部分失敗 SaveResult を偽装、`autoId` と対) / **`firstOrFail(scribe, exception)`** (0件時に渡した業務例外をそのまま throw) / 実行層の成功パステスト (Group/GroupMember)。⚠️ IEloquent にメソッド追加 (独自実装者は対応要) |
| **v3.4.0 / v2.3.0** | 🐛 (2026-08-05): `MockEloquent` の**ラベル別 FIFO エラーキュー** (同一ラベルに複数の失敗を積める) / `attach`・`failSave`・`whenLabel` が**コピーを返すよう immutability を統一** / `copy()` が `saveErrorsById` を落とすバグ修正 / 例外時に**ラベルを解放** (以前は失敗してもラベルが消費済みになりリトライが書けなかった) / strict モードの到達不能設定ガード / `whenLabel` の引数ガード |
| **v3.4.1** | 🐛 (2026-08-06): `AbstractEntry.describeResult` を **`transient`** 化。`Schema.DescribeSObjectResult` はシリアライズ不可なので、**`IEntry` を `Database.Stateful` のバッチ状態に載せるとチャンクをまたぐ時点で `SerializationException`** になっていた。しかも**キャッシュの温まり具合で発生が変わる** (その型で最初にキャッシュミスしたインスタンスだけが値を持つ) ため、同じコードが通ったり落ちたりした。あわせて未知項目診断の NPE 2 箇所を修正 |
| **v3.5.0** | 🐛 (2026-08-06): **① null 値エラーを組み立て時まで遅延** — `whereGreaterThan` / `whereIn` など **null を受け付けない 12 メソッド**はチェーン時点で即例外だったため、後続の `ignoreWhen()` に到達できなかった。v3.5.0 から `ignoreWhen(true)` で取り下げられる (`whereEqual` は対象外。`X = null` は正当な SOQL)。**② AND/OR 混在を組み立て時にガード** — OR-after-AND がすり抜け、実行時に SOQL 側が `unexpected token: OR` で落ちていた。**③ 複数条件の `parentCondition` を括弧で囲む** — 従来は `A AND B OR C` という実行不能な SOQL を生成しており、`toSoql()` は成功するのに実クエリだけが失敗していた |

本 SKILL.md の本文は **v2.1 以降を前提**に書いてある (v3 固有箇所は明記)。v1〜v2.0 のプロジェクトでは上表の無い機能を使わないこと (例: v1 系では `label()` が無いので依存は用途別に複数本 DI する)。

⚠️ **v3 の破壊的変更** (v2 以前から上げる時):
1. `Eloquent` が `with sharing` → **`inherited sharing`** — 呼び出し元の共有コンテキストを継承するようになった。共有を外したい system プロセスは呼び出し元クラスを `without sharing` にする
2. SOQL/DML がデフォルト **`USER_MODE`** (FLS・オブジェクト権限を強制) — 旧挙動 (FLS 無視) を維持するには `.systemMode()` を明示
3. `getChildrenByRelationName` / `getThroughByRelationName` は **@deprecated** (`getChildren` / `getThrough` に統合)

昇格判断 (`without sharing` × `.systemMode()` をどこに付けるか) と v3 移行手順・runAs 監査テストは **Skill ツールで `apex-access-mode` をロードして従う**こと。

---

## 🧊 大前提: これらの OSS は **すべてイミュータブル** で設計されている

`Scribe` / `MockEntry` / `MockEloquent` / `SBlueprint` は **どのメソッドも自分を書き換えず、新しいインスタンスを返す**。
したがって **戻り値を受け取らない呼び出しは、何もしなかったのと同じ**:

```apex
// ❌ 何も起きない。set の戻り値が捨てられている
MockEntry card = MockEntry.of(BusinessCard__c.class).autoId(1);
card.set('ResolvedDept__c', deptId);          // ← このレコードに部署は入らない
card.set('MatchStatus__c', '未処理');          // ← これも入らない

// ✅ チェーンするか、必ず代入する
MockEntry card = MockEntry.of(BusinessCard__c.class).autoId(1)
  .set('ResolvedDept__c', deptId)
  .set('MatchStatus__c', '未処理');
```

**これはテストが「なぜか値が入っていない」形で落ちるので発見しにくい。** 逆に、アサーションが緩いと
*落ちずに素通りする*ので質が悪い (実例: 単独文の `set` を書いた 5 テストが全部
「所有者部署が不明」になり、原因の特定に時間を取られた)。

> 🚨 **イミュータブルでない挙動を見つけたら、それは「そういう仕様」ではなく OSS 側の不具合と考える。**
> 呼び出し側で回避策を書く前に submodule の実装を確認し、直すべきは OSS 側かを判断すること。
> (作者はユーザー本人なので、フレームワーク側を直すのが正しい選択肢になりうる)

---

## 🛑 最初に守る 2 つ (レビューで毎回指摘されている)

### 1. `getAsSObject` を使わない。`IEntry` で揃える

**`SObject` に落とした瞬間、その先で SELECT 漏れ検知が死ぬ。** 受け取ったメソッド/クラスが未 SELECT の項目を触っても `null` が返るだけで、テストも本番も静かに通る。`IEntry.get()` なら `SObjectException` / `ApexEloquentException` で即落ちる。

```apex
// ❌ 「今はこっちが書きやすい」で始まると歯止めが効かなくなる (割れ窓理論)
List<EstimateItems__c> items = (List<EstimateItems__c>) eloquent.getAsSObject(scribe);
LineAmount.stamp(items[0], rule);   // stamp が TaxCategory__c を get → SELECT し忘れても素通り

// ✅ IEntry のまま渡す。stamp(IEntry, TaxRule) が未 SELECT を叩き落とす
List<IEntry> items = eloquent.get(scribe);
LineAmount.stamp(items[0], rule);
```

派生する定石:
- **値を変えて `doUpdate`/`doUpsert` するなら、取得した `IEntry` を `put()` してそのまま渡す。** `IEloquent` の DML は `IEntry` / `List<IEntry>` を受ける。`new Foo__c(Id = e.getId(), X__c = v)` と Id だけの SObject を組み直さない
- **新規レコードも `new Entry(new Foo__c(...))` で包んで** `doUpsert(List<IEntry>)` に渡す
  — ⚠️ これは **upsert の話であって insert には広げない**。理由は下記「`doInsert` に `IEntry` 版が無い理由」
- **before トリガの `Trigger.new` も `new Entry(record)` で包んでから**共通ロジックへ渡す。`put()` は元のレコードに書き戻るので in-place 更新はそのまま効く
- **他のクラス/メソッドの引数型を `SObject` にしない。** 型付き SObject (`EstimateItems__c`) でも同じ穴が開く

**`getAsSObject` / `firstAsSObject` / `firstOrFailAsSObject` が許されるのは、標準 API が `SObject` を要求する時だけ** (`Database.SaveResult` 系、`Approval.process`、`Messaging` 等)。「キャストが面倒」「今は SObject の方が速く書ける」は理由にならない。

### 2. `whereIn` / `whereNotIn` に `Set` をそのまま渡す (v2.0.0+)

第2引数は `Object`。プリミティブなら `Set` でも `List` でも受ける。**詰め替えは不要。** (v1 系のみ List 詰め替えが必要 — 冒頭のバージョン表参照)

```apex
// ❌ 無駄な詰め替え
.whereIn('Id', new List<Id>(itemIds))
.whereIn('Id', new List<Id>(recordsById.keySet()))

// ✅
.whereIn('Id', itemIds)
.whereIn('Id', recordsById.keySet())
```

---

## Scribe (クエリビルダ)

`Scribe.of(SObjectType.class)` から始めてチェーン。フィールド名は **文字列**。**immutable なので分岐時は再代入** (`scribe = scribe.whereXxx(...)`)。

```apex
List<String> accountFields = new List<String>{ 'Id', 'Name', 'Industry' };  // 3項目以上は先に List で定義
Scribe accountScribe = Scribe.of(Account.class)
  .fields(accountFields)
  .whereEqual('Industry', 'Technology')
  .whereGreaterThan('Amount', 1000)
  .whereIn('OwnerId', ownerIds)        // Set / List をそのまま。全プリミティブ受けなのでキャスト不要。空は Id = null (=必ず偽)
  .orderBy('Name', 'ASC')
  .take(10);
List<IEntry> accounts = new Eloquent().get(accountScribe);
```

### SELECT の書き方 (`field` / `fields` の使い分け)
選択項目数で書き分ける (可読性・diff の一貫性):
- **2 項目まで**: `field('Id').field('Name')` と `field()` を連ねる。インラインの `.fields(new List<String>{ 'Id', 'Name' })` は使わない。
- **3 項目以上**: **先に `List<String>` を変数へ定義**し、それを `fields(...)` に渡す。`.fields(new List<String>{ ...長い... })` をチェーン中にインラインで埋めない。1 メソッドに複数クエリがあるときは `cardFields` / `accountFields` のように用途別の名前にする。
```apex
// ✅ 2項目
Scribe q = Scribe.of(Account.class).field('Id').field('Name').whereEqual('Id', accId);

// ✅ 3項目以上は先に List 定義 → fields() に渡す
List<String> cardFields = new List<String>{ 'Id', 'CompanyName__c', 'MatchStatus__c', 'MatchTier__c' };
Scribe cardQuery = Scribe.of(BusinessCard__c.class).fields(cardFields).whereEqual('Id', cardId);
```

### WHERE の勘所
- 連続 `whereXxx` はデフォルト **AND**。
- **OR は `orCondition()` を次の where の前に挟む**。一度 OR を入れたら以降すべて OR (AND に戻せない)。AND/OR 混在は `whereGroup(Scribe.asGroup()...)` で片側を括弧化。
- **`orCondition()` は条件ゼロなら no-op (v3.1.0+)**: `ignoreWhen` で先頭条件が消えても落ちず、残った側が単独条件になる。「A または B、どちらも消えるかも」が if 分岐なしで書ける (v3.0.2 以前はこの形が例外になるので注意)。
- 🛑 `whereIn` / `whereNotIn` の値は `Set` でも `List` でも、**全プリミティブ型を受ける (`Object` 引数)。`new List<Id>(someSet)` のような詰め替え・キャストは不要** — `Set` / `keySet()` をそのまま渡す (冒頭「最初に守る 2 つ」参照)。
- ⚠️ **空コレクションの扱いは非対称**: `whereIn` に空を渡すと**常に偽の条件** (0件になる) だが、`whereNotIn` に空を渡すと**条件ごと無視** (絞り込みなし=全件側に倒れる)。「空なら全件返したい/除外なしにしたい」意図なら `whereIn(...).ignoreWhen(ids.isEmpty())` (v3) か if 分岐で明示する。
- `whereIn` の第2引数に **別の `Scribe`** を渡すと `IN (SELECT ...)` サブクエリになる (`whereNotIn` も同様)。
- `whereLike` は `'` を自動エスケープ (SOQL インジェクション安全)。
- その他: `whereNotEqual` / `whereLessThan(OrEqual)` / `whereNotLike` / `whereIncludes` / `whereExcludes` / `whereNull` / `whereNotNull`。
- **`ignoreWhen(Boolean)` (v3.0.0+)**: true なら**直前の `where...()` 条件を破棄**する。「値が空なら条件を付けない」の if 分岐・再代入を 1 チェーンに畳める (空 `IN ()` = 常に 0 件の罠も回避)。
  ```apex
  // ❌ v2 まではこう書いていた
  if (!ids.isEmpty()) scribe = scribe.whereIn('Id', ids);
  // ✅ v3
  Scribe scribe = Scribe.of(Opportunity.class).field('Id')
    .whereIn('Id', ids).ignoreWhen(ids.isEmpty())
    .whereLike('Name', keyword).ignoreWhen(String.isBlank(keyword));
  ```
  🛑 **`where...()` の直後にしかチェーンできない** — 先頭・`orderBy()` の後・2 連続で呼ぶと `ApexEloquentException`。破棄対象は**直前の 1 条件だけ** (`whereGroup` の直後ならそのグループ全体)。

### 動的組み立て
```apex
Scribe scribe = Scribe.of(Opportunity.class).field('Id').field('Name');
if (industry != null)            scribe = scribe.whereEqual('Industry', industry);
if (stages != null && !stages.isEmpty()) scribe = scribe.whereIn('StageName', stages);
```
immutable なので 1 本の `baseScribe` から件数用 `baseScribe.count('Id','cnt')` と一覧用 `baseScribe.field(...).take(50)` を派生させても干渉しない。

### 📐 Scribe は必ず先に変数へ定義してから IEloquent に渡す (インライン禁止)
`eloquent.get(Scribe.of(...)...)` のように Scribe を実行メソッドの引数へ**インライン**で埋めない。**一度ローカル変数(`cardQuery` 等)に定義**してから `get`/`first`/`doDelete` に渡す。クエリの「定義」と「実行」が視覚的に分離され、diff・レビュー・組み立ての追い方が楽になる。
```apex
// ❌ インライン
IEntry card = this.eloquent.label(LBL_CARD).first(
  Scribe.of(BusinessCard__c.class).field('Id').whereEqual('Id', this.cardId));

// ✅ 定義 → 実行
Scribe cardQuery = Scribe.of(BusinessCard__c.class).field('Id').whereEqual('Id', this.cardId);
IEntry card = this.eloquent.label(LBL_CARD).first(cardQuery);
```

### 並び替え / LIMIT / ロック
`orderBy(f[, 'ASC'|'DESC'[, 'FIRST'|'LAST']])` — ⚠️ 3 つ目は **`'FIRST'`/`'LAST'` だけ**を渡す (フレームワークが `NULLS ` を前置する)。`'NULLS LAST'` と書くと `ApexEloquentException` / `take(n)` / `offset(n)` (最大2000) / `forUpdate()`。**`forUpdate` は `orderBy` / `offset` と併用すると組み立て時に例外**。

### 集計クエリ
`count` / `countDistinct` / `sum` / `average` / `max` / `min` の 6 種。**`alias` は必須引数** (`expr0` の罠回避)。取得は通常の `get(scribe)` のまま (集計判定は自動)。

```apex
Scribe eventScribe = Scribe.of(Event.class)
  .field('WhatId')               // GROUP BY する field は SELECT にも明示
  .count('Id', 'eventCount')
  .whereIn('WhatId', oppIds)
  .groupByField('WhatId');
for (IEntry e : new Eloquent().get(eventScribe)) {
  Id whatId = (Id) e.get('WhatId');
  Integer cnt = ((Decimal) e.get('eventCount')).intValue();  // 集計結果は Decimal 型
}
```
- GROUP BY: `groupByField` / `groupByFields` / `groupByParent(Scribe.asParent(...).groupByField(...))`、HAVING: `havingCondition(Scribe.asHaving().whereGreaterThan('alias', v))`。
- 親フィールドの集計/GROUP BY は `field('Account.Industry')` 不可。`Scribe.asParent(...)` 経由で。
- 同じ alias の重複は例外。`withChildren` と集計の併用は不可。

### static ファクトリ早見表
`Scribe.of(Type)` 起点 / `asParent('AccountId')` (parentField/parentCondition/groupByParent) / `asChild(Type)` (withChildren) / `asGroup()` (whereGroup) / `asHaving()` (havingCondition) / `asThrough(Junction, 'relatedKey')` (through)。
検査用: `toSoql()` (デバッグ) / `isAggregate()`。

---

## リレーション

```apex
// 親項目: parentField → getParent
Scribe oppScribe = Scribe.of(Opportunity.class)
  .field('Id')
  .parentField(Scribe.asParent('AccountId').field('Name').field('Industry'));
IEntry accountEntry = oppEntry.getParent('AccountId');

// 子サブクエリ: withChildren → getChildren (取得キーはオブジェクト名 'Opportunity'、複数形/__r 不要)
Scribe accScribe = Scribe.of(Account.class).field('Id')
  .withChildren(Scribe.asChild(Opportunity.class).field('Id').field('StageName'));
List<IEntry> opps = accountEntry.getChildren('Opportunity');

// 親条件で絞る (SELECT には出さない): parentCondition
.parentCondition(Scribe.asParent('OpportunityId').whereLike('Name', 'Test%'))

// 多対多 (Junction): asThrough → getThrough。フィールド/WHERE は通過先基準で書く
Scribe.of(Order.class).field('Id')
  .through(Scribe.asThrough(OrderItem.class, 'Product2Id').field('Name').whereEqual('IsActive', true));
List<IEntry> products = orderEntry.getThrough('OrderItem', 'Product2Id');
```
- `withChildren` を 2 回続ければ並列子サブクエリ、ネストで子の子 (最大 4 レベル)。
- 同一オブジェクトへの lookup が複数ある等で曖昧な時は `relationName('CustomOpportunities__r')` を明示し、取得側 (`getChildren` / `setChildren`) も同じキーを使う。
- v3.0.0+ では `getChildren` / `getThrough` が **SObject 名・リレーション名どちらでも解決** する (SObject 名優先 → リレーション名フォールバック)。旧 `getChildrenByRelationName` / `getThroughByRelationName` は **@deprecated** — 新規コードで使わない。
- **`parentField` / `withChildren` し忘れた親子に `getParent`/`getChildren` するとテスト時 (MockEntry 経由) で例外** = SELECT 漏れをテストで検出。

### 🚀 親子で畳めるクエリは畳む — 段階降りの `whereIn` を避ける
階層を下る集計 (商談→見積→納品→請求 等) を **各段ごとに `whereIn(field, Set)` で個別取得すると、クエリ数が「読む SObject の種類数」ぶん積み上がる** (4 段 = 4 SOQL)。トリガー再入があると 1 パスの SOQL がそのまま再入回数だけ乗算され、バルクで 1 トランザクション 100 SOQL のガバナに触れる。**親子でまとめられるものは `withChildren` (子サブクエリ) と `asParent`/親パス条件で 1 パスあたりのクエリ数を最小化する。**

```apex
// ❌ 段階降り: 見たい SObject の種類数だけ SOQL が出る (Opp/Estimate/Delivery/Invoice = 4 SOQL)
List<IEntry> opps = e.label('o').get(Scribe.of(Opportunity.class).whereIn('Id', oppIds));
List<IEntry> ests = e.label('e').get(Scribe.of(Estimate__c.class).whereIn('Opportunity__c', oppIds));
// …Delivery, Invoice と続けるほど SOQL が増える

// ✅ 親＋子を子サブクエリで畳む (1 SOQL)
Scribe s = Scribe.of(Opportunity.class).field('Id').field('PhaseStatus__c')
  .withChildren(Scribe.asChild(Estimate__c.class).field('Confirmed__c'));
for (IEntry opp : e.label('o').get(s)) {
  for (IEntry est : opp.getChildren('Estimate__c')) { /* … */ }
}
```
- **SOQL の子サブクエリは 1 階層のみ** (孫は同一 SOQL に畳めない)。深い木は「子サブクエリ + 親パスで絞る WHERE (`Delivery__c WHERE Estimate__r.Opportunity__c IN :oppIds` 等)」を組み合わせ、**種類数ぶんの `whereIn` ではなく親子ペアぶんの数クエリ**に抑える (例: 4 階層を 2 SOQL で読む)。
- **畳んでも `label().get()` は 1 コール = MockEloquent の継ぎ目は 1 つ**。子は `MockEntry.setChildren(...)` で注入し `IEntry.getChildren(...)` で読む → **効率とテスタビリティは両立する** (段階降り `whereIn` にする理由にはならない)。
- ⚠️ 子サブクエリ (`withChildren`) と集計 (`sum`/`count` 等) は同一 SOQL に同居不可 (Scribe が例外)。集計する段は別クエリに分ける。
- 目安: **「1 パスの SOQL 数を、読む SObject の種類数に比例させない」**。トリガー再入で乗算されるので、1 パスを 4→2 に削るとバルク全体のガバナ余白に効く。**この非効率は Mock 単体では見えない** (実 SOQL を発行しないため) → 下記のバルク結合テストで担保する。

---

## IEloquent / Eloquent / MockEloquent (実行層の DI)

Usecase は `IEloquent` 型に依存し、本番 `new Eloquent()` / テスト `new MockEloquent(...)` を Layered Constructor で受ける。

### 取得系
`get(scribe)` → `List<IEntry>` (0件は空) / `first(scribe)` → 0件 null / `firstOrFail(scribe)` → 0件で例外。
🛑 `getAsSObject` / `firstAsSObject` / `firstOrFailAsSObject` は **標準 API が SObject を要求する時だけ**(冒頭「最初に守る 2 つ」参照)。`rawSoql(soql)` も Scribe で書けない時のみ (**SELECT 漏れ検知が無効化**)。

「0件を業務エラーとして画面に返したい」ときは **`firstOrFail(scribe, new UsecaseException('...'))` (v3.3.0+)** — 渡した例外がそのまま throw される。v3.2.0 以前では `first` + null 判定 + 自前 throw で書く (引数なし `firstOrFail` は `ApexEloquentException` を投げ、`UsecaseException` に化けさせられない)。

### DML 系
`doInsert` / `doUpdate` / `doUpsert` / `doDelete`。**bulk (List) 版が基本**、単件版は常に 1 件確定時のみ。**`IEntry` を持っているなら `IEntry` 版に渡す** (SObject に戻さない)。`doUpsert(List<IEntry>)` は採番後の Id が入った `IEntry` を返すので、`entry.getId()` で新規レコードの Id を拾える。

#### 🛑 `doInsert` に `IEntry` 版が無いのは意図的 (SObject で渡す)

オーバーロードは対称ではない。**これは設計判断であって漏れではない**:

| メソッド | `SObject` | `List<SObject>` | `IEntry` | `List<IEntry>` |
|---|:---:|:---:|:---:|:---:|
| `doInsert` | ✅ | ✅ | ❌ | ❌ |
| `doUpdate` | ✅ | ✅ | ✅ | ✅ |
| `doUpsert` | ✅ | ✅ | ✅ | ✅ |
| `doDelete` | ✅ | ✅ | ✅ | ✅ |

**`IEntry` が要るのは「レコードのライフサイクルが混ざる」操作だけ**:
- `doUpsert` — クエリで取った既存 (`IEntry`) と新規が**同じリストに混ざる**。だから `IEntry` で揃える必要がある
- `doUpdate` / `doDelete` — 対象は必ずクエリ由来なので `IEntry` を持っている
- **`doInsert` — 対象は定義上すべて「まだ存在しないレコード」**。その場で `new Foo__c(...)` した SObject に SELECT の概念は無く、`IEntry` に包んでも未 SELECT 検知は効きようがない。包む理由が原理的に無い

```apex
// ❌ 過剰適用。新規レコードを Entry で包んでも得るものが無い (そもそもコンパイルが通らない)
List<IEntry> inserted = eloquent.doInsert(new List<IEntry>{ new Entry(new Contact(LastName = '山田')) });

// ✅ insert は SObject で渡す。戻り値は採番後の Id が入った SObject
Contact contact = new Contact(LastName = '山田', AccountId = accountId);
List<SObject> inserted = eloquent.label(LBL_INSERT).doInsert(new List<SObject>{ contact });
Id contactId = inserted[0].Id;   // Id は全 SObject 共通なのでキャスト不要
```

> 💡 迷ったら「**このレコードはクエリ由来か？**」で判断する。Yes なら `IEntry`、その場で `new` したなら `SObject`。
> 「常に `IEntry` で揃える」は取得・更新の話であって、insert まで広げると存在しないオーバーロードを探すことになる。
- `doUpdate(records, allOrNone)` は `Database.SaveResult` を返すので partial-success のエラー集約を検証できる。
- `doUpsertByExternalId(records, extIdField, allOrNone)` で外部 ID upsert (ETL/増分同期向け)。

### userMode() / systemMode() (v3.0.0+)
v3 はデフォルト **`USER_MODE`** (FLS・CRUD・共有を実行ユーザー基準で強制)。system プロセス (集計/焼付/移行など、誰が起こしても完遂すべき処理) だけ `.systemMode()` で明示的にオプトアウトする:

```apex
// system プロセス: クラスを without sharing にした上で systemMode を明示
this.eloquent.systemMode().label(LBL_UPDATE).doUpdate(entries);
```
- **sticky**: 一度呼ぶとそのインスタンスの以降の全操作に効く (`label()` と違い操作ごとにリセットされない)
- `MockEloquent` では **no-op** (そのまま自身を返す) — 単体テストはモード無関係に書ける
- 🛑 `.systemMode()` は FLS だけ外す。**共有も外すにはクラス側を `without sharing` にする必要がある** (2軸は独立)。どちらを付けるかの判断基準・移行手順・runAs 監査テストは **Skill `apex-access-mode` をロード**して従う

### label() でラベル多重化 (v2.1+、推奨)
1 本の `IEloquent` を用途別にラベルで仕分ける。用途別に複数 DI する代わりに 1 本にまとめられる。

```apex
List<IEntry> opps = this.eloquent.label(LBL_FETCH).get(oppScribe);
for (IEntry o : opps) { o.put('Industry__c', 'Technology'); }
this.eloquent.label(LBL_UPDATE).doUpdate(opps);   // IEntry のまま更新できる
```
- **opt-in strict mode**: 一度でも `.label()` を呼ぶと以降そのインスタンスは全操作にラベル必須 (ラベル忘れ・同一ラベル2回は例外)。一度も呼ばなければ従来通り lenient。
- `'default'` は予約語、null/空文字は例外。
- ラベルは **操作が例外で失敗しても消費される** (`failOn*` と組む時、同一ラベルでのリトライ再実行は consume-once 例外になる)。`label('a').label('b')` と重ねると最後の `'b'` だけ消費され `'a'` は未消費のまま。

### Usecase での受け方 (Layered Constructor Pattern)
public は業務入力のみ、`@TestVisible private` で依存を受け null-coalescing で本番デフォルト化。

```apex
public with sharing class CopyAccountIndustryToOpportunityUsecase {
  @TestVisible static final String LBL_FETCH = 'oppFetch';
  @TestVisible static final String LBL_UPDATE = 'oppUpdate';
  private final Set<Id> opportunityIds;
  private final IEloquent eloquent;
  private Trace t = Trace.of('商談に親取引先の業種をコピー');

  public CopyAccountIndustryToOpportunityUsecase(Set<Id> opportunityIds) {
    this(opportunityIds, null);
  }
  @TestVisible
  private CopyAccountIndustryToOpportunityUsecase(Set<Id> opportunityIds, IEloquent eloquent) {
    this.opportunityIds = opportunityIds;
    this.eloquent = eloquent ?? new Eloquent();   // null なら本番デフォルト
  }
  public void invoke() { /* ... */ }
}
```
- `IEloquent` 単体なら **ラベル多重化で 1 本に** まとめるのが v2.1+ 推奨 (旧来は 2 本フィールド DI でコンストラクタが太る)。
- 異種依存 (`IEloquent` + Reader/Validator/Mapper) はフィールドごとに分割 DI。各部品も `new AccountReader(new Eloquent())` のように生焼けを作らず DI で完成させる。

---

## MockEloquent (単体テスト)

`Scribe` の WHERE は **評価しない**。コンストラクタ/`attach` で渡したリストをそのまま返す。条件違いのクエリはラベルか用途別 DI で区別する。

### コンストラクタとプリロード
`new MockEloquent()` (空) / `new MockEloquent(entry)` (1件) / `new MockEloquent(List<IEntry>)` (複数)。
ラベル版は `.attach(label, entry | List<IEntry>)` で各ラベルに個別プリロード。同一ラベルへの 2 度目の `attach` は**上書き**。
- **attach していないラベルで `get`/`first`/`firstOrFail` すると例外** (v3.2.0+、attach 済みラベル一覧付き)。0件経路をテストしたい時は `.attach(label, new List<IEntry>())` で**空を明示 attach** する。v3.1.0 以前は例外ではなく**沈黙して空リスト**が返り「0件経路のテストが誤って緑」になる罠だった — 旧バージョンではラベル名の定数 (`Usecase.LBL_FETCH`) を使い文字列を手打ちしないことで防衛する。

### Spy (DML 検証)
- `upsertedRecordsAt(label)` → `List<SObject>` (insert/update/upsert 累積)、`deletedCountAt(label)` → `Integer`。
- ラベルなし時は `upsertedRecords` / `deletedCount` (= `'default'` バケット、`@deprecated`、新規は `*At('default')` 推奨)。

```apex
MockEntry oppEntry = MockEntry.of(Opportunity.class).alias('opp').autoId(1)
  .set('Name', 'Test Opp').set('Amount', 1000);

MockEloquent mock = (new MockEloquent())
  .attach(CopyAccountIndustryToOpportunityUsecase.LBL_FETCH, new List<IEntry>{ oppEntry });

(new CopyAccountIndustryToOpportunityUsecase(oppIds, mock)).invoke();

List<SObject> updated = mock.upsertedRecordsAt(CopyAccountIndustryToOpportunityUsecase.LBL_UPDATE);
Assert.areEqual(1, updated.size());
Assert.areEqual('Technology', ((Opportunity) updated[0]).Industry__c);
```

### 部分失敗: failSave (v3.3.0+)

`allOrNone` 付き DML (`doUpdate(records, false)` / `doUpsertByExternalId`) の **SaveResult/UpsertResult を部分失敗にできる**。対象は record Id で指名 — `MockEntry.autoId()` と対で使う:

```apex
Account bad = new Account(Id = MockEntry.of(Account.class).autoId(2).getId(), Name = 'x');
MockEloquent mock = (new MockEloquent()).failSave(bad.Id, '入力規則で拒否');
List<Database.SaveResult> results = mock.doUpdate(new List<SObject>{ good, bad }, false);
// results[1].isSuccess() == false / getErrors()[0].getMessage() が届く / spy には good だけ記録
```

- `allOrNone=true` で対象が含まれると **記録前に全体が throw** (実 DML の all-or-nothing と同じ意味論)
- 失敗レコードは upsert spy に載らない (保存されなかったものとして扱う)

### 異常系: failOn シリーズ
各メソッドに `failOnGet` / `failOnFirstOrFail` / `failOnDoUpdate` / `failOnDoDelete` 等。引数なし (デフォルト例外) / `Exception` 受け取り版あり。
- `.whenLabel(label)` で「このラベルの操作だけ失敗」にスコープ (直前に `failOn*` が無い状態で呼ぶと例外)。
- `.repeat()` で「以降ずっと失敗」(回数指定不可) → 「N 回リトライ後に中止」パスの検証に使う (こちらも直前に `failOn*` 必須)。
- 消費モデル: `failOnGet()` 1 回 = 最初の 1 回だけ失敗 (2 回目からは正常)。2 連続で書けば先頭 2 回失敗。カスタム例外を渡すと**そのまま** throw される (`ApexEloquentException` にラップされない)。

```apex
MockEloquent mock = (new MockEloquent())
  .failOnDoUpdate(new DmlException('Simulated failure'))
  .whenLabel(YourUsecase.LBL_UPDATE);
try {
  (new YourUsecase(input, mock)).invoke();
  Assert.fail('例外が投げられるはず');
} catch (DmlException e) {
  Assert.isTrue(TraceFlow.isLastAbort());
}
```

---

## IEntry / Entry / MockEntry (レコードラッパー)

`get(scribe)` 等は `List<IEntry>` を返す。**`SObject` に早期変換しない** — IEntry のままだと (1) 未 SELECT フィールドアクセスの偽陽性検知、(2) non-writable 項目もモック可、(3) 取得→`put`→`doUpdate` が一気通貫、(4) SObject と AggregateResult を同じ型で扱える、の 4 つの恩恵を受けられる。

### フィールドアクセス
```apex
Id id = entry.getId();              // 専用 getter (キャスト不要)
String name = entry.getName();      // 専用 getter
String ind = (String) entry.get('Industry');   // それ以外はキャスト必須
entry.put('Status__c', 'Active');   // 書き込み
```
変数名は `{SObject名}Entry` (`accountEntry`) に揃える。最終手段 `getRecord()` で内包 SObject を取り出す。
- ⚠️ `getRecord()` が返すのは**防御コピー** — 取り出した SObject を書き換えても Entry には反映されない (更新は必ず `entry.put()` で)。集計モード (`asAggregateResult`) の Entry で呼ぶと例外。

### MockEntry 構築 (DB 不要のテストデータ)
`MockEntry.set()` は **数式・ロールアップ・auto-number・親リレーションなど non-writable 項目にも値を書ける** (override map による。JSON シリアライズハック不要)。SObject に存在しないフィールド名を `set`/`setParent` 等に渡すと **即 `ApexEloquentException`** (typo を setup の瞬間に叩き出す)。

```apex
MockEntry oppEntry = MockEntry.of(Opportunity.class)
  .alias('opp').autoId(1)
  .template(new Map<String, Object>{ 'StageName' => 'Prospecting', 'Amount' => 1000 })
  .set('PriceBand__c', 'Large');     // 数式項目に直接値を入れられる
Id oppId = oppEntry.getAliasId('opp');   // 生成 Id をアサーションで使う

// 量産: {#}/{A}/{a} プレースホルダ + times(count[, startAt[, interval]])
List<MockEntry> contacts = MockEntry.of(Contact.class)
  .autoId('{#}').set('LastName', 'Contact-{#}').alias('con_{#}').times(3);

// 集計結果は SObject 型に縛られない asAggregateResult
MockEntry agg = MockEntry.asAggregateResult().set('WhatId', oppId).set('eventCount', 3);
```

### 🚫 Id を自作しない — 必ず MockEntry 経由で採番する
テストで Id が要るとき、`keyPrefix + '0'.repeat(...)` のような **偽 Id を手組みするヘルパーは絶対に作らない**。フレームワークが採番する:

```apex
// ❌ 絶対 NG — keyPrefix から偽 Id を捏造するヘルパー
private static Id fakeId(Schema.SObjectType sot) {
  return sot.getDescribe().getKeyPrefix() + '0'.repeat(12) + '1';
}

// ✅ 使い捨ての Id が 1 個欲しいだけ (attach 不要)
Id acctId = MockEntry.of(Account.class).autoId(1).getId();

// ✅ attach する行と Id を参照する行を結ぶ (alias で名前付け)
MockEntry card = MockEntry.of(BusinessCard__c.class).alias('c').autoId(1);
MockEloquent mock = (new MockEloquent()).attach(Usecase.LBL_CARD, new List<IEntry>{ card });
Usecase.Result r = (new Usecase(card.getAliasId('c'), mock)).invoke();
```

`autoId(n)` は SObjectType 正しい keyPrefix で妥当な 18 桁 Id を生成する。`.getId()` は単発、`.alias('c').getAliasId('c')` は「attach したレコードと同じ Id を後段で参照する」ときに使う (どのレコードの Id かが名前で読める)。

### MockEntry で親子をモック
```apex
MockEntry accountEntry = MockEntry.of(Account.class).alias('acc').autoId(1)
  .set('Name', 'Acme')
  .setChildren('Opportunity', new List<MockEntry>{          // キーは Scribe と同じ (relationName 指定時はその名前)
    MockEntry.of(Opportunity.class).autoId(1).set('Name', 'Opp A'),
    MockEntry.of(Opportunity.class).autoId(2).set('Name', 'Opp B')
  });

MockEntry oppEntry = MockEntry.of(Opportunity.class).autoId(1)
  .setParent('AccountId', MockEntry.of(Account.class).set('Industry', 'Technology'));
```
- 親子の Id 連結は MockEntry が自動。子は `setChildren` の引数内で **インライン定義** (構造を視覚的に見せる、再利用理由がなければ変数化しない)。
- **ネストした掛け算 (親 N × 子 M) は未対応** — 子は個別に列挙する。

### 未 SELECT 検知 (取得と実行のズレを単体テストで叩き出す)
`List<IEntry>` を受け渡す設計にすると、「呼び出し元の Scribe が SELECT していない項目を、呼ばれた側が `get()` した」ズレが**単体テストで落ちる**(素の SObject は未選択でも null を返すだけで検知できない)。`Entry.get('X')` は実クエリで未 SELECT なら `SObjectException`、`MockEntry.get('X')` は FieldStructure に無いと `ApexEloquentException "not selected in Scribe"`。

**MockEntry の FieldStructure をどう用意するかは、テストでエントリが `.get(scribe)` を通るかで決まる**(ここを取り違えると「手写しが効かない」「素だと検知しない」の混乱になる):

| テストの経路 | FieldStructure | 理由 |
|---|---|---|
| **`.get()`/`.first()` を通す**(親 Usecase の実クエリ経由 等) | **持たせない**。値だけの素の MockEntry でよい | `MockEloquent.get` が `scribe.buildFieldStructure()` を各エントリに**焼き付けて上書き**する → 本番 Scribe が唯一の真実源。SELECT の手写しは上書きされるデッドコード |
| **クラスに直接 DI**(テスト対象のコンストラクタに直接注入) | **本番の Scribe から `buildFieldStructure()` で作る** | 本番 Scribe を通らず自動焼き付けが効かない。`.set('X')` は値を入れるだけで structure に X を登録しない |

### `set()` と `put()` の役割分担 (この非対称が検知を成立させている)

| | 使う場所 | FieldStructure への登録 |
|---|---|---|
| `set()` | **テストがモックデータを組み立てる**とき | **しない** |
| `put()` | **本番コードが `IEntry` に値を書く**とき | する |

`set()` が登録してしまうと、**モックデータを作る行為そのものが未 SELECT 検知を無効化する**。
`MockEntry.of(X).set('Foo__c', v)` で `get('Foo__c')` が合法になれば、「本番の Scribe が
SELECT した範囲」という制約が消え、検知機構が丸ごと死ぬ。`set()` は登録しないのではなく
**してはいけない**。

`put()` はテストでも**本番コード側が呼ぶ** (注入された `IEntry` に production が書き込む)。
自分で書いた項目を読み返せるべきなので登録する。`Entry.put` と挙動を揃える必要もある。

### 🛑 `new FieldStructure(...)` を手で組まない — 必ず Scribe から作る

直接 DI するケースで項目名を手写しすると、**「テストを通すために項目を足したが、本番のクエリは間違ったまま」**という状態を作れてしまう。テストが本番の SELECT から独立してしまい、退行検知という目的そのものが失われる。`Scribe.buildFieldStructure()` は public なので、**本番のクエリそのものから生成する**。

```apex
// ✅ v3.3.0+: fetchedBy で本番クエリの契約を1ステップで焼く (これが現行の正)
MockEntry card = MockEntry.of(BusinessCard__c.class)
  .autoId(1)
  .set('CompanyName__c', 'キクテック')
  .fetchedBy(MatchBusinessCardsHandler.cardScribe());

// v3.2.0 以前は buildFieldStructure() を手で渡す (fetchedBy が無いため)
private static final FieldStructure CARD_SELECTED = MatchBusinessCardsHandler.cardScribe().buildFieldStructure();
MockEntry legacyCard = new MockEntry(new BusinessCard__c(), CARD_SELECTED).autoId(1).set('CompanyName__c', 'キクテック');
```

`buildFieldStructure()` は内部で全項目を小文字化し、**SObject に実在しない項目名を例外**にする。項目名リストを自分で書き起こすと、この2つを自前で気にすることになる。

> 💡 `not selected in Scribe` で落ちたときに「構造に項目を足す」のは正しい直し方。ただし
> **足す先は手書きのリストではなく Scribe** で、メッセージが名指ししているのもそれ。

**Scribe が表に出ない場所ではテスト用に切り出す。** バッチは `Database.getQueryLocator(scribe.toSoql())` で SOQL 文字列を渡すため Scribe が外から見えず、委譲先の Usecase はレコードを引数で受け取る形になる (= 直接 DI)。この場合は**バッチ側に `@TestVisible` な取得口**を用意し、`QueryLocator` とテストの双方がそこを参照する:

```apex
public Database.QueryLocator start(Database.BatchableContext bc) {
  return Database.getQueryLocator(cardScribe().toSoql());
}

/** 🧪 単体テストが FieldStructure の生成元として使う。ここを変えるとテストに伝わる。 */
@TestVisible
private static Scribe cardScribe() {
  return Scribe.of(BusinessCard__c.class).fields(cardFields).whereEqual('MatchStatus__c', STATUS_UNPROCESSED);
}
```

**本番 Scribe が存在しない純粋関数** (`IEntry` を引数に取る Util 等) は、**テスト内に Scribe を書いて「呼び出し元に要求する SELECT の契約」を宣言する**。手写しより意図が伝わり、小文字・typo の問題も起きない:

```apex
private static final FieldStructure SELECTED = Scribe.of(Account.class)
  .field('Id').field('Name').field('ParentId')
  .buildFieldStructure();
```

⚠️ **検知が効いていることを一度確かめる。** 本番の SELECT から項目を1つ落として**テストが赤くなること**を確認しておくと、「検知しているつもりで実は素通り」を防げる (`.set()` が structure を登録しない前提に依存しているため)。

**クラス跨ぎで効かせるには、親 A が自分の `IEloquent` を子 B に注入する** (`new B(entries, this.eloquent)`)。A の MockEntry が B まで貫通し、B が A の SELECT 外を `get()` した瞬間、実DMLに触れず落ちる(数十ms)。注入しないと B が `new Eloquent()` を握り、取得漏れは遅い実DML結合テスト(数秒)か本番でしか出ない(「B だけ直して B のテストだけ緑でリリース → 本番で `SObjectException`」)。この合成境界のため B の DI コンストラクタ `(List<IEntry>, IEloquent)` は `@TestVisible private` ではなく **`public`**。⚠️ 検知が効くのは `.get()`/`.first()` の IEntry 経路のみ — `getAsSObject`(SObject を返す)・`rawSoql`(`withoutFieldValidation`)は焼き付かない。

**検知の無効化 (原則使わない)**: `withoutFieldValidation()`(未 SELECT get 許可)/ `withoutSObjectFieldValidation()`(実在しない set 許可)。2系統独立で、片方を緩めても他方は維持される。

---

## 深掘り (元ドキュメント)

⚠️ **以下の KrileWorks ドキュメントは v2 世代のまま** (2026-07 時点)。`userMode` / `systemMode` / `ignoreWhen` / `inherited sharing` 化を反映していない。**v3 の記述はこの SKILL.md と submodule 実ソース (`force-app/main/default/classes/ApexEloquent/`) が正**。食い違ったらドキュメント側が古い。ApexEloquent 本体の README にも旧 API 例 (`getAggregate()` の public 呼び出し等) が残っているので同様に注意。

- https://krileworks.com/document/ja/apex-eloquent-scribe-guide.md — Scribe の使い方ガイド (SELECT/WHERE/集計/動的組み立ての実コード)
- https://krileworks.com/document/ja/apex-eloquent-api-scribe.md — Scribe 全 API シグネチャ一覧 (ファクトリ・WHERE・集計・GROUP BY/HAVING)
- https://krileworks.com/document/ja/apex-eloquent-api-eloquent.md — IEloquent/Eloquent/MockEloquent 全 API (label/attach/failOn/whenLabel/repeat の挙動)
- https://krileworks.com/document/ja/apex-eloquent-api-entry.md — IEntry/Entry/MockEntry 全 API (set/template/autoId/alias/times/setParent/setChildren)
- https://krileworks.com/document/ja/apex-eloquent-data-access.md — データ取得と DML、IEntry の扱い、MockEloquent でのテストの使い方ガイド
- https://krileworks.com/document/ja/apex-eloquent-mockentry-deep-dive.md — MockEntry が解く課題 (書き込み不可項目/JSON ハックの限界/SELECT 漏れ検知) の深掘り
- https://krileworks.com/document/ja/apex-eloquent-relations.md — 親項目/子サブクエリ/親条件/多対多 (Junction) の取得とモックの実コード
- https://krileworks.com/document/ja/dynamic-query-creation-apex-eloquent.md — 生 SOQL から Scribe ORM への移行入門 (文字列連結との比較)
- https://krileworks.com/document/ja/query-delegation-pattern.md — クエリ構築と実行を分離する設計哲学 (Selector Pattern の運用課題からの出発)
- https://krileworks.com/document/ja/layered-constructor-pattern.md — Usecase の 2 コンストラクタ DI 定石 (本番デフォルト + テスト注入)
- https://krileworks.com/document/ja/repository-pattern-challenges-builtin-solution-apex.md — Apex で Repository を内蔵化 (IEloquent) した経緯と Selector Pattern との比較
