refactor(spec): 折叠 #6619 漏掉的两个手写 unrecognized_keys 映射,并把闭合钉从实例拓宽为类(#6805) - #6935
Conversation
…e the class (#6805) #6416's blind spot was declared closed by #6619/PR #6804, but its inventory was two short. `strictToolError` (ai/tool.zod.ts) and `strictCapabilitiesError` (data/object.zod.ts) were the same shape — `unrecognized_keys` prescription tables attached to a `.strict()` object via `{ error: … }`, seen by no registry — and `TOOL_RETIRED_KEY_GUIDANCE` is a hand-maintained per-key retirement table, the most rot-prone content the audit exists for. - Both maps fold into `strictObject`'s `guidance` channel. #6619's stated blocker (the template appends `history` unconditionally; these surfaces emitted no trailing sentence) was a gap in the TEXT, not a limit of the template — the slot encodes position, and both surfaces had a real history nobody had written down. - Registry visibility 291 -> 293 surfaces (129 -> 131 carrying guidance); added exactly `the tool definition` and `` `enable` ``, removed none. - alias-integrity gains a CLASS pin: no module may hand a zod shape a hand-written map that decides `unrecognized_keys`. Two conjuncts (attached AND deciding that code), no allowlist — `uniqueScopeError` (invalid_union) and `objectStackErrorMap` (per-parse, never attached) are out of class by measurement, each pinned as a live control. - Acceptance byte-for-byte unchanged: 46-case probe matrix identical before and after (parse output + issue code/path). Message assembly moves per the #6804 precedent; 8 keys gain the rename channel. - strictness-ledger fixture moves to PerOperationRequiredPermissionsSchema, per that test's own instruction, with a control on the two vacated sites. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018ffcE95NaMJcL9XJ9VDYgk
…d-residual-errmaps
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
📓 Docs Drift CheckThis PR changes 1 package(s): 113 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
⛔ merge queue 构建失败 — 先分诊,再决定要不要重排队列构建 31297316580 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集), 失败的 job(日志抽取,best effort):
历史信号:
分诊清单:
Generated by Claude Code · merge-queue-triage workflow (#4859) |
|
队列失败分诊 —— 与本 PR 无关,且⛔ 本席暂不重排。 spec-surface 席 #6298。 按 E15 三路分诊走,而不是直接重投。 清单第 1 条 —— 失败测试在本 PR 触及的包里吗?否。 本 PR 只改 清单第 2 条 —— 他处见过吗?见过,而且就在几分钟前。 本席的另一张 PR #6918 在一次独立的队列构建里红在完全相同的四条断言上(build 31297338477,本 PR 是 31297316580)。两张 PR 面向完全不同的文件面(#6918 是 两次独立构建、同一组失败、两个无关的 diff —— 这排除了「某一张 PR 造成的回归」。 所以为什么不重排: 重排只在「失败与本 PR 无关且下一次构建条件会不同」时才有意义。这里两张都中,说明触发条件与我的 diff 无关 —— 重排任何一张都只是再烧一轮全队列,然后大概率红在同一处。今晚已有先例:#6847 的 flaky 重排一次即过,但那次的判据是同批的 #6846 跑同一套全量套件并合并成功,即存在绿的对照。这次没有那个对照,两张都红。 背景读数(git 协议,不吃 REST 配额):
待确认的一项:该钉在干净 main 上是否已红。本席正在独立探针里跑这一条 —— 结论决定这是「main 已坏、影响所有 PR」还是「同批语义冲突」。⛔ 在拿到读数之前不重排。 若探针显示 main 已红,本席会立卡并通知维护者:那是全队列级阻塞,归 Generated by Claude Code |
|
结案 —— 探针读数出来了,两张也都已合入。⛔ 全程没有重排,而不重排是对的。 探针结论:该钉在干净
所以不是 main 已坏。排除了那个分支之后,剩下的是同批语义冲突 —— 而它自己解决了:GitHub 的自动队列管理在批次组成变化后重投,两张都直接通过并合入( 这正是不重排的价值所在。 若当时手动重排,得到的是同样的结果加上一轮全队列重建的代价;而如果重排后仍红,还会诱使人再排第三次。判据不是「看起来像不像 flaky」,而是有没有一个绿的对照:
补充一条对下一个撞上这条钉的人有用的读数:两张 PR 的队列分支相对 该钉是 Generated by Claude Code |
Fixes #6805
前提复核(不继承卡片计数)
在新鲜
origin/main@2672f855f上重跑 census,卡片前提成立:issue.code判)strictToolErrorai/tool.zod.ts:83(消费:180):83/:180✓unrecognized_keys— 在类strictCapabilitiesErrordata/object.zod.ts:169(消费:274):160,PM 分诊评论已更正为:169/:274✓unrecognized_keys— 在类uniqueScopeErrordata/field.zod.ts:271invalid_union— 不在类,未触碰区分「存在 vs 缺席」的对照:同一条查询在同一次运行里找不到
strictVisibilityError/strictWidgetAnalyticsError/strictTenancyError(#6804 折掉的三个),因此不是恒真查询。做了什么
Route 1 + Route 2 都做了,理由与 lane 评估在 #6805 的实施前披露评论里(实施前发布)。
Route 1 — 折叠两张表
strictToolErrorstrictObject+history: TOOL_STRICT_HISTORY+guidance: TOOL_RETIRED_KEY_GUIDANCEthe tool definitionstrictCapabilitiesErrorstrictObject+history: CAPABILITIES_HISTORY+guidance: CAPABILITIES_RETIRED_KEY_GUIDANCEenable两者的 surface 字符串与消费点的
.strict()/.describe()链均原样保留,因此前言一句(Unrecognized key(s) on …)逐字节不变。#6619 记录的「折不了的理由」被证伪,而不是被绕过。
strictCapabilitiesError的 docblock 明确写着:模板无条件追加history(${message} ${history}),而这两张表不发解释句,「Fold it only when the template can express a history-less surface」。复核后这是文案的缺口,不是模板的极限——history槽位编码的是位置(两条修复通道之后,#5955/#6416 的排序契约),#6804 折strictTenancyError时已经据此把常驻解释句放进该槽;这两个面同样有真实可陈述的历史(关门前未声明键被静默 strip,#1535 类),写下来即可折。两句都写进了源码 docblock,连同「为什么之前是空的」。Route 2 — 闭合类,不是实例
alias-integrity.test.ts新增按 AST 判定的类闭合钉:包内任何模块把自己写的、分支在unrecognized_keys上的映射交给一个 zod 工厂调用的{ error }参数,即红。判据是两个合取项,无豁免名单:
z.xxx(…)工厂调用的 params 对象里。schema.safeParse(data, { error: map })是调用方一次解析的选择,不是 shape 的属性。unrecognized_keys——按映射分支的 code 判,不按名字。两个活体反例把这两个合取项各自钉成实测对照,而不是写进豁免表:
data/field.zod.ts的uniqueScopeError:挂载方式与本类完全一致(z.union([…], { error: uniqueScopeError })),扫描够得着它,然后按 code 判它出局(invalid_union)。「不要把它扫进来」由仪器执行,不由名字。shared/error-map.zod.ts的objectStackErrorMap:确实决断unrecognized_keys,但从不挂到 shape 上(按次解析的全局兜底,只有一句通用「check for typos」,没有按键内容)。第一版粗判据(裸字面量)把它和邻居carriesUnknownKey(一个只读issue.code排序 union 分支的谓词)一起误报了——这是本 PR 实现过程中的真实发现,判据据此收紧。扫描覆盖全部模块,含
HELPER_MODULES,刻意不设豁免:收紧后的判据下 helper 本身也不命中(strict-object.ts唯一的z.object(shape, { error: … })传的是对共享工厂的调用,而「调用」正是「非手写」的样子)。实测确认,而非假设——一条什么都不豁免的豁免,读起来像覆盖率,正是本钉要取代的那份清单的失败形态。为什么 Route 2 不占用 #6635 的设计空间
unrecognized_keys的映射挂到 shape 上关键点:本钉抓不到 #6635 要抓的东西。折叠之后
TOOL_RETIRED_KEY_GUIDANCE仍可能含有指向已不存在键的处方(#6756/#6758 那一类),结构钉对此完全沉默——那正是 #6635 存在的理由。这句话写进了钉子自身的注释。盲区闭合的实测证据(注册表可见性 before/after)
strictObjectDeclarations()全量强制遍历后计数,与alias-integrity.test.ts同一套 force + dedup:2672f855f)guidance/guidanceSetsthe tool definitionenable新增恰为两个,与折叠的两张表一一对应。
接受面:逐字节不变(探针矩阵)
46 个用例(22 tool + 20 capabilities + 4 经
ObjectSchema.enable的真实作者门),逐例记录 parse 输出与 issuecode+path(刻意不记 message——message 允许移动)。折叠前后diff为空,合并origin/main@183b4c47b后再跑一次仍为空。覆盖:最小/完整合法体、五个退役键各自单独与全上、编辑距离内/外的未知键、多未知键、未知键+退役键、值级拒绝(regex 不匹配、缺必填、类型错、非对象、null、空对象)、protection envelope 键、
apiMethods的 primitives/legacy/legacy-only/空数组/非法值。消息装配的刻意变化(按 #6804 既有的三类)
labl→label、paramaters→parameters、objectname→objectName、outputSchem→outputSchema、searchible→searchable、trackHistroy→trackHistory、clon→clone、feed→feeds。此前它们只被告知「xis not a ToolSchema field.」/「is not anenablecapability flag.」——说了问题、没给修复。与 refactor(spec): 三个手写 unrecognized_keys 错误映射折叠进 strictObject 的按集合取键 guidance(#6619) #6804 折 tenancy 时(tenantfield→tenantField)同一处置。发射顺序契约(前言 → 修复通道 → 解释句最后)在两个面各有一条专钉。
#6804 遗产的处置
strict-object.test.ts):零改动。本折叠只用精确guidance,不引入guidanceSets,四条优先级规则原样有效且全绿。alias-integrity.test.ts):扩展,不替换。[spec] Fold the three hand-written unrecognized_keys error maps intostrictObjectguidance — requires a set-keyed guidance form, and closes the alias-integrity gate's blind spot (#6416 direction 2) #6619 的三面钉逐字保留;新增一条 [finding][spec] #6619's inventory of hand-written$ZodErrorMaps was two short —strictToolErrorandstrictCapabilitiesErrorsurvive the fold, still invisible toalias-integrity.test.ts#6805 的两面钉(与之并列而非合并,好让两次闭合各自可读),再加类钉与其反真空对照。strictness-ledger.test.ts夹具:按该测试自己的指示搬家(security/permission.zod.ts→TenancyConfigSchema@把 44 个strictUnknownKeyError直调点批量迁到strictObject,棘轮降到 0(路线 1 消不掉手抄数组与 shape 的漂移) #5593 →ObjectCapabilities@[spec] Fold the three hand-written unrecognized_keys error maps intostrictObjectguidance — requires a set-keyed guidance form, and closes the alias-integrity gate's blind spot (#6416 direction 2) #6619 → 本 PR 的PerOperationRequiredPermissionsSchema,同文件、无 guidance 表因而不会被吸向 helper),并新增对照——被腾空的两处现读为strictObject,否则「一个不再区分 idiom 的读数器」也能满足原断言。反向验证(方向在运行前预测)
strictToolError手写映射ai/tool.zod.ts:195 — strictToolErrordata/object.zod.ts:625 — rv2BrandNewError),含 #6804 三面钉在内的实例钉全绿origin/main(钉子保持 #6805 形态)RV2 是把 route 2 与 route 1 分开的那次测量:一个全新的手写映射对每一条实例钉都是隐形的,只有类钉抓得住。
RV3 里刻意保持绿色的对照:「超出编辑距离的键仍被点名、且不给误导性建议」(×2)与「处方文本逐字节保留」(×2)——它们两侧都绿,这正是它们作为对照而非变更钉的资格。
反真空守卫
类钉断言的是「搜索结果为空」,正是仪器坏掉时同样会通过的形态。四条对照:
main上strictToolError及其接线的真实源码作标本,扫描命中第 9 行。unrecognized_keys的模块——本包遍地都是,包括这条钉自己的注释——命中为 0。文本扫描会把它们全部误报,判据就只能退化成豁免名单。objectStackErrorMap决断该 code 却不在类(实测负例,非豁免)。uniqueScopeError挂载方式在类却不在类。(没有把「helper 模块会亮」当对照——粗判据下它会,收紧后不会,因此诚实的活性证据是 (a) 那个仪器从未见过的新文件。)
顺带修掉的一处 in-radius 陈述腐烂
data/field.zod.ts的 docblock 写着「pattern ofstrictCapabilitiesError」——本 PR 折掉该符号后这个指针会失效(正是 #6635 那一类)。替换为「本文件为什么不跟着折」的说明,并点明类钉正把这个站点当活体对照读。test-typecheck-debt.json台账before == after,文件未改动:58 文件 / 266 错误。
check:test-typecheck两次运行均报同一数字。验证(均在合并
origin/main@183b4c47b之后复跑)pnpm --filter @objectstack/spec test:349 文件 / 9055 用例全绿pnpm --filter @objectstack/spec typecheck:绿(tsc+check:scripts-typecheck+check:test-typecheck58/266 未增)pnpm --filter @objectstack/spec check:generated:10 个生成工件全部最新(含check:api-surface——两张映射均为 module-private,公开导出面零变化;含check:authorable-surface,它以OS_EAGER_SCHEMAS=1运行,即ObjectCapabilities模块作用域strictObject调用的 TDZ 实测清账;含check:strictness-ledger,counts 工件零再生)check:api-surface报 stale——新 worktree 未 build 的幻影,pnpm --filter @objectstack/spec build后复跑全绿check:exported-any/check:dual-source-exports/check:liveness:绿node scripts/check-nul-bytes.mjs:绿;触碰文件另做超出门面的自扫([\x00-\x08\x0b\x0c\x0e-\x1f\x7f]):无命中npx eslint(7 个触碰文件)--no-inline-config:exit 0check:empty-changeset/check:doc-authoring/check:role-word/check:adr-anchors:绿Failed to resolve entry假红):@objectstack/lint67 文件 / 1759 用例绿,@objectstack/metadata-protocol64 文件 / 763 用例绿消费半径清扫
packages/spec之外对两个旧符号名、两条旧兜底消息字节的引用为零;下游测试中没有任何一条钉住这两个面的消息(按字面短语与toMatch(/…/)转义正则两种拼法各扫一遍)。声明顺序(#5593 的 TDZ 约束)
ObjectCapabilities不是lazySchema,strictObject在模块作用域求值其 options,因此CAPABILITIES_RETIRED_KEY_GUIDANCE与CAPABILITIES_HISTORY必须在其之上声明——两者本就在。约束写进了ObjectCapabilities的 docblock(与ObjectSchemaBase上 #5593 的同类警示对齐),并由OS_EAGER_SCHEMAS=1的全量 build 实测清账。Changeset
.changeset/fold-residual-unrecognized-key-maps.md(@objectstack/spec: patch)Generated by Claude Code