diff --git a/CHANGELOG.md b/CHANGELOG.md index c8ef489a6..deeea7fbe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,26 @@ it shipped into a `## [x.y.z] - date` section of their own; the tag's ## Unreleased +### Changed + +- `is_package_name` (the gate behind `AC_add_package_to_executor` and + `JE_AUTOCONTROL_ALLOWED_PACKAGES`) no longer accepts a name with a trailing + newline. The pattern it replaced let one through because `$` matches before + a final newline; such a name could never be imported. +- `agent_trace.operation` records an `error` span when its block is left by a + `KeyboardInterrupt`, `SystemExit` or `GeneratorExit`, where it used to record + nothing. The exception propagates as before. +- The signaling server logs a traceback when the config store fails; the HTTP + responses are unchanged. + +### Security + +- `examples/18_slack_daily_report.py` resolves both paths it writes (the HTML + report and the PDF) with `os.path.realpath` and refuses one that leaves the + report directory. The PDF path was not checked before. +- The REST dashboard reports a failed refresh on its panels instead of leaving + the promise rejection unhandled. + ## [1.0.0] - 2026-10-09 The first release under semantic versioning. `docs/API_LIFECYCLE.md` says what diff --git a/Progress.md b/Progress.md index 4fb9183ca..dcfaffc5e 100644 --- a/Progress.md +++ b/Progress.md @@ -329,6 +329,7 @@ U-20261009-18 的每一個裝置面呼叫都只對假的 ADB/uiautomator2/WD 這台機器沒有 free-threaded 直譯器:驗證用的是模擬繼承與真的 `-X thread_inherit_context=1`(3.14.7,GIL 開著);`free-threaded-scope` job(3.14t)是第一次真的跑。 - **能力探測**:Windows 的鎖定工作站、session 0、低完整性與擷取失敗分支只用假資料判斷;macOS 的三個 preflight 呼叫只對假物件跑過, pyobjc 的 `Quartz` 有沒有 `CGPreflightListenEventAccess` 未知(沒有的話 `recording`/`stop_shortcut` 回 `unknown`)。 +- **SonarCloud/Codacy 的抑制還沒被下一次分析確認**(U-20261009-42):`web_viewer/index.html` 裡內嵌 script 的四個 `// NOSONAR`(S9382,同檔先前的 NOSONAR 就沒被認)、`run_history/history_store.py` 分兩行寫的 `nosemgrep`、以及 `examples/18_slack_daily_report.py` 的 realpath 檢查是否被 Sonar 認成 sanitizer(S2083)。分析跑完後還開著的:內嵌 script 搬到 `.js` 檔,其餘在儀表板標 accepted。 - **Windows 的版面鍵名**(`slash` 等 8 個)只在美式配置上實際呼叫過。 - **狀態機 `on_enter` 的單一動作寫法**以前一直被拒絕、從沒執行過(已修,U-20261009-24):依賴「它不會跑」的既有狀態機現在會跑它。 diff --git a/architecture_explore.md b/architecture_explore.md index ec3742dae..208b444f8 100644 --- a/architecture_explore.md +++ b/architecture_explore.md @@ -20,7 +20,7 @@ iOS(WebDriverAgent)。核心能力是滑鼠/鍵盤控制、影像辨識、 | 指標 | 數值 | | --- | ---: | | Python 模組總數(含周邊子專案) | 1,177 | -| 程式碼總行數 | 188,730 | +| 程式碼總行數 | 188,836 | | `je_auto_control/utils/` 子套件數 | 311 | | `AC_*` 動作指令數(`known_commands()` 實測) | 852 | | 套件門面 `__all__` 公開名稱數 | 1,244 | @@ -156,7 +156,7 @@ socket server 有 8 MiB 讀取上限與 30 秒 handler timeout。 | --- | ---: | --- | | `je_auto_control/__init__.py` | 2,156 | **套件門面**。集中匯入並再匯出 1,426 個公開名稱,以功能區塊註解分段(callback/exception/executor/a11y/vision/clipboard…)。 | | `je_auto_control/__main__.py` | 98 | 舊版 argparse 進入點:`-e` 執行單檔、`-d` 執行整個目錄、`--execute_str` 執行 JSON 字串、`-c` 建立專案。 | -| `je_auto_control/cli.py` | 422 | **主 CLI**(`je_auto_control` console script)。子命令:`run`(含 `--var`/`--dry-run`)、`validate`/`lint`、`list-commands`、`fmt`、`record`、`codegen`、`failure-bundle`、`list-jobs`、`start-server`、`start-rest`、`version`。所有子命令延遲匯入,確保不碰 Qt。 | +| `je_auto_control/cli.py` | 416 | **主 CLI**(`je_auto_control` console script)。子命令:`run`(含 `--var`/`--dry-run`)、`validate`/`lint`、`list-commands`、`fmt`、`record`、`codegen`、`failure-bundle`、`list-jobs`、`start-server`、`start-rest`、`version`。所有子命令延遲匯入,確保不碰 Qt。 | | `je_auto_control/api/__init__.py` | 22 | 版本化整合進入點。 | | `je_auto_control/api/core.py` | 19 | **穩定無頭 API 門面**:只暴露 `execute_action`、`execute_action_with_vars`、`generate_code`、`run_diagnostics`、`create_failure_bundle`、`failure_bundle_on_error`、`FailureBundleOptions`。mypy 型別契約以此為起點,現已擴到整包(見「設定基線」)。 | | `je_auto_control/utils/cli_output.py` | 44 | 本套件命令列工具與 stdio 伺服器的標準串流:不論碼頁一律 UTF-8。 | @@ -187,13 +187,13 @@ socket server 有 8 MiB 讀取上限與 30 秒 handler timeout。 ### 5.3 平台後端 -#### Windows(`windows/`,24 檔/2,266 行) +#### Windows(`windows/`,24 檔/2,265 行) | 模組 | 行數 | 職責 | | --- | ---: | --- | | `core/utils/win32_ctype_input.py` | 76 | `SendInput` 的 ctypes 結構定義與送出。 | | `core/utils/win32_vk.py` | 205 | Windows 虛擬鍵碼對照表。 | -| `core/utils/win32_keypress_check.py` | 22 | `GetAsyncKeyState` 按鍵狀態查詢。 | +| `core/utils/win32_keypress_check.py` | 21 | `GetAsyncKeyState` 按鍵狀態查詢。 | | `mouse/win32_ctype_mouse_control.py` | 220 | 滑鼠事件產生(含多螢幕絕對座標換算)。 | | `keyboard/win32_ctype_keyboard_control.py` | 98 | 鍵盤事件產生。 | | `record/win32_input_hook.py` | 253 | 單一一組低階鍵鼠 hook(`WH_KEYBOARD_LL`/`WH_MOUSE_LL`)+訊息迴圈,產生帶時間戳的事件時間軸;停止時以 `PostThreadMessageW(WM_QUIT)` 收掉執行緒,不會每錄一次就漏一條。 | @@ -260,7 +260,7 @@ socket server 有 8 MiB 讀取上限與 30 秒 handler timeout。 | `android/adb_client.py` | 342 | `adb` CLI 的薄封裝。 | | `android/client.py` | 129 | `uiautomator2.Device` 的延遲封裝。 | | `android/find.py` | 108 | uiautomator2 widget 樹的元素查詢。 | -| `ios/client.py` | 124 | `facebook-wda`(WebDriverAgent)封裝。 | +| `ios/client.py` | 126 | `facebook-wda`(WebDriverAgent)封裝。 | | `ios/find.py` | 94 | XCUITest 無障礙查詢。 | | `ios/input.py` | 83 | iOS 觸控與按鍵原語。 | | `ios/screen.py` | 85 | iOS 裝置螢幕擷取與尺寸。 | @@ -272,14 +272,14 @@ socket server 有 8 MiB 讀取上限與 30 秒 handler timeout。 ### 5.4.1 執行引擎與腳本資產 -> 25 個套件、約 18,499 行。 +> 25 個套件、約 18,511 行。 | 模組 | 行數 | 職責 | | --- | ---: | --- | | `utils/action_lint/` | 429 | action 檔 linter 與 JSON Schema 產生器(CI 用 `python -m` 進入點) | -| `utils/action_signing/` | 1,080 | action 檔 HMAC-SHA256 簽章與 Fernet 加密,`execute_files` 會強制驗簽 | +| `utils/action_signing/` | 1,079 | action 檔 HMAC-SHA256 簽章與 Fernet 加密,`execute_files` 會強制驗簽 | | `utils/checkpoint/` | 129 | 流程檢查點與續跑,讓長 action list 具持久性 | -| `utils/codegen/` | 1,106 | 由 action list 產生可執行的 pytest / python / robot 測試碼 | +| `utils/codegen/` | 1,115 | 由 action list 產生可執行的 pytest / python / robot 測試碼 | | `utils/dag/` | 564 | 跨主機 DAG 編排器(圖模型 + runner) | | `utils/decision_table/` | 112 | DMN 風格決策表:規則 + 命中策略,把分支外部化 | | `utils/deterministic/` | 116 | 決定性執行控制:固定亂數種子 + 凍結時鐘 | @@ -288,7 +288,7 @@ socket server 有 8 MiB 讀取上限與 30 秒 handler timeout。 | `utils/flow_debugger/` | 166 | action list 的單步除錯器與追蹤器 | | `utils/input_macro/` | 469 | 定時輸入事件:錄製結果的整形(`timeline`/`InputRecorder`,Windows 與 macOS 共用)、重播與宣告式輸入序列 DSL | | `utils/json/` | 99 | action JSON 檔讀寫與正規化格式化(`fmt --check` 的後端) | -| `utils/json_store/` | 278 | JSON 字典檔持久化的共用小工具(內部管線) | +| `utils/json_store/` | 281 | JSON 字典檔持久化的共用小工具(內部管線) | | `utils/loop_guard/` | 158 | 機械式卡死迴圈偵測(agent loop 用) | | `utils/plugin_loader/` | 147 | 掃描外部 Python 外掛目錄並註冊其 `AC_` callable | | `utils/plugin_sdk/` | 80 | 外掛 SDK:透過 entry points 發佈/載入第三方 `AC_*` 指令 | @@ -300,11 +300,11 @@ socket server 有 8 MiB 讀取上限與 30 秒 handler timeout。 | `utils/state_machine/` | 294 | 宣告式有限狀態機驅動 action JSON | | `utils/stubs/` | 311 | 為 `AC_*` 指令面產生型別 stub | | `utils/test_record/` | 70 | 全域測試紀錄單例,記錄每個動作的參數與例外 | -| `utils/work_queue/` | 280 | 交易式工作佇列(dispatcher/performer),支撐大量批次執行 | +| `utils/work_queue/` | 281 | 交易式工作佇列(dispatcher/performer),支撐大量批次執行 | ### 5.4.2 框架基礎設施 -> 14 個套件、約 3,277 行。 +> 14 個套件、約 3,281 行。 | 模組 | 行數 | 職責 | | --- | ---: | --- | @@ -317,7 +317,7 @@ socket server 有 8 MiB 讀取上限與 30 秒 handler timeout。 | `utils/failure_bundle/` | 229 | 可攜、已遮蔽的失敗診斷 ZIP(截圖 + 診斷 + log 尾段) | | `utils/file_process/` | 40 | 目錄檔案列舉(`execute_dir` 的後端) | | `utils/logging/` | 168 | `autocontrol_logger` 單例 + 家目錄共用記錄檔 handler(`JE_AUTOCONTROL_LOG_FILE` 可改) | -| `utils/package_manager/` | 189 | 動態載入套件並把 executor 注入其中;載入前先過套件閘門(允許清單加上只能從 Python 呼叫的開關,工作區 X-12) | +| `utils/package_manager/` | 193 | 動態載入套件並把 executor 注入其中;載入前先過套件閘門(允許清單加上只能從 Python 呼叫的開關,工作區 X-12) | | `utils/path_guard/` | 231 | 命令列傳入路徑的正規化與邊界檢查(防路徑穿越) | | `utils/platform_id/` | 62 | 作業系統家族的單一判定點。`sys.platform` 原本在一百多處跟字面清單比對,而那些清單都沒有 BSD;`is_x11_unix()` 問的是「這是不是 X11 unix」,這才是守衛一直想問的問題 | | `utils/shell_process/` | 279 | `ShellManager`:以 argv list 執行外部命令(禁用 `shell=True`) | @@ -372,11 +372,11 @@ socket server 有 8 MiB 讀取上限與 30 秒 handler timeout。 ### 5.4.5 影像辨識與畫面分析 -> 37 個套件、約 6,397 行。 +> 37 個套件、約 6,398 行。 | 模組 | 行數 | 職責 | | --- | ---: | --- | -| `utils/annotate/` | 122 | 截圖標註:畫框、highlight、箭頭、標籤 | +| `utils/annotate/` | 123 | 截圖標註:畫框、highlight、箭頭、標籤 | | `utils/barcode/` | 59 | 一維條碼(EAN/UPC)解碼,解碼器可注入 | | `utils/color_match/` | 151 | 在 HSV 通道上做顏色感知的樣板比對 | | `utils/color_region/` | 107 | 以顏色定位畫面區域(遮罩 + 連通元件) | @@ -442,12 +442,12 @@ socket server 有 8 MiB 讀取上限與 30 秒 handler timeout。 ### 5.4.7 無障礙樹與原生控制項 -> 16 個套件、約 4,825 行。 +> 16 個套件、約 4,827 行。 | 模組 | 行數 | 職責 | | --- | ---: | --- | | `utils/a11y_audit/` | 362 | 以無障礙樹 + OCR 進行無障礙與 i18n 稽核 | -| `utils/accessibility/` | 3,290 | 跨平台無障礙樹定位與錄製;Windows UIA/macOS AX/null 三後端。支援限定視窗(換搜尋起點,不是過濾)、逐節點可中斷走訪、`IUIAutomation2` 連線逾時、名稱子字串比對與排序、`control_get_state` 一次讀完值/勾選/選取/數值(密碼欄位不回內容) | +| `utils/accessibility/` | 3,292 | 跨平台無障礙樹定位與錄製;Windows UIA/macOS AX/null 三後端。支援限定視窗(換搜尋起點,不是過濾)、逐節點可中斷走訪、`IUIAutomation2` 連線逾時、名稱子字串比對與排序、`control_get_state` 一次讀完值/勾選/選取/數值(密碼欄位不回內容) | | `utils/ax_events/` | 35 | 反應式 UIA 事件等待(focus-changed) | | `utils/ax_props/` | 44 | 讀取豐富 UIA 屬性(enabled/offscreen/help/status/快捷鍵) | | `utils/ax_text/` | 102 | 透過 UIA TextPattern 取得原生文字(讀取/尋找/選取/屬性) | @@ -465,7 +465,7 @@ socket server 有 8 MiB 讀取上限與 30 秒 handler timeout。 ### 5.4.8 元素定位、自我修復與智慧等待 -> 23 個套件、約 6,308 行。 +> 23 個套件、約 6,311 行。 | 模組 | 行數 | 職責 | | --- | ---: | --- | @@ -488,41 +488,41 @@ socket server 有 8 MiB 讀取上限與 30 秒 handler timeout。 | `utils/observation_delta/` | 122 | token 預算內的觀察差異:兩個 UI 影格之間變了什麼 | | `utils/screen_state/` | 191 | 語義畫面狀態:快照/差異與結構化畫面描述 | | `utils/scroll_find/` | 103 | 捲動直到目標影像/文字可見 | -| `utils/self_healing/` | 2,219 | 自癒定位器:先影像樣板、失敗改用 VLM,並留稽核記錄 | +| `utils/self_healing/` | 2,233 | 自癒定位器:先影像樣板、失敗改用 VLM,並留稽核記錄 | | `utils/semantic_recording/` | 498 | 為錄製內容加上語義錨點,支援換機重播與自癒重播 | | `utils/settle_detector/` | 79 | 以純函式介面判定 UI 是否已靜止 | -| `utils/smart_waits/` | 674 | 智慧等待:以影格差異取代 `time.sleep` | +| `utils/smart_waits/` | 663 | 智慧等待:以影格差異取代 `time.sleep` | ### 5.4.9 AI / Agent / LLM -> 13 個套件、約 25,906 行。 +> 13 個套件、約 25,909 行。 | 模組 | 行數 | 職責 | | --- | ---: | --- | | `utils/a2a/` | 92 | A2A(agent-to-agent)agent card 產生 | -| `utils/agent/` | 2,251 | 閉環 Computer-Use Agent 主迴圈 + Anthropic/OpenAI/Computer-Use 三後端 | +| `utils/agent/` | 2,252 | 閉環 Computer-Use Agent 主迴圈 + Anthropic/OpenAI/Computer-Use 三後端 | | `utils/agent_memory/` | 166 | agent 的持久化情節記憶(goal → trajectory → outcome) | | `utils/agent_replay/` | 67 | 可攜的 agent 軌跡追蹤(記錄 observation→action 並重播) | | `utils/agent_trace/` | 172 | agent 可觀測性:OpenTelemetry GenAI 慣例的 LLM span | -| `utils/cost_telemetry/` | 345 | 每次呼叫的 LLM 成本遙測:token 數 + 估算美金 | +| `utils/cost_telemetry/` | 347 | 每次呼叫的 LLM 成本遙測:token 數 + 估算美金 | | `utils/cua_action/` | 204 | 標準化 computer-use 動作結構(Anthropic/OpenAI → `AC_*`) | -| `utils/llm/` | 387 | 自然語言 → action list 規劃器 + Anthropic/null 後端 | +| `utils/llm/` | 384 | 自然語言 → action list 規劃器 + Anthropic/null 後端 | | `utils/mcp_registry/` | 97 | MCP registry `server.json` 資訊清單產生(可被發現) | -| `utils/mcp_server/` | 21,218 | **無頭 MCP 伺服器**(16K LOC,預設註冊 757 個工具=738 個 `ac_*` + 19 個別名):stdio + HTTP 傳輸、工具工廠與處理器、資源、prompt、稽核、限流、外掛熱重載 | +| `utils/mcp_server/` | 21,219 | **無頭 MCP 伺服器**(16K LOC,預設註冊 757 個工具=738 個 `ac_*` + 19 個別名):stdio + HTTP 傳輸、工具工廠與處理器、資源、prompt、稽核、限流、外掛熱重載 | | `utils/tool_use_schema/` | 195 | 把 `AC_*` 指令匯出成 Claude/OpenAI 的 tool-use schema | | `utils/trajectory_eval/` | 132 | agent 軌跡評估:依評分規準為一次執行打分 | -| `utils/vision/` | 580 | VLM 元素定位器(依描述找元素)+ Anthropic/OpenAI/null 後端 | +| `utils/vision/` | 582 | VLM 元素定位器(依描述找元素)+ Anthropic/OpenAI/null 後端 | ### 5.4.10 遠端桌面與 USB -> 6 個套件、約 24,961 行。 +> 6 個套件、約 24,960 行。 | 模組 | 行數 | 職責 | | --- | ---: | --- | -| `utils/admin/` | 418 | 多主機管理主控台:平行輪詢 N 個 AutoControl REST 端點 | -| `utils/config_sync/` | 4,096 | 透過訊令伺服器做跨機器設定同步 | +| `utils/admin/` | 417 | 多主機管理主控台:平行輪詢 N 個 AutoControl REST 端點 | +| `utils/config_sync/` | 4,097 | 透過訊令伺服器做跨機器設定同步 | | `utils/device_matrix/` | 203 | 行動裝置矩陣:同一 action list 於多台裝置平行執行 | -| `utils/remote_desktop/` | 14,179 | **遠端桌面子系統**(56 檔/11.7K LOC):TCP/WebSocket/WebRTC 三條傳輸路徑、主機與檢視端、訊令伺服器、TURN/中繼、多檢視者、錄影、信任清單、TOTP、稽核鏈 | +| `utils/remote_desktop/` | 14,178 | **遠端桌面子系統**(56 檔/11.7K LOC):TCP/WebSocket/WebRTC 三條傳輸路徑、主機與檢視端、訊令伺服器、TURN/中繼、多檢視者、錄影、信任清單、TOTP、稽核鏈 | | `utils/usb/` | 5,057 | 跨平台 USB 列舉/熱插拔/裝置直通(WinUSB、IOKit、libusb 後端 + ACL + WebRTC DataChannel 通道) | | `utils/usbip/` | 1,008 | USB/IP 線路協定主機端(協定封包、TCP 伺服器、libusb URB 後端) | @@ -540,13 +540,13 @@ socket server 有 8 MiB 讀取上限與 30 秒 handler timeout。 ### 5.4.11 伺服器、網路協定與外部整合 -> 24 個套件、約 7,411 行。 +> 24 個套件、約 7,413 行。 | 模組 | 行數 | 職責 | | --- | ---: | --- | | `utils/acme_v2/` | 617 | 完整 ACME v2 用戶端(RFC 8555),不依賴 certbot | | `utils/chatops/` | 928 | Chat-ops bot:接收 Slack/Discord/webhook 的 slash 指令並路由到動作 | -| `utils/cookie_jar/` | 191 | RFC 6265 cookie jar | +| `utils/cookie_jar/` | 192 | RFC 6265 cookie jar | | `utils/email_send/` | 118 | SMTP 寄信(email 觸發器的發送端搭檔) | | `utils/events/` | 106 | 對外 CloudEvents 發送(執行生命週期事件) | | `utils/http_cassette/` | 200 | 錄製/重播 HTTP 互動,做離線決定性 API 測試 | @@ -554,7 +554,7 @@ socket server 有 8 MiB 讀取上限與 30 秒 handler timeout。 | `utils/http_conditional/` | 148 | 條件式 HTTP 請求與快取驗證器 | | `utils/http_content/` | 158 | HTTP 內容協商與回應解壓縮 | | `utils/http_problem/` | 122 | RFC 9457 problem+json 解析 | -| `utils/jwt/` | 270 | JWT(HMAC 家族)編碼、解碼與 claim 驗證 | +| `utils/jwt/` | 271 | JWT(HMAC 家族)編碼、解碼與 claim 驗證 | | `utils/link_header/` | 176 | RFC 8288 Link header 解析與分頁 | | `utils/multipart/` | 224 | multipart/form-data 建構與解析 | | `utils/notify/` | 106 | 跨平台桌面通知 | @@ -571,7 +571,7 @@ socket server 有 8 MiB 讀取上限與 30 秒 handler timeout。 ### 5.4.12 報表、可觀測性與測試治理 -> 34 個套件、約 7,795 行。 +> 34 個套件、約 7,803 行。 | 模組 | 行數 | 職責 | | --- | ---: | --- | @@ -588,7 +588,7 @@ socket server 有 8 MiB 讀取上限與 30 秒 handler timeout。 | `utils/flakiness/` | 151 | 以執行歷史分析不穩定測試 | | `utils/generate_report/` | 347 | HTML/JSON/XML 三種報表產生器(Template Method) | | `utils/media_assert/` | 273 | 媒體斷言:音訊活動與影片動態檢查 | -| `utils/observability/` | 710 | Prometheus 格式指標 + OpenTelemetry 相容 trace + `/metrics` 匯出伺服器 | +| `utils/observability/` | 711 | Prometheus 格式指標 + OpenTelemetry 相容 trace + `/metrics` 匯出伺服器 | | `utils/otlp_export/` | 114 | OTLP/JSON span 匯出 | | `utils/percentiles/` | 119 | 可合併的串流延遲摘要與精確百分位數 | | `utils/process_doc/` | 108 | 由錄製的 action list 產生逐步 SOP 文件 | @@ -596,7 +596,7 @@ socket server 有 8 MiB 讀取上限與 30 秒 handler timeout。 | `utils/profiler/` | 451 | 逐動作效能剖析器 + 資源剖析器 | | `utils/quarantine/` | 204 | 易碎測試隔離區,讓套件執行器跳過已知不穩定案例 | | `utils/run_diff/` | 143 | 兩次執行軌跡的差異(LCS 對齊:新增/移除/狀態翻轉/退化) | -| `utils/run_history/` | 513 | 執行歷史儲存與產出物管理 | +| `utils/run_history/` | 520 | 執行歷史儲存與產出物管理 | | `utils/sarif/` | 187 | 以 SARIF 2.1.0 匯出發現項,供 GitHub/Azure code scanning | | `utils/slo/` | 115 | SLO 評估:SLI、錯誤預算與多視窗燃燒率告警 | | `utils/smoothing/` | 67 | 數列移動平均平滑 | @@ -612,7 +612,7 @@ socket server 有 8 MiB 讀取上限與 30 秒 handler timeout。 ### 5.4.13 資料來源、結構驗證與 i18n -> 24 個套件、約 5,196 行。 +> 24 個套件、約 5,197 行。 | 模組 | 行數 | 職責 | | --- | ---: | --- | @@ -628,7 +628,7 @@ socket server 有 8 MiB 讀取上限與 30 秒 handler timeout。 | `utils/json_contract/` | 158 | JSON 契約/快照比對:`match_json`、`diff_json`、`snapshot_json` | | `utils/json_patch/` | 352 | JSON Pointer(6901)、JSON Patch(6902)與 Merge Patch(7386) | | `utils/json_schema/` | 666 | JSON Schema(Draft 2020-12 子集)驗證 | -| `utils/jsonpath/` | 360 | 精簡 JSONPath 查詢 | +| `utils/jsonpath/` | 361 | 精簡 JSONPath 查詢 | | `utils/list_format/` | 82 | 地區感知清單格式化(CLDR 風格的「A、B 和 C」) | | `utils/locale_collation/` | 139 | 地區感知字串排序(決定性多層排序鍵) | | `utils/locale_parse/` | 80 | 地區感知數字/貨幣/日期解析與格式化(選用 babel) | @@ -643,13 +643,13 @@ socket server 有 8 MiB 讀取上限與 30 秒 handler timeout。 ### 5.4.14 安全、機密與合規 -> 13 個套件、約 3,940 行。 +> 13 個套件、約 3,941 行。 | 模組 | 行數 | 職責 | | --- | ---: | --- | | `utils/config_redaction/` | 86 | 設定結構與 log 字串的機密遮蔽 | | `utils/egress/` | 169 | 無頭 HTTP 用戶端的網路外連允許清單守衛 | -| `utils/governance/` | 242 | 治理:maker-checker 核准閘門與即時憑證租約 | +| `utils/governance/` | 243 | 治理:maker-checker 核准閘門與即時憑證租約 | | `utils/license_policy/` | 240 | 以 SBOM 元件評估 SPDX 授權允許/拒絕政策 | | `utils/provenance/` | 126 | SLSA 建置來源證明(in-toto v1) | | `utils/rbac/` | 1,194 | 角色型存取控制:使用者、角色與權杖驗證(尚未接到 REST/MCP) | @@ -684,11 +684,11 @@ socket server 有 8 MiB 讀取上限與 30 秒 handler timeout。 ### 5.4.16 系統、視窗與剪貼簿 -> 16 個套件、約 2,699 行。 +> 16 個套件、約 2,695 行。 | 模組 | 行數 | 職責 | | --- | ---: | --- | -| `utils/clipboard/` | 465 | 跨平台無頭剪貼簿存取(文字 + 影像)+ `win32_clipboard_api.py`:**所有剪貼簿格式共用的 Win32 原型與 open/alloc/lock 流程**(`open_clipboard()` 會等過短暫被別的行程佔住的剪貼簿——Win32 一次只允許一個行程開啟,別人正在複製就必然失敗)(`argtypes` 只宣告一半曾讓四支 writer 在 64 位元上必然丟 `OverflowError`,見 CHANGELOG)。`set_clipboard_image` 同時接受 PNG 位元組與檔案路徑——先前這個名字在本子套件裡有**兩份不同簽章的實作**(`clipboard.py` 吃 bytes、`clipboard_image.py` 吃路徑),匯錯來源只會在執行期才炸,已合併成一支 | +| `utils/clipboard/` | 461 | 跨平台無頭剪貼簿存取(文字 + 影像)+ `win32_clipboard_api.py`:**所有剪貼簿格式共用的 Win32 原型與 open/alloc/lock 流程**(`open_clipboard()` 會等過短暫被別的行程佔住的剪貼簿——Win32 一次只允許一個行程開啟,別人正在複製就必然失敗)(`argtypes` 只宣告一半曾讓四支 writer 在 64 位元上必然丟 `OverflowError`,見 CHANGELOG)。`set_clipboard_image` 同時接受 PNG 位元組與檔案路徑——先前這個名字在本子套件裡有**兩份不同簽章的實作**(`clipboard.py` 吃 bytes、`clipboard_image.py` 吃路徑),匯錯來源只會在執行期才炸,已合併成一支 | | `utils/clipboard_files/` | 118 | 剪貼簿檔案清單(CF_HDROP):純 DROPFILES 封裝 + Win32 存取 | | `utils/clipboard_formats/` | 156 | 檢視與分類剪貼簿可用格式(純分類/差異 + Win32 列舉) | | `utils/clipboard_history/` | 114 | 剪貼簿歷史:環形緩衝 + 背景輪詢器 | @@ -721,7 +721,7 @@ socket server 有 8 MiB 讀取上限與 30 秒 handler timeout。 | `mouse_aliases.py` | 39 | 單鍵點擊別名(`AC_click_left` 等),executor 與 callback executor 共用。 | | `flags.py` | 19 | 旗標讀取 `as_bool`:依拼法(`"true"`/`"yes"`/`"on"`/`"1"`)而非 Python 真值判斷,所以 JSON、CLI、MCP 傳來的 `"false"` 是關;所有 adapter 與流程指令共用。 | -#### `utils/mcp_server/`(21,218 行,757 個工具)— 最大子系統 +#### `utils/mcp_server/`(21,219 行,757 個工具)— 最大子系統 | 檔案 | 行數 | 職責 | | --- | ---: | --- | @@ -738,7 +738,7 @@ socket server 有 8 MiB 讀取上限與 30 秒 handler timeout。 | `tools/_handlers_locators.py` | 483 | 同一種 adapter,定位主題:無障礙樹、智慧等待、自我修復、螢幕觀察、座標空間、視覺與 OCR、影像去重、元件倉庫、A/B 定位。 | | `tools/_handlers_operations.py` | 654 | 同一種 adapter,營運主題:agent 與其記憶/追蹤、治理與合規、成本與遙測、失敗掛鉤、看門狗、速率限制、檢查點、核可、產物與資產、測試選擇與分片、佇列與 saga。 | | `server.py` | 647 | JSON-RPC 2.0 over stdio 的最小 MCP 伺服器:連線範圍狀態、行內/併發分派、工具與 resource/prompt 處理器;握手時代的方法表(`_run_method`),兩個協定時代的逐請求分派在 `_stateless.py`。 | -| `http_transport.py` | 655 | MCP 的 HTTP 傳輸;宣告 2026-07-28 的請求走 `_http_stateless.py` 的標頭規則,不發 session。 | +| `http_transport.py` | 656 | MCP 的 HTTP 傳輸;宣告 2026-07-28 的請求走 `_http_stateless.py` 的標頭規則,不發 session。 | | `_http_stateless.py` | 185 | MCP 2026-07-28 在 Streamable HTTP 上的規則:`MCP-Protocol-Version`/`Mcp-Method`/`Mcp-Name` 必須與 body 相符(`=?base64?…?=` 先解碼),不符是 400+`HeaderMismatch`;版本與中繼資料錯誤 400、未知方法 404。純函式,由 `http_transport.py` 回覆。 | | `http_sessions.py` | 290 | MCP 的 HTTP 傳輸用的 session 身分:`Mcp-Session-Id` 註冊表,以及每個 session 那條常駐的 server→client SSE 串流。 | | `_client_requests.py` | 274 | 伺服器主動送出的請求:`roots/list`/`elicitation/create`/`sampling/createMessage`,對應表與回應路由,以及破壞性工具的確認交握。只屬於握手時代:無狀態請求裡送出會丟例外。 | @@ -754,12 +754,12 @@ socket server 有 8 MiB 讀取上限與 30 秒 handler timeout。 | `tools/_validation.py` | 122 | MCP 工具用到的 JSON Schema 子集驗證器。 | | `tools/plugin_tools.py` | 147 | 把外掛載入的 `AC_*` callable 包成 `MCPTool`。 | | `log_bridge.py` | 118 | 把 Python logging 記錄橋接成 MCP `notifications/message`;2026-07-28 的請求只收到自己設了 `logLevel` 時產生的記錄。 | -| `audit.py` | 126 | MCP 工具呼叫稽核記錄。 | +| `audit.py` | 125 | MCP 工具呼叫稽核記錄。 | | `context.py` | 71 | 傳給 opt-in 工具處理器的每次呼叫上下文。 | | `rate_limit.py` | 48 | 工具呼叫的 token bucket 限流。 | | `__main__.py` | 107 | `je_auto_control_mcp` console script 進入點。 | -#### `utils/remote_desktop/`(14,179 行/57 檔) +#### `utils/remote_desktop/`(14,178 行/57 檔) 三條傳輸路徑並存:**TCP**(JPEG 影格)、**WebSocket**(同協定換傳輸)、**WebRTC**(aiortc 視訊 + DataChannel)。 @@ -768,18 +768,18 @@ socket server 有 8 MiB 讀取上限與 30 秒 handler timeout。 | `webrtc_host.py` | 722 | WebRTC 主機:串流螢幕視訊並接受檢視端輸入;session 生命週期、DataChannel 接線、檔案收發。 | | `webrtc_viewer.py` | 677 | WebRTC 檢視端:接收視訊並送出輸入。 | | `host.py` | 693 | TCP 主機:接受迴圈、TLS 包裝、連線/認證握手、音訊與剪貼簿廣播、檔案推送、單次 token。 | -| `viewer.py` | 725 | TCP 檢視端。 | +| `viewer.py` | 723 | TCP 檢視端。 | | `host_service.py` | 562 | 無頭 WebRTC 主機執行器 + 多平台服務安裝器。 | -| `host_client.py` | 459 | TCP 主機的每連線處理器:一個檢視端一個實例,擁有它的認證交換、sender/audio/receiver 三條執行緒,以及入站訊息的路由表。 | +| `host_client.py` | 460 | TCP 主機的每連線處理器:一個檢視端一個實例,擁有它的認證交換、sender/audio/receiver 三條執行緒,以及入站訊息的路由表。 | | `registry.py` | 564 | `AC_remote_*` 指令使用的行程級單例。 | | `webrtc_transport.py` | 421 | 共用 WebRTC 管線:asyncio 橋接執行緒、螢幕視訊軌、設定。 | | `multi_viewer.py` | 357 | 每個連入檢視端各跑一個 `WebRTCDesktopHost` 的協調器。 | -| `signaling_server.py` | 670 | 獨立的 WebRTC SDP 交換 rendezvous 服務。 | +| `signaling_server.py` | 671 | 獨立的 WebRTC SDP 交換 rendezvous 服務。 | | `audit_log.py` | 355 | SQLite 雜湊鏈稽核記錄。 | | `host_capture.py` | 323 | TCP 主機的影格與游標產生:螢幕列舉、監視器索引轉擷取區域、預設 JPEG/游標 provider,以及 `FrameProductionMixin`(游標輪詢、擷取迴圈、上線編碼)。 | | `ws_protocol.py` | 318 | 最小 RFC 6455 WebSocket 框架與握手。 | | `file_transfer.py` | 455 | 分塊檔案傳輸。 | -| `relay.py` | 315 | NAT 穿透失敗時的 TCP 中繼。 | +| `relay.py` | 316 | NAT 穿透失敗時的 TCP 中繼。 | | `fingerprint.py` | 246 | TOFU 主機指紋驗證。 | | `turn_config.py` | 249 | coturn 設定產生器。 | | `presence.py` | 238 | 多檢視者的執行緒安全在場註冊表。 | @@ -800,7 +800,7 @@ socket server 有 8 MiB 讀取上限與 30 秒 handler timeout。 | `webrtc_inspector.py` | 138 | 行程級的 `StatsSnapshot` 滾動視窗。 | | `input_dispatch.py` | 161 | 在主機端套用輸入訊息。 | | `session_recorder.py` | 139 | 以 PyAV 把 WebRTC 影格錄成 mp4。 | -| `totp.py` | 160 | RFC 6238 TOTP(零外部相依)。 | +| `totp.py` | 159 | RFC 6238 TOTP(零外部相依)。 | | `file_sync.py` | 385 | 輪詢式資料夾鏡像。 | | `transport.py` | 126 | 可插拔的型別化訊息傳輸。 | | `host_access.py` | 112 | TCP 主機的檢視端核准與存取控制:`PendingViewer`、權限字串、分享碼的 TOTP 候選值、IP 白名單。`host` 與 `host_client` 共用,所以獨立成模組。 | @@ -915,8 +915,8 @@ GUI 是**選用 extra**(`pip install je_auto_control[gui]`,PySide6),且 | `_validators.py` | 29 | `int_validator()`/`double_validator()`:以 C locale 驗證的數字輸入框 validator,接受的正是 `int()`/`float()` 讀得懂的寫法(預設 locale 在法文、德文下只收小數逗號)。所有數字 `QLineEdit` 都用它。 | | `_screen_geometry.py` | 55 | Qt 邏輯座標與截圖用的原生像素互轉:`native_region()`、`screen_at_native()`、`logical_point()`(每個螢幕的左上角在兩者相同,螢幕內依 device pixel ratio 縮放)。區域選取與主機端標註覆蓋層都用它。 | | `_daemon_thread.py` | 79 | `DaemonThread`:`QThread` 的替代品,保留遠端桌面 worker 用到的介面(`start`/`run`/`isRunning`/`wait`/`requestInterruption`/`started`/`finished`),但 `run()` 跑在 daemon `threading.Thread` 上,刪除物件或程式結束都不會銷毀執行中的執行緒。 | -| `_worker_thread.py` | 266 | `start_worker()`:在 daemon `threading.Thread` 上執行 `QObject` worker 的 `run()`(沒有 `QThread` 可被銷毀),並經由分頁擁有的中繼物件回報結果(回呼一律在 GUI 執行緒;worker 沒處理的例外也送到 `on_fail`);worker 留在模組登錄表直到 GUI 執行緒看到它結束,回傳 `WorkerHandle`(`isRunning()`);程式結束時先呼叫 worker 的 `request_stop()`,最多等 10 秒,仍在跑的隨行程結束。 | -| `language_wrapper/` | 6,107 | 四語系字典(英/日/簡中/繁中)+ `multi_language_wrapper` 執行期切換器與監聽註冊表。 | +| `_worker_thread.py` | 267 | `start_worker()`:在 daemon `threading.Thread` 上執行 `QObject` worker 的 `run()`(沒有 `QThread` 可被銷毀),並經由分頁擁有的中繼物件回報結果(回呼一律在 GUI 執行緒;worker 沒處理的例外也送到 `on_fail`);worker 留在模組登錄表直到 GUI 執行緒看到它結束,回傳 `WorkerHandle`(`isRunning()`);程式結束時先呼叫 worker 的 `request_stop()`,最多等 10 秒,仍在跑的隨行程結束。 | +| `language_wrapper/` | 6,112 | 四語系字典(英/日/簡中/繁中)+ `multi_language_wrapper` 執行期切換器與監聽註冊表。 | | `selector/` | 216 | 拖曳選取螢幕區域的半透明全螢幕覆蓋層與樣板裁切工具(互動式,但都有對應的程式化 API)。 | > **分頁指令一律走 Actions 選單**:分頁本身只放輸入、表格與結果檢視,指令由視窗層選單暴露。 @@ -1101,17 +1101,17 @@ socket 預設綁 `127.0.0.1`;資源一律用 `with`。 | 層/子系統 | 檔案數 | 行數 | | --- | ---: | ---: | -| `gui/` | 126 | 35,088 | -| `utils/mcp_server/` | 43 | 21,218 | -| `utils/remote_desktop/` | 57 | 14,179 | +| `gui/` | 126 | 35,161 | +| `utils/mcp_server/` | 43 | 21,219 | +| `utils/remote_desktop/` | 57 | 14,178 | | `utils/executor/` | 10 | 10,658 | | `utils/usb/` | 20 | 5,057 | -| `je_auto_control/`(頂層 3 檔) | 3 | 2,676 | -| `utils/accessibility/` | 15 | 3,290 | -| `wrapper/` | 26 | 6,583 | -| `windows/` | 24 | 2,266 | +| `je_auto_control/`(頂層 3 檔) | 3 | 2,670 | +| `utils/accessibility/` | 15 | 3,292 | +| `wrapper/` | 26 | 6,590 | +| `windows/` | 24 | 2,265 | | `utils/rest_api/` | 8 | 2,080 | -| `utils/agent/` | 13 | 2,251 | +| `utils/agent/` | 13 | 2,252 | | `linux_with_x11/` | 19 | 1,281 | | `linux_wayland/` | 23 | 5,160 | | `utils/triggers/` | 4 | 1,396 | @@ -1121,6 +1121,6 @@ socket 預設綁 `127.0.0.1`;資源一律用 `with`。 | `osx/` | 17 | 981 | | `autocontrol-lsp/` | 8 | 744 | | `utils/hotkey/` | 7 | 861 | -| 其餘模組(約 286 個 `utils/` 子套件 + `android/`/`ios/`/周邊小工具) | 724 | 68,843 | -| **總計** | **1,165** | **187,890** | +| 其餘模組(約 286 個 `utils/` 子套件 + `android/`/`ios/`/周邊小工具) | 724 | 68,874 | +| **總計** | **1,165** | **187,997** | diff --git a/benchmarks/gui_startup.py b/benchmarks/gui_startup.py index 5dda1e384..4359ad0b1 100644 --- a/benchmarks/gui_startup.py +++ b/benchmarks/gui_startup.py @@ -97,8 +97,8 @@ def run(runs: int) -> Dict[str, Any]: } -def main(argv: Optional[List[str]] = None) -> int: - """Command line entry point.""" +def main(argv: Optional[List[str]] = None) -> None: + """Command line entry point; a failure leaves as an exception, so there is no status to return.""" parser = argparse.ArgumentParser(description=__doc__.splitlines()[0]) parser.add_argument("--runs", type=int, default=5, help="cold starts to measure (default 5)") parser.add_argument("--output", help="write the JSON report here instead of stdout") @@ -109,10 +109,9 @@ def main(argv: Optional[List[str]] = None) -> int: _child() if args.compare: bench.emit(bench.compare(bench.load(args.compare[0]), bench.load(args.compare[1])), args.output) - return 0 + return bench.emit(run(args.runs), args.output) - return 0 if __name__ == "__main__": - sys.exit(main()) + main() diff --git a/docker/Dockerfile b/docker/Dockerfile index 912da703e..4ac766b41 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -32,14 +32,27 @@ ARG DEBIAN_FRONTEND=noninteractive # - libusb-1.0-0: USB enumeration via pyusb / libusb. RUN apt-get update \ && apt-get install -y --no-install-recommends \ - xvfb xauth x11-utils xdotool \ - libgl1 libglib2.0-0 \ - libxkbcommon-x11-0 libdbus-1-3 \ - libxcb-cursor0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 \ - libxcb-randr0 libxcb-render-util0 libxcb-shape0 libxcb-sync1 \ - libxcb-xfixes0 libxcb-xinerama0 libxcb-xkb1 \ - libusb-1.0-0 \ ca-certificates \ + libdbus-1-3 \ + libgl1 \ + libglib2.0-0 \ + libusb-1.0-0 \ + libxcb-cursor0 \ + libxcb-icccm4 \ + libxcb-image0 \ + libxcb-keysyms1 \ + libxcb-randr0 \ + libxcb-render-util0 \ + libxcb-shape0 \ + libxcb-sync1 \ + libxcb-xfixes0 \ + libxcb-xinerama0 \ + libxcb-xkb1 \ + libxkbcommon-x11-0 \ + x11-utils \ + xauth \ + xdotool \ + xvfb \ && rm -rf /var/lib/apt/lists/* WORKDIR /app diff --git a/docker/Dockerfile.eis b/docker/Dockerfile.eis index 72c8369b9..4dd581f2c 100644 --- a/docker/Dockerfile.eis +++ b/docker/Dockerfile.eis @@ -56,10 +56,14 @@ ARG DEBIAN_FRONTEND=noninteractive # libgl1 and libglib2.0-0 are opencv-python's hard import-time requirements. RUN apt-get update \ && apt-get install -y --no-install-recommends \ - libei1 libeis1 \ - grim wlr-randr wtype \ - libgl1 libglib2.0-0 \ ca-certificates \ + grim \ + libei1 \ + libeis1 \ + libgl1 \ + libglib2.0-0 \ + wlr-randr \ + wtype \ && rm -rf /var/lib/apt/lists/* WORKDIR /app diff --git a/docker/Dockerfile.portal b/docker/Dockerfile.portal index e23c7f22f..e8e170d50 100644 --- a/docker/Dockerfile.portal +++ b/docker/Dockerfile.portal @@ -69,11 +69,18 @@ ARG DEBIAN_FRONTEND=noninteractive # libgl1 and libglib2.0-0 are opencv-python's hard import-time requirements. RUN apt-get update \ && apt-get install -y --no-install-recommends \ - libei1 libeis1 liboeffis1 \ - dbus gir1.2-glib-2.0 python3-gi \ - grim wlr-randr wtype \ - libgl1 libglib2.0-0 \ ca-certificates \ + dbus \ + gir1.2-glib-2.0 \ + grim \ + libei1 \ + libeis1 \ + libgl1 \ + libglib2.0-0 \ + liboeffis1 \ + python3-gi \ + wlr-randr \ + wtype \ && rm -rf /var/lib/apt/lists/* WORKDIR /app diff --git a/docker/Dockerfile.seat b/docker/Dockerfile.seat index 9a813f41a..e9d93ba7a 100644 --- a/docker/Dockerfile.seat +++ b/docker/Dockerfile.seat @@ -78,10 +78,16 @@ RUN printf 'deb https://deb.debian.org/debian sid main\n' \ && apt-get update \ && apt-get install -y --no-install-recommends -t sid ydotool \ && apt-get install -y --no-install-recommends \ - grim sway wlr-randr wtype \ - dmz-cursor-theme libinput-tools udev \ - libgl1 libglib2.0-0 \ ca-certificates \ + dmz-cursor-theme \ + grim \ + libgl1 \ + libglib2.0-0 \ + libinput-tools \ + sway \ + udev \ + wlr-randr \ + wtype \ && rm -rf /var/lib/apt/lists/* WORKDIR /app diff --git a/docker/Dockerfile.wayland b/docker/Dockerfile.wayland index c36524af9..9d159fec3 100644 --- a/docker/Dockerfile.wayland +++ b/docker/Dockerfile.wayland @@ -54,10 +54,14 @@ ARG DEBIAN_FRONTEND=noninteractive # libgthread-2.0.so.0 at import, so the package cannot even load without them. RUN apt-get update \ && apt-get install -y --no-install-recommends \ - grim sway wlr-randr wtype \ - libei1 \ - libgl1 libglib2.0-0 \ ca-certificates \ + grim \ + libei1 \ + libgl1 \ + libglib2.0-0 \ + sway \ + wlr-randr \ + wtype \ && rm -rf /var/lib/apt/lists/* WORKDIR /app diff --git a/docker/Dockerfile.x11 b/docker/Dockerfile.x11 index 75ff25d87..bc0173cad 100644 --- a/docker/Dockerfile.x11 +++ b/docker/Dockerfile.x11 @@ -71,11 +71,20 @@ ARG DEBIAN_FRONTEND=noninteractive # libgthread-2.0.so.0 at import, so the package cannot even load without them. RUN apt-get update \ && apt-get install -y --no-install-recommends \ - xvfb xauth x11-utils x11-xserver-utils xdotool \ - imagemagick openbox xterm \ - at-spi2-core dbus-x11 zenity \ - libgl1 libglib2.0-0 \ + at-spi2-core \ ca-certificates \ + dbus-x11 \ + imagemagick \ + libgl1 \ + libglib2.0-0 \ + openbox \ + x11-utils \ + x11-xserver-utils \ + xauth \ + xdotool \ + xterm \ + xvfb \ + zenity \ && rm -rf /var/lib/apt/lists/* WORKDIR /app @@ -90,8 +99,9 @@ COPY docker/x11_verify.py /opt/verify/x11_verify.py COPY docker/x11_window_verify.py /opt/verify/x11_window_verify.py COPY docker/x11_atspi_verify.py /opt/verify/x11_atspi_verify.py COPY docker/entrypoint-x11.sh /usr/local/bin/autocontrol-x11-verify -RUN chmod +x /usr/local/bin/autocontrol-x11-verify +# One RUN layer: make the entrypoint executable, then create the user below. +# # Unprivileged, unlike the Wayland verification image. That one runs as root # because XDG_RUNTIME_DIR ownership would otherwise be the only thing # standing between it and an answer; X11 has no such requirement — the @@ -100,7 +110,8 @@ RUN chmod +x /usr/local/bin/autocontrol-x11-verify # between this container and the desktop it stands in for. # /app is the working directory, and the library opens its log file in the # cwd at import, so the verifying user has to own it. -RUN useradd --create-home --shell /bin/sh verify \ +RUN chmod +x /usr/local/bin/autocontrol-x11-verify \ + && useradd --create-home --shell /bin/sh verify \ && chown -R verify:verify /app USER verify diff --git a/docker/Dockerfile.xfce b/docker/Dockerfile.xfce index cd0269651..b0c4ba849 100644 --- a/docker/Dockerfile.xfce +++ b/docker/Dockerfile.xfce @@ -29,19 +29,38 @@ FROM python:3.12-slim AS runtime ARG DEBIAN_FRONTEND=noninteractive +# One sorted list, so a package is easy to find and a diff touches one line: +# - display: xvfb, xauth, x11-utils, xdotool, x11vnc. +# - XFCE desktop (slim selection): xfce4, xfce4-terminal. +# - Qt / PySide6 + opencv runtime: libgl1, libglib2.0-0 (opencv needs its +# libgthread-2.0.so.0), libxkbcommon-x11-0, libdbus-1-3 and the libxcb-* set. +# - other deps: libusb-1.0-0, ca-certificates. RUN apt-get update \ && apt-get install -y --no-install-recommends \ - # display - xvfb xauth x11-utils xdotool x11vnc \ - # XFCE desktop (slim selection) - xfce4 xfce4-terminal \ - # Qt / PySide6 + opencv runtime (libgthread-2.0.so.0 ← libglib2.0-0) - libgl1 libglib2.0-0 libxkbcommon-x11-0 libdbus-1-3 \ - libxcb-cursor0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 \ - libxcb-randr0 libxcb-render-util0 libxcb-shape0 libxcb-sync1 \ - libxcb-xfixes0 libxcb-xinerama0 libxcb-xkb1 \ - # other deps - libusb-1.0-0 ca-certificates \ + ca-certificates \ + libdbus-1-3 \ + libgl1 \ + libglib2.0-0 \ + libusb-1.0-0 \ + libxcb-cursor0 \ + libxcb-icccm4 \ + libxcb-image0 \ + libxcb-keysyms1 \ + libxcb-randr0 \ + libxcb-render-util0 \ + libxcb-shape0 \ + libxcb-sync1 \ + libxcb-xfixes0 \ + libxcb-xinerama0 \ + libxcb-xkb1 \ + libxkbcommon-x11-0 \ + x11-utils \ + x11vnc \ + xauth \ + xdotool \ + xfce4 \ + xfce4-terminal \ + xvfb \ && rm -rf /var/lib/apt/lists/* WORKDIR /app diff --git a/docker/Dockerfile.ydotool b/docker/Dockerfile.ydotool index f94b81f56..67a272f04 100644 --- a/docker/Dockerfile.ydotool +++ b/docker/Dockerfile.ydotool @@ -71,9 +71,12 @@ RUN printf 'deb https://deb.debian.org/debian sid main\n' \ && apt-get update \ && apt-get install -y --no-install-recommends -t sid ydotool \ && apt-get install -y --no-install-recommends \ - grim wlr-randr wtype \ - libgl1 libglib2.0-0 \ ca-certificates \ + grim \ + libgl1 \ + libglib2.0-0 \ + wlr-randr \ + wtype \ && rm -rf /var/lib/apt/lists/* WORKDIR /app diff --git a/docker/eis_verify.py b/docker/eis_verify.py index 7ea91d21a..5001f48c6 100644 --- a/docker/eis_verify.py +++ b/docker/eis_verify.py @@ -640,17 +640,16 @@ def _helper_measurements_in_a_child() -> None: "about the opt-in helper only and is not counted") -def _helper_measurements_main() -> int: - """Entry point of the measurement child.""" +def _helper_measurements_main() -> None: + """Entry point of the measurement child; nothing here is counted, so it has no status.""" sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) from eis_server import RecordingEisServer, load_symbols from je_auto_control.linux_wayland import libei if load_symbols() is None: print("note libeis is not installed; nothing to measure") - return 0 + return _run_helper_measurements(os.path.join(_scratch_dir(), "eis-helper"), libei, RecordingEisServer) - return 0 def _run_helper_measurements(path, libei, server_class) -> None: @@ -901,5 +900,6 @@ def main() -> int: if __name__ == "__main__": if _HELPER_FLAG in sys.argv[1:]: - sys.exit(_helper_measurements_main()) + _helper_measurements_main() + sys.exit(0) sys.exit(main()) diff --git a/docker/entrypoint-x11.sh b/docker/entrypoint-x11.sh index 4966c1639..04716f65f 100644 --- a/docker/entrypoint-x11.sh +++ b/docker/entrypoint-x11.sh @@ -29,6 +29,8 @@ fi GEOMETRY="${SCREEN_GEOMETRY:-1280x800x24}" DISPLAY_NUM="${DISPLAY:-:99}" +# The rule drawn around every heading in the log. +RULE="========================================================================" echo "starting Xvfb on ${DISPLAY_NUM} at ${GEOMETRY}" Xvfb "${DISPLAY_NUM}" -screen 0 "${GEOMETRY}" +extension RANDR \ @@ -88,9 +90,9 @@ export PYTHONPATH=/opt/verify # Runs one pass over one monitor layout; echoes the number of failed checks. run_pass() { label="$1" - echo "========================================================================" + echo "${RULE}" echo "layout: ${label}" - echo "========================================================================" + echo "${RULE}" xrandr --listmonitors || true rc=0 python3 /opt/verify/x11_verify.py || rc=$? @@ -121,21 +123,21 @@ run_pass "two monitors side by side" || total=$((total + $?)) # Window management does not depend on the monitor layout — it is about what # the window manager agrees to — so it runs once, after both layout passes. echo -echo "========================================================================" +echo "${RULE}" echo "window management against a real window manager" -echo "========================================================================" +echo "${RULE}" python3 /opt/verify/x11_window_verify.py || total=$((total + $?)) echo -echo "========================================================================" +echo "${RULE}" echo "accessibility against a real AT-SPI bus" -echo "========================================================================" +echo "${RULE}" python3 /opt/verify/x11_atspi_verify.py || total=$((total + $?)) echo -echo "========================================================================" +echo "${RULE}" echo "total failed checks: ${total}" -echo "========================================================================" +echo "${RULE}" kill "$OPENBOX_PID" 2>/dev/null || true kill "$XVFB_PID" 2>/dev/null || true diff --git a/docker/portal_server.py b/docker/portal_server.py index d98619f47..c0df38fa9 100644 --- a/docker/portal_server.py +++ b/docker/portal_server.py @@ -312,8 +312,9 @@ def _respond(self, sender: str, path: str, code: int, # --- the four RemoteDesktop calls ------------------------------------- - def _do_CreateSession(self, sender: str, parameters: GLib.Variant, # noqa: N802 # reason: D-Bus method name - invocation: Gio.DBusMethodInvocation) -> None: + def _do_CreateSession( # noqa: N802 # NOSONAR python:S100 # reason: dispatched by its D-Bus method name + self, sender: str, parameters: GLib.Variant, + invocation: Gio.DBusMethodInvocation) -> None: options = parameters.unpack()[0] self._note("CreateSession", options) request = self._request_path(sender, options) @@ -340,8 +341,9 @@ def _close_session(self, sender: str, session: str) -> bool: GLib.Variant("(a{sv})", ({},))) return False - def _do_SelectDevices(self, sender: str, parameters: GLib.Variant, # noqa: N802 # reason: D-Bus method name - invocation: Gio.DBusMethodInvocation) -> None: + def _do_SelectDevices( # noqa: N802 # NOSONAR python:S100 # reason: dispatched by its D-Bus method name + self, sender: str, parameters: GLib.Variant, + invocation: Gio.DBusMethodInvocation) -> None: session, options = parameters.unpack() self._note("SelectDevices", {"session": session, "options": options}) types = options.get("types") @@ -353,8 +355,9 @@ def _do_SelectDevices(self, sender: str, parameters: GLib.Variant, # noqa: N802 invocation.return_value(GLib.Variant("(o)", (request,))) GLib.idle_add(self._respond, sender, request, RESPONSE_SUCCESS, {}) - def _do_Start(self, sender: str, parameters: GLib.Variant, # noqa: N802 # reason: D-Bus method name - invocation: Gio.DBusMethodInvocation) -> None: + def _do_Start( # noqa: N802 # NOSONAR python:S100 # reason: dispatched by its D-Bus method name + self, sender: str, parameters: GLib.Variant, + invocation: Gio.DBusMethodInvocation) -> None: session, parent_window, options = parameters.unpack() self._note("Start", {"session": session, "parent": parent_window, "options": options}) @@ -368,8 +371,9 @@ def _do_Start(self, sender: str, parameters: GLib.Variant, # noqa: N802 # reas results = {} if denied else {"devices": GLib.Variant("u", 3)} GLib.idle_add(self._respond, sender, request, code, results) - def _do_ConnectToEIS(self, _sender: str, parameters: GLib.Variant, # noqa: N802 # reason: D-Bus method name - invocation: Gio.DBusMethodInvocation) -> None: + def _do_ConnectToEIS( # noqa: N802 # NOSONAR python:S100 # reason: dispatched by its D-Bus method name + self, _sender: str, parameters: GLib.Variant, + invocation: Gio.DBusMethodInvocation) -> None: session, options = parameters.unpack() self._note("ConnectToEIS", {"session": session, "options": options}) if self.behaviour == "no-fd": @@ -396,8 +400,9 @@ def _do_ConnectToEIS(self, _sender: str, parameters: GLib.Variant, # noqa: N802 # --- the Screenshot call ---------------------------------------------- - def _do_Screenshot(self, sender: str, parameters: GLib.Variant, # noqa: N802 # reason: D-Bus method name - invocation: Gio.DBusMethodInvocation) -> None: + def _do_Screenshot( # noqa: N802 # NOSONAR python:S100 # reason: dispatched by its D-Bus method name + self, sender: str, parameters: GLib.Variant, + invocation: Gio.DBusMethodInvocation) -> None: """Answer the interface every desktop implements, however it captures. The client here is not a C library but AutoControl's own D-Bus diff --git a/docker/x11_verify.py b/docker/x11_verify.py index b12f6ffce..6ae7f85e0 100644 --- a/docker/x11_verify.py +++ b/docker/x11_verify.py @@ -496,8 +496,9 @@ def check_pointer(layout: Layout) -> None: def _pointer_at(point: Tuple[int, int]) -> str: set_mouse_position(*point) shell = _run(["xdotool", "getmouselocation", "--shell"]) - values = dict( - line.split("=", 1) for line in shell.splitlines() if "=" in line) + values = { + name: value for name, _, value in + (line.partition("=") for line in shell.splitlines() if "=" in line)} server = (int(values["X"]), int(values["Y"])) _assert_eq(server, point) _assert_eq(tuple(get_mouse_position()), point) diff --git a/docs/updates/2026-10.md b/docs/updates/2026-10.md index e2e9dbebf..3af894a1a 100644 --- a/docs/updates/2026-10.md +++ b/docs/updates/2026-10.md @@ -467,3 +467,13 @@ Jeffrey_RPA kept its own `KEY_ALIASES` (40 friendly names) and `_EXTRA_KEY_CODES - **Tests**: `test_stable_release.py` (13). - **Files**: `scripts/stable_release.py` (new), `.github/workflows/stable.yml`, `pyproject.toml`, `dev.toml`, `CHANGELOG.md`, `docs/API_LIFECYCLE.md`, `architecture.md`, `architecture_explore.md`, `CLAUDE.md`, `test/unit_test/headless/test_stable_release.py` (new). - **Open items**: none new. Real-hardware verification stays in `Progress.md`. + +## U-20261009-42 · 2026-10-09 · SonarCloud and Codacy backlog on main: 2,344 and 8 findings worked through · #done #quality #sonar #codacy + +- **What**: every open SonarCloud issue (2,344: 2 bugs, 2 vulnerabilities, 2,340 code smells; quality gate failing on new reliability and security ratings) and every open Codacy issue (8) on `main` after 1.0.0 was fixed or suppressed inline with a reason. The lists were taken from the two APIs, not from the dashboards. +- **By script, from the reported lines**: 1,491 `assert a and b` split into one assertion per condition (S9073), 146 empty decorator parentheses removed (S9083), 137 `Union[...]` hints written with `|` (S6546, `ruff --select UP007`). +- **By hand**: 249 `pytest.raises` blocks reduced to the one call that raises (S5778); 63 manual patches of globals moved to `monkeypatch` (S8997); 24 teardown-less `yield` fixtures (S9100); the fakes' signatures brought in line with `AccessibilityBackend` (Codacy W0221); and about 220 findings in `je_auto_control/`, `docker/`, `examples/` and `benchmarks/`. +- **Behaviour**: see `CHANGELOG.md` › Unreleased. Regex changes (`\w` / `\d` now always with `re.ASCII`, the Robot header and package-name patterns rewritten without nested quantifiers) were checked against the old patterns over every code point and on random strings; the one intended difference is the trailing newline `is_package_name` no longer accepts. +- **Suppressed, not changed** (each with `# NOSONAR # reason:` on its line): `dataclasses.replace` return types (S5886 / S5655, Sonar's stub is wrong); NaN guards written as `not x >= 0` (S1940); snapshots taken with `list()` before a loop that mutates (S7504); D-Bus and Qt method names (S100); `json_store._replace` (S8707: the source is its own `mkstemp` sibling); `history_store.list_runs` (Semgrep: only `?` placeholders are interpolated); sequential awaits in `web_viewer/index.html` (S9382). +- **Files**: 280 files; `architecture_explore.md` line counts re-measured with `test_doc_line_counts.py --fix`. +- **Open items**: whether the suppressions inside `web_viewer/index.html` and the two `nosemgrep` lines are honoured is only known after the next analysis. `Progress.md` › 雜項. diff --git a/docs/updates/README.md b/docs/updates/README.md index 2496c6e49..19013bd1e 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `Progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20261009-42 | 2026-10-09 | SonarCloud and Codacy backlog on main: 2,344 and 8 findings worked through | #done #quality #sonar #codacy | [2026-10](2026-10.md) | | U-20261009-41 | 2026-10-09 | Release 1.0.0; the publish job releases a hand-set version as written | #release #ci | [2026-10](2026-10.md) | | U-20261009-40 | 2026-10-09 | A result hook reports nested command results; secret-named result fields are masked at any depth and offered once | #done #executor #gui #security | [2026-10](2026-10.md) | | U-20261009-39 | 2026-10-09 | REST and MCP resources follow the read_data rule; a Slack /stop no longer waits for the run it stops | #done #security #rbac #mcp #chatops | [2026-10](2026-10.md) | diff --git a/examples/18_slack_daily_report.py b/examples/18_slack_daily_report.py index e4121c3aa..7ff94288e 100644 --- a/examples/18_slack_daily_report.py +++ b/examples/18_slack_daily_report.py @@ -150,6 +150,23 @@ def summarise(messages: Sequence[SlackMessage]) -> str: return "Today's digest:\n" + bullet_lines +def _path_inside(base_dir: Path, name: str) -> Path: + """Return ``base_dir/name``, refusing a name that would land outside it. + + The pattern to copy whenever part of a file name comes from outside the + program (a channel name, a date in a request, a user-chosen title): + resolve both ends with ``os.path.realpath`` — which follows ``..`` and + symlinks — and write only if the result is still under the base folder. + """ + base = os.path.realpath(base_dir) + candidate = os.path.realpath(os.path.join(base, name)) + # join(base, "") ends in exactly one separator, so "digests-old" is not + # taken for a child of "digests". + if not candidate.startswith(os.path.join(base, "")): + raise ValueError(f"refusing a path outside {base}: {name!r}") + return Path(candidate) + + def render_report(summary: str, raw_messages: Sequence[SlackMessage], output_dir: Path) -> Path: """Render an HTML report + (if weasyprint is available) a PDF. @@ -179,22 +196,14 @@ def render_report(summary: str, raw_messages: Sequence[SlackMessage], """ - # Anchor the filename inside output_dir's resolved path so a - # malicious ``today`` (e.g. ``../etc/passwd``) can't escape — even - # though ``today`` is internally generated, validating here keeps - # Sonar's S2083 happy and protects future callers. - safe_name = os.path.basename(f"digest-{today}.html") - safe_root = output_dir.resolve() - html_path = (safe_root / safe_name).resolve() - if safe_root not in html_path.parents: - raise ValueError(f"refusing path-traversal name: {today!r}") + html_path = _path_inside(output_dir, f"digest-{today}.html") html_path.write_text(html, encoding="utf-8") try: from weasyprint import HTML except ImportError: print(" (no weasyprint — sending the HTML instead)") return html_path - pdf_path = output_dir / f"digest-{today}.pdf" + pdf_path = _path_inside(output_dir, f"digest-{today}.pdf") HTML(string=html).write_pdf(str(pdf_path)) return pdf_path diff --git a/examples/28_wayland_diagnostics.py b/examples/28_wayland_diagnostics.py index d0bd187ab..6666a961e 100644 --- a/examples/28_wayland_diagnostics.py +++ b/examples/28_wayland_diagnostics.py @@ -62,6 +62,7 @@ _WAYLAND_ENV = {"XDG_SESSION_TYPE": "wayland", "WAYLAND_DISPLAY": "wayland-0", "XDG_RUNTIME_DIR": "/run/user/1000"} +_SWAY_YDOTOOL_GRIM = "sway with ydotool and grim" def show(title: str, snapshot: ac.CapabilitySnapshot) -> None: @@ -98,7 +99,7 @@ def validate() -> int: _WAYLAND_ENV, tools=("gnome-screenshot",), libei=True), "GNOME, consent refused": _context( _WAYLAND_ENV, tools=("gnome-screenshot", "ydotool"), libei=True, ledger=refused), - "sway with ydotool and grim": _context( + _SWAY_YDOTOOL_GRIM: _context( {**_WAYLAND_ENV, "JE_AUTOCONTROL_WAYLAND_RECORD_DEVICES": "/dev/input/event3"}, tools=("ydotool", "grim"), bus=False), "X11 backend on a Wayland session": _context( @@ -120,7 +121,7 @@ def validate() -> int: "GNOME, portal not asked yet": ("not_requested", "libei", True), # ydotool is installed, and is still not used: a refusal is not a fallback. "GNOME, consent refused": ("needs_permission", "libei", True), - "sway with ydotool and grim": ("available", "ydotool", True), + _SWAY_YDOTOOL_GRIM: ("available", "ydotool", True), "X11 backend on a Wayland session": ("available", "xwayland", False), } problems: List[str] = [] @@ -129,7 +130,7 @@ def validate() -> int: got = (found.state.value, found.backend, found.desktop_wide) if got != wanted: problems.append(f"{title}: input is {got}, expected {wanted}") - if snapshots["sway with ydotool and grim"].get("recording").state.value != "needs_permission": + if snapshots[_SWAY_YDOTOOL_GRIM].get("recording").state.value != "needs_permission": problems.append("an unreadable recording device should read as needs_permission") if len(log.steps) != 2: problems.append(f"expected 2 journalled steps, got {len(log.steps)}") diff --git a/examples/29_config_sync.py b/examples/29_config_sync.py index 526c7b64d..b03c9d2e9 100644 --- a/examples/29_config_sync.py +++ b/examples/29_config_sync.py @@ -61,6 +61,7 @@ import je_auto_control as ac _USER = "alice" +_SCRIPT = "login.json" def _handler(store_path: Path) -> type: @@ -182,10 +183,10 @@ def _walkthrough(root: Path) -> Tuple[List[str], Dict[str, Any]]: seen: Dict[str, Any] = {} try: print("1. a script written on the laptop reaches the desktop") - laptop.write("login.json", [["AC_set_var", {"name": "user", "value": "alice"}]]) + laptop.write(_SCRIPT, [["AC_set_var", {"name": "user", "value": "alice"}]]) laptop.sync() desktop.sync() - seen["arrived"] = desktop.read("login.json") + seen["arrived"] = desktop.read(_SCRIPT) print("2. the server restarts; the bucket is still there") committed = ac.ConfigStore(store_path).revision(_USER) @@ -207,8 +208,8 @@ def _walkthrough(root: Path) -> Tuple[List[str], Dict[str, Any]]: print("4. both machines edit login.json apart: neither edit is dropped") desktop.sync() - laptop.write("login.json", [["AC_set_var", {"name": "user", "value": "from-laptop"}]]) - desktop.write("login.json", [["AC_set_var", {"name": "user", "value": "from-desktop"}]]) + laptop.write(_SCRIPT, [["AC_set_var", {"name": "user", "value": "from-laptop"}]]) + desktop.write(_SCRIPT, [["AC_set_var", {"name": "user", "value": "from-desktop"}]]) laptop.sync() seen["conflict"] = desktop.sync() details = desktop.status()["conflict_details"] @@ -223,7 +224,7 @@ def _walkthrough(root: Path) -> Tuple[List[str], Dict[str, Any]]: desktop.resolve(details[0]["key"], choices.index("laptop")) seen["resolved"] = desktop.sync() laptop.sync() - seen["final"] = (laptop.read("login.json"), desktop.read("login.json")) + seen["final"] = (laptop.read(_SCRIPT), desktop.read(_SCRIPT)) finally: server.stop() return _check(seen), seen diff --git a/examples/30_mobile_devices.py b/examples/30_mobile_devices.py index f69899401..dd28235e6 100644 --- a/examples/30_mobile_devices.py +++ b/examples/30_mobile_devices.py @@ -54,6 +54,7 @@ _SERIAL = "emulator-5554" _SIZE = (1080, 1920) +_SHELL_PREFIX = "shell " _INPUT_COMMANDS = ("input ", "am broadcast", "am start", "monkey ") @@ -103,8 +104,8 @@ def _reply(self, args: List[str]) -> bytes: @property def input_sent(self) -> List[str]: """The shell commands that would have acted on a device.""" - return [line[len("shell "):] for line in self.sent - if line.startswith("shell ") and line[len("shell "):].startswith(_INPUT_COMMANDS)] + return [line[len(_SHELL_PREFIX):] for line in self.sent + if line.startswith(_SHELL_PREFIX) and line[len(_SHELL_PREFIX):].startswith(_INPUT_COMMANDS)] def show_report(report: ac.DeviceSetupReport) -> None: diff --git a/examples/31_healing_comparison.py b/examples/31_healing_comparison.py index b8e0ea464..e755028c5 100644 --- a/examples/31_healing_comparison.py +++ b/examples/31_healing_comparison.py @@ -120,7 +120,7 @@ def validate() -> int: problems.append("v1 should fail exactly the HiDPI sample") if comparison.failures("v2-multi-scale"): problems.append("v2 should get every labelled sample right") - if not (old.accuracy.value or 0) < (new.accuracy.value or 0): + if (old.accuracy.value or 0) >= (new.accuracy.value or 0): problems.append("v2 should be measurably more accurate than v1") for problem in problems: print(f"FAILED: {problem}") diff --git a/examples/33_mcp_progressive.py b/examples/33_mcp_progressive.py index 64f4641d3..e43bd1011 100644 --- a/examples/33_mcp_progressive.py +++ b/examples/33_mcp_progressive.py @@ -118,7 +118,7 @@ def validate() -> int: print(f"state: {json.dumps(state)[:200]}") problems = [] - if not len(core) < len(full): + if len(core) >= len(full): problems.append("progressive mode did not start smaller than full mode") if target in core or target not in after: problems.append(f"{target} was not added by ac_tools_enable") diff --git a/je_auto_control/android/apps.py b/je_auto_control/android/apps.py index 9033a42fd..8477bfbf4 100644 --- a/je_auto_control/android/apps.py +++ b/je_auto_control/android/apps.py @@ -22,7 +22,7 @@ DeviceCapability, DeviceError, ) -_PACKAGE = re.compile(r"[A-Za-z0-9_]+(?:\.[A-Za-z0-9_]+)+") +_PACKAGE = re.compile(r"\w+(?:\.\w+)+", re.ASCII) _COMPONENT = re.compile(r"[A-Za-z0-9_.]+/[A-Za-z0-9_.$]+") _RESUMED = re.compile( r"(?:topResumedActivity|mResumedActivity|ResumedActivity)[=:]\s*ActivityRecord\{\S+ \S+ ([\w.]+)/") diff --git a/je_auto_control/cli.py b/je_auto_control/cli.py index 537043b3f..b5172312b 100644 --- a/je_auto_control/cli.py +++ b/je_auto_control/cli.py @@ -33,11 +33,7 @@ import time from typing import Callable, Dict, List, Optional, Sequence -from je_auto_control.utils.exception.exceptions import ( - AutoControlActionException, - AutoControlException, - AutoControlJsonActionException, -) +from je_auto_control.utils.exception.exceptions import AutoControlException def _parse_vars(pairs: Optional[Sequence[str]]) -> Dict[str, object]: @@ -411,9 +407,7 @@ def main(argv: Optional[List[str]] = None) -> int: utf8_stdout() try: return args.func(args) - except (AutoControlActionException, AutoControlException, - AutoControlJsonActionException, OSError, RuntimeError, - ValueError) as error: + except (AutoControlException, OSError, RuntimeError, ValueError) as error: sys.stderr.write(f"error: {error}\n") return 1 diff --git a/je_auto_control/gui/_worker_thread.py b/je_auto_control/gui/_worker_thread.py index 8ae112ce5..123e892bc 100644 --- a/je_auto_control/gui/_worker_thread.py +++ b/je_auto_control/gui/_worker_thread.py @@ -94,7 +94,7 @@ def cancel(self) -> None: if relay is not None: relay.drop_outcome() - def isRunning(self) -> bool: # noqa: N802 # reason: the QThread spelling its callers use + def isRunning(self) -> bool: # NOSONAR python:S100 # noqa: N802 # reason: the QThread spelling callers use """Whether ``run()`` has not returned yet.""" return self._thread is not None and self._thread.is_alive() @@ -201,7 +201,8 @@ def cancel_workers(owner: QObject) -> int: What a tab's ``dispose()`` needs: see :meth:`WorkerHandle.cancel`. """ - handles = [handle for handle in list(_RUNNING) + # reason (suppression below): list() is an atomic snapshot of a registry start_worker and the reaper mutate + handles = [handle for handle in list(_RUNNING) # NOSONAR python:S7504 if handle._owner is not None and handle._owner() is owner] # noqa: SLF001 # reason: own class for handle in handles: handle.cancel() diff --git a/je_auto_control/gui/language_wrapper/english.py b/je_auto_control/gui/language_wrapper/english.py index 606825fa4..bb1f91917 100644 --- a/je_auto_control/gui/language_wrapper/english.py +++ b/je_auto_control/gui/language_wrapper/english.py @@ -106,7 +106,7 @@ "dm_col_error": "Error", "dm_summary": "{passed} passed, {failed} failed of {total}", "dm_error": "Run failed: {error}", - "dm_running": "Running...", + "dm_running": "Running...", # NOSONAR python:S1192 # reason: one catalogue entry per key, by design # Media checks tab "media_audio_label": "Audio activity check", diff --git a/je_auto_control/gui/language_wrapper/japanese.py b/je_auto_control/gui/language_wrapper/japanese.py index 45c29fd9e..c42f6270b 100644 --- a/je_auto_control/gui/language_wrapper/japanese.py +++ b/je_auto_control/gui/language_wrapper/japanese.py @@ -9,6 +9,9 @@ _CLEAR_ALL_JA = "すべて削除" _BROWSE = "参照..." # NOSONAR python:S1192 — shared button label _LOCATE_CLICK = "検索してクリック" # NOSONAR python:S1192 +_HOTKEYS = "ホットキー" +_ACTION = "アクション" +_RUNNING = "実行中..." japanese_word_dict = { "application_name": "AutoControlGUI", @@ -25,7 +28,7 @@ "tab_scheduler": "スケジューラー", "tab_socket_server": "ソケットサーバー", "tab_live_hud": "ライブ HUD", - "tab_hotkeys": "ホットキー", + "tab_hotkeys": _HOTKEYS, "tab_triggers": "トリガー", "tab_webhooks": "Webhook", "tab_email_triggers": "Email トリガー", @@ -843,7 +846,7 @@ "rh_source_all": "すべて", "rh_source_scheduler": "スケジューラー", "rh_source_trigger": "トリガー", - "rh_source_hotkey": "ホットキー", + "rh_source_hotkey": _HOTKEYS, "rh_source_manual": "手動", "rh_source_rest": "REST", "rh_refresh": "更新", @@ -995,7 +998,7 @@ "prof_paused": "プロファイラは停止中です。", "prof_total_label": "{n} アクションを記録", "prof_total_empty": "サンプルがまだありません。", - "prof_col_name": "アクション", + "prof_col_name": _ACTION, "prof_col_calls": "回数", "prof_col_total": "合計", "prof_col_avg": "平均", @@ -1139,7 +1142,7 @@ "computer_use_run_btn": "実行", "computer_use_stop_btn": "停止", "computer_use_stopping": "現在のステップの後で停止します…", - "computer_use_running": "実行中...", + "computer_use_running": _RUNNING, "computer_use_already_running": "既に実行中です。少々お待ちください。", "computer_use_success": "目標達成しました。", "computer_use_failure": "目標達成前に停止しました。", @@ -1190,7 +1193,7 @@ "config_sync_assets_label": "共有アセットフォルダー (任意):", "config_sync_locators_label": "ロケーターリポジトリファイル (任意):", "config_sync_sections_label": "同期するセクション:", - "config_sync_section_hotkeys": "ホットキー", + "config_sync_section_hotkeys": _HOTKEYS, "config_sync_section_triggers": "トリガー", "config_sync_section_address_book": "アドレス帳", "config_sync_section_scripts": "スクリプト (フォルダーが必要)", @@ -1266,7 +1269,7 @@ "trace_last_btn": "末尾へ", "trace_status": "フレーム {step} / {total} ({seconds}秒)", "trace_col_timestamp": "時刻", - "trace_col_action_name": "アクション", + "trace_col_action_name": _ACTION, "trace_col_args": "引数", "trace_col_error": "エラー", @@ -1288,7 +1291,7 @@ "ocr_regex_required": "先に正規表現を入力", "ocr_regex_invalid": "正規表現が不正", "ocr_running": "読み取り中...", - "dm_running": "実行中...", + "dm_running": _RUNNING, # Variables Tab "vars_current_group": "現在のスコープ", @@ -1327,10 +1330,10 @@ "llm_planning": "計画中...", "llm_plan_count": "{n} 個のアクションを計画", "llm_no_plan": "まず「プラン作成」を押してください", - "llm_running": "実行中...", + "llm_running": _RUNNING, "llm_run_done": "完了", # Background runs shared by the tabs (gui/_tab_task.py) - "task_running": "実行中...", + "task_running": _RUNNING, "task_busy": "すでに実行中です。終了を待つか、先に停止してください。", "task_stopping": "停止中...", "task_stopped": "停止しました。", @@ -1488,7 +1491,7 @@ "menu_language": "言語", "menu_help": "ヘルプ", "menu_help_about": "AutoControlGUI について", - "menu_actions": "アクション", + "menu_actions": _ACTION, "menu_actions_none": "(このタブにアクションはありません)", "menu_choose_script_dir": "スクリプトフォルダーを選択...", "execute_editor_script": "エディター内容を実行", diff --git a/je_auto_control/gui/language_wrapper/simplified_chinese.py b/je_auto_control/gui/language_wrapper/simplified_chinese.py index 36524d712..47d6c7bad 100644 --- a/je_auto_control/gui/language_wrapper/simplified_chinese.py +++ b/je_auto_control/gui/language_wrapper/simplified_chinese.py @@ -1,5 +1,6 @@ _BROWSE = "浏览..." # NOSONAR python:S1192 — shared button label _LOCATE_CLICK = "定位并点击" # NOSONAR python:S1192 +_RUNNING = "执行中..." simplified_chinese_word_dict = { "application_name": "AutoControlGUI", @@ -1255,7 +1256,7 @@ "ocr_regex_required": "请先输入 regex 表达式", "ocr_regex_invalid": "Regex 不正确", "ocr_running": "识别中...", - "dm_running": "执行中...", + "dm_running": _RUNNING, # Variables Tab "vars_current_group": "当前作用域", @@ -1294,10 +1295,10 @@ "llm_planning": "规划中...", "llm_plan_count": "已规划 {n} 个指令", "llm_no_plan": "请先按下「规划」", - "llm_running": "执行中...", + "llm_running": _RUNNING, "llm_run_done": "完成", # Background runs shared by the tabs (gui/_tab_task.py) - "task_running": "执行中...", + "task_running": _RUNNING, "task_busy": "已在执行中:请等它结束,或先停止。", "task_stopping": "正在停止...", "task_stopped": "已停止。", diff --git a/je_auto_control/gui/language_wrapper/traditional_chinese.py b/je_auto_control/gui/language_wrapper/traditional_chinese.py index 918b9ab13..3583514a5 100644 --- a/je_auto_control/gui/language_wrapper/traditional_chinese.py +++ b/je_auto_control/gui/language_wrapper/traditional_chinese.py @@ -1,5 +1,6 @@ _TOKEN_LABEL_ZH = "Token:" _BROWSE = "瀏覽..." # NOSONAR python:S1192 — shared button label +_RUNNING = "執行中..." traditional_chinese_word_dict = { # Main @@ -1116,7 +1117,7 @@ "computer_use_run_btn": "執行", "computer_use_stop_btn": "停止", "computer_use_stopping": "目前步驟結束後停止…", - "computer_use_running": "執行中...", + "computer_use_running": _RUNNING, "computer_use_already_running": "已有任務執行中,請稍候。", "computer_use_success": "成功達成目標。", "computer_use_failure": "未達目標即停止。", @@ -1263,7 +1264,7 @@ "ocr_regex_required": "請先輸入 regex 樣式", "ocr_regex_invalid": "Regex 不正確", "ocr_running": "辨識中...", - "dm_running": "執行中...", + "dm_running": _RUNNING, # Variables Tab "vars_current_group": "目前作用域", @@ -1302,10 +1303,10 @@ "llm_planning": "規劃中...", "llm_plan_count": "已規劃 {n} 個指令", "llm_no_plan": "請先按下「規劃」", - "llm_running": "執行中...", + "llm_running": _RUNNING, "llm_run_done": "完成", # Background runs shared by the tabs (gui/_tab_task.py) - "task_running": "執行中...", + "task_running": _RUNNING, "task_busy": "已在執行中:請等它結束,或先停止。", "task_stopping": "正在停止...", "task_stopped": "已停止。", diff --git a/je_auto_control/gui/main_widget.py b/je_auto_control/gui/main_widget.py index 2af3c61a2..b002b45c7 100644 --- a/je_auto_control/gui/main_widget.py +++ b/je_auto_control/gui/main_widget.py @@ -1,6 +1,6 @@ """Workspace widget: owns the tab bar and the registry of every tab it can open.""" import json -from typing import Any, Dict, List, Optional, Union +from typing import Any, Dict, List, Optional from PySide6.QtCore import QTimer, Signal, QObject from PySide6.QtGui import QKeyEvent, Qt @@ -175,7 +175,7 @@ def _build_remote_desktop_tab() -> QWidget: # --- tab registry API ---------------------------------------------------- def _add_tab( - self, key: str, title_key: str, widget: Union[QWidget, WidgetFactory], + self, key: str, title_key: str, widget: QWidget | WidgetFactory, category: str = "core", default_visible: bool = False, actions: MenuActions = (), scrollable: bool = True, ) -> None: diff --git a/je_auto_control/gui/script_builder/command_schema.py b/je_auto_control/gui/script_builder/command_schema.py index 232f4ae32..f12bc4a90 100644 --- a/je_auto_control/gui/script_builder/command_schema.py +++ b/je_auto_control/gui/script_builder/command_schema.py @@ -59,6 +59,13 @@ class CommandSpec: _POINT_PLACEHOLDER = "[10, 20]" _RECT_PLACEHOLDER = "[x, y, width, height]" _RECT4_PLACEHOLDER = "[x, y, w, h]" +_MODIFIER_PLACEHOLDER = "ctrl | command" +_UNICODE_TEXT_PLACEHOLDER = "café 🚀 値" +_SIZE_PLACEHOLDER = "[width, height]" +_JOURNAL_PLACEHOLDER = "action_journal.jsonl" +_TIMEOUT_PLACEHOLDER = "timeout seconds" +_POLL_PLACEHOLDER = "poll interval seconds" +_RGB_PLACEHOLDER = "[r, g, b]" _APPROVALS_DIR = ".approvals" _DOTTED_KEY_PLACEHOLDER = "db.host" _POINTS_JSON_PLACEHOLDER = '[{"x":..,"y":..,"width":..,"height":..}]' @@ -99,182 +106,186 @@ def _add_mobile_specs(specs: List[CommandSpec]) -> None: def _add_mouse_specs(specs: List[CommandSpec]) -> None: - specs.append(CommandSpec( - "AC_click_mouse", "Mouse", "Click Mouse", - fields=( - FieldSpec("mouse_keycode", FieldType.ENUM, choices=_MOUSE_BUTTONS, - default="mouse_left"), - FieldSpec("x", FieldType.INT, optional=True), - FieldSpec("y", FieldType.INT, optional=True), - FieldSpec("clicks", FieldType.INT, optional=True, default=1, - min_value=1), - FieldSpec("interval", FieldType.FLOAT, optional=True, default=0.0, - min_value=0.0), + specs.extend([ + CommandSpec( + "AC_click_mouse", "Mouse", "Click Mouse", + fields=( + FieldSpec("mouse_keycode", FieldType.ENUM, choices=_MOUSE_BUTTONS, + default="mouse_left"), + FieldSpec("x", FieldType.INT, optional=True), + FieldSpec("y", FieldType.INT, optional=True), + FieldSpec("clicks", FieldType.INT, optional=True, default=1, + min_value=1), + FieldSpec("interval", FieldType.FLOAT, optional=True, default=0.0, + min_value=0.0), + ), ), - )) - specs.append(CommandSpec( - "AC_set_mouse_position", "Mouse", "Move Mouse To", - fields=( - FieldSpec("x", FieldType.INT, default=0), - FieldSpec("y", FieldType.INT, default=0), + CommandSpec( + "AC_set_mouse_position", "Mouse", "Move Mouse To", + fields=( + FieldSpec("x", FieldType.INT, default=0), + FieldSpec("y", FieldType.INT, default=0), + ), ), - )) - specs.append(CommandSpec( - "AC_human_move", "Mouse", "Human-like Move To", - fields=( - FieldSpec("x", FieldType.INT, default=0), - FieldSpec("y", FieldType.INT, default=0), - FieldSpec("duration_s", FieldType.FLOAT, optional=True, - default=0.4, min_value=0.0), - FieldSpec("curve", FieldType.FLOAT, optional=True, default=0.2), - FieldSpec("overshoot", FieldType.FLOAT, optional=True, - default=0.0), - FieldSpec("jitter", FieldType.FLOAT, optional=True, default=1.0), - FieldSpec("seed", FieldType.INT, optional=True), - ), - description="Move the cursor along a curved, human-like path.", - )) - specs.append(CommandSpec( - "AC_press_mouse", "Mouse", "Press Mouse Button", - fields=( - FieldSpec("mouse_keycode", FieldType.ENUM, choices=_MOUSE_BUTTONS, - default="mouse_left"), - FieldSpec("x", FieldType.INT, optional=True), - FieldSpec("y", FieldType.INT, optional=True), + CommandSpec( + "AC_human_move", "Mouse", "Human-like Move To", + fields=( + FieldSpec("x", FieldType.INT, default=0), + FieldSpec("y", FieldType.INT, default=0), + FieldSpec("duration_s", FieldType.FLOAT, optional=True, + default=0.4, min_value=0.0), + FieldSpec("curve", FieldType.FLOAT, optional=True, default=0.2), + FieldSpec("overshoot", FieldType.FLOAT, optional=True, + default=0.0), + FieldSpec("jitter", FieldType.FLOAT, optional=True, default=1.0), + FieldSpec("seed", FieldType.INT, optional=True), + ), + description="Move the cursor along a curved, human-like path.", ), - )) - specs.append(CommandSpec( - "AC_release_mouse", "Mouse", "Release Mouse Button", - fields=( - FieldSpec("mouse_keycode", FieldType.ENUM, choices=_MOUSE_BUTTONS, - default="mouse_left"), - FieldSpec("x", FieldType.INT, optional=True), - FieldSpec("y", FieldType.INT, optional=True), + CommandSpec( + "AC_press_mouse", "Mouse", "Press Mouse Button", + fields=( + FieldSpec("mouse_keycode", FieldType.ENUM, choices=_MOUSE_BUTTONS, + default="mouse_left"), + FieldSpec("x", FieldType.INT, optional=True), + FieldSpec("y", FieldType.INT, optional=True), + ), ), - )) - specs.append(CommandSpec( - "AC_mouse_scroll", "Mouse", "Scroll Wheel", - fields=( - FieldSpec("scroll_value", FieldType.INT, default=1), - FieldSpec("x", FieldType.INT, optional=True), - FieldSpec("y", FieldType.INT, optional=True), + CommandSpec( + "AC_release_mouse", "Mouse", "Release Mouse Button", + fields=( + FieldSpec("mouse_keycode", FieldType.ENUM, choices=_MOUSE_BUTTONS, + default="mouse_left"), + FieldSpec("x", FieldType.INT, optional=True), + FieldSpec("y", FieldType.INT, optional=True), + ), ), - )) - specs.append(CommandSpec( - "AC_get_mouse_position", "Mouse", "Get Mouse Position" - )) + CommandSpec( + "AC_mouse_scroll", "Mouse", "Scroll Wheel", + fields=( + FieldSpec("scroll_value", FieldType.INT, default=1), + FieldSpec("x", FieldType.INT, optional=True), + FieldSpec("y", FieldType.INT, optional=True), + ), + ), + CommandSpec( + "AC_get_mouse_position", "Mouse", "Get Mouse Position" + ), + ]) def _add_keyboard_specs(specs: List[CommandSpec]) -> None: - specs.append(CommandSpec( - "AC_type_keyboard", "Keyboard", "Type Key", - fields=( - FieldSpec("keycode", FieldType.STRING, placeholder="e.g. a, enter, 65"), + specs.extend([ + CommandSpec( + "AC_type_keyboard", "Keyboard", "Type Key", + fields=( + FieldSpec("keycode", FieldType.STRING, placeholder="e.g. a, enter, 65"), + ), ), - )) - specs.append(CommandSpec( - "AC_press_keyboard_key", "Keyboard", "Press Key", - fields=( - FieldSpec("keycode", FieldType.STRING, placeholder="e.g. shift"), + CommandSpec( + "AC_press_keyboard_key", "Keyboard", "Press Key", + fields=( + FieldSpec("keycode", FieldType.STRING, placeholder="e.g. shift"), + ), ), - )) - specs.append(CommandSpec( - "AC_release_keyboard_key", "Keyboard", "Release Key", - fields=( - FieldSpec("keycode", FieldType.STRING, placeholder="e.g. shift"), + CommandSpec( + "AC_release_keyboard_key", "Keyboard", "Release Key", + fields=( + FieldSpec("keycode", FieldType.STRING, placeholder="e.g. shift"), + ), ), - )) - specs.append(CommandSpec( - "AC_write", "Keyboard", "Write Text", - fields=( - FieldSpec("write_string", FieldType.STRING, placeholder="Hello, world"), + CommandSpec( + "AC_write", "Keyboard", "Write Text", + fields=( + FieldSpec("write_string", FieldType.STRING, placeholder="Hello, world"), + ), ), - )) - specs.append(CommandSpec( - "AC_write_secret", "Keyboard", "Write Secret Text", - fields=(FieldSpec("secret", FieldType.STRING),), - description="Type a password or token exactly; never logged, recorded or returned.", - )) - specs.append(CommandSpec( - "AC_human_type", "Keyboard", "Human-like Type", - fields=( - FieldSpec("text", FieldType.STRING, placeholder="Hello, world"), - FieldSpec("base_delay", FieldType.FLOAT, optional=True, - default=0.05, min_value=0.0), - FieldSpec("jitter", FieldType.FLOAT, optional=True, default=0.04, - min_value=0.0), - FieldSpec("pause_chance", FieldType.FLOAT, optional=True, - default=0.0, min_value=0.0, max_value=1.0), - FieldSpec("seed", FieldType.INT, optional=True), - ), - description="Type text with randomized per-key delays.", - )) - specs.append(CommandSpec( - "AC_set_field_text", "Keyboard", "Set Field Text", - fields=( - FieldSpec("text", FieldType.STRING, placeholder="new value"), - FieldSpec("clear", FieldType.ENUM, choices=("select_all", "none"), - optional=True, default="select_all"), - FieldSpec("paste", FieldType.BOOL, optional=True, default=False), - FieldSpec("modifier", FieldType.STRING, optional=True, - default="ctrl", placeholder="ctrl | command"), - ), - description="Clear the focused field then enter text (paste for Unicode).", - )) - specs.append(CommandSpec( - "AC_hold_key", "Keyboard", "Hold Key", - fields=( - FieldSpec("key", FieldType.STRING, placeholder="e.g. key_d, space"), - FieldSpec("duration_s", FieldType.FLOAT, default=1.0, - min_value=0.01), - FieldSpec("rate_hz", FieldType.FLOAT, optional=True, - placeholder="auto-repeat presses/sec (blank = hold)"), + CommandSpec( + "AC_write_secret", "Keyboard", "Write Secret Text", + fields=(FieldSpec("secret", FieldType.STRING),), + description="Type a password or token exactly; never logged, recorded or returned.", ), - description="Hold a key for a duration, or auto-repeat it at rate_hz.", - )) - specs.append(CommandSpec( - "AC_type_unicode", "Keyboard", "Type Unicode (emoji / CJK)", - fields=( - FieldSpec("text", FieldType.STRING, placeholder="café 🚀 値"), - FieldSpec("modifier", FieldType.STRING, optional=True, - default="ctrl", placeholder="ctrl | command"), + CommandSpec( + "AC_human_type", "Keyboard", "Human-like Type", + fields=( + FieldSpec("text", FieldType.STRING, placeholder="Hello, world"), + FieldSpec("base_delay", FieldType.FLOAT, optional=True, + default=0.05, min_value=0.0), + FieldSpec("jitter", FieldType.FLOAT, optional=True, default=0.04, + min_value=0.0), + FieldSpec("pause_chance", FieldType.FLOAT, optional=True, + default=0.0, min_value=0.0, max_value=1.0), + FieldSpec("seed", FieldType.INT, optional=True), + ), + description="Type text with randomized per-key delays.", ), - description="Enter any Unicode text via clipboard paste (write can't).", - )) - specs.append(CommandSpec( - "AC_type_unicode_keys", "Keyboard", "Type Unicode (key events)", - fields=( - FieldSpec("text", FieldType.STRING, placeholder="café 🚀 値"), + CommandSpec( + "AC_set_field_text", "Keyboard", "Set Field Text", + fields=( + FieldSpec("text", FieldType.STRING, placeholder="new value"), + FieldSpec("clear", FieldType.ENUM, choices=("select_all", "none"), + optional=True, default="select_all"), + FieldSpec("paste", FieldType.BOOL, optional=True, default=False), + FieldSpec("modifier", FieldType.STRING, optional=True, + default="ctrl", placeholder=_MODIFIER_PLACEHOLDER), + ), + description="Clear the focused field then enter text (paste for Unicode).", ), - description="Enter any Unicode text as key events; leaves the clipboard " - "untouched. Needs a backend that supports it (Windows).", - )) - specs.append(CommandSpec( - "AC_type_unicode_text", "Keyboard", "Type Unicode (best route)", - fields=( - FieldSpec("text", FieldType.STRING, placeholder="café 🚀 値"), - FieldSpec("modifier", FieldType.STRING, optional=True, - default="ctrl", placeholder="ctrl | command"), + CommandSpec( + "AC_hold_key", "Keyboard", "Hold Key", + fields=( + FieldSpec("key", FieldType.STRING, placeholder="e.g. key_d, space"), + FieldSpec("duration_s", FieldType.FLOAT, default=1.0, + min_value=0.01), + FieldSpec("rate_hz", FieldType.FLOAT, optional=True, + placeholder="auto-repeat presses/sec (blank = hold)"), + ), + description="Hold a key for a duration, or auto-repeat it at rate_hz.", ), - description="Enter any Unicode text: key events where available, " - "clipboard paste otherwise.", - )) - specs.append(CommandSpec( - "AC_with_modifiers", "Keyboard", "With Modifiers Held", - fields=( - FieldSpec("modifiers", FieldType.STRING, placeholder="ctrl+shift"), - FieldSpec("actions", FieldType.STRING, - placeholder='[["AC_click_mouse", {...}], ...]'), + CommandSpec( + "AC_type_unicode", "Keyboard", "Type Unicode (emoji / CJK)", + fields=( + FieldSpec("text", FieldType.STRING, placeholder=_UNICODE_TEXT_PLACEHOLDER), + FieldSpec("modifier", FieldType.STRING, optional=True, + default="ctrl", placeholder=_MODIFIER_PLACEHOLDER), + ), + description="Enter any Unicode text via clipboard paste (write can't).", ), - description="Run nested actions while modifiers are held (release-safe).", - )) - specs.append(CommandSpec( - "AC_hotkey", "Keyboard", "Hotkey", - fields=( - FieldSpec("key_code_list", FieldType.STRING, - placeholder="ctrl, shift, s"), + CommandSpec( + "AC_type_unicode_keys", "Keyboard", "Type Unicode (key events)", + fields=( + FieldSpec("text", FieldType.STRING, placeholder=_UNICODE_TEXT_PLACEHOLDER), + ), + description="Enter any Unicode text as key events; leaves the clipboard " + "untouched. Needs a backend that supports it (Windows).", ), - )) + CommandSpec( + "AC_type_unicode_text", "Keyboard", "Type Unicode (best route)", + fields=( + FieldSpec("text", FieldType.STRING, placeholder=_UNICODE_TEXT_PLACEHOLDER), + FieldSpec("modifier", FieldType.STRING, optional=True, + default="ctrl", placeholder=_MODIFIER_PLACEHOLDER), + ), + description="Enter any Unicode text: key events where available, " + "clipboard paste otherwise.", + ), + CommandSpec( + "AC_with_modifiers", "Keyboard", "With Modifiers Held", + fields=( + FieldSpec("modifiers", FieldType.STRING, placeholder="ctrl+shift"), + FieldSpec("actions", FieldType.STRING, + placeholder='[["AC_click_mouse", {...}], ...]'), + ), + description="Run nested actions while modifiers are held (release-safe).", + ), + CommandSpec( + "AC_hotkey", "Keyboard", "Hotkey", + fields=( + FieldSpec("key_code_list", FieldType.STRING, + placeholder="ctrl, shift, s"), + ), + ), + ]) def _add_screen_specs(specs: List[CommandSpec]) -> None: @@ -290,619 +301,623 @@ def _add_screen_specs(specs: List[CommandSpec]) -> None: def _add_image_specs(specs: List[CommandSpec]) -> None: - specs.append(CommandSpec( - "AC_locate_image_center", "Image", "Locate Image", - fields=( - FieldSpec("image", FieldType.FILE_PATH), - FieldSpec("detect_threshold", FieldType.FLOAT, optional=True, - default=1.0, min_value=0.0, max_value=1.0), - FieldSpec("draw_image", FieldType.BOOL, optional=True, default=False), + specs.extend([ + CommandSpec( + "AC_locate_image_center", "Image", "Locate Image", + fields=( + FieldSpec("image", FieldType.FILE_PATH), + FieldSpec("detect_threshold", FieldType.FLOAT, optional=True, + default=1.0, min_value=0.0, max_value=1.0), + FieldSpec("draw_image", FieldType.BOOL, optional=True, default=False), + ), ), - )) - specs.append(CommandSpec( - "AC_locate_and_click", "Image", "Locate & Click", - fields=( - FieldSpec("image", FieldType.FILE_PATH), - FieldSpec("mouse_keycode", FieldType.ENUM, choices=_MOUSE_BUTTONS, - default="mouse_left"), - FieldSpec("detect_threshold", FieldType.FLOAT, optional=True, - default=1.0, min_value=0.0, max_value=1.0), - FieldSpec("draw_image", FieldType.BOOL, optional=True, default=False), + CommandSpec( + "AC_locate_and_click", "Image", "Locate & Click", + fields=( + FieldSpec("image", FieldType.FILE_PATH), + FieldSpec("mouse_keycode", FieldType.ENUM, choices=_MOUSE_BUTTONS, + default="mouse_left"), + FieldSpec("detect_threshold", FieldType.FLOAT, optional=True, + default=1.0, min_value=0.0, max_value=1.0), + FieldSpec("draw_image", FieldType.BOOL, optional=True, default=False), + ), ), - )) - specs.append(CommandSpec( - "AC_locate_all_image", "Image", "Locate All", - fields=( - FieldSpec("image", FieldType.FILE_PATH), - FieldSpec("detect_threshold", FieldType.FLOAT, optional=True, - default=1.0, min_value=0.0, max_value=1.0), + CommandSpec( + "AC_locate_all_image", "Image", "Locate All", + fields=( + FieldSpec("image", FieldType.FILE_PATH), + FieldSpec("detect_threshold", FieldType.FLOAT, optional=True, + default=1.0, min_value=0.0, max_value=1.0), + ), ), - )) - specs.append(CommandSpec( - "AC_match_template", "Image", "Match Template (scored)", - fields=( - FieldSpec("template", FieldType.FILE_PATH), - FieldSpec("min_score", FieldType.FLOAT, optional=True, default=0.8, - min_value=0.0, max_value=1.0), - FieldSpec("scales", FieldType.STRING, optional=True, - placeholder=_SCALES_PLACEHOLDER), - FieldSpec("region", FieldType.STRING, optional=True, - placeholder=_RECT_PLACEHOLDER), - ), - description="Locate a template and return its confidence score + scale.", - )) - specs.append(CommandSpec( - "AC_match_template_all", "Image", "Match Template All (scored)", - fields=( - FieldSpec("template", FieldType.FILE_PATH), - FieldSpec("min_score", FieldType.FLOAT, optional=True, default=0.8, - min_value=0.0, max_value=1.0), - FieldSpec("max_results", FieldType.INT, optional=True, default=20), - FieldSpec("nms_iou", FieldType.FLOAT, optional=True, default=0.3, - min_value=0.0, max_value=1.0), - ), - description="Find every occurrence of a template (scored, NMS-deduped).", - )) - specs.append(CommandSpec( - "AC_match_masked", "Image", "Match Masked Template", - fields=( - FieldSpec("template", FieldType.FILE_PATH), - FieldSpec("mask", FieldType.FILE_PATH, optional=True), - FieldSpec("min_score", FieldType.FLOAT, optional=True, default=0.9, - min_value=0.0, max_value=1.0), - FieldSpec("region", FieldType.STRING, optional=True, - placeholder=_RECT_PLACEHOLDER), - ), - description="Match counting only opaque/masked pixels (alpha or mask).", - )) - specs.append(CommandSpec( - "AC_match_masked_all", "Image", "Match Masked Template All", - fields=( - FieldSpec("template", FieldType.FILE_PATH), - FieldSpec("mask", FieldType.FILE_PATH, optional=True), - FieldSpec("min_score", FieldType.FLOAT, optional=True, default=0.9, - min_value=0.0, max_value=1.0), - FieldSpec("max_results", FieldType.INT, optional=True, default=20), - FieldSpec("nms_iou", FieldType.FLOAT, optional=True, default=0.3, - min_value=0.0, max_value=1.0), - ), - description="Find every masked match of a template (NMS-deduped).", - )) - specs.append(CommandSpec( - "AC_match_rotated", "Image", "Match Template (rotated)", - fields=( - FieldSpec("template", FieldType.FILE_PATH), - FieldSpec("min_score", FieldType.FLOAT, optional=True, default=0.8, - min_value=0.0, max_value=1.0), - FieldSpec("angles", FieldType.STRING, optional=True, - placeholder="[-10, 0, 10]"), - FieldSpec("scales", FieldType.STRING, optional=True, - placeholder=_SCALES_PLACEHOLDER), - FieldSpec("region", FieldType.STRING, optional=True, - placeholder=_RECT_PLACEHOLDER), - ), - description="Locate a template tolerating rotation + scale; reports angle.", - )) - specs.append(CommandSpec( - "AC_match_rotated_all", "Image", "Match Template All (rotated)", - fields=( - FieldSpec("template", FieldType.FILE_PATH), - FieldSpec("min_score", FieldType.FLOAT, optional=True, default=0.8, - min_value=0.0, max_value=1.0), - FieldSpec("angles", FieldType.STRING, optional=True, - placeholder="[-10, 0, 10]"), - FieldSpec("scales", FieldType.STRING, optional=True, - placeholder=_SCALES_PLACEHOLDER), - FieldSpec("max_results", FieldType.INT, optional=True, default=20), - FieldSpec("nms_iou", FieldType.FLOAT, optional=True, default=0.3, - min_value=0.0, max_value=1.0), - ), - description="Find every rotation/scale-tolerant match (NMS-deduped).", - )) - specs.append(CommandSpec( - "AC_match_with_trust", "Image", "Match Template (trust-scored)", - fields=( - FieldSpec("template", FieldType.FILE_PATH), - FieldSpec("min_score", FieldType.FLOAT, optional=True, default=0.0, - min_value=0.0, max_value=1.0), - FieldSpec("ambiguous_ratio", FieldType.FLOAT, optional=True, - default=0.9, min_value=0.0, max_value=1.0), - FieldSpec("scales", FieldType.STRING, optional=True, - placeholder=_SCALES_PLACEHOLDER), - FieldSpec("region", FieldType.STRING, optional=True, - placeholder=_RECT_PLACEHOLDER), - ), - description="Match a template and flag if it is ambiguous (duplicate peak).", - )) - specs.append(CommandSpec( - "AC_match_auto", "Image", "Match Template (auto-threshold)", - fields=( - FieldSpec("template", FieldType.FILE_PATH), - FieldSpec("floor", FieldType.FLOAT, optional=True, default=0.5, - min_value=0.0, max_value=1.0), - FieldSpec("max_results", FieldType.INT, optional=True, default=20), - FieldSpec("region", FieldType.STRING, optional=True, - placeholder=_RECT_PLACEHOLDER), - ), - description="Find a template with an Otsu auto-threshold (no min_score).", - )) - specs.append(CommandSpec( - "AC_auto_threshold", "Image", "Auto Threshold (Otsu on scores)", - fields=( - FieldSpec("template", FieldType.FILE_PATH), - FieldSpec("region", FieldType.STRING, optional=True, - placeholder=_RECT_PLACEHOLDER), + CommandSpec( + "AC_match_template", "Image", "Match Template (scored)", + fields=( + FieldSpec("template", FieldType.FILE_PATH), + FieldSpec("min_score", FieldType.FLOAT, optional=True, default=0.8, + min_value=0.0, max_value=1.0), + FieldSpec("scales", FieldType.STRING, optional=True, + placeholder=_SCALES_PLACEHOLDER), + FieldSpec("region", FieldType.STRING, optional=True, + placeholder=_RECT_PLACEHOLDER), + ), + description="Locate a template and return its confidence score + scale.", ), - description="Derive a match threshold + separability from the score map.", - )) - specs.append(CommandSpec( - "AC_edge_match", "Image", "Match Template (edge shape)", - fields=( - FieldSpec("template", FieldType.FILE_PATH), - FieldSpec("min_score", FieldType.FLOAT, optional=True, default=0.7, - min_value=0.0, max_value=1.0), - FieldSpec("scales", FieldType.STRING, optional=True, - placeholder=_SCALES_PLACEHOLDER), - FieldSpec("region", FieldType.STRING, optional=True, - placeholder=_RECT_PLACEHOLDER), - ), - description="Locate by edge shape (Chamfer) — robust to fill / theme / AA.", - )) - specs.append(CommandSpec( - "AC_edge_match_all", "Image", "Match Template All (edge shape)", - fields=( - FieldSpec("template", FieldType.FILE_PATH), - FieldSpec("min_score", FieldType.FLOAT, optional=True, default=0.7, - min_value=0.0, max_value=1.0), - FieldSpec("max_results", FieldType.INT, optional=True, default=20), - FieldSpec("nms_iou", FieldType.FLOAT, optional=True, default=0.3, - min_value=0.0, max_value=1.0), - ), - description="Find every edge-shape (Chamfer) match (NMS-deduped).", - )) - specs.append(CommandSpec( - "AC_match_subpixel", "Image", "Match Template (sub-pixel)", - fields=( - FieldSpec("template", FieldType.FILE_PATH), - FieldSpec("min_score", FieldType.FLOAT, optional=True, default=0.0, - min_value=0.0, max_value=1.0), - FieldSpec("region", FieldType.STRING, optional=True, - placeholder=_RECT_PLACEHOLDER), + CommandSpec( + "AC_match_template_all", "Image", "Match Template All (scored)", + fields=( + FieldSpec("template", FieldType.FILE_PATH), + FieldSpec("min_score", FieldType.FLOAT, optional=True, default=0.8, + min_value=0.0, max_value=1.0), + FieldSpec("max_results", FieldType.INT, optional=True, default=20), + FieldSpec("nms_iou", FieldType.FLOAT, optional=True, default=0.3, + min_value=0.0, max_value=1.0), + ), + description="Find every occurrence of a template (scored, NMS-deduped).", ), - description="Match with a sub-pixel-refined centre (drag / slider precision).", - )) - specs.append(CommandSpec( - "AC_match_ensemble", "Image", "Match Ensemble (vote references)", - fields=( - FieldSpec("templates", FieldType.STRING, - placeholder='["default.png", "hover.png", "pressed.png"]'), - FieldSpec("min_score", FieldType.FLOAT, optional=True, default=0.8, - min_value=0.0, max_value=1.0), - FieldSpec("agree_px", FieldType.INT, optional=True, default=10), - FieldSpec("min_votes", FieldType.INT, optional=True, default=2), - FieldSpec("region", FieldType.STRING, optional=True, - placeholder=_RECT_PLACEHOLDER), - ), - description="Vote several reference crops onto one consensus location.", - )) - specs.append(CommandSpec( - "AC_vote_centers", "Image", "Vote Centers (consensus)", - fields=( - FieldSpec("centers", FieldType.STRING, - placeholder="[[100, 100], [102, 98]]"), - FieldSpec("agree_px", FieldType.INT, optional=True, default=10), - FieldSpec("min_votes", FieldType.INT, optional=True, default=2), + CommandSpec( + "AC_match_masked", "Image", "Match Masked Template", + fields=( + FieldSpec("template", FieldType.FILE_PATH), + FieldSpec("mask", FieldType.FILE_PATH, optional=True), + FieldSpec("min_score", FieldType.FLOAT, optional=True, default=0.9, + min_value=0.0, max_value=1.0), + FieldSpec("region", FieldType.STRING, optional=True, + placeholder=_RECT_PLACEHOLDER), + ), + description="Match counting only opaque/masked pixels (alpha or mask).", ), - description="Vote candidate hit centres into one consensus target.", - )) - specs.append(CommandSpec( - "AC_match_color", "Image", "Match Template (colour/HSV)", - fields=( - FieldSpec("template", FieldType.FILE_PATH), - FieldSpec("channels", FieldType.STRING, optional=True, - placeholder='["h", "s"]'), - FieldSpec("min_score", FieldType.FLOAT, optional=True, default=0.7, - min_value=0.0, max_value=1.0), - FieldSpec("region", FieldType.STRING, optional=True, - placeholder=_REGION_PLACEHOLDER), - ), - description="Match by colour on HSV channels (red vs green, not grayscale).", - )) - specs.append(CommandSpec( - "AC_match_color_all", "Image", "Match Template All (colour/HSV)", - fields=( - FieldSpec("template", FieldType.FILE_PATH), - FieldSpec("channels", FieldType.STRING, optional=True, - placeholder='["h", "s"]'), - FieldSpec("min_score", FieldType.FLOAT, optional=True, default=0.7, - min_value=0.0, max_value=1.0), - FieldSpec("max_results", FieldType.INT, optional=True, default=20), - FieldSpec("nms_iou", FieldType.FLOAT, optional=True, default=0.3, - min_value=0.0, max_value=1.0), - ), - description="Find every colour (HSV) match of a template (NMS-deduped).", - )) - specs.append(CommandSpec( - "AC_region_stability", "Image", "Region Stability (frames)", - fields=( - FieldSpec("frames", FieldType.STRING, - placeholder='["f1.png", "f2.png", "f3.png"]'), - FieldSpec("settle_threshold", FieldType.FLOAT, optional=True, - default=0.99, min_value=0.0, max_value=1.0), + CommandSpec( + "AC_match_masked_all", "Image", "Match Masked Template All", + fields=( + FieldSpec("template", FieldType.FILE_PATH), + FieldSpec("mask", FieldType.FILE_PATH, optional=True), + FieldSpec("min_score", FieldType.FLOAT, optional=True, default=0.9, + min_value=0.0, max_value=1.0), + FieldSpec("max_results", FieldType.INT, optional=True, default=20), + FieldSpec("nms_iou", FieldType.FLOAT, optional=True, default=0.3, + min_value=0.0, max_value=1.0), + ), + description="Find every masked match of a template (NMS-deduped).", ), - description="Is a frame sequence settled? (consecutive-frame SSIM).", - )) - specs.append(CommandSpec( - "AC_match_persistence", "Image", "Match Persistence (frames)", - fields=( - FieldSpec("template", FieldType.FILE_PATH), - FieldSpec("frames", FieldType.STRING, - placeholder='["f1.png", "f2.png", "f3.png"]'), - FieldSpec("min_score", FieldType.FLOAT, optional=True, default=0.8, - min_value=0.0, max_value=1.0), - FieldSpec("agree_px", FieldType.INT, optional=True, default=8), - ), - description="Does a template match hold steady across frames? (jitter).", - )) - specs.append(CommandSpec( - "AC_grid_cells", "Image", "Grid Cells (coarse grounding)", - fields=( - FieldSpec("rows", FieldType.INT, default=3), - FieldSpec("cols", FieldType.INT, default=3), - FieldSpec("region", FieldType.STRING, optional=True, - placeholder=_REGION_PLACEHOLDER), + CommandSpec( + "AC_match_rotated", "Image", "Match Template (rotated)", + fields=( + FieldSpec("template", FieldType.FILE_PATH), + FieldSpec("min_score", FieldType.FLOAT, optional=True, default=0.8, + min_value=0.0, max_value=1.0), + FieldSpec("angles", FieldType.STRING, optional=True, + placeholder="[-10, 0, 10]"), + FieldSpec("scales", FieldType.STRING, optional=True, + placeholder=_SCALES_PLACEHOLDER), + FieldSpec("region", FieldType.STRING, optional=True, + placeholder=_RECT_PLACEHOLDER), + ), + description="Locate a template tolerating rotation + scale; reports angle.", ), - description="Label an rows x cols grid over the screen for VLM grounding.", - )) - specs.append(CommandSpec( - "AC_cell_for_point", "Image", "Cell For Point", - fields=( - FieldSpec("x", FieldType.INT), - FieldSpec("y", FieldType.INT), - FieldSpec("rows", FieldType.INT, default=3), - FieldSpec("cols", FieldType.INT, default=3), - FieldSpec("region", FieldType.STRING, optional=True, - placeholder=_REGION_PLACEHOLDER), - ), - description="Return the grid cell label containing a screen point.", - )) - specs.append(CommandSpec( - "AC_point_for_cell", "Image", "Point For Cell", - fields=( - FieldSpec("label", FieldType.STRING, placeholder="C3"), - FieldSpec("rows", FieldType.INT, default=3), - FieldSpec("cols", FieldType.INT, default=3), - FieldSpec("region", FieldType.STRING, optional=True, - placeholder=_REGION_PLACEHOLDER), + CommandSpec( + "AC_match_rotated_all", "Image", "Match Template All (rotated)", + fields=( + FieldSpec("template", FieldType.FILE_PATH), + FieldSpec("min_score", FieldType.FLOAT, optional=True, default=0.8, + min_value=0.0, max_value=1.0), + FieldSpec("angles", FieldType.STRING, optional=True, + placeholder="[-10, 0, 10]"), + FieldSpec("scales", FieldType.STRING, optional=True, + placeholder=_SCALES_PLACEHOLDER), + FieldSpec("max_results", FieldType.INT, optional=True, default=20), + FieldSpec("nms_iou", FieldType.FLOAT, optional=True, default=0.3, + min_value=0.0, max_value=1.0), + ), + description="Find every rotation/scale-tolerant match (NMS-deduped).", ), - description="Return the centre point of a named grid cell (click target).", - )) - specs.append(CommandSpec( - "AC_find_color_region", "Image", "Find Colour Region", - fields=( - FieldSpec("rgb", FieldType.STRING, placeholder="[0, 200, 0]"), - FieldSpec("tolerance", FieldType.INT, optional=True, default=20), - FieldSpec("min_area", FieldType.INT, optional=True, default=50), - FieldSpec("region", FieldType.STRING, optional=True, - placeholder=_REGION_PLACEHOLDER), + CommandSpec( + "AC_match_with_trust", "Image", "Match Template (trust-scored)", + fields=( + FieldSpec("template", FieldType.FILE_PATH), + FieldSpec("min_score", FieldType.FLOAT, optional=True, default=0.0, + min_value=0.0, max_value=1.0), + FieldSpec("ambiguous_ratio", FieldType.FLOAT, optional=True, + default=0.9, min_value=0.0, max_value=1.0), + FieldSpec("scales", FieldType.STRING, optional=True, + placeholder=_SCALES_PLACEHOLDER), + FieldSpec("region", FieldType.STRING, optional=True, + placeholder=_RECT_PLACEHOLDER), + ), + description="Match a template and flag if it is ambiguous (duplicate peak).", ), - description="Locate regions by colour (status light / banner / fill).", - )) - specs.append(CommandSpec( - "AC_ssim_compare", "Image", "SSIM Compare", - fields=( - FieldSpec("reference", FieldType.FILE_PATH), - FieldSpec("current", FieldType.FILE_PATH, optional=True), - FieldSpec("ignore", FieldType.STRING, optional=True, - placeholder="[[x, y, w, h], ...]"), - FieldSpec("region", FieldType.STRING, optional=True, - placeholder=_REGION_PLACEHOLDER), - ), - description="Structural-similarity score (0..1) vs reference / screen.", - )) - specs.append(CommandSpec( - "AC_ssim_changed_regions", "Image", "SSIM Changed Regions", - fields=( - FieldSpec("reference", FieldType.FILE_PATH), - FieldSpec("current", FieldType.FILE_PATH, optional=True), - FieldSpec("ignore", FieldType.STRING, optional=True, - placeholder="[[x, y, w, h], ...]"), - FieldSpec("threshold", FieldType.FLOAT, optional=True, default=0.35, - min_value=0.0, max_value=1.0), - FieldSpec("min_area", FieldType.INT, optional=True, default=50), - FieldSpec("region", FieldType.STRING, optional=True, - placeholder=_REGION_PLACEHOLDER), - ), - description="Boxes of the regions that structurally changed.", - )) - specs.append(CommandSpec( - "AC_feature_match", "Image", "Feature Match (ORB)", - fields=( - FieldSpec("template", FieldType.FILE_PATH), - FieldSpec("region", FieldType.STRING, optional=True, - placeholder=_RECT_PLACEHOLDER), - FieldSpec("max_features", FieldType.INT, optional=True, default=500), - FieldSpec("ratio", FieldType.FLOAT, optional=True, default=0.75, - min_value=0.0, max_value=1.0), - FieldSpec("min_inliers", FieldType.INT, optional=True, default=10), - ), - description="Locate a template under rotation / scale / theme change.", - )) - specs.append(CommandSpec( - "AC_find_shapes", "Image", "Find Shapes", - fields=( - FieldSpec("region", FieldType.STRING, optional=True, - placeholder=_RECT_PLACEHOLDER), - FieldSpec("min_area", FieldType.INT, optional=True, default=400), - FieldSpec("max_area", FieldType.INT, optional=True), + CommandSpec( + "AC_match_auto", "Image", "Match Template (auto-threshold)", + fields=( + FieldSpec("template", FieldType.FILE_PATH), + FieldSpec("floor", FieldType.FLOAT, optional=True, default=0.5, + min_value=0.0, max_value=1.0), + FieldSpec("max_results", FieldType.INT, optional=True, default=20), + FieldSpec("region", FieldType.STRING, optional=True, + placeholder=_RECT_PLACEHOLDER), + ), + description="Find a template with an Otsu auto-threshold (no min_score).", ), - description="Locate distinct shapes by edge/contour detection (no template).", - )) - specs.append(CommandSpec( - "AC_find_rectangles", "Image", "Find Rectangles", - fields=( - FieldSpec("region", FieldType.STRING, optional=True, - placeholder=_RECT_PLACEHOLDER), - FieldSpec("min_area", FieldType.INT, optional=True, default=400), - FieldSpec("max_area", FieldType.INT, optional=True), - FieldSpec("aspect_range", FieldType.STRING, optional=True, - placeholder="[1.5, 8.0]"), - FieldSpec("epsilon", FieldType.FLOAT, optional=True, default=0.04, - min_value=0.0, max_value=1.0), - ), - description="Locate rectangular regions (buttons / cards / input fields).", - )) - specs.append(CommandSpec( - "AC_preprocess_image", "Image", "Preprocess Image (OCR/match)", - fields=( - FieldSpec("output_path", FieldType.FILE_PATH), - FieldSpec("source", FieldType.FILE_PATH, optional=True), - FieldSpec("steps", FieldType.STRING, optional=True, - placeholder="grayscale,upscale,binarize"), - FieldSpec("scale", FieldType.FLOAT, optional=True, default=2.0), - FieldSpec("region", FieldType.STRING, optional=True, - placeholder=_REGION_PLACEHOLDER), - ), - description="Clean up an image for OCR / matching (grayscale/binarize/…).", - )) - specs.append(CommandSpec( - "AC_segment_hsv", "Image", "Segment by HSV", - fields=( - FieldSpec("lower_hsv", FieldType.STRING, placeholder="[40, 80, 80]"), - FieldSpec("upper_hsv", FieldType.STRING, placeholder="[80, 255, 255]"), - FieldSpec("min_area", FieldType.INT, optional=True, default=50), - FieldSpec("region", FieldType.STRING, optional=True, - placeholder=_REGION_PLACEHOLDER), + CommandSpec( + "AC_auto_threshold", "Image", "Auto Threshold (Otsu on scores)", + fields=( + FieldSpec("template", FieldType.FILE_PATH), + FieldSpec("region", FieldType.STRING, optional=True, + placeholder=_RECT_PLACEHOLDER), + ), + description="Derive a match threshold + separability from the score map.", ), - description="Locate regions inside an HSV band (lighting-robust).", - )) - specs.append(CommandSpec( - "AC_dominant_hue_regions", "Image", "Find Hue Regions", - fields=( - FieldSpec("hue", FieldType.INT, placeholder="0=red, 60=green, 120=blue"), - FieldSpec("hue_tol", FieldType.INT, optional=True, default=10), - FieldSpec("sat_min", FieldType.INT, optional=True, default=80), - FieldSpec("val_min", FieldType.INT, optional=True, default=80), - FieldSpec("min_area", FieldType.INT, optional=True, default=50), - FieldSpec("region", FieldType.STRING, optional=True, - placeholder=_REGION_PLACEHOLDER), - ), - description="Locate any shade of a hue, any brightness (handles red wrap).", - )) - specs.append(CommandSpec( - "AC_find_text_regions", "Image", "Find Text Regions (MSER)", - fields=( - FieldSpec("min_area", FieldType.INT, optional=True, default=60), - FieldSpec("max_area", FieldType.INT, optional=True), - FieldSpec("merge", FieldType.BOOL, optional=True, default=True), - FieldSpec("max_aspect", FieldType.FLOAT, optional=True, default=12.0), - FieldSpec("region", FieldType.STRING, optional=True, - placeholder=_RECT_PLACEHOLDER), - ), - description="Locate text regions without OCR (crop to feed an OCR engine).", - )) - specs.append(CommandSpec( - "AC_find_text_lines", "Image", "Find Text Lines (MSER)", - fields=( - FieldSpec("y_tolerance", FieldType.INT, optional=True, default=8), - FieldSpec("region", FieldType.STRING, optional=True, - placeholder=_RECT_PLACEHOLDER), + CommandSpec( + "AC_edge_match", "Image", "Match Template (edge shape)", + fields=( + FieldSpec("template", FieldType.FILE_PATH), + FieldSpec("min_score", FieldType.FLOAT, optional=True, default=0.7, + min_value=0.0, max_value=1.0), + FieldSpec("scales", FieldType.STRING, optional=True, + placeholder=_SCALES_PLACEHOLDER), + FieldSpec("region", FieldType.STRING, optional=True, + placeholder=_RECT_PLACEHOLDER), + ), + description="Locate by edge shape (Chamfer) — robust to fill / theme / AA.", ), - description="Locate horizontal text lines without OCR.", - )) - specs.append(CommandSpec( - "AC_find_lines", "Image", "Find Lines (Hough)", - fields=( - FieldSpec("min_length", FieldType.INT, optional=True, default=80), - FieldSpec("max_gap", FieldType.INT, optional=True, default=10), - FieldSpec("orientation", FieldType.ENUM, optional=True, default="any", - choices=("any", "horizontal", "vertical", "diagonal")), - FieldSpec("region", FieldType.STRING, optional=True, - placeholder=_RECT_PLACEHOLDER), - ), - description="Detect straight line segments on raw pixels.", - )) - specs.append(CommandSpec( - "AC_find_grid", "Image", "Find Table Grid", - fields=( - FieldSpec("min_length", FieldType.INT, optional=True, default=120), - FieldSpec("tol", FieldType.INT, optional=True, default=10), - FieldSpec("region", FieldType.STRING, optional=True, - placeholder=_RECT_PLACEHOLDER), + CommandSpec( + "AC_edge_match_all", "Image", "Match Template All (edge shape)", + fields=( + FieldSpec("template", FieldType.FILE_PATH), + FieldSpec("min_score", FieldType.FLOAT, optional=True, default=0.7, + min_value=0.0, max_value=1.0), + FieldSpec("max_results", FieldType.INT, optional=True, default=20), + FieldSpec("nms_iou", FieldType.FLOAT, optional=True, default=0.3, + min_value=0.0, max_value=1.0), + ), + description="Find every edge-shape (Chamfer) match (NMS-deduped).", ), - description="Recover a table's rows / columns / cells from its lines.", - )) - specs.append(CommandSpec( - "AC_find_separators", "Image", "Find Separator Lines", - fields=( - FieldSpec("axis", FieldType.ENUM, optional=True, default="horizontal", - choices=("horizontal", "vertical")), - FieldSpec("min_length", FieldType.INT, optional=True, default=120), - FieldSpec("tol", FieldType.INT, optional=True, default=10), - FieldSpec("region", FieldType.STRING, optional=True, - placeholder=_RECT_PLACEHOLDER), - ), - description="Coordinates of long divider lines along an axis.", - )) - specs.append(CommandSpec( - "AC_fuse_elements", "Image", "Fuse Element Boxes", - fields=( - FieldSpec("ocr", FieldType.STRING, optional=True, - placeholder=_POINTS_JSON_PLACEHOLDER), - FieldSpec("icon", FieldType.STRING, optional=True), - FieldSpec("a11y", FieldType.STRING, optional=True), - FieldSpec("iou_threshold", FieldType.FLOAT, optional=True, default=0.9, - min_value=0.0, max_value=1.0), - ), - description="Union OCR/icon/a11y boxes, dropping cross-source duplicates.", - )) - specs.append(CommandSpec( - "AC_reading_order", "Image", "Reading Order", - fields=( - FieldSpec("elements", FieldType.STRING, - placeholder=_POINTS_JSON_PLACEHOLDER), - FieldSpec("row_tol", FieldType.INT, optional=True, default=12), + CommandSpec( + "AC_match_subpixel", "Image", "Match Template (sub-pixel)", + fields=( + FieldSpec("template", FieldType.FILE_PATH), + FieldSpec("min_score", FieldType.FLOAT, optional=True, default=0.0, + min_value=0.0, max_value=1.0), + FieldSpec("region", FieldType.STRING, optional=True, + placeholder=_RECT_PLACEHOLDER), + ), + description="Match with a sub-pixel-refined centre (drag / slider precision).", ), - description="Order element boxes top-to-bottom, left-to-right (+ index).", - )) - specs.append(CommandSpec( - "AC_locate_chain", "Image", "Locate Chain (refine boxes)", - fields=( - FieldSpec("boxes", FieldType.STRING, - placeholder=_POINTS_JSON_PLACEHOLDER), - FieldSpec("ops", FieldType.STRING, - placeholder='[{"op":"filter","has_text":"OK"},{"op":"first"}]'), + CommandSpec( + "AC_match_ensemble", "Image", "Match Ensemble (vote references)", + fields=( + FieldSpec("templates", FieldType.STRING, + placeholder='["default.png", "hover.png", "pressed.png"]'), + FieldSpec("min_score", FieldType.FLOAT, optional=True, default=0.8, + min_value=0.0, max_value=1.0), + FieldSpec("agree_px", FieldType.INT, optional=True, default=10), + FieldSpec("min_votes", FieldType.INT, optional=True, default=2), + FieldSpec("region", FieldType.STRING, optional=True, + placeholder=_RECT_PLACEHOLDER), + ), + description="Vote several reference crops onto one consensus location.", ), - description="Refine candidate boxes (within / filter / reading / nth / …).", - )) - specs.append(CommandSpec( - "AC_image_histogram", "Image", "Image Histogram", - fields=( - FieldSpec("source", FieldType.FILE_PATH, optional=True), - FieldSpec("bins", FieldType.INT, optional=True, default=32), - FieldSpec("space", FieldType.ENUM, optional=True, default="hsv", - choices=("hsv", "rgb", "gray")), - FieldSpec("region", FieldType.STRING, optional=True, - placeholder=_REGION_PLACEHOLDER), - ), - description="Colour-histogram fingerprint of an image / the screen.", - )) - specs.append(CommandSpec( - "AC_histogram_changed", "Image", "Histogram Changed?", - fields=( - FieldSpec("reference", FieldType.FILE_PATH), - FieldSpec("current", FieldType.FILE_PATH, optional=True), - FieldSpec("method", FieldType.ENUM, optional=True, - default="correlation", - choices=("correlation", "chisqr", "intersection", - "bhattacharyya")), - FieldSpec("threshold", FieldType.FLOAT, optional=True, default=0.9, - min_value=0.0, max_value=1.0), - FieldSpec("space", FieldType.ENUM, optional=True, default="hsv", - choices=("hsv", "rgb", "gray")), - FieldSpec("region", FieldType.STRING, optional=True, - placeholder=_REGION_PLACEHOLDER), - ), - description="Detect a palette/view change vs a reference (illumination-robust).", - )) - specs.append(CommandSpec( - "AC_image_quality", "Image", "Image Quality", - fields=( - FieldSpec("source", FieldType.FILE_PATH, optional=True), - FieldSpec("region", FieldType.STRING, optional=True, - placeholder=_RECT_PLACEHOLDER), + CommandSpec( + "AC_vote_centers", "Image", "Vote Centers (consensus)", + fields=( + FieldSpec("centers", FieldType.STRING, + placeholder="[[100, 100], [102, 98]]"), + FieldSpec("agree_px", FieldType.INT, optional=True, default=10), + FieldSpec("min_votes", FieldType.INT, optional=True, default=2), + ), + description="Vote candidate hit centres into one consensus target.", ), - description="Sharpness / contrast / brightness of an image or the screen.", - )) - specs.append(CommandSpec( - "AC_quality_gate", "Image", "Quality Gate (OCR-ready?)", - fields=( - FieldSpec("source", FieldType.FILE_PATH, optional=True), - FieldSpec("region", FieldType.STRING, optional=True, - placeholder=_RECT_PLACEHOLDER), - FieldSpec("min_sharpness", FieldType.FLOAT, optional=True, - default=100.0), - FieldSpec("min_contrast", FieldType.FLOAT, optional=True, - default=12.0), - ), - description="Pass/fail an image for OCR readability with named issues.", - )) - specs.append(CommandSpec( - "AC_detect_scale", "Image", "Detect Display Scale (DPI)", - fields=( - FieldSpec("template", FieldType.FILE_PATH), - FieldSpec("haystack", FieldType.FILE_PATH, optional=True), - FieldSpec("region", FieldType.STRING, optional=True, - placeholder=_RECT_PLACEHOLDER), - FieldSpec("scales", FieldType.STRING, optional=True, - placeholder="[1.0, 1.25, 1.5, 1.75, 2.0]"), - ), - description="Infer the display scale a template renders at (visual DPI).", - )) - specs.append(CommandSpec( - "AC_scale_sweep", "Image", "Scale Sweep (per-scale scores)", - fields=( - FieldSpec("template", FieldType.FILE_PATH), - FieldSpec("haystack", FieldType.FILE_PATH, optional=True), - FieldSpec("region", FieldType.STRING, optional=True, - placeholder=_RECT_PLACEHOLDER), - FieldSpec("scales", FieldType.STRING, optional=True, - placeholder="[1.0, 1.25, 1.5, 1.75, 2.0]"), - ), - description="Per-scale match-score profile of a template.", - )) - saliency_fields = ( - FieldSpec("source", FieldType.FILE_PATH, optional=True), - FieldSpec("region", FieldType.STRING, optional=True, - placeholder=_RECT_PLACEHOLDER), - FieldSpec("size", FieldType.INT, optional=True, default=64), - FieldSpec("threshold", FieldType.FLOAT, optional=True), - FieldSpec("min_area", FieldType.INT, optional=True, default=4), - ) - specs.append(CommandSpec( - "AC_salient_regions", "Image", "Salient Regions", - fields=saliency_fields, - description="Visually salient regions (spectral-residual; where to look).", - )) - specs.append(CommandSpec( - "AC_most_salient", "Image", "Most Salient Region", - fields=saliency_fields, - description="The single most visually salient region of an image/screen.", - )) - specs.append(CommandSpec( - "AC_changed_regions", "Image", "Changed Regions (motion)", - fields=( - FieldSpec("before", FieldType.FILE_PATH), - FieldSpec("after", FieldType.FILE_PATH, optional=True), - FieldSpec("threshold", FieldType.INT, optional=True, default=25), - FieldSpec("min_area", FieldType.INT, optional=True, default=80), - FieldSpec("blur", FieldType.INT, optional=True, default=5), + CommandSpec( + "AC_match_color", "Image", "Match Template (colour/HSV)", + fields=( + FieldSpec("template", FieldType.FILE_PATH), + FieldSpec("channels", FieldType.STRING, optional=True, + placeholder='["h", "s"]'), + FieldSpec("min_score", FieldType.FLOAT, optional=True, default=0.7, + min_value=0.0, max_value=1.0), + FieldSpec("region", FieldType.STRING, optional=True, + placeholder=_REGION_PLACEHOLDER), + ), + description="Match by colour on HSV channels (red vs green, not grayscale).", ), - description="Boxes of regions that moved between two frames (after=screen).", - )) - specs.append(CommandSpec( - "AC_has_motion", "Image", "Has Motion?", - fields=( - FieldSpec("before", FieldType.FILE_PATH), - FieldSpec("after", FieldType.FILE_PATH, optional=True), - FieldSpec("threshold", FieldType.INT, optional=True, default=25), - FieldSpec("min_area", FieldType.INT, optional=True, default=80), + CommandSpec( + "AC_match_color_all", "Image", "Match Template All (colour/HSV)", + fields=( + FieldSpec("template", FieldType.FILE_PATH), + FieldSpec("channels", FieldType.STRING, optional=True, + placeholder='["h", "s"]'), + FieldSpec("min_score", FieldType.FLOAT, optional=True, default=0.7, + min_value=0.0, max_value=1.0), + FieldSpec("max_results", FieldType.INT, optional=True, default=20), + FieldSpec("nms_iou", FieldType.FLOAT, optional=True, default=0.3, + min_value=0.0, max_value=1.0), + ), + description="Find every colour (HSV) match of a template (NMS-deduped).", ), - description="Whether anything moved between two frames (+ activity score).", - )) - specs.append(CommandSpec( - "AC_perceptual_diff", "Image", "Perceptual Diff (YIQ)", - fields=( - FieldSpec("actual", FieldType.FILE_PATH), - FieldSpec("expected", FieldType.FILE_PATH), - FieldSpec("threshold", FieldType.FLOAT, optional=True, default=0.1, - min_value=0.0, max_value=1.0), - FieldSpec("include_aa", FieldType.BOOL, optional=True, default=False), - FieldSpec("max_diff_ratio", FieldType.FLOAT, optional=True, - min_value=0.0, max_value=1.0), - ), - description="Perceptual image diff that ignores anti-aliased edges.", - )) + CommandSpec( + "AC_region_stability", "Image", "Region Stability (frames)", + fields=( + FieldSpec("frames", FieldType.STRING, + placeholder='["f1.png", "f2.png", "f3.png"]'), + FieldSpec("settle_threshold", FieldType.FLOAT, optional=True, + default=0.99, min_value=0.0, max_value=1.0), + ), + description="Is a frame sequence settled? (consecutive-frame SSIM).", + ), + CommandSpec( + "AC_match_persistence", "Image", "Match Persistence (frames)", + fields=( + FieldSpec("template", FieldType.FILE_PATH), + FieldSpec("frames", FieldType.STRING, + placeholder='["f1.png", "f2.png", "f3.png"]'), + FieldSpec("min_score", FieldType.FLOAT, optional=True, default=0.8, + min_value=0.0, max_value=1.0), + FieldSpec("agree_px", FieldType.INT, optional=True, default=8), + ), + description="Does a template match hold steady across frames? (jitter).", + ), + CommandSpec( + "AC_grid_cells", "Image", "Grid Cells (coarse grounding)", + fields=( + FieldSpec("rows", FieldType.INT, default=3), + FieldSpec("cols", FieldType.INT, default=3), + FieldSpec("region", FieldType.STRING, optional=True, + placeholder=_REGION_PLACEHOLDER), + ), + description="Label an rows x cols grid over the screen for VLM grounding.", + ), + CommandSpec( + "AC_cell_for_point", "Image", "Cell For Point", + fields=( + FieldSpec("x", FieldType.INT), + FieldSpec("y", FieldType.INT), + FieldSpec("rows", FieldType.INT, default=3), + FieldSpec("cols", FieldType.INT, default=3), + FieldSpec("region", FieldType.STRING, optional=True, + placeholder=_REGION_PLACEHOLDER), + ), + description="Return the grid cell label containing a screen point.", + ), + CommandSpec( + "AC_point_for_cell", "Image", "Point For Cell", + fields=( + FieldSpec("label", FieldType.STRING, placeholder="C3"), + FieldSpec("rows", FieldType.INT, default=3), + FieldSpec("cols", FieldType.INT, default=3), + FieldSpec("region", FieldType.STRING, optional=True, + placeholder=_REGION_PLACEHOLDER), + ), + description="Return the centre point of a named grid cell (click target).", + ), + CommandSpec( + "AC_find_color_region", "Image", "Find Colour Region", + fields=( + FieldSpec("rgb", FieldType.STRING, placeholder="[0, 200, 0]"), + FieldSpec("tolerance", FieldType.INT, optional=True, default=20), + FieldSpec("min_area", FieldType.INT, optional=True, default=50), + FieldSpec("region", FieldType.STRING, optional=True, + placeholder=_REGION_PLACEHOLDER), + ), + description="Locate regions by colour (status light / banner / fill).", + ), + CommandSpec( + "AC_ssim_compare", "Image", "SSIM Compare", + fields=( + FieldSpec("reference", FieldType.FILE_PATH), + FieldSpec("current", FieldType.FILE_PATH, optional=True), + FieldSpec("ignore", FieldType.STRING, optional=True, + placeholder="[[x, y, w, h], ...]"), + FieldSpec("region", FieldType.STRING, optional=True, + placeholder=_REGION_PLACEHOLDER), + ), + description="Structural-similarity score (0..1) vs reference / screen.", + ), + CommandSpec( + "AC_ssim_changed_regions", "Image", "SSIM Changed Regions", + fields=( + FieldSpec("reference", FieldType.FILE_PATH), + FieldSpec("current", FieldType.FILE_PATH, optional=True), + FieldSpec("ignore", FieldType.STRING, optional=True, + placeholder="[[x, y, w, h], ...]"), + FieldSpec("threshold", FieldType.FLOAT, optional=True, default=0.35, + min_value=0.0, max_value=1.0), + FieldSpec("min_area", FieldType.INT, optional=True, default=50), + FieldSpec("region", FieldType.STRING, optional=True, + placeholder=_REGION_PLACEHOLDER), + ), + description="Boxes of the regions that structurally changed.", + ), + CommandSpec( + "AC_feature_match", "Image", "Feature Match (ORB)", + fields=( + FieldSpec("template", FieldType.FILE_PATH), + FieldSpec("region", FieldType.STRING, optional=True, + placeholder=_RECT_PLACEHOLDER), + FieldSpec("max_features", FieldType.INT, optional=True, default=500), + FieldSpec("ratio", FieldType.FLOAT, optional=True, default=0.75, + min_value=0.0, max_value=1.0), + FieldSpec("min_inliers", FieldType.INT, optional=True, default=10), + ), + description="Locate a template under rotation / scale / theme change.", + ), + CommandSpec( + "AC_find_shapes", "Image", "Find Shapes", + fields=( + FieldSpec("region", FieldType.STRING, optional=True, + placeholder=_RECT_PLACEHOLDER), + FieldSpec("min_area", FieldType.INT, optional=True, default=400), + FieldSpec("max_area", FieldType.INT, optional=True), + ), + description="Locate distinct shapes by edge/contour detection (no template).", + ), + CommandSpec( + "AC_find_rectangles", "Image", "Find Rectangles", + fields=( + FieldSpec("region", FieldType.STRING, optional=True, + placeholder=_RECT_PLACEHOLDER), + FieldSpec("min_area", FieldType.INT, optional=True, default=400), + FieldSpec("max_area", FieldType.INT, optional=True), + FieldSpec("aspect_range", FieldType.STRING, optional=True, + placeholder="[1.5, 8.0]"), + FieldSpec("epsilon", FieldType.FLOAT, optional=True, default=0.04, + min_value=0.0, max_value=1.0), + ), + description="Locate rectangular regions (buttons / cards / input fields).", + ), + CommandSpec( + "AC_preprocess_image", "Image", "Preprocess Image (OCR/match)", + fields=( + FieldSpec("output_path", FieldType.FILE_PATH), + FieldSpec("source", FieldType.FILE_PATH, optional=True), + FieldSpec("steps", FieldType.STRING, optional=True, + placeholder="grayscale,upscale,binarize"), + FieldSpec("scale", FieldType.FLOAT, optional=True, default=2.0), + FieldSpec("region", FieldType.STRING, optional=True, + placeholder=_REGION_PLACEHOLDER), + ), + description="Clean up an image for OCR / matching (grayscale/binarize/…).", + ), + CommandSpec( + "AC_segment_hsv", "Image", "Segment by HSV", + fields=( + FieldSpec("lower_hsv", FieldType.STRING, placeholder="[40, 80, 80]"), + FieldSpec("upper_hsv", FieldType.STRING, placeholder="[80, 255, 255]"), + FieldSpec("min_area", FieldType.INT, optional=True, default=50), + FieldSpec("region", FieldType.STRING, optional=True, + placeholder=_REGION_PLACEHOLDER), + ), + description="Locate regions inside an HSV band (lighting-robust).", + ), + CommandSpec( + "AC_dominant_hue_regions", "Image", "Find Hue Regions", + fields=( + FieldSpec("hue", FieldType.INT, placeholder="0=red, 60=green, 120=blue"), + FieldSpec("hue_tol", FieldType.INT, optional=True, default=10), + FieldSpec("sat_min", FieldType.INT, optional=True, default=80), + FieldSpec("val_min", FieldType.INT, optional=True, default=80), + FieldSpec("min_area", FieldType.INT, optional=True, default=50), + FieldSpec("region", FieldType.STRING, optional=True, + placeholder=_REGION_PLACEHOLDER), + ), + description="Locate any shade of a hue, any brightness (handles red wrap).", + ), + CommandSpec( + "AC_find_text_regions", "Image", "Find Text Regions (MSER)", + fields=( + FieldSpec("min_area", FieldType.INT, optional=True, default=60), + FieldSpec("max_area", FieldType.INT, optional=True), + FieldSpec("merge", FieldType.BOOL, optional=True, default=True), + FieldSpec("max_aspect", FieldType.FLOAT, optional=True, default=12.0), + FieldSpec("region", FieldType.STRING, optional=True, + placeholder=_RECT_PLACEHOLDER), + ), + description="Locate text regions without OCR (crop to feed an OCR engine).", + ), + CommandSpec( + "AC_find_text_lines", "Image", "Find Text Lines (MSER)", + fields=( + FieldSpec("y_tolerance", FieldType.INT, optional=True, default=8), + FieldSpec("region", FieldType.STRING, optional=True, + placeholder=_RECT_PLACEHOLDER), + ), + description="Locate horizontal text lines without OCR.", + ), + CommandSpec( + "AC_find_lines", "Image", "Find Lines (Hough)", + fields=( + FieldSpec("min_length", FieldType.INT, optional=True, default=80), + FieldSpec("max_gap", FieldType.INT, optional=True, default=10), + FieldSpec("orientation", FieldType.ENUM, optional=True, default="any", + choices=("any", "horizontal", "vertical", "diagonal")), + FieldSpec("region", FieldType.STRING, optional=True, + placeholder=_RECT_PLACEHOLDER), + ), + description="Detect straight line segments on raw pixels.", + ), + CommandSpec( + "AC_find_grid", "Image", "Find Table Grid", + fields=( + FieldSpec("min_length", FieldType.INT, optional=True, default=120), + FieldSpec("tol", FieldType.INT, optional=True, default=10), + FieldSpec("region", FieldType.STRING, optional=True, + placeholder=_RECT_PLACEHOLDER), + ), + description="Recover a table's rows / columns / cells from its lines.", + ), + CommandSpec( + "AC_find_separators", "Image", "Find Separator Lines", + fields=( + FieldSpec("axis", FieldType.ENUM, optional=True, default="horizontal", + choices=("horizontal", "vertical")), + FieldSpec("min_length", FieldType.INT, optional=True, default=120), + FieldSpec("tol", FieldType.INT, optional=True, default=10), + FieldSpec("region", FieldType.STRING, optional=True, + placeholder=_RECT_PLACEHOLDER), + ), + description="Coordinates of long divider lines along an axis.", + ), + CommandSpec( + "AC_fuse_elements", "Image", "Fuse Element Boxes", + fields=( + FieldSpec("ocr", FieldType.STRING, optional=True, + placeholder=_POINTS_JSON_PLACEHOLDER), + FieldSpec("icon", FieldType.STRING, optional=True), + FieldSpec("a11y", FieldType.STRING, optional=True), + FieldSpec("iou_threshold", FieldType.FLOAT, optional=True, default=0.9, + min_value=0.0, max_value=1.0), + ), + description="Union OCR/icon/a11y boxes, dropping cross-source duplicates.", + ), + CommandSpec( + "AC_reading_order", "Image", "Reading Order", + fields=( + FieldSpec("elements", FieldType.STRING, + placeholder=_POINTS_JSON_PLACEHOLDER), + FieldSpec("row_tol", FieldType.INT, optional=True, default=12), + ), + description="Order element boxes top-to-bottom, left-to-right (+ index).", + ), + CommandSpec( + "AC_locate_chain", "Image", "Locate Chain (refine boxes)", + fields=( + FieldSpec("boxes", FieldType.STRING, + placeholder=_POINTS_JSON_PLACEHOLDER), + FieldSpec("ops", FieldType.STRING, + placeholder='[{"op":"filter","has_text":"OK"},{"op":"first"}]'), + ), + description="Refine candidate boxes (within / filter / reading / nth / …).", + ), + CommandSpec( + "AC_image_histogram", "Image", "Image Histogram", + fields=( + FieldSpec("source", FieldType.FILE_PATH, optional=True), + FieldSpec("bins", FieldType.INT, optional=True, default=32), + FieldSpec("space", FieldType.ENUM, optional=True, default="hsv", + choices=("hsv", "rgb", "gray")), + FieldSpec("region", FieldType.STRING, optional=True, + placeholder=_REGION_PLACEHOLDER), + ), + description="Colour-histogram fingerprint of an image / the screen.", + ), + CommandSpec( + "AC_histogram_changed", "Image", "Histogram Changed?", + fields=( + FieldSpec("reference", FieldType.FILE_PATH), + FieldSpec("current", FieldType.FILE_PATH, optional=True), + FieldSpec("method", FieldType.ENUM, optional=True, + default="correlation", + choices=("correlation", "chisqr", "intersection", + "bhattacharyya")), + FieldSpec("threshold", FieldType.FLOAT, optional=True, default=0.9, + min_value=0.0, max_value=1.0), + FieldSpec("space", FieldType.ENUM, optional=True, default="hsv", + choices=("hsv", "rgb", "gray")), + FieldSpec("region", FieldType.STRING, optional=True, + placeholder=_REGION_PLACEHOLDER), + ), + description="Detect a palette/view change vs a reference (illumination-robust).", + ), + CommandSpec( + "AC_image_quality", "Image", "Image Quality", + fields=( + FieldSpec("source", FieldType.FILE_PATH, optional=True), + FieldSpec("region", FieldType.STRING, optional=True, + placeholder=_RECT_PLACEHOLDER), + ), + description="Sharpness / contrast / brightness of an image or the screen.", + ), + CommandSpec( + "AC_quality_gate", "Image", "Quality Gate (OCR-ready?)", + fields=( + FieldSpec("source", FieldType.FILE_PATH, optional=True), + FieldSpec("region", FieldType.STRING, optional=True, + placeholder=_RECT_PLACEHOLDER), + FieldSpec("min_sharpness", FieldType.FLOAT, optional=True, + default=100.0), + FieldSpec("min_contrast", FieldType.FLOAT, optional=True, + default=12.0), + ), + description="Pass/fail an image for OCR readability with named issues.", + ), + CommandSpec( + "AC_detect_scale", "Image", "Detect Display Scale (DPI)", + fields=( + FieldSpec("template", FieldType.FILE_PATH), + FieldSpec("haystack", FieldType.FILE_PATH, optional=True), + FieldSpec("region", FieldType.STRING, optional=True, + placeholder=_RECT_PLACEHOLDER), + FieldSpec("scales", FieldType.STRING, optional=True, + placeholder="[1.0, 1.25, 1.5, 1.75, 2.0]"), + ), + description="Infer the display scale a template renders at (visual DPI).", + ), + CommandSpec( + "AC_scale_sweep", "Image", "Scale Sweep (per-scale scores)", + fields=( + FieldSpec("template", FieldType.FILE_PATH), + FieldSpec("haystack", FieldType.FILE_PATH, optional=True), + FieldSpec("region", FieldType.STRING, optional=True, + placeholder=_RECT_PLACEHOLDER), + FieldSpec("scales", FieldType.STRING, optional=True, + placeholder="[1.0, 1.25, 1.5, 1.75, 2.0]"), + ), + description="Per-scale match-score profile of a template.", + ), + ]) + saliency_fields = ( + FieldSpec("source", FieldType.FILE_PATH, optional=True), + FieldSpec("region", FieldType.STRING, optional=True, + placeholder=_RECT_PLACEHOLDER), + FieldSpec("size", FieldType.INT, optional=True, default=64), + FieldSpec("threshold", FieldType.FLOAT, optional=True), + FieldSpec("min_area", FieldType.INT, optional=True, default=4), + ) + specs.extend([ + CommandSpec( + "AC_salient_regions", "Image", "Salient Regions", + fields=saliency_fields, + description="Visually salient regions (spectral-residual; where to look).", + ), + CommandSpec( + "AC_most_salient", "Image", "Most Salient Region", + fields=saliency_fields, + description="The single most visually salient region of an image/screen.", + ), + CommandSpec( + "AC_changed_regions", "Image", "Changed Regions (motion)", + fields=( + FieldSpec("before", FieldType.FILE_PATH), + FieldSpec("after", FieldType.FILE_PATH, optional=True), + FieldSpec("threshold", FieldType.INT, optional=True, default=25), + FieldSpec("min_area", FieldType.INT, optional=True, default=80), + FieldSpec("blur", FieldType.INT, optional=True, default=5), + ), + description="Boxes of regions that moved between two frames (after=screen).", + ), + CommandSpec( + "AC_has_motion", "Image", "Has Motion?", + fields=( + FieldSpec("before", FieldType.FILE_PATH), + FieldSpec("after", FieldType.FILE_PATH, optional=True), + FieldSpec("threshold", FieldType.INT, optional=True, default=25), + FieldSpec("min_area", FieldType.INT, optional=True, default=80), + ), + description="Whether anything moved between two frames (+ activity score).", + ), + CommandSpec( + "AC_perceptual_diff", "Image", "Perceptual Diff (YIQ)", + fields=( + FieldSpec("actual", FieldType.FILE_PATH), + FieldSpec("expected", FieldType.FILE_PATH), + FieldSpec("threshold", FieldType.FLOAT, optional=True, default=0.1, + min_value=0.0, max_value=1.0), + FieldSpec("include_aa", FieldType.BOOL, optional=True, default=False), + FieldSpec("max_diff_ratio", FieldType.FLOAT, optional=True, + min_value=0.0, max_value=1.0), + ), + description="Perceptual image diff that ignores anti-aliased edges.", + ), + ]) def _add_ocr_specs(specs: List[CommandSpec]) -> None: @@ -927,779 +942,785 @@ def _add_ocr_specs(specs: List[CommandSpec]) -> None: description="Installed Tesseract languages; null when the engine cannot be asked.", ), )) - specs.append(CommandSpec( - "AC_read_qr", "OCR", "Read QR Codes", - fields=( - FieldSpec("region", FieldType.STRING, optional=True, - placeholder=_REGION_PLACEHOLDER), + specs.extend([ + CommandSpec( + "AC_read_qr", "OCR", "Read QR Codes", + fields=( + FieldSpec("region", FieldType.STRING, optional=True, + placeholder=_REGION_PLACEHOLDER), + ), + description="Decode QR codes in a screen region (OpenCV).", ), - description="Decode QR codes in a screen region (OpenCV).", - )) - specs.append(CommandSpec( - "AC_read_barcodes", "OCR", "Read Barcodes (1-D)", - fields=( - FieldSpec("source", FieldType.FILE_PATH, optional=True), - FieldSpec("region", FieldType.STRING, optional=True, - placeholder=_RECT_PLACEHOLDER), + CommandSpec( + "AC_read_barcodes", "OCR", "Read Barcodes (1-D)", + fields=( + FieldSpec("source", FieldType.FILE_PATH, optional=True), + FieldSpec("region", FieldType.STRING, optional=True, + placeholder=_RECT_PLACEHOLDER), + ), + description="Decode 1-D barcodes (EAN / UPC) in an image / screen region.", ), - description="Decode 1-D barcodes (EAN / UPC) in an image / screen region.", - )) - specs.append(CommandSpec( - "AC_populate_table", "OCR", "Fill Table From Grid + OCR", - fields=( - FieldSpec("grid", FieldType.STRING, - placeholder='{"rows": [0, 30, 60], "cols": [0, 100, 200]}'), - FieldSpec("text_boxes", FieldType.STRING, - placeholder='[{"x": 10, "y": 5, "width": 60, "height": 20, ' - '"text": "Name"}]'), - FieldSpec("overlap", FieldType.FLOAT, optional=True, default=0.4, - min_value=0.0, max_value=1.0), - ), - description="Drop OCR text boxes into a ruling-line grid → addressable table.", - )) - specs.append(CommandSpec( - "AC_detect_borderless_table", "OCR", "Detect Borderless Table", - fields=( - FieldSpec("boxes", FieldType.STRING, - placeholder='[{"x":10,"y":0,"width":60,"height":18,' - '"text":"Name"}]'), - FieldSpec("min_gap", FieldType.INT, optional=True, default=8), - FieldSpec("page_width", FieldType.INT, optional=True), + CommandSpec( + "AC_populate_table", "OCR", "Fill Table From Grid + OCR", + fields=( + FieldSpec("grid", FieldType.STRING, + placeholder='{"rows": [0, 30, 60], "cols": [0, 100, 200]}'), + FieldSpec("text_boxes", FieldType.STRING, + placeholder='[{"x": 10, "y": 5, "width": 60, "height": 20, ' + '"text": "Name"}]'), + FieldSpec("overlap", FieldType.FLOAT, optional=True, default=0.4, + min_value=0.0, max_value=1.0), + ), + description="Drop OCR text boxes into a ruling-line grid → addressable table.", ), - description="Infer a borderless table from OCR boxes via whitespace columns.", - )) - specs.append(CommandSpec( - "AC_column_gutters", "OCR", "Column Gutters (whitespace)", - fields=( - FieldSpec("boxes", FieldType.STRING, - placeholder='[{"x":10,"y":0,"width":60,"height":18}]'), - FieldSpec("min_gap", FieldType.INT, optional=True, default=8), - FieldSpec("page_width", FieldType.INT, optional=True), + CommandSpec( + "AC_detect_borderless_table", "OCR", "Detect Borderless Table", + fields=( + FieldSpec("boxes", FieldType.STRING, + placeholder='[{"x":10,"y":0,"width":60,"height":18,' + '"text":"Name"}]'), + FieldSpec("min_gap", FieldType.INT, optional=True, default=8), + FieldSpec("page_width", FieldType.INT, optional=True), + ), + description="Infer a borderless table from OCR boxes via whitespace columns.", ), - description="Find borderless-table column separators by whitespace projection.", - )) - specs.append(CommandSpec( - "AC_associate_fields", "OCR", "Associate Form Fields", - fields=( - FieldSpec("text_boxes", FieldType.STRING, - placeholder='[{"x":0,"y":0,"width":60,"height":20,' - '"text":"Name:"}]'), - FieldSpec("directions", FieldType.STRING, optional=True, - placeholder='["right", "below"]'), - FieldSpec("max_gap", FieldType.INT, optional=True, default=150), - ), - description="Pair 'label:' boxes with the nearest aligned value (right/below).", - )) - specs.append(CommandSpec( - "AC_match_labels_to_widgets", "OCR", "Match Labels To Widgets", - fields=( - FieldSpec("labels", FieldType.STRING, - placeholder='[{"x":0,"y":0,"width":60,"height":20,' - '"text":"Accept"}]'), - FieldSpec("widgets", FieldType.STRING, - placeholder='[{"x":120,"y":0,"width":16,"height":16}]'), + CommandSpec( + "AC_column_gutters", "OCR", "Column Gutters (whitespace)", + fields=( + FieldSpec("boxes", FieldType.STRING, + placeholder='[{"x":10,"y":0,"width":60,"height":18}]'), + FieldSpec("min_gap", FieldType.INT, optional=True, default=8), + FieldSpec("page_width", FieldType.INT, optional=True), + ), + description="Find borderless-table column separators by whitespace projection.", ), - description="Match each checkbox/radio/input to its nearest label by centre.", - )) - specs.append(CommandSpec( - "AC_flow_order", "OCR", "Reading Order (column-aware)", - fields=( - FieldSpec("boxes", FieldType.STRING, - placeholder='[{"x":0,"y":0,"width":40,"height":20,' - '"text":"A1"}]'), - FieldSpec("min_gap", FieldType.INT, optional=True, default=12), + CommandSpec( + "AC_associate_fields", "OCR", "Associate Form Fields", + fields=( + FieldSpec("text_boxes", FieldType.STRING, + placeholder='[{"x":0,"y":0,"width":60,"height":20,' + '"text":"Name:"}]'), + FieldSpec("directions", FieldType.STRING, optional=True, + placeholder='["right", "below"]'), + FieldSpec("max_gap", FieldType.INT, optional=True, default=150), + ), + description="Pair 'label:' boxes with the nearest aligned value (right/below).", ), - description="Column-aware reading order via XY-cut (reads down columns).", - )) - specs.append(CommandSpec( - "AC_xy_cut", "OCR", "XY-Cut Region Tree", - fields=( - FieldSpec("boxes", FieldType.STRING, - placeholder='[{"x":0,"y":0,"width":40,"height":20}]'), - FieldSpec("min_gap", FieldType.INT, optional=True, default=12), + CommandSpec( + "AC_match_labels_to_widgets", "OCR", "Match Labels To Widgets", + fields=( + FieldSpec("labels", FieldType.STRING, + placeholder='[{"x":0,"y":0,"width":60,"height":20,' + '"text":"Accept"}]'), + FieldSpec("widgets", FieldType.STRING, + placeholder='[{"x":120,"y":0,"width":16,"height":16}]'), + ), + description="Match each checkbox/radio/input to its nearest label by centre.", ), - description="Recursive XY-cut region tree (split at widest whitespace).", - )) - specs.append(CommandSpec( - "AC_group_paragraphs", "OCR", "Group Paragraphs", - fields=( - FieldSpec("lines", FieldType.STRING, - placeholder='[{"x":0,"y":0,"width":200,"height":20,' - '"text":"line"}]'), - FieldSpec("line_gap_factor", FieldType.FLOAT, optional=True, - default=1.6), + CommandSpec( + "AC_flow_order", "OCR", "Reading Order (column-aware)", + fields=( + FieldSpec("boxes", FieldType.STRING, + placeholder='[{"x":0,"y":0,"width":40,"height":20,' + '"text":"A1"}]'), + FieldSpec("min_gap", FieldType.INT, optional=True, default=12), + ), + description="Column-aware reading order via XY-cut (reads down columns).", ), - description="Group OCR lines into paragraphs by vertical spacing.", - )) - specs.append(CommandSpec( - "AC_detect_lists", "OCR", "Detect Lists", - fields=( - FieldSpec("lines", FieldType.STRING, - placeholder='[{"x":0,"y":0,"width":200,"height":20,' - '"text":"* item"}]'), + CommandSpec( + "AC_xy_cut", "OCR", "XY-Cut Region Tree", + fields=( + FieldSpec("boxes", FieldType.STRING, + placeholder='[{"x":0,"y":0,"width":40,"height":20}]'), + FieldSpec("min_gap", FieldType.INT, optional=True, default=12), + ), + description="Recursive XY-cut region tree (split at widest whitespace).", ), - description="Detect bulleted / numbered list items among OCR lines.", - )) - specs.append(CommandSpec( - "AC_classify_lines", "OCR", "Classify Headings vs Body", - fields=( - FieldSpec("lines", FieldType.STRING, - placeholder='[{"x":0,"y":0,"width":200,"height":40,' - '"text":"Title"}]'), - FieldSpec("heading_ratio", FieldType.FLOAT, optional=True, default=1.2), + CommandSpec( + "AC_group_paragraphs", "OCR", "Group Paragraphs", + fields=( + FieldSpec("lines", FieldType.STRING, + placeholder='[{"x":0,"y":0,"width":200,"height":20,' + '"text":"line"}]'), + FieldSpec("line_gap_factor", FieldType.FLOAT, optional=True, + default=1.6), + ), + description="Group OCR lines into paragraphs by vertical spacing.", ), - description="Tag OCR lines as heading/body by height; assign heading levels.", - )) - specs.append(CommandSpec( - "AC_outline", "OCR", "Document Outline", - fields=( - FieldSpec("lines", FieldType.STRING, - placeholder='[{"x":0,"y":0,"width":200,"height":40,' - '"text":"Title"}]'), - FieldSpec("heading_ratio", FieldType.FLOAT, optional=True, default=1.2), + CommandSpec( + "AC_detect_lists", "OCR", "Detect Lists", + fields=( + FieldSpec("lines", FieldType.STRING, + placeholder='[{"x":0,"y":0,"width":200,"height":20,' + '"text":"* item"}]'), + ), + description="Detect bulleted / numbered list items among OCR lines.", ), - description="Headings in order with levels (document outline) from OCR lines.", - )) - specs.append(CommandSpec( - "AC_scroll_to_find", "OCR", "Scroll Until Visible", - fields=( - FieldSpec("target", FieldType.STRING), - FieldSpec("kind", FieldType.ENUM, choices=("image", "text"), - default="image"), - FieldSpec("direction", FieldType.ENUM, - choices=("down", "up", "left", "right"), default="down"), - FieldSpec("max_scrolls", FieldType.INT, optional=True, default=10, - min_value=1), - FieldSpec("scroll_amount", FieldType.INT, optional=True, default=3, - min_value=1), - ), - description="Scroll until a template image or OCR text appears.", - )) - specs.append(CommandSpec( - "AC_ocr_to_var", "OCR", "Read Text into Variable", - fields=( - FieldSpec("var", FieldType.STRING, default="ocr_text"), - FieldSpec("region", FieldType.STRING, optional=True, - placeholder=_RECT_PLACEHOLDER), - FieldSpec("lang", FieldType.STRING, optional=True, default="eng"), - FieldSpec("min_confidence", FieldType.FLOAT, optional=True, - default=60.0, min_value=0.0, max_value=100.0), + CommandSpec( + "AC_classify_lines", "OCR", "Classify Headings vs Body", + fields=( + FieldSpec("lines", FieldType.STRING, + placeholder='[{"x":0,"y":0,"width":200,"height":40,' + '"text":"Title"}]'), + FieldSpec("heading_ratio", FieldType.FLOAT, optional=True, default=1.2), + ), + description="Tag OCR lines as heading/body by height; assign heading levels.", ), - description="OCR a region and store the text in a flow variable.", - )) - specs.append(CommandSpec( - "AC_wait_text", "OCR", "Wait for Text", - fields=( - FieldSpec("target", FieldType.STRING), - FieldSpec("lang", FieldType.STRING, optional=True, default="eng"), - FieldSpec("timeout", FieldType.FLOAT, optional=True, default=10.0), - FieldSpec("poll", FieldType.FLOAT, optional=True, default=0.5), - FieldSpec("min_confidence", FieldType.FLOAT, optional=True, - default=60.0, min_value=0.0, max_value=100.0), + CommandSpec( + "AC_outline", "OCR", "Document Outline", + fields=( + FieldSpec("lines", FieldType.STRING, + placeholder='[{"x":0,"y":0,"width":200,"height":40,' + '"text":"Title"}]'), + FieldSpec("heading_ratio", FieldType.FLOAT, optional=True, default=1.2), + ), + description="Headings in order with levels (document outline) from OCR lines.", ), - )) - specs.append(CommandSpec( - "AC_click_text", "OCR", "Click Text", - fields=( - FieldSpec("target", FieldType.STRING), - FieldSpec("mouse_keycode", FieldType.ENUM, - choices=_MOUSE_BUTTONS, default="mouse_left"), - FieldSpec("lang", FieldType.STRING, optional=True, default="eng"), - FieldSpec("min_confidence", FieldType.FLOAT, optional=True, - default=60.0, min_value=0.0, max_value=100.0), + CommandSpec( + "AC_scroll_to_find", "OCR", "Scroll Until Visible", + fields=( + FieldSpec("target", FieldType.STRING), + FieldSpec("kind", FieldType.ENUM, choices=("image", "text"), + default="image"), + FieldSpec("direction", FieldType.ENUM, + choices=("down", "up", "left", "right"), default="down"), + FieldSpec("max_scrolls", FieldType.INT, optional=True, default=10, + min_value=1), + FieldSpec("scroll_amount", FieldType.INT, optional=True, default=3, + min_value=1), + ), + description="Scroll until a template image or OCR text appears.", ), - )) + CommandSpec( + "AC_ocr_to_var", "OCR", "Read Text into Variable", + fields=( + FieldSpec("var", FieldType.STRING, default="ocr_text"), + FieldSpec("region", FieldType.STRING, optional=True, + placeholder=_RECT_PLACEHOLDER), + FieldSpec("lang", FieldType.STRING, optional=True, default="eng"), + FieldSpec("min_confidence", FieldType.FLOAT, optional=True, + default=60.0, min_value=0.0, max_value=100.0), + ), + description="OCR a region and store the text in a flow variable.", + ), + CommandSpec( + "AC_wait_text", "OCR", "Wait for Text", + fields=( + FieldSpec("target", FieldType.STRING), + FieldSpec("lang", FieldType.STRING, optional=True, default="eng"), + FieldSpec("timeout", FieldType.FLOAT, optional=True, default=10.0), + FieldSpec("poll", FieldType.FLOAT, optional=True, default=0.5), + FieldSpec("min_confidence", FieldType.FLOAT, optional=True, + default=60.0, min_value=0.0, max_value=100.0), + ), + ), + CommandSpec( + "AC_click_text", "OCR", "Click Text", + fields=( + FieldSpec("target", FieldType.STRING), + FieldSpec("mouse_keycode", FieldType.ENUM, + choices=_MOUSE_BUTTONS, default="mouse_left"), + FieldSpec("lang", FieldType.STRING, optional=True, default="eng"), + FieldSpec("min_confidence", FieldType.FLOAT, optional=True, + default=60.0, min_value=0.0, max_value=100.0), + ), + ), + ]) def _add_window_specs(specs: List[CommandSpec]) -> None: - specs.append(CommandSpec( - "AC_focus_window", "Window", "Focus Window", - fields=( - FieldSpec("title_substring", FieldType.STRING), - FieldSpec("case_sensitive", FieldType.BOOL, optional=True, default=False), + specs.extend([ + CommandSpec( + "AC_focus_window", "Window", "Focus Window", + fields=( + FieldSpec("title_substring", FieldType.STRING), + FieldSpec("case_sensitive", FieldType.BOOL, optional=True, default=False), + ), ), - )) - specs.append(CommandSpec( - "AC_wait_window", "Window", "Wait for Window", - fields=( - FieldSpec("title_substring", FieldType.STRING), - FieldSpec("timeout", FieldType.FLOAT, optional=True, default=10.0), - FieldSpec("poll", FieldType.FLOAT, optional=True, default=0.5), + CommandSpec( + "AC_wait_window", "Window", "Wait for Window", + fields=( + FieldSpec("title_substring", FieldType.STRING), + FieldSpec("timeout", FieldType.FLOAT, optional=True, default=10.0), + FieldSpec("poll", FieldType.FLOAT, optional=True, default=0.5), + ), ), - )) - specs.append(CommandSpec( - "AC_close_window", "Window", "Close Window", - fields=(FieldSpec("title_substring", FieldType.STRING),), - description="Ask the first matching window to close (posts WM_CLOSE).", - )) - specs.append(CommandSpec( - "AC_minimize_window", "Window", "Minimize Window", - fields=(FieldSpec("title_substring", FieldType.STRING),), - description="Minimise the first matching window.", - )) - specs.append(CommandSpec( - "AC_foreground_window", "Window", "Foreground Window", - description="The window the user is currently working in.", - )) - specs.append(CommandSpec( - "AC_window_rect", "Window", "Window Rectangle", - fields=(FieldSpec("title_substring", FieldType.STRING),), - description="Screen rectangle (left, top, right, bottom) of a window.", - )) - specs.append(CommandSpec( - "AC_foreground_window_pid", "Window", "Foreground Window PID", - description="PID owning the foreground window (0 when unavailable).", - )) - specs.append(CommandSpec( - "AC_windows_for_pid", "Window", "Windows by Process ID", - fields=(FieldSpec("pid", FieldType.INT),), - description="Visible top-level windows owned by a process.", - )) - specs.append(CommandSpec( - "AC_minimize_windows_for_pid", "Window", "Minimize Windows by PID", - fields=(FieldSpec("pid", FieldType.INT),), - description="Minimise every window a process owns.", - )) - specs.append(CommandSpec( - "AC_post_key_to_window", "Window", "Post Key to Window", - fields=( - FieldSpec("title_substring", FieldType.STRING), - FieldSpec("key", FieldType.STRING), + CommandSpec( + "AC_close_window", "Window", "Close Window", + fields=(FieldSpec("title_substring", FieldType.STRING),), + description="Ask the first matching window to close (posts WM_CLOSE).", ), - description="Type one key into a window without focusing it " - "(best effort: some applications ignore posted input).", - )) - specs.append(CommandSpec( - "AC_post_click_to_window", "Window", "Post Click to Window", - fields=( - FieldSpec("title_substring", FieldType.STRING), - FieldSpec("button", FieldType.STRING, optional=True), - FieldSpec("x", FieldType.INT, optional=True), - FieldSpec("y", FieldType.INT, optional=True), + CommandSpec( + "AC_minimize_window", "Window", "Minimize Window", + fields=(FieldSpec("title_substring", FieldType.STRING),), + description="Minimise the first matching window.", ), - description="Click inside a window without focusing it; x / y are " - "relative to the window's top-left corner.", - )) - specs.append(CommandSpec( - "AC_window_pid", "Window", "Window PID by Title", - fields=(FieldSpec("title_substring", FieldType.STRING),), - description="PID owning the first matching window (0 when none).", - )) - specs.append(CommandSpec( - "AC_move_window", "Window", "Move Window by Title", - fields=( - FieldSpec("title_substring", FieldType.STRING), - FieldSpec("x", FieldType.INT), - FieldSpec("y", FieldType.INT), - FieldSpec("width", FieldType.INT, optional=True), - FieldSpec("height", FieldType.INT, optional=True), + CommandSpec( + "AC_foreground_window", "Window", "Foreground Window", + description="The window the user is currently working in.", ), - description="Move, and optionally resize, the first matching window.", - )) - specs.append(CommandSpec( - "AC_drop_files", "Window", "Drop Files onto Window", - fields=( - FieldSpec("hwnd", FieldType.INT), - FieldSpec("paths", FieldType.STRING, - placeholder='["C:\\\\a\\\\one.txt"]'), - FieldSpec("point", FieldType.STRING, optional=True, - placeholder=_POINT_PLACEHOLDER), + CommandSpec( + "AC_window_rect", "Window", "Window Rectangle", + fields=(FieldSpec("title_substring", FieldType.STRING),), + description="Screen rectangle (left, top, right, bottom) of a window.", ), - description="Drop files onto a window via WM_DROPFILES (Windows).", - )) - specs.append(CommandSpec( - "AC_plan_file_drop", "Window", "Plan File Drop", - fields=( - FieldSpec("paths", FieldType.STRING, - placeholder='["C:\\\\a\\\\one.txt"]'), - FieldSpec("point", FieldType.STRING, optional=True, - placeholder=_POINT_PLACEHOLDER), + CommandSpec( + "AC_foreground_window_pid", "Window", "Foreground Window PID", + description="PID owning the foreground window (0 when unavailable).", ), - description="Build the WM_DROPFILES payload without sending (pure).", - )) - specs.append(CommandSpec( - "AC_snap_window", "Window", "Snap / Tile Window", - fields=( - FieldSpec("title", FieldType.STRING), - FieldSpec("position", FieldType.ENUM, - choices=("left", "right", "top", "bottom", - "top-left", "top-right", "bottom-left", - "bottom-right", "max"), - default="left"), - ), - description="Move a window to a screen half / quarter / maximize.", - )) - specs.append(CommandSpec( - "AC_arrange_grid", "Window", "Arrange Windows in Grid", - fields=( - FieldSpec("titles", FieldType.STRING, - placeholder='["Editor", "Browser", "Terminal"]'), - FieldSpec("rows", FieldType.INT, optional=True), - FieldSpec("cols", FieldType.INT, optional=True), - FieldSpec("gap", FieldType.INT, optional=True, default=0), + CommandSpec( + "AC_windows_for_pid", "Window", "Windows by Process ID", + fields=(FieldSpec("pid", FieldType.INT),), + description="Visible top-level windows owned by a process.", ), - description="Tile a list of windows into a grid (auto-shape if unset).", - )) - specs.append(CommandSpec( - "AC_arrange_cascade", "Window", "Arrange Windows in Cascade", - fields=( - FieldSpec("titles", FieldType.STRING, - placeholder='["Editor", "Browser", "Terminal"]'), - FieldSpec("offset", FieldType.INT, optional=True, default=30), + CommandSpec( + "AC_minimize_windows_for_pid", "Window", "Minimize Windows by PID", + fields=(FieldSpec("pid", FieldType.INT),), + description="Minimise every window a process owns.", ), - description="Cascade a list of windows diagonally.", - )) - specs.append(CommandSpec( - "AC_set_topmost", "Window", "Set Always-On-Top", - fields=( - FieldSpec("title", FieldType.STRING), - FieldSpec("on", FieldType.BOOL, optional=True, default=True), + CommandSpec( + "AC_post_key_to_window", "Window", "Post Key to Window", + fields=( + FieldSpec("title_substring", FieldType.STRING), + FieldSpec("key", FieldType.STRING), + ), + description="Type one key into a window without focusing it " + "(best effort: some applications ignore posted input).", ), - description="Pin a window always-on-top (or release it).", - )) - specs.append(CommandSpec( - "AC_bring_to_front", "Window", "Bring Window to Front", - fields=(FieldSpec("title", FieldType.STRING),), - description="Raise a window to the top of the z-order.", - )) - specs.append(CommandSpec( - "AC_send_to_back", "Window", "Send Window to Back", - fields=(FieldSpec("title", FieldType.STRING),), - description="Send a window to the bottom of the z-order.", - )) - specs.append(CommandSpec( - "AC_get_client_rect", "Window", "Get Client Rect", - fields=(FieldSpec("title", FieldType.STRING),), - description="A window's client-area rect (excludes title bar / borders).", - )) - specs.append(CommandSpec( - "AC_client_point", "Window", "Client-Relative Point", - fields=( - FieldSpec("title", FieldType.STRING), - FieldSpec("x", FieldType.INT), - FieldSpec("y", FieldType.INT), + CommandSpec( + "AC_post_click_to_window", "Window", "Post Click to Window", + fields=( + FieldSpec("title_substring", FieldType.STRING), + FieldSpec("button", FieldType.STRING, optional=True), + FieldSpec("x", FieldType.INT, optional=True), + FieldSpec("y", FieldType.INT, optional=True), + ), + description="Click inside a window without focusing it; x / y are " + "relative to the window's top-left corner.", ), - description="Screen point for an (x, y) inside a window's client area.", - )) - specs.append(CommandSpec( - "AC_wait_window_closed", "Window", "Wait for Window to Close", - fields=( - FieldSpec("title", FieldType.STRING), - FieldSpec("timeout_s", FieldType.FLOAT, optional=True, - default=10.0), - FieldSpec("poll_interval_s", FieldType.FLOAT, optional=True, - default=0.2, min_value=0.01), + CommandSpec( + "AC_window_pid", "Window", "Window PID by Title", + fields=(FieldSpec("title_substring", FieldType.STRING),), + description="PID owning the first matching window (0 when none).", ), - description="Wait until a window matching the title disappears.", - )) - specs.append(CommandSpec( - "AC_wait_for_file", "Flow", "Wait for File", - fields=( - FieldSpec("path", FieldType.FILE_PATH), - FieldSpec("timeout_s", FieldType.FLOAT, optional=True, - default=30.0), - FieldSpec("stable_for_s", FieldType.FLOAT, optional=True, - default=1.0, min_value=0.0), - FieldSpec("min_size", FieldType.INT, optional=True, default=1, - min_value=0), - FieldSpec("poll_interval_s", FieldType.FLOAT, optional=True, - default=0.25, min_value=0.01), - ), - description="Wait until a file appears and stops growing (download done).", - )) - specs.append(CommandSpec( - "AC_wait_for_port", "Flow", "Wait for TCP Port", - fields=( - FieldSpec("host", FieldType.STRING, default="127.0.0.1"), - FieldSpec("port", FieldType.INT, min_value=1, max_value=65535), - FieldSpec("timeout_s", FieldType.FLOAT, optional=True, - default=30.0), - FieldSpec("connect_timeout_s", FieldType.FLOAT, optional=True, - default=1.0, min_value=0.01), - FieldSpec("poll_interval_s", FieldType.FLOAT, optional=True, - default=0.25, min_value=0.01), - ), - description="Wait until a TCP host:port accepts connections.", - )) - specs.append(CommandSpec( - "AC_wait_for_process", "Flow", "Wait for Process", - fields=( - FieldSpec("name", FieldType.STRING), - FieldSpec("present", FieldType.BOOL, optional=True, default=True), - FieldSpec("timeout_s", FieldType.FLOAT, optional=True, - default=30.0), - FieldSpec("poll_interval_s", FieldType.FLOAT, optional=True, - default=0.25, min_value=0.01), - ), - description="Wait until a process appears (or exits). Requires psutil.", - )) - specs.append(CommandSpec("AC_list_windows", "Window", "List Windows")) - specs.append(CommandSpec( - "AC_capture_window", "Window", "Capture Window", - fields=( - FieldSpec("title", FieldType.STRING), - FieldSpec("output_path", FieldType.STRING), + CommandSpec( + "AC_move_window", "Window", "Move Window by Title", + fields=( + FieldSpec("title_substring", FieldType.STRING), + FieldSpec("x", FieldType.INT), + FieldSpec("y", FieldType.INT), + FieldSpec("width", FieldType.INT, optional=True), + FieldSpec("height", FieldType.INT, optional=True), + ), + description="Move, and optionally resize, the first matching window.", ), - description="Screenshot the window matching title to a PNG.", - )) - specs.append(CommandSpec( - "AC_save_window_layout", "Window", "Save Window Layout", - fields=(FieldSpec("path", FieldType.STRING, optional=True),), - description="Snapshot every window's position (optionally to a file).", - )) - specs.append(CommandSpec( - "AC_restore_window_layout", "Window", "Restore Window Layout", - fields=(FieldSpec("layout", FieldType.FILE_PATH),), - description="Move windows back to a saved layout file.", - )) - specs.append(CommandSpec( - "AC_tile_rect", "Window", "Tile Rect (compute)", - fields=( - FieldSpec("slot", FieldType.ENUM, - choices=("full", "left", "right", "top", "bottom", - "top_left", "top_right", "bottom_left", - "bottom_right", "center", "left_third", - "center_third", "right_third"), default="left"), - FieldSpec("screen", FieldType.STRING, optional=True, - placeholder=_RECT_PLACEHOLDER), - FieldSpec("gap", FieldType.INT, optional=True, default=0), - ), - description="Compute the rectangle for a tiling slot of the screen.", - )) - specs.append(CommandSpec( - "AC_grid_rects", "Window", "Grid Rects (compute)", - fields=( - FieldSpec("rows", FieldType.INT, default=2), - FieldSpec("cols", FieldType.INT, default=2), - FieldSpec("screen", FieldType.STRING, optional=True, - placeholder=_RECT_PLACEHOLDER), - FieldSpec("gap", FieldType.INT, optional=True, default=0), + CommandSpec( + "AC_drop_files", "Window", "Drop Files onto Window", + fields=( + FieldSpec("hwnd", FieldType.INT), + FieldSpec("paths", FieldType.STRING, + placeholder='["C:\\\\a\\\\one.txt"]'), + FieldSpec("point", FieldType.STRING, optional=True, + placeholder=_POINT_PLACEHOLDER), + ), + description="Drop files onto a window via WM_DROPFILES (Windows).", ), - description="Compute the cell rectangles of an R×C screen grid.", - )) - specs.append(CommandSpec( - "AC_cascade_rects", "Window", "Cascade Rects (compute)", - fields=( - FieldSpec("count", FieldType.INT, default=3), - FieldSpec("screen", FieldType.STRING, optional=True, - placeholder=_RECT_PLACEHOLDER), - FieldSpec("offset", FieldType.INT, optional=True, default=30), - FieldSpec("size", FieldType.STRING, optional=True, - placeholder="[width, height]"), - ), - description="Compute staggered, overlapping window rectangles.", - )) - specs.append(CommandSpec( - "AC_enumerate_monitors", "Window", "Enumerate Monitors", - description="List monitors + virtual bounds (multi-display geometry).", - )) - specs.append(CommandSpec( - "AC_monitor_at_point", "Window", "Monitor at Point", - fields=( - FieldSpec("x", FieldType.INT), - FieldSpec("y", FieldType.INT), + CommandSpec( + "AC_plan_file_drop", "Window", "Plan File Drop", + fields=( + FieldSpec("paths", FieldType.STRING, + placeholder='["C:\\\\a\\\\one.txt"]'), + FieldSpec("point", FieldType.STRING, optional=True, + placeholder=_POINT_PLACEHOLDER), + ), + description="Build the WM_DROPFILES payload without sending (pure).", ), - description="Report which monitor contains a virtual point.", - )) + CommandSpec( + "AC_snap_window", "Window", "Snap / Tile Window", + fields=( + FieldSpec("title", FieldType.STRING), + FieldSpec("position", FieldType.ENUM, + choices=("left", "right", "top", "bottom", + "top-left", "top-right", "bottom-left", + "bottom-right", "max"), + default="left"), + ), + description="Move a window to a screen half / quarter / maximize.", + ), + CommandSpec( + "AC_arrange_grid", "Window", "Arrange Windows in Grid", + fields=( + FieldSpec("titles", FieldType.STRING, + placeholder='["Editor", "Browser", "Terminal"]'), + FieldSpec("rows", FieldType.INT, optional=True), + FieldSpec("cols", FieldType.INT, optional=True), + FieldSpec("gap", FieldType.INT, optional=True, default=0), + ), + description="Tile a list of windows into a grid (auto-shape if unset).", + ), + CommandSpec( + "AC_arrange_cascade", "Window", "Arrange Windows in Cascade", + fields=( + FieldSpec("titles", FieldType.STRING, + placeholder='["Editor", "Browser", "Terminal"]'), + FieldSpec("offset", FieldType.INT, optional=True, default=30), + ), + description="Cascade a list of windows diagonally.", + ), + CommandSpec( + "AC_set_topmost", "Window", "Set Always-On-Top", + fields=( + FieldSpec("title", FieldType.STRING), + FieldSpec("on", FieldType.BOOL, optional=True, default=True), + ), + description="Pin a window always-on-top (or release it).", + ), + CommandSpec( + "AC_bring_to_front", "Window", "Bring Window to Front", + fields=(FieldSpec("title", FieldType.STRING),), + description="Raise a window to the top of the z-order.", + ), + CommandSpec( + "AC_send_to_back", "Window", "Send Window to Back", + fields=(FieldSpec("title", FieldType.STRING),), + description="Send a window to the bottom of the z-order.", + ), + CommandSpec( + "AC_get_client_rect", "Window", "Get Client Rect", + fields=(FieldSpec("title", FieldType.STRING),), + description="A window's client-area rect (excludes title bar / borders).", + ), + CommandSpec( + "AC_client_point", "Window", "Client-Relative Point", + fields=( + FieldSpec("title", FieldType.STRING), + FieldSpec("x", FieldType.INT), + FieldSpec("y", FieldType.INT), + ), + description="Screen point for an (x, y) inside a window's client area.", + ), + CommandSpec( + "AC_wait_window_closed", "Window", "Wait for Window to Close", + fields=( + FieldSpec("title", FieldType.STRING), + FieldSpec("timeout_s", FieldType.FLOAT, optional=True, + default=10.0), + FieldSpec("poll_interval_s", FieldType.FLOAT, optional=True, + default=0.2, min_value=0.01), + ), + description="Wait until a window matching the title disappears.", + ), + CommandSpec( + "AC_wait_for_file", "Flow", "Wait for File", + fields=( + FieldSpec("path", FieldType.FILE_PATH), + FieldSpec("timeout_s", FieldType.FLOAT, optional=True, + default=30.0), + FieldSpec("stable_for_s", FieldType.FLOAT, optional=True, + default=1.0, min_value=0.0), + FieldSpec("min_size", FieldType.INT, optional=True, default=1, + min_value=0), + FieldSpec("poll_interval_s", FieldType.FLOAT, optional=True, + default=0.25, min_value=0.01), + ), + description="Wait until a file appears and stops growing (download done).", + ), + CommandSpec( + "AC_wait_for_port", "Flow", "Wait for TCP Port", + fields=( + FieldSpec("host", FieldType.STRING, default="127.0.0.1"), + FieldSpec("port", FieldType.INT, min_value=1, max_value=65535), + FieldSpec("timeout_s", FieldType.FLOAT, optional=True, + default=30.0), + FieldSpec("connect_timeout_s", FieldType.FLOAT, optional=True, + default=1.0, min_value=0.01), + FieldSpec("poll_interval_s", FieldType.FLOAT, optional=True, + default=0.25, min_value=0.01), + ), + description="Wait until a TCP host:port accepts connections.", + ), + CommandSpec( + "AC_wait_for_process", "Flow", "Wait for Process", + fields=( + FieldSpec("name", FieldType.STRING), + FieldSpec("present", FieldType.BOOL, optional=True, default=True), + FieldSpec("timeout_s", FieldType.FLOAT, optional=True, + default=30.0), + FieldSpec("poll_interval_s", FieldType.FLOAT, optional=True, + default=0.25, min_value=0.01), + ), + description="Wait until a process appears (or exits). Requires psutil.", + ), + CommandSpec("AC_list_windows", "Window", "List Windows"), + CommandSpec( + "AC_capture_window", "Window", "Capture Window", + fields=( + FieldSpec("title", FieldType.STRING), + FieldSpec("output_path", FieldType.STRING), + ), + description="Screenshot the window matching title to a PNG.", + ), + CommandSpec( + "AC_save_window_layout", "Window", "Save Window Layout", + fields=(FieldSpec("path", FieldType.STRING, optional=True),), + description="Snapshot every window's position (optionally to a file).", + ), + CommandSpec( + "AC_restore_window_layout", "Window", "Restore Window Layout", + fields=(FieldSpec("layout", FieldType.FILE_PATH),), + description="Move windows back to a saved layout file.", + ), + CommandSpec( + "AC_tile_rect", "Window", "Tile Rect (compute)", + fields=( + FieldSpec("slot", FieldType.ENUM, + choices=("full", "left", "right", "top", "bottom", + "top_left", "top_right", "bottom_left", + "bottom_right", "center", "left_third", + "center_third", "right_third"), default="left"), + FieldSpec("screen", FieldType.STRING, optional=True, + placeholder=_RECT_PLACEHOLDER), + FieldSpec("gap", FieldType.INT, optional=True, default=0), + ), + description="Compute the rectangle for a tiling slot of the screen.", + ), + CommandSpec( + "AC_grid_rects", "Window", "Grid Rects (compute)", + fields=( + FieldSpec("rows", FieldType.INT, default=2), + FieldSpec("cols", FieldType.INT, default=2), + FieldSpec("screen", FieldType.STRING, optional=True, + placeholder=_RECT_PLACEHOLDER), + FieldSpec("gap", FieldType.INT, optional=True, default=0), + ), + description="Compute the cell rectangles of an R×C screen grid.", + ), + CommandSpec( + "AC_cascade_rects", "Window", "Cascade Rects (compute)", + fields=( + FieldSpec("count", FieldType.INT, default=3), + FieldSpec("screen", FieldType.STRING, optional=True, + placeholder=_RECT_PLACEHOLDER), + FieldSpec("offset", FieldType.INT, optional=True, default=30), + FieldSpec("size", FieldType.STRING, optional=True, + placeholder=_SIZE_PLACEHOLDER), + ), + description="Compute staggered, overlapping window rectangles.", + ), + CommandSpec( + "AC_enumerate_monitors", "Window", "Enumerate Monitors", + description="List monitors + virtual bounds (multi-display geometry).", + ), + CommandSpec( + "AC_monitor_at_point", "Window", "Monitor at Point", + fields=( + FieldSpec("x", FieldType.INT), + FieldSpec("y", FieldType.INT), + ), + description="Report which monitor contains a virtual point.", + ), + ]) def _add_flow_specs(specs: List[CommandSpec]) -> None: - specs.append(CommandSpec( - "AC_sleep", "Flow", "Sleep", - fields=( - FieldSpec("seconds", FieldType.FLOAT, default=1.0, min_value=0.0), + specs.extend([ + CommandSpec( + "AC_sleep", "Flow", "Sleep", + fields=( + FieldSpec("seconds", FieldType.FLOAT, default=1.0, min_value=0.0), + ), ), - )) - specs.append(CommandSpec( - "AC_wait_image", "Flow", "Wait for Image", - fields=( - FieldSpec("image", FieldType.FILE_PATH), - FieldSpec("threshold", FieldType.FLOAT, optional=True, default=0.8, - min_value=0.0, max_value=1.0), - FieldSpec("timeout", FieldType.FLOAT, optional=True, default=10.0), - FieldSpec("poll", FieldType.FLOAT, optional=True, default=0.2, - min_value=0.01), + CommandSpec( + "AC_wait_image", "Flow", "Wait for Image", + fields=( + FieldSpec("image", FieldType.FILE_PATH), + FieldSpec("threshold", FieldType.FLOAT, optional=True, default=0.8, + min_value=0.0, max_value=1.0), + FieldSpec("timeout", FieldType.FLOAT, optional=True, default=10.0), + FieldSpec("poll", FieldType.FLOAT, optional=True, default=0.2, + min_value=0.01), + ), ), - )) - specs.append(CommandSpec( - "AC_wait_actionable", "Flow", "Wait Until Actionable", - fields=( - FieldSpec("template", FieldType.FILE_PATH), - FieldSpec("timeout_s", FieldType.FLOAT, optional=True, default=5.0), - FieldSpec("stable_for_s", FieldType.FLOAT, optional=True, default=0.3), - FieldSpec("min_score", FieldType.FLOAT, optional=True, default=0.8, - min_value=0.0, max_value=1.0), - FieldSpec("region", FieldType.STRING, optional=True, - placeholder=_RECT_PLACEHOLDER), - ), - description="Wait until a target is visible + stable before acting.", - )) - specs.append(CommandSpec( - "AC_expect_poll", "Flow", "Expect (poll until match)", - fields=( - FieldSpec("action", FieldType.STRING, - placeholder='["AC_get_clipboard"]'), - FieldSpec("key", FieldType.STRING, optional=True, - placeholder="result dict key, e.g. text"), - FieldSpec("op", FieldType.ENUM, optional=True, default="truthy", - choices=("truthy", "equals", "contains", "gt", "regex")), - FieldSpec("expected", FieldType.STRING, optional=True), - FieldSpec("timeout_s", FieldType.FLOAT, optional=True, default=5.0), - FieldSpec("interval_s", FieldType.FLOAT, optional=True, default=0.25), - ), - description="Re-run an action until a key of its result matches.", - )) - specs.append(CommandSpec( - "AC_soft_assert", "Flow", "Soft Assert (aggregate)", - fields=( - FieldSpec("checks", FieldType.STRING, - placeholder='[{"value":5,"op":"gt","expected":3}]'), - FieldSpec("raise_on_fail", FieldType.BOOL, optional=True, - default=False), + CommandSpec( + "AC_wait_actionable", "Flow", "Wait Until Actionable", + fields=( + FieldSpec("template", FieldType.FILE_PATH), + FieldSpec("timeout_s", FieldType.FLOAT, optional=True, default=5.0), + FieldSpec("stable_for_s", FieldType.FLOAT, optional=True, default=0.3), + FieldSpec("min_score", FieldType.FLOAT, optional=True, default=0.8, + min_value=0.0, max_value=1.0), + FieldSpec("region", FieldType.STRING, optional=True, + placeholder=_RECT_PLACEHOLDER), + ), + description="Wait until a target is visible + stable before acting.", ), - description="Aggregate many checks and report all failures (not just first).", - )) - specs.append(CommandSpec( - "AC_replay_trace", "Flow", "Replay Agent Trace", - fields=( - FieldSpec("trace", FieldType.STRING, - placeholder='[{"action":["AC_click_mouse",{...}]}]'), + CommandSpec( + "AC_expect_poll", "Flow", "Expect (poll until match)", + fields=( + FieldSpec("action", FieldType.STRING, + placeholder='["AC_get_clipboard"]'), + FieldSpec("key", FieldType.STRING, optional=True, + placeholder="result dict key, e.g. text"), + FieldSpec("op", FieldType.ENUM, optional=True, default="truthy", + choices=("truthy", "equals", "contains", "gt", "regex")), + FieldSpec("expected", FieldType.STRING, optional=True), + FieldSpec("timeout_s", FieldType.FLOAT, optional=True, default=5.0), + FieldSpec("interval_s", FieldType.FLOAT, optional=True, default=0.25), + ), + description="Re-run an action until a key of its result matches.", ), - description="Replay a recorded trajectory's actions through the executor.", - )) - specs.append(CommandSpec( - "AC_wait_pixel", "Flow", "Wait for Pixel", - fields=( - FieldSpec("x", FieldType.INT), - FieldSpec("y", FieldType.INT), - FieldSpec("rgb", FieldType.RGB, placeholder="255,255,255"), - FieldSpec("tolerance", FieldType.INT, optional=True, default=0, - min_value=0, max_value=255), - FieldSpec("timeout", FieldType.FLOAT, optional=True, default=10.0), - FieldSpec("poll", FieldType.FLOAT, optional=True, default=0.2, - min_value=0.01), + CommandSpec( + "AC_soft_assert", "Flow", "Soft Assert (aggregate)", + fields=( + FieldSpec("checks", FieldType.STRING, + placeholder='[{"value":5,"op":"gt","expected":3}]'), + FieldSpec("raise_on_fail", FieldType.BOOL, optional=True, + default=False), + ), + description="Aggregate many checks and report all failures (not just first).", ), - )) - specs.append(CommandSpec( - "AC_wait_clipboard_change", "Flow", "Wait for Clipboard Change", - fields=( - FieldSpec("target", FieldType.STRING, optional=True), - FieldSpec("contains", FieldType.BOOL, optional=True, default=False), - FieldSpec("timeout_s", FieldType.FLOAT, optional=True, - default=10.0), - FieldSpec("poll_interval_s", FieldType.FLOAT, optional=True, - default=0.2, min_value=0.01), - ), - description="Wait until the clipboard changes or matches target.", - )) - specs.append(CommandSpec( - "AC_wait_image_gone", "Flow", "Wait for Image to Vanish", - fields=( - FieldSpec("image", FieldType.STRING, placeholder="path/to/spinner.png"), - FieldSpec("detect_threshold", FieldType.FLOAT, optional=True, - default=1.0), - FieldSpec("timeout_s", FieldType.FLOAT, optional=True, default=10.0), - FieldSpec("poll_interval_s", FieldType.FLOAT, optional=True, - default=0.2, min_value=0.01), - FieldSpec("gone_for_s", FieldType.FLOAT, optional=True, default=0.0), - ), - description="Block until an image (spinner/toast) leaves the screen.", - )) - specs.append(CommandSpec( - "AC_wait_text_gone", "Flow", "Wait for Text to Vanish", - fields=( - FieldSpec("text", FieldType.STRING, placeholder="Loading..."), - FieldSpec("timeout_s", FieldType.FLOAT, optional=True, default=10.0), - FieldSpec("poll_interval_s", FieldType.FLOAT, optional=True, - default=0.2, min_value=0.01), - FieldSpec("gone_for_s", FieldType.FLOAT, optional=True, default=0.0), + CommandSpec( + "AC_replay_trace", "Flow", "Replay Agent Trace", + fields=( + FieldSpec("trace", FieldType.STRING, + placeholder='[{"action":["AC_click_mouse",{...}]}]'), + ), + description="Replay a recorded trajectory's actions through the executor.", ), - description="Block until on-screen text (OCR) disappears.", - )) - specs.append(CommandSpec( - "AC_wait_color", "Flow", "Wait for Region Colour", - fields=( - FieldSpec("target_rgb", FieldType.STRING, placeholder="[0, 200, 0]"), - FieldSpec("region", FieldType.STRING, optional=True, - placeholder=_REGION_PLACEHOLDER), - FieldSpec("tolerance", FieldType.INT, optional=True, default=10), - FieldSpec("min_fraction", FieldType.FLOAT, optional=True, - default=0.5, min_value=0.0, max_value=1.0), - FieldSpec("present", FieldType.BOOL, optional=True, default=True), - FieldSpec("timeout_s", FieldType.FLOAT, optional=True, default=10.0), - FieldSpec("poll_interval_s", FieldType.FLOAT, optional=True, - default=0.2, min_value=0.01), - ), - description="Block until a colour fills (or leaves) a screen region.", - )) - specs.append(CommandSpec( - "AC_wait_window_title", "Flow", "Wait for Window Title", - fields=( - FieldSpec("pattern", FieldType.STRING, placeholder="Checkout$"), - FieldSpec("present", FieldType.BOOL, optional=True, default=True), - FieldSpec("regex", FieldType.BOOL, optional=True, default=True), - FieldSpec("timeout_s", FieldType.FLOAT, optional=True, default=10.0), - FieldSpec("poll_interval_s", FieldType.FLOAT, optional=True, - default=0.2, min_value=0.01), - ), - description="Block until a window title matches a regex (or vanishes).", - )) - specs.append(CommandSpec( - "AC_loop", "Flow", "Loop (N times)", - fields=( - FieldSpec("times", FieldType.INT, default=3, min_value=1), + CommandSpec( + "AC_wait_pixel", "Flow", "Wait for Pixel", + fields=( + FieldSpec("x", FieldType.INT), + FieldSpec("y", FieldType.INT), + FieldSpec("rgb", FieldType.RGB, placeholder="255,255,255"), + FieldSpec("tolerance", FieldType.INT, optional=True, default=0, + min_value=0, max_value=255), + FieldSpec("timeout", FieldType.FLOAT, optional=True, default=10.0), + FieldSpec("poll", FieldType.FLOAT, optional=True, default=0.2, + min_value=0.01), + ), ), - body_keys=("body",), - )) - specs.append(CommandSpec( - "AC_while_image", "Flow", "While Image Visible", - fields=( - FieldSpec("image", FieldType.FILE_PATH), - FieldSpec("threshold", FieldType.FLOAT, optional=True, default=0.8, - min_value=0.0, max_value=1.0), - FieldSpec("max_iter", FieldType.INT, optional=True, default=100, - min_value=1), + CommandSpec( + "AC_wait_clipboard_change", "Flow", "Wait for Clipboard Change", + fields=( + FieldSpec("target", FieldType.STRING, optional=True), + FieldSpec("contains", FieldType.BOOL, optional=True, default=False), + FieldSpec("timeout_s", FieldType.FLOAT, optional=True, + default=10.0), + FieldSpec("poll_interval_s", FieldType.FLOAT, optional=True, + default=0.2, min_value=0.01), + ), + description="Wait until the clipboard changes or matches target.", ), - body_keys=("body",), - )) - specs.append(CommandSpec( - "AC_if_image_found", "Flow", "If Image Found", - fields=( - FieldSpec("image", FieldType.FILE_PATH), - FieldSpec("threshold", FieldType.FLOAT, optional=True, default=0.8, - min_value=0.0, max_value=1.0), + CommandSpec( + "AC_wait_image_gone", "Flow", "Wait for Image to Vanish", + fields=( + FieldSpec("image", FieldType.STRING, placeholder="path/to/spinner.png"), + FieldSpec("detect_threshold", FieldType.FLOAT, optional=True, + default=1.0), + FieldSpec("timeout_s", FieldType.FLOAT, optional=True, default=10.0), + FieldSpec("poll_interval_s", FieldType.FLOAT, optional=True, + default=0.2, min_value=0.01), + FieldSpec("gone_for_s", FieldType.FLOAT, optional=True, default=0.0), + ), + description="Block until an image (spinner/toast) leaves the screen.", ), - body_keys=("then", "else"), - )) - specs.append(CommandSpec( - "AC_if_pixel", "Flow", "If Pixel Matches", - fields=( - FieldSpec("x", FieldType.INT), - FieldSpec("y", FieldType.INT), - FieldSpec("rgb", FieldType.RGB, placeholder="255,255,255"), - FieldSpec("tolerance", FieldType.INT, optional=True, default=0, - min_value=0, max_value=255), + CommandSpec( + "AC_wait_text_gone", "Flow", "Wait for Text to Vanish", + fields=( + FieldSpec("text", FieldType.STRING, placeholder="Loading..."), + FieldSpec("timeout_s", FieldType.FLOAT, optional=True, default=10.0), + FieldSpec("poll_interval_s", FieldType.FLOAT, optional=True, + default=0.2, min_value=0.01), + FieldSpec("gone_for_s", FieldType.FLOAT, optional=True, default=0.0), + ), + description="Block until on-screen text (OCR) disappears.", ), - body_keys=("then", "else"), - )) - specs.append(CommandSpec( - "AC_retry", "Flow", "Retry on Failure", - fields=( - FieldSpec("max_attempts", FieldType.INT, optional=True, default=3, - min_value=1), - FieldSpec("backoff", FieldType.FLOAT, optional=True, default=0.5, - min_value=0.0), + CommandSpec( + "AC_wait_color", "Flow", "Wait for Region Colour", + fields=( + FieldSpec("target_rgb", FieldType.STRING, placeholder="[0, 200, 0]"), + FieldSpec("region", FieldType.STRING, optional=True, + placeholder=_REGION_PLACEHOLDER), + FieldSpec("tolerance", FieldType.INT, optional=True, default=10), + FieldSpec("min_fraction", FieldType.FLOAT, optional=True, + default=0.5, min_value=0.0, max_value=1.0), + FieldSpec("present", FieldType.BOOL, optional=True, default=True), + FieldSpec("timeout_s", FieldType.FLOAT, optional=True, default=10.0), + FieldSpec("poll_interval_s", FieldType.FLOAT, optional=True, + default=0.2, min_value=0.01), + ), + description="Block until a colour fills (or leaves) a screen region.", ), - body_keys=("body",), - )) - specs.append(CommandSpec("AC_break", "Flow", "Break Loop")) - specs.append(CommandSpec("AC_continue", "Flow", "Continue Loop")) - specs.append(CommandSpec( - "AC_transform_var", "Flow", "Transform Variable", - fields=( - FieldSpec("name", FieldType.STRING), - FieldSpec("op", FieldType.ENUM, - choices=("upper", "lower", "strip", "title", - "lstrip", "rstrip", "replace", "regex", - "slice"), - default="strip"), - FieldSpec("into", FieldType.STRING, optional=True), - FieldSpec("find", FieldType.STRING, optional=True), - FieldSpec("replace_with", FieldType.STRING, optional=True), - FieldSpec("pattern", FieldType.STRING, optional=True), - ), - description="String-transform a variable (upper/strip/replace/regex/...).", - )) - specs.append(CommandSpec( - "AC_now_to_var", "Flow", "Timestamp into Variable", - fields=( - FieldSpec("var", FieldType.STRING, default="now"), - FieldSpec("format", FieldType.STRING, optional=True, - default="%Y-%m-%d %H:%M:%S"), + CommandSpec( + "AC_wait_window_title", "Flow", "Wait for Window Title", + fields=( + FieldSpec("pattern", FieldType.STRING, placeholder="Checkout$"), + FieldSpec("present", FieldType.BOOL, optional=True, default=True), + FieldSpec("regex", FieldType.BOOL, optional=True, default=True), + FieldSpec("timeout_s", FieldType.FLOAT, optional=True, default=10.0), + FieldSpec("poll_interval_s", FieldType.FLOAT, optional=True, + default=0.2, min_value=0.01), + ), + description="Block until a window title matches a regex (or vanishes).", ), - description="Store the current time (strftime format) in a variable.", - )) - specs.append(CommandSpec( - "AC_random_to_var", "Flow", "Random into Variable", - fields=( - FieldSpec("var", FieldType.STRING, default="random"), - FieldSpec("kind", FieldType.ENUM, - choices=("int", "float", "choice"), default="int"), - FieldSpec("min", FieldType.FLOAT, optional=True, default=0.0), - FieldSpec("max", FieldType.FLOAT, optional=True, default=100.0), - FieldSpec("seed", FieldType.INT, optional=True), - ), - description="Store a random int / float / choice in a variable.", - )) - specs.append(CommandSpec( - "AC_assert_var", "Flow", "Assert Variable", - fields=( - FieldSpec("name", FieldType.STRING), - FieldSpec("op", FieldType.ENUM, - choices=("eq", "ne", "lt", "le", "gt", "ge", - "contains", "startswith", "endswith", - "regex"), default="eq"), - FieldSpec("value", FieldType.STRING, optional=True), - ), - description="Fail if a flow variable doesn't satisfy the condition.", - )) - specs.append(CommandSpec( - "AC_assert_duration", "Flow", "Assert Duration (perf budget)", - fields=( - FieldSpec("max_ms", FieldType.FLOAT, default=1000.0, min_value=0.0), - FieldSpec("min_ms", FieldType.FLOAT, optional=True, default=0.0, - min_value=0.0), + CommandSpec( + "AC_loop", "Flow", "Loop (N times)", + fields=( + FieldSpec("times", FieldType.INT, default=3, min_value=1), + ), + body_keys=("body",), ), - body_keys=("body",), - description="Fail if the body takes longer than max_ms.", - )) - specs.append(CommandSpec( - "AC_parallel", "Flow", "Parallel Branches", - fields=( - FieldSpec("branches", FieldType.STRING, - placeholder='[[["AC_sleep",{"seconds":1}]]]'), + CommandSpec( + "AC_while_image", "Flow", "While Image Visible", + fields=( + FieldSpec("image", FieldType.FILE_PATH), + FieldSpec("threshold", FieldType.FLOAT, optional=True, default=0.8, + min_value=0.0, max_value=1.0), + FieldSpec("max_iter", FieldType.INT, optional=True, default=100, + min_value=1), + ), + body_keys=("body",), ), - description="Run each branch action list concurrently (JSON list).", - )) - specs.append(CommandSpec( - "AC_run_stoppable", "Flow", "Stoppable Run", - fields=( - FieldSpec("run_id", FieldType.STRING, optional=True, placeholder="nightly"), + CommandSpec( + "AC_if_image_found", "Flow", "If Image Found", + fields=( + FieldSpec("image", FieldType.FILE_PATH), + FieldSpec("threshold", FieldType.FLOAT, optional=True, default=0.8, + min_value=0.0, max_value=1.0), + ), + body_keys=("then", "else"), ), - body_keys=("body",), - description="Run the body as a named run that AC_stop_execution can end.", - )) - specs.append(CommandSpec( - "AC_stop_execution", "Flow", "Stop Execution", - fields=( - FieldSpec("run_id", FieldType.STRING, optional=True, placeholder="nightly"), - FieldSpec("reason", FieldType.STRING, optional=True), + CommandSpec( + "AC_if_pixel", "Flow", "If Pixel Matches", + fields=( + FieldSpec("x", FieldType.INT), + FieldSpec("y", FieldType.INT), + FieldSpec("rgb", FieldType.RGB, placeholder="255,255,255"), + FieldSpec("tolerance", FieldType.INT, optional=True, default=0, + min_value=0, max_value=255), + ), + body_keys=("then", "else"), ), - description="Ask a stoppable run (or every other one) to stop at its next checkpoint.", - )) - specs.append(CommandSpec( - "AC_list_executions", "Flow", "List Stoppable Runs", - description="List the stoppable runs in progress.", - )) - specs.append(CommandSpec( - "AC_define_macro", "Flow", "Define Macro", - fields=( - FieldSpec("name", FieldType.STRING), - FieldSpec("params", FieldType.STRING, optional=True, - placeholder="x, y"), + CommandSpec( + "AC_retry", "Flow", "Retry on Failure", + fields=( + FieldSpec("max_attempts", FieldType.INT, optional=True, default=3, + min_value=1), + FieldSpec("backoff", FieldType.FLOAT, optional=True, default=0.5, + min_value=0.0), + ), + body_keys=("body",), + ), + CommandSpec("AC_break", "Flow", "Break Loop"), + CommandSpec("AC_continue", "Flow", "Continue Loop"), + CommandSpec( + "AC_transform_var", "Flow", "Transform Variable", + fields=( + FieldSpec("name", FieldType.STRING), + FieldSpec("op", FieldType.ENUM, + choices=("upper", "lower", "strip", "title", + "lstrip", "rstrip", "replace", "regex", + "slice"), + default="strip"), + FieldSpec("into", FieldType.STRING, optional=True), + FieldSpec("find", FieldType.STRING, optional=True), + FieldSpec("replace_with", FieldType.STRING, optional=True), + FieldSpec("pattern", FieldType.STRING, optional=True), + ), + description="String-transform a variable (upper/strip/replace/regex/...).", ), - body_keys=("body",), - description="Register a named, parameterised action sub-routine.", - )) - specs.append(CommandSpec( - "AC_call_macro", "Flow", "Call Macro", - fields=( - FieldSpec("name", FieldType.STRING), - FieldSpec("args", FieldType.STRING, optional=True, - placeholder='{"x": 10, "y": 20}'), + CommandSpec( + "AC_now_to_var", "Flow", "Timestamp into Variable", + fields=( + FieldSpec("var", FieldType.STRING, default="now"), + FieldSpec("format", FieldType.STRING, optional=True, + default="%Y-%m-%d %H:%M:%S"), + ), + description="Store the current time (strftime format) in a variable.", ), - description="Invoke a macro defined by AC_define_macro.", - )) + CommandSpec( + "AC_random_to_var", "Flow", "Random into Variable", + fields=( + FieldSpec("var", FieldType.STRING, default="random"), + FieldSpec("kind", FieldType.ENUM, + choices=("int", "float", "choice"), default="int"), + FieldSpec("min", FieldType.FLOAT, optional=True, default=0.0), + FieldSpec("max", FieldType.FLOAT, optional=True, default=100.0), + FieldSpec("seed", FieldType.INT, optional=True), + ), + description="Store a random int / float / choice in a variable.", + ), + CommandSpec( + "AC_assert_var", "Flow", "Assert Variable", + fields=( + FieldSpec("name", FieldType.STRING), + FieldSpec("op", FieldType.ENUM, + choices=("eq", "ne", "lt", "le", "gt", "ge", + "contains", "startswith", "endswith", + "regex"), default="eq"), + FieldSpec("value", FieldType.STRING, optional=True), + ), + description="Fail if a flow variable doesn't satisfy the condition.", + ), + CommandSpec( + "AC_assert_duration", "Flow", "Assert Duration (perf budget)", + fields=( + FieldSpec("max_ms", FieldType.FLOAT, default=1000.0, min_value=0.0), + FieldSpec("min_ms", FieldType.FLOAT, optional=True, default=0.0, + min_value=0.0), + ), + body_keys=("body",), + description="Fail if the body takes longer than max_ms.", + ), + CommandSpec( + "AC_parallel", "Flow", "Parallel Branches", + fields=( + FieldSpec("branches", FieldType.STRING, + placeholder='[[["AC_sleep",{"seconds":1}]]]'), + ), + description="Run each branch action list concurrently (JSON list).", + ), + CommandSpec( + "AC_run_stoppable", "Flow", "Stoppable Run", + fields=( + FieldSpec("run_id", FieldType.STRING, optional=True, placeholder="nightly"), + ), + body_keys=("body",), + description="Run the body as a named run that AC_stop_execution can end.", + ), + CommandSpec( + "AC_stop_execution", "Flow", "Stop Execution", + fields=( + FieldSpec("run_id", FieldType.STRING, optional=True, placeholder="nightly"), + FieldSpec("reason", FieldType.STRING, optional=True), + ), + description="Ask a stoppable run (or every other one) to stop at its next checkpoint.", + ), + CommandSpec( + "AC_list_executions", "Flow", "List Stoppable Runs", + description="List the stoppable runs in progress.", + ), + CommandSpec( + "AC_define_macro", "Flow", "Define Macro", + fields=( + FieldSpec("name", FieldType.STRING), + FieldSpec("params", FieldType.STRING, optional=True, + placeholder="x, y"), + ), + body_keys=("body",), + description="Register a named, parameterised action sub-routine.", + ), + CommandSpec( + "AC_call_macro", "Flow", "Call Macro", + fields=( + FieldSpec("name", FieldType.STRING), + FieldSpec("args", FieldType.STRING, optional=True, + placeholder='{"x": 10, "y": 20}'), + ), + description="Invoke a macro defined by AC_define_macro.", + ), + ]) def _add_native_control_specs(specs: List[CommandSpec]) -> None: @@ -1709,354 +1730,360 @@ def _add_native_control_specs(specs: List[CommandSpec]) -> None: FieldSpec("app_name", FieldType.STRING, optional=True), FieldSpec("automation_id", FieldType.STRING, optional=True), ) - specs.append(CommandSpec( - "AC_control_get_value", _NATIVE_UI, "Get Control Value", - fields=fields, - description="Read a native control's value via the accessibility API.", - )) - specs.append(CommandSpec( - "AC_control_set_value", _NATIVE_UI, "Set Control Value", - fields=(FieldSpec("value", FieldType.STRING),) + fields, - description="Set a native control's value directly (no per-key typing).", - )) - specs.append(CommandSpec( - "AC_control_invoke", _NATIVE_UI, "Invoke Control", - fields=fields, - description="Invoke a native control (e.g. press a button).", - )) - specs.append(CommandSpec( - "AC_control_toggle", _NATIVE_UI, "Toggle Control", - fields=fields, - description="Toggle a native control (e.g. a checkbox).", - )) - specs.append(CommandSpec( - "AC_read_table", _NATIVE_UI, "Read Table / Grid", - fields=fields, - description="Read a grid/table/list control as rows of cell strings.", - )) - specs.append(CommandSpec( - "AC_expand_control", _NATIVE_UI, "Expand Control", - fields=fields, - description="Expand a tree node / combobox (ExpandCollapsePattern).", - )) - specs.append(CommandSpec( - "AC_collapse_control", _NATIVE_UI, "Collapse Control", - fields=fields, - description="Collapse a tree node / combobox (ExpandCollapsePattern).", - )) - specs.append(CommandSpec( - "AC_control_expand_state", _NATIVE_UI, "Control Expand State", - fields=fields, - description="Read expanded/collapsed/partial/leaf state of a control.", - )) - specs.append(CommandSpec( - "AC_select_control_item", _NATIVE_UI, "Select Control Item", - fields=fields, - description="Select a list / tree / tab item (SelectionItemPattern).", - )) - specs.append(CommandSpec( - "AC_control_range", _NATIVE_UI, "Get Control Range", - fields=fields, - description="Read a slider / progress range (RangeValuePattern).", - )) - specs.append(CommandSpec( - "AC_set_control_range", _NATIVE_UI, "Set Control Range", - fields=(FieldSpec("value", FieldType.FLOAT),) + fields, - description="Set a slider / progress / spinner value (RangeValuePattern).", - )) - specs.append(CommandSpec( - "AC_scroll_control_into_view", _NATIVE_UI, "Scroll Control Into View", - fields=fields, - description="Scroll a control into view (ScrollItemPattern).", - )) - specs.append(CommandSpec( - "AC_realize_item", _NATIVE_UI, "Realize Virtualized Item", - fields=( - FieldSpec("item_name", FieldType.STRING), - FieldSpec("by", FieldType.ENUM, optional=True, default="name", - choices=("name", "automation_id")), - FieldSpec("container_name", FieldType.STRING, optional=True), - FieldSpec("container_role", FieldType.STRING, optional=True), - FieldSpec("app_name", FieldType.STRING, optional=True), - FieldSpec("automation_id", FieldType.STRING, optional=True), - ), - description="Realize an off-screen item in a virtualized list/grid.", - )) - specs.append(CommandSpec( - "AC_get_element_properties", _NATIVE_UI, "Get Element Properties", - fields=fields, - description="Read rich UIA props (enabled/offscreen/help/status/keys).", - )) - specs.append(CommandSpec( - "AC_table_headers", _NATIVE_UI, "Get Table Headers", - fields=fields, - description="Read a table's row/column header labels (TablePattern).", - )) - specs.append(CommandSpec( - "AC_table_cell", _NATIVE_UI, "Get Table Cell (by index)", - fields=(FieldSpec("row", FieldType.INT), - FieldSpec("column", FieldType.INT)) + fields, - description="Read the cell at (row, column) with its span.", - )) - specs.append(CommandSpec( - "AC_cell_by_header", _NATIVE_UI, "Get Table Cell (by header)", - fields=(FieldSpec("row", FieldType.INT), - FieldSpec("column_header", FieldType.STRING)) + fields, - description="Read the cell at (row, named column) — assert by header.", - )) - specs.append(CommandSpec( - "AC_move_element", _NATIVE_UI, "Move Element (Transform)", - fields=(FieldSpec("x", FieldType.FLOAT), - FieldSpec("y", FieldType.FLOAT)) + fields, - description="Move a UIA element to (x, y) (TransformPattern).", - )) - specs.append(CommandSpec( - "AC_resize_element", _NATIVE_UI, "Resize Element (Transform)", - fields=(FieldSpec("width", FieldType.FLOAT), - FieldSpec("height", FieldType.FLOAT)) + fields, - description="Resize a UIA element (TransformPattern).", - )) - specs.append(CommandSpec( - "AC_set_window_state", _NATIVE_UI, "Set Window State", - fields=(FieldSpec("state", FieldType.ENUM, default="normal", - choices=("normal", "maximized", "minimized")),) + fields, - description="Minimize / maximize / restore a window (WindowPattern).", - )) - specs.append(CommandSpec( - "AC_window_interaction_state", _NATIVE_UI, "Window Interaction State", - fields=fields, - description="Read window readiness (ready/blocked_by_modal/...).", - )) - specs.append(CommandSpec( - "AC_legacy_info", _NATIVE_UI, "Legacy (MSAA) Info", - fields=fields, - description="Read an old control's MSAA info (LegacyIAccessible).", - )) - specs.append(CommandSpec( - "AC_legacy_default_action", _NATIVE_UI, "Legacy (MSAA) Default Action", - fields=fields, - description="Fire an old control's MSAA default action (fallback).", - )) - specs.append(CommandSpec( - "AC_get_selection", _NATIVE_UI, "Get Container Selection", - fields=fields, - description="Read a container's selection (SelectionPattern).", - )) - specs.append(CommandSpec( - "AC_list_views", _NATIVE_UI, "List Control Views", - fields=fields, - description="List a control's selectable views (MultipleViewPattern).", - )) - specs.append(CommandSpec( - "AC_set_view", _NATIVE_UI, "Set Control View", - fields=(FieldSpec("view", FieldType.STRING),) + fields, - description="Switch a control to the named view (MultipleViewPattern).", - )) - specs.append(CommandSpec( - "AC_a11y_focused", _NATIVE_UI, "Get Focused Element", - fields=(FieldSpec("app_name", FieldType.STRING, optional=True),), - description="Read the element holding keyboard focus (None if none).", - )) - specs.append(CommandSpec( - "AC_wait_for_focus_change", _NATIVE_UI, "Wait for Focus Change", - fields=(FieldSpec("timeout", FieldType.FLOAT, optional=True, - default=5.0),), - description="Block until keyboard focus moves (real UIA focus event).", - )) - specs.append(CommandSpec( - "AC_get_control_text", _NATIVE_UI, "Get Control Text", - fields=fields, - description="Read full text via TextPattern (multiline / document safe).", - )) - specs.append(CommandSpec( - "AC_find_control_text", _NATIVE_UI, "Find Text in Control", - fields=(FieldSpec("text", FieldType.STRING), - FieldSpec("ignore_case", FieldType.BOOL, optional=True, - default=True)) + fields, - description="Whether text occurs in a control (TextPattern.FindText).", - )) - specs.append(CommandSpec( - "AC_select_control_text", _NATIVE_UI, "Select Text in Control", - fields=(FieldSpec("text", FieldType.STRING), - FieldSpec("ignore_case", FieldType.BOOL, optional=True, - default=True)) + fields, - description="Find + select text in a control (FindText + Select).", - )) - specs.append(CommandSpec( - "AC_control_text_attributes", _NATIVE_UI, "Get Text Attributes", - fields=fields, - description="Read selection formatting (font/size/bold/italic/colour).", - )) - specs.append(CommandSpec( - "AC_get_selected_text", _NATIVE_UI, "Get Selected Text", - fields=fields, - description="Read the currently selected text via TextPattern.", - )) - specs.append(CommandSpec( - "AC_get_visible_text", _NATIVE_UI, "Get Visible Text", - fields=fields, - description="Read only the on-screen text via TextPattern.GetVisibleRanges.", - )) - specs.append(CommandSpec( - "AC_walk_tree", _NATIVE_UI, "Walk Accessibility Tree", - fields=(FieldSpec("app_name", FieldType.STRING, optional=True), - FieldSpec("max_results", FieldType.INT, optional=True, - default=500)), - description="Dump the a11y tree with friendly roles + a path per node.", - )) - specs.append(CommandSpec( - "AC_humanize_role", _NATIVE_UI, "Humanize UIA Role", - fields=(FieldSpec("role", FieldType.STRING),), - description="Translate a raw UIA role (ControlType_50000) to a name.", - )) + specs.extend([ + CommandSpec( + "AC_control_get_value", _NATIVE_UI, "Get Control Value", + fields=fields, + description="Read a native control's value via the accessibility API.", + ), + CommandSpec( + "AC_control_set_value", _NATIVE_UI, "Set Control Value", + fields=(FieldSpec("value", FieldType.STRING),) + fields, + description="Set a native control's value directly (no per-key typing).", + ), + CommandSpec( + "AC_control_invoke", _NATIVE_UI, "Invoke Control", + fields=fields, + description="Invoke a native control (e.g. press a button).", + ), + CommandSpec( + "AC_control_toggle", _NATIVE_UI, "Toggle Control", + fields=fields, + description="Toggle a native control (e.g. a checkbox).", + ), + CommandSpec( + "AC_read_table", _NATIVE_UI, "Read Table / Grid", + fields=fields, + description="Read a grid/table/list control as rows of cell strings.", + ), + CommandSpec( + "AC_expand_control", _NATIVE_UI, "Expand Control", + fields=fields, + description="Expand a tree node / combobox (ExpandCollapsePattern).", + ), + CommandSpec( + "AC_collapse_control", _NATIVE_UI, "Collapse Control", + fields=fields, + description="Collapse a tree node / combobox (ExpandCollapsePattern).", + ), + CommandSpec( + "AC_control_expand_state", _NATIVE_UI, "Control Expand State", + fields=fields, + description="Read expanded/collapsed/partial/leaf state of a control.", + ), + CommandSpec( + "AC_select_control_item", _NATIVE_UI, "Select Control Item", + fields=fields, + description="Select a list / tree / tab item (SelectionItemPattern).", + ), + CommandSpec( + "AC_control_range", _NATIVE_UI, "Get Control Range", + fields=fields, + description="Read a slider / progress range (RangeValuePattern).", + ), + CommandSpec( + "AC_set_control_range", _NATIVE_UI, "Set Control Range", + fields=(FieldSpec("value", FieldType.FLOAT),) + fields, + description="Set a slider / progress / spinner value (RangeValuePattern).", + ), + CommandSpec( + "AC_scroll_control_into_view", _NATIVE_UI, "Scroll Control Into View", + fields=fields, + description="Scroll a control into view (ScrollItemPattern).", + ), + CommandSpec( + "AC_realize_item", _NATIVE_UI, "Realize Virtualized Item", + fields=( + FieldSpec("item_name", FieldType.STRING), + FieldSpec("by", FieldType.ENUM, optional=True, default="name", + choices=("name", "automation_id")), + FieldSpec("container_name", FieldType.STRING, optional=True), + FieldSpec("container_role", FieldType.STRING, optional=True), + FieldSpec("app_name", FieldType.STRING, optional=True), + FieldSpec("automation_id", FieldType.STRING, optional=True), + ), + description="Realize an off-screen item in a virtualized list/grid.", + ), + CommandSpec( + "AC_get_element_properties", _NATIVE_UI, "Get Element Properties", + fields=fields, + description="Read rich UIA props (enabled/offscreen/help/status/keys).", + ), + CommandSpec( + "AC_table_headers", _NATIVE_UI, "Get Table Headers", + fields=fields, + description="Read a table's row/column header labels (TablePattern).", + ), + CommandSpec( + "AC_table_cell", _NATIVE_UI, "Get Table Cell (by index)", + fields=(FieldSpec("row", FieldType.INT), + FieldSpec("column", FieldType.INT)) + fields, + description="Read the cell at (row, column) with its span.", + ), + CommandSpec( + "AC_cell_by_header", _NATIVE_UI, "Get Table Cell (by header)", + fields=(FieldSpec("row", FieldType.INT), + FieldSpec("column_header", FieldType.STRING)) + fields, + description="Read the cell at (row, named column) — assert by header.", + ), + CommandSpec( + "AC_move_element", _NATIVE_UI, "Move Element (Transform)", + fields=(FieldSpec("x", FieldType.FLOAT), + FieldSpec("y", FieldType.FLOAT)) + fields, + description="Move a UIA element to (x, y) (TransformPattern).", + ), + CommandSpec( + "AC_resize_element", _NATIVE_UI, "Resize Element (Transform)", + fields=(FieldSpec("width", FieldType.FLOAT), + FieldSpec("height", FieldType.FLOAT)) + fields, + description="Resize a UIA element (TransformPattern).", + ), + CommandSpec( + "AC_set_window_state", _NATIVE_UI, "Set Window State", + fields=(FieldSpec("state", FieldType.ENUM, default="normal", + choices=("normal", "maximized", "minimized")),) + fields, + description="Minimize / maximize / restore a window (WindowPattern).", + ), + CommandSpec( + "AC_window_interaction_state", _NATIVE_UI, "Window Interaction State", + fields=fields, + description="Read window readiness (ready/blocked_by_modal/...).", + ), + CommandSpec( + "AC_legacy_info", _NATIVE_UI, "Legacy (MSAA) Info", + fields=fields, + description="Read an old control's MSAA info (LegacyIAccessible).", + ), + CommandSpec( + "AC_legacy_default_action", _NATIVE_UI, "Legacy (MSAA) Default Action", + fields=fields, + description="Fire an old control's MSAA default action (fallback).", + ), + CommandSpec( + "AC_get_selection", _NATIVE_UI, "Get Container Selection", + fields=fields, + description="Read a container's selection (SelectionPattern).", + ), + CommandSpec( + "AC_list_views", _NATIVE_UI, "List Control Views", + fields=fields, + description="List a control's selectable views (MultipleViewPattern).", + ), + CommandSpec( + "AC_set_view", _NATIVE_UI, "Set Control View", + fields=(FieldSpec("view", FieldType.STRING),) + fields, + description="Switch a control to the named view (MultipleViewPattern).", + ), + CommandSpec( + "AC_a11y_focused", _NATIVE_UI, "Get Focused Element", + fields=(FieldSpec("app_name", FieldType.STRING, optional=True),), + description="Read the element holding keyboard focus (None if none).", + ), + CommandSpec( + "AC_wait_for_focus_change", _NATIVE_UI, "Wait for Focus Change", + fields=(FieldSpec("timeout", FieldType.FLOAT, optional=True, + default=5.0),), + description="Block until keyboard focus moves (real UIA focus event).", + ), + CommandSpec( + "AC_get_control_text", _NATIVE_UI, "Get Control Text", + fields=fields, + description="Read full text via TextPattern (multiline / document safe).", + ), + CommandSpec( + "AC_find_control_text", _NATIVE_UI, "Find Text in Control", + fields=(FieldSpec("text", FieldType.STRING), + FieldSpec("ignore_case", FieldType.BOOL, optional=True, + default=True)) + fields, + description="Whether text occurs in a control (TextPattern.FindText).", + ), + CommandSpec( + "AC_select_control_text", _NATIVE_UI, "Select Text in Control", + fields=(FieldSpec("text", FieldType.STRING), + FieldSpec("ignore_case", FieldType.BOOL, optional=True, + default=True)) + fields, + description="Find + select text in a control (FindText + Select).", + ), + CommandSpec( + "AC_control_text_attributes", _NATIVE_UI, "Get Text Attributes", + fields=fields, + description="Read selection formatting (font/size/bold/italic/colour).", + ), + CommandSpec( + "AC_get_selected_text", _NATIVE_UI, "Get Selected Text", + fields=fields, + description="Read the currently selected text via TextPattern.", + ), + CommandSpec( + "AC_get_visible_text", _NATIVE_UI, "Get Visible Text", + fields=fields, + description="Read only the on-screen text via TextPattern.GetVisibleRanges.", + ), + CommandSpec( + "AC_walk_tree", _NATIVE_UI, "Walk Accessibility Tree", + fields=(FieldSpec("app_name", FieldType.STRING, optional=True), + FieldSpec("max_results", FieldType.INT, optional=True, + default=500)), + description="Dump the a11y tree with friendly roles + a path per node.", + ), + CommandSpec( + "AC_humanize_role", _NATIVE_UI, "Humanize UIA Role", + fields=(FieldSpec("role", FieldType.STRING),), + description="Translate a raw UIA role (ControlType_50000) to a name.", + ), + ]) tree_fields = (FieldSpec("app_name", FieldType.STRING, optional=True), FieldSpec("max_results", FieldType.INT, optional=True, default=500)) - specs.append(CommandSpec( - "AC_tab_order", _NATIVE_UI, "Keyboard Tab Order", - fields=tree_fields, - description="List focusable controls in keyboard Tab (reading) order.", - )) - specs.append(CommandSpec( - "AC_audit_focus_order", _NATIVE_UI, "Audit Focus Order (WCAG)", - fields=tree_fields, - description="WCAG 2.4.x focus-order audit: tab sequence + flagged issues.", - )) - specs.append(CommandSpec( - "AC_focus_control", _NATIVE_UI, "Set Keyboard Focus", - fields=fields, - description="Set keyboard focus on a control natively (UIA SetFocus).", - )) + specs.extend([ + CommandSpec( + "AC_tab_order", _NATIVE_UI, "Keyboard Tab Order", + fields=tree_fields, + description="List focusable controls in keyboard Tab (reading) order.", + ), + CommandSpec( + "AC_audit_focus_order", _NATIVE_UI, "Audit Focus Order (WCAG)", + fields=tree_fields, + description="WCAG 2.4.x focus-order audit: tab sequence + flagged issues.", + ), + CommandSpec( + "AC_focus_control", _NATIVE_UI, "Set Keyboard Focus", + fields=fields, + description="Set keyboard focus on a control natively (UIA SetFocus).", + ), + ]) def _add_misc_specs(specs: List[CommandSpec]) -> None: _add_native_control_specs(specs) - specs.append(CommandSpec( - "AC_set_clipboard_html", "Data", "Set Clipboard HTML", - fields=( - FieldSpec("html", FieldType.STRING, - placeholder="Bold rich text"), - FieldSpec("fragment_plaintext", FieldType.STRING, optional=True), + specs.extend([ + CommandSpec( + "AC_set_clipboard_html", "Data", "Set Clipboard HTML", + fields=( + FieldSpec("html", FieldType.STRING, + placeholder="Bold rich text"), + FieldSpec("fragment_plaintext", FieldType.STRING, optional=True), + ), + description="Put rich HTML on the clipboard (CF_HTML, Windows).", ), - description="Put rich HTML on the clipboard (CF_HTML, Windows).", - )) - specs.append(CommandSpec( - "AC_get_clipboard_html", "Data", "Get Clipboard HTML", - description="Read the clipboard's HTML fragment (CF_HTML, Windows).", - )) - specs.append(CommandSpec( - "AC_set_clipboard_files", "Data", "Set Clipboard Files", - fields=( - FieldSpec("paths", FieldType.STRING, - placeholder='["C:\\\\a\\\\one.txt", "C:\\\\b\\\\two.png"]'), + CommandSpec( + "AC_get_clipboard_html", "Data", "Get Clipboard HTML", + description="Read the clipboard's HTML fragment (CF_HTML, Windows).", ), - description="Put a file-drop list on the clipboard (CF_HDROP, Windows).", - )) - specs.append(CommandSpec( - "AC_get_clipboard_files", "Data", "Get Clipboard Files", - description="Read the clipboard's file-drop list (CF_HDROP, Windows).", - )) - specs.append(CommandSpec( - "AC_set_clipboard_rtf", "Data", "Set Clipboard RTF", - fields=(FieldSpec("text", FieldType.STRING, - placeholder="Styled paste text"),), - description="Put text on the clipboard as Rich Text Format (Windows).", - )) - specs.append(CommandSpec( - "AC_get_clipboard_rtf", "Data", "Get Clipboard RTF", - description="Read the clipboard's RTF document string (Windows).", - )) - specs.append(CommandSpec( - "AC_set_clipboard_csv", "Data", "Set Clipboard CSV/TSV", - fields=( - FieldSpec("rows", FieldType.STRING, - placeholder='[["a", "b"], ["1", "2"]]'), - FieldSpec("delimiter", FieldType.STRING, optional=True, default=","), + CommandSpec( + "AC_set_clipboard_files", "Data", "Set Clipboard Files", + fields=( + FieldSpec("paths", FieldType.STRING, + placeholder='["C:\\\\a\\\\one.txt", "C:\\\\b\\\\two.png"]'), + ), + description="Put a file-drop list on the clipboard (CF_HDROP, Windows).", ), - description="Put a table on the clipboard as the Csv format (Windows).", - )) - specs.append(CommandSpec( - "AC_get_clipboard_csv", "Data", "Get Clipboard CSV/TSV", - fields=(FieldSpec("delimiter", FieldType.STRING, optional=True, - default=","),), - description="Read the clipboard's Csv content as rows (Windows).", - )) - specs.append(CommandSpec( - "AC_clipboard_formats", "Data", "List Clipboard Formats", - description="Enumerate + classify the clipboard's formats (Windows).", - )) - specs.append(CommandSpec( - "AC_clipboard_get_image", "Data", "Clipboard: Save Image", - fields=(FieldSpec("path", FieldType.FILE_PATH, - placeholder="clipboard.png"),), - description="Save the clipboard's image to a PNG file, if it has one.", - )) - specs.append(CommandSpec( - "AC_clipboard_set_image", "Data", "Clipboard: Set Image", - fields=(FieldSpec("path", FieldType.FILE_PATH),), - description="Put an image file onto the clipboard.", - )) - specs.append(CommandSpec( - "AC_classify_formats", "Data", "Classify Clipboard Formats", - fields=(FieldSpec("formats", FieldType.STRING, - placeholder='[1, 13, {"id": 49161, "name": "Csv"}]'),), - description="Classify a provided list of clipboard formats (pure).", - )) - specs.append(CommandSpec( - "AC_diff_formats", "Data", "Diff Clipboard Formats", - fields=( - FieldSpec("before", FieldType.STRING, placeholder="[1, 13]"), - FieldSpec("after", FieldType.STRING, placeholder="[1, 13, 15]"), + CommandSpec( + "AC_get_clipboard_files", "Data", "Get Clipboard Files", + description="Read the clipboard's file-drop list (CF_HDROP, Windows).", ), - description="Diff two clipboard-format snapshots (pure).", - )) - specs.append(CommandSpec( - "AC_watchdog_add", "Flow", "Watchdog: Add Popup Rule", - fields=( - FieldSpec("title", FieldType.STRING), - FieldSpec("action", FieldType.STRING, optional=True, - default="close", placeholder="close / enter / esc"), - FieldSpec("case_sensitive", FieldType.BOOL, optional=True, - default=False), - FieldSpec("name", FieldType.STRING, optional=True), - ), - description="Auto-dismiss an unexpected window when it appears.", - )) - specs.append(CommandSpec( - "AC_watchdog_start", "Flow", "Watchdog: Start")) - specs.append(CommandSpec( - "AC_watchdog_stop", "Flow", "Watchdog: Stop")) - specs.append(CommandSpec( - "AC_watchdog_list", "Flow", "Watchdog: List Rules / Hits")) - specs.append(CommandSpec( - "AC_otp_to_var", "Flow", "OTP (TOTP) into Variable", - fields=( - FieldSpec("secret", FieldType.STRING), - FieldSpec("var", FieldType.STRING, default="otp"), - FieldSpec("digits", FieldType.INT, optional=True, default=6), - FieldSpec("step", FieldType.INT, optional=True, default=30), + CommandSpec( + "AC_set_clipboard_rtf", "Data", "Set Clipboard RTF", + fields=(FieldSpec("text", FieldType.STRING, + placeholder="Styled paste text"),), + description="Put text on the clipboard as Rich Text Format (Windows).", + ), + CommandSpec( + "AC_get_clipboard_rtf", "Data", "Get Clipboard RTF", + description="Read the clipboard's RTF document string (Windows).", + ), + CommandSpec( + "AC_set_clipboard_csv", "Data", "Set Clipboard CSV/TSV", + fields=( + FieldSpec("rows", FieldType.STRING, + placeholder='[["a", "b"], ["1", "2"]]'), + FieldSpec("delimiter", FieldType.STRING, optional=True, default=","), + ), + description="Put a table on the clipboard as the Csv format (Windows).", + ), + CommandSpec( + "AC_get_clipboard_csv", "Data", "Get Clipboard CSV/TSV", + fields=(FieldSpec("delimiter", FieldType.STRING, optional=True, + default=","),), + description="Read the clipboard's Csv content as rows (Windows).", + ), + CommandSpec( + "AC_clipboard_formats", "Data", "List Clipboard Formats", + description="Enumerate + classify the clipboard's formats (Windows).", + ), + CommandSpec( + "AC_clipboard_get_image", "Data", "Clipboard: Save Image", + fields=(FieldSpec("path", FieldType.FILE_PATH, + placeholder="clipboard.png"),), + description="Save the clipboard's image to a PNG file, if it has one.", + ), + CommandSpec( + "AC_clipboard_set_image", "Data", "Clipboard: Set Image", + fields=(FieldSpec("path", FieldType.FILE_PATH),), + description="Put an image file onto the clipboard.", + ), + CommandSpec( + "AC_classify_formats", "Data", "Classify Clipboard Formats", + fields=(FieldSpec("formats", FieldType.STRING, + placeholder='[1, 13, {"id": 49161, "name": "Csv"}]'),), + description="Classify a provided list of clipboard formats (pure).", + ), + CommandSpec( + "AC_diff_formats", "Data", "Diff Clipboard Formats", + fields=( + FieldSpec("before", FieldType.STRING, placeholder="[1, 13]"), + FieldSpec("after", FieldType.STRING, placeholder="[1, 13, 15]"), + ), + description="Diff two clipboard-format snapshots (pure).", + ), + CommandSpec( + "AC_watchdog_add", "Flow", "Watchdog: Add Popup Rule", + fields=( + FieldSpec("title", FieldType.STRING), + FieldSpec("action", FieldType.STRING, optional=True, + default="close", placeholder="close / enter / esc"), + FieldSpec("case_sensitive", FieldType.BOOL, optional=True, + default=False), + FieldSpec("name", FieldType.STRING, optional=True), + ), + description="Auto-dismiss an unexpected window when it appears.", + ), + CommandSpec( + "AC_watchdog_start", "Flow", "Watchdog: Start"), + CommandSpec( + "AC_watchdog_stop", "Flow", "Watchdog: Stop"), + CommandSpec( + "AC_watchdog_list", "Flow", "Watchdog: List Rules / Hits"), + CommandSpec( + "AC_otp_to_var", "Flow", "OTP (TOTP) into Variable", + fields=( + FieldSpec("secret", FieldType.STRING), + FieldSpec("var", FieldType.STRING, default="otp"), + FieldSpec("digits", FieldType.INT, optional=True, default=6), + FieldSpec("step", FieldType.INT, optional=True, default=30), + ), + description="Generate a TOTP 2FA code from a base32 secret.", ), - description="Generate a TOTP 2FA code from a base32 secret.", - )) - specs.append(CommandSpec( - "AC_handle_file_dialog", _NATIVE_UI, "Handle File Dialog", - fields=( - FieldSpec("path", FieldType.STRING), - FieldSpec("action", FieldType.ENUM, - choices=("open", "save", "folder"), - optional=True, default="open"), - FieldSpec("window_title", FieldType.STRING, optional=True), - FieldSpec("timeout_s", FieldType.FLOAT, optional=True, - default=10.0), - FieldSpec("confirm_key", FieldType.STRING, optional=True, - default="enter"), - ), - description="Wait for a native file dialog, type a path, confirm.", - )) - specs.append(CommandSpec( - "AC_assert_session_active", "Flow", "Assert Session Active", - description="Fail if the session is locked / non-interactive.", - )) + CommandSpec( + "AC_handle_file_dialog", _NATIVE_UI, "Handle File Dialog", + fields=( + FieldSpec("path", FieldType.STRING), + FieldSpec("action", FieldType.ENUM, + choices=("open", "save", "folder"), + optional=True, default="open"), + FieldSpec("window_title", FieldType.STRING, optional=True), + FieldSpec("timeout_s", FieldType.FLOAT, optional=True, + default=10.0), + FieldSpec("confirm_key", FieldType.STRING, optional=True, + default="enter"), + ), + description="Wait for a native file dialog, type a path, confirm.", + ), + CommandSpec( + "AC_assert_session_active", "Flow", "Assert Session Active", + description="Fail if the session is locked / non-interactive.", + ), + ]) _add_work_queue_specs(specs) _add_tooling_specs(specs) _add_authoring_specs(specs) @@ -2073,1511 +2100,1519 @@ def _add_misc_specs(specs: List[CommandSpec]) -> None: _add_devex_specs(specs) _add_audit_specs(specs) _add_webrunner_specs(specs) - specs.append(CommandSpec( - "AC_tween_drag", "Mouse", "Tweened Drag", - fields=( - FieldSpec("steps", FieldType.INT, optional=True, default=30), - FieldSpec("easing", FieldType.ENUM, - choices=("linear", "ease_in_out_quad", "ease_out_cubic", - "ease_in_cubic"), - optional=True, default="ease_in_out_quad"), - FieldSpec("button", FieldType.ENUM, choices=_MOUSE_BUTTONS, - optional=True, default="mouse_left"), - FieldSpec("step_delay_s", FieldType.FLOAT, optional=True, - default=0.0, min_value=0.0), - FieldSpec("settle_s", FieldType.FLOAT, optional=True, - default=0.0, min_value=0.0), - ), - description="Drag along an eased path; 'start'/'end' [x,y] via JSON " - "view.", - )) - specs.append(CommandSpec( - "AC_move_along_path", "Mouse", "Move Along Path", - fields=( - FieldSpec("waypoints", FieldType.STRING, - placeholder="[[100,100],[400,150],[400,500]]"), - FieldSpec("easing", FieldType.ENUM, - choices=("linear", "ease_in_out_quad", "ease_out_cubic", - "ease_in_cubic"), - optional=True, default="linear"), - FieldSpec("per_segment_steps", FieldType.INT, optional=True, - default=20), - ), - description="Move the pointer through a polyline of waypoints.", - )) - specs.append(CommandSpec( - "AC_drag_path", "Mouse", "Drag Along Path", - fields=( - FieldSpec("waypoints", FieldType.STRING, - placeholder="[[50,50],[300,50],[300,300]]"), - FieldSpec("button", FieldType.ENUM, choices=_MOUSE_BUTTONS, - optional=True, default="mouse_left"), - FieldSpec("easing", FieldType.ENUM, - choices=("linear", "ease_in_out_quad", "ease_out_cubic", - "ease_in_cubic"), - optional=True, default="linear"), - FieldSpec("per_segment_steps", FieldType.INT, optional=True, - default=20), - FieldSpec("step_delay_s", FieldType.FLOAT, optional=True, - default=0.0, min_value=0.0), - FieldSpec("settle_s", FieldType.FLOAT, optional=True, - default=0.0, min_value=0.0), - ), - description="Press, drag through a polyline of waypoints, release.", - )) - specs.append(CommandSpec( - "AC_move_mouse_relative", "Mouse", "Move Relative", - fields=( - FieldSpec("dx", FieldType.INT, placeholder="-40"), - FieldSpec("dy", FieldType.INT, placeholder="12"), + specs.extend([ + CommandSpec( + "AC_tween_drag", "Mouse", "Tweened Drag", + fields=( + FieldSpec("steps", FieldType.INT, optional=True, default=30), + FieldSpec("easing", FieldType.ENUM, + choices=("linear", "ease_in_out_quad", "ease_out_cubic", + "ease_in_cubic"), + optional=True, default="ease_in_out_quad"), + FieldSpec("button", FieldType.ENUM, choices=_MOUSE_BUTTONS, + optional=True, default="mouse_left"), + FieldSpec("step_delay_s", FieldType.FLOAT, optional=True, + default=0.0, min_value=0.0), + FieldSpec("settle_s", FieldType.FLOAT, optional=True, + default=0.0, min_value=0.0), + ), + description="Drag along an eased path; 'start'/'end' [x,y] via JSON " + "view.", ), - description="Move the pointer by (dx, dy) from its current position.", - )) - specs.append(CommandSpec( - "AC_grid_cell", "Mouse", "Grid Cell (row / col)", - fields=( - FieldSpec("boxes", FieldType.STRING, - placeholder="[[10,100,20,10],[110,100,20,10], ...]"), - FieldSpec("row", FieldType.INT, placeholder="0"), - FieldSpec("col", FieldType.INT, placeholder="0"), - FieldSpec("row_tolerance", FieldType.INT, optional=True, default=10), - FieldSpec("box_format", FieldType.ENUM, optional=True, default="xywh", - choices=("xywh", "ltrb")), - ), - description="Resolve a table cell centre by row/col from cell boxes.", - )) - specs.append(CommandSpec( - "AC_list_plugins", "Tools", "List Plugin Commands", - fields=(FieldSpec("group", FieldType.STRING, optional=True, - default="je_auto_control.commands"),), - description="Discover third-party AC_* commands from entry points.", - )) - specs.append(CommandSpec( - "AC_load_plugins", "Tools", "Load Plugin Commands", - fields=(FieldSpec("group", FieldType.STRING, optional=True, - default="je_auto_control.commands"),), - description="Discover + register third-party plugin commands.", - )) - specs.append(CommandSpec( - "AC_approval_request", "Tools", "Approval: Request", - fields=( - FieldSpec("action", FieldType.STRING), - FieldSpec("requester", FieldType.STRING, optional=True), - FieldSpec("db", FieldType.STRING, optional=True), + CommandSpec( + "AC_move_along_path", "Mouse", "Move Along Path", + fields=( + FieldSpec("waypoints", FieldType.STRING, + placeholder="[[100,100],[400,150],[400,500]]"), + FieldSpec("easing", FieldType.ENUM, + choices=("linear", "ease_in_out_quad", "ease_out_cubic", + "ease_in_cubic"), + optional=True, default="linear"), + FieldSpec("per_segment_steps", FieldType.INT, optional=True, + default=20), + ), + description="Move the pointer through a polyline of waypoints.", ), - description="Maker-checker: file a high-risk action for approval; returns {token}, the " - "request id to hand to the checker (shown, not masked).", - )) - specs.append(CommandSpec( - "AC_approval_approve", "Tools", "Approval: Approve", - fields=( - FieldSpec("token", FieldType.STRING), - FieldSpec("approver", FieldType.STRING), - FieldSpec("db", FieldType.STRING, optional=True), + CommandSpec( + "AC_drag_path", "Mouse", "Drag Along Path", + fields=( + FieldSpec("waypoints", FieldType.STRING, + placeholder="[[50,50],[300,50],[300,300]]"), + FieldSpec("button", FieldType.ENUM, choices=_MOUSE_BUTTONS, + optional=True, default="mouse_left"), + FieldSpec("easing", FieldType.ENUM, + choices=("linear", "ease_in_out_quad", "ease_out_cubic", + "ease_in_cubic"), + optional=True, default="linear"), + FieldSpec("per_segment_steps", FieldType.INT, optional=True, + default=20), + FieldSpec("step_delay_s", FieldType.FLOAT, optional=True, + default=0.0, min_value=0.0), + FieldSpec("settle_s", FieldType.FLOAT, optional=True, + default=0.0, min_value=0.0), + ), + description="Press, drag through a polyline of waypoints, release.", ), - description="Approve a request (approver must differ from requester).", - )) - specs.append(CommandSpec( - "AC_approval_reject", "Tools", "Approval: Reject", - fields=( - FieldSpec("token", FieldType.STRING), - FieldSpec("approver", FieldType.STRING), - FieldSpec("db", FieldType.STRING, optional=True), + CommandSpec( + "AC_move_mouse_relative", "Mouse", "Move Relative", + fields=( + FieldSpec("dx", FieldType.INT, placeholder="-40"), + FieldSpec("dy", FieldType.INT, placeholder="12"), + ), + description="Move the pointer by (dx, dy) from its current position.", ), - description="Reject a request (approver must differ from requester).", - )) - specs.append(CommandSpec( - "AC_approval_status", "Tools", "Approval: Status", - fields=( - FieldSpec("token", FieldType.STRING), - FieldSpec("db", FieldType.STRING, optional=True), + CommandSpec( + "AC_grid_cell", "Mouse", "Grid Cell (row / col)", + fields=( + FieldSpec("boxes", FieldType.STRING, + placeholder="[[10,100,20,10],[110,100,20,10], ...]"), + FieldSpec("row", FieldType.INT, placeholder="0"), + FieldSpec("col", FieldType.INT, placeholder="0"), + FieldSpec("row_tolerance", FieldType.INT, optional=True, default=10), + FieldSpec("box_format", FieldType.ENUM, optional=True, default="xywh", + choices=("xywh", "ltrb")), + ), + description="Resolve a table cell centre by row/col from cell boxes.", ), - description="Report a request's status and approved flag.", - )) - specs.append(CommandSpec( - "AC_lease_secret", "Tools", "Lease: Issue", - fields=( - FieldSpec("name", FieldType.STRING), - FieldSpec("ttl", FieldType.FLOAT, optional=True, default=300.0), + CommandSpec( + "AC_list_plugins", "Tools", "List Plugin Commands", + fields=(FieldSpec("group", FieldType.STRING, optional=True, + default="je_auto_control.commands"),), + description="Discover third-party AC_* commands from entry points.", ), - description="Issue a short-lived JIT lease for a secret (no value); returns {token, ttl}. " - "This builder masks the token -- read it with 'One-time values' after the run.", - sensitive_result=True, - )) - specs.append(CommandSpec( - "AC_lease_valid", "Tools", "Lease: Valid?", - fields=(FieldSpec("token", FieldType.STRING),), - description="Report whether a lease token is still valid.", - )) - specs.append(CommandSpec( - "AC_revoke_lease", "Tools", "Lease: Revoke", - fields=(FieldSpec("token", FieldType.STRING),), - description="Revoke a lease token immediately.", - )) - specs.append(CommandSpec( - "AC_lease_active", "Tools", "Lease: List Active", - fields=(), - description="List active leases (token, name, ttl_remaining). This builder masks each " - "token -- read them with 'One-time values' after the run.", - sensitive_result=True, - )) - specs.append(CommandSpec( - "AC_egress_allow", "Tools", "Egress: Set Allowlist", - fields=( - FieldSpec("allow", FieldType.STRING, optional=True, - placeholder="*.example.com, api.foo.com"), - FieldSpec("deny", FieldType.STRING, optional=True, - placeholder="bad.example.com"), + CommandSpec( + "AC_load_plugins", "Tools", "Load Plugin Commands", + fields=(FieldSpec("group", FieldType.STRING, optional=True, + default="je_auto_control.commands"),), + description="Discover + register third-party plugin commands.", ), - description="Lock the HTTP client to an egress allow/deny policy.", - )) - specs.append(CommandSpec( - "AC_egress_check", "Tools", "Egress: Check URL", - fields=(FieldSpec("url", FieldType.STRING, - placeholder="https://api.example.com"),), - description="Report whether a URL is permitted by the egress policy.", - )) - specs.append(CommandSpec( - "AC_egress_reset", "Tools", "Egress: Reset (allow-all)", - fields=(), - description="Clear the egress policy back to allow-all.", - )) - specs.append(CommandSpec( - "AC_verify_artifact", "Testing", "Approval: Verify Artifact", - fields=( - FieldSpec("name", FieldType.STRING, placeholder="login_screen"), - FieldSpec("content", FieldType.STRING), - FieldSpec("approvals_dir", FieldType.STRING, optional=True, - default=_APPROVALS_DIR), - FieldSpec("extension", FieldType.STRING, optional=True, - default="txt"), - ), - description="Compare content to its approved baseline (snapshot test).", - )) - specs.append(CommandSpec( - "AC_approve_artifact", "Testing", "Approval: Promote Received", - fields=( - FieldSpec("name", FieldType.STRING), - FieldSpec("approvals_dir", FieldType.STRING, optional=True, - default=_APPROVALS_DIR), - FieldSpec("extension", FieldType.STRING, optional=True, - default="txt"), + CommandSpec( + "AC_approval_request", "Tools", "Approval: Request", + fields=( + FieldSpec("action", FieldType.STRING), + FieldSpec("requester", FieldType.STRING, optional=True), + FieldSpec("db", FieldType.STRING, optional=True), + ), + description="Maker-checker: file a high-risk action for approval; returns {token}, the " + "request id to hand to the checker (shown, not masked).", ), - description="Promote a received artifact to the approved baseline.", - )) - specs.append(CommandSpec( - "AC_pending_artifacts", "Testing", "Approval: List Pending", - fields=(FieldSpec("approvals_dir", FieldType.STRING, optional=True, - default=_APPROVALS_DIR),), - description="List artifacts awaiting approval.", - )) - specs.append(CommandSpec( - "AC_evaluate_trajectory", "Agent", "Evaluate Trajectory", - fields=( - FieldSpec("trajectory", FieldType.STRING, - placeholder='[{"action": "AC_click_mouse"}]'), - FieldSpec("rubric", FieldType.STRING, - placeholder='{"required_actions": ["AC_type_text"]}'), + CommandSpec( + "AC_approval_approve", "Tools", "Approval: Approve", + fields=( + FieldSpec("token", FieldType.STRING), + FieldSpec("approver", FieldType.STRING), + FieldSpec("db", FieldType.STRING, optional=True), + ), + description="Approve a request (approver must differ from requester).", ), - description="Score an agent trajectory against a rubric (JSON).", - )) - specs.append(CommandSpec( - "AC_compliance_report", "Report", "Compliance Control Report", - fields=( - FieldSpec("evidence", FieldType.STRING, - placeholder='{"egress_allowlist_enforced": true}'), - FieldSpec("frameworks", FieldType.STRING, optional=True, - placeholder="SOC2, ISO27001"), - FieldSpec("path", FieldType.STRING, optional=True), - FieldSpec("fmt", FieldType.ENUM, optional=True, default="json", - choices=("json", "html")), - ), - description="Map governance evidence to SOC2/ISO 27001 controls.", - )) - specs.append(CommandSpec( - "AC_trace_record", "Agent", "Trace: Record Span", - fields=( - FieldSpec("operation", FieldType.STRING, placeholder="chat"), - FieldSpec("model", FieldType.STRING, optional=True), - FieldSpec("system", FieldType.STRING, optional=True), - FieldSpec("input_tokens", FieldType.INT, optional=True), - FieldSpec("output_tokens", FieldType.INT, optional=True), - FieldSpec("tool_name", FieldType.STRING, optional=True), - FieldSpec("duration_s", FieldType.FLOAT, optional=True, - default=0.0), - FieldSpec("status", FieldType.ENUM, optional=True, default="ok", - choices=("ok", "error")), - ), - description="Record a GenAI-convention span on the default trace.", - )) - specs.append(CommandSpec( - "AC_trace_summary", "Agent", "Trace: Summary", - fields=(), - description="Roll up the default agent trace (count/tokens/duration).", - )) - specs.append(CommandSpec( - "AC_trace_export", "Agent", "Trace: Export (OTLP)", - fields=(), - description="Export the default agent trace as OTLP-friendly spans.", - )) - specs.append(CommandSpec( - "AC_trace_reset", "Agent", "Trace: Reset", - fields=(), - description="Clear the default agent trace.", - )) - specs.append(CommandSpec( - "AC_write_step_video", "Report", "Step-Overlay Video", - fields=( - FieldSpec("steps", FieldType.STRING, - placeholder='[{"image": "s1.png", "caption": "Step 1"}]'), - FieldSpec("output", FieldType.STRING, default="walkthrough.mp4"), - FieldSpec("fps", FieldType.INT, optional=True, default=10), - FieldSpec("seconds_per_step", FieldType.FLOAT, optional=True, - default=2.0), - ), - description="Render captioned screenshots into a walkthrough video.", - )) - specs.append(CommandSpec( - "AC_fuzzy_ratio", "Data", "Fuzzy: Similarity Ratio", - fields=( - FieldSpec("left", FieldType.STRING), - FieldSpec("right", FieldType.STRING), - FieldSpec("ignore_case", FieldType.BOOL, optional=True, - default=True), + CommandSpec( + "AC_approval_reject", "Tools", "Approval: Reject", + fields=( + FieldSpec("token", FieldType.STRING), + FieldSpec("approver", FieldType.STRING), + FieldSpec("db", FieldType.STRING, optional=True), + ), + description="Reject a request (approver must differ from requester).", ), - description="Similarity score (0..1) between two strings.", - )) - specs.append(CommandSpec( - "AC_fuzzy_best_match", "Data", "Fuzzy: Best Match", - fields=( - FieldSpec("query", FieldType.STRING), - FieldSpec("choices", FieldType.STRING, - placeholder='["Save", "Cancel", "Submit"]'), - FieldSpec("score_cutoff", FieldType.FLOAT, optional=True, - default=0.0), - FieldSpec("ignore_case", FieldType.BOOL, optional=True, - default=True), - ), - description="Best fuzzy match of query within choices (JSON list).", - )) - specs.append(CommandSpec( - "AC_fuzzy_dedupe", "Data", "Fuzzy: Dedupe", - fields=( - FieldSpec("items", FieldType.STRING, - placeholder='["foo", "foo ", "bar"]'), - FieldSpec("threshold", FieldType.FLOAT, optional=True, - default=0.9), - FieldSpec("ignore_case", FieldType.BOOL, optional=True, - default=True), - ), - description="Collapse near-duplicate strings (JSON list).", - )) - specs.append(CommandSpec( - "AC_s3_upload", "Tools", "S3: Upload Artifact", - fields=( - FieldSpec("local_path", FieldType.FILE_PATH), - FieldSpec("key", FieldType.STRING, optional=True), + CommandSpec( + "AC_approval_status", "Tools", "Approval: Status", + fields=( + FieldSpec("token", FieldType.STRING), + FieldSpec("db", FieldType.STRING, optional=True), + ), + description="Report a request's status and approved flag.", ), - description="Upload a file to the configured default S3 store.", - )) - specs.append(CommandSpec( - "AC_s3_download", "Tools", "S3: Download Artifact", - fields=( - FieldSpec("key", FieldType.STRING), - FieldSpec("local_path", FieldType.STRING), + CommandSpec( + "AC_lease_secret", "Tools", "Lease: Issue", + fields=( + FieldSpec("name", FieldType.STRING), + FieldSpec("ttl", FieldType.FLOAT, optional=True, default=300.0), + ), + description="Issue a short-lived JIT lease for a secret (no value); returns {token, ttl}. " + "This builder masks the token -- read it with 'One-time values' after the run.", + sensitive_result=True, ), - description="Download an object from the default S3 store.", - )) - specs.append(CommandSpec( - "AC_s3_list", "Tools", "S3: List Artifacts", - fields=(FieldSpec("prefix", FieldType.STRING, optional=True),), - description="List object keys in the default S3 store.", - )) - specs.append(CommandSpec( - "AC_s3_delete", "Tools", "S3: Delete Artifact", - fields=(FieldSpec("key", FieldType.STRING),), - description="Delete an object from the default S3 store.", - )) - specs.append(CommandSpec( - "AC_image_hash", "Image", "Perceptual Hash", - fields=( - FieldSpec("path", FieldType.FILE_PATH), - FieldSpec("algo", FieldType.ENUM, optional=True, default="average", - choices=("average", "dhash")), + CommandSpec( + "AC_lease_valid", "Tools", "Lease: Valid?", + fields=(FieldSpec("token", FieldType.STRING),), + description="Report whether a lease token is still valid.", ), - description="Perceptual hash of an image (average or dhash).", - )) - specs.append(CommandSpec( - "AC_dedupe_images", "Image", "Dedupe Near-Identical Images", - fields=( - FieldSpec("paths", FieldType.STRING, - placeholder='["a.png", "b.png"]'), - FieldSpec("max_distance", FieldType.INT, optional=True, default=5), + CommandSpec( + "AC_revoke_lease", "Tools", "Lease: Revoke", + fields=(FieldSpec("token", FieldType.STRING),), + description="Revoke a lease token immediately.", ), - description="Collapse near-duplicate images by perceptual hash.", - )) - specs.append(CommandSpec( - "AC_canonicalize_url", "Data", "URL: Canonicalize", - fields=( - FieldSpec("url", FieldType.STRING, - placeholder="HTTP://Example.COM:80/a/../b?b=2&a=1#frag"), + CommandSpec( + "AC_lease_active", "Tools", "Lease: List Active", + fields=(), + description="List active leases (token, name, ttl_remaining). This builder masks each " + "token -- read them with 'One-time values' after the run.", + sensitive_result=True, ), - description="Canonical form of a URL, for equality and de-duplication.", - )) - specs.append(CommandSpec( - "AC_normalize_url", "Data", "URL: Normalize", - fields=( - FieldSpec("url", FieldType.STRING, - placeholder="https://EXAMPLE.com:443/p%2fx/"), - FieldSpec("sort_query", FieldType.BOOL, optional=True, - default=False), - FieldSpec("drop_fragment", FieldType.BOOL, optional=True, - default=False), - ), - description="RFC 3986 syntax-based normalisation of a URL.", - )) - specs.append(CommandSpec( - "AC_urls_equal", "Data", "URL: Compare", - fields=( - # https, because nothing in this example depends on the scheme — - # it shows that query order and the fragment are ignored. The - # canonicalize placeholder above keeps http on purpose: it needs - # port 80 to demonstrate default-port removal. - FieldSpec("first", FieldType.STRING, - placeholder="https://x.com/a?b=1&a=2"), - FieldSpec("second", FieldType.STRING, - placeholder="https://x.com/a?a=2&b=1#top"), - ), - description="Whether two URLs are equivalent after canonicalisation.", - )) - specs.append(CommandSpec( - "AC_parse_decimal", "Data", "Locale: Parse Decimal", - fields=( - FieldSpec("text", FieldType.STRING, placeholder="1.234,56"), - FieldSpec("locale", FieldType.STRING, optional=True, - default="en_US"), + CommandSpec( + "AC_egress_allow", "Tools", "Egress: Set Allowlist", + fields=( + FieldSpec("allow", FieldType.STRING, optional=True, + placeholder="*.example.com, api.foo.com"), + FieldSpec("deny", FieldType.STRING, optional=True, + placeholder="bad.example.com"), + ), + description="Lock the HTTP client to an egress allow/deny policy.", ), - description="Parse a locale-formatted decimal string to a float.", - )) - specs.append(CommandSpec( - "AC_parse_number", "Data", "Locale: Parse Number", - fields=( - FieldSpec("text", FieldType.STRING, placeholder="1,234"), - FieldSpec("locale", FieldType.STRING, optional=True, - default="en_US"), + CommandSpec( + "AC_egress_check", "Tools", "Egress: Check URL", + fields=(FieldSpec("url", FieldType.STRING, + placeholder="https://api.example.com"),), + description="Report whether a URL is permitted by the egress policy.", + ), + CommandSpec( + "AC_egress_reset", "Tools", "Egress: Reset (allow-all)", + fields=(), + description="Clear the egress policy back to allow-all.", + ), + CommandSpec( + "AC_verify_artifact", "Testing", "Approval: Verify Artifact", + fields=( + FieldSpec("name", FieldType.STRING, placeholder="login_screen"), + FieldSpec("content", FieldType.STRING), + FieldSpec("approvals_dir", FieldType.STRING, optional=True, + default=_APPROVALS_DIR), + FieldSpec("extension", FieldType.STRING, optional=True, + default="txt"), + ), + description="Compare content to its approved baseline (snapshot test).", + ), + CommandSpec( + "AC_approve_artifact", "Testing", "Approval: Promote Received", + fields=( + FieldSpec("name", FieldType.STRING), + FieldSpec("approvals_dir", FieldType.STRING, optional=True, + default=_APPROVALS_DIR), + FieldSpec("extension", FieldType.STRING, optional=True, + default="txt"), + ), + description="Promote a received artifact to the approved baseline.", + ), + CommandSpec( + "AC_pending_artifacts", "Testing", "Approval: List Pending", + fields=(FieldSpec("approvals_dir", FieldType.STRING, optional=True, + default=_APPROVALS_DIR),), + description="List artifacts awaiting approval.", + ), + CommandSpec( + "AC_evaluate_trajectory", "Agent", "Evaluate Trajectory", + fields=( + FieldSpec("trajectory", FieldType.STRING, + placeholder='[{"action": "AC_click_mouse"}]'), + FieldSpec("rubric", FieldType.STRING, + placeholder='{"required_actions": ["AC_type_text"]}'), + ), + description="Score an agent trajectory against a rubric (JSON).", + ), + CommandSpec( + "AC_compliance_report", "Report", "Compliance Control Report", + fields=( + FieldSpec("evidence", FieldType.STRING, + placeholder='{"egress_allowlist_enforced": true}'), + FieldSpec("frameworks", FieldType.STRING, optional=True, + placeholder="SOC2, ISO27001"), + FieldSpec("path", FieldType.STRING, optional=True), + FieldSpec("fmt", FieldType.ENUM, optional=True, default="json", + choices=("json", "html")), + ), + description="Map governance evidence to SOC2/ISO 27001 controls.", + ), + CommandSpec( + "AC_trace_record", "Agent", "Trace: Record Span", + fields=( + FieldSpec("operation", FieldType.STRING, placeholder="chat"), + FieldSpec("model", FieldType.STRING, optional=True), + FieldSpec("system", FieldType.STRING, optional=True), + FieldSpec("input_tokens", FieldType.INT, optional=True), + FieldSpec("output_tokens", FieldType.INT, optional=True), + FieldSpec("tool_name", FieldType.STRING, optional=True), + FieldSpec("duration_s", FieldType.FLOAT, optional=True, + default=0.0), + FieldSpec("status", FieldType.ENUM, optional=True, default="ok", + choices=("ok", "error")), + ), + description="Record a GenAI-convention span on the default trace.", + ), + CommandSpec( + "AC_trace_summary", "Agent", "Trace: Summary", + fields=(), + description="Roll up the default agent trace (count/tokens/duration).", + ), + CommandSpec( + "AC_trace_export", "Agent", "Trace: Export (OTLP)", + fields=(), + description="Export the default agent trace as OTLP-friendly spans.", + ), + CommandSpec( + "AC_trace_reset", "Agent", "Trace: Reset", + fields=(), + description="Clear the default agent trace.", + ), + CommandSpec( + "AC_write_step_video", "Report", "Step-Overlay Video", + fields=( + FieldSpec("steps", FieldType.STRING, + placeholder='[{"image": "s1.png", "caption": "Step 1"}]'), + FieldSpec("output", FieldType.STRING, default="walkthrough.mp4"), + FieldSpec("fps", FieldType.INT, optional=True, default=10), + FieldSpec("seconds_per_step", FieldType.FLOAT, optional=True, + default=2.0), + ), + description="Render captioned screenshots into a walkthrough video.", + ), + CommandSpec( + "AC_fuzzy_ratio", "Data", "Fuzzy: Similarity Ratio", + fields=( + FieldSpec("left", FieldType.STRING), + FieldSpec("right", FieldType.STRING), + FieldSpec("ignore_case", FieldType.BOOL, optional=True, + default=True), + ), + description="Similarity score (0..1) between two strings.", + ), + CommandSpec( + "AC_fuzzy_best_match", "Data", "Fuzzy: Best Match", + fields=( + FieldSpec("query", FieldType.STRING), + FieldSpec("choices", FieldType.STRING, + placeholder='["Save", "Cancel", "Submit"]'), + FieldSpec("score_cutoff", FieldType.FLOAT, optional=True, + default=0.0), + FieldSpec("ignore_case", FieldType.BOOL, optional=True, + default=True), + ), + description="Best fuzzy match of query within choices (JSON list).", + ), + CommandSpec( + "AC_fuzzy_dedupe", "Data", "Fuzzy: Dedupe", + fields=( + FieldSpec("items", FieldType.STRING, + placeholder='["foo", "foo ", "bar"]'), + FieldSpec("threshold", FieldType.FLOAT, optional=True, + default=0.9), + FieldSpec("ignore_case", FieldType.BOOL, optional=True, + default=True), + ), + description="Collapse near-duplicate strings (JSON list).", + ), + CommandSpec( + "AC_s3_upload", "Tools", "S3: Upload Artifact", + fields=( + FieldSpec("local_path", FieldType.FILE_PATH), + FieldSpec("key", FieldType.STRING, optional=True), + ), + description="Upload a file to the configured default S3 store.", + ), + CommandSpec( + "AC_s3_download", "Tools", "S3: Download Artifact", + fields=( + FieldSpec("key", FieldType.STRING), + FieldSpec("local_path", FieldType.STRING), + ), + description="Download an object from the default S3 store.", + ), + CommandSpec( + "AC_s3_list", "Tools", "S3: List Artifacts", + fields=(FieldSpec("prefix", FieldType.STRING, optional=True),), + description="List object keys in the default S3 store.", + ), + CommandSpec( + "AC_s3_delete", "Tools", "S3: Delete Artifact", + fields=(FieldSpec("key", FieldType.STRING),), + description="Delete an object from the default S3 store.", + ), + CommandSpec( + "AC_image_hash", "Image", "Perceptual Hash", + fields=( + FieldSpec("path", FieldType.FILE_PATH), + FieldSpec("algo", FieldType.ENUM, optional=True, default="average", + choices=("average", "dhash")), + ), + description="Perceptual hash of an image (average or dhash).", + ), + CommandSpec( + "AC_dedupe_images", "Image", "Dedupe Near-Identical Images", + fields=( + FieldSpec("paths", FieldType.STRING, + placeholder='["a.png", "b.png"]'), + FieldSpec("max_distance", FieldType.INT, optional=True, default=5), + ), + description="Collapse near-duplicate images by perceptual hash.", + ), + CommandSpec( + "AC_canonicalize_url", "Data", "URL: Canonicalize", + fields=( + FieldSpec("url", FieldType.STRING, + placeholder="HTTP://Example.COM:80/a/../b?b=2&a=1#frag"), + ), + description="Canonical form of a URL, for equality and de-duplication.", + ), + CommandSpec( + "AC_normalize_url", "Data", "URL: Normalize", + fields=( + FieldSpec("url", FieldType.STRING, + placeholder="https://EXAMPLE.com:443/p%2fx/"), + FieldSpec("sort_query", FieldType.BOOL, optional=True, + default=False), + FieldSpec("drop_fragment", FieldType.BOOL, optional=True, + default=False), + ), + description="RFC 3986 syntax-based normalisation of a URL.", + ), + CommandSpec( + "AC_urls_equal", "Data", "URL: Compare", + fields=( + # https, because nothing in this example depends on the scheme — + # it shows that query order and the fragment are ignored. The + # canonicalize placeholder above keeps http on purpose: it needs + # port 80 to demonstrate default-port removal. + FieldSpec("first", FieldType.STRING, + placeholder="https://x.com/a?b=1&a=2"), + FieldSpec("second", FieldType.STRING, + placeholder="https://x.com/a?a=2&b=1#top"), + ), + description="Whether two URLs are equivalent after canonicalisation.", + ), + CommandSpec( + "AC_parse_decimal", "Data", "Locale: Parse Decimal", + fields=( + FieldSpec("text", FieldType.STRING, placeholder="1.234,56"), + FieldSpec("locale", FieldType.STRING, optional=True, + default="en_US"), + ), + description="Parse a locale-formatted decimal string to a float.", + ), + CommandSpec( + "AC_parse_number", "Data", "Locale: Parse Number", + fields=( + FieldSpec("text", FieldType.STRING, placeholder="1,234"), + FieldSpec("locale", FieldType.STRING, optional=True, + default="en_US"), + ), + description="Parse a locale-formatted integer string to an int.", + ), + CommandSpec( + "AC_format_decimal", "Data", "Locale: Format Decimal", + fields=( + FieldSpec("value", FieldType.FLOAT), + FieldSpec("locale", FieldType.STRING, optional=True, + default="en_US"), + ), + description="Format a number for a locale.", + ), + CommandSpec( + "AC_format_currency", "Data", "Locale: Format Currency", + fields=( + FieldSpec("value", FieldType.FLOAT), + FieldSpec("currency", FieldType.STRING, placeholder="USD"), + FieldSpec("locale", FieldType.STRING, optional=True, + default="en_US"), + ), + description="Format a value as currency (ISO 4217) for a locale.", + ), + CommandSpec( + "AC_format_date", "Data", "Locale: Format Date", + fields=( + FieldSpec("value", FieldType.STRING, placeholder="2026-06-20"), + FieldSpec("locale", FieldType.STRING, optional=True, + default="en_US"), + FieldSpec("fmt", FieldType.ENUM, optional=True, default="medium", + choices=("short", "medium", "long", "full")), + ), + description="Format an ISO date string for a locale.", + ), + CommandSpec( + "AC_voice_register", "Agent", "Voice: Register Command", + fields=( + FieldSpec("phrase", FieldType.STRING, placeholder="save file"), + FieldSpec("actions", FieldType.STRING, + placeholder='[["AC_hotkey", {"keys": ["ctrl", "s"]}]]'), + ), + description="Map a spoken phrase to an action list (JSON).", + ), + CommandSpec( + "AC_voice_dispatch", "Agent", "Voice: Dispatch Text", + fields=(FieldSpec("text", FieldType.STRING, + placeholder="save the file"),), + description="Run the command best matching recognized text.", + ), + CommandSpec( + "AC_voice_list", "Agent", "Voice: List Commands", + fields=(), + description="List registered voice-command phrases.", + ), + CommandSpec( + "AC_voice_clear", "Agent", "Voice: Clear Commands", + fields=(), + description="Remove all registered voice commands.", + ), + CommandSpec( + "AC_to_physical", "Agent", "Coords: Model -> Physical", + fields=( + FieldSpec("x", FieldType.FLOAT), FieldSpec("y", FieldType.FLOAT), + FieldSpec("physical_w", FieldType.INT), + FieldSpec("physical_h", FieldType.INT), + FieldSpec("model_w", FieldType.INT), + FieldSpec("model_h", FieldType.INT), + ), + description="Map a model-grid coordinate to physical pixels.", + ), + CommandSpec( + "AC_to_model", "Agent", "Coords: Physical -> Model", + fields=( + FieldSpec("x", FieldType.INT), FieldSpec("y", FieldType.INT), + FieldSpec("physical_w", FieldType.INT), + FieldSpec("physical_h", FieldType.INT), + FieldSpec("model_w", FieldType.INT), + FieldSpec("model_h", FieldType.INT), + ), + description="Map a physical-pixel coordinate to a model grid.", + ), + CommandSpec( + "AC_loop_guard_observe", "Agent", "Loop Guard: Observe Step", + fields=( + FieldSpec("tool", FieldType.STRING, placeholder="AC_click_mouse"), + FieldSpec("args", FieldType.STRING, optional=True, + placeholder='{"x": 10, "y": 20}'), + FieldSpec("result_digest", FieldType.STRING, optional=True), + ), + description="Detect repeat/ping-pong/no-op stuck-loop patterns.", + ), + CommandSpec( + "AC_loop_guard_reset", "Agent", "Loop Guard: Reset", + fields=(), + description="Clear the default loop guard's history.", + ), + CommandSpec( + "AC_mine_actions", "Report", "Mine Action Log", + fields=( + FieldSpec("actions", FieldType.STRING, + placeholder='[["AC_click_mouse", {}], ...]'), + FieldSpec("min_len", FieldType.INT, optional=True, default=2), + FieldSpec("max_len", FieldType.INT, optional=True, default=5), + FieldSpec("min_count", FieldType.INT, optional=True, default=3), + ), + description="Find repeated sequences + rank automation candidates.", + ), + CommandSpec( + "AC_set_asset", "Data", "Asset: Set", + fields=( + FieldSpec("name", FieldType.STRING), + FieldSpec("value", FieldType.STRING), + FieldSpec("asset_type", FieldType.ENUM, optional=True, + default="text", + choices=("text", "int", "bool", "credential")), + FieldSpec("environment", FieldType.STRING, optional=True, + default="default"), + FieldSpec("db", FieldType.STRING, optional=True), + ), + description="Store a typed, environment-scoped asset.", + ), + CommandSpec( + "AC_get_asset", "Data", "Asset: Get", + fields=( + FieldSpec("name", FieldType.STRING), + FieldSpec("environment", FieldType.STRING, optional=True, + default="default"), + FieldSpec("db", FieldType.STRING, optional=True), + ), + description="Read a typed asset (credential stays a reference).", + ), + CommandSpec( + "AC_list_assets", "Data", "Asset: List", + fields=( + FieldSpec("environment", FieldType.STRING, optional=True), + FieldSpec("db", FieldType.STRING, optional=True), + ), + description="List assets (name/type/environment, no values).", + ), + CommandSpec( + "AC_emit_event", "Tools", "Emit CloudEvent", + fields=( + FieldSpec("event_type", FieldType.STRING, + placeholder="com.example.run.finished"), + FieldSpec("data", FieldType.STRING, optional=True, + placeholder='{"run_id": "42"}'), + FieldSpec("source", FieldType.STRING, optional=True, + default="je_auto_control"), + FieldSpec("subject", FieldType.STRING, optional=True), + FieldSpec("url", FieldType.STRING, optional=True, + placeholder="https://hooks.example.com/ce"), + ), + description="Wrap data in a CloudEvents envelope; optionally POST it.", + ), + CommandSpec( + "AC_notify_webhook", "Tools", "Notify: Webhook/Chat", + fields=( + FieldSpec("url", FieldType.STRING, + placeholder="https://hooks.example.com/..."), + FieldSpec("text", FieldType.STRING), + FieldSpec("transport", FieldType.ENUM, optional=True, + default="raw", + choices=("raw", "slack", "discord", "teams")), + FieldSpec("title", FieldType.STRING, optional=True), + ), + description="Send a Slack/Discord/Teams/raw webhook notification.", + ), + CommandSpec( + "AC_json_query", "Data", "JSONPath: Query", + fields=( + FieldSpec("data", FieldType.STRING, + placeholder='{"a": [1, 2]}'), + FieldSpec("path", FieldType.STRING, placeholder="$.a[*]"), + ), + description="Query parsed JSON with a JSONPath subset (all matches).", + ), + CommandSpec( + "AC_json_extract", "Data", "JSONPath: Extract Mapping", + fields=( + FieldSpec("data", FieldType.STRING), + FieldSpec("mapping", FieldType.STRING, + placeholder='{"name": "$.user.name"}'), + ), + description="Extract a {key: jsonpath} mapping into a flat object.", + ), + CommandSpec( + "AC_validate_json", "Data", "JSON Schema: Validate", + fields=( + FieldSpec("data", FieldType.STRING, + placeholder='{"name": "Jo", "age": 30}'), + FieldSpec("schema", FieldType.STRING, + placeholder='{"type": "object", "required": ["name"]}'), + ), + description="Validate JSON against a JSON Schema; returns {ok, errors}.", + ), + CommandSpec( + "AC_match_json", "Data", "JSON Contract: Match", + fields=( + FieldSpec("actual", FieldType.STRING, + placeholder='{"id": 1, "name": "Ada"}'), + FieldSpec("expected", FieldType.STRING, + placeholder='{"id": 1, "name": "Ada"}'), + FieldSpec("partial", FieldType.BOOL, optional=True, default=False), + FieldSpec("match_type", FieldType.BOOL, optional=True, + default=False), + ), + description="Match JSON against expected (partial/type); {ok, mismatches}.", + ), + CommandSpec( + "AC_diff_json", "Data", "JSON Contract: Diff", + fields=( + FieldSpec("actual", FieldType.STRING, placeholder='[1, 2, 3]'), + FieldSpec("expected", FieldType.STRING, placeholder='[1, 2]'), + ), + description="Path-tagged diff between two JSON payloads; {diffs}.", + ), + CommandSpec( + "AC_evaluate_flag", "Flow", "Feature Flag: Evaluate", + fields=( + FieldSpec("flags", FieldType.STRING, + placeholder='{"flags": {"f": {"variants": {...}}}}'), + FieldSpec("key", FieldType.STRING, placeholder="new-checkout"), + FieldSpec("context", FieldType.STRING, optional=True, + placeholder='{"targeting_key": "user1", "country": "US"}'), + ), + description="Evaluate a feature flag; returns {value, variant, reason}.", + ), + CommandSpec( + "AC_flag_enabled", "Flow", "Feature Flag: Enabled?", + fields=( + FieldSpec("flags", FieldType.STRING), + FieldSpec("key", FieldType.STRING, placeholder="new-checkout"), + FieldSpec("context", FieldType.STRING, optional=True), + ), + description="Boolean feature-flag check; returns {enabled}.", + ), + CommandSpec( + "AC_unified_diff", "Data", "Text: Unified Diff", + fields=( + FieldSpec("a", FieldType.STRING, placeholder="original text"), + FieldSpec("b", FieldType.STRING, placeholder="changed text"), + ), + description="Unified diff transforming a into b; returns {diff}.", + ), + CommandSpec( + "AC_apply_unified", "Data", "Text: Apply Diff", + fields=( + FieldSpec("text", FieldType.STRING, placeholder="original text"), + FieldSpec("diff", FieldType.STRING, placeholder="@@ -1 +1 @@ ..."), + ), + description="Apply a unified diff to text; returns {result}.", + ), + CommandSpec( + "AC_three_way_merge", "Data", "Text: Three-Way Merge", + fields=( + FieldSpec("base", FieldType.STRING, placeholder="base text"), + FieldSpec("ours", FieldType.STRING, placeholder="our text"), + FieldSpec("theirs", FieldType.STRING, placeholder="their text"), + ), + description="Merge ours/theirs against base; returns {text, clean, conflicts}.", ), - description="Parse a locale-formatted integer string to an int.", - )) - specs.append(CommandSpec( - "AC_format_decimal", "Data", "Locale: Format Decimal", - fields=( - FieldSpec("value", FieldType.FLOAT), - FieldSpec("locale", FieldType.STRING, optional=True, - default="en_US"), + CommandSpec( + "AC_rrule_occurrences", "Flow", "Recurrence: Expand (RRULE)", + fields=( + FieldSpec("rule", FieldType.STRING, + placeholder="FREQ=MONTHLY;BYDAY=2TU"), + FieldSpec("dtstart", FieldType.STRING, + placeholder="2026-01-01T09:00:00"), + FieldSpec("count", FieldType.INT, optional=True, default=10), + ), + description="Expand an RFC 5545 RRULE into ISO datetimes.", ), - description="Format a number for a locale.", - )) - specs.append(CommandSpec( - "AC_format_currency", "Data", "Locale: Format Currency", - fields=( - FieldSpec("value", FieldType.FLOAT), - FieldSpec("currency", FieldType.STRING, placeholder="USD"), - FieldSpec("locale", FieldType.STRING, optional=True, - default="en_US"), + CommandSpec( + "AC_rrule_next", "Flow", "Recurrence: Next Occurrence", + fields=( + FieldSpec("rule", FieldType.STRING, + placeholder="FREQ=WEEKLY;BYDAY=MO,WE,FR"), + FieldSpec("dtstart", FieldType.STRING, + placeholder="2026-01-01T09:00:00"), + FieldSpec("now", FieldType.STRING, optional=True, + placeholder="2026-03-15T00:00:00"), + ), + description="Next RRULE occurrence at/after now; returns {next}.", ), - description="Format a value as currency (ISO 4217) for a locale.", - )) - specs.append(CommandSpec( - "AC_format_date", "Data", "Locale: Format Date", - fields=( - FieldSpec("value", FieldType.STRING, placeholder="2026-06-20"), - FieldSpec("locale", FieldType.STRING, optional=True, - default="en_US"), - FieldSpec("fmt", FieldType.ENUM, optional=True, default="medium", - choices=("short", "medium", "long", "full")), + CommandSpec( + "AC_describe_stats", "Data", "Describe Statistics", + fields=( + FieldSpec("values", FieldType.STRING, + placeholder="[12.0, 9.5, 14.2, 11.1]"), + ), + description="Summary stats + percentiles of a numeric list.", ), - description="Format an ISO date string for a locale.", - )) - specs.append(CommandSpec( - "AC_voice_register", "Agent", "Voice: Register Command", - fields=( - FieldSpec("phrase", FieldType.STRING, placeholder="save file"), - FieldSpec("actions", FieldType.STRING, - placeholder='[["AC_hotkey", {"keys": ["ctrl", "s"]}]]'), + CommandSpec( + "AC_ab_significance", "Data", "A/B Significance (z-test)", + fields=( + FieldSpec("a_conv", FieldType.INT, placeholder="90"), + FieldSpec("a_n", FieldType.INT, placeholder="200"), + FieldSpec("b_conv", FieldType.INT, placeholder="110"), + FieldSpec("b_n", FieldType.INT, placeholder="200"), + ), + description="Two-proportion z-test; returns {z, p_value, significant, ci}.", ), - description="Map a spoken phrase to an action list (JSON).", - )) - specs.append(CommandSpec( - "AC_voice_dispatch", "Agent", "Voice: Dispatch Text", - fields=(FieldSpec("text", FieldType.STRING, - placeholder="save the file"),), - description="Run the command best matching recognized text.", - )) - specs.append(CommandSpec( - "AC_voice_list", "Agent", "Voice: List Commands", - fields=(), - description="List registered voice-command phrases.", - )) - specs.append(CommandSpec( - "AC_voice_clear", "Agent", "Voice: Clear Commands", - fields=(), - description="Remove all registered voice commands.", - )) - specs.append(CommandSpec( - "AC_to_physical", "Agent", "Coords: Model -> Physical", - fields=( - FieldSpec("x", FieldType.FLOAT), FieldSpec("y", FieldType.FLOAT), - FieldSpec("physical_w", FieldType.INT), - FieldSpec("physical_h", FieldType.INT), - FieldSpec("model_w", FieldType.INT), - FieldSpec("model_h", FieldType.INT), + CommandSpec( + "AC_search_documents", "Data", "Full-Text Search (BM25)", + fields=( + FieldSpec("docs", FieldType.STRING, + placeholder='{"d1": "quick brown fox", "d2": "lazy dog"}'), + FieldSpec("query", FieldType.STRING, placeholder="quick fox"), + FieldSpec("top_k", FieldType.INT, optional=True, default=10), + FieldSpec("mode", FieldType.STRING, optional=True, + placeholder="bm25", choices=("bm25", "tfidf")), + ), + description="Rank a {id: text} corpus for a query; returns {hits}.", ), - description="Map a model-grid coordinate to physical pixels.", - )) - specs.append(CommandSpec( - "AC_to_model", "Agent", "Coords: Physical -> Model", - fields=( - FieldSpec("x", FieldType.INT), FieldSpec("y", FieldType.INT), - FieldSpec("physical_w", FieldType.INT), - FieldSpec("physical_h", FieldType.INT), - FieldSpec("model_w", FieldType.INT), - FieldSpec("model_h", FieldType.INT), + CommandSpec( + "AC_resolve_pointer", "Data", "JSON Pointer: Resolve", + fields=( + FieldSpec("doc", FieldType.STRING, placeholder='{"a": {"b": [1, 2]}}'), + FieldSpec("pointer", FieldType.STRING, placeholder="/a/b/0"), + ), + description="Resolve an RFC 6901 JSON Pointer; returns {value}.", ), - description="Map a physical-pixel coordinate to a model grid.", - )) - specs.append(CommandSpec( - "AC_loop_guard_observe", "Agent", "Loop Guard: Observe Step", - fields=( - FieldSpec("tool", FieldType.STRING, placeholder="AC_click_mouse"), - FieldSpec("args", FieldType.STRING, optional=True, - placeholder='{"x": 10, "y": 20}'), - FieldSpec("result_digest", FieldType.STRING, optional=True), + CommandSpec( + "AC_apply_json_patch", "Data", "JSON Patch: Apply", + fields=( + FieldSpec("doc", FieldType.STRING, placeholder='{"a": 1}'), + FieldSpec("patch", FieldType.STRING, + placeholder='[{"op": "add", "path": "/b", "value": 2}]'), + ), + description="Apply an RFC 6902 JSON Patch; returns {result}.", ), - description="Detect repeat/ping-pong/no-op stuck-loop patterns.", - )) - specs.append(CommandSpec( - "AC_loop_guard_reset", "Agent", "Loop Guard: Reset", - fields=(), - description="Clear the default loop guard's history.", - )) - specs.append(CommandSpec( - "AC_mine_actions", "Report", "Mine Action Log", - fields=( - FieldSpec("actions", FieldType.STRING, - placeholder='[["AC_click_mouse", {}], ...]'), - FieldSpec("min_len", FieldType.INT, optional=True, default=2), - FieldSpec("max_len", FieldType.INT, optional=True, default=5), - FieldSpec("min_count", FieldType.INT, optional=True, default=3), + CommandSpec( + "AC_make_json_patch", "Data", "JSON Patch: Diff", + fields=( + FieldSpec("old", FieldType.STRING, placeholder='{"a": 1}'), + FieldSpec("new", FieldType.STRING, placeholder='{"a": 2}'), + ), + description="Compute an RFC 6902 patch from old to new; returns {patch}.", ), - description="Find repeated sequences + rank automation candidates.", - )) - specs.append(CommandSpec( - "AC_set_asset", "Data", "Asset: Set", - fields=( - FieldSpec("name", FieldType.STRING), - FieldSpec("value", FieldType.STRING), - FieldSpec("asset_type", FieldType.ENUM, optional=True, - default="text", - choices=("text", "int", "bool", "credential")), - FieldSpec("environment", FieldType.STRING, optional=True, - default="default"), - FieldSpec("db", FieldType.STRING, optional=True), - ), - description="Store a typed, environment-scoped asset.", - )) - specs.append(CommandSpec( - "AC_get_asset", "Data", "Asset: Get", - fields=( - FieldSpec("name", FieldType.STRING), - FieldSpec("environment", FieldType.STRING, optional=True, - default="default"), - FieldSpec("db", FieldType.STRING, optional=True), + CommandSpec( + "AC_merge_patch", "Data", "JSON Merge Patch: Apply", + fields=( + FieldSpec("doc", FieldType.STRING, placeholder='{"a": 1, "b": 2}'), + FieldSpec("patch", FieldType.STRING, + placeholder='{"b": null, "c": 3}'), + ), + description="Apply an RFC 7386 merge patch (null deletes); returns {result}.", ), - description="Read a typed asset (credential stays a reference).", - )) - specs.append(CommandSpec( - "AC_list_assets", "Data", "Asset: List", - fields=( - FieldSpec("environment", FieldType.STRING, optional=True), - FieldSpec("db", FieldType.STRING, optional=True), + CommandSpec( + "AC_scan_vulns", "Security", "Scan Dependencies for Vulnerabilities", + fields=( + FieldSpec("components", FieldType.STRING, + placeholder='{"components": [{"name": "foo", ' + '"version": "1.0", "purl": "pkg:pypi/foo@1.0"}]}'), + FieldSpec("advisories", FieldType.STRING, + placeholder='[{"id": "GHSA-...", "affected": [...]}]'), + FieldSpec("sarif_path", FieldType.STRING, optional=True, + placeholder="vulns.sarif"), + ), + description="Match SBOM components against an OSV advisory database.", ), - description="List assets (name/type/environment, no values).", - )) - specs.append(CommandSpec( - "AC_emit_event", "Tools", "Emit CloudEvent", - fields=( - FieldSpec("event_type", FieldType.STRING, - placeholder="com.example.run.finished"), - FieldSpec("data", FieldType.STRING, optional=True, - placeholder='{"run_id": "42"}'), - FieldSpec("source", FieldType.STRING, optional=True, - default="je_auto_control"), - FieldSpec("subject", FieldType.STRING, optional=True), - FieldSpec("url", FieldType.STRING, optional=True, - placeholder="https://hooks.example.com/ce"), - ), - description="Wrap data in a CloudEvents envelope; optionally POST it.", - )) - specs.append(CommandSpec( - "AC_notify_webhook", "Tools", "Notify: Webhook/Chat", - fields=( - FieldSpec("url", FieldType.STRING, - placeholder="https://hooks.example.com/..."), - FieldSpec("text", FieldType.STRING), - FieldSpec("transport", FieldType.ENUM, optional=True, - default="raw", - choices=("raw", "slack", "discord", "teams")), - FieldSpec("title", FieldType.STRING, optional=True), - ), - description="Send a Slack/Discord/Teams/raw webhook notification.", - )) - specs.append(CommandSpec( - "AC_json_query", "Data", "JSONPath: Query", - fields=( - FieldSpec("data", FieldType.STRING, - placeholder='{"a": [1, 2]}'), - FieldSpec("path", FieldType.STRING, placeholder="$.a[*]"), + CommandSpec( + "AC_apply_vex", "Security", "Apply VEX Triage to Findings", + fields=( + FieldSpec("findings", FieldType.STRING, + placeholder='[{"id": "GHSA-...", "package": "foo"}]'), + FieldSpec("vex", FieldType.STRING, + placeholder='{"statements": [{"vulnerability": ' + '{"name": "CVE-..."}, "status": "not_affected"}]}'), + ), + description="Suppress not_affected/fixed vulns via an OpenVEX document.", ), - description="Query parsed JSON with a JSONPath subset (all matches).", - )) - specs.append(CommandSpec( - "AC_json_extract", "Data", "JSONPath: Extract Mapping", - fields=( - FieldSpec("data", FieldType.STRING), - FieldSpec("mapping", FieldType.STRING, - placeholder='{"name": "$.user.name"}'), + CommandSpec( + "AC_check_licenses", "Security", "Check Dependency Licenses", + fields=( + FieldSpec("components", FieldType.STRING, + placeholder='{"components": [{"name": "x", ' + '"licenses": [{"license": {"name": "MIT"}}]}]}'), + FieldSpec("allow", FieldType.STRING, optional=True, + placeholder='["MIT", "Apache-2.0", "BSD-3-Clause"]'), + FieldSpec("deny", FieldType.STRING, optional=True, + placeholder='["GPL-3.0-only", "AGPL-3.0-only"]'), + ), + description="Evaluate SBOM licenses against allow/deny SPDX lists.", ), - description="Extract a {key: jsonpath} mapping into a flat object.", - )) - specs.append(CommandSpec( - "AC_validate_json", "Data", "JSON Schema: Validate", - fields=( - FieldSpec("data", FieldType.STRING, - placeholder='{"name": "Jo", "age": 30}'), - FieldSpec("schema", FieldType.STRING, - placeholder='{"type": "object", "required": ["name"]}'), + CommandSpec( + "AC_build_provenance", "Security", "Provenance: Build (SLSA)", + fields=( + FieldSpec("paths", FieldType.STRING, + placeholder='["dist/app.whl", "sbom.cdx.json"]'), + FieldSpec("builder_id", FieldType.STRING, optional=True, + placeholder="je_auto_control"), + ), + description="Build a SLSA in-toto provenance statement over files.", ), - description="Validate JSON against a JSON Schema; returns {ok, errors}.", - )) - specs.append(CommandSpec( - "AC_match_json", "Data", "JSON Contract: Match", - fields=( - FieldSpec("actual", FieldType.STRING, - placeholder='{"id": 1, "name": "Ada"}'), - FieldSpec("expected", FieldType.STRING, - placeholder='{"id": 1, "name": "Ada"}'), - FieldSpec("partial", FieldType.BOOL, optional=True, default=False), - FieldSpec("match_type", FieldType.BOOL, optional=True, - default=False), - ), - description="Match JSON against expected (partial/type); {ok, mismatches}.", - )) - specs.append(CommandSpec( - "AC_diff_json", "Data", "JSON Contract: Diff", - fields=( - FieldSpec("actual", FieldType.STRING, placeholder='[1, 2, 3]'), - FieldSpec("expected", FieldType.STRING, placeholder='[1, 2]'), + CommandSpec( + "AC_verify_provenance", "Security", "Provenance: Verify", + fields=( + FieldSpec("statement", FieldType.STRING, + placeholder='{"subject": [...], "predicate": {...}}'), + FieldSpec("files", FieldType.STRING, + placeholder='{"app.whl": "dist/app.whl"}'), + ), + description="Re-hash files against a provenance statement; {ok, mismatches}.", ), - description="Path-tagged diff between two JSON payloads; {diffs}.", - )) - specs.append(CommandSpec( - "AC_evaluate_flag", "Flow", "Feature Flag: Evaluate", - fields=( - FieldSpec("flags", FieldType.STRING, - placeholder='{"flags": {"f": {"variants": {...}}}}'), - FieldSpec("key", FieldType.STRING, placeholder="new-checkout"), - FieldSpec("context", FieldType.STRING, optional=True, - placeholder='{"targeting_key": "user1", "country": "US"}'), + CommandSpec( + "AC_jwt_encode", "Security", "JWT: Sign Token", + fields=( + FieldSpec("claims", FieldType.STRING, + placeholder='{"sub": "user1", "exp": 1893456000}'), + FieldSpec("key", FieldType.STRING, placeholder="shared secret"), + FieldSpec("alg", FieldType.STRING, optional=True, + placeholder="HS256", + choices=("HS256", "HS384", "HS512")), + ), + description="Sign a compact JWT (HMAC) from claims; returns {token}. This builder masks " + "the token -- read it with 'One-time values' after the run.", + sensitive_result=True, ), - description="Evaluate a feature flag; returns {value, variant, reason}.", - )) - specs.append(CommandSpec( - "AC_flag_enabled", "Flow", "Feature Flag: Enabled?", - fields=( - FieldSpec("flags", FieldType.STRING), - FieldSpec("key", FieldType.STRING, placeholder="new-checkout"), - FieldSpec("context", FieldType.STRING, optional=True), + CommandSpec( + "AC_jwt_decode", "Security", "JWT: Verify Token", + fields=( + FieldSpec("token", FieldType.STRING, placeholder="eyJhbGci..."), + FieldSpec("key", FieldType.STRING, placeholder="shared secret"), + FieldSpec("algorithms", FieldType.STRING, optional=True, + placeholder='["HS256"]'), + FieldSpec("audience", FieldType.STRING, optional=True), + ), + description="Verify a JWT (alg allowlist + exp/nbf/aud); returns {ok, claims}.", ), - description="Boolean feature-flag check; returns {enabled}.", - )) - specs.append(CommandSpec( - "AC_unified_diff", "Data", "Text: Unified Diff", - fields=( - FieldSpec("a", FieldType.STRING, placeholder="original text"), - FieldSpec("b", FieldType.STRING, placeholder="changed text"), + CommandSpec( + "AC_percentiles", "Report", "Percentiles", + fields=( + FieldSpec("samples", FieldType.STRING, + placeholder="[12.0, 9.5, 14.2, 11.1]"), + FieldSpec("qs", FieldType.STRING, optional=True, + placeholder="[50, 90, 99]"), + ), + description="Exact percentiles of a numeric sample list.", ), - description="Unified diff transforming a into b; returns {diff}.", - )) - specs.append(CommandSpec( - "AC_apply_unified", "Data", "Text: Apply Diff", - fields=( - FieldSpec("text", FieldType.STRING, placeholder="original text"), - FieldSpec("diff", FieldType.STRING, placeholder="@@ -1 +1 @@ ..."), + CommandSpec( + "AC_evaluate_slo", "Report", "SLO: Evaluate (SLI + Error Budget)", + fields=( + FieldSpec("records", FieldType.STRING, + placeholder='[{"timestamp": 1700000000, "ok": true}]'), + FieldSpec("target", FieldType.FLOAT, placeholder="0.99"), + FieldSpec("window_s", FieldType.FLOAT, optional=True), + ), + description="SLI + error budget for outcome records vs a target.", ), - description="Apply a unified diff to text; returns {result}.", - )) - specs.append(CommandSpec( - "AC_three_way_merge", "Data", "Text: Three-Way Merge", - fields=( - FieldSpec("base", FieldType.STRING, placeholder="base text"), - FieldSpec("ours", FieldType.STRING, placeholder="our text"), - FieldSpec("theirs", FieldType.STRING, placeholder="their text"), + CommandSpec( + "AC_burn_alerts", "Report", "SLO: Burn-Rate Alerts", + fields=( + FieldSpec("records", FieldType.STRING, + placeholder='[{"timestamp": 1700000000, "ok": false}]'), + FieldSpec("target", FieldType.FLOAT, placeholder="0.99"), + ), + description="Multi-window burn-rate alerts (Google SRE tiers).", ), - description="Merge ours/theirs against base; returns {text, clean, conflicts}.", - )) - specs.append(CommandSpec( - "AC_rrule_occurrences", "Flow", "Recurrence: Expand (RRULE)", - fields=( - FieldSpec("rule", FieldType.STRING, - placeholder="FREQ=MONTHLY;BYDAY=2TU"), - FieldSpec("dtstart", FieldType.STRING, - placeholder="2026-01-01T09:00:00"), - FieldSpec("count", FieldType.INT, optional=True, default=10), + CommandSpec( + "AC_run_chaos", "Flow", "Run Chaos Experiment", + fields=( + FieldSpec("spec", FieldType.STRING, + placeholder='{"title": "...", "probes": [{"name": "p", ' + '"action": [...]}], "method": [{"name": "f", ' + '"action": [...]}], "rollbacks": [[...]]}'), + ), + description="Verify steady state, inject faults, re-verify, roll back.", ), - description="Expand an RFC 5545 RRULE into ISO datetimes.", - )) - specs.append(CommandSpec( - "AC_rrule_next", "Flow", "Recurrence: Next Occurrence", - fields=( - FieldSpec("rule", FieldType.STRING, - placeholder="FREQ=WEEKLY;BYDAY=MO,WE,FR"), - FieldSpec("dtstart", FieldType.STRING, - placeholder="2026-01-01T09:00:00"), - FieldSpec("now", FieldType.STRING, optional=True, - placeholder="2026-03-15T00:00:00"), - ), - description="Next RRULE occurrence at/after now; returns {next}.", - )) - specs.append(CommandSpec( - "AC_describe_stats", "Data", "Describe Statistics", - fields=( - FieldSpec("values", FieldType.STRING, - placeholder="[12.0, 9.5, 14.2, 11.1]"), + CommandSpec( + "AC_run_saga", "Flow", "Run Saga (Compensating Rollback)", + fields=( + FieldSpec("steps", FieldType.STRING, + placeholder='[{"name": "s1", "action": [...], ' + '"compensation": [...]}]'), + ), + description="Run steps; on failure undo completed steps LIFO.", ), - description="Summary stats + percentiles of a numeric list.", - )) - specs.append(CommandSpec( - "AC_ab_significance", "Data", "A/B Significance (z-test)", - fields=( - FieldSpec("a_conv", FieldType.INT, placeholder="90"), - FieldSpec("a_n", FieldType.INT, placeholder="200"), - FieldSpec("b_conv", FieldType.INT, placeholder="110"), - FieldSpec("b_n", FieldType.INT, placeholder="200"), + CommandSpec( + "AC_decision_table", "Flow", "Decision Table (DMN)", + fields=( + FieldSpec("spec", FieldType.STRING, + placeholder='{"inputs": ["age"], "hit_policy": "FIRST", ' + '"rules": [...]}'), + FieldSpec("context", FieldType.STRING, + placeholder='{"age": 30}'), + ), + description="Evaluate inputs against a rule table (hit policy).", ), - description="Two-proportion z-test; returns {z, p_value, significant, ci}.", - )) - specs.append(CommandSpec( - "AC_search_documents", "Data", "Full-Text Search (BM25)", - fields=( - FieldSpec("docs", FieldType.STRING, - placeholder='{"d1": "quick brown fox", "d2": "lazy dog"}'), - FieldSpec("query", FieldType.STRING, placeholder="quick fox"), - FieldSpec("top_k", FieldType.INT, optional=True, default=10), - FieldSpec("mode", FieldType.STRING, optional=True, - placeholder="bm25", choices=("bm25", "tfidf")), - ), - description="Rank a {id: text} corpus for a query; returns {hits}.", - )) - specs.append(CommandSpec( - "AC_resolve_pointer", "Data", "JSON Pointer: Resolve", - fields=( - FieldSpec("doc", FieldType.STRING, placeholder='{"a": {"b": [1, 2]}}'), - FieldSpec("pointer", FieldType.STRING, placeholder="/a/b/0"), + CommandSpec( + "AC_repair_record", "Tools", "Locator Repair: Record", + fields=( + FieldSpec("key", FieldType.STRING), + FieldSpec("method", FieldType.STRING, placeholder="vlm/image"), + FieldSpec("coordinates", FieldType.STRING, optional=True, + placeholder=_POINT_PLACEHOLDER), + FieldSpec("description", FieldType.STRING, optional=True), + FieldSpec("confidence", FieldType.FLOAT, optional=True, + default=1.0), + FieldSpec("auto_threshold", FieldType.FLOAT, optional=True, + default=0.9), + FieldSpec("db", FieldType.STRING, optional=True), + ), + description="Persist a corrected locator from a heal (auto/queue).", ), - description="Resolve an RFC 6901 JSON Pointer; returns {value}.", - )) - specs.append(CommandSpec( - "AC_apply_json_patch", "Data", "JSON Patch: Apply", - fields=( - FieldSpec("doc", FieldType.STRING, placeholder='{"a": 1}'), - FieldSpec("patch", FieldType.STRING, - placeholder='[{"op": "add", "path": "/b", "value": 2}]'), + CommandSpec( + "AC_repair_resolved", "Tools", "Locator Repair: Resolved", + fields=( + FieldSpec("key", FieldType.STRING), + FieldSpec("db", FieldType.STRING, optional=True), + ), + description="Get the learned corrected locator for a key.", ), - description="Apply an RFC 6902 JSON Patch; returns {result}.", - )) - specs.append(CommandSpec( - "AC_make_json_patch", "Data", "JSON Patch: Diff", - fields=( - FieldSpec("old", FieldType.STRING, placeholder='{"a": 1}'), - FieldSpec("new", FieldType.STRING, placeholder='{"a": 2}'), + CommandSpec( + "AC_repair_pending", "Tools", "Locator Repair: Pending", + fields=(FieldSpec("db", FieldType.STRING, optional=True),), + description="List locator-repair suggestions awaiting review.", ), - description="Compute an RFC 6902 patch from old to new; returns {patch}.", - )) - specs.append(CommandSpec( - "AC_merge_patch", "Data", "JSON Merge Patch: Apply", - fields=( - FieldSpec("doc", FieldType.STRING, placeholder='{"a": 1, "b": 2}'), - FieldSpec("patch", FieldType.STRING, - placeholder='{"b": null, "c": 3}'), + CommandSpec( + "AC_repair_approve", "Tools", "Locator Repair: Approve", + fields=( + FieldSpec("suggestion_id", FieldType.STRING), + FieldSpec("db", FieldType.STRING, optional=True), + ), + description="Approve a pending locator-repair suggestion.", ), - description="Apply an RFC 7386 merge patch (null deletes); returns {result}.", - )) - specs.append(CommandSpec( - "AC_scan_vulns", "Security", "Scan Dependencies for Vulnerabilities", - fields=( - FieldSpec("components", FieldType.STRING, - placeholder='{"components": [{"name": "foo", ' - '"version": "1.0", "purl": "pkg:pypi/foo@1.0"}]}'), - FieldSpec("advisories", FieldType.STRING, - placeholder='[{"id": "GHSA-...", "affected": [...]}]'), - FieldSpec("sarif_path", FieldType.STRING, optional=True, - placeholder="vulns.sarif"), - ), - description="Match SBOM components against an OSV advisory database.", - )) - specs.append(CommandSpec( - "AC_apply_vex", "Security", "Apply VEX Triage to Findings", - fields=( - FieldSpec("findings", FieldType.STRING, - placeholder='[{"id": "GHSA-...", "package": "foo"}]'), - FieldSpec("vex", FieldType.STRING, - placeholder='{"statements": [{"vulnerability": ' - '{"name": "CVE-..."}, "status": "not_affected"}]}'), + CommandSpec( + "AC_detect_pii", "Data", "PII: Detect", + fields=( + FieldSpec("text", FieldType.STRING), + FieldSpec("kinds", FieldType.STRING, optional=True, + placeholder='["email", "phone"]'), + ), + description="Detect PII spans (email/phone/ssn/card/ip/iban) in text.", ), - description="Suppress not_affected/fixed vulns via an OpenVEX document.", - )) - specs.append(CommandSpec( - "AC_check_licenses", "Security", "Check Dependency Licenses", - fields=( - FieldSpec("components", FieldType.STRING, - placeholder='{"components": [{"name": "x", ' - '"licenses": [{"license": {"name": "MIT"}}]}]}'), - FieldSpec("allow", FieldType.STRING, optional=True, - placeholder='["MIT", "Apache-2.0", "BSD-3-Clause"]'), - FieldSpec("deny", FieldType.STRING, optional=True, - placeholder='["GPL-3.0-only", "AGPL-3.0-only"]'), - ), - description="Evaluate SBOM licenses against allow/deny SPDX lists.", - )) - specs.append(CommandSpec( - "AC_build_provenance", "Security", "Provenance: Build (SLSA)", - fields=( - FieldSpec("paths", FieldType.STRING, - placeholder='["dist/app.whl", "sbom.cdx.json"]'), - FieldSpec("builder_id", FieldType.STRING, optional=True, - placeholder="je_auto_control"), + CommandSpec( + "AC_redact_pii", "Data", "PII: Redact", + fields=( + FieldSpec("text", FieldType.STRING), + FieldSpec("kinds", FieldType.STRING, optional=True, + placeholder='["email"]'), + FieldSpec("mode", FieldType.ENUM, optional=True, default="label", + choices=("label", "mask", "partial", "hash")), + FieldSpec("mask_char", FieldType.STRING, optional=True, + default="*"), + ), + description="Redact PII in text (label/mask/partial/hash).", ), - description="Build a SLSA in-toto provenance statement over files.", - )) - specs.append(CommandSpec( - "AC_verify_provenance", "Security", "Provenance: Verify", - fields=( - FieldSpec("statement", FieldType.STRING, - placeholder='{"subject": [...], "predicate": {...}}'), - FieldSpec("files", FieldType.STRING, - placeholder='{"app.whl": "dist/app.whl"}'), + CommandSpec( + "AC_export_sarif", "Report", "Export SARIF (Code Scanning)", + fields=( + FieldSpec("findings", FieldType.STRING, + placeholder='[{"rule_id": "AC1", "message": "...", ' + '"level": "error"}]'), + FieldSpec("path", FieldType.STRING, optional=True, + placeholder="results.sarif"), + FieldSpec("tool_name", FieldType.STRING, optional=True, + default="AutoControl"), + ), + description="Unify findings into a SARIF 2.1.0 document.", ), - description="Re-hash files against a provenance statement; {ok, mismatches}.", - )) - specs.append(CommandSpec( - "AC_jwt_encode", "Security", "JWT: Sign Token", - fields=( - FieldSpec("claims", FieldType.STRING, - placeholder='{"sub": "user1", "exp": 1893456000}'), - FieldSpec("key", FieldType.STRING, placeholder="shared secret"), - FieldSpec("alg", FieldType.STRING, optional=True, - placeholder="HS256", - choices=("HS256", "HS384", "HS512")), - ), - description="Sign a compact JWT (HMAC) from claims; returns {token}. This builder masks " - "the token -- read it with 'One-time values' after the run.", - sensitive_result=True, - )) - specs.append(CommandSpec( - "AC_jwt_decode", "Security", "JWT: Verify Token", - fields=( - FieldSpec("token", FieldType.STRING, placeholder="eyJhbGci..."), - FieldSpec("key", FieldType.STRING, placeholder="shared secret"), - FieldSpec("algorithms", FieldType.STRING, optional=True, - placeholder='["HS256"]'), - FieldSpec("audience", FieldType.STRING, optional=True), + CommandSpec( + "AC_generate_sop", "Report", "Generate SOP Document", + fields=( + FieldSpec("title", FieldType.STRING, optional=True, + default="Automation Procedure"), + FieldSpec("path", FieldType.FILE_PATH, optional=True), + ), + description="Build a step-by-step SOP (HTML) from 'actions' (JSON " + "view).", + ), + ]) + + +def _add_audit_specs(specs: List[CommandSpec]) -> None: + specs.extend([ + CommandSpec( + "AC_heal_stats", "Testing", "Self-Heal Analytics", + fields=(FieldSpec("limit", FieldType.INT, optional=True, + default=200),), + description="Aggregate the self-heal log (heal rate, brittle " + "locators).", ), - description="Verify a JWT (alg allowlist + exp/nbf/aud); returns {ok, claims}.", - )) - specs.append(CommandSpec( - "AC_percentiles", "Report", "Percentiles", - fields=( - FieldSpec("samples", FieldType.STRING, - placeholder="[12.0, 9.5, 14.2, 11.1]"), - FieldSpec("qs", FieldType.STRING, optional=True, - placeholder="[50, 90, 99]"), + CommandSpec( + "AC_self_heal_evaluate", "Testing", "Self-Heal: Evaluate Dataset", + fields=(FieldSpec("dataset_path", FieldType.FILE_PATH, + placeholder="dataset.json"),), + description="Score locator versions on the same labelled frames " + "(accuracy, false positives, recovery, p50/p95, model calls). " + "A 'vlm' version in the dataset calls the VLM backend.", ), - description="Exact percentiles of a numeric sample list.", - )) - specs.append(CommandSpec( - "AC_evaluate_slo", "Report", "SLO: Evaluate (SLI + Error Budget)", - fields=( - FieldSpec("records", FieldType.STRING, - placeholder='[{"timestamp": 1700000000, "ok": true}]'), - FieldSpec("target", FieldType.FLOAT, placeholder="0.99"), - FieldSpec("window_s", FieldType.FLOAT, optional=True), + CommandSpec( + "AC_self_heal_revision_propose", "Testing", "Self-Heal: Propose Template Revision", + fields=( + FieldSpec("template_path", FieldType.FILE_PATH), + FieldSpec("candidate_path", FieldType.FILE_PATH), + FieldSpec("source", FieldType.STRING, optional=True, default="manual"), + FieldSpec("note", FieldType.STRING, optional=True), + ), + description="Store a candidate template; the live template is not changed.", ), - description="SLI + error budget for outcome records vs a target.", - )) - specs.append(CommandSpec( - "AC_burn_alerts", "Report", "SLO: Burn-Rate Alerts", - fields=( - FieldSpec("records", FieldType.STRING, - placeholder='[{"timestamp": 1700000000, "ok": false}]'), - FieldSpec("target", FieldType.FLOAT, placeholder="0.99"), + CommandSpec( + "AC_self_heal_revision_preview", "Testing", "Self-Heal: Preview Template Revision", + fields=( + FieldSpec("revision_id", FieldType.STRING), + FieldSpec("dataset_path", FieldType.FILE_PATH, optional=True, + placeholder="dataset.json"), + FieldSpec("detect_threshold", FieldType.FLOAT, optional=True, + default=0.9, min_value=0.0, max_value=1.0), + ), + description="Compare candidate and current template; a dataset validates it.", ), - description="Multi-window burn-rate alerts (Google SRE tiers).", - )) - specs.append(CommandSpec( - "AC_run_chaos", "Flow", "Run Chaos Experiment", - fields=( - FieldSpec("spec", FieldType.STRING, - placeholder='{"title": "...", "probes": [{"name": "p", ' - '"action": [...]}], "method": [{"name": "f", ' - '"action": [...]}], "rollbacks": [[...]]}'), + CommandSpec( + "AC_self_heal_revision_accept", "Testing", "Self-Heal: Accept Template Revision", + fields=( + FieldSpec("revision_id", FieldType.STRING), + FieldSpec("allow_unvalidated", FieldType.BOOL, optional=True, default=False), + ), + description="Replace the template with a validated candidate (backup kept).", ), - description="Verify steady state, inject faults, re-verify, roll back.", - )) - specs.append(CommandSpec( - "AC_run_saga", "Flow", "Run Saga (Compensating Rollback)", - fields=( - FieldSpec("steps", FieldType.STRING, - placeholder='[{"name": "s1", "action": [...], ' - '"compensation": [...]}]'), + CommandSpec( + "AC_self_heal_revision_revert", "Testing", "Self-Heal: Revert Template Revision", + fields=(FieldSpec("revision_id", FieldType.STRING),), + description="Restore the template an accepted revision replaced.", ), - description="Run steps; on failure undo completed steps LIFO.", - )) - specs.append(CommandSpec( - "AC_decision_table", "Flow", "Decision Table (DMN)", - fields=( - FieldSpec("spec", FieldType.STRING, - placeholder='{"inputs": ["age"], "hit_policy": "FIRST", ' - '"rules": [...]}'), - FieldSpec("context", FieldType.STRING, - placeholder='{"age": 30}'), + CommandSpec( + "AC_self_heal_revision_list", "Testing", "Self-Heal: List Template Revisions", + description="List stored template revisions and their status.", ), - description="Evaluate inputs against a rule table (hit policy).", - )) - specs.append(CommandSpec( - "AC_repair_record", "Tools", "Locator Repair: Record", - fields=( - FieldSpec("key", FieldType.STRING), - FieldSpec("method", FieldType.STRING, placeholder="vlm/image"), - FieldSpec("coordinates", FieldType.STRING, optional=True, - placeholder=_POINT_PLACEHOLDER), - FieldSpec("description", FieldType.STRING, optional=True), - FieldSpec("confidence", FieldType.FLOAT, optional=True, - default=1.0), - FieldSpec("auto_threshold", FieldType.FLOAT, optional=True, - default=0.9), - FieldSpec("db", FieldType.STRING, optional=True), - ), - description="Persist a corrected locator from a heal (auto/queue).", - )) - specs.append(CommandSpec( - "AC_repair_resolved", "Tools", "Locator Repair: Resolved", - fields=( - FieldSpec("key", FieldType.STRING), - FieldSpec("db", FieldType.STRING, optional=True), + CommandSpec( + "AC_journal_start", "Testing", "Start Action Journal", + fields=( + FieldSpec("path", FieldType.FILE_PATH, optional=True, + placeholder=_JOURNAL_PLACEHOLDER), + FieldSpec("run_id", FieldType.STRING, optional=True), + FieldSpec("session", FieldType.STRING, optional=True), + ), + description="Append every executed action to a JSON-lines journal " + "(secrets masked before they are written).", ), - description="Get the learned corrected locator for a key.", - )) - specs.append(CommandSpec( - "AC_repair_pending", "Tools", "Locator Repair: Pending", - fields=(FieldSpec("db", FieldType.STRING, optional=True),), - description="List locator-repair suggestions awaiting review.", - )) - specs.append(CommandSpec( - "AC_repair_approve", "Tools", "Locator Repair: Approve", - fields=( - FieldSpec("suggestion_id", FieldType.STRING), - FieldSpec("db", FieldType.STRING, optional=True), + CommandSpec( + "AC_journal_stop", "Testing", "Stop Action Journal", + description="Stop the action journal.", ), - description="Approve a pending locator-repair suggestion.", - )) - specs.append(CommandSpec( - "AC_detect_pii", "Data", "PII: Detect", - fields=( - FieldSpec("text", FieldType.STRING), - FieldSpec("kinds", FieldType.STRING, optional=True, - placeholder='["email", "phone"]'), + CommandSpec( + "AC_journal_status", "Testing", "Action Journal Status", + description="Report whether an action journal is started.", ), - description="Detect PII spans (email/phone/ssn/card/ip/iban) in text.", - )) - specs.append(CommandSpec( - "AC_redact_pii", "Data", "PII: Redact", - fields=( - FieldSpec("text", FieldType.STRING), - FieldSpec("kinds", FieldType.STRING, optional=True, - placeholder='["email"]'), - FieldSpec("mode", FieldType.ENUM, optional=True, default="label", - choices=("label", "mask", "partial", "hash")), - FieldSpec("mask_char", FieldType.STRING, optional=True, - default="*"), - ), - description="Redact PII in text (label/mask/partial/hash).", - )) - specs.append(CommandSpec( - "AC_export_sarif", "Report", "Export SARIF (Code Scanning)", - fields=( - FieldSpec("findings", FieldType.STRING, - placeholder='[{"rule_id": "AC1", "message": "...", ' - '"level": "error"}]'), - FieldSpec("path", FieldType.STRING, optional=True, - placeholder="results.sarif"), - FieldSpec("tool_name", FieldType.STRING, optional=True, - default="AutoControl"), - ), - description="Unify findings into a SARIF 2.1.0 document.", - )) - specs.append(CommandSpec( - "AC_generate_sop", "Report", "Generate SOP Document", - fields=( - FieldSpec("title", FieldType.STRING, optional=True, - default="Automation Procedure"), - FieldSpec("path", FieldType.FILE_PATH, optional=True), + CommandSpec( + "AC_journal_read", "Testing", "Read Action Journal", + fields=( + FieldSpec("path", FieldType.FILE_PATH, + placeholder=_JOURNAL_PLACEHOLDER), + FieldSpec("run_id", FieldType.STRING, optional=True), + FieldSpec("limit", FieldType.INT, optional=True, default=0), + ), + description="Read a journal's events (the last 'limit' when positive).", ), - description="Build a step-by-step SOP (HTML) from 'actions' (JSON " - "view).", - )) - - -def _add_audit_specs(specs: List[CommandSpec]) -> None: - specs.append(CommandSpec( - "AC_heal_stats", "Testing", "Self-Heal Analytics", - fields=(FieldSpec("limit", FieldType.INT, optional=True, - default=200),), - description="Aggregate the self-heal log (heal rate, brittle " - "locators).", - )) - specs.append(CommandSpec( - "AC_self_heal_evaluate", "Testing", "Self-Heal: Evaluate Dataset", - fields=(FieldSpec("dataset_path", FieldType.FILE_PATH, - placeholder="dataset.json"),), - description="Score locator versions on the same labelled frames " - "(accuracy, false positives, recovery, p50/p95, model calls). " - "A 'vlm' version in the dataset calls the VLM backend.", - )) - specs.append(CommandSpec( - "AC_self_heal_revision_propose", "Testing", "Self-Heal: Propose Template Revision", - fields=( - FieldSpec("template_path", FieldType.FILE_PATH), - FieldSpec("candidate_path", FieldType.FILE_PATH), - FieldSpec("source", FieldType.STRING, optional=True, default="manual"), - FieldSpec("note", FieldType.STRING, optional=True), + CommandSpec( + "AC_journal_runs", "Testing", "List Journal Runs", + fields=(FieldSpec("path", FieldType.FILE_PATH, + placeholder=_JOURNAL_PLACEHOLDER),), + description="Summarise each run a journal file holds.", ), - description="Store a candidate template; the live template is not changed.", - )) - specs.append(CommandSpec( - "AC_self_heal_revision_preview", "Testing", "Self-Heal: Preview Template Revision", - fields=( - FieldSpec("revision_id", FieldType.STRING), - FieldSpec("dataset_path", FieldType.FILE_PATH, optional=True, - placeholder="dataset.json"), - FieldSpec("detect_threshold", FieldType.FLOAT, optional=True, - default=0.9, min_value=0.0, max_value=1.0), + CommandSpec( + "AC_generate_code_from_journal", "Testing", "Candidate Script from Journal", + fields=( + FieldSpec("path", FieldType.FILE_PATH, + placeholder=_JOURNAL_PLACEHOLDER), + FieldSpec("run_id", FieldType.STRING, optional=True), + FieldSpec("target", FieldType.ENUM, optional=True, default="pytest", + choices=("pytest", "python", "robot")), + FieldSpec("style", FieldType.ENUM, optional=True, default="actions", + choices=("actions", "calls")), + FieldSpec("output", FieldType.FILE_PATH, optional=True), + FieldSpec("diff_against", FieldType.FILE_PATH, optional=True), + ), + description="Build a reviewable candidate script from one journal " + "run; nothing from the log is executed. diff_against " + "adds a diff against the file the candidate would replace.", ), - description="Compare candidate and current template; a dataset validates it.", - )) - specs.append(CommandSpec( - "AC_self_heal_revision_accept", "Testing", "Self-Heal: Accept Template Revision", - fields=( - FieldSpec("revision_id", FieldType.STRING), - FieldSpec("allow_unvalidated", FieldType.BOOL, optional=True, default=False), + CommandSpec( + "AC_failure_signature", "Testing", "Failure Signature", + fields=( + FieldSpec("error", FieldType.STRING, + placeholder="Timeout at C:\\app.py line 42 (0x7ff..)"), + FieldSpec("length", FieldType.INT, optional=True, default=12), + ), + description="Normalise + hash an error to a stable failure signature.", ), - description="Replace the template with a validated candidate (backup kept).", - )) - specs.append(CommandSpec( - "AC_self_heal_revision_revert", "Testing", "Self-Heal: Revert Template Revision", - fields=(FieldSpec("revision_id", FieldType.STRING),), - description="Restore the template an accepted revision replaced.", - )) - specs.append(CommandSpec( - "AC_self_heal_revision_list", "Testing", "Self-Heal: List Template Revisions", - description="List stored template revisions and their status.", - )) - specs.append(CommandSpec( - "AC_journal_start", "Testing", "Start Action Journal", - fields=( - FieldSpec("path", FieldType.FILE_PATH, optional=True, - placeholder="action_journal.jsonl"), - FieldSpec("run_id", FieldType.STRING, optional=True), - FieldSpec("session", FieldType.STRING, optional=True), + CommandSpec( + "AC_group_failures", "Testing", "Group Failures by Signature", + fields=(FieldSpec("errors", FieldType.STRING, + placeholder='["err one", "err two"]'),), + description="Group error messages by failure signature (most frequent).", ), - description="Append every executed action to a JSON-lines journal " - "(secrets masked before they are written).", - )) - specs.append(CommandSpec( - "AC_journal_stop", "Testing", "Stop Action Journal", - description="Stop the action journal.", - )) - specs.append(CommandSpec( - "AC_journal_status", "Testing", "Action Journal Status", - description="Report whether an action journal is started.", - )) - specs.append(CommandSpec( - "AC_journal_read", "Testing", "Read Action Journal", - fields=( - FieldSpec("path", FieldType.FILE_PATH, - placeholder="action_journal.jsonl"), - FieldSpec("run_id", FieldType.STRING, optional=True), - FieldSpec("limit", FieldType.INT, optional=True, default=0), + CommandSpec( + "AC_diff_runs", "Testing", "Diff Two Run Traces", + fields=( + FieldSpec("before", FieldType.STRING, + placeholder='[{"name": "login", "status": "ok"}]'), + FieldSpec("after", FieldType.STRING, + placeholder='[{"name": "login", "status": "error"}]'), + FieldSpec("key", FieldType.STRING, optional=True, default="name"), + FieldSpec("regress_factor", FieldType.FLOAT, optional=True, + default=1.5), + ), + description="LCS-align two run step-traces: added/removed/flips/regress.", ), - description="Read a journal's events (the last 'limit' when positive).", - )) - specs.append(CommandSpec( - "AC_journal_runs", "Testing", "List Journal Runs", - fields=(FieldSpec("path", FieldType.FILE_PATH, - placeholder="action_journal.jsonl"),), - description="Summarise each run a journal file holds.", - )) - specs.append(CommandSpec( - "AC_generate_code_from_journal", "Testing", "Candidate Script from Journal", - fields=( - FieldSpec("path", FieldType.FILE_PATH, - placeholder="action_journal.jsonl"), - FieldSpec("run_id", FieldType.STRING, optional=True), - FieldSpec("target", FieldType.ENUM, optional=True, default="pytest", - choices=("pytest", "python", "robot")), - FieldSpec("style", FieldType.ENUM, optional=True, default="actions", - choices=("actions", "calls")), - FieldSpec("output", FieldType.FILE_PATH, optional=True), - FieldSpec("diff_against", FieldType.FILE_PATH, optional=True), - ), - description="Build a reviewable candidate script from one journal " - "run; nothing from the log is executed. diff_against " - "adds a diff against the file the candidate would replace.", - )) - specs.append(CommandSpec( - "AC_failure_signature", "Testing", "Failure Signature", - fields=( - FieldSpec("error", FieldType.STRING, - placeholder="Timeout at C:\\app.py line 42 (0x7ff..)"), - FieldSpec("length", FieldType.INT, optional=True, default=12), + CommandSpec( + "AC_failure_clusters", "Testing", "Cluster Co-Failing Tests", + fields=( + FieldSpec("runs", FieldType.STRING, + placeholder='[["test_a", "test_b"], ["test_a", "test_b"]]'), + FieldSpec("threshold", FieldType.FLOAT, optional=True, default=0.5), + FieldSpec("min_size", FieldType.INT, optional=True, default=2), + ), + description="Cluster tests that flake together (co-failure Jaccard).", + ), + CommandSpec( + "AC_cofailure_pairs", "Testing", "Co-Failing Test Pairs", + fields=( + FieldSpec("runs", FieldType.STRING, + placeholder='[["test_a", "test_b"]]'), + FieldSpec("threshold", FieldType.FLOAT, optional=True, default=0.5), + ), + description="Test pairs that fail together above a Jaccard threshold.", ), - description="Normalise + hash an error to a stable failure signature.", - )) - specs.append(CommandSpec( - "AC_group_failures", "Testing", "Group Failures by Signature", - fields=(FieldSpec("errors", FieldType.STRING, - placeholder='["err one", "err two"]'),), - description="Group error messages by failure signature (most frequent).", - )) - specs.append(CommandSpec( - "AC_diff_runs", "Testing", "Diff Two Run Traces", - fields=( - FieldSpec("before", FieldType.STRING, - placeholder='[{"name": "login", "status": "ok"}]'), - FieldSpec("after", FieldType.STRING, - placeholder='[{"name": "login", "status": "error"}]'), - FieldSpec("key", FieldType.STRING, optional=True, default="name"), - FieldSpec("regress_factor", FieldType.FLOAT, optional=True, - default=1.5), - ), - description="LCS-align two run step-traces: added/removed/flips/regress.", - )) - specs.append(CommandSpec( - "AC_failure_clusters", "Testing", "Cluster Co-Failing Tests", - fields=( - FieldSpec("runs", FieldType.STRING, - placeholder='[["test_a", "test_b"], ["test_a", "test_b"]]'), - FieldSpec("threshold", FieldType.FLOAT, optional=True, default=0.5), - FieldSpec("min_size", FieldType.INT, optional=True, default=2), + CommandSpec( + "AC_build_timeline", "Testing", "Step Timeline (waterfall)", + fields=(FieldSpec("steps", FieldType.STRING, + placeholder='[{"name": "login", "duration": 1.2}]'),), + description="Per-run step waterfall: offsets, durations, bottleneck.", ), - description="Cluster tests that flake together (co-failure Jaccard).", - )) - specs.append(CommandSpec( - "AC_cofailure_pairs", "Testing", "Co-Failing Test Pairs", - fields=( - FieldSpec("runs", FieldType.STRING, - placeholder='[["test_a", "test_b"]]'), - FieldSpec("threshold", FieldType.FLOAT, optional=True, default=0.5), + CommandSpec( + "AC_critical_steps", "Testing", "Critical (Bottleneck) Steps", + fields=( + FieldSpec("steps", FieldType.STRING, + placeholder='[{"name": "login", "duration": 1.2}]'), + FieldSpec("top", FieldType.INT, optional=True, default=3), + ), + description="The steps that dominate a run's time, longest first.", ), - description="Test pairs that fail together above a Jaccard threshold.", - )) - specs.append(CommandSpec( - "AC_build_timeline", "Testing", "Step Timeline (waterfall)", - fields=(FieldSpec("steps", FieldType.STRING, - placeholder='[{"name": "login", "duration": 1.2}]'),), - description="Per-run step waterfall: offsets, durations, bottleneck.", - )) - specs.append(CommandSpec( - "AC_critical_steps", "Testing", "Critical (Bottleneck) Steps", - fields=( - FieldSpec("steps", FieldType.STRING, - placeholder='[{"name": "login", "duration": 1.2}]'), - FieldSpec("top", FieldType.INT, optional=True, default=3), + CommandSpec( + "AC_scan_secrets", "Tools", "Scan for Hardcoded Secrets", + description="Scan 'data' (JSON view) for hardcoded secrets that " + "should use ${secrets.*}.", ), - description="The steps that dominate a run's time, longest first.", - )) - specs.append(CommandSpec( - "AC_scan_secrets", "Tools", "Scan for Hardcoded Secrets", - description="Scan 'data' (JSON view) for hardcoded secrets that " - "should use ${secrets.*}.", - )) + ]) def _add_devex_specs(specs: List[CommandSpec]) -> None: - specs.append(CommandSpec( - "AC_ci_annotations", "Tools", "Emit CI Annotations", - description="Emit GitHub Actions annotations from 'annotations' " - "(JSON view).", - )) - specs.append(CommandSpec( - "AC_clip_history_capture", "Misc", "Clipboard History: Capture")) - specs.append(CommandSpec( - "AC_clip_history_list", "Misc", "Clipboard History: List")) - specs.append(CommandSpec( - "AC_clip_history_search", "Misc", "Clipboard History: Search", - fields=(FieldSpec("query", FieldType.STRING),))) - specs.append(CommandSpec( - "AC_clip_history_start", "Misc", "Clipboard History: Start")) - specs.append(CommandSpec( - "AC_clip_history_stop", "Misc", "Clipboard History: Stop")) + specs.extend([ + CommandSpec( + "AC_ci_annotations", "Tools", "Emit CI Annotations", + description="Emit GitHub Actions annotations from 'annotations' " + "(JSON view).", + ), + CommandSpec( + "AC_clip_history_capture", "Misc", "Clipboard History: Capture"), + CommandSpec( + "AC_clip_history_list", "Misc", "Clipboard History: List"), + CommandSpec( + "AC_clip_history_search", "Misc", "Clipboard History: Search", + fields=(FieldSpec("query", FieldType.STRING),)), + CommandSpec( + "AC_clip_history_start", "Misc", "Clipboard History: Start"), + CommandSpec( + "AC_clip_history_stop", "Misc", "Clipboard History: Stop"), + ]) def _add_resilience_specs(specs: List[CommandSpec]) -> None: - specs.append(CommandSpec( - "AC_circuit_call", "Flow", "Circuit Breaker Call", - fields=( - FieldSpec("name", FieldType.STRING), - FieldSpec("threshold", FieldType.INT, optional=True, default=5), - FieldSpec("reset_s", FieldType.FLOAT, optional=True, default=30.0), + specs.extend([ + CommandSpec( + "AC_circuit_call", "Flow", "Circuit Breaker Call", + fields=( + FieldSpec("name", FieldType.STRING), + FieldSpec("threshold", FieldType.INT, optional=True, default=5), + FieldSpec("reset_s", FieldType.FLOAT, optional=True, default=30.0), + ), + description="Run 'actions' (JSON view) via a named circuit breaker.", ), - description="Run 'actions' (JSON view) via a named circuit breaker.", - )) - specs.append(CommandSpec( - "AC_bulkhead_run", "Flow", "Bulkhead (Bounded Concurrency)", - fields=( - FieldSpec("name", FieldType.STRING), - FieldSpec("max_concurrent", FieldType.INT, default=4), - FieldSpec("actions", FieldType.STRING, - placeholder="[[\"AC_click_mouse\", {...}]]"), + CommandSpec( + "AC_bulkhead_run", "Flow", "Bulkhead (Bounded Concurrency)", + fields=( + FieldSpec("name", FieldType.STRING), + FieldSpec("max_concurrent", FieldType.INT, default=4), + FieldSpec("actions", FieldType.STRING, + placeholder="[[\"AC_click_mouse\", {...}]]"), + ), + description="Run 'actions' (JSON view) under a named bulkhead permit.", ), - description="Run 'actions' (JSON view) under a named bulkhead permit.", - )) - specs.append(CommandSpec( - "AC_retry_after", "Flow", "Parse Retry-After / RateLimit", - fields=( - FieldSpec("response", FieldType.STRING, - placeholder='{"status": 429, "headers": {"Retry-After": "30"}}'), + CommandSpec( + "AC_retry_after", "Flow", "Parse Retry-After / RateLimit", + fields=( + FieldSpec("response", FieldType.STRING, + placeholder='{"status": 429, "headers": {"Retry-After": "30"}}'), + ), + description="Server-advised wait (seconds) from an HTTP response; {delay}.", ), - description="Server-advised wait (seconds) from an HTTP response; {delay}.", - )) - specs.append(CommandSpec( - "AC_http_replay", "Data", "HTTP Cassette: Replay", - fields=( - FieldSpec("cassette", FieldType.STRING, - placeholder='{"interactions": [{"request": {...}, ' - '"response": {...}}]}'), - FieldSpec("url", FieldType.STRING, - placeholder="https://api.example.com/users/1"), - FieldSpec("method", FieldType.STRING, optional=True, - placeholder="GET"), - ), - description="Replay a recorded HTTP response from a cassette (no network).", - )) - specs.append(CommandSpec( - "AC_trace_inject", "Data", "Trace Context: Inject", - fields=( - FieldSpec("headers", FieldType.STRING, optional=True, - placeholder='{"accept": "application/json"}'), - FieldSpec("traceparent", FieldType.STRING, optional=True, - placeholder="00-<32 hex>-<16 hex>-01 (parent; omit for root)"), + CommandSpec( + "AC_http_replay", "Data", "HTTP Cassette: Replay", + fields=( + FieldSpec("cassette", FieldType.STRING, + placeholder='{"interactions": [{"request": {...}, ' + '"response": {...}}]}'), + FieldSpec("url", FieldType.STRING, + placeholder="https://api.example.com/users/1"), + FieldSpec("method", FieldType.STRING, optional=True, + placeholder="GET"), + ), + description="Replay a recorded HTTP response from a cassette (no network).", ), - description="Set a W3C traceparent on outgoing headers (root or child).", - )) - specs.append(CommandSpec( - "AC_trace_extract", "Data", "Trace Context: Extract", - fields=( - FieldSpec("headers", FieldType.STRING, - placeholder='{"traceparent": "00-...-...-01"}'), + CommandSpec( + "AC_trace_inject", "Data", "Trace Context: Inject", + fields=( + FieldSpec("headers", FieldType.STRING, optional=True, + placeholder='{"accept": "application/json"}'), + FieldSpec("traceparent", FieldType.STRING, optional=True, + placeholder="00-<32 hex>-<16 hex>-01 (parent; omit for root)"), + ), + description="Set a W3C traceparent on outgoing headers (root or child).", ), - description="Extract the W3C trace context from request headers.", - )) - specs.append(CommandSpec( - "AC_baggage_parse", "Data", "Baggage: Parse Header", - fields=( - FieldSpec("header", FieldType.STRING, - placeholder="tenant=acme,run=42"), + CommandSpec( + "AC_trace_extract", "Data", "Trace Context: Extract", + fields=( + FieldSpec("headers", FieldType.STRING, + placeholder='{"traceparent": "00-...-...-01"}'), + ), + description="Extract the W3C trace context from request headers.", ), - description="Parse a W3C baggage header into key-value items.", - )) - specs.append(CommandSpec( - "AC_baggage_format", "Data", "Baggage: Format Header", - fields=( - FieldSpec("items", FieldType.STRING, - placeholder='{"tenant": "acme", "run": "42"}'), + CommandSpec( + "AC_baggage_parse", "Data", "Baggage: Parse Header", + fields=( + FieldSpec("header", FieldType.STRING, + placeholder="tenant=acme,run=42"), + ), + description="Parse a W3C baggage header into key-value items.", ), - description="Serialise items into a percent-encoded baggage header.", - )) - specs.append(CommandSpec( - "AC_canonical_log", "Report", "Canonical Log: Build Line", - fields=( - FieldSpec("fields", FieldType.STRING, - placeholder='{"event": "run", "ok": true, "ms": 42}'), + CommandSpec( + "AC_baggage_format", "Data", "Baggage: Format Header", + fields=( + FieldSpec("items", FieldType.STRING, + placeholder='{"tenant": "acme", "run": "42"}'), + ), + description="Serialise items into a percent-encoded baggage header.", ), - description="Build a canonical wide-event log line (rendered as JSON).", - )) - specs.append(CommandSpec( - "AC_normalize_text", "Data", "Text: Normalize (Unicode)", - fields=( - FieldSpec("text", FieldType.STRING, placeholder="Café Menu"), - FieldSpec("form", FieldType.STRING, optional=True, - placeholder="NFKC"), - FieldSpec("casefold", FieldType.BOOL, optional=True, default=True), - FieldSpec("collapse_ws", FieldType.BOOL, optional=True, - default=True), - ), - description="Unicode-normalise text (form + casefold + ws fold).", - )) - specs.append(CommandSpec( - "AC_slugify", "Data", "Text: Slugify", - fields=( - FieldSpec("text", FieldType.STRING, placeholder="Café Menu!"), - FieldSpec("sep", FieldType.STRING, optional=True, placeholder="-"), + CommandSpec( + "AC_canonical_log", "Report", "Canonical Log: Build Line", + fields=( + FieldSpec("fields", FieldType.STRING, + placeholder='{"event": "run", "ok": true, "ms": 42}'), + ), + description="Build a canonical wide-event log line (rendered as JSON).", ), - description="Produce an ASCII slug (de-accent, lowercase, join).", - )) - specs.append(CommandSpec( - "AC_text_similarity", "Data", "Text: Similarity", - fields=( - FieldSpec("a", FieldType.STRING, placeholder="login"), - FieldSpec("b", FieldType.STRING, placeholder="lgoin"), - FieldSpec("metric", FieldType.STRING, optional=True, - placeholder="jaro_winkler | levenshtein | jaccard | dice"), + CommandSpec( + "AC_normalize_text", "Data", "Text: Normalize (Unicode)", + fields=( + FieldSpec("text", FieldType.STRING, placeholder="Café Menu"), + FieldSpec("form", FieldType.STRING, optional=True, + placeholder="NFKC"), + FieldSpec("casefold", FieldType.BOOL, optional=True, default=True), + FieldSpec("collapse_ws", FieldType.BOOL, optional=True, + default=True), + ), + description="Unicode-normalise text (form + casefold + ws fold).", ), - description="Normalised string similarity (Jaro-Winkler / edit / Jaccard).", - )) - specs.append(CommandSpec( - "AC_simhash", "Data", "Near-Dup: SimHash", - fields=( - FieldSpec("text", FieldType.STRING, placeholder="some text"), - FieldSpec("bits", FieldType.INT, optional=True, default=64), + CommandSpec( + "AC_slugify", "Data", "Text: Slugify", + fields=( + FieldSpec("text", FieldType.STRING, placeholder="Café Menu!"), + FieldSpec("sep", FieldType.STRING, optional=True, placeholder="-"), + ), + description="Produce an ASCII slug (de-accent, lowercase, join).", ), - description="SimHash fingerprint (int) of text.", - )) - specs.append(CommandSpec( - "AC_near_duplicates", "Data", "Near-Dup: Cluster Texts", - fields=( - FieldSpec("texts", FieldType.STRING, - placeholder='["the cat sat", "the cat sat down", "dog"]'), - FieldSpec("max_distance", FieldType.INT, optional=True, default=3), + CommandSpec( + "AC_text_similarity", "Data", "Text: Similarity", + fields=( + FieldSpec("a", FieldType.STRING, placeholder="login"), + FieldSpec("b", FieldType.STRING, placeholder="lgoin"), + FieldSpec("metric", FieldType.STRING, optional=True, + placeholder="jaro_winkler | levenshtein | jaccard | dice"), + ), + description="Normalised string similarity (Jaro-Winkler / edit / Jaccard).", ), - description="Cluster near-duplicate texts by SimHash distance.", - )) - specs.append(CommandSpec( - "AC_spans_to_otlp", "Report", "OTLP: Export Spans", - fields=( - FieldSpec("spans", FieldType.STRING, - placeholder='[{"trace_id": "...", "span_id": "...", ' - '"name": "step", "start_unix_nano": 1, ' - '"end_unix_nano": 2}]'), - FieldSpec("resource_attrs", FieldType.STRING, optional=True, - placeholder='{"service.name": "autocontrol"}'), - ), - description="Wrap spans in an OTLP/JSON resourceSpans envelope.", - )) - specs.append(CommandSpec( - "AC_resolve_ref", "Security", "Secret Ref: Resolve", - fields=( - FieldSpec("ref", FieldType.STRING, - placeholder="env://TOKEN | file://./token | secret://api-key"), + CommandSpec( + "AC_simhash", "Data", "Near-Dup: SimHash", + fields=( + FieldSpec("text", FieldType.STRING, placeholder="some text"), + FieldSpec("bits", FieldType.INT, optional=True, default=64), + ), + description="SimHash fingerprint (int) of text.", ), - description="Resolve an env:// / file:// / secret:// value reference.", - )) - specs.append(CommandSpec( - "AC_resolve_refs", "Security", "Secret Ref: Resolve In Structure", - fields=( - FieldSpec("obj", FieldType.STRING, - placeholder='{"token": "env://TOKEN", "url": "..."}'), + CommandSpec( + "AC_near_duplicates", "Data", "Near-Dup: Cluster Texts", + fields=( + FieldSpec("texts", FieldType.STRING, + placeholder='["the cat sat", "the cat sat down", "dog"]'), + FieldSpec("max_distance", FieldType.INT, optional=True, default=3), + ), + description="Cluster near-duplicate texts by SimHash distance.", ), - description="Recursively resolve references inside a JSON structure.", - )) - specs.append(CommandSpec( - "AC_validate_config", "Data", "Config Schema: Validate", - fields=( - FieldSpec("schema", FieldType.STRING, - placeholder='{"port": {"type": "int", "required": true}}'), - FieldSpec("config", FieldType.STRING, - placeholder='{"port": "8080"}'), + CommandSpec( + "AC_spans_to_otlp", "Report", "OTLP: Export Spans", + fields=( + FieldSpec("spans", FieldType.STRING, + placeholder='[{"trace_id": "...", "span_id": "...", ' + '"name": "step", "start_unix_nano": 1, ' + '"end_unix_nano": 2}]'), + FieldSpec("resource_attrs", FieldType.STRING, optional=True, + placeholder='{"service.name": "autocontrol"}'), + ), + description="Wrap spans in an OTLP/JSON resourceSpans envelope.", ), - description="Validate a config mapping against a typed schema spec.", - )) - specs.append(CommandSpec( - "AC_redact_config", "Security", "Redaction: Redact Config", - fields=( - FieldSpec("obj", FieldType.STRING, - placeholder='{"db": {"password": "hunter2longvalue"}}'), - FieldSpec("mask", FieldType.STRING, optional=True, default="***"), + CommandSpec( + "AC_resolve_ref", "Security", "Secret Ref: Resolve", + fields=( + FieldSpec("ref", FieldType.STRING, + placeholder="env://TOKEN | file://./token | secret://api-key"), + ), + description="Resolve an env:// / file:// / secret:// value reference.", ), - description="Mask secret-looking values in a JSON config structure.", - )) - specs.append(CommandSpec( - "AC_redact_secret_text", "Security", "Redaction: Redact Secret Text", - fields=( - FieldSpec("text", FieldType.STRING, - placeholder="log line with AKIA... or a bearer token"), - FieldSpec("mask", FieldType.STRING, optional=True, default="***"), + CommandSpec( + "AC_resolve_refs", "Security", "Secret Ref: Resolve In Structure", + fields=( + FieldSpec("obj", FieldType.STRING, + placeholder='{"token": "env://TOKEN", "url": "..."}'), + ), + description="Recursively resolve references inside a JSON structure.", ), - description="Mask secret-looking tokens within a free-text string.", - )) - specs.append(CommandSpec( - "AC_profile_rows", "Data", "Data Profile: Profile Rows", - fields=( - FieldSpec("rows", FieldType.STRING, - placeholder='[{"id": 1, "name": "a"}, {"id": 2}]'), - FieldSpec("columns", FieldType.STRING, optional=True, - placeholder='["id", "name"] (omit for all)'), + CommandSpec( + "AC_validate_config", "Data", "Config Schema: Validate", + fields=( + FieldSpec("schema", FieldType.STRING, + placeholder='{"port": {"type": "int", "required": true}}'), + FieldSpec("config", FieldType.STRING, + placeholder='{"port": "8080"}'), + ), + description="Validate a config mapping against a typed schema spec.", ), - description="Per-column stats: null fraction, cardinality, type, ranges.", - )) - specs.append(CommandSpec( - "AC_infer_schema", "Data", "Data Profile: Infer Schema", - fields=( - FieldSpec("rows", FieldType.STRING, - placeholder='[{"id": 1, "name": "a"}, {"id": 2}]'), - FieldSpec("columns", FieldType.STRING, optional=True, - placeholder='["id", "name"] (omit for all)'), + CommandSpec( + "AC_redact_config", "Security", "Redaction: Redact Config", + fields=( + FieldSpec("obj", FieldType.STRING, + placeholder='{"db": {"password": "hunter2longvalue"}}'), + FieldSpec("mask", FieldType.STRING, optional=True, default="***"), + ), + description="Mask secret-looking values in a JSON config structure.", ), - description="Infer a validate_rows-compatible schema from observed rows.", - )) - specs.append(CommandSpec( - "AC_parse_problem", "Data", "HTTP Problem (RFC 9457): Parse", - fields=( - FieldSpec("response", FieldType.STRING, - placeholder='{"status": 400, "headers": ' - '{"Content-Type": "application/problem+json"}, ' - '"json": {"title": "Bad Request"}}'), + CommandSpec( + "AC_redact_secret_text", "Security", "Redaction: Redact Secret Text", + fields=( + FieldSpec("text", FieldType.STRING, + placeholder="log line with AKIA... or a bearer token"), + FieldSpec("mask", FieldType.STRING, optional=True, default="***"), + ), + description="Mask secret-looking tokens within a free-text string.", ), - description="Parse an application/problem+json error response.", - )) - specs.append(CommandSpec( - "AC_parse_dotenv", "Data", "Dotenv: Parse Text", - fields=( - FieldSpec("text", FieldType.STRING, - placeholder='KEY=value\nexport TOKEN="abc" # comment'), + CommandSpec( + "AC_profile_rows", "Data", "Data Profile: Profile Rows", + fields=( + FieldSpec("rows", FieldType.STRING, + placeholder='[{"id": 1, "name": "a"}, {"id": 2}]'), + FieldSpec("columns", FieldType.STRING, optional=True, + placeholder='["id", "name"] (omit for all)'), + ), + description="Per-column stats: null fraction, cardinality, type, ranges.", ), - description="Parse .env text (KEY=VALUE, quotes, escapes) into values.", - )) - specs.append(CommandSpec( - "AC_load_dotenv", "Data", "Dotenv: Load File", - fields=( - FieldSpec("path", FieldType.STRING, placeholder=".env"), - FieldSpec("override", FieldType.BOOL, optional=True, default=False), + CommandSpec( + "AC_infer_schema", "Data", "Data Profile: Infer Schema", + fields=( + FieldSpec("rows", FieldType.STRING, + placeholder='[{"id": 1, "name": "a"}, {"id": 2}]'), + FieldSpec("columns", FieldType.STRING, optional=True, + placeholder='["id", "name"] (omit for all)'), + ), + description="Infer a validate_rows-compatible schema from observed rows.", ), - description="Load a .env file into a values dict.", - )) - specs.append(CommandSpec( - "AC_parse_sse", "Data", "SSE: Parse Event Stream", - fields=( - FieldSpec("text", FieldType.STRING, - placeholder='event: ping\ndata: {"x": 1}\n\n'), + CommandSpec( + "AC_parse_problem", "Data", "HTTP Problem (RFC 9457): Parse", + fields=( + FieldSpec("response", FieldType.STRING, + placeholder='{"status": 400, "headers": ' + '{"Content-Type": "application/problem+json"}, ' + '"json": {"title": "Bad Request"}}'), + ), + description="Parse an application/problem+json error response.", ), - description="Parse a text/event-stream blob into events.", - )) - specs.append(CommandSpec( - "AC_parse_link_header", "Data", "Link Header: Parse", - fields=( - FieldSpec("value", FieldType.STRING, - placeholder='; rel="next"'), + CommandSpec( + "AC_parse_dotenv", "Data", "Dotenv: Parse Text", + fields=( + FieldSpec("text", FieldType.STRING, + placeholder='KEY=value\nexport TOKEN="abc" # comment'), + ), + description="Parse .env text (KEY=VALUE, quotes, escapes) into values.", ), - description="Parse an RFC 8288 Link header into links.", - )) - specs.append(CommandSpec( - "AC_next_url", "Data", "Link Header: Next URL", - fields=( - FieldSpec("value", FieldType.STRING, - placeholder='; rel="next"'), + CommandSpec( + "AC_load_dotenv", "Data", "Dotenv: Load File", + fields=( + FieldSpec("path", FieldType.STRING, placeholder=".env"), + FieldSpec("override", FieldType.BOOL, optional=True, default=False), + ), + description="Load a .env file into a values dict.", ), - description="Return the rel=next URL from a Link header.", - )) - specs.append(CommandSpec( - "AC_build_multipart", "Data", "Multipart: Build Body", - fields=( - FieldSpec("fields", FieldType.STRING, optional=True, - placeholder='{"name": "report", "tag": "v1"}'), - FieldSpec("files", FieldType.STRING, optional=True, - placeholder='[{"name": "f", "filename": "a.txt", ' - '"content": "hi"}]'), - FieldSpec("boundary", FieldType.STRING, optional=True), - ), - description="Build a multipart/form-data body (base64) for upload.", - )) - specs.append(CommandSpec( - "AC_parse_multipart", "Data", "Multipart: Parse Body", - fields=( - FieldSpec("content_type", FieldType.STRING, - placeholder="multipart/form-data; boundary=..."), - FieldSpec("body_base64", FieldType.STRING, - placeholder=""), + CommandSpec( + "AC_parse_sse", "Data", "SSE: Parse Event Stream", + fields=( + FieldSpec("text", FieldType.STRING, + placeholder='event: ping\ndata: {"x": 1}\n\n'), + ), + description="Parse a text/event-stream blob into events.", ), - description="Parse a base64 multipart body into fields and files.", - )) - specs.append(CommandSpec( - "AC_decode_body", "Data", "HTTP Content: Decode Body", - fields=( - FieldSpec("headers", FieldType.STRING, - placeholder='{"Content-Encoding": "gzip"}'), - FieldSpec("body_base64", FieldType.STRING, - placeholder=""), + CommandSpec( + "AC_parse_link_header", "Data", "Link Header: Parse", + fields=( + FieldSpec("value", FieldType.STRING, + placeholder='; rel="next"'), + ), + description="Parse an RFC 8288 Link header into links.", ), - description="Decode a gzip/deflate response body by Content-Encoding.", - )) - specs.append(CommandSpec( - "AC_parse_quality_values", "Data", "HTTP Content: Quality Values", - fields=( - FieldSpec("header", FieldType.STRING, - placeholder="text/html;q=0.8, application/json"), + CommandSpec( + "AC_next_url", "Data", "Link Header: Next URL", + fields=( + FieldSpec("value", FieldType.STRING, + placeholder='; rel="next"'), + ), + description="Return the rel=next URL from a Link header.", ), - description="Parse an Accept/Accept-Encoding header by q-value.", - )) - specs.append(CommandSpec( - "AC_cookie_header", "Data", "Cookie Jar: Build Cookie Header", - fields=( - FieldSpec("set_cookies", FieldType.STRING, - placeholder='sid=abc; Path=/ (or ["a=1", "b=2"])'), + CommandSpec( + "AC_build_multipart", "Data", "Multipart: Build Body", + fields=( + FieldSpec("fields", FieldType.STRING, optional=True, + placeholder='{"name": "report", "tag": "v1"}'), + FieldSpec("files", FieldType.STRING, optional=True, + placeholder='[{"name": "f", "filename": "a.txt", ' + '"content": "hi"}]'), + FieldSpec("boundary", FieldType.STRING, optional=True), + ), + description="Build a multipart/form-data body (base64) for upload.", ), - description="Build a Cookie request header from Set-Cookie value(s).", - )) - specs.append(CommandSpec( - "AC_parse_set_cookie", "Data", "Cookie Jar: Parse Set-Cookie", - fields=( - FieldSpec("header", FieldType.STRING, - placeholder="sid=abc; Path=/; Max-Age=3600"), + CommandSpec( + "AC_parse_multipart", "Data", "Multipart: Parse Body", + fields=( + FieldSpec("content_type", FieldType.STRING, + placeholder="multipart/form-data; boundary=..."), + FieldSpec("body_base64", FieldType.STRING, + placeholder=""), + ), + description="Parse a base64 multipart body into fields and files.", ), - description="Parse one Set-Cookie header into name/value/attributes.", - )) - specs.append(CommandSpec( - "AC_parse_cache_control", "Data", "HTTP Conditional: Cache-Control", - fields=( - FieldSpec("headers", FieldType.STRING, - placeholder='{"Cache-Control": "max-age=60, public"}'), + CommandSpec( + "AC_decode_body", "Data", "HTTP Content: Decode Body", + fields=( + FieldSpec("headers", FieldType.STRING, + placeholder='{"Content-Encoding": "gzip"}'), + FieldSpec("body_base64", FieldType.STRING, + placeholder=""), + ), + description="Decode a gzip/deflate response body by Content-Encoding.", ), - description="Parse a Cache-Control header into directives.", - )) - specs.append(CommandSpec( - "AC_store_validators", "Data", "HTTP Conditional: Store Validators", - fields=( - FieldSpec("response", FieldType.STRING, - placeholder='{"headers": {"ETag": "\\"abc\\""}}'), + CommandSpec( + "AC_parse_quality_values", "Data", "HTTP Content: Quality Values", + fields=( + FieldSpec("header", FieldType.STRING, + placeholder="text/html;q=0.8, application/json"), + ), + description="Parse an Accept/Accept-Encoding header by q-value.", ), - description="Extract ETag / Last-Modified / Cache-Control validators.", - )) - specs.append(CommandSpec( - "AC_resolve_config", "Data", "Layered Config: Resolve", - fields=( - FieldSpec("layers", FieldType.STRING, - placeholder='[{"name": "defaults", "mapping": {}}, ' - '{"name": "env", "mapping": {}, ' - '"priority": 10}]'), + CommandSpec( + "AC_cookie_header", "Data", "Cookie Jar: Build Cookie Header", + fields=( + FieldSpec("set_cookies", FieldType.STRING, + placeholder='sid=abc; Path=/ (or ["a=1", "b=2"])'), + ), + description="Build a Cookie request header from Set-Cookie value(s).", ), - description="Deep-merge ordered config layers into one config.", - )) - specs.append(CommandSpec( - "AC_explain_config", "Data", "Layered Config: Explain Key", - fields=( - FieldSpec("layers", FieldType.STRING, - placeholder='[{"name": "defaults", "mapping": {}}]'), - FieldSpec("key", FieldType.STRING, placeholder=_DOTTED_KEY_PLACEHOLDER), + CommandSpec( + "AC_parse_set_cookie", "Data", "Cookie Jar: Parse Set-Cookie", + fields=( + FieldSpec("header", FieldType.STRING, + placeholder="sid=abc; Path=/; Max-Age=3600"), + ), + description="Parse one Set-Cookie header into name/value/attributes.", ), - description="Show the value and winning layer for a dotted config key.", - )) + CommandSpec( + "AC_parse_cache_control", "Data", "HTTP Conditional: Cache-Control", + fields=( + FieldSpec("headers", FieldType.STRING, + placeholder='{"Cache-Control": "max-age=60, public"}'), + ), + description="Parse a Cache-Control header into directives.", + ), + CommandSpec( + "AC_store_validators", "Data", "HTTP Conditional: Store Validators", + fields=( + FieldSpec("response", FieldType.STRING, + placeholder='{"headers": {"ETag": "\\"abc\\""}}'), + ), + description="Extract ETag / Last-Modified / Cache-Control validators.", + ), + CommandSpec( + "AC_resolve_config", "Data", "Layered Config: Resolve", + fields=( + FieldSpec("layers", FieldType.STRING, + placeholder='[{"name": "defaults", "mapping": {}}, ' + '{"name": "env", "mapping": {}, ' + '"priority": 10}]'), + ), + description="Deep-merge ordered config layers into one config.", + ), + CommandSpec( + "AC_explain_config", "Data", "Layered Config: Explain Key", + fields=( + FieldSpec("layers", FieldType.STRING, + placeholder='[{"name": "defaults", "mapping": {}}]'), + FieldSpec("key", FieldType.STRING, placeholder=_DOTTED_KEY_PLACEHOLDER), + ), + description="Show the value and winning layer for a dotted config key.", + ), + ]) sync_target = ( FieldSpec("server_url", FieldType.STRING, placeholder="https://sync.example"), FieldSpec("user_id", FieldType.STRING, placeholder="alice"), @@ -3593,409 +3628,411 @@ def _add_resilience_specs(specs: List[CommandSpec]) -> None: FieldSpec("assets_dir", FieldType.FILE_PATH, optional=True), FieldSpec("assets_server", FieldType.BOOL, optional=True, default=False), ) - specs.append(CommandSpec( - "AC_config_sync_run", "Data", "Config Sync: Sync Now", - fields=sync_target + sync_options + ( - FieldSpec("force", FieldType.BOOL, optional=True, default=False), - ), - description="Sync settings with the server once. Received hotkeys and " - "triggers arrive disabled; nothing is run. 'force' skips the " - "retry delay left by an earlier failure (state backing_off). " - "Without 'sections' this syncs hotkeys, triggers and the address " - "book, plus scripts / locators when their path is given; set " - "sections (e.g. scripts) to sync only those.", - )) - specs.append(CommandSpec( - "AC_config_sync_status", "Data", "Config Sync: Status", - fields=sync_target, - description="Recorded sync state (revision, pending, conflicts); no network.", - )) - specs.append(CommandSpec( - "AC_config_sync_resolve", "Data", "Config Sync: Resolve Conflict", - fields=sync_target + ( - FieldSpec("section", FieldType.STRING, placeholder="hotkeys"), - FieldSpec("key", FieldType.STRING), - FieldSpec("choice", FieldType.INT, default=0), - ) + sync_options, - description="Keep one candidate of a conflicted entry (0-based choice).", - )) - specs.append(CommandSpec( - "AC_config_sync_full_resync", "Data", "Config Sync: Full Resync", - fields=sync_target + sync_options, - description="Adopt the server's state after this device was retired; " - "pending local changes are discarded.", - )) - specs.append(CommandSpec( - "AC_config_sync_collect_blobs", "Data", "Config Sync: Collect Unused Blobs", - fields=sync_target + ( - FieldSpec("keep", FieldType.STRING, optional=True, - placeholder="sha256,sha256"), - FieldSpec("min_age_s", FieldType.FLOAT, optional=True, default=86400.0), - FieldSpec("dry_run", FieldType.BOOL, optional=True, default=False), - ) + sync_options, - description="Delete the account's asset blobs on the sync server that no " - "entry names any more. Blobs newer than min_age_s and the " - "digests in 'keep' stay; dry_run only lists what would go.", - )) - specs.append(CommandSpec( - "AC_detect_drift", "Data", "Data Drift: Detect (PSI + KS)", - fields=( - FieldSpec("reference", FieldType.STRING, - placeholder="[1.0, 2.0, 3.0, ...]"), - FieldSpec("current", FieldType.STRING, - placeholder="[1.1, 2.2, 9.9, ...]"), - FieldSpec("threshold", FieldType.FLOAT, optional=True, - default=0.25), - FieldSpec("bins", FieldType.INT, optional=True, default=10), - ), - description="Numeric drift: Population Stability Index + KS two-sample.", - )) - specs.append(CommandSpec( - "AC_categorical_drift", "Data", "Data Drift: Categorical", - fields=( - FieldSpec("reference", FieldType.STRING, - placeholder='["a", "b", "a", ...]'), - FieldSpec("current", FieldType.STRING, - placeholder='["a", "c", "c", ...]'), + specs.extend([ + CommandSpec( + "AC_config_sync_run", "Data", "Config Sync: Sync Now", + fields=sync_target + sync_options + ( + FieldSpec("force", FieldType.BOOL, optional=True, default=False), + ), + description="Sync settings with the server once. Received hotkeys and " + "triggers arrive disabled; nothing is run. 'force' skips the " + "retry delay left by an earlier failure (state backing_off). " + "Without 'sections' this syncs hotkeys, triggers and the address " + "book, plus scripts / locators when their path is given; set " + "sections (e.g. scripts) to sync only those.", ), - description="Categorical drift: chi-square + total-variation distance.", - )) - specs.append(CommandSpec( - "AC_check_compatibility", "Data", "Schema Compat: Check", - fields=( - FieldSpec("old", FieldType.STRING, - placeholder='{"properties": {"id": {"type": "integer"}}}'), - FieldSpec("new", FieldType.STRING, - placeholder='{"properties": {"id": {"type": "integer"}}, ' - '"required": ["id"]}'), - FieldSpec("mode", FieldType.STRING, optional=True, - placeholder="backward | forward | full"), - ), - description="Classify JSON-Schema changes as backward/forward/full.", - )) - specs.append(CommandSpec( - "AC_ts_rate", "Data", "Time-Series: Counter Rate", - fields=( - FieldSpec("series", FieldType.STRING, - placeholder="[[0, 0], [10, 50], [20, 120]]"), - FieldSpec("window_s", FieldType.FLOAT, optional=True), + CommandSpec( + "AC_config_sync_status", "Data", "Config Sync: Status", + fields=sync_target, + description="Recorded sync state (revision, pending, conflicts); no network.", ), - description="Per-second counter rate (reset-aware) over a series.", - )) - specs.append(CommandSpec( - "AC_ts_downsample", "Data", "Time-Series: Downsample", - fields=( - FieldSpec("series", FieldType.STRING, - placeholder="[[0, 1], [5, 3], [12, 9]]"), - FieldSpec("bucket_s", FieldType.FLOAT, placeholder="10"), - FieldSpec("agg", FieldType.STRING, optional=True, - placeholder="avg|sum|min|max|first|last|count"), + CommandSpec( + "AC_config_sync_resolve", "Data", "Config Sync: Resolve Conflict", + fields=sync_target + ( + FieldSpec("section", FieldType.STRING, placeholder="hotkeys"), + FieldSpec("key", FieldType.STRING), + FieldSpec("choice", FieldType.INT, default=0), + ) + sync_options, + description="Keep one candidate of a conflicted entry (0-based choice).", ), - description="Roll a series into tumbling buckets by aggregate.", - )) - specs.append(CommandSpec( - "AC_detect_anomalies", "Data", "Anomaly: Detect in Series", - fields=( - FieldSpec("values", FieldType.STRING, - placeholder="[10, 11, 9, 10, 95, 10]"), - FieldSpec("method", FieldType.STRING, optional=True, - placeholder="mad | zscore"), - FieldSpec("threshold", FieldType.FLOAT, optional=True), + CommandSpec( + "AC_config_sync_full_resync", "Data", "Config Sync: Full Resync", + fields=sync_target + sync_options, + description="Adopt the server's state after this device was retired; " + "pending local changes are discarded.", ), - description="Flag outliers in a numeric series (MAD / z-score).", - )) - specs.append(CommandSpec( - "AC_sma", "Data", "Smoothing: Simple Moving Average", - fields=( - FieldSpec("values", FieldType.STRING, placeholder="[1, 2, 3, 4, 5]"), - FieldSpec("window", FieldType.INT, placeholder="3"), + CommandSpec( + "AC_config_sync_collect_blobs", "Data", "Config Sync: Collect Unused Blobs", + fields=sync_target + ( + FieldSpec("keep", FieldType.STRING, optional=True, + placeholder="sha256,sha256"), + FieldSpec("min_age_s", FieldType.FLOAT, optional=True, default=86400.0), + FieldSpec("dry_run", FieldType.BOOL, optional=True, default=False), + ) + sync_options, + description="Delete the account's asset blobs on the sync server that no " + "entry names any more. Blobs newer than min_age_s and the " + "digests in 'keep' stay; dry_run only lists what would go.", ), - description="Trailing simple moving average over a window.", - )) - specs.append(CommandSpec( - "AC_ewma", "Data", "Smoothing: EWMA", - fields=( - FieldSpec("values", FieldType.STRING, placeholder="[1, 2, 3, 4, 5]"), - FieldSpec("alpha", FieldType.FLOAT, optional=True, default=0.3), + CommandSpec( + "AC_detect_drift", "Data", "Data Drift: Detect (PSI + KS)", + fields=( + FieldSpec("reference", FieldType.STRING, + placeholder="[1.0, 2.0, 3.0, ...]"), + FieldSpec("current", FieldType.STRING, + placeholder="[1.1, 2.2, 9.9, ...]"), + FieldSpec("threshold", FieldType.FLOAT, optional=True, + default=0.25), + FieldSpec("bins", FieldType.INT, optional=True, default=10), + ), + description="Numeric drift: Population Stability Index + KS two-sample.", ), - description="Exponentially-weighted moving average of a series.", - )) - specs.append(CommandSpec( - "AC_idempotency_begin", "Flow", "Idempotency: Begin", - fields=( - FieldSpec("name", FieldType.STRING, placeholder="payments"), - FieldSpec("key", FieldType.STRING, placeholder="order-42"), - FieldSpec("request", FieldType.STRING, optional=True, - placeholder='{"amount": 100}'), + CommandSpec( + "AC_categorical_drift", "Data", "Data Drift: Categorical", + fields=( + FieldSpec("reference", FieldType.STRING, + placeholder='["a", "b", "a", ...]'), + FieldSpec("current", FieldType.STRING, + placeholder='["a", "c", "c", ...]'), + ), + description="Categorical drift: chi-square + total-variation distance.", ), - description="Register/look up an idempotency key (new/in_progress/done).", - )) - specs.append(CommandSpec( - "AC_idempotency_complete", "Flow", "Idempotency: Complete", - fields=( - FieldSpec("name", FieldType.STRING, placeholder="payments"), - FieldSpec("key", FieldType.STRING, placeholder="order-42"), - FieldSpec("response", FieldType.STRING, placeholder='{"ok": true}'), + CommandSpec( + "AC_check_compatibility", "Data", "Schema Compat: Check", + fields=( + FieldSpec("old", FieldType.STRING, + placeholder='{"properties": {"id": {"type": "integer"}}}'), + FieldSpec("new", FieldType.STRING, + placeholder='{"properties": {"id": {"type": "integer"}}, ' + '"required": ["id"]}'), + FieldSpec("mode", FieldType.STRING, optional=True, + placeholder="backward | forward | full"), + ), + description="Classify JSON-Schema changes as backward/forward/full.", + ), + CommandSpec( + "AC_ts_rate", "Data", "Time-Series: Counter Rate", + fields=( + FieldSpec("series", FieldType.STRING, + placeholder="[[0, 0], [10, 50], [20, 120]]"), + FieldSpec("window_s", FieldType.FLOAT, optional=True), + ), + description="Per-second counter rate (reset-aware) over a series.", + ), + CommandSpec( + "AC_ts_downsample", "Data", "Time-Series: Downsample", + fields=( + FieldSpec("series", FieldType.STRING, + placeholder="[[0, 1], [5, 3], [12, 9]]"), + FieldSpec("bucket_s", FieldType.FLOAT, placeholder="10"), + FieldSpec("agg", FieldType.STRING, optional=True, + placeholder="avg|sum|min|max|first|last|count"), + ), + description="Roll a series into tumbling buckets by aggregate.", + ), + CommandSpec( + "AC_detect_anomalies", "Data", "Anomaly: Detect in Series", + fields=( + FieldSpec("values", FieldType.STRING, + placeholder="[10, 11, 9, 10, 95, 10]"), + FieldSpec("method", FieldType.STRING, optional=True, + placeholder="mad | zscore"), + FieldSpec("threshold", FieldType.FLOAT, optional=True), + ), + description="Flag outliers in a numeric series (MAD / z-score).", + ), + CommandSpec( + "AC_sma", "Data", "Smoothing: Simple Moving Average", + fields=( + FieldSpec("values", FieldType.STRING, placeholder="[1, 2, 3, 4, 5]"), + FieldSpec("window", FieldType.INT, placeholder="3"), + ), + description="Trailing simple moving average over a window.", + ), + CommandSpec( + "AC_ewma", "Data", "Smoothing: EWMA", + fields=( + FieldSpec("values", FieldType.STRING, placeholder="[1, 2, 3, 4, 5]"), + FieldSpec("alpha", FieldType.FLOAT, optional=True, default=0.3), + ), + description="Exponentially-weighted moving average of a series.", + ), + CommandSpec( + "AC_idempotency_begin", "Flow", "Idempotency: Begin", + fields=( + FieldSpec("name", FieldType.STRING, placeholder="payments"), + FieldSpec("key", FieldType.STRING, placeholder="order-42"), + FieldSpec("request", FieldType.STRING, optional=True, + placeholder='{"amount": 100}'), + ), + description="Register/look up an idempotency key (new/in_progress/done).", + ), + CommandSpec( + "AC_idempotency_complete", "Flow", "Idempotency: Complete", + fields=( + FieldSpec("name", FieldType.STRING, placeholder="payments"), + FieldSpec("key", FieldType.STRING, placeholder="order-42"), + FieldSpec("response", FieldType.STRING, placeholder='{"ok": true}'), + ), + description="Store the completed response for an idempotency key.", + ), + CommandSpec( + "AC_idempotency_release", "Flow", "Idempotency: Release", + fields=( + FieldSpec("name", FieldType.STRING, placeholder="payments"), + FieldSpec("key", FieldType.STRING, placeholder="order-42"), + ), + description="Drop an in-progress idempotency key whose work failed.", + ), + CommandSpec( + "AC_dedup_check", "Flow", "Dedup Window: Check", + fields=( + FieldSpec("name", FieldType.STRING, placeholder="webhooks"), + FieldSpec("message_id", FieldType.STRING, placeholder="evt-123"), + FieldSpec("ttl_s", FieldType.FLOAT, optional=True, default=3600), + ), + description="Check-and-mark a message id; first_seen false on duplicate.", ), - description="Store the completed response for an idempotency key.", - )) - specs.append(CommandSpec( - "AC_idempotency_release", "Flow", "Idempotency: Release", - fields=( - FieldSpec("name", FieldType.STRING, placeholder="payments"), - FieldSpec("key", FieldType.STRING, placeholder="order-42"), + CommandSpec( + "AC_sequence_observe", "Flow", "Sequence: Observe", + fields=( + FieldSpec("name", FieldType.STRING, placeholder="ingest"), + FieldSpec("stream_id", FieldType.STRING, placeholder="orders"), + FieldSpec("seq", FieldType.INT, placeholder="14"), + ), + description="Classify a sequence number (ok/duplicate/gap/reorder).", ), - description="Drop an in-progress idempotency key whose work failed.", - )) - specs.append(CommandSpec( - "AC_dedup_check", "Flow", "Dedup Window: Check", - fields=( - FieldSpec("name", FieldType.STRING, placeholder="webhooks"), - FieldSpec("message_id", FieldType.STRING, placeholder="evt-123"), - FieldSpec("ttl_s", FieldType.FLOAT, optional=True, default=3600), + CommandSpec( + "AC_cas_put", "Flow", "Optimistic: Put (CAS)", + fields=( + FieldSpec("name", FieldType.STRING, placeholder="config"), + FieldSpec("key", FieldType.STRING, placeholder=_DOTTED_KEY_PLACEHOLDER), + FieldSpec("value", FieldType.STRING, placeholder='"prod-1"'), + FieldSpec("expected_version", FieldType.INT, optional=True), + ), + description="Put only if expected_version matches (returns new version).", ), - description="Check-and-mark a message id; first_seen false on duplicate.", - )) - specs.append(CommandSpec( - "AC_sequence_observe", "Flow", "Sequence: Observe", - fields=( - FieldSpec("name", FieldType.STRING, placeholder="ingest"), - FieldSpec("stream_id", FieldType.STRING, placeholder="orders"), - FieldSpec("seq", FieldType.INT, placeholder="14"), + CommandSpec( + "AC_cas_get", "Flow", "Optimistic: Get", + fields=( + FieldSpec("name", FieldType.STRING, placeholder="config"), + FieldSpec("key", FieldType.STRING, placeholder=_DOTTED_KEY_PLACEHOLDER), + ), + description="Read a versioned record {value, version}.", ), - description="Classify a sequence number (ok/duplicate/gap/reorder).", - )) - specs.append(CommandSpec( - "AC_cas_put", "Flow", "Optimistic: Put (CAS)", - fields=( - FieldSpec("name", FieldType.STRING, placeholder="config"), - FieldSpec("key", FieldType.STRING, placeholder=_DOTTED_KEY_PLACEHOLDER), - FieldSpec("value", FieldType.STRING, placeholder='"prod-1"'), - FieldSpec("expected_version", FieldType.INT, optional=True), + CommandSpec( + "AC_outbox_enqueue", "Flow", "Outbox: Enqueue", + fields=( + FieldSpec("name", FieldType.STRING, placeholder="orders"), + FieldSpec("event", FieldType.STRING, + placeholder='{"type": "order.created", "id": 7}'), + ), + description="Durably buffer an event for at-least-once delivery.", ), - description="Put only if expected_version matches (returns new version).", - )) - specs.append(CommandSpec( - "AC_cas_get", "Flow", "Optimistic: Get", - fields=( - FieldSpec("name", FieldType.STRING, placeholder="config"), - FieldSpec("key", FieldType.STRING, placeholder=_DOTTED_KEY_PLACEHOLDER), + CommandSpec( + "AC_outbox_pending", "Flow", "Outbox: Pending", + fields=( + FieldSpec("name", FieldType.STRING, placeholder="orders"), + ), + description="List events still awaiting successful delivery.", ), - description="Read a versioned record {value, version}.", - )) - specs.append(CommandSpec( - "AC_outbox_enqueue", "Flow", "Outbox: Enqueue", - fields=( - FieldSpec("name", FieldType.STRING, placeholder="orders"), - FieldSpec("event", FieldType.STRING, - placeholder='{"type": "order.created", "id": 7}'), + CommandSpec( + "AC_collation_sort", "Data", "Text: Collation Sort", + fields=( + FieldSpec("items", FieldType.STRING, + placeholder='["zebra", "apple", "Äpple"]'), + FieldSpec("strength", FieldType.STRING, optional=True, + placeholder="tertiary"), + FieldSpec("tailoring", FieldType.STRING, optional=True, + placeholder="abc...xyzåäö"), + FieldSpec("reverse", FieldType.BOOL, optional=True), + ), + description="Locale-aware sort (base letter, then accent, then case).", ), - description="Durably buffer an event for at-least-once delivery.", - )) - specs.append(CommandSpec( - "AC_outbox_pending", "Flow", "Outbox: Pending", - fields=( - FieldSpec("name", FieldType.STRING, placeholder="orders"), + CommandSpec( + "AC_collation_compare", "Data", "Text: Collation Compare", + fields=( + FieldSpec("first", FieldType.STRING, placeholder="apple"), + FieldSpec("second", FieldType.STRING, placeholder="Äpple"), + FieldSpec("strength", FieldType.STRING, optional=True, + placeholder="tertiary"), + FieldSpec("tailoring", FieldType.STRING, optional=True), + ), + description="Locale-aware compare; returns order -1/0/1.", ), - description="List events still awaiting successful delivery.", - )) - specs.append(CommandSpec( - "AC_collation_sort", "Data", "Text: Collation Sort", - fields=( - FieldSpec("items", FieldType.STRING, - placeholder='["zebra", "apple", "Äpple"]'), - FieldSpec("strength", FieldType.STRING, optional=True, - placeholder="tertiary"), - FieldSpec("tailoring", FieldType.STRING, optional=True, - placeholder="abc...xyzåäö"), - FieldSpec("reverse", FieldType.BOOL, optional=True), - ), - description="Locale-aware sort (base letter, then accent, then case).", - )) - specs.append(CommandSpec( - "AC_collation_compare", "Data", "Text: Collation Compare", - fields=( - FieldSpec("first", FieldType.STRING, placeholder="apple"), - FieldSpec("second", FieldType.STRING, placeholder="Äpple"), - FieldSpec("strength", FieldType.STRING, optional=True, - placeholder="tertiary"), - FieldSpec("tailoring", FieldType.STRING, optional=True), + CommandSpec( + "AC_confusable_scan", "Data", "Text: Confusable Scan", + fields=( + FieldSpec("text", FieldType.STRING, placeholder="pаypal.com"), + ), + description="Homoglyph / mixed-script spoofing report for a string.", ), - description="Locale-aware compare; returns order -1/0/1.", - )) - specs.append(CommandSpec( - "AC_confusable_scan", "Data", "Text: Confusable Scan", - fields=( - FieldSpec("text", FieldType.STRING, placeholder="pаypal.com"), + CommandSpec( + "AC_confusable_compare", "Data", "Text: Confusable Compare", + fields=( + FieldSpec("first", FieldType.STRING, placeholder="paypal"), + FieldSpec("second", FieldType.STRING, placeholder="pаypal"), + ), + description="Whether two strings share the same confusable skeleton.", ), - description="Homoglyph / mixed-script spoofing report for a string.", - )) - specs.append(CommandSpec( - "AC_confusable_compare", "Data", "Text: Confusable Compare", - fields=( - FieldSpec("first", FieldType.STRING, placeholder="paypal"), - FieldSpec("second", FieldType.STRING, placeholder="pаypal"), + CommandSpec( + "AC_readability_report", "Data", "Text: Readability Report", + fields=( + FieldSpec("text", FieldType.STRING, + placeholder="The cat sat on the mat."), + ), + description="Flesch / Flesch-Kincaid / Fog / SMOG / ARI scores + counts.", ), - description="Whether two strings share the same confusable skeleton.", - )) - specs.append(CommandSpec( - "AC_readability_report", "Data", "Text: Readability Report", - fields=( - FieldSpec("text", FieldType.STRING, - placeholder="The cat sat on the mat."), + CommandSpec( + "AC_bidi_check", "Data", "Text: Bidi / Trojan-Source Check", + fields=( + FieldSpec("text", FieldType.STRING, placeholder="value = admin"), + ), + description="Bidi controls, nesting balance, base dir, Trojan-source flag.", ), - description="Flesch / Flesch-Kincaid / Fog / SMOG / ARI scores + counts.", - )) - specs.append(CommandSpec( - "AC_bidi_check", "Data", "Text: Bidi / Trojan-Source Check", - fields=( - FieldSpec("text", FieldType.STRING, placeholder="value = admin"), + CommandSpec( + "AC_bidi_strip", "Data", "Text: Strip Bidi Controls", + fields=( + FieldSpec("text", FieldType.STRING, placeholder="value = admin"), + ), + description="Remove all bidirectional control characters from a string.", ), - description="Bidi controls, nesting balance, base dir, Trojan-source flag.", - )) - specs.append(CommandSpec( - "AC_bidi_strip", "Data", "Text: Strip Bidi Controls", - fields=( - FieldSpec("text", FieldType.STRING, placeholder="value = admin"), + CommandSpec( + "AC_format_list", "Data", "Text: Format List", + fields=( + FieldSpec("items", FieldType.STRING, + placeholder='["apple", "pear", "grape"]'), + FieldSpec("style", FieldType.STRING, optional=True, + placeholder="and | or | unit"), + FieldSpec("locale", FieldType.STRING, optional=True, + placeholder="en | es | fr | de | pt"), + ), + description="Join items into a localised list ('A, B, and C').", ), - description="Remove all bidirectional control characters from a string.", - )) - specs.append(CommandSpec( - "AC_format_list", "Data", "Text: Format List", - fields=( - FieldSpec("items", FieldType.STRING, - placeholder='["apple", "pear", "grape"]'), - FieldSpec("style", FieldType.STRING, optional=True, - placeholder="and | or | unit"), - FieldSpec("locale", FieldType.STRING, optional=True, - placeholder="en | es | fr | de | pt"), - ), - description="Join items into a localised list ('A, B, and C').", - )) - specs.append(CommandSpec( - "AC_format_message", "Data", "Text: Format Message (ICU)", - fields=( - FieldSpec("pattern", FieldType.STRING, - placeholder="{count, plural, one {# item} other {# items}}"), - FieldSpec("args", FieldType.STRING, placeholder='{"count": 3}'), - FieldSpec("locale", FieldType.STRING, optional=True, - placeholder="en | fr"), + CommandSpec( + "AC_format_message", "Data", "Text: Format Message (ICU)", + fields=( + FieldSpec("pattern", FieldType.STRING, + placeholder="{count, plural, one {# item} other {# items}}"), + FieldSpec("args", FieldType.STRING, placeholder='{"count": 3}'), + FieldSpec("locale", FieldType.STRING, optional=True, + placeholder="en | fr"), + ), + description="Render ICU plural/select/selectordinal message.", ), - description="Render ICU plural/select/selectordinal message.", - )) - specs.append(CommandSpec( - "AC_gettext_translate", "Data", "Text: gettext Translate (.po)", - fields=( - FieldSpec("po", FieldType.STRING, - placeholder='msgid "Hello"\\nmsgstr "Hola"'), - FieldSpec("msgid", FieldType.STRING, placeholder="Hello"), - FieldSpec("context", FieldType.STRING, optional=True), + CommandSpec( + "AC_gettext_translate", "Data", "Text: gettext Translate (.po)", + fields=( + FieldSpec("po", FieldType.STRING, + placeholder='msgid "Hello"\\nmsgstr "Hola"'), + FieldSpec("msgid", FieldType.STRING, placeholder="Hello"), + FieldSpec("context", FieldType.STRING, optional=True), + ), + description="Look up a singular translation in a gettext .po catalog.", ), - description="Look up a singular translation in a gettext .po catalog.", - )) - specs.append(CommandSpec( - "AC_gettext_ngettext", "Data", "Text: gettext Plural (.po)", - fields=( - FieldSpec("po", FieldType.STRING, placeholder="(.po source)"), - FieldSpec("msgid", FieldType.STRING, placeholder="file"), - FieldSpec("msgid_plural", FieldType.STRING, placeholder="files"), - FieldSpec("n", FieldType.INT, placeholder="3"), + CommandSpec( + "AC_gettext_ngettext", "Data", "Text: gettext Plural (.po)", + fields=( + FieldSpec("po", FieldType.STRING, placeholder="(.po source)"), + FieldSpec("msgid", FieldType.STRING, placeholder="file"), + FieldSpec("msgid_plural", FieldType.STRING, placeholder="files"), + FieldSpec("n", FieldType.INT, placeholder="3"), + ), + description="Pick the plural-correct translation for count n.", ), - description="Pick the plural-correct translation for count n.", - )) - specs.append(CommandSpec( - "AC_checksum_validate", "Data", "Checksum: Validate", - fields=( - FieldSpec("scheme", FieldType.STRING, - placeholder="luhn | verhoeff | damm | mod97"), - FieldSpec("number", FieldType.STRING, placeholder="4111111111111111"), + CommandSpec( + "AC_checksum_validate", "Data", "Checksum: Validate", + fields=( + FieldSpec("scheme", FieldType.STRING, + placeholder="luhn | verhoeff | damm | mod97"), + FieldSpec("number", FieldType.STRING, placeholder="4111111111111111"), + ), + description="Validate a number's check digit (Luhn/Verhoeff/Damm/mod97).", ), - description="Validate a number's check digit (Luhn/Verhoeff/Damm/mod97).", - )) - specs.append(CommandSpec( - "AC_checksum_digit", "Data", "Checksum: Check Digit", - fields=( - FieldSpec("scheme", FieldType.STRING, - placeholder="luhn | verhoeff | damm | mod97"), - FieldSpec("partial", FieldType.STRING, placeholder="799273987"), + CommandSpec( + "AC_checksum_digit", "Data", "Checksum: Check Digit", + fields=( + FieldSpec("scheme", FieldType.STRING, + placeholder="luhn | verhoeff | damm | mod97"), + FieldSpec("partial", FieldType.STRING, placeholder="799273987"), + ), + description="Compute the check digit(s) to append to a value.", ), - description="Compute the check digit(s) to append to a value.", - )) - specs.append(CommandSpec( - "AC_diff_rows", "Data", "Dataset Diff: Rows by Key", - fields=( - FieldSpec("old_rows", FieldType.STRING, - placeholder='[{"id": 1, "name": "a"}]'), - FieldSpec("new_rows", FieldType.STRING, - placeholder='[{"id": 1, "name": "b"}]'), - FieldSpec("key", FieldType.STRING, - placeholder='id (or ["id", "region"])'), - ), - description="Diff two row-sets by key: added/removed/changed/unchanged.", - )) - specs.append(CommandSpec( - "AC_cell_changes", "Data", "Dataset Diff: Cell Changes", - fields=( - FieldSpec("old_rows", FieldType.STRING, - placeholder='[{"id": 1, "name": "a"}]'), - FieldSpec("new_rows", FieldType.STRING, - placeholder='[{"id": 1, "name": "b"}]'), - FieldSpec("key", FieldType.STRING, placeholder="id"), + CommandSpec( + "AC_diff_rows", "Data", "Dataset Diff: Rows by Key", + fields=( + FieldSpec("old_rows", FieldType.STRING, + placeholder='[{"id": 1, "name": "a"}]'), + FieldSpec("new_rows", FieldType.STRING, + placeholder='[{"id": 1, "name": "b"}]'), + FieldSpec("key", FieldType.STRING, + placeholder='id (or ["id", "region"])'), + ), + description="Diff two row-sets by key: added/removed/changed/unchanged.", ), - description="Per-cell {key, column, old, new} changes between row-sets.", - )) - specs.append(CommandSpec( - "AC_check_foreign_key", "Data", "Referential: Foreign Key", - fields=( - FieldSpec("child_rows", FieldType.STRING, - placeholder='[{"user_id": 1}]'), - FieldSpec("child_col", FieldType.STRING, placeholder="user_id"), - FieldSpec("parent_rows", FieldType.STRING, - placeholder='[{"id": 1}]'), - FieldSpec("parent_col", FieldType.STRING, placeholder="id"), - ), - description="Every child value must exist in the parent column.", - )) - specs.append(CommandSpec( - "AC_check_unique_key", "Data", "Referential: Unique Key", - fields=( - FieldSpec("rows", FieldType.STRING, - placeholder='[{"id": 1}, {"id": 1}]'), - FieldSpec("cols", FieldType.STRING, - placeholder='id (or ["region", "id"])'), + CommandSpec( + "AC_cell_changes", "Data", "Dataset Diff: Cell Changes", + fields=( + FieldSpec("old_rows", FieldType.STRING, + placeholder='[{"id": 1, "name": "a"}]'), + FieldSpec("new_rows", FieldType.STRING, + placeholder='[{"id": 1, "name": "b"}]'), + FieldSpec("key", FieldType.STRING, placeholder="id"), + ), + description="Per-cell {key, column, old, new} changes between row-sets.", ), - description="A single or composite key must be unique across rows.", - )) - specs.append(CommandSpec( - "AC_check_accepted_values", "Data", "Referential: Accepted Values", - fields=( - FieldSpec("rows", FieldType.STRING, - placeholder='[{"status": "open"}]'), - FieldSpec("col", FieldType.STRING, placeholder="status"), - FieldSpec("allowed", FieldType.STRING, - placeholder='["open", "closed"]'), + CommandSpec( + "AC_check_foreign_key", "Data", "Referential: Foreign Key", + fields=( + FieldSpec("child_rows", FieldType.STRING, + placeholder='[{"user_id": 1}]'), + FieldSpec("child_col", FieldType.STRING, placeholder="user_id"), + FieldSpec("parent_rows", FieldType.STRING, + placeholder='[{"id": 1}]'), + FieldSpec("parent_col", FieldType.STRING, placeholder="id"), + ), + description="Every child value must exist in the parent column.", ), - description="Every non-null column value must be in the allowed set.", - )) - specs.append(CommandSpec( - "AC_check_row_count", "Data", "Referential: Row Count", - fields=( - FieldSpec("rows", FieldType.STRING, placeholder='[{"id": 1}]'), - FieldSpec("minimum", FieldType.INT, optional=True), - FieldSpec("maximum", FieldType.INT, optional=True), + CommandSpec( + "AC_check_unique_key", "Data", "Referential: Unique Key", + fields=( + FieldSpec("rows", FieldType.STRING, + placeholder='[{"id": 1}, {"id": 1}]'), + FieldSpec("cols", FieldType.STRING, + placeholder='id (or ["region", "id"])'), + ), + description="A single or composite key must be unique across rows.", ), - description="The row count must fall within the given bounds.", - )) - specs.append(CommandSpec( - "AC_rate_limit", "Flow", "Rate Limit (Token Bucket)", - fields=( - FieldSpec("name", FieldType.STRING), - FieldSpec("rate", FieldType.FLOAT, optional=True, default=1.0), - FieldSpec("capacity", FieldType.FLOAT, optional=True, default=1.0), - FieldSpec("n", FieldType.FLOAT, optional=True, default=1.0), + CommandSpec( + "AC_check_accepted_values", "Data", "Referential: Accepted Values", + fields=( + FieldSpec("rows", FieldType.STRING, + placeholder='[{"status": "open"}]'), + FieldSpec("col", FieldType.STRING, placeholder="status"), + FieldSpec("allowed", FieldType.STRING, + placeholder='["open", "closed"]'), + ), + description="Every non-null column value must be in the allowed set.", ), - description="Try to take 'n' tokens from a named limiter; {acquired, wait}.", - )) + CommandSpec( + "AC_check_row_count", "Data", "Referential: Row Count", + fields=( + FieldSpec("rows", FieldType.STRING, placeholder='[{"id": 1}]'), + FieldSpec("minimum", FieldType.INT, optional=True), + FieldSpec("maximum", FieldType.INT, optional=True), + ), + description="The row count must fall within the given bounds.", + ), + CommandSpec( + "AC_rate_limit", "Flow", "Rate Limit (Token Bucket)", + fields=( + FieldSpec("name", FieldType.STRING), + FieldSpec("rate", FieldType.FLOAT, optional=True, default=1.0), + FieldSpec("capacity", FieldType.FLOAT, optional=True, default=1.0), + FieldSpec("n", FieldType.FLOAT, optional=True, default=1.0), + ), + description="Try to take 'n' tokens from a named limiter; {acquired, wait}.", + ), + ]) def _add_input_macro_specs(specs: List[CommandSpec]) -> None: @@ -4014,1336 +4051,1364 @@ def _add_input_macro_specs(specs: List[CommandSpec]) -> None: def _add_screen_state_specs(specs: List[CommandSpec]) -> None: app = FieldSpec("app_name", FieldType.STRING, optional=True) - specs.append(CommandSpec( - "AC_screen_snapshot", _NATIVE_UI, "Screen: Snapshot Baseline", - fields=(app,), - description="Snapshot the a11y tree as a semantic-diff baseline.", - )) - specs.append(CommandSpec( - "AC_screen_diff", _NATIVE_UI, "Screen: Diff Snapshots", - description="Semantic diff of 'before'/'after' snapshots (JSON view).", - )) - specs.append(CommandSpec( - "AC_screen_changed", _NATIVE_UI, "Screen: What Changed", - fields=(app,), - description="Diff the live screen against the last snapshot baseline.", - )) - specs.append(CommandSpec( - "AC_describe_screen", _NATIVE_UI, "Screen: Describe", - fields=(app,), - description="Structured 'where am I' (role counts + control labels).", - )) + specs.extend([ + CommandSpec( + "AC_screen_snapshot", _NATIVE_UI, "Screen: Snapshot Baseline", + fields=(app,), + description="Snapshot the a11y tree as a semantic-diff baseline.", + ), + CommandSpec( + "AC_screen_diff", _NATIVE_UI, "Screen: Diff Snapshots", + description="Semantic diff of 'before'/'after' snapshots (JSON view).", + ), + CommandSpec( + "AC_screen_changed", _NATIVE_UI, "Screen: What Changed", + fields=(app,), + description="Diff the live screen against the last snapshot baseline.", + ), + CommandSpec( + "AC_describe_screen", _NATIVE_UI, "Screen: Describe", + fields=(app,), + description="Structured 'where am I' (role counts + control labels).", + ), + ]) def _add_set_of_marks_specs(specs: List[CommandSpec]) -> None: - specs.append(CommandSpec( - "AC_cua_command", _NATIVE_UI, "Computer-Use: Map Action", - fields=( - FieldSpec("payload", FieldType.STRING, - placeholder='{"action":"left_click","coordinate":[x,y]}'), - FieldSpec("source", FieldType.ENUM, optional=True, default="canonical", - choices=("canonical", "anthropic", "openai")), + specs.extend([ + CommandSpec( + "AC_cua_command", _NATIVE_UI, "Computer-Use: Map Action", + fields=( + FieldSpec("payload", FieldType.STRING, + placeholder='{"action":"left_click","coordinate":[x,y]}'), + FieldSpec("source", FieldType.ENUM, optional=True, default="canonical", + choices=("canonical", "anthropic", "openai")), + ), + description="Map an Anthropic / OpenAI computer-use action to an AC command.", ), - description="Map an Anthropic / OpenAI computer-use action to an AC command.", - )) - specs.append(CommandSpec( - "AC_serialize_observation", _NATIVE_UI, "Observation: Serialize Elements", - fields=( - FieldSpec("elements", FieldType.STRING, - placeholder=_MARKS_JSON_PLACEHOLDER), - FieldSpec("viewport", FieldType.STRING, optional=True, - placeholder=_RECT4_PLACEHOLDER), - FieldSpec("max_elements", FieldType.INT, optional=True, default=80), + CommandSpec( + "AC_serialize_observation", _NATIVE_UI, "Observation: Serialize Elements", + fields=( + FieldSpec("elements", FieldType.STRING, + placeholder=_MARKS_JSON_PLACEHOLDER), + FieldSpec("viewport", FieldType.STRING, optional=True, + placeholder=_RECT4_PLACEHOLDER), + FieldSpec("max_elements", FieldType.INT, optional=True, default=80), + ), + description="Indexed text observation of UI elements for a VLM (act by index).", ), - description="Indexed text observation of UI elements for a VLM (act by index).", - )) - specs.append(CommandSpec( - "AC_observation_index", _NATIVE_UI, "Observation: Index Elements", - fields=( - FieldSpec("elements", FieldType.STRING, - placeholder=_MARKS_JSON_PLACEHOLDER), - FieldSpec("viewport", FieldType.STRING, optional=True, - placeholder=_RECT4_PLACEHOLDER), - FieldSpec("max_elements", FieldType.INT, optional=True, default=80), + CommandSpec( + "AC_observation_index", _NATIVE_UI, "Observation: Index Elements", + fields=( + FieldSpec("elements", FieldType.STRING, + placeholder=_MARKS_JSON_PLACEHOLDER), + FieldSpec("viewport", FieldType.STRING, optional=True, + placeholder=_RECT4_PLACEHOLDER), + FieldSpec("max_elements", FieldType.INT, optional=True, default=80), + ), + description="Reading-ordered, viewport-clipped, indexed element list.", ), - description="Reading-ordered, viewport-clipped, indexed element list.", - )) - specs.append(CommandSpec( - "AC_delta_observation", _NATIVE_UI, "Observation: Delta (what changed)", - fields=( - FieldSpec("prev", FieldType.STRING, - placeholder=_MARKS_JSON_PLACEHOLDER), - FieldSpec("curr", FieldType.STRING, - placeholder=_MARKS_JSON_PLACEHOLDER), - FieldSpec("viewport", FieldType.STRING, optional=True, - placeholder=_RECT4_PLACEHOLDER), - FieldSpec("max_elements", FieldType.INT, optional=True, default=80), - FieldSpec("max_lines", FieldType.INT, optional=True, default=40), - ), - description="Token-budgeted '+/~/-' summary of what changed between frames.", - )) - specs.append(CommandSpec( - "AC_classify_effect", _NATIVE_UI, "Classify Action Effect", - fields=( - FieldSpec("before", FieldType.STRING, - placeholder='[{"role":"button","name":"OK","x":0,"y":0}]'), - FieldSpec("after", FieldType.STRING, - placeholder='[{"role":"button","name":"OK","x":0,"y":0}]'), - FieldSpec("action", FieldType.STRING, - placeholder='{"type":"click","x":50,"y":50}'), - FieldSpec("radius", FieldType.INT, optional=True, default=64), - ), - description="Did the action change the screen near its target? (no_op/…).", - )) - specs.append(CommandSpec( - "AC_effect_near_point", _NATIVE_UI, "Effect Near Point?", - fields=( - FieldSpec("before", FieldType.STRING, - placeholder=_BOXES_JSON_PLACEHOLDER), - FieldSpec("after", FieldType.STRING, - placeholder=_BOXES_JSON_PLACEHOLDER), - FieldSpec("point", FieldType.STRING, placeholder="[50, 50]"), - FieldSpec("radius", FieldType.INT, optional=True, default=64), - ), - description="Did any before/after change land within radius of a point?", - )) - specs.append(CommandSpec( - "AC_check_postcondition", _NATIVE_UI, "Check Postcondition", - fields=( - FieldSpec("after", FieldType.STRING, - placeholder='[{"role":"dialog","name":"Saved"}]'), - FieldSpec("spec", FieldType.STRING, - placeholder='{"appears":{"role":"dialog"},' - '"disabled":{"name":"Submit"}}'), - FieldSpec("before", FieldType.STRING, optional=True, - placeholder='[{"role":"button","name":"Submit"}]'), - ), - description="Check expected outcome clauses against after/before frames.", - )) - specs.append(CommandSpec( - "AC_plan_repair", _NATIVE_UI, "Plan Repair Tactics", - fields=( - FieldSpec("verdict", FieldType.STRING, - placeholder="no_op / changed_elsewhere / changed"), - FieldSpec("max_attempts", FieldType.INT, optional=True, default=3), + CommandSpec( + "AC_delta_observation", _NATIVE_UI, "Observation: Delta (what changed)", + fields=( + FieldSpec("prev", FieldType.STRING, + placeholder=_MARKS_JSON_PLACEHOLDER), + FieldSpec("curr", FieldType.STRING, + placeholder=_MARKS_JSON_PLACEHOLDER), + FieldSpec("viewport", FieldType.STRING, optional=True, + placeholder=_RECT4_PLACEHOLDER), + FieldSpec("max_elements", FieldType.INT, optional=True, default=80), + FieldSpec("max_lines", FieldType.INT, optional=True, default=40), + ), + description="Token-budgeted '+/~/-' summary of what changed between frames.", ), - description="Ordered repair tactics for a failed/no-effect action verdict.", - )) - specs.append(CommandSpec( - "AC_consensus_point", _NATIVE_UI, "Grounding Consensus Point", - fields=( - FieldSpec("candidates", FieldType.STRING, - placeholder="[[100, 100], [104, 98], [97, 103]]"), - FieldSpec("cluster_radius", FieldType.INT, optional=True, default=24), + CommandSpec( + "AC_classify_effect", _NATIVE_UI, "Classify Action Effect", + fields=( + FieldSpec("before", FieldType.STRING, + placeholder='[{"role":"button","name":"OK","x":0,"y":0}]'), + FieldSpec("after", FieldType.STRING, + placeholder='[{"role":"button","name":"OK","x":0,"y":0}]'), + FieldSpec("action", FieldType.STRING, + placeholder='{"type":"click","x":50,"y":50}'), + FieldSpec("radius", FieldType.INT, optional=True, default=64), + ), + description="Did the action change the screen near its target? (no_op/…).", ), - description="Agreed target point from clustered grounding proposals.", - )) - specs.append(CommandSpec( - "AC_settle_point", "Flow", "Settle Point (churn series)", - fields=( - FieldSpec("churns", FieldType.STRING, - placeholder="[5, 4, 0.5, 0.3, 0.2]"), - FieldSpec("quiet_samples", FieldType.INT, optional=True, default=3), - FieldSpec("max_churn", FieldType.FLOAT, optional=True, default=1.0), + CommandSpec( + "AC_effect_near_point", _NATIVE_UI, "Effect Near Point?", + fields=( + FieldSpec("before", FieldType.STRING, + placeholder=_BOXES_JSON_PLACEHOLDER), + FieldSpec("after", FieldType.STRING, + placeholder=_BOXES_JSON_PLACEHOLDER), + FieldSpec("point", FieldType.STRING, placeholder="[50, 50]"), + FieldSpec("radius", FieldType.INT, optional=True, default=64), + ), + description="Did any before/after change land within radius of a point?", ), - description="Index where a churn series first settles (offline settle check).", - )) - specs.append(CommandSpec( - "AC_build_critic_record", _NATIVE_UI, "Build Critic Record", - fields=( - FieldSpec("action", FieldType.STRING, - placeholder='{"type":"click","x":50,"y":50}'), - FieldSpec("before", FieldType.STRING, - placeholder=_BOXES_JSON_PLACEHOLDER), - FieldSpec("after", FieldType.STRING, - placeholder='[{"role":"dialog","x":40,"y":40}]'), - FieldSpec("postcondition", FieldType.STRING, optional=True, - placeholder='{"appears":{"role":"dialog"}}'), - FieldSpec("radius", FieldType.INT, optional=True, default=64), - ), - description="Per-step critic evidence (effect + delta + postcondition).", - )) - specs.append(CommandSpec( - "AC_score_step", _NATIVE_UI, "Score Step (rule-based)", - fields=( - FieldSpec("record", FieldType.STRING, - placeholder='{"effect":{"effect":"changed_near_target"}}'), + CommandSpec( + "AC_check_postcondition", _NATIVE_UI, "Check Postcondition", + fields=( + FieldSpec("after", FieldType.STRING, + placeholder='[{"role":"dialog","name":"Saved"}]'), + FieldSpec("spec", FieldType.STRING, + placeholder='{"appears":{"role":"dialog"},' + '"disabled":{"name":"Submit"}}'), + FieldSpec("before", FieldType.STRING, optional=True, + placeholder='[{"role":"button","name":"Submit"}]'), + ), + description="Check expected outcome clauses against after/before frames.", ), - description="Rule-based outcome + process score of a critic record.", - )) - specs.append(CommandSpec( - "AC_consensus_element", _NATIVE_UI, "Grounding Consensus Element", - fields=( - FieldSpec("candidates", FieldType.STRING, - placeholder="[[8, 8], [12, 10]]"), - FieldSpec("elements", FieldType.STRING, - placeholder=_BOXES_JSON_PLACEHOLDER), + CommandSpec( + "AC_plan_repair", _NATIVE_UI, "Plan Repair Tactics", + fields=( + FieldSpec("verdict", FieldType.STRING, + placeholder="no_op / changed_elsewhere / changed"), + FieldSpec("max_attempts", FieldType.INT, optional=True, default=3), + ), + description="Ordered repair tactics for a failed/no-effect action verdict.", ), - description="Vote grounding proposals to the nearest element.", - )) - specs.append(CommandSpec( - "AC_validate_action", _NATIVE_UI, "Validate / Snap Action", - fields=( - FieldSpec("action", FieldType.STRING, - placeholder='{"type":"click","x":..,"y":..}'), - FieldSpec("screen", FieldType.STRING, optional=True, - placeholder="[width, height]"), - FieldSpec("targets", FieldType.STRING, optional=True, - placeholder=_POINTS_JSON_PLACEHOLDER), - ), - description="Reject out-of-bounds clicks; snap a near-miss to the nearest " - "element.", - )) - specs.append(CommandSpec( - "AC_match_elements", _NATIVE_UI, "Match Elements (frames)", - fields=( - FieldSpec("before", FieldType.STRING, - placeholder=_POINTS_JSON_PLACEHOLDER), - FieldSpec("after", FieldType.STRING, - placeholder=_POINTS_JSON_PLACEHOLDER), - FieldSpec("iou_threshold", FieldType.FLOAT, optional=True, default=0.5, - min_value=0.0, max_value=1.0), - ), - description="Match element boxes across two frames by overlap (move/rename).", - )) - specs.append(CommandSpec( - "AC_assign_stable_ids", _NATIVE_UI, "Assign Stable Element IDs", - fields=( - FieldSpec("elements", FieldType.STRING, - placeholder=_POINTS_JSON_PLACEHOLDER), - FieldSpec("prior", FieldType.STRING, optional=True, - placeholder="prior frame's elements (with ids)"), - FieldSpec("iou_threshold", FieldType.FLOAT, optional=True, default=0.5, - min_value=0.0, max_value=1.0), - ), - description="Tag elements with IDs carried across frames by overlap.", - )) - specs.append(CommandSpec( - "AC_score_candidates", _NATIVE_UI, "Score Candidates", - fields=( - FieldSpec("candidates", FieldType.STRING, - placeholder=_MARKS_JSON_PLACEHOLDER), - FieldSpec("want_role", FieldType.STRING, optional=True), - FieldSpec("want_name", FieldType.STRING, optional=True), - FieldSpec("anchor", FieldType.STRING, optional=True, - placeholder="[x, y]"), - ), - description="Rank candidate elements by role / name / proximity confidence.", - )) - specs.append(CommandSpec( - "AC_best_candidate", _NATIVE_UI, "Best Candidate", - fields=( - FieldSpec("candidates", FieldType.STRING, - placeholder=_MARKS_JSON_PLACEHOLDER), - FieldSpec("want_role", FieldType.STRING, optional=True), - FieldSpec("want_name", FieldType.STRING, optional=True), - FieldSpec("anchor", FieldType.STRING, optional=True, - placeholder="[x, y]"), - ), - description="The single highest-scoring candidate element.", - )) - specs.append(CommandSpec( - "AC_mark_screen", _NATIVE_UI, "Set-of-Marks: Number Elements", - fields=( - FieldSpec("app_name", FieldType.STRING, optional=True), - FieldSpec("render_path", FieldType.FILE_PATH, optional=True), + CommandSpec( + "AC_consensus_point", _NATIVE_UI, "Grounding Consensus Point", + fields=( + FieldSpec("candidates", FieldType.STRING, + placeholder="[[100, 100], [104, 98], [97, 103]]"), + FieldSpec("cluster_radius", FieldType.INT, optional=True, default=24), + ), + description="Agreed target point from clustered grounding proposals.", ), - description="Number live UI elements (id->bbox legend) for VLM " - "grounding; optional numbered-box overlay screenshot.", - )) - specs.append(CommandSpec( - "AC_mark_click", _NATIVE_UI, "Set-of-Marks: Click Number", - fields=(FieldSpec("mark_id", FieldType.INT),), - description="Click the element behind a numbered mark.", - )) + CommandSpec( + "AC_settle_point", "Flow", "Settle Point (churn series)", + fields=( + FieldSpec("churns", FieldType.STRING, + placeholder="[5, 4, 0.5, 0.3, 0.2]"), + FieldSpec("quiet_samples", FieldType.INT, optional=True, default=3), + FieldSpec("max_churn", FieldType.FLOAT, optional=True, default=1.0), + ), + description="Index where a churn series first settles (offline settle check).", + ), + CommandSpec( + "AC_build_critic_record", _NATIVE_UI, "Build Critic Record", + fields=( + FieldSpec("action", FieldType.STRING, + placeholder='{"type":"click","x":50,"y":50}'), + FieldSpec("before", FieldType.STRING, + placeholder=_BOXES_JSON_PLACEHOLDER), + FieldSpec("after", FieldType.STRING, + placeholder='[{"role":"dialog","x":40,"y":40}]'), + FieldSpec("postcondition", FieldType.STRING, optional=True, + placeholder='{"appears":{"role":"dialog"}}'), + FieldSpec("radius", FieldType.INT, optional=True, default=64), + ), + description="Per-step critic evidence (effect + delta + postcondition).", + ), + CommandSpec( + "AC_score_step", _NATIVE_UI, "Score Step (rule-based)", + fields=( + FieldSpec("record", FieldType.STRING, + placeholder='{"effect":{"effect":"changed_near_target"}}'), + ), + description="Rule-based outcome + process score of a critic record.", + ), + CommandSpec( + "AC_consensus_element", _NATIVE_UI, "Grounding Consensus Element", + fields=( + FieldSpec("candidates", FieldType.STRING, + placeholder="[[8, 8], [12, 10]]"), + FieldSpec("elements", FieldType.STRING, + placeholder=_BOXES_JSON_PLACEHOLDER), + ), + description="Vote grounding proposals to the nearest element.", + ), + CommandSpec( + "AC_validate_action", _NATIVE_UI, "Validate / Snap Action", + fields=( + FieldSpec("action", FieldType.STRING, + placeholder='{"type":"click","x":..,"y":..}'), + FieldSpec("screen", FieldType.STRING, optional=True, + placeholder=_SIZE_PLACEHOLDER), + FieldSpec("targets", FieldType.STRING, optional=True, + placeholder=_POINTS_JSON_PLACEHOLDER), + ), + description="Reject out-of-bounds clicks; snap a near-miss to the nearest " + "element.", + ), + CommandSpec( + "AC_match_elements", _NATIVE_UI, "Match Elements (frames)", + fields=( + FieldSpec("before", FieldType.STRING, + placeholder=_POINTS_JSON_PLACEHOLDER), + FieldSpec("after", FieldType.STRING, + placeholder=_POINTS_JSON_PLACEHOLDER), + FieldSpec("iou_threshold", FieldType.FLOAT, optional=True, default=0.5, + min_value=0.0, max_value=1.0), + ), + description="Match element boxes across two frames by overlap (move/rename).", + ), + CommandSpec( + "AC_assign_stable_ids", _NATIVE_UI, "Assign Stable Element IDs", + fields=( + FieldSpec("elements", FieldType.STRING, + placeholder=_POINTS_JSON_PLACEHOLDER), + FieldSpec("prior", FieldType.STRING, optional=True, + placeholder="prior frame's elements (with ids)"), + FieldSpec("iou_threshold", FieldType.FLOAT, optional=True, default=0.5, + min_value=0.0, max_value=1.0), + ), + description="Tag elements with IDs carried across frames by overlap.", + ), + CommandSpec( + "AC_score_candidates", _NATIVE_UI, "Score Candidates", + fields=( + FieldSpec("candidates", FieldType.STRING, + placeholder=_MARKS_JSON_PLACEHOLDER), + FieldSpec("want_role", FieldType.STRING, optional=True), + FieldSpec("want_name", FieldType.STRING, optional=True), + FieldSpec("anchor", FieldType.STRING, optional=True, + placeholder="[x, y]"), + ), + description="Rank candidate elements by role / name / proximity confidence.", + ), + CommandSpec( + "AC_best_candidate", _NATIVE_UI, "Best Candidate", + fields=( + FieldSpec("candidates", FieldType.STRING, + placeholder=_MARKS_JSON_PLACEHOLDER), + FieldSpec("want_role", FieldType.STRING, optional=True), + FieldSpec("want_name", FieldType.STRING, optional=True), + FieldSpec("anchor", FieldType.STRING, optional=True, + placeholder="[x, y]"), + ), + description="The single highest-scoring candidate element.", + ), + CommandSpec( + "AC_mark_screen", _NATIVE_UI, "Set-of-Marks: Number Elements", + fields=( + FieldSpec("app_name", FieldType.STRING, optional=True), + FieldSpec("render_path", FieldType.FILE_PATH, optional=True), + ), + description="Number live UI elements (id->bbox legend) for VLM " + "grounding; optional numbered-box overlay screenshot.", + ), + CommandSpec( + "AC_mark_click", _NATIVE_UI, "Set-of-Marks: Click Number", + fields=(FieldSpec("mark_id", FieldType.INT),), + description="Click the element behind a numbered mark.", + ), + ]) def _add_checkpoint_specs(specs: List[CommandSpec]) -> None: run_id = FieldSpec("run_id", FieldType.STRING) db = FieldSpec("db", FieldType.FILE_PATH) - specs.append(CommandSpec( - "AC_run_resumable", "Flow", "Run Resumable (checkpoint)", - fields=(run_id, db), - description="Run 'actions' (JSON view) with checkpoint/resume keyed " - "by run_id; resumes past completed steps after a crash.", - )) - specs.append(CommandSpec( - "AC_checkpoint_status", "Flow", "Checkpoint: Status", - fields=(run_id, db), - description="Return the saved checkpoint for a run (step + variables).", - )) - specs.append(CommandSpec( - "AC_checkpoint_clear", "Flow", "Checkpoint: Clear", - fields=(run_id, db), - description="Delete a run's checkpoint.", - )) - specs.append(CommandSpec( - "AC_wcag_audit", "Accessibility", "WCAG 2.2 Conformance Audit", - fields=( - FieldSpec("app_name", FieldType.STRING, optional=True), - FieldSpec("level", FieldType.ENUM, choices=("A", "AA", "AAA"), - optional=True, default="AA"), - FieldSpec("min_target_px", FieldType.INT, optional=True, - default=24), + specs.extend([ + CommandSpec( + "AC_run_resumable", "Flow", "Run Resumable (checkpoint)", + fields=(run_id, db), + description="Run 'actions' (JSON view) with checkpoint/resume keyed " + "by run_id; resumes past completed steps after a crash.", ), - description="WCAG 2.2 audit: SC-tagged findings + Target Size 2.5.8.", - )) + CommandSpec( + "AC_checkpoint_status", "Flow", "Checkpoint: Status", + fields=(run_id, db), + description="Return the saved checkpoint for a run (step + variables).", + ), + CommandSpec( + "AC_checkpoint_clear", "Flow", "Checkpoint: Clear", + fields=(run_id, db), + description="Delete a run's checkpoint.", + ), + CommandSpec( + "AC_wcag_audit", "Accessibility", "WCAG 2.2 Conformance Audit", + fields=( + FieldSpec("app_name", FieldType.STRING, optional=True), + FieldSpec("level", FieldType.ENUM, choices=("A", "AA", "AAA"), + optional=True, default="AA"), + FieldSpec("min_target_px", FieldType.INT, optional=True, + default=24), + ), + description="WCAG 2.2 audit: SC-tagged findings + Target Size 2.5.8.", + ), + ]) _add_observer_specs(specs) _add_ops_specs(specs) def _add_ops_specs(specs: List[CommandSpec]) -> None: - specs.append(CommandSpec( - "AC_generate_sbom", "Tools", "Generate SBOM (CycloneDX)", - fields=( - FieldSpec("path", FieldType.FILE_PATH, optional=True, - placeholder="sbom.cdx.json"), - FieldSpec("root", FieldType.STRING, optional=True, - default="je_auto_control"), + specs.extend([ + CommandSpec( + "AC_generate_sbom", "Tools", "Generate SBOM (CycloneDX)", + fields=( + FieldSpec("path", FieldType.FILE_PATH, optional=True, + placeholder="sbom.cdx.json"), + FieldSpec("root", FieldType.STRING, optional=True, + default="je_auto_control"), + ), + description="Emit a CycloneDX 1.6 dependency SBOM for the project.", ), - description="Emit a CycloneDX 1.6 dependency SBOM for the project.", - )) - specs.append(CommandSpec( - "AC_shard_suite", "Testing", "Shard Suite (duration-aware)", - fields=( - FieldSpec("shards", FieldType.INT, default=2), - FieldSpec("history_path", FieldType.FILE_PATH, optional=True), - FieldSpec("window", FieldType.INT, optional=True, default=20), + CommandSpec( + "AC_shard_suite", "Testing", "Shard Suite (duration-aware)", + fields=( + FieldSpec("shards", FieldType.INT, default=2), + FieldSpec("history_path", FieldType.FILE_PATH, optional=True), + FieldSpec("window", FieldType.INT, optional=True, default=20), + ), + description="Balance 'flows' (JSON view) into N shards by duration.", ), - description="Balance 'flows' (JSON view) into N shards by duration.", - )) - specs.append(CommandSpec( - "AC_merge_results", "Testing", "Merge Shard Results", - description="Merge per-shard 'reports' (JSON view) into one report.", - )) + CommandSpec( + "AC_merge_results", "Testing", "Merge Shard Results", + description="Merge per-shard 'reports' (JSON view) into one report.", + ), + ]) def _add_observer_specs(specs: List[CommandSpec]) -> None: - specs.append(CommandSpec( - "AC_observe_add", "Flow", "Observe: Add Watch", - fields=( - FieldSpec("name", FieldType.STRING), - FieldSpec("kind", FieldType.ENUM, - choices=("image", "text", "pixel"), default="image"), - FieldSpec("event", FieldType.ENUM, - choices=("appear", "vanish", "change"), - default="appear"), - FieldSpec("image", FieldType.FILE_PATH, optional=True), - FieldSpec("threshold", FieldType.FLOAT, optional=True, - default=0.8), - FieldSpec("text", FieldType.STRING, optional=True), - FieldSpec("x", FieldType.INT, optional=True), - FieldSpec("y", FieldType.INT, optional=True), - ), - description="Run 'actions' (JSON view) on appear/vanish/change of an " - "image/text/pixel.", - )) - specs.append(CommandSpec( - "AC_observe_remove", "Flow", "Observe: Remove Watch", - fields=(FieldSpec("name", FieldType.STRING),), - description="Remove a registered watch.", - )) - specs.append(CommandSpec( - "AC_observe_list", "Flow", "Observe: List Watches", - description="List registered watch names.")) - specs.append(CommandSpec( - "AC_observe_poll", "Flow", "Observe: Poll Once", - description="Evaluate all watches once; return fired events.")) - specs.append(CommandSpec( - "AC_observe_start", "Flow", "Observe: Start", - description="Start the background observer thread.")) - specs.append(CommandSpec( - "AC_observe_stop", "Flow", "Observe: Stop", - description="Stop the background observer thread.")) + specs.extend([ + CommandSpec( + "AC_observe_add", "Flow", "Observe: Add Watch", + fields=( + FieldSpec("name", FieldType.STRING), + FieldSpec("kind", FieldType.ENUM, + choices=("image", "text", "pixel"), default="image"), + FieldSpec("event", FieldType.ENUM, + choices=("appear", "vanish", "change"), + default="appear"), + FieldSpec("image", FieldType.FILE_PATH, optional=True), + FieldSpec("threshold", FieldType.FLOAT, optional=True, + default=0.8), + FieldSpec("text", FieldType.STRING, optional=True), + FieldSpec("x", FieldType.INT, optional=True), + FieldSpec("y", FieldType.INT, optional=True), + ), + description="Run 'actions' (JSON view) on appear/vanish/change of an " + "image/text/pixel.", + ), + CommandSpec( + "AC_observe_remove", "Flow", "Observe: Remove Watch", + fields=(FieldSpec("name", FieldType.STRING),), + description="Remove a registered watch.", + ), + CommandSpec( + "AC_observe_list", "Flow", "Observe: List Watches", + description="List registered watch names."), + CommandSpec( + "AC_observe_poll", "Flow", "Observe: Poll Once", + description="Evaluate all watches once; return fired events."), + CommandSpec( + "AC_observe_start", "Flow", "Observe: Start", + description="Start the background observer thread."), + CommandSpec( + "AC_observe_stop", "Flow", "Observe: Stop", + description="Stop the background observer thread."), + ]) def _add_i18n_specs(specs: List[CommandSpec]) -> None: - specs.append(CommandSpec( - "AC_pseudo_localize", "Data", "Pseudo-Localize", - fields=( - FieldSpec("text", FieldType.STRING, optional=True), - FieldSpec("expansion", FieldType.FLOAT, optional=True, - default=0.4), + specs.extend([ + CommandSpec( + "AC_pseudo_localize", "Data", "Pseudo-Localize", + fields=( + FieldSpec("text", FieldType.STRING, optional=True), + FieldSpec("expansion", FieldType.FLOAT, optional=True, + default=0.4), + ), + description="Accent+pad a string (or 'mapping' via JSON view) for " + "i18n stress testing.", ), - description="Accent+pad a string (or 'mapping' via JSON view) for " - "i18n stress testing.", - )) - specs.append(CommandSpec( - "AC_check_overflow", "Data", "Check Text Overflow", - fields=( - FieldSpec("app_name", FieldType.STRING, optional=True), - FieldSpec("avg_char_px", FieldType.FLOAT, optional=True, - default=7.0), + CommandSpec( + "AC_check_overflow", "Data", "Check Text Overflow", + fields=( + FieldSpec("app_name", FieldType.STRING, optional=True), + FieldSpec("avg_char_px", FieldType.FLOAT, optional=True, + default=7.0), + ), + description="Flag text wider than its widget (translation overflow).", ), - description="Flag text wider than its widget (translation overflow).", - )) - specs.append(CommandSpec( - "AC_check_catalog", "Data", "Check Translation Catalog", - description="Diff 'target' vs 'base' catalog (JSON view): missing / " - "empty / placeholder mismatch.", - )) + CommandSpec( + "AC_check_catalog", "Data", "Check Translation Catalog", + description="Diff 'target' vs 'base' catalog (JSON view): missing / " + "empty / placeholder mismatch.", + ), + ]) def _add_data_quality_specs(specs: List[CommandSpec]) -> None: - specs.append(CommandSpec( - "AC_validate_rows", "Data", "Validate Rows (schema)", - description="Validate 'rows' against a 'schema' (both via JSON view).", - )) - specs.append(CommandSpec( - "AC_extract_fields", "Data", "Extract Fields (regex)", - fields=(FieldSpec("text", FieldType.STRING),), - description="Pull email/url/phone/amount/... from text; 'fields' / " - "'patterns' via JSON view.", - )) - specs.append(CommandSpec( - "AC_mask_rows", "Data", "Mask Rows", - description="Mask columns in 'rows' per 'rules' (redact/hash/partial)," - " via JSON view.", - )) + specs.extend([ + CommandSpec( + "AC_validate_rows", "Data", "Validate Rows (schema)", + description="Validate 'rows' against a 'schema' (both via JSON view).", + ), + CommandSpec( + "AC_extract_fields", "Data", "Extract Fields (regex)", + fields=(FieldSpec("text", FieldType.STRING),), + description="Pull email/url/phone/amount/... from text; 'fields' / " + "'patterns' via JSON view.", + ), + CommandSpec( + "AC_mask_rows", "Data", "Mask Rows", + description="Mask columns in 'rows' per 'rules' (redact/hash/partial)," + " via JSON view.", + ), + ]) def _add_memory_specs(specs: List[CommandSpec]) -> None: db = FieldSpec("db", FieldType.FILE_PATH) - specs.append(CommandSpec( - "AC_memory_remember", "Agent", "Memory: Remember Episode", - fields=(db, FieldSpec("goal", FieldType.STRING), - FieldSpec("outcome", FieldType.STRING, optional=True)), - description="Store an episode (goal -> 'steps' via JSON view -> " - "outcome).", - )) - specs.append(CommandSpec( - "AC_memory_recall", "Agent", "Memory: Recall", - fields=(db, FieldSpec("query", FieldType.STRING), - FieldSpec("limit", FieldType.INT, optional=True, default=5)), - description="Recall episodes most relevant to a query.", - )) - specs.append(CommandSpec( - "AC_memory_recent", "Agent", "Memory: Recent", - fields=(db, FieldSpec("limit", FieldType.INT, optional=True, - default=10)), - description="List the most recent episodes.", - )) - specs.append(CommandSpec( - "AC_memory_forget", "Agent", "Memory: Forget", - fields=(db, FieldSpec("episode_id", FieldType.INT)), - description="Delete an episode by id.", - )) - specs.append(CommandSpec( - "AC_memory_stats", "Agent", "Memory: Stats", - fields=(db,), - description="Episode count for a memory store.", - )) - specs.append(CommandSpec( - "AC_seed_everything", "Flow", "Seed RNG (deterministic)", - fields=(FieldSpec("seed", FieldType.INT, optional=True, default=0),), - description="Seed all RNG run-wide for reproducible runs.", - )) + specs.extend([ + CommandSpec( + "AC_memory_remember", "Agent", "Memory: Remember Episode", + fields=(db, FieldSpec("goal", FieldType.STRING), + FieldSpec("outcome", FieldType.STRING, optional=True)), + description="Store an episode (goal -> 'steps' via JSON view -> " + "outcome).", + ), + CommandSpec( + "AC_memory_recall", "Agent", "Memory: Recall", + fields=(db, FieldSpec("query", FieldType.STRING), + FieldSpec("limit", FieldType.INT, optional=True, default=5)), + description="Recall episodes most relevant to a query.", + ), + CommandSpec( + "AC_memory_recent", "Agent", "Memory: Recent", + fields=(db, FieldSpec("limit", FieldType.INT, optional=True, + default=10)), + description="List the most recent episodes.", + ), + CommandSpec( + "AC_memory_forget", "Agent", "Memory: Forget", + fields=(db, FieldSpec("episode_id", FieldType.INT)), + description="Delete an episode by id.", + ), + CommandSpec( + "AC_memory_stats", "Agent", "Memory: Stats", + fields=(db,), + description="Episode count for a memory store.", + ), + CommandSpec( + "AC_seed_everything", "Flow", "Seed RNG (deterministic)", + fields=(FieldSpec("seed", FieldType.INT, optional=True, default=0),), + description="Seed all RNG run-wide for reproducible runs.", + ), + ]) def _add_office_specs(specs: List[CommandSpec]) -> None: xlsx = FieldSpec("path", FieldType.FILE_PATH) - specs.append(CommandSpec( - "AC_read_workbook", "Office", "Excel: Read Workbook", - fields=(xlsx, FieldSpec("sheet", FieldType.STRING, optional=True)), - description="Read an .xlsx worksheet into rows (needs [office] extra).", - )) - specs.append(CommandSpec( - "AC_write_workbook", "Office", "Excel: Write Workbook", - fields=(xlsx, FieldSpec("sheet", FieldType.STRING, optional=True, - default="Sheet1")), - description="Write 'rows' (JSON view) to an .xlsx file.", - )) - specs.append(CommandSpec( - "AC_read_document", "Office", "Word: Read Document", - fields=(xlsx,), - description="Read a .docx file's paragraphs (needs [office] extra).", - )) - specs.append(CommandSpec( - "AC_write_document", "Office", "Word: Write Document", - fields=(xlsx,), - description="Write 'paragraphs' (JSON view) to a .docx file.", - )) - specs.append(CommandSpec( - "AC_read_presentation", "Office", "PowerPoint: Read", - fields=(xlsx,), - description="Read a .pptx file's per-slide text (needs [office]).", - )) - specs.append(CommandSpec( - "AC_write_presentation", "Office", "PowerPoint: Write", - fields=(xlsx,), - description="Write 'slides' (JSON view) to a .pptx file.", - )) + specs.extend([ + CommandSpec( + "AC_read_workbook", "Office", "Excel: Read Workbook", + fields=(xlsx, FieldSpec("sheet", FieldType.STRING, optional=True)), + description="Read an .xlsx worksheet into rows (needs [office] extra).", + ), + CommandSpec( + "AC_write_workbook", "Office", "Excel: Write Workbook", + fields=(xlsx, FieldSpec("sheet", FieldType.STRING, optional=True, + default="Sheet1")), + description="Write 'rows' (JSON view) to an .xlsx file.", + ), + CommandSpec( + "AC_read_document", "Office", "Word: Read Document", + fields=(xlsx,), + description="Read a .docx file's paragraphs (needs [office] extra).", + ), + CommandSpec( + "AC_write_document", "Office", "Word: Write Document", + fields=(xlsx,), + description="Write 'paragraphs' (JSON view) to a .docx file.", + ), + CommandSpec( + "AC_read_presentation", "Office", "PowerPoint: Read", + fields=(xlsx,), + description="Read a .pptx file's per-slide text (needs [office]).", + ), + CommandSpec( + "AC_write_presentation", "Office", "PowerPoint: Write", + fields=(xlsx,), + description="Write 'slides' (JSON view) to a .pptx file.", + ), + ]) def _add_authoring_specs(specs: List[CommandSpec]) -> None: path = FieldSpec("path", FieldType.FILE_PATH) key = FieldSpec("key", FieldType.STRING) - specs.append(CommandSpec( - "AC_element_save", _NATIVE_UI, "Element: Save Locator", - fields=(path, key, - FieldSpec("name", FieldType.STRING, optional=True), - FieldSpec("role", FieldType.STRING, optional=True), - FieldSpec("app_name", FieldType.STRING, optional=True)), - description="Save a named native-UI locator (object repository).", - )) - specs.append(CommandSpec( - "AC_element_find", _NATIVE_UI, "Element: Find Saved", - fields=(path, key), - description="Resolve a saved locator to a live element summary.", - )) - specs.append(CommandSpec( - "AC_element_click", _NATIVE_UI, "Element: Click Saved", - fields=(path, key), - description="Click the element behind a saved locator.", - )) - specs.append(CommandSpec( - "AC_element_remove", _NATIVE_UI, "Element: Remove Saved", - fields=(path, key), - description="Delete a saved locator.", - )) - specs.append(CommandSpec( - "AC_element_list", _NATIVE_UI, "Element: List Saved", - fields=(path,), - description="List saved locator names in a repository file.", - )) - specs.append(CommandSpec( - "AC_debug_trace", "Flow", "Debug: Trace Actions", - fields=(FieldSpec("dry_run", FieldType.BOOL, optional=True, - default=False),), - description="Run 'actions' (JSON view) and return a per-step trace.", - )) + specs.extend([ + CommandSpec( + "AC_element_save", _NATIVE_UI, "Element: Save Locator", + fields=(path, key, + FieldSpec("name", FieldType.STRING, optional=True), + FieldSpec("role", FieldType.STRING, optional=True), + FieldSpec("app_name", FieldType.STRING, optional=True)), + description="Save a named native-UI locator (object repository).", + ), + CommandSpec( + "AC_element_find", _NATIVE_UI, "Element: Find Saved", + fields=(path, key), + description="Resolve a saved locator to a live element summary.", + ), + CommandSpec( + "AC_element_click", _NATIVE_UI, "Element: Click Saved", + fields=(path, key), + description="Click the element behind a saved locator.", + ), + CommandSpec( + "AC_element_remove", _NATIVE_UI, "Element: Remove Saved", + fields=(path, key), + description="Delete a saved locator.", + ), + CommandSpec( + "AC_element_list", _NATIVE_UI, "Element: List Saved", + fields=(path,), + description="List saved locator names in a repository file.", + ), + CommandSpec( + "AC_debug_trace", "Flow", "Debug: Trace Actions", + fields=(FieldSpec("dry_run", FieldType.BOOL, optional=True, + default=False),), + description="Run 'actions' (JSON view) and return a per-step trace.", + ), + ]) def _add_agent_specs(specs: List[CommandSpec]) -> None: path = FieldSpec("path", FieldType.FILE_PATH) name = FieldSpec("name", FieldType.STRING) - specs.append(CommandSpec( - "AC_skill_save", "Agent", "Skill: Save Playbook", - fields=(path, name, - FieldSpec("description", FieldType.STRING, optional=True), - FieldSpec("tags", FieldType.STRING, optional=True)), - description="Save a reusable action sequence ('actions' via JSON " - "view) under a name.", - )) - specs.append(CommandSpec( - "AC_skill_run", "Agent", "Skill: Run Playbook", - fields=(path, name), - description="Execute a stored skill's actions.", - )) - specs.append(CommandSpec( - "AC_skill_list", "Agent", "Skill: List", - fields=(path,), - description="List saved skill names.", - )) - specs.append(CommandSpec( - "AC_skill_remove", "Agent", "Skill: Remove", - fields=(path, name), - description="Delete a saved skill.", - )) - specs.append(CommandSpec( - "AC_skill_search", "Agent", "Skill: Search", - fields=(path, FieldSpec("query", FieldType.STRING)), - description="Search skills by name/description/tags.", - )) - specs.append(CommandSpec( - "AC_guard_text", "Agent", "Guardrail: Scan Text", - fields=(FieldSpec("text", FieldType.STRING), - FieldSpec("threshold", FieldType.INT, optional=True, - default=2)), - description="Scan untrusted text for prompt-injection patterns.", - )) - specs.append(CommandSpec( - "AC_agent_card", "Agent", "A2A Agent Card", - fields=(FieldSpec("path", FieldType.FILE_PATH, optional=True, - placeholder="agent-card.json"),), - description="Write an A2A agent card describing AutoControl's skills.", - )) + specs.extend([ + CommandSpec( + "AC_skill_save", "Agent", "Skill: Save Playbook", + fields=(path, name, + FieldSpec("description", FieldType.STRING, optional=True), + FieldSpec("tags", FieldType.STRING, optional=True)), + description="Save a reusable action sequence ('actions' via JSON " + "view) under a name.", + ), + CommandSpec( + "AC_skill_run", "Agent", "Skill: Run Playbook", + fields=(path, name), + description="Execute a stored skill's actions.", + ), + CommandSpec( + "AC_skill_list", "Agent", "Skill: List", + fields=(path,), + description="List saved skill names.", + ), + CommandSpec( + "AC_skill_remove", "Agent", "Skill: Remove", + fields=(path, name), + description="Delete a saved skill.", + ), + CommandSpec( + "AC_skill_search", "Agent", "Skill: Search", + fields=(path, FieldSpec("query", FieldType.STRING)), + description="Search skills by name/description/tags.", + ), + CommandSpec( + "AC_guard_text", "Agent", "Guardrail: Scan Text", + fields=(FieldSpec("text", FieldType.STRING), + FieldSpec("threshold", FieldType.INT, optional=True, + default=2)), + description="Scan untrusted text for prompt-injection patterns.", + ), + CommandSpec( + "AC_agent_card", "Agent", "A2A Agent Card", + fields=(FieldSpec("path", FieldType.FILE_PATH, optional=True, + placeholder="agent-card.json"),), + description="Write an A2A agent card describing AutoControl's skills.", + ), + ]) def _add_tooling_specs(specs: List[CommandSpec]) -> None: - specs.append(CommandSpec( - "AC_generate_data", "Data", "Generate Synthetic Data", - fields=( - FieldSpec("count", FieldType.INT, optional=True, default=10), - FieldSpec("path", FieldType.FILE_PATH, optional=True), - FieldSpec("fmt", FieldType.ENUM, choices=("json", "csv"), - optional=True), - FieldSpec("seed", FieldType.INT, optional=True), - ), - description="Generate seeded fake rows from a 'schema' (JSON view); " - "writes a file when 'path' is set.", - )) - specs.append(CommandSpec( - "AC_mcp_manifest", "Tools", "MCP Registry Manifest", - fields=( - FieldSpec("path", FieldType.FILE_PATH, optional=True, - placeholder="server.json"), - FieldSpec("include_tools", FieldType.BOOL, optional=True, - default=False), + specs.extend([ + CommandSpec( + "AC_generate_data", "Data", "Generate Synthetic Data", + fields=( + FieldSpec("count", FieldType.INT, optional=True, default=10), + FieldSpec("path", FieldType.FILE_PATH, optional=True), + FieldSpec("fmt", FieldType.ENUM, choices=("json", "csv"), + optional=True), + FieldSpec("seed", FieldType.INT, optional=True), + ), + description="Generate seeded fake rows from a 'schema' (JSON view); " + "writes a file when 'path' is set.", ), - description="Write an MCP registry server.json for this server.", - )) - specs.append(CommandSpec( - "AC_rank_tests", "Testing", "Rank Tests by Risk", - fields=( - FieldSpec("history_path", FieldType.FILE_PATH, optional=True), - FieldSpec("window", FieldType.INT, optional=True, default=10), + CommandSpec( + "AC_mcp_manifest", "Tools", "MCP Registry Manifest", + fields=( + FieldSpec("path", FieldType.FILE_PATH, optional=True, + placeholder="server.json"), + FieldSpec("include_tools", FieldType.BOOL, optional=True, + default=False), + ), + description="Write an MCP registry server.json for this server.", ), - description="Score 'flows' (JSON view) by risk from run history.", - )) - specs.append(CommandSpec( - "AC_select_tests", "Testing", "Select Risky Tests", - fields=( - FieldSpec("k", FieldType.INT, optional=True), - FieldSpec("threshold", FieldType.FLOAT, optional=True), - FieldSpec("history_path", FieldType.FILE_PATH, optional=True), - FieldSpec("window", FieldType.INT, optional=True, default=10), + CommandSpec( + "AC_rank_tests", "Testing", "Rank Tests by Risk", + fields=( + FieldSpec("history_path", FieldType.FILE_PATH, optional=True), + FieldSpec("window", FieldType.INT, optional=True, default=10), + ), + description="Score 'flows' (JSON view) by risk from run history.", ), - description="Pick riskiest 'flows' (JSON view): top-k or threshold.", - )) + CommandSpec( + "AC_select_tests", "Testing", "Select Risky Tests", + fields=( + FieldSpec("k", FieldType.INT, optional=True), + FieldSpec("threshold", FieldType.FLOAT, optional=True), + FieldSpec("history_path", FieldType.FILE_PATH, optional=True), + FieldSpec("window", FieldType.INT, optional=True, default=10), + ), + description="Pick riskiest 'flows' (JSON view): top-k or threshold.", + ), + ]) def _add_webrunner_specs(specs: List[CommandSpec]) -> None: """Browser automation through the optional ``je_web_runner`` bridge.""" - specs.append(CommandSpec( - "AC_web_open", "Browser", "Open Browser at URL", - fields=( - FieldSpec("url", FieldType.STRING, placeholder="https://example.com"), - FieldSpec("browser", FieldType.ENUM, optional=True, default="chrome", - choices=("chrome", "chromium", "firefox", "edge", "safari")), + specs.extend([ + CommandSpec( + "AC_web_open", "Browser", "Open Browser at URL", + fields=( + FieldSpec("url", FieldType.STRING, placeholder="https://example.com"), + FieldSpec("browser", FieldType.ENUM, optional=True, default="chrome", + choices=("chrome", "chromium", "firefox", "edge", "safari")), + ), + description="Start a WebRunner Selenium browser and open the URL (needs je_web_runner).", ), - description="Start a WebRunner Selenium browser and open the URL (needs je_web_runner).", - )) - specs.append(CommandSpec( - "AC_web_run", "Browser", "Run WebRunner Command", - fields=(FieldSpec("action", FieldType.STRING, placeholder="WR_to_url"),), - description="Run one WR_* command; its arguments go in 'params' (JSON view).", - )) - specs.append(CommandSpec( - "AC_web_run_actions", "Browser", "Run WebRunner Commands", - description="Run 'actions' (JSON view), a list of {action, params}; stops at the first failure.", - )) - specs.append(CommandSpec( - "AC_web_screenshot", "Browser", "Browser Screenshot", - fields=(FieldSpec("file_path", FieldType.FILE_PATH, placeholder="page.png"),), - )) - specs.append(CommandSpec("AC_web_current_url", "Browser", "Current Browser URL")) - specs.append(CommandSpec("AC_web_quit", "Browser", "Quit Browser")) - specs.append(CommandSpec("AC_web_available", "Browser", "Is WebRunner Installed")) - specs.append(CommandSpec("AC_web_list_commands", "Browser", "List WebRunner Commands")) + CommandSpec( + "AC_web_run", "Browser", "Run WebRunner Command", + fields=(FieldSpec("action", FieldType.STRING, placeholder="WR_to_url"),), + description="Run one WR_* command; its arguments go in 'params' (JSON view).", + ), + CommandSpec( + "AC_web_run_actions", "Browser", "Run WebRunner Commands", + description="Run 'actions' (JSON view), a list of {action, params}; stops at the first failure.", + ), + CommandSpec( + "AC_web_screenshot", "Browser", "Browser Screenshot", + fields=(FieldSpec("file_path", FieldType.FILE_PATH, placeholder="page.png"),), + ), + CommandSpec("AC_web_current_url", "Browser", "Current Browser URL"), + CommandSpec("AC_web_quit", "Browser", "Quit Browser"), + CommandSpec("AC_web_available", "Browser", "Is WebRunner Installed"), + CommandSpec("AC_web_list_commands", "Browser", "List WebRunner Commands"), + ]) def _add_work_queue_specs(specs: List[CommandSpec]) -> None: db = FieldSpec("db", FieldType.FILE_PATH) name = FieldSpec("name", FieldType.STRING, optional=True, default="default") - specs.append(CommandSpec( - "AC_queue_add", "Queue", "Queue: Add Item", - fields=(db, FieldSpec("reference", FieldType.STRING, optional=True), - name), - description="Enqueue a work item (data via JSON view); dedupes by " - "reference.", - )) - specs.append(CommandSpec( - "AC_queue_next", "Queue", "Queue: Get Next Item", - fields=(db, name, FieldSpec("stale_after_s", FieldType.FLOAT, optional=True)), - description="Atomically claim the next work item (performer).", - )) - specs.append(CommandSpec( - "AC_queue_complete", "Queue", "Queue: Complete Item", - fields=(db, FieldSpec("item_id", FieldType.INT), name, - FieldSpec("claim", FieldType.INT, optional=True)), - description="Mark a work item successfully processed.", - )) - specs.append(CommandSpec( - "AC_queue_fail", "Queue", "Queue: Fail Item", - fields=(db, FieldSpec("item_id", FieldType.INT), - FieldSpec("error", FieldType.STRING), - FieldSpec("kind", FieldType.ENUM, - choices=("application", "business"), - optional=True, default="application"), - FieldSpec("max_retries", FieldType.INT, optional=True, + specs.extend([ + CommandSpec( + "AC_queue_add", "Queue", "Queue: Add Item", + fields=(db, FieldSpec("reference", FieldType.STRING, optional=True), + name), + description="Enqueue a work item (data via JSON view); dedupes by " + "reference.", + ), + CommandSpec( + "AC_queue_next", "Queue", "Queue: Get Next Item", + fields=(db, name, FieldSpec("stale_after_s", FieldType.FLOAT, optional=True)), + description="Atomically claim the next work item (performer).", + ), + CommandSpec( + "AC_queue_complete", "Queue", "Queue: Complete Item", + fields=(db, FieldSpec("item_id", FieldType.INT), name, + FieldSpec("claim", FieldType.INT, optional=True)), + description="Mark a work item successfully processed.", + ), + CommandSpec( + "AC_queue_fail", "Queue", "Queue: Fail Item", + fields=(db, FieldSpec("item_id", FieldType.INT), + FieldSpec("error", FieldType.STRING), + FieldSpec("kind", FieldType.ENUM, + choices=("application", "business"), + optional=True, default="application"), + FieldSpec("max_retries", FieldType.INT, optional=True, + default=3), + name, FieldSpec("claim", FieldType.INT, optional=True)), + description="Fail an item; application errors retry, business don't.", + ), + CommandSpec( + "AC_queue_stats", "Queue", "Queue: Stats", + fields=(db, name), + description="Per-status counts for a work queue.", + ), + CommandSpec( + "AC_shell_command", "Shell", "Shell Command", + fields=(FieldSpec("shell_command", FieldType.STRING),), + ), + CommandSpec( + "AC_open_path", "Shell", "Open File / URL (default app)", + fields=( + FieldSpec("target", FieldType.STRING, + placeholder="report.pdf or https://example.com"), + FieldSpec("verb", FieldType.STRING, optional=True, default="open", + placeholder="open / print / edit"), + ), + description="Open a file with its default app, or a URL in the browser.", + ), + CommandSpec( + "AC_plan_open", "Shell", "Plan Open (classify)", + fields=( + FieldSpec("target", FieldType.STRING), + FieldSpec("verb", FieldType.STRING, optional=True, default="open"), + ), + description="Classify how a file/URL would be opened (pure, no launch).", + ), + CommandSpec( + "AC_idle_seconds", "Shell", "Idle Seconds", + fields=(), + description="Seconds since the last user keyboard / mouse input.", + ), + CommandSpec( + "AC_is_idle", "Shell", "Is User Idle", + fields=( + FieldSpec("threshold", FieldType.FLOAT, default=300.0, + placeholder="idle seconds threshold"), + ), + description="True if the user has been idle for >= threshold seconds.", + ), + CommandSpec( + "AC_plan_keep_awake", "Shell", "Plan Keep Awake", + fields=( + FieldSpec("display", FieldType.BOOL, optional=True, default=True), + FieldSpec("system", FieldType.BOOL, optional=True, default=True), + ), + description="Describe a keep-awake request (pure, no OS call).", + ), + CommandSpec( + "AC_keep_awake_on", "Shell", "Keep Machine Awake", + fields=( + FieldSpec("display", FieldType.BOOL, optional=True, default=True), + FieldSpec("system", FieldType.BOOL, optional=True, default=True), + ), + description="Keep the machine awake until Allow Sleep is run.", + ), + CommandSpec( + "AC_allow_sleep", "Shell", "Allow Machine to Sleep", + fields=(), + description="Release a previously-started keep-awake.", + ), + CommandSpec( + "AC_get_volume", "Shell", "Get System Volume", + fields=(), + description="Read the master volume percent and mute state.", + ), + CommandSpec( + "AC_set_volume", "Shell", "Set System Volume", + fields=( + FieldSpec("level", FieldType.INT, default=50, + placeholder="volume percent 0-100"), + ), + description="Set the master volume to level percent (clamped 0-100).", + ), + CommandSpec( + "AC_change_volume", "Shell", "Change System Volume", + fields=( + FieldSpec("delta", FieldType.INT, default=10, + placeholder="percent delta (may be negative)"), + ), + description="Add delta percent to the master volume (clamped 0-100).", + ), + CommandSpec( + "AC_set_mute", "Shell", "Set Mute", + fields=( + FieldSpec("muted", FieldType.BOOL, optional=True, default=True), + ), + description="Mute or unmute the master output.", + ), + CommandSpec( + "AC_toggle_mute", "Shell", "Toggle Mute", + fields=(), + description="Flip the master mute flag.", + ), + CommandSpec( + "AC_lock_session", "Shell", "Lock Workstation", + fields=(), + description="Lock the workstation now (interrupts the session).", + ), + CommandSpec( + "AC_plan_lock_session", "Shell", "Plan Lock Workstation", + fields=(), + description="Describe how the workstation would be locked (pure).", + ), + CommandSpec( + "AC_wait_for_unlock", "Shell", "Wait for Unlock", + fields=( + FieldSpec("timeout", FieldType.FLOAT, optional=True, default=30.0, + placeholder=_TIMEOUT_PLACEHOLDER), + FieldSpec("interval", FieldType.FLOAT, optional=True, default=0.5, + placeholder=_POLL_PLACEHOLDER), + ), + description="Block until the session is unlocked or timeout.", + ), + CommandSpec( + "AC_classify_lock_transitions", "Shell", "Classify Lock Transitions", + fields=( + FieldSpec("states", FieldType.STRING, + placeholder="JSON list of booleans"), + ), + description="Reduce lock-state samples to lock / unlock events.", + ), + CommandSpec( + "AC_ime_state", "Shell", "IME State", + fields=(), + description="Read the focused window's live IME composition state.", + ), + CommandSpec( + "AC_is_composing", "Shell", "Is IME Composing", + fields=(), + description="True while the IME has an uncommitted composition.", + ), + CommandSpec( + "AC_wait_for_composition_commit", "Shell", "Wait for IME Commit", + fields=( + FieldSpec("timeout", FieldType.FLOAT, optional=True, default=5.0, + placeholder=_TIMEOUT_PLACEHOLDER), + FieldSpec("interval", FieldType.FLOAT, optional=True, default=0.1, + placeholder=_POLL_PLACEHOLDER), + ), + description="Block until the IME finishes composing or timeout.", + ), + CommandSpec( + "AC_decode_conversion_mode", "Shell", "Decode IME Conversion Mode", + fields=( + FieldSpec("flags", FieldType.INT, default=0, + placeholder="IMM32 conversion bitmask"), + ), + description="Decode an IMM32 conversion bitmask into named flags.", + ), + CommandSpec( + "AC_retry_delay", "Flow", "Retry Backoff Delay", + fields=( + FieldSpec("attempt", FieldType.INT, default=1, + placeholder="1-based retry attempt"), + FieldSpec("base", FieldType.FLOAT, optional=True, default=0.1), + FieldSpec("max_delay", FieldType.FLOAT, optional=True, + default=5.0), + FieldSpec("multiplier", FieldType.FLOAT, optional=True, + default=2.0), + FieldSpec("jitter", FieldType.STRING, optional=True, + default="none", placeholder="none / full / equal"), + ), + description="Capped exponential backoff delay before a retry attempt.", + ), + CommandSpec( + "AC_plan_retry_delays", "Flow", "Plan Retry Delays", + fields=( + FieldSpec("attempts", FieldType.INT, default=5, + placeholder="number of retries"), + FieldSpec("base", FieldType.FLOAT, optional=True, default=0.1), + FieldSpec("max_delay", FieldType.FLOAT, optional=True, + default=5.0), + FieldSpec("multiplier", FieldType.FLOAT, optional=True, + default=2.0), + FieldSpec("jitter", FieldType.STRING, optional=True, + default="none", placeholder="none / full / equal"), + ), + description="The backoff delay schedule for the first N retries.", + ), + CommandSpec( + "AC_compare_field_value", "Flow", "Compare Field Value", + fields=( + FieldSpec("expected", FieldType.STRING, placeholder="expected"), + FieldSpec("actual", FieldType.STRING, placeholder="actual"), + FieldSpec("mode", FieldType.STRING, optional=True, default="exact", + placeholder="exact / trim / ci / normalized / contains"), + ), + description="Compare expected vs actual field value under a mode.", + ), + CommandSpec( + "AC_verify_field_value", "Flow", "Verify Field Value", + fields=( + FieldSpec("expected", FieldType.STRING, placeholder="expected"), + FieldSpec("name", FieldType.STRING, optional=True, + placeholder="control name"), + FieldSpec("role", FieldType.STRING, optional=True, + placeholder="control role"), + FieldSpec("app_name", FieldType.STRING, optional=True, + placeholder="app name"), + FieldSpec("automation_id", FieldType.STRING, optional=True, + placeholder="automation id"), + FieldSpec("mode", FieldType.STRING, optional=True, default="exact", + placeholder="exact / trim / ci / normalized / contains"), + ), + description="Read a control's value back and confirm it equals expected.", + ), + CommandSpec( + "AC_adaptive_timeout", "Flow", "Adaptive Timeout", + fields=( + FieldSpec("durations", FieldType.STRING, + placeholder="JSON list of durations (seconds)"), + FieldSpec("percentile_q", FieldType.FLOAT, optional=True, + default=95.0), + FieldSpec("factor", FieldType.FLOAT, optional=True, default=1.5), + FieldSpec("min_s", FieldType.FLOAT, optional=True, default=1.0), + FieldSpec("max_s", FieldType.FLOAT, optional=True, default=60.0), + ), + description="Recommend a wait timeout from observed step durations.", + ), + CommandSpec( + "AC_timeout_stats", "Flow", "Timeout Stats", + fields=( + FieldSpec("durations", FieldType.STRING, + placeholder="JSON list of durations (seconds)"), + FieldSpec("percentile_q", FieldType.FLOAT, optional=True, + default=95.0), + FieldSpec("factor", FieldType.FLOAT, optional=True, default=1.5), + FieldSpec("min_s", FieldType.FLOAT, optional=True, default=1.0), + FieldSpec("max_s", FieldType.FLOAT, optional=True, default=60.0), + ), + description="Timeout recommendation plus percentiles and clamp flags.", + ), + CommandSpec( + "AC_ensure_field_value", "Flow", "Ensure Field Value", + fields=( + FieldSpec("desired", FieldType.STRING, placeholder="desired value"), + FieldSpec("name", FieldType.STRING, optional=True, + placeholder="control name"), + FieldSpec("role", FieldType.STRING, optional=True, + placeholder="control role"), + FieldSpec("app_name", FieldType.STRING, optional=True, + placeholder="app name"), + FieldSpec("automation_id", FieldType.STRING, optional=True, + placeholder="automation id"), + FieldSpec("attempts", FieldType.INT, optional=True, default=2), + ), + description="Idempotently set a control's value (no-op if already set).", + ), + CommandSpec( + "AC_wait_until_app_idle", "Flow", "Wait Until App Idle", + fields=( + FieldSpec("quiet_samples", FieldType.INT, optional=True, + default=3, placeholder="consecutive idle polls"), + FieldSpec("timeout", FieldType.FLOAT, optional=True, default=10.0, + placeholder=_TIMEOUT_PLACEHOLDER), + FieldSpec("interval", FieldType.FLOAT, optional=True, default=0.1, + placeholder=_POLL_PLACEHOLDER), + ), + description="Block until the app's busy cursor settles idle or timeout.", + ), + CommandSpec( + "AC_idle_point", "Flow", "Idle Point", + fields=( + FieldSpec("busy_samples", FieldType.STRING, + placeholder="JSON list of busy booleans"), + FieldSpec("quiet_samples", FieldType.INT, optional=True, default=3), - name, FieldSpec("claim", FieldType.INT, optional=True)), - description="Fail an item; application errors retry, business don't.", - )) - specs.append(CommandSpec( - "AC_queue_stats", "Queue", "Queue: Stats", - fields=(db, name), - description="Per-status counts for a work queue.", - )) - specs.append(CommandSpec( - "AC_shell_command", "Shell", "Shell Command", - fields=(FieldSpec("shell_command", FieldType.STRING),), - )) - specs.append(CommandSpec( - "AC_open_path", "Shell", "Open File / URL (default app)", - fields=( - FieldSpec("target", FieldType.STRING, - placeholder="report.pdf or https://example.com"), - FieldSpec("verb", FieldType.STRING, optional=True, default="open", - placeholder="open / print / edit"), + ), + description="Index where a busy/idle series first settles idle.", + ), + CommandSpec( + "AC_act_in_view", "Flow", "Act In View (scroll + click)", + fields=( + FieldSpec("target", FieldType.STRING, + placeholder="template path or text"), + FieldSpec("kind", FieldType.STRING, optional=True, + default="image", placeholder="image / text"), + FieldSpec("direction", FieldType.STRING, optional=True, + default="down", placeholder="up / down"), + FieldSpec("max_scrolls", FieldType.INT, optional=True, default=10), + FieldSpec("scroll_amount", FieldType.INT, optional=True, + default=3), + FieldSpec("button", FieldType.STRING, optional=True, + default="left"), + ), + description="Scroll a target into view, then click it when actionable.", ), - description="Open a file with its default app, or a URL in the browser.", - )) - specs.append(CommandSpec( - "AC_plan_open", "Shell", "Plan Open (classify)", - fields=( - FieldSpec("target", FieldType.STRING), - FieldSpec("verb", FieldType.STRING, optional=True, default="open"), + CommandSpec( + "AC_act_with_mode", "Flow", "Click with Mode (auto/trial/force)", + fields=( + FieldSpec("x", FieldType.INT, placeholder="x"), + FieldSpec("y", FieldType.INT, placeholder="y"), + FieldSpec("mode", FieldType.STRING, optional=True, default="auto", + placeholder="auto / trial / force"), + FieldSpec("button", FieldType.STRING, optional=True, + default="left"), + ), + description="Click a point under an action mode (gate / dry-run / force).", ), - description="Classify how a file/URL would be opened (pure, no launch).", - )) - specs.append(CommandSpec( - "AC_idle_seconds", "Shell", "Idle Seconds", - fields=(), - description="Seconds since the last user keyboard / mouse input.", - )) - specs.append(CommandSpec( - "AC_is_idle", "Shell", "Is User Idle", - fields=( - FieldSpec("threshold", FieldType.FLOAT, default=300.0, - placeholder="idle seconds threshold"), + CommandSpec( + "AC_simulate_cvd", "Image", "Simulate Colour-Vision Deficiency", + fields=( + FieldSpec("rgb", FieldType.STRING, placeholder=_RGB_PLACEHOLDER), + FieldSpec("kind", FieldType.STRING, optional=True, + default="deuteranopia", + placeholder="protanopia / deuteranopia / tritanopia"), + FieldSpec("severity", FieldType.FLOAT, optional=True, default=1.0), + ), + description="Map an RGB colour through a CVD simulation matrix.", ), - description="True if the user has been idle for >= threshold seconds.", - )) - specs.append(CommandSpec( - "AC_plan_keep_awake", "Shell", "Plan Keep Awake", - fields=( - FieldSpec("display", FieldType.BOOL, optional=True, default=True), - FieldSpec("system", FieldType.BOOL, optional=True, default=True), + CommandSpec( + "AC_colors_collide", "Image", "Colours Collide Under CVD", + fields=( + FieldSpec("left", FieldType.STRING, placeholder=_RGB_PLACEHOLDER), + FieldSpec("right", FieldType.STRING, placeholder=_RGB_PLACEHOLDER), + FieldSpec("kind", FieldType.STRING, optional=True, + default="deuteranopia", + placeholder="protanopia / deuteranopia / tritanopia"), + FieldSpec("severity", FieldType.FLOAT, optional=True, default=1.0), + FieldSpec("threshold", FieldType.FLOAT, optional=True, + default=40.0), + ), + description="Whether two colours become confusable under a CVD type.", ), - description="Describe a keep-awake request (pure, no OS call).", - )) - specs.append(CommandSpec( - "AC_keep_awake_on", "Shell", "Keep Machine Awake", - fields=( - FieldSpec("display", FieldType.BOOL, optional=True, default=True), - FieldSpec("system", FieldType.BOOL, optional=True, default=True), + CommandSpec( + "AC_place_labels", "Image", "Place Mark Labels", + fields=( + FieldSpec("marks", FieldType.STRING, + placeholder="JSON list of {id, bbox}"), + FieldSpec("label_width", FieldType.INT, optional=True, default=22), + FieldSpec("label_height", FieldType.INT, optional=True, + default=16), + FieldSpec("bounds", FieldType.STRING, optional=True, + placeholder=_SIZE_PLACEHOLDER), + ), + description="Lay out non-overlapping Set-of-Marks label boxes.", ), - description="Keep the machine awake until Allow Sleep is run.", - )) - specs.append(CommandSpec( - "AC_allow_sleep", "Shell", "Allow Machine to Sleep", - fields=(), - description="Release a previously-started keep-awake.", - )) - specs.append(CommandSpec( - "AC_get_volume", "Shell", "Get System Volume", - fields=(), - description="Read the master volume percent and mute state.", - )) - specs.append(CommandSpec( - "AC_set_volume", "Shell", "Set System Volume", - fields=( - FieldSpec("level", FieldType.INT, default=50, - placeholder="volume percent 0-100"), + CommandSpec( + "AC_label_color", "Image", "Label Colour for Background", + fields=( + FieldSpec("background", FieldType.STRING, placeholder=_RGB_PLACEHOLDER), + ), + description="Higher-contrast label colour (black/white) for a background.", ), - description="Set the master volume to level percent (clamped 0-100).", - )) - specs.append(CommandSpec( - "AC_change_volume", "Shell", "Change System Volume", - fields=( - FieldSpec("delta", FieldType.INT, default=10, - placeholder="percent delta (may be negative)"), + CommandSpec( + "AC_grade_contrast", "Image", "Grade Contrast (WCAG)", + fields=( + FieldSpec("foreground", FieldType.STRING, placeholder=_RGB_PLACEHOLDER), + FieldSpec("background", FieldType.STRING, placeholder=_RGB_PLACEHOLDER), + ), + description="Grade a foreground/background colour pair against WCAG.", ), - description="Add delta percent to the master volume (clamped 0-100).", - )) - specs.append(CommandSpec( - "AC_set_mute", "Shell", "Set Mute", - fields=( - FieldSpec("muted", FieldType.BOOL, optional=True, default=True), + CommandSpec( + "AC_dominant_pair", "Image", "Dominant FG/BG Colours", + fields=( + FieldSpec("pixels", FieldType.STRING, + placeholder="JSON list of [r, g, b] pixels"), + ), + description="Split sampled pixels into dominant foreground/background.", ), - description="Mute or unmute the master output.", - )) - specs.append(CommandSpec( - "AC_toggle_mute", "Shell", "Toggle Mute", - fields=(), - description="Flip the master mute flag.", - )) - specs.append(CommandSpec( - "AC_lock_session", "Shell", "Lock Workstation", - fields=(), - description="Lock the workstation now (interrupts the session).", - )) - specs.append(CommandSpec( - "AC_plan_lock_session", "Shell", "Plan Lock Workstation", - fields=(), - description="Describe how the workstation would be locked (pure).", - )) - specs.append(CommandSpec( - "AC_wait_for_unlock", "Shell", "Wait for Unlock", - fields=( - FieldSpec("timeout", FieldType.FLOAT, optional=True, default=30.0, - placeholder="timeout seconds"), - FieldSpec("interval", FieldType.FLOAT, optional=True, default=0.5, - placeholder="poll interval seconds"), + CommandSpec( + "AC_region_contrast", "Image", "Region Text Contrast", + fields=( + FieldSpec("region", FieldType.STRING, optional=True, + placeholder=_REGION_PLACEHOLDER), + ), + description="Sample a screen region and grade its text contrast.", ), - description="Block until the session is unlocked or timeout.", - )) - specs.append(CommandSpec( - "AC_classify_lock_transitions", "Shell", "Classify Lock Transitions", - fields=( - FieldSpec("states", FieldType.STRING, - placeholder="JSON list of booleans"), + CommandSpec( + "AC_match_theme", "Image", "Match (Theme-Invariant)", + fields=( + FieldSpec("template", FieldType.STRING, + placeholder="template image path"), + FieldSpec("region", FieldType.STRING, optional=True, + placeholder=_RECT4_PLACEHOLDER), + FieldSpec("method", FieldType.STRING, optional=True, + default="sobel", + placeholder="sobel / laplacian / zscore"), + FieldSpec("min_score", FieldType.FLOAT, optional=True, + default=0.5), + ), + description="Locate a template across a light/dark theme flip.", ), - description="Reduce lock-state samples to lock / unlock events.", - )) - specs.append(CommandSpec( - "AC_ime_state", "Shell", "IME State", - fields=(), - description="Read the focused window's live IME composition state.", - )) - specs.append(CommandSpec( - "AC_is_composing", "Shell", "Is IME Composing", - fields=(), - description="True while the IME has an uncommitted composition.", - )) - specs.append(CommandSpec( - "AC_wait_for_composition_commit", "Shell", "Wait for IME Commit", - fields=( - FieldSpec("timeout", FieldType.FLOAT, optional=True, default=5.0, - placeholder="timeout seconds"), - FieldSpec("interval", FieldType.FLOAT, optional=True, default=0.1, - placeholder="poll interval seconds"), + CommandSpec( + "AC_rank_changes", "Image", "Rank Changed Boxes", + fields=( + FieldSpec("scored_boxes", FieldType.STRING, + placeholder="JSON list of {box, score}"), + FieldSpec("threshold", FieldType.FLOAT, optional=True, + default=0.1), + ), + description="Rank scored element boxes by how much they changed.", ), - description="Block until the IME finishes composing or timeout.", - )) - specs.append(CommandSpec( - "AC_decode_conversion_mode", "Shell", "Decode IME Conversion Mode", - fields=( - FieldSpec("flags", FieldType.INT, default=0, - placeholder="IMM32 conversion bitmask"), + CommandSpec( + "AC_localize_changes", "Image", "Localize Changed Elements", + fields=( + FieldSpec("reference", FieldType.STRING, + placeholder="reference image path"), + FieldSpec("boxes", FieldType.STRING, + placeholder="JSON list of [x, y, w, h]"), + FieldSpec("current", FieldType.STRING, optional=True, + placeholder="current image path (else screen)"), + FieldSpec("threshold", FieldType.FLOAT, optional=True, + default=0.1), + FieldSpec("region", FieldType.STRING, optional=True, + placeholder=_RECT4_PLACEHOLDER), + ), + description="Rank which element boxes changed between two frames.", ), - description="Decode an IMM32 conversion bitmask into named flags.", - )) - specs.append(CommandSpec( - "AC_retry_delay", "Flow", "Retry Backoff Delay", - fields=( - FieldSpec("attempt", FieldType.INT, default=1, - placeholder="1-based retry attempt"), - FieldSpec("base", FieldType.FLOAT, optional=True, default=0.1), - FieldSpec("max_delay", FieldType.FLOAT, optional=True, - default=5.0), - FieldSpec("multiplier", FieldType.FLOAT, optional=True, - default=2.0), - FieldSpec("jitter", FieldType.STRING, optional=True, - default="none", placeholder="none / full / equal"), - ), - description="Capped exponential backoff delay before a retry attempt.", - )) - specs.append(CommandSpec( - "AC_plan_retry_delays", "Flow", "Plan Retry Delays", - fields=( - FieldSpec("attempts", FieldType.INT, default=5, - placeholder="number of retries"), - FieldSpec("base", FieldType.FLOAT, optional=True, default=0.1), - FieldSpec("max_delay", FieldType.FLOAT, optional=True, - default=5.0), - FieldSpec("multiplier", FieldType.FLOAT, optional=True, - default=2.0), - FieldSpec("jitter", FieldType.STRING, optional=True, - default="none", placeholder="none / full / equal"), - ), - description="The backoff delay schedule for the first N retries.", - )) - specs.append(CommandSpec( - "AC_compare_field_value", "Flow", "Compare Field Value", - fields=( - FieldSpec("expected", FieldType.STRING, placeholder="expected"), - FieldSpec("actual", FieldType.STRING, placeholder="actual"), - FieldSpec("mode", FieldType.STRING, optional=True, default="exact", - placeholder="exact / trim / ci / normalized / contains"), + CommandSpec( + "AC_classify_widget", "Image", "Classify Widget (features)", + fields=( + FieldSpec("features", FieldType.STRING, + placeholder="JSON {aspect, circularity, fill}"), + ), + description="Map geometric features to a widget type.", ), - description="Compare expected vs actual field value under a mode.", - )) - specs.append(CommandSpec( - "AC_verify_field_value", "Flow", "Verify Field Value", - fields=( - FieldSpec("expected", FieldType.STRING, placeholder="expected"), - FieldSpec("name", FieldType.STRING, optional=True, - placeholder="control name"), - FieldSpec("role", FieldType.STRING, optional=True, - placeholder="control role"), - FieldSpec("app_name", FieldType.STRING, optional=True, - placeholder="app name"), - FieldSpec("automation_id", FieldType.STRING, optional=True, - placeholder="automation id"), - FieldSpec("mode", FieldType.STRING, optional=True, default="exact", - placeholder="exact / trim / ci / normalized / contains"), - ), - description="Read a control's value back and confirm it equals expected.", - )) - specs.append(CommandSpec( - "AC_adaptive_timeout", "Flow", "Adaptive Timeout", - fields=( - FieldSpec("durations", FieldType.STRING, - placeholder="JSON list of durations (seconds)"), - FieldSpec("percentile_q", FieldType.FLOAT, optional=True, - default=95.0), - FieldSpec("factor", FieldType.FLOAT, optional=True, default=1.5), - FieldSpec("min_s", FieldType.FLOAT, optional=True, default=1.0), - FieldSpec("max_s", FieldType.FLOAT, optional=True, default=60.0), - ), - description="Recommend a wait timeout from observed step durations.", - )) - specs.append(CommandSpec( - "AC_timeout_stats", "Flow", "Timeout Stats", - fields=( - FieldSpec("durations", FieldType.STRING, - placeholder="JSON list of durations (seconds)"), - FieldSpec("percentile_q", FieldType.FLOAT, optional=True, - default=95.0), - FieldSpec("factor", FieldType.FLOAT, optional=True, default=1.5), - FieldSpec("min_s", FieldType.FLOAT, optional=True, default=1.0), - FieldSpec("max_s", FieldType.FLOAT, optional=True, default=60.0), - ), - description="Timeout recommendation plus percentiles and clamp flags.", - )) - specs.append(CommandSpec( - "AC_ensure_field_value", "Flow", "Ensure Field Value", - fields=( - FieldSpec("desired", FieldType.STRING, placeholder="desired value"), - FieldSpec("name", FieldType.STRING, optional=True, - placeholder="control name"), - FieldSpec("role", FieldType.STRING, optional=True, - placeholder="control role"), - FieldSpec("app_name", FieldType.STRING, optional=True, - placeholder="app name"), - FieldSpec("automation_id", FieldType.STRING, optional=True, - placeholder="automation id"), - FieldSpec("attempts", FieldType.INT, optional=True, default=2), - ), - description="Idempotently set a control's value (no-op if already set).", - )) - specs.append(CommandSpec( - "AC_wait_until_app_idle", "Flow", "Wait Until App Idle", - fields=( - FieldSpec("quiet_samples", FieldType.INT, optional=True, - default=3, placeholder="consecutive idle polls"), - FieldSpec("timeout", FieldType.FLOAT, optional=True, default=10.0, - placeholder="timeout seconds"), - FieldSpec("interval", FieldType.FLOAT, optional=True, default=0.1, - placeholder="poll interval seconds"), - ), - description="Block until the app's busy cursor settles idle or timeout.", - )) - specs.append(CommandSpec( - "AC_idle_point", "Flow", "Idle Point", - fields=( - FieldSpec("busy_samples", FieldType.STRING, - placeholder="JSON list of busy booleans"), - FieldSpec("quiet_samples", FieldType.INT, optional=True, - default=3), + CommandSpec( + "AC_classify_icon", "Image", "Classify Icon (box)", + fields=( + FieldSpec("source", FieldType.STRING, placeholder="image path"), + FieldSpec("box", FieldType.STRING, placeholder=_RECT4_PLACEHOLDER), + ), + description="Classify the widget in an image box from its pixels.", ), - description="Index where a busy/idle series first settles idle.", - )) - specs.append(CommandSpec( - "AC_act_in_view", "Flow", "Act In View (scroll + click)", - fields=( - FieldSpec("target", FieldType.STRING, - placeholder="template path or text"), - FieldSpec("kind", FieldType.STRING, optional=True, - default="image", placeholder="image / text"), - FieldSpec("direction", FieldType.STRING, optional=True, - default="down", placeholder="up / down"), - FieldSpec("max_scrolls", FieldType.INT, optional=True, default=10), - FieldSpec("scroll_amount", FieldType.INT, optional=True, - default=3), - FieldSpec("button", FieldType.STRING, optional=True, - default="left"), - ), - description="Scroll a target into view, then click it when actionable.", - )) - specs.append(CommandSpec( - "AC_act_with_mode", "Flow", "Click with Mode (auto/trial/force)", - fields=( - FieldSpec("x", FieldType.INT, placeholder="x"), - FieldSpec("y", FieldType.INT, placeholder="y"), - FieldSpec("mode", FieldType.STRING, optional=True, default="auto", - placeholder="auto / trial / force"), - FieldSpec("button", FieldType.STRING, optional=True, - default="left"), - ), - description="Click a point under an action mode (gate / dry-run / force).", - )) - specs.append(CommandSpec( - "AC_simulate_cvd", "Image", "Simulate Colour-Vision Deficiency", - fields=( - FieldSpec("rgb", FieldType.STRING, placeholder="[r, g, b]"), - FieldSpec("kind", FieldType.STRING, optional=True, - default="deuteranopia", - placeholder="protanopia / deuteranopia / tritanopia"), - FieldSpec("severity", FieldType.FLOAT, optional=True, default=1.0), + CommandSpec( + "AC_propose_elements", "Image", "Propose Elements (template-free)", + fields=( + FieldSpec("region", FieldType.STRING, optional=True, + placeholder=_RECT4_PLACEHOLDER), + FieldSpec("min_area", FieldType.INT, optional=True, default=80), + FieldSpec("iou_threshold", FieldType.FLOAT, optional=True, + default=0.5), + ), + description="Propose text/widget element boxes from raw screen pixels.", ), - description="Map an RGB colour through a CVD simulation matrix.", - )) - specs.append(CommandSpec( - "AC_colors_collide", "Image", "Colours Collide Under CVD", - fields=( - FieldSpec("left", FieldType.STRING, placeholder="[r, g, b]"), - FieldSpec("right", FieldType.STRING, placeholder="[r, g, b]"), - FieldSpec("kind", FieldType.STRING, optional=True, - default="deuteranopia", - placeholder="protanopia / deuteranopia / tritanopia"), - FieldSpec("severity", FieldType.FLOAT, optional=True, default=1.0), - FieldSpec("threshold", FieldType.FLOAT, optional=True, - default=40.0), - ), - description="Whether two colours become confusable under a CVD type.", - )) - specs.append(CommandSpec( - "AC_place_labels", "Image", "Place Mark Labels", - fields=( - FieldSpec("marks", FieldType.STRING, - placeholder="JSON list of {id, bbox}"), - FieldSpec("label_width", FieldType.INT, optional=True, default=22), - FieldSpec("label_height", FieldType.INT, optional=True, - default=16), - FieldSpec("bounds", FieldType.STRING, optional=True, - placeholder="[width, height]"), - ), - description="Lay out non-overlapping Set-of-Marks label boxes.", - )) - specs.append(CommandSpec( - "AC_label_color", "Image", "Label Colour for Background", - fields=( - FieldSpec("background", FieldType.STRING, placeholder="[r, g, b]"), + CommandSpec( + "AC_tag_kinds", "Image", "Tag Element Kinds", + fields=( + FieldSpec("elements", FieldType.STRING, + placeholder="JSON list of fused boxes"), + ), + description="Label fused element boxes text/widget by source.", ), - description="Higher-contrast label colour (black/white) for a background.", - )) - specs.append(CommandSpec( - "AC_grade_contrast", "Image", "Grade Contrast (WCAG)", - fields=( - FieldSpec("foreground", FieldType.STRING, placeholder="[r, g, b]"), - FieldSpec("background", FieldType.STRING, placeholder="[r, g, b]"), + CommandSpec( + "AC_normalize_ext", "Shell", "Normalize Extension", + fields=( + FieldSpec("target", FieldType.STRING, + placeholder="report.pdf or .pdf or pdf"), + ), + description="Lowercased file extension (with dot) of a path / ext.", ), - description="Grade a foreground/background colour pair against WCAG.", - )) - specs.append(CommandSpec( - "AC_dominant_pair", "Image", "Dominant FG/BG Colours", - fields=( - FieldSpec("pixels", FieldType.STRING, - placeholder="JSON list of [r, g, b] pixels"), + CommandSpec( + "AC_file_association", "Shell", "File Association (default app)", + fields=( + FieldSpec("target", FieldType.STRING, + placeholder="report.pdf or .pdf"), + ), + description="Which app is registered to open a file type (Windows).", ), - description="Split sampled pixels into dominant foreground/background.", - )) - specs.append(CommandSpec( - "AC_region_contrast", "Image", "Region Text Contrast", - fields=( - FieldSpec("region", FieldType.STRING, optional=True, - placeholder=_REGION_PLACEHOLDER), + CommandSpec( + "AC_take_golden", "Report", "Capture Golden Image", + fields=(FieldSpec("path", FieldType.FILE_PATH),), + description="Capture and save a baseline image for visual regression.", ), - description="Sample a screen region and grade its text contrast.", - )) - specs.append(CommandSpec( - "AC_match_theme", "Image", "Match (Theme-Invariant)", - fields=( - FieldSpec("template", FieldType.STRING, - placeholder="template image path"), - FieldSpec("region", FieldType.STRING, optional=True, - placeholder="[x, y, w, h]"), - FieldSpec("method", FieldType.STRING, optional=True, - default="sobel", - placeholder="sobel / laplacian / zscore"), - FieldSpec("min_score", FieldType.FLOAT, optional=True, - default=0.5), - ), - description="Locate a template across a light/dark theme flip.", - )) - specs.append(CommandSpec( - "AC_rank_changes", "Image", "Rank Changed Boxes", - fields=( - FieldSpec("scored_boxes", FieldType.STRING, - placeholder="JSON list of {box, score}"), - FieldSpec("threshold", FieldType.FLOAT, optional=True, - default=0.1), + CommandSpec( + "AC_assert_visual", "Report", "Assert Visual (Golden)", + fields=( + FieldSpec("golden_path", FieldType.FILE_PATH), + FieldSpec("tolerance", FieldType.FLOAT, optional=True, default=0.0, + min_value=0.0), + FieldSpec("per_pixel_threshold", FieldType.INT, optional=True, + default=16, min_value=0), + FieldSpec("diff_path", FieldType.FILE_PATH, optional=True), + ), + description=("Compare the screen to a golden image; first run creates " + "the baseline. Use the JSON view for a region / masks."), ), - description="Rank scored element boxes by how much they changed.", - )) - specs.append(CommandSpec( - "AC_localize_changes", "Image", "Localize Changed Elements", - fields=( - FieldSpec("reference", FieldType.STRING, - placeholder="reference image path"), - FieldSpec("boxes", FieldType.STRING, - placeholder="JSON list of [x, y, w, h]"), - FieldSpec("current", FieldType.STRING, optional=True, - placeholder="current image path (else screen)"), - FieldSpec("threshold", FieldType.FLOAT, optional=True, - default=0.1), - FieldSpec("region", FieldType.STRING, optional=True, - placeholder="[x, y, w, h]"), - ), - description="Rank which element boxes changed between two frames.", - )) - specs.append(CommandSpec( - "AC_classify_widget", "Image", "Classify Widget (features)", - fields=( - FieldSpec("features", FieldType.STRING, - placeholder="JSON {aspect, circularity, fill}"), + CommandSpec( + "AC_run_state_machine", "Flow", "Run State Machine", + description=("Run a finite-state-machine; configure the 'spec' " + "{initial, states} dict in the JSON view."), ), - description="Map geometric features to a widget type.", - )) - specs.append(CommandSpec( - "AC_classify_icon", "Image", "Classify Icon (box)", - fields=( - FieldSpec("source", FieldType.STRING, placeholder="image path"), - FieldSpec("box", FieldType.STRING, placeholder="[x, y, w, h]"), + CommandSpec( + "AC_shell_to_var", "Shell", "Shell Output into Variable", + fields=( + FieldSpec("command", FieldType.STRING), + FieldSpec("var", FieldType.STRING, default="shell_output"), + FieldSpec("timeout", FieldType.FLOAT, optional=True, default=30.0), + FieldSpec("encoding", FieldType.STRING, optional=True), + ), + description="Run a command and store its stdout in a flow variable.", ), - description="Classify the widget in an image box from its pixels.", - )) - specs.append(CommandSpec( - "AC_propose_elements", "Image", "Propose Elements (template-free)", - fields=( - FieldSpec("region", FieldType.STRING, optional=True, - placeholder="[x, y, w, h]"), - FieldSpec("min_area", FieldType.INT, optional=True, default=80), - FieldSpec("iou_threshold", FieldType.FLOAT, optional=True, - default=0.5), + CommandSpec( + "AC_read_file_to_var", "Shell", "Read File into Variable", + fields=( + FieldSpec("path", FieldType.FILE_PATH), + FieldSpec("var", FieldType.STRING, default="file_content"), + FieldSpec("encoding", FieldType.STRING, optional=True, + default="utf-8"), + ), + description="Read a file's text content into a flow variable.", ), - description="Propose text/widget element boxes from raw screen pixels.", - )) - specs.append(CommandSpec( - "AC_tag_kinds", "Image", "Tag Element Kinds", - fields=( - FieldSpec("elements", FieldType.STRING, - placeholder="JSON list of fused boxes"), + CommandSpec( + "AC_sql_to_var", "Report", "SQL Query into Variable", + fields=( + FieldSpec("database", FieldType.FILE_PATH), + FieldSpec("query", FieldType.STRING, + placeholder="SELECT name FROM users WHERE id = ?"), + FieldSpec("var", FieldType.STRING, default="sql_result"), + FieldSpec("fetch", FieldType.ENUM, + choices=("all", "one", "scalar"), + optional=True, default="all"), + ), + description=("Run a read-only SQLite SELECT; store rows / a row / a " + "scalar in a variable. Bind values via params (JSON view)."), ), - description="Label fused element boxes text/widget by source.", - )) - specs.append(CommandSpec( - "AC_normalize_ext", "Shell", "Normalize Extension", - fields=( - FieldSpec("target", FieldType.STRING, - placeholder="report.pdf or .pdf or pdf"), + CommandSpec( + "AC_assert_db", "Report", "Assert SQL Result", + fields=( + FieldSpec("database", FieldType.FILE_PATH), + FieldSpec("query", FieldType.STRING, + placeholder="SELECT COUNT(*) FROM users"), + FieldSpec("op", FieldType.ENUM, + choices=("eq", "ne", "lt", "le", "gt", "ge", + "contains", "startswith", "endswith"), + optional=True, default="eq"), + FieldSpec("expected", FieldType.STRING, optional=True), + ), + description=("Run a scalar SELECT and assert its value (use the JSON " + "view for non-string expected values / params)."), ), - description="Lowercased file extension (with dot) of a path / ext.", - )) - specs.append(CommandSpec( - "AC_file_association", "Shell", "File Association (default app)", - fields=( - FieldSpec("target", FieldType.STRING, - placeholder="report.pdf or .pdf"), + CommandSpec( + "AC_http_to_var", "Report", "HTTP Request into Variable", + fields=( + FieldSpec("url", FieldType.STRING, placeholder="https://..."), + FieldSpec("method", FieldType.ENUM, + choices=("GET", "POST", "PUT", "PATCH", "DELETE", "HEAD"), + optional=True, default="GET"), + FieldSpec("var", FieldType.STRING, default="http_response"), + FieldSpec("json_path", FieldType.STRING, optional=True, + placeholder="data.0.name"), + FieldSpec("timeout", FieldType.FLOAT, optional=True, default=30.0), + ), + description="Request a URL; store the body or a JSON field in a variable.", ), - description="Which app is registered to open a file type (Windows).", - )) - specs.append(CommandSpec( - "AC_take_golden", "Report", "Capture Golden Image", - fields=(FieldSpec("path", FieldType.FILE_PATH),), - description="Capture and save a baseline image for visual regression.", - )) - specs.append(CommandSpec( - "AC_assert_visual", "Report", "Assert Visual (Golden)", - fields=( - FieldSpec("golden_path", FieldType.FILE_PATH), - FieldSpec("tolerance", FieldType.FLOAT, optional=True, default=0.0, - min_value=0.0), - FieldSpec("per_pixel_threshold", FieldType.INT, optional=True, - default=16, min_value=0), - FieldSpec("diff_path", FieldType.FILE_PATH, optional=True), - ), - description=("Compare the screen to a golden image; first run creates " - "the baseline. Use the JSON view for a region / masks."), - )) - specs.append(CommandSpec( - "AC_run_state_machine", "Flow", "Run State Machine", - description=("Run a finite-state-machine; configure the 'spec' " - "{initial, states} dict in the JSON view."), - )) - specs.append(CommandSpec( - "AC_shell_to_var", "Shell", "Shell Output into Variable", - fields=( - FieldSpec("command", FieldType.STRING), - FieldSpec("var", FieldType.STRING, default="shell_output"), - FieldSpec("timeout", FieldType.FLOAT, optional=True, default=30.0), - FieldSpec("encoding", FieldType.STRING, optional=True), + CommandSpec( + "AC_pdf_to_var", "Report", "PDF Text into Variable", + fields=( + FieldSpec("path", FieldType.FILE_PATH), + FieldSpec("var", FieldType.STRING, default="pdf_text"), + FieldSpec("page", FieldType.INT, optional=True, min_value=1), + ), + description="Extract a PDF's text (all pages or one) into a variable.", ), - description="Run a command and store its stdout in a flow variable.", - )) - specs.append(CommandSpec( - "AC_read_file_to_var", "Shell", "Read File into Variable", - fields=( - FieldSpec("path", FieldType.FILE_PATH), - FieldSpec("var", FieldType.STRING, default="file_content"), - FieldSpec("encoding", FieldType.STRING, optional=True, - default="utf-8"), + CommandSpec( + "AC_assert_pdf_text", "Report", "Assert PDF Text", + fields=( + FieldSpec("path", FieldType.FILE_PATH), + FieldSpec("text", FieldType.STRING), + FieldSpec("present", FieldType.BOOL, optional=True, default=True), + FieldSpec("page", FieldType.INT, optional=True, min_value=1), + FieldSpec("case_sensitive", FieldType.BOOL, optional=True, + default=True), + ), + description="Assert text is present (or absent) in a PDF document.", ), - description="Read a file's text content into a flow variable.", - )) - specs.append(CommandSpec( - "AC_sql_to_var", "Report", "SQL Query into Variable", - fields=( - FieldSpec("database", FieldType.FILE_PATH), - FieldSpec("query", FieldType.STRING, - placeholder="SELECT name FROM users WHERE id = ?"), - FieldSpec("var", FieldType.STRING, default="sql_result"), - FieldSpec("fetch", FieldType.ENUM, - choices=("all", "one", "scalar"), - optional=True, default="all"), - ), - description=("Run a read-only SQLite SELECT; store rows / a row / a " - "scalar in a variable. Bind values via params (JSON view)."), - )) - specs.append(CommandSpec( - "AC_assert_db", "Report", "Assert SQL Result", - fields=( - FieldSpec("database", FieldType.FILE_PATH), - FieldSpec("query", FieldType.STRING, - placeholder="SELECT COUNT(*) FROM users"), - FieldSpec("op", FieldType.ENUM, - choices=("eq", "ne", "lt", "le", "gt", "ge", - "contains", "startswith", "endswith"), - optional=True, default="eq"), - FieldSpec("expected", FieldType.STRING, optional=True), - ), - description=("Run a scalar SELECT and assert its value (use the JSON " - "view for non-string expected values / params)."), - )) - specs.append(CommandSpec( - "AC_http_to_var", "Report", "HTTP Request into Variable", - fields=( - FieldSpec("url", FieldType.STRING, placeholder="https://..."), - FieldSpec("method", FieldType.ENUM, - choices=("GET", "POST", "PUT", "PATCH", "DELETE", "HEAD"), - optional=True, default="GET"), - FieldSpec("var", FieldType.STRING, default="http_response"), - FieldSpec("json_path", FieldType.STRING, optional=True, - placeholder="data.0.name"), - FieldSpec("timeout", FieldType.FLOAT, optional=True, default=30.0), - ), - description="Request a URL; store the body or a JSON field in a variable.", - )) - specs.append(CommandSpec( - "AC_pdf_to_var", "Report", "PDF Text into Variable", - fields=( - FieldSpec("path", FieldType.FILE_PATH), - FieldSpec("var", FieldType.STRING, default="pdf_text"), - FieldSpec("page", FieldType.INT, optional=True, min_value=1), + CommandSpec( + "AC_send_email", "Report", "Send Email", + description=("Send an email via SMTP. Configure the 'message' " + "{sender,to,subject,body,attachments} and 'smtp' " + "{host,port,username,password} dicts in the JSON view."), ), - description="Extract a PDF's text (all pages or one) into a variable.", - )) - specs.append(CommandSpec( - "AC_assert_pdf_text", "Report", "Assert PDF Text", - fields=( - FieldSpec("path", FieldType.FILE_PATH), - FieldSpec("text", FieldType.STRING), - FieldSpec("present", FieldType.BOOL, optional=True, default=True), - FieldSpec("page", FieldType.INT, optional=True, min_value=1), - FieldSpec("case_sensitive", FieldType.BOOL, optional=True, - default=True), - ), - description="Assert text is present (or absent) in a PDF document.", - )) - specs.append(CommandSpec( - "AC_send_email", "Report", "Send Email", - description=("Send an email via SMTP. Configure the 'message' " - "{sender,to,subject,body,attachments} and 'smtp' " - "{host,port,username,password} dicts in the JSON view."), - )) - specs.append(CommandSpec( - "AC_http_request", "Report", "HTTP Request", - fields=( - FieldSpec("url", FieldType.STRING, placeholder="https://..."), - FieldSpec("method", FieldType.ENUM, - choices=("GET", "POST", "PUT", "PATCH", "DELETE", "HEAD"), - default="GET"), - FieldSpec("data", FieldType.STRING, optional=True, - placeholder="raw request body"), - FieldSpec("timeout", FieldType.FLOAT, optional=True, default=30.0), - ), - description=("Perform an HTTP(S) request; returns status/headers/text/" - "json. Use the JSON view for headers/json_body/auth."), - )) - specs.append(CommandSpec( - "AC_execute_process", "Shell", "Start Executable", - # Must match start_exe(exe_path); the executor dispatches - # event(**params), so a renamed field is a guaranteed TypeError. - fields=(FieldSpec("exe_path", FieldType.FILE_PATH),), - )) - specs.append(CommandSpec( - "AC_move_to_trash", "Shell", "Move File to Recycle Bin", - fields=(FieldSpec("path", FieldType.FILE_PATH),), - description="Delete a file to the OS recycle bin (recoverable).", - )) - specs.append(CommandSpec( - "AC_create_signing_keypair", "Security", "Create Signing Key Pair", - fields=( - FieldSpec("private_path", FieldType.STRING), - FieldSpec("public_path", FieldType.STRING), - FieldSpec("passphrase", FieldType.STRING, optional=True, - placeholder="encrypts the private key file"), + CommandSpec( + "AC_http_request", "Report", "HTTP Request", + fields=( + FieldSpec("url", FieldType.STRING, placeholder="https://..."), + FieldSpec("method", FieldType.ENUM, + choices=("GET", "POST", "PUT", "PATCH", "DELETE", "HEAD"), + default="GET"), + FieldSpec("data", FieldType.STRING, optional=True, + placeholder="raw request body"), + FieldSpec("timeout", FieldType.FLOAT, optional=True, default=30.0), + ), + description=("Perform an HTTP(S) request; returns status/headers/text/" + "json. Use the JSON view for headers/json_body/auth."), ), - description="Create an Ed25519 key pair: keep the private key on the " - "signing machine, give execution endpoints the public key.", - )) - specs.append(CommandSpec( - "AC_sign_action_file", "Security", "Sign Action File", - fields=( - FieldSpec("path", FieldType.FILE_PATH), - FieldSpec("key", FieldType.STRING, optional=True), - FieldSpec("private_key_path", FieldType.FILE_PATH, optional=True), - FieldSpec("passphrase", FieldType.STRING, optional=True, - placeholder="of an encrypted private key"), - ), - description="Write a signature sidecar for an action file " - "(Ed25519 with a private key, else HMAC-SHA256).", - )) - specs.append(CommandSpec( - "AC_verify_action_file", "Security", "Verify Action File", - fields=( - FieldSpec("path", FieldType.FILE_PATH), - FieldSpec("key", FieldType.STRING, optional=True), - FieldSpec("raise_on_fail", FieldType.BOOL, optional=True, - default=False), - FieldSpec("public_key_path", FieldType.FILE_PATH, optional=True), + CommandSpec( + "AC_execute_process", "Shell", "Start Executable", + # Must match start_exe(exe_path); the executor dispatches + # event(**params), so a renamed field is a guaranteed TypeError. + fields=(FieldSpec("exe_path", FieldType.FILE_PATH),), ), - description="Verify an action file against its signature sidecar.", - )) - specs.append(CommandSpec( - "AC_encrypt_action_file", "Security", "Encrypt Action File", - fields=( - FieldSpec("path", FieldType.FILE_PATH), - FieldSpec("key", FieldType.STRING, optional=True), + CommandSpec( + "AC_move_to_trash", "Shell", "Move File to Recycle Bin", + fields=(FieldSpec("path", FieldType.FILE_PATH),), + description="Delete a file to the OS recycle bin (recoverable).", ), - description="Fernet-encrypt an action file to .enc.", - )) - specs.append(CommandSpec( - "AC_decrypt_action_file", "Security", "Decrypt Action File", - fields=( - FieldSpec("enc_path", FieldType.FILE_PATH), - FieldSpec("key", FieldType.STRING, optional=True), - FieldSpec("output_path", FieldType.STRING, optional=True), + CommandSpec( + "AC_create_signing_keypair", "Security", "Create Signing Key Pair", + fields=( + FieldSpec("private_path", FieldType.STRING), + FieldSpec("public_path", FieldType.STRING), + FieldSpec("passphrase", FieldType.STRING, optional=True, + placeholder="encrypts the private key file"), + ), + description="Create an Ed25519 key pair: keep the private key on the " + "signing machine, give execution endpoints the public key.", ), - description="Decrypt a Fernet-encrypted action file.", - )) - specs.append(CommandSpec( - "AC_annotate_screenshot", "Report", "Annotate Screenshot", - fields=( - FieldSpec("source", FieldType.FILE_PATH), - FieldSpec("output_path", FieldType.STRING), - FieldSpec("annotations", FieldType.STRING, - placeholder='[{"type":"box","rect":[10,10,80,40]}]'), + CommandSpec( + "AC_sign_action_file", "Security", "Sign Action File", + fields=( + FieldSpec("path", FieldType.FILE_PATH), + FieldSpec("key", FieldType.STRING, optional=True), + FieldSpec("private_key_path", FieldType.FILE_PATH, optional=True), + FieldSpec("passphrase", FieldType.STRING, optional=True, + placeholder="of an encrypted private key"), + ), + description="Write a signature sidecar for an action file " + "(Ed25519 with a private key, else HMAC-SHA256).", ), - description="Draw boxes / highlights / arrows / labels onto an image.", - )) - specs.append(CommandSpec( - "AC_notify", "Report", "Desktop Notification", - fields=( - FieldSpec("title", FieldType.STRING), - FieldSpec("message", FieldType.STRING, optional=True), + CommandSpec( + "AC_verify_action_file", "Security", "Verify Action File", + fields=( + FieldSpec("path", FieldType.FILE_PATH), + FieldSpec("key", FieldType.STRING, optional=True), + FieldSpec("raise_on_fail", FieldType.BOOL, optional=True, + default=False), + FieldSpec("public_key_path", FieldType.FILE_PATH, optional=True), + ), + description="Verify an action file against its signature sidecar.", ), - description="Show a cross-platform desktop notification.", - )) - specs.append(CommandSpec( - "AC_region_color_stats", "Report", "Region Colour Stats", - fields=( - FieldSpec("region", FieldType.STRING, optional=True, - placeholder=_REGION_PLACEHOLDER), - FieldSpec("buckets", FieldType.INT, optional=True, default=8, - min_value=1), + CommandSpec( + "AC_encrypt_action_file", "Security", "Encrypt Action File", + fields=( + FieldSpec("path", FieldType.FILE_PATH), + FieldSpec("key", FieldType.STRING, optional=True), + ), + description="Fernet-encrypt an action file to .enc.", ), - description="Average + dominant colour of a screen region.", - )) + CommandSpec( + "AC_decrypt_action_file", "Security", "Decrypt Action File", + fields=( + FieldSpec("enc_path", FieldType.FILE_PATH), + FieldSpec("key", FieldType.STRING, optional=True), + FieldSpec("output_path", FieldType.STRING, optional=True), + ), + description="Decrypt a Fernet-encrypted action file.", + ), + CommandSpec( + "AC_annotate_screenshot", "Report", "Annotate Screenshot", + fields=( + FieldSpec("source", FieldType.FILE_PATH), + FieldSpec("output_path", FieldType.STRING), + FieldSpec("annotations", FieldType.STRING, + placeholder='[{"type":"box","rect":[10,10,80,40]}]'), + ), + description="Draw boxes / highlights / arrows / labels onto an image.", + ), + CommandSpec( + "AC_notify", "Report", "Desktop Notification", + fields=( + FieldSpec("title", FieldType.STRING), + FieldSpec("message", FieldType.STRING, optional=True), + ), + description="Show a cross-platform desktop notification.", + ), + CommandSpec( + "AC_region_color_stats", "Report", "Region Colour Stats", + fields=( + FieldSpec("region", FieldType.STRING, optional=True, + placeholder=_REGION_PLACEHOLDER), + FieldSpec("buckets", FieldType.INT, optional=True, default=8, + min_value=1), + ), + description="Average + dominant colour of a screen region.", + ), + ]) def _add_user_specs(specs: List[CommandSpec]) -> None: @@ -5354,41 +5419,43 @@ def _add_user_specs(specs: List[CommandSpec]) -> None: roles = ("viewer", "operator", "admin") token_note = (" The token is in the run's record once; this builder masks it -- " "read it with 'One-time values' after the run (top-level steps only).") - specs.append(CommandSpec( - "AC_user_add", "Security", "User: Add", - fields=( - user_id, - FieldSpec("role", FieldType.ENUM, optional=True, default="viewer", choices=roles), - FieldSpec("display_name", FieldType.STRING, optional=True), - users_path, - ), - description="Add an RBAC user ('tags' via JSON view) and issue its bearer token." - + token_note, - sensitive_result=True, - )) - specs.append(CommandSpec( - "AC_user_remove", "Security", "User: Remove", - fields=(user_id, users_path), - description="Remove an RBAC user; the only admin cannot be removed.", - )) - specs.append(CommandSpec( - "AC_user_set_role", "Security", "User: Set Role", - fields=(user_id, FieldSpec("role", FieldType.ENUM, choices=roles), users_path), - description="Change a user's role; applies to their next request and their " - "deferred work.", - )) - specs.append(CommandSpec( - "AC_user_rotate_token", "Security", "User: Rotate Token", - fields=(user_id, users_path), - description="Replace a user's bearer token; the old one stops working." - + token_note, - sensitive_result=True, - )) - specs.append(CommandSpec( - "AC_user_list", "Security", "User: List", - fields=(users_path,), - description="List the RBAC users (id, display name, role, tags; never a token).", - )) + specs.extend([ + CommandSpec( + "AC_user_add", "Security", "User: Add", + fields=( + user_id, + FieldSpec("role", FieldType.ENUM, optional=True, default="viewer", choices=roles), + FieldSpec("display_name", FieldType.STRING, optional=True), + users_path, + ), + description="Add an RBAC user ('tags' via JSON view) and issue its bearer token." + + token_note, + sensitive_result=True, + ), + CommandSpec( + "AC_user_remove", "Security", "User: Remove", + fields=(user_id, users_path), + description="Remove an RBAC user; the only admin cannot be removed.", + ), + CommandSpec( + "AC_user_set_role", "Security", "User: Set Role", + fields=(user_id, FieldSpec("role", FieldType.ENUM, choices=roles), users_path), + description="Change a user's role; applies to their next request and their " + "deferred work.", + ), + CommandSpec( + "AC_user_rotate_token", "Security", "User: Rotate Token", + fields=(user_id, users_path), + description="Replace a user's bearer token; the old one stops working." + + token_note, + sensitive_result=True, + ), + CommandSpec( + "AC_user_list", "Security", "User: List", + fields=(users_path,), + description="List the RBAC users (id, display name, role, tags; never a token).", + ), + ]) _SPECS: Tuple[CommandSpec, ...] = tuple(_build_specs()) diff --git a/je_auto_control/gui/window_settings.py b/je_auto_control/gui/window_settings.py index 7c19666f7..ddeaef498 100644 --- a/je_auto_control/gui/window_settings.py +++ b/je_auto_control/gui/window_settings.py @@ -18,7 +18,7 @@ import re from dataclasses import dataclass from pathlib import Path -from typing import Any, Dict, Mapping, Optional, Tuple, Union +from typing import Any, Dict, Mapping, Optional, Tuple from PySide6.QtCore import QByteArray, QSettings @@ -32,7 +32,7 @@ _TEXT_SIZE_RANGE = (6, 48) _GROUP = "main_window" _FORMS_GROUP = "forms" -_FORM_NAME = re.compile(r"[A-Za-z][A-Za-z0-9_]{0,63}") +_FORM_NAME = re.compile(r"[A-Za-z]\w{0,63}", re.ASCII) #: A remembered field longer than this was not typed into a one-line input. _FORM_VALUE_LIMIT = 2048 _UNSET: Any = object() @@ -87,7 +87,7 @@ def _as_bytes(value: Any) -> bytes: class WindowSettings: """Load and save a :class:`WindowState`; a store without a path does neither.""" - def __init__(self, path: Union[str, Path, None] = _UNSET) -> None: + def __init__(self, path: str | Path | None = _UNSET) -> None: resolved = settings_path() if path is _UNSET else path self._path: Optional[Path] = None if resolved is None else Path(resolved) diff --git a/je_auto_control/ios/client.py b/je_auto_control/ios/client.py index a8248dad8..f374cbec2 100644 --- a/je_auto_control/ios/client.py +++ b/je_auto_control/ios/client.py @@ -102,11 +102,13 @@ def reset_default_ios_device() -> None: def _sdk_errors() -> tuple: """facebook-wda's base error (an invalid session, a crashed app...), if installed.""" + errors: tuple = () try: from wda import exceptions as wda_exceptions except ImportError: - return () - return (wda_exceptions.WDAError,) + return errors + errors += (wda_exceptions.WDAError,) + return errors def translate_device_errors(function: Callable[..., _Result]) -> Callable[..., _Result]: diff --git a/je_auto_control/linux_wayland/authorisation.py b/je_auto_control/linux_wayland/authorisation.py index f1c3371b8..1ffe223b1 100644 --- a/je_auto_control/linux_wayland/authorisation.py +++ b/je_auto_control/linux_wayland/authorisation.py @@ -172,7 +172,7 @@ def transition(self, channel: str, state: AuthorisationState, current, state=state, detail=detail, compositor=granted_by if granted else current.compositor) self._records[channel] = updated - return updated + return updated # NOSONAR python:S5886 # reason: dataclasses.replace returns its argument's type def reset(self, channel: Optional[str] = None) -> None: """Forget ``channel`` (or every channel), so it may be asked again.""" diff --git a/je_auto_control/linux_wayland/ei_client.py b/je_auto_control/linux_wayland/ei_client.py index e79d8e86f..cab35080c 100644 --- a/je_auto_control/linux_wayland/ei_client.py +++ b/je_auto_control/linux_wayland/ei_client.py @@ -25,9 +25,9 @@ from je_auto_control.linux_wayland.ei_transport import ( ABS_X, ABS_Y, BUTTON_CODES, REL_HWHEEL, REL_WHEEL, EiDependencyMissing, - EiEmitRefused, EiProtocolError, EiWorkerCancelled, EiWorkerDied, - EiWorkerError, EiWorkerTimeout, Held, InputAck, encode_batch, read_frame, - write_frame, + EiEmitRefused, EiProtocolError, EiRequestUncertain, EiWorkerCancelled, + EiWorkerDied, EiWorkerError, EiWorkerTimeout, Held, InputAck, encode_batch, + read_frame, write_frame, ) from je_auto_control.linux_wayland.input_events import ( EV_ABS, EV_KEY, EV_REL, EV_SYN, InputEvent, @@ -270,7 +270,7 @@ def _next_frame(self, timeout: float) -> Optional[Dict[str, Any]]: return frame def _abandon(self, request_id: int, events: List[List[int]], - cancelled: bool) -> Exception: + cancelled: bool) -> EiRequestUncertain: """Tell the helper to stop this request; build what to raise.""" self._write({"op": "cancel", "id": request_id}) applied = None @@ -353,7 +353,7 @@ def _presses(events: Sequence[Sequence[int]]) -> Set[Held]: return down -def _start_failure(frame: Dict[str, Any]) -> Exception: +def _start_failure(frame: Dict[str, Any]) -> LibeiUnavailable: """The typed error a ``failed`` frame stands for.""" message = str(frame.get("message", "the libei helper could not start")) code = frame.get("code") diff --git a/je_auto_control/linux_wayland/ei_transport.py b/je_auto_control/linux_wayland/ei_transport.py index 665375a35..375ca1429 100644 --- a/je_auto_control/linux_wayland/ei_transport.py +++ b/je_auto_control/linux_wayland/ei_transport.py @@ -181,7 +181,7 @@ def read_frame(stream: BinaryIO) -> Optional[Dict[str, Any]]: raise EiProtocolError("the stream ended inside a frame") try: message = json.loads(payload.decode("utf-8")) - except (UnicodeDecodeError, ValueError) as error: + except ValueError as error: # a UnicodeDecodeError is one too raise EiProtocolError(f"a frame was not valid JSON: {error}") from error if not isinstance(message, dict): raise EiProtocolError("a frame was not a JSON object") diff --git a/je_auto_control/linux_wayland/ei_worker.py b/je_auto_control/linux_wayland/ei_worker.py index 7ee7482bc..81bb934f4 100644 --- a/je_auto_control/linux_wayland/ei_worker.py +++ b/je_auto_control/linux_wayland/ei_worker.py @@ -309,7 +309,7 @@ def _unwind(_signum: int, _frame: Any) -> None: signal.signal(signal.SIGTERM, _unwind) try: code = main() - except SystemExit as stop: + except SystemExit as stop: # NOSONAR python:S5754 # reason: its status is what os._exit ends with below code = stop.code if isinstance(stop.code, int) else 0 logging.shutdown() sys.stderr.flush() diff --git a/je_auto_control/utils/accessibility/backends/base.py b/je_auto_control/utils/accessibility/backends/base.py index 0a8435e0f..306e9872c 100644 --- a/je_auto_control/utils/accessibility/backends/base.py +++ b/je_auto_control/utils/accessibility/backends/base.py @@ -44,7 +44,8 @@ def get_value(self, name: Optional[str] = None, role: Optional[str] = None, ``window_title`` / ``contains`` narrow which control is meant, the same way :meth:`get_state` does — the two reads stay a pair. """ - self._unsupported("get_value", name, role, app_name, automation_id) + self._unsupported("get_value", name, role, app_name, automation_id, + window_title, contains) def set_value(self, value: str, name: Optional[str] = None, role: Optional[str] = None, app_name: Optional[str] = None, @@ -220,7 +221,8 @@ def get_state(self, name: Optional[str] = None, key is absent when the control does not support it — distinct from the value being empty. A password field reports only ``{"password": True}``. """ - self._unsupported("get_state", name, role, app_name, automation_id) + self._unsupported("get_state", name, role, app_name, automation_id, + window_title, contains) # --- table headers + cell addressing (TablePattern / GridItemPattern) --- diff --git a/je_auto_control/utils/action_journal/events.py b/je_auto_control/utils/action_journal/events.py index e15d5407f..bd4d71f40 100644 --- a/je_auto_control/utils/action_journal/events.py +++ b/je_auto_control/utils/action_journal/events.py @@ -186,7 +186,7 @@ def apply_end(event: ActionEvent, data: Mapping[str, Any], where: str) -> Action raise JournalFormatError(f"{where}: 'error' must be a string") # An end line without the field keeps what the start line carried. artifacts = read_artifacts(data, where) if "artifacts" in data else event.artifacts - return dataclasses.replace( + return dataclasses.replace( # NOSONAR python:S5886 # reason: replace() returns its argument's type event, status=data.get("status", STATUS_INCOMPLETE), finished_at=_number(data, "finished_at", where), error=error, outcome=outcome, artifacts=artifacts) diff --git a/je_auto_control/utils/action_journal/recorder.py b/je_auto_control/utils/action_journal/recorder.py index b15f20e56..69dfc7998 100644 --- a/je_auto_control/utils/action_journal/recorder.py +++ b/je_auto_control/utils/action_journal/recorder.py @@ -28,7 +28,7 @@ import uuid from pathlib import Path from types import TracebackType -from typing import Any, Callable, Dict, List, Optional, Tuple, Type, TypeVar, Union +from typing import Any, Callable, Dict, List, Optional, Tuple, Type, TypeVar from je_auto_control.utils.action_journal.events import ( STATUS_ERROR, STATUS_INCOMPLETE, STATUS_OK, ActionEvent, @@ -92,7 +92,7 @@ def start_event(self, event: ActionEvent) -> ActionEvent: event, sequence=sequence, step_id=f"{self.token}-{sequence}") self.journal.append(numbered) self.started = sequence - return numbered + return numbered # NOSONAR python:S5886 # reason: replace() returns its argument's type def status(self) -> Dict[str, Any]: """The session as a JSON-ready dict.""" @@ -263,7 +263,7 @@ def _ended(self, event: ActionEvent, exc: Optional[BaseException]) -> ActionEven found = self.artifacts + [ item for item in artifacts_of_step(params, self._result, event.started_at) if item not in self.artifacts] - return dataclasses.replace( + return dataclasses.replace( # NOSONAR python:S5886 # reason: replace() returns its argument's type event, status=status, error=error, outcome=outcome, finished_at=time.time(), artifacts=_masked_artifacts(found, known)) @@ -280,7 +280,7 @@ def _fail(session: _Session, error: OSError) -> None: _ACTIVE, _LAST = None, session -def step(action: Any) -> Union[_Step, _NullStep]: +def step(action: Any) -> _Step | _NullStep: """A context manager recording ``action``; a shared no-op when journalling is off.""" session = _ACTIVE if session is None: @@ -389,7 +389,7 @@ def carried(*args: Any, **kwargs: Any) -> Any: return carried # type: ignore[return-value] # reason: same call signature as work -def start_action_journal(path: Union[str, Path, None] = None, *, +def start_action_journal(path: str | Path | None = None, *, run_id: Optional[str] = None, session: Optional[str] = None) -> Dict[str, Any]: """Start writing every executed action to the journal at ``path``. diff --git a/je_auto_control/utils/action_journal/store.py b/je_auto_control/utils/action_journal/store.py index a9d81a631..903a3a4ba 100644 --- a/je_auto_control/utils/action_journal/store.py +++ b/je_auto_control/utils/action_journal/store.py @@ -13,7 +13,7 @@ import threading from dataclasses import dataclass, field from pathlib import Path -from typing import Any, Dict, List, Optional, Tuple, Union +from typing import Any, Dict, List, Optional, Tuple from je_auto_control.utils.action_journal.events import ( RECORD_END, STATUS_ERROR, STATUS_INCOMPLETE, STATUS_OK, ActionEvent, @@ -30,7 +30,7 @@ def default_journal_path() -> Path: class ActionJournal: """Thread-safe append-only journal file. Appending creates it.""" - def __init__(self, path: Union[str, Path, None] = None) -> None: + def __init__(self, path: str | Path | None = None) -> None: self._path = Path(path) if path is not None else default_journal_path() self._lock = threading.Lock() @@ -102,7 +102,7 @@ def _fold(path: Path, run_id: Optional[str]) -> JournalContents: return JournalContents(tuple(events.values()), lines, tuple(torn)) -def load_journal(path: Union[str, Path], *, run_id: Optional[str] = None +def load_journal(path: str | Path, *, run_id: Optional[str] = None ) -> JournalContents: """Read ``path`` with line numbers and the torn lines it skipped. @@ -116,7 +116,7 @@ def load_journal(path: Union[str, Path], *, run_id: Optional[str] = None return _fold(file_path, run_id) -def read_events(path: Union[str, Path], *, run_id: Optional[str] = None +def read_events(path: str | Path, *, run_id: Optional[str] = None ) -> List[ActionEvent]: """The events in the journal at ``path``, in the order they started. @@ -126,7 +126,7 @@ def read_events(path: Union[str, Path], *, run_id: Optional[str] = None return list(load_journal(path, run_id=run_id).events) -def list_journal_runs(path: Union[str, Path]) -> List[Dict[str, Any]]: +def list_journal_runs(path: str | Path) -> List[Dict[str, Any]]: """One summary per run in the journal at ``path``, oldest first.""" runs: Dict[str, Dict[str, Any]] = {} for event in load_journal(path).events: diff --git a/je_auto_control/utils/action_signing/_private_file.py b/je_auto_control/utils/action_signing/_private_file.py index 668919ad7..d08536524 100644 --- a/je_auto_control/utils/action_signing/_private_file.py +++ b/je_auto_control/utils/action_signing/_private_file.py @@ -39,7 +39,7 @@ "BG": ("S-1-5-32-546", "Guests"), "NU": ("S-1-5-2", "Network"), } -_BROAD_BY_SID = {sid: name for sid, name in _BROAD_PRINCIPALS.values()} +_BROAD_BY_SID = dict(_BROAD_PRINCIPALS.values()) # Two-letter SDDL rights that include reading the file's data. _READ_RIGHTS = frozenset({"FA", "FR", "GA", "GR", "CC"}) _FILE_READ_DATA = 0x1 diff --git a/je_auto_control/utils/action_signing/asymmetric.py b/je_auto_control/utils/action_signing/asymmetric.py index 6f99a0d96..e8a52ca49 100644 --- a/je_auto_control/utils/action_signing/asymmetric.py +++ b/je_auto_control/utils/action_signing/asymmetric.py @@ -24,7 +24,6 @@ signing must keep working without it. This module is GUI-free and imports no Qt. """ import base64 -import binascii import hashlib import json from pathlib import Path @@ -225,6 +224,6 @@ def envelope_failure(sidecar_text: str, data: bytes, public_key: Any) -> Optiona try: signature = base64.b64decode(envelope["signature"], validate=True) public_key.verify(signature, _CONTEXT + data) - except (binascii.Error, ValueError, _ed25519().invalid_signature): + except (ValueError, _ed25519().invalid_signature): # binascii.Error is a ValueError return "signature mismatch (tampered or wrong key)" return None diff --git a/je_auto_control/utils/action_signing/cipher.py b/je_auto_control/utils/action_signing/cipher.py index f1e38988e..e7cc2302a 100644 --- a/je_auto_control/utils/action_signing/cipher.py +++ b/je_auto_control/utils/action_signing/cipher.py @@ -14,7 +14,7 @@ import hashlib import os from pathlib import Path -from typing import Optional, Union +from typing import Optional from je_auto_control.utils.action_signing._key_file import load_or_create_key_file from je_auto_control.utils.exception.exceptions import ( @@ -35,7 +35,7 @@ def _default_key_path() -> Path: # scrypt cost: 2**15 * 8 * 128 bytes = 32 MiB, about 0.1 s per derivation. _SCRYPT_N, _SCRYPT_R, _SCRYPT_P = 2 ** 15, 8, 1 -KeyType = Optional[Union[bytes, str]] +KeyType = Optional[bytes | str] def _fernet_types() -> tuple: @@ -63,7 +63,7 @@ def _persistent_key() -> bytes: _default_key_path(), fernet_cls.generate_key, _FERNET_KEY_LENGTH) -def _passphrase(key: Union[bytes, str]) -> bytes: +def _passphrase(key: bytes | str) -> bytes: raw = key if isinstance(key, bytes) else str(key).encode("utf-8") if not raw: raise AutoControlException("an empty passphrase encrypts nothing") @@ -105,7 +105,7 @@ def _decrypt(blob: bytes, key: KeyType) -> bytes: return fernet_cls(_legacy_key(passphrase)).decrypt(blob) -def encrypt_action_file(path: Union[str, Path], key: KeyType = None) -> str: +def encrypt_action_file(path: str | Path, key: KeyType = None) -> str: """Encrypt the file at ``path`` to ``.enc``; return the enc path.""" target = Path(path) token = _encrypt(target.read_bytes(), key) @@ -115,8 +115,8 @@ def encrypt_action_file(path: Union[str, Path], key: KeyType = None) -> str: return str(enc_path) -def decrypt_action_file(enc_path: Union[str, Path], key: KeyType = None, - output_path: Optional[Union[str, Path]] = None) -> str: +def decrypt_action_file(enc_path: str | Path, key: KeyType = None, + output_path: Optional[str | Path] = None) -> str: """Decrypt ``enc_path`` to a plaintext file; return its path. ``output_path`` defaults to ``enc_path`` with the ``.enc`` suffix @@ -136,7 +136,7 @@ def decrypt_action_file(enc_path: Union[str, Path], key: KeyType = None, return str(out) -def _output_path(enc: Path, output_path: Optional[Union[str, Path]]) -> Path: +def _output_path(enc: Path, output_path: Optional[str | Path]) -> Path: if output_path is not None: return Path(output_path) if enc.name.endswith(_ENC_SUFFIX): diff --git a/je_auto_control/utils/action_signing/signer.py b/je_auto_control/utils/action_signing/signer.py index bef18d5d1..4ace0e926 100644 --- a/je_auto_control/utils/action_signing/signer.py +++ b/je_auto_control/utils/action_signing/signer.py @@ -27,7 +27,7 @@ import os from dataclasses import asdict, dataclass from pathlib import Path -from typing import Any, Dict, Optional, Union +from typing import Any, Dict, Optional from je_auto_control.utils.action_signing import asymmetric from je_auto_control.utils.action_signing._key_file import load_or_create_key_file @@ -49,8 +49,8 @@ def _default_key_path() -> Path: _REQUIRE_ENV = "JE_AUTOCONTROL_REQUIRE_SIGNED_ACTIONS" _KEY_LENGTH = 32 -KeyType = Optional[Union[bytes, str]] -KeyPath = Optional[Union[str, Path]] +KeyType = Optional[bytes | str] +KeyPath = Optional[str | Path] @dataclass(frozen=True) @@ -92,7 +92,7 @@ def _digest(data: bytes, key: bytes) -> str: def _signature_text(data: bytes, key: KeyType, private_key_path: KeyPath, - passphrase: Optional[Union[bytes, str]] = None) -> str: + passphrase: Optional[bytes | str] = None) -> str: """Sign ``data`` with the key pair when one applies, else with HMAC.""" config = action_signing_config() private = private_key_path @@ -108,9 +108,9 @@ def _signature_text(data: bytes, key: KeyType, private_key_path: KeyPath, return _digest(data, _load_or_create_key(key)) -def sign_action_file(path: Union[str, Path], key: KeyType = None, +def sign_action_file(path: str | Path, key: KeyType = None, *, private_key_path: KeyPath = None, - passphrase: Optional[Union[bytes, str]] = None) -> str: + passphrase: Optional[bytes | str] = None) -> str: """Write a signature sidecar for the file at ``path``. With ``private_key_path`` -- or, when no ``key`` is given, the private key @@ -130,7 +130,7 @@ def sign_action_file(path: Union[str, Path], key: KeyType = None, return str(sig_path) -def verify_action_file(path: Union[str, Path], key: KeyType = None, +def verify_action_file(path: str | Path, key: KeyType = None, *, raise_on_fail: bool = False, public_key_path: KeyPath = None) -> VerifyResult: """Verify the action file at ``path`` against its ``.sig`` sidecar. @@ -172,7 +172,7 @@ def _signature_failure(sidecar: str, data: bytes, key: KeyType, return None -def _verify_bytes(path: Union[str, Path], data: bytes, key: KeyType, +def _verify_bytes(path: str | Path, data: bytes, key: KeyType, raise_on_fail: bool, public_key_path: KeyPath = None) -> VerifyResult: """Check ``data`` -- the content of ``path`` -- against its sidecar.""" sig_path = _sig_path(Path(path)) @@ -188,7 +188,7 @@ def _verify_bytes(path: Union[str, Path], data: bytes, key: KeyType, return VerifyResult(str(path), True, "signature valid") -def _fail(path: Union[str, Path], reason: str, +def _fail(path: str | Path, reason: str, raise_on_fail: bool) -> VerifyResult: if raise_on_fail: raise AutoControlSignatureException( @@ -198,7 +198,7 @@ def _fail(path: Union[str, Path], reason: str, return VerifyResult(str(path), False, reason) -def require_signed_actions(path: Union[str, Path], +def require_signed_actions(path: str | Path, key: KeyType = None) -> None: """Verify ``path`` only when signed-action enforcement is enabled. @@ -216,7 +216,7 @@ def signing_required() -> bool: return bool(os.environ.get(_REQUIRE_ENV)) -def read_signed_action_bytes(path: Union[str, Path], key: KeyType = None) -> bytes: +def read_signed_action_bytes(path: str | Path, key: KeyType = None) -> bytes: """Read ``path`` once and, when enforcement is on, verify those bytes. The caller parses what this returns, so the content that ran is the diff --git a/je_auto_control/utils/admin/admin_client.py b/je_auto_control/utils/admin/admin_client.py index 503b66fc8..bc93f36fd 100644 --- a/je_auto_control/utils/admin/admin_client.py +++ b/je_auto_control/utils/admin/admin_client.py @@ -137,7 +137,6 @@ def fetch_thumbnails(self, *, labels: Optional[List[str]] = None, scales the resulting image down to a thumbnail tile. """ import base64 - import binascii targets = self._resolve_targets(labels) if not targets: return {} @@ -157,7 +156,7 @@ def grab(host: AdminHost) -> tuple: return host.label, None try: return host.label, base64.b64decode(data) - except (ValueError, binascii.Error): + except ValueError: # binascii.Error is one return host.label, None with ThreadPoolExecutor(max_workers=self._max_parallel) as pool: diff --git a/je_auto_control/utils/agent/backends/_computer_beta_path.py b/je_auto_control/utils/agent/backends/_computer_beta_path.py index 8979ef062..153c1dd46 100644 --- a/je_auto_control/utils/agent/backends/_computer_beta_path.py +++ b/je_auto_control/utils/agent/backends/_computer_beta_path.py @@ -65,7 +65,7 @@ def _handle_response(self, response: Any) -> Dict[str, Any]: self._pending_tool_use_id = _attr(block, "id") decision = unscale_decision(_decision_from_computer_action(payload), self._scale) return _clamp_decision(decision, *self._display) - return _final_answer(response, content) + return _final_answer(content) def _pending_result(self, history: Sequence[AgentStep], screenshot: Optional[bytes]) -> List[Dict[str, Any]]: diff --git a/je_auto_control/utils/agent/backends/_computer_messages.py b/je_auto_control/utils/agent/backends/_computer_messages.py index e3de02835..db0588095 100644 --- a/je_auto_control/utils/agent/backends/_computer_messages.py +++ b/je_auto_control/utils/agent/backends/_computer_messages.py @@ -14,7 +14,7 @@ ) -def _final_answer(response: Any, content: List[Any]) -> Dict[str, Any]: +def _final_answer(content: List[Any]) -> Dict[str, Any]: """A turn without tool calls: the final answer (a truncated turn was refused earlier).""" text_parts: List[str] = [ _attr(b, "text") or "" diff --git a/je_auto_control/utils/agent/backends/_computer_toolset.py b/je_auto_control/utils/agent/backends/_computer_toolset.py index fe485bb84..5d4885058 100644 --- a/je_auto_control/utils/agent/backends/_computer_toolset.py +++ b/je_auto_control/utils/agent/backends/_computer_toolset.py @@ -50,7 +50,8 @@ #: "claude--[-]", minor being one or two digits (a date #: suffix is eight), anywhere in the id so Bedrock / Vertex ids match too. -_MODEL_VERSION = re.compile(r"claude-[a-z]+-(\d+)(?:-(\d{1,2}))?(?![0-9])") +_MODEL_VERSION = re.compile( + r"claude-[a-z]+-(\d+)(?:-(\d{1,2}))?(?![0-9])") # NOSONAR python:S6353 # reason: \d also matches Unicode digits def image_tier(model: Optional[str]) -> Tuple[int, int]: diff --git a/je_auto_control/utils/agent/backends/_computer_toolset_path.py b/je_auto_control/utils/agent/backends/_computer_toolset_path.py index 6fa1d57d6..fb120fc9a 100644 --- a/je_auto_control/utils/agent/backends/_computer_toolset_path.py +++ b/je_auto_control/utils/agent/backends/_computer_toolset_path.py @@ -61,7 +61,7 @@ def _handle_toolset_response(self, response: Any, calls = [(_attr(block, "id"), self._toolset_decision(block)) for block in content if _block_type(block) == "tool_use"] if not calls: - return _final_answer(response, content) + return _final_answer(content) batch.load(calls) return batch.next_decision() diff --git a/je_auto_control/utils/agent_trace/agent_trace.py b/je_auto_control/utils/agent_trace/agent_trace.py index 7c058ac82..a9bf1ee4b 100644 --- a/je_auto_control/utils/agent_trace/agent_trace.py +++ b/je_auto_control/utils/agent_trace/agent_trace.py @@ -92,12 +92,12 @@ def operation(self, operation: str, **kwargs: Any """ fields: Dict[str, Any] = {} start = self._clock() + status = STATUS_ERROR try: yield fields - except Exception: - self._record_block(operation, start, STATUS_ERROR, kwargs, fields) - raise - self._record_block(operation, start, STATUS_OK, kwargs, fields) + status = STATUS_OK + finally: + self._record_block(operation, start, status, kwargs, fields) def _record_block(self, operation: str, start: float, status: str, kwargs: Dict[str, Any], fields: Dict[str, Any]) -> None: diff --git a/je_auto_control/utils/annotate/annotate.py b/je_auto_control/utils/annotate/annotate.py index f2289c468..e5a2a4b18 100644 --- a/je_auto_control/utils/annotate/annotate.py +++ b/je_auto_control/utils/annotate/annotate.py @@ -25,7 +25,8 @@ if TYPE_CHECKING: # pragma: no cover - annotations only from PIL import Image, ImageDraw -ImageSource = Union[str, Path, bytes, "Image.Image"] +# "Image.Image" is a string forward reference, which "|" cannot take at runtime. +ImageSource = Union[str, Path, bytes, "Image.Image"] # NOSONAR python:S6546 # reason: see the line above def _load_image(source: ImageSource) -> "Image.Image": @@ -91,7 +92,7 @@ def _draw_text(draw: ImageDraw.ImageDraw, ann: Dict[str, Any]) -> None: def annotate_screenshot(source: ImageSource, annotations: List[Dict[str, Any]], - output_path: Union[str, Path]) -> str: + output_path: str | Path) -> str: """Draw ``annotations`` onto ``source`` and save the result as PNG. Returns the output path. ``source`` may be a file path, PNG bytes, or a diff --git a/je_auto_control/utils/ax_tree_walk/ax_tree_walk.py b/je_auto_control/utils/ax_tree_walk/ax_tree_walk.py index 3ca424f0e..554b58bb8 100644 --- a/je_auto_control/utils/ax_tree_walk/ax_tree_walk.py +++ b/je_auto_control/utils/ax_tree_walk/ax_tree_walk.py @@ -17,7 +17,7 @@ Pure-stdlib over :class:`AXTreeNode` values; no device or backend access, no ``PySide6``. Compose it on top of any ``dump_accessibility_tree`` output. """ -from typing import Optional, Union +from typing import Optional from je_auto_control.utils.accessibility.tree import AXTreeNode @@ -47,7 +47,7 @@ def control_type_name(control_type: int) -> str: return _CONTROL_TYPE_NAMES.get(cid, f"{_ROLE_PREFIX}{cid}") -def humanize_role(role: Union[str, int]) -> str: +def humanize_role(role: str | int) -> str: """Map a raw UIA role to a friendly name. Accepts an int id (``50000``), a ``"ControlType_50000"`` string, or a bare diff --git a/je_auto_control/utils/chatops/slack_bot.py b/je_auto_control/utils/chatops/slack_bot.py index 384f85712..ca4f15456 100644 --- a/je_auto_control/utils/chatops/slack_bot.py +++ b/je_auto_control/utils/chatops/slack_bot.py @@ -215,7 +215,8 @@ def _handle(self, text: str, message: Dict[str, Any]) -> bool: argv = self._background_argv(text) if argv is None: return self._route_one(text, message) is not None - return self._start_background(argv, text, message) + self._start_background(argv, text, message) + return True def _context(self, message: Dict[str, Any]) -> Dict[str, Any]: return {"slack_user": message.get("user"), "slack_ts": message.get("ts"), @@ -249,8 +250,8 @@ def _background_argv(self, text: str) -> Optional[List[str]]: return argv def _start_background(self, argv: List[str], text: str, - message: Dict[str, Any]) -> bool: - """Start ``text`` on a worker, or say why not; always ``True`` (it was a command).""" + message: Dict[str, Any]) -> None: + """Start ``text`` on a worker, or say why not.""" name = argv[0].lower() listed = threading.Event() refusal = "" @@ -270,11 +271,10 @@ def _start_background(self, argv: List[str], text: str, self._worker.start() if refusal: self.post_message(refusal, thread_ts=str(message.get("ts") or "")) - return True + return # The next message may be the /stop for this run: read it only once # the run is listed (or has already ended). listed.wait(_START_WAIT) - return True def _run_background(self, text: str, message: Dict[str, Any], token: StopToken, listed: threading.Event) -> None: diff --git a/je_auto_control/utils/clipboard/clipboard.py b/je_auto_control/utils/clipboard/clipboard.py index 07d4e2466..b2d9cff8d 100644 --- a/je_auto_control/utils/clipboard/clipboard.py +++ b/je_auto_control/utils/clipboard/clipboard.py @@ -15,10 +15,12 @@ import subprocess # nosec B404 # reason: required for pbcopy/pbpaste/xclip/xsel import sys from io import BytesIO -from typing import Optional, Union +from typing import Optional from je_auto_control.utils.exception.exceptions import AutoControlException +_PILLOW_REQUIRED = "Pillow is required for clipboard image support" + def get_clipboard() -> str: """Return the current clipboard text (empty string if empty).""" @@ -51,7 +53,7 @@ def get_clipboard_image() -> Optional[bytes]: return _linux_get_image() -def _as_png_bytes(image: Union[bytes, bytearray, str, os.PathLike]) -> bytes: +def _as_png_bytes(image: bytes | bytearray | str | os.PathLike) -> bytes: """PNG bytes for either raw bytes or a path to any Pillow-readable file.""" if isinstance(image, (bytes, bytearray)): if not image: @@ -64,9 +66,7 @@ def _as_png_bytes(image: Union[bytes, bytearray, str, os.PathLike]) -> bytes: try: from PIL import Image # noqa: PLC0415 lazy import except ImportError as error: - raise RuntimeError( - "Pillow is required for clipboard image support" - ) from error + raise RuntimeError(_PILLOW_REQUIRED) from error buffer = BytesIO() with Image.open(safe_path) as opened: opened.convert("RGB").save(buffer, format="PNG") @@ -75,7 +75,7 @@ def _as_png_bytes(image: Union[bytes, bytearray, str, os.PathLike]) -> bytes: def set_clipboard_image( - image: Union[bytes, bytearray, str, os.PathLike]) -> None: + image: bytes | bytearray | str | os.PathLike) -> None: """Place an image on the clipboard, from PNG bytes **or** a file path. Both are accepted because both callers are real: the remote-desktop viewer @@ -207,9 +207,7 @@ def _win_get_image() -> Optional[bytes]: try: from PIL import ImageGrab # noqa: PLC0415 lazy import except ImportError as error: - raise RuntimeError( - "Pillow is required for clipboard image support" - ) from error + raise RuntimeError(_PILLOW_REQUIRED) from error image = ImageGrab.grabclipboard() if image is None or isinstance(image, list): return None @@ -225,9 +223,7 @@ def _win_set_image(png_bytes: bytes) -> None: try: from PIL import Image # noqa: PLC0415 lazy import except ImportError as error: - raise RuntimeError( - "Pillow is required for clipboard image support" - ) from error + raise RuntimeError(_PILLOW_REQUIRED) from error # `Image.open` returns an `ImageFile`; `convert` returns a plain # `Image`, so the variable has to be declared as the wider one. image: Image.Image = Image.open(BytesIO(png_bytes)) diff --git a/je_auto_control/utils/codegen/candidate_diff.py b/je_auto_control/utils/codegen/candidate_diff.py index ae3049205..1ecd36319 100644 --- a/je_auto_control/utils/codegen/candidate_diff.py +++ b/je_auto_control/utils/codegen/candidate_diff.py @@ -18,7 +18,7 @@ import json from dataclasses import dataclass from pathlib import Path -from typing import Any, Dict, List, Optional, Sequence, Union +from typing import Any, Dict, List, Optional, Sequence from je_auto_control.utils.codegen.journal_import import CandidateScript, JournalImportError from je_auto_control.utils.json.json_file import read_action_json @@ -110,7 +110,7 @@ def diff_candidate(candidate: CandidateScript, *, actions: Optional[Sequence[Any return diff_code(code or "", candidate.code, before_label=before_label, context=context) -def diff_candidate_against_file(candidate: CandidateScript, path: Union[str, Path], *, +def diff_candidate_against_file(candidate: CandidateScript, path: str | Path, *, context: int = 3) -> CandidateDiff: """Diff ``candidate`` against a file: a ``.json`` action file, or any other file as code. diff --git a/je_auto_control/utils/codegen/codegen.py b/je_auto_control/utils/codegen/codegen.py index 589ba9247..f817e85ec 100644 --- a/je_auto_control/utils/codegen/codegen.py +++ b/je_auto_control/utils/codegen/codegen.py @@ -19,7 +19,7 @@ import pprint import re import textwrap -from typing import Any, Dict, Sequence, Tuple, Union +from typing import Any, Dict, Sequence, Tuple from je_auto_control.utils.json.json_file import read_action_json from je_auto_control.utils.json_store.json_store import atomic_write_text @@ -255,7 +255,7 @@ def _check_shapes(actions: list) -> None: } -def generate_code(actions: Union[Sequence, Dict[str, Any]], target: str = "pytest", +def generate_code(actions: Sequence | Dict[str, Any], target: str = "pytest", name: str = "recorded_flow", style: str = "calls", failure_bundle: bool = False) -> str: """Render ``actions`` as source code for ``target`` (pytest/python/robot). diff --git a/je_auto_control/utils/codegen/journal_import.py b/je_auto_control/utils/codegen/journal_import.py index 69e61ea6c..c07d07b63 100644 --- a/je_auto_control/utils/codegen/journal_import.py +++ b/je_auto_control/utils/codegen/journal_import.py @@ -37,7 +37,7 @@ from dataclasses import dataclass, field from pathlib import Path from typing import ( - Any, Dict, FrozenSet, List, Mapping, Optional, Sequence, Set, Tuple, Union, + Any, Dict, FrozenSet, List, Mapping, Optional, Sequence, Set, Tuple, ) from je_auto_control.utils.action_journal.events import ( @@ -337,7 +337,7 @@ def _script_name(run_id: str) -> str: return "journal_run_" + re.sub(r"\W+", "_", run_id)[:40] -def generate_candidate_from_log(path: Union[str, Path], *, run_id: str, +def generate_candidate_from_log(path: str | Path, *, run_id: str, target: str = "pytest", style: str = "actions") -> CandidateScript: """Build a reviewable candidate script from run ``run_id`` of a journal. @@ -377,7 +377,7 @@ def generate_candidate_from_log(path: Union[str, Path], *, run_id: str, actions=builder.actions, observed_path_only=builder.observed) -def only_run_id(path: Union[str, Path]) -> str: +def only_run_id(path: str | Path) -> str: """The id of the journal's single run; raise when there is none or several.""" runs = [run["run_id"] for run in list_journal_runs(os.path.realpath(os.fspath(path)))] if len(runs) != 1: @@ -387,8 +387,8 @@ def only_run_id(path: Union[str, Path]) -> str: return runs[0] -def write_candidate(candidate: CandidateScript, output: Union[str, Path], - manifest: Optional[Union[str, Path]] = None) -> Mapping[str, str]: +def write_candidate(candidate: CandidateScript, output: str | Path, + manifest: Optional[str | Path] = None) -> Mapping[str, str]: """Write the candidate's code (and its manifest, when a path is given).""" written = {"output": os.path.realpath(os.fspath(output))} atomic_write_text(written["output"], candidate.code) diff --git a/je_auto_control/utils/codegen/robot_check.py b/je_auto_control/utils/codegen/robot_check.py index 9ac447364..19ec1e923 100644 --- a/je_auto_control/utils/codegen/robot_check.py +++ b/je_auto_control/utils/codegen/robot_check.py @@ -50,11 +50,9 @@ "END", "ELSE", "ELSE IF", "EXCEPT", "FINALLY", "BREAK", "CONTINUE", "RETURN", "VAR", }) -_HEADER = re.compile(r"^\*+\s*(.*?)\s*\**\s*$") _CELLS = re.compile(r" {2,}|\t+") _ASSIGNMENT = re.compile(r"^[$@&]\{[^{}]+\}\s?=?$") _VARIABLE_NAME = re.compile(r"^[$@&]\{[^{}]+\}\s?=?$") -_SETTING_CELL = re.compile(r"^\[\s*(.+?)\s*\]$") _CONTINUATION = "..." @@ -117,9 +115,20 @@ def _unbalanced_variable(cell: str) -> bool: return depth != 0 +def _header_name(text: str) -> str: + """The name in ``*** Name ***``: no leading or trailing asterisks, no padding.""" + return text.lstrip("*").strip().rstrip("*").rstrip() + + +def _setting_name(cell: str) -> Optional[str]: + """The name in a ``[Name]`` cell; ``None`` for a cell that is not one.""" + if len(cell) < 3 or cell[0] != "[" or cell[-1] != "]": + return None + return cell[1:-1].strip() + + def _check_header(state: _State, number: int, text: str) -> None: - match = _HEADER.match(text) - name = (match.group(1) if match else "").lower() + name = _header_name(text).lower() state.close_owner() state.section = _SECTIONS.get(name) if state.section is None: @@ -169,9 +178,9 @@ def _check_blocks(owner: _Owner, state: _State, number: int, first: str) -> None def _check_body_cells(state: _State, number: int, cells: Sequence[str]) -> None: first = cells[0] - setting = _SETTING_CELL.match(first) + setting = _setting_name(first) if setting is not None: - if setting.group(1).lower() not in _BODY_SETTINGS: + if setting.lower() not in _BODY_SETTINGS: state.note(number, f"unknown setting {first!r}") return if first in _OPENERS or first in _MARKERS: diff --git a/je_auto_control/utils/config_sync/adapters.py b/je_auto_control/utils/config_sync/adapters.py index 6eb5b23c6..41a3f3283 100644 --- a/je_auto_control/utils/config_sync/adapters.py +++ b/je_auto_control/utils/config_sync/adapters.py @@ -27,7 +27,7 @@ import json from dataclasses import dataclass, field, fields from pathlib import Path, PurePosixPath -from typing import Any, ClassVar, Dict, Iterable, List, Mapping, Optional, Set, Tuple, Union +from typing import Any, ClassVar, Dict, Iterable, List, Mapping, Optional, Set, Tuple from je_auto_control.utils.config_sync.bucket import ConfigSyncError from je_auto_control.utils.config_sync.versions import SyncEntry, SyncOperation @@ -81,7 +81,7 @@ class SyncAdapter: #: Fields that describe this machine only and are never sent. local_fields: ClassVar[Tuple[str, ...]] = () - def __init__(self, origin: str, *, scripts_dir: Union[str, Path, None] = None) -> None: + def __init__(self, origin: str, *, scripts_dir: str | Path | None = None) -> None: if not origin: raise ConfigSyncError("an adapter needs this device's id") self.origin = origin @@ -300,7 +300,7 @@ class ScriptSyncAdapter(SyncAdapter): section = "scripts" - def __init__(self, origin: str, scripts_dir: Union[str, Path]) -> None: + def __init__(self, origin: str, scripts_dir: str | Path) -> None: super().__init__(origin, scripts_dir=scripts_dir) self._root = Path(scripts_dir) #: Scripts left out of the last snapshot, and why. @@ -393,7 +393,7 @@ class HotkeySyncAdapter(SyncAdapter): arms_on_enable = True def __init__(self, origin: str, daemon: Any, *, - scripts_dir: Union[str, Path, None] = None) -> None: + scripts_dir: str | Path | None = None) -> None: super().__init__(origin, scripts_dir=scripts_dir) self._daemon = daemon @@ -459,7 +459,7 @@ class TriggerSyncAdapter(SyncAdapter): arms_on_enable = True def __init__(self, origin: str, engine: Any, *, - scripts_dir: Union[str, Path, None] = None) -> None: + scripts_dir: str | Path | None = None) -> None: super().__init__(origin, scripts_dir=scripts_dir) self._engine = engine #: Triggers left out of the last snapshot, and why. diff --git a/je_auto_control/utils/config_sync/assets.py b/je_auto_control/utils/config_sync/assets.py index 1573cbb37..e83b40adb 100644 --- a/je_auto_control/utils/config_sync/assets.py +++ b/je_auto_control/utils/config_sync/assets.py @@ -17,7 +17,7 @@ import urllib.parse from dataclasses import dataclass, field from pathlib import Path, PurePosixPath -from typing import Any, Dict, Iterable, List, Mapping, Optional, Protocol, Tuple, Union +from typing import Any, Dict, Iterable, List, Mapping, Optional, Protocol, Tuple from je_auto_control.utils.config_sync.bucket import ConfigSyncError from je_auto_control.utils.exception.exceptions import AutoControlException @@ -33,7 +33,7 @@ class AssetSyncError(ConfigSyncError): """An asset could not be read, fetched, verified or written.""" -def file_sha256(path: Union[str, Path]) -> str: +def file_sha256(path: str | Path) -> str: """The SHA-256 of a file's content, read in chunks.""" digest = hashlib.sha256() with open(path, "rb") as handle: @@ -68,7 +68,7 @@ class AssetManifest: assets: Tuple[AssetRef, ...] = () @classmethod - def from_directory(cls, root: Union[str, Path], + def from_directory(cls, root: str | Path, patterns: Iterable[str] = ("*",)) -> "AssetManifest": """List the files under ``root`` matching any of ``patterns`` (recursive).""" base = Path(root) @@ -81,7 +81,7 @@ def from_directory(cls, root: Union[str, Path], return cls(root=base, assets=tuple(found[key] for key in sorted(found))) @classmethod - def from_entries(cls, root: Union[str, Path], + def from_entries(cls, root: str | Path, entries: Mapping[str, Mapping[str, Any]]) -> "AssetManifest": """Build a manifest from ``{relative path: {"sha256": ..., "size": ...}}``.""" refs = [] @@ -129,7 +129,7 @@ class DirectoryAssetTransport: Blobs are named by their hash, so identical files are stored once. """ - def __init__(self, directory: Union[str, Path]) -> None: + def __init__(self, directory: str | Path) -> None: self._directory = Path(directory) def _blob(self, sha256: str) -> Path: @@ -438,7 +438,7 @@ def publish_assets(manifest: AssetManifest, transport: AssetTransport, *, def _checked_grace(min_age_s: Any) -> float: if isinstance(min_age_s, bool) or not isinstance(min_age_s, (int, float)) \ - or not min_age_s >= 0: + or not min_age_s >= 0: # NOSONAR python:S1940 # reason: `< 0` would let NaN through raise AssetSyncError(f"min_age_s must be a number >= 0, got {min_age_s!r}") return float(min_age_s) diff --git a/je_auto_control/utils/config_sync/blobs.py b/je_auto_control/utils/config_sync/blobs.py index 700de17b8..6c17ffe6d 100644 --- a/je_auto_control/utils/config_sync/blobs.py +++ b/je_auto_control/utils/config_sync/blobs.py @@ -29,7 +29,7 @@ import threading import time from pathlib import Path -from typing import Any, Dict, List, Optional, Union +from typing import Any, Dict, List, Optional from je_auto_control.utils.config_sync.bucket import ConfigSyncError @@ -89,7 +89,7 @@ class BlobStore: ``create_app`` does) has no side effect. """ - def __init__(self, root: Union[str, Path, None] = None, *, + def __init__(self, root: str | Path | None = None, *, max_blob_bytes: int = DEFAULT_MAX_BLOB_BYTES, quota_bytes: int = DEFAULT_BLOB_QUOTA_BYTES, max_users: int = DEFAULT_MAX_BLOB_USERS) -> None: diff --git a/je_auto_control/utils/config_sync/client.py b/je_auto_control/utils/config_sync/client.py index 1498e595f..6f072bec8 100644 --- a/je_auto_control/utils/config_sync/client.py +++ b/je_auto_control/utils/config_sync/client.py @@ -245,7 +245,8 @@ def push_operations(self, operations: Sequence["SyncOperation"], *, device_id: s merged = ConfigBucket.from_dict(remote.to_dict()) conflicts: List["SyncConflict"] = [] changed = [apply_operation(merged, operation, conflicts) for operation in operations] - if not any(changed) and not awaits_ack(remote, device_id): + # reason (suppression below): every operation must be applied; any() over a generator stops early + if not any(changed) and not awaits_ack(remote, device_id): # NOSONAR python:S7492 return SyncResult(bucket=remote, revision=remote.revision, conflicts=conflicts) settle(merged, device_id, remote.revision + 1, stamp, max_offline_s, self._tombstone_hold_s) diff --git a/je_auto_control/utils/config_sync/merge.py b/je_auto_control/utils/config_sync/merge.py index 7bdf12de4..0d99105fe 100644 --- a/je_auto_control/utils/config_sync/merge.py +++ b/je_auto_control/utils/config_sync/merge.py @@ -239,7 +239,7 @@ def settle(bucket: ConfigBucket, device_id: str, revision: int, now: float, if device != device_id and peer.last_seen < now - max_offline_s else peer) for device, peer in peers.items()} bucket.peers = {device: peer.to_dict() for device, peer in peers.items()} - for section in list(bucket.sections): + for section in bucket.sections: # each value is replaced; no key is added or removed versioned = _stamped(bucket.sync_entries(section), revision, now) kept = collect_tombstones(versioned, peers.values(), now=now, hold_s=hold_s) flat = {key: body for key, body in bucket.sections[section].items() if key not in versioned} diff --git a/je_auto_control/utils/config_sync/outbox.py b/je_auto_control/utils/config_sync/outbox.py index 3653092da..faef09366 100644 --- a/je_auto_control/utils/config_sync/outbox.py +++ b/je_auto_control/utils/config_sync/outbox.py @@ -22,7 +22,7 @@ import time from dataclasses import dataclass from pathlib import Path -from typing import Any, Callable, ContextManager, List, Optional, Sequence, Union +from typing import Any, Callable, ContextManager, List, Optional, Sequence from je_auto_control.utils.config_sync.client import ( ConfigBucket, ConfigSyncError, FullResyncRequired, @@ -86,7 +86,7 @@ class DrainReport: class SyncOutbox: """The pending operations of one account on one sync endpoint.""" - def __init__(self, db_path: Union[str, Path, None] = None, *, account: str, endpoint: str, + def __init__(self, db_path: str | Path | None = None, *, account: str, endpoint: str, base_delay_s: float = DEFAULT_BASE_DELAY_S, max_delay_s: float = DEFAULT_MAX_DELAY_S) -> None: if not account or not endpoint: diff --git a/je_auto_control/utils/config_sync/store.py b/je_auto_control/utils/config_sync/store.py index 1697202cc..2f83749a0 100644 --- a/je_auto_control/utils/config_sync/store.py +++ b/je_auto_control/utils/config_sync/store.py @@ -24,7 +24,7 @@ import json import time from pathlib import Path -from typing import Any, ContextManager, Optional, Union +from typing import Any, ContextManager, Optional from je_auto_control.utils.config_sync.bucket import ( ConfigBucket, ConfigSyncError, OperationMismatchError, @@ -115,7 +115,7 @@ class ConfigStore: a store (as ``create_app`` does) has no side effect on disk. """ - def __init__(self, db_path: Union[str, Path, None] = None, *, + def __init__(self, db_path: str | Path | None = None, *, max_users: int = DEFAULT_MAX_USERS, max_operations: int = DEFAULT_MAX_OPERATIONS) -> None: self._configured_path = db_path diff --git a/je_auto_control/utils/cookie_jar/cookie_jar.py b/je_auto_control/utils/cookie_jar/cookie_jar.py index df92a1871..6b450af0e 100644 --- a/je_auto_control/utils/cookie_jar/cookie_jar.py +++ b/je_auto_control/utils/cookie_jar/cookie_jar.py @@ -27,12 +27,13 @@ # RFC 6265bis 5.6 step 1: a set-cookie-string with a CTL other than HTAB is # ignored entirely -- "a=b\r\nX-Injected: 1" reached the Cookie header. _CTL = re.compile(r"[\x00-\x08\x0a-\x1f\x7f]") -_MAX_AGE = re.compile(r"-?[0-9]+") # 5.2.2: ASCII DIGIT and "-" only +_MAX_AGE = re.compile(r"-?\d+", re.ASCII) # 5.2.2: ASCII DIGIT and "-" only # RFC 6265 5.1.1 cookie-date grammar. _DATE_DELIMITER = re.compile(r"[\x09\x20-\x2f\x3b-\x40\x5b-\x60\x7b-\x7e]+") -_DATE_TIME = re.compile(r"([0-9]{1,2}):([0-9]{1,2}):([0-9]{1,2})(?:[^0-9].*)?", re.S) -_DATE_DAY = re.compile(r"([0-9]{1,2})(?:[^0-9].*)?", re.S) -_DATE_YEAR = re.compile(r"([0-9]{2,4})(?:[^0-9].*)?", re.S) +# re.ASCII: "\d" is then the ASCII digits the grammar names, and "\D" everything else. +_DATE_TIME = re.compile(r"(\d{1,2}):(\d{1,2}):(\d{1,2})(?:\D.*)?", re.S | re.ASCII) +_DATE_DAY = re.compile(r"(\d{1,2})(?:\D.*)?", re.S | re.ASCII) +_DATE_YEAR = re.compile(r"(\d{2,4})(?:\D.*)?", re.S | re.ASCII) _MONTHS = ("jan", "feb", "mar", "apr", "may", "jun", "jul", "aug", "sep", "oct", "nov", "dec") diff --git a/je_auto_control/utils/cost_telemetry/pricing.py b/je_auto_control/utils/cost_telemetry/pricing.py index 00e1d63e7..d2ae96f48 100644 --- a/je_auto_control/utils/cost_telemetry/pricing.py +++ b/je_auto_control/utils/cost_telemetry/pricing.py @@ -72,7 +72,9 @@ def pricing_for(model: str, # "global." as well as the two-to-four-letter regions, and OpenAI's # "-YYYY-MM-DD" snapshots: both priced at $0.00. _PROVIDER_PREFIX = re.compile(r"^(?:[a-z]+\.)?anthropic\.") -_ID_SUFFIX = re.compile(r"(?:[-@]\d{8}|-\d{4}-\d{2}-\d{2})?(?:-v\d+(?::\d+)?)?$") +_DATE_PART = r"(?:[-@]\d{8}|-\d{4}-\d{2}-\d{2})" +_VERSION_PART = r"(?:-v\d+(?::\d+)?)" +_ID_SUFFIX = re.compile(f"{_DATE_PART}?{_VERSION_PART}?$") def _base_model_id(model: str) -> str: diff --git a/je_auto_control/utils/critical_exit/critical_exit.py b/je_auto_control/utils/critical_exit/critical_exit.py index 3177a85b8..076e5682c 100644 --- a/je_auto_control/utils/critical_exit/critical_exit.py +++ b/je_auto_control/utils/critical_exit/critical_exit.py @@ -2,7 +2,7 @@ import signal import threading from threading import Event, Thread -from typing import Optional, Union +from typing import Optional from je_auto_control.utils.logging.logging_instance import autocontrol_logger from je_auto_control.wrapper.auto_control_keyboard import _resolve_keycode @@ -58,7 +58,7 @@ def __init__(self, default_daemon: bool = True): self._stop_event = Event() def set_critical_key(self, - keycode: Optional[Union[int, str]] = None) -> None: + keycode: Optional[int | str] = None) -> None: """ 設定退出鍵 Set critical exit key diff --git a/je_auto_control/utils/executor/action_executor.py b/je_auto_control/utils/executor/action_executor.py index 0ad0568ea..f3b0709ec 100644 --- a/je_auto_control/utils/executor/action_executor.py +++ b/je_auto_control/utils/executor/action_executor.py @@ -1,6 +1,6 @@ import threading import types -from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple, Union +from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple from je_auto_control.utils.exception.exception_tags import ( action_is_null_error_message, add_command_exception_error_message, @@ -2408,7 +2408,7 @@ def _decrypt_action_file(enc_path: str, key: Optional[str] = None, def _annotate_screenshot(source: str, - annotations: Union[List[Dict[str, Any]], str], + annotations: List[Dict[str, Any]] | str, output_path: str) -> Dict[str, Any]: """Executor adapter: draw annotations onto a screenshot and save it. @@ -2435,7 +2435,7 @@ def _move_to_trash(path: str) -> Dict[str, Any]: return {"trashed": move_to_trash(path)} -def _read_qr(region: Optional[Union[List[int], str]] = None) -> Dict[str, Any]: +def _read_qr(region: Optional[List[int] | str] = None) -> Dict[str, Any]: """Executor adapter: decode QR codes in a screen region. ``region`` is ``[x1, y1, x2, y2]`` (or a JSON string for the builder); @@ -2515,14 +2515,14 @@ def _save_window_layout(path: Optional[str] = None) -> Dict[str, Any]: return {"count": len(layout), "path": path, "layout": layout} -def _restore_window_layout(layout: Union[List[Dict[str, Any]], str] +def _restore_window_layout(layout: List[Dict[str, Any]] | str ) -> Dict[str, Any]: """Executor adapter: move windows back to a saved layout (list or path).""" from je_auto_control.utils.window_capture import restore_window_layout return {"restored": restore_window_layout(layout)} -def _region_color_stats(region: Optional[Union[List[int], str]] = None, +def _region_color_stats(region: Optional[List[int] | str] = None, buckets: int = 8) -> Dict[str, Any]: """Executor adapter: average + dominant colour of a screen region. @@ -8379,7 +8379,7 @@ def known_commands(self) -> set: """Return the set of all command names the executor recognises.""" return set(self.event_dict.keys()) | set(self._block_commands.keys()) - def unknown_commands_in(self, action_list: Union[list, dict]) -> List[str]: + def unknown_commands_in(self, action_list: list | dict) -> List[str]: """Return the unrecognised command names in ``action_list``, in order. Nothing is executed. Structural problems raise exactly as @@ -8457,7 +8457,7 @@ def _execute_event(self, action: list) -> Any: return event() raise AutoControlActionException(cant_execute_action_error_message + " " + describe_action(action)) - def execute_action(self, action_list: Union[list, dict], + def execute_action(self, action_list: list | dict, raise_on_error: bool = False, _validated: bool = False, dry_run: bool = False, @@ -8502,7 +8502,7 @@ def execute_action(self, action_list: Union[list, dict], _STRICT_BODIES.value = inherited _RUNNING.value = running - def _execute_list(self, action_list: Union[list, dict], raise_on_error: bool, + def _execute_list(self, action_list: list | dict, raise_on_error: bool, _validated: bool, dry_run: bool, step_callback: Optional[Callable[[list], None]], ) -> Dict[str, str]: @@ -8573,7 +8573,7 @@ def _record_unwound_signal(signal: Exception, record: Dict[str, Any], _count_recorded_failure() @staticmethod - def _unwrap_action_list(action_list: Union[list, dict]) -> list: + def _unwrap_action_list(action_list: list | dict) -> list: """Normalise the ``action_list`` argument or raise on invalid input.""" actions: Any = action_list if isinstance(actions, dict): @@ -8703,7 +8703,7 @@ def add_command_to_executor(command_dict: dict) -> None: raise AutoControlAddCommandException(add_command_exception_error_message) -def execute_action(action_list: Union[list, dict], *, +def execute_action(action_list: list | dict, *, raise_on_error: bool = False, dry_run: bool = False, step_callback: Optional[Callable[[list], None]] = None, result_callback: Optional[ResultHook] = None, diff --git a/je_auto_control/utils/executor/action_redaction.py b/je_auto_control/utils/executor/action_redaction.py index d2e941a13..6f11eabb7 100644 --- a/je_auto_control/utils/executor/action_redaction.py +++ b/je_auto_control/utils/executor/action_redaction.py @@ -190,7 +190,7 @@ def describe_action(action: Any) -> str: def _mask(argument: Any) -> Any: if isinstance(argument, dict): - return {key: _MASK for key in argument} + return dict.fromkeys(argument, _MASK) if isinstance(argument, list): return [_MASK for _ in argument] return _MASK diff --git a/je_auto_control/utils/feature_match/feature_match.py b/je_auto_control/utils/feature_match/feature_match.py index 93b4b50d5..e6c2b4682 100644 --- a/je_auto_control/utils/feature_match/feature_match.py +++ b/je_auto_control/utils/feature_match/feature_match.py @@ -128,7 +128,7 @@ def feature_match(template: ImageSource, *, located = _locate(template_gray.shape, kp1, kp2, good, min_inliers) if located is None or not (origin_x or origin_y): return located - return dataclasses.replace( + return dataclasses.replace( # NOSONAR python:S5886 # reason: replace() returns its argument's type located, corners=[[x + origin_x, y + origin_y] for x, y in located.corners], center=[located.center[0] + origin_x, located.center[1] + origin_y]) diff --git a/je_auto_control/utils/focus_order/focus_order.py b/je_auto_control/utils/focus_order/focus_order.py index 7c65d2120..3f5e192ff 100644 --- a/je_auto_control/utils/focus_order/focus_order.py +++ b/je_auto_control/utils/focus_order/focus_order.py @@ -17,7 +17,7 @@ ``accessibility.backends.get_backend()`` seam; the real ``SetFocus`` call lives in the Windows backend. Imports no ``PySide6``. """ -from typing import Any, Dict, List, Optional, Sequence, Union +from typing import Any, Dict, List, Optional, Sequence from je_auto_control.utils.accessibility.element import AccessibilityElement from je_auto_control.utils.ax_tree_walk import humanize_role @@ -44,7 +44,7 @@ }) -def _role_key(role: Union[str, int]) -> str: +def _role_key(role: str | int) -> str: """``role`` as a lookup key: humanized, AX prefix dropped, lower case, no spaces.""" text = humanize_role(role) if text.startswith("AX") and text[2:3].isupper(): @@ -52,7 +52,7 @@ def _role_key(role: Union[str, int]) -> str: return "".join(text.lower().split()).replace("_", "") -def is_interactive_role(role: Union[str, int]) -> bool: +def is_interactive_role(role: str | int) -> bool: """Return True if ``role`` is one that normally accepts keyboard focus (any platform's spelling).""" return _role_key(role) in _INTERACTIVE_KEYS diff --git a/je_auto_control/utils/generate_report/generate_xml_report.py b/je_auto_control/utils/generate_report/generate_xml_report.py index 4d1e8daee..eee0271b8 100644 --- a/je_auto_control/utils/generate_report/generate_xml_report.py +++ b/je_auto_control/utils/generate_report/generate_xml_report.py @@ -1,4 +1,4 @@ -from typing import Tuple, Union +from typing import Tuple from defusedxml.minidom import parseString # nosec B405 # nosemgrep: python.lang.security.use-defused-xml.use-defused-xml # reason: defusedxml is the safe replacement @@ -9,7 +9,7 @@ from je_auto_control.utils.xml.change_xml_structure.change_xml_structure import dict_to_elements_tree -def generate_xml() -> Tuple[Union[str, bytes], Union[str, bytes]]: +def generate_xml() -> Tuple[str | bytes, str | bytes]: """ Generate XML strings from test records. 從測試紀錄生成 XML 字串 diff --git a/je_auto_control/utils/governance/credential_broker.py b/je_auto_control/utils/governance/credential_broker.py index 32f7252ef..aaf30c486 100644 --- a/je_auto_control/utils/governance/credential_broker.py +++ b/je_auto_control/utils/governance/credential_broker.py @@ -104,7 +104,8 @@ def active(self) -> List[Dict[str, object]]: now = self._clock() result: List[Dict[str, object]] = [] with self._lock: - for token, lease in list(self._leases.items()): + # reason (suppression below): expired leases are popped inside the loop, so it walks a snapshot + for token, lease in list(self._leases.items()): # NOSONAR python:S7504 remaining = float(lease["expires_at"]) - now if remaining > 0: result.append({"token": token, "name": lease["name"], diff --git a/je_auto_control/utils/json/json_file.py b/je_auto_control/utils/json/json_file.py index 71f5bfe6c..bbad7b45b 100644 --- a/je_auto_control/utils/json/json_file.py +++ b/je_auto_control/utils/json/json_file.py @@ -2,7 +2,7 @@ import os from pathlib import Path from threading import Lock -from typing import Dict, List, Union +from typing import Dict, List from je_auto_control.utils.exception.exception_tags import cant_find_json_error_message, cant_save_json_error_message from je_auto_control.utils.exception.exceptions import AutoControlJsonActionException @@ -55,7 +55,7 @@ def read_executable_action_json(json_file_path: str) -> List[List[Dict[str, Dict raise AutoControlJsonActionException(f"{cant_find_json_error_message}: {repr(error)}") from error -def write_action_json(json_save_path: str, action_json: Union[list, dict]) -> None: +def write_action_json(json_save_path: str, action_json: list | dict) -> None: """ Write action JSON file. 寫入動作 JSON 檔案 diff --git a/je_auto_control/utils/json_schema/json_schema.py b/je_auto_control/utils/json_schema/json_schema.py index 0a9d36431..b4fa9d8ef 100644 --- a/je_auto_control/utils/json_schema/json_schema.py +++ b/je_auto_control/utils/json_schema/json_schema.py @@ -353,7 +353,7 @@ def _check_combinators(instance: Any, schema: Dict, path: str, root: _Root) -> L ) -_ARRAY_INDEX = re.compile(r"0|[1-9][0-9]*") +_ARRAY_INDEX = re.compile(r"0|[1-9]\d*", re.ASCII) def _ref_step(node: Any, token: str, ref: str) -> Any: diff --git a/je_auto_control/utils/json_store/json_store.py b/je_auto_control/utils/json_store/json_store.py index 542237030..0ab7d6468 100644 --- a/je_auto_control/utils/json_store/json_store.py +++ b/je_auto_control/utils/json_store/json_store.py @@ -12,14 +12,14 @@ import time from contextlib import contextmanager from pathlib import Path -from typing import Any, Callable, Dict, Iterator, Optional, TypeVar, Union +from typing import Any, Callable, Dict, Iterator, Optional, TypeVar from je_auto_control.utils.logging.logging_instance import autocontrol_logger _Result = TypeVar("_Result") -def atomic_write_text(path: Union[str, Path], text: str, +def atomic_write_text(path: str | Path, text: str, encoding: str = "utf-8") -> None: """Atomically write ``text`` to ``path`` via a sibling temp file + rename. @@ -31,7 +31,7 @@ def atomic_write_text(path: Union[str, Path], text: str, _atomic_write(path, lambda handle_fd: _write_text_fd(handle_fd, text, encoding)) -def atomic_write_bytes(path: Union[str, Path], data: bytes) -> None: +def atomic_write_bytes(path: str | Path, data: bytes) -> None: """Atomically write ``data`` to ``path``, byte for byte. Like :func:`atomic_write_text` -- same temp file, same rename, and on @@ -49,7 +49,7 @@ def _write_text_fd(handle_fd: int, text: str, encoding: str) -> None: handle.write(text) -def _atomic_write(path: Union[str, Path], write: Callable[[int], None]) -> None: +def _atomic_write(path: str | Path, write: Callable[[int], None]) -> None: file_path = Path(path) directory = str(file_path.parent) or "." handle_fd, tmp_name = tempfile.mkstemp( @@ -77,7 +77,10 @@ def _replace(source: str, destination: str) -> None: deadline = time.monotonic() + _REPLACE_WAIT_S while True: try: - os.replace(source, destination) + # ``source`` is the mkstemp() sibling _atomic_write made; ``destination`` + # is the file its caller asked to write. This is the shared atomic + # writer: it joins no path and takes none from a request itself. + os.replace(source, destination) # NOSONAR pythonsecurity:S8707 # reason: see above return except PermissionError: if time.monotonic() > deadline: @@ -85,7 +88,7 @@ def _replace(source: str, destination: str) -> None: time.sleep(0.01) -def append_json_line(path: Union[str, Path], line: str) -> None: +def append_json_line(path: str | Path, line: str) -> None: """Append ``line`` and a newline to a JSON-lines file. If the file does not end in a newline -- the last write was cut off -- @@ -102,7 +105,7 @@ def append_json_line(path: Union[str, Path], line: str) -> None: handle.write(line.encode("utf-8") + b"\n") -def read_json_dict(path: Optional[Union[str, Path]]) -> Dict[str, Any]: +def read_json_dict(path: Optional[str | Path]) -> Dict[str, Any]: """Return the JSON object at ``path``, or ``{}`` if missing/unreadable.""" if path is None: return {} @@ -128,7 +131,7 @@ def _read_json_object(path: Path) -> Dict[str, Any]: return data -def write_json_dict(path: Union[str, Path], data: Dict[str, Any]) -> None: +def write_json_dict(path: str | Path, data: Dict[str, Any]) -> None: """Write ``data`` as indented JSON to ``path`` (creating parent dirs).""" file_path = Path(path) file_path.parent.mkdir(parents=True, exist_ok=True) @@ -192,7 +195,7 @@ class SharedJsonDict: locators) where writing back an empty dict would erase the file. """ - def __init__(self, path: Optional[Union[str, Path]], *, + def __init__(self, path: Optional[str | Path], *, strict: bool = False) -> None: self._path = Path(path) if path is not None else None self._memory: Dict[str, Any] = {} @@ -228,7 +231,7 @@ def update(self, mutate: Callable[[Dict[str, Any]], _Result]) -> _Result: return result -def quarantine_file(path: Union[str, Path], label: str, reason: Any) -> Optional[Path]: +def quarantine_file(path: str | Path, label: str, reason: Any) -> Optional[Path]: """Move an unusable store file aside as ``.corrupt-