[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M699QR2SBG2A64GQZWS2Q73G":3,"skill-same:01M699QR2SBG2A64GQZWS2Q73G":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":51,"linkOnly":52,"markdownContent":53,"markdownBody":54,"frontMatter":55,"refName":57,"commitSha":58,"skillDirectory":59,"forksCount":60,"repositoryHtmlUrl":61,"repositoryHomepageUrl":62,"ownerHtmlUrl":63,"ownerBlogUrl":64,"ownerDescription":65,"updatedAt":66,"install":67,"resources":79},"01M699QR2SBG2A64GQZWS2Q73G","generate-pcf-companion",null,"Generate the dispatcher PCF for a third-party `.ppmplugin` (wrap-runtime) control. Runs `pac pcf init` in the pcf\u002F subfolder, rewrites ControlManifest.Input.xml from ARCHITECTURE §6, and writes index.ts derived from ARCHITECTURE §4 (message contract), §8 (PCF surface), §9 (error UX) — no placeholders. The bridge dispatches the composite key `\u003Cname>\u002F\u003Creceiver>` to `NativeModules.\u003CnativeModule>.\u003Cmethod>` via the host-injected `window.PowerApps.NativeExtension.sendAsync` global (never `cordova.exec` — not in the PCF sandbox); also emits a `PowerAppsNativeExtension.d.ts` ambient declaration. Responses are peeled with `extractResponse`. Emits structured JSON debug\u002Ferror logs. Validated by `npm run build`. **Local only** — does not deploy. Needs only `pac` CLI. Run after the native module exists. Uses npm (not pnpm).","microsoft\u002Fpower-platform-skills","microsoft","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F6154722?v=4","01M698VYDXZ6JW5WH3CRV650R8","Microsoft","01M699JVYJFB75D9JM70BN4H0Q",1149,"MIT","https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002Fpower-platform-skills\u002Fblob\u002Fmain\u002Fplugins\u002Fpower-apps-mobile-extension\u002Fskills\u002Fgenerate-pcf-companion\u002FSKILL.md",[18,23],{"categoryNo":19,"categoryName":20,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000030","开发工程师","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,49,50],"companion","pcf","generate pcf companion","generate","write","for","opus","receiver","input","host","pnpm","local","run build","bridge","edit","cli","read","logs","run","dispatcher","deploy","architecture","init","sandbox","2026-11-10 10:12:30",false,"---\nname: generate-pcf-companion\ndescription: Generate the dispatcher PCF for a third-party `.ppmplugin` (wrap-runtime) control. Runs `pac pcf init` in the pcf\u002F subfolder, rewrites ControlManifest.Input.xml from ARCHITECTURE §6, and writes index.ts derived from ARCHITECTURE §4 (message contract), §8 (PCF surface), §9 (error UX) — no placeholders. The bridge dispatches the composite key `\u003Cname>\u002F\u003Creceiver>` to `NativeModules.\u003CnativeModule>.\u003Cmethod>` via the host-injected `window.PowerApps.NativeExtension.sendAsync` global (never `cordova.exec` — not in the PCF sandbox); also emits a `PowerAppsNativeExtension.d.ts` ambient declaration. Responses are peeled with `extractResponse`. Emits structured JSON debug\u002Ferror logs. Validated by `npm run build`. **Local only** — does not deploy. Needs only `pac` CLI. Run after the native module exists. Uses npm (not pnpm).\nallowed-tools: Read, Write, Edit, Bash, Glob, Grep, AskUserQuestion, Skill\nmodel: opus\n---\n\n# \u002Fgenerate-pcf-companion\n\nGenerates the dispatcher PCF — the Canvas Studio control that calls the third-party native module through the host-injected `window.PowerApps.NativeExtension.sendAsync` global, routed by the composite key `\u003Cname>\u002F\u003Creceiver>` read from the committed `.\u002Fmanifest.json` (the source of truth `\u002Fgenerate-native-extension` authors at scaffold time). Lives at `pcf\u002F\u003CPascal>PCF\u002F` in the same repo the native module lives in. The PCF is a Studio-side companion; it is **NOT** part of the `.ppmplugin` bundle (the bundle ships native binaries only — `manifest.json` + `android\u002F`\u002F`ios\u002F`).\n\nThis skill assumes the native module already exists in the repo. Run it after the module is in place.\n\n> **PCF framework reference (public Microsoft Learn docs).** Ground `pac pcf init`, the `ControlManifest.Input.xml` schema, the `init`\u002F`updateView`\u002F`getOutputs`\u002F`destroy` lifecycle, and the `usage` (`bound`\u002F`input`\u002F`output`) rules against the official Power Apps Component Framework docs — they are the authority when this skill's templates and the live framework disagree. (The `sendAsync` transport + `extractResponse` response-unwrap specifics are this track's own, in [`shared\u002Fppmplugin-format.md §2`](..\u002F..\u002Fshared\u002Fppmplugin-format.md) — not in these generic PCF docs.)\n> - Overview: \u003Chttps:\u002F\u002Flearn.microsoft.com\u002Fen-us\u002Fpower-apps\u002Fdeveloper\u002Fcomponent-framework\u002Foverview>\n> - Create a code component: \u003Chttps:\u002F\u002Flearn.microsoft.com\u002Fen-us\u002Fpower-apps\u002Fdeveloper\u002Fcomponent-framework\u002Fcreate-custom-controls-using-pcf>\n> - Custom controls overview: \u003Chttps:\u002F\u002Flearn.microsoft.com\u002Fen-us\u002Fpower-apps\u002Fdeveloper\u002Fcomponent-framework\u002Fcustom-controls-overview>\n\n---\n\n## Step 1 — Read the shared docs and the PRD\n\n1. Read [`shared\u002Fshared-instructions.md`](..\u002F..\u002Fshared\u002Fshared-instructions.md), [`shared\u002Fnaming-conventions.md`](..\u002F..\u002Fshared\u002Fnaming-conventions.md), [`shared\u002Fppmplugin-format.md`](..\u002F..\u002Fshared\u002Fppmplugin-format.md), [`shared\u002Frepo-layout.md`](..\u002F..\u002Fshared\u002Frepo-layout.md).\n2. Apply the **per-skill minimal prereq policy** ([`shared-instructions.md §1.5`](..\u002F..\u002Fshared\u002Fshared-instructions.md)). This skill needs Node + `pac` CLI only — `pac pcf init` is a local file generator and `npm install`\u002F`npm run build` under `pcf\u002F` only needs Node. It does NOT need pnpm, package-feed authentication, .NET SDK runtime, or active `pac auth`.\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-pcf-companion\n   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n    ? ✓ Node 20+ installed                   (for npm install + tsc under pcf\u002F)\n    ? ✓ pac CLI installed                    (for pac pcf init)\n\n    ? 2 checks passed. Ready to proceed.\n   ```\n\n   If `pac` is missing, STOP with the fix command (`dotnet tool install -g Microsoft.PowerApps.CLI.Tool` — note: installing `pac` requires .NET SDK as a one-time install, but neither .NET nor `pac auth` is needed at runtime for scaffold). If Node is missing, STOP with the install instruction. Run the **`\u002Fgenerate-pcf-companion` check** from [`prereq-check.md`](..\u002F..\u002Fshared\u002Fprereq-check.md) (Node + `pac` only — this self-contained track has no \"baseline\" check).\n\n   **.NET SDK + active `pac auth` are NOT checked here.** If the user later picks the optional \"Yes, also deploy now\" path in Step 2, the deploy prereq one-liner is run **at that point** (just-in-time, before `pac pcf push`).\n3. Read `.\u002FPRD.md`. If missing or §8 (PCF surface) is incomplete (any `\u003CNEEDS INPUT>` or missing fields in §8.1–§8.4), STOP with `BLOCKED: PRD.md §8 PCF surface is incomplete — re-run \u002Fdesign-native-extension-feature and complete the PCF section.`\n4. Read `.\u002F.extension-state.md`. If Phase isn't at least `scaffold`, STOP with `BLOCKED: run \u002Fgenerate-native-extension first.`\nThe structural patterns this skill needs to emit (manifest shape, `index.ts` bridge wiring, output mapping) are fully prescribed in this SKILL.md (§4–§5) and in [`shared\u002Fppmplugin-format.md`](..\u002F..\u002Fshared\u002Fppmplugin-format.md) §2 (Runtime dispatch contract). Do NOT fetch the reference extension repo at runtime — its lessons are already encoded here, and fetching it would risk reference-specific UI logic bleeding into an unrelated PCF.\n\n---\n\n## Step 1.5 — Resolve the dispatch contract from `.\u002Fmanifest.json` and the native module\n\nThe wrap **runtime dispatch contract** ([`shared\u002Fppmplugin-format.md`](..\u002F..\u002Fshared\u002Fppmplugin-format.md) §2) is the **authoritative specification** of how a host call reaches the bundle. The `.ppmplugin` bundle is native-only (no TS `handleMessageAsync` layer *in the bundle*), but the **companion PCF** dispatches through the host-injected **`window.PowerApps.NativeExtension.sendAsync`** global — it must **NEVER** call `cordova.exec` directly (the raw `cordova` global is not exposed to the PCF sandbox; a direct call is a silent no-op on device, worst on Android). `sendAsync` performs the underlying `cordova.exec(\"SendMessagePlugin\", …)` transport *inside the host context* and routes to the React Native module the binary ships:\n\n```\nPCF → window.PowerApps.NativeExtension.sendAsync(\"\u003Cname>\u002F\u003Creceiver>\", { method, args: [request] })\n    → host global (host context): cordova.exec(\"SendMessagePlugin\", \"\u003Cname>\u002F\u003Creceiver>\", [JSON.stringify({method,args}), corrId])\n    → proxy → NativeModules[\u003CnativeModule>][\u003Cmethod>].apply(mod, \u003Cargs-array>)\n```\n\nThe **composite routing key** `\u003Cname>\u002F\u003Creceiver>` is what the host resolves to a module; the **method** is one entry from that receiver's `methods[]`. One PCF drives **both iOS and Android** through this global — no platform branch.\n\n> **⚠️ TWO invariants — both confirmed on device; getting either wrong = silent failure:**\n> 1. **Dispatch via `sendAsync`, NEVER `cordova.exec`.** The envelope is a **RAW object** `{ method, args: [request] }` — the PCF does **not** stringify it; `sendAsync` does the `JSON.stringify` internally. A PCF that calls `cordova.exec` directly, or that pre-stringifies the payload, fails silently on the first device tap (no error on screen; nothing dispatches — worst on Android).\n> 2. **The inner `args` MUST be a JSON ARRAY ([ppmplugin-format §2](..\u002F..\u002Fshared\u002Fppmplugin-format.md)).** After parsing the envelope the proxy runs `Array.isArray(parsed.args) ? parsed.args : []` then `fn.apply(mod, args)` — spreading it as **positional arguments**. A bare object → dropped → the native method gets no request data. **Our convention: `args: [request]`** — one request object, and the native method takes exactly one `ReadableMap`\u002F`NSDictionary` first parameter.\n\nOn `status === \"ok\"`, `sendAsync` resolves `result.data` — the native method's resolved string. The wrap host both re-stringifies it once **and** nests it in a `{ isUpdate, message }` transport container, so the PCF normalizes it with an `extractResponse` helper (parse `result.data`, then — if the parsed object has no top-level `status` — unwrap the `message` container to reach the module's `{status, result}` object; total-fail → `PARSE` error). A bare single parse lands on the container and fails every call with `UNEXPECTED_PAYLOAD` though native succeeded — see [`shared\u002Fppmplugin-format.md §2`](..\u002F..\u002Fshared\u002Fppmplugin-format.md). `status !== \"ok\"` → surface `result.error` (fall back to `BRIDGE_FAILED`); missing host global → `NOT_IN_WRAP`.\n\n### Required reads — must succeed before Step 2\n\n1. **Resolve the composite routing key** `\u003Cname>\u002F\u003Creceiver>` from the **committed `.\u002Fmanifest.json`** — the source of truth `\u002Fgenerate-native-extension` writes at scaffold time, so on the normal flow it already exists when this skill runs. Prefer it; fall back to the staged copy, then ARCHITECTURE only if no manifest exists yet (a hand-authored module). **OS-neutral: read `.\u002Fmanifest.json` with the Read tool and parse the JSON directly — don't shell out to `grep`\u002F`sed` (the bash below is illustrative; it won't run on Windows):**\n   ```bash\n   MANIFEST=$( [ -f .\u002Fmanifest.json ] && echo .\u002Fmanifest.json || echo ppmplugin\u002Fstaging\u002Fmanifest.json )\n   if [ -f \"$MANIFEST\" ]; then\n     NAME=$(grep -o '\"name\"[[:space:]]*:[[:space:]]*\"[^\"]*\"' \"$MANIFEST\" | head -1 | sed 's\u002F.*\"\\([^\"]*\\)\"$\u002F\\1\u002F')\n     echo \"manifest ($MANIFEST) name: $NAME — receiver\u002FnativeModule\u002Fmethods read from receivers[]\"\n   else\n     echo \"no manifest.json yet (hand-authored module) — derive \u003Cname>=kebab(className), \u003Creceiver>=\u003CPascal>Extension, nativeModule=\u003CclassName> from ARCHITECTURE; \u002Fgenerate-ppmplugin-manifest will author it\"\n   fi\n   ```\n   The dispatch key the PCF binds and the receiver the manifest registers MUST match — a PCF that dispatches `\u003Cname>\u002F\u003Creceiver>` while the manifest registers a different receiver fails on first dispatch. Because `.\u002Fmanifest.json` is authored **before** this skill runs (at native-gen), the PCF **follows the manifest's** receiver — bind exactly the `receivers[].name` it declares.\n\n2. **Read `manifest.json` `receivers[]`** (canonical dispatch target):\n   - `receivers[].name` — the `\u003Creceiver>` half of the composite key\n   - `receivers[].nativeModule` — what the host resolves as `NativeModules.\u003CnativeModule>` (the module's `getName()`)\n   - `receivers[].methods` — the `Method` values the host may dispatch; each is a real `@ReactMethod` \u002F `RCT_EXPORT_METHOD` name. The PCF's `onTrigger` calls one of these.\n\n3. **Native source** (verification only — `ios\u002FRCT\u003CPascal>Module.m`, `android\u002Fsrc\u002Fmain\u002Fjava\u002F...\u002F\u003CPascal>Module.kt`):\n   - Android `getName()` \u002F iOS `+ (NSString *)moduleName` MUST equal `receivers[].nativeModule`\n   - Every `method` the PCF dispatches MUST be a real `@ReactMethod` \u002F `RCT_EXPORT_METHOD` on the module (an unknown method = `method '\u003Cm>' not found` on device)\n   - If native drifts from the manifest, STOP with `NEEDS_CONTEXT: native module drifted from manifest.json receivers[]; reconcile and re-run`\n\n### Compose the resolved contract\n\n```\nTransport (host global — fixed):\n  window.PowerApps.NativeExtension.sendAsync(\"\u003Cname>\u002F\u003Creceiver>\", { method, args: [request] })\n                                              ↑ envelope is a RAW object; sendAsync stringifies it internally\n  result.status === \"ok\"  → extractResponse(result.data) yields the module's response object (unwraps the wrap `message` container)\n  result.status !== \"ok\"  → bridge\u002Ftransport failure (result.error ?? BRIDGE_FAILED); parse-fail → PARSE\n  no window.PowerApps.NativeExtension → NOT_IN_WRAP (Studio preview \u002F non-PAM host \u002F CordovaV2 off)\n\nDispatch target (from manifest.json receivers[]):\n  Composite key: \u003Cname>\u002F\u003Creceiver>\n  nativeModule:  \u003CNativeModules.\u003CnativeModule>>\n  method:        \u003Cone of methods[]>\n  args:          [request]  — a JSON ARRAY (spread positionally via fn.apply). Our convention: ONE request\n                 object at args[0]; the @ReactMethod \u002F RCT_EXPORT_METHOD takes one ReadableMap\u002FNSDictionary param.\n  Response shape: \u003Clist — module's own {status, result, error, message}>  (message = human-readable failure reason)\n  Module error codes: \u003Clist — USER_CANCELLED, INVALID_INPUT, ...>  (canonical set + meanings: shared\u002Ferror-codes.md)\n\nVerification (native source):\n  All checks: \u003Cpass | fail with mismatch>\n```\n\n### Drift detection\n\n| Disagreement | Action |\n|---|---|\n| `manifest.json receivers[]` ↔ native source disagree on `nativeModule` \u002F method names | STOP with `NEEDS_CONTEXT`. List the mismatches. Reconcile before generating PCF. |\n| PCF composite key `\u003Cname>\u002F\u003Creceiver>` ↔ manifest's registered receiver disagree | The PCF and manifest must agree on the key. `.\u002Fmanifest.json` is authored first (at native-gen), so the **PCF follows the manifest** — bind the `receivers[].name` it declares. (Only if the user deliberately renames the receiver in the PCF, update `.\u002Fmanifest.json` to match and re-run.) |\n| Host `sendAsync` payload\u002Fresponse wire format ↔ what this skill emits | The exact envelope is owned by the host global + wrap proxy; confirm against `shared\u002Fppmplugin-format.md §2`. The PCF guarantees only the composite key + `method` the bridge ultimately targets. |\n\n**Use the resolved contract** — not the PRD's §5.1\u002F§5.2 — as the source for the PCF's dispatch args and response parsing in Step 5. The PRD describes intent; `.\u002Fmanifest.json` + native is the actual dispatch contract. When they agree, all three are consistent; when they don't, the native source wins (since that's what the running app sees).\n\n---\n\n## Step 2 — Confirm the plan with the user\n\nPrint a summary derived from ARCHITECTURE §6 and the derived names, then gate on approval.\n\n```\nPCF scaffold plan\n─────────────────\nFolder: pcf\u002F\u003CPascal>PCF\u002F\nNamespace: PowerApps  (constant for all native-extension PCFs)\nControl name: \u003CPascal>PCF\nDispatches: composite key '\u003Cname>\u002F\u003Creceiver>' → NativeModules.\u003CnativeModule>.\u003Cmethod>\n                                                  via window.PowerApps.NativeExtension.sendAsync (host global)\n\nBound input (ARCHITECTURE §6.1 (bound input)):\n  \u003CName> : \u003CType>   \u003C— bound, required>\n\nConfigurable inputs (ARCHITECTURE §6.1 (configurable inputs)):\n  \u003CName> : \u003CType> = \u003Cdefault>   \u003C— purpose>\n  ...\n\nOutput properties (ARCHITECTURE §6.1 (output properties)):\n  \u003CName> : \u003CType>   \u003C— purpose>\n  ...\n\nTrigger (ARCHITECTURE §6.2): \u003Cone line>\n```\n\nUse `AskUserQuestion`:\n\n> Proceed with this PCF scaffold?\n> - **Yes** — run `pac pcf init`, write\u002Frewrite files, run `npm install` + `npm run build` smoke check. All local — no environment deploy.\n> - **Edit the PRD first** — exit; user runs `\u002Fdesign-native-extension-feature` to fix §8.\n> - **Cancel**\n\nDeployment to a Power Platform environment is a separate, on-demand step via `\u002Fpublish-pcf-companion`. This skill is purely local — it doesn't touch `pac auth`, doesn't call `pac pcf push`, doesn't need .NET SDK.\n\n---\n\n## Step 3 — Run `pac pcf init`\n\nInside the repo root:\n\n```bash\nmkdir -p pcf\ncd pcf\npac pcf init --namespace PowerApps --name \u003CPascal>PCF --template field --framework none\n```\n\nNotes on the flags:\n- `--namespace PowerApps` — constant. All native-extension PCFs share this namespace so they group together in Canvas Studio's Insert panel.\n- `--template field` — single-bound-value control. Matches the \"trigger a native operation on a maker-set input\" pattern. Don't use `dataset` for v0.\n- `--framework none` — vanilla DOM. No React. Keeps the bundle tiny and avoids version friction with the host's managed build's React.\n\n`pac pcf init` creates `pcf\u002F\u003CPascal>PCF\u002F` with this structure:\n- `\u003CPascal>PCF.pcfproj` (MSBuild project)\n- `package.json` (uses npm — PCF tooling convention)\n- `pcfconfig.json`\n- `tsconfig.json`\n- `eslint.config.mjs`\n- `\u003CPascal>PCF\u002F` (nested) — `ControlManifest.Input.xml` + `index.ts` + `PowerAppsNativeExtension.d.ts` (ambient host-global decl, emitted in Step 5.5) + (later) `generated\u002FManifestTypes.d.ts`\n\nIf `pac pcf init` fails:\n- **\"pac not found\"** → re-run the prereq check. The `pwsh` prefix may be needed on Windows.\n- **\"folder already exists\"** → ask whether to delete it and regenerate, or merge (only safe if no manual edits were made).\n- **Auth-related** → run `pac auth list` and surface which profile is active; suggest `pac auth create` if none.\n\nAfter `pac pcf init` succeeds, also write `pcf\u002FREADME.md` (one level up from the control folder). Sections:\n\n1. **Overview** — one paragraph from PRD §1 explaining what this PCF does.\n2. **Not in the npm tarball** — explicit note that the PCF folder is excluded from `package.json`'s `files` array; it ships to Power Platform via `pac pcf push`, not via npm.\n3. **Properties** — three short tables from ARCHITECTURE §6 (bound, configurable, output).\n4. **Build & iterate** — `npm install`, `npm run build`, `pac pcf push --publisher-prefix \u003C2–8 char prefix>` (see `\u002Fpublish-pcf-companion` for prefix selection).\n5. **Trigger behavior** — one line from ARCHITECTURE §6.2.\n\nKeep it ~50 lines. Tailor every section to the PRD; don't invent boilerplate.\n\n---\n\n## Step 4 — Rewrite `ControlManifest.Input.xml`\n\n`pac pcf init` produces a single-property manifest. Rewrite it to match ARCHITECTURE §6 exactly.\n\n**Use human-readable text for `display-name-key` and `description-key`.** These attributes are what the maker sees in Power Apps Studio's properties panel — they're not just internal keys. Without `.resx` resource files (which this scaffold doesn't ship), Studio displays the attribute value verbatim. Write friendly labels and sentences, not programmer-style keys.\n\n> **⚠️ HARD RULE — no apostrophes (and no raw `\u003C` `>` `&`) in these attributes.** `pac pcf push` validates the manifest against an XSD where `display-name-key` \u002F `description-key` are `noAposStringType` — **a literal ASCII apostrophe (`'`) fails the push** with `noAposStringType` validation. It also breaks on raw XML metacharacters. So when deriving these strings:\n> - **Rephrase to avoid possessives\u002Fcontractions** rather than emitting an apostrophe — e.g. \"the phone's flashlight\" → **\"the device flashlight\"** \u002F \"the phone flashlight\"; \"doesn't\" → \"does not\"; \"user's\" → \"the user\". This reads cleanest.\n> - If a string genuinely must keep the punctuation, use the typographic right single quote **`’` (U+2019)**, which is NOT the ASCII apostrophe and passes the XSD — but prefer rephrasing.\n> - Escape or avoid `&` (`&amp;`), `\u003C`, `>`. Keep these attributes plain ASCII sentences.\n> - This applies to **every** `display-name-key` \u002F `description-key` in the manifest (control + each property). Scan the final manifest for `'` before writing it.\n\nDerivation rules:\n\n| Attribute | Value |\n|---|---|\n| `\u003Ccontrol display-name-key=\"...\">` | PRD §2 \"Human-readable name\" if present; else convert `\u003CPascal>PCF` to title case (e.g. `BarcodeScannerPCF` → `Barcode scanner`) |\n| `\u003Ccontrol description-key=\"...\">` | PRD §1 Summary, trimmed to ~120 chars (single sentence) |\n| `\u003Cproperty display-name-key=\"...\">` | Convert the property `name` to title case with spaces (e.g. `PenColor` → `Pen color`, `SignatureBase64` → `Signature base64`) |\n| `\u003Cproperty description-key=\"...\">` | The \"Purpose\" column from ARCHITECTURE §6.1 (bound input) \u002F §8.2 \u002F §8.3 for that property |\n\nThe manifest structure (substitute the human-readable strings, NOT placeholder keys):\n\n```xml\n\u003C?xml version=\"1.0\" encoding=\"utf-8\" ?>\n\u003Cmanifest>\n  \u003Ccontrol namespace=\"PowerApps\"\n           constructor=\"\u003CPascal>PCF\"\n           version=\"0.0.1\"\n           display-name-key=\"\u003Chuman-readable name from PRD §2>\"\n           description-key=\"\u003Cshort summary from PRD §1>\"\n           control-type=\"standard\">\n\n    \u003C!-- §8.1 Bound input — OPTIONAL, at most one, usage=bound. OMIT this block\n         entirely unless there is a single primary column the control both reads AND\n         writes back (text editor, scrubber, chart). Most native-extension PCFs are\n         action\u002Fconfig controls and have NO bound property — see the usage table below. -->\n    \u003Cproperty name=\"\u003CBoundName>\"\n              display-name-key=\"\u003Ctitle-cased BoundName>\"\n              description-key=\"\u003CPurpose from ARCHITECTURE §6.1 (bound input)>\"\n              of-type=\"\u003CType>\"\n              usage=\"bound\"\n              required=\"true\" \u002F>\n\n    \u003C!-- §8.2 Configurable inputs — usage=input, required=\"false\". Values the maker\n         TYPES or PICKS in the property panel (read-only to the control). -->\n    \u003Cproperty name=\"\u003CConfigName>\"\n              display-name-key=\"\u003Ctitle-cased ConfigName>\"\n              description-key=\"\u003CPurpose from ARCHITECTURE §6.1 (configurable inputs)>\"\n              of-type=\"\u003CType>\"\n              usage=\"input\"\n              required=\"false\"\n              default-value=\"\u003Cdefault>\" \u002F>\n    \u003C!-- ... one \u003Cproperty> per configurable input ... -->\n\n    \u003C!-- §8.3 Output properties — usage=output. Values the control PRODUCES that the\n         maker READS in Power Fx (Self.PropertyName) — status, result, error, computed\n         text. These are NOT bound and NOT input. Every runtime value the maker consumes\n         is an output, NOT a bound prop. Declare each in IOutputs + return from getOutputs(). -->\n    \u003Cproperty name=\"\u003COutputName>\"\n              display-name-key=\"\u003Ctitle-cased OutputName>\"\n              description-key=\"\u003CPurpose from ARCHITECTURE §6.1 (output properties)>\"\n              of-type=\"\u003CType>\"\n              usage=\"output\" \u002F>\n    \u003C!-- ... one \u003Cproperty> per output ... -->\n\n    \u003C!-- On-device diagnostic — the ONE legitimate usage=\"bound\" in a wrap PCF.\n         On a release wrap build the WebView console is unreachable from logcat \u002F\n         chrome:\u002F\u002Finspect, so the PCF surfaces the RAW bridge response (the wire string\n         exactly as it arrived, before extractResponse) here. The maker drops it on a Power Fx\n         label (Self.\u003Cname>Json) and reads what actually came back with no connected\n         debugger. See shared\u002Fppmplugin-format.md §2 \"Wrap-bridge response quirks\". -->\n    \u003Cproperty name=\"\u003Cname>Json\"\n              display-name-key=\"\u003Ctitle-cased name> raw response\"\n              description-key=\"Raw bridge response for on-device debugging — drop on a label as Self.\u003Cname>Json.\"\n              of-type=\"SingleLine.Text\"\n              usage=\"bound\" \u002F>\n\n    \u003Cresources>\n      \u003Ccode path=\"index.ts\" order=\"1\" \u002F>\n    \u003C\u002Fresources>\n  \u003C\u002Fcontrol>\n\u003C\u002Fmanifest>\n```\n\nIllustrative example (substitute the actual `\u003CPascal>` and property names from PRD §2 + §8):\n\n```xml\n\u003Ccontrol namespace=\"PowerApps\"\n         constructor=\"\u003CPascal>PCF\"\n         version=\"0.0.1\"\n         display-name-key=\"\u003CHuman-readable name from PRD §2>\"\n         description-key=\"\u003COne-line description from PRD §1.>\"\n         control-type=\"standard\">\n\n  \u003Cproperty name=\"\u003CInputPropertyName from ARCHITECTURE §6.1 (configurable inputs)>\"\n            display-name-key=\"\u003CHuman-readable label>\"\n            description-key=\"\u003COne-line description>\"\n            of-type=\"SingleLine.Text\"\n            usage=\"input\"\n            required=\"false\"\n            default-value=\"\u003Cdefault from ARCHITECTURE §6.1 (configurable inputs)>\" \u002F>\n\n  \u003Cproperty name=\"\u003COutputPropertyName from ARCHITECTURE §6.1 (output properties)>\"\n            display-name-key=\"\u003CHuman-readable label>\"\n            description-key=\"\u003COne-line description>\"\n            of-type=\"SingleLine.Text\"\n            usage=\"output\" \u002F>\n  ...\n\u003C\u002Fcontrol>\n```\n\nPCF property types you'll commonly see: `SingleLine.Text`, `SingleLine.URL`, `SingleLine.Email`, `Whole.None`, `Decimal`, `TwoOptions`, `DateAndTime.DateOnly`, `DateAndTime.DateAndTime`. Map the PRD's TypeScript types accordingly (e.g. `string` → `SingleLine.Text` unless context says URL).\n\n#### Standard diagnostic outputs — ALWAYS emit these three\n\nIn **addition** to the operation's result outputs (and the `\u003Cname>Json` raw-response bound output above), every dispatcher PCF MUST declare three diagnostic outputs (all `of-type=\"SingleLine.Text\"`, `usage=\"output\"`). For a third-party control this matters even more than first-party: the native binary runs inside the customer's wrap shell with **no logcat \u002F Xcode console \u002F native debugger reachable**, so the only way a failure is visible at all is if the code + message ride back through the bridge into a formula-readable output:\n\n```xml\n\u003Cproperty name=\"Status\"       display-name-key=\"Status\"        description-key=\"ok | error | cancelled\" of-type=\"SingleLine.Text\" usage=\"output\" \u002F>\n\u003Cproperty name=\"ErrorCode\"    display-name-key=\"Error Code\"    description-key=\"Machine-readable error code; empty on success\" of-type=\"SingleLine.Text\" usage=\"output\" \u002F>\n\u003Cproperty name=\"ErrorMessage\" display-name-key=\"Error Message\" description-key=\"Human-readable failure reason; empty on success\" of-type=\"SingleLine.Text\" usage=\"output\" \u002F>\n```\n\n- `Status` — `\"ok\"` | `\"error\"` | any lifecycle state the control uses (e.g. `\"cancelled\"`).\n- `ErrorCode` — the machine-readable code from the native error response (`USER_CANCELLED`, `INVALID_INPUT`, `BRIDGE_FAILED`, `PARSE`, …); `\"\"` on success. Makers branch on it.\n- `ErrorMessage` — the **human-readable** `message` the native side attached (the exception text, the offending field, the denied permission); `\"\"` on success. **This is the field a maker or support engineer reads first when something fails in the field** — without it, a failure is a silent no-op.\n\nThe two debugging outputs are complementary: `\u003Cname>Json` shows the *raw wire bytes* (transport-level forensics); `ErrorCode`\u002F`ErrorMessage` show the *parsed, structured* failure (what the native module meant). Maker pattern: `If(Self.Status = \"error\", Notify(Self.ErrorMessage, NotificationType.Error))`. Declare all three in `IOutputs`, set them in `setError` \u002F `setSuccess`, and return them from `getOutputs()`.\n\n#### Optional visual-style inputs (color + border) — ONLY if the user asks\n\nEvery dispatcher PCF already ships a **good-looking themed default** (see `applyStyles()` \u002F `ensureStyleTag()` in Step 5) that follows the host Fluent theme — so it never renders as a raw browser button **without** any extra inputs. Do **NOT** add maker-facing color\u002Fborder inputs by default; they clutter the property panel for controls that don't need them.\n\n**Emit these ONLY when the PRD \u002F user explicitly calls for maker-configurable color or border options.** When they do, add just the knobs requested (from the set below), as `usage=\"input\"`, `required=\"false\"`, all `SingleLine.Text` except the numeric radius:\n\n```xml\n\u003Cproperty name=\"AccentColor\"  display-name-key=\"Accent color\"  description-key=\"Button background color (hex, e.g. #0f6cbd). Blank = host theme.\" of-type=\"SingleLine.Text\" usage=\"input\" required=\"false\" default-value=\"\" \u002F>\n\u003Cproperty name=\"TextColor\"    display-name-key=\"Text color\"    description-key=\"Label color (hex). Blank = auto for contrast on the accent.\" of-type=\"SingleLine.Text\" usage=\"input\" required=\"false\" default-value=\"\" \u002F>\n\u003Cproperty name=\"BorderColor\"  display-name-key=\"Border color\"  description-key=\"Border color (hex). Blank = matches the accent color.\" of-type=\"SingleLine.Text\" usage=\"input\" required=\"false\" default-value=\"\" \u002F>\n\u003Cproperty name=\"BorderRadius\" display-name-key=\"Corner radius\" description-key=\"Corner radius in pixels (0 = square, 4 = default, 20 = pill).\" of-type=\"Whole.None\" usage=\"input\" required=\"false\" default-value=\"4\" \u002F>\n```\n\nRules that keep this **small and safe** (not a theming engine):\n- **The default is to emit NONE of these.** The themed baseline + host theme already look right; only surface a knob the user actually requested. `applyStyles()` reads each one **only if its `\u003Cproperty>` exists**, so omitting them changes nothing about the default look.\n- **Contrast is guaranteed, not the maker's problem.** If `AccentColor` is emitted and set but `TextColor` is blank, `applyStyles()` computes black\u002Fwhite by luminance so the label always clears WCAG AA — a maker can't accidentally create an invisible-label button.\n- **Don't add width\u002Fheight\u002Ffont-size inputs** — the host box sizes the control; sizing inputs fight the canvas resize handle.\n\n### Choosing `usage` per property — decide BEFORE emitting any `\u003Cproperty>`\n\n`usage` is a **required** attribute and the single most common thing to get wrong. The manifest schema defines exactly **three** values ([property element reference](https:\u002F\u002Flearn.microsoft.com\u002Fpower-apps\u002Fdeveloper\u002Fcomponent-framework\u002Fmanifest-schema-reference\u002Fproperty)) — the property \"represents a column the component can change (`bound`), read-only (`input`), or output values (`output`)\". Pick deliberately; the wrong choice clutters the maker's input panel (everything as a typeable input) or hides values that should be formula-readable (an output mislabeled `bound`).\n\nFor **every** property, run this decision in order — first match wins:\n\n1. **Does the control *produce* this value for the maker to read?** (status, result, current value, last error, computed\u002Freturned text — anything the maker references as `Self.\u003CName>` \u002F `\u003CControl>.\u003CName>` in Power Fx) → **`output`**. This is the default for everything the native operation returns. *If the maker reads it in a formula, it is an output — never `bound`.*\n2. **Does the maker *set\u002Fconfigure* this value?** (URL, table name, id, color, interval, toggle, JSON config — typed or picked in the property panel, or bound to a field for reference) → **`input`** (`required=\"false\"`, give a `default-value`).\n3. **Is there a single primary column the control both displays AND writes back** (two-way edit — text editor, scrubber, chart)? → **`bound`** (at most one). Otherwise **no bound property at all.**\n\n| `usage` | Meaning (authoritative) | Maker \u002F Studio behavior | TS wiring |\n|---|---|---|---|\n| `output` | A value the control **produces**. The control writes it; the maker only reads it. | **Hidden** from the input panel; readable in Power Fx as `Self.\u003CName>`. | declared in `IOutputs`; returned from `getOutputs()`; never read from `context.parameters`. |\n| `input` | A **read-only** input. The maker provides it — a static value (`default-value`) or a bound field — and the control reads but never writes it. | Editable field in the property panel. | read via `context.parameters.\u003CName>.raw`; not in `getOutputs()`. |\n| `bound` | A column the control can **change** — two-way. The control reads the field AND writes it back. At most one; **omit for action\u002Fconfig controls.** | Bound to a Dataverse column; `context.parameters.\u003CName>` also exposes `.formatted` \u002F `.security` \u002F `.attributes`. | read in `updateView` AND returned from `getOutputs()`; `notifyOutputChanged()` on change. |\n\n**Default for native-extension PCFs (the common case): NO `bound` property.** Most of these are action \u002F configuration \u002F status controls (trigger a native op on a maker-set input, surface the result). They use **`input` for what the maker sets** and **`output` for everything the control returns** — and omit `bound` entirely. Reaching for `bound` because a property feels like \"the main input\" is the #1 mistake: if the maker reads it in a formula it's an `output`; if the maker sets it it's an `input`. `bound` is *only* for a single column the control edits in place.\n\n> **The ONE allowed `bound` in a wrap PCF is the `\u003Cname>Json` diagnostic** added to the template above — it surfaces the raw bridge response for on-device debugging where the WebView console is unreachable (`shared\u002Fppmplugin-format.md` §2). That is the only exception; classify every *domain* property through the decision above and never reach for `bound` for them.\n\n> **Don't blindly inherit `bound` from ARCHITECTURE §8.1.** The design doc's \"§8.1 Bound input\" heading does not mean the property must be `usage=\"bound\"` — re-classify each property through the decision above. A value the native operation *returns* is an `output` even if §8 listed it under inputs.\n\n> **If you later need real localization,** the canonical PCF pattern is to put resource KEYS here (e.g. `\u003CPropertyName>_Display`) and create `strings\u002F\u003CPascal>PCF.1033.resx` (and additional `.resx` per locale) mapping keys to localized strings. For v0 with no i18n requirement, plain strings as shown above are correct and friendlier.\n\nAfter writing, validate the XML parses with `pac pcf build --no-restore` or by checking for the `\u003CPascal>PCF\u002Fgenerated\u002FManifestTypes.d.ts` file that `pcf-scripts` generates on build.\n\n---\n\n## Step 5 — Write `index.ts`\n\n### Step 5.0 — Branch on ARCHITECTURE §6.0 visual style\n\nBefore generating, read ARCHITECTURE §6.0 to know which visual style the PCF should render:\n\n| §8.0 value | Generated UI shape |\n|---|---|\n| `minimal` (default) | Single themed button. Click → `onTrigger()`. Outputs are read by the maker's Power Fx; the PCF itself doesn't render them. |\n| `with-preview` | Button + preview pane. Preview is an `\u003Cimg>` (for image outputs like base64 PNG \u002F data URI), `\u003Cdiv>` (for text outputs like scan result), or `\u003Cspan>` (for status). Preview reads from the success-output field and updates in `updateView` when the underlying value changes. |\n| `inline-surface` | Custom — the PCF renders an interactive surface itself rather than triggering a native modal. v0 emits a `\u002F\u002F TODO: design the inline surface` placeholder and STOPs with `DONE_WITH_CONCERNS`. |\n\nThe skeleton below is **for `minimal` mode**. Adapt for `with-preview` by adding a preview element and a `renderPreview()` method called from `updateView` + after `setSuccess`. For `inline-surface`, the skeleton doesn't apply — see the v1+ guidance.\n\n\n\n**Generate complete working code, not a skeleton with placeholders.** Every line in `index.ts` is derived from a specific source — and dispatch args \u002F response shapes come from the **native ground-truth contract resolved in Step 1.5**, not directly from the PRD. PRD describes intent; `manifest.json` + native source is what the running app actually exchanges.\n\n| Block in `index.ts` | Derived from |\n|---|---|\n| `COMPOSITE_KEY` + `METHOD` constants | `manifest.json` → `COMPOSITE_KEY = \"\u003Cname>\u002F\u003Creceiver>\"` (composite routing key) and `METHOD = \"\u003Cone of receivers[].methods>\"`. The composite key MUST match the receiver the manifest registers. |\n| Types | `\u003CPascal>Request`, `\u003CPascal>Response` defined inline in this file (the native-only bundle ships no shared TS `src\u002Ftypes.ts` to import — model the args the `@ReactMethod` parses and the `{status, result, error}` it resolves). |\n| Private output-field declarations | ARCHITECTURE §6.1 (output properties) (one private field per output, typed from §8.3's `Type` column, initialized to a safe default — `\"\"` for text, `0` for numbers, `false` for boolean) — plus the `\u003Cname>Json` raw-response diagnostic field. |\n| `applyStyles()` body | ARCHITECTURE §6.1 (configurable inputs) (one assignment per configurable input — button text, background, foreground, padding, etc.) using the actual property names from §8.2 |\n| `onTrigger()` payload-build (dispatch args) | **`\u003CPascal>Request`** (the object the `@ReactMethod` \u002F `RCT_EXPORT_METHOD` reads as its one `ReadableMap`\u002F`NSDictionary` param). ARCHITECTURE §6.1 (bound input) names which configurable\u002Fbound input flows into which field. It rides in the `sendAsync` envelope **`{ method: METHOD, args: [request] }`** — a **RAW object** (the PCF does NOT stringify it; `sendAsync` does) AND the inner `args` MUST be an array (§2). |\n| `onTrigger()` outcome branch | Single nested if\u002Felse covering four cases of the two-level error model, **each passing both a code AND a message to `setError`**: ① bridge OK + `payload.status === \"ok\"` → `setSuccess(payload.result)`. ② bridge OK + `payload.status === \"error\"` → `setError(payload.error, payload.message ?? \"\")` (the native-supplied human-readable reason). ③ bridge OK + payload shape unrecognized (even after `extractResponse` unwraps the wrap `message` container) → `setError(\"UNEXPECTED_PAYLOAD\", \"native response shape not recognized: \" + this.\u003Cname>Json.slice(0, 200))` — surface the RAW wire string, not the post-parse object. ④ `sendAsync` status !== \"ok\" \u002F parse-fail \u002F no host global → `setError(\"BRIDGE_FAILED\", \u003Cresult.error \u002F reason>)` \u002F `setError(\"PARSE\", \u003Craw string that failed to parse>)` \u002F `setError(\"NOT_IN_WRAP\", \u003Creason>)`. The RAW wire response is ALSO surfaced via the `\u003Cname>Json` output. Single `notifyOutputChanged()` at the end. |\n| `setSuccess(result)` body | `\u003CPascal>Response[\"result\"]` → ARCHITECTURE §6.1 (output properties). One assignment per §8.3 output, sourced from the corresponding response field. Sets `status=\"ok\"` and **clears `errorCode=\"\"` and `errorMessage=\"\"`**. |\n| `setError(code, message)` body | ARCHITECTURE §5 (codes) + ARCHITECTURE §6.3 (error UX mapping) (UX per code). Simplest form: zero out result fields, set `status=\"error\"`, `errorCode=code`, **`errorMessage=message`** (the human-readable reason — never drop it). If ARCHITECTURE §6.3 says specific codes need different output UX (e.g. USER_CANCELLED → status=\"cancelled\"), branch inside `setError`; still set `errorMessage`. |\n| `getOutputs()` body | ARCHITECTURE §6.1 (output properties) — one returned entry per output, reading the private field. **MUST include `Status`, `ErrorCode`, and `ErrorMessage`** (and the `\u003Cname>Json` raw output) so the failure is visible in Power Fx with no native debugger. |\n\n### The skeleton (with derivation rules inline)\n\nReplace the default scaffolded `pcf\u002F\u003CPascal>PCF\u002F\u003CPascal>PCF\u002Findex.ts` with this structure, **substituting every value from the PRD**:\n\n```ts\nimport { IInputs, IOutputs } from \".\u002Fgenerated\u002FManifestTypes\";\n\n\u002F\u002F Domain contract — modeled INLINE. A native-only .ppmplugin ships NO shared TS layer,\n\u002F\u002F so there's nothing to import: \u003CPascal>Request is the args the @ReactMethod parses,\n\u002F\u002F \u003CPascal>Response is the {status, result, error} object it resolves. Mirror the module.\ninterface \u003CPascal>Request { \u002F* one field per dispatch arg the @ReactMethod parses *\u002F }\ninterface \u003CPascal>Response { status: \"ok\" | \"error\"; result?: Record\u003Cstring, unknown>; error?: string; message?: string; }\n\u002F\u002F   error   — machine code (present when status === \"error\"); the PCF branches on it.\n\u002F\u002F   message — HUMAN-READABLE failure reason (present when status === \"error\"); the PCF surfaces it as ErrorMessage.\n\n\u002F\u002F Bridge declaration — the wrap host injects `window.PowerApps.NativeExtension`\n\u002F\u002F onto the Canvas WebView at boot (when CordovaV2 is enabled). The PCF dispatches\n\u002F\u002F through its `sendAsync` global — it must NEVER call `cordova.exec` directly (the\n\u002F\u002F raw `cordova` global is NOT exposed to the PCF sandbox, so a direct call is a\n\u002F\u002F silent no-op on device, worst on Android). See shared\u002Fppmplugin-format.md §2.\n\u002F\u002F\n\u002F\u002F Type it with a local ambient declaration in PowerAppsNativeExtension.d.ts (emitted\n\u002F\u002F alongside this file) so the PCF stays host-agnostic and pins no SDK package.\n\n\u002F\u002F deepParse: the wrap host double-\u002Ftriple-stringifies the bridge response, so peel string\n\u002F\u002F layers (bounded) until we reach an object. This is a BOUNDED helper used by extractResponse\n\u002F\u002F to reach the container\u002Fpayload — NOT a blind transport walk used on its own.\n\u002F\u002F See shared\u002Fppmplugin-format.md §2 \"Wrap-bridge response quirks\".\nfunction deepParse(v: unknown, max = 4): unknown {\n  let cur = v;\n  for (let i = 0; i \u003C max && typeof cur === \"string\"; i++) {\n    try { cur = JSON.parse(cur); } catch { break; }\n  }\n  return cur;\n}\n\n\u002F\u002F extractResponse: the wrap transport ALSO wraps the module's JSON in a container object,\n\u002F\u002F nesting it (still stringified) under a `message` key:\n\u002F\u002F   {\"isUpdate\":false,\"message\":\"{\\\"status\\\":\\\"ok\\\",\\\"result\\\":{…}}\"}\n\u002F\u002F A bare parse lands on {isUpdate, message} (no top-level `status`) → UNEXPECTED_PAYLOAD\n\u002F\u002F even though native succeeded. So peel string layers, THEN unwrap the container: probe\n\u002F\u002F `message` (the confirmed wrap key) first, then defensive fallbacks, accepting the first\n\u002F\u002F nested value that has a top-level `status`. When result.data already IS the {status,…}\n\u002F\u002F object (the simple already-unwrapped case), the first check returns it directly — so this\n\u002F\u002F is a strict superset of a single guarded parse. See shared\u002Fppmplugin-format.md §2.\nfunction extractResponse(raw: unknown): unknown {\n  const top = deepParse(raw);\n  if (top && typeof top === \"object\" && \"status\" in top) return top;\n  if (top && typeof top === \"object\") {\n    for (const k of [\"message\", \"result\", \"data\", \"value\", \"response\", \"body\", \"payload\"]) {\n      if (k in (top as Record\u003Cstring, unknown>)) {\n        const inner = deepParse((top as Record\u003Cstring, unknown>)[k]);\n        if (inner && typeof inner === \"object\" && \"status\" in inner) return inner;\n      }\n    }\n  }\n  return top;   \u002F\u002F fall through — UNEXPECTED_PAYLOAD surfaces the raw string for diagnosis\n}\n\nconst COMPOSITE_KEY = \"\u003Cname>\u002F\u003Creceiver>\";   \u002F\u002F manifest.json — composite routing key; MUST match the receiver the manifest registers\nconst METHOD = \"\u003Cmethod>\";                   \u002F\u002F manifest.json receivers[].methods — a real @ReactMethod \u002F RCT_EXPORT_METHOD name\n\nexport class \u003CPascal>PCF implements ComponentFramework.StandardControl\u003CIInputs, IOutputs> {\n  private container!: HTMLDivElement;\n  private notifyOutputChanged!: () => void;\n  private context!: ComponentFramework.Context\u003CIInputs>;\n  private button!: HTMLButtonElement;\n  private seq = 0;                          \u002F\u002F per-tap correlation id for the structured logs\n\n  \u002F\u002F ── Structured JSON logging (crash-proof) — the only way to diagnose on-device. ──\n  \u002F\u002F On a release wrap build the WebView console is unreachable, so every dispatch step\n  \u002F\u002F emits a single-line JSON record (grep-able if a console IS attached, and the raw\n  \u002F\u002F response is ALSO surfaced via the usage=\"bound\" \u003Cname>Json output for Power Fx).\n  private static readonly LOG_TAG = \"\u003CPascal>PCF\";\n  private logDebug(event: string, data?: Record\u003Cstring, unknown>): void {\n    try { console.log(`[${\u003CPascal>PCF.LOG_TAG}] ` + JSON.stringify({ ts: Date.now(), level: \"debug\", event, ...data })); } catch { \u002F* logging must never throw *\u002F }\n  }\n  private logError(event: string, data?: Record\u003Cstring, unknown>): void {\n    try { console.error(`[${\u003CPascal>PCF.LOG_TAG}] ` + JSON.stringify({ ts: Date.now(), level: \"error\", event, ...data })); } catch { \u002F* logging must never throw *\u002F }\n  }\n\n  \u002F\u002F ── Outputs: one private field per ARCHITECTURE §6.1 (output properties) row ──\n  \u002F\u002F Emit one declaration per output, typed from §8.3's \"Type\" column (mapping the\n  \u002F\u002F module's response field type to its PCF property type, inverse direction),\n  \u002F\u002F initialized to a safe default for that type.\n  \u002F\u002F\n  \u002F\u002F Defaults by type:\n  \u002F\u002F   SingleLine.Text \u002F .URL \u002F .Email → \"\"\n  \u002F\u002F   Whole.None \u002F Decimal             → 0\n  \u002F\u002F   TwoOptions                       → false\n  \u002F\u002F   DateAndTime.*                    → new Date(0)   (or a sentinel; ARCHITECTURE §6.1 (output properties) may specify)\n  \u002F\u002F\n  \u002F\u002F Shape: `private \u003CfieldName>: \u003CtsType> = \u003Cdefault>;`\n  \u002F\u002F Always include these diagnostic fields (back the standard outputs):\n  \u002F\u002F   private status: string = \"idle\";       \u002F\u002F \"idle\" | \"ok\" | \"error\" | extension-specific values\n  \u002F\u002F   private errorCode: string = \"\";        \u002F\u002F empty in success state; the machine code in error state\n  \u002F\u002F   private errorMessage: string = \"\";     \u002F\u002F empty in success state; the HUMAN-READABLE reason in error state\n  \u002F\u002F                                           \u002F\u002F (the field the maker\u002Fsupport reads first — no native debugger on device)\n  \u002F\u002F   private \u003Cname>Json: string = \"\";    \u002F\u002F RAW bridge response (wire string, before extractResponse) — the\n  \u002F\u002F                                        \u002F\u002F usage=\"bound\" on-device diagnostic; surfaced via Self.\u003Cname>Json\n\n  public init(\n    context: ComponentFramework.Context\u003CIInputs>,\n    notifyOutputChanged: () => void,\n    _state: ComponentFramework.Dictionary,\n    container: HTMLDivElement,\n  ): void {\n    this.container = container;\n    this.notifyOutputChanged = notifyOutputChanged;\n    this.context = context;\n\n    this.button = document.createElement(\"button\");\n    this.button.className = \"pam-ext-trigger\";   \u002F\u002F scopes the injected \u003Cstyle> below\n    \u002F\u002F ── Default: fill the host-allocated rectangle ──────────────────────────\n    \u002F\u002F The maker resizes the control on the canvas; the host gives us the\n    \u002F\u002F resulting box via `container`. The button fills that box so the whole\n    \u002F\u002F control area is the tap target (no dead zone around a small button).\n    \u002F\u002F To make the control SMALLER by default, shrink the control's default\n    \u002F\u002F Width\u002FHeight in the canvas (see ARCHITECTURE §6 default size), NOT the\n    \u002F\u002F button — the button just fills whatever box it's given.\n    this.button.style.width = \"100%\";\n    this.button.style.height = \"100%\";\n    this.button.style.boxSizing = \"border-box\";\n    this.ensureStyleTag();   \u002F\u002F interaction states (hover\u002Factive\u002Fdisabled\u002Ffocus) — pseudo-classes need a \u003Cstyle>\n    this.applyStyles();\n    this.button.addEventListener(\"click\", () => { void this.onTrigger(); });\n    container.appendChild(this.button);\n    this.logDebug(\"init\", { inWrap: !!window.PowerApps?.NativeExtension, key: COMPOSITE_KEY, method: METHOD });\n  }\n\n  \u002F\u002F Inject a scoped \u003Cstyle> ONCE. Inline styles can't express :hover \u002F :active \u002F\n  \u002F\u002F :disabled \u002F :focus-visible, and a control with no interaction feedback reads\n  \u002F\u002F as \"dead\" on device. Keep the selectors scoped to .pam-ext-trigger so we never\n  \u002F\u002F leak styles into the host page. `--pam-accent` \u002F `--pam-fg` are set per-instance\n  \u002F\u002F in applyStyles(), so these rules follow the resolved (possibly maker-overridden) palette.\n  private ensureStyleTag(): void {\n    if (document.getElementById(\"pam-ext-trigger-style\")) return;\n    const s = document.createElement(\"style\");\n    s.id = \"pam-ext-trigger-style\";\n    s.textContent = `\n      .pam-ext-trigger {\n        font-family: \"Segoe UI\", -apple-system, BlinkMacSystemFont, Roboto, sans-serif;\n        font-size: 14px; font-weight: 600; cursor: pointer;\n        border-style: solid; border-width: 1px;\n        transition: filter .1s ease, box-shadow .1s ease;\n      }\n      .pam-ext-trigger:hover:not(:disabled)  { filter: brightness(0.94); }\n      .pam-ext-trigger:active:not(:disabled) { filter: brightness(0.88); }\n      .pam-ext-trigger:disabled              { opacity: .5; cursor: default; }\n      .pam-ext-trigger:focus-visible         { outline: 2px solid var(--pam-fg, #201f1e); outline-offset: 2px; }\n    `;\n    document.head.appendChild(s);\n  }\n\n  private applyStyles(): void {\n    const p = this.context.parameters;\n    const b = this.button;\n\n    \u002F\u002F ── 1. Themed defaults (a real control, not a raw browser button) ────────\n    \u002F\u002F A polished baseline the maker gets for free. Prefer the host Fluent theme\n    \u002F\u002F when the runtime exposes it, so the control looks native in maker portal\n    \u002F\u002F AND on mobile; fall back to these Fluent-ish hexes in Studio preview \u002F older hosts.\n    const theme = (this.context as unknown as { fluentDesignLanguage?: { palette?: Record\u003Cstring, string> } }).fluentDesignLanguage;\n    let accent = theme?.palette?.themePrimary ?? \"#0f6cbd\";   \u002F\u002F brand blue\n    let fg     = theme?.palette?.white ?? \"#ffffff\";           \u002F\u002F text on accent\n    let border = accent;                                        \u002F\u002F border matches accent by default\n    let radius = \"4px\";                                         \u002F\u002F Fluent-standard corner\n\n    \u002F\u002F ── 2. Maker overrides (the color + border \"options\") ────────────────────\n    \u002F\u002F Read ONLY if the corresponding configurable input exists in ARCHITECTURE §6.1.\n    \u002F\u002F Emit a line per knob the manifest declares (AccentColor \u002F TextColor \u002F BorderColor \u002F BorderRadius).\n    \u002F\u002F   accent = (p.AccentColor?.raw   || \"\").trim() || accent;\n    \u002F\u002F   fg     = (p.TextColor?.raw     || \"\").trim() || fg;\n    \u002F\u002F   border = (p.BorderColor?.raw   || \"\").trim() || border;\n    \u002F\u002F   radius = (p.BorderRadius?.raw != null ? p.BorderRadius.raw + \"px\" : radius);\n\n    \u002F\u002F ── 3. Guarantee legible contrast (accessibility, not optional) ──────────\n    \u002F\u002F If the maker set a background but no explicit text color, pick black\u002Fwhite\n    \u002F\u002F by luminance so text on the accent always clears WCAG AA (~4.5:1). Prevents\n    \u002F\u002F the classic \"pale button, invisible label\" a hand-set color causes.\n    if (!((p as Record\u003Cstring, { raw?: string }>).TextColor?.raw ?? \"\").trim()) {\n      fg = this.readableTextOn(accent);\n    }\n\n    b.style.backgroundColor = accent;\n    b.style.color = fg;\n    b.style.borderColor = border;\n    b.style.borderRadius = radius;\n    b.style.padding = \"8px 16px\";\n    b.style.setProperty(\"--pam-accent\", accent);\n    b.style.setProperty(\"--pam-fg\", fg);\n\n    \u002F\u002F ── 4. Label ─────────────────────────────────────────────────────────────\n    \u002F\u002F Button text from the §8.2 label input (or a sensible default). For a\n    \u002F\u002F toggleable control label it \"\u003CFunction> : \u003CState>\" (e.g. \"Location Tracking : On\")\n    \u002F\u002F so the tap-to-toggle affordance is discoverable — not a bare state word.\n    b.textContent = ((p as Record\u003Cstring, { raw?: string }>).ButtonLabel?.raw ?? \"\").trim() || \"\u003Cdefault label from PRD §6>\";\n\n    \u002F\u002F Map any remaining §8.2 configurable inputs to DOM properties here (one line each).\n    \u002F\u002F DO NOT set width\u002Fheight from inputs — the host box already sizes the control;\n    \u002F\u002F a width\u002Fheight input just fights the canvas resize handle.\n  }\n\n  \u002F\u002F Black or white text for the given background, chosen by relative luminance so\n  \u002F\u002F the label always clears WCAG AA contrast on the resolved accent color.\n  private readableTextOn(hex: string): string {\n    const m = \u002F^#?([0-9a-f]{6})$\u002Fi.exec(hex.trim());\n    if (!m) return \"#ffffff\";\n    const n = parseInt(m[1], 16);\n    const [r, g, bl] = [(n >> 16) & 255, (n >> 8) & 255, n & 255].map((c) => {\n      const s = c \u002F 255; return s \u003C= 0.03928 ? s \u002F 12.92 : Math.pow((s + 0.055) \u002F 1.055, 2.4);\n    });\n    const L = 0.2126 * r + 0.7152 * g + 0.0722 * bl;   \u002F\u002F relative luminance\n    return L > 0.4 ? \"#201f1e\" : \"#ffffff\";            \u002F\u002F dark text on light bg, white on dark\n  }\n\n  private async onTrigger(): Promise\u003Cvoid> {\n    \u002F\u002F ─────────────────────────────────────────────────────────────────────────\n    \u002F\u002F 1. Build the REQUEST args (the object the @ReactMethod parses)\n    \u002F\u002F ─────────────────────────────────────────────────────────────────────────\n    \u002F\u002F The request rides inside the sendAsync envelope as a RAW object:\n    \u002F\u002F   { method: METHOD, args: [request] }\n    \u002F\u002F   (the PCF does NOT stringify it — sendAsync does; inner args MUST be an array — §2).\n    \u002F\u002F\n    \u002F\u002F Shape: one assignment per \u003CPascal>Request field; source values from\n    \u002F\u002F   this.context.parameters.\u003C§8.2-name>.raw  (configurable inputs), or\n    \u002F\u002F   this.context.parameters.\u003C§8.1-name>.raw  (bound input)\n    const request: \u003CPascal>Request = { \u002F* one entry per \u003CPascal>Request field *\u002F };\n    const seq = ++this.seq;\n    this.logDebug(\"dispatch\", { seq, key: COMPOSITE_KEY, method: METHOD, request });\n\n    \u002F\u002F ─────────────────────────────────────────────────────────────────────────\n    \u002F\u002F 2. Dispatch via window.PowerApps.NativeExtension.sendAsync — invokeBridge\n    \u002F\u002F    resolves the parsed response object, or THROWS on a bridge\u002Ftransport\n    \u002F\u002F    failure or when the host global isn't available (Studio preview \u002F non-PAM host).\n    \u002F\u002F ─────────────────────────────────────────────────────────────────────────\n    let response: \u003CPascal>Response;\n    try {\n      response = await this.invokeBridge(request);\n      this.logDebug(\"bridge_returned\", { seq, status: response?.status, error: response?.error });\n    } catch (e) {\n      \u002F\u002F status!==\"ok\" \u002F parse-fail \u002F no host global. The thrown message carries the code.\n      const code = (e as Error)?.message === \"PARSE\" ? \"PARSE\"\n                 : (e as Error)?.message === \"NOT_IN_WRAP\" ? \"NOT_IN_WRAP\"\n                 : \"BRIDGE_FAILED\";\n      \u002F\u002F Message: prefer the raw wire string (set on this.\u003Cname>Json by invokeBridge) so a\n      \u002F\u002F transport\u002Fparse failure is debuggable from Power Fx; fall back to the error string.\n      const reason = this.\u003Cname>Json || String((e as Error)?.message ?? e);\n      this.logError(\"bridge_error\", { seq, code, reason });\n      this.setError(code, reason);\n      this.notifyOutputChanged();\n      return;\n    }\n\n    \u002F\u002F ─────────────────────────────────────────────────────────────────────────\n    \u002F\u002F 3. Branch on the module's two-level {status, result, error} response.\n    \u002F\u002F    extractResponse already peeled the host's string layers and unwrapped the\n    \u002F\u002F    wrap `message` container — `response` is the module's {status,...} object.\n    \u002F\u002F ─────────────────────────────────────────────────────────────────────────\n    if (response?.status === \"ok\" && response.result) {\n      \u002F\u002F Operation succeeded. Map response.result.* → output fields.\n      this.logDebug(\"module_success\", { seq });\n      this.setSuccess(response.result);\n    } else if (response?.status === \"error\") {\n      \u002F\u002F Operation failed with a module-defined code (USER_CANCELLED, etc., per ARCHITECTURE §5).\n      \u002F\u002F Pass the native-supplied human-readable message through to the ErrorMessage output.\n      this.logError(\"module_error\", { seq, code: response.error, message: response.message });\n      this.setError(response.error ?? \"INTERNAL_ERROR\", response.message ?? \"\");\n    } else {\n      \u002F\u002F Bridge succeeded but the payload doesn't match \u003CPascal>Response shape — even after\n      \u002F\u002F extractResponse tried to unwrap the wrap `message` container. Surface the RAW wire\n      \u002F\u002F string (not the post-parse object) so the real shape is diagnosable from Power Fx.\n      this.logError(\"unexpected_payload\", { seq, raw: this.\u003Cname>Json });\n      this.setError(\"UNEXPECTED_PAYLOAD\", \"native response shape not recognized: \" + this.\u003Cname>Json.slice(0, 200));\n    }\n    this.notifyOutputChanged();\n    this.logDebug(\"final_state\", { seq, status: this.status, errorCode: this.errorCode, errorMessage: this.errorMessage });\n  }\n\n  private setSuccess(result: NonNullable\u003C\u003CPascal>Response[\"result\"]>): void {\n    \u002F\u002F ── Map the module's `result` payload → PCF output fields ──\n    \u002F\u002F For each output declared in ARCHITECTURE §6.1 (output properties), assign from the matching field in `result`\n    \u002F\u002F (the shape is the inline `\u003CPascal>Response[\"result\"]` type modeled at the top of this file).\n    \u002F\u002F Set the status\u002FerrorCode\u002FerrorMessage outputs to their success values.\n    \u002F\u002F\n    \u002F\u002F Shape:\n    \u002F\u002F   this.\u003C§8.3-field-A> = result.\u003Cresponse-field> ?? \u003Cdefault>;\n    \u002F\u002F   this.\u003C§8.3-field-B> = result.\u003Cresponse-field> ?? \u003Cdefault>;\n    \u002F\u002F   ...\n    \u002F\u002F   this.status = \"ok\";\n    \u002F\u002F   this.errorCode = \"\";\n    \u002F\u002F   this.errorMessage = \"\";   \u002F\u002F clear any prior failure's message\n    \u002F\u002F\n    \u002F\u002F One assignment per §8.3 row. If a §8.3 output has no matching source field\n    \u002F\u002F in \u003CPascal>Response[\"result\"], the scaffold STOPS in Step 1.5 — it shouldn't reach here.\n    \u002F\u002F (this.\u003Cname>Json is set in invokeBridge from the raw response — don't touch it here.)\n  }\n\n  private setError(code: string, message: string): void {\n    \u002F\u002F ── Map error code → output fields per ARCHITECTURE §6.3 (error UX mapping) ──\n    \u002F\u002F Default behavior (covers most extensions): zero out result-bearing outputs, set\n    \u002F\u002F status=\"error\", surface the code AND the human-readable message as-is.\n    \u002F\u002F\n    \u002F\u002F Shape:\n    \u002F\u002F   this.\u003C§8.3-result-field-A> = \u003Cdefault>;\n    \u002F\u002F   this.\u003C§8.3-result-field-B> = \u003Cdefault>;\n    \u002F\u002F   ...\n    \u002F\u002F   this.status = \"error\";\n    \u002F\u002F   this.errorCode = code;\n    \u002F\u002F   this.errorMessage = message;   \u002F\u002F NEVER drop this — it's what makes the failure debuggable on-device\n    \u002F\u002F\n    \u002F\u002F If ARCHITECTURE §6.3 (error UX mapping) specifies that certain codes need different output state (e.g. a code\n    \u002F\u002F that maps to status=\"cancelled\" instead of \"error\", or a code that preserves a\n    \u002F\u002F partial result), branch on the code value:\n    \u002F\u002F\n    \u002F\u002F   switch (code) {\n    \u002F\u002F     case \"\u003CCODE_A>\":\n    \u002F\u002F       \u002F* ARCHITECTURE §6.3 (error UX mapping) row for CODE_A *\u002F\n    \u002F\u002F       break;\n    \u002F\u002F     case \"\u003CCODE_B>\":\n    \u002F\u002F       \u002F* ARCHITECTURE §6.3 (error UX mapping) row for CODE_B *\u002F\n    \u002F\u002F       break;\n    \u002F\u002F     default:\n    \u002F\u002F       \u002F* the standard zero-and-surface mapping above *\u002F\n    \u002F\u002F   }\n    \u002F\u002F\n    \u002F\u002F The codes that reach here include BOTH bridge\u002Ftransport codes (BRIDGE_FAILED, PARSE,\n    \u002F\u002F UNEXPECTED_PAYLOAD, NOT_IN_WRAP) and module-defined codes from ARCHITECTURE §5.\n    \u002F\u002F Cover what ARCHITECTURE §6.3 (error UX mapping) specifies; let the default branch handle\n    \u002F\u002F anything else.\n  }\n\n  \u002F**\n   * Bridge transport. Dispatches the composite key COMPOSITE_KEY (\"\u003Cname>\u002F\u003Creceiver>\")\n   * to NativeModules.\u003CnativeModule>.\u003CMETHOD> through the host-injected\n   * `window.PowerApps.NativeExtension.sendAsync` global, and RESOLVES the parsed\n   * \u003CPascal>Response object.\n   *\n   * REJECTS with:\n   *   - new Error(\"BRIDGE_FAILED\") when sendAsync resolves status !== \"ok\" (transport\u002Fproxy\n   *                                failure, e.g. \"native module 'X' not loaded\")\n   *   - new Error(\"PARSE\")         when the resolved data can't be reduced to an object\n   *   - new Error(\"NOT_IN_WRAP\")   when the host global is absent — Studio preview, a non-PAM\n   *                                web host, or CordovaV2 disabled. Do NOT fall back to\n   *                                cordova.exec — it is not exposed to the PCF sandbox.\n   *\n   * sendAsync resolves { status, data?, error? }. On status===\"ok\", `data` is the native\n   * method's resolved string; the host both re-stringifies it AND wraps it in a { isUpdate,\n   * message } transport container, so the raw is run through extractResponse (parse + unwrap\n   * the `message` container). The raw string is also captured into the usage=\"bound\" \u003Cname>Json\n   * output so a maker can read it on-device via Self.\u003Cname>Json when the WebView console is unreachable.\n   *\n   * The envelope is a RAW object { method, args: [request] } — the PCF does NOT stringify it;\n   * sendAsync does that internally. The inner `args` MUST be an array (the proxy spreads it\n   * positionally via fn.apply), so the single request object rides at args[0] → the native\n   * method's one ReadableMap\u002FNSDictionary param. There is no SDK-side timeoutMs to tune here.\n   *\u002F\n  private async invokeBridge(payload: \u003CPascal>Request): Promise\u003C\u003CPascal>Response> {\n    const bridge = window.PowerApps?.NativeExtension;\n    if (!bridge || typeof bridge.sendAsync !== \"function\") {\n      throw new Error(\"NOT_IN_WRAP\");\n    }\n    \u002F\u002F Envelope is a RAW object; args MUST be an array (one request object at args[0]).\n    const result = await bridge.sendAsync(COMPOSITE_KEY, { method: METHOD, args: [payload] });\n    \u002F\u002F Capture the raw wire response for the on-device diagnostic output.\n    this.\u003Cname>Json = typeof result === \"string\" ? result : JSON.stringify(result);\n    if (!result || result.status !== \"ok\") {\n      throw new Error((result && result.error) || \"BRIDGE_FAILED\");\n    }\n    \u002F\u002F result.data is re-stringified AND may be wrapped in the wrap transport's response\n    \u002F\u002F container ({ isUpdate, message:\"\u003Cjson of {status,result}>\" }). extractResponse parses\n    \u002F\u002F it and, if there's no top-level `status`, unwraps the `message` container — NOT a bare\n    \u002F\u002F parse (which would land on {isUpdate, message} → UNEXPECTED_PAYLOAD though native succeeded). §2.\n    const parsed = extractResponse(result.data);\n    if (parsed && typeof parsed === \"object\") {\n      return parsed as \u003CPascal>Response;\n    }\n    throw new Error(\"PARSE\");\n  }\n\n  public updateView(context: ComponentFramework.Context\u003CIInputs>): void {\n    this.context = context;\n    this.applyStyles();\n  }\n\n  public getOutputs(): IOutputs {\n    return {\n      \u002F\u002F ── One returned key per ARCHITECTURE §6.1 (output properties) output, reading the private field ──\n      \u002F\u002F Shape: `\u003C§8.3-property-name>: this.\u003CprivateFieldName>,`\n      \u002F\u002F The property name on the LEFT matches `\u003Cproperty name=\"...\">` in ControlManifest.Input.xml.\n      \u002F\u002F The field on the RIGHT is the private declared at the top of the class.\n      \u002F\u002F MUST include the three standard diagnostic outputs so the failure is visible in\n      \u002F\u002F Power Fx with no native debugger:\n      \u002F\u002F   Status: this.status,\n      \u002F\u002F   ErrorCode: this.errorCode,\n      \u002F\u002F   ErrorMessage: this.errorMessage,\n      \u002F\u002F ALSO include the usage=\"bound\" diagnostic so the raw response reaches Self.\u003Cname>Json:\n      \u002F\u002F   \u003Cname>Json: this.\u003Cname>Json,\n    };\n  }\n\n  public destroy(): void { \u002F* No-op for v0. *\u002F }\n}\n```\n\n### Adapting the skeleton for `with-preview` visual style\n\nWhen ARCHITECTURE §6.0 = `with-preview`, extend the skeleton above with a preview pane:\n\n1. **Add a preview element** in `init`:\n   ```typescript\n   this.previewEl = document.createElement(\"img\");   \u002F\u002F \u003Cimg> for image outputs; \u003Cdiv> for text\n   this.previewEl.style.maxWidth = \"100%\";\n   this.previewEl.style.maxHeight = \"200px\";   \u002F\u002F or per ARCHITECTURE §6.0 sizing\n   this.previewEl.style.display = \"none\";       \u002F\u002F hidden until first result\n   container.appendChild(this.previewEl);\n   ```\n\n2. **Update `setSuccess` to populate the preview**:\n   ```typescript\n   private setSuccess(result: \u003CPascal>Response[\"result\"]): void {\n     \u002F\u002F ... map to outputs as before ...\n     this.\u003CsuccessField> = result.\u003Cfield>;\n     \u002F\u002F Show preview\n     if (this.previewEl instanceof HTMLImageElement) {\n       this.previewEl.src = result.\u003CimageField>;\n       this.previewEl.style.display = \"block\";\n     }\n   }\n   ```\n\n3. **Update `setError` to hide the preview** (or show a placeholder):\n   ```typescript\n   private setError(code: string, message: string): void {\n     \u002F\u002F ... map error state as before (set status\u002FerrorCode\u002FerrorMessage) ...\n     this.previewEl.style.display = \"none\";\n   }\n   ```\n\n4. **`updateView` re-renders the preview** from the current output field (in case the maker changed bound input via Power Fx):\n   ```typescript\n   public updateView(context: ComponentFramework.Context\u003CIInputs>): void {\n     this.context = context;\n     this.applyStyles();\n     if (this.\u003CsuccessField>) {\n       this.renderPreview(this.\u003CsuccessField>);\n     }\n   }\n   ```\n\nThe preview type follows the output type per ARCHITECTURE §6.1 (output properties) → §8.0 mapping:\n- Base64 PNG \u002F data URI → `\u003Cimg>` element, `src = result.\u003Cfield>`\n- SVG string → `\u003Cdiv>` with `innerHTML` (caution: only if the SVG source is trusted — sanitize otherwise)\n- Plain text result → `\u003Cspan>` with `textContent`\n- Numeric output → `\u003Cspan>` with formatted number\n\n### Adapting for `inline-surface` (v1+ pattern)\n\nEmit a minimal skeleton with the trigger button replaced by a `\u003Cdiv>` for the interactive surface, plus a `\u002F\u002F TODO: implement the inline surface per ARCHITECTURE §6.0` comment. STOP with `DONE_WITH_CONCERNS: ARCHITECTURE §6.0 inline-surface style requires custom UI design beyond the v0 scaffold template`. The engineer fills in the interactive surface; the bridge wiring (`sendAsync` dispatch, `extractResponse`, error handling, output mapping) stays the same.\n\n### Step 5.5 — Emit the ambient `PowerAppsNativeExtension.d.ts`\n\nThe `index.ts` skeleton calls `window.PowerApps.NativeExtension.sendAsync`, so the TypeScript build needs the global typed. **Do NOT import the host SDK** (the PCF must stay host-agnostic and pin no SDK package). Write a one-file ambient declaration next to `index.ts` at `pcf\u002F\u003CPascal>PCF\u002FPowerAppsNativeExtension.d.ts`:\n\n```ts\n\u002F\u002F! Ambient declaration for the host-injected `window.PowerApps.NativeExtension`\n\u002F\u002F! global. Wired at boot by PAM's published-app-loader (when CordovaV2 is enabled).\n\u002F\u002F! A local declaration (rather than importing an SDK package) keeps the PCF\n\u002F\u002F! host-agnostic — any PCF can call `sendAsync` without pinning the SDK.\n\ninterface PowerAppsNativeExtensionResult {\n  status: \"ok\" | \"error\";\n  data?: unknown;\n  error?: string;\n}\n\ninterface Window {\n  PowerApps?: {\n    NativeExtension?: {\n      sendAsync(\n        extensionName: string,\n        payload: unknown,\n        options?: { timeoutMs?: number },\n      ): Promise\u003CPowerAppsNativeExtensionResult>;\n    };\n  };\n}\n```\n\nThis is a **required emit** — without it, `npm run build` (Step 6) fails to type-check `window.PowerApps`. `pcf-scripts` picks up any `*.d.ts` under the control folder automatically, so no `tsconfig` change is needed.\n\n### Hard rules for index.ts generation\n\n- **No `\u003Cplaceholder>` text** in the emitted file. Every value is substituted from the PRD.\n- **No `\u002F\u002F TODO`** comments. If a required mapping isn't in the PRD, STOP with `NEEDS_CONTEXT: PRD §\u003Cn>.\u003Cn> is incomplete for PCF code generation — re-run \u002Fdesign-native-extension-feature` and name the missing column.\n- **Dispatch via `sendAsync`, never `cordova.exec`.** The emitted `invokeBridge` MUST call `window.PowerApps.NativeExtension.sendAsync` with a RAW `{ method, args: [request] }` envelope. A direct `cordova.exec` call (or any `cordova.*` reference) in the PCF is a defect — it is not exposed to the PCF sandbox and fails silently on device (worst on Android).\n- **Cover every error code** from ARCHITECTURE §5 in `applyErrorState`. The default branch is for unknown codes only; don't skip cases by relying on the default.\n- **Verify symmetry with ARCHITECTURE §4.2.** Every output field declared in §8.3 must map to a response field in §5.2 (or to a derived constant from the error state). If §8.3 declares an output that §5.2 has no corresponding field for, STOP and ask the user.\n\n### Pattern coverage\n\n- **One-shot** — the structure above is sufficient. Generate complete code.\n- **Streaming** (§6 = streaming) — the `sendAsync` dispatch resolves one response; streaming would need the native module to push updates the PCF receives via an event subscription. The wrap bridge doesn't expose a clean PCF-side subscription API. For v0, emit the one-shot structure and add a `\u002F\u002F TODO (streaming): wire up update subscription when the wrap bridge exposes it` comment at the top of the file; STOP scaffolding with `DONE_WITH_CONCERNS: streaming PCF needs wrap-bridge support not yet available — emitted one-shot fallback`. Don't fabricate an API that doesn't exist.\n- **Two-way** (ARCHITECTURE §2 = two-way) — add a second button per operation that fires the follow-up message. Generate the second button + handler from ARCHITECTURE §3.\u003Cn>'s follow-up spec (which the design skill captures in the implementation walkthrough). If §3.\u003Cn> doesn't specify the follow-up surface, STOP with `NEEDS_CONTEXT: ARCHITECTURE §3.\u003Cn> doesn't specify the two-way UI affordance — re-run design walkthrough`.\n\nThe `NOT_IN_WRAP` rejection surfaces in the test harness (`pcf-scripts start watch` opens a browser preview where `window.PowerApps.NativeExtension` isn't injected) — that's expected. Don't try to mock the bridge in the harness; it's a known gap.\n\n---\n\n## Step 5b — Generate the manual-validation test harness\n\nGenerate `test-harness\u002F` (one level up from the PCF folder, at the extension repo root) so the engineer can build a real Canvas app that wires the PCF and confirm end-to-end behavior on a device.\n\nThere are two viable approaches; ship both, low-cost:\n\n### 5b.1 — A typed `test-harness\u002FREADME.md` with the exact Power Fx recipe\n\nWrite `test-harness\u002FREADME.md` containing step-by-step instructions, tailored from the PRD. Sections to include:\n\n```markdown\n# Test harness — \u003CHuman-Readable Name>\n\nManual validation recipe. Builds a Canvas app in Studio that wires the `\u003CPascal>PCF` control to confirm end-to-end behavior on a real wrap build.\n\n## Prerequisites\n- The dispatcher PCF is deployed to your environment: run `\u002Fpublish-pcf-companion` (handles publisher prefix selection, version bump, env confirmation, and the actual push).\n- A wrap build that has the `.ppmplugin` bundle loaded (the DEX\u002Fframework + `manifest.json` whose `\u003Cname>\u002F\u003Creceiver>` this PCF dispatches to) — see \u002Ftest-native-extension for the load recipe.\n- `pac auth list` shows your test environment as active.\n\n## Build the test app in Canvas Studio\n\n1. Open \u003Chttps:\u002F\u002Fmake.powerapps.com>; switch to your test environment.\n2. Apps → New app → Blank canvas → Phone layout.\n3. Insert → Custom → search `\u003CPascal>PCF` (under the `PowerApps` namespace).\n4. Drop it onto Screen1.\n5. In the right-side properties panel, set:\n   - `\u003CBoundName>` = \u003Cone realistic test value, e.g. an HTTPS URL or a base64 sample>\n   - `\u003Ceach configurable input>` = \u003Cleave default or set what ARCHITECTURE §6.1 (configurable inputs) says>\n6. Insert → Text label, name it `lblResult`. Set its `Text` property to:\n   `\u003CPascal>PCF1.\u003COutputName>` (the primary output from ARCHITECTURE §6.1 (output properties))\n7. (For more outputs:) Add one label per additional output property.\n\n## Run\n\n- Studio preview (▶): runs the PCF in the browser, where `window.PowerApps.NativeExtension` isn't injected; bridge calls reject with `NOT_IN_WRAP` — that's the expected signal that you need a real wrap build to test end-to-end.\n- Wrap on device: open the app in Power Apps Mobile (logged into the same tenant). The bridge dispatches, the native module fires, and the output property updates.\n\n## What to verify\n\nPer ARCHITECTURE §6.3 (error UX mapping) (Edge cases):\n\n| Trigger | Expected `\u003COutputName>` value |\n|---|---|\n| \u003Cerror code 1> | \u003Cexpected user experience> |\n| \u003Cerror code 2> | \u003Cexpected user experience> |\n| \u003Chappy path>   | \u003Cexpected user experience> |\n\n## Optional — save as a reusable .msapp\n\nIf you want to share the test app with another machine or check it into source control:\n\n```bash\n# Inside Studio: File → Save as → save to your Power Platform environment first.\n# Then export from Studio (File → See all versions → Download) — gives you a .msapp file.\n# Drop it into .\u002Ftest-harness\u002F\u003CPascal>.msapp and commit.\n```\n\nTo later unpack and edit as source:\n\n```bash\npac canvas unpack --msapp .\u002Ftest-harness\u002F\u003CPascal>.msapp --sources .\u002Ftest-harness\u002F\u003CPascal>-source\u002F\n```\n\nTo repack after editing:\n\n```bash\npac canvas pack --sources .\u002Ftest-harness\u002F\u003CPascal>-source\u002F --msapp .\u002Ftest-harness\u002F\u003CPascal>.msapp\n```\n```\n\nTailor the recipe to the PRD: substitute the actual bound input name + a realistic example value, list every configurable input from §8.2, list every output from §8.3, fill the verification table from §9 (one row per error code + the happy path).\n\n### 5b.2 — Skeleton Canvas source folder (best-effort)\n\nCreate `test-harness\u002Fcanvas-app-source\u002F` with the bare minimum Canvas source format:\n\n```\ntest-harness\u002Fcanvas-app-source\u002F\n├── CanvasManifest.json       ← minimal manifest with app name = \"\u003CPascal>-test\"\n├── Src\u002F\n│   ├── App.pa.yaml           ← App.OnStart = false; no special config\n│   └── Screen1.pa.yaml       ← \u003CPascal>PCF instance (compact default size) + lblResult bound to its output\n└── README.md                 ← \"Run: pac canvas pack --sources . --msapp ..\u002F\u003CPascal>.msapp\"\n```\n\n**Give the control instance a compact default size** in `Screen1.pa.yaml` (`Width: =200`, `Height: =44` for a button-style control — adjust per ARCHITECTURE §6 if it specifies a preferred footprint). The button fills the control box (`width\u002Fheight: 100%`), so the control's `Width`\u002F`Height` here ARE its rendered size — set them small so the sample app shows a normal-sized button, not one that fills a tall default cell.\n\n> ⚠️ **Canvas source format is fragile.** `Pa.yaml` is sensitive to indentation and specific control-property casing. The skeleton you generate may not import cleanly into Studio. If `pac canvas pack` errors at this step, **fall back to the manual-in-Studio recipe in 5b.1** — that's the supported v0 path. Treat the generated source folder as a convenience, not a guarantee.\n\nAfter 5b.2 succeeds (or is skipped on failure), the engineer always has 5b.1 as a fallback.\n\nUpdate `.gitignore` to allow the harness sources but exclude the `.msapp` binary by default (it's user-environment-specific):\n\n```\n# Test harness — source is committed, binary is per-machine\ntest-harness\u002F*.msapp\n```\n\n---\n\n## Step 5.7 — Self-verify the round-trip contract (before building)\n\nThe bridge round-trip is the class of bug that **passes `npm run build` and every zip\u002Fmanifest\u002FDEX check, then fails silently on device** — a composite key that doesn't match the manifest receiver, a `cordova.exec` call the sandbox drops, a pre-stringified envelope, a bare `args` object, or a success path that doesn't unwrap the wrap `message` container. Historically these were only caught at the very end by `\u002Faudit-ppmplugin` Category F. Run them **here**, over the `index.ts` you just emitted, so a gap is caught at generation instead.\n\nThis is **Gate 11 (PCF ↔ native round-trip contract)** from [`shared\u002Fself-critique-protocol.md`](..\u002F..\u002Fshared\u002Fself-critique-protocol.md). Re-read the `index.ts` (and `PowerAppsNativeExtension.d.ts`) fresh from disk, then check each invariant against the **resolved contract from Step 1.5** and [`shared\u002Fppmplugin-format.md §2`](..\u002F..\u002Fshared\u002Fppmplugin-format.md):\n\n| Check | Assert | Severity |\n|---|---|---|\n| Composite key | `COMPOSITE_KEY` in `index.ts` == `\u003Cmanifest.name>\u002F\u003Creceivers[].name>` resolved in Step 1.5; `METHOD` ∈ `receivers[].methods` | blocker |\n| Transport | dispatch is `window.PowerApps.NativeExtension.sendAsync`; NO `cordova.exec` \u002F `cordova.*` anywhere in `index.ts` | blocker |\n| Envelope | `sendAsync` payload is a **raw** `{ method: METHOD, args: [request] }` object — NOT `JSON.stringify(...)`'d | blocker |\n| Args array | the inner `args` is a JSON **array** with the request object at `args[0]` (not a bare object) | blocker |\n| Response unwrap | the success path calls `extractResponse` (parse `result.data` + unwrap the `{ isUpdate, message }` container), NOT a bare single `JSON.parse` | blocker |\n| Error mapping | every ARCHITECTURE §5 code AND the transport codes (`BRIDGE_FAILED`, `PARSE`, `UNEXPECTED_PAYLOAD`, `NOT_IN_WRAP`) reach a PCF output carrying **both** code and human message | concern |\n| Ambient global | `PowerAppsNativeExtension.d.ts` typing `window.PowerApps.NativeExtension.sendAsync` was emitted (Step 5.5) | concern |\n\n**Print a visible verdict block** (`shared-instructions.md §9.1`) and gate on it:\n\n```\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n Round-trip self-verify — \u003CPascal>PCF (dispatches \u003Cname>\u002F\u003Creceiver>)\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n ? ✓ Composite key matches manifest receiver; METHOD ∈ receivers[].methods\n ? ✓ sendAsync only (no cordova.exec); raw envelope; args is [request]\n ? ✓ extractResponse unwraps the { isUpdate, message } container\n ? ✓ All §5 + transport error codes map to outputs with a message\n```\n\n- Any **blocker** → **fix the emitted `index.ts` inline** (these are mechanical wiring fixes, not design choices — align the key, replace a `cordova.exec` with `sendAsync`, drop a stray `JSON.stringify`, wrap `args` in an array, route the success path through `extractResponse`), re-read, re-check. Only if a blocker is a genuine drift between the manifest and native source (not a PCF-emit mistake) → STOP with `NEEDS_CONTEXT` and route back per the Step 1.5 drift table.\n- Any **concern** → note it in the final summary and in `.extension-state.md`.\n- All clear → proceed to Step 6.\n\nThis does not replace `\u002Faudit-ppmplugin` Category F (the final gate over the built artifact) — it front-loads the same reasoning so the round-trip is right before the PCF is even built.\n\n---\n\n## Step 6 — Install and smoke-build\n\nInside `pcf\u002F\u003CPascal>PCF\u002F`:\n\n```bash\nnpm install              # PCF tooling is npm; do NOT use pnpm here\nnpm run build            # runs `pcf-scripts build`\n```\n\nThis regenerates `\u003CPascal>PCF\u002Fgenerated\u002FManifestTypes.d.ts` from the manifest, type-checks `index.ts`, and bundles. Output lands at `pcf\u002F\u003CPascal>PCF\u002Fout\u002F` (gitignored).\n\nIf `npm install` fails:\n- Network \u002F feed issues are less likely than for the extension's pnpm install — PCF tooling pulls from the public npm registry, not Azure Artifacts.\n- If it fails anyway, surface the failing line and direct to standard `npm install` troubleshooting.\n\nIf `npm run build` fails:\n- Manifest XML errors — pcf-scripts surfaces them with line numbers. Re-write the offending property.\n- TypeScript errors in `index.ts` — most likely a property name mismatch between the manifest and the code. Re-read the manifest's `\u003Cproperty name=\"...\">` values and align.\n\nDO NOT mark the PCF scaffold as complete in `.extension-state.md` until `npm run build` passes.\n\n### Deployment to a Power Platform environment\n\nNOT done in this skill. Run `\u002Fpublish-pcf-companion` when ready — it handles the just-in-time deploy prereq check (.NET SDK + active `pac auth`), the env-confirmation gate, and `pac pcf push`. This separation lets the engineer iterate on `index.ts` and the manifest locally without needing auth set up, then deploy when the work is ready to share.\n\n---\n\n## Step 7 — Update state and summarize\n\nUpdate `.\u002F.extension-state.md`:\n- Phase: `Last completed: scaffold-pcf` \u002F `Next: \u002Ftest-native-extension`.\n- Add a line under \"Validation history\": `PCF build (Layer 4): pass — \u003CISO timestamp>`.\n- Add a line: `Test harness: README ✓` and, if 5b.2 succeeded, `canvas-app-source ✓` (or `canvas-app-source skipped — pa.yaml generation failed` if it didn't).\n- Do NOT touch the \"PCF deployments\" section — deploy is `\u002Fpublish-pcf-companion`'s responsibility.\n\nPrint:\n\n```\nPCF scaffold complete\n─────────────────────\nFolder: pcf\u002F\u003CPascal>PCF\u002F\nManifest properties: 1 bound (\u003Cname>Json diagnostic), \u003Ccount input> input, \u003Ccount output> output\nDispatches: \u003Cname>\u002F\u003Creceiver> → NativeModules.\u003CnativeModule>.\u003Cmethod> via window.PowerApps.NativeExtension.sendAsync\nBuild: npm install ✓ | npm run build ✓\nDeployment: not yet deployed (run \u002Fpublish-pcf-companion when ready)\n\nNext steps\n──────────\n1. Run \u002Ftest-native-extension to validate the contract now that native + manifest + PCF all exist (Layer 0 cross-check + Layer 4 PCF compile) — cheap pre-flight before the binary build.\n2. Run \u002Fgenerate-ppmplugin to produce the verified `.ppmplugin` binary bundle — it reads this PCF's composite key back so the manifest's receiver stays aligned.\n3. Run \u002Fpublish-pcf-companion to push the dispatcher PCF to a Power Platform environment (handles deploy prereq check, env confirmation, and `pac pcf push`).\n4. (Iterate) If ARCHITECTURE §6 changes, re-run \u002Fgenerate-pcf-companion — it will detect the existing folder and ask before regenerating.\n```\n\n---\n\n## Step 8 — Offer next-step skills\n\nPCF generation is the natural moment to make several decisions — the dispatcher PCF now exists, so building the `.ppmplugin` bundle and deploying are both in play. Per `shared\u002Fshared-instructions.md §9.1`, use `AskUserQuestion` with all plausible next skills as options (not 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 \u002Ftest-native-extension\"\n     description: \"Validate the contract now that all three sides exist — native module ↔ .\u002Fmanifest.json ↔ this PCF (Layer 0 cross-check: composite key, nativeModule, methods, request\u002Fresponse\u002Ferror parity) + Layer 4 PCF compile. Cheap structural pre-flight (seconds) that catches drift before the minutes-long binary build. Recommended next step.\"\n  2. \"Run \u002Fgenerate-ppmplugin\"\n     description: \"Produce the verified .ppmplugin binary bundle end-to-end (manifest → build → assemble → audit). Reads this PCF's composite key back so the manifest's receiver stays aligned.\"\n  3. \"Run \u002Fpublish-pcf-companion\"\n     description: \"Deploy the dispatcher PCF to a Power Platform environment for Studio testing. Three gates: publisher prefix, version bump, env confirmation.\"\n  4. \"Stay — I'll review the PCF code first\"\n     description: \"Skill exits. Inspect pcf\u002F\u003CX>PCF\u002Findex.ts and the manifest, decide what's next yourself.\"\n```\n\n**For options 1 (test), 2 (generate-ppmplugin), or 3 (publish):** **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 prereq check + gates.\n\n**For option 4 (stay):** print one line: `PCF scaffold complete. Run any of the suggested skills when you're ready.` Then proceed to return-status.\n\n(Need diagnostic logging? Just ask — `console.log` \u002F `NSLog` \u002F `Log.d` can be added to any code area on request; there's no dedicated skill for it. Note: in the wrap runtime a third-party control's native logs aren't surfaced through normal dev tooling, so logging is of limited use here anyway.)\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` | PCF generated, manifest matches PRD, `npm run build` passed. State file updated. |\n| `DONE_WITH_CONCERNS: \u003Clist>` | Build passed but with non-fatal warnings (e.g. streaming pattern requires UI work the scaffold left as TODO). |\n| `NEEDS_CONTEXT: \u003Cmissing>` | A required ARCHITECTURE §6 sub-section was incomplete and we couldn't proceed. |\n| `BLOCKED: \u003Creason>` | Prereq failed, `pac pcf init` failed, or `npm run build` 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** PCF code from a PRD. It's not a linter; it doesn't audit existing working code against the new template. When the skill is re-run on a repo that already has a `pcf\u002F` folder, the regenerate \u002F resume \u002F abort gate asks before overwriting — and the user should pick \"resume\" or \"abort\" unless they explicitly want a fresh template.\n\n**A diff between \"what we'd generate now\" and \"what exists\" is NOT a list of defects.** Existing PCF code that achieves the same runtime behavior through a different code shape is fine. Don't list stylistic deltas as issues. Flag only:\n\n- Code that doesn't compile (`npm run build` failing)\n- Code that produces incorrect outputs (wrong field names, missing error handling that lets crashes propagate)\n- Genuine dispatch-contract violations (wrong composite key, wrong `method` name, or a payload shape the `@ReactMethod` can't parse — such that runtime breaks)\n\nA `BridgeResponse` type defined with a different name is NOT a defect. An `applyErrorState(code)` method instead of `setSuccess`\u002F`setError` is NOT a defect. A hand-rolled unwrap that reaches the `{status,…}` object instead of `extractResponse` is NOT a defect. The template prefers one shape; equivalent shapes are not \"wrong.\"\n\n## Hard rules — correctness (these must be true for the PCF to work)\n\n- **PCF tooling uses npm, not pnpm** for the inner `pcf\u002F\u003CPascal>PCF\u002F` package. PCF ecosystem convention. Mixing produces lockfile chaos.\n- **Manifest namespace is always `PowerApps`.** Don't let the user override; this is how Canvas groups native-extension PCFs.\n- **The composite key (`COMPOSITE_KEY` constant) MUST equal `\u003Cname>\u002F\u003Creceiver>` from `manifest.json`** and match the receiver the manifest registers. Mismatch = the wrap host can't route, silent runtime failure.\n- **`METHOD` MUST be a real `@ReactMethod` \u002F `RCT_EXPORT_METHOD` name** from `receivers[].methods`. An unknown method = `method '\u003Cm>' not found` on device.\n- **All required fields from `\u003CPascal>Request` MUST be present in the dispatched args.** Missing fields = native parses `undefined` = INVALID_INPUT at runtime.\n- **`display-name-key` and `description-key` MUST be human-readable strings**, not programmer keys ending in `_Display` \u002F `_Desc`. Studio shows these verbatim when no `.resx` ships.\n- **Every output declared in ARCHITECTURE §6.1 (output properties) MUST be returned by `getOutputs()`.** TypeScript catches this via `IOutputs` typing; don't disable that.\n- **`npm run build` MUST pass** before declaring scaffold success.\n- **Manifest property types from ARCHITECTURE §6** must be PCF-valid (`SingleLine.Text`, `Whole.None`, etc.) — map the module's response field type to its PCF property type.\n- **Exactly ONE `usage=\"bound\"` property — the `\u003Cname>Json` raw-response diagnostic — and no other.** Every domain property is `input` (maker sets) or `output` (control produces).\n\n## Recommended template style (preferences for new scaffolds; existing code that works is fine)\n\nThese describe the cleanest shape for newly-generated PCF code. They're how this skill renders fresh output. Existing working code that takes a different path is **not** in violation — don't flag stylistic differences as defects when auditing.\n\n- **Prefer the `extractResponse` helper** over a raw `JSON.parse(result.data)`. The wrap host re-stringifies the response AND nests it in a `{ isUpdate, message }` container; `extractResponse` parses the string layers and unwraps the `message` container (returning `result.data` directly when it already has a top-level `status`). A bare single parse lands on the container → `UNEXPECTED_PAYLOAD` though native succeeded. An inline unwrap that reaches the `{status,…}` object works fine too.\n- **Prefer narrowing the parsed result to `\u003CPascal>Response`** and branching on `response.status` over ad-hoc shape detection. Defensive code that achieves the same outcome works fine.\n- **Prefer modeling `\u003CPascal>Request` \u002F `\u003CPascal>Response` inline** (a native-only bundle ships no shared TS layer to import). Mirror exactly what the `@ReactMethod` parses and resolves; inline annotations scattered at each call site drift more easily.\n- **Prefer the `sendAsync` envelope `{ method, args: [request] }`** dispatched on the composite `COMPOSITE_KEY` over a hand-built ad-hoc envelope. The envelope is a **RAW object** — the PCF does NOT stringify it (`sendAsync` does); a PCF that pre-stringifies double-encodes → `BRIDGE_FAILED`. NEVER call `cordova.exec` directly.\n- **Prefer the nested if\u002Felse with `setSuccess`\u002F`setError` helpers** for the response branch. An `applyErrorState` method that handles the same cases is functionally equivalent.\n\n## Things the skill enforces at generation time (only when generating fresh code)\n\nWhen emitting a new `index.ts` from scratch, the skill follows the template above. Strict requirements:\n\n- **No `\u003Cplaceholder>` text in the emitted file.** Substitute every value from the PRD or use a sensible default. Placeholders that compile (like `\u003CPascal>` substituted as the actual class name) are fine; literal `\u003CPLACEHOLDER>` strings that ship into runtime are not.\n- **Map every output declared in ARCHITECTURE §6.1 (output properties).** Each output needs an assignment in `setSuccess` (or equivalent), an initialization at the field declaration, and a return entry in `getOutputs()`. Missing outputs cause TypeScript to complain via `IOutputs`.\n- **Cross-check `manifest.json receivers[]` against native source in Step 1.5.** If `nativeModule` \u002F method names drift, STOP with `NEEDS_CONTEXT` — that's a real bug the user must reconcile.\n- **STOP with `NEEDS_CONTEXT` only when blocked.** Genuine blockers: missing manifest\u002FARCHITECTURE name+receiver (no composite key derivable), missing ARCHITECTURE §4 (no request\u002Fresponse shape), missing ARCHITECTURE §6 (no PCF surface). Non-blockers that should NOT STOP: ARCHITECTURE §6.3 (error UX mapping) missing UX detail for a specific error code (use the default error mapping), §8.2 missing a default value for a configurable input (use the type's natural default), PRD §1 summary brevity (don't need to STOP for cosmetic prose).\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 — streaming subscription wiring when the SDK doesn't yet expose it, preview thumbnail layout, button corner radius \u002F animation timing not specified in ARCHITECTURE §6.1 (configurable inputs), edge-case UX the PRD intentionally defers. A clear annotated TODO is better than fabricating arbitrary defaults.\n- **Fallback to a generic error mapping** in `setError` for codes not explicitly listed in ARCHITECTURE §6.3 (error UX mapping). The default branch handling \"any code → status='error', errorCode=code, errorMessage=message, result-fields=defaults\" is the right answer for codes the PRD doesn't customize. The message always flows through, even for unrecognized codes.\n- **Defensive `?? \u003Cdefault>` on optional response fields.** If a response field is typed `string | undefined` in `\u003CPascal>Response`, `result.fieldX ?? \"\"` at the read site is correct; don't refuse to compile because the source is optional.\n\nWhen the skill encounters an existing PCF folder, the gate offers regenerate \u002F resume \u002F abort — and the user owns that choice. Resume or abort preserve the existing code; regenerate overwrites. **The skill doesn't second-guess existing code unless the user explicitly asks for a regen.**\n\n## Runtime fallbacks the generated PCF SHOULD have\n\nThese are correctness-positive — the skill should ensure they're present. They make the PCF resilient against unexpected runtime conditions that aren't bugs in the PCF:\n\n- **Try\u002Fcatch around `invokeBridge`** for Studio preview (host global not injected, throws `NOT_IN_WRAP`) → `setError(\"NOT_IN_WRAP\", \u003Creason>)` so the not-in-wrap state is diagnosable rather than masked as a generic internal error. Already in the template.\n- **`extractResponse` in `invokeBridge`** to peel the host's re-stringification AND unwrap the wrap `{ isUpdate, message }` response container off `result.data`; total-fail throws `PARSE`. The raw response is also captured into the `\u003Cname>Json` diagnostic output. Already in the template.\n- **Structured JSON logging** (`logDebug`\u002F`logError` with a per-tap `seq`) at each dispatch step so a failure is diagnosable from a single grep-able line even when the WebView console is barely reachable. Already in the template.\n- **`\u003Cname>Json` on-device diagnostic** (`usage=\"bound\"`) surfacing the raw bridge response so a maker can read what came back via `Self.\u003Cname>Json` when the WebView console is unreachable on a release wrap build. Already in the template.\n- **Default error mapping in `setError`** for codes the skill couldn't anticipate (the wrap shell or module emitting new codes between releases) → status='error', code surfaced as-is, message surfaced as-is, result fields zeroed.\n- **`UNEXPECTED_PAYLOAD` branch** when the parsed response doesn't match `\u003CPascal>Response` shape → `setError(\"UNEXPECTED_PAYLOAD\", \u003Cserialized-payload-snippet>)`. Catches deployed-module-version drift gracefully instead of letting a TypeError propagate — and the snippet shows exactly what shape arrived.\n- **Optional-field guards** (`result.fieldX ?? \u003Cdefault>`) when mapping response fields to outputs. If a response field is documented as optional in `\u003CPascal>Response`, the PCF reads it defensively.\n\nThese are runtime safety nets, not template style. They make the PCF degrade gracefully when something unexpected happens at runtime — and they're allowed\u002Fencouraged regardless of what other style decisions the code makes.\n\n## Diagnostic logging the generated PCF SHOULD have\n\nIn production, the #1 problem when a PCF doesn't work is \"I have no idea what happened.\" Studio's debugger isn't available on device; the Power Apps Mobile log surface is `console.log` redirected to the device console (Xcode Console for iOS, `adb logcat` for Android).\n\nThe generated PCF should `console.log` at decision points with a consistent prefix so production debugging is possible without re-deploying with extra instrumentation:\n\n```typescript\n\u002F\u002F Pattern: console.log(\"[\u003CPascal>PCF] \u003Cevent>:\", \u003Crelevant context>);\n\nconsole.log(\"[\u003CPascal>PCF] Dispatching:\", COMPOSITE_KEY, METHOD, request);\nconsole.log(\"[\u003CPascal>PCF] Bridge returned - status:\", response?.status, \"error:\", response?.error);\nconsole.log(\"[\u003CPascal>PCF] Module success - \u003Ckey field>:\", response.\u003Cfield>);\nconsole.error(\"[\u003CPascal>PCF] Module error:\", response.error);\nconsole.error(\"[\u003CPascal>PCF] Unexpected payload shape:\", response);\nconsole.error(\"[\u003CPascal>PCF] Bridge error:\", code);\nconsole.log(\"[\u003CPascal>PCF] Output state - status:\", this.status, \"errorCode:\", this.errorCode, \"errorMessage:\", this.errorMessage);\n```\n\nThis is light enough not to spam, dense enough to diagnose a field issue from a log dump. Prefix with `[\u003CPascal>PCF]` so a `adb logcat | grep PCF` filter works.\n\n## Lessons baked into this template (production-PCF wisdom)\n\nThings we learned the hard way from earlier extensions; the template now handles them by default so future PCFs don't repeat the mistake:\n\n| Lesson | How the template addresses it |\n|---|---|\n| Host re-stringifies `result.data` AND wraps it in a `{ isUpdate, message }` container; a raw read gets `{isUpdate, message}` (no `status`) → `UNEXPECTED_PAYLOAD` | `invokeBridge` runs `result.data` through `extractResponse` (parse string layers + unwrap the `message` container); non-object → `PARSE`. |\n| Release wrap build's WebView console is unreachable from logcat \u002F `chrome:\u002F\u002Finspect` | Template exposes the `\u003Cname>Json` `usage=\"bound\"` output carrying the raw response — readable on-device via `Self.\u003Cname>Json`. |\n| PCF dispatched a different receiver than the manifest registered (real bug: PCF → `Snapshot`, manifest → `DeviceInfoExtension`) | `COMPOSITE_KEY` is `\u003Cname>\u002F\u003Creceiver>` resolved from `manifest.json`; Step 1.5 confirms it matches the registered receiver. |\n| Unknown method dispatched → `method '\u003Cm>' not found` on device | `METHOD` is taken from `receivers[].methods`, verified against a real `@ReactMethod` \u002F `RCT_EXPORT_METHOD` in Step 1.5. |\n| Display names show `_Display_Key` literal strings to makers | Manifest uses human-readable text from PRD §2 \u002F §8 |\n| Publisher prefix > 8 chars rejected by `pac pcf push` | `\u002Fpublish-pcf-companion` Gate 3.0 validates length 2–8 |\n| Hard-coded paths break on layout variation | `find pcf -name ControlManifest.Input.xml` discovers regardless of nesting |\n| Field issues unobservable without re-deploying with instrumentation | Diagnostic logging at decision points, in by default |\n| Re-publish without version bump → app cache doesn't refresh | `\u002Fpublish-pcf-companion` Gate 3.1 prompts for version bump |\n\n## Other operational rules\n\n- **No PCF push without an explicit confirmation gate.** Deploy is `\u002Fpublish-pcf-companion`'s job; this skill is local-only.\n- **Don't try to test the PCF end-to-end here.** That requires a wrap build with the `.ppmplugin` bundle loaded — that's `\u002Ftest-native-extension` (manual recipe).\n- **Two-way pattern (ARCHITECTURE §2 = two-way)**: add a follow-up button per the ARCHITECTURE §3.\u003Cn> spec. If §3.\u003Cn> doesn't specify the UI, STOP with `NEEDS_CONTEXT`.\n- **Streaming pattern (ARCHITECTURE §2 = streaming)**: the wrap `SendMessagePlugin` bridge doesn't expose a PCF-side subscription API. Emit the one-shot structure with a top-of-file comment flagging the streaming gap; STOP with `DONE_WITH_CONCERNS`.\n","# \u002Fgenerate-pcf-companion\n\nGenerates the dispatcher PCF — the Canvas Studio control that calls the third-party native module through the host-injected `window.PowerApps.NativeExtension.sendAsync` global, routed by the composite key `\u003Cname>\u002F\u003Creceiver>` read from the committed `.\u002Fmanifest.json` (the source of truth `\u002Fgenerate-native-extension` authors at scaffold time). Lives at `pcf\u002F\u003CPascal>PCF\u002F` in the same repo the native module lives in. The PCF is a Studio-side companion; it is **NOT** part of the `.ppmplugin` bundle (the bundle ships native binaries only — `manifest.json` + `android\u002F`\u002F`ios\u002F`).\n\nThis skill assumes the native module already exists in the repo. Run it after the module is in place.\n\n> **PCF framework reference (public Microsoft Learn docs).** Ground `pac pcf init`, the `ControlManifest.Input.xml` schema, the `init`\u002F`updateView`\u002F`getOutputs`\u002F`destroy` lifecycle, and the `usage` (`bound`\u002F`input`\u002F`output`) rules against the official Power Apps Component Framework docs — they are the authority when this skill's templates and the live framework disagree. (The `sendAsync` transport + `extractResponse` response-unwrap specifics are this track's own, in [`shared\u002Fppmplugin-format.md §2`](..\u002F..\u002Fshared\u002Fppmplugin-format.md) — not in these generic PCF docs.)\n> - Overview: \u003Chttps:\u002F\u002Flearn.microsoft.com\u002Fen-us\u002Fpower-apps\u002Fdeveloper\u002Fcomponent-framework\u002Foverview>\n> - Create a code component: \u003Chttps:\u002F\u002Flearn.microsoft.com\u002Fen-us\u002Fpower-apps\u002Fdeveloper\u002Fcomponent-framework\u002Fcreate-custom-controls-using-pcf>\n> - Custom controls overview: \u003Chttps:\u002F\u002Flearn.microsoft.com\u002Fen-us\u002Fpower-apps\u002Fdeveloper\u002Fcomponent-framework\u002Fcustom-controls-overview>\n\n---\n\n## Step 1 — Read the shared docs and the PRD\n\n1. Read [`shared\u002Fshared-instructions.md`](..\u002F..\u002Fshared\u002Fshared-instructions.md), [`shared\u002Fnaming-conventions.md`](..\u002F..\u002Fshared\u002Fnaming-conventions.md), [`shared\u002Fppmplugin-format.md`](..\u002F..\u002Fshared\u002Fppmplugin-format.md), [`shared\u002Frepo-layout.md`](..\u002F..\u002Fshared\u002Frepo-layout.md).\n2. Apply the **per-skill minimal prereq policy** ([`shared-instructions.md §1.5`](..\u002F..\u002Fshared\u002Fshared-instructions.md)). This skill needs Node + `pac` CLI only — `pac pcf init` is a local file generator and `npm install`\u002F`npm run build` under `pcf\u002F` only needs Node. It does NOT need pnpm, package-feed authentication, .NET SDK runtime, or active `pac auth`.\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-pcf-companion\n   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n    ? ✓ Node 20+ installed                   (for npm install + tsc under pcf\u002F)\n    ? ✓ pac CLI installed                    (for pac pcf init)\n\n    ? 2 checks passed. Ready to proceed.\n   ```\n\n   If `pac` is missing, STOP with the fix command (`dotnet tool install -g Microsoft.PowerApps.CLI.Tool` — note: installing `pac` requires .NET SDK as a one-time install, but neither .NET nor `pac auth` is needed at runtime for scaffold). If Node is missing, STOP with the install instruction. Run the **`\u002Fgenerate-pcf-companion` check** from [`prereq-check.md`](..\u002F..\u002Fshared\u002Fprereq-check.md) (Node + `pac` only — this self-contained track has no \"baseline\" check).\n\n   **.NET SDK + active `pac auth` are NOT checked here.** If the user later picks the optional \"Yes, also deploy now\" path in Step 2, the deploy prereq one-liner is run **at that point** (just-in-time, before `pac pcf push`).\n3. Read `.\u002FPRD.md`. If missing or §8 (PCF surface) is incomplete (any `\u003CNEEDS INPUT>` or missing fields in §8.1–§8.4), STOP with `BLOCKED: PRD.md §8 PCF surface is incomplete — re-run \u002Fdesign-native-extension-feature and complete the PCF section.`\n4. Read `.\u002F.extension-state.md`. If Phase isn't at least `scaffold`, STOP with `BLOCKED: run \u002Fgenerate-native-extension first.`\nThe structural patterns this skill needs to emit (manifest shape, `index.ts` bridge wiring, output mapping) are fully prescribed in this SKILL.md (§4–§5) and in [`shared\u002Fppmplugin-format.md`](..\u002F..\u002Fshared\u002Fppmplugin-format.md) §2 (Runtime dispatch contract). Do NOT fetch the reference extension repo at runtime — its lessons are already encoded here, and fetching it would risk reference-specific UI logic bleeding into an unrelated PCF.\n\n---\n\n## Step 1.5 — Resolve the dispatch contract from `.\u002Fmanifest.json` and the native module\n\nThe wrap **runtime dispatch contract** ([`shared\u002Fppmplugin-format.md`](..\u002F..\u002Fshared\u002Fppmplugin-format.md) §2) is the **authoritative specification** of how a host call reaches the bundle. The `.ppmplugin` bundle is native-only (no TS `handleMessageAsync` layer *in the bundle*), but the **companion PCF** dispatches through the host-injected **`window.PowerApps.NativeExtension.sendAsync`** global — it must **NEVER** call `cordova.exec` directly (the raw `cordova` global is not exposed to the PCF sandbox; a direct call is a silent no-op on device, worst on Android). `sendAsync` performs the underlying `cordova.exec(\"SendMessagePlugin\", …)` transport *inside the host context* and routes to the React Native module the binary ships:\n\n```\nPCF → window.PowerApps.NativeExtension.sendAsync(\"\u003Cname>\u002F\u003Creceiver>\", { method, args: [request] })\n    → host global (host context): cordova.exec(\"SendMessagePlugin\", \"\u003Cname>\u002F\u003Creceiver>\", [JSON.stringify({method,args}), corrId])\n    → proxy → NativeModules[\u003CnativeModule>][\u003Cmethod>].apply(mod, \u003Cargs-array>)\n```\n\nThe **composite routing key** `\u003Cname>\u002F\u003Creceiver>` is what the host resolves to a module; the **method** is one entry from that receiver's `methods[]`. One PCF drives **both iOS and Android** through this global — no platform branch.\n\n> **⚠️ TWO invariants — both confirmed on device; getting either wrong = silent failure:**\n> 1. **Dispatch via `sendAsync`, NEVER `cordova.exec`.** The envelope is a **RAW object** `{ method, args: [request] }` — the PCF does **not** stringify it; `sendAsync` does the `JSON.stringify` internally. A PCF that calls `cordova.exec` directly, or that pre-stringifies the payload, fails silently on the first device tap (no error on screen; nothing dispatches — worst on Android).\n> 2. **The inner `args` MUST be a JSON ARRAY ([ppmplugin-format §2](..\u002F..\u002Fshared\u002Fppmplugin-format.md)).** After parsing the envelope the proxy runs `Array.isArray(parsed.args) ? parsed.args : []` then `fn.apply(mod, args)` — spreading it as **positional arguments**. A bare object → dropped → the native method gets no request data. **Our convention: `args: [request]`** — one request object, and the native method takes exactly one `ReadableMap`\u002F`NSDictionary` first parameter.\n\nOn `status === \"ok\"`, `sendAsync` resolves `result.data` — the native method's resolved string. The wrap host both re-stringifies it once **and** nests it in a `{ isUpdate, message }` transport container, so the PCF normalizes it with an `extractResponse` helper (parse `result.data`, then — if the parsed object has no top-level `status` — unwrap the `message` container to reach the module's `{status, result}` object; total-fail → `PARSE` error). A bare single parse lands on the container and fails every call with `UNEXPECTED_PAYLOAD` though native succeeded — see [`shared\u002Fppmplugin-format.md §2`](..\u002F..\u002Fshared\u002Fppmplugin-format.md). `status !== \"ok\"` → surface `result.error` (fall back to `BRIDGE_FAILED`); missing host global → `NOT_IN_WRAP`.\n\n### Required reads — must succeed before Step 2\n\n1. **Resolve the composite routing key** `\u003Cname>\u002F\u003Creceiver>` from the **committed `.\u002Fmanifest.json`** — the source of truth `\u002Fgenerate-native-extension` writes at scaffold time, so on the normal flow it already exists when this skill runs. Prefer it; fall back to the staged copy, then ARCHITECTURE only if no manifest exists yet (a hand-authored module). **OS-neutral: read `.\u002Fmanifest.json` with the Read tool and parse the JSON directly — don't shell out to `grep`\u002F`sed` (the bash below is illustrative; it won't run on Windows):**\n   ```bash\n   MANIFEST=$( [ -f .\u002Fmanifest.json ] && echo .\u002Fmanifest.json || echo ppmplugin\u002Fstaging\u002Fmanifest.json )\n   if [ -f \"$MANIFEST\" ]; then\n     NAME=$(grep -o '\"name\"[[:space:]]*:[[:space:]]*\"[^\"]*\"' \"$MANIFEST\" | head -1 | sed 's\u002F.*\"\\([^\"]*\\)\"$\u002F\\1\u002F')\n     echo \"manifest ($MANIFEST) name: $NAME — receiver\u002FnativeModule\u002Fmethods read from receivers[]\"\n   else\n     echo \"no manifest.json yet (hand-authored module) — derive \u003Cname>=kebab(className), \u003Creceiver>=\u003CPascal>Extension, nativeModule=\u003CclassName> from ARCHITECTURE; \u002Fgenerate-ppmplugin-manifest will author it\"\n   fi\n   ```\n   The dispatch key the PCF binds and the receiver the manifest registers MUST match — a PCF that dispatches `\u003Cname>\u002F\u003Creceiver>` while the manifest registers a different receiver fails on first dispatch. Because `.\u002Fmanifest.json` is authored **before** this skill runs (at native-gen), the PCF **follows the manifest's** receiver — bind exactly the `receivers[].name` it declares.\n\n2. **Read `manifest.json` `receivers[]`** (canonical dispatch target):\n   - `receivers[].name` — the `\u003Creceiver>` half of the composite key\n   - `receivers[].nativeModule` — what the host resolves as `NativeModules.\u003CnativeModule>` (the module's `getName()`)\n   - `receivers[].methods` — the `Method` values the host may dispatch; each is a real `@ReactMethod` \u002F `RCT_EXPORT_METHOD` name. The PCF's `onTrigger` calls one of these.\n\n3. **Native source** (verification only — `ios\u002FRCT\u003CPascal>Module.m`, `android\u002Fsrc\u002Fmain\u002Fjava\u002F...\u002F\u003CPascal>Module.kt`):\n   - Android `getName()` \u002F iOS `+ (NSString *)moduleName` MUST equal `receivers[].nativeModule`\n   - Every `method` the PCF dispatches MUST be a real `@ReactMethod` \u002F `RCT_EXPORT_METHOD` on the module (an unknown method = `method '\u003Cm>' not found` on device)\n   - If native drifts from the manifest, STOP with `NEEDS_CONTEXT: native module drifted from manifest.json receivers[]; reconcile and re-run`\n\n### Compose the resolved contract\n\n```\nTransport (host global — fixed):\n  window.PowerApps.NativeExtension.sendAsync(\"\u003Cname>\u002F\u003Creceiver>\", { method, args: [request] })\n                                              ↑ envelope is a RAW object; sendAsync stringifies it internally\n  result.status === \"ok\"  → extractResponse(result.data) yields the module's response object (unwraps the wrap `message` container)\n  result.status !== \"ok\"  → bridge\u002Ftransport failure (result.error ?? BRIDGE_FAILED); parse-fail → PARSE\n  no window.PowerApps.NativeExtension → NOT_IN_WRAP (Studio preview \u002F non-PAM host \u002F CordovaV2 off)\n\nDispatch target (from manifest.json receivers[]):\n  Composite key: \u003Cname>\u002F\u003Creceiver>\n  nativeModule:  \u003CNativeModules.\u003CnativeModule>>\n  method:        \u003Cone of methods[]>\n  args:          [request]  — a JSON ARRAY (spread positionally via fn.apply). Our convention: ONE request\n                 object at args[0]; the @ReactMethod \u002F RCT_EXPORT_METHOD takes one ReadableMap\u002FNSDictionary param.\n  Response shape: \u003Clist — module's own {status, result, error, message}>  (message = human-readable failure reason)\n  Module error codes: \u003Clist — USER_CANCELLED, INVALID_INPUT, ...>  (canonical set + meanings: shared\u002Ferror-codes.md)\n\nVerification (native source):\n  All checks: \u003Cpass | fail with mismatch>\n```\n\n### Drift detection\n\n| Disagreement | Action |\n|---|---|\n| `manifest.json receivers[]` ↔ native source disagree on `nativeModule` \u002F method names | STOP with `NEEDS_CONTEXT`. List the mismatches. Reconcile before generating PCF. |\n| PCF composite key `\u003Cname>\u002F\u003Creceiver>` ↔ manifest's registered receiver disagree | The PCF and manifest must agree on the key. `.\u002Fmanifest.json` is authored first (at native-gen), so the **PCF follows the manifest** — bind the `receivers[].name` it declares. (Only if the user deliberately renames the receiver in the PCF, update `.\u002Fmanifest.json` to match and re-run.) |\n| Host `sendAsync` payload\u002Fresponse wire format ↔ what this skill emits | The exact envelope is owned by the host global + wrap proxy; confirm against `shared\u002Fppmplugin-format.md §2`. The PCF guarantees only the composite key + `method` the bridge ultimately targets. |\n\n**Use the resolved contract** — not the PRD's §5.1\u002F§5.2 — as the source for the PCF's dispatch args and response parsing in Step 5. The PRD describes intent; `.\u002Fmanifest.json` + native is the actual dispatch contract. When they agree, all three are consistent; when they don't, the native source wins (since that's what the running app sees).\n\n---\n\n## Step 2 — Confirm the plan with the user\n\nPrint a summary derived from ARCHITECTURE §6 and the derived names, then gate on approval.\n\n```\nPCF scaffold plan\n─────────────────\nFolder: pcf\u002F\u003CPascal>PCF\u002F\nNamespace: PowerApps  (constant for all native-extension PCFs)\nControl name: \u003CPascal>PCF\nDispatches: composite key '\u003Cname>\u002F\u003Creceiver>' → NativeModules.\u003CnativeModule>.\u003Cmethod>\n                                                  via window.PowerApps.NativeExtension.sendAsync (host global)\n\nBound input (ARCHITECTURE §6.1 (bound input)):\n  \u003CName> : \u003CType>   \u003C— bound, required>\n\nConfigurable inputs (ARCHITECTURE §6.1 (configurable inputs)):\n  \u003CName> : \u003CType> = \u003Cdefault>   \u003C— purpose>\n  ...\n\nOutput properties (ARCHITECTURE §6.1 (output properties)):\n  \u003CName> : \u003CType>   \u003C— purpose>\n  ...\n\nTrigger (ARCHITECTURE §6.2): \u003Cone line>\n```\n\nUse `AskUserQuestion`:\n\n> Proceed with this PCF scaffold?\n> - **Yes** — run `pac pcf init`, write\u002Frewrite files, run `npm install` + `npm run build` smoke check. All local — no environment deploy.\n> - **Edit the PRD first** — exit; user runs `\u002Fdesign-native-extension-feature` to fix §8.\n> - **Cancel**\n\nDeployment to a Power Platform environment is a separate, on-demand step via `\u002Fpublish-pcf-companion`. This skill is purely local — it doesn't touch `pac auth`, doesn't call `pac pcf push`, doesn't need .NET SDK.\n\n---\n\n## Step 3 — Run `pac pcf init`\n\nInside the repo root:\n\n```bash\nmkdir -p pcf\ncd pcf\npac pcf init --namespace PowerApps --name \u003CPascal>PCF --template field --framework none\n```\n\nNotes on the flags:\n- `--namespace PowerApps` — constant. All native-extension PCFs share this namespace so they group together in Canvas Studio's Insert panel.\n- `--template field` — single-bound-value control. Matches the \"trigger a native operation on a maker-set input\" pattern. Don't use `dataset` for v0.\n- `--framework none` — vanilla DOM. No React. Keeps the bundle tiny and avoids version friction with the host's managed build's React.\n\n`pac pcf init` creates `pcf\u002F\u003CPascal>PCF\u002F` with this structure:\n- `\u003CPascal>PCF.pcfproj` (MSBuild project)\n- `package.json` (uses npm — PCF tooling convention)\n- `pcfconfig.json`\n- `tsconfig.json`\n- `eslint.config.mjs`\n- `\u003CPascal>PCF\u002F` (nested) — `ControlManifest.Input.xml` + `index.ts` + `PowerAppsNativeExtension.d.ts` (ambient host-global decl, emitted in Step 5.5) + (later) `generated\u002FManifestTypes.d.ts`\n\nIf `pac pcf init` fails:\n- **\"pac not found\"** → re-run the prereq check. The `pwsh` prefix may be needed on Windows.\n- **\"folder already exists\"** → ask whether to delete it and regenerate, or merge (only safe if no manual edits were made).\n- **Auth-related** → run `pac auth list` and surface which profile is active; suggest `pac auth create` if none.\n\nAfter `pac pcf init` succeeds, also write `pcf\u002FREADME.md` (one level up from the control folder). Sections:\n\n1. **Overview** — one paragraph from PRD §1 explaining what this PCF does.\n2. **Not in the npm tarball** — explicit note that the PCF folder is excluded from `package.json`'s `files` array; it ships to Power Platform via `pac pcf push`, not via npm.\n3. **Properties** — three short tables from ARCHITECTURE §6 (bound, configurable, output).\n4. **Build & iterate** — `npm install`, `npm run build`, `pac pcf push --publisher-prefix \u003C2–8 char prefix>` (see `\u002Fpublish-pcf-companion` for prefix selection).\n5. **Trigger behavior** — one line from ARCHITECTURE §6.2.\n\nKeep it ~50 lines. Tailor every section to the PRD; don't invent boilerplate.\n\n---\n\n## Step 4 — Rewrite `ControlManifest.Input.xml`\n\n`pac pcf init` produces a single-property manifest. Rewrite it to match ARCHITECTURE §6 exactly.\n\n**Use human-readable text for `display-name-key` and `description-key`.** These attributes are what the maker sees in Power Apps Studio's properties panel — they're not just internal keys. Without `.resx` resource files (which this scaffold doesn't ship), Studio displays the attribute value verbatim. Write friendly labels and sentences, not programmer-style keys.\n\n> **⚠️ HARD RULE — no apostrophes (and no raw `\u003C` `>` `&`) in these attributes.** `pac pcf push` validates the manifest against an XSD where `display-name-key` \u002F `description-key` are `noAposStringType` — **a literal ASCII apostrophe (`'`) fails the push** with `noAposStringType` validation. It also breaks on raw XML metacharacters. So when deriving these strings:\n> - **Rephrase to avoid possessives\u002Fcontractions** rather than emitting an apostrophe — e.g. \"the phone's flashlight\" → **\"the device flashlight\"** \u002F \"the phone flashlight\"; \"doesn't\" → \"does not\"; \"user's\" → \"the user\". This reads cleanest.\n> - If a string genuinely must keep the punctuation, use the typographic right single quote **`’` (U+2019)**, which is NOT the ASCII apostrophe and passes the XSD — but prefer rephrasing.\n> - Escape or avoid `&` (`&amp;`), `\u003C`, `>`. Keep these attributes plain ASCII sentences.\n> - This applies to **every** `display-name-key` \u002F `description-key` in the manifest (control + each property). Scan the final manifest for `'` before writing it.\n\nDerivation rules:\n\n| Attribute | Value |\n|---|---|\n| `\u003Ccontrol display-name-key=\"...\">` | PRD §2 \"Human-readable name\" if present; else convert `\u003CPascal>PCF` to title case (e.g. `BarcodeScannerPCF` → `Barcode scanner`) |\n| `\u003Ccontrol description-key=\"...\">` | PRD §1 Summary, trimmed to ~120 chars (single sentence) |\n| `\u003Cproperty display-name-key=\"...\">` | Convert the property `name` to title case with spaces (e.g. `PenColor` → `Pen color`, `SignatureBase64` → `Signature base64`) |\n| `\u003Cproperty description-key=\"...\">` | The \"Purpose\" column from ARCHITECTURE §6.1 (bound input) \u002F §8.2 \u002F §8.3 for that property |\n\nThe manifest structure (substitute the human-readable strings, NOT placeholder keys):\n\n```xml\n\u003C?xml version=\"1.0\" encoding=\"utf-8\" ?>\n\u003Cmanifest>\n  \u003Ccontrol namespace=\"PowerApps\"\n           constructor=\"\u003CPascal>PCF\"\n           version=\"0.0.1\"\n           display-name-key=\"\u003Chuman-readable name from PRD §2>\"\n           description-key=\"\u003Cshort summary from PRD §1>\"\n           control-type=\"standard\">\n\n    \u003C!-- §8.1 Bound input — OPTIONAL, at most one, usage=bound. OMIT this block\n         entirely unless there is a single primary column the control both reads AND\n         writes back (text editor, scrubber, chart). Most native-extension PCFs are\n         action\u002Fconfig controls and have NO bound property — see the usage table below. -->\n    \u003Cproperty name=\"\u003CBoundName>\"\n              display-name-key=\"\u003Ctitle-cased BoundName>\"\n              description-key=\"\u003CPurpose from ARCHITECTURE §6.1 (bound input)>\"\n              of-type=\"\u003CType>\"\n              usage=\"bound\"\n              required=\"true\" \u002F>\n\n    \u003C!-- §8.2 Configurable inputs — usage=input, required=\"false\". Values the maker\n         TYPES or PICKS in the property panel (read-only to the control). -->\n    \u003Cproperty name=\"\u003CConfigName>\"\n              display-name-key=\"\u003Ctitle-cased ConfigName>\"\n              description-key=\"\u003CPurpose from ARCHITECTURE §6.1 (configurable inputs)>\"\n              of-type=\"\u003CType>\"\n              usage=\"input\"\n              required=\"false\"\n              default-value=\"\u003Cdefault>\" \u002F>\n    \u003C!-- ... one \u003Cproperty> per configurable input ... -->\n\n    \u003C!-- §8.3 Output properties — usage=output. Values the control PRODUCES that the\n         maker READS in Power Fx (Self.PropertyName) — status, result, error, computed\n         text. These are NOT bound and NOT input. Every runtime value the maker consumes\n         is an output, NOT a bound prop. Declare each in IOutputs + return from getOutputs(). -->\n    \u003Cproperty name=\"\u003COutputName>\"\n              display-name-key=\"\u003Ctitle-cased OutputName>\"\n              description-key=\"\u003CPurpose from ARCHITECTURE §6.1 (output properties)>\"\n              of-type=\"\u003CType>\"\n              usage=\"output\" \u002F>\n    \u003C!-- ... one \u003Cproperty> per output ... -->\n\n    \u003C!-- On-device diagnostic — the ONE legitimate usage=\"bound\" in a wrap PCF.\n         On a release wrap build the WebView console is unreachable from logcat \u002F\n         chrome:\u002F\u002Finspect, so the PCF surfaces the RAW bridge response (the wire string\n         exactly as it arrived, before extractResponse) here. The maker drops it on a Power Fx\n         label (Self.\u003Cname>Json) and reads what actually came back with no connected\n         debugger. See shared\u002Fppmplugin-format.md §2 \"Wrap-bridge response quirks\". -->\n    \u003Cproperty name=\"\u003Cname>Json\"\n              display-name-key=\"\u003Ctitle-cased name> raw response\"\n              description-key=\"Raw bridge response for on-device debugging — drop on a label as Self.\u003Cname>Json.\"\n              of-type=\"SingleLine.Text\"\n              usage=\"bound\" \u002F>\n\n    \u003Cresources>\n      \u003Ccode path=\"index.ts\" order=\"1\" \u002F>\n    \u003C\u002Fresources>\n  \u003C\u002Fcontrol>\n\u003C\u002Fmanifest>\n```\n\nIllustrative example (substitute the actual `\u003CPascal>` and property names from PRD §2 + §8):\n\n```xml\n\u003Ccontrol namespace=\"PowerApps\"\n         constructor=\"\u003CPascal>PCF\"\n         version=\"0.0.1\"\n         display-name-key=\"\u003CHuman-readable name from PRD §2>\"\n         description-key=\"\u003COne-line description from PRD §1.>\"\n         control-type=\"standard\">\n\n  \u003Cproperty name=\"\u003CInputPropertyName from ARCHITECTURE §6.1 (configurable inputs)>\"\n            display-name-key=\"\u003CHuman-readable label>\"\n            description-key=\"\u003COne-line description>\"\n            of-type=\"SingleLine.Text\"\n            usage=\"input\"\n            required=\"false\"\n            default-value=\"\u003Cdefault from ARCHITECTURE §6.1 (configurable inputs)>\" \u002F>\n\n  \u003Cproperty name=\"\u003COutputPropertyName from ARCHITECTURE §6.1 (output properties)>\"\n            display-name-key=\"\u003CHuman-readable label>\"\n            description-key=\"\u003COne-line description>\"\n            of-type=\"SingleLine.Text\"\n            usage=\"output\" \u002F>\n  ...\n\u003C\u002Fcontrol>\n```\n\nPCF property types you'll commonly see: `SingleLine.Text`, `SingleLine.URL`, `SingleLine.Email`, `Whole.None`, `Decimal`, `TwoOptions`, `DateAndTime.DateOnly`, `DateAndTime.DateAndTime`. Map the PRD's TypeScript types accordingly (e.g. `string` → `SingleLine.Text` unless context says URL).\n\n#### Standard diagnostic outputs — ALWAYS emit these three\n\nIn **addition** to the operation's result outputs (and the `\u003Cname>Json` raw-response bound output above), every dispatcher PCF MUST declare three diagnostic outputs (all `of-type=\"SingleLine.Text\"`, `usage=\"output\"`). For a third-party control this matters even more than first-party: the native binary runs inside the customer's wrap shell with **no logcat \u002F Xcode console \u002F native debugger reachable**, so the only way a failure is visible at all is if the code + message ride back through the bridge into a formula-readable output:\n\n```xml\n\u003Cproperty name=\"Status\"       display-name-key=\"Status\"        description-key=\"ok | error | cancelled\" of-type=\"SingleLine.Text\" usage=\"output\" \u002F>\n\u003Cproperty name=\"ErrorCode\"    display-name-key=\"Error Code\"    description-key=\"Machine-readable error code; empty on success\" of-type=\"SingleLine.Text\" usage=\"output\" \u002F>\n\u003Cproperty name=\"ErrorMessage\" display-name-key=\"Error Message\" description-key=\"Human-readable failure reason; empty on success\" of-type=\"SingleLine.Text\" usage=\"output\" \u002F>\n```\n\n- `Status` — `\"ok\"` | `\"error\"` | any lifecycle state the control uses (e.g. `\"cancelled\"`).\n- `ErrorCode` — the machine-readable code from the native error response (`USER_CANCELLED`, `INVALID_INPUT`, `BRIDGE_FAILED`, `PARSE`, …); `\"\"` on success. Makers branch on it.\n- `ErrorMessage` — the **human-readable** `message` the native side attached (the exception text, the offending field, the denied permission); `\"\"` on success. **This is the field a maker or support engineer reads first when something fails in the field** — without it, a failure is a silent no-op.\n\nThe two debugging outputs are complementary: `\u003Cname>Json` shows the *raw wire bytes* (transport-level forensics); `ErrorCode`\u002F`ErrorMessage` show the *parsed, structured* failure (what the native module meant). Maker pattern: `If(Self.Status = \"error\", Notify(Self.ErrorMessage, NotificationType.Error))`. Declare all three in `IOutputs`, set them in `setError` \u002F `setSuccess`, and return them from `getOutputs()`.\n\n#### Optional visual-style inputs (color + border) — ONLY if the user asks\n\nEvery dispatcher PCF already ships a **good-looking themed default** (see `applyStyles()` \u002F `ensureStyleTag()` in Step 5) that follows the host Fluent theme — so it never renders as a raw browser button **without** any extra inputs. Do **NOT** add maker-facing color\u002Fborder inputs by default; they clutter the property panel for controls that don't need them.\n\n**Emit these ONLY when the PRD \u002F user explicitly calls for maker-configurable color or border options.** When they do, add just the knobs requested (from the set below), as `usage=\"input\"`, `required=\"false\"`, all `SingleLine.Text` except the numeric radius:\n\n```xml\n\u003Cproperty name=\"AccentColor\"  display-name-key=\"Accent color\"  description-key=\"Button background color (hex, e.g. #0f6cbd). Blank = host theme.\" of-type=\"SingleLine.Text\" usage=\"input\" required=\"false\" default-value=\"\" \u002F>\n\u003Cproperty name=\"TextColor\"    display-name-key=\"Text color\"    description-key=\"Label color (hex). Blank = auto for contrast on the accent.\" of-type=\"SingleLine.Text\" usage=\"input\" required=\"false\" default-value=\"\" \u002F>\n\u003Cproperty name=\"BorderColor\"  display-name-key=\"Border color\"  description-key=\"Border color (hex). Blank = matches the accent color.\" of-type=\"SingleLine.Text\" usage=\"input\" required=\"false\" default-value=\"\" \u002F>\n\u003Cproperty name=\"BorderRadius\" display-name-key=\"Corner radius\" description-key=\"Corner radius in pixels (0 = square, 4 = default, 20 = pill).\" of-type=\"Whole.None\" usage=\"input\" required=\"false\" default-value=\"4\" \u002F>\n```\n\nRules that keep this **small and safe** (not a theming engine):\n- **The default is to emit NONE of these.** The themed baseline + host theme already look right; only surface a knob the user actually requested. `applyStyles()` reads each one **only if its `\u003Cproperty>` exists**, so omitting them changes nothing about the default look.\n- **Contrast is guaranteed, not the maker's problem.** If `AccentColor` is emitted and set but `TextColor` is blank, `applyStyles()` computes black\u002Fwhite by luminance so the label always clears WCAG AA — a maker can't accidentally create an invisible-label button.\n- **Don't add width\u002Fheight\u002Ffont-size inputs** — the host box sizes the control; sizing inputs fight the canvas resize handle.\n\n### Choosing `usage` per property — decide BEFORE emitting any `\u003Cproperty>`\n\n`usage` is a **required** attribute and the single most common thing to get wrong. The manifest schema defines exactly **three** values ([property element reference](https:\u002F\u002Flearn.microsoft.com\u002Fpower-apps\u002Fdeveloper\u002Fcomponent-framework\u002Fmanifest-schema-reference\u002Fproperty)) — the property \"represents a column the component can change (`bound`), read-only (`input`), or output values (`output`)\". Pick deliberately; the wrong choice clutters the maker's input panel (everything as a typeable input) or hides values that should be formula-readable (an output mislabeled `bound`).\n\nFor **every** property, run this decision in order — first match wins:\n\n1. **Does the control *produce* this value for the maker to read?** (status, result, current value, last error, computed\u002Freturned text — anything the maker references as `Self.\u003CName>` \u002F `\u003CControl>.\u003CName>` in Power Fx) → **`output`**. This is the default for everything the native operation returns. *If the maker reads it in a formula, it is an output — never `bound`.*\n2. **Does the maker *set\u002Fconfigure* this value?** (URL, table name, id, color, interval, toggle, JSON config — typed or picked in the property panel, or bound to a field for reference) → **`input`** (`required=\"false\"`, give a `default-value`).\n3. **Is there a single primary column the control both displays AND writes back** (two-way edit — text editor, scrubber, chart)? → **`bound`** (at most one). Otherwise **no bound property at all.**\n\n| `usage` | Meaning (authoritative) | Maker \u002F Studio behavior | TS wiring |\n|---|---|---|---|\n| `output` | A value the control **produces**. The control writes it; the maker only reads it. | **Hidden** from the input panel; readable in Power Fx as `Self.\u003CName>`. | declared in `IOutputs`; returned from `getOutputs()`; never read from `context.parameters`. |\n| `input` | A **read-only** input. The maker provides it — a static value (`default-value`) or a bound field — and the control reads but never writes it. | Editable field in the property panel. | read via `context.parameters.\u003CName>.raw`; not in `getOutputs()`. |\n| `bound` | A column the control can **change** — two-way. The control reads the field AND writes it back. At most one; **omit for action\u002Fconfig controls.** | Bound to a Dataverse column; `context.parameters.\u003CName>` also exposes `.formatted` \u002F `.security` \u002F `.attributes`. | read in `updateView` AND returned from `getOutputs()`; `notifyOutputChanged()` on change. |\n\n**Default for native-extension PCFs (the common case): NO `bound` property.** Most of these are action \u002F configuration \u002F status controls (trigger a native op on a maker-set input, surface the result). They use **`input` for what the maker sets** and **`output` for everything the control returns** — and omit `bound` entirely. Reaching for `bound` because a property feels like \"the main input\" is the #1 mistake: if the maker reads it in a formula it's an `output`; if the maker sets it it's an `input`. `bound` is *only* for a single column the control edits in place.\n\n> **The ONE allowed `bound` in a wrap PCF is the `\u003Cname>Json` diagnostic** added to the template above — it surfaces the raw bridge response for on-device debugging where the WebView console is unreachable (`shared\u002Fppmplugin-format.md` §2). That is the only exception; classify every *domain* property through the decision above and never reach for `bound` for them.\n\n> **Don't blindly inherit `bound` from ARCHITECTURE §8.1.** The design doc's \"§8.1 Bound input\" heading does not mean the property must be `usage=\"bound\"` — re-classify each property through the decision above. A value the native operation *returns* is an `output` even if §8 listed it under inputs.\n\n> **If you later need real localization,** the canonical PCF pattern is to put resource KEYS here (e.g. `\u003CPropertyName>_Display`) and create `strings\u002F\u003CPascal>PCF.1033.resx` (and additional `.resx` per locale) mapping keys to localized strings. For v0 with no i18n requirement, plain strings as shown above are correct and friendlier.\n\nAfter writing, validate the XML parses with `pac pcf build --no-restore` or by checking for the `\u003CPascal>PCF\u002Fgenerated\u002FManifestTypes.d.ts` file that `pcf-scripts` generates on build.\n\n---\n\n## Step 5 — Write `index.ts`\n\n### Step 5.0 — Branch on ARCHITECTURE §6.0 visual style\n\nBefore generating, read ARCHITECTURE §6.0 to know which visual style the PCF should render:\n\n| §8.0 value | Generated UI shape |\n|---|---|\n| `minimal` (default) | Single themed button. Click → `onTrigger()`. Outputs are read by the maker's Power Fx; the PCF itself doesn't render them. |\n| `with-preview` | Button + preview pane. Preview is an `\u003Cimg>` (for image outputs like base64 PNG \u002F data URI), `\u003Cdiv>` (for text outputs like scan result), or `\u003Cspan>` (for status). Preview reads from the success-output field and updates in `updateView` when the underlying value changes. |\n| `inline-surface` | Custom — the PCF renders an interactive surface itself rather than triggering a native modal. v0 emits a `\u002F\u002F TODO: design the inline surface` placeholder and STOPs with `DONE_WITH_CONCERNS`. |\n\nThe skeleton below is **for `minimal` mode**. Adapt for `with-preview` by adding a preview element and a `renderPreview()` method called from `updateView` + after `setSuccess`. For `inline-surface`, the skeleton doesn't apply — see the v1+ guidance.\n\n\n\n**Generate complete working code, not a skeleton with placeholders.** Every line in `index.ts` is derived from a specific source — and dispatch args \u002F response shapes come from the **native ground-truth contract resolved in Step 1.5**, not directly from the PRD. PRD describes intent; `manifest.json` + native source is what the running app actually exchanges.\n\n| Block in `index.ts` | Derived from |\n|---|---|\n| `COMPOSITE_KEY` + `METHOD` constants | `manifest.json` → `COMPOSITE_KEY = \"\u003Cname>\u002F\u003Creceiver>\"` (composite routing key) and `METHOD = \"\u003Cone of receivers[].methods>\"`. The composite key MUST match the receiver the manifest registers. |\n| Types | `\u003CPascal>Request`, `\u003CPascal>Response` defined inline in this file (the native-only bundle ships no shared TS `src\u002Ftypes.ts` to import — model the args the `@ReactMethod` parses and the `{status, result, error}` it resolves). |\n| Private output-field declarations | ARCHITECTURE §6.1 (output properties) (one private field per output, typed from §8.3's `Type` column, initialized to a safe default — `\"\"` for text, `0` for numbers, `false` for boolean) — plus the `\u003Cname>Json` raw-response diagnostic field. |\n| `applyStyles()` body | ARCHITECTURE §6.1 (configurable inputs) (one assignment per configurable input — button text, background, foreground, padding, etc.) using the actual property names from §8.2 |\n| `onTrigger()` payload-build (dispatch args) | **`\u003CPascal>Request`** (the object the `@ReactMethod` \u002F `RCT_EXPORT_METHOD` reads as its one `ReadableMap`\u002F`NSDictionary` param). ARCHITECTURE §6.1 (bound input) names which configurable\u002Fbound input flows into which field. It rides in the `sendAsync` envelope **`{ method: METHOD, args: [request] }`** — a **RAW object** (the PCF does NOT stringify it; `sendAsync` does) AND the inner `args` MUST be an array (§2). |\n| `onTrigger()` outcome branch | Single nested if\u002Felse covering four cases of the two-level error model, **each passing both a code AND a message to `setError`**: ① bridge OK + `payload.status === \"ok\"` → `setSuccess(payload.result)`. ② bridge OK + `payload.status === \"error\"` → `setError(payload.error, payload.message ?? \"\")` (the native-supplied human-readable reason). ③ bridge OK + payload shape unrecognized (even after `extractResponse` unwraps the wrap `message` container) → `setError(\"UNEXPECTED_PAYLOAD\", \"native response shape not recognized: \" + this.\u003Cname>Json.slice(0, 200))` — surface the RAW wire string, not the post-parse object. ④ `sendAsync` status !== \"ok\" \u002F parse-fail \u002F no host global → `setError(\"BRIDGE_FAILED\", \u003Cresult.error \u002F reason>)` \u002F `setError(\"PARSE\", \u003Craw string that failed to parse>)` \u002F `setError(\"NOT_IN_WRAP\", \u003Creason>)`. The RAW wire response is ALSO surfaced via the `\u003Cname>Json` output. Single `notifyOutputChanged()` at the end. |\n| `setSuccess(result)` body | `\u003CPascal>Response[\"result\"]` → ARCHITECTURE §6.1 (output properties). One assignment per §8.3 output, sourced from the corresponding response field. Sets `status=\"ok\"` and **clears `errorCode=\"\"` and `errorMessage=\"\"`**. |\n| `setError(code, message)` body | ARCHITECTURE §5 (codes) + ARCHITECTURE §6.3 (error UX mapping) (UX per code). Simplest form: zero out result fields, set `status=\"error\"`, `errorCode=code`, **`errorMessage=message`** (the human-readable reason — never drop it). If ARCHITECTURE §6.3 says specific codes need different output UX (e.g. USER_CANCELLED → status=\"cancelled\"), branch inside `setError`; still set `errorMessage`. |\n| `getOutputs()` body | ARCHITECTURE §6.1 (output properties) — one returned entry per output, reading the private field. **MUST include `Status`, `ErrorCode`, and `ErrorMessage`** (and the `\u003Cname>Json` raw output) so the failure is visible in Power Fx with no native debugger. |\n\n### The skeleton (with derivation rules inline)\n\nReplace the default scaffolded `pcf\u002F\u003CPascal>PCF\u002F\u003CPascal>PCF\u002Findex.ts` with this structure, **substituting every value from the PRD**:\n\n```ts\nimport { IInputs, IOutputs } from \".\u002Fgenerated\u002FManifestTypes\";\n\n\u002F\u002F Domain contract — modeled INLINE. A native-only .ppmplugin ships NO shared TS layer,\n\u002F\u002F so there's nothing to import: \u003CPascal>Request is the args the @ReactMethod parses,\n\u002F\u002F \u003CPascal>Response is the {status, result, error} object it resolves. Mirror the module.\ninterface \u003CPascal>Request { \u002F* one field per dispatch arg the @ReactMethod parses *\u002F }\ninterface \u003CPascal>Response { status: \"ok\" | \"error\"; result?: Record\u003Cstring, unknown>; error?: string; message?: string; }\n\u002F\u002F   error   — machine code (present when status === \"error\"); the PCF branches on it.\n\u002F\u002F   message — HUMAN-READABLE failure reason (present when status === \"error\"); the PCF surfaces it as ErrorMessage.\n\n\u002F\u002F Bridge declaration — the wrap host injects `window.PowerApps.NativeExtension`\n\u002F\u002F onto the Canvas WebView at boot (when CordovaV2 is enabled). The PCF dispatches\n\u002F\u002F through its `sendAsync` global — it must NEVER call `cordova.exec` directly (the\n\u002F\u002F raw `cordova` global is NOT exposed to the PCF sandbox, so a direct call is a\n\u002F\u002F silent no-op on device, worst on Android). See shared\u002Fppmplugin-format.md §2.\n\u002F\u002F\n\u002F\u002F Type it with a local ambient declaration in PowerAppsNativeExtension.d.ts (emitted\n\u002F\u002F alongside this file) so the PCF stays host-agnostic and pins no SDK package.\n\n\u002F\u002F deepParse: the wrap host double-\u002Ftriple-stringifies the bridge response, so peel string\n\u002F\u002F layers (bounded) until we reach an object. This is a BOUNDED helper used by extractResponse\n\u002F\u002F to reach the container\u002Fpayload — NOT a blind transport walk used on its own.\n\u002F\u002F See shared\u002Fppmplugin-format.md §2 \"Wrap-bridge response quirks\".\nfunction deepParse(v: unknown, max = 4): unknown {\n  let cur = v;\n  for (let i = 0; i \u003C max && typeof cur === \"string\"; i++) {\n    try { cur = JSON.parse(cur); } catch { break; }\n  }\n  return cur;\n}\n\n\u002F\u002F extractResponse: the wrap transport ALSO wraps the module's JSON in a container object,\n\u002F\u002F nesting it (still stringified) under a `message` key:\n\u002F\u002F   {\"isUpdate\":false,\"message\":\"{\\\"status\\\":\\\"ok\\\",\\\"result\\\":{…}}\"}\n\u002F\u002F A bare parse lands on {isUpdate, message} (no top-level `status`) → UNEXPECTED_PAYLOAD\n\u002F\u002F even though native succeeded. So peel string layers, THEN unwrap the container: probe\n\u002F\u002F `message` (the confirmed wrap key) first, then defensive fallbacks, accepting the first\n\u002F\u002F nested value that has a top-level `status`. When result.data already IS the {status,…}\n\u002F\u002F object (the simple already-unwrapped case), the first check returns it directly — so this\n\u002F\u002F is a strict superset of a single guarded parse. See shared\u002Fppmplugin-format.md §2.\nfunction extractResponse(raw: unknown): unknown {\n  const top = deepParse(raw);\n  if (top && typeof top === \"object\" && \"status\" in top) return top;\n  if (top && typeof top === \"object\") {\n    for (const k of [\"message\", \"result\", \"data\", \"value\", \"response\", \"body\", \"payload\"]) {\n      if (k in (top as Record\u003Cstring, unknown>)) {\n        const inner = deepParse((top as Record\u003Cstring, unknown>)[k]);\n        if (inner && typeof inner === \"object\" && \"status\" in inner) return inner;\n      }\n    }\n  }\n  return top;   \u002F\u002F fall through — UNEXPECTED_PAYLOAD surfaces the raw string for diagnosis\n}\n\nconst COMPOSITE_KEY = \"\u003Cname>\u002F\u003Creceiver>\";   \u002F\u002F manifest.json — composite routing key; MUST match the receiver the manifest registers\nconst METHOD = \"\u003Cmethod>\";                   \u002F\u002F manifest.json receivers[].methods — a real @ReactMethod \u002F RCT_EXPORT_METHOD name\n\nexport class \u003CPascal>PCF implements ComponentFramework.StandardControl\u003CIInputs, IOutputs> {\n  private container!: HTMLDivElement;\n  private notifyOutputChanged!: () => void;\n  private context!: ComponentFramework.Context\u003CIInputs>;\n  private button!: HTMLButtonElement;\n  private seq = 0;                          \u002F\u002F per-tap correlation id for the structured logs\n\n  \u002F\u002F ── Structured JSON logging (crash-proof) — the only way to diagnose on-device. ──\n  \u002F\u002F On a release wrap build the WebView console is unreachable, so every dispatch step\n  \u002F\u002F emits a single-line JSON record (grep-able if a console IS attached, and the raw\n  \u002F\u002F response is ALSO surfaced via the usage=\"bound\" \u003Cname>Json output for Power Fx).\n  private static readonly LOG_TAG = \"\u003CPascal>PCF\";\n  private logDebug(event: string, data?: Record\u003Cstring, unknown>): void {\n    try { console.log(`[${\u003CPascal>PCF.LOG_TAG}] ` + JSON.stringify({ ts: Date.now(), level: \"debug\", event, ...data })); } catch { \u002F* logging must never throw *\u002F }\n  }\n  private logError(event: string, data?: Record\u003Cstring, unknown>): void {\n    try { console.error(`[${\u003CPascal>PCF.LOG_TAG}] ` + JSON.stringify({ ts: Date.now(), level: \"error\", event, ...data })); } catch { \u002F* logging must never throw *\u002F }\n  }\n\n  \u002F\u002F ── Outputs: one private field per ARCHITECTURE §6.1 (output properties) row ──\n  \u002F\u002F Emit one declaration per output, typed from §8.3's \"Type\" column (mapping the\n  \u002F\u002F module's response field type to its PCF property type, inverse direction),\n  \u002F\u002F initialized to a safe default for that type.\n  \u002F\u002F\n  \u002F\u002F Defaults by type:\n  \u002F\u002F   SingleLine.Text \u002F .URL \u002F .Email → \"\"\n  \u002F\u002F   Whole.None \u002F Decimal             → 0\n  \u002F\u002F   TwoOptions                       → false\n  \u002F\u002F   DateAndTime.*                    → new Date(0)   (or a sentinel; ARCHITECTURE §6.1 (output properties) may specify)\n  \u002F\u002F\n  \u002F\u002F Shape: `private \u003CfieldName>: \u003CtsType> = \u003Cdefault>;`\n  \u002F\u002F Always include these diagnostic fields (back the standard outputs):\n  \u002F\u002F   private status: string = \"idle\";       \u002F\u002F \"idle\" | \"ok\" | \"error\" | extension-specific values\n  \u002F\u002F   private errorCode: string = \"\";        \u002F\u002F empty in success state; the machine code in error state\n  \u002F\u002F   private errorMessage: string = \"\";     \u002F\u002F empty in success state; the HUMAN-READABLE reason in error state\n  \u002F\u002F                                           \u002F\u002F (the field the maker\u002Fsupport reads first — no native debugger on device)\n  \u002F\u002F   private \u003Cname>Json: string = \"\";    \u002F\u002F RAW bridge response (wire string, before extractResponse) — the\n  \u002F\u002F                                        \u002F\u002F usage=\"bound\" on-device diagnostic; surfaced via Self.\u003Cname>Json\n\n  public init(\n    context: ComponentFramework.Context\u003CIInputs>,\n    notifyOutputChanged: () => void,\n    _state: ComponentFramework.Dictionary,\n    container: HTMLDivElement,\n  ): void {\n    this.container = container;\n    this.notifyOutputChanged = notifyOutputChanged;\n    this.context = context;\n\n    this.button = document.createElement(\"button\");\n    this.button.className = \"pam-ext-trigger\";   \u002F\u002F scopes the injected \u003Cstyle> below\n    \u002F\u002F ── Default: fill the host-allocated rectangle ──────────────────────────\n    \u002F\u002F The maker resizes the control on the canvas; the host gives us the\n    \u002F\u002F resulting box via `container`. The button fills that box so the whole\n    \u002F\u002F control area is the tap target (no dead zone around a small button).\n    \u002F\u002F To make the control SMALLER by default, shrink the control's default\n    \u002F\u002F Width\u002FHeight in the canvas (see ARCHITECTURE §6 default size), NOT the\n    \u002F\u002F button — the button just fills whatever box it's given.\n    this.button.style.width = \"100%\";\n    this.button.style.height = \"100%\";\n    this.button.style.boxSizing = \"border-box\";\n    this.ensureStyleTag();   \u002F\u002F interaction states (hover\u002Factive\u002Fdisabled\u002Ffocus) — pseudo-classes need a \u003Cstyle>\n    this.applyStyles();\n    this.button.addEventListener(\"click\", () => { void this.onTrigger(); });\n    container.appendChild(this.button);\n    this.logDebug(\"init\", { inWrap: !!window.PowerApps?.NativeExtension, key: COMPOSITE_KEY, method: METHOD });\n  }\n\n  \u002F\u002F Inject a scoped \u003Cstyle> ONCE. Inline styles can't express :hover \u002F :active \u002F\n  \u002F\u002F :disabled \u002F :focus-visible, and a control with no interaction feedback reads\n  \u002F\u002F as \"dead\" on device. Keep the selectors scoped to .pam-ext-trigger so we never\n  \u002F\u002F leak styles into the host page. `--pam-accent` \u002F `--pam-fg` are set per-instance\n  \u002F\u002F in applyStyles(), so these rules follow the resolved (possibly maker-overridden) palette.\n  private ensureStyleTag(): void {\n    if (document.getElementById(\"pam-ext-trigger-style\")) return;\n    const s = document.createElement(\"style\");\n    s.id = \"pam-ext-trigger-style\";\n    s.textContent = `\n      .pam-ext-trigger {\n        font-family: \"Segoe UI\", -apple-system, BlinkMacSystemFont, Roboto, sans-serif;\n        font-size: 14px; font-weight: 600; cursor: pointer;\n        border-style: solid; border-width: 1px;\n        transition: filter .1s ease, box-shadow .1s ease;\n      }\n      .pam-ext-trigger:hover:not(:disabled)  { filter: brightness(0.94); }\n      .pam-ext-trigger:active:not(:disabled) { filter: brightness(0.88); }\n      .pam-ext-trigger:disabled              { opacity: .5; cursor: default; }\n      .pam-ext-trigger:focus-visible         { outline: 2px solid var(--pam-fg, #201f1e); outline-offset: 2px; }\n    `;\n    document.head.appendChild(s);\n  }\n\n  private applyStyles(): void {\n    const p = this.context.parameters;\n    const b = this.button;\n\n    \u002F\u002F ── 1. Themed defaults (a real control, not a raw browser button) ────────\n    \u002F\u002F A polished baseline the maker gets for free. Prefer the host Fluent theme\n    \u002F\u002F when the runtime exposes it, so the control looks native in maker portal\n    \u002F\u002F AND on mobile; fall back to these Fluent-ish hexes in Studio preview \u002F older hosts.\n    const theme = (this.context as unknown as { fluentDesignLanguage?: { palette?: Record\u003Cstring, string> } }).fluentDesignLanguage;\n    let accent = theme?.palette?.themePrimary ?? \"#0f6cbd\";   \u002F\u002F brand blue\n    let fg     = theme?.palette?.white ?? \"#ffffff\";           \u002F\u002F text on accent\n    let border = accent;                                        \u002F\u002F border matches accent by default\n    let radius = \"4px\";                                         \u002F\u002F Fluent-standard corner\n\n    \u002F\u002F ── 2. Maker overrides (the color + border \"options\") ────────────────────\n    \u002F\u002F Read ONLY if the corresponding configurable input exists in ARCHITECTURE §6.1.\n    \u002F\u002F Emit a line per knob the manifest declares (AccentColor \u002F TextColor \u002F BorderColor \u002F BorderRadius).\n    \u002F\u002F   accent = (p.AccentColor?.raw   || \"\").trim() || accent;\n    \u002F\u002F   fg     = (p.TextColor?.raw     || \"\").trim() || fg;\n    \u002F\u002F   border = (p.BorderColor?.raw   || \"\").trim() || border;\n    \u002F\u002F   radius = (p.BorderRadius?.raw != null ? p.BorderRadius.raw + \"px\" : radius);\n\n    \u002F\u002F ── 3. Guarantee legible contrast (accessibility, not optional) ──────────\n    \u002F\u002F If the maker set a background but no explicit text color, pick black\u002Fwhite\n    \u002F\u002F by luminance so text on the accent always clears WCAG AA (~4.5:1). Prevents\n    \u002F\u002F the classic \"pale button, invisible label\" a hand-set color causes.\n    if (!((p as Record\u003Cstring, { raw?: string }>).TextColor?.raw ?? \"\").trim()) {\n      fg = this.readableTextOn(accent);\n    }\n\n    b.style.backgroundColor = accent;\n    b.style.color = fg;\n    b.style.borderColor = border;\n    b.style.borderRadius = radius;\n    b.style.padding = \"8px 16px\";\n    b.style.setProperty(\"--pam-accent\", accent);\n    b.style.setProperty(\"--pam-fg\", fg);\n\n    \u002F\u002F ── 4. Label ─────────────────────────────────────────────────────────────\n    \u002F\u002F Button text from the §8.2 label input (or a sensible default). For a\n    \u002F\u002F toggleable control label it \"\u003CFunction> : \u003CState>\" (e.g. \"Location Tracking : On\")\n    \u002F\u002F so the tap-to-toggle affordance is discoverable — not a bare state word.\n    b.textContent = ((p as Record\u003Cstring, { raw?: string }>).ButtonLabel?.raw ?? \"\").trim() || \"\u003Cdefault label from PRD §6>\";\n\n    \u002F\u002F Map any remaining §8.2 configurable inputs to DOM properties here (one line each).\n    \u002F\u002F DO NOT set width\u002Fheight from inputs — the host box already sizes the control;\n    \u002F\u002F a width\u002Fheight input just fights the canvas resize handle.\n  }\n\n  \u002F\u002F Black or white text for the given background, chosen by relative luminance so\n  \u002F\u002F the label always clears WCAG AA contrast on the resolved accent color.\n  private readableTextOn(hex: string): string {\n    const m = \u002F^#?([0-9a-f]{6})$\u002Fi.exec(hex.trim());\n    if (!m) return \"#ffffff\";\n    const n = parseInt(m[1], 16);\n    const [r, g, bl] = [(n >> 16) & 255, (n >> 8) & 255, n & 255].map((c) => {\n      const s = c \u002F 255; return s \u003C= 0.03928 ? s \u002F 12.92 : Math.pow((s + 0.055) \u002F 1.055, 2.4);\n    });\n    const L = 0.2126 * r + 0.7152 * g + 0.0722 * bl;   \u002F\u002F relative luminance\n    return L > 0.4 ? \"#201f1e\" : \"#ffffff\";            \u002F\u002F dark text on light bg, white on dark\n  }\n\n  private async onTrigger(): Promise\u003Cvoid> {\n    \u002F\u002F ─────────────────────────────────────────────────────────────────────────\n    \u002F\u002F 1. Build the REQUEST args (the object the @ReactMethod parses)\n    \u002F\u002F ─────────────────────────────────────────────────────────────────────────\n    \u002F\u002F The request rides inside the sendAsync envelope as a RAW object:\n    \u002F\u002F   { method: METHOD, args: [request] }\n    \u002F\u002F   (the PCF does NOT stringify it — sendAsync does; inner args MUST be an array — §2).\n    \u002F\u002F\n    \u002F\u002F Shape: one assignment per \u003CPascal>Request field; source values from\n    \u002F\u002F   this.context.parameters.\u003C§8.2-name>.raw  (configurable inputs), or\n    \u002F\u002F   this.context.parameters.\u003C§8.1-name>.raw  (bound input)\n    const request: \u003CPascal>Request = { \u002F* one entry per \u003CPascal>Request field *\u002F };\n    const seq = ++this.seq;\n    this.logDebug(\"dispatch\", { seq, key: COMPOSITE_KEY, method: METHOD, request });\n\n    \u002F\u002F ─────────────────────────────────────────────────────────────────────────\n    \u002F\u002F 2. Dispatch via window.PowerApps.NativeExtension.sendAsync — invokeBridge\n    \u002F\u002F    resolves the parsed response object, or THROWS on a bridge\u002Ftransport\n    \u002F\u002F    failure or when the host global isn't available (Studio preview \u002F non-PAM host).\n    \u002F\u002F ─────────────────────────────────────────────────────────────────────────\n    let response: \u003CPascal>Response;\n    try {\n      response = await this.invokeBridge(request);\n      this.logDebug(\"bridge_returned\", { seq, status: response?.status, error: response?.error });\n    } catch (e) {\n      \u002F\u002F status!==\"ok\" \u002F parse-fail \u002F no host global. The thrown message carries the code.\n      const code = (e as Error)?.message === \"PARSE\" ? \"PARSE\"\n                 : (e as Error)?.message === \"NOT_IN_WRAP\" ? \"NOT_IN_WRAP\"\n                 : \"BRIDGE_FAILED\";\n      \u002F\u002F Message: prefer the raw wire string (set on this.\u003Cname>Json by invokeBridge) so a\n      \u002F\u002F transport\u002Fparse failure is debuggable from Power Fx; fall back to the error string.\n      const reason = this.\u003Cname>Json || String((e as Error)?.message ?? e);\n      this.logError(\"bridge_error\", { seq, code, reason });\n      this.setError(code, reason);\n      this.notifyOutputChanged();\n      return;\n    }\n\n    \u002F\u002F ─────────────────────────────────────────────────────────────────────────\n    \u002F\u002F 3. Branch on the module's two-level {status, result, error} response.\n    \u002F\u002F    extractResponse already peeled the host's string layers and unwrapped the\n    \u002F\u002F    wrap `message` container — `response` is the module's {status,...} object.\n    \u002F\u002F ─────────────────────────────────────────────────────────────────────────\n    if (response?.status === \"ok\" && response.result) {\n      \u002F\u002F Operation succeeded. Map response.result.* → output fields.\n      this.logDebug(\"module_success\", { seq });\n      this.setSuccess(response.result);\n    } else if (response?.status === \"error\") {\n      \u002F\u002F Operation failed with a module-defined code (USER_CANCELLED, etc., per ARCHITECTURE §5).\n      \u002F\u002F Pass the native-supplied human-readable message through to the ErrorMessage output.\n      this.logError(\"module_error\", { seq, code: response.error, message: response.message });\n      this.setError(response.error ?? \"INTERNAL_ERROR\", response.message ?? \"\");\n    } else {\n      \u002F\u002F Bridge succeeded but the payload doesn't match \u003CPascal>Response shape — even after\n      \u002F\u002F extractResponse tried to unwrap the wrap `message` container. Surface the RAW wire\n      \u002F\u002F string (not the post-parse object) so the real shape is diagnosable from Power Fx.\n      this.logError(\"unexpected_payload\", { seq, raw: this.\u003Cname>Json });\n      this.setError(\"UNEXPECTED_PAYLOAD\", \"native response shape not recognized: \" + this.\u003Cname>Json.slice(0, 200));\n    }\n    this.notifyOutputChanged();\n    this.logDebug(\"final_state\", { seq, status: this.status, errorCode: this.errorCode, errorMessage: this.errorMessage });\n  }\n\n  private setSuccess(result: NonNullable\u003C\u003CPascal>Response[\"result\"]>): void {\n    \u002F\u002F ── Map the module's `result` payload → PCF output fields ──\n    \u002F\u002F For each output declared in ARCHITECTURE §6.1 (output properties), assign from the matching field in `result`\n    \u002F\u002F (the shape is the inline `\u003CPascal>Response[\"result\"]` type modeled at the top of this file).\n    \u002F\u002F Set the status\u002FerrorCode\u002FerrorMessage outputs to their success values.\n    \u002F\u002F\n    \u002F\u002F Shape:\n    \u002F\u002F   this.\u003C§8.3-field-A> = result.\u003Cresponse-field> ?? \u003Cdefault>;\n    \u002F\u002F   this.\u003C§8.3-field-B> = result.\u003Cresponse-field> ?? \u003Cdefault>;\n    \u002F\u002F   ...\n    \u002F\u002F   this.status = \"ok\";\n    \u002F\u002F   this.errorCode = \"\";\n    \u002F\u002F   this.errorMessage = \"\";   \u002F\u002F clear any prior failure's message\n    \u002F\u002F\n    \u002F\u002F One assignment per §8.3 row. If a §8.3 output has no matching source field\n    \u002F\u002F in \u003CPascal>Response[\"result\"], the scaffold STOPS in Step 1.5 — it shouldn't reach here.\n    \u002F\u002F (this.\u003Cname>Json is set in invokeBridge from the raw response — don't touch it here.)\n  }\n\n  private setError(code: string, message: string): void {\n    \u002F\u002F ── Map error code → output fields per ARCHITECTURE §6.3 (error UX mapping) ──\n    \u002F\u002F Default behavior (covers most extensions): zero out result-bearing outputs, set\n    \u002F\u002F status=\"error\", surface the code AND the human-readable message as-is.\n    \u002F\u002F\n    \u002F\u002F Shape:\n    \u002F\u002F   this.\u003C§8.3-result-field-A> = \u003Cdefault>;\n    \u002F\u002F   this.\u003C§8.3-result-field-B> = \u003Cdefault>;\n    \u002F\u002F   ...\n    \u002F\u002F   this.status = \"error\";\n    \u002F\u002F   this.errorCode = code;\n    \u002F\u002F   this.errorMessage = message;   \u002F\u002F NEVER drop this — it's what makes the failure debuggable on-device\n    \u002F\u002F\n    \u002F\u002F If ARCHITECTURE §6.3 (error UX mapping) specifies that certain codes need different output state (e.g. a code\n    \u002F\u002F that maps to status=\"cancelled\" instead of \"error\", or a code that preserves a\n    \u002F\u002F partial result), branch on the code value:\n    \u002F\u002F\n    \u002F\u002F   switch (code) {\n    \u002F\u002F     case \"\u003CCODE_A>\":\n    \u002F\u002F       \u002F* ARCHITECTURE §6.3 (error UX mapping) row for CODE_A *\u002F\n    \u002F\u002F       break;\n    \u002F\u002F     case \"\u003CCODE_B>\":\n    \u002F\u002F       \u002F* ARCHITECTURE §6.3 (error UX mapping) row for CODE_B *\u002F\n    \u002F\u002F       break;\n    \u002F\u002F     default:\n    \u002F\u002F       \u002F* the standard zero-and-surface mapping above *\u002F\n    \u002F\u002F   }\n    \u002F\u002F\n    \u002F\u002F The codes that reach here include BOTH bridge\u002Ftransport codes (BRIDGE_FAILED, PARSE,\n    \u002F\u002F UNEXPECTED_PAYLOAD, NOT_IN_WRAP) and module-defined codes from ARCHITECTURE §5.\n    \u002F\u002F Cover what ARCHITECTURE §6.3 (error UX mapping) specifies; let the default branch handle\n    \u002F\u002F anything else.\n  }\n\n  \u002F**\n   * Bridge transport. Dispatches the composite key COMPOSITE_KEY (\"\u003Cname>\u002F\u003Creceiver>\")\n   * to NativeModules.\u003CnativeModule>.\u003CMETHOD> through the host-injected\n   * `window.PowerApps.NativeExtension.sendAsync` global, and RESOLVES the parsed\n   * \u003CPascal>Response object.\n   *\n   * REJECTS with:\n   *   - new Error(\"BRIDGE_FAILED\") when sendAsync resolves status !== \"ok\" (transport\u002Fproxy\n   *                                failure, e.g. \"native module 'X' not loaded\")\n   *   - new Error(\"PARSE\")         when the resolved data can't be reduced to an object\n   *   - new Error(\"NOT_IN_WRAP\")   when the host global is absent — Studio preview, a non-PAM\n   *                                web host, or CordovaV2 disabled. Do NOT fall back to\n   *                                cordova.exec — it is not exposed to the PCF sandbox.\n   *\n   * sendAsync resolves { status, data?, error? }. On status===\"ok\", `data` is the native\n   * method's resolved string; the host both re-stringifies it AND wraps it in a { isUpdate,\n   * message } transport container, so the raw is run through extractResponse (parse + unwrap\n   * the `message` container). The raw string is also captured into the usage=\"bound\" \u003Cname>Json\n   * output so a maker can read it on-device via Self.\u003Cname>Json when the WebView console is unreachable.\n   *\n   * The envelope is a RAW object { method, args: [request] } — the PCF does NOT stringify it;\n   * sendAsync does that internally. The inner `args` MUST be an array (the proxy spreads it\n   * positionally via fn.apply), so the single request object rides at args[0] → the native\n   * method's one ReadableMap\u002FNSDictionary param. There is no SDK-side timeoutMs to tune here.\n   *\u002F\n  private async invokeBridge(payload: \u003CPascal>Request): Promise\u003C\u003CPascal>Response> {\n    const bridge = window.PowerApps?.NativeExtension;\n    if (!bridge || typeof bridge.sendAsync !== \"function\") {\n      throw new Error(\"NOT_IN_WRAP\");\n    }\n    \u002F\u002F Envelope is a RAW object; args MUST be an array (one request object at args[0]).\n    const result = await bridge.sendAsync(COMPOSITE_KEY, { method: METHOD, args: [payload] });\n    \u002F\u002F Capture the raw wire response for the on-device diagnostic output.\n    this.\u003Cname>Json = typeof result === \"string\" ? result : JSON.stringify(result);\n    if (!result || result.status !== \"ok\") {\n      throw new Error((result && result.error) || \"BRIDGE_FAILED\");\n    }\n    \u002F\u002F result.data is re-stringified AND may be wrapped in the wrap transport's response\n    \u002F\u002F container ({ isUpdate, message:\"\u003Cjson of {status,result}>\" }). extractResponse parses\n    \u002F\u002F it and, if there's no top-level `status`, unwraps the `message` container — NOT a bare\n    \u002F\u002F parse (which would land on {isUpdate, message} → UNEXPECTED_PAYLOAD though native succeeded). §2.\n    const parsed = extractResponse(result.data);\n    if (parsed && typeof parsed === \"object\") {\n      return parsed as \u003CPascal>Response;\n    }\n    throw new Error(\"PARSE\");\n  }\n\n  public updateView(context: ComponentFramework.Context\u003CIInputs>): void {\n    this.context = context;\n    this.applyStyles();\n  }\n\n  public getOutputs(): IOutputs {\n    return {\n      \u002F\u002F ── One returned key per ARCHITECTURE §6.1 (output properties) output, reading the private field ──\n      \u002F\u002F Shape: `\u003C§8.3-property-name>: this.\u003CprivateFieldName>,`\n      \u002F\u002F The property name on the LEFT matches `\u003Cproperty name=\"...\">` in ControlManifest.Input.xml.\n      \u002F\u002F The field on the RIGHT is the private declared at the top of the class.\n      \u002F\u002F MUST include the three standard diagnostic outputs so the failure is visible in\n      \u002F\u002F Power Fx with no native debugger:\n      \u002F\u002F   Status: this.status,\n      \u002F\u002F   ErrorCode: this.errorCode,\n      \u002F\u002F   ErrorMessage: this.errorMessage,\n      \u002F\u002F ALSO include the usage=\"bound\" diagnostic so the raw response reaches Self.\u003Cname>Json:\n      \u002F\u002F   \u003Cname>Json: this.\u003Cname>Json,\n    };\n  }\n\n  public destroy(): void { \u002F* No-op for v0. *\u002F }\n}\n```\n\n### Adapting the skeleton for `with-preview` visual style\n\nWhen ARCHITECTURE §6.0 = `with-preview`, extend the skeleton above with a preview pane:\n\n1. **Add a preview element** in `init`:\n   ```typescript\n   this.previewEl = document.createElement(\"img\");   \u002F\u002F \u003Cimg> for image outputs; \u003Cdiv> for text\n   this.previewEl.style.maxWidth = \"100%\";\n   this.previewEl.style.maxHeight = \"200px\";   \u002F\u002F or per ARCHITECTURE §6.0 sizing\n   this.previewEl.style.display = \"none\";       \u002F\u002F hidden until first result\n   container.appendChild(this.previewEl);\n   ```\n\n2. **Update `setSuccess` to populate the preview**:\n   ```typescript\n   private setSuccess(result: \u003CPascal>Response[\"result\"]): void {\n     \u002F\u002F ... map to outputs as before ...\n     this.\u003CsuccessField> = result.\u003Cfield>;\n     \u002F\u002F Show preview\n     if (this.previewEl instanceof HTMLImageElement) {\n       this.previewEl.src = result.\u003CimageField>;\n       this.previewEl.style.display = \"block\";\n     }\n   }\n   ```\n\n3. **Update `setError` to hide the preview** (or show a placeholder):\n   ```typescript\n   private setError(code: string, message: string): void {\n     \u002F\u002F ... map error state as before (set status\u002FerrorCode\u002FerrorMessage) ...\n     this.previewEl.style.display = \"none\";\n   }\n   ```\n\n4. **`updateView` re-renders the preview** from the current output field (in case the maker changed bound input via Power Fx):\n   ```typescript\n   public updateView(context: ComponentFramework.Context\u003CIInputs>): void {\n     this.context = context;\n     this.applyStyles();\n     if (this.\u003CsuccessField>) {\n       this.renderPreview(this.\u003CsuccessField>);\n     }\n   }\n   ```\n\nThe preview type follows the output type per ARCHITECTURE §6.1 (output properties) → §8.0 mapping:\n- Base64 PNG \u002F data URI → `\u003Cimg>` element, `src = result.\u003Cfield>`\n- SVG string → `\u003Cdiv>` with `innerHTML` (caution: only if the SVG source is trusted — sanitize otherwise)\n- Plain text result → `\u003Cspan>` with `textContent`\n- Numeric output → `\u003Cspan>` with formatted number\n\n### Adapting for `inline-surface` (v1+ pattern)\n\nEmit a minimal skeleton with the trigger button replaced by a `\u003Cdiv>` for the interactive surface, plus a `\u002F\u002F TODO: implement the inline surface per ARCHITECTURE §6.0` comment. STOP with `DONE_WITH_CONCERNS: ARCHITECTURE §6.0 inline-surface style requires custom UI design beyond the v0 scaffold template`. The engineer fills in the interactive surface; the bridge wiring (`sendAsync` dispatch, `extractResponse`, error handling, output mapping) stays the same.\n\n### Step 5.5 — Emit the ambient `PowerAppsNativeExtension.d.ts`\n\nThe `index.ts` skeleton calls `window.PowerApps.NativeExtension.sendAsync`, so the TypeScript build needs the global typed. **Do NOT import the host SDK** (the PCF must stay host-agnostic and pin no SDK package). Write a one-file ambient declaration next to `index.ts` at `pcf\u002F\u003CPascal>PCF\u002FPowerAppsNativeExtension.d.ts`:\n\n```ts\n\u002F\u002F! Ambient declaration for the host-injected `window.PowerApps.NativeExtension`\n\u002F\u002F! global. Wired at boot by PAM's published-app-loader (when CordovaV2 is enabled).\n\u002F\u002F! A local declaration (rather than importing an SDK package) keeps the PCF\n\u002F\u002F! host-agnostic — any PCF can call `sendAsync` without pinning the SDK.\n\ninterface PowerAppsNativeExtensionResult {\n  status: \"ok\" | \"error\";\n  data?: unknown;\n  error?: string;\n}\n\ninterface Window {\n  PowerApps?: {\n    NativeExtension?: {\n      sendAsync(\n        extensionName: string,\n        payload: unknown,\n        options?: { timeoutMs?: number },\n      ): Promise\u003CPowerAppsNativeExtensionResult>;\n    };\n  };\n}\n```\n\nThis is a **required emit** — without it, `npm run build` (Step 6) fails to type-check `window.PowerApps`. `pcf-scripts` picks up any `*.d.ts` under the control folder automatically, so no `tsconfig` change is needed.\n\n### Hard rules for index.ts generation\n\n- **No `\u003Cplaceholder>` text** in the emitted file. Every value is substituted from the PRD.\n- **No `\u002F\u002F TODO`** comments. If a required mapping isn't in the PRD, STOP with `NEEDS_CONTEXT: PRD §\u003Cn>.\u003Cn> is incomplete for PCF code generation — re-run \u002Fdesign-native-extension-feature` and name the missing column.\n- **Dispatch via `sendAsync`, never `cordova.exec`.** The emitted `invokeBridge` MUST call `window.PowerApps.NativeExtension.sendAsync` with a RAW `{ method, args: [request] }` envelope. A direct `cordova.exec` call (or any `cordova.*` reference) in the PCF is a defect — it is not exposed to the PCF sandbox and fails silently on device (worst on Android).\n- **Cover every error code** from ARCHITECTURE §5 in `applyErrorState`. The default branch is for unknown codes only; don't skip cases by relying on the default.\n- **Verify symmetry with ARCHITECTURE §4.2.** Every output field declared in §8.3 must map to a response field in §5.2 (or to a derived constant from the error state). If §8.3 declares an output that §5.2 has no corresponding field for, STOP and ask the user.\n\n### Pattern coverage\n\n- **One-shot** — the structure above is sufficient. Generate complete code.\n- **Streaming** (§6 = streaming) — the `sendAsync` dispatch resolves one response; streaming would need the native module to push updates the PCF receives via an event subscription. The wrap bridge doesn't expose a clean PCF-side subscription API. For v0, emit the one-shot structure and add a `\u002F\u002F TODO (streaming): wire up update subscription when the wrap bridge exposes it` comment at the top of the file; STOP scaffolding with `DONE_WITH_CONCERNS: streaming PCF needs wrap-bridge support not yet available — emitted one-shot fallback`. Don't fabricate an API that doesn't exist.\n- **Two-way** (ARCHITECTURE §2 = two-way) — add a second button per operation that fires the follow-up message. Generate the second button + handler from ARCHITECTURE §3.\u003Cn>'s follow-up spec (which the design skill captures in the implementation walkthrough). If §3.\u003Cn> doesn't specify the follow-up surface, STOP with `NEEDS_CONTEXT: ARCHITECTURE §3.\u003Cn> doesn't specify the two-way UI affordance — re-run design walkthrough`.\n\nThe `NOT_IN_WRAP` rejection surfaces in the test harness (`pcf-scripts start watch` opens a browser preview where `window.PowerApps.NativeExtension` isn't injected) — that's expected. Don't try to mock the bridge in the harness; it's a known gap.\n\n---\n\n## Step 5b — Generate the manual-validation test harness\n\nGenerate `test-harness\u002F` (one level up from the PCF folder, at the extension repo root) so the engineer can build a real Canvas app that wires the PCF and confirm end-to-end behavior on a device.\n\nThere are two viable approaches; ship both, low-cost:\n\n### 5b.1 — A typed `test-harness\u002FREADME.md` with the exact Power Fx recipe\n\nWrite `test-harness\u002FREADME.md` containing step-by-step instructions, tailored from the PRD. Sections to include:\n\n```markdown\n# Test harness — \u003CHuman-Readable Name>\n\nManual validation recipe. Builds a Canvas app in Studio that wires the `\u003CPascal>PCF` control to confirm end-to-end behavior on a real wrap build.\n\n## Prerequisites\n- The dispatcher PCF is deployed to your environment: run `\u002Fpublish-pcf-companion` (handles publisher prefix selection, version bump, env confirmation, and the actual push).\n- A wrap build that has the `.ppmplugin` bundle loaded (the DEX\u002Fframework + `manifest.json` whose `\u003Cname>\u002F\u003Creceiver>` this PCF dispatches to) — see \u002Ftest-native-extension for the load recipe.\n- `pac auth list` shows your test environment as active.\n\n## Build the test app in Canvas Studio\n\n1. Open \u003Chttps:\u002F\u002Fmake.powerapps.com>; switch to your test environment.\n2. Apps → New app → Blank canvas → Phone layout.\n3. Insert → Custom → search `\u003CPascal>PCF` (under the `PowerApps` namespace).\n4. Drop it onto Screen1.\n5. In the right-side properties panel, set:\n   - `\u003CBoundName>` = \u003Cone realistic test value, e.g. an HTTPS URL or a base64 sample>\n   - `\u003Ceach configurable input>` = \u003Cleave default or set what ARCHITECTURE §6.1 (configurable inputs) says>\n6. Insert → Text label, name it `lblResult`. Set its `Text` property to:\n   `\u003CPascal>PCF1.\u003COutputName>` (the primary output from ARCHITECTURE §6.1 (output properties))\n7. (For more outputs:) Add one label per additional output property.\n\n## Run\n\n- Studio preview (▶): runs the PCF in the browser, where `window.PowerApps.NativeExtension` isn't injected; bridge calls reject with `NOT_IN_WRAP` — that's the expected signal that you need a real wrap build to test end-to-end.\n- Wrap on device: open the app in Power Apps Mobile (logged into the same tenant). The bridge dispatches, the native module fires, and the output property updates.\n\n## What to verify\n\nPer ARCHITECTURE §6.3 (error UX mapping) (Edge cases):\n\n| Trigger | Expected `\u003COutputName>` value |\n|---|---|\n| \u003Cerror code 1> | \u003Cexpected user experience> |\n| \u003Cerror code 2> | \u003Cexpected user experience> |\n| \u003Chappy path>   | \u003Cexpected user experience> |\n\n## Optional — save as a reusable .msapp\n\nIf you want to share the test app with another machine or check it into source control:\n\n```bash\n# Inside Studio: File → Save as → save to your Power Platform environment first.\n# Then export from Studio (File → See all versions → Download) — gives you a .msapp file.\n# Drop it into .\u002Ftest-harness\u002F\u003CPascal>.msapp and commit.\n```\n\nTo later unpack and edit as source:\n\n```bash\npac canvas unpack --msapp .\u002Ftest-harness\u002F\u003CPascal>.msapp --sources .\u002Ftest-harness\u002F\u003CPascal>-source\u002F\n```\n\nTo repack after editing:\n\n```bash\npac canvas pack --sources .\u002Ftest-harness\u002F\u003CPascal>-source\u002F --msapp .\u002Ftest-harness\u002F\u003CPascal>.msapp\n```\n```\n\nTailor the recipe to the PRD: substitute the actual bound input name + a realistic example value, list every configurable input from §8.2, list every output from §8.3, fill the verification table from §9 (one row per error code + the happy path).\n\n### 5b.2 — Skeleton Canvas source folder (best-effort)\n\nCreate `test-harness\u002Fcanvas-app-source\u002F` with the bare minimum Canvas source format:\n\n```\ntest-harness\u002Fcanvas-app-source\u002F\n├── CanvasManifest.json       ← minimal manifest with app name = \"\u003CPascal>-test\"\n├── Src\u002F\n│   ├── App.pa.yaml           ← App.OnStart = false; no special config\n│   └── Screen1.pa.yaml       ← \u003CPascal>PCF instance (compact default size) + lblResult bound to its output\n└── README.md                 ← \"Run: pac canvas pack --sources . --msapp ..\u002F\u003CPascal>.msapp\"\n```\n\n**Give the control instance a compact default size** in `Screen1.pa.yaml` (`Width: =200`, `Height: =44` for a button-style control — adjust per ARCHITECTURE §6 if it specifies a preferred footprint). The button fills the control box (`width\u002Fheight: 100%`), so the control's `Width`\u002F`Height` here ARE its rendered size — set them small so the sample app shows a normal-sized button, not one that fills a tall default cell.\n\n> ⚠️ **Canvas source format is fragile.** `Pa.yaml` is sensitive to indentation and specific control-property casing. The skeleton you generate may not import cleanly into Studio. If `pac canvas pack` errors at this step, **fall back to the manual-in-Studio recipe in 5b.1** — that's the supported v0 path. Treat the generated source folder as a convenience, not a guarantee.\n\nAfter 5b.2 succeeds (or is skipped on failure), the engineer always has 5b.1 as a fallback.\n\nUpdate `.gitignore` to allow the harness sources but exclude the `.msapp` binary by default (it's user-environment-specific):\n\n```\n# Test harness — source is committed, binary is per-machine\ntest-harness\u002F*.msapp\n```\n\n---\n\n## Step 5.7 — Self-verify the round-trip contract (before building)\n\nThe bridge round-trip is the class of bug that **passes `npm run build` and every zip\u002Fmanifest\u002FDEX check, then fails silently on device** — a composite key that doesn't match the manifest receiver, a `cordova.exec` call the sandbox drops, a pre-stringified envelope, a bare `args` object, or a success path that doesn't unwrap the wrap `message` container. Historically these were only caught at the very end by `\u002Faudit-ppmplugin` Category F. Run them **here**, over the `index.ts` you just emitted, so a gap is caught at generation instead.\n\nThis is **Gate 11 (PCF ↔ native round-trip contract)** from [`shared\u002Fself-critique-protocol.md`](..\u002F..\u002Fshared\u002Fself-critique-protocol.md). Re-read the `index.ts` (and `PowerAppsNativeExtension.d.ts`) fresh from disk, then check each invariant against the **resolved contract from Step 1.5** and [`shared\u002Fppmplugin-format.md §2`](..\u002F..\u002Fshared\u002Fppmplugin-format.md):\n\n| Check | Assert | Severity |\n|---|---|---|\n| Composite key | `COMPOSITE_KEY` in `index.ts` == `\u003Cmanifest.name>\u002F\u003Creceivers[].name>` resolved in Step 1.5; `METHOD` ∈ `receivers[].methods` | blocker |\n| Transport | dispatch is `window.PowerApps.NativeExtension.sendAsync`; NO `cordova.exec` \u002F `cordova.*` anywhere in `index.ts` | blocker |\n| Envelope | `sendAsync` payload is a **raw** `{ method: METHOD, args: [request] }` object — NOT `JSON.stringify(...)`'d | blocker |\n| Args array | the inner `args` is a JSON **array** with the request object at `args[0]` (not a bare object) | blocker |\n| Response unwrap | the success path calls `extractResponse` (parse `result.data` + unwrap the `{ isUpdate, message }` container), NOT a bare single `JSON.parse` | blocker |\n| Error mapping | every ARCHITECTURE §5 code AND the transport codes (`BRIDGE_FAILED`, `PARSE`, `UNEXPECTED_PAYLOAD`, `NOT_IN_WRAP`) reach a PCF output carrying **both** code and human message | concern |\n| Ambient global | `PowerAppsNativeExtension.d.ts` typing `window.PowerApps.NativeExtension.sendAsync` was emitted (Step 5.5) | concern |\n\n**Print a visible verdict block** (`shared-instructions.md §9.1`) and gate on it:\n\n```\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n Round-trip self-verify — \u003CPascal>PCF (dispatches \u003Cname>\u002F\u003Creceiver>)\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n ? ✓ Composite key matches manifest receiver; METHOD ∈ receivers[].methods\n ? ✓ sendAsync only (no cordova.exec); raw envelope; args is [request]\n ? ✓ extractResponse unwraps the { isUpdate, message } container\n ? ✓ All §5 + transport error codes map to outputs with a message\n```\n\n- Any **blocker** → **fix the emitted `index.ts` inline** (these are mechanical wiring fixes, not design choices — align the key, replace a `cordova.exec` with `sendAsync`, drop a stray `JSON.stringify`, wrap `args` in an array, route the success path through `extractResponse`), re-read, re-check. Only if a blocker is a genuine drift between the manifest and native source (not a PCF-emit mistake) → STOP with `NEEDS_CONTEXT` and route back per the Step 1.5 drift table.\n- Any **concern** → note it in the final summary and in `.extension-state.md`.\n- All clear → proceed to Step 6.\n\nThis does not replace `\u002Faudit-ppmplugin` Category F (the final gate over the built artifact) — it front-loads the same reasoning so the round-trip is right before the PCF is even built.\n\n---\n\n## Step 6 — Install and smoke-build\n\nInside `pcf\u002F\u003CPascal>PCF\u002F`:\n\n```bash\nnpm install              # PCF tooling is npm; do NOT use pnpm here\nnpm run build            # runs `pcf-scripts build`\n```\n\nThis regenerates `\u003CPascal>PCF\u002Fgenerated\u002FManifestTypes.d.ts` from the manifest, type-checks `index.ts`, and bundles. Output lands at `pcf\u002F\u003CPascal>PCF\u002Fout\u002F` (gitignored).\n\nIf `npm install` fails:\n- Network \u002F feed issues are less likely than for the extension's pnpm install — PCF tooling pulls from the public npm registry, not Azure Artifacts.\n- If it fails anyway, surface the failing line and direct to standard `npm install` troubleshooting.\n\nIf `npm run build` fails:\n- Manifest XML errors — pcf-scripts surfaces them with line numbers. Re-write the offending property.\n- TypeScript errors in `index.ts` — most likely a property name mismatch between the manifest and the code. Re-read the manifest's `\u003Cproperty name=\"...\">` values and align.\n\nDO NOT mark the PCF scaffold as complete in `.extension-state.md` until `npm run build` passes.\n\n### Deployment to a Power Platform environment\n\nNOT done in this skill. Run `\u002Fpublish-pcf-companion` when ready — it handles the just-in-time deploy prereq check (.NET SDK + active `pac auth`), the env-confirmation gate, and `pac pcf push`. This separation lets the engineer iterate on `index.ts` and the manifest locally without needing auth set up, then deploy when the work is ready to share.\n\n---\n\n## Step 7 — Update state and summarize\n\nUpdate `.\u002F.extension-state.md`:\n- Phase: `Last completed: scaffold-pcf` \u002F `Next: \u002Ftest-native-extension`.\n- Add a line under \"Validation history\": `PCF build (Layer 4): pass — \u003CISO timestamp>`.\n- Add a line: `Test harness: README ✓` and, if 5b.2 succeeded, `canvas-app-source ✓` (or `canvas-app-source skipped — pa.yaml generation failed` if it didn't).\n- Do NOT touch the \"PCF deployments\" section — deploy is `\u002Fpublish-pcf-companion`'s responsibility.\n\nPrint:\n\n```\nPCF scaffold complete\n─────────────────────\nFolder: pcf\u002F\u003CPascal>PCF\u002F\nManifest properties: 1 bound (\u003Cname>Json diagnostic), \u003Ccount input> input, \u003Ccount output> output\nDispatches: \u003Cname>\u002F\u003Creceiver> → NativeModules.\u003CnativeModule>.\u003Cmethod> via window.PowerApps.NativeExtension.sendAsync\nBuild: npm install ✓ | npm run build ✓\nDeployment: not yet deployed (run \u002Fpublish-pcf-companion when ready)\n\nNext steps\n──────────\n1. Run \u002Ftest-native-extension to validate the contract now that native + manifest + PCF all exist (Layer 0 cross-check + Layer 4 PCF compile) — cheap pre-flight before the binary build.\n2. Run \u002Fgenerate-ppmplugin to produce the verified `.ppmplugin` binary bundle — it reads this PCF's composite key back so the manifest's receiver stays aligned.\n3. Run \u002Fpublish-pcf-companion to push the dispatcher PCF to a Power Platform environment (handles deploy prereq check, env confirmation, and `pac pcf push`).\n4. (Iterate) If ARCHITECTURE §6 changes, re-run \u002Fgenerate-pcf-companion — it will detect the existing folder and ask before regenerating.\n```\n\n---\n\n## Step 8 — Offer next-step skills\n\nPCF generation is the natural moment to make several decisions — the dispatcher PCF now exists, so building the `.ppmplugin` bundle and deploying are both in play. Per `shared\u002Fshared-instructions.md §9.1`, use `AskUserQuestion` with all plausible next skills as options (not 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 \u002Ftest-native-extension\"\n     description: \"Validate the contract now that all three sides exist — native module ↔ .\u002Fmanifest.json ↔ this PCF (Layer 0 cross-check: composite key, nativeModule, methods, request\u002Fresponse\u002Ferror parity) + Layer 4 PCF compile. Cheap structural pre-flight (seconds) that catches drift before the minutes-long binary build. Recommended next step.\"\n  2. \"Run \u002Fgenerate-ppmplugin\"\n     description: \"Produce the verified .ppmplugin binary bundle end-to-end (manifest → build → assemble → audit). Reads this PCF's composite key back so the manifest's receiver stays aligned.\"\n  3. \"Run \u002Fpublish-pcf-companion\"\n     description: \"Deploy the dispatcher PCF to a Power Platform environment for Studio testing. Three gates: publisher prefix, version bump, env confirmation.\"\n  4. \"Stay — I'll review the PCF code first\"\n     description: \"Skill exits. Inspect pcf\u002F\u003CX>PCF\u002Findex.ts and the manifest, decide what's next yourself.\"\n```\n\n**For options 1 (test), 2 (generate-ppmplugin), or 3 (publish):** **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 prereq check + gates.\n\n**For option 4 (stay):** print one line: `PCF scaffold complete. Run any of the suggested skills when you're ready.` Then proceed to return-status.\n\n(Need diagnostic logging? Just ask — `console.log` \u002F `NSLog` \u002F `Log.d` can be added to any code area on request; there's no dedicated skill for it. Note: in the wrap runtime a third-party control's native logs aren't surfaced through normal dev tooling, so logging is of limited use here anyway.)\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` | PCF generated, manifest matches PRD, `npm run build` passed. State file updated. |\n| `DONE_WITH_CONCERNS: \u003Clist>` | Build passed but with non-fatal warnings (e.g. streaming pattern requires UI work the scaffold left as TODO). |\n| `NEEDS_CONTEXT: \u003Cmissing>` | A required ARCHITECTURE §6 sub-section was incomplete and we couldn't proceed. |\n| `BLOCKED: \u003Creason>` | Prereq failed, `pac pcf init` failed, or `npm run build` 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** PCF code from a PRD. It's not a linter; it doesn't audit existing working code against the new template. When the skill is re-run on a repo that already has a `pcf\u002F` folder, the regenerate \u002F resume \u002F abort gate asks before overwriting — and the user should pick \"resume\" or \"abort\" unless they explicitly want a fresh template.\n\n**A diff between \"what we'd generate now\" and \"what exists\" is NOT a list of defects.** Existing PCF code that achieves the same runtime behavior through a different code shape is fine. Don't list stylistic deltas as issues. Flag only:\n\n- Code that doesn't compile (`npm run build` failing)\n- Code that produces incorrect outputs (wrong field names, missing error handling that lets crashes propagate)\n- Genuine dispatch-contract violations (wrong composite key, wrong `method` name, or a payload shape the `@ReactMethod` can't parse — such that runtime breaks)\n\nA `BridgeResponse` type defined with a different name is NOT a defect. An `applyErrorState(code)` method instead of `setSuccess`\u002F`setError` is NOT a defect. A hand-rolled unwrap that reaches the `{status,…}` object instead of `extractResponse` is NOT a defect. The template prefers one shape; equivalent shapes are not \"wrong.\"\n\n## Hard rules — correctness (these must be true for the PCF to work)\n\n- **PCF tooling uses npm, not pnpm** for the inner `pcf\u002F\u003CPascal>PCF\u002F` package. PCF ecosystem convention. Mixing produces lockfile chaos.\n- **Manifest namespace is always `PowerApps`.** Don't let the user override; this is how Canvas groups native-extension PCFs.\n- **The composite key (`COMPOSITE_KEY` constant) MUST equal `\u003Cname>\u002F\u003Creceiver>` from `manifest.json`** and match the receiver the manifest registers. Mismatch = the wrap host can't route, silent runtime failure.\n- **`METHOD` MUST be a real `@ReactMethod` \u002F `RCT_EXPORT_METHOD` name** from `receivers[].methods`. An unknown method = `method '\u003Cm>' not found` on device.\n- **All required fields from `\u003CPascal>Request` MUST be present in the dispatched args.** Missing fields = native parses `undefined` = INVALID_INPUT at runtime.\n- **`display-name-key` and `description-key` MUST be human-readable strings**, not programmer keys ending in `_Display` \u002F `_Desc`. Studio shows these verbatim when no `.resx` ships.\n- **Every output declared in ARCHITECTURE §6.1 (output properties) MUST be returned by `getOutputs()`.** TypeScript catches this via `IOutputs` typing; don't disable that.\n- **`npm run build` MUST pass** before declaring scaffold success.\n- **Manifest property types from ARCHITECTURE §6** must be PCF-valid (`SingleLine.Text`, `Whole.None`, etc.) — map the module's response field type to its PCF property type.\n- **Exactly ONE `usage=\"bound\"` property — the `\u003Cname>Json` raw-response diagnostic — and no other.** Every domain property is `input` (maker sets) or `output` (control produces).\n\n## Recommended template style (preferences for new scaffolds; existing code that works is fine)\n\nThese describe the cleanest shape for newly-generated PCF code. They're how this skill renders fresh output. Existing working code that takes a different path is **not** in violation — don't flag stylistic differences as defects when auditing.\n\n- **Prefer the `extractResponse` helper** over a raw `JSON.parse(result.data)`. The wrap host re-stringifies the response AND nests it in a `{ isUpdate, message }` container; `extractResponse` parses the string layers and unwraps the `message` container (returning `result.data` directly when it already has a top-level `status`). A bare single parse lands on the container → `UNEXPECTED_PAYLOAD` though native succeeded. An inline unwrap that reaches the `{status,…}` object works fine too.\n- **Prefer narrowing the parsed result to `\u003CPascal>Response`** and branching on `response.status` over ad-hoc shape detection. Defensive code that achieves the same outcome works fine.\n- **Prefer modeling `\u003CPascal>Request` \u002F `\u003CPascal>Response` inline** (a native-only bundle ships no shared TS layer to import). Mirror exactly what the `@ReactMethod` parses and resolves; inline annotations scattered at each call site drift more easily.\n- **Prefer the `sendAsync` envelope `{ method, args: [request] }`** dispatched on the composite `COMPOSITE_KEY` over a hand-built ad-hoc envelope. The envelope is a **RAW object** — the PCF does NOT stringify it (`sendAsync` does); a PCF that pre-stringifies double-encodes → `BRIDGE_FAILED`. NEVER call `cordova.exec` directly.\n- **Prefer the nested if\u002Felse with `setSuccess`\u002F`setError` helpers** for the response branch. An `applyErrorState` method that handles the same cases is functionally equivalent.\n\n## Things the skill enforces at generation time (only when generating fresh code)\n\nWhen emitting a new `index.ts` from scratch, the skill follows the template above. Strict requirements:\n\n- **No `\u003Cplaceholder>` text in the emitted file.** Substitute every value from the PRD or use a sensible default. Placeholders that compile (like `\u003CPascal>` substituted as the actual class name) are fine; literal `\u003CPLACEHOLDER>` strings that ship into runtime are not.\n- **Map every output declared in ARCHITECTURE §6.1 (output properties).** Each output needs an assignment in `setSuccess` (or equivalent), an initialization at the field declaration, and a return entry in `getOutputs()`. Missing outputs cause TypeScript to complain via `IOutputs`.\n- **Cross-check `manifest.json receivers[]` against native source in Step 1.5.** If `nativeModule` \u002F method names drift, STOP with `NEEDS_CONTEXT` — that's a real bug the user must reconcile.\n- **STOP with `NEEDS_CONTEXT` only when blocked.** Genuine blockers: missing manifest\u002FARCHITECTURE name+receiver (no composite key derivable), missing ARCHITECTURE §4 (no request\u002Fresponse shape), missing ARCHITECTURE §6 (no PCF surface). Non-blockers that should NOT STOP: ARCHITECTURE §6.3 (error UX mapping) missing UX detail for a specific error code (use the default error mapping), §8.2 missing a default value for a configurable input (use the type's natural default), PRD §1 summary brevity (don't need to STOP for cosmetic prose).\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 — streaming subscription wiring when the SDK doesn't yet expose it, preview thumbnail layout, button corner radius \u002F animation timing not specified in ARCHITECTURE §6.1 (configurable inputs), edge-case UX the PRD intentionally defers. A clear annotated TODO is better than fabricating arbitrary defaults.\n- **Fallback to a generic error mapping** in `setError` for codes not explicitly listed in ARCHITECTURE §6.3 (error UX mapping). The default branch handling \"any code → status='error', errorCode=code, errorMessage=message, result-fields=defaults\" is the right answer for codes the PRD doesn't customize. The message always flows through, even for unrecognized codes.\n- **Defensive `?? \u003Cdefault>` on optional response fields.** If a response field is typed `string | undefined` in `\u003CPascal>Response`, `result.fieldX ?? \"\"` at the read site is correct; don't refuse to compile because the source is optional.\n\nWhen the skill encounters an existing PCF folder, the gate offers regenerate \u002F resume \u002F abort — and the user owns that choice. Resume or abort preserve the existing code; regenerate overwrites. **The skill doesn't second-guess existing code unless the user explicitly asks for a regen.**\n\n## Runtime fallbacks the generated PCF SHOULD have\n\nThese are correctness-positive — the skill should ensure they're present. They make the PCF resilient against unexpected runtime conditions that aren't bugs in the PCF:\n\n- **Try\u002Fcatch around `invokeBridge`** for Studio preview (host global not injected, throws `NOT_IN_WRAP`) → `setError(\"NOT_IN_WRAP\", \u003Creason>)` so the not-in-wrap state is diagnosable rather than masked as a generic internal error. Already in the template.\n- **`extractResponse` in `invokeBridge`** to peel the host's re-stringification AND unwrap the wrap `{ isUpdate, message }` response container off `result.data`; total-fail throws `PARSE`. The raw response is also captured into the `\u003Cname>Json` diagnostic output. Already in the template.\n- **Structured JSON logging** (`logDebug`\u002F`logError` with a per-tap `seq`) at each dispatch step so a failure is diagnosable from a single grep-able line even when the WebView console is barely reachable. Already in the template.\n- **`\u003Cname>Json` on-device diagnostic** (`usage=\"bound\"`) surfacing the raw bridge response so a maker can read what came back via `Self.\u003Cname>Json` when the WebView console is unreachable on a release wrap build. Already in the template.\n- **Default error mapping in `setError`** for codes the skill couldn't anticipate (the wrap shell or module emitting new codes between releases) → status='error', code surfaced as-is, message surfaced as-is, result fields zeroed.\n- **`UNEXPECTED_PAYLOAD` branch** when the parsed response doesn't match `\u003CPascal>Response` shape → `setError(\"UNEXPECTED_PAYLOAD\", \u003Cserialized-payload-snippet>)`. Catches deployed-module-version drift gracefully instead of letting a TypeError propagate — and the snippet shows exactly what shape arrived.\n- **Optional-field guards** (`result.fieldX ?? \u003Cdefault>`) when mapping response fields to outputs. If a response field is documented as optional in `\u003CPascal>Response`, the PCF reads it defensively.\n\nThese are runtime safety nets, not template style. They make the PCF degrade gracefully when something unexpected happens at runtime — and they're allowed\u002Fencouraged regardless of what other style decisions the code makes.\n\n## Diagnostic logging the generated PCF SHOULD have\n\nIn production, the #1 problem when a PCF doesn't work is \"I have no idea what happened.\" Studio's debugger isn't available on device; the Power Apps Mobile log surface is `console.log` redirected to the device console (Xcode Console for iOS, `adb logcat` for Android).\n\nThe generated PCF should `console.log` at decision points with a consistent prefix so production debugging is possible without re-deploying with extra instrumentation:\n\n```typescript\n\u002F\u002F Pattern: console.log(\"[\u003CPascal>PCF] \u003Cevent>:\", \u003Crelevant context>);\n\nconsole.log(\"[\u003CPascal>PCF] Dispatching:\", COMPOSITE_KEY, METHOD, request);\nconsole.log(\"[\u003CPascal>PCF] Bridge returned - status:\", response?.status, \"error:\", response?.error);\nconsole.log(\"[\u003CPascal>PCF] Module success - \u003Ckey field>:\", response.\u003Cfield>);\nconsole.error(\"[\u003CPascal>PCF] Module error:\", response.error);\nconsole.error(\"[\u003CPascal>PCF] Unexpected payload shape:\", response);\nconsole.error(\"[\u003CPascal>PCF] Bridge error:\", code);\nconsole.log(\"[\u003CPascal>PCF] Output state - status:\", this.status, \"errorCode:\", this.errorCode, \"errorMessage:\", this.errorMessage);\n```\n\nThis is light enough not to spam, dense enough to diagnose a field issue from a log dump. Prefix with `[\u003CPascal>PCF]` so a `adb logcat | grep PCF` filter works.\n\n## Lessons baked into this template (production-PCF wisdom)\n\nThings we learned the hard way from earlier extensions; the template now handles them by default so future PCFs don't repeat the mistake:\n\n| Lesson | How the template addresses it |\n|---|---|\n| Host re-stringifies `result.data` AND wraps it in a `{ isUpdate, message }` container; a raw read gets `{isUpdate, message}` (no `status`) → `UNEXPECTED_PAYLOAD` | `invokeBridge` runs `result.data` through `extractResponse` (parse string layers + unwrap the `message` container); non-object → `PARSE`. |\n| Release wrap build's WebView console is unreachable from logcat \u002F `chrome:\u002F\u002Finspect` | Template exposes the `\u003Cname>Json` `usage=\"bound\"` output carrying the raw response — readable on-device via `Self.\u003Cname>Json`. |\n| PCF dispatched a different receiver than the manifest registered (real bug: PCF → `Snapshot`, manifest → `DeviceInfoExtension`) | `COMPOSITE_KEY` is `\u003Cname>\u002F\u003Creceiver>` resolved from `manifest.json`; Step 1.5 confirms it matches the registered receiver. |\n| Unknown method dispatched → `method '\u003Cm>' not found` on device | `METHOD` is taken from `receivers[].methods`, verified against a real `@ReactMethod` \u002F `RCT_EXPORT_METHOD` in Step 1.5. |\n| Display names show `_Display_Key` literal strings to makers | Manifest uses human-readable text from PRD §2 \u002F §8 |\n| Publisher prefix > 8 chars rejected by `pac pcf push` | `\u002Fpublish-pcf-companion` Gate 3.0 validates length 2–8 |\n| Hard-coded paths break on layout variation | `find pcf -name ControlManifest.Input.xml` discovers regardless of nesting |\n| Field issues unobservable without re-deploying with instrumentation | Diagnostic logging at decision points, in by default |\n| Re-publish without version bump → app cache doesn't refresh | `\u002Fpublish-pcf-companion` Gate 3.1 prompts for version bump |\n\n## Other operational rules\n\n- **No PCF push without an explicit confirmation gate.** Deploy is `\u002Fpublish-pcf-companion`'s job; this skill is local-only.\n- **Don't try to test the PCF end-to-end here.** That requires a wrap build with the `.ppmplugin` bundle loaded — that's `\u002Ftest-native-extension` (manual recipe).\n- **Two-way pattern (ARCHITECTURE §2 = two-way)**: add a follow-up button per the ARCHITECTURE §3.\u003Cn> spec. If §3.\u003Cn> doesn't specify the UI, STOP with `NEEDS_CONTEXT`.\n- **Streaming pattern (ARCHITECTURE §2 = streaming)**: the wrap `SendMessagePlugin` bridge doesn't expose a PCF-side subscription API. Emit the one-shot structure with a top-of-file comment flagging the streaming gap; STOP with `DONE_WITH_CONCERNS`.\n",{"name":5,"model":33,"description":7,"allowed-tools":56},"Read, Write, Edit, Bash, Glob, Grep, AskUserQuestion, Skill","main","0a55a88fa2855404029d1e5450faf92878baf763","plugins\u002Fpower-apps-mobile-extension\u002Fskills\u002Fgenerate-pcf-companion",232,"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-14 21:46:39",{"source":8,"skillName":5,"commands":68,"prompt":77,"zipAvailable":78},[69,72,75],{"tool":70,"command":71},"npx","npx skills add microsoft\u002Fpower-platform-skills --skill generate-pcf-companion",{"tool":73,"command":74},"bunx","bunx skills add microsoft\u002Fpower-platform-skills --skill generate-pcf-companion",{"tool":37,"command":76},"pnpm dlx skills add microsoft\u002Fpower-platform-skills --skill generate-pcf-companion","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002Fpower-platform-skills\u002Fblob\u002Fmain\u002Fplugins\u002Fpower-apps-mobile-extension\u002Fskills\u002Fgenerate-pcf-companion\u002FSKILL.md",true,[],[81,102,122,140,154,164],{"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":51},"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,43,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":51},"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,32,88,121],"report issue","report","issue","support","user","bug","summary","draft","operation","bug-report",{"skillNo":123,"name":124,"nameZh":6,"description":125,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":126,"categories":127,"keywords":128,"publishedAt":51},"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,129,130,131,88,132,133,134,135,43,136,94,137,138,139],"investigate","investigate process","mining","process mining","investigation","perspectives","question","business","pattern","across","analytical",{"skillNo":141,"name":142,"nameZh":6,"description":143,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":144,"categories":145,"keywords":146,"publishedAt":51},"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",[],[147,148,43,94,149,150,151,152,153,88],"metric","derive metric","formula","missing","reuse","saved","custom",{"skillNo":155,"name":156,"nameZh":6,"description":157,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":158,"categories":159,"keywords":160,"publishedAt":51},"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",[],[161,162,88,94,95,163,43],"compare","compare cohorts","results",{"skillNo":165,"name":166,"nameZh":6,"description":167,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":168,"categories":169,"keywords":170,"publishedAt":51},"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",[],[171,172,173,132,88,131,174,175,94,95,43,176,177],"analyze","variants","analyze variants","case","paths","work","common"]