From c0607e80ba8bd1c635dc00e96a85ee921ef25fbb Mon Sep 17 00:00:00 2001 From: Richard McDaniel <1130888+rmcdaniel@users.noreply.github.com> Date: Fri, 13 Nov 2026 10:13:41 +0000 Subject: [PATCH 1/4] Propose recorded parallel policies and shared replay vectors --- conformance/parallel-groups/README.md | 101 +++ conformance/parallel-groups/fixtures-v1.json | 773 +++++++++++++++++++ 2 files changed, 874 insertions(+) create mode 100644 conformance/parallel-groups/README.md create mode 100644 conformance/parallel-groups/fixtures-v1.json diff --git a/conformance/parallel-groups/README.md b/conformance/parallel-groups/README.md new file mode 100644 index 0000000..7143088 --- /dev/null +++ b/conformance/parallel-groups/README.md @@ -0,0 +1,101 @@ +# Parallel group failure policy, version 1 + +Proposal for [issue 232](https://github.com/durable-workflow/.github/issues/232). +Workflow acknowledged sdk-python's handoff for this contract and fixture slice. +Workflow retains embedded producer admission, Server retains native admission, +and the first-party bindings retain authoring and replay. + +This document defines a proposed wire contract. It does not advertise runtime +support or qualify a release. Producers and consumers require review and +published-artifact qualification before enabling it. + +## Recorded discriminator + +Each canonical group metadata entry may contain `parallel_group_policy` with +exactly one of `propagate`, `cancel_remaining` or `settle`. It appears in every +applicable frame of `parallel_group_path`; the top-level flattened fields match +the innermost frame, as the other group fields do today. + +The policy belongs to a group identity and is immutable after admission. Every +member's frame for that group must agree, including presence versus absence. +An unknown, null, empty or inconsistent value is refused before writes. +Selection frames (`parallel_group_mode: select`) retain their existing contract. + +Absence preserves the recording binding's legacy behavior, including failure +selection and replay clock. In particular, legacy Python list groups wait for +every member and select the first failure in member order. A replacement worker +must not reinterpret that history as explicit `propagate`. + +Newly admitted version-1 groups record their chosen policy, including the default +`propagate`. Replay uses the recorded policy, not a new worker's default. An +already-open legacy group keeps absence even when replayed by a newer worker. + +## Capability admission + +The proposed discovery field is +`worker_protocol.server_capabilities.parallel_group_policies`: + +```json +{"supported": true, "version": 1, "policies": ["propagate"]} +``` + +`policies` is a nonempty unique subset of the three policy names. Producers may +admit one policy before the others, and list only policies actually implemented +and qualified. Wrong types, unknown versions or names, duplicates, missing +fields and `supported: false` do not authorize admission. + +The worker capability manifest uses the same `parallel_group_policies` +descriptor. Producer discovery alone does not establish consumer support. +Admission requires both sides to support the selected policy. A worker must +not advertise a policy implemented only by a test flag or source proposal. + +If either side lacks the selected policy, refuse explicit admission before +recording commands. Never drop the policy, silently downgrade it to legacy +behavior or partially admit the member batch. Existing absent-policy histories +remain replayable under their original semantics. + +## Outcomes and replay boundaries + +`propagate` raises the first member failure in durable history order. It does +not wait for open siblings or issue sibling cancellation. Successful groups +still return values in member order after every member succeeds. A later +sibling result cannot become an outcome for a successor authored call. + +`cancel_remaining` uses the same first-failure boundary and requests cancellation +for unfinished siblings in member order before raising the initiating failure. +It preserves existing operation cancellation policies, ownership and deadlines. +Cancellation is recorded and replayed; replay never sends another request for +the same recorded cancellation. This policy needs a producer-reviewed mapping +to existing operation cancellation commands before it can be advertised. The +fixture specifies required targets, not a new unreviewed command kind. + +`settle` waits for every member and returns an ordered outcome per member. Each +outcome distinguishes success (including a null value) from failure; a member +failure is not thrown at the group call. The language-neutral outcome envelope +requires binding/producer review before this policy is advertised. + +First-failure ties follow durable event order, never member position or a sort +of timestamps. The explicit-policy failure continuation receives only the +selected failure's qualified event clock. Later sibling results and later +signals cannot supply that boundary's time. Absent-policy histories keep their +binding's original clock rule. A terminal run-close settlement is not a member +outcome, as defined by the existing run-close contract. + +Nested groups record policy per group frame. Initial consumers may support only +uniform explicit `propagate` frames; mixed nested policies must remain refused +until their outcome composition is qualified. Local/prepared operations need +their own atomic admission support and must not inherit support merely because +remote activities work. + +## Fixtures and remaining decisions + +[`fixtures-v1.json`](fixtures-v1.json) contains synthetic canonical history +prefixes and expected semantic outcomes. Binding owners copy applicable inputs +into their existing replay suites and retain legacy fixtures unchanged. These +source vectors are not an installed conformance result. + +The propagate wire discriminator and proposed per-policy capability can be +reviewed independently of the producer mappings for `cancel_remaining` and +`settle`. Those two policies stay unadvertised until their cancellation command +mapping and ordered outcome envelope are agreed and implemented. No runtime +change or release is authorized by a passing fixture-only check. diff --git a/conformance/parallel-groups/fixtures-v1.json b/conformance/parallel-groups/fixtures-v1.json new file mode 100644 index 0000000..a732719 --- /dev/null +++ b/conformance/parallel-groups/fixtures-v1.json @@ -0,0 +1,773 @@ +{ + "schema": "durable-workflow.parallel-group-policy-fixtures/v1", + "status": "proposal-not-qualified", + "capability": { + "discovery_path": "worker_protocol.server_capabilities.parallel_group_policies", + "descriptor": { + "supported": true, + "version": 1, + "policies": [ + "propagate" + ] + } + }, + "cases": [ + { + "id": "legacy-python-pending", + "history": [ + { + "event_type": "WorkflowStarted", + "recorded_at": "2026-01-01T00:00:00Z", + "payload": {} + }, + { + "event_type": "ActivityScheduled", + "recorded_at": "2026-01-01T00:00:00Z", + "payload": { + "sequence": 1, + "activity_execution_id": "execution-0", + "activity_type": "slow-first", + "parallel_group_id": "parallel-activities:1:2", + "parallel_group_kind": "activity", + "parallel_group_base_sequence": 1, + "parallel_group_size": 2, + "parallel_group_index": 0, + "parallel_group_path": [ + { + "parallel_group_id": "parallel-activities:1:2", + "parallel_group_kind": "activity", + "parallel_group_base_sequence": 1, + "parallel_group_size": 2, + "parallel_group_index": 0 + } + ] + } + }, + { + "event_type": "ActivityScheduled", + "recorded_at": "2026-01-01T00:00:00Z", + "payload": { + "sequence": 2, + "activity_execution_id": "execution-1", + "activity_type": "fast-second", + "parallel_group_id": "parallel-activities:1:2", + "parallel_group_kind": "activity", + "parallel_group_base_sequence": 1, + "parallel_group_size": 2, + "parallel_group_index": 1, + "parallel_group_path": [ + { + "parallel_group_id": "parallel-activities:1:2", + "parallel_group_kind": "activity", + "parallel_group_base_sequence": 1, + "parallel_group_size": 2, + "parallel_group_index": 1 + } + ] + } + }, + { + "event_type": "ActivityFailed", + "recorded_at": "2026-01-01T00:00:01Z", + "payload": { + "sequence": 2, + "activity_execution_id": "execution-1", + "activity_type": "fast-second", + "parallel_group_id": "parallel-activities:1:2", + "parallel_group_kind": "activity", + "parallel_group_base_sequence": 1, + "parallel_group_size": 2, + "parallel_group_index": 1, + "parallel_group_path": [ + { + "parallel_group_id": "parallel-activities:1:2", + "parallel_group_kind": "activity", + "parallel_group_base_sequence": 1, + "parallel_group_size": 2, + "parallel_group_index": 1 + } + ], + "message": "higher position failed first" + } + } + ], + "expected": { + "continuation": "pending", + "failure": null, + "open_member_indexes": [ + 0 + ] + }, + "legacy_binding": "python" + }, + { + "id": "propagate-first-recorded-failure", + "history": [ + { + "event_type": "WorkflowStarted", + "recorded_at": "2026-01-01T00:00:00Z", + "payload": {} + }, + { + "event_type": "ActivityScheduled", + "recorded_at": "2026-01-01T00:00:00Z", + "payload": { + "sequence": 1, + "activity_execution_id": "execution-0", + "activity_type": "slow-first", + "parallel_group_id": "parallel-activities:1:2", + "parallel_group_kind": "activity", + "parallel_group_base_sequence": 1, + "parallel_group_size": 2, + "parallel_group_index": 0, + "parallel_group_path": [ + { + "parallel_group_id": "parallel-activities:1:2", + "parallel_group_kind": "activity", + "parallel_group_base_sequence": 1, + "parallel_group_size": 2, + "parallel_group_index": 0, + "parallel_group_policy": "propagate" + } + ], + "parallel_group_policy": "propagate" + } + }, + { + "event_type": "ActivityScheduled", + "recorded_at": "2026-01-01T00:00:00Z", + "payload": { + "sequence": 2, + "activity_execution_id": "execution-1", + "activity_type": "fast-second", + "parallel_group_id": "parallel-activities:1:2", + "parallel_group_kind": "activity", + "parallel_group_base_sequence": 1, + "parallel_group_size": 2, + "parallel_group_index": 1, + "parallel_group_path": [ + { + "parallel_group_id": "parallel-activities:1:2", + "parallel_group_kind": "activity", + "parallel_group_base_sequence": 1, + "parallel_group_size": 2, + "parallel_group_index": 1, + "parallel_group_policy": "propagate" + } + ], + "parallel_group_policy": "propagate" + } + }, + { + "event_type": "ActivityFailed", + "recorded_at": "2026-01-01T00:00:01Z", + "payload": { + "sequence": 2, + "activity_execution_id": "execution-1", + "activity_type": "fast-second", + "parallel_group_id": "parallel-activities:1:2", + "parallel_group_kind": "activity", + "parallel_group_base_sequence": 1, + "parallel_group_size": 2, + "parallel_group_index": 1, + "parallel_group_path": [ + { + "parallel_group_id": "parallel-activities:1:2", + "parallel_group_kind": "activity", + "parallel_group_base_sequence": 1, + "parallel_group_size": 2, + "parallel_group_index": 1, + "parallel_group_policy": "propagate" + } + ], + "message": "higher position failed first", + "parallel_group_policy": "propagate" + } + } + ], + "expected": { + "continuation": "failed", + "failure_member_index": 1, + "failure_event_index": 3, + "cancellation_requests": [], + "open_member_indexes": [ + 0 + ], + "clock_event_index": 3 + } + }, + { + "id": "cancel-remaining-targets-open-sibling", + "history": [ + { + "event_type": "WorkflowStarted", + "recorded_at": "2026-01-01T00:00:00Z", + "payload": {} + }, + { + "event_type": "ActivityScheduled", + "recorded_at": "2026-01-01T00:00:00Z", + "payload": { + "sequence": 1, + "activity_execution_id": "execution-0", + "activity_type": "slow-first", + "parallel_group_id": "parallel-activities:1:2", + "parallel_group_kind": "activity", + "parallel_group_base_sequence": 1, + "parallel_group_size": 2, + "parallel_group_index": 0, + "parallel_group_path": [ + { + "parallel_group_id": "parallel-activities:1:2", + "parallel_group_kind": "activity", + "parallel_group_base_sequence": 1, + "parallel_group_size": 2, + "parallel_group_index": 0, + "parallel_group_policy": "cancel_remaining" + } + ], + "parallel_group_policy": "cancel_remaining" + } + }, + { + "event_type": "ActivityScheduled", + "recorded_at": "2026-01-01T00:00:00Z", + "payload": { + "sequence": 2, + "activity_execution_id": "execution-1", + "activity_type": "fast-second", + "parallel_group_id": "parallel-activities:1:2", + "parallel_group_kind": "activity", + "parallel_group_base_sequence": 1, + "parallel_group_size": 2, + "parallel_group_index": 1, + "parallel_group_path": [ + { + "parallel_group_id": "parallel-activities:1:2", + "parallel_group_kind": "activity", + "parallel_group_base_sequence": 1, + "parallel_group_size": 2, + "parallel_group_index": 1, + "parallel_group_policy": "cancel_remaining" + } + ], + "parallel_group_policy": "cancel_remaining" + } + }, + { + "event_type": "ActivityFailed", + "recorded_at": "2026-01-01T00:00:01Z", + "payload": { + "sequence": 2, + "activity_execution_id": "execution-1", + "activity_type": "fast-second", + "parallel_group_id": "parallel-activities:1:2", + "parallel_group_kind": "activity", + "parallel_group_base_sequence": 1, + "parallel_group_size": 2, + "parallel_group_index": 1, + "parallel_group_path": [ + { + "parallel_group_id": "parallel-activities:1:2", + "parallel_group_kind": "activity", + "parallel_group_base_sequence": 1, + "parallel_group_size": 2, + "parallel_group_index": 1, + "parallel_group_policy": "cancel_remaining" + } + ], + "message": "higher position failed first", + "parallel_group_policy": "cancel_remaining" + } + } + ], + "expected": { + "continuation": "failed", + "failure_member_index": 1, + "cancellation_requests": [ + { + "member_index": 0, + "sequence": 1 + } + ] + } + }, + { + "id": "settle-waits-for-open-sibling", + "history": [ + { + "event_type": "WorkflowStarted", + "recorded_at": "2026-01-01T00:00:00Z", + "payload": {} + }, + { + "event_type": "ActivityScheduled", + "recorded_at": "2026-01-01T00:00:00Z", + "payload": { + "sequence": 1, + "activity_execution_id": "execution-0", + "activity_type": "slow-first", + "parallel_group_id": "parallel-activities:1:2", + "parallel_group_kind": "activity", + "parallel_group_base_sequence": 1, + "parallel_group_size": 2, + "parallel_group_index": 0, + "parallel_group_path": [ + { + "parallel_group_id": "parallel-activities:1:2", + "parallel_group_kind": "activity", + "parallel_group_base_sequence": 1, + "parallel_group_size": 2, + "parallel_group_index": 0, + "parallel_group_policy": "settle" + } + ], + "parallel_group_policy": "settle" + } + }, + { + "event_type": "ActivityScheduled", + "recorded_at": "2026-01-01T00:00:00Z", + "payload": { + "sequence": 2, + "activity_execution_id": "execution-1", + "activity_type": "fast-second", + "parallel_group_id": "parallel-activities:1:2", + "parallel_group_kind": "activity", + "parallel_group_base_sequence": 1, + "parallel_group_size": 2, + "parallel_group_index": 1, + "parallel_group_path": [ + { + "parallel_group_id": "parallel-activities:1:2", + "parallel_group_kind": "activity", + "parallel_group_base_sequence": 1, + "parallel_group_size": 2, + "parallel_group_index": 1, + "parallel_group_policy": "settle" + } + ], + "parallel_group_policy": "settle" + } + }, + { + "event_type": "ActivityFailed", + "recorded_at": "2026-01-01T00:00:01Z", + "payload": { + "sequence": 2, + "activity_execution_id": "execution-1", + "activity_type": "fast-second", + "parallel_group_id": "parallel-activities:1:2", + "parallel_group_kind": "activity", + "parallel_group_base_sequence": 1, + "parallel_group_size": 2, + "parallel_group_index": 1, + "parallel_group_path": [ + { + "parallel_group_id": "parallel-activities:1:2", + "parallel_group_kind": "activity", + "parallel_group_base_sequence": 1, + "parallel_group_size": 2, + "parallel_group_index": 1, + "parallel_group_policy": "settle" + } + ], + "message": "higher position failed first", + "parallel_group_policy": "settle" + } + } + ], + "expected": { + "continuation": "pending", + "open_member_indexes": [ + 0 + ] + } + }, + { + "id": "legacy-python-positional-failure", + "history": [ + { + "event_type": "WorkflowStarted", + "recorded_at": "2026-01-01T00:00:00Z", + "payload": {} + }, + { + "event_type": "ActivityScheduled", + "recorded_at": "2026-01-01T00:00:00Z", + "payload": { + "sequence": 1, + "activity_execution_id": "execution-0", + "activity_type": "slow-first", + "parallel_group_id": "parallel-activities:1:2", + "parallel_group_kind": "activity", + "parallel_group_base_sequence": 1, + "parallel_group_size": 2, + "parallel_group_index": 0, + "parallel_group_path": [ + { + "parallel_group_id": "parallel-activities:1:2", + "parallel_group_kind": "activity", + "parallel_group_base_sequence": 1, + "parallel_group_size": 2, + "parallel_group_index": 0 + } + ] + } + }, + { + "event_type": "ActivityScheduled", + "recorded_at": "2026-01-01T00:00:00Z", + "payload": { + "sequence": 2, + "activity_execution_id": "execution-1", + "activity_type": "fast-second", + "parallel_group_id": "parallel-activities:1:2", + "parallel_group_kind": "activity", + "parallel_group_base_sequence": 1, + "parallel_group_size": 2, + "parallel_group_index": 1, + "parallel_group_path": [ + { + "parallel_group_id": "parallel-activities:1:2", + "parallel_group_kind": "activity", + "parallel_group_base_sequence": 1, + "parallel_group_size": 2, + "parallel_group_index": 1 + } + ] + } + }, + { + "event_type": "ActivityFailed", + "recorded_at": "2026-01-01T00:00:01Z", + "payload": { + "sequence": 2, + "activity_execution_id": "execution-1", + "activity_type": "fast-second", + "parallel_group_id": "parallel-activities:1:2", + "parallel_group_kind": "activity", + "parallel_group_base_sequence": 1, + "parallel_group_size": 2, + "parallel_group_index": 1, + "parallel_group_path": [ + { + "parallel_group_id": "parallel-activities:1:2", + "parallel_group_kind": "activity", + "parallel_group_base_sequence": 1, + "parallel_group_size": 2, + "parallel_group_index": 1 + } + ], + "message": "higher position failed first" + } + }, + { + "event_type": "ActivityFailed", + "recorded_at": "2026-01-01T00:00:03Z", + "payload": { + "sequence": 1, + "activity_execution_id": "execution-0", + "activity_type": "slow-first", + "parallel_group_id": "parallel-activities:1:2", + "parallel_group_kind": "activity", + "parallel_group_base_sequence": 1, + "parallel_group_size": 2, + "parallel_group_index": 0, + "parallel_group_path": [ + { + "parallel_group_id": "parallel-activities:1:2", + "parallel_group_kind": "activity", + "parallel_group_base_sequence": 1, + "parallel_group_size": 2, + "parallel_group_index": 0 + } + ], + "message": "lower position failed later" + } + } + ], + "expected": { + "continuation": "failed", + "failure_member_index": 0 + }, + "legacy_binding": "python" + }, + { + "id": "propagate-preserves-first-after-late-loser", + "history": [ + { + "event_type": "WorkflowStarted", + "recorded_at": "2026-01-01T00:00:00Z", + "payload": {} + }, + { + "event_type": "ActivityScheduled", + "recorded_at": "2026-01-01T00:00:00Z", + "payload": { + "sequence": 1, + "activity_execution_id": "execution-0", + "activity_type": "slow-first", + "parallel_group_id": "parallel-activities:1:2", + "parallel_group_kind": "activity", + "parallel_group_base_sequence": 1, + "parallel_group_size": 2, + "parallel_group_index": 0, + "parallel_group_path": [ + { + "parallel_group_id": "parallel-activities:1:2", + "parallel_group_kind": "activity", + "parallel_group_base_sequence": 1, + "parallel_group_size": 2, + "parallel_group_index": 0, + "parallel_group_policy": "propagate" + } + ], + "parallel_group_policy": "propagate" + } + }, + { + "event_type": "ActivityScheduled", + "recorded_at": "2026-01-01T00:00:00Z", + "payload": { + "sequence": 2, + "activity_execution_id": "execution-1", + "activity_type": "fast-second", + "parallel_group_id": "parallel-activities:1:2", + "parallel_group_kind": "activity", + "parallel_group_base_sequence": 1, + "parallel_group_size": 2, + "parallel_group_index": 1, + "parallel_group_path": [ + { + "parallel_group_id": "parallel-activities:1:2", + "parallel_group_kind": "activity", + "parallel_group_base_sequence": 1, + "parallel_group_size": 2, + "parallel_group_index": 1, + "parallel_group_policy": "propagate" + } + ], + "parallel_group_policy": "propagate" + } + }, + { + "event_type": "ActivityFailed", + "recorded_at": "2026-01-01T00:00:01Z", + "payload": { + "sequence": 2, + "activity_execution_id": "execution-1", + "activity_type": "fast-second", + "parallel_group_id": "parallel-activities:1:2", + "parallel_group_kind": "activity", + "parallel_group_base_sequence": 1, + "parallel_group_size": 2, + "parallel_group_index": 1, + "parallel_group_path": [ + { + "parallel_group_id": "parallel-activities:1:2", + "parallel_group_kind": "activity", + "parallel_group_base_sequence": 1, + "parallel_group_size": 2, + "parallel_group_index": 1, + "parallel_group_policy": "propagate" + } + ], + "message": "higher position failed first", + "parallel_group_policy": "propagate" + } + }, + { + "event_type": "ActivityFailed", + "recorded_at": "2026-01-01T00:00:03Z", + "payload": { + "sequence": 1, + "activity_execution_id": "execution-0", + "activity_type": "slow-first", + "parallel_group_id": "parallel-activities:1:2", + "parallel_group_kind": "activity", + "parallel_group_base_sequence": 1, + "parallel_group_size": 2, + "parallel_group_index": 0, + "parallel_group_path": [ + { + "parallel_group_id": "parallel-activities:1:2", + "parallel_group_kind": "activity", + "parallel_group_base_sequence": 1, + "parallel_group_size": 2, + "parallel_group_index": 0, + "parallel_group_policy": "propagate" + } + ], + "message": "lower position failed later", + "parallel_group_policy": "propagate" + } + } + ], + "expected": { + "continuation": "failed", + "failure_member_index": 1, + "failure_event_index": 3, + "clock_event_index": 3 + } + }, + { + "id": "settle-orders-failures-by-member", + "history": [ + { + "event_type": "WorkflowStarted", + "recorded_at": "2026-01-01T00:00:00Z", + "payload": {} + }, + { + "event_type": "ActivityScheduled", + "recorded_at": "2026-01-01T00:00:00Z", + "payload": { + "sequence": 1, + "activity_execution_id": "execution-0", + "activity_type": "slow-first", + "parallel_group_id": "parallel-activities:1:2", + "parallel_group_kind": "activity", + "parallel_group_base_sequence": 1, + "parallel_group_size": 2, + "parallel_group_index": 0, + "parallel_group_path": [ + { + "parallel_group_id": "parallel-activities:1:2", + "parallel_group_kind": "activity", + "parallel_group_base_sequence": 1, + "parallel_group_size": 2, + "parallel_group_index": 0, + "parallel_group_policy": "settle" + } + ], + "parallel_group_policy": "settle" + } + }, + { + "event_type": "ActivityScheduled", + "recorded_at": "2026-01-01T00:00:00Z", + "payload": { + "sequence": 2, + "activity_execution_id": "execution-1", + "activity_type": "fast-second", + "parallel_group_id": "parallel-activities:1:2", + "parallel_group_kind": "activity", + "parallel_group_base_sequence": 1, + "parallel_group_size": 2, + "parallel_group_index": 1, + "parallel_group_path": [ + { + "parallel_group_id": "parallel-activities:1:2", + "parallel_group_kind": "activity", + "parallel_group_base_sequence": 1, + "parallel_group_size": 2, + "parallel_group_index": 1, + "parallel_group_policy": "settle" + } + ], + "parallel_group_policy": "settle" + } + }, + { + "event_type": "ActivityFailed", + "recorded_at": "2026-01-01T00:00:01Z", + "payload": { + "sequence": 2, + "activity_execution_id": "execution-1", + "activity_type": "fast-second", + "parallel_group_id": "parallel-activities:1:2", + "parallel_group_kind": "activity", + "parallel_group_base_sequence": 1, + "parallel_group_size": 2, + "parallel_group_index": 1, + "parallel_group_path": [ + { + "parallel_group_id": "parallel-activities:1:2", + "parallel_group_kind": "activity", + "parallel_group_base_sequence": 1, + "parallel_group_size": 2, + "parallel_group_index": 1, + "parallel_group_policy": "settle" + } + ], + "message": "higher position failed first", + "parallel_group_policy": "settle" + } + }, + { + "event_type": "ActivityFailed", + "recorded_at": "2026-01-01T00:00:03Z", + "payload": { + "sequence": 1, + "activity_execution_id": "execution-0", + "activity_type": "slow-first", + "parallel_group_id": "parallel-activities:1:2", + "parallel_group_kind": "activity", + "parallel_group_base_sequence": 1, + "parallel_group_size": 2, + "parallel_group_index": 0, + "parallel_group_path": [ + { + "parallel_group_id": "parallel-activities:1:2", + "parallel_group_kind": "activity", + "parallel_group_base_sequence": 1, + "parallel_group_size": 2, + "parallel_group_index": 0, + "parallel_group_policy": "settle" + } + ], + "message": "lower position failed later", + "parallel_group_policy": "settle" + } + } + ], + "expected": { + "continuation": "completed", + "outcomes": [ + { + "member_index": 0, + "status": "failed" + }, + { + "member_index": 1, + "status": "failed" + } + ] + } + } + ], + "refusal_vectors": [ + { + "id": "unknown-policy", + "policy": "best_effort" + }, + { + "id": "null-policy", + "policy": null + }, + { + "id": "inconsistent-member-policy", + "member_policies": [ + "propagate", + "settle" + ] + }, + { + "id": "missing-producer-capability", + "producer_capability": null, + "policy": "propagate" + }, + { + "id": "unsupported-selected-policy", + "producer_capability": { + "supported": true, + "version": 1, + "policies": [ + "propagate" + ] + }, + "policy": "settle" + } + ] +} From 70e3c2cb24acafcb302bdd7542e5a3fd559c2ce3 Mon Sep 17 00:00:00 2001 From: Richard McDaniel <1130888+rmcdaniel@users.noreply.github.com> Date: Fri, 13 Nov 2026 10:37:58 +0000 Subject: [PATCH 2/4] Specify replacement eligibility and durable failure boundaries --- conformance/parallel-groups/README.md | 17 +- conformance/parallel-groups/fixtures-v1.json | 394 +++++++++++++++++-- 2 files changed, 375 insertions(+), 36 deletions(-) diff --git a/conformance/parallel-groups/README.md b/conformance/parallel-groups/README.md index 7143088..74bf94b 100644 --- a/conformance/parallel-groups/README.md +++ b/conformance/parallel-groups/README.md @@ -19,7 +19,9 @@ the innermost frame, as the other group fields do today. The policy belongs to a group identity and is immutable after admission. Every member's frame for that group must agree, including presence versus absence. An unknown, null, empty or inconsistent value is refused before writes. -Selection frames (`parallel_group_mode: select`) retain their existing contract. +Policy is refused on selection frames (`parallel_group_mode: select`). An +enclosing ordinary all-group frame may carry its own policy; that does not +change the selection frame's winner/handle contract. Absence preserves the recording binding's legacy behavior, including failure selection and replay clock. In particular, legacy Python list groups wait for @@ -49,6 +51,15 @@ descriptor. Producer discovery alone does not establish consumer support. Admission requires both sides to support the selected policy. A worker must not advertise a policy implemented only by a test flag or source proposal. +The same fence applies to replacement-worker claim and replay eligibility. +Before assigning or confirming a claim, the producer verifies that the worker +supports every explicit policy needed by that run's retained history. A legacy +worker must not claim an explicit-policy run merely because it supports the +workflow type. If no eligible worker exists, keep the run waiting with an +operator-visible capability reason. Never strip recorded policy or downgrade +it during repair, registration replacement, retry or cold replay. Completion +also retains the existing current-worker, lease and attempt fences. + If either side lacks the selected policy, refuse explicit admission before recording commands. Never drop the policy, silently downgrade it to legacy behavior or partially admit the member batch. Existing absent-policy histories @@ -81,6 +92,10 @@ signals cannot supply that boundary's time. Absent-policy histories keep their binding's original clock rule. A terminal run-close settlement is not a member outcome, as defined by the existing run-close contract. +Activity-group failure clocks keep the established canonical-v1 millisecond +normalization. If resource identity or timestamp checks cannot prove the selected +boundary, the clock remains unavailable rather than borrowing another event. + Nested groups record policy per group frame. Initial consumers may support only uniform explicit `propagate` frames; mixed nested policies must remain refused until their outcome composition is qualified. Local/prepared operations need diff --git a/conformance/parallel-groups/fixtures-v1.json b/conformance/parallel-groups/fixtures-v1.json index a732719..a31a3a2 100644 --- a/conformance/parallel-groups/fixtures-v1.json +++ b/conformance/parallel-groups/fixtures-v1.json @@ -18,7 +18,8 @@ { "event_type": "WorkflowStarted", "recorded_at": "2026-01-01T00:00:00Z", - "payload": {} + "payload": {}, + "sequence": 1 }, { "event_type": "ActivityScheduled", @@ -41,7 +42,8 @@ "parallel_group_index": 0 } ] - } + }, + "sequence": 2 }, { "event_type": "ActivityScheduled", @@ -64,7 +66,8 @@ "parallel_group_index": 1 } ] - } + }, + "sequence": 3 }, { "event_type": "ActivityFailed", @@ -88,7 +91,8 @@ } ], "message": "higher position failed first" - } + }, + "sequence": 4 } ], "expected": { @@ -106,7 +110,8 @@ { "event_type": "WorkflowStarted", "recorded_at": "2026-01-01T00:00:00Z", - "payload": {} + "payload": {}, + "sequence": 1 }, { "event_type": "ActivityScheduled", @@ -131,7 +136,8 @@ } ], "parallel_group_policy": "propagate" - } + }, + "sequence": 2 }, { "event_type": "ActivityScheduled", @@ -156,7 +162,8 @@ } ], "parallel_group_policy": "propagate" - } + }, + "sequence": 3 }, { "event_type": "ActivityFailed", @@ -182,18 +189,19 @@ ], "message": "higher position failed first", "parallel_group_policy": "propagate" - } + }, + "sequence": 4 } ], "expected": { "continuation": "failed", "failure_member_index": 1, - "failure_event_index": 3, "cancellation_requests": [], "open_member_indexes": [ 0 ], - "clock_event_index": 3 + "failure_event_sequence": 4, + "clock_event_sequence": 4 } }, { @@ -202,7 +210,8 @@ { "event_type": "WorkflowStarted", "recorded_at": "2026-01-01T00:00:00Z", - "payload": {} + "payload": {}, + "sequence": 1 }, { "event_type": "ActivityScheduled", @@ -227,7 +236,8 @@ } ], "parallel_group_policy": "cancel_remaining" - } + }, + "sequence": 2 }, { "event_type": "ActivityScheduled", @@ -252,7 +262,8 @@ } ], "parallel_group_policy": "cancel_remaining" - } + }, + "sequence": 3 }, { "event_type": "ActivityFailed", @@ -278,7 +289,8 @@ ], "message": "higher position failed first", "parallel_group_policy": "cancel_remaining" - } + }, + "sequence": 4 } ], "expected": { @@ -298,7 +310,8 @@ { "event_type": "WorkflowStarted", "recorded_at": "2026-01-01T00:00:00Z", - "payload": {} + "payload": {}, + "sequence": 1 }, { "event_type": "ActivityScheduled", @@ -323,7 +336,8 @@ } ], "parallel_group_policy": "settle" - } + }, + "sequence": 2 }, { "event_type": "ActivityScheduled", @@ -348,7 +362,8 @@ } ], "parallel_group_policy": "settle" - } + }, + "sequence": 3 }, { "event_type": "ActivityFailed", @@ -374,7 +389,8 @@ ], "message": "higher position failed first", "parallel_group_policy": "settle" - } + }, + "sequence": 4 } ], "expected": { @@ -390,7 +406,8 @@ { "event_type": "WorkflowStarted", "recorded_at": "2026-01-01T00:00:00Z", - "payload": {} + "payload": {}, + "sequence": 1 }, { "event_type": "ActivityScheduled", @@ -413,7 +430,8 @@ "parallel_group_index": 0 } ] - } + }, + "sequence": 2 }, { "event_type": "ActivityScheduled", @@ -436,7 +454,8 @@ "parallel_group_index": 1 } ] - } + }, + "sequence": 3 }, { "event_type": "ActivityFailed", @@ -460,7 +479,8 @@ } ], "message": "higher position failed first" - } + }, + "sequence": 4 }, { "event_type": "ActivityFailed", @@ -484,7 +504,8 @@ } ], "message": "lower position failed later" - } + }, + "sequence": 5 } ], "expected": { @@ -499,7 +520,8 @@ { "event_type": "WorkflowStarted", "recorded_at": "2026-01-01T00:00:00Z", - "payload": {} + "payload": {}, + "sequence": 1 }, { "event_type": "ActivityScheduled", @@ -524,7 +546,8 @@ } ], "parallel_group_policy": "propagate" - } + }, + "sequence": 2 }, { "event_type": "ActivityScheduled", @@ -549,7 +572,8 @@ } ], "parallel_group_policy": "propagate" - } + }, + "sequence": 3 }, { "event_type": "ActivityFailed", @@ -575,7 +599,8 @@ ], "message": "higher position failed first", "parallel_group_policy": "propagate" - } + }, + "sequence": 4 }, { "event_type": "ActivityFailed", @@ -601,14 +626,15 @@ ], "message": "lower position failed later", "parallel_group_policy": "propagate" - } + }, + "sequence": 5 } ], "expected": { "continuation": "failed", "failure_member_index": 1, - "failure_event_index": 3, - "clock_event_index": 3 + "failure_event_sequence": 4, + "clock_event_sequence": 4 } }, { @@ -617,7 +643,8 @@ { "event_type": "WorkflowStarted", "recorded_at": "2026-01-01T00:00:00Z", - "payload": {} + "payload": {}, + "sequence": 1 }, { "event_type": "ActivityScheduled", @@ -642,7 +669,8 @@ } ], "parallel_group_policy": "settle" - } + }, + "sequence": 2 }, { "event_type": "ActivityScheduled", @@ -667,7 +695,8 @@ } ], "parallel_group_policy": "settle" - } + }, + "sequence": 3 }, { "event_type": "ActivityFailed", @@ -693,7 +722,8 @@ ], "message": "higher position failed first", "parallel_group_policy": "settle" - } + }, + "sequence": 4 }, { "event_type": "ActivityFailed", @@ -719,7 +749,8 @@ ], "message": "lower position failed later", "parallel_group_policy": "settle" - } + }, + "sequence": 5 } ], "expected": { @@ -735,6 +766,289 @@ } ] } + }, + { + "id": "propagate-tied-failures-follow-recorded-order", + "history": [ + { + "event_type": "WorkflowStarted", + "recorded_at": "2026-01-01T00:00:00Z", + "payload": {}, + "sequence": 1 + }, + { + "event_type": "ActivityScheduled", + "recorded_at": "2026-01-01T00:00:00Z", + "payload": { + "sequence": 1, + "activity_execution_id": "execution-0", + "activity_type": "slow-first", + "parallel_group_id": "parallel-activities:1:2", + "parallel_group_kind": "activity", + "parallel_group_base_sequence": 1, + "parallel_group_size": 2, + "parallel_group_index": 0, + "parallel_group_path": [ + { + "parallel_group_id": "parallel-activities:1:2", + "parallel_group_kind": "activity", + "parallel_group_base_sequence": 1, + "parallel_group_size": 2, + "parallel_group_index": 0, + "parallel_group_policy": "propagate" + } + ], + "parallel_group_policy": "propagate" + }, + "sequence": 2 + }, + { + "event_type": "ActivityScheduled", + "recorded_at": "2026-01-01T00:00:00Z", + "payload": { + "sequence": 2, + "activity_execution_id": "execution-1", + "activity_type": "fast-second", + "parallel_group_id": "parallel-activities:1:2", + "parallel_group_kind": "activity", + "parallel_group_base_sequence": 1, + "parallel_group_size": 2, + "parallel_group_index": 1, + "parallel_group_path": [ + { + "parallel_group_id": "parallel-activities:1:2", + "parallel_group_kind": "activity", + "parallel_group_base_sequence": 1, + "parallel_group_size": 2, + "parallel_group_index": 1, + "parallel_group_policy": "propagate" + } + ], + "parallel_group_policy": "propagate" + }, + "sequence": 3 + }, + { + "event_type": "ActivityFailed", + "recorded_at": "2026-01-01T00:00:01Z", + "payload": { + "sequence": 2, + "activity_execution_id": "execution-1", + "activity_type": "fast-second", + "parallel_group_id": "parallel-activities:1:2", + "parallel_group_kind": "activity", + "parallel_group_base_sequence": 1, + "parallel_group_size": 2, + "parallel_group_index": 1, + "parallel_group_path": [ + { + "parallel_group_id": "parallel-activities:1:2", + "parallel_group_kind": "activity", + "parallel_group_base_sequence": 1, + "parallel_group_size": 2, + "parallel_group_index": 1, + "parallel_group_policy": "propagate" + } + ], + "message": "higher position failed first", + "parallel_group_policy": "propagate" + }, + "sequence": 4 + }, + { + "event_type": "ActivityFailed", + "recorded_at": "2026-01-01T00:00:01Z", + "payload": { + "sequence": 1, + "activity_execution_id": "execution-0", + "activity_type": "slow-first", + "parallel_group_id": "parallel-activities:1:2", + "parallel_group_kind": "activity", + "parallel_group_base_sequence": 1, + "parallel_group_size": 2, + "parallel_group_index": 0, + "parallel_group_path": [ + { + "parallel_group_id": "parallel-activities:1:2", + "parallel_group_kind": "activity", + "parallel_group_base_sequence": 1, + "parallel_group_size": 2, + "parallel_group_index": 0, + "parallel_group_policy": "propagate" + } + ], + "message": "lower position failed later", + "parallel_group_policy": "propagate" + }, + "sequence": 5 + } + ], + "expected": { + "continuation": "failed", + "failure_member_index": 1, + "failure_event_sequence": 4, + "clock_event_sequence": 4 + } + }, + { + "id": "propagate-late-sibling-does-not-supply-successor-clock", + "history": [ + { + "event_type": "WorkflowStarted", + "recorded_at": "2026-01-01T00:00:00Z", + "payload": {}, + "sequence": 1 + }, + { + "event_type": "ActivityScheduled", + "recorded_at": "2026-01-01T00:00:00Z", + "payload": { + "sequence": 1, + "activity_execution_id": "execution-0", + "activity_type": "slow-first", + "parallel_group_id": "parallel-activities:1:2", + "parallel_group_kind": "activity", + "parallel_group_base_sequence": 1, + "parallel_group_size": 2, + "parallel_group_index": 0, + "parallel_group_path": [ + { + "parallel_group_id": "parallel-activities:1:2", + "parallel_group_kind": "activity", + "parallel_group_base_sequence": 1, + "parallel_group_size": 2, + "parallel_group_index": 0, + "parallel_group_policy": "propagate" + } + ], + "parallel_group_policy": "propagate" + }, + "sequence": 2 + }, + { + "event_type": "ActivityScheduled", + "recorded_at": "2026-01-01T00:00:00Z", + "payload": { + "sequence": 2, + "activity_execution_id": "execution-1", + "activity_type": "fast-second", + "parallel_group_id": "parallel-activities:1:2", + "parallel_group_kind": "activity", + "parallel_group_base_sequence": 1, + "parallel_group_size": 2, + "parallel_group_index": 1, + "parallel_group_path": [ + { + "parallel_group_id": "parallel-activities:1:2", + "parallel_group_kind": "activity", + "parallel_group_base_sequence": 1, + "parallel_group_size": 2, + "parallel_group_index": 1, + "parallel_group_policy": "propagate" + } + ], + "parallel_group_policy": "propagate" + }, + "sequence": 3 + }, + { + "event_type": "ActivityFailed", + "recorded_at": "2026-01-01T00:00:01Z", + "payload": { + "sequence": 2, + "activity_execution_id": "execution-1", + "activity_type": "fast-second", + "parallel_group_id": "parallel-activities:1:2", + "parallel_group_kind": "activity", + "parallel_group_base_sequence": 1, + "parallel_group_size": 2, + "parallel_group_index": 1, + "parallel_group_path": [ + { + "parallel_group_id": "parallel-activities:1:2", + "parallel_group_kind": "activity", + "parallel_group_base_sequence": 1, + "parallel_group_size": 2, + "parallel_group_index": 1, + "parallel_group_policy": "propagate" + } + ], + "message": "higher position failed first", + "parallel_group_policy": "propagate" + }, + "sequence": 4 + }, + { + "sequence": 5, + "event_type": "ActivityScheduled", + "recorded_at": "2026-01-01T00:00:01Z", + "payload": { + "sequence": 3, + "activity_type": "after-group", + "activity_execution_id": "tail" + } + }, + { + "sequence": 6, + "event_type": "ActivityCompleted", + "recorded_at": "2026-01-01T00:00:02Z", + "payload": { + "sequence": 3, + "activity_type": "after-group", + "activity_execution_id": "tail", + "result": { + "codec": "avro", + "blob": "wwHioz3/VYAiNwoWdGFpbC1yZXN1bHQ=" + } + } + }, + { + "sequence": 7, + "event_type": "ActivityCompleted", + "recorded_at": "2026-01-01T00:00:09Z", + "payload": { + "sequence": 1, + "activity_execution_id": "execution-0", + "activity_type": "slow-first", + "parallel_group_id": "parallel-activities:1:2", + "parallel_group_kind": "activity", + "parallel_group_base_sequence": 1, + "parallel_group_size": 2, + "parallel_group_index": 0, + "parallel_group_path": [ + { + "parallel_group_id": "parallel-activities:1:2", + "parallel_group_kind": "activity", + "parallel_group_base_sequence": 1, + "parallel_group_size": 2, + "parallel_group_index": 0, + "parallel_group_policy": "propagate" + } + ], + "parallel_group_policy": "propagate", + "result": { + "codec": "avro", + "blob": "wwHioz3/VYAiNwoUbGF0ZS1sb3Nlcg==" + } + } + } + ], + "expected": { + "continuation": "failed", + "failure_member_index": 1, + "cancellation_requests": [], + "open_member_indexes": [ + 0 + ], + "failure_event_sequence": 4, + "clock_event_sequence": 4, + "successor_result": "tail-result", + "successor_clock_event_sequence": 6 + }, + "successor": { + "sequence": 3, + "activity_type": "after-group" + } } ], "refusal_vectors": [ @@ -768,6 +1082,16 @@ ] }, "policy": "settle" + }, + { + "id": "legacy-replacement-worker", + "recorded_policy": "propagate", + "replacement_worker_capability": null + }, + { + "id": "policy-on-select-frame", + "parallel_group_mode": "select", + "parallel_group_policy": "propagate" } ] } From 3c21dc1ac584d60e8f3f2416f43c3a63ea2bf5bd Mon Sep 17 00:00:00 2001 From: Richard McDaniel <1130888+rmcdaniel@users.noreply.github.com> Date: Fri, 13 Nov 2026 10:47:16 +0000 Subject: [PATCH 3/4] Bind parallel policy registration and replacement claim declarations --- conformance/parallel-groups/README.md | 38 +- conformance/parallel-groups/fixtures-v1.json | 353 ++++++++++++++++++- 2 files changed, 386 insertions(+), 5 deletions(-) diff --git a/conformance/parallel-groups/README.md b/conformance/parallel-groups/README.md index 74bf94b..8ae59d2 100644 --- a/conformance/parallel-groups/README.md +++ b/conformance/parallel-groups/README.md @@ -46,10 +46,37 @@ admit one policy before the others, and list only policies actually implemented and qualified. Wrong types, unknown versions or names, duplicates, missing fields and `supported: false` do not authorize admission. -The worker capability manifest uses the same `parallel_group_policies` -descriptor. Producer discovery alone does not establish consumer support. -Admission requires both sides to support the selected policy. A worker must -not advertise a policy implemented only by a test flag or source proposal. +Worker registration uses the existing `POST /worker/register` body fields: + +```json +{ + "capabilities": ["parallel_group_policies"], + "capability_manifest": { + "parallel_group_policies": {"supported": true, "version": 1, "policies": ["propagate"]} + } +} +``` + +The structured descriptor is exactly at +`capability_manifest.parallel_group_policies`, separate from the existing flat +string array `capabilities`. The flag and descriptor must both be present. +Neither alone establishes policy support. A legacy worker omits both. An object +inside `capabilities`, a misplaced descriptor, an invalid descriptor, or a +flag/descriptor mismatch refuses registration before normalization or writes. +Other existing capability entries retain their contracts. + +The producer validates and persists this entry as structured metadata. It must +not discard the descriptor during registration normalization or route it through +the flat string-list normalizer. It validates exact version and policy names, rejects +duplicates, and canonicalizes the policy list in `propagate`, `cancel_remaining`, +`settle` order. The canonical descriptor participates in registration identity, +held-poll snapshots and in-transaction claim/confirmation fences. A descriptor +change during a held poll makes that old poll stale, even if its flat flag is +unchanged. It cannot become the replacement process's claim authority. + +Producer discovery alone does not establish consumer support. Admission requires +both sides to support the selected policy. A worker must not advertise a policy +implemented only by a test flag or source proposal. The same fence applies to replacement-worker claim and replay eligibility. Before assigning or confirming a claim, the producer verifies that the worker @@ -108,6 +135,9 @@ remote activities work. prefixes and expected semantic outcomes. Binding owners copy applicable inputs into their existing replay suites and retain legacy fixtures unchanged. These source vectors are not an installed conformance result. +Registration/claim vectors separately cover the exact manifest placement, +legacy eligibility, malformed or incomplete declarations, retained-policy +requirements and a capability change during a held poll. The propagate wire discriminator and proposed per-policy capability can be reviewed independently of the producer mappings for `cancel_remaining` and diff --git a/conformance/parallel-groups/fixtures-v1.json b/conformance/parallel-groups/fixtures-v1.json index a31a3a2..0740fdc 100644 --- a/conformance/parallel-groups/fixtures-v1.json +++ b/conformance/parallel-groups/fixtures-v1.json @@ -9,7 +9,9 @@ "policies": [ "propagate" ] - } + }, + "registration_path": "capability_manifest.parallel_group_policies", + "registration_flag": "parallel_group_policies" }, "cases": [ { @@ -1093,5 +1095,354 @@ "parallel_group_mode": "select", "parallel_group_policy": "propagate" } + ], + "registration_claim_vectors": [ + { + "id": "legacy-registration-legacy-run", + "registration": { + "capabilities": [], + "capability_manifest": {} + }, + "run_required_policies": [], + "expected": { + "registration": "accepted", + "claim": "eligible" + } + }, + { + "id": "qualified-propagate-registration", + "registration": { + "capabilities": [ + "parallel_group_policies" + ], + "capability_manifest": { + "parallel_group_policies": { + "supported": true, + "version": 1, + "policies": [ + "propagate" + ] + } + } + }, + "producer_descriptor": { + "supported": true, + "version": 1, + "policies": [ + "propagate" + ] + }, + "run_required_policies": [ + "propagate" + ], + "expected": { + "registration": "accepted", + "stored_descriptor": { + "supported": true, + "version": 1, + "policies": [ + "propagate" + ] + }, + "claim": "eligible" + } + }, + { + "id": "flat-flag-without-descriptor", + "registration": { + "capabilities": [ + "parallel_group_policies" + ], + "capability_manifest": {} + }, + "expected": { + "registration": "refused", + "writes": false + } + }, + { + "id": "descriptor-without-flat-flag", + "registration": { + "capabilities": [], + "capability_manifest": { + "parallel_group_policies": { + "supported": true, + "version": 1, + "policies": [ + "propagate" + ] + } + } + }, + "expected": { + "registration": "refused", + "writes": false + } + }, + { + "id": "object-in-flat-capabilities", + "registration": { + "capabilities": [ + { + "parallel_group_policies": { + "supported": true, + "version": 1, + "policies": [ + "propagate" + ] + } + } + ], + "capability_manifest": { + "parallel_group_policies": { + "supported": true, + "version": 1, + "policies": [ + "propagate" + ] + } + } + }, + "expected": { + "registration": "refused", + "writes": false + } + }, + { + "id": "descriptor-at-wrong-registration-path", + "registration": { + "capabilities": [ + "parallel_group_policies" + ], + "parallel_group_policies": { + "supported": true, + "version": 1, + "policies": [ + "propagate" + ] + }, + "capability_manifest": {} + }, + "expected": { + "registration": "refused", + "writes": false + } + }, + { + "id": "duplicate-registration-policies", + "registration": { + "capabilities": [ + "parallel_group_policies" + ], + "capability_manifest": { + "parallel_group_policies": { + "supported": true, + "version": 1, + "policies": [ + "propagate", + "propagate" + ] + } + } + }, + "expected": { + "registration": "refused", + "writes": false + } + }, + { + "id": "future-registration-version", + "registration": { + "capabilities": [ + "parallel_group_policies" + ], + "capability_manifest": { + "parallel_group_policies": { + "supported": true, + "version": 2, + "policies": [ + "propagate" + ] + } + } + }, + "expected": { + "registration": "refused", + "writes": false + } + }, + { + "id": "legacy-worker-cannot-claim-explicit-run", + "registration": { + "capabilities": [], + "capability_manifest": {} + }, + "run_required_policies": [ + "propagate" + ], + "expected": { + "registration": "accepted", + "claim": "ineligible", + "run": "waiting" + } + }, + { + "id": "settled-group-retains-policy-requirement", + "registration": { + "capabilities": [ + "parallel_group_policies" + ], + "capability_manifest": { + "parallel_group_policies": { + "supported": true, + "version": 1, + "policies": [ + "propagate" + ] + } + } + }, + "run_required_policies": [ + "propagate", + "settle" + ], + "open_group_policies": [], + "expected": { + "registration": "accepted", + "claim": "ineligible", + "run": "waiting" + }, + "producer_descriptor": { + "supported": true, + "version": 1, + "policies": [ + "propagate", + "settle" + ] + } + }, + { + "id": "descriptor-change-invalidates-held-poll", + "held_poll_registration": { + "capabilities": [ + "parallel_group_policies" + ], + "capability_manifest": { + "parallel_group_policies": { + "supported": true, + "version": 1, + "policies": [ + "propagate", + "settle" + ] + } + } + }, + "replacement_registration": { + "capabilities": [ + "parallel_group_policies" + ], + "capability_manifest": { + "parallel_group_policies": { + "supported": true, + "version": 1, + "policies": [ + "propagate" + ] + } + } + }, + "producer_descriptor": { + "supported": true, + "version": 1, + "policies": [ + "propagate", + "settle" + ] + }, + "run_required_policies": [ + "propagate" + ], + "expected": { + "registration": "accepted", + "held_poll_claim": "stale_worker_registration", + "replacement_claim": "eligible" + } + }, + { + "id": "policy-order-canonicalization", + "registration": { + "capabilities": [ + "parallel_group_policies" + ], + "capability_manifest": { + "parallel_group_policies": { + "supported": true, + "version": 1, + "policies": [ + "settle", + "propagate" + ] + } + } + }, + "producer_descriptor": { + "supported": true, + "version": 1, + "policies": [ + "propagate", + "settle" + ] + }, + "run_required_policies": [ + "settle" + ], + "expected": { + "registration": "accepted", + "stored_descriptor": { + "supported": true, + "version": 1, + "policies": [ + "propagate", + "settle" + ] + }, + "claim": "eligible" + } + }, + { + "id": "null-registration-descriptor", + "registration": { + "capabilities": [ + "parallel_group_policies" + ], + "capability_manifest": { + "parallel_group_policies": null + } + }, + "expected": { + "registration": "refused", + "writes": false + } + }, + { + "id": "registration-policy-list-wrong-type", + "registration": { + "capabilities": [ + "parallel_group_policies" + ], + "capability_manifest": { + "parallel_group_policies": { + "supported": true, + "version": 1, + "policies": "propagate" + } + } + }, + "expected": { + "registration": "refused", + "writes": false + } + } ] } From d3ffbd1001fd6ae1d877fc39c533aa07544cd095 Mon Sep 17 00:00:00 2001 From: Richard McDaniel <1130888+rmcdaniel@users.noreply.github.com> Date: Fri, 13 Nov 2026 10:52:58 +0000 Subject: [PATCH 4/4] Close parallel policy descriptors and enumerate malformed declarations --- conformance/parallel-groups/README.md | 10 +- conformance/parallel-groups/fixtures-v1.json | 283 +++++++++++++++++++ 2 files changed, 291 insertions(+), 2 deletions(-) diff --git a/conformance/parallel-groups/README.md b/conformance/parallel-groups/README.md index 8ae59d2..202c4a4 100644 --- a/conformance/parallel-groups/README.md +++ b/conformance/parallel-groups/README.md @@ -43,8 +43,14 @@ The proposed discovery field is `policies` is a nonempty unique subset of the three policy names. Producers may admit one policy before the others, and list only policies actually implemented -and qualified. Wrong types, unknown versions or names, duplicates, missing -fields and `supported: false` do not authorize admission. +and qualified. This descriptor is a closed object with exactly `supported`, +`version` and `policies`. `supported` must be the boolean `true`, `version` must +be the integer `1`, and every policy must be a string. Unknown members, wrong +types (including boolean or string versions), unknown versions or names, +duplicates, missing fields, an empty policy list and `supported: false` refuse +explicit admission. No value is coerced or silently discarded. The same closed +descriptor rules apply to discovery and worker registration. Workers without +support omit the registration flag and descriptor. Worker registration uses the existing `POST /worker/register` body fields: diff --git a/conformance/parallel-groups/fixtures-v1.json b/conformance/parallel-groups/fixtures-v1.json index 0740fdc..58e2be7 100644 --- a/conformance/parallel-groups/fixtures-v1.json +++ b/conformance/parallel-groups/fixtures-v1.json @@ -1443,6 +1443,289 @@ "registration": "refused", "writes": false } + }, + { + "id": "unknown-registration-descriptor-member", + "registration": { + "capabilities": [ + "parallel_group_policies" + ], + "capability_manifest": { + "parallel_group_policies": { + "supported": true, + "version": 1, + "policies": [ + "propagate" + ], + "allow_legacy": true + } + } + }, + "expected": { + "registration": "refused", + "writes": false + } + }, + { + "id": "registration-supported-false", + "registration": { + "capabilities": [ + "parallel_group_policies" + ], + "capability_manifest": { + "parallel_group_policies": { + "supported": false, + "version": 1, + "policies": [ + "propagate" + ] + } + } + }, + "expected": { + "registration": "refused", + "writes": false + } + }, + { + "id": "registration-supported-string", + "registration": { + "capabilities": [ + "parallel_group_policies" + ], + "capability_manifest": { + "parallel_group_policies": { + "supported": "true", + "version": 1, + "policies": [ + "propagate" + ] + } + } + }, + "expected": { + "registration": "refused", + "writes": false + } + }, + { + "id": "registration-supported-number", + "registration": { + "capabilities": [ + "parallel_group_policies" + ], + "capability_manifest": { + "parallel_group_policies": { + "supported": 1, + "version": 1, + "policies": [ + "propagate" + ] + } + } + }, + "expected": { + "registration": "refused", + "writes": false + } + }, + { + "id": "registration-version-boolean", + "registration": { + "capabilities": [ + "parallel_group_policies" + ], + "capability_manifest": { + "parallel_group_policies": { + "supported": true, + "version": true, + "policies": [ + "propagate" + ] + } + } + }, + "expected": { + "registration": "refused", + "writes": false + } + }, + { + "id": "registration-version-string", + "registration": { + "capabilities": [ + "parallel_group_policies" + ], + "capability_manifest": { + "parallel_group_policies": { + "supported": true, + "version": "1", + "policies": [ + "propagate" + ] + } + } + }, + "expected": { + "registration": "refused", + "writes": false + } + }, + { + "id": "registration-version-fractional-number", + "registration": { + "capabilities": [ + "parallel_group_policies" + ], + "capability_manifest": { + "parallel_group_policies": { + "supported": true, + "version": 1.0, + "policies": [ + "propagate" + ] + } + } + }, + "expected": { + "registration": "refused", + "writes": false + } + }, + { + "id": "registration-policies-empty", + "registration": { + "capabilities": [ + "parallel_group_policies" + ], + "capability_manifest": { + "parallel_group_policies": { + "supported": true, + "version": 1, + "policies": [] + } + } + }, + "expected": { + "registration": "refused", + "writes": false + } + }, + { + "id": "registration-policy-unknown", + "registration": { + "capabilities": [ + "parallel_group_policies" + ], + "capability_manifest": { + "parallel_group_policies": { + "supported": true, + "version": 1, + "policies": [ + "future" + ] + } + } + }, + "expected": { + "registration": "refused", + "writes": false + } + }, + { + "id": "registration-policy-member-wrong-type", + "registration": { + "capabilities": [ + "parallel_group_policies" + ], + "capability_manifest": { + "parallel_group_policies": { + "supported": true, + "version": 1, + "policies": [ + "propagate", + 1 + ] + } + } + }, + "expected": { + "registration": "refused", + "writes": false + } + }, + { + "id": "registration-descriptor-wrong-object-type", + "registration": { + "capabilities": [ + "parallel_group_policies" + ], + "capability_manifest": { + "parallel_group_policies": [] + } + }, + "expected": { + "registration": "refused", + "writes": false + } + }, + { + "id": "registration-missing-supported", + "registration": { + "capabilities": [ + "parallel_group_policies" + ], + "capability_manifest": { + "parallel_group_policies": { + "version": 1, + "policies": [ + "propagate" + ] + } + } + }, + "expected": { + "registration": "refused", + "writes": false + } + }, + { + "id": "registration-missing-version", + "registration": { + "capabilities": [ + "parallel_group_policies" + ], + "capability_manifest": { + "parallel_group_policies": { + "supported": true, + "policies": [ + "propagate" + ] + } + } + }, + "expected": { + "registration": "refused", + "writes": false + } + }, + { + "id": "registration-missing-policies", + "registration": { + "capabilities": [ + "parallel_group_policies" + ], + "capability_manifest": { + "parallel_group_policies": { + "supported": true, + "version": 1 + } + } + }, + "expected": { + "registration": "refused", + "writes": false + } } ] }