Skip to content
Closed
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
49688f3
fix(mobile-apps): harden Dataverse schema creation
Jul 27, 2026
3860205
Potential fix for pull request finding
kanu-shubham Jul 27, 2026
38938f5
refactor(mobile-apps): drop create-payload script; unblock draft plan…
Jul 28, 2026
e7437d7
docs(mobile-apps): keep pre-flight note inside the blockquote
Jul 28, 2026
e1a3251
perf(mobile-apps): short-circuit Dataverse tenant resolution
Jul 28, 2026
881eaca
perf(mobile-apps): batch Dataverse metadata reads into single queries
Jul 28, 2026
c1311c9
perf(mobile-apps): fold key/M:N pre-flights into the snapshot
Jul 28, 2026
2797ba8
fix(mobile-apps): pre-flight lookups and calc columns for idempotent …
Jul 28, 2026
3922e37
fix(mobile-apps): never block the user on a data-modelling conflict
Jul 29, 2026
fbd90e2
docs(mobile-apps): complete decision set and fix dead reference link
Jul 29, 2026
ac30603
Optimize Dataverse metadata execution
Aug 13, 2026
94e3a20
Address Dataverse V2 review feedback
Aug 14, 2026
9e2e389
Fix Windows test PATH resolution
Aug 14, 2026
596602c
Make Azure CLI test doubles portable
Aug 14, 2026
58b6086
harden Dataverse derived metadata reconciliation
Aug 14, 2026
f17a7f2
Harden mobile Dataverse runtime contracts
Aug 18, 2026
5181c52
Enforce route checks in mobile create flow
Aug 18, 2026
7ffd79d
Make mobile identity and GUID contracts deterministic
Aug 18, 2026
f2c8068
Remove obsolete mobile GUID helper guidance
Aug 18, 2026
741c3f8
Align Dataverse planning conflict decisions
Aug 18, 2026
7ffe629
Potential fix for pull request finding
kanu-shubham Aug 18, 2026
9428959
Normalize route paths on Windows
Aug 18, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions .github/workflows/mobile-apps-script-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Functional unit tests for the mobile-apps plugin across supported operating
# systems and Node versions. These scripts spawn Azure CLI shims and manipulate
# filesystem paths, so cross-platform coverage protects real behavior.
name: mobile-apps-script-tests

on:
pull_request:
branches:
- main
paths:
- "plugins/mobile-apps/**"
- ".github/workflows/mobile-apps-script-tests.yml"

jobs:
test-mobile-apps-scripts:
name: test-mobile-apps-scripts (${{ matrix.os }}, node ${{ matrix.node }})
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os:
- ubuntu-latest
- windows-latest
- macos-latest
node:
- 20
- 22
steps:
- name: checkout
uses: actions/checkout@v4

- name: setup-node
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}

