Skip to content

Commit e076552

Browse files
authored
Merge pull request #275 from Integration-Automation/feat/text-diff
Add text diff apply and three-way merge
2 parents 99c7085 + 9027dc8 commit e076552

15 files changed

Lines changed: 446 additions & 1 deletion

File tree

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
## Table of Contents
1515

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

120121
---
121122

123+
## What's new (2026-06-21) — Text Diff, Patch & Three-Way Merge
124+
125+
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).
126+
127+
- **`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`.
128+
122129
## What's new (2026-06-21) — Calendar Recurrence Rules (RRULE)
123130

124131
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).

README/README_zh-CN.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
## 目录
1414

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

119120
---
120121

122+
## 本次更新 (2026-06-21) — 文本 Diff、应用与三方合并
123+
124+
应用并合并文本 diff。完整参考:[`docs/source/Zh/doc/new_features/v67_features_doc.rst`](../docs/source/Zh/doc/new_features/v67_features_doc.rst)
125+
126+
- **`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`
127+
121128
## 本次更新 (2026-06-21) — 日历周期规则(RRULE)
122129

123130
排程「每月第 2 个星期二」。完整参考:[`docs/source/Zh/doc/new_features/v66_features_doc.rst`](../docs/source/Zh/doc/new_features/v66_features_doc.rst)

README/README_zh-TW.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
## 目錄
1414

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

119120
---
120121

122+
## 本次更新 (2026-06-21) — 文字 Diff、套用與三方合併
123+
124+
套用並合併文字 diff。完整參考:[`docs/source/Zh/doc/new_features/v67_features_doc.rst`](../docs/source/Zh/doc/new_features/v67_features_doc.rst)
125+
126+
- **`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`
127+
121128
## 本次更新 (2026-06-21) — 行事曆週期規則(RRULE)
122129

123130
排程「每月第 2 個星期二」。完整參考:[`docs/source/Zh/doc/new_features/v66_features_doc.rst`](../docs/source/Zh/doc/new_features/v66_features_doc.rst)
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
Text Diff, Patch & Three-Way Merge
2+
==================================
3+
4+
``difflib`` *generates* a unified diff but the standard library cannot *apply*
5+
one, and there was no three-way merge anywhere — so updating a ``.received``
6+
artifact, replaying a recorded text edit, or merging two edits of a base file
7+
had no headless primitive. This adds the missing pieces. It complements
8+
``utils/json_patch`` (structured JSON); this is line-based text.
9+
10+
Pure standard library (``difflib`` + ``re``); imports no ``PySide6``.
11+
12+
Headless API
13+
------------
14+
15+
.. code-block:: python
16+
17+
from je_auto_control import unified_diff, apply_unified, three_way_merge
18+
19+
diff = unified_diff(original, edited)
20+
restored = apply_unified(original, diff) # == edited
21+
22+
merge = three_way_merge(base, ours, theirs)
23+
if merge.clean:
24+
save(merge.text)
25+
else:
26+
print(merge.conflicts, "conflict(s)") # text has <<<<<<< markers
27+
28+
``unified_diff`` wraps ``difflib``; ``apply_unified`` is the missing applier —
29+
it walks each ``@@`` hunk, verifies the context/removed lines match, and raises
30+
``PatchApplyError`` on mismatch. ``three_way_merge`` merges line-based:
31+
non-overlapping edits from each side combine cleanly; if both sides edit the
32+
same region (and differ), it emits a conflict block with
33+
``<<<<<<< / ======= / >>>>>>>`` markers and reports ``clean=False``. Trivial
34+
cases (one side unchanged, or identical edits) resolve automatically.
35+
36+
Executor commands
37+
-----------------
38+
39+
``AC_unified_diff`` (``{diff}``), ``AC_apply_unified`` (``{result}``) and
40+
``AC_three_way_merge`` (``{text, clean, conflicts}``). Each is also exposed as
41+
an MCP tool (``ac_unified_diff`` / ``ac_apply_unified`` / ``ac_three_way_merge``)
42+
and as a Script Builder command under **Data**.

