fix(metadata-protocol,rest): one seam for flow canonicalization — duplicatePackage (#4498) + an admin route for --stored (#4327) - #4504
Merged
Conversation
…plicatePackage` (#4498) and an admin route for `--stored` (#4327) `duplicatePackage` promised "duplication never mints new rows in a pre-protocol dialect" and delivered it through `convertStoredItem`, which returns `flow` bodies untouched. `FlowNodeSchema.config` is an open `z.record`, so a pre-17 body sailed through `saveMetaItem`'s gate and landed verbatim in a brand-new row — making ADR-0087's "strictly shrinking" premise false for flows: run the migration, get a clean report, duplicate a package, and the population is back. The capability was already reachable. The protocol is constructed with an accessor for the kernel's service table (the same one `analytics` and `package` are read from) and the automation service registers under `automation`, so one private `resolveFlowCanonicalizer` serves every caller running next to a live engine: - `duplicatePackage` canonicalizes flow rows through it. A refused rename fails the item into the existing `failed[]` naming the token; a flow that cannot canonicalize fails the same way; with no engine reachable the source body is copied as-is. - `migrateStoredMetadata`'s `canonicalizeFlow` defaults to it, so the CLI stopped passing one — it booted the inert engine into the same kernel, so both routes reached the same instance. - `POST /meta/_migrate-stored` therefore needs no hook at all: gated on `manage_metadata`, preview unless `apply` is literally `true`, attributed to the caller, mounted on both the REST server and the runtime dispatcher and ledgered in both, plus `client.meta.migrateStored()`. Operators without shell access finally have the finish line the CLI form gives everyone else. Resolution is lazy per call: plugin init order does not guarantee `automation` is in the table when the protocol is assembled, and caching `undefined` from a too-early read would disable flow canonicalization for the process. An integration test boots the real CLI stack against a real database, seeds a pre-17 flow row, and asserts the rewrite lands with no hook threaded — plus the negative, that dropping the automation plugin reports `skipped` with the reason rather than counting the row done. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WoZPKPDqJ7WB7z84xk9y3f
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckThis PR changes 5 package(s): 41 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
…or metadata calls The SDK reference's `client.meta` sample covers the governance/lifecycle family (publish, rollback, diff, diagnostics, audit); the new stored-row canonicalization call belongs in the same neighbourhood. The full behaviour — capability gate, preview posture, the CLI's `--stored` equivalent — stays in `deployment/cli.mdx` rather than being duplicated here. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WoZPKPDqJ7WB7z84xk9y3f
…s lookups
The new stored-flow integration test reached the engine via
`const ql: any = stack.kernel.getService('objectql')`, which the `slot-lookup`
rule refuses — and correctly: `: any` switches off checking for every `ql.*`
call below it while reading identically to code that has it. `SchemaStack.kernel`
is untyped, so a type argument is a TS2347; the contract is stated on the
RESULT instead, via one `engineOf()` helper the two tests share.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WoZPKPDqJ7WB7z84xk9y3f
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #4498. Completes the #4327 / #4454 line by giving the stored-metadata migration a server-side entry point.
The bug (#4498)
duplicatePackagecanonicalizes each source row before re-saving, under a stated guarantee:convertStoredItemopens withif (singular === 'flow') return { item: data, notices: [] }. So for flows the guarantee was not delivered — and it did not fail loudly either:FlowNodeSchema.configis an openz.record, so a pre-17 body (adelete_recordcarryingconfig.filters) sails throughsaveMetaItem's schema gate and lands verbatim in a brand-new row.That falsifies the premise ADR-0087 rests the whole stored-metadata design on — "new rows are always canonical, so the stored pass is a strictly shrinking concern". An operator could run
os migrate meta --stored --apply, get a clean report, duplicate a package, and be back to having pre-protocol rows, with the report still saying protocol N until the next run.The fix: the wiring already existed
The reason for the flow skip is real — flow-node conversions carry ADR-0078's open-namespace conflict guard, which needs the automation engine's live executor registry to tell a rename from a clobber. #4454 built that capability (
AutomationEngine.canonicalizeStoredFlow) and handed it tomigrateStoredMetadataas an explicit hook, because the CLI has to boot an engine of its own to hold one.But inside a server there is nothing to thread: the protocol is constructed with an accessor for the kernel's service table — the same one
analyticsandpackageare already read from — and the automation service registers underautomation. One privateresolveFlowCanonicalizerreadscanonicalizeStoredFlowoff it, and three call sites share it:duplicatePackagefailed[]naming the tokenmigrateStoredMetadatacanonicalizeFlowrequired from the callerPOST /meta/_migrate-storedResolution is lazy, per call. Plugin init order does not guarantee
automationis in the table when the protocol is assembled (the CLI'sbuildDataMigrationPluginsadds it after ObjectQL by design), so cachingundefinedfrom a too-early read would silently disable flow canonicalization for the life of the process.The CLI stopped passing its own hook. It boots the inert engine into the same kernel, so both routes reached the same instance — two routes to one capability is how they drift.
Failure posture
duplicatePackage's existingfailed[]naming the token, consistent with [P3]os migrate meta --storedskips flow rows — the finish line stops short of the one type that needs the executor registry #4454'sfailedoutcome. Copying the un-renamed body would mint exactly the row this fixes.FlowSchemais strict since 未知键静默剥离仍是全仓默认:把 #3405 的 strict 收紧从一个 schema 推广到整个可授权面(ADR-0078 完整性闸门) #4001) → same, with the parse message.Two smaller honesty fixes ride along: a source item that fails conversion (a tombstoned key throws) is reported as such rather than as
unparseable metadata, and the "no engine" skip reason says no automation service is reachable rather than blaming the caller for not supplying one.The admin route (#4327's remaining gap)
os migrate meta --storedneeds shell access to the deployment's database. A hosted operator has none, so on a managed deployment ADR-0087's chain had no finish line — only the per-read conversion, running forever, with no way to assert what protocol the rows are on.applymust be literallytrue; an empty body, a missing body and"apply": "yes"all preview.manage_metadata(ADR-0066 D1) rather than on merely holding a session, because unlike the single-itemPUT /meta/:type/:namenext door it rewrites every eligible row in the deployment. The gate runs before the protocol is probed, so an unauthorized caller cannot use403-vs-501to learn which kernels can be migrated./meta's anonymous-deny umbrella still closes it first./:typeso the leading-underscore segment is never read as a metadata type name.Flows need no extra setup on this path — the server already holds a live engine.
Reads were deliberately not changed
getMetaItems/getMetaItem/getMetaItemLayered/loadMetaFromDbstill skip flows. They are reads, covered byregisterFlowcanonicalizing at execution, and are not producing bad data. Duplication was the one that writes. The resolver is the seam they would adopt if that changes.Verification
The riskiest change here is removing the CLI's explicit hook — every flag unit test still passes if the protocol silently fails to find the engine, and the only symptom is flow rows quietly reporting
skippedagain. So that one is pinned by an integration test against the real stack:bootSchemaStack+buildDataMigrationPlugins({ automation: true }), a pre-17 flow row seeded intosys_metadata, no hook threaded, asserting the rewrite lands on disk, that no schema defaults were persisted, and that a second pass reports the row canonical. Plus the negative — dropping the automation plugin reportsskippedwith the reason rather than counting the row done.packages/metadata-protocol/src/protocol.flow-canonicalizer.test.ts— 14 tests (resolver +duplicatePackage)packages/rest/src/rest-meta-migrate-stored.test.ts— 15 tests (mount order, gate, preview posture)packages/runtime/src/domains/meta-migrate-stored.test.ts— 13 testspackages/cli/src/commands/migrate/meta.stored-flow-resolution.integration.test.ts— 2 integration testsFull suites green: metadata-protocol 177, runtime 1039, rest 553, cli 694, objectql 1534, client 204.
eslint,typecheck, and theroute-envelope/slot-lookup/error-code-casing/wildcard-fallthrough/doc-authoringgates all pass.Docs: an ADR-0087 addendum recording that "strictly shrinking" was false and how it is restored, and a
content/docs/deployment/cli.mdxsection for the HTTP form. Two changesets.🤖 Generated with Claude Code
https://claude.ai/code/session_01WoZPKPDqJ7WB7z84xk9y3f
Generated by Claude Code