What the /app-builder skill can do today, what's still pending (grouped by phase), and what's
intentionally deferred (with the why). The build engine is scripts/lib/sdk-build.js; the App Spec
contract is ../references/app-spec-schema.md; the wiring diagrams
are in architecture.md.
Status legend
- ✅ verified live — built end-to-end on a real Dataverse env and torn down clean.
- 🧪 tested — has automated (unit / golden) coverage, not exercised on a live org.
- ⚠ not live-verified — plumbed through and unit-tested, awaiting a live shakeout.
- Interactive levelled authoring in the main loop — jobs-to-be-done first, then data model, artifacts + page-intents, then access (env select, App Spec, lint gate, plan-mode approval).
- Deterministic, idempotent build engine — discovers via the SDK (
findTables/findColumns/fetchEntityMetadata) and creates only what's missing; new / existing / mixed envs all work. - All Dataverse access via the vendored headless SDK; metadata cached under
<app-folder>/.maker-workspace/for reuse. - Phase selection (
--only/--skip/--from/--to),[n/total]narration,BuildHaltgate, dry-run by default,--sample-data/--publishopt-in. - Bounded-concurrency for independent ops; one publish round-trip per entity + the app.
az-token HttpClient with transient (429 / 5xx) retry. - Guardrail lint (
spec-lint.js) + hard validator (app-spec.js). 🧪 fullnode:testsuite + the vendored SDK's Jest suite green (node scripts/run-tests.js --with-sdk <ppux>).
- All column types: Text · Memo · Choice · MultiChoice · Boolean · Money · DateTime · Integer · BigInt · Decimal · Double · File · Image · AutoNumber · Customer (with per-type options), incl. AutoNumber primary columns.
- Global option sets, status reasons (idempotent, deterministic pinned values), alternate keys (idempotent).
- Relationships: OneToMany (lookup) and ManyToMany; Customer (polymorphic) columns.
- Sample data: Choice/MultiChoice label→int resolution (inline and global choices),
$parent/$parentslookup binds (incl. junction rows with both sides), custom status reasons; resolve-by-name idempotency via the SDK'sseedRecordGraph. - ⚠ Calculated / Rollup formula columns —
source+formulaplumbed through, not live-verified. - Table icons (
entities[].vectorIcon= SVG web resource →IconVectorName;entities[].icon= raster web resource →IconMediumName) — sets a custom table's own icon (what the modern designer + nav render). Applied after the web-resources phase via the SDK'ssetEntityIcon; hard-validated against declared web resources so an unresolvable value can't break the designer (glimmer). Live-verified:IconVectorNameset to a published SVG web resource.
- Adaptive main forms (auto + explicit tabs/sections), related-record sub-grids (1:N and N:N), Notes/timeline section.
- Quick-create + quick-view forms (
forms[].formType); quick-view placement on a host form via a lookup (forms[].quickViews[]). - Form JS event handlers (
onload/onsave/onchange) wired via web resources. - Views with rich filters (
eq-userid/this-week/in/not-in/… + Choice-label resolution); default Active/Inactive view column enrichment via the SDK'senrichDefaultViews. - Choice-column charts.
- App module + sitemap; multi-area sitemaps — every
appShell.areas[]maps to its own<Area>(icon + groups + subareas; order follows array order). The app is self-contained for export/import: its sitemap is added to the solution (componenttype 62), and its tile icon is an in-solution web resource —app.icon(a declared image web resource) or a generated default SVG — never an arbitrary external/managed icon. - Generative pages (genpage-first) for overview / dashboard surfaces — uploaded via
pac model genpage upload; the SDK finalizes the sitemap withGenPagesubareas. - Dashboards (chart / list / iframe / webresource tiles) with sitemap placement (auto-pinned as an app component). Tiles render in a multi-column grid (2-wide) rather than one stacked full-width column.
- Modern command-bar buttons — functional JS on-click + static hidden/disabled, incl. flyout / split-button menus.
- Web resources (JS / HTML / CSS) shipped + added to the solution; idempotent (reuse by name).
- Three-authority page identity. IDENTITY = durable
<app>_pagemanifest(key→pageId map), outranked by the spec's ownpages[].pageIdfor a downloaded (edit-snapshot) spec. EXISTENCE = env-widepac model genpage list— decides create-vs-reuse for crash-safe convergence (enumerateEnv, no--app-id). MEMBERSHIP = the app's sitemapGenPageIdset, read fail-closed viafetchSitemap(scripts/lib/sitemap-pages.js) — drives placement, download enumeration, and verify. All page matching is by id, never by display name. - Edit-snapshot
pageId. Download keeps each page's deployedGenPageIdaspages[].pageIdin the emitted spec. On rebuild, this highest-authority id is confirmed against EXISTENCE and reused — so a downloaded app (including Maker-added pages) rebuilds without creating duplicates. - Validation: every page must be sitemap-placed. A
pages[]entry absent from theappShellsitemap is rejected; navigation-only (headless) pages are not supported. A "detail" page is a normal sitemap page receiving input viapageInput. - Safety HALTs:
pages-removed(live page dropped from spec — re-add or--allow-destructivedetaches the nav SubArea, page left deployed),pages-shared-across-apps(--allow-destructivedoes NOT bypass; detach in Maker),pages-identity-conflict,pages-manifest-corrupt,pages-existence-failed,pages-sitemap-read-failed,pages-shared-check-failed. - Verify and download match by id with exact set-equality (EXISTENCE + MEMBERSHIP); a manifest-uncorrelatable page surfaces
unableToRun.
aiblock →ai-featuresphase: form-fill, NL search, NL charts, M365 Copilot, and per-table Copilot row summaries with tailoredGptDynamicPrompt-2prompts (auto-selected candidate tables; skips lookup/config/junction + D365-owned incident/lead/opportunity).- Admin-gated: preflights each setting (
RetrieveSetting), skips/warns when off, never fails the build. NL grid search is environment-gated (EnableNLGridSearch), not per-app. Standalone reporterscripts/ai-preflight.js.
download-model-app.jspulls a deployed app back into an editable App Spec (+ page code, icons, referenced entities, and the app's real unmanaged solution —recoverAppSolutionenumerates the app's solution memberships and excludes the built-inActive/Default/Basicsystem solutions, so the spec names the right container for a later clean teardown); edit the spec and re-run the idempotent build — create and edit share one path (reuses app/tables, updates pages in place, keepsGenPagesubareas). The app-shell phase re-syncs the sitemap + components of any existing app (fetch → recompute-from-spec → push → publish), so subarea add/rename/reorder edits land for page-less apps too — not just generative-page apps — and--only app-shellcan force the rewrite. Classic DashBoard subareas round-trip too: the dashboard is reconstructed intodashboards[]with id-passthrough tiles (each tile carries the deployed view/chart ids), so a rebuild recreates it against the existing views/charts without re-declaring them. Round-trip scope (not yet "complete"): tables, sitemap/appShell, generative pages, dashboards, icons, and solution round-trip; forms, views, charts, and commands do NOT yet — view hydration was tried and reverted (LIVE-verified the deployed savedquery set can't reliably distinguish author views from Dataverse's auto-generated Active/Inactive/QuickFind system views). All four survive on the live app (a rebuild preserves them by discovery) but are absent from the downloaded spec, so edit them in Maker or a fresh spec.- Live regression on the edit path found + fixed 4 bugs, then re-verified clean.
verify-model-app.js— read-only reconcile of spec vs deployed (exits non-zero on anything missing). Sitemap checks are element-scoped: an area/subarea icon is matched on its own<Area>/<SubArea>element, and a dashboard subarea is verified by resolving the dashboard id (systemform type 0, by name) and matching the sitemap'sDefaultDashboard— so a value reused elsewhere can't produce a false pass. Multi-area sitemaps and the dashboard-subarea path were re-verified live (positive + negative).
teardown-model-app.jsdeletes exactly what an App Spec declares, in dependency-safe order (app → dashboards → commands → forms → charts → views → relationships → AI row summaries → tables [children-first] → web-resources → global choices → solution). Forms/charts/views/relationships are removed before tables (a table delete doesn't reliably cascade cross-references); web resources are removed AFTER tables (a table's icon web resource is referenced by the table).- Classifier-safe (every id resolved from a spec-declared name via an exact-match, entity-scoped filter), dry-run by default, best-effort continue, not-found aware, undeletable (system/managed) artifacts recorded as
skipped. A restricted system solution (Active/Default/Basic) is skipped rather than attempted (Dataverse 400s any delete of one), so a downloaded spec that defaulted its solution toDefaulttears down cleanly. An already-gone relationship (Dataverse 400 "…but 0 were found") is tolerated as deleted, like the table not-found case.
- ASCII form wireframe preview (
preview-form.js); phase-grouped build log with per-step status glyphs (✓/⊘/✗) + a closing summary. - SDK consolidation — the SDK owns the Dataverse mechanics (
seedRecordGraph,enrichDefaultViews, AI settings/row-summaries, artifactresolveArtifact/findArtifact/deleteAppCascade); the plugin keeps the judgment (spec validation, choice/status resolution,$parent→bind translation, candidate selection).
Evaluated 2026-07-28 against powerplatform-modelpages-ade/docs/topics/ModelBuilder/MDA-Agentic-Authoring-spec.md
(§3 MVP definition + §5 stack rank). 8 of 10 core P0 primitives are ✅ complete — data model + sample data
- auto-number + dedup, Active/Inactive + authored views, main forms, charts, gen-page landings, custom SVG sitemap
icons, in-app agents (
ai.appFeatures= formFill/nlSearch/nlChart), Insight Card summaries (ai.summaries), and the dedup/verify quality gates. The open MVP items:
- 🔲 Wave 2 (header/navigation refresh) enabled by default (spec rank 3, P0) — set the app-module header/nav refresh flag on new apps, and turn it on when editing an app that isn't on it yet (spec rank 20). Not set today.
- ✅ Roles + JTBD as first-class planning outputs (spec rank 1, P0) — the Level-(c) design flow now models
personas and their jobs-to-be-done: the author declares the entity access each job needs, the builder
unions it into one security role per persona, and the plan/preview surfaces the proposed roles for review. Built
on the SDK security surface (
createPersonaRole). JTBD-driven view/summary/sitemap coherence (below) still builds further on this. - 🔲 Default-on + coherence wiring — the AI agents, Insight Card summaries, and (once added) Wave 2 exist as primitives, but the SKILL flow must enable them by default and author JTBD-quality content (entity-specific summary prompts per the §4 Group G guidelines, the right view columns). A ✅ primitive is necessary but not sufficient for the spec's coherence bar.
Important, P1 (not MVP-gating; tracked here for visibility):
- ✅ Security roles per persona (spec rank 14, Group N P1) —
personas[]authors one security role per persona, sized from its jobs-to-be-done, and grants the app to each role so it opens for non-admins, not just sysadmins. Idempotent + converging (replace-privileges), fail-closed on a foreign same-name role, torn down with the app. Column-level (field) security and access teams / hierarchy security (Group N P3) remain a tracked SDK follow-up. - 🔲 Rich AI descriptions on every artifact (rank 22) · quick-find / relevance-search config (rank 23) ·
custom app theme + logo (rank 18 — the
designblock styles gen pages, not the app theme).
P0.5 stretch (not built): modern grid visualizations by default (rank 11, contingent on the SDK grid-customizer)
and MCP server + Catalyst by default (rank 12). A status-annotated copy of the spec's §5 stack rank lives on
branch users/akmaloo/mda-spec-impl-status in powerplatform-modelpages-ade.
- 🔲 Delta / diff-based edit (full desired-state convergence) — today the build is additive: it creates what's missing and reuses what exists, but does NOT re-apply an edit to an already-deployed artifact (changed column type, removed view column, edited form/command/dashboard) and never removes an artifact dropped from the spec. The converging edit path today is teardown + rebuild-fresh;
--verifynow catches unapplied edits via content checks (view column set, relationship + command existence) so divergence is loud, not silent. The future increment: spec-diff against the deployed app and apply only the changed delta (SDKupdateColumn/deleteColumn/updateTable/deleteRelationship/updateWebResource,fetchArtifactsnapshots,diffArtifact) instead of a full re-apply — driven by real edit-workflow usage. (Deliberately NOT a per-resourcemanaged/additive/referencemode system: that's backward-compat machinery for a prod tool; this is pre-prod, so convergence-by-default is the likely direction when it lands.) - 🔲 Form events on existing forms — fetch an existing form, add/replace handlers, publish (current wiring assumes a freshly built form).
- 🔲
--dry-rundiff view — show the spec-vs-deployed delta before apply (precursor to delta-based edit).
- 🔲 Business rules (
businessRules[]) — ⚠ org-gated + Power-Fx-flavored; build behind a capability flag (see Deferred). - 🔲 Standard system views (All Records / Active / Inactive / Lookup / Associated) auto-generated per table.
- 🔲 Security roles, environment variables, connection references.
- 🔲 Solution packaging —
exportSolution/importSolutionfor hand-off / source control (managed / unmanaged).
- ✅ Global option-set find-by-name — DONE. The vendored
createGlobalOptionSetis now IDEMPOTENT (probeGlobalOptionSetDefinitions(Name=…)→ REUSE the existingMetadataIdinstead of a duplicate-NamePOST).entity-provision.jscaptures the returned id on a rebuild so aglobalChoicecolumn binds to it (globalChoiceMetadataId) instead of falling back to inline options; a genuine create failure now surfaces as a clean phase halt (previously ALL errors were swallowed). Locked byvendor-sdk-smoke.test.js(idempotent reuse, no duplicate POST) +entity-provision.test.js. - 🔲 Solution-component idempotency — query existing solution components instead of assuming reused web resources are already present.
- 🔲 Live-verify Calculated / Rollup formula columns end-to-end.
- 🔲 Publish granularity — optionally publish web resources separately from entity customizations.
- 🔲 Richer
BuildHaltrecovery — skip-phase / retry-step / edit-spec-and-resume prompts. - ✅ Entity-subarea sitemap icons round-trip — DONE. The sitemap writer no longer DROPS a custom
vectorIconon an entity nav subarea, and validation no longer REJECTS a platform/OOB icon reference the download itself wrote. New sharedisPlatformIconRef(a value starting with/or$webresource:is a platform reference the platform resolves directly, vs a bare declared-web-resource NAME):checkIconpasses platform refs through (a bare NAME still must be a declared image WR);subAreaJsonemits a valid platformvectorIconon ANY subarea incl. Entity (a bare Fluent token is still dropped — it breaks the modern app-designer — but now with awarnings[]entry, not silently) and preserves icon paths VERBATIM (case-sensitive; only bare names are lower-cased);collectSitemapskips platform-path icons. So download→edit→rebuild on a real app with custom/OOB nav icons no longer fails validation or silently loses the icon. Adversarially reviewed (Sol + Opus — Opus caught a residual area-icon case-corruption, fixed); live-verified on aurorabapenv03468 (entity-subareaVectorIconlands in the deployed sitemap XML; the reporter's exact OOB…/CDSEntityicon rebuildsok:true; round-trip clean). Follow-up (now resolved — see below): deploying a/WebResources/<pub>/icons/x.svgreference to a DIFFERENT env originally rendered a broken icon (the WR was assumed pre-existing). - ✅ Custom nav-icon web resources are portable across environments — DONE. Resolves the follow-up
above. A sitemap nav icon that references a custom image web resource by path
(
/WebResources/<pub>/icons/x.svgor$webresource:<name>) previously round-tripped only the reference, so a rebuild into a fresh env rendered a broken icon. Now download re-declares the referenced web resource intowebResources[](base64 content) so the build recreates it cross-env — gated to be safe: only a WR that is (a) owned by this app (name starts with the app's real publisher prefix — see the identity round-trip below), (b) custom (unmanaged), and (c) an image type. A path ref processed BEFORE bare names so an overlap keepsexternal; a foreign-prefix / managed / OOB reference is left as a bare reference (recreating a foreign prefix on a fresh env would BuildHalt). A re-declared entry is flaggedexternal: true: the idempotent web-resources phase creates-if-missing / reuses-if-present, and teardown skipsexternalWRs (including the derived generated-icon/manifest cleanup, which now honors a declared-name collision) so a shared publisher WR is never deleted (fail-safe, mirrorsexisting: trueon downloaded tables). An own-prefix icon that can't be captured (absent on source, or read failure) is surfaced as a download warning; the build adds a non-blocking portability warning for an own-prefix path-ref icon missing fromwebResources[]. Adversarially re-reviewed across four rounds (Sol + Opus, high effort); full offline coverage (re-declare / skip-foreign / skip-managed / overlap-external / unresolved + teardown-skip). - ✅ App identity round-trips by its real uniquename (no duplicate app on rebuild) — DONE. A downloaded
spec captures the app module's real, immutable
uniquenameinapp.uniqueName;appUniqueName(spec)(the single identity chokepoint for build's existing-appfindArtifactAND teardown's app resolution) returns it verbatim instead of re-deriving<publisherPrefix>_<appName>from the mutable display name. So renaming an app then rebuilding no longer misses the existing app and creates a duplicate. The publisher prefix (which scopes safe-to-re-declare custom icons and recreates the app's own solution) is derived from the app's own uniquename — parsed before the solution-recovery I/O (a failure can't lose it) and validated as a real<prefix>_<name>shape — not from an arbitrary unmanaged solution membership (an app can belong to several, under different publishers).recoverAppSolutionreturns only the real solution uniquename (teardown container); the transient prefix-trust signal is stripped from the persisted spec. This resolved 4 adversarial-review findings (3 High + 1 Medium, Sol); full offline coverage (identity-over-rename, prefix-from-uniquename-not-solution, malformed-shape rejection). - ✅ Pre-existing duplicate page names don't block an unrelated build — DONE. Two-layer, fail-closed:
(1)
validateAppSpecscopes the case-insensitive page-name uniqueness rule to pages the run CREATES (nopageId): a NEW collision is rejected (prevention), a collision purely among PRE-EXISTING pages (all carry a deployedpageId) is a warning (tolerated) so a form-only edit on a downloaded app with two identically-named pages still builds. (2) Because a specpageIdis only a CLAIM, the pages phase re-checks afterreconcilePageIds: if a tolerated dup-name group has any member whose id reconciles as ABSENT (a stale snapshot — e.g. the page was deleted in Maker since download), the build would re-materialize the duplicate, so it HALTs (pages-duplicate-name-create) BEFORE any write instead of creating it and only failing verify after. The pages phase is otherwise id/key-matched (never name-matched —genpage-cli.jsenumerateEnvis id-keyed), so distinct-id same-name pages build fine.validateAppSpecgained awarnings[]channel;build-model-app.jsnarrates + attaches them. Follow-up: enforce name-uniqueness at/genpage-native page creation too (the app got into the dupe state via non-app-builder tooling). - ✅ Same-named forms (Main/Quick View/Card) no longer block a form edit — DONE. A Dataverse form name
is unique only per
(entity, type), so a table's auto-created Main (2), Quick View (6), and Card (11) forms are commonly ALL named "Information". The build resolved a form by(entity, name)only, so a name-only lookup matched multiple rows → the SDK'sAmbiguousArtifactError→ the fail-closed preflight HALTED, blocking aformType:"Main"edit entirely (author had to hand-patch formxml by id). New sharedresolveExistingFormIdscopes by TYPE (FORM_TYPE_CODE→type eq <code>) so a Main edit reconciles ONLY the Main form; applied CONSISTENTLY across the form phase (buildArtifact), the preflightdiscoverOpDiffState, the spec verifier (verify-spec.jsno longer false-passes a Main when only a same-named Quick View exists), and teardown (which previously deleted EVERY same-named match — a data-loss risk on same-named Quick View / Card siblings; now deletes exactly the intended form, skips on absent, halts on a residual collision). The residual(entity, type, name)collision errors with actionable guidance; new optionalforms[].formIdpins the exact form (GUID-validated invalidateAppSpec+ re-checked at resolve — the id is interpolated unquoted into an Edm.Guid filter — and verified to match the target table, TYPE, AND name; a stale pin fails loud instead of minting duplicate forms). Quick-view form references are keyed by(targetEntity, QuickView, name)so same-named Quick View forms on different entities — or a same-named Main on the target entity — don't cross-wire (build map,validateAppSpec,spec-lint, and teardown dependency ordering all consistent); verify reusesresolveExistingFormIdfor the same identity.FORM_TYPE_CODE/FORM_GUID_RElive in the sharedapp-spec.js.artifactIdentityQuery('form')is also type-scoped. Reviewed adversarially across three rounds (Sol — the name-only-identity flaw was pervasive; all High/Medium findings fixed). Live-reprozava_javavendor(three "Information" forms) unblocked; live-verified end-to-end on aurora (build a table → it gets 3 same-named "Information" forms → edit the Main without a halt → the edit lands on the Main ONLY, Quick View/Card untouched → teardown resolves the Main type-scoped). Locked by resolver / preflight / verify / teardown / validation unit tests. Follow-up: a quick-view control already cross-wired by the OLD global-name bug is not auto-repaired on a rebuild (hasQuickViewmatches only the lookup field, not the embedded form id) — a rare pre-existing-state edge that self-heals on teardown + rebuild. - ✅ Env-wide appmodule pagination — DONE. The vendored
queryRecords({ paginate:true })now follows the OData@odata.nextLinkto completion. The cross-app shared-page scan (sitemap-pages.jsfetchAppsForPages) enumerates EVERY app instead of one ~5000-row page, so it no longer fails closed at the cap (apps-truncatedremoved). Fail-closed is preserved on: an enumeration throw, a pagination-abort (repeated-nextLink guard), AND an empty result (a live env always has ≥1 appmodule, so empty ⇒ a malformed/partial read ⇒apps-enumeration-emptyhalt). Locked byvendor-sdk-smoke.test.js(paginate follows nextLink; rejects paginate+top / paginate+fetchXml) +sitemap-pages.test.js. - ✅ Paginate throw-on-malformed-page (SDK hardening) — DONE. The vendored
queryRecords({ paginate:true })now THROWS aConnectionError(carrying the page URL + HTTP status) when a paginated page'svalueis not an array (missing/null/object — previously coerced to empty, silently dropping rows; a bare-stringvaluewas iterated char-by-char, fabricating single-char rows) or when@odata.nextLinkis a truthy non-string. The single-page (non-paginate) path stays lenient;queryAllRecordsinherits the guard by delegation. The cross-app scan'sfetchAppsForPagestry/catch catches this → fail-closed at the source (the plugin's empty-enumeration guard remains as defense-in-depth). Locked byvendor-sdk-smoke.test.js("THROWS on a malformed page"); re-vendored fromcds-maker-sdkhardening-3 (f066a644). - 🔲 Conditional
updateTable(If-Match / skip-if-unchanged) — the SDK'supdateTabledoes an unconditional GET→PUT of the wholeEntityDefinitionsrow (strips@odata.etag, noIf-Match), so a concurrent Maker edit to another property of the SAME table in the GET→PUT window is last-writer-wins. This is pre-existing (icons/audit already useupdateTable); the quick-create flag adds one more caller. Follow-ups: preserve the ETag + conditional PUT (retry/surface 412), and skip the PUT when the requested flag is already set (avoids a redundant write on every opted-in rebuild). Same class as the build'srequireSuccessfulPush412 posture for artifacts.
Source: IMPROVEMENTS-07-15-app-builder.md (Project Management V1/V2 diff + a sample data-load). Status 2026-07-27: all 8 addressed — the "Allow quick create" table flag (#8) now ships; auto-GENERATING the Quick Create form's field layout remains a follow-up (see below). Severity from the source doc.
- ✅ [High] Validate lookup binds; stop silent data-load lookup failures — DONE.
validateAppSpecnow validates$parents(junction) the same as$parentand flags a$parent/$parents.matchthat resolves to no parent sample row (the bind would be dropped and the lookup left unset);buildSeedGroupTHROWS (fail loud) on an unresolvable parent instead of silently skipping. Runs insiderunner.run(clean phase failure).app-spec.jssampleData validation +entity-provision.jsbuildSeedGroup. - ✅ [Medium] Don't truncate parent lookups in default-view enrichment — DONE.
defaultViewColumnsnow reserves the parent-lookup slots up front and caps scalar columns at the remaining budget, then appends every lookup — so a lookup-heavy table never drops a parent link (sdk-build.js). Teardown's{ includeLookups:false }reset path unchanged. - ✅ [Medium] Normalize N:N relationship schema-name ordering — DONE.
manyToManySchemaNamesorts the two entity logical names alphabetically before composing, so the N:N name is stable regardless ofentity1/entity2declaration order (app-spec.js). 1:N keeps its semanticreferenced_referencingorder; explicitschemaNamestill wins. - ✅ [Medium] Resolve Choice values; lint sample data — lint part DONE.
validateAppSpecflags a Choice/MultiChoice sample value (per comma token for MultiChoice) that is not a declared option label; raw numeric option values still pass. (The live-metadata label→int resolution at load time is still spec-positional — tracked separately if cross-env option drift becomes a real problem.) - ✅ [Medium] Sub-grid placement + titling — DONE. Each sub-grid now lands in its own 1-column full-width section (
subgridSectionIntent+firstColumnSectionsPointer) instead of a half-width cell in a field section, and the title defaults to the child'spluralName→displayName(not the logical name), withforms[].subgrids[].labeloverriding (sdk-build.jsforms phase). - ✅ [Medium] Handle the stock "Information" form — DONE (opt-in). New
forms[].deactivateOtherMainFormsflag: when set, after promoting our form default the build deactivates every OTHER active main form on the entity (the stock Information form). OFF by default; gated to our own custom table; a flagged form must be the only Main form declared for its entity, and deactivation is skipped if the promote failed. Teardown'srestoreStockMainFormreactivates a stock main form to ENABLE deletion — a delete-enabler, not a faithful restore of pre-build activation state (see the deferred follow-up below).promoteDefaultForminsdk-build.js. - ✅ [Low–Med] Drop "Created On" from enriched default views — DONE (already the SDK's behavior; now locked). Traced the vendored
enrichDefaultViews:updateElement('/columns')is a REPLACE (not a union) and the view serializer reconciles the fetchxml+grid to exactly our column set (which never containscreatedon), removing the stock Created On. Locked with a real-bundle regression test (default-view-createdon.test.js). - ✅ [Medium] Enable "Allow quick create" on key tables (#8, flag) — DONE. The vendored
updateTablenow supportsquickCreateEnabled(→IsQuickCreateEnabled), so the build enables "Allow quick create" on a table when the spec opts in — either an explicitentities[].quickCreate: trueOR an authoredformType: "QuickCreate"form (auto-derived viaquickCreateEnabledFor, so an authored Quick Create form is actually reachable from the inline "+ New"). Idempotent (applied to fresh AND reused tables).entity-provision.jsdata-model phase +planForstep; locked byvendor-sdk-smoke.test.js(PUT sendsIsQuickCreateEnabled),entity-provision.test.js,app-spec.test.js, and eval #4. Follow-up (open): auto-GENERATE the Quick Create form's field layout (primary + required + parent lookups) for lookup-target / sub-grid-child tables so the maker doesn't hand-author it — the flag + a hand-authoredformType:"QuickCreate"form work today; the auto-layout does not yet. - ✅ [Low–Med] Authored form column full width — DONE (SDK safety net). hardening-3's
FormAdapter.normalizeColumndefaults a synthesized (authored) column'swidthto100%when undefined, so a reconcile-added field can never emit invalid formxml ("required attribute 'width' is missing"). The plugin still sets width explicitly for exact control; locked byhardening2-real-bundle.test.js(a width-stripped authored column serializeswidth="100%").
- 🔲 Planner enrichment — proactively propose the full surface (status model, dashboard, validation, default views, security), not just forms/views/charts.
- 🔲 Form-JS scaffolding — generate small, real onload/onchange handlers from intent (e.g. "warn when priority is High") instead of empty stubs.
- 🔲 Spec templates — domain starters (support desk, CRM, asset tracking) as one-shot scaffolds.
- ✅ Security-role and jobs-to-be-done verification (2026-08-14) — both metadata-only:
verifynow proves what a persona role GRANTS, not just that it exists. Therolecheck only asserted a row carrying the SDK marker, so a role built with the wrong access — or one whose privilege write failed after the row landed — verified clean. The newrole-privilegescheck resolves each declared(entity, access)to its DataversePrivilegeIdfrom the same metadata source the SDK writes against and asserts the role holds it at at least the declared depth. Deliberately a subset check (lib/role-privileges.jsexplains why equality would false-fail onappAccessinjection, max-scope union, and shared privileges), and fail-closed on an unreadable role or table.surfaces[]is no longer documentary.lib/surface-resolver.jsresolves everypersonas[].jobs[].surfaces[]entry against the spec's own views/forms/pages/dashboards/tables /sitemap titles;spec-lintwarns when one matches nothing (a warning, not an error — a surface may legitimately name an OOB artifact), andverifyadds ajob-surfacerollup that reports a deployed failure as the job it broke ("persona P can no longer do job J") rather than only "view X is missing".
- ✅ Sample-run UX fixes (2026-07-27, from a live Property-Listings build).
- ✅ #1 live build status. A long build now writes
<workspace>/.maker-workspace/build-status.json(a single-object snapshot —state/steps/lastPhase/lastLabel— overwritten every step) alongside thebuild-log.jsonltrace, and prints a▸ live progress:path at start. So a multi-minute build is observable even when the launching shell buffers stdout. SKILL.md now tells the agent to stream (Tee, not Select-Object) and read the status file. - ✅ #2 wireframes shown. SKILL.md Phase-1 preview step now REQUIRES pasting the
preview-app.js/preview-form.jswireframe output to the user (not summarizing "looks right") before the approval gate — the user must see the forms/sitemap/pages they approve. - ✅ #3 (foundation) track the diff. After a successful full apply the CLI persists
last-applied.json; a later dry-run prints▸ Changed since last apply: <phases>(purephase-diff.js, stable-stringify deep-equal). Still open (tracked below): actually running ONLY the changed phases on--apply. - ✅ #5 auto-number reseed lint.
spec-lintnow WARNS that sampleData for a table whose primary is auto-numbered with no single-column alternate key will DUPLICATE on every re-run (and blocks refreshing data with--sample-dataon an edit) — nudging a natural alternate key.
- ✅ #1 live build status. A long build now writes
- 🔄 #3 execution / #7 — SAFE
--changed-onlypartial apply (Preview; implemented + review-hardened + LIVE-VERIFIED). Design Sol-approved (7 rounds) + implementation adversarially reviewed (GPT-5.6 Sol + Claude Opus, max effort; all Critical/High fixed with tests) + live regression PASSED on aurorabapenv03468 (baseline→eligible, page edit→fast pages-only, chart edit→full fallback+debt, teardown→snapshot deleted). Canonical specdocs/changed-only-design.md(read its v1 scope + contract). Because the engine is ADDITIVE-not-convergent, the partial apply is fail-closed: identity-bound schema-3 snapshot (live orgId/env/app match, appId required), a durable eligibility state machine + sticky debt + teardown tombstone + generation-fenced CAS. v1 wires ONE shape end-to-end — page-content re-upload:--apply --changed-onlyafter a FRESH baseline runs ONLY the pages phase for a.tsxbyte edit (uploads just the changed keys — never clobbers an unchanged page — skips the sitemap finalize, records the measured deployed hash, re-blesses only when page verify passes); anything else falls back to a full build. Deliverables ①–⑨ ✅ (919 offline tests green + live regression). Off by default; opt in with--changed-only. Follow-ups (deferred): pre-mutation live page-content drift verifier; view/sitemap/form fast submodes +expectedSitemapgating;clearDebtMatchingproduction caller; unifycontentPathconfinement. - 🔲 #4 page visual preview before deploy. The generated
.tsxis only structurally grep-checked; the user never sees the page (they hit the double-render live). Add a page preview/screenshot gate before the page is uploaded. - 🔲 #6 lifecycle status modeling. The skill authored "Listing/Offer/Showing Status" as plain Choices, so the record
statecode(Active/Inactive) is disconnected from business status (a "Sold" row is still an "active record", andactiveOnlyviews need an extra Choice filter to compensate). Steer lifecycle fields toward DataversestatusReasons(statecode/statuscode), which the App Spec already supports. - 🔲 #8 approval gate shows artifacts, not counts. The plan gate presents a phase-count table; the user approves counts, not the reviewable data model / wireframes. Fold the wireframe (#2) + a data-model summary into the approval gate.
- 🔲 app-builder eval fixtures — the offline harness now has a
4-hardeningfixture +uioracles that grade the 2026-07-15 fixes (default-view lookups #2 / no createdon #7, N:N ordering #3, sub-grid section+title #5, valid relational sample data #1/#4). Further spec→expected-plan/calls cases still welcome. - 🔲 Workspace reuse — load
.maker-workspace/metadata to skip re-discovery on iterative runs. - 🔲 Worked samples — a Form-JS spec (web resource + onchange handler) and a dashboard spec in
samples/. - 🔲 Refresh
authoring-flow.mdLevel (a) column-type list (still shows the pre-Tier-1 short list). - 🔲 KNOWN BEHAVIOR — an authored view named identically to a stock default view unions onto it. An authored
views[]entry whose name equals the Dataverse stock default ("Active/Inactive <PluralName>") is matched byfindArtifact('view', {name,entity})and reconciled (unioned) onto that stock default rather than created as a new view.reconcileView(sdk-build.js) updates only/columns, so the authored filters and sort are silently ignored and the stockcreatedonis kept. Live-observed onaurorabapenv03468(2026-07). Mitigated:spec-lintnow WARNS when a view name matches the stock-default pattern (recommending a distinct name). Still to decide (design call): whether to also (a) fully reconcile a detected default view (columns + filters + sort) or (b) hard-reject the collision — deferred pending a decision; the warning makes it loud in the meantime. - 🔲 Review follow-ups deferred from the 2026-07-27 Sol PROD-readiness review (pre-prod-acceptable, documented here so they aren't lost).
- Existing old-style sub-grid migration.
addSubgridsis idempotent by relationship (hasSubgrid), so a rebuild does NOT move a sub-grid that a previous, pre-#5 build placed as a half-width cell into the new full-width section, nor re-apply a changed label/view. No deployed apps predate #5 (the skill is pre-prod and the edit path is teardown+rebuild-fresh), so there is nothing to migrate today; this is consistent with the documented additive-build limitation (edits aren't re-applied in place). Revisit if in-place convergence lands. - Self-referential / cyclic sample-data binds. A sample row that
$parent-binds to its own entity can't be seeded (the record-graph seeder resolves@odata.bindbefore the current group's ids exist). Not yet rejected at lint — needs SDK confirmation before a hard reject so a case the SDK can handle isn't blocked. Rare; document + revisit. - Choice wire-type normalization. The lint catches typo labels, but a numeric STRING for a single Choice stays a string and a numeric VALUE for a MultiChoice stays an Int32 (which Dataverse rejects for the multi-select CSV). Related edge: a MultiChoice option label that itself contains a comma passes whole-label validation but is split on the comma at resolve time (
resolveChoiceValue), so it mis-resolves. Authors are steered to plain labels, so these are narrow edges; normalize-or-reject is a follow-up. - Teardown activation-state restore.
restoreStockMainFormreactivates a stock main form to enable deletion, not to perfectly restore pre-build activation state (a form inactive before the build may be left active after teardown), and it runs for reused-table forms too. Acceptable for cleanup; a faithful save/restore is a follow-up.
- Existing old-style sub-grid migration.
- 🔲 KNOWN ISSUE — /genpage vs /app-builder subagent-interaction contradiction (design review F10):
/app-builder's SKILL says aTasksubagent is headless (AskUserQuestion/plan-mode do not reach the user from inside one), while/genpagePhase 1 mandatesAskUserQuestion+ plan-mode inside thegenpage-plannersubagent (skills/genpage/SKILL.md:92-124,agents/genpage-planner.md). One is wrong about the host runtime. Portable fix: run interactive steps in the main loop and use the planner subagent only for headless prereq/auth/detection that returns data — align/genpageto/app-builder. Needs host-behavior confirmation before restructuring.
Intentionally punted (a real blocker, not just "not done yet"), with the why, so we don't re-litigate. Two hard blockers dominate: (A) anything needing Power Fx + a component library can't be authored headlessly; (B) PCF control bindings need solution-import delivery the SDK doesn't package.
- 🔴 PCF custom-control bindings — the SDK emits correct formxml, but a plain
pushArtifactstrips the controluniqueid, so the binding persists only via solution import (export → unzip → patchcustomizations.xml→ rezip → import). The SDK exposes no artifact→zip packaging, and it needs a pre-deployed control to bind to. Unblock: an SDK helper that packages a form artifact (with its$meta.formxml) into an importable solution zip. - 🔴 Conditional (rule-based) command visibility — modern commands express this only as Power Fx (component-library bind) → blocker A. Static
hidden/disabledships. - 🔴 Power Fx command on-click — same component-library blocker (A). JavaScript on-click ships.
- 🟡 Titled command groups — a titled group needs a parent command-bar row the adapter doesn't synthesize for from-scratch commands (re-confirmed live: Dataverse 400 "Group button must have parentappactionid"). Buttons emit as loose controls; flyout / split-button menus do work. Unblock: SDK synthesis of the parent group rows.
- 🟡 Interactive (type 10) dashboards — different formxml machinery (streams/tiles keyed by cell id); the tile generator targets Standard (type 0). Unblock: an interactive-dashboard tile generator.
- ⚠ Business-rule validation — org-gated on the Aurora test orgs (missing the
*ProcessWithWfomJsonaction) so it can't be live-verified here, and the modern path is Power-Fx-flavored. Build behind a capability flag once an org supports it. - 🟡 Explicit app-component re-pin on an app EDIT — a NEW chart added to an ALREADY-DEPLOYED app on an edit rebuild is not re-pinned as an explicit app component: the SDK's generic surface can't add a missing
componentsobject to a fetched app (setAppDefinitionwas retired). Low impact — the chart is still added to the solution and shows on its table's chart pane; rebuild the app fresh, or surface the chart via a dashboard/sitemap subarea, if it must be an explicit component. Unblock: an SDK component-set API for a fetched app, or fetch populatingcomponents.
- New artifact types reuse the
buildArtifact(type, def)helper (createArtifact → optional pre-push tweaks → pushArtifact → addSolutionComponent) plus a newCOMPONENT_TYPEentry. - Add a phase to
PHASES, aplanForbranch, aspec-lint/app-specvalidation block, schema-doc + skill notes, and both a mock-SDK engine test and avendor-sdk-smokeassertion against the real bundle. - Rebundle the SDK (
node scripts/_vendor-build/build.js --sdk <ppux>) only when pulling new SDK methods. See../AGENTS.md→ Building & Testing.