openadapt-flow 0.1.0__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.
Files changed (170) hide show
  1. openadapt_flow-0.1.0/.github/workflows/ci.yml +40 -0
  2. openadapt_flow-0.1.0/.github/workflows/release.yml +47 -0
  3. openadapt_flow-0.1.0/.gitignore +11 -0
  4. openadapt_flow-0.1.0/DESIGN.md +246 -0
  5. openadapt_flow-0.1.0/LICENSE +21 -0
  6. openadapt_flow-0.1.0/PKG-INFO +138 -0
  7. openadapt_flow-0.1.0/README.md +116 -0
  8. openadapt_flow-0.1.0/docs/L1_INTEGRATION.md +76 -0
  9. openadapt_flow-0.1.0/docs/showcase/baseline-run/REPORT.md +55 -0
  10. openadapt_flow-0.1.0/docs/showcase/baseline-run/report.json +213 -0
  11. openadapt_flow-0.1.0/docs/showcase/baseline-run/steps/step_000_after.png +0 -0
  12. openadapt_flow-0.1.0/docs/showcase/baseline-run/steps/step_000_before.png +0 -0
  13. openadapt_flow-0.1.0/docs/showcase/baseline-run/steps/step_001_after.png +0 -0
  14. openadapt_flow-0.1.0/docs/showcase/baseline-run/steps/step_001_before.png +0 -0
  15. openadapt_flow-0.1.0/docs/showcase/baseline-run/steps/step_002_after.png +0 -0
  16. openadapt_flow-0.1.0/docs/showcase/baseline-run/steps/step_002_before.png +0 -0
  17. openadapt_flow-0.1.0/docs/showcase/baseline-run/steps/step_003_after.png +0 -0
  18. openadapt_flow-0.1.0/docs/showcase/baseline-run/steps/step_003_before.png +0 -0
  19. openadapt_flow-0.1.0/docs/showcase/baseline-run/steps/step_004_after.png +0 -0
  20. openadapt_flow-0.1.0/docs/showcase/baseline-run/steps/step_004_before.png +0 -0
  21. openadapt_flow-0.1.0/docs/showcase/baseline-run/steps/step_005_after.png +0 -0
  22. openadapt_flow-0.1.0/docs/showcase/baseline-run/steps/step_005_before.png +0 -0
  23. openadapt_flow-0.1.0/docs/showcase/baseline-run/steps/step_006_after.png +0 -0
  24. openadapt_flow-0.1.0/docs/showcase/baseline-run/steps/step_006_before.png +0 -0
  25. openadapt_flow-0.1.0/docs/showcase/baseline-run/steps/step_007_after.png +0 -0
  26. openadapt_flow-0.1.0/docs/showcase/baseline-run/steps/step_007_before.png +0 -0
  27. openadapt_flow-0.1.0/docs/showcase/baseline-run/steps/step_008_after.png +0 -0
  28. openadapt_flow-0.1.0/docs/showcase/baseline-run/steps/step_008_before.png +0 -0
  29. openadapt_flow-0.1.0/docs/showcase/baseline-run/steps/step_009_after.png +0 -0
  30. openadapt_flow-0.1.0/docs/showcase/baseline-run/steps/step_009_before.png +0 -0
  31. openadapt_flow-0.1.0/docs/showcase/baseline-run/steps/step_010_after.png +0 -0
  32. openadapt_flow-0.1.0/docs/showcase/baseline-run/steps/step_010_before.png +0 -0
  33. openadapt_flow-0.1.0/docs/showcase/bundle/templates/step_000.png +0 -0
  34. openadapt_flow-0.1.0/docs/showcase/bundle/templates/step_002.png +0 -0
  35. openadapt_flow-0.1.0/docs/showcase/bundle/templates/step_004.png +0 -0
  36. openadapt_flow-0.1.0/docs/showcase/bundle/templates/step_005.png +0 -0
  37. openadapt_flow-0.1.0/docs/showcase/bundle/templates/step_006.png +0 -0
  38. openadapt_flow-0.1.0/docs/showcase/bundle/templates/step_007.png +0 -0
  39. openadapt_flow-0.1.0/docs/showcase/bundle/templates/step_008.png +0 -0
  40. openadapt_flow-0.1.0/docs/showcase/bundle/templates/step_010.png +0 -0
  41. openadapt_flow-0.1.0/docs/showcase/bundle/workflow.json +581 -0
  42. openadapt_flow-0.1.0/docs/showcase/bundle/workflow.py +74 -0
  43. openadapt_flow-0.1.0/docs/showcase/healed-bundle/templates/step_000.png +0 -0
  44. openadapt_flow-0.1.0/docs/showcase/healed-bundle/templates/step_002.png +0 -0
  45. openadapt_flow-0.1.0/docs/showcase/healed-bundle/templates/step_004.png +0 -0
  46. openadapt_flow-0.1.0/docs/showcase/healed-bundle/templates/step_005.png +0 -0
  47. openadapt_flow-0.1.0/docs/showcase/healed-bundle/templates/step_006.png +0 -0
  48. openadapt_flow-0.1.0/docs/showcase/healed-bundle/templates/step_007.png +0 -0
  49. openadapt_flow-0.1.0/docs/showcase/healed-bundle/templates/step_008.png +0 -0
  50. openadapt_flow-0.1.0/docs/showcase/healed-bundle/templates/step_010.png +0 -0
  51. openadapt_flow-0.1.0/docs/showcase/healed-bundle/workflow.json +581 -0
  52. openadapt_flow-0.1.0/docs/showcase/recording/events.jsonl +11 -0
  53. openadapt_flow-0.1.0/docs/showcase/recording/frames/0000_after.png +0 -0
  54. openadapt_flow-0.1.0/docs/showcase/recording/frames/0000_before.png +0 -0
  55. openadapt_flow-0.1.0/docs/showcase/recording/frames/0001_after.png +0 -0
  56. openadapt_flow-0.1.0/docs/showcase/recording/frames/0001_before.png +0 -0
  57. openadapt_flow-0.1.0/docs/showcase/recording/frames/0002_after.png +0 -0
  58. openadapt_flow-0.1.0/docs/showcase/recording/frames/0002_before.png +0 -0
  59. openadapt_flow-0.1.0/docs/showcase/recording/frames/0003_after.png +0 -0
  60. openadapt_flow-0.1.0/docs/showcase/recording/frames/0003_before.png +0 -0
  61. openadapt_flow-0.1.0/docs/showcase/recording/frames/0004_after.png +0 -0
  62. openadapt_flow-0.1.0/docs/showcase/recording/frames/0004_before.png +0 -0
  63. openadapt_flow-0.1.0/docs/showcase/recording/frames/0005_after.png +0 -0
  64. openadapt_flow-0.1.0/docs/showcase/recording/frames/0005_before.png +0 -0
  65. openadapt_flow-0.1.0/docs/showcase/recording/frames/0006_after.png +0 -0
  66. openadapt_flow-0.1.0/docs/showcase/recording/frames/0006_before.png +0 -0
  67. openadapt_flow-0.1.0/docs/showcase/recording/frames/0007_after.png +0 -0
  68. openadapt_flow-0.1.0/docs/showcase/recording/frames/0007_before.png +0 -0
  69. openadapt_flow-0.1.0/docs/showcase/recording/frames/0008_after.png +0 -0
  70. openadapt_flow-0.1.0/docs/showcase/recording/frames/0008_before.png +0 -0
  71. openadapt_flow-0.1.0/docs/showcase/recording/frames/0009_after.png +0 -0
  72. openadapt_flow-0.1.0/docs/showcase/recording/frames/0009_before.png +0 -0
  73. openadapt_flow-0.1.0/docs/showcase/recording/frames/0010_after.png +0 -0
  74. openadapt_flow-0.1.0/docs/showcase/recording/frames/0010_before.png +0 -0
  75. openadapt_flow-0.1.0/docs/showcase/recording/meta.json +12 -0
  76. openadapt_flow-0.1.0/docs/showcase/theme-drift-run/REPORT.md +161 -0
  77. openadapt_flow-0.1.0/docs/showcase/theme-drift-run/heals/step_000/heal.json +69 -0
  78. openadapt_flow-0.1.0/docs/showcase/theme-drift-run/heals/step_000/screen.png +0 -0
  79. openadapt_flow-0.1.0/docs/showcase/theme-drift-run/heals/step_000/template.png +0 -0
  80. openadapt_flow-0.1.0/docs/showcase/theme-drift-run/heals/step_002/heal.json +69 -0
  81. openadapt_flow-0.1.0/docs/showcase/theme-drift-run/heals/step_002/screen.png +0 -0
  82. openadapt_flow-0.1.0/docs/showcase/theme-drift-run/heals/step_002/template.png +0 -0
  83. openadapt_flow-0.1.0/docs/showcase/theme-drift-run/heals/step_004/heal.json +69 -0
  84. openadapt_flow-0.1.0/docs/showcase/theme-drift-run/heals/step_004/screen.png +0 -0
  85. openadapt_flow-0.1.0/docs/showcase/theme-drift-run/heals/step_004/template.png +0 -0
  86. openadapt_flow-0.1.0/docs/showcase/theme-drift-run/heals/step_005/heal.json +69 -0
  87. openadapt_flow-0.1.0/docs/showcase/theme-drift-run/heals/step_005/screen.png +0 -0
  88. openadapt_flow-0.1.0/docs/showcase/theme-drift-run/heals/step_005/template.png +0 -0
  89. openadapt_flow-0.1.0/docs/showcase/theme-drift-run/heals/step_006/heal.json +69 -0
  90. openadapt_flow-0.1.0/docs/showcase/theme-drift-run/heals/step_006/screen.png +0 -0
  91. openadapt_flow-0.1.0/docs/showcase/theme-drift-run/heals/step_006/template.png +0 -0
  92. openadapt_flow-0.1.0/docs/showcase/theme-drift-run/heals/step_007/heal.json +69 -0
  93. openadapt_flow-0.1.0/docs/showcase/theme-drift-run/heals/step_007/screen.png +0 -0
  94. openadapt_flow-0.1.0/docs/showcase/theme-drift-run/heals/step_007/template.png +0 -0
  95. openadapt_flow-0.1.0/docs/showcase/theme-drift-run/heals/step_008/heal.json +69 -0
  96. openadapt_flow-0.1.0/docs/showcase/theme-drift-run/heals/step_008/screen.png +0 -0
  97. openadapt_flow-0.1.0/docs/showcase/theme-drift-run/heals/step_008/template.png +0 -0
  98. openadapt_flow-0.1.0/docs/showcase/theme-drift-run/heals/step_010/heal.json +69 -0
  99. openadapt_flow-0.1.0/docs/showcase/theme-drift-run/heals/step_010/screen.png +0 -0
  100. openadapt_flow-0.1.0/docs/showcase/theme-drift-run/heals/step_010/template.png +0 -0
  101. openadapt_flow-0.1.0/docs/showcase/theme-drift-run/report.json +758 -0
  102. openadapt_flow-0.1.0/docs/showcase/theme-drift-run/steps/step_000_after.png +0 -0
  103. openadapt_flow-0.1.0/docs/showcase/theme-drift-run/steps/step_000_before.png +0 -0
  104. openadapt_flow-0.1.0/docs/showcase/theme-drift-run/steps/step_001_after.png +0 -0
  105. openadapt_flow-0.1.0/docs/showcase/theme-drift-run/steps/step_001_before.png +0 -0
  106. openadapt_flow-0.1.0/docs/showcase/theme-drift-run/steps/step_002_after.png +0 -0
  107. openadapt_flow-0.1.0/docs/showcase/theme-drift-run/steps/step_002_before.png +0 -0
  108. openadapt_flow-0.1.0/docs/showcase/theme-drift-run/steps/step_003_after.png +0 -0
  109. openadapt_flow-0.1.0/docs/showcase/theme-drift-run/steps/step_003_before.png +0 -0
  110. openadapt_flow-0.1.0/docs/showcase/theme-drift-run/steps/step_004_after.png +0 -0
  111. openadapt_flow-0.1.0/docs/showcase/theme-drift-run/steps/step_004_before.png +0 -0
  112. openadapt_flow-0.1.0/docs/showcase/theme-drift-run/steps/step_005_after.png +0 -0
  113. openadapt_flow-0.1.0/docs/showcase/theme-drift-run/steps/step_005_before.png +0 -0
  114. openadapt_flow-0.1.0/docs/showcase/theme-drift-run/steps/step_006_after.png +0 -0
  115. openadapt_flow-0.1.0/docs/showcase/theme-drift-run/steps/step_006_before.png +0 -0
  116. openadapt_flow-0.1.0/docs/showcase/theme-drift-run/steps/step_007_after.png +0 -0
  117. openadapt_flow-0.1.0/docs/showcase/theme-drift-run/steps/step_007_before.png +0 -0
  118. openadapt_flow-0.1.0/docs/showcase/theme-drift-run/steps/step_008_after.png +0 -0
  119. openadapt_flow-0.1.0/docs/showcase/theme-drift-run/steps/step_008_before.png +0 -0
  120. openadapt_flow-0.1.0/docs/showcase/theme-drift-run/steps/step_009_after.png +0 -0
  121. openadapt_flow-0.1.0/docs/showcase/theme-drift-run/steps/step_009_before.png +0 -0
  122. openadapt_flow-0.1.0/docs/showcase/theme-drift-run/steps/step_010_after.png +0 -0
  123. openadapt_flow-0.1.0/docs/showcase/theme-drift-run/steps/step_010_before.png +0 -0
  124. openadapt_flow-0.1.0/openadapt_flow/__init__.py +17 -0
  125. openadapt_flow-0.1.0/openadapt_flow/__main__.py +349 -0
  126. openadapt_flow-0.1.0/openadapt_flow/backend.py +33 -0
  127. openadapt_flow-0.1.0/openadapt_flow/backends/__init__.py +24 -0
  128. openadapt_flow-0.1.0/openadapt_flow/backends/playwright_backend.py +152 -0
  129. openadapt_flow-0.1.0/openadapt_flow/bench.py +162 -0
  130. openadapt_flow-0.1.0/openadapt_flow/compiler/__init__.py +6 -0
  131. openadapt_flow-0.1.0/openadapt_flow/compiler/codegen.py +88 -0
  132. openadapt_flow-0.1.0/openadapt_flow/compiler/compile.py +604 -0
  133. openadapt_flow-0.1.0/openadapt_flow/demo_driver.py +78 -0
  134. openadapt_flow-0.1.0/openadapt_flow/emit/__init__.py +13 -0
  135. openadapt_flow-0.1.0/openadapt_flow/emit/l1_artifact.py +151 -0
  136. openadapt_flow-0.1.0/openadapt_flow/emit/mcp_tool.py +174 -0
  137. openadapt_flow-0.1.0/openadapt_flow/emit/skill.py +145 -0
  138. openadapt_flow-0.1.0/openadapt_flow/ir.py +196 -0
  139. openadapt_flow-0.1.0/openadapt_flow/mockmed/__init__.py +9 -0
  140. openadapt_flow-0.1.0/openadapt_flow/mockmed/server.py +52 -0
  141. openadapt_flow-0.1.0/openadapt_flow/mockmed/static/app.js +209 -0
  142. openadapt_flow-0.1.0/openadapt_flow/mockmed/static/index.html +18 -0
  143. openadapt_flow-0.1.0/openadapt_flow/mockmed/static/styles.css +213 -0
  144. openadapt_flow-0.1.0/openadapt_flow/recorder.py +169 -0
  145. openadapt_flow-0.1.0/openadapt_flow/report.py +247 -0
  146. openadapt_flow-0.1.0/openadapt_flow/runtime/__init__.py +47 -0
  147. openadapt_flow-0.1.0/openadapt_flow/runtime/grounder.py +190 -0
  148. openadapt_flow-0.1.0/openadapt_flow/runtime/heal.py +262 -0
  149. openadapt_flow-0.1.0/openadapt_flow/runtime/replayer.py +419 -0
  150. openadapt_flow-0.1.0/openadapt_flow/runtime/resolver.py +297 -0
  151. openadapt_flow-0.1.0/openadapt_flow/vision/__init__.py +25 -0
  152. openadapt_flow-0.1.0/openadapt_flow/vision/hashing.py +55 -0
  153. openadapt_flow-0.1.0/openadapt_flow/vision/match.py +157 -0
  154. openadapt_flow-0.1.0/openadapt_flow/vision/ocr.py +147 -0
  155. openadapt_flow-0.1.0/openadapt_flow/vision/settle.py +61 -0
  156. openadapt_flow-0.1.0/pyproject.toml +37 -0
  157. openadapt_flow-0.1.0/tests/conftest.py +11 -0
  158. openadapt_flow-0.1.0/tests/e2e/__init__.py +0 -0
  159. openadapt_flow-0.1.0/tests/e2e/conftest.py +129 -0
  160. openadapt_flow-0.1.0/tests/e2e/test_record_compile_replay.py +468 -0
  161. openadapt_flow-0.1.0/tests/test_compiler.py +480 -0
  162. openadapt_flow-0.1.0/tests/test_emit.py +267 -0
  163. openadapt_flow-0.1.0/tests/test_heal.py +315 -0
  164. openadapt_flow-0.1.0/tests/test_l1_artifact.py +96 -0
  165. openadapt_flow-0.1.0/tests/test_mockmed.py +256 -0
  166. openadapt_flow-0.1.0/tests/test_recorder.py +208 -0
  167. openadapt_flow-0.1.0/tests/test_replayer.py +460 -0
  168. openadapt_flow-0.1.0/tests/test_report.py +379 -0
  169. openadapt_flow-0.1.0/tests/test_resolver.py +324 -0
  170. openadapt_flow-0.1.0/tests/test_vision.py +323 -0
