From a230b64c8e8b6591a2bcca11fcc0515adae28a15 Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Wed, 24 Jun 2026 05:49:50 +0800 Subject: [PATCH] Add text_blocks: paragraph & list grouping of OCR lines text_regions merges glyphs into lines but nothing grouped those lines into paragraphs or detected lists; ocr/structure stops at flat rows. group_paragraphs starts a new paragraph wherever the vertical gap exceeds line_gap_factor x the median line height; detect_lists recognises bullet / ordinal items by their leading marker and left indent. --- README/WHATS_NEW_zh-CN.md | 6 ++ README/WHATS_NEW_zh-TW.md | 6 ++ WHATS_NEW.md | 6 ++ .../doc/new_features/v174_features_doc.rst | 38 +++++++++ docs/source/Eng/eng_index.rst | 1 + .../Zh/doc/new_features/v174_features_doc.rst | 36 ++++++++ docs/source/Zh/zh_index.rst | 1 + je_auto_control/__init__.py | 6 ++ .../gui/script_builder/command_schema.py | 20 +++++ .../utils/executor/action_executor.py | 22 +++++ .../utils/mcp_server/tools/_factories.py | 25 ++++++ .../utils/mcp_server/tools/_handlers.py | 10 +++ je_auto_control/utils/text_blocks/__init__.py | 6 ++ .../utils/text_blocks/text_blocks.py | 82 +++++++++++++++++++ .../headless/test_text_blocks_batch.py | 59 +++++++++++++ 15 files changed, 324 insertions(+) create mode 100644 docs/source/Eng/doc/new_features/v174_features_doc.rst create mode 100644 docs/source/Zh/doc/new_features/v174_features_doc.rst create mode 100644 je_auto_control/utils/text_blocks/__init__.py create mode 100644 je_auto_control/utils/text_blocks/text_blocks.py create mode 100644 test/unit_test/headless/test_text_blocks_batch.py diff --git a/README/WHATS_NEW_zh-CN.md b/README/WHATS_NEW_zh-CN.md index a8c6d268..51f42407 100644 --- a/README/WHATS_NEW_zh-CN.md +++ b/README/WHATS_NEW_zh-CN.md @@ -1,5 +1,11 @@ # 本次更新 — AutoControl +## 本次更新 (2026-06-24) — OCR 行的段落与列表分组 + +把 OCR 行分组成段落,并检测项目符号 / 编号列表。完整参考:[`docs/source/Zh/doc/new_features/v174_features_doc.rst`](../docs/source/Zh/doc/new_features/v174_features_doc.rst)。 + +- **`group_paragraphs` / `detect_lists`**(`AC_group_paragraphs`、`AC_detect_lists`):`text_regions` 把字形并成行,但没有功能把那些行分组成段落或检测列表;`ocr/structure` 止于平面行。`group_paragraphs` 在垂直间距超过 `line_gap_factor` × 中位行高处开始新段落;`detect_lists` 识别项目符号(`•`/`-`/`*`)或序号(`1.`/`2)`/`a.`)项目,返回 `{text, marker, indent, box}`。纯标准库,作用于行字典;重用 `table_grid_fill` 的框读取器;不导入 `PySide6`。 + ## 本次更新 (2026-06-24) — 列感知阅读顺序(XY-Cut) 读多列布局时*完整读完*每一列,而非交错。完整参考:[`docs/source/Zh/doc/new_features/v173_features_doc.rst`](../docs/source/Zh/doc/new_features/v173_features_doc.rst)。 diff --git a/README/WHATS_NEW_zh-TW.md b/README/WHATS_NEW_zh-TW.md index 92a1a35c..65ca68c9 100644 --- a/README/WHATS_NEW_zh-TW.md +++ b/README/WHATS_NEW_zh-TW.md @@ -1,5 +1,11 @@ # 本次更新 — AutoControl +## 本次更新 (2026-06-24) — OCR 行的段落與清單分組 + +把 OCR 行分組成段落,並偵測項目符號 / 編號清單。完整參考:[`docs/source/Zh/doc/new_features/v174_features_doc.rst`](../docs/source/Zh/doc/new_features/v174_features_doc.rst)。 + +- **`group_paragraphs` / `detect_lists`**(`AC_group_paragraphs`、`AC_detect_lists`):`text_regions` 把字形併成行,但沒有功能把那些行分組成段落或偵測清單;`ocr/structure` 止於平面列。`group_paragraphs` 在垂直間距超過 `line_gap_factor` × 中位行高處開始新段落;`detect_lists` 辨識項目符號(`•`/`-`/`*`)或序號(`1.`/`2)`/`a.`)項目,回傳 `{text, marker, indent, box}`。純標準函式庫,作用於行字典;重用 `table_grid_fill` 的框讀取器;不匯入 `PySide6`。 + ## 本次更新 (2026-06-24) — 欄位感知閱讀順序(XY-Cut) 讀多欄版面時*完整讀完*每一欄,而非交錯。完整參考:[`docs/source/Zh/doc/new_features/v173_features_doc.rst`](../docs/source/Zh/doc/new_features/v173_features_doc.rst)。 diff --git a/WHATS_NEW.md b/WHATS_NEW.md index a4b3bcdf..144438db 100644 --- a/WHATS_NEW.md +++ b/WHATS_NEW.md @@ -1,5 +1,11 @@ # What's New — AutoControl +## What's new (2026-06-24) — Paragraph & List Grouping of OCR Lines + +Group OCR lines into paragraphs and detect bulleted / numbered lists. Full reference: [`docs/source/Eng/doc/new_features/v174_features_doc.rst`](docs/source/Eng/doc/new_features/v174_features_doc.rst). + +- **`group_paragraphs` / `detect_lists`** (`AC_group_paragraphs`, `AC_detect_lists`): `text_regions` merges glyphs into lines but nothing grouped those lines into paragraphs or detected lists; `ocr/structure` stops at flat rows. `group_paragraphs` starts a new paragraph wherever the vertical gap exceeds `line_gap_factor` × the median line height; `detect_lists` recognises bullet (`•`/`-`/`*`) or ordinal (`1.`/`2)`/`a.`) items, returning `{text, marker, indent, box}`. Pure-stdlib over line dicts; reuses `table_grid_fill`'s box reader; no `PySide6`. + ## What's new (2026-06-24) — Column-Aware Reading Order (XY-Cut) Read multi-column layouts down each column instead of interleaving them. Full reference: [`docs/source/Eng/doc/new_features/v173_features_doc.rst`](docs/source/Eng/doc/new_features/v173_features_doc.rst). diff --git a/docs/source/Eng/doc/new_features/v174_features_doc.rst b/docs/source/Eng/doc/new_features/v174_features_doc.rst new file mode 100644 index 00000000..9fc976ea --- /dev/null +++ b/docs/source/Eng/doc/new_features/v174_features_doc.rst @@ -0,0 +1,38 @@ +Paragraph & List Grouping of OCR Lines +====================================== + +``text_regions.find_text_lines`` merges glyphs into lines, but nothing groups those *lines* +into paragraphs or detects lists — ``ocr/structure`` stops at flat rows. ``text_blocks`` adds +that: ``group_paragraphs`` splits lines into paragraphs wherever the vertical gap exceeds a +multiple of the median line height (the standard whitespace-grouping heuristic), and +``detect_lists`` recognises bulleted / numbered items by their leading marker and left indent. + +Pure-stdlib over plain line dicts (text + bbox); fully unit-testable with no image and no OCR +engine. Reuses ``table_grid_fill``'s box-bounds reader. Imports no ``PySide6``. + +Headless API +------------ + +.. code-block:: python + + from je_auto_control import group_paragraphs, detect_lists + + for para in group_paragraphs(ocr_lines, line_gap_factor=1.6): + print(para["n_lines"], para["text"]) + + for item in detect_lists(ocr_lines): + print(item["marker"], item["indent"], item["text"]) + +``group_paragraphs`` returns paragraph dicts (``left`` / ``top`` / ``right`` / ``bottom`` / +``text`` / ``n_lines``) — a new paragraph begins when the gap from the previous line's bottom +exceeds ``line_gap_factor`` times the median line height. ``detect_lists`` returns the lines +whose text starts with a bullet (``•`` / ``-`` / ``*``) or an ordinal (``1.`` / ``2)`` / +``a.``), each as ``{text, marker, indent, box}`` (``indent`` is the left x, for nesting). + +Executor commands +----------------- + +``AC_group_paragraphs`` (``lines`` / ``line_gap_factor`` → ``{count, paragraphs}``) and +``AC_detect_lists`` (``lines`` → ``{count, items}``). They are exposed as the MCP tools +``ac_group_paragraphs`` / ``ac_detect_lists`` (read-only) and as the Script Builder commands +**Group Paragraphs** / **Detect Lists** under **OCR**. diff --git a/docs/source/Eng/eng_index.rst b/docs/source/Eng/eng_index.rst index 8c45b355..7d1ed13f 100644 --- a/docs/source/Eng/eng_index.rst +++ b/docs/source/Eng/eng_index.rst @@ -196,6 +196,7 @@ Comprehensive guides for all AutoControl features. doc/new_features/v171_features_doc doc/new_features/v172_features_doc doc/new_features/v173_features_doc + doc/new_features/v174_features_doc doc/ocr_backends/ocr_backends_doc doc/observability/observability_doc doc/operations_layer/operations_layer_doc diff --git a/docs/source/Zh/doc/new_features/v174_features_doc.rst b/docs/source/Zh/doc/new_features/v174_features_doc.rst new file mode 100644 index 00000000..b2827984 --- /dev/null +++ b/docs/source/Zh/doc/new_features/v174_features_doc.rst @@ -0,0 +1,36 @@ +OCR 行的段落與清單分組 +====================== + +``text_regions.find_text_lines`` 把字形併成行,但沒有任何功能把那些*行*分組成段落或偵測 +清單——``ocr/structure`` 止於平面列。``text_blocks`` 補上這點:``group_paragraphs`` 在垂直 +間距超過中位行高的某倍數處把行切成段落(標準的留白分組啟發法),而 ``detect_lists`` 以前導 +標記與左縮排辨識項目符號 / 編號項目。 + +純標準函式庫,作用於純行字典(text + bbox);可在無影像、無 OCR 引擎下完整單元測試。重用 +``table_grid_fill`` 的框邊界讀取器。不匯入 ``PySide6``。 + +無頭 API +-------- + +.. code-block:: python + + from je_auto_control import group_paragraphs, detect_lists + + for para in group_paragraphs(ocr_lines, line_gap_factor=1.6): + print(para["n_lines"], para["text"]) + + for item in detect_lists(ocr_lines): + print(item["marker"], item["indent"], item["text"]) + +``group_paragraphs`` 回傳段落字典(``left`` / ``top`` / ``right`` / ``bottom`` / ``text`` / +``n_lines``)——當與前一行底部的間距超過 ``line_gap_factor`` 乘以中位行高時,開始新段落。 +``detect_lists`` 回傳文字以項目符號(``•`` / ``-`` / ``*``)或序號(``1.`` / ``2)`` / +``a.``)開頭的行,每個為 ``{text, marker, indent, box}``(``indent`` 為左 x,供巢狀用)。 + +執行器指令 +---------- + +``AC_group_paragraphs``(``lines`` / ``line_gap_factor`` → ``{count, paragraphs}``)與 +``AC_detect_lists``(``lines`` → ``{count, items}``)。兩者以 MCP 工具 ``ac_group_paragraphs`` / +``ac_detect_lists``(唯讀)及 Script Builder 指令 **Group Paragraphs** / **Detect Lists** +(位於 **OCR** 分類下)形式提供。 diff --git a/docs/source/Zh/zh_index.rst b/docs/source/Zh/zh_index.rst index 73f0b3c7..198b3e6a 100644 --- a/docs/source/Zh/zh_index.rst +++ b/docs/source/Zh/zh_index.rst @@ -196,6 +196,7 @@ AutoControl 所有功能的完整使用指南。 doc/new_features/v171_features_doc doc/new_features/v172_features_doc doc/new_features/v173_features_doc + doc/new_features/v174_features_doc doc/ocr_backends/ocr_backends_doc doc/observability/observability_doc doc/operations_layer/operations_layer_doc diff --git a/je_auto_control/__init__.py b/je_auto_control/__init__.py index 22ed3605..0be599b3 100644 --- a/je_auto_control/__init__.py +++ b/je_auto_control/__init__.py @@ -319,6 +319,10 @@ from je_auto_control.utils.reading_flow import ( flow_order, to_blocks, xy_cut, ) +# Group OCR lines into paragraphs and bulleted / numbered lists +from je_auto_control.utils.text_blocks import ( + detect_lists, group_paragraphs, +) # Associate form labels with values (multi-direction) + checkbox state from je_auto_control.utils.form_fields import ( associate_fields, checkbox_state, match_labels_to_widgets, @@ -1279,6 +1283,8 @@ def start_autocontrol_gui(*args, **kwargs): "xy_cut", "flow_order", "to_blocks", + "group_paragraphs", + "detect_lists", "associate_fields", "match_labels_to_widgets", "checkbox_state", diff --git a/je_auto_control/gui/script_builder/command_schema.py b/je_auto_control/gui/script_builder/command_schema.py index ae7cb05e..a022c6d5 100644 --- a/je_auto_control/gui/script_builder/command_schema.py +++ b/je_auto_control/gui/script_builder/command_schema.py @@ -807,6 +807,26 @@ def _add_ocr_specs(specs: List[CommandSpec]) -> None: ), 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), + ), + 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"}]'), + ), + description="Detect bulleted / numbered list items among OCR lines.", + )) specs.append(CommandSpec( "AC_scroll_to_find", "OCR", "Scroll Until Visible", fields=( diff --git a/je_auto_control/utils/executor/action_executor.py b/je_auto_control/utils/executor/action_executor.py index 6b8cc60a..cc4bd1b7 100644 --- a/je_auto_control/utils/executor/action_executor.py +++ b/je_auto_control/utils/executor/action_executor.py @@ -3517,6 +3517,26 @@ def _xy_cut(boxes: Any, min_gap: Any = 12) -> Dict[str, Any]: return {"tree": xy_cut(boxes, min_gap=int(min_gap))} +def _group_paragraphs(lines: Any, line_gap_factor: Any = 1.6) -> Dict[str, Any]: + """Adapter: group OCR lines into paragraphs by vertical spacing.""" + import json + from je_auto_control.utils.text_blocks import group_paragraphs + if isinstance(lines, str): + lines = json.loads(lines) + paragraphs = group_paragraphs(lines, line_gap_factor=float(line_gap_factor)) + return {"count": len(paragraphs), "paragraphs": paragraphs} + + +def _detect_lists(lines: Any) -> Dict[str, Any]: + """Adapter: detect bulleted / numbered list items among OCR lines.""" + import json + from je_auto_control.utils.text_blocks import detect_lists + if isinstance(lines, str): + lines = json.loads(lines) + items = detect_lists(lines) + return {"count": len(items), "items": items} + + def _find_color_region(rgb: Any, tolerance: Any = 20, min_area: Any = 50, region: Any = None) -> Dict[str, Any]: """Adapter: locate coloured regions on the screen, largest first.""" @@ -6054,6 +6074,8 @@ def __init__(self): "AC_match_labels_to_widgets": _match_labels_to_widgets, "AC_flow_order": _flow_order, "AC_xy_cut": _xy_cut, + "AC_group_paragraphs": _group_paragraphs, + "AC_detect_lists": _detect_lists, "AC_ssim_compare": _ssim_compare, "AC_ssim_changed_regions": _ssim_changed_regions, "AC_feature_match": _feature_match, diff --git a/je_auto_control/utils/mcp_server/tools/_factories.py b/je_auto_control/utils/mcp_server/tools/_factories.py index 3a0d9802..2793876e 100644 --- a/je_auto_control/utils/mcp_server/tools/_factories.py +++ b/je_auto_control/utils/mcp_server/tools/_factories.py @@ -3950,6 +3950,31 @@ def screen_grid_tools() -> List[MCPTool]: handler=h.xy_cut, annotations=READ_ONLY, ), + MCPTool( + name="ac_group_paragraphs", + description=("Group OCR 'lines' (text + bbox) into paragraphs: a new " + "paragraph starts where the vertical gap exceeds " + "'line_gap_factor' x the median line height. Returns {count, " + "paragraphs:[{left,top,right,bottom,text,n_lines}]}."), + input_schema=schema({ + "lines": {"type": "array", "items": {"type": "object"}}, + "line_gap_factor": {"type": "number"}}, + required=["lines"]), + handler=h.group_paragraphs, + annotations=READ_ONLY, + ), + MCPTool( + name="ac_detect_lists", + description=("Detect bulleted / numbered list items among OCR 'lines' by " + "their leading marker (•/-/* or 1./2)/a.). Returns {count, " + "items:[{text,marker,indent,box}]}; 'indent' is the left x " + "for nesting."), + input_schema=schema({ + "lines": {"type": "array", "items": {"type": "object"}}}, + required=["lines"]), + handler=h.detect_lists, + annotations=READ_ONLY, + ), ] diff --git a/je_auto_control/utils/mcp_server/tools/_handlers.py b/je_auto_control/utils/mcp_server/tools/_handlers.py index d94c489c..8a6fc2ef 100644 --- a/je_auto_control/utils/mcp_server/tools/_handlers.py +++ b/je_auto_control/utils/mcp_server/tools/_handlers.py @@ -2195,6 +2195,16 @@ def xy_cut(boxes, min_gap=12): return _xy_cut(boxes, min_gap) +def group_paragraphs(lines, line_gap_factor=1.6): + from je_auto_control.utils.executor.action_executor import _group_paragraphs + return _group_paragraphs(lines, line_gap_factor) + + +def detect_lists(lines): + from je_auto_control.utils.executor.action_executor import _detect_lists + return _detect_lists(lines) + + def find_color_region(rgb, tolerance=20, min_area=50, region=None): from je_auto_control.utils.executor.action_executor import ( _find_color_region) diff --git a/je_auto_control/utils/text_blocks/__init__.py b/je_auto_control/utils/text_blocks/__init__.py new file mode 100644 index 00000000..3ca86a09 --- /dev/null +++ b/je_auto_control/utils/text_blocks/__init__.py @@ -0,0 +1,6 @@ +"""Group OCR lines into paragraphs and bulleted / numbered lists.""" +from je_auto_control.utils.text_blocks.text_blocks import ( + detect_lists, group_paragraphs, +) + +__all__ = ["group_paragraphs", "detect_lists"] diff --git a/je_auto_control/utils/text_blocks/text_blocks.py b/je_auto_control/utils/text_blocks/text_blocks.py new file mode 100644 index 00000000..36ba4e10 --- /dev/null +++ b/je_auto_control/utils/text_blocks/text_blocks.py @@ -0,0 +1,82 @@ +"""Group OCR lines into paragraphs and bulleted / numbered lists. + +``text_regions.find_text_lines`` merges glyphs into lines, but nothing groups those *lines* +into paragraphs or detects lists — ``ocr/structure`` stops at flat rows. ``text_blocks`` adds +that: ``group_paragraphs`` splits lines into paragraphs wherever the vertical gap exceeds a +multiple of the median line height (the standard whitespace-grouping heuristic), and +``detect_lists`` recognises bulleted / numbered items by their leading marker and left indent. + +Pure-stdlib over plain line dicts (text + bbox); fully unit-testable with no image and no OCR +engine. Reuses ``table_grid_fill``'s box-bounds reader. Imports no ``PySide6``. +""" +import re +from typing import Any, Dict, List, Sequence + +from je_auto_control.utils.table_grid_fill.table_grid_fill import _box_bounds + +Line = Dict[str, Any] + +_MARKER = re.compile(r"^\s*([•‣◦\-\*]|\d+[.)]|[A-Za-z][.)])\s+") + + +def _height(line: Line) -> int: + _, top, _, bottom = _box_bounds(line) + return bottom - top + + +def _make_paragraph(lines: Sequence[Line]) -> Dict[str, Any]: + """Build a paragraph dict (union bbox + joined text) from its lines.""" + bounds = [_box_bounds(line) for line in lines] + left = min(b[0] for b in bounds) + top = min(b[1] for b in bounds) + right = max(b[2] for b in bounds) + bottom = max(b[3] for b in bounds) + text = " ".join(str(line.get("text", "")).strip() for line in lines).strip() + return {"left": left, "top": top, "right": right, "bottom": bottom, + "text": text, "n_lines": len(lines)} + + +def group_paragraphs(lines: Sequence[Line], *, + line_gap_factor: float = 1.6) -> List[Dict[str, Any]]: + """Group lines into paragraphs, splitting where the vertical gap is large. + + A new paragraph starts when the gap from the previous line's bottom exceeds + ``line_gap_factor`` times the median line height. Returns paragraph dicts + (``left`` / ``top`` / ``right`` / ``bottom`` / ``text`` / ``n_lines``). + """ + ordered = sorted(lines, key=lambda line: _box_bounds(line)[1]) + if not ordered: + return [] + heights = sorted(_height(line) for line in ordered) + threshold = heights[len(heights) // 2] * float(line_gap_factor) + paragraphs: List[List[Line]] = [[ordered[0]]] + prev_bottom = _box_bounds(ordered[0])[3] + for line in ordered[1:]: + top, bottom = _box_bounds(line)[1], _box_bounds(line)[3] + if top - prev_bottom > threshold: + paragraphs.append([line]) + else: + paragraphs[-1].append(line) + prev_bottom = bottom + return [_make_paragraph(group) for group in paragraphs] + + +def detect_lists(lines: Sequence[Line]) -> List[Dict[str, Any]]: + """Return the lines that are list items: ``{text, marker, indent, box}``. + + A line is a list item when its text starts with a bullet (``•`` / ``-`` / ``*``) + or an ordinal (``1.`` / ``2)`` / ``a.``); ``indent`` is its left x (for nesting), + ``text`` is the content after the marker. + """ + items: List[Dict[str, Any]] = [] + for line in lines: + text = str(line.get("text", "")) + match = _MARKER.match(text) + if match is None: + continue + left, top, right, bottom = _box_bounds(line) + items.append({"text": text[match.end():].strip(), + "marker": match.group(1), "indent": left, + "box": {"left": left, "top": top, "right": right, + "bottom": bottom}}) + return items diff --git a/test/unit_test/headless/test_text_blocks_batch.py b/test/unit_test/headless/test_text_blocks_batch.py new file mode 100644 index 00000000..46d837b6 --- /dev/null +++ b/test/unit_test/headless/test_text_blocks_batch.py @@ -0,0 +1,59 @@ +"""Headless tests for paragraph / list grouping of OCR lines (pure stdlib).""" +import je_auto_control as ac +from je_auto_control.utils.text_blocks import detect_lists, group_paragraphs + + +def _line(y, text, x=0, w=200, h=20): + return {"x": x, "y": y, "width": w, "height": h, "text": text} + + +def test_group_paragraphs_splits_on_large_gap(): + # two tight lines, a big gap, then two more tight lines → 2 paragraphs + lines = [_line(0, "a1"), _line(25, "a2"), + _line(120, "b1"), _line(145, "b2")] + paras = group_paragraphs(lines, line_gap_factor=1.6) + assert len(paras) == 2 + assert paras[0]["text"] == "a1 a2" and paras[1]["text"] == "b1 b2" + assert paras[0]["n_lines"] == 2 + + +def test_group_paragraphs_single_block(): + lines = [_line(0, "x"), _line(25, "y"), _line(50, "z")] + paras = group_paragraphs(lines) + assert len(paras) == 1 and paras[0]["text"] == "x y z" + + +def test_detect_lists_bullets_and_ordinals(): + lines = [_line(0, "• first"), _line(30, "2) second"), + _line(60, "a. third"), _line(90, "not a list item")] + items = detect_lists(lines) + assert [i["text"] for i in items] == ["first", "second", "third"] + assert items[0]["marker"] == "•" and items[1]["marker"] == "2)" + + +def test_detect_lists_indent_recorded(): + items = detect_lists([_line(0, "- top", x=10), _line(30, "- nested", x=40)]) + assert items[0]["indent"] == 10 and items[1]["indent"] == 40 + + +def test_empty(): + assert group_paragraphs([]) == [] + assert detect_lists([]) == [] + + +# --- wiring --------------------------------------------------------------- + +def test_wiring(): + known = set(ac.executor.known_commands()) + assert {"AC_group_paragraphs", "AC_detect_lists"} <= known + from je_auto_control.utils.mcp_server.tools import build_default_tool_registry + names = {t.name for t in build_default_tool_registry()} + assert {"ac_group_paragraphs", "ac_detect_lists"} <= names + from je_auto_control.gui.script_builder.command_schema import _build_specs + specs = {s.command for s in _build_specs()} + assert {"AC_group_paragraphs", "AC_detect_lists"} <= specs + + +def test_facade_exports(): + for name in ("group_paragraphs", "detect_lists"): + assert hasattr(ac, name) and name in ac.__all__