Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
14 commits
Select commit Hold shift + click to select a range
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
1 change: 1 addition & 0 deletions changelog.d/added-workflowmodel-editor.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Workflow models, RFC phase 4 UI: Settings → Workflows grows an **Editor** — a visual design surface for lifecycle models rather than a form. Columns are vertical lanes (add, rename by double-click, reorder and resize by dragging the header, toggle terminal); states are chips you drag within and between lanes or drop from a templates palette (common states and columns, hive-default stages); transitions are drawn by dragging a chip's port onto another chip, with backward edges dashed and self-loops drawn as loops. The canvas pans, zooms, fits, shows a minimap, supports marquee and shift-click multi-select, keyboard nudging and Delete, undo/redo, and an Auto-layout that orders lanes by topological depth. An inspector edits the selected state, transition, column or the model itself; a bottom drawer hosts Problems (live compile diagnostics, badged on the canvas and clickable), YAML (two-way via the new `POST /api/workflows/yaml`), Diff (client-side structural diff against the last save, or the stored versions diff) and Simulate (facts → `POST /api/workflows/simulate` against the unsaved model, highlighting the decided state). Save assigns the next version through `POST /api/workflows` with an empty `meta.version`; Save as… and Revert are one click; inferred states carry a ✓ Confirm affordance with a toolbar "Confirm all inferred". Export offers self-contained SVG, PNG, model JSON and YAML. Library rows and detail gain Edit (owner) / Edit a copy (hive-default), and ingest results gain "Open in editor" ([#17620](https://github.com/hivecommons/hive/issues/17620))
1 change: 1 addition & 0 deletions changelog.d/added-workflowmodel-versions-diff-confirm.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Workflow models (RFC phase 4, backend): `GET /api/workflows/{name}/versions`, a deterministic structural diff between two versions (`GET …/{name}/diff?from&to`, optionally cross-model against hive-default), `POST …/{name}/{version}/confirm` to confirm an ingested model's inferred elements as a new local version (owner, audited `workflow_confirm`), `GET …/{name}/{version}/labels` to preview the labels and commands a model regenerates, and the stage-label provisioner now creates or recolours a bound model's own label vocabulary and re-provisions when the binding changes ([#17620](https://github.com/hivecommons/hive/issues/17620))
110 changes: 110 additions & 0 deletions dashboard/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -35459,6 +35459,116 @@
}
}
},
"/api/workflows/yaml": {
"post": {
"tags": [
"Workflows"
],
"summary": "Convert a workflow model between JSON and YAML",
"description": "Editor helper. Send `model` to get its YAML (field names are the JSON tags), or `yaml` to get the parsed model back. A YAML parse or schema error answers 200 with `error` set so the editor can show it inline. Read-only; no store or owner role needed.",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"model": {
"$ref": "#/components/schemas/WorkflowModel"
},
"yaml": {
"type": "string"
}
}
}
}
}
},
"responses": {
"200": {
"description": "yaml, or model (or error) depending on the input",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"yaml": {
"type": "string"
},
"model": {
"$ref": "#/components/schemas/WorkflowModel"
},
"error": {
"type": "string"
}
}
}
}
}
},
"400": {
"description": "Neither model nor yaml supplied, or malformed body"
}
}
}
},
"/api/workflows/simulate": {
"post": {
"tags": [
"Workflows"
],
"summary": "Simulate an issue or PR against an unsaved workflow model",
"description": "Editor variant of /api/workflows/{name}/{version}/simulate: the model travels in the body and `facts` are required (no forge fetch). Same decision and trace shape. Read-only; no store or owner role needed.",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"model",
"kind",
"facts"
],
"properties": {
"model": {
"$ref": "#/components/schemas/WorkflowModel"
},
"kind": {
"type": "string",
"enum": [
"issue",
"pr"
]
},
"facts": {
"$ref": "#/components/schemas/WorkflowSimulateFacts"
}
}
}
}
}
},
"responses": {
"200": {
"description": "Decision, trace and normalized facts",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/WorkflowSimulateResult"
}
}
}
},
"400": {
"description": "Missing model, kind or facts"
},
"422": {
"description": "Model does not compile; body carries diagnostics"
}
}
}
},
"/api/workflows/bindings": {
"get": {
"tags": [
Expand Down
111 changes: 111 additions & 0 deletions src/docs/design/workflow-models.md
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,117 @@ exactly the pre-P4 path: one list, the built-in family, no model labels.
- A model without a `labels` block regenerates nothing (TechDocs as shipped); the preview still
shows its commands and the built-in stages its numbered states land on.

## Editor (RFC phase 4, UI)

The owner's brief was "the editor needs to be a good design tool too": a design surface in the
spirit of a lightweight draw.io for state machines, not a form over the JSON. Settings → Workflows
grows an **Editor** nav item (`config-workflows-editor.js`, `workflows-editor.css`, registered
right after the library module), and the library rows and detail page grow **Edit** (owner) or
**Edit a copy** (hive-default and other built-ins, which are read-only); an ingest result gains
**Open in editor** so an ingested draft can be reviewed on the canvas before it is saved.

**Layout.** Toolbar · left templates palette · centre SVG canvas · right inspector · bottom
tabbed drawer (Problems · YAML · Diff · Simulate). With no model loaded the Editor shows a start
screen: New blank model, Duplicate hive-default, Open from Library, From Ingest, Marketplace
(disabled placeholder for phase 7).

**Canvas.** Columns are vertical lanes: `+ Column` adds one, double-click the header renames it,
drag the header horizontally to reorder, drag its right edge to resize (snapped to an 8px grid,
`view.laneWidths`), terminal lanes are shaded and dashed. States are chips inside lanes: drag
within or between lanes (rewrites `columns[].states` and `lifecycle.column`), double-click to
rename the label, Delete/Backspace removes the selection and its transitions (confirmed when more
than one), marquee on empty canvas, shift-click to extend, arrow keys nudge order. Each chip has
a right-edge port; dragging it onto another chip creates a transition with `on: ''`, selects it
and focuses the inspector's `on` field; clicking an edge selects it. Pan with space+drag, middle button or wheel; zoom with
ctrl/⌘+wheel or the toolbar ± / Fit; a minimap appears when the model outgrows the viewport.
**Auto-layout** orders lanes by BFS depth from the initial state (terminal lanes last), drops
unassigned states into the nearest lane by depth, and sorts each lane by stage number then
label. Compile diagnostics appear as badges on chips (error red, warning amber, tooltip text);
inferred states are dashed with a one-click ✓ **Confirm** that sets `confidence: local` locally,
and a toolbar **Confirm all inferred** that, for a stored ingested version, also calls
`POST /api/workflows/{name}/{version}/confirm` after saving. The initial state carries a marker.

**Edge layer: quiet by default, detail on focus.** A real model (hive-default has 22 states and
~90 transitions) turns a naive one-bezier-per-transition drawing into a knot, so the canvas and
the read-only library diagram share one pure router (`wfRouteEdges` in
`config-workflows-library.js`) and a set of rules:

- *No labels by default.* Every edge renders its event as a floating pill, but CSS shows it only
while that edge is hovered or selected. A toolbar **Labels** toggle shows labels for the
exceptional events only — the model's most common `on` (e.g. `stage-change`, detected by
`wfEdgeCommonEvent`) is never worth labelling and stays quiet.
- *Orthogonal routing through gutters, bundled per lane pair.* Edges leave the source chip's
right port, run into the gutter between the lanes, drop vertically in a channel reserved for
that (from-lane → to-lane) pair and enter the target's left port with rounded corners
(`wfOrthoPath`). Edges that share a lane pair share one vertical trunk, so the Actionable →
Claimed bundle is one line that fans out at both ends. Same-lane edges hug the lane's right
margin, stacked by distance. Edges that skip lanes run below the lanes in a corridor row;
backward edges between adjacent lanes run down the shared gutter (both ports face it), and
backward edges that skip lanes route around the *outside* above the lane headers, dashed and
muted. `wfEdgeChannels` counts the corridor rows so the layout reserves that space.
- *Focus mode.* Hovering or selecting a state dims every edge not incident to it
(`--wf-edge-dim`), lifts the incident edges into a front layer in the accent colour with
arrowheads (`fill="context-stroke"`), and outlines the neighbouring chips; hovering an edge
highlights it and both end chips and reveals its label. The hover handler
(`wfEdgeFocusHover`) is DOM-only and shared by the editor canvas and the library diagram.
- *Fan-out collapse.* More than four transitions from one state into the same lane collapse to
one trunk with a **+N** badge at the source port; hovering expands the fan, and in the editor
clicking the badge pins it open (`view.fansOpen`).

Ports, drag-to-connect, arrowheads and the initial/terminal styling are unchanged; every colour,
opacity and size is a token so the style ratchet stays green.

**Pure core.** Everything that mutates or lays out the model is a pure function over plain JSON
(never mutates its input, deterministic), so the undo stack is a snapshot list and the core is
unit-tested under node (`workflows_editor_ui_test.go`): `wfEdNewModel`, `wfEdNormalize`,
`wfEdAddColumn` / `wfEdMoveColumn` / `wfEdRenameColumn` / `wfEdRemoveColumn(model, id,
moveStatesTo)`, `wfEdAddState` / `wfEdMoveState` / `wfEdRenameState` (rewrites transitions,
initial, label entries, derivation rules, lanes and agent grants) / `wfEdRemoveState`,
`wfEdAddTransition` / `wfEdRemoveTransition`, `wfEdUpdate(model, path, value)` with paths like
`spec.states[id=open].lifecycle.number` (empty value deletes), `wfEdUniqueId` (slug + `-N`),
`wfEdAutoLayout`, `wfEdLayout(model, profile, view)` → lanes/chips/edges/fans geometry (edges via the shared `wfRouteEdges` / `wfOrthoPath` / `wfEdgeChannels` router),
`wfEdLaneAt` / `wfEdSlotAt` for drops, `wfEdLocalProblems` (mirrors the compiler's refusals —
`state-no-exit`, `state-no-column`, `unknown-column`, `unknown-state`, `column-empty`,
`no-terminal-column`, `duplicate-stage-number`, `unknown-event`, `inferred-element` — for
instant feedback before the server answers), `wfEdDiff(a, b)` (the client twin of
`workflowmodel.Diff`: keyed change sets for columns, states, transitions, labels and rules plus
meta field changes and a summary), `wfEdSvg(model, layout, state)` and `wfEdMinimapSvg`.

**Runtime.** `_wfEd = {model, saved, history[], future[], selection, view{panX,panY,zoom,
laneWidths}, compile{busy,profile,diags}, drawer, sim, yaml, …}`. `wfEdCommit(next)` pushes a
deep-copy snapshot (cap 200), bumps a revision, re-renders only the affected regions and
schedules a live compile: 300 ms after the last change the model is posted to
`POST /api/workflows/compile` and the toolbar pill reads `compiles` / `N warnings` / `does not
compile`. Pointer interactions attach once per render (`wfEdAttach(root)`, idempotent) to the
canvas root; every other control is a delegated `data-action`. Cmd/Ctrl-Z, Shift-Cmd-Z and Cmd-S
work when the editor has focus. Leaving the editor with unsaved changes asks via `hiveConfirm`.

**Drawers.** *Problems* lists diagnostics (click selects the element). *YAML* is a textarea
bound two-way through the new `POST /api/workflows/yaml` (`{model}` → `{yaml}` and `{yaml}` →
`{model, error?}`; model→YAML refreshes while the tab is visible and the textarea is not focused;
Apply parses back and commits; parse errors show inline). *Diff* shows the client-side
`wfEdDiff` of the working copy against the last save as grouped lists, and, for a stored
version, the stored-versions diff (`GET …/diff?from&to`) against any earlier version. *Simulate*
uses the library's declarative `WF_SIM_FIELDS` form and posts `{model, kind, facts}` to the new
`POST /api/workflows/simulate`, which compiles the unsaved body and runs `stageengine.Explain`
(422 with diagnostics when the model does not compile); the decided state is highlighted on the
canvas (`.wf-ed-sim-hit`, others dimmed) and the reason and trace render below the form.

**Persistence.** *Save* posts the model with `meta.version: ''`, which `handleWorkflowSave` now
resolves through `store.NextVersion(name)`, then reloads the library and reopens the saved entry
in the editor. *Save as…* prompts a new name (version 1, provenance local). *Revert* restores the
last saved snapshot. hive-default opens read-only with an *Edit a copy* button.

**Export.** SVG (the canvas serialized with the CSS custom-property values resolved via
`getComputedStyle`, so it is self-contained), PNG (drawn to a canvas and shown in a dialog with
open-in-new-tab — no forced download), model JSON and model YAML, all through
`hiveExportViewer`.

**Deferred.** Editing events, gates, roles and derivation rules (they round-trip untouched and
are editable through the YAML drawer); nested regions beyond flattening; marketplace (phase 7);
keyboard-only lane reorder; collaborative/locking semantics; server-side layout persistence
(lane widths live in the session).

## Ingest: intake, triage, roles and branch rules (RFC phase 5)

`ingest.Lifecycle` now reads the repository's process conventions next to its pipelines and adds
Expand Down
11 changes: 11 additions & 0 deletions src/pkg/dashboard/api_workflows.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"errors"
"fmt"
"net/http"
"strings"
"time"

"github.com/hivecommons/hive/pkg/config"
Expand All @@ -28,6 +29,7 @@ func (s *Server) registerWorkflowRoutes() {
s.mux.HandleFunc("DELETE /api/workflows/{name}/{version}", s.handleWorkflowDelete)
s.registerWorkflowVersionRoutes()
s.mux.HandleFunc("POST /api/workflows/{name}/{version}/simulate", s.handleWorkflowSimulate)
s.registerWorkflowEditorRoutes()
s.registerWorkflowIngestSourceRoutes()
}

Expand Down Expand Up @@ -130,6 +132,15 @@ func (s *Server) handleWorkflowSave(w http.ResponseWriter, r *http.Request) {
jsonError(w, fmt.Sprintf("body is %s@%s, path is %s@%s", m.Metadata.Name, m.Spec.Meta.Version, name, r.PathValue("version")), http.StatusBadRequest)
return
}
// The editor posts meta.version "" to mean "next free integer version".
if r.PathValue("name") == "" && strings.TrimSpace(m.Spec.Meta.Version) == "" && m.Metadata.Name != "" {
v, err := st.NextVersion(m.Metadata.Name)
if err != nil {
workflowStoreError(w, err)
return
}
m.Spec.Meta.Version = v
}
e, err := st.Save(&m, requestUser(r), time.Now().UTC())
if err != nil {
workflowStoreError(w, err)
Expand Down
Loading
Loading