This is an automated email from the ASF dual-hosted git repository.

github-merge-queue[bot] pushed a commit to branch 
gh-readonly-queue/dev/pr-12241-febe1f947a37dfee43064a5c49b34bd4ff7cf6ff
in repository https://gitbox.apache.org/repos/asf/seatunnel.git

commit c24e46adfd2efc32e2aa6b37caf8eb808dc491ed
Author: Goutam Adwant <[email protected]>
AuthorDate: Fri Sep 18 14:54:08 2026 +0000

    [Test][CLI] Add opt-in paraphrase benchmark suite (#12241)
    
    Signed-off-by: Goutam Adwant <[email protected]>
---
 docs/en/ai-cli/benchmark.md                       |  37 +++
 docs/zh/ai-cli/benchmark.md                       |  32 ++
 seatunnel-cli/benchmark/README.md                 |  50 +++
 seatunnel-cli/benchmark/compare.py                |  18 +
 seatunnel-cli/benchmark/paraphrases.py            |  73 ++++
 seatunnel-cli/benchmark/runner.py                 |  58 +++-
 seatunnel-cli/benchmark/tasks/paraphrase.json     |  81 +++++
 seatunnel-cli/tests/test_benchmark_comparison.py  |  44 +++
 seatunnel-cli/tests/test_benchmark_paraphrases.py | 386 ++++++++++++++++++++++
 9 files changed, 775 insertions(+), 4 deletions(-)

diff --git a/docs/en/ai-cli/benchmark.md b/docs/en/ai-cli/benchmark.md
index 4c0c3e6d35..173397d17c 100644
--- a/docs/en/ai-cli/benchmark.md
+++ b/docs/en/ai-cli/benchmark.md
@@ -8,6 +8,43 @@ The AI CLI's accuracy is measured — not assumed — by a 
dedicated benchmark:
 
 ## Comparing CLI Revisions
 
+### Optional alternative-wording checks
+
+The default 100-task benchmark is unchanged. To run the separate public
+paraphrase suite of 12 tasks:
+
+```bash
+cd seatunnel-cli
+python -m benchmark.runner --provider openai --model gpt-4o \
+    --suite paraphrase --out benchmark/paraphrase-baseline
+```
+
+The suite covers routing, CDC prerequisites and connector options/mode 
selection,
+including one Chinese routing prompt. It runs only variants, without adding the
+original tasks to the denominator. `--tiers` filters inherited tiers; `--tasks`
+uses distinct IDs such as `t2_cdc_pg_kafka_p1`. Invalid or duplicate variant
+selections fail before provider setup. The usual generation, repair and gate
+pipeline applies: this command calls a model, even with `--level l1`.
+
+Each variant inherits all assertions and execution fixtures from a 
fingerprinted
+baseline task. Changed parents require review and explicit repinning, not 
silent
+expectation updates. Saved results retain parent provenance and fingerprint the
+complete expanded variant. Run the same suite in a separate candidate directory
+and use the comparison command below; do not compare parent IDs against variant
+IDs or combine their rates as independent evidence.
+New results record the run-level `suite`; cross-suite comparisons are rejected
+before task pairing. Older unmarked baseline results remain compatible, but
+unmarked paraphrase results need a fresh run. This initial corpus intentionally
+has one reviewed alternative wording per parent; additional wordings are a
+separate corpus expansion.
+
+This public suite is a regression tool, **not an unseen holdout**. Offline 
tests
+validate its harness contracts, not generation accuracy or full semantic
+equivalence of output data. Existing default prompts, scoring and report 
formats
+are unchanged. See the benchmark README for the corpus contract.
+
+### Saved-result comparison
+
 Save baseline and candidate benchmark runs in separate directories, then 
compare
 their `results.json` files without making additional model calls:
 
diff --git a/docs/zh/ai-cli/benchmark.md b/docs/zh/ai-cli/benchmark.md
index e2f14bd16c..aa74ae4d9a 100644
--- a/docs/zh/ai-cli/benchmark.md
+++ b/docs/zh/ai-cli/benchmark.md
@@ -4,6 +4,38 @@ sidebar_position: 4
 
 # 模型基准测试
 
+## 可选的改写提示词测试集
+
+默认的 100 个基准任务保持不变。使用以下命令单独运行包含 12 个任务的公开改写测试集:
+
+```bash
+cd seatunnel-cli
+python -m benchmark.runner --provider openai --model gpt-4o \
+    --suite paraphrase --out benchmark/paraphrase-baseline
+```
+
+该测试集覆盖路由、CDC 前置条件以及连接器选项和模式选择,其中包含一个中文路由提示词。
+它只运行改写任务,不会把原始任务一起计入分母。`--tiers` 按继承的复杂度层级筛选,
+`--tasks` 使用独立 ID,例如 `t2_cdc_pg_kafka_p1`。无效、重复、空或不属于所选层级的
+改写任务选择会在初始化模型提供方之前报错。生成、修复和验证流程仍沿用现有实现;
+即使指定 `--level l1`,上述命令也会调用模型。
+
+每个改写任务继承原始任务的全部断言和执行测试数据,并通过 `parent_sha256` 固定完整的
+原始任务定义。原始任务发生变化时,必须重新检查语义等价性并显式更新指纹,不能静默
+改变期望结果。保存的结果包含 `parent_id` 和 `parent_sha256`,`task_sha256` 则覆盖
+展开后的完整改写任务,包括提示词和来源信息。
+
+在不同输出目录中运行同一测试集的基准和候选版本,再使用 `benchmark.compare` 比较相同
+改写任务 ID 的结果。不要把原始任务与改写任务直接配对,也不要将两者的通过率合并为独立证据。
+新结果在运行级别记录 `suite`,不同测试集的比较会在任务配对前被拒绝。旧版未标记的
+基准结果仍兼容,但未标记的改写结果需要重新运行。首个版本有意为每个原始任务保留
+一个经过审查的改写提示词,更多改写属于后续测试集扩展。
+现有默认提示词、评分、报告格式以及跳过验证层的排除规则均保持不变。
+
+这是一个**公开回归测试集,而不是未见过的保留测试集**。离线测试只能验证任务选择、
+测试数据继承、评分和报告契约,不能证明模型准确率、泛化能力或输出数据的完整语义等价性。
+模型质量结论仍需要实际模型运行;生产提示词调整应与测试集变更分开进行。
+
 AI CLI 的准确率靠实测而非假设:专门的基准测试包含 100 个任务、三档复杂度,判定门层层递进直至**真实作业执行**(连接 Docker 
化的真实数据源),覆盖 7 个主流大模型。本页汇总方法论、测试结果与选型建议。
 
 ## 比较 CLI 版本
diff --git a/seatunnel-cli/benchmark/README.md 
b/seatunnel-cli/benchmark/README.md
index d3f32b3cf4..e4d877da6f 100644
--- a/seatunnel-cli/benchmark/README.md
+++ b/seatunnel-cli/benchmark/README.md
@@ -109,6 +109,56 @@ Task format:
 }
 ```
 
+### Optional public paraphrase suite
+
+The default remains the same 100 baseline tasks. Select `--suite paraphrase`
+to run **only 12 alternative-wording tasks**, not the baseline plus its 
variants:
+
+```bash
+cd seatunnel-cli
+python -m benchmark.runner --provider openai --model gpt-4o \
+    --suite paraphrase --out benchmark/paraphrase-baseline
+
+# Filter using the inherited tier and the distinct variant ID.
+python -m benchmark.runner --provider openai --model gpt-4o \
+    --suite paraphrase --tiers 2 --tasks t2_cdc_pg_kafka_p1 \
+    --out benchmark/paraphrase-postgres
+```
+
+These commands call the selected model and use the normal gate and repair
+pipeline. `--level l1` removes engine execution, **not model calls**. The 
wrapper
+`run_benchmark.sh` also forwards `--suite`. Invalid, duplicate, empty, or
+out-of-tier task selections in the paraphrase suite fail before provider setup.
+Baseline selection behavior is unchanged.
+
+`tasks/paraphrase.json` contains four routing variants, four CDC variants, and
+four connector-option/mode variants, including one Chinese routing prompt.
+Each record provides only `parent_id`, `parent_sha256`, and alternative 
`prompt`.
+The loader copies the entire canonical task, including assertions and execution
+fixtures, and uses `<parent_id>_p1` as its stable task ID. It refuses unknown 
or
+duplicate parents, assertion overrides, unchanged wording, and parent 
fingerprint
+mismatches. A parent edit requires reviewing semantic equivalence and 
explicitly
+repinning the full parent contract; do not automatically refresh pins.
+This first slice intentionally uses one reviewed wording per parent; multiple
+wordings and their ID/weighting policy are left for a separate corpus 
expansion.
+
+Saved variant entries include `parent_id` and `parent_sha256`. The existing
+`task_sha256` covers the expanded variant, including its wording and 
provenance.
+New results also record `suite` at run level. Cross-suite comparisons are 
rejected
+before task pairing. Older baseline results without a suite marker remain
+compatible; unmarked paraphrase results must be collected again with this 
marker.
+Keep baseline and candidate runs in separate directories and compare the same
+variant IDs using `benchmark.compare`; parent tasks and variants are different
+tasks, not directly paired samples. Changed wording/contracts are excluded from
+cross-revision comparisons. Existing report formats and skipped-gate exclusions
+are unchanged.
+
+This is a **public regression corpus**, not an unseen holdout or a measurement 
of
+generalization. A wording-sensitive regression can only be established by 
actual
+model runs. Offline tests verify selection, fixture inheritance, scoring and
+reporting contracts; they do not prove model accuracy or full output-data 
semantic
+equivalence. Keep production prompt tuning separate from corpus changes.
+
 ## Docker data environment
 
 `docker/docker-compose.yml` provides sources and sinks with pre-seeded data
diff --git a/seatunnel-cli/benchmark/compare.py 
b/seatunnel-cli/benchmark/compare.py
index 3be9d0af38..949eb2f4d4 100644
--- a/seatunnel-cli/benchmark/compare.py
+++ b/seatunnel-cli/benchmark/compare.py
@@ -49,6 +49,22 @@ def _run_issues(baseline: dict, candidate: dict) -> 
list[str]:
     for name, run in (("baseline", baseline), ("candidate", candidate)):
         if not isinstance(run, dict):
             raise ValueError(f"{name} results must be an object")
+        # Results predating suite selection are baseline runs. Older paraphrase
+        # results carry task provenance, so require a marker instead of 
guessing.
+        suite = run.get("suite", "baseline")
+        if suite not in ("baseline", "paraphrase"):
+            issues.append(f"{name}: suite metadata invalid")
+        models = run.get("models", [])
+        if isinstance(models, list):
+            for model in models:
+                tasks = model.get("tasks", []) if isinstance(model, dict) else 
[]
+                if isinstance(tasks, list) and any(
+                    isinstance(task, dict)
+                    and ("parent_id" in task) != (suite == "paraphrase")
+                    for task in tasks
+                ):
+                    issues.append(f"{name}: suite metadata does not match task 
provenance")
+                    break
         if (
             not isinstance(run.get("cli"), dict)
             or not isinstance(run["cli"].get("cli_commit"), str)
@@ -65,6 +81,8 @@ def _run_issues(baseline: dict, candidate: dict) -> list[str]:
     for field in ("levels", "trials", "max_repairs"):
         if baseline.get(field) != candidate.get(field):
             issues.append(f"{field} differs between runs")
+    if baseline.get("suite", "baseline") != candidate.get("suite", "baseline"):
+        issues.append("suite differs between runs")
     return issues
 
 
diff --git a/seatunnel-cli/benchmark/paraphrases.py 
b/seatunnel-cli/benchmark/paraphrases.py
new file mode 100644
index 0000000000..2c9296209d
--- /dev/null
+++ b/seatunnel-cli/benchmark/paraphrases.py
@@ -0,0 +1,73 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+"""Public alternative wording, pinned to reviewed baseline task contracts."""
+
+import copy
+import hashlib
+import json
+from pathlib import Path
+
+PARAPHRASES_PATH = Path(__file__).resolve().parent / "tasks" / 
"paraphrase.json"
+
+
+def load_paraphrases(parents: list[dict]) -> list[dict]:
+    """Inherit a reviewed task without silently accepting contract drift."""
+    by_id = {task["id"]: task for task in parents}
+    if len(by_id) != len(parents):
+        raise ValueError("Duplicate baseline task IDs")
+    data = json.loads(PARAPHRASES_PATH.read_text(encoding="utf-8"))
+    variants = data.get("paraphrases") if isinstance(data, dict) else None
+    if not isinstance(variants, list) or not variants:
+        raise ValueError("Expected a nonempty paraphrases list")
+    tasks = []
+    seen = set(by_id)
+    for variant in variants:
+        if not isinstance(variant, dict) or set(variant) != {
+            "parent_id",
+            "parent_sha256",
+            "prompt",
+        }:
+            raise ValueError(
+                "Paraphrases may only specify parent_id, parent_sha256 and 
prompt"
+            )
+        parent_id = variant["parent_id"]
+        if not isinstance(parent_id, str) or parent_id not in by_id:
+            raise ValueError(f"Unknown paraphrase parent: {parent_id!r}")
+        task_id = parent_id + "_p1"
+        if task_id in seen:
+            raise ValueError(f"Duplicate paraphrase task ID: {task_id}")
+        seen.add(task_id)
+        parent = by_id[parent_id]
+        fingerprint = hashlib.sha256(
+            json.dumps(
+                parent, sort_keys=True, ensure_ascii=False, separators=(",", 
":")
+            ).encode("utf-8")
+        ).hexdigest()
+        if variant["parent_sha256"] != fingerprint:
+            raise ValueError(f"Paraphrase parent changed; review and repin 
{parent_id}")
+        prompt = variant["prompt"]
+        if (
+            not isinstance(prompt, str)
+            or not prompt.strip()
+            or prompt.strip() == parent["prompt"].strip()
+        ):
+            raise ValueError(f"Paraphrase needs distinct, nonempty wording: 
{task_id}")
+        task = copy.deepcopy(parent)
+        task.update(variant, id=task_id)
+        tasks.append(task)
+    return tasks
diff --git a/seatunnel-cli/benchmark/runner.py 
b/seatunnel-cli/benchmark/runner.py
index 2775df8756..811602b203 100644
--- a/seatunnel-cli/benchmark/runner.py
+++ b/seatunnel-cli/benchmark/runner.py
@@ -67,7 +67,38 @@ CLARIFICATION_REPLY = (
 )
 
 
-def load_tasks(tiers: list[int], task_ids: list[str] | None = None) -> 
list[dict]:
+def load_tasks(
+    tiers: list[int], task_ids: list[str] | None = None, suite: str = 
"baseline"
+) -> list[dict]:
+    if suite not in ("baseline", "paraphrase"):
+        raise ValueError(f"Unknown task suite: {suite}")
+    if suite == "paraphrase":
+        from benchmark.paraphrases import load_paraphrases
+
+        if (
+            not tiers
+            or len(tiers) != len(set(tiers))
+            or any(t not in TIER_FILES for t in tiers)
+        ):
+            raise ValueError("Select distinct tiers from 1, 2 and 3")
+        tasks = [
+            task
+            for task in load_paraphrases(load_tasks(list(TIER_FILES)))
+            if task["tier"] in tiers
+        ]
+        if task_ids is not None:
+            if not task_ids or len(task_ids) != len(set(task_ids)):
+                raise ValueError("Select one or more distinct paraphrase task 
IDs")
+            unknown = set(task_ids) - {task["id"] for task in tasks}
+            if unknown:
+                raise ValueError(
+                    "Unknown paraphrase tasks in selected tiers: "
+                    + ", ".join(sorted(unknown))
+                )
+            tasks = [task for task in tasks if task["id"] in task_ids]
+        if not tasks:
+            raise ValueError("No paraphrase tasks selected")
+        return tasks
     tasks = []
     for tier in tiers:
         path = TASKS_DIR / TIER_FILES[tier]
@@ -319,7 +350,12 @@ def collect_cli_fingerprint() -> dict:
 
 
 def run_benchmark(models: list[dict], tasks: list[dict], levels: list[str],
-                  max_repairs: int, trials: int, out_dir: Path) -> dict:
+                  max_repairs: int, trials: int, out_dir: Path,
+                  suite: str = "baseline") -> dict:
+    if suite not in ("baseline", "paraphrase"):
+        raise ValueError("Unknown benchmark suite")
+    if any(("parent_id" in task) != (suite == "paraphrase") for task in tasks):
+        raise ValueError("Task provenance does not match the selected suite")
     out_dir.mkdir(parents=True, exist_ok=True)
     configs_dir = out_dir / "configs"
     configs_dir.mkdir(exist_ok=True)
@@ -333,6 +369,7 @@ def run_benchmark(models: list[dict], tasks: list[dict], 
levels: list[str],
     }
 
     all_results = {
+        "suite": suite,
         "levels": levels,
         "max_repairs": max_repairs,
         "trials": trials,
@@ -369,6 +406,9 @@ def run_benchmark(models: list[dict], tasks: list[dict], 
levels: list[str],
                     "task_sha256": task_fingerprints[task["id"]],
                     "trials": [],
                 }
+                if "parent_id" in task:
+                    task_entry.update(parent_id=task["parent_id"],
+                                      parent_sha256=task["parent_sha256"])
                 for trial in range(trials):
                     label = task["id"] + (f" trial {trial + 1}/{trials}" if 
trials > 1 else "")
                     print(f"  [{label}] ...", end="", flush=True)
@@ -541,6 +581,10 @@ def main() -> None:
                              "(DeepSeek, Azure, local vLLM, ...)")
     parser.add_argument("--tiers", type=int, nargs="+", default=[1, 2, 3],
                         choices=[1, 2, 3])
+    parser.add_argument("--suite", choices=["baseline", "paraphrase"],
+                        default="baseline",
+                        help="Task suite: baseline (100 tasks, default) or "
+                             "paraphrase (12 alternative-wording tasks only)")
     parser.add_argument("--tasks", nargs="*", default=None,
                         help="Optional task id filter")
     parser.add_argument("--level", default="l3", choices=["l1", "l2", "l3"],
@@ -555,13 +599,19 @@ def main() -> None:
     parser.add_argument("--out", default="benchmark/results")
     args = parser.parse_args()
 
+    # Reject invalid task selections before provider setup can require 
credentials.
+    try:
+        tasks = load_tasks(args.tiers, args.tasks, args.suite)
+    except (ValueError, OSError) as error:
+        parser.error(str(error))
     models = build_models_from_args(args)
-    tasks = load_tasks(args.tiers, args.tasks)
     if not tasks:
         print("No tasks selected.", file=sys.stderr)
         sys.exit(1)
 
     levels = resolve_levels(args.level, tasks)
+    if args.suite == "paraphrase":
+        print("Task suite: paraphrase (public alternative wording)")
     print(f"Running {len(tasks)} tasks × {len(models)} models × "
           f"{args.trials} trial(s), gates: {' → '.join(levels)}, "
           f"max repairs: {args.max_repairs}")
@@ -578,7 +628,7 @@ def main() -> None:
             os.environ.setdefault(key, value)
 
     results = run_benchmark(models, tasks, levels, args.max_repairs,
-                            args.trials, Path(args.out))
+                            args.trials, Path(args.out), suite=args.suite)
 
     from benchmark.report import print_summary, write_reports
     print_summary(results)
diff --git a/seatunnel-cli/benchmark/tasks/paraphrase.json 
b/seatunnel-cli/benchmark/tasks/paraphrase.json
new file mode 100644
index 0000000000..cbd44618c8
--- /dev/null
+++ b/seatunnel-cli/benchmark/tasks/paraphrase.json
@@ -0,0 +1,81 @@
+{
+  "_license": [
+    "Licensed to the Apache Software Foundation (ASF) under one or more",
+    "contributor license agreements.  See the NOTICE file distributed with",
+    "this work for additional information regarding copyright ownership.",
+    "The ASF licenses this file to You under the Apache License, Version 2.0",
+    "(the \"License\"); you may not use this file except in compliance with",
+    "the License.  You may obtain a copy of the License at",
+    "",
+    "   http://www.apache.org/licenses/LICENSE-2.0";,
+    "",
+    "Unless required by applicable law or agreed to in writing, software",
+    "distributed under the License is distributed on an \"AS IS\" BASIS,",
+    "WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.",
+    "See the License for the specific language governing permissions and",
+    "limitations under the License."
+  ],
+  "description": "Public paraphrases; inherit reviewed baseline task contracts 
without changing assertions or fixtures.",
+  "paraphrases": [
+    {
+      "parent_id": "t1_probe_parquet_conditional",
+      "parent_sha256": 
"af32bf2a72fda5b103550a02f4f3519daedd4d049d5c909359cbddb678860682",
+      "prompt": "I need a batch export of shop.products from MySQL at 
localhost:3306, using root. Save the data locally in 
/tmp/st_bench/products_parquet as Parquet, compressed with snappy."
+    },
+    {
+      "parent_id": "t1_probe_streaming_checkpoint",
+      "parent_sha256": 
"374f09790dfc69d2ed9588d8c61c39fb2b03a7f3580785ba11ef031caf55fdeb",
+      "prompt": "Keep producing synthetic records indefinitely and display 
them on the console. Each record has id of type bigint and name of type string; 
the source must not stop after a fixed number of rows."
+    },
+    {
+      "parent_id": "t2_cdc_mysql_initial_snapshot",
+      "parent_sha256": 
"58380d160e4479a06dce4f27ebcc0a54b9ce4e62652ad0ee0dc6b3bc74ae3360",
+      "prompt": "For shop.users in MySQL at localhost:3306 (user root), print 
all existing rows to the console first, then keep printing incremental changes 
through CDC. Include the full initial snapshot before following changes."
+    },
+    {
+      "parent_id": "t2_cdc_pg_kafka",
+      "parent_sha256": 
"cb054552e1319c3b09c8abd7379e23b71cbed5c6a5a611caa13fc3ae1ba747be",
+      "prompt": "Send a live CDC feed from the inventory table in PostgreSQL 
database analytics to Kafka topic wms.inventory at localhost:9092. PostgreSQL 
is at localhost:5432, the login is bench, logical replication is already 
enabled, and the decoding plugin is pgoutput."
+    },
+    {
+      "parent_id": "t2_cdc_probe_batch_mode",
+      "parent_sha256": 
"20cf05311949730f71d1778ee3ce496e821e8fdd7dbdc70985b504ffaa2bfa9a",
+      "prompt": "Whenever orders change in MySQL database shop, show the 
changes on the console and keep listening for later changes. Connect as root to 
localhost:3306. This replication should continue without ending."
+    },
+    {
+      "parent_id": "t2_cdc_probe_privileges",
+      "parent_sha256": 
"1f018216caa10640944adfc73e3c0459cc74e39c18e70c4fd465a0bdf6d0426b",
+      "prompt": "Monitor shop.orders with MySQL CDC and send the changes to 
Console, connecting to localhost:3306 as root. Other CDC jobs share this 
server: allocate this job a separate, unique server id range so they do not 
conflict."
+    },
+    {
+      "parent_id": "t2_si_probe_group_offsets",
+      "parent_sha256": 
"5f4c435a7875f430d3256795e492a22d843b3debfdd515a966c7a1ed34d09a12",
+      "prompt": "Resume Kafka consumption at the offsets already committed for 
group st_bench_group, then stream messages to Console. Use topic events at 
localhost:9092 with fields id bigint, name string, and ts string."
+    },
+    {
+      "parent_id": "t2_bs_probe_sink_options_on_source",
+      "parent_sha256": 
"ab96a591e60f173ef1fbe9a039a63abc04cae93bc31a5e3ae21ea33105a5923c",
+      "prompt": "Run a batch copy from shop.users to the existing 
shop.users_replica table on the same MySQL server, localhost:3306, with user 
root. Have the sink derive its INSERT statements from the destination database 
and table; do not supply handwritten INSERT SQL."
+    },
+    {
+      "parent_id": "t3_cr_amount_split",
+      "parent_sha256": 
"26eca94cabaac65274f42f5965918dafde06fbdf00226e66d4fb6fd8a3e4f6ee",
+      "prompt": "Make a batch job reading shop.transactions from MySQL at 
localhost:3306 as root. Feed that one source output into two SQL transforms: 
amount >= 1000 goes to JSON files in /tmp/st_bench/tx_large, while amount < 
1000 goes to JSON files in /tmp/st_bench/tx_small."
+    },
+    {
+      "parent_id": "t3_cr_status_routing",
+      "parent_sha256": 
"d03acd9c710a5f3853b7d2f1033333619055fccecff784b4e24dbc68ad439df8",
+      "prompt": "In one batch job, read shop.orders once from MySQL 
localhost:3306 using root. Branch through two SQL transforms: send status = 
'PAID' to Console and status <> 'PAID' to local JSON files at 
/tmp/st_bench/unpaid."
+    },
+    {
+      "parent_id": "t3_cr_log_level_split_zh",
+      "parent_sha256": 
"9e11eee428d592410fe74e3e06176389ae85a6eabaf0b89673071017af2ec810",
+      "prompt": "请创建批处理任务,连接 localhost:5432 上的 PostgreSQL,以 bench 用户读取 
analytics 数据库的 app_logs 表。通过两个 SQL transform 分流:level = 'ERROR' 的记录输出到控制台,level 
<> 'ERROR' 的记录以 JSON 格式保存到本地 /tmp/st_bench/non_error_logs。"
+    },
+    {
+      "parent_id": "t3_cr_stream_split",
+      "parent_sha256": 
"37f1ef729bb42f0f0c7171070b6cc982c6605e372ae6a61bf67db4100bd0705e",
+      "prompt": "Use one Kafka source to continuously read JSON from clicks on 
localhost:9092, starting at the earliest offset. Its schema is event_id string, 
user_id bigint, url string. With two SQL transforms, publish user_id = 1 events 
to vip_clicks and all other events to other_clicks on that same broker."
+    }
+  ]
+}
diff --git a/seatunnel-cli/tests/test_benchmark_comparison.py 
b/seatunnel-cli/tests/test_benchmark_comparison.py
index ca5aadc3b2..dec249bc3f 100644
--- a/seatunnel-cli/tests/test_benchmark_comparison.py
+++ b/seatunnel-cli/tests/test_benchmark_comparison.py
@@ -117,6 +117,49 @@ def 
test_identical_runs_are_unchanged_and_inputs_are_immutable():
     )
 
 
+def test_legacy_baseline_results_compare_with_explicit_baseline_suite():
+    before = results({"a": True})
+    after = copy.deepcopy(before)
+    after["suite"] = "baseline"
+    comparison = compare_results(before, after)
+    assert not comparison["issues"]
+    assert len(comparison["rows"]) == 1
+
+
+def test_cross_suite_comparison_is_rejected_before_task_pairing():
+    before = results({"a": True})
+    before["suite"] = "baseline"
+    after = results({"a_p1": True})
+    after["suite"] = "paraphrase"
+    after["models"][0]["tasks"][0]["parent_id"] = "a"
+    comparison = compare_results(before, after)
+    assert comparison["issues"] == ["suite differs between runs"]
+    assert not comparison["rows"]
+
+
[email protected]("suite", [None, "unknown", [], {}])
+def test_invalid_suite_metadata_is_not_scored(suite):
+    before = results({"a": True})
+    after = copy.deepcopy(before)
+    after["suite"] = suite
+    comparison = compare_results(before, after)
+    assert "candidate: suite metadata invalid" in comparison["issues"]
+    assert not comparison["rows"]
+
+
[email protected]("suite", [None, "baseline", "paraphrase"])
+def test_suite_and_task_provenance_must_agree(suite):
+    before = results({"a": True})
+    after = copy.deepcopy(before)
+    if suite is not None:
+        after["suite"] = suite
+    if suite != "paraphrase":
+        after["models"][0]["tasks"][0]["parent_id"] = "parent"
+    comparison = compare_results(before, after)
+    assert "candidate: suite metadata does not match task provenance" in 
comparison["issues"]
+    assert not comparison["rows"]
+
+
 @pytest.mark.parametrize(
     "key,value",
     [
@@ -280,6 +323,7 @@ def 
test_runner_saved_results_compare_without_changing_legacy_reports(
     models = [{"name": "model-a", "provider": "openai", "model": "model-a"}]
     original_tasks = copy.deepcopy(tasks)
     first = run_benchmark(models, tasks, ["l1"], 3, 2, tmp_path / "before")
+    assert first["suite"] == "baseline"
     second = run_benchmark(models, tasks, ["l1"], 3, 2, tmp_path / "after")
     before = json.loads((tmp_path / "before/results.json").read_text())
     after = json.loads((tmp_path / "after/results.json").read_text())
diff --git a/seatunnel-cli/tests/test_benchmark_paraphrases.py 
b/seatunnel-cli/tests/test_benchmark_paraphrases.py
new file mode 100644
index 0000000000..b9d3dee3ce
--- /dev/null
+++ b/seatunnel-cli/tests/test_benchmark_paraphrases.py
@@ -0,0 +1,386 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+"""Offline contracts for the optional, public alternative-wording suite."""
+
+import copy
+import hashlib
+import json
+import re
+import subprocess
+import sys
+from pathlib import Path
+from unittest import mock
+
+import pytest
+
+sys.path.insert(0, str(Path(__file__).parent.parent))
+
+from benchmark import paraphrases, runner  # noqa: E402
+from benchmark.compare import compare_results  # noqa: E402
+from benchmark.report import write_reports  # noqa: E402
+from benchmark.scoring import score_task  # noqa: E402
+
+VARIANTS = 
json.loads(paraphrases.PARAPHRASES_PATH.read_text(encoding="utf-8"))[
+    "paraphrases"
+]
+
+
+def replace_variants(monkeypatch, tmp_path, variants):
+    path = tmp_path / "paraphrase.json"
+    path.write_text(json.dumps({"paraphrases": variants}), encoding="utf-8")
+    monkeypatch.setattr(paraphrases, "PARAPHRASES_PATH", path)
+
+
+def fingerprint(task):
+    return hashlib.sha256(
+        json.dumps(
+            task, sort_keys=True, ensure_ascii=False, separators=(",", ":")
+        ).encode("utf-8")
+    ).hexdigest()
+
+
+STREAM_CONFIG = """
+env {
+  parallelism = 1
+  job.mode = "STREAMING"
+  checkpoint.interval = 10000
+}
+source {
+  FakeSource {
+    schema { fields { id = "bigint", name = "string" } }
+  }
+}
+sink { Console {} }
+"""
+
+
+def test_baseline_stays_identical_and_does_not_load_paraphrases(monkeypatch, 
tmp_path):
+    replace_variants(monkeypatch, tmp_path, [{"invalid": "ignored"}])
+    tasks = runner.load_tasks([1, 2, 3])
+    assert len(tasks) == 100
+    assert tasks == runner.load_tasks([1, 2, 3], suite="baseline")
+    # Baseline loading must preserve every canonical field and its ordering,
+    # without adding variant metadata that changes saved-result fingerprints.
+    canonical = []
+    for tier, filename in runner.TIER_FILES.items():
+        data = json.loads((runner.TASKS_DIR / 
filename).read_text(encoding="utf-8"))
+        canonical.extend(dict(task, tier=tier) for task in data["tasks"])
+    assert tasks == canonical
+    assert fingerprint(tasks) == fingerprint(canonical)
+    assert all("parent_id" not in task for task in tasks)
+    assert runner.load_tasks([1], ["t1_probe_parquet_conditional_p1"]) == []
+    assert len(runner.load_tasks([1], [])) == 20
+
+
[email protected]("variant", VARIANTS, ids=lambda item: 
item["parent_id"])
+def test_variants_inherit_the_complete_task_without_aliasing(variant):
+    parents = runner.load_tasks([1, 2, 3])
+    original = copy.deepcopy(parents)
+    parent = next(task for task in parents if task["id"] == 
variant["parent_id"])
+    task = next(
+        task
+        for task in paraphrases.load_paraphrases(parents)
+        if task["parent_id"] == parent["id"]
+    )
+    assert task["id"] == parent["id"] + "_p1"
+    assert task["prompt"] != parent["prompt"]
+    assert task["parent_sha256"] == fingerprint(parent)
+    assert fingerprint(task) != fingerprint(parent)
+    inherited = {
+        key: value
+        for key, value in task.items()
+        if key not in ("id", "prompt", "parent_id", "parent_sha256")
+    }
+    assert inherited == {
+        key: value for key, value in parent.items() if key not in ("id", 
"prompt")
+    }
+    assert task["expect"]["source"] and task["expect"]["sink"]
+    assert task["execution"]["l3"] in ("run", "skip")
+    assert (task["expect"]["job_mode"] == "STREAMING") == (
+        task["execution"]["mode"] == "streaming"
+    )
+    for pattern in task["expect"].get("must_match", []) + task["expect"].get(
+        "must_not_match", []
+    ):
+        re.compile(pattern)
+    task["expect"]["source"].clear()
+    task["execution"]["services"].append("additional-service")
+    assert parents == original
+
+
+def test_selection_is_opt_in_and_tier_filtered():
+    tasks = runner.load_tasks([1, 2, 3], suite="paraphrase")
+    assert len(tasks) == len({task["id"] for task in tasks}) == 12
+    assert [
+        len(runner.load_tasks([tier], suite="paraphrase")) for tier in (1, 2, 
3)
+    ] == [2, 6, 4]
+    assert not (
+        {task["id"] for task in tasks}
+        & {task["id"] for task in runner.load_tasks([1, 2, 3])}
+    )
+    assert runner.load_tasks([1], [tasks[0]["id"]], "paraphrase") == [tasks[0]]
+
+
[email protected](
+    "tiers,ids",
+    [
+        ([1], []),
+        ([1], ["unknown"]),
+        ([1], ["t1_probe_parquet_conditional"]),
+        ([1], ["t1_probe_parquet_conditional_p1", "unknown"]),
+        ([1], ["t1_probe_parquet_conditional_p1"] * 2),
+        ([2], ["t1_probe_parquet_conditional_p1"]),
+        ([1, 1], None),
+        ([], None),
+        ([4], None),
+    ],
+)
+def test_invalid_paraphrase_selection_fails_closed(tiers, ids):
+    with pytest.raises(ValueError):
+        runner.load_tasks(tiers, ids, "paraphrase")
+
+
[email protected](
+    "field", ["prompt", "expect", "execution", "tier", "category", "name"]
+)
+def test_parent_changes_require_review_and_repinning(field):
+    parents = runner.load_tasks([1, 2, 3])
+    parent = next(task for task in parents if task["id"] == 
VARIANTS[0]["parent_id"])
+    parent[field] = "changed"
+    with pytest.raises(ValueError, match="review and repin"):
+        paraphrases.load_paraphrases(parents)
+
+
[email protected](
+    "change",
+    [
+        lambda variants: variants.append(copy.deepcopy(variants[0])),
+        lambda variants: variants[0].update(parent_id="unknown"),
+        lambda variants: variants[0].update(parent_id=[]),
+        lambda variants: variants[0].update(parent_sha256="0" * 64),
+        lambda variants: variants[0].update(prompt=" "),
+        lambda variants: variants[0].update(prompt=None),
+        lambda variants: variants[0].update(expect={}),
+        lambda variants: variants[0].pop("parent_sha256"),
+    ],
+)
+def test_invalid_variant_definitions_are_rejected(change, monkeypatch, 
tmp_path):
+    variants = copy.deepcopy(VARIANTS)
+    change(variants)
+    replace_variants(monkeypatch, tmp_path, variants)
+    with pytest.raises(ValueError):
+        runner.load_tasks([1, 2, 3], suite="paraphrase")
+
+
+def test_identical_prompt_and_duplicate_parent_are_rejected(monkeypatch, 
tmp_path):
+    parents = runner.load_tasks([1, 2, 3])
+    with pytest.raises(ValueError, match="Duplicate baseline"):
+        paraphrases.load_paraphrases(parents + [parents[0]])
+    variants = copy.deepcopy(VARIANTS)
+    variants[0]["prompt"] = next(
+        task["prompt"] for task in parents if task["id"] == 
variants[0]["parent_id"]
+    )
+    replace_variants(monkeypatch, tmp_path, variants)
+    with pytest.raises(ValueError, match="distinct"):
+        paraphrases.load_paraphrases(parents)
+
+
[email protected](
+    "content",
+    [
+        "{",
+        "[]",
+        "{}",
+        '{"paraphrases": []}',
+        '{"paraphrases": {}}',
+        '{"paraphrases": [null]}',
+    ],
+)
+def test_malformed_or_empty_corpus_fails_closed(content, monkeypatch, 
tmp_path):
+    path = tmp_path / "paraphrase.json"
+    path.write_text(content, encoding="utf-8")
+    monkeypatch.setattr(paraphrases, "PARAPHRASES_PATH", path)
+    with pytest.raises(ValueError):
+        runner.load_tasks([1, 2, 3], suite="paraphrase")
+
+
[email protected](
+    "config", [STREAM_CONFIG, STREAM_CONFIG.replace("STREAMING", "BATCH"), 
"invalid {"]
+)
+def test_same_config_receives_same_verdict_for_every_family(config):
+    parents = {task["id"]: task for task in runner.load_tasks([1, 2, 3])}
+    for task in runner.load_tasks([1, 2, 3], suite="paraphrase"):
+        original = score_task(parents[task["parent_id"]], config).to_dict()
+        alternate = score_task(task, config).to_dict()
+        original.pop("task_id")
+        alternate.pop("task_id")
+        assert original == alternate
+    assert score_task(parents["t1_probe_streaming_checkpoint"], 
STREAM_CONFIG).passed
+
+
+def 
test_only_variant_wording_reaches_generation_and_skipped_gate_is_not_pass():
+    task = runner.load_tasks([1], ["t1_probe_streaming_checkpoint_p1"], 
"paraphrase")[0]
+    with mock.patch("seatunnel_cli.agents.Orchestrator") as factory, 
mock.patch(
+        "benchmark.execution.run_execute",
+        return_value={"passed": None, "detail": "SKIPPED: fixture"},
+    ):
+        orchestrator = factory.return_value
+        orchestrator.process_user_input.return_value = {
+            "type": "config",
+            "config": STREAM_CONFIG,
+        }
+        record = runner.run_task_with_repairs(object(), task, ["l1", "l3"], 2)
+    orchestrator.process_user_input.assert_called_once_with(task["prompt"])
+    orchestrator._run_fix.assert_not_called()
+    assert record["first_pass_round"] is None
+    assert record["attempts"][0]["layers"]["skipped_layers"] == ["l3"]
+
+
+def test_cli_dispatches_selected_suite_before_provider_setup(monkeypatch, 
tmp_path):
+    monkeypatch.setattr(
+        sys,
+        "argv",
+        [
+            "benchmark.runner",
+            "--suite",
+            "paraphrase",
+            "--tiers",
+            "1",
+            "--tasks",
+            "t1_probe_streaming_checkpoint_p1",
+            "--level",
+            "l1",
+        ],
+    )
+    with mock.patch.object(
+        runner, "build_models_from_args", return_value=[]
+    ) as models, mock.patch.object(
+        runner, "resolve_levels", return_value=["l1"]
+    ), mock.patch.object(
+        runner, "run_benchmark", return_value={}
+    ) as run, mock.patch(
+        "benchmark.report.print_summary"
+    ), mock.patch(
+        "benchmark.report.write_reports"
+    ), mock.patch.object(
+        runner.tempfile, "mkdtemp", return_value=str(tmp_path)
+    ):
+        runner.main()
+        selected = run.call_args.args[1]
+        assert [task["id"] for task in selected] == 
["t1_probe_streaming_checkpoint_p1"]
+        assert run.call_args.kwargs["suite"] == "paraphrase"
+        models.assert_called_once()
+        sys.argv[-1] = "l3"
+        sys.argv[6] = "unknown"
+        models.reset_mock()
+        with pytest.raises(SystemExit) as error:
+            runner.main()
+        assert error.value.code == 2
+        models.assert_not_called()
+
+
[email protected](
+    "args", [["--suite", "unknown"], ["--suite", "paraphrase", "--tasks", 
"unknown"]]
+)
+def test_invalid_suite_cli_needs_no_third_party_packages_or_provider(args):
+    completed = subprocess.run(
+        [sys.executable, "-S", "-m", "benchmark.runner", *args],
+        capture_output=True,
+        text=True,
+        timeout=10,
+        check=False,
+    )
+    assert completed.returncode == 2
+    assert "Traceback" not in completed.stderr
+    assert "error:" in completed.stderr
+
+
+def test_saved_variants_compare_by_identity_and_keep_provenance(tmp_path, 
monkeypatch):
+    monkeypatch.setattr(
+        runner, "collect_cli_fingerprint", lambda: {"cli_commit": 
"test-revision"}
+    )
+    monkeypatch.setattr(
+        "seatunnel_cli.llm_provider.create_provider", lambda _: object()
+    )
+    monkeypatch.setattr(
+        runner,
+        "run_task_with_repairs",
+        lambda *args: {
+            "attempts": [
+                {
+                    "round": 0,
+                    "seconds": 0.1,
+                    "layers": {
+                        "l1": {"passed": True},
+                        "l2": None,
+                        "l3": None,
+                        "skipped_layers": [],
+                        "all_gates_executed": True,
+                    },
+                }
+            ],
+            "first_pass_round": 0,
+        },
+    )
+    tasks = runner.load_tasks([1, 2, 3], suite="paraphrase")
+    original = copy.deepcopy(tasks)
+    models = [{"name": "fixture", "provider": "openai", "model": "fixture"}]
+    first = runner.run_benchmark(
+        models, tasks, ["l1"], 0, 1, tmp_path / "before", suite="paraphrase"
+    )
+    runner.run_benchmark(
+        models, tasks, ["l1"], 0, 1, tmp_path / "after", suite="paraphrase"
+    )
+    saved = json.loads((tmp_path / "after/results.json").read_text())
+    assert first["suite"] == saved["suite"] == "paraphrase"
+    rows = compare_results(first, saved)["rows"]
+    assert len(rows) == 12 and all(row["first"] == "pass→pass" for row in rows)
+    assert tasks == original
+    for task, entry in zip(tasks, saved["models"][0]["tasks"]):
+        assert entry["task_sha256"] == fingerprint(task)
+        assert entry["parent_id"] == task["parent_id"]
+        assert entry["parent_sha256"] == task["parent_sha256"]
+    without_provenance = copy.deepcopy(saved)
+    for entry in without_provenance["models"][0]["tasks"]:
+        del entry["parent_id"], entry["parent_sha256"]
+    write_reports(saved, tmp_path / "reports")
+    write_reports(without_provenance, tmp_path / "legacy-reports")
+    for filename in ("summary.md", "summary.csv"):
+        assert (tmp_path / "reports" / filename).read_bytes() == (
+            tmp_path / "legacy-reports" / filename
+        ).read_bytes()
+    tasks[0]["prompt"] += " Revised wording."
+    changed = runner.run_benchmark(
+        models, tasks, ["l1"], 0, 1, tmp_path / "changed", suite="paraphrase"
+    )
+    assert (
+        sum(
+            row["reason"] == "task definition differs"
+            for row in compare_results(first, changed)["rows"]
+        )
+        == 1
+    )
+
+
[email protected]("suite", ["unknown", "baseline"])
+def test_runner_rejects_invalid_suite_before_creating_results(tmp_path, suite):
+    tasks = runner.load_tasks([1], suite="paraphrase")
+    output = tmp_path / "invalid"
+    with pytest.raises(ValueError, match="suite"):
+        runner.run_benchmark([], tasks, ["l1"], 0, 1, output, suite=suite)
+    assert not output.exists()

Reply via email to