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

---
name: oss-unlocked-package
description: >
  OSS フレームワーク (ApexEloquent / ApexBlueprint / ApexTrace / ApexTools) を
  submodule ソース投入ではなく **アンロックパッケージ**で org へ入れる手順と、その効果・制約。
  発火する典型: アンロックパッケージ / unlocked package / sf package install / 04t /
  ManageableState / installedEditable / RunLocalTests でフレームワークのテストが落ちる /
  本番の validate が通らない / デプロイ対象からフレームワークを外したい /
  RunSpecifiedTests の個別75% / パッケージインストールでテストは走るのか。
---

# OSS フレームワークをアンロックパッケージで入れる

⚠️ **この skill は実測 n=3 (実案件 pdev / 実案件 本番 2026-09-01、実案件 partialco 2026-09-15) しかない。**
他 org で試したら**必ずここに追記して母数を増やす**こと。反証が出たら記述を書き換える。

---

## 1. なぜソース投入ではなくパッケージなのか

submodule のソースを deploy すると、フレームワークの約700クラスが**自分たちのデプロイ対象**になる。これが2つの壁を作る。

| 壁 | ソース投入 | アンロックパッケージ |
|---|---|---|
| デプロイ対象 | 自分のコード + **FW 約700クラス** | **自分のコードだけ** |
| `RunSpecifiedTests` の個別75%カバレッジ | FW 全クラスに必要（実質不可能） | **自分のコードだけ** |
| `ManageableState` | `unmanaged` | **`installedEditable`** |
| FW 自身のテストが落ちたとき | ❌ 無視できない（自分たちのコード扱い） | ✅ **無視してよい** |

🚨 **`ManageableState` が判断の分かれ目。** CLAUDE.md の「フレームワークのテストが落ちても無視してよい」は
`installedEditable` のときだけ成立する。ソース投入した org では `unmanaged` になり、
**無視するとフレームワークを壊したまま本番に上げることになる**。

```bash
sf data query -o <org> --use-tooling-api -r csv \
  -q "SELECT Name, NamespacePrefix, ManageableState FROM ApexClass WHERE Name = '<落ちたテストクラス>'"
```

---

## 2. 🚨 インストールでは全テストは走らない（実測）

**本番 org でも走らなかった。** 「パッケージのデプロイは全テストが強制的に走る」という理解は、
少なくとも `sf package install` には当てはまらない。

**実測 (実案件 本番 / 2026-09-01)**:

| パッケージ | 所要時間 |
|---|---|
| ApexTools v1.1.0 | 42 秒 |
| ApexTrace v1.5.0 | 31 秒 |
| ApexBlueprint v2.1.0 | 38 秒 |
| ApexEloquent v3.8.0 | 37 秒 |

この org は `RunLocalTests` で 1,700件超のテストを持ち、**そのうち 89件が恒常的に失敗していた**が、
インストールは4本とも成功した。1,700件が走れば数分〜数十分かかるので、走っていない。

⚠️ **未解明**: CLAUDE.md には 実案件 org で `ScribeTest` が落ちた記録がある。あれが何によるものかは
説明できていない。有力なのは「手動の `sf apex run test -l RunLocalTests` だった」(§5: 手動実行では
名前空間なしのパッケージのテストも走る) か「submodule ソース (`unmanaged`) の時期だった」のどちらか。
**パッケージ*バージョン作成時* (`sf package version create`) にテストが走る仕様との混同**もあり得る。いずれも推測。

### `--apex-compile`

```
-a, --apex-compile=all|package   [default: all]
    Compile all Apex in the org and package, or only Apex in the package; unlocked packages only.
```

既定は `all` で **org 全体の Apex をコンパイル**する。`package` にすると
パッケージ内だけになる。⚠️ ただしヘルパー曰く「これを指定してもやるものはやる」処理があり、
**テスト実行範囲の指定ではない**。上の実測では既定 (`all`) のまま走らなかった。

---

## 3. 既存の unmanaged クラスは「引き取られる」

同名の unmanaged クラスが既にある org へインストールすると、**衝突せずパッケージが所有権を引き取る**。
削除は不要。

**実測 (実案件 pdev)**: submodule ソースで入っていた 7クラスが、インストール後に
`unmanaged` → `installedEditable` へ変わった。既存クラスの削除も手動対応も要らなかった。

🚨 **引き取った後は、submodule をデプロイ対象に含めないこと。** 含めると
`installedEditable` のクラスを `unmanaged` で上書きし、状態が巻き戻る。
リポジトリの submodule は「ソースの参照元・バージョンの記録」として残す。

---

## 4. 手順

### 4-1. パッケージバージョン ID を調べる

submodule の `sfdx-project.json` に `packageAliases` として入っている。

```bash
python3 -c "
import json,io
d=json.load(io.open('force-app/main/default/classes/ApexEloquent/sfdx-project.json'))
print(json.dumps(d.get('packageAliases',{}), ensure_ascii=False, indent=2))
"
```

**2026-09-01 時点の最新** (参考。必ず上のコマンドで現物を確認すること):

| FW | バージョン | 04t ID |
|---|---|---|
| ApexEloquent | 3.8.0-1 | `04tgK000000I1vJQAS` |
| ApexBlueprint | 2.1.0-1 | `04tgK000000HuDtQAK` |
| ApexTrace | 1.5.0-1 | `04tgK000000HuAfQAK` |
| ApexTools | 1.1.0-1 | `04tgK000000HuCHQA0` |

