跳转到内容

特征名与参数名的本地化:文档层「码 / 自由文本」二分设计

状态:Research Note 版本:0.2.0 日期:2026-09-08 研究问题:界面文案已全量码化并可切换语言,但用户看到的模型自己的名字(特征树里的 Constrained L profile、HUD 里的 Extrusion distance)仍是英文。要做到「应用生成的默认名跟随界面语言,人或 AI 起的名字原样保留」,该改哪一层、代价是什么、能不能不迁移已存盘的项目? 起因:中文界面下特征树整列英文(2026-09-08 会话)。当时判定「这是模型数据不是界面文案,显示层查表翻译是错的」,本文是对该判定的源码级取证与落地设计。 前置:界面 i18n 目录化(7cc7ac9→d1d1ee8)、AMIR 现状审计 证据来源:packages/aira-contracts/schema、packages/aira-contracts/fixtures、crates/aira-core/src、apps/web/src、apps/server/src;全部为当前工作树实测。 修订:0.1.0 曾论证「翻译 label 会污染内容身份」。该论据错误,已在 §5 更正:modelHash 本就排除所有 label。结论方向不变,但真正的约束换成了 §6 的 AI 耦合。 处置:本文不授权实施。 §10 有三个待产品决定的问题,未决之前不动 schema。

  1. 规范早已把 label 定为显示字段。 modelHash 的语义投影是显式白名单,注释逐字写明排除 “every label”:PARAMETER = ["type","value","range","mutable"]、BODY = ["authority"]、 NODE_V04 无 label(crates/aira-core/src/canonical.rs:87-111,规范依据 AMIR-v0.1.md:1110,1113)。[实测]
  2. 而且规范已经预演过非英文名字。 一致性夹具 model-hash/display-fields-changed.amir.json 与 base 共享同一个 expectedModelHash,差异正是「每个 label 都不同」,其中包含中文 label "宽度" 和空串 label。 本设计不是在破坏既有约定,是在把规范早已划出的那条线补齐到产品里。 [实测]
  3. 但 revisionId 不等于 modelHash。 revision_id 的原像含 patch_hash,而 patch_hash 是对整个补丁信封 取规范化哈希、不做投影(canonical.rs:435-437,857-878),node.put / parameter.put 的载荷里就带着 label。 推论:已存盘的补丁字节必须原样不动,一旦重写,其后所有 revisionId 全变,读历史时会撞 HISTORICAL_REVISION_ID_MISMATCH。[实测]
  4. 真正的硬约束不在哈希,在 AI 契约。 对 exact.program 节点,网关故意剥掉程序的 names / arguments 输入, 并注明「every name is the label of a listed parameter」(packages/aira-cad-tools/src/tools.ts:628-632); 写入侧 apps/web/src/authoring/exact-cad-plan.ts:549 把模型声明的参数名原样写成 label。 参数 label 同时是显示名和机器标识符。 这是本设计最容易踩塌的地方。[实测]
  5. 好消息:没有任何解析逻辑按 label 找对象。 全仓库对 label 的文本匹配只有三类:结构树搜索、命令面板过滤、 HUD 去重;外加六处 ?.trim() || 空值判断。所有权一律走 id,WorkbenchParameterScope.ts:36-40 明文写着「UI labels and intent text are never trusted as parameter ownership evidence」。[实测]
  6. schema 三个定义带 label,且都封闭。 Parameter、Body、ModuleNode,均为可选裸字符串, 均 additionalProperties: false。加兄弟字段必须改 schema、重生成、更新 lock。ModuleNode 有 extensions 逃生舱而 Parameter 没有,所以「靠 extensions 免改 schema」只能走一半,不成立。[实测]
  7. 应用自己写死的英文字面量共 89 条:参数 40、节点 35、实体 14,分布在 13 个 apps/web/src/authoring/ 模块 加 2 个 workbench driver(另有 9 条在休眠的 mesh 夹具里)。而且 amir-authoring-values.ts 的四个构造器 把 label 设成必填位参,等于强制每个新参数携带一个英文串。[实测]
  8. 今天没有重命名入口。 全仓库零 rename / setLabel 实现,用户改参数走 parameter.setValue,该操作根本没有 label 字段。所以「自由文本」分支当前只有两个真实来源:创世构造器与 AI。[实测]
  9. 显示侧三条命名规则互相打架,同一个参数在不同界面可能显示不同名字。统一它们本就该做, 而统一的方向正好是本设计要的「标准名优先」。[实测]
  10. 可以不迁移旧数据。 新字段可选、旧文档不含它、渲染时「有码查目录,无码用存名」。旧文档一个字节不改, 补丁不动,revisionId 不变。仓库已有两个同形先例:IndexedDB 版本迁移与可选字段加宽读取。
  11. 判定:分两步。第一步只做参数显示名,纯显示层、零 schema 风险、不碰 AI 契约; 第二步做节点名,需要 schema 改动与 §10 的产品决定。
  • schema 事实以 amir-core-v0.4.schema.json 为准,不采信生成后的 TS(生成物里 label 出现四处,是 NodePut 内联展开导致的重复,源里只有三个定义)。
  • 哈希事实以 Rust 侧 canonical.rs 的投影白名单与 fixtures/model-hash/manifest.json 的黄金值为准。
  • 仓库无测试框架(AGENTS.md 明令不新增测试文件),本文所称「夹具」指脚本生成的签入 JSON。
  • [待验证] AmirV04ModuleValidator 会断言 schema hash,但该类在自身文件外无调用方,活的校验路径是 Rust core。 改 schema 时需确认这条死路径是否要一并处理。

