Skip to content

Nothing pins register(type, name, d)get(type, name) across the shipped IMetadataService implementations — the hole #6725 fell through #7223

Description

@os-zhuang

Observation-class finding, filed per Prime Directive #10 while implementing #6725 (PR #7211). Unassigned, deliberately not queued. This is a test-coverage gap, not a defect in shipped behaviour — filed separately rather than folded into #6725 because the fix is a conformance suite, not a line of engine code.

The fact (read on origin/main @ 55da611)

IMetadataService.register and .get are the contract's first two CRUD members, and the round-trip between them is exercised in exactly one place:

packages/spec/src/contracts/metadata-service.test.ts — "should register and retrieve metadata items asynchronously":

const store = new Map<string, Map<string, unknown>>();
const service: IMetadataService = {
  register: async (type, name, data) => { …store.get(type)!.set(name, data); },
  get: async (type, name) => store.get(type)?.get(name),};
await service.register('object', 'account', objectDef);
expect(await service.get('object', 'account')).toEqual(objectDef);
expect(await service.getObject('account')).toEqual(objectDef);

Every subject in that file is an inline literal written in the test itself. The suite asserts that a Map behaves like a Map. It is a type-conformance file — which is the right thing for packages/spec to host, since the contract has no runtime and spec is the dependency root — but it means the round-trip is pinned against no shipped implementation at all.

The cross-implementation pin that does exist,
packages/objectql/src/metadata-service-getobject-equivalence.test.ts (#6745), covers a different proposition — getObject(name)get('object', name) — and seeds every subject through registry.registerObject / manager.register, explicitly not through the facade's own write. Its header says so, and says why: seeding the facade the other way produced undefined on both members.

So: no test anywhere asserts that writing through an implementation's register makes the item readable through that same implementation's get.

Why this is worth a card

That is precisely the hole #6725 fell through. MetadataFacade.register('object', …) wrote into a map none of its own reads consult; every read answered undefined; the full packages/objectql suite (167 files, 2917 tests) stayed green, and so did all 64 lint.yml gates. A shipped, exported implementation of the platform's central metadata contract could not perform its own most basic round-trip, and nothing in the repo was positioned to notice.

packages/objectql is again the only package that can see all three implementations at once — it depends on @objectstack/metadata (MetadataManager) and @objectstack/core (createMemoryMetadata) and owns MetadataFacade — which is exactly the argument #6745's header already makes for living there.

Shape worth pricing (no recommendation forced)

A describe.each over the same implementation table #6745 builds, asserting the CRUD contract per implementation and per metadata type: registerget / exists / list / listNames; unregister → all four answer absent; re-register replaces rather than duplicates. At minimum one object type and one non-object type, since the object type is special-cased on the read side of SchemaRegistry and the generic types are not — the asymmetry that produced #6725.

Two things to decide before writing it:

Refs #6725, PR #7211, #6745, #6505 / PR #6723.

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions