mcpField
P
plan-alm

plan-alm

github.com/microsoftクラウド・DevOps1.1k159 回インストール今日更新
リポジトリ

plan-alm について

plan-almは、Power Pagesチームがソリューションの昇格方法を決めるためのALM計画スキルです。プロジェクト状態を検出し、方針を確認して承認用のHTML計画を作成します。デプロイは実行しません。

主な機能

  1. 01サイト、ソリューション、パイプライン状態の検出
  2. 02昇格方針の確認
  3. 03HTMLデプロイ計画の生成
  4. 04承認要求と計画状態の保存
  5. 05計画と実行の分離

活用シーン

  1. 01Power Platform Pipelinesと手動移行の選択
  2. 02デプロイ前に推奨ALM手順を確認
  3. 03再計画せず保存済みドラフトを承認

インストール

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

$npx skills add https://github.com/microsoft/power-platform-skills --skill plan-alm

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

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

assets/
alm-plan-template.html
scripts/
render-alm-plan.js
validate-plan-alm.js

詳細

プラグインチェック:node "${PLUGIN_ROOT}/scripts/check-version.js" を実行します。メッセージが出力された場合は、続行する前にユーザーに表示してください。

plan-alm

ユーザーから ALM 戦略を聞き取り、HTML のデプロイ計画を生成して承認を得る、4 フェーズのプランナーです。何も実行しません。実行は個々の ALM スキルに委ねられ、ユーザーがその後に実行します。

概要

このスキルは現在のプロジェクトの状態(既存のソリューション、パイプライン)を検出し、希望する昇格戦略(Power Platform Pipelines または手動のエクスポート/インポート)について的を絞った質問を行い、視覚的な docs/alm-plan.html を生成してユーザーの承認を得ます。4 つのフェーズは、Phase 1 — 検出、Phase 2 — 戦略の聞き取り、Phase 3 — 計画の生成、Phase 4 — 承認と保存です。

plan-alm はデプロイを行いません。 計画の steps[] 配列には、推奨される実行順序が記録されます。承認後、ユーザーは個々のスキルである setup-solution、setup-pipeline(または export-solution)、deploy-pipeline(または import-solution)をこの順序で実行します。それぞれのスキルは Phase 0 のゲートで承認済みの計画を検出し、再度催促することなく処理を進め、完了時に計画を更新します。この分離は意図的なものです。単一の回答で取り消せないデプロイが実行されることがないため、plan-alm を(たとえば自動操縦下で)無人実行しても安全です。

最初にタスクを作成しないでください。戦略が確定するのは Phase 2 の完了後です。戦略が決まったら、Phase 3 で両方のタスクを作成してください。


Phase 1 — プロジェクト状態の検出

まだタスクを作成しないでください。 このフェーズでは、自然言語による進捗報告のみを使用してください。

手順:

  1. このプロジェクトで以前に ALM が延期されたか検出します。 検出作業を始める前に、プロジェクトルートに .alm-deferred マーカーファイルがあるか確認してください。このマーカーは、「このサイトは別途管理されています」や「ni-dev — ALM なし」など、「成果物がありません」という警告について ALM スキルのバリデーターを明示的に無効にすることを選んだユーザーが書き込むものです。ユーザーが今 plan-alm を実行している場合は、黙って処理を進める(ユーザーが以前に維持しないと決めた計画を作成することになる)ことも、黙ってマーカーを削除する(他の ALM スキルで毎回警告が再び表示されるようになる)こともせず、マーカーが存在することを伝えて、どうするかを尋ねます。

    node "${PLUGIN_ROOT}/scripts/lib/check-alm-plan.js" --projectRoot "." --no-heartbeat
    

    ここでは --no-heartbeat を使用してください。これはプランナーによる読み取り専用の延期チェックであり、実行スキルの Phase 0 ゲートではありません。これを指定しないと、check-alm-plan.js は plan-alm を再度開いただけで、すでに Approved の計画を In Execution に進め(ハートビートも更新し)てしまいます。しかし、計画の再作成は実行ではありません。実行スキルは --no-heartbeat を付けずに呼び出すため、最初に実行されるスキルが Approved → In Execution の移行を行います。

    🚦 ゲート(進捗 · plan-alm:1.deferral): .alm-deferred マーカーがあります。続行して削除する、マーカーを維持して続行する、またはキャンセルするかを選択してください。これにより、後続の ALM スキルでゲートの適用を再開するかどうかが決まります。

    ヘルパーは { deferred, deferral, ... } を返します。deferred === true の場合は、延期理由(deferral.reason またはマーカーの生テキスト)を読み取り、AskUserQuestion を使って次のように尋ねます。

    「このプロジェクトには .alm-deferred マーカーがあります — {reason}。ここでは以前 ALM が延期されていたため、他の ALM スキル(setup-solution、setup-pipeline、deploy-pipeline、…)は、このプロジェクトの計画の完全性チェックをスキップします。どのように進めますか?」

    質問ヘッダー選択肢
    どのように進めますか?ALM 延期マーカー計画を続行してマーカーを削除する(推奨)、計画を続行するがマーカーは維持する(延期のコンテキストを計画に記録する)、キャンセル
    • 続行してマーカーを削除(推奨) → .alm-deferred を削除します(ユーザーが ALM を再開します)。DEFERRAL_CLEARED = true に設定し、ステップ 1 に進みます。
    • 続行してマーカーを保持 → DEFERRAL_PRESERVED = true および DEFERRAL_REASON = {reason} に設定します。ステップ 1 に進みます。フェーズ 1 のステップ 9 のユーザーレポートに、1 行の注記を表示します(例:「注:.alm-deferred は保持されます。このプロジェクトでは、他の ALM スキルは引き続き計画の完全性チェックをスキップします。」)。これにより、計画後もマーカーが有効なままであることをユーザーが覚えておけます。
    • キャンセル → 正常に終了します(マーカーには触れません)。

    deferred === false の場合は、このステップを何も表示せずにスキップし、ステップ 0b に進みます。

0b. 既存のドラフトをその場で承認するか確認します(再計画をスキップ)。 ステップ 0 の同じ check-alm-plan.js の出力には、exists と planStatus も含まれています。exists === true かつ planStatus === "Draft" の場合、ユーザーはすでに保存済みのドラフト計画を持っています。計画全体を再生成する代わりに、直接承認する方法を提示します。(これはドラフトから承認済みにできる唯一の経路です。この方法を使わずにドラフトを承認するには、計画を最初からやり直す必要があります。)

🚦 ゲート(plan · plan-alm:1.approve-draft): 既存のドラフト計画が見つかりました。そのまま承認する(再計画なし)、最初から再計画する、またはキャンセルできます。ここで承認すると、set-plan-status.js を使ってステータスが書き込まれ、ディスカバリーを再実行せずに終了します。デプロイは開始されません。

AskUserQuestion で質問します:

"このサイトには、ALM 計画が Draft として保存されています(docs/alm-plan.html)。どのようにしますか?"

質問ヘッダー選択肢
どのようにしますか?既存のドラフト計画このドラフトを今すぐ承認する — 再計画なし(推奨)、最初から再計画する、キャンセル
  • このドラフトを今すぐ承認する(推奨) → フェーズ 4 の承認者取得手順(Git/OS 名を事前入力した、常に対話形式のプロンプト)を使って承認者を取得し、ヘルパーを使ってステータスをアトミックに書き込みます。

    node "${PLUGIN_ROOT}/scripts/lib/set-plan-status.js" --projectRoot "." --status Approved --approver "{APPROVER}" --render
    

    コミットし(git add docs/alm-plan.html docs/.alm-plan-data.json && git commit -m "Approve ALM plan for {siteName}")、スキルトラッキングを実行して(フェーズ 4 の最終処理)、フェーズ 4 の次のステップの案内を出力し、終了します。再計画するものはないため、ステップ 1 には進みません。

  • 最初から再計画する → ステップ 1 に進みます(フェーズ 1 の残りの手順で計画を再生成し、フェーズ 4 で新しいバージョンを保存します)。

  • キャンセル → 正常に終了します(ドラフトはそのまま残します)。

exists === false の場合、または planStatus が "Draft" 以外(Approved/In Execution/Completed/null)の場合は、このステップを何も表示せずにスキップし、ステップ 1 に進みます。

  1. ローカルプロジェクトからサイトの識別情報を解決します。 .powerpages-site/website.yml は websiteRecordId と siteName の信頼できる情報源であり、Power Pages サイトの両方の種類で使用できます。

    • コード/SPA サイト — /power-pages:create-site によってスキャフォールディングされ、pac pages download-code-site でダウンロードされます。これらのサイトには powerpages.config.json と SPA ソース(src/、dist//build/ 内のビルド出力)もあります。
    • データモデルサイト(標準および拡張データモデル/「EDM」) — pac pages download --modelVersion 1|2 でダウンロードされます。これらのサイトには powerpages.config.json はなく、その代わりに .powerpages-site/ に設定ツリー(web-pages/、web-templates/、content-snippets/、…)と .powerpages-site/.portalconfig/ のマニフェストペアが格納されます。ローカルのビルド出力はありません。 解決の優先順位(最初に一致したものを使用):
    1. .powerpages-site/website.yml(優先。ダウンロードまたはデプロイされたすべてのサイトに存在)— Read ツールで読み取り、次の値を抽出します。
      • id フィールド → websiteRecordId
      • name フィールド → siteName(このファイルでは短いキーを使用しているため、adx_name: ではなく name: です)
    2. powerpages.config.json(フォールバック。SPA サイトのみ。リポジトリのルートからのプラグイン開発時、またはスキャフォールディング済みでまだデプロイされていないサイトで使用)— siteName と websiteRecordId を読み取ります。

    SITE_TYPE を判定します(planData に siteType として記録され、以下の SPA のみを対象とした前提をスキップするために使用されます。これは docs/.alm-plan-data.json 内のデータフィールドであり、HTML にはレンダリングされません)。

    • .powerpages-site/.portalconfig/ が存在する場合、または powerpages.config.json が存在しない状態で .powerpages-site/website.yml が解決された場合は、non-spa とします。(名前変更前に作成されたプランでは、同等の従来値である declarative または data-model が設定されている場合があります。)
    • powerpages.config.json が存在する場合は、spa とします。(名前変更前に作成されたプランでは、同等の従来値である code が設定されている場合があります。)

    どちらのマーカーも見つからない場合は、次のメッセージを表示して停止します。

    "現在のディレクトリに Power Pages サイトが見つかりません。サイトプロジェクトのルートからこのスキルを実行してください。pac pages download-code-site(SPA サイト)または pac pages download --modelVersion 2(拡張データモデルのサイト)の実行後、.powerpages-site/ が配置される場所です。まだサイトを作成していない場合は、まず /power-pages:create-site を実行してください。"

    environmentUrl はステップ 4 で pac env who から必ず再確認されるため、どちらのソースから取得する必要もありません。

  2. プロジェクトのルートに .solution-manifest.json があるか確認します。

    • 見つかった場合は SOLUTION_DONE = true、それ以外は false を設定します。
    • 見つかった場合は solution.uniqueName を読み取り、SOLUTION_UNIQUE_NAME として保存します。
  3. プロジェクトのルートに docs/alm/last-pipeline.json があるか確認します。

    • 見つかった場合は PIPELINE_DONE = true、それ以外は false を設定します。
    • 見つかった場合は、後で使用するために pipelineName と stages[] を読み取ります。
  4. サイレントに実行します。

    pac env who
    

    環境 URL と表示名を取得します。DEV_ENV_URL と DEV_ENV_NAME として保存します。URL ラベルは PAC のバージョンによって異なります。現在の PAC(2.8.x)では Org URL: の下に表示されます。以前のビルドでは Environment URL: が使われていました。どちらのラベルが存在するかを確認してください(2.8.x には Environment URL: の行がないため、そのラベルだけを探さないでください)。表示名は Friendly Name: または Connected to... の値です。確実に解析できない場合は DEV_ENV_URL を空のままにします。ステップ 6 の verify-alm-prerequisites.js が、共有の getEnvironmentUrl() ヘルパーを介して pac env who から信頼できる URL を解決し、.envUrl として返すためです(このヘルパーは両方のラベルに対応しています)。

  5. サイレントに実行します。

    node "${PLUGIN_ROOT}/scripts/lib/list-environments.js"
    

    JSON 配列を ENV_LIST として保存し、フェーズ 2 で環境 URL を事前入力します。(このヘルパーは pac env list を解析します。以前の pac env list --output json は現在の PAC CLI では無効です。pac env list が受け付けるのは --filter のみであるため、テーブル形式では生成できない JSON を出力するヘルパーが用意されています。PAC が未認証の場合は [] を出力して終了コード 0 で終了するため、事前入力は行われず、手動入力に切り替わります。)各エントリは { displayName, environmentId, environmentUrl, uniqueName, active } です。

  6. 開発環境トークンを取得します(サイレントに実行)。

    node "${PLUGIN_ROOT}/scripts/lib/verify-alm-prerequisites.js" --envUrl "{DEV_ENV_URL}"
    

    .token を DEV_TOKEN として、.userId を userId として保存します。

    プランの品質を追跡します。 フェーズ 1 の開始時に、PLAN_QUALITY の初期値を "complete" に設定します。このトークン取得に失敗した場合(認証エラー)、DEV_TOKEN = null、PLAN_QUALITY = "degraded" に設定し、原因(例:"開発環境の認証に失敗 — コンテンツ/サイズ/ホストの検出をスキップ")を記録してから、処理を続行します。コンテンツの検出は問題なくスキップされますが、結果のプランは一部の入力に基づいて作成されます。フェーズ 3 では、ユーザーが実行前に確認できるよう、これを目立つリスクとして提示します。(ここでブロックすべき実行パスはありません。plan-alm はプランの作成のみを行いますが、品質が低下したプランにはその旨を明確に示す必要があります。) 6b. 環境一致性ガード — 検出を実行する前に、pac env who がプロジェクトの環境を指していることを確認します。DEV_ENV_URL は PAC が接続している環境から取得されますが、それがプロジェクトの環境であるとは限りません。異なる場合、Step 7~12 のすべてのクエリが誤った環境に対して実行され、劣化したプラン(サイト設定がゼロまたは誤っている、サイズが誤っている、ホストが誤っている)が気付かれないまま生成されますが、見た目には有効に見えます。利用可能な両方のシグナルを照合します。

    1. 記録済み URL の比較(トークン不要):プロジェクトにすでに記録されている環境 URL をすべて収集します。powerpages.config.json → トップレベルの environmentUrl(SPA サイト。非 SPA/EDM サイトでは存在しません)と、.solution-manifest.json → 存在する場合はトップレベルの environmentUrl です。オリジンで正規化します(ホストを小文字にし、末尾のスラッシュとパス/クエリを削除)。記録済み URL があり、そのオリジンが DEV_ENV_URL のオリジンと異なる場合は、不一致です。
    2. サイト存在確認プローブ(URL を記録しない非 SPA/EDM サイトを対象。DEV_TOKEN が利用可能な場合のみ):サイトの websiteRecordId が接続先環境に実際に存在することを確認します。
      GET {DEV_ENV_URL}/api/data/v9.2/powerpagesites({websiteRecordId})?$select=powerpagesiteid
      Authorization: Bearer {DEV_TOKEN}
      
      404(または空の結果)は、接続先環境にこのサイトが含まれていないことを意味し、不一致です。(DEV_TOKEN = null の場合はこのプローブをスキップします。Step 6 ですでにプランは劣化しているため、二重に確認を求めないでください。)

    どちらのシグナルでも不一致が示されない場合は、何も表示せず Step 7 に進みます。プロンプトは表示しません。不一致が検出された場合のみプロンプトを表示します。

    🚦 ゲート(進行状況 · plan-alm:1.env-match): PAC CLI は、プロジェクトの環境と一致しない環境に接続されています。環境を切り替えて再実行するか、接続先環境に対して続行してください(劣化したプランになります)。

    AskUserQuestion で質問します。

    質問ヘッダー選択肢
    PAC CLI は {DEV_ENV_NAME}({DEV_ENV_URL})に接続されていますが、この環境はプロジェクトに設定された環境({recorded URL, or "this site was not found there"})と一致しません。検出は接続先環境に対して実行されます。どのように進めますか?環境の不一致PAC 環境を切り替えて再実行(推奨)、{DEV_ENV_NAME} に対して続行

    結果は厳密に 2 つです(どちらも停止または続行であり、別個の「キャンセル」はありません。「切り替えて再実行」を選ぶと、スキルは停止します)。

    • PAC 環境を切り替えて再実行(推奨):スキルを停止します。PAC を正しい環境に向けて(pac auth select --name <profile> または pac org select --environment <url>)、/power-pages:plan-alm を再実行するようユーザーに伝えます。何も書き込まれていません。
    • {DEV_ENV_NAME} に対して続行:DEV_ENV_URL に対して Step 7 に進みます。ただし、PLAN_QUALITY = "degraded" を設定し、原因(「検出は {DEV_ENV_NAME} に対して実行されました。この環境はプロジェクトの環境ではない可能性があります。実行前にプランのサイト設定/サイズ/ホストを確認してください」)を記録し、Phase 3 で目立つリスクとして提示します。

    このガードがある理由:実際の EDM サイトの実行で、PAC がプロジェクトの対象とは異なる環境に接続したままになっていることに気付かず、有効に見えるプランが生成されました。サイト存在確認プローブと記録済み URL の比較により、検出が始まる前の最初のゲートでこれを検出できます。

  7. サイト設定を検出して分類します(DEV_TOKEN が利用可能で、websiteRecordId が判明している場合)。

    Node.js の https モジュールを使用してクエリを実行します。@odata.nextLink を使用してページネーションします。500 件を超える設定があるサイトでは、ページネーションしないと設定が気付かれないまま切り捨てられ、ティア分類が失われ、plannedEnvVarCount が過少報告されます。Dataverse が継続リンクを返すように Prefer: odata.maxpagesize=5000 を送信し、すべてのページを取得するまでループします。

    GET {DEV_ENV_URL}/api/data/v9.2/mspp_sitesettings?$filter=_mspp_websiteid_value eq '{websiteRecordId}'&$select=mspp_name,mspp_value&$top=5000
    Authorization: Bearer {DEV_TOKEN}
    Prefer: odata.maxpagesize=5000
    OData-MaxVersion: 4.0
    OData-Version: 4.0
    Accept: application/json
    

    各レスポンスで value[] を累積配列に追加します。@odata.nextLink がある場合は、同じヘッダーでその URL に GET リクエストを送信します(nextLink にクエリがすでに含まれているため、フィルターを再度追加する必要はありません)。レスポンスに @odata.nextLink がなくなったら停止します。安全のため、最大 100 回の反復に制限します。 返された設定を ${PLUGIN_ROOT}/scripts/lib/classify-site-settings.js を使って分類します。これは、setup-solution のフェーズ 5 と共有される、資格情報の正規表現とティアマッピングの単一の信頼できる情報源です。{name, value} 行の JSON 配列をスクリプトの標準入力にパイプする(CLI モード)か、インラインで require() するかのいずれかです。

    echo '<JSON array of {name,value}>' \
      | node "${PLUGIN_ROOT}/scripts/lib/classify-site-settings.js"
    

    出力(下流のフェーズと setup-solution が直接消費する 4 つのバケットの形状):

    SITE_SETTINGS_DATA = {
      keepAsIs: [{name}],                          // regular settings (Tier 3 — Search/Bootstrap/WebApi/feature flags)
      authNoValue: [{name}],                       // Authentication/* or AzureAD/* with empty value (Tier 2b — added as-is, set in target env)
      promoteToEnvVar: [{name, value}],            // Authentication/* or AzureAD/* with value (Tier 2a — setup-solution offers env-var promotion)
      credentialNeedsDecision: [{name, value}]     // ConsumerKey/ConsumerSecret/ClientId/ClientSecret/AppSecret/AppKey/ApiKey/Password (Tier 1 — bulk-with-override prompt in setup-solution Phase 5.4.C)
    }
    

    ティアの意味を平易な言葉で説明します(計画を読むレビュアーが各バケットの意味を理解できるようにするため):

    • ティア 1(credentialNeedsDecision) — 資格情報形式の名前です。setup-solution のフェーズ 5.4.C は 1 回の一括プロンプトを実行します。名前による自動分類(*Secret/*Password/*ApiKey/*AppKey は Secret 型の環境変数、*Id/*ConsumerKey は String 型)、すべて Secret、すべて String、すべてスキップ、または資格情報ごとに選択、のいずれかです。
    • ティア 2a(promoteToEnvVar) — 開発用の値を持つ認証設定です。setup-solution のフェーズ 5.4.A は、各ステージで異なる値を使用できるように、どれを環境変数で支えるかを尋ねます。
    • ティア 2b(authNoValue) — まだ開発用の値がない認証設定です。そのままソリューションに追加されます。ユーザーはデプロイ後に各ターゲット環境で値を設定します。
    • ティア 3(keepAsIs) — それ以外のすべてです。変更なしで追加されます。

    OData クエリが失敗するか、ヘルパーがエラーになった場合は、SITE_SETTINGS_DATA = null を設定して続行します。計画は引き続きレンダリングされますが、サイト設定をティア別に分類できなくなるだけです。

  8. SOLUTION_CONTENTS_DATA を構築します:

    {
      tables: solutionManifest?.components?.tables || [],     // from .solution-manifest.json if SOLUTION_DONE
      botComponents: solutionManifest?.botComponents || [],   // from manifest if available
      siteSettings: SITE_SETTINGS_DATA                        // from step 7, or null
    }
    

    SOLUTION_DONE = false でマニフェストが存在しない場合、tables と botComponents は空の配列になります。計画には、それらが setup-solution 中に検出されるという注記が表示されます。

  9. ユーザーに報告します:

    Found: **{siteName}** on `{devEnvUrl}`.
    Solution: {✓ already set up ({solutionUniqueName}) / ✗ not yet}.
    Pipeline: {✓ already set up ({pipelineName}) / ✗ not yet}.
    Site settings: {N total — K regular (keep as-is), P auth settings to review for env var, A auth settings (no dev value), C credential-style settings (setup-solution will prompt per credential) / unable to query}.
    
  10. ソリューションのサイズを見積もり、分割の決定木を評価します。 まず ALM アーティファクトディレクトリが存在することを確認します(すべての .alm-* および last-* アーティファクトは、プロジェクトルートを散らかさないように docs/alm/ の下に置かれます):

    node -e "require('fs').mkdirSync('docs/alm',{recursive:true})"
    

    推定ヘルパーを実行して、サイトをサイズ、コンポーネント数、スキーマの重さ、Web ファイルの集計、環境変数の数にわたって分類します。tmp ファイル書き込みパターンを使用します。推定器が失敗した場合、以前の正常な docs/alm/alm-size-estimate.json が、空または部分的なファイルで上書きされることなく保持されます。SOLUTION_DONE = true の場合(.solution-manifest.json が存在する場合)、--solutionId {solutionId} を渡して、環境変数の数がターゲットソリューションにスコープされるようにします。これがないと、推定器はパブリッシャープレフィックスのテナント全体クエリにフォールバックし、プレフィックスがプロジェクト間で共有されている場合(よくある new_ / cr5fe_ のリグレッション)に過大にカウントします:

    node "${PLUGIN_ROOT}/scripts/lib/estimate-solution-size.js" \
      --envUrl "{DEV_ENV_URL}" --websiteRecordId "{websiteRecordId}" \
      --publisherPrefix "{publisherPrefix}" --siteName "{siteName}" \
      {if SOLUTION_DONE: --solutionId "{solutionManifest.solution.solutionId}"} \
      --projectRoot "." --siteType "{SITE_TYPE}" \
      --datamodelManifest "./.datamodel-manifest.json" > ./docs/alm/alm-size-estimate.json.tmp \
      && mv ./docs/alm/alm-size-estimate.json.tmp ./docs/alm/alm-size-estimate.json
    

    SOLUTION_DONE = false の場合、--solutionId を省略します。推定器の出力には、より広いスコープを示す envVarCountScope: "publisher-prefix" が含まれ、レンダラーはこの注意事項を Env Variables タブに表示するので、レビュアーはその数値が特定のソリューションではなくテナントビューを反映していることを把握できます。--projectRoot "." はディスクのクロスチェックを有効にします。推定器はローカルのビルド出力(dist/、public-output/、build/、.output/)を走査し、webFilesDiskMeasuredMB を表示します。その数値が Dataverse で測定された webFilesAggregateMB よりもはるかに大きい場合、推定器は警告とともに truncationSuspected: true に切り替えます。$select=content でバイト数が返されないファイル型の列が通常の原因であり、計画はディスクの数値を信頼すべきです。

SITE_TYPE = "non-spa"(EDM/標準データモデル)のサイトにはビルド出力がありません。そのため、ディスクとの照合では dist//build/ ディレクトリが見つからず、webFilesDiskMeasuredMB は null のままになります。これは想定どおりであり、問題ではありません。非 SPA サイトの Web ファイルは .powerpages-site/web-files/ 内のレコードとして保存され、Dataverse クエリで測定されるため、サイズの推定は引き続き有効です。照合対象となる SPA バンドルがディスク上にないだけです。これらのサイトでは何も行われませんが、--projectRoot "." は指定してください。 続いて、決定木を実行します(同じ一時ファイルのパターンを使用します)。 bash node "${PLUGIN_ROOT}/scripts/lib/compute-split-plan.js" \ --estimate ./docs/alm/alm-size-estimate.json \ --projectRoot "." \ --siteName "{siteName}" \ --publisherPrefix "{publisherPrefix}" > ./docs/alm/alm-split-plan.json.tmp \ && mv ./docs/alm/alm-split-plan.json.tmp ./docs/alm/alm-split-plan.json いずれかのコマンドがゼロ以外の終了コードを返した場合は、処理を中止し、stderr メッセージをユーザーに報告してください。有効な分割計画がない状態で、フェーズ 2 の Q1b に進まないでください。 出力を SPLIT_PLAN として保存します。読み取るフィールド:splitStrategy、proposedSolutions[]、appliedStrategies[]、assetAdvisory、sizeAnalysis、recommendations[]。

`SPLIT_PLAN.proposedSolutions.length > 1` の場合は、`RECOMMEND_SPLIT = true` に設定します。それ以外の場合は `false` に設定します。

次の内容をユーザーに報告します。
```
推定サイズ:{totalSizeMB} MB — コンポーネント数:{count} — ティア:{overall tier}。
テーブル数:{tableCount} — サイトのテーブル権限を対象にしています({tableCountScope})。
決定木の結果:{splitStrategy} → 推奨ソリューション数:{N}。
アセットに関する注意:Azure Blob への外部化が必要なファイル:{K} 件。
```

> **テーブル数はパブリッシャーのプレフィックスではなく、サイトが参照するテーブルに基づきます。** 推定ツールは、サイトが実際に参照するテーブル(テーブル権限とデータモデルのマニフェスト)を対象にカスタムテーブルを集計するため、共有または既定のパブリッシャー(`new_`)によって数が水増しされることはありません。`tableCountScope` は対象範囲を示します。`site-referenced`(テーブル権限)、`manifest-only`、または `unavailable`(ローカルの `.powerpages-site/` の情報がない場合。テーブル数は 0 で、環境全体のデータは決して取得しません)。`unavailable` の場合は、テーブルに基づく分割の判断材料をスキップしたことを明記してください。推定コマンドではすでに `--projectRoot "."` が渡されており、ローカルのテーブル権限が提供されます。

10b. 環境変数の定義を列挙する(サイズ推定ツールでは件数のみが得られ、変数ごとのメタデータは得られないため、DEV_TOKEN が利用可能な場合は常に実行します)。

レンダラーの「Env Variables」タブでは、定義ごとにスキーマ名、種類、既定値、バインド先のサイト設定が必要です。この手順を実行しない場合、タブには件数の概要メモしか表示されませんが、サイズのシグナルと警告の引用には数値が表示されます。この3つの表示は完全には一致しません。このクエリを実行すると行単位のデータが取得され、テーブルが正しく表示されます。

返される `envVars[]` を対象のソリューションに絞り込むには、`SOLUTION_DONE = true` の場合に `--solutionId` を渡してください。ヘルパーは対象範囲にかかわらず、正しくページネーションを行います(Prefer: odata.maxpagesize + @odata.nextLink)。違いは、返される環境変数だけです。

```bash
node "${PLUGIN_ROOT}/scripts/lib/discover-env-var-definitions.js" \
  --envUrl "{DEV_ENV_URL}" --token "{DEV_TOKEN}" \
  --publisherPrefix "{publisherPrefix}" \
  --websiteRecordId "{websiteRecordId}" \
  {if SOLUTION_DONE: --solutionId "{solutionManifest.solution.solutionId}"} > ./docs/alm/alm-env-vars.json.tmp \
  && mv ./docs/alm/alm-env-vars.json.tmp ./docs/alm/alm-env-vars.json
```

JSON を読み取ります:`{ envVars: [{ schemaName, type, defaultValue, siteSetting }], count, scope }`。`scope` フィールドは、`--solutionId` が渡され、ソリューションに環境変数の定義があった場合は `'solution'`、それ以外の場合は `'publisher-prefix'`、ヘルパーが早期終了した場合(プレフィックスがない、または認証が失効している場合)は `'none'` です。`envVars` は `ENV_VARS_DETAILS` として、`scope` は `ENV_VARS_SCOPE` として保存し、フェーズ 3 で `planData.envVars` を構築する際に使用します(そのまま渡してください)。
パブリッシャープレフィックスが不明な場合、トークンの有効期限が切れている場合、またはクエリでエラーが発生した場合、ヘルパーは適切にフォールバックし(`{ envVars: [], count: 0, scope: 'none' }` を返し)ます。この場合、レンダラーはサイズ推定ツールのカウント `sizeAnalysis.envVarCount.value` にフォールバックします(コミット `8cbc39a`)。`ENV_VARS_DETAILS = []` で問題ありません。

> **ここでスコープ指定が重要な理由**:`--solutionId` を指定しない場合、ヘルパーはテナント全体を対象に、パブリッシャープレフィックスで環境変数定義を絞り込みます。汎用プレフィックス(`new_`、`cr5fe_`)を使用しているテナントでは、無関係なプロジェクトの環境変数も返されるため、カウントとレンダラーが描画する `envVars[]` テーブルの行数が水増しされます。プランの内容は正しく見えます(「環境変数を12個検出しました」)が、実際には別のプロジェクトの行が表示されています。`--solutionId` を指定すると、ヘルパーは対象ソリューションの `solutioncomponents.componenttype=380` と照合するため、そのソリューションが実際に所有する環境変数だけが対象になります。

**スキップルール**:`DEV_TOKEN` が null の場合(ステップ6で認証を利用できなかった場合)、このステップをスキップし、`ENV_VARS_DETAILS = []` を設定します。この場合は、レンダラーのカウント概要のフォールバックで補います。

11. プラン作成前の完全性チェック(SOLUTION_DONE = true の場合のみ実行)。

ユーザーがプランを承認する前に、既存のソリューションがライブサイトのすべての内容を網羅していることを確認します。前回の `/power-pages:setup-solution` 実行後に作成されたコンポーネント(`add-server-logic` によるサーバーロジック、`add-cloud-flow` によるフロー、`configure-env-variables` または `setup-auth` による環境変数)は、古いソリューションを基に作成されるプランから、通知なく除外されます。

ソース環境に対して共通の検出ヘルパーを実行します。

```bash
node "${PLUGIN_ROOT}/scripts/lib/discover-site-components.js" \
  --envUrl "{envUrl}" --token "{token}" \
  --siteId "{powerpages.config.json の websiteRecordId}" \
  --publisherPrefix "{solutionManifest.publisher.prefix}" \
  --solutionId "{solutionManifest.solution.solutionId}" \
  --projectRoot "."
```

標準出力を解析し、`missing.*` を評価します。

- **すべての `missing.*` 配列が空** → 「ソリューションの内容はサイトと一致しています。最新のプラン入力を使って続行します。」と報告します。フェーズ2に進みます。
- **空でない `missing.*` 配列がある** → 次のように簡潔な概要を報告します。
  > 「サイトに存在する**コンポーネントが{N}個、ソリューションにありません**。
  >
  > - サイトコンポーネントが **{X}** 個(例:{最初の3件の名前})
  > - サイト言語が **{L}** 個(powerpagesitelanguage — 必須です。これがないと、認証後に対象サイトのレンダリングが通知なく失敗します)
  > - クラウドフローが **{Y}** 個
  > - 環境変数定義が **{Z}** 個
  > - カスタムテーブルが **{W}** 個
  >
  > 今プランを作成すると、これらのコンポーネントは無視されます。どのように進めますか?」

  他のカテゴリが0件でも、`missing.siteLanguages.length > 0` の場合は、必ず**サイト言語**の行を表示します。discover-site-components が `powerpagesitelanguages` の列挙を開始するまでは、この不足が通知なく失敗する原因として繰り返し発生していました。3エンティティモデルについては `references/solution-api-patterns.md` を参照してください。

  
  > 🚦 **ゲート(進行 · plan-alm:1.completeness):** 完全性チェックで、ライブサイトとの不一致が見つかりました。先に同期するか、不足を記録してプランを作成するか、キャンセルしてください。

  `AskUserQuestion` で質問します。

  | 質問 | ヘッダー | 選択肢 |
  |---|---|---|
  | プラン作成前に、不足しているコンポーネントを取り込むため、同期モードで `/power-pages:setup-solution` を実行しますか? | 完全性チェック | はい — 先に同期(推奨)、いいえ — 現在のソリューション内容でプランを作成、キャンセル |

  - **はい — 先に同期(推奨)**:`/power-pages:setup-solution` を実行します(既存のマニフェストを自動検出し、同期モードに切り替わります)。完了後、検出ヘルパーを再実行します。`missing.*` が空になっていればフェーズ2に進み、そうでなければプロンプトを再表示します。
  - **いいえ — 現在の内容でプランを作成**:不足の概要を `KNOWN_GAPS` として保存し、フェーズ3でプランHTMLのリスクセクションに表示できるようにしてから続行します。
  - **キャンセル**:ユーザーが調査できるよう、スキルを停止します。
> **この処理がある理由**:同じチェックは、エクスポート(`export-solution` フェーズ 2.5)とデプロイ(`deploy-pipeline` フェーズ 3.5)でも実行されます。ここに追加することで、古い入力に基づいて作成された計画の確認にユーザーが時間を費やす前に、できるだけ早いゲートで不足を検出できます。AGENTS.md → ALM-aware by default を参照してください。

> **`SOLUTION_DONE = false` の場合はスキップ**:まだマニフェストがない場合、古くなったものと照合する対象がありません。初回のソリューション設定はフェーズ 2 Q1 で処理します。

12. ホストの解決を実行する(PP Pipelines パスのみ。完全性チェックの後に実行します)。

**スキップ条件:** `PIPELINE_DONE = true` の場合、このステップ全体をスキップします。ホスト情報は `docs/alm/last-pipeline.json` から取得します。解決が必要なのは、新規パイプラインのプロジェクトのみです。

BAP 対象のアクセストークンを取得します(BAP API は Dataverse とは異なる対象を使用します)。
```bash
az account get-access-token --resource "https://service.powerapps.com/" --query accessToken -o tsv
```
出力を `BAP_TOKEN` として記録します。取得に失敗した場合は、`HOST_RESOLUTION = { status: 'DetectionFailed', error: '<stderr>' }` に設定し、検出呼び出しをスキップします。

検出のみを行うラッパーを実行します。スクリプトの書き込み途中で失敗しても既存の正常な `docs/alm/alm-host-resolution.json` が保持されるよう、フェーズ 1 のステップ 10 と同じ一時ファイルへの書き込み後に mv する方法を使用します。`--skus Production,Sandbox,Trial` を渡すと、試用版ライセンスと開発者テナントで、環境優先メニューに利用可能な環境が表示されます(ヘルパーの既定値は `Production,Sandbox` です。Trial 環境では新規作成の高速パスを使用できませんが、plan-alm の NoHost 分岐では既存環境へのインストールパスが常に提示され、Trial 環境も利用できるため、ここでは Trial を含めます)。
```bash
node "${PLUGIN_ROOT}/scripts/lib/ensure-pipelines-host-detect.js" \
  --envUrl "{DEV_ENV_URL}" --token "{DEV_TOKEN}" --userId "{userId}" \
  --bapToken "{BAP_TOKEN}" \
  --projectRoot "." \
  --cacheMaxAgeHours 24 \
  --skus Production,Sandbox,Trial > ./docs/alm/alm-host-resolution.json.tmp \
  && mv ./docs/alm/alm-host-resolution.json.tmp ./docs/alm/alm-host-resolution.json
```

> **注**:`ensure-pipelines-host-detect.js` は、オーケストレーター向けに `ensure-pipelines-host` スキルが公開する**検出専用ラッパー**です。このワークフローのフェーズ 1.0(キャッシュの高速パス)、フェーズ 2(テナント全体の列挙を含む解決順序)、フェーズ 5(ホストが見つかった場合の確認)を実行しますが、フェーズ 3(判断ツリー)やフェーズ 4(プロビジョニング)には進みません。出力は `docs/alm/last-host-check.json` の schemaVersion 2 と一致し、`actionTaken: "none"` が常に設定されます。

**失敗時の処理:**検出スクリプトがゼロ以外の終了コードを返した場合は、`HOST_RESOLUTION = { status: 'DetectionFailed', error: '<stderr>' }` に設定して続行します。フェーズ 2 Q4 では、現在の「URL を手動で入力する」分岐にフォールバックします。

成功した場合は、`docs/alm/alm-host-resolution.json` を解析し、ラッパーのフィールド名を plan-alm の形式にマッピングして `HOST_RESOLUTION` として保存します。
```js
HOST_RESOLUTION = {
  status: parsed.resolutionStatus,                  // 次のいずれか:AvailableUsingCustomHost | AvailableUsingCustomHostByAdminDefault | AvailableUsingPlatformHost | AvailableUnboundCustomHost | MultipleUnboundCustomHosts | PlatformHostExistsUnbound | CannotRedirect | NoHost | OrgSettingStale | PermissionDenied
  finalHostEnvUrl: parsed.finalHostEnvUrl,          // string | null
  finalHostEnvId: parsed.finalHostEnvId,            // string | null
  hostType: parsed.isPlatformHost ? 'platform' : (parsed.finalHostEnvUrl ? 'custom' : null),
  pipelinesSolutionVersion: parsed.pipelinesSolutionVersion,  // string | null
  candidates: parsed.candidates                     // { existingCustomHosts[], existingPlatformHost, eligibleForAppInstall[], inaccessibleEnvs[] }
}
```

次の 1 行を報告します。
```
Pipeline host: {finalHostEnvUrl} ({status})
```
URL がまだ設定されていない場合は、次のように報告します。
```
Pipeline host: will be ensured during setup-pipeline ({status})
```

フェーズ 2 — ALM 戦略の確認

質問を順番に行います。ソリューションは常に Q1 です。ほかのすべてのステップの前提条件となります。Q2 の後は、選択されたプロモーション戦略に基づいて分岐します。

主要な決定をすべて記録します。 以下の各決定(Q1 ソリューション、Q1b 分割/上書き、Q2 戦略、Q3 ステージ/ターゲット、Q4 ホスト、Q5 承認モード、Q5 手動エクスポートの種類)が行われるたびに、DECISIONS_LOG 配列に { field, value, source } を追加します。推奨値/自動値を変更せずに受け入れた場合は source = "default"、ユーザーがデフォルト以外のオプションを選んだ場合は source = "explicit" です。フェーズ 4 では、このログをもとに 「デフォルトで選択された決定(要確認)」 セクションを表示し、レビュー担当者がどの選択がデフォルトで、どの選択が意図的なものかを一目で確認できるようにします(*「マネージドエクスポートに同意した覚えがない」*という問題を解消します)。新しい質問は追加されません。既存の質問で得られた回答を記録するだけです。

Q1 — ソリューションのセットアップ(常に最初に質問)

SOLUTION_DONE = true の場合(フェーズ 1 でマニフェストが見つかった場合):

🚦 ゲート(プラン · plan-alm:2.q1-existing): 既存のソリューションが見つかりました。再利用する(setup-solution をスキップする)か、新規作成する(setup-solution を実行する)かを選択してください。

AskUserQuestion で質問します:

「このサイトには Dataverse ソリューションがすでに設定されています:{SOLUTION_UNIQUE_NAME}。この既存のソリューションを使用しますか?」

オプション:

  1. はい、既存のソリューションを使用します — プランでは setup-solution がスキップされます
  2. いいえ、新しいソリューションを作成します — SOLUTION_DONE = false に設定します。setup-solution が実行されます

SOLUTION_DONE = false の場合(マニフェストが見つからなかった場合):

ユーザーに伝えます(AskUserQuestion ではなく、情報提供のみ):

「このサイトには Dataverse ソリューションがまだ設定されていません。setup-solution がプランの最初のステップになります。 セットアップ時に選択する発行元のプレフィックスは取り消せません。慎重に選択してください。」

🚦 ゲート(プラン · plan-alm:2.q1-fresh): 既存のソリューションはありません。プランに setup-solution を含めるか、ユーザーが指定した一意の名前を受け入れてください。

AskUserQuestion で質問します:

「ソリューションのセットアップをプランに含めますか?」

オプション:

  1. はい、ソリューションのセットアップを含めます — 続行します
  2. すでにソリューションがあります(名前を入力) — テキストでソリューションの一意の名前を受け取り、SOLUTION_DONE = true、SOLUTION_UNIQUE_NAME = user input に設定します

Q1b — 分割の推奨(RECOMMEND_SPLIT = true の場合のみ)

🚦 ゲート(計画 · plan-alm:2.q1b-split): 推奨される分割戦略に従う、単一ソリューションに変更する、まず Asset Advisory を受け入れる、または移行ガイダンスを表示します。

フェーズ 1 のステップ 10 の決定木では、複数のソリューションに分割することが推奨されました。AskUserQuestion で質問します。

「サイトの規模とコンポーネントの分析に基づくと、推奨されるアプローチは {splitStrategy} です。1 つではなく {N} 個のソリューションを作成します。この推奨に従いますか?」

選択肢:

  1. 推奨された分割を使用する — 決定木で得られた proposedSolutions[] を使用して続行します。setup-solution が N 個すべてのソリューションを作成します。
  2. それでも単一のソリューションにする — 単一のソリューションに変更します。変更理由を記録します。setup-solution はすべてのコンポーネントを含む 1 つのソリューションを作成します。
  3. まず Asset Advisory を受け入れる(assetAdvisory.candidates.length > 0 の場合のみ表示)— ユーザーは、フラグが付けられたアセットを外部化することに同意します。それらのファイルを除外してサイズを再計算し、決定木を再実行して、新しい推奨を提示します。
  4. 移行ガイダンスを表示する(既存の .solution-manifest.json が見つかり、推奨と一致しない場合のみ表示)— docs/alm-migration-plan.md を作成して終了します。実行はしません。

選択肢 1 の場合: proposedSolutions を使用して続行します。

選択肢 2 — それでも単一のソリューションにする場合: これは、データに基づく推奨(多くの場合、正しい推奨)を覆す操作です。変更を受け入れる前に、ユーザーがワンクリックで却下するのではなく、十分な情報に基づいて選択できるよう、階層シグナルを改めて提示します。 SPLIT_PLAN.sizeAnalysis から読み取ります。

You're about to override a {splitStrategy} recommendation. Before doing that, here's what the estimator measured:

• Total size:           {totalSizeMB.value} MB   (tier: {totalSizeMB.tier} — threshold {thresholds.maxSolutionSizeMB} MB)
• Component count:      {componentCount.value}   (tier: {componentCount.tier} — threshold {thresholds.maxComponentCount})
• Schema attributes:    {schemaAttrCount.value}  (tier: {schemaAttrCount.tier} — threshold {thresholds.maxSchemaAttrs})
• Web files aggregate:  {webFilesAggregateMB.value} MB  (tier: {webFilesAggregateMB.tier} — threshold {thresholds.maxAggregateWebFilesMB} MB)
• Env var definitions:  {envVarCount.value}      (tier: {envVarCount.tier})

{if SPLIT_PLAN.truncationSuspected === true:
  ⚠ The estimator flagged its inputs as possibly truncated:
    {SPLIT_PLAN.truncationWarnings.join('\n       ')}
  The numbers above could be UNDER-counted. Investigate before overriding.
}

Solutions exceeding the platform thresholds frequently fail to import (timeouts, OOM, partial state). A single-solution plan that lands in the red tier is the most common cause of "the deploy hung overnight" reports. Recovering means splitting after the fact, which is harder than splitting upfront.

🚦 ゲート(同意 · plan-alm:2.q1b-override): データに基づく分割の推奨を覆し、単一のソリューションにします。「はい」の場合は、自由記述の overrideReason を確認します。

次に、AskUserQuestion で質問します。

質問ヘッダー選択肢
それでも単一のソリューションにしますか?変更の確認いいえ — 推奨された {splitStrategy} による分割を使用する(推奨)、はい — それでも変更し、理由を記録する、キャンセル — 戦略を再検討する
  • いいえ → 選択肢 1(推奨された分割を使用する)に戻ります。
  • はい → 追加の AskUserQuestion(「簡潔にお答えください。このサイトでは、なぜ単一ソリューションが適切ですか?」)で自由記述の overrideReason を必須にします。overrideReason と、上に表示した階層別シグナルである overrideConfirmedSignals を計画に記録します。その後に限り、レンダリング用として SPLIT_PLAN.proposedSolutions を単一ソリューションの構成に変更します。
  • キャンセル → Q1b の冒頭に戻ります。

確認を挟む理由: 現場から報告されたセッションでは、「それでも単一にする」はワンクリックで変更でき、誤った推奨につながる最も一般的な経路であることが判明しました。再確認はユーザーの考えを変えさせるためではなく、変更が十分な情報に基づいて行われ、その理由が監査用に記録されることを確認するためのものです。理由を記録したうえでの変更は全面的に尊重されます。このゲートが防ぐのは、変更理由を記録しないままワンクリックで進むことだけです。

選択肢 3 の場合: 見積もりからアドバイザリー候補のサイズを差し引き、compute-split-plan.js を再実行して、結果を再提示します。 選択肢 4 の場合: docs/alm-migration-plan.md を作成し(仕様ドキュメント solution-splitting-logic.md §7 を参照)、コミットして、計画を Deferred としてマークし、終了します。


Q2 — 戦略の選択(常に質問)

🚦 ゲート(plan · plan-alm:2.q2-strategy): 昇格戦略を選択します — PP Pipelines(推奨)、手動エクスポート/インポート、既存のパイプライン、または選択のサポート。以降の計画の分岐点になります。

推奨事項の前置き(プロンプト本文内に表示)。 選択肢を列挙する前に、推奨事項と判断に影響する状態を提示し、選択肢 4 を確認するまで分からないということがないようにします。

推奨:継続的な CI/CD には Power Platform Pipelines を使用 — 自動昇格、承認ゲート、デプロイ履歴を一か所で管理できます。手動エクスポート/インポートは一度限りの移行を目的としており、変更のたびに繰り返すと監査証跡が失われ、バージョン不整合によるバグが気付かれないまま発生する可能性があります。

(条件付き。HOST_RESOLUTION.resolutionStatus ∈ {AvailableUsingCustomHost, AvailableUsingCustomHostByAdminDefault, AvailableUsingPlatformHost, AvailableUnboundCustomHost} の場合に追加): テナントには、すでに {HOST_RESOLUTION.finalHostEnvUrl} に Pipelines ホストがあります(Pipelines v{HOST_RESOLUTION.pipelinesSolutionVersion})。このプロジェクトでは、PP Pipelines 用の新しいインフラストラクチャは不要です。

AskUserQuestion で質問します。

「環境間でソリューションをどのように昇格させますか?」

選択肢:

  1. Power Platform Pipelines(継続的な CI/CD に推奨) — Microsoft ネイティブの CI/CD、管理されたデプロイ、承認ゲート
  2. 手動エクスポート/インポート(一度限りの移行のみ) — 開発環境から zip をエクスポートし、各ターゲット環境に直接インポートします
  3. すでにパイプラインを設定済み — 今すぐデプロイを実行します
  4. 選択のサポート — 比較の全容を再表示します

選択内容を DECISIONS_LOG に記録します({ field: "strategy", value, source })。

選択肢 4 が選択された場合: 上記の推奨事項の前置きを再表示し、選択肢 1~3 のみで Q2 を再度質問します。

選択肢 3 が選択された場合: docs/alm/last-pipeline.json を読み取り、パイプライン名とステージを確認してから、strategy = pp-pipelines、PIPELINE_DONE = true として Phase 3(計画の生成)に進みます。


PP Pipelines の手順 — Q3 から Q6

🚦 ゲート(plan · plan-alm:2.q3-stages): デプロイステージ数を選択します — ステージングのみ/+本番/本番へ直接/カスタム。

Q3: AskUserQuestion で質問します。

「このパイプラインにはいくつのデプロイステージが必要ですか?」

選択肢:

  1. ステージングのみ — Dev → Staging(本番は後で追加します)
  2. ステージング+本番 — Dev → Staging → Production(完全な昇格チェーン)
  3. 本番へ直接 — Dev → Production のみ(ステージングを省略)
  4. カスタム — 独自のステージ構成を説明します

選択肢 4 の場合:自由記述(「その他」経由)を受け付け、その回答からステージのリストを作成します。

ステージは PP_STAGES({ label, envUrl, envName, type } の配列)として保存します。Dev は常にソースです。

各ステージの envName は、ENV_LIST(Phase 1 の Step 5 で list-environments.js により取得)から設定します。各エントリの environmentUrl と照合する際は、URL のオリジンを基準にし(小文字化し、末尾のスラッシュを除去し、パスとクエリを無視)、一致したエントリの displayName を envName にコピーします。一致するものがない場合(通常、「その他」からユーザーがカスタム URL を貼り付けた場合)は、envName を未設定のままにします。レンダラーはステージカードに URL のみを表示します。レンダラーはステージラベルと URL の間に envName を配置します(例:Staging / Supplier Portal Staging / https://orgd6a9894f.crm5.dynamics.com/*)。これにより、レビュー担当者は環境をひと目で認識でき、URL からワンクリックで環境に移動できます。開発/ソースステージには type: "source" を、下流の各ステージには type: "target" を設定し、レンダラーがアクティブなステージに適切なスタイルを適用できるようにします。 選択したステージを既存のパイプラインと照合します(ソフト警告、PP パス)。 PIPELINE_DONE = true(docs/alm/last-pipeline.json が存在する)の場合、PP_STAGES の選択された各 ターゲット ステージを、そのファイルの stages[] と照合します(targetEnvironmentUrl のオリジンで照合し、次にステージの name で照合します)。既存のパイプラインに一致するステージがない選択済みターゲットごとに、プランの Risks セクションに { type: "warning" } エントリーを記録します。「選択したターゲット '{label}' ({envUrl}) に、既存のパイプライン '{pipelineName}' 上で一致するステージがありません。setup-pipeline で追加する必要があります。」* これにより、保存済みプランでは Dev → Production へ直接 とされているのに、実際のパイプラインには Deploy to Staging ステージしかない、といった実際の不整合を検出できます。これは情報提供のみを目的としています。ブロックしたり、再度質問したりしないでください。ユーザーは引き続きプランを承認でき、setup-pipeline が実行時に実際のステージを調整します。

🚦 ゲート(plan · plan-alm:2.q4-host): ホスト環境の選択 — HOST_RESOLUTION.status に応じて分岐し、適切なメニュー(検出済みを使用/リストから選択/NoHost のホストタイプ/Sandbox の確認/CannotRedirect のブロック/手動貼り付け)を表示します。以降の plan-alm と ensure-pipelines-host で使用する HOST_ENV_URL と WILL_PROVISION_* フラグを設定します。分岐ごとに AskUserQuestion を使用します。

トリガー: Phase 2 Q4 の開始時。HOST_RESOLUTION は Phase 1 のステップ 12 で設定されます。 質問する理由: ホストを自動選択すると、ユーザーの意図に反して新しい Custom Host をプロビジョニング(WILL_PROVISION_CUSTOM)し、Azure の容量クォータを消費する可能性があります。または、誤った環境を選択して、パイプラインを無関係なホスト経由で実行する可能性もあります。後続の ensure-pipelines-host スキルはこの回答を信頼し、独自の 3.C メニューを省略します。 キャンセル時: 何も起こりません。プロビジョニングはまだ開始されていません。

Q4(ホスト環境 — Phase 1 のステップ 12 で設定された HOST_RESOLUTION.status に応じて分岐):

この質問では、Phase 1 のステップ 12 で実行された新しい検出専用ラッパーによって設定された HOST_RESOLUTION を使用します。各分岐で HOST_ENV_URL(以降の plan-alm で使用)を設定し、補助フラグ CHOSEN_ENV_URL、WILL_PROVISION_PLATFORM、WILL_PROVISION_CUSTOM、WILL_USE_PPAC、WILL_ENSURE_HOST、USER_CHOSE_DEFER_TO_SETUP_PIPELINE も設定する場合があります。デフォルト値:HOST_ENV_URL = HOST_RESOLUTION.finalHostEnvUrl、すべてのフラグは false/null です。

NoHost 分岐で、ensure-pipelines-host の Phase 3.C まで保留せず、ここで環境優先メニューを表示する理由: 当初の設計では、plan-alm で「新規にプロビジョニングします。続行しますか?」という yes/no の質問をし、実行時に 3.C で環境優先の選択肢を表示する想定でした。しかし実際には、エージェントが plan-alm での yes の確認を、3.C 全体(または 4.A の事前呼び出しゲート)を省略する許可として扱い、ユーザーが 4.A → 409 の試用ライセンスエラーに直面していました。既存環境へのインストール(4.B)であれば問題なく進められたケースです。ユーザーが十分な情報を持っている計画時点で、ここで一度だけ環境優先メニューを表示することで、曖昧さを解消できます。その後、ensure-pipelines-host は CHOSEN_ENV_URL を信頼し、独自の 3.C メニューを省略します(ensure-pipelines-host の Phase 3 のスキップルールを参照)。

statusQ4 プロンプト結果
AvailableUsingCustomHost, AvailableUsingCustomHostByAdminDefault, AvailableUsingPlatformHost「ホスト {finalHostEnvUrl}(Pipelines v{pipelinesSolutionVersion})が検出されました。このホストを使用しますか?」選択肢:1. はい、これを使用する/2. 別のホスト環境を使用する(その他)はい → HOST_ENV_URL = HOST_RESOLUTION.finalHostEnvUrl。いいえ → 現在の「別の URL を入力」分岐にフォールバックします(「その他」からの自由入力)。
AvailableUnboundCustomHost「テナント内に既存のカスタムホスト {displayName}({finalHostEnvUrl})が見つかりました。開発環境にはまだバインドされていません。setup-pipeline はこれを再利用します(推奨。重複を避けられます)。このホストを使用しますか?」選択肢:1. はい、これを使用する/2. 別のホスト環境を使用する(その他)はい → HOST_ENV_URL = HOST_RESOLUTION.finalHostEnvUrl、WILL_ENSURE_HOST = true。いいえ → 「別の URL を入力」にフォールバックします。
MultipleUnboundCustomHosts「テナント内にカスタムホストが {N} 件見つかりました。setup-pipeline で使用するホストを選択してください。」選択肢:HOST_RESOLUTION.candidates.existingCustomHosts[] を表示名と URL で列挙(最大 3 件)し、カスタム URL 用の「その他」と、「後で決める — setup-pipeline で確認します」を追加します。選択した候補 → HOST_ENV_URL = candidate.instanceApiUrl、WILL_ENSURE_HOST = true。後で決める → HOST_ENV_URL = null、WILL_ENSURE_HOST = true、USER_CHOSE_DEFER_TO_SETUP_PIPELINE = true。
PlatformHostExistsUnbound「テナントの Platform Host {finalHostEnvUrl} が存在します。これを使用しますか(管理者ロールは不要)、それとも新しいカスタムホストを作成しますか?」選択肢:1. Platform Host を使用する/2. 新しいカスタムホストを作成する/3. キャンセル1 → HOST_ENV_URL = HOST_RESOLUTION.finalHostEnvUrl、WILL_ENSURE_HOST = true。2 → HOST_ENV_URL = null、WILL_PROVISION_CUSTOM = true、WILL_ENSURE_HOST = true。3 → 終了します。
NoHostホストタイプのプロンプト — ensure-pipelines-host のフェーズ 3.C と同じ形式にして、実行時に再度尋ねるのではなく、ここでホストを一度だけ選択できるようにします。次の内容を提示します。「{devEnvUrl} にバインドされた Pipelines ホストがありません。Pipelines をホストする環境を選択してください。Pipelines はテナントごとに 1 つの環境に存在し、パイプライン、ステージ、実行履歴はそこに保存されます。ソース環境はその環境を介してデプロイされます。」 最上位の選択肢:1. 「Platform Host をプロビジョニングする(推奨)— テナントのホーム地理的リージョンに自動プロビジョニングされる、Microsoft 管理の Dataverse 環境です。Pipelines アプリは事前にインストールされています。冪等です。約 3~5 分。」2. 「カスタムホストをセットアップする — Pipelines は管理者が制御する Dataverse 環境に配置されます。既存の環境を使用するか、新しい専用環境を作成するかを確認します。」3. 「PPAC を開いて手動で作成する(管理者向けの代替手段)。」4. 「手動のエクスポート/インポート戦略に切り替える。」5. 「キャンセル。」ユーザーがオプション 2 を選択した場合、カスタムホストのサブプロンプトを表示します。HOST_RESOLUTION.candidates.eligibleForAppInstall[] から、適格な環境のリストを作成し、オリジンとの一致に応じてロールラベル(dev env、source env、staging env、production env)を付けます。ロールを考慮して順位付けし、表示するリストは最大 5 件に制限します(下記「適格環境の表示上限」を参照)。サブ選択肢:a. 表示対象の各環境(表示名 + URL + ロールラベル)に「この環境に Pipelines アプリをインストール」というラベルを付けます。sandbox-sku 環境には「(Sandbox — 確認が必要)」という接尾辞を付け、環境ごとの選択肢の最後に「その他(URL を貼り付け)」を追加します。b. 「新しい専用環境を作成する(D365_ProjectHost テンプレート、約 5~10 分、Power Platform 管理者が必要)。」c. 「戻る — ホストタイプメニューに戻る。」適格な環境のリストが空の場合は、サブ選択肢 a を除外し、b と c のみを表示します。オプション 1(Platform Host)→ HOST_ENV_URL = null、WILL_PROVISION_PLATFORM = true、WILL_ENSURE_HOST = true。オプション 2 → サブプロンプトを表示します。サブ選択肢 a で環境を選択 → HOST_ENV_URL = picked.instanceApiUrl、CHOSEN_ENV_URL = picked.instanceApiUrl、WILL_ENSURE_HOST = true(該当する場合は、この解決を確定する前に Sandbox の確認ゲートを通過する必要があります。「その他(URL を貼り付け)」の場合は、自由入力で環境 URL を尋ねたうえで、選択した適格環境として処理します)。サブ選択肢 b → HOST_ENV_URL = null、WILL_PROVISION_CUSTOM = true、WILL_ENSURE_HOST = true。サブ選択肢 c → 最上位のメニューを再表示します。オプション 3(PPAC)→ HOST_ENV_URL = null、WILL_USE_PPAC = true、WILL_ENSURE_HOST = true。オプション 4(手動戦略)→ STRATEGY = manual としてフェーズ 2 を再開します。オプション 5 → 終了します。
CannotRedirectブロックします。 HOST_RESOLUTION.candidates/warnings にある、組織設定とテナント既定値の不一致エラーを表示してスキルを停止します。解決できるのは Power Platform 管理者のみです。具体的なエラーを表示して終了します。
OrgSettingStale, PermissionDenied, DetectionFailedエラーを表示し、pac env list の事前入力候補を使って、ユーザーにホスト URL を手動で入力するよう求めます(現在のフォールバック)。ENV_LIST から最大 3 件の既知の環境 URL と、カスタム URL 用の「その他」を事前入力候補として表示します。docs/alm/last-pipeline.json が存在する場合は、そこから取得した値を最初の候補として事前入力します。HOST_ENV_URL = user-supplied。
結果として得られた HOST_ENV_URL を、plan-alm の残りの部分で使用できるように保存します。補助フラグ CHOSEN_ENV_URL、WILL_PROVISION_PLATFORM、WILL_PROVISION_CUSTOM、WILL_USE_PPAC、WILL_ENSURE_HOST、USER_CHOSE_DEFER_TO_SETUP_PIPELINE は、フェーズ 3 の planData hostResolution ブロックと、フェーズ 4 のインラインサマリーに渡されます。ensure-pipelines-host は、そのブロックから chosenEnvUrl、willProvisionPlatform、willProvisionCustom、willUsePpac を読み取り、ユーザーがここで既に選択を行っている場合は、自身のフェーズ 3.C メニューをバイパスします。

対象環境の表示上限(上記の NoHost 行および MultipleUnboundCustomHosts で使用)。対象リストが長くなる場合は、プロンプトが読みやすく保たれるように、表示される選択肢を次のように構築します。

  1. 常に表示されるロールラベル付き環境を最優先。 dev env、source env、staging env、production env のいずれかのラベルを持つ対象環境をすべて含めます(URL オリジンによって devEnvUrl および PP_STAGES[].envUrl と照合)。これらはプロジェクト自身の環境であり、ほぼ常に正しい選択です。オリジンで重複排除します。
  2. 残りの枠を最大 5 件まで埋める — 対象リストの残りから、list-tenant-envs.js が返す順序(名前ヒントのパターン pipeline|deploy|host|alm|cicd|govern → 管理者権限 → 最近度)で埋めます。
  3. 「その他(URL を貼り付け)」を追加 — 選択肢 1 のネストされたリスト内の最後の環境エントリとして。上限に含まれなかった環境のためのエスケープハッチです。
  4. eligible.length > 5 の場合、選択肢 1 の見出しに次の接尾辞を付けます: Showing top 5 of {N}; the remaining {N-5} eligible env(s) can be reached via the "Other (paste URL)" entry. eligible.length <= 5 の場合、接尾辞なし(すべての環境がインラインで表示されます)。
  5. ユーザーが「その他(URL を貼り付け)」を選択した場合、URL 入力欄に ENV_LIST を事前入力します(フェーズ 1 で収集した list-environments.js の出力 — 各エントリの environmentUrl)。これにより、ユーザーは URL を手入力する代わりに、完全なインベントリから貼り付けたり選択したりできます。

同じ上限ポリシーが ensure-pipelines-host フェーズ 3.C にも適用されます — 同じルールについては、そのスキルのステップ 3a を参照してください。plan-alm 経由で入る場合でも、setup-pipeline → ensure-pipelines-host を直接経由して入る場合でも、ユーザーが同じプロンプト形状を見られるように、2 つの実装を一貫させてください。

🚦 ゲート (plan · plan-alm:2.q5-approval): 承認モードを選択してください — ステージごとに必須 / ステージングは自動 + 本番は必須 / ゲートなし。

Q5: AskUserQuestion で尋ねます:

「デプロイは各ステージの前に承認を必要とすべきですか?」

選択肢:

  1. 各ステージの前に必須(本番環境に推奨)
  2. ステージングは自動承認、本番は承認が必要
  3. 承認ゲートなし — 自動的にデプロイ

PP_APPROVAL_MODE として保存します。

注記: PP Pipelines は常にターゲット環境に対して managed ソリューションとしてエクスポートされます。EXPORT_TYPE = "managed" を自動的に設定します — 質問は不要です。

Q6(自動検出、質問なし): 次の順序で HAS_ENV_VARS を解決します:

  1. ENV_VARS_DETAILS.length > 0 の場合(フェーズ 1 ステップ 10b が変数ごとの行を返した): HAS_ENV_VARS = true。これが最も正確なシグナルです — 定義を直接列挙しています。
  2. そうでなく、SPLIT_PLAN.sizeAnalysis.envVarCount.value > 0 の場合(サイズ推定器が定義をカウントしたが、列挙子が行を返さなかった — 通常は発行元プレフィックスまたはトークンが欠けていたため): HAS_ENV_VARS = true。プランのカウントサマリーのフォールバックが、変数ごとの詳細は後で列挙されることをユーザーに説明します。
  3. そうでなく、SOLUTION_DONE = true で .solution-manifest.json が componenttype 380 のコンポーネントをリストしている場合: HAS_ENV_VARS = true。(古いマニフェストのフォールバック — 10b が導入された現在ではほとんど発火しないはずです。)
  4. それ以外の場合: HAS_ENV_VARS = false。変数は setup-solution 中に検出されます。

HAS_ENV_VARS = true の場合、プランは deploy-pipeline がステージごとの環境変数の値を求めるプロンプトを表示することを記します(フェーズ 3 のリスクの設定を参照)。


手動パス — Q3~Q6

🚦 ゲート(plan · plan-alm:2.q3-manual): 手動エクスポート/インポートパスの対象環境数を選択します。

Q3: AskUserQuestion で質問します:

「デプロイ先の対象環境はいくつ必要ですか?」

選択肢:

  1. 対象環境は1つ(例:Production)
  2. 対象環境は2つ(例:Staging、次にProduction)
  3. Dev のみ — まだデプロイしない

MANUAL_TARGET_COUNT として保存します。

選択肢3の場合:MANUAL_TARGET_COUNT = 0 に設定します。Q5 に進みます。

🚦 ゲート(plan · plan-alm:2.q4-manual-target): 手動で指定する各対象環境のURLを選択します。MANUAL_TARGET_COUNT のループ内で対象ごとに実行されます。 対象環境が2つ(Staging + Production)の場合、個別のプロンプトが2回表示されます。各プロンプトには pac env list の内容があらかじめ入力され、異なるURLを指定できます。すべての対象環境のURLを1つの複数入力プロンプトで収集しないでください。各対象環境は個別の判断事項です(対象ユーザーや環境の特性が異なり、SKUも異なる場合があります)。

Q4(ステージごとに1回): 必要な対象環境ごとに、AskUserQuestion で質問します:

「対象環境 {N} のURLは何ですか?」

ENV_LIST からあらかじめ入力します。pac env list で取得した環境URLを最大3つ選択肢として表示し、最後に「別のURLを入力する」を追加します。

対象環境のURLは MANUAL_TARGETS(配列)として保存します。

🚦 ゲート(consent · plan-alm:2.q5-manual-type): マネージドとアンマネージドのエクスポートを選択します。作成されるzipに対する、取り消し不能な選択です。

Q5: AskUserQuestion で質問します:

「ソリューションをどのようにエクスポートしますか?」

選択肢:

  1. Managed — Staging/Production 用(対象環境で編集できません)
  2. Unmanaged — Dev から Dev への移行用(対象環境で編集できます)

EXPORT_TYPE として保存し、決定内容を記録します({ field: "exportType", value: EXPORT_TYPE, source: "default"|"explicit" })。

チェックポイントに関する希望は収集しません。 plan-alm はエクスポート/インポート自体を実行しなくなったため、以前の「エクスポートとインポートの間に一時停止しますか?」という質問(実行を制御するだけの質問)はなくなりました。計画には推奨手順として、対象環境ごとに export-solution、次に import-solution を記録します。ユーザーはそれらを実行する際、2つの手順の間にzipを確認します。この点を説明する常設の注記を、フェーズ3で計画のリスク/手順に追加します。

Q6(自動検出、質問なし): PP Pipelines Q6 と同様に、環境変数の定義を確認します。


フェーズ3 — HTML 計画の生成

ここで2つのプランナータスクを作成します — 戦略が決まりました。plan-alm はプランナーであるため、パスにかかわらずタスクは必ず2つです:

#件名activeForm説明
1ALM 計画を生成ALM 計画を生成中planData を構築し、docs/alm-plan.html をレンダリングする
2ALM 計画を承認して保存計画の承認を待機中インライン要約を提示し、承認者を記録して保存する(承認済みまたはドラフト)

Setup/Deploy/Activate/Test/Import/Finalize のタスクは作成しないでください。 これらはユーザーが承認後に実行する個別の ALM スキルによる実行手順であり、plan-alm のタスクではありません。推奨手順は引き続き計画に表示されます。以下の planData steps[] 配列に手順が記載され(すべて status: "pending")、レンダリングされた HTML の Execution Checklist に正式な計画として表示されます。下流の各スキルは、ユーザーが実行したときに refresh-alm-plan-data.js を介して、それぞれの手順を完了としてマークします。

タスク1(「ALM 計画を生成」)を in_progress に設定します。

planData を構築する

収集したすべての戦略入力を含む planData オブジェクトを構築します:

{
  "SITE_NAME": "{siteName}",
  "siteType": "spa | non-spa",        // from Phase 1 Step 1; readers also accept legacy code/declarative/data-model values
  "GENERATED_AT": "{ISO timestamp}",
  "STRATEGY": "pp-pipelines | manual",
  "EXPORT_TYPE": "managed | unmanaged",   // PP Pipelines path: always "managed"
  "APPROVAL_MODE": "{approvalMode description}",
  "HAS_ENV_VARS": true | false,
  "SOLUTION_DONE": true | false,
  "PIPELINE_DONE": true | false,
  "PLAN_STATUS": "Draft",                 // set to "Approved" or "Draft" in Phase 4; plan-alm never sets "In Execution"/"Completed"
  "PLAN_MODE": "draft",                   // "approved" | "draft" — set in Phase 4. Tooling/downstream skills distinguish an approved plan from a draft. Never "executed" (plan-alm doesn't execute).
  "PLAN_QUALITY": "complete",             // "complete" | "degraded" — degraded when discovery was incomplete (Phase 1 auth failure or the Phase 3 completeness check failed). Surfaced as a prominent risk.
  "decisionsLog": [                       // from DECISIONS_LOG (Phase 2) — drives the Phase 4 "Decisions defaulted" section
    { "field": "strategy", "value": "pp-pipelines", "source": "explicit" }
  ],
  "LAST_INVOCATION_AT": null,
  "APPROVED_BY": "",
  "APPROVAL_DATE": "",
  "stages": [
    { "label": "Dev", "envUrl": "{devEnvUrl}", "type": "source" },
    { "label": "Staging", "envUrl": "{stagingUrl}", "type": "target" },
    { "label": "Production", "envUrl": "{prodUrl}", "type": "target" }
  ],
  "steps": [
    { "name": "Setup solution", "status": "pending", "skip": false },
    { "name": "Setup pipeline", "status": "pending", "skip": false },
    { "name": "Deploy via pipeline to Staging", "status": "pending", "skip": false },
    { "name": "Activate site in Staging", "status": "pending", "skip": false },
    { "name": "Test site in Staging", "status": "pending", "skip": false },
    { "name": "Deploy via pipeline to Production", "status": "pending", "skip": false },
    { "name": "Activate site in Production", "status": "pending", "skip": false },
    { "name": "Test site in Production", "status": "pending", "skip": false }
  ],
  // The steps[] above is the PP Pipelines path. For the MANUAL path (STRATEGY = "manual")
  // emit this shape instead — do NOT include "Setup pipeline"/"Deploy via pipeline":
  //   { "name": "Setup solution", ... },
  //   { "name": "Export solution", ... },
  //   then PER Manual target {targetLabel}:
  //     { "name": "Import to {targetLabel}", ... },
  //     { "name": "Activate site in {targetLabel}", ... },   // import-solution does NOT activate
  //     { "name": "Test site in {targetLabel}", ... }
  // These names are what refresh-alm-plan-data.js step-sync (export/import/activate/test)
  // and computeNextStep match on, so the manual checklist and next-step nudges resolve
  // to /power-pages:export-solution → /power-pages:import-solution → /power-pages:activate-site → /power-pages:test-site.
  "validationRuns": {
    "Staging": null,
    "Production": null
  },
  "pipelineMeta": null,                   // populated when docs/alm/last-pipeline.json exists — see "pipelineMeta block" below
  "risks": [
    { "type": "info", "message": "..." }
  ],
  "solutionContents": {
    "tables": ["{table1}", "{table2}"],
    "botComponents": [{ "name": "..." }],
    "siteSettings": {
      "keepAsIs": [{ "name": "..." }],
      "promoteToEnvVar": [{ "name": "...", "value": "..." }],
      "credentialNeedsDecision": [{ "name": "..." }]
    }
  },

  // --- v2 fields from the split decision tree (Phase 1 Step 10) ---
  "sizeAnalysis": { /* tier-classified signals from SPLIT_PLAN.sizeAnalysis */ },
  "assetAdvisory": { /* candidates + recommendation from SPLIT_PLAN.assetAdvisory */ },
  "splitStrategy": "single | strategy-1-layer | strategy-2-change-frequency | strategy-3-schema-segmentation | strategy-4-config-isolation",
  "appliedStrategies": ["strategy-1-layer"],            // may include "composite-sub-partition" when a Layer-split child still exceeded the size or count cap and was sub-divided
  "compositeSubPartitioned": false,                     // mirrors SPLIT_PLAN.compositeSubPartitioned — true when the renderer's strategy rationale should mention sub-partitioning
  "proposedSolutions": [ /* from SPLIT_PLAN.proposedSolutions — ALWAYS at least 1 entry */ ],
  "recommendations": [ /* from SPLIT_PLAN.recommendations */ ],
  "envVars": [ /* from ENV_VARS_DETAILS (Phase 1 Step 10b) — { schemaName, type, defaultValue, siteSetting } per definition; empty array when DEV_TOKEN unavailable */ ],
  "plannedEnvVarCount": 0,                // sum of SITE_SETTINGS_DATA.promoteToEnvVar.length + credentialNeedsDecision.length — env vars setup-solution will offer to create. Renderer shows this as "N planned" alongside the existing-count stat so a fresh project (envVars: []) doesn't look like nothing is happening when the risks list says auth settings will be promoted. Reset to 0 by refresh-alm-plan-data setup-solution phase.
  "breakdown": { /* bytes-per-category from the estimate */ },
  "estimationMethod": "metadata-based",
  "estimationAccuracyPct": 15,
  "truncationSuspected": false,             // true when the estimator's truncation canary fired — surfaced as a red banner on the rendered plan and gates the "keep as single solution anyway" override in Phase 2 Q1b
  "truncationWarnings": [],                 // specific category-level reasons the canary fired (e.g. "Dataverse reports 6050 powerpagecomponent rows but discovery returned 500")
  "webFilesDiskMeasuredMB": null,           // disk-measured total when `--projectRoot` was passed to estimate-solution-size.js and a build-output dir (dist/public-output/build/.output) was found; null otherwise. Renderer surfaces this under the Web Files signal when it disagrees materially with `webFilesAggregateMB` — useful when file-typed columns hold bytes that $select=content can't return.
  "webFilesDiskMeasuredPath": null,         // the build-output directory that produced webFilesDiskMeasuredMB; null when not measured.
  "webFileSampleSize": 0,                   // how many web-file rows the stratified sampler actually read for size measurement. Compared with webFileCount this tells reviewers how aggressively the aggregate was extrapolated.

  // --- Raw discovery snapshot — embedded verbatim for diagnosability ---
  // The mapped fields above (sizeAnalysis, splitStrategy, hostResolution, …)
  // are derived from these. When a rendered plan looks wrong, this block is
  // what a reviewer (or another agent) needs to diagnose without re-running
  // discovery. Never reference these fields from the renderer's display
  // paths — keep them as a sealed diagnostic envelope.
  "rawDiscovery": {
    "estimate": { /* full output of estimate-solution-size.js, verbatim */ },
    "splitPlan": { /* full output of compute-split-plan.js, verbatim */ },
    "hostResolution": { /* full output of ensure-pipelines-host-detect.js (PP path only) or null */ }
  },

  // --- v3 fields from the host resolution (Phase 1 Step 12) — PP Pipelines path only ---
  "hostResolution": {
    "status": "AvailableUsingCustomHost | AvailableUsingCustomHostByAdminDefault | AvailableUsingPlatformHost | AvailableUnboundCustomHost | MultipleUnboundCustomHosts | PlatformHostExistsUnbound | CannotRedirect | NoHost | OrgSettingStale | PermissionDenied | DetectionFailed",
    "hostEnvUrl": "https://pascalepipelineshost.crm.dynamics.com" | null,
    "hostEnvId": "0817fd3d-a664-e99a-a758-dd9dc03ceb01" | null,
    "hostType": "custom | platform | null",
    "pipelinesSolutionVersion": "9.x.y.z" | null,
    "candidatesCount": 0,
    "willEnsureDuringExecution": true | false,
    "willProvisionPlatform": true | false,
    "willProvisionCustom": true | false,
    "willUsePpac": true | false,
    "chosenEnvUrl": "https://orgc4f78248.crm5.dynamics.com/" | null,
    "userChoseDeferToSetupPipeline": false
  }
}

solutionContents には、フェーズ1で構築した SOLUTION_CONTENTS_DATA を設定します。検出できなかった場合は null を渡します。レンダラーが代替の注記を表示します。 plannedEnvVarCount は SITE_SETTINGS_DATA から計算します(Phase 1 Step 7):(SITE_SETTINGS_DATA.promoteToEnvVar?.length || 0) + (SITE_SETTINGS_DATA.credentialNeedsDecision?.length || 0)。SITE_SETTINGS_DATA が null の場合(Step 7 のクエリに失敗した場合)、plannedEnvVarCount = 0 に設定します。レンダラーはこれを、envVars.length(既存)および sizeAnalysis.envVarCount.value(サイズ推定器のカウント)と合わせて読み取り、Overview の統計カードと Size Analysis のシグナルに「今日の N 件/計画中の +M 件」と表示します。

validationRuns ブロック(PP Pipelines パスのみ — 対象ステージごとに値を null にしたエントリーを 1 つずつ初期化します)。plan-alm は計画時点ではこれらを null のままにします。ユーザーが各ステージで test-site を実行すると、後で test-site 自身の最終フェーズの更新処理(refresh-alm-plan-data.js --phase test-site --stageName {stage})によって値が設定されます。カテゴリ別の完全なテストレポートが、レンダリングされた HTML の Validation タブに表示されます。ステージごとの形式:

{
  "validationRuns": {
    "Staging": {
      "url": "https://example.powerappsportals.com",
      "runAt": "2026-04-27T15:00:00.000Z",
      "durationSec": 120,
      "runOutcome": "passed | passed-with-warnings | failed",
      "summary": {
        "critical": 0, "high": 1, "medium": 0, "low": 2,
        "total": 3, "automated": 2, "manual": 1,
        "passed": 2, "failed": 1, "skipped": 0
      },
      "categories": [
        {
          "id": "site-load",
          "name": "Site Load",
          "icon": "📦",
          "tests": [
            {
              "id": "t01",
              "name": "Homepage returns 200 OK",
              "severity": "critical",
              "type": "automated",
              "status": "passed",
              "description": "...",
              "steps": ["GET /", "Expect 200"],
              "expected": "200 OK",
              "actual": "200 OK",
              "validates": "Site activation"
            }
          ]
        }
      ]
    },
    "Production": null
  }
}

この形式は、test-site Phase 6.7a が書き込む docs/alm/last-test-site.json と同一です。更新ヘルパーは、test-site の実行時にそのファイルをそのまま読み取り、validationRuns[stageName] に割り当てます。レンダラーは runOutcome を緑/黄/赤の Outcome バッジに対応付け、Validation タブにステージごとのサブタブを作成します。Manual パスでは、validationRuns を planData から省略します。

