Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 80 additions & 0 deletions docs/features/export-viz-output.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
Feature: Export Output Flag

Add `--output` / `-o` flag to `flowr export` that writes export output to a file
instead of stdout, with automatic `.js` wrapping for `file://` compatibility
when combined with `--format json`. Wires `task regenerate-flowviz` to use
the native export command, eliminating the need for a separate generation script.

Status: ELICITING

Rules (Business):
- `--output` / `-o` flag writes export output to a file for all formats, creating parent directories as needed
- When `--format json` and `--output` path ends in `.js`, the content is wrapped as `window.FLOWVIZ_DATA = <json>;` for `file://` compatibility
- The `generate-flowviz-data.py` script is deleted; `app.js` handles the new JSON shape via its `transformFlow` function
- `regenerate-flowviz` task added to pyproject.toml

Constraints:
- CLI exit codes: 0 = success, 1 = command failed, 2 = usage error (ADR_20260426_cli_io_convention)
- Zero new runtime dependencies introduced
- No changes to existing domain types (Flow, State, Transition, GuardCondition)
- `.js` wrapping applies ONLY to JSON format — mermaid output with `.js` extension is written as-is without wrapping

## Questions

| ID | Question | Status | Answer / Assumption |
|----|----------|--------|---------------------|
| Q1 | Should `.js` wrapping apply to all formats or only JSON? | Resolved | Only JSON — wrapping non-JSON (e.g. mermaid) as `window.FLOWVIZ_DATA = ...;` would produce invalid JavaScript |
| Q2 | Should `--output` to an unwritable path produce exit code 1? | Resolved | Yes — OSError during write is caught and reported as exit code 1 |
| Q3 | Should `--output` overwrite existing files? | Resolved | Yes — consistent with shell `>` redirection behavior |

## Changes

| Session | Q-IDs | Change |
|---------|-------|--------|
| 2026-05-07 planning | — | Created: split from export-viz-pipeline (INVEST: must_examples <= 8) |

Rule: Output-to-file via --output flag
As a CLI user
I want to write export output to a file via `--output` / `-o`
So that scripted workflows (like `task regenerate-flowviz`) can produce output files directly without shell redirection

@id:a7b9c1d3
Example: --output writes to file instead of stdout
Given a flow definition file exists at `examples/simple.yaml`
When the user runs `flowr export --format json --output /tmp/flowr-out.json examples/simple.yaml`
Then the file `/tmp/flowr-out.json` contains valid JSON output and nothing is printed to stdout

@id:b8c0d2e4
Example: --output creates parent directories automatically
Given a flow definition file exists at `examples/simple.yaml`
When the user runs `flowr export --format json --output /tmp/flowr/deep/nested/out.json examples/simple.yaml`
Then the parent directories `/tmp/flowr/deep/nested/` are created and the file is written successfully

@id:c9d1e3f5
Example: --output works for all export formats
Given a flow definition file exists at `examples/simple.yaml`
When the user runs `flowr export --format mermaid --output /tmp/flowr-out.mmd examples/simple.yaml`
Then the file `/tmp/flowr-out.mmd` contains valid Mermaid stateDiagram-v2 output

Rule: JavaScript wrapping for file:// compatibility
As a tool author
I want the `--output` flag to auto-wrap JSON output as a JavaScript variable assignment when the output file has a `.js` extension
So that the D3 visualizer can load flow data from local files via the `file://` protocol without CORS restrictions

@id:d0e2f4a6
Example: .js extension wraps JSON as window.FLOWVIZ_DATA assignment
Given a flow definition file exists at `examples/simple.yaml`
When the user runs `flowr export --format json --output .flowr/viz/data.js examples/simple.yaml`
Then the file content starts with `window.FLOWVIZ_DATA = ` followed by the JSON object and ending with `;\n`

@id:e1f3a5b7
Example: .js wrapping does not apply to non-JSON formats
Given a flow definition file exists at `examples/simple.yaml`
When the user runs `flowr export --format mermaid --output /tmp/out.js examples/simple.yaml`
Then the file contains plain Mermaid output without `window.FLOWVIZ_DATA` wrapping

@id:f2a4b6c8
Example: Non-.js extension writes JSON without wrapping
Given a flow definition file exists at `examples/simple.yaml`
When the user runs `flowr export --format json --output /tmp/out.json examples/simple.yaml`
Then the file contains raw JSON without any JavaScript wrapping
74 changes: 74 additions & 0 deletions docs/features/export-viz-pipeline.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
Feature: Export Viz Pipeline

Enrich JSON export with viz-required metadata (version, exits, subflow fields)
and restructure directory export as a named collection so that downstream tools
(like the D3 visualizer) can render complete flow information without additional
lookups or duplicated parsing.

Status: ELICITING

Rules (Business):
- JSON single-flow export must include `version` and `exits` from the Flow domain object, and subflow-state nodes must include `subflow` and `subflowVersion` fields
- JSON directory export must change from `[{...}]` array to `{"defaultFlow": "...", "flows": [...]}` object so downstream tools can identify the entry-point flow

Constraints:
- This feature MODIFIES existing export-json behavior — existing tests (99a274dd, unit/export_test.py directory tests) must be updated to match the new output shape
- Zero new runtime dependencies introduced
- No changes to existing domain types (Flow, State, Transition, GuardCondition)

## Questions

| ID | Question | Status | Answer / Assumption |
|----|----------|--------|---------------------|

## Changes

| Session | Q-IDs | Change |
|---------|-------|--------|
| 2026-05-07 planning | — | Created: feature breakdown from stakeholder specification |

Rule: JSON single-flow enrichment
As a tool author
I want the JSON single-flow export to include version, exits, and subflow metadata
So that downstream tools (like the D3 visualizer) can render complete flow information without additional lookups or duplicated parsing

@id:a1c3e5f7
Example: Single-flow export includes version and exits fields
Given a flow definition with version "1.0.20260507" and exits ["done", "failed"]
When the user runs `flowr export --format json examples/simple.yaml`
Then the JSON output contains a `version` field matching "1.0.20260507" and an `exits` field matching ["done", "failed"]

@id:b2d4f6a8
Example: Subflow-state nodes include subflow and subflowVersion
Given a flow where state "drill-down" has `flow: child-flow` and `flow_version: 2.0.0`
When the user runs `flowr export --format json main.yaml`
Then the node for "drill-down" includes `"subflow": "child-flow"` and `"subflowVersion": "2.0.0"`

@id:c3e5f7a9
Example: Non-subflow state nodes omit subflow fields
Given a flow with states that have no `flow` field
When the user runs `flowr export --format json examples/simple.yaml`
Then no node in the output contains a `subflow` or `subflowVersion` field

Rule: JSON directory export restructured as named collection
As a tool author
I want the JSON directory export to produce a structured object with a `defaultFlow` key and a `flows` array
So that downstream tools can identify the entry-point flow without hardcoding assumptions

@id:d4f6a8b0
Example: Directory export produces object with defaultFlow and flows array
Given a directory `flows/` contains `alpha.yaml` and `beta.yaml`
When the user runs `flowr export --format json flows/`
Then the output is a JSON object (not array) with a `defaultFlow` key and a `flows` key containing an array of flow entries sorted alphabetically by filename

@id:e5f7a9b1
Example: defaultFlow selects main-flow when present
Given a directory contains `main-flow.yaml` and `other.yaml`
When the user runs `flowr export --format json flows/`
Then the `defaultFlow` value is `"main-flow"`

@id:f6a8b0c2
Example: defaultFlow falls back to alphabetically first flow name
Given a directory contains `beta.yaml` and `gamma.yaml` but no `main-flow.yaml`
When the user runs `flowr export --format json flows/`
Then the `defaultFlow` value is `"beta"`
17 changes: 16 additions & 1 deletion flowr/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,12 @@ def _add_subcommands(parser: argparse.ArgumentParser) -> None:
dest="export_format",
help="Export format",
)
p_export.add_argument(
"--output",
"-o",
dest="output_path",
help="Write output to file instead of stdout",
)
from flowr.exporters.registry import EXPORTERS as EXPORTERS_FOR_ARGS

for _name, adapter in EXPORTERS_FOR_ARGS.items():
Expand Down Expand Up @@ -542,7 +548,16 @@ def _cmd_export(args: argparse.Namespace) -> int:
flow = load_flow_from_file(input_path)
subflows = _load_subflows(flow, input_path.parent)
output = adapter.export(flow, options, subflows=subflows)
print(output) # noqa: T201
output_path = getattr(args, "output_path", None)
if output_path:
out = Path(output_path)
if out.suffix == ".js" and args.export_format == "json":
var_name = "window.FLOWVIZ_DATA"
output = f"{var_name} = {output};\n"
out.parent.mkdir(parents=True, exist_ok=True)
out.write_text(output, encoding="utf-8")
else:
print(output) # noqa: T201
return 0


Expand Down
16 changes: 14 additions & 2 deletions flowr/exporters/json_exporter.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,10 @@ def _flow_to_dict(
"id": s.id,
"type": "subflow" if s.flow else "state",
}
if s.flow:
node["subflow"] = s.flow
if s.flow_version:
node["subflowVersion"] = s.flow_version
if include_attrs and s.attrs:
node["attrs"] = s.attrs
nodes.append(node)
Expand All @@ -152,7 +156,13 @@ def _flow_to_dict(
if transition.conditions:
edge["conditions"] = dict(transition.conditions.conditions)
edges.append(edge)
result = {"flow": flow.flow, "nodes": nodes, "edges": edges}
result = {
"flow": flow.flow,
"version": flow.version,
"exits": flow.exits,
"nodes": nodes,
"edges": edges,
}
return result

def export(
Expand All @@ -172,4 +182,6 @@ def export_directory(self, flows: list[tuple[str, Flow]], options: dict) -> str:
for _name, flow in flows:
entry = self._flow_to_dict(flow, options)
entries.append(entry)
return json.dumps(entries)
flow_names = {e["flow"] for e in entries}
default_flow = "main-flow" if "main-flow" in flow_names else min(flow_names)
return json.dumps({"defaultFlow": default_flow, "flows": entries})
10 changes: 6 additions & 4 deletions tests/features/export/export_json_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,9 @@ def test_export_json_99a274dd(

captured = capsys.readouterr()
data = json.loads(captured.out)
assert isinstance(data, list)
assert len(data) == 2
assert data[0]["flow"] == "alpha"
assert data[1]["flow"] == "beta"
assert isinstance(data, dict)
assert data["defaultFlow"] == "alpha"
flows = data["flows"]
assert len(flows) == 2
assert flows[0]["flow"] == "alpha"
assert flows[1]["flow"] == "beta"
Loading
Loading