Conversation
The CodeRabbit review of the SEP-UI-into-PMM migration PR (percona/pmm#5653) raised these against the ported copy, but every one is present in the original — so they are fixed here to keep the two trees from diverging at the next sync. PMM-only wiring (route constants, the route-level admin gate, the dev-proxy variables) is not part of this. Correctness: - `refreshAccessToken` called the injected `_onRefreshed` handler inside the async executor, so a synchronous throw from it rejected the shared `refreshInFlight` promise: every awaiting caller saw a failed refresh — and a force-logout — for a cookie rotation that had already succeeded on the backend. The function's own comment says this must not happen. - `SchemaFormRenderer` returned from `handleFormSubmit` on a section violation, which react-hook-form reads as a successful submit (`isSubmitSuccessful = true`). `useUnsavedChangesGuard` is `isDirty && !isSubmitSuccessful` and only re-arms when `submitError` is truthy — never on this path — so the guard stayed disarmed and the user could navigate away from a dirty form with no prompt. The gate moved into the submit event handler, ahead of `handleSubmit`. The new test fails against the old ordering. - `normalizeChoiceDefaults` read and wrote flat keys, but `flattenSectionFields` also returns `one_of` branch fields, whose names are dotted paths stored nested. A case-mismatched nested choice value was never canonicalised and rendered as an empty selection. Now goes through `getAtPath` / `setAtPath`; no deep copy needed, since `setAtPath` clones the intermediates it walks (a test pins that). - `AppSchema.list_view` is optional, and an unresolved entity route falls back to the top-level schema, so `schema.list_view!.columns` could throw in `AppListPage` and in `AppDetailPage`'s Overview tab. The list page renders `Not found` (after every hook, so hook order is stable) and the overview falls back to an empty column set, still listing the task's own fields. - `HostSelector` looked up `errors[name]`, which never resolves for a dotted branch-field name, hiding that field's validation error. - `extractId` used `Number`, which turns a whitespace-only string into `0` and accepts `'1.5'` / `'0x10'`. Each result then reads as a resolvable inventory id: `useResolvedServiceField` enables a lookup for service `0`, and `SchemaSelector` fires `useSchemas({ serviceId: 0 })` for a service that cannot exist. - `validationMapper` submitted `parseInt('2.5', 10)` as `2` and `parseFloat('3.14invalid')` as `3.14`. Numeric fields now validate with `Number.isFinite` (plus `Number.isInteger` for `integer`) and coerce with `Number`, so partly-numeric input is rejected instead of truncated. - `useTaskLogs` guarded on `!step`, dropping a log line with `step: ''`. `useExecutionEvents` treats `''` as the stepless bucket and the viewer labels it "General", so the two streams disagreed and stepless output was silently lost. Stability: - `useExecutionEvents`' `onerror` returned `undefined` for every non-sentinel error, so `fetchEventSource` retried forever while `sseError` stayed unset and `sseLoading` stayed true — a persistently failing endpoint left the panel spinning and reconnecting indefinitely. Consecutive failures are now counted (reset on a successful open) and the loop stops with the error surfaced. - `StandaloneHostSelector` disabled its Autocomplete on a hosts-query failure, but `onOpen` holds the only `refetch()` trigger and a disabled Autocomplete never opens: one failure wedged the control until remount. - A scheduled-task enable/disable toggle sent `kwargs: '{}'` in a full PUT, wiping the arguments of any task created with non-default kwargs. `kwargs` is preserved when the response carries it; `'{}'` stays the fallback until `PeriodicTaskResponse` declares the field. Contract and consistency: - `useCascadingField` cleared with `undefined` while `buildFormDefaults` seeds these selector types to `''`, and counted `''` as ready — so a downstream selector fetched options for an empty parent, and a bound MUI input flipped from controlled to uncontrolled. Standardised on `''`. - `SchemaDrivenApp`'s `renderEditForm` invocation omitted `capabilities`, `submitError` and `fieldErrors`, so a consumer supplying the slot could render neither the 422 banner nor the inline field errors and `mapSubmitError`'s result was dead state. `AppTaskEditPage` already passes all three to the same slot type. - `SchemaListView` pinned `bgcolor: 'common.white'` on the table paper and container, which renders a white table in dark mode; it now reads the mode's own opaque surface. - The file-picker `IconButton` in `FileField` had no accessible name. - `useResolvedServiceField` discarded `useServices`' error, leaving callers unable to tell a failed lookup from an id that matched no service — the exact signal its doc comment defines as "not resolving". - `packages/framework/test/setup.ts` was an unreferenced sibling of `tests/setup.ts` that registered the jest-dom matchers but no `afterEach(cleanup)`; deleted so a future `setupFiles` edit cannot silently drop DOM isolation for the package. Not ported, matching the decisions recorded on the PMM PR: production SEP routing / token exchange, flattening dotted app argument names (they denote nested backend models, which `coerceFormValues` already produces), converting `ScriptPreviewField` to TanStack Query, committing untrimmed free-solo input, relocating the `api` / `atw` test suites, `related_apps` alongside `entities`, and the hard-coded `Roboto Mono` stacks. `formatCellValue`'s `undefined` guard is already here, in a better form than the port has. Gates: type-check clean across every package, oxlint 0 errors, 1177 tests pass (742 in @sep/framework), oxfmt clean.
There was a problem hiding this comment.
Pull request overview
Backports a set of correctness/stability/accessibility fixes to the in-repo copies of @sep/api and @sep/framework so they don’t reappear during ongoing SEP↔PMM UI migration syncing.
Changes:
- Fixes several edge-case correctness issues (token refresh handler isolation, schema-driven form submission gating, dotted-path choice normalization, safer id extraction, numeric validation/coercion, stepless log handling).
- Improves stability/UX around streaming and selectors (bounded SSE reconnects, retryable host selector after failures, preserve scheduled-task kwargs on toggle).
- Addresses UI/accessibility consistency (dark-mode table surface, file-picker accessible name) and adds/updates tests + changelog fragment.
Reviewed changes
Copilot reviewed 23 out of 23 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| frontend/packages/framework/test/setup.ts | Removes an unreferenced test setup file. |
| frontend/packages/framework/src/utils/extractId.ts | Tightens string-id parsing to decimal integers only and safe integers. |
| frontend/packages/framework/src/utils/extractId.test.ts | Adds coverage for id extraction edge cases. |
| frontend/packages/framework/src/hooks/useTaskLogs.ts | Accepts step: '' as a valid “General/stepless” log line. |
| frontend/packages/framework/src/hooks/useResolvedServiceField.ts | Surfaces useServices error state to distinguish “no match” vs “failed lookup”. |
| frontend/packages/framework/src/hooks/useExecutionEvents.ts | Bounds transient SSE retry loop and surfaces persistent failures. |
| frontend/packages/framework/src/components/SchemaListView/SchemaListView.tsx | Uses a theme-aware opaque surface instead of pinning white background. |
| frontend/packages/framework/src/components/SchemaFormRenderer/utils/validationMapper.ts | Updates numeric validation/coercion to avoid parseInt/parseFloat truncation. |
| frontend/packages/framework/src/components/SchemaFormRenderer/SchemaFormRenderer.tsx | Prevents section-violation submits from being treated as successful by RHF. |
| frontend/packages/framework/src/components/SchemaFormRenderer/SchemaFormRenderer.test.tsx | Updates rendering assertions and adds regression coverage for unsaved-changes guard. |
| frontend/packages/framework/src/components/SchemaFormRenderer/hooks/useCascadingField.ts | Aligns cascading clears/readiness with '' sentinel to avoid uncontrolled inputs and premature fetches. |
| frontend/packages/framework/src/components/SchemaFormRenderer/fields/FileField.tsx | Adds an accessible name to the file-picker icon button. |
| frontend/packages/framework/src/components/SchemaDrivenApp/SchemaDrivenApp.tsx | Passes capabilities + error props through the edit-form render slot. |
| frontend/packages/framework/src/components/SchemaDrivenApp/AppTaskEditPage.tsx | Normalizes choice defaults for dotted-path one-of branch fields using path-aware getters/setters. |
| frontend/packages/framework/src/components/SchemaDrivenApp/AppTaskEditPage.test.tsx | Adds coverage for dotted-path one-of normalization and non-mutation. |
| frontend/packages/framework/src/components/SchemaDrivenApp/AppListPage.tsx | Guards optional list_view and renders “Not found” when absent. |
| frontend/packages/framework/src/components/SchemaDrivenApp/AppDetailPage.tsx | Avoids crashing when list_view is absent; falls back to task fields. |
| frontend/packages/framework/src/components/ScheduledTasksPanel/ScheduledTasksPanel.tsx | Preserves kwargs on enable/disable toggle when present in the response payload. |
| frontend/packages/framework/src/components/HostSelector/StandaloneHostSelector.tsx | Keeps selector enabled after query failure so onOpen can retry. |
| frontend/packages/framework/src/components/HostSelector/StandaloneHostSelector.test.tsx | Updates behavior expectations to match retryable-on-open design. |
| frontend/packages/framework/src/components/HostSelector/HostSelector.tsx | Uses path-aware error lookup for dotted field names (get(errors, name)). |
| frontend/packages/api/src/client.ts | Prevents _onRefreshed sync throws from rejecting the shared refresh promise. |
| changelog.d/SEP-1760.fixed.md | Documents the user-visible fixes in a changelog fragment. |
Suppressed comments (1)
frontend/packages/framework/src/components/SchemaFormRenderer/utils/validationMapper.ts:133
Number(raw)will coerce whitespace-only strings to0(e.g.' '→ 0). Because this block only treats''as empty, a whitespace-only numeric field would be coerced and submitted as 0. Consider trimming string input first and treating a trimmed-empty string as empty (coerce toundefined).
if (field.type === 'integer' || field.type === 'float') {
if (raw === '' || raw === undefined || raw === null) {
setAtPath(out, field.name, undefined);
continue;
}
CodeRabbit reviewed the PMM side again (percona/pmm#5653) after the first round landed there; these are the follow-ups, ported here for the same reason as the original set. - `extractId`: hold the numeric branch to the same bar as the string one. `Number.isSafeInteger` rejects 1.5 and unsafe integers, which previously passed straight through and enabled a service lookup nothing can satisfy — the exact asymmetry the string branch was tightened to close. - `ScheduledTasksPanel`: the preserved `kwargs` now accepts either wire shape. `PeriodicTaskResponse` does not declare the field, so a decoded object is as likely as a JSON string, and only the string case was kept — the object case fell back to '{}' and wiped the arguments this guard exists to protect. A partial-update route for the toggle would remove the whole class of problem; that is a backend change. - `client.ts`: log when the injected `_onRefreshed` handler throws. The isolation is right, but swallowing it silently left the auth layer without the rotated token while `refreshAccessToken` returned it as applied, with no diagnostic. The trace carries neither the token nor its expiry. - `SchemaFormRenderer` test: restore the `removeEventListener` spy through `onTestFinished`, so a failing assertion cannot leak the spy into the rest of the file. - `HostSelector` had the same wedge as `StandaloneHostSelector`, in all three of its branches: the control was disabled on a hosts-query failure while `onOpen` held the only `refetch()` trigger, so one failure blocked recovery until the page remounted. Not flagged by the review — the diff hunk did not reach those lines — but the same defect, so it is fixed here rather than left behind. Declined, with the reasoning recorded on the PMM PR: removing `ctrl.abort()` from `useExecutionEvents`' terminal path. The claim was that the library creates a fresh `AbortController` per retry, so aborting ours is ineffective. `fetch-event-source@2.0.1` registers an `abort` listener on the signal we pass and calls its own `dispose()` from it, which clears the retry timer and aborts the in-flight request; `curRequestController` is a separate internal controller. Every other terminal path in this file (`finish`, `sep-error`) stops the stream the same way, so the call stays. Gates: type-check clean across every package, oxlint 0 errors, 1178 tests pass (743 in @sep/framework), oxfmt clean.
|
Second commit (
Declined: removing The changelog fragment was updated in place (the executor-host bullet now covers both selectors rather than just the standalone one). Gates: type-check clean across every package, oxlint 0 errors, 1178 tests pass (743 in |
`Number(' ')` is 0, so a numeric field containing only whitespace passed
the validate rule and `coerceFormValues` submitted 0 for a value the user
never typed. RHF's built-in `required` rule does not fire on it either,
since the string is non-empty.
Both the validate rule and the coercion now trim string input and treat a
trimmed-empty string as empty: required fields report the required error,
optional fields serialise as absent.
`Number(' ')` is 0, so a numeric field containing only whitespace passed
the validate rule and `coerceFormValues` submitted 0 for a value the user
never typed. RHF's built-in `required` rule does not fire on it either,
since the string is non-empty.
Both the validate rule and the coercion now trim string input and treat a
trimmed-empty string as empty: required fields report the required error,
optional fields serialise as absent.
Ported from SEP-1760 (percona/SEP#1283, 4bf7adcc4), where a Copilot review
caught it on the same code.
`Number(' ')` is 0, so a numeric field containing only whitespace passed
the validate rule and `coerceFormValues` submitted 0 for a value the user
never typed. RHF's built-in `required` rule does not fire on it either,
since the string is non-empty.
Both the validate rule and the coercion now trim string input and treat a
trimmed-empty string as empty: required fields report the required error,
optional fields serialise as absent.
Ported from SEP-1760 (percona/SEP#1283, 4bf7adcc4), where a Copilot review
caught it on the same code.
Signed-off-by: Ignacio Durand <nachodurand@gmail.com>
Ported from the PMM side (percona/pmm#5728), where CodeRabbit caught it in the mirrored config. Oxlint enables only `eslint`, `typescript`, `unicorn` and `oxc` by default — `--react-plugin` and `--import-plugin` are opt-in, and the config-file equivalent is the `plugins` array, which we never set. Every `react/*`, `react-hooks/*` and `import/*` rule in `oxlintrc.json` was therefore inert: oxlint parses the key, matches no plugin, reports nothing. Measured on the pinned oxlint 1.64: 108 rules over 474 files before, 135 rules over 491 files after. `plugins` replaces the default set rather than extending it, so typescript, unicorn and oxc are named explicitly. Turning the rules on surfaced one error and eight warnings: - `react/no-children-prop` in `appRouteGuard.ts`. The rule wants JSX, but that module is plain `.ts`, and moving `element` to `createElement`'s third argument does not type-check either — `AppDisabledGuardProps.children` is required and the overload does not satisfy it. Suppressed on the line with that reasoning rather than weakening the component's contract. - Four `react-hooks/exhaustive-deps` (AppListPage, ScriptPreviewField, ExecutionEventsPanel, useExecutionEvents) and four `import/no-cycle` (the `App` ↔ `routes` pairs in alters and backup_mongo). All pre-existing, all warnings, so CI stays green; they are left for their own triage rather than mixed into this backport. Two smaller items from the same review: - `$schema` pointed at the Oxc `main` branch while the workspace pins oxlint 1.64.0, so editor validation could drift from the CLI. Now the package-local schema, matching `.oxfmtrc.json`, which already does this. - `*.config.ts` / `*.config.js` were excluded from linting, which hid real code — the Vite and Vitest configs are as much a part of the build as anything under `src`. Patterns dropped; that is where the 17 extra linted files come from. No changelog fragment: tooling-only, no user-visible behaviour change. Gates: oxlint 0 errors, type-check clean across every package, 1178 tests pass, oxfmt clean.
|
Ported one more finding from the PMM side ( Oxlint never ran our React or import rules. Only
What the rules found once live — 1 error, 8 warnings:
Also from that review: No changelog fragment for this one: tooling-only, no user-visible behaviour change. The existing Gates: oxlint 0 errors, type-check clean across every package, 1178 tests pass, oxfmt clean. |
`Number(' ')` is 0, so a numeric field containing only whitespace passed
the validate rule and `coerceFormValues` submitted 0 for a value the user
never typed. RHF's built-in `required` rule does not fire on it either,
since the string is non-empty.
Both the validate rule and the coercion now trim string input and treat a
trimmed-empty string as empty: required fields report the required error,
optional fields serialise as absent.
Ported from SEP-1760 (percona/SEP#1283, 4bf7adcc4), where a Copilot review
caught it on the same code.
Signed-off-by: Ignacio Durand <nachodurand@gmail.com>
`Number(' ')` is 0, so a numeric field containing only whitespace passed
the validate rule and `coerceFormValues` submitted 0 for a value the user
never typed. RHF's built-in `required` rule does not fire on it either,
since the string is non-empty.
Both the validate rule and the coercion now trim string input and treat a
trimmed-empty string as empty: required fields report the required error,
optional fields serialise as absent.
Ported from SEP-1760 (percona/SEP#1283, 4bf7adcc4), where a Copilot review
caught it on the same code.
Signed-off-by: Ignacio Durand <nachodurand@gmail.com>
Upstreams the change PMM needed for the embedded UI (percona/pmm#5739), so the next sync of this tree into PMM does not clobber it. `refreshAccessToken()` hardcoded `POST /oauth/refresh` as the only way to obtain a token. That is right for this SPA, but PMM embeds SEP with no refresh cookie at all — it trades its own session cookie for a bearer via `POST /oauth/session/exchange` (SEP-1692) — so there the default 401s on every recovery attempt, and the whole retry machinery is dead weight. `setTokenMinter()` replaces just that call and defaults to the existing one, so nothing changes here. Everything downstream was already minter-agnostic: the single-flight coalescer, the axios 401 retry, and the `setOnRefreshed` notification all work the same whichever endpoint produced the token. `isTokenMintRequest` composes the existing `isRefreshRequest` and `isSessionRequest` guards, which the axios retry condition already listed separately, and is exported so the typed client shares one definition. Keeping them together matters: minting is single-flighted, so letting a mint's own 401 into the retry path would hand the interceptor the very promise it is running inside. The `openapi-fetch` transport gained the 401 retry the axios one already had. It previously only reported unauthorized, so every typed hook — `useCurrentUser` and all the generated-path ones — surfaced an expired token as a failure instead of recovering from it. This is a fix for both deployments, not just the embedded one. `fetch` consumes a Request's body, so the middleware stashes a clone taken before dispatch and replays that; only replay-eligible requests are cloned, and the replay goes through raw `fetch` so it cannot re-enter the middleware and loop.
Summary
The CodeRabbit review of the SEP-UI-into-PMM migration PR (percona/pmm#5653, PMM-15216) raised these findings against the ported copy of
@sep/api/@sep/framework. Every one of them is present in the original, so they are fixed here too — otherwise the next sync in either direction re-introduces them. PMM-only wiring (route constants, the route-level admin gate, the dev-proxy env variables) is out of scope.Token-minter seam (upstreamed, not a review fix)
The last commit is a different kind of change from the rest of this PR, so it is called out separately: it upstreams a capability PMM needed (percona/pmm#5739, PMM-15293) rather than fixing a review finding. It lands here for the same reason as everything else — otherwise the next sync clobbers it.
refreshAccessToken()hardcodedPOST /oauth/refreshas the only way to obtain a token. That is correct for this SPA. It is not workable for PMM, which embeds SEP with no refresh cookie at all: it trades its own session cookie for a bearer throughPOST /oauth/session/exchange(SEP-1692), so the default 401s on every recovery attempt and the entire retry path is dead weight there.setTokenMinter()replaces just that one call, defaulting to the existing behaviour — nothing changes for a standalone SEP deployment. Everything downstream was already minter-agnostic: the single-flight coalescer, the axios 401 retry, and thesetOnRefreshednotification do not care which endpoint produced the token.isTokenMintRequestcomposes the existingisRefreshRequestandisSessionRequestguards — which the axios retry condition already listed separately — and is exported so the typed client shares one definition rather than growing a second copy. Keeping them together is load-bearing: minting is single-flighted, so letting a mint's own 401 into the retry path hands the interceptor the very promise it is running inside, an await on itself that never settles. There is a regression test that times out rather than fails if that guard is lost.openapi-fetchtransport gained the 401 retry the axios one already had. This one is a fix for both deployments: it previously only reported unauthorized, so every typed hook —useCurrentUserand all the generated-path ones — surfaced an expired token as a failure instead of recovering.fetchconsumes a Request's body, so the middleware stashes a clone taken before dispatch and replays that; only replay-eligible requests are cloned, and the replay goes through rawfetchso it cannot re-enter the middleware and loop.@sep/apigrows 10 tests for this: minting through a registered minter, coalescing a burst of 401s into one exchange, the self-await regression guard, a null-resolving minter, restoring the default, and on the typed side mint-and-replay, replaying a request body, replaying at most once, one mint across concurrent 401s, and no recovery attempt on a minting endpoint's own 401.Correctness
refreshAccessTokencalled the injected_onRefreshedhandler inside the async executor, so a synchronous throw from it rejected the sharedrefreshInFlightpromise — every awaiting caller saw a failed refresh, and a force-logout, for a cookie rotation that had already succeeded on the backend. The function's own comment says this must not happen.SchemaFormRendererreturned fromhandleFormSubmiton a section violation, which react-hook-form reads as a successful submit (isSubmitSuccessful = true).useUnsavedChangesGuardisisDirty && !isSubmitSuccessfuland only re-arms whensubmitErroris truthy — never on this path — so the guard stayed disarmed: nobeforeunloadprompt, noUnsavedChangesBlocker, and the user could navigate away from a dirty form and lose it. The gate now runs in the submit event handler, ahead ofhandleSubmit.normalizeChoiceDefaultsread and wrote flat keys, butflattenSectionFieldsalso returnsone_ofbranch fields, whose names are dotted paths stored nested. A case-mismatched nested choice value was never canonicalised and rendered as an empty selection — the exact failure that function exists to prevent.AppListPage/AppDetailPagedereferenced the optionallist_view(schema.list_view!.columns), reachable through an unresolved entity route. The list page now rendersNot found; the Overview tab falls back to an empty column set and still lists the task's own fields.HostSelectorlooked uperrors[name], which never resolves for a dotted branch-field name, so an affected field showed no validation error.extractIdusedNumber, which turns a whitespace-only string into0and accepts'1.5'/'0x10'. Each result reads as a resolvable inventory id downstream:useResolvedServiceFieldenables a lookup for service0, andSchemaSelectorfiresuseSchemas({ serviceId: 0 })for a service that cannot exist.validationMappersubmittedparseInt('2.5', 10)as2andparseFloat('3.14invalid')as3.14. Numeric fields now validate withNumber.isFinite(plusNumber.isIntegerforinteger) and coerce withNumber.useTaskLogsguarded on!step, dropping a log line withstep: ''.useExecutionEventstreats''as the stepless bucket and the viewer labels it "General", so the two streams disagreed and stepless output was silently lost.Stability
useExecutionEventsreturnedundefinedfromonerrorfor every non-sentinel error, sofetchEventSourceretried forever whilesseErrorstayed unset andsseLoadingstayed true — a persistently failing endpoint (500, DNS failure) left the panel spinning and reconnecting indefinitely. Consecutive failures are now counted, reset on a successful open, and the loop stops with the error surfaced.StandaloneHostSelectordisabled its Autocomplete when the hosts query failed, butonOpenholds the onlyrefetch()trigger and a disabled Autocomplete never opens — one failure wedged the control until the page remounted.kwargs: '{}'in a full PUT, wiping the arguments of any task created with non-default kwargs.kwargsis preserved when the response carries it;'{}'stays the fallback untilPeriodicTaskResponsedeclares the field.Contract and consistency
useCascadingFieldcleared withundefinedwhilebuildFormDefaultsseeds these selector types to'', and counted''as ready — a downstream selector fetched options for an empty parent, and a bound MUI input flipped from controlled to uncontrolled.SchemaDrivenApp'srenderEditForminvocation omittedcapabilities,submitErrorandfieldErrors, so a consumer supplying the slot could render neither the 422 banner nor the inline field errors.AppTaskEditPagealready passes all three to the same slot type.SchemaListViewpinnedbgcolor: 'common.white', which renders a white table in dark mode; it now reads the mode's own opaque surface.FileField's file-pickerIconButtonhad no accessible name.useResolvedServiceFielddiscardeduseServices' error, so callers could not tell a failed lookup from an id that matched no service.packages/framework/test/setup.tswas an unreferenced sibling oftests/setup.tsregistering the jest-dom matchers but noafterEach(cleanup). Deleted, so a futuresetupFilesedit cannot silently drop DOM isolation for the package.Deliberately not ported, matching the decisions recorded on the PMM PR: production SEP routing / token exchange, flattening dotted app argument names (they denote nested backend models, which
coerceFormValuesalready produces), convertingScriptPreviewFieldto TanStack Query, committing untrimmed free-solo input, relocating theapi/atwtest suites,related_appsalongsideentities, and the hard-codedRoboto Monostacks.formatCellValue'sundefinedguard is already here — in a better form than the port has, which should go back to PMM.Tested
Automated, no manual run: this is a set of narrow fixes in already-covered code, and each behaviour change is pinned by a test.
pnpm test— 1187 tests pass (746 in@sep/framework, 103 in@sep/api), including three new/updated cases plus the 10 minter-seam cases above:extractId.test.ts(new): whitespace-only, fractional, hex and partly-numeric strings all resolve tonull.SchemaFormRenderer.test.tsx: a violation-blocked submit must not drop thebeforeunloadlistener. Verified this test fails against the pre-fix ordering, so it pins the actual regression.AppTaskEditPage.test.tsx: aone_ofbranch choice stored atsource.transportis canonicalised, and the input object is not mutated.StandaloneHostSelector(disabled-on-error → enabled and retryable on open) and theSchemaFormRendererfield-rendering case (the file-picker button's new accessible name).pnpm type-checkclean across every package;pnpm lint0 errors;oxfmtclean; pre-commit hooks pass./execution-eventsendpoint.Checklist
make test)make run-pre-commit)make makemigrations) — N/A, no model changesaria-labeland the numeric validation messageschangelog.d/if the change is user-facing (make changelog-add), or confirmed N/A