Found while wiring #3903 (stored-metadata conversion, PR #4317). Filed per Prime Directive #10 rather than widening that PR.
What happens
getMetaItem / getMetaItems decorate every served item with _diagnostics (decorateMetadataItem, metadata-diagnostics.ts), and draft reads add _draft: true. saveMetaItem deliberately persists the request body verbatim (ADR-0005 §Validation — parsed.data would strip Studio-only auxiliary fields), and it strips neither decoration. So the standard Studio round-trip
GET (body + _diagnostics) → edit → PUT the whole body → persisted verbatim
writes the stale read-time verdict into the stored row: into sys_metadata.metadata, into the checksum, into sys_metadata_history diffs.
Verified against the built spec: the type schemas are strip-mode, so the save gate does not reject the key (view/object/action all safeParse successfully with _diagnostics present) — it just persists.
Why it stays invisible (and why it still matters)
- Reads always recompute:
decorateMetadataItem spreads { ...item, _diagnostics }, so the fresh verdict shadows the persisted stale one — users never see wrong diagnostics.
- But the row body, its checksum, and every history diff carry diagnostic noise; a body-equality or checksum comparison across a GET→PUT round-trip is polluted; and
computeMetadataDiagnostics already has to strip its own key defensively before validating (metadata-diagnostics.ts stripDiagnostics) — the write path never got the matching half.
Suggested fix
saveMetaItem strips the read-only decorations (_diagnostics, and _draft unless the write is itself the draft-mode marker) from request.item before the destructive-change diff / validation / persistence — mirroring the existing layered-envelope rejection, but as a silent strip since these keys are our own decoration, not author data. One place, both decorations, plus a pin test that a GET→PUT round-trip persists a byte-identical body.
Refs #3903, PR #4317, ADR-0005 §Validation.
Found while wiring #3903 (stored-metadata conversion, PR #4317). Filed per Prime Directive #10 rather than widening that PR.
What happens
getMetaItem/getMetaItemsdecorate every served item with_diagnostics(decorateMetadataItem, metadata-diagnostics.ts), and draft reads add_draft: true.saveMetaItemdeliberately persists the request body verbatim (ADR-0005 §Validation —parsed.datawould strip Studio-only auxiliary fields), and it strips neither decoration. So the standard Studio round-tripwrites the stale read-time verdict into the stored row: into
sys_metadata.metadata, into the checksum, intosys_metadata_historydiffs.Verified against the built spec: the type schemas are strip-mode, so the save gate does not reject the key (
view/object/actionallsafeParsesuccessfully with_diagnosticspresent) — it just persists.Why it stays invisible (and why it still matters)
decorateMetadataItemspreads{ ...item, _diagnostics }, so the fresh verdict shadows the persisted stale one — users never see wrong diagnostics.computeMetadataDiagnosticsalready has to strip its own key defensively before validating (metadata-diagnostics.tsstripDiagnostics) — the write path never got the matching half.Suggested fix
saveMetaItemstrips the read-only decorations (_diagnostics, and_draftunless the write is itself the draft-mode marker) fromrequest.itembefore the destructive-change diff / validation / persistence — mirroring the existing layered-envelope rejection, but as a silent strip since these keys are our own decoration, not author data. One place, both decorations, plus a pin test that a GET→PUT round-trip persists a byte-identical body.Refs #3903, PR #4317, ADR-0005 §Validation.