Skip to content

fix(spec): 参考文档生成器按 typeof 决定字面量是否加引号,数值字面量不再被记成字符串 (#5729) - #6127

Merged
os-zhuang merged 2 commits into
mainfrom
claude/issue-5729-numeric-literal-rendering
Aug 7, 2026
Merged

fix(spec): 参考文档生成器按 typeof 决定字面量是否加引号,数值字面量不再被记成字符串 (#5729)#6127
os-zhuang merged 2 commits into
mainfrom
claude/issue-5729-numeric-literal-rendering

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Fixes #5729

问题

packages/spec/scripts/lib/format-type.ts 的两个字面量分支无条件加引号,不看值的类型:

if (prop.enum) {
  return `Enum<${prop.enum.map((e: any) => `'${e}'`).join(' | ')}>`;   // 每个成员都加引号
}

if (prop.const !== undefined) {
  return `'${prop.const}'`;                                            // const 一律加引号
}

于是任何「只收数值」的字面量联合都被记成只收字符串,而参考页正是 AI 作者的权威来源(ADR-0033)。

⚠️ issue 正文把落点写成 build-docs.ts,实际不在那里。引号出自 format-type.ts#6058
build-docs.ts 抽出来的模块),本 PR 未触碰 build-docs.ts —— 并行派发 #5059 正在编辑它。

前提复核(vs origin/main 7efa127,已含 #6069 / #6096 / #6100 修复)

两条都仍然成立,没有被前面几个 PR 顺手改掉:

  • format-type.ts:148 / :152(改前行号)两个分支确实仍无条件加引号 —— 见上方摘录。

  • content/docs/references/ui/view.mdx:184 仍是 issue 报告的那一行:

    | **columns** | `Enum<'1' \| '2' \| '3' \| '4'> \| '1' \| '2' \| '3' \| '4'` | optional |  |
    

    后半段四个就是 FormSectionSchema.columnspackages/spec/src/ui/view.zod.ts:1502-1510)里的四个
    数值 字面量 z.literal(1..4)

补一条实测(对着真转换器探的,不是猜的)——四种字面量的 JSON Schema 编码:

声明 z.toJSONSchema 产出
z.literal(2) { type: 'number', const: 2 }
z.literal('2') { type: 'string', const: '2' }
z.literal(true) { type: 'boolean', const: true }
z.literal(null) { type: 'null', const: null }
z.enum(['a','b']) { type: 'string', enum: ['a','b'] }
z.nativeEnum({ A: 1, B: 2 }) { type: 'number', enum: [1, 2] }

修复

新增 formatLiteral(value),按 typeof 决定:字符串加引号,number / bigint / boolean 裸渲染,
null 渲染为关键字;复合 const(对象/数组,目前没有 schema 产出)退回 JSON.stringify,因为旧代码
在这种节点上会打印 '[object Object]'。两个分支都改为调用它。

为什么逐「值」而不是逐「节点」判断:JSON Schema 逐值声明成员类型,一个 enum 数组可以混装类型 ——
上表最后一行 z.nativeEnum({ A: 1, B: 2 }) 就产出数值 enum。用节点级的 prop.type 去判断,混装那一
类就会渲染错;逐值判断则顺带就对了。

反向验证(方向在运行前先行判定)

预判:普通方向 —— 把无条件加引号改回去,新增的「非字符串」pin 转红;「字符串保持引号」的 pin
两边都绿(它们是 over-reach guard:一个只会「删引号」而不按 typeof 选择的修法,会把它们转红)。

实测5 failed | 30 passed。五个红全部落在断言非字符串字面量的用例上:

AssertionError: expected ''2'' to be '2'
AssertionError: expected ''true'' to be 'true'
AssertionError: expected 'Enum<'1' | '2'>' to be 'Enum<1 | 2>'
AssertionError: expected ''1' | '2'' to be '1 | 2'
AssertionError: expected 'Enum<'1' | '2' | '3' | '4'> |…' to be 'Enum<'1' | '2' | '3' | '4'> |…'   ← columns 那一格

三个「字符串不变」的用例两边都绿,符合预判。

诚实记录一处偏差:我最初在测试文件的 MEASURED 注释里预判「4 红 / 2 绿」,实跑是 5 红。原因不是
判断方向错了,而是测试结构写坏了 —— 我把「字符串保留引号」的断言和数值断言塞进了同一个 it,它永远
在红色兄弟的阴影下运行,绿了也证明不了自己。把三个字符串守卫拆成独立 it 后重新测量,才得到上面的
5 failed | 30 passed。仓库里那段 MEASURED 注释记的是重测后的真实数字,并写明了这次修正。

参考页改动逐页确认

pnpm --filter @objectstack/spec gen:schema && gen:docs 的产物,无手工编辑,单独一个 commit。
共 8 页 / 11 行:

页面 改前 改后 值的类型
api/dispatcher.mdx success 'false' false boolean
api/errors.mdx success 'false' false boolean
data/data-engine.mdx sort Record< string, '1' | '-1' > Record< string, 1 | -1 > number
data/driver-nosql.mdx options.projection Record< string, '0' | '1' > Record< string, 0 | 1 > number
data/driver-nosql.mdx projection Record< string, '0' | '1' > Record< string, 0 | 1 > number
data/object.mdx systemFields 'false' | {...} false | {...} boolean
data/object.mdx stageField string | 'false' string | false boolean
security/explain.mdx version '1' 1 number
ui/dashboard.mdx filterBindings Record< string, string | 'false' > Record< string, string | false > boolean
ui/view.mdx columns Enum<'1' | '2' | '3' | '4'> | '1' | '2' | '3' | '4' Enum<'1' | '2' | '3' | '4'> | 1 | 2 | 3 | 4 number

每一处都对着 zod 源核过,确认声明的就是非字符串字面量,例如
api/dispatcher.zod.ts:184 success: z.literal(false)
data/data-engine.zod.ts:43 z.record(z.string(), z.union([z.literal(1), z.literal(-1)]))
security/explain.zod.ts:332 version: z.literal(1).default(1)

方向机检(脚本逐行比对改动前后每行的「带引号字面量多重集」,而不是肉眼看):

changed lines: 11
quotes removed (all non-string): 18 处 —— boolean × 6, number × 12
✅ direction confirmed: only non-string literals lost quotes;
   nothing newly quoted; no string literal changed

也就是说:引号只从非字符串字面量上移除,没有任何字符串字面量失去引号。页面上所有
Enum<'asc' \| 'desc'>Enum<'1' \| '2' \| '3' \| '4'> 这类字符串枚举一律原样保留 ——
columns 那一格前半段的字符串枚举带引号、后半段的数值裸写,正是这个键真实的契约。

验证

pnpm --filter @objectstack/spec build                  # 完整含 DTS
pnpm --filter @objectstack/spec test                   # 326 files / 8363 tests passed
pnpm --filter @objectstack/spec typecheck              # tsc --noEmit + check:test-typecheck OK
pnpm --filter @objectstack/spec check:docs             # 232 generated files in sync
pnpm --filter @objectstack/spec check:generated        # ✓ All 10 generated artifacts are up to date
pnpm lint                                              # 干净
pnpm check:nul-bytes / check:doc-authoring / check:slot-lookup
     / check:empty-changeset / check:release-notes / check:published-files
     / check:docs-audit-scope / check:role-word / check:adr-anchors   # 全绿

format-type.test.ts 新增 8 个用例(27 → 35),其中 5 个钉非字符串字面量、3 个守住字符串不变。

一处过程记录:先用 OS_SKIP_DTS=1 建包时 check:generatedapi-surface/ stale —— 那是
gen:api-surfacedist/**/*.d.ts 而我跳过了 DTS 所致,不是本改动引起(api-surface/ 在 git 里
未被改动)。改跑完整 build 后复测即 ✓ All 10 generated artifacts are up to date

changeset

有 —— .changeset/docs-gen-numeric-literal-quoting.md@objectstack/spec: patch

理由:scripts/ 不在 packages/specfiles 白名单里(不随包发布),但本改动改变了对读者可见的
产物
:8 个参考页的渲染结果变了,而这些页面就是作者(尤其 AI 作者)读的契约。这与 #6058(同样是
format-type.ts 改动 + 参考页重新生成)的判例形状完全一致,沿用它的 named patch 写法。

