Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
57cedc8
Add Flow Visualization 2.0 supervision
longquanzheng Sep 18, 2026
cd34238
Hydrate blob-backed Supervision RPC outputs.
longquanzheng Sep 18, 2026
5093720
Split Dex Web into /v1 and /v2 workspaces.
longquanzheng Sep 19, 2026
53499d7
Overlay selected v2 runs onto steps with continue-as-new load-more.
longquanzheng Sep 19, 2026
5c0d7eb
Label every disjoint v2 group band so split regions keep their declar…
longquanzheng Sep 19, 2026
5eaa559
Show real Step execution data in the v2 detail panel.
longquanzheng Sep 19, 2026
eb835b0
Split the v2 Step panel into Definition and selectable Executions.
longquanzheng Sep 19, 2026
80658aa
Require Step explanations and keep Execution visible in the v2 panel.
longquanzheng Sep 19, 2026
df510f5
Make the v2 Step detail panel width resizable.
longquanzheng Sep 19, 2026
89b37b8
Hide source paths from the v2 Step Definition panel.
longquanzheng Sep 19, 2026
9f897a2
Make the Definition/Execution divider vertically resizable.
longquanzheng Sep 19, 2026
fbff3a6
Derive Step Decision from nextSteps and closeDecision.
longquanzheng Sep 19, 2026
8980d18
Merge origin/main into flow-viz-v2-supervision.
longquanzheng Sep 19, 2026
e78a477
Bump Java examples to dex-sdk 0.9.0 for LoadBlobs wire compatibility.
longquanzheng Sep 19, 2026
e276b07
Rename Dex Web Supervision surfaces to v2.
longquanzheng Sep 19, 2026
8bb2ebc
Split Execution into WaitFor and Execute with separate IO tabs.
longquanzheng Sep 19, 2026
d178177
Order Execution tabs Context, Input, Output and clarify empty Execute.
longquanzheng Sep 19, 2026
f26a72b
Keep Execution tab selection across history poll refreshes.
longquanzheng Sep 19, 2026
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
7 changes: 7 additions & 0 deletions .cursor/rules/project-core.mdc
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,13 @@ merged, do not mark the feature complete. Report the blocker explicitly.
Prefer capturing a small id/string or building a new tiny value over cloning.
Copy only when an algorithm must mutate a distinct shared value in place.

## FDG 2.0 Step Explanations

Every Step type in a Flow Definition Graph 2.0 Go source must declare exactly
one `// dex:explanation text:"..."` directive next to its `dex:group`. The text
is one sentence that states what the Step does. The analyzer stores it on the
Step node as `metadata.explanation` for Dex Web v2 Definition.

## Python Examples

Do not use `del` in Python examples merely to mark parameters or local values
Expand Down
7 changes: 7 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,13 @@ LazyLock<T>`. Do not use a function to construct or return one of these
definitions. Reuse the static directly, or clone its initialized value only
when an owned field is required.

### FDG 2.0 Step Explanations

Every Step type in a Flow Definition Graph 2.0 Go source must declare exactly
one `// dex:explanation text:"..."` directive next to its `dex:group`. The text
is one sentence that states what the Step does. The analyzer stores it on the
Step node as `metadata.explanation` for Dex Web v2 Definition.

### Python Examples

Do not use `del` in Python examples merely to mark parameters or local values
Expand Down
7 changes: 7 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,13 @@ LazyLock<T>`. Do not use a function to construct or return one of these
definitions. Reuse the static directly, or clone its initialized value only
when an owned field is required.

# FDG 2.0 Step Explanations

Every Step type in a Flow Definition Graph 2.0 Go source must declare exactly
one `// dex:explanation text:"..."` directive next to its `dex:group`. The text
is one sentence that states what the Step does. The analyzer stores it on the
Step node as `metadata.explanation` for Dex Web v2 Definition.

# Python Examples

Do not use `del` in Python examples merely to mark parameters or local values
Expand Down
20 changes: 17 additions & 3 deletions cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,7 @@ with **--out**:
```bash
dexcli visualize ./order_flow.py --json
dexcli visualize ./order_flow.go --json --out ./build/order-flow
dexcli visualize ./refund_flow.go --schema-version 2.0 --json --out ./build/refund
dexcli dev --flow-rendering-dir ./build
```

Expand Down Expand Up @@ -237,14 +238,27 @@ Dynamic targets produce an Unknown node and a blocking diagnostic. The default
renderer still shows the partial graph. With **--json**, a partial JSON artifact
is written, and the command exits with status 1.

