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

---
name: apex-trace
description: Use this skill when adding lifecycle logging to a Usecase or verifying Usecase behavior in tests with ApexTrace — i.e. when you see Trace.of, t.start, t.skip, t.log, t.abort, t.finish in production code, or TraceFlow / TraceHistory / TraceUsage / TraceFlow.isLastFinish / TraceFlow.isLastSkip / TraceFlow.isLastAbort / TraceFlow.lastHistoryContains / TraceFlow.contains / TraceFlow.usageOf / TraceFlow.lastUsage / assertSoqlQueriesAtMost / assertInvocationsAtMost in tests. ApexTrace は Usecase のライフサイクルログ・テスト経路検証 (偽陽性検知)・ガバナ消費計測 (v1.1.0+) を担う OSS。
---

# ApexTrace 実用リファレンス

Usecase の処理フローを記録する `Trace` と、その記録をテストで検証する `TraceFlow` の 2 本柱。
**①Usecase にログをどう仕込むか / ②テストで経路をどう縛るか** に集中する。

---

## 1. Trace — Usecase のライフサイクルログ

### instance field に 1 回だけ持つ

`Trace.of(...)` は Usecase の **instance field の初期化時に一度だけ**。`invoke()` 内で呼び直すと毎回新しいインスタンスができ、ネスト/集約ロジックが破綻する。

```apex
public with sharing class CopyAccountIndustryToOpportunityUsecase {
  private Trace t = Trace.of('商談に親取引先の業種をコピー');  // ✅ field で 1 回だけ
  // ...
}
```

`Trace.of('...')` のラベルは start/log/finish 等の出力に **プレフィックス自動付与** され、`System.debug` を手書きするより一貫した構造化ログになる。

### 4 メソッド・3 終了パス

| メソッド | 用途 | 呼ぶタイミング |
|---|---|---|
| `t.start()` | 処理開始 | `invoke()` 冒頭 |
| `t.log(msg)` | 中間ログ | 任意・複数回 OK |
| `t.skip(msg)` | スキップ終了 | 早期 return 直前 (対象なし・条件不一致) |
| `t.abort(msg)` | 異常終了 | 例外・業務エラーで中止 |
| `t.finish(msg)` | 正常終了 | `invoke()` 末尾 |

- `log` / `skip` / `abort` / `finish` には **引数なしオーバーロード** あり (経路だけ残してメッセージ不要なとき)
- **3 終了パスの使い分け**が核心: `finish`=正常完了 / `skip`=何もせず正常に抜けた / `abort`=正常完了しなかった。この区別が後段の TraceFlow 検証を可能にする

### Usecase でのログ仕込み (正規例)

CLAUDE.md の Usecase 標準パターン (本番/テスト用 2 コンストラクタ、`invoke()` のみ public、依存は 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();
  }

  public void invoke() {
    this.t.start();

    if (this.opportunityIds == null || this.opportunityIds.isEmpty()) {
      this.t.skip('対象の商談がないため終了。');   // ← skip パスで早期 return
      return;
    }

    Scribe oppScribe = Scribe.of(Opportunity.class)
      .field('Id')
      .parentField(Scribe.asParent('AccountId').field('Industry'))
      .whereIn('Id', this.opportunityIds);
    List<IEntry> oppEntries = this.eloquent.label(LBL_FETCH).get(oppScribe);

    for (IEntry oppEntry : oppEntries) {
      IEntry accountEntry = oppEntry.getParent('AccountId');
      oppEntry.put('Industry__c', accountEntry.get('Industry'));
    }
    this.eloquent.label(LBL_UPDATE).doUpdate(oppEntries);

    this.t.finish(oppEntries.size() + ' 件の商談に業種をコピー。');  // ← finish パス
  }
}
```

---

## 2. テストでの Trace の二役

| 役割 | 担い手 | 性質 |
|---|---|---|
| **何を検証するか (機械可読)** | メソッド名 `test{Method}_When{条件}_Then{結果}` | 識別子・grep 可能 |
| **何を検証するか (人間可読)** | `Trace.of('正常系: ...完全な日本語文')` | 説明・hover で読む |

- メソッド上に `// 正常系: ...` の独立コメントは書かない (`Trace.of` と重複)
- テスト本体は冒頭で `Trace.of('...').start()`、末尾で `t.finish()` で囲む

