ai-kit-python 1.4.2__py3-none-any.whl
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.
- ai_kit_python-1.4.2.dist-info/METADATA +132 -0
- ai_kit_python-1.4.2.dist-info/RECORD +278 -0
- ai_kit_python-1.4.2.dist-info/WHEEL +5 -0
- ai_kit_python-1.4.2.dist-info/entry_points.txt +2 -0
- ai_kit_python-1.4.2.dist-info/licenses/LICENSE +21 -0
- ai_kit_python-1.4.2.dist-info/licenses/template/ai-kit/vendor/security-audit/LICENSE +21 -0
- ai_kit_python-1.4.2.dist-info/licenses/template/ai-kit/vendor/strix/LICENSE +201 -0
- ai_kit_python-1.4.2.dist-info/top_level.txt +1 -0
- aikit/.agents/skills/go-work/SKILL.md +8 -0
- aikit/.agents/skills/laravel-work/SKILL.md +8 -0
- aikit/.agents/skills/moodle-work/SKILL.md +8 -0
- aikit/.agents/skills/project-continuity/SKILL.md +34 -0
- aikit/.agents/skills/python-work/SKILL.md +8 -0
- aikit/.dockerignore +90 -0
- aikit/.gitattributes +3 -0
- aikit/.github/workflows/check-kit.yml +70 -0
- aikit/.github/workflows/publish-pypi.yml +108 -0
- aikit/.gitignore +60 -0
- aikit/BOOTSTRAP_PROMPT.md +18 -0
- aikit/CHANGELOG.md +85 -0
- aikit/CLAUDE.md +3 -0
- aikit/CONTRIBUTING.md +46 -0
- aikit/Dockerfile +44 -0
- aikit/KIMI.md +3 -0
- aikit/LICENSE +21 -0
- aikit/MANIFEST.in +22 -0
- aikit/MANUS.md +3 -0
- aikit/Makefile +70 -0
- aikit/README.md +1161 -0
- aikit/SECURITY.md +8 -0
- aikit/SKILL.md +105 -0
- aikit/VERSION +1 -0
- aikit/__init__.py +1 -0
- aikit/ai-kit/ADAPTERS.md +5 -0
- aikit/ai-kit/CONTAINERS.md +5 -0
- aikit/ai-kit/ENGINEERING.md +5 -0
- aikit/ai-kit/INTENT.md +5 -0
- aikit/ai-kit/MODELS.md +5 -0
- aikit/ai-kit/SKILLS.md +5 -0
- aikit/ai-kit/stacks/GO.md +5 -0
- aikit/ai-kit/stacks/LARAVEL.md +5 -0
- aikit/ai-kit/stacks/MOODLE.md +5 -0
- aikit/ai-kit/stacks/PYTHON.md +5 -0
- aikit/aikit_cli.py +59 -0
- aikit/deploy/k8s/install-preview-job.yaml +56 -0
- aikit/docker/build-requirements.txt +4 -0
- aikit/docker/check_version.py +29 -0
- aikit/docker/pypi-requirements.txt +3 -0
- aikit/docs/ADAPTER_PATH_FIX.md +23 -0
- aikit/docs/BENCHMARK.md +106 -0
- aikit/docs/CONTEXT7.md +33 -0
- aikit/docs/DECISIONS.md +22 -0
- aikit/docs/DEVELOPMENT_PLAN.md +16 -0
- aikit/docs/DIALOGUE_MEMORY.md +70 -0
- aikit/docs/DOCKER.md +74 -0
- aikit/docs/DOCKER_HUB_OVERVIEW.md +311 -0
- aikit/docs/EVALUATION.md +30 -0
- aikit/docs/IMPLEMENTATION.md +134 -0
- aikit/docs/MANUS_REVIEW.md +46 -0
- aikit/docs/PYPI.md +87 -0
- aikit/docs/PYPI_DESCRIPTION.md +103 -0
- aikit/docs/SECURITY_AUDIT.md +26 -0
- aikit/docs/STRIX.md +28 -0
- aikit/docs/V0_5_0.md +27 -0
- aikit/docs/V0_6_4.md +25 -0
- aikit/docs/V1_4_0.md +73 -0
- aikit/docs/V1_4_1.md +55 -0
- aikit/docs/V1_4_2.md +33 -0
- aikit/docs/WEB_DESIGN.md +72 -0
- aikit/docs/WINDOWS_CI_FIX.md +39 -0
- aikit/docs/adr/0000-template.md +24 -0
- aikit/docs/adr/0001-adaptive-model-routing.md +30 -0
- aikit/docs/adr/0002-template-separation.md +24 -0
- aikit/docs/adr/0003-agent-registry-and-router.md +24 -0
- aikit/docs/adr/0004-onboarding-and-project-facts.md +24 -0
- aikit/docs/adr/0005-client-settings-ownership.md +30 -0
- aikit/docs/adr/0006-routing-evidence-and-local-measurements.md +26 -0
- aikit/docs/adr/0007-generated-client-configuration-and-presets.md +34 -0
- aikit/docs/adr/0008-unified-cli-and-fact-drift.md +20 -0
- aikit/docs/adr/0009-provider-review-doctor-fix-and-ci-self-install.md +20 -0
- aikit/docs/adr/0010-metrics-analysis-and-gemini-verification.md +20 -0
- aikit/docs/adr/0011-evidence-lifecycle-and-evaluation.md +20 -0
- aikit/docs/adr/0012-measurement-led-routing.md +31 -0
- aikit/docs/adr/0013-docker-cli-image.md +24 -0
- aikit/docs/adr/0014-local-dialogue-memory.md +22 -0
- aikit/docs/history/original-bundle-checks.md +11 -0
- aikit/docs/history/v0.1-changelog.txt +37 -0
- aikit/docs/history/v0.10-changelog.md +12 -0
- aikit/docs/history/v0.2-changelog.md +35 -0
- aikit/docs/history/v0.3-changelog.md +25 -0
- aikit/docs/history/v0.4-changelog.md +31 -0
- aikit/docs/history/v0.5-changelog.md +14 -0
- aikit/docs/history/v0.6.4-changelog.md +12 -0
- aikit/docs/history/v0.6.5-v0.9-changelog.md +35 -0
- aikit/docs/history/v1.1.0-changelog.md +12 -0
- aikit/docs/history/v1.2.0-changelog.md +11 -0
- aikit/docs/history/verification-before-measurements.md +53 -0
- aikit/integrations/CONVENTIONS.md +3 -0
- aikit/integrations/README.md +7 -0
- aikit/integrations/agents.json +136 -0
- aikit/integrations/ai-kit-check.yml +66 -0
- aikit/integrations/ai-kit-cline.md +4 -0
- aikit/integrations/ai-kit-roo.md +4 -0
- aikit/integrations/ai-kit-windsurf.md +4 -0
- aikit/integrations/ai-kit.mdc +6 -0
- aikit/integrations/claude-settings.data-ask.json +23 -0
- aikit/integrations/claude-settings.git-ask.json +10 -0
- aikit/integrations/copilot-instructions.md +3 -0
- aikit/integrations/security-audit.json +135 -0
- aikit/integrations/strix.json +69 -0
- aikit/pyproject.toml +60 -0
- aikit/scripts/__init__.py +1 -0
- aikit/scripts/adapters.py +184 -0
- aikit/scripts/adaptive.py +167 -0
- aikit/scripts/adr.py +61 -0
- aikit/scripts/benchmark.py +543 -0
- aikit/scripts/changelog.py +55 -0
- aikit/scripts/check_distribution.py +178 -0
- aikit/scripts/check_kit.py +406 -0
- aikit/scripts/context.py +184 -0
- aikit/scripts/docker_test_usage.py +55 -0
- aikit/scripts/doctor.py +313 -0
- aikit/scripts/evaluate.py +125 -0
- aikit/scripts/install.py +1584 -0
- aikit/scripts/learning.py +260 -0
- aikit/scripts/memory.py +344 -0
- aikit/scripts/memory_import.py +177 -0
- aikit/scripts/memory_store.py +125 -0
- aikit/scripts/metrics.py +351 -0
- aikit/scripts/router.py +399 -0
- aikit/template/.agents/hooks/session-start.md +3 -0
- aikit/template/.agents/hooks/session-start.sh +21 -0
- aikit/template/.agents/skills/ai-kit-bootstrap/SKILL.md +14 -0
- aikit/template/.agents/skills/ai-kit-review/SKILL.md +11 -0
- aikit/template/.agents/skills/ai-kit-sync-context/SKILL.md +12 -0
- aikit/template/.agents/skills/context7-docs/SKILL.md +11 -0
- aikit/template/.agents/skills/design-extract/SKILL.md +9 -0
- aikit/template/.agents/skills/dotnet-work/SKILL.md +9 -0
- aikit/template/.agents/skills/filament-work/SKILL.md +9 -0
- aikit/template/.agents/skills/frontend-work/SKILL.md +9 -0
- aikit/template/.agents/skills/go-work/SKILL.md +9 -0
- aikit/template/.agents/skills/infra-work/SKILL.md +9 -0
- aikit/template/.agents/skills/java-work/SKILL.md +9 -0
- aikit/template/.agents/skills/laravel-blade-ui/SKILL.md +9 -0
- aikit/template/.agents/skills/laravel-work/SKILL.md +10 -0
- aikit/template/.agents/skills/library-work/SKILL.md +9 -0
- aikit/template/.agents/skills/moodle-work/SKILL.md +9 -0
- aikit/template/.agents/skills/php-work/SKILL.md +9 -0
- aikit/template/.agents/skills/project-continuity/SKILL.md +21 -0
- aikit/template/.agents/skills/python-work/SKILL.md +9 -0
- aikit/template/.agents/skills/rust-work/SKILL.md +9 -0
- aikit/template/.agents/skills/security-audit/SKILL.md +16 -0
- aikit/template/.agents/skills/security-audit/agents/openai.yaml +2 -0
- aikit/template/.agents/skills/strix/SKILL.md +16 -0
- aikit/template/.agents/skills/strix/agents/openai.yaml +2 -0
- aikit/template/.agents/skills/ui-review/SKILL.md +9 -0
- aikit/template/.agents/skills/web-design/SKILL.md +9 -0
- aikit/template/AGENTS.md +16 -0
- aikit/template/CHANGELOG.md +6 -0
- aikit/template/CLAUDE.md +5 -0
- aikit/template/GEMINI.md +4 -0
- aikit/template/KIMI.md +4 -0
- aikit/template/MANUS.md +4 -0
- aikit/template/PROJECT_CONTEXT.md +35 -0
- aikit/template/README.md +6 -0
- aikit/template/WIKI.md +37 -0
- aikit/template/ai-kit/ADAPTERS.md +100 -0
- aikit/template/ai-kit/BOOTSTRAP.md +15 -0
- aikit/template/ai-kit/CONTAINERS.md +63 -0
- aikit/template/ai-kit/CONTEXT7.md +96 -0
- aikit/template/ai-kit/CORE.md +23 -0
- aikit/template/ai-kit/ENGINEERING.md +34 -0
- aikit/template/ai-kit/MCP.md +17 -0
- aikit/template/ai-kit/METRICS.md +83 -0
- aikit/template/ai-kit/SECURITY-AUDIT.md +21 -0
- aikit/template/ai-kit/SECURITY.md +34 -0
- aikit/template/ai-kit/SKILLS.md +37 -0
- aikit/template/ai-kit/STRIX.md +44 -0
- aikit/template/ai-kit/UPSTREAM.md +38 -0
- aikit/template/ai-kit/WEB-DESIGN.md +37 -0
- aikit/template/ai-kit/profiles/OWNER.md +19 -0
- aikit/template/ai-kit/project.json +14 -0
- aikit/template/ai-kit/router/POLICY.md +79 -0
- aikit/template/ai-kit/router/providers/anthropic.json +44 -0
- aikit/template/ai-kit/router/providers/anthropic.md +14 -0
- aikit/template/ai-kit/router/providers/gemini.json +13 -0
- aikit/template/ai-kit/router/providers/gemini.md +10 -0
- aikit/template/ai-kit/router/providers/kimi.json +26 -0
- aikit/template/ai-kit/router/providers/kimi.md +13 -0
- aikit/template/ai-kit/router/providers/local.json +18 -0
- aikit/template/ai-kit/router/providers/local.md +10 -0
- aikit/template/ai-kit/router/providers/openai.json +48 -0
- aikit/template/ai-kit/router/providers/openai.md +14 -0
- aikit/template/ai-kit/router/routing.json +11 -0
- aikit/template/ai-kit/router/selection.json +14 -0
- aikit/template/ai-kit/settings.json +11 -0
- aikit/template/ai-kit/stacks/DOTNET.md +13 -0
- aikit/template/ai-kit/stacks/FILAMENT.md +18 -0
- aikit/template/ai-kit/stacks/FRONTEND.md +9 -0
- aikit/template/ai-kit/stacks/GO.md +19 -0
- aikit/template/ai-kit/stacks/INFRA.md +12 -0
- aikit/template/ai-kit/stacks/JAVA.md +13 -0
- aikit/template/ai-kit/stacks/LARAVEL.md +15 -0
- aikit/template/ai-kit/stacks/LIBRARY.md +9 -0
- aikit/template/ai-kit/stacks/MOODLE.md +12 -0
- aikit/template/ai-kit/stacks/PHP.md +11 -0
- aikit/template/ai-kit/stacks/PYTHON.md +11 -0
- aikit/template/ai-kit/stacks/RUST.md +18 -0
- aikit/template/ai-kit/vendor/security-audit/AI-AND-LLM.md +83 -0
- aikit/template/ai-kit/vendor/security-audit/ATTACK-CLASSES.md +130 -0
- aikit/template/ai-kit/vendor/security-audit/CLIENT-SIDE.md +83 -0
- aikit/template/ai-kit/vendor/security-audit/CLOUD-AND-DEPLOYMENT.md +86 -0
- aikit/template/ai-kit/vendor/security-audit/DATA-ISOLATION-AND-LIFECYCLE.md +84 -0
- aikit/template/ai-kit/vendor/security-audit/DESKTOP-MOBILE-AND-LOCAL-IPC.md +89 -0
- aikit/template/ai-kit/vendor/security-audit/HUNTING.md +251 -0
- aikit/template/ai-kit/vendor/security-audit/LICENSE +21 -0
- aikit/template/ai-kit/vendor/security-audit/MEMORY-SAFETY-AND-BINARY.md +101 -0
- aikit/template/ai-kit/vendor/security-audit/PROTOCOLS-RPC-AND-MESSAGING.md +81 -0
- aikit/template/ai-kit/vendor/security-audit/RECONNAISSANCE.md +156 -0
- aikit/template/ai-kit/vendor/security-audit/RESOURCE-EXHAUSTION-AND-AVAILABILITY.md +78 -0
- aikit/template/ai-kit/vendor/security-audit/SKILL.md +192 -0
- aikit/template/ai-kit/vendor/security-audit/SUPPLY-CHAIN-AND-RELEASE.md +73 -0
- aikit/template/ai-kit/vendor/security-audit/VALIDATION-AND-REPORTING.md +186 -0
- aikit/template/ai-kit/vendor/security-audit/WEB-PROTOCOL-AND-AUTH.md +105 -0
- aikit/template/ai-kit/vendor/security-audit/report-schema.json +461 -0
- aikit/template/ai-kit/vendor/security-audit/validate-coverage-ledger.cjs +872 -0
- aikit/template/ai-kit/vendor/security-audit/validate-coverage-ledger.test.cjs +740 -0
- aikit/template/ai-kit/vendor/security-audit/validate-findings.cjs +773 -0
- aikit/template/ai-kit/vendor/security-audit/validate-findings.test.cjs +652 -0
- aikit/template/ai-kit/vendor/strix/LICENSE +201 -0
- aikit/template/ai-kit/vendor/strix/api-security-testing/SKILL.md +61 -0
- aikit/template/ai-kit/vendor/strix/application-security-testing/SKILL.md +66 -0
- aikit/template/ai-kit/vendor/strix/ci-security-scanning-with-strix/SKILL.md +149 -0
- aikit/template/ai-kit/vendor/strix/find-security-vulnerabilities-in-code/SKILL.md +62 -0
- aikit/template/ai-kit/vendor/strix/fix-security-vulnerabilities-with-strix/SKILL.md +77 -0
- aikit/template/ai-kit/vendor/strix/managed-pentesting-with-strix/SKILL.md +321 -0
- aikit/template/ai-kit/vendor/strix/owasp-top-10-testing/SKILL.md +64 -0
- aikit/template/ai-kit/vendor/strix/penetration-testing-with-strix/SKILL.md +162 -0
- aikit/template/ai-kit/vendor/strix/web-app-penetration-testing/SKILL.md +54 -0
- aikit/template/ai-kit/web-design/LARAVEL.md +51 -0
- aikit/template/ai-kit/web-design/LIBRARIES.md +39 -0
- aikit/template/ai-kit/web-design/REVIEW.md +37 -0
- aikit/template/ai-kit/web-design/TOOLS.md +43 -0
- aikit/template/docs/DECISIONS.md +7 -0
- aikit/template/docs/adr/0000-template.md +20 -0
- aikit/template/docs/design/ACCEPTANCE.md +24 -0
- aikit/template/docs/design/BRIEF.md +26 -0
- aikit/template/docs/design/SYSTEM.md +30 -0
- aikit/templates/agent-secrets.ignore +19 -0
- aikit/templates/gitignore.private +160 -0
- aikit/templates/gitignore.team +139 -0
- aikit/tests/build_distribution.py +73 -0
- aikit/tests/docker_smoke.py +172 -0
- aikit/tests/fixtures/create-junction.ps1 +18 -0
- aikit/tests/fixtures/golden/claude-settings-local.json +58 -0
- aikit/tests/test_adapters.py +105 -0
- aikit/tests/test_aikit.py +79 -0
- aikit/tests/test_benchmark.py +241 -0
- aikit/tests/test_check_kit.py +203 -0
- aikit/tests/test_context.py +76 -0
- aikit/tests/test_context7.py +153 -0
- aikit/tests/test_context_selection.py +70 -0
- aikit/tests/test_distribution.py +181 -0
- aikit/tests/test_docker_image.py +63 -0
- aikit/tests/test_docker_test_usage.py +54 -0
- aikit/tests/test_doctor.py +148 -0
- aikit/tests/test_evaluate.py +66 -0
- aikit/tests/test_install.py +939 -0
- aikit/tests/test_learning.py +247 -0
- aikit/tests/test_memory.py +371 -0
- aikit/tests/test_metrics.py +209 -0
- aikit/tests/test_native.py +342 -0
- aikit/tests/test_onboarding.py +301 -0
- aikit/tests/test_router.py +336 -0
- aikit/tests/test_security_audit.py +237 -0
- aikit/tests/test_strix.py +245 -0
- aikit/tests/test_web_design.py +219 -0
- aikit/tests/wheel_smoke.py +104 -0
aikit/scripts/adr.py
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""Scaffold a decision record (ADR) from the project's ADR template."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
from datetime import date
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
import re
|
|
8
|
+
import sys
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def slugify(title: str) -> str:
|
|
12
|
+
slug = re.sub(r"[^a-z0-9]+", "-", title.lower()).strip("-")
|
|
13
|
+
return slug or "decision"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def next_number(adr_dir: Path) -> int:
|
|
17
|
+
numbers = [0]
|
|
18
|
+
for path in adr_dir.glob("*.md"):
|
|
19
|
+
match = re.match(r"(\d+)-", path.name)
|
|
20
|
+
if match:
|
|
21
|
+
numbers.append(int(match.group(1)))
|
|
22
|
+
return max(numbers) + 1
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def scaffold(title: str, root: Path) -> Path:
|
|
26
|
+
adr_dir = root / "docs" / "adr"
|
|
27
|
+
template_path = adr_dir / "0000-template.md"
|
|
28
|
+
if not template_path.is_file():
|
|
29
|
+
raise ValueError(f"Missing ADR template: {template_path}")
|
|
30
|
+
number = next_number(adr_dir)
|
|
31
|
+
text = template_path.read_text(encoding="utf-8")
|
|
32
|
+
content = (text
|
|
33
|
+
.replace("# ADR-NNNN: Title", f"# ADR-{number:04d}: {title}", 1)
|
|
34
|
+
.replace("use the verified decision date", date.today().isoformat(), 1)
|
|
35
|
+
.replace("proposed / accepted / superseded", "proposed", 1))
|
|
36
|
+
destination = adr_dir / f"{number:04d}-{slugify(title)}.md"
|
|
37
|
+
destination.parent.mkdir(parents=True, exist_ok=True)
|
|
38
|
+
with destination.open("x", encoding="utf-8") as stream:
|
|
39
|
+
stream.write(content)
|
|
40
|
+
return destination
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def main(argv: list[str] | None = None) -> int:
|
|
44
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
45
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
46
|
+
new_parser = sub.add_parser("new", help="Create a new ADR from the template")
|
|
47
|
+
new_parser.add_argument("title")
|
|
48
|
+
new_parser.add_argument("--root", type=Path, default=Path.cwd(),
|
|
49
|
+
help="Project root (default: current directory)")
|
|
50
|
+
args = parser.parse_args(argv)
|
|
51
|
+
try:
|
|
52
|
+
path = scaffold(args.title, args.root.absolute())
|
|
53
|
+
except (OSError, ValueError, UnicodeError) as exc:
|
|
54
|
+
print(f"ADR creation refused: {exc}", file=sys.stderr)
|
|
55
|
+
return 1
|
|
56
|
+
print(str(path))
|
|
57
|
+
return 0
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
if __name__ == "__main__":
|
|
61
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,543 @@
|
|
|
1
|
+
"""Preview-first isolated A/B/C trials using an explicitly configured worker protocol."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import copy
|
|
6
|
+
import fnmatch
|
|
7
|
+
import hashlib
|
|
8
|
+
import io
|
|
9
|
+
import json
|
|
10
|
+
import os
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
import re
|
|
13
|
+
import shutil
|
|
14
|
+
import subprocess
|
|
15
|
+
import sys
|
|
16
|
+
import time
|
|
17
|
+
|
|
18
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
19
|
+
import adaptive
|
|
20
|
+
import context
|
|
21
|
+
import install
|
|
22
|
+
import learning
|
|
23
|
+
import metrics
|
|
24
|
+
import router
|
|
25
|
+
|
|
26
|
+
ARMS = ("baseline", "kit-fixed", "kit-adaptive")
|
|
27
|
+
ROOT = Path(__file__).resolve().parents[1]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def argv(value: object) -> list[str]:
|
|
31
|
+
if (not isinstance(value, list) or not value or
|
|
32
|
+
any(not isinstance(item, str) or not item or "\x00" in item for item in value)):
|
|
33
|
+
raise ValueError("Commands must be nonempty argv lists; shell strings are unsupported")
|
|
34
|
+
if not Path(value[0]).is_absolute() or not Path(value[0]).is_file():
|
|
35
|
+
raise ValueError("Command executable must be an existing absolute file path")
|
|
36
|
+
return value
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def validate_pack(value: object, project: Path) -> dict:
|
|
40
|
+
pack = metrics._fields(value, {"schema", "experiment", "revision", "repeats", "fixed", "worker",
|
|
41
|
+
"tasks", "checks", "timeout_seconds", "kit_agents", "kit_preset"},
|
|
42
|
+
"benchmark pack")
|
|
43
|
+
if type(pack.get("schema")) is not int or pack["schema"] != 1:
|
|
44
|
+
raise ValueError("Expected benchmark schema 1")
|
|
45
|
+
metrics._identifier(pack.get("experiment"), "experiment")
|
|
46
|
+
if not re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9._-]{0,79}", pack["experiment"]):
|
|
47
|
+
raise ValueError("Experiment must be a portable directory name")
|
|
48
|
+
revision = pack.get("revision")
|
|
49
|
+
if not isinstance(revision, str) or not re.fullmatch(r"[0-9a-f]{40}|[0-9a-f]{64}", revision):
|
|
50
|
+
raise ValueError("Pin a full Git commit ID")
|
|
51
|
+
if type(pack.get("repeats")) is not int or not 1 <= pack["repeats"] <= 100:
|
|
52
|
+
raise ValueError("Repeats must be 1-100")
|
|
53
|
+
adaptive.validate_pin(pack.get("fixed"))
|
|
54
|
+
pack["kit_agents"] = install.agent_selection(pack.get("kit_agents", ["codex"]),
|
|
55
|
+
install.load_agent_registry()["names"], unique=True)
|
|
56
|
+
pack["kit_preset"] = pack.get("kit_preset", "minimal")
|
|
57
|
+
if pack["kit_preset"] not in install.PRESETS:
|
|
58
|
+
raise ValueError(f"Unknown kit preset: {pack['kit_preset']}")
|
|
59
|
+
worker = metrics._fields(pack.get("worker"), {"protocol", "argv"}, "worker")
|
|
60
|
+
if worker.get("protocol") != "aikit-worker-v1":
|
|
61
|
+
raise ValueError("Worker must explicitly implement aikit-worker-v1")
|
|
62
|
+
argv(worker.get("argv"))
|
|
63
|
+
if type(pack.get("timeout_seconds")) is not int or not 1 <= pack["timeout_seconds"] <= 86400:
|
|
64
|
+
raise ValueError("Timeout must be 1-86400 seconds per process")
|
|
65
|
+
if not isinstance(pack.get("checks"), list):
|
|
66
|
+
raise ValueError("Checks must be an explicit list (empty means not-run)")
|
|
67
|
+
for command in pack["checks"]:
|
|
68
|
+
argv(command)
|
|
69
|
+
tasks = pack.get("tasks")
|
|
70
|
+
if not isinstance(tasks, list) or not tasks:
|
|
71
|
+
raise ValueError("Include at least one task")
|
|
72
|
+
seen = set()
|
|
73
|
+
for task in tasks:
|
|
74
|
+
if not isinstance(task, dict) or set(task) != {"task_id", "prompt_file", "rubric_file", "path", "stack", "operation", "risks", "capabilities"}:
|
|
75
|
+
raise ValueError("Invalid benchmark task fields")
|
|
76
|
+
identifier = metrics._identifier(task["task_id"], "task_id")
|
|
77
|
+
if len(identifier) > 110 or identifier in seen:
|
|
78
|
+
raise ValueError("Task IDs must be unique and at most 110 characters")
|
|
79
|
+
seen.add(identifier)
|
|
80
|
+
for field in ("prompt_file", "rubric_file", "path"):
|
|
81
|
+
if not isinstance(task[field], str) or not task[field]:
|
|
82
|
+
raise ValueError("Task paths must be nonempty relative paths")
|
|
83
|
+
if task["path"] != ".":
|
|
84
|
+
install.safe_path(project, task["path"] + "/__path_probe__")
|
|
85
|
+
if task["stack"] not in metrics.STACKS or task["operation"] not in router.OPERATIONS:
|
|
86
|
+
raise ValueError("Unknown stack/operation")
|
|
87
|
+
if not isinstance(task["risks"], list) or any(r not in router.RISKS for r in task["risks"]):
|
|
88
|
+
raise ValueError("Unknown risks")
|
|
89
|
+
if not isinstance(task["capabilities"], list):
|
|
90
|
+
raise ValueError("Capabilities must be a list")
|
|
91
|
+
for capability in task["capabilities"]:
|
|
92
|
+
metrics._identifier(capability, "capability")
|
|
93
|
+
return copy.deepcopy(pack)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def git_command(project: Path, args: list[str], *, git: str | None = None, input_bytes: bytes | None = None) -> bytes:
|
|
97
|
+
executable = git or shutil.which("git")
|
|
98
|
+
if not executable:
|
|
99
|
+
raise ValueError("Git is unavailable")
|
|
100
|
+
try:
|
|
101
|
+
environment = {k: v for k, v in os.environ.items() if not k.startswith("GIT_")}
|
|
102
|
+
return subprocess.run([executable, "--no-replace-objects", "-C", str(project), *args], check=True,
|
|
103
|
+
capture_output=True, env=environment, input=input_bytes, timeout=120).stdout
|
|
104
|
+
except (subprocess.CalledProcessError, subprocess.TimeoutExpired) as exc:
|
|
105
|
+
raise ValueError("Git snapshot command failed") from exc
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def secret_path(name: str, patterns: list[str]) -> bool:
|
|
109
|
+
blocked = False
|
|
110
|
+
for pattern in patterns:
|
|
111
|
+
negative = pattern.startswith("!")
|
|
112
|
+
pattern = pattern.lstrip("!")
|
|
113
|
+
directory = pattern.endswith("/")
|
|
114
|
+
pattern = pattern.strip("/")
|
|
115
|
+
if (fnmatch.fnmatch(name, pattern) or ("/" not in pattern and any(fnmatch.fnmatch(p, pattern) for p in name.split("/"))) or
|
|
116
|
+
(directory and name.startswith(pattern + "/"))):
|
|
117
|
+
blocked = not negative
|
|
118
|
+
return blocked
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def snapshot_files(project: Path, revision: str, *, git: str | None = None) -> list[tuple[str, bytes, int]]:
|
|
122
|
+
toplevel = Path(git_command(project, ["rev-parse", "--show-toplevel"], git=git).decode().strip()).resolve()
|
|
123
|
+
if toplevel != project.resolve():
|
|
124
|
+
raise ValueError("Benchmark project must be the Git working-tree root")
|
|
125
|
+
resolved = git_command(project, ["rev-parse", "--verify", revision + "^{commit}"], git=git).decode().strip()
|
|
126
|
+
if resolved != revision:
|
|
127
|
+
raise ValueError("Starting revision is not the specified commit")
|
|
128
|
+
listing = git_command(project, ["ls-tree", "-r", "-z", revision], git=git)
|
|
129
|
+
names, portable_names = [], set()
|
|
130
|
+
detected = install.detect_facts(project)
|
|
131
|
+
patterns = install.secret_patterns(ROOT, detected["modules"])
|
|
132
|
+
patterns.extend(["auth.json", ".pypirc"])
|
|
133
|
+
for entry in listing.split(b"\x00"):
|
|
134
|
+
if not entry:
|
|
135
|
+
continue
|
|
136
|
+
header, name_bytes = entry.split(b"\t", 1)
|
|
137
|
+
mode, _, blob_id = header.split()
|
|
138
|
+
name = name_bytes.decode("utf-8")
|
|
139
|
+
portable = name.casefold()
|
|
140
|
+
if portable in portable_names:
|
|
141
|
+
raise ValueError("Case-colliding Git paths are not a portable benchmark snapshot")
|
|
142
|
+
portable_names.add(portable)
|
|
143
|
+
if mode not in {b"100644", b"100755"}:
|
|
144
|
+
raise ValueError("Snapshots refuse symlinks, submodules and non-regular Git entries")
|
|
145
|
+
# Refuse a compromised fixture instead of silently changing its starting state.
|
|
146
|
+
parts = name.split("/")
|
|
147
|
+
if (any(p in {".git", ".metrics", ".aws", ".ssh"} for p in parts) or
|
|
148
|
+
secret_path(name, patterns) or
|
|
149
|
+
parts[-1] == ".env" or parts[-1].endswith((".pem", ".key"))):
|
|
150
|
+
raise ValueError("Tracked secret/runtime path in benchmark revision; use a sanitized test repository")
|
|
151
|
+
install.safe_path(project, name)
|
|
152
|
+
if name.startswith("ai-kit/"):
|
|
153
|
+
raise ValueError("Starting revision already contains AI-KIT; baseline requires a pre-kit fixture")
|
|
154
|
+
names.append((name, mode, blob_id.decode()))
|
|
155
|
+
# Read immutable blobs directly: archive/checkout filters can apply CRLF, export-ignore or substitutions.
|
|
156
|
+
blobs = git_command(project, ["cat-file", "--batch"], git=git,
|
|
157
|
+
input_bytes="".join(blob_id + "\n" for _, _, blob_id in names).encode())
|
|
158
|
+
files = []
|
|
159
|
+
with io.BytesIO(blobs) as source:
|
|
160
|
+
for name, mode, blob_id in names:
|
|
161
|
+
header = source.readline().split()
|
|
162
|
+
if len(header) != 3 or header[0].decode() != blob_id or header[1] != b"blob":
|
|
163
|
+
raise ValueError("Unexpected Git blob response")
|
|
164
|
+
data = source.read(int(header[2]))
|
|
165
|
+
if source.read(1) != b"\n":
|
|
166
|
+
raise ValueError("Incomplete Git blob response")
|
|
167
|
+
blob = b"blob " + str(len(data)).encode() + b"\x00" + data
|
|
168
|
+
digest = hashlib.sha256(blob) if len(blob_id) == 64 else hashlib.sha1(blob)
|
|
169
|
+
if digest.hexdigest() != blob_id:
|
|
170
|
+
raise ValueError("Git blob integrity check failed")
|
|
171
|
+
files.append((name, data, 0o755 if mode == b"100755" else 0o644))
|
|
172
|
+
return files
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def prepare(project: Path, pack_file: Path, *, git: str | None = None) -> dict:
|
|
176
|
+
project = metrics._target(project)
|
|
177
|
+
pack_file = pack_file.absolute()
|
|
178
|
+
pack_file = install.safe_path(metrics._target(pack_file.parent), pack_file.name)
|
|
179
|
+
pack = validate_pack(json.loads(pack_file.read_text(encoding="utf-8")), project)
|
|
180
|
+
task_fingerprints = set()
|
|
181
|
+
for task in pack["tasks"]:
|
|
182
|
+
for field in ("prompt_file", "rubric_file"):
|
|
183
|
+
path = install.safe_path(pack_file.parent, task[field])
|
|
184
|
+
if path.stat().st_size > 1_000_000:
|
|
185
|
+
raise ValueError("Task material exceeds 1 MB")
|
|
186
|
+
task[field.replace("_file", "")] = path.read_text(encoding="utf-8")
|
|
187
|
+
material = {k: task[k] for k in ("prompt", "rubric", "path", "stack", "operation", "risks", "capabilities")}
|
|
188
|
+
material.update(risks=sorted(task["risks"]), capabilities=sorted(task["capabilities"]), path=task["path"].rstrip("/"))
|
|
189
|
+
fingerprint = install.digest(json.dumps(material, sort_keys=True).encode())
|
|
190
|
+
if fingerprint in task_fingerprints:
|
|
191
|
+
raise ValueError("Duplicate task material cannot count as a distinct case; use repeats")
|
|
192
|
+
task_fingerprints.add(fingerprint)
|
|
193
|
+
files = snapshot_files(project, pack["revision"], git=git)
|
|
194
|
+
selection, providers = router._selection_and_providers(project, ROOT)
|
|
195
|
+
config = adaptive.load_config(project)
|
|
196
|
+
records = metrics.read_records(project)
|
|
197
|
+
units = []
|
|
198
|
+
for repetition in range(1, pack["repeats"] + 1):
|
|
199
|
+
for task in pack["tasks"]:
|
|
200
|
+
# Rotate all arms together to reduce systematic order/cache effects.
|
|
201
|
+
order = ARMS[(repetition - 1) % 3:] + ARMS[:(repetition - 1) % 3]
|
|
202
|
+
for arm in order:
|
|
203
|
+
route = dict(pack["fixed"])
|
|
204
|
+
if arm == "kit-adaptive":
|
|
205
|
+
recommendation = adaptive.recommend_route(task["prompt"], selection, providers, config=config,
|
|
206
|
+
records=records, mode="adaptive", task_id=task["task_id"], stack=task["stack"],
|
|
207
|
+
operation=task["operation"], risks=tuple(task["risks"]), capabilities=task["capabilities"])
|
|
208
|
+
if recommendation["resolved"]:
|
|
209
|
+
route = {key: recommendation[key] for key in ("provider", "model", "effort")}
|
|
210
|
+
unit_id = f"unit-{len(units) + 1:06d}"
|
|
211
|
+
units.append({"unit_id": unit_id, "task_id": task["task_id"] + f":r{repetition}",
|
|
212
|
+
"case_id": task["task_id"], "arm": arm, "repetition": repetition,
|
|
213
|
+
"route": route})
|
|
214
|
+
relative = "ai-kit/.metrics/benchmarks/" + pack["experiment"]
|
|
215
|
+
install.safe_path(project, relative + "/plan.json")
|
|
216
|
+
return {"project": project, "relative": relative, "pack": pack, "files": files, "units": units,
|
|
217
|
+
"context_budget": config["context_bytes"][config["profile"]]}
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def clean_environment(workspace: Path) -> dict[str, str]:
|
|
221
|
+
# Do not inherit credentials, DB URLs, production service configuration or user home files.
|
|
222
|
+
environment = {key: os.environ[key] for key in ("PATH", "SystemRoot", "WINDIR", "PATHEXT") if key in os.environ}
|
|
223
|
+
temporary = install.safe_path(workspace, ".benchmark-tmp/probe").parent
|
|
224
|
+
temporary.mkdir(exist_ok=True)
|
|
225
|
+
# Git applies ceilings above cwd; naming cwd itself does not stop parent discovery.
|
|
226
|
+
environment.update({"HOME": str(temporary), "USERPROFILE": str(temporary), "TMP": str(temporary),
|
|
227
|
+
"TEMP": str(temporary), "TMPDIR": str(temporary), "AIKIT_BENCHMARK": "1",
|
|
228
|
+
"GIT_CEILING_DIRECTORIES": str(workspace.parent)})
|
|
229
|
+
return environment
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def invoke(command: list[str], workspace: Path, seconds: int, *, request: dict | None = None) -> tuple[int | None, str]:
|
|
233
|
+
try:
|
|
234
|
+
completed = subprocess.run(command, cwd=workspace, env=clean_environment(workspace),
|
|
235
|
+
input=json.dumps(request) if request is not None else None, capture_output=True, text=True,
|
|
236
|
+
encoding="utf-8", timeout=seconds, shell=False)
|
|
237
|
+
return completed.returncode, completed.stdout if request is not None else ""
|
|
238
|
+
except (OSError, subprocess.TimeoutExpired, UnicodeError):
|
|
239
|
+
return None, ""
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def run_unit(plan: dict, manifest: dict, run: Path, unit: dict, tasks: dict) -> dict:
|
|
243
|
+
"""Execute one unit or raise; callers record infrastructure failures per unit."""
|
|
244
|
+
workspace = install.safe_path(run, unit["unit_id"] + "/workspace/probe").parent
|
|
245
|
+
workspace.mkdir(parents=True)
|
|
246
|
+
for name, data, file_mode in plan["files"]:
|
|
247
|
+
destination = install.safe_path(workspace, name)
|
|
248
|
+
install.atomic_write(destination, data)
|
|
249
|
+
destination.chmod(file_mode)
|
|
250
|
+
task = tasks[unit["case_id"]]
|
|
251
|
+
pack = plan["pack"]
|
|
252
|
+
context_report = {"selected": [], "total_bytes": 0}
|
|
253
|
+
if unit["arm"] != "baseline":
|
|
254
|
+
install_plan = install.build_plan(workspace, root=ROOT, agents=pack["kit_agents"],
|
|
255
|
+
preset=pack["kit_preset"])
|
|
256
|
+
if install_plan["conflicts"] or not install.apply_plan(install_plan):
|
|
257
|
+
raise ValueError("Kit installation conflict inside benchmark workspace")
|
|
258
|
+
context_report = context.select_context(workspace, path=task["path"],
|
|
259
|
+
budget=plan["context_budget"])
|
|
260
|
+
if task["stack"] != "unknown" and task["stack"] not in context_report["profiles"]:
|
|
261
|
+
raise ValueError("Task stack is not evidenced by its module; confirm the test fixture")
|
|
262
|
+
request = {"schema": 1, "protocol": "aikit-worker-v1", "task_id": unit["task_id"],
|
|
263
|
+
"arm": unit["arm"], "revision": pack["revision"], "workspace": str(workspace),
|
|
264
|
+
"prompt": task["prompt"], "rubric": task["rubric"], "requested": unit["route"],
|
|
265
|
+
"context": context_report,
|
|
266
|
+
"constraints": {"disposable_services_only": True, "no_commit_push": True,
|
|
267
|
+
"credentials_external": True}}
|
|
268
|
+
start = time.monotonic()
|
|
269
|
+
status, output = invoke(pack["worker"]["argv"], workspace, pack["timeout_seconds"], request=request)
|
|
270
|
+
wall = time.monotonic() - start
|
|
271
|
+
attempts, rework, escalations = [{"disposition": "inconclusive"}], None, None
|
|
272
|
+
telemetry_valid = False
|
|
273
|
+
if status == 0:
|
|
274
|
+
try:
|
|
275
|
+
response = json.loads(output)
|
|
276
|
+
if (not isinstance(response, dict) or set(response) != {"schema", "attempts", "rework", "escalations"} or
|
|
277
|
+
type(response["schema"]) is not int or response["schema"] != 1):
|
|
278
|
+
raise ValueError("Invalid worker response")
|
|
279
|
+
attempts, rework, escalations = response["attempts"], response["rework"], response["escalations"]
|
|
280
|
+
if isinstance(attempts, list) and len(attempts) == 1 and attempts[0].get("duration_seconds") is None:
|
|
281
|
+
attempts[0]["duration_seconds"] = wall
|
|
282
|
+
telemetry_valid = True
|
|
283
|
+
except (ValueError, TypeError, AttributeError):
|
|
284
|
+
attempts, rework, escalations = [{"disposition": "inconclusive"}], None, None
|
|
285
|
+
checks = "not-run"
|
|
286
|
+
if pack["checks"]:
|
|
287
|
+
check_results = [invoke(cmd, workspace, pack["timeout_seconds"])[0] for cmd in pack["checks"]]
|
|
288
|
+
checks = "passed" if all(code == 0 for code in check_results) else "failed"
|
|
289
|
+
record = {"schema": 3, "task_id": unit["task_id"], "experiment": pack["experiment"],
|
|
290
|
+
"arm": unit["arm"], "kit_version": manifest["kit_version"], "outcome": "incomplete",
|
|
291
|
+
"checks": checks, "attempts": attempts, "rework": rework, "escalations": escalations,
|
|
292
|
+
"metadata": {"case_id": unit["case_id"], "stack": task["stack"], "task_class": task["operation"],
|
|
293
|
+
"level": router.classify_task(task["prompt"], operation=task["operation"],
|
|
294
|
+
risks=tuple(task["risks"]))["level"],
|
|
295
|
+
"revision": pack["revision"], "repetition": unit["repetition"],
|
|
296
|
+
"review": "pending", "capabilities": task["capabilities"]}}
|
|
297
|
+
try:
|
|
298
|
+
record = metrics.validate_record(record)
|
|
299
|
+
except (ValueError, TypeError):
|
|
300
|
+
record.update(attempts=[{"disposition": "inconclusive"}], rework=None, escalations=None)
|
|
301
|
+
record = metrics.validate_record(record)
|
|
302
|
+
telemetry_valid = False
|
|
303
|
+
matches = telemetry_valid and all(all(a.get(k) == unit["route"][k] for k in ("provider", "model", "effort"))
|
|
304
|
+
for a in record["attempts"])
|
|
305
|
+
return {"unit_id": unit["unit_id"], "record": record, "worker_exit_code": status,
|
|
306
|
+
"worker_wall_seconds": wall, "telemetry_valid": telemetry_valid,
|
|
307
|
+
"requested": unit["route"], "observed_matches_request": matches,
|
|
308
|
+
"context_bytes": context_report["total_bytes"]}
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def failure_result(manifest: dict, unit: dict, task: dict, message: str) -> dict:
|
|
312
|
+
"""Record an infrastructure failure without hiding it as an executed attempt."""
|
|
313
|
+
return {"unit_id": unit["unit_id"],
|
|
314
|
+
"record": {"schema": 3, "task_id": unit["task_id"], "experiment": manifest["experiment"],
|
|
315
|
+
"arm": unit["arm"], "kit_version": manifest["kit_version"], "outcome": "incomplete",
|
|
316
|
+
"checks": "not-run", "attempts": [{"disposition": "inconclusive"}],
|
|
317
|
+
"rework": None, "escalations": None,
|
|
318
|
+
"metadata": {"case_id": unit["case_id"], "stack": task["stack"],
|
|
319
|
+
"task_class": task["operation"],
|
|
320
|
+
"level": router.classify_task(task["prompt"], operation=task["operation"],
|
|
321
|
+
risks=tuple(task["risks"]))["level"],
|
|
322
|
+
"revision": manifest["revision"], "repetition": unit["repetition"],
|
|
323
|
+
"review": "pending", "capabilities": task["capabilities"]}},
|
|
324
|
+
"worker_exit_code": None, "worker_wall_seconds": None, "telemetry_valid": False,
|
|
325
|
+
"requested": unit["route"], "observed_matches_request": False,
|
|
326
|
+
"context_bytes": 0, "error": message}
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def execute(plan: dict) -> Path:
|
|
330
|
+
project, pack = plan["project"], plan["pack"]
|
|
331
|
+
run = install.safe_path(project, plan["relative"] + "/plan.json").parent
|
|
332
|
+
identity = install.digest(json.dumps({"experiment": pack["experiment"], "revision": pack["revision"],
|
|
333
|
+
"pack": pack, "units": plan["units"]}, sort_keys=True).encode())
|
|
334
|
+
manifest_path = install.safe_path(run, "plan.json")
|
|
335
|
+
if run.exists():
|
|
336
|
+
if not manifest_path.is_file():
|
|
337
|
+
raise ValueError("Interrupted run directory without a manifest; preserve it and use a new experiment ID")
|
|
338
|
+
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
|
|
339
|
+
if manifest.get("identity_sha256") != identity:
|
|
340
|
+
raise ValueError("Existing run has a different plan; preserve it and use a new experiment ID")
|
|
341
|
+
else:
|
|
342
|
+
run.parent.mkdir(parents=True, exist_ok=True)
|
|
343
|
+
run.mkdir() # Exclusive ownership: never remove a workspace.
|
|
344
|
+
manifest = {"schema": 1, "experiment": pack["experiment"], "revision": pack["revision"],
|
|
345
|
+
"kit_version": (ROOT / "VERSION").read_text(encoding="utf-8").strip(),
|
|
346
|
+
"pack_sha256": install.digest(json.dumps(pack, sort_keys=True).encode()),
|
|
347
|
+
"identity_sha256": identity,
|
|
348
|
+
"units": plan["units"], "complete": False}
|
|
349
|
+
install.atomic_write(manifest_path, json_bytes(manifest))
|
|
350
|
+
tasks = {task["task_id"]: task for task in pack["tasks"]}
|
|
351
|
+
finished = {path.parent.name for path in run.glob("unit-*/result.json")}
|
|
352
|
+
for unit in plan["units"]:
|
|
353
|
+
if unit["unit_id"] in finished:
|
|
354
|
+
continue # Resume: never re-run paid work or rebuild an existing workspace.
|
|
355
|
+
try:
|
|
356
|
+
result = run_unit(plan, manifest, run, unit, tasks)
|
|
357
|
+
except (OSError, ValueError) as exc:
|
|
358
|
+
result = failure_result(manifest, unit, tasks[unit["case_id"]], str(exc))
|
|
359
|
+
install.atomic_write(install.safe_path(run, unit["unit_id"] + "/result.json"), json_bytes(result))
|
|
360
|
+
manifest["complete"] = True
|
|
361
|
+
install.atomic_write(manifest_path, json_bytes(manifest))
|
|
362
|
+
write_report(run)
|
|
363
|
+
return run
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def json_bytes(value: object) -> bytes:
|
|
367
|
+
return (json.dumps(value, indent=2, allow_nan=False) + "\n").encode("utf-8")
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def report(run: Path) -> dict:
|
|
371
|
+
run = metrics._target(run)
|
|
372
|
+
manifest = json.loads(install.safe_path(run, "plan.json").read_text(encoding="utf-8"))
|
|
373
|
+
results = []
|
|
374
|
+
for unit in manifest["units"]:
|
|
375
|
+
if not re.fullmatch(r"unit-\d{6}", unit["unit_id"]):
|
|
376
|
+
raise ValueError("Invalid run unit ID")
|
|
377
|
+
path = install.safe_path(run, unit["unit_id"] + "/result.json")
|
|
378
|
+
if path.exists():
|
|
379
|
+
value = json.loads(path.read_text(encoding="utf-8"))
|
|
380
|
+
record = metrics.validate_record(value["record"])
|
|
381
|
+
if (value["unit_id"] != unit["unit_id"] or record["experiment"] != manifest["experiment"] or
|
|
382
|
+
record["task_id"] != unit["task_id"] or record["arm"] != unit["arm"] or
|
|
383
|
+
record.get("metadata", {}).get("revision") != manifest["revision"] or
|
|
384
|
+
record["metadata"]["case_id"] != unit["case_id"] or
|
|
385
|
+
record["metadata"]["repetition"] != unit["repetition"]):
|
|
386
|
+
raise ValueError("Result does not match its planned identity/starting state")
|
|
387
|
+
adaptive.validate_pin(unit["route"])
|
|
388
|
+
value["observed_matches_request"] = all(all(a.get(k) == unit["route"][k] for k in ("provider", "model", "effort"))
|
|
389
|
+
for a in record["attempts"])
|
|
390
|
+
results.append(value)
|
|
391
|
+
records = [r["record"] for r in results]
|
|
392
|
+
comparisons = [learning.compare(records, manifest["experiment"], candidate=arm) for arm in ARMS[1:]]
|
|
393
|
+
invalid = not manifest["complete"] or len(results) != len(manifest["units"]) or any(
|
|
394
|
+
not r["telemetry_valid"] or r["worker_exit_code"] != 0 or not r["observed_matches_request"] for r in results)
|
|
395
|
+
if invalid:
|
|
396
|
+
for comparison in comparisons:
|
|
397
|
+
comparison["savings_percent"] = None
|
|
398
|
+
comparison["unknown_reasons"].append("Incomplete run or unconfirmed actual execution")
|
|
399
|
+
return {"schema": 1, "experiment": manifest["experiment"], "revision": manifest["revision"],
|
|
400
|
+
"complete": manifest["complete"] and len(results) == len(manifest["units"]),
|
|
401
|
+
"expected_units": len(manifest["units"]), "observed_units": len(results),
|
|
402
|
+
"results": results, "summary": metrics.summarize(records), "stacks": learning.by_stack(records),
|
|
403
|
+
"comparisons": comparisons,
|
|
404
|
+
"note": "Worker-reported usage is evidence, not proof of client control. Semantic review is separate from checks."}
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
def write_report(run: Path) -> dict:
|
|
408
|
+
value = report(run)
|
|
409
|
+
lines = ["# Benchmark report", "", "Experiment: " + value["experiment"], "", "Revision: " + value["revision"], ""]
|
|
410
|
+
for item in value["comparisons"]:
|
|
411
|
+
lines.extend(["| Comparison | Paired tasks | Correct baseline/candidate | Savings (%) |", "| --- | --- | --- | --- |"])
|
|
412
|
+
lines.append(f'| {item["candidate"]} | {item["paired_tasks"]} | {item["baseline_correct"]}/{item["candidate_correct"]} | {item["savings_percent"] or "unknown"} |')
|
|
413
|
+
lines.extend(["", "Unknown reasons: " + ("; ".join(item["unknown_reasons"]) or "none"), ""])
|
|
414
|
+
lines.extend([value["note"], "", "All attempt costs, including failures, review and recovery, must be included.", ""])
|
|
415
|
+
install.atomic_write(install.safe_path(run, "results.json"), json_bytes(value))
|
|
416
|
+
install.atomic_write(install.safe_path(run, "report.md"), "\n".join(lines).encode("utf-8"))
|
|
417
|
+
return value
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
def review(run: Path, review_file: Path, *, apply: bool = False) -> dict:
|
|
421
|
+
run = metrics._target(run)
|
|
422
|
+
value = metrics._fields(json.loads(review_file.read_text(encoding="utf-8")), {"schema", "reviews"}, "reviews")
|
|
423
|
+
if type(value.get("schema")) is not int or value["schema"] != 1 or not isinstance(value.get("reviews"), list) or not value["reviews"]:
|
|
424
|
+
raise ValueError("Expected schema 1 with reviews")
|
|
425
|
+
current_report = report(run)
|
|
426
|
+
observed = {(r["record"]["arm"], r["record"]["task_id"]): r for r in current_report["results"]}
|
|
427
|
+
changes, seen = [], set()
|
|
428
|
+
for item in value["reviews"]:
|
|
429
|
+
if not isinstance(item, dict) or set(item) != {"task_id", "arm", "review", "outcome"}:
|
|
430
|
+
raise ValueError("Invalid review item")
|
|
431
|
+
identity = (item["arm"], item["task_id"])
|
|
432
|
+
if identity not in observed or identity in seen or item["review"] not in {"manual", "independent"}:
|
|
433
|
+
raise ValueError("Unknown/duplicate review identity or reviewer type")
|
|
434
|
+
seen.add(identity)
|
|
435
|
+
result = copy.deepcopy(observed[identity])
|
|
436
|
+
if item["outcome"] == "correct" and (result["worker_exit_code"] != 0 or not result["telemetry_valid"]):
|
|
437
|
+
raise ValueError("Cannot accept a failed or malformed worker execution")
|
|
438
|
+
record = result["record"]
|
|
439
|
+
record["outcome"], record["metadata"]["review"] = item["outcome"], item["review"]
|
|
440
|
+
metrics.validate_record(record)
|
|
441
|
+
path = install.safe_path(run, result["unit_id"] + "/result.json")
|
|
442
|
+
changes.append((path, path.read_bytes(), json_bytes(result)))
|
|
443
|
+
if apply:
|
|
444
|
+
lock = install.safe_path(run, "review.lock")
|
|
445
|
+
descriptor = os.open(lock, os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o600)
|
|
446
|
+
try:
|
|
447
|
+
os.close(descriptor)
|
|
448
|
+
if any(path.read_bytes() != old for path, old, _ in changes):
|
|
449
|
+
raise ValueError("Results changed during review")
|
|
450
|
+
for path, old, new in changes:
|
|
451
|
+
backup = install.safe_path(run, "reviews/" + install.digest(old) + ".json")
|
|
452
|
+
if backup.exists() and backup.read_bytes() != old:
|
|
453
|
+
raise ValueError("Review backup conflict")
|
|
454
|
+
if not backup.exists():
|
|
455
|
+
install.atomic_write(backup, old)
|
|
456
|
+
install.atomic_write(path, new)
|
|
457
|
+
write_report(run)
|
|
458
|
+
finally:
|
|
459
|
+
lock.unlink()
|
|
460
|
+
return {"preview": not apply, "reviewed_units": len(changes)}
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
def export_records(run: Path, project: Path, *, apply: bool = False) -> dict:
|
|
464
|
+
value = report(run)
|
|
465
|
+
if not value["complete"] or any(r["record"]["metadata"]["review"] == "pending" for r in value["results"]):
|
|
466
|
+
raise ValueError("Finish execution and semantic review before exporting")
|
|
467
|
+
project = metrics._target(project)
|
|
468
|
+
records = [r["record"] for r in value["results"]]
|
|
469
|
+
|
|
470
|
+
def merged() -> tuple[list[dict], int]:
|
|
471
|
+
current = metrics.read_records(project)
|
|
472
|
+
existing = {(r["experiment"], r["arm"], r["task_id"]): r for r in current}
|
|
473
|
+
new = []
|
|
474
|
+
for record in records:
|
|
475
|
+
key = (record["experiment"], record["arm"], record["task_id"])
|
|
476
|
+
if key in existing and existing[key] != record:
|
|
477
|
+
raise ValueError("Export conflicts with reviewed journal data; preserve it")
|
|
478
|
+
if key not in existing:
|
|
479
|
+
new.append(record)
|
|
480
|
+
return [*current, *new], len(new)
|
|
481
|
+
|
|
482
|
+
rows, count = merged()
|
|
483
|
+
if apply and count:
|
|
484
|
+
lock = install.safe_path(project, metrics.LOCK)
|
|
485
|
+
lock.parent.mkdir(parents=True, exist_ok=True)
|
|
486
|
+
descriptor = os.open(lock, os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o600)
|
|
487
|
+
try:
|
|
488
|
+
os.close(descriptor)
|
|
489
|
+
rows, count = merged()
|
|
490
|
+
install.atomic_write(install.safe_path(project, metrics.JOURNAL),
|
|
491
|
+
"".join(json.dumps(r, allow_nan=False) + "\n" for r in rows).encode())
|
|
492
|
+
finally:
|
|
493
|
+
lock.unlink()
|
|
494
|
+
return {"preview": not apply, "new_records": count}
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
def main(arguments: list[str] | None = None) -> int:
|
|
498
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
499
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
500
|
+
run_parser = sub.add_parser("run", help="Preview or execute a reviewed worker in isolated snapshots")
|
|
501
|
+
run_parser.add_argument("project", type=Path)
|
|
502
|
+
run_parser.add_argument("--pack", type=Path, required=True)
|
|
503
|
+
run_parser.add_argument("--apply", action="store_true")
|
|
504
|
+
report_parser = sub.add_parser("report", help="Read run coverage, metrics and savings without writing")
|
|
505
|
+
report_parser.add_argument("run", type=Path)
|
|
506
|
+
review_parser = sub.add_parser("review", help="Preview/apply explicit semantic acceptance and refresh reports")
|
|
507
|
+
review_parser.add_argument("run", type=Path)
|
|
508
|
+
review_parser.add_argument("--from-json", type=Path, required=True)
|
|
509
|
+
review_parser.add_argument("--apply", action="store_true")
|
|
510
|
+
export_parser = sub.add_parser("export", help="Preview/export reviewed records to the private journal")
|
|
511
|
+
export_parser.add_argument("run", type=Path)
|
|
512
|
+
export_parser.add_argument("project", type=Path)
|
|
513
|
+
export_parser.add_argument("--apply", action="store_true")
|
|
514
|
+
args = parser.parse_args(arguments)
|
|
515
|
+
try:
|
|
516
|
+
if args.command == "run":
|
|
517
|
+
plan = prepare(args.project, args.pack)
|
|
518
|
+
run_dir = plan["project"] / plan["relative"]
|
|
519
|
+
finished = ({path.parent.name for path in run_dir.glob("unit-*/result.json")}
|
|
520
|
+
if run_dir.is_dir() else set())
|
|
521
|
+
value = {"preview": not args.apply, "experiment": plan["pack"]["experiment"],
|
|
522
|
+
"revision": plan["pack"]["revision"], "units": plan["units"],
|
|
523
|
+
"tracked_files": len(plan["files"]), "run": str(run_dir),
|
|
524
|
+
"existing_run": run_dir.is_dir(),
|
|
525
|
+
"remaining_units": sum(1 for unit in plan["units"]
|
|
526
|
+
if unit["unit_id"] not in finished)}
|
|
527
|
+
if args.apply:
|
|
528
|
+
execute(plan)
|
|
529
|
+
elif args.command == "review":
|
|
530
|
+
value = review(args.run, args.from_json, apply=args.apply)
|
|
531
|
+
elif args.command == "export":
|
|
532
|
+
value = export_records(args.run, args.project, apply=args.apply)
|
|
533
|
+
else:
|
|
534
|
+
value = report(args.run)
|
|
535
|
+
print(json.dumps(value, indent=2, allow_nan=False))
|
|
536
|
+
return 0
|
|
537
|
+
except (OSError, ValueError, TypeError, KeyError, UnicodeError) as exc:
|
|
538
|
+
print(f"Benchmark refused: {exc}", file=sys.stderr)
|
|
539
|
+
return 1
|
|
540
|
+
|
|
541
|
+
|
|
542
|
+
if __name__ == "__main__":
|
|
543
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""Append a changelog entry to a project's CHANGELOG.md."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
HEADING = "## Unreleased"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def add_entry(root: Path, message: str) -> Path:
|
|
12
|
+
path = root / "CHANGELOG.md"
|
|
13
|
+
if not path.is_file():
|
|
14
|
+
raise ValueError(f"Missing changelog: {path}")
|
|
15
|
+
text = path.read_text(encoding="utf-8")
|
|
16
|
+
bullet = f"- {message.strip()}\n"
|
|
17
|
+
index = text.find(HEADING)
|
|
18
|
+
if index == -1:
|
|
19
|
+
lines = text.splitlines(keepends=True)
|
|
20
|
+
insert_at = 0
|
|
21
|
+
for position, line in enumerate(lines):
|
|
22
|
+
if line.startswith("# "):
|
|
23
|
+
insert_at = position + 1
|
|
24
|
+
break
|
|
25
|
+
lines[insert_at:insert_at] = ["\n", HEADING, "\n", bullet]
|
|
26
|
+
path.write_text("".join(lines), encoding="utf-8")
|
|
27
|
+
return path
|
|
28
|
+
after = index + len(HEADING)
|
|
29
|
+
if text[after:after + 2] == "\r\n":
|
|
30
|
+
after += 2
|
|
31
|
+
elif text[after:after + 1] == "\n":
|
|
32
|
+
after += 1
|
|
33
|
+
path.write_text(text[:after] + bullet + text[after:], encoding="utf-8")
|
|
34
|
+
return path
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def main(argv: list[str] | None = None) -> int:
|
|
38
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
39
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
40
|
+
add_parser = sub.add_parser("add", help="Append a bullet under the top changelog section")
|
|
41
|
+
add_parser.add_argument("message")
|
|
42
|
+
add_parser.add_argument("--root", type=Path, default=Path.cwd(),
|
|
43
|
+
help="Project root (default: current directory)")
|
|
44
|
+
args = parser.parse_args(argv)
|
|
45
|
+
try:
|
|
46
|
+
path = add_entry(args.root.absolute(), args.message)
|
|
47
|
+
except (OSError, ValueError, UnicodeError) as exc:
|
|
48
|
+
print(f"Changelog update refused: {exc}", file=sys.stderr)
|
|
49
|
+
return 1
|
|
50
|
+
print(str(path))
|
|
51
|
+
return 0
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
if __name__ == "__main__":
|
|
55
|
+
raise SystemExit(main())
|