Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

## Table of Contents

- [What's new (2026-06-21) — Text Diff, Patch & Three-Way Merge](#whats-new-2026-06-21--text-diff-patch--three-way-merge)
- [What's new (2026-06-21) — Calendar Recurrence Rules (RRULE)](#whats-new-2026-06-21--calendar-recurrence-rules-rrule)
- [What's new (2026-06-21) — Statistics & A/B Significance](#whats-new-2026-06-21--statistics--ab-significance)
- [What's new (2026-06-21) — Full-Text Search (BM25)](#whats-new-2026-06-21--full-text-search-bm25)
Expand Down Expand Up @@ -119,6 +120,12 @@

---

## What's new (2026-06-21) — Text Diff, Patch & Three-Way Merge

Apply and merge text diffs. Full reference: [`docs/source/Eng/doc/new_features/v67_features_doc.rst`](docs/source/Eng/doc/new_features/v67_features_doc.rst).

- **`unified_diff` / `apply_unified` / `three_way_merge`** (`AC_unified_diff`, `AC_apply_unified`, `AC_three_way_merge`): `difflib` *generates* a unified diff but the stdlib can't *apply* one, and there was no three-way merge. This adds the missing applier (walks `@@` hunks, verifies context, raises on mismatch) and a line-based three-way merge (non-overlapping edits combine cleanly; overlapping ones emit `<<<<<<<` conflict markers). Complements `json_patch` (structured JSON); pure-stdlib `difflib`.

## What's new (2026-06-21) — Calendar Recurrence Rules (RRULE)

Schedule "every 2nd Tuesday". Full reference: [`docs/source/Eng/doc/new_features/v66_features_doc.rst`](docs/source/Eng/doc/new_features/v66_features_doc.rst).
Expand Down
7 changes: 7 additions & 0 deletions README/README_zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

## 目录

- [本次更新 (2026-06-21) — 文本 Diff、应用与三方合并](#本次更新-2026-06-21--文本-diff应用与三方合并)
- [本次更新 (2026-06-21) — 日历周期规则(RRULE)](#本次更新-2026-06-21--日历周期规则rrule)
- [本次更新 (2026-06-21) — 统计与 A/B 显著性](#本次更新-2026-06-21--统计与-ab-显著性)
- [本次更新 (2026-06-21) — 全文搜索(BM25)](#本次更新-2026-06-21--全文搜索bm25)
Expand Down Expand Up @@ -118,6 +119,12 @@

---

## 本次更新 (2026-06-21) — 文本 Diff、应用与三方合并

应用并合并文本 diff。完整参考:[`docs/source/Zh/doc/new_features/v67_features_doc.rst`](../docs/source/Zh/doc/new_features/v67_features_doc.rst)。

- **`unified_diff` / `apply_unified` / `three_way_merge`**(`AC_unified_diff`、`AC_apply_unified`、`AC_three_way_merge`):`difflib` 会*生成* unified diff,但标准库无法*应用*,也没有三方合并。本功能补上缺少的应用器(走访 `@@` 块、验证 context、不符即抛出)与以行为单位的三方合并(不重叠编辑干净合并;重叠则产生 `<<<<<<<` 冲突标记)。与 `json_patch`(结构化 JSON)互补;纯标准库 `difflib`。

## 本次更新 (2026-06-21) — 日历周期规则(RRULE)

排程「每月第 2 个星期二」。完整参考:[`docs/source/Zh/doc/new_features/v66_features_doc.rst`](../docs/source/Zh/doc/new_features/v66_features_doc.rst)。
Expand Down
7 changes: 7 additions & 0 deletions README/README_zh-TW.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

## 目錄

- [本次更新 (2026-06-21) — 文字 Diff、套用與三方合併](#本次更新-2026-06-21--文字-diff套用與三方合併)
- [本次更新 (2026-06-21) — 行事曆週期規則(RRULE)](#本次更新-2026-06-21--行事曆週期規則rrule)
- [本次更新 (2026-06-21) — 統計與 A/B 顯著性](#本次更新-2026-06-21--統計與-ab-顯著性)
- [本次更新 (2026-06-21) — 全文搜尋(BM25)](#本次更新-2026-06-21--全文搜尋bm25)
Expand Down Expand Up @@ -118,6 +119,12 @@

---

## 本次更新 (2026-06-21) — 文字 Diff、套用與三方合併

套用並合併文字 diff。完整參考:[`docs/source/Zh/doc/new_features/v67_features_doc.rst`](../docs/source/Zh/doc/new_features/v67_features_doc.rst)。

- **`unified_diff` / `apply_unified` / `three_way_merge`**(`AC_unified_diff`、`AC_apply_unified`、`AC_three_way_merge`):`difflib` 會*產生* unified diff,但標準函式庫無法*套用*,也沒有三方合併。本功能補上缺少的套用器(走訪 `@@` 區塊、驗證 context、不符即拋出)與以行為單位的三方合併(不重疊編輯乾淨合併;重疊則產生 `<<<<<<<` 衝突標記)。與 `json_patch`(結構化 JSON)互補;純標準函式庫 `difflib`。

## 本次更新 (2026-06-21) — 行事曆週期規則(RRULE)

排程「每月第 2 個星期二」。完整參考:[`docs/source/Zh/doc/new_features/v66_features_doc.rst`](../docs/source/Zh/doc/new_features/v66_features_doc.rst)。
Expand Down
42 changes: 42 additions & 0 deletions docs/source/Eng/doc/new_features/v67_features_doc.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
Text Diff, Patch & Three-Way Merge
==================================

``difflib`` *generates* a unified diff but the standard library cannot *apply*
one, and there was no three-way merge anywhere — so updating a ``.received``
artifact, replaying a recorded text edit, or merging two edits of a base file
had no headless primitive. This adds the missing pieces. It complements
``utils/json_patch`` (structured JSON); this is line-based text.

Pure standard library (``difflib`` + ``re``); imports no ``PySide6``.

Headless API
------------

.. code-block:: python

from je_auto_control import unified_diff, apply_unified, three_way_merge

diff = unified_diff(original, edited)
restored = apply_unified(original, diff) # == edited

merge = three_way_merge(base, ours, theirs)
if merge.clean:
save(merge.text)
else:
print(merge.conflicts, "conflict(s)") # text has <<<<<<< markers

``unified_diff`` wraps ``difflib``; ``apply_unified`` is the missing applier —
it walks each ``@@`` hunk, verifies the context/removed lines match, and raises
``PatchApplyError`` on mismatch. ``three_way_merge`` merges line-based:
non-overlapping edits from each side combine cleanly; if both sides edit the
same region (and differ), it emits a conflict block with
``<<<<<<< / ======= / >>>>>>>`` markers and reports ``clean=False``. Trivial
cases (one side unchanged, or identical edits) resolve automatically.

Executor commands
-----------------

``AC_unified_diff`` (``{diff}``), ``AC_apply_unified`` (``{result}``) and
``AC_three_way_merge`` (``{text, clean, conflicts}``). Each is also exposed as
an MCP tool (``ac_unified_diff`` / ``ac_apply_unified`` / ``ac_three_way_merge``)
and as a Script Builder command under **Data**.
1 change: 1 addition & 0 deletions docs/source/Eng/eng_index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ Comprehensive guides for all AutoControl features.
doc/new_features/v64_features_doc
doc/new_features/v65_features_doc
doc/new_features/v66_features_doc
doc/new_features/v67_features_doc
doc/ocr_backends/ocr_backends_doc
doc/observability/observability_doc
doc/operations_layer/operations_layer_doc
Expand Down
37 changes: 37 additions & 0 deletions docs/source/Zh/doc/new_features/v67_features_doc.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
文字 Diff、套用與三方合併
========================

``difflib`` 會*產生* unified diff,但標準函式庫無法*套用*它,而且各處都沒有三方合併 —— 因此更新
``.received`` 產物、重播錄製的文字編輯,或合併對同一基底檔案的兩份編輯,都缺少無頭原語。本功能補上
缺少的部分,與 ``utils/json_patch``(結構化 JSON)互補;這裡處理以行為單位的文字。

純標準函式庫(``difflib`` + ``re``);不匯入 ``PySide6``。

無頭 API
--------

.. code-block:: python

from je_auto_control import unified_diff, apply_unified, three_way_merge

diff = unified_diff(original, edited)
restored = apply_unified(original, diff) # == edited

merge = three_way_merge(base, ours, theirs)
if merge.clean:
save(merge.text)
else:
print(merge.conflicts, "個衝突") # text 含 <<<<<<< 標記

``unified_diff`` 包裝 ``difflib``;``apply_unified`` 是缺少的套用器 —— 它逐一走訪每個 ``@@``
區塊,驗證 context/移除行是否相符,不符時拋出 ``PatchApplyError``。``three_way_merge`` 以行為單位
合併:兩側不重疊的編輯會乾淨合併;若兩側編輯同一區域(且不同),則產生帶有
``<<<<<<< / ======= / >>>>>>>`` 標記的衝突區塊並回報 ``clean=False``。瑣碎情況(一側未變動,或兩側
相同編輯)會自動解決。

執行器命令
----------

``AC_unified_diff``(``{diff}``)、``AC_apply_unified``(``{result}``)與
``AC_three_way_merge``(``{text, clean, conflicts}``)。每個亦以 MCP 工具(``ac_unified_diff`` /
``ac_apply_unified`` / ``ac_three_way_merge``)以及 Script Builder 中 **Data** 分類下的命令提供。
1 change: 1 addition & 0 deletions docs/source/Zh/zh_index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ AutoControl 所有功能的完整使用指南。
doc/new_features/v64_features_doc
doc/new_features/v65_features_doc
doc/new_features/v66_features_doc
doc/new_features/v67_features_doc
doc/ocr_backends/ocr_backends_doc
doc/observability/observability_doc
doc/operations_layer/operations_layer_doc
Expand Down
6 changes: 6 additions & 0 deletions je_auto_control/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,10 @@
from je_auto_control.utils.recurrence import (
Recurrence, next_occurrence, occurrences, parse_rrule,
)
# Unified-diff generate/apply + three-way text merge
from je_auto_control.utils.text_diff import (
MergeResult, PatchApplyError, apply_unified, three_way_merge, unified_diff,
)
# Background popup/interrupt watchdog (unattended automation)
from je_auto_control.utils.watchdog import (
PopupWatchdog, WatchdogRule, default_popup_watchdog,
Expand Down Expand Up @@ -828,6 +832,8 @@ def start_autocontrol_gui(*args, **kwargs):
"chi_square_2x2", "cohens_d", "describe", "normal_cdf", "percentile",
"two_proportion_z_test", "welch_t_test",
"Recurrence", "next_occurrence", "occurrences", "parse_rrule",
"MergeResult", "PatchApplyError", "apply_unified", "three_way_merge",
"unified_diff",
# MCP server
"AuditLogger", "HttpMCPServer", "MCPContent", "MCPPrompt",
"MCPPromptArgument", "MCPResource", "MCPServer", "MCPTool",
Expand Down
25 changes: 25 additions & 0 deletions je_auto_control/gui/script_builder/command_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -1155,6 +1155,31 @@ def _add_misc_specs(specs: List[CommandSpec]) -> None:
),
description="Validate JSON against a JSON Schema; returns {ok, errors}.",
))
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"),
),
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 @@ ..."),
),
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"),
),
description="Merge ours/theirs against base; returns {text, clean, conflicts}.",
))
specs.append(CommandSpec(
"AC_rrule_occurrences", "Flow", "Recurrence: Expand (RRULE)",
fields=(
Expand Down
23 changes: 23 additions & 0 deletions je_auto_control/utils/executor/action_executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -2928,6 +2928,26 @@ def _rate_limit(name: str, rate: float = 1.0, capacity: float = 1.0,
"wait": round(bucket.time_until_available(float(n)), 4)}


def _unified_diff(a: str, b: str) -> Dict[str, Any]:
"""Adapter: unified diff transforming text a into b."""
from je_auto_control.utils.text_diff import unified_diff
return {"diff": unified_diff(a, b)}


def _apply_unified(text: str, diff: str) -> Dict[str, Any]:
"""Adapter: apply a unified diff to text."""
from je_auto_control.utils.text_diff import apply_unified
return {"result": apply_unified(text, diff)}


def _three_way_merge(base: str, ours: str, theirs: str) -> Dict[str, Any]:
"""Adapter: three-way merge ours/theirs against base."""
from je_auto_control.utils.text_diff import three_way_merge
outcome = three_way_merge(base, ours, theirs)
return {"text": outcome.text, "clean": outcome.clean,
"conflicts": outcome.conflicts}


def _rrule_occurrences(rule: str, dtstart: str,
count: int = 10) -> Dict[str, Any]:
"""Adapter: expand an RRULE from an ISO dtstart into ISO datetimes."""
Expand Down Expand Up @@ -3834,6 +3854,9 @@ def __init__(self):
"AC_ab_significance": _ab_significance,
"AC_rrule_occurrences": _rrule_occurrences,
"AC_rrule_next": _rrule_next,
"AC_unified_diff": _unified_diff,
"AC_apply_unified": _apply_unified,
"AC_three_way_merge": _three_way_merge,
"AC_resolve_pointer": _resolve_pointer,
"AC_apply_json_patch": _apply_json_patch,
"AC_make_json_patch": _make_json_patch,
Expand Down
36 changes: 35 additions & 1 deletion je_auto_control/utils/mcp_server/tools/_factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -3343,6 +3343,40 @@ def rate_limit_tools() -> List[MCPTool]:
]


def text_diff_tools() -> List[MCPTool]:
return [
MCPTool(
name="ac_unified_diff",
description="Unified diff transforming text 'a' into 'b'. "
"Returns {diff}.",
input_schema=schema(
{"a": {"type": "string"}, "b": {"type": "string"}}, ["a", "b"]),
handler=h.unified_diff,
annotations=READ_ONLY,
),
MCPTool(
name="ac_apply_unified",
description="Apply a unified 'diff' to 'text' (raises on context "
"mismatch). Returns {result}.",
input_schema=schema(
{"text": {"type": "string"}, "diff": {"type": "string"}},
["text", "diff"]),
handler=h.apply_unified,
annotations=READ_ONLY,
),
MCPTool(
name="ac_three_way_merge",
description="Three-way merge 'ours' and 'theirs' against 'base' "
"(line-based). Returns {text, clean, conflicts}.",
input_schema=schema(
{"base": {"type": "string"}, "ours": {"type": "string"},
"theirs": {"type": "string"}}, ["base", "ours", "theirs"]),
handler=h.three_way_merge,
annotations=READ_ONLY,
),
]


def recurrence_tools() -> List[MCPTool]:
return [
MCPTool(
Expand Down Expand Up @@ -4656,7 +4690,7 @@ def media_assert_tools() -> List[MCPTool]:
process_mining_tools, asset_tools, events_tools, notify_channel_tools,
jsonpath_tools, json_schema_tools, vuln_scan_tools, vex_tools,
license_policy_tools, jwt_tools, rate_limit_tools, json_patch_tools,
search_index_tools, stats_tools, recurrence_tools,
search_index_tools, stats_tools, recurrence_tools, text_diff_tools,
saga_tools, decision_table_tools, locator_repair_tools,
pii_text_tools, sarif_tools,
screen_record_tools,
Expand Down
17 changes: 17 additions & 0 deletions je_auto_control/utils/mcp_server/tools/_handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -1664,6 +1664,23 @@ def rrule_next(rule, dtstart, now=None):
return {"next": moment.isoformat() if moment else None}


def unified_diff(a, b):
from je_auto_control.utils.text_diff import unified_diff as _diff
return {"diff": _diff(a, b)}


def apply_unified(text, diff):
from je_auto_control.utils.text_diff import apply_unified as _apply
return {"result": _apply(text, diff)}


def three_way_merge(base, ours, theirs):
from je_auto_control.utils.text_diff import three_way_merge as _merge
outcome = _merge(base, ours, theirs)
return {"text": outcome.text, "clean": outcome.clean,
"conflicts": outcome.conflicts}


def run_saga(steps):
from je_auto_control.utils.saga import run_saga as _run
result = _run(steps)
Expand Down
9 changes: 9 additions & 0 deletions je_auto_control/utils/text_diff/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
"""Unified-diff generation, application and three-way text merge."""
from je_auto_control.utils.text_diff.text_diff import (
MergeResult, PatchApplyError, apply_unified, three_way_merge, unified_diff,
)

__all__ = [
"MergeResult", "PatchApplyError", "apply_unified", "three_way_merge",
"unified_diff",
]
Loading
Loading