# caldova-demo-setup

A Microsoft Scout / Copilot CLI skill for standing up a **CDX Caldova demo tenant** end to end.

Built from a real Aug 2026 build, including the parts that didn't work.

## What it covers

0. **Claiming the Caldova tenant** on CDX, then requesting the NET NEW Azure subscription — a
   strictly sequential pair, plus the tenant-slot cap that silently blocks a claim
0.5. **Profiling the operator** — asking what industry, territory and accounts *you* cover, so the
   reskin is yours and not the author's
1. Creating the Dataverse environment **with Dynamics 365 apps enabled** (an irreversible flag
   that is easy to miss)
2. Installing the CE app portfolio — realistic timings, which apps can't co-exist
3. **Connections and connection references** — the undocumented blocker that silently stops
   every first-party agent
4. Configuring the Customer Service agents (Case Management, Customer Intent, Knowledge)
5. Converting to Production to drop the **SANDBOX** badge
6. Custom branding via modern theming — including the master switch that isn't documented
7. Reskinning the stock coffee-equipment demo data to your own industry
8. Installing **Customer Insights (Journeys + Data)** and generating a CDP dataset with
   built-in identity-resolution difficulty
9. Adding **Finance & Operations** apps in their own environment — and, when the template deploys
   empty, **building the whole financial foundation from scratch over OData**: legal entities,
   fiscal calendar, chart of accounts, ledger setup and posting profiles
10. Standing up **first-party agents** and **predictive lead/opportunity scoring** — including
    seeding the qualified/disqualified history the models need to train on
11. **CI – Data measures, segments, segment suggestions, segment insights and prediction models**
    (customer churn, CLV) — plus the semantic activity mapping that quietly gates all of them

## ⚠️ Answer questions once, at the start — then walk away

The playbook was written from one seller's build (Retail & Consumer Goods, US East). Every
industry example in it — the CPG brand families, the grocery/convenience archetypes, the CDP
product list — is a **worked example, not a default**.

So **Phase 0.5 is a single intake** that collects every decision the whole build needs, before any
long-running work starts: your industry, territory, accounts and audience; which workloads to
build; how deep the demo data should be; whether to authorise the Sales Premium trial; whether
journeys send real email; what to do if a deploy comes back empty; and your admin UPN with all
sign-ins batched into one sitting.

That design is deliberate. A full build contains **four multi-hour waits** — app installs, CI – Data
(~14 h), F&O (~3 h 41 m), Sales Premium (~1 h). If you are asked a question each time one is
reached, you are tied to the session all day. Answer up front and the build runs unattended,
interrupting you only for a password prompt, an unauthorised destructive action, or a true dead
end. See `reference/00-operator-profile.md`.

It will also recommend **fictional company names**, and it is worth knowing why: a demo tenant
carrying your real account names shows Customer B's pipeline to Customer A the moment you demo to
them.

## Starting from nothing

No tenant yet? `reference/00-claim-tenant.md` covers the claim end to end.

⚠️ **The two halves of Phase 0 are strictly sequential.** Claim the tenant first; the Azure
subscription request needs an existing tenant to attach to. You cannot fire them off in parallel.

- **Check `Roles & Limits` first.** Caldova consumes a **1-year tenant slot** and CDX caps you at
  3. A full `3 of 3` blocks the claim, and expired tenants keep occupying slots until deleted.
- **Claim it** by clicking **Start demo** on the *Caldova Live Demo Environment* card at
  <https://cdx.transform.microsoft.com/caldova>. It is a JS button — you cannot deep-link it.
- **⏱️ Claiming takes several minutes — but the launcher is not a progress bar.** It can keep
  spinning for **15–30 minutes after the tenant has already been created**. Verify in
  **`My Dashboard` → Overview**; if the tenant shows `Completed`, it's ready — close the launcher.
  **Do not re-click Start demo**, or you may burn a second tenant slot.
- **Never change a tenant password.** CDX states in capitals that they must remain as issued or the
  add-on experiences break. Easy to trip over if you habitually harden new tenants.
- **Sign in as the Caldova admin, not yourself.** Get the admin UPN
  (`admin@caldova<NNNNNNNN>.onmicrosoft.com`) from the Credentials panel, then `pac auth create`,
  browser, and later `az login --tenant` all as that account. Wrong-identity failures are silent —
  empty environment lists, empty portals, uploads landing in the wrong OneDrive.
- **Then** request the NET NEW Azure subscription via <https://aka.ms/MCAPSBYOT> — once the tenant
  shows `Completed`. It must be net new, it lands in the *Caldova* tenant rather than your
  corporate one, and it carries real procurement lead time.

The content pack is named `Caldova-BlankTenant-V1`, which tells you what you are getting: full
licensing, zero installed apps. Everything from phase 1 onward exists to fix that.

## Install

**Microsoft Scout**

```powershell
Expand-Archive caldova-demo-setup.zip -DestinationPath "$env:USERPROFILE\.scout\m-skills\"
```

**GitHub Copilot CLI**

```powershell
Expand-Archive caldova-demo-setup.zip -DestinationPath "$env:USERPROFILE\.copilot\skills\"
```

Restart the client, then invoke with `/caldova-demo-setup` or just describe the task
("help me stand up my Caldova tenant").

## Contents

```
caldova-demo-setup/
├── SKILL.md                              Main playbook — phase order, verification rules
├── reference/
│   ├── 00-claim-tenant.md                Phase 0 — claim the CDX experience + Azure subscription
│   ├── 00-operator-profile.md            Phase 0.5 — single intake: 13 decisions, batched sign-ins, unattended rules
│   ├── 01-environment-provisioning.md    Env creation, solo-only apps, install timings
│   ├── 02-connections-and-agents.md      The connection blocker, agent config, Entra app user
│   ├── 03-branding-and-polish.md         Production conversion, modern theming
│   ├── 04-demo-data-reskin.md            Reskin methodology + the fields everyone forgets
│   ├── 05-troubleshooting.md             Every failure hit, with diagnosis
│   ├── 06-customer-insights.md           CI Journeys + Data, the Azure constraint, CDP data
│   ├── 07-finance-and-operations.md      F&O: licence checks, Sandbox-only demo data, wizard traps
│   ├── 08-agents-and-sales-insights.md   First-party agents + predictive scoring + seeding a corpus
│   └── 09-reference-build-state.md       Snapshot of a real build: what's done, what's outstanding
└── scripts/
    ├── bind-connection-references.js     Bind all connection refs in one pass
    ├── verify-environment.js             Health check across apps/refs/agents/data/theming
    ├── reskin-demo-data.js               Bulk demo-data rewrite template
    └── gen-ci-data.py                    Generate an 11-source CPG CDP dataset (deterministic)
```

## The five things most worth knowing

**1. Progress indicators lie — at every layer.** Five `pac` commands returned confidently wrong
answers (`application list`, `solution list`, `copilot list`, and `admin list` twice — wrong
environment type, and **only 1 of 3 environments returned**). Three UI surfaces did the same: the
**CDX tenant launcher spins 15–30 min after the tenant already exists**, the CI provisioning portal
reports `Installing` after the app module has landed, and PPAC direct URLs 404 on pages that are
perfectly reachable via the tab strip. Use tooling to *do* things; verify at the management
surface — My Dashboard, the org's apps page, PPAC's environment hub, or `Xrm.WebApi`.
**A spinner is not evidence.**

**2. A fresh environment has zero connections.** Every connection reference is unbound, which
silently blocks every first-party agent. Their admin pages just spin on "Loading" with no error.
This is phase 3 and it is the highest-value thing in the skill.

**3. Legacy theming does nothing.** Since April 2026 the Fluent 2 look is mandatory and ignores
the classic `theme` entity. `PublishTheme` returns HTTP 204 and changes nothing. Use the modern
XML web resource path — and set `EnableDesignLanguageThemeSystem = true`, which defaults to
false and isn't documented prominently.

**4. Renaming demo data means more than renaming `name`.** Stale content hides in
`description`, `emailaddress1`, `websiteurl`, `jobtitle` and `companyname`. Sweep all of them.

**5. Customer Insights installs are irreversible and serial.** They cannot be cancelled
mid-cycle, may consume production licensing, and CI – Data cannot start until CI – Journeys
finishes. CI – Data does NOT require your own Azure Data Lake — it offers `Customer Insights managed
storage` at install time.

**6. Chapters 2 & 3 need a NET NEW Azure subscription — but they are not blocked forever.**
The two Add-On cards on the Caldova CDX page (Chapter 2: Build, Observe, and Tune; Chapter 3:
Security + Agent 365) are gated on `MUST ADD NEW AZURE SUB VIA MCAPS BYOT`. Request one at
<https://aka.ms/MCAPSBYOT>; once it shows `Completed` under Subscriptions on the CDX tenant
page, the **Get Add-On** buttons work. Also: **never change tenant passwords** — the add-on
experiences break if you do.

**7. F&O apps need their own environment, and the demo data is Sandbox-only.** Finance and
Supply Chain Management are *environment templates* — they cannot be added to an existing
environment. The "Finance and Operations DemoHub All-in-One" template (Finance + SCM +
Contoso demo data) only appears when Type = **Sandbox**; choose Production and it vanishes,
deploying empty apps instead. Deploy Sandbox, then convert to Production. Also: the
**Security group** field is required and silently disables Save — set it to **None**.
Commerce usually has **0 licences** and additionally needs a Commerce Scale Unit.
🚨 **And selecting DemoHub still does not guarantee demo data** — a verified deployment with the
template selected produced an environment with **no USMF and zero records**, while reporting
`Create — Succeeded`. Verify with `/data/LegalEntities?$top=50` before building anything on it.

## Known unsolved

**Publishing the first-party Copilot Studio agents.** 38 of 44 ship as Draft and would not
publish by any route — `PvaPublish` → 409, `pac copilot publish` → OrganizationServiceFault,
admin-center Publish → error, Copilot Studio Publish button disabled with status "Setting up…".
Appears to be a server-side provisioning state. Agents enabled directly through the admin
surface still work for demos.

If you crack this, please update `reference/02-connections-and-agents.md`.

## Changelog

**v3.7 — 2026-09-01 (o) — Guided start**

- **New `reference/00-guided-start.md` — the skill now onboards you instead of expecting you to read
  it.** This is the choreography for the first ten minutes; `00-operator-profile.md` remains the
  *content* of each question. Cross-linked both ways so they cannot drift.
- **Detect before you ask.** Opens with `pac auth list`, `pac env list`, a record-count sweep and a
  check for an existing profile, then *tells the user what it found* and picks one of four branches:
  **A** claim a tenant · **B** fresh build · **C** resume an existing build · **D** reuse a saved
  decision record. Dry-run on the reference tenant correctly resolved to **C**.
- **Six themed batches instead of thirteen questions in one wall** — who you sell to, workloads,
  data depth, gated decisions, operating preferences, sign-ins. A single dump reads as a form and
  gets abandoned.
- **Irreversible and chargeable choices get their own explicit yes** — CI – Data (~14 h, cannot be
  cancelled), F&O (separate environment), and the Sales Premium trial terms, which is a **licensing**
  decision and must never ride along inside a skimmed multi-select.
- **Claiming is now a guided branch**, including the tenant-slot pre-check, the "launcher is not a
  progress bar" warning, and the MCAPS BYOT Azure subscription — with the advice to **run the intake
  while the tenant provisions** rather than waiting.
- **Plan confirmation before execution**: play back one summary, get one approval, then state the
  operating contract (the only three acceptable interruptions).
- **Decision record written before the first build step** (`~/.caldova-demo-profile.json`) so a crash
  or a new session never re-interviews the user. Explicitly **never stores a password**.
- **New "do not ask what you should decide" table** — queue names, product lists and verification are
  the skill's job. Questions are reserved for what is irreversible, chargeable, or a matter of taste.
- **Fixed the skill description**, which was truncated mid-sentence at *"Starts by asking which
  account"* and did not mention most of what the skill now does.

**v3.6 — 2026-09-01 (n) — Contact Center is LIVE**

- **✅ CORRECTS v3.5.** That release concluded the Omnichannel runtime "was never created" and treated
  a DNS failure as a verdict. **Wrong.** The runtime had simply never been switched on: all five
  channel toggles in **Copilot Service admin center → Channels → Manage channels** ship **OFF**.
- **The fix is one checkbox.** Ticking **Chat** and pressing **Save** provisions the Omnichannel
  runtime for the org in **~90 seconds**. Verified end to end: the chat widget went from 7 console
  errors and a blank corner to **zero errors and a live bubble reading "We're online."**
- **⚠️ The Save looks like it failed** — reload during provisioning and the checkbox is unchecked
  again, with no error and no toast. Documented so nobody re-toggles it in a loop.
- **Proper readiness probes documented**, both far better than the UI:
  `CCaaS_GetContactCenterState()` transitioning **`State=2/Status=2001` → `State=1/Status=2002`**
  (and the channel record `2101 → 1001`), plus the engagement-hub hostname flipping from
  "DNS name does not exist" to resolving. Both `m-` and `e-` prefixes exist.