pipelineMeta ブロック(PP Pipelines パスのみ — planData の構築時に docs/alm/last-pipeline.json と docs/alm/last-deploy.json から読み取ります。パイプラインがまだ構成されていない新規プランでは null です。ユーザーが実行すると、後で setup-pipeline と deploy-pipeline 自身の最終フェーズの更新処理によって更新されます)。このプロジェクトの構成を実際に移行しているパイプラインを示します。形式:

{
  "pipelineMeta": {
    "isActive": true,
    "pipelineId": "2b8b5de8-8f43-f111-bec7-6045bd569497",
    "pipelineName": "BYOC Supplier Portal Pipeline",
    "reusedByWiring": null,
    "lastDeploy": {
      "status": "Succeeded",
      "stageName": "Deploy to Staging",
      "deployedAt": "2026-04-29T08:42:00.000Z",
      "artifactVersion": "1.0.0.2",
      "componentCount": 118
    }
  }
}
  • isActive:プロジェクトに構成済みのパイプラインがある場合(docs/alm/last-pipeline.json が存在する場合)は常に true です。Pipelines タブの ACTIVE チップを表示するために使用します。
  • pipelineName:docs/alm/last-pipeline.json の値です。pipelineMeta がない場合、レンダラーは ${SITE_NAME}-Pipeline にフォールバックします。
  • reusedByWiring:パイプラインを新規作成した場合は null です。create-deployment-pipeline.js がソースとターゲットの接続設定で既存のパイプラインを照合し、既存の名前のまま再利用した場合は、オブジェクト { originalName, requestedName } になります。レンダラーは説明を注記として表示するため、レビュー担当者はプラン上の名前と実際のパイプライン名が異なる理由を理解できます。
  • lastDeploy:docs/alm/last-deploy.json から取得します。初回デプロイ前は省略(null に設定)します。

データの設定方法。 Phase 3 の planData 構築時に、両方のファイルを読み取り(インラインの Node.js)、次の内容を挿入します。

node -e "
const fs = require('fs');
const meta = { isActive: false, pipelineId: null, pipelineName: null, reusedByWiring: null, lastDeploy: null };
try {
  const lp = JSON.parse(fs.readFileSync('docs/alm/last-pipeline.json','utf8'));
  meta.isActive = true;
  meta.pipelineId = lp.pipelineId || null;
  meta.pipelineName = lp.pipelineName || null;
  meta.reusedByWiring = lp.reusedByWiring || null;
} catch {}
try {
  const ld = JSON.parse(fs.readFileSync('docs/alm/last-deploy.json','utf8'));
  meta.lastDeploy = {
    status: ld.status, stageName: ld.stageName, deployedAt: ld.deployedAt,
    artifactVersion: ld.artifactVersion, componentCount: ld.componentCount,
  };
} catch {}
process.stdout.write(JSON.stringify(meta));
"

結果を planData.pipelineMeta として埋め込みます。

v2 フィールド(sizeAnalysis、assetAdvisory、splitStrategy、appliedStrategies、compositeSubPartitioned、proposedSolutions、recommendations、breakdown)は、Phase 1 Step 10 で計算され、Q1b のユーザー選択によって変更された SPLIT_PLAN から直接取得します。変更せずにレンダラーへ渡します。envVars は Phase 1 Step 10b で設定された ENV_VARS_DETAILS から取得し、変更せずに渡します。配列が空の場合、レンダラーのカウント対応フォールバックでは sizeAnalysis.envVarCount.value を使用するため、Env Variables タブには引き続きカウントの概要注記が表示されます。

ディスク測定値の照合(webFilesDiskMeasuredMB、webFilesDiskMeasuredPath、webFileSampleSize)— 推定器の出力(rawDiscovery.estimate.webFilesDiskMeasuredMB など)から取得し、planData のトップレベルに移します。これらの値が設定されるのは、estimate-solution-size.js に --projectRoot "." が渡され、かつビルド出力ディレクトリが検出された場合のみです。2 つの数値に実質的な差がある場合、レンダラーは Dataverse で測定された Web Files のシグナルの横に webFilesDiskMeasuredMB を表示します(truncationSuspected を切り替えるのと同じ条件です)。これにより、レビュー担当者はどちらの数値を信頼すべきかをすぐに確認できます。推定器がディスクを測定していない場合は、3 つすべてに null を渡します。 切り詰め検出フラグ(truncationSuspected、truncationWarnings)— SPLIT_PLAN.truncationSuspected/SPLIT_PLAN.truncationWarnings からそのまま引き継ぎます(compute-split-plan.js 自体は推定結果からこれらを読み取ります)。truncationSuspected === true の場合、描画された計画ではサイズ分析の上に赤いバナーが表示され、Phase 2 Q1b の「とにかく単一のままにする」上書きには追加の確認手順が必要になります(Phase 2 Q1b を参照)。一般的な原因は、estimate-solution-size.js における Dataverse のページネーションの不具合、または Web ファイルの過少カウント($select=content 経由ではバイト数が返されないファイル型列)です。ディスクとの照合を有効にしている場合、後者は明示的に検出されます。

