diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4fe7905..d7414ea 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,7 @@
## 0.2.0 — unreleased
- Process Contract Schema v0.2 with typed transitions, I/O contracts, SLA/KPI, risk, evidence and agent policy metadata.
+- Process Analysis Canvas with observed timing, ownership, variants, evidence-backed pain points, data flows and a standalone HTML renderer.
- v0.1 migration command.
- BPMN supported-subset import and richer export with lanes and parallel gateways.
- Process governance policy gates.
diff --git a/README.md b/README.md
index 0649ee2..14c62be 100644
--- a/README.md
+++ b/README.md
@@ -70,6 +70,7 @@ process-code schema -o process.schema.json
```bash
process-code validate examples/customer-creation.process.yaml --strict
process-code mermaid examples/customer-creation.process.yaml
+process-code canvas examples/process-analysis/order-entry.process.yaml -o order-entry-analysis.html
process-code bpmn examples/customer-creation.process.yaml -o customer.bpmn
process-code diff examples/customer-creation.process.yaml examples/changes/customer-creation-v2.process.yaml
process-code impact examples/customer-creation.process.yaml examples/changes/customer-creation-v2.process.yaml
@@ -143,6 +144,7 @@ The source-tree schema is [`schemas/process.schema.json`](schemas/process.schema
| Contract validation | `process-code validate` |
| v0.1 -> v0.2 migration | `process-code migrate` |
| Mermaid | `process-code mermaid` |
+| Process Analysis Canvas | `process-code canvas` |
| BPMN 2.0 export | `process-code bpmn` |
| BPMN supported-subset import | `process-code bpmn-import` |
| Markdown documentation | `process-code docs` |
@@ -163,6 +165,44 @@ The source-tree schema is [`schemas/process.schema.json`](schemas/process.schema
| AI drafting context | `process-code draft-context` |
| Vendor adapter discovery/import | `process-code adapter-list` / `adapter-import` |
+## Process Analysis Canvas
+
+Process discovery needs more than a flowchart. A useful assessment view must show the real execution details that disappear from high-level BPMN: who performs each step, which system is used, what goes in and out, how much active and waiting time is observed, what variants exist, where evidence-backed pain points occur, how data moves, and who can change the step.
+
+The optional analysis fields keep those facts in the same governed process contract:
+
+```yaml
+steps:
+ - id: save_draft
+ name: Save order as draft
+ actor: sales_ops
+ system: s4
+ inputs: [{name: Entered order data}]
+ outputs: [{name: Draft sales order}]
+ timing: {touch_time: PT1M, wait_time: PT0M}
+ ownership:
+ process_owner: o2c_process_owner
+ change_owner: sap_sd_team
+
+analysis:
+ pain_points:
+ - id: slow_customer_search
+ step: check_customer
+ observation: Customer search takes 20 minutes
+ cause: Search help cannot filter the observed list by creation date
+ evidence: [observed_session_01]
+ impact: {duration: PT20M}
+```
+
+Generate the one-page HTML artifact with:
+
+```bash
+process-code canvas examples/process-analysis/order-entry.process.yaml \
+ -o order-entry-analysis.html
+```
+
+The complete evidence-based example is [`examples/process-analysis/order-entry.process.yaml`](examples/process-analysis/order-entry.process.yaml).
+
## Pull-request impact gate
The repository contains a reusable composite GitHub Action. In a consumer repository:
diff --git a/docs/specification.md b/docs/specification.md
index 36f896e..ca2e9f8 100644
--- a/docs/specification.md
+++ b/docs/specification.md
@@ -23,7 +23,7 @@ The core catalog sections are `roles`, `systems`, `objects`, `interfaces`, `cont
Supported `type` values are `task`, `user_task`, `service_task`, `decision`, `parallel`, `event`, `end`, and `subprocess`.
-A step may include `actor`, `system`, referenced catalog IDs, `inputs`, `outputs`, `sla`, `kpis`, `raci`, `agent`, and `transitions`.
+A step may include `actor`, `system`, referenced catalog IDs, `inputs`, `outputs`, `timing`, `ownership`, `sla`, `kpis`, `raci`, `agent`, and `transitions`.
## Transitions
@@ -62,6 +62,38 @@ Liveness findings are deterministic model-quality diagnostics. They do not claim
Each contract can declare `id`, `name`, `type`, `ref`, and `required`. At least one of `id`, `name`, or `ref` must exist.
+## Process analysis fields
+
+Process discovery and process design are different concerns. The contract therefore separates observed execution from targets:
+
+- `timing.touch_time`: active work time observed or measured for the step.
+- `timing.wait_time`: time spent waiting before the step can continue.
+- `timing.elapsed_time`: end-to-end time for the step when it is known.
+- `sla.duration`: target or service commitment. It is not a substitute for observed duration.
+- `ownership.process_owner`: role accountable for the business behavior of the step.
+- `ownership.change_owner`: role or team allowed to change the step, configuration, or supporting solution.
+
+The optional top-level `analysis` object records facts that belong to the process analysis rather than to one step:
+
+- `analysis.variants`: conditions that create a different path or handling rule.
+- `analysis.pain_points`: evidence-backed observations, causes, impacts, and affected steps.
+- `analysis.data_flows`: system-to-system data movement and field-level source-of-truth ownership.
+
+A recommended terminology is:
+
+- **Process Discovery**: the activity of observing the real process.
+- **Process Analysis Canvas**: the one-page visual summary generated from the contract.
+- **Process Analysis Note**: the detailed evidence-backed analysis document.
+- **Process as Code**: the machine-readable source behind those views.
+
+The reference renderer exposes the one-page view with:
+
+```bash
+process-code canvas process.yaml -o process-analysis.html
+```
+
+The canvas is intentionally not a BPMN replacement. BPMN focuses on process flow semantics. The analysis canvas focuses on how the process actually operates: actor, system, input, output, observed time, variants, pain points, data movement, and ownership.
+
## Risk, control and evidence
Risks are catalog entities and may include a severity such as `low`, `medium`, `high`, or `critical`. Policy gates can require controls and evidence for high-risk steps.
diff --git a/examples/process-analysis/order-entry.process.yaml b/examples/process-analysis/order-entry.process.yaml
new file mode 100644
index 0000000..caff02c
--- /dev/null
+++ b/examples/process-analysis/order-entry.process.yaml
@@ -0,0 +1,159 @@
+version: "0.2"
+process:
+ id: sales_order_entry_analysis
+ name: Sales Order Entry — Process Analysis
+ owner: o2c_process_owner
+ start: receive_po
+ trigger: Customer PO arrives in the shared sales mailbox
+ outcome: Sales order is released for fulfillment
+ tags: [sap, s4hana, discovery, process-analysis]
+
+roles:
+ - {id: sales_ops, name: Sales Operations}
+ - {id: sales_manager, name: Sales Manager}
+ - {id: o2c_process_owner, name: O2C Process Owner}
+ - {id: sap_sd_team, name: SAP SD Team}
+
+systems:
+ - {id: outlook, name: Outlook}
+ - {id: s4, name: SAP S/4HANA}
+
+evidence:
+ - {id: observed_session_01, name: Observed user session 01}
+ - {id: search_screen_01, name: Customer search screenshot}
+
+steps:
+ - id: receive_po
+ name: Open customer PO
+ type: user_task
+ actor: sales_ops
+ system: outlook
+ inputs: [{name: Customer PO, type: document, required: true}]
+ outputs: [{name: Order request, type: work_item}]
+ timing: {touch_time: PT2M, wait_time: PT15M, elapsed_time: PT17M}
+ ownership: {process_owner: o2c_process_owner, change_owner: sales_manager}
+ transitions: [{to: check_customer}]
+
+ - id: check_customer
+ name: Search and verify customer
+ type: user_task
+ actor: sales_ops
+ system: s4
+ inputs: [{name: Customer name and address, type: data, required: true}]
+ outputs: [{name: Confirmed customer account, type: data}]
+ timing: {touch_time: PT20M, wait_time: PT0M, elapsed_time: PT20M}
+ ownership: {process_owner: o2c_process_owner, change_owner: sap_sd_team}
+ evidence: [observed_session_01, search_screen_01]
+ transitions: [{to: enter_order}]
+
+ - id: enter_order
+ name: Enter sales order data
+ type: user_task
+ actor: sales_ops
+ system: s4
+ inputs:
+ - {name: Customer PO, type: document, required: true}
+ - {name: Confirmed customer account, type: data, required: true}
+ outputs: [{name: Entered order data, type: data}]
+ timing: {touch_time: PT8M, wait_time: PT0M, elapsed_time: PT8M}
+ ownership: {process_owner: o2c_process_owner, change_owner: sap_sd_team}
+ transitions: [{to: save_draft}]
+
+ - id: save_draft
+ name: Save order as draft
+ type: user_task
+ actor: sales_ops
+ system: s4
+ inputs: [{name: Entered order data, type: data, required: true}]
+ outputs: [{name: Draft sales order, type: business_object}]
+ timing: {touch_time: PT1M, wait_time: PT0M, elapsed_time: PT1M}
+ ownership: {process_owner: o2c_process_owner, change_owner: sap_sd_team}
+ transitions: [{to: approval_required}]
+
+ - id: approval_required
+ name: Is manager approval required?
+ type: decision
+ actor: sales_ops
+ system: s4
+ inputs: [{name: Draft sales order, type: business_object, required: true}]
+ outputs: [{name: Approval route decision, type: decision}]
+ timing: {touch_time: PT1M, wait_time: PT0M, elapsed_time: PT1M}
+ ownership: {process_owner: o2c_process_owner, change_owner: sales_manager}
+ transitions:
+ - {to: release_order, when: amount <= 50000, label: no approval}
+ - {to: send_approval_request, when: amount > 50000, label: approval required}
+
+ - id: send_approval_request
+ name: Send approval request
+ type: user_task
+ actor: sales_ops
+ system: outlook
+ inputs: [{name: Draft sales order, type: business_object, required: true}]
+ outputs: [{name: Approval email, type: message}]
+ timing: {touch_time: PT2M, wait_time: PT0M, elapsed_time: PT2M}
+ ownership: {process_owner: o2c_process_owner, change_owner: sales_manager}
+ transitions: [{to: check_approval_email}]
+
+ - id: check_approval_email
+ name: Check approval email
+ type: user_task
+ actor: sales_ops
+ system: outlook
+ inputs: [{name: Approval email, type: message, required: true}]
+ outputs: [{name: Approval decision, type: decision}]
+ timing: {touch_time: PT1M, wait_time: PT45M, elapsed_time: PT46M}
+ ownership: {process_owner: o2c_process_owner, change_owner: sales_manager}
+ transitions: [{to: release_order}]
+
+ - id: release_order
+ name: Release sales order
+ type: user_task
+ actor: sales_ops
+ system: s4
+ inputs: [{name: Draft sales order, type: business_object, required: true}]
+ outputs: [{name: Released sales order, type: business_object}]
+ timing: {touch_time: PT2M, wait_time: PT0M, elapsed_time: PT2M}
+ sla: {duration: PT1H}
+ ownership: {process_owner: o2c_process_owner, change_owner: sap_sd_team}
+ transitions: [{to: complete}]
+
+ - id: complete
+ name: Sales order released
+ type: end
+
+analysis:
+ variants:
+ - id: high_value_order
+ name: High-value order
+ when: amount > 50000
+ path: [send_approval_request, check_approval_email, release_order]
+ notes: Manager approval is required before release.
+ - id: new_customer
+ name: New customer
+ when: customer_account_not_found
+ notes: Customer creation is handled by a separate master-data process before order entry resumes.
+
+ pain_points:
+ - id: slow_customer_search
+ step: check_customer
+ observation: Customer search takes 20 minutes
+ cause: Search help cannot filter the observed list by creation date
+ evidence: [observed_session_01, search_screen_01]
+ impact: {duration: PT20M}
+ - id: approval_email_wait
+ step: check_approval_email
+ observation: Approval waits in email for 45 minutes on the observed case
+ cause: The approval request has no workflow notification or queue
+ evidence: [observed_session_01]
+ impact: {wait_time: PT45M}
+
+ data_flows:
+ - id: customer_po_to_s4
+ from: outlook
+ to: s4
+ data: [customer_po_number, requested_date, material, quantity]
+ source_of_truth:
+ customer_po_number: customer_po
+ requested_date: customer_po
+ material: s4
+ quantity: customer_po
diff --git a/schemas/process.schema.json b/schemas/process.schema.json
index e6099bd..b4a1f47 100644
--- a/schemas/process.schema.json
+++ b/schemas/process.schema.json
@@ -9,6 +9,7 @@
"process": {"type": "object", "required": ["id", "name"], "properties": {"id": {"type": "string", "minLength": 1}, "name": {"type": "string", "minLength": 1}, "description": {"type": "string"}, "owner": {"type": "string"}, "start": {"type": "string"}, "trigger": {"type": "string"}, "outcome": {"type": "string"}, "tags": {"type": "array", "items": {"type": "string"}}}, "additionalProperties": true},
"roles": {"$ref": "#/$defs/catalog"}, "systems": {"$ref": "#/$defs/catalog"}, "objects": {"$ref": "#/$defs/catalog"}, "interfaces": {"$ref": "#/$defs/catalog"}, "controls": {"$ref": "#/$defs/catalog"}, "risks": {"$ref": "#/$defs/catalog"}, "evidence": {"$ref": "#/$defs/catalog"},
"artifacts": {"type": "array", "items": {"type": "object", "required": ["id", "uri"], "properties": {"id": {"type": "string"}, "kind": {"type": "string"}, "relation": {"type": "string"}, "uri": {"type": "string"}}, "additionalProperties": true}},
+ "analysis": {"$ref": "#/$defs/analysis"},
"steps": {"type": "array", "minItems": 1, "items": {"$ref": "#/$defs/step"}},
"catalogs": {"type": "array", "items": {"type": "object", "required": ["uri"], "properties": {"uri": {"type": "string"}, "include": {"type": "array", "items": {"type": "string"}}}, "additionalProperties": true}}
},
@@ -16,10 +17,17 @@
"catalog": {"type": "array", "items": {"type": "object", "required": ["id"], "properties": {"id": {"type": "string", "minLength": 1}, "name": {"type": "string"}, "description": {"type": "string"}}, "additionalProperties": true}},
"contract": {"type": "object", "properties": {"id": {"type": "string"}, "name": {"type": "string"}, "type": {"type": "string"}, "ref": {"type": "string"}, "required": {"type": "boolean"}}, "anyOf": [{"required": ["id"]}, {"required": ["name"]}, {"required": ["ref"]}], "additionalProperties": true},
"transition": {"type": "object", "required": ["to"], "properties": {"to": {"type": "string"}, "label": {"type": "string"}, "when": {"type": "string"}, "data": {"type": "object"}}, "additionalProperties": true},
+ "timing": {"type": "object", "properties": {"touch_time": {"type": "string"}, "wait_time": {"type": "string"}, "elapsed_time": {"type": "string"}}, "additionalProperties": true},
+ "ownership": {"type": "object", "properties": {"process_owner": {"type": "string"}, "change_owner": {"type": "string"}}, "additionalProperties": true},
+ "variant": {"type": "object", "required": ["id"], "properties": {"id": {"type": "string", "minLength": 1}, "name": {"type": "string"}, "when": {"type": "string"}, "condition": {"type": "string"}, "path": {"type": "array", "items": {"type": "string"}}, "notes": {"type": "string"}}, "additionalProperties": true},
+ "pain_point": {"type": "object", "required": ["id", "observation"], "properties": {"id": {"type": "string", "minLength": 1}, "name": {"type": "string"}, "step": {"type": "string"}, "observation": {"type": "string", "minLength": 1}, "cause": {"type": "string"}, "evidence": {"type": "array", "items": {"type": "string"}}, "impact": {}}, "additionalProperties": true},
+ "data_flow": {"type": "object", "required": ["id", "from", "to"], "properties": {"id": {"type": "string", "minLength": 1}, "from": {"type": "string"}, "to": {"type": "string"}, "data": {"type": "array", "items": {"type": "string"}}, "source_of_truth": {"type": "object", "additionalProperties": {"type": "string"}}}, "additionalProperties": true},
+ "analysis": {"type": "object", "properties": {"variants": {"type": "array", "items": {"$ref": "#/$defs/variant"}}, "pain_points": {"type": "array", "items": {"$ref": "#/$defs/pain_point"}}, "data_flows": {"type": "array", "items": {"$ref": "#/$defs/data_flow"}}}, "additionalProperties": true},
"step": {"type": "object", "required": ["id", "name"], "properties": {
"id": {"type": "string", "minLength": 1}, "name": {"type": "string", "minLength": 1}, "type": {"enum": ["task", "user_task", "service_task", "decision", "parallel", "event", "end", "subprocess"]}, "actor": {"type": "string"}, "system": {"type": "string"},
"objects": {"type": "array", "items": {"type": "string"}}, "interfaces": {"type": "array", "items": {"type": "string"}}, "controls": {"type": "array", "items": {"type": "string"}}, "risks": {"type": "array", "items": {"type": "string"}}, "evidence": {"type": "array", "items": {"type": "string"}}, "artifacts": {"type": "array", "items": {"type": "string"}},
- "inputs": {"type": "array", "items": {"$ref": "#/$defs/contract"}}, "outputs": {"type": "array", "items": {"$ref": "#/$defs/contract"}}, "transitions": {"type": "array", "items": {"$ref": "#/$defs/transition"}},
+ "inputs": {"type": "array", "items": {"$ref": "#/$defs/contract"}}, "outputs": {"type": "array", "items": {"$ref": "#/$defs/contract"}},
+ "timing": {"$ref": "#/$defs/timing"}, "ownership": {"$ref": "#/$defs/ownership"}, "transitions": {"type": "array", "items": {"$ref": "#/$defs/transition"}},
"sla": {"type": "object", "properties": {"duration": {"type": "string"}, "target": {}, "business_hours": {"type": "boolean"}}, "additionalProperties": true},
"kpis": {"type": "array", "items": {"type": "object", "required": ["id"], "properties": {"id": {"type": "string"}, "name": {"type": "string"}, "target": {}, "unit": {"type": "string"}}, "additionalProperties": true}},
"agent": {"type": "object", "properties": {"exposable": {"type": "boolean"}, "executable": {"type": "boolean"}, "allowed_actions": {"type": "array", "items": {"type": "string"}}}, "additionalProperties": true},
diff --git a/src/process_as_code/cli.py b/src/process_as_code/cli.py
index b7bf9b3..6329352 100644
--- a/src/process_as_code/cli.py
+++ b/src/process_as_code/cli.py
@@ -20,7 +20,7 @@
from .process_tests import affected_process_tests, run_process_tests
from .raci import extract_raci, raci_markdown
from .refs import resolve_artifacts
-from .render import to_markdown, to_mermaid
+from .render import to_analysis_canvas, to_markdown, to_mermaid
from .testgen import generate_test_scope, test_scope_markdown
from .validate import validate_process
@@ -42,6 +42,7 @@ def build_parser() -> argparse.ArgumentParser:
for name, help_text in (
("mermaid", "render Mermaid flowchart"),
+ ("canvas", "render one-page Process Analysis Canvas HTML"),
("bpmn", "export BPMN 2.0 XML"),
("docs", "generate Markdown documentation"),
("raci", "extract RACI table"),
@@ -278,6 +279,8 @@ def main(argv: list[str] | None = None) -> int:
return 1
if args.command == "mermaid":
text = to_mermaid(data)
+ elif args.command == "canvas":
+ text = to_analysis_canvas(data)
elif args.command == "bpmn":
text = to_bpmn(data)
elif args.command == "docs":
diff --git a/src/process_as_code/render.py b/src/process_as_code/render.py
index 0d74baa..a9fd8d7 100644
--- a/src/process_as_code/render.py
+++ b/src/process_as_code/render.py
@@ -1,5 +1,6 @@
from __future__ import annotations
+from html import escape
from typing import Any
from .graph import step_edges
@@ -9,6 +10,39 @@ def _escape(text: str) -> str:
return text.replace('"', "'")
+def _html(value: Any, fallback: str = "—") -> str:
+ if value is None or value == "":
+ return fallback
+ return escape(str(value))
+
+
+def _catalog_names(data: dict[str, Any], section: str) -> dict[str, str]:
+ result: dict[str, str] = {}
+ for item in data.get(section, []) or []:
+ if isinstance(item, dict) and item.get("id"):
+ result[str(item["id"])] = str(item.get("name") or item["id"])
+ return result
+
+
+def _contract_list(items: Any) -> str:
+ labels: list[str] = []
+ for item in items or []:
+ if isinstance(item, dict):
+ labels.append(str(item.get("name") or item.get("id") or item.get("ref") or ""))
+ elif item:
+ labels.append(str(item))
+ return ", ".join(label for label in labels if label) or "—"
+
+
+def _compact_value(value: Any) -> str:
+ if isinstance(value, dict):
+ parts = [f"{key}: {val}" for key, val in value.items() if val not in (None, "", [], {})]
+ return "; ".join(parts) or "—"
+ if isinstance(value, list):
+ return ", ".join(str(item) for item in value) or "—"
+ return str(value) if value not in (None, "") else "—"
+
+
def to_mermaid(data: dict[str, Any]) -> str:
lines = ["flowchart TD"]
steps = [s for s in data.get("steps", []) if isinstance(s, dict) and s.get("id")]
@@ -31,6 +65,204 @@ def to_mermaid(data: dict[str, Any]) -> str:
return "\n".join(lines) + "\n"
+def to_analysis_canvas(data: dict[str, Any]) -> str:
+ """Render a compact, evidence-oriented one-page process analysis view."""
+ meta = data.get("process", {}) or {}
+ analysis = data.get("analysis", {}) or {}
+ steps = [step for step in data.get("steps", []) if isinstance(step, dict)]
+ role_names = _catalog_names(data, "roles")
+ system_names = _catalog_names(data, "systems")
+ system_count = len({step.get("system") for step in steps if step.get("system")})
+ variants = analysis.get("variants", []) or []
+ pain_points = analysis.get("pain_points", []) or []
+ data_flows = analysis.get("data_flows", []) or []
+
+ rows: list[str] = []
+ ownership_rows: list[str] = []
+ for index, step in enumerate(steps, 1):
+ timing = step.get("timing") if isinstance(step.get("timing"), dict) else {}
+ ownership = step.get("ownership") if isinstance(step.get("ownership"), dict) else {}
+ sla = step.get("sla") if isinstance(step.get("sla"), dict) else {}
+ actor = role_names.get(str(step.get("actor")), str(step.get("actor") or "—"))
+ system = system_names.get(str(step.get("system")), str(step.get("system") or "—"))
+ rows.append(
+ "
"
+ f"| {index:02d} | "
+ f"{_html(step.get('name'))}{_html(step.get('id'), '')} | "
+ f"{_html(actor)} | "
+ f"{_html(system)} | "
+ f"{_html(_contract_list(step.get('inputs')))} | "
+ f"{_html(_contract_list(step.get('outputs')))} | "
+ f"{_html(timing.get('touch_time'))} | "
+ f"{_html(timing.get('wait_time'))} | "
+ f"{_html(timing.get('elapsed_time'))} | "
+ f"{_html(sla.get('duration'))} | "
+ "
"
+ )
+ if ownership:
+ owner = role_names.get(str(ownership.get("process_owner")), str(ownership.get("process_owner") or "—"))
+ changer = role_names.get(str(ownership.get("change_owner")), str(ownership.get("change_owner") or "—"))
+ ownership_rows.append(
+ f"{_html(step.get('name'))}"
+ f"Process: {_html(owner)} · Change: {_html(changer)}
"
+ )
+
+ variant_cards: list[str] = []
+ for item in variants:
+ if not isinstance(item, dict):
+ continue
+ title = item.get("name") or item.get("id") or "Variant"
+ condition = item.get("when") or item.get("condition")
+ path = item.get("path")
+ variant_cards.append(
+ ""
+ f"{_html(title)}"
+ f"{_html(condition)}"
+ f"Path: {_html(_compact_value(path))}"
+ "
"
+ )
+
+ pain_cards: list[str] = []
+ for item in pain_points:
+ if not isinstance(item, dict):
+ continue
+ title = item.get("observation") or item.get("name") or item.get("id") or "Pain point"
+ cause = item.get("cause")
+ impact = item.get("impact")
+ step = item.get("step")
+ evidence = item.get("evidence")
+ detail_parts = []
+ if step:
+ detail_parts.append(f"Step: {step}")
+ if cause:
+ detail_parts.append(f"Cause: {cause}")
+ if evidence:
+ detail_parts.append(f"Evidence: {_compact_value(evidence)}")
+ if impact:
+ detail_parts.append(f"Impact: {_compact_value(impact)}")
+ pain_cards.append(
+ ""
+ f"{_html(title)}"
+ f"{_html(' · '.join(detail_parts))}"
+ "
"
+ )
+
+ flow_cards: list[str] = []
+ for item in data_flows:
+ if not isinstance(item, dict):
+ continue
+ source = system_names.get(str(item.get("from")), str(item.get("from") or "—"))
+ target = system_names.get(str(item.get("to")), str(item.get("to") or "—"))
+ payload = _compact_value(item.get("data"))
+ truth = _compact_value(item.get("source_of_truth"))
+ flow_cards.append(
+ ""
+ f"{_html(source)} → {_html(target)}"
+ f"{_html(payload)}"
+ f"Source of truth: {_html(truth)}"
+ "
"
+ )
+
+ return f"""
+
+
+
+
+{_html(meta.get("name", "Process"))} — Process Analysis Canvas
+
+
+
+
+
+
+
+Actual executionActor · system · input · output · observed time · target
+
+| # | Step | Actor | System | Input | Output | Touch | Wait | Elapsed | Target |
+{"".join(rows)}
+
+
+
+
+
Variants & exceptions
{"".join(variant_cards) or "No variants recorded.
"}
+
Pain points with evidence
{"".join(pain_cards) or "No evidence-backed pain points recorded.
"}
+
Ownership
{"".join(ownership_rows) or "No step-level ownership recorded.
"}
+
+
+
+Data flowMovement and source-of-truth responsibility
+{"".join(flow_cards) or "
No data flows recorded.
"}
+
+
+
+
+
+
+"""
+
+
def to_markdown(data: dict[str, Any]) -> str:
meta = data.get("process", {})
lines = [f'# {meta.get("name", meta.get("id", "Process"))}', ""]
@@ -42,13 +274,20 @@ def to_markdown(data: dict[str, Any]) -> str:
if value:
lines.append(f"| {key} | {value} |")
lines += ["", "## Flow", "", "```mermaid", to_mermaid(data).rstrip(), "```", ""]
- lines += ["## Steps", "", "| # | ID | Step | Actor | System | Type | SLA |", "| ---: | --- | --- | --- | --- | --- | --- |"]
+ lines += ["## Steps", "", "| # | ID | Step | Actor | System | Type | Input | Output | Touch | Wait | Elapsed | SLA |", "| ---: | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |"]
for idx, step in enumerate(data.get("steps", []), 1):
if not isinstance(step, dict):
continue
sla = step.get("sla") or {}
+ timing = step.get("timing") or {}
sla_value = sla.get("duration") if isinstance(sla, dict) else ""
- lines.append(f"| {idx} | `{step.get('id', '')}` | {step.get('name', '')} | {step.get('actor', '')} | {step.get('system', '')} | {step.get('type', 'task')} | {sla_value or ''} |")
+ touch = timing.get("touch_time") if isinstance(timing, dict) else ""
+ wait = timing.get("wait_time") if isinstance(timing, dict) else ""
+ elapsed = timing.get("elapsed_time") if isinstance(timing, dict) else ""
+ lines.append(
+ f"| {idx} | `{step.get('id', '')}` | {step.get('name', '')} | {step.get('actor', '')} | {step.get('system', '')} | {step.get('type', 'task')} | "
+ f"{_contract_list(step.get('inputs'))} | {_contract_list(step.get('outputs'))} | {touch or ''} | {wait or ''} | {elapsed or ''} | {sla_value or ''} |"
+ )
for section, title in (("controls", "Controls"), ("risks", "Risks"), ("evidence", "Evidence"), ("interfaces", "Interfaces"), ("objects", "Business objects"), ("artifacts", "Linked artifacts")):
items = data.get(section, []) or []
if items:
diff --git a/src/process_as_code/resources/process.schema.json b/src/process_as_code/resources/process.schema.json
index e6099bd..b4a1f47 100644
--- a/src/process_as_code/resources/process.schema.json
+++ b/src/process_as_code/resources/process.schema.json
@@ -9,6 +9,7 @@
"process": {"type": "object", "required": ["id", "name"], "properties": {"id": {"type": "string", "minLength": 1}, "name": {"type": "string", "minLength": 1}, "description": {"type": "string"}, "owner": {"type": "string"}, "start": {"type": "string"}, "trigger": {"type": "string"}, "outcome": {"type": "string"}, "tags": {"type": "array", "items": {"type": "string"}}}, "additionalProperties": true},
"roles": {"$ref": "#/$defs/catalog"}, "systems": {"$ref": "#/$defs/catalog"}, "objects": {"$ref": "#/$defs/catalog"}, "interfaces": {"$ref": "#/$defs/catalog"}, "controls": {"$ref": "#/$defs/catalog"}, "risks": {"$ref": "#/$defs/catalog"}, "evidence": {"$ref": "#/$defs/catalog"},
"artifacts": {"type": "array", "items": {"type": "object", "required": ["id", "uri"], "properties": {"id": {"type": "string"}, "kind": {"type": "string"}, "relation": {"type": "string"}, "uri": {"type": "string"}}, "additionalProperties": true}},
+ "analysis": {"$ref": "#/$defs/analysis"},
"steps": {"type": "array", "minItems": 1, "items": {"$ref": "#/$defs/step"}},
"catalogs": {"type": "array", "items": {"type": "object", "required": ["uri"], "properties": {"uri": {"type": "string"}, "include": {"type": "array", "items": {"type": "string"}}}, "additionalProperties": true}}
},
@@ -16,10 +17,17 @@
"catalog": {"type": "array", "items": {"type": "object", "required": ["id"], "properties": {"id": {"type": "string", "minLength": 1}, "name": {"type": "string"}, "description": {"type": "string"}}, "additionalProperties": true}},
"contract": {"type": "object", "properties": {"id": {"type": "string"}, "name": {"type": "string"}, "type": {"type": "string"}, "ref": {"type": "string"}, "required": {"type": "boolean"}}, "anyOf": [{"required": ["id"]}, {"required": ["name"]}, {"required": ["ref"]}], "additionalProperties": true},
"transition": {"type": "object", "required": ["to"], "properties": {"to": {"type": "string"}, "label": {"type": "string"}, "when": {"type": "string"}, "data": {"type": "object"}}, "additionalProperties": true},
+ "timing": {"type": "object", "properties": {"touch_time": {"type": "string"}, "wait_time": {"type": "string"}, "elapsed_time": {"type": "string"}}, "additionalProperties": true},
+ "ownership": {"type": "object", "properties": {"process_owner": {"type": "string"}, "change_owner": {"type": "string"}}, "additionalProperties": true},
+ "variant": {"type": "object", "required": ["id"], "properties": {"id": {"type": "string", "minLength": 1}, "name": {"type": "string"}, "when": {"type": "string"}, "condition": {"type": "string"}, "path": {"type": "array", "items": {"type": "string"}}, "notes": {"type": "string"}}, "additionalProperties": true},
+ "pain_point": {"type": "object", "required": ["id", "observation"], "properties": {"id": {"type": "string", "minLength": 1}, "name": {"type": "string"}, "step": {"type": "string"}, "observation": {"type": "string", "minLength": 1}, "cause": {"type": "string"}, "evidence": {"type": "array", "items": {"type": "string"}}, "impact": {}}, "additionalProperties": true},
+ "data_flow": {"type": "object", "required": ["id", "from", "to"], "properties": {"id": {"type": "string", "minLength": 1}, "from": {"type": "string"}, "to": {"type": "string"}, "data": {"type": "array", "items": {"type": "string"}}, "source_of_truth": {"type": "object", "additionalProperties": {"type": "string"}}}, "additionalProperties": true},
+ "analysis": {"type": "object", "properties": {"variants": {"type": "array", "items": {"$ref": "#/$defs/variant"}}, "pain_points": {"type": "array", "items": {"$ref": "#/$defs/pain_point"}}, "data_flows": {"type": "array", "items": {"$ref": "#/$defs/data_flow"}}}, "additionalProperties": true},
"step": {"type": "object", "required": ["id", "name"], "properties": {
"id": {"type": "string", "minLength": 1}, "name": {"type": "string", "minLength": 1}, "type": {"enum": ["task", "user_task", "service_task", "decision", "parallel", "event", "end", "subprocess"]}, "actor": {"type": "string"}, "system": {"type": "string"},
"objects": {"type": "array", "items": {"type": "string"}}, "interfaces": {"type": "array", "items": {"type": "string"}}, "controls": {"type": "array", "items": {"type": "string"}}, "risks": {"type": "array", "items": {"type": "string"}}, "evidence": {"type": "array", "items": {"type": "string"}}, "artifacts": {"type": "array", "items": {"type": "string"}},
- "inputs": {"type": "array", "items": {"$ref": "#/$defs/contract"}}, "outputs": {"type": "array", "items": {"$ref": "#/$defs/contract"}}, "transitions": {"type": "array", "items": {"$ref": "#/$defs/transition"}},
+ "inputs": {"type": "array", "items": {"$ref": "#/$defs/contract"}}, "outputs": {"type": "array", "items": {"$ref": "#/$defs/contract"}},
+ "timing": {"$ref": "#/$defs/timing"}, "ownership": {"$ref": "#/$defs/ownership"}, "transitions": {"type": "array", "items": {"$ref": "#/$defs/transition"}},
"sla": {"type": "object", "properties": {"duration": {"type": "string"}, "target": {}, "business_hours": {"type": "boolean"}}, "additionalProperties": true},
"kpis": {"type": "array", "items": {"type": "object", "required": ["id"], "properties": {"id": {"type": "string"}, "name": {"type": "string"}, "target": {}, "unit": {"type": "string"}}, "additionalProperties": true}},
"agent": {"type": "object", "properties": {"exposable": {"type": "boolean"}, "executable": {"type": "boolean"}, "allowed_actions": {"type": "array", "items": {"type": "string"}}}, "additionalProperties": true},
diff --git a/tests/test_analysis_canvas.py b/tests/test_analysis_canvas.py
new file mode 100644
index 0000000..755b76a
--- /dev/null
+++ b/tests/test_analysis_canvas.py
@@ -0,0 +1,32 @@
+from pathlib import Path
+
+from process_as_code.cli import main
+from process_as_code.io import load_process
+from process_as_code.render import to_analysis_canvas
+from process_as_code.validate import validate_process
+
+ROOT = Path(__file__).parents[1]
+EXAMPLE = ROOT / "examples/process-analysis/order-entry.process.yaml"
+
+
+def test_process_analysis_example_and_canvas():
+ data = load_process(EXAMPLE)
+ result = validate_process(data)
+ assert result.ok, result.errors
+
+ html = to_analysis_canvas(data)
+ assert "Process Analysis Canvas" in html
+ assert "Save order as draft" in html
+ assert "Check approval email" in html
+ assert "Customer search takes 20 minutes" in html
+ assert "Source of truth" in html
+ assert "PT45M" in html
+
+
+def test_cli_canvas_writes_standalone_html(tmp_path):
+ output = tmp_path / "analysis.html"
+ assert main(["canvas", str(EXAMPLE), "-o", str(output)]) == 0
+ rendered = output.read_text(encoding="utf-8")
+ assert rendered.startswith("")
+ assert "Actual execution" in rendered
+ assert "Variants & exceptions" in rendered
diff --git a/vscode-extension/process.schema.json b/vscode-extension/process.schema.json
index e6099bd..b4a1f47 100644
--- a/vscode-extension/process.schema.json
+++ b/vscode-extension/process.schema.json
@@ -9,6 +9,7 @@
"process": {"type": "object", "required": ["id", "name"], "properties": {"id": {"type": "string", "minLength": 1}, "name": {"type": "string", "minLength": 1}, "description": {"type": "string"}, "owner": {"type": "string"}, "start": {"type": "string"}, "trigger": {"type": "string"}, "outcome": {"type": "string"}, "tags": {"type": "array", "items": {"type": "string"}}}, "additionalProperties": true},
"roles": {"$ref": "#/$defs/catalog"}, "systems": {"$ref": "#/$defs/catalog"}, "objects": {"$ref": "#/$defs/catalog"}, "interfaces": {"$ref": "#/$defs/catalog"}, "controls": {"$ref": "#/$defs/catalog"}, "risks": {"$ref": "#/$defs/catalog"}, "evidence": {"$ref": "#/$defs/catalog"},
"artifacts": {"type": "array", "items": {"type": "object", "required": ["id", "uri"], "properties": {"id": {"type": "string"}, "kind": {"type": "string"}, "relation": {"type": "string"}, "uri": {"type": "string"}}, "additionalProperties": true}},
+ "analysis": {"$ref": "#/$defs/analysis"},
"steps": {"type": "array", "minItems": 1, "items": {"$ref": "#/$defs/step"}},
"catalogs": {"type": "array", "items": {"type": "object", "required": ["uri"], "properties": {"uri": {"type": "string"}, "include": {"type": "array", "items": {"type": "string"}}}, "additionalProperties": true}}
},
@@ -16,10 +17,17 @@
"catalog": {"type": "array", "items": {"type": "object", "required": ["id"], "properties": {"id": {"type": "string", "minLength": 1}, "name": {"type": "string"}, "description": {"type": "string"}}, "additionalProperties": true}},
"contract": {"type": "object", "properties": {"id": {"type": "string"}, "name": {"type": "string"}, "type": {"type": "string"}, "ref": {"type": "string"}, "required": {"type": "boolean"}}, "anyOf": [{"required": ["id"]}, {"required": ["name"]}, {"required": ["ref"]}], "additionalProperties": true},
"transition": {"type": "object", "required": ["to"], "properties": {"to": {"type": "string"}, "label": {"type": "string"}, "when": {"type": "string"}, "data": {"type": "object"}}, "additionalProperties": true},
+ "timing": {"type": "object", "properties": {"touch_time": {"type": "string"}, "wait_time": {"type": "string"}, "elapsed_time": {"type": "string"}}, "additionalProperties": true},
+ "ownership": {"type": "object", "properties": {"process_owner": {"type": "string"}, "change_owner": {"type": "string"}}, "additionalProperties": true},
+ "variant": {"type": "object", "required": ["id"], "properties": {"id": {"type": "string", "minLength": 1}, "name": {"type": "string"}, "when": {"type": "string"}, "condition": {"type": "string"}, "path": {"type": "array", "items": {"type": "string"}}, "notes": {"type": "string"}}, "additionalProperties": true},
+ "pain_point": {"type": "object", "required": ["id", "observation"], "properties": {"id": {"type": "string", "minLength": 1}, "name": {"type": "string"}, "step": {"type": "string"}, "observation": {"type": "string", "minLength": 1}, "cause": {"type": "string"}, "evidence": {"type": "array", "items": {"type": "string"}}, "impact": {}}, "additionalProperties": true},
+ "data_flow": {"type": "object", "required": ["id", "from", "to"], "properties": {"id": {"type": "string", "minLength": 1}, "from": {"type": "string"}, "to": {"type": "string"}, "data": {"type": "array", "items": {"type": "string"}}, "source_of_truth": {"type": "object", "additionalProperties": {"type": "string"}}}, "additionalProperties": true},
+ "analysis": {"type": "object", "properties": {"variants": {"type": "array", "items": {"$ref": "#/$defs/variant"}}, "pain_points": {"type": "array", "items": {"$ref": "#/$defs/pain_point"}}, "data_flows": {"type": "array", "items": {"$ref": "#/$defs/data_flow"}}}, "additionalProperties": true},
"step": {"type": "object", "required": ["id", "name"], "properties": {
"id": {"type": "string", "minLength": 1}, "name": {"type": "string", "minLength": 1}, "type": {"enum": ["task", "user_task", "service_task", "decision", "parallel", "event", "end", "subprocess"]}, "actor": {"type": "string"}, "system": {"type": "string"},
"objects": {"type": "array", "items": {"type": "string"}}, "interfaces": {"type": "array", "items": {"type": "string"}}, "controls": {"type": "array", "items": {"type": "string"}}, "risks": {"type": "array", "items": {"type": "string"}}, "evidence": {"type": "array", "items": {"type": "string"}}, "artifacts": {"type": "array", "items": {"type": "string"}},
- "inputs": {"type": "array", "items": {"$ref": "#/$defs/contract"}}, "outputs": {"type": "array", "items": {"$ref": "#/$defs/contract"}}, "transitions": {"type": "array", "items": {"$ref": "#/$defs/transition"}},
+ "inputs": {"type": "array", "items": {"$ref": "#/$defs/contract"}}, "outputs": {"type": "array", "items": {"$ref": "#/$defs/contract"}},
+ "timing": {"$ref": "#/$defs/timing"}, "ownership": {"$ref": "#/$defs/ownership"}, "transitions": {"type": "array", "items": {"$ref": "#/$defs/transition"}},
"sla": {"type": "object", "properties": {"duration": {"type": "string"}, "target": {}, "business_hours": {"type": "boolean"}}, "additionalProperties": true},
"kpis": {"type": "array", "items": {"type": "object", "required": ["id"], "properties": {"id": {"type": "string"}, "name": {"type": "string"}, "target": {}, "unit": {"type": "string"}}, "additionalProperties": true}},
"agent": {"type": "object", "properties": {"exposable": {"type": "boolean"}, "executable": {"type": "boolean"}, "allowed_actions": {"type": "array", "items": {"type": "string"}}}, "additionalProperties": true},