Skip to content

test(objectql): pin getObject(n) = get('object', n) across all three IMetadataService implementations - #6839

Merged
os-project-manager merged 1 commit into
mainfrom
claude/issue-6745-getobject-equivalence-pin
Aug 9, 2026
Merged

test(objectql): pin getObject(n) = get('object', n) across all three IMetadataService implementations#6839
os-project-manager merged 1 commit into
mainfrom
claude/issue-6745-getobject-equivalence-pin

Conversation

@os-project-manager

Copy link
Copy Markdown
Collaborator

Fixes #6745

背景

PR #6723(为 #6505)在 IMetadataService.getObject 的 TSDoc 上写下:getObject(name)get('object', name) 在本仓库发布的每一个实现里都走同一次查找,两个成员交回同一个对象。这句话当时只是声明,没有任何门禁——此后任一实现的改动让这对成员发生分歧,不会有任何测试变红,而契约 TSDoc 就开始说谎。正是本仓库反复付过学费的 declared-not-enforced 形状。

本 PR 把它变成门禁。

落点

新增 packages/objectql/src/metadata-service-getobject-equivalence.test.ts(纯测试)。

packages/objectql 是唯一能同时看见三个实现的包:它同时依赖 @objectstack/metadata(MetadataManager)与 @objectstack/core(createMemoryMetadata),而 MetadataFacade 本就是它自己的。packages/spec 无法承载——契约没有运行时。

先测量,再钉住

在写断言之前,先用一次性探针实测了三个实现在被钉住的路径上实际返回什么。结论:等价关系今天成立,而且成立在最强的形式上——同一引用(不只是深相等),命中与未命中两条路径都成立。

实现 命中 未命中
MetadataManager(注册表命中) 同一引用,keys name/label/fields 双方 undefined
MetadataManager(loader 回退) 同一引用(loader 路径不克隆,两种调用顺序都成立) 双方 undefined
createMemoryMetadata 同一引用(两个成员读同一个 Map) 双方 undefined
MetadataFacade 同一引用,keys 含 nameField / _packageId / _provenance 双方 undefined

所以断言用 toBe(引用相等)而不是深相等:那才是三个实现的机制今天真正交付的东西,也正是契约声称的。facade 那一行额外印证了 TSDoc 的 "runtime-effective object" 说法——回来的对象带着物化接缝(registerObjectresolveObject)加上的列。

两处形状是承重的

1. facade 必须用 registry.registerObject 播种,不能用 facade.register('object', …) 这两者不可互换:facade 走 SchemaRegistry.registerItem 写进通用 metadata map,而它自己的两个对象读法都从 objectContributors 解析(getItemobject 类型直接特判回 registry.getObject)。用后者写进去的对象,两个成员都读不回来——实测双方都是 undefined。若照那样播种,"命中"用例就变成拿 undefinedundefined 相比,绿得毫无内容。这条写读劈叉本身是已归档的独立 finding #6725,本 PR 刻意不对它下断言;present 用例里的 expect(...).toBeDefined() 就是防止这种空转版本再次通过的那道闩。

2. MetadataManager 出现两次,覆盖它的两条解析路径。 它的 get 能从内存注册表答就从注册表答,答不了才回退到 loaders;getObject 委托给 get。所以一次"把 getObject 改写成只读注册表"的改动,会在所有注册表播种的用例上继续一致,只在 loader 支撑的用例上分歧。两条路径各播种一个 subject,才让这种断裂可见。

反向验证(先预测方向,再测量)

预测写在跑之前。基线 12/12 绿(4 subject × 3 用例)。三次消融,每次只打断一个实现的 getObject:

消融 预测 实测
A1 facade 的 getObject 改读 #6725 那个存储位置 facade 行 present + 辨名两例红,miss 例仍绿,其余 9 例绿 完全一致:2 failed / 10 passed,红在 expected undefined to be defined
A2 createMemoryMetadata.getObject 读错类型名(objects) memory 行 present + 辨名两例红,miss 例绿,其余 9 例绿 完全一致:2 failed / 10 passed
A3 MetadataManager.getObject 不再委托 get,改为只读注册表 registry-hit 行 3/3 全绿,loader-fallback 行 present + 辨名两例红 完全一致:2 failed / 10 passed

A3 是最有价值的一次:它证明第二个 MetadataManager subject 是承重的——只放注册表播种那一个 subject 的钉子,面对一次真实分歧会全绿放行。

三次消融还一致地说明了一件值得写下来的事:miss 用例抓不到这一类断裂(双方都答 undefined,等价仍然成立)。真正抓到的是 present 用例里的反空转断言。miss 用例钉的是另一件事——两个成员在未命中时给出同一个答案。

三次消融已全部回滚,工作树只剩新增的测试文件。

门禁(真实输出)

  • pnpm --filter @objectstack/objectql testTest Files 155 passed (155) / Tests 2680 passed (2680)(新增文件前为 154 / 2668,新增 12 例)
  • pnpm --filter @objectstack/objectql typechecktsc --noEmit,无输出
  • pnpm lint → 无输出
  • check:nul-bytes / check:engine-double-contract / check:error-code-casing / check:route-envelope / check:slot-lookup → 全部 PASS

changeset

无。纯测试改动(单个新增 *.test.ts),不发布任何东西,已加 skip-changeset 标签。

刻意没做

Refs #6505, PR #6723, #6724, #6725.


Generated by Claude Code

…IMetadataService implementations

PR #6723 documented on `IMetadataService.getObject` that the pair resolves
through one lookup in every shipped implementation, leaving the statement
declared-but-ungated. This adds the conformance table that gates it.

Four subjects over three implementations: MetadataManager under BOTH of its
resolution paths (in-memory registry hit and loader fallback),
createMemoryMetadata, and MetadataFacade. Three cases each: a present object
answered identically (reference identity, which is what every implementation's
mechanism actually delivers), `undefined` from both members on a miss, and a
name-discrimination case that a member ignoring its argument cannot survive.

The facade is seeded through `SchemaRegistry.registerObject`, never through
`facade.register('object', ...)`: the latter writes where neither of the
facade's object reads look (#6725), so seeding that way would compare undefined
to undefined and call it equivalence. The `toBeDefined()` in the present-object
case is what keeps that vacuous version from passing.

Measured before pinning — the equivalence holds today on all four subjects, on
both the hit and miss paths, at reference identity. Verified non-vacuous by
ablation: breaking each implementation's getObject in turn reddens exactly its
own subject's present + discrimination cases and leaves the miss case green.

Refs #6505, PR #6723, #6724, #6725.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F8q5J1MQyocgtNspb15fSn
@vercel

vercel Bot commented Aug 8, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 8, 2026 11:56pm

Request Review

@os-project-manager os-project-manager added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 8, 2026 — with Claude
@github-actions github-actions Bot added the size/m label Aug 8, 2026
@github-actions

github-actions Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

No hand-written docs reference the 0 changed package(s). ✅

@github-actions github-actions Bot added the tests label Aug 8, 2026
@os-project-manager
os-project-manager marked this pull request as ready for review August 9, 2026 01:14
@os-project-manager
os-project-manager added this pull request to the merge queue Aug 9, 2026
Merged via the queue into main with commit 7a5ef00 Aug 9, 2026
28 checks passed
@os-project-manager
os-project-manager deleted the claude/issue-6745-getobject-equivalence-pin branch August 9, 2026 01:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/m skip-changeset PR has no user-facing published change; bypasses the changeset gate tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Pin the getObject(n) = get('object', n) equivalence across all three IMetadataService implementations

2 participants