Skip to content

fix(core,fields,plugin-dashboard): 清掉最后四处裸控制字节,并清空扫描门禁的基线 (objectstack#5450) - #3389

Merged
yinlianghui merged 1 commit into
mainfrom
claude/issue-5450-control-bytes-cleanup
Aug 5, 2026
Merged

fix(core,fields,plugin-dashboard): 清掉最后四处裸控制字节,并清空扫描门禁的基线 (objectstack#5450)#3389
yinlianghui merged 1 commit into
mainfrom
claude/issue-5450-control-bytes-cleanup

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Fixes objectstack-ai/objectstack#5450

这是什么

四个源文件在字符串字面量里带着裸控制字节,正是 objectui#3388 落地门禁时记进 KNOWN_OFFENDERS 的四条基线债:

文件 字节 用途
packages/core/src/evaluator/listConditional.ts U+0000 一次性求值失败告警的去重复合 key
packages/core/src/utils/record-title.ts U+0000 EMPTY_TOKEN 空占位哨兵
packages/fields/src/widgets/PeoplePicker.tsx U+0000 记录 id 签名(键盘光标据此重置)的分隔符
packages/plugin-dashboard/src/DatasetWidget.tsx U+0001 透视表行维度拼成 row id 的分隔符

前三个是内容检索黑洞:grep 和 ripgrep 把整个文件判为 binary,一行都不打印。改动前在本仓实测:

grep -rn "EMPTY_TOKEN" packages/core/src/utils/record-title.ts
grep: packages/core/src/utils/record-title.ts: binary file matches

第四个不是。U+0001 不触发 binary 判定(门禁注释里已量化过:GNU grep 3.11 / ripgrep 14 上只有 U+0000 会),DatasetWidget.tsx 一直是能被搜到的。它的危害是另一种:字面量里一个谁都看不见的字节 —— 编辑器不显示、diff 不渲染、和写错的邻居无法区分,没有任何 reviewer 能确认这个分隔符到底是什么。两种危害不同,下面的断言也就分了两种形状。

改法(逐处判定,不批量套同一招)

1. listConditional.ts:根本不再需要分隔符。
去重 key 改成 JSON.stringify([label ?? '', source]) —— label 与 source 的边界对任意输入都无歧义,不再建立在"这个字符不可能出现"的假设上。与 objectui#3388 对 include key 的处置同形。

2. 其余三处保持运行时值逐字节不变,只把字节改写成可读的六字符转义(反斜杠 + 小写 u + 四位十六进制)。三处都有各自"不能换成可打印字符"的具体理由,不是图省事:

  • EMPTY_TOKEN原样插进两个 RegExp,并且必须是记录值不可能携带的字符;换成可打印分隔符会把真实标题里的字符吃掉。
  • PeoplePicker 里同一个分隔符,在下方 cursorEpoch本来就是转义写法;这次只是让相隔十几行的两处终于一致。
  • 透视表 row id 拼的是任意维度值,而且它是 cell 查找 key 的组成部分。

**3. 四条 KNOWN_OFFENDERS 条目在同一个 commit 里删除。**基线是棘轮不是跳过名单:scan() 对陈旧条目和新违规一样红,所以修复与基线不可能漂移。这个 map 现在是空的,注释里写明了它应当保持空。

验证

  • 门禁:node scripts/check-control-bytes.mjs 全绿,且输出里不再带 "N baselined" 后缀(map 已空)。
  • 测试:仓根跑 packages/core packages/fields packages/plugin-dashboard scripts,147 files / 2548 tests 全绿;三个受影响包 type-check 全绿、lint 0 error。

反向验证(先定方向,再跑)

两次,方向都在跑之前写死:

**其一 —— 留下陈旧基线条目应当变红,且必须是 stale 而不是 offender。**四条逐个复现,全部命中预期:

EMPTY BASELINE   -> offenders=0 stale=[] scanned=3577
STALE ENTRY KEPT -> packages/core/src/evaluator/listConditional.ts : offenders=0 stale=["packages/core/src/evaluator/listConditional.ts"]
(其余三条同形)

其二 —— 把 DatasetWidget.tsx 的转义换回裸字节。预期:门禁测试变红,而透视行为测试保持绿。实测正是如此:

× scripts/__tests__/check-control-bytes.test.ts > ... > DatasetWidget.tsx carries no control byte at all
× scripts/__tests__/check-control-bytes.test.ts > repo state ... > has no un-baselined control byte anywhere in the tree
✓ packages/plugin-dashboard/src/__tests__/DatasetWidget.test.tsx > ... > buildPivot keeps two row dimensions apart ...

这一条如实记下,不套模板:转义与裸字节是同一个运行时字符,所以这三处等价改写在行为层面不可能做出 before-green/after-red。能动的只有字节层面的断言;行为测试钉的是"分隔符在语义上仍然把维度隔开",它要变红得是有人改掉分隔符的(比如换成空串或会碰撞的可打印字符),而那正是它该防的事。

新增测试

  • scripts/__tests__/check-control-bytes.test.ts:四个文件"无任何控制字节";其中三个原本瞎掉的另加一条真实 grep 断言(要能打印出行,且不得出现 binary file matches)。U+0001 那个故意不写 grep 断言 —— 它本来就没瞎过,写了就是编造证据。另加一条:四个文件在基线里都不得再有条目。
  • listConditional:告警去重的两半 —— 同一 (label, source) 只告警一次;两对"拼接起来一模一样"的 (label, source) 必须仍是两条告警(空格分隔符会把它们合成一个 key,这正是"换个可打印字符"这种修法会踩的坑)。
  • record-title:哨兵不泄漏进渲染结果(按码位断言,测试文件里不写任何控制字符),孤立分隔符照常清除,两端都有值的分隔符不被误删。
  • DatasetWidget:原有 buildPivot 用例全部只有一个行维度,那个分隔符从来没被任何用例跑到过(把它改成空串也照样全绿)。补一条两个行维度、且拼接会碰撞的用例。

不带 changeset

三处运行时逐字节不变,第四处是内存里的告警去重 key,没有任何用户可见的行为变化 —— 按 .changeset/README.md 的 DON'T(internal refactoring with no API changes)免。

顺带发现(不在本 PR 修)

buildPivot 的 cell key 是用普通空格拼 rowId 与 colId 的,维度值含空格时会静默串格 —— 已单独开 objectstack#5473,按越界即停留在那边。

写作纪律

正文、代码注释、commit message 一律以 U+XXXX 或"转义"的措辞描述字节,不贴字节本体;测试与改写脚本里的控制字符一律由数字构造(String.fromCharCode),避免转义序列在工具载荷里被解码成真字节 —— 这个家族已经有两次事故就发生在"写这条规则的时候"。


Generated by Claude Code

…tes and empty the baseline (objectstack#5450)

Four source files still carried a raw control character in a string literal, all
four listed in `KNOWN_OFFENDERS` when the gate landed in objectui#3388:

  - `packages/core/src/evaluator/listConditional.ts` — U+0000, the composite
    dedup key of the one-time evaluation-failure warning.
  - `packages/core/src/utils/record-title.ts` — U+0000, the `EMPTY_TOKEN`
    sentinel marking an empty titleFormat placeholder.
  - `packages/fields/src/widgets/PeoplePicker.tsx` — U+0000, the separator of
    the record-id signature the keyboard cursor is keyed on.
  - `packages/plugin-dashboard/src/DatasetWidget.tsx` — U+0001, the separator
    joining several row dimensions into a pivot row id.

The first three were invisible to content search: grep and ripgrep classified
the whole file as binary and printed no line at all, which is why grepping this
repo for `EMPTY_TOKEN` or `recordsSignature` returned nothing. The fourth is a
different harm — U+0001 never triggers binary classification — but written raw
it is invisible in every editor and every diff, so no reviewer could tell what
the separator actually was.

Two shapes of fix, chosen per site rather than in bulk:

  - `listConditional.ts` no longer needs a separator at all. The key is now
    `JSON.stringify([label ?? '', source])`, so the label/source boundary is
    unambiguous for any input instead of resting on "this character cannot
    occur" — the same move objectui#3388 made for the include key.
  - The other three keep a byte-identical runtime value and are re-spelled as
    escapes. Each has a reason it must not become a printable character:
    `EMPTY_TOKEN` is interpolated verbatim into two RegExps and would strip real
    text out of titles; the PeoplePicker signature already spelled the very same
    separator as an escape twelve lines below; the pivot row id joins arbitrary
    dimension values and is a component of the cell-lookup key.

All four `KNOWN_OFFENDERS` entries are deleted in this commit, as the map's
contract requires — `scan()` reports a stale entry as loudly as a new offender,
so the fix and the baseline cannot drift apart. The map is now empty.

No changeset: nothing here is user-visible. Three changes are byte-identical at
runtime and the fourth is an in-memory dedup key for a console warning.

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

vercel Bot commented Aug 5, 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 5, 2026 1:35pm

Request Review

@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

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

📦 Bundle Size Report

Package Size Gzipped
app-shell (index.js) 8.47KB 3.09KB
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) 18.38KB 4.49KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 3.65KB 1.42KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.25KB 0.53KB
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) 477.32KB 104.75KB
core (index.js) 2.25KB 0.80KB
create-plugin (index.js) 9.28KB 2.98KB
data-objectstack (index.js) 136.23KB 34.75KB
fields (index.js) 227.18KB 55.72KB
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) 3.26KB 1.44KB
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.05KB 1.53KB
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) 112.01KB 28.86KB
plugin-designer (index.js) 210.51KB 42.51KB
plugin-detail (index.js) 231.29KB 57.04KB
plugin-editor (index.js) 2.46KB 1.10KB
plugin-form (index.js) 111.54KB 26.97KB
plugin-gantt (index.js) 162.55KB 39.57KB
plugin-grid (index.js) 185.08KB 49.04KB
plugin-kanban (index.js) 47.89KB 13.18KB
plugin-list (index.js) 105.02KB 25.36KB
plugin-map (index.js) 16.81KB 5.24KB
plugin-markdown (index.js) 13.72KB 4.69KB
plugin-report (index.js) 40.55KB 10.59KB
plugin-timeline (index.js) 25.76KB 7.33KB
plugin-tree (index.js) 8.34KB 2.82KB
plugin-view (index.js) 83.67KB 20.43KB
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.46KB 1.21KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 0.20KB 0.18KB
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 5, 2026 13:43
@yinlianghui
yinlianghui added this pull request to the merge queue Aug 5, 2026
Merged via the queue into main with commit 4ed8250 Aug 5, 2026
16 checks passed
@yinlianghui
yinlianghui deleted the claude/issue-5450-control-bytes-cleanup branch August 5, 2026 13:43
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.

objectui: four more source files carry raw control bytes — three are invisible to grep, one is a stray U+0001

2 participants