四个来源,性质完全不同,却挤在同一个字符串字段里。这是问题的根。

来源 例子 持久化 该不该本地化
应用创世构造器(节点/实体) Exact L-bracket extrusion、Constrained L profile 是 该
应用创世构造器(参数) Extrusion distance、Overall width、Hole radius 是 该
投影层兜底 featureLabel(op)、humanizeInput(inputName) 否 该
AI 命名 programLabel(summary, intent) 写节点与实体名;模型声明的 p.name 原样写成参数名 是 绝不
STEP 导入 固定两串 Imported exact STEP shape / Imported STEP shape,文件名被丢弃 是 存疑,见 §10
用户重命名 今天不存在 — 绝不

本审计时支架名称来自样例生成器与专用 Driver;这些文件已于 2026-09-12 随组织层清理删除。当前名称保存在 apps/web/src/workbench/workbench-samples.json,通过通用 Graph 宿主加载。

位置 规则 结果
WorkbenchProjectionService.ts:353 parameter.label?.trim() || humanizeInput(inputName) 存名优先
WorkbenchProjectionService.ts:126 写死 'Fillet radius',连读都不读 忽略前两者
WorkbenchParameterScope.ts:267 STANDARD_INPUT_LABELS[op/input] || declared || 派生 标准名优先

WorkbenchProjectionService.ts:109 同样写死 'Exact edge fillet' 覆盖节点存名。 另有一处死代码:WorkbenchFeatureGraph.ts:153 算出的 label 无人消费,投影层在 :142 自己又算了一遍。

STANDARD_INPUT_LABELS 八条键为 operation/inputName,已经是一张稳定键到显示名的表, 只是值写死了英文。它就是本设计的现成原型。

0.1.0 版本写的是「翻译 label 会让同一零件因作者语言不同得到不同 revision 哈希」。这是错的, 因为它把 modelHash 和 revisionId 混为一谈。更正后的事实分两层:

  • 几何身份(modelHash)与 label 无关,这是规范的明确设计。 投影白名单不含 label,黄金夹具用一份 「所有 label 都改了、含中文与空串」的文档证明哈希不变。所以在文档里放什么名字,都不影响几何身份。
  • 版本身份(revisionId)含 patchHash,而 patchHash 不做投影。 所以约束不是「不能翻译」, 而是不能重写已存盘的补丁。这恰好与本设计的加性方案一致:旧数据一律不动。

结论方向没变,但理由要换:能进文档的应当是语言无关的码,不是因为哈希会变,而是因为 (a)规范已把 label 定为显示字段,翻译后的文本存进去只是把一个语言的产物冻结成数据; (b)显示层按文本查表翻译已被本次 i18n 重构证伪(agent-task-copy.ts 正是因此删除:用户一改名即失效, 且分不清应用默认名与人写的字)。

6. 最大风险:参数 label 是 AI 的机器标识符

Section titled “6. 最大风险:参数 label 是 AI 的机器标识符”

这一节独立成节,因为它比 schema 改动难对付。

网关给模型的模型上下文里,exact.program 节点的 names / arguments 输入被故意剥掉 (packages/aira-cad-tools/src/tools.ts:636-644),理由写在 :628-632:

Nodes keep their identity, operation, source and ports. A program’s parameter names and argument bindings are omitted: every name is the label of a listed parameter, and the host rebuilds the bindings from its own document when the program is replaced.

也就是说,模型判断程序源码里 p.width 指什么,唯一依据就是那个参数的 label。写入侧对称: apps/web/src/authoring/exact-cad-plan.ts:546,549 把模型声明的 p.name(受 ^[A-Za-z_$][A-Za-z0-9_$]*$ 约束) 原样写成 label。

推论,全部是本设计必须遵守的:

  1. AI 写的参数 label 必须永远是自由文本,且必须原样送回模型。 它不是名字,是标识符。
  2. 应用默认参数名若改成码,送给模型的上下文必须渲染成稳定的英文,不能跟界面语言走, 否则中文界面下模型会看到中文标识符。现成先例:WorkbenchAgentTaskCoordinator.ts:288 已经在用 createTranslator('en'),注释写着「The model reads English whatever the interface shows」。
  3. 模型上下文投影必须继续输出普通字符串(contextLabel 只做空白压缩与 120 字截断), 码的解析必须发生在送出之前。

