Skip to content

fix(console): FormPage 的 redirect 按 os#7496 裁定当 in-shell 相对路由,越契约目标响亮拒绝 (#4190) - #4992

Merged
yinlianghui merged 4 commits into
mainfrom
claude/issue-4190-submit-redirect-contract
Aug 17, 2026
Merged

fix(console): FormPage 的 redirect 按 os#7496 裁定当 in-shell 相对路由,越契约目标响亮拒绝 (#4190)#4992
yinlianghui merged 4 commits into
mainfrom
claude/issue-4190-submit-redirect-contract

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Fixes#4190

前提测量(先行,三项)

① GA spec 内 FormView.submitBehavior 的裁定后形状 —— os#7496(2026-08-11 裁定)经 PR os#7657 落地,本仓 pin 已由 PR #4953 升到 @objectstack/spec17.0.0 GA,^17.0.0 覆盖,实测安装版本 17.0.0。形状(实测 parse,非读文档推断):

  • submitBehaviorz.discriminatedUnion('kind', …) 四成员;redirect 成员为 { kind: 'redirect', url: z.string().superRefine(…), delayMs?: int >= 0 }
  • url 的 refine 按「最具体优先」顺序拒绝七族:空串 / 前导 scheme(含 javascript:data:)/ 前导 // 协议相对 / 任意反斜杠 / 空白与 C0-DEL 控制字符 / 非 {{record.field_name}} 形状的花括号 / 不以 / 开头。
  • 插值仅 {{record.field_name}}(字段段为扁平 snake_case,与 object.fields 键同一文法),且裁定把「每个插值值 URL 转义」划给构建重定向的时刻(消费侧)。
  • delayMs 仍在契约内 → 延迟语义保留。

即卡正文那句「undecided whether it is a console route or an arbitrary destination」已被裁定解决:绝对目标出契约

FormPage.tsx redirect 臂现状 —— 行号漂移已重定位(:574 → :1018 → :1102),代码同一行仍在:setTimeout(() => window.location.assign(behavior.url), delay)。前提成立。

withConsoleBase 现行为 —— path.startsWith('/_') 直通,否则拼 BASE_URL 前缀。卡里「不能简单套 withConsoleBase」的理由仍然成立:它会把绝对值拼坏而不是修好。

改动

