4.0 KiB
4.0 KiB
T3.4 — StepId stability checks
| Field | Value |
|---|---|
| Phase | P3 — Workflow as data |
| Size | S — under 1 day |
| Status | Not started |
| Flags | — |
| Spec | inlined below |
| Blocks | — |
Goal
Report added, removed and retained StepIds across a version bump, and reject a
bump that retains nothing unless it is explicitly marked a rewrite.
Facts (inlined — no spec read needed)
- Credit assignment attributes outcomes to steps across workflow versions.
That requires a step identity surviving edits: insert a step at position 2 and
every positional index shifts, but
StepIddoes not. - The framework cannot infer this. It is a documented obligation on the
workflow author, enforced by three load-time checks:
StepIdunique within a version.- On a version bump, report added, removed and retained ids. A version retaining no ids from its parent is almost certainly a renumbering accident and is rejected unless explicitly marked as a rewrite.
StepIdis opaque to the framework — never parsed, never ordered, never assumed numeric.
- This is why the stability contract is load-bearing rather than cosmetic: per-step credit (T5.10) and the whole attribution path depend on it.
Steps
- Take parent and child
WorkflowDefs; compute the three sets: added, removed, retained. - Emit the report as structured data, not a log line — it belongs on the version record so it is auditable later.
- Reject when
retained.is_empty()and the child is not marked as a rewrite. Name the count of added and removed ids in the error. - Add the rewrite marker as an explicit field on the workflow definition, so marking it is a deliberate edit in the user's own file.
- Audit for any code that parses, sorts numerically, or ranges over
StepId. Delete it — ordering comes fromtransitions, not from the id. - Test both directions: renumbering every id rejected by default, accepted with the marker.
Acceptance
- Renumbering every step id is rejected by default and accepted with the explicit marker.
Verify
Harness: version pairs committed as fixtures — parent and child — covering each diff shape.
Integration test — tests/it_stepid_stability.rs:
- Renumber everything, no marker: assert rejected, and that the error names the added/removed counts.
- Renumber everything, marker set: assert accepted.
- Insert a step at position 2, ids unchanged: assert accepted, and that the report shows 1 added, 0 removed, N retained. This is the case the whole contract exists for.
- Rename one of five ids: assert accepted with 1 added, 1 removed, 4 retained — a partial rename must not trip the all-or-nothing rule.
- Duplicate id within one version: rejected.
- Assert the report is persisted on the version record, readable after a restart — not emitted only as a log line.
- Opacity audit: grep the codebase for numeric parsing, sorting or ranging over
StepId; assert none. Add aStepIdin a fixture that is non-numeric and contains a., and assert nothing breaks.
Command: cargo test -p workflow stepid_stability
False pass:
- Only testing steps 1 and 2. The rule "reject when retained is empty" is trivially satisfiable by an implementation that computes nothing else — steps 3 and 4 are what prove the diff is real.
- Asserting the report exists in stdout. Credit assignment reads it later from storage; step 6 is the check that matters.
- Fixtures whose ids are
step1,step2, … so accidental numeric ordering is indistinguishable from correct behaviour. Step 7's odd id is the guard.
Traps
- Sorting steps by
StepIdfor display and then depending on that order. - Auto-generating
StepIdfrom position when the author omits one. That guarantees the renumbering accident the check exists to catch.
Background (not required to do this task): rust-agentic-sys.md §4.3, §11.7 · rust-agentic-task.md