[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M699QTY4V7EX2RW0YHFFHKEF":3,"skill-same:01M699QTY4V7EX2RW0YHFFHKEF":72},{"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":18,"publishedAt":41,"linkOnly":42,"markdownContent":43,"markdownBody":44,"frontMatter":45,"refName":48,"commitSha":49,"skillDirectory":50,"forksCount":51,"repositoryHtmlUrl":52,"repositoryHomepageUrl":53,"ownerHtmlUrl":54,"ownerBlogUrl":55,"ownerDescription":56,"updatedAt":57,"install":58,"resources":71},"01M699QTY4V7EX2RW0YHFFHKEF","generate-ppmplugin-manifest",null,"Validate, reconcile to the chosen target(s), and stage the `manifest.json` for a `.ppmplugin` bundle. In the normal flow the committed `.\u002Fmanifest.json` already exists (authored by \u002Fgenerate-native-extension), so this stage reads it, runs the plugin's upload-compatibility checks locally as a pre-flight gate (name regex, canonical prefix, known incompatible names, method and identifier shapes), reconciles `entrypoints` down to the platform(s) you ship, and writes the gitignored staged copy `ppmplugin\u002Fstaging\u002Fmanifest.json` that \u002Fassemble-ppmplugin zips. If no committed manifest exists (a hand-authored module) it falls back to deriving every field from the class name and Android module source, writing both the committed file and the staged copy. No toolchain — pure read, validate, write. Target-aware. Run after the native module exists, before the build skills.","microsoft\u002Fpower-platform-skills","microsoft","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F6154722?v=4","01M698VYDXZ6JW5WH3CRV650R8","Microsoft","01M699JVYJFB75D9JM70BN4H0Q",1134,"MIT","https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002Fpower-platform-skills\u002Fblob\u002Fmain\u002Fplugins\u002Fpower-apps-mobile-extension\u002Fskills\u002Fgenerate-ppmplugin-manifest\u002FSKILL.md",[],[19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40],"generate ppmplugin manifest","generate ppmplugin","generate","manifest","write","normal","source","hand","canonical","you","native","back","assemble","reconcile","edit","writing","read","Bash","flow","run","validate","android","2026-11-10 10:12:30",false,"---\nname: generate-ppmplugin-manifest\ndescription: \"Validate, reconcile to the chosen target(s), and stage the `manifest.json` for a `.ppmplugin` bundle. In the normal flow the committed `.\u002Fmanifest.json` already exists (authored by \u002Fgenerate-native-extension), so this stage reads it, runs the plugin's upload-compatibility checks locally as a pre-flight gate (name regex, canonical prefix, known incompatible names, method and identifier shapes), reconciles `entrypoints` down to the platform(s) you ship, and writes the gitignored staged copy `ppmplugin\u002Fstaging\u002Fmanifest.json` that \u002Fassemble-ppmplugin zips. If no committed manifest exists (a hand-authored module) it falls back to deriving every field from the class name and Android module source, writing both the committed file and the staged copy. No toolchain — pure read, validate, write. Target-aware. Run after the native module exists, before the build skills.\"\nallowed-tools: Read, Write, Edit, Bash, Glob, Grep, AskUserQuestion, Skill\nmodel: sonnet\n---\n\n# \u002Fgenerate-ppmplugin-manifest\n\nStages the `manifest.json` that goes inside a `.ppmplugin` bundle — the small descriptor the wrap runtime and upload service read to identify the plugin and route calls into it. In the normal flow the **committed `.\u002Fmanifest.json` already exists** (authored by [`\u002Fgenerate-native-extension`](..\u002Fgenerate-native-extension\u002FSKILL.md) next to the code it describes); this stage's job is to **validate it, reconcile its `entrypoints` to the shipped target(s), and write the staged build copy** `ppmplugin\u002Fstaging\u002Fmanifest.json`. It is the \"get the strings right\" gate: it runs instantly, needs no build tools, and catches common upload failures before any Android build. If the repo has no `.\u002Fmanifest.json` (a hand-authored native module that skipped the scaffold), this stage **authors it from source** as a fallback — see Step 2.\n\nRead [`shared\u002Fppmplugin-format.md`](..\u002F..\u002Fshared\u002Fppmplugin-format.md) — it is the source of truth for the schema, the derivation table, and the validation rules this skill enforces.\n\n**Two manifests, one contract.** `.\u002Fmanifest.json` (repo root, **committed**) is the source-of-truth contract — it declares *every platform the module supports*. `ppmplugin\u002Fstaging\u002Fmanifest.json` (gitignored) is the **build copy** this stage produces — same contract, but `entrypoints` trimmed to the platform(s) actually being shipped. The build\u002Fassemble skills only ever read the staged copy; the committed root file is what the PCF and humans read.\n\n## What this skill does NOT do\n- Does not compile anything — no Gradle, no DEX. That's [`\u002Fbuild-android-binary`](..\u002Fbuild-android-binary\u002FSKILL.md).\n- Does not zip the bundle — that's [`\u002Fassemble-ppmplugin`](..\u002Fassemble-ppmplugin\u002FSKILL.md).\n- Does not verify a declared platform's binary actually exists — it stages the *intended* `entrypoints`; [`\u002Fassemble-ppmplugin`](..\u002Fassemble-ppmplugin\u002FSKILL.md) reconciles them against the binaries actually staged and gates on any mismatch.\n- Does not rewrite the committed `.\u002Fmanifest.json` on the normal path — it reads it. It only *writes* `.\u002Fmanifest.json` in the fallback case (no committed manifest existed). It never touches `src\u002F`, `ios\u002F`, `android\u002F`, PRD, or `package.json` beyond reading them.\n\n---\n\n## Step 1 — Read shared docs + prereq block\n\n1. Read [`shared\u002Fshared-instructions.md`](..\u002F..\u002Fshared\u002Fshared-instructions.md), [`shared\u002Fnaming-conventions.md`](..\u002F..\u002Fshared\u002Fnaming-conventions.md), and [`shared\u002Fppmplugin-format.md`](..\u002F..\u002Fshared\u002Fppmplugin-format.md).\n2. This skill does no installs \u002F auth \u002F network. Print the zero-prereq block (per shared-instructions §9.2):\n\n```\nPrereq check — \u002Fgenerate-ppmplugin-manifest: skipped (skill does no installs \u002F auth \u002F network — failures surface at validation).\n```\n\n3. Confirm the working directory is a third-party-control repo: a `package.json` (the **dev-only, private, plain `\u003Ckebab>-control`** name — NOT a published `@powerapps\u002Fextension-*` scope; this track ships a binary, not an npm package — see [`repo-layout.md`](..\u002F..\u002Fshared\u002Frepo-layout.md)) **and** an `android\u002F` and\u002For `ios\u002F` native module exist. If not, STOP with `NEEDS_CONTEXT: not a third-party-control repo (no package.json \u002F native module)`.\n4. Read `.extension-state.md` — if it carries a `## ppmplugin (third-party controls)` block, note the last **target** choice and last manifest write (the re-run mode below uses them).\n\n---\n\n## Step 1.5 — Locate the source manifest (which mode are we in?)\n\nThis stage has two modes, decided by whether the committed `.\u002Fmanifest.json` exists:\n\n- **Validate-and-stage mode (the normal flow).** `.\u002Fmanifest.json` exists at the repo root (authored by `\u002Fgenerate-native-extension`). This is the source of truth — **do not re-author it**. Read it, validate it (Step 3), reconcile its `entrypoints` to the chosen target (Step 2 → *Target* only), and write the staged copy (Step 4). Step 2's field-derivation is **skipped** — the contract is already authored; you're verifying and staging it, not regenerating it. (Optionally re-derive `methods` from the Android module source and, if they've drifted from `.\u002Fmanifest.json` — e.g. a `@ReactMethod` was added by hand after scaffold — surface the diff and offer to update the committed file; never silently rewrite it.)\n\n- **Author-from-source mode (fallback).** No `.\u002Fmanifest.json` exists — a hand-authored native module that skipped the scaffold. Derive every field mechanically from source (Step 2 in full), then write **both** the committed `.\u002Fmanifest.json` and the staged copy (Step 4).\n\n**Re-run within a build session.** If the *staged* copy `ppmplugin\u002Fstaging\u002Fmanifest.json` already exists from a prior run, don't blindly overwrite — diff the target\u002Fentrypoints against the committed source and ask via `AskUserQuestion`: **Update** (re-stage from the current `.\u002Fmanifest.json` + target) \u002F **Keep as-is** (report and stop). Default the target to the prior choice in `.extension-state.md`; don't re-ask an answered question.\n\n---\n\n## Step 2 — Determine target(s); derive fields only in author-from-source mode\n\nThe structure preflight + target selection below run in **both** modes (you always need to know which platforms are viable and which to ship). The **field-derivation** sub-section (items 1–6 + the compute block) runs **only in author-from-source mode** (Step 1.5) — in the normal validate-and-stage mode the fields already live in `.\u002Fmanifest.json`; read them from there and skip derivation, keeping only the *target* choice.\n\n**First, run a structure preflight.** The ppmplugin path expects the canonical PAM-extension layout — the shape `\u002Fgenerate-native-extension` produces (see [`shared\u002Frepo-layout.md`](..\u002F..\u002Fshared\u002Frepo-layout.md)). A hand-rolled, foreign, or drifted repo may be missing pieces; flag exactly *what*, here and now, rather than failing later with a cryptic Gradle\u002FXcode error or a silently-wrong manifest. Print a visible ✓\u002F✗ block:\n\n- **Common:** `package.json` (dev-only `\u003Ckebab>-control` name — there is **no** `@powerapps\u002Fextension-*` scope and **no** `src\u002F` TS layer in this track); class name resolvable from the **native module** (`android\u002F...\u002F\u003CPascal>Module.kt` \u002F `ios\u002FRCT\u003CPascal>Module.m`), `.\u002Fmanifest.json`, or `.extension-state.md`.\n- **Android** (if `android\u002F` present): `android\u002Fbuild.gradle`; a Kotlin module class extending `ReactContextBaseJavaModule` with an `override fun getName()`; a `ReactPackage` class; ≥1 `@ReactMethod`.\n- **iOS** (if `ios\u002F` present): `ios\u002FRCT\u003CPascal>Module.h` declaring `\u003CRCTBridgeModule>`; `.m` with `+ (NSString *)moduleName` (**NOT** `RCT_EXPORT_MODULE`) and ≥1 `RCT_EXPORT_METHOD`.\n\nA platform whose structure is **incomplete is not a valid target** — exclude it and say which element is missing. If neither platform is structurally complete, STOP with `NEEDS_CONTEXT: repo doesn't match the expected PAM-extension layout — missing \u003Clist>` pointing at `shared\u002Frepo-layout.md`. (Working on a staged copy protects the *source*; it does NOT make a missing module appear — that's what this preflight is for.)\n\n**Then determine which platform(s) this bundle targets** (only from the structurally-complete ones) — it controls which `entrypoints` get declared:\n- Confirm via `AskUserQuestion`: **Android-only** \u002F **iOS-only** \u002F **Both** — offer only the targets that passed the preflight. Default to what's present, or — on a re-run in **Update** mode — to the prior target recorded in `.extension-state.md`.\n- Note availability: `\u002Fbuild-android-binary` is stable; `\u002Fbuild-ios-binary` is **v0** (Mac-only; **known limitation:** its React-Core weak-link config still needs validation against a live PAM\u002Fwrap shell). If the user targets iOS\u002FBoth, the manifest declares `entrypoints.ios`; `\u002Fassemble-ppmplugin` will still gate if the iOS binary isn't staged at packaging time.\n\n**Then derive fields from the actual files, not from assumptions** *(author-from-source mode only — in validate-and-stage mode skip to Step 3 with the fields read from `.\u002Fmanifest.json`)*:\n\n1. **Class name** `\u003CPascal>` — from the **native module** (the Android `\u003CPascal>Module.kt` filename \u002F its `getName()` = `\"\u003CPascal>Module\"`, or the iOS `RCT\u003CPascal>Module`), or the `.extension-state.md` Identity block. There is **no** `src\u002F\u003CPascal>Extension.ts` in this track. This is the basis for `name`, `nativeModule`, `receivers[].name`.\n2. **`version`** — `package.json` `version`.\n3. **`nativeModule`** — read the Android module's `override fun getName(): String = \"\u003CX>\"`. Use `\u003CX>` verbatim.\n4. **`packageClass`** — read the `ReactPackage` `.kt` file: combine its `package \u003C...>` line with the class name → FQN (e.g. `com.powerapps.peninput.PenInputPackage`).\n5. **`methods`** — scan the Android module (via the Read\u002FGrep tools, not a shell-specific command — this skill is OS-neutral) for `@ReactMethod` and collect each annotated function name. Cross-check the count against the operations in `ARCHITECTURE.md §3` \u002F `PRD §4`; if a documented operation has no matching `@ReactMethod`, surface it as a warning (the manifest reflects what the binary actually exposes, but the mismatch usually means an operation wasn't wired).\n\n6. **iOS entrypoint fields** *(only when targeting iOS \u002F Both)* — read `ios\u002FRCT\u003CPascal>Module.h` for the class name (`@interface RCT\u003CPascal>Module : NSObject \u003CRCTBridgeModule>`) → that is `entrypoints.ios.moduleClass`. Read `ios\u002FRCT\u003CPascal>Module.m` for `+ (NSString *)moduleName { return @\"\u003CX>\"; }` and **assert `\u003CX>` equals the `nativeModule` from step 3** — it's the same bridge symbol on both platforms; a mismatch means the iOS and Android modules disagree. (The `.m` must **not** use `RCT_EXPORT_MODULE` — that macro's `+load` registration is invisible to the framework's `dlopen` flat namespace, so the module never loads on device.)\n\nThen compute, per [`ppmplugin-format.md §3`](..\u002F..\u002Fshared\u002Fppmplugin-format.md):\n\n- `name = kebab(className)` — **from the class name, not the capability\u002Frepo name.** If `kebab(className)` differs from the repo's capability kebab (e.g. repo `powerapps-pdf-control` but class `PdfViewer` → `name: pdf-viewer`), print a one-line note so the user knows the `.ppmplugin` filename won't match the repo name. This is required for the canonical-prefix rule to pass.\n- **`receivers[].name` — take it from the PCF if one exists, do NOT blindly default.** If a sibling PCF is present (`pcf\u002F\u003C…>\u002Findex.ts`), grep it for the dispatch key — `COMPOSITE_KEY = \"\u003Cname>\u002F\u003Creceiver>\"` (or the `ReceiverKey` it binds) — and use **that** `\u003Creceiver>` value, because the PCF already dispatches to it; a manifest that registers a different receiver name will fail on first dispatch (real bug: PCF dispatched to `Snapshot` while the manifest defaulted to `DeviceInfoExtension`). Only if no PCF exists, fall back to `\u003CPascal>Extension`. **Either way, surface the chosen value as a confirmation point:** *\"PCF dispatches to `\u003Cname>\u002F\u003Creceiver>`; the manifest will register receiver `\u003Creceiver>` — confirm?\"* (Audit re-checks this — see `\u002Faudit-ppmplugin` `pcf-composite-key-matches-receiver`.)\n\n> **`receivers[]` IS the runtime dispatch contract** ([ppmplugin-format §2](..\u002F..\u002Fshared\u002Fppmplugin-format.md) — *Runtime dispatch contract*). The wrap host routes a call by the composite key `\u003Cname>\u002F\u003Creceivers[].name>` to `NativeModules.\u003CnativeModule>.\u003Cmethod>` — there is no TS `handleMessageAsync` \u002F `sendAsync` layer in a native-only bundle. So `nativeModule` must equal the module's `getName()`, and every entry in `methods` must be a real `@ReactMethod` \u002F `RCT_EXPORT_METHOD` name (the host calls it directly; an absent method = `method '\u003Cm>' not found` on device). Derive `methods` from the module source, never guess.\n- **`entrypoints` — declare ONLY the chosen target(s):**\n  - Android \u002F Both → `entrypoints.android = { dex: \"\u003CPascal>Plugin.dex\", packageClass: \"\u003CFQN from step 4>\" }`\n  - iOS \u002F Both → `entrypoints.ios = { framework: \"\u003CPascal>Plugin\", moduleClass: \"RCT\u003CPascal>Module\" }`\n\n---\n\n## Step 3 — Validate locally (pre-flight gate)\n\nRun **every** rule from [`ppmplugin-format.md §4`](..\u002F..\u002Fshared\u002Fppmplugin-format.md) against the derived manifest. For each, pass or fail with the exact rule:\n\n- `name` matches `^[a-z0-9][a-z0-9-]{0,63}$`\n- each `nativeModule` starts with the **canonical prefix** of `name` — computed precisely as *split `name` on `-`\u002F`_`, PascalCase each segment, join* (ppmplugin-format §3), Ordinal\u002Fcase-sensitive. (Note the `helloworld` → `Helloworld` subtlety; `kebab(className)` must hyphenate at camelCase boundaries so the round-trip holds.)\n- no `nativeModule` starts with a reserved prefix (case-insensitive list in §4)\n- **no `nativeModule` is a known incompatible exact name** — block (Ordinal, case-sensitive) if it matches the locally checked subset in [§4](..\u002F..\u002Fshared\u002Fppmplugin-format.md) (`DeviceInfo`, `AuthenticationHelper`, `NetworkClient`, `DataverseOfflineProvider`, `IntuneMAM`). The subset is **non-exhaustive**: passing it locally does not guarantee the upload service will accept the name. Rename the module's `getName()` (and iOS `+moduleName`) to a non-reserved form — add a `Module` suffix (`DeviceInfo` → `DeviceInfoModule`) or a vendor prefix (`ContosoDeviceInfo`) — and re-derive `nativeModule` to match.\n- **`nativeModule` looks like a generic platform noun → WARNING.** If `nativeModule` matches `^(Device|Network|File|Audio|Camera|Sensor|Location|Storage|Notification|Bluetooth|Wifi|Media|Photo|Contact|Calendar|Battery)`, warn that generic single-noun names are both upload-conflict-prone and collision-prone (`NativeModules` is a shared namespace across every plugin the wrap host loads). Recommend a `Module` suffix or vendor prefix. Surface it; let the user proceed if deliberate.\n- `methods` non-empty; ≤32; each matches `^[a-zA-Z_$][a-zA-Z0-9_$]{0,127}$`\n- each `receivers[].name` matches the same JS-identifier regex\n- **no SDK-era \u002F JS-layer fields** — the manifest carries none of `entrypoints.js`, `entrypoints.ts`, `extension.js`, `extension.hbc`, `extensionClassName`, `jsLayer` (the bundle is native-only; these are leakage the wrap runtime no longer reads — §4). Since this skill authors the manifest it won't emit them, but the re-run mode reads an existing manifest, so assert it.\n\nPrint the result as a visible block:\n\n```\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n manifest.json validation\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n ? ✓ name 'pen-input' — valid kebab identifier\n ? ✓ nativeModule 'PenInputModule' starts with canonical prefix 'PenInput'\n ? ✓ nativeModule 'PenInputModule' — no reserved prefix\n ? ✓ nativeModule 'PenInputModule' — not a known reserved name\n ? ✓ methods ['capturePenInput'] — non-empty, valid identifiers\n ? ✓ receiver name 'PenInputExtension' — valid identifier\n ? ✓ no SDK-era \u002F JS-layer fields (native-only manifest)\n ? 7 checks passed, 0 failed.\n ⓘ Known incompatible-name subset checked locally; the upload service may apply additional checks.\n```\n\nIf any check **fails**, STOP with `BLOCKED: manifest validation — \u003Crule>` and the concrete fix (e.g. \"rename the Kotlin module's `getName()` so it starts with `PenInput`\"). Do NOT write an invalid manifest.\n\n---\n\n## Step 4 — Confirmation gate + write\n\nShow the manifest (the validated source + the target-reconciled `entrypoints`) and wait for confirmation (shared-instructions §7.1). On approval:\n\n1. Ensure `ppmplugin\u002F` is in `.gitignore` (append the line if absent). Do **not** add `manifest.json` (the committed root file stays tracked).\n2. **Write the staged build copy** `ppmplugin\u002Fstaging\u002Fmanifest.json` — the contract with `entrypoints` trimmed to the chosen target(s). This is what `\u002Fassemble-ppmplugin` zips.\n3. **Author-from-source mode only:** also write the committed `.\u002Fmanifest.json` at the repo root (the full contract, all viable platforms) — this is the source of truth the scaffold would normally have produced. In validate-and-stage mode, leave `.\u002Fmanifest.json` untouched (it's already the source) unless the user accepted a drift-update offer in Step 1.5.\n4. Update the `## ppmplugin (third-party controls)` block in `.extension-state.md` (create it if absent — schema in [`repo-layout.md`](..\u002F..\u002Fshared\u002Frepo-layout.md)): set `Target`, `Manifest: staged \u003CISO timestamp>`. This is what the re-run mode (Step 1.5) and the build\u002Fassemble skills read for state.\n\nReturn `DONE` with the manifest path. Then surface next steps via `AskUserQuestion` (shared-instructions §9.1), **offering the build skill(s) for the chosen target(s)**:\n\n- **Run \u002Fbuild-android-binary** — if target is Android or Both (compile the Kotlin module → DEX)\n- **Run \u002Fbuild-ios-binary** — if target is iOS or Both (compile the Obj-C module → framework; Mac-only)\n- **Run \u002Fassemble-ppmplugin** — only if the binary\u002Fbinaries already exist\n- **Stay — I'll review the manifest first**\n\n(For a Both target, list both build skills. Cap at 4 options per `AskUserQuestion`.)\n\n**When the user picks a `Run \u002F…` option, immediately invoke that skill via the `Skill` tool in the same turn** (sub-skill invocation, shared-instructions §8 + §9.1 \"Execute, don't describe\"). Do NOT print a \"run it when ready\" instruction and stop — selecting the option IS the request to run it. Only \"Stay\" ends the run.\n\n---\n\n## Worked example (pen-input)\n\n```json\n{\n  \"name\": \"pen-input\",\n  \"version\": \"0.1.4\",\n  \"abi\": { \"compatibleShells\": \">=1.0.0\", \"builtAgainst\": \"1.0.0\" },\n  \"entrypoints\": {\n    \"android\": { \"dex\": \"PenInputPlugin.dex\", \"packageClass\": \"com.powerapps.peninput.PenInputPackage\" }\n  },\n  \"receivers\": [\n    { \"name\": \"PenInputExtension\", \"nativeModule\": \"PenInputModule\", \"methods\": [\"capturePenInput\"] }\n  ]\n}\n```\n\n(This example targets **Android-only**, so `entrypoints.ios` is omitted. For a **Both** target it would also carry `\"ios\": { \"framework\": \"PenInputPlugin\", \"moduleClass\": \"RCTPenInputModule\" }`. `name` = `pen-input` = kebab of class `PenInput`; `nativeModule` = `PenInputModule` (the `Module` suffix avoids reserved bare names) and starts with canonical prefix `PenInput` ✓; not a reserved prefix ✓.)\n","# \u002Fgenerate-ppmplugin-manifest\n\nStages the `manifest.json` that goes inside a `.ppmplugin` bundle — the small descriptor the wrap runtime and upload service read to identify the plugin and route calls into it. In the normal flow the **committed `.\u002Fmanifest.json` already exists** (authored by [`\u002Fgenerate-native-extension`](..\u002Fgenerate-native-extension\u002FSKILL.md) next to the code it describes); this stage's job is to **validate it, reconcile its `entrypoints` to the shipped target(s), and write the staged build copy** `ppmplugin\u002Fstaging\u002Fmanifest.json`. It is the \"get the strings right\" gate: it runs instantly, needs no build tools, and catches common upload failures before any Android build. If the repo has no `.\u002Fmanifest.json` (a hand-authored native module that skipped the scaffold), this stage **authors it from source** as a fallback — see Step 2.\n\nRead [`shared\u002Fppmplugin-format.md`](..\u002F..\u002Fshared\u002Fppmplugin-format.md) — it is the source of truth for the schema, the derivation table, and the validation rules this skill enforces.\n\n**Two manifests, one contract.** `.\u002Fmanifest.json` (repo root, **committed**) is the source-of-truth contract — it declares *every platform the module supports*. `ppmplugin\u002Fstaging\u002Fmanifest.json` (gitignored) is the **build copy** this stage produces — same contract, but `entrypoints` trimmed to the platform(s) actually being shipped. The build\u002Fassemble skills only ever read the staged copy; the committed root file is what the PCF and humans read.\n\n## What this skill does NOT do\n- Does not compile anything — no Gradle, no DEX. That's [`\u002Fbuild-android-binary`](..\u002Fbuild-android-binary\u002FSKILL.md).\n- Does not zip the bundle — that's [`\u002Fassemble-ppmplugin`](..\u002Fassemble-ppmplugin\u002FSKILL.md).\n- Does not verify a declared platform's binary actually exists — it stages the *intended* `entrypoints`; [`\u002Fassemble-ppmplugin`](..\u002Fassemble-ppmplugin\u002FSKILL.md) reconciles them against the binaries actually staged and gates on any mismatch.\n- Does not rewrite the committed `.\u002Fmanifest.json` on the normal path — it reads it. It only *writes* `.\u002Fmanifest.json` in the fallback case (no committed manifest existed). It never touches `src\u002F`, `ios\u002F`, `android\u002F`, PRD, or `package.json` beyond reading them.\n\n---\n\n## Step 1 — Read shared docs + prereq block\n\n1. Read [`shared\u002Fshared-instructions.md`](..\u002F..\u002Fshared\u002Fshared-instructions.md), [`shared\u002Fnaming-conventions.md`](..\u002F..\u002Fshared\u002Fnaming-conventions.md), and [`shared\u002Fppmplugin-format.md`](..\u002F..\u002Fshared\u002Fppmplugin-format.md).\n2. This skill does no installs \u002F auth \u002F network. Print the zero-prereq block (per shared-instructions §9.2):\n\n```\nPrereq check — \u002Fgenerate-ppmplugin-manifest: skipped (skill does no installs \u002F auth \u002F network — failures surface at validation).\n```\n\n3. Confirm the working directory is a third-party-control repo: a `package.json` (the **dev-only, private, plain `\u003Ckebab>-control`** name — NOT a published `@powerapps\u002Fextension-*` scope; this track ships a binary, not an npm package — see [`repo-layout.md`](..\u002F..\u002Fshared\u002Frepo-layout.md)) **and** an `android\u002F` and\u002For `ios\u002F` native module exist. If not, STOP with `NEEDS_CONTEXT: not a third-party-control repo (no package.json \u002F native module)`.\n4. Read `.extension-state.md` — if it carries a `## ppmplugin (third-party controls)` block, note the last **target** choice and last manifest write (the re-run mode below uses them).\n\n---\n\n## Step 1.5 — Locate the source manifest (which mode are we in?)\n\nThis stage has two modes, decided by whether the committed `.\u002Fmanifest.json` exists:\n\n- **Validate-and-stage mode (the normal flow).** `.\u002Fmanifest.json` exists at the repo root (authored by `\u002Fgenerate-native-extension`). This is the source of truth — **do not re-author it**. Read it, validate it (Step 3), reconcile its `entrypoints` to the chosen target (Step 2 → *Target* only), and write the staged copy (Step 4). Step 2's field-derivation is **skipped** — the contract is already authored; you're verifying and staging it, not regenerating it. (Optionally re-derive `methods` from the Android module source and, if they've drifted from `.\u002Fmanifest.json` — e.g. a `@ReactMethod` was added by hand after scaffold — surface the diff and offer to update the committed file; never silently rewrite it.)\n\n- **Author-from-source mode (fallback).** No `.\u002Fmanifest.json` exists — a hand-authored native module that skipped the scaffold. Derive every field mechanically from source (Step 2 in full), then write **both** the committed `.\u002Fmanifest.json` and the staged copy (Step 4).\n\n**Re-run within a build session.** If the *staged* copy `ppmplugin\u002Fstaging\u002Fmanifest.json` already exists from a prior run, don't blindly overwrite — diff the target\u002Fentrypoints against the committed source and ask via `AskUserQuestion`: **Update** (re-stage from the current `.\u002Fmanifest.json` + target) \u002F **Keep as-is** (report and stop). Default the target to the prior choice in `.extension-state.md`; don't re-ask an answered question.\n\n---\n\n## Step 2 — Determine target(s); derive fields only in author-from-source mode\n\nThe structure preflight + target selection below run in **both** modes (you always need to know which platforms are viable and which to ship). The **field-derivation** sub-section (items 1–6 + the compute block) runs **only in author-from-source mode** (Step 1.5) — in the normal validate-and-stage mode the fields already live in `.\u002Fmanifest.json`; read them from there and skip derivation, keeping only the *target* choice.\n\n**First, run a structure preflight.** The ppmplugin path expects the canonical PAM-extension layout — the shape `\u002Fgenerate-native-extension` produces (see [`shared\u002Frepo-layout.md`](..\u002F..\u002Fshared\u002Frepo-layout.md)). A hand-rolled, foreign, or drifted repo may be missing pieces; flag exactly *what*, here and now, rather than failing later with a cryptic Gradle\u002FXcode error or a silently-wrong manifest. Print a visible ✓\u002F✗ block:\n\n- **Common:** `package.json` (dev-only `\u003Ckebab>-control` name — there is **no** `@powerapps\u002Fextension-*` scope and **no** `src\u002F` TS layer in this track); class name resolvable from the **native module** (`android\u002F...\u002F\u003CPascal>Module.kt` \u002F `ios\u002FRCT\u003CPascal>Module.m`), `.\u002Fmanifest.json`, or `.extension-state.md`.\n- **Android** (if `android\u002F` present): `android\u002Fbuild.gradle`; a Kotlin module class extending `ReactContextBaseJavaModule` with an `override fun getName()`; a `ReactPackage` class; ≥1 `@ReactMethod`.\n- **iOS** (if `ios\u002F` present): `ios\u002FRCT\u003CPascal>Module.h` declaring `\u003CRCTBridgeModule>`; `.m` with `+ (NSString *)moduleName` (**NOT** `RCT_EXPORT_MODULE`) and ≥1 `RCT_EXPORT_METHOD`.\n\nA platform whose structure is **incomplete is not a valid target** — exclude it and say which element is missing. If neither platform is structurally complete, STOP with `NEEDS_CONTEXT: repo doesn't match the expected PAM-extension layout — missing \u003Clist>` pointing at `shared\u002Frepo-layout.md`. (Working on a staged copy protects the *source*; it does NOT make a missing module appear — that's what this preflight is for.)\n\n**Then determine which platform(s) this bundle targets** (only from the structurally-complete ones) — it controls which `entrypoints` get declared:\n- Confirm via `AskUserQuestion`: **Android-only** \u002F **iOS-only** \u002F **Both** — offer only the targets that passed the preflight. Default to what's present, or — on a re-run in **Update** mode — to the prior target recorded in `.extension-state.md`.\n- Note availability: `\u002Fbuild-android-binary` is stable; `\u002Fbuild-ios-binary` is **v0** (Mac-only; **known limitation:** its React-Core weak-link config still needs validation against a live PAM\u002Fwrap shell). If the user targets iOS\u002FBoth, the manifest declares `entrypoints.ios`; `\u002Fassemble-ppmplugin` will still gate if the iOS binary isn't staged at packaging time.\n\n**Then derive fields from the actual files, not from assumptions** *(author-from-source mode only — in validate-and-stage mode skip to Step 3 with the fields read from `.\u002Fmanifest.json`)*:\n\n1. **Class name** `\u003CPascal>` — from the **native module** (the Android `\u003CPascal>Module.kt` filename \u002F its `getName()` = `\"\u003CPascal>Module\"`, or the iOS `RCT\u003CPascal>Module`), or the `.extension-state.md` Identity block. There is **no** `src\u002F\u003CPascal>Extension.ts` in this track. This is the basis for `name`, `nativeModule`, `receivers[].name`.\n2. **`version`** — `package.json` `version`.\n3. **`nativeModule`** — read the Android module's `override fun getName(): String = \"\u003CX>\"`. Use `\u003CX>` verbatim.\n4. **`packageClass`** — read the `ReactPackage` `.kt` file: combine its `package \u003C...>` line with the class name → FQN (e.g. `com.powerapps.peninput.PenInputPackage`).\n5. **`methods`** — scan the Android module (via the Read\u002FGrep tools, not a shell-specific command — this skill is OS-neutral) for `@ReactMethod` and collect each annotated function name. Cross-check the count against the operations in `ARCHITECTURE.md §3` \u002F `PRD §4`; if a documented operation has no matching `@ReactMethod`, surface it as a warning (the manifest reflects what the binary actually exposes, but the mismatch usually means an operation wasn't wired).\n\n6. **iOS entrypoint fields** *(only when targeting iOS \u002F Both)* — read `ios\u002FRCT\u003CPascal>Module.h` for the class name (`@interface RCT\u003CPascal>Module : NSObject \u003CRCTBridgeModule>`) → that is `entrypoints.ios.moduleClass`. Read `ios\u002FRCT\u003CPascal>Module.m` for `+ (NSString *)moduleName { return @\"\u003CX>\"; }` and **assert `\u003CX>` equals the `nativeModule` from step 3** — it's the same bridge symbol on both platforms; a mismatch means the iOS and Android modules disagree. (The `.m` must **not** use `RCT_EXPORT_MODULE` — that macro's `+load` registration is invisible to the framework's `dlopen` flat namespace, so the module never loads on device.)\n\nThen compute, per [`ppmplugin-format.md §3`](..\u002F..\u002Fshared\u002Fppmplugin-format.md):\n\n- `name = kebab(className)` — **from the class name, not the capability\u002Frepo name.** If `kebab(className)` differs from the repo's capability kebab (e.g. repo `powerapps-pdf-control` but class `PdfViewer` → `name: pdf-viewer`), print a one-line note so the user knows the `.ppmplugin` filename won't match the repo name. This is required for the canonical-prefix rule to pass.\n- **`receivers[].name` — take it from the PCF if one exists, do NOT blindly default.** If a sibling PCF is present (`pcf\u002F\u003C…>\u002Findex.ts`), grep it for the dispatch key — `COMPOSITE_KEY = \"\u003Cname>\u002F\u003Creceiver>\"` (or the `ReceiverKey` it binds) — and use **that** `\u003Creceiver>` value, because the PCF already dispatches to it; a manifest that registers a different receiver name will fail on first dispatch (real bug: PCF dispatched to `Snapshot` while the manifest defaulted to `DeviceInfoExtension`). Only if no PCF exists, fall back to `\u003CPascal>Extension`. **Either way, surface the chosen value as a confirmation point:** *\"PCF dispatches to `\u003Cname>\u002F\u003Creceiver>`; the manifest will register receiver `\u003Creceiver>` — confirm?\"* (Audit re-checks this — see `\u002Faudit-ppmplugin` `pcf-composite-key-matches-receiver`.)\n\n> **`receivers[]` IS the runtime dispatch contract** ([ppmplugin-format §2](..\u002F..\u002Fshared\u002Fppmplugin-format.md) — *Runtime dispatch contract*). The wrap host routes a call by the composite key `\u003Cname>\u002F\u003Creceivers[].name>` to `NativeModules.\u003CnativeModule>.\u003Cmethod>` — there is no TS `handleMessageAsync` \u002F `sendAsync` layer in a native-only bundle. So `nativeModule` must equal the module's `getName()`, and every entry in `methods` must be a real `@ReactMethod` \u002F `RCT_EXPORT_METHOD` name (the host calls it directly; an absent method = `method '\u003Cm>' not found` on device). Derive `methods` from the module source, never guess.\n- **`entrypoints` — declare ONLY the chosen target(s):**\n  - Android \u002F Both → `entrypoints.android = { dex: \"\u003CPascal>Plugin.dex\", packageClass: \"\u003CFQN from step 4>\" }`\n  - iOS \u002F Both → `entrypoints.ios = { framework: \"\u003CPascal>Plugin\", moduleClass: \"RCT\u003CPascal>Module\" }`\n\n---\n\n## Step 3 — Validate locally (pre-flight gate)\n\nRun **every** rule from [`ppmplugin-format.md §4`](..\u002F..\u002Fshared\u002Fppmplugin-format.md) against the derived manifest. For each, pass or fail with the exact rule:\n\n- `name` matches `^[a-z0-9][a-z0-9-]{0,63}$`\n- each `nativeModule` starts with the **canonical prefix** of `name` — computed precisely as *split `name` on `-`\u002F`_`, PascalCase each segment, join* (ppmplugin-format §3), Ordinal\u002Fcase-sensitive. (Note the `helloworld` → `Helloworld` subtlety; `kebab(className)` must hyphenate at camelCase boundaries so the round-trip holds.)\n- no `nativeModule` starts with a reserved prefix (case-insensitive list in §4)\n- **no `nativeModule` is a known incompatible exact name** — block (Ordinal, case-sensitive) if it matches the locally checked subset in [§4](..\u002F..\u002Fshared\u002Fppmplugin-format.md) (`DeviceInfo`, `AuthenticationHelper`, `NetworkClient`, `DataverseOfflineProvider`, `IntuneMAM`). The subset is **non-exhaustive**: passing it locally does not guarantee the upload service will accept the name. Rename the module's `getName()` (and iOS `+moduleName`) to a non-reserved form — add a `Module` suffix (`DeviceInfo` → `DeviceInfoModule`) or a vendor prefix (`ContosoDeviceInfo`) — and re-derive `nativeModule` to match.\n- **`nativeModule` looks like a generic platform noun → WARNING.** If `nativeModule` matches `^(Device|Network|File|Audio|Camera|Sensor|Location|Storage|Notification|Bluetooth|Wifi|Media|Photo|Contact|Calendar|Battery)`, warn that generic single-noun names are both upload-conflict-prone and collision-prone (`NativeModules` is a shared namespace across every plugin the wrap host loads). Recommend a `Module` suffix or vendor prefix. Surface it; let the user proceed if deliberate.\n- `methods` non-empty; ≤32; each matches `^[a-zA-Z_$][a-zA-Z0-9_$]{0,127}$`\n- each `receivers[].name` matches the same JS-identifier regex\n- **no SDK-era \u002F JS-layer fields** — the manifest carries none of `entrypoints.js`, `entrypoints.ts`, `extension.js`, `extension.hbc`, `extensionClassName`, `jsLayer` (the bundle is native-only; these are leakage the wrap runtime no longer reads — §4). Since this skill authors the manifest it won't emit them, but the re-run mode reads an existing manifest, so assert it.\n\nPrint the result as a visible block:\n\n```\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n manifest.json validation\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n ? ✓ name 'pen-input' — valid kebab identifier\n ? ✓ nativeModule 'PenInputModule' starts with canonical prefix 'PenInput'\n ? ✓ nativeModule 'PenInputModule' — no reserved prefix\n ? ✓ nativeModule 'PenInputModule' — not a known reserved name\n ? ✓ methods ['capturePenInput'] — non-empty, valid identifiers\n ? ✓ receiver name 'PenInputExtension' — valid identifier\n ? ✓ no SDK-era \u002F JS-layer fields (native-only manifest)\n ? 7 checks passed, 0 failed.\n ⓘ Known incompatible-name subset checked locally; the upload service may apply additional checks.\n```\n\nIf any check **fails**, STOP with `BLOCKED: manifest validation — \u003Crule>` and the concrete fix (e.g. \"rename the Kotlin module's `getName()` so it starts with `PenInput`\"). Do NOT write an invalid manifest.\n\n---\n\n## Step 4 — Confirmation gate + write\n\nShow the manifest (the validated source + the target-reconciled `entrypoints`) and wait for confirmation (shared-instructions §7.1). On approval:\n\n1. Ensure `ppmplugin\u002F` is in `.gitignore` (append the line if absent). Do **not** add `manifest.json` (the committed root file stays tracked).\n2. **Write the staged build copy** `ppmplugin\u002Fstaging\u002Fmanifest.json` — the contract with `entrypoints` trimmed to the chosen target(s). This is what `\u002Fassemble-ppmplugin` zips.\n3. **Author-from-source mode only:** also write the committed `.\u002Fmanifest.json` at the repo root (the full contract, all viable platforms) — this is the source of truth the scaffold would normally have produced. In validate-and-stage mode, leave `.\u002Fmanifest.json` untouched (it's already the source) unless the user accepted a drift-update offer in Step 1.5.\n4. Update the `## ppmplugin (third-party controls)` block in `.extension-state.md` (create it if absent — schema in [`repo-layout.md`](..\u002F..\u002Fshared\u002Frepo-layout.md)): set `Target`, `Manifest: staged \u003CISO timestamp>`. This is what the re-run mode (Step 1.5) and the build\u002Fassemble skills read for state.\n\nReturn `DONE` with the manifest path. Then surface next steps via `AskUserQuestion` (shared-instructions §9.1), **offering the build skill(s) for the chosen target(s)**:\n\n- **Run \u002Fbuild-android-binary** — if target is Android or Both (compile the Kotlin module → DEX)\n- **Run \u002Fbuild-ios-binary** — if target is iOS or Both (compile the Obj-C module → framework; Mac-only)\n- **Run \u002Fassemble-ppmplugin** — only if the binary\u002Fbinaries already exist\n- **Stay — I'll review the manifest first**\n\n(For a Both target, list both build skills. Cap at 4 options per `AskUserQuestion`.)\n\n**When the user picks a `Run \u002F…` option, immediately invoke that skill via the `Skill` tool in the same turn** (sub-skill invocation, shared-instructions §8 + §9.1 \"Execute, don't describe\"). Do NOT print a \"run it when ready\" instruction and stop — selecting the option IS the request to run it. Only \"Stay\" ends the run.\n\n---\n\n## Worked example (pen-input)\n\n```json\n{\n  \"name\": \"pen-input\",\n  \"version\": \"0.1.4\",\n  \"abi\": { \"compatibleShells\": \">=1.0.0\", \"builtAgainst\": \"1.0.0\" },\n  \"entrypoints\": {\n    \"android\": { \"dex\": \"PenInputPlugin.dex\", \"packageClass\": \"com.powerapps.peninput.PenInputPackage\" }\n  },\n  \"receivers\": [\n    { \"name\": \"PenInputExtension\", \"nativeModule\": \"PenInputModule\", \"methods\": [\"capturePenInput\"] }\n  ]\n}\n```\n\n(This example targets **Android-only**, so `entrypoints.ios` is omitted. For a **Both** target it would also carry `\"ios\": { \"framework\": \"PenInputPlugin\", \"moduleClass\": \"RCTPenInputModule\" }`. `name` = `pen-input` = kebab of class `PenInput`; `nativeModule` = `PenInputModule` (the `Module` suffix avoids reserved bare names) and starts with canonical prefix `PenInput` ✓; not a reserved prefix ✓.)\n",{"name":5,"model":46,"description":7,"allowed-tools":47},"sonnet","Read, Write, Edit, Bash, Glob, Grep, AskUserQuestion, Skill","main","0a55a88fa2855404029d1e5450faf92878baf763","plugins\u002Fpower-apps-mobile-extension\u002Fskills\u002Fgenerate-ppmplugin-manifest",228,"https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002Fpower-platform-skills","https:\u002F\u002Fmicrosoft.github.io\u002Fpower-platform-skills\u002F","https:\u002F\u002Fgithub.com\u002Fmicrosoft","https:\u002F\u002Fopensource.microsoft.com","Open source projects and samples from Microsoft","2026-11-12 02:16:25",{"source":8,"skillName":5,"commands":59,"prompt":69,"zipAvailable":70},[60,63,66],{"tool":61,"command":62},"npx","npx skills add microsoft\u002Fpower-platform-skills --skill generate-ppmplugin-manifest",{"tool":64,"command":65},"bunx","bunx skills add microsoft\u002Fpower-platform-skills --skill generate-ppmplugin-manifest",{"tool":67,"command":68},"pnpm","pnpm dlx skills add microsoft\u002Fpower-platform-skills --skill generate-ppmplugin-manifest","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002Fpower-platform-skills\u002Fblob\u002Fmain\u002Fplugins\u002Fpower-apps-mobile-extension\u002Fskills\u002Fgenerate-ppmplugin-manifest\u002FSKILL.md",true,[],[73,93,116,134,148,158],{"skillNo":74,"name":75,"nameZh":6,"description":76,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":77,"categories":78,"keywords":79,"publishedAt":41},"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",[],[75,35,80,81,82,83,84,85,86,87,88,89,90,91,92,36],"when","connection","error","sign","intelligence","configuration","use","process","diagnostics","environment","authentication","cloud","recovery",{"skillNo":94,"name":95,"nameZh":6,"description":96,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":97,"categories":98,"keywords":104,"publishedAt":41},"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",[99],{"categoryNo":100,"categoryName":101,"taxonomyType":102,"taxonomyTypeDesc":103},"01JBSEEDCAT000000000000034","行政与办公人员","PROFESSION","适用职业",[105,106,107,108,109,87,86,110,111,84,112,113,114,80,115],"report issue","report","issue","support","user","bug","summary","draft","operation","for","bug-report",{"skillNo":117,"name":118,"nameZh":6,"description":119,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":120,"categories":121,"keywords":122,"publishedAt":41},"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",[],[87,123,124,125,80,126,127,128,129,35,130,86,131,132,133],"investigate","investigate process","mining","process mining","investigation","perspectives","question","business","pattern","across","analytical",{"skillNo":135,"name":136,"nameZh":6,"description":137,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":138,"categories":139,"keywords":140,"publishedAt":41},"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",[],[141,142,35,86,143,144,145,146,147,80],"metric","derive metric","formula","missing","reuse","saved","custom",{"skillNo":149,"name":150,"nameZh":6,"description":151,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":152,"categories":153,"keywords":154,"publishedAt":41},"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",[],[155,156,80,86,87,157,35],"compare","compare cohorts","results",{"skillNo":159,"name":160,"nameZh":6,"description":161,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":162,"categories":163,"keywords":164,"publishedAt":41},"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",[],[165,166,167,126,80,125,168,169,86,87,35,170,171],"analyze","variants","analyze variants","case","paths","work","common"]