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
…rsed-convention-adr-phase1 # Conflicts: # .github/workflows/lint.yml # packages/spec/api-surface.json # packages/spec/src/shared/mapping.zod.ts
生成物:三个单体产物已被分片目录取代,api-surface 重生成进 `api-surface/` 13 个分片,+635 导出名 / 0 删除。json-schema.manifest/ 与 authorable-surface/ 零变化(`XParsed` 是纯类型别名)。 别名面按 ADR-0122 D5 对合并后的树重测,#5552 让两条同时移动 —— 这正是 D6 两个方向各走了一次,都没有靠人发现: - `FieldMappingTransform` 整个联合被退役,其裸名与本 PR 曾加的 `FieldMappingTransformParsed` 一并消失; - `FieldMapping` 的两形状差**只**来自 `transform` 一个键。键被墓碑化后 `z.input` 与 `z.infer` 同构,按 D5 不再属于覆盖集、按 D3 不得保留第二个 名字 —— 故删掉 `FieldMappingParsed`、改为钉进 pin 文件。方向不是猜的: 探针带一条故意失败的 control 断言,确认不是空转后才判定同构。 读数 657 别名 / 149 文件;1383 裸名 = 665 配对 + 718 pin(门每次自算)。 ADR 附录 F 与 changeset 已按交付态订正,并记下这次双向 graduation。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011M7UwH25Unfi73UHim7ajY
#6072 的防回流门 check:spec-parsed-alias 已落 main,本分支建于其前,新增导出需补齐时序。 `ConnectorActionEffectSchema` 是一个裸 z.enum —— 无 default、无 transform,z.input 与 z.infer 同构 —— 按 D5 判据不配 `XParsed`(那会给作者一个只能选错的同义名),按 D6 改为在 type-alias-convention.pin.test.ts 立编译期断言。与其上方两行的 ConnectorType / ConnectorStatus 是同一类,故并入 integration/connector.zod.ts 分块;编号接文件末尾续号 (IsoNNN 只是唯一名,门读的是 z.input 出现处),避免为补一个空位重排其后 300 余行。 同时把 pin 计数断言 718 → 719 —— 该用例原注释只预期「减一」,这里补记它同样会因新增 同构别名而「加一」。 实测 D6 确实咬得住:临时给该 schema 加 .default('read') 后,check:test-typecheck 报 `src/type-alias-convention.pin.test.ts: 1 type error(s) in a file the ledger does not cover`(注:裸 tsc --noEmit 不覆盖 *.test.ts,证明落在 typecheck 脚本的第二步)。 api-surface/integration.json 一并重生成:regen 合并驱动在 merge 时把该产物留给合并后重生成, 本次补回 #6072 的 10 个 XParsed 条目,本 PR 自己的两个条目原样保留。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011M7UwH25Unfi73UHim7ajY
…hase-2 flip (#5775) `check:spec-parsed-alias` (the phase-1 gate from #6072) refuses a bare `X = z.infer<typeof XSchema>` alias that is neither paired with an `XParsed` nor pinned as isomorphic. `PageContainerProps` is a lone optional array with no default, transform, catch or pipe anywhere in its tree, so `z.input` and `z.infer` coincide and the phase-2 flip of the bare name changes nothing observable — which is the pin route, not the `XParsed` route (a permanent synonym is a name an author can only pick wrongly). `component.zod.ts` had no bare alias until now, so this adds the file's first module import to the registry. It takes the next free M-index rather than the alphabetical slot: those indices are positional identifiers the 720 pin lines reference by number. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AwTBGD3gTnPKWmJLYU9R7T
…tack-ai#6083) (objectstack-ai#6279) * feat(spec)!: ADR-0122 phase 2 — the bare type name is the AUTHOR state (objectstack-ai#6083) Flips all 1384 bare `export type X = z.infer<typeof XSchema>` aliases in `packages/spec/src/**/*.zod.ts` to `z.input`, so `const c: Connector = { … }` is correct by default in every domain — the ADR-0033 keystroke argument that decided ADR-0122. Phase 1 (objectstack-ai#5551 / PR objectstack-ai#6072) had already given every schema with two distinct shapes its `XParsed` name, so nothing is stranded by the move. Retires 102 `XInput` aliases. After the flip each one denotes character-for- character what its bare name denotes, and D3 forbids a permanent synonym. Nine `*Input` names survive because they are not synonyms: four are the bare aliases of their own `…InputSchema`, five are composed (recursive / `Partial`-shaped) types no bare alias denotes. Inverts `check:spec-parsed-alias` rather than extending it. The flip empties the population the phase-1 gate was written over, and measured on this tree that gate reports 0 coverage findings (vacuously green) and 754 stale-pin findings (all false) — one arm silently stops working while the other misfires on the whole registry. The gate now refuses a bare name that reads `z.infer` (the flip, enforced), refuses an `XInput` synonym of a bare name (the retirement, enforced), and keeps the paired-or-pinned and stale-pin arms on the flipped form. Inverting it widened it, and the widening found real work: the 86 aliases that already read `z.input` had never been asked whether their parsed state was named. 57 were neither paired nor pinned; the same type-level probe that chose phase 1's split (with its control assertion, which failed as required) split them 22 differ / 35 isomorphic. The 22 gained an `XParsed`, the 35 became pins — 719 → 754. That is objectstack-ai#5507's remaining scope, absorbed here. Consumer migration is compiler-named except in one place: a parse result is structurally assignable to the author state, so a function's declared return type can silently start promising less. 24 `defineX` factories were therefore re-declared as `XParsed` by hand; every other migration in this change was a tsc error first. Evidence the direction is right: `test-typecheck-debt.json`, whose own header describes its contents as fixture literals annotated with the OUTPUT type while holding an authored INPUT literal, went from 79 files / 691 errors to 59 / 270 without a single fixture being edited. No runtime behaviour changes. `json-schema/` and `authorable-surface/` are byte-identical — those generators read runtime `z.ZodType` exports, never aliases. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015bLZKxxUUk4mahNfn3g3Ed * fix: migrate the consumers the flip named, and the two classes it cannot Everything here is a site where a value's type said "defaults applied" and the flip made that claim testable. Three shapes: 1. **Holders of a parse result** move to `XParsed` — `buildAccessMatrix`'s return, the health-monitor / hot-reload internal config maps, `DEFAULT_METADATA_TYPE_REGISTRY`, `MetadataManager.typeRegistry`, `MigrationExecutor`'s operations, the seed-loader service's request/result/graph plumbing, the FLS mask the permission evaluator builds. Each was `z.infer` before the flip; naming `XParsed` restores exactly that and changes no runtime behaviour. 2. **Readers of a defaulted key off an author-state value** state the schema's own default at the read — `ObjectLogger`'s `rotation`, the REST server's three nested operation maps, `ScriptBody.capabilities`, the delegated-admin report's flags. Per key, not per object: the schemas default *inside* those objects, so `??` on the whole object only ever filled a wholly absent one. 3. **Factories the compiler cannot name.** `createEvalUser` returns a complete user and declared `EvalUser`; a parse result is assignable to the author state, so it kept compiling while promising less. Now `EvalUserParsed`, like the 24 `defineX` factories. `IDataEngine.find`/`findOne` keep the parsed query type. Loosening them to accept the author state is a real improvement and deliberately NOT taken here: it needs the engine to apply `SortNode`'s `order` default at runtime, which is a behaviour change and belongs in its own change. Also updates the three gates that pinned a retired spelling: the `EnvironmentArtifact` dual-source pin (which named `EnvironmentArtifactInput`), `def-key-collisions`' mutation anchor, and `connector-author-shape`, whose "`ConnectorInput` is the author shape" probes now read `Connector` / `ConnectorParsed` with the literal and both verdicts unchanged — the flip's claim as a test. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015bLZKxxUUk4mahNfn3g3Ed * fix: finish the consumer sweep — repo-wide typecheck is green The remaining sites the compiler named, all the same shape as the last commit: `metadata-protocol`'s seed-loader and search query plumbing hold parse results, so they name `XParsed`; `ISeedLoaderService`'s request/result/graph follow, and its contract test's mocks with them. Two test-typecheck ledgers move because the flip fixed what they recorded, not because anything was suppressed: @objectstack/spec 79 files / 691 errors -> 58 / 268 @objectstack/client 3 files / 6 errors -> 0 / 0 (ledger now empty) Both ledgers' own headers describe their contents as "fixture literals annotated with a schema OUTPUT type while holding an authored INPUT literal" — which is the defect ADR-0122 exists to end. No fixture was edited to achieve this; the bare name simply means the input type now. The four entries that reached zero are deleted, which is what the shrink-only ratchet requires. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015bLZKxxUUk4mahNfn3g3Ed * docs(changeset): state the measured consumer-migration size The FROM→TO section claimed the compiler named "1 file outside packages/spec". It named 40, across 1127 candidate files — still the point being made (every one was a compile error, none silent), but the number was wrong. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015bLZKxxUUk4mahNfn3g3Ed * refactor(plugin-security): type HeldScope as what resolveHeldScopes builds `resolveHeldScopes` already normalises every adminScope flag to a boolean (`!== false` / `=== true`) and the allowlist to a `string[]`, so `HeldScope.scope` is the PARSED shape and now says so. The previous commit had reached for `?? true` / `?? false` at the report boundary instead — correct output, but dead code that reads like a default being applied where none is needed. The authored scope, which really does arrive as raw JSON with any subset of the flags stated, keeps the bare `AdminScope` at the two `parseMaybeJson` sites. Both containment paths there already read it with `!== false` / `=== true`, so the author state was the honest type for them all along — the flip is what made the two shapes distinguishable enough to say which is which. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015bLZKxxUUk4mahNfn3g3Ed * fix(spec): make the new ADR-0087 entry render correctly in the upgrade guide Two defects visible only in the generated output, caught by reading it: - `surface` carried backticks. `build-upgrade-guide.ts` renders that field INSIDE a code span (and again inside a table cell), so the nested backticks broke the span — the reader saw stray backticks mid-sentence. Every neighbouring entry writes `surface` as plain text for this reason (`actor-user-roles-to-positions` is `'action body / AI route: ctx.user.roles (req.user.roles)'`); this one now matches, and is shortened so the table cell stays readable. - the `rg` command in `acceptanceCriteria` had lost a backslash in escaping and read `\bw+Input\b` — it matches the literal `w`, so a reader running the acceptance check verbatim would get no hits and conclude the migration was already done. Now `\b\w+Input\b`. Regenerated spec-changes.json and the upgrade guide from the corrected entry. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015bLZKxxUUk4mahNfn3g3Ed * fix(core,objectql): migrate the debt-frozen test fixtures the flip named `check:type-check-debt` caught 19 raw errors the strict-zone typecheck could not: both packages exclude their own tests, so these live behind a DEBT/TEST_DEBT entry and only surface on the ratchet's re-measure. Every one is the same misalignment already fixed in the strict packages, so all 19 are FIXED — neither ledger entry is raised, and neither `note` needed rewriting. @objectstack/core, DEBT 111 -> 98 (the recorded number, exactly): All 13 were TS2345, a code the entry's note did not list at all — the pile's composition is unchanged otherwise (code-tier still exactly 3: TS18046/TS2739/ TS2352; config-tier 23; noise 72). The fixtures in health-monitor.test.ts and hot-reload.test.ts write every key of their config explicitly — they ARE parsed values — but were annotated with the bare name, which this branch made the author state. Annotating them `PluginHealthCheckParsed` / `HotReloadConfigParsed` restores exactly what they meant before the flip, which is also what `registerPlugin` takes. @objectstack/objectql, TEST_DEBT 361 -> 348: The gate reported +6 against the ledger's 355, but origin/main actually measures 351 — the ledger is 4 above main because of an improvement someone else landed and did not re-record. Measured against main rather than against the stale number, this branch added 13 and removed 3, and all 13 are the same shape: `EngineQueryOptions` annotations on query bags handed to `find`/`findOne`. The engine's parameter type is byte-identical to main's (`EngineQueryOptionsParsed` IS what main called `EngineQueryOptions`), so the fix is on the annotation, and it keeps these bags TYPED rather than cast — `hook-input-shape-contract.test.ts` and the objectstack-ai#4918 ratchet both exist to stop an erased query-options bag, in test code included. The entry is deliberately NOT lowered to 348. Four of the seven points of headroom are that other author's improvement; harvesting them into this PR's ledger edit would bank someone else's work under this change's name. The ratchet only forbids upward drift, so leaving 355 is green and honest. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015bLZKxxUUk4mahNfn3g3Ed --------- Co-authored-by: Claude <noreply@anthropic.com>
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