- **`CCaaS_GetContactCenterState()` 404 is transient** — it 404s *during* provisioning. Not evidence
  the API is unsupported.
- **DNS check re-framed** from verdict to readiness probe, with an explicit warning against the
  conclusion v3.5 drew from it.
- **Order of operations now leads with the switch**, because queues/workstreams/widgets all succeed
  happily against an unprovisioned runtime and none of it connects.
- Live in the tenant: 2 messaging queues, 2 Unified Routing workstreams, 2 chat widgets,
  10 agents across the queues, Chat channel enabled — a working live-chat demo.
- Four rows in the SKILL.md indicator table rewritten around the real behaviour.

**v3.5 — 2026-09-01 (m)**

- **New section: Contact Center / Omnichannel — configured in Dataverse, dead at the runtime.**
  The sharpest false-positive found in the whole build. Every Dataverse indicator says Omnichannel is
  installed — config record, all `msdyn_oc*` tables, 14 workstream templates, four telephony
  solutions, both Copilot Service apps. **The service instance behind it was never created.**
- **The five-second check that settles it**, now the first step of the section:
  `Resolve-DnsName m-<organizationid>.us.omnichannelengagementhub.com`. "DNS name does not exist"
  means no runtime. The shared CDN resolving is not evidence of anything.
- **The cruellest tell documented:** creating `msdyn_livechatconfig` over the Web API returns a real
  `msdyn_widgetappid` and a complete embed snippet — generated *locally by a plugin*, pointing at an
  endpoint that may not exist. Dropped on a page it bootstraps, builds its iframe, then fails every
  config call with `ERR_NAME_NOT_RESOLVED`. **The page merely looks empty; only the console names it.**
  Diagnose from the console, never the screenshot.
- **Full API recipes for everything that IS buildable headlessly**: messaging queues (modelled on the
  stock `Default messaging queue`), Unified Routing workstreams (`msdyn_mode` **717210001 = Simplified**,
  plus the required fields the errors do not volunteer, incl. `msdyn_allowedpresences` as a
  **multi-select**), chat widgets, and queue membership.
- **Queue membership uses `queuemembership_association/$ref` and returns 204** — the exact pattern
  that *fails* for marketing lists, which need the `AddMemberList` action instead. Noted explicitly
  that there is no consistent rule: test the association with one record first.
- **Schema `NOT NULL` is not always binding**: `msdyn_azurenotificationhubid` is declared `NOT NULL`
  with **zero** rows in its table, yet the widget create succeeds and the platform backfills. Do not
  let the schema talk you out of attempting the write.
- **Voice guidance hardened:** `msdyn_occommunicationprovidersetting` and `msdyn_ocphonenumber` are
  both 0. **Never hand-write a phone-number row** — acquiring a number is a purchase transaction
  against ACS and a fake row renders as configured then fails live, which is worse than empty.
- Two new rows in the SKILL.md indicator table.

**v3.4 — 2026-08-28 (l)**

- **✅ CORRECTS the framing in v3.2/v3.3.** Both said cross-tenant Dataverse MCP was not worth
  chasing. **Wrong — it works fine.** The real root cause is a **missing CLI auth profile**, not
  cross-tenant impossibility. On the verified machine a sibling server pointed at an org in a
  *completely different tenant* worked perfectly, because `dataverse auth list` held a matching
  profile. The demo org had **no profile at all**, so the CLI fell back to the corporate WAM
  identity and hit `AADSTS90072` on every session.
- **New section: "Wiring a Dataverse MCP server to a demo tenant, in the right order."**
  **`auth create` FIRST, register SECOND**, restart, then verify at the tool list. Registering
  before the profile exists is precisely what produces the endless sign-in prompts.
- **One-command diagnosis**: every registered MCP org should have a matching row in
  `dataverse auth list`. A missing row and an expired token look identical in the settings UI but
  need different fixes — **no row** means it never authenticated (remove it or create the profile);
  **a row plus a still-failing server** means the token expired (re-run `auth create`).
- **Maintenance reality documented**: CDX tenants expire and demo credentials rotate, so
  `auth create` is not one-and-done. A registered-but-broken server is silent dead weight that costs
  a spawn every session and contributes nothing.
- **Guidance for demo builds:** do not register an MCP server for a tenant you only touch through
  the browser. The Web API path needs no profile, no registration and no maintenance — and it is
  what every recipe in this skill actually uses.
- Two new rows in the SKILL.md indicator table; the sign-in-prompt row rewritten around the real
  cause.

**v3.3 — 2026-08-28 (k)**

- **✅ CORRECTS v3.2. `enabled: false` does not stop the prompts.** v3.2 said to disable the
  offending Dataverse MCP server in host settings. That was **insufficient** — `enabled` gates
  *tool exposure*, not *process spawning*. The host kept relaunching the server every session, it
  kept attempting the cross-tenant token, and the Windows prompt kept appearing on a ~30–90 minute
  cadence for two hours after the "fix".
- **The fix that works:** delete the entry from `~/.scout/m-mcp-servers.json` (back it up first),
  stop the live process by PID, and restart the host — removal only applies to new sessions.
- **Proof source added:** `~/.scout/m-diagnostics.jsonl` logs
  `MCP server "dataverse-<demo>" failed to connect` timestamped to the exact second the dialog
  appears. That single line ends the guesswork.
- **Diagnostic recipe added**, with a caution that matters: filter the AAD operational log on your
  **demo tenant GUID**, *not* on `AADSTS`. Windows emits constant `AADSTS50011` redirect-URI noise
  from ordinary first-party apps — in the verified case the signal was **8 events in 6 hours** out
  of hundreds. The useful event is `AADSTS90072`, which names the client app outright
  (`0c412cc3-…` = Dataverse CLI).
- **`dsregcmd /listaccounts`** documented as the one-command proof of why it can never succeed:
  the machine holds a single WAM account, in the corporate tenant, with nothing for the demo tenant.
- Two new rows in the SKILL.md indicator table, including the `enabled: false` trap.

**v3.2 — 2026-08-28 (j)**

- **🔁 New troubleshooting entry: endless Windows sign-in prompts for the demo tenant.** A Dataverse
  **MCP server registered against the demo org** asks WAM for a token on every launch. The
  workstation is Azure AD-joined to the *corporate* tenant while the CDX demo tenant is a *different*
  tenant, so WAM can never resolve it silently and raises the OS credential dialog. The token never
  persists, so it re-prompts forever and signing in does not help.
