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.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)

  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.

Activity

  1. os-zhuang commented on Aug 8, 2026

    @os-zhuang
    Contributor

    Findings triage — routing repair + grade.

    Routing — added domain:spec; routing only, finding grade unchanged, no ownership taken. Landing rationale: all three priced dispositions run through packages/spec/src/contracts/metadata-service.ts (document the equivalence, document the divergence, or retire the member), and disposition 3 is ADR-0049 enforce-or-remove territory — the same family as #5295 / #5703, both domain:spec. Dispositions 1 and 3 change contract behavior/surface, so this is the semantic face, not spec-surface; the reverse red line keeps behavior-affecting cards in domain:spec.

    Anchors verified on origin/main @ 3a1d9c7 — getObject required member at packages/spec/src/contracts/metadata-service.ts:334 with no equivalence sentence; the two divergent implementations at packages/metadata/src/metadata-manager.ts:1370 (delegates to get('object', name)) and packages/objectql/src/metadata-facade.ts:104 (delegates to registry.getObject). Dedup: getObject in:title — only this card.

    Grade: hold as finding — contract-documentation gap; nothing a user hits today (the one consumer that stumbled, #6055, already paid its way around it and documented the trade). Why still held rather than promoted: the disposition choice itself is the work, and it needs the spec seat's pricing pass, not a dev dispatch. Note for the spec seat: this card is also a member of the "TypeScript export/contract surface" boundary family flagged to the maintainer on 2026-08-08 (seat-post comment, #4593/#5507/#5975) — if that ruling lands, it may re-route this card.

    本评论来自分诊座位 Routine(#5474 试点),不构成认领。


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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