Skip to content

Commit 577ff2d

Browse files
authored
add new plugin to build model-apps (#229)
add new plugin to build model-apps!
1 parent c8455ac commit 577ff2d

209 files changed

Lines changed: 38025 additions & 1991 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,9 +63,13 @@ Create and deploy Power Pages sites using modern development approaches.
6363

6464
### [Model Apps](plugins/model-apps/README.md) (`plugins/model-apps`)
6565

66-
Build and deploy Power Apps generative pages for model-driven apps.
66+
Build model-driven Power Apps end to end, and the generative pages that go in them.
6767

68-
**Stack**: React + TypeScript + Fluent, deployed via PAC CLI
68+
**Skills**: `/app-builder` (**Preview**) builds and edits a whole app — tables, relationships, forms,
69+
views, charts, security roles, app + sitemap — from a natural-language intent; `/genpage` builds
70+
generative pages for an app that already exists. Use either independently — neither requires the other
71+
72+
**Stack**: React + TypeScript + Fluent, deployed via PAC CLI and the headless `cds-maker-sdk`
6973

7074
### [MCP Apps](plugins/mcp-apps/README.md) (`plugins/mcp-apps`)
7175

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
# App-Builder Offline Structural Eval Harness — Guide
2+
3+
> Companion to `evals/model-apps/genpage/EVAL_GUIDE.md`.
4+
> Source of truth: `plugins/model-apps/docs/app-builder-staged-flow-design.md` §13.
5+
6+
## What we evaluate
7+
8+
**Structural per-stage facts** — not `.tsx` snapshots, not live Dataverse state. Each fixture is an App Spec (`app-spec.json`); the runner loads it offline, computes deterministic facts for every build stage, and grades them against assertions in `evals.json`.
9+
10+
### Stage → oracle table
11+
12+
| Stage | Oracle | Plugin primitive |
13+
|---|---|---|
14+
| **author** | `validateAppSpec(plan profile)` passes · spec-lint clean | `app-spec.js`, `spec-lint.js` |
15+
| **plan** | Every planned item targets a known engine phase | `sdk-build.js` `planFor` + `PHASES` |
16+
| **data** | Normalized data-model facts match the `expect.tables/relationships` block | `schema-facts.js` `schemaFacts` |
17+
| **ui** | Normalized view/chart/form facts match the `expect.views/charts` block · enriched **default views** keep parent lookups (#2) and drop `createdon` (#7) · each **sub-grid** is a full-width 1-column section titled by the child display name (#5) | `sdk-build.js` `viewDef` / `chartDef` / `compileFormIntent` / `defaultViewColumns` / `subgridLabel` · `artifact-intent.js` `subgridSectionIntent` |
18+
| **app** | Every sitemap subarea resolves to a concrete target · no dangling `navigatesTo` keys | `sdk-build.js` `appDef` |
19+
| **security** | Each declared persona maps to exactly one role · the role **injects** `appmodule` read for app-access personas (and only those) · every persona privilege on an **app-owned** table (the app's own publisher prefix) resolves to a provisioned entity (JTBD coverage) · the role grants **exactly** its jobs' declared privileges — no extra entity, access token, or inflated scope beyond the declared union + the documented `appmodule` injection (least privilege) | `sdk-build.js` `personaRoleSpecFor` |
20+
| **verify** | Reconcile against a synthetic all-present reader returns `ok: true` | `verify-spec.js` `verifySpec` |
21+
| **generate-pages** | No `PAGEREF_` nav targets unresolved (Plan 3 — degrades to SKIP if absent) | `pageref-resolver.js` `resolvePageRefs` |
22+
| **teardown** | The reverse-of-build delete plan is dependency-safe (solution last · web resources after tables · every table has a step) | `sdk-teardown.js` `planTeardown` |
23+
| **round-trip** | The download→rebuild is lossless — a synthetic deployed read hydrates back the same solution / tables / page-keys / sitemap subareas (incl. classic dashboards) | `hydrate-spec.js` `hydrateSpec` |
24+
25+
## Fixtures
26+
27+
Each fixture lives in `fixtures/<id>-<slug>/` and contains `app-spec.json`.
28+
Naming is numeric-prefix; `fixture-loader.js` matches `^(\d+)(?:-(.+))?$`.
29+
30+
| # | Slug | What it tests |
31+
|---|---|---|
32+
| 1 | `1-support-desk` | Full data/ui/app/verify oracle (no pages) · two **personas** (Support Agent, Support Lead) exercise the **security** oracle — one role each, app-module read injected, JTBD coverage + least-privilege |
33+
| 2 | `2-orders-multipage` | Page intents + navigation + design contract; page-key round-trip |
34+
| 3 | `3-assets-dashboard` | Global choice + column binding, on-click command, and a classic **dashboard** pinned to the nav — exercises teardown (dashboard/command/web-resource/global-choice steps) + the dashboard round-trip |
35+
| 4 | `4-hardening` | The 2026-07-15 review fixes: a lookup-heavy child (8 scalars + a 1:N parent lookup) proves the default view keeps the lookup (#2) and drops `createdon` (#7); an N:N proves the alphabetically-sorted schema name `new_tag_new_ticket` (#3); a no-label sub-grid proves the own-section + pluralName title (#5); relational sample data proves `validateAppSpec` accepts a resolvable `$parent` match and declared Choice labels (#1/#4) |
36+
37+
> **Fixture 2 note:** `appShell.subAreas[].page` references use the page's **key** (e.g. `"overview"`). For `schemaVersion: 2`, `validateAppSpec` validates `sa.page` against `pages[].key`, while `lintAppSpec` validates against `pages[].name`. Setting `p.key === p.name` (lowercase identifiers) satisfies both validators without modifying plugin code.
38+
39+
## `evals.json`
40+
41+
- `skill_name` — identifies this suite.
42+
- `eval_instructions` — description used by eval runners.
43+
- `common_stage_assertions` — run for every fixture; registered in `lib/assertions.js`.
44+
- `evals[].expect` — per-eval expected counts/names (tables, rels, views, charts, pages).
45+
- `evals[].expectations` — additional per-eval assertion texts (can be empty).
46+
- `evals[].tier``smoke` (fast subset) or `full`.
47+
48+
## Running
49+
50+
From the **repo root** (`evals/` lives there, sibling to `plugins/`):
51+
52+
```bash
53+
# All fixtures, TAP v13 output; exit 0 = all pass, 1 = fail, 2 = harness error
54+
node evals/model-apps/app-builder/run-app-builder.js
55+
56+
# Specific fixture
57+
node evals/model-apps/app-builder/run-app-builder.js --eval 1
58+
59+
# Smoke tier only
60+
node evals/model-apps/app-builder/run-app-builder.js --tier smoke
61+
62+
# Unit + e2e tests (node:test, NOT part of the plugin run-tests.js)
63+
node --test evals/model-apps/app-builder/tests/*.test.js
64+
```
65+
66+
## TAP output
67+
68+
```
69+
TAP version 13
70+
1..2
71+
# Subtest: 1-support-desk
72+
ok 1 - author: validateAppSpec(plan profile) passes with no errors
73+
ok 2 - author: spec-lint reports no errors
74+
...
75+
ok 1 - 1-support-desk
76+
# Subtest: 2-orders-multipage
77+
...
78+
ok 2 - 2-orders-multipage
79+
# tests 20
80+
# pass 20
81+
# fail 0
82+
# skip 0
83+
# fixtures 2 (pass 2, fail 0)
84+
```
85+
86+
## Adding an eval
87+
88+
1. Create `fixtures/<N>-<slug>/app-spec.json`.
89+
2. Add an entry to `evals.json` with matching `id`, an `expect` block, and any extra `expectations`.
90+
3. If you need a new assertion, add it to `lib/assertions.js` (text must match exactly).
91+
92+
## Adding an assertion
93+
94+
Register the check in `lib/assertions.js`:
95+
96+
```javascript
97+
ASSERTIONS.set('my-stage: my assertion text', ({ facts, spec, eval: ev }) => {
98+
if (someConditionNotMet) return { status: 'fail', reason: 'explains why' };
99+
if (notApplicable) return { status: 'skip', reason: 'explains why skipped' };
100+
return { status: 'pass' };
101+
});
102+
```
103+
104+
Then add the text to `evals.json` `common_stage_assertions` (applies to all) or `evals[].expectations` (per-eval).
105+
106+
## Page oracle graceful degrade
107+
108+
`pageref-resolver.js` (Plan 3) is loaded inside a `try/catch`. If it's absent:
109+
- `pageFacts(spec)` returns `null`.
110+
- The `'generate-pages: …'` assertion emits `SKIP` for all fixtures.
111+
- No other assertion is affected.
112+
113+
## Live evals
114+
115+
The live tier (`plugins/model-apps/scripts/smoke-eval.js`) is the thin live smoke; a multi-page live eval case is a follow-up. These offline evals are complementary: they grade structural facts (no live env needed) while the live smoke grades real Dataverse provisioning.
116+
117+
## Cross-links
118+
119+
- Plugin `AGENTS.md`*Eval Suite*
120+
- `plugins/model-apps/docs/app-builder-staged-flow-design.md` §13 — structural eval oracles
121+
- `evals/model-apps/genpage/EVAL_GUIDE.md` — the parallel eval suite for `/genpage`
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
{
2+
"skill_name": "app-builder",
3+
"eval_instructions": "Offline, deterministic structural evals for /app-builder. See EVAL_GUIDE.md. Each eval's fixture is an App Spec (app-spec.json); the runner computes per-stage facts (author/plan/data/ui/app/verify/page) from the plugin's pure primitives and grades them against the common_stage_assertions + per-eval expect block. No live env, no network.",
4+
"common_stage_assertions": [
5+
"author: validateAppSpec(plan profile) passes with no errors",
6+
"author: spec-lint reports no errors",
7+
"plan: every planned item targets a known engine phase",
8+
"security: the plan authors exactly one role per declared persona",
9+
"security: each app-access persona role injects app-module read; opted-out personas do not",
10+
"design: every mapped persona job resolves to a surface that exists",
11+
"design: every custom table icon is described by what it depicts, not a token name",
12+
"security: every persona privilege on an app-owned table resolves to a provisioned entity",
13+
"security: each persona role grants exactly its declared job privileges (no over-grant)",
14+
"data: schema-facts provision exactly the expected tables",
15+
"data: schema-facts provision exactly the expected relationships",
16+
"ui: wire-facts build exactly the expected views and charts",
17+
"app: every sitemap subarea resolves to a concrete target (no dangling entity/page/dashboard)",
18+
"app: every navigatesTo target resolves to a known page key",
19+
"verify: reconcile against an all-present reader returns ok with no missing",
20+
"generate-pages: no PAGEREF_ navigation target is left unresolved",
21+
"teardown: the solution container is deleted last",
22+
"teardown: every declared table has a teardown step",
23+
"teardown: web resources are deleted after tables",
24+
"round-trip: download hydrate recovers the same solution and tables",
25+
"round-trip: the sitemap round-trips with the same subareas",
26+
"round-trip: generative pages preserve their keys",
27+
"teardown: every declared dashboard has a teardown step",
28+
"teardown: every declared command bar has a teardown step",
29+
"ui: enriched default views keep parent lookups and drop createdon",
30+
"ui: each sub-grid is a full-width section titled by the child display name"
31+
],
32+
"evals": [
33+
{
34+
"id": 1,
35+
"tier": "smoke",
36+
"spec": "app-spec.json",
37+
"prompt": "Support desk: customers, tickets, and comments.",
38+
"expect": {
39+
"tables": [
40+
"new_customer",
41+
"new_ticket",
42+
"new_comment"
43+
],
44+
"relationships": [
45+
"new_customer_new_ticket",
46+
"new_ticket_new_comment"
47+
],
48+
"views": [
49+
"Active Customers",
50+
"Active Tickets",
51+
"Active Comments"
52+
],
53+
"charts": [
54+
"Tickets by Priority",
55+
"Tickets by Status"
56+
]
57+
},
58+
"expectations": []
59+
},
60+
{
61+
"id": 2,
62+
"tier": "full",
63+
"spec": "app-spec.json",
64+
"prompt": "Orders app with an overview page that navigates to an order-detail page.",
65+
"expect": {
66+
"tables": [
67+
"new_customer",
68+
"new_order"
69+
],
70+
"relationships": [
71+
"new_customer_new_order"
72+
],
73+
"views": [
74+
"Active Orders"
75+
],
76+
"charts": [
77+
"Orders by Status"
78+
],
79+
"pages": [
80+
"overview",
81+
"order-detail"
82+
]
83+
},
84+
"expectations": []
85+
},
86+
{
87+
"id": 3,
88+
"tier": "full",
89+
"spec": "app-spec.json",
90+
"prompt": "Assets app with a shared status choice, an on-click command, and an operations dashboard pinned to the nav.",
91+
"expect": {
92+
"tables": [
93+
"new_asset"
94+
],
95+
"relationships": [],
96+
"views": [
97+
"Active Assets"
98+
],
99+
"charts": [
100+
"Assets by Status"
101+
]
102+
},
103+
"expectations": []
104+
},
105+
{
106+
"id": 4,
107+
"tier": "full",
108+
"spec": "app-spec.json",
109+
"prompt": "Hardening: a lookup-heavy ticket table, an N:N to tags, a sub-grid, and valid relational sample data — exercises fixes #1/#2/#3/#4/#5/#7.",
110+
"expect": {
111+
"tables": [
112+
"new_customer",
113+
"new_ticket",
114+
"new_tag"
115+
],
116+
"relationships": [
117+
"new_customer_new_ticket",
118+
"new_tag_new_ticket"
119+
],
120+
"views": [
121+
"Open Tickets"
122+
],
123+
"charts": []
124+
},
125+
"expectations": [
126+
"the ticket default view keeps new_customerid even though the table has 8 scalar columns (#2)",
127+
"no enriched default view carries createdon (#7)",
128+
"the N:N relationship name is the alphabetically-sorted new_tag_new_ticket (#3)",
129+
"the customer form's ticket sub-grid is a 1-column full-width section titled 'Tickets' from the child pluralName (#5)",
130+
"validateAppSpec accepts the relational sample data: the $parent match resolves and the Choice labels are declared (#1/#4)",
131+
"quick create is enabled on the new_ticket table (#8)"
132+
]
133+
}
134+
]
135+
}

0 commit comments

Comments
 (0)