VS_CODE_INTEGRATION_PHASE_5_PLAN

Phase 5: Conditional Branch Filtering + Connection Source Completions

Branch: wf_tool_state
Working dir: /Users/jxc755/projects/worktrees/galaxy-workflows-vscode/branch/wf_tool_state
Upstream: vs_code_branch worktree at /Users/jxc755/projects/worktrees/galaxy-tool-util/branch/vs_code_branch


Overview

Phase 5 has two independent functional deliverables:

But before implementing either, two upstream PRs are required to prevent building logic in the extension that should live upstream:

Pre-A and Pre-B share a common bridge needed in the extension: converting a YAML ObjectASTNode to a plain Record<string, unknown>. That bridge is where the extension’s AST-specific work is concentrated; everything else moves upstream.


Prior work completed

Upstream commit 6fb2ffe on vs_code_branch:

Extension commit c30f68d on wf_tool_state:

Upstream commits aa8ebc5 + aeaab8e on vs_code_branch: ✅ DONE


Pre-A ✅ DONE — Upstream findParamAtPath utility

What was added to @galaxy-tool-util/schema

export interface ParamNavigationResult {
  /** The param at the final path segment (undefined if path doesn't resolve). */
  param: ToolParameterModel | undefined;
  /** All params available at the final level — used for name completions. */
  availableParams: ToolParameterModel[];
}

export function findParamAtPath(
  params: ToolParameterModel[],
  path: (string | number)[],
  state?: Record<string, unknown>
): ParamNavigationResult

Key behavioral detail: when state === undefined, conditional branches are ALL merged (discriminator not set → show all params). When state !== undefined (even {}), selectWhichWhen is used to pick the active branch.

Extension shim after Pre-A

The extension needs a bridge function (add to toolStateTypes.ts alongside getStringPropertyFromStep):

/** Convert a YAML ObjectASTNode to a nested plain dict for upstream param navigation. */
export function yamlObjectNodeToRecord(node: ObjectASTNode): Record<string, unknown> {
  const dict: Record<string, unknown> = {};
  for (const prop of node.properties) {
    const key = String(prop.keyNode.value);
    const val = prop.valueNode;
    if (!val) continue;
    if (val.type === "string")  dict[key] = String(val.value);
    if (val.type === "boolean") dict[key] = Boolean(val.value);  // preserve native bool for selectWhichWhen
    if (val.type === "number")  dict[key] = Number(val.value);
    if (val.type === "object")  dict[key] = yamlObjectNodeToRecord(val as ObjectASTNode);
    // arrays (repeats) left empty — selectWhichWhen only needs scalar values
  }
  return dict;
}

Then:

The LSP-specific logic (name-vs-value mode detection, afterColon check, completion item builders, hover markdown) stays in the extension; only the tree-walking moves upstream.


Pre-B ✅ DONE — Upstream ToolStateValidator strict mode

What was added to @galaxy-tool-util/schema

validateFormat2StepStateStrict in stateful-validate.ts (uses onExcessProperty: "error"), wrapped by ToolStateValidator.validateFormat2StepStrict. ToolStateDiagnostic consolidated as the canonical type in stateful-validate.ts.

class ToolStateValidator {
  // existing: ignores unknown keys (used for conversion validation)
  async validateFormat2Step(toolId, toolVersion, state): Promise<ToolStateDiagnostic[]>

  // NEW: reports unknown keys as errors (for LSP diagnostics)
  async validateFormat2StepStrict(
    toolId: string,
    toolVersion: string | null,
    format2State: Record<string, unknown>
  ): Promise<ToolStateDiagnostic[]>
}

ToolStateDiagnostic has path: string (dot-separated), message: string, severity: "error" | "warning".

Extension shim after Pre-B

ToolStateValidationService becomes:

export class ToolStateValidationService {
  private readonly validator: ToolStateValidator;

  constructor(toolRegistryService: ToolRegistryService) {
    // ToolRegistryServiceImpl wraps ToolInfoService; expose it via an accessor
    this.validator = new ToolStateValidator(toolRegistryService.getToolInfo());
  }

  async doValidation(documentContext: GxFormat2WorkflowDocument): Promise<Diagnostic[]> {
    const result: Diagnostic[] = [];
    const nodeManager = documentContext.nodeManager;

    for (const stepNode of nodeManager.getStepNodes()) {
      const toolId = /* extract as before */;
      const toolVersion = /* extract as before */;
      const stateProperty = /* find state or tool_state property */;
      if (!stateProperty?.valueNode || stateProperty.valueNode.type !== "object") continue;

      if (!this.toolRegistryService.hasCached(toolId, toolVersion)) {
        // Information diagnostic stays in extension (cache UX, not validation)
        result.push(/* existing info diagnostic */);
        continue;
      }

      const stateDict = yamlObjectNodeToRecord(stateProperty.valueNode as ObjectASTNode);
      const diags = await this.validator.validateFormat2StepStrict(toolId, toolVersion, stateDict);

      for (const diag of diags) {
        const range = dotPathToYamlRange(stateProperty.valueNode as ObjectASTNode, diag.path, nodeManager);
        result.push({
          message: diag.message,
          severity: diag.severity === "error" ? DiagnosticSeverity.Error : DiagnosticSeverity.Warning,
          range,
        });
      }
    }
    return result;
  }
}

dotPathToYamlRange is extension-specific (~20 lines): split the dot-path on ".", navigate the YAML ObjectASTNode property by property, return the range of the final key node (or value node for value errors).

Note: ToolRegistryServiceImpl currently doesn’t expose the underlying ToolInfoService. This accessor needs to be added to the ToolRegistryService interface or handled by constructing ToolStateValidator inside ToolRegistryServiceImpl.


Part A: Conditional Branch Filtering (after Pre-A)

What remains after Pre-A

With findParamAtPath upstream (including selectWhichWhen integration), the extension changes are:

Step A1 ✅ DONE — Type updated

Step A2 — Add yamlObjectNodeToRecord bridge (shared with Pre-B)

Already described in Pre-A. Lives in toolStateTypes.ts.

Step A3 — Replace navigateParams in completion

toolStateCompletionService.ts:

The completion context (name vs. value mode) is derived from ParamNavigationResult:

Step A4 — Replace local findParamAtPath in hover

hoverService.ts:

Step A4 validation — Replace validateStateNode in validation

toolStateValidationService.ts:

Step A5 — Tests

Same as before — split the existing conditional test and add branch-filtered cases:

  1. toolStateCompletion.test.ts — split existing test into 3 (fast, sensitive, no value set)
  2. toolStateValidation.test.ts — 2 new tests (stale branch param warns, active branch is clean)
  3. toolStateHover.test.ts — 1 new test (hover in correct branch)

Test count: +6


Part B: Connection Source Completions

(Unchanged from previous revision — no upstream prerequisites needed for this part.)

Format2 source: conventions

Detection

export interface SourceInPath { stepName: string; }

export function findSourceInPath(path: NodePath): SourceInPath | undefined {
  const n = path.length;
  // Explicit: ["steps", stepName, "in", index, "source"]
  if (n >= 5 && path[n-1] === "source" && typeof path[n-2] === "number"
      && path[n-3] === "in" && path[n-5] === "steps")
    return { stepName: String(path[n-4]) };
  // Map shorthand: ["steps", stepName, "in", inputName] — value IS the source
  if (n >= 4 && typeof path[n-1] === "string" && path[n-1] !== "in"
      && path[n-2] === "in" && path[n-4] === "steps")
    return { stepName: String(path[n-3]) };
  return undefined;
}

Map shorthand path shape needs integration test confirmation before finalizing.

Steps B1–B3

Same as previous revision:


File Impact Summary

Upstream (@galaxy-tool-util/schema, vs_code_branch)

FileChange
src/workflow/param-navigation.tsNEW ✅ — findParamAtPath, ParamNavigationResult
src/workflow/stateful-validate.tsMODIFIED ✅ — validateFormat2StepStateStrict, ToolStateDiagnostic (canonical def)
src/tool-state-validator.tsMODIFIED ✅ — validateFormat2StepStrict, re-exports ToolStateDiagnostic
src/workflow/index.tsMODIFIED ✅ — exports above
src/index.tsMODIFIED ✅ — exports above

Extension (wf_tool_state)

FileChange
src/services/toolStateTypes.tsMODIFIED ✅ — Add ToolParamBase, getObjectNodeFromStep, yamlObjectNodeToRecord; dotPathToYamlRange deferred to A4-val
src/services/toolStateCompletionService.tsMODIFIED ✅ — Delete navigateParams; call upstream findParamAtPath with YAML state dict
src/services/toolStateValidationService.tsDelete recursive validateStateNode; call validateFormat2StepStrict, map paths to ranges
src/services/hoverService.tsMODIFIED ✅ — Delete local findParamAtPath; call upstream with YAML state dict
src/services/workflowConnectionService.tsNEW ✅ — getAvailableSources, findSourceInPath
src/services/completionService.tsMODIFIED ✅ — Add findSourceInPath check
tests/integration/toolStateCompletion.test.tsMODIFIED ✅ — Split conditional test into 3 (no discriminator / fast / sensitive); +2 new tests
tests/integration/toolStateValidation.test.ts+2 tests
tests/integration/toolStateHover.test.tsMODIFIED ✅ — +2 conditional hover tests (active / inactive branch)
tests/integration/workflowSourceCompletion.test.tsNEW ✅ — 7 tests

Implementation Order

  1. A1: ConditionalParam type ✅ DONE
  2. Pre-A upstream: Add findParamAtPath to @galaxy-tool-util/schema; rebuild; tests ✅ DONE (aa8ebc5, refined in 0b073a7)
  3. Pre-B upstream: Add validateFormat2StepStrict to ToolStateValidator; rebuild ✅ DONE (aa8ebc5, aeaab8e)
  4. Extension bridge: Add yamlObjectNodeToRecord + getObjectNodeFromStep to toolStateTypes.ts ✅ DONE (483bb12)
  5. A3: Replace navigateParams with upstream findParamAtPath call ✅ DONE (483bb12)
  6. A4: Replace hover’s local findParamAtPath with upstream call ✅ DONE (483bb12)
  7. A4 validation: Replace validateStateNode recursion with validateFormat2StepStrict shim + dotPathToYamlRange ✅ DONE (5ea7143)
  8. A5: +2 validation tests (stale branch param, active branch clean) ✅ DONE (5ea7143)
  9. B1: workflowConnectionService.ts ✅ DONE
  10. B2: Wire into completionService.ts ✅ DONE
  11. B3: Tests (red-to-green) ✅ DONE

Unresolved Questions

  1. ToolStateValidator severity mapping: RESOLVED: detect “is unexpected” in extension, remap to Warning + custom message; merge per-union-member value errors into one “Invalid value” Error.
  2. ToolRegistryServiceImpl wiring: RESOLVED: call validateFormat2StepStateStrict(rawParams, stateDict) directly — rawParams already available from getToolParameters.
  3. dotPathToYamlRange edge cases: RESOLVED: works for flat, section, conditional; repeat paths not tested (not exercised by current tests).
  4. Map shorthand in: path shape: RESOLVED: integration test confirms ["steps", "step", "in", "key"] — completions work for shorthand form.
  5. Validation: warn on stale branch params? RESOLVED: stale branch params flagged as Warning via “is unexpected” detection.
  6. out: on subworkflow steps: Skip for now — only handle plain out: array form.