- name: run-mobile-apps-script-tests
shell: bash
working-directory: plugins/mobile-apps
run: node --test scripts/tests/*.test.js
134 changes: 81 additions & 53 deletions plugins/mobile-apps/agents/data-model-architect.md

Large diffs are not rendered by default.

22 changes: 13 additions & 9 deletions plugins/mobile-apps/agents/native-app-planner.md
Original file line number Diff line number Diff line change
Expand Up @@ -498,13 +498,16 @@ Reject loop = re-spawn `screen-planner` with the user's feedback (layout, screen
### Step 5c — Cross-entity Read Audit (Round 2 data-model pass)

**Print before spawning:**
> "→ Auditing the locked screen plan for cross-entity reads (calc-column candidates from related_entity_fields blocks)…"
> "→ Auditing the locked screen plan for supported cross-entity read paths…"

**Run condition:** execute this step ONLY after Gate 4b has been approved AND the screen-planner's per-screen specs include at least one `related_entity_fields` block. Skip silently otherwise (no cross-entity reads = no calc-column proposals needed).
**Run condition:** execute this step ONLY after Gate 4b has been approved AND the screen-planner's per-screen specs include at least one `related_entity_fields` block. Skip silently otherwise.

**Detection (cheap):** before spawning, `Grep` the locked plan for `related_entity_fields:` in `<working_dir>/native-app-plan.md`. Zero matches → skip Step 5c entirely, mark `[x]` and proceed to Step 6. One or more matches → spawn the audit pass below.

This step exists because of the runtime constraint documented at [`shared/references/data-performance.md` § Cross-entity Reads](${PLUGIN_ROOT}/shared/references/data-performance.md#cross-entity-reads) — the SDK has no `$expand`, so cross-entity fields on hot paths (lists, dashboards) MUST be denormalized via calculated columns at the data-model layer. The screen-planner emits `related_entity_fields` per screen; this step turns those into calc-column proposals.
This step exists because the SDK has no `$expand`. It verifies that every
cross-entity field uses a formatted lookup or bounded chained fetch, and flags
hot-path fields that require an externally supplied projection. It never
synthesizes calculated/formula metadata.

#### 5c.1 — Spawn `data-model-architect` in `cross-entity-audit` mode

Expand All @@ -530,22 +533,23 @@ Wait for return; apply the Step 3.0 status switch:
- `DONE_WITH_CONCERNS: <list>` → embed addendum, propagate concerns into your own final `DONE_WITH_CONCERNS:`.
- `NEEDS_CONTEXT:` / `BLOCKED:` — propagate up per the standard switch.

#### 5c.2 — Gate 1 addendum (calc-column approval)
#### 5c.2 — Gate 1 addendum (cross-entity read paths)

If 5c.1 wrote a `### Cross-entity Reads` addendum, present it to the user as a Gate 1 addendum (NOT a fresh Gate 1 — the original schema is already approved and unchanged):

```
## Gate 1 — Addendum: Cross-entity Reads

The screen plan you approved at Gate 4b reads N fields from related entities (gate names on inspections, customer phones on orders, etc.). Because the Power Apps SDK has no $expand, those fields need calculated columns on the parent tables to display efficiently — otherwise list screens would either render "—" or trigger N+1 fetches per row.
The screen plan reads N fields from related entities. The generated SDK has no
$expand, so each field must use a formatted lookup, a bounded chained fetch, or
an external server-owned projection.

Proposed calculated columns (auto-derived from your screen plan, no schema reshape):
Proposed read paths:

[paste the ### Cross-entity Reads table from _dm_section.md]

[paste the Chained-fetch fields (informational) table if present — these need NO schema change, the screen-builder handles them at scaffold time]

Approve to add these calc columns to the data model? (Reject → revise the audit. Approve → /setup-datamodel will create them in Phase 6.1b.)
Approve these read paths? Any `external-projection-required` row remains a
blocker until the user supplies that projection outside this workflow.
```

Reject loop = re-spawn data-model-architect in `mode: cross-entity-audit` with the user's feedback (e.g. "drop cr3e9_tailnumber_calc, the list doesn't actually show it"). Approve = mark `[x]` Gate 1 addendum approved, proceed to Step 6.
Expand Down
24 changes: 12 additions & 12 deletions plugins/mobile-apps/agents/screen-builder.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,26 +116,26 @@ You will be invoked by `/create-mobile-app` Step 11 or `/edit-app` screen-rebuil
```

For lookup labels, select the real `_<lookup>_value` field in your `$select` and read with `lookupName(record, '<lookupLogicalName>')`. For choice / status / boolean / datetime / money labels, read with `formattedValue(record, '<columnLogicalName>')` or fall back to the generated option const. On bounded lists that use `useSearchFilter(...)`, fields MUST be real string properties from generated types; never add an inferred display-name field just to make search prettier. Cursor lists do not use `useSearchFilter`; they push search into the service `filter` option.
- **HARD RULE — Cross-entity Field Resolution.** Before writing the screen's `select: [...]` or load step, walk every UI field your spec displays. For each field that sources data from an entity OTHER than the screen's primary fetch target, follow this algorithm exactly. The full reference (with cost-profile rationale, calc-column naming, and pattern examples) is at [`shared/references/data-performance.md` § Cross-entity Reads](${PLUGIN_ROOT}/shared/references/data-performance.md#cross-entity-reads). The screen-builder MUST apply the rule mechanically — do NOT invent your own resolution.
- **HARD RULE — Cross-entity Field Resolution.** Before writing the screen's `select: [...]` or load step, walk every UI field your spec displays. For each field that sources data from an entity OTHER than the screen's primary fetch target, follow this algorithm exactly. The full supported-path reference is at [`shared/references/data-performance.md` § Cross-entity Reads](${PLUGIN_ROOT}/shared/references/data-performance.md#cross-entity-reads). The screen-builder MUST apply the rule mechanically — do NOT invent your own resolution.

1. **Calc column check first.** Open `src/generated/models/<PrimaryEntity>Model.ts` and search for a column matching `<prefix>_<field>_calc` (or any `_calc`-suffixed column resolving the field you need). If present, add it to your `select: [...]` and render directly. Done — no chained fetch needed.
2. **No calc column? Branch on screen archetype × cardinality** (archetype is in your spec under `**Archetype:**`):
1. **Follow the planned recommendation** from `related_entity_fields`.

| Archetype | Cardinality | Action |
| Recommendation | Action |
|---|---|---|
| List (`top ≥ 5`), Tab-root, Dashboard | 1:1 (N:1 lookup chain) | **STOP — do NOT chain a fetch in the list `map()` / `renderItem`.** Emit a `// TODO(cross-entity-read):` header comment naming the field, the related entity, and the recommended `<prefix>_<field>_calc` column to add. Render the cell as `'—'` for now. Then return `BLOCKED [<screen_name>]: list field <field> requires cross-entity read; needs calc column <prefix>_<field>_calc on <primary_entity> — re-run /setup-datamodel (or /add-dataverse for existing apps) to add the calc column.` Do NOT scaffold an N+1 fetch storm. |
| Detail (single record) | 1:1 (N:1 lookup chain) | Scaffold a chained `<RelatedService>.get(record._<lookup>_value, { select: [...] })` in the screen's load step. One record on screen = one extra round trip is fine. Display via `lookupName(...)` or direct field read. |
| Any | 1:many or M:N | Scaffold a chained `<ChildService>.getAll({ filter: \`_<parentid>_value eq '${id}'\`, select: [...] })`. Calc columns CANNOT traverse 1:many or M:N — chained fetch is the ONLY pattern. |
| `formatted-lookup` | Select the real `_<lookup>_value` field and render `lookupName(record, '<lookupLogicalName>')`. |
| `chained-fetch` | Perform one bounded related `get` / `getAll` in the screen load step, never inside `map()` or `renderItem`. |
| `external-projection-required` | Render no fake fallback data. Return `BLOCKED` and name the field/source. The user must supply a supported server-owned projection outside this workflow. |

3. **Verify before exit.** Every UI field in your spec must have either (a) a `select` entry on the primary fetch (covered by direct column or calc column) OR (b) a chained fetch path. If a field has neither, return `BLOCKED [<screen_name>]: field <field> on <screen> has no fetch path — add to spec or add calc column`.
2. **Verify before exit.** Every UI field in your spec must have either a
primary select, formatted lookup annotation, or bounded chained fetch.
Otherwise return `BLOCKED [<screen_name>]: field <field> requires an external projection`.

4. **TODO comment shape** (when emitting at step 2 / list branch):
3. **TODO comment shape** for an external projection:

```ts
// TODO(cross-entity-read): screen displays <field> from related <entity>.
// Re-run /setup-datamodel (or /add-dataverse for existing apps) and add calc
// column <prefix>_<field>_calc to <primary_entity> for one-round-trip reads.
// List screens MUST NOT chain fetches in renderItem — N+1 storm.
// Supply a maker-created formula column or another server-owned projection,
// then rerun Dataverse reconciliation. Never chain reads in renderItem.
```

- **HARD RULE — server-managed columns are NEVER in a create or update payload.** The Dataverse server owns these fields; including them in a `*Service.create({...})` or `*Service.update({...})` returns HTTP 400 on every save. Generated `create()` types may include server-managed fields (`ownerid`, `statecode`, primary IDs, etc.) because they mirror the full model; do **not** satisfy those types by emitting junk values. For any screen with create/update behavior, use a narrow write helper/type whose input contains only editable fields. If the skeleton imports an app-level helper, call it; otherwise define the helper inside your assigned screen file. Do **not** create or modify shared `src/utils/`, `src/hooks/`, or service files from a screen-builder. Forbidden keys in any create/update payload:
Expand Down
17 changes: 10 additions & 7 deletions plugins/mobile-apps/agents/screen-planner.md
Original file line number Diff line number Diff line change
Expand Up @@ -379,24 +379,27 @@ For each screen the user adds, provide this compact shape:
- **Profile content** — REQUIRED on the Profile screen only. List 2-4 app-specific profile sections based on the app requirements and target users, for example `Role + team`, `Assigned site/territory`, `Default queue filters`, `App support/contact`, `Environment/app version`. Include any generated service needed for those sections; otherwise use local/static app context plus `useAuth()`.
- **Sign-out affordance** — REQUIRED on the Profile screen and omitted from every other screen. Write `visible Button "Sign out" using useAuth().signOut with confirm`; sign-out returns to `/login` after completion.
- **Data** — which generated services it calls, with method names (e.g., bounded lookup: `AccountsService.getAll({ top: 50, orderBy: ['name asc'], select: ['name'] })`; cursor list: `InspectionsService.getAll({ maxPageSize: 50, orderBy: ['scheduledDate asc', 'inspectionid asc'], select: [...] })` plus `skipToken` continuation support)
- **Related entity fields** (REQUIRED if any UI field on the screen displays data from an entity OTHER than the primary `Data` service's table; OMIT entirely otherwise) — one entry per cross-entity field. The `data-model-architect`'s Step 6a Cross-entity Read Audit reads this block to decide which calculated columns to propose. Mechanical schema:
- **Related entity fields** (REQUIRED if any UI field on the screen displays data from an entity OTHER than the primary `Data` service's table; OMIT entirely otherwise) — one entry per cross-entity field. The `data-model-architect` audits that each field has a supported read path. Mechanical schema:

```yaml
related_entity_fields:
- field: <user-visible field name, e.g. "Gate name">
source: <dotted path from primary entity to resolved column, e.g. cr3e9_flightid → cr3e9_gateid → cr3e9_gatename>
cardinality: "1:1" | "1:many" | "M:N"
archetype_class: list | detail | tab-root | dashboard
recommends: calc-column | chained-fetch
recommends: formatted-lookup | chained-fetch | external-projection-required
```

**Mechanical derivation of `recommends`** (no judgement — pick from this table):

| `archetype_class` | `cardinality` | `recommends` |
|---|---|---|
| `list`, `tab-root`, `dashboard` | `1:1` | `calc-column` |
| `list`, `tab-root`, `dashboard` | direct lookup primary display | `formatted-lookup` |
| `list`, `tab-root`, `dashboard` | any other `1:1` related field | `external-projection-required` |
| `detail` | `1:1` | `chained-fetch` |
| any | `1:many` or `M:N` | `chained-fetch` (calc columns can't traverse) |
| `list`, `tab-root`, `dashboard` | `1:many` or `M:N` per-row field/aggregate | `external-projection-required` |
| `detail` | `1:many` | `chained-fetch` |
| `detail` | `M:N` | `external-projection-required` unless a generated intersect-table service and exact bounded query contract are already named in the approved data model |

**`archetype_class` mapping from `Archetype`:** `List` → `list`; `Tab-root` → `tab-root` (or `dashboard` if `Operational pattern: home-dashboard` / `assignment-dashboard`); `Detail` → `detail`; `Form` / `Modal-Sheet` / `Auth` / `Empty-onboarding` → `detail` (cold path, single-record context).

Expand All @@ -410,17 +413,17 @@ For each screen the user adds, provide this compact shape:
source: cr3e9_flightid → cr3e9_flightnumber
cardinality: "1:1"
archetype_class: list
recommends: calc-column
recommends: formatted-lookup
- field: "Gate name"
source: cr3e9_flightid → cr3e9_gateid → cr3e9_gatename
cardinality: "1:1"
archetype_class: list
recommends: calc-column
recommends: external-projection-required
- field: "Defect count"
source: cr3e9_inspectionzoneid → cr3e9_defect (1:many)
cardinality: "1:many"
archetype_class: list
recommends: chained-fetch
recommends: external-projection-required
```

**Hard rule:** if the screen displays a related-entity field but you do NOT emit a `related_entity_fields` block for it, the data-model-architect cannot propose the calc column, the screen-builder will hit `BLOCKED` at scaffold time, and the user will see a `—` cell in the built app. The block is the ONLY signal — there is no fallback inference.
Expand Down
Loading
Loading