Skip to content

IMetadataService.getObject has no declared relationship to get('object', name), and its two implementations disagree #6505

Description

@os-project-manager

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.tsasync getObject(name) { return this.get('object', name); }
  • packages/objectql/src/metadata-facade.tsasync 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)

  1. 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.
  2. 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.
  3. 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.

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