harbor-dsh-evolution 0.1.0__tar.gz

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.
@@ -0,0 +1,18 @@
1
+ .DS_Store
2
+ __pycache__/
3
+ *.py[cod]
4
+ .pytest_cache/
5
+ .venv/
6
+ dist/
7
+
8
+ # Harbor execution results are local evidence, not template source.
9
+ jobs/*
10
+ !jobs/.gitkeep
11
+
12
+ # Local Docker/Buildx state.
13
+ .runtime/
14
+
15
+ # Generated Candidate snapshots and local package installs.
16
+ examples/**/candidate-manifest.json
17
+ examples/**/node_modules/
18
+ packages/**/node_modules/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 istarwyh
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.
@@ -0,0 +1,56 @@
1
+ Metadata-Version: 2.5
2
+ Name: harbor-dsh-evolution
3
+ Version: 0.1.0
4
+ Summary: Candidate identity, evaluation summaries, and promotion gates for DeepSeek Harness agents evaluated with Harbor.
5
+ Project-URL: Homepage, https://github.com/istarwyh/harbor-self-evolving
6
+ Project-URL: Documentation, https://github.com/istarwyh/harbor-self-evolving#readme
7
+ Project-URL: Issues, https://github.com/istarwyh/harbor-self-evolving/issues
8
+ Project-URL: Repository, https://github.com/istarwyh/harbor-self-evolving.git
9
+ Author: istarwyh
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Keywords: agent-evaluation,agent-evolution,deepseek-harness,harbor
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Environment :: Console
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: License :: OSI Approved :: MIT License
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Requires-Python: >=3.12
20
+ Requires-Dist: harbor<0.22,>=0.21.0
21
+ Description-Content-Type: text/markdown
22
+
23
+ # harbor-dsh-evolution
24
+
25
+ Harbor-side integration for DeepSeek Harness Candidate evolution.
26
+
27
+ It provides:
28
+
29
+ - `DshCandidateAgent`: verifies and uploads an immutable Candidate, installs its locked npm dependencies, and runs it through Harbor's ACP runner.
30
+ - `EvolutionPlugin`: records Candidate identity, evaluation-context identity, Trial events, and `evaluation-summary.json` for each Job.
31
+ - `harbor-dsh`: snapshots/verifies Candidates, fingerprints datasets, summarizes Jobs, and runs the deterministic Promotion Gate.
32
+
33
+ Install it into the same Python environment as Harbor so the plugin entry point is discoverable:
34
+
35
+ ```bash
36
+ uv venv .venv
37
+ uv pip install --python .venv/bin/python harbor-dsh-evolution
38
+ source .venv/bin/activate
39
+ harbor plugins list
40
+ harbor-dsh --help
41
+ ```
42
+
43
+ Development from this repository:
44
+
45
+ ```bash
46
+ uv sync
47
+ uv run harbor plugins list
48
+ uv run harbor-dsh --help
49
+ uv run harbor-dsh context ../../examples/deep-research/task
50
+ uv run pytest
51
+ uv build
52
+ ```
53
+
54
+ Harbor and this package must be installed into the same Python environment for the `dsh-evolution` entry point to appear in `harbor plugins list`.
55
+
56
+ `snapshot` derives Candidate id and version from `package.json` unless they are explicitly supplied. Promotion rejects Jobs whose `evaluation-context` digests are missing or different.
@@ -0,0 +1,34 @@
1
+ # harbor-dsh-evolution
2
+
3
+ Harbor-side integration for DeepSeek Harness Candidate evolution.
4
+
5
+ It provides:
6
+
7
+ - `DshCandidateAgent`: verifies and uploads an immutable Candidate, installs its locked npm dependencies, and runs it through Harbor's ACP runner.
8
+ - `EvolutionPlugin`: records Candidate identity, evaluation-context identity, Trial events, and `evaluation-summary.json` for each Job.
9
+ - `harbor-dsh`: snapshots/verifies Candidates, fingerprints datasets, summarizes Jobs, and runs the deterministic Promotion Gate.
10
+
11
+ Install it into the same Python environment as Harbor so the plugin entry point is discoverable:
12
+
13
+ ```bash
14
+ uv venv .venv
15
+ uv pip install --python .venv/bin/python harbor-dsh-evolution
16
+ source .venv/bin/activate
17
+ harbor plugins list
18
+ harbor-dsh --help
19
+ ```
20
+
21
+ Development from this repository:
22
+
23
+ ```bash
24
+ uv sync
25
+ uv run harbor plugins list
26
+ uv run harbor-dsh --help
27
+ uv run harbor-dsh context ../../examples/deep-research/task
28
+ uv run pytest
29
+ uv build
30
+ ```
31
+
32
+ Harbor and this package must be installed into the same Python environment for the `dsh-evolution` entry point to appear in `harbor plugins list`.
33
+
34
+ `snapshot` derives Candidate id and version from `package.json` unless they are explicitly supplied. Promotion rejects Jobs whose `evaluation-context` digests are missing or different.
@@ -0,0 +1,50 @@
1
+ [project]
2
+ name = "harbor-dsh-evolution"
3
+ version = "0.1.0"
4
+ description = "Candidate identity, evaluation summaries, and promotion gates for DeepSeek Harness agents evaluated with Harbor."
5
+ readme = "README.md"
6
+ requires-python = ">=3.12"
7
+ license = "MIT"
8
+ license-files = ["LICENSE"]
9
+ authors = [{ name = "istarwyh" }]
10
+ keywords = [
11
+ "agent-evaluation",
12
+ "agent-evolution",
13
+ "deepseek-harness",
14
+ "harbor",
15
+ ]
16
+ classifiers = [
17
+ "Development Status :: 4 - Beta",
18
+ "Environment :: Console",
19
+ "Intended Audience :: Developers",
20
+ "License :: OSI Approved :: MIT License",
21
+ "Programming Language :: Python :: 3",
22
+ "Programming Language :: Python :: 3.12",
23
+ ]
24
+ dependencies = ["harbor>=0.21.0,<0.22"]
25
+
26
+ [project.urls]
27
+ Homepage = "https://github.com/istarwyh/harbor-self-evolving"
28
+ Documentation = "https://github.com/istarwyh/harbor-self-evolving#readme"
29
+ Issues = "https://github.com/istarwyh/harbor-self-evolving/issues"
30
+ Repository = "https://github.com/istarwyh/harbor-self-evolving.git"
31
+
32
+ [project.entry-points."harbor.plugins"]
33
+ dsh-evolution = "harbor_dsh_evolution.plugin:EvolutionPlugin"
34
+
35
+ [project.scripts]
36
+ harbor-dsh = "harbor_dsh_evolution.cli:main"
37
+
38
+ [dependency-groups]
39
+ dev = ["pytest>=8.4", "pytest-asyncio>=1.0"]
40
+
41
+ [build-system]
42
+ requires = ["hatchling>=1.27"]
43
+ build-backend = "hatchling.build"
44
+
45
+ [tool.hatch.build.targets.wheel]
46
+ packages = ["src/harbor_dsh_evolution"]
47
+
48
+ [tool.pytest.ini_options]
49
+ testpaths = ["tests"]
50
+ addopts = "-q"
@@ -0,0 +1,7 @@
1
+ """Harbor integration for DeepSeek Harness Candidate evolution.
2
+
3
+ Keep this module dependency-free so the Candidate CLI can create and verify
4
+ manifests even when it is invoked outside Harbor's Python environment.
5
+ """
6
+
7
+ __version__ = "0.1.0"
@@ -0,0 +1,151 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import shlex
5
+ from pathlib import Path
6
+ from typing import override
7
+
8
+ from harbor.agents.installed.acp import AcpAgent, DistributionKind
9
+ from harbor.environments.base import BaseEnvironment
10
+ from harbor.models.agent.context import AgentContext
11
+
12
+ from harbor_dsh_evolution.candidate import CandidateManifest, verify_candidate
13
+
14
+
15
+ class DshCandidateAgent(AcpAgent):
16
+ """Run one immutable DeepSeek Harness Candidate through Harbor's ACP runner."""
17
+
18
+ _REMOTE_ROOT = "/opt/harbor-dsh-candidate"
19
+ _DSH_ACP_PACKAGE = "@deepseek-ai/dsh-acp-demo@0.1.0-rc.6"
20
+
21
+ def __init__(
22
+ self,
23
+ logs_dir: Path,
24
+ candidate_path: str,
25
+ candidate_digest: str,
26
+ candidate_version: str | None = None,
27
+ **kwargs,
28
+ ) -> None:
29
+ self.candidate_path = Path(candidate_path).expanduser().resolve(strict=True)
30
+ self.manifest: CandidateManifest = verify_candidate(
31
+ self.candidate_path, expected_digest=candidate_digest
32
+ )
33
+ if candidate_version is not None and candidate_version != self.manifest.version:
34
+ raise ValueError(
35
+ "Candidate version mismatch: "
36
+ f"requested={candidate_version}, manifest={self.manifest.version}"
37
+ )
38
+ self.candidate_digest = candidate_digest
39
+
40
+ registry_entry = {
41
+ "id": self.manifest.candidate_id,
42
+ "name": f"DeepSeek Harness Candidate {self.manifest.candidate_id}",
43
+ "version": self.manifest.version,
44
+ "description": "Immutable Cordis composition evaluated through ACP.",
45
+ "distribution": {
46
+ "npx": {
47
+ "package": self._DSH_ACP_PACKAGE,
48
+ "args": ["--config", f"{self._REMOTE_ROOT}/cordis.yml"],
49
+ "env": {
50
+ "DSH_SESSION_ROOT": f"{self._REMOTE_ROOT}/.sessions",
51
+ },
52
+ }
53
+ },
54
+ }
55
+ super().__init__(
56
+ logs_dir=logs_dir,
57
+ registry_entry=registry_entry,
58
+ distribution_preference="npx",
59
+ auth_policy="disabled",
60
+ # This adapter only exposes an architecture-neutral npx distribution.
61
+ # Supplying a supported platform avoids a best-effort uname probe on
62
+ # deliberately minimal task images where that probe can be unavailable.
63
+ target_platform="linux-x86_64",
64
+ **kwargs,
65
+ )
66
+
67
+ @staticmethod
68
+ @override
69
+ def name() -> str:
70
+ return "dsh-candidate"
71
+
72
+ @override
73
+ def version(self) -> str:
74
+ return self.manifest.version
75
+
76
+ @override
77
+ def _build_dependencies_command(self, kind: DistributionKind) -> str:
78
+ """Avoid network installation when the Task image is ACP-ready.
79
+
80
+ Harbor's generic ACP installer supports arbitrary base images, but a
81
+ stable evaluation image should pin these dependencies at build time.
82
+ ``stdbuf`` is included because Harbor's ACP log pipeline requires it.
83
+ """
84
+ install = super()._build_dependencies_command(kind)
85
+ return f"""
86
+ set -euo pipefail
87
+ if command -v bash >/dev/null 2>&1 \
88
+ && command -v python3 >/dev/null 2>&1 \
89
+ && command -v curl >/dev/null 2>&1 \
90
+ && command -v node >/dev/null 2>&1 \
91
+ && command -v npm >/dev/null 2>&1 \
92
+ && command -v stdbuf >/dev/null 2>&1 \
93
+ && [ -x {self._RUNNER_VENV_PATH}/bin/python ] \
94
+ && {self._RUNNER_VENV_PATH}/bin/python -c 'import acp' >/dev/null 2>&1; then
95
+ exit 0
96
+ fi
97
+ {install}
98
+ if ! command -v stdbuf >/dev/null 2>&1; then
99
+ if command -v apk >/dev/null 2>&1; then
100
+ apk add --no-cache coreutils
101
+ elif command -v apt-get >/dev/null 2>&1; then
102
+ apt-get install -y coreutils
103
+ fi
104
+ fi
105
+ """.strip()
106
+
107
+ @override
108
+ async def setup(self, environment: BaseEnvironment) -> None:
109
+ await super().setup(environment)
110
+ await environment.exec(
111
+ f"rm -rf {shlex.quote(self._REMOTE_ROOT)} && "
112
+ f"mkdir -p {shlex.quote(self._REMOTE_ROOT)}",
113
+ user="root",
114
+ )
115
+ await environment.upload_dir(self.candidate_path, self._REMOTE_ROOT)
116
+
117
+ package_command = (
118
+ "npm ci" if (self.candidate_path / "package-lock.json").is_file()
119
+ else "npm install"
120
+ )
121
+ install = await environment.exec(
122
+ f"{package_command} --omit=dev --ignore-scripts --no-audit --no-fund",
123
+ cwd=self._REMOTE_ROOT,
124
+ user="root",
125
+ timeout_sec=600,
126
+ )
127
+ if install.return_code != 0:
128
+ raise RuntimeError(
129
+ "Failed to install Candidate runtime dependencies: "
130
+ f"{install.stderr or install.stdout}"
131
+ )
132
+ await environment.exec(
133
+ f"chmod -R a+rX {shlex.quote(self._REMOTE_ROOT)}", user="root"
134
+ )
135
+
136
+ @override
137
+ def populate_context_post_run(self, context: AgentContext) -> None:
138
+ super().populate_context_post_run(context)
139
+ metadata = dict(context.metadata or {})
140
+ metadata["candidate"] = {
141
+ "id": self.manifest.candidate_id,
142
+ "version": self.manifest.version,
143
+ "digest": self.candidate_digest,
144
+ "runtime": self.manifest.runtime,
145
+ }
146
+ context.metadata = metadata
147
+
148
+ self.logs_dir.mkdir(parents=True, exist_ok=True)
149
+ (self.logs_dir / "candidate.json").write_text(
150
+ json.dumps(metadata["candidate"], ensure_ascii=False, indent=2) + "\n"
151
+ )
@@ -0,0 +1,176 @@
1
+ from __future__ import annotations
2
+
3
+ import hashlib
4
+ import json
5
+ import re
6
+ from dataclasses import asdict, dataclass
7
+ from datetime import datetime, timezone
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+ MANIFEST_NAME = "candidate-manifest.json"
12
+ _DIGEST_PREFIX = b"harbor-dsh-candidate-v1\0"
13
+ _EXCLUDED_DIRS = {".git", "node_modules", "__pycache__"}
14
+ _EXCLUDED_FILES = {MANIFEST_NAME, ".DS_Store"}
15
+ _DIGEST_PATTERN = re.compile(r"^sha256:[0-9a-f]{64}$")
16
+
17
+
18
+ @dataclass(frozen=True)
19
+ class CandidateFile:
20
+ path: str
21
+ size: int
22
+ sha256: str
23
+
24
+
25
+ @dataclass(frozen=True)
26
+ class CandidateManifest:
27
+ schema_version: int
28
+ candidate_id: str
29
+ version: str
30
+ digest: str
31
+ created_at: str
32
+ runtime: dict[str, str]
33
+ files: list[CandidateFile]
34
+ metadata: dict[str, Any]
35
+
36
+ def to_dict(self) -> dict[str, Any]:
37
+ return asdict(self)
38
+
39
+ @classmethod
40
+ def from_dict(cls, value: dict[str, Any]) -> "CandidateManifest":
41
+ schema_version = int(value["schema_version"])
42
+ if schema_version != 1:
43
+ raise ValueError(f"Unsupported Candidate schema_version: {schema_version}")
44
+ candidate_id = str(value["candidate_id"])
45
+ version = str(value["version"])
46
+ digest = str(value["digest"])
47
+ if not candidate_id or not version:
48
+ raise ValueError("Candidate id and version must not be empty")
49
+ if not _DIGEST_PATTERN.fullmatch(digest):
50
+ raise ValueError(f"Invalid Candidate digest: {digest}")
51
+ return cls(
52
+ schema_version=schema_version,
53
+ candidate_id=candidate_id,
54
+ version=version,
55
+ digest=digest,
56
+ created_at=str(value["created_at"]),
57
+ runtime=dict(value["runtime"]),
58
+ files=[CandidateFile(**item) for item in value["files"]],
59
+ metadata=dict(value.get("metadata") or {}),
60
+ )
61
+
62
+
63
+ def _candidate_files(candidate_dir: Path) -> list[Path]:
64
+ paths: list[Path] = []
65
+ for path in candidate_dir.rglob("*"):
66
+ relative = path.relative_to(candidate_dir)
67
+ if any(part in _EXCLUDED_DIRS for part in relative.parts):
68
+ continue
69
+ if path.name in _EXCLUDED_FILES:
70
+ continue
71
+ if path.is_symlink():
72
+ raise ValueError(f"Candidate must not contain symlinks: {relative}")
73
+ if path.is_file():
74
+ paths.append(path)
75
+ return sorted(paths, key=lambda item: item.relative_to(candidate_dir).as_posix())
76
+
77
+
78
+ def compute_candidate(candidate_dir: Path) -> tuple[str, list[CandidateFile]]:
79
+ candidate_dir = candidate_dir.expanduser().resolve(strict=True)
80
+ if not candidate_dir.is_dir():
81
+ raise ValueError(f"Candidate path is not a directory: {candidate_dir}")
82
+
83
+ digest = hashlib.sha256()
84
+ digest.update(_DIGEST_PREFIX)
85
+ files: list[CandidateFile] = []
86
+ for path in _candidate_files(candidate_dir):
87
+ relative = path.relative_to(candidate_dir).as_posix()
88
+ content = path.read_bytes()
89
+ file_digest = hashlib.sha256(content).hexdigest()
90
+ files.append(
91
+ CandidateFile(path=relative, size=len(content), sha256=file_digest)
92
+ )
93
+ digest.update(relative.encode("utf-8"))
94
+ digest.update(b"\0")
95
+ digest.update(str(len(content)).encode("ascii"))
96
+ digest.update(b"\0")
97
+ digest.update(content)
98
+ digest.update(b"\0")
99
+ return f"sha256:{digest.hexdigest()}", files
100
+
101
+
102
+ def snapshot_candidate(
103
+ candidate_dir: Path,
104
+ *,
105
+ candidate_id: str | None = None,
106
+ version: str | None = None,
107
+ runtime_version: str = "0.1.0-rc.6",
108
+ metadata: dict[str, Any] | None = None,
109
+ ) -> CandidateManifest:
110
+ candidate_dir = candidate_dir.expanduser().resolve(strict=True)
111
+ required = [candidate_dir / "cordis.yml", candidate_dir / "package.json"]
112
+ missing = [path.name for path in required if not path.is_file()]
113
+ if missing:
114
+ raise ValueError(f"Candidate is missing required files: {', '.join(missing)}")
115
+
116
+ try:
117
+ package = json.loads((candidate_dir / "package.json").read_text())
118
+ except json.JSONDecodeError as error:
119
+ raise ValueError("Candidate package.json is not valid JSON") from error
120
+ if candidate_id is None:
121
+ candidate_id = str(package.get("name") or "")
122
+ if version is None:
123
+ version = str(package.get("version") or "")
124
+ if not candidate_id or not version or not runtime_version:
125
+ raise ValueError(
126
+ "Candidate id, version, and runtime version must not be empty; "
127
+ "set package.json name/version or pass explicit values"
128
+ )
129
+
130
+ digest, files = compute_candidate(candidate_dir)
131
+ manifest = CandidateManifest(
132
+ schema_version=1,
133
+ candidate_id=candidate_id,
134
+ version=version,
135
+ digest=digest,
136
+ created_at=datetime.now(timezone.utc).isoformat(),
137
+ runtime={
138
+ "kind": "deepseek-harness",
139
+ "version": runtime_version,
140
+ "transport": "acp",
141
+ },
142
+ files=files,
143
+ metadata=metadata or {},
144
+ )
145
+ (candidate_dir / MANIFEST_NAME).write_text(
146
+ json.dumps(manifest.to_dict(), ensure_ascii=False, indent=2) + "\n"
147
+ )
148
+ return manifest
149
+
150
+
151
+ def load_manifest(candidate_dir: Path) -> CandidateManifest:
152
+ path = candidate_dir.expanduser().resolve(strict=True) / MANIFEST_NAME
153
+ return CandidateManifest.from_dict(json.loads(path.read_text()))
154
+
155
+
156
+ def verify_candidate(
157
+ candidate_dir: Path,
158
+ *,
159
+ expected_digest: str | None = None,
160
+ ) -> CandidateManifest:
161
+ candidate_dir = candidate_dir.expanduser().resolve(strict=True)
162
+ manifest = load_manifest(candidate_dir)
163
+ actual_digest, actual_files = compute_candidate(candidate_dir)
164
+ if actual_digest != manifest.digest:
165
+ raise ValueError(
166
+ "Candidate digest mismatch: "
167
+ f"manifest={manifest.digest}, actual={actual_digest}"
168
+ )
169
+ if expected_digest is not None and actual_digest != expected_digest:
170
+ raise ValueError(
171
+ "Candidate does not match requested digest: "
172
+ f"expected={expected_digest}, actual={actual_digest}"
173
+ )
174
+ if actual_files != manifest.files:
175
+ raise ValueError("Candidate file inventory does not match its manifest")
176
+ return manifest
@@ -0,0 +1,69 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import json
5
+ from pathlib import Path
6
+
7
+ from harbor_dsh_evolution.candidate import snapshot_candidate, verify_candidate
8
+ from harbor_dsh_evolution.context import build_evaluation_context
9
+ from harbor_dsh_evolution.promotion import compare_jobs, write_report
10
+ from harbor_dsh_evolution.summary import load_or_create_summary
11
+
12
+
13
+ def _parser() -> argparse.ArgumentParser:
14
+ parser = argparse.ArgumentParser(prog="harbor-dsh")
15
+ commands = parser.add_subparsers(dest="command", required=True)
16
+
17
+ snapshot = commands.add_parser("snapshot", help="Freeze a Candidate directory")
18
+ snapshot.add_argument("candidate_dir", type=Path)
19
+ snapshot.add_argument("--id", dest="candidate_id")
20
+ snapshot.add_argument("--version")
21
+ snapshot.add_argument("--runtime-version", default="0.1.0-rc.6")
22
+
23
+ verify = commands.add_parser("verify", help="Verify a Candidate digest")
24
+ verify.add_argument("candidate_dir", type=Path)
25
+ verify.add_argument("--digest")
26
+
27
+ summary = commands.add_parser("summarize", help="Summarize a Harbor Job")
28
+ summary.add_argument("job_dir", type=Path)
29
+
30
+ context = commands.add_parser("context", help="Fingerprint a Harbor dataset")
31
+ context.add_argument("dataset_dir", type=Path)
32
+
33
+ promote = commands.add_parser("promote", help="Apply a Promotion Gate")
34
+ promote.add_argument("baseline_job", type=Path)
35
+ promote.add_argument("candidate_job", type=Path)
36
+ promote.add_argument("--policy", required=True, type=Path)
37
+ promote.add_argument("--output", type=Path)
38
+ return parser
39
+
40
+
41
+ def main() -> int:
42
+ args = _parser().parse_args()
43
+ if args.command == "snapshot":
44
+ result = snapshot_candidate(
45
+ args.candidate_dir,
46
+ candidate_id=args.candidate_id,
47
+ version=args.version,
48
+ runtime_version=args.runtime_version,
49
+ ).to_dict()
50
+ elif args.command == "verify":
51
+ result = verify_candidate(
52
+ args.candidate_dir, expected_digest=args.digest
53
+ ).to_dict()
54
+ elif args.command == "summarize":
55
+ result = load_or_create_summary(args.job_dir)
56
+ elif args.command == "context":
57
+ result = build_evaluation_context(args.dataset_dir).to_dict()
58
+ else:
59
+ report = compare_jobs(args.baseline_job, args.candidate_job, args.policy)
60
+ output = args.output or args.candidate_job / "promotion-report.json"
61
+ write_report(report, output)
62
+ result = report
63
+
64
+ print(json.dumps(result, ensure_ascii=False, indent=2))
65
+ return 0 if result.get("decision") != "REJECT" else 1
66
+
67
+
68
+ if __name__ == "__main__":
69
+ raise SystemExit(main())