Skip to content

test(filter-parity): 给两处 spec 词表减法加排除项存活棘轮 (#3628) - #3640

Merged
yinlianghui merged 1 commit into
mainfrom
claude/issue-3628-parity-liveness-ratchet
Aug 7, 2026
Merged

test(filter-parity): 给两处 spec 词表减法加排除项存活棘轮 (#3628)#3640
yinlianghui merged 1 commit into
mainfrom
claude/issue-3628-parity-liveness-ratchet

Conversation

@yinlianghui

@yinlianghui yinlianghui commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator

Fixes #3628

#3601 / PR #3623 同一手法的预防性棘轮:两处 filter-operator 平价测试各自从 spec 派生的 VIEW_FILTER_OPERATORS 里减去一个手写排除集,却没有任何一条断言被减的 token 仍然是那份词表的成员。各加一条存活断言。

问题形状

  • packages/plugin-list/src/__tests__/filter-operator-ast-parity.test.ts —— HANDLED_BEFORE_MAPPING
  • packages/data-objectstack/src/filter-operator-ast-parity.test.ts —— NOT_THIS_ADAPTERS_JOB

两处减的都是 is_empty / is_not_empty,排除理由都成立(视图层在 mapOperator / 适配器被问到之前就把它们改写成 null 比较)。缺的是排除项自身的存活断言。

上游一旦把这两个 token 退役或改名,减法就变成一次空减法:平价扫描依然全绿(它对剩下的 token 仍然是完整的),但排除行成了死重,注释还会继续对下一位读者宣称「视图层会先改写这一个」—— 而那时已经没有作者能声明这个 operator 了。这正是 #3601 里 82 条拒绝名单烂掉 37 条的同一个形状:一份手写清单挂在 spec 派生的词表旁边,却没有一条断言清单成员仍然存在于那份词表

测量(在本分支 worktree 重测,非照抄 issue 正文)

worktree 装的是 lockfile 解析的 @objectstack/spec@17.0.0-rc.5:

spec version: 17.0.0-rc.5
VIEW_FILTER_OPERATORS count: 19
is_empty       LIVE in spec
is_not_empty   LIVE in spec

与 issue 正文一致:19 项词表,两条今天都还在,排除当前是有效的 —— 这条是预防性的,不是已兑现的腐化,没有任何现存排除项需要删除。

实现取舍

it('every HANDLED_BEFORE_MAPPING token is still in the spec view vocabulary', () => {
  const vocabulary = new Set< string >(VIEW_FILTER_OPERATORS);
  const retired = [...HANDLED_BEFORE_MAPPING].filter((op) => !vocabulary.has(op));
  expect(retired, `VIEW_FILTER_OPERATORS no longer lists these … tokens: ${retired.join(', ')}…`)
    .toEqual([]);
});
  • 收集后一次性断言,而非逐条 toBe(true)(照抄 PR test(types): 删除 37 条随 spec 退役而失效的 DROPPED_SCHEMA_EXPORTS,并加棘轮断言 (#3601) #3623 的设计取舍):词表退役是整族落地的,逐条断言会停在第一条、藏住其余。失败信息点名死项,并提示「spec 已退役,请把它从排除集删除」而不是留一次死减法。
  • new Set< string >(VIEW_FILTER_OPERATORS) 而不是 issue 正文建议的 VIEW_FILTER_OPERATORS.includes(op):VIEW_FILTER_OPERATORS 在 spec 的 d.ts 里是 readonly ["equals", …] 字面量元组,.includes() 只接受该字面量联合,传 Set< string > 迭代出的 string 会是 TS 错误。Set 查表既过了类型也是 O(1)。
  • 两个文件的既有断言一行未改,只加了新 it 与排除集 doc 注释里指向棘轮的一句。

逆向验证(先预测后运行,两个方向都如预测)

在两个排除集里临时塞一个词表从未有过的 token never_existed_op:

预测 实测
修前(未改的两个文件) 绿 —— 排除集只被当作减数用,一个不在词表里的成员什么都匹配不到 ✅ 绿:Test Files 2 passed (2),Tests 42 passed (42)
修后(本 PR) 红,且点名该 token;其余断言仍绿 ✅ 红:Test Files 2 failed (2),Tests 2 failed / 42 passed (44)

修后那一轮的失败输出(两个文件对称,摘 plugin-list 一条):

FAIL |unit| packages/plugin-list/src/__tests__/filter-operator-ast-parity.test.ts >
  mapOperator bridges the spec view vocabulary onto the AST vocabulary >
  every HANDLED_BEFORE_MAPPING token is still in the spec view vocabulary
AssertionError: VIEW_FILTER_OPERATORS no longer lists these HANDLED_BEFORE_MAPPING
tokens: never_existed_op. The spec has retired them, so subtracting them from the
sweep below excuses nothing — delete each from the set …
  expected [ 'never_existed_op' ] to deeply equal []

修前那一轮绿就是恒真缺陷的现场演示:两条排除行这些年一直处在「没人能发现它失效」的状态。验证后假 token 已还原,提交里不含它(见 diff)。

验证

$ npx vitest run packages/plugin-list/…/filter-operator-ast-parity.test.ts \
                 packages/data-objectstack/src/filter-operator-ast-parity.test.ts --maxWorkers=2
 Test Files  2 passed (2)
      Tests  44 passed (44)          # 42 + 2 条新棘轮

$ npx vitest run packages/plugin-list packages/data-objectstack --maxWorkers=2
 Test Files  46 passed (46)
      Tests  706 passed (706)

$ pnpm --workspace-concurrency=2 --filter @object-ui/plugin-list --filter @object-ui/data-objectstack type-check
packages/data-objectstack type-check: Done
packages/plugin-list type-check: Done

$ npx eslint <两个测试文件>        # exit 0,无告警
$ node scripts/check-control-bytes.mjs
✅  check-control-bytes: OK (scanned 3655 tracked text file(s); skipped 85 binary).

首轮 type-check 因新 worktree 未构建依赖而报 Cannot find module '@object-ui/components'(AGENTS.md §9 那个陷阱),先跑 pnpm --workspace-concurrency=2 --filter '@object-ui/plugin-list^...' --filter '@object-ui/data-objectstack^...' build 后即通过 —— 与本改动无关。

无 changeset:测试-only 改动,不触碰任何包的公开面、运行时行为或类型导出 —— 按仓例(changeset 用于 user-visible 变更)无需 changeset,同 PR #3623 先例。

文件面:两个测试文件,+63 −1。


🤖 Generated with Claude Code

https://claude.ai/code/session_01GTRjn8xBqp75dk7kFupVRt

Both filter-operator parity tests subtract a hand-written exclusion set from
the spec-derived VIEW_FILTER_OPERATORS vocabulary, but neither asserted that
the subtracted tokens are still members of it. Once the spec retires or
renames is_empty / is_not_empty the subtraction silently becomes a no-op: the
sweep stays green (still total over what remains) while the excluded rows turn
into dead weight whose comments describe an operator no author can declare.

Same shape as #3601, where 37 of 82 deny-list entries went inert unnoticed.
Adds one collected-then-asserted ratchet per file, naming the dead tokens in
the failure message. Existing assertions unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GTRjn8xBqp75dk7kFupVRt
@vercel

vercel Bot commented Aug 7, 2026

Copy link
Copy Markdown

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

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectui Ignored Ignored Aug 7, 2026 4:49pm

Request Review

@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Main entry (gzip) 28.1 KB 350 KB
Entry file index-CuqUPlqJ.js
Status PASS

📦 Bundle Size Report

Package Size Gzipped
app-shell (index.js) 8.66KB 3.13KB
app-shell (runtime-config.js) 7.42KB 2.32KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 7.57KB 2.97KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 1.17KB 0.53KB
auth (AuthProvider.js) 22.10KB 4.37KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.13KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.64KB 2.21KB
auth (SocialSignInButtons.js) 9.60KB 3.89KB
auth (UserMenu.js) 3.40KB 1.22KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 35.76KB 9.11KB
auth (createAuthenticatedFetch.js) 4.37KB 1.69KB
auth (index.js) 2.35KB 1.07KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 4.91KB 0.87KB
auth (useIsWorkspaceAdmin.js) 1.61KB 0.85KB
collaboration (CommentThread.js) 26.07KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.65KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 480.72KB 105.64KB
core (index.js) 2.96KB 1.13KB
create-plugin (index.js) 9.28KB 2.98KB
data-objectstack (index.js) 137.51KB 35.11KB
fields (index.js) 230.87KB 56.83KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (currency.js) 1.22KB 0.64KB
i18n (i18n.js) 4.32KB 1.77KB
i18n (index.js) 2.65KB 1.06KB
i18n (pickLocalized.js) 1.70KB 0.83KB
i18n (provider.js) 9.48KB 3.27KB
i18n (useObjectLabel.js) 26.14KB 6.07KB
i18n (useSafeTranslation.js) 4.52KB 1.96KB
layout (index.js) 38.53KB 10.71KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.74KB
mobile (index.js) 1.50KB 0.62KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.71KB 0.42KB
mobile (useResponsiveConfig.js) 1.36KB 0.63KB
mobile (useSpecGesture.js) 4.32KB 1.64KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 8.75KB 3.06KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 3.67KB 1.12KB
permissions (evaluator.js) 4.41KB 1.44KB
permissions (index.js) 0.91KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.52KB
permissions (usePermissions.js) 1.55KB 0.71KB
plugin-ai (index.js) 15.71KB 3.79KB
plugin-calendar (index.js) 44.98KB 12.37KB
plugin-charts (index.js) 61.04KB 17.31KB
plugin-chatbot (index.js) 180.09KB 42.72KB
plugin-dashboard (index.js) 115.50KB 29.96KB
plugin-designer (index.js) 210.51KB 42.51KB
plugin-detail (index.js) 232.79KB 57.42KB
plugin-editor (index.js) 2.46KB 1.10KB
plugin-form (index.js) 112.10KB 27.10KB
plugin-gantt (index.js) 162.55KB 39.57KB
plugin-grid (index.js) 186.61KB 49.34KB
plugin-kanban (index.js) 48.30KB 13.28KB
plugin-list (index.js) 105.12KB 25.48KB
plugin-map (index.js) 16.81KB 5.24KB
plugin-markdown (index.js) 13.72KB 4.69KB
plugin-report (index.js) 40.58KB 10.58KB
plugin-timeline (index.js) 25.76KB 7.33KB
plugin-tree (index.js) 8.50KB 2.88KB
plugin-view (index.js) 84.03KB 20.55KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.71KB 3.53KB
providers (index.js) 0.44KB 0.22KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.67KB 2.37KB
react (LazyPluginLoader.js) 3.77KB 1.33KB
react (SchemaRenderer.js) 19.28KB 6.38KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 1.02KB 0.55KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 4.09KB 1.74KB
sdui-parser (index.js) 4.47KB 2.03KB
sdui-parser (parse.js) 10.04KB 2.82KB
sdui-parser (types.js) 0.29KB 0.24KB
sdui-parser (validate.js) 4.69KB 1.48KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 0.99KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 0.20KB 0.18KB
types (crud.js) 0.20KB 0.18KB
types (data-display.js) 0.20KB 0.18KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.87KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-retry.js) 4.32KB 2.02KB
types (index.js) 2.71KB 1.34KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 2.59KB 1.31KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (spec-report.js) 5.05KB 1.93KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 0.20KB 0.18KB
types (ui-action.js) 3.40KB 1.71KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

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

@yinlianghui
yinlianghui marked this pull request as ready for review August 7, 2026 17:03
@yinlianghui
yinlianghui added this pull request to the merge queue Aug 7, 2026
Merged via the queue into main with commit 1e635d6 Aug 7, 2026
18 checks passed
@yinlianghui
yinlianghui deleted the claude/issue-3628-parity-liveness-ratchet branch August 7, 2026 17:53
Sign up for free to 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.

两处 filter-operator 平价测试从 spec 词表里减去 is_empty / is_not_empty,但没有一条断言这两个 token 仍在词表里

2 participants