[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M699QNN4QMSYSBCMB6HZ1BWZ":3,"skill-same:01M699QNN4QMSYSBCMB6HZ1BWZ":80},{"skillNo":4,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":16,"categories":17,"keywords":26,"publishedAt":49,"linkOnly":50,"markdownContent":51,"markdownBody":52,"frontMatter":53,"refName":56,"commitSha":57,"skillDirectory":58,"forksCount":59,"repositoryHtmlUrl":60,"repositoryHomepageUrl":61,"ownerHtmlUrl":62,"ownerBlogUrl":63,"ownerDescription":64,"updatedAt":65,"install":66,"resources":79},"01M699QNN4QMSYSBCMB6HZ1BWZ","generate-native-extension",null,"Read the approved PRD.md and generate the native sources for a third-party PAM control (the compiled `.ppmplugin` track) — iOS Obj-C `\u003CPascal>Module` plus optional system-frameworks podspec, Android Kotlin `\u003CPascal>Module` with build.gradle, AndroidManifest and ReactPackage, a dev-only private package.json (react + react-native devDeps for the builds), and the committed `.\u002Fmanifest.json` dispatch contract the PCF and build stage both read. No TypeScript INativeExtension layer — the contract is the manifest plus the native modules' dispatch surface. Emits the layout in shared\u002Frepo-layout.md and generates substantially complete native code (compiled later by \u002Fbuild-android-binary and \u002Fbuild-ios-binary, not here). Local only — writes files, runs no git and touches no remote or feed. PCF is generated by \u002Fgenerate-pcf-companion; the bundle is built by \u002Fgenerate-ppmplugin.","microsoft\u002Fpower-platform-skills","microsoft","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F6154722?v=4","01M698VYDXZ6JW5WH3CRV650R8","Microsoft","01M699JVYJFB75D9JM70BN4H0Q",1134,"MIT","https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002Fpower-platform-skills\u002Fblob\u002Fmain\u002Fplugins\u002Fpower-apps-mobile-extension\u002Fskills\u002Fgenerate-native-extension\u002FSKILL.md",[18,23],{"categoryNo":19,"categoryName":20,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000031","产品经理","PROFESSION","适用职业",{"categoryNo":24,"categoryName":25,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000037","律师与法务",[27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48],"generate native extension","generate","native","extension","files","OBJ","track","pascal","companion","remote","shared","local","later","typescript","react-native","modules","edit","write","read","pcf","react","android","2026-11-10 10:12:30",false,"---\nname: generate-native-extension\ndescription: \"Read the approved PRD.md and generate the native sources for a third-party PAM control (the compiled `.ppmplugin` track) — iOS Obj-C `\u003CPascal>Module` plus optional system-frameworks podspec, Android Kotlin `\u003CPascal>Module` with build.gradle, AndroidManifest and ReactPackage, a dev-only private package.json (react + react-native devDeps for the builds), and the committed `.\u002Fmanifest.json` dispatch contract the PCF and build stage both read. No TypeScript INativeExtension layer — the contract is the manifest plus the native modules' dispatch surface. Emits the layout in shared\u002Frepo-layout.md and generates substantially complete native code (compiled later by \u002Fbuild-android-binary and \u002Fbuild-ios-binary, not here). Local only — writes files, runs no git and touches no remote or feed. PCF is generated by \u002Fgenerate-pcf-companion; the bundle is built by \u002Fgenerate-ppmplugin.\"\nallowed-tools: Read, Write, Edit, Bash, Glob, Grep, AskUserQuestion, Skill\nmodel: opus\n---\n\n# \u002Fgenerate-native-extension\n\nReads `PRD.md` in the working directory and writes the native sources for a third-party PAM control following the layout in [`shared\u002Frepo-layout.md`](..\u002F..\u002Fshared\u002Frepo-layout.md). This is the **native-only** (compiled `.ppmplugin`) track — there is NO TypeScript `INativeExtension` \u002F `handleMessageAsync` layer; the wrap host dispatches straight to `NativeModules.\u003CPascal>Module.\u003Cmethod>` per the manifest's receivers contract (see [`shared\u002Fppmplugin-format.md §2`](..\u002F..\u002Fshared\u002Fppmplugin-format.md)). The output is substantially complete native code so the engineer starts at customizing OS-specific code, not writing boilerplate.\n\nThis skill writes the **native module** half of the repo (`ios\u002F`, `android\u002F`, optional podspec, dev-only package.json) **and the committed `.\u002Fmanifest.json`** — the dispatch-contract source of truth. The manifest is authored *here*, alongside the native code it describes, because every field in it is derived from the names this scaffold emits (`getName()`, the `@ReactMethod` list, the package class); authoring it now means the **Companion PCF** (`\u002Fgenerate-pcf-companion`) reads a real contract instead of re-deriving one, so the composite key `\u003Cname>\u002F\u003Creceiver>` can't drift between the PCF and the module. The build stage `\u002Fgenerate-ppmplugin-manifest` (inside `\u002Fgenerate-ppmplugin`) then **validates + reconciles + stages** this manifest rather than authoring it from scratch. The **Companion PCF** is generated separately by `\u002Fgenerate-pcf-companion` because it requires `pac` CLI and a different toolchain.\n\n---\n\n## Step 1 — Read the shared docs and the PRD\n\nBefore any write:\n\n1. Read [`shared\u002Fshared-instructions.md`](..\u002F..\u002Fshared\u002Fshared-instructions.md).\n2. Apply the **per-skill minimal prereq policy** ([`shared-instructions.md §1.5`](..\u002F..\u002Fshared\u002Fshared-instructions.md)). This track is **self-contained** ([`shared-instructions §0a`](..\u002F..\u002Fshared\u002Fshared-instructions.md)) and uses only the working tree and public package registries. This skill needs no toolchain to write the files — optionally Node + pnpm to seed the dev-only `package.json`'s devDeps from the public npm registry (used later by `\u002Fbuild-android-binary` \u002F `\u002Fbuild-ios-binary`, not here). Step 4's smoke check is a structural self-check — it does NOT compile anything. Run the **`\u002Fgenerate-native-extension` check** from [`prereq-check.md`](..\u002F..\u002Fshared\u002Fprereq-check.md) (git required; Node\u002Fpnpm optional — there is no \"baseline\" check in this self-contained track).\n\n   **Print the prereq status as a visible block per `shared-instructions.md §9.2`** before continuing:\n\n   ```\n   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    Prereq check — \u002Fgenerate-native-extension\n   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n    ? ✓ git installed\n    ? ✓ Node 20+ installed     (optional — only to seed package.json devDeps from public npm)\n    ? ✓ pnpm installed         (optional — same)\n\n    ? checks passed. Ready to proceed.\n   ```\n\n   If `git` is missing, print its `→ Fix:` line and STOP. Node\u002Fpnpm are optional here — if absent, note them as `n\u002Fa (devDeps seed deferred to build skills)` rather than failing.\n3. Read [`shared\u002Fnaming-conventions.md`](..\u002F..\u002Fshared\u002Fnaming-conventions.md) — the derived-identifier table is canonical, including the **`Module`-suffix rule** for the native module symbol. Derive all file paths and class names from §2 of the PRD using that table; do not invent.\n4. Read [`shared\u002Fppmplugin-format.md`](..\u002F..\u002Fshared\u002Fppmplugin-format.md) — §2 (the runtime dispatch contract: `\u003Cname>\u002F\u003Creceiver>` → `NativeModules.\u003CnativeModule>.\u003Cmethod>`, where `\u003CnativeModule>` = `\u003CPascal>Module`) and §4 (the upload-compatibility checks that the native module symbol must satisfy). The native modules this skill emits dispatch straight off that contract — there is NO TS `INativeExtension` layer mediating; see §3.3 below.\n5. Read [`shared\u002Frepo-layout.md`](..\u002F..\u002Fshared\u002Frepo-layout.md) — the exact tree, file list, and `package.json` shape to emit.\n6. Read `.\u002FPRD.md` from the current working directory. If missing or empty, STOP with `BLOCKED: PRD.md not found — run \u002Fdesign-native-extension-feature first.`\n7. Read `.\u002F.extension-state.md` if present. If the phase shows `scaffold-complete`, ask the user whether to **regenerate** (with confirm — overwrites files), **resume** (only fill in missing files), or **abort**.\n\nThe structural patterns this skill needs to emit (iOS module shape, Android module shape, podspec, package.json) are fully prescribed in this SKILL.md (§3.1–§3.7) and in [`shared\u002Frepo-layout.md`](..\u002F..\u002Fshared\u002Frepo-layout.md). Do NOT fetch the reference extension repo at runtime — its lessons are already encoded here, and fetching it would risk copying PDF-specific code into a non-PDF extension.\n\nIf any read fails, STOP and report which file is missing.\n\n---\n\n## Step 2 — Confirm the scaffold plan with the user\n\nPrint a concise summary derived from the PRD, then gate on approval before any write.\n\n```\nScaffold plan\n─────────────\nRepo: powerapps-\u003Ckebab>\npackage: \u003Ckebab>-control  (dev-only, private — not published)\nClass: \u003CPascal>\nNative module: \u003CPascal>Module  → NativeModules.\u003CPascal>Module  (== .\u002Fmanifest.json receivers[].nativeModule)\niOS class: RCT\u003CPascal>Module  (+moduleName returns \u003CPascal>Module)\nAndroid module: \u003CPascal>Module  (com.powerapps.\u003Clower>)\nPodspec: \u003CPascal>Extension.podspec (optional, system-frameworks-only)\nDispatch contract: .\u002Fmanifest.json (committed — written by this skill; read by the PCF + build stage)\n\nFrameworks\n  iOS:     \u003Clist from ARCHITECTURE §1.2>\n  Android: \u003Clist from ARCHITECTURE §1.3>\n\nOperations (\u003Ccount from PRD §4>): \u003Ccomma-separated names>\nPattern: \u003Cone-shot | streaming | two-way>\nError codes: \u003Ccount from ARCHITECTURE §5>\n\nTarget directory: \u003Ccwd> (writes \u003CN> files; no existing files will be overwritten without confirm)\nDistribution: the compiled `.ppmplugin` bundle (built later by \u002Fgenerate-ppmplugin). This skill is purely local — no remote, no feed, no registry.\n```\n\nUse `AskUserQuestion` (single-select):\n\n> Proceed with this scaffold?\n> - **Yes — generate the files** (recommended): write the control's sources into the current directory. This skill does **not** run git — no `git init`, no staging, no commit (the control lives in your existing repo; you commit when you're ready).\n> - **Edit the PRD first** — exit; user re-runs `\u002Fdesign-native-extension-feature` to adjust.\n> - **Cancel**\n\n---\n\n## Step 3 — Generate the files\n\nWrite files in the order below. After each top-level group, print a one-line progress update (`✓ wrote ios\u002F (3 files)`). Don't dump file contents — the user sees the diff via the IDE.\n\nEvery file path is **relative to the current working directory** (the repo root). Names are derived per [`shared\u002Fnaming-conventions.md`](..\u002F..\u002Fshared\u002Fnaming-conventions.md).\n\n### 3.1 Top-level repo files\n\nWrite:\n\n- **`.gitignore`** — emit exactly the following entries:\n  - Node: `node_modules\u002F`, `dist\u002F`, `build\u002F`\n  - **`.ppmplugin` build staging — MANDATORY**: `ppmplugin\u002F` (the gitignored staging dir where `\u002Fgenerate-ppmplugin` writes the **staged copy** of the manifest, the binaries, and the final bundle — never committed. NOTE: the **committed** source-of-truth `manifest.json` lives at the repo **root** (`.\u002Fmanifest.json`, written below), NOT under `ppmplugin\u002F` — do not gitignore it; see [`shared\u002Fppmplugin-format.md §1`](..\u002F..\u002Fshared\u002Fppmplugin-format.md))\n  - OS \u002F editor: `.DS_Store`, `.idea\u002F`, `.vscode\u002F`\n  - Claude Code local state (per-user, not shared): `.claude\u002F`\n  - Env: `.env*` (but allow `!.env.example`)\n  - iOS build: `Pods\u002F`, `*.xcworkspace`, `DerivedData\u002F`, `*.xcodeproj\u002Fxcuserdata\u002F`\n  - Android build: `*.iml`, `.gradle\u002F`, `local.properties`, `captures\u002F`, `.externalNativeBuild\u002F`, `.cxx\u002F`\n  - PCF build dirs only — NOT the `pcf\u002F` folder itself; source files (`index.ts`, `ControlManifest.Input.xml`, `package.json`, `pcfconfig.json`, etc.) stay tracked: `pcf\u002F**\u002F{out,Solutions,node_modules,obj,bin,generated}\u002F`\n  - Test-harness artifacts: `test-harness\u002F*.msapp`\n  - Skill-generated backups: `*.bak.*` (skills that replace tracked content may save a timestamped backup; those are intentionally local-only)\n  - Design-time previews: `.pcf-preview\u002F` (HTML mockup of the PCF as it appears in Canvas Studio — written by `\u002Fdesign-native-extension-feature` Step 8.0 for visual review; regenerated each design iteration; not a source-of-truth artifact)\n\n- **`package.json`** — per the dev-only shape in [`shared\u002Frepo-layout.md`](..\u002F..\u002Fshared\u002Frepo-layout.md) §\"`package.json` shape (dev-only)\". Fill in `name` (a plain local name, e.g. `\u003Ckebab>-control`) and `description` from the PRD. `version` starts at `0.1.0`. Set `\"private\": true`.\n\n  This manifest is **never published** — no `publishConfig`, no feed registry, no `files` array, no `main`\u002F`types`, no `.npmrc`. Its only job is to pin the React Native version the native builds compile against:\n  ```json\n  {\n    \"name\": \"\u003Ckebab>-control\",\n    \"version\": \"0.1.0\",\n    \"private\": true,\n    \"description\": \"\u003Cfrom PRD §1>\",\n    \"devDependencies\": {\n      \"react\": \"18.2.0\",\n      \"react-native\": \"0.79.7\"\n    }\n  }\n  ```\n  The `react-native` devDep supplies the iOS headers (`\u002Fbuild-ios-binary`) and pins the `react-android` coordinate the Android build resolves (`\u002Fbuild-android-binary`); add any other build-time devDeps the native modules need. All deps resolve from the **public npm registry** — there is no internal feed.\n\n- **`manifest.json`** (repo root, **committed** — the dispatch-contract source of truth) — author it now from the names this scaffold emits, per [`shared\u002Fppmplugin-format.md`](..\u002F..\u002Fshared\u002Fppmplugin-format.md) §2 (schema) + §3 (derivation). This is the single artifact the Companion PCF (`\u002Fgenerate-pcf-companion`) and the build stage (`\u002Fgenerate-ppmplugin-manifest`) both read; authoring it here, next to the native code it describes, is what keeps the composite key `\u003Cname>\u002F\u003Creceiver>` from drifting between the PCF and the module. Fields:\n  - `name` = `kebab(\u003CPascal>)` of the **class** name (not the repo\u002Fcapability name) — e.g. class `PenInput` → `pen-input`.\n  - `version` = the `package.json` version (`0.1.0`).\n  - `abi` = `{ \"compatibleShells\": \">=1.0.0\", \"builtAgainst\": \"1.0.0\" }` (default; the build skills don't change it).\n  - `receivers[]` = a single entry `{ \"name\": \"\u003CPascal>Extension\", \"nativeModule\": \"\u003CPascal>Module\", \"methods\": [\u003Cevery @ReactMethod \u002F RCT_EXPORT_METHOD name emitted in §3.4 \u002F §3.5>] }`. `nativeModule` MUST equal Android `getName()` and the iOS `+moduleName` return value — the **`Module`-suffixed** name (the reserved-name dodge).\n  - `entrypoints` = declare **every platform this scaffold generated** (so the committed manifest is the *full* contract; the build stage trims it to the shipped target):\n    - Android → `\"android\": { \"dex\": \"\u003CPascal>Plugin.dex\", \"packageClass\": \"com.powerapps.\u003Clower>.\u003CPascal>Package\" }`\n    - iOS → `\"ios\": { \"framework\": \"\u003CPascal>Plugin\", \"moduleClass\": \"RCT\u003CPascal>Module\" }`\n\n  This is a **logical contract**, not a built artifact — it lists the platforms the module *supports*; the per-platform binaries are compiled later and the staged copy under `ppmplugin\u002Fstaging\u002F` is reconciled down to whatever actually ships. Do NOT emit any `entrypoints.js` \u002F `extension.hbc` \u002F `extensionClassName` \u002F `jsLayer` field — those are SDK-era leakage `\u002Faudit-ppmplugin` rejects. (The build stage re-runs the full validator on this file, so a malformed manifest is caught either way — but emit it correctly here.)\n\n- **`README.md`** — one-page user-facing doc tailored to the control. Sections: \"What's in the box\" (the compiled `.ppmplugin` bundle + PCF companion), \"Build\" (run `\u002Fgenerate-ppmplugin` to produce the `.ppmplugin`), \"Architecture\" (a Mermaid-or-ASCII diagram of Canvas formula → PCF → wrap-bridge → `NativeModules.\u003CPascal>Module`), \"Development\" (`pnpm install` to seed devDeps; native code is compiled by the build skills, not here), \"Reference docs\" (link to `shared\u002Fppmplugin-format.md`). Use the PRD's §1 Summary verbatim. Drive every section from the PRD — never inject example values, prose, or screenshots from any other control's README.\n\n- **`CHANGELOG.md`** — single entry:\n  ```markdown\n  # Changelog\n\n  ## 0.1.0 — \u003CISO date>\n\n  - Initial scaffold for \u003CHuman-Readable Name> native control.\n  - Generated by pam-native-extensions plugin from PRD.md.\n  ```\n\n- **`LICENSE`** — MIT.\n\n### 3.2 The podspec (optional, at repo root)\n\nWrite **`\u003CPascal>Extension.podspec`** at the repo root (NOT inside `ios\u002F`) **only if** ARCHITECTURE §1.2 names additional iOS system frameworks the module links. The `.ppmplugin` iOS build (`\u002Fbuild-ios-binary`) compiles from a throwaway staged Xcode project and does NOT npm-autolink against this podspec — so it lists **system frameworks only** (no `React-Core` \u002F RN-CLI autolink dependency, no remote `source`). It exists for local `pod lib lint` convenience, not the bundle build. Template:\n\n```ruby\nrequire \"json\"\n\npackage_json = JSON.parse(File.read(File.join(__dir__, \"package.json\")))\n\nPod::Spec.new do |s|\n  s.name         = \"\u003CPascal>Extension\"\n  s.version      = package_json[\"version\"]\n  s.summary      = \"\u003Cone-line description from PRD>\"\n  s.description  = \u003C\u003C-DESC\n    \u003C2-3 sentence description from PRD — what it does, what it bridges to>\n  DESC\n  s.license      = \"MIT\"\n  s.author       = { \"Author\" => \"\" }\n  s.platform     = :ios, \"\u003Cmin-deployment-target from ARCHITECTURE §1.2>\"\n  s.source       = { :path => \".\" }\n  s.source_files = \"ios\u002F**\u002F*.{h,m}\"   # change to {h,m,swift} if Swift used\n  s.frameworks   = \u003Ccomma-quoted list of SYSTEM frameworks from ARCHITECTURE §1.2>\n  # No React-Core dependency: the .ppmplugin build resolves RN headers from the\n  # react-native devDep in package.json, not via CocoaPods autolinking.\nend\n```\n\n### 3.3 No TypeScript layer — the dispatch contract\n\nThis is the **native-only** track: there is **no `src\u002F` TypeScript layer**, no `src\u002F\u003CPascal>Extension.ts`, no `src\u002Ftypes.ts`, no `INativeExtension` \u002F `handleMessageAsync` implementation, and no `sendAsync` transport. (Those belong to the first-party SDK track — **NOT in this track**.) Do NOT generate any of them; reintroducing a TS contract layer here produces SDK-era leakage that `\u002Faudit-ppmplugin` rejects.\n\nThe contract instead is the **manifest's runtime dispatch** ([`shared\u002Fppmplugin-format.md §2`](..\u002F..\u002Fshared\u002Fppmplugin-format.md)): the wrap host routes a call by the composite key `\u003Cname>\u002F\u003Creceiver>` **straight to** `NativeModules.\u003CPascal>Module.\u003Cmethod>(args, promise)`. There is no JS mediator. This means:\n\n- The **request shape** (the `args` object) and **response shape** (the object the promise resolves with) from ARCHITECTURE §4 are realized **directly** in the native `@ReactMethod` \u002F `RCT_EXPORT_METHOD` signatures + their JSON responses — see §3.4 (iOS) and §3.5 (Android). The per-operation JSON parsing, request validation, operation branching, and error-code responses that a first-party TS `handleMessageAsync` would have done are emitted **inside each native method** instead. That dispatch logic is the valuable part this skill generates.\n- The `manifest.json` that declares `name`, `receivers[].method`, and `receivers[].nativeModule` (= `\u003CPascal>Module`) is written by **this skill** at the repo root (§3.1) — the native module symbols it emits and the manifest's `receivers[]` are authored together, so they can't disagree. `\u002Fgenerate-ppmplugin-manifest` later validates + reconciles + stages this file rather than re-authoring it (§2\u002F§3 below + [`shared\u002Fppmplugin-format.md §3`](..\u002F..\u002Fshared\u002Fppmplugin-format.md)).\n- The error-code set from ARCHITECTURE §5 is realized as the string codes the native `errorJson(code, message)` helpers emit (§3.4 \u002F §3.5), each paired with a human-readable `message` — there is no TS error-union type to declare. These codes are the **stable strings** from the canonical catalog [`shared\u002Ferror-codes.md`](..\u002F..\u002Fshared\u002Ferror-codes.md) (Canvas formulas branch on them, so they must not drift); emit exactly the catalog spelling for any code ARCHITECTURE §5 reuses. The PCF reads both: the `error` code to branch on, the `message` to surface as its `ErrorMessage` output.\n\n### 3.4 iOS (`ios\u002F`)\n\nWrite:\n\n- **`ios\u002FRCT\u003CPascal>Module.h`** — minimal Obj-C header importing `\u003CReact\u002FRCTBridgeModule.h>`, declaring `@interface RCT\u003CPascal>Module : NSObject \u003CRCTBridgeModule> @end`.\n\n- **`ios\u002FRCT\u003CPascal>Module.m`** — the implementation. **Generate complete working code, not TODO placeholders.** For each operation in PRD §4, the per-operation §3.\u003Cn> block prescribes every implementation decision (framework, hosting, key APIs, export shape, edge case handling). Generate the implementation verbatim from §3.\u003Cn>:\n\n  - Imports: include `RCT\u003CPascal>Module.h`, `UIKit`, plus every framework named in ARCHITECTURE §3.\u003Cn>'s \"Framework \u002F class\" field for any operation (e.g. `#import \u003CPencilKit\u002FPencilKit.h>` if any §3.\u003Cn> names PencilKit).\n  - Module identity: **do NOT emit `RCT_EXPORT_MODULE(...)`** in a wrap plugin framework. That macro registers via `+load` and `_RCTRegisterModule`, which is not visible to the framework's `dlopen` flat namespace. Instead emit a class method `+ (NSString *)moduleName { return @\"\u003CPascal>Module\"; }` — the **`Module`-suffixed** name. The Obj-C class name stays `RCT\u003CPascal>Module` (matching `entrypoints.ios.moduleClass`), while `+moduleName` MUST equal the manifest's `receivers[].nativeModule` and JS sees `NativeModules.\u003CPascal>Module`. Do NOT strip the suffix.\n  - `+ (BOOL)requiresMainQueueSetup` returning `NO` unless any §3.\u003Cn> requires main-thread init.\n  - **`init` safety — the module is instantiated eagerly at load via `[cls new]`, so `init` MUST NOT throw or do heavy\u002Fside-effecting work** ([`ppmplugin-format §5`](..\u002F..\u002Fshared\u002Fppmplugin-format.md)). Do not acquire hardware, register `NSNotification`\u002FKVO observers, or touch `AVCaptureSession`\u002F`CLLocationManager` in `init` — defer to the first `RCT_EXPORT_METHOD` call (lazy), and wrap any unavoidable init work in `@try\u002F@catch`. An uncaught exception in `init` crashes the host at launch (the iOS analogue of the Android Looper-less-`Handler` crash).\n  - For each operation, write an `RCT_EXPORT_METHOD` taking **exactly one `NSDictionary *request` parameter**, then `RCTPromiseResolveBlock resolve`, `RCTPromiseRejectBlock reject` — e.g. `RCT_EXPORT_METHOD(capturePenInput:(NSDictionary *)request resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)`. This matches the wrap dispatch contract: the PCF sends `args: [request]` (a one-element array) spread positionally, so the method's first positional param is the request dictionary ([`ppmplugin-format §2`](..\u002F..\u002Fshared\u002Fppmplugin-format.md)). Read fields off `request` (`request[@\"…\"]`); do NOT expand into multiple positional params. Also: the Obj-C class MUST instantiate via a no-arg `[cls new]` after the runtime loads it — don't add a custom designated initializer that takes arguments. The body implements §3.\u003Cn>'s iOS spec **completely**:\n    - The hosting setup (\"dedicated UIViewController presented modally, full-screen\" → emit a `UIViewController` subclass or inline VC + `presentViewController:animated:completion:`). **The presented VC's `viewDidLoad` MUST constrain custom content views to `view.safeAreaLayoutGuide`, not `view` directly** — this prevents content from intruding under the notch \u002F Dynamic Island \u002F home indicator. Set `modalPresentationStyle = UIModalPresentationFullScreen` (or `.pageSheet` per ARCHITECTURE §3.\u003Cn>). Add a `UINavigationBar` with Done \u002F Cancel `UIBarButtonItem`s for clear action affordance — same Material-toolbar-equivalent pattern as Android.\n    - The key API calls in the order §3.\u003Cn> specifies (e.g. `PKCanvasView` init, `PKToolPicker` attachment, drawing capture)\n    - Each Done\u002FCancel\u002Fdismiss handler as §3.\u003Cn> specifies\n    - The export step as §3.\u003Cn>'s \"Export\" line specifies (e.g. `drawing.image(from: canvas.bounds, scale: 2.0)` → PNG → base64)\n    - Each edge case from §3.\u003Cn>'s \"Edge cases handled\" list, with the exact behavior named (e.g. \"User taps Cancel → resolve with USER_CANCELLED\")\n  - Threading: background work on `dispatch_get_global_queue`; UI presentation on `dispatch_get_main_queue`. Long-running native work must not block the JS thread.\n  - Error helper: emit `- (NSString *)errorJsonWithCode:(NSString *)code message:(NSString *)message` that builds the dict `@{@\"status\": @\"error\", @\"error\": code, @\"message\": (message ?: @\"\")}` and serializes it via **`NSJSONSerialization`** — the SAME serializer as the success helper. Do **NOT** use `stringWithFormat`: a `message` (or code) containing a `\"`, `\\`, or newline would emit invalid JSON, which the PCF's response parse would surface as a misleading `PARSE` instead of the real failure — defeating the whole point of the message. The `message` is a **human-readable diagnostic** that makes the failure debuggable from the PCF without a native debugger: for a caught exception pass `error.localizedDescription`; for a validation failure a specific reason (e.g. `@\"missing required field 'uri'\"`); for `USER_CANCELLED` a short note. **Every error path calls this with BOTH a code and a message — never a bare code.**\n  - Success helper: emit `- (NSString *)successJsonWith:(NSDictionary *)result` that builds `{\"status\":\"ok\",\"result\":\u003Cresult>}` via `NSJSONSerialization`.\n  - **Error propagation — wrap the operation body so every failure reaches the PCF with a code AND a message.** Any framework\u002Fruntime failure must `resolve` with `errorJsonWithCode:message:` carrying a specific code and reason — never throw an uncaught Obj-C exception, crash, or `resolve` empty. Use `@try\u002F@catch` around risky synchronous work and resolve the `@catch` with `INTERNAL_ERROR` plus `exception.reason`.\n  - UI hygiene boilerplate for each presented `UIViewController`'s `viewDidLoad` (mirrors Android's insets handling — prevents the most common iOS issue: content under safe areas, status bar, home indicator):\n    ```objc\n    - (void)viewDidLoad {\n        [super viewDidLoad];\n        self.view.backgroundColor = [UIColor systemBackgroundColor];\n\n        \u002F\u002F Navigation bar with Done \u002F Cancel — equivalent to Android's MaterialToolbar.\n        UINavigationBar *navBar = [[UINavigationBar alloc] init];\n        navBar.translatesAutoresizingMaskIntoConstraints = NO;\n        UINavigationItem *navItem = [[UINavigationItem alloc] initWithTitle:@\"\u003CHuman-readable from PRD §2>\"];\n        navItem.leftBarButtonItem = [[UIBarButtonItem alloc]\n            initWithBarButtonSystemItem:UIBarButtonSystemItemCancel\n            target:self action:@selector(handleCancel)];\n        navItem.rightBarButtonItem = [[UIBarButtonItem alloc]\n            initWithBarButtonSystemItem:UIBarButtonSystemItemDone\n            target:self action:@selector(handleDone)];\n        navBar.items = @[navItem];\n        [self.view addSubview:navBar];\n\n        \u002F\u002F Content view — the operation-specific surface (e.g. PKCanvasView, AVCaptureVideoPreviewLayer host).\n        \u002F\u002F Constrain to safeAreaLayoutGuide so content doesn't extend under the notch \u002F home indicator.\n        UIView *contentView = [[UIView alloc] init];   \u002F\u002F Replace with operation-specific view per ARCHITECTURE §3.\u003Cn>\n        contentView.translatesAutoresizingMaskIntoConstraints = NO;\n        [self.view addSubview:contentView];\n\n        [NSLayoutConstraint activateConstraints:@[\n            [navBar.topAnchor constraintEqualToAnchor:self.view.safeAreaLayoutGuide.topAnchor],\n            [navBar.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor],\n            [navBar.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor],\n            [contentView.topAnchor constraintEqualToAnchor:navBar.bottomAnchor],\n            [contentView.leadingAnchor constraintEqualToAnchor:self.view.safeAreaLayoutGuide.leadingAnchor],\n            [contentView.trailingAnchor constraintEqualToAnchor:self.view.safeAreaLayoutGuide.trailingAnchor],\n            [contentView.bottomAnchor constraintEqualToAnchor:self.view.safeAreaLayoutGuide.bottomAnchor],\n        ]];\n    }\n\n    - (UIStatusBarStyle)preferredStatusBarStyle {\n        \u002F\u002F Adapt to system appearance — matches Android's windowLightStatusBar in light theme.\n        return UIStatusBarStyleDefault;   \u002F\u002F automatic light\u002Fdark per system\n    }\n    ```\n  - Modal helper: emit `- (UIViewController *)topViewController` if any operation presents modally:\n    ```objc\n    - (UIViewController *)topViewController {\n        UIViewController *root = UIApplication.sharedApplication.keyWindow.rootViewController;\n        while (root.presentedViewController) { root = root.presentedViewController; }\n        return root;\n    }\n    ```\n\n  **No TODO placeholders. No `\u002F\u002F implement this`.** If a §3.\u003Cn> block is incomplete (any \"Key APIs and decisions\" item is vague or missing), STOP with `NEEDS_CONTEXT: ARCHITECTURE §3.\u003Cn> implementation block is incomplete — re-run \u002Fdesign-native-extension-feature Step 7 (per-operation implementation walkthrough) to complete it`. Don't paper over a vague spec with a guess.\n\n### 3.5 Android (`android\u002F`)\n\nWrite:\n\n- **`android\u002Fbuild.gradle`** — **library-only** gradle config. The module is consumed by the host's managed build, which provides the root project setup. (For the standalone `.ppmplugin` build, `\u002Fbuild-android-binary` compiles from a throwaway staged copy with pinned versions — this canonical file is never edited; see [`shared\u002Fppmplugin-format.md §5`](..\u002F..\u002Fshared\u002Fppmplugin-format.md).) Do NOT emit a `buildscript { ... }`, `allprojects { ... }`, or any classpath declarations — those belong to the root project, not this library module.\n\n  Library-only shape (this is the entire file — no preamble, no root-project blocks):\n  ```gradle\n  \u002F\u002F \u003Ckebab>-control\n  \u002F\u002F Android library module — consumed by the host's managed build.\n\n  apply plugin: 'com.android.library'\n  apply plugin: 'kotlin-android'\n\n  def safeExtGet(prop, fallback) {\n      rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback\n  }\n\n  android {\n      namespace \"com.powerapps.\u003Clower>\"\n      compileSdkVersion safeExtGet('compileSdkVersion', 35)\n      defaultConfig {\n          minSdkVersion safeExtGet('minSdkVersion', \u003CPRD min — default 24>)\n          targetSdkVersion 35\n      }\n      compileOptions {\n          sourceCompatibility JavaVersion.VERSION_17\n          targetCompatibility JavaVersion.VERSION_17\n      }\n      kotlinOptions { jvmTarget = '17' }\n  }\n\n  dependencies {\n      \u002F\u002F 'react-android' (renamed from 'react-native' in RN 0.73). compileOnly + pinned:\n      \u002F\u002F the wrap shell provides RN at runtime, so never bundle it, and the legacy\n      \u002F\u002F 'react-native:+' coordinate does not resolve in the standalone build.\n      \u002F\u002F Read \u003CrnVersion> from package.json devDependencies (currently 0.79.7).\n      compileOnly \"com.facebook.react:react-android:\u003CrnVersion>\"\n      implementation 'androidx.appcompat:appcompat:1.6.1'\n      implementation 'androidx.core:core-ktx:1.12.0'                  \u002F\u002F WindowCompat \u002F WindowInsetsCompat for UI hygiene\n      implementation 'androidx.constraintlayout:constraintlayout:2.1.4' \u002F\u002F for the generated layout XML\n      implementation 'com.google.android.material:material:1.11.0'    \u002F\u002F Material 3 theme + components\n      \u002F\u002F Plus any ARCHITECTURE §1.3 \u002F §1.4-specified additions (e.g. ML Kit, FusedLocationProvider)\n  }\n  ```\n\n- **Files NOT to generate** (these are root-project \u002F standalone-build concerns; the host's managed build — or, for the `.ppmplugin`, `\u002Fbuild-android-binary`'s staged copy — owns them):\n  - `android\u002Fsettings.gradle` — root project's responsibility\n  - `android\u002Fgradle.properties` — root project's properties; `android.useAndroidX` and `android.enableJetifier` are supplied ambiently by the host (and generated into the staged copy by `\u002Fbuild-android-binary`), not by the library\n  - `android\u002Fgradlew` + `android\u002Fgradle\u002Fwrapper\u002F*` — the Gradle wrapper; library modules don't need their own wrapper\n  - Any top-level `buildscript { ext, repositories, dependencies (classpath) }` block in `build.gradle` — the host provides AGP + Kotlin classpaths\n\n  > **No standalone build script.** The `android\u002F` directory is consumed by the host's managed build (and copied into a pinned staging dir by `\u002Fbuild-android-binary` for the `.ppmplugin`); it doesn't have to compile in isolation. Don't add a top-level `buildscript { ... }` \u002F `allprojects { ... }` block — the host provides those. Standalone `.\u002Fgradlew assembleDebug` against this directory is **not** a validation path we support (native compile happens in `\u002Fbuild-android-binary`, not here — see [`shared\u002Fppmplugin-format.md §5`](..\u002F..\u002Fshared\u002Fppmplugin-format.md)).\n\n- **`android\u002Fsrc\u002Fmain\u002FAndroidManifest.xml`** — registers permissions from ARCHITECTURE §1.4 AND the dedicated capture Activity (if ARCHITECTURE §3.\u003Cn> hosts in one) with a Material 3 theme:\n  ```xml\n  \u003Cmanifest xmlns:android=\"http:\u002F\u002Fschemas.android.com\u002Fapk\u002Fres\u002Fandroid\" package=\"com.powerapps.\u003Clower>\">\n      \u003C!-- One \u003Cuses-permission android:name=\"...\" \u002F> per entry in ARCHITECTURE §1.4 Android permissions -->\n\n      \u003Capplication>\n          \u003C!-- One \u003Cactivity> per ARCHITECTURE §3.\u003Cn> that hosts in a dedicated Activity.\n               Theme references generated themes.xml; screenOrientation per ARCHITECTURE §3.\u003Cn>. -->\n          \u003Cactivity\n              android:name=\".\u003CPascal>CaptureActivity\"\n              android:theme=\"@style\u002FTheme.\u003CPascal>\"\n              android:screenOrientation=\"portrait\"\n              android:exported=\"false\" \u002F>\n      \u003C\u002Fapplication>\n  \u003C\u002Fmanifest>\n  ```\n\n- **`android\u002Fsrc\u002Fmain\u002Fres\u002Fvalues\u002Fthemes.xml`** — Material 3 theme so all components render with proper Material styling, not the bare AppCompat defaults. Without this, generated UIs hit issues like status bar overlap and unthemed buttons.\n  ```xml\n  \u003C?xml version=\"1.0\" encoding=\"utf-8\"?>\n  \u003Cresources xmlns:tools=\"http:\u002F\u002Fschemas.android.com\u002Ftools\">\n      \u003Cstyle name=\"Theme.\u003CPascal>\" parent=\"Theme.Material3.DayNight.NoActionBar\">\n          \u003C!-- System bars: drawn by the OS but content extends behind them; the Activity applies insets. -->\n          \u003Citem name=\"android:statusBarColor\">@android:color\u002Ftransparent\u003C\u002Fitem>\n          \u003Citem name=\"android:navigationBarColor\">@android:color\u002Ftransparent\u003C\u002Fitem>\n          \u003Citem name=\"android:windowLightStatusBar\" tools:targetApi=\"m\">true\u003C\u002Fitem>\n          \u003Citem name=\"android:windowLightNavigationBar\" tools:targetApi=\"o_mr1\">true\u003C\u002Fitem>\n      \u003C\u002Fstyle>\n  \u003C\u002Fresources>\n  ```\n\n- **`android\u002Fsrc\u002Fmain\u002Fres\u002Flayout\u002Factivity_\u003Clower>_capture.xml`** — root layout uses Material components. Toolbar at top, bounded content area in a `MaterialCardView` (drawing surface, camera preview, etc.):\n  ```xml\n  \u003C?xml version=\"1.0\" encoding=\"utf-8\"?>\n  \u003Candroidx.constraintlayout.widget.ConstraintLayout\n      xmlns:android=\"http:\u002F\u002Fschemas.android.com\u002Fapk\u002Fres\u002Fandroid\"\n      xmlns:app=\"http:\u002F\u002Fschemas.android.com\u002Fapk\u002Fres\u002Fauto\"\n      android:id=\"@+id\u002Froot\"\n      android:layout_width=\"match_parent\"\n      android:layout_height=\"match_parent\"\n      android:background=\"?attr\u002FcolorSurface\">\n\n      \u003Ccom.google.android.material.appbar.MaterialToolbar\n          android:id=\"@+id\u002Ftoolbar\"\n          android:layout_width=\"match_parent\"\n          android:layout_height=\"?attr\u002FactionBarSize\"\n          android:elevation=\"4dp\"\n          app:layout_constraintTop_toTopOf=\"parent\"\n          app:menu=\"@menu\u002F\u003Clower>_capture_menu\"\n          app:navigationIcon=\"@drawable\u002Fic_close\"\n          app:title=\"\u003CHuman-readable name from PRD §2>\" \u002F>\n\n      \u003Ccom.google.android.material.card.MaterialCardView\n          android:id=\"@+id\u002Fcontent_card\"\n          android:layout_width=\"0dp\"\n          android:layout_height=\"0dp\"\n          android:layout_margin=\"16dp\"\n          app:cardCornerRadius=\"8dp\"\n          app:cardElevation=\"2dp\"\n          app:layout_constraintTop_toBottomOf=\"@id\u002Ftoolbar\"\n          app:layout_constraintBottom_toBottomOf=\"parent\"\n          app:layout_constraintStart_toStartOf=\"parent\"\n          app:layout_constraintEnd_toEndOf=\"parent\">\n\n          \u003C!-- The operation-specific surface goes here: drawing View, camera SurfaceView,\n               photo preview, etc. — substituted per ARCHITECTURE §3.\u003Cn>'s \"Hosting\" specification. -->\n          \u003CView\n              android:id=\"@+id\u002Fcapture_surface\"\n              android:layout_width=\"match_parent\"\n              android:layout_height=\"match_parent\"\n              android:background=\"?attr\u002FcolorSurfaceContainerLowest\" \u002F>\n      \u003C\u002Fcom.google.android.material.card.MaterialCardView>\n  \u003C\u002Fandroidx.constraintlayout.widget.ConstraintLayout>\n  ```\n\n- **`android\u002Fsrc\u002Fmain\u002Fres\u002Fmenu\u002F\u003Clower>_capture_menu.xml`** — toolbar action items. **`action_done` is MANDATORY** for any capture-flow operation; without it the user has no way to submit. Additional actions (Clear, Undo, etc.) per ARCHITECTURE §3.\u003Cn>'s UI actions:\n  ```xml\n  \u003Cmenu xmlns:android=\"http:\u002F\u002Fschemas.android.com\u002Fapk\u002Fres\u002Fandroid\"\n        xmlns:app=\"http:\u002F\u002Fschemas.android.com\u002Fapk\u002Fres\u002Fauto\">\n      \u003C!-- MANDATORY for capture flows. NEVER omit Done — user cannot complete the operation otherwise. -->\n      \u003Citem\n          android:id=\"@+id\u002Faction_done\"\n          android:title=\"@string\u002Faction_done\"\n          app:showAsAction=\"always\" \u002F>\n\n      \u003C!-- Optional: one \u003Citem> per additional toolbar action declared in ARCHITECTURE §3.\u003Cn>\n           (e.g. Clear All, Undo). Set app:showAsAction=\"ifRoom\" for non-critical actions. -->\n  \u003C\u002Fmenu>\n  ```\n\n- **For multi-mode capture operations (pen\u002Feraser, photo\u002Fvideo, etc.):** the toolbar \u002F mode-selection row uses `MaterialButtonToggleGroup`, not plain `Button`s. Toggle group provides the active-state visual feedback the user needs to know which mode is currently selected. Example layout fragment to include in `activity_\u003Clower>_capture.xml`:\n  ```xml\n  \u003C!-- Insert into the toolbar or just below it, when ARCHITECTURE §3.\u003Cn> has multiple modes. -->\n  \u003Ccom.google.android.material.button.MaterialButtonToggleGroup\n      android:id=\"@+id\u002Fmode_toggle_group\"\n      android:layout_width=\"wrap_content\"\n      android:layout_height=\"wrap_content\"\n      app:singleSelection=\"true\"\n      app:selectionRequired=\"true\">\n\n      \u003C!-- One \u003CButton style=\"?attr\u002FmaterialButtonOutlinedStyle\"> per mode in ARCHITECTURE §3.\u003Cn>.\n           Example for pen\u002Feraser\u002Fclear: -->\n      \u003CButton android:id=\"@+id\u002Fmode_pen\"     android:text=\"@string\u002Fmode_pen\"     style=\"?attr\u002FmaterialButtonOutlinedStyle\" \u002F>\n      \u003CButton android:id=\"@+id\u002Fmode_eraser\"  android:text=\"@string\u002Fmode_eraser\"  style=\"?attr\u002FmaterialButtonOutlinedStyle\" \u002F>\n  \u003C\u002Fcom.google.android.material.button.MaterialButtonToggleGroup>\n  ```\n  And wire the listener in the Activity's `onCreate`:\n  ```kotlin\n  val toggleGroup: MaterialButtonToggleGroup = findViewById(R.id.mode_toggle_group)\n  toggleGroup.check(R.id.mode_pen)   \u002F\u002F default\n  toggleGroup.addOnButtonCheckedListener { _, checkedId, isChecked ->\n      if (!isChecked) return@addOnButtonCheckedListener\n      when (checkedId) {\n          R.id.mode_pen -> captureSurface.setMode(\u003CPascal>Mode.PEN)\n          R.id.mode_eraser -> captureSurface.setMode(\u003CPascal>Mode.ERASER)\n      }\n  }\n  ```\n  Without this, the user sees a row of identical-looking buttons and has no idea which mode is active. Confirmed UX-blocking failure mode in v0 extensions.\n\n- **`android\u002Fsrc\u002Fmain\u002Fres\u002Fvalues\u002Fstrings.xml`** — string resources for the menu items + content descriptions (accessibility):\n  ```xml\n  \u003Cresources>\n      \u003Cstring name=\"action_done\">Done\u003C\u002Fstring>\n      \u003C!-- Plus one entry per ARCHITECTURE §3.\u003Cn> action; one content-description per accessible element. -->\n  \u003C\u002Fresources>\n  ```\n\n- **`android\u002Fsrc\u002Fmain\u002Fjava\u002Fcom\u002Fpowerapps\u002F\u003Clower>\u002F\u003CPascal>Module.kt`** — Kotlin native module. **Generate complete working code, not TODO placeholders.** For each operation, the per-operation §3.\u003Cn> block's \"Android implementation\" sub-section prescribes every implementation decision. Generate the implementation verbatim from §3.\u003Cn>:\n  - Class: extends `ReactContextBaseJavaModule`.\n  - `getName()` returns `\"\u003CPascal>Module\"` — the **`Module`-suffixed** name (matches `NativeModules.\u003CPascal>Module` on JS side, the iOS `+moduleName` return value, and the manifest's `receivers[].nativeModule`). Do NOT strip the suffix — it's the reserved-name dodge (see [`shared\u002Fppmplugin-format.md §4`](..\u002F..\u002Fshared\u002Fppmplugin-format.md)).\n  - For each operation, write a `@ReactMethod` function taking **exactly one `ReadableMap request` parameter** followed by `Promise promise` — e.g. `@ReactMethod fun capturePenInput(request: ReadableMap, promise: Promise)`. This matches the wrap dispatch contract: the PCF sends `args: [request]` (a one-element array) and the proxy does `fn.apply(mod, [request])`, so the method receives the request object as its single positional param ([`ppmplugin-format §2`](..\u002F..\u002Fshared\u002Fppmplugin-format.md)). Read each field off `request` (`request.getString(\"…\")`, `request.getInt(\"…\")`, etc.); do NOT expand the request into multiple positional params. The body implements §3.\u003Cn>'s Android spec **completely**:\n    - The hosting (dedicated `Activity` via `Intent`, or `Fragment`, or in-place — whatever §3.\u003Cn> specifies)\n    - The key API calls in the order §3.\u003Cn> specifies (e.g. `View.onTouchEvent` registration; `Path` accumulation; stylus pressure handling)\n    - Each Done\u002FCancel handler as §3.\u003Cn> specifies\n    - The export step as §3.\u003Cn>'s \"Export\" line specifies (e.g. render to `Bitmap`, compress to PNG, base64-encode)\n    - Each edge case from §3.\u003Cn>'s \"Edge cases handled\" list, with the exact behavior named\n  - If §3.\u003Cn> requires a dedicated `Activity`, emit it as a separate `.kt` file under the same package (e.g. `\u003CPascal>CaptureActivity.kt`) and register it in `AndroidManifest.xml`. The Activity's `onCreate` MUST emit the following UI hygiene boilerplate so the generated UI doesn't suffer from status bar overlap, missing Material theming, or rotation issues (these were repeat issues in v0 extensions):\n    ```kotlin\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        \u002F\u002F Edge-to-edge layout; we apply system-bar padding ourselves below.\n        WindowCompat.setDecorFitsSystemWindows(window, false)\n        setContentView(R.layout.activity_\u003Clower>_capture)\n\n        \u002F\u002F Pad root by status\u002Fnav bar insets so toolbar doesn't sit UNDER the status bar.\n        \u002F\u002F This is the fix for the most common Android UI bug in PAM extensions:\n        \u002F\u002F \"buttons overlapping with system clock \u002F status icons\".\n        ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.root)) { v, insets ->\n            val bars = insets.getInsets(WindowInsetsCompat.Type.systemBars())\n            v.setPadding(bars.left, bars.top, bars.right, bars.bottom)\n            WindowInsetsCompat.CONSUMED\n        }\n\n        \u002F\u002F Toolbar with Done\u002FCancel via Material menu.\n        val toolbar: MaterialToolbar = findViewById(R.id.toolbar)\n        setSupportActionBar(toolbar)\n        toolbar.setNavigationOnClickListener { onCancelled() }   \u002F\u002F navigation icon = Cancel\n\n        \u002F\u002F Wire up the operation-specific surface (drawing View, camera preview, etc.)\n        \u002F\u002F — per ARCHITECTURE §3.\u003Cn>'s \"Hosting\" + \"Key APIs and decisions\" specification.\n        val captureSurface: \u003CPRD-§3.\u003Cn>-View-class> = findViewById(R.id.capture_surface)\n        \u002F\u002F ... operation-specific setup per §3.\u003Cn> ...\n    }\n\n    override fun onCreateOptionsMenu(menu: Menu): Boolean {\n        menuInflater.inflate(R.menu.\u003Clower>_capture_menu, menu)\n        return true\n    }\n\n    override fun onOptionsItemSelected(item: MenuItem): Boolean {\n        return when (item.itemId) {\n            R.id.action_done -> { onDone(); true }\n            \u002F\u002F Plus one branch per additional toolbar action declared in §3.\u003Cn>.\n            else -> super.onOptionsItemSelected(item)\n        }\n    }\n    ```\n    Required imports: `androidx.core.view.WindowCompat`, `androidx.core.view.ViewCompat`, `androidx.core.view.WindowInsetsCompat`, `com.google.android.material.appbar.MaterialToolbar`.\n  - **Constructor \u002F `init{}` safety — the module is built eagerly at bridge startup on a possibly Looper-less thread, so construction MUST NOT throw** ([`ppmplugin-format §5`](..\u002F..\u002Fshared\u002Fppmplugin-format.md)). Keep the constructor cheap: do NOT register system callbacks, acquire camera\u002Fsensor\u002Flocation managers, or do file I\u002FO in the constructor \u002F `init{}` — **defer them to the first `@ReactMethod` call (lazy init)**. If a listener genuinely must be registered at construction, pass an **explicit `Handler(Looper.getMainLooper())` — never `null`** (a `null` or implicit `Looper` throws `Can't create handler inside thread that has not called Looper.prepare()`), and wrap it in try\u002Fcatch so a subsystem hiccup leaves the module in a safe default state instead of crashing the host at launch. Example: `private val mainHandler = Handler(Looper.getMainLooper())`, then `try { cameraManager?.registerTorchCallback(cb, mainHandler) } catch (e: Exception) { \u002F* best-effort *\u002F }`. Mirror the same discipline in `invalidate()` (unregister in try\u002Fcatch).\n  - **Runtime permissions (dangerous perms only).** For any op whose ARCHITECTURE §1.4 lists a *dangerous* Android permission (`CAMERA`, `RECORD_AUDIO`, `ACCESS_FINE\u002FCOARSE_LOCATION`, …), the `@ReactMethod` MUST check `ContextCompat.checkSelfPermission(...)` before calling the API and, if not granted, `promise.resolve(errorJson(\"PERMISSION_DENIED\", \"\u003Cpermission> not granted\"))` (a declared §5 code) rather than let the framework throw `SecurityException`. Declaring it in `AndroidManifest.xml` is necessary but **not** sufficient — API 23+ requires the runtime grant.\n  - **`currentActivity` nullability.** Any method that presents UI \u002F starts an Activity MUST guard `val activity = currentActivity ?: return promise.resolve(errorJson(\"NO_ACTIVITY\", \"no foreground activity\"))` — `currentActivity` is `null` when the app is backgrounded, and dereferencing it NPE-crashes the host.\n  - Threading: heavy work in a coroutine (`CoroutineScope(Dispatchers.IO).launch { ... }`) or `Thread { ... }.start()`; UI work back via `Handler(Looper.getMainLooper()).post { ... }` or `withContext(Dispatchers.Main)`.\n  - Response JSON: must mirror iOS exactly. Both platforms must produce byte-identical JSON for the same input — this is non-negotiable, scaffold MUST verify by visual inspection of the two implementations after writing them.\n  - Helper `private fun errorJson(code: String, message: String): String` building `{\"status\":\"error\",\"error\":\"\u003Ccode>\",\"message\":\"\u003Cmessage>\"}` via **`JSONObject`** (the same serializer as `successJson` — `JSONObject().put(\"status\",\"error\").put(\"error\",code).put(\"message\",message).toString()`). Do **NOT** build it with string interpolation: a `message` containing a `\"`, `\\`, or newline would emit invalid JSON, which the PCF's response parse would surface as a misleading `PARSE` instead of the real failure. The `message` is a **human-readable diagnostic**: for a caught exception pass `e.message ?: e.toString()`; for a validation failure a specific reason; for `USER_CANCELLED` a short note. This MUST mirror iOS byte-for-byte. **Every error path calls this with BOTH a code and a message.**\n  - Helper `private fun successJson(result: Map\u003CString, Any?>): String` building `{\"status\":\"ok\",\"result\":\u003Cjson>}` via `JSONObject`.\n  - **Error propagation — every failure resolves the Promise with `errorJson(code, message)`, never a thrown\u002Funcaught exception.** Wrap the operation body (and any coroutine\u002FThread) in try\u002Fcatch; on catch, `promise.resolve(errorJson(\"INTERNAL_ERROR\", e.message ?: e.toString()))`. A coroutine that throws without catching, or a path that never resolves the Promise, leaves the maker with a hung control and no code\u002Fmessage.\n\n  **No TODO placeholders.** If a §3.\u003Cn> block is incomplete, STOP with the same `NEEDS_CONTEXT:` error as iOS — don't write Android code from a vague spec.\n\n- **`android\u002Fsrc\u002Fmain\u002Fjava\u002Fcom\u002Fpowerapps\u002F\u003Clower>\u002F\u003CPascal>Package.kt`** — `ReactPackage` implementation that registers `\u003CPascal>Module`. **It MUST have a public no-arg constructor** — the wrap runtime instantiates it via `loadClass(packageClass).getDeclaredConstructor().newInstance()`, so a `ReactPackage` with only an arg-ed constructor throws `NoSuchMethodException` and the plugin silently fails to load ([`ppmplugin-format §5`](..\u002F..\u002Fshared\u002Fppmplugin-format.md)). The form below is correct — Kotlin gives `class \u003CPascal>Package : ReactPackage` an implicit no-arg constructor. **Do NOT add a constructor with parameters to the package class** (the *module* takes `reactContext` via `createNativeModules` — that's fine; the *package* must stay no-arg):\n  ```kotlin\n  class \u003CPascal>Package : ReactPackage {   \u002F\u002F no-arg constructor required by the wrap runtime\n      override fun createNativeModules(reactContext: ReactApplicationContext) =\n          listOf\u003CNativeModule>(\u003CPascal>Module(reactContext))\n      override fun createViewManagers(reactContext: ReactApplicationContext) =\n          emptyList\u003CViewManager\u003C*, *>>()\n  }\n  ```\n\n### 3.6 Tests — none generated in this track\n\nThere is **no `tests\u002F` directory** to write. The first-party SDK track validates conformance with TypeScript type-level fixtures (`*.test-d.ts` + `tsc --noEmit`) against the `INativeExtension` interface — but **that layer does not exist here** (§3.3), so there is nothing to type-check.\n\nThe third-party-control analogue is **structural contract verification done by [`\u002Ftest-native-extension`](..\u002Ftest-native-extension\u002FSKILL.md)**, not generated source: it greps the native modules' request\u002Fresponse\u002Ferror usage against each other and against the manifest (the Android `getName()` ↔ iOS `+moduleName` ↔ `receivers[].nativeModule` agreement, the cross-platform JSON parity). That runs as a separate skill on the working tree — this skill does not emit test files for it. **No Jest, no `tsconfig`, no fixtures.**\n\n### 3.7 State file\n\nWrite **`.extension-state.md`** using the template in [`shared\u002Frepo-layout.md`](..\u002F..\u002Fshared\u002Frepo-layout.md) §\"`.extension-state.md` template\". Fill:\n\n- **Identity** block — from PRD §2 plus the derived names (Capability, Class, Native module = `\u003CPascal>Module`, Android package, PRD path).\n- **Phase**: `Last completed: scaffold` \u002F `Next: \u002Ftest-native-extension` \u002F `Status: ok`.\n- **`## ppmplugin (third-party controls)`** block — leave at its defaults (`Target: unset`, `Manifest: not written`, `Android DEX: not built`, `iOS framework: not built`, `Bundle: not assembled`). The `\u002Fgenerate-ppmplugin` build skills fill it.\n\nIf a `.extension-state.md` already existed and we agreed to \"resume\" in Step 1.7, only update the **Phase** block (leave `## ppmplugin` untouched — it captures any later build state).\n\n---\n\n## Step 3.8 — Self-critique against the proactive protocol\n\n> **This step exists because the smoke check (Step 4) is structural only — it compiles nothing. Generated Kotlin and Obj-C don't compile-check here; they compile later in `\u002Fbuild-android-binary` (Gradle) and `\u002Fbuild-ios-binary` (xcodebuild). Runtime UI and UX bugs (toolbar overflows, dead-end modals, missing exit paths, asymmetric platform features) don't surface in *any* compile — only on a real device, often after the engineer has lost an afternoon. The self-critique protocol forces the skill to reason about its own output *before* declaring done, gate-by-gate, instead of trusting \"looks fine.\"**\n\nThe protocol lives at [`shared\u002Fself-critique-protocol.md`](..\u002F..\u002Fshared\u002Fself-critique-protocol.md). It is **proactive reasoning, not pattern matching** — each gate enumerates a category of thing (PRD-listed behaviors, user-reachable controls, mutually-exclusive modes, etc.) and forces specific questions about each enumerated item. Read the protocol file in full before running this step; what follows here is the integration contract.\n\n### 3.8.1 Re-read every file emitted in this run\n\nFresh-read from disk. Files of interest:\n\n- `android\u002Fsrc\u002Fmain\u002Fjava\u002Fcom\u002Fpowerapps\u002F\u003Clower>\u002F\u003CPascal>Module.kt` (its `@ReactMethod` request\u002Fresponse\u002Ferror surface — this IS the dispatch contract, since there is no TS layer)\n- `android\u002Fsrc\u002Fmain\u002Fjava\u002Fcom\u002Fpowerapps\u002F\u003Clower>\u002F\u003CPascal>CaptureActivity.kt` and any other Activities\n- `android\u002Fsrc\u002Fmain\u002Fres\u002F**\u002F*.xml` (layouts, menus, themes, manifest)\n- `ios\u002FRCT\u003CPascal>Module.{h,m}` (or `.swift` if ARCHITECTURE §1.2 chose Swift)\n- `ios\u002F*.podspec`\n- `pcf\u002F\u003CPascal>PCF\u002Findex.ts` and `ControlManifest.Input.xml` (if PCF was part of this run)\n\n### 3.8.2 Walk the protocol gates in order\n\nFor each gate in the protocol — **PRD coverage**, **User journey**, **Layout feasibility**, **State coverage**, **Cross-platform parity**, **Reversibility**, **Asymptotic \u002F lifecycle**, **Spec-drift**, **Runtime safety & lifecycle**, **Buildability & bundle-fit feasibility**, **PCF ↔ native round-trip contract** — execute the gate's \"enumerate + ask\" procedure as described in the protocol file.\n\n**Gate 10 (Buildability) is not optional for this track.** It re-runs, over the *generated code*, the same reasoning `\u002Fdesign-native-extension-feature` Step 7.5 ran over the design — so a dependency, return shape, or construction cost that slipped past design (or was introduced during codegen) is caught here, before `\u002Fbuild-android-binary` \u002F `\u002Fbuild-ios-binary` fail minutes later. **Gate 11 (PCF round-trip)** runs only when a PCF was emitted in this run; when the PCF is generated separately, `\u002Fgenerate-pcf-companion` Step 5.7 runs it instead.\n\n**Do not short-circuit.** Each gate enumerates real items (PRD bullet points, user-reachable controls, horizontal container children, etc.) and answers a concrete question per item. Skipping a gate because \"intuitively the code looks fine\" defeats the purpose — the whole point is to force structured analysis where past intuition has failed.\n\n### 3.8.3 Build the self-critique report\n\nAggregate findings across all gates. Each finding follows the protocol's structure:\n\n```\n- Gate: \u003Cname>\n- File: \u003Cpath>:\u003Cline range>\n- Severity: blocker | concern\n- Problem: \u003Cone sentence>\n- Why this is wrong: \u003Cone sentence anchored in the gate's principle>\n- Suggested fix: \u003Cwhat to change>\n- Autofix: applied | proposed | requires human review\n```\n\n### 3.8.4 Apply auto-fixes inline\n\nFor generation context (this skill), the user has no intermediate review checkpoint between code-emit and Step 4 smoke check. The protocol's auto-fix policy translates to:\n\n- **`autofix: applied`** (mechanical fix, no design choice) → apply silently, log to `.extension-state.md` under `Auto-fixes`.\n- **`autofix: proposed`** (structural fix, unambiguous) → apply, surface in the final summary with a one-line note. The user can read `.extension-state.md` to see the diff against their expected output.\n- **`autofix: requires human review`** (design judgment) → do NOT modify code. Promote to a top-level `NEEDS_CONTEXT` item in the final summary.\n\n(Contrast with `\u002Fdebug-extension` Step 7.5, which gates `proposed` fixes on explicit user confirm because it sits inside an interactive review cadence.)\n\n### 3.8.5 Re-loop after fixes\n\nIf 3.8.4 applied any fixes, repeat from 3.8.1. A fix at one gate can either unblock or surface a new finding elsewhere. Cap the loop at 3 iterations; if findings remain after iteration 3, surface them as concerns regardless of severity.\n\n### 3.8.6 Return-status impact\n\n- All gates clean OR fixed → continue to Step 4. Run can return `DONE`.\n- Concerns remain (acceptable per gate severity rules) → run returns `DONE_WITH_CONCERNS: \u003Cone-line summary>`. Log every concern in `.extension-state.md`.\n- Any **blocker** remains → run returns `BLOCKED: self-critique found \u003Clist>`. Surface the report. The scaffold is not shippable until the user resolves blockers.\n\n### What this step specifically catches\n\nThe protocol's gates are derived from first principles, not from a list of past failures. By construction, it should catch:\n\n| Gate | Bug class |\n|---|---|\n| PRD coverage (Gate 1) | Behaviors promised in ARCHITECTURE §3.\u003Cn> not implemented in code (e.g. \"Done button is anchored right\" → no Done button at all). |\n| User journey (Gate 2) | Modals with no exit, controls that don't respond, error paths that never resolve the Promise. |\n| Layout feasibility (Gate 3) | Toolbar overflows on small screens, controls pushed off-edge, primary actions unreachable at narrow widths or high font scales. |\n| State coverage (Gate 4) | Mode toggles without visible active state, error codes returned but not displayed, boolean toggles without observable UI. |\n| Cross-platform parity (Gate 5) | iOS got 5 features, Android got 3 — with no PRD note explaining why. |\n| Reversibility (Gate 6) | Destructive actions without confirmation, modals with one entry and no exit, multi-step flows with no back. |\n| Asymptotic \u002F lifecycle (Gate 7) | Rotation loses state but PRD says it shouldn't; primary action not idempotent under fast double-tap. |\n| Spec-drift (Gate 8) | Code does things PRD doesn't document (drift the other direction from Gate 1). |\n| Runtime safety & lifecycle (Gate 9) | Looper-less constructor `Handler` crashes the host at launch; a Promise path that never settles hangs the caller; an acquired listener\u002Fcamera never released. |\n| Buildability & bundle-fit (Gate 10) | A dependency that needs `compileSdk > 34` \u002F AGP `8.x`, an iOS framework that can't ship flat, a non-JSON-serializable return, or a streaming pattern with no channel — caught before the toolchain fails. |\n| PCF ↔ native round-trip (Gate 11) | PCF composite key ≠ manifest receiver, `cordova.exec` instead of `sendAsync`, a pre-stringified envelope, a bare `args` object, or a success path that doesn't unwrap the wrap `message` container — all silent-on-device otherwise. |\n\nWhen a new failure mode is observed, the question to ask is **\"which gate should have caught this?\"** If the answer is \"none of them\" — that's a signal the protocol needs a new gate (or an existing gate's enumeration list needs expanding). Do NOT add a one-off pattern check that doesn't generalize.\n\nThis step is LLM reasoning over generated source. It is not a real compiler. The protocol is structured enough to force enumeration (and stop \"looks fine\" from being a valid answer), but it depends on the reasoning at each gate actually being careful. Pair with `\u002Fbuild-android-binary` \u002F `\u002Fbuild-ios-binary` (real compile) and on-device verification for runtime errors the protocol can't simulate.\n\n---\n\n## Step 4 — Run a smoke check\n\nThere is **no TypeScript layer to compile** in this track, so the smoke check does NOT run `tsc`. Native code is compiled later by `\u002Fbuild-android-binary` and `\u002Fbuild-ios-binary` — not here. The smoke check is a fast **structural** pass (each is a few seconds):\n\n**OS-neutral:** the `pnpm install` below is a real, cross-platform command; the symbol-agreement check is **read+parse — run it with the built-in Grep tool**, not shell `grep` (the bash is illustrative; `grep -R` isn't on Windows).\n\n```bash\n# 1. (Optional) seed the dev-only devDeps from the public npm registry. Skip if Node\u002Fpnpm absent.\npnpm install   # resolves react + react-native (RN headers + react-android coordinate) for the build skills\n\n# 2. Structural self-check (no compile) — use the Grep tool, not shell grep:\n#    confirm +moduleName returns \u003CPascal>Module in ios\u002F AND getName() == \"\u003CPascal>Module\" in android\u002F.\ngrep -R '+ (NSString *)moduleName' ios\u002F && grep -R 'return @\"\u003CPascal>Module\"' ios\u002F  # iOS +moduleName == \u003CPascal>Module\ngrep -R 'getName()' android\u002F | grep '\"\u003CPascal>Module\"'                       # Android getName() == \u003CPascal>Module\n```\n\nThe `pnpm install` here pulls **only from the public npm registry** and requires no organization-specific package-feed authentication. It's optional: if it fails or Node\u002Fpnpm are absent, note it and continue (the build skills will seed devDeps when they run).\n\n**Ordering precondition:** ensure `.gitignore` is on disk first (it is, per §3.1) — `pnpm install` populates `node_modules\u002F`, and the `.gitignore` keeps it (and `ppmplugin\u002F`) out of the repo when *you* later commit. This skill doesn't stage or commit anything, but a missing `.gitignore` would leave `node_modules\u002F` showing as untracked. If §3.1 was skipped (e.g. resumed mid-flow), re-run from §3.1 before the smoke check.\n\nIf a step fails:\n- For `pnpm install` failures: this is non-fatal here (only the build skills truly need the devDeps). Print the failing line, note `devDeps seed deferred`, and continue.\n- For the symbol-agreement greps: a mismatch means iOS `+moduleName`, Android `getName()`, and the intended manifest `receivers[].nativeModule` have drifted. Fix so all three read `\u003CPascal>Module` and re-run. This is a real gate — the wrap host can't dispatch if they disagree.\n\nDO NOT mark the scaffold as complete in `.extension-state.md` if the symbol-agreement check fails. Set `Status: blocked` with the failure reason.\n\n---\n\n## Step 5 — Summary\n\nPrint:\n\n```\nScaffold complete\n─────────────────\nDirectory: \u003Ccwd> (\u003CN> files written — git untouched; commit when you're ready)\nNative module: \u003CPascal>Module  (== .\u002Fmanifest.json receivers[].nativeModule — both authored here, in sync)\nDispatch contract: .\u002Fmanifest.json written (committed source of truth; PCF + build stage read it)\nSmoke check: devDeps seed \u003C✓ | deferred> | native symbol agreement ✓\nState file: .\u002F.extension-state.md → Phase: scaffold \u002F Next: \u002Ftest-native-extension\n\nNext steps\n──────────\n1. Review the generated implementations in ios\u002FRCT\u003CPascal>Module.m and android\u002F...\u002F\u003CPascal>Module.kt against ARCHITECTURE §3.\u003Cn>. Customize cosmetic details (button styling, modal chrome) if you want — but the framework wiring should be working out of the box.\n2. Run \u002Ftest-native-extension for the structural contract pre-flight (native module shape, Android getName ↔ iOS +moduleName ↔ .\u002Fmanifest.json agreement, cross-platform JSON parity). Native iOS \u002F Android compile is NOT validated here — it happens in \u002Fbuild-android-binary and \u002Fbuild-ios-binary.\n3. Run \u002Fgenerate-pcf-companion to generate the Companion PCF under pcf\u002F — it reads .\u002Fmanifest.json for the composite key so the PCF and module stay aligned.\n4. When ready to ship, run \u002Fgenerate-ppmplugin — it validates + stages .\u002Fmanifest.json, compiles the native binaries, and assembles the .ppmplugin bundle for upload to Dataverse.\n```\n\n---\n\n## Step 6 — Offer next-step skills\n\nPer `shared\u002Fshared-instructions.md §9.1`, use `AskUserQuestion` with all plausible next skills as options (not a Yes\u002FNo), and include an escape-hatch option.\n\n```\nQuestion: \"What would you like to do next?\"\nHeader:   \"Next step\"\n\nOptions:\n  1. \"Run \u002Fgenerate-pcf-companion\"\n     description: \"Next major scaffold step in the build flow. Runs `pac pcf init` under pcf\u002F and writes the Companion PCF that dispatches on the composite key \u003Cname>\u002F\u003Creceiver> read from .\u002Fmanifest.json (authored here), cross-checked against ARCHITECTURE §4 + §8 + §9.\"\n  2. \"Run \u002Ftest-native-extension\"\n     description: \"Structural contract pre-flight (native module shape, Android getName ↔ iOS +moduleName ↔ .\u002Fmanifest.json agreement, cross-platform JSON parity). Quick sanity check before adding more code. No compile.\"\n  3. \"Run \u002Fgenerate-ppmplugin\"\n     description: \"Build the deliverable: validates + stages .\u002Fmanifest.json, compiles the native binaries (DEX \u002F iOS framework), and assembles the .ppmplugin bundle. Usually run after the PCF exists and the contract is settled.\"\n  4. \"Stay — I'll review the generated code first\"\n     description: \"Skill exits. Run git diff to inspect the scaffold and decide what to run next yourself.\"\n```\n\n**For options 1, 2, or 3:** **invoke that skill via the `Skill` tool in the same turn** — selecting the option IS the request to run it (Execute, don't describe — shared-instructions §9.1 HARD RULE). Do NOT stop and tell the user to run it themselves. The invoked skill runs its own Step 1 prereq check + gates, so just hand off to it.\n\n**For option 4 (stay):** print one line: `Scaffold complete. Run any of the suggested skills when you're ready.` Then proceed to return-status.\n\n---\n\n## Return-status protocol\n\nThe literal first line of your final message MUST be one of:\n\n| Code | Meaning |\n|---|---|\n| `DONE` | All files written, smoke check (native symbol agreement) passed, state file updated. Next: `\u002Ftest-native-extension`. |\n| `DONE_WITH_CONCERNS: \u003Clist>` | Files written but the smoke check raised non-fatal warnings (e.g. devDeps seed deferred), OR a `\u002F\u002F TODO` count higher than expected (PRD operations that are particularly complex), OR a derived-name collision was deferred to the user. |\n| `NEEDS_CONTEXT: \u003Cmissing>` | A required PRD section was incomplete; couldn't proceed without re-running `\u002Fdesign-native-extension-feature` on that section. |\n| `BLOCKED: \u003Creason>` | Prereq failed (`git` missing), a reserved-name \u002F generic-noun collision on the native module symbol could not be resolved, or the native symbol-agreement check failed and could not be auto-fixed. |\n\nAfter the first line, blank line, then the human-readable summary.\n\n---\n\n## Scope of this skill — generating vs auditing\n\nThis skill **generates** the extension repo from a PRD. It's not a linter; it doesn't audit existing working code against the new template. When run in a directory that already has a scaffold, the regenerate \u002F resume \u002F abort gate asks before overwriting — and \"regenerate\" is destructive (loses any manual edits to generated files).\n\n**A diff between \"what we'd generate now\" and \"what exists\" is NOT a list of defects.** Existing code that builds (in `\u002Fbuild-android-binary` \u002F `\u002Fbuild-ios-binary`) and produces correct runtime behavior is fine even if the code shape differs from the current template. Flag only:\n\n- Code that won't compile in the build skills (malformed Kotlin \u002F Obj-C)\n- Code that violates the dispatch contract such that runtime breaks (native module symbol that doesn't match `receivers[].nativeModule`, wrong method signature the host can't dispatch, wrong response shape that PAM can't deliver)\n- Cross-platform drift (iOS and Android emitting different request field names or response shapes for the same operation)\n\nA locally-defined helper that does the same job as a template helper is NOT a defect. A method that was originally generated with a `\u002F\u002F TODO` marker and has since been filled in by the engineer is the EXPECTED state — don't flag it as \"different from the template.\"\n\n## Hard rules — correctness (these must be true for the control to work)\n\n- **There is NO TS `INativeExtension` layer.** The contract is the manifest's runtime dispatch (`\u003Cname>\u002F\u003Creceiver>` → `NativeModules.\u003CPascal>Module.\u003Cmethod>`, [`shared\u002Fppmplugin-format.md §2`](..\u002F..\u002Fshared\u002Fppmplugin-format.md)). Do NOT generate `src\u002F\u003CPascal>Extension.ts`, `handleMessageAsync`, or a `sendAsync` transport — they're SDK-era leakage that `\u002Faudit-ppmplugin` rejects.\n- **The native module symbol MUST be `\u003CPascal>Module` across all runtime surfaces.** iOS `+moduleName` returns `@\"\u003CPascal>Module\"`, Android `getName() = \"\u003CPascal>Module\"`, and the `.\u002Fmanifest.json` `receivers[].nativeModule` (authored here in §3.1, alongside these modules) MUST agree, and `\u003CPascal>Module` is what JS sees as `NativeModules.\u003CPascal>Module`. The Obj-C class name stays `RCT\u003CPascal>Module` and matches `entrypoints.ios.moduleClass`. Mismatch = the wrap host's dispatch finds nothing.\n- **The native module symbol MUST pass the validator's reserved-prefix \u002F reserved-name rules** ([`shared\u002Fppmplugin-format.md §4`](..\u002F..\u002Fshared\u002Fppmplugin-format.md)). The `Module` suffix is the structural dodge for bare reserved names (`DeviceInfo` → `DeviceInfoModule`); do NOT strip it. If the derived symbol still hits a reserved prefix or the known reserved-name subset, STOP and have the user pick a vendor-prefixed class.\n- **iOS and Android JSON response shapes MUST be byte-identical for the same input.** Both implementations are derived from the same §3.\u003Cn> response shape. Drift here causes maker-side branching bugs that are hard to diagnose.\n- **Request\u002Fresponse field names MUST match what the manifest + PCF expect.** The field names each native method parses and emits must match the request\u002Fresponse shapes in ARCHITECTURE §4 (which the manifest and PCF both derive from). Drift = the PCF sends\u002Freads the wrong fields.\n- **The native symbol-agreement smoke check (Step 4) MUST pass** before marking `.extension-state.md` Phase = `scaffold`. On failure, set `Status: blocked` with the reason.\n- **No invented APIs.** If §3.\u003Cn> specifies a framework\u002FAPI you're uncertain exists on the named min-OS version, STOP and flag. Don't ship code referencing a method that doesn't exist.\n\n## Recommended template style (preferences for new scaffolds; existing working code is fine)\n\nThese describe the cleanest shape for newly-generated code. They're how this skill renders fresh output. Existing code that achieves the same outcome differently is **not** in violation — don't flag stylistic differences as defects when auditing.\n\n- **Prefer the `Module`-suffixed native symbol** (`\u003CPascal>Module`) consistently across iOS, Android, and the future manifest. It satisfies the reserved-name rule and keeps the three dispatch surfaces in lockstep.\n- **Prefer complete implementations from ARCHITECTURE §3.\u003Cn>** rather than `\u002F\u002F TODO: implement using \u003Cframework>` placeholders. Generated code should compile and run on the happy path out of the gate.\n- **Prefer the pinned `react-native` devDep** from `package.json` for the build skills to resolve RN headers \u002F the `react-android` coordinate, rather than `latest`. Reproducible builds.\n\n## Things the skill enforces at generation time\n\nWhen emitting new code from scratch, the skill follows the template above. Strict requirements:\n\n- **No file overwrites without explicit confirm** — the regenerate \u002F resume \u002F abort gate at Step 2 is real.\n- **STOP with `NEEDS_CONTEXT` only when blocked.** Genuine blockers: PRD §2 missing (no class name → no scaffold), PRD §4 missing (no operations → nothing to generate), or a reserved-name \u002F generic-noun collision on the native module symbol the user won't resolve. Non-blockers that should NOT STOP: ARCHITECTURE §3.\u003Cn> missing fine implementation detail (use a reasonable default + annotate inline), ARCHITECTURE §8 missing UX for a specific code (use default mapping), framework choice in ARCHITECTURE §1.2 \u002F §1.3 missing a deployment-target detail (use the framework's documented minimum).\n- **Smoke check is a real gate.** If the native symbol-agreement check (Step 4) fails, `.extension-state.md` Phase stays below `scaffold` with `Status: blocked`. (The optional `pnpm install` is non-fatal — deferring the devDeps seed doesn't block.)\n- **Every function\u002Fmethod called from generated native code MUST be defined in the same file or imported.** This is enforced by Step 3.8 (self-review). When writing `this.createFoo()` in a Kotlin Activity, also write `private fun createFoo() { ... }` in the same file. When writing `[self setupBar]` in an Obj-C module, also write `- (void)setupBar { ... }`. The smoke check (Step 4) compiles nothing — native code errors surface in `\u002Fbuild-android-binary` \u002F `\u002Fbuild-ios-binary`. The self-review catches the \"called but never defined\" class of bug at generation time, not later when an engineer runs the binary build.\n- **Capture \u002F modal-presentation operations MUST include a Done action.** Any operation whose pattern is one-shot OR two-way with a confirm-and-return UX (drawing capture, photo capture, signature, scanner with manual confirm, etc.) MUST emit a toolbar Done action that triggers result submission. Cancel alone is not sufficient — the user has no way to complete the operation. On Android: `R.menu.\u003Clower>_capture_menu` MUST include `\u003Citem android:id=\"@+id\u002Faction_done\">` and `onOptionsItemSelected` handles it. On iOS: the `UINavigationItem` MUST set `rightBarButtonItem` to a Done `UIBarButtonItem`. Non-negotiable.\n- **Multi-mode operations (mutually exclusive UI modes — e.g. capture\u002Fplayback, photo\u002Fvideo, edit\u002Fpreview) MUST include active-state visual feedback.** On Android: use `MaterialButtonToggleGroup` with `app:singleSelection=\"true\"` for the mode toggle row; toggled-on button shows `colorPrimaryContainer`, toggled-off transparent. On iOS: rely on the platform's tool-picker (e.g. PencilKit, `AVCaptureSession`'s built-in UI) which handles this natively, OR use `UISegmentedControl` for custom toggles. Without active-state feedback, the user can't tell which mode is selected.\n\nAllowed in generated code (these are not defects):\n\n- **`\u002F\u002F TODO:` or `\u002F\u002F Customize:` comments** for items that legitimately need engineer judgment — non-trivial UI details that ARCHITECTURE §3.\u003Cn> doesn't specify (button corner radius, animation curves, edge case handling that the PRD intentionally defers). Clear annotated TODOs beat fabricated defaults.\n- **Reasonable defaults for unspecified detail.** If ARCHITECTURE §3.\u003Cn> says \"Done button right side\" but doesn't specify the button's tint color, use the platform default (e.g. `.systemBlue` on iOS, `?attr\u002FcolorAccent` on Android). Add a `\u002F\u002F Customize: tint per design` comment if it's likely the engineer will want to change it.\n- **Fallback error responses** when something at runtime doesn't match the contract. If a native method receives args it can't parse, resolving the promise with an error JSON (`{\"status\":\"error\",\"error\":\"INVALID_INPUT\",\"message\":\"\u003Cthe offending field \u002F parse reason>\"}`) via the `errorJson(code, message)` helper is the right answer — not a thrown exception. Always include the `message` — same for `INTERNAL_ERROR` catch-alls (carry the underlying reason).\n\nOther operational rules:\n\n- No reference-repo fetch at runtime — structural patterns are encoded in this SKILL.md + `shared\u002F*.md`.\n- No PCF generation (that's `\u002Fgenerate-pcf-companion`).\n- No manifest, binary build, or bundle assembly (that's `\u002Fgenerate-ppmplugin` and its stages).\n- No `src\u002F` TS layer, no tests (there's nothing to type-check; structural verification is `\u002Ftest-native-extension`).\n- No remote, feed, or registry access — this track is self-contained and local-only.\n\n## Runtime fallbacks the generated control SHOULD have\n\nThese are correctness-positive — make the control resilient against unexpected runtime conditions:\n\n- **Construction must not throw (crash-at-launch guard).** The module is built eagerly at bridge startup on a possibly Looper-less thread ([`ppmplugin-format §5`](..\u002F..\u002Fshared\u002Fppmplugin-format.md)); an uncaught throw in the constructor \u002F `init{}` (Android) or `init` (iOS) takes down the whole host before any UI. Defer listener\u002Fhardware registration to first method call; pass an explicit `Handler(Looper.getMainLooper())` (never `null`); wrap unavoidable init in try\u002Fcatch. See §3.4 (iOS) \u002F §3.5 (Android).\n- **Runtime permission + `currentActivity` guards** (Android): check a dangerous permission before the API call and resolve `PERMISSION_DENIED` on denial; guard `currentActivity != null` before presenting UI and resolve `NO_ACTIVITY` on null — never let `SecurityException` \u002F NPE crash the host. See §3.5.\n- **Native methods try\u002Fcatch around argument parsing** → resolve with `{status: 'error', error: 'INVALID_INPUT', message: '\u003Coffending field \u002F parse reason>'}` instead of throwing. The wrap bridge expects a resolved promise (an error JSON), not a rejected\u002Fthrown one.\n- **Native error responses go through the `errorJson(code, message)` helper** (code + human-readable reason), not as thrown exceptions. The native side resolves the Promise with an error JSON string carrying both fields; the wrap bridge delivers it to the PCF verbatim (which surfaces `message` as its `ErrorMessage` output).\n- **Native main-thread guarding for UI presentation** (`dispatch_async(dispatch_get_main_queue(), ...)` on iOS, `Handler(Looper.getMainLooper()).post { ... }` on Android). Presenting UI off main thread crashes; the dispatch is required.\n- **Unknown error codes from native** that the PCF didn't anticipate → pass through to the response verbatim. Don't drop or remap; let the maker's Power Fx formula see the actual code.\n\nThese are runtime safety nets. They're allowed\u002Fencouraged regardless of other style decisions.\n","# \u002Fgenerate-native-extension\n\nReads `PRD.md` in the working directory and writes the native sources for a third-party PAM control following the layout in [`shared\u002Frepo-layout.md`](..\u002F..\u002Fshared\u002Frepo-layout.md). This is the **native-only** (compiled `.ppmplugin`) track — there is NO TypeScript `INativeExtension` \u002F `handleMessageAsync` layer; the wrap host dispatches straight to `NativeModules.\u003CPascal>Module.\u003Cmethod>` per the manifest's receivers contract (see [`shared\u002Fppmplugin-format.md §2`](..\u002F..\u002Fshared\u002Fppmplugin-format.md)). The output is substantially complete native code so the engineer starts at customizing OS-specific code, not writing boilerplate.\n\nThis skill writes the **native module** half of the repo (`ios\u002F`, `android\u002F`, optional podspec, dev-only package.json) **and the committed `.\u002Fmanifest.json`** — the dispatch-contract source of truth. The manifest is authored *here*, alongside the native code it describes, because every field in it is derived from the names this scaffold emits (`getName()`, the `@ReactMethod` list, the package class); authoring it now means the **Companion PCF** (`\u002Fgenerate-pcf-companion`) reads a real contract instead of re-deriving one, so the composite key `\u003Cname>\u002F\u003Creceiver>` can't drift between the PCF and the module. The build stage `\u002Fgenerate-ppmplugin-manifest` (inside `\u002Fgenerate-ppmplugin`) then **validates + reconciles + stages** this manifest rather than authoring it from scratch. The **Companion PCF** is generated separately by `\u002Fgenerate-pcf-companion` because it requires `pac` CLI and a different toolchain.\n\n---\n\n## Step 1 — Read the shared docs and the PRD\n\nBefore any write:\n\n1. Read [`shared\u002Fshared-instructions.md`](..\u002F..\u002Fshared\u002Fshared-instructions.md).\n2. Apply the **per-skill minimal prereq policy** ([`shared-instructions.md §1.5`](..\u002F..\u002Fshared\u002Fshared-instructions.md)). This track is **self-contained** ([`shared-instructions §0a`](..\u002F..\u002Fshared\u002Fshared-instructions.md)) and uses only the working tree and public package registries. This skill needs no toolchain to write the files — optionally Node + pnpm to seed the dev-only `package.json`'s devDeps from the public npm registry (used later by `\u002Fbuild-android-binary` \u002F `\u002Fbuild-ios-binary`, not here). Step 4's smoke check is a structural self-check — it does NOT compile anything. Run the **`\u002Fgenerate-native-extension` check** from [`prereq-check.md`](..\u002F..\u002Fshared\u002Fprereq-check.md) (git required; Node\u002Fpnpm optional — there is no \"baseline\" check in this self-contained track).\n\n   **Print the prereq status as a visible block per `shared-instructions.md §9.2`** before continuing:\n\n   ```\n   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    Prereq check — \u002Fgenerate-native-extension\n   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n    ? ✓ git installed\n    ? ✓ Node 20+ installed     (optional — only to seed package.json devDeps from public npm)\n    ? ✓ pnpm installed         (optional — same)\n\n    ? checks passed. Ready to proceed.\n   ```\n\n   If `git` is missing, print its `→ Fix:` line and STOP. Node\u002Fpnpm are optional here — if absent, note them as `n\u002Fa (devDeps seed deferred to build skills)` rather than failing.\n3. Read [`shared\u002Fnaming-conventions.md`](..\u002F..\u002Fshared\u002Fnaming-conventions.md) — the derived-identifier table is canonical, including the **`Module`-suffix rule** for the native module symbol. Derive all file paths and class names from §2 of the PRD using that table; do not invent.\n4. Read [`shared\u002Fppmplugin-format.md`](..\u002F..\u002Fshared\u002Fppmplugin-format.md) — §2 (the runtime dispatch contract: `\u003Cname>\u002F\u003Creceiver>` → `NativeModules.\u003CnativeModule>.\u003Cmethod>`, where `\u003CnativeModule>` = `\u003CPascal>Module`) and §4 (the upload-compatibility checks that the native module symbol must satisfy). The native modules this skill emits dispatch straight off that contract — there is NO TS `INativeExtension` layer mediating; see §3.3 below.\n5. Read [`shared\u002Frepo-layout.md`](..\u002F..\u002Fshared\u002Frepo-layout.md) — the exact tree, file list, and `package.json` shape to emit.\n6. Read `.\u002FPRD.md` from the current working directory. If missing or empty, STOP with `BLOCKED: PRD.md not found — run \u002Fdesign-native-extension-feature first.`\n7. Read `.\u002F.extension-state.md` if present. If the phase shows `scaffold-complete`, ask the user whether to **regenerate** (with confirm — overwrites files), **resume** (only fill in missing files), or **abort**.\n\nThe structural patterns this skill needs to emit (iOS module shape, Android module shape, podspec, package.json) are fully prescribed in this SKILL.md (§3.1–§3.7) and in [`shared\u002Frepo-layout.md`](..\u002F..\u002Fshared\u002Frepo-layout.md). Do NOT fetch the reference extension repo at runtime — its lessons are already encoded here, and fetching it would risk copying PDF-specific code into a non-PDF extension.\n\nIf any read fails, STOP and report which file is missing.\n\n---\n\n## Step 2 — Confirm the scaffold plan with the user\n\nPrint a concise summary derived from the PRD, then gate on approval before any write.\n\n```\nScaffold plan\n─────────────\nRepo: powerapps-\u003Ckebab>\npackage: \u003Ckebab>-control  (dev-only, private — not published)\nClass: \u003CPascal>\nNative module: \u003CPascal>Module  → NativeModules.\u003CPascal>Module  (== .\u002Fmanifest.json receivers[].nativeModule)\niOS class: RCT\u003CPascal>Module  (+moduleName returns \u003CPascal>Module)\nAndroid module: \u003CPascal>Module  (com.powerapps.\u003Clower>)\nPodspec: \u003CPascal>Extension.podspec (optional, system-frameworks-only)\nDispatch contract: .\u002Fmanifest.json (committed — written by this skill; read by the PCF + build stage)\n\nFrameworks\n  iOS:     \u003Clist from ARCHITECTURE §1.2>\n  Android: \u003Clist from ARCHITECTURE §1.3>\n\nOperations (\u003Ccount from PRD §4>): \u003Ccomma-separated names>\nPattern: \u003Cone-shot | streaming | two-way>\nError codes: \u003Ccount from ARCHITECTURE §5>\n\nTarget directory: \u003Ccwd> (writes \u003CN> files; no existing files will be overwritten without confirm)\nDistribution: the compiled `.ppmplugin` bundle (built later by \u002Fgenerate-ppmplugin). This skill is purely local — no remote, no feed, no registry.\n```\n\nUse `AskUserQuestion` (single-select):\n\n> Proceed with this scaffold?\n> - **Yes — generate the files** (recommended): write the control's sources into the current directory. This skill does **not** run git — no `git init`, no staging, no commit (the control lives in your existing repo; you commit when you're ready).\n> - **Edit the PRD first** — exit; user re-runs `\u002Fdesign-native-extension-feature` to adjust.\n> - **Cancel**\n\n---\n\n## Step 3 — Generate the files\n\nWrite files in the order below. After each top-level group, print a one-line progress update (`✓ wrote ios\u002F (3 files)`). Don't dump file contents — the user sees the diff via the IDE.\n\nEvery file path is **relative to the current working directory** (the repo root). Names are derived per [`shared\u002Fnaming-conventions.md`](..\u002F..\u002Fshared\u002Fnaming-conventions.md).\n\n### 3.1 Top-level repo files\n\nWrite:\n\n- **`.gitignore`** — emit exactly the following entries:\n  - Node: `node_modules\u002F`, `dist\u002F`, `build\u002F`\n  - **`.ppmplugin` build staging — MANDATORY**: `ppmplugin\u002F` (the gitignored staging dir where `\u002Fgenerate-ppmplugin` writes the **staged copy** of the manifest, the binaries, and the final bundle — never committed. NOTE: the **committed** source-of-truth `manifest.json` lives at the repo **root** (`.\u002Fmanifest.json`, written below), NOT under `ppmplugin\u002F` — do not gitignore it; see [`shared\u002Fppmplugin-format.md §1`](..\u002F..\u002Fshared\u002Fppmplugin-format.md))\n  - OS \u002F editor: `.DS_Store`, `.idea\u002F`, `.vscode\u002F`\n  - Claude Code local state (per-user, not shared): `.claude\u002F`\n  - Env: `.env*` (but allow `!.env.example`)\n  - iOS build: `Pods\u002F`, `*.xcworkspace`, `DerivedData\u002F`, `*.xcodeproj\u002Fxcuserdata\u002F`\n  - Android build: `*.iml`, `.gradle\u002F`, `local.properties`, `captures\u002F`, `.externalNativeBuild\u002F`, `.cxx\u002F`\n  - PCF build dirs only — NOT the `pcf\u002F` folder itself; source files (`index.ts`, `ControlManifest.Input.xml`, `package.json`, `pcfconfig.json`, etc.) stay tracked: `pcf\u002F**\u002F{out,Solutions,node_modules,obj,bin,generated}\u002F`\n  - Test-harness artifacts: `test-harness\u002F*.msapp`\n  - Skill-generated backups: `*.bak.*` (skills that replace tracked content may save a timestamped backup; those are intentionally local-only)\n  - Design-time previews: `.pcf-preview\u002F` (HTML mockup of the PCF as it appears in Canvas Studio — written by `\u002Fdesign-native-extension-feature` Step 8.0 for visual review; regenerated each design iteration; not a source-of-truth artifact)\n\n- **`package.json`** — per the dev-only shape in [`shared\u002Frepo-layout.md`](..\u002F..\u002Fshared\u002Frepo-layout.md) §\"`package.json` shape (dev-only)\". Fill in `name` (a plain local name, e.g. `\u003Ckebab>-control`) and `description` from the PRD. `version` starts at `0.1.0`. Set `\"private\": true`.\n\n  This manifest is **never published** — no `publishConfig`, no feed registry, no `files` array, no `main`\u002F`types`, no `.npmrc`. Its only job is to pin the React Native version the native builds compile against:\n  ```json\n  {\n    \"name\": \"\u003Ckebab>-control\",\n    \"version\": \"0.1.0\",\n    \"private\": true,\n    \"description\": \"\u003Cfrom PRD §1>\",\n    \"devDependencies\": {\n      \"react\": \"18.2.0\",\n      \"react-native\": \"0.79.7\"\n    }\n  }\n  ```\n  The `react-native` devDep supplies the iOS headers (`\u002Fbuild-ios-binary`) and pins the `react-android` coordinate the Android build resolves (`\u002Fbuild-android-binary`); add any other build-time devDeps the native modules need. All deps resolve from the **public npm registry** — there is no internal feed.\n\n- **`manifest.json`** (repo root, **committed** — the dispatch-contract source of truth) — author it now from the names this scaffold emits, per [`shared\u002Fppmplugin-format.md`](..\u002F..\u002Fshared\u002Fppmplugin-format.md) §2 (schema) + §3 (derivation). This is the single artifact the Companion PCF (`\u002Fgenerate-pcf-companion`) and the build stage (`\u002Fgenerate-ppmplugin-manifest`) both read; authoring it here, next to the native code it describes, is what keeps the composite key `\u003Cname>\u002F\u003Creceiver>` from drifting between the PCF and the module. Fields:\n  - `name` = `kebab(\u003CPascal>)` of the **class** name (not the repo\u002Fcapability name) — e.g. class `PenInput` → `pen-input`.\n  - `version` = the `package.json` version (`0.1.0`).\n  - `abi` = `{ \"compatibleShells\": \">=1.0.0\", \"builtAgainst\": \"1.0.0\" }` (default; the build skills don't change it).\n  - `receivers[]` = a single entry `{ \"name\": \"\u003CPascal>Extension\", \"nativeModule\": \"\u003CPascal>Module\", \"methods\": [\u003Cevery @ReactMethod \u002F RCT_EXPORT_METHOD name emitted in §3.4 \u002F §3.5>] }`. `nativeModule` MUST equal Android `getName()` and the iOS `+moduleName` return value — the **`Module`-suffixed** name (the reserved-name dodge).\n  - `entrypoints` = declare **every platform this scaffold generated** (so the committed manifest is the *full* contract; the build stage trims it to the shipped target):\n    - Android → `\"android\": { \"dex\": \"\u003CPascal>Plugin.dex\", \"packageClass\": \"com.powerapps.\u003Clower>.\u003CPascal>Package\" }`\n    - iOS → `\"ios\": { \"framework\": \"\u003CPascal>Plugin\", \"moduleClass\": \"RCT\u003CPascal>Module\" }`\n\n  This is a **logical contract**, not a built artifact — it lists the platforms the module *supports*; the per-platform binaries are compiled later and the staged copy under `ppmplugin\u002Fstaging\u002F` is reconciled down to whatever actually ships. Do NOT emit any `entrypoints.js` \u002F `extension.hbc` \u002F `extensionClassName` \u002F `jsLayer` field — those are SDK-era leakage `\u002Faudit-ppmplugin` rejects. (The build stage re-runs the full validator on this file, so a malformed manifest is caught either way — but emit it correctly here.)\n\n- **`README.md`** — one-page user-facing doc tailored to the control. Sections: \"What's in the box\" (the compiled `.ppmplugin` bundle + PCF companion), \"Build\" (run `\u002Fgenerate-ppmplugin` to produce the `.ppmplugin`), \"Architecture\" (a Mermaid-or-ASCII diagram of Canvas formula → PCF → wrap-bridge → `NativeModules.\u003CPascal>Module`), \"Development\" (`pnpm install` to seed devDeps; native code is compiled by the build skills, not here), \"Reference docs\" (link to `shared\u002Fppmplugin-format.md`). Use the PRD's §1 Summary verbatim. Drive every section from the PRD — never inject example values, prose, or screenshots from any other control's README.\n\n- **`CHANGELOG.md`** — single entry:\n  ```markdown\n  # Changelog\n\n  ## 0.1.0 — \u003CISO date>\n\n  - Initial scaffold for \u003CHuman-Readable Name> native control.\n  - Generated by pam-native-extensions plugin from PRD.md.\n  ```\n\n- **`LICENSE`** — MIT.\n\n### 3.2 The podspec (optional, at repo root)\n\nWrite **`\u003CPascal>Extension.podspec`** at the repo root (NOT inside `ios\u002F`) **only if** ARCHITECTURE §1.2 names additional iOS system frameworks the module links. The `.ppmplugin` iOS build (`\u002Fbuild-ios-binary`) compiles from a throwaway staged Xcode project and does NOT npm-autolink against this podspec — so it lists **system frameworks only** (no `React-Core` \u002F RN-CLI autolink dependency, no remote `source`). It exists for local `pod lib lint` convenience, not the bundle build. Template:\n\n```ruby\nrequire \"json\"\n\npackage_json = JSON.parse(File.read(File.join(__dir__, \"package.json\")))\n\nPod::Spec.new do |s|\n  s.name         = \"\u003CPascal>Extension\"\n  s.version      = package_json[\"version\"]\n  s.summary      = \"\u003Cone-line description from PRD>\"\n  s.description  = \u003C\u003C-DESC\n    \u003C2-3 sentence description from PRD — what it does, what it bridges to>\n  DESC\n  s.license      = \"MIT\"\n  s.author       = { \"Author\" => \"\" }\n  s.platform     = :ios, \"\u003Cmin-deployment-target from ARCHITECTURE §1.2>\"\n  s.source       = { :path => \".\" }\n  s.source_files = \"ios\u002F**\u002F*.{h,m}\"   # change to {h,m,swift} if Swift used\n  s.frameworks   = \u003Ccomma-quoted list of SYSTEM frameworks from ARCHITECTURE §1.2>\n  # No React-Core dependency: the .ppmplugin build resolves RN headers from the\n  # react-native devDep in package.json, not via CocoaPods autolinking.\nend\n```\n\n### 3.3 No TypeScript layer — the dispatch contract\n\nThis is the **native-only** track: there is **no `src\u002F` TypeScript layer**, no `src\u002F\u003CPascal>Extension.ts`, no `src\u002Ftypes.ts`, no `INativeExtension` \u002F `handleMessageAsync` implementation, and no `sendAsync` transport. (Those belong to the first-party SDK track — **NOT in this track**.) Do NOT generate any of them; reintroducing a TS contract layer here produces SDK-era leakage that `\u002Faudit-ppmplugin` rejects.\n\nThe contract instead is the **manifest's runtime dispatch** ([`shared\u002Fppmplugin-format.md §2`](..\u002F..\u002Fshared\u002Fppmplugin-format.md)): the wrap host routes a call by the composite key `\u003Cname>\u002F\u003Creceiver>` **straight to** `NativeModules.\u003CPascal>Module.\u003Cmethod>(args, promise)`. There is no JS mediator. This means:\n\n- The **request shape** (the `args` object) and **response shape** (the object the promise resolves with) from ARCHITECTURE §4 are realized **directly** in the native `@ReactMethod` \u002F `RCT_EXPORT_METHOD` signatures + their JSON responses — see §3.4 (iOS) and §3.5 (Android). The per-operation JSON parsing, request validation, operation branching, and error-code responses that a first-party TS `handleMessageAsync` would have done are emitted **inside each native method** instead. That dispatch logic is the valuable part this skill generates.\n- The `manifest.json` that declares `name`, `receivers[].method`, and `receivers[].nativeModule` (= `\u003CPascal>Module`) is written by **this skill** at the repo root (§3.1) — the native module symbols it emits and the manifest's `receivers[]` are authored together, so they can't disagree. `\u002Fgenerate-ppmplugin-manifest` later validates + reconciles + stages this file rather than re-authoring it (§2\u002F§3 below + [`shared\u002Fppmplugin-format.md §3`](..\u002F..\u002Fshared\u002Fppmplugin-format.md)).\n- The error-code set from ARCHITECTURE §5 is realized as the string codes the native `errorJson(code, message)` helpers emit (§3.4 \u002F §3.5), each paired with a human-readable `message` — there is no TS error-union type to declare. These codes are the **stable strings** from the canonical catalog [`shared\u002Ferror-codes.md`](..\u002F..\u002Fshared\u002Ferror-codes.md) (Canvas formulas branch on them, so they must not drift); emit exactly the catalog spelling for any code ARCHITECTURE §5 reuses. The PCF reads both: the `error` code to branch on, the `message` to surface as its `ErrorMessage` output.\n\n### 3.4 iOS (`ios\u002F`)\n\nWrite:\n\n- **`ios\u002FRCT\u003CPascal>Module.h`** — minimal Obj-C header importing `\u003CReact\u002FRCTBridgeModule.h>`, declaring `@interface RCT\u003CPascal>Module : NSObject \u003CRCTBridgeModule> @end`.\n\n- **`ios\u002FRCT\u003CPascal>Module.m`** — the implementation. **Generate complete working code, not TODO placeholders.** For each operation in PRD §4, the per-operation §3.\u003Cn> block prescribes every implementation decision (framework, hosting, key APIs, export shape, edge case handling). Generate the implementation verbatim from §3.\u003Cn>:\n\n  - Imports: include `RCT\u003CPascal>Module.h`, `UIKit`, plus every framework named in ARCHITECTURE §3.\u003Cn>'s \"Framework \u002F class\" field for any operation (e.g. `#import \u003CPencilKit\u002FPencilKit.h>` if any §3.\u003Cn> names PencilKit).\n  - Module identity: **do NOT emit `RCT_EXPORT_MODULE(...)`** in a wrap plugin framework. That macro registers via `+load` and `_RCTRegisterModule`, which is not visible to the framework's `dlopen` flat namespace. Instead emit a class method `+ (NSString *)moduleName { return @\"\u003CPascal>Module\"; }` — the **`Module`-suffixed** name. The Obj-C class name stays `RCT\u003CPascal>Module` (matching `entrypoints.ios.moduleClass`), while `+moduleName` MUST equal the manifest's `receivers[].nativeModule` and JS sees `NativeModules.\u003CPascal>Module`. Do NOT strip the suffix.\n  - `+ (BOOL)requiresMainQueueSetup` returning `NO` unless any §3.\u003Cn> requires main-thread init.\n  - **`init` safety — the module is instantiated eagerly at load via `[cls new]`, so `init` MUST NOT throw or do heavy\u002Fside-effecting work** ([`ppmplugin-format §5`](..\u002F..\u002Fshared\u002Fppmplugin-format.md)). Do not acquire hardware, register `NSNotification`\u002FKVO observers, or touch `AVCaptureSession`\u002F`CLLocationManager` in `init` — defer to the first `RCT_EXPORT_METHOD` call (lazy), and wrap any unavoidable init work in `@try\u002F@catch`. An uncaught exception in `init` crashes the host at launch (the iOS analogue of the Android Looper-less-`Handler` crash).\n  - For each operation, write an `RCT_EXPORT_METHOD` taking **exactly one `NSDictionary *request` parameter**, then `RCTPromiseResolveBlock resolve`, `RCTPromiseRejectBlock reject` — e.g. `RCT_EXPORT_METHOD(capturePenInput:(NSDictionary *)request resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)`. This matches the wrap dispatch contract: the PCF sends `args: [request]` (a one-element array) spread positionally, so the method's first positional param is the request dictionary ([`ppmplugin-format §2`](..\u002F..\u002Fshared\u002Fppmplugin-format.md)). Read fields off `request` (`request[@\"…\"]`); do NOT expand into multiple positional params. Also: the Obj-C class MUST instantiate via a no-arg `[cls new]` after the runtime loads it — don't add a custom designated initializer that takes arguments. The body implements §3.\u003Cn>'s iOS spec **completely**:\n    - The hosting setup (\"dedicated UIViewController presented modally, full-screen\" → emit a `UIViewController` subclass or inline VC + `presentViewController:animated:completion:`). **The presented VC's `viewDidLoad` MUST constrain custom content views to `view.safeAreaLayoutGuide`, not `view` directly** — this prevents content from intruding under the notch \u002F Dynamic Island \u002F home indicator. Set `modalPresentationStyle = UIModalPresentationFullScreen` (or `.pageSheet` per ARCHITECTURE §3.\u003Cn>). Add a `UINavigationBar` with Done \u002F Cancel `UIBarButtonItem`s for clear action affordance — same Material-toolbar-equivalent pattern as Android.\n    - The key API calls in the order §3.\u003Cn> specifies (e.g. `PKCanvasView` init, `PKToolPicker` attachment, drawing capture)\n    - Each Done\u002FCancel\u002Fdismiss handler as §3.\u003Cn> specifies\n    - The export step as §3.\u003Cn>'s \"Export\" line specifies (e.g. `drawing.image(from: canvas.bounds, scale: 2.0)` → PNG → base64)\n    - Each edge case from §3.\u003Cn>'s \"Edge cases handled\" list, with the exact behavior named (e.g. \"User taps Cancel → resolve with USER_CANCELLED\")\n  - Threading: background work on `dispatch_get_global_queue`; UI presentation on `dispatch_get_main_queue`. Long-running native work must not block the JS thread.\n  - Error helper: emit `- (NSString *)errorJsonWithCode:(NSString *)code message:(NSString *)message` that builds the dict `@{@\"status\": @\"error\", @\"error\": code, @\"message\": (message ?: @\"\")}` and serializes it via **`NSJSONSerialization`** — the SAME serializer as the success helper. Do **NOT** use `stringWithFormat`: a `message` (or code) containing a `\"`, `\\`, or newline would emit invalid JSON, which the PCF's response parse would surface as a misleading `PARSE` instead of the real failure — defeating the whole point of the message. The `message` is a **human-readable diagnostic** that makes the failure debuggable from the PCF without a native debugger: for a caught exception pass `error.localizedDescription`; for a validation failure a specific reason (e.g. `@\"missing required field 'uri'\"`); for `USER_CANCELLED` a short note. **Every error path calls this with BOTH a code and a message — never a bare code.**\n  - Success helper: emit `- (NSString *)successJsonWith:(NSDictionary *)result` that builds `{\"status\":\"ok\",\"result\":\u003Cresult>}` via `NSJSONSerialization`.\n  - **Error propagation — wrap the operation body so every failure reaches the PCF with a code AND a message.** Any framework\u002Fruntime failure must `resolve` with `errorJsonWithCode:message:` carrying a specific code and reason — never throw an uncaught Obj-C exception, crash, or `resolve` empty. Use `@try\u002F@catch` around risky synchronous work and resolve the `@catch` with `INTERNAL_ERROR` plus `exception.reason`.\n  - UI hygiene boilerplate for each presented `UIViewController`'s `viewDidLoad` (mirrors Android's insets handling — prevents the most common iOS issue: content under safe areas, status bar, home indicator):\n    ```objc\n    - (void)viewDidLoad {\n        [super viewDidLoad];\n        self.view.backgroundColor = [UIColor systemBackgroundColor];\n\n        \u002F\u002F Navigation bar with Done \u002F Cancel — equivalent to Android's MaterialToolbar.\n        UINavigationBar *navBar = [[UINavigationBar alloc] init];\n        navBar.translatesAutoresizingMaskIntoConstraints = NO;\n        UINavigationItem *navItem = [[UINavigationItem alloc] initWithTitle:@\"\u003CHuman-readable from PRD §2>\"];\n        navItem.leftBarButtonItem = [[UIBarButtonItem alloc]\n            initWithBarButtonSystemItem:UIBarButtonSystemItemCancel\n            target:self action:@selector(handleCancel)];\n        navItem.rightBarButtonItem = [[UIBarButtonItem alloc]\n            initWithBarButtonSystemItem:UIBarButtonSystemItemDone\n            target:self action:@selector(handleDone)];\n        navBar.items = @[navItem];\n        [self.view addSubview:navBar];\n\n        \u002F\u002F Content view — the operation-specific surface (e.g. PKCanvasView, AVCaptureVideoPreviewLayer host).\n        \u002F\u002F Constrain to safeAreaLayoutGuide so content doesn't extend under the notch \u002F home indicator.\n        UIView *contentView = [[UIView alloc] init];   \u002F\u002F Replace with operation-specific view per ARCHITECTURE §3.\u003Cn>\n        contentView.translatesAutoresizingMaskIntoConstraints = NO;\n        [self.view addSubview:contentView];\n\n        [NSLayoutConstraint activateConstraints:@[\n            [navBar.topAnchor constraintEqualToAnchor:self.view.safeAreaLayoutGuide.topAnchor],\n            [navBar.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor],\n            [navBar.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor],\n            [contentView.topAnchor constraintEqualToAnchor:navBar.bottomAnchor],\n            [contentView.leadingAnchor constraintEqualToAnchor:self.view.safeAreaLayoutGuide.leadingAnchor],\n            [contentView.trailingAnchor constraintEqualToAnchor:self.view.safeAreaLayoutGuide.trailingAnchor],\n            [contentView.bottomAnchor constraintEqualToAnchor:self.view.safeAreaLayoutGuide.bottomAnchor],\n        ]];\n    }\n\n    - (UIStatusBarStyle)preferredStatusBarStyle {\n        \u002F\u002F Adapt to system appearance — matches Android's windowLightStatusBar in light theme.\n        return UIStatusBarStyleDefault;   \u002F\u002F automatic light\u002Fdark per system\n    }\n    ```\n  - Modal helper: emit `- (UIViewController *)topViewController` if any operation presents modally:\n    ```objc\n    - (UIViewController *)topViewController {\n        UIViewController *root = UIApplication.sharedApplication.keyWindow.rootViewController;\n        while (root.presentedViewController) { root = root.presentedViewController; }\n        return root;\n    }\n    ```\n\n  **No TODO placeholders. No `\u002F\u002F implement this`.** If a §3.\u003Cn> block is incomplete (any \"Key APIs and decisions\" item is vague or missing), STOP with `NEEDS_CONTEXT: ARCHITECTURE §3.\u003Cn> implementation block is incomplete — re-run \u002Fdesign-native-extension-feature Step 7 (per-operation implementation walkthrough) to complete it`. Don't paper over a vague spec with a guess.\n\n### 3.5 Android (`android\u002F`)\n\nWrite:\n\n- **`android\u002Fbuild.gradle`** — **library-only** gradle config. The module is consumed by the host's managed build, which provides the root project setup. (For the standalone `.ppmplugin` build, `\u002Fbuild-android-binary` compiles from a throwaway staged copy with pinned versions — this canonical file is never edited; see [`shared\u002Fppmplugin-format.md §5`](..\u002F..\u002Fshared\u002Fppmplugin-format.md).) Do NOT emit a `buildscript { ... }`, `allprojects { ... }`, or any classpath declarations — those belong to the root project, not this library module.\n\n  Library-only shape (this is the entire file — no preamble, no root-project blocks):\n  ```gradle\n  \u002F\u002F \u003Ckebab>-control\n  \u002F\u002F Android library module — consumed by the host's managed build.\n\n  apply plugin: 'com.android.library'\n  apply plugin: 'kotlin-android'\n\n  def safeExtGet(prop, fallback) {\n      rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback\n  }\n\n  android {\n      namespace \"com.powerapps.\u003Clower>\"\n      compileSdkVersion safeExtGet('compileSdkVersion', 35)\n      defaultConfig {\n          minSdkVersion safeExtGet('minSdkVersion', \u003CPRD min — default 24>)\n          targetSdkVersion 35\n      }\n      compileOptions {\n          sourceCompatibility JavaVersion.VERSION_17\n          targetCompatibility JavaVersion.VERSION_17\n      }\n      kotlinOptions { jvmTarget = '17' }\n  }\n\n  dependencies {\n      \u002F\u002F 'react-android' (renamed from 'react-native' in RN 0.73). compileOnly + pinned:\n      \u002F\u002F the wrap shell provides RN at runtime, so never bundle it, and the legacy\n      \u002F\u002F 'react-native:+' coordinate does not resolve in the standalone build.\n      \u002F\u002F Read \u003CrnVersion> from package.json devDependencies (currently 0.79.7).\n      compileOnly \"com.facebook.react:react-android:\u003CrnVersion>\"\n      implementation 'androidx.appcompat:appcompat:1.6.1'\n      implementation 'androidx.core:core-ktx:1.12.0'                  \u002F\u002F WindowCompat \u002F WindowInsetsCompat for UI hygiene\n      implementation 'androidx.constraintlayout:constraintlayout:2.1.4' \u002F\u002F for the generated layout XML\n      implementation 'com.google.android.material:material:1.11.0'    \u002F\u002F Material 3 theme + components\n      \u002F\u002F Plus any ARCHITECTURE §1.3 \u002F §1.4-specified additions (e.g. ML Kit, FusedLocationProvider)\n  }\n  ```\n\n- **Files NOT to generate** (these are root-project \u002F standalone-build concerns; the host's managed build — or, for the `.ppmplugin`, `\u002Fbuild-android-binary`'s staged copy — owns them):\n  - `android\u002Fsettings.gradle` — root project's responsibility\n  - `android\u002Fgradle.properties` — root project's properties; `android.useAndroidX` and `android.enableJetifier` are supplied ambiently by the host (and generated into the staged copy by `\u002Fbuild-android-binary`), not by the library\n  - `android\u002Fgradlew` + `android\u002Fgradle\u002Fwrapper\u002F*` — the Gradle wrapper; library modules don't need their own wrapper\n  - Any top-level `buildscript { ext, repositories, dependencies (classpath) }` block in `build.gradle` — the host provides AGP + Kotlin classpaths\n\n  > **No standalone build script.** The `android\u002F` directory is consumed by the host's managed build (and copied into a pinned staging dir by `\u002Fbuild-android-binary` for the `.ppmplugin`); it doesn't have to compile in isolation. Don't add a top-level `buildscript { ... }` \u002F `allprojects { ... }` block — the host provides those. Standalone `.\u002Fgradlew assembleDebug` against this directory is **not** a validation path we support (native compile happens in `\u002Fbuild-android-binary`, not here — see [`shared\u002Fppmplugin-format.md §5`](..\u002F..\u002Fshared\u002Fppmplugin-format.md)).\n\n- **`android\u002Fsrc\u002Fmain\u002FAndroidManifest.xml`** — registers permissions from ARCHITECTURE §1.4 AND the dedicated capture Activity (if ARCHITECTURE §3.\u003Cn> hosts in one) with a Material 3 theme:\n  ```xml\n  \u003Cmanifest xmlns:android=\"http:\u002F\u002Fschemas.android.com\u002Fapk\u002Fres\u002Fandroid\" package=\"com.powerapps.\u003Clower>\">\n      \u003C!-- One \u003Cuses-permission android:name=\"...\" \u002F> per entry in ARCHITECTURE §1.4 Android permissions -->\n\n      \u003Capplication>\n          \u003C!-- One \u003Cactivity> per ARCHITECTURE §3.\u003Cn> that hosts in a dedicated Activity.\n               Theme references generated themes.xml; screenOrientation per ARCHITECTURE §3.\u003Cn>. -->\n          \u003Cactivity\n              android:name=\".\u003CPascal>CaptureActivity\"\n              android:theme=\"@style\u002FTheme.\u003CPascal>\"\n              android:screenOrientation=\"portrait\"\n              android:exported=\"false\" \u002F>\n      \u003C\u002Fapplication>\n  \u003C\u002Fmanifest>\n  ```\n\n- **`android\u002Fsrc\u002Fmain\u002Fres\u002Fvalues\u002Fthemes.xml`** — Material 3 theme so all components render with proper Material styling, not the bare AppCompat defaults. Without this, generated UIs hit issues like status bar overlap and unthemed buttons.\n  ```xml\n  \u003C?xml version=\"1.0\" encoding=\"utf-8\"?>\n  \u003Cresources xmlns:tools=\"http:\u002F\u002Fschemas.android.com\u002Ftools\">\n      \u003Cstyle name=\"Theme.\u003CPascal>\" parent=\"Theme.Material3.DayNight.NoActionBar\">\n          \u003C!-- System bars: drawn by the OS but content extends behind them; the Activity applies insets. -->\n          \u003Citem name=\"android:statusBarColor\">@android:color\u002Ftransparent\u003C\u002Fitem>\n          \u003Citem name=\"android:navigationBarColor\">@android:color\u002Ftransparent\u003C\u002Fitem>\n          \u003Citem name=\"android:windowLightStatusBar\" tools:targetApi=\"m\">true\u003C\u002Fitem>\n          \u003Citem name=\"android:windowLightNavigationBar\" tools:targetApi=\"o_mr1\">true\u003C\u002Fitem>\n      \u003C\u002Fstyle>\n  \u003C\u002Fresources>\n  ```\n\n- **`android\u002Fsrc\u002Fmain\u002Fres\u002Flayout\u002Factivity_\u003Clower>_capture.xml`** — root layout uses Material components. Toolbar at top, bounded content area in a `MaterialCardView` (drawing surface, camera preview, etc.):\n  ```xml\n  \u003C?xml version=\"1.0\" encoding=\"utf-8\"?>\n  \u003Candroidx.constraintlayout.widget.ConstraintLayout\n      xmlns:android=\"http:\u002F\u002Fschemas.android.com\u002Fapk\u002Fres\u002Fandroid\"\n      xmlns:app=\"http:\u002F\u002Fschemas.android.com\u002Fapk\u002Fres\u002Fauto\"\n      android:id=\"@+id\u002Froot\"\n      android:layout_width=\"match_parent\"\n      android:layout_height=\"match_parent\"\n      android:background=\"?attr\u002FcolorSurface\">\n\n      \u003Ccom.google.android.material.appbar.MaterialToolbar\n          android:id=\"@+id\u002Ftoolbar\"\n          android:layout_width=\"match_parent\"\n          android:layout_height=\"?attr\u002FactionBarSize\"\n          android:elevation=\"4dp\"\n          app:layout_constraintTop_toTopOf=\"parent\"\n          app:menu=\"@menu\u002F\u003Clower>_capture_menu\"\n          app:navigationIcon=\"@drawable\u002Fic_close\"\n          app:title=\"\u003CHuman-readable name from PRD §2>\" \u002F>\n\n      \u003Ccom.google.android.material.card.MaterialCardView\n          android:id=\"@+id\u002Fcontent_card\"\n          android:layout_width=\"0dp\"\n          android:layout_height=\"0dp\"\n          android:layout_margin=\"16dp\"\n          app:cardCornerRadius=\"8dp\"\n          app:cardElevation=\"2dp\"\n          app:layout_constraintTop_toBottomOf=\"@id\u002Ftoolbar\"\n          app:layout_constraintBottom_toBottomOf=\"parent\"\n          app:layout_constraintStart_toStartOf=\"parent\"\n          app:layout_constraintEnd_toEndOf=\"parent\">\n\n          \u003C!-- The operation-specific surface goes here: drawing View, camera SurfaceView,\n               photo preview, etc. — substituted per ARCHITECTURE §3.\u003Cn>'s \"Hosting\" specification. -->\n          \u003CView\n              android:id=\"@+id\u002Fcapture_surface\"\n              android:layout_width=\"match_parent\"\n              android:layout_height=\"match_parent\"\n              android:background=\"?attr\u002FcolorSurfaceContainerLowest\" \u002F>\n      \u003C\u002Fcom.google.android.material.card.MaterialCardView>\n  \u003C\u002Fandroidx.constraintlayout.widget.ConstraintLayout>\n  ```\n\n- **`android\u002Fsrc\u002Fmain\u002Fres\u002Fmenu\u002F\u003Clower>_capture_menu.xml`** — toolbar action items. **`action_done` is MANDATORY** for any capture-flow operation; without it the user has no way to submit. Additional actions (Clear, Undo, etc.) per ARCHITECTURE §3.\u003Cn>'s UI actions:\n  ```xml\n  \u003Cmenu xmlns:android=\"http:\u002F\u002Fschemas.android.com\u002Fapk\u002Fres\u002Fandroid\"\n        xmlns:app=\"http:\u002F\u002Fschemas.android.com\u002Fapk\u002Fres\u002Fauto\">\n      \u003C!-- MANDATORY for capture flows. NEVER omit Done — user cannot complete the operation otherwise. -->\n      \u003Citem\n          android:id=\"@+id\u002Faction_done\"\n          android:title=\"@string\u002Faction_done\"\n          app:showAsAction=\"always\" \u002F>\n\n      \u003C!-- Optional: one \u003Citem> per additional toolbar action declared in ARCHITECTURE §3.\u003Cn>\n           (e.g. Clear All, Undo). Set app:showAsAction=\"ifRoom\" for non-critical actions. -->\n  \u003C\u002Fmenu>\n  ```\n\n- **For multi-mode capture operations (pen\u002Feraser, photo\u002Fvideo, etc.):** the toolbar \u002F mode-selection row uses `MaterialButtonToggleGroup`, not plain `Button`s. Toggle group provides the active-state visual feedback the user needs to know which mode is currently selected. Example layout fragment to include in `activity_\u003Clower>_capture.xml`:\n  ```xml\n  \u003C!-- Insert into the toolbar or just below it, when ARCHITECTURE §3.\u003Cn> has multiple modes. -->\n  \u003Ccom.google.android.material.button.MaterialButtonToggleGroup\n      android:id=\"@+id\u002Fmode_toggle_group\"\n      android:layout_width=\"wrap_content\"\n      android:layout_height=\"wrap_content\"\n      app:singleSelection=\"true\"\n      app:selectionRequired=\"true\">\n\n      \u003C!-- One \u003CButton style=\"?attr\u002FmaterialButtonOutlinedStyle\"> per mode in ARCHITECTURE §3.\u003Cn>.\n           Example for pen\u002Feraser\u002Fclear: -->\n      \u003CButton android:id=\"@+id\u002Fmode_pen\"     android:text=\"@string\u002Fmode_pen\"     style=\"?attr\u002FmaterialButtonOutlinedStyle\" \u002F>\n      \u003CButton android:id=\"@+id\u002Fmode_eraser\"  android:text=\"@string\u002Fmode_eraser\"  style=\"?attr\u002FmaterialButtonOutlinedStyle\" \u002F>\n  \u003C\u002Fcom.google.android.material.button.MaterialButtonToggleGroup>\n  ```\n  And wire the listener in the Activity's `onCreate`:\n  ```kotlin\n  val toggleGroup: MaterialButtonToggleGroup = findViewById(R.id.mode_toggle_group)\n  toggleGroup.check(R.id.mode_pen)   \u002F\u002F default\n  toggleGroup.addOnButtonCheckedListener { _, checkedId, isChecked ->\n      if (!isChecked) return@addOnButtonCheckedListener\n      when (checkedId) {\n          R.id.mode_pen -> captureSurface.setMode(\u003CPascal>Mode.PEN)\n          R.id.mode_eraser -> captureSurface.setMode(\u003CPascal>Mode.ERASER)\n      }\n  }\n  ```\n  Without this, the user sees a row of identical-looking buttons and has no idea which mode is active. Confirmed UX-blocking failure mode in v0 extensions.\n\n- **`android\u002Fsrc\u002Fmain\u002Fres\u002Fvalues\u002Fstrings.xml`** — string resources for the menu items + content descriptions (accessibility):\n  ```xml\n  \u003Cresources>\n      \u003Cstring name=\"action_done\">Done\u003C\u002Fstring>\n      \u003C!-- Plus one entry per ARCHITECTURE §3.\u003Cn> action; one content-description per accessible element. -->\n  \u003C\u002Fresources>\n  ```\n\n- **`android\u002Fsrc\u002Fmain\u002Fjava\u002Fcom\u002Fpowerapps\u002F\u003Clower>\u002F\u003CPascal>Module.kt`** — Kotlin native module. **Generate complete working code, not TODO placeholders.** For each operation, the per-operation §3.\u003Cn> block's \"Android implementation\" sub-section prescribes every implementation decision. Generate the implementation verbatim from §3.\u003Cn>:\n  - Class: extends `ReactContextBaseJavaModule`.\n  - `getName()` returns `\"\u003CPascal>Module\"` — the **`Module`-suffixed** name (matches `NativeModules.\u003CPascal>Module` on JS side, the iOS `+moduleName` return value, and the manifest's `receivers[].nativeModule`). Do NOT strip the suffix — it's the reserved-name dodge (see [`shared\u002Fppmplugin-format.md §4`](..\u002F..\u002Fshared\u002Fppmplugin-format.md)).\n  - For each operation, write a `@ReactMethod` function taking **exactly one `ReadableMap request` parameter** followed by `Promise promise` — e.g. `@ReactMethod fun capturePenInput(request: ReadableMap, promise: Promise)`. This matches the wrap dispatch contract: the PCF sends `args: [request]` (a one-element array) and the proxy does `fn.apply(mod, [request])`, so the method receives the request object as its single positional param ([`ppmplugin-format §2`](..\u002F..\u002Fshared\u002Fppmplugin-format.md)). Read each field off `request` (`request.getString(\"…\")`, `request.getInt(\"…\")`, etc.); do NOT expand the request into multiple positional params. The body implements §3.\u003Cn>'s Android spec **completely**:\n    - The hosting (dedicated `Activity` via `Intent`, or `Fragment`, or in-place — whatever §3.\u003Cn> specifies)\n    - The key API calls in the order §3.\u003Cn> specifies (e.g. `View.onTouchEvent` registration; `Path` accumulation; stylus pressure handling)\n    - Each Done\u002FCancel handler as §3.\u003Cn> specifies\n    - The export step as §3.\u003Cn>'s \"Export\" line specifies (e.g. render to `Bitmap`, compress to PNG, base64-encode)\n    - Each edge case from §3.\u003Cn>'s \"Edge cases handled\" list, with the exact behavior named\n  - If §3.\u003Cn> requires a dedicated `Activity`, emit it as a separate `.kt` file under the same package (e.g. `\u003CPascal>CaptureActivity.kt`) and register it in `AndroidManifest.xml`. The Activity's `onCreate` MUST emit the following UI hygiene boilerplate so the generated UI doesn't suffer from status bar overlap, missing Material theming, or rotation issues (these were repeat issues in v0 extensions):\n    ```kotlin\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        \u002F\u002F Edge-to-edge layout; we apply system-bar padding ourselves below.\n        WindowCompat.setDecorFitsSystemWindows(window, false)\n        setContentView(R.layout.activity_\u003Clower>_capture)\n\n        \u002F\u002F Pad root by status\u002Fnav bar insets so toolbar doesn't sit UNDER the status bar.\n        \u002F\u002F This is the fix for the most common Android UI bug in PAM extensions:\n        \u002F\u002F \"buttons overlapping with system clock \u002F status icons\".\n        ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.root)) { v, insets ->\n            val bars = insets.getInsets(WindowInsetsCompat.Type.systemBars())\n            v.setPadding(bars.left, bars.top, bars.right, bars.bottom)\n            WindowInsetsCompat.CONSUMED\n        }\n\n        \u002F\u002F Toolbar with Done\u002FCancel via Material menu.\n        val toolbar: MaterialToolbar = findViewById(R.id.toolbar)\n        setSupportActionBar(toolbar)\n        toolbar.setNavigationOnClickListener { onCancelled() }   \u002F\u002F navigation icon = Cancel\n\n        \u002F\u002F Wire up the operation-specific surface (drawing View, camera preview, etc.)\n        \u002F\u002F — per ARCHITECTURE §3.\u003Cn>'s \"Hosting\" + \"Key APIs and decisions\" specification.\n        val captureSurface: \u003CPRD-§3.\u003Cn>-View-class> = findViewById(R.id.capture_surface)\n        \u002F\u002F ... operation-specific setup per §3.\u003Cn> ...\n    }\n\n    override fun onCreateOptionsMenu(menu: Menu): Boolean {\n        menuInflater.inflate(R.menu.\u003Clower>_capture_menu, menu)\n        return true\n    }\n\n    override fun onOptionsItemSelected(item: MenuItem): Boolean {\n        return when (item.itemId) {\n            R.id.action_done -> { onDone(); true }\n            \u002F\u002F Plus one branch per additional toolbar action declared in §3.\u003Cn>.\n            else -> super.onOptionsItemSelected(item)\n        }\n    }\n    ```\n    Required imports: `androidx.core.view.WindowCompat`, `androidx.core.view.ViewCompat`, `androidx.core.view.WindowInsetsCompat`, `com.google.android.material.appbar.MaterialToolbar`.\n  - **Constructor \u002F `init{}` safety — the module is built eagerly at bridge startup on a possibly Looper-less thread, so construction MUST NOT throw** ([`ppmplugin-format §5`](..\u002F..\u002Fshared\u002Fppmplugin-format.md)). Keep the constructor cheap: do NOT register system callbacks, acquire camera\u002Fsensor\u002Flocation managers, or do file I\u002FO in the constructor \u002F `init{}` — **defer them to the first `@ReactMethod` call (lazy init)**. If a listener genuinely must be registered at construction, pass an **explicit `Handler(Looper.getMainLooper())` — never `null`** (a `null` or implicit `Looper` throws `Can't create handler inside thread that has not called Looper.prepare()`), and wrap it in try\u002Fcatch so a subsystem hiccup leaves the module in a safe default state instead of crashing the host at launch. Example: `private val mainHandler = Handler(Looper.getMainLooper())`, then `try { cameraManager?.registerTorchCallback(cb, mainHandler) } catch (e: Exception) { \u002F* best-effort *\u002F }`. Mirror the same discipline in `invalidate()` (unregister in try\u002Fcatch).\n  - **Runtime permissions (dangerous perms only).** For any op whose ARCHITECTURE §1.4 lists a *dangerous* Android permission (`CAMERA`, `RECORD_AUDIO`, `ACCESS_FINE\u002FCOARSE_LOCATION`, …), the `@ReactMethod` MUST check `ContextCompat.checkSelfPermission(...)` before calling the API and, if not granted, `promise.resolve(errorJson(\"PERMISSION_DENIED\", \"\u003Cpermission> not granted\"))` (a declared §5 code) rather than let the framework throw `SecurityException`. Declaring it in `AndroidManifest.xml` is necessary but **not** sufficient — API 23+ requires the runtime grant.\n  - **`currentActivity` nullability.** Any method that presents UI \u002F starts an Activity MUST guard `val activity = currentActivity ?: return promise.resolve(errorJson(\"NO_ACTIVITY\", \"no foreground activity\"))` — `currentActivity` is `null` when the app is backgrounded, and dereferencing it NPE-crashes the host.\n  - Threading: heavy work in a coroutine (`CoroutineScope(Dispatchers.IO).launch { ... }`) or `Thread { ... }.start()`; UI work back via `Handler(Looper.getMainLooper()).post { ... }` or `withContext(Dispatchers.Main)`.\n  - Response JSON: must mirror iOS exactly. Both platforms must produce byte-identical JSON for the same input — this is non-negotiable, scaffold MUST verify by visual inspection of the two implementations after writing them.\n  - Helper `private fun errorJson(code: String, message: String): String` building `{\"status\":\"error\",\"error\":\"\u003Ccode>\",\"message\":\"\u003Cmessage>\"}` via **`JSONObject`** (the same serializer as `successJson` — `JSONObject().put(\"status\",\"error\").put(\"error\",code).put(\"message\",message).toString()`). Do **NOT** build it with string interpolation: a `message` containing a `\"`, `\\`, or newline would emit invalid JSON, which the PCF's response parse would surface as a misleading `PARSE` instead of the real failure. The `message` is a **human-readable diagnostic**: for a caught exception pass `e.message ?: e.toString()`; for a validation failure a specific reason; for `USER_CANCELLED` a short note. This MUST mirror iOS byte-for-byte. **Every error path calls this with BOTH a code and a message.**\n  - Helper `private fun successJson(result: Map\u003CString, Any?>): String` building `{\"status\":\"ok\",\"result\":\u003Cjson>}` via `JSONObject`.\n  - **Error propagation — every failure resolves the Promise with `errorJson(code, message)`, never a thrown\u002Funcaught exception.** Wrap the operation body (and any coroutine\u002FThread) in try\u002Fcatch; on catch, `promise.resolve(errorJson(\"INTERNAL_ERROR\", e.message ?: e.toString()))`. A coroutine that throws without catching, or a path that never resolves the Promise, leaves the maker with a hung control and no code\u002Fmessage.\n\n  **No TODO placeholders.** If a §3.\u003Cn> block is incomplete, STOP with the same `NEEDS_CONTEXT:` error as iOS — don't write Android code from a vague spec.\n\n- **`android\u002Fsrc\u002Fmain\u002Fjava\u002Fcom\u002Fpowerapps\u002F\u003Clower>\u002F\u003CPascal>Package.kt`** — `ReactPackage` implementation that registers `\u003CPascal>Module`. **It MUST have a public no-arg constructor** — the wrap runtime instantiates it via `loadClass(packageClass).getDeclaredConstructor().newInstance()`, so a `ReactPackage` with only an arg-ed constructor throws `NoSuchMethodException` and the plugin silently fails to load ([`ppmplugin-format §5`](..\u002F..\u002Fshared\u002Fppmplugin-format.md)). The form below is correct — Kotlin gives `class \u003CPascal>Package : ReactPackage` an implicit no-arg constructor. **Do NOT add a constructor with parameters to the package class** (the *module* takes `reactContext` via `createNativeModules` — that's fine; the *package* must stay no-arg):\n  ```kotlin\n  class \u003CPascal>Package : ReactPackage {   \u002F\u002F no-arg constructor required by the wrap runtime\n      override fun createNativeModules(reactContext: ReactApplicationContext) =\n          listOf\u003CNativeModule>(\u003CPascal>Module(reactContext))\n      override fun createViewManagers(reactContext: ReactApplicationContext) =\n          emptyList\u003CViewManager\u003C*, *>>()\n  }\n  ```\n\n### 3.6 Tests — none generated in this track\n\nThere is **no `tests\u002F` directory** to write. The first-party SDK track validates conformance with TypeScript type-level fixtures (`*.test-d.ts` + `tsc --noEmit`) against the `INativeExtension` interface — but **that layer does not exist here** (§3.3), so there is nothing to type-check.\n\nThe third-party-control analogue is **structural contract verification done by [`\u002Ftest-native-extension`](..\u002Ftest-native-extension\u002FSKILL.md)**, not generated source: it greps the native modules' request\u002Fresponse\u002Ferror usage against each other and against the manifest (the Android `getName()` ↔ iOS `+moduleName` ↔ `receivers[].nativeModule` agreement, the cross-platform JSON parity). That runs as a separate skill on the working tree — this skill does not emit test files for it. **No Jest, no `tsconfig`, no fixtures.**\n\n### 3.7 State file\n\nWrite **`.extension-state.md`** using the template in [`shared\u002Frepo-layout.md`](..\u002F..\u002Fshared\u002Frepo-layout.md) §\"`.extension-state.md` template\". Fill:\n\n- **Identity** block — from PRD §2 plus the derived names (Capability, Class, Native module = `\u003CPascal>Module`, Android package, PRD path).\n- **Phase**: `Last completed: scaffold` \u002F `Next: \u002Ftest-native-extension` \u002F `Status: ok`.\n- **`## ppmplugin (third-party controls)`** block — leave at its defaults (`Target: unset`, `Manifest: not written`, `Android DEX: not built`, `iOS framework: not built`, `Bundle: not assembled`). The `\u002Fgenerate-ppmplugin` build skills fill it.\n\nIf a `.extension-state.md` already existed and we agreed to \"resume\" in Step 1.7, only update the **Phase** block (leave `## ppmplugin` untouched — it captures any later build state).\n\n---\n\n## Step 3.8 — Self-critique against the proactive protocol\n\n> **This step exists because the smoke check (Step 4) is structural only — it compiles nothing. Generated Kotlin and Obj-C don't compile-check here; they compile later in `\u002Fbuild-android-binary` (Gradle) and `\u002Fbuild-ios-binary` (xcodebuild). Runtime UI and UX bugs (toolbar overflows, dead-end modals, missing exit paths, asymmetric platform features) don't surface in *any* compile — only on a real device, often after the engineer has lost an afternoon. The self-critique protocol forces the skill to reason about its own output *before* declaring done, gate-by-gate, instead of trusting \"looks fine.\"**\n\nThe protocol lives at [`shared\u002Fself-critique-protocol.md`](..\u002F..\u002Fshared\u002Fself-critique-protocol.md). It is **proactive reasoning, not pattern matching** — each gate enumerates a category of thing (PRD-listed behaviors, user-reachable controls, mutually-exclusive modes, etc.) and forces specific questions about each enumerated item. Read the protocol file in full before running this step; what follows here is the integration contract.\n\n### 3.8.1 Re-read every file emitted in this run\n\nFresh-read from disk. Files of interest:\n\n- `android\u002Fsrc\u002Fmain\u002Fjava\u002Fcom\u002Fpowerapps\u002F\u003Clower>\u002F\u003CPascal>Module.kt` (its `@ReactMethod` request\u002Fresponse\u002Ferror surface — this IS the dispatch contract, since there is no TS layer)\n- `android\u002Fsrc\u002Fmain\u002Fjava\u002Fcom\u002Fpowerapps\u002F\u003Clower>\u002F\u003CPascal>CaptureActivity.kt` and any other Activities\n- `android\u002Fsrc\u002Fmain\u002Fres\u002F**\u002F*.xml` (layouts, menus, themes, manifest)\n- `ios\u002FRCT\u003CPascal>Module.{h,m}` (or `.swift` if ARCHITECTURE §1.2 chose Swift)\n- `ios\u002F*.podspec`\n- `pcf\u002F\u003CPascal>PCF\u002Findex.ts` and `ControlManifest.Input.xml` (if PCF was part of this run)\n\n### 3.8.2 Walk the protocol gates in order\n\nFor each gate in the protocol — **PRD coverage**, **User journey**, **Layout feasibility**, **State coverage**, **Cross-platform parity**, **Reversibility**, **Asymptotic \u002F lifecycle**, **Spec-drift**, **Runtime safety & lifecycle**, **Buildability & bundle-fit feasibility**, **PCF ↔ native round-trip contract** — execute the gate's \"enumerate + ask\" procedure as described in the protocol file.\n\n**Gate 10 (Buildability) is not optional for this track.** It re-runs, over the *generated code*, the same reasoning `\u002Fdesign-native-extension-feature` Step 7.5 ran over the design — so a dependency, return shape, or construction cost that slipped past design (or was introduced during codegen) is caught here, before `\u002Fbuild-android-binary` \u002F `\u002Fbuild-ios-binary` fail minutes later. **Gate 11 (PCF round-trip)** runs only when a PCF was emitted in this run; when the PCF is generated separately, `\u002Fgenerate-pcf-companion` Step 5.7 runs it instead.\n\n**Do not short-circuit.** Each gate enumerates real items (PRD bullet points, user-reachable controls, horizontal container children, etc.) and answers a concrete question per item. Skipping a gate because \"intuitively the code looks fine\" defeats the purpose — the whole point is to force structured analysis where past intuition has failed.\n\n### 3.8.3 Build the self-critique report\n\nAggregate findings across all gates. Each finding follows the protocol's structure:\n\n```\n- Gate: \u003Cname>\n- File: \u003Cpath>:\u003Cline range>\n- Severity: blocker | concern\n- Problem: \u003Cone sentence>\n- Why this is wrong: \u003Cone sentence anchored in the gate's principle>\n- Suggested fix: \u003Cwhat to change>\n- Autofix: applied | proposed | requires human review\n```\n\n### 3.8.4 Apply auto-fixes inline\n\nFor generation context (this skill), the user has no intermediate review checkpoint between code-emit and Step 4 smoke check. The protocol's auto-fix policy translates to:\n\n- **`autofix: applied`** (mechanical fix, no design choice) → apply silently, log to `.extension-state.md` under `Auto-fixes`.\n- **`autofix: proposed`** (structural fix, unambiguous) → apply, surface in the final summary with a one-line note. The user can read `.extension-state.md` to see the diff against their expected output.\n- **`autofix: requires human review`** (design judgment) → do NOT modify code. Promote to a top-level `NEEDS_CONTEXT` item in the final summary.\n\n(Contrast with `\u002Fdebug-extension` Step 7.5, which gates `proposed` fixes on explicit user confirm because it sits inside an interactive review cadence.)\n\n### 3.8.5 Re-loop after fixes\n\nIf 3.8.4 applied any fixes, repeat from 3.8.1. A fix at one gate can either unblock or surface a new finding elsewhere. Cap the loop at 3 iterations; if findings remain after iteration 3, surface them as concerns regardless of severity.\n\n### 3.8.6 Return-status impact\n\n- All gates clean OR fixed → continue to Step 4. Run can return `DONE`.\n- Concerns remain (acceptable per gate severity rules) → run returns `DONE_WITH_CONCERNS: \u003Cone-line summary>`. Log every concern in `.extension-state.md`.\n- Any **blocker** remains → run returns `BLOCKED: self-critique found \u003Clist>`. Surface the report. The scaffold is not shippable until the user resolves blockers.\n\n### What this step specifically catches\n\nThe protocol's gates are derived from first principles, not from a list of past failures. By construction, it should catch:\n\n| Gate | Bug class |\n|---|---|\n| PRD coverage (Gate 1) | Behaviors promised in ARCHITECTURE §3.\u003Cn> not implemented in code (e.g. \"Done button is anchored right\" → no Done button at all). |\n| User journey (Gate 2) | Modals with no exit, controls that don't respond, error paths that never resolve the Promise. |\n| Layout feasibility (Gate 3) | Toolbar overflows on small screens, controls pushed off-edge, primary actions unreachable at narrow widths or high font scales. |\n| State coverage (Gate 4) | Mode toggles without visible active state, error codes returned but not displayed, boolean toggles without observable UI. |\n| Cross-platform parity (Gate 5) | iOS got 5 features, Android got 3 — with no PRD note explaining why. |\n| Reversibility (Gate 6) | Destructive actions without confirmation, modals with one entry and no exit, multi-step flows with no back. |\n| Asymptotic \u002F lifecycle (Gate 7) | Rotation loses state but PRD says it shouldn't; primary action not idempotent under fast double-tap. |\n| Spec-drift (Gate 8) | Code does things PRD doesn't document (drift the other direction from Gate 1). |\n| Runtime safety & lifecycle (Gate 9) | Looper-less constructor `Handler` crashes the host at launch; a Promise path that never settles hangs the caller; an acquired listener\u002Fcamera never released. |\n| Buildability & bundle-fit (Gate 10) | A dependency that needs `compileSdk > 34` \u002F AGP `8.x`, an iOS framework that can't ship flat, a non-JSON-serializable return, or a streaming pattern with no channel — caught before the toolchain fails. |\n| PCF ↔ native round-trip (Gate 11) | PCF composite key ≠ manifest receiver, `cordova.exec` instead of `sendAsync`, a pre-stringified envelope, a bare `args` object, or a success path that doesn't unwrap the wrap `message` container — all silent-on-device otherwise. |\n\nWhen a new failure mode is observed, the question to ask is **\"which gate should have caught this?\"** If the answer is \"none of them\" — that's a signal the protocol needs a new gate (or an existing gate's enumeration list needs expanding). Do NOT add a one-off pattern check that doesn't generalize.\n\nThis step is LLM reasoning over generated source. It is not a real compiler. The protocol is structured enough to force enumeration (and stop \"looks fine\" from being a valid answer), but it depends on the reasoning at each gate actually being careful. Pair with `\u002Fbuild-android-binary` \u002F `\u002Fbuild-ios-binary` (real compile) and on-device verification for runtime errors the protocol can't simulate.\n\n---\n\n## Step 4 — Run a smoke check\n\nThere is **no TypeScript layer to compile** in this track, so the smoke check does NOT run `tsc`. Native code is compiled later by `\u002Fbuild-android-binary` and `\u002Fbuild-ios-binary` — not here. The smoke check is a fast **structural** pass (each is a few seconds):\n\n**OS-neutral:** the `pnpm install` below is a real, cross-platform command; the symbol-agreement check is **read+parse — run it with the built-in Grep tool**, not shell `grep` (the bash is illustrative; `grep -R` isn't on Windows).\n\n```bash\n# 1. (Optional) seed the dev-only devDeps from the public npm registry. Skip if Node\u002Fpnpm absent.\npnpm install   # resolves react + react-native (RN headers + react-android coordinate) for the build skills\n\n# 2. Structural self-check (no compile) — use the Grep tool, not shell grep:\n#    confirm +moduleName returns \u003CPascal>Module in ios\u002F AND getName() == \"\u003CPascal>Module\" in android\u002F.\ngrep -R '+ (NSString *)moduleName' ios\u002F && grep -R 'return @\"\u003CPascal>Module\"' ios\u002F  # iOS +moduleName == \u003CPascal>Module\ngrep -R 'getName()' android\u002F | grep '\"\u003CPascal>Module\"'                       # Android getName() == \u003CPascal>Module\n```\n\nThe `pnpm install` here pulls **only from the public npm registry** and requires no organization-specific package-feed authentication. It's optional: if it fails or Node\u002Fpnpm are absent, note it and continue (the build skills will seed devDeps when they run).\n\n**Ordering precondition:** ensure `.gitignore` is on disk first (it is, per §3.1) — `pnpm install` populates `node_modules\u002F`, and the `.gitignore` keeps it (and `ppmplugin\u002F`) out of the repo when *you* later commit. This skill doesn't stage or commit anything, but a missing `.gitignore` would leave `node_modules\u002F` showing as untracked. If §3.1 was skipped (e.g. resumed mid-flow), re-run from §3.1 before the smoke check.\n\nIf a step fails:\n- For `pnpm install` failures: this is non-fatal here (only the build skills truly need the devDeps). Print the failing line, note `devDeps seed deferred`, and continue.\n- For the symbol-agreement greps: a mismatch means iOS `+moduleName`, Android `getName()`, and the intended manifest `receivers[].nativeModule` have drifted. Fix so all three read `\u003CPascal>Module` and re-run. This is a real gate — the wrap host can't dispatch if they disagree.\n\nDO NOT mark the scaffold as complete in `.extension-state.md` if the symbol-agreement check fails. Set `Status: blocked` with the failure reason.\n\n---\n\n## Step 5 — Summary\n\nPrint:\n\n```\nScaffold complete\n─────────────────\nDirectory: \u003Ccwd> (\u003CN> files written — git untouched; commit when you're ready)\nNative module: \u003CPascal>Module  (== .\u002Fmanifest.json receivers[].nativeModule — both authored here, in sync)\nDispatch contract: .\u002Fmanifest.json written (committed source of truth; PCF + build stage read it)\nSmoke check: devDeps seed \u003C✓ | deferred> | native symbol agreement ✓\nState file: .\u002F.extension-state.md → Phase: scaffold \u002F Next: \u002Ftest-native-extension\n\nNext steps\n──────────\n1. Review the generated implementations in ios\u002FRCT\u003CPascal>Module.m and android\u002F...\u002F\u003CPascal>Module.kt against ARCHITECTURE §3.\u003Cn>. Customize cosmetic details (button styling, modal chrome) if you want — but the framework wiring should be working out of the box.\n2. Run \u002Ftest-native-extension for the structural contract pre-flight (native module shape, Android getName ↔ iOS +moduleName ↔ .\u002Fmanifest.json agreement, cross-platform JSON parity). Native iOS \u002F Android compile is NOT validated here — it happens in \u002Fbuild-android-binary and \u002Fbuild-ios-binary.\n3. Run \u002Fgenerate-pcf-companion to generate the Companion PCF under pcf\u002F — it reads .\u002Fmanifest.json for the composite key so the PCF and module stay aligned.\n4. When ready to ship, run \u002Fgenerate-ppmplugin — it validates + stages .\u002Fmanifest.json, compiles the native binaries, and assembles the .ppmplugin bundle for upload to Dataverse.\n```\n\n---\n\n## Step 6 — Offer next-step skills\n\nPer `shared\u002Fshared-instructions.md §9.1`, use `AskUserQuestion` with all plausible next skills as options (not a Yes\u002FNo), and include an escape-hatch option.\n\n```\nQuestion: \"What would you like to do next?\"\nHeader:   \"Next step\"\n\nOptions:\n  1. \"Run \u002Fgenerate-pcf-companion\"\n     description: \"Next major scaffold step in the build flow. Runs `pac pcf init` under pcf\u002F and writes the Companion PCF that dispatches on the composite key \u003Cname>\u002F\u003Creceiver> read from .\u002Fmanifest.json (authored here), cross-checked against ARCHITECTURE §4 + §8 + §9.\"\n  2. \"Run \u002Ftest-native-extension\"\n     description: \"Structural contract pre-flight (native module shape, Android getName ↔ iOS +moduleName ↔ .\u002Fmanifest.json agreement, cross-platform JSON parity). Quick sanity check before adding more code. No compile.\"\n  3. \"Run \u002Fgenerate-ppmplugin\"\n     description: \"Build the deliverable: validates + stages .\u002Fmanifest.json, compiles the native binaries (DEX \u002F iOS framework), and assembles the .ppmplugin bundle. Usually run after the PCF exists and the contract is settled.\"\n  4. \"Stay — I'll review the generated code first\"\n     description: \"Skill exits. Run git diff to inspect the scaffold and decide what to run next yourself.\"\n```\n\n**For options 1, 2, or 3:** **invoke that skill via the `Skill` tool in the same turn** — selecting the option IS the request to run it (Execute, don't describe — shared-instructions §9.1 HARD RULE). Do NOT stop and tell the user to run it themselves. The invoked skill runs its own Step 1 prereq check + gates, so just hand off to it.\n\n**For option 4 (stay):** print one line: `Scaffold complete. Run any of the suggested skills when you're ready.` Then proceed to return-status.\n\n---\n\n## Return-status protocol\n\nThe literal first line of your final message MUST be one of:\n\n| Code | Meaning |\n|---|---|\n| `DONE` | All files written, smoke check (native symbol agreement) passed, state file updated. Next: `\u002Ftest-native-extension`. |\n| `DONE_WITH_CONCERNS: \u003Clist>` | Files written but the smoke check raised non-fatal warnings (e.g. devDeps seed deferred), OR a `\u002F\u002F TODO` count higher than expected (PRD operations that are particularly complex), OR a derived-name collision was deferred to the user. |\n| `NEEDS_CONTEXT: \u003Cmissing>` | A required PRD section was incomplete; couldn't proceed without re-running `\u002Fdesign-native-extension-feature` on that section. |\n| `BLOCKED: \u003Creason>` | Prereq failed (`git` missing), a reserved-name \u002F generic-noun collision on the native module symbol could not be resolved, or the native symbol-agreement check failed and could not be auto-fixed. |\n\nAfter the first line, blank line, then the human-readable summary.\n\n---\n\n## Scope of this skill — generating vs auditing\n\nThis skill **generates** the extension repo from a PRD. It's not a linter; it doesn't audit existing working code against the new template. When run in a directory that already has a scaffold, the regenerate \u002F resume \u002F abort gate asks before overwriting — and \"regenerate\" is destructive (loses any manual edits to generated files).\n\n**A diff between \"what we'd generate now\" and \"what exists\" is NOT a list of defects.** Existing code that builds (in `\u002Fbuild-android-binary` \u002F `\u002Fbuild-ios-binary`) and produces correct runtime behavior is fine even if the code shape differs from the current template. Flag only:\n\n- Code that won't compile in the build skills (malformed Kotlin \u002F Obj-C)\n- Code that violates the dispatch contract such that runtime breaks (native module symbol that doesn't match `receivers[].nativeModule`, wrong method signature the host can't dispatch, wrong response shape that PAM can't deliver)\n- Cross-platform drift (iOS and Android emitting different request field names or response shapes for the same operation)\n\nA locally-defined helper that does the same job as a template helper is NOT a defect. A method that was originally generated with a `\u002F\u002F TODO` marker and has since been filled in by the engineer is the EXPECTED state — don't flag it as \"different from the template.\"\n\n## Hard rules — correctness (these must be true for the control to work)\n\n- **There is NO TS `INativeExtension` layer.** The contract is the manifest's runtime dispatch (`\u003Cname>\u002F\u003Creceiver>` → `NativeModules.\u003CPascal>Module.\u003Cmethod>`, [`shared\u002Fppmplugin-format.md §2`](..\u002F..\u002Fshared\u002Fppmplugin-format.md)). Do NOT generate `src\u002F\u003CPascal>Extension.ts`, `handleMessageAsync`, or a `sendAsync` transport — they're SDK-era leakage that `\u002Faudit-ppmplugin` rejects.\n- **The native module symbol MUST be `\u003CPascal>Module` across all runtime surfaces.** iOS `+moduleName` returns `@\"\u003CPascal>Module\"`, Android `getName() = \"\u003CPascal>Module\"`, and the `.\u002Fmanifest.json` `receivers[].nativeModule` (authored here in §3.1, alongside these modules) MUST agree, and `\u003CPascal>Module` is what JS sees as `NativeModules.\u003CPascal>Module`. The Obj-C class name stays `RCT\u003CPascal>Module` and matches `entrypoints.ios.moduleClass`. Mismatch = the wrap host's dispatch finds nothing.\n- **The native module symbol MUST pass the validator's reserved-prefix \u002F reserved-name rules** ([`shared\u002Fppmplugin-format.md §4`](..\u002F..\u002Fshared\u002Fppmplugin-format.md)). The `Module` suffix is the structural dodge for bare reserved names (`DeviceInfo` → `DeviceInfoModule`); do NOT strip it. If the derived symbol still hits a reserved prefix or the known reserved-name subset, STOP and have the user pick a vendor-prefixed class.\n- **iOS and Android JSON response shapes MUST be byte-identical for the same input.** Both implementations are derived from the same §3.\u003Cn> response shape. Drift here causes maker-side branching bugs that are hard to diagnose.\n- **Request\u002Fresponse field names MUST match what the manifest + PCF expect.** The field names each native method parses and emits must match the request\u002Fresponse shapes in ARCHITECTURE §4 (which the manifest and PCF both derive from). Drift = the PCF sends\u002Freads the wrong fields.\n- **The native symbol-agreement smoke check (Step 4) MUST pass** before marking `.extension-state.md` Phase = `scaffold`. On failure, set `Status: blocked` with the reason.\n- **No invented APIs.** If §3.\u003Cn> specifies a framework\u002FAPI you're uncertain exists on the named min-OS version, STOP and flag. Don't ship code referencing a method that doesn't exist.\n\n## Recommended template style (preferences for new scaffolds; existing working code is fine)\n\nThese describe the cleanest shape for newly-generated code. They're how this skill renders fresh output. Existing code that achieves the same outcome differently is **not** in violation — don't flag stylistic differences as defects when auditing.\n\n- **Prefer the `Module`-suffixed native symbol** (`\u003CPascal>Module`) consistently across iOS, Android, and the future manifest. It satisfies the reserved-name rule and keeps the three dispatch surfaces in lockstep.\n- **Prefer complete implementations from ARCHITECTURE §3.\u003Cn>** rather than `\u002F\u002F TODO: implement using \u003Cframework>` placeholders. Generated code should compile and run on the happy path out of the gate.\n- **Prefer the pinned `react-native` devDep** from `package.json` for the build skills to resolve RN headers \u002F the `react-android` coordinate, rather than `latest`. Reproducible builds.\n\n## Things the skill enforces at generation time\n\nWhen emitting new code from scratch, the skill follows the template above. Strict requirements:\n\n- **No file overwrites without explicit confirm** — the regenerate \u002F resume \u002F abort gate at Step 2 is real.\n- **STOP with `NEEDS_CONTEXT` only when blocked.** Genuine blockers: PRD §2 missing (no class name → no scaffold), PRD §4 missing (no operations → nothing to generate), or a reserved-name \u002F generic-noun collision on the native module symbol the user won't resolve. Non-blockers that should NOT STOP: ARCHITECTURE §3.\u003Cn> missing fine implementation detail (use a reasonable default + annotate inline), ARCHITECTURE §8 missing UX for a specific code (use default mapping), framework choice in ARCHITECTURE §1.2 \u002F §1.3 missing a deployment-target detail (use the framework's documented minimum).\n- **Smoke check is a real gate.** If the native symbol-agreement check (Step 4) fails, `.extension-state.md` Phase stays below `scaffold` with `Status: blocked`. (The optional `pnpm install` is non-fatal — deferring the devDeps seed doesn't block.)\n- **Every function\u002Fmethod called from generated native code MUST be defined in the same file or imported.** This is enforced by Step 3.8 (self-review). When writing `this.createFoo()` in a Kotlin Activity, also write `private fun createFoo() { ... }` in the same file. When writing `[self setupBar]` in an Obj-C module, also write `- (void)setupBar { ... }`. The smoke check (Step 4) compiles nothing — native code errors surface in `\u002Fbuild-android-binary` \u002F `\u002Fbuild-ios-binary`. The self-review catches the \"called but never defined\" class of bug at generation time, not later when an engineer runs the binary build.\n- **Capture \u002F modal-presentation operations MUST include a Done action.** Any operation whose pattern is one-shot OR two-way with a confirm-and-return UX (drawing capture, photo capture, signature, scanner with manual confirm, etc.) MUST emit a toolbar Done action that triggers result submission. Cancel alone is not sufficient — the user has no way to complete the operation. On Android: `R.menu.\u003Clower>_capture_menu` MUST include `\u003Citem android:id=\"@+id\u002Faction_done\">` and `onOptionsItemSelected` handles it. On iOS: the `UINavigationItem` MUST set `rightBarButtonItem` to a Done `UIBarButtonItem`. Non-negotiable.\n- **Multi-mode operations (mutually exclusive UI modes — e.g. capture\u002Fplayback, photo\u002Fvideo, edit\u002Fpreview) MUST include active-state visual feedback.** On Android: use `MaterialButtonToggleGroup` with `app:singleSelection=\"true\"` for the mode toggle row; toggled-on button shows `colorPrimaryContainer`, toggled-off transparent. On iOS: rely on the platform's tool-picker (e.g. PencilKit, `AVCaptureSession`'s built-in UI) which handles this natively, OR use `UISegmentedControl` for custom toggles. Without active-state feedback, the user can't tell which mode is selected.\n\nAllowed in generated code (these are not defects):\n\n- **`\u002F\u002F TODO:` or `\u002F\u002F Customize:` comments** for items that legitimately need engineer judgment — non-trivial UI details that ARCHITECTURE §3.\u003Cn> doesn't specify (button corner radius, animation curves, edge case handling that the PRD intentionally defers). Clear annotated TODOs beat fabricated defaults.\n- **Reasonable defaults for unspecified detail.** If ARCHITECTURE §3.\u003Cn> says \"Done button right side\" but doesn't specify the button's tint color, use the platform default (e.g. `.systemBlue` on iOS, `?attr\u002FcolorAccent` on Android). Add a `\u002F\u002F Customize: tint per design` comment if it's likely the engineer will want to change it.\n- **Fallback error responses** when something at runtime doesn't match the contract. If a native method receives args it can't parse, resolving the promise with an error JSON (`{\"status\":\"error\",\"error\":\"INVALID_INPUT\",\"message\":\"\u003Cthe offending field \u002F parse reason>\"}`) via the `errorJson(code, message)` helper is the right answer — not a thrown exception. Always include the `message` — same for `INTERNAL_ERROR` catch-alls (carry the underlying reason).\n\nOther operational rules:\n\n- No reference-repo fetch at runtime — structural patterns are encoded in this SKILL.md + `shared\u002F*.md`.\n- No PCF generation (that's `\u002Fgenerate-pcf-companion`).\n- No manifest, binary build, or bundle assembly (that's `\u002Fgenerate-ppmplugin` and its stages).\n- No `src\u002F` TS layer, no tests (there's nothing to type-check; structural verification is `\u002Ftest-native-extension`).\n- No remote, feed, or registry access — this track is self-contained and local-only.\n\n## Runtime fallbacks the generated control SHOULD have\n\nThese are correctness-positive — make the control resilient against unexpected runtime conditions:\n\n- **Construction must not throw (crash-at-launch guard).** The module is built eagerly at bridge startup on a possibly Looper-less thread ([`ppmplugin-format §5`](..\u002F..\u002Fshared\u002Fppmplugin-format.md)); an uncaught throw in the constructor \u002F `init{}` (Android) or `init` (iOS) takes down the whole host before any UI. Defer listener\u002Fhardware registration to first method call; pass an explicit `Handler(Looper.getMainLooper())` (never `null`); wrap unavoidable init in try\u002Fcatch. See §3.4 (iOS) \u002F §3.5 (Android).\n- **Runtime permission + `currentActivity` guards** (Android): check a dangerous permission before the API call and resolve `PERMISSION_DENIED` on denial; guard `currentActivity != null` before presenting UI and resolve `NO_ACTIVITY` on null — never let `SecurityException` \u002F NPE crash the host. See §3.5.\n- **Native methods try\u002Fcatch around argument parsing** → resolve with `{status: 'error', error: 'INVALID_INPUT', message: '\u003Coffending field \u002F parse reason>'}` instead of throwing. The wrap bridge expects a resolved promise (an error JSON), not a rejected\u002Fthrown one.\n- **Native error responses go through the `errorJson(code, message)` helper** (code + human-readable reason), not as thrown exceptions. The native side resolves the Promise with an error JSON string carrying both fields; the wrap bridge delivers it to the PCF verbatim (which surfaces `message` as its `ErrorMessage` output).\n- **Native main-thread guarding for UI presentation** (`dispatch_async(dispatch_get_main_queue(), ...)` on iOS, `Handler(Looper.getMainLooper()).post { ... }` on Android). Presenting UI off main thread crashes; the dispatch is required.\n- **Unknown error codes from native** that the PCF didn't anticipate → pass through to the response verbatim. Don't drop or remap; let the maker's Power Fx formula see the actual code.\n\nThese are runtime safety nets. They're allowed\u002Fencouraged regardless of other style decisions.\n",{"name":5,"model":54,"description":7,"allowed-tools":55},"opus","Read, Write, Edit, Bash, Glob, Grep, AskUserQuestion, Skill","main","0a55a88fa2855404029d1e5450faf92878baf763","plugins\u002Fpower-apps-mobile-extension\u002Fskills\u002Fgenerate-native-extension",228,"https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002Fpower-platform-skills","https:\u002F\u002Fmicrosoft.github.io\u002Fpower-platform-skills\u002F","https:\u002F\u002Fgithub.com\u002Fmicrosoft","https:\u002F\u002Fopensource.microsoft.com","Open source projects and samples from Microsoft","2026-11-12 02:16:25",{"source":8,"skillName":5,"commands":67,"prompt":77,"zipAvailable":78},[68,71,74],{"tool":69,"command":70},"npx","npx skills add microsoft\u002Fpower-platform-skills --skill generate-native-extension",{"tool":72,"command":73},"bunx","bunx skills add microsoft\u002Fpower-platform-skills --skill generate-native-extension",{"tool":75,"command":76},"pnpm","pnpm dlx skills add microsoft\u002Fpower-platform-skills --skill generate-native-extension","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002Fpower-platform-skills\u002Fblob\u002Fmain\u002Fplugins\u002Fpower-apps-mobile-extension\u002Fskills\u002Fgenerate-native-extension\u002FSKILL.md",true,[],[81,102,123,141,155,165],{"skillNo":82,"name":83,"nameZh":6,"description":84,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":85,"categories":86,"keywords":87,"publishedAt":49},"01M699WVHX0XN59EPBTTPEKYPG","setup","Use when Process Intelligence needs building, explicit cloud\u002Faccount\u002Fenvironment configuration, sign-in, connection diagnostics, or recovery from an authentication error.","https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002Fpower-platform-skills\u002Fblob\u002Fmain\u002Fplugins\u002Fprocess-intelligence\u002Fskills\u002Fsetup\u002FSKILL.md",[],[83,45,88,89,90,91,92,93,94,95,96,97,98,99,100,101],"when","connection","error","sign","intelligence","configuration","use","process","diagnostics","environment","authentication","cloud","recovery","Bash",{"skillNo":103,"name":104,"nameZh":6,"description":105,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":106,"categories":107,"keywords":111,"publishedAt":49},"01M699WSR9EX55YY1WD9J22FEG","report-issue","Use when the user wants a Process Intelligence bug-report draft or support summary for a failed operation.","https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002Fpower-platform-skills\u002Fblob\u002Fmain\u002Fplugins\u002Fprocess-intelligence\u002Fskills\u002Freport-issue\u002FSKILL.md",[108],{"categoryNo":109,"categoryName":110,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000034","行政与办公人员",[112,113,114,115,116,95,94,117,118,92,119,120,121,88,122],"report issue","report","issue","support","user","bug","summary","draft","operation","for","bug-report",{"skillNo":124,"name":125,"nameZh":6,"description":126,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":127,"categories":128,"keywords":129,"publishedAt":49},"01M699WR26YQ9Q21KE6MJF5B2G","investigate-process","Use when a broad Process Mining business question, observed pattern or competing explanation needs investigation across analytical perspectives.","https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002Fpower-platform-skills\u002Fblob\u002Fmain\u002Fplugins\u002Fprocess-intelligence\u002Fskills\u002Finvestigate-process\u002FSKILL.md",[],[95,130,131,132,88,133,134,135,136,45,137,94,138,139,140],"investigate","investigate process","mining","process mining","investigation","perspectives","question","business","pattern","across","analytical",{"skillNo":142,"name":143,"nameZh":6,"description":144,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":145,"categories":146,"keywords":147,"publishedAt":49},"01M699WP9SDG5ZR12P8VYNXKTN","derive-metric","Use when a requested measure is missing, a saved custom metric needs reuse, or an ephemeral formula or derived scalar is needed.","https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002Fpower-platform-skills\u002Fblob\u002Fmain\u002Fplugins\u002Fprocess-intelligence\u002Fskills\u002Fderive-metric\u002FSKILL.md",[],[148,149,45,94,150,151,152,153,154,88],"metric","derive metric","formula","missing","reuse","saved","custom",{"skillNo":156,"name":157,"nameZh":6,"description":158,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":159,"categories":160,"keywords":161,"publishedAt":49},"01M699WMG5R8P6SMTDAS1TKF9E","compare-cohorts","Use when comparing process cohorts, departments, periods or before-and-after results under matched definitions.","https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002Fpower-platform-skills\u002Fblob\u002Fmain\u002Fplugins\u002Fprocess-intelligence\u002Fskills\u002Fcompare-cohorts\u002FSKILL.md",[],[162,163,88,94,95,164,45],"compare","compare cohorts","results",{"skillNo":166,"name":167,"nameZh":6,"description":168,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":169,"categories":170,"keywords":171,"publishedAt":49},"01M699WJWNVTMP9XK3NCJHV14D","analyze-variants","Use when investigating common or slow variants, repeated work, unusual paths, or a specific Process Mining case.","https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002Fpower-platform-skills\u002Fblob\u002Fmain\u002Fplugins\u002Fprocess-intelligence\u002Fskills\u002Fanalyze-variants\u002FSKILL.md",[],[172,173,174,133,88,132,175,176,94,95,45,177,178],"analyze","variants","analyze variants","case","paths","work","common"]