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
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
"""Validate local release archives and manual publication identity without uploading."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import configparser
|
|
6
|
+
from email import policy
|
|
7
|
+
from email.parser import BytesParser
|
|
8
|
+
import hashlib
|
|
9
|
+
import json
|
|
10
|
+
from pathlib import Path, PurePosixPath
|
|
11
|
+
import re
|
|
12
|
+
import stat
|
|
13
|
+
import tarfile
|
|
14
|
+
import zipfile
|
|
15
|
+
|
|
16
|
+
ROOT = Path(__file__).resolve().parents[1]
|
|
17
|
+
PROJECT_NAME = "ai-kit-python"
|
|
18
|
+
LICENSES = ("LICENSE", "template/ai-kit/vendor/security-audit/LICENSE",
|
|
19
|
+
"template/ai-kit/vendor/strix/LICENSE")
|
|
20
|
+
LOCAL_FILES = {"AGENTS.md", "PROJECT_CONTEXT.md", "WIKI.md"}
|
|
21
|
+
PRIVATE_PARTS = {".git", ".aws", ".codex", ".claude", ".secrets", ".metrics", ".memory",
|
|
22
|
+
".upstream-cache", ".design", ".private", "__pycache__"}
|
|
23
|
+
REQUIRED = {"VERSION", "LICENSE", "aikit_cli.py", "scripts/install.py", "scripts/check_distribution.py",
|
|
24
|
+
"template/AGENTS.md", "template/PROJECT_CONTEXT.md", "template/WIKI.md",
|
|
25
|
+
"template/ai-kit/settings.json", "docs/PYPI_DESCRIPTION.md", "docs/PYPI.md",
|
|
26
|
+
"docker/pypi-requirements.txt", ".github/workflows/publish-pypi.yml", "tests/build_distribution.py", *LICENSES}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def release_identity(version: str, expected: str, destination: str, ref: str, repository: str) -> dict:
|
|
30
|
+
"""Require an explicit matching version/tag before either registry upload."""
|
|
31
|
+
if destination not in {"validate", "testpypi", "pypi"}:
|
|
32
|
+
raise ValueError("Unknown publication destination")
|
|
33
|
+
if not re.fullmatch(r"[0-9]+\.[0-9]+\.[0-9]+(?:(?:a|b|rc)[0-9]+)?", version):
|
|
34
|
+
raise ValueError("VERSION must be a three-part release or pre-release")
|
|
35
|
+
if expected and expected != version:
|
|
36
|
+
raise ValueError("Requested version does not match VERSION")
|
|
37
|
+
if destination != "validate":
|
|
38
|
+
if expected != version or ref != "refs/tags/v" + version:
|
|
39
|
+
raise ValueError("Publication requires an explicit matching version and vVERSION tag")
|
|
40
|
+
if repository != "tigusigalpa/ai-kit":
|
|
41
|
+
raise ValueError("Publication is restricted to the canonical repository")
|
|
42
|
+
return {"name": PROJECT_NAME, "version": version, "destination": destination,
|
|
43
|
+
"upload_performed": False}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _members(path: Path) -> dict[str, bytes]:
|
|
47
|
+
result: dict[str, bytes] = {}
|
|
48
|
+
total = 0
|
|
49
|
+
|
|
50
|
+
def add(name: str, size: int, read) -> None:
|
|
51
|
+
nonlocal total
|
|
52
|
+
parts = PurePosixPath(name).parts
|
|
53
|
+
if not parts or "\\" in name or name.startswith("/") or ".." in parts or ":" in parts[0]:
|
|
54
|
+
raise ValueError("Unsafe archive member path")
|
|
55
|
+
if name in result:
|
|
56
|
+
raise ValueError("Duplicate archive member")
|
|
57
|
+
total += size
|
|
58
|
+
if size > 32_000_000 or total > 128_000_000 or len(result) >= 5000:
|
|
59
|
+
raise ValueError("Archive exceeds inspection limits")
|
|
60
|
+
result[name] = read()
|
|
61
|
+
|
|
62
|
+
if path.suffix == ".whl":
|
|
63
|
+
with zipfile.ZipFile(path) as archive:
|
|
64
|
+
for item in archive.infolist():
|
|
65
|
+
if stat.S_ISLNK(item.external_attr >> 16):
|
|
66
|
+
raise ValueError("Linked archive member")
|
|
67
|
+
if not item.is_dir():
|
|
68
|
+
add(item.filename, item.file_size, lambda item=item: archive.read(item))
|
|
69
|
+
else:
|
|
70
|
+
with tarfile.open(path, "r:gz") as archive:
|
|
71
|
+
for item in archive.getmembers():
|
|
72
|
+
if item.isdir():
|
|
73
|
+
continue
|
|
74
|
+
if not item.isfile():
|
|
75
|
+
raise ValueError("Linked or special archive member")
|
|
76
|
+
def read(item=item):
|
|
77
|
+
with archive.extractfile(item) as stream:
|
|
78
|
+
return stream.read()
|
|
79
|
+
add(item.name, item.size, read)
|
|
80
|
+
return result
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def inspect_distribution(path: Path, version: str) -> dict:
|
|
84
|
+
"""Check metadata, CLI/resources, licenses and private-file boundaries in one archive."""
|
|
85
|
+
if path.is_symlink() or not path.is_file():
|
|
86
|
+
raise ValueError("Distribution must be a regular unlinked file")
|
|
87
|
+
files = _members(path)
|
|
88
|
+
wheel = path.suffix == ".whl"
|
|
89
|
+
if wheel:
|
|
90
|
+
metadata_paths = [name for name in files if re.fullmatch(r"[^/]+\.dist-info/METADATA", name)]
|
|
91
|
+
if len(metadata_paths) != 1:
|
|
92
|
+
raise ValueError("Wheel must contain exactly one metadata record")
|
|
93
|
+
info = metadata_paths[0].rsplit("/", 1)[0]
|
|
94
|
+
payload = {name[6:]: data for name, data in files.items() if name.startswith("aikit/")}
|
|
95
|
+
metadata_bytes = files[metadata_paths[0]]
|
|
96
|
+
parser = configparser.ConfigParser(interpolation=None)
|
|
97
|
+
parser.read_string(files.get(info + "/entry_points.txt", b"").decode("utf-8"))
|
|
98
|
+
if parser.get("console_scripts", "aikit", fallback="") != "aikit.aikit_cli:main":
|
|
99
|
+
raise ValueError("Missing aikit console entry point")
|
|
100
|
+
wheel_metadata = files.get(info + "/WHEEL", b"")
|
|
101
|
+
if b"Root-Is-Purelib: true" not in wheel_metadata or b"Tag: py3-none-any" not in wheel_metadata:
|
|
102
|
+
raise ValueError("Expected a portable pure-Python wheel")
|
|
103
|
+
else:
|
|
104
|
+
roots = {name.split("/", 1)[0] for name in files}
|
|
105
|
+
if len(roots) != 1:
|
|
106
|
+
raise ValueError("Source archive must have one root")
|
|
107
|
+
prefix = next(iter(roots)) + "/"
|
|
108
|
+
payload = {name[len(prefix):]: data for name, data in files.items()}
|
|
109
|
+
metadata_bytes = payload.get("PKG-INFO", b"")
|
|
110
|
+
metadata = BytesParser(policy=policy.default).parsebytes(metadata_bytes)
|
|
111
|
+
if metadata.get("Name") != PROJECT_NAME or metadata.get("Version") != version:
|
|
112
|
+
raise ValueError("Archive name/version metadata does not match the release")
|
|
113
|
+
if metadata.get("Requires-Python") != ">=3.10" or metadata.get_all("Requires-Dist"):
|
|
114
|
+
raise ValueError("Unexpected runtime Python/dependency contract")
|
|
115
|
+
if metadata.get("License-Expression") != "MIT AND Apache-2.0":
|
|
116
|
+
raise ValueError("Missing complete distribution license expression")
|
|
117
|
+
if set(metadata.get_all("License-File", [])) != set(LICENSES):
|
|
118
|
+
raise ValueError("Missing bundled license declarations")
|
|
119
|
+
if metadata.get("Description-Content-Type") != "text/markdown":
|
|
120
|
+
raise ValueError("Missing Markdown description content type")
|
|
121
|
+
if not metadata.get("Author") or len(metadata.get_all("Project-URL", [])) < 5:
|
|
122
|
+
raise ValueError("Missing author or project URLs")
|
|
123
|
+
if not REQUIRED.issubset(payload) or payload["VERSION"].decode().strip() != version:
|
|
124
|
+
raise ValueError("Missing required distribution resources or bundled VERSION")
|
|
125
|
+
for name in payload:
|
|
126
|
+
parts = PurePosixPath(name).parts
|
|
127
|
+
if name in LOCAL_FILES or PRIVATE_PARTS.intersection(parts) or any(p.startswith(".env") for p in parts):
|
|
128
|
+
raise ValueError("Private local content included in distribution")
|
|
129
|
+
if (parts[-1] in {"auth.json", ".netrc", ".pypirc"}
|
|
130
|
+
or parts[-1].endswith((".pem", ".p12", ".pfx"))
|
|
131
|
+
or parts[-1].startswith(("id_rsa", "id_ed25519"))):
|
|
132
|
+
raise ValueError("Credential file included in distribution")
|
|
133
|
+
description = metadata.get_payload().replace("\r\n", "\n").strip()
|
|
134
|
+
if description != payload["docs/PYPI_DESCRIPTION.md"].decode("utf-8").replace("\r\n", "\n").strip():
|
|
135
|
+
raise ValueError("Package description differs from its canonical source")
|
|
136
|
+
if re.search(r"\]\((?!https://|mailto:|#)[^)]+\)", description) or "```mermaid" in description:
|
|
137
|
+
raise ValueError("PyPI description contains nonportable links or diagrams")
|
|
138
|
+
if wheel:
|
|
139
|
+
for name in LICENSES:
|
|
140
|
+
if files.get(info + "/licenses/" + name) != payload[name]:
|
|
141
|
+
raise ValueError("Wheel license payload is missing or changed")
|
|
142
|
+
return {"file": path.name, "version": version, "payload_files": len(payload),
|
|
143
|
+
"sha256": hashlib.sha256(path.read_bytes()).hexdigest()}
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def check_archives(directory: Path, version: str) -> list[dict]:
|
|
147
|
+
paths = sorted(directory.glob("*.whl")) + sorted(directory.glob("*.tar.gz"))
|
|
148
|
+
if len(paths) != 2 or sum(path.suffix == ".whl" for path in paths) != 1:
|
|
149
|
+
raise ValueError("Expected exactly one wheel and one source archive")
|
|
150
|
+
return [inspect_distribution(path, version) for path in paths]
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def main(argv: list[str] | None = None) -> int:
|
|
154
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
155
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
156
|
+
identity = sub.add_parser("identity", help="Validate a manual build/publication request; never uploads")
|
|
157
|
+
identity.add_argument("--expected-version", default="")
|
|
158
|
+
identity.add_argument("--destination", choices=("validate", "testpypi", "pypi"), default="validate")
|
|
159
|
+
identity.add_argument("--ref", default="")
|
|
160
|
+
identity.add_argument("--repository", default="")
|
|
161
|
+
archives = sub.add_parser("archives", help="Inspect a prepared wheel and sdist; never uploads")
|
|
162
|
+
archives.add_argument("directory", type=Path)
|
|
163
|
+
args = parser.parse_args(argv)
|
|
164
|
+
try:
|
|
165
|
+
version = (ROOT / "VERSION").read_text(encoding="utf-8").strip()
|
|
166
|
+
if args.command == "identity":
|
|
167
|
+
result = release_identity(version, args.expected_version, args.destination, args.ref, args.repository)
|
|
168
|
+
else:
|
|
169
|
+
result = {"archives": check_archives(args.directory, version), "upload_performed": False}
|
|
170
|
+
print(json.dumps(result, indent=2))
|
|
171
|
+
return 0
|
|
172
|
+
except (OSError, ValueError, KeyError, configparser.Error, tarfile.TarError, zipfile.BadZipFile) as exc:
|
|
173
|
+
print(json.dumps({"error": str(exc), "upload_performed": False}))
|
|
174
|
+
return 1
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
if __name__ == "__main__":
|
|
178
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
"""Offline validation of reference/template boundaries, metadata, links, and visibility."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
import re
|
|
9
|
+
import shutil
|
|
10
|
+
import subprocess
|
|
11
|
+
import sys
|
|
12
|
+
import tempfile
|
|
13
|
+
from urllib.parse import unquote
|
|
14
|
+
|
|
15
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
16
|
+
import install
|
|
17
|
+
|
|
18
|
+
ROOT = Path(__file__).resolve().parents[1]
|
|
19
|
+
# User-facing onboarding/publication copy has no size quota; instruction budgets remain scoped.
|
|
20
|
+
DOC_BUDGET = 12000
|
|
21
|
+
UNBOUNDED_DOCUMENTS = frozenset(("README.md", "docs/DOCKER_HUB_OVERVIEW.md"))
|
|
22
|
+
LOCAL_MAINTAINER_FILES = frozenset(("AGENTS.md", "PROJECT_CONTEXT.md", "WIKI.md"))
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def source_files(root: Path):
|
|
27
|
+
# IDE and tool state is local to a checkout, never a kit source.
|
|
28
|
+
excluded = {".git", "__pycache__", ".upstream-cache", ".pytest_cache", ".mypy_cache", ".ruff_cache",
|
|
29
|
+
".idea", ".vscode", ".fleet", ".zed", ".nova", ".history", ".claude"}
|
|
30
|
+
for directory, dirs, files in os.walk(root, followlinks=False):
|
|
31
|
+
dirs[:] = [d for d in dirs if d not in excluded]
|
|
32
|
+
if Path(directory) == root / "ai-kit":
|
|
33
|
+
dirs[:] = [d for d in dirs if d not in {".metrics", ".memory"}]
|
|
34
|
+
for name in files:
|
|
35
|
+
path = Path(directory) / name
|
|
36
|
+
if path.parent == root and name in LOCAL_MAINTAINER_FILES:
|
|
37
|
+
continue
|
|
38
|
+
if path.suffix != ".pyc":
|
|
39
|
+
yield path
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def headings(text: str) -> set[str]:
|
|
43
|
+
result, counts = set(), {}
|
|
44
|
+
fenced = False
|
|
45
|
+
marker = ""
|
|
46
|
+
for line in text.splitlines():
|
|
47
|
+
if re.match(r"^\s*([~]{3,}|[\x60]{3,})", line):
|
|
48
|
+
token = line.strip()[0]
|
|
49
|
+
if not fenced:
|
|
50
|
+
fenced, marker = True, token
|
|
51
|
+
elif token == marker:
|
|
52
|
+
fenced = False
|
|
53
|
+
continue
|
|
54
|
+
if fenced or not re.match(r"^#{1,6}\s", line):
|
|
55
|
+
continue
|
|
56
|
+
label = re.sub(r"^#{1,6}\s+", "", line).strip().lower()
|
|
57
|
+
label = re.sub(r"<[^>]+>", "", label)
|
|
58
|
+
label = re.sub(r"[^\w -]", "", label).replace(" ", "-")
|
|
59
|
+
count = counts.get(label, 0)
|
|
60
|
+
counts[label] = count + 1
|
|
61
|
+
result.add(label + (f"-{count}" if count else ""))
|
|
62
|
+
return result
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def metadata(text: str) -> dict[str, str]:
|
|
66
|
+
match = re.match(r"\A---\n(.*?)\n---\n", text.replace("\r\n", "\n"), re.S)
|
|
67
|
+
if not match:
|
|
68
|
+
raise ValueError("Missing frontmatter")
|
|
69
|
+
fields = {}
|
|
70
|
+
for line in match.group(1).splitlines():
|
|
71
|
+
if ":" not in line:
|
|
72
|
+
raise ValueError("Unsupported frontmatter line")
|
|
73
|
+
key, value = line.split(":", 1)
|
|
74
|
+
if key in fields:
|
|
75
|
+
raise ValueError("Duplicate frontmatter field")
|
|
76
|
+
value = value.strip()
|
|
77
|
+
fields[key] = json.loads(value) if value.startswith('"') else value
|
|
78
|
+
if not re.fullmatch(r"[a-z0-9]+(?:-[a-z0-9]+)*", fields.get("name", "")):
|
|
79
|
+
raise ValueError("Invalid skill name")
|
|
80
|
+
if not isinstance(fields.get("description"), str) or len(fields["description"]) < 20:
|
|
81
|
+
raise ValueError("Missing useful description")
|
|
82
|
+
return fields
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def links(text: str):
|
|
86
|
+
# Fenced examples are data, not Markdown links.
|
|
87
|
+
text = re.sub(r"(?ms)^\s*(?:~~~|[\x60]{3}).*?^\s*(?:~~~|[\x60]{3})\s*$", "", text)
|
|
88
|
+
return re.findall(r"!?\[[^\]]*\]\(([^)\s]+)(?:\s+\"[^\"]*\")?\)", text)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def visibility(root: Path, git: str) -> list[str]:
|
|
92
|
+
errors = []
|
|
93
|
+
with tempfile.TemporaryDirectory(prefix="ai-kit-ignore-") as folder:
|
|
94
|
+
scratch = Path(folder)
|
|
95
|
+
subprocess.run([git, "init", "-q", str(scratch)], check=True, capture_output=True)
|
|
96
|
+
|
|
97
|
+
def ignored(path: str) -> bool:
|
|
98
|
+
result = subprocess.run([git, "-c", "core.excludesFile=" + os.devnull,
|
|
99
|
+
"-C", str(scratch), "check-ignore", "--no-index", "--", path],
|
|
100
|
+
capture_output=True)
|
|
101
|
+
if result.returncode not in {0, 1}:
|
|
102
|
+
raise ValueError(result.stderr.decode(errors="replace"))
|
|
103
|
+
return result.returncode == 0
|
|
104
|
+
|
|
105
|
+
(scratch / ".gitignore").write_bytes((root / ".gitignore").read_bytes())
|
|
106
|
+
for relative in sorted(LOCAL_MAINTAINER_FILES):
|
|
107
|
+
if not ignored(relative):
|
|
108
|
+
errors.append("Local maintainer file must be ignored: " + relative)
|
|
109
|
+
for p in source_files(root):
|
|
110
|
+
relative = p.relative_to(root).as_posix()
|
|
111
|
+
if ignored(relative):
|
|
112
|
+
errors.append("Reference source ignored: " + relative)
|
|
113
|
+
# Compose the policy with installer-detected module rules, as apply does.
|
|
114
|
+
(scratch / "package.json").write_text("{}\n", encoding="utf-8")
|
|
115
|
+
(scratch / "pyproject.toml").write_text("[project]\nname = \"scratch\"\n", encoding="utf-8")
|
|
116
|
+
for mode in ("private", "team"):
|
|
117
|
+
policy = (root / "templates" / ("gitignore." + mode)).read_text(encoding="utf-8")
|
|
118
|
+
merged, _ = install.merge_ignore("", policy, install.module_ignores(scratch))
|
|
119
|
+
(scratch / ".gitignore").write_bytes(merged.encode("utf-8"))
|
|
120
|
+
for path in ("AGENTS.md", "PROJECT_CONTEXT.md", "ai-kit/CORE.md", "GEMINI.md",
|
|
121
|
+
".agents/skills/go-work/SKILL.md", ".claude/skills/go-work/SKILL.md",
|
|
122
|
+
".agents/skills/security-audit/SKILL.md",
|
|
123
|
+
".agents/skills/security-audit/agents/openai.yaml",
|
|
124
|
+
".claude/skills/security-audit/SKILL.md",
|
|
125
|
+
"ai-kit/vendor/security-audit/LICENSE", "ai-kit/vendor/security-audit/UPSTREAM.json",
|
|
126
|
+
".agents/skills/strix/SKILL.md", ".agents/skills/strix/agents/openai.yaml",
|
|
127
|
+
".claude/skills/strix/SKILL.md", "ai-kit/vendor/strix/LICENSE",
|
|
128
|
+
"ai-kit/vendor/strix/UPSTREAM.json",
|
|
129
|
+
".agents/skills/context7-docs/SKILL.md", ".claude/skills/context7-docs/SKILL.md",
|
|
130
|
+
"ai-kit/CONTEXT7.md",
|
|
131
|
+
"ai-kit/WEB-DESIGN.md", "ai-kit/web-design/LARAVEL.md",
|
|
132
|
+
".agents/skills/web-design/SKILL.md", ".claude/skills/web-design/SKILL.md",
|
|
133
|
+
".agents/skills/laravel-blade-ui/SKILL.md", ".claude/skills/laravel-blade-ui/SKILL.md",
|
|
134
|
+
".agents/skills/ui-review/SKILL.md", ".claude/skills/ui-review/SKILL.md",
|
|
135
|
+
".agents/skills/design-extract/SKILL.md", ".claude/skills/design-extract/SKILL.md",
|
|
136
|
+
".agents/hooks/session-start.md", ".agents/hooks/session-start.sh",
|
|
137
|
+
".agents/skills/ai-kit-bootstrap/SKILL.md", ".claude/rules/ai-kit-web.md",
|
|
138
|
+
".cursor/rules/ai-kit-web.mdc", ".github/instructions/ai-kit-web.instructions.md",
|
|
139
|
+
".windsurf/rules/ai-kit-web.md", ".clinerules/ai-kit-web.md",
|
|
140
|
+
".windsurf/rules/ai-kit.md", "docs/DECISIONS.md",
|
|
141
|
+
".codex/config.toml", ".cursor/mcp.json", ".gemini/settings.json", ".clinerules",
|
|
142
|
+
".github/prompts/review.prompt.md", ".aider.conf.yml"):
|
|
143
|
+
if ignored(path) != (mode == "private"):
|
|
144
|
+
errors.append(f"{mode}: wrong shared visibility: {path}")
|
|
145
|
+
for path in (".env", ".claude/settings.local.json", "ai-kit/.upstream-cache/a",
|
|
146
|
+
"ai-kit/.metrics/tasks.jsonl", "ai-kit/.metrics/write.lock", "ai-kit/.memory/session/imports/chat.json",
|
|
147
|
+
"ai-kit/.design/screenshots/home.png", "ai-kit/.design/extractions/DESIGN.md",
|
|
148
|
+
"go-cache/a", "test.out", "__pycache__/a.pyc", ".idea/workspace.xml",
|
|
149
|
+
".netrc", ".pypirc", ".aider.chat.history.md", ".aider.tags.cache.v4/a",
|
|
150
|
+
"AI-KIT-0.6.5/README.md", "web/.next/cache/a", "__debug_bin1234"):
|
|
151
|
+
if not ignored(path):
|
|
152
|
+
errors.append(f"{mode}: private/generated data visible: {path}")
|
|
153
|
+
for path in ("composer.lock", "go.mod", "go.sum", "uv.lock", ".env.example",
|
|
154
|
+
"vendor/modules.txt", "src/SKILL.md", "bin/worker.py", ".cursorignore",
|
|
155
|
+
".github/workflows/ci.yml", ".aiderignore"):
|
|
156
|
+
if ignored(path):
|
|
157
|
+
errors.append(f"{mode}: intended project source ignored: {path}")
|
|
158
|
+
for path in install.OPTIONAL_DOCUMENTS["web-design"]:
|
|
159
|
+
if ignored(path):
|
|
160
|
+
errors.append(f"{mode}: project design document ignored: {path}")
|
|
161
|
+
return errors
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def agent_registry_links(root: Path) -> list[str]:
|
|
165
|
+
"""Cross-check registry entries and optional sources against the distribution."""
|
|
166
|
+
errors = []
|
|
167
|
+
registry = install.load_agent_registry(root)
|
|
168
|
+
for entry in registry["entries"]:
|
|
169
|
+
if not (root / "template" / entry).is_file():
|
|
170
|
+
errors.append("Agent registry entry missing from template: " + entry)
|
|
171
|
+
destinations: set[str] = set()
|
|
172
|
+
for agent, items in registry["optional"].items():
|
|
173
|
+
for source, destination in items:
|
|
174
|
+
if not (root / "integrations" / source).is_file():
|
|
175
|
+
errors.append("Agent registry optional source missing: " + source)
|
|
176
|
+
if destination in destinations:
|
|
177
|
+
errors.append("Agent registry destination owned by multiple agents: " + destination)
|
|
178
|
+
destinations.add(destination)
|
|
179
|
+
return errors
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def check(root: Path, *, git: str | None = None) -> tuple[list[str], list[str]]:
|
|
183
|
+
errors, warnings = [], []
|
|
184
|
+
required = ["VERSION", "LICENSE", "CONTRIBUTING.md", "README.md",
|
|
185
|
+
"Dockerfile", ".dockerignore", "docker/check_version.py", "docker/build-requirements.txt",
|
|
186
|
+
"deploy/k8s/install-preview-job.yaml", "tests/docker_smoke.py", "docs/DOCKER.md",
|
|
187
|
+
"template/AGENTS.md", "template/PROJECT_CONTEXT.md", "template/WIKI.md",
|
|
188
|
+
"template/ai-kit/CORE.md", "template/ai-kit/settings.json",
|
|
189
|
+
"template/ai-kit/project.json", "template/ai-kit/MCP.md",
|
|
190
|
+
"scripts/install.py", "scripts/router.py", "scripts/adr.py", "scripts/changelog.py",
|
|
191
|
+
"scripts/metrics.py", "scripts/context.py", "scripts/adapters.py", "scripts/evaluate.py",
|
|
192
|
+
"scripts/memory.py", "scripts/memory_import.py", "scripts/memory_store.py", "docs/DIALOGUE_MEMORY.md",
|
|
193
|
+
"scripts/benchmark.py", "scripts/adaptive.py", "scripts/learning.py",
|
|
194
|
+
"scripts/__init__.py", "template/ai-kit/METRICS.md",
|
|
195
|
+
"template/ai-kit/stacks/FILAMENT.md", "template/.agents/skills/filament-work/SKILL.md",
|
|
196
|
+
"integrations/agents.json", "integrations/claude-settings.git-ask.json",
|
|
197
|
+
"integrations/claude-settings.data-ask.json", "templates/agent-secrets.ignore",
|
|
198
|
+
"SKILL.md", "template/ai-kit/CONTEXT7.md", "template/.agents/skills/context7-docs/SKILL.md",
|
|
199
|
+
"template/.agents/hooks/session-start.md", "template/.agents/hooks/session-start.sh",
|
|
200
|
+
"template/ai-kit/router/selection.json",
|
|
201
|
+
"template/ai-kit/router/routing.json",
|
|
202
|
+
"template/ai-kit/router/providers/local.json",
|
|
203
|
+
"template/ai-kit/router/providers/gemini.json",
|
|
204
|
+
"templates/gitignore.private", "templates/gitignore.team",
|
|
205
|
+
"aikit_cli.py", "__init__.py", "MANIFEST.in", "pyproject.toml", "tests/wheel_smoke.py",
|
|
206
|
+
"scripts/check_distribution.py", "tests/test_distribution.py", "docs/PYPI.md", "docs/PYPI_DESCRIPTION.md",
|
|
207
|
+
"docker/pypi-requirements.txt", ".github/workflows/publish-pypi.yml",
|
|
208
|
+
"tests/build_distribution.py",
|
|
209
|
+
".github/workflows/check-kit.yml", "docs/IMPLEMENTATION.md",
|
|
210
|
+
]
|
|
211
|
+
for name in install.OPTIONAL_SKILLS:
|
|
212
|
+
required.extend(["integrations/" + name + ".json", "template/.agents/skills/" + name + "/SKILL.md",
|
|
213
|
+
"template/.agents/skills/" + name + "/agents/openai.yaml"])
|
|
214
|
+
required.extend("template/" + path for paths in install.OPTIONAL_RESOURCES.values() for path in paths)
|
|
215
|
+
required.extend("template/" + path for paths in install.OPTIONAL_DOCUMENTS.values() for path in paths)
|
|
216
|
+
required.extend("template/.agents/skills/" + name + "/SKILL.md"
|
|
217
|
+
for name in install.OPTIONAL_ENTRY_POINTS["web-design"])
|
|
218
|
+
for relative in required:
|
|
219
|
+
if not (root / relative).is_file():
|
|
220
|
+
errors.append("Missing: " + relative)
|
|
221
|
+
pyproject = root / "pyproject.toml"
|
|
222
|
+
if pyproject.is_file():
|
|
223
|
+
text = pyproject.read_text(encoding="utf-8")
|
|
224
|
+
if 'aikit = "aikit.aikit_cli:main"' not in text:
|
|
225
|
+
errors.append("pyproject.toml must declare the aikit console script")
|
|
226
|
+
if "version = {file = [\"VERSION\"]}" not in text:
|
|
227
|
+
errors.append("pyproject.toml must derive its version from VERSION")
|
|
228
|
+
elif not (root / "aikit_cli.py").is_file():
|
|
229
|
+
errors.append("Missing unified CLI entry point")
|
|
230
|
+
vendored_files: set[Path] = set()
|
|
231
|
+
for name in install.OPTIONAL_SKILLS:
|
|
232
|
+
try:
|
|
233
|
+
package_files = install.upstream_skill_files(name, root)
|
|
234
|
+
vendored_files.update(root / "template" / path for path in package_files
|
|
235
|
+
if path != "ai-kit/vendor/" + name + "/UPSTREAM.json")
|
|
236
|
+
except (OSError, ValueError) as exc:
|
|
237
|
+
errors.append(f"Invalid pinned {name} package: " + str(exc))
|
|
238
|
+
for p in source_files(root):
|
|
239
|
+
if p.is_symlink():
|
|
240
|
+
errors.append("Linked source: " + str(p))
|
|
241
|
+
continue
|
|
242
|
+
try:
|
|
243
|
+
text = p.read_text(encoding="utf-8")
|
|
244
|
+
except UnicodeError:
|
|
245
|
+
errors.append("Not UTF-8 text: " + str(p))
|
|
246
|
+
continue
|
|
247
|
+
# Pinned upstream Unicode fixtures are external data, not authored project language.
|
|
248
|
+
if p not in vendored_files and re.search(r"[\u0400-\u04ff]", text):
|
|
249
|
+
errors.append("Unexpected Cyrillic project content: " + str(p))
|
|
250
|
+
if p.suffix == ".json":
|
|
251
|
+
try:
|
|
252
|
+
json.loads(text)
|
|
253
|
+
except ValueError as exc:
|
|
254
|
+
errors.append(f"{p}: invalid JSON: {exc}")
|
|
255
|
+
if p.name == "SKILL.md":
|
|
256
|
+
try:
|
|
257
|
+
fields = metadata(text)
|
|
258
|
+
# The repository root is itself a skill; its folder name depends on the checkout.
|
|
259
|
+
expected = "ai-kit" if p.parent == root else p.parent.name
|
|
260
|
+
if fields["name"] != expected:
|
|
261
|
+
errors.append("Skill name/folder mismatch: " + str(p))
|
|
262
|
+
except (ValueError, TypeError) as exc:
|
|
263
|
+
errors.append(f"{p}: {exc}")
|
|
264
|
+
if p.suffix != ".md":
|
|
265
|
+
continue
|
|
266
|
+
if any(line.rstrip() != line for line in text.splitlines()):
|
|
267
|
+
errors.append("Trailing whitespace: " + str(p))
|
|
268
|
+
if not text.endswith("\n"):
|
|
269
|
+
errors.append("Missing final newline: " + str(p))
|
|
270
|
+
# Immutable upstream references are conditional assets; the entry point retains its budget.
|
|
271
|
+
if p.relative_to(root).as_posix() not in UNBOUNDED_DOCUMENTS and p not in vendored_files and len(text.encode()) > DOC_BUDGET:
|
|
272
|
+
warnings.append("Large conditional document: " + str(p))
|
|
273
|
+
for target in links(text):
|
|
274
|
+
if re.match(r"^[a-z][a-z0-9+.-]*:", target, re.I):
|
|
275
|
+
continue
|
|
276
|
+
filename, _, anchor = unquote(target).partition("#")
|
|
277
|
+
destination = (p.parent / filename).resolve() if filename else p.resolve()
|
|
278
|
+
try:
|
|
279
|
+
destination.relative_to(root.resolve())
|
|
280
|
+
except ValueError:
|
|
281
|
+
errors.append(f"{p}: link escapes reference: {target}")
|
|
282
|
+
continue
|
|
283
|
+
if destination.parent == root.resolve() and destination.name in LOCAL_MAINTAINER_FILES:
|
|
284
|
+
errors.append(f"{p}: public link depends on local maintainer file: {target}")
|
|
285
|
+
continue
|
|
286
|
+
if not destination.exists():
|
|
287
|
+
errors.append(f"{p}: missing link: {target}")
|
|
288
|
+
elif anchor and destination.is_file() and destination.suffix == ".md":
|
|
289
|
+
if anchor not in headings(destination.read_text(encoding="utf-8")):
|
|
290
|
+
errors.append(f"{p}: missing heading: {target}")
|
|
291
|
+
budgets = {"template/AGENTS.md": 2500, "template/ai-kit/CORE.md": 3000,
|
|
292
|
+
"BOOTSTRAP_PROMPT.md": 2000}
|
|
293
|
+
for relative, limit in budgets.items():
|
|
294
|
+
if (root / relative).is_file() and (root / relative).stat().st_size > limit:
|
|
295
|
+
errors.append(f"Always-loaded entry exceeds budget: {relative}")
|
|
296
|
+
registry = root / "template/ai-kit/SKILLS.md"
|
|
297
|
+
if registry.exists():
|
|
298
|
+
expected = {p.parent.name for p in (root / "template/.agents/skills").glob("*/SKILL.md")}
|
|
299
|
+
entries = re.findall(r"\.\./\.agents/skills/([^/]+)/SKILL.md", registry.read_text())
|
|
300
|
+
if expected != set(entries) or len(entries) != len(set(entries)):
|
|
301
|
+
errors.append("Skill registry differs from actual source skills")
|
|
302
|
+
for relative in ("template/PROJECT_CONTEXT.md", "template/docs/DECISIONS.md", "template/CHANGELOG.md"):
|
|
303
|
+
if (root / relative).exists() and re.search(r"2026-\d\d-\d\d|5dd530b9|voice conversation",
|
|
304
|
+
(root / relative).read_text()):
|
|
305
|
+
errors.append("Kit-maintenance history leaked into project template: " + relative)
|
|
306
|
+
project_json = root / "template/ai-kit/project.json"
|
|
307
|
+
if project_json.is_file():
|
|
308
|
+
try:
|
|
309
|
+
value = json.loads(project_json.read_text(encoding="utf-8"))
|
|
310
|
+
if type(value.get("schema")) is not int or value["schema"] != 1:
|
|
311
|
+
raise ValueError("schema must be 1")
|
|
312
|
+
evidence = value.get("evidence")
|
|
313
|
+
if (value.get("modules") != [] or not isinstance(value.get("operations"), dict) or
|
|
314
|
+
not isinstance(evidence, dict) or evidence.get("schema") != 1 or
|
|
315
|
+
evidence.get("modules") != []):
|
|
316
|
+
errors.append("Project template project.json must start empty of module facts")
|
|
317
|
+
except (ValueError, json.JSONDecodeError) as exc:
|
|
318
|
+
errors.append("Invalid project template project.json: " + str(exc))
|
|
319
|
+
providers_dir = root / "template" / "ai-kit" / "router" / "providers"
|
|
320
|
+
model_ids: list[str] = []
|
|
321
|
+
if providers_dir.is_dir():
|
|
322
|
+
for provider_path in sorted(providers_dir.glob("*.json")):
|
|
323
|
+
try:
|
|
324
|
+
config = json.loads(provider_path.read_text(encoding="utf-8"))
|
|
325
|
+
if not isinstance(config, dict):
|
|
326
|
+
raise ValueError("provider must be an object")
|
|
327
|
+
tier = config.get("tier", "cloud")
|
|
328
|
+
if tier not in {"cloud", "local", "free"}:
|
|
329
|
+
errors.append(f"Unknown provider tier: {provider_path.name}")
|
|
330
|
+
user_supplied = config.get("user_supplied_models") is True
|
|
331
|
+
if "user_supplied_models" in config and not isinstance(config["user_supplied_models"], bool):
|
|
332
|
+
raise ValueError("user_supplied_models must be a boolean")
|
|
333
|
+
status = config.get("verification_status", "verified")
|
|
334
|
+
if status not in {"verified", "pending"}:
|
|
335
|
+
errors.append(f"Unknown verification_status: {provider_path.name}")
|
|
336
|
+
verified = config.get("verified_documentation_date")
|
|
337
|
+
if isinstance(verified, str) and re.fullmatch(r"\d{4}-\d{2}-\d{2}", verified):
|
|
338
|
+
pass
|
|
339
|
+
elif user_supplied:
|
|
340
|
+
pass
|
|
341
|
+
elif status == "pending":
|
|
342
|
+
warnings.append(f"Provider {provider_path.name} verification is pending")
|
|
343
|
+
else:
|
|
344
|
+
errors.append("Provider lacks a verified_documentation_date: " + provider_path.name)
|
|
345
|
+
supported = config.get("supported_efforts", {})
|
|
346
|
+
if not isinstance(supported, dict):
|
|
347
|
+
raise ValueError("supported_efforts must be an object")
|
|
348
|
+
roles = config.get("roles")
|
|
349
|
+
if not isinstance(roles, dict) or not roles:
|
|
350
|
+
raise ValueError("roles must be a nonempty object")
|
|
351
|
+
for role, spec in roles.items():
|
|
352
|
+
if not isinstance(spec, dict):
|
|
353
|
+
raise ValueError(f"Invalid role spec: {role}")
|
|
354
|
+
model = spec.get("model")
|
|
355
|
+
if model is not None and not isinstance(model, str):
|
|
356
|
+
raise ValueError(f"Invalid model for role {role}")
|
|
357
|
+
if model is not None:
|
|
358
|
+
model_ids.append(model)
|
|
359
|
+
elif not (user_supplied or status == "pending"):
|
|
360
|
+
errors.append(f"Provider role lacks a model: {provider_path.name} role {role}")
|
|
361
|
+
if "effort" in spec:
|
|
362
|
+
if spec["effort"] not in supported.get(role, []):
|
|
363
|
+
errors.append(f"Unsupported configured effort: {provider_path.name} role {role}")
|
|
364
|
+
elif role in supported and not user_supplied:
|
|
365
|
+
errors.append(f"Role declares supported efforts but uses none: "
|
|
366
|
+
f"{provider_path.name} role {role}")
|
|
367
|
+
except (ValueError, KeyError, TypeError) as exc:
|
|
368
|
+
errors.append("Invalid provider configuration: " + str(exc))
|
|
369
|
+
try:
|
|
370
|
+
errors.extend(agent_registry_links(root))
|
|
371
|
+
except (OSError, ValueError, json.JSONDecodeError) as exc:
|
|
372
|
+
errors.append("Invalid agent registry: " + str(exc))
|
|
373
|
+
for p in (root / "template").rglob("*.md"):
|
|
374
|
+
if p in vendored_files:
|
|
375
|
+
# Upstream examples are immutable external data, not active routing selections.
|
|
376
|
+
continue
|
|
377
|
+
text = p.read_text()
|
|
378
|
+
if re.search(r"gpt-[0-9]", text) or any(model_id in text for model_id in model_ids):
|
|
379
|
+
errors.append("Active model IDs duplicated outside provider configuration: " + str(p))
|
|
380
|
+
for mode in ("private", "team"):
|
|
381
|
+
p = root / "templates" / ("gitignore." + mode)
|
|
382
|
+
if p.exists() and any(line in {"SKILL.md", "vendor/"} for line in p.read_text().splitlines()):
|
|
383
|
+
errors.append("Overbroad skill/vendor ignore: " + mode)
|
|
384
|
+
git = git or os.environ.get("AI_KIT_GIT") or shutil.which("git")
|
|
385
|
+
if git:
|
|
386
|
+
try:
|
|
387
|
+
errors.extend(visibility(root, git))
|
|
388
|
+
except (OSError, ValueError, subprocess.CalledProcessError) as exc:
|
|
389
|
+
errors.append("Git visibility verification failed: " + str(exc))
|
|
390
|
+
else:
|
|
391
|
+
warnings.append("Git unavailable: visibility checks were not run")
|
|
392
|
+
return errors, warnings
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
def main(argv: list[str] | None = None) -> int:
|
|
396
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
397
|
+
parser.add_argument("--root", type=Path, default=ROOT)
|
|
398
|
+
parser.add_argument("--git")
|
|
399
|
+
args = parser.parse_args(argv)
|
|
400
|
+
errors, warnings = check(args.root, git=args.git)
|
|
401
|
+
print(json.dumps({"errors": errors, "warnings": warnings}, indent=2))
|
|
402
|
+
return int(bool(errors))
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
if __name__ == "__main__":
|
|
406
|
+
raise SystemExit(main())
|