@echelon-foundry/design-system 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/README.md +29 -0
  2. package/contracts/diagram-presentation.json +109 -0
  3. package/contracts/workflow-capabilities.json +25 -0
  4. package/dist/all.css +175 -3
  5. package/dist/build-metrics.json +32 -4
  6. package/dist/components.css +173 -3
  7. package/dist/foundations.css +2 -0
  8. package/dist/marketing/forma-all.css +176 -4
  9. package/dist/marketing/forma-echelon-marketing.css +3 -1
  10. package/dist/marketing/forma-marketing-theme-echelon.css +1 -1
  11. package/dist/marketing/forma-marketing.css +3 -1
  12. package/dist/marketing/forma-marketing.manifest.json +9 -9
  13. package/dist/marketing/forma-marketing.sha256 +4 -4
  14. package/dist/patterns/bounded-overflow.html +9 -0
  15. package/dist/patterns/button.html +5 -0
  16. package/dist/patterns/container.html +6 -0
  17. package/dist/patterns/date-time-field.html +5 -0
  18. package/dist/patterns/grid.html +5 -0
  19. package/dist/patterns/image-choice.html +2 -2
  20. package/dist/patterns/key-value-list.html +5 -0
  21. package/dist/patterns/section.html +4 -0
  22. package/dist/patterns/skip-link.html +6 -0
  23. package/dist/patterns/split.html +12 -0
  24. package/dist/patterns/surface.html +4 -0
  25. package/dist/patterns/text-field.html +5 -0
  26. package/dist/patterns/text-roles.html +7 -0
  27. package/dist/patterns/textarea.html +5 -0
  28. package/dist/patterns/visually-hidden.html +4 -0
  29. package/docs/MARKETING-SITES.md +1 -1
  30. package/docs/workflow/EMBEDDING.md +148 -0
  31. package/docs/workflow/EXTERNAL-PRODUCERS.md +34 -0
  32. package/docs/workflow/FORMAT.md +196 -0
  33. package/docs/workflow/RENDERING.md +90 -0
  34. package/docs/workflow/REQUIREMENTS-AUDIT-2026-10-01.md +55 -0
  35. package/docs/workflow/VALIDATION.md +91 -0
  36. package/examples/workflows/fixtures.json +101 -0
  37. package/examples/workflows/forma/workflows/accessible.forma-workflow.json +90 -0
  38. package/examples/workflows/forma/workflows/branching.forma-workflow.json +91 -0
  39. package/examples/workflows/forma/workflows/branded.forma-workflow.json +85 -0
  40. package/examples/workflows/forma/workflows/color-mapping.forma-workflow.json +111 -0
  41. package/examples/workflows/forma/workflows/colored.forma-workflow.json +131 -0
  42. package/examples/workflows/forma/workflows/edge-metadata.forma-workflow.json +66 -0
  43. package/examples/workflows/forma/workflows/embedded-edit.forma-workflow.json +117 -0
  44. package/examples/workflows/forma/workflows/embedded-view.forma-workflow.json +93 -0
  45. package/examples/workflows/forma/workflows/extensions.forma-workflow.json +90 -0
  46. package/examples/workflows/forma/workflows/groups.forma-workflow.json +89 -0
  47. package/examples/workflows/forma/workflows/interactive-export.forma-workflow.json +62 -0
  48. package/examples/workflows/forma/workflows/minimal.forma-workflow.json +17 -0
  49. package/examples/workflows/forma/workflows/mobile.forma-workflow.json +80 -0
  50. package/examples/workflows/forma/workflows/multi-step.forma-workflow.json +82 -0
  51. package/examples/workflows/forma/workflows/no-layout.forma-workflow.json +90 -0
  52. package/examples/workflows/forma/workflows/node-metadata.forma-workflow.json +80 -0
  53. package/examples/workflows/forma/workflows/runtime-ports.forma-workflow.json +118 -0
  54. package/examples/workflows/forma/workflows/static-export.forma-workflow.json +77 -0
  55. package/examples/workflows/forma/workflows/swimlanes.forma-workflow.json +130 -0
  56. package/figma/component-contracts.json +140 -0
  57. package/package.json +28 -9
  58. package/patterns/bounded-overflow.html +9 -0
  59. package/patterns/button.html +5 -0
  60. package/patterns/container.html +6 -0
  61. package/patterns/date-time-field.html +5 -0
  62. package/patterns/grid.html +5 -0
  63. package/patterns/image-choice.html +2 -2
  64. package/patterns/key-value-list.html +5 -0
  65. package/patterns/section.html +4 -0
  66. package/patterns/skip-link.html +6 -0
  67. package/patterns/split.html +12 -0
  68. package/patterns/surface.html +4 -0
  69. package/patterns/text-field.html +5 -0
  70. package/patterns/text-roles.html +7 -0
  71. package/patterns/textarea.html +5 -0
  72. package/patterns/visually-hidden.html +4 -0
  73. package/schemas/workflow/1.0/forma-workflow.schema.json +1045 -0