rawDiscovery ブロック — フィールドのマッピングを行う前に、推定、分割計画、ホスト解決の各出力を完全な形でそのまま埋め込みます。これは診断用のエンベロープです。通常、レンダラーは表示パスからその内容を参照してはいけません。限定的な例外:レンダラーの webFilesDiskMeasured*/webFileSampleSize 読み取りパスは、トップレベルへの引き上げがない場合、rawDiscovery.estimate.* にフォールバックします。そのため、引き上げが仕様に定められる前に作成された古い計画ファイルでも、ディスク比較の注記が表示されます。このフォールバックが使われている場合の適切な対処は、引き上げを設定して planData を再構築することであり、フォールバックパスを有効なままにすることではありません。それ以外は、原則は変わりません。rawDiscovery は、レビュー担当者(または誤った計画をデバッグする将来のエージェント)が docs/.alm-plan-data.json を読み、レンダラーが使ったマッピング済みフィールドと並べて、検出スクリプトが実際に生成した内容を確認できるようにするためのものです。各ソースを一度だけ読み込んで割り当てます。

planData.rawDiscovery = {
  estimate: readJson('./docs/alm/alm-size-estimate.json'),
  splitPlan: readJson('./docs/alm/alm-split-plan.json'),
  hostResolution: PIPELINE_DONE
    ? null                                       // host info comes from .last-pipeline.json in that path
    : readJson('./docs/alm/alm-host-resolution.json'),  // PP path with detection; null for Manual path
};

スキップされたソースには null を使用します(Manual パスではホスト解決は実行されず、パイプライン導入前のプロジェクトにはマニフェストがありません)。値を伏せたり要約したりしないでください。スナップショットの価値は、生の入力を保持することにあります。

proposedSolutions[] が空になることはありません。 splitStrategy === "single" で、意思決定ツリーがソリューションを 1 つにするよう推奨する場合でも、compute-split-plan.js はベースソリューションを記述するエントリを 1 つ返します(uniqueName、displayName、sizeMB、componentCount、componentTypes)。これをそのまま引き継いでください。レンダラーはこの配列を使って Solutions タブを表示します。空のままにすると、「構造は決定されます」という役に立たないプレースホルダーが表示されます。compute-split-plan が実行されていないために proposedSolutions = [] となっている場合は、空の配列を引き継ぐのではなく、solutionContents.solution/data.SITE_NAME/componentCount/totalSizeMB からベースエントリを 1 つ合成してください。このケースに備えてレンダラーにはセーフティーネットとしての合成処理がありますが、適切な修正は上流で明示的に値を設定することです。

