From 966d308e273f951691a60ebf0095f5a611240b22 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 8 Aug 2026 13:32:15 +0000 Subject: [PATCH] docs(spec): declare what IMetadataService.getObject answers with (#6505) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `getObject` was the only member of the convenience family with no stated relationship to the generic reader it wraps — `getView` and `getDashboard` both say "Equivalent to get('view'|'dashboard', name)", `getObject` said nothing. #6055 paid for that silence: it declined to swap the resolver for `getDiagnosed('object', name)` and bought a second read on the miss path rather than presume an equivalence the contract never made (PD #12). Two measured facts now sit on the member: - The equivalence HOLDS in every implementation this repo ships. `MetadataManager.getObject` delegates to its own `get`; `createMemoryMetadata` reads the same `object` map from both; and `MetadataFacade.getObject` calls `SchemaRegistry.getObject`, which is also what `SchemaRegistry.getItem('object', ...)` — and therefore the facade's own `get` — special-cases to. Measured on the facade: the two return the identical object reference, and both answer undefined on a miss. - What the pair answers is the RUNTIME-EFFECTIVE object, not the stored document. On a SchemaRegistry-backed host the answer has been through the registry's materialization seam and carries every `extend` contribution; on a plain-store host there is no contribution layer, so effective and stored coincide. The raw per-package contributions are not reachable through this contract at all. Documentation only — no implementation changed. Nothing regenerates. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01K94yzy5CVgC2JtrqYAuDk2 --- ...data-service-getobject-effective-object.md | 37 +++++++++++++++ .../spec/src/contracts/metadata-service.ts | 45 ++++++++++++++++++- 2 files changed, 80 insertions(+), 2 deletions(-) create mode 100644 .changeset/metadata-service-getobject-effective-object.md diff --git a/.changeset/metadata-service-getobject-effective-object.md b/.changeset/metadata-service-getobject-effective-object.md new file mode 100644 index 0000000000..55371f6d49 --- /dev/null +++ b/.changeset/metadata-service-getobject-effective-object.md @@ -0,0 +1,37 @@ +--- +'@objectstack/spec': patch +--- + +**`IMetadataService.getObject` now declares what it answers with (#6505).** + +`getObject` was the only member of the convenience family without a stated +relationship to the generic reader it wraps — `getView` and `getDashboard` both +say "Equivalent to `get('view'|'dashboard', name)`", `getObject` said nothing. +Consumers paid for that silence: #6055 needed the ADR-0110 D3 verdict for the +`objectstack://objects/{objectName}` MCP resource, could not presume +`getObject(name)` and `get('object', name)` resolve the same item, and bought a +second read on the miss path rather than invent the equivalence at a consumer +(Prime Directive #12). + +Two facts are now written on the member, both measured against `main`: + +- **The equivalence holds.** Every implementation the repo ships resolves the + pair through one lookup. `MetadataManager.getObject` delegates to its own + `get`; `createMemoryMetadata` reads the same `object` map from both; and + `MetadataFacade.getObject` calls `SchemaRegistry.getObject`, which is also + what `SchemaRegistry.getItem('object', …)` — and therefore the facade's own + `get` — special-cases to. On the facade the two return the *identical object + reference*, and both answer `undefined` on a miss. +- **What the pair answers is the runtime-effective object**, not the stored + document. On a `SchemaRegistry`-backed host the answer has been through the + registry's materialization seam (system columns, primary-title designation, + protection/provenance stamping) and carries every `extend` contribution from + other packages. On a plain-store host there is no contribution layer, so the + effective object and the stored item coincide — which is exactly why a + consumer must read the answer as the effective object on both, instead of + inferring the stored one from whichever host it happens to run on. The raw + per-package contributions are not reachable through this contract at all. + +Documentation only — no implementation changed, and the doc comment ships in the +package's `.d.ts`. The effective-over-stored rule is the contract-layer +statement of the same fork #6562 rules on for the HTTP meta read surface. diff --git a/packages/spec/src/contracts/metadata-service.ts b/packages/spec/src/contracts/metadata-service.ts index a1980a6ec2..7297b20f70 100644 --- a/packages/spec/src/contracts/metadata-service.ts +++ b/packages/spec/src/contracts/metadata-service.ts @@ -327,9 +327,50 @@ export interface IMetadataService { listNames(type: string): Promise; /** - * Convenience: get an object definition by name + * Convenience: get an object definition by name. + * Equivalent to `get('object', name)` — the same relationship the optional + * members of this family declare ({@link getView}, {@link getDashboard}). + * Every implementation this repo ships resolves the pair through one + * lookup: `MetadataManager.getObject` delegates to its own `get`; + * `createMemoryMetadata` reads the same `object` map from both; and + * `MetadataFacade.getObject` calls `SchemaRegistry.getObject`, which is + * also what `SchemaRegistry.getItem('object', …)` — and therefore the + * facade's own `get` — special-cases to, so both members hand back the + * identical object. + * + * What that lookup ANSWERS is the **runtime-effective** object: the object + * as the engine runs it, not the document its author wrote. On a + * `SchemaRegistry`-backed host the difference is material. The answer is + * the owning package's definition after the registry's materialization + * seam has run — system-column injection, primary-title designation, + * protection/provenance stamping — with every `extend` contribution from + * other packages merged in (`registerObject` → `resolveObject`). An object + * authored with one field comes back carrying the audit and ownership + * columns, `organization_id` on a multi-tenant host, a resolved + * `nameField`, and the extenders' fields. On a plain-store host + * (`MetadataManager`, `createMemoryMetadata`) there is no contribution + * layer to fold, so the effective object and the stored item coincide. + * + * Read the answer as the effective object in **both** cases. A consumer + * that needs the raw stored document — provenance, diffing an authored file + * against what is installed, round-tripping an edit — must not take this + * for one, and `get('object', name)` is not the escape hatch either: on a + * registry-backed host the per-package contributions are reachable only + * through `SchemaRegistry` itself, never through this contract. + * + * `undefined` carries the same ambiguity {@link get} documents — prefer + * {@link getDiagnosed} wherever "absent" and "could not be read" would lead + * to different decisions (#5840). + * + * [#6505] Written down because the silence was being paid for: #6055 + * declined to swap this resolver for `getDiagnosed('object', name)` and + * bought a second read on the miss path instead, rather than presume an + * equivalence the contract never made (Prime Directive #12). The + * effective-over-stored half is the contract-layer statement of the fork + * #6562 rules on for the HTTP meta read surface. + * * @param name - Object name (snake_case) - * @returns The object definition, or undefined if not found + * @returns The runtime-effective object definition, or undefined if not found */ getObject(name: string): Promise;