@@ -0,0 +1,40 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ pull_request:
6
+
7
+ jobs:
8
+ test:
9
+ runs-on: ubuntu-latest
10
+ steps:
11
+ - uses: actions/checkout@v4
12
+
13
+ - uses: actions/setup-python@v5
14
+ with:
15
+ python-version: "3.12"
16
+
17
+ - name: Install
18
+ run: pip install -e .[dev]
19
+
20
+ - name: Install Playwright browser
21
+ run: playwright install --with-deps chromium
22
+
23
+ # --basetemp pins pytest's tmp_path factory inside the workspace so
24
+ # run dirs (REPORT.md + step/heal PNGs) survive for artifact upload;
25
+ # by default they land in the system temp dir and are unrecoverable
26
+ # when a drift/heal test fails in CI.
27
+ - name: Test
28
+ run: mkdir -p runs && pytest -q --basetemp=runs/ci
29
+
30
+ - name: Upload run artifacts
31
+ if: always()
32
+ uses: actions/upload-artifact@v4
33
+ with:
34
+ name: runs
35
+ path: |
36
+ runs/**/REPORT.md
37
+ runs/**/BENCH.md
38
+ runs/**/report.json
39
+ runs/**/*.png
40
+ if-no-files-found: warn
@@ -0,0 +1,47 @@
1
+ name: Release and PyPI Publish
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - "v*"
7
+
8
+ jobs:
9
+ publish:
10
+ runs-on: ubuntu-latest
11
+ environment: pypi
12
+ permissions:
13
+ id-token: write # PyPI Trusted Publishing (OIDC)
14
+ contents: write # GitHub Release
15
+
16
+ steps:
17
+ - name: Checkout repository
18
+ uses: actions/checkout@v4
19
+
20
+ - name: Set up Python
21
+ uses: actions/setup-python@v5
22
+ with:
23
+ python-version: "3.12"
24
+
25
+ - name: Verify tag matches package version
26
+ run: |
27
+ PKG_VERSION=$(python -c "import tomllib; print(tomllib.load(open('pyproject.toml','rb'))['project']['version'])")
28
+ TAG_VERSION="${GITHUB_REF_NAME#v}"
29
+ if [ "$PKG_VERSION" != "$TAG_VERSION" ]; then
30
+ echo "Tag $GITHUB_REF_NAME does not match pyproject version $PKG_VERSION" >&2
31
+ exit 1
32
+ fi
33
+
34
+ - name: Build package
35
+ run: |
36
+ python -m pip install --quiet build
37
+ python -m build
38
+
39
+ - name: Publish to PyPI
40
+ uses: pypa/gh-action-pypi-publish@release/v1
41
+
42
+ - name: Publish GitHub Release
43
+ env:
44
+ GH_TOKEN: ${{ github.token }}
45
+ run: |
46
+ gh release create "$GITHUB_REF_NAME" dist/* \
47
+ --title "$GITHUB_REF_NAME" --generate-notes
@@ -0,0 +1,11 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.pyc
4
+ *.egg-info/
5
+ dist/
6
+ build/
7
+ .pytest_cache/
8
+ .DS_Store
9
+ /runs/
10
+ /recordings/
11
+ dist/
@@ -0,0 +1,246 @@
1
+ # openadapt-flow — Design & Module Contracts (v0)
2
+
3
+ **Product:** record a workflow demonstration once → compile it into a
4
+ deterministic, vision-anchored script → replay it locally with a resolution
5
+ ladder → when the UI drifts, heal the script (as a reviewable diff) instead of
6
+ re-reasoning every run.
7
+
8
+ The runtime is **vision-only**: it consumes PNG bytes and emits clicks/keys at
9
+ pixel coordinates through the `Backend` protocol (`openadapt_flow/backend.py`).
10
+ The reference backend is Playwright-driven (headless-capable, CI-friendly,
11
+ permission-free); native OS / RDP backends are future adapters.
12
+
13
+ ## Frozen contracts (do not change without updating this doc)
14
+
15
+ - `openadapt_flow/ir.py` — Workflow/Step/Anchor/Postcondition + runtime result
16
+ models. FROZEN for v0. Additive changes only, and only by the integrator.
17
+ - `openadapt_flow/backend.py` — Backend protocol. FROZEN.
18
+
19
+ ## Bundle & recording formats
20
+
21
+ **Workflow bundle** (compiler output, replayer input):
22
+ ```
23
+ <bundle>/
24
+ workflow.json # ir.Workflow
25
+ templates/*.png # anchor crops (paths referenced from workflow.json)
26
+ ```
27
+
28
+ **Recording** (recorder output, compiler input):
29
+ ```
30
+ <recording>/
31
+ meta.json # {"id", "created_at", "viewport": [w,h], "app_url",
32
+ # "params": {"<param_name>": "<value typed during demo>"}}
33
+ events.jsonl # one JSON object per line, in order:
34
+ # {"i":0,"kind":"click","x":123,"y":45,"t":1.20}
35
+ # {"i":1,"kind":"type","text":"...","param":"note","t":2.03}
36
+ # {"i":2,"kind":"key","key":"Enter","t":3.10}
37
+ # "param" present iff the typed value is a parameter
38
+ frames/{i:04d}_before.png
39
+ frames/{i:04d}_after.png # after the action settled
40
+ ```
41
+
42
+ **Run directory** (replayer output):
43
+ ```
44
+ <run>/
45
+ report.json # ir.RunReport
46
+ steps/{step_id}_before.png / _after.png
47
+ heals/{step_id}/… # heal crops + heal.json per heal event
48
+ ```
49
+
50
+ ## Module ownership (build phase — do not edit files outside your area)
51
+
52
+ | Area | Owner | Files |
53
+ |---|---|---|
54
+ | Mock app + backend + recorder | Agent A | `openadapt_flow/mockmed/**`, `openadapt_flow/backends/playwright_backend.py`, `openadapt_flow/recorder.py`, `openadapt_flow/demo_driver.py`, `tests/test_mockmed.py`, `tests/test_recorder.py` |
55
+ | Vision + compiler | Agent B | `openadapt_flow/vision/**`, `openadapt_flow/compiler/**`, `tests/test_vision.py`, `tests/test_compiler.py` |
56
+ | Runtime (ladder/heal/verify) | Agent C | `openadapt_flow/runtime/**`, `tests/test_resolver.py`, `tests/test_replayer.py`, `tests/test_heal.py` |
57
+ | Report, emit, CLI, bench, CI | Agent D | `openadapt_flow/report.py`, `openadapt_flow/bench.py`, `openadapt_flow/emit/**`, `openadapt_flow/__main__.py`, `.github/workflows/ci.yml`, `tests/test_report.py`, `tests/test_emit.py` |
58
+ | E2E integration | Integrator | `tests/e2e/**`, cross-module fixes anywhere |
59
+
60
+ ## Vision API (Agent B implements; Agent C consumes via injection)
61
+
62
+ `openadapt_flow/vision/__init__.py` re-exports:
63
+
64
+ ```python
65
+ class Match(BaseModel):
66
+ point: Point # click/center point, screen coords
67
+ region: Region # matched region, screen coords
68
+ confidence: float
69
+
70
+ # match.py
71
+ def find_template(screen_png: bytes, template_png: bytes, *,
72
+ search_region: Region | None = None,
73
+ scales: tuple[float, ...] = (0.85, 1.0, 1.18),
74
+ threshold: float = 0.82) -> Match | None
75
+
76
+ # ocr.py (rapidocr_onnxruntime; instantiate the engine once, module-level lazy)
77
+ class OcrLine(BaseModel):
78
+ text: str; region: Region; confidence: float
79
+ def ocr(screen_png: bytes, *, region: Region | None = None) -> list[OcrLine]
80
+ def find_text(screen_png: bytes, text: str, *,
81
+ region: Region | None = None, min_ratio: float = 0.8) -> Match | None
82
+ # normalized fuzzy match (difflib ratio on lowercased/stripped text)
83
+
84
+ # hashing.py
85
+ def phash_png(png: bytes, region: Region | None = None) -> str
86
+ def phash_distance(a: str, b: str) -> int
87
+
88
+ # settle.py
89
+ def wait_settled(backend, *, interval_s: float = 0.1,
90
+ stable_frames: int = 2, timeout_s: float = 3.0) -> bytes
91
+ # poll screenshots until N consecutive identical (phash dist 0) or timeout;
92
+ # return the last PNG
93
+ ```
94
+
95
+ ## Compiler (Agent B)
96
+
97
+ `openadapt_flow.compiler.compile_recording(recording_dir, out_bundle_dir, *, name) -> Workflow`
98
+
99
+ Per click event (and `double_click`, compiled identically with action
100
+ DOUBLE_CLICK): crop a template around the click point (target-sized crop,
101
+ e.g. 160x64 clamped to frame, centered on click), OCR the crop for `ocr_text`,
102
+ extract up to 2 landmarks (nearest OCR lines outside the crop, carrying both
103
+ relation/distance and exact `dx_px`/`dy_px` offsets to the click point), set
104
+ `click_point`, `region`. Per type event: TYPE step with `text` or `param` (from
105
+ events.jsonl). Postconditions from the after-frame: pick the largest changed
106
+ region between before/after (cv2.absdiff + threshold + bounding rect),
107
+ REGION_STABLE with its phash; plus TEXT_PRESENT for the most distinctive new
108
+ OCR text (text in after, not in before — compared whitespace-insensitively
109
+ so OCR jitter cannot make permanently visible chrome look "new"; prefer
110
+ longest). Parameterized typed values vary per run and are NEVER asserted in
111
+ any step's postconditions — including downstream steps whose after-frames
112
+ embed the typed value (e.g. a save-confirmation banner). Click target labels
113
+ (any anchor's `ocr_text`) are likewise never asserted: they are mutable
114
+ evidence the resolution ladder heals through under rename drift, not
115
+ invariants. Intent: rule-based `"click '<ocr_text>'"` /
116
+ `"type <param or text preview>"` (VLM annotation is a later enhancement —
117
+ design for it, don't call any API).
118
+
119
+ Also emit a readable Python *rendering* of the workflow (`workflow.py` in the
120
+ bundle, generated, not parsed back) so humans can code-review the automation.
121
+
122
+ ## Runtime (Agent C)
123
+
124
+ ```python
125
+ class Replayer:
126
+ def __init__(self, backend, *, vision=None, grounder=None): ...
127
+ def run(self, workflow: Workflow, *, params: dict[str, str] | None = None,
128
+ bundle_dir: Path, run_dir: Path,
129
+ save_healed_to: Path | None = None) -> RunReport
130
+ ```
131
+
132
+ Parameters not supplied in `params` fall back to the recorded example/default
133
+ values in `workflow.params`, so a bundle replays without any explicit params.
134
+
135
+ Resolution ladder per step with an anchor (record rung + confidence + ms):
136
+ 1. `template` — find_template within anchor.region padded by search_pad
137
+ 2. `template_global` — find_template full frame
138
+ 3. `ocr` — find_text(anchor.ocr_text) full frame
139
+ 4. `geometry` — landmarks: locate landmark text, offset by the exact
140
+ `dx_px`/`dy_px` offsets when recorded, else by relation/distance
141
+ 5. `grounder` — optional injected `Grounder.locate(png, intent) -> Match|None`
142
+ (protocol in `runtime/grounder.py`; ship a `NullGrounder`; an Anthropic
143
+ implementation goes behind the `grounder` extra and is NOT used in tests)
144
+
145
+ Click point = matched region origin + (anchor.click_point - anchor.region
146
+ origin), scaled by match scale. After acting: `wait_settled`, then check
147
+ postconditions (poll until each passes or times out). Postcondition failure →
148
+ re-settle and re-check once → fail the step and abort the run (semantic drift
149
+ halts; the report must name the step and embed before/after screenshots).
150
+
151
+ **Heal:** when a step succeeds via any rung other than `template`, emit a
152
+ HealEvent: new template crop at the resolved region from the live frame,
153
+ updated region/click_point/ocr_text (re-OCR). Apply to the in-memory workflow;
154
+ if `save_healed_to` is set, write the healed bundle (updated workflow.json +
155
+ new crops). Record heal crops + heal.json under `run_dir/heals/<step_id>/`.
156
+ Irreversible steps: if resolution needed a rung below `ocr`, do NOT act — fail
157
+ with a clear "needs human confirmation" error (v0 policy).
158
+
159
+ ## MockMed (Agent A)
160
+
161
+ Static single-page app (`openadapt_flow/mockmed/static/index.html` + app.js +
162
+ styles.css, no external resources, no CSS transitions/animations, font-size
163
+ ≥ 14px). Served by `openadapt_flow.mockmed.server.serve(port=0) -> (url, stop)`
164
+ (threaded http.server). Hash-routed screens:
165
+
166
+ - `#login` — Username, Password fields; "Sign In" button
167
+ - `#tasks` — "Referral Tasks" table, rows with fake patient names
168
+ (obviously fake: "Jane Sample", "Alex Testcase"), reason, priority, an
169
+ "Open" button per row
170
+ - `#patient/<id>` — patient banner, "New Encounter" button, Encounters list
171
+ - `#encounter` — Type chooser as segmented BUTTONS ("Triage" / "Consult" —
172
+ no native <select>), Note textarea, "Save Encounter" button
173
+ - after save → back to patient screen with a banner `Encounter saved — <first
174
+ 40 chars of note>` and the encounter listed
175
+
176
+ Drift modes via query string `?drift=a,b` (applied before render):
177
+ - `theme` — dark palette (breaks template matching for every anchor; OCR
178
+ still works for labeled targets, while unlabeled input-field anchors fall
179
+ through to the geometry rung)
180
+ - `move` — "New Encounter" and "Save Encounter" buttons relocated to the
181
+ opposite side of their container (breaks local template search)
182
+ - `rename` — "Save Encounter"→"Submit Encounter", "Open"→"View" (breaks
183
+ template + OCR; geometry rung must resolve — keep the button in the SAME
184
+ position as default so landmarks/geometry succeed)
185
+ - `modal` — after clicking Save, a blocking "Survey" modal appears instead of
186
+ the banner (semantic drift: replay must FAIL gracefully)
187
+
188
+ `demo_driver.py`: `record_triage_demo(url, out_dir, *, note_text, param_name
189
+ = "note") -> Path` — drives the canonical demo via Playwright locators (record
190
+ time may cheat with selectors; replay never does): login → tasks → Open first
191
+ referral → New Encounter → click "Triage" → click Note field → type note
192
+ (param) → click Save Encounter → done. It performs every action through
193
+ `Recorder` so frames/events are captured (before frame, act, wait settle,
194
+ after frame).
195
+
196
+ `PlaywrightBackend(page)` implements `Backend` (chromium, fixed viewport
197
+ 1280x800, deviceScaleFactor=1). `Recorder(backend, out_dir)` wraps a backend
198
+ with the same action methods plus `type_text(text, param=None)` and
199
+ `finish() -> recording dir`.
200
+
201
+ ## Report / emit / CLI / bench / CI (Agent D)
202
+
203
+ - `report.py`: `render_run_report(run_dir) -> Path` — REPORT.md in run_dir:
204
+ outcome, per-step table (intent, rung, confidence, ms, heal?), embedded
205
+ relative-path images for key steps and every heal (before/after side by
206
+ side), rung histogram, totals (ms, model calls, est cost).
207
+ `render_bench_report(bench.json, out) -> Path`.
208
+ - `bench.py`: `run_bench(workflow_bundle, backend_factory, n) -> dict` —
209
+ replay N times; success rate, p50/p95 total ms, rung histogram, model calls
210
+ (0 in v0), cost 0; serialize bench.json.
211
+ - `emit/skill.py`: workflow bundle → `SKILL.md` folder (Agent Skills format:
212
+ name, description, when-to-use, `openadapt-flow replay bundle --param k=v`
213
+ invocation). The bundle is copied into the skill folder (`bundle/`) so the
214
+ artifact is self-contained and portable. `emit/mcp_tool.py`: generate a
215
+ standalone `server.py` exposing the workflow as an MCP tool (string
216
+ template; must `ast.parse`; do not import mcp at generation time); the
217
+ bundle is copied next to `server.py` and referenced relative to
218
+ `__file__`, never by an emitting-machine absolute path.
219
+ - `__main__.py` CLI: `demo-record`, `compile`, `replay`, `bench`, `emit-skill`,
220
+ `emit-mcp` (thin wrappers over the module APIs above). `replay --run-dir`
221
+ is optional and defaults to `runs/replay-<UTC timestamp>`.
222
+ - CI (`.github/workflows/ci.yml`): ubuntu-latest, py3.12,
223
+ `pip install -e .[dev]`, `playwright install --with-deps chromium`,
224
+ `pytest -q --basetemp=runs/ci` (temp dirs pinned inside the workspace so
225
+ run artifacts survive), upload `runs/**/REPORT.md` + PNGs as artifacts.
226
+
227
+ ## Test policy
228
+
229
+ - Unit tests per module, cross-module deps faked/injected (Agent C must not
230
+ import Agent B's vision in unit tests — inject fakes; the integrator wires
231
+ real parts in `tests/e2e/`).
232
+ - Deterministic: no sleeps besides settle polling; server on ephemeral port.
233
+ - No network beyond localhost. No API keys required anywhere in tests.
234
+ - E2E matrix (integrator): baseline ×3 all-template zero-heal; theme / move /
235
+ rename each succeed WITH heals then replay-healed all-template; modal fails
236
+ gracefully naming the step; params substitution verified via banner OCR
237
+ with a note value DIFFERENT from the recorded one (the identity case
238
+ cannot distinguish substitution from replaying the baked-in literal); the
239
+ irreversible-step risk gate exercised end-to-end (step marked irreversible
240
+ + drift forcing a below-ocr rung must refuse to act).
241
+
242
+ ## Repo policies
243
+
244
+ - Fake data only; no real patient/clinic/person names anywhere.
245
+ - No references to customers or design partners in this repo.
246
+ - Conventional commits; feature branches; never push to main.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 OpenAdapt.AI (MLDSAI Inc.)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,138 @@
1
+ Metadata-Version: 2.4
2
+ Name: openadapt-flow
3
+ Version: 0.1.0
4
+ Summary: Demonstration compiler: record a desktop workflow once, compile it into a deterministic, self-healing, locally-run script.
5
+ Author: OpenAdapt.AI
6
+ License: MIT
7
+ License-File: LICENSE
8
+ Requires-Python: >=3.10
9
+ Requires-Dist: imagehash>=4.3
10
+ Requires-Dist: numpy>=1.26
11
+ Requires-Dist: opencv-python-headless>=4.9
12
+ Requires-Dist: pillow>=10.0
13
+ Requires-Dist: playwright>=1.44
14
+ Requires-Dist: pydantic>=2.5
15
+ Requires-Dist: rapidocr-onnxruntime>=1.3
16
+ Provides-Extra: dev
17
+ Requires-Dist: pytest-timeout>=2.2; extra == 'dev'
18
+ Requires-Dist: pytest>=8; extra == 'dev'
19
+ Provides-Extra: grounder
20
+ Requires-Dist: anthropic>=0.40; extra == 'grounder'
21
+ Description-Content-Type: text/markdown
22
+
23
+ # openadapt-flow
24
+
25
+ Record a GUI workflow once. Replay it deterministically, locally, for free.
26
+ A model only touches the script to repair it.
27
+
28
+ Computer-use agents re-reason on every run: screenshot, think, click, repeat.
29
+ That's the right shape for a task nobody has automated before, and the wrong
30
+ one for the 500th referral this month. openadapt-flow makes the other bet. It
31
+ compiles a human demonstration into a plain, reviewable script with vision
32
+ anchors and per-step assertions. A healthy script replays in milliseconds with
33
+ zero model calls. When the UI shifts, a fallback ladder still finds the
34
+ target, and the fix gets written back into the script as a diff you can
35
+ review. The automation gets cheaper over time instead of billing you to
36
+ re-think the same eleven clicks.
37
+
38
+ ## How it works
39
+
40
+ A recording captures screenshots and input events while a person does the
41
+ task once. The compiler turns that into a workflow bundle: for every click it
42
+ stores a template crop, the OCR'd label, and offsets to nearby landmark text,
43
+ then derives postconditions from what actually changed on screen after the
44
+ action. Typed values can be tagged as parameters, so one demo yields a
45
+ reusable, parameterized automation plus a generated `workflow.py` rendering a
46
+ human can code-review.
47
+
48
+ At replay time, each step walks a resolution ladder:
49
+
50
+ 1. template match near the recorded location (milliseconds, no model)
51
+ 2. template match across the whole screen
52
+ 3. OCR match on the label
53
+ 4. geometry from surrounding landmarks
54
+ 5. optionally, a grounding model (local or API)
55
+
56
+ Resolving below the top rung triggers a heal: fresh crop, updated
57
+ coordinates, re-read label, saved as a reviewable change to the bundle.
58
+ Postconditions check that each step actually worked. If the screen stops
59
+ matching expectations entirely (a surprise dialog, a changed process), the
60
+ run halts with an illustrated report rather than guessing. Steps tagged
61
+ irreversible refuse to act on low-confidence resolutions at all.
62
+
63
+ The runtime is vision-only: PNG in, clicks and keys out, behind a small
64
+ `Backend` protocol. The reference backend drives a headless browser, which is
65
+ what lets the entire record→compile→replay→heal loop run in CI with no OS
66
+ permissions. Native desktop and RDP backends are planned adapters behind the
67
+ same protocol.
68
+
69
+ ## What it looks like
70
+
71
+ One demonstration was recorded on the light UI (left). The compiled workflow
72
+ then ran unattended against a dark theme it had never seen (right): all 11
73
+ steps succeeded, every anchor re-resolved through the OCR or geometry rungs,
74
+ and all 8 anchors healed themselves back onto the fast path. No human
75
+ involved.
76
+
77
+ | Recorded UI → baseline replay (all `template` rung, 0 model calls) | Theme-drifted UI → replay + self-heal (8/8 anchors healed) |
78
+ | --- | --- |
79
+ | ![Baseline replay final screen](docs/showcase/baseline-run/steps/step_010_after.png) | ![Theme-drift replay final screen](docs/showcase/theme-drift-run/steps/step_010_after.png) |
80
+
81
+ Every replay generates an illustrated run report:
82
+ [baseline](docs/showcase/baseline-run/REPORT.md) ·
83
+ [theme drift with heals](docs/showcase/theme-drift-run/REPORT.md).
84
+
85
+ ## Quickstart
86
+
87
+ ```bash
88
+ pip install -e '.[dev]'
89
+ playwright install chromium
90
+
91
+ # 1. Record a demonstration (drives the bundled MockMed demo app):
92
+ openadapt-flow demo-record --out /tmp/rec
93
+
94
+ # 2. Compile it into a workflow bundle:
95
+ openadapt-flow compile /tmp/rec --out /tmp/bundle --name triage-demo
96
+
97
+ # 3. Replay it — deterministic, local, zero model calls:
98
+ openadapt-flow replay /tmp/bundle --run-dir /tmp/run
99
+ open /tmp/run/REPORT.md
100
+
101
+ # 4. Drift the UI (dark theme it has never seen) and watch it heal:
102
+ openadapt-flow replay /tmp/bundle --drift theme \
103
+ --run-dir /tmp/run-drift --save-healed-to /tmp/healed
104
+ open /tmp/run-drift/REPORT.md
105
+ ```
106
+
107
+ Steps 3–4 serve the bundled MockMed app automatically. To replay against your
108
+ own running app, pass `--url` (parameters default to the values recorded
109
+ during the demo; `--param` overrides them):
110
+
111
+ ```bash
112
+ openadapt-flow replay /tmp/bundle --url <APP_URL> \
113
+ --run-dir /tmp/run --param note="Booking 3 months"
114
+ ```
115
+
116
+ `bench` replays a bundle N times and aggregates success rate, latency
117
+ percentiles, and cost: `openadapt-flow bench /tmp/bundle --n 10 --run-root
118
+ /tmp/bench`.
119
+
120
+ The test suite includes the full drift matrix (theme, moved buttons, renamed
121
+ buttons, surprise modal) end to end:
122
+
123
+ ```bash
124
+ pytest -q
125
+ ```
126
+
127
+ ## Status
128
+
129
+ v0. The record→compile→replay→heal loop runs end to end against MockMed, the
130
+ bundled mock clinical app, under four kinds of deliberate UI drift. The suite
131
+ (124 tests) runs headlessly in GitHub Actions and uploads run reports as
132
+ build artifacts. Compiled workflows can also be emitted as Agent Skills
133
+ (`SKILL.md`) and MCP servers, so other agents can invoke them.
134
+
135
+ Solid for the reference browser backend; everything else should be treated as
136
+ a design seam, not a finished feature. See `DESIGN.md` for module contracts
137
+ and `docs/L1_INTEGRATION.md` for feeding layered clinical-data platforms
138
+ (L1→L2 acquisition contracts). MIT license.
@@ -0,0 +1,116 @@
1
+ # openadapt-flow
2
+
3
+ Record a GUI workflow once. Replay it deterministically, locally, for free.
4
+ A model only touches the script to repair it.
5
+
6
+ Computer-use agents re-reason on every run: screenshot, think, click, repeat.
7
+ That's the right shape for a task nobody has automated before, and the wrong
8
+ one for the 500th referral this month. openadapt-flow makes the other bet. It
9
+ compiles a human demonstration into a plain, reviewable script with vision
10
+ anchors and per-step assertions. A healthy script replays in milliseconds with
11
+ zero model calls. When the UI shifts, a fallback ladder still finds the
12
+ target, and the fix gets written back into the script as a diff you can
13
+ review. The automation gets cheaper over time instead of billing you to
14
+ re-think the same eleven clicks.
15
+
16
+ ## How it works
17
+
18
+ A recording captures screenshots and input events while a person does the
19
+ task once. The compiler turns that into a workflow bundle: for every click it
20
+ stores a template crop, the OCR'd label, and offsets to nearby landmark text,
21
+ then derives postconditions from what actually changed on screen after the
22
+ action. Typed values can be tagged as parameters, so one demo yields a
23
+ reusable, parameterized automation plus a generated `workflow.py` rendering a
24
+ human can code-review.
25
+
26
+ At replay time, each step walks a resolution ladder:
27
+
28
+ 1. template match near the recorded location (milliseconds, no model)
29
+ 2. template match across the whole screen
30
+ 3. OCR match on the label
31
+ 4. geometry from surrounding landmarks
32
+ 5. optionally, a grounding model (local or API)
33
+
34
+ Resolving below the top rung triggers a heal: fresh crop, updated
35
+ coordinates, re-read label, saved as a reviewable change to the bundle.
36
+ Postconditions check that each step actually worked. If the screen stops
37
+ matching expectations entirely (a surprise dialog, a changed process), the
38
+ run halts with an illustrated report rather than guessing. Steps tagged
39
+ irreversible refuse to act on low-confidence resolutions at all.
40
+
41
+ The runtime is vision-only: PNG in, clicks and keys out, behind a small
42
+ `Backend` protocol. The reference backend drives a headless browser, which is
43
+ what lets the entire record→compile→replay→heal loop run in CI with no OS
44
+ permissions. Native desktop and RDP backends are planned adapters behind the
45
+ same protocol.
46
+
47
+ ## What it looks like
48
+
49
+ One demonstration was recorded on the light UI (left). The compiled workflow
50
+ then ran unattended against a dark theme it had never seen (right): all 11
51
+ steps succeeded, every anchor re-resolved through the OCR or geometry rungs,
52
+ and all 8 anchors healed themselves back onto the fast path. No human
53
+ involved.
54
+
55
+ | Recorded UI → baseline replay (all `template` rung, 0 model calls) | Theme-drifted UI → replay + self-heal (8/8 anchors healed) |
56
+ | --- | --- |
57
+ | ![Baseline replay final screen](docs/showcase/baseline-run/steps/step_010_after.png) | ![Theme-drift replay final screen](docs/showcase/theme-drift-run/steps/step_010_after.png) |
58
+
59
+ Every replay generates an illustrated run report:
60
+ [baseline](docs/showcase/baseline-run/REPORT.md) ·
61
+ [theme drift with heals](docs/showcase/theme-drift-run/REPORT.md).
62
+
63
+ ## Quickstart
64
+
65
+ ```bash
66
+ pip install -e '.[dev]'
67
+ playwright install chromium
68
+
69
+ # 1. Record a demonstration (drives the bundled MockMed demo app):
70
+ openadapt-flow demo-record --out /tmp/rec
71
+
72
+ # 2. Compile it into a workflow bundle:
73
+ openadapt-flow compile /tmp/rec --out /tmp/bundle --name triage-demo
74
+
75
+ # 3. Replay it — deterministic, local, zero model calls:
76
+ openadapt-flow replay /tmp/bundle --run-dir /tmp/run
77
+ open /tmp/run/REPORT.md
78
+
79
+ # 4. Drift the UI (dark theme it has never seen) and watch it heal:
80
+ openadapt-flow replay /tmp/bundle --drift theme \
81
+ --run-dir /tmp/run-drift --save-healed-to /tmp/healed
82
+ open /tmp/run-drift/REPORT.md
83
+ ```
84
+
85
+ Steps 3–4 serve the bundled MockMed app automatically. To replay against your
86
+ own running app, pass `--url` (parameters default to the values recorded
87
+ during the demo; `--param` overrides them):
88
+
89
+ ```bash
90
+ openadapt-flow replay /tmp/bundle --url <APP_URL> \
91
+ --run-dir /tmp/run --param note="Booking 3 months"
92
+ ```
93
+
94
+ `bench` replays a bundle N times and aggregates success rate, latency
95
+ percentiles, and cost: `openadapt-flow bench /tmp/bundle --n 10 --run-root
96
+ /tmp/bench`.
97
+
98
+ The test suite includes the full drift matrix (theme, moved buttons, renamed
99
+ buttons, surprise modal) end to end:
100
+
101
+ ```bash
102
+ pytest -q
103
+ ```
104
+
105
+ ## Status
106
+
107
+ v0. The record→compile→replay→heal loop runs end to end against MockMed, the
108
+ bundled mock clinical app, under four kinds of deliberate UI drift. The suite
109
+ (124 tests) runs headlessly in GitHub Actions and uploads run reports as
110
+ build artifacts. Compiled workflows can also be emitted as Agent Skills
111
+ (`SKILL.md`) and MCP servers, so other agents can invoke them.
112
+
113
+ Solid for the reference browser backend; everything else should be treated as
114
+ a design seam, not a finished feature. See `DESIGN.md` for module contracts
115
+ and `docs/L1_INTEGRATION.md` for feeding layered clinical-data platforms
116
+ (L1→L2 acquisition contracts). MIT license.