flowproof 0.2.0__tar.gz → 0.2.1__tar.gz
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-0.2.0 → flowproof-0.2.1}/Cargo.lock +8 -7
- {flowproof-0.2.0 → flowproof-0.2.1}/Cargo.toml +1 -1
- flowproof-0.2.1/PKG-INFO +66 -0
- flowproof-0.2.1/README.md +47 -0
- flowproof-0.2.1/crates/flowproof-agent/src/clarify.rs +177 -0
- {flowproof-0.2.0 → flowproof-0.2.1}/crates/flowproof-agent/src/lib.rs +2 -0
- {flowproof-0.2.0 → flowproof-0.2.1}/crates/flowproof-agent/src/recorder.rs +151 -10
- {flowproof-0.2.0 → flowproof-0.2.1}/crates/flowproof-agent/src/rules.rs +62 -1
- {flowproof-0.2.0 → flowproof-0.2.1}/crates/flowproof-agent/src/spec.rs +76 -1
- {flowproof-0.2.0 → flowproof-0.2.1}/crates/flowproof-cli/Cargo.toml +1 -0
- {flowproof-0.2.0 → flowproof-0.2.1}/crates/flowproof-cli/src/lib.rs +172 -2
- flowproof-0.2.1/crates/flowproof-cli/tests/api_pipeline.rs +177 -0
- flowproof-0.2.1/crates/flowproof-cli/tests/examples_resolve.rs +64 -0
- flowproof-0.2.1/crates/flowproof-cli/tests/suite_env_from.rs +86 -0
- {flowproof-0.2.0 → flowproof-0.2.1}/crates/flowproof-driver/src/oob.rs +30 -7
- {flowproof-0.2.0 → flowproof-0.2.1}/crates/flowproof-python/src/lib.rs +20 -6
- {flowproof-0.2.0 → flowproof-0.2.1}/crates/flowproof-replay/src/lib.rs +18 -3
- {flowproof-0.2.0 → flowproof-0.2.1}/crates/flowproof-trace/schema/trace-v1.schema.json +9 -1
- {flowproof-0.2.0 → flowproof-0.2.1}/crates/flowproof-trace/src/format.rs +4 -0
- {flowproof-0.2.0 → flowproof-0.2.1}/crates/flowproof-trace/src/secret.rs +63 -0
- {flowproof-0.2.0 → flowproof-0.2.1}/crates/flowproof-trace/tests/fixtures/sample.trace.jsonl +1 -0
- {flowproof-0.2.0 → flowproof-0.2.1}/crates/flowproof-trace/tests/schema_conformance.rs +3 -1
- {flowproof-0.2.0 → flowproof-0.2.1}/flowproof/__init__.py +8 -6
- {flowproof-0.2.0 → flowproof-0.2.1}/flowproof/flow.py +24 -1
- {flowproof-0.2.0 → flowproof-0.2.1}/flowproof/mcp_server.py +8 -1
- {flowproof-0.2.0 → flowproof-0.2.1}/pyproject.toml +2 -2
- flowproof-0.2.0/PKG-INFO +0 -62
- flowproof-0.2.0/README.md +0 -43
- flowproof-0.2.0/crates/flowproof-cli/tests/api_pipeline.rs +0 -79
- {flowproof-0.2.0 → flowproof-0.2.1}/crates/flowproof-adapters/Cargo.toml +0 -0
- {flowproof-0.2.0 → flowproof-0.2.1}/crates/flowproof-adapters/src/lib.rs +0 -0
- {flowproof-0.2.0 → flowproof-0.2.1}/crates/flowproof-adapters/src/sap_com.rs +0 -0
- {flowproof-0.2.0 → flowproof-0.2.1}/crates/flowproof-adapters/src/vision.rs +0 -0
- {flowproof-0.2.0 → flowproof-0.2.1}/crates/flowproof-adapters/src/web.rs +0 -0
- {flowproof-0.2.0 → flowproof-0.2.1}/crates/flowproof-agent/Cargo.toml +0 -0
- {flowproof-0.2.0 → flowproof-0.2.1}/crates/flowproof-agent/src/author.rs +0 -0
- {flowproof-0.2.0 → flowproof-0.2.1}/crates/flowproof-agent/src/heal.rs +0 -0
- {flowproof-0.2.0 → flowproof-0.2.1}/crates/flowproof-agent/src/llm.rs +0 -0
- {flowproof-0.2.0 → flowproof-0.2.1}/crates/flowproof-cli/src/main.rs +0 -0
- {flowproof-0.2.0 → flowproof-0.2.1}/crates/flowproof-cli/tests/calc_e2e.rs +0 -0
- {flowproof-0.2.0 → flowproof-0.2.1}/crates/flowproof-cli/tests/llm_author_e2e.rs +0 -0
- {flowproof-0.2.0 → flowproof-0.2.1}/crates/flowproof-cli/tests/notepad_author_e2e.rs +0 -0
- {flowproof-0.2.0 → flowproof-0.2.1}/crates/flowproof-cli/tests/notepad_e2e.rs +0 -0
- {flowproof-0.2.0 → flowproof-0.2.1}/crates/flowproof-cli/tests/sap_e2e.rs +0 -0
- {flowproof-0.2.0 → flowproof-0.2.1}/crates/flowproof-cli/tests/sap_pipeline.rs +0 -0
- {flowproof-0.2.0 → flowproof-0.2.1}/crates/flowproof-cli/tests/sap_sim_e2e.rs +0 -0
- {flowproof-0.2.0 → flowproof-0.2.1}/crates/flowproof-cli/tests/support/sap_simulator.py +0 -0
- {flowproof-0.2.0 → flowproof-0.2.1}/crates/flowproof-cli/tests/vision_pipeline.rs +0 -0
- {flowproof-0.2.0 → flowproof-0.2.1}/crates/flowproof-cli/tests/web_e2e.rs +0 -0
- {flowproof-0.2.0 → flowproof-0.2.1}/crates/flowproof-driver/Cargo.toml +0 -0
- {flowproof-0.2.0 → flowproof-0.2.1}/crates/flowproof-driver/src/app.rs +0 -0
- {flowproof-0.2.0 → flowproof-0.2.1}/crates/flowproof-driver/src/backend.rs +0 -0
- {flowproof-0.2.0 → flowproof-0.2.1}/crates/flowproof-driver/src/gdi.rs +0 -0
- {flowproof-0.2.0 → flowproof-0.2.1}/crates/flowproof-driver/src/lib.rs +0 -0
- {flowproof-0.2.0 → flowproof-0.2.1}/crates/flowproof-driver/src/mock.rs +0 -0
- {flowproof-0.2.0 → flowproof-0.2.1}/crates/flowproof-driver/src/recording.rs +0 -0
- {flowproof-0.2.0 → flowproof-0.2.1}/crates/flowproof-driver/src/redact.rs +0 -0
- {flowproof-0.2.0 → flowproof-0.2.1}/crates/flowproof-driver/src/window.rs +0 -0
- {flowproof-0.2.0 → flowproof-0.2.1}/crates/flowproof-python/Cargo.toml +0 -0
- {flowproof-0.2.0 → flowproof-0.2.1}/crates/flowproof-replay/Cargo.toml +0 -0
- {flowproof-0.2.0 → flowproof-0.2.1}/crates/flowproof-replay/src/report.rs +0 -0
- {flowproof-0.2.0 → flowproof-0.2.1}/crates/flowproof-replay/tests/replay_calc.rs +0 -0
- {flowproof-0.2.0 → flowproof-0.2.1}/crates/flowproof-trace/Cargo.toml +0 -0
- {flowproof-0.2.0 → flowproof-0.2.1}/crates/flowproof-trace/src/lib.rs +0 -0
- {flowproof-0.2.0 → flowproof-0.2.1}/flowproof/cli.py +0 -0
- {flowproof-0.2.0 → flowproof-0.2.1}/flowproof/py.typed +0 -0
|
@@ -722,7 +722,7 @@ dependencies = [
|
|
|
722
722
|
|
|
723
723
|
[[package]]
|
|
724
724
|
name = "flowproof-adapters"
|
|
725
|
-
version = "0.2.
|
|
725
|
+
version = "0.2.1"
|
|
726
726
|
dependencies = [
|
|
727
727
|
"anyhow",
|
|
728
728
|
"flowproof-driver",
|
|
@@ -738,7 +738,7 @@ dependencies = [
|
|
|
738
738
|
|
|
739
739
|
[[package]]
|
|
740
740
|
name = "flowproof-agent"
|
|
741
|
-
version = "0.2.
|
|
741
|
+
version = "0.2.1"
|
|
742
742
|
dependencies = [
|
|
743
743
|
"chrono",
|
|
744
744
|
"flowproof-driver",
|
|
@@ -753,7 +753,7 @@ dependencies = [
|
|
|
753
753
|
|
|
754
754
|
[[package]]
|
|
755
755
|
name = "flowproof-cli"
|
|
756
|
-
version = "0.2.
|
|
756
|
+
version = "0.2.1"
|
|
757
757
|
dependencies = [
|
|
758
758
|
"ab_glyph",
|
|
759
759
|
"clap",
|
|
@@ -765,12 +765,13 @@ dependencies = [
|
|
|
765
765
|
"image",
|
|
766
766
|
"imageproc",
|
|
767
767
|
"serde_json",
|
|
768
|
+
"serde_yaml",
|
|
768
769
|
"tiny_http",
|
|
769
770
|
]
|
|
770
771
|
|
|
771
772
|
[[package]]
|
|
772
773
|
name = "flowproof-driver"
|
|
773
|
-
version = "0.2.
|
|
774
|
+
version = "0.2.1"
|
|
774
775
|
dependencies = [
|
|
775
776
|
"image",
|
|
776
777
|
"postgres",
|
|
@@ -784,7 +785,7 @@ dependencies = [
|
|
|
784
785
|
|
|
785
786
|
[[package]]
|
|
786
787
|
name = "flowproof-python"
|
|
787
|
-
version = "0.2.
|
|
788
|
+
version = "0.2.1"
|
|
788
789
|
dependencies = [
|
|
789
790
|
"flowproof-agent",
|
|
790
791
|
"flowproof-cli",
|
|
@@ -796,7 +797,7 @@ dependencies = [
|
|
|
796
797
|
|
|
797
798
|
[[package]]
|
|
798
799
|
name = "flowproof-replay"
|
|
799
|
-
version = "0.2.
|
|
800
|
+
version = "0.2.1"
|
|
800
801
|
dependencies = [
|
|
801
802
|
"chrono",
|
|
802
803
|
"flowproof-agent",
|
|
@@ -809,7 +810,7 @@ dependencies = [
|
|
|
809
810
|
|
|
810
811
|
[[package]]
|
|
811
812
|
name = "flowproof-trace"
|
|
812
|
-
version = "0.2.
|
|
813
|
+
version = "0.2.1"
|
|
813
814
|
dependencies = [
|
|
814
815
|
"jsonschema",
|
|
815
816
|
"serde",
|
|
@@ -3,7 +3,7 @@ resolver = "2"
|
|
|
3
3
|
members = ["crates/flowproof-driver", "crates/flowproof-trace", "crates/flowproof-replay", "crates/flowproof-agent", "crates/flowproof-adapters", "crates/flowproof-cli", "crates/flowproof-python"]
|
|
4
4
|
|
|
5
5
|
[workspace.package]
|
|
6
|
-
version = "0.2.
|
|
6
|
+
version = "0.2.1"
|
|
7
7
|
edition = "2021"
|
|
8
8
|
license = "Apache-2.0"
|
|
9
9
|
repository = "https://github.com/automators-com/flowproof"
|
flowproof-0.2.1/PKG-INFO
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: flowproof
|
|
3
|
+
Version: 0.2.1
|
|
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: Generic open-source automation framework for the AI-agent era: automated testing and agentic process automation across web, desktop, and Citrix.
|
|
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
|
+
A generic open-source automation framework for the AI-agent era:
|
|
22
|
+
automated testing and agentic process automation across web, desktop,
|
|
23
|
+
and Citrix.
|
|
24
|
+
|
|
25
|
+
**Agents author, a deterministic engine executes.** A flow is described in
|
|
26
|
+
YAML with natural-language steps, recorded once against the live app, and
|
|
27
|
+
replayed deterministically in CI with zero LLM calls. Built agent-native:
|
|
28
|
+
every operation returns structured results a program can reason over, and
|
|
29
|
+
agents can drive record/run/get_trace/heal over MCP
|
|
30
|
+
(`pip install flowproof[mcp]`, run `flowproof-mcp`).
|
|
31
|
+
|
|
32
|
+
```yaml
|
|
33
|
+
# calc.flow.yaml
|
|
34
|
+
name: Add two numbers
|
|
35
|
+
app: calc
|
|
36
|
+
steps:
|
|
37
|
+
- Type 5
|
|
38
|
+
- Press plus
|
|
39
|
+
- Type 3
|
|
40
|
+
- Press equals
|
|
41
|
+
- assert: display shows 8
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
from flowproof import Flow
|
|
46
|
+
|
|
47
|
+
flow = Flow("calc.flow.yaml")
|
|
48
|
+
flow.record() # performs the flow live, writes the trace
|
|
49
|
+
result = flow.run() # deterministic replay -> RunResult
|
|
50
|
+
assert result.passed
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Or from the shell: `flowproof record calc.flow.yaml`, then
|
|
54
|
+
`flowproof run calc.flow.yaml` (add `--json` for the structured report).
|
|
55
|
+
|
|
56
|
+
The wheel bundles the Rust engine; no separate install. Targets: `web`
|
|
57
|
+
(headless Chromium, any OS), Windows desktop via UI Automation, `sap`
|
|
58
|
+
(SAP GUI Scripting), `vision` (pixels-only for Citrix/RDP), and `api`
|
|
59
|
+
(no UI — HTTP/SQL assertion flows, any OS).
|
|
60
|
+
|
|
61
|
+
**Status: early, in active development (v0.2).** Record→replay,
|
|
62
|
+
model-grounded authoring, healing with reviewable diffs, suites, run
|
|
63
|
+
recordings, and the MCP server all work and are tested in CI.
|
|
64
|
+
|
|
65
|
+
Docs and source: [github.com/automators-com/flowproof](https://github.com/automators-com/flowproof)
|
|
66
|
+
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# flowproof
|
|
2
|
+
|
|
3
|
+
A generic open-source automation framework for the AI-agent era:
|
|
4
|
+
automated testing and agentic process automation across web, desktop,
|
|
5
|
+
and Citrix.
|
|
6
|
+
|
|
7
|
+
**Agents author, a deterministic engine executes.** A flow is described in
|
|
8
|
+
YAML with natural-language steps, recorded once against the live app, and
|
|
9
|
+
replayed deterministically in CI with zero LLM calls. Built agent-native:
|
|
10
|
+
every operation returns structured results a program can reason over, and
|
|
11
|
+
agents can drive record/run/get_trace/heal over MCP
|
|
12
|
+
(`pip install flowproof[mcp]`, run `flowproof-mcp`).
|
|
13
|
+
|
|
14
|
+
```yaml
|
|
15
|
+
# calc.flow.yaml
|
|
16
|
+
name: Add two numbers
|
|
17
|
+
app: calc
|
|
18
|
+
steps:
|
|
19
|
+
- Type 5
|
|
20
|
+
- Press plus
|
|
21
|
+
- Type 3
|
|
22
|
+
- Press equals
|
|
23
|
+
- assert: display shows 8
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
```python
|
|
27
|
+
from flowproof import Flow
|
|
28
|
+
|
|
29
|
+
flow = Flow("calc.flow.yaml")
|
|
30
|
+
flow.record() # performs the flow live, writes the trace
|
|
31
|
+
result = flow.run() # deterministic replay -> RunResult
|
|
32
|
+
assert result.passed
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Or from the shell: `flowproof record calc.flow.yaml`, then
|
|
36
|
+
`flowproof run calc.flow.yaml` (add `--json` for the structured report).
|
|
37
|
+
|
|
38
|
+
The wheel bundles the Rust engine; no separate install. Targets: `web`
|
|
39
|
+
(headless Chromium, any OS), Windows desktop via UI Automation, `sap`
|
|
40
|
+
(SAP GUI Scripting), `vision` (pixels-only for Citrix/RDP), and `api`
|
|
41
|
+
(no UI — HTTP/SQL assertion flows, any OS).
|
|
42
|
+
|
|
43
|
+
**Status: early, in active development (v0.2).** Record→replay,
|
|
44
|
+
model-grounded authoring, healing with reviewable diffs, suites, run
|
|
45
|
+
recordings, and the MCP server all work and are tested in CI.
|
|
46
|
+
|
|
47
|
+
Docs and source: [github.com/automators-com/flowproof](https://github.com/automators-com/flowproof)
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
//! Structured "this step could not be authored" payloads.
|
|
2
|
+
//!
|
|
3
|
+
//! flowproof deliberately has no in-loop clarification: when neither the
|
|
4
|
+
//! rules nor the model can turn a step into a grounded action, recording
|
|
5
|
+
//! stops and the *driving* agent (a human, or an MCP caller like DataMaker's
|
|
6
|
+
//! agent) resolves the ambiguity — by consulting an external source of
|
|
7
|
+
//! truth, rewriting the vague step into concrete grammar, and re-recording.
|
|
8
|
+
//! This module is the machine-readable half of that loop: everything the
|
|
9
|
+
//! driving agent needs to know *what* was ambiguous and *what the live
|
|
10
|
+
//! screen offered* at the moment authoring gave up.
|
|
11
|
+
|
|
12
|
+
use serde::{Deserialize, Serialize};
|
|
13
|
+
|
|
14
|
+
/// Where authoring gave up on the step.
|
|
15
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
|
16
|
+
#[serde(rename_all = "snake_case")]
|
|
17
|
+
pub enum ClarifyStage {
|
|
18
|
+
/// The rules couldn't parse the step and no model backend is
|
|
19
|
+
/// configured — the payload carries the rules diagnostic.
|
|
20
|
+
NoModel,
|
|
21
|
+
/// The model was consulted (with one self-correcting retry) and still
|
|
22
|
+
/// couldn't ground the step to a listed element.
|
|
23
|
+
Model,
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
impl std::fmt::Display for ClarifyStage {
|
|
27
|
+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
28
|
+
f.write_str(match self {
|
|
29
|
+
ClarifyStage::NoModel => "no model backend",
|
|
30
|
+
ClarifyStage::Model => "model could not ground",
|
|
31
|
+
})
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/// One interactable element from the live scene at the moment of failure —
|
|
36
|
+
/// the same grounding set the author saw, in structured form so the driving
|
|
37
|
+
/// agent can enumerate fields ("which of these should the vague step
|
|
38
|
+
/// change?") instead of re-scraping the app.
|
|
39
|
+
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
40
|
+
pub struct SceneElement {
|
|
41
|
+
/// Neutral target token (`css:…`, `id:…`, `text:…`) — usable verbatim
|
|
42
|
+
/// as a quoted target in a rewritten step.
|
|
43
|
+
pub target: String,
|
|
44
|
+
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
45
|
+
pub tag: Option<String>,
|
|
46
|
+
/// Associated label / aria-label / placeholder, when the driver saw one.
|
|
47
|
+
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
48
|
+
pub label: Option<String>,
|
|
49
|
+
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
50
|
+
pub text: Option<String>,
|
|
51
|
+
/// Input type (`text`, `password`, …), when applicable.
|
|
52
|
+
#[serde(default, rename = "type", skip_serializing_if = "Option::is_none")]
|
|
53
|
+
pub input_type: Option<String>,
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/// The full clarification payload. Text fields keep `${VAR}` references
|
|
57
|
+
/// raw — nothing here ever holds a resolved secret.
|
|
58
|
+
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
59
|
+
pub struct Clarification {
|
|
60
|
+
/// The spec step that could not be authored, verbatim.
|
|
61
|
+
pub step: String,
|
|
62
|
+
/// 0-based index of that step in the spec.
|
|
63
|
+
pub step_index: usize,
|
|
64
|
+
pub stage: ClarifyStage,
|
|
65
|
+
/// Human-readable diagnostic from the stage that gave up.
|
|
66
|
+
pub reason: String,
|
|
67
|
+
/// The rules diagnostic, when the model stage was reached (the rules
|
|
68
|
+
/// failed first — their error often names the accepted forms).
|
|
69
|
+
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
70
|
+
pub rules_error: Option<String>,
|
|
71
|
+
/// Intents already performed, in order — the app is left in the state
|
|
72
|
+
/// AFTER these, which is the state `scene` describes.
|
|
73
|
+
pub completed_steps: Vec<String>,
|
|
74
|
+
/// Interactable inventory of the live screen (may be empty when the
|
|
75
|
+
/// driver has no scene support).
|
|
76
|
+
pub scene: Vec<SceneElement>,
|
|
77
|
+
/// What to do next, for agents that don't know the loop.
|
|
78
|
+
pub hint: String,
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
impl Clarification {
|
|
82
|
+
pub const HINT: &'static str = "Rewrite the step using the grammar in docs/authoring.md, \
|
|
83
|
+
targeting a listed element (quote its label, or use its target token verbatim), \
|
|
84
|
+
then re-record. Consult your data source for domain questions the scene cannot \
|
|
85
|
+
answer (e.g. which fields are required).";
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/// Parse a driver scene JSON into the structured inventory. Mirrors the
|
|
89
|
+
/// author's grounding rules (`author.rs::scene_targets`): a modern element
|
|
90
|
+
/// carries a `target` token; a legacy web element with only a `css` key is
|
|
91
|
+
/// lifted to `css:<sel>`. Anything unparseable yields an empty inventory —
|
|
92
|
+
/// a clarification without a scene is still useful.
|
|
93
|
+
pub fn scene_inventory(scene: &str) -> Vec<SceneElement> {
|
|
94
|
+
serde_json::from_str::<Vec<serde_json::Value>>(scene)
|
|
95
|
+
.unwrap_or_default()
|
|
96
|
+
.iter()
|
|
97
|
+
.filter_map(|e| {
|
|
98
|
+
let target = e["target"]
|
|
99
|
+
.as_str()
|
|
100
|
+
.map(str::to_string)
|
|
101
|
+
.or_else(|| e["css"].as_str().map(|css| format!("css:{css}")))?;
|
|
102
|
+
let field = |key: &str| e[key].as_str().map(str::to_string);
|
|
103
|
+
Some(SceneElement {
|
|
104
|
+
target,
|
|
105
|
+
tag: field("tag"),
|
|
106
|
+
label: field("label"),
|
|
107
|
+
text: field("text"),
|
|
108
|
+
input_type: field("type"),
|
|
109
|
+
})
|
|
110
|
+
})
|
|
111
|
+
.collect()
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
#[cfg(test)]
|
|
115
|
+
mod tests {
|
|
116
|
+
use super::*;
|
|
117
|
+
|
|
118
|
+
#[test]
|
|
119
|
+
fn parses_a_web_shaped_scene() {
|
|
120
|
+
let scene = r##"[
|
|
121
|
+
{"target": "css:#price", "css": "#price", "tag": "input",
|
|
122
|
+
"type": "text", "label": "Net Price"},
|
|
123
|
+
{"target": "text:Save", "tag": "button", "text": "Save"}
|
|
124
|
+
]"##;
|
|
125
|
+
let inv = scene_inventory(scene);
|
|
126
|
+
assert_eq!(inv.len(), 2);
|
|
127
|
+
assert_eq!(inv[0].target, "css:#price");
|
|
128
|
+
assert_eq!(inv[0].label.as_deref(), Some("Net Price"));
|
|
129
|
+
assert_eq!(inv[0].input_type.as_deref(), Some("text"));
|
|
130
|
+
assert_eq!(inv[1].target, "text:Save");
|
|
131
|
+
assert_eq!(inv[1].text.as_deref(), Some("Save"));
|
|
132
|
+
assert!(inv[1].label.is_none());
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
#[test]
|
|
136
|
+
fn lifts_legacy_bare_css_entries() {
|
|
137
|
+
let inv = scene_inventory(r##"[{"css": "#total", "tag": "span"}]"##);
|
|
138
|
+
assert_eq!(inv.len(), 1);
|
|
139
|
+
assert_eq!(inv[0].target, "css:#total");
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
#[test]
|
|
143
|
+
fn garbage_scene_yields_empty_inventory() {
|
|
144
|
+
assert!(scene_inventory("not json").is_empty());
|
|
145
|
+
assert!(scene_inventory("[{\"no_target\": 1}]").is_empty());
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
#[test]
|
|
149
|
+
fn payload_serializes_to_the_documented_field_names() {
|
|
150
|
+
let c = Clarification {
|
|
151
|
+
step: "make required field changes".into(),
|
|
152
|
+
step_index: 4,
|
|
153
|
+
stage: ClarifyStage::Model,
|
|
154
|
+
reason: "target 'invented' is not one of the listed elements".into(),
|
|
155
|
+
rules_error: Some("no rule matches".into()),
|
|
156
|
+
completed_steps: vec!["Press the \"Edit\" button".into()],
|
|
157
|
+
scene: vec![SceneElement {
|
|
158
|
+
target: "css:#price".into(),
|
|
159
|
+
tag: Some("input".into()),
|
|
160
|
+
label: Some("Net Price".into()),
|
|
161
|
+
text: None,
|
|
162
|
+
input_type: Some("text".into()),
|
|
163
|
+
}],
|
|
164
|
+
hint: Clarification::HINT.into(),
|
|
165
|
+
};
|
|
166
|
+
let v = serde_json::to_value(&c).expect("serializes");
|
|
167
|
+
assert_eq!(v["stage"], "model");
|
|
168
|
+
assert_eq!(v["step_index"], 4);
|
|
169
|
+
assert_eq!(v["scene"][0]["type"], "text");
|
|
170
|
+
assert_eq!(v["scene"][0]["label"], "Net Price");
|
|
171
|
+
// Optional fields absent when None.
|
|
172
|
+
assert!(v["scene"][0].get("text").is_none());
|
|
173
|
+
// Round-trips.
|
|
174
|
+
let back: Clarification = serde_json::from_value(v).expect("deserializes");
|
|
175
|
+
assert_eq!(back, c);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
@@ -5,12 +5,14 @@
|
|
|
5
5
|
//! scene graph. The replayer never touches this crate.
|
|
6
6
|
|
|
7
7
|
pub mod author;
|
|
8
|
+
pub mod clarify;
|
|
8
9
|
pub mod heal;
|
|
9
10
|
pub mod llm;
|
|
10
11
|
pub mod recorder;
|
|
11
12
|
pub mod rules;
|
|
12
13
|
pub mod spec;
|
|
13
14
|
|
|
15
|
+
pub use clarify::{Clarification, ClarifyStage};
|
|
14
16
|
pub use heal::{heal, heal_with_author, HealError, HealReport};
|
|
15
17
|
pub use llm::{HttpModelClient, ModelClient};
|
|
16
18
|
pub use recorder::{record, record_with_author, Author, RecordError, RecordSummary};
|
|
@@ -76,6 +76,12 @@ pub enum RecordError {
|
|
|
76
76
|
NoAuthor { step: String, rules_error: String },
|
|
77
77
|
#[error("driver cannot describe its scene; LLM authoring is unavailable for app '{0}'")]
|
|
78
78
|
NoScene(String),
|
|
79
|
+
#[error(
|
|
80
|
+
"cannot author step '{}' ({}): {} — a structured clarification payload with the \
|
|
81
|
+
live-screen inventory is available via `record --json` or the MCP record tool",
|
|
82
|
+
.0.step, .0.stage, .0.reason
|
|
83
|
+
)]
|
|
84
|
+
NeedsClarification(Box<crate::clarify::Clarification>),
|
|
79
85
|
}
|
|
80
86
|
|
|
81
87
|
/// Outcome of a recording session.
|
|
@@ -372,6 +378,8 @@ fn step_for(id: usize, intent: &str, app: &str, action: &ResolvedAction) -> Step
|
|
|
372
378
|
ResolvedAction::AssertApi {
|
|
373
379
|
method,
|
|
374
380
|
url,
|
|
381
|
+
headers,
|
|
382
|
+
body,
|
|
375
383
|
status,
|
|
376
384
|
body_contains,
|
|
377
385
|
timeout_ms,
|
|
@@ -384,10 +392,13 @@ fn step_for(id: usize, intent: &str, app: &str, action: &ResolvedAction) -> Step
|
|
|
384
392
|
(
|
|
385
393
|
Vec::new(),
|
|
386
394
|
Action::Assert(Assertion::Api {
|
|
395
|
+
// Raw clones: body string leaves and header values keep
|
|
396
|
+
// their ${VAR} refs — tokens never enter the trace.
|
|
387
397
|
request: flowproof_trace::format::ApiRequest {
|
|
388
398
|
method: method.clone(),
|
|
389
399
|
url: url.clone(),
|
|
390
|
-
body:
|
|
400
|
+
body: body.clone(),
|
|
401
|
+
headers: headers.clone(),
|
|
391
402
|
},
|
|
392
403
|
status: *status,
|
|
393
404
|
expect: Some(serde_json::Value::Object(expect)),
|
|
@@ -601,11 +612,38 @@ fn author_actions<D: AppDriver, C: ModelClient>(
|
|
|
601
612
|
if author == Author::Rules {
|
|
602
613
|
return Err(RecordError::Rules(rules_error));
|
|
603
614
|
}
|
|
604
|
-
|
|
605
|
-
|
|
615
|
+
// Ambiguity from here on ends in a structured clarification:
|
|
616
|
+
// the driving agent — not flowproof — resolves it and re-records.
|
|
617
|
+
// `prior` holds the intents already performed, so its length is
|
|
618
|
+
// this step's index and the live scene reflects their effects.
|
|
619
|
+
let clarify = |stage, reason: String, rules_err: Option<String>, scene: Vec<_>| {
|
|
620
|
+
RecordError::NeedsClarification(Box::new(crate::clarify::Clarification {
|
|
606
621
|
step: intent.to_string(),
|
|
607
|
-
|
|
608
|
-
|
|
622
|
+
step_index: prior.len(),
|
|
623
|
+
stage,
|
|
624
|
+
reason,
|
|
625
|
+
rules_error: rules_err,
|
|
626
|
+
completed_steps: prior.to_vec(),
|
|
627
|
+
scene,
|
|
628
|
+
hint: crate::clarify::Clarification::HINT.into(),
|
|
629
|
+
}))
|
|
630
|
+
};
|
|
631
|
+
let Some(client) = client.as_mut() else {
|
|
632
|
+
let inventory = driver
|
|
633
|
+
.scene()
|
|
634
|
+
.ok()
|
|
635
|
+
.flatten()
|
|
636
|
+
.map(|s| crate::clarify::scene_inventory(&s))
|
|
637
|
+
.unwrap_or_default();
|
|
638
|
+
return Err(clarify(
|
|
639
|
+
crate::clarify::ClarifyStage::NoModel,
|
|
640
|
+
format!(
|
|
641
|
+
"no model backend is configured (set FLOWPROOF_AI_PROVIDER / \
|
|
642
|
+
FLOWPROOF_AI_API_KEY to enable LLM authoring): {rules_error}"
|
|
643
|
+
),
|
|
644
|
+
Some(rules_error.to_string()),
|
|
645
|
+
inventory,
|
|
646
|
+
));
|
|
609
647
|
};
|
|
610
648
|
let scene = driver
|
|
611
649
|
.scene()?
|
|
@@ -618,9 +656,21 @@ fn author_actions<D: AppDriver, C: ModelClient>(
|
|
|
618
656
|
intent,
|
|
619
657
|
scene: &scene,
|
|
620
658
|
};
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
659
|
+
match author_step(*client, &ctx) {
|
|
660
|
+
Ok(action) => {
|
|
661
|
+
*llm_used = true;
|
|
662
|
+
Ok(vec![action])
|
|
663
|
+
}
|
|
664
|
+
// Grounding failure after the retry = genuine ambiguity.
|
|
665
|
+
// Config errors (bad key, network) stay plain errors.
|
|
666
|
+
Err(crate::AgentError::Authoring { reason, .. }) => Err(clarify(
|
|
667
|
+
crate::clarify::ClarifyStage::Model,
|
|
668
|
+
reason,
|
|
669
|
+
Some(rules_error.to_string()),
|
|
670
|
+
crate::clarify::scene_inventory(&scene),
|
|
671
|
+
)),
|
|
672
|
+
Err(other) => Err(other.into()),
|
|
673
|
+
}
|
|
624
674
|
}
|
|
625
675
|
}
|
|
626
676
|
}
|
|
@@ -734,16 +784,33 @@ pub fn record_with_client<D: AppDriver, C: ModelClient>(
|
|
|
734
784
|
ResolvedAction::AssertApi {
|
|
735
785
|
method,
|
|
736
786
|
url,
|
|
787
|
+
headers,
|
|
788
|
+
body,
|
|
737
789
|
status,
|
|
738
790
|
body_contains,
|
|
739
791
|
timeout_ms,
|
|
740
792
|
} => {
|
|
793
|
+
// Resolved like `equals` above: the trace keeps the raw
|
|
794
|
+
// ${VAR}; only the live probe sees values — including
|
|
795
|
+
// header tokens and body string leaves.
|
|
741
796
|
let probe = flowproof_driver::oob::OobProbe::Api {
|
|
742
797
|
method: method.clone(),
|
|
743
798
|
url: flowproof_trace::secret::resolve_refs(url)?,
|
|
744
|
-
body:
|
|
799
|
+
body: match body {
|
|
800
|
+
Some(b) => Some(flowproof_trace::secret::resolve_refs_in_json(b)?),
|
|
801
|
+
None => None,
|
|
802
|
+
},
|
|
803
|
+
headers: headers
|
|
804
|
+
.iter()
|
|
805
|
+
.map(|(k, v)| {
|
|
806
|
+
Ok((k.clone(), flowproof_trace::secret::resolve_refs(v)?))
|
|
807
|
+
})
|
|
808
|
+
.collect::<Result<_, flowproof_trace::secret::MissingSecret>>()?,
|
|
745
809
|
status: *status,
|
|
746
|
-
body_contains: body_contains
|
|
810
|
+
body_contains: match body_contains {
|
|
811
|
+
Some(needle) => Some(flowproof_trace::secret::resolve_refs(needle)?),
|
|
812
|
+
None => None,
|
|
813
|
+
},
|
|
747
814
|
};
|
|
748
815
|
poll_oob(&probe, *timeout_ms, &spec_step.intent())?
|
|
749
816
|
}
|
|
@@ -1212,6 +1279,80 @@ steps:
|
|
|
1212
1279
|
assert_eq!(client.calls, 0);
|
|
1213
1280
|
}
|
|
1214
1281
|
|
|
1282
|
+
#[test]
|
|
1283
|
+
fn no_model_backend_yields_clarification_with_scene() {
|
|
1284
|
+
let spec = FlowSpec::parse(
|
|
1285
|
+
"name: Vague
|
|
1286
|
+
app: web
|
|
1287
|
+
url: https://e.test/x
|
|
1288
|
+
steps:
|
|
1289
|
+
- Type acme into the \"Supplier\" field
|
|
1290
|
+
- make required field changes
|
|
1291
|
+
",
|
|
1292
|
+
)
|
|
1293
|
+
.expect("parses");
|
|
1294
|
+
let mut driver = MockAppDriver::new(&["Supplier"]);
|
|
1295
|
+
driver.scene = Some(
|
|
1296
|
+
r##"[{"target":"css:#price","tag":"input","type":"text","label":"Net Price"}]"##.into(),
|
|
1297
|
+
);
|
|
1298
|
+
let out = std::env::temp_dir().join("flowproof-clarify-nomodel.trace.jsonl");
|
|
1299
|
+
let err = record_with_client(
|
|
1300
|
+
&spec,
|
|
1301
|
+
&mut driver,
|
|
1302
|
+
&out,
|
|
1303
|
+
Author::Auto,
|
|
1304
|
+
Option::<&mut CountingClient>::None,
|
|
1305
|
+
)
|
|
1306
|
+
.expect_err("vague step with no model must need clarification");
|
|
1307
|
+
let RecordError::NeedsClarification(c) = err else {
|
|
1308
|
+
panic!("expected NeedsClarification, got: {err}");
|
|
1309
|
+
};
|
|
1310
|
+
assert_eq!(c.step, "make required field changes");
|
|
1311
|
+
assert_eq!(c.step_index, 1);
|
|
1312
|
+
assert_eq!(c.stage, crate::ClarifyStage::NoModel);
|
|
1313
|
+
assert_eq!(
|
|
1314
|
+
c.completed_steps,
|
|
1315
|
+
vec!["Type acme into the \"Supplier\" field"]
|
|
1316
|
+
);
|
|
1317
|
+
// The live inventory reached the payload — the driving agent can
|
|
1318
|
+
// see the "Net Price" field exists and rewrite the step.
|
|
1319
|
+
assert_eq!(c.scene.len(), 1);
|
|
1320
|
+
assert_eq!(c.scene[0].label.as_deref(), Some("Net Price"));
|
|
1321
|
+
assert!(c.rules_error.is_some());
|
|
1322
|
+
std::fs::remove_file(&out).ok();
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
#[test]
|
|
1326
|
+
fn ungrounded_model_yields_clarification() {
|
|
1327
|
+
let spec = FlowSpec::parse(
|
|
1328
|
+
"name: Vague
|
|
1329
|
+
app: web
|
|
1330
|
+
url: https://e.test/x
|
|
1331
|
+
steps:
|
|
1332
|
+
- make required field changes
|
|
1333
|
+
",
|
|
1334
|
+
)
|
|
1335
|
+
.expect("parses");
|
|
1336
|
+
let mut driver = MockAppDriver::new(&["#price"]);
|
|
1337
|
+
driver.scene = Some(r##"[{"target":"css:#price","tag":"input"}]"##.into());
|
|
1338
|
+
// The model invents a selector on both attempts — grounding fails.
|
|
1339
|
+
let mut client = CountingClient {
|
|
1340
|
+
reply: r##"{"action":"click","target":"css:#invented"}"##.into(),
|
|
1341
|
+
calls: 0,
|
|
1342
|
+
};
|
|
1343
|
+
let out = std::env::temp_dir().join("flowproof-clarify-model.trace.jsonl");
|
|
1344
|
+
let err = record_with_client(&spec, &mut driver, &out, Author::Auto, Some(&mut client))
|
|
1345
|
+
.expect_err("ungroundable step must need clarification");
|
|
1346
|
+
let RecordError::NeedsClarification(c) = err else {
|
|
1347
|
+
panic!("expected NeedsClarification, got: {err}");
|
|
1348
|
+
};
|
|
1349
|
+
assert_eq!(c.stage, crate::ClarifyStage::Model);
|
|
1350
|
+
assert_eq!(client.calls, 2, "one attempt + one self-correcting retry");
|
|
1351
|
+
assert!(c.rules_error.is_some(), "rules diagnostic travels along");
|
|
1352
|
+
assert_eq!(c.scene[0].target, "css:#price");
|
|
1353
|
+
std::fs::remove_file(&out).ok();
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1215
1356
|
#[test]
|
|
1216
1357
|
fn unknown_app_is_rejected() {
|
|
1217
1358
|
let spec =
|