Observation-class finding, filed per Prime Directive #10 while implementing #6055. Unassigned, finding, deliberately not queued — nothing a user hits today; it is a contract-documentation gap that made a consumer-side decision harder than it should have been.
The fact (verified on origin/main)
packages/spec/src/contracts/metadata-service.ts documents the sibling convenience readers by their equivalence:
/**
* Convenience: get a view definition by name
* Equivalent to get('view', name)
*/
getView?(name: string): Promise< unknown | undefined >;
getObject — the only one of the family that is required rather than optional — carries no such sentence:
/**
* Convenience: get an object definition by name
* @param name - Object name (snake_case)
* @returns The object definition, or undefined if not found
*/
getObject(name: string): Promise< unknown | undefined >;
And the two implementations do not agree:
packages/metadata/src/metadata-manager.ts — async getObject(name) { return this.get('object', name); }
packages/objectql/src/metadata-facade.ts — async getObject(name) { return this.registry.getObject(name); }, where the facade's own get() is registry.getItem(type, name, pkg) unwrapped to item?.content ?? item. Different lookup, different returned shape.
So a consumer holding an IMetadataService cannot know whether getObject(n) and get('object', n) answer the same thing, and in one of the two occupants of the metadata slot they do not.
Why it cost something concretely
#6055 needed the ADR-0110 D3 verdict (getDiagnosed, #5840) for the objectstack://objects/{objectName} MCP resource, whose resolver is getObject. getDiagnosed(type, name) is get's diagnosed twin, so the obvious fix — swap getObject(name) for getDiagnosed('object', name) — would have silently re-pointed the lookup on any MetadataFacade-backed host. Presuming an undeclared equivalence at a consumer is the private dialect Prime Directive #12 forbids, so PR for #6055 kept getObject as the resolver and asked getDiagnosed for the verdict only, paying one extra read on the miss path. That trade is written up in diagnoseEmptyRead's TSDoc.
Dispositions worth pricing (no recommendation forced)
- Document the equivalence and make it true — add "Equivalent to
get('object', name)" to the contract and change MetadataFacade.getObject to delegate to its own get. Cheapest to state, but it changes what the facade returns (ServiceObject becomes the unwrapped content), so it needs a consumer sweep of the facade's readers.
- Document the divergence as intentional — say in the contract that
getObject may resolve differently and that consumers needing the canonical object item must use get('object', name). Zero behaviour change; makes the existing consumer-side caution correct instead of defensive.
- Retire
getObject from the contract — it is a convenience over a member every implementation already has. Largest sweep; only worth it if the divergence is judged to have no business pull.
Refs #6055, #5840, PR #6051, Prime Directive #12.
Observation-class finding, filed per Prime Directive #10 while implementing #6055. Unassigned,
finding, deliberately not queued — nothing a user hits today; it is a contract-documentation gap that made a consumer-side decision harder than it should have been.The fact (verified on
origin/main)packages/spec/src/contracts/metadata-service.tsdocuments the sibling convenience readers by their equivalence:getObject— the only one of the family that is required rather than optional — carries no such sentence:And the two implementations do not agree:
packages/metadata/src/metadata-manager.ts—async getObject(name) { return this.get('object', name); }packages/objectql/src/metadata-facade.ts—async getObject(name) { return this.registry.getObject(name); }, where the facade's ownget()isregistry.getItem(type, name, pkg)unwrapped toitem?.content ?? item. Different lookup, different returned shape.So a consumer holding an
IMetadataServicecannot know whethergetObject(n)andget('object', n)answer the same thing, and in one of the two occupants of themetadataslot they do not.Why it cost something concretely
#6055 needed the ADR-0110 D3 verdict (
getDiagnosed, #5840) for theobjectstack://objects/{objectName}MCP resource, whose resolver isgetObject.getDiagnosed(type, name)isget's diagnosed twin, so the obvious fix — swapgetObject(name)forgetDiagnosed('object', name)— would have silently re-pointed the lookup on anyMetadataFacade-backed host. Presuming an undeclared equivalence at a consumer is the private dialect Prime Directive #12 forbids, so PR for #6055 keptgetObjectas the resolver and askedgetDiagnosedfor the verdict only, paying one extra read on the miss path. That trade is written up indiagnoseEmptyRead's TSDoc.Dispositions worth pricing (no recommendation forced)
get('object', name)" to the contract and changeMetadataFacade.getObjectto delegate to its ownget. Cheapest to state, but it changes what the facade returns (ServiceObjectbecomes the unwrapped content), so it needs a consumer sweep of the facade's readers.getObjectmay resolve differently and that consumers needing the canonicalobjectitem must useget('object', name). Zero behaviour change; makes the existing consumer-side caution correct instead of defensive.getObjectfrom the contract — it is a convenience over a member every implementation already has. Largest sweep; only worth it if the divergence is judged to have no business pull.Refs #6055, #5840, PR #6051, Prime Directive #12.