#5340 的关系

#5340(同文件的 inline enum 宽度问题,finding 挂起)没有被本 PR 实现,也不是搭车项。

问:本改动让 #5340 更容易、更难、没必要、还是无影响?

答:基本无影响,边际上略微更容易#5340 谈的是内联 enum 太宽需要收窄/省略,属于 formatType
布局/长度决策;本改动动的是单个字面量的拼写决策,两者在代码上不重叠(#5340 要改的是 enum 成员
的省略策略,本 PR 只改了成员如何转成字符串)。边际上略微更容易的地方是:成员渲染现在集中在
formatLiteral() 一个函数里,#5340 若要做「超过 N 个成员就省略」之类的处理,拿到的是一个已经归一化的
成员字符串列表,不必再关心引号。宽度上也有极小的减少(数值成员各少 2 个字符),但那不足以改变 #5340
的结论 —— 它要解决的问题依然存在。

并行冲突提示

并行派发 #5059 也会重新生成 content/docs/references/**。两边代码文件不相交#5059
build-docs.ts,本 PR 改 format-type.ts),只有生成产物可能撞车;谁后落地谁 merge main 后重新
生成一次即可。


Generated by Claude Code

claude added 2 commits August 7, 2026 02:42
`formatType()` 的两个字面量分支无条件给每个值套上 `'…'`,数值字面量因此
被写成字符串字面量。`FormSection.columns` 声明为
`z.union([z.enum(['1','2','3','4']), z.literal(1) … z.literal(4)])`,
`ui/view.mdx` 却渲染成 `Enum<'1' | '2' | '3' | '4'> | '1' | '2' | '3' | '4'`
—— 后半段那四个数值字面量与前半段的字符串完全无法区分,页面因此宣称这个键
只收字符串,而 schema 同时接受 `2` 和 `'2'`。

