You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Found while retiring the three deprecated aliases (#3855 / #3883). Filed per Prime Directive #10 rather than widening that PR.
The shape of it
Every mechanism this platform has for evolving the metadata contract — the ActionSchema/FieldSchema transforms, the ADR-0087 D2 conversion layer, the D3 migration chain, the #3883 tombstones, the #3895 authorable-surface ratchet — operates on authored source. None of it reaches metadata stored in sys_metadata, which is a first-class authoring path: object is allowRuntimeCreate: true (metadata-plugin.zod.ts:599) and Studio's build surface writes there.
Four things I verified rather than assumed:
1. Stored rows are rehydrated without parsing.plugin.ts:1422 reads sys_metadata rows with type: 'object', pulls their embedded actions, and hands each one straight to runner(action) at :1549. No ActionSchema.parse, no normalization, no conversion.
2. The conversion layer has exactly one consumer outside spec.applyConversionsToFlow in services/service-automation/src/engine.ts:1453 — the registerFlow rehydration seam PD #12 already names. Flows only. Nothing in objectql or metadata converts on the stored-metadata read path.
3. Where ObjectSchema.parsedoes run on the registration path, its result is discarded.registry.ts:1099:
try{this.validate(type,item);// → ObjectSchema.parse(item) for type === 'object'}catch(e: any){console.error(`[Registry] Validation failed for ${type}${baseName}: ${e.message}`);}
Registration proceeds regardless. So a schema rejection on this path is a log line, not a gate.
4. os migrate meta rewrites sources, not rows. It reads the config module and writes a migrated stack; nothing walks sys_metadata.
What follows
No regression from #3883 — worth stating plainly, because it was my first worry and it turned out to be wrong. The alias was never lowered on the stored path either (that path never parsed), so nothing that worked stopped working. The no runtime behaviour changes claim in that changeset holds.
But the two contracts have silently diverged, and have for a long time. On the authored path an alias is lowered, then removed, then rejected with a prescription. On the stored path it is whatever an ad-hoc reader happens to do with it. Today that is exactly two hand-rolled accommodations:
Surface
Stored-path handling
flow node config
applyConversionsToFlow — the real conversion layer, wired in by hand at one seam
field.conditionalRequired
a bare ?? in objectql/src/validation/rule-validator.ts
everything else
nothing
And the ?? cannot be retired on a schedule. I kept it deliberately in #3883 and wrote that it "retires when that stored metadata is migrated". Having now looked: there is no mechanism that would ever migrate it. As written it is permanent, and it is precisely the form PD #12 names as debt — a bare ?? rather than a declared, warned, lintable shim. Unlike the six in #3796 it cannot be fixed by warning the author, because after protocol 17 there is no author: the key is unwritable, and the only thing that still carries it is data at rest.
The #3883 tombstones are audible to authors and inaudible to the runtime. By design they surface as a tsc error and a parse error. On the registration path the parse error is swallowed at registry.ts:1099, so the same removal that stops an author cold passes a stored row through with a console.error.
The decision this needs
Not something to pick unilaterally — it is a question about how far the protocol contract is meant to extend:
Convert on the stored read path. Run applyConversions where sys_metadata rows are rehydrated, the way service-automation already does for flows. Makes conversions universal and gives the ?? an actual expiry. Cost: a conversion pass on every metadata load, and a decision about what a retired conversion means for data at rest (a row written under 14 may outlive its window).
Declare stored metadata out of scope and say so where it can be read: the accommodations stay permanent by policy, PD Add comprehensive test suite for Zod schema validation #12 gets an explicit carve-out for data at rest, and rule-validator.ts's comment stops promising a retirement that will not come.
Whichever way it goes, registry.ts:1099 swallowing a validation failure deserves its own answer — a rejection that only reaches console.error is the "declared ≠ enforced" pattern this repo keeps closing everywhere else.
Found while retiring the three deprecated aliases (#3855 / #3883). Filed per Prime Directive #10 rather than widening that PR.
The shape of it
Every mechanism this platform has for evolving the metadata contract — the
ActionSchema/FieldSchematransforms, the ADR-0087 D2 conversion layer, the D3 migration chain, the #3883 tombstones, the #3895 authorable-surface ratchet — operates on authored source. None of it reaches metadata stored insys_metadata, which is a first-class authoring path:objectisallowRuntimeCreate: true(metadata-plugin.zod.ts:599) and Studio's build surface writes there.Four things I verified rather than assumed:
1. Stored rows are rehydrated without parsing.
plugin.ts:1422readssys_metadatarows withtype: 'object', pulls their embeddedactions, and hands each one straight torunner(action)at:1549. NoActionSchema.parse, no normalization, no conversion.2. The conversion layer has exactly one consumer outside
spec.applyConversionsToFlowinservices/service-automation/src/engine.ts:1453— theregisterFlowrehydration seam PD #12 already names. Flows only. Nothing inobjectqlormetadataconverts on the stored-metadata read path.3. Where
ObjectSchema.parsedoes run on the registration path, its result is discarded.registry.ts:1099:Registration proceeds regardless. So a schema rejection on this path is a log line, not a gate.
4.
os migrate metarewrites sources, not rows. It reads the config module and writes a migrated stack; nothing walkssys_metadata.What follows
No regression from #3883 — worth stating plainly, because it was my first worry and it turned out to be wrong. The alias was never lowered on the stored path either (that path never parsed), so nothing that worked stopped working. The
no runtime behaviour changesclaim in that changeset holds.But the two contracts have silently diverged, and have for a long time. On the authored path an alias is lowered, then removed, then rejected with a prescription. On the stored path it is whatever an ad-hoc reader happens to do with it. Today that is exactly two hand-rolled accommodations:
applyConversionsToFlow— the real conversion layer, wired in by hand at one seamfield.conditionalRequired??inobjectql/src/validation/rule-validator.tsAnd the
??cannot be retired on a schedule. I kept it deliberately in #3883 and wrote that it "retires when that stored metadata is migrated". Having now looked: there is no mechanism that would ever migrate it. As written it is permanent, and it is precisely the form PD #12 names as debt — a bare??rather than a declared, warned, lintable shim. Unlike the six in #3796 it cannot be fixed by warning the author, because after protocol 17 there is no author: the key is unwritable, and the only thing that still carries it is data at rest.The #3883 tombstones are audible to authors and inaudible to the runtime. By design they surface as a
tscerror and a parse error. On the registration path the parse error is swallowed atregistry.ts:1099, so the same removal that stops an author cold passes a stored row through with aconsole.error.The decision this needs
Not something to pick unilaterally — it is a question about how far the protocol contract is meant to extend:
applyConversionswheresys_metadatarows are rehydrated, the way service-automation already does for flows. Makes conversions universal and gives the??an actual expiry. Cost: a conversion pass on every metadata load, and a decision about what a retired conversion means for data at rest (a row written under 14 may outlive its window).os migrate metaa stored mode (--stored/ an admin route) that rewrites rows in place. Keeps the load path cheap and makes migration an explicit operator action — but only helps deployments that run it, which is the thing [P3] Retire the three deprecated aliases — via the ADR-0087 D2 conversion layer, not by deleting the keys #3855 concluded we cannot rely on.rule-validator.ts's comment stops promising a retirement that will not come.Whichever way it goes,
registry.ts:1099swallowing a validation failure deserves its own answer — a rejection that only reachesconsole.erroris the "declared ≠ enforced" pattern this repo keeps closing everywhere else.Refs #3713, #3742, #3754, #3796, #3855, #3883, #3895, ADR-0087 D2/D3, AGENTS.md PD #12.