Skip to content

fix(types): 把 objectstack#4171 三处反向 pin 对准真正的触发条件 (#3177) - #3194

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-3177-inverted-pin-burndown
Aug 2, 2026
Merged

fix(types): 把 objectstack#4171 三处反向 pin 对准真正的触发条件 (#3177)#3194
os-zhuang merged 1 commit into
mainfrom
claude/issue-3177-inverted-pin-burndown

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Fixes#3177

前提被修正了:pin 触发 ≠ burn-down 到期

spec 17.0.0-rc.1 给 NavigationItem / FormField / ConditionalValidation.then·.otherwise 都定了型,三处 IsAny / IsUnknown 反向 pin 按设计触发。按 issue 计划第 1 步逐符号 triage 之后,结论是:触发它们的谓词问错了问题

any 从来不是这三个符号里唯一的阻碍,所以「不再是 any」从来就不是正确的准入条件。三个符号今天都不该绑定,一个都没绑。改的是探针:每条现在问那个真正说了算的条件,并且断言当下的事实 —— 今天绿,而各自的阻碍被解除的那天就编译失败,那时才是 burn-down 真正到期。

维护者决定见 #3177 的决策评论(Q1 B+C / Q2 B / Q3 B / Q4 B)。

逐符号

NavigationItem —— 阻碍有三个,没有一个是 any 造成的

spec 把导航建模成九个 $strict 变体的判别联合 + superRefine 互斥规则;objectui 是一个扁平全可选形状。除此之外,下面这些 spec 在两个 tier 上都没有:

本地语义谁在用实测代价
visible: booleanNavigationRendererevalVis;menuItemToNavigationItem 把 legacy MenuItem.hidden 取反生产出布尔值绑到 input tier:3 个 TS2322
pinneduseNavPins + FavoritesProviderspec 无此键
defaultOpen已发布 app metadata 的兼容承诺,navigation-spec-parity.test.ts 有用例spec 无此键
separator 带 labelmenuItemToNavigationItem 会发一个绑定:TS2353

四条探针,一个阻碍一条。另有一条伞形判定 [NavigationItem] extends [SpecNavigationItem] 记录总体结论。

注意不能用结构化 extends 当探针:它对多余属性是放行的,所以看不见「spec 根本没声明 pinned」。这里用的是逐键、逐 tier 的 KeysOfUnion 探测。

FormField —— 两个层,不是一个概念的两种方言

决定性的、机械化的形式是:两边的必填键不相交。objectui 必填 name(表单数据路径),spec 必填 field(对象字段引用),而且 spec 两个 tier 都没有 name。同名的 field 一边是字符串、一边是解析后的元数据对象 —— form.ts 早就用 ⚠️ 标了这个双关。

仓库自己已经有判词,select-option-spec-parity.test.ts 开头:

Unlike the FormField pair — two genuinely different concepts on two layers — a select option is ONE concept in two dialects …

而且 index.ts单独导出 SpecFormField 的,那正是 #3090 tripwire 要强制的消歧;绑定会让 FormField === SpecFormField,消歧一起塌掉。顺带还会把 framework#4074 的 dependsOn 放宽原地退回去。

ConditionalValidation —— 从 unknown 变成了「弱一档」

分支现在是 BaseValidationRuleShape:

interfaceBaseValidationRuleShape{type: string;name: string;message: string;;[key: string]: unknown;}

unknown 强,但仍然不能派生:type 不是字面量联合,分支无法按判别式收窄;then.condition 读回来是 unknown;索引签名放行任何成员 —— 包括拼错的 type: 'formatt'。spec 自己在 ValidationRuleSchema 上就写了:

it types the KNOWN keys and accepts any others … but it is not strictness … Removing the index signature is the #4075 family of work, not this change.

所以整体派生等于用一个带索引签名的 bag 换掉 9 成员判别联合 —— 正是当初立 pin 要防的那类倒退,只是弱了一档。探针改为钉「字面量判别式 / 无索引签名」,objectstack#4075 落地那天自动到期。

为什么这一处的代价比看上去大:渲染器读裸对象、从不 parse,所以 spec 真正拒绝畸形规则的那道 parse 期联合根本不在客户端路径上。作者端(含 AI 生成)的判别式检查是这些规则运行前唯一的关。

