Skip to content

refactor(workflows): split the expression evaluator into a package - #5114

Merged
mnriem merged 3 commits into
github:mainfrom
markuswondrak:chore/workflow-expression-package
Oct 26, 2026
Merged

mnriem merged 3 commits into
github:mainfrom
markuswondrak:chore/workflow-expression-package

Conversation

@markuswondrak

Copy link
Copy Markdown
Contributor

Preparation for #5060, PR 1 of 3. Splits the 1581-line src/specify_cli/workflows/expressions.py into a layered package and makes engine.py the single place where an engine implementation is chosen. No behavior change.

Layout

Module Role
namespace.py dot-path traversal and list indexing
operators.py comparison operators and their coercions
filters.py the registered filter set
template.py template scanning and interpolation
engine_current.py today's evaluator, unchanged in behavior
engine.py the single engine selection point, re-exporting the engine contract
api.py evaluate_expression() / evaluate_condition()
diagnostics.py condition validation and remediation text
__init__.py logic-free facade keeping the historical public and test-only private names importable

namespace, operators, filters and template are leaves with no internal dependencies; engine_current builds on them; engine, api, diagnostics and the facade are the surfaces.

The boundary this establishes

The point of the split is that a later engine can be swapped at one place. tests/unit/test_expression_package_boundary.py pins that property in three assertions:

  • no module except engine.py and the facade imports an engine_* implementation,
  • engine.py really does import one (so the first test cannot pass vacuously),
  • the check itself detects a direct from .engine_current import ....

The two existing drift guards keep their assertions; only the monkeypatch target moves to the module that now owns each global (namespace.py, engine_current.py).

Also in here

workflows/ARCHITECTURE.md referenced src/specify_cli/workflows/expressions.py in two places — a file that no longer exists after this change. Both now point at the package, naming engine.py and api.py.

Verification

  • uv sync --extra test && .venv/bin/python -m pytest → 12757 passed, 435 skipped, 0 failed
  • ruff check src tests (the CI invocation) → clean
  • Every top-level definition of the previous expressions.py (46) exists in the package
  • Merged main up to date; main's fix(workflows): skip the right side of and/or when the left side decides #4923 short-circuit change is carried into engine_current.py (probes still visit both operands)

Not in this PR

PR 2 adds gap tests that pass on main (including the three hand-written-parser bugs reported as #5071, #5072 and #5073). PR 3 replaces the parser with the Lark LALR grammar.

AI Disclosure: PR body drafted with Pi Coding Agent (model: deepseek-v4.1-flash, reasoning effort high), under human supervision. The split itself was produced earlier with OpenCode (model: deepseek-v4.1-flash, supervised) and rebased, conflict-resolved and verified in this session with Pi Coding Agent (model: deepseek-v4.1-flash, supervised).

Markus added 3 commits October 13, 2026 07:29
Move src/specify_cli/workflows/expressions.py (1571 lines) into a layered
package so the evaluator, its shared leaves, the public API and the condition
diagnostics are separately testable and a future engine can be swapped at a
single point (engine.py). Behaviour is unchanged.

Layout:
- namespace/operators/filters/template: shared leaves, no internal deps
- engine_current: the hand-written evaluator, interpolation and probe helpers
- engine: the selection point, re-exporting the full engine contract
- api: evaluate_expression / evaluate_condition
- diagnostics: condition validation and remediation
- __init__: logic-free facade keeping the historical public surface and the
  test-only private names importable

The two drift-guard tests keep their assertions; only the monkeypatch target
moves to the module that owns the global (namespace / engine_current). A new
boundary test asserts that no module but engine.py and the facade imports an
engine implementation (engine_*).

Verified in the harness worktree against the S3 corpus: current backend
7/7 required, 78/78 compat expression, 11/11 compat condition.

Assisted-by: opencode (model: deepseek-v4.1-flash, supervised)
Brings the package split up to date with main (79 commits). One conflict:
main changed the monolith src/specify_cli/workflows/expressions.py, which this
branch replaced with a package.

Resolution: main's change to expressions.py (github#4923, short-circuit the right
operand of and/or, with probes still visiting both operands) is ported into
expressions/engine_current.py, and the monolith stays deleted. No other
main-side change touched the evaluator. Every top-level definition of main's
expressions.py exists in the package (46 of 46).

Verified: uv sync --extra test, .venv/bin/python -m pytest -> 12757 passed,
435 skipped, 0 failed, including the probe/short-circuit and package-boundary
tests.

Assisted-by: Pi Coding Agent (model: deepseek-v4.1-flash, supervised)
The evaluator is no longer a single module, so both references in
workflows/ARCHITECTURE.md named a file that no longer exists. Point the
feature intro and the module tree at the package, naming the selection
point (engine.py) and the public API module.

Assisted-by: Pi Coding Agent (model: deepseek-v4.1-flash, supervised)
@markuswondrak
markuswondrak requested a review from mnriem as a code owner October 25, 2026 08:41
Copilot AI max review requested due to automatic review settings October 25, 2026 08:41

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota.

@mnriem mnriem added the triage-nice-to-have Verdict: evidence-backed fix or greenlit feature — land after review label Oct 25, 2026
@mnriem
mnriem requested a max review from Copilot October 26, 2026 13:58

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Restructuring the shared runtime and validation evaluator warrants maintainer confirmation of the new engine boundary before the planned parser replacement.

No open issues 🎉

Review effort: Max

@mnriem
mnriem merged commit fdad05f into github:main Oct 26, 2026
16 checks passed
@mnriem

mnriem commented Oct 26, 2026

Copy link
Copy Markdown
Collaborator

Thank you!

kevinnguyenhoang91 pushed a commit to kevinnguyenhoang91/spec-kit that referenced this pull request Nov 10, 2026
…ithub#5114)

* refactor: split workflow expression evaluator into a package

Move src/specify_cli/workflows/expressions.py (1571 lines) into a layered
package so the evaluator, its shared leaves, the public API and the condition
diagnostics are separately testable and a future engine can be swapped at a
single point (engine.py). Behaviour is unchanged.

Layout:
- namespace/operators/filters/template: shared leaves, no internal deps
- engine_current: the hand-written evaluator, interpolation and probe helpers
- engine: the selection point, re-exporting the full engine contract
- api: evaluate_expression / evaluate_condition
- diagnostics: condition validation and remediation
- __init__: logic-free facade keeping the historical public surface and the
  test-only private names importable

The two drift-guard tests keep their assertions; only the monkeypatch target
moves to the module that owns the global (namespace / engine_current). A new
boundary test asserts that no module but engine.py and the facade imports an
engine implementation (engine_*).

Verified in the harness worktree against the S3 corpus: current backend
7/7 required, 78/78 compat expression, 11/11 compat condition.

Assisted-by: opencode (model: deepseek-v4.1-flash, supervised)

* docs(workflows): point the architecture doc at the expression package

The evaluator is no longer a single module, so both references in
workflows/ARCHITECTURE.md named a file that no longer exists. Point the
feature intro and the module tree at the package, naming the selection
point (engine.py) and the public API module.

Assisted-by: Pi Coding Agent (model: deepseek-v4.1-flash, supervised)

---------

Co-authored-by: Markus <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

triage-nice-to-have Verdict: evidence-backed fix or greenlit feature — land after review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants