Skip to content

MetadataFacade answers three registerget round-trip cases differently from every other shipped IMetadataService #7378

Description

@os-zhuang

Observation-class finding, filed per Prime Directive #10 while implementing #7223 (PR #7371). Unassigned, deliberately not queued.

This is a divergence report, not a defect claim. #7223's suite pins each implementation's behaviour as measured; nothing in this repo currently RULES which of the two answers is correct in any of the three rows below. That ruling is what this card is for. ⛔ Do not "fix" one side without making the ruling — two of the three rows have a plausible case for the facade's answer.

How this was measured

METADATA_ROUNDTRIP_CASES (@objectstack/spec/contracts, added in PR #7371) replayed against every shipped implementation by packages/objectql/src/metadata-service-roundtrip-conformance.test.ts. Subjects: MetadataManager (registry only), MetadataManager (writable datasource: loader), createMemoryMetadata, MetadataFacade, plus the contract's own reference double in packages/spec. The first four columns agree on every row in the table; MetadataFacade is alone on these three.

The cells

1. The effective key is data.name, not the name argument

Cases key-is-the-name-argument-object, key-is-the-name-argument-nonobject.

await service.register('object', 'pin_key', { name: 'pin_data_name',});
await service.get('object', 'pin_key');   // manager/memory/reference: the document
                                          // MetadataFacade:            undefined
await service.exists('object', 'pin_key');   // …: true   /  facade: false
await service.listNames('object');           // …: ['pin_key'] / facade: ['pin_data_name']

MetadataFacade.register builds { ...data, name: data.name ?? name } and hands the DOCUMENT to SchemaRegistry.registerObject / registerItem, which key on the document's own name. The name argument is therefore only a fallback for a document that carries none. Measured identically on an object-typed and a view-typed write, so it is not object special-casing.

This is the round-trip #7223 is about: register(t, n, d)get(t, n) does not hold on MetadataFacade whenever d.name !== n.

The contract TSDoc names the parameter on both members (@param name - Item name/identifier (snake_case)) and says nothing about data.name. Neither answer is written down.

Worth noting before ruling: an implementation whose store is keyed by the document's own identity is not obviously wrong — it is arguably the reason SchemaRegistry behaves this way. The candidate rulings are (a) the argument wins, (b) the document wins, (c) a disagreement is refused loudly rather than silently resolved either way. (c) is the only one that cannot silently misplace an item, and it is a behaviour change on all four implementations, not just the facade.

2. The plural objects type is aliased to object

Case plural-objects-type-is-its-own-store.

await service.register('objects', 'pin_plural', objectDoc);
await service.get('object', 'pin_plural');   // manager/memory/reference: undefined
                                             // MetadataFacade:            the document

MetadataFacade.isObjectType treats 'object' and 'objects' as one type — deliberately, per its own header (#6725 left the plural with the same read/write split the singular had). The consequence measured here is on the READ side. MetadataManager and createMemoryMetadata key their type stores on the string they are handed, so the two spellings are two stores.

This one has a live cross-reference: check:meta-type-normalized exists as a gate, so the repo already has an opinion somewhere about type-name normalization. Whoever rules on this should reconcile the two rather than treat this as a fresh question.

3. A non-object data value is silently dropped

Case primitive-data-roundtrips.

await service.register('setting', 'pin_flag', 'enabled');   // ACCEPTED, no throw
await service.get('setting', 'pin_flag');       // manager/memory/reference: 'enabled'
                                                // MetadataFacade:            undefined
await service.exists('setting', 'pin_flag');    // …: true / facade: false
await service.listNames('setting');             // …: ['pin_flag'] / facade: []

The contract declares data: unknown, not object. MetadataFacade.register passes a non-object value through unchanged (its spread branch is guarded on typeof data === 'object' && data !== null) and then registers it under the document's own name — which a string does not have. The registry logs Registered setting: undefined and the value is readable back through no member.

This is the one row where the facade's answer looks hard to defend as it stands: the write is accepted and then lost, with no error and no way for the caller to notice. That is the same silent-loss family as #6725. Note the fix is not necessarily "store it" — refusing a data the implementation cannot key is equally consistent with the contract. What is not defensible is accepting and dropping.

Where the pins live

packages/objectql/src/metadata-service-roundtrip-conformance.test.ts, under // DIVERGENCE markers 1–3, each with the mechanism written out. A behaviour change to any of these rows turns that file red — which is the intended prompt to update the pin in the PR that makes the ruling, not silently.

Refs #7223, PR #7371, #6725, PR #7211, #6745.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions