diff --git a/README.md b/README.md index 1d3bc8e5..293cfc31 100644 --- a/README.md +++ b/README.md @@ -5,12 +5,12 @@ [![Python](https://img.shields.io/pypi/pyversions/openadapt-flow)](https://pypi.org/project/openadapt-flow/) [![License: MIT](https://img.shields.io/badge/license-MIT-green)](LICENSE) -Show OpenAdapt any repeated GUI task — in the browser, in a native desktop -app, or inside a Citrix or virtual-desktop session — demonstrated once. It -compiles the demonstration into a governed, deterministic, locally executable -program that replays exactly, with zero model calls on a healthy run. When the -interface drifts, it re-resolves from recorded evidence or proposes a governed -repair, and it halts instead of guessing when verification fails. +OpenAdapt is built for repeated workflows behind browser, legacy desktop, and +remote-display interfaces. It compiles demonstrated GUI workflows into +deterministic, locally executable programs. Each target application and +environment is qualified separately. Healthy runs make no model calls. When +interfaces drift, OpenAdapt re-resolves from retained evidence or proposes a +governed repair and halts when verification fails. ![One demonstration, two UIs, same compiled workflow — the right side self-heals under a theme it has never seen](docs/showcase/demo.gif) diff --git a/openadapt_flow/__main__.py b/openadapt_flow/__main__.py index 5382b555..a5d22d07 100644 --- a/openadapt_flow/__main__.py +++ b/openadapt_flow/__main__.py @@ -352,17 +352,11 @@ def _build_and_run_replayer( ) -def _finish_replay(run_dir: Path, report, *, synthetic_demo: bool = False) -> int: - """Render the run report, print the outcome, and map it to an exit code. - - ``synthetic_demo`` is True only for the bundled-MockMed demo replay with no - operator ``--param`` overrides (see ``render_run_report``); it softens the - first-run plaintext-PHI warning for known-synthetic demo data and nothing - else. - """ +def _finish_replay(run_dir: Path, report) -> int: + """Render the run report, print the outcome, and map it to an exit code.""" from openadapt_flow.report import render_run_report - report_md = render_run_report(run_dir, synthetic_demo=synthetic_demo) + report_md = render_run_report(run_dir) outcome = "success" if report.success else "FAILED" print(f"Replay {outcome}: {report_md}") if report.screenshots_may_leave_box: @@ -736,16 +730,7 @@ def _cmd_replay(args: argparse.Namespace) -> int: if stop is not None: stop() - # Soften the first-run plaintext-PHI warning ONLY when the CLI itself - # served the bundled synthetic MockMed demo (no --url) and no operator - # values flowed in (--param / --params-file / worklists) — every - # identity-like value is then the recorded fake demo data. Real targets - # or operator-supplied values keep the full warning. - return _finish_replay( - run_dir, - report, - synthetic_demo=(stop is not None and not params and not worklists), - ) + return _finish_replay(run_dir, report) def _cmd_run(args: argparse.Namespace) -> int: diff --git a/openadapt_flow/report.py b/openadapt_flow/report.py index 46655de6..990704f7 100644 --- a/openadapt_flow/report.py +++ b/openadapt_flow/report.py @@ -38,7 +38,7 @@ def _report_has_identity_like_text(report: RunReport) -> bool: return any((r.intent or "").strip() for r in report.results) -def _warn_if_plaintext_phi(report: RunReport, *, synthetic_demo: bool = False) -> None: +def _warn_if_plaintext_phi(report: RunReport) -> None: """Warn (once) when REPORT.md will contain plaintext identity-like text. Fires only when scrubbing is *not* active (default ``auto`` with the @@ -47,17 +47,7 @@ def _warn_if_plaintext_phi(report: RunReport, *, synthetic_demo: bool = False) - ``on`` fails closed upstream before reaching here. Not a behavior change — the report is still written; this only makes the plaintext write visible. ``warnings`` dedups per call site, so it is effectively one-time per process. - - ``synthetic_demo=True`` suppresses the warning. The ONLY caller that sets - it is the ``replay`` CLI, and only when the CLI itself served the bundled - MockMed demo app in-process (no ``--url``) AND the operator supplied no - ``--param`` overrides — i.e. the identity-like free text is the recorded - synthetic demo data, so alarming a first-run user about PHI would be - noise. Any replay against a real app, or with operator-supplied values, - keeps the full warning. """ - if synthetic_demo: - return if _scrub_mode() == "off" or _text_scrubbing_enabled(): return if not _report_has_identity_like_text(report): @@ -111,16 +101,12 @@ def _before_after_table(result: StepResult) -> list[str]: ] -def render_run_report(run_dir: Path | str, *, synthetic_demo: bool = False) -> Path: +def render_run_report(run_dir: Path | str) -> Path: """Render ``REPORT.md`` inside ``run_dir`` from its ``report.json``. Args: run_dir: Run directory containing ``report.json`` (and the ``steps/`` / ``heals/`` image folders it references). - synthetic_demo: True ONLY when the caller knows every identity-like - free-text field is bundled synthetic demo data (the CLI's - bundled-MockMed replay with no operator ``--param`` overrides); - suppresses the plaintext-PHI warning, nothing else. Returns: Path to the written ``REPORT.md``. @@ -132,7 +118,7 @@ def render_run_report(run_dir: Path | str, *, synthetic_demo: bool = False) -> P report = RunReport.model_validate_json( (run / "report.json").read_text(encoding="utf-8") ) - _warn_if_plaintext_phi(report, synthetic_demo=synthetic_demo) + _warn_if_plaintext_phi(report) ok_count = sum(1 for r in report.results if r.ok) icon = "✅" if report.success else "❌" diff --git a/scripts/check_consistency.py b/scripts/check_consistency.py index d47032e1..9d38adf9 100644 --- a/scripts/check_consistency.py +++ b/scripts/check_consistency.py @@ -12,8 +12,8 @@ backticked ``a/b.ext`` token in ``README.md`` / ``DESIGN.md`` / ``docs/LIMITS.md``, and every such path in a ``.github/workflows/*.yml`` comment, actually exists. -* **banned phrases** — stale claims the README must never carry again - (``vision-only``, ``adapters to come``, ``864 tests``). +* **banned phrases** — stale or unbounded claims the README must never carry + again (including substrate universals and single-demonstration guarantees). * **test count** — the README deliberately carries NO hardcoded test count (a number that rots on every test added). If someone reintroduces one, it is checked against ``pytest --collect-only`` and must match within a tolerance. @@ -46,8 +46,17 @@ DOC_FILES = ["README.md", "DESIGN.md", "docs/LIMITS.md"] README = "README.md" -# Stale claims the README must never carry again. Lowercased substring match. -BANNED_PHRASES = ["vision-only", "adapters to come", "864 tests"] +# Stale or unbounded claims the README must never carry again. Lowercased +# substring match. Substrate maturity belongs in the evidence matrix; the +# opening must not silently turn intended scope into universal availability. +BANNED_PHRASES = [ + "vision-only", + "adapters to come", + "864 tests", + "any repeated gui task", + "demonstrated once", + "replays exactly", +] # A referenced path is only checked when it ends in one of these (prose slashes # like "O/0" or "20/20" carry no extension and are ignored), or is a directory diff --git a/tests/test_backend_factory.py b/tests/test_backend_factory.py index ad0dc7f5..cfeb5c04 100644 --- a/tests/test_backend_factory.py +++ b/tests/test_backend_factory.py @@ -307,8 +307,7 @@ def fake_run(backend, **kwargs): monkeypatch.setattr(m, "_build_and_run_replayer", fake_run) monkeypatch.setattr( - "openadapt_flow.report.render_run_report", - lambda run_dir, **_kw: "REPORT.md", + "openadapt_flow.report.render_run_report", lambda run_dir: "REPORT.md" ) monkeypatch.setattr("openadapt_flow.ir.Workflow.load", lambda bundle: object()) diff --git a/tests/test_cli_new_commands.py b/tests/test_cli_new_commands.py index 3cd45391..2a60ed6d 100644 --- a/tests/test_cli_new_commands.py +++ b/tests/test_cli_new_commands.py @@ -330,9 +330,7 @@ def run(self, workflow, **kwargs): monkeypatch.setattr(bs, "ensure_chromium_installed", lambda: None) monkeypatch.setattr(grounder_mod, "build_grounder", lambda fallback=None: None) monkeypatch.setattr(remote_mod, "appliance_from_env", lambda: None) - monkeypatch.setattr( - report_mod, "render_run_report", lambda run_dir, **_kw: "REPORT.md" - ) + monkeypatch.setattr(report_mod, "render_run_report", lambda run_dir: "REPORT.md") monkeypatch.setattr(runtime_mod, "Replayer", _FakeReplayer) @@ -530,9 +528,7 @@ def __init__(self, backend, **kwargs): monkeypatch.setattr( durable_mod, "resume", lambda run_dir, replayer, key=None: _FakeReport() ) - monkeypatch.setattr( - report_mod, "render_run_report", lambda run_dir, **_kw: "REPORT.md" - ) + monkeypatch.setattr(report_mod, "render_run_report", lambda run_dir: "REPORT.md") rc = main( [ diff --git a/tests/test_consistency.py b/tests/test_consistency.py index 0aa249f8..a42d50e1 100644 --- a/tests/test_consistency.py +++ b/tests/test_consistency.py @@ -63,7 +63,14 @@ def test_lock_version_match_ok(): # banned phrases are caught # --------------------------------------------------------------------------- # def test_banned_phrase_detected(): - for phrase in ("vision-only", "adapters to come", "864 tests"): + for phrase in ( + "vision-only", + "adapters to come", + "864 tests", + "any repeated GUI task", + "demonstrated once", + "replays exactly", + ): errors = cc.check_banned_phrases(f"prose {phrase} more prose") assert errors, f"{phrase!r} not flagged" diff --git a/tests/test_privacy.py b/tests/test_privacy.py index 437fd789..ce9a1d72 100644 --- a/tests/test_privacy.py +++ b/tests/test_privacy.py @@ -16,7 +16,6 @@ from __future__ import annotations -import json from pathlib import Path import pytest @@ -298,25 +297,72 @@ def test_report_md_no_warning_when_scrub_off(monkeypatch, tmp_path: Path): render_run_report(run_dir) # must not raise -def test_report_md_no_warning_for_bundled_synthetic_demo(monkeypatch, tmp_path: Path): - """``synthetic_demo=True`` (bundled MockMed, no operator values) => silent. +def test_default_mockmed_replay_warns_for_arbitrary_phi_bundle( + monkeypatch, tmp_path: Path +): + """Default MockMed routing cannot declare an arbitrary bundle synthetic. - The replay CLI sets this only when it served the bundled synthetic MockMed - demo itself (no ``--url``) and no ``--param``/worklist values flowed in, - so the identity-like text is known-fake demo data. + ``replay`` accepts any bundle while defaulting the target URL to MockMed. + The bundle's provenance is therefore unknown, even with no parameter or + worklist overrides, and its identity-like report text must still warn. """ + from contextlib import contextmanager + from types import SimpleNamespace + + import openadapt_flow.__main__ as cli from openadapt_flow.report import PlaintextPHIWarning monkeypatch.setattr(privacy, "_build_provider", lambda: None) privacy.reset_scrubbers() + bundle = tmp_path / "arbitrary-phi-bundle" + bundle.mkdir() run_dir = tmp_path / "run" - run_dir.mkdir() - _phi_report().save(run_dir) + observed: dict[str, object] = {"served": False, "stopped": False} - import warnings as _w + def fake_load(path): + observed["bundle"] = Path(path) + return object() - with _w.catch_warnings(): - _w.simplefilter("error", PlaintextPHIWarning) - md = render_run_report(run_dir, synthetic_demo=True).read_text() - # Only the warning is softened — the report content is unchanged. - assert "John Smith" in md + def fake_serve(*, port): + assert port == 0 + observed["served"] = True + + def stop() -> None: + observed["stopped"] = True + + return "http://127.0.0.1:45678/", stop + + def fake_run(_backend, **kwargs): + report = _phi_report() + report.save(kwargs["run_dir"]) + return report + + page = SimpleNamespace( + url="http://127.0.0.1:45678/", video=None, goto=lambda _url: None + ) + browser = SimpleNamespace(new_page=lambda **_kw: page, close=lambda: None) + playwright = SimpleNamespace(chromium=SimpleNamespace(launch=lambda **_kw: browser)) + + @contextmanager + def fake_sync_playwright(): + yield playwright + + monkeypatch.setattr("openadapt_flow.ir.Workflow.load", fake_load) + monkeypatch.setattr("openadapt_flow.mockmed.server.serve", fake_serve) + monkeypatch.setattr( + "openadapt_flow._browser_setup.ensure_chromium_installed", lambda: None + ) + monkeypatch.setattr("playwright.sync_api.sync_playwright", fake_sync_playwright) + monkeypatch.setattr( + "openadapt_flow.backends.factory.build_backend", lambda *_a, **_k: object() + ) + monkeypatch.setattr(cli, "_build_and_run_replayer", fake_run) + + args = cli.build_parser().parse_args( + ["replay", str(bundle), "--run-dir", str(run_dir)] + ) + + with pytest.warns(PlaintextPHIWarning): + assert cli._cmd_replay(args) == 1 + assert observed == {"served": True, "stopped": True, "bundle": bundle} + assert "John Smith" in (run_dir / "REPORT.md").read_text(encoding="utf-8")