cross-project-linting

Cross-Project Workflow Linting: Testing Alignment Plan

Workflow linting exists in three places that evolved independently and need to be reconciled — not at the implementation level, but at the level of which rules exist and how they are tested. This document is the living plan + the rule inventory.

Projects in Scope

ProjectPath (worktree)Lint entry points
gxformat2 (Python)worktrees/gxformat2/branch/abstraction_applicationsgxformat2/lint.py, gxformat2/linting.py
galaxy-tool-util (TS)worktrees/galaxy-tool-util/branch/gxwf-web (this repo)packages/schema/src/workflow/lint.ts
galaxy wf_tool_state (Python)worktrees/galaxy/branch/wf_tool_statelib/galaxy/tool_util/workflow_state/lint_stateful.py (composes gxformat2 + stateful checks)
galaxy-workflows-vscodeworktrees/galaxy-workflows-vscode/branch/wf_tool_stateserver/packages/server-common/src/providers/validation/ + per-format validation/rules/

Note: the Galaxy Python wf_tool_state backend is not a separate linter — it wraps gxformat2’s lint and adds stateful tool-state validation. It is listed here because its CLI reports surface lint results and because it’s the source of truth for the lint_stateful fixtures under packages/schema/test/fixtures/workflow-state/.

Premise Check (Why Linting ≠ Validation ≠ Shared Impl)

Plan Steps

Step 1 — Rule inventory table (this document)

Enumerate every lint operation anywhere in the three projects. For each: description, severity, implementation status per project, VS Code profile membership, and pointers to positive/negative declarative fixtures. Deliverable is the tables in the next section — see Rule Inventory below. Step 1 will not be fully filled in on first pass; the TODO cells are tracking debt that Step 3 iterates on.

Step 2 — Share declarative fixtures

Pick one of two approaches (open question):

Either way, VS Code needs a new test file that loads expectation YAMLs and asserts that (a) baseline validation + (b) each profile’s rules produce the expected diagnostics. The assertion shape differs from the TS side because diagnostics include ranges, so the YAML format may need a diagnostics: [{message_contains, range_hint}] extension.

Step 3 — Fill in the gaps

Document the architecture (this file) and iterate: for each rule in the table where any of the “Implemented” columns is no or any fixture column is , open a targeted task. Most are either “add a missing rule to the weaker project” or “add an isolated fixture for a rule that currently only has a composite fixture”.


Rule Inventory

Legend for “Implemented” columns:

VS Code column format: basic|iwc per format (f2 = format2, ga = native) — e.g. iwc(f2+ga) means IWC profile, both formats.

A. Structural Lint (core correctness)

“Schema?” column: YES = the rule is fully redundant with JSON-schema/Effect-decode validation (candidate for deletion as a separate lint check); partial = schema enforces presence/type but not the specific value or cross-reference; NO = schema cannot express this check.

#RuleSeveritySchema?gxformat2gtutil-tsgalaxy-pyVS CodePositive fixtureNegative fixture
A1missing class (format2)errorYES (Schema.Literal("GalaxyWorkflow"))lint_format2lintFormat2indbaseline schema (f2)synthetic-lint-no-class.gxwf.ymlsynthetic-basic.gxwf.yml
A2missing steps (format2)errorYES (steps is required, not optional)lint_format2lintFormat2indbaseline schema (f2)synthetic-missing-steps.gxwf.ymlsynthetic-basic.gxwf.yml
A3missing a_galaxy_workflow (native)errorYES (required Schema.String)lint_galintNativeindbaseline schema (ga)synthetic-lint-bad-marker.gareal-unicycler-assembly.ga
A4a_galaxy_workflow value != “true”errorYES (Literal['true'] in pydantic + Effect schemas — see note below)lint_galintNativeindbaseline schema (ga)synthetic-missing-marker.ga (TODO verify)
A5missing format-version (native)errorYES (required, no default)lint_galintNativeindbaseline schema (ga)TODOreal-unicycler-assembly.ga
A6format-version != “0.1”errorYES (Literal['0.1'] in pydantic + Effect schemas)lint_galintNativeindbaseline schema (ga)synthetic-lint-bad-format-version.gareal-unicycler-assembly.ga
A7step key not integer (native)errorpartial (depends on whether keys are typed Dict[int,...])lint_galintNativeindsynthetic-lint-non-integer-step.gareal-unicycler-assembly.ga
A8subworkflow missing/empty stepserrorpartial (presence enforced; emptiness not)lint_format2, lint_galint{Format2,Native}indsynthetic-lint-nested-no-steps.gxwf.yml, synthetic-lint-nested-no-steps.gasynthetic-nested-subworkflow.gxwf.yml
A9step has export error (step.errors)warningNO (semantic — schema permits the field)_lint_step_errorslintStepErrorsindStepExportErrorValidationRule — `basiciwc(f2+ga)`synthetic-lint-step-errors.gxwf.yml, real-hacked-unicycler-assembly-no-tool.gasynthetic-basic.gxwf.yml
A10step uses testtoolshed toolwarningNO (substring policy)_lint_tool_if_presentlintToolIfPresentindTestToolshedValidationRule — `basiciwc(f2+ga)` (error in VS Code)synthetic-lint-testtoolshed.gxwf.yml, real-hacked-unicycler-assembly-testtoolshed.gasynthetic-basic.gxwf.yml
A11no outputs (native)warningNO (semantic — empty is schema-valid)lint_galintNativeindreal-shed-tools-raw.ga, synthetic-minimal-tool.gareal-unicycler-assembly.ga
A12output without label (native)warningNO (label is optional in schema)lint_galintNativeindWorkflowOutputLabelValidationRuleiwc(ga) (error)synthetic-lint-output-no-label.gareal-unicycler-assembly.ga
A13outputSource references nonexistent steperrorNO (cross-reference, not expressible in JSONSchema)_validate_output_sources (f2)validateOutputSourcesindsynthetic-lint-bad-output-source.gxwf.ymlsynthetic-basic.gxwf.yml
A14input default type mismatcherrorNO (default is Any, type is sibling field)_validate_input_types (f2)validateInputTypesindInputTypeValidationRuleiwc(f2)synthetic-lint-bad-int-default.gxwf.yml, synthetic-lint-bad-float-default.gxwf.yml, synthetic-lint-bad-string-default.gxwf.ymlsynthetic-float-input-default.gxwf.yml
A15report markdown not a stringerrorYES (markdown: Schema.String)_validate_reportvalidateReportindsynthetic-lint-report-bad-type.gxwf.yml, synthetic-lint-report-bad-type.gasynthetic-lint-report.gxwf.yml
A16report markdown galaxy-directive validationerrorNO (semantic content check)_validate_report— (not ported)indTODOsynthetic-lint-report.gxwf.yml
A17schema strict extra-field (pydantic)warning= schema (this is the strict pass)lint_pydantic_validation— (not ported)indbaseline (f2 + ga)synthetic-extra-field.gxwf.yml, synthetic-extra-field.gasynthetic-basic.gxwf.yml
A18schema lax type/structural errorserror= schema (this is the lax pass)lint_pydantic_validation— (baseline via Effect)indbaseline (f2 + ga)TODOsynthetic-basic.gxwf.yml

B. Best-Practices Lint (style / IWC)

Almost everything in this table is NO for “Schema?” — best-practice rules check for absence of optional fields, which schemas can’t express.

#RuleSeveritySchema?gxformat2gtutil-tsgalaxy-pyVS CodePositive fixtureNegative fixture
B1workflow doc / annotation missingwarningNOlint_best_practiceslintBestPracticesindRequiredPropertyValidationRule("doc" f2 / "annotation" ga)iwc(f2+ga)synthetic-bp-no-annotation.gxwf.yml, synthetic-minimal-tool.gasynthetic-tags.gxwf.yml (TODO verify has doc)
B2workflow creator missingwarningNOlint_best_practiceslintBestPracticesindRequiredPropertyValidationRule("creator")iwc(f2+ga)synthetic-basic.gxwf.yml, synthetic-minimal-tool.gaTODO (no fixture currently has creator)
B3workflow license missingwarningNOlint_best_practiceslintBestPracticesindRequiredPropertyValidationRule("license")iwc(f2+ga)synthetic-basic.gxwf.yml, synthetic-minimal-tool.gaTODO
B4workflow release missingerrorNORequiredPropertyValidationRule("release")iwc(f2+ga)TODO (VS Code only, not yet in declarative fixtures)TODO
B5creator identifier not a URIwarningNO (semantic)lint_best_practiceslintBestPracticesindsynthetic-unlinted-best-practices-bad-identifier.gaTODO
B6step has no labelwarningNO_lint_step_best_practiceslintStepBestPracticesindsynthetic-bp-step-no-label.gxwf.ymlsynthetic-basic.gxwf.yml
B7step has no annotation/docwarningNO_lint_step_best_practiceslintStepBestPracticesindChildrenRequiredPropertyValidationRule("steps","doc")iwc(f2)synthetic-unlinted-best-practices.gxwf.yml (TODO verify)TODO
B8step input disconnectedwarningNO (cross-ref)_lint_step_best_practices, _lint_native_step_best_practiceslintStepBestPractices, lintNativeStepBestPracticesindsynthetic-bp-disconnected-input.gxwf.yml, synthetic-unlinted-best-practices.gasynthetic-basic.gxwf.yml
B9step tool_state has untyped param (${...})warningNO (string-content pattern)_lint_step_best_practiceslintStepBestPracticesindsynthetic-unlinted-best-practices.gaTODO
B10step PJA has untyped paramwarningNO_lint_native_step_best_practiceslintNativeStepBestPracticesindsynthetic-unlinted-best-practices.gasynthetic-pja-hide-rename.gxwf.yml
B11training-workflow missing tagwarningNO_lint_training— (not ported)indTODOTODO
B12training-workflow missing docwarningNO_lint_training— (not ported)indTODOTODO

C. Stateful Lint (tool-state against cached tool defs)

Only present in galaxy-python lint_stateful.py and in galaxy-workflows-vscode ToolStateValidationService. galaxy-tool-util is the primitive both call — validateFormat2StepStateStrict. gxformat2 does not do stateful validation.

#RuleSeveritygxformat2gtutil-tsgalaxy-pyVS CodePositive fixtureNegative fixture
C1unknown tool parameter (excess prop)warningvalidateFormat2StepState*lint_statefulbaseline (f2, via ToolStateValidationService)synthetic-cat1-stale.ga, synthetic-cat1-stale.gxwf.ymlsynthetic-cat1-clean.ga, synthetic-cat1.gxwf.yml
C2invalid value for tool parametererrorvalidateFormat2StepStateStrictlint_statefulbaseline (f2)TODOTODO
C3tool not in cacheinfolint_stateful (precheck)baseline (f2) — emits info diagnosticTODOTODO
C4stale keys (category policy)warninglint_stateful (StaleKeyPolicy)synthetic-cat1-stale.gasynthetic-cat1-clean.ga
C5step input connections consistencywarninglint_stateful --connectionsTODOTODO

D. Baseline Syntax / Schema (always runs in VS Code)

These are not pluggable rules in VS Code but appear here because they overlap with explicit lint checks in gxformat2.

#Checkgxformat2gtutil-tsgalaxy-pyVS Code
D1YAML syntax errorsvia ordered_loadvia yaml.parseind"YAML Syntax" diagnostic (f2)
D2JSON syntax errorsvia json.loadvia JSON.parseindvscode-json-languageservice (ga)
D3Format2 schema validationlint_pydantic_validation(format2=True)Effect GalaxyWorkflowSchema decodeind"Format2 Schema" via @galaxy-tool-util/schema
D4Native schema validationlint_pydantic_validation(format2=False)Effect NativeGalaxyWorkflowSchema decodeind"Native Workflow Schema" via vscode-json-languageservice

Pruning Candidates (Schema-Redundant Rules)

A rule that’s Schema? = YES is doing the same job as the baseline schema decode pass. The schema pass always runs (in gxformat2 via lint_pydantic_validation, in the TS port via Effect decode, in VS Code as the "Format2 Schema" / "Native Workflow Schema" baseline diagnostic, in galaxy-python via gxformat2). Keeping a hand-rolled lint rule for the same condition just means: (a) two error messages for the same input, (b) drift risk when the schema gets stricter, and (c) extra cells to maintain in this table.

Recommended drops:

RuleWhat replaces itNotes
A1 missing classFormat2 schema decodeSchema literal "GalaxyWorkflow" already errors.
A2 missing steps (format2)Format2 schema decodesteps is required.
A3 missing a_galaxy_workflow (native)Native schema decodeRequired field.
A5 missing format-version (native)Native schema decodeRequired field.
A15 report markdown not a stringFormat2 schema decodemarkdown: Schema.String.
A17 strict-extra-fieldStrict-mode schema decodeThis is literally the strict pass — fold into the harness, not into a separate “lint rule”.
A18 lax structural errorLax schema decodeSame.

A4 / A6 update (April 2026): Pydantic schemas in gxformat2 (schema/native_v0_1/workflow.yml) now use pydantic:type: "Literal['true']" and pydantic:type: "Literal['0.1']". Both gxformat2/schema/native.py and native_strict.py were regenerated. The single-quote form is deliberate: schema-salad-plus-pydantic’s codegen auto-injects a default= for any field whose type annotation matches the regex ^Literal\["(.+)"\]$ — i.e. double-quoted Literals only — and the auto-default would mask missing-field errors (A3/A5). Single-quoted Literal['true'] is the same Python type but doesn’t trip the regex, so no default is added and pydantic still rejects missing fields. The TS Effect-schema codegen needs the same treatment when this gets resynced (run make generate-schemas after make sync-schema-sources).

Recommended keep (still NOT schema-redundant):

Everything in Table B stays — best-practice rules check absence-of-optional, which schemas can’t express.

The cleanup pattern this implies:

  1. Tighten the schemas (A4, A6, A8) so more rules become YES rather than partial.
  2. Drop the redundant rules from lint.py and lint.ts once their schema replacement is in place.
  3. The rule inventory shrinks; the test fixtures still exercise the schema baseline (the operation key in the YAML just changes from lint_format2 to validate_format2).

This is a behavior change for users who run gxwf lint --skip-best-practices and expect structural-only errors. After pruning, those errors come from the schema decode pass instead — same content, possibly different message text. Worth flagging in a changeset.

Test Harness Shape per Project

ProjectRunnerFixture locationDriver
gxformat2pytestgxformat2/examples/ + gxformat2/examples/expectations/*.ymltests/test_declarative_normalized.py
galaxy-tool-util TSvitestpackages/schema/test/fixtures/{workflows,expectations}/ (synced from gxformat2)packages/schema/test/declarative-normalized.test.ts
galaxy wf_tool_statepytesttest/unit/tool_util/workflow_state/fixtures/ + expectations/test/unit/tool_util/workflow_state/test_*.py
galaxy-workflows-vscodemocha (per package)server/*/tests/integration/, imperativeno declarative harness yet — this is the gap Step 2 closes

Architecture for Shared Fixtures (Step 2 target)

                            ┌──────────────────────────────┐
                            │        gxformat2             │
                            │  examples/ + expectations/   │  (source of truth)
                            └──────────────┬───────────────┘
                                           │ make sync-workflow-{fixtures,expectations}

                    ┌────────────────────────────────────────┐
                    │   galaxy-tool-util/packages/schema     │
                    │   test/fixtures/{workflows,expectations}│  ← already wired
                    └──────────────┬─────────────────────────┘
                                   │   Step 2: publish as npm artifact
                                   │   (@galaxy-tool-util/workflow-fixtures)
                                   │   OR per-release tarball

        ┌──────────────────────────┴───────────────────────────┐
        ▼                                                       ▼
┌──────────────────┐                        ┌────────────────────────────┐
│ galaxy-workflows │                        │   galaxy wf_tool_state     │
│    -vscode       │                        │ (already has its own       │
│                  │                        │  synced fixtures for       │
│ new declarative  │                        │  stateful checks)          │
│  test harness    │                        └────────────────────────────┘
│  loads fixtures  │
│  + asserts       │
│  diagnostics     │
└──────────────────┘

The assertion YAML may need to grow a VS-Code-specific extension for range hints, since a lint result in VS Code is {message, range, severity} not a flat string. Proposed extension (backwards compatible — ignored by pytest/vitest):

test_lint_format2_bad_int_default:
  fixture: synthetic-lint-bad-int-default.gxwf.yml
  operation: lint_format2
  assertions:
    - path: [error_count]
      value: 1
    - path: [errors, 0]
      value_contains: "invalid type"
  # new:
  diagnostics:
    - severity: error
      source: "Input Type"
      message_contains: "invalid type"
      at_path: ["inputs", 0, "default"]   # JSONPath-ish — resolved to range by VS Code harness

Branches to Hack On

ProjectBranch
galaxy-tool-utilgxwf-client (this worktree) or a new cross-lint-testing branch
gxformat2abstraction_applications — add missing isolated fixtures (e.g. for B2/B3 negative cases, B4, B11, B12)
galaxy wf_tool_statewf_tool_state — minimal touches, mostly consumer
galaxy-workflows-vscodewf_tool_state (or fresh feature branch off main) — new declarative test harness + fixture consumption

Unresolved Questions