Repository navigation
[Bug]: Unsupported expression syntax evaluates to None silently instead of raising #5073
Description
Bug Description
When the workflow expression evaluator meets syntax it does not parse, it does not raise. It treats the text as a variable path, which resolves to None (_resolve_dot_path, reached from src/specify_cli/workflows/expressions.py:969). A condition that uses such syntax then quietly gives the wrong result. The following cases were confirmed:
- Exponent-form numbers. Numeric literals and numeric strings are parsed with
float()only when they contain a., and otherwise withint()(expressions.py:930-935for literals,expressions.py:972-976for_coerce_number). So1e3is not read as a number: as a literal it is looked up as a variable and becomesNone, and the string"1e3"stays a string in comparisons. Workflow input coercion does accept"1e3"as the number1000(src/specify_cli/workflows/engine.py:1449-1456), so the same text means different things in different places. - Escaped quotes in string literals. A string literal is recognised only if its first matching quote is its last character (
expressions.py:800), so'it\'s'is looked up as a variable and becomesNone. As a result,inputs.name == 'it\'s'is false forname = "it's". - Chained indexing. The indexed-segment pattern accepts one index only (
expressions.py:304), somatrix[0][1]is looked up as the literal key"matrix[0][1]"(expressions.py:331) and returnsNone. - Word operators next to a quote.
and,orandinare only found with a space on both sides (expressions.py:884,expressions.py:893,expressions.py:715).inputs.a=='x'and inputs.bis therefore split only at==, its right side'x'and inputs.bis looked up as a variable and becomesNone, and the result is false.
Steps to Reproduce
- From a fresh clone of spec-kit, run
uv sync. - Save the script below as
repro.pyin the repository root. - Run
uv run python -I repro.py.
from specify_cli.workflows.base import StepContext
from specify_cli.workflows.engine import WorkflowEngine
from specify_cli.workflows.expressions import evaluate_condition, evaluate_expression
ctx = StepContext(
inputs={"n": 5, "s": "1e3", "name": "it's", "a": "x", "b": True},
steps={"plan": {"output": {"matrix": [["a", "b"], ["c", "d"]]}}},
)
cases = [
("1", "{{ 1e3 }}", "value"),
("1", "{{ inputs.n < 1e3 }}", "cond"),
("1", "{{ inputs.s > 100 }}", "cond"),
("2", r"{{ 'it\'s' }}", "value"),
("2", r"{{ inputs.name == 'it\'s' }}", "cond"),
("3", "{{ steps.plan.output.matrix[0] }}", "value"),
("3", "{{ steps.plan.output.matrix[0][1] }}", "value"),
("4", "{{ inputs.a == 'x' and inputs.b }}", "cond"),
("4", "{{ inputs.a=='x'and inputs.b }}", "cond"),
("4", "{{ 'x'in inputs.a }}", "cond"),
]
for num, expr, kind in cases:
fn = evaluate_expression if kind == "value" else evaluate_condition
print(f"[{num}] {expr} -> {fn(expr, ctx)!r}")
print("input coercion of '1e3' (type: number):",
WorkflowEngine._coerce_input("x", "1e3", {"type": "number"}))Expected Behavior
The module docstring describes the evaluator as "a safe Jinja2 subset" (src/specify_cli/workflows/expressions.py:3), and _evaluate_simple_expression lists "String and numeric literals" as supported (expressions.py:791). For each case, the expression should either evaluate as it would in Jinja2 or raise a ValueError naming the unsupported syntax. Malformed filters already get such a ValueError (docs/reference/workflows.md, "Types are validated, not coerced"). Specifically:
{{ 1e3 }}should be1000.0, and{{ inputs.n < 1e3 }}should be true forn = 5.{{ inputs.s > 100 }}withs = "1e3"should be true, in line with the_safe_comparedocstring (expressions.py:1003-1012) and the input-coercion testtests/test_workflows.py:7092(("1e3", 1000)).{{ 'it\'s' }}should be"it's", and the comparison should be true.{{ steps.plan.output.matrix[0][1] }}should be'b'. The_resolve_dot_pathdocstring promises list indexing (expressions.py:312).{{ inputs.a=='x'and inputs.b }}should be true, like the spaced form.{{ 'x'in inputs.a }}should be true.
Actual Behavior
[1] {{ 1e3 }} -> None
[1] {{ inputs.n < 1e3 }} -> False
[1] {{ inputs.s > 100 }} -> False
[2] {{ 'it\'s' }} -> None
[2] {{ inputs.name == 'it\'s' }} -> False
[3] {{ steps.plan.output.matrix[0] }} -> ['a', 'b']
[3] {{ steps.plan.output.matrix[0][1] }} -> None
[4] {{ inputs.a == 'x' and inputs.b }} -> True
[4] {{ inputs.a=='x'and inputs.b }} -> False
[4] {{ 'x'in inputs.a }} -> False
input coercion of '1e3' (type: number): 1000
Specify CLI Version
1.2.4.dev0 (main at 27d14e2)
AI Agent
Not applicable
Operating System
Linux 6.18 (Ubuntu container)
Python Version
Python 3.12.3
Additional Context
When cases 1, 3 and 4 are used as an if condition, validate_workflow returns no errors. Case 2 is flagged in a condition, but as unbalanced delimiters and quotes rather than as an escaped quote.
Related: #5060 proposes replacing the hand-written expression parser with a grammar so that invalid input raises. It does not mention this case.
Also reproduced on macOS (Darwin 25.6, arm64) with Python 3.14.4 and uv run python -I repro.py against 27d14e2; the output matches the above.
AI Disclosure
Found and reproduced with Claude Code (model: Claude Opus 4.8), run autonomously under human supervision. The human reporter re-ran every reproduction above before filing.
Activity