### 4-2. 入れる

**小さいものから1本ずつ。** まとめて入れると、落ちたときの切り分けができない。

```bash
sf package install --package 04tgK000000HuCHQA0 -o <org> --wait 40 --no-prompt   # ApexTools
sf package install --package 04tgK000000HuAfQAK -o <org> --wait 40 --no-prompt   # ApexTrace
sf package install --package 04tgK000000HuDtQAK -o <org> --wait 40 --no-prompt   # ApexBlueprint
sf package install --package 04tgK000000I1vJQAS -o <org> --wait 60 --no-prompt   # ApexEloquent
```

- `--security-type` の既定は `AdminsOnly`。フレームワークは権限セットでクラスアクセスを配るので既定でよい
- ⚠️ `sf package installed list` への反映は遅れることがある。**確認は `ManageableState` で行う**

### 4-3. 確認

```bash
sf data query -o <org> --use-tooling-api --json \
  -q "SELECT ManageableState, COUNT(Id) c FROM ApexClass WHERE NamespacePrefix = null GROUP BY ManageableState"
```

`installedEditable` が 95前後 (4本合計)、自分たちのコードが `unmanaged` に分かれていれば成功。

---

## 5. 🚨 これで解ける問題 / 解けない問題

### 解ける

- **`RunSpecifiedTests` の個別75%カバレッジ** — デプロイ対象が自分のコードだけになる
- **FW 自身のテストが org のスキーマで落ちる問題** — `installedEditable` なので正当に無視できる
  (典型: `Ambiguous Relationship: Multiple relationships exist for Case on Contact` /
  `SBlueprintAnalyzer.inferParentField` で `Opportunity → Account` が2本、など)

### 解けない

- 🚨 **org の既存テストが落ちている問題**。`-l RunLocalTests` を使った **メタデータの** validate は
  相変わらず通らない。パッケージとメタデータデプロイは別経路であることに注意
- ⚠️ **`RunLocalTests` は場面で定義が違う** (Salesforce CLI 2.147 のヘルプ文面):
  デプロイ/validate では「installed managed and unlocked packages」のテストを**除外**、
  手動の `sf apex run test` では「no-namespaced unlocked packages」のテストを**含める**。
  OSS は名前空間なしなので、**リリースでは走らず、手動実行では走る。手動実行で落ちても `installedEditable` なら無視してよい**
  (デプロイ側はヘルプ文面とユーザーの記憶が一致した段階で、実測はまだ無い)

---

## 6. 実測ログ (追記していく)

| 日付 | org | 種別 | 結果 |
|---|---|---|---|
| 2026-09-01 | 実案件 | sandbox | 4本成功。既存 unmanaged クラスを引き取り。テストは走らず |
| 2026-09-01 | 実案件 | **本番** | 4本成功 (合計約3分)。**全テストは走らなかった**。既存 94クラスは無傷 |
| 2026-09-15 | 実案件 | sandbox | 4本成功 (1本あたり 2〜3分)。submodule ソースで入れていた **96クラスを引き取り** `unmanaged` → `installedEditable`。テストは走らず。その後 OSS を外した validate (コンポーネント19本) が成功し、75%要件を通ることを確認 |

### 🔬 「75% の壁」を本番で実測した (実案件 / 2026-09-15)

**§1 の表で「実質不可能」と書いていた壁を、数字で確認した。** submodule ソースのまま
`RunSpecifiedTests` で本番 validate を流すと:

| 指定したテスト | 結果 |
|---|---|
| 自前のテスト 7本だけ | **失敗**。テストは 43/43 全緑。カバレッジ 75%未満が **48クラス**。**全部 OSS で、自前クラスは0件** |
| 自前 7本 + **OSS のテスト 37本** | テスト **1294/1295 成功**。75%未満は **`Eloquent` 74.108%** と **`InvalidConditionClause` 72.222%** の **2クラスだけ**に減ったが、**それでも通らない** |

🚨 **要点は 2 つ。**

1. **OSS のテストを総動員しても届かない。** 「`-t` に OSS のテストクラスを全部並べる」回避策は、
   あと 2 クラス・数%のところで失敗する。**しかも上げるには OSS リポジトリ側を直すしかない**
   (このバージョンの upstream 自身のカバレッジがその値なので)
2. **落ちるのは OSS だけで、自前クラスは 1 つも挙がらない。** つまりこの壁は
   「自分たちのテストの出来」とは無関係で、**submodule 投入という形そのものが作っている**

⚠️ **1回目の validate は「自前クラスは 98〜100% なので満たす」と判断して流し、失敗した。**
`RunSpecifiedTests` の 75% は**デプロイに含まれる全クラス・全トリガー**に掛かる。
数える対象を「自分が書いたクラス」に限定するのが典型的な間違い。

💡 **Inactive なトリガーは対象外だった。** `LinyTrigger` (status = Inactive / カバレッジ 0%) は
デプロイに含めていたが、75%未満の一覧に**挙がらなかった**。トリガーがこの要件の対象になるかを
心配する必要は (少なくともこの org では) なかった。

