@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 +21 -21
- package/README.md +20 -2
- package/assets/skills/ecat-refine/config.dsh.yaml +104 -104
- package/assets/skills/ecat-refine/scripts/_cli_path.py +24 -5
- package/assets/skills/ecat-refine/scripts/start.py +21 -6
- package/assets/skills/ecat-setup/references/plugin-design.md +111 -111
- package/assets/skills/ecat-setup/scripts/_profile.py +104 -104
- package/assets/skills/ecat-setup/scripts/plugins.py +478 -478
- package/assets/skills/ecat-setup/scripts/seeds.py +244 -244
- package/assets/skills/ecat-setup/scripts/setup.py +543 -543
- package/assets/wheels/ecat-0.1.0-py3-none-any.whl +0 -0
- package/cordis.patch.yml +4 -2
- package/icon.svg +6 -6
- package/locale/en.json +6 -6
- package/package.json +58 -56
- package/src/client.js +102 -0
- package/src/plugin.js +57 -5
- package/src/probe_deps.py +128 -128
- package/src/proc.js +10 -1
- package/src/web-app.css +224 -0
- package/src/web-app.js +476 -0
- package/src/web-page.html +13 -0
- package/src/web.js +723 -0
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
|
-
#
|
|
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
|
|
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
|
-
"""返回 (可执行文件路径,
|
|
61
|
-
|
|
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 上
|
|
71
|
-
|
|
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 曲线不动时才发现。
|