generate-native-extension について
generate-native-extensionは、.ppmpluginバンドルで配布するサードパーティ製Power Appsコントロールのネイティブソースとルートのmanifest.jsonを生成するスキルです。後続のビルドやコンパニオンPCF生成で使うモジュール構成とディスパッチ契約を用意します。
主な機能
- 01iOSとAndroidのネイティブソース生成
- 02ルートmanifest.jsonの作成
- 03PRDに基づくモジュール名の導出
- 04リポジトリと開発用ファイルの作成
- 05スキャフォールド構造の確認
活用シーン
- 01ネイティブPower Appsコントロールのスキャフォールド作成
- 02ネイティブディスパッチ用マニフェストの作成
- 03バイナリパッケージ化に向けたソース準備
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ microsoft/ power-platform-skills --skill generate-native-extensionインストール後、Claude Code で/generate-native-extensionを入力して読み込まれたか確認します。
詳細
/generate-native-extension
作業ディレクトリ内の PRD.md を読み込み、shared/repo-layout.md のレイアウトに従って、サードパーティ製 PAM コントロールのネイティブソースを作成します。これは ネイティブのみ(コンパイル済み .ppmplugin)のトラックです。TypeScript の INativeExtension / handleMessageAsync レイヤーはなく、ラップホストはマニフェストの receivers 契約に従って、NativeModules.<Pascal>Module.<method> に直接ディスパッチします(shared/ppmplugin-format.md §2 を参照)。出力はほぼ完成したネイティブコードなので、エンジニアはボイラープレートを書くのではなく、OS 固有のコードをカスタマイズするところから始められます。
このスキルは、リポジトリの ネイティブモジュール部分(ios/、android/、任意の podspec、開発専用の package.json)と、コミット対象の ./manifest.json(ディスパッチ契約の信頼できる情報源)を作成します。マニフェストは、そこに記述されるネイティブコードとともにここで作成します。マニフェストの各フィールドは、このスキャフォールドが生成する名前(getName()、@ReactMethod の一覧、パッケージクラス)から導出されるためです。ここで作成することで、Companion PCF(/generate-pcf-companion)は契約を再導出するのではなく、実際の契約を読み取れるようになり、複合キー <name>/<receiver> が PCF とモジュール間で食い違うこともありません。ビルド段階の /generate-ppmplugin-manifest(/generate-ppmplugin 内)は、このマニフェストを一から作成するのではなく、検証・照合・ステージングします。Companion PCF は pac CLI と異なるツールチェーンが必要なため、/generate-pcf-companion で別途生成します。
ステップ 1 — 共有ドキュメントと PRD を読む
書き込みを行う前に:
-
スキルごとの最小限の前提条件ポリシー(
shared-instructions.md §1.5)を適用します。このトラックは自己完結型(shared-instructions §0a)で、作業ツリーと公開パッケージレジストリのみを使用します。このスキルでファイルを書き込むためのツールチェーンは不要です。必要に応じて、後で/build-android-binary//build-ios-binaryが使用する開発専用のpackage.jsonの devDeps を公開 npm レジストリから用意するため、Node と pnpm を使用できます。ステップ 4 のスモークチェックは構造上のセルフチェックであり、コンパイルは行いません。この自己完結型トラックには「baseline」チェックがないため、prereq-check.mdにある/generate-native-extensionチェックを実行します(git は必須、Node/pnpm は任意です)。続行する前に、
shared-instructions.md §9.2に従って前提条件の状態をブロック形式で表示します。━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Prereq check — /generate-native-extension ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 🟢 ✓ git installed 🟢 ✓ Node 20+ installed (optional — only to seed package.json devDeps from public npm) 🟢 ✓ pnpm installed (optional — same) 🟢 checks passed. Ready to proceed.gitがない場合は、その→ Fix:行を表示して停止します。ここでは Node/pnpm は任意です。不在の場合は、失敗として扱わずn/a (devDeps seed deferred to build skills)と記載します。 -
shared/naming-conventions.mdを読みます。派生識別子の表は正式な基準であり、ネイティブモジュールのシンボルにはModule接尾辞の規則も含まれます。この表を使って PRD の §2 からすべてのファイルパスとクラス名を導出し、独自に作成しないでください。 -
shared/ppmplugin-format.mdを読みます。§2(ランタイムのディスパッチ契約:<name>/<receiver>→NativeModules.<nativeModule>.<method>。ここで<nativeModule>=<Pascal>Module)と §4(ネイティブモジュールのシンボルが満たす必要のあるアップロード互換性チェック)を確認します。このスキルが出力するネイティブモジュールは、この契約に従って直接ディスパッチします。間に入る TS のINativeExtensionレイヤーはありません。詳しくは以下の §3.3 を参照してください。 -
shared/repo-layout.mdを読みます。出力する正確なツリー、ファイル一覧、およびpackage.jsonの形式が記載されています。 -
現在の作業ディレクトリにある
./PRD.mdを読みます。存在しないか空の場合は、BLOCKED: PRD.md not found — run /design-native-extension-feature first.と表示して停止します。 -
./.extension-state.mdが存在する場合は読みます。フェーズがscaffold-completeの場合、再生成(確認後、ファイルを上書き)、再開(不足ファイルのみを補完)、または中止のどれにするかユーザーに確認します。
このスキルで出力する必要がある構造パターン(iOS モジュールの形式、Android モジュールの形式、podspec、package.json)は、この SKILL.md(§3.1~§3.7)と shared/repo-layout.md で完全に規定されています。実行時に参照用拡張機能のリポジトリを取得しないでください。そこから得られる情報はすでにここに記載されており、取得すると PDF 固有のコードを PDF 以外の拡張機能にコピーするおそれがあります。
読み込みに失敗した場合は、停止して、見つからないファイルを報告します。
ステップ 2 — スキャフォールド計画をユーザーに確認する
PRD から導出した簡潔な概要を表示し、書き込みの前に承認を求めます。
Scaffold plan
─────────────
Repo: powerapps-<kebab>
package: <kebab>-control (dev-only, private — not published)
Class: <Pascal>
Native module: <Pascal>Module → NativeModules.<Pascal>Module (== ./manifest.json receivers[].nativeModule)
iOS class: RCT<Pascal>Module (+moduleName returns <Pascal>Module)
Android module: <Pascal>Module (com.powerapps.<lower>)
Podspec: <Pascal>Extension.podspec (optional, system-frameworks-only)
Dispatch contract: ./manifest.json (committed — written by this skill; read by the PCF + build stage)
Frameworks
iOS: <list from ARCHITECTURE §1.2>
Android: <list from ARCHITECTURE §1.3>
Operations (<count from PRD §4>): <comma-separated names>
Pattern: <one-shot | streaming | two-way>
Error codes: <count from ARCHITECTURE §5>
Target directory: <cwd> (writes <N> files; no existing files will be overwritten without confirm)
Distribution: the compiled `.ppmplugin` bundle (built later by /generate-ppmplugin). This skill is purely local — no remote, no feed, no registry.
AskUserQuestion(単一選択)を使用します。
このスキャフォールドを進めますか?
- はい — ファイルを生成する(推奨):現在のディレクトリにコントロールのソースを書き込みます。このスキルは git を実行しません。
git init、ステージング、コミットは行いません(コントロールは既存のリポジトリ内にあり、準備ができたらコミットできます)。- 先に PRD を編集する — 終了します。ユーザーは
/design-native-extension-featureを再実行して調整します。- キャンセル
ステップ 3 — ファイルを生成する
以下の順序でファイルを書き込みます。最上位のグループごとに、1 行の進捗状況(✓ wrote ios/ (3 files))を表示します。ファイルの内容は出力しません。ユーザーは IDE で差分を確認できます。
すべてのファイルパスは、現在の作業ディレクトリ(リポジトリルート)からの相対パスです。名前は shared/naming-conventions.md に従って導出します。
3.1 リポジトリの最上位ファイル
次を記述します。
-
.gitignore— 次のエントリを正確に記述します。- Node:
node_modules/、dist/、build/ .ppmpluginのビルド用ステージング — 必須:ppmplugin/(/generate-ppmpluginがマニフェスト、バイナリ、最終バンドルのステージング済みコピーを書き込む、Git で無視されるステージングディレクトリです。コミットしないでください。注:コミット対象であり、信頼できる唯一の情報源となるmanifest.jsonはリポジトリのルート(./manifest.json、後述)に置きます。ppmplugin/の下には置きません。Git の無視対象にしないでください。詳細はshared/ppmplugin-format.md §1を参照してください)- OS/エディター:
.DS_Store、.idea/、.vscode/ - Claude Code のローカル状態(ユーザーごと。共有しない):
.claude/ - 環境変数:
.env*(ただし!.env.exampleは許可) - iOS ビルド:
Pods/、*.xcworkspace、DerivedData/、*.xcodeproj/xcuserdata/ - Android ビルド:
*.iml、.gradle/、local.properties、captures/、.externalNativeBuild/、.cxx/ - PCF のビルドディレクトリのみ。
pcf/フォルダー自体は対象外です。ソースファイル(index.ts、ControlManifest.Input.xml、package.json、pcfconfig.jsonなど)は追跡対象のままにします:pcf/**/{out,Solutions,node_modules,obj,bin,generated}/ - テストハーネスの生成物:
test-harness/*.msapp - スキルが生成するバックアップ:
*.bak.*(追跡対象の内容を置き換えるスキルは、タイムスタンプ付きのバックアップを保存する場合があります。これらは意図的にローカルのみとします) - デザイン時のプレビュー:
.pcf-preview/(Canvas Studio に表示される PCF の HTML モックアップです。視覚的なレビュー用に/design-native-extension-featureの Step 8.0 で書き込まれ、デザインの各反復で再生成されます。信頼できる唯一の情報源となる成果物ではありません)
- Node:
-
package.json—shared/repo-layout.md§"package.jsonshape (dev-only)" に記載された開発専用の形式に従います。name(<kebab>-controlのようなローカル名)と、PRD に基づくdescriptionを設定します。versionは0.1.0から開始します。"private": trueを設定します。このマニフェストは公開されません。
publishConfig、フィードレジストリ、files配列、main/types、.npmrcは設定しません。目的は、ネイティブビルドがコンパイルに使用する React Native のバージョンを固定することだけです。{ "name": "<kebab>-control", "version": "0.1.0", "private": true, "description": "<from PRD §1>", "devDependencies": { "react": "18.2.0", "react-native": "0.79.7" } }react-nativeの devDep は iOS ヘッダー(/build-ios-binary)を提供し、Android ビルドが解決するreact-androidの座標(/build-android-binary)を固定します。ネイティブモジュールに必要なその他のビルド時 devDep も追加します。すべての依存関係は公開 npm レジストリから解決されます。社内フィードはありません。 -
manifest.json(リポジトリのルート、コミット対象。ディスパッチ契約の信頼できる唯一の情報源)— このスキャフォールドが生成する名前を使って、shared/ppmplugin-format.md§2(スキーマ)および §3(導出方法)に従い、ここで作成します。これは、Companion PCF(/generate-pcf-companion)とビルドステージ(/generate-ppmplugin-manifest)がどちらも読み取る唯一の成果物です。ここに、説明対象のネイティブコードの隣に作成することで、複合キー<name>/<receiver>が PCF とモジュール間で食い違わないようにします。フィールド:name= クラス名(リポジトリ名/機能名ではありません)のkebab(<Pascal>)。例:クラス名がPenInputの場合はpen-input。version=package.jsonのバージョン(0.1.0)。abi={ "compatibleShells": ">=1.0.0", "builtAgainst": "1.0.0" }(デフォルト。ビルドスキルでは変更しません)。receivers[]= 単一のエントリ{ "name": "<Pascal>Extension", "nativeModule": "<Pascal>Module", "methods": [<§3.4 / §3.5 で生成されたすべての @ReactMethod / RCT_EXPORT_METHOD 名>] }。nativeModuleは Android のgetName()および iOS の+moduleNameの戻り値と一致させる必要があります。予約名を回避するため、名前の末尾にModuleを付けます。entrypoints= このスキャフォールドが生成したすべてのプラットフォームを宣言します(コミット済みのマニフェストが契約の全体となるため、ビルドステージでは出荷対象に合わせて絞り込みます)。- Android →
"android": { "dex": "<Pascal>Plugin.dex", "packageClass": "com.powerapps.<lower>.<Pascal>Package" } - iOS →
"ios": { "framework": "<Pascal>Plugin", "moduleClass": "RCT<Pascal>Module" }これは論理的な契約であり、ビルド済みの成果物ではありません — モジュールがサポートするプラットフォームを列挙するもので、プラットフォームごとのバイナリは後からコンパイルされ、ppmplugin/staging/配下のステージングされたコピーは、実際に出荷されるものに合わせて調整されます。entrypoints.js/extension.hbc/extensionClassName/jsLayerフィールドは一切出力しないでください — これらは SDK 時代の名残であり、/audit-ppmpluginが拒否します。(ビルドステージではこのファイルに対して完全なバリデータが再実行されるため、マニフェストが不正な場合はどちらの経路でも検出されます — ただし、ここでは正しく出力してください。)
- Android →
-
README.md— このコントロール向けに調整された 1 ページのユーザー向けドキュメント。セクション:「What's in the box」(コンパイル済みの.ppmpluginバンドル + PCF コンパニオン)、「Build」(.ppmpluginを生成するには/generate-ppmpluginを実行します)、「Architecture」(Canvas フォーミュラ → PCF → wrap-bridge →NativeModules.<Pascal>Moduleの Mermaid または ASCII 図)、「Development」(pnpm installで devDeps を初期化します。ネイティブコードはビルドスキルによってコンパイルされ、ここではコンパイルしません)、「Reference docs」(shared/ppmplugin-format.mdへのリンク)。PRD の §1 Summary をそのまま使用してください。すべてのセクションは PRD から導出してください — 他のコントロールの README からサンプル値、文章、スクリーンショットを決して注入しないでください。 -
CHANGELOG.md— 単一のエントリ:# Changelog ## 0.1.0 — <ISO date> - Initial scaffold for <Human-Readable Name> native control. - Generated by pam-native-extensions plugin from PRD.md. -
LICENSE— MIT。
3.2 podspec(任意、リポジトリルートに配置)
ARCHITECTURE §1.2 が、モジュールがリンクする追加の iOS システムフレームワークを挙げている場合に限り、リポジトリルート(ios/ の中ではない)に <Pascal>Extension.podspec を記述します。.ppmplugin の iOS ビルド(/build-ios-binary)は、使い捨てのステージングされた Xcode プロジェクトからコンパイルされ、この podspec に対して npm 自動リンクを行いません — そのため、システムフレームワークのみを列挙します(React-Core / RN-CLI の自動リンク依存関係や、リモートの source は含みません)。これはローカルの pod lib lint の利便性のために存在するものであり、バンドルのビルドのためではありません。テンプレート:
require "json"
package_json = JSON.parse(File.read(File.join(__dir__, "package.json")))
Pod::Spec.new do |s|
s.name = "<Pascal>Extension"
s.version = package_json["version"]
s.summary = "<one-line description from PRD>"
s.description = <<-DESC
<2-3 sentence description from PRD — what it does, what it bridges to>
DESC
s.license = "MIT"
s.author = { "Author" => "" }
s.platform = :ios, "<min-deployment-target from ARCHITECTURE §1.2>"
s.source = { :path => "." }
s.source_files = "ios/**/*.{h,m}" # change to {h,m,swift} if Swift used
s.frameworks = <comma-quoted list of SYSTEM frameworks from ARCHITECTURE §1.2>
# No React-Core dependency: the .ppmplugin build resolves RN headers from the
# react-native devDep in package.json, not via CocoaPods autolinking.
end
3.3 TypeScript レイヤーなし — ディスパッチの契約
これはネイティブのみのトラックです。src/ の TypeScript レイヤー、src/<Pascal>Extension.ts、src/types.ts、INativeExtension/handleMessageAsync の実装、sendAsync トランスポートはありません。(これらはファーストパーティ SDK トラックに属するもので、このトラックには含まれません。)これらを生成してはいけません。ここに TS の契約レイヤーを再導入すると、SDK 時代の名残が混入し、/audit-ppmplugin で拒否されます。
代わりに、契約はマニフェストのランタイムディスパッチです(shared/ppmplugin-format.md §2)。ラップホストは複合キー <name>/<receiver> を使って、呼び出しを直接 NativeModules.<Pascal>Module.<method>(args, promise) にルーティングします。JS の仲介層はありません。つまり、次のようになります。
- ARCHITECTURE §4 のリクエストの形(
argsオブジェクト)とレスポンスの形(promise の解決時に返されるオブジェクト)は、ネイティブの@ReactMethod/RCT_EXPORT_METHODシグネチャとその JSON レスポンスによって直接実現されます。§3.4(iOS)および §3.5(Android)を参照してください。ファーストパーティ TS のhandleMessageAsyncが行うはずだった操作ごとの JSON 解析、リクエスト検証、操作の分岐、エラーコードのレスポンスは、代わりに各ネイティブメソッド内に出力されます。このディスパッチロジックこそ、このスキルが生成する価値のある部分です。 name、receivers[].method、receivers[].nativeModule(=<Pascal>Module)を宣言するmanifest.jsonは、§3.1 のとおりこのスキルがリポジトリのルートに書き込みます。出力されるネイティブモジュールのシンボルとマニフェストのreceivers[]は同時に作成されるため、食い違うことはありません。後で/generate-ppmplugin-manifestがこのファイルを新たに作成するのではなく、検証、調整、ステージングします(以下の §2/§3 とshared/ppmplugin-format.md §3を参照)。- ARCHITECTURE §5 のエラーコード一式は、ネイティブの
errorJson(code, message)ヘルパーが出力する文字列コードとして実現されます(§3.4/§3.5)。各コードには、人が読めるmessageが対応します。宣言する TS のエラーユニオン型はありません。これらのコードは、正規のカタログshared/error-codes.mdにある固定文字列です(Canvas の数式がこれらを条件分岐に使用するため、変更してはいけません)。ARCHITECTURE §5 で再利用されるコードは、カタログとまったく同じ綴りで出力してください。PCF は両方を読み取り、分岐にはerrorコードを、ErrorMessage出力への表示にはmessageを使います。
3.4 iOS(ios/)
以下を作成します。
-
ios/RCT<Pascal>Module.h—<React/RCTBridgeModule.h>をインポートし、@interface RCT<Pascal>Module : NSObject <RCTBridgeModule> @endを宣言する最小限の Objective-C ヘッダーです。 -
ios/RCT<Pascal>Module.m— 実装ファイルです。TODO のプレースホルダーではなく、完全に動作するコードを生成してください。 PRD §4 の各操作について、操作ごとの §3. ブロックで、フレームワーク、ホスティング、主要 API、エクスポートの形式、エッジケースの処理など、実装上の判断をすべて指定します。§3. のとおりに実装を生成してください。- インポート:
RCT<Pascal>Module.h、UIKit、および ARCHITECTURE §3. の「Framework / class」欄に、いずれかの操作で指定されているすべてのフレームワークを含めてください(例:§3. のいずれかで PencilKit が指定されている場合は、#import <PencilKit/PencilKit.h>)。 - モジュールの識別:ラッププラグインのフレームワークでは、
RCT_EXPORT_MODULE(...)を出力しないでください。このマクロは+loadと_RCTRegisterModuleを通じて登録しますが、これらはフレームワークのdlopenフラット名前空間からは参照できません。その代わりに、+ (NSString *)moduleName { return @"<Pascal>Module"; }というクラスメソッドを出力してください。名前にはModuleの接尾辞を付けます。Obj-C クラス名はRCT<Pascal>Moduleのままです(entrypoints.ios.moduleClassと一致)。一方、+moduleNameはマニフェストのreceivers[].nativeModuleと一致しなければならず、JS からはNativeModules.<Pascal>Moduleとして参照されます。接尾辞を削除しないでください。 - §3. でメインスレッドでの初期化が必要とされていない限り、
+ (BOOL)requiresMainQueueSetupはNOを返すようにしてください。 initの安全性 — モジュールはロード時に[cls new]で即座にインスタンス化されるため、initは例外をスローしたり、重い処理や副作用を伴う処理を実行したりしてはなりません(ppmplugin-format §5)。ハードウェアの取得、NSNotification/KVO オブザーバーの登録、AVCaptureSession/CLLocationManagerへのアクセスはinitで行わず、最初のRCT_EXPORT_METHOD呼び出しまで遅延してください。また、避けられない初期化処理は@try/@catchで囲んでください。initで捕捉されない例外が発生すると、起動時にホストがクラッシュします(Android の Looper がない状態でHandlerを使った場合のクラッシュに相当します)。- 各操作について、パラメーターを
NSDictionary *requestだけ受け取るRCT_EXPORT_METHODを記述し、その後にRCTPromiseResolveBlock resolve、RCTPromiseRejectBlock rejectを続けてください。例:RCT_EXPORT_METHOD(capturePenInput:(NSDictionary *)request resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)。これはラップのディスパッチ契約に一致します。PCF はargs: [request](要素数1の配列)を送り、位置引数として展開するため、メソッドの最初の位置引数は request 辞書になります(ppmplugin-format §2)。フィールドはrequest(request[@"…"])から読み取ってください。複数の位置引数に分割しないでください。また、ランタイムによるロード後に Obj-C クラスを引数なしの[cls new]でインスタンス化できなければなりません。引数を取るカスタム指定イニシャライザーを追加しないでください。メソッド本体では、§3. の iOS 仕様を完全に実装してください。- ホスト側のセットアップ(「専用の UIViewController を全画面でモーダル表示」→
UIViewControllerのサブクラス、またはインライン VC とpresentViewController:animated:completion:を出力)。表示する VC のviewDidLoadでは、カスタムコンテンツビューをviewではなくview.safeAreaLayoutGuideに制約してください。これにより、ノッチ/Dynamic Island/ホームインジケーターの下にコンテンツが入り込むのを防げます。modalPresentationStyle = UIModalPresentationFullScreen(または ARCHITECTURE §3. に従って.pageSheet)を設定してください。操作を明確に示すため、Done/Cancel のUIBarButtonItemを備えたUINavigationBarを追加してください。これは Android の Material ツールバーに相当するパターンです。 - §3. で指定された順序で主要な API 呼び出しを行います(例:
PKCanvasViewの初期化、PKToolPickerの接続、描画の取得)。 - §3. の指定に従って、Done/Cancel/dismiss の各ハンドラーを実装します。
- §3. の「Export」行の指定に従ってエクスポートします(例:
drawing.image(from: canvas.bounds, scale: 2.0)→ PNG → base64)。 - §3. の「Edge cases handled」リストにある各エッジケースを、指定された動作どおりに処理します(例:「ユーザーが Cancel をタップ →
USER_CANCELLEDで resolve」)。
- ホスト側のセットアップ(「専用の UIViewController を全画面でモーダル表示」→
- スレッド処理:バックグラウンド処理には
dispatch_get_global_queue、UI の表示にはdispatch_get_main_queueを使用してください。時間のかかるネイティブ処理で JS スレッドをブロックしないでください。 - エラーヘルパー:辞書
@{@"status": @"error", @"error": code, @"message": (message ?: @"")}を作成し、成功ヘルパーと同じシリアライザーであるNSJSONSerializationを使ってシリアライズする- (NSString *)errorJsonWithCode:(NSString *)code message:(NSString *)messageを出力してください。stringWithFormatは使わないでください。message(または code)に"、\、改行が含まれていると不正な JSON が出力され、PCF でのレスポンス解析時に実際の失敗ではなく誤解を招くPARSEエラーとして扱われるためです。これはメッセージを付ける目的そのものに反します。messageは、ネイティブデバッガーなしでも PCF から問題を診断できる人間向けの診断メッセージです。捕捉した例外にはerror.localizedDescriptionを、検証失敗には具体的な理由(例:@"missing required field 'uri'")を、USER_CANCELLEDには短い説明を指定してください。すべてのエラーパスで、code と message の両方を指定してこのメソッドを呼び出してください。code だけを渡してはいけません。 - 成功ヘルパー:
NSJSONSerializationを使って{"status":"ok","result":<result>}を作成する- (NSString *)successJsonWith:(NSDictionary *)resultを出力してください。 - エラーの伝播 — 操作本体を囲み、すべての失敗が code と message を付けて PCF に届くようにしてください。 フレームワーク/ランタイムの失敗はすべて、具体的な code と理由を含む
errorJsonWithCode:message:でresolveしてください。捕捉されない Obj-C 例外をスローしたり、クラッシュしたり、空の値でresolveしたりしてはいけません。危険性のある同期処理には@try/@catchを使い、@catchではINTERNAL_ERRORとexception.reasonを指定して resolve してください。 - 表示する
UIViewControllerごとのviewDidLoadに、UI の安全性を保つ定型処理を追加してください(Android のインセット処理に相当し、コンテンツがセーフエリア、ステータスバー、ホームインジケーターの下に入り込むという、iOS で最もよくある問題を防ぎます)。- (void)viewDidLoad { [super viewDidLoad]; self.view.backgroundColor = [UIColor systemBackgroundColor]; // Done / Cancel を備えたナビゲーションバー — Android の MaterialToolbar に相当します。 UINavigationBar *navBar = [[UINavigationBar alloc] init]; navBar.translatesAutoresizingMaskIntoConstraints = NO; UINavigationItem *navItem = [[UINavigationItem alloc] initWithTitle:@"<Human-readable from PRD §2>"]; navItem.leftBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemCancel target:self action:@selector(handleCancel)]; navItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemDone target:self action:@selector(handleDone)]; navBar.items = @[navItem]; [self.view addSubview:navBar]; // コンテンツビュー — 操作固有のサーフェス(例: PKCanvasView、AVCaptureVideoPreviewLayer のホスト)。 // safeAreaLayoutGuide に制約することで、コンテンツがノッチ / ホームインジケーターの下に拡張されないようにします。 UIView *contentView = [[UIView alloc] init]; // ARCHITECTURE §3.<n> に従い、操作固有のビューに置き換えてください contentView.translatesAutoresizingMaskIntoConstraints = NO; [self.view addSubview:contentView]; [NSLayoutConstraint activateConstraints:@[ [navBar.topAnchor constraintEqualToAnchor:self.view.safeAreaLayoutGuide.topAnchor], [navBar.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor], [navBar.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor], [contentView.topAnchor constraintEqualToAnchor:navBar.bottomAnchor], [contentView.leadingAnchor constraintEqualToAnchor:self.view.safeAreaLayoutGuide.leadingAnchor], [contentView.trailingAnchor constraintEqualToAnchor:self.view.safeAreaLayoutGuide.trailingAnchor], [contentView.bottomAnchor constraintEqualToAnchor:self.view.safeAreaLayoutGuide.bottomAnchor], ]]; } - (UIStatusBarStyle)preferredStatusBarStyle { // システムの外観に適応します — ライトテーマにおける Android の windowLightStatusBar に相当します。 return UIStatusBarStyleDefault; // automatic light/dark per system } - モーダルヘルパー: いずれかの操作がモーダル表示される場合は
- (UIViewController *)topViewControllerを出力します:- (UIViewController *)topViewController { UIViewController *root = UIApplication.sharedApplication.keyWindow.rootViewController; while (root.presentedViewController) { root = root.presentedViewController; } return root; }
TODO プレースホルダーは禁止です。
// implement thisも禁止です。 §3. のブロックが不完全な場合(「Key APIs and decisions」の項目のいずれかが曖昧または欠落している場合)は、NEEDS_CONTEXT: ARCHITECTURE §3.<n> implementation block is incomplete — re-run /design-native-extension-feature Step 7 (per-operation implementation walkthrough) to complete itを出力して STOP してください。曖昧な仕様を推測で覆い隠さないでください。 - インポート:
3.5 Android (android/)
以下を作成します:
-
android/build.gradle— ライブラリ専用の gradle 設定です。このモジュールは、ルートプロジェクトのセットアップを提供するホストの管理ビルドによって利用されます。(スタンドアロンの.ppmpluginビルドでは、/build-android-binaryがバージョンを固定した使い捨てのステージングコピーからコンパイルします — この正規のファイルが編集されることはありません。詳しくはshared/ppmplugin-format.md §5を参照してください。)buildscript { ... }、allprojects { ... }、またはあらゆる classpath 宣言を出力しないでください — それらはこのライブラリモジュールではなくルートプロジェクトに属します。ライブラリ専用の形式(これがファイル全体です — 前書きもルートプロジェクトのブロックもありません):
// <kebab>-control // Android library module — consumed by the host's managed build. apply plugin: 'com.android.library' apply plugin: 'kotlin-android' def safeExtGet(prop, fallback) { rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback } android { namespace "com.powerapps.<lower>" compileSdkVersion safeExtGet('compileSdkVersion', 35) defaultConfig { minSdkVersion safeExtGet('minSdkVersion', <PRD min — default 24>) targetSdkVersion 35 } compileOptions { sourceCompatibility JavaVersion.VERSION_17 targetCompatibility JavaVersion.VERSION_17 } kotlinOptions { jvmTarget = '17' } } dependencies { // 'react-android' (renamed from 'react-native' in RN 0.73). compileOnly + pinned: // the wrap shell provides RN at runtime, so never bundle it, and the legacy // 'react-native:+' coordinate does not resolve in the standalone build. // Read <rnVersion> from package.json devDependencies (currently 0.79.7). compileOnly "com.facebook.react:react-android:<rnVersion>" implementation 'androidx.appcompat:appcompat:1.6.1' implementation 'androidx.core:core-ktx:1.12.0' // WindowCompat / WindowInsetsCompat for UI hygiene implementation 'androidx.constraintlayout:constraintlayout:2.1.4' // for the generated layout XML implementation 'com.google.android.material:material:1.11.0' // Material 3 theme + components // Plus any ARCHITECTURE §1.3 / §1.4-specified additions (e.g. ML Kit, FusedLocationProvider) } -
生成してはいけないファイル(ルートプロジェクト/単体ビルドに関するものであり、ホストの管理ビルド、または
.ppmpluginの場合は/build-android-binaryのステージングコピーが管理します):android/settings.gradle— ルートプロジェクトの責任範囲ですandroid/gradle.properties— ルートプロジェクトのプロパティです。android.useAndroidXとandroid.enableJetifierはホストから暗黙的に提供されます(また、/build-android-binaryによってステージングコピーに生成されます)。ライブラリが提供するものではありませんandroid/gradlew+android/gradle/wrapper/*— Gradleラッパーです。ライブラリモジュールに専用のラッパーは必要ありませんbuild.gradle内のトップレベルのbuildscript { ext, repositories, dependencies (classpath) }ブロック — AGPとKotlinのクラスパスはホストが提供します
単体ビルド用スクリプトはありません。
android/ディレクトリはホストの管理ビルドで使用され(また、.ppmplugin用に/build-android-binaryによって固定されたステージングディレクトリにコピーされます)、単独でコンパイルできる必要はありません。トップレベルのbuildscript { ... }/allprojects { ... }ブロックは追加しないでください。これらはホストが提供します。このディレクトリに対する単体の./gradlew assembleDebugは、サポート対象の検証手順ではありません(ネイティブコンパイルはここではなく/build-android-binaryで実行します。詳細はshared/ppmplugin-format.md §5を参照してください)。 -
android/src/main/AndroidManifest.xml— ARCHITECTURE §1.4に記載された権限と、専用のキャプチャActivity(ARCHITECTURE §3.でホストする場合)をMaterial 3テーマで登録します:<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.powerapps.<lower>"> <application> <activity android:name=".<Pascal>CaptureActivity" android:theme="@style/Theme.<Pascal>" android:screenOrientation="portrait" android:exported="false" /> </application> </manifest> -
android/src/main/res/values/themes.xml— すべてのコンポーネントがAppCompatの基本設定ではなく、適切なMaterialスタイルで表示されるようにするMaterial 3テーマです。これがないと、生成されるUIでステータスバーとの重なりや、テーマが適用されないボタンなどの問題が発生します。<?xml version="1.0" encoding="utf-8"?> <resources xmlns:tools="http://schemas.android.com/tools"> <style name="Theme.<Pascal>" parent="Theme.Material3.DayNight.NoActionBar"> <item name="android:statusBarColor">@android:color/transparent</item> <item name="android:navigationBarColor">@android:color/transparent</item> <item name="android:windowLightStatusBar" tools:targetApi="m">true</item> <item name="android:windowLightNavigationBar" tools:targetApi="o_mr1">true</item> </style> </resources> -
android/src/main/res/layout/activity_<lower>_capture.xml— ルートレイアウトにはMaterialコンポーネントを使用します。上部にツールバーを配置し、MaterialCardView内にサイズが制限されたコンテンツ領域(描画サーフェス、カメラプレビューなど)を設けます:<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res/auto" android:id="@+id/root" android:layout_width="match_parent" android:layout_height="match_parent" android:background="?attr/colorSurface"> <com.google.android.material.appbar.MaterialToolbar android:id="@+id/toolbar" android:layout_width="match_parent" android:layout_height="?attr/actionBarSize" android:elevation="4dp" app:layout_constraintTop_toTopOf="parent" app:menu="@menu/<lower>_capture_menu" app:navigationIcon="@drawable/ic_close" app:title="<Human-readable name from PRD §2>" /> <com.google.android.material.card.MaterialCardView android:id="@+id/content_card" android:layout_width="0dp" android:layout_height="0dp" android:layout_margin="16dp" app:cardCornerRadius="8dp" app:cardElevation="2dp" app:layout_constraintTop_toBottomOf="@id/toolbar" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent"> <View android:id="@+id/capture_surface" android:layout_width="match_parent" android:layout_height="match_parent" android:background="?attr/colorSurfaceContainerLowest" /> </com.google.android.material.card.MaterialCardView> </androidx.constraintlayout.widget.ConstraintLayout> -
android/src/main/res/menu/<lower>_capture_menu.xml— ツールバーのアクション項目です。キャプチャフローを伴う操作では、action_doneが必須です。これがないと、ユーザーは送信する方法がありません。追加のアクション(クリア、取り消しなど)は、ARCHITECTURE §3.のUIアクションに応じて指定します:<menu xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res/auto"> <item android:id="@+id/action_done" android:title="@string/action_done" app:showAsAction="always" /> </menu> -
複数のモードがあるキャプチャ操作(ペン/消しゴム、写真/動画など)の場合: ツールバー/モード選択行では、通常の
ButtonではなくMaterialButtonToggleGroupを使用します。トグルグループにより、現在選択中のモードをユーザーが把握するために必要なアクティブ状態の視覚的フィードバックが得られます。activity_<lower>_capture.xmlに含めるレイアウト断片の例:<com.google.android.material.button.MaterialButtonToggleGroup android:id="@+id/mode_toggle_group" android:layout_width="wrap_content" android:layout_height="wrap_content" app:singleSelection="true" app:selectionRequired="true"> <Button android:id="@+id/mode_pen" android:text="@string/mode_pen" style="?attr/materialButtonOutlinedStyle" /> <Button android:id="@+id/mode_eraser" android:text="@string/mode_eraser" style="?attr/materialButtonOutlinedStyle" /> </com.google.android.material.button.MaterialButtonToggleGroup>Activityの
onCreateでリスナーを設定します:val toggleGroup: MaterialButtonToggleGroup = findViewById(R.id.mode_toggle_group) toggleGroup.check(R.id.mode_pen) // default toggleGroup.addOnButtonCheckedListener { _, checkedId, isChecked -> if (!isChecked) return@addOnButtonCheckedListener when (checkedId) { R.id.mode_pen -> captureSurface.setMode(<Pascal>Mode.PEN) R.id.mode_eraser -> captureSurface.setMode(<Pascal>Mode.ERASER) } }これがないと、ユーザーには見た目が同じボタンが並んでいるように見え、どのモードが有効なのかわかりません。v0拡張機能で、UXを妨げる障害モードとして確認されています。
-
android/src/main/res/values/strings.xml— メニュー項目とコンテンツ説明(アクセシビリティ用)の文字列リソースです:<resources> <string name="action_done">Done</string> </resources> -
android/src/main/java/com/powerapps/<lower>/<Pascal>Module.kt— Kotlin ネイティブモジュールです。TODO プレースホルダーではなく、完全に動作するコードを生成してください。 各操作について、操作ごとの §3. ブロックにある「Android implementation」サブセクションで、実装に関するすべての判断を指定しています。§3. に忠実に実装を生成してください。- クラスは
ReactContextBaseJavaModuleを継承します。 getName()は"<Pascal>Module"を返します。これはModuleサフィックス付きの名前です(JS 側のNativeModules.<Pascal>Module、iOS の+moduleNameの戻り値、マニフェストのreceivers[].nativeModuleと一致します)。サフィックスを取り除かないでください。これは予約名との衝突を避けるための仕組みです(shared/ppmplugin-format.md §4を参照)。- 各操作について、
@ReactMethod関数にはReadableMap requestパラメーターを必ず1つだけ指定し、その後にPromise promiseを続けます。例:@ReactMethod fun capturePenInput(request: ReadableMap, promise: Promise)。これはラップされたディスパッチの契約に一致します。PCF はargs: [request](要素が1つの配列)を送信し、プロキシはfn.apply(mod, [request])を実行するため、このメソッドはリクエストオブジェクトを位置引数として1つ受け取ります(ppmplugin-format §2を参照)。各フィールドはrequestから取得します(request.getString("…")、request.getInt("…")など)。request を複数の位置引数に展開しないでください。関数本体では、§3. の Android 仕様を完全に実装します。- ホスティング方法(専用の
ActivityをIntent経由で起動する方法、Fragment、またはインプレースなど、§3. の指定に従います) - §3. で指定された順序で実行する主要な API 呼び出し(例:
View.onTouchEventの登録、Pathの蓄積、スタイラスの筆圧処理) - §3. の指定に従った各 Done/Cancel ハンドラー
- §3. の「Export」行で指定されたエクスポート手順(例:
Bitmapへのレンダリング、PNG への圧縮、base64 エンコード) - §3. の「Edge cases handled」リストにある各エッジケースと、指定されたとおりの動作
- ホスティング方法(専用の
- §3. で専用の
Activityが必要とされている場合は、同じパッケージ内の別の.ktファイル(例:<Pascal>CaptureActivity.kt)として出力し、AndroidManifest.xmlに登録します。Activity のonCreateでは、生成された UI でステータスバーとの重なり、Material テーマの欠落、画面回転の問題が発生しないよう、以下の UI 衛生処理の定型コードを必ず出力してください(これらは v0 拡張機能で繰り返し発生した問題です)。
override fun onOptionsItemSelected(item: MenuItem): Boolean { return when (item.itemId) { R.id.action_done -> { onDone(); true } // §3. で宣言された追加のツールバーアクションごとに 1 ブランチを追加します。 else -> super.onOptionsItemSelected(item) } }override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // エッジツーエッジのレイアウトを設定します。システムバーのパディングは以下で適用します。 WindowCompat.setDecorFitsSystemWindows(window, false) setContentView(R.layout.activity_<lower>_capture) // ステータスバーとナビゲーションバーのインセット分だけルートにパディングを追加し、ツールバーがステータスバーの下に潜り込まないようにします。 // これは PAM 拡張機能の Android UI で最もよくある不具合、 // 「ボタンがシステム時計/ステータスアイコンと重なる」を修正する方法です。 ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.root)) { v, insets -> val bars = insets.getInsets(WindowInsetsCompat.Type.systemBars()) v.setPadding(bars.left, bars.top, bars.right, bars.bottom) WindowInsetsCompat.CONSUMED } // Material メニュー経由の Done/Cancel を備えたツールバーです。 val toolbar: MaterialToolbar = findViewById(R.id.toolbar) setSupportActionBar(toolbar) toolbar.setNavigationOnClickListener { onCancelled() } // ナビゲーションアイコン = Cancel // 操作固有のサーフェス(描画 View、カメラプレビューなど)を接続します。 // — ARCHITECTURE §3.<n> の「Hosting」および「Key APIs and decisions」の仕様に従います。 val captureSurface: <PRD-§3.<n>-View-class> = findViewById(R.id.capture_surface) // ... §3.<n> に従った操作固有の設定 ... } override fun onCreateOptionsMenu(menu: Menu): Boolean { menuInflater.inflate(R.menu.<lower>_capture_menu, menu) return true }必要なインポート: `androidx.core.view.WindowCompat`、`androidx.core.view.ViewCompat`、`androidx.core.view.WindowInsetsCompat`、`com.google.android.material.appbar.MaterialToolbar`。 - コンストラクタ /
init{}の安全性 — このモジュールはブリッジ起動時に、Looper を持たない可能性のあるスレッド上で即座に構築されるため、構築時に例外を投げてはなりません (ppmplugin-format §5)。コンストラクタは軽量に保ってください。コンストラクタ /init{}内でシステムコールバックを登録したり、カメラ / センサー / 位置情報マネージャーを取得したり、ファイル I/O を行ったりしないでください — それらは最初の@ReactMethod呼び出しまで遅延させます (遅延初期化)。リスナーをどうしても構築時に登録する必要がある場合は、明示的なHandler(Looper.getMainLooper())を渡してください — 決してnullにしないでください (nullまたは暗黙のLooperはCan't create handler inside thread that has not called Looper.prepare()をスローします)。また、try/catch でラップし、サブシステムの不調が起動時にホストをクラッシュさせるのではなく、モジュールを安全なデフォルト状態に保つようにしてください。例:private val mainHandler = Handler(Looper.getMainLooper())、その後にtry { cameraManager?.registerTorchCallback(cb, mainHandler) } catch (e: Exception) { /* best-effort */ }。同じ規律をinvalidate()にも反映してください (try/catch 内で登録解除)。 - 実行時パーミッション (危険なパーミッションのみ)。 ARCHITECTURE §1.4 で危険な Android パーミッション (
CAMERA、RECORD_AUDIO、ACCESS_FINE/COARSE_LOCATION、…) が記載されている操作では、@ReactMethodは API を呼び出す前にContextCompat.checkSelfPermission(...)をチェックし、付与されていない場合は、フレームワークにSecurityExceptionをスローさせるのではなく、promise.resolve(errorJson("PERMISSION_DENIED", "<permission> not granted"))(宣言済みの §5 コード) を返さなければなりません。AndroidManifest.xmlでの宣言は必要ですが十分ではありません — API 23 以降では実行時の付与が必要です。 currentActivityの null 許容性。 UI を表示する / Activity を起動するメソッドはすべて、val activity = currentActivity ?: return promise.resolve(errorJson("NO_ACTIVITY", "no foreground activity"))をガードしなければなりません — アプリがバックグラウンドにあるときcurrentActivityはnullになり、これを逆参照すると NPE でホストがクラッシュします。- スレッド処理: 重い処理はコルーチン (
CoroutineScope(Dispatchers.IO).launch { ... }) またはThread { ... }.start()で行い、UI 処理はHandler(Looper.getMainLooper()).post { ... }またはwithContext(Dispatchers.Main)を介して戻します。 - レスポンス JSON: iOS と完全に一致させる必要があります。両プラットフォームは同じ入力に対してバイト単位で同一の JSON を生成しなければなりません — これは譲れません。scaffold は両方の実装を書いた後、目視検査で検証しなければなりません。
- ヘルパー
private fun errorJson(code: String, message: String): StringはJSONObjectを介して{"status":"error","error":"<code>","message":"<message>"}を構築します (successJsonと同じシリアライザー —JSONObject().put("status","error").put("error",code).put("message",message).toString())。文字列補間で構築しないでください:"、\、または改行を含むmessageは無効な JSON を出力し、PCF のレスポンス解析で実際の失敗ではなく誤解を招くPARSEとして表面化してしまいます。messageは人間が読める診断情報です: 捕捉した例外にはe.message ?: e.toString()を、検証失敗には具体的な理由を、USER_CANCELLEDには短い注記を渡します。これは iOS とバイト単位で一致しなければなりません。すべてのエラーパスはコードとメッセージの両方を指定してこれを呼び出します。 - ヘルパー
private fun successJson(result: Map<String, Any?>): StringはJSONObjectを介して{"status":"ok","result":<json>}を構築します。 - エラー伝播 — すべての失敗は
errorJson(code, message)で Promise を resolve し、スローされた / 捕捉されていない例外には決してしません。 操作本体 (およびあらゆるコルーチン / Thread) を try/catch でラップし、catch 時にはpromise.resolve(errorJson("INTERNAL_ERROR", e.message ?: e.toString()))を呼び出します。捕捉せずにスローするコルーチンや、Promise を決して resolve しないパスは、メイカーにコードもメッセージもないハングしたコントロールを残します。 TODO プレースホルダーは禁止です。 §3. ブロックが不完全な場合は、iOS と同じNEEDS_CONTEXT:エラーで停止してください。曖昧な仕様から Android コードを書いてはいけません。
- クラスは
-
android/src/main/java/com/powerapps/<lower>/<Pascal>Package.kt—<Pascal>Moduleを登録するReactPackageの実装です。パブリックな引数なしコンストラクターを必ず持たなければなりません — wrap ランタイムはloadClass(packageClass).getDeclaredConstructor().newInstance()を介してこれをインスタンス化するため、引数付きコンストラクターしか持たないReactPackageはNoSuchMethodExceptionをスローし、プラグインはサイレントに読み込みに失敗します(ppmplugin-format §5)。以下の形式は正しいものです — Kotlin はclass <Pascal>Package : ReactPackageに暗黙の引数なしコンストラクターを与えます。パッケージクラスに引数を持つコンストラクターを追加しないでください(モジュール はcreateNativeModulesを介してreactContextを受け取ります — それは問題ありません。パッケージ は引数なしのままにしなければなりません):class <Pascal>Package : ReactPackage { // no-arg constructor required by the wrap runtime override fun createNativeModules(reactContext: ReactApplicationContext) = listOf<NativeModule>(<Pascal>Module(reactContext)) override fun createViewManagers(reactContext: ReactApplicationContext) = emptyList<ViewManager<*, *>>() }
3.6 テスト — このトラックでは何も生成されません
記述すべき tests/ ディレクトリはありません。ファーストパーティ SDK トラックでは、INativeExtension インターフェースに対して TypeScript の型レベルフィクスチャ(*.test-d.ts + tsc --noEmit)で適合性を検証します — しかし そのレイヤーはここには存在しません(§3.3)ので、型チェックするものは何もありません。
サードパーティ制御における相当物は、生成されたソースではなく、/test-native-extension によって行われる構造的コントラクト検証です。これはネイティブモジュールのリクエスト・レスポンス・エラーの使用状況を互いに、そしてマニフェストと突き合わせて grep します(Android の getName() ↔ iOS の +moduleName ↔ receivers[].nativeModule の一致、クロスプラットフォームの JSON パリティ)。これはワーキングツリー上で別個のスキルとして実行されます — 本スキルはそのためのテストファイルを出力しません。Jest も tsconfig もフィクスチャもありません。
3.7 状態ファイル
shared/repo-layout.md の §「.extension-state.md template」にあるテンプレートを使用して .extension-state.md を記述します。以下を埋めてください:
- Identity ブロック — PRD §2 と、そこから導出された名前(Capability、Class、Native module =
<Pascal>Module、Android パッケージ、PRD パス)から。 - Phase:
Last completed: scaffold/Next: /test-native-extension/Status: ok。 ## ppmplugin (third-party controls)ブロック — デフォルトのままにします(Target: unset、Manifest: not written、Android DEX: not built、iOS framework: not built、Bundle: not assembled)。/generate-ppmpluginビルドスキルがこれを埋めます。
.extension-state.md がすでに存在し、ステップ 1.7 で「再開」することに合意した場合は、Phase ブロックのみを更新してください(## ppmplugin はそのままにします — これは以降のビルド状態を記録するものです)。
ステップ 3.8 — プロアクティブプロトコルに対する自己批評
このステップが存在するのは、スモークチェック(ステップ 4)が構造的なものにすぎないからです — 何もコンパイルしません。生成された Kotlin と Obj-C はここではコンパイルチェックされず、後で
/build-android-binary(Gradle)と/build-ios-binary(xcodebuild)でコンパイルされます。実行時の UI と UX のバグ(ツールバーのはみ出し、行き止まりのモーダル、終了パスの欠如、非対称なプラットフォーム機能)は、どの コンパイルでも表面化せず、実機でのみ、しばしばエンジニアが午後を無駄にした後に発覚します。自己批評プロトコルは、スキルに「問題なさそう」と信じるのではなく、完了を宣言する 前に、ゲートごとに自身の出力について推論することを強制します。
プロトコルは shared/self-critique-protocol.md にあります。これは パターンマッチングではなくプロアクティブな推論です — 各ゲートはあるカテゴリのもの(PRD に記載された振る舞い、ユーザーが到達できるコントロール、相互排他的なモードなど)を列挙し、列挙された各項目について具体的な問いを強制します。このステップを実行する前にプロトコルファイルを全文お読みください。ここに続くのは統合契約です。
3.8.1 この実行で生成されたすべてのファイルを再読み込みする
ディスクから新たに読み込みます。対象ファイル:
android/src/main/java/com/powerapps/<lower>/<Pascal>Module.kt(@ReactMethodのリクエスト/レスポンス/エラーのインターフェース。TS レイヤーがないため、これがディスパッチの契約です)android/src/main/java/com/powerapps/<lower>/<Pascal>CaptureActivity.ktおよびその他の Activityandroid/src/main/res/**/*.xml(レイアウト、メニュー、テーマ、マニフェスト)ios/RCT<Pascal>Module.{h,m}(または、ARCHITECTURE §1.2 で Swift が選択されている場合は.swift)ios/*.podspecpcf/<Pascal>PCF/index.tsおよびControlManifest.Input.xml(この実行で PCF が含まれていた場合)
3.8.2 プロトコルのゲートを順番に確認する
プロトコル内の各ゲート — PRD の網羅性、ユーザージャーニー、レイアウトの実現可能性、状態の網羅性、クロスプラットフォームの同等性、可逆性、漸近的特性/ライフサイクル、仕様との乖離、ランタイムの安全性とライフサイクル、ビルド可能性とバンドルサイズの実現可能性、PCF ↔ ネイティブの往復契約 — について、プロトコルファイルの記載に従って各ゲートの「列挙+確認」手順を実行します。
このトラックでは、ゲート 10(ビルド可能性)は必須です。 /design-native-extension-feature のステップ 7.5 で設計に対して行ったのと同じ検討を、生成されたコードに対して再実行します。これにより、設計時に見落とされた(またはコード生成中に持ち込まれた)依存関係、戻り値の形状、構築コストを、数分後に /build-android-binary//build-ios-binary が失敗する前に検出できます。**ゲート 11(PCF の往復確認)**は、この実行で PCF が生成された場合にのみ実行します。PCF が別途生成される場合は、代わりに /generate-pcf-companion のステップ 5.7 で実行します。
途中で打ち切らないでください。 各ゲートでは実際の項目(PRD の箇条書き、ユーザーが操作できるコントロール、横方向コンテナーの子要素など)を列挙し、項目ごとに具体的な質問へ回答します。「直感的にコードに問題はなさそう」という理由でゲートを省略すると、目的が損なわれます。過去に直感だけではうまくいかなかった分析を、構造化して行うことが本来の目的です。
3.8.3 自己批評レポートを作成する
すべてのゲートの検出事項を集約します。各検出事項は、プロトコルの形式に従います。
- Gate: <name>
- File: <path>:<line range>
- Severity: blocker | concern
- Problem: <one sentence>
- Why this is wrong: <one sentence anchored in the gate's principle>
- Suggested fix: <what to change>
- Autofix: applied | proposed | requires human review
3.8.4 自動修正をその場で適用する
生成時のコンテキスト(このスキル)では、コード出力からステップ 4 のスモークチェックまでの間に、ユーザーが中間レビューを行う機会はありません。プロトコルの自動修正ポリシーは、次のように適用されます。
autofix: applied(機械的な修正で、設計上の判断を要しない)→ 通知せずに適用し、.extension-state.mdのAuto-fixesに記録します。autofix: proposed(構造的な修正で、判断の余地がない)→ 適用し、最終サマリーに1行の注記を添えて提示します。ユーザーは.extension-state.mdで、期待していた出力との差分を確認できます。autofix: requires human review(設計上の判断を要する)→ コードを変更しません。最終サマリーの最上位にNEEDS_CONTEXT項目として提示します。
(対照的に、インタラクティブなレビューサイクル内で実行される /debug-extension のステップ 7.5 では、proposed の修正を明示的なユーザー確認の後に適用します。)
3.8.5 修正後に再実行する
3.8.4 で修正を適用した場合は、3.8.1 から繰り返します。あるゲートでの修正によって、別の箇所の問題が解消されたり、新たな検出事項が見つかったりすることがあります。繰り返しは最大 3 回までとします。3 回目の反復後も検出事項が残っている場合は、重大度にかかわらず懸念事項として提示します。
3.8.6 戻り値のステータスへの影響
- すべてのゲートに問題がない、または修正済み → ステップ 4 に進みます。実行結果は
DONEを返せます。 - 懸念事項が残っている(ゲートの重大度ルール上、許容されるもの)→ 実行結果は
DONE_WITH_CONCERNS: <one-line summary>を返します。すべての懸念事項を.extension-state.mdに記録します。 - ブロッカーが残っている → 実行結果は
BLOCKED: self-critique found <list>を返します。レポートを提示します。ユーザーがブロッカーを解消するまで、スキャフォールドはリリースできません。
このステップで具体的に検出できること
このプロトコルのゲートは、過去の失敗一覧ではなく、第一原理から導き出されています。構造上、次の問題を検出できるはずです。
| ゲート | バグの種類 |
|---|---|
| PRD の網羅性(ゲート 1) | ARCHITECTURE §3. で約束されている動作がコードに実装されていない(例:「Done ボタンは右端に固定される」とあるのに、Done ボタン自体が存在しない)。 |
| ユーザージャーニー(ゲート 2) | 終了手段のないモーダル、反応しないコントロール、Promise が解決されないままになるエラーパス。 |
| レイアウトの実現可能性(ゲート 3) | 小さな画面でツールバーがはみ出す、コントロールが画面の端から押し出される、幅が狭い場合やフォントサイズが大きい場合に主要な操作へアクセスできない。 |
| 状態の網羅性(ゲート 4) | 有効状態が視覚的に示されないモード切り替え、返されても表示されないエラーコード、UI から確認できないブール値の切り替え。 |
| クロスプラットフォームの同等性(ゲート 5) | iOS には 5 つの機能があるのに Android には 3 つしかないうえ、その理由を説明する PRD の注記もない。 |
| 可逆性(ゲート 6) | 確認なしで実行される破壊的な操作、入る手段はあるが終了手段のないモーダル、戻る手段のない複数ステップのフロー。 |
| 漸近的挙動/ライフサイクル(ゲート 7) | PRD では維持されるはずの状態が画面の回転で失われる、主要な操作が高速なダブルタップで冪等でない。 |
| 仕様との乖離(ゲート 8) | PRD に記載されていない動作をコードが行う(ゲート 1 とは逆方向の乖離)。 |
| 実行時の安全性とライフサイクル(ゲート 9) | Looper のないコンストラクターの Handler が起動時にホストをクラッシュさせる、Promise が解決も拒否もされないパスによって呼び出し元がハングする、取得したリスナー/カメラが解放されない。 |
| ビルド可能性とバンドルへの適合性(ゲート 10) | compileSdk > 34/AGP 8.x を必要とする依存関係、フラットな形式で出荷できない iOS フレームワーク、JSON にシリアライズできない戻り値、チャネルのないストリーミングパターン。これらをツールチェーンで失敗する前に検出します。 |
| PCF ↔ ネイティブ間の往復(ゲート 11) | PCF の複合キーとマニフェストのレシーバーが一致しない、sendAsync ではなく cordova.exec を使っている、文字列化済みのエンベロープ、単独の args オブジェクト、成功パスでラップされた message コンテナーのラップを解除していない。いずれも、それ以外の場合はデバイス上で何も通知されません。 |
新しい障害モードが観測されたら、**「どのゲートで検出されるべきだったか?」**と問いかけてください。答えが「どのゲートでもない」なら、プロトコルに新しいゲートが必要(または既存のゲートの列挙リストを拡張する必要がある)という兆候です。汎用化できない、その場限りのパターンチェックを追加してはいけません。
このステップでは、生成されたソースを LLM が推論します。実際のコンパイラーではありません。このプロトコルは、項目を列挙せざるを得ない構成にすることで(「問題なさそう」が有効な回答になるのを防ぎ)、十分な網羅性を確保していますが、各ゲートでの推論が実際に慎重に行われるかどうかに依存します。プロトコルではシミュレートできない実行時エラーについては、/build-android-binary//build-ios-binary(実際のコンパイル)およびデバイス上での検証を併用してください。
ステップ 4 — スモークチェックを実行する
このトラックではコンパイルする TypeScript レイヤーがないため、スモークチェックでは tsc を実行しません。ネイティブコードは後ほど /build-android-binary と /build-ios-binary でコンパイルします。ここでは行いません。スモークチェックはすばやく行う構造的な確認です(各項目は数秒で完了します)。
OS に依存しない確認: 以下の pnpm install は、実際にクロスプラットフォームで使えるコマンドです。シンボル一致チェックは読み取り+解析です。シェルの grep ではなく、組み込みの Grep ツールで実行してください(bash の例はあくまで説明用です。Windows では grep -R を使えません)。
# 1. (Optional) seed the dev-only devDeps from the public npm registry. Skip if Node/pnpm absent.
pnpm install # resolves react + react-native (RN headers + react-android coordinate) for the build skills
# 2. Structural self-check (no compile) — use the Grep tool, not shell grep:
# confirm +moduleName returns <Pascal>Module in ios/ AND getName() == "<Pascal>Module" in android/.
grep -R '+ (NSString *)moduleName' ios/ && grep -R 'return @"<Pascal>Module"' ios/ # iOS +moduleName == <Pascal>Module
grep -R 'getName()' android/ | grep '"<Pascal>Module"' # Android getName() == <Pascal>Module
ここで実行する pnpm install は公開 npm レジストリからのみパッケージを取得し、組織固有のパッケージフィード認証は不要です。省略可能です。失敗した場合、または Node/pnpm がない場合は、その旨を記録して続行してください(ビルドスキルの実行時に devDeps が用意されます)。
実行順序の前提条件: まず .gitignore がディスク上にあることを確認してください(§3.1 のとおりです)。pnpm install は node_modules/ を作成し、.gitignore によって、後であなたがコミットする際に node_modules/ と ppmplugin/ がリポジトリに含まれないようにします。このスキルはステージングもコミットも行いませんが、.gitignore がないと node_modules/ が未追跡ファイルとして表示されます。§3.1 をスキップした場合(たとえば、フローの途中から再開した場合)は、スモークチェックの前に §3.1 からやり直してください。
ステップに失敗した場合:
pnpm installの失敗:ここでは致命的ではありません(devDeps が本当に必要なのはビルドスキルのみです)。失敗した行を表示し、devDeps seed deferredと記録して続行してください。- シンボル一致チェックの grep:不一致は、iOS の
+moduleName、Android のgetName()、および意図したマニフェストのreceivers[].nativeModuleが食い違っていることを意味します。3 つすべてが<Pascal>Moduleになるように修正して、再実行してください。これは必須のゲートです。一致していないと、ラップホストがディスパッチできません。
シンボル一致チェックが失敗した場合は、.extension-state.md でスキャフォールドを完了としてマークしないでください。失敗理由とともに Status: blocked を設定してください。
ステップ 5 — 概要
次を出力します。
Scaffold complete
─────────────────
Directory: <cwd> (<N> files written — git untouched; commit when you're ready)
Native module: <Pascal>Module (== ./manifest.json receivers[].nativeModule — both authored here, in sync)
Dispatch contract: ./manifest.json written (committed source of truth; PCF + build stage read it)
Smoke check: devDeps seed <✓ | deferred> | native symbol agreement ✓
State file: ./.extension-state.md → Phase: scaffold / Next: /test-native-extension
Next steps
──────────
1. Review the generated implementations in ios/RCT<Pascal>Module.m and android/.../<Pascal>Module.kt against ARCHITECTURE §3.<n>. Customize cosmetic details (button styling, modal chrome) if you want — but the framework wiring should be working out of the box.
2. Run /test-native-extension for the structural contract pre-flight (native module shape, Android getName ↔ iOS +moduleName ↔ ./manifest.json agreement, cross-platform JSON parity). Native iOS / Android compile is NOT validated here — it happens in /build-android-binary and /build-ios-binary.
3. Run /generate-pcf-companion to generate the Companion PCF under pcf/ — it reads ./manifest.json for the composite key so the PCF and module stay aligned.
4. When ready to ship, run /generate-ppmplugin — it validates + stages ./manifest.json, compiles the native binaries, and assembles the .ppmplugin bundle for upload to Dataverse.
ステップ 6 — 次のステップのスキルを提示する
shared/shared-instructions.md §9.1 に従い、AskUserQuestion で、実行可能なすべての次のスキルを選択肢として提示してください(「はい/いいえ」ではありません)。また、代替手段となる選択肢も含めてください。
Question: "What would you like to do next?"
Header: "Next step"
Options:
1. "Run /generate-pcf-companion"
description: "Next major scaffold step in the build flow. Runs `pac pcf init` under pcf/ and writes the Companion PCF that dispatches on the composite key <name>/<receiver> read from ./manifest.json (authored here), cross-checked against ARCHITECTURE §4 + §8 + §9."
2. "Run /test-native-extension"
description: "Structural contract pre-flight (native module shape, Android getName ↔ iOS +moduleName ↔ ./manifest.json agreement, cross-platform JSON parity). Quick sanity check before adding more code. No compile."
3. "Run /generate-ppmplugin"
description: "Build the deliverable: validates + stages ./manifest.json, compiles the native binaries (DEX / iOS framework), and assembles the .ppmplugin bundle. Usually run after the PCF exists and the contract is settled."
4. "Stay — I'll review the generated code first"
description: "Skill exits. Run git diff to inspect the scaffold and decide what to run next yourself."
選択肢 1、2、または 3 の場合: 同じターン内で Skill ツールを使って、そのスキルを実行してください。 選択肢の選択は、そのスキルの実行依頼です(説明ではなく実行してください。shared-instructions §9.1 の必須ルールです)。そこで止めて、ユーザー自身に実行するよう伝えてはいけません。呼び出されたスキルは自身のステップ 1 の前提条件チェックとゲートを実行するため、そのまま引き継いでください。
選択肢 4(ここで終了)の場合: 次の 1 行を出力します:Scaffold complete. Run any of the suggested skills when you're ready. その後、ステータス返却へ進んでください。
ステータス返却プロトコル
最終メッセージの文字どおりの 1 行目は、必ず次のいずれかにしてください。
| コード | 意味 |
|---|---|
DONE | すべてのファイルを書き込み、スモークチェック(ネイティブシンボルの一致)に合格し、状態ファイルを更新しました。次:/test-native-extension。 |
DONE_WITH_CONCERNS: <list> | ファイルは書き込み済みですが、スモークチェックで致命的ではない警告が出ました(例:devDeps の準備が保留された)、または // TODO の数が想定より多い(特に複雑な PRD 操作)、あるいは派生名の衝突をユーザーに委ねました。 |
NEEDS_CONTEXT: <missing> | 必須の PRD セクションが未完成で、そのセクションについて /design-native-extension-feature を再実行しないと進められません。 |
BLOCKED: <reason> | 前提条件に不備がある(git がない)、ネイティブモジュールシンボルの予約名/一般名との衝突を解決できない、またはネイティブシンボル一致チェックの失敗を自動修正できませんでした。 |
1 行目の後に空行を入れ、その後に人が読める概要を記載します。
このスキルの対象範囲 — 生成と監査
このスキルはPRDから拡張機能リポジトリを生成します。リンターではなく、既存の動作するコードを新しいテンプレートに照らして監査するものではありません。すでにスキャフォールドがあるディレクトリで実行すると、再生成/再開/中止の確認で上書きする前に確認します。「再生成」は破壊的な操作であり、生成済みファイルに対する手動編集は失われます。
「今生成するもの」と「既存のもの」の差分は、不具合の一覧ではありません。 ビルドでき(/build-android-binary//build-ios-binary 内)、正しいランタイム動作をする既存コードであれば、コードの形式が現在のテンプレートと異なっていても問題ありません。次のものだけを指摘してください。
- ビルドスキルでコンパイルできないコード(不正なKotlin/Obj-C)
- ディスパッチ契約に違反し、ランタイムが動作しなくなるコード(
receivers[].nativeModuleと一致しないネイティブモジュールシンボル、ホストがディスパッチできないメソッドシグネチャ、PAM が配信できないレスポンス形式) - クロスプラットフォーム間の不整合(同じ操作で、iOS と Android が異なるリクエストフィールド名やレスポンス形式を出力する)
テンプレートのヘルパーと同じ処理を行う、ローカル定義のヘルパーは不具合ではありません。もともと // TODO マーカー付きで生成され、その後エンジニアが実装を完了したメソッドは、想定どおりの状態です。「テンプレートと異なる」として指摘しないでください。
ハードルール — 正確性(制御を機能させるには、以下を満たす必要があります)
- TS の
INativeExtensionレイヤーは存在しません。 契約はマニフェストのランタイムディスパッチ(<name>/<receiver>→NativeModules.<Pascal>Module.<method>、shared/ppmplugin-format.md §2)です。src/<Pascal>Extension.ts、handleMessageAsync、またはsendAsyncトランスポートを生成してはいけません。これらは SDK 時代の名残であり、/audit-ppmpluginによって拒否されます。 - ネイティブモジュールシンボルは、すべてのランタイムサーフェスで
<Pascal>Moduleでなければなりません。 iOS の+moduleNameは@"<Pascal>Module"を返し、Android のgetName() = "<Pascal>Module"と一致し、./manifest.jsonのreceivers[].nativeModule(§3.1 でこれらのモジュールとともに作成)も一致しなければなりません。また、JS からはNativeModules.<Pascal>Moduleとして見える必要があります。Obj-C クラス名はRCT<Pascal>Moduleのままにし、entrypoints.ios.moduleClassと一致させます。一致しない場合、ラップホストのディスパッチで何も見つかりません。 - ネイティブモジュールシンボルは、バリデーターの予約済みプレフィックス/予約済み名前のルールに従わなければなりません(
shared/ppmplugin-format.md §4)。Moduleサフィックスは、単独では予約済みの名前(DeviceInfo→DeviceInfoModule)を回避するための構造上の工夫です。削除してはいけません。派生したシンボルが予約済みプレフィックスまたは既知の予約済み名前のサブセットに該当する場合は、作業を止め、ベンダープレフィックス付きのクラスをユーザーに選んでもらってください。 - 同じ入力に対する iOS と Android の JSON レスポンス形式は、バイト単位で完全に一致しなければなりません。 両方の実装は、同じ §3. のレスポンス形式から作成されます。ここで不整合があると、原因を特定しにくい Maker 側の分岐バグが発生します。
- リクエスト/レスポンスのフィールド名は、マニフェストと PCF が想定するものと一致しなければなりません。 各ネイティブメソッドが解析・出力するフィールド名は、ARCHITECTURE §4 のリクエスト/レスポンス形式(マニフェストと PCF の両方がこれを基に作成されます)と一致する必要があります。不整合があると、PCF が誤ったフィールドを送信または読み取ります。
- ネイティブシンボル一致のスモークチェック(Step 4)に合格してから、
.extension-state.mdの Phase をscaffoldに設定しなければなりません。 失敗した場合は、理由を添えてStatus: blockedに設定してください。 - API を創作してはいけません。 §3. で指定されたフレームワーク/API が、指定された最小OSバージョンに存在するか確信が持てない場合は、作業を止めて指摘してください。存在しないメソッドを参照するコードを出荷しないでください。
推奨するテンプレートのスタイル(新しいスキャフォールドでの推奨事項。既存の動作するコードは問題ありません)
これらは、新たに生成するコードの最も簡潔な形を示すものです。このスキルが新規出力を生成する際の指針です。同じ結果を別の方法で実現している既存コードは違反ではありません。監査時に、スタイルの違いを欠陥として指摘しないでください。
- iOS、Android、将来のマニフェスト全体で、
Moduleを末尾に付けたネイティブシンボル(<Pascal>Module)を一貫して使用してください。予約名のルールを満たし、3つのディスパッチサーフェスの整合性を保てます。 // TODO: implement using <framework>のプレースホルダーではなく、ARCHITECTURE §3. の完全な実装を優先してください。生成されたコードは、最初から正常系でコンパイルして実行できる必要があります。latestではなく、package.jsonの 固定されたreact-nativedevDep を優先してください。これにより、ビルドスキルが RN ヘッダーやreact-androidの座標を解決できます。再現可能なビルドにつながります。
生成時にスキルが適用する事項
コードを一から新規生成する際、スキルは上記のテンプレートに従います。厳格な要件は次のとおりです。
- 明示的な確認なしにファイルを上書きしないでください — Step 2 の再生成/再開/中止のゲートは実際に適用されます。
- ブロックされている場合に限り、
NEEDS_CONTEXTで STOP してください。 実際に処理を妨げる要因は、PRD §2 がない(クラス名がないためスキャフォールドを作成できない)、PRD §4 がない(操作がないため生成するものがない)、またはネイティブモジュールのシンボルが予約名/一般名と衝突し、ユーザーが解決しない場合です。STOP してはならない要因は、ARCHITECTURE §3. に詳細な実装がない場合(妥当なデフォルトを使用し、インラインで注記する)、ARCHITECTURE §8 に特定コードの UX がない場合(デフォルトのマッピングを使用する)、ARCHITECTURE §1.2/§1.3 にデプロイ対象の詳細がない場合(フレームワークの文書に記載された最小要件を使用する)です。 - スモークチェックは実際のゲートです。 ネイティブシンボルの一致チェック(Step 4)が失敗した場合、
.extension-state.mdの Phase はscaffoldより前のままとなり、Status: blockedになります。(任意のpnpm installは失敗しても問題ありません。devDeps の追加を後回しにしてもブロックにはなりません。) - 生成されたネイティブコードから呼び出される関数/メソッドはすべて、同じファイル内で定義するか、インポートする必要があります。 これは Step 3.8(自己レビュー)で適用されます。Kotlin の Activity に
this.createFoo()と書く場合は、同じファイルにprivate fun createFoo() { ... }も記述してください。Obj-C モジュールに[self setupBar]と書く場合は、- (void)setupBar { ... }も記述してください。スモークチェック(Step 4)では何もコンパイルされません。ネイティブコードのエラーは/build-android-binary//build-ios-binaryで表面化します。自己レビューにより、エンジニアがバイナリをビルドするときではなく、生成時に「呼び出されているが定義されていない」種類のバグを検出します。 - キャプチャ操作/モーダル表示操作には、必ず Done アクションを含めてください。 操作のパターンがワンショット、または確認して戻る UX を伴う双方向(描画キャプチャ、写真撮影、署名、手動確認付きスキャナーなど)の場合は、結果の送信を実行するツールバーの Done アクションを必ず生成してください。Cancel だけでは不十分です。ユーザーが操作を完了できません。Android では、
R.menu.<lower>_capture_menuに<item android:id="@+id/action_done">を必ず含め、onOptionsItemSelectedで処理してください。iOS では、UINavigationItemのrightBarButtonItemに Done のUIBarButtonItemを必ず設定してください。これは必須です。 - 複数モードの操作(相互に排他的な UI モード。例:キャプチャ/再生、写真/動画、編集/プレビュー)には、必ずアクティブ状態を視覚的にフィードバックしてください。 Android では、モード切り替えの行に
app:singleSelection="true"を指定したMaterialButtonToggleGroupを使用します。オンに切り替えたボタンはcolorPrimaryContainer、オフに切り替えたボタンは透明にします。iOS では、これをネイティブに処理するプラットフォームのツールピッカー(例:PencilKit、AVCaptureSessionの組み込み UI)を使用するか、カスタムの切り替えにはUISegmentedControlを使用します。アクティブ状態のフィードバックがなければ、ユーザーはどのモードが選択されているか判断できません。
生成されたコードで許容される事項(これらは欠陥ではありません):
// TODO:または// Customize:コメントは、エンジニアの判断が正当に必要となる項目に使用します。たとえば、ARCHITECTURE §3. で指定されていない重要な UI の詳細(ボタンの角の半径、アニメーション曲線、PRD で意図的に先送りされているエッジケースの処理など)です。作り上げたデフォルト値より、明確な注釈付き TODO のほうが適切です。- 指定されていない詳細には妥当なデフォルト値を使用します。 ARCHITECTURE §3. に「Done ボタンは右側」と記載されていても、ボタンの色合いが指定されていない場合は、プラットフォームのデフォルト(iOS では
.systemBlue、Android では?attr/colorAccentなど)を使用します。エンジニアが変更したくなりそうな場合は、// Customize: tint per designコメントを追加してください。 - 実行時に契約と一致しない場合は、フォールバックエラーレスポンスを返します。 ネイティブメソッドが解析できない引数を受け取った場合は、例外をスローするのではなく、
errorJson(code, message)ヘルパーを使ってエラー JSON({"status":"error","error":"INVALID_INPUT","message":"<the offending field / parse reason>"})で Promise を解決するのが適切です。必ずmessageを含めてください。INTERNAL_ERRORの包括的なキャッチ処理でも同様に、根本原因を含めます。
その他の運用ルール:
- 実行時にリファレンスリポジトリを取得しません。構造上のパターンはこの SKILL.md と
shared/*.mdに記載されています。 - PCF は生成しません(その用途には
/generate-pcf-companionを使用します)。 - マニフェスト、バイナリビルド、バンドルの組み立ては行いません(その用途には
/generate-ppmpluginとそのステージを使用します)。 src/の TS レイヤーとテストはありません(型チェックするものはありません。構造の検証には/test-native-extensionを使用します)。- リモート、フィード、レジストリへのアクセスはありません。このトラックは自己完結型で、ローカルのみで動作します。
生成されるコントロールに備えるべき実行時フォールバック
これらは正確性を高めるものであり、予期しない実行時の状況に対するコントロールの堅牢性を高めます。
- コンストラクターで例外をスローしない(起動時クラッシュの防止)。 モジュールはブリッジの起動時に、Looper がない可能性のあるスレッド上で先行して構築されます(
ppmplugin-format §5)。コンストラクターやinit{}(Android)、またはinit(iOS)でキャッチされない例外が発生すると、UI が表示される前にホスト全体が停止します。リスナーやハードウェアの登録は最初のメソッド呼び出しまで遅らせ、明示的なHandler(Looper.getMainLooper())を渡し(nullは絶対に渡さないでください)、避けられない初期化処理は try/catch で囲みます。§3.4(iOS)/§3.5(Android)を参照してください。 - 実行時の権限と
currentActivityのガード(Android):API 呼び出しの前に危険な権限を確認し、拒否された場合はPERMISSION_DENIEDで解決します。UI を表示する前にcurrentActivity != nullを確認し、null の場合はNO_ACTIVITYで解決します。SecurityExceptionや NPE によってホストをクラッシュさせてはいけません。§3.5 を参照してください。 - 引数の解析をネイティブメソッドの try/catch で囲みます → 例外をスローするのではなく、
{status: 'error', error: 'INVALID_INPUT', message: '<offending field / parse reason>'}で解決します。wrap ブリッジが想定しているのは、拒否されたり例外がスローされた Promise ではなく、解決された Promise(エラー JSON)です。 - ネイティブのエラーレスポンスには、スローされた例外ではなく
errorJson(code, message)ヘルパー(コードと人間が読める理由)を使用します。 ネイティブ側では、両方のフィールドを含むエラー JSON 文字列で Promise を解決します。wrap ブリッジはこれをそのまま PCF に渡し、messageは PCF のErrorMessage出力として表示されます。 - UI 表示時はネイティブ側でメインスレッドを確認します(iOS では
dispatch_async(dispatch_get_main_queue(), ...)、Android ではHandler(Looper.getMainLooper()).post { ... })。メインスレッド以外で UI を表示するとクラッシュします。ディスパッチは必須です。 - PCF が想定していないネイティブのエラーコード → そのままレスポンスに渡します。破棄したり、別のコードに置き換えたりしないでください。作成者が Power Fx の数式で実際のコードを参照できるようにします。
これらは実行時の安全策です。他のスタイル上の判断にかかわらず、使用が認められており、推奨されています。