Version 2 is Go-only and adds ordered Step groups plus a Dex Web v2 contract.
The contract declares Indexed Attributes, the fixed `GetDexSummary` and
`GetDexDisplay` RPCs, editable Display fields, and conditional Action RPCs.
Every Version 2 Step must declare exactly one `dex:group` and one
`dex:explanation text:"..."` directive. The explanation is one sentence that
states what the Step does; it is stored on the Step node as
`metadata.explanation`. Directives use unordered named `name:value` arguments.
Repeated directive lines retain source order; no `order` property is generated.
Every Version 2 Step, Attribute, RPC, input struct, and directive must be in the
same source file.

```text
dexcli visualize SOURCE [--language auto|go|python] [--open=true|false]
dexcli visualize SOURCE [--language auto|go|python] [--schema-version 1.0|2.0]
[--open=true|false]
[--json [--out PATH_PREFIX|-]]
[--python PYTHON_PATH]
```

Invalid command usage exits with status 2. The JSON contract is documented by
[`schema/flow-definition-graph.v1.schema.json`](schema/flow-definition-graph.v1.schema.json).
Invalid command usage exits with status 2. The JSON contracts are documented by
[`schema/flow-definition-graph.v1.schema.json`](schema/flow-definition-graph.v1.schema.json)
and [`schema/flow-definition-graph.v2.schema.json`](schema/flow-definition-graph.v2.schema.json).

The friendly Flow commands are:

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
module github.com/superdurable/dex/visualization-v2-invalid

go 1.26.0

require github.com/superdurable/dex/sdk-go v0.0.0

replace github.com/superdurable/dex/sdk-go => ../../../../../sdk-go
131 changes: 131 additions & 0 deletions cli/internal/command/testfixtures/visualization-v2-invalid/workflow.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
// Copyright (c) 2026 Super Durable, Inc.
//
// Licensed under the Sustainable Use License 1.0.
// You may not use this file except in compliance with the License.
// See the LICENSE file in the repository root.
//
// SPDX-License-Identifier: LicenseRef-Sustainable-Use-1.0

package visualizationv2invalid

import "github.com/superdurable/dex/sdk-go/dex"

// dex:indexed-attribute attribute-key:state attribute-key:state index-key:state index-type:keyword value-type:string description:"State"
var state = dex.DefineAttribute[string](
"state",
dex.Indexed(dex.AttributeIndex{Type: dex.IndexKeyword}),
)

// dex:indexed-attribute attribute-key:declared-indexed index-key:actual-indexed index-type:keyword value-type:string description:"Mismatched index"
var mismatchedIndex = dex.DefineAttribute[string](
"actual-indexed",
dex.Indexed(dex.AttributeIndex{Type: dex.IndexKeyword}),
)

var flag = dex.DefineAttribute[bool]("flag")

type BrokenActionInput struct {
Reason string `json:"reason"`
}

type InvalidV2Flow struct {
dex.FlowDefaults
}

func (*InvalidV2Flow) GetFlowType() string {
return "InvalidV2Flow"
}

func (*InvalidV2Flow) GetSteps() []dex.StepDef {
return []dex.StepDef{
dex.DefineStartStep(invalidV2Step{}),
dex.DefineStep(missingGroupStep{}),
}
}

func (flow *InvalidV2Flow) GetRPCs() []dex.RPCDef {
return []dex.RPCDef{
dex.DefineRPC(flow.GetDexSummary, nil),
dex.DefineRPC(flow.GetDexDisplay, nil),
dex.DefineRPC(flow.BreakActionInput, nil),
}
}

func (*InvalidV2Flow) GetPersistenceSchema() dex.PersistenceSchema {
return dex.PersistenceSchema{Attributes: []dex.AttributeDef{state, mismatchedIndex, flag}}
}

// dex:field attribute-key:state value-type:string editable:false
func (*InvalidV2Flow) GetDexSummary(
_ dex.Context,
_ dex.None,
) (*dex.RPCResult[map[string]any], error) {
return &dex.RPCResult[map[string]any]{Output: map[string]any{"state": nil}}, nil
}

// dex:field attribute-key:state value-type:string editable:false description:"State"
// dex:field attribute-key:flag value-type:string editable:false description:"Wrong field type"
// dex:field attribute-key:state value-type:string editable:false description:"unterminated
func (*InvalidV2Flow) GetDexDisplay(
ctx dex.Context,
_ dex.None,
) (*dex.RPCResult[map[string]any], error) {
if err := mutateStateFromView(ctx); err != nil {
return nil, err
}
return &dex.RPCResult[map[string]any]{Output: map[string]any{"state": nil, "flag": nil}}, nil
}

func mutateStateFromView(ctx dex.Context) error {
return state.Set(ctx, "changed")
}

// dex:action action-label:"Break input"
// dex:when attribute-key:state operator:in values:["open"]
// dex:input field-name:missing value-type:string source:user required:true description:"Missing field"
func (*InvalidV2Flow) BreakActionInput(
_ dex.Context,
_ BrokenActionInput,
) (*dex.RPCResult[dex.None], error) {
return &dex.RPCResult[dex.None]{}, nil
}