参考页是 AI 作者的权威输入(ADR-0033),字面量联合又是「照抄拼写」的表面:
页面上的引号会被原样抄进 metadata。#5611 已经付过代价——它的
`RecordDetailsProps.sections[].columns` 本打算写成数值字面量联合,照生成的
参考写是硬解析错误,PR 里被迫退回 `z.number().int().min(1).max(4)`。生成器
在反向定义契约。

引号改为按**值**判断而非按节点判断:JSON Schema 逐值声明成员类型,`enum`
可以混装(`z.nativeEnum({A: 1})` 就产出数值 `enum`)。字符串保留引号,
`number`/`boolean` 裸渲染,`null` 渲染为关键字。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014wsZeReNTqiceBfLb5Pyf5
`pnpm --filter @objectstack/spec gen:schema && gen:docs` 的产物,无手工编辑。

8 个页面 / 11 行,方向单一:18 处非字符串字面量去掉引号,没有任何字符串
字面量失去引号(逐行机检,详见 PR 正文)。

- api/dispatcher, api/errors —— `success: 'false'` → `false`
- data/data-engine —— `sort: Record<string, '1' | '-1'>` → `Record<string, 1 | -1>`
- data/driver-nosql —— `projection: Record<string, '0' | '1'>` → `Record<string, 0 | 1>`
- data/object —— `systemFields` / `stageField` 的 `'false'` → `false`
- security/explain —— `version: '1'` → `1`
- ui/dashboard —— `filterBindings` 的 `'false'` → `false`
- ui/view —— `columns: … | '1' | '2' | '3' | '4'` → `… | 1 | 2 | 3 | 4`

页面上所有 `Enum<'a' | 'b'>` 形态的字符串枚举一律未变。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014wsZeReNTqiceBfLb5Pyf5
@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)
objectstack Ignored Ignored Aug 7, 2026 2:48am

Request Review

@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Aug 7, 2026
@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

No hand-written docs reference the 0 changed package(s). ✅

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

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

参考文档生成器把数值字面量 z.literal(2) 渲染成带引号的 '2',数值型联合被记成字符串型

2 participants