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 + + + +
+
+
+
Process Analysis Canvas
+

{_html(meta.get("name") or meta.get("id") or "Process")}

+
+
Trigger{_html(meta.get("trigger"))}
+
→
+
End state{_html(meta.get("outcome"))}
+
+
+
+
{len(steps)}steps
+
{system_count}systems
+
{len(variants)}variants
+
{len(pain_points)}pain points
+
+
+ +
+
Actual executionActor · system · input · output · observed time · target
+ + +{"".join(rows)} +
#StepActorSystemInputOutputTouchWaitElapsedTarget
+
+ +
+

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},