Found while implementing #13913 (getViewsByObject() container expansion). Filed unassigned; not fixed in that PR — different function, different failure path, and a repair here changes list() for every metadata type and every consumer, which is a materially larger, separately-scoped change.
The defect
MetadataManager.readListUncached() (packages/metadata/src/metadata-manager.ts) admits a loader-supplied item only if its stored body carries a string name:
for(constitemofloaderItems){constitemAny=itemasany;if(itemAny&&typeofitemAny.name==='string'&&!items.has(itemAny.name)){items.set(itemAny.name,item);}}An aggregated defineView container has no own name by design. plugin.ts says so in its own words: "Most metadata items carry a top-level name. The View container (UI namespace) is an exception: it has no own name — its identity is the target object". Its identity lives in the row's name COLUMN, and DatabaseLoader.rowToData() returns the parsed metadata payload without folding the column into the body.
⇒ A container persisted by MetadataManager.register('view', OBJECTNAME, container) — which stores the body verbatim through loader.save(type, name, data) — is registered in the in-memory registry AND written to sys_metadata, but on the next process start, when the registry is cold and only the loader answers, it is dropped from list('view') entirely.
Not scoped to views: any loader-held metadata whose body has no top-level name is invisible to list(), and listDiagnosed() reports the set as complete — degraded is false, because no loader threw. A short answer presented as a full one.
Measured
Positive control first, on the #13913 branch at the pin
packages/metadata/src/metadata-manager-views-by-object-container.test.ts, case "reaches a container that arrived through a LOADER, not only the registry":
- stored body
{ name: 'crm_lead', ...container } → green, 3 expanded ViewItems. - same case, the single token
name: 'crm_lead', removed from the stored body, nothing else changed (mutation confirmed on disk by marker counts and a git hash-object diff against the HEAD blob; restored and re-proved after) → red, expected [] to deeply equal [ 'crm_lead.default', 'crm_lead.edit', 'crm_lead.pipeline' ].
So the container reaches getViewsByObject() only when its stored body happens to repeat the name. #13913's repair is correct and complete for what it claims — it expands whatever list('view') holds — but it cannot see a row list() never admitted.
Why it is not obviously a one-liner
Folding the row's name column into the body inside DatabaseLoader.rowToData(), or keying loader items by the row rather than by body.name in readListUncached(), both change what every list() consumer receives. The register contract (assertMetadataRegisterContract) also has a stated position on a data.name that disagrees with the name argument, so synthesising a name into bodies that deliberately have none needs a ruling, not a patch. Contract-first: decide where identity for a nameless body lives, then implement.
Related: #13913 (the exit that surfaced it), #7736, #13407.
Generated by Claude Code
Found while implementing #13913 (
getViewsByObject()container expansion). Filed unassigned; not fixed in that PR — different function, different failure path, and a repair here changeslist()for every metadata type and every consumer, which is a materially larger, separately-scoped change.The defect
MetadataManager.readListUncached()(packages/metadata/src/metadata-manager.ts) admits a loader-supplied item only if its stored body carries a stringname:An aggregated
defineViewcontainer has no ownnameby design.plugin.tssays so in its own words: "Most metadata items carry a top-levelname. TheViewcontainer (UI namespace) is an exception: it has no ownname— its identity is the target object". Its identity lives in the row'snameCOLUMN, andDatabaseLoader.rowToData()returns the parsedmetadatapayload without folding the column into the body.⇒ A container persisted by
MetadataManager.register('view', OBJECTNAME, container)— which stores the body verbatim throughloader.save(type, name, data)— is registered in the in-memory registry AND written tosys_metadata, but on the next process start, when the registry is cold and only the loader answers, it is dropped fromlist('view')entirely.Not scoped to views: any loader-held metadata whose body has no top-level
nameis invisible tolist(), andlistDiagnosed()reports the set as complete —degradedis false, because no loader threw. A short answer presented as a full one.Measured
Positive control first, on the #13913 branch at the pin
packages/metadata/src/metadata-manager-views-by-object-container.test.ts, case "reaches a container that arrived through a LOADER, not only the registry":{ name: 'crm_lead', ...container }→ green, 3 expanded ViewItems.name: 'crm_lead',removed from the stored body, nothing else changed (mutation confirmed on disk by marker counts and agit hash-objectdiff against the HEAD blob; restored and re-proved after) → red,expected [] to deeply equal [ 'crm_lead.default', 'crm_lead.edit', 'crm_lead.pipeline' ].So the container reaches
getViewsByObject()only when its stored body happens to repeat the name. #13913's repair is correct and complete for what it claims — it expands whateverlist('view')holds — but it cannot see a rowlist()never admitted.Why it is not obviously a one-liner
Folding the row's
namecolumn into the body insideDatabaseLoader.rowToData(), or keying loader items by the row rather than bybody.nameinreadListUncached(), both change what everylist()consumer receives. The register contract (assertMetadataRegisterContract) also has a stated position on adata.namethat disagrees with the name argument, so synthesising anameinto bodies that deliberately have none needs a ruling, not a patch. Contract-first: decide where identity for a nameless body lives, then implement.Related: #13913 (the exit that surfaced it), #7736, #13407.
Generated by Claude Code