def discover(
root: dict[str, Any], processes: dict[str, dict[str, Any]]
) -> list[dict[str, Any]]:
"""Return one coverage entry per reachable tool invocation, with inheritance."""
records: list[dict[str, Any]] = []
def visit(
node: dict[str, Any],
path: str,
required: dict[str, Any] | None,
hinted: dict[str, Any] | None,
ancestors: frozenset[str],
) -> None:
identity = str(node.get("id", path))
if identity in ancestors:
raise PluginFailureError(f"Recursive workflow reference: {identity}")
own_req, own_hint = docker(node, "requirements"), docker(node, "hints")
required = own_req if own_req is not None else required
hinted = own_hint if own_hint is not None else hinted
if node.get("class") == "Workflow":
for step in node.get("steps", []):
run = step["run"]
child = (
run
if isinstance(run, dict)
else (processes.get(run) or processes.get(run.removeprefix("#")))
)
if child is None:
raise PluginFailureError(f"Unresolved workflow step: {run}")
step_req, step_hint = (
docker(step, "requirements"),
docker(step, "hints"),
)
visit(
child,
f"{path}/{step['id']}",
step_req if step_req is not None else required,
step_hint if step_hint is not None else hinted,
ancestors | {identity},
)
return
if node.get("class") == "ExpressionTool":
records.append(
{
"step": path,
"process": identity,
"status": "not-applicable",
"reason": "ExpressionTool runs in the CWL engine",
}
)
return
image, reason = image_reference(required if required is not None else hinted)
records.append(
{
"step": path,
"process": identity,
"image": image,
"status": "declared" if image else "uncovered",
"reason": reason,
"declaration": "requirement" if required is not None else "hint",
}
)
visit(root, str(root["id"]), None, None, frozenset())
return records