From 2d64db6af777ca5e89ac6d34c8c523de9a363103 Mon Sep 17 00:00:00 2001 From: wshallwshall Date: Thu, 6 Aug 2026 09:09:39 -0500 Subject: [PATCH 1/2] docs(research): OpenFlow step-attribute vocabulary gap-map (BACKLOG #238) A findings note comparing Windmill's seven OpenFlow step-attributes (retry, timeout, stop_after_if, skip_if, continue_on_error, mock, cache_ttl) against MessageFoundry's engine/handler vocabulary. Per attribute: what it is, the engine analogue (grounded in a real symbol/file), and the gap (covered-different-locus / partial / absent). This is a review, not a feature. OpenFlow is explicitly NOT a compatibility target; the note is an informational vocabulary map, not a gap-to-close list. Adopting a declarative artifact stays declined by ADR 0076 section 7 and BACKLOG #26. Framing is conditional throughout (not-deployed beta). Lands at docs/research/openflow-step-attributes.md, mirroring the existing docs/research/ review-note convention (config-ux-review.md, message-model-eval.md, ide-low-code-options.md). The mock row cites config/db_lookup.py / config/fhir_lookup.py for the pure-dry-run raise guard (config/db_lookup.py raise DbLookupError / config/fhir_lookup.py raise FhirLookupError), not config/wiring.py whose raise text is about the router phase. --- docs/research/openflow-step-attributes.md | 49 +++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 docs/research/openflow-step-attributes.md diff --git a/docs/research/openflow-step-attributes.md b/docs/research/openflow-step-attributes.md new file mode 100644 index 00000000..4b67698d --- /dev/null +++ b/docs/research/openflow-step-attributes.md @@ -0,0 +1,49 @@ +# OpenFlow step attributes vs. the engine vocabulary (research / findings) + +**Date:** 2026-08-06 · **Status:** research / findings (no code) · **Owner action:** none required — informational vocabulary map. + +This is BACKLOG **[#238](../BACKLOG.md)**. It reads Windmill's **OpenFlow** step-attribute vocabulary +(Apache-2.0, safe to read and cite) as a **completeness checklist** against MessageFoundry's own +step/connector semantics, and records, per attribute, whether the engine already covers it (and where), +covers it partially, or does not have it — and why. **OpenFlow is explicitly NOT a compatibility +target.** Emitting or consuming it is a separate, unauthorized question, and adopting a *declarative +artifact* remains declined by [ADR 0076](../adr/0076-typed-action-vocabulary-action-list-lens.md) §7 and +BACKLOG #26. This note exists so the mapping need not be re-derived; the gaps below are described as +**vocabulary differences, not defects**, and nothing here is a to-build list. MessageFoundry is a +not-deployed beta, so claims are stated in the conditional. + +The one fact worth leading with: **most of these attributes already have an engine analogue, but at a +different locus** — a per-connection / delivery / pipeline policy, or plain Python control flow inside a +Handler — rather than as an attribute hanging off a single step row. Saying *where* each lives precisely +is most of the value here. + +## The seven attributes, mapped + +| Attribute | What it is (OpenFlow) | Engine analogue (symbol · file) | Gap | +|---|---|---|---| +| `retry` | A per-step retry policy (constant or exponential backoff, N attempts) before the step is treated as failed. | `RetryPolicy` (`config/models.py`) — `max_attempts` / `backoff_seconds` / `backoff_multiplier` / `max_backoff_seconds`; attached per-outbound as `Destination.retry`, drained by the outbound delivery worker under the staged-queue at-least-once model. | **Covered, different locus** — retry is a per-**outbound-connection** delivery policy, not a per-handler-row attribute. | +| `timeout` | A per-step wall-clock timeout after which the step is killed. | Boundary timeouts only: `Validation.strict_timeout_s` (`config/models.py`) / `_STRICT_VALIDATE_TIMEOUT_SECONDS` (`pipeline/wiring_runner.py`) bound strict validation; `_LOOKUP_RESULT_TIMEOUT_SECONDS` (`pipeline/wiring_runner.py`) bounds a bridged `db_lookup`/`fhir_lookup`; connectors carry their own (`timeout_seconds`/`connect_timeout` in `transports/tcp.py`, `transports/mllp.py`; `acquire_timeout` in `transports/database.py`). | **Partial** — timeouts bound the external / parse boundaries; there is **no** generic per-handler/transform wall-clock timeout (a pure transform is CPU-bounded by design). | +| `stop_after_if` | Stop the flow early (as success or skip) when an expression over the step result is true. | A Router that forwards to no / fewer handlers yields `UNROUTED`; a Handler that returns no `Send` yields `FILTERED` (`disposition_for`, `pipeline/dryrun.py`; the count-and-log invariant). Control flow projects as `if` rows in the Steps view (`_emit_if`, `lens.py`). | **Partial / structural mismatch** — the config is a **graph** with no linear "steps after this" to stop; "stop the flow" is expressed by a Router/Handler declining to forward, not a post-step early-terminate knob. | +| `skip_if` | Skip this step when an expression is true; the flow continues past it. | Ordinary Python `if cond: return` in a `@router` / `@handler`; the router's `accepts=` seam filters which handlers ever see a message; also projects as an `if` row (`_emit_if`, `lens.py`). | **Covered, as code** — conditional skipping is plain control flow in a code-first Handler, deliberately Python rather than a declarative attribute (the #26 differentiator). | +| `continue_on_error` | Let the flow proceed when this step errors, instead of failing the run. | `InternalErrorPolicy.CONTINUE` (`config/models.py`) is the **default** — dead-letter the offending row (replayable) and keep the lane moving; each outbound drains independently; post-ACK routing/transform errors are logged `ERROR` / dead-lettered, never fatal (count-and-log). `STOP` is the opt-in opposite. | **Covered, different locus + inverted default** — error-and-continue is the pipeline **default** at the delivery / row level, per-connection, not a per-handler-row toggle. | +| `mock` | Replace a step's execution with a fixed, canned result (for testing a flow). | No per-step canned-result substitution. Adjacent: the traced dry-run (`pipeline/dryrun.py`; [ADR 0072](../adr/0072-traced-dryrun-mode.md)) runs routing + handling with **no** connectors / network (delivery previewed, not executed), and `Destination.simulate` (`config/models.py`) shadow-suppresses real egress. | **Largely absent** — the engine can mock **delivery** (dry-run / shadow suppress egress) but cannot feed a step a fixed stand-in result; `db_lookup` / `fhir_lookup` **raise** in a pure dry-run rather than returning a mock (`config/db_lookup.py` / `config/fhir_lookup.py`). | +| `cache_ttl` | Cache a step's result for N seconds, reusing it on re-run within the window. | None for handler / step results. The reliability model **requires** routers / transforms to be pure and re-runnable with identical output ([ADR 0001](../adr/0001-staged-pipeline-architecture.md)); the sanctioned non-pure inputs `db_lookup` / `fhir_lookup` ([ADR 0010](../adr/0010-handler-callable-db-lookup.md) / [ADR 0043](../adr/0043-fhir-read-lookup.md)) are deliberately **live** reads whose result may differ on a re-run. | **Absent, by design** — a per-step result cache would contradict the pure-re-run at-least-once invariant; the caches the engine does keep are engine-internal infrastructure, not step-result caches. | + +*Caveat:* `file:line` anchors are a 2026-08-06 snapshot and drift — the table cites **symbol names** as +the primary anchor (`RetryPolicy`, `InternalErrorPolicy`, `Destination.simulate`, +`_LOOKUP_RESULT_TIMEOUT_SECONDS`, `disposition_for`, `_emit_if`); re-confirm at read time and cite +symbols, not just lines. + +## Reading of the gap-map + +Most of the seven are **already covered engine-side, at a different locus** — a connection / delivery / +pipeline policy (`retry`, `continue_on_error`, and most of `timeout`) or plain Python control flow inside +a Handler (`skip_if`, and `stop_after_if` as a Router / Handler declining to forward) — rather than as a +per-step-row attribute. Only two are genuinely absent: `mock` (the engine mocks **delivery**, not step +results) and `cache_ttl` — and `cache_ttl` is absent **by design**, because a per-step result cache would +fight the pure-re-run at-least-once invariant the pipeline depends on. + +This confirms the item's framing: OpenFlow compatibility is unnecessary and unwanted here. The +vocabulary is a **lens on semantics the engine already has**, expressed at the connection / delivery / +pipeline level or as code-first control flow — not a gap-to-close list, and not a case for emitting or +consuming a declarative artifact. From 7ddc690b3bb4948f0c549bb1273fc1c80057f75b Mon Sep 17 00:00:00 2001 From: wshallwshall Date: Thu, 6 Aug 2026 09:10:04 -0500 Subject: [PATCH 2/2] docs(backlog): flip #238 banner to CLOSED (BACKLOG #238) The findings note (docs/research/openflow-step-attributes.md) is the item's expected output and is now delivered, so #238's banner flips from open to closed. One banner line only, under the #238 heading. Census NOT recomputed: this commit changes only #238's banner line and does not touch the ranked table or the four census distribution lines. --- docs/BACKLOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/BACKLOG.md b/docs/BACKLOG.md index 911cf3da..6bfde870 100644 --- a/docs/BACKLOG.md +++ b/docs/BACKLOG.md @@ -2513,7 +2513,7 @@ def route_demo_oru(msg): ## 238. OpenFlow step-attribute completeness pass over the engine vocabulary -> 🔢 **Filed 2026-07-30 — not started.** Value **1/10** · Difficulty **1/10** · _fill-in_. A review whose output is findings, not a feature; OpenFlow is explicitly **not** a compatibility target. +> ✅ **CLOSED 2026-08-06 — findings note delivered.** Value **1/10** · Difficulty **1/10**. The gap-map lives at [docs/research/openflow-step-attributes.md](research/openflow-step-attributes.md); OpenFlow remains explicitly **not** a compatibility target — the note is a vocabulary map, not a gap-to-close list. **Cluster:** IDE & Authoring / Engine. **Priority:** P3. **Verdict:** build (a review, not a feature). **Severity:** none — this is a gap-analysis task whose output is findings.