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.
Files changed (278) hide show
  1. ai_kit_python-1.4.2.dist-info/METADATA +132 -0
  2. ai_kit_python-1.4.2.dist-info/RECORD +278 -0
  3. ai_kit_python-1.4.2.dist-info/WHEEL +5 -0
  4. ai_kit_python-1.4.2.dist-info/entry_points.txt +2 -0
  5. ai_kit_python-1.4.2.dist-info/licenses/LICENSE +21 -0
  6. ai_kit_python-1.4.2.dist-info/licenses/template/ai-kit/vendor/security-audit/LICENSE +21 -0
  7. ai_kit_python-1.4.2.dist-info/licenses/template/ai-kit/vendor/strix/LICENSE +201 -0
  8. ai_kit_python-1.4.2.dist-info/top_level.txt +1 -0
  9. aikit/.agents/skills/go-work/SKILL.md +8 -0
  10. aikit/.agents/skills/laravel-work/SKILL.md +8 -0
  11. aikit/.agents/skills/moodle-work/SKILL.md +8 -0
  12. aikit/.agents/skills/project-continuity/SKILL.md +34 -0
  13. aikit/.agents/skills/python-work/SKILL.md +8 -0
  14. aikit/.dockerignore +90 -0
  15. aikit/.gitattributes +3 -0
  16. aikit/.github/workflows/check-kit.yml +70 -0
  17. aikit/.github/workflows/publish-pypi.yml +108 -0
  18. aikit/.gitignore +60 -0
  19. aikit/BOOTSTRAP_PROMPT.md +18 -0
  20. aikit/CHANGELOG.md +85 -0
  21. aikit/CLAUDE.md +3 -0
  22. aikit/CONTRIBUTING.md +46 -0
  23. aikit/Dockerfile +44 -0
  24. aikit/KIMI.md +3 -0
  25. aikit/LICENSE +21 -0
  26. aikit/MANIFEST.in +22 -0
  27. aikit/MANUS.md +3 -0
  28. aikit/Makefile +70 -0
  29. aikit/README.md +1161 -0
  30. aikit/SECURITY.md +8 -0
  31. aikit/SKILL.md +105 -0
  32. aikit/VERSION +1 -0
  33. aikit/__init__.py +1 -0
  34. aikit/ai-kit/ADAPTERS.md +5 -0
  35. aikit/ai-kit/CONTAINERS.md +5 -0
  36. aikit/ai-kit/ENGINEERING.md +5 -0
  37. aikit/ai-kit/INTENT.md +5 -0
  38. aikit/ai-kit/MODELS.md +5 -0
  39. aikit/ai-kit/SKILLS.md +5 -0
  40. aikit/ai-kit/stacks/GO.md +5 -0
  41. aikit/ai-kit/stacks/LARAVEL.md +5 -0
  42. aikit/ai-kit/stacks/MOODLE.md +5 -0
  43. aikit/ai-kit/stacks/PYTHON.md +5 -0
  44. aikit/aikit_cli.py +59 -0
  45. aikit/deploy/k8s/install-preview-job.yaml +56 -0
  46. aikit/docker/build-requirements.txt +4 -0
  47. aikit/docker/check_version.py +29 -0
  48. aikit/docker/pypi-requirements.txt +3 -0
  49. aikit/docs/ADAPTER_PATH_FIX.md +23 -0
  50. aikit/docs/BENCHMARK.md +106 -0
  51. aikit/docs/CONTEXT7.md +33 -0
  52. aikit/docs/DECISIONS.md +22 -0
  53. aikit/docs/DEVELOPMENT_PLAN.md +16 -0
  54. aikit/docs/DIALOGUE_MEMORY.md +70 -0
  55. aikit/docs/DOCKER.md +74 -0
  56. aikit/docs/DOCKER_HUB_OVERVIEW.md +311 -0
  57. aikit/docs/EVALUATION.md +30 -0
  58. aikit/docs/IMPLEMENTATION.md +134 -0
  59. aikit/docs/MANUS_REVIEW.md +46 -0
  60. aikit/docs/PYPI.md +87 -0
  61. aikit/docs/PYPI_DESCRIPTION.md +103 -0
  62. aikit/docs/SECURITY_AUDIT.md +26 -0
  63. aikit/docs/STRIX.md +28 -0
  64. aikit/docs/V0_5_0.md +27 -0
  65. aikit/docs/V0_6_4.md +25 -0
  66. aikit/docs/V1_4_0.md +73 -0
  67. aikit/docs/V1_4_1.md +55 -0
  68. aikit/docs/V1_4_2.md +33 -0
  69. aikit/docs/WEB_DESIGN.md +72 -0
  70. aikit/docs/WINDOWS_CI_FIX.md +39 -0
  71. aikit/docs/adr/0000-template.md +24 -0
  72. aikit/docs/adr/0001-adaptive-model-routing.md +30 -0
  73. aikit/docs/adr/0002-template-separation.md +24 -0
  74. aikit/docs/adr/0003-agent-registry-and-router.md +24 -0
  75. aikit/docs/adr/0004-onboarding-and-project-facts.md +24 -0
  76. aikit/docs/adr/0005-client-settings-ownership.md +30 -0
  77. aikit/docs/adr/0006-routing-evidence-and-local-measurements.md +26 -0
  78. aikit/docs/adr/0007-generated-client-configuration-and-presets.md +34 -0
  79. aikit/docs/adr/0008-unified-cli-and-fact-drift.md +20 -0
  80. aikit/docs/adr/0009-provider-review-doctor-fix-and-ci-self-install.md +20 -0
  81. aikit/docs/adr/0010-metrics-analysis-and-gemini-verification.md +20 -0
  82. aikit/docs/adr/0011-evidence-lifecycle-and-evaluation.md +20 -0
  83. aikit/docs/adr/0012-measurement-led-routing.md +31 -0
  84. aikit/docs/adr/0013-docker-cli-image.md +24 -0
  85. aikit/docs/adr/0014-local-dialogue-memory.md +22 -0
  86. aikit/docs/history/original-bundle-checks.md +11 -0
  87. aikit/docs/history/v0.1-changelog.txt +37 -0
  88. aikit/docs/history/v0.10-changelog.md +12 -0
  89. aikit/docs/history/v0.2-changelog.md +35 -0
  90. aikit/docs/history/v0.3-changelog.md +25 -0
  91. aikit/docs/history/v0.4-changelog.md +31 -0
  92. aikit/docs/history/v0.5-changelog.md +14 -0
  93. aikit/docs/history/v0.6.4-changelog.md +12 -0
  94. aikit/docs/history/v0.6.5-v0.9-changelog.md +35 -0
  95. aikit/docs/history/v1.1.0-changelog.md +12 -0
  96. aikit/docs/history/v1.2.0-changelog.md +11 -0
  97. aikit/docs/history/verification-before-measurements.md +53 -0
  98. aikit/integrations/CONVENTIONS.md +3 -0
  99. aikit/integrations/README.md +7 -0
  100. aikit/integrations/agents.json +136 -0
  101. aikit/integrations/ai-kit-check.yml +66 -0
  102. aikit/integrations/ai-kit-cline.md +4 -0
  103. aikit/integrations/ai-kit-roo.md +4 -0
  104. aikit/integrations/ai-kit-windsurf.md +4 -0
  105. aikit/integrations/ai-kit.mdc +6 -0
  106. aikit/integrations/claude-settings.data-ask.json +23 -0
  107. aikit/integrations/claude-settings.git-ask.json +10 -0
  108. aikit/integrations/copilot-instructions.md +3 -0
  109. aikit/integrations/security-audit.json +135 -0
  110. aikit/integrations/strix.json +69 -0
  111. aikit/pyproject.toml +60 -0
  112. aikit/scripts/__init__.py +1 -0
  113. aikit/scripts/adapters.py +184 -0
  114. aikit/scripts/adaptive.py +167 -0
  115. aikit/scripts/adr.py +61 -0
  116. aikit/scripts/benchmark.py +543 -0
  117. aikit/scripts/changelog.py +55 -0
  118. aikit/scripts/check_distribution.py +178 -0
  119. aikit/scripts/check_kit.py +406 -0
  120. aikit/scripts/context.py +184 -0
  121. aikit/scripts/docker_test_usage.py +55 -0
  122. aikit/scripts/doctor.py +313 -0
  123. aikit/scripts/evaluate.py +125 -0
  124. aikit/scripts/install.py +1584 -0
  125. aikit/scripts/learning.py +260 -0
  126. aikit/scripts/memory.py +344 -0
  127. aikit/scripts/memory_import.py +177 -0
  128. aikit/scripts/memory_store.py +125 -0
  129. aikit/scripts/metrics.py +351 -0
  130. aikit/scripts/router.py +399 -0
  131. aikit/template/.agents/hooks/session-start.md +3 -0
  132. aikit/template/.agents/hooks/session-start.sh +21 -0
  133. aikit/template/.agents/skills/ai-kit-bootstrap/SKILL.md +14 -0
  134. aikit/template/.agents/skills/ai-kit-review/SKILL.md +11 -0
  135. aikit/template/.agents/skills/ai-kit-sync-context/SKILL.md +12 -0
  136. aikit/template/.agents/skills/context7-docs/SKILL.md +11 -0
  137. aikit/template/.agents/skills/design-extract/SKILL.md +9 -0
  138. aikit/template/.agents/skills/dotnet-work/SKILL.md +9 -0
  139. aikit/template/.agents/skills/filament-work/SKILL.md +9 -0
  140. aikit/template/.agents/skills/frontend-work/SKILL.md +9 -0
  141. aikit/template/.agents/skills/go-work/SKILL.md +9 -0
  142. aikit/template/.agents/skills/infra-work/SKILL.md +9 -0
  143. aikit/template/.agents/skills/java-work/SKILL.md +9 -0
  144. aikit/template/.agents/skills/laravel-blade-ui/SKILL.md +9 -0
  145. aikit/template/.agents/skills/laravel-work/SKILL.md +10 -0
  146. aikit/template/.agents/skills/library-work/SKILL.md +9 -0
  147. aikit/template/.agents/skills/moodle-work/SKILL.md +9 -0
  148. aikit/template/.agents/skills/php-work/SKILL.md +9 -0
  149. aikit/template/.agents/skills/project-continuity/SKILL.md +21 -0
  150. aikit/template/.agents/skills/python-work/SKILL.md +9 -0
  151. aikit/template/.agents/skills/rust-work/SKILL.md +9 -0
  152. aikit/template/.agents/skills/security-audit/SKILL.md +16 -0
  153. aikit/template/.agents/skills/security-audit/agents/openai.yaml +2 -0
  154. aikit/template/.agents/skills/strix/SKILL.md +16 -0
  155. aikit/template/.agents/skills/strix/agents/openai.yaml +2 -0
  156. aikit/template/.agents/skills/ui-review/SKILL.md +9 -0
  157. aikit/template/.agents/skills/web-design/SKILL.md +9 -0
  158. aikit/template/AGENTS.md +16 -0
  159. aikit/template/CHANGELOG.md +6 -0
  160. aikit/template/CLAUDE.md +5 -0
  161. aikit/template/GEMINI.md +4 -0
  162. aikit/template/KIMI.md +4 -0
  163. aikit/template/MANUS.md +4 -0
  164. aikit/template/PROJECT_CONTEXT.md +35 -0
  165. aikit/template/README.md +6 -0
  166. aikit/template/WIKI.md +37 -0
  167. aikit/template/ai-kit/ADAPTERS.md +100 -0
  168. aikit/template/ai-kit/BOOTSTRAP.md +15 -0
  169. aikit/template/ai-kit/CONTAINERS.md +63 -0
  170. aikit/template/ai-kit/CONTEXT7.md +96 -0
  171. aikit/template/ai-kit/CORE.md +23 -0
  172. aikit/template/ai-kit/ENGINEERING.md +34 -0
  173. aikit/template/ai-kit/MCP.md +17 -0
  174. aikit/template/ai-kit/METRICS.md +83 -0
  175. aikit/template/ai-kit/SECURITY-AUDIT.md +21 -0
  176. aikit/template/ai-kit/SECURITY.md +34 -0
  177. aikit/template/ai-kit/SKILLS.md +37 -0
  178. aikit/template/ai-kit/STRIX.md +44 -0
  179. aikit/template/ai-kit/UPSTREAM.md +38 -0
  180. aikit/template/ai-kit/WEB-DESIGN.md +37 -0
  181. aikit/template/ai-kit/profiles/OWNER.md +19 -0
  182. aikit/template/ai-kit/project.json +14 -0
  183. aikit/template/ai-kit/router/POLICY.md +79 -0
  184. aikit/template/ai-kit/router/providers/anthropic.json +44 -0
  185. aikit/template/ai-kit/router/providers/anthropic.md +14 -0
  186. aikit/template/ai-kit/router/providers/gemini.json +13 -0
  187. aikit/template/ai-kit/router/providers/gemini.md +10 -0
  188. aikit/template/ai-kit/router/providers/kimi.json +26 -0
  189. aikit/template/ai-kit/router/providers/kimi.md +13 -0
  190. aikit/template/ai-kit/router/providers/local.json +18 -0
  191. aikit/template/ai-kit/router/providers/local.md +10 -0
  192. aikit/template/ai-kit/router/providers/openai.json +48 -0
  193. aikit/template/ai-kit/router/providers/openai.md +14 -0
  194. aikit/template/ai-kit/router/routing.json +11 -0
  195. aikit/template/ai-kit/router/selection.json +14 -0
  196. aikit/template/ai-kit/settings.json +11 -0
  197. aikit/template/ai-kit/stacks/DOTNET.md +13 -0
  198. aikit/template/ai-kit/stacks/FILAMENT.md +18 -0
  199. aikit/template/ai-kit/stacks/FRONTEND.md +9 -0
  200. aikit/template/ai-kit/stacks/GO.md +19 -0
  201. aikit/template/ai-kit/stacks/INFRA.md +12 -0
  202. aikit/template/ai-kit/stacks/JAVA.md +13 -0
  203. aikit/template/ai-kit/stacks/LARAVEL.md +15 -0
  204. aikit/template/ai-kit/stacks/LIBRARY.md +9 -0
  205. aikit/template/ai-kit/stacks/MOODLE.md +12 -0
  206. aikit/template/ai-kit/stacks/PHP.md +11 -0
  207. aikit/template/ai-kit/stacks/PYTHON.md +11 -0
  208. aikit/template/ai-kit/stacks/RUST.md +18 -0
  209. aikit/template/ai-kit/vendor/security-audit/AI-AND-LLM.md +83 -0
  210. aikit/template/ai-kit/vendor/security-audit/ATTACK-CLASSES.md +130 -0
  211. aikit/template/ai-kit/vendor/security-audit/CLIENT-SIDE.md +83 -0
  212. aikit/template/ai-kit/vendor/security-audit/CLOUD-AND-DEPLOYMENT.md +86 -0
  213. aikit/template/ai-kit/vendor/security-audit/DATA-ISOLATION-AND-LIFECYCLE.md +84 -0
  214. aikit/template/ai-kit/vendor/security-audit/DESKTOP-MOBILE-AND-LOCAL-IPC.md +89 -0
  215. aikit/template/ai-kit/vendor/security-audit/HUNTING.md +251 -0
  216. aikit/template/ai-kit/vendor/security-audit/LICENSE +21 -0
  217. aikit/template/ai-kit/vendor/security-audit/MEMORY-SAFETY-AND-BINARY.md +101 -0
  218. aikit/template/ai-kit/vendor/security-audit/PROTOCOLS-RPC-AND-MESSAGING.md +81 -0
  219. aikit/template/ai-kit/vendor/security-audit/RECONNAISSANCE.md +156 -0
  220. aikit/template/ai-kit/vendor/security-audit/RESOURCE-EXHAUSTION-AND-AVAILABILITY.md +78 -0
  221. aikit/template/ai-kit/vendor/security-audit/SKILL.md +192 -0
  222. aikit/template/ai-kit/vendor/security-audit/SUPPLY-CHAIN-AND-RELEASE.md +73 -0
  223. aikit/template/ai-kit/vendor/security-audit/VALIDATION-AND-REPORTING.md +186 -0
  224. aikit/template/ai-kit/vendor/security-audit/WEB-PROTOCOL-AND-AUTH.md +105 -0
  225. aikit/template/ai-kit/vendor/security-audit/report-schema.json +461 -0
  226. aikit/template/ai-kit/vendor/security-audit/validate-coverage-ledger.cjs +872 -0
  227. aikit/template/ai-kit/vendor/security-audit/validate-coverage-ledger.test.cjs +740 -0
  228. aikit/template/ai-kit/vendor/security-audit/validate-findings.cjs +773 -0
  229. aikit/template/ai-kit/vendor/security-audit/validate-findings.test.cjs +652 -0
  230. aikit/template/ai-kit/vendor/strix/LICENSE +201 -0
  231. aikit/template/ai-kit/vendor/strix/api-security-testing/SKILL.md +61 -0
  232. aikit/template/ai-kit/vendor/strix/application-security-testing/SKILL.md +66 -0
  233. aikit/template/ai-kit/vendor/strix/ci-security-scanning-with-strix/SKILL.md +149 -0
  234. aikit/template/ai-kit/vendor/strix/find-security-vulnerabilities-in-code/SKILL.md +62 -0
  235. aikit/template/ai-kit/vendor/strix/fix-security-vulnerabilities-with-strix/SKILL.md +77 -0
  236. aikit/template/ai-kit/vendor/strix/managed-pentesting-with-strix/SKILL.md +321 -0
  237. aikit/template/ai-kit/vendor/strix/owasp-top-10-testing/SKILL.md +64 -0
  238. aikit/template/ai-kit/vendor/strix/penetration-testing-with-strix/SKILL.md +162 -0
  239. aikit/template/ai-kit/vendor/strix/web-app-penetration-testing/SKILL.md +54 -0
  240. aikit/template/ai-kit/web-design/LARAVEL.md +51 -0
  241. aikit/template/ai-kit/web-design/LIBRARIES.md +39 -0
  242. aikit/template/ai-kit/web-design/REVIEW.md +37 -0
  243. aikit/template/ai-kit/web-design/TOOLS.md +43 -0
  244. aikit/template/docs/DECISIONS.md +7 -0
  245. aikit/template/docs/adr/0000-template.md +20 -0
  246. aikit/template/docs/design/ACCEPTANCE.md +24 -0
  247. aikit/template/docs/design/BRIEF.md +26 -0
  248. aikit/template/docs/design/SYSTEM.md +30 -0
  249. aikit/templates/agent-secrets.ignore +19 -0
  250. aikit/templates/gitignore.private +160 -0
  251. aikit/templates/gitignore.team +139 -0
  252. aikit/tests/build_distribution.py +73 -0
  253. aikit/tests/docker_smoke.py +172 -0
  254. aikit/tests/fixtures/create-junction.ps1 +18 -0
  255. aikit/tests/fixtures/golden/claude-settings-local.json +58 -0
  256. aikit/tests/test_adapters.py +105 -0
  257. aikit/tests/test_aikit.py +79 -0
  258. aikit/tests/test_benchmark.py +241 -0
  259. aikit/tests/test_check_kit.py +203 -0
  260. aikit/tests/test_context.py +76 -0
  261. aikit/tests/test_context7.py +153 -0
  262. aikit/tests/test_context_selection.py +70 -0
  263. aikit/tests/test_distribution.py +181 -0
  264. aikit/tests/test_docker_image.py +63 -0
  265. aikit/tests/test_docker_test_usage.py +54 -0
  266. aikit/tests/test_doctor.py +148 -0
  267. aikit/tests/test_evaluate.py +66 -0
  268. aikit/tests/test_install.py +939 -0
  269. aikit/tests/test_learning.py +247 -0
  270. aikit/tests/test_memory.py +371 -0
  271. aikit/tests/test_metrics.py +209 -0
  272. aikit/tests/test_native.py +342 -0
  273. aikit/tests/test_onboarding.py +301 -0
  274. aikit/tests/test_router.py +336 -0
  275. aikit/tests/test_security_audit.py +237 -0
  276. aikit/tests/test_strix.py +245 -0
  277. aikit/tests/test_web_design.py +219 -0
  278. 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())