Uh oh!
There was an error while loading. Please reload this page.
refactor(spec)!: 按 ADR-0049 摘除 IStorageService.list(prefix) —— 零消费方、双适配器语义分叉 (#5540) - #5983
Conversation
One contract method, two adapter dialects, both silently incomplete, and no caller. ADR-0049 enforce-or-remove; maintainer ruling 2026-08-05 on #5266. Same disposition as IDataDriver.findStream (#4484): a TS/API contract that code IMPLEMENTS and nothing ever .parse()s, so there is no tombstone and no D2 source rewrite -- tsc is the channel and it reports at the call site. The retirement is registered as the ADR-0087 D3 semantic entry `storage-service-list-retired` in the protocol-17 chain step. 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
|
📓 Docs Drift CheckThis PR changes 2 package(s): 113 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
…through (#5540) The contract member it forwarded to is gone, so `this.inner.list` no longer type-checks. That break is CI-visible through tsup's DTS step (which runs tsc), not through a `typecheck` script -- `@objectstack/service-storage#build` failed and took Build Core / Test Core / Dogfood with it. PM ruling on #5540: land the contract removal and its only caller's deletion atomically, so `main` is never red. Scope is the proxy passthrough plus its two test sites only; the adapters' own `list` implementations stay for #5541. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011M7UwH25Unfi73UHim7ajY
⛔ merge queue 构建失败 — 先分诊,再决定要不要重排队列构建 31114891451 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集), 失败的 job(日志抽取,best effort):
历史信号:
分诊清单:
Generated by Claude Code · merge-queue-triage workflow (#4859) |
Uh oh!
There was an error while loading. Please reload this page.
merge origin/main 后在合并树 e8db1a2 上重跑 --re-measure:5 条上漂 (service-analytics 7->10、runtime 218->227、rest 143->153、plugin-auth 129->131、lint 30->32)、1 条下降(service-storage 42->41),其余 28 条与 记录值严格相等。 按维护者 2026-08-07 的裁决执行选项 A:今日实证的三个热包 (objectql / lint / rest)按实测 +10 记档,note 逐条写明「bootstrap margin (+10 over N measured at e8db1a2) -- tighten via the ℹ hint immediately after landing」,并在 MEASURED 文档块加一段说明这是台账里唯一的余量、 每轮打印的 ℹ 行就是收紧工单。其余条目一律精确等于实测。 note 成分按实测重写,delta 能归因的逐条归因、不能的明说: - runtime +9 全部是 TS18048,全在本窗口新增的一个文件 src/domains/meta-item-envelope.test.ts(#5563 / PR #5895); - rest +10 中 8 条来自三个新增测试文件,余 2 条落在既有文件、不再编造归因; - plugin-auth +2 中 1 条在新增的 last-admin-guard.test.ts(#5941 / PR #5993); - service-analytics +3 仍全在 measure-source-field-gate.test.ts 同一文件、 同一错误码(#5716 / PR #5963),该包 3 -> 7 -> 10 全程无人看见; - lint +2 两条 TS7006 均在既有文件,pre-merge 逐文件计数未留存,如实记录不归因; - service-storage -1 跟着 #5540 / PR #5983 摘除 IStorageService.list(prefix), 下调而非留着——高于实测又没写明的余量就是暗账。 tests 文件数一并对齐实测(objectql 130、runtime 102、rest 62、plugin-auth 38、 plugin-security 35、service-sms 5)。 合并冲突只有 TEST_DEBT 一处:main 的 #5773 / PR #5982 改过 service-sms 一行,本分支重写了整块——取本分支并把该条的事实并入,两侧都不丢。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014wsZeReNTqiceBfLb5Pyf5
合并 main 到 dca5bd3 后再全量重测,余量在一小时内被兑付了两笔,记录如下: - `@objectstack/objectql` 实测 339 -> **345**(+6 全是 TS2554,全在 `src/summary-rollup.test.ts`,由飞行途中落地的 #5749 / PR #6013 扩写)。 记档 349 把它静默吸收了 —— 若按精确值 339 记账,这就是同一场赛跑的第 6 次红。 按裁决「实测 +10」把记录抬到 **355**,恢复满额余量。 - `@objectstack/service-storage` 42 -> 41 -> **42**:`IStorageService.list(prefix)` 的退休被拆成两个 PR,spec 半边(#5540 / PR #5983)减 1、适配器半边 (#5541 / PR #6061)删旧测试(-1 TS7006)又新增 `storage-adapter-list-retirement.test.ts`(+2 TS2835),净 +1。上一轮我按实测 下调到 41,一小时后就被咬红 —— 正是派发令说的「非余量条目被基漂移咬住」, 按同一记档规则给这条加 +10,记 **52**,不开精确校准 lap。 一个值得写进文档块的新形状:**拆成两个 PR 的退休会让计数先降后升**,在两半之间 记下的精确值,推上去之前就已经过期。 `rest` / `lint` 两条实测未动(153 / 32),余量原样,note 补记「一小时后在 77c7c88 复测仍是该值」。 重测输出:四条记档余量各打印一行 ℹ(各 -10),无一条上漂,exit 0。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014wsZeReNTqiceBfLb5Pyf5
Fixes#5540
按 ADR-0049 enforce-or-remove 摘除
IStorageService.list(prefix),并原子地删除它在仓内的唯一调用方(SwappableStorageService透传)。维护者 2026-08-05 在 #5266 批准方案 2(摘除);落地方式经 PM 2026-08-06 裁决为「合并为一个 PR」(见下)。为什么摘
一个契约方法,两个自带适配器两种语义,而且双双静默给不完整答案:
LocalStorageAdapter.list是单层readdir—— 嵌套 keya/b/c在list('a')下看不见(只回a/b),而且stat成功的子目录被当成文件推进结果,产出一个size是目录 inode、download()根本取不到的StorageFileInfo;S3StorageAdapter.list是递归的(ListObjectsV2按整串 key 前缀匹配),且不读IsTruncated/ContinuationToken—— 超过 1000 个对象时调用方拿到的「全部文件」其实是第一页,没有任何信号。同一个调用在一种部署上是「单层 + 混入目录」、在另一种上是「递归 + 静默截断」,第一个真正需要枚举前缀的功能(备份 / 孤儿清理 / 迁移校验)会拿到两个不同的错误答案,两边都不报错。#5172 差点就是那个功能:原计划用
list(EMAIL_ATTACHMENT_KEY_PREFIX)驱动附件回收,发现本地适配器连一层都看不下去,改走队列延迟任务。原子落地依据(PM 裁决,2026-08-06)
本 PR 最初只做 spec 面。CI 实测证明那样不可能绿,于是按 #5540 上的 PM 裁决评论 扩为原子落地。
实测证据:契约成员一摘,
SwappableStorageService按IStorageService类型读this.inner.list(那是调用方,不是「类上多余的方法」)立刻不再 type-check:而这会让 CI 红 —— 不经由
typecheckscript(@objectstack/service-storage确无该 script,挂在check-type-check-coverage.mjs的 DEBT 账本上),而是经由 tsup 的 DTS 生成步骤(它跑 tsc 产.d.ts),TypeScript Type Checkjob 先pnpm build就炸在@objectstack/service-storage#build,并连带Build Core/Test Core (1-3/3)/Dogfood (1-3/3)全红 —— 同一根因,11 个红 job。上一版 commit(521b6fb)的实测读数就是如此。顺便订正 issue 正文一句:「TS 层面…故本单可独立先行」前半句成立(适配器类上多余方法不是错误),后半句不成立(代理是调用方)。也因此 #5266 原定的
#5541 Blocked-by #5540箭头是反的:list?()在契约上是 optional,先删代理透传是绿的,先摘契约成员反而不行。本 PR 的 services 面范围严格限定为 PM 裁决申报的那部分:
swappable-storage-service.ts—— 删除list()透传(留注说明去向);swappable-storage-service.test.ts—— 删掉forwards list()用例,并从「rejects optional methods」用例里去掉proxy.list那一行(其余三个 optional 方法仍在测);local-storage-adapter.test.ts—— 去掉expect(typeof storage.list)(它断言的正是被退役的「契约宣称 list」)。⛔ 双适配器(local / S3)的
list实现没有动,测试替身FakeAdapter.list也没动 —— 类上多一个方法合法且可编译,那是 #5541 重定价后的剩余范围(#5541 已留注)。前提重验
「零消费方」在本仓成立。 对
origin/main用带引号精确名逐条排除:IStorageService的全部命中,除packages/spec声明与其自身测试外,全部落在packages/services/service-storage内部;REST / CLI / storage-routes / 插件均无调用点;skills/、docs-import-surface.baseline.json零命中。零命中做了反查证伪「扫描器坏了」:同一命令查邻近成员getInfo返回 13 个文件。兄弟仓:⚠️
objectui实测干净(查storage作反查返回大量命中)。cloud本会话无法核验:add_repo报无访问权限,GitHub code search 对该仓连必然存在的对照词(objectstack/defineStack)都返回 0,所以它的 0 不构成证据 —— 如实标注,不冒充已验。退役路线:为什么没有 tombstone,为什么是 D3 而不是 D2
spec-property-retirementplaybook 主要覆盖 authorable property(Zod schema 里的 key)。IStorageService不是 —— 它是纯 TypeScript 契约,由代码 implements,没有任何地方.parse()过一个 storage adapter。所以:retiredKey()tombstone(playbook §2 第三行「nothing parses it → neither」):处方没有接收者就是噪声,唯一能承载处方的通道是tsc,它报在调用点 —— 本次 CI 恰好把这条演示了一遍;storage-service-list-retired(protocol-17 step 的semantic[]),带reason+acceptanceCriteria,于是spec-changes.json、生成的 upgrade guide、spec_changesMCP 工具都携带它。仓内有逐条对应的先例:
IDataDriver.findStream(#4484)—— 同样 TS 契约成员摘除、同样零调用方、同样「no tombstone / no source rewrite / tsc 是通道」,登记为同一 step 的data-driver-find-stream-retired。本条reason显式点了这层同构。四张 ratchet 零变化 —— 路线决定的预期读数,不是漏做
先定路线再看读数:本次既不是枚举值收窄也不是整 def 删除,而是一个 TS 接口成员的摘除。
api-surface.json记的是导出面(IStorageService (interface)仍在,打印的是类型引用不是展开形状),authorable-surface.json/json-schema.manifest.json记 Zod 声明面,api-surface-signatures.json只覆盖defineX函数 —— 四张对「接口少一个成员」结构上不可见。实测吻合:12 张 gate 逐条捕获退出码,只有check:spec-changes/check:upgrade-guide因新增 D3 条目变红,重生成后check:generated报 10/10 全绿。反向验证(先定方向,再跑)
预测方向红且量级明确:把
list?()放回接口 → 两条@ts-expect-error变 unused(TS2578 x2)→check:test-typecheck这张精确 ratchet 该文件从 1 涨到 3。实测:方向与量级都对上,pin 是活的(不是「因为什么都没产出所以绿」的空 pin)。已还原。
文档
两页手写文档同步(它们原样在教
list?(prefix)怎么调,含可复制示例;契约摘掉而文档不动正是 Prime Directive #10 的「advertise a capability the runtime doesn't deliver」):content/docs/kernel/contracts/storage-service.mdx、content/docs/kernel/runtime-services/storage-service.mdx。两页都不是生成物,content/docs/releases/一个字没碰。验证
一条读数值得记:service-storage 手跑
tsc --noEmit现为 41 条(全部先于本 PR 存在)。我早先记过 72 —— 那次是在依赖未构建时测的,@objectstack/types/@objectstack/observability未解析导致 TS7006 噪声级联(AGENTS.md 点名的 NodeNext 陷阱)。归因之所以没被这 31 条噪声带偏,是因为我按名字('list')grep 而不是信总数。