feat(grammar): an app that needs asking twice had no way to be asked - #366
Merged
Conversation
Repetition and branching were refused on the grounds that a trace must be a recording of what happened, not a program that decides what to do. That reasoning holds. What it did not follow from is that the loop had to be refused - only that it must not survive into the trace. `repeat:` (with `until:` and a required `max:`) and `when:` are blocks, and both expand while RECORDING: the condition is read against the live app, and what lands in the trace is the passes that actually ran. Replay reads a flat list and still decides nothing. If the condition never holds within `max:`, recording fails and names the bound rather than pressing forever. The step-shaped refusals stay, and now name the block that does the job.
There was a problem hiding this comment.
Pull request overview
This PR extends the flow authoring grammar with recording-time-only control-flow blocks (repeat: and when:) so authors can express “press until it settles” / “run recovery if error is visible” while still emitting a flat, deterministic trace for replay.
Changes:
- Added
repeat:andwhen:step shapes to the YAML spec model, including strict parsing/serialization and round-trip tests. - Implemented recording-time expansion in the recorder so only the concrete steps that actually ran are written into the trace.
- Updated refusals, docs, and changelog to reflect the new block-level approach (loops/branches remain refused as step-shaped prose).
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| docs/authoring.md | Documents repeat:/when: semantics and updates the “Refused shapes” guidance to point at the new blocks. |
| crates/flowproof-agent/src/spec.rs | Adds SpecStep::Repeat / SpecStep::When, their structs, YAML parsing/serialization, and spec-level tests. |
| crates/flowproof-agent/src/rules.rs | Updates declined-shape error messaging and corresponding tests to reference repeat:/when:. |
| crates/flowproof-agent/src/recorder.rs | Implements recording-time expansion and adds recorder tests proving traces remain flat and bounded. |
| CHANGELOG.md | Adds a changelog entry explaining the motivation and guarantee (“expanded while recording, trace stays flat”). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+1731
to
+1735
| ResolvedAction::AssertText { | ||
| target, | ||
| expected, | ||
| matcher, | ||
| .. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What was wrong
Repetition and branching were refused, on the grounds that a trace must be a
recording of what happened and not a program that decides what to do.
That reasoning holds. What it did not follow from is that the loop had to be
refused — only that it must not survive into the trace.
What holds now
repeat:(withuntil:and a requiredmax:) andwhen:are blocks, and bothexpand while recording. The condition is read against the live app, and what
lands in the trace is the passes that actually ran — ordinary concrete steps, no
repeatand nowhen. Replay reads a flat list and still decides nothing.foreachalready made this bargain one phase earlier, at parse time — which itcan only do because its list is known before anything runs.
If the condition never holds within
max:, recording fails and names the boundrather than pressing forever. Each
repeat:carries its own budget, so two loopsin one flow do not share one.
Conditions read state and never wait:
page [does not] show <text>,the "<target>" shows <text>,the "<target>" is [not] visible. Anything elseis refused by name rather than guessed at.
The step-shaped refusals (
Click "Next" until …,If … , …) stay, and now namethe block that does the job — a refusal whose alternative is real is the one
worth keeping.
Proof
Two pages of a public obstacle web page, recorded and replayed green:
pinned seed; replays deterministically. Unpinned it does not replay, and the
failure is the honest one: the page redraws a fresh random bound, so the extra
press lands on a modal. Pinning randomness is what makes a recorded loop mean
something on the next run.
which is
repeat:with awhen:recovery inside it. The trace is 18 presses:three faults recovered, then a clean run of ten. Flat, and readable top to
bottom.
Both conditions had to be scoped to an element rather than the page.
page shows Enoughandpage shows ERRORboth matched the page's own description text —the second one fired the recovery on every pass and drove the loop into its
bound, which is exactly what the bound is for. That trap is documented.
Tests
repeat:records the passes that actually ran — two, not a loop and not themaxof five — and the trace contains no control flowrepeat:that never settles stops at its bound and names itwhen:whose condition is false records nothingrepeat:/when:survive a YAML round trip;when:serializes back flatrepeat:missinguntil:ormax:is rejected; a barewhen:is told theshape it should have had
cargo fmt --check,cargo clippy --workspace --all-targets -D warningsandcargo test --workspace(57 suites) are green. Ratchets: rust tests 792 → 796,diff 399 lines.
Not in this PR
Obstacle 73589 is a hand-driven bubble sort — the decision at each position is
"is the left number greater than the right", and conditions have no numeric
comparison. That predicate, and the corpus status doc it would update, follow
next.