Repository navigation
Conversation
There was a problem hiding this comment.
🟡 Changes recommended
Chained comparisons eagerly evaluate operands after a false comparison, so an otherwise skipped JSON parse can raise ValueError and fail the workflow.
1 open issue
What changed in this PR
This PR addresses #5071 by making Spec Kit workflow expressions evaluate chained comparisons pairwise, correcting range checks that could select the wrong workflow branch.
Changes:
- Scan top-level comparison operators left to right, respecting quoted strings and brackets.
- Compare adjacent operands with an implicit
and. - Add regression coverage for range checks, mixed operators, boolean precedence, and quoted operator text.
AI disclosure: GitHub Copilot (model: unknown; autonomous) reviewed the changes and suggested a fix on behalf of the requester.
| File | Description |
|---|---|
tests/test_workflows.py |
Adds chained-comparison regression cases. |
src/specify_cli/workflows/expressions/engine_current.py |
Adds comparison-chain scanning and pairwise evaluation. |
Review effort: Balanced (auto)
| values = [ | ||
| _evaluate_simple_expression(operand.strip(), namespace) | ||
| for operand in operands | ||
| ] |
|
Please address Copilot feedback |
|
Re-checked against This PR's diff targets the removed hand-written engine, hence the conflict. Before I rework it against the grammar, which direction do you want?
I will implement whichever you prefer, with regression tests either way. The Copilot finding on the previous diff (eager operand evaluation after a decided pair) will be addressed in the rework regardless — the new engine's short-circuit style for AI disclosure: this comment and the analysis behind it were produced with Claude Code (model: Claude Fable 5, default reasoning effort, human-supervised), posted on behalf of @aarjang, who reviewed it before posting. |
|
Let's go with option 1: pairwise comparisons with Python-style short-circuiting. Python evaluates AI disclosure: Posted on behalf of @mnriem using GitHub Copilot (model: GPT-6.1 Sol, interactive mode, human-supervised). AI assessed Python's comparison semantics and drafted this response; @mnriem approved the wording for posting. |
A chained comparison such as 0 <= inputs.n <= 10 grouped left to right, comparing a boolean with the next operand, so the range check answered True for n = 50 and an if/while step silently took the wrong branch. Evaluate comparisons as Python and Jinja2 do: a OP b OP c is a OP b and b OP c, with the shared operand evaluated once and evaluation stopping at the first false pair, so a later operand is only reached when every pair before it held (the same guard behavior as and). Explicit parentheses still end a chain: (0 <= n) <= 10 compares the grouped boolean, exactly as in Python. The grammar is unchanged; cmp nodes flatten into one chain node the way and/or and filter chains already do, a parenthesized comparison stays one grouped operand, and validation still walks every operand. Fixes github#5071 Assisted-by: Claude Code (model: Claude Fable 5, supervised)
b4309cc to
a324625
Compare
| elif tag == "group": | ||
| _visit(node[1], paths, filters) | ||
| elif tag == "chain": | ||
| for operand in node[1]: | ||
| _visit(operand, paths, filters) |
|
Reworked as option 1 in a324625 — the branch is rebuilt on current
AI disclosure: This comment and the commit were drafted with Claude Code (model: Claude Fable 5), human-directed and reviewed, on behalf of @aarjang. |
|
Please address Copilot feedback |


Fixes #5071.
Problem
A chained comparison such as
{{ 0 <= inputs.n <= 10 }}does not behave like a range check. On the Lark engine (after #5171) the documented left-to-right grouping evaluates it as(0 <= inputs.n) <= 10, so withn = 50the first pair yieldsTrueand_safe_compare(True, 10, "<=")answersTrue— anif/whilestep silently takes the wrong branch for every out-of-range value. Authors reading the expression as Python or Jinja2 (wherea <= b <= cmeansa <= b and b <= c) get the opposite of what they wrote, with no error.Fix (option 1, as chosen by the maintainer in the PR discussion)
Python/Jinja2 chained-comparison semantics in
engine.py, grammar unchanged:_BuildAst.cmpnow flattens the grammar's left-nested comparisons into one("chain", operands, ops)node — the same flatteningand/orand filter chains already use._evalevaluates a chain pairwise, left to right: each shared operand is evaluated once, and evaluation stops at the first false pair, so a later operand is only reached when every pair before it held (the same guard behavior asand; a reached operand's errors still fail the run)._BuildAst.groupkeeps a parenthesized comparison as a single grouped operand, so{{ (0 <= inputs.n) <= 10 }}compares the grouped boolean with10— exactly Python's own reading of that expression._visitwalks every chain operand, so_collect_leaves/_evaluator_rejectsstill see all names and filters (as they do forand/or).docs/workflows/expressions.mdand the engine docstring now document chaining instead of left-to-right grouping.Tests & evidence
test_comparisons_chain_like_python— every case asserted against Python's own result for the same expression, including mixed-operator chains, a four-operand chain, and the parenthesized forms. Fails onmain(e.g.0 <= 50 <= 10evaluatesTruethere), passes on this branch.test_chained_comparison_short_circuits_like_and— a failing pair skips a later operand whose filter would raise; a reached operand still raises. Fails onmain, passes here.test_chained_comparison_evaluates_the_shared_operand_once— pins the single evaluation of the shared operand (guards against a naivea <= b and b <= crewrite); passes on both, kept as a contract.maincheckout of this macOS machine — the 11 known composed-template script-parity flakes (each passes in isolation), plustests/specify_cli/bundles/test_references.py::test_community_step_is_not_treated_as_bundled(fails identically on cleanmain) andtest_unknown_reference_errors_online(full-suite only; passes in isolation). None touch the expressions package.Notes
main(the original patch targetedengine_current.py, which feat(workflows): parse workflow expressions with a Lark LALR grammar #5171 removed), hence the force-push.AI disclosure
This PR was developed with Claude Code (model: Claude Fable 5), human-directed and reviewed, on behalf of @aarjang. Commits are authored by @aarjang and carry an
Assisted-bytrailer.