@fuxi-ailabs/dsh-ecat 0.1.0-alpha.1 → 0.1.0-alpha.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE CHANGED
@@ -1,21 +1,21 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 Fuxi AI Labs
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Fuxi AI Labs
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # @fuxi-ailabs/dsh-ecat
1
+ # dsh-ecat
2
2
 
3
3
  Two DSH commands over the ECAT refinement loop.
4
4
 
@@ -14,7 +14,7 @@ argv array; the raw input is never passed to a shell.
14
14
  ## Install
15
15
 
16
16
  ```
17
- dsh plugin --profile <profile> add @fuxi-ailabs/dsh-ecat
17
+ dsh plugin --profile <profile> add dsh-ecat
18
18
  ```
19
19
 
20
20
  Or add the market index source and install from the discovers page. The package is MIT
@@ -149,3 +149,21 @@ than silently reading the session's cwd, which could start the loop on the wrong
149
149
  | `spawn failed: ENOENT` | `pythonPath` does not exist on the DSH host |
150
150
  | preflight `exit 15` | `ecat` is not on the host PATH |
151
151
  | preflight `exit 13` | the variable named by `api_key_env` has no value in `<ws>/.env` |
152
+
153
+ ## Releasing the npm package
154
+
155
+ Everything needed to build and publish ships on the `dsh-plugin` branch: `src/`,
156
+ the two skills under `assets/skills/`, and the ECAT wheel under `assets/wheels/`.
157
+ The `ECAT_core` branch is not required for a release (refresh the wheel only when
158
+ core behaviour that the plugin relies on changed).
159
+
160
+ 1. `git checkout dsh-plugin` and make sure the tree is clean.
161
+ 2. `node integrations/dsh/scripts/sync-skills.mjs` — keeps `assets/skills` identical to `./skills`.
162
+ 3. If ECAT core changed: build a wheel from that revision (`uv build`) and replace `assets/wheels/ecat-*.whl`.
163
+ 4. Bump the version: `npm version prerelease --preid=alpha --no-git-tag-version`.
164
+ 5. `cd integrations/dsh && npm pack --dry-run` — expect the same 37 files, including `src/client.js`.
165
+ 6. `npm publish --tag alpha`.
166
+ 7. `git push origin dsh-plugin`.
167
+
168
+ Keep `name` in `package.json` and the `insert` entry in `cordis.patch.yml`
169
+ identical (`@fuxi-ailabs/dsh-ecat`); a mismatch makes the plugin fail to import.
@@ -1,104 +1,104 @@
1
- # ECAT — DeepSeek Harness (dsh) runtime, headless one-shot mode.
2
- #
3
- # Differences from the claude_code config:
4
- # * no base_url — dsh owns its endpoint (official route or DEEPSEEK_BASE_URL);
5
- # ECAT rejects base_url for this runtime rather than silently ignoring it.
6
- # * model is written into a per-call patch, because headless has no --model.
7
- # * api_key_env defaults to DEEPSEEK_API_KEY; the runner exports the resolved
8
- # value under that name whatever variable the config points at.
9
- # * each role gets its own DSH_HOME under .ecat/agents/<role>/ (profile +
10
- # AGENTS.md), created automatically on the first run.
11
- #
12
- # Verify with: python <skill>/scripts/preflight.py --config <this file> --workspace <WS>
13
-
14
- # ─── Identity ────────────────────────────────────────────────────────────────
15
- project: my-project
16
- work_dir: .
17
-
18
- # ─── Prompt profile ──────────────────────────────────────────────────────────
19
- # Domain fragment under ecat/prompts/profiles/<name>/. "a2h" ships with the
20
- # repo; create your own with the ecat-setup skill.
21
- profile: a2h
22
-
23
- # ─── Source / Target paths ───────────────────────────────────────────────────
24
- paths:
25
- # source: ./reference # optional: only for a task with a ground-truth tree
26
- target: ./target # required, must exist
27
-
28
- # ─── Agents ──────────────────────────────────────────────────────────────────
29
- generator:
30
- runtime: dsh
31
- model: deepseek-flash
32
- provider: deepseek
33
- base_url: null # must stay null for dsh
34
- api_key_env: DEEPSEEK_API_KEY
35
- config_dir: .ecat/agents/generator
36
- timeout_s: 5400
37
- extra_args: []
38
- # Continue the same dsh session across iterations (--session-id). Saves
39
- # cache reads; context grows, so pair it with the loop's compaction setting.
40
- resume_session: true
41
-
42
- discriminator:
43
- runtime: dsh
44
- model: deepseek-flash
45
- provider: deepseek
46
- base_url: null
47
- api_key_env: DEEPSEEK_API_KEY
48
- config_dir: .ecat/agents/discriminator
49
- timeout_s: 3600
50
- extra_args: []
51
- evolution_write_paths:
52
- - .ecat/agents/generator/skills
53
- - .ecat/agents/generator/memory
54
-
55
- # ─── Adversarial loop ────────────────────────────────────────────────────────
56
- loop:
57
- max_iterations: 100
58
- entropy_threshold: 0.01
59
- stable_iters_to_stop: 2
60
- regression_tolerance: 0.1
61
- revert_on_regression: true
62
- oscillation_guard:
63
- enabled: true
64
- window: 5
65
- min_delta: 0.02
66
- # dsh runs one process per call and has no in-session /compact, so this knob
67
- # is a no-op here. Context growth is handled by dsh's own compaction-basic
68
- # (threshold 0.8 of the model window) inside each resumed session.
69
- generator_compact_interval: 0
70
- max_items_per_iter: 25
71
-
72
- # ─── Entropy plugin set ──────────────────────────────────────────────────────
73
- # `example` ships with the repo. Replace with the set you build; every
74
- # dimension listed in weights must be registered by some plugin.
75
- entropy:
76
- plugin_set: example
77
- judge_max_issues: 10
78
- weights:
79
- compile: 0.30
80
- feature_parity: 0.35
81
-
82
- # ─── Retry ────────────────────────────────────────────────────────────────────
83
- # dsh retries transient provider failures inside the turn (dsh-llm-retry).
84
- # Keep this modest so an upstream outage surfaces as a failed iteration rather
85
- # than hours of silent retrying.
86
- retry:
87
- max_attempts: 2
88
- base_delay_s: 2
89
- max_delay_s: 30
90
- jitter: true
91
-
92
- # ─── Phase gates ─────────────────────────────────────────────────────────────
93
- features:
94
- memory_evolution: false
95
- skill_evolution: false
96
- multi_discriminator: false
97
- in_loop_intervention: false
98
-
99
- # ─── Reporting ───────────────────────────────────────────────────────────────
100
- reporting:
101
- chart_format: png
102
- chart_on_each_iter: true
103
- keep_run_artifacts: -1
104
- log_level: info
1
+ # ECAT — DeepSeek Harness (dsh) runtime, headless one-shot mode.
2
+ #
3
+ # Differences from the claude_code config:
4
+ # * no base_url — dsh owns its endpoint (official route or DEEPSEEK_BASE_URL);
5
+ # ECAT rejects base_url for this runtime rather than silently ignoring it.
6
+ # * model is written into a per-call patch, because headless has no --model.
7
+ # * api_key_env defaults to DEEPSEEK_API_KEY; the runner exports the resolved
8
+ # value under that name whatever variable the config points at.
9
+ # * each role gets its own DSH_HOME under .ecat/agents/<role>/ (profile +
10
+ # AGENTS.md), created automatically on the first run.
11
+ #
12
+ # Verify with: python <skill>/scripts/preflight.py --config <this file> --workspace <WS>
13
+
14
+ # ─── Identity ────────────────────────────────────────────────────────────────
15
+ project: my-project
16
+ work_dir: .
17
+
18
+ # ─── Prompt profile ──────────────────────────────────────────────────────────
19
+ # Domain fragment under ecat/prompts/profiles/<name>/. "a2h" ships with the
20
+ # repo; create your own with the ecat-setup skill.
21
+ profile: a2h
22
+
23
+ # ─── Source / Target paths ───────────────────────────────────────────────────
24
+ paths:
25
+ # source: ./reference # optional: only for a task with a ground-truth tree
26
+ target: ./target # required, must exist
27
+
28
+ # ─── Agents ──────────────────────────────────────────────────────────────────
29
+ generator:
30
+ runtime: dsh
31
+ model: deepseek-flash
32
+ provider: deepseek
33
+ base_url: null # must stay null for dsh
34
+ api_key_env: DEEPSEEK_API_KEY
35
+ config_dir: .ecat/agents/generator
36
+ timeout_s: 5400
37
+ extra_args: []
38
+ # Continue the same dsh session across iterations (--session-id). Saves
39
+ # cache reads; context grows, so pair it with the loop's compaction setting.
40
+ resume_session: true
41
+
42
+ discriminator:
43
+ runtime: dsh
44
+ model: deepseek-flash
45
+ provider: deepseek
46
+ base_url: null
47
+ api_key_env: DEEPSEEK_API_KEY
48
+ config_dir: .ecat/agents/discriminator
49
+ timeout_s: 3600
50
+ extra_args: []
51
+ evolution_write_paths:
52
+ - .ecat/agents/generator/skills
53
+ - .ecat/agents/generator/memory
54
+
55
+ # ─── Adversarial loop ────────────────────────────────────────────────────────
56
+ loop:
57
+ max_iterations: 100
58
+ entropy_threshold: 0.01
59
+ stable_iters_to_stop: 2
60
+ regression_tolerance: 0.1
61
+ revert_on_regression: true
62
+ oscillation_guard:
63
+ enabled: true
64
+ window: 5
65
+ min_delta: 0.02
66
+ # dsh runs one process per call and has no in-session /compact, so this knob
67
+ # is a no-op here. Context growth is handled by dsh's own compaction-basic
68
+ # (threshold 0.8 of the model window) inside each resumed session.
69
+ generator_compact_interval: 0
70
+ max_items_per_iter: 25
71
+
72
+ # ─── Entropy plugin set ──────────────────────────────────────────────────────
73
+ # `example` ships with the repo. Replace with the set you build; every
74
+ # dimension listed in weights must be registered by some plugin.
75
+ entropy:
76
+ plugin_set: example
77
+ judge_max_issues: 10
78
+ weights:
79
+ compile: 0.30
80
+ feature_parity: 0.35
81
+
82
+ # ─── Retry ────────────────────────────────────────────────────────────────────
83
+ # dsh retries transient provider failures inside the turn (dsh-llm-retry).
84
+ # Keep this modest so an upstream outage surfaces as a failed iteration rather
85
+ # than hours of silent retrying.
86
+ retry:
87
+ max_attempts: 2
88
+ base_delay_s: 2
89
+ max_delay_s: 30
90
+ jitter: true
91
+
92
+ # ─── Phase gates ─────────────────────────────────────────────────────────────
93
+ features:
94
+ memory_evolution: false
95
+ skill_evolution: false
96
+ multi_discriminator: false
97
+ in_loop_intervention: false
98
+
99
+ # ─── Reporting ───────────────────────────────────────────────────────────────
100
+ reporting:
101
+ chart_format: png
102
+ chart_on_each_iter: true
103
+ keep_run_artifacts: -1
104
+ log_level: info
@@ -56,9 +56,14 @@ def _npm_bin() -> Path | None:
56
56
  return prefix if os.name == "nt" else prefix / "bin"
