feat(model-apps): SDK uptake, silent-failure fixes, genpage input contract, 2.5.0 - #458
feat(model-apps): SDK uptake, silent-failure fixes, genpage input contract, 2.5.0#458Akshay Maloo (akshaymaloo) wants to merge 9 commits into
Conversation
…it deliberately broke
Re-vendors `scripts/vendor/cds-maker-sdk.cjs` from the SDK's master (572 KB -> 641 KB).
The important part is not the re-vendor, it is what it revealed. Four tests failed,
and only one of them was a stale assertion.
**`requireSuccessfulPush` had been silently disarmed.** The SDK renamed
`PushResult.success` to `saved`, and says why in its own type comment: "the rename
forces every existing call site to be looked at once." This guard is such a call
site, and it is the only thing standing between a 412 and a silently dropped Maker
edit. Its check was `result.success === false`, which against the new shape reads
`undefined === false` -> false. The guard would simply stop firing: no error, no log,
and a concurrent edit overwritten -- the exact failure it exists to prevent. It now
reads `saved` and falls back to `success`, so it fails CLOSED against both bundle
generations rather than assuming one; a mismatch in EITHER direction disarms it.
The rename also separates "saved" from "shipped" (live only after a VERIFIED
publish). Recorded in the guard so a future reader does not conflate them.
The other three:
- `deleteAppCascade` no longer cascades generative-page deletes -- a `uxagentproject`
is REFERENCED by an app, not owned by one -- and reports the page in a new
`retained[]` array. `lib/sdk-teardown.js` was ALREADY written for this; only the
contract test lagged. Re-pinned to `retained[]`, since silently reverting to a
cascade would delete a page another app still surfaces.
- Two `pushArtifact` tests asserted `success`; migrated to accept either spelling so
they pin the contract rather than one bundle.
- The default-view fixture carried no `@odata.etag`, and the SDK now REFUSES an
unconditional write (`ARTIFACT_UPDATE_NO_ETAG`) rather than risk overwriting a
concurrent edit. That is a hardening, not a regression; real Dataverse always
returns an etag, so the fixture was simply unrealistic.
Live-verified end to end against a real environment, artifacts torn down:
build 17/17 created, 0 failed (solution, data-model incl. global choice,
status reason, alt key and relationship;
views + default-view enrichment; charts;
forms; app-shell)
verify PASS 11/11
rebuild idempotent -- exercises the new etag/concurrency path by re-pushing
view, form and app against existing rows
teardown 11 deleted, 0 failed, incl. deleteAppCascade
Offline tests are mock-based and could not have proven any of that; the only phase
not covered was `pages`, which fails on `pac genpage list` auth and does not touch
the SDK.
Note the earlier claim that the uptake broke three contract tests, then that it broke
one, were both measured against a checkout 154 commits behind master. The real delta
is 92 files / +8085 lines.
1519 pass, 0 fail. The push-guard migration is red-green verified: restoring the old
`success === false` check turns two tests red.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 42626da2-b66f-4162-acaa-b1127ef23d89
Three of the five deferred items from the #447 fix, plus the version bump. - A conflicting `--language-code 1031 --languageCode 1036` pair silently took the kebab form. That is the wrong answer when the two disagree: the user passed both and believes one is in effect, so guessing produces a build they did not ask for with no indication why. Now rejected. Extracted as `readAliasedFlag` in dataverse-auth.js next to `parseArgs` rather than duplicated in both CLIs. - `--language-code` was accepted, validated and threaded for stage selectors that skip `data-model`, then silently discarded -- only that phase creates labels. It now warns, because finding out afterwards means re-running the whole build. - The #447 entry sat under `### Added` while its headline was a fix. Split: the fix under `Fixed`, the new `languageCode` field and flag under `Added`. Also records the push-guard fix under `Fixed`, since a 412 being silently swallowed is user-visible even though it was found while taking up the SDK. Not done, and left on #456 with reasoning: validating the resolved LCID against `RetrieveProvisionedLanguages` (needs an httpClient threaded into resolveLanguageCode), and preserving a hand-pinned `languageCode` across download (the naive fix -- stamping the source org's LCID into a portable spec -- causes the very failure #447 was). Version 2.4.4 -> 2.5.0 in both the Open Plugins manifest and the legacy mirror; minor, since this adds a spec field and two CLI flags on top of the SDK uptake. 1523 pass, 0 fail; 7 validators green. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 42626da2-b66f-4162-acaa-b1127ef23d89
…onflict
Two rules contradicted each other, and an author had no way to satisfy both:
- Every `pages[]` entry MUST be a sitemap subarea. The sitemap is download's
only membership oracle, so a page reached only by `navigatesTo` is invisible
to download and gets re-created as a DUPLICATE on the next build.
- But a detail page declares `pageInput` because it is opened from a list with
a row id.
Being sitemap-placed means the page is also reachable straight from the app
navigation, with no input at all. That is a real user-reachable state -- it is
what clicking the nav entry does -- and nothing made the author account for it,
so the generated page read `undefined` context on that path.
Resolved in favour of keeping the membership invariant. Allowing navigation-only
pages was considered and rejected: it would require the sitemap to stop being the
membership oracle, and the duplicate-page bug that prevents is worse than an extra
nav entry.
A page declaring `pageInput` must now declare `directEntry`:
`{ behavior: 'selector' }` (show a picker, then the record) or
`{ behavior: 'emptyState' }` (explain, render nothing broken), with an optional
`note` passed to the generator verbatim. `page-plan` emits it into the generation
prompt, so the .tsx actually implements the path rather than being told about it.
Also adds the input-contract trace the review asked for: every key in
`pageInput.data` must be produced by an incoming `navigatesTo[].data` edge. An
input no caller supplies is either a typo or a page that can only be entered
directly -- both generate a page reading a key nothing ever sets.
`directEntry` round-trips via the page manifest and hydrate-spec, because a
downloaded spec that lost it would fail its own validation on the next build.
The one existing test that broke is itself the conflict: it declared a `detail`
page with `pageInput` and no answer for direct entry, and validated cleanly.
1527 pass, 0 fail; 7 validators green.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 42626da2-b66f-4162-acaa-b1127ef23d89
…s throw-to-return refactor
Peer review found that I migrated the push half of this refactor and missed the
publish half, which is the worse one. The SDK moved `publishArtifact` from throwing
to reporting by value in the same way it moved `pushArtifact`, and all NINE call
sites in sdk-build.js were bare `await provision.publishArtifact(...)` with the
result discarded. Measured against the real bundle: a `/PublishXml` 500 returns
`{ publish: { kind: 'failed', error } }` on this bundle and THREW on the previous
one, and a build in which every publish failed still reported `ok: true` with zero
error events and zero warnings.
Three things made it worse than it sounds. It is not gated on `--publish` --
reconciling an existing form or view publishes on every `--apply`. The trigger is
routine rather than exotic: 429, 503 and customization-lock are the everyday
PublishXml failures in a real tenant. And because they no longer throw, the
existing transient-retry loop -- which enumerates exactly those codes -- had become
unreachable for publish failures.
Adds `reportPartialPush` next to `requireSuccessfulPush` and wires it into all nine
publish sites. It never halts: the primary write committed, so the build should
continue -- but silence turns a partial success into a reported clean one.
Also drains `PushResult.warnings` at all eleven push sites. That field exists
precisely so an app whose components could not all be pinned, or whose
system-administrator role assignment failed (which yields an app nobody can open),
is not read as a good build. This is also the concrete `saved` vs `shipped` gap: an
app CREATE publishes inside the SDK, so a failed publish arrives as `saved:true`
with no top-level error -- a shape the push check alone waves straight through.
Two further review findings:
- The guard reported an `ARTIFACT_ALREADY_EXISTS` collision as a version conflict,
giving two contradictory remedies in one message. They are now distinguished:
adopt the existing row, versus re-download after a concurrent edit.
- My comment claiming "a 412 is the only failure pushArtifact signals by return
value" was false as of this bundle. Corrected.
Six test mocks still returned the OLD `{ success: true }` PushResult and a void
publish, so the entire sdk-build integration surface was exercising the legacy
branch of the new guard and was blind to the shape it actually ships against.
Migrated to the real shapes.
Tests +6, red-green verified: making the reporter a no-op turns four red.
1533 pass, 0 fail; 7 validators green.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 42626da2-b66f-4162-acaa-b1127ef23d89
There was a problem hiding this comment.
Pull request overview
Updates the model-apps plugin to a newer vendored Maker SDK, hardens the build pipeline against SDK by-value failure reporting (push/publish), and resolves the genpage sitemap-membership vs pageInput contract conflict by introducing directEntry. This culminates in a 2.5.0 release bump with corresponding documentation and changelog updates.
Changes:
- Uptake of the newer Maker SDK surface by updating push/publish result handling and refreshing affected test mocks/contracts.
- Adds and enforces the
directEntry+pageInputinput contract for genpages (including manifest/download round-trip support). - Introduces stricter CLI flag alias handling (
--language-codevs--languageCode) and bumps plugin version/docs to 2.5.0.
Reviewed changes
Copilot reviewed 25 out of 26 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| plugins/model-apps/scripts/tests/vendor-sdk-smoke.test.js | Updates SDK contract tests (PushResult rename handling; deleteAppCascade now asserts retained[]). |
| plugins/model-apps/scripts/tests/sdk-build.test.js | Updates SDK build mock push result shape (needs publish mock alignment per review). |
| plugins/model-apps/scripts/tests/sdk-build-pages-order.test.js | Updates page build harness mocks to new push/publish result shapes. |
| plugins/model-apps/scripts/tests/sdk-build-pages-migrate.test.js | Updates page migration harness mock push result shape (needs publish mock alignment per review). |
| plugins/model-apps/scripts/tests/sdk-build-pages-deploy.test.js | Updates page deploy harness mocks to new push/publish result shapes. |
| plugins/model-apps/scripts/tests/helpers/mock-sdk.js | Refreshes shared mock SDK to return publish-by-value results. |
| plugins/model-apps/scripts/tests/hardening2-real-bundle.test.js | Pins push conflict contract across success/saved rename. |
| plugins/model-apps/scripts/tests/entity-provision.test.js | Adds tests for renamed push contract, publish-by-value reporting, and warnings propagation. |
| plugins/model-apps/scripts/tests/default-view-createdon.test.js | Makes the view fixture realistic by adding required @odata.etag. |
| plugins/model-apps/scripts/tests/build-model-app.test.js | Adds tests for rejecting conflicting aliased language flags. |
| plugins/model-apps/scripts/tests/app-spec-keys.test.js | Adds validation tests for directEntry requirements and page input traceability. |
| plugins/model-apps/scripts/provision-entities.js | Switches language flag parsing to readAliasedFlag to reject conflicting spellings. |
| plugins/model-apps/scripts/lib/sdk-build.js | Starts consuming publish results via reportPartialPush and threads warning sink into push guard. |
| plugins/model-apps/scripts/lib/page-plan.js | Includes directEntry behavior in generated page plan output. |
| plugins/model-apps/scripts/lib/page-manifest.js | Persists and parses directEntry in the durable page manifest. |
| plugins/model-apps/scripts/lib/hydrate-spec.js | Hydrates directEntry from manifest/spec into the in-memory spec shape. |
| plugins/model-apps/scripts/lib/entity-provision.js | Hardens push guard to handle saved vs success, adds reportPartialPush, distinguishes already-exists vs 412. |
| plugins/model-apps/scripts/lib/dataverse-auth.js | Adds readAliasedFlag helper to reject conflicting alias pairs. |
| plugins/model-apps/scripts/lib/app-spec.js | Adds directEntry enum + validation rules tying pageInput keys to incoming navigation edges. |
| plugins/model-apps/scripts/build-model-app.js | Uses readAliasedFlag for language-code parsing and warns when LCID is no-op for selected phases. |
| plugins/model-apps/references/app-spec-schema.md | Documents the new directEntry + pageInput input contract and rationale. |
| plugins/model-apps/CHANGELOG.md | Updates release notes for 2.5.0, including SDK behavioral changes and contract fixes. |
| plugins/model-apps/AGENTS.md | Documents the membership invariant corollary and the new directEntry contract. |
| plugins/model-apps/.plugin/plugin.json | Bumps plugin version to 2.5.0. |
| plugins/model-apps/.claude-plugin/plugin.json | Mirrors plugin version bump to 2.5.0 for legacy manifest compatibility. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
CI caught what I did not: the `2-orders-multipage` app-builder eval fixture is
itself an instance of the conflict the previous commit resolved. Its `order-detail`
page declares `pageInput: { orderId }` and is sitemap-placed, with nothing saying
what opening it from the nav shows -- so it now fails validation, correctly.
Given `behavior: "selector"`, which is the honest answer for an order-detail page:
opened with no orderId, show a picker and then render the chosen order.
My miss was procedural rather than analytical: I looked for an eval runner under
`plugins/model-apps/` and concluded there was none. The harness lives at the repo
root under `evals/model-apps/`, in a separate CI job gated by the same path filter.
Both suites have to be run:
cd plugins/model-apps && node scripts/run-tests.js
node --test evals/model-apps/tests/*.test.js \
evals/model-apps/app-builder/tests/*.test.js \
evals/model-apps/genpage/tests/*.test.js
159 eval tests pass; 1533 plugin tests pass.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 42626da2-b66f-4162-acaa-b1127ef23d89
Peer review found the half I missed — and it was the worse halfAn adversarial review of this branch caught something I had not: I migrated the push half of the SDK's throw-to-return refactor and missed the publish half. All nine End to end, a build in which every publish failed still reported Three things make it worse than the push bug I did catch:
Fixed with Also from the review
My own comment was false. I wrote "a 412 is the only failure Six test mocks still returned the old Where the review corrected me
Verification1533 plugin tests + 159 eval tests pass; 10/10 CI green. Both guards red-green verified — restoring CI also caught one thing I missed procedurally: the |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 26 out of 27 changed files in this pull request and generated no new comments.
Suppressed comments (2)
Previously missed (2) — in code that hasn't changed since the last review.
plugins/model-apps/scripts/lib/app-spec.js:860
pageInputis only validated as an object, but the new input-contract logic treatspageInput.dataas a plain object and callsObject.keys(...)on it. IfpageInput.datais missing or not an object/array (e.g., string/number), validation can produce misleading errors (or treat string indices as keys) instead of a clear schema error. Add an explicit validation thatpageInput.datais a non-null, non-array object wheneverpageInputis present (or at least before computinginputKeys).
for (const p of spec.pages || []) {
const key = p.key || p.name;
const inputKeys = Object.keys((p.pageInput && p.pageInput.data) || {});
if (!inputKeys.length) continue;
plugins/model-apps/scripts/lib/page-manifest.js:154
parseManifestvalidates thatpageInputis an object, but it doesn't validate thepageInput.datasub-shape even though the rest of the codebase treats it as{ data: object }(and validatesnavigatesTo[].datasimilarly). A corrupted/hand-edited manifest could round-trip a non-objectpageInput.dataand then fail later in spec validation/build. Consider validatingpageInput.datais a non-null, non-array object when present (fail-closed here, likenavigatesTo[].data).
if (p.pageInput !== undefined) {
if (!p.pageInput || typeof p.pageInput !== 'object' || Array.isArray(p.pageInput)) return null;
}
// directEntry — same shape rule. Validated here too so a corrupt manifest is rejected whole
…est check, two stale mocks
Four review findings, all correct.
- `reportPartialPush` told the operator to "re-run with --publish". It runs for
three callers and that is right for only one of them: an explicit publishArtifact
has already attempted the publish, and an app CREATE publishes inside the SDK --
so both were being told to pass a flag they had effectively already used. The hint
now says what is true for all three: the save survived, the build is idempotent,
re-run once the cause clears.
- `parseManifest` claimed in a comment to reject a corrupt `directEntry` so it could
not round-trip into a rebuild that fails validation, but only checked object-ness.
A bogus `behavior` sailed through and failed App Spec validation later, far from
its cause -- which is what parsing a STORED artifact strictly is supposed to
prevent. It now validates `behavior` against the same list App Spec uses, imported
rather than mirrored so the two cannot drift into disagreeing about what is legal.
- Two `publishArtifact` mocks were still void. My earlier migration matched
`async () => undefined` and `async () => {}` but missed the
`async (t, id) => { calls.push(...) }` form, so `sdk-build.test.js` and
`sdk-build-pages-migrate.test.js` were still feeding `undefined` into a
`reportPartialPush` that production now feeds a real result.
Tests +1: the manifest parser rejects every illegal `directEntry.behavior` and
accepts both legal ones. 1534 plugin + 159 eval tests pass; 7 validators green.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 42626da2-b66f-4162-acaa-b1127ef23d89
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 27 out of 28 changed files in this pull request and generated no new comments.
Suppressed comments (3)
Previously missed (3) — in code that hasn't changed since the last review.
plugins/model-apps/scripts/lib/page-plan.js:266
buildPagePlanprints a “Direct entry (no input)” plan wheneverpageInputis present, but App Spec validation only requiresdirectEntrywhenpageInput.datadeclares at least one key. For pages withpageInput: { data: {} }(or other empty/non-object shapes), this output is misleading and defaults the behavior toemptyStatewithout the author opting in. Consider gating this block onObject.keys(p.pageInput?.data ?? {}).length > 0(orp.directEntrybeing present) so the plan output matches the input-contract rules and doesn’t invent a default behavior.
if (p.pageInput !== undefined) {
out.push('- **Page Input:** reads `pageInput` (caller-supplied context)');
// The page is sitemap-placed, so it is ALSO reachable straight from the app navigation with
// no input at all. The generator has to be told what that renders, or it writes a page that
// reads `undefined` context on a path a user can reach by clicking the nav entry.
plugins/model-apps/scripts/lib/app-spec.js:860
validateAppSpecusesObject.keys((p.pageInput && p.pageInput.data) || {})to drive the newdirectEntry/input-trace rules, but there’s no validation thatpageInput.dataitself is a plain object. Non-object values (e.g.null, arrays, numbers, strings) can makeinputKeysempty (silently skipping the contract) or produce unexpected keys. Add an explicit check thatpageInput.datais an object (and not null/array) wheneverpageInputis present, so malformed specs fail fast with a clear error.
const key = p.key || p.name;
const inputKeys = Object.keys((p.pageInput && p.pageInput.data) || {});
if (!inputKeys.length) continue;
plugins/model-apps/scripts/lib/sdk-build.js:1080
- The new publish-result warnings use labels like
form ${formId}/view ${viewId}. These GUID-only identifiers make the warning hard to act on (especially in large builds) even thoughdef.nameis available in this scope. Consider using a human-friendly label (e.g.form ${def.name} (${formId})) soreportPartialPushwarnings clearly identify the artifact.
requireSuccessfulPush(await provision.pushArtifact('form', formId), `form ${def.name}`, opts.warn);
reportPartialPush(await provision.publishArtifact('form', formId), `form ${formId}`, opts.warn);
await provision.addSolutionComponent({ componentId: formId, componentType: COMPONENT_TYPE.form, solutionUniqueName: sol.uniqueName });
) Download deliberately does not read `languageCode` from Dataverse, and that stays true: an LCID copied out of the source org would be re-applied verbatim when the spec is rebuilt somewhere else, which is exactly how a spec starts failing in an org that has not provisioned that language (#447). Leaving it absent lets every target org resolve its own base language. But silently dropping a value the AUTHOR wrote is its own bug, and a quiet one. The next build resolves the org default, so columns created from then on get one language while the ones from the pinned build keep another -- a mixed-language app with no error anywhere to explain it. So the value is restored from the previous `app-spec.json` at the output path -- the author's own file -- and never synthesized from the environment. That keeps the portability property (a spec carries no org-specific LCID unless someone chose one) while making a deliberate pin survive a round trip. Best-effort by design: a missing, unreadable or malformed previous spec simply means there is nothing to preserve, and an invalid value is not carried forward, since failing the next build for something the operator did not do on this run would be worse than the wart being fixed. This was deferred from the #447 work with the reasoning recorded on #456, because the obvious fix -- emitting the org's LCID -- is actively harmful. Tests +4, red-green verified. 1523 plugin + 159 eval tests pass; 7 validators green. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 42626da2-b66f-4162-acaa-b1127ef23d89
…ing that reads it Closes the gap that let the publish regression land unnoticed in the first place. `publishArtifact` was in the method-PRESENCE guard but had no BEHAVIOURAL contract test, so the SDK moving it from throwing to reporting by value was invisible to CI. Adds a real-bundle test asserting it RESOLVES with `publish.kind === 'failed'` on a failed PublishXml rather than throwing. Which one the bundle does is load-bearing in both directions: a throw silently re-arms the transient-retry path, a by-value failure silently disarms it. Also adds the integration test that no unit test can stand in for: that a by-value publish failure actually reaches the warn channel through the whole `runSdkBuild`. The unit tests pin `reportPartialPush`; they say nothing about whether the engine is wired to it, and a wiring break is exactly what the original bug was -- nine call sites that discarded the result, producing `ok: true` with zero warnings. A note on verifying that second test, because the first attempt was misleading: my mutation replaced only 8 of the 9 publish sites -- the `mapLimit` form ends in `))` rather than `);` and slipped the regex -- and the surviving site was the one the test exercises, so it stayed green and looked ineffective. Re-run with a paren-matching unwire that reaches all 9: the test turns red, as it should. A test that passes for the wrong reason is worse than no test, so this is recorded rather than quietly fixed. 1540 plugin + 159 eval tests pass; 7 validators green. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 42626da2-b66f-4162-acaa-b1127ef23d89
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 29 out of 30 changed files in this pull request and generated 1 comment.
Suppressed comments (5)
Previously missed (2) — in code that hasn't changed since the last review.
plugins/model-apps/scripts/lib/page-plan.js:266
directEntry.noteis appended into the plan without passing throughmdText, even though this file otherwise hardens all spec-sourced strings against markdown structure injection (newlines/headings/tables). Since the plan is executed as instructions by a write-capable worker,de.noteshould be sanitized the same way (and ideally only rendered whendirectEntryis actually present/valid, rather than defaulting missing/unknown behavior to the emptyState text).
if (p.pageInput !== undefined) {
out.push('- **Page Input:** reads `pageInput` (caller-supplied context)');
// The page is sitemap-placed, so it is ALSO reachable straight from the app navigation with
// no input at all. The generator has to be told what that renders, or it writes a page that
// reads `undefined` context on a path a user can reach by clicking the nav entry.
const de = p.directEntry || {};
const behavior = de.behavior === 'selector'
? 'render a picker/list so the user can choose the record, then show it'
: 'render an explanatory empty state (do NOT render a broken/blank detail)';
out.push(`- **Direct entry (no input):** the page is in the sitemap, so it can be opened from the nav with no \`pageInput\` — ${behavior}.${de.note ? ` ${de.note}` : ''}`);
plugins/model-apps/scripts/lib/sdk-build.js:1080
- The publish warning label uses the GUID (
form ${formId}), even though the human-friendly name is available asdef.nameand is already used for the push label. If a publish fails, warning output likepublish form <guid> FAILEDis much harder to act on thanpublish form <name> FAILED; consider using the same label string for both push and publish here.
This issue also appears in the following locations of the same file:
- line 1099
- line 1299
- line 1339
requireSuccessfulPush(await provision.pushArtifact('form', formId), `form ${def.name}`, opts.warn);
reportPartialPush(await provision.publishArtifact('form', formId), `form ${formId}`, opts.warn);
await provision.addSolutionComponent({ componentId: formId, componentType: COMPONENT_TYPE.form, solutionUniqueName: sol.uniqueName });
plugins/model-apps/scripts/lib/sdk-build.js:1101
- Same as the form path above: the publish warning label uses
view ${viewId}(GUID) even thoughdef.nameis available and used for the push label. Using the name for publish warnings would make failures much more actionable.
requireSuccessfulPush(await provision.pushArtifact('view', viewId), `view ${def.name}`, opts.warn);
reportPartialPush(await provision.publishArtifact('view', viewId), `view ${viewId}`, opts.warn);
await provision.addSolutionComponent({ componentId: viewId, componentType: COMPONENT_TYPE.view, solutionUniqueName: sol.uniqueName });
plugins/model-apps/scripts/lib/sdk-build.js:1301
- When wiring form events, the publish warning label is
form ${id}(GUID) while the surrounding step labels already used.f.name || d.f.entity. For consistency and debuggability, consider passing the same human-readable label intoreportPartialPush(e.g.form ${d.f.name || d.f.entity} events) so a failure points at the form/step the operator recognizes, not just the GUID.
requireSuccessfulPush(await provision.pushArtifact('form', id), `form ${d.f.name || d.f.entity} events`, opts.warn);
reportPartialPush(await provision.publishArtifact('form', id), `form ${id}`, opts.warn);
}
plugins/model-apps/scripts/lib/sdk-build.js:1341
- In the quick-view placement path, the publish warning label is
form ${hostId}(GUID) while the push label usesform ${f.name || f.entity} quick-views. Consider using the same human-readable label forreportPartialPushso publish failures don’t require correlating GUIDs back to a form.
requireSuccessfulPush(await provision.pushArtifact('form', hostId), `form ${f.name || f.entity} quick-views`, opts.warn);
reportPartialPush(await provision.publishArtifact('form', hostId), `form ${hostId}`, opts.warn);
}
#459 folded in, plus the contract test that would have caught this classCombined per request — one PR is easier to live-test and peer-review as a unit, and both halves touch the same language-code surface. #459 is closed; its commit is Also added the two things that were missing rather than broken:
One verification note worth recordingThe first red-green of that integration test looked ineffective — I unwired the publish sites and it stayed green. That was the mutation being incomplete, not the test: the What is and is not live-verifiedLive, artifacts torn down, 0 failures: full build 17/17 + verify 11/11, idempotent rebuild (the new etag/concurrency path), a Not live-verified: the download path (the 1540 plugin + 159 eval tests pass; 7 validators green; 10/10 CI. |
…ross a download Review catch: the guard validated the previous value with normalizeLanguageCode but then assigned the RAW one, so `"1031"` or `" 1031 "` round-tripped verbatim. A downloaded spec is a generated artifact and should be canonical -- writing the author's string or whitespace form back out makes the file's diff noisy and its type inconsistent with every other numeric field the download emits. Assigns the normalized number instead. Adds a test over `"1031"`, `" 1031 "` and `"01031"` asserting all three land as the number 1031. 1541 plugin + 159 eval tests pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 42626da2-b66f-4162-acaa-b1127ef23d89
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 29 out of 30 changed files in this pull request and generated no new comments.
Suppressed comments (1)
Previously missed (1) — in code that hasn't changed since the last review.
plugins/model-apps/scripts/lib/sdk-build.js:1080
reportPartialPushwarnings will currently identify artifacts by GUID (form ${formId}/view ${viewId}), even though the human-readabledef.nameis available at these call sites. That makes publish-failure warnings hard to act on (operators typically know the form/view name, not the id). Consider using a label likeform ${def.name} (${formId})/view ${def.name} (${viewId})for clearer output, consistent with the push labels on the previous line.
requireSuccessfulPush(await provision.pushArtifact('form', formId), `form ${def.name}`, opts.warn);
reportPartialPush(await provision.publishArtifact('form', formId), `form ${formId}`, opts.warn);
await provision.addSolutionComponent({ componentId: formId, componentType: COMPONENT_TYPE.form, solutionUniqueName: sol.uniqueName });
Takes up the current maker SDK and fixes the two silent-failure classes it revealed, resolves the genpage sitemap-placement vs
pageInputpolicy conflict, closes the actionable #456 follow-ups, and releases 2.5.0.Closes #457. Addresses #456 (items 2–5; item 1 remains, with reasoning on the issue). Supersedes #459, folded in here so the whole language-code surface is reviewed and live-tested as one unit.
The SDK uptake found two silent failures, not stale tests
Re-vendored from the SDK's master (572 KB → 641 KB; the delta is 92 files / +8085/−656). The SDK moved both
pushArtifactandpublishArtifactfrom throwing to reporting by value. We were checking neither.1. The 412 guard had been silently disarmed.
PushResult.successwas renamed tosaved— the SDK's own type comment says the rename exists to "force every existing call site to be looked at once".requireSuccessfulPushcheckedsuccess === false, which against the new shape readsundefined === falseand simply stops firing. A concurrent Maker edit would be overwritten with no error and no log.2. Every publish failure was silent — peer review caught this one, and it is worse. All nine
publishArtifactcall sites discarded the result. Measured: a/PublishXml500 returns{ publish: { kind: 'failed' } }on this bundle and threw on the previous one, and a build where every publish failed still reportedok: truewith zero error events. It is not gated on--publish(reconciling an existing form or view publishes on every--apply), the trigger is routine (429 / 503 / customization-lock), and because those no longer throw, the transient-retry loop had become unreachable for them.Also drains
PushResult.warningsat all eleven push sites — that field exists so an app whose components could not all be pinned, or whose system-administrator role assignment failed (which yields an app nobody can open), is not read as a clean success. It is also the concretesavedvsshippedgap: an app create publishes inside the SDK, so a failed publish arrives assaved: truewith no top-level error — a shape the push check alone waves straight through.And closes the gap that let this land.
publishArtifactwas in the method-presence guard but had no behavioural contract test, so a throw→return change was invisible to CI. This PR adds one, plus the integration test that the engine is actually wired to read the result — which no unit test can stand in for, since the original bug was a wiring break.Language code
languageCodein an App Spec, and--language-codeon both CLIs, with a conflicting pair of spellings now rejected rather than silently resolved to the kebab form.data-model, where it is a no-op.languageCodesurvives a download (was fix(model-apps): keep a hand-pinned languageCode across a download (#456) #459). Download still never reads the LCID from Dataverse — copying the source org's language into a portable spec is how it starts failing in an org that lacks that language — but a value the author wrote is carried over from the previousapp-spec.json. Losing it was quiet: the next build resolved the org default, so new columns got one language while the pinned ones kept another.Genpage: sitemap-placement vs
pageInputTwo rules contradicted each other with no way for an author to satisfy both. Every page must be sitemap-placed — the sitemap is download's only membership oracle, so a page reached only by
navigatesTois invisible to download and gets re-created as a duplicate. But that means a detail page is reachable from the nav with no input, and nothing made the author account for it, so the generated page readundefinedcontext on a path a user reaches by clicking.Resolved by keeping the membership invariant and adding
directEntry(selector|emptyState), whichpage-planfeeds to the generator so the.tsxactually implements that path. Plus the input-contract trace: everypageInput.datakey must be produced by an incomingnavigatesTo[].dataedge. Allowing navigation-only pages was considered and rejected — the duplicate-page bug is worse than an extra nav entry.Verification
1540 plugin tests + 159 eval tests pass; 7 validators green. Every guard is red-green verified — restoring
success === falseturns two red; making the publish reporter a no-op turns four red; unwiring all nine publish sites turns the integration test red; dropping the download restore turns the pin test red.Live, against a real environment, all artifacts torn down (0 failures each):
--verify--publishdeleteAppCascadeCorrections recorded rather than quietly fixed
pushArtifactsignals by return value" was false;ARTIFACT_ALREADY_EXISTSis a second, and the guard was reporting it with the opposite remedy.parseManifest's comment claimed it rejected corruptdirectEntryvalues when it only checked object-ness.mapLimitsite ends in))and slipped the regex, and it was the very site the test exercises. Re-verified with a paren-matching unwire.