hostResolution ブロック(PP Pipelines パスのみ。Manual パスでは省略)。HOST_RESOLUTION(Phase 1 のステップ 12)と、Phase 2 Q4 で設定される補助フラグから構築します。

  • status ← HOST_RESOLUTION.status
  • hostEnvUrl ← HOST_ENV_URL(Q4 から)— ユーザーが後回しにするか、新規プロビジョニングを選択した場合は null になることがあります
  • hostEnvId ← HOST_RESOLUTION.finalHostEnvId
  • hostType ← HOST_RESOLUTION.hostType
  • pipelinesSolutionVersion ← HOST_RESOLUTION.pipelinesSolutionVersion
  • candidatesCount ← HOST_RESOLUTION.candidates.existingCustomHosts.length
  • willEnsureDuringExecution ← Q4 の WILL_ENSURE_HOST フラグ(setup-pipeline が実行時に ensure-pipelines-host を参照する必要がある場合に常に true です。つまり、ステータスが NoHost、いずれかの *Unbound*、またはユーザーが後回しにした場合です)
  • willProvisionPlatform ← Q4 の WILL_PROVISION_PLATFORM フラグ(ユーザーが NoHost のホストタイプメニューでオプション 1「プラットフォームホストをプロビジョニング」を選択した場合に設定されます。ensure-pipelines-host はこれを Phase 4.0 に直接進む指示として扱います)
  • willProvisionCustom ← Q4 の WILL_PROVISION_CUSTOM フラグ
  • willUsePpac ← Q4 の WILL_USE_PPAC フラグ(ユーザーが NoHost のホストタイプメニューで「PPAC を開いて手動で作成」を選択した場合に設定されます。ensure-pipelines-host はこれを Phase 4.C に直接進む指示として扱います)
  • chosenEnvUrl ← Q4 の CHOSEN_ENV_URL フラグ(ユーザーが NoHost のホストタイプメニューでオプション 2 → サブオプション a を選択した場合に設定されます。ensure-pipelines-host はこれを Phase 3.C のメニューをスキップして、この環境とともに Phase 4.B に直接進む指示として扱います)
  • userChoseDeferToSetupPipeline ← Q4 の USER_CHOSE_DEFER_TO_SETUP_PIPELINE フラグ(MultipleUnboundCustomHosts の「後で決める」分岐でのみ設定されます) 収集したデータに基づいて risks を設定します。
  • HAS_ENV_VARS = true の場合:{ type: "warning", message: "このソリューションには環境変数があります({N} 件検出)— デプロイ中にステージごとの値を求められます。" }。{N} は、ENV_VARS_DETAILS.length が 0 より大きい場合はその値、そうでなければ SPLIT_PLAN.sizeAnalysis.envVarCount.value(サイズ推定ツールが報告した件数)に置き換えます。どちらの値も正の件数でない場合は、括弧内の記述を省略します("このソリューションには環境変数があります — デプロイ中にステージごとの値を求められます。")。
  • SITE_SETTINGS_DATA.promoteToEnvVar.length > 0 の場合:{ type: "info", message: "{N} 件の認証関連サイト設定(Authentication/* および AzureAD/*)で値が検出されました。setup-solution は、各ステージで異なる値(例:異なる OAuth コールバック URL)を使用できるよう、どの設定を環境変数で管理するかを確認します。ステージごとに値を変える必要がない設定はスキップしてください。" }。{N} は SITE_SETTINGS_DATA.promoteToEnvVar.length に置き換えます。(以前の「will be promoted」という表現を置き換えます。自動的に処理されるという誤解を招くためです。実際には、設定ごとにユーザーが選択します。)
  • SITE_SETTINGS_DATA.credentialNeedsDecision.length > 0 の場合:{ type: "info", message: "{N} 件の認証情報形式のサイト設定(ConsumerKey / ClientId / ClientSecret / など)が検出されました。setup-solution は、これらすべてをまとめて処理するプロンプトを 1 回表示します。名前に基づく自動分類(推奨。*Secret/Password/ApiKey/AppKey は Secret 環境変数に、*Id/ConsumerKey は String 環境変数になります)、すべてを Secret にする、すべてを String にする、すべてスキップする、または個別の認証情報を選択して細かく制御する方法を選べます。" }。{N} は SITE_SETTINGS_DATA.credentialNeedsDecision.length に置き換えます。「ソリューションから除外/手動で設定」といった記述は出力しないでください。これは IronItOut 前の動作であり、現在は廃止されています。
  • EXPORT_TYPE = "unmanaged" で、戦略に本番ターゲットが含まれる場合:{ type: "warning", message: "アンマネージドソリューションはターゲット環境で編集できます。本番環境では Managed の使用を検討してください。" }
  • SOLUTION_DONE = false の場合:{ type: "info", message: "まず Dataverse ソリューションが作成されます。発行元プレフィックスは選択後に変更できません。" }
  • 必ず(PP Pipelines パスを計画しており、Phase 4 後に SOLUTION_DONE が true になる場合):{ type: "info", message: "後からコンポーネント(サーバーロジック、クラウドフロー、環境変数、カスタムテーブル)を追加したときは、同期モードで /power-pages:setup-solution を再実行して、このソリューションに取り込んでください。このスキルの完全性チェック(Phase 1 Step 11)では、次回の plan-alm 実行前に、稼働中のサイトとソリューションの差異を検出します。" }。plan-alm の開始時点で SOLUTION_DONE がすでに true の場合は省略します(その時点では同期モードであることが明らかなためです)。
  • KNOWN_GAPS が設定されている場合(Phase 1 Step 11 の事前計画完全性チェックで不足が見つかり、ユーザーが続行を選択した場合):{ type: "warning", message: "{X} 件のサイトコンポーネント、{Y} 件のクラウドフロー、{Z} 件の環境変数、{W} 件のカスタムテーブルがサイト上に存在しますが、現在のソリューションには含まれていません。この計画ではこれらを昇格しません。デプロイ前に /power-pages:setup-solution の同期モードを実行するか、同期後に plan-alm を再実行してください。" }。{X}、{Y}、{Z}、{W} は KNOWN_GAPS.missing.*.length の件数に置き換えます。
  • HOST_RESOLUTION.status === "NoHost" かつ WILL_PROVISION_PLATFORM === true の場合:{ type: "info", message: "Pipelines ホストが見つかりません。setup-pipeline が新しい Platform Host をプロビジョニングします(冪等、約 3~5 分)。呼び出し前にテナント ID 確認ゲートを通過するまで、プランの実行は一時停止します。" }。管理者ロールの要件や API 名に関する記述は含めないでください。これらは実装の詳細です。
  • HOST_RESOLUTION.status === "NoHost" かつ WILL_PROVISION_CUSTOM === true の場合:{ type: "info", message: "Pipelines ホストが見つかりません。setup-pipeline が新しい Custom Host を作成します。プランの実行は、管理者ロールの確認と呼び出し前の確認が完了するまで一時停止します。" }
  • HOST_RESOLUTION.status === "NoHost" かつプロビジョニングフラグが設定されていない場合(ユーザーが Option 2 → sub-option a で既存の環境を選択した、PPAC の手動操作を選んだ、または保留にした場合):CHOSEN_ENV_URL / WILL_USE_PPAC からステータスに応じた情報エントリーを出力します。
  • HOST_RESOLUTION.status === "AvailableUnboundCustomHost" の場合:{ type: "info", message: "既存の Custom Host (" + HOST_RESOLUTION.finalHostEnvUrl + ") を再利用します。ソース環境は自動的にバインドされます。" }
  • HOST_RESOLUTION.status === "MultipleUnboundCustomHosts" の場合:{ type: "info", message: "テナント内に既存の Custom Host が " + HOST_RESOLUTION.candidates.existingCustomHosts.length + " 件見つかりました。setup-pipeline で選択を求めます。" }
  • HOST_RESOLUTION.status === "PlatformHostExistsUnbound" の場合:{ type: "info", message: "テナントには Platform Host があります。再利用が最も手間の少ない方法です。代わりに Custom Host を作成すると、別テナントやガバナンスが必要なシナリオで、より適切な管理が可能になります。" }
  • HOST_RESOLUTION.status === "CannotRedirect" の場合:{ type: "warning", message: "CannotRedirect:ソース環境の ProjectHostEnvironmentId が PE を指していますが、テナントの既定のカスタムホストは別の場所に設定されています。解決には Power Platform 管理者が必要です。" }(注:Phase 2 Q4 では通常、この状態でのプラン生成をブロックします。これは、何らかの理由でプランが生成された場合に備えた防御的なエントリーです。)
  • 手動パス(STRATEGY = "manual" の場合は常に):{ type: "info", message: "推奨される手順:/power-pages:export-solution を実行し、生成された zip を確認してから、各ターゲットに対して /power-pages:import-solution を実行します。plan-alm 自体はエクスポート/インポートを実行しません。" }
  • 未実行の検出処理による不足(#9) — rawDiscovery.estimate、rawDiscovery.splitPlan、および(PP パスの場合)rawDiscovery.hostResolution のうち、planData の構築時に null である項目ごとに:{ type: "warning", message: "{X} の検出処理が実行されていません。このプランの関連するサイズ/分割/ホストの判断は未検証です。" }({X} は「ソリューションサイズの推定」/「分割分析」/「パイプラインホストの解決」に置き換えます)。例外:rawDiscovery.hostResolution:PIPELINE_DONE = true の場合、ホスト解決は意図的にスキップされています(Phase 1 Step 12 のスキップルール。ホストは新たに検査するのではなく、docs/alm/last-pipeline.json から取得します)。そのため、hostResolution が null でも問題はなく、不足には該当しません。PIPELINE_DONE = true の場合、「パイプラインホストの解決」警告を出力しないでください。パイプラインがすでに存在するすべてのプロジェクトで「ホスト解決が実行されていません」という誤った警告が表示されてしまいます。estimate と splitPlan の不足警告は、PIPELINE_DONE の値にかかわらず適用されます。 計画の完全性チェック(#10)。 planData を書き込む前に、計画が実際の調査に基づいていることを確認します。
  • sizeAnalysis.totalSizeMB が null ではないこと
  • stages[] が空ではないこと
  • ソリューションが設定されている場合(SOLUTION_DONE = true)、solutionContents が入力されていること

いずれかのチェックに失敗した場合は、PLAN_QUALITY = "degraded" に設定し、失敗したチェックを記録します。(すでに degraded に設定される Phase 1 Step 6 の認証失敗時の処理に加えて実施します。)

計画の品質低下によるリスク(#8) — PLAN_QUALITY === "degraded" の場合(認証失敗時の処理または完全性チェックが原因)、risks の先頭に次の内容を追加します。{ type: "warning", message: "⚠ PLAN QUALITY: DEGRADED — discovery was incomplete ({cause}). Review carefully before executing; re-run /power-pages:plan-alm after fixing the cause (commonly dev-environment auth) to regenerate a complete plan." }。type: "warning" を使用してください(レンダラーがこのスタイルを適用します)。{cause} は、記録された理由に置き換えます。

planData を docs/.alm-plan-data.json に書き込みます(存在しない場合は docs/ を作成します)。

HTML 計画をレンダリングする
node "${PLUGIN_ROOT}/skills/plan-alm/scripts/render-alm-plan.js" \
  --output "<projectRoot>/docs/alm-plan.html" \
  --data "<projectRoot>/docs/.alm-plan-data.json"

docs/.alm-plan-data.json はディスク上に保持し、削除しないでください。 plan-alm の終了後、2 つの利用元がこのファイルに依存します。(1)実行スキルの最終フェーズでの更新(refresh-alm-plan-data.js)は、このファイルを読み込んで hostResolution、pipelineMeta、validationRuns、risks、steps[] のステータス、およびフッターを各スキルの実行に応じて更新し、docs/alm-plan.html を再レンダリングします。これにより、レンダリングされたタブには事前実行時の計画ではなく、実際の実行状態が反映されます。(2)check-alm-plan.js は、setup-pipeline / deploy-pipeline / setup-solution / export-solution / import-solution / configure-env-variables の Phase 0 ALM 計画ゲートでこのファイルを読み込みます。このファイルを削除すると、後続のすべてのスキルが計画は存在しないと判断し、計画なしのゲートが作動します。初回のレンダリング後にこのファイルを削除するよう以前案内していたのは誤りであり、計画が有効な間、Pipelines タブとリスク一覧が実行前の状態で固定される原因になっていました。

docs/alm/alm-plan-context.json に書き込みます(setup-solution が読み取れるよう保持します)。

{
  "generatedAt": "{ISO timestamp}",
  "siteName": "{siteName}",
  "siteSettings": {
    "keepAsIs": [{name}],
    "authNoValue": [{name}],
    "promoteToEnvVar": [{name, value}],
    "excluded": [{name}]
  }
}

このファイルは意図的に削除しません。setup-solution やその他のスキルがこれを読み込み、再調査を省略します。

HTML プランをユーザーの既定のブラウザーで開く

Phase 4 で表示されるインライン Markdown の概要は、意図的に簡潔にしています。レビュー担当者が十分な情報に基づいて承認できるよう、完全なレンダリング済みプラン(サイズゲージ、シグナルカード、ソリューションごとの内訳、アセットに関する助言、パイプラインのステージ)を確認する必要があります。CLI の概要を読んでいる間にユーザーがプラン全体を確認できるよう、承認を求める前に docs/alm-plan.html を既定のブラウザーで開いてください。

Node ラッパーを使わない理由。 以前の node -e "spawn('powershell.exe', [...])" の連鎖は、エージェントのサンドボックス分類器に検知されます(Node がプロセスを起動し、そのプロセスがさらに別のプロセスを起動するのは、分類器がブロックする典型的なパターンです)。エージェントは OS ネイティブのシェルツールを直接使ってください。Node を介さず、child_process も、デタッチされたサブプロセスも使用しないでください。

Step 1. 絶対 file:// URL を目立つように 最初に 表示します。 これは、起動がブロックされた場合にユーザーが確実に使える代替手段です。

node -e "process.stdout.write('Plan URL: file:///' + require('path').resolve('docs/alm-plan.html').replace(/\\\\/g, '/') + '\n')"

(Node の呼び出しは 1 回のみで、spawn はなく、ブロックされません。出力例:Plan URL: file:///C:/Projects/.../docs/alm-plan.html。)

Step 2. OS ネイティブのシェルを使ってブラウザーを起動します。 エージェントが利用できるシェルツールを選んでください。

  • Windows/PowerShell ツール — Start-Process を直接呼び出します。

    Start-Process "docs/alm-plan.html"
    
  • Windows/Bash ツール(Git Bash、WSL パススルー) — Node ラッパーを使わず、Bash から PowerShell を直接 1 回呼び出します。

    powershell.exe -NoProfile -Command "Start-Process 'docs/alm-plan.html'"
    
  • macOS — open を直接呼び出します。

    open docs/alm-plan.html
    
  • Linux — xdg-open を直接呼び出します。

    xdg-open docs/alm-plan.html
    

Step 3. URL をユーザーに伝えます。 起動を試みた後、Step 1 でエージェントが表示した file:// URL を提示し、クリックできる代替リンクを常にユーザーが使えるようにします。

「ブラウザーで docs/alm-plan.html を開きました。自動的に開かなかった場合は、このリンクを使ってください:file:///C:/Projects/.../docs/alm-plan.html。内容を確認してから、下の承認プロンプトに回答してください。」

起動に失敗しても、エラーが表示されない場合(サンドボックス化されたターミナル、SSH セッション、ヘッドレス環境など)は、再試行したり、処理を止めたり、繰り返したりしないでください。Step 1 で表示した URL があれば十分です。ユーザー自身がクリックまたは貼り付けて開けます。Phase 4 に進み、代替手段として CLI の概要を使用してください。

タスク 1 を completed にします。


Phase 4 — プランの提示、承認、保存

タスク 2(「ALM プランの承認と保存」)を in_progress にします。

plan-alm はここで終了します。 このフェーズでは、プラン(承認済みまたはドラフト)を保存して終了します。setup-solution、setup-pipeline、deploy-pipeline、export-solution、import-solution は実行しません。ユーザーはその後、これらを実行します。

簡潔なインライン Markdown の概要を提示します。

## ALM Plan: {siteName}

**Strategy:** {PP Pipelines / Manual export/import}
**Stages:** {Dev} → {Staging} → {Production (if applicable)}
**Approval gates:** {description from PP_APPROVAL_MODE, or "N/A — manual path"}
**Solution export:** {Managed / Unmanaged}
**Pipeline host:** {hostEnvUrl} ({status}) — *(PP Pipelines path only; when `WILL_ENSURE_HOST = true`, render as `Will be ensured during setup-pipeline ({status})` instead)*
**Plan quality:** {complete / ⚠ DEGRADED — <cause>}

**Decisions defaulted (please review):**
{For each DECISIONS_LOG entry, one line: "- {field}: {value} ({default} or {your pick})". Tag source:"default" as (default) and source:"explicit" as (your pick). If every decision was explicit, write "- (none — every choice was an explicit pick)".}

**Recommended execution sequence (you run these after approval):**
- [ ] /power-pages:setup-solution {(SKIP — already set up) if SOLUTION_DONE}
- [ ] /power-pages:setup-pipeline {(SKIP — already set up) if PIPELINE_DONE} {(PP path only)}
- [ ] /power-pages:export-solution {(manual path only)}
- [ ] /power-pages:deploy-pipeline — per stage {(PP path only)}
- [ ] /power-pages:import-solution — per target {(manual path only)}

Full plan written to: docs/alm-plan.html

🚦 ゲート(plan · plan-alm:4.approve): 最終段階です。ユーザーがレンダリング済みの HTML プランを保存します。plan-alm は実行されず、このゲートでは、実行スキルをユーザーが実行できる Approved として保存するか、Draft として保存するか、またはユーザーを修正に戻すかだけを選択します。ここでは後続のスキルはディスパッチされません。

AskUserQuestion で次のように尋ねます。

「この ALM プランをどのように保存しますか?」

選択肢:

  1. プランを承認済みとして保存し、実行可能にする(Approved として保存します。次に実行スキルを実行します)
  2. プランをドラフトとして保存する(Draft として保存します。後で承認するには plan-alm を再実行してください)
  3. 変更したい項目がある — 質問に戻る
  • 選択肢 3 の場合: Phase 2 を再実行します(変更するセクションを尋ねてから、その回答を再収集します)。プランを再生成し(Phase 3 を繰り返し)、承認のために再提示します。

  • 選択肢 1(承認済み)の場合: 承認者を記録してから(下記参照)、決定的なヘルパーを使ってステータスと承認者をアトミックに書き込みます。HTML の span 要素を手動で編集しないでください。 バッジ、approved-by、approval-date はすべてレンダリング時に docs/.alm-plan-data.json から派生するため、span を手動で編集しても保持されません(次回の実行スキルによる更新時に再生成されます)。ヘルパーはプランデータ(PLAN_STATUS、PLAN_MODE、APPROVED_BY、APPROVAL_DATE の 4 つすべて)をまとめて更新し、docs/alm-plan.html を再レンダリングします。

    node "${PLUGIN_ROOT}/scripts/lib/set-plan-status.js" --projectRoot "." --status Approved --approver "{APPROVER}" --render
    

    次に、以下の finalize ステップ(スキルトラッキング + コミット)を実行し、next-steps ガイダンスを出力して、タスク 2 を completed にマークし、終了します。

  • オプション 2(下書き)の場合: 承認者を取得しないでください。同じヘルパーを使用して下書きとして保存します(plan-data を書き込み、再レンダリングします。古い承認者フィールドをクリアするため、プランが「下書き + 承認者」の状態になることはありません):

    node "${PLUGIN_ROOT}/scripts/lib/set-plan-status.js" --projectRoot "." --status Draft --render
    

    以下の finalize ステップを実行し(コミットのみ — スキルトラッキングはスキップするか実行するかは任意です。コミットメッセージは "Add ALM plan for {siteName} (draft)")、承認する準備ができたら /power-pages:plan-alm を再実行するようユーザーに伝え、タスク 2 を completed にマークし、終了します。

承認者の取得(オプション 1 のみ) — 常に対話形式(#1):

名前を黙って自動適用しないでください。まず git / OS ユーザーからプリフィル候補を計算します:

node -e "const {execSync}=require('child_process');let n='';try{n=execSync('git config user.name',{encoding:'utf8'}).trim();}catch{};if(!n){n=process.env.USER||process.env.USERNAME||'';}process.stdout.write(n);"

次に、常に AskUserQuestion を介して確認します(候補が空でない場合でも)。これにより、監査証跡に記録される承認者を人間が能動的に確認できます:

「このプランを承認するのは誰ですか?(docs/alm-plan.html の監査証跡に記録されます)」

選択肢: 1. {git/OS からの候補名がある場合} · 2. その他(名前を入力)

コマンドが空文字列を返した場合は、オプション 2(自由入力)のみを提示します。確認された結果を APPROVER として保存し、set-plan-status.js(上記のオプション 1)に渡します — approved-by / approval-date スパンを手動で編集しないでください。ヘルパーは APPROVED_BY + APPROVAL_DATE(現在の ISO タイムスタンプ)を docs/.alm-plan-data.json に書き込み、再レンダリングします。テンプレートは plan-data から両方のスパンを埋めます。これにより、監査証跡とステータスが同期した状態に保たれます — 「承認者は記録されたがステータスは依然として下書き」という中途半端な状態(validate-plan-alm.js が現在ブロックしています)は発生しません。

Finalize(両方の保存オプション):

  1. スキルトラッキング(オプション 1、下書きの場合は任意):

    参照: ${PLUGIN_ROOT}/references/skill-tracking-reference.md

    node "${PLUGIN_ROOT}/scripts/update-skill-tracking.js" \
      --projectRoot "." --skillName "PlanAlm" --authoringTool "ClaudeCode"
    
  2. プランをコミット — ユーザーが選択した保存オプションに応じてコミットメッセージを選びます:
    • オプション 1(承認済み):
      git add docs/alm-plan.html && git commit -m "Add ALM plan for {siteName}"
      
    • オプション 2(下書き):
      git add docs/alm-plan.html && git commit -m "Add ALM plan for {siteName} (draft)"
      
    docs/.alm-plan-data.json はディスク上に残ります — これは下流の各スキルのフェーズ 0 ゲートのために check-alm-plan.js によって読み取られ、それらのスキルの実行に応じて更新されます。決して削除しないでください。

Next-steps ガイダンス(オプション 1 — ユーザーに出力し、呼び出さないでください):

「プランが承認され、保存されました。plan-alm はデプロイを行いません — 次を順番に実行してください。各ステップはこのプランを検出して再確認なしで進み、完了時にプランを更新します:

  • PP Pipelines パス: /power-pages:setup-solution → /power-pages:setup-pipeline → /power-pages:deploy-pipeline(ターゲットステージごとに 1 回。デプロイフローがサイトを自動的にアクティベートします — ただしアクティベーションを先送りする場合は /power-pages:activate-site を実行してください)→ 各ステージを検証するには /power-pages:test-site。
  • 手動パス: /power-pages:setup-solution → /power-pages:export-solution → zip を確認 → /power-pages:import-solution(ターゲットごとに 1 回)。 already set up とマークされたステップはスキップしてください。プランは docs/alm-plan.html でいつでも再度開くことができます。」

進捗トラッキングテーブル

タスクの主題activeForm説明
ALM プランを生成ALM プランを生成中戦略の入力情報を収集し、planData を構築し、docs/alm-plan.html をレンダリングする
ALM プランを承認して保存プランの承認待ちインライン要約(デフォルト決定 + プラン品質を含む)を提示し、承認者を取得し、承認済みまたは下書きとして保存し、コミットし、next-steps を出力する

plan-alm にはまさにこの 2 つのタスクがあります。セットアップ / パイプライン / デプロイ / エクスポート / インポート / アクティベーション / テストは、ユーザーが承認後に実行する個々の ALM スキルによって実行されます — これらはプランの steps[](推奨シーケンス)に表示されますが、plan-alm のタスクになることはありません。


重要な判断ポイント(ユーザーの応答を待機)

  1. フェーズ 1:.alm-deferred マーカーの処理。事前のプラン完全性チェック(解決策が存在する場合)
  2. フェーズ 2、Q1:ソリューションのセットアップ — 既存のものを確認するか、プランに setup-solution を含めます
  3. フェーズ 2、Q1b:分割の推奨と上書きの確認(推奨された場合)
  4. フェーズ 2、Q2:昇格戦略 — PP Pipelines、手動、またはセットアップ済み
  5. フェーズ 2、Q3〜Q5(PP の場合):ステージ数、ホスト環境、承認ゲート(マネージド環境では自動設定) フェーズ 2、Q3〜Q5(手動の場合):ターゲット数、ターゲット環境の URL、エクスポートの種類
  6. フェーズ 4:プランを保存 — 承認、ドラフト、または修正。これが唯一の「承認」です。以降、実行はありません。

エラー処理

  • .powerpages-site/website.yml と powerpages.config.json のどちらもない場合:停止し、/power-pages:create-site を案内します(フェーズ 1 ではどちらのマーカーからでもサイト ID を解決します。データモデル/EDM サイトには website.yml だけがあるため、設定ファイルがないだけで処理を中断しないでください)
  • pac env list が失敗した場合:ENV_LIST の事前入力をスキップし、環境 URL を手動で尋ねます
  • render-alm-plan.js が失敗した場合(終了コードが 0 以外):エラーを報告し、代替として planData JSON を表示して、続行するかユーザーに尋ねます
  • 検出/認証に失敗した場合:PLAN_QUALITY = "degraded" を設定し、目立つ形でリスクを示したうえで、プランの作成を続行します(ユーザーが認証を修正して再実行し、再生成します)
  • プランの承認でオプション 3(何かを変更する)が選択された場合:フェーズ 2 を完全に再実行してからプランを再生成します。古い回答を引き継がないでください

コメント