Skip to content

Commit 75fd301

Browse files
os-zhuangclaude
andauthored
fix(metadata-protocol,objectql): the /meta read exits materialize a served base the way the registry materializes its own (#8268) (#8380)
* wip(#8268): shared materialization seam at the /meta read exits * test(rest): class-level materialization agreement pin (#8268) * fix(metadata-protocol,objectql): the /meta read exits materialize a served base the way the registry does (#8268) * test(rest): reference the two filed findings (#8375, #8376) from the pins that measure them * test(objectql): pin the A-vs-C strip divergence as an expected gap (#8381) --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent edff010 commit 75fd301

5 files changed

Lines changed: 1081 additions & 60 deletions

File tree

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
---
2+
"@objectstack/objectql": patch
3+
"@objectstack/metadata-protocol": patch
4+
---
5+
6+
`/meta` object reads now materialize a served base the way the registry materializes its own
7+
8+
`GET /meta/object/:name` served `nameField: undefined` for an object whose by-name read is
9+
answered from the `metadata` service (an artifact-booted deployment), while `GET /meta/object`
10+
and the registry's own resolved schema served the ADR-0079 designation for the same object at
11+
the same moment. Every title-rendering decision derived from the by-name answer — forms, record
12+
headers, lookup labels — was made against a document the platform itself did not agree with.
13+
14+
The cause was structural rather than specific to `nameField`. A `/meta` object read resolves
15+
`sys_metadata` overlay to MetadataService to SchemaRegistry, and only the last of those three has
16+
been through the registry's object-materialization seam; each convergence installed at the read
17+
exits so far reached for ONE named stamp, so each further stamp arrived as a further bug report
18+
(injected system columns, then the `__search` companion, then this).
19+
20+
`SchemaRegistry.registerObject`'s materialization block is now a single method, and
21+
`materializeServedObjectOnto` replays that same code onto a body that never came through
22+
`registerObject`. The `/meta` read exits ask for the whole seam instead of naming one stamp, so a
23+
stamp added to the block converges on served documents the day it is added. The convergence
24+
withholds a title designation the registry itself declined, so it can only move a served copy onto
25+
the registry's answer and never manufacture one.

packages/metadata-protocol/src/protocol.ts

Lines changed: 88 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -4261,97 +4261,138 @@ export class ObjectStackProtocolImplementation implements
42614261
}
42624262

42634263
/**
4264-
* [#8038] The `__search` half of {@link governServedItem}'s presence
4265-
* convergence — the third stamp in the same family, and the one the
4266-
* module-level function cannot carry on its own.
4264+
* [#8268, generalising #8038] The REGISTRY-SIDE half of
4265+
* {@link governServedItem}'s presence convergence: replay the registry's
4266+
* object-materialization seam onto the served body.
42674267
*
42684268
* `applyInjectedSystemColumns` (#6562) converges the columns whose
42694269
* membership is a pure function of the document
42704270
* (`resolveInjectedSystemColumns`), so it needs nothing but the body. The
4271-
* search companion is DEPLOYMENT-gated: `SchemaRegistry` provisions it at
4272-
* the object-materialization seam only when its own `searchCompanion` flag
4271+
* materialization stamps are not like that. `__search` is DEPLOYMENT-gated
4272+
* (`SchemaRegistry` provisions it only when its own `searchCompanion` flag
42734273
* is on, and that flag is `options.searchCompanion ??
4274-
* resolveSearchPinyinEnabled()` — a host may set it explicitly. So the
4275-
* answer has to come from the registry that made the decision, which is why
4276-
* this is a method here and a `provisionSearchCompanionOnto` there, exactly
4277-
* as {@link foldObjectExtendersFromRegistry} is a method here and a
4274+
* resolveSearchPinyinEnabled()`, which a host may set explicitly), and the
4275+
* ADR-0079 `nameField` designation must match the answer THAT registry
4276+
* reached over THAT object's contributors. So the answer has to come from
4277+
* the registry that made the decision, which is why this is a method here
4278+
* and a `materializeServedObjectOnto` there, exactly as
4279+
* {@link foldObjectExtendersFromRegistry} is a method here and a
42784280
* `foldObjectExtendersOnto` there (#7556).
42794281
*
4282+
* ⛔ It asks for the WHOLE seam, deliberately, and this is the point of
4283+
* #8268 rather than an implementation detail. Three stamps of one seam —
4284+
* #6562's injected system columns, #8038's `__search`, #8268's `nameField`
4285+
* — diverged on this exact read path and were found, filed and fixed ONE AT
4286+
* A TIME, because each convergence reached for one named stamp. A stamp
4287+
* added to `materializeBaseLayer` now arrives here already converged, with
4288+
* no fourth method and no fourth card.
4289+
*
42804290
* Applied AFTER `governServedItem` at each exit, because the registry
4281-
* provisions the companion after `applySystemFields` too: the source field
4282-
* is resolved by `resolveDisplayField` over the POST-injection field set,
4283-
* so injecting first is what makes this pass and the registry's own answer
4284-
* the same answer rather than two independent guesses.
4291+
* materializes after `applySystemFields` too: the title is resolved by
4292+
* `resolveDisplayField` over the POST-injection field set, so injecting
4293+
* first is what makes this pass and the registry's own answer the same
4294+
* answer rather than two independent guesses.
42854295
*
42864296
* No-op for every type but `object`, and — via the registry — for a
42874297
* deployment with companions off, an object with no eligible display field,
4288-
* and a body that already carries the column (every registry-backed read,
4298+
* and a body already carrying every stamp (every registry-backed read,
42894299
* which is the majority path this converges the minority onto).
42904300
*/
4291-
private provisionSearchCompanionFromRegistry<T>(type: string, body: T): T {
4301+
private materializeFromRegistry<T>(type: string, body: T): T {
42924302
if (canonicalMetaType(type) !== 'object') return body;
42934303
if (body === null || typeof body !== 'object') return body;
42944304
const registry = (this.engine as any)?.registry;
42954305
// Partial registry doubles in tests predate this method; a host that
4296-
// cannot answer the gate answers exactly as it did before.
4297-
if (!registry || typeof registry.provisionSearchCompanionOnto !== 'function') return body;
4298-
try {
4299-
return registry.provisionSearchCompanionOnto(body) as T;
4300-
} catch {
4301-
// A read over an in-memory flag and the body's own fields; a failure
4302-
// here must not turn a served schema into a 5xx.
4303-
return body;
4306+
// cannot answer the gate answers exactly as it did before. The
4307+
// `provisionSearchCompanionOnto` fallback keeps a double that was
4308+
// written against #8038's narrower seam serving what it served then,
4309+
// rather than silently losing the companion convergence to a rename.
4310+
if (registry && typeof registry.materializeServedObjectOnto === 'function') {
4311+
try {
4312+
return registry.materializeServedObjectOnto(body) as T;
4313+
} catch {
4314+
// A read over an in-memory flag, the registry's own resolved
4315+
// answer and the body's own fields; a failure here must not turn
4316+
// a served schema into a 5xx.
4317+
return body;
4318+
}
4319+
}
4320+
if (registry && typeof registry.provisionSearchCompanionOnto === 'function') {
4321+
try {
4322+
return registry.provisionSearchCompanionOnto(body) as T;
4323+
} catch {
4324+
return body;
4325+
}
43044326
}
4327+
return body;
43054328
}
43064329

43074330
/**
4308-
* {@link governServedItem} plus the deployment-gated half it cannot reach
4309-
* ({@link provisionSearchCompanionFromRegistry}) — the whole of what a
4310-
* `/meta` READ EXIT owes an object document. Every call site of the free
4311-
* function that is a read exit goes through this instead; the one call site
4312-
* that is not — `getMetaItemLayered`'s `code` / `overlay` layers, which are
4313-
* deliberately raw — never called it in the first place (#6562 ruling
4314-
* constraint 1, #7556's boundary).
4331+
* {@link governServedItem} plus the registry-gated half it cannot reach
4332+
* ({@link materializeFromRegistry}) — the whole of what a `/meta` READ EXIT
4333+
* owes an object document. Every call site of the free function that is a
4334+
* read exit goes through this instead; the one call site that is not —
4335+
* `getMetaItemLayered`'s `code` / `overlay` layers, which are deliberately
4336+
* raw — never called it in the first place (#6562 ruling constraint 1,
4337+
* #7556's boundary).
43154338
*/
43164339
private governServedObject<T>(type: string, item: T): T {
4317-
return this.provisionSearchCompanionFromRegistry(type, governServedItem(type, item));
4340+
return this.materializeFromRegistry(type, governServedItem(type, item));
43184341
}
43194342

43204343
/**
4321-
* [#8038] The write-side counterpart of
4322-
* {@link provisionSearchCompanionFromRegistry}, owed for the reason
4344+
* [#8268, generalising #8038] The write-side counterpart of
4345+
* {@link materializeFromRegistry}, owed for the reason
43234346
* {@link stripServedSystemColumns} is owed one field family over: the write
43244347
* path persists the request body verbatim (ADR-0005 §Validation), so a
4325-
* document this service's read added the companion to would otherwise be
4326-
* handed straight back and stored carrying it.
4348+
* document this service's read added a stamp to would otherwise be handed
4349+
* straight back and stored carrying it.
43274350
*
43284351
* Measured on the runtime-created object path — the write door type
43294352
* `object` has open by default, an artifact-backed object refusing the save
43304353
* outright with `NOT_OVERRIDABLE` — the stored row went from
43314354
* `fields: [name]` to `fields: [__search, name]` on a single GET → PUT
4332-
* before this was added.
4355+
* before #8038 added the companion half, and gained a `nameField` the
4356+
* author never wrote on the same round trip before #8268 added the title
4357+
* half. The landed `#4326` round-trip pin catches either omission, which is
4358+
* why the read half and this half must move together.
4359+
*
4360+
* ⛔ Asks the registry for the WHOLE seam's inverse rather than for one
4361+
* named stamp — the same reason {@link materializeFromRegistry} does.
43334362
*/
4334-
private stripSearchCompanionFromRegistry<T>(type: string, item: T): T {
4363+
private stripMaterializedFromRegistry<T>(type: string, item: T): T {
43354364
if (canonicalMetaType(type) !== 'object') return item;
43364365
if (item === null || typeof item !== 'object') return item;
43374366
const registry = (this.engine as any)?.registry;
4338-
if (!registry || typeof registry.stripProvisionedSearchCompanionFrom !== 'function') return item;
4339-
try {
4340-
return registry.stripProvisionedSearchCompanionFrom(item) as T;
4341-
} catch {
4342-
// A read over the body's own fields; a failure here must not turn a
4343-
// save into a 5xx.
4344-
return item;
4367+
// Partial registry doubles in tests predate this method; the narrower
4368+
// #8038 spelling keeps such a host stripping what it stripped before
4369+
// rather than silently losing the companion strip to a rename.
4370+
if (registry && typeof registry.stripMaterializedStampsFrom === 'function') {
4371+
try {
4372+
return registry.stripMaterializedStampsFrom(item) as T;
4373+
} catch {
4374+
// A read over the body's own fields; a failure here must not
4375+
// turn a save into a 5xx.
4376+
return item;
4377+
}
43454378
}
4379+
if (registry && typeof registry.stripProvisionedSearchCompanionFrom === 'function') {
4380+
try {
4381+
return registry.stripProvisionedSearchCompanionFrom(item) as T;
4382+
} catch {
4383+
return item;
4384+
}
4385+
}
4386+
return item;
43464387
}
43474388

43484389
/**
4349-
* {@link stripServedSystemColumns} plus the companion half
4350-
* ({@link stripSearchCompanionFromRegistry}) — the whole of what the write
4390+
* {@link stripServedSystemColumns} plus the registry-seam half
4391+
* ({@link stripMaterializedFromRegistry}) — the whole of what the write
43514392
* path owes {@link governServedObject}.
43524393
*/
43534394
private stripServedObjectColumns<T>(type: string, item: T): T {
4354-
return this.stripSearchCompanionFromRegistry(type, stripServedSystemColumns(type, item));
4395+
return this.stripMaterializedFromRegistry(type, stripServedSystemColumns(type, item));
43554396
}
43564397

43574398
/**

0 commit comments

Comments
 (0)