Skip to content

Commit 751ddb8

Browse files
committed
docs(spec): retry-policy-converged stays for the job default flip alone; the save-door fallback reaches the canonicalizer's verdict for every retired flow shape, pinned on flow.active
Claude-Session: https://claude.ai/code/session_01D6F9axfoPzR7t8G1J7Ytzz Co-authored-by: Claude <noreply@anthropic.com>
1 parent 9dd24f8 commit 751ddb8

3 files changed

Lines changed: 34 additions & 18 deletions

File tree

‎.changeset/23487-retire-overdue-conversions.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ Run `os migrate meta --from 16` to list the mechanical edits for existing source
2222

2323
Stored metadata needs no edit. The automation engine's flow rehydration and stored-row rehydration still replay the whole conversion chain, these seven entries included, so `sys_metadata` flows and sharing-rule rows written under an old spelling keep loading in canonical form. A built artifact whose declared protocol floor is at or below `17.7.0` is also still converted at the artifact-ingestion door.
2424

25-
The metadata save door's flow fallback keeps its verdict: when no automation service is reachable (or its canonicalizer throws), `saveMetaItem` judges a flow body after replaying the same conversion chain the automation engine's canonicalizer replays, retired entries included, so a host's verdict on a saved flow still does not depend on whether it runs an automation service.
25+
The metadata save door's flow fallback now reaches the canonicalizer's verdict for every retired flow shape. When no automation service is reachable (or its canonicalizer throws), `saveMetaItem` judges a flow body after replaying the full conversion chain the automation engine's canonicalizer replays, retired entries included (the default flip `flow-decision-mode-inclusive-explicit` excepted, as there). That covers the seven entries retired here and those retired earlier that walk a flow: `flow-inert-keys-removed` (`flow.active`, `flow.template`, `nodes[].outputSchema`, `errorHandling.fallbackNodeId`) and `flow-node-wait-timeout-keys-removed` (`waitEventConfig.timeoutMs` / `onTimeout`). On such a host a body carrying one of those older keys was refused 422 by its tombstone before; it is now healed for the verdict, which is what a host with an automation service already did. What is stored stays the body as submitted, so a host's verdict on a saved flow no longer depends on whether it runs an automation service.
2626

2727
Eight more entries below 18 stay on the load path, each with the reason recorded in its docblock: `flow-node-http-callout-rename` and `page-kind-jsx-to-html` (the old value is still accepted by its schema), `view-visibleOn-to-visibleWhen` and `page-component-visibility-to-visibleWhen` (the schema still declares and folds the deprecated key), `flow-node-wait-event-config-lift` and `flow-node-connector-config-lift` (the node's `config` has no contract to refuse the loose keys), `datasource-driver-mongo-to-mongodb` and `retry-policy-converged`.
2828

‎packages/metadata-protocol/src/protocol.save-flow-canonicalization.test.ts‎

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -401,10 +401,13 @@ describe('saveMetaItem canonicalizes flow bodies (#4542)', () => {
401401
* [#21982] On the fallback path — no canonicalizer resolved, or it threw —
402402
* the gate judges the body in its ADR-0087 D2-converted spelling, for the
403403
* verdict only. The flow parse refuses an undeclared config key on the
404-
* builtin node types, so without the conversion a D2 spelling every other
405-
* door converts first (`os validate`, `defineStack`, `registerFlow`) would
406-
* be refused here, and only here: a verdict that depended on whether the
407-
* host runs an automation service. What is STORED stays the raw body.
404+
* builtin node types, so without the conversion a D2 spelling the
405+
* canonicalizer converts first (`registerFlow`'s full chain, retired
406+
* entries included) would be refused here, and only here: a verdict that
407+
* depended on whether the host runs an automation service. So the fallback
408+
* replays that same full chain (#23487) — for the seven flow spellings
409+
* retired from the authoring funnel at 18 and for those retired before
410+
* them alike. What is STORED stays the raw body.
408411
*/
409412
describe('[#21982] the fallback gate judges the D2-converted body, and stores the raw one', () => {
410413
const scriptBody = (config: Record<string, unknown>) => ({
@@ -430,6 +433,20 @@ describe('saveMetaItem canonicalizes flow bodies (#4542)', () => {
430433
expect(storedFlow(rows)!.body.nodes[0].config).toEqual({ functionName: 'helpdesk.triage' });
431434
});
432435

436+
it('an older-retired flow shape (`flow.active`) reaches the canonicalizer\'s verdict too — healed for the verdict, stored raw', async () => {
437+
// `flow-inert-keys-removed` (retired from the load path after 16.1.0)
438+
// tombstones `active`. On a host with no automation service the
439+
// fallback used to judge it in the authoring posture and refuse it
440+
// 422; the service-backed path replays the entry and heals it. Since
441+
// #23487 the fallback replays the same chain, so both hosts agree.
442+
const { protocol, rows } = makeProtocol(new Map());
443+
444+
const result = await save(protocol, { ...flowBody({ objectName: 'lead', filter: { status: 'stale' } }), active: false });
445+
446+
expect(result.success).toBe(true);
447+
expect(storedFlow(rows)!.body.active).toBe(false);
448+
});
449+
433450
it('an undeclared key on delete_record is refused 422 INVALID_METADATA at its path — the alias beside it is not', async () => {
434451
const { protocol, rows } = makeProtocol(new Map());
435452

‎packages/spec/src/conversions/registry.ts‎

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4187,19 +4187,18 @@ const objectIndexTypePartialRemoved: MetadataConversion = {
41874187
* default flip 3 → 0 never needed a materialization step while the job one
41884188
* did: nothing was ever deployed under the old reading.
41894189
*
4190-
* `retiredFromLoadPath` is NOT set: `FlowNodeSchema.config` is an unconstrained
4191-
* record, so no schema rejection can reach `config.retry.retryDelayMs` and the
4192-
* conversion layer is the only seam that can declare and retire that spelling.
4193-
* The `RetryPolicySchema` tombstone still fires for anyone who reaches the
4194-
* policy through a parsed job.
4195-
*
4196-
* **A recorded verdict at 18 (#23487):** it stays. Re-measured on the 18 line,
4197-
* a `try_catch` node's `config.retry: { retryDelayMs }` still passes the flow
4198-
* parse unrefused, and a source job that omits `maxRetries` would silently drop
4199-
* from three retries to none without the step-0 default materialization. It can
4200-
* retire once that `retry` block refuses `retryDelayMs` naming `backoffMs`, and
4201-
* the job default flip is settled for sources (or the entry is recorded as
4202-
* permanent).
4190+
* `retiredFromLoadPath` is NOT set, and the base-delay rename is not why:
4191+
* every surface that carried `retryDelayMs` already refuses it with the rename
4192+
* to `backoffMs` (the `RetryPolicySchema` tombstone, which a `try_catch` node's
4193+
* `config.retry` is, and `flow.errorHandling`'s own).
4194+
*
4195+
* **A recorded verdict at 18 (#23487):** it stays, for the job half alone. It
4196+
* writes the pre-17 `maxRetries: 3` / `backoffMultiplier: 2` into a source
4197+
* `job.retryPolicy` that omits them; retired, the authoring funnel would stop
4198+
* doing that, and such a job would silently drop from three retries to none
4199+
* under the merged 0 / 1 defaults — a default flip no gate can see. It can
4200+
* retire once that default flip has a ruling for authored sources (or the entry
4201+
* is recorded as permanent).
42034202
*/
42044203
const retryPolicyConverged: MetadataConversion = {
42054204
id: 'retry-policy-converged',

0 commit comments

Comments
 (0)