三个定义各加一个可选兄弟字段,与既有 label 并存:

// ModuleNode / Parameter / Body
{
"label": { "type": "string" }, // 既有:人或 AI 起的名字,原样显示,永不翻译
"labelCode": { "type": "string" } // 新增:应用生成的默认名,语言无关,渲染时查目录
}

不改 label 语义,不设必填,不做 oneOf 联合。联合类型会让每一份旧文档失效,而并存只需要一条渲染优先级。

7.2 渲染优先级(一条规则,替掉 §4 的三条)

Section titled “7.2 渲染优先级(一条规则,替掉 §4 的三条)”
labelCode 存在 → t(labelCode) // 应用默认名,跟随界面语言
label 存在 → verbatim(label) // 人或 AI 的名字,原样
都没有 → t(标准名码[op/inputName]) // 算子级默认
再没有 → 派生名

写入侧对称:创世构造器只写 labelCode 不写 label;将来做重命名时只写 label 并清掉 labelCode, 「人改过名」这件事因此被如实记录,而不是靠猜。

送模型时走同一条优先级,但固定用英文翻译器。

参数码直接复用既有稳定键 operation/inputName,不再造映射。节点与实体码按算子域命名, 例如 feature.bracket.throughHole。

仓库没有运行时 AMIR 文档迁移:migrationRecords 在 crates/aira-core/src/patch.rs 里恒写空数组, AMIR 现状审计 §15 已将迁移判为「只有槽位」。 本设计因此刻意做成加性的:

  • 旧文档没有 labelCode → 落到 label 分支 → 显示与今天完全一致,不掉名字。
  • 旧文档与旧补丁一个字节不改 → patchHash、revisionId 全部不变 → 历史可读。
  • 新文档带 labelCode → 旧校验器会因 additionalProperties: false 拒绝。这是部署顺序问题 (先发校验器再发写入方),不是数据问题。

两个同形先例可直接照抄:

  • 可选字段加宽读取:apps/web/src/sketch/legacy-shell-evidence.ts:275-299 「旧 0.1 备份保持可读;扩展在则必须完整,不在则只是能力降级」。
  • 备份格式版本位:aira-store.ts 的 formatVersion 已有 0.1.0/0.2.0 两档, assertPortableProject 是唯一加宽点。文档形状变化时升到 0.3.0 是自然信号。

若最终仍需改动已存盘数据,aira-store.ts 的 upgradeDatabase → migrateBranchOwnedRevisions 是仓库唯一一个完整的运行时迁移范例(读旧记录、剥字段、回填、失败即 transaction.abort()),照它写。

第一步:参数显示名,纯显示层,不碰 schema、不碰 AI 契约。

把 STANDARD_INPUT_LABELS 八条换成消息码,并把投影层统一到「标准名优先」。 立即效果:Extrusion distance、Fillet radius、Shell thickness 等跟随界面语言; 顺带消掉 §4 的三条规则分歧与 WorkbenchProjectionService.ts:109,126 两处写死。 风险:低。不触碰任何持久化字节,也不触碰模型上下文(模型读的是 parameter.label,不是这张表)。

第二步:节点名与实体名,需要 schema 改动。

层 改动 风险
schema + 生成类型 + lock 三个定义各加可选字段 低,加性
创世构造器 89 条字面量换码;amir-authoring-values.ts 四个构造器的 label 位参要放开 中,量大
投影层 落实 §7.2 优先级 中
模型上下文 码在送出前用英文翻译器解析 高,见 §6
备份格式 formatVersion 升 0.3.0 低
夹具与生成脚本 model-hash 等五份 manifest 与三个生成脚本需重跑 中
  1. 旧文档里的英文默认名怎么办? (a) 原样显示,永不追认;(b) 按 op 反推默认码,让旧项目也跟随语言, 代价是可能覆盖某个恰好同名的人工命名。本文倾向 (a):不猜人的意图。
  2. AI 写的节点名是否一律当自由文本? 参数名必须是(§6)。节点名来自 programLabel(summary, intent), 是模型对自己做了什么的自述,倾向同样原样保留。
  3. STEP 导入的两串固定名算应用默认名还是导入产物? 若算前者就该码化;若将来改成取自源文件的产品名, 则应转为自由文本。两条路会导出不同的字段写法,需要先定。
  • 不翻译已存盘的名字,不重写已存盘的补丁。
  • 不做显示层文本查表。
  • 不把 label 改成联合类型或必填。
  • 不借 extensions 塞码(Parameter 没有 extensions,半条路不叫路)。
  • 不动 modelHash 的投影白名单。