redirect 臂按裁定当 in-shell 相对路由走 router 导航。 本渲染器两个挂载(/f/:slug/forms/:name)都是 console 路由内的兄弟路由,故 navigate() 由路由自己施加挂载前缀 —— 而整页跳转看不到 basename,把 authored 的 /objects/lead 相对 origin 根解析,直接把提交者甩出 SPA(#4181 在 auth 页修掉的同一类)。绝对值既已在门口被拒,withConsoleBase 也就不需要了:挂载只有一个来源(注入的 base href 元素,路由与该 helper 读的是同一个),不再有前缀算术。

越契约目标响亮拒绝。 写入成功、仅目标越契约时:确认提交(「Your submission has been received.」)+ 屏幕上显示拒绝原因 + toast。既不静默丢弃,也不照跑,更不会让提交者盯着一个不该发生的「Redirecting…」。

插值 + 转义(裁定 point 2 的消费侧半边)。 此前 token 从未被替换 —— 契约接受 {{record.id}},而消费侧原样跳转会把花括号送进 URL。作用域是「本次提交刚写入的记录」:提交值 + 服务端回显的 record 覆盖其上 + id;id 用的是 created-record同一条规则(editingId ?? readCreatedRecordId(result)),所以一个 {{record.id}} 不会在两条臂里解析出两个答案。

形状裁决不在本 app 重写一遍。resolveSubmitRedirect 在使用时刻直接 parse spec 自己的 FormViewSchema(最小 view,只提交这一个值受审,只回读该 path 上的 issue —— 整份存量 view 会因任何无关键被 strict schema 拒掉,那样一次元数据漂移就能连带毁掉重定向)。

两条轴上都是它更优:

  • 长期:仓里没有第二份安全规则可漂移。裁定明说「绝对源白名单等有度量的需求」再放宽 —— 届时 console 跟着 pin 走,不必再改这里一次。这正是本卡自己卡了六天的 pin-lag 类。
  • 让 AI 写的元数据难以出错:作者读到的是 spec 自带的处方(点名 key、给出该写什么、并指出真要往站外跳应该用 app navigation item { type: 'url', url }),与发布门说的是同一句话,不可能各说各话。

反面就是「本地重写七族 + 手写 parity 表」:scripts/check-spec-symbol-derivation.mjs 存在的理由正是本仓为这个形状付过四次代价(os#4115、#4074/#4588/#4592)—— 手抄件在原件挪动的那个 release 之前,能通过一切值比较。

delayMs 语义保留,但等待移入绑定组件的 effect:延迟期间离开页面的提交者不再被活过页面的定时器拽回来。

反向验证(先预判后跑,commit 后变异,git checkout 还原)

变异预判实测
① 回退到整页跳转路由钉红rendered 7 红(5 导航 + 2 拒绝);单测文件全绿
② 撤掉拒绝逻辑、喂绝对 URL拒绝钉红恰好 2 红(两条拒绝),6 条契约内绿
③ 合法相对 URL 对照保持绿变异 ② 下 6 条绿;见下

① 有一处值得写下来:该变异杀的是调用点,submitRedirect.test.ts 34 条全绿 —— 模块仍正确、只是无人使用。所以机制必须钉在 rendered 页,单测钉不住。唯一在 ① 下仍绿的 rendered 用例是那条「整页跳转会解析到哪」的说明性断言,它陈述的是 URL 解析事实、与组件无关。

④ 额外一跑推翻了我自己写下的理由,已按实测改正。 删掉转义、原样插值 → 两文件共 10 红,但重新 parse 发射值不足以抓住未转义的值:/t/{{record.slug}}(slug 为地址)原样插值得到 /t/https://evil.example/steal,以 / 开头、无前导 scheme,是完全合法的相对路径,却指向作者没写的地方。relative-only 管的是路径从哪开始,对更靠后注入的结构无话可说 —— 真正抓住它的是「转义后的值仍在其中」那条断言。schema oracle 只抓那些原样形式在任意位置都破坏形状的值(反斜杠、空白、控制字符)。故该块两条断言覆盖「转义够不够」的两个不相交半边,不是双保险;文档与 commit message 已改成这个测得的说法。

测试

pnpm exec vitest run apps/console/ → 52 files / 604 tests 全绿
pnpm exec vitest run apps/console/src/components/ → 10 files / 194 tests 全绿
├─ submitRedirect.test.ts 34 passed(含以真 FormViewSchema 为 oracle 的验判平权表)
└─ FormPage.redirect.test.tsx 8 passed(basename 挂载下 rendered)
pnpm --filter '@object-ui/console' type-check → 通过
pnpm --filter '@object-ui/console' lint → exit 0(200 warnings 全为既存,新增文件 0 条)
check:control-bytes / spec-symbols / phantom-deps / self-import → 全绿

以发射值解析钉契约:两个测试文件都用真的FormViewSchema 当 oracle(specAccepts() 就是授权门做的那次 parse),既钉「拒绝族与契约逐一同判」,也钉「本模块发射的字符串本身仍是契约接受的值」。所以它们是契约的漂移探测器,而不是我这份实现的意见快照:spec 日后收窄或放宽,它们跟着走。

rendered 测试全部挂在 /_console 挂载下 —— 裸 / 部署上前缀与不带前缀的拼写重合,那正是这个 bug(和 #4181 的兄弟缺陷)藏身之处。

顺带发现(⛔ 未并入本 PR,已另立卡)

按 Prime Directive #10 立卡不并修;两张都未指派。两张都先做了关键词搜索(open issues 无同类,仅命中 #4190 自身)。

一处需要读者知情的读法

spec 的 describe 文本把 point 3 写成「a verbatim redirect on the resolved relative path is the intended consumption」,并在注释里提到 window.location.assign。我把「verbatim」读作约束(不得改写、不得白名单、不得套信封),而非指定导航 API:同一份裁定通篇称它为「the in-app path」「the rooted in-app path」,而挂载下用整页跳转消费一个 in-app 路径恰恰落不进 app —— 那就是 #4190 报的缺陷本身。router 导航是让「in-app」为真的那个机制。spec 侧那句 window.location.assign 是照当时 objectui 的既有消费点描述的(#4190 正是它的来源卡),不构成对机制的裁定。若维护者读法不同,请在 review 里点出:那会把本 PR 的机制部分反过来,而拒绝与插值两半不受影响。


Generated by Claude Code

objectstack#7496(2026-08-11 裁定,objectstack#7657 落 spec,本仓 17.0.0 GA pin
已覆盖)把 `submitBehavior.url` 裁为:仅相对路径、仅 `{{record.field}}` 插值、
构建重定向时对每个插值做 URL 转义。此前 redirect 臂把authored 字符串原样交给
浏览器级整页跳转,带来两个后果:
- 裁定的「应用内路径」会离开应用。整页跳转看不到 React Router 的 basename,
而 framework CLI 为每个嵌入式部署都配挂载点,于是 `/objects/lead` 相对
origin 根解析,把提交者甩出 SPA —— 即 objectui#4181 在 auth 页修掉的同一类。
本渲染器两个挂载(`/f/:slug` 与 `/forms/:name`)都在 console 路由内,故改为
router 导航,由路由自己施加挂载前缀。不用 `withConsoleBase()`:它会给非
绝对 SPA 挂载的值加前缀,对绝对目标是弄坏而非修好。
- `{{record.field}}` 从未被替换。裁定接受该 token,并把替换与转义划给「构建
重定向」的时刻,也就是这里。作用域是本次提交刚写入的记录。
形状裁决不在本 app 重写一遍:`resolveSubmitRedirect` 在使用时刻直接 parse
`@objectstack/spec` 自己的 `FormViewSchema`,因此绝对 URL、协议相对 `//host`、
反斜杠、控制字符走私、畸形 token、文档相对路径都以 spec 自带的面向作者的
处方被拒;裁定日后放宽由 pin 跟随,而非在此再改一次。拒绝时确认提交(写入
成功,只是目标越契约)并显示原因,而不是让提交者盯着一个不该发生的跳转。
`delayMs` 语义不变,但等待移入绑定组件的 effect:延迟期间离开页面的提交者
不再被一个活过页面的定时器拽回来。
Co-authored-by: Claude <noreply@anthropic.com>
三次变异均先预判后跑,commit 后变异、git checkout 还原:
1. 回退到浏览器级整页跳转 → rendered 7 红(5 个导航 + 2 个拒绝),
`submitRedirect.test.ts` 全绿 —— 该变异杀的是调用点,模块本身仍正确
但无人使用。这正是机制必须钉在 rendered 页而非单测的原因。
2. 撤掉拒绝分支、照跑 authored 值 → 恰好 2 个拒绝红,6 个契约内绿。
3. 删掉转义、原样插值 → 两文件共 10 红。
第 3 项推翻了我原先写下的理由,按实测改正:重新 parse 发射值【不足以】
抓住未转义的值。原样插值下 `/t/{{record.slug}}`(slug 为地址)得到
`/t/https://evil.example/steal` —— 以 `/` 开头、无前导 scheme,是一个
完全合法的相对路径,却指向作者没写的地方。relative-only 管的是路径从哪
开始,对更靠后注入的结构无话可说;真正抓住它的是「转义后的值仍在其中」
这条断言。故该块两条断言覆盖的是「转义够不够」的两个不相交半边,不是
双保险。
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Main entry (gzip)25.3 KB350 KB
Entry fileindex-CaA_SiRo.js
StatusPASS

📦 Bundle Size Report

PackageSizeGzipped
app-shell (index.js)9.56KB3.59KB
app-shell (runtime-config.js)7.42KB2.32KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)8.92KB3.41KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)1.17KB0.53KB
auth (AuthProvider.js)25.13KB5.40KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.13KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.64KB2.21KB
auth (SocialSignInButtons.js)9.60KB3.89KB
auth (UserMenu.js)3.40KB1.22KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.79KB
auth (createAuthenticatedFetch.js)6.34KB2.43KB
auth (index.js)2.71KB1.22KB
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.02KB0.88KB
auth (useIsWorkspaceAdmin.js)1.61KB0.85KB
collaboration (CommentThread.js)26.07KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.65KB0.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)498.79KB111.24KB
core (index.js)4.11KB1.62KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)157.40KB43.42KB
fields (index.js)233.27KB58.22KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.35KB1.38KB
i18n (pickLocalized.js)3.69KB1.73KB
i18n (provider.js)23.12KB7.62KB
i18n (useDisplayLocale.js)2.84KB1.45KB
i18n (useObjectLabel.js)27.59KB6.63KB
i18n (useSafeTranslation.js)7.77KB3.13KB
layout (index.js)39.16KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.74KB
mobile (index.js)1.50KB0.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.71KB0.42KB
mobile (useResponsiveConfig.js)1.36KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)9.35KB3.31KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)4.42KB1.42KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.91KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.52KB
permissions (usePermissions.js)1.81KB0.83KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.62KB12.83KB
plugin-charts (index.js)64.75KB18.37KB
plugin-chatbot (index.js)181.21KB43.14KB
plugin-dashboard (index.js)127.85KB32.73KB
plugin-designer (index.js)212.39KB42.83KB
plugin-detail (index.js)240.03KB60.04KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)120.42KB29.03KB
plugin-gantt (index.js)164.10KB39.87KB
plugin-grid (index.js)197.61KB53.03KB
plugin-kanban (index.js)52.72KB14.54KB
plugin-list (index.js)111.22KB26.98KB
plugin-map (index.js)17.91KB5.72KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)41.97KB11.33KB
plugin-timeline (index.js)26.68KB7.66KB
plugin-tree (index.js)8.50KB2.88KB
plugin-view (index.js)83.81KB20.49KB
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.44KB0.22KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)3.77KB1.33KB
react (SchemaRenderer.js)27.53KB9.41KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)1.28KB0.68KB
react (schema-input.js)1.45KB0.83KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)4.09KB1.74KB
sdui-parser (index.js)4.55KB2.07KB
sdui-parser (parse.js)10.76KB3.17KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.29KB0.24KB
sdui-parser (validate.js)4.69KB1.48KB
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)0.20KB0.18KB
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-retry.js)4.32KB2.02KB
types (index.js)3.05KB1.52KB
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 (system-fields.js)3.33KB1.54KB
types (theme.js)0.20KB0.18KB
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

