Skip to content

Commit 58b6086

Browse files
author
Shubham Agarwal
committed
harden Dataverse derived metadata reconciliation
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3e31c561-a943-4464-92f3-04ab071393c0
1 parent 596602c commit 58b6086

11 files changed

Lines changed: 717 additions & 546 deletions

File tree

plugins/mobile-apps/agents/data-model-architect.md

Lines changed: 19 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -207,56 +207,45 @@ and document their role instead.
207207
## Step 6a — Cross-entity Read Audit
208208

209209
**Print before starting:**
210-
> "→ Auditing planned screens for cross-entity reads (calc-column candidates)…"
210+
> "→ Auditing planned screens for supported cross-entity read paths…"
211211
212212
**Run condition:** execute this step when EITHER (a) `<working_dir>/_screens_section.md` exists at this point in the workflow OR (b) you were invoked with `mode: cross-entity-audit`. **Skip silently otherwise** (default-mode first-pass run, before screen-planner has produced its section) — the orchestrator will re-spawn you in `mode: cross-entity-audit` after Gate 4a/4b lands.
213213

214214
When `mode: cross-entity-audit`, the orchestrator's prompt also includes the path to the existing `_dm_section.md` so you can append (do NOT regenerate it from scratch — Steps 1–6 are skipped in this mode).
215215

216-
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. This step proposes those calc columns based on the screen plan; `/setup-datamodel` (or `/add-dataverse`) Phase 6.1b creates them.
216+
This step exists because the generated SDK has no `$expand`. It classifies each
217+
cross-entity field into a supported formatted lookup or bounded chained fetch.
218+
Dataverse does not support defining calculated/formula expressions through
219+
code, so this audit never proposes generated formula metadata.
217220

218221
**Algorithm:**
219222

220223
1. **Read the screen plan.** Look for `<working_dir>/_screens_section.md` first (graph-only mode after Gate 4a). If absent, parse `<working_dir>/native-app-plan.md` and extract the `## Screens` section. Walk every per-screen spec and collect every `related_entity_fields` block.
221224

222225
2. **Per entry, branch on `recommends`:**
223226

224-
- **`recommends: calc-column`** — first classify the projection instead of assuming every dotted path is a supported formula:
225-
- **Supported calculated projection** — a scalar value reachable only through N:1 lookups and supported by the target Dataverse formula capability. Emit a calc column.
226-
- **Lookup annotation** — the screen only needs the related record's display name already exposed by the lookup. Reuse the lookup annotation; create no column.
227-
- **Materialized projection** — the value must be snapshotted for history/offline use or the formula capability is unsupported. Emit an ordinary column and state which create/update flow owns synchronization.
228-
- **Chained fetch** — 1:many, M:N, aggregate, conditional, or otherwise unsupported navigation. Create no column; the screen-builder fetches it.
227+
- **`recommends: formatted-lookup`** — verify the source is the primary
228+
display name of a direct lookup. Create no column.
229+
- **`recommends: chained-fetch`** — create no column. The screen-builder
230+
performs one bounded related request outside row rendering.
231+
- **`recommends: external-projection-required`** — record a blocker for a hot
232+
list/dashboard field that cannot use the direct lookup annotation. Omit
233+
the field until the user supplies a maker-created formula column or other
234+
server-owned projection.
229235

230-
Confirm the `cardinality` is `1:1` (calc columns CANNOT traverse 1:many or M:N). If cardinality or formula capability is unsupported, choose `materialized projection` only when snapshot semantics are required; otherwise downgrade to `chained-fetch` and add a `DONE_WITH_CONCERNS` note. For a supported calculation, propose a calculated column on the **primary entity of that screen** (the entity its primary `Data` service queries):
231-
- **Logical name:** `<prefix>_<resolved_field>_calc` (lowercased, e.g. `cr3e9_gatename_calc`)
232-
- **Schema name:** PascalCase variant (e.g. `Cr3e9_GateName_calc`)
233-
- **Display name:** human label from the planner's `field` value (e.g. "Gate name")
234-
- **Type:** matches the resolved field's TypeScript type → Dataverse type (`string``Edm.String`, `datetime``Edm.DateTimeOffset`, `decimal` / `money``Edm.Decimal`, `integer``Edm.Int32`, `boolean``Edm.Boolean`)
235-
- **Formula source:** the dotted path from the planner's `source` field, normalized — e.g. `cr3e9_flightid → cr3e9_gateid → cr3e9_gatename` becomes `cr3e9_flightid.cr3e9_gateid.cr3e9_gatename`. The formula is N:1 lookup chain only; no aggregations, no conditionals, no string concat in v0.
236-
237-
- **`recommends: chained-fetch`** — do NOT add any column. The screen-builder handles this at scaffold time per the decision table in `data-performance.md`. Just include the entry in the addendum's `Chained-fetch fields (informational)` row so the user sees what the screen-builder will scaffold.
238-
239-
3. **De-duplicate.** A field driven by N screens (e.g. "Gate name" used on home, list, AND detail) collapses to ONE calc-column row in the addendum. Track all driving screens in the `Driven by` column.
240-
241-
4. **Cap at 20 calc columns per parent entity.** If you exceed, truncate and add a `DONE_WITH_CONCERNS` note — large calc-column counts indicate a denormalization problem that should be solved at the data-model level (probably an extracted entity), not by piling on calc columns.
236+
3. **De-duplicate.** Collapse identical source/resolution pairs and track all
237+
consuming screens.
242238

