Skip to content

workflow DSL: decide the eight open design questions, and fix what verifying them exposed - #610

Merged
Masterplanner25 merged 1 commit into
mainfrom
workflow-dsl-decisions
Aug 26, 2026
Merged

Masterplanner25 merged 1 commit into
mainfrom
workflow-dsl-decisions

Conversation

@Masterplanner25

Copy link
Copy Markdown
Owner

Summary

A decision session over the eight open workflow-DSL design questions — #468,
#472, #479, #480, #481, #488, #577, #578. Each was filed separately, from a
different corpus, against v5.0.4. Read side by side at 5.5.0 they constrain each
other, and three stop being independent.

docs/design/workflow-dsl/00-cluster-decisions.md carries a decision for each,
the sequence, and what was rejected. This PR does not implement any of them.

The decisions, in one line each

Decision
D1 Prerequisite (#609): an unrecognised type name becomes an error; map is added; record is made spellable or removed. Warn in 5.x, error at 6.0.0
D2 #479 splits. tool.register schema-from-signature ships first and forces D1; a step's returns: waits for it. Two vocabularies with a derivation between them, deliberately not unified
D3 #472 uses the runtime schema dialect. workflow_wait argument 2 type-dispatches — string is correlation_key as today, map is an options map carrying schema. There is no free positional slot
D4 #481 declares parameters on the workflow and binds them at run_workflow(flow, args_map) — not run_workflow(build("lite")). Arguments become part of run identity and ride #470's mechanism
D5 #480 adopts Airflow's mapped-node model: the graph never grows, a declared node's cardinality is discovered at run time. Empty upstream → skipped, non-list → upstream_failed, re-expansion to a different length → refused as topology drift
D6 #468 is subsumed by D5. Close it. TaskNode already carries max_retries/timeout_ms/cache per node, so a mapped node gives "retry only the two that failed" for free — no eighth status, no envelope, no second definition of "done" for @exactly_once to disagree with
D7 #577 takes per-step compensate_with, not a workflow exit hook — the hook needs run-status visibility inside a step body, which is a larger change than the feature. A compensated run is terminal
D8 #578's deciding question is answered (conditional writers are refused, mirroring #500 and merge: validation). The barrier itself is deferred — it duplicates after, more narrowly
D9 #488 gets budget { ..., limits: { <meter>: <n> } }. Nested, so the outer vocabulary stays closed and parse-checkable while the open part is explicitly host-resolved. A declared meter with no accountant is an error

What verifying them exposed

Every claim in all eight issues was re-executed against src/ at 5.5.0 before
any decision was made. Three findings the issues do not record:

1. The static type vocabulary is unsound — and three of the eight want to build
on it.
parse_type_name is self.eat("ID").val; type_system falls back to
ANY. So one transposed letter silently disables checking:

fn a(name: string) -> string { return name }
fn b(name: strng)  -> string { return name }
fn main() { print(a(42i)); print(b(42i)) }
$ nodus check typo.nd
Type error at typo.nd:3:21: expected string but got int

One error, for a. Also: map is unnameable and looks nameable — and it is what
run_workflow and most step bodies return. And record is in TYPE_NAMES but is
a keyword, so it is a dead entry. This is the declared-but-not-enforced shape
(#467, #473, #478, #492) at the annotation surface. Reported (open): #609.

2. An after edge carries the dependency's value, and nothing said so. Inside
a step body each name declared with after is bound to that step's return value,
with correct scoping — an undeclared read is Undefined variable. Documented in
neither the guide, llms.txt nor llms-full.txt, so a reader concludes all data
must route through state. New §3.1, with the consequence: a skipped producer
binds nil, indistinguishable from a step that returned nil, and only
r["statuses"] tells them apart — r["steps"] omits it entirely.

3. llms-full.txt's workflow example had never parsed. step validate after []
is Expected identifier, got '['. It survived because the gate's document list had
two AI-discoverability files in it and covered one — llms.txt was scanned,
llms-full.txt was not. So the file 5.5.0 shipped inside the wheel for agents to
read
was the one nothing checked. Fixed, and llms-full.txt is now in the list;
its other three blocks were already clean.

Two symbol suppressions come with it, both prose rather than claims: std:module
is the placeholder spelling of the import form, and std:string is named
deliberately in order to say that it fails.

Testing

Gate Result
--static PASS 135/135
--runtime PASS 251/251 (was 245 — six new blocks, all executed)
--contracts PASS 6/6
--opcodes PASS 26/26
--shapes PASS 39 known, 0 new
--versions PASS 15/15
ruff / mypy clean

Every guide and llms-full.txt example in this PR was run against dev source and
its output pasted verbatim.

Noticed, not done

docs/guide/workflows-and-tasks.md has two ### 4.2 headings, both under ## 5. Failure and recovery (lines 508 and 582), alongside a ### 4.1 and a ### 4.0.
Pre-existing; renumbering would move anchors other documents may link to, so it is
left for a pass that can check inbound links.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Ng5gg1i8SSCgpHbkNt6zZa

…rifying them exposed

Reads #468, #472, #479, #480, #481, #488, #577 and #578 side by side against
src/ at 5.5.0 -- each was filed separately, from a different corpus, against
5.0.4 -- and records a decision for every one. Three of them stop being
independent when read together.

docs/design/workflow-dsl/00-cluster-decisions.md carries the decisions, the
sequence, and what was rejected. Not version-pinned, because the cluster is
not: most of it is additive across 5.x minors and exactly one member joins the
6.0.0 cohort.

The verification pass turned up three things the issues do not record.

The static type vocabulary is unsound, and three of the eight want to build on
it. parse_type_name is `self.eat("ID").val` and type_system falls back to ANY,
so `fn b(name: strng)` checks clean and silently disables checking on that
parameter; `map` is unnameable and looks nameable; `record` is in TYPE_NAMES
but is a keyword, so it is a dead entry. Reported (open): #609. Nothing typed
should be added to the DSL until it is fixed -- a `returns: "recrod"` meaning
"any type at all" would be a new instance of the declared-but-not-enforced
shape the last three releases were spent removing.

An `after` edge carries the dependency's value, and nothing said so. Inside a
step body each name declared with `after` is bound to that step's return value,
with correct scoping -- an undeclared read is `Undefined variable`. Documented
in neither docs/guide/, llms.txt nor llms-full.txt, so a reader concludes all
data must route through `state`. New guide section 3.1, with the case that
follows from it: a `skipped` producer binds `nil`, indistinguishable from a
step that returned `nil`, and only r["statuses"] tells them apart.

llms-full.txt's workflow example had never parsed. `step validate after []` is
`Expected identifier, got '['`. It survived because the gate's document list
had two AI-discoverability files in it and covered one -- llms.txt was scanned,
llms-full.txt was not, so the file 5.5.0 shipped inside the wheel for agents to
read was the one nothing checked. Fixed, and llms-full.txt is now in the list;
its other three blocks were already clean. Two symbol suppressions come with
it, both prose rather than claims: `std:module` is the placeholder spelling of
the import form, and `std:string` is named deliberately to say that it fails.

Gates: static 135/135, runtime 251/251 (was 245 -- six new blocks, all
executed), contracts 6/6, opcodes 26/26, shapes 0 new, versions 15/15, ruff and
mypy clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ng5gg1i8SSCgpHbkNt6zZa
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant