Found while working #11061 (the importObject name-override seam). Filed rather than
fixed there: #11061's triage scoped that card to the import seam, and its own body calls
this "a much wider question than this seam". Unassigned — recording, not starting.
What was measured
MetadataManager.register(type, name, data) (packages/metadata/src/metadata-manager.ts)
applies exactly two guards before writing: assertMetadataRegisterContract (name/data
agreement, document shape, canonical type) and the persistence.writable gate. It applies
no namespace check of any kind on register('object', ...) — validateObjectNamespacePrefix
is not called, and no other name gate exists on that path.
The ADR-0028 prefix rule runs at exactly two gates today:
defineStack() / os validate — compile time (packages/spec/src/stack.zod.ts,
validateNamespacePrefix).- the publish pre-flight —
packages/metadata-protocol/src/protocol.ts
(NAMESPACE_PREFIX violations, batch-refused with the validator's own message).
metadata.register is neither, so any runtime caller that persists an object through it
bypasses the declared invariant entirely.
Live exposure, as of #11061's fix
In-tree register('object', ...) callers measured:
So after #11061 the known live exposure is closed, but the invariant remains enforced
caller-by-caller rather than at the write path itself. The next runtime feature that
persists an object via register starts un-gated by default — the same
declared-vs-enforced shape #11061 closed, one layer down.
The question to grade
Should register('object', ...) itself gate the name (making declared = enforced at the
write path), or is the gate deliberately a publish/authoring-time concern with register
kept as the low-level primitive? Considerations pulling in both directions, stated so
triage does not have to rediscover them:
- A gate at
register needs a namespace to judge against; MetadataManager has no
manifest concept (publishPackage's own doc says it indexes by packageId and cannot
prove a namespace — that is why the publish gate takes namespace as an option). - A gate at
register would also sit on the boot-time bridge writes and any rehydration
path that reaches it, where refusing a legacy stored row is a different decision than
refusing a fresh runtime write.
Related: #11061 (the import seam and its ruling that the prefix rule is declared
behavior), #10712 / PR #11059 (the draft half).
Found while working #11061 (the
importObjectname-override seam). Filed rather thanfixed there: #11061's triage scoped that card to the import seam, and its own body calls
this "a much wider question than this seam". Unassigned — recording, not starting.
What was measured
MetadataManager.register(type, name, data)(packages/metadata/src/metadata-manager.ts)applies exactly two guards before writing:
assertMetadataRegisterContract(name/dataagreement, document shape, canonical type) and the
persistence.writablegate. It appliesno namespace check of any kind on
register('object', ...)—validateObjectNamespacePrefixis not called, and no other name gate exists on that path.
The ADR-0028 prefix rule runs at exactly two gates today:
defineStack()/os validate— compile time (packages/spec/src/stack.zod.ts,validateNamespacePrefix).packages/metadata-protocol/src/protocol.ts(
NAMESPACE_PREFIXviolations, batch-refused with the validator's own message).metadata.registeris neither, so any runtime caller that persists an object through itbypasses the declared invariant entirely.
Live exposure, as of #11061's fix
In-tree
register('object', ...)callers measured:packages/services/service-datasource/src/plugin.ts(persistObjectforimportObject) — was the live bypass;importObject'sopts.nameoverride bypasses the namespace prefix and persists an unprefixed federated object #11061's PR closes it at the service seam,not at
register.packages/objectql/src/plugin.ts(two boot-time bridge writes, SchemaRegistry tometadata service) — objects that already passed
defineStack, so no live violationminted there.
So after #11061 the known live exposure is closed, but the invariant remains enforced
caller-by-caller rather than at the write path itself. The next runtime feature that
persists an object via
registerstarts un-gated by default — the samedeclared-vs-enforced shape #11061 closed, one layer down.
The question to grade
Should
register('object', ...)itself gate the name (making declared = enforced at thewrite path), or is the gate deliberately a publish/authoring-time concern with
registerkept as the low-level primitive? Considerations pulling in both directions, stated so
triage does not have to rediscover them:
registerneeds a namespace to judge against;MetadataManagerhas nomanifest concept (
publishPackage's own doc says it indexes bypackageIdand cannotprove a namespace — that is why the publish gate takes
namespaceas an option).registerwould also sit on the boot-time bridge writes and any rehydrationpath that reaches it, where refusing a legacy stored row is a different decision than
refusing a fresh runtime write.
Related: #11061 (the import seam and its ruling that the prefix rule is declared
behavior), #10712 / PR #11059 (the draft half).