### テストを Trace で囲む理由

1. **第一の理由 — Usecase 側のライフサイクル不整合を事前検出**: Usecase が `finish/skip/abort` を呼ばずに `return` すると「開きっぱなしの Trace」が残る。テスト側のアウター Trace に包むと、テスト実行時 default の **Strict モード**がそのミスマッチをテスト段階で叩き出す (本番に出る前に気づける)
2. **副次効果 — 直前の Trace 状態リセット**: 前テストが残したコンテキストを巻き直す

---

## 3. TraceFlow — 経路をテストで検証する

直前に実行された Trace の終了経路を覗く静的ユーティリティ。`invoke()` が `void` でも「経路」を別軸で縛れる。

| メソッド | 検証内容 |
|---|---|
| `TraceFlow.isLastFinish()` | 直前の Trace が `finish()` で終わったか |
| `TraceFlow.isLastSkip()` | 直前の Trace が `skip()` で終わったか |
| `TraceFlow.isLastAbort()` | 直前の Trace が `abort()` で終わったか |
| `TraceFlow.lastHistoryContains(text)` | **最後の1エントリ** に文字列が含まれるか |
| `TraceFlow.contains(text)` | **履歴全体のどこか** に文字列が含まれるか (v1.1.0+) |
| `TraceFlow.lastHistory()` | 直前の `TraceHistory` オブジェクトを取得 |
| `TraceFlow.usageOf(name)` | **その名前のコンテキストのガバナ消費 (exclusive 合計)** (v1.2.0+、下記) |
| `TraceFlow.lastUsage()` | 直近に閉じた **1 コンテキスト**のガバナ消費 (v1.1.0+。バルク IT では使わない → 下記) |

### lastHistoryContains と contains の使い分け

