feat(spec): ADR-0122 第一期 —— 解析态统一命名 XParsed(纯增量)+ 防回流门 - #6072
Conversation
`packages/spec` named a schema's two types (`z.input` = author state, `z.infer` = parsed state) two different ways, with nothing recorded about which was which. Measured on origin/main: 1384 bare aliases mean the parsed state, 86 mean the author state, and three first-hand sources each called the 8-file minority "the house convention". No ADR recorded either. ADR-0122 settles it — bare name = author state, `XParsed` = parsed state — and lands additively. Phase 1 declares `XParsed` for the 660 aliases whose schema genuinely has two shapes, so the major-window flip has a migration target everywhere it changes meaning. Nothing renamed, nothing removed, every `XInput` left in place. - ADR-0122, with both measurement rounds as its appendix - 660 `XParsed` aliases across 152 `*.zod.ts` files - 717 isomorphic schemas pinned with compile-time assertions, so the D3 exemption cannot rot silently when one later gains a `.default()` - `check:spec-parsed-alias` backflow gate (lint.yml), reading the pin file as its exemption registry - SYNC_ARCHITECTURE.md: correct the sentence that called the minority spelling the house convention Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011M7UwH25Unfi73UHim7ajY
…se 1 - api-surface.json: +637 export names, 0 removed. 660 aliases were declared; 23 live in modules no public entry point re-exports. - json-schema/ and authorable-surface.json verified unchanged — `XParsed` is a pure type alias, so `gen:schema` / `gen:openapi` produce no diff at all. - the pin file's count case now reads the pin count out of the source instead of asserting a literal against itself. tsc proves each pin is TRUE; nothing proved they were still THERE, so a red pin could be made green by deleting it. Counting them makes that edit fail `pnpm test` too. - ADR appendix: 151 files edited (not 152 — `automation/execution.zod.ts`'s three differing aliases already had their `XParsed`), plus the two findings the gate produced on its first real run. 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
|
…rsed-convention-adr-phase1
📓 Docs Drift CheckThis PR changes 1 package(s): 112 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
CI 收敛记录(
|
| job | conclusion | 备注 |
|---|---|---|
| TypeScript Type Check | ✅ success | 本改动风险最高的 job —— spec 全部八个生成物门都在这里跑 |
| ESLint | ⏳ queued,未分配 runner | 首轮 17:33 排队 21 分钟后被 cancelled(零 step 执行、runner: (none assigned));rerun_failed_jobs 重试后 18:12 起再排队 70+ 分钟,仍未分配 |
这是全仓 runner 饥饿,不是本 PR
最近 30 个 workflow run 里有 19–22 个处于 queued/in_progress,横跨多个分支,连 merge queue 自己的 run(gh-readonly-queue/main/pr-6031、pr-6034)也在排队。同一时段 Console Pin Freshness / Duplicate Fix Guard / Validate Dependencies 三个 workflow 的 run 结论是 failure,但逐个查 job 全部是 cancelled(零 step),同一个饥饿现象。
替代证据:把 ESLint job 的命令逐条在合并后的树上跑了一遍
不是「本地测试绿」,是把该 job YAML 里的 27 条命令原样抽出来逐条执行,含全仓 pnpm lint 本身:
green: pnpm install --frozen-lockfile green: pnpm check:route-envelope
green: pnpm lint green: pnpm check:error-code-casing
green: pnpm check:slot-lookup green: pnpm check:wildcard-fallthrough
green: pnpm check:query-options-erasure green: pnpm check:init-service-contract
green: pnpm check:nul-bytes green: pnpm check:durability-log-level
green: pnpm check:doc-authoring green: pnpm check:startup-registry-verdict
green: pnpm check:docs-audit-scope green: pnpm check:objectui-changeset
green: pnpm check:role-word green: pnpm check:release-notes
green: pnpm check:adr-anchors green: pnpm check:release-body
green: pnpm check:org-identifier green: pnpm check:node-version
green: pnpm check:authz-resolver green: pnpm check:workflow-status-functions
green: pnpm check:service-providers green: pnpm check:published-files
green: pnpm check:engine-double-contract green: pnpm check:resume-authority-declared
green: pnpm check:spec-parsed-alias
LINTJOB_ALL_EXIT=0
其中 pnpm check:spec-parsed-alias 是本 PR 新增的门,也在这个 job 里 —— 所以它是否误报,这一轮已经用真语料验过(全仓 0 误报)。
合并前还需确认
- ESLint job 拿到 runner 并出 conclusion(这是唯一没拿到的那块)。
Check Changeset同轮被 cancel,需重跑;本 PR 带 changeset(.changeset/spec-type-alias-parsed-convention.md,@objectstack/spec: minor),因此不需要skip-changeset标签。- 落地次序按 PM 定的 refactor(spec)!: 按 ADR-0049 摘除
IStorageService.list(prefix)—— 零消费方、双适配器语义分叉 (#5540) #5983 → spec 生成物按 category 分片:拆掉三个单体 ratchet 文件的合并队列串行税(维护者 2026-08-06 已拍板) #5837 → 本单;refactor(spec)!: 按 ADR-0049 摘除IStorageService.list(prefix)—— 零消费方、双适配器语义分叉 (#5540) #5983 已在main(本分支已 merge 并重新校验:check:generated十个门全绿)。若 spec 生成物按 category 分片:拆掉三个单体 ratchet 文件的合并队列串行税(维护者 2026-08-06 已拍板) #5837 分片先落,本 PR 需按分片后的生成流程重跑一次gen:api-surface。
Generated by Claude Code
Generated by Claude Code
Refs #5551
按 2026-08-06 维护者批复(裁 C,分期 C2)落地第一期。原单叙事(「翻 connector 单文件」)已被前任 dev 证伪,本 PR 不执行它。
一、先说结论:判据是「形状差」,不是「有没有 XInput」
裁决把第一期铺设判据交给 dev 定并论证。我重跑了全仓测量,结论与派发单沿用的 03:24Z 摘要不同,且差异是决定性的:
派发单里的「B 族 ~55 文件 / ~108 别名」是**「已经写了
XInput的那批」**。它不是需要迁移目标的那批 —— 第二期翻裸名会改变含义的别名共 663 个,其中只有 91 个今天带XInput,572 个裸名连作者态的名字都没有。按「有 XInput」铺,会漏掉 572 个 —— 恰恰是第一期要防的那种静默破坏。所以 ADR-0122 D5 写下的判据是:
三轴论证(写进 ADR):
XParsed是永久同义词。全仓同构的有 721 个 —— 按「一律铺」会给公开面凭空加 721 个「选哪个都行、于是必然有人选错」的名字。创业期收紧口径,名字要靠用途挣位置。XParsed只在「解析态确实是另一个类型」时存在,名字才不撒谎。二、改了什么(纯增量)
XParsed别名*.zod.ts文件XInput一个没动(qa downstream-contract 的 FROZEN fixtures 按文件头明文原样不迁移);A 族 8 文件未触碰。三、同构豁免是钉住的,不是写在注释里的
D3 说同构者不给第二个名字。但「同构」不是谁声明的,是 schema 树的事实,而且会烂 —— 哪天某个嵌套字段加了
.default(),这个别名就悄悄进了形状差集合,第二期翻它时消费方没有迁移目标,正是第一期要防的那件事。所以 717 个豁免全部落在
packages/spec/src/type-alias-convention.pin.test.ts,每条都是一句编译期断言。tsc 在给包做类型检查的同一趟里证明它们为真,某条不再成立的当天这个文件就红,并直接点名是哪个别名。修法只有一种:给裸名补XParsed,然后删掉那行 pin。四、防回流门:落在
scripts/,不在packages/lint派发单写的是「
packages/lint/**新增一条规则」。这条做不到,是架构事实不是偏好:@objectstack/lint的模块头写着它校验的是内存里、已 schema-parse 的元数据图,"no I/O, no runtime, no filesystem",每条规则都是纯(stack) => Finding[]。一条要读我们自己.zod.ts源码的规则放进去,会是该包里唯一打开文件的规则,直接破坏它的契约。所以它落在本仓源码形态门那一族(
check:error-code-casing/check:route-envelope/check:engine-double-contract的邻居),挂进 lint.yml 的lintjob:pnpm check:spec-parsed-alias—— 每个裸z.infer别名必须要么配XParsed,要么在 pin 文件里被钉为同构;顺带报告「已经没人依赖」的过期 pin。--self-test(11 条正反断言),照本族惯例先自检再扫。由此不涉及
packages/lint,与 devx 车道在飞的 #4251 零文件相交(注册表/barrel 都没碰)。门与 pin 的实测信噪比
1384 bare z.infer aliases, 717 pinned isomorphic, 667 paired with an XParsed. OK(667 + 717 = 1384,门每次运行自己算这道账)。ServiceObject(ObjectSchemaBase未导出,探针够不到 —— 单独验证确有形状差,已补ServiceObjectParsed)+automation/execution.zod.ts4 条本就配好XParsed、因而多余的 pin。五、反向验证(方向事先定,两处都预期变红)
1. 删掉一个新铺的
XParsed→ 门必须红并点名该别名。 删ConnectorParsed:还原后回到
... 667 paired with an XParsed. OK。2. 给一个被钉为同构的 schema 加
.default()→ pin 必须红。 给SyncStrategySchema(pin 文件第 781 行 =Iso376)加.default('full'):红的正是事先点名的那一行,不是别的行。还原后
check:test-typecheck恢复绿。六、SYNC_ARCHITECTURE.md 误导句
原文说 connector「还没搬到那条 house convention 上」,把 8 文件的少数派说成全仓惯例 —— 与实测相反。已改写为:两种拼法都无记录、ADR-0122 才是那条记录、第一期已给
Connector备好ConnectorParsed、第二期在 major 翻裸名。七、生成物
XParsed是纯类型别名,所以只有api-surface.json动:+637 导出名,0 删除。声明了 660 个,其中 23 个所在模块没有被任何公开入口再导出,故不上公开面 —— 这个差额是核对过的,不是漏生成。json-schema/、authorable-surface.json零变化:gen:schema+gen:openapi跑完git status只有我自己手改的两个文件。这是验证结果,不是预期。check:generated十个门:9 绿,唯一 stale 的api-surface.json已按它给的命令重生成(未整套重跑)。八、必答:本改动对 #5837(生成物分片)的影响
变简单一点点,主要是无影响。 本 PR 不碰
packages/spec/scripts/**(#5837 的源文件面),两边源文件零相交;唯一相交是api-surface.json这个生成物本身,而这正是分片要解决的问题 —— 本 PR 给它加 637 个导出名,是分片收益的一个具体例证(单文件更大),但既不改变它的生成方式,也不增加分片的实现难度,更没让分片变得不必要。落地次序按 PM 定的 #5983 → #5837 → 本单,我在末次同步圈按分片后的生成流程整体重生成。九、验证
pnpm --filter @objectstack/spec typecheckTYPECHECK_EXIT=0pnpm --filter @objectstack/spec testTest Files 326 passed (326)/Tests 8311 passed (8311)pnpm --filter @objectstack/lint testTest Files 61 passed (61)/Tests 1449 passed | 4 skippedpnpm check:spec-parsed-alias(含--self-test)pnpm check:adr-anchorspnpm check:nul-bytesnpx eslint(改动文件)ESLINT_EXIT=0Generated by Claude Code