一个事实一个读者:`error` 是本页的加载/提交失败通道,而「目标越契约」不属于
那一类 —— 提交是成功的。原先渲染分支从 `error` 取文案、由 redirect 状态取
标志,是把一个事实拆到两处。改为 refusal 与标志同行。
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Main entry (gzip)25.3 KB350 KB
Entry fileindex-D1VE95QL.js
StatusPASS

📦 Bundle Size Report

PackageSizeGzipped
app-shell (index.js)9.56KB3.59KB
app-shell (runtime-config.js)7.42KB2.32KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)8.92KB3.41KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)1.17KB0.53KB
auth (AuthProvider.js)25.13KB5.40KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.13KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.64KB2.21KB
auth (SocialSignInButtons.js)9.60KB3.89KB
auth (UserMenu.js)3.40KB1.22KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.79KB
auth (createAuthenticatedFetch.js)6.34KB2.43KB
auth (index.js)2.71KB1.22KB
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.02KB0.88KB
auth (useIsWorkspaceAdmin.js)1.61KB0.85KB
collaboration (CommentThread.js)26.07KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.65KB0.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)498.83KB111.26KB
core (index.js)4.11KB1.62KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)159.03KB44.08KB
fields (index.js)233.27KB58.22KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.35KB1.38KB
i18n (pickLocalized.js)3.69KB1.73KB
i18n (provider.js)23.12KB7.62KB
i18n (useDisplayLocale.js)2.84KB1.45KB
i18n (useObjectLabel.js)27.59KB6.63KB
i18n (useSafeTranslation.js)7.77KB3.13KB
layout (index.js)39.16KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.74KB
mobile (index.js)1.50KB0.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.71KB0.42KB
mobile (useResponsiveConfig.js)1.36KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)9.35KB3.31KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)4.42KB1.42KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.91KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.52KB
permissions (usePermissions.js)1.81KB0.83KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.62KB12.83KB
plugin-charts (index.js)64.75KB18.37KB
plugin-chatbot (index.js)181.21KB43.14KB
plugin-dashboard (index.js)127.85KB32.73KB
plugin-designer (index.js)212.39KB42.83KB
plugin-detail (index.js)240.05KB60.05KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)120.42KB29.03KB
plugin-gantt (index.js)164.10KB39.87KB
plugin-grid (index.js)197.61KB53.03KB
plugin-kanban (index.js)52.72KB14.54KB
plugin-list (index.js)111.22KB26.98KB
plugin-map (index.js)17.91KB5.72KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)41.97KB11.33KB
plugin-timeline (index.js)26.68KB7.66KB
plugin-tree (index.js)8.50KB2.88KB
plugin-view (index.js)83.81KB20.49KB
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.44KB0.22KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)3.77KB1.33KB
react (SchemaRenderer.js)27.53KB9.41KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)1.28KB0.68KB
react (schema-input.js)1.45KB0.83KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (index.js)4.77KB2.16KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)10.76KB3.17KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.29KB0.24KB
sdui-parser (validate.js)6.92KB2.40KB
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)0.20KB0.18KB
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-retry.js)4.32KB2.02KB
types (index.js)3.05KB1.52KB
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 (system-fields.js)3.33KB1.54KB
types (theme.js)0.20KB0.18KB
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

两条都是「衡量该设计代价」时会先问的问题,直接写在模块文档里:
- `@object-ui/app-shell` 的 `views/metadata-admin/clientValidation.ts` 早已在
客户端用同一批 spec schema 校验元数据草稿并展示其 issue 文案 —— 「在消费侧
问 spec」是既有模式,不是本 PR 新开的路。
- 打包成本实测为零:`@objectstack/spec` 本来就在 console 的
`vendor-objectstack` 块里(vite.config.ts 里就是一个 manualChunks 组),
因为 app-shell 这个核心依赖在运行时多处 import `@objectstack/spec/ui`。
实测 console 主入口 25.3 KB gzip,预算 350 KB。这也是为什么这里用静态
import 而非 clientValidation 那种 lazy:没有块可延迟,改成 lazy 只会把这个
函数在提交路径上变成 async。
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Main entry (gzip)25.3 KB350 KB
Entry fileindex-D1VE95QL.js
StatusPASS

📦 Bundle Size Report

PackageSizeGzipped
app-shell (index.js)9.56KB3.59KB
app-shell (runtime-config.js)7.42KB2.32KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)8.92KB3.41KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)1.17KB0.53KB
auth (AuthProvider.js)25.13KB5.40KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.13KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.64KB2.21KB
auth (SocialSignInButtons.js)9.60KB3.89KB
auth (UserMenu.js)3.40KB1.22KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.79KB
auth (createAuthenticatedFetch.js)6.34KB2.43KB
auth (index.js)2.71KB1.22KB
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.02KB0.88KB
auth (useIsWorkspaceAdmin.js)1.61KB0.85KB
collaboration (CommentThread.js)26.07KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.65KB0.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)498.83KB111.26KB
core (index.js)4.11KB1.62KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)159.03KB44.08KB
fields (index.js)233.27KB58.22KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.35KB1.38KB
i18n (pickLocalized.js)3.69KB1.73KB
i18n (provider.js)23.12KB7.62KB
i18n (useDisplayLocale.js)2.84KB1.45KB
i18n (useObjectLabel.js)27.59KB6.63KB
i18n (useSafeTranslation.js)7.77KB3.13KB
layout (index.js)39.16KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.74KB
mobile (index.js)1.50KB0.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.71KB0.42KB
mobile (useResponsiveConfig.js)1.36KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)9.35KB3.31KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)4.42KB1.42KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.91KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.52KB
permissions (usePermissions.js)1.81KB0.83KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.62KB12.83KB
plugin-charts (index.js)64.75KB18.37KB
plugin-chatbot (index.js)181.21KB43.14KB
plugin-dashboard (index.js)127.85KB32.73KB
plugin-designer (index.js)212.39KB42.83KB
plugin-detail (index.js)240.05KB60.05KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)120.42KB29.03KB
plugin-gantt (index.js)164.10KB39.87KB
plugin-grid (index.js)197.61KB53.03KB
plugin-kanban (index.js)52.72KB14.54KB
plugin-list (index.js)111.22KB26.98KB
plugin-map (index.js)17.91KB5.72KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)41.97KB11.33KB
plugin-timeline (index.js)26.68KB7.66KB
plugin-tree (index.js)8.50KB2.88KB
plugin-view (index.js)83.81KB20.49KB
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.44KB0.22KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)3.77KB1.33KB
react (SchemaRenderer.js)27.53KB9.41KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)1.28KB0.68KB
react (schema-input.js)1.45KB0.83KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (index.js)4.77KB2.16KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)10.76KB3.17KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.29KB0.24KB
sdui-parser (validate.js)6.92KB2.40KB
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)0.20KB0.18KB
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-retry.js)4.32KB2.02KB
types (index.js)3.05KB1.52KB
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 (system-fields.js)3.33KB1.54KB
types (theme.js)0.20KB0.18KB
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

@yinlianghuiClaude

Copy link
Copy Markdown
CollaboratorAuthor

PM 验收 ✅ ACCEPT(#4190,批次 20)

实物核验(merge-base 7d1017790 与报告一致):5 files,+922/−3,面全在 apps/console + changeset。模型标识 msg/diff 双 0;releases/ 零触碰。

CI 亲读:20/20 check runs completed,零失败(两项 path-filter skipped 计绿)。

契约消费的三件事都到位:①redirect 臂改走 router navigate()(两个挂载点都在 BrowserRouter basename 内 —— 整页导航无视挂载正是本卡立案缺陷);②契约外目的地响亮拒绝(错误面板 + toast,写入已成功所以提交确认保留)且裁决不在本 app 重述 —— resolveSubmitRedirect 用 spec 自己的 FormViewSchema 解析最小视图,后续放宽跟 pin 不跟编辑,派生成本已实测(spec 已在 vendor chunk,app-shell 先例在位);③{{record.field}} 插值 + encodeURIComponent + 未解析 token 的 fail-closed 兜底 —— 裁定把转义归到「构建 redirect 的时刻」,消费侧实现正确。

「verbatim」读法的否决窗(dev 自己在 PR 末节点名,验收记录之):spec 的 describe 写「a verbatim redirect on the resolved relative path」,dev 读作约束(不改写、不白名单、不包壳)而非规定导航 API —— 依据是同一裁定通篇称「the in-app path」,且整页导航消费 in-app 路径正是 #4190 的缺陷本身。合理。维护者若按导航 API 读,只有机制半边反转,拒绝与插值半边不动。

反向验证读数:变异 1 的诚实细节有价值 —— 单测文件在「杀调用点」变异下保持 34/34 绿(模块正确只是没人用),这正是机制必须钉在渲染文件的实测理由;变异 4 是 dev 自加且证伪了自己已写下的理由,两处 docblock 按实测改写(schema oracle 与 toContain 断言覆盖「转义充分」的两个不相交半边)。写控制字符测试用例时物化出真实控制字节(#4890 形态)被越门自扫抓住并按字节修复,记为纪律加分。

findings 已定级:#4989(plugin-form 同键按裁定前形状消费,静默丢弃 + 挂载盲)、#4990(授权门比 schema 宽)均入队;#4535 的范围复核提示(item 2 或已部分落地)记入 PM 案头,派发前先核。undraft + auto-merge(squash)。


Generated by Claude Code

@yinlianghui
yinlianghui marked this pull request as ready for review August 17, 2026 13:25
@yinlianghui
yinlianghui added this pull request to the merge queueAug 17, 2026
Merged via the queue into main with commit a34c0b2Aug 17, 2026
21 checks passed
@yinlianghui
yinlianghui deleted the claude/issue-4190-submit-redirect-contract branch August 17, 2026 13:25
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

console: FormPage's post-submit redirect uses the authored URL verbatim — undecided whether it is a console route or an arbitrary destination

2 participants

@yinlianghui@claude