Repository navigation
[P2] objectui: a spec-named symbol must be an import, not a declaration — make "derived vs hand-written" a CI rule, not a review convention #4115
Description
Systematizes what #4074 / objectui#3003 / objectui#3009 did by hand for the third time. Filed so the rule outlives the sessions that keep rediscovering it.
The failure class
objectui declares local types/consts under the same names as @objectstack/spec exports, with doc comments claiming spec canonicity. Every audited instance had either drifted or was one spec release away from it:
| symbol | comment claimed | code did | found |
|---|---|---|---|
ActionType |
"canonical definition from @objectstack/spec" | hand union, missing form |
#2231/#2901 |
ChartType |
(sibling schema re-exported under spec's name) | 7 of 19 members | objectui#2944 |
ActionLocation + 2 |
"single source of truth… re-export here" | re-declared union + tuple + z.enum |
#4074 |
ActionParamFieldType |
"aligned with the field types available in spec" | 16 of 49 members | #4074 |
The mechanism that makes this class expensive is specific to agent-driven development: an agent reads the comment, takes it as ground truth, and builds on it. #2901 was literally filed with a backwards premise because the fork was re-exported under the spec's own symbol name. A wrong canonical-claim is not stale documentation — it is a planted premise for the next session.
And the absence of derivation is what made #3856 silent: spec 17 removed action.execute and objectui produced zero compile errors, because nothing was bound to the spec's z.infer.
The rule
Any exported type or const in objectui whose name matches a
@objectstack/specexport must be animport/re-exportof it — or carry an allowlist entry with a reason.
Enforced by a CI script (~100 lines), not prose:
- Enumerate spec's export names per subpath (
@objectstack/spec/ui,/data, …) at runtime. - Grep objectui
packages/*/srcforexport (type|const|interface) <SameName>declarations not backed by an import from spec. - Fail with the symbol, the file, and the fix ("import it, or add an allowlist entry with a reason").
Value exports additionally pin by reference identity (expect(X).toBe(SpecX)), because a faithful copy passes every value comparison — identity is the only check that distinguishes a re-export from a fork. This insight is already written down in objectui's spec-subschema-parity.test.ts and already applied to ACTION_LOCATIONS / ActionLocationSchema / ACTION_PARAM_FIELD_TYPES (objectui#3003); this issue is about applying it uniformly instead of one symbol per incident.
The allowlist (what keeps this from being a nuisance)
Same governance as check-type-check-coverage.mjs's DEBT map — declared, reasoned, shrink-only. Known-legitimate first entries:
@object-ui/typesActionSchema— a deliberate objectui-side view over the spec's action (carries renderer-only fields likeopenIn); documented as such.ObjectUiLocalActionType/ObjectUiLocalParamFieldType— named local dialects, each with a tripwire test asserting the spec does not own the name (so the day spec adopts it, CI names the alias to retire). This is the sanctioned escape hatch for "implementation accepts more than spec": declare the dialect next to the vocabulary it extends, never hide it in aRecord<string, string>in another package.- The
legacy ActionSchemaincrud.tsand similar explicitly-legacy shapes, if any collide.
A same-name symbol that is narrower or wider on purpose belongs in the allowlist with that purpose written down — at which point the comment is load-bearing instead of decorative.
One meta-rule
This rule must land as the check, not as an AGENTS.md paragraph. A prose-only rule is precisely the "declared ≠ enforced" landmine this whole thread is about (#4074, objectui#3009 — where a guard file's own header falsely claimed its checks were "the real enforcement" for the entire interval nothing compiled it).
Refs #4074, #3856, objectui#3003, objectui#3009, objectui#2944, #2901, #2231.
Activity
os-zhuang commentedon Jul 30, 2026
The rule landed as CI in objectui#3032 (
62311b6) —scripts/check-spec-symbol-derivation.mjs, in thetype-checkjob, 5.5s.One implementation note that mattered. The issue's step 1 says "enumerate spec's export names per subpath at runtime." That doesn't work: a runtime
import()sees only values, and the drifted symbols in the table above are mostly types —ActionType,ChartType,ActionParamFieldTypewould all have been invisible. The script goes through the compiler's view of each subpath's.d.tsinstead, which yields 4231 export names, types and values alike.The three allowlist forms are supported as prescribed: re-export,
z.inferderivation, or a ledger entry. Derivation must sit in a structural position — a type alias's own type node, an interface'sextends, a const's initializer — because a spec name merely mentioned inside a members block is what a fork looks like. Consts are stricter still: array and object literals aren't descended into, soexport const ACTION_LOCATIONS = [...SpecLocations]is flagged. That is the reference-identity argument from objectui#3003 applied statically — a faithful copy passes every value comparison.Leaving this open, because the allowlist is not the size this issue imagined. The ask sketched "known-legitimate first entries" — roughly the three named above. The measured reality is 156 collisions across 19 packages. So what shipped is a debt ledger, not a small permanent exception list, and the governance detail matters more than expected: it names symbols, not per-package counts. A count would be a budget — it would let a new fork land as long as an unrelated one was fixed in the same PR, and it would make the failure message point at whichever symbol sorts first rather than the one just written.
--ledgerregenerates the block, so burning entries down is mechanical.Each of the 156 resolves one of three ways: it's a fork (→ re-export, delete the declaration), it needs local shaping (→
z.infer), or it's a genuine dialect (→ stays, but with a written reason and the tripwire test this issue prescribes, so the day spec adopts the name CI says which alias to retire). Starting the burn-down at@object-ui/types, since that's where the repo takes its vocabulary from and a fork there travels furthest.Generated by Claude Code
os-zhuang commentedon Jul 30, 2026
@object-ui/types全量分诊完成:73 条碰撞,55 条已经漂了承 objectui#3017(「手抄 spec 清单」模式排查)的收尾结论 —— 本 issue 的债务台账是同一失效类的超集,且
@object-ui/types占最大一块、又是全仓传播根(其他包都 import 它),所以先从它开刀。对 73 条碰撞记录(71 个唯一名)逐条两边对读:本 issue 正文说这批是「已漂移或离漂移只差一个 spec 版本」。实测明显偏前者:四分之三已经漂了,而且不止是「少几个枚举成员」——有几处是活的缺陷。
已落地(objectui#3057,台账 149 → 144)
只取可证明安全的一档(本地是 spec 严格子集,或逐成员相同),其中两条是活缺陷:
PageVariableSchema漏source,即整个 ADR-0049 写绑定 —— spec 写法{name:'selectedProjectId', source:'project_picker'}解析后变成谁也写不了的变量;type枚举还漏record_id。同一文件下三行的PageTypeSchema已是 spec 引用,注释写着「镜像漏了list导致 spec 合法页面校验失败」——修了一个、漏了隔壁那个。这是本 issue 描述的失效模式最干净的一个现场。FeedItemType7/13。缺的 6 个(file/sharing/note/record_create/record_delete/approval)在RecordActivityTimeline里既无图标也无颜色 —— 服务端发出即渲染不出。因为两张表是Record<FeedItemType,…>全键映射,widen 后编译器直接把缺口报出来。PermissionAction8/11(漏execute/manage/configure)、ObjectIndex(漏fulltext/unique:'global'/partial)、PageVariable(与 spec 逐成员相同)。剩余 50 条 drifted:按修法难度分三组
A 组 · 需要重命名(概念真不同,名字撞了) —— 正解是改本地名,但多数从 barrel 公开导出,是 breaking change,需先定约定:
FilterCondition(本地是 FilterBuilder 的一行{field,operator,value},spec 是递归 ObjectQL AST)、FilterOperator、EventHandler(SDUI 事件绑定 vs 内核事件总线订阅)、CacheStrategy(service-worker 取数策略 vs 缓存淘汰策略)、MutationEvent(对象 payload vs 字符串枚举)、SpanSchema(HTML<span>vs OpenTelemetry span)、ValidationRule(×2)、BatchOperationResult(本地是整批汇总,spec 是单条结果 —— 本地形状其实精确对应 spec 的BatchUpdateResponse)、ReportSchedule、WidgetManifest/WidgetSource、GestureType/GestureConfig等。B 组 · 渲染节点 vs 元数据文档(
type语义冲突) ——PageSchema/AppSchema/DashboardSchema/ReportSchema,TS + Zod 各一份共 8 处。核心冲突:spec 的type是页面/报表的种类(record|app|utility|list|home),objectui 的type是渲染器判别式(z.literal('page')),种类被挪到pageType。所以 spec 页面{type:'list'}在本地直接 fail,本地页面在 spec 侧pageType被静默丢弃。得先定命名约定(*NodeSchema?)才能动。C 组 ·
.passthrough()静默放行(建议优先) —— 这是 objectstack#4120 那个失效形态在 objectui 的等价物,且不需要重命名决策:AppSchema/DashboardSchema/PageSchema的 Zod 侧继承BaseSchema.passthrough(),而 spec 侧是.strict()。后果是 spec 独有的 22/13/15 个键未经任何校验穿过去 ——branding/sharing/protection/_lock*全部不验,brading:这种拼写错误完全隐形。已有范式可抄,不必新发明
zod/objectql.zod.ts的ListViewSchema是全场最佳实践:spec 字段经SpecListViewFields.shape按引用流入,配list-view-spec-parity.test.ts漂移守卫(spec 长出新字段而 objectui 未分诊即报红),注释所述与代码所做完全一致。B/C 两组应向它收敛,而不是各自发明。顺带:守卫本身工作良好,值得说一句
烧掉 5 个名字后跑守卫,它主动报红说台账里有 5 个「不再碰撞」的条目、要求删除 —— 也就是登记表自身的腐烂被防住了(这正是 #4120 抓到的那个终末形态:注释指向一个早已删除的符号)。两个方向都做了变异测试:重新 fork 一个已烧名字 → 按名报红;烧掉的名字留在台账 → 按腐烂报红。
--ledger重生成 + 逐条 diff 也确认没误抹其他包的债务。os-zhuang commentedon Jul 30, 2026
C 组已落地(objectui#3063)
分诊评论里划出的三组中,C 组(
.passthrough()静默放行)已合入 main。选它先做的理由成立:它是本 issue 失效类里唯一不需要重命名决策的一组,A/B 两组都得先定命名约定。实际规模比分诊估计的大
AppSchema/DashboardSchema/PageSchema都 extend 自.passthrough()的BaseSchema,spec 侧是 strict —— 合计 39 个 spec 键完全不经校验穿过去:App 23(branding/sharing/embed/objects/apis/requiredPermissions/homePageId/protection+ 整个_lock*/_package*/_provenance包锁封套)、Dashboard 10、Page 6。Page 那 6 个里有一个活缺陷:
source从未被声明,而kind: 'html' | 'react'的页面正是把正文放在source里 —— 这类页面在 objectui 里根本无法表达。修法要点(两条值得记下)
没有加
.strict(),那是错解。这三个是组件节点,开放封套是有意为之(渲染器属性该穿透)。正解是让 spec 字段按引用流入 —— 即zod/objectql.zod.ts的ListViewSchema已在用的范式。收紧的只是 spec 拥有的那部分,并专门加了断言守住「未知渲染器属性仍然放行」。zod 4 拒绝对带 refinement 的对象调
.omit(),而 spec 的PageSchema恰好有一个 —— 所以本 issue 里到处推荐的惯用写法SpecSchema.omit({…}).partial()会在 import 期直接抛异常,不是运行期。新的specFieldsExcept()(objectuibase.zod.ts)改读.shape绕开,坑记在它的文档注释里。后续给其他 schema 加派生的人会再撞上这个,建议在本 issue 正文的修法模板里补一句。闸门
新增
page-app-dashboard-spec-parity.test.ts(19 断言),覆盖三个方向 + 一组「这些键现在真的被校验了」的钉扎(branding: 'blue'从通过变拒绝、kind: 'nonsense'从通过变拒绝、kind:'html' + source现在能表达)。变异测试两个方向都红:偷偷 omit 掉branding→ 3 个测试按名报红;加未登记的本地键 → rogue-key 断言报红。验证:全仓 type-check 78/78,types 271 断言,下游 app-shell/plugin-dashboard/plugin-list/core 共 3626 断言全绿 —— 收紧校验没挡住任何现有渲染路径。
剩下的 A/B 两组需要先拍板
两组都卡在命名决策上,不适合由实现方自行决定:
FilterCondition/FilterOperator/EventHandler/CacheStrategy/MutationEvent/ValidationRule×2/BatchOperationResult等,正解是改本地名,但多数从 barrel 公开导出 —— 是 breaking change,得先定是直接改、还是先加别名过渡。type语义冲突 —— spec 的type是页面/报表种类,objectui 的是渲染器判别式。需要先定约定(*NodeSchema之类)才能动。本 PR 已把 Page 的这处冲突在代码注释里写明并 omit 掉,没有擅自调和。os-zhuang commentedon Jul 30, 2026
A 组已落地(objectui#3068)—— types 侧从 71 降到 44
按维护者指示直接改名、不做别名过渡。这 15 个是分诊里的 COINCIDENCE 判决:本地概念与 spec 某个导出同名但建模的是别的东西 —— 也就是本 issue 正文点名的那类「名字本身就是缺陷」。
几个对照值得记录(左本地、右 spec 同名符号的真实含义):
SpanSchema= HTML<span>↔ OpenTelemetry 追踪 span;CacheStrategy= service-worker 取数顺序 ↔ 缓存淘汰策略;MutationEvent= 对象 payload ↔ 字符串枚举;BatchOperationResult= 整批汇总 ↔ 单条记录结果;EventHandler= SDUI 事件绑定 ↔ 内核事件总线订阅。全部改为FilterBuilderCondition/FetchCacheStrategy/TextSpanSchema/BatchOperationSummary/UIEventHandler等。改名过程暴露的两个既有问题
objectui 自己有两个互相冲突的
ValidationRule:field-types.ts是扁平约束包(minLength?: number),form.ts是 RHF 风格包装形(minLength?: {value, message})—— 键名全同、值形状不兼容。barrel 只导出后者,前者是被刻意从导出块省掉以躲开碰撞的,所以深引field-types的消费者会静默拿到另一个类型。这个碰撞在本仓内部藏了很久,是被 spec 改名工作顺带挖出来的。zod/reports.zod.ts的ReportComponentSchema已被判别联合占用,而zod/theme.zod.ts早已在用「节点 =*ComponentSchema、联合 =*UnionSchema」的约定 —— 两个文件的命名约定原本就不一致。借这次统一了。方法上的一条建议,值得写进本 issue 的修法模板
不要对这类改名做全局字符串替换。
FilterCondition/ValidationRule/ThemeSchema/ReportSchema/SpanSchema这些名字两边都有,盲替会把消费 spec 的引用一并改错 —— 那正好制造出本 issue 要消灭的那种「名字说的和实际指的不是一回事」。可靠做法是先改声明与 barrel,让 tsc 把所有断裂点列出来逐个判定。这次人工排除的真危险点有三处(
clientValidation.ts从@objectstack/spec/ui导入ReportSchema/ThemeSchema、report-schema.ts、spec-report.ts的ReportSchema as SpecReportSchema_导入别名),同时 tsc 也确实抓到了 3 处我漏改的引用。验证
台账 144 → 129(types 侧 71 → 44),
--ledger重生成后逐条 diff:removed 恰为这 15 个、added 为空。变异测试:把FilterBuilderCondition改回FilterCondition→ 守卫按名+按文件报红。全仓 type-check 78/78,全量测试 8696 断言绿(752 文件)。本仓进度小结
.passthrough()静默放行)type语义冲突)B 组仍需先定命名约定:spec 的
type是页面/报表种类,objectui 的是渲染器判别式。注意 A 组已经把*ComponentSchema(节点)/*UnionSchema(联合)这套约定在 report/theme 两处统一了,B 组若沿用同一套会比较自洽 —— 但那是维护者的决定,本轮没有擅自扩大。fix(spec): five exported symbols resolved to `any` — type the recursi…
os-zhuang commentedon Jul 30, 2026
B 组已落地(objectui#3074)—— objectui 侧四组全部处理完
B 组是
type的语义冲突:spec 的type是页面/仪表盘的种类(record|app|utility|list|home),objectui 的type是组件判别式('page'),种类被挪到pageType。所以 spec 写法{type:'list'}的页面在 objectui 直接校验失败,objectui 的pageType在 spec 那边被静默丢弃 —— 而两者顶着同一个名字。AppSchema→AppComponentSchema,DashboardSchema→DashboardComponentSchema,PageSchema→PageNodeSchema。一个值得写进 issue 正文的教训:改名也会撞名
Page 本来要叫
PageComponentSchema(维护者钦定的约定)。但PageComponentSchema本身已经是@objectstack/spec/ui的导出,而且它指的是页面区域内部的一个组件({type: PageComponentType, properties, …}),不是页面本身。用它等于把本 issue 要消灭的缺陷原样搬到隔壁概念上。这不是推演 —— 那个 PR 的第一版真的用了
PageComponentSchema,被守卫按名拦下。 所以:给撞名符号挑新名字时,新名字必须先过一遍守卫,否则很容易从一个碰撞滑进另一个。建议在修法模板里补一句。方法上的两个坑(都被工具抓到,不是靠眼睛)
import {\n PageSchema as SpecPageSchema,\n} from '@objectstack/spec/ui'—— 成员行本身不含 spec 路径,所以任何「跳过含@objectstack/spec的行」的启发式都会漏。4 处 spec 导入别名被误改,特征是…ComponentSchema as Spec…。apps/目录:objectui 的消费者不只在packages/下,apps/console也有引用。tsc 抓到了。再次印证前一条评论的建议:不要做全局字符串替换,先改声明和 barrel、让 tsc 列出断裂点逐个判定。这一轮 tsc + 守卫合计抓到我 3 类疏漏。
objectui 侧总账
.passthrough()静默放行)type语义冲突,3 个)全仓台账 156 → 126,
@object-ui/types71 → 41。余下的性质变了
types 剩的 41 条不再有「纯撞名」这一类 —— 全是 FORK_DRIFTED,即同一个概念但两边形状已经不一致(
FormField/SelectOption/ActionParam/NavigationItem/ObjectPermission/SharingRule/五个*Validation/QueryAST/DriverInterface等)。每一条都要单独决定「派生 + 收敛差异」还是「保留差异 + 记录理由」,没有机械做法,也不适合再按组批量推进。其中有几条我在分诊时标注过、值得优先看:
ObjectPermission—— objectui 内部有两个互不兼容的版本,而app-shell/…/PermissionPreview.tsx里那个恰恰是 spec 形状的手抄件;NavigationItem/NavigationItemSchema—— spec 侧是 9 个变体的判别联合且每个目标字段必填,objectui 是一个全可选的扁平接口,badgeVariant少了secondary、group 展开键名不同(expandedvsdefaultOpen);*Validation(Script/StateMachine/CrossField/Conditional/Format)—— 注释都写着 "ObjectStack Spec v2.0.1",而装的是 17.0.0-rc.0,condition在 spec 已是Expression对象而本地仍是裸字符串。os-zhuang commentedon Jul 31, 2026
Navigation 那一簇的漂移已修(objectui#3088)—— 但台账不减,理由如下
一个改变影响评估的事实,建议写进本 issue 正文
objectui 的 zod schema 在生产代码里只有一条运行时路径:已发布的
objectui validate命令(@object-ui/cli,非 private,bin 名objectui)。渲染器直接读普通对象,从不 parse。这把剩余台账分成了两类,值得分开排期:
*Schema(zod)条目 → 影响的是 CLI 校验的正确性;而且 schema 是 strip 模式(丢弃未知键而非报错),所以失效必然是静默的;我之前把这两件事混在一起谈了,这是修正。
实测出来的六处(spec 17.0.0-rc.0)
最值得记录的两条:
requiresObject/requiresService是NavigationItem一直声明着、NavigationRenderer一直在执行的运行时门控,只有 zod schema 落后 —— 于是校验一个 app,会把它自己 nav 元数据里的门控丢掉。这是「同一概念的三份表述(TS 类型 / zod / 渲染器)各自漂移」的典型。expanded那处渲染器写的是(item as any).expanded,注释还写着「honor either so app authors don't get silently-ignored config」—— 作者知道 spec 用这个拼写并做了处理,却因为类型没声明而被迫 cast。一个as any就是漂移已经发生的现场证据,值得作为排查线索:grep "as any).<field>"能找出更多「运行时已适配、契约未跟上」的地方。其余四条:
actionDef被剥掉(动作项校验通过却什么都不做)、badgeVariant:'secondary'硬拒、{type:'separator'}被拒、NavigationArea的order/description被丢。台账为什么不减少
spec 的
NavigationItemSchema声明为z.ZodType<any>(递归的 group 变体让联合类型被擦除),所以type NavigationItem = z.infer<...>=any—— re-export 会删掉全部导航类型信息。objectui 必须保留本地声明,名字碰撞就还在。但它的各个变体类型是完整的(
ObjectNavItem/ActionNavItem/GroupNavItem/…)。所以本 PR 的badgeVariant是从ObjectNavItem['badgeVariant']派生的,而不是再抄一遍枚举。建议补进修法模板:联合擦除成any时,先看变体是否可派生,不要因为伞形类型不可用就整份手抄。一条方法教训
为放行 spec 合法的
{type:'separator'},我第一版把id/label对所有类型改成可选 —— 这会让{type:'object'}这种无标识项也通过。是既有测试拦下的(navigation-model.test.ts两条断言),测试对、我的 schema 错。改成用superRefine只豁免 separator,并补断言钉住边界。放宽一个 schema 去接纳 spec 的某个合法形状时,很容易顺手放宽掉别的不该放宽的。验证
9 条新断言;变异测试:四处 schema 修复逐个撤掉各自按名报红,豁免放宽成无条件则三条一起报红。全仓 type-check 78/78,全量 8932 断言绿。
objectui 侧剩余
台账 124(types 39)。下一个候选是 FormField 那一簇(
FormField/FormFieldSchema/SelectOption/SelectOptionSchema),失败模式最恶劣:spec 用field作字段身份键、objectui 用name且必填,实测objectui validate对任何 spec 写法的表单都硬拒(fields.0.name: expected string, received undefined)。但它需要先定namevsfield的取舍,已交维护者拍板。os-zhuang commentedon Jul 31, 2026
objectui 侧表单簇进展(objectui#3090 / objectui#3091,已合入):
台账 124 → 122:
SelectOption+SelectOptionSchema完成 triage 后移入 ALLOW —— zod 侧改为 spec shape 按引用派生,两个钉住的有意分歧(value放宽给独立 UI 表单;visibleWhen保持 objectui wire 契约而非 ExpressionInput 的 envelope 规范化管道)+ 两个 UI 扩展(disabled/icon),parity 测试四向守卫(spec 加键/退键/认领扩展名/自行放宽 value 任一即红)。FormField/FormFieldSchema两条确认不减,分类为双层概念:spec 的 FormField 是 authored 层(field= 对象字段引用,presentation delta),objectui 的是 runtime 层(name= 数据路径,自足控件配置)。转换枢纽normalizeSectionField一直存在但零闸门 —— 对照 spec 17.0.0-rc.0 盘点发现它丢四个键,包括 ADR-0089 的规范拼写visibleWhen(弃用拼写visibleOn反而能用)。已补齐 + 28 键逐键行为闸门(mutation-tested)。修法模板可补一条:当同名符号是「双层词汇」而非「同概念方言」时,burn-down 的正确形态不是派生而是「枢纽 + 键集覆盖闸门」 —— 枢纽是翻译(每键有各自落点),闸门枚举
SpecSchema.in.shape双向比对。判别法:问「这两个类型描述的是同一个对象,还是管道上下游的两个阶段?」SelectOption 是前者(可派生),FormField 是后者(枢纽 + 闸门)。os-zhuang commentedon Jul 31, 2026
objectui 表单簇战役收官(objectui#3090 已关,7 个 PR:objectui#3091/#3092/#3097/#3100 + 前期 #3043/#3057 相关件)。台账 124 → 120,表单簇 6 条全部处置:2 烧(SelectOption 对,spec 派生 + 四向 parity 守卫)、2 ALLOW(FormField 对,双层论证 + no-restricted-imports tripwire)、加上前期 SelectOption TS/zod 双侧的 documented 处理。
给修法模板再补两条经验:
「误导入会被 tsc 抓」在 [P2] spec: four exported types resolve to
any, so "bind the spec-named symbol to the spec" actively degrades the consumer that obeys it #4171 修好前是错的——spec 类型擦除成any时,误导入是 tsc 完全沉默的类型安全删除。机制解法是no-restricted-imports按importNames禁导 + 报错文案写明两层与正确路径(文案会被 agent 当修复指令执行,值得当 API 设计)。规则上线即抓到按名 grep 不可见的三种形态:命名空间导入、export-all、多行 re-export——5 个既有点位全是合法消费者(守卫测试、Spec 前缀公共别名),逐一行内豁免带理由。已知残余侧门:export type * as UI形态 base 规则不查。跨包类型影响的本地验证必须先重建依赖 dist——per-package tsc 解析到改前的 types dist 会假绿(turbo 缓存再叠一层),CI 才抓到 OptionLike 波及。
turbo build --filter=<dep>后再type-check --force才是真话。11 remaining items
os-zhuang commentedon Aug 2, 2026
跨包定名:
OfflineConfig归@object-ui/react,types 侧改名 —— 批次 5 请直接沿用(附Theme结论)批次 2(objectui#3156,
@object-ui/types页面/导航/杂项簇 16 条)已处置完毕,PR 待开。先把批次 5 正在等的那条单独发出来。OfflineConfig:两侧不是方言,是两个概念 —— 而且拿错名字的是 types 侧对读三处后的实测:
@objectstack/spec/ui{ enabled, strategy: cache_first|network_first|…, cache: { maxSize, ttl, persistStorage, evictionPolicy }, sync: { strategy, conflictResolution, retryInterval, maxRetries, batchSize }, offlineIndicator, offlineMessage, queueMaxSize }@object-ui/types(mobile.ts){ enabled, defaultStrategy: 'cache-first'|…, routes: OfflineRoute[], maxCacheSize, cacheExpiration, offlineObjects, syncStrategy, conflictResolution }PWAConfig/PWAIcon同段@object-ui/react(hooks/useOffline.ts){ enabled?, strategy?, cache?, sync?, offlineIndicator?, offlineMessage?, queueMaxSize? }所以结论与直觉相反:
@object-ui/types的改名为PWAOfflineConfig(本批已落地)。它建模的是 service-worker 取数顺序 —— 注意它的策略类型早在 A 组就已被改名为FetchCacheStrategy(「service-worker 取数顺序 ↔ 缓存淘汰策略」),这次只是把伞形类型补齐到同一判断上。枚举拼写也不同(kebab vs snake)。@object-ui/react的保留OfflineConfig这个名字,走派生,不要改名。 它是 spec 概念的手抄件,正解与FilterCondition在 app-shell 的处置同构(objectui#3169:看着像方言,其实是该 re-export 的那个)。批次 5(objectui#3159)施工建议:
OfflineConfig/OfflineCacheConfig/OfflineSyncConfig三条一起派生;并且要用z.input而不是z.infer——enabled/strategy/persistStorage/evictionPolicy/conflictResolution/offlineIndicator全带.default(),而useOffline(config = {})是授权侧(调用方构造配置),用 output 类型会让「传空对象」不可表达。这正是 objectui#3169 那条判据:在写这份配置 →z.input,在读已 parse 的结果 →z.infer。同批的OfflineStrategy/ConflictResolutionStrategy大概率可直接 re-export(OfflineStrategy/ConflictResolution是 spec 导出),但落地前每个新名字/每次 re-export 仍要自己跑一遍守卫与对读——本轮就有一条「早期分诊标注为疑似真撞名、实测是可派生」的反例(见下)。Theme:types 侧改为 re-export spec 的ThemeInput(批次 7 请据此再分诊 providers)@object-ui/types的Theme与 spec 的ThemeInput逐成员相同(双向可赋值探针为identical),所以本批直接export type { ThemeInput as Theme }。要点是取
ThemeInput而不是Theme:spec 的z.infer<typeof ThemeSchema>是 parse 后的形状,mode因.default('auto')变成必填;objectui 里流转的 theme JSON 是授权侧(存储、编辑、交给ThemeProvider),mode必须可省。原声明的注释其实一直写着这件事(「ObjectUI uses the input shape」)—— 它知道自己是 input 形状,却仍然顶着 output 类型的名字自己抄了一份。批次 7(objectui#3161)的
@object-ui/providers:Theme因此不能照搬结论:先问它是「运行时已解析的主题」还是「授权 JSON」。若是后者,它多半也能直接 re-export(types 侧现在就是),那样两个包会自然收敛到同一个 spec 类型;若它带了解析后的 CSS 变量/必填mode,那才是z.infer那一侧,应派生自 spec 的Theme而非改名。两种情况都不需要新方言名。顺带一条给修法模板:「早期分诊的疑似撞名,仍要自己对读」
本批正文预判
GestureConfig/GestureType、WidgetManifest/WidgetSource「大概率走改名」——实测四条全部成立,但同一份分诊里被顺手带过的ActionParam却是可派生的(且派生后补回三个 spec 键)。反过来,NavigationArea看着像「#3088 修完只剩名字问题」,实测 spec 侧navigation是any[](NavigationItemSchema的z.ZodType<any>传染到了伞形类型),整份 re-export 会把导航树类型删掉,只能「继承其余键 + 钉住这一键」。也就是说:分诊结论的颗粒度是「符号」,不是「簇」。同一簇里三条走三条不同的路,这一轮就发生了。
Generated by Claude Code
os-zhuang commentedon Aug 2, 2026
批次 4(objectui#3158)已开草稿 PR:objectui#3188 —— 台账 77 → 64,
@object-ui/core整包出账@object-ui/core13 条,6 派生 + 7 改名 + 0 ALLOW。判决与逐符号理由见 PR 正文,这里只记三条对后续批次有用的东西。一、三个副本在自己的注释里就写明了自己是副本
这批最值得记的不是数量,而是失效证据都是现成的,写在被烧掉的代码旁边:
CONTEXT_TOKENS的注释写着「mirrored here … because the installed@objectstack/specpredates that export … 临时,直到下次协调发布」。而装的 spec(17.0.0-rc.0)早就导出了它,副本与 spec 逐字节相同 —— 也就是说,在它的理由为假的整段时间里,它通过了所有值比较和所有行为测试。RowHeight的注释自称「the spec'sRowHeightSchemavocabulary」,实际是手抄 union。碰巧还是对的。managedBy.ts自称「UI-side mirror of the framework'sresolveCrudAffordances()」,并自带一份DEFAULTS—— 与 spec 的CRUD_AFFORDANCE_DEFAULTS逐行相同,连 override 解析器也抄了一份。另外
defineView已经把错误前提种到隔壁包了:app-shell/…/view-item-normalize.ts的注释在讲「per-objectdefineView({list, form, listViews, formViews})文档」—— 那是 spec 的defineView,而这个名字在本仓解析到的是 core 的「冻结 + 打标」helper。正是 #2901 的机制,只是这次被抓在了发生现场。二、改名不够,要把抄来的那一半交还给 spec
resolveCrudAffordances如果只改名,那张抄来的 bucket 表还在。所以 bucket/userActions那一半改为委托 spec 的resolveCrudAffordances(),全平台只剩一份定义;留在 objectui 的只有 spec 没有的那部分 —— 与服务端 effective API operation 求交(#3391),名字(resolveEffectiveCrudAffordances)现在说的就是这件事。配了一条 parity 测试,跨全部 7 个 managedBy 值 + #2614 对象形逐一比对 spec 输出。建议补进修法模板:同名符号如果是「spec 的那件事 + objectui 的额外一层」,正解是 委托 + 改名,而不是改名了事 —— 否则台账条目没了,fork 还在。
三、变异测试把 #3003 那句话演了一遍
把
CONTEXT_TOKENS改回那份刚删掉的逐字节副本,守卫按名 + 按文件报红(filter-tokens.ts:61),同时新加的 parity 测试报:两个长得一模一样的数组,同文件另外 20 条断言全部照常通过。忠实副本满足一切值比较,只有引用同一性能区分 re-export 与 fork —— 这条以后可以直接引用这个失败信息,比讲道理省事。
四、两条施工规约建议(都是本轮踩到的)
turbo type-check全绿不等于改名改完了 —— 测试文件不在 type-check 范围内。 78/78 绿的时候,PluginSystem.test.ts和managedBy.test.ts还引用着旧名字,是跑测试才暴露的。建议写在「禁止全局替换」旁边。re.sub,结果把对照说明也改了 —— 「NOT the spec'sValidationError」变成「NOT the spec'sSchemaNodeValidationError」,等于用修误描述的动作现制造了一条新误描述。已在自查时发现并还原。现有规约说的是「不要跨仓全局替换」,应当补一句:在记录该碰撞的文件内部同样不要。验证
台账
--ledger重生成逐条 diff:removed 恰为本批 13 条、added 为空、其余 17 包逐字未动(有并行批次)。守卫绿;turbo build --filter=@object-ui/core重建 dist 后type-check --force→ 78/78;全量 9689 断言通过、0 失败(821 文件),受影响的包单独跑也全绿(core 61 文件/1335;app-shell + plugin-form + plugin-grid + plugin-list 347 文件/3127)。PR:objectui#3188(草稿)。未自行合并,合入前不关闭 objectui#3158。
Generated by Claude Code
os-zhuang commentedon Aug 2, 2026
批次 2 已开草稿 PR:objectui#3191(台账 77 → 61,
@object-ui/types20 → 4)。16 条两种处置,本轮无新增 ALLOW:
ActionParam(z.input+type一处放宽)、CreateExportJobRequest(Omit<…,'object'>)、CreateExportJobResult、ImportRowResult、NavigationArea、NavigationAreaSchema、Theme(= spec 的ThemeInput,逐成员相同)。顺带ExportJobFormat也改为 re-export spec 的ExportFormat。FileMetadata→UploadedFileMetadata、GestureType→TouchGestureType、GestureConfig→TouchGestureConfig、OfflineConfig→PWAOfflineConfig、PageRegion→PageNodeRegion、PageRegionSchema→PageNodeRegionSchema、ResponsiveConfig→MobileResponsiveConfig、WidgetManifest→RuntimeWidgetManifest、WidgetSource→RuntimeWidgetSource。跨包定名见上一条评论(
OfflineConfig归 react 侧派生保名、types 侧改名;Theme两侧都不需要新方言名)。挖出的活缺陷、验证与变异测试细节都写在 PR 正文里,这里只补一条值得进修法模板的方法结论:parity 测试对「忠实手抄」是瞎的 —— 必须钉引用同一性
变异测试第二组:把已派生的
NavigationAreaSchema改回手写z.object({…}),键集完全一致。结果是 39 条 parity 断言全绿 —— 键集覆盖、双向 parse、_input/_output分歧钉扎,一条都没红,因为那份手抄件确实是忠实的。只有守卫按引用抓到了它。这正是 objectui#3003 的论证(也是 objectui#3169 在
isAggregatedViewContainer上再次实证的那条),但之前的表述是「re-export 一个 value 时闸门要用toBe」。实际范围更大:凡是specFieldsExcept这类「spec 字段按引用流入」的 schema 派生,parity 测试也必须断言引用同一性,否则它只能证明「今天两边一样」,而那恰恰是手抄件也能通过的那句话。具体写法有个坑:
specFieldsExcept结尾是.partial(),会把每个字段重新包一层ZodOptional,所以local.shape.icon === spec.shape.icon为 false,local.shape.icon.unwrap() === spec.shape.icon才是 true。即往里剥一层再比,那层新 wrapper 是唯一允许的新分配。补上之后,同一个变异两个闸门都报红。建议把这条加进修法模板的「派生」一节:派生的闸门 = 键集 + 分歧钉扎 + 引用同一性;缺第三项时,把派生改回忠实手抄是测不出来的。
Generated by Claude Code
os-zhuang commentedon Aug 2, 2026
批次 5 已开 PR:objectui#3199(草稿)—— 台账 62 → 45,包数 17 → 14
@object-ui/auth+@object-ui/components+@object-ui/react三包整包退出台账。18 个符号:12 派生、5 改名、1 进 ALLOW,另有 1 个根本不需要动(见下)。TenancyPosture、DelegableScope(+DelegableAdminScope)、ShareLinkPermission、ShareLinkAudience、OfflineStrategyAuthUser(extends)、ShareLink、SortItem(extends)、OfflineCacheConfig、OfflineSyncConfig、OfflineConfig、NavigationConfigAuthSession→AuthClientSession、AuthProviderConfig→AuthProviderOptions、FilterCondition→FilterBuilderCondition、Field→FieldContainer、ConflictResolutionStrategy→**ConflictResolution(spec 自己的名字)**AuthProviderPerformanceConfig一、
FilterCondition的跨包结论:三处三判,本批实测判定这个名字的跨包指引现在两个方向各错过一次了,所以把三处一次列清:
@objectstack/spec/data$and/$or/$not)@object-ui/app-shellRecord<string, any>@object-ui/typesFilterBuilderCondition(#3068)@object-ui/components(本批){id, field, operator, value},operator取自 builder 自己的FILTER_BUILDER_OPERATORS下拉词汇,不是 spec 的$记号FilterBuilderCondition,与 types 一致照搬 app-shell 会把一行扁平条件换成递归查询类型;照搬 types 这次恰好是对的,但只有对读两边才知道。
二、
OfflineConfig按更正派生并保留原名批次 2 已确认 types 侧那个是 service-worker 路由缓存、改名
PWAOfflineConfig,所以名字归 react。本批直接取 spec 绑定,无方言。取 input 侧而不是
z.infer,而这一次判据就写在函数签名里:useOffline(config: OfflineConfig = {})默认空对象,而 output 侧enabled/strategy/offlineIndicator在.default()跑完后全是必填 ——z.infer会让这个 hook 自带的默认值不可表达。建议把这条写进模板:判断 input/output,先看这个类型有没有一个默认值形态的调用点,比这更好记。@object-ui/react没有 zod 依赖(它渲染普通对象、从不 parse)。与其为了z.input加一个依赖,新增的packages/react/src/spec-input.ts从 schema 自身的_zod内部读 input 类型 —— 仍然是 spec 的绑定,零新依赖、零形状拷贝。三、
ConflictResolutionStrategy:本批最锋利的坑这个名字本身就是 spec 导出 —— 在
@objectstack/spec/api,含义是元数据合并策略(error | priority | first-wins | last-wins)。而 hook 真正用的那个联合(manual | client_wins | server_wins | last_write_wins)在 spec 里叫ConflictResolution,在/ui。在旧名字下做派生会造出"名字指向一个联合、值是另一个联合"的符号 —— 正是本 issue 要消灭的形态,而且守卫会放行(派生就是派生)。正解是一步同时拿回 spec 的名字和它的绑定,与 #3169 的
FieldGroup→ObjectFieldGroup同型。建议补进模板:改名前先查旧名字在 spec 里到底指什么,别只查新名字。四、
PerformanceConfig:spec 在 17.0.0-rc.1 把这个名字退掉了PerformanceConfigSchema/PerformanceConfig已不在@objectstack/spec/ui导出,碰撞自行消失,名字归 objectui。仍然配了 tripwire —— "spec 退掉了"是一句有保质期的断言,spec 哪天加回来,测试报红、符号重新分诊,而不是悄悄变成 fork。但键还在 spec 里(
virtualScroll、debounceMs等嵌套在@objectstack/spec/api下),所以这可能是搬家而非退休。若是搬家,usePerformance应从新位置派生 —— 需要了解 spec 计划的人拍板。五、
AuthUser:索引签名把丢键藏了本地声明从来没有 spec 的
positions/tenantId(授权判定的输入),而[key: string]: unknown让这个缺口在每个调用点和任何结构比较中都不可见 —— 就是 #4075 那个机制:索引签名吸收一切,[Local] extends [Spec]无论漂多远都答"相同"。改成extendsspec 主体后,展示用字段(image/role/roles/emailVerified)是全部增量,spec 新增字段会自己到位。反面案例是
AuthSession:spec 要求 client 永远看不到的id/userId,expiresAt是 ISO 字符串,而本地持有 JSDate、token必填、还有 spec 完全没有的refreshToken。两个方向都不可赋值 —— re-export 会是一次静默的契约调包。所以改名。六、
AuthProvider与AuthProviderConfig判决故意不同组件进 ALLOW,它的配置类型改名。理由不是"组件一律豁免",而是失效模式本身:没人会把
<AuthProvider authUrl=…>当成一个 provider id 的z.ZodEnum,而改名会为了零缺陷破坏 React<XProvider>惯例和所有消费者的 JSX;它的配置类型恰好相反 —— 同一个领域、同样的字眼,spec 的AuthProviderConfig是 OAuth 注册形状{id, clientId, clientSecret, scope?},正是下一个 session 会当成权威去读的那个符号。七、方法学:逐行忠实的 re-fork 能通过全部 parity 断言(变异测试实证)
这一条回应上一轮提出的"派生闸门光有键集比对不够"。把
TenancyPosture改回一份与 spec 完全一致的手写联合、并删掉 import 让它干净编译:pnpm type-check—— 绿types.ts:33)对类型符号而言没有引用同一性可查(不像 #3169 的
isAggregatedViewContainer,expect(local).toBe(spec.x)能抓),所以Assert<Equal<Local, Spec>>对忠实副本和真绑定的判定完全一样。结论应写进模板:parity 测试钉的是"有意分歧的形状",它在原理上无法发现拷贝;发现拷贝的只有守卫。(本批没有 zod schema 派生,所以".unwrap()后比引用"这条这次用不上 —— 全部是类型别名与extends。)变异测试共四向,全部报红:重新 fork(按名+按文件)、撤销改名、把已烧名字留在 DEBT(腐烂棘轮)、删掉 ALLOW 条目。
八、又两处"为了不依赖 X 所以本地拷贝"的注释,都是假的
useNavigationOverlay写着「inline … to avoid importing from@object-ui/types(which may not be a direct dependency of@object-ui/react)」—— 但这套词汇属于@objectstack/spec,而它本来就是该包的直接依赖。与 #3169 在 app-shell 抓到的完全同型。另外useOffline/usePerformance都宣称对齐「@objectstack/spec v2.0.7」,装的是 17.0.0-rc.1。九、编译期钉扎现在真的被编译了
三个 parity 测试都带
Assert<Equal<…>>,而本分支最初把它们当作已生效发了出去 —— 每个包的 tsconfig 都排除**/*.test.ts,CI 唯一的类型闸门跑的就是那些配置。objectui#3187 在本分支在飞期间落地了这个模式的修法,所以本 PR 改用它的约定(每包tsconfig.typetests.json+ 显式 include + 从type-check串接),而不是本分支先写的根级方案。check-type-check-coverage.mjs现在报 4 个包有窄类型断言工程(原为 1)。验证
--ledger清块重生成后逐条 set-diff:removed 恰为本批 17 条(PerformanceConfig已在 main 侧因 spec 退名而消失)、added 为空、其余包逐字节不变;守卫绿;三包先turbo build --filter重建 dist 再turbo type-check --force→ 78/78(含apps/console);全量测试 9511 通过 / 25 skipped / 0 失败(813 文件);ESLint 三包 0 error。待拍板
PerformanceConfigSchema是退休还是搬家? 键仍嵌套存在于@objectstack/spec/api。若是搬家,usePerformance应从新位置派生。packages/。apps/console/src/pages/shared-record-shape.ts自带第三份ShareLink,且audience是裸string—— 任何拼错的 audience 在那里完全不可见。台账是 packages-only 所以本批未动,但这是同一个缺陷挪了一个目录,而 feat(metadata-protocol/objectql): turn-atomic package publish — a commit cannot half-land (ADR-0067 D2, #3066) #3074 已经提醒过apps/消费者容易漏。@objectstack/spec在@object-ui/components从devDependencies移入dependencies—— 它的公开类型面现在引用 spec。若该包有意保持不依赖 spec,请指出(import 是 type-only、JS 侧会擦除,但产出的.d.ts引用它)。剩余
台账 45 条 / 14 个包。批次 2(objectui#3156)PR objectui#3191 未合;批次 6、7 未认领;批次 8 待 objectstack#4171 的残余项解除。
Generated by Claude Code
os-zhuang commentedon Aug 2, 2026
本轮收官:批次 1–5 全部合入,台账 115 → 29
五个批次合并完毕,子 issue 均已关闭。台账数字是从 main 上守卫文件的 DEBT 块实测得出,不是各分支基线相减:
types报表/图表/查询簇app-shell整包core整包types页面/导航/杂项簇auth+components+react三包当前:29 条 / 14 个包 / 9 个 declared dialects。 已整包清空:
app-shell、core、auth、components、react;types从 71 一路降到 3。剩余分布:
data-objectstack5、plugin-chatbot4、plugin-list3、types3、fields/layout/plugin-detail/plugin-grid各 2,collaboration/plugin-charts/plugin-form/providers/runner/sdui-parser各 1。objectui#3162 登记 4 条(
JoinNode/NavigationItem/NavigationItemSchema/JoinedReportBlock),但 main 上types只剩 3 条碰撞——rc.1 之后有一条已不再碰撞。开工前须以--ledger实际输出为准。修出的活缺陷
用户可见的四个:一条已过期的权限集贡献被渲染成生效中(
ExplainLayer.contributors[].state本地未声明);widget 的requiresObject/requiresService能力门控在校验时被静默删除,而渲染器运行时真在执行它(DashboardWidgetSchema只声明了 spec 22 键中的 10 个);服务端已在发的索引差异在校验面板成为无名条目(SchemaDiffEntryKind缺两个成员);AuthUser缺positions/tenantId,而[key: string]: unknown索引签名把这个缺失同时对所有调用点和任何结构比较隐藏了。三份副本自己的注释就是假的:
CONTEXT_TOKENS写着"临时重复,等 spec 导出后删除"(早已导出)、RowHeight自称"spec 的词汇表"、managedBy.ts自称"UI 侧镜像"而装着逐行副本。resolveCrudAffordances光改名不够——那张复制的分桶表会换个名字继续说谎,已改为委托 spec。还挡下一个正要新造的缺陷:
ConflictResolutionStrategy若照原名派生,那个名字在 spec 里是另一个东西(/api的元数据合并策略),会造成名字指向一个联合、值指向另一个。方法论修正(比符号数更值钱)
unknown。 文件头的0 extends (1 & T)探针对unknown返回false,只筛any的分诊会把"删掉整份类型"判成安全派生。已补 case 2b + 反向 pin。NavigationAreaSchema39 条、TenancyPosture14 条断言全绿,只有守卫抓到。zod 侧须补引用同一性断言(.partial()会重新包一层,要.unwrap()一层再比);纯类型符号没有同一性可断言,守卫是唯一防线。turbo type-check全绿不代表改名做完——测试文件不参与类型检查,须另行 grep。any" 不等于"可以安全绑定"(case 2c)。rc.1 给NavigationItem真实类型后,NavigationArea的钉扎理由当场过期,反向 pin 按设计报红,tsc 拒绝断言并指向注释;重新分诊后覆盖依然成立,但依据换成"形状不同 +visible被 spec 每层拒绝"。这是机制第一次证明它能防住"理由过期而结论还挂着"。SchemaValidationResult与SchemaValidationReport双双是 spec 导出。FilterCondition是 ObjectQL AST(应 re-export),components 的才是 FilterBuilder 行(应改名);OfflineConfig该保留原名的是react而非types。照搬跨包结论就是本 issue 要消灭的失效模式本身。并行的代价(流程结论)
三个批次并行导致容器 OOM 全灭、损失约七小时,且 DEBT 块反复冲突。现行标准已改为上一批 PR 真正合入 main 后才派下一批——不是"执行者空闲就派"。冲突的机械解法已验证多次:合并 main → 冲突块清空 →
--ledger整块重生成 → 守卫双向棘轮背书。待拍板
packages/:apps/console/src/pages/shared-record-shape.ts里有第三个ShareLink(audience: string),同样缺陷、台账完全看不见。这是覆盖盲区,建议单独处理。PerformanceConfigSchema是退休还是搬家?键仍嵌在@objectstack/spec/api下。@objectstack/spec在@object-ui/components从 dev 依赖变为正式依赖(公开类型面now引用 spec),若该包本意保持 spec-free 需另想办法。condition)、objectql: 批插入部分成功 API——根治降级重写下 beforeInsert 钩子 at-least-once(#3152 跟进) #3172(节点几何三种拼写)、rest/import: 纯 insert 模式(无 matchFields)的重试幂等——importBatchId 方案(#3149 跟进) #3173(responsive仍是any)、test(showcase): exercise the spec bare-string sort form on a real list view #3174(已修,feat(mcp): validate_expression tool — validate CEL against a schema before authoring (#1928) #3203);objectstack#4525(spec 是否建模DecisionOutputDef.required)。剩余
批次 6(objectui#3160,12 条)、批次 7(objectui#3161,14 条)未开始;批次 8(objectui#3162)等 objectstack#4171。本轮到此暂停。
Generated by Claude Code
os-zhuang commentedon Aug 2, 2026
批次 6/8 已合入 —— objectui#3160 → objectui PR #3220(2026-08-02)。
台账 29 → 17,包数 14 → 11:
@object-ui/data-objectstack、@object-ui/plugin-chatbot、@object-ui/plugin-list三包整包出账。DroppedFieldsEvent、PendingActionRow、PendingActionStatusViewTab(取 schema 的 input 侧 —— 三个.default()键在 output 侧变必填,会让授权侧形状不可表达;同ObjectFieldGroup/OfflineConfig踩过的_input/_output陷阱)CacheStats→MetadataCacheStats、MetadataSaveOptions→MetadataClientSaveOptions、SecurityPolicy→SecurityManagerPolicy、ValidationError→DataApiValidationErrorListView、UserFiltersTool、MessageContent(plugin-chatbot/src/elements/是 vendored 的 Vercel AI Elements + shadcn 原件,与已跳过的components/src/ui/同类)方法学:本批新增两条
1. 「已知情报」要逐符号对读,不能整簇采信。 立单时预判「
SecurityPolicy/DroppedFieldsEvent大概率是手抄件,优先派生」,实测只中一半:DroppedFieldsEvent确是手抄件,SecurityPolicy与 spec 的那个一个键都不重合(spec 是软件包供应链策略,本地是浏览器 CSP + 审计日志 + 字段脱敏),MetadataSaveOptions同理(spec 两份都在描述「把元数据写成文件」,本地是MetadataClient.save()的 HTTP 信封)。两对均Extract = never,已按该式钉住。与批次 5 的结论同源、方向相反(那次是「疑似撞名实测可派生」)。2. 手抄件的真正危害是它注销了一个检查,不是它「少了个键」。 本批三份各用一种方式关掉编译期检查:
DroppedFieldsEvent.reason被拓宽成string,注释自称「for forward-compatibility with reasons added server-side」—— 而AdapterProvider的 toast 真的按readonly_when分支(审批场景下记录可写性的反馈全线失真:能改的显示「已锁定」,改不了的提示「更新成功」 #3794)。spec 加第三个 reason 时唯一会提醒我们的信号,被这行注释亲手删掉;PendingActionRow.status: PendingActionStatus | string—— 与string求并吸收掉全部字面量,标注等于没写;PendingActionRow的[k: string]: unknown—— [P3] objectuiActionDef's[key: string]: anyabsorbs every retired and misspelled action key — removing a spec key produces no compile error #4075 那个机制,索引签名让任何结构比较都答「相同」,只有守卫抓得住,parity 测试原理上抓不到(批次 5 方法学第 7 条)。三种写法都能过 code review、都能过测试。这是本燃尽工作最核心的价值。
3. 路径跳过要配 provenance 测试。 跳过
src/elements/会开一个洞:objectui 自己写的文件藏进该目录就不被扫描。本批一并补上——该目录每个文件必须带 vendor banner,唯一的 objectui barrel 只能是纯 re-export;变异测试第 5 向验证了洞与补丁都成立。同时按批次 5PerformanceConfig的做法配了反向 pin:spec 若退掉Tool/MessageContent,跳过的理由就过期,测试报红要求重读。4. 改名不动线上契约。
DataApiValidationError的运行时name仍是'ValidationError'——normaliseClientError与@object-ui/react的error-message都在嗅探该字符串,那是线上契约,改它是行为变更。renames 全部按标识符边界替换,字面量一个没动。待复核(非阻塞)
ListView/UserFilters走 ALLOW 而非改名,依据是批次 5 的AuthProvider判据(非「组件一律豁免」——同批Field → FieldContainer被改名):两者都是渲染器且在声明处就绑住 spec 派生类型(ListViewProps.schema/UserFiltersProps.config),自身不声明形状,故无可漂移的形状;@object-ui/types早已从另一侧 re-export 为SpecListView做消歧。ALLOW 配了会过期即报红的钉扎。若倾向改名,ObjectListView/UserFilterBar已跑过守卫确认可用。剩余
批次 7/8(objectui#3161,长尾 9 包 14 符号)与 批次 8/8(objectui#3162,被 #4171 阻塞的
any擦除簇 3 符号)。批次 7 即将派发。Generated by Claude Code
os-zhuang commentedon Aug 2, 2026
批次 7 已合并(objectui#3224)—— 台账 17 → 3,11 包 → 1 包
十个包整包出账。剩余全部是批次 8(objectui#3162)的
@object-ui/types三条,逐字节未动:批次 8 仍被 objectstack#4171 阻塞,不派。 它一解除,燃尽即可收口。
验收门:
--ledgerset-diffremoved恰 14 /added空 / 其余包逐字节不变;零新增ALLOW——14 个符号全部真解决,没有一个被寄存成新债(对比批次 6 的ListView/UserFilters走 ALLOW)。CI 15 项 13 绿 2 skipped。本批换来的方法学(五条,前两条是新形态)
一、「同名但前置条件不同」比普通 fork 更危险
plugin-grid的isMultiValueField与 spec 同名同语义,预判是「语义一致就 re-export」——不能 re-export:spec 那个要求type必填,本地那个的调用点是objectFields?.[p.name],例行传undefined。危险在于:两个模块之间调错 import 仍然能编译,只在「字段不存在」这一条路径上改变行为。普通 fork 至少两边都能独立跑对;前置条件不同的同名函数,是一颗只在稀有路径上炸的雷。
→ 判据修正:re-export 的条件是语义一致 且前置条件一致,不是语义一致就够。落法是改名
hasMultiValueShape+ 把分类委托给 spec 的函数(原来它拿MULTI_OPTION_TYPES自己重推规则——一个自称 "consumed instead of restated" 的 helper 最后还是把规则 restate 了)。二、新形态:顶着 spec 的名字,却是一个更宽的类型
spec 的
ColumnSummaryConfig只是ListColumn.summary的对象形态;简写形态'sum'在 spec 里叫ColumnSummary。本地那个顶着ColumnSummaryConfig的名字装了整个并集。于是:同一个名字,一边接受
'sum',另一边拒绝。任何 parity/行为测试都发现不了——两边各自都是对的,只是在说不同的事。→ 之前记录的碰撞形态都是「窄了」或「漂了」;「宽了」是第三种,而且它是唯一一种在测试里表现为「两边都对」的。落法:取
NonNullable<ListColumn['summary']>,由 spec 自己定义它接受几种形态。三、预判错在第三个方向:部分顶着整体的名字
跟踪评论建议「先问 providers 的
Theme是运行时已解析的主题,还是授权 JSON」。实测两个都不是:它是Theme['mode']顶着Theme的名字——一个字符串联合,站在 spec 一整份文档(colors/typography)的位置上。这比一份漂了的副本更误导:下一个 session 读到
@object-ui/providers的Theme,会得出「ObjectStack 的主题是个字符串」的结论。顺带第三次印证批次 3 的规则:显而易见的新名字恰恰最容易已被占用——这个概念在 spec 里就叫
ThemeMode,也是 spec 导出。新名字必须先跑守卫。四、「派生」不等于「派生自正确的东西」——第二个活样本
变异测试的第六向:把
ThemePreference改回叫Theme,但保留派生——守卫放行(派生就是派生),只有 tripwire 测试报红。这是批次 5
ConflictResolutionStrategy那条结论的第二个实例,现在可以升为规则:守卫能看见「有没有派生」,看不见「派生自哪个概念」。 所以每一次改名都必须配一条钉住新名字的测试,不能只靠守卫。本批 10 个 tripwire 文件 / 30 条断言就是这条规则的执行。五、逐字副本的内容如果是线上契约,共享的收益是另一个量级
fields的isFileIdToken与 spec 逐字符相同(/^[A-Za-z0-9_-]{1,64}$/),注释写着 "Mirrors the platform's"。与批次 3 的isAggregatedViewContainer同型——所有行为断言全绿,只有引用同一性能区分(闸门:expect(isFileIdToken).toBe(specIsFileIdToken))。但这一份特别:那个正则是线上契约。服务端哪天放宽它,本地副本仍按老边界回答「这不是一个引用」,widget 于是朝一个期待引用的后端提交 legacy inline blob——症状是缩略图坏掉,离一个正则十万八千里。
裁决:spec 依赖边按 A(接受),批次 5 留下的 待拍板 一并结掉
批次 5 在
@object-ui/components上提出、至今挂在本单的同一问题,连同批次 7 的fields/providers(devDep → dep)、layout/runner/sdui-parser/collaboration(新增 devDep),统一按接受:.d.ts引用了 spec 却把 spec 放 devDependencies,消费方拿到解析不了的类型——这不是洁癖问题,是坏掉的包;@object-ui/types转一道(选项 C)保住绑定但让来源多一跳、更难读——与「让 AI 写元数据不容易出错」相反。若某个包是按策略刻意 spec-free,在本单指出是哪个,我改派回滚那一个。 package.json 级改动,回退成本接近零。
燃尽进度
@object-ui/types× 3本批捎带立单:objectui#3221(
[key: string]: any,已入队,是 #3222 的硬前置)、objectui#3222(协议面,已标needs-user-decision)、objectui#3223(死渲染器,已入队按 ADR-0049 派删除)。Generated by Claude Code
os-zhuang commentedon Aug 5, 2026
分诊:补
repo:objectui路由标签(仅分类半边,不动任何认领状态)。@object-ui/*包台账、tripwire 测试),按 rule 1 打repo:*不打domain:*。@object-ui/types三条),批次 8(objectui#3162)明确阻塞于 objectstack#4171,认领在案且有持续产出 —— 不属 stale-claim,不回收、不改派。补标签只为把本单纳入路由索引,消除「有 assignee 但无 pm/domain 标签」的扫描悬置(上轮收尾简报的遗留核验项)。apps/、PerformanceConfigSchema退休 vs 搬家、spec 依赖边已按 A 接受)——收口前值得让维护者过目一次。本评论来自分诊座位 Routine(#5474 试点),不构成认领。
Generated by Claude Code
os-zhuang commentedon Aug 6, 2026
存量裁决轮(维护者 2026-08-06 委托,session_01LeEfA7CFwbJb7JJmXm2KM3):补落
tracking标签——CI 规则本体已落地(objectui#3032),批次 1–7 已合并、台账 115→3,仅剩批次 8(objectui#3162)等 objectui 侧开工信号(上游 #4171 已关)。在飞多批次程序卡,认领活跃非 stale,不是单次派发单。打标使其退出分诊扫描范围。Generated by Claude Code
os-zhuang commentedon Aug 10, 2026
Migrated to objectstack-ai/objectui#4043 under #7167 (file-at-destination ruling, maintainer 2026-08-10). Native GitHub issue transfer is not available to this session's credential, so the card was recreated at the destination; this thread stays as the authoritative history and is linked from the new card. Closing as not planned here — moved, not rejected.
Generated by Claude Code
claude commentedon Aug 30, 2026
Ledger closed — batch 8/8 landed with zero re-exports
Final batch (objectui#3162) is done, via objectui PR objectstack-ai/objectui#6883. Leaving
the summary here as the record; this issue stays closed.
The last four symbols are all NOT burnable, and three of them for reasons that are not
the one the ledger recorded. The batch was filed as "blocked on objectstack#4171 — burn down
when the upstream
anyerasure lifts". #4171 closed completed 2026-07-30, which was thedesigned start signal. Re-measuring each symbol in the built dist at
@objectstack/spec17.2.0 — not in source, and not from the fact that #4171 closed, because dist erasure is
invisible from either:
JoinNodeTS2305from both/uiand/data, against a control import that resolves. Spec 17.0.0 retired thequery.joinscluster. Nothing to bind to.NavigationItemNavigationItemSchemaJoinedReportBlockunknownz.ZodTypeAny. #4171 typed the recursive schemas and never touched this one.The finding worth keeping
NavigationItemSchemais the shape this whole ledger was created to catch, and it is themost dangerous form of it: its upstream became precise, which reads as "safe to bind
now". It is not. That schema has a published consumer in
objectui validate, andreferencing the spec's would make it reject navigation metadata the renderer accepts today —
pinned,defaultOpenand a separator carryinglabelfailunrecognized_keysagainst thespec's strict branches,
visible: booleanfailsinvalid_union, and a one-characteridfails
too_small. All measured, behind positive controls.Generalising: "no longer
any" was never the right admission question — objectui#3177established that, and batch 8 confirms it at the runtime layer too. A type-level erasure
probe answers whether the upstream type is EMPTY, never whether binding it is SAFE.
Two comments were found asserting an upstream type "erases to
any" when it no longer does— one for
NavigationItemSchema, one forFormField. That is this ledger's own failure modeturned inside out: a spent justification is a planted premise, and the next triage that
checks it finds it false and burns down something it should not. Both corrected.
Ledger state
The enforceable half lived in objectui's
check-spec-symbol-derivation.mjsas aDEBTmapkeyed to this issue. It is now empty — the three remaining symbols moved to that guard's
ALLOWregistry as declared dialects, each carrying its measured reason and naming its ownrelease condition as a pin:
So the ledger's remaining debt is no longer a card pointing at an upstream issue that has
since closed without settling it. Every symbol that is still a local dialect now says why, in
a place that fails when the reason stops being true.
Generated by Claude Code
fix(spec): os migrate meta guidance for the ui-* and plugin-* migrati…