- **Two decisive tells**, both documented with commands: there is **no `msal_<demo-tenant>.cache`**
  in `%LOCALAPPDATA%\.IdentityService` (auth never completed, as opposed to expired), and the
  server's **tools are absent from the agent's tool list** even though it is registered *and*
  enabled — it spawns, blocks on auth, and never initializes.
- **Fix:** disable the server (keep the registration so it is reversible), **stop the live process by
  PID** because the setting change does not kill it, and optionally enable the `authCooldown`
  experiment. Nothing is lost — every Dataverse change in this playbook goes through the browser
  against the Web API, so the MCP server was never the working path.
- **Hygiene note:** duplicate Dataverse MCP instances accumulate across restarts (3 copies each of
  two servers, ~1 GB of node processes in the verified case), multiplying the prompts.

**v3.1 — 2026-08-27 (i)**

- **✅ CORRECTED the F&O "nothing can post" diagnosis.** Previous versions blamed a stuck/idle batch
  service. The real cause is **`FiscalPeriods` = 0** — creating a fiscal year over OData produces a
  year *header with no periods inside it*, so no ledger periods exist and no journal can post. Also
  documented that `POST /data/FiscalPeriods` dies with an internal `TargetInvocationException`, so
  the periods cannot be backfilled over OData; the fix is the **`mi=FiscalCalendars`** form.
- **Working F&O menu items** recorded: `FiscalCalendars` ✅ and `BatchJob` ✅;
  `LedgerFiscalCalendar` and `BatchJobListPage` do not exist.
- **⚠️ Knowledge articles: count vs state.** Caldova showed 118 articles but only **6 Published** —
  41 correctly written CPG articles sat in Draft, invisible to knowledge search and agent assist.
  Publishing is a plain `PATCH {statecode:3, statuscode:7}`, which also flips `islatestversion`.
  Took the tenant from 6 to **47 searchable articles**.
- **Marketing list membership**: only **`AddMemberList`** (single member) works. `$ref` on
  `listmember_association` returns 400 and `AddListMembersList` returns 404.
- **`contacts` polymorphic lookup**: `$expand=parentcustomerid` fails; use
  `parentcustomerid_account`.
- **Contracts**: the `description` field alone poisons an otherwise-valid payload, and the error
  names only the entity — **bisect the payload** rather than reading the message.
- **Forecast configuration is wizard-only** and should not be faked over the API — a partial config
  renders a broken Forecasts tab, which is worse than an empty one.
- New "empty CE tabs" audit sweep and seeding recipes in
  `reference/08-agents-and-sales-insights.md`; five new rows in the SKILL.md indicator table.

**v2.9 — 2026-08-27 (g)**

- **🛑 New blocker documented: the Close Agent may not be startable at all.** `Knowledge sources`
  can show a **spinner that never resolves**, permanently greying `Start agent` while the other six
  sections are green. New section in `reference/08-agents-and-sales-insights.md` with the full
  what-doesn't-fix-it table.
- **✅ Corrected v2.8.** That release said `Knowledge sources = "is not filled"` was harmless because
  `Save` succeeds. It is harmless for **Save** and **fatal for Start** — the aria text is the
  *loading* state, not a validation verdict. Both SKILL.md and the reference now say so.
- **Exhausted remedies recorded** so nobody repeats them: ticking the 4th prerequisite, confirming
  the Copilot Studio consent prompt, uploading a PDF knowledge source to `Ready`, and **publishing
  the Copilot Studio agent** (it ships *Not published*) — none clear it.
- **`msdyn_salesagentconfigurations` stays empty (count 0)** even for a saved agent, so there is no
  supported API record to flip to start it manually. Documented to stop the search.
- **`SharePoint location` is a red herring** — despite its "path to the file, not the root folder"
  tooltip, it already holds a full path, and `Default Agent playbook` is a *download link* that
  proves the file exists.
- **Two Copilot Studio automation traps**: the knowledge page has two `input[type=file]` (index 0 is
  the chat sendbox — target index 1 by its `accept` list), and `Add to agent` is a **split button**
  that breaks Playwright strict mode.
- **New reusable assets**: `assets/caldova-product-catalog.html` + `assets/Caldova-Product-Catalog.pdf`,
  a Caldova product/trade catalog for the Product-documentation knowledge source, with the Playwright
  PDF-generation recipe (LibreOffice/markitdown both fail on Windows).
- **Diagnostic upgrade**: screenshot the tab rather than reasoning from `innerText` — a spinner is
  invisible to text extraction and cost a long detour here.

**v2.8 — 2026-08-27 (f)**

- **The Sales Close Agent, documented end to end.** The fifth and highest-consequence Sales agent —
  the only one that composes and **sends outbound customer email autonomously**. New section in
  `reference/08-agents-and-sales-insights.md` covering its seven-tab configurator (not eleven), the
  prerequisites it inherits from the Qualification agent, and a safe order of operations.
- **🚨 There is no autonomy switch on `Email delivery`.** The tab self-satisfies with zero input, and
  a started Close Agent runs a **fixed five-touch cadence** (follow-ups at 2, 3, 4, 5 days, plus a
  7-day branch). Added the OData query to measure the real blast radius *before* starting, and the
  recommendation to seed demo contacts on **`.example` / `.invalid`** domains — Caldova's 5 target
  contacts were all `.example`, which is what makes this agent safe to demo live.
- **New automation trap: `role="menu"` pickers.** The Close Agent's dropdowns render options as
  `role="menuitemcheckbox"` inside `.fui-Combobox__listbox`, so `[role="option"]` probes return zero
  and the field looks broken. They are also search-as-you-type and need a real `fill()`. Notably,
  `.click()` *does* work on these items — an exception to the usual Fluent rule.
- **New trap: `Email signature` is a full Dataverse record.** The textarea is permanently disabled;
  the only path is `Modify signature` → a complete entity form with a **CKEditor** body that needs
  real keyboard input.
- **New trap: Products validates field *content*, not just selection.** `Add missing info` reveals
  exactly what is missing — for Caldova, a required **`Product page URL`** on every product. The
  same grid doubles as a data-quality audit and surfaced two inverted product prices.
- **✅ Corrected a stale section.** The old *"first `Start agent` silently reverts to Draft — just
  retry"* guidance is now explicitly marked **superseded** and cross-linked to the real fix (the
  second confirmation click below the fold), so a reader who hits it first cannot act on it.
- Six new rows in the SKILL.md *"progress indicators lie"* table.

**v2.7 — 2026-08-27 (e)**

- **All available Sales agents now On: 4 of 5.** Added `Data Enrichment`, which uses a
  **different setup pattern** to the others — a linear `Next → Next → Next → Finish` wizard opening
  with "Default settings loaded successfully", and its **prerequisite gate comes at the END**
  (after Finish) rather than before Continue.