`lastHistoryContains` は最後のエントリしか見ないため、後からログ行が増えると壊れる。**「この処理のどこかでこのメッセージが出たか」を縛るなら `contains` を使う** (順番による壊れやすさの回避が追加動機。issue #1):

```apex
Assert.isTrue(TraceFlow.contains('3 件の商談に業種をコピー'));  // 履歴のどこにあっても通る
```

- `finish/skip/abort の理由メッセージを直後に縛る` → `lastHistoryContains` のままで OK
- `途中の t.log(...) の内容を縛る` / `ログ追加に強くしたい` → `contains`
- どちらも `null` を渡すと `false` (v1.1.0 で NPE 修正済み)

**TraceHistory**: `lastHistory()` で取れる、直前に記録されたトレースの履歴オブジェクト。ログ列・終了経路を辿れる。`lastHistoryContains` は内部でこれを走査するショートカット。

### skip と finish を別テストで区別する

```apex
@isTest
static void testInvoke_WhenOpportunityHasAccount_ThenIndustryCopied() {
  Trace t = Trace.of('正常系: 商談に親取引先の業種がコピーされること');
  t.start();

  MockEntry oppEntry = MockEntry.of(Opportunity.class)
    .alias('opp').autoId(1)
    .setParent('AccountId',
      MockEntry.of(Account.class).set('Industry', 'Technology'));
  MockEloquent mock = (new MockEloquent())
    .attach(CopyAccountIndustryToOpportunityUsecase.LBL_FETCH, new List<IEntry>{ oppEntry });

  (new CopyAccountIndustryToOpportunityUsecase(
    new Set<Id>{ oppEntry.getAliasId('opp') }, mock
  )).invoke();

  // 副作用 (DML の中身) と 経路 (finish) の 2 軸で縛る
  List<SObject> updated = mock.upsertedRecordsAt(CopyAccountIndustryToOpportunityUsecase.LBL_UPDATE);
  Assert.areEqual(1, updated.size());
  Assert.areEqual('Technology', ((Opportunity) updated[0]).Industry__c);
  Assert.isTrue(TraceFlow.isLastFinish());

  t.finish();
}

@isTest
static void testInvoke_WhenNoOpportunityIds_ThenSkipped() {
  Trace t = Trace.of('正常系: 対象の商談がないときスキップされること');
  t.start();

  MockEloquent mock = new MockEloquent();

  (new CopyAccountIndustryToOpportunityUsecase(new Set<Id>(), mock)).invoke();

  Assert.isTrue(TraceFlow.isLastSkip());
  Assert.isTrue(TraceFlow.lastHistoryContains('対象の商談がないため終了'));  // メッセージまで縛れる

  t.finish();
}
```

`finish` パスと `skip` パスを、副作用 (`upsertedRecords`) とは独立した軸で検証できる。これが ApexTrace を標準装備する理由。

---

## 3.5 TraceUsage — コンテキスト単位のガバナ消費を計測・assert する (v1.1.0+)

各コンテキストは `start()`〜クローズ (`finish`/`skip`/`abort`) 間の **ガバナ消費を自動記録** する。計測は決定的5指標のみ: **SOQL数 / SOQL行数 / DML文数 / DML行数 / コールアウト数** (CPU・ヒープはブレるので意図的に対象外)。

### 🚨 設計意図: 「上限の保険」であって「コストの予算」ではない

**これを取り違えると、脆いだけのテストを量産する。** TraceUsage は「この Usecase は SOQL を何本使うべきか」を決める道具ではない。狙いは **バルクで走ったとき 1 件ごとにクエリを撃つ実装 (N+1) が混入していないこと** を、**緩い上限**で縛る保険。

- ❌ **実測ちょうどの値を書かない**。実測 2 本に `assertSoqlQueriesAtMost(2)` は実質「ちょうど 2 本」の主張で、正当なリファクタでクエリが 1 本増えただけで落ちる
- ✅ **N+1 なら確実に落ち、正当な追加 1〜2 本では落ちない水準**に置く (201 件のバルクなら上限 10〜15)
- ✅ 従来の `Limits.getQueries() < 上限の半分` と**同じ思想**。単に測る単位が「トランザクション全体」から「コンテキスト」に細かくなっただけ
- ✅ **例外は「1 本も発行しない」ことに意味がある場合** (差分なしで何もしない等)。ここは `AtMost(0)` で縛ってよい
- ⚠️ **件数が小さいと N+1 は検出できない**。`times(2)` では per-record (2 回/2 本) がどんなしきい値も通る → **`times(201)`** にする (N+1 が線形に浮き上がり、かつ**トリガーの 200 件分割**も同時に越えられる。`apex-blueprint` skill 参照)
- ⚠️ **`assertInvocationsAtMost(n)` の上限値は「そのテストの件数」とセットで意味を持つ**。分割で起動回数が変わるため (実測: 30件→2回 / 200件→3回 / 201件→5回)、件数を変えるとバグが無くても落ちる。**アサートの隣に件数をコメントで残す**
- ⚠️ **単体テスト (`MockEloquent`) に書くと無条件に通る**。実 SOQL を発行しないので usage は全ゼロ。「ガバナを縛ったつもり」が量産される (`Limits` を stopTest 後に読む罠と**同じ構造**)

### 置き場所: ApexBlueprint の `times()` で組むガバナ IT (トリガー経由)

**Usecase を直接 `invoke()` するのではなく、トリガーを発火させる**のが本来の形。ApexBlueprint は宣言を実 DML で realize するので、**`create()` そのものが発火装置を兼ねる** (テストデータを作ってから別途 insert する段取りは不要)。

```apex
@isTest
static void testInsert_WhenBulk_ThenWithinGovernorLimits() {
  Trace t = Trace.of('エッジケース: 商談を一括登録してもガバナに余裕があること');
  t.start();

  SOrchestrator o = SOrchestrator.start()
    .add(SBlueprint.of(Account.class)
      .template(Blueprints.accBasic())
      .withChildren(
        SBlueprint.of(Opportunity.class)
          .template(Blueprints.oppBasic())
          .times(201)));          // ← 201 件以上。トリガーは 200 件ずつ分割して呼ばれる

  Test.startTest();
  o.create();                              // Act: before/afterInsert がバルクで発火
  Integer soqlUsed = Limits.getQueries();  // ★ 必ずブロックの中で掴む (後述)
  Test.stopTest();

  // ① 結果の正しさ (主眼)
  Assert.areEqual(201, [SELECT COUNT() FROM Opportunity], '201 件すべてが処理されていること');

  // ② 狙った Usecase の消費 (v1.2.0+)。reason は v1.3.0+
  TraceFlow.usageOf('集金オブジェクトの再生成')
    .assertInvocationsAtMost(6, '201 件 insert 時の実測は 5。増えたら配線が増えた可能性がある')
    .assertSoqlQueriesAtMost(15, '件数に比例してクエリを撃っていないこと');

  // ③ トランザクション全体の余白
  Assert.isTrue(soqlUsed < Limits.getLimitQueries() / 2,
    'バルクでも SOQL は上限の半分未満。実測 ' + soqlUsed);

  t.finish();
}
```

**3 段それぞれ守る対象が違う。どれも他で代替できない**:

| # | 保険の対象 | 破れるとき |
|---|---|---|
| ① | **取りこぼしていないこと** (主眼) | `take(200)` / `LIMIT` / 「1 回で全件来る」前提の実装 |
| ②-1 回数 | **配線** | 別トリガーからも呼ばれ始めた / 再入が増えた |
| ②-2 SOQL | **中身** | ループ内クエリ (N+1) の混入 |
| ③ | **カスケード全体の余白** | 自分以外も含めてトランザクションが太った |

🛑 **① を省かない**。ガバナだけ見ていると「**取りこぼしているのに消費は少ない**」を見逃す (`take(200)` はクエリ数がむしろ減るので ②③ は緑のまま通る)。
🛑 **② は回数と消費の 2 行に分ける**。片方だけでは原因まで辿れない (「回数は 2 なのに SOQL が 32」→ 配線ではなく中身のループ、と切り分けられるのはこの 2 軸があるから)。

🚨 **`Limits` を `Test.stopTest()` の後で読んではいけない。** `stopTest()` はガバナカウンタを `startTest()` 前の状態に戻すため、その後の `Limits.getQueries()` は **Act ではなく Arrange の値**を返す。`Assert.isTrue(Limits.getQueries() < 上限/2)` は **常に真になり何も検証しない**。

```
実測 (商談 30 件を o.create() で一括登録):
  startTest 前     soql=0  dml=3    ← Arrange
  startTest 直後   soql=0           ← リセット
  Act 後(ブロック内) soql=7  dml=2    ← ★これが本当のカスケード消費
  stopTest 後      soql=0  dml=3    ← startTest 前に戻る (= assert が空振り)
```

### 3 者の棲み分け — 測りたいものが違う

| 測りたいもの | 手段 |
|---|---|
| トリガーの**同期**カスケード全体 | `Limits` を **ブロック内で変数に掴む** |
| **特定の Usecase** が引き起こした分 | **`TraceFlow.usageOf(name)`** (v1.2.0+) |
| **非同期** (stopTest で走るバッチ / Queueable) | **`TraceUsage` しかない** |

3 行目が `TraceUsage` の独壇場。**バッチは `Test.stopTest()` で初めて走るので `Limits` では原理的に測れない** (ブロック内で掴んでも、その時点ではまだ走っていない)。非同期処理のガバナ消費を縛れる唯一の手段。

### 🎯 API の選び方 — まずこの表だけ見る

**境界に `discardArrange()`、あとは `usageOf` + 緩い上限。** これで足りないときだけ下を読む。

| 主張したいこと | 書くもの |
|---|---|
| バルクで撃ちすぎていない (**保険。9 割これ**) | `TraceFlow.usageOf(name).assertSoqlQueriesAtMost(緩い値, reason)` |
| この Usecase は N 回しか動かない (**配線**) | `TraceFlow.usageOf(name).assertInvocationsAtMost(N, reason)` |
| そもそも走らないこと | `assertInvocationsAtMost(0)` |
| Act 内で複数回走る**内訳**を個別に見たい | `TraceFlow.usagesOf(name)` の各要素 |
| カスケード**全体**の余白 | `Limits` を `startTest`〜`stopTest` の**ブロック内**で変数に掴む |

### 🚧 `discardArrange()` を Arrange / Act の境界に置く (v1.4.0+)

**Trace の履歴はテストメソッドの先頭から溜まる。** Arrange の DML でもトリガー経由で同じ Usecase が走り、`usageOf` の合算に混ざる。`TraceFlow.discardArrange()` で履歴を区切ると、以降のアサートは Act だけを見る。

```apex
setupAccountWithOpportunities();   // Arrange

TraceFlow.discardArrange();        // ← ここ
Test.startTest();                  // ← と、ここは同じ境界

new ResummarizeUsecase(ids).invoke();
Test.stopTest();

TraceFlow.usageOf(NAME)
  .assertInvocationsAtMost(1, 'Act で 1 回だけ')
  .assertSoqlQueriesAtMost(0, '差分が無ければクエリを撃たない');
```

- **`Test.startTest()` を置くのと同じ判断**。新しく覚える概念は無い。書く場所も隣
- **経路アサートにも効く**。`isLastSkip()` 等は「最後に閉じたコンテキスト」を見るので、Act が何も起こさないと Arrange のコンテキストを読んでしまう。置くと観測窓が Act に揃う
- ⚠️ **書き忘れてもライブラリは何も言えない** (Arrange の有無を判定できない)。ただし `assertInvocationsAtMost` が番人になる (Arrange の 1 回ぶん多く出るので落ちる)
- 🛑 **Usecase のコンテキストが開いている最中には呼べない** (Start エントリが消えて `usagesOf` から静かに 1 件落ちるため)。境界なら開いているのはテスト自身の `Trace` が最大 1 つ。それ以上で `TraceException`

🚨 **`usagesOf(name)` の末尾を取って「Act の 1 回ぶん」とするのは禁止。** 正しいのは Act がその Usecase を**ちょうど 1 回だけ**起動したときに限られ、チャンクが 2 つに割れた瞬間に「最後のチャンクだけ」を測ることになる。**Act 全体を測るなら `discardArrange()` + `usageOf`。**

### 📐 inclusive / exclusive の正確な仕様 (ソース確認済み)

| 取り出し方 | 返るもの |
|---|---|
| 履歴に記録される生の値 / `lastUsage()` / `lastHistory().getUsage()` / **本番デバッグログの `Usage:` 行** | **inclusive** (`start`〜close の総量。子を含む) |
| **`usageOf(name)` / `usagesOf(name)`** | **exclusive** (自分の inclusive − **直下の**子の inclusive 合計) |

- **合計を出す `usageOf` が exclusive でないと二重計上になる**: ハンドラ 6 + UsecaseA 2 + UsecaseB 4 = 12 だが実際は 6 (ハンドラ自身の exclusive は 0)
- ✅ **Usecase にしか `Trace` を貼っていなければ inclusive = exclusive**。差が出るのはハンドラなど外側にも貼ったときだけ
- 🛑 **inclusive / exclusive は「回数」には掛からない**。`getInvocations()` は「そのコンテキストが閉じた回数」で、分解の対象ではない。**消費と回数は別の軸**

### 🛑 lastUsage() は使わない

返るのは**最後に閉じた 1 コンテキスト**だけ。ハンドラが Usecase を 3 つ呼ぶなら **3 本目しか取れない** (実測: カスケード全体 soql=7 に対し `lastUsage()` は 3 本目の soql=1)。**v1.3.0 から、同じ深さで 2 つ以上閉じていれば `TraceException`** (候補名が列挙されるのでそのまま `usageOf` に移せる)。テスト実行時のみのガードで、本番では素通りする。

### ⚠️ TraceUsage の算術の罠

| API | invocations の扱い |
|---|---|
| `plus()` (= `usageOf` の合算) | **加算される** (閉じた回数の合計になる) |
| `since()` (差分) | **1 に固定される** (5 引数コンストラクタが `invocations = 1` を渡すため) |

🛑 したがって **`usageOf(N).since(before)` で「Act のぶんだけ」を作るのは不可**。消費は正しく差分になるが**回数が意味を失う**。Act だけを測るなら `discardArrange()` を使う (v1.4.0+)。

### 🚫 トリガーハンドラに Trace を常設しない (調査時だけ貼る)

必ず迷う点。**答えは「保険としては不要。調査の道具として一時的に貼る」**。

- **貼れば機能はする** (実測: `afterInsert` を包むとネストは LIFO なので再入も全部含めて最後に閉じ、カスケード込み 6 SOQL / 1 DML が取れた)
- **しかし保険としては冗長**。`usageOf(name)` で Usecase を名前指定で引けるので、ハンドラに貼らなくても同じアサートが書ける。本番コードに try/catch/abort 込み 18 行を先払いする理由がない
- 🛑 **`beforeInsert` の頭で開いて `afterInsert` の末尾で閉じる案は効果がない** (実測)。**DML 文のカウンタは `beforeInsert` が走る前に増えている**ため、どう挟んでも `insert` 文そのものを取り逃す:

| 測り方 | SOQL | DML |
|---|---|---|
| ブロック内の `Limits` (= 真の全量) | 7 | 2 |
| `afterInsert` だけを包む | 6 | 1 |
| `beforeInsert` 頭 → `afterInsert` 末で挟む | **6** | **1** (改善しない) |

加えて static 前提・操作ごとにペアが必要・`undelete` に before が無い・例外で開きっぱなし、とリスクだけ増える。

- ✅ **貼る価値が残るのはログの可読性だけ**。ハンドラ Trace が無いと、ログ上で **200 件分割の 2 回目とトリガー再入が兄弟として並び、区別できない**。貼ると分割は別の外枠、再入は内側のネストとして現れる
- ✅ 一時的に貼る条件: **同じ Usecase を before と after の両方から呼び始めたとき** / **カスケードの順序をログで追う必要が出たとき**

### 💡 回数のアサートは消費量より安定する

消費本数は正当な変更で揺れるが、**回数は設計意図そのもの**なので動かない。「この Usecase は 1 回の保存につき 1 回だけ動くはず」は、**別トリガに配線された瞬間に落ちる** → CLAUDE.md「🔌 配線の禁欲」の妥当性を縛る保険として使える。

```apex
// 201 件を 1 DML で挿入した場合の上限 (分割で起動が増えるため)
TraceFlow.usageOf('法人商談(X01)への個店商談(X02)集計')
  .assertInvocationsAtMost(6);
```

⚠️ **回数だけは件数依存**。消費量 (SOQL/DML) の上限は件数に対してほぼ一定だが、起動回数は分割で変わる (30件→2 / 200件→3 / 201件→5)。**30 件のテストで `AtMost(2)` と書いたあと件数を 201 に増やすと、バグが無いのに落ちる。**

生値が欲しいときは getter を使う:

```apex
TraceUsage usage = TraceFlow.usageOf('商談に親取引先の業種をコピー');
Integer soql = usage.getSoqlQueries();  // getSoqlRows / getDmlStatements / getDmlRows / getCallouts
```

### 知っておくべき仕様

(inclusive / exclusive と `lastUsage()` の制約は上記「📐 正確な仕様」「🛑 lastUsage() は使わない」を参照)

- **assert 失敗は catch 可能な `TraceException`** (`Assert.fail` の `AssertException` は catch 不能でヘルパー自体をテストできないため)。メッセージに消費内訳 (`Actual usage: SOQL: 3 (rows: 120), ...`) が全部載る
- **起動 0 回への消費 assert は例外** (v1.3.0+): `invocations = 0` に `assertSoqlQueriesAtMost(n)` は「走った上で n 以下」という主張の根拠が無いので `TraceException`。**コンテキスト名は文字列なので、`Trace.of(...)` のリネーム漏れがこれで赤くなる**。`assertInvocationsAtMost(0)` は正当な主張なので対象外
- **`reason` オーバーロード** (v1.3.0+): 6 つの assert すべてに `(Integer max, String reason)`。理由は**数字より前**に出る。**実測値**と**増えたら何を疑うか**を書く (数字だけだと「上限が厳しすぎる」と解釈されて閾値を上げられる)
- 全コンテキストを走査するなら `TraceFlow.getAllHistories()` の各 `getUsage()` (Start/Log エントリは `null`)
- **本番のデバッグログにも出る**: `FINISH: ...\nUsage: SOQL: 3 (rows: 120), DML: 1 (rows: 30), Callouts: 0` — テスト外でも Usecase 単位のコストが常時見える

### ガバナ IT との関係 (CLAUDE.md「🛡 ガバナIT」)

CLAUDE.md が **MUST** としているバルクガバナ IT の、**assert を 1 段細かくする追加装備**。従来は `Limits.getQueries()` (トランザクション全体) しか縛れず、落ちても犯人が分からなかった。TraceUsage を併記すると「カスケード全体が重い」のか「この Usecase が件数に比例している」のかが切り分けられる。

**ガバナ IT 自体を置き換えるものではない** — `Limits` の全体値 (ブロック内で掴む) は引き続き必須。上の「置き場所」の形 (ApexBlueprint `times()` → `create()` でトリガー発火 → 2 段で assert) をそのままガバナ IT の雛形として使う。

**唯一の例外が非同期**。バッチ / Queueable のガバナ消費は `Limits` では原理的に取れないので、**そこだけは `TraceUsage` が代替ではなく唯一の手段**になる。

⚠️ 単体テスト (MockEloquent) では実 SOQL が出ないため usage は全ゼロになる。**ガバナの assert は実 DML を流す結合テスト側に書く** (単体でやっても意味がない)。

---

## 4. false-positive 検出 — このフレームワークの核心思想

「テストは通っているのに何も検証していない / 本番で落ちる」偽陽性を構造的に塞ぐ。2 つの仕組みが組み合わさる。

### (a) 経路の偽陽性を TraceFlow で塞ぐ

`void` Usecase で副作用だけを見ると、「実は早期 return していて DML が 0 件 → アサートも空振り」を見逃す。`TraceFlow.isLastFinish()` を併せて縛ると **「期待した経路を本当に通ったか」** まで保証され、空振りテストが落ちるようになる。

- 「正常完了したつもりが skip していた」→ `isLastFinish()` が false で検出
- 「skip するはずが処理を走らせていた」→ `isLastSkip()` が false で検出

### (b) SELECT 漏れの偽陽性を Scribe×MockEntry で塞ぐ

`MockEntry` は `Scribe` の SELECT 句 (`.field()`) と照合し、**SELECT していない項目へのアクセスを単体テスト段階で `ApexEloquentException`** にする。モックに値があっても SELECT に無ければ例外。本番 (`SObject row was retrieved via SOQL without querying the requested field`) と同じ漏れをテストで先取りする。主オブジェクト/親 `parentField`/子 `withChildren`/集計エイリアスの 4 方向で同じく働く。

```apex
// Usecase が .field('Name') を漏らすと、この「正常系テスト」が invoke 内で例外落ちする
// → リファクタや機能追加で SOQL とアクセス側がズレた瞬間に気づける安全網
Scribe.of(Opportunity.class).field('Id').whereEqual('Id', oppId);  // Name 漏れ
// ...entry.getName() で ApexEloquentException
```

### Strict / Relaxed モード

| モード | 既定切替 | 挙動 |
|---|---|---|
| **Strict** | テスト実行時 (`Test.isRunningTest()`) | ネスト不整合・順序違反で即例外。バグ早期検出 |
| **Relaxed** | 本番実行時 | 不整合は自動 abort で吸収。本番が落ちにくい |

⚠️ **切替は `Test.isRunningTest()` による自動判定のみ。利用者からは切り替えられない** (v1.1.x で `changeModeTo` / `TraceMode` はどちらも `private`)。「テストで通った=本番でも通る」と過信せず、モード差を意識する。

### ネスト Trace の順序ルール (Usecase が Usecase を呼ぶとき)

Inner を先に閉じてから Outer を閉じる (LIFO)。違反すると Strict で即例外。

```apex
Trace outer = Trace.of('Outer'); outer.start();
  Trace inner = Trace.of('Inner'); inner.start();
    inner.log('inside inner');
  inner.finish();   // ✅ Inner を先に閉じる
outer.finish();      // ✅
```

---

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

- https://krileworks.com/document/ja/apex-trace-guide.md — Trace 4 メソッド / TraceFlow API / ネスト・Strict/Relaxed モードの総合ガイド
- https://krileworks.com/document/ja/false-positive-detection-comprehensive-guide.md — Scribe×MockEntry による SELECT 漏れ偽陽性検知の 4 ケース実例 (主/親/子/集計)
- https://krileworks.com/document/ja/test-strategy.md — 単体/結合の責任分離、TraceFlow が活きるテスト設計と失敗時の判断マトリクス
