feat(spec): 关闭 chart 最后两个 STRIP 站点,并把 44 个直调 alias 表迁到 strictObject (#5583, #5593) - #6595
Merged
Merged
Conversation
…ect alias tables to strictObject (#5583, #5593) Two issues from the #4001 unknown-key campaign, landed together because they share one four-step regeneration and one CI lap. ── #5583 · ChartAggregateSchema / ChartGroupBySchema ──────────────────────── Both object shapes are `strictObject` now, so an undeclared key inside `aggregate={{…}}` is a named rejection carrying the surface, the offending key and a rename, instead of being silently stripped by the parse. `groupby`, `fn` and `dateGranularty` each degraded a chart with `build`/`validate` green. The order is the record worth keeping: `.strict()` is a property of a PARSE, and until #5020 wired one nothing parsed these schemas. Closing them first would have shipped a precisely-validated door with nothing behind it (#4583). The zod-4 union collapse is load-bearing here and pinned on both sides: `groupBy` is a union, so the arm's `unrecognized_keys` never reaches `error.issues` — the named message reaches an author only through `packages/lint`'s `describeIssue` arm unpacking. The product question this pair carried is ANSWERED and it did not move the schema: `groupBy` stays REQUIRED. Measured — the example corpus authors one `ObjectChart aggregate` and it carries `groupBy`; the ungrouped single-value need is served by objectui's separate `object-metric` block (7 instances in the showcase); and the renderer's three `schema.aggregate?.groupBy || schema.xAxisKey` reads are optional-chained on `aggregate` itself, so they serve charts with no aggregate at all. #5020's warning-level tolerance stays a tolerance; its hint now states the ruling rather than an open question. Records synchronised in three places: the file header, the two `chart.test.ts` pins (INVERTED in place, not deleted), and the ledger — whose `chart.zod.ts` remaining-strip row is deleted on the reverse pin (0 strip) with a departure note. Counts regenerated: strip 185 → 183, authorable 43 → 41. ── #5593 · 44 direct `strictUnknownKeyError` call sites → strictObject ────── All 44 migrated; 16 hand-transcribed `*_KEYS` arrays and 9 drift-probe tests deleted; the shrink-only ratchet is a hard ZERO and now forbids the weaker instrument rather than measuring it; #5483's transitional registry (`shared/alias-table-registry.ts`) is deleted with its last call site, along with the suppression hook `strictObject` needed to stay out of it. The migration found what the transcriptions were hiding — the half route 1 could not close: * 11 suggestion targets were `retiredKey()` tombstones (`app` ×8, `flow` ×2, `flow node` ×1), so a near-miss was steered onto the one key guaranteed to be rejected next — ledger finding 12, live on main; * the nav `separator` answered with keys it rejects: it spreads nothing and declares `type`/`id`/`order`, while the shared transcription handed every variant the base keys, so `title` was answered "did you mean `label`?" and `label` was rejected too — finding 7; * three ADR-0010 protection envelopes were missing from their own suggestion pools (`datasource`, `hook`, `sharing rule`). Zero-behaviour-regression is proved exhaustively rather than sampled: every migrated surface's old array is compared against its new derived pool (36 of 42 byte-identical; the 6 differences are exactly the defects above), plus a real rejection message per surface and per nav variant. `data/object.zod.ts`'s error map was deferred to dodge a temporal dead zone; `strictObject` evaluates options at construction, so the deferral is replaced by declaration order and that order is now documented as load-bearing. Exemption disposition, with one correction to the migration's own forecast: `VARIANT_LEGAL_GUIDANCE` is DELETED by a fix (the `children` prescription is filed only on the seven variants that can fire it), while `PROSE_ALIAS_TARGETS` MOVED into the shape-backed half rather than dying — migrating the tables did not turn seven deliberate prose targets into key names, so claim 2 met the same 93 entries from the shape side. It is stronger where it now sits. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011M7UwH25Unfi73UHim7ajY
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
…counts on the merged tree The header's first paragraph still said two sites were deliberately left open, which #5583 closed. It now records the ORDER instead (批 15 refused to close first, #5020 wired the parse, #5583 moved the posture), because a later sweep meeting a `no gate` verdict needs the refusal's reasoning, not just the result. The counts artifact carries merge=os-regen and was merged without a text merge, so it is recomputed from the merged tree rather than reconciled by hand — main's etl.zod.ts retirement (#6414) landed in the same window, so the totals move for two independent reasons and only a wholesale regeneration gets both right: 444 -> 434 sites, 183 -> 180 strip. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011M7UwH25Unfi73UHim7ajY
Contributor
📓 Docs Drift CheckThis PR changes 3 package(s): 112 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
…strict-object import cycle under eager schemas DECLARATIONS moved from a module-level const (TDZ at import time when the cycle enters through the automation barrel — an ordering #5593's migration itself introduced) to a hoisted function-owned store, the same property flowNodeObject() relies on (#4415). Pinned from both sides; the eager reproduction path is gen:schema (OS_EAGER_SCHEMAS=1). Recovery commit: the dev agent was killed by a container restart during its final verification run; verification re-run to completion by the PM — gen:schema eager pass, targeted cycle tests 36/36, full spec suite 342 files / 8768 tests green.
qq9340100
marked this pull request as ready for review
August 8, 2026 09:30
This was referenced Aug 8, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #5583
Fixes #5593
严格性族 sweep 一棒两单:#5583 把
chart.zod.ts最后两个 STRIP 站点转strictObject,#5593 把 44 个strictUnknownKeyError直调点全部迁到strictObject、棘轮 44 → 0、并删掉 #5483 的过渡登记表。两单同属 #4001 战役,落在一起是因为它们共用同一套四步重生成与 CI 圈。单 1 · #5583 — ChartAggregateSchema / ChartGroupBySchema 转 strictObject
前提复核(origin/main 6f657f4)
正文的行号已漂移(
:716/:691现为:764/:739),但前提成立:两处仍是 STRIP 姿态,chart.test.ts的两条「still STRIPS — deliberate」pin 仍在,#5020 的 lint 侧 parse 已落地(validate-react-page-props.ts调ChartAggregateSchema.safeParse(),CHART_FUNCTIONS与手写 count/field 孪生体已删)。分诊提示 ① 要求以合并后代码为准复核「过渡期缺口」——实测缺口描述准确:解包逻辑在zod-issue-format.ts的describeIssue,warning 级容忍在checkChartAggregate的groupByAbsent分支。实施
两个 object 分支转
strictObject,按战役标准件配surface/history/aliases/guidance。curation 只收有据的近义词:fn(本文件头部自己点名的被吞键)/agg/aggregation→function;measure→field;ADR-0021 dataset 路径的dimension/category→groupBy;granularity/bucket→dateGranularity;as/label→alias。groupby、dateGranularty、functoin不入表——折叠编辑距离已覆盖,重复拼写是死条目(别名表内部撞车:aliasProbe归一化后两个 key 相同,后者静默覆盖前者(snap.grid被指向showGrid) #5481)。错层键给 prescription 而不是改名:
dateGranularity写在groupBy旁边(本文件头部点名的最贵一个)、alias/filter/objectName/measures各自指回真正的归属面。zod 4 联合分支消息塌缩(分诊提示 ③)实测通过。
ChartGroupBySchema是 union,strict 分支的unrecognized_keys不会进error.issues——顶层塌成一条invalid_union,自身 message 就是裸的Invalid input。具名消息端到端到达作者,靠的是packages/lint/src/zod-issue-format.ts的 arm 解包。样本实测(aggregate.groupBy = { field: 'created_at', dateGranularty: 'month' }):两侧都钉住了:
chart.test.ts钉原始 issue 形状(code === 'invalid_union'、message === 'Invalid input'、arm 里能取到具名消息),validate-react-page-props.test.ts钉端到端(删掉解包即红,而 spec 自己的测试仍绿)。三处记录同步:
chart.zod.ts头部两块散文改写(两步走的顺序作为可复用结论保留)、chart.test.ts两条 pin 断言方向反转(改写而非删除,前后两态都留给下一个读者)、台账ui/triage 行翻位 + remaining-strip 行删除(reverse pin:0 strip 还留着行就是红)+ 一段离场记录。.counts.md走gen:strictness-ledger整体重算:strip 185 → 183,authorable 桶 43 → 41。内嵌产品问题的判定(否决窗口)
判定:属债 —— 无分组单值图不是受支持的 ObjectChart 形状,
groupBy保持必填。 按 #5068「渲染器兑现的键先声明后设闸」条款,先测「渲染器是否真的兑现」,实测三条:ObjectChart aggregate=只有 1 处(renewals-pipeline.page.ts:138),且带groupBy;文档validating-metadata.mdx:205同样带。无分组用例 0 个。object-metric/ObjectMetricWidget,showcase 里 7 处aggregate: { field: 'id', function: 'count' }、无groupBy。ObjectChart.tsx的三处schema.aggregate?.groupBy || schema.xAxisKey(:340 / :497 / :640)都对aggregate本身做了可选链——它们服务的是根本没有 aggregate 的图(data=/dataset=绑定),让选项配色、compareTo 合并、drill-down 过滤在那条路径上不崩;没有一处让「无分组 aggregate」画得出来。真正做客户端聚合的aggregateRecords反而把groupBy: string声明为必填,传undefined时按String(undefined)分桶,产出一行键名字面为undefined的数据。三轴结论:业务需求——需求真实但已由
object-metric兑现,schema 放宽没有实测拉力;长期健康——放宽等于声明一个渲染器交付不了的形状(PD#10 / ADR-0049),正是本文件头部引 #4583 要避免的;让 AI 写的元数据难写错——groupBy可选会让「忘了写类目轴」成为合法声明,构建全绿、图上一个点。因此 #5020 的 warning 保持 warning,但语义从「未决」改为「未升级的闸门」:hint 现在说明裁定结果(不是支持形状,单值请用 metric block),pin 测试同步。没有在本 PR 升 error——那是独立的验收面(要过所有消费者的页面,不只是示例语料),按 #4949 纪律另立。若维护者认为应当同轮升 error,这是否决窗口。
逆向验证(方向先判后跑)
方向预判:红。 反转后的 pin 断言的是「拒绝」,把两个 object 分支撤回 STRIP,
chart.test.ts的两条与validate-react-page-props.test.ts的两条应同时转红。⏳ 真实读数在容器共享验证锁排队中,落地后回填本节(本 PR 正文会更新)。未跑完之前本节不写读数——按仓内纪律,模板形状的假证据比空白更糟。
单 2 · #5593 — 44 个直调点迁 strictObject,棘轮 44 → 0
前提复核
基线以 PR #5609 合并后的 main 为准,逐条核实:
shared/alias-table-registry.ts存在、alias-integrity.test.ts的toBeLessThanOrEqual(44)棘轮在、三处显式豁免在(PROSE_ALIAS_TARGETS实际是七条不是六条——#5555 的修法把第七条加了进去,正文的「六条」已过期)。AST 复数:44 个直调点、14 个文件,与正文分布表一致。实施
strictObject(options, shape);16 个手抄*_KEYS数组连同 9 个漂移探针测试删除。options保持内联对象字面量,否则闸门的 AST 覆盖扫描读不到该站点)。getFullStart()删语句,把语句前面的模块散文一起吞了(mongo.zod.ts的驱动 docblock 因此从生成文档里消失,是check:docs的 diff 抓到的)。改成「只吃紧邻上一行、属于该语句自己的注释块」,并把那块注释搬到迁移后的调用点——很多文件里它是 guidance 条目唯一的解释,个别文件里它其实是 schema 的 docblock(停在 const 上是因为 const 挡在中间)。重跑后生成物零漂移。data/object.zod.ts的 TDZ:objectUnknownKeyErrorImpl ??=的延迟构建是为了绕开UNKNOWN_KEY_GUIDANCE声明在 shape 之下。strictObject在构造时求值 options(这正是alias-integrity.test.ts能拿到真.shape判的原因),延迟必须去掉,改用声明顺序:UNKNOWN_KEY_GUIDANCE整块上移到ObjectSchemaBase之上,并在原地写明这个顺序是承重的(挪回去会在OS_EAGER_SCHEMAS=1下模块初始化即崩,而测试因为懒导入照样绿)。ui/app.zod.ts九变体工厂:navItemUnknownKeyError(variant)改为navItemSurface(variant): StrictObjectOptions,九个分支各自strictObject(navItemSurface('…'), { … })。九张表的覆盖判据(运行时计数 = 9)从直调段搬进 shape 判据段。散文式 target 是刻意形状,原样保留。data/hook*.zod.ts已无在飞冲突,直接迁。迁移当场抓出的三类真缺陷(这正是路线 1 消不掉的那一半)
app8 个(apis/aria/embed/homePageId/mobileNavigation/objects/sharing/version)、flow2 个(active/template)、flow node1 个(outputSchema)都是retiredKey(),手抄数组照列不误,于是近似拼写被指向「下一步保证再被拒一次」的那个键——账本 finding 12,三个文件,main上活着。strictObject按acceptsNothing把它们排除出候选池,作者改为直接拿到墓碑自己的升级处方。separator在用它自己拒绝的键回答作者。 九变体共用一份[...BASE_NAV_ITEM_KEYS, ...]转写,但SeparatorNavItemSchema不 spread 基础形状,只声明type/id/order。在 separator 上写title会被答「did you meanlabel?」,而label同样被拒——finding 7,出自专为终结它而建的战役。现在 separator 只保留 target 确实存在的别名(name→id、sort/sortorder/position→order),其余九个基础键转成一句 prescription(分隔线不带标签/图标/徽标/门禁,要带标题请用group)。datasource/hook/sharing rule):shape 里 spread 了MetadataProtectionFields,手抄数组没抄,于是这些键的拼写错误一条建议也拿不到。迁移后从 shape 派生,自动补齐。三处豁免的实际去向(对作者预告的一处更正)
作者在 #5593 评论区预告:迁完导航族后三处豁免「同时失去依据转红,应当删除而不是改写」。实测一半对、一半需要更正,如实记录:
VARIANT_LEGAL_GUIDANCE(2 条)children的 prescription 现在只落在能触发的七个变体上(object/group由NavigationItemSchema的.extend()接收children,另给extraKeys)。转写做不到这个区分,shape 做得到——与 #5555 的「降级而非容忍」同一手法。PROSE_ALIAS_TARGETS(7 条)target in shape而非knownKeys.includes(target)),陈旧检查一并带走。这是 #5593 唯一没能删掉的豁免,已在闸门里写明原委。棘轮与登记表
toBeLessThanOrEqual(44)→ 硬零,断言含义随数字一起变了:不再度量「多少还跑在弱仪器上」,而是禁止弱仪器。strictUnknownKeyError仍是已发布 API(api-surface.json的./shared),这条只约束本包。shared/alias-table-registry.ts随最后一个调用点删除;strict-object.ts里为躲开它而存在的withoutDirectAliasTableRegistration抑制钩子、suggestions.zod.ts里的registerDirectAliasTable(options)一并删除。data/object.zod.ts的表确实注册(延迟已去掉,但断言留着,声明顺序被挪回去时它会点名是哪个 schema)。零行为回归验收
比逐条抽样更强的做法:穷举比对。作者体验唯一可能改变的,是「did you mean」的候选池——旧的是手抄数组,新的是
Object.keys(shape).filter(!acceptsNothing) + extraKeys。脚本从git show HEAD:里 AST 解析出全部旧数组,与运行时新池逐面比对:(42 面 = 44 个调用点减两个脚本读不出旧数组的:导航族工厂的
surface是模板串,data/object.zod.ts的knownKeys本来就写的是Object.keys(ObjectSchemaBase.shape)——那一个从来没有过转写,也就没有可比对的旧数组。两者都另有覆盖:导航九变体逐一走消息样本,object 面由闸门的注册断言看住。)36 面逐字节相同;6 面的差异全部是上面那三类缺陷的修正,没有一条是回归(lost 的 11 个全是
retiredKey()墓碑,gained 的全是 shape 里真有、数组漏抄的 ADR-0010 信封键)。另外对 42 个面各取一条真实拒绝消息(未知键 + 别名键各一),三要素——surface 名、点名的键、改名建议——逐条在案;ui/app.zod.ts九个变体逐一过(含dashboardname在非 dashboard 变体上答散文 target、children只在七个变体上出 prescription)。sharing.zod.ts是唯一需要额外动作的一面:它的表挂在 module-private 的BaseSharingRuleSchema上,而唯一被 parse 的是.extend()出来的CriteriaSharingRuleSchema,condition/type声明在扩展上。旧转写把两边的键混在一个数组里遮住了这个边界;现在用extraKeys: ['type', 'condition']明写,闸门的 claim 2 相应承认extraKeys(这是strictObject为该场景提供的字段,suggester 本来就把它当候选读)。逆向验证
方向预判:红。 棘轮已是硬零,任选 3 个迁移点(
identity/position.zod.ts/security/rls.zod.ts/ui/bulk-action.zod.ts)回贴HEAD~1的手抄数组版本,NOTHING in packages/spec calls strictUnknownKeyError directly any more应报出这三个文件。⏳ 同上,读数排队中,落地后回填。
验证读数
pnpm --filter @objectstack/spec testconvert-source-map在格式化某条 stack 时抛SyntaxError),与本改动无关联证据,正在定位是否为既有现象——见下pnpm --filter @objectstack/lint testpnpm --filter @objectstack/spec typecheckcheck:scripts-typecheck/check:test-typecheck)check:generatedcheck:strictness-ledgercheck:spec-parsed-aliascheck:adr-0087-registrationpnpm lintnode scripts/check-nul-bytes.mjsscripts/strictness-ledger.test.ts的一条 fixture 需要跟着改:它拿security/permission.zod.ts当「更老的z.object(…).strict()拼写」的样本,而那四个站点正好被本单迁走了。改指向data/object.zod.ts的TenancyConfigSchema——那两张手写$ZodErrorMap(strictCapabilitiesError/strictTenancyError)按 #6416 的记录刻意不在 #5593 射程内,会继续携带该拼写;permission 的四条留作控制组,确保这条测试仍是在说读取器而不是在说某一个文件。check:i18n-coverage未跑:它要求先pnpm build出 CLI,本改动没有增删任何带 label 的可授权键(check:authorable-surface绿、authorable-surface/生成物零 diff),不在其半径内。Generated by Claude Code