- 🔎 **There are FIVE agent types, and one provisions itself.** Creating Data Enrichment silently
  also creates a **`Recommended Actions Agent`** owned by `SYSTEM` and started automatically — the
  list gains two rows, not one. Don't mistake it for a duplicate.
- **Data Enrichment is safe to enable**: read-only Outlook source, and behaviour is
  *"suggests field updates … seller approval required"* — it proposes rather than writes.
- ⚠️ **Sales Close Agent still deliberately not created** — automates outbound email.

**v2.6 — 2026-08-27 (d)**

- **Audited the Sales agents: only 1 of 4 existed.** The gallery offers **Sales Qualification,
  Sales Opportunity, Sales Close and Data Enrichment**, one agent per type. Created and started
  `Caldova Opportunity Insights Agent` with a real CPG value proposition and a
  `Caldova high-value trade opportunities` segment.
- 🔴 **CORRECTED a wrong finding.** The skill previously said a first Start "silently reverts to
  Draft — just retry". Not true: **`Start agent` opens a second confirmation prompt below the fold**,
  and the header stays `Draft` until you confirm. Two identically-named buttons — click the last in
  DOM order. Also: the section tabs' `aria-label` reports validation state
  (`"Component X under General is filled"`), so you can tell a config problem from an unconfirmed
  prompt before retrying.
- ⚠️ **Sales Close Agent deliberately NOT created** — it automates outbound email, so it falls under
  the same authorisation rule as the Journeys live send.
**v2.5 — 2026-08-27 (c)**

Completed the CE demo surface. An audit found the tenant looked finished but had **zero queue
items, quotes, orders, invoices, territories and goals** — the pipeline stopped at Opportunity and
the service queues were empty shells.

- 🔴 **The reskin leaks into the CONFIG layer.** Public queues (`Contoso Coffee Questions/Support`),
  **all six case subjects** (`Auto-drip`, `Clogged tubes`, `Water supply`…) and the price list kept
  their stock Contoso names — coffee-machine fault codes on a beverage company's case forms. Now
  documented as a mandatory post-reskin sweep in `04-demo-data-reskin.md`.
- **Customer Service:** created a `Caldova Service Hours` business-hours calendar (SLAs were running
  24/7), attached it to all 3 SLAs, and **routed all 25 active cases** across the two renamed queues.
  Notes the `calendars` **business-unit requirement** and that **`AddToQueue` 404s** — POST
  `queueitems` directly.
- **Sales lifecycle built:** **60 products priced**, then **56 quotes / 135 lines / 35 orders /
  20 invoices** off 49 won opportunities, tapering so the funnel looks real. Plus 4 territories and
  4 FY26 goals — **reuse the stock `Revenue` metric**, a hand-rolled `metrics` POST just 400s.
- **Field Service:** 4 recurring-maintenance agreements. `msdyn_serviceterritories` **does not
  exist** (404) — Field Service reuses `territory`; and agreements require `msdyn_pricelist`.
- ⏱️ **Long OData batches time out the tool call but keep running in the browser.** Counts read 38
  quotes at timeout and 56 minutes later. **Never blindly re-run** — re-query and resume by diff.

**v2.4 — 2026-08-27 (b)**

- **Added `assets/caldova-demo-setup-overview.pptx`** — an 8-slide deck covering what the skill does,
  how to install it and how to use it, plus the `pptxgenjs` generator that produced it
  (`assets/build-overview-deck.js`) so it can be re-worded or re-branded.
- **Segment suggestions completed: 22 RFM cells**, and two were promoted to real segments in seconds
  via each suggestion's `Create segment` button. **This supersedes hand-building segments** — the
  dialog arrives pre-filled and valid. Segment count 2 → 4.
- **Segment insight `High value vs Platinum differentiators` → Successful.**
- 🔴 **Two CI-Data dialog gotchas documented:** segment names **reject hyphens** (Save silently
  disables with no error), and the Save gate **requires real typing** — a React value-setter plus
  synthetic `input`/`change` populates the field but does not satisfy the validator. Generalised into
  a rule for all CI-Data wizards.
- **F&O batch service confirmed idle** — the account-structure activation has now sat queued and
  unrun for 70+ minutes. Downgraded from "looks idle" to a confirmed blocker.

**v2.3 — 2026-08-27**

The session that turned two "blocked" areas into working ones, and corrected a piece of guidance
that was actively harmful.

*CI – Data*

- 🔑 **THE finding: prediction models and measure templates need a *semantic* activity type and a
  populated semantic field map.** `CaldovaTransactions` was typed `Purchase` — a reasonable label
  that renders identically on the timeline — and was therefore **invisible** to every OOB model
  ("There are no available tables for the selected activity type"). Retyped to **`SalesOrder`** via
  `Configure activities` (the grid's Rename/Delete-only toolbar makes the type *look* immutable; it
  isn't). Then the second half: the **"Map field types?" → Yes** prompt exposes `Sales order ID` /
  `Order date` / **`Sales amount`**, and *nothing downstream can see a column you did not map there*
  — which is also why the ATV measure template offered no monetary field.
- **2 prediction models built:** `Caldova customer lifetime value` (**Successful**) and
  `Caldova transaction churn` (**Completed with warnings** — thin training set).
- **Measures 1 → 5**, all Active. The builder's **`Run`** saves + activates + refreshes in one step
  (`Save and close` leaves them Inactive). Builder is reachable at `/app/measures/builder`;
  `/app/measures/new` 404s.
- **Segments / Suggestions / Insights** all exercised, with three traps documented: the
  suggestions `Run` button greyed by a source-table checkbox that renders late; differentiators
  rejecting date measures *after* offering them; and differentiators needing two **refreshed**
  segments. Also: "Estimated segment size" previewed 79 and resolved to 50.
- **CI compute is one serialized queue** — activities block measures block predictions. Sequence
  deliberately and don't debug "Queued".

*Finance and Operations*

- ⭐ **Ask Murray Fife first.** `{{USER_EMAIL}}` (Sr Solution Engineer, AMI TMG Specialist
  Sales US) maintains F&O configuration/demo-data templates, requested directly from him — no
  portal or form. **This is now option 0**, ahead of both redeploying and hand-building.
- 🔴 **RETRACTED the `?cmp=USMF` "forcing test".** The old advice said to browse to a non-existent
  company and read the error as proof. F&O **persists `cmp` to the session**, so that breaks every
  later page load with *"associated with a company which does not exist"* — which reads like a
  permissions fault. Recover with `?cmp=DAT`. Related correction: **`USMF` is not universal** — it
  ships only with the Contoso dataset, so its absence is not a fault. Count `LegalEntities` rows and
  check `MainAccounts` instead.
- **Built an F&O foundation from scratch over OData** (~20 min, vs a ~4h redeploy that had already
  failed once): USD currency, legal entities **USRT** + **USMT**, fiscal calendar + 3 years, a
  CPG-shaped chart of accounts (**45 accounts**, incl. trade-promotion accrual, slotting fees and a
  3-stage inventory), ledger binding, and the AR/automatic-transaction **posting profiles**.
  Four OData rules make or break it — chiefly **omit RecId fields entirely, don't send `0`**.
- ⚠️ **Two posting setups have no OData write entity at all:** vendor posting profiles and
  **inventory posting** — the latter being the one that matters most for a manufacturer. Searching
  metadata for "PostingProfile" is misleading; it returns ten unrelated sets.
- **F&O MCP server verified live** and `Microsoft Scout` already allow-listed (Microsoft seeds seven
  first-party clients — check before adding a duplicate). Probe with `GET /mcp` → 401 and
  `/.well-known/oauth-protected-resource` → 200.
- **Honest status:** no journal has posted yet. Five things gate it and each fails differently.

**v2.2 — 2026-08-26 (k)**

- **⛔ CI – Data activities did not persist.** A prior pass reported 5 of 9 activity tables
  configured; the Activities page later showed the empty state. **Nothing had saved.** The skill's
  own core lesson, from the inside — re-verify wizard output in a fresh session before building on it.
- **CI – Data segment builder documented.** Attribute picking works well and proves the profile
  store is live (it auto-suggested a real `LoyaltyTier` value). Always choose the
  `Table: Customer | Data source: ConflationResolve…` option — the unified entity, not a source copy.
  Note **Output table name is immutable once saved**.
- **Save gate is unreliable.** `button[data-testid="save"]` stays disabled despite a complete rule and
  name; synthetic events and real `fill()` both failed to release it — same stale-panel failure mode as
  the Activities wizard. **Recommendation: hand-build segments (under a minute each); automate
  ingestion and unification, which are fully verified.**
- **Reinforced upstream conclusion:** segment quality is decided during *data generation*. Seed clean,
  exact-match, low-cardinality attributes — the same finding the CI – Journeys segment DSL forced.

**v2.1 — 2026-08-26 (j)**

CI – Journeys email authoring investigated end to end against a live tenant. Result is a **negative
finding with a precise boundary**, plus a diagnostic technique that generalises.

- **`msdynmkt_ValidateEmail` is the only surface that tells the truth.** The UI says *"Please check
  validation errors"*; `updateRecord` says *"Validation returned errors"*. The action returns a
  structured per-field report naming every failure. Full payload shape documented.
- **Technique worth reusing anywhere:** to learn an undocumented action's payload, click the real
  button in the UI and read the outgoing request body from the network log. Guessing returns a bare
  `400`.
- **The designer owns three fields, and only one is authoritative.** `msdynmkt_designerhtml` is the
  source; `msdynmkt_emailbody` is compiled *from* it; `msdynmkt_placeholders` is derived on save.
  **Writing `emailbody` alone renders nothing** — and a subsequent designer save *destroys* it
  (observed: 1,993 → 10,943 bytes, authored content replaced).
- **Documented the working authoring pattern** — save one email in the designer, harvest its
  `designerhtml` as a skeleton, then inject `data-editorblocktype="Text"` blocks for the rest. Nine
  emails authored in one API pass this way.
- **Publish prerequisites in the order they surface:** non-null placeholders (`{}` accepted, `[]`
  rejected) → `msdynmkt_purpose` **mandatory** (not enforced on the form) → compliance profile bound
  → compliance placeholders bound → `to` resolvable.
- **⛔ Confirmed boundary: emails cannot be published by API alone.** `{{CompanyAddress}}` /
  `{{PreferenceCenter}}` fail `NoBinding`, and completing the compliance profile does **not** fix it
  — the binding lives in the placeholder JSON the designer writes. `msdynmkt_to` additionally fails
  `UnsupportedProperty` standalone. Verified independent of the footer by stripping it and switching
  to Transactional purpose. **Each email needs ~1 minute in the designer; script everything else.**
- **Entity-name traps:** `msdynmkt_complianceprofile` and `msdynmkt_senderprofile` **do not exist**;
  the real entity is `msdynmkt_compliancesettings4`, and a *different* older `msdynmkt_compliancesettings`
  also exists.
- **CDX tenants ship a pre-validated sending domain** (`*.dyn365mktg.com`) — no DNS work, which
  normally is the slowest part of standing up marketing email. Compliance profiles and the
  preference center, by contrast, ship as unlinked stubs.
- **Segment DSL limits quantified:** 2 of 10 segments went live — exactly those using only `==` and
  `&&`. Every query using `contains()` or `||` stayed Draft. Implication moved upstream: **seed
  contacts with clean exact-match `jobtitle` values** so segmentation is demoable at all.

**v2.0 — 2026-08-26 (i)**

Two false-negative fixes, both reported from the field.

- **CRITICAL — `home.ci.ai.dynamics.com` is the authoritative CI surface, and its first-run region
  form IS the install.** Another operator was told by this skill that CI – Data and CI – Journeys
  were not installed. Both were available; all that was needed was opening the portal and selecting
  a region. The previous text called that screen "transient provisioning UI" and advised **waiting
  for it to disappear** — which is an indefinite wait on a form that is asking a question. Corrected,
  with the wrong advice explicitly flagged so nobody reinstates it.
- **New rule: a form is not a spinner.** Never report a product unavailable until you have opened
  its own portal and completed whatever it asks for. Report "not installed" only when the portal
  itself refuses with a licensing or entitlement error.
- **The admin-center provisioning grid is now documented as non-authoritative** — it reflects
  admin-driven provisioning only and shows `Not Installed` for tenants that are fully entitled and
  self-service ready. Added to the "progress indicators lie" table in `SKILL.md`.
- **Hydration timing trap.** Reading `document.body.innerText` right after navigation returns the
  pre-hydration skeleton, where every checklist step reads `Not started`. Observed on a tenant with
  408 unified customers. Use an accessibility snapshot or wait for a known-final string.
- **CRITICAL — "not licensed for CI" is almost always a wrong check.** The same operator was also
  told they lacked a CI licence. Verified on the reference tenant: the
  **`Dynamics 365 and Power Platform multi-app demo`** SKU *does* include Customer Insights,
  Customer Insights Accounts Add-on, Engagement Insights, Dataverse for Cust Insights BASE and
  Dynamics 365 for Marketing (CI – Journeys) — **all assigned at the ORGANIZATION level**, labelled
  *"can't be assigned per user"*. A per-user licence check therefore **can never** show CI, on any
  tenant, however well licensed. Sales / Customer Service / Field Service *are* per-user, which is
  what makes the per-user method look trustworthy until it silently fails on CI.
- **Billing → "Your products" is not the licence list.** It showed **1 product** on a tenant whose
  **Licenses** page showed **17**. It reflects billing-account purchases, not entitlement. Both
  traps added to the `SKILL.md` verification tables with the correct surface for each.

**v1.9 — 2026-08-26 (h)**

Restructured the skill around a **front-loaded intake** so a build runs unattended after the first
ten minutes. Driven by a real observation: the reference build interrupted its operator five
separate times — Sales Premium terms, journey shape, live-send policy, F&O redeploy, activity-type
choice — and **every one of those was knowable at the start**.

- **`reference/00-operator-profile.md` rewritten** from a 5-question interview into a **13-decision
  single intake**, grouped as: who you sell to (5) → scope and data depth (2) → decisions that
  otherwise interrupt you (5) → credentials (1).
- **Batched authentication.** All interactive sign-ins — `pac auth create`, the browser portals, and
  the F&O client, which forces a password prompt even with a live session — now happen in one
  sitting at intake. This was the single most common mid-build stall.
- **Data-depth tiers** (Light / Standard / Deep) replace open-ended "how many records?", with an
  explicit note that predictive scoring will not train on Light.
- **Pre-authorisation questions** for the two commercial decisions the build cannot make alone:
  accepting Sales Premium trial terms, and sending real email.
- **Recovery policy captured up front** — e.g. "if F&O deploys empty, redeploy automatically or
  stop?" — so a known ~3-hour failure does not need a live decision.
- **Unattended operating rules** added to both `SKILL.md` and the profile: what to do on a long
  wait, a failure, a masked error, and an ambiguous choice — *instead of* asking.
- **Exactly three acceptable interruptions** now defined: an interactive password prompt, an
  unauthorised destructive/commercial action, and a genuine dead end after diagnosis.
- **Decision-record template** so answers are written down once and every later phase reads from
  them rather than re-asking.
- Self-maintaining rule: *if you are about to ask a mid-build question, it belongs in the intake* —
  add it there so the next operator is never asked at that point again.

**v1.8 — 2026-08-26 (g)**
- **Customer Service and Field Service seeding** added to `08-agents-and-sales-insights.md` (Part E).
- 🚨 **Cases cannot be resolved with `updateRecord`** — same masked `"An error has occurred.
  {1}{0}"` as opportunities. Use the **`CloseIncident`** action with an `incidentresolution`
  payload. Failed closes still leave the case created.
- **Field Service build order documented.** A fresh org is completely empty. Strict chain:
  incident types + work order types + price list → customer assets → work orders → resources →
  bookings. Two schema traps: `msdyn_incidenttype` **rejects `msdyn_defaultduration`**, and
  `msdyn_workorder` **requires `msdyn_pricelist`** ("Price List is a required field").
- Note to bind work orders to an asset **at the same account** (read `_msdyn_account_value` off the
  asset) rather than picking an account independently.
- **CI – Data activities (Part D):** semantic activity types (`SalesOrder`, `Loyalty`,
  `Subscription`, `Feedback`) add a mandatory "Map field types" section needing up to 10 extra
  mappings — **prefer non-semantic types for demos**. Enabling the timeline toggle makes **Icon**
  required, and the reason Next is disabled appears only in the tab's `aria-label`.

**v1.7 — 2026-08-26 (f)**
- ✅ **Found the Sales agent surface the skill was missing:** `App Settings → Dynamics 365 AI hub →
  Agent manager`. Not Copilot Studio, **not Premium-gated**. Documents the four Sales agents
  (Qualification, Opportunity, Close, Data Enrichment) and the three prerequisites that gate
  creation — Copilot Studio capacity, cross-region data terms, AI prompts.
- ⚠️ **Predictive scoring is Sales Premium**, behind a terms-and-conditions checkbox, and
  activation takes **up to an hour**. Until it completes there is no Predictive scoring node in the
  nav at all. Flagged as a licensing decision the operator must make, not a config step.
- 🚨 **You cannot close an opportunity with `updateRecord`** — it fails with a masked
  `"An error has occurred. {1}{0}"`. Must POST to `WinOpportunity` / `LoseOpportunity` with an
  `opportunityclose` payload. **The failed loop still creates the records**, so a blind re-run
  duplicates everything — observed `ok=25 fail=85` where all 110 records actually existed.
- 🚨 **NEW CRITICAL FINDING — the F&O DemoHub template can silently deploy an EMPTY environment.**
  Verified: template selected, Type = Sandbox, `Create — Succeeded`, F&O client loads with the full
  module set — and yet **1 legal entity (`dat`), 0 customers / vendors / products / sales
  orders / main accounts / workers**. Added a 30-second OData verification recipe and recovery
  options. `Create — Succeeded` covers provisioning, **not content**.
  *(Corrected 2026-08-27: the original write-up recommended a `?cmp=USMF` "forcing test" — **retracted**.
  An unknown `cmp` value persists to the session and breaks subsequent loads; and `USMF` only exists
  where the Contoso dataset was deployed, so its absence is not itself a fault. Count `LegalEntities`
  rows instead.)*
- **NEW `reference/08-agents-and-sales-insights.md`** (Phase 5.5) — first-party agent inventory and
  predictive lead/opportunity scoring.
- **Predictive scoring will not train on a stock demo org.** Documented the real blocker: 16 leads
  all Open, 22 opportunities. Models need ≥40 Qualified **and** ≥40 Disqualified leads. Includes
  full lead/opportunity `statuscode` → `statecode` maps, the create-then-update pattern,
  `overriddencreatedon` backdating, and why disqualified records must look statistically *different*
  from qualified ones or the model learns nothing.
- **Two more "looks configured but isn't" surfaces**: the `bots` table reports all 45 agents as
  `statecode=0 status=1` regardless of publish state, and `msdyn_aiconfiguration`'s 50 rows are
  **Copilot prompts, not scoring models**.
- **`Xrm.WebApi` batch-size warning** promoted into `SKILL.md`: batch at 50–60, verify after each,
  never schedule a whole record class last. Observed a 300-record loop time out at 180 with the
  entire disqualified tranche missing — the exact state that yields an untrainable model.
- F&O timing **measured end to end: 3h 41m** (11:04:38 → 2:45:49, `Create — Succeeded`), with zero
  progress indication throughout. Also recorded the two-URL structure (`.crm.` + `.operations.`)
  and that reaching the F&O client forces an **interactive password prompt** — not automatable.
- `gen-ci-data.py` extended from 5 to **11 sources** (~6,800 rows): support cases, subscriptions,
  product reviews, NPS/survey, app usage, legacy ESP engagement. All reuse the same 420-person
  population, so existing unification results are unaffected. Support cases deliberately use a
  **third phone format** to deepen the identity-resolution demo.

**v1.6 — 2026-08-26 (e)**
- **NEW `reference/00-claim-tenant.md`** — the skill now starts from *no tenant at all*: claiming
  the Caldova experience on CDX, and requesting the NET NEW Azure subscription via MCAPS BYOT.
  Verified against the live CDX page.
- **The two are strictly sequential, not parallel** — the tenant must exist before the BYOT request
  has anything to attach to. Documented as a critical path with two back-to-back waits, neither
  startable early.
- **Claiming takes several minutes**, but the **CDX launcher is not a progress bar** — it can keep
  spinning for **15–30 minutes after the tenant has already been created**. Verify in
  `My Dashboard` → Overview, not the launcher. Explicit warning not to re-click **Start demo**,
  which risks burning a second tenant slot.
- Generalised the skill's headline lesson from "`pac` CLI lies" to **"progress indicators lie"**,
  now covering three UI surfaces (CDX launcher, CI provisioning portal, PPAC direct URLs) alongside
  the five `pac` commands, with a single verification-surface lookup table.
- **NEW § 2.5 "Identity"** and a matching gate at the top of `SKILL.md`: the skill now explicitly
  asks the operator for the **Caldova admin UPN** and signs in as that account across `pac`, the
  browser and `az` before any work starts. Documents how wrong-identity failures actually present
  (empty environment lists, empty portals, uploads to the wrong OneDrive) and the rule
  **"if something is inexplicably empty, check identity first."** Asks for the UPN only — never a
  password, in chat, in a file or in a script.
- Documented the **tenant-slot cap** (`Roles & Limits` → 1-year tenants, capped at 3; expired
  tenants keep occupying slots until deleted) — a silent blocker on claiming.
- Documented the claim entry point precisely: **Start demo** on the *Caldova Live Demo Environment*
  card, a JS button with no deep link.
- Captured the **`Caldova-BlankTenant-V1`** content-pack name, which explains the skill's founding
  premise: full licensing, zero installed apps.
- Recorded the tenant-vs-experience distinction, dashboard table shapes, credential retrieval, and
  the **never change tenant passwords** rule.
- Phases renumbered: claim = Phase 0, operator profile = Phase 0.5 (and 0.5 is the right way to
  spend a Phase 0 wait, since it needs no tenant).

**v1.5 — 2026-08-26 (d)**
- **Ingestion and unification executed end to end and verified.** `reference/06-customer-insights.md`
  now carries a full walkthrough of both, replacing the "planned but not executed" placeholders.
- Result validated against generator ground truth: **408 profiles / 262 matched / 0 duplicates —
  all three exact.**
- Corrected the generator's `expected unified profiles 420` figure: that assumes three profile
  sources and contradicts this skill's own advice to treat rebates as an activity. The correct
  target for the recommended config is **408**.
- Documented that the matching-rule dialog has **no normalisation control**, which invalidates the
  older "normalise phone inside a match rule" plan.
- Documented that the unify wizard is now Customer data → Deduplication → Matching → Unified data
  view → Review, not Map/Match/Merge.
- Housekeeping note: `gen-ci-data.py` writes output beside itself and will pollute the skill zip.

**v1.4 — 2026-08-26 (c)**
- Recorded the **verified landed end state of CI – Journeys and CI – Data** (both installed, both
  at zero content) with the exact probes used, in `reference/06-customer-insights.md`.
- Documented that the **CI – Data "Getting started" checklist is the authoritative progress
  surface**, unlike the provisioning portal status which lags.
- Two false alarms captured: Copilot preview reports "Failed to connect" on an empty instance
  (cosmetic), and `msdyncrm_segment` has no `msdyncrm_name` column (breaks naive status sweeps).

**v1.3 — 2026-08-26 (b)**
- **NEW `reference/00-operator-profile.md`** and a mandatory **Phase 0**: the skill now asks the
  operator what industry, territory and accounts they cover before reskinning anything, with
  archetype / brand-axis / opportunity-motion / case-type starters for six industries. Added
  because this skill is exportable and every industry example in it came from one seller's
  territory.
- Privacy rule promoted from a footnote in the reskin file to a stated recommendation in Phase 0.
- `04-demo-data-reskin.md` gated on Phase 0; CPG examples explicitly relabelled as worked
  examples with the transferable *structure* called out separately from the names.
- `gen-ci-data.py` flagged in SKILL.md as carrying CPG placeholders that must be replaced.
- **`pac` lies table extended to five rows**: `pac admin list` returned 1 of 3 environments.
  Verified — it only surfaces environments that already have a Dataverse DB, so an F&O
  environment is invisible to it while `Preparing`. Added to `07-finance-and-operations.md` with
  a DevTools snippet that reads the PPAC grid instead.
- F&O timing: real measurement recorded (created 11:04, still `Preparing` at 11:41, 37+ min).

**v1.2 — 2026-08-26 (a)**
- Added `reference/07-finance-and-operations.md` — F&O as environment templates, the Sandbox-only
  DemoHub All-in-One demo-data template, the Security-group Save blocker, wizard traps.
- Chapters 2 & 3 reclassified from "dead end" to "procurement step" after the MCAPS BYOT Azure
  subscription landed.

**v1.1 — 2026-08-26**
- Added `reference/06-customer-insights.md` — CI Journeys + Data install surface, irreversibility,
  managed-storage option, `gen-ci-data.py`.

**v1.0 — 2026-08-25**
- Initial build from the end-to-end Aug 2026 Caldova tenant stand-up.

## Partially verified

`reference/06-customer-insights.md` — install, **CSV ingestion via Power Query, and unification**
are all executed and verified (2026-08-26), with the unification result validated against the
generator's ground truth (408 profiles / 262 matched / 0 duplicates, all exact). Still not
executed: **activity configuration, measures, segments and predictions**. Those sections remain
plans and are flagged inline. Correct that file once you have run them.

## Verifying a build

```js
// paste scripts/verify-environment.js into DevTools on any model-driven app page
await verifyEnvironment();
```

Healthy output: ~18–22 apps, 0 unbound connection references, 0 dirty demo-data records,
`EnableDesignLanguageThemeSystem: "true"`.


