Skip to content

feat(recipe): actionable recipe validation and review navigation gating - #342

Merged
JacksonFergusonDev merged 4 commits into
mainfrom
feat/actionable-recipe-validation
Sep 26, 2026
Merged

JacksonFergusonDev merged 4 commits into
mainfrom
feat/actionable-recipe-validation

Conversation

@JacksonFergusonDev

@JacksonFergusonDev JacksonFergusonDev commented Sep 26, 2026 •

Copy link
Copy Markdown
Owner

Summary & Motivation

Previously, entering invalid values in recipe fields (such as minimum Python version, container port, or GitHub username) produced generic or obscure errors like Invalid [tool.protostar] project recipe. Users received no guidance on whether an input was fundamentally malformed or simply out of accepted bounds. Furthermore, when fatal validation issues were shown in the TUI live preview, users could still press ^s (ctrl+s) or click Continue, navigating into an empty Review screen that failed to prepare.

This pull request:

  1. Centralizes and refines validation for critical recipe and metadata fields with domain-specific, actionable hints.
  2. Ensures identical error messages and hints are delivered consistently across the interactive TUI preview, headless terminal runs, and headless --json envelopes.
  3. Gates navigation in the recipe editor so users cannot advance to the review screen when fatal draft errors exist.

Planning & Key Decisions

  • Exact, never over-strict: Each validator rejects only values that are actually invalid, so no recipe or config that holds a valid value stops loading, and decode_recipe can safely check recorded state on sync. An earlier revision range-checked Python to 3.8–3.14. That rejected adopting a project with requires-python = ">=3.7" and would have broken every recipe recording 3.15, which ships next month. Minor versions are no longer range-checked.
  • Python version errors in two stages: A malformed value (foo, 3, 3.x) gets Write the Python version as major.minor, such as '3.13'., and a well-formed non-3 major (2.7, 4.0) gets Unsupported Python version, which explains that Protostar scaffolds Python 3 projects.
  • Field validators:
    • docker_port: exactly digits, or an integer, from 1 to 65535. +8000 and 80 are rejected because they would be copied as written into generated files.
    • github_username: rejects a leading @ (the hint gives the corrected name). Otherwise it follows GitHub's own rules: at most 39 characters, starting with a letter or digit. Legacy names that end in or repeat a hyphen, and Enterprise Managed User _shortcode names, stay valid.
    • package_name and project_name: the rules decode_recipe already enforced, now with specific messages and hints.
    • An empty metadata value means the field is unset and is never invalid. Clearing Minimum Python in the editor falls back to the configured or detected default.
    • Free-text fields (author_email, author_name, description) are deliberately left unvalidated.
  • One set of validators: The Python and name validators live in src/protostar/workspace.py and the metadata validators in src/protostar/metadata.py. MetadataField.validator lets decode_recipe validate every metadata field present. UserConfig checks python_version and github_username when the config loads.
  • TUI review navigation gating:
    • Each field change runs check_draft (init_draft.py) synchronously. It applies the same validators without rendering the template, so Continue is disabled at once without blocking the UI thread.
    • PlanPreview.PlanUpdated keeps Continue's disabled state in sync with the background planner, so any plan error also blocks it.
    • action_continue re-checks the draft so ^s can't open ReviewScreen while a field is invalid.

Architectural Invariants

  • Decisions Precede Screen Transitions: Reinforces the headless invariant that a screen opens only for a decision it can settle. An unresolvable draft must never transition to ReviewScreen, eliminating empty review states.
  • Headless Boundary Intact: Engine code strictly communicates via immutable request/result models and raises typed domain exceptions (ConfigurationError) containing user-facing hint fields.

… and metadata fields

- Centralize Python version check with float format and accepted range (3.8-3.14)
- Add validation for container port (1-65535) and GitHub username format
- Enforce valid Python identifier for package name and legal characters for project name
- Wire metadata validator registry into decode_recipe
- Render hints with error messages in TUI preview and propagate errors in headless mode
- Disable Continue button and guard ctrl+s when recipe draft has errors
- Check draft validity synchronously on field change to update button state
- Emit PlanUpdated message from PlanPreview to keep continue button in sync
- Verify with TUI tests that ctrl+s and continue are blocked until issues resolved
… value

Validation now rejects only values that are actually invalid:

- Python versions: any 3.x[.y] is accepted. The 3.8-3.14 range blocked
  adopting a project with requires-python >=3.7 and would have broken every
  recipe and config recording 3.15. A malformed version and a non-3 major
  get separate hints.
- GitHub usernames: legacy names ending in or repeating a hyphen, and
  Enterprise Managed User `_shortcode` names, are accepted.
- Container ports: exactly digits, so '+8000' and ' 80 ' no longer pass
  and render verbatim into generated files.
- Empty metadata is unset, not invalid, and clearing Minimum Python in the
  editor falls back to the default again.
- Project names: the rule main enforced; a whitespace-only name is not
  newly rejected.

The editor checks field values with the new check_draft, which renders
nothing, instead of running resolve_init on the UI thread. Nits:
validator typing, the dead key branch in validate_metadata, lazy imports
in config.py, and PlanPreview's unused error attribute.
The constraints test asserted right after pilot.click, before the
Checkbox.Changed handler had run, which failed on a slow Windows runner.
@JacksonFergusonDev
JacksonFergusonDev merged commit 1d9b325 into main Sep 26, 2026
28 checks passed
@JacksonFergusonDev
JacksonFergusonDev deleted the feat/actionable-recipe-validation branch September 26, 2026 01:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant