flowproof 0.1.0__cp39-abi3-win_amd64.whl

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.
flowproof/__init__.py ADDED
@@ -0,0 +1,38 @@
1
+ """FlowProof — AI-native E2E testing for the apps Selenium can't reach.
2
+
3
+ Open-source AI-native E2E testing framework for enterprise Windows apps
4
+ (SAP GUI, Oracle, Citrix, legacy desktop). AI authors a flow once from a
5
+ natural-language spec and records a trace; a deterministic engine replays
6
+ the trace in CI with zero LLM calls.
7
+
8
+ The Rust engine ships inside this package as the `flowproof._native`
9
+ extension module; the `flowproof` command drives it.
10
+ See https://github.com/automators-com/flowproof
11
+ """
12
+
13
+ from flowproof.flow import (
14
+ Flow,
15
+ HealResult,
16
+ RecordResult,
17
+ RunResult,
18
+ StepResult,
19
+ get_trace,
20
+ heal,
21
+ record,
22
+ run,
23
+ )
24
+
25
+ __version__ = "0.1.0"
26
+
27
+ __all__ = [
28
+ "Flow",
29
+ "HealResult",
30
+ "RecordResult",
31
+ "RunResult",
32
+ "StepResult",
33
+ "__version__",
34
+ "get_trace",
35
+ "heal",
36
+ "record",
37
+ "run",
38
+ ]
flowproof/_native.pyd ADDED
Binary file
flowproof/cli.py ADDED
@@ -0,0 +1,16 @@
1
+ """Console entry point: delegates to the Rust CLI inside the extension
2
+ module so `flowproof` behaves identically however the engine is invoked."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import sys
7
+
8
+ from flowproof import _native
9
+
10
+
11
+ def main() -> None:
12
+ sys.exit(_native.cli_main(sys.argv[1:]))
13
+
14
+
15
+ if __name__ == "__main__":
16
+ main()
flowproof/flow.py ADDED
@@ -0,0 +1,190 @@
1
+ """Public API surface of the flowproof SDK, backed by the Rust engine.
2
+
3
+ Designed for programmatic callers (typically AI agents): every operation
4
+ returns structured data — never something that has to be scraped out of
5
+ stdout. The ``flowproof`` CLI wraps these same code paths for humans.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ from dataclasses import dataclass
12
+ from pathlib import Path
13
+ from typing import Any
14
+
15
+ from flowproof import _native
16
+
17
+
18
+ @dataclass(frozen=True)
19
+ class RecordResult:
20
+ """Outcome of recording a flow."""
21
+
22
+ trace_path: Path
23
+ steps: int
24
+
25
+
26
+ @dataclass(frozen=True)
27
+ class StepResult:
28
+ """One replayed step. ``status`` is ``passed``, ``failed`` or ``skipped``."""
29
+
30
+ id: str
31
+ intent: str
32
+ status: str
33
+ duration_ms: int
34
+ detail: str | None = None
35
+ started_ms: int = 0
36
+ """Offset from run start — with duration_ms, the step→time mapping into
37
+ the run's recording."""
38
+ selector_tier: str | None = None
39
+ """Selector-ladder tier that resolved the target (native_id,
40
+ structural, text_anchor)."""
41
+ degraded: bool = False
42
+ """True when a fallback rung matched instead of the recorded primary
43
+ selector — the step ran, but the trace should be healed."""
44
+
45
+
46
+ @dataclass(frozen=True)
47
+ class RunResult:
48
+ """Structured outcome of a replay. Truthy exactly when the flow passed."""
49
+
50
+ name: str
51
+ trace_id: str
52
+ passed: bool
53
+ duration_ms: int
54
+ steps: tuple[StepResult, ...]
55
+ report_path: Path
56
+ html_path: Path
57
+ """Human-readable rendering generated from the JSON report."""
58
+ recording: dict[str, Any] | None = None
59
+ """The run's recording bundle: format, frame refs, per-step time ranges
60
+ (None when the driver cannot capture)."""
61
+ degraded: bool = False
62
+ """True when any step resolved via a fallback selector rung: the run
63
+ passed, but the app drifted from the trace — schedule a heal."""
64
+
65
+ def __bool__(self) -> bool:
66
+ return self.passed
67
+
68
+ @property
69
+ def junit_path(self) -> Path:
70
+ """JUnit XML rendering of this run, for CI systems that ingest it
71
+ (Jenkins, GitLab, Azure DevOps, ...). Written alongside
72
+ ``result.json``."""
73
+ return self.report_path.with_name("junit.xml")
74
+
75
+
76
+ def _parse_run_result(payload: str) -> RunResult:
77
+ data = json.loads(payload)
78
+ report = data["report"]
79
+ return RunResult(
80
+ name=report["name"],
81
+ trace_id=report["trace_id"],
82
+ passed=report["passed"],
83
+ duration_ms=report["duration_ms"],
84
+ steps=tuple(
85
+ StepResult(
86
+ id=s["id"],
87
+ intent=s["intent"],
88
+ status=s["status"],
89
+ duration_ms=s["duration_ms"],
90
+ detail=s.get("detail"),
91
+ started_ms=s.get("started_ms", 0),
92
+ selector_tier=s.get("selector_tier"),
93
+ degraded=s.get("degraded", False),
94
+ )
95
+ for s in report["steps"]
96
+ ),
97
+ report_path=Path(data["report_path"]),
98
+ html_path=Path(data["report_path"]).with_name("report.html"),
99
+ recording=report.get("recording"),
100
+ degraded=report.get("degraded", False),
101
+ )
102
+
103
+
104
+ @dataclass(frozen=True)
105
+ class HealResult:
106
+ """Outcome of a heal pass: a PROPOSED trace diff, never a silent fix."""
107
+
108
+ changed: bool
109
+ steps_changed: tuple[dict[str, Any], ...]
110
+ steps_added: int
111
+ steps_removed: int
112
+ proposed_path: Path | None
113
+ applied: bool
114
+ diff_html: Path | None = None
115
+ """Human review page: before/after per changed step, with frames."""
116
+
117
+
118
+ def _parse_heal_result(payload: str) -> HealResult:
119
+ data = json.loads(payload)
120
+ report = data["report"]
121
+ proposed = report.get("proposed_path")
122
+ diff_html = report.get("diff_html")
123
+ return HealResult(
124
+ changed=report["changed"],
125
+ steps_changed=tuple(report["steps_changed"]),
126
+ steps_added=report["steps_added"],
127
+ steps_removed=report["steps_removed"],
128
+ proposed_path=Path(proposed) if proposed else None,
129
+ applied=data["applied"],
130
+ diff_html=Path(diff_html) if diff_html else None,
131
+ )
132
+
133
+
134
+ @dataclass(frozen=True)
135
+ class Flow:
136
+ """A flow, defined by a YAML spec with natural-language steps."""
137
+
138
+ spec: Path
139
+
140
+ def __init__(self, spec: str | Path) -> None:
141
+ object.__setattr__(self, "spec", Path(spec))
142
+
143
+ def record(self, out: str | Path | None = None) -> RecordResult:
144
+ """Perform the flow once against the live app and write a trace.
145
+
146
+ Requires Windows and the target app.
147
+ """
148
+ data = json.loads(_native.record(self.spec, Path(out) if out else None))
149
+ return RecordResult(trace_path=Path(data["trace_path"]), steps=data["steps"])
150
+
151
+ def run(self, trace: str | Path | None = None) -> RunResult:
152
+ """Deterministically replay the recorded trace (zero LLM calls).
153
+
154
+ A failing test is a ``RunResult`` with ``passed=False``, not an
155
+ exception; ``RuntimeError`` means the run could not execute at all
156
+ (missing trace, unsupported platform, ...).
157
+ """
158
+ return _parse_run_result(_native.run(self.spec, Path(trace) if trace else None))
159
+
160
+ def get_trace(self, trace: str | Path | None = None) -> dict[str, Any]:
161
+ """Load the recorded trace for inspection: ``{"header": …, "steps": […]}``."""
162
+ return json.loads(_native.get_trace(Path(trace) if trace else self.spec))
163
+
164
+ def heal(self, trace: str | Path | None = None, apply: bool = False) -> HealResult:
165
+ """Re-author the flow against the live app and propose a trace diff.
166
+
167
+ Never modifies the trace unless ``apply=True`` is passed explicitly;
168
+ the proposal lands next to the trace as ``*.proposed.jsonl``.
169
+ """
170
+ return _parse_heal_result(_native.heal(self.spec, Path(trace) if trace else None, apply))
171
+
172
+
173
+ def record(spec: str | Path, out: str | Path | None = None) -> RecordResult:
174
+ """Record a flow from a YAML spec. See :meth:`Flow.record`."""
175
+ return Flow(spec).record(out)
176
+
177
+
178
+ def run(spec: str | Path, trace: str | Path | None = None) -> RunResult:
179
+ """Replay a recorded flow deterministically. See :meth:`Flow.run`."""
180
+ return Flow(spec).run(trace)
181
+
182
+
183
+ def get_trace(path: str | Path) -> dict[str, Any]:
184
+ """Load a trace (from a spec path or a ``.jsonl`` file) for inspection."""
185
+ return json.loads(_native.get_trace(Path(path)))
186
+
187
+
188
+ def heal(spec: str | Path, trace: str | Path | None = None, apply: bool = False) -> HealResult:
189
+ """Propose a heal diff for a stale trace. See :meth:`Flow.heal`."""
190
+ return Flow(spec).heal(trace, apply)
@@ -0,0 +1,68 @@
1
+ """flowproof as MCP tools: record, run, get_trace, heal over stdio.
2
+
3
+ Lets any MCP-capable agent (Claude, opencode, DataMaker, ...) drive
4
+ flowproof directly. Thin wrappers over the same engine the CLI uses;
5
+ every tool takes and returns JSON-serializable data.
6
+
7
+ Requires the ``mcp`` extra: ``pip install flowproof[mcp]`` (Python 3.10+).
8
+ Run with ``flowproof-mcp`` or ``python -m flowproof.mcp_server``.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import json
14
+ from typing import Any
15
+
16
+ from flowproof import _native
17
+
18
+ try:
19
+ from mcp.server.fastmcp import FastMCP
20
+ except ImportError as exc: # pragma: no cover - exercised via main() guard
21
+ FastMCP = None
22
+ _IMPORT_ERROR = exc
23
+
24
+ if FastMCP is not None:
25
+ mcp = FastMCP("flowproof")
26
+
27
+ @mcp.tool()
28
+ def flowproof_record(spec: str, out: str | None = None) -> dict[str, Any]:
29
+ """Record a flow: perform it once against the live app and write a
30
+ deterministic trace. Requires the target platform (Windows for UIA
31
+ apps like calc/notepad; any OS for `app: web`). Returns
32
+ {"trace_path", "steps"}."""
33
+ return json.loads(_native.record(spec, out))
34
+
35
+ @mcp.tool()
36
+ def flowproof_run(spec: str, trace: str | None = None) -> dict[str, Any]:
37
+ """Deterministically replay a recorded flow (zero LLM calls). A
38
+ failing test is data ({"report": {"passed": false, ...}}), not an
39
+ error. Returns {"report", "report_path"}."""
40
+ return json.loads(_native.run(spec, trace))
41
+
42
+ @mcp.tool()
43
+ def flowproof_get_trace(path: str) -> dict[str, Any]:
44
+ """Load a recorded trace for inspection. `path` may be the flow spec
45
+ (the default trace next to it is used) or a .jsonl trace file.
46
+ Returns {"header", "steps"}."""
47
+ return json.loads(_native.get_trace(path))
48
+
49
+ @mcp.tool()
50
+ def flowproof_heal(spec: str, trace: str | None = None, apply: bool = False) -> dict[str, Any]:
51
+ """Re-author the flow against the live app and propose a reviewable
52
+ trace diff (written as *.proposed.jsonl). Never modifies the trace
53
+ unless apply=true is passed explicitly. Returns
54
+ {"report": {"changed", "steps_changed", ...}, "applied"}."""
55
+ return json.loads(_native.heal(spec, trace, apply))
56
+
57
+
58
+ def main() -> None:
59
+ if FastMCP is None:
60
+ raise SystemExit(
61
+ "flowproof-mcp requires the 'mcp' extra (Python 3.10+): "
62
+ f"pip install 'flowproof[mcp]' ({_IMPORT_ERROR})"
63
+ )
64
+ mcp.run()
65
+
66
+
67
+ if __name__ == "__main__":
68
+ main()
flowproof/py.typed ADDED
File without changes
@@ -0,0 +1,62 @@
1
+ Metadata-Version: 2.4
2
+ Name: flowproof
3
+ Version: 0.1.0
4
+ Classifier: Development Status :: 2 - Pre-Alpha
5
+ Classifier: Intended Audience :: Developers
6
+ Classifier: Programming Language :: Python :: 3
7
+ Classifier: Programming Language :: Rust
8
+ Classifier: Topic :: Software Development :: Testing
9
+ Requires-Dist: mcp>=1.2 ; python_full_version >= '3.10' and extra == 'mcp'
10
+ Provides-Extra: mcp
11
+ Summary: AI-native E2E testing for the apps Selenium can't reach — SAP GUI, Oracle Forms, Citrix, Windows desktop.
12
+ Author-email: Automators <hello@automators.com>
13
+ License-Expression: Apache-2.0
14
+ Requires-Python: >=3.9
15
+ Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
16
+ Project-URL: Homepage, https://github.com/automators-com/flowproof
17
+ Project-URL: Repository, https://github.com/automators-com/flowproof
18
+
19
+ # flowproof
20
+
21
+ AI-native E2E testing for the apps Selenium can't reach — SAP GUI, Oracle
22
+ Forms, Citrix, and any Windows desktop app.
23
+
24
+ **AI authors, deterministic engine executes.** A flow is described in YAML
25
+ with natural-language steps, recorded once against the live app, and replayed
26
+ deterministically in CI with zero LLM calls. Built agent-first: every
27
+ operation returns structured results a program can reason over.
28
+
29
+ ```yaml
30
+ # calc.flow.yaml
31
+ name: Add two numbers
32
+ app: calc
33
+ steps:
34
+ - Type 5
35
+ - Press plus
36
+ - Type 3
37
+ - Press equals
38
+ - assert: display shows 8
39
+ ```
40
+
41
+ ```python
42
+ from flowproof import Flow
43
+
44
+ flow = Flow("calc.flow.yaml")
45
+ flow.record() # performs the flow live (Windows), writes the trace
46
+ result = flow.run() # deterministic replay -> RunResult
47
+ assert result.passed
48
+ ```
49
+
50
+ Or from the shell: `flowproof record calc.flow.yaml`, then
51
+ `flowproof run calc.flow.yaml` (add `--json` for the structured report).
52
+
53
+ The wheel bundles the Rust engine (Windows UI Automation driver); no separate
54
+ install. Windows is required to record/run flows — the package imports fine
55
+ elsewhere for inspection and tooling.
56
+
57
+ **Status: early (v0.1)** — the deterministic record→replay spine, proven on
58
+ Windows Calculator. LLM authoring agents, SAP GUI adapter, Citrix vision
59
+ mode, and self-healing are on the roadmap.
60
+
61
+ Docs and source: [github.com/automators-com/flowproof](https://github.com/automators-com/flowproof)
62
+
@@ -0,0 +1,11 @@
1
+ flowproof/__init__.py,sha256=OulOoeVDJnBsQiNthUE3ykLTubS9csVcdgGckoN8h_k,893
2
+ flowproof/_native.pyd,sha256=V5W-EGXNRwmCVMC3StsmegUV4lQW6D6qPVFTjCRBEPc,15764480
3
+ flowproof/cli.py,sha256=n3JQGOgfoQV67ED4aeAZ2NQCyOOriborpXnd4DASPWE,349
4
+ flowproof/flow.py,sha256=PzHgwrdt6HjoGTgUt0m7cd4-N7Y6HdTuxlaLixbmpM0,6772
5
+ flowproof/mcp_server.py,sha256=4M-2lciAxXZltzztWMRWK6Mhtsubix2f2Dr394gOnzA,2580
6
+ flowproof/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
+ flowproof-0.1.0.dist-info/METADATA,sha256=DvcFeS3XbYBWuit9lwLc9ur1eLmk5cUza4swZnP1-WQ,2281
8
+ flowproof-0.1.0.dist-info/WHEEL,sha256=_rgZdQc9sLFEqt2IemX7qyxsGt5fFRnBDATxGzerdmg,95
9
+ flowproof-0.1.0.dist-info/entry_points.txt,sha256=pSWgAeBHdGFEQ0htyNsQykPg2OdPrXYpQ0hiOwg20wA,87
10
+ flowproof-0.1.0.dist-info/sboms/flowproof-python.cyclonedx.json,sha256=warBanOkF_UbFycEJETG-JnLTK0eNUibRTfSqpQsWy0,397961
11
+ flowproof-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: maturin (1.14.1)
3
+ Root-Is-Purelib: false
4
+ Tag: cp39-abi3-win_amd64
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ flowproof=flowproof.cli:main
3
+ flowproof-mcp=flowproof.mcp_server:main