docs/source/Eng/eng_index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ Comprehensive guides for all AutoControl features.
8989
doc/new_features/v64_features_doc
9090
doc/new_features/v65_features_doc
9191
doc/new_features/v66_features_doc
92+
doc/new_features/v67_features_doc
9293
doc/ocr_backends/ocr_backends_doc
9394
doc/observability/observability_doc
9495
doc/operations_layer/operations_layer_doc
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
文字 Diff、套用與三方合併
2+
========================
3+
4+
``difflib`` 會*產生* unified diff,但標準函式庫無法*套用*它,而且各處都沒有三方合併 —— 因此更新
5+
``.received`` 產物、重播錄製的文字編輯,或合併對同一基底檔案的兩份編輯,都缺少無頭原語。本功能補上
6+
缺少的部分,與 ``utils/json_patch``(結構化 JSON)互補;這裡處理以行為單位的文字。
7+
8+
純標準函式庫(``difflib`` + ``re``);不匯入 ``PySide6``。
9+
10+
無頭 API
11+
--------
12+
13+
.. code-block:: python
14+
15+
from je_auto_control import unified_diff, apply_unified, three_way_merge
16+
17+
diff = unified_diff(original, edited)
18+
restored = apply_unified(original, diff) # == edited
19+
20+
merge = three_way_merge(base, ours, theirs)
21+
if merge.clean:
22+
save(merge.text)
23+
else:
24+
print(merge.conflicts, "個衝突") # text 含 <<<<<<< 標記
25+
26+
``unified_diff`` 包裝 ``difflib``;``apply_unified`` 是缺少的套用器 —— 它逐一走訪每個 ``@@``
27+
區塊,驗證 context/移除行是否相符,不符時拋出 ``PatchApplyError``。``three_way_merge`` 以行為單位
28+
合併:兩側不重疊的編輯會乾淨合併;若兩側編輯同一區域(且不同),則產生帶有
29+
``<<<<<<< / ======= / >>>>>>>`` 標記的衝突區塊並回報 ``clean=False``。瑣碎情況(一側未變動,或兩側
30+
相同編輯)會自動解決。
31+
32+
執行器命令
33+
----------
34+
35+
``AC_unified_diff``(``{diff}``)、``AC_apply_unified``(``{result}``)與
36+
``AC_three_way_merge``(``{text, clean, conflicts}``)。每個亦以 MCP 工具(``ac_unified_diff`` /
37+
``ac_apply_unified`` / ``ac_three_way_merge``)以及 Script Builder 中 **Data** 分類下的命令提供。

docs/source/Zh/zh_index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ AutoControl 所有功能的完整使用指南。
8989
doc/new_features/v64_features_doc
9090
doc/new_features/v65_features_doc
9191
doc/new_features/v66_features_doc
92+
doc/new_features/v67_features_doc
9293
doc/ocr_backends/ocr_backends_doc
9394
doc/observability/observability_doc
9495
doc/operations_layer/operations_layer_doc

je_auto_control/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -342,6 +342,10 @@
342342
from je_auto_control.utils.recurrence import (
343343
Recurrence, next_occurrence, occurrences, parse_rrule,
344344
)
345+
# Unified-diff generate/apply + three-way text merge
346+
from je_auto_control.utils.text_diff import (
347+
MergeResult, PatchApplyError, apply_unified, three_way_merge, unified_diff,
348+
)
345349
# Background popup/interrupt watchdog (unattended automation)
346350
from je_auto_control.utils.watchdog import (
347351
PopupWatchdog, WatchdogRule, default_popup_watchdog,
@@ -828,6 +832,8 @@ def start_autocontrol_gui(*args, **kwargs):
828832
"chi_square_2x2", "cohens_d", "describe", "normal_cdf", "percentile",
829833
"two_proportion_z_test", "welch_t_test",
830834
"Recurrence", "next_occurrence", "occurrences", "parse_rrule",
835+
"MergeResult", "PatchApplyError", "apply_unified", "three_way_merge",
836+
"unified_diff",
831837
# MCP server
832838
"AuditLogger", "HttpMCPServer", "MCPContent", "MCPPrompt",
833839
"MCPPromptArgument", "MCPResource", "MCPServer", "MCPTool",

je_auto_control/gui/script_builder/command_schema.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1155,6 +1155,31 @@ def _add_misc_specs(specs: List[CommandSpec]) -> None:
11551155
),
11561156
description="Validate JSON against a JSON Schema; returns {ok, errors}.",
11571157
))
1158+
specs.append(CommandSpec(
1159+
"AC_unified_diff", "Data", "Text: Unified Diff",
1160+
fields=(
1161+
FieldSpec("a", FieldType.STRING, placeholder="original text"),
1162+
FieldSpec("b", FieldType.STRING, placeholder="changed text"),
1163+
),
1164+
description="Unified diff transforming a into b; returns {diff}.",
1165+
))
1166+
specs.append(CommandSpec(
1167+
"AC_apply_unified", "Data", "Text: Apply Diff",
1168+
fields=(
1169+
FieldSpec("text", FieldType.STRING, placeholder="original text"),
1170+
FieldSpec("diff", FieldType.STRING, placeholder="@@ -1 +1 @@ ..."),
1171+
),
1172+
description="Apply a unified diff to text; returns {result}.",
1173+
))
1174+
specs.append(CommandSpec(
1175+
"AC_three_way_merge", "Data", "Text: Three-Way Merge",
1176+
fields=(
1177+
FieldSpec("base", FieldType.STRING, placeholder="base text"),
1178+
FieldSpec("ours", FieldType.STRING, placeholder="our text"),
1179+
FieldSpec("theirs", FieldType.STRING, placeholder="their text"),
1180+
),
1181+
description="Merge ours/theirs against base; returns {text, clean, conflicts}.",
1182+
))
11581183
specs.append(CommandSpec(
11591184
"AC_rrule_occurrences", "Flow", "Recurrence: Expand (RRULE)",
11601185
fields=(

je_auto_control/utils/executor/action_executor.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2928,6 +2928,26 @@ def _rate_limit(name: str, rate: float = 1.0, capacity: float = 1.0,
29282928
"wait": round(bucket.time_until_available(float(n)), 4)}
29292929

29302930

2931+
def _unified_diff(a: str, b: str) -> Dict[str, Any]:
2932+
"""Adapter: unified diff transforming text a into b."""
2933+
from je_auto_control.utils.text_diff import unified_diff
2934+
return {"diff": unified_diff(a, b)}
2935+
2936+
2937+
def _apply_unified(text: str, diff: str) -> Dict[str, Any]:
2938+
"""Adapter: apply a unified diff to text."""
2939+
from je_auto_control.utils.text_diff import apply_unified
2940+
return {"result": apply_unified(text, diff)}
2941+
2942+
2943+
def _three_way_merge(base: str, ours: str, theirs: str) -> Dict[str, Any]:
2944+
"""Adapter: three-way merge ours/theirs against base."""
2945+
from je_auto_control.utils.text_diff import three_way_merge
2946+
outcome = three_way_merge(base, ours, theirs)
2947+
return {"text": outcome.text, "clean": outcome.clean,
2948+
"conflicts": outcome.conflicts}
2949+
2950+
29312951
def _rrule_occurrences(rule: str, dtstart: str,
29322952
count: int = 10) -> Dict[str, Any]:
29332953
"""Adapter: expand an RRULE from an ISO dtstart into ISO datetimes."""
@@ -3834,6 +3854,9 @@ def __init__(self):
38343854
"AC_ab_significance": _ab_significance,
38353855
"AC_rrule_occurrences": _rrule_occurrences,
38363856
"AC_rrule_next": _rrule_next,
3857+
"AC_unified_diff": _unified_diff,
3858+
"AC_apply_unified": _apply_unified,
3859+
"AC_three_way_merge": _three_way_merge,
38373860
"AC_resolve_pointer": _resolve_pointer,
38383861
"AC_apply_json_patch": _apply_json_patch,
38393862
"AC_make_json_patch": _make_json_patch,

0 commit comments

Comments
 (0)