57
57
 
58
58
 
59
- def find_cli(name: str) -> tuple[Path | None, bool]:
60
- """返回 (可执行文件路径, 是否已在 PATH 上)。找不到返回 (None, False)。"""
61
- on_path = shutil.which(name)
59
+ def find_cli(name: str, path: str | None = None) -> tuple[Path | None, bool]:
60
+ """返回 (可执行文件路径, 是否在给定 PATH 上)。找不到返回 (None, False)。
61
+
62
+ *path* 是可选的自定义 PATH 串(默认用当前进程的 PATH)。start.py 传入的是
63
+ `ensure_on_path` 加过目录的 child_env,这样"体检报告的 ecat"和"启动执行的 ecat"
64
+ 解析的是同一个环境。
65
+ """
66
+ on_path = shutil.which(name, path=path) if path is not None else shutil.which(name)
62
67
  if on_path:
63
68
  return Path(on_path), True
64
69
 
@@ -67,8 +72,9 @@ def find_cli(name: str) -> tuple[Path | None, bool]:
67
72
  if npm:
68
73
  dirs.insert(0, npm)
69
74
 
70
- # Windows 上 npm 装出来的是 name.cmd(外加一个无扩展名的 shell 包装,不能直接跑)
71
- names = [f"{name}.cmd", f"{name}.exe", name] if os.name == "nt" else [name]
75
+ # Windows 上 .exe 优先:`CreateProcess` 只补 .exe,`.cmd` 必须经 cmd /c 才能跑
76
+ # (见 cli_command)。若这里先返回 .cmd,start.py 的裸命令会解析到另一个 exe。
77
+ names = [f"{name}.exe", f"{name}.cmd", f"{name}.bat", name] if os.name == "nt" else [name]
72
78
  for d in dirs:
73
79
  if not d.is_dir():
74
80
  continue
@@ -79,6 +85,19 @@ def find_cli(name: str) -> tuple[Path | None, bool]:
79
85
  return None, False
80
86
 
81
87
 
88
+ def cli_command(path: str | Path, *args: str) -> list[str]:
89
+ """构造执行 *path* 的 argv。
90
+
91
+ Windows 的 `CreateProcess` 不能直接跑 `.cmd/.bat`(只补 `.exe`),必须显式经
92
+ `cmd /c`;start.py 与 preflight 共用这个函数,保证"预检认为可用"与"实际能启动"
93
+ 指向同一个入口。
94
+ """
95
+ p = Path(path)
96
+ if os.name == "nt" and p.suffix.lower() in (".cmd", ".bat"):
97
+ return ["cmd", "/c", str(p), *args]
98
+ return [str(p), *args]
99
+
100
+
82
101
  def ensure_on_path(names: list[str], env: dict[str, str] | None = None) -> tuple[dict[str, str], list[str]]:
83
102
  """把 *names* 里不在 PATH 上、但能在已知位置找到的 CLI,其目录前置进 PATH。
84
103
 
@@ -22,7 +22,7 @@ from datetime import datetime
22
22
  from pathlib import Path
23
23
 
24
24
  sys.path.insert(0, str(Path(__file__).parent))
25
- from _cli_path import ensure_on_path # noqa: E402
25
+ from _cli_path import cli_command, ensure_on_path, find_cli # noqa: E402
26
26
  from _proc import pid_alive # noqa: E402
27
27
 
28
28
  STATE_REL = Path(".ecat") / "skill-refine.state"
@@ -62,11 +62,6 @@ def main() -> int:
62
62
  file=sys.stderr)
63
63
  return 2
64
64
 
65
- cmd = ["ecat", "refine", "--config", str(cfg)]
66
- if args.max_iter:
67
- cmd += ["--max-iter", str(args.max_iter)]
68
- cmd += args.extra
69
-
70
65
  # npm -g 装的 CLI 常常不在 PATH(实测 opencode 在 %APPDATA%\npm)。ECAT 起子进程
71
66
  # 时继承的就是这里的环境,所以在这一层补进去即可,不动用户的 shell profile。
72
67
  # deveco 是 opencode 的 fork(npm @deveco/deveco-code),同样装 in npm 前缀目录。
@@ -74,6 +69,26 @@ def main() -> int:
74
69
  for n in notes:
75
70
  print(f" PATH 注入 {n}")
76
71
 
72
+ # 子进程(ecat CLI)的 stdout 直接写进 boot.log,失败时 start.py 按 UTF-8 读回来。
73
+ # 默认中文 Windows 上 Python 会按 CP936 写,这段中文就会变成乱码;强制 UTF-8,
74
+ # 让写入方和读取方用同一个编码(run.log 由 ECAT 自己的 FileHandler 写,本来就是 UTF-8)。
75
+ child_env["PYTHONUTF8"] = "1"
76
+ child_env["PYTHONIOENCODING"] = "utf-8"
77
+
78
+ # 与 preflight 用同一个解析结果(child_env 的 PATH),不要用裸命令:
79
+ # Windows 的 CreateProcess 只补 .exe,并且先搜解释器所在目录(venv\Scripts),
80
+ # 裸 "ecat" 可能执行到另一个安装;若只有 .cmd,则根本起不来。
81
+ ecat_cli, _ = find_cli("ecat", path=child_env.get("PATH"))
82
+ if ecat_cli is None:
83
+ print("找不到 ecat 命令(uv tool install --editable <ECAT 仓>)", file=sys.stderr)
84
+ return 3
85
+ print(f" ecat {ecat_cli}")
86
+
87
+ cmd = cli_command(ecat_cli, "refine", "--config", str(cfg))
88
+ if args.max_iter:
89
+ cmd += ["--max-iter", str(args.max_iter)]
90
+ cmd += args.extra
91
+
77
92
  (ws / ".ecat").mkdir(parents=True, exist_ok=True)
78
93
  boot = ws / BOOT_REL
79
94
  # 脱离调用方进程组:调用方会话结束时不该带走几小时的 run
@@ -1,111 +1,111 @@
1
- # 熵维度插件设计 / Designing entropy dimensions
2
-
3
- 配置 ECAT 时最容易做错的一步。`profile` 决定 Generator **怎么写**代码,插件决定 ECAT **怎么判断**代码好不好 —— 权重里每个维度都必须有插件来算,否则该维度静默失效(不报错、不派 judge、Generator 永远收不到该维度的工单)。
4
-
5
- ## 一个维度该多大
6
-
7
- 一个维度 = **一个可以独立打分的质量主张**。判断标准:它的分数能不能单独解释给用户听?
8
-
9
- - 好:"目标仓的公开 API 覆盖了源仓的 `__all__`"(能单独解释、能单独修)
10
- - 好:"目标仓能编译通过"(能单独解释)
11
- - 坏:"代码质量"(什么都沾、分数说不清、修起来没头)
12
-
13
- 3–5 个维度是舒适区。**每个维度必须有独立的 `name`** —— 它就是 `entropy.weights` 里的键。
14
-
15
- ## 先问:这个判断能写成确定性检查吗?
16
-
17
- | 能 → **hard 插件** | 不能 → **agentic 插件** |
18
- |---|---|
19
- | 编译/构建成功、错误条数 | 行为是否与源一致 |
20
- | 导出符号表、公开 API 签名比对 | 语义是否等价 |
21
- | 文件/函数/用例计数 | 移植是否完整(结构在、行为丢) |
22
- | 静态扫描:禁用写法、TODO 残留、桩函数 | 抽象是否合理、错误处理是否到位 |
23
- | 测试通过率、覆盖的测试数 | API 用法是否符合目标平台惯例 |
24
- | 二进制体积、启动耗时 | 文档与实现是否一致 |
25
-
26
- **判据**:答案能不能用一段不调 LLM 的代码写出来。能,就写 hard —— hard 维度便宜、稳定、可复现,是熵曲线的骨架。
27
-
28
- **至少保留一个 hard 维度**:一个全是 agentic 判分的配置,每轮分数都在 LLM 的主观波动里,Generator 会沿着一堆"感觉不对"的工单乱改,而 H 曲线看不出它是在进步还是在打转。
29
-
30
- ## 两种插件形状
31
-
32
- ### hard 插件
33
-
34
- ```python
35
- class ExportApiPlugin(EntropyPlugin):
36
- name = "build_api"
37
- default_weight = 0.3
38
- judge_mode = "hard" # 默认值,可不写
39
-
40
- def evaluate(self, target_dir, *, source_dir=None, config=None):
41
- src = _read_exports(source_dir) # 确定性检查
42
- dst = _read_exports(target_dir)
43
- missing = src - dst
44
- return len(missing) / max(len(src), 1), {
45
- "missing": sorted(missing)[:50],
46
- "total": len(src),
47
- }
48
-
49
- def extract_work_items(self, score, details): # 必须实现
50
- return [WorkItem(file=..., severity="high", summary=...) for ... in details["missing"]]
51
- ```
52
-
53
- 要点:
54
- - `evaluate` 返回 `(score, details)`,`0.0` = 无熵。**`details` 是给人看的 JSON** —— 它会进判别器的提示词,所以要写"哪里不对"而不是只写一个数。
55
- - **`extract_work_items` 是 hard 插件的价值所在**:它把 details 变成 Generator 能直接执行的工单(文件 + 问题 + 严重度)。返回 `[]` 等于告诉 Generator "这维有问题但你自己找" —— 浪费一整轮。
56
- - 不配 `source_dir` 时要么跳过(`{"skipped": "no_source_tree"}`),要么退化成"自查"的规则;别拿 `None` 去比。
57
-
58
- ### agentic 插件
59
-
60
- 照抄 `src/ecat/entropy/plugins/example/check_feature_parity.py`。三件必需的事:
61
-
62
- ```python
63
- class BehaviourParityPlugin(EntropyPlugin):
64
- name = "behaviour_parity"
65
- default_weight = 0.35
66
- judge_mode = "agentic"
67
- hard_weight = 0.0 # 不掺 hard 分,取 judge 的分
68
- judge_prompt = "..." # 打分标准(rubric)—— 领域措辞只属于这里
69
-
70
- def evaluate(self, target_dir, *, source_dir=None, config=None):
71
- if source_dir is None or not Path(source_dir).exists():
72
- return 0.0, {"skipped": "no_source_tree"}
73
- return 0.0, {…} # 只给 judge 一点定位线索,不打分
74
-
75
- def extract_work_items(self, score, details):
76
- return [] # 工单由 judge 的 issues 生成,这里必须空
77
- ```
78
-
79
- - **`hard_weight = 0.0`**:默认 0.4 会把 `evaluate` 的分和 judge 的分混合。agentic 插件的 `evaluate` 通常只做定位、不做打分,掺进去等于用一个假分数稀释真判断。
80
- - **`extract_work_items` 必须返回 `[]`**:judge 的每条 issue 会被自动转成工单(按 issue 开头的文件路径分组)。这里再返回东西会**重复产出工单**,Generator 干两遍。
81
- - **`judge_prompt` 里写死"每条 issue 以目标文件路径开头"**:judge 的 issue 靠前导路径分组才能按文件并行派发。骨架里也有这条,但插件自己的 rubric 里重申一遍更保险。
82
- - **`{"skipped": ...}` 的语义**:该维度既不派 judge、也不参与权重归一化。源树缺失、清单没配时用它 —— 不要"默认通过"(那是骗自己,会掩盖漏配)。
83
-
84
- ## 权重怎么定
85
-
86
- - 权重**不必和为 1**,ECAT 会归一化。写相对重要性即可。
87
- - 每一维都问:"**这维出问题时,用户会真的在意吗?**" 只是"最好有"的维度给 0.05–0.1,核心正确性维度给 0.3–0.4。
88
- - **权重 0 或不在 weights 里的维度会被禁用** —— 这是关掉某维度的正确方式,不是删插件。
89
- - 想调但不确定:先都写 0.2–0.3 跑 2–3 轮,看 H 曲线里哪一维贡献最大再重配。
90
-
91
- ## 三方库迁移(PC numpy → HarmonyOS)该配什么
92
-
93
- 这个任务的特点是**源和目标语言不同、且可能需要改底层 C++**,所以"结构像"完全不代表"能用"。维度应按**可验证程度从强到弱**排:
94
-
95
- | 维度 | 形状 | 怎么打分 | 建议权重 |
96
- |---|---|---|---|
97
- | `compile_native` | hard | 跑目标平台的编译命令(hvigor / clang 交叉编译),按错误条数归一 | 0.30 |
98
- | `export_api` | hard | 解析源包的 `__all__` / 公开符号表(Python 侧可用 `ast`,C 侧解析头文件),与目标侧导出的 NAPI/ArkTS 接口比对,算缺失比例 | 0.25 |
99
- | `test_parity` | hard | 源包的测试用例能在目标上跑通的比例。**有测试就优先用它** —— 这是最强的信号,比任何 LLM 判断都硬 | 0.25 |
100
- | `behaviour_parity` | agentic | judge 读源实现 + 目标实现,报告行为不一致处(数值精度、边界条件、dtype 提升规则、广播语义、异常行为) | 0.15 |
101
- | `native_impl_gap` | agentic | judge 找目标侧留下的桩:`// TODO`、直接 `return None`、把 C++ 实现换成纯 JS 近似、砍掉的 SIMD 路径 | 0.05–0.10 |
102
-
103
- **算例里的三个坑**:
104
-
105
- 1. **数值库的"能跑"和"算得对"是两回事。** 只做 hard 编译维度会让 H 降到 0 但结果全错。所以 `test_parity` 或 `behaviour_parity` 必须占足够权重。如果源包自带测试套(numpy 有),`test_parity` 是最高性价比的维度 —— 直接把源测试搬到目标上跑,通过率就是分数。
106
- 2. **C++ 侧没有现成的符号表可比。** `export_api` 要分两层:Python/ArkTS 层的公开 API(能解析、能做),C 层函数的覆盖(往往只能靠 `native_impl_gap` 的 agentic 判断)。别硬写一个解析 C++ 的 hard 插件 —— 写不准,还会给 Generator 假工单。
107
- 3. **单树任务退化的情形。** 如果用户的场景是"从零实现一个 numpy 子集"而没有源包,`source` 留空,`export_api`/`test_parity` 要改成对着 **spec 文件**(一份 API 清单 / 需求文档)比对,`evaluate` 里检查 spec 是否配置:未配就 `{"skipped": "no_spec_configured"}`。这正对应 `config.paths` 里 `spec` / `feature_checklist` 那类可选路径。
108
-
109
- ## 写完要过的一关
110
-
111
- `check.py` 会核对 `entropy.weights` 里的每个维度名是否都有插件注册。**这一步必须 0 才起跑** —— 漏配的维度不会报错,只会安静地不生效,而你要等到跑完几小时、看 H 曲线不动时才发现。
1
+ # 熵维度插件设计 / Designing entropy dimensions
2
+
3
+ 配置 ECAT 时最容易做错的一步。`profile` 决定 Generator **怎么写**代码,插件决定 ECAT **怎么判断**代码好不好 —— 权重里每个维度都必须有插件来算,否则该维度静默失效(不报错、不派 judge、Generator 永远收不到该维度的工单)。
4
+
5
+ ## 一个维度该多大
6
+
7
+ 一个维度 = **一个可以独立打分的质量主张**。判断标准:它的分数能不能单独解释给用户听?
8
+
9
+ - 好:"目标仓的公开 API 覆盖了源仓的 `__all__`"(能单独解释、能单独修)
10
+ - 好:"目标仓能编译通过"(能单独解释)
11
+ - 坏:"代码质量"(什么都沾、分数说不清、修起来没头)
12
+
13
+ 3–5 个维度是舒适区。**每个维度必须有独立的 `name`** —— 它就是 `entropy.weights` 里的键。
14
+
15
+ ## 先问:这个判断能写成确定性检查吗?
16
+
17
+ | 能 → **hard 插件** | 不能 → **agentic 插件** |
18
+ |---|---|
19
+ | 编译/构建成功、错误条数 | 行为是否与源一致 |
20
+ | 导出符号表、公开 API 签名比对 | 语义是否等价 |
21
+ | 文件/函数/用例计数 | 移植是否完整(结构在、行为丢) |
22
+ | 静态扫描:禁用写法、TODO 残留、桩函数 | 抽象是否合理、错误处理是否到位 |
23
+ | 测试通过率、覆盖的测试数 | API 用法是否符合目标平台惯例 |
24
+ | 二进制体积、启动耗时 | 文档与实现是否一致 |
25
+
26
+ **判据**:答案能不能用一段不调 LLM 的代码写出来。能,就写 hard —— hard 维度便宜、稳定、可复现,是熵曲线的骨架。
27
+
28
+ **至少保留一个 hard 维度**:一个全是 agentic 判分的配置,每轮分数都在 LLM 的主观波动里,Generator 会沿着一堆"感觉不对"的工单乱改,而 H 曲线看不出它是在进步还是在打转。
29
+
30
+ ## 两种插件形状
31
+
32
+ ### hard 插件
33
+
34
+ ```python
35
+ class ExportApiPlugin(EntropyPlugin):
36
+ name = "build_api"
37
+ default_weight = 0.3
38
+ judge_mode = "hard" # 默认值,可不写
39
+
40
+ def evaluate(self, target_dir, *, source_dir=None, config=None):
41
+ src = _read_exports(source_dir) # 确定性检查
42
+ dst = _read_exports(target_dir)
43
+ missing = src - dst
44
+ return len(missing) / max(len(src), 1), {
45
+ "missing": sorted(missing)[:50],
46
+ "total": len(src),
47
+ }
48
+
49
+ def extract_work_items(self, score, details): # 必须实现
50
+ return [WorkItem(file=..., severity="high", summary=...) for ... in details["missing"]]
51
+ ```
52
+
53
+ 要点:
54
+ - `evaluate` 返回 `(score, details)`,`0.0` = 无熵。**`details` 是给人看的 JSON** —— 它会进判别器的提示词,所以要写"哪里不对"而不是只写一个数。
55
+ - **`extract_work_items` 是 hard 插件的价值所在**:它把 details 变成 Generator 能直接执行的工单(文件 + 问题 + 严重度)。返回 `[]` 等于告诉 Generator "这维有问题但你自己找" —— 浪费一整轮。
56
+ - 不配 `source_dir` 时要么跳过(`{"skipped": "no_source_tree"}`),要么退化成"自查"的规则;别拿 `None` 去比。
57
+
58
+ ### agentic 插件
59
+
60
+ 照抄 `src/ecat/entropy/plugins/example/check_feature_parity.py`。三件必需的事:
61
+
62
+ ```python
63
+ class BehaviourParityPlugin(EntropyPlugin):
64
+ name = "behaviour_parity"
65
+ default_weight = 0.35
66
+ judge_mode = "agentic"
67
+ hard_weight = 0.0 # 不掺 hard 分,取 judge 的分
68
+ judge_prompt = "..." # 打分标准(rubric)—— 领域措辞只属于这里
69
+
70
+ def evaluate(self, target_dir, *, source_dir=None, config=None):
71
+ if source_dir is None or not Path(source_dir).exists():
72
+ return 0.0, {"skipped": "no_source_tree"}
73
+ return 0.0, {…} # 只给 judge 一点定位线索,不打分
74
+
75
+ def extract_work_items(self, score, details):
76
+ return [] # 工单由 judge 的 issues 生成,这里必须空
77
+ ```
78
+
79
+ - **`hard_weight = 0.0`**:默认 0.4 会把 `evaluate` 的分和 judge 的分混合。agentic 插件的 `evaluate` 通常只做定位、不做打分,掺进去等于用一个假分数稀释真判断。
80
+ - **`extract_work_items` 必须返回 `[]`**:judge 的每条 issue 会被自动转成工单(按 issue 开头的文件路径分组)。这里再返回东西会**重复产出工单**,Generator 干两遍。
81
+ - **`judge_prompt` 里写死"每条 issue 以目标文件路径开头"**:judge 的 issue 靠前导路径分组才能按文件并行派发。骨架里也有这条,但插件自己的 rubric 里重申一遍更保险。
82
+ - **`{"skipped": ...}` 的语义**:该维度既不派 judge、也不参与权重归一化。源树缺失、清单没配时用它 —— 不要"默认通过"(那是骗自己,会掩盖漏配)。
83
+
84
+ ## 权重怎么定
85
+
86
+ - 权重**不必和为 1**,ECAT 会归一化。写相对重要性即可。
87
+ - 每一维都问:"**这维出问题时,用户会真的在意吗?**" 只是"最好有"的维度给 0.05–0.1,核心正确性维度给 0.3–0.4。
88
+ - **权重 0 或不在 weights 里的维度会被禁用** —— 这是关掉某维度的正确方式,不是删插件。
89
+ - 想调但不确定:先都写 0.2–0.3 跑 2–3 轮,看 H 曲线里哪一维贡献最大再重配。
90
+
91
+ ## 三方库迁移(PC numpy → HarmonyOS)该配什么
92
+
93
+ 这个任务的特点是**源和目标语言不同、且可能需要改底层 C++**,所以"结构像"完全不代表"能用"。维度应按**可验证程度从强到弱**排:
94
+
95
+ | 维度 | 形状 | 怎么打分 | 建议权重 |
96
+ |---|---|---|---|
97
+ | `compile_native` | hard | 跑目标平台的编译命令(hvigor / clang 交叉编译),按错误条数归一 | 0.30 |
98
+ | `export_api` | hard | 解析源包的 `__all__` / 公开符号表(Python 侧可用 `ast`,C 侧解析头文件),与目标侧导出的 NAPI/ArkTS 接口比对,算缺失比例 | 0.25 |
99
+ | `test_parity` | hard | 源包的测试用例能在目标上跑通的比例。**有测试就优先用它** —— 这是最强的信号,比任何 LLM 判断都硬 | 0.25 |
100
+ | `behaviour_parity` | agentic | judge 读源实现 + 目标实现,报告行为不一致处(数值精度、边界条件、dtype 提升规则、广播语义、异常行为) | 0.15 |
101
+ | `native_impl_gap` | agentic | judge 找目标侧留下的桩:`// TODO`、直接 `return None`、把 C++ 实现换成纯 JS 近似、砍掉的 SIMD 路径 | 0.05–0.10 |
102
+
103
+ **算例里的三个坑**:
104
+
105
+ 1. **数值库的"能跑"和"算得对"是两回事。** 只做 hard 编译维度会让 H 降到 0 但结果全错。所以 `test_parity` 或 `behaviour_parity` 必须占足够权重。如果源包自带测试套(numpy 有),`test_parity` 是最高性价比的维度 —— 直接把源测试搬到目标上跑,通过率就是分数。
106
+ 2. **C++ 侧没有现成的符号表可比。** `export_api` 要分两层:Python/ArkTS 层的公开 API(能解析、能做),C 层函数的覆盖(往往只能靠 `native_impl_gap` 的 agentic 判断)。别硬写一个解析 C++ 的 hard 插件 —— 写不准,还会给 Generator 假工单。
107
+ 3. **单树任务退化的情形。** 如果用户的场景是"从零实现一个 numpy 子集"而没有源包,`source` 留空,`export_api`/`test_parity` 要改成对着 **spec 文件**(一份 API 清单 / 需求文档)比对,`evaluate` 里检查 spec 是否配置:未配就 `{"skipped": "no_spec_configured"}`。这正对应 `config.paths` 里 `spec` / `feature_checklist` 那类可选路径。
108
+
109
+ ## 写完要过的一关
110
+
111
+ `check.py` 会核对 `entropy.weights` 里的每个维度名是否都有插件注册。**这一步必须 0 才起跑** —— 漏配的维度不会报错,只会安静地不生效,而你要等到跑完几小时、看 H 曲线不动时才发现。