// dex:action action-label:"Unregistered"
// dex:when attribute-key:state operator:in values:["open"]
func (*InvalidV2Flow) UnregisteredAction(
_ dex.Context,
_ dex.None,
) (*dex.RPCResult[dex.None], error) {
return &dex.RPCResult[dex.None]{}, nil
}

// dex:group group-id:invalid group-label:"Invalid" unexpected:true
type invalidV2Step struct {
dex.StepDefaultsNoWaitFor[dex.None]
}

func (invalidV2Step) GetStepType() string {
return "InvalidV2Step"
}

func (invalidV2Step) Execute(
_ dex.Context,
_ dex.None,
) (*dex.StepDecision, error) {
return dex.ForceComplete(nil), nil
}

type missingGroupStep struct {
dex.StepDefaultsNoWaitFor[dex.None]
}

func (missingGroupStep) GetStepType() string {
return "MissingGroupStep"
}

func (missingGroupStep) Execute(
_ dex.Context,
_ dex.None,
) (*dex.StepDecision, error) {
return dex.ForceComplete(nil), nil
}
23 changes: 15 additions & 8 deletions cli/internal/command/visualize.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,17 +29,19 @@ import (
)

type visualizeOptions struct {
language string
json bool
openBrowser bool
output string
pythonPath string
language string
schemaVersion string
json bool
openBrowser bool
output string
pythonPath string
}

func (a *App) executeVisualize(ctx context.Context, args []string) error {
flags := newFlagSet("dexcli visualize", a.stderr)
options := visualizeOptions{openBrowser: true}
flags.StringVar(&options.language, "language", "auto", "auto, go, or python")
flags.StringVar(&options.schemaVersion, "schema-version", flowviz.SchemaVersionV1, "Flow Definition Graph schema version: 1.0 or 2.0")
flags.BoolVar(&options.json, "json", false, "write Flow Definition Graph JSON instead of opening Flow Rendering")
flags.BoolVar(&options.openBrowser, "open", true, "open Flow Rendering in the default browser")
flags.StringVar(&options.output, "out", "", "JSON output prefix, or - for stdout (requires --json)")
Expand All @@ -56,8 +58,9 @@ func (a *App) executeVisualize(ctx context.Context, args []string) error {
return newUsageError("visualize", err)
}
graph, err := flowviz.Analyze(ctx, source, flowviz.AnalyzeOptions{
Language: options.language,
PythonPath: options.pythonPath,
Language: options.language,
PythonPath: options.pythonPath,
SchemaVersion: options.schemaVersion,
})
if err != nil {
return newOperationError("visualize", err)
Expand Down Expand Up @@ -112,6 +115,9 @@ func validateVisualizeOptions(options visualizeOptions) error {
default:
return fmt.Errorf("language must be auto, go, or python")
}
if options.schemaVersion != flowviz.SchemaVersionV1 && options.schemaVersion != flowviz.SchemaVersionV2 {
return fmt.Errorf("schema-version must be 1.0 or 2.0")
}
if !options.json && options.output != "" {
return fmt.Errorf("--out requires --json")
}
Expand All @@ -136,7 +142,7 @@ func (a *App) renderVisualization(ctx context.Context, isValid bool, graph []byt
}
serverErrors <- err
}()
url := "http://" + listener.Addr().String() + "/rendering"
url := "http://" + listener.Addr().String() + "/v1/rendering"
if options.openBrowser {
if err := a.openBrowser(url); err != nil {
shutdownErr := shutdownVisualizationServer(server, serverErrors)
Expand Down Expand Up @@ -219,6 +225,7 @@ func printVisualizeUsage(output io.Writer) {
fmt.Fprintln(output)
fmt.Fprintln(output, "Flags:")
fmt.Fprintln(output, " --language auto|go|python source language (default auto)")
fmt.Fprintln(output, " --schema-version 1.0|2.0 Flow Definition Graph schema version (default 1.0)")
fmt.Fprintln(output, " --json write Flow Definition Graph JSON instead of rendering")
fmt.Fprintln(output, " --open open Flow Rendering in the default browser (default true)")
fmt.Fprintln(output, " --out path-prefix|- JSON output prefix, or - for stdout (requires --json)")
Expand Down
2 changes: 1 addition & 1 deletion cli/internal/command/visualize_integ_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ func TestVisualizeDefaultsToFlowRendering(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
app.openBrowser = func(url string) error {
response, err := http.Get(strings.TrimSuffix(url, "/rendering") + "/api/flow-definitions")
response, err := http.Get(strings.TrimSuffix(url, "/v1/rendering") + "/api/flow-definitions")
if err != nil {
return err
}
Expand Down
Loading
Loading