@@ -0,0 +1,12 @@
1
+ <div class="ef-site">
2
+ <div class="ef-split">
3
+ <div>
4
+ <h2>Install</h2>
5
+ <p>The primary region takes the available space. The supporting region wraps below it when the row runs out of room.</p>
6
+ </div>
7
+ <aside aria-label="Requirements">
8
+ <h3>Requirements</h3>
9
+ <p>Node 22 or later.</p>
10
+ </aside>
11
+ </div>
12
+ </div>
@@ -0,0 +1,4 @@
1
+ <section class="ef-surface" aria-labelledby="surface-title">
2
+ <h2 id="surface-title">Billing contact</h2>
3
+ <p>Invoices are sent to the finance team on the first business day of each month.</p>
4
+ </section>
@@ -0,0 +1,5 @@
1
+ <div class="ef-field">
2
+ <label class="ef-field__label" for="text-field-email">Work email</label>
3
+ <span class="ef-field__description" id="text-field-email-description">We send sign-in links to this address.</span>
4
+ <input id="text-field-email" name="email" type="email" autocomplete="email" aria-describedby="text-field-email-description" required>
5
+ </div>
@@ -0,0 +1,7 @@
1
+ <div class="ef-site">
2
+ <p class="ef-eyebrow">Release 0.3</p>
3
+ <h2>Faster onboarding for new teams</h2>
4
+ <p class="ef-lead">A lead paragraph introduces the section in one or two sentences.</p>
5
+ <blockquote class="ef-statement"><p>Forma keeps behavior in the application and presentation in the design system.</p></blockquote>
6
+ <span class="ef-component-kicker">Component kicker</span>
7
+ </div>
@@ -0,0 +1,5 @@
1
+ <div class="ef-field">
2
+ <label class="ef-field__label" for="textarea-summary">Change summary</label>
3
+ <span class="ef-field__description" id="textarea-summary-description">Describe what changed and why. Plain text, up to 500 characters.</span>
4
+ <textarea id="textarea-summary" name="summary" rows="4" maxlength="500" aria-describedby="textarea-summary-description"></textarea>
5
+ </div>
@@ -0,0 +1,4 @@
1
+ <button type="button">
2
+ <span aria-hidden="true">×</span>
3
+ <span class="ef-visually-hidden">Remove filter: Region is Europe</span>
4
+ </button>
@@ -314,7 +314,7 @@ Keep the `@v<version>` of the Forma actions equal to the version in
314
314
  3. Add it in Forma:
315
315
  - pattern HTML;
316
316
  - CSS in `src/marketing/` using role tokens only;
317
- - metadata in `tools/component-meta.mjs`;
317
+ - a catalog entry in `catalog/components/<slug>.mjs` (see `docs/CATALOG-AUTHORING.md`);
318
318
  - tests (structure, browser, axe, 320 px);
319
319
  - a gap entry closed in `requirements/MARKETING-PRESENTATION.md`.
320
320
  4. Release it. The sites then upgrade (question 7) and remove any local