真正能派生的,已经派生了

按决策 Q1 的 C 子集,扩展现有的 badgeVariant 先例:

  • NavigationItemType ← spec 导航联合的判别式。原来是手写的九成员副本,正是 objectstack#4115 那一类(ChartType 19 个里只有 7 个、ActionType 漏了 form)。派生还让未来 spec 新增变体在穷举消费者处编译失败(plugin-designerNAV_TYPE_METARecord< NavigationItemType, … >),而不是掉进死的 default:
  • recordMode / filters / badge / target / params / actionDef ← 各自所属的 spec 分支。actionDef 是结构化载荷,恰恰是最容易悄悄走样的那种 —— 它当初被 objectui validate 整个吃掉就是这么来的。

成员今天一个没变,所以没有消费者受影响。

visible / pinned / defaultOpen 保持本地,声明处都写清了「为什么不派生」以及对应探针在哪。

顺带

scripts/check-spec-symbol-derivation.mjs 的注释还写着这三个符号「the SPEC export resolves to any」—— 这已经是错的(JoinNode 随 framework#4286 退役,另两个 rc.1 已定型)。这个 guard 自己就警告过「a wrong canonical-claim is not stale documentation — it is a planted premise for the next session」,所以一并修正,并新增 case 2c:spec 侧已定型但不够精确,anyunknown 两个探针都报干净。

验证

  • pnpm type-check(全仓 turbo,即 spec-symbol-parity.test.ts 的「编译期」断言从未被编译 —— 整个 tripwire 是哑的 #3181 的 typetests lane —— packages/types/tsconfig.test.json 由该包 type-check 串联):78 successful, 78 total
  • 四个 parity/union 测试文件:Test Files 4 passed (4) / Tests 69 passed (69)
  • vitest run packages/types packages/layout:26 files / 399 tests passed
  • vitest run packages/plugin-designer packages/plugin-form:33 files / 332 tests passed
  • vitest run packages/core/src/validation:4 files / 62 tests passed
  • pnpm --filter @object-ui/types lint:0 errors
  • node scripts/check-spec-symbol-derivation.mjs:✅;node scripts/check-changeset-fixed.mjs:✅

Revert-proof(改动探针的期望值必须让 typetests lane 红):

_specNavStillHasNoPinned false -> true
→ error TS1360: Type 'true' does not satisfy the expected type 'false'.
_specThenStillCarriesAnIndexSignature true -> false
→ error TS1360: Type 'false' does not satisfy the expected type 'true'.
NavigationItemType 退回手写联合(去掉 'action')
→ error TS1360: Type '"object" | … | "action"' does not satisfy the expected type 'NavigationItemType'.
Type '"action"' is not assignable to type 'NavigationItemType'.

changeset:patch(无用户可见形状变化)。

🤖 Generated with Claude Code


Generated by Claude Code

… trigger (#3177)
Spec 17.0.0-rc.1 typed NavigationItem, FormField and
ConditionalValidation.then/.otherwise, so the IsAny / IsUnknown pins fired.
Firing was supposed to mean the burn-down was due. Per-symbol triage found it
was not: `any` was never the only blocker for any of the three, so "no longer
any" was never the right admission question.
No symbol is bound. Each probe now asks the condition that actually governs it,
asserts today's state (green now), and stops compiling the day that blocker
lifts:
- NavigationItem: flat shape vs the spec's nine-variant union, plus three
semantics absent at BOTH spec tiers (visible: boolean, pinned, defaultOpen)
and a labelled separator. Four probes, one per blocker.
- FormField: two layers, not two dialects — disjoint required keys (local
`name` vs spec `field`), and `field` itself is a string on one side and the
resolved metadata object on the other.
- ConditionalValidation: the branches became BaseValidationRuleShape,
`{ type: string; …; [key: string]: unknown }` — no literal discriminant and
an index signature that absorbs anything. The spec says so itself and names
objectstack#4075 as the remaining work; the probe now pins that condition.
What did become derivable is derived: NavigationItemType comes off the spec's
nav-item discriminant instead of a hand-written nine-member copy, and
recordMode / filters / badge / target / params / actionDef each come off the
spec branch that owns them, extending the badgeVariant precedent. No member
changes today.
Also corrects the stale premise in check-spec-symbol-derivation.mjs, which
still named these three as "the spec export resolves to any" — the guard's own
warning about a wrong canonical-claim being a planted premise for the next
session.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012C2cd7tL8QDoZ2QKN3djJ5
@vercel

vercelBot commented Aug 2, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
objectuiIgnoredIgnoredAug 2, 2026 10:25am

Request Review

@os-zhuang
os-zhuang marked this pull request as ready for review August 2, 2026 10:26
@os-zhuang
os-zhuang added this pull request to the merge queueAug 2, 2026
Merged via the queue into main with commit f572849Aug 2, 2026
16 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-3177-inverted-pin-burndown branch August 2, 2026 10:26
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Main entry (gzip)28.1 KB350 KB
Entry fileindex-BqdNCMwS.js
StatusPASS

📦 Bundle Size Report

PackageSizeGzipped
app-shell (index.js)8.47KB3.09KB
app-shell (runtime-config.js)7.42KB2.32KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)7.57KB2.97KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)1.17KB0.53KB
auth (AuthProvider.js)22.10KB4.37KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.12KB3.41KB
auth (LoginForm.js)17.86KB5.29KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.43KB2.09KB
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)35.76KB9.11KB
auth (createAuthenticatedFetch.js)4.37KB1.69KB
auth (index.js)2.35KB1.07KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)4.91KB0.87KB
auth (useIsWorkspaceAdmin.js)1.61KB0.85KB
collaboration (CommentThread.js)18.38KB4.49KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)3.65KB1.42KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.25KB0.53KB
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)476.11KB104.47KB
core (index.js)2.25KB0.80KB
create-plugin (index.js)9.28KB2.98KB
data-objectstack (index.js)136.20KB34.74KB
fields (index.js)223.45KB54.66KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (i18n.js)4.32KB1.77KB
i18n (index.js)2.46KB0.96KB
i18n (pickLocalized.js)1.70KB0.83KB
i18n (provider.js)5.37KB1.72KB
i18n (useObjectLabel.js)26.14KB6.07KB
i18n (useSafeTranslation.js)3.26KB1.44KB
layout (index.js)38.47KB10.67KB
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.05KB1.53KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)8.75KB3.06KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)3.67KB1.12KB
permissions (evaluator.js)4.41KB1.44KB
permissions (index.js)0.91KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.52KB
permissions (usePermissions.js)1.55KB0.71KB
plugin-ai (index.js)15.71KB3.79KB
plugin-calendar (index.js)44.90KB12.35KB
plugin-charts (index.js)60.53KB17.12KB
plugin-chatbot (index.js)180.09KB42.72KB
plugin-dashboard (index.js)111.87KB28.82KB
plugin-designer (index.js)210.51KB42.50KB
plugin-detail (index.js)224.52KB54.98KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)111.38KB26.93KB
plugin-gantt (index.js)162.26KB39.53KB
plugin-grid (index.js)185.04KB49.03KB
plugin-kanban (index.js)47.82KB13.18KB
plugin-list (index.js)104.87KB25.31KB
plugin-map (index.js)16.80KB5.24KB
plugin-markdown (index.js)13.65KB4.67KB
plugin-report (index.js)40.48KB10.57KB
plugin-timeline (index.js)25.76KB7.32KB
plugin-tree (index.js)8.34KB2.82KB
plugin-view (index.js)83.54KB20.39KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.71KB3.53KB
providers (index.js)0.44KB0.22KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.67KB2.37KB
react (LazyPluginLoader.js)3.77KB1.33KB
react (SchemaRenderer.js)19.28KB6.38KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)1.02KB0.55KB
sdui-parser (codegen.js)4.09KB1.74KB
sdui-parser (index.js)4.47KB2.03KB
sdui-parser (parse.js)10.04KB2.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 (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)2.46KB1.21KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)0.20KB0.18KB
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

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

objectstack#4171 的三处 inverted pin 在 spec 17.0.0-rc.1 翻转了 —— NavigationItem / FormField / ConditionalValidation 该做 burn-down 了

2 participants

@os-zhuang@claude