fix(metadata): capabilities.write 同时约束 save() —— 可写 datasource loader 必须实现写入与删除两半 (#5654) - #5761
Merged
Merged
Conversation
…ource loader must implement both halves (#5654) `registerLoader()`'s #5276 gate required `delete()` for a `datasource:` loader declaring `capabilities.write: true`, and stopped there. `register()`'s persistence loop read `loader.save &&` FIRST, so the same declaration with no `save()` method was silently skipped — no warn, no error — while the registry write, list-cache invalidation, `created`/`updated` event and watcher notification all proceeded, telling the caller the write had landed. The item read back for the life of the process and was gone at the next restart. The gate (renamed `assertWritableLoaderContract`) now requires both methods and rejects with one message naming which are missing, the consequence, and both repairs. `register()`'s `save` short-circuit survives as annotated defensive code whose unreachability is guaranteed by `registerLoader()` being the sole writer of the loader map — mirroring `unregister()`'s `delete` guard. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V7WetGmnfoXNn8cLieKKmx
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckThis PR changes 1 package(s): 7 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
…ere the author is standing (#5654) `delete?` carried #5276's note that it is mandatory for a `datasource:` loader declaring `capabilities.write`; `save?` carried nothing, so after #5654 widened the gate the asymmetry pointed the wrong way — the interface documented one half of a contract that binds both. `save?` now states the same thing in its own terms (silent skip in `register()`, the write announced but never landed, gone at the next restart), and `delete?`'s note points at the widened gate by name (`assertWritableLoaderContract`, #5276 + #5654) instead of describing a delete-only one. Comments only — no signature change, no runtime effect. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V7WetGmnfoXNn8cLieKKmx
Contributor
Author
|
已按 PM 裁决(#5654 评论 5200271661)追加纯注释提交 轻量验证(按裁决要求,未重跑全套件——注释无运行时影响): PR 正文的「一处按硬约束留手的地方」一节已改写为「作者面:接口 TSDoc 已按 PM 裁决补齐」,请裁决措辞已删除。PR 仍为 draft,未自行转 ready。 Generated by Claude Code |
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 #5654
前提复核(先证伪,再动手)
在
origin/main上逐条复核了单子的前提,成立:packages/metadata/src/metadata-manager.ts:129的assertWritableLoaderCanDelete只查delete;:665的持久化循环把loader.save &&排在最前,声明了protocol: 'datasource:'+capabilities.write: true却没有save()的 loader 会被一声不吭地跳过;datasource:协议的 loader 只有DatabaseLoader,save/delete都有 —— 所以今天没有用户路径踩得到,这是留给第三方 / AI 照接口新写 loader 的坑(单子的判断准确)。改动
一句话:#5276 在
delete一侧立的门禁,补齐save一侧。同一个capabilities.write声明,此前在一个生命周期端点上是强制的、在另一端是装饰性的。assertWritableLoaderCanDelete→assertWritableLoaderContract,判据从「必须有delete」扩为「必须同时有save与delete」,缺哪个查哪个,registerLoader()注册期响亮拒绝。buildWritableLoaderMissingDeleteMessage→buildWritableLoaderMissingMethodsMessage(loaderName, missing),措辞与 fix(metadata): 声明了capabilities.write的 datasource loader 必须实现delete(),注册期响亮拒绝 (#5276) #5652 同族(后果 + 两条修法),按缺失方法各贡献一句后果:save:register()跳过 → 内存 registry 照写、listCache 照失效、created/updated照广播、watcher 照通知 → 调用方被告知写入成功,本进程读得回,重启即消失;delete:fix(metadata): 声明了capabilities.write的 datasource loader 必须实现delete(),注册期响亮拒绝 (#5276) #5652 原文逐字保留(unregister()宣告删除但行还在)。implements neither a save() nor a delete() method,修法列出两个签名。packages/metadata/src/index.ts只导出MetadataManager等),改名不触及已发布 API 面。loaders/loader-interface.ts:save?补上与delete?对称的 TSDoc(对datasource:+write而言是强制的、注册期由assertWritableLoaderContract强制、其余协议合法缺席),delete?上MetadataLoader不声明delete?:一个capabilities.write的 datasource loader 若没有 delete 方法,unregister()会静默跳过它并照常宣告「已删除」 #5276 那段改为指向加宽后的同一个门禁。纯注释,无签名变化 —— 详见下方「作者面」一节。关于
:665的loader.save &&短路(按认领评论的方向)保留,并改写成与
unregister()完全对称的两行 + 注释:推理:
registerLoader()是this.loaders的唯一写入口(构造函数的config.loaders也走它),所以「datasource:+write+ 无save」对已注册 loader 不可达;但save?在MetadataLoader接口上仍是可选的(门禁不覆盖的协议合法地没有它),删掉这行的代价是下一行直接 TypeError。所以它从「策略」降级为「类型收窄」,而不是被删除 —— 与 #5652 对delete的处理一字不差。读取顺序也调成先协议/能力(策略)后方法(收窄),与删除侧一致。反向验证(先定方向,再跑)
预测:把门禁的
save一臂摘掉(WRITABLE_LOADER_METHODS只留'delete'),save 侧的拒绝用例应转红,delete 侧与作用域/正向用例应保持绿。实测:
Tests 5 failed | 19 passed (24)—— 方向一致,但数量与预测差一个,如实记录:转红的是 5 条而非 6 条。第 6 条(the delete-only rejection #5276 shipped still reads exactly as it did)钉的是「只缺 delete 时消息与 #5652 逐字一致」,摘掉 save 一臂后这条消息仍然产生,所以它应当保持绿 —— 它本来就不是 save 一臂的钉子。delete 侧 11 条全绿,证明本次改动没有动到 #5276 的既有语义。测试
新增
packages/metadata/src/metadata-manager-loader-save-contract.test.ts(13 条),钉住:save的可写 datasource loader 在registerLoader()与构造函数两条入口都被拒,且没有半注册(不在私有 loaders map 里);created/updated已广播、gone at the next restart)、两条修法;并且toBe(buildWritableLoaderMissingMethodsMessage(...))—— 是 builder 那一份文本本身,不是转述(顺带让这个 export 真正被引用);register()真的写进 store → 用同一个 store 新建一个 manager(模拟重启)仍读得到 —— 正是静默跳过会丢的那一次重启;register()作用的组合:只读datasource:、以及file:/memory:/http:/s3:四种协议即使write: true且无save也照常注册;DatabaseLoader在加宽后的门禁下不受影响。命令与真实输出(均在共享 verify 锁内、
--max-old-space-size=4096、--maxWorkers=2):类型:
@objectstack/metadata无typecheckscript(在check-type-check-coverage.mjs的 DEBT 账本里)。所以直接跑了tsc --noEmit -p packages/metadata/tsconfig.json并与改动前对拍:其他门禁:
合入前已
git merge origin/main(无冲突,来项不触及packages/metadata),重建依赖后重跑上述 metadata 用例仍 508/508。影响面与 changeset
.changeset/metadata-loader-save-contract.md(patch)写明了 FROM → TO:只影响注册自定义datasource:+write: trueloader 且没有save()的使用者 —— 此前注册成功并静默丢写,现在注册即抛,错误里同时给出两条修法(实现save(type, name, data, options?): Promise< MetadataSaveResult >,或诚实地声明capabilities.write: false)。仓内唯一的datasource:loaderDatabaseLoader两个方法都有,不受影响。作者面:接口 TSDoc 已按 PM 裁决补齐
delete?上本来带着 #5276 写下的「对datasource:+write而言是强制的」那段,而save?上没有 —— 门禁加宽后,这个不对称正好指反了:接口只讲了一个约束两半的契约的其中一半。已按 PM 裁决(#5654 评论 5200271661)在同一分支追加一次纯注释提交补齐:save?用自己的措辞讲同一件事:register()的静默跳过、写入被宣告却没落地、重启即消失,其余协议合法缺席;delete?那段改为按名字指向加宽后的同一个门禁(assertWritableLoaderContract,MetadataLoader不声明delete?:一个capabilities.write的 datasource loader 若没有 delete 方法,unregister()会静默跳过它并照常宣告「已删除」 #5276 +register()对「声明capabilities.write却没有save方法」的 datasource loader 同样静默跳过 —— #5276 只堵了 delete 一侧,save 一侧的同形缺口还在 #5654),不再描述一个只管 delete 的门禁。无签名变化、无运行时影响;
tsc增量 0、eslint 无输出。