Skip to content

fix(plugin-form): narrow navigateOnSuccess to the ruled relative-only acceptance set and escape the interpolated id - #6344

Merged
os-support-ai merged 2 commits into
mainfrom
claude/issue-5034-navigateonsuccess-url-contract
Aug 25, 2026
Merged

fix(plugin-form): narrow navigateOnSuccess to the ruled relative-only acceptance set and escape the interpolated id#6344
os-support-ai merged 2 commits into
mainfrom
claude/issue-5034-navigateonsuccess-url-contract

Conversation

@claude

@claudeclaudeBot commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Fixes#5034

⚠️Clause-② gate — this PR changes contract accept/reject behaviour. It carries needs:contract-review, stays a draft, and must not be marked ready or auto-merged. Landing needs a second seat or the maintainer.

Head b96b2ba4a. Points 1 and 2 of the card landed in PR #5558 and are not in scope; this is point 3 alone — the url contract — under the maintainer ruling recorded on the card 2026-08-17 (verbatim 「同意」).

What the ruling says, and what each clause became

As a compat alias it runs under the os#7496 ruled semantics: relative-only (same-origin absolutes refused like any out-of-contract value), navigation through the injected seam ruled on #4989 once it lands (mount-aware), and the interpolated id URL-escaped.

ruling clauseimplementation
relative-only; same-origin absolutes refused at the doorresolveSuccessNavigate admits via a relative-reference test instead of the same-origin guard
the interpolated id URL-escapedthe substituted value goes through encodeURIComponent; the template is the author's and is untouched
navigation through the injected seam (landed)already true; with admission now relative-only the seam handoff is unconditional and the window.location.assign fork is deleted
the {id} / {recordId} dialect stays for existing authorsunchanged, and pinned
docs point at submitBehaviornavigateOnSuccess marked @deprecated in packages/types, with the contract restated in the zod .describe()

Premise re-derivation — every PM assumption re-measured on my own checkout

origin/main resolved to c38162d7c for me too, so line numbers are comparable.

#assumptionmeasured
M1admission door is resolveSuccessNavigate at successBehavior.ts:52-61, admitting via isSameOriginUrl (:36)held, exactly those lines
M2interpolation at :59 is raw String(id)held
M3execution fork at ObjectForm.tsx:957/960 and WizardForm.tsx:681/684held, both to the line
M4isSameOriginUrl exported, docblock :18-35 narrates "survives because resolveSuccessNavigate still needs it"held
M57 dangling objectui#5548 references at the 7 named locationsheld — 7, at exactly those locations
M6no open PR touches packages/plugin-form/held — all 6 open PRs (#6337, #6334, #6315, #6082, #5400, #4958) fetched and none touches the package; also re-checked for packages/types/src/objectql.ts and objectql.zod.ts, none

No PM assumption was false. One addition to M4 worth recording: the census of isSameOriginUrl found one code reader (resolveSuccessNavigate itself) and nine prose readers across five files, not the zero the "delete it" route implies. Handled below.

The one deliberate deviation from the suggested route, with the measurement behind it

The route said "replace the isSameOriginUrl(url) admission with a relative-only test" without naming which test. Three candidates, and the choice is the reviewable decision in this PR:

A — spell the relative test locally in successBehavior.ts (taken).

B — import isAppRelativeDestination from thankYouRedirectNavigation.ts. Rejected. It answers WHO TRAVELS to an already-accepted thankYouPage.redirectUrl, and objectui#5112 ruled that key the opposite way on this very shape — a same-origin absolute there deliberately keeps browser-level navigation. Binding one symbol to two keys ruled in opposite directions means the next ruling on either silently moves the other. That is the hazard the dispatch's R4 names, and importing rather than editing does not remove it, it just defers it.

C — derive the verdict from @objectstack/spec's FormViewSchema, the way the ruled sibling submitRedirect.ts does. Rejected on a measurement, not on taste. I ran the corpus through it:

"/r/r1" ACCEPT
"r/r1" REFUSE: must start with `/`
"?a=1" REFUSE: must start with `/`
"#x" REFUSE: must start with `/`
"https://x.example/r" REFUSE: RELATIVE path only
"//x.example/r" REFUSE: leading `//` is protocol-relative
"/r/{unknown}" REFUSE: interpolates ONLY declared record fields
"/r/a/b c" REFUSE: must not contain whitespace
"\\evil.example/r" REFUSE: must not contain a backslash

That schema is considerably stricter than "relative-only". Adopting it would additionally refuse document-relative, query-only, fragment-only, whitespace-bearing and stray-brace templates on a compat key the same ruling orders kept working for existing authors. Those refusals are not in the ruling, and narrowing beyond a ruling on a deprecated alias is not mine to decide. So option A implements exactly the clause that was ruled and nothing more.

The cost of A is a second spelling of one string test. That cost is paid for explicitly rather than hoped away: the two predicates agree pins the two over a shared token-free corpus, so a divergence is loud on the day it happens instead of silently moving this key's acceptance set. If review prefers C's breadth, that is a contract decision and this is the place to make it — the code change is one line.

Proving the deleted branch was dead, not just unwanted

The route required proof before deletion. It is a corpus property, asked of the real predicate the deleted arm branched on:

// every accepted destination is app-relative ⇒ nothing could reach the else-armfor(const[template,id]ofCORPUS){constresult=resolveSuccessNavigate(template,{ id });if(result===null)continue;expect(isAppRelativeDestination(result)).toBe(true);}

It is a real change detector, not a tautology: under mutation A (admission widened back to same-origin) it goes RED — the corpus immediately produces an accepted destination that is not app-relative, i.e. one that would have reached the deleted arm. The absent-seam fallback is untouched and still window.location.assign, inside useSubmitRedirectNavigation; the negative-control case pins it.

Assertions that moved, with the ruling clause per row

⛔ Nothing here was rewritten to match new behaviour. Each row states the old value, the new value, and the clause that moves it.

locationoldnewclause
mountSeam:372resolveSuccessNavigate('{id}', { id: '<origin>/r' })`${origin}/r` — the id becoming the whole destinationencodeURIComponent(`${origin}/r`) — one opaque segmentthe interpolated id URL-escaped
mountSeam:373resolveSuccessNavigate('<origin>/r/{id}', { id: 'r1' })`${origin}/r/r1` (accepted)null (refused)relative-only / same-origin absolutes refused
mountSeam:383resolveSuccessNavigate('/r/{id}', { id: 'a/b c' })'/r/a/b c' — an id silently growing a path segment'/r/a%2Fb%20c'the interpolated id URL-escaped
mountSeam:247-267 the whole same-origin absolute describe block (×2 components)asserted window.location.assign(resolved), navigate not calledasserts neither traveller runs and the toast carries NAVIGATE_ON_SUCCESS_REFUSED_NOTErelative-only / same-origin absolutes refused

Not in the dispatch's list, so flagged explicitly: that fourth row is a describe block the trap note did not name. Its subject — WHO travels to a same-origin absolute — stops existing when the value is refused at the door, so it could not stay as written. It was re-pointed at the refusal rather than deleted, which keeps the end-to-end measurement that nobody navigates.

Assertions that did NOT move, as required: the {id} / {recordId} / _id compat cases, the cross-origin refusal, and the three null cases (no template, no id, empty id). Re-asserted verbatim.

Reverse verification — predicted first, measured after

Baseline: 2 files, 33 tests, all green. Every mutation was proved on disk (removed-text count 0, injected-text count ≥1, non-empty git diff --stat) before the run, and every restore was proved by git diff HEAD empty and a blob-hash comparison against HEAD, with an empty hash read as failure.

mutationredmeasured
A — restore the pre-ruling same-origin admission8 (5 url-contract, 3 mount-seam)the 2 relative-only refusals, neither half substitutes, THE DELETED ARM IS UNREACHABLE, the two predicates agree, the 2 component absolute cases, the verdict table
B — delete the encodeURIComponent6 (5 url-contract, 1 mount-seam)the 4 escaping cases, neither half substitutes, the verdict table
C — replace the seam handoff with window.location.assign(nav)3, all mount-seampredicted 3 — exact
D — drop the refusal note from both toasts9, all mount-seampredicted 9 — exact

⚠️One prediction was wrong and is corrected in the file rather than quietly dropped. The url-contract docblock predicted that under mutation B one case would go red by turning ACCEPTED-and-escaped into REFUSED. That phenomenon is real but lands in the mount-seam verdict table, not in the escaping block:

FAIL navigateOnSuccess.mountSeam.test.tsx > `resolveSuccessNavigate` verdicts > answers the ruled acceptance set
AssertionError: expected null to be 'http%3A%2F%2Flocalhost%3A3000%2Fr'

All five url-contract failures under B are value mismatches instead:

FAIL … the interpolated id is URL-escaped > escapes structure out of the substituted value
AssertionError: expected '/r/a/b c' to be '/r/a%2Fb%20c'
FAIL … the interpolated id is URL-escaped > cannot let an id become the destination
AssertionError: expected '/r/https://evil.example.com/steal' to be '/r/https%3A%2F%2Fevil.example.com%2Fs…'
FAIL … neither half substitutes for the other > shows which half refuses which value
AssertionError: expected '/r/https://evil.example.com/steal' not to contain '/steal'

Also recorded: NO WIDENING stays green under mutation A, correctly rather than incidentally. It asks about ORIGIN, and the pre-ruling admission was itself a same-origin test, so it has nothing to detect there — it is a detector for a future widening past same-origin. Counting it as a mutation-A detection would have overstated the file.

One test caught a defect in its own first draft: the equivalence pin's expect(templates.length).toBeGreaterThan(0) guard fired, because every corpus template carries an {id} token and the token-free filter selected nothing. It now has its own token-free corpus. Without that guard the loop would have passed vacuously.

No widening — the T3 argument, stated because it is not obvious

Escaping changes what an out-of-contract input resolves to, so "did anything become reachable?" deserves an argument rather than an assertion. Every destination the helper now returns is a relative reference; a relative reference cannot carry an authority (RFC 3986), so it always resolves to the current origin — which is exactly what the guard it replaces asked. The accepted destination set is therefore a strict subset of the previous one. Machine-checked, not argued: NO WIDENING asserts it over the corpus, with an accepted > 0 guard so it cannot pass vacuously.

Concretely, an id spelling https://evil.example.com/steal used to make the whole destination that address (template {id}) or leave it embedded raw; it now becomes one escaped segment inside the path the author wrote. Nothing previously refused is now followed.

The 7 dangling objectui#5548 references

All 7 corrected; the repo now greps clean for 5548. The card is the point-3 home, per the 2026-08-25 triage ruling, so each site now names #5034 or the ruling rather than a number that was never filed:

  • ObjectForm.tsx:939 and WizardForm.tsx:60 — rewritten as part of the comments the change touches anyway.
  • navigateOnSuccess.mountSeam.test.tsx:20,356,362,371,381 — the "acceptance set this card does not touch" framing is now false; the section is retitled and points at the new contract file.

Scope note — one bounded inline fix, declared

Deleting isSameOriginUrl (zero code readers left after the narrowing) leaves prose in three other test files pointing at a symbol that no longer exists — the same defect class as the dangling #5548. Line drawn and applied: prose that asks a reader to perform a mutation gets a marker naming the deletion; prose that merely narrates history ("the old line asked X") does not.

  • marked: submitRedirect.test.ts:53,91, ObjectForm.submitRedirect.test.tsx:20,35, WizardForm.submitRedirect.test.tsx:30
  • left alone as history: ObjectForm.tsx:867, WizardForm.tsx:616, WizardForm.successBehavior.test.tsx:62, ObjectForm.submitBehavior.test.tsx:43

isAppRelativeDestination, submitRedirect.ts and submitRedirectNavigation.ts are not touched — confirmed absent from the diff. objectui#5112's ruling is not reopened.

Gates — derived from the CI job step lists, each with its own verdict line

Derived by reading .github/workflows/{ci,lint,changeset-guard,changeset-presence,control-bytes,vi-mock-specifiers,readme-exports,…}.yml, not from memory. Run at head b96b2ba4a.

gateexitits own verdict line
vitest run packages/plugin-form/0Test Files 66 passed (66) · Tests 666 passed (666)
type-check (plugin-form + types)0packages/types type-check: Done · packages/plugin-form type-check: Done
lint (plugin-form + types)0✖ 673 problems (0 errors, 673 warnings) · ✖ 240 problems (0 errors, 240 warnings)
check:control-bytes0✅ check-control-bytes: OK (scanned 5223 tracked text file(s); skipped 85 binary)
changeset:check0✅ All workspace packages are in the changeset fixed group. · ✅ No changeset declares a 'major' bump.
check:spec-symbols0✅ spec symbol derivation: 1305 files scanned against 4959 spec export names
type-check:coverage0✅ test type-check coverage: 41/41 packages compile their tests
lint:coverage0✅ lint coverage: 46/46 packages linted, 0 with outstanding errors
check:vi-mock-specifiers0✅ check-vi-mock-specifiers: OK (3747 tracked source file(s)…)
check:phantom-deps0✅ Every in-scope import is declared by the package that publishes it.
check:self-import0✅ No package names itself inside its own src/.
check:readme-exports1⚠️NOT MEASURED, prerequisite unmet — see below

Exit codes were captured before any pipe (cmd > file 2>&1; EXIT=$?), never off a tail.

check:readme-exports fails locally for a reason that is not this diff: 298 self-import(s) could not be judged … its type entry ./dist/index.d.ts is not on disk -- run 'pnpm build' first. Its own workflow runs a full pnpm build first; this worktree built only the plugin-form dependency closure. Reading it as red would be reading a missing prerequisite as a measurement. The relevant question it would ask was answered directly instead: no README under packages/ mentions isSameOriginUrl, resolveSuccessNavigate or navigateOnSuccess, and the deleted symbol was never on the package entry (plugin-form/src/index.tsx does not export from successBehavior), so no published export was removed.

pnpm lint repo-wide is a declared narrowing, not a skip. Three pieces of evidence, all three required:

  1. Universe from eslint's own config, not from my judgement: pnpm lint is turbo run lint = eslint . per package, and lint:coverage reports the universe as 46/46 packages linted.
  2. Count from --format json: 95 files (plugin-form) + 121 files (types) = 216 files linted, 0 errors.
  3. Invariance for untouched files: eslint.config.js configures noparserOptions.project and no projectService, so type-aware linting is off and a file's verdict is a function of its own contents plus config. This diff changes no eslint config and no file in any other package, so it cannot move the verdict on any file I did not lint.

CI runs the full farm regardless; this narrowing is a measurement, not an omission.


Generated by Claude Code

…e the interpolated id
The admission door (`resolveSuccessNavigate`) accepted any same-origin value,
including an ABSOLUTE url the author spelled out in full, and interpolated
`{id}` / `{recordId}` raw. Per the 2026-08-17 maintainer ruling this key is the
pre-ruling ancestor of the `submitBehavior` family, so as a compat alias it runs
under the semantics objectstack#7496 ruled there: relative paths only, with the
substituted value URL-escaped when the destination is built.
Relative-only and escaping are separate rules and neither implies the other:
relative-only says where a destination may START, so it cannot see structure a
token injects further along; the escape runs only on record data, so it cannot
see an absolute the author wrote. Both are applied, in that order.
With every accepted destination relative, the `window.location.assign(nav)` arm
at both call sites is unreachable and is deleted — the caller now judges the
destination once, at the admission door, rather than twice. The absent-seam
fallback inside `useSubmitRedirectNavigation` is untouched.
The relative test is spelled locally rather than importing
`isAppRelativeDestination`: that predicate answers WHO TRAVELS to an accepted
`thankYouPage.redirectUrl`, a key ruled the OPPOSITE way on this very shape, and
the two must stay free to diverge. A test pins them agreeing so a divergence is
loud rather than silent.
Both mount-seam predictions were exact (3 red and 9 red). The url-contract
file's prediction about mutation B was right about the phenomenon and wrong
about its address: the accepted-turns-refused case lands in the mount-seam
verdict table, not in the escaping block, where all five failures are value
mismatches. Corrected to what was measured rather than left as written.
Also records why the corpus properties split under mutation A: `THE DELETED ARM
IS UNREACHABLE` goes red (so the dead-branch proof is a real change detector,
not a tautology) while `NO WIDENING` correctly stays green, because it asks
about origin and the pre-ruling admission was itself a same-origin test.
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 52 chunks)3223.6 KB3266.6 KB
Main entry chunk (gzip)154.2 KB350 KB
Entry fileindex-B6N51tUs.js
StatusPASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)10.96KB4.16KB
app-shell (runtime-config.js)18.10KB6.51KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)506.08KB114.67KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)171.74KB47.48KB
fields (index.js)238.89KB60.02KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)9.53KB3.38KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)4.64KB1.50KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)1.93KB0.88KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.62KB12.83KB
plugin-charts (index.js)64.66KB18.32KB
plugin-chatbot (index.js)188.21KB44.67KB
plugin-dashboard (index.js)133.35KB34.45KB
plugin-designer (index.js)211.95KB42.75KB
plugin-detail (index.js)245.10KB62.31KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)126.93KB30.90KB
plugin-gantt (index.js)164.14KB39.87KB
plugin-grid (index.js)201.21KB54.43KB
plugin-kanban (index.js)52.83KB14.55KB
plugin-list (index.js)111.94KB27.24KB
plugin-map (index.js)20.09KB6.62KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.49KB11.93KB
plugin-timeline (index.js)26.49KB7.59KB
plugin-tree (index.js)9.26KB3.13KB
plugin-view (index.js)84.55KB20.74KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)54.84KB18.43KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)1.35KB0.70KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)12.13KB3.65KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)7.54KB2.63KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)0.20KB0.18KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.87KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.49KB2.14KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@os-support-ai
os-support-ai marked this pull request as ready for review August 25, 2026 14:39
@os-support-ai
os-support-ai added this pull request to the merge queueAug 25, 2026
Merged via the queue into main with commit 636b236Aug 25, 2026
28 checks passed
@os-support-ai
os-support-ai deleted the claude/issue-5034-navigateonsuccess-url-contract branch August 25, 2026 14:51
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

1 participant

@os-support-ai