243239
5. **Emit the addendum.** Write the `### Cross-entity Reads (auto-derived from screen plan)` subsection of `_dm_section.md`. Schema:
244240

245241
```markdown
246242
### Cross-entity Reads (auto-derived from screen plan)
247243

248-
| Calc column | On table | Type | Resolves | Driven by |
249-
|---|---|---|---|---|
250-
| cr3e9_flightnumber_calc | cr3e9_inspection | string | cr3e9_flightid.cr3e9_flightnumber | inspections list |
251-
| cr3e9_gatename_calc | cr3e9_inspection | string | cr3e9_flightid.cr3e9_gateid.cr3e9_gatename | home, inspections list |
252-
| cr3e9_tailnumber_calc | cr3e9_inspection | string | cr3e9_flightid.cr3e9_aircraftid.cr3e9_tailnumber | inspections list |
253-
254-
**Chained-fetch fields (informational — screen-builder will scaffold these, no schema changes):**
255-
256-
| Field | On screen | Cardinality | Source |
244+
| Field | Resolution | Source | Driven by |
257245
|---|---|---|---|
258-
| Defect count | inspection detail | 1:many | cr3e9_inspectionzoneid → cr3e9_defect |
259-
| Inspector email | inspection detail | 1:1 | _ownerid_value → systemuser.internalemailaddress |
246+
| Flight | formatted-lookup | cr3e9_flightid primary display | inspections list |
247+
| Inspector email | chained-fetch | _ownerid_value → systemuser.internalemailaddress | inspection detail |
248+
| Gate code | external-projection-required | cr3e9_flightid → cr3e9_gateid → cr3e9_code | home |
260249
```
261250

262251
In `mode: default` (Step 6a runs because `_screens_section.md` was found), append this subsection to the Step 7 output. In `mode: cross-entity-audit`, append it directly to the existing `_dm_section.md` (read it, append the subsection AFTER `### Notes` if present, otherwise at the end, then write back) and skip Step 7 entirely — return immediately.

plugins/mobile-apps/agents/native-app-planner.md

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -498,13 +498,16 @@ Reject loop = re-spawn `screen-planner` with the user's feedback (layout, screen
498498
### Step 5c — Cross-entity Read Audit (Round 2 data-model pass)
499499

500500
**Print before spawning:**
501-
> "→ Auditing the locked screen plan for cross-entity reads (calc-column candidates from related_entity_fields blocks)…"
501+
> "→ Auditing the locked screen plan for supported cross-entity read paths…"
502502
503-
**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).
503+
**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.
504504

505505
**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.
506506

507-
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.
507+
This step exists because the SDK has no `$expand`. It verifies that every
508+
cross-entity field uses a formatted lookup or bounded chained fetch, and flags
509+
hot-path fields that require an externally supplied projection. It never
510+
synthesizes calculated/formula metadata.
508511

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

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

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

535538
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):
536539

537540
```
538541
## Gate 1 — Addendum: Cross-entity Reads
539542
540-
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.
543+
The screen plan reads N fields from related entities. The generated SDK has no
544+
$expand, so each field must use a formatted lookup, a bounded chained fetch, or
545+
an external server-owned projection.
541546
542-
Proposed calculated columns (auto-derived from your screen plan, no schema reshape):
547+
Proposed read paths:
543548
544549
[paste the ### Cross-entity Reads table from _dm_section.md]
545550
546-
[paste the Chained-fetch fields (informational) table if present — these need NO schema change, the screen-builder handles them at scaffold time]
547-
548-
Approve to add these calc columns to the data model? (Reject → revise the audit. Approve → /setup-datamodel will create them in Phase 6.1b.)
551+
Approve these read paths? Any `external-projection-required` row remains a
552+
blocker until the user supplies that projection outside this workflow.
549553
```
550554

551555
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.

plugins/mobile-apps/agents/screen-builder.md

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -116,26 +116,26 @@ You will be invoked by `/create-mobile-app` Step 11 or `/edit-app` screen-rebuil
116116
```
117117

118118
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.
119-
- **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.
119+
- **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.
120120

121-
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.
122-
2. **No calc column? Branch on screen archetype × cardinality** (archetype is in your spec under `**Archetype:**`):
121+
1. **Follow the planned recommendation** from `related_entity_fields`.
123122

124-
| Archetype | Cardinality | Action |
123+
| Recommendation | Action |
125124
|---|---|---|
126-
| 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. |
127-
| 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. |
128-
| 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. |
125+
| `formatted-lookup` | Select the real `_<lookup>_value` field and render `lookupName(record, '<lookupLogicalName>')`. |
126+
| `chained-fetch` | Perform one bounded related `get` / `getAll` in the screen load step, never inside `map()` or `renderItem`. |
127+
| `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. |
129128

130-
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`.
129+
2. **Verify before exit.** Every UI field in your spec must have either a
130+
primary select, formatted lookup annotation, or bounded chained fetch.
131+
Otherwise return `BLOCKED [<screen_name>]: field <field> requires an external projection`.
131132

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

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

141141
- **HARD RULEserver-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:

0 commit comments

Comments
 (0)