@@ -0,0 +1,148 @@
1
+ # Embedding the workflow renderer and editor
2
+
3
+ `@echelon-foundry/forma-workflow` is the reusable workflow component. It
4
+ provides the public `<forma-workflow>` element and runs the F# engine
5
+ (`Forma.Workflow`) on .NET WebAssembly. It is a separate, opt-in package. The
6
+ design-system package stays zero-runtime (ADR-0006). You do not need Forma
7
+ Studio.
8
+
9
+ A complete host application that is not Studio lives in
10
+ [`examples/workflow-embedding/`](../../examples/workflow-embedding/). It shows
11
+ runtime, edit, pick and isolated modes and a partner brand. Its browser tests
12
+ are in [`tests/browser/workflow-embed.spec.mjs`](../../tests/browser/workflow-embed.spec.mjs).
13
+
14
+ ## Same-page embedding (no iframe)
15
+
16
+ ```html
17
+ <link rel="stylesheet" href="/assets/forma/tokens.css">
18
+ <link rel="stylesheet" href="/assets/forma/foundations.css">
19
+ <link rel="stylesheet" href="/assets/forma/components.css">
20
+ <link rel="stylesheet" href="/assets/forma-workflow/forma-workflow.css">
21
+ <script type="module" src="/assets/forma-workflow/forma-workflow.js"></script>
22
+
23
+ <forma-workflow id="designer" mode="edit"></forma-workflow>
24
+ <script type="module">
25
+ const designer = document.getElementById("designer");
26
+ const workflow = await (await fetch("/forma/workflows/launch.forma-workflow.json")).json();
27
+ await designer.load(workflow);
28
+ designer.addEventListener("forma-workflow-change", (e) => save(e.detail.workflow));
29
+ </script>
30
+ ```
31
+
32
+ You can also put the document inline as an inert data block. The element then
33
+ loads it on start, and the static figure inside the element is the
34
+ no-script fallback:
35
+
36
+ ```html
37
+ <forma-workflow mode="view">
38
+ <script type="application/json" class="forma-workflow-source">{ "format": "forma-workflow", … }</script>
39
+ <figure class="ef-diagram">…static rendering…</figure>
40
+ </forma-workflow>
41
+ ```
42
+
43
+ The element inherits Forma tokens, brands (`data-ef-brand` on any ancestor),
44
+ themes, forced colours, reduced motion and responsive behaviour from the host
45
+ page.
46
+
47
+ ## Host modes
48
+
49
+ | `mode` | What people can do |
50
+ |---|---|
51
+ | `view` | Read. Zoom. Activate declared intents. |
52
+ | `inspect` | Select objects and read their metadata, references, status and extensions. |
53
+ | `edit` | Create, connect, reconnect, move, resize, rename and recolour. Set status, ports, groups and lanes, metadata, references, interactions and accessibility. Arrange. Undo and redo. |
54
+ | `pick` | Choose one or more objects; the host receives them on "Use selection". |
55
+ | `runtime` | Read. The host projects live state with `setRuntimeState`. The document is never changed. |
56
+
57
+ ## Host contract `forma-workflow-host/1`
58
+
59
+ The contract is small and versioned. Hosts never see the engine's internal state.
60
+
61
+ **Attributes:** `mode`, `instance` (optional stable id), `engine` (optional URL of the engine directory).
62
+
63
+ **Methods** (each returns a promise):
64
+
65
+ | Method | Effect |
66
+ |---|---|
67
+ | `load(workflow)` | Load a document (object or JSON text). Emits `forma-workflow-load`. |
68
+ | `getWorkflow()` | The current document, canonical JSON. |
69
+ | `execute(command)` | Run a validated command, for example `{ name: "addNode", kind: "task", label: "Review" }`, `{ name: "connect", source: { node: "a" }, target: { node: "b" } }`, `{ name: "setLabel", object: "node:a", value: "…" }`, `{ name: "setMetadata", object: "node:a", key: "owner", value: "Ops" }`, `{ name: "setStatus", object: "node:a", status: { state: "active" } }`, `{ name: "setColor", object: "node:a", property: "fill", value: "#e8f0ff" }`, `{ name: "move", nodes: ["a"], dx: 8, dy: 0 }`, `{ name: "delete", objects: ["edge:e1"] }`, `{ name: "autoLayout" }`. |
70
+ | `undo()`, `redo()` | History. |
71
+ | `select(objects)` | `["node:a", { type: "edge", id: "e1" }]`. |
72
+ | `focusObject(object)` | Move keyboard focus to an object. |
73
+ | `setRuntimeState(states)` | `{ "nodeId": { state: "active", label: "In progress" }, "other": null }`. |
74
+ | `validate(workflow)` | The validation report, without loading. |
75
+
76
+ **Events** (bubbling `CustomEvent`s; `detail` is JSON):
77
+
78
+ | Event | `detail` |
79
+ |---|---|
80
+ | `forma-workflow-ready` | Engine started. |
81
+ | `forma-workflow-load` | `valid`, `class`, `findings`, `workflow`. |
82
+ | `forma-workflow-change` | `description`, `workflow` (the new document), `valid`, `class`, `findings`. Only legal, validated changes arrive. |
83
+ | `forma-workflow-selection` | `objects: [{ type, id }]`. |
84
+ | `forma-workflow-focus` | `object`. |
85
+ | `forma-workflow-intent` | `object`, `action`, `target`, `command`, `label`, `parameters`. The host decides what to do. |
86
+ | `forma-workflow-pick` | `objects`. |
87
+ | `forma-workflow-refused` | `message`. A command was refused and nothing changed. |
88
+ | `forma-workflow-error` | `message` (the engine failed to start). |
89
+
90
+ A command that would introduce a new validation error is refused. Examples are a
91
+ dangling connection, a port over its limit, or a blank name. Unknown metadata
92
+ and extensions survive every edit.
93
+
94
+ ## Interaction model
95
+
96
+ - **Keyboard.** Every object is a button in the Objects list. Arrow keys move the selected steps (Shift moves further). Delete removes them. Escape cancels. All inspector fields are native form controls.
97
+ - **Pointer.** Click selects. Ctrl, Cmd or Shift-click adds to the selection. Drag a selected step to move it; it is one change when released. Drag the corner handle to resize. Drag the round handle onto another step to connect.
98
+ - **Touch and mobile.** Tap selects. No operation needs dragging: "Connect" then tap a target, the Connect form, the Move buttons and the Position and size fields all work without it. Handles have a 44px hit area. Below 64rem the layout stacks, with the canvas first, and the canvas scrolls inside its own region.
99
+ - **Zoom and pan.** Use the zoom buttons. Pan by scrolling the canvas. Zoom is view state and is never saved.
100
+ - **Announcements.** A polite live region reports each change, refusal and selection.
101
+
102
+ ## .NET hosts
103
+
104
+ A host that already runs .NET, such as Forma Studio, links `Forma.Workflow`.
105
+ It supplies the element's transport itself instead of loading a second runtime:
106
+
107
+ ```js
108
+ element.transport = {
109
+ async dispatch(json) { return myExports.WorkflowDispatch(json); }, // backed by EmbedHost.dispatch
110
+ async document(id) { return myExports.WorkflowDocument(id); },
111
+ async dispose(id) { myExports.WorkflowDispose(id); },
112
+ async validate(text) { return JSON.parse(myExports.WorkflowValidate(text)); }
113
+ };
114
+ ```
115
+
116
+ ## Isolated mode (iframe)
117
+
118
+ Use isolated mode only when you need stronger separation. It carries the same
119
+ portable document over the same `forma-workflow-host/1` contract, through
120
+ `postMessage`:
121
+
122
+ ```html
123
+ <iframe src="/assets/forma-workflow/frame.html?parent=https://app.example.org&forma=https://app.example.org/assets/forma/&brand=echelon"></iframe>
124
+ ```
125
+
126
+ | From the parent | Fields |
127
+ |---|---|
128
+ | `{ protocol, id, type: "load", document }` | |
129
+ | `{ protocol, id, type: "mode", mode }` | |
130
+ | `{ protocol, id, type: "command", command }` | |
131
+ | `{ protocol, id, type: "select", objects }`, `"focus"`, `"runtime"`, `"undo"`, `"redo"`, `"get"` | |
132
+
133
+ | From the frame | Fields |
134
+ |---|---|
135
+ | `{ protocol, type: "frame-ready" }` | |
136
+ | `{ protocol, type: "reply", id, ok, workflow?, error? }` | |
137
+ | `{ protocol, type: "event", event: { type, … } }` | the same events as above |
138
+
139
+ The frame accepts messages only from the origin in `?parent=`, and only from
140
+ `window.parent`. It posts only to that origin.
141
+
142
+ ## Security
143
+
144
+ Workflow files are data. The engine builds every view from a typed HTML tree
145
+ that escapes all text and attributes. It never emits event-handler attributes
146
+ or `javascript:` URLs. Intents are reported, never executed. The host element
147
+ has no `eval`, no `new Function` and no decision logic; a package test checks
148
+ this.
@@ -0,0 +1,34 @@
1
+ # Producing workflows outside Forma
2
+
3
+ Any system can create a valid workflow from the published schema alone.
4
+ [`examples/external-producer/produce.mjs`](../../examples/external-producer/produce.mjs)
5
+ does it with plain Node.js and no Forma code at all.
6
+ [`consume.mjs`](../../examples/external-producer/consume.mjs) reads the file
7
+ back after other tools have edited it.
8
+
9
+ ## Rules for producers
10
+
11
+ 1. Write `format`, `formatVersion: "1.0.0"`, `id`, `title`, `forma.version` and `nodes`.
12
+ 2. Give every node, edge and group a stable id. Never reuse array positions as ids.
13
+ 3. Leave out coordinates unless you mean them. Forma lays out a workflow with no layout deterministically.
14
+ 4. Put meaning in core fields: `kind`, `status`, `color`, `references`, group `members`. Put your own data in `metadata` or in `extensions["your.namespace"]`.
15
+ 5. Validate before publishing: `forma-workflow validate --json your.forma-workflow.json`.
16
+ 6. Save to `forma/workflows/<id>.forma-workflow.json`.
17
+
18
+ ## Rules for consumers
19
+
20
+ - Read the core fields you understand. Keep everything else, including unknown extensions and metadata, and write it back unchanged.
21
+ - Check `formatVersion` and the validation class before relying on a document.
22
+
23
+ ## The round-trip proof
24
+
25
+ 1. An external producer writes a `.forma-workflow.json` file.
26
+ 2. Forma validates it.
27
+ 3. Forma Studio opens it and edits it.
28
+ 4. Forma Studio saves it.
29
+ 5. An external consumer reads it.
30
+
31
+ `npm run proof:external` in kemiller2002/forma-studio runs all five steps. It
32
+ checks that stable ids, nodes, edges, semantics, colours, metadata, unknown
33
+ namespaced extensions and domain references survive. The evidence is recorded
34
+ in that repository under `docs/evidence/`.
@@ -0,0 +1,196 @@
1
+ # Forma portable workflow format 1.0
2
+
3
+ Status: **normative**. Requirement: [`requirements/PORTABLE-WORKFLOW-INTERCHANGE.md`](../../requirements/PORTABLE-WORKFLOW-INTERCHANGE.md).
4
+ Decision: [ADR-0006](../decisions/ADR-0006-portable-workflow-capability.md).
5
+
6
+ A Forma workflow is a JSON document. Any system can write one: an application,
7
+ an agent, a backend service, a CLI or Forma Studio. Forma validates it, lays it
8
+ out, renders it to HTML and edits it. You do not need Forma Studio for any of
9
+ these.
10
+
11
+ | Item | Value |
12
+ |---|---|
13
+ | File suffix | `.forma-workflow.json` |
14
+ | Repository convention | `forma/workflows/<workflow-id>.forma-workflow.json` |
15
+ | Format identifier | `"format": "forma-workflow"` |
16
+ | Format version | `"formatVersion": "1.0.0"` (semantic versioning) |
17
+ | JSON Schema (repository) | [`schemas/workflow/1.0/forma-workflow.schema.json`](../../schemas/workflow/1.0/forma-workflow.schema.json) |
18
+ | JSON Schema (package) | `@echelon-foundry/design-system/workflow/forma-workflow.schema.json` |
19
+ | JSON Schema (`$id`) | `https://forma.echelonfoundry.com/schemas/workflow/1.0/forma-workflow.schema.json` |
20
+ | Capability contract | `@echelon-foundry/design-system/contracts/workflow-capabilities.json` |
21
+ | Reference fixtures | [`examples/workflows/forma/workflows/`](../../examples/workflows/forma/workflows/) with [`fixtures.json`](../../examples/workflows/fixtures.json) |
22
+
23
+ ## The smallest workflow
24
+
25
+ ```json
26
+ {
27
+ "format": "forma-workflow",
28
+ "formatVersion": "1.0.0",
29
+ "id": "minimal",
30
+ "title": "Minimal workflow",
31
+ "forma": { "version": "0.4.0" },
32
+ "nodes": [ { "id": "start", "kind": "start", "label": "Start" } ]
33
+ }
34
+ ```
35
+
36
+ ## A realistic workflow
37
+
38
+ ```json
39
+ {
40
+ "$schema": "https://forma.echelonfoundry.com/schemas/workflow/1.0/forma-workflow.schema.json",
41
+ "format": "forma-workflow",
42
+ "formatVersion": "1.0.0",
43
+ "id": "launch-readiness",
44
+ "title": "Launch readiness poll",
45
+ "forma": { "version": "0.4.0" },
46
+ "metadata": { "program": "Example mission", "revision": 3 },
47
+ "nodes": [
48
+ { "id": "poll", "kind": "start", "label": "Begin readiness poll" },
49
+ { "id": "go", "kind": "decision", "label": "All stations go?",
50
+ "metadata": { "owner": "Launch director" } },
51
+ { "id": "hold", "kind": "task", "label": "Call a hold",
52
+ "status": { "state": "pending" },
53
+ "color": { "fill": "#fff1d6", "foreground": "#1d1d1b" } },
54
+ { "id": "launch", "kind": "end", "label": "Proceed to launch",
55
+ "references": [ { "id": "count", "system": "gov.nasa.example.range", "type": "Countdown", "key": "CD-77" } ],
56
+ "extensions": { "com.example.console": { "panel": 4 } } }
57
+ ],
58
+ "edges": [
59
+ { "id": "e1", "source": { "node": "poll" }, "target": { "node": "go" } },
60
+ { "id": "e2", "source": { "node": "go" }, "target": { "node": "launch" }, "kind": "conditional", "label": "Go" },
61
+ { "id": "e3", "source": { "node": "go" }, "target": { "node": "hold" }, "kind": "conditional", "label": "No go", "line": "dashed",
62
+ "metadata": { "logged": true } }
63
+ ]
64
+ }
65
+ ```
66
+
67
+ No coordinates appear anywhere. Forma places the nodes with its deterministic
68
+ default layout.
69
+
70
+ ## Top-level properties
71
+
72
+ | Property | Required | Meaning |
73
+ |---|---|---|
74
+ | `format`, `formatVersion` | yes | Identify the format and its version. |
75
+ | `id` | yes | Stable workflow id: lowercase letters, digits and hyphens. It is also the file stem. |
76
+ | `title` | yes | Human name. It becomes the accessible name of the rendered diagram. |
77
+ | `description`, `language`, `direction` | no | Description, BCP 47 language and base text direction (`ltr`, `rtl`, `auto`). |
78
+ | `forma` | yes | `{ "version": "0.4.0", "requires": ["workflow.ports"] }`: the target Forma release and, optionally, the capabilities it requires. |
79
+ | `metadata` | no | Arbitrary JSON object. It is preserved and never interpreted. |
80
+ | `palette` | no | Named colour slots, `{ "finance": "token:accent-primary" }`. |
81
+ | `nodes` | yes | Steps or items. |
82
+ | `edges` | no | Connections. |
83
+ | `groups` | no | Groups, containers, swimlanes and phases. |
84
+ | `layout` | no | Workflow layout settings: `direction` (`right` or `down`), `mode` (`authored` or `auto`) and `canvas`. |
85
+ | `presentation` | no | `density` and a `legend` that explains colour and line meaning in text. |
86
+ | `extensions` | no | Producer data by namespace. |
87
+ | `provenance` | no | `producer`, `createdAt`, `modifiedAt`, `source` and `modifiedBy`. |
88
+
89
+ ## Identity
90
+
91
+ Every addressable object has a stable `id`. Nodes, edges and groups share one id
92
+ namespace per workflow. Ports are unique within their node. References are
93
+ unique within their owner. Legend entries are unique within the legend.
94
+
95
+ Connections and memberships always point at ids. They never use array
96
+ positions, DOM identity or editor-only ids. Ids survive editing, re-layout and
97
+ saving.
98
+
99
+ ## Nodes
100
+
101
+ | Property | Meaning |
102
+ |---|---|
103
+ | `kind` | `start`, `end`, `task`, `decision`, `merge`, `event`, `subprocess`, `data`, `note` or `external`. A producer kind is `<namespace>:<name>`, such as `com.example.erp:approval`, and must carry `kindLabel`. |
104
+ | `kindLabel` | Display text for the kind. Optional for core kinds. |
105
+ | `label`, `description` | Visible text. |
106
+ | `variant` | Forma shape: `rectangle`, `rounded`, `pill`, `ellipse` or `diamond`. Presentation only; a shape never implies a kind. |
107
+ | `color` | `fill`, `stroke`, `accent` and `foreground`. Each is a `#rrggbb` literal, a Forma token (`token:accent-primary`) or a palette slot (`palette:finance`). |
108
+ | `status` | `{ "state": "active", "label": "In progress", "detail": "...", "updatedAt": "..." }`. Core states are `none`, `pending`, `ready`, `active`, `waiting`, `complete`, `blocked`, `failed`, `skipped`, `cancelled` and `unknown`. A namespaced state needs a `label`. |
109
+ | `ports` | Connection points: `id`, `side` (`top`, `right`, `bottom`, `left`), `direction` (`in`, `out`, `inout`), `label`, `maxConnections`. |
110
+ | `references` | Domain references: `id`, `system` (a namespace), `type`, `key`, `label` and `href`. Forma displays them and never dereferences them. |
111
+ | `metadata` | Arbitrary JSON object. It is preserved unchanged. |
112
+ | `accessibility` | `name` and `description` for assistive technology, when they should differ from the visible text. |
113
+ | `interaction` | A declarative intent (see below). |
114
+ | `layout` | Optional geometry and layout intent: `x`/`y` (together or not at all), `width`, `height`, `rank`, `order` and `size` (`compact`, `standard`, `wide`). |
115
+ | `extensions` | Producer data by namespace. |
116
+
117
+ ## Edges
118
+
119
+ `id`, `source` and `target` are required. `source` and `target` are
120
+ `{ "node": "...", "port": "..." }`. Edges also accept these properties:
121
+
122
+ - `direction`: `forward` (the default), `backward`, `both` or `none`.
123
+ - `kind`: `sequence` (the default), `conditional`, `default`, `exception`, `message`, `association`, `data` or a namespaced kind with `kindLabel`.
124
+ - `label`, `description`, `line` (`solid`, `dashed`, `dotted`), `width` (1 to 8) and `color.stroke`.
125
+ - `status`, `references`, `metadata`, `accessibility`, `interaction`.
126
+ - `layout`: `routing` (`straight` or `orthogonal`), `waypoints` and the label position.
127
+ - `extensions`.
128
+
129
+ ## Groups, containers, swimlanes and phases
130
+
131
+ `kind` is one of `group`, `container`, `swimlane` or `phase`. A group lists its
132
+ `members` (node ids) explicitly. Nesting uses `parent`. Canvas overlap never
133
+ implies membership. A node belongs to at most one swimlane. A phase may cut
134
+ across swimlanes.
135
+
136
+ ## Interactions are intents, not code
137
+
138
+ ```json
139
+ { "action": "command", "command": "com.example.lab:open-review", "label": "Open review",
140
+ "parameters": { "queue": "peer" } }
141
+ ```
142
+
143
+ `action` is one of the following:
144
+
145
+ - `none`, `select`, `inspect`.
146
+ - `navigate` or `open`, each with a `target` naming exactly one destination: `workflow`, `node`, `edge`, `group`, `reference` or `url`.
147
+ - `command`, which needs a namespaced `command` and a `label`.
148
+
149
+ A renderer or editor reports intents to the host application. The host decides
150
+ what they do. Nothing in a workflow file ever executes.
151
+
152
+ ## Semantics versus layout
153
+
154
+ Everything about meaning is outside `layout`. Remove every `layout` object and
155
+ the workflow means the same thing. Layout operations only write `layout` fields.
156
+
157
+ - **Default layout** (`Layout.resolve`) is deterministic. Ranks come from the
158
+ graph (longest path, with cycles broken in document order). Swimlanes, groups
159
+ and containers become nested bands, so members stay inside their boundary.
160
+ Authored positions are kept.
161
+ - **Explicit layout** (`Layout.apply FillMissing` or `Layout.apply Recompute`,
162
+ or `forma-workflow layout [--recompute]`) writes positions into `layout`
163
+ fields. Tests prove it never changes ids, semantics, metadata, references or
164
+ extensions.
165
+
166
+ ## Extensions and metadata
167
+
168
+ - `metadata` is the application's own descriptive data. It can be any JSON
169
+ object. Numbers keep their spelling (`1.50` stays `1.50`), and key order is
170
+ kept.
171
+ - `extensions` are keyed by a reverse-DNS namespace (`com.example.erp`). Each
172
+ value is a JSON object. The `forma` and `forma.*` namespaces are reserved.
173
+ Unknown extensions survive load, edit and save unchanged.
174
+ - Interoperable meaning (kinds, status, colour, references, membership) is
175
+ always in core fields. Never put it only in an extension.
176
+
177
+ ## Canonical serialization
178
+
179
+ `Codec.serialize` and `forma-workflow normalize` write a fixed property order,
180
+ two-space indentation and a final newline. Empty optional arrays are omitted.
181
+ Canonical output is a fixed point: serializing a loaded canonical file gives the
182
+ same bytes.
183
+
184
+ ## Versioning
185
+
186
+ `formatVersion` follows semantic versioning. This library reads `1.0.0`.
187
+
188
+ - A pre-release of the current version (`1.0.0-rc.1`) migrates automatically. It is reported as *migration required, automatic*.
189
+ - Any other version is reported as *migration required* with the reason. The document is not decoded.
190
+
191
+ The Forma release a workflow needs is declared separately in `forma.version`
192
+ and `forma.requires`. It is checked against
193
+ [`contracts/workflow-capabilities.json`](../../contracts/workflow-capabilities.json).
194
+
195
+ See also [VALIDATION.md](VALIDATION.md), [RENDERING.md](RENDERING.md),
196
+ [EMBEDDING.md](EMBEDDING.md) and [EXTERNAL-PRODUCERS.md](EXTERNAL-PRODUCERS.md).
@@ -0,0 +1,90 @@
1
+ # Rendering workflows to HTML
2
+
3
+ Forma renders a workflow to standards-based HTML with its public diagram
4
+ contract (`.ef-diagram*`, contract 2.1.0). Static output is HTML and CSS only.
5
+ Forma Studio is never involved.
6
+
7
+ ## Fragment
8
+
9
+ ```bash
10
+ forma-workflow render forma/workflows/launch.forma-workflow.json --out launch.html --deps launch.deps.json
11
+ ```
12
+
13
+ The fragment starts with a comment that declares what it needs:
14
+
15
+ ```html
16
+ <!-- Requires @echelon-foundry/design-system@0.4.0/tokens.css, @echelon-foundry/design-system@0.4.0/foundations.css, @echelon-foundry/design-system@0.4.0/components.css -->
17
+ <figure class="ef-diagram" id="wf-static-export" aria-labelledby="wf-static-export-title">
18
+ <figcaption id="wf-static-export-title">Static export reference</figcaption>
19
+ <div class="ef-diagram__viewport" tabindex="0" role="group" aria-label="Static export reference diagram, scroll to see all items">
20
+ <div class="ef-diagram__canvas" style="--ef-diagram-canvas-w: 792px; --ef-diagram-canvas-h: 136px;">
21
+ <svg class="ef-diagram__wires" viewBox="0 0 792 136" aria-hidden="true" focusable="false">…</svg>
22
+ <article class="ef-diagram-node" id="wf-static-export-n-a" data-ef-shape="pill" aria-labelledby="wf-static-export-n-a-label" style="…">
23
+ <p class="ef-diagram-node__kind">Start</p>
24
+ <h3 class="ef-diagram-node__label" id="wf-static-export-n-a-label">Request</h3>
25
+ </article>
26
+ …
27
+ </div>
28
+ </div>
29
+ <ol class="ef-diagram__relations" aria-label="Relationships">
30
+ <li>Request leads to Review.</li>
31
+ <li>Review leads to Done: “Approved”.</li>
32
+ </ol>
33
+ </figure>
34
+ ```
35
+
36
+ The complete reference outputs are in
37
+ [`examples/workflows/exports/`](../../examples/workflows/exports/). Tests
38
+ check them byte for byte.
39
+
40
+ ## Complete document
41
+
42
+ ```bash
43
+ forma-workflow render launch.forma-workflow.json --document --forma-base /assets/forma/ --brand example-harbor --out launch.html
44
+ ```
45
+
46
+ The document contains a doctype, `lang` and `dir`, charset, viewport, title,
47
+ description and generator metadata. It links exactly the declared stylesheets,
48
+ applies an optional public brand (`data-ef-brand`) and theme, and puts the
49
+ figure in `<main>`.
50
+
51
+ ## Interactive output
52
+
53
+ ```bash
54
+ forma-workflow render launch.forma-workflow.json --document --interactive view \
55
+ --runtime-base /assets/forma-workflow/ --out launch.html
56
+ ```
57
+
58
+ This wraps the same static figure in the public `<forma-workflow>` element. It
59
+ adds the workflow source as an inert `<script type="application/json">` data
60
+ block and one module script from `@echelon-foundry/forma-workflow`. The
61
+ dependency list names that runtime. Without script, the static figure still
62
+ renders. Nothing private to Studio is ever emitted.
63
+
64
+ ## What the output guarantees
65
+
66
+ Each guarantee is tested in `tests/workflow` and `tests/browser/workflow-static.spec.mjs`.
67
+
68
+ - **Deterministic.** The same workflow and options give byte-identical HTML.
69
+ - **Public contracts only.** Every class is a public Forma class that exists in Forma CSS. There are no Studio or editor classes and no editor-only ids.
70
+ - **No script unless asked.** Static output has no `<script>`, no event-handler attributes and no `javascript:` URLs.
71
+ - **Safe.** Every label, description, metadata value, reference and extension value is escaped. Unsafe URLs are dropped even if a host skips validation. The JSON data block cannot close its `<script>` element.
72
+ - **Accessible.** The figure is named by the title. Each node is named by its label (or its accessible name). Kind and status are text. Every connector has a sentence in `ol.ef-diagram__relations`, so relationships never depend on lines alone. Groups and lanes list their members in `dl.ef-diagram__membership`. A legend explains colour. Reading order follows the flow, not the paint order.
73
+ - **Responsive.** There is no page-level overflow at 320px. The canvas scrolls inside its focusable, named viewport. The relationship and membership lists reflow.
74
+ - **Forced colours, print and grayscale.** These follow the diagram contract fallbacks.
75
+
76
+ ## Metadata in output
77
+
78
+ Metadata stays in the workflow file. It is not rendered unless the host names
79
+ fields to show (`--metadata owner,requirements` or
80
+ `RenderOptions.VisibleMetadata`). This follows FMD-META-006/007 and the
81
+ disclosure rules.
82
+
83
+ ## .NET
84
+
85
+ ```fsharp
86
+ let w = (Validation.load text).Workflow.Value
87
+ let fragment = WorkflowDocument.fragment DocumentOptions.defaults w
88
+ let page = WorkflowDocument.document { DocumentOptions.defaults with FormaBase = "/assets/forma/"; Brand = Some "echelon" } w
89
+ // fragment.Html, page.Html, page.Dependencies
90
+ ```
@@ -0,0 +1,55 @@
1
+ # Requirements-to-evidence audit: portable workflows, embedding and HTML export
2
+
3
+ Date: 2026-10-01. Scope: kemiller2002/forma PR #95 (FORMA-GH-93, FORMA-GH-94)
4
+ and kemiller2002/forma-studio PR #17 (STUDIO-GH-14, STUDIO-GH-15).
5
+
6
+ **Paths.** "Forma" paths are in kemiller2002/forma; "Studio" paths are in
7
+ kemiller2002/forma-studio.
8
+
9
+ **Test counts** are local results from Chromium 1194. Forma has 58 F# workflow
10
+ tests and 25 workflow browser tests. Studio has 68 engine tests and 46 browser
11
+ tests. CI also runs Firefox and WebKit for Forma.
12
+
13
+ | # | Requirement | Implementation | Evidence |
14
+ |---|---|---|---|
15
+ | 3 | Forma owns the format: `.forma-workflow.json`, `forma/workflows/<id>`, versioned schema at a stable package path; all listed top-level, node and edge fields | Forma: `schemas/workflow/1.0/forma-workflow.schema.json`; package export `./workflow/forma-workflow.schema.json`; `src/workflow/Forma.Workflow/Model.fs` | Forma: `CoreTests` (fixture classes, round trip, paths); `tests/workflow-package.test.mjs` |
16
+ | 3 | Unknown metadata and namespaced extensions round-trip | `Codec` keeps metadata as ordered JSON with number spelling preserved | Forma: `CoreTests` "Unknown metadata …", "Namespaced extensions …"; `EditorTests`. Studio: `WorkflowStudioTests` |
17
+ | 4 | Semantics separate from layout; valid without coordinates; deterministic default layout; layout never changes semantics | Forma: `Layout.resolve`, `Layout.apply`, `Layout.strip` | Forma: `CoreTests` layout invariance, determinism, no overlap, lanes contain members |
18
+ | 5 | Validation independent of Studio; six classes; schema, ids, references, ports, groups, extensions, compatibility, static accessibility | Forma: `Validation.fs`, `JsonSchema.fs` (runs the published schema), `forma-workflow validate` | Forma: `CoreTests`, `AdversarialTests`, `tests/workflow-external.test.mjs`; `docs/workflow/VALIDATION.md` |
19
+ | 6 | Reusable renderer and editor in Forma: view, inspect, edit, pick and runtime; small versioned host boundary | Forma: `Editor.fs`, `Embed.fs`, `EmbedView.fs`, `EmbedProtocol.fs` (`forma-workflow-host/1`); `packages/workflow` | Forma: `EditorTests`; `tests/browser/workflow-embed.spec.mjs` |
20
+ | 7 | Embedding without an iframe; optional iframe with a versioned message contract; styling, brand, accessibility, keyboard, responsive, touch and reduced motion preserved; example outside Studio | Forma: `<forma-workflow>`, `frame.html`/`frame.js`, `examples/workflow-embedding` | Forma: `workflow-embed.spec.mjs` (no iframe, isolated mode, keyboard, pointer, touch at 390px, axe, forced colours and reduced motion, runtime, pick, intents) |
21
+ | 8 | Studio workflow editor uses the public component (dogfooding); selection, multi-selection, drag, move, resize, nodes, connections, ports, groups, lanes, metadata, colour, states, references, interactions, zoom, pan, keyboard, touch, validation, responsive | Studio: `src/kernel/workflows.js`, `StudioWorkflowInterop`, `WorkflowLibrary`; the component's capabilities | Studio: `tests/browser/workflow.spec.mjs`. Forma: `workflow-embed.spec.mjs` |
22
+ | 9 | Metadata is first-class, editable, and preserved when unknown | Inspector metadata editor (typed); extensions displayed and preserved | Forma: `EditorTests` metadata test, embed browser metadata test. Studio: lifecycle test |
23
+ | 10 | HTML fragment and complete document for a page, composition, component hierarchy or workflow; no Studio runtime, plugin or format | Studio: `HtmlExport.page`, `componentTree`, `workflow`; export panel; CLI `html`/`workflow-html` | Studio: `WorkflowStudioTests` export tests; `html-consumer.spec.mjs` |
24
+ | 11 | Static stays static; runtime only from the public contract, declared | Static by default; opt-in interactive output declares `@echelon-foundry/forma-workflow` | Studio: interactive export tests. Forma: `RenderTests` "Static output has no script", interactive golden |
25
+ | 12 | Maintainable, deterministic, public-contract HTML | Forma `Markup` tree; component-to-pattern map | Forma: `RenderTests` (public classes, determinism, goldens). Studio: `npm run html:check` |
26
+ | 13 | Workflow fragment and document, static and interactive, with an accessible relationship representation | Forma: `Render.figure`, `WorkflowDocument`; `ol.ef-diagram__relations`, `dl.ef-diagram__membership` | Forma: `RenderTests`, `workflow-static.spec.mjs` (axe, 320px, forced colours, brand) |
27
+ | 14 | Security: data only; escaping; no execution; adversarial tests | Typed intents; escaping Markup; URL policy; JSON depth and size limits; duplicate-key rejection | Forma: `AdversarialTests`, `RenderTests` escaping and data-block tests, `EditorTests` escaping. Studio: export escaping test |
28
+ | 15 | 17 reference fixtures as executable compatibility fixtures | Forma: `examples/workflows/forma/workflows` (19) and `fixtures.json` | Forma: `CoreTests` (every fixture validated with its expected class; required list) |
29
+ | 16 | External producer → validation → Studio open, edit, save → external consumer | Forma: `examples/external-producer`. Studio: the round-trip test | Studio: `docs/evidence/external-workflow-roundtrip.md` (39 preserved, 0 lost) |
30
+ | 17 | Studio design → HTML → ordinary web project → render, for application, responsive, workflow and branded compositions, with accessibility verified | Studio: `examples/html-consumer` | Studio: `html-consumer.spec.mjs`; `docs/evidence/html-consumer.md` |
31
+ | 18 | Forma gaps fixed in Forma, not Studio | Diagram contract 2.1.0 (ports, status, description, membership); `.ef-actions`; description contrast on surfaces | Forma: `tests/diagram-contract.test.mjs`, catalog check, commits on PR #95 |
32
+ | 19 | F#, explicit state, minimal JavaScript, Limen where appropriate | All decisions in F#. The element and adapters are plumbing. Limen is page-scoped, so it is not used for the embeddable island (ADR-0006) | `tests/workflow-package.test.mjs` (no decision code in the element); Studio: `limen verify --strict` |
33
+ | 20 | Unit, schema, integration, accessibility, responsive, serialization, round-trip, rendering, embedding and export tests | As listed above | As listed above |
34
+ | 21 | Documentation | Forma: `docs/workflow/*` (format, validation, rendering, embedding, producers), README, agent usage, diagram catalog example. Studio: `docs/WORKFLOWS.md`, `docs/HTML-EXPORT.md`, README | |
35
+ | 22 | Package surface: small, and usable without Studio | Forma core exports the schema, contract and examples; `@echelon-foundry/forma-workflow` exports the element, CSS, frame, schema and engine | Forma: `tests/workflow-package.test.mjs`, `tests/zero-runtime.test.mjs` |
36
+ | 23 | CI and publishing | Forma: `workflow-validation.yml`; browser jobs build the package; the publish job publishes the workflow package. Studio: vendor check, export determinism, browser suite | CI on PR #95 and PR #17 |
37
+ | 24 | Durable checkpoints | ROS work items FORMA-GH-93/94 and STUDIO-GH-14/15; HANDOFF and CURRENT-STATE entries | `./ros validate` (PR mode) passes in both repositories |
38
+
39
+ ## Open items (not hidden)
40
+
41
+ 1. **Flow convergence (forma-studio#16).** Studio's existing Flow surface, with
42
+ its profiles, named styles, mappings and merge, is not yet migrated onto the
43
+ portable format. Portable workflow authoring in Studio already uses the
44
+ public component. The broader Flow requirements in Studio's
45
+ `requirements/16` remain the Flow surface's backlog.
46
+ 2. **Release pinning.** Studio vendors Forma from PR #95 with a hash manifest.
47
+ The pinned npm artifacts become available when PR #95 merges and the
48
+ publish workflow releases Forma 0.4.0 and `@echelon-foundry/forma-workflow`
49
+ 1.0.0.
50
+ 3. **Cross-engine browser runs.** Firefox and WebKit run in Forma CI. They were
51
+ not available in the implementing environment. Studio CI is Chromium only,
52
+ which matches its existing configuration.
53
+ 4. **Site pages.** The Forma site shows generated workflow markup in the
54
+ diagram catalog. The format, embedding and validation guides are repository
55
+ documentation (`docs/workflow/`), not separate generated site pages.