DEBRIEF_GA_EXPORT_UNSTRINGIFY_CLEAN_VALIDATE

Debrief: .ga export un-stringify + validate-gated clean

Date: 2026-06-30 Branch: wf_tool_state Trigger: IWC PR #1158 (mvdbeek: “Galaxy can just remove the extra JSON encoding on tool_state”)

What was asked

  1. Make .ga exports drop the extra JSON encoding on tool_state (emit a JSON object, not a double-encoded string). Safe transform.
  2. Clean workflows and, per step, only export the cleaned state if it validates. Scarier transform; validate is the safety net.

Decisions (confirmed with user)

What shipped

AreaChange
clean.pyclean_stale_state(..., validate=False); _revert_if_invalid() per-step gate; reverted_steps on CleanResult; threaded through clean_single/clean_tree/run_clean/options.
_report_models.pyCleanStepResult.reverted + revert_reason (golden clean_tree.json regenerated).
CLI--validate flag on gxwf-state-clean + -tree (dest validate_state to avoid shadowing Pydantic validate).
managers/workflows.pyworkflow_to_dict(..., clean_validate, tool_state_as_dict); _clean_native_dict(..., validate); new _normalize_native_tool_state() (ga style only; recurses subworkflows).
api/workflows.pyclean_validate + tool_state_as_dict query params on GET /download.
populators.pydownload_workflow() gained clean/clean_strip/clean_preserve/clean_validate/tool_state_as_dict.
Teststest_clean_validate_gate.py (5 unit), TestWfExportToolStateEncoding (4 API).

Bug found & fixed (the real find)

clean=true never stripped anything through the API. _workflow_to_dict_export keys steps by int order_index, but clean_stale_state looked up raw_steps.get(str(step_id), raw_steps.get(step_id, {})) — both branches resolve to the same string-ish value, never the int — so every step fell through to {} and was silently skipped. Disk-loaded .ga (string keys, used by every existing test + the whole declarative/IWC corpus) masked it; no test had ever exercised the in-memory export dict. Fixed with _raw_step_def() (tries str then int key). Regression test: test_clean_with_integer_step_keys.

This means the prior CURRENT_STATE claim “clean via GET /download?clean=true works” was untested and false for the native path.

Verification

Caveats / follow-ups