mcpField
T
test-native-extension

test-native-extension

github.com/microsoftテスト・品質1.1k71 回インストール今日更新
リポジトリ

test-native-extension について

サードパーティ製ネイティブコントロールのリポジトリ向けテストスキルです。ネイティブソース、manifest、PCF の契約を検査し、デバイスでの手動テスト手順を提示します。ネイティブバイナリのコンパイルは行いません。

主な機能

  1. 01ネイティブ、manifest、PCF の契約を照合
  2. 02ネイティブモジュールの構造とシンボルを検証
  3. 03manifest のルールを検査
  4. 04PCF がある場合にビルド
  5. 05デバイステスト手順を提示

活用シーン

  1. 01ネイティブモジュール変更後の構造検査
  2. 02ネイティブコード、manifest、PCF 間の契約ずれの発見
  3. 03PCF をビルドしてデバイス検証を準備

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/microsoft/power-platform-skills --skill test-native-extension

インストール後、Claude Code で/test-native-extensionを入力して読み込まれたか確認します。

詳細

/test-native-extension

TypeScript 拡張機能ではなく、.ppmplugin バイナリバンドルとして提供されるサードパーティ製コントロールのリポジトリに対して、4 層の検証手順を実行します。レイヤー 1〜4 は自動化されています。レイヤー 5 は対話型です(実機またはシミュレーターと、テスト環境にデプロイされた Companion PCF が必要です)。

レイヤー内容モード所要時間必要なもの
0全体的な契約の整合性(ネイティブ ↔ マニフェスト ↔ PCF のクロスチェック)自動、警告のみ数秒ディスク上に少なくともネイティブモジュールがあること
1ネイティブソースの構造アサーション(Android の getName() ↔ iOS の +moduleName ↔ マニフェスト)自動、grep/解析数秒android/ または ios/
2マニフェスト検証(ppmplugin-format §4 のルール)自動数秒(ディスク上にマニフェストがない場合のみスキップ)./manifest.json(コミット済み。なければステージング済みのコピー)
3ネイティブソースの契約アサーション(リクエスト/レスポンス/エラーの grep クロスチェック)自動数秒ネイティブモジュール
4PCF のコンパイル(pcf/<Pascal>PCF/ で npm run build)自動数秒(初回インストール後)pcf/<Pascal>PCF/ が存在すること(存在しない場合はスキップ)
5実機/シミュレーターでの手動エンドツーエンドテストレシピのみ — スキルが手順を出力し、ユーザーが任意のタイミングで実行5〜10 分、スキルの実行外pcf/ が存在すること + PCF がデプロイ済みであること

レイヤー 1〜4 はレイヤーごとに順番に実行します。自動化されたレイヤーでは、最初の失敗で停止します。レイヤー 5 はスキルのゲート対象ではありません。スキルはデバイスでの手順を出力して終了し、ユーザーが任意のタイミングで実行して、.extension-state.md を手動で更新します。

このスキルで検証しないこと: ネイティブコードがロード可能な DEX/フレームワークにコンパイルされるかどうかです。それは /build-android-binary と /build-ios-binary の役割です。これらは固定された RN バージョンに対して実際の Gradle/xcodebuild ツールチェーンを実行し、実際のコンパイラーエラーを提示します。このスキルで単独の pod lib lint や ./gradlew assembleDebug を実行すると、誤った安心感を招きます(依存関係をパブリック CDN/maven から解決し、wrap ホストの固定バージョンに対して解決するわけではないためです)。このスキルは、ツールチェーンなしで数秒で実行できる構造面の事前チェックです。ネイティブソースの構造が正しいこと(正しい基底クラス、正しいシンボル、Android の getName() ↔ iOS の +moduleName ↔ マニフェストの整合性)をアサートすることで、数秒で修正可能なミスが原因でビルドスキルが後から失敗するのを防ぎます。このトラックには、型チェック対象となる TypeScript/INativeExtension レイヤーはありません。ネイティブのみの .ppmplugin バンドルは、直接 NativeModules.<nativeModule>.<method> にディスパッチします(ppmplugin-format §2 — ランタイムディスパッチ契約)。

ステップ 1 — 共通ドキュメントと PRD を読む

  1. shared/shared-instructions.md、shared/naming-conventions.md、shared/ppmplugin-format.mdを読みます。

  2. スキルごとの最小限の前提条件ポリシー(shared-instructions.md §1.5)を適用します。レイヤー 1~3 ではツールチェーンは不要です(読み取りと grep のみで、作業ツリーと照合して検証します)。レイヤー 4 では、PCF が存在する場合に限り Node + npm が必要です。しかも必要なのは初回実行時のみです(PCF 自体の依存関係を公開 npm レジストリから npm install するため)。このトラックは自己完結しており、パッケージフィードやソース管理の認証は不要です(shared-instructions §0a)。prereq-check.md にある /test-native-extension チェックを実行します(レイヤー 0~3 では何も必要ありません。レイヤー 4 で PCF が存在する場合に限り Node + npm が必要です。この自己完結型トラックには「ベースライン」チェックはありません)。

    続行する前に、shared-instructions.md §9.2 に従って前提条件のステータスを見えるブロックとして出力します。

    ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
     Prereq check — /test-native-extension
    ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    
     🟢 ✓ git installed
     🟢 ✓ Node 20+ installed            (only needed for Layer 4 — PCF compile)
     🟢 ✓ npm installed                 (only needed for Layer 4 — PCF compile)
    
     🟢 3 checks passed. Ready to proceed.
    

    pcf/ が存在しない場合、Node/npm はまったく必要ありません。失敗とせず、n/a (no PCF) と記載します。レイヤー 1~3 は必ず実行します。チェックに失敗した場合は、そのチェックの → Fix: 行を出力して停止します。

  3. ./PRD.md を読みます。存在しない場合、レイヤー 3 の契約アサーションはネイティブソース自体を信頼できる唯一の情報源として使用します(それでも有用です)。その旨を記載して、停止せずに続行します。

  4. ./.extension-state.md を読みます。Phase が manifest より前の場合(ネイティブモジュールがまだディスク上にない場合)は、テスト対象がまだ構築されていないため、停止します。


OS に依存しません。これらのチェックにはシェルではなく、組み込みの Read/Grep ツールを使用してください。 このスキルにあるすべての抽出/アサーション(以下の find / grep / sed / awk のスニペット)は、読みやすさのために bash で表記しているだけです。これらはシェルで実行するコマンドではなく、照合対象を示しています。エージェントに組み込まれた Read および Grep ツール(と独自の解析)を使って実行してください。これらは macOS、Linux、Windows PowerShell で同じように動作します。grep/sed/awk/sort/find をシェルから呼び出さないでください。標準状態の Windows にはこれらがなく、レイヤー 0~3 は意図的に純粋な読み取りと解析のみ(ツールチェーン不要)として、あらゆる環境で実行できるようになっています。レイヤー 4 の npm run build だけが実際のコマンドで、npm はクロスプラットフォームです。

ステップ 2 — ユーザーに対象範囲を確認する

まず、PCF コンパニオンがディスク上にあるか自動検出します。大文字・小文字の違いや軽微なレイアウトの違いで検出に失敗しないよう、Grep ツール(glob: pcf/**/ControlManifest.Input.xml)を使用してください。(以下は参考用の bash です。Windows ではそのまま実行しないでください。)

PCF_MANIFEST=$(find pcf -type f -name "ControlManifest.Input.xml" 2>/dev/null | head -1)
[ -n "$PCF_MANIFEST" ] && PCF_PROJECT_ROOT=$(dirname $(dirname "$PCF_MANIFEST"))

$PCF_MANIFEST が設定されている場合、PCF はスキャフォールド済みです。レイヤー 4 のビルド手順には $PCF_PROJECT_ROOT(例:pcf/<Pascal>PCF)を使用します。空の場合は、次のように区別します。pcf/ 自体がない場合は「未スキャフォールド」、pcf/ はあるもののマニフェストがない場合は「スキャフォールドが不完全と思われます。/generate-pcf-companion を再実行するか、フォルダーを確認してください。」

レイヤー 2 を実行するマニフェストも検出します。コミット済みの ./manifest.json(スキャフォールド時に /generate-native-extension が書き込む信頼できる情報源であり、通常はスキャフォールド直後から存在します)を優先し、なければステージ済みのビルドコピーを使用します。

MANIFEST=$( [ -f ./manifest.json ] && echo ./manifest.json || ls ppmplugin/staging/manifest.json 2>/dev/null )

$MANIFEST が空の場合、レイヤー 2 はスキップされます(まだディスク上にマニフェストがありません。/generate-ppmplugin-manifest を実行していない手書きのモジュールが該当します。スキャフォールド済みの場合は、最初から ./manifest.json が存在します)。

この結果に基づき、レイヤー 4(PCF のコンパイル)とレイヤー 5(手動手順。どちらも PCF が存在する必要があります)を実行します。

状態デフォルトのレイヤー構成
pcf/ フォルダーがない(PCF は未スキャフォールド)レイヤー 1、2、3 を実行します。レイヤー 4 と 5 は 保留 — /generate-pcf-companion の実行後に再実行 と表示します
pcf/ フォルダーがあるレイヤー 1~4 を自動実行します。レイヤー 5 はデバイス手順を表示します(待機もゲートもありません)

次を表示します。

Test plan
─────────
Repo: <cwd>
Extension: @powerapps/extension-<kebab>  (class <Pascal>Extension, module <Pascal>Module)
PCF companion: <found pcf/<Pascal>PCF/ | NOT yet scaffolded>
Manifest: <found ./manifest.json (committed) | ppmplugin/staging/manifest.json (staged) | NONE>

Automated layers (skill runs these and reports pass/fail):
  0. Holistic contract check — native ↔ manifest ↔ PCF cross-grep (warn-only)
  1. Native-source structure — getName()/+moduleName/@ReactMethod asserts + load/init readiness (no-arg package ctor, iOS [cls new]/requiresMainQueueSetup, non-throwing construction)
  2. Manifest validation     — <ppmplugin-format §4 rules | SKIPPED — no manifest>
  3. Native-source contract  — request/response/error grep cross-check
  4. PCF compile             — <npm run build in pcf/<Pascal>PCF | DEFERRED>

Manual layer (skill prints a recipe; you run it on a device on your own time):
  5. Device end-to-end       — <print recipe | DEFERRED>

Not validated by this skill: native iOS / Android compile into a loadable DEX / framework (run /build-android-binary // /build-ios-binary for that).

Stop on first failure: <yes by default>

AskUserQuestion を使用します。

テスト計画を実行しますか?

  • 上記のデフォルト構成を実行する(推奨)
  • レイヤー 1~3 のみを実行する(PCF のコンパイルとレイヤー 5 の手順をスキップ)
  • 単一のレイヤーを実行する(対象を指定)
  • キャンセル

ユーザーが単一のレイヤーを選択した場合は、依存関係を確認します。例:「レイヤー 3(コントラクトのアサーション)は、今回または最近の実行でレイヤー 1(構造のアサーション)に合格していると、より有用です。確認をスキップして実行しますか?」厳密に強制する必要はありません。影響を伝えたうえで、ユーザーに判断してもらいます。


ステップ 2.5 — レイヤー 0:全体的なコントラクトの整合性

診断レイヤーであり、ハードゲートではありません。 問題点を報告し、ブロックするのではなく警告に格下げします。レイヤー 5 でデバッグしにくいランタイム症状が現れる前に、ネイティブモジュール ↔ manifest.json ↔ PCF 間の不整合を検出するのに役立ちます。

このレイヤーでは、ネイティブモジュール、ステージ済みの manifest.json、PCF 間で受け渡されるコントラクトを相互チェックします。いずれのチェックもコードのコンパイルや実行は行いません。すべて grep/解析ベースです。いずれかのチェックで不一致が見つかった場合、このスキルは不一致の値と推奨する修正方法を番号付きの警告として表示しますが、ユーザーが停止を選択しない限り、レイヤー 1 に進みます。目的は不整合を早期に明らかにすることです。どの問題が重要かはエンジニアが判断します。

チェック項目
#コントラクト相互照合するソース不一致が現れる箇所
1ルーティングキーmanifest の receivers[].name → pcf/<Pascal>PCF/<Pascal>PCF/index.ts の ROUTING_KEY 定数どちらも同じレシーバーキーである必要があります(スキャフォールドでは '<Pascal>Extension' を使用します。任意の JS 識別子を使用できます。ppmplugin-format §2 を参照してください)。不一致があると、実行時にルーティングが無言で失敗します(ラップブリッジがディスパッチできません)。
1bPCF トランスポート(ワイヤーフォーマット)index.ts のディスパッチ呼び出し:window.PowerApps.NativeExtension.sendAsync("<key>", { method, args: [request] }) を呼び出す必要があり、cordova.exec(または任意の cordova.*)を直接呼び出してはいけません。sendAsync のペイロードは生のオブジェクトである必要があります(事前に JSON.stringify してはいけません。sendAsync が内部で文字列化します)。内側の args は [request](配列)である必要があります。ファイル内に \.sendAsync\( があり、cordova\.exec がないことを grep で確認してください。ワイヤーフォーマットのバグに該当する唯一の構造チェックです。 直接 cordova.exec を呼び出しても、ほかのすべてのチェック(配列は配列であり、キーも一致しています)を通過し、最初のデバイスでのタップ時にのみ失敗します。生の cordova グローバルは PCF サンドボックス内にないため、タップしても何も起きません(Android で最も深刻です)。同様に、事前に文字列化したペイロードは二重エンコードされ、BRIDGE_FAILED になります。sendAsync がない場合、または cordova.exec がある場合は、目立つ形で指摘してください(最優先の Layer 0 の検出事項として扱い、/generate-pcf-companion と ppmplugin-format §2 を提示してください)。
2ネイティブモジュールシンボルiOS のクラス名 RCT<Pascal>Module == manifest の entrypoints.ios.moduleClass。iOS の +moduleName の戻り値 → Android の override fun getName() = "<X>" → manifest の receivers[].nativeModule+moduleName、getName()、receivers[].nativeModule はすべて同じ '<Pascal>Module' を使用する必要があります(正規プレフィックスのルール。ppmplugin-format §3)。Obj-C クラス名は別の値であり、entrypoints.ios.moduleClass と一致する必要があります。不一致があると、一方のプラットフォームでは JS 側のディスパッチ先が見つからず、もう一方では動作するか、バリデーターが manifest を拒否します。
3操作メソッド名PRD §4 の表 → RCT_EXPORT_METHOD(<methodName>:...)(iOS)→ @ReactMethod fun <methodName>(...)(Android)→ manifest の receivers[].methods[]4 つすべてが一致する必要があります。コンパイルは通るものの、どこにもルーティングされないタイプミスや、methods[] に含まれていないためホストがディスパッチできないメソッドを検出します。
4リクエストフィールド名ARCHITECTURE §4.1 → iOS パーサー(.m 内の NSDictionary キー読み取り)→ Android パーサー(.kt 内の ReadableMap キー読み取り)→ PCF ペイロードの構築(index.ts 内の request.<field>)4 つすべてで同じフィールド名を参照する必要があります。iOS のみ/Android のみで不一致がある場合、プラットフォーム固有の INVALID_INPUT になります。
5レスポンスフィールド名ARCHITECTURE §4.2 → iOS JSON の構築(successJsonWith: のキー)→ Android JSON の構築(successJson のキー)→ PCF のレスポンス読み取り(index.ts 内の response.result.<field>)4 つすべてが一致する必要があります。「PCF が undefined を読み取る」問題を検出します。
6エラーコードARCHITECTURE §5 のエラーコードの union → iOS の errorJsonWithCode:message: の引数文字列 → Android の errorJson(code, message) の引数文字列 → PCF の setError の case ラベル → ARCHITECTURE §8 に行があるか§5 の各コードは、少なくとも 1 つのネイティブ出力箇所に記述され、PCF の setError にも case(または default)がある必要があります。ネイティブ側で出力されているものの §5 に記載されていないコードは警告してください(PRD との乖離)。§5 にあるものの、PCF の default で処理されていないコードも警告してください(処理が不完全です)。
6bエラーメッセージの連携ネイティブのエラーヘルパーのシグネチャが message を受け取る(errorJsonWithCode:message:/errorJson(code: String, message: String))→ PCF の setError(code, message) が 2 引数で this.errorMessage を設定する → ControlManifest.Input.xml が Status、ErrorCode、ErrorMessage を usage="output" として宣言する → getOutputs() が 3 つすべてを返すネイティブのヘルパーが code だけを受け取る場合、setError が 1 引数の場合、または ErrorMessage の出力が欠けている場合は警告してください。失敗時に、人間が読める理由がなく、コードだけ(または何も)maker に届きます。このチェックは、デバイス上でのデバッグにおけるこのギャップを解消するためのものです。
7PCF manifest のプロパティ ↔ index.tspcf/<Pascal>PCF/<Pascal>PCF/ControlManifest.Input.xml 内の <property name="..."> → index.ts 内の IInputs/IOutputs の参照(p.<Name>.raw および getOutputs() の戻り値キー経由)manifest のすべてのプロパティが読み取られ、すべての IOutputs キーが getOutputs() に含まれている必要があります。Layer 4 の tsc でも実際に検証されますが、Layer 0 ではより読みやすい差分で早期に検出します。
8権限 ↔ §3.2ARCHITECTURE §1.4 → iOS の Info.plist 使用目的文字列(例:NSCameraUsageDescription)→ Android の AndroidManifest.xml 内の <uses-permission> エントリーARCHITECTURE §1.4 の各行に対応するネイティブ側のエントリーが必要です。不一致があると、実行時に OS によって拒否され、ユーザーにはメッセージが表示されません。
9RN の固定バージョンpackage.json の devDependencies にある React Native の固定バージョン → android/build.gradle の compileOnly RN 行 → manifest の abi/ビルドの固定バージョン(ppmplugin-format §0 の定数)すべてラップホストの RN(0.79.7)と一致する必要があります。差異があると、バイナリがホストの読み込む RN とは異なるバージョンに対してコンパイルされ、デバイス上で ABI の不一致が無言で発生します。
10未解決のネイティブ参照(ヒューリスティック)ios/ と android/ 内の *.kt、*.m、*.swift ファイルをスキャンします。各関数/メソッドの呼び出し箇所について、同じファイル内に定義があるか、先頭に対応する import/#import があるかを確認します。/build-android-binary // /build-ios-binary のコンパイルで検出される前に、Unresolved reference のバグを検出します。ヘルパーメソッドが呼び出されているのに出力されていなかった(例:createTopNavBar())という過去のリグレッションも、ここで表面化します。
11SDK 時代の名残がないこと(/audit-ppmplugin の拒否リストを反映)廃止された TS 拡張モデルに属するシンボルについて、ネイティブソースと package.json(PCF は対象外)を grep します。対象は、INativeExtension の import/implements INativeExtension、sendAsync トランスポート呼び出し、handleMessageAsync エントリーポイント、extensionClassName/jsLayer フィールド、または Android モジュール上の @ReactModule アノテーションです。.ppmplugin が配布するのはネイティブバイナリのみであり、NativeModules.<nativeModule>.<method> に直接ディスパッチします。これらのシンボルは配布バンドルに含まれてはいけません。ネイティブソース/package.json 内で検出された場合は警告してください(/audit-ppmplugin の時点では重大な CRITICAL になるため、今すぐ修正してください)。注: PCF(pcf/…/index.ts)内の sendAsync は正しく、必須です。名残のスキャン対象はネイティブ/バンドルソースのみであり、PCF は対象外です。ppmplugin-format §6(この形式で対象外のもの)を参照してください。
12コンストラクター/init{} の安全性(起動時クラッシュの静的解析)スキャン対象をファイル全体ではなく、モジュールの構築クロージャに限定します。 対象は、プライマリ/セカンダリコンストラクター、init{} ブロック、プロパティ初期化子(構築時に実行される private val x = …)、およびそれらが呼び出すプライベート関数(foo()/this.foo() を 1 階層たどります)です。このクロージャ内で、次のものを検出します:register*Callback(…, null)、明示的な Looper を指定しない Handler()/Handler(...)、および try { } catch で囲まれていない副作用のある呼び出し(register*/add*Listener/observe/getSystemService とその使用/ファイルまたはネットワークの I/O/runBlocking)。@ReactMethod の本体は対象外です(構築時ではなく、呼び出しごとに実行されます)。モジュールはブリッジ起動時に、Looper がない可能性のあるスレッド上で即座に構築されます。このとき未処理例外が発生すると、UI が表示される前に起動時にホストがクラッシュします(ppmplugin-format §5)。検出された場合は警告してください。初回呼び出し時まで初期化を遅らせる、Handler(Looper.getMainLooper()) を渡す、または避けられない初期化を try/catch で囲むよう提案してください。iOS にも同様のルールが適用されます: 例外をスローする/負荷の高い init([cls new] によるモジュールの即時インスタンス化)にも同じルールが適用されます(§5b)。ここでの検出はヒューリスティックです。確定的な静的検出は、現在 Layer 1 の Load & initialization readiness アサーションが担います(明確なトリガーに加え、ReactPackage の引数なしコンストラクター/iOS の [cls new]/requiresMainQueueSetup のロードチェックをハードゲートします)。これは /audit-ppmplugin の src-ctor-no-throwable-sideeffects として反映されます。実行時の検出は Layer 5 の起動時クラッシュスキャンが担います。
13PCF がレスポンスコンテナーを展開することpcf/<Pascal>PCF/<Pascal>PCF/index.ts の invokeBridge/sendAsync の成功パスでは、単一の JSON.parse だけではなく、extractResponse のような展開処理(result.data をパースし、message コンテナーを調べる)を実行する必要があります。sendAsync の結果パスに extractResponse( の呼び出し(またはインラインの "message" in 展開)があるか、パターンマッチで確認します。ラップトランスポートではモジュールの JSON が message キー({isUpdate, message})の下にネストされています。単一のパースしか行わない PCF はコンテナー自体を取得してしまい、ネイティブ側が成功していてもすべての呼び出しが UNEXPECTED_PAYLOAD で失敗します(ppmplugin-format §2)。展開処理がない場合は警告してください。これは /audit-ppmplugin Category F の pcf-response-unwraps-message として反映されます。
14リスナー/リソースのリーク(登録後に解放されない)モジュール内で取得された各 register*/add*Listener/observe/getSystemService リソースについて、invalidate()/onCatalystInstanceDestroy()/終了処理パスで対応する解放(unregister*/remove*Listener/.close()/.release())が行われるかを確認します。登録済みコールバックや取得したマネージャーに解放処理がないと、モジュールのライフサイクルをまたいでリークし、破棄済みモジュールに対して呼び出しが行われる可能性があります。解放処理がない場合は警告し、invalidate() で登録解除するよう提案してください。
15Promise が必ず解決または拒否されること(ハング防止)Promise/resolver+rejecter を受け取る各 @ReactMethod(Android)/RCT_EXPORT_METHOD(iOS)には、到達可能なパス上に少なくとも 1 つの promise.resolve/promise.reject(または resolve(...)/reject(...))が必要です。Promise を一度も解決または拒否せずに戻るメソッドは、maker にハングしたコントロールを残し、コード/メッセージも通知しません。resolve/reject の箇所が 0 の Promise 受け取りメソッドは警告してください(確実にハングします)。
16危険な権限が宣言されているのに確認されていないAndroidManifest.xml で危険な権限(CAMERA、RECORD_AUDIO、ACCESS_FINE/COARSE_LOCATION、READ/WRITE_EXTERNAL_STORAGE、READ_CONTACTS、…)が宣言されている場合、モジュールのソースには checkSelfPermission/ContextCompat.checkSelfPermission/権限リクエストへの参照が必要です。API 23 以降では、manifest での権限付与だけでは不十分です。実行時チェックなしで API を呼び出すと SecurityException が発生します。宣言されているのに確認されていない場合は警告し、権限を確認して、拒否された場合に PERMISSION_DENIED を解決するよう提案してください。
17currentActivity の null ガードモジュール内の currentActivity の使用箇所はすべて null ガードする必要があります(currentActivity ?: return …/currentActivity?./明示的な == null チェック)。currentActivity!! や currentActivity.<member> による無条件のデリファレンスを検出してください。アプリがバックグラウンドにある場合、currentActivity は null です。ガードのないデリファレンスは NPE を引き起こし、ホストをクラッシュさせます。ガードがない場合は警告し、ガードを追加して NO_ACTIVITY を解決するよう提案してください。
実行方法

各チェックでは、スキルが grep/読み取り/比較処理を順に実行します。

# Example for check #1 (routing key)
PRD_CLASS=$(grep -oE "Class name \(Pascal\) \| .+" PRD.md | sed 's/.* | //')
MANIFEST_KEY=$(grep -oE '"name"\s*:\s*"[^"]+"' "$MANIFEST" | head -1)  # first receiver name ($MANIFEST = ./manifest.json or staged copy)
PCF_KEY=$(grep -oE 'ROUTING_KEY = "[^"]+"' pcf/${PRD_CLASS}PCF/${PRD_CLASS}PCF/index.ts)
# Compare; if mismatch, print:
#   ⚠️  Layer 0 check 1 (Routing key): manifest says '<X>', PCF says '<Z>'
#       Fix: align both to '<expected>'

チェック #10(未解決のネイティブ参照)では、ヒューリスティックな grep フローを使用します。

# For each .kt file in android/, build the set of in-file definitions + imports,
# then for each call site, check membership.
for kt in $(find android/src -name "*.kt"); do
  IN_FILE_FUNS=$(grep -oE 'fun\s+[a-zA-Z_][a-zA-Z0-9_]*' "$kt" | awk '{print $2}' | sort -u)
  IMPORTS=$(grep -oE '^import\s+[a-zA-Z0-9_.]+(\.[a-zA-Z0-9_*]+)?$' "$kt" | awk '{print $2}' | awk -F. '{print $NF}' | sort -u)
  # Call sites: identifiers followed by `(`, excluding keywords + same-line definitions
  CALL_SITES=$(grep -oE '\b[a-zA-Z_][a-zA-Z0-9_]*\(' "$kt" \
               | sed 's/($//' \
               | grep -vE '^(if|when|while|for|return|require|listOf|arrayOf|mapOf|setOf|Pair|Triple|let|run|with|apply|also|takeIf|takeUnless)$' \
               | sort -u)
  # Flag any call site not in IN_FILE_FUNS or IMPORTS or known Android/Kotlin builtins
  ...
done

(.m/.swift についても同じパターンを使用し、Obj-C セレクター/Swift 関数宣言に合わせて正規表現を調整します。)これはヒューリスティックな手法であり、インポートされた型のメンバー呼び出しと未定義の関数呼び出しを完全には区別できませんが、主要なケース(createTopNavBar() が呼び出されているのに、fun createTopNavBar がどこにもなく、それを提供する可能性のあるインポートもないケース)は検出できます。

スキルは17個すべてのチェックを実行し、検出結果を集約して、レイヤーの最後に番号付きリストとして出力します。

合格

ネイティブモジュール、マニフェスト、PCF のすべてで、各チェックの結果が一致しています。✓ Layer 0 (contract consistency): pass — <ISO time> を出力します。

警告(レイヤー 1 に続行し、実行は失敗しません)

1つ以上のチェックで不一致が見つかりました。次のように出力します。

⚠️  Layer 0 (contract consistency): <N> warning(s)

1. Routing key mismatch:
   - manifest receivers[].name: '<Pascal>Extension'  ✓
   - pcf/.../index.ts uses: '<Pascal>'                ✗ — fix this
   Suggested fix: in pcf/<Pascal>PCF/<Pascal>PCF/index.ts line N, change ROUTING_KEY to "<Pascal>Extension"

2. Response field name mismatch:
   - ARCHITECTURE §4.2 expects: 'signatureBase64'
   - ios/.../Module.m emits key: 'result'  ✗ — should be 'signatureBase64'
   - android/.../Module.kt emits key: 'result'  ✗
   - pcf/.../index.ts reads: response.result.signatureBase64
   Suggested fix: ARCHITECTURE §4.2 and the native emit sites disagree. Either update the native modules to emit 'signatureBase64', or update ARCHITECTURE §4.2 + the PCF read to use 'result'.

...

修正してから続行する(修正後に再実行する)か、レイヤー 1 に進む(不整合を認識したうえで続行する)かはユーザーが判断します。AskUserQuestion を使用します。

Layer 0 found contract inconsistencies. Proceed?

  • Continue to Layer 1 — warnings recorded in .extension-state.md but don't block
  • Stop here, fix the warnings first — exit; user re-runs after fixing
失敗(実行を停止します)

ハードフェイルとなるのは、チェックが参照するソースファイルが完全に欠落している場合です(例:android/ 配下に <Pascal>Module.kt がない、ios/ 配下に RCT<Pascal>Module.m がない)。これはコントラクトの不整合ではなく、成果物の欠落です。❌ Layer 0 (contract consistency): cannot proceed — <missing file> を出力して停止します。


ステップ 3 — レイヤー 1:ネイティブソースの構造アサーション

このトラックでは実行する tsc はありません。コントロールは TS 拡張機能ではなくネイティブバイナリとして提供されます。その代わり、ネイティブモジュールのソースを grep/解析し、ラップランタイムとプラグインのアップロード互換性チェックに適した構造になっていることをアサートします。これらは /build-android-binary と /build-ios-binary がコンパイル前に実行するのと同じ適合性アサーション(ppmplugin-format §5、§5b)です。ここで実行することで、数分かかるビルドの前に、数秒で修正できるミスを見つけられます。

Android(android/.../<Pascal>Module.kt)の場合:

  • モジュールクラスが ReactContextBaseJavaModule を継承すること(ホストはこれを React Native モジュールとして読み込みます)。
  • override fun getName() が 標準のプレフィックス付きの '<Pascal>Module'(ppmplugin-format §3)を返すこと。マニフェストがステージングされている場合は、マニフェストの receivers[].nativeModule と一致する必要があります。
  • 少なくとも1つの @ReactMethod fun <m>(...) が宣言されていること(@ReactMethod がないモジュールは何もディスパッチしません)。
  • ReactPackage が存在すること(DEX が必要とする createNativeModules の登録であり、その FQN は entrypoints.android.packageClass になります)。
  • @ReactModule アノテーションがないこと(これは SDK 時代の登録方法です。ラップホストはアノテーションではなく ReactPackage 経由で登録します。アノテーションが存在すると SDK の混入となり、/audit-ppmplugin によって拒否されます)。

iOS(ios/RCT<Pascal>Module.h/.m)の場合:

  • ヘッダーのクラスが <RCTBridgeModule> を宣言していること。
  • RCT_EXPORT_MODULE(...) マクロが存在しないこと。.m で + (NSString *)moduleName を宣言し、その戻り値の文字列が Android の getName() およびマニフェストの receivers[].nativeModule と一致すること。Obj-C クラス名が entrypoints.ios.moduleClass と一致すること。
  • 少なくとも1つの RCT_EXPORT_METHOD(<m>:...) が宣言されており、メソッド名がマニフェストの receivers[].methods[] のサブセットであること。
読み込みと初期化の準備状況(起動時のクラッシュ/読み込めない場合のアサーション)— HARD ゲート

最も多く報告されるフィールド障害は、「アプリをインストールできない、または起動時にクラッシュする」ことです。その根本原因は、ほとんどの場合、プラグインが読み込めないか、UI が表示される前にモジュールの即時構築時に例外が発生することです。これらはソースから確認できるため、警告のみの Layer 0 のチェックやビルド後の監査に委ねず、ここでアサーションしてください(失敗時は Layer 1 で停止します)。ppmplugin-format §5(Android DEX の読み込み+即時構築)および §5b(iOS の dlopen + [cls new])に基づいています。

Android(android/.../<Pascal>Package.kt +モジュール):

  • ReactPackage クラスには public な引数なしコンストラクターがあること。パラメーターリスト付きの class <Pascal>Package(...) は不可です。ラップランタイムは getDeclaredConstructor().newInstance() でインスタンスを生成します。引数付きコンストラクターでは NoSuchMethodException が発生し、プラグインは通知なく読み込みに失敗します(Loaded 0 plugin package(s))。モジュール自体は ReactApplicationContext を受け取ってもかまいませんが、パッケージは引数なしでなければなりません。
  • 構築クロージャーで例外が発生しないこと。モジュールのコンストラクター、init{} ブロック、プロパティ初期化子(および、それらが呼び出すプライベート関数を 1 階層分)を調べ、確実なクラッシュ要因を確認してください。対象となるのは、Looper がない可能性のあるスレッドでの裸の Handler() や register*Callback(…, null)、未捕捉の I/O エラーやハードウェアの取得です。ここで例外が発生すると、起動時にホストがクラッシュします(これは Layer 0 の #12 の経験則を強化し、ハードゲートにしたものです。Layer 0 は広く警告し、Layer 1 は明確な要因があればブロックします)。

iOS(ios/RCT<Pascal>Module.{h,m}):

  • モジュールクラスが [cls new] でインスタンス化されること。引数を取るカスタム初期化子は不可です(プレーヤーは NSClassFromString(moduleClass) → [cls new] を実行します。引数付きの初期化子しかない場合、モジュールは読み込み時にスキップされます)。
  • (宣言されている場合)+ (BOOL)requiresMainQueueSetup が NO を返すこと。YES を返すと、起動時にメインスレッドでセットアップが強制されるため、処理の重い、または例外を発生させる init と組み合わさると、起動が停止したりクラッシュしたりします。
  • init/+load では例外を発生させる処理や重い処理を行わないこと。Android と同じ即時構築のルールを適用し、ハードウェアやリスナーの処理は最初のメソッド呼び出しまで延期してください。
合格

🟢 ✓ Layer 1 (native-source structure): pass — <ISO time> と出力します。Layer 2 に進みます。

失敗

上記のアサーションが失敗しました。最初の 3 件の不一致を次の形式で示します — <file>:<line> — <what's wrong>。よくある分類と修正方法:

  1. 関連する grep のヒット/不一致を取得します。
  2. 最も関連性の高い 最初の 3 件の不一致を表示します — grep の行をすべて表示するのではありません。
  3. 分類ごとに修正方法を提案します:
    • Android の getName() が '<X>' を返す一方、iOS の +moduleName/マニフェストの nativeModule が '<Y>' を返す → ランタイムシンボルが一致していません。3 つすべてを正規のプレフィックス付き '<Pascal>Module' に揃えます(ppmplugin-format §3)。導出名が予約済みプレフィックス/拒否リストに該当する場合は、ppmplugin-format §4 に従って名前を変更します。Obj-C クラス名が entrypoints.ios.moduleClass と異なる場合は、クラスまたはマニフェストのエントリーポイントを別途揃えます。
    • モジュールが ReactContextBaseJavaModule を(Android で)継承していない/<RCTBridgeModule> を(iOS で)宣言していない → ネイティブモジュールとして登録されません。クラス宣言を修正します。
    • @ReactMethod/RCT_EXPORT_METHOD が見つからない → モジュールがホストから呼び出せるものを公開していません。ARCHITECTURE §4 に従って操作メソッドを追加します。
    • Android モジュールに @ReactModule アノテーションがある → SDK 時代の名残です。ラップホストは ReactPackage 経由で登録するため、削除します。これは /audit-ppmplugin 実行時の重大な CRITICAL です。
    • ReactPackage に引数付きコンストラクターがある(class <Pascal>Package(...))→ プラグインがデバイス上で 0 個のパッケージを読み込みます。公開された引数なしコンストラクターを用意します(ppmplugin-format §5)。
    • 構築処理のクロージャーが例外をスローする(Looper がない状態での Handler()、register*(…, null)、コンストラクター/init{} 内で捕捉されない I/O)→ 起動時にホストがクラッシュします。初回呼び出し時の遅延初期化に変更し、Handler(Looper.getMainLooper()) を渡し、避けられない初期化は try/catch で囲みます。
    • iOS モジュールに引数なしの init がない/requiresMainQueueSetup が YES を返す/init が重い処理や例外をスローする処理を行う → dlopen 読み込み時にモジュールがスキップされるか、起動が停滞します。[cls new] でインスタンス化し、+requiresMainQueueSetupからNO を返し、init の処理を軽く保ちます(ppmplugin-format §5b`)。
  4. .extension-state.md を更新します:Native-source structure (Layer 1): fail — <timestamp>;Status: blocked;Blocked reason: <first mismatch>。
  5. 停止します。後続のレイヤーは実行しないでください。

ステップ 4 — レイヤー 2:マニフェストの検証

マニフェストが見つからなかった場合に限り自動スキップします(ステップ 2 の検出によると $MANIFEST が空 — ./manifest.json もステージ済みのコピーもない場合)。⊝ Layer 2 (manifest validation): SKIPPED — no manifest on disk (hand-authored module). Run /generate-ppmplugin-manifest first. と表示します。状態を n/a として記録します。レイヤー 3 に進みます。(スキャフォールドでは native-gen によって ./manifest.json が作成されるため、通常このレイヤーはここで実行されます。)

マニフェストがディスク上にある場合($MANIFEST — コミット済みの ./manifest.json またはステージ済みのコピー)、プラグイン管理のアップロード互換性チェックを同じものに対してローカルで再実行します。これは /generate-ppmplugin-manifest が実行するチェックと同じです(ppmplugin-format §4)。この簡易な事前チェックにより、ビルド前に、誤った形式の name や nativeModule など、よくあるアップロード失敗を検出できます。このレイヤーでは、ルールセットの参照元として /generate-ppmplugin-manifest を優先します。マニフェストを新たに作成するのではなく、ディスク上にあるマニフェストだけを検証し、修正方法についてそのスキルを案内します。

合格

✓ Layer 2 (manifest validation): pass — <ISO time> と表示します。レイヤー 3 に進みます。

失敗

ppmplugin-format §4 のルールに違反しています。よくあるクラスと修正方法:

クラス対応
name の正規表現に不一致/クラス名のケバブケースではないname は CLASS 名のケバブケースである必要があります(ppmplugin-format §3)。/generate-ppmplugin-manifest を再実行して再導出してください。
正規プレフィックス違反各 receivers[].nativeModule は、name の正規プレフィックス(-/_ で分割し、それぞれをパスカルケースにして結合したもの)で始まる必要があります。モジュールの getName() の名前を変更するか、name を修正してください。
予約済みプレフィックス/拒否リストnativeModule が予約済みプレフィックス、または Microsoft 所有の単独名を使用しています(ppmplugin-format §4)。予約されていない形式に変更してください(Module サフィックスまたはベンダープレフィックスを追加します)。
methods[] とソースの不一致methods[] 内のメソッドに対応する @ReactMethod/RCT_EXPORT_METHOD がモジュールにない、またはその逆です。/generate-ppmplugin-manifest を再実行し、ソースから methods を再導出してください。

状態を更新します:Manifest validation (Layer 2): fail — <timestamp>;Status: blocked。停止します。

スコープに関する注意: このレイヤーでは manifest.json のみを検証します。ネイティブソースの 構造 はレイヤー 1 の対象です。宣言されたバイナリが実際に存在するかどうかは /assemble-ppmplugin の照合ゲートで確認し、ビルドされた .ppmplugin がデバイスで読み込まれるかどうかは /audit-ppmplugin で確認します。


ステップ 5 — レイヤー 3:ネイティブソースの契約アサーション

このトラックでは実行する tsc --noEmit フィクスチャはありません。TS の src/ も、型フィクスチャで形状を固定できる src/types.ts もないためです。その代わり、このレイヤーでは ARCHITECTURE §4 から期待されるフィールドセットを導出し(PRD がない場合はネイティブソースを信頼できる唯一の情報源として扱います)、2 つのネイティブパーサー(iOS .m、Android .kt)と PCF の間でリクエスト/レスポンス/エラーコードの契約を grep で相互検証します。これはレイヤー 0 のチェック 4~6 をより深く検証するものです。レイヤー 0 では全体的なチェックの一部として警告のみを出しますが、レイヤー 3 では合否判定を行います。

各操作について:

  • リクエストフィールド — iOS パーサーの NSDictionary キー読み取り、Android パーサーの ReadableMap キー読み取り、および PCF の request.<field> の構築で、同じフィールド名を参照する必要があります(ARCHITECTURE §4.1)。一方のプラットフォームで読み取られ、もう一方で読み取られないフィールドは、プラットフォーム固有の INVALID_INPUT になります。
  • レスポンスフィールド — iOS の successJsonWith: の構築、Android の successJson の構築、および PCF の response.result.<field> の読み取りで、同じキーを使用する必要があります(ARCHITECTURE §4.2)。
  • エラーコード — ARCHITECTURE §5 のすべてのコードは、少なくとも 1 つのネイティブ箇所(iOS の errorJsonWithCode:message:、Android の errorJson(code, message))で出力され、PCF の setError(case または default)で処理される必要があります。
  • エラーメッセージの受け渡し — ネイティブのエラーヘルパーは message 引数を受け取る必要があります。PCF の setError(code, message) は 2 引数を受け取り、this.errorMessage に代入する必要があります。また、ControlManifest は 3 つの標準診断出力(Status、ErrorCode、ErrorMessage)を宣言し、getOutputs() はそれらを返す必要があります。これにより、ネイティブコンソールを使わずに Power Fx からフィールドの失敗をデバッグできます。
合格

✓ Layer 3 (native-source contract): pass — <ISO time> と出力します。レイヤー 4(実行する場合)に進みます。

失敗

ここで最もよくある失敗は、一方のプラットフォームではフィールドを読み取り/出力しているのに、もう一方では読み取り/出力していないケースや、§5 で宣言されたエラーコードを出力するネイティブ箇所がないケースです。

エラークラス考えられる修正
一方のパーサーにフィールドがあり、もう一方にはないiOS と Android のパーサーにずれが生じています。ARCHITECTURE §4.1 に従って両方が同じフィールド名を参照するよう、不足しているキー読み取りを追加するか、余分なものを削除してください。
ネイティブにはあるが PCF では読み取られないレスポンスキー(またはその逆)ネイティブは PCF が読み取らないキーを出力しているか、PCF がどのネイティブ箇所からも出力されない response.result.<x> を読み取っています。ネイティブの構築箇所と PCF の読み取りを一致させてください。
§5 のエラーコードを出力するネイティブ箇所がない宣言されたコードに到達できません。該当するネイティブのエラーパスから出力するか、§5 から削除してください(PRD との不整合)。

状態を更新します:Native-source contract (Layer 3): fail — <timestamp>;Status: blocked。停止します。レイヤー 4 は実行しないでください。


ステップ 5.5 — レイヤー 3.5:モックコンテキストのランタイム契約

このトラックに相当するものはありません — 常に n/a です。 モックコンテキストのランタイムレイヤーでは、TS 拡張クラス(new <Pascal>Extension(ctx))をインスタンス化し、最小限/完全なホストコンテキストの形状に対して handleMessageAsync を実行していました。.ppmplugin には TS 拡張クラスも、handleMessageAsync も、ホストの INativeExtensionContext も含まれていません — ディスパッチはラップブリッジ経由で NativeModules.<nativeModule>.<method> に直接送られます(wrap ブリッジ、「ppmplugin-format §2」、§6)。デバイス外でインスタンス化できるものはありません。

ネイティブのみのバンドルで「実際に動作するか」を保証する同等の確認は、次の 2 か所で行います。どちらもこのスキルではツールチェーン外で実行できません。

  • ネイティブコードが読み込み可能なバイナリにコンパイルされるか → /build-android-binary // /build-ios-binary(実際の Gradle/xcodebuild)。
  • ビルド済み .ppmplugin が wrap ランタイムで読み込まれ、ディスパッチされるか → /audit-ppmplugin(バイトスキャン+構造ゲート)とレイヤー 5 のデバイス手順。

⊝ Layer 3.5 (mock-context runtime): n/a — native-only track has no TS extension class to instantiate (see /audit-ppmplugin + Layer 5). と表示し、レイヤー 4 に進みます。


ステップ 6 — レイヤー 4:PCF のコンパイル

pcf/ 配下に ControlManifest.Input.xml が見つからない場合は自動的にスキップします(ステップ 2 の find ベースの検出によるものです)。⊝ Layer 4 (PCF compile): SKIPPED — PCF not yet scaffolded. Run /generate-pcf-companion first. と表示します。状態は n/a とします。レイヤー 5 に進みます。

このレイヤーでは、コンパニオン PCF(マニフェスト XML+index.ts+エンジニアによるカスタマイズ)をコンパイルします。次の問題を検出します。

  • index.ts の TS エラー(スキャフォールド後に手動編集で発生)
  • ControlManifest.Input.xml と index.ts のプロパティ名の不一致(例:マニフェストでは PenColor と宣言されているのに、コードでは p.penColor を参照している)
  • 出力プロパティ宣言の欠落(例:§8.3 を編集したが、PCF を再生成していない)
  • pcf-scripts の依存関係の破損

PCF ツールでは pnpm ではなく npm を使用します — これは PCF エコシステムの慣例です。統一しないでください。

実行

リポジトリのルートから実行します。

macOS/Linux/Windows(同じコマンド):

cd "$PCF_PROJECT_ROOT"   # from the find-based detection in Step 2
[ -d node_modules ] || npm install --no-audit --no-fund   # first time only; ~30s
npm run build --silent

注:

  • npm install は初回実行時(または node_modules/ が削除された場合)のみ実行されます。以降の実行は、ウォームキャッシュで pcf-scripts build が高速なため、数秒で完了します。
  • --silent によって出力を簡潔に保ちます。エラーは引き続き表示されます。
  • npm run build は pcf-scripts build を実行し、次の処理を行います。
    1. マニフェスト XML から pcf/<Pascal>PCF/<Pascal>PCF/generated/ManifestTypes.d.ts を再生成します。
    2. 再生成された ManifestTypes.d.ts に対して index.ts の型チェックを行います。
    3. 出力を pcf/<Pascal>PCF/out/ にバンドルします。
成功

npm run build が終了コード 0 で終了し、out/ アーティファクトを生成します。✓ Layer 4 (PCF compile): pass — <ISO time> と表示します。

失敗

npm run build がゼロ以外の終了コードで終了します。よくあるエラーの種類:

エラーの種類考えられる修正
Property '<X>' does not exist on type 'IInputs'マニフェストで宣言された名前と、index.ts で参照される名前が異なります。ControlManifest.Input.xml と index.ts を並べて開き、プロパティ名を一致させてください(大文字と小文字を区別します)。
Property '<X>' is missing in type(getOutputs() の戻り値で発生)マニフェストに出力が追加されていますが、getOutputs() から返されていないか、その逆です。不足しているフィールドを getOutputs() に追加するか、不要なマニフェストエントリを削除してください。
Cannot find module 'pcf-scripts'初回実行時の npm install が完了していません。スキルで実行されているはずですが、必要に応じて npm install を手動で再実行してください。
XML parsing failed at line <n>マニフェスト XML の形式が正しくありません。多くの場合、タグが閉じられていないか、デフォルト値を設定できないプロパティに default-value 属性が指定されています(例:usage="output" のプロパティにはデフォルト値を設定できません)。
index.ts の標準的な tsc エラーエンジニアが加えた回帰です。行と列を確認し、ソースを修正してください。

状態を更新します:PCF build (Layer 4): fail — <timestamp>;Status: blocked。停止します。レイヤー 5 は実行しないでください。


ステップ 6.5 — レイヤー 4.5:Android lint

「Android ビルドで lint が原因でこのモジュールが拒否される」種類のバグをローカルで検出します。 pen-input で実際に発生したコストは、統合ビルドの実行時に初めて Android lint の警告が 8 件検出され、パッチの再適用が必要になったことです。いずれもここで検出できたはずです。

Android Studio の lint ルールは、簡単な grep よりも厳格です(呼び出しパターン、リソース参照、非推奨 API、アクセシビリティをチェックします)。/build-android-binary はステージングコピーのモジュールを固定された AGP に対してコンパイルします。ここで先に ./gradlew lint を実行すると、そのビルドの前に同じ警告を検出できます。

実行方法
# Detect whether gradle wrapper is present
if [ -f android/gradlew ]; then
  cd android && ./gradlew lint --warning-mode=summary
fi

android/gradlew が存在しない場合(このレイヤーが標準になる前に拡張機能のひな形が作成されていた場合)→ Layer 4.5 (Android lint): skipped (no gradle wrapper at android/gradlew — hand-add the gradle wrapper to android/). としてスキップします。実行を失敗扱いにせず、不足していることだけを記録します。

Gradle ラッパーは存在するものの ./gradlew lint がゼロ以外で終了した場合 → android/build/reports/lint-results-debug.html(または stderr)のレポートを解析し、警告を番号付きリストで表示します。

合格

✓ Layer 4.5 (Android lint): pass — <ISO time> と出力します。レイヤー 5 に進みます。

警告(実行は失敗扱いにせず、レイヤー 5 に進みます)
⚠️  Layer 4.5 (Android lint): <N> warning(s) — the Android build may reject these

1. NewApi at <file>:<line>
   Call requires API level X (current min is Y). Either bump minSdk, gate the call,
   or suppress with @TargetApi(X) annotation.

2. UnusedResources at res/drawable/ic_unused.png
   Drawable not referenced; safe to remove.

... etc.

続いて AskUserQuestion を実行します。

Question: "Android lint found <N> warning(s). What now?"
Header:   "Lint warnings"
Options:
  1. "Continue to Layer 5"        description: "Warnings noted in .extension-state.md. Fix later; not all warnings block /build-android-binary."
  2. "Stop, I'll fix first"       description: "Skill exits. Address the warnings (typically straightforward), then re-run /test-native-extension."
  3. "Show me the full lint report"  description: "Print the full HTML report path + key offending lines, then re-ask."
失敗

./gradlew lint がエラーで終了した場合(警告ではなく、Gradle デーモンのクラッシュや依存関係の解決失敗などの実際のエラー)→ エラーの末尾を表示し、Layer 4.5: fail — <one-line cause> として停止します。ユーザーは再試行する前に Gradle の設定を修正します。

スキップ

プロジェクトに android/ フォルダーがまったくない場合(TS のみ、または iOS のみの拡張機能 — まれ)→ Layer 4.5: skipped (no android/ in repo). とします。レイヤー 5 に進みます。


ステップ 7 — レイヤー 5:デバイス/シミュレーターの手動ガイダンス

このレイヤーは対話的なものであり、自動化の対象外です。ビルドおよび監査済みの .ppmplugin、それを読み込むラップホスト、デバイスまたはシミュレーター、そしてテスト環境にデプロイされたコンパニオン PCF が必要です。スキルはコピーして貼り付けられる手順を出力し、エンジニアが実行して、スキルが結果を記録します。

レイヤー 0〜4 がすべて成功した後も、レイヤー 5 が重複ではない理由。 レイヤー 0〜4 では構造を検証します。シンボルが一致していること、キーが一致していること、マニフェストが整合していること、PCF がコンパイルできることを確認します。レイヤー 5 で初めて、実際のバイト列がワイヤー上を移動します。 すべての構造レイヤーでは検出できない、ワイヤーフォーマットや実行時のバグがここで初めて表面化します。たとえば、sendAsync ではなく cordova.exec を直接呼び出す(→何もディスパッチされず、Android では無言のまま)、ホストがレスポンスを再度文字列化する、PCF がアンラップする必要のあるラップの {message} レスポンスコンテナ、起動時に例外をスローするネイティブモジュールのコンストラクター(→UI が表示される前に起動時クラッシュ)、実行時の権限拒否などです。レイヤー 0〜4 がすべて成功しているということは「形状が正しい」という意味であり、「デバイス上でディスパッチされる」という意味ではありません。自動テストがすべて成功しても、エンドツーエンドで問題ないとは判断しないでください。最初の起動と最初のタップが本当のテストです。(レイヤー 0 では、デバイスに接続する前に、sendAsync トランスポート、コンストラクターのクラッシュパターン、レスポンスのアンラップについて、チェック 1b/12/13 の grep で確認します。以下のレイヤー 5 のクラッシュスキャンが決定的な検出方法です。)

出力:

Layer 5 — Manual end-to-end validation (interactive)
─────────────────────────────────────────────────────

This validates the full path: Canvas formula → PCF → SendMessagePlugin bridge → NativeModules.<nativeModule>.<method> → response back to Canvas.

PREREQUISITES (one-time per machine):
  • A built + audited bundle: run /generate-ppmplugin (or the stage skills) so ppmplugin/<name>.ppmplugin exists and passed /audit-ppmplugin
  • A wrap host configured to side-load the .ppmplugin (the wrap shell from the wrap pipeline — its load/upload flow is owned there)
  • Active pac auth profile against your test environment (`pac auth list` — star should be on the test env)
  • For iOS device: a provisioning profile that allows running the wrap app on your device
  • For Android device: USB debugging enabled + `adb devices` shows your device

BUILD & AUDIT THE BUNDLE (each iteration of the control):

  # 1. From the control repo, produce a verified bundle:
  cd <this repo>
  /generate-ppmplugin
  # → drives manifest → build-android/ios → assemble → audit; lands ppmplugin/<name>.ppmplugin

  # 2. Confirm it's READY TO UPLOAD:
  /audit-ppmplugin
  # → CRITICAL-free verdict before you side-load it

LOAD INTO THE WRAP HOST:

  # Side-load ppmplugin/<name>.ppmplugin into the wrap host per the wrap pipeline' load flow.
  # The host runtime registers the DEX's ReactPackage / the iOS framework's moduleClass +moduleName;
  # the manifest's receivers[] become the routing keys the SendMessagePlugin bridge dispatches to.

DEPLOY THE PCF (if not already done):

  # Easiest: use the dedicated skill — it handles prefix selection + env confirmation:
  /publish-pcf-companion

  # Manual equivalent (substitute a 2–8 char prefix):
  cd pcf/<Pascal>PCF
  pac pcf push --publisher-prefix pamext
  # The control appears in Canvas Studio under Custom controls > PowerApps namespace.

RUN ON DEVICE:

  # iOS: build & run the wrap app on simulator or device (Xcode).
  # Android: install the wrap app on connected device / running emulator.

RUNTIME CRASH-SCAN (Android — the DEFINITIVE catch for the launch-crash + response-shape classes
that every static layer is blind to; run right after launching the wrap app):

  # A) CONSTRUCTION CRASH — a native-module constructor / init{} that throws at bridge startup
  #    takes down the host BEFORE any UI (e.g. registerTorchCallback(cb, null) on a Looper-less
  #    thread → "Can't create handler…"). Launch the app, then scan logcat for the signatures:
  adb logcat -c
  adb shell monkey -p <wrap-package> -c android.intent.category.LAUNCHER 1 >/dev/null
  sleep 8
  if adb logcat -d | grep -iE "Looper\.prepare|Can't create handler|Loaded 0 plugin package|UnsatisfiedLinkError|FATAL EXCEPTION"; then
    echo "✗ LAUNCH CRASH / plugin-load failure — see the matched line (fix: defer ctor side-effects to lazy init, Handler(Looper.getMainLooper()), try/catch)"
  else
    echo "✓ no launch crash / plugin-load failure in the first 8s"
  fi

  # B) RESPONSE ROUND-TRIP — a PCF that only deepParses misses the wrap {isUpdate, message:"<json>"}
  #    container and fails EVERY call with UNEXPECTED_PAYLOAD though native succeeded. On the canvas
  #    screen bind a label to Self.<name>Json, tap the control once, and read it:
  #      ✓ shows the module's own {"status":"ok",…}                     → PCF unwraps correctly
  #      ✗ shows {"isUpdate":…,"message":"{\"status\"…"}               → PCF isn't unwrapping the
  #        container (fix: extractResponse, not a bare deepParse — /generate-pcf-companion + §2)
  #      ✗ Status=error, ErrorCode=UNEXPECTED_PAYLOAD                    → same root cause

INSTALL-FAILURE SCAN (run if the wrap app won't install at all — before the launch scan):

  # Android — a failed `adb install` prints the reason; the common ones map to a fix:
  #   INSTALL_FAILED_INSUFFICIENT_STORAGE  → free space / wipe the emulator
  #   INSTALL_FAILED_UPDATE_INCOMPATIBLE / signatures do not match  → uninstall the old app first:
  #       adb uninstall <wrap-package>
  #   INSTALL_FAILED_NO_MATCHING_ABIS      → the .so / ABI in android/lib/<abi> doesn't match the device
  #   INSTALL_FAILED_OLDER_SDK             → device OS below the wrap app minSdk
  # iOS — install/launch failing before any JS runs is usually signing, not your module:
  #   "Unable to install" / codesign / provisioning  → the wrap app's profile doesn't allow this device
  #   dyld: Library not loaded / @rpath …<Name>.framework  → the framework wasn't embedded by the wrap
  #     pipeline (a packaging issue, not a source bug) — re-check /assemble-ppmplugin + /audit-ppmplugin.

RUNTIME CRASH-SCAN (iOS — the analogue of the Android scan; run right after launching the wrap app on
a simulator/device. A throwing/heavy init or a module that won't load surfaces in the device log):

  # Stream the wrap app's log and scan for the iOS launch/init + load-failure signatures:
  xcrun simctl spawn booted log stream --level=default --predicate 'processImagePath CONTAINS "<wrap-app>"' &
  # (device: use Console.app filtered to the app, or `idevicesyslog`.) Then launch the app and watch for:
  #   ✗ "Native module '<X>' not loaded" / NSClassFromString → nil   → +moduleName / dead-code-stripping / used RCT_EXPORT_MODULE (§5b)
  #   ✗ dyld: Symbol not found / @rpath …React                       → React linked strongly, or RN pin ≠ host RN
  #   ✗ *** Terminating app due to uncaught exception … in -[<Module> init]  → throwing init (defer to lazy first call)
  #   ✓ no such lines in the first ~8s after launch                  → no iOS launch/init crash

WHAT TO VERIFY (per ARCHITECTURE §8 Edge cases — copy into a checklist):

  [ ] Happy path: <one-line happy-path scenario>
       Expected: <Pascal>PCF1.<OutputName> = '<value>' per ARCHITECTURE §8
  [ ] Error case <CODE_1>: <how to trigger>
       Expected: <Pascal>PCF1.<OutputName> = 'error: <CODE_1>'
  [ ] Error case <CODE_2>: <how to trigger>
       Expected: <Pascal>PCF1.<OutputName> = 'error: <CODE_2>'
  ... (one row per error code in ARCHITECTURE §5)

TROUBLESHOOTING:

  • Bridge resolves "NOT_IN_PAM" → opening in Studio preview (no bridge). Open in the wrap app on a device.
  • "native module '<nativeModule>' not loaded" → the .ppmplugin didn't register. Re-run /audit-ppmplugin (catches the DEX/framework structure + SDK-leakage causes on disk) and re-side-load.
  • Bridge times out → check Logs via Xcode Console (iOS) or `adb logcat` (Android) filtered to `<nativeModule>` or `SendMessagePlugin`.

各 <...> はドキュメントに合わせて調整してください。成功パスのシナリオは ARCHITECTURE §6.2(ブリッジ配線のトリガー)と PRD §1(概要)から取得します。各エラー行は ARCHITECTURE §8 から取得します。

出力したら次に進んでください。ユーザーが手順を実行して結果を報告するのを待たないでください。デバイスの手動テストには 5〜10 分かかることがあり、スキルをそこで待機させるとイテレーションの流れが悪くなります。この手順は次のステップのガイダンスであり、ゲート付きのレイヤーではありません。

レイヤー 5 におけるスキルの役割はここで終了します。ユーザーは都合のよいときに手順を実行します。後から結果を記録するには、次のどちらかを行います。

  • .extension-state.md を直接編集する — 「Validation history」の下にある Manual / device (Layer 5): の行を、pass | partial | fail | manual、ISO タイムスタンプ、および(任意で)失敗した行に関するメモで更新します。
  • /test-native-extension を再実行する — レイヤー 1〜4 は短時間で済みます。レイヤー 5 の手順が再度出力されるため、ユーザーはもう一度実行した後、手動で状態を更新できます。

スキルの責任は、手順が正しく、現在の PRD に合わせて調整されていることを確認することです。実行と結果の記録はユーザーが行います。


ステップ 7.5 — レイヤー 6(任意):詳細な .ppmplugin 監査への案内

案内のみ — /audit-ppmplugin に誘導します。ここでは実行しません。 ビルド済みバンドルがディスク上にある場合にのみ関係します。

ビルド済みの ppmplugin/*.ppmplugin がある場合(このスキルの構造レイヤーはソースを対象としており、バンドルを解凍しません)、独自に再実装するのではなく、専用の詳細検証ゲートを案内します。

BUILT_BUNDLE=$(ls ppmplugin/*.ppmplugin 2>/dev/null | head -1)

$BUILT_BUNDLE が設定されている場合は、次を出力します。

ⓘ Layer 6 (deep bundle audit): a built bundle exists (<path>).
   Run /audit-ppmplugin for the byte-level verification this skill can't do on source:
   zip structure, manifest↔bundle consistency, DEX SDK-leakage byte-scan, iOS framework
   structure — the gate that proves it will actually load + dispatch on the wrap runtime.

ビルド済みバンドルがない場合は、このレイヤーを何も言わずに省略します(まだ監査対象がありません。ビルドスキルが実行されていません)。


Step 7.6 — Layer 7(オプトイン):ランタイム安全性に関する深い推論レビュー

オプトイン、助言目的、リポジトリ全体が対象です。Layer 0 の高速で決定論的な grep に対する、推論による補完です。 Layer 0(チェック 12~17)と /audit-ppmplugin の Category F では、ランタイム安全性に関する問題のうち、決定論的に検出できるものを検出します。たとえば、Looper を持たないコンストラクターの Handler、{message} レスポンスのアンラップ漏れ、resolve または reject 箇所が一つもない Promise を受け取るメソッド、宣言されているのにチェックされない危険な権限、ガードされていない currentActivity などです。対象を絞っているからこそ高速で、誤検出も少なくなっています。このレイヤーでは grep では検出できない問題を、広範な推論によって検出します。たとえば、一部の経路でしか解放されないリソース、エラーブランチで二重に resolve または reject される Promise、呼び出しを 3 階層たどった先でメインスレッド以外から表示される UI、ヘルパーで取得したまま破棄されないマネージャーなどです。

実行を提案するタイミング。 自動レイヤーのすべてが通過した後、コントロールにネイティブモジュール(ios/ または android/ が存在する)がある場合、Step 8.1 の次のステップのゲートで選択肢の一つとして提案します。デフォルトでは決して実行しないでください。 リポジトリ全体を対象に推論するため(時間がかかります)、結果は助言目的です。DONE_WITH_CONCERNS を返し、Overall をブロックに切り替えることはありません。広範な推論レビューで誤検出が起きても、動作するコントロールをゲートで止めてはなりません。

実行内容。 shared/self-critique-protocol.md を読み、全体レビュー モード(同ファイルの Run modes を参照)で実行します。つまり、今回変更したファイルだけではなく、コントロール内のすべてのソースファイル(ios/、android/、pcf/、manifest.json、AndroidManifest.xml / Info.plist、build.gradle / podspec)を列挙し、**Gate 9(ランタイム安全性とリソースのライフサイクル)**を重点的に確認します。Gate 1~8 も対象ですが、デバイスに依存しないこのレイヤーが、Layer 5 のデバイス上でのクラッシュスキャンを補完する目的で設けられた理由は Gate 9 にあります。

出力。 モジュールごとの指摘事項テーブル(Item | Question | Finding | Severity | Fix)を出力します。ⓘ Layer 7 (deep runtime-safety review): <N> concern(s) — advisory, not blocking を表示し、指摘事項を列挙します。件数を .extension-state.md の Validation history に Deep review (Layer 7): <N> concerns — <timestamp> として記録します。ユーザーが指摘事項の修正を希望する場合は、/debug-extension に引き継ぎます。


Step 8 — 最終レポートと状態の更新

次の要約テーブルを作成します。

Test summary
────────────
Layer 0 (contract check):       <pass | warn-N | skip> — <time>
Layer 1 (native structure):     <pass | fail | skip> — <time>
Layer 2 (manifest validation):  <pass | fail | n/a — no manifest> — <time>
Layer 3 (native contract):      <pass | fail | skip> — <time>
Layer 4 (PCF compile):          <pass | fail | n/a — PCF not scaffolded> — <time>
Layer 5 (manual E2E):           recipe printed (run on device when ready)
Layer 7 (deep runtime review):  <N concerns | not run — opt-in>

Overall (automated): <pass | blocked>

Note: native iOS / Android compile into a loadable DEX / framework is NOT validated by this skill — run /build-android-binary // /build-ios-binary.

このスキルの Overall に反映されるのは、自動レイヤー(1~4)のみです。 Layer 5 は手順書のみであり、その実行と結果の記録はユーザーの責任です。ユーザーはデバイスで手順を実行した後、.extension-state.md を手動で更新します。

./.extension-state.md を更新します。

  • Validation history ブロック:実行した自動レイヤー(1~4)ごとに、結果と ISO タイムスタンプを 1 行ずつ記載します。Layer 5 の行は manual / device (Layer 5): recipe printed at <timestamp> のままにし、デバイスで実行した後、ユーザーが手動で更新します。
  • Phase:実行した自動レイヤー(1~4)がすべて通過した場合は、Last completed: tested と Next: /generate-ppmplugin or /audit-ppmplugin or /publish-pcf-companion(ユーザーの次の目的に該当するもの)を設定します。それ以外の場合は、最初に失敗したレイヤーの理由を添えて Status: blocked のままにします。
8.1 次のステップのゲート

shared/shared-instructions.md §9.1 とフィードバックメモ [[feedback-skills-execute-not-describe]] に従い、実際に次のステップの選択肢がある場合は、状況に応じた選択肢を AskUserQuestion で提示してください。プレーンテキストの箇条書きや、ここから実行可能なことを無視した固定の選択肢リストは使わないでください。

失敗時(自動処理のいずれかのレイヤーが失敗):

次に取るべき合理的な手順は修正して再実行することだけなので、AskUserQuestion は使わないでください。次を出力してください。

Fix the <failing layer> error above (most relevant log line surfaced in Step 8 summary),
then re-run /test-native-extension. Re-runs are idempotent — pass-through to the failing layer.

成功時(自動処理のすべてのレイヤーを通過):

まず、リポジトリと状態ファイルを確認し、どの選択肢が適用可能かを判断してください。これは「その後の状況でどのスキルを実行できるかを判断する」ということです。ここから実行できない選択肢は提示しないでください。

検出条件意味
./manifest.json があるが、何もビルドされていない(ppmplugin/ が空)/generate-ppmplugin が標準的な次のステップです。./manifest.json を検証してステージングし、ビルド → アセンブル → 監査を実行して、検証済みのバンドルを生成します。
./manifest.json がない(手作業で作成されたモジュール)/generate-ppmplugin-manifest(または、これを工程として実行する /generate-ppmplugin)で、まずソースからマニフェストを作成します。
ビルド済みの ppmplugin/*.ppmplugin がある/audit-ppmplugin が自然な次のステップです。このスキルのソースのみを対象とするレイヤーではできない、詳細な検証ゲートです。
pcf/ フォルダーに ControlManifest.Input.xml があるPCF のひな形が作成済みです。/publish-pcf-companion を実行できます。
pcf/ がない/generate-pcf-companion が該当します。/publish-pcf-companion はまだ適用できません。
.extension-state.md に ## PCF deployments のエントリーがあるPCF が少なくとも一度デプロイされています。/publish-pcf-companion の再実行では、バージョンの更新とプッシュのみを行います。
ios/ または android/ があり、このセッションでレイヤー 7 がまだ実行されていない**ディープランタイム安全性レビュー(レイヤー 7)**を提示します。これはオプトインのリポジトリ全体を対象にした推論パス(ゲート 9)で、レイヤー 0 の grep では検出できない、複数経路にまたがるリソース/Promise/スレッド/権限のバグを検出します。助言的なものであり、デフォルトでは実行しません。

その後、最も関連性の高い 3~4 個の選択肢と、終了用の「このまま続ける」を指定して AskUserQuestion を呼び出してください。順序は、標準的な次のステップを最初にし、次に優先度順で代替案を並べ、最後に終了用の選択肢を置きます(§9.1 の複数選択肢ルールに従います)。

選択肢セットの例(実際の選択肢は、実行ごとに上記の検出条件から導き出してください):

リポジトリの状態質問の選択肢
./manifest.json があり、何もビルドされていない/generate-ppmplugin(推奨)//generate-pcf-companion//debug-extension/このまま続ける
./manifest.json がない(手作業で作成されたモジュール)/generate-ppmplugin(推奨)//generate-ppmplugin-manifest//debug-extension/このまま続ける
バンドルがあり、まだ監査されていない/audit-ppmplugin(推奨)//publish-pcf-companion//debug-extension/このまま続ける
バンドルの監査済み、PCF のデプロイ済み/publish-pcf-companion(推奨)//audit-ppmplugin(再検証)//debug-extension/このまま続ける
/debug-extension の後に再実行(増分)/generate-ppmplugin(推奨)//debug-extension/このまま続ける(ユーザーが明示的にリリースを希望するまでは公開を選択肢から外す)

各選択肢のヘッダー説明では、その選択肢が現在の状態に適している理由を明記してください。たとえば /generate-ppmplugin なら、「マニフェスト → build-android/ios → assemble → audit を実行し、検証済みの .ppmplugin をディスク上に生成します。構造の事前チェックを通過したのは、このバイナリを生成する準備が整ったためです。」のようにします。これにより、各選択肢が提示される理由をユーザーが理解できます。

質問のテンプレート:

Question: "What would you like to do next?"
Header:   "Next step"

Options:
  1. "Run /<recommended>"   description: <what it does in this state>
  2. "Run /<alternate-1>"   description: <…>
  3. "Run /<alternate-2>"   description: <…>
  4. "Stay — I'll review the test report"
     description: "Skill exits. Inspect Layers 1–5 output above; decide what to run next yourself."

レイヤー 1~4 を通過した場合は、検証済みバンドルがないときは /generate-ppmplugin、あるときは /audit-ppmplugin を、必ず選択肢の一つに含めてください。リポジトリの状態にかかわらず、構造の事前チェックを問題なく通過した後の自然な次のステップは、.ppmplugin のビルドまたは検証です。


戻り値ステータスプロトコル

最終メッセージの文字どおりの1行目は、必ず次のいずれかにしてください。

コード意味
DONE該当する自動レイヤー(1~4)がすべて合格しました。ステータスは Phase=tested に更新されました。(レイヤー5のデバイス用手順は出力済みです。ユーザーが別途実行します。)
DONE_WITH_CONCERNS: <list>自動レイヤーはすべて合格しましたが、注意事項があります。通常はレイヤー4を保留(PCFがまだスキャフォールドされていない)した場合です。その場合、テスト完了とみなす前に、エンジニアは /generate-pcf-companion を実行してください。
NEEDS_CONTEXT: <missing>必須の入力が不足しているため実行できませんでした(例:ARCHITECTURE §8 が空のため、レイヤー5の手順チェックリストを作成できない。ただし、残りの実行は完了しており、これはソフトな NEEDS_CONTEXT です)。
BLOCKED: <reason>自動レイヤーが失敗し、自動修正できませんでした。最初に失敗したレイヤーのエラーメッセージとともに、ステータスを blocked に設定しました。

1行目の後に空行を入れ、その後に人が読める要約を記載してください。


厳守事項

  • 最初の失敗時に停止してください(自動レイヤーのみ)。 レイヤー1が失敗した場合、レイヤー2を実行しないでください。そうしないと、計算リソースが無駄になり、報告内容も混乱します。
  • レイヤー5は出力のみです。 ユーザーに「合格しましたか?」と尋ねないでください。所要時間が5~10分の手動デバイステストを、スキルの完了条件にしないでください。手順を出力し、レイヤー1~4の結果を確定して終了してください。ユーザーは都合のよいときに手順を実行し、状態を手動で更新します。
  • ここではネイティブコンパイルを実行しないでください。 ロード可能な DEX/フレームワークへのネイティブコンパイルは /build-android-binary // /build-ios-binary が担当します。これらは、固定された AGP/xcodebuild の設定(ppmplugin-format §5、§5b)に基づいて、ステージングコピーのモジュールをビルドします。このスキルで単独の pod lib lint/./gradlew assembleDebug を実行すると、誤った安心感を与えます(依存関係をラップホストに固定された RN ではなく、公開 CDN/maven から解決するためです)。以前のバージョンのこのスキルが .test/ios/ または .test/android/ のスキャフォールドを生成した場合は、それらのディレクトリを削除してください。現在のフローには含まれません。
  • レイヤーを「合格」させるために --force フラグを使わないでください。 ソースを修正してください。厳格モードを無効にしたり、警告を抑制したり、依存関係の解決をスキップしたりしないでください。
  • レイヤーの実行中に .extension-state.md に書き込まないでください。 最後にすべてのレイヤーの結果をまとめて更新するか、失敗するたびに Status: blocked を含めて逐次更新してください。ただし、実行途中で停止する際に、ファイルを中途半端な状態のままにしないでください(Crashed-Claude の問題)。
  • npm publish/pnpm publish/pac pcf push を実行しないでください。 これらは公開用スキルの担当です。

コメント