ai-kit-python 1.4.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ai_kit_python-1.4.2.dist-info/METADATA +132 -0
- ai_kit_python-1.4.2.dist-info/RECORD +278 -0
- ai_kit_python-1.4.2.dist-info/WHEEL +5 -0
- ai_kit_python-1.4.2.dist-info/entry_points.txt +2 -0
- ai_kit_python-1.4.2.dist-info/licenses/LICENSE +21 -0
- ai_kit_python-1.4.2.dist-info/licenses/template/ai-kit/vendor/security-audit/LICENSE +21 -0
- ai_kit_python-1.4.2.dist-info/licenses/template/ai-kit/vendor/strix/LICENSE +201 -0
- ai_kit_python-1.4.2.dist-info/top_level.txt +1 -0
- aikit/.agents/skills/go-work/SKILL.md +8 -0
- aikit/.agents/skills/laravel-work/SKILL.md +8 -0
- aikit/.agents/skills/moodle-work/SKILL.md +8 -0
- aikit/.agents/skills/project-continuity/SKILL.md +34 -0
- aikit/.agents/skills/python-work/SKILL.md +8 -0
- aikit/.dockerignore +90 -0
- aikit/.gitattributes +3 -0
- aikit/.github/workflows/check-kit.yml +70 -0
- aikit/.github/workflows/publish-pypi.yml +108 -0
- aikit/.gitignore +60 -0
- aikit/BOOTSTRAP_PROMPT.md +18 -0
- aikit/CHANGELOG.md +85 -0
- aikit/CLAUDE.md +3 -0
- aikit/CONTRIBUTING.md +46 -0
- aikit/Dockerfile +44 -0
- aikit/KIMI.md +3 -0
- aikit/LICENSE +21 -0
- aikit/MANIFEST.in +22 -0
- aikit/MANUS.md +3 -0
- aikit/Makefile +70 -0
- aikit/README.md +1161 -0
- aikit/SECURITY.md +8 -0
- aikit/SKILL.md +105 -0
- aikit/VERSION +1 -0
- aikit/__init__.py +1 -0
- aikit/ai-kit/ADAPTERS.md +5 -0
- aikit/ai-kit/CONTAINERS.md +5 -0
- aikit/ai-kit/ENGINEERING.md +5 -0
- aikit/ai-kit/INTENT.md +5 -0
- aikit/ai-kit/MODELS.md +5 -0
- aikit/ai-kit/SKILLS.md +5 -0
- aikit/ai-kit/stacks/GO.md +5 -0
- aikit/ai-kit/stacks/LARAVEL.md +5 -0
- aikit/ai-kit/stacks/MOODLE.md +5 -0
- aikit/ai-kit/stacks/PYTHON.md +5 -0
- aikit/aikit_cli.py +59 -0
- aikit/deploy/k8s/install-preview-job.yaml +56 -0
- aikit/docker/build-requirements.txt +4 -0
- aikit/docker/check_version.py +29 -0
- aikit/docker/pypi-requirements.txt +3 -0
- aikit/docs/ADAPTER_PATH_FIX.md +23 -0
- aikit/docs/BENCHMARK.md +106 -0
- aikit/docs/CONTEXT7.md +33 -0
- aikit/docs/DECISIONS.md +22 -0
- aikit/docs/DEVELOPMENT_PLAN.md +16 -0
- aikit/docs/DIALOGUE_MEMORY.md +70 -0
- aikit/docs/DOCKER.md +74 -0
- aikit/docs/DOCKER_HUB_OVERVIEW.md +311 -0
- aikit/docs/EVALUATION.md +30 -0
- aikit/docs/IMPLEMENTATION.md +134 -0
- aikit/docs/MANUS_REVIEW.md +46 -0
- aikit/docs/PYPI.md +87 -0
- aikit/docs/PYPI_DESCRIPTION.md +103 -0
- aikit/docs/SECURITY_AUDIT.md +26 -0
- aikit/docs/STRIX.md +28 -0
- aikit/docs/V0_5_0.md +27 -0
- aikit/docs/V0_6_4.md +25 -0
- aikit/docs/V1_4_0.md +73 -0
- aikit/docs/V1_4_1.md +55 -0
- aikit/docs/V1_4_2.md +33 -0
- aikit/docs/WEB_DESIGN.md +72 -0
- aikit/docs/WINDOWS_CI_FIX.md +39 -0
- aikit/docs/adr/0000-template.md +24 -0
- aikit/docs/adr/0001-adaptive-model-routing.md +30 -0
- aikit/docs/adr/0002-template-separation.md +24 -0
- aikit/docs/adr/0003-agent-registry-and-router.md +24 -0
- aikit/docs/adr/0004-onboarding-and-project-facts.md +24 -0
- aikit/docs/adr/0005-client-settings-ownership.md +30 -0
- aikit/docs/adr/0006-routing-evidence-and-local-measurements.md +26 -0
- aikit/docs/adr/0007-generated-client-configuration-and-presets.md +34 -0
- aikit/docs/adr/0008-unified-cli-and-fact-drift.md +20 -0
- aikit/docs/adr/0009-provider-review-doctor-fix-and-ci-self-install.md +20 -0
- aikit/docs/adr/0010-metrics-analysis-and-gemini-verification.md +20 -0
- aikit/docs/adr/0011-evidence-lifecycle-and-evaluation.md +20 -0
- aikit/docs/adr/0012-measurement-led-routing.md +31 -0
- aikit/docs/adr/0013-docker-cli-image.md +24 -0
- aikit/docs/adr/0014-local-dialogue-memory.md +22 -0
- aikit/docs/history/original-bundle-checks.md +11 -0
- aikit/docs/history/v0.1-changelog.txt +37 -0
- aikit/docs/history/v0.10-changelog.md +12 -0
- aikit/docs/history/v0.2-changelog.md +35 -0
- aikit/docs/history/v0.3-changelog.md +25 -0
- aikit/docs/history/v0.4-changelog.md +31 -0
- aikit/docs/history/v0.5-changelog.md +14 -0
- aikit/docs/history/v0.6.4-changelog.md +12 -0
- aikit/docs/history/v0.6.5-v0.9-changelog.md +35 -0
- aikit/docs/history/v1.1.0-changelog.md +12 -0
- aikit/docs/history/v1.2.0-changelog.md +11 -0
- aikit/docs/history/verification-before-measurements.md +53 -0
- aikit/integrations/CONVENTIONS.md +3 -0
- aikit/integrations/README.md +7 -0
- aikit/integrations/agents.json +136 -0
- aikit/integrations/ai-kit-check.yml +66 -0
- aikit/integrations/ai-kit-cline.md +4 -0
- aikit/integrations/ai-kit-roo.md +4 -0
- aikit/integrations/ai-kit-windsurf.md +4 -0
- aikit/integrations/ai-kit.mdc +6 -0
- aikit/integrations/claude-settings.data-ask.json +23 -0
- aikit/integrations/claude-settings.git-ask.json +10 -0
- aikit/integrations/copilot-instructions.md +3 -0
- aikit/integrations/security-audit.json +135 -0
- aikit/integrations/strix.json +69 -0
- aikit/pyproject.toml +60 -0
- aikit/scripts/__init__.py +1 -0
- aikit/scripts/adapters.py +184 -0
- aikit/scripts/adaptive.py +167 -0
- aikit/scripts/adr.py +61 -0
- aikit/scripts/benchmark.py +543 -0
- aikit/scripts/changelog.py +55 -0
- aikit/scripts/check_distribution.py +178 -0
- aikit/scripts/check_kit.py +406 -0
- aikit/scripts/context.py +184 -0
- aikit/scripts/docker_test_usage.py +55 -0
- aikit/scripts/doctor.py +313 -0
- aikit/scripts/evaluate.py +125 -0
- aikit/scripts/install.py +1584 -0
- aikit/scripts/learning.py +260 -0
- aikit/scripts/memory.py +344 -0
- aikit/scripts/memory_import.py +177 -0
- aikit/scripts/memory_store.py +125 -0
- aikit/scripts/metrics.py +351 -0
- aikit/scripts/router.py +399 -0
- aikit/template/.agents/hooks/session-start.md +3 -0
- aikit/template/.agents/hooks/session-start.sh +21 -0
- aikit/template/.agents/skills/ai-kit-bootstrap/SKILL.md +14 -0
- aikit/template/.agents/skills/ai-kit-review/SKILL.md +11 -0
- aikit/template/.agents/skills/ai-kit-sync-context/SKILL.md +12 -0
- aikit/template/.agents/skills/context7-docs/SKILL.md +11 -0
- aikit/template/.agents/skills/design-extract/SKILL.md +9 -0
- aikit/template/.agents/skills/dotnet-work/SKILL.md +9 -0
- aikit/template/.agents/skills/filament-work/SKILL.md +9 -0
- aikit/template/.agents/skills/frontend-work/SKILL.md +9 -0
- aikit/template/.agents/skills/go-work/SKILL.md +9 -0
- aikit/template/.agents/skills/infra-work/SKILL.md +9 -0
- aikit/template/.agents/skills/java-work/SKILL.md +9 -0
- aikit/template/.agents/skills/laravel-blade-ui/SKILL.md +9 -0
- aikit/template/.agents/skills/laravel-work/SKILL.md +10 -0
- aikit/template/.agents/skills/library-work/SKILL.md +9 -0
- aikit/template/.agents/skills/moodle-work/SKILL.md +9 -0
- aikit/template/.agents/skills/php-work/SKILL.md +9 -0
- aikit/template/.agents/skills/project-continuity/SKILL.md +21 -0
- aikit/template/.agents/skills/python-work/SKILL.md +9 -0
- aikit/template/.agents/skills/rust-work/SKILL.md +9 -0
- aikit/template/.agents/skills/security-audit/SKILL.md +16 -0
- aikit/template/.agents/skills/security-audit/agents/openai.yaml +2 -0
- aikit/template/.agents/skills/strix/SKILL.md +16 -0
- aikit/template/.agents/skills/strix/agents/openai.yaml +2 -0
- aikit/template/.agents/skills/ui-review/SKILL.md +9 -0
- aikit/template/.agents/skills/web-design/SKILL.md +9 -0
- aikit/template/AGENTS.md +16 -0
- aikit/template/CHANGELOG.md +6 -0
- aikit/template/CLAUDE.md +5 -0
- aikit/template/GEMINI.md +4 -0
- aikit/template/KIMI.md +4 -0
- aikit/template/MANUS.md +4 -0
- aikit/template/PROJECT_CONTEXT.md +35 -0
- aikit/template/README.md +6 -0
- aikit/template/WIKI.md +37 -0
- aikit/template/ai-kit/ADAPTERS.md +100 -0
- aikit/template/ai-kit/BOOTSTRAP.md +15 -0
- aikit/template/ai-kit/CONTAINERS.md +63 -0
- aikit/template/ai-kit/CONTEXT7.md +96 -0
- aikit/template/ai-kit/CORE.md +23 -0
- aikit/template/ai-kit/ENGINEERING.md +34 -0
- aikit/template/ai-kit/MCP.md +17 -0
- aikit/template/ai-kit/METRICS.md +83 -0
- aikit/template/ai-kit/SECURITY-AUDIT.md +21 -0
- aikit/template/ai-kit/SECURITY.md +34 -0
- aikit/template/ai-kit/SKILLS.md +37 -0
- aikit/template/ai-kit/STRIX.md +44 -0
- aikit/template/ai-kit/UPSTREAM.md +38 -0
- aikit/template/ai-kit/WEB-DESIGN.md +37 -0
- aikit/template/ai-kit/profiles/OWNER.md +19 -0
- aikit/template/ai-kit/project.json +14 -0
- aikit/template/ai-kit/router/POLICY.md +79 -0
- aikit/template/ai-kit/router/providers/anthropic.json +44 -0
- aikit/template/ai-kit/router/providers/anthropic.md +14 -0
- aikit/template/ai-kit/router/providers/gemini.json +13 -0
- aikit/template/ai-kit/router/providers/gemini.md +10 -0
- aikit/template/ai-kit/router/providers/kimi.json +26 -0
- aikit/template/ai-kit/router/providers/kimi.md +13 -0
- aikit/template/ai-kit/router/providers/local.json +18 -0
- aikit/template/ai-kit/router/providers/local.md +10 -0
- aikit/template/ai-kit/router/providers/openai.json +48 -0
- aikit/template/ai-kit/router/providers/openai.md +14 -0
- aikit/template/ai-kit/router/routing.json +11 -0
- aikit/template/ai-kit/router/selection.json +14 -0
- aikit/template/ai-kit/settings.json +11 -0
- aikit/template/ai-kit/stacks/DOTNET.md +13 -0
- aikit/template/ai-kit/stacks/FILAMENT.md +18 -0
- aikit/template/ai-kit/stacks/FRONTEND.md +9 -0
- aikit/template/ai-kit/stacks/GO.md +19 -0
- aikit/template/ai-kit/stacks/INFRA.md +12 -0
- aikit/template/ai-kit/stacks/JAVA.md +13 -0
- aikit/template/ai-kit/stacks/LARAVEL.md +15 -0
- aikit/template/ai-kit/stacks/LIBRARY.md +9 -0
- aikit/template/ai-kit/stacks/MOODLE.md +12 -0
- aikit/template/ai-kit/stacks/PHP.md +11 -0
- aikit/template/ai-kit/stacks/PYTHON.md +11 -0
- aikit/template/ai-kit/stacks/RUST.md +18 -0
- aikit/template/ai-kit/vendor/security-audit/AI-AND-LLM.md +83 -0
- aikit/template/ai-kit/vendor/security-audit/ATTACK-CLASSES.md +130 -0
- aikit/template/ai-kit/vendor/security-audit/CLIENT-SIDE.md +83 -0
- aikit/template/ai-kit/vendor/security-audit/CLOUD-AND-DEPLOYMENT.md +86 -0
- aikit/template/ai-kit/vendor/security-audit/DATA-ISOLATION-AND-LIFECYCLE.md +84 -0
- aikit/template/ai-kit/vendor/security-audit/DESKTOP-MOBILE-AND-LOCAL-IPC.md +89 -0
- aikit/template/ai-kit/vendor/security-audit/HUNTING.md +251 -0
- aikit/template/ai-kit/vendor/security-audit/LICENSE +21 -0
- aikit/template/ai-kit/vendor/security-audit/MEMORY-SAFETY-AND-BINARY.md +101 -0
- aikit/template/ai-kit/vendor/security-audit/PROTOCOLS-RPC-AND-MESSAGING.md +81 -0
- aikit/template/ai-kit/vendor/security-audit/RECONNAISSANCE.md +156 -0
- aikit/template/ai-kit/vendor/security-audit/RESOURCE-EXHAUSTION-AND-AVAILABILITY.md +78 -0
- aikit/template/ai-kit/vendor/security-audit/SKILL.md +192 -0
- aikit/template/ai-kit/vendor/security-audit/SUPPLY-CHAIN-AND-RELEASE.md +73 -0
- aikit/template/ai-kit/vendor/security-audit/VALIDATION-AND-REPORTING.md +186 -0
- aikit/template/ai-kit/vendor/security-audit/WEB-PROTOCOL-AND-AUTH.md +105 -0
- aikit/template/ai-kit/vendor/security-audit/report-schema.json +461 -0
- aikit/template/ai-kit/vendor/security-audit/validate-coverage-ledger.cjs +872 -0
- aikit/template/ai-kit/vendor/security-audit/validate-coverage-ledger.test.cjs +740 -0
- aikit/template/ai-kit/vendor/security-audit/validate-findings.cjs +773 -0
- aikit/template/ai-kit/vendor/security-audit/validate-findings.test.cjs +652 -0
- aikit/template/ai-kit/vendor/strix/LICENSE +201 -0
- aikit/template/ai-kit/vendor/strix/api-security-testing/SKILL.md +61 -0
- aikit/template/ai-kit/vendor/strix/application-security-testing/SKILL.md +66 -0
- aikit/template/ai-kit/vendor/strix/ci-security-scanning-with-strix/SKILL.md +149 -0
- aikit/template/ai-kit/vendor/strix/find-security-vulnerabilities-in-code/SKILL.md +62 -0
- aikit/template/ai-kit/vendor/strix/fix-security-vulnerabilities-with-strix/SKILL.md +77 -0
- aikit/template/ai-kit/vendor/strix/managed-pentesting-with-strix/SKILL.md +321 -0
- aikit/template/ai-kit/vendor/strix/owasp-top-10-testing/SKILL.md +64 -0
- aikit/template/ai-kit/vendor/strix/penetration-testing-with-strix/SKILL.md +162 -0
- aikit/template/ai-kit/vendor/strix/web-app-penetration-testing/SKILL.md +54 -0
- aikit/template/ai-kit/web-design/LARAVEL.md +51 -0
- aikit/template/ai-kit/web-design/LIBRARIES.md +39 -0
- aikit/template/ai-kit/web-design/REVIEW.md +37 -0
- aikit/template/ai-kit/web-design/TOOLS.md +43 -0
- aikit/template/docs/DECISIONS.md +7 -0
- aikit/template/docs/adr/0000-template.md +20 -0
- aikit/template/docs/design/ACCEPTANCE.md +24 -0
- aikit/template/docs/design/BRIEF.md +26 -0
- aikit/template/docs/design/SYSTEM.md +30 -0
- aikit/templates/agent-secrets.ignore +19 -0
- aikit/templates/gitignore.private +160 -0
- aikit/templates/gitignore.team +139 -0
- aikit/tests/build_distribution.py +73 -0
- aikit/tests/docker_smoke.py +172 -0
- aikit/tests/fixtures/create-junction.ps1 +18 -0
- aikit/tests/fixtures/golden/claude-settings-local.json +58 -0
- aikit/tests/test_adapters.py +105 -0
- aikit/tests/test_aikit.py +79 -0
- aikit/tests/test_benchmark.py +241 -0
- aikit/tests/test_check_kit.py +203 -0
- aikit/tests/test_context.py +76 -0
- aikit/tests/test_context7.py +153 -0
- aikit/tests/test_context_selection.py +70 -0
- aikit/tests/test_distribution.py +181 -0
- aikit/tests/test_docker_image.py +63 -0
- aikit/tests/test_docker_test_usage.py +54 -0
- aikit/tests/test_doctor.py +148 -0
- aikit/tests/test_evaluate.py +66 -0
- aikit/tests/test_install.py +939 -0
- aikit/tests/test_learning.py +247 -0
- aikit/tests/test_memory.py +371 -0
- aikit/tests/test_metrics.py +209 -0
- aikit/tests/test_native.py +342 -0
- aikit/tests/test_onboarding.py +301 -0
- aikit/tests/test_router.py +336 -0
- aikit/tests/test_security_audit.py +237 -0
- aikit/tests/test_strix.py +245 -0
- aikit/tests/test_web_design.py +219 -0
- aikit/tests/wheel_smoke.py +104 -0
aikit/scripts/context.py
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"""Capture manifest evidence for an installed project's verified context."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import hashlib
|
|
6
|
+
import json
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
import re
|
|
9
|
+
import sys
|
|
10
|
+
|
|
11
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
12
|
+
import install
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
PROJECT_FACTS = "ai-kit/project.json"
|
|
16
|
+
DESIGN_WORKFLOWS = {"web-design": "web-design", "ui-review": "ui-review", "design-extract": "design-extract"}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def target_root(target: Path) -> Path:
|
|
20
|
+
target = target.absolute()
|
|
21
|
+
if install.is_link(target):
|
|
22
|
+
raise ValueError("Refusing a linked project root")
|
|
23
|
+
target = target.resolve()
|
|
24
|
+
if not target.is_dir():
|
|
25
|
+
raise ValueError("Project must be an existing directory")
|
|
26
|
+
return target
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def snapshot_plan(target: Path) -> dict:
|
|
30
|
+
"""Prepare, but do not write, a manifest-evidence snapshot."""
|
|
31
|
+
target = target_root(target)
|
|
32
|
+
path = install.safe_path(target, PROJECT_FACTS)
|
|
33
|
+
if not path.is_file():
|
|
34
|
+
raise ValueError("Missing ai-kit/project.json; install AI-KIT before recording context evidence")
|
|
35
|
+
value = install.read_json(path, {})
|
|
36
|
+
if type(value.get("schema")) is not int or value["schema"] != 1:
|
|
37
|
+
raise ValueError("Unsupported project.json schema; expected schema 1")
|
|
38
|
+
if not isinstance(value.get("modules"), list) or any(not isinstance(module, dict) for module in value["modules"]):
|
|
39
|
+
raise ValueError("project.json modules must be a list of objects")
|
|
40
|
+
facts = install.detect_facts(target)
|
|
41
|
+
updated = dict(value)
|
|
42
|
+
updated["evidence"] = install.project_fact_evidence(target, facts)
|
|
43
|
+
data = (json.dumps(updated, indent=2) + "\n").encode("utf-8")
|
|
44
|
+
old = path.read_bytes()
|
|
45
|
+
return {"target": str(target), "path": PROJECT_FACTS, "old": old, "data": data,
|
|
46
|
+
"changed": old != data, "detected_modules": [module["path"] for module in facts["modules"]]}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def apply_snapshot(plan: dict) -> bool:
|
|
50
|
+
target = Path(plan["target"])
|
|
51
|
+
path = install.safe_path(target, plan["path"])
|
|
52
|
+
actual = path.read_bytes() if path.exists() else None
|
|
53
|
+
if actual != plan["old"]:
|
|
54
|
+
raise ValueError("project.json changed after preview")
|
|
55
|
+
if not plan["changed"]:
|
|
56
|
+
return True
|
|
57
|
+
install.atomic_write(path, plan["data"])
|
|
58
|
+
return True
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def select_context(target: Path, *, path: str = ".", budget: int = 32000,
|
|
62
|
+
workflow: str | None = None) -> dict:
|
|
63
|
+
"""Select instructions by declared module scope; budgets cannot remove critical rules."""
|
|
64
|
+
target = target_root(target)
|
|
65
|
+
if type(budget) is not int or budget < 1:
|
|
66
|
+
raise ValueError("Context budget must be a positive byte count")
|
|
67
|
+
if workflow is not None and workflow not in DESIGN_WORKFLOWS:
|
|
68
|
+
raise ValueError("Unknown context workflow: " + str(workflow))
|
|
69
|
+
if path == ".":
|
|
70
|
+
relative = ""
|
|
71
|
+
else:
|
|
72
|
+
# Probe a file rather than accepting directory links through resolve().
|
|
73
|
+
install.safe_path(target, path.rstrip("/") + "/__context_probe__")
|
|
74
|
+
relative = path.strip("/")
|
|
75
|
+
if path.startswith(("/", "\\")):
|
|
76
|
+
raise ValueError("Module path must be relative")
|
|
77
|
+
facts = install.read_json(install.safe_path(target, PROJECT_FACTS), {})
|
|
78
|
+
if facts.get("schema") != 1 or not isinstance(facts.get("modules"), list):
|
|
79
|
+
raise ValueError("Missing/invalid project module map")
|
|
80
|
+
profiles, matching, scoped = set(), [], []
|
|
81
|
+
for module in facts["modules"]:
|
|
82
|
+
if not isinstance(module, dict) or not isinstance(module.get("path"), str) or not install.MODULE_PATH.fullmatch(module["path"]):
|
|
83
|
+
raise ValueError("Invalid declared module path")
|
|
84
|
+
scope = module["path"].strip("/")
|
|
85
|
+
if not scope or relative == scope or relative.startswith(scope + "/"):
|
|
86
|
+
if not isinstance(module.get("profiles"), list) or any(not isinstance(p, str) or not re.fullmatch(r"[A-Z][A-Z0-9_]*", p) for p in module["profiles"]):
|
|
87
|
+
raise ValueError("Invalid module profiles")
|
|
88
|
+
scoped.append(module)
|
|
89
|
+
# A nested declared module supersedes its parent's stack, while shared instructions still apply.
|
|
90
|
+
deepest = max((len(m["path"]) for m in scoped), default=0)
|
|
91
|
+
for module in scoped:
|
|
92
|
+
if len(module["path"]) == deepest:
|
|
93
|
+
matching.append(module["path"])
|
|
94
|
+
profiles.update(module["profiles"])
|
|
95
|
+
critical = ["AGENTS.md", "ai-kit/CORE.md", "ai-kit/settings.json", "PROJECT_CONTEXT.md",
|
|
96
|
+
"ai-kit/ENGINEERING.md", "ai-kit/SECURITY.md", "docs/DECISIONS.md"]
|
|
97
|
+
settings = install.read_json(install.safe_path(target, "ai-kit/settings.json"), {})
|
|
98
|
+
workflow_files = []
|
|
99
|
+
if workflow is not None:
|
|
100
|
+
if settings.get("web_design") is not True:
|
|
101
|
+
raise ValueError("Web Design workflow is disabled; install with --with-web-design first")
|
|
102
|
+
workflow_files = ["ai-kit/WEB-DESIGN.md", ".agents/skills/" + DESIGN_WORKFLOWS[workflow] + "/SKILL.md",
|
|
103
|
+
*install.OPTIONAL_DOCUMENTS["web-design"]]
|
|
104
|
+
if workflow == "ui-review":
|
|
105
|
+
workflow_files.append("ai-kit/web-design/REVIEW.md")
|
|
106
|
+
elif workflow == "design-extract":
|
|
107
|
+
workflow_files.append("ai-kit/web-design/TOOLS.md")
|
|
108
|
+
elif profiles & {"LARAVEL", "FILAMENT"}:
|
|
109
|
+
workflow_files.extend(["ai-kit/web-design/LARAVEL.md", ".agents/skills/laravel-blade-ui/SKILL.md"])
|
|
110
|
+
critical.extend(workflow_files)
|
|
111
|
+
if settings.get("conventions") == "owner" and profiles & {"LARAVEL", "FILAMENT"}:
|
|
112
|
+
critical.append("ai-kit/profiles/OWNER.md")
|
|
113
|
+
# ADRs preserve architectural constraints between sessions; do not truncate them for a budget.
|
|
114
|
+
adr_dir = target / "docs/adr"
|
|
115
|
+
if install.is_link(adr_dir):
|
|
116
|
+
raise ValueError("Refusing linked ADR directory")
|
|
117
|
+
if adr_dir.is_dir():
|
|
118
|
+
critical.extend(p.relative_to(target).as_posix() for p in sorted(adr_dir.glob("*.md")) if p.name != "0000-template.md")
|
|
119
|
+
if relative:
|
|
120
|
+
parts = relative.split("/")
|
|
121
|
+
critical.extend("/".join(parts[:i]) + "/AGENTS.md" for i in range(1, len(parts) + 1))
|
|
122
|
+
required_profiles = ["ai-kit/stacks/" + p + ".md" for p in sorted(profiles)]
|
|
123
|
+
critical.extend(required_profiles)
|
|
124
|
+
selected, missing, duplicates = [], [], []
|
|
125
|
+
hashes: dict[str, str] = {}
|
|
126
|
+
for name in dict.fromkeys(critical):
|
|
127
|
+
candidate = install.safe_path(target, name)
|
|
128
|
+
if not candidate.is_file():
|
|
129
|
+
# Nested instructions and project decision records are optional; Core/settings are required.
|
|
130
|
+
if name in {"AGENTS.md", "ai-kit/CORE.md", "ai-kit/settings.json", "PROJECT_CONTEXT.md",
|
|
131
|
+
"ai-kit/ENGINEERING.md", "ai-kit/SECURITY.md"} or name in required_profiles or name in workflow_files:
|
|
132
|
+
missing.append(name)
|
|
133
|
+
continue
|
|
134
|
+
data = candidate.read_bytes()
|
|
135
|
+
digest = hashlib.sha256(data).hexdigest()
|
|
136
|
+
if digest in hashes:
|
|
137
|
+
duplicates.append({"path": name, "same_as": hashes[digest]})
|
|
138
|
+
else:
|
|
139
|
+
hashes[digest] = name
|
|
140
|
+
selected.append({"path": name, "bytes": len(data), "sha256": digest, "critical": True})
|
|
141
|
+
if missing:
|
|
142
|
+
raise ValueError("Missing required context: " + ", ".join(missing))
|
|
143
|
+
total = sum(item["bytes"] for item in selected)
|
|
144
|
+
return {"selected": selected, "duplicates": duplicates, "modules": matching, "profiles": sorted(profiles),
|
|
145
|
+
"workflow": workflow,
|
|
146
|
+
"total_bytes": total, "budget_bytes": budget, "over_budget": total > budget,
|
|
147
|
+
"selection_complete": bool(matching),
|
|
148
|
+
"note": "Declared modules may still need bootstrap verification. Byte counts are not token counts. "
|
|
149
|
+
"Critical rules, matching profiles and architecture decisions are retained even above budget. "
|
|
150
|
+
"Duplicate critical files retain their scope/relative-link meaning. "
|
|
151
|
+
"Unrelated profiles and instruction history are excluded; no files are changed."}
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def main(argv: list[str] | None = None) -> int:
|
|
155
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
156
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
157
|
+
snapshot = sub.add_parser("snapshot", help="Preview or record current manifest evidence in project.json")
|
|
158
|
+
snapshot.add_argument("project", type=Path)
|
|
159
|
+
snapshot.add_argument("--apply", action="store_true")
|
|
160
|
+
select = sub.add_parser("select", help="Select module instructions and measure a byte budget without writing")
|
|
161
|
+
select.add_argument("project", type=Path)
|
|
162
|
+
select.add_argument("--path", default=".", help="Relative module/directory scope")
|
|
163
|
+
select.add_argument("--budget", type=int, default=32000, help="Byte budget; critical context is never dropped")
|
|
164
|
+
select.add_argument("--workflow", choices=sorted(DESIGN_WORKFLOWS),
|
|
165
|
+
help="Include the enabled design workflow and project design documents")
|
|
166
|
+
args = parser.parse_args(argv)
|
|
167
|
+
try:
|
|
168
|
+
if args.command == "select":
|
|
169
|
+
print(json.dumps(select_context(args.project, path=args.path, budget=args.budget,
|
|
170
|
+
workflow=args.workflow), indent=2))
|
|
171
|
+
return 0
|
|
172
|
+
plan = snapshot_plan(args.project)
|
|
173
|
+
print(json.dumps({"target": plan["target"], "preview": not args.apply, "changed": plan["changed"],
|
|
174
|
+
"path": plan["path"], "detected_modules": plan["detected_modules"]}, indent=2))
|
|
175
|
+
if args.apply:
|
|
176
|
+
apply_snapshot(plan)
|
|
177
|
+
return 0
|
|
178
|
+
except (OSError, ValueError, UnicodeError, json.JSONDecodeError) as exc:
|
|
179
|
+
print(f"Context snapshot refused: {exc}", file=sys.stderr)
|
|
180
|
+
return 1
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
if __name__ == "__main__":
|
|
184
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""Collect read-only Docker evidence; no pruning or volume/container deletion."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import json
|
|
6
|
+
import shutil
|
|
7
|
+
import subprocess
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def command(argv: list[str]) -> str:
|
|
11
|
+
result = subprocess.run(argv, capture_output=True, text=True, timeout=60, check=True)
|
|
12
|
+
return result.stdout
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def collect(project: str, *, docker: str = "docker", run=command, builder: str | None = None) -> dict:
|
|
16
|
+
context = run([docker, "context", "show"]).strip()
|
|
17
|
+
run([docker, "info", "--format", "{{.ServerVersion}}"])
|
|
18
|
+
names = run([docker, "volume", "ls", "--filter", "label=com.docker.compose.project=" + project,
|
|
19
|
+
"--format", "{{.Name}}"]).splitlines()
|
|
20
|
+
volumes = []
|
|
21
|
+
for name in names:
|
|
22
|
+
# Pass exact names as arguments, never as shell fragments.
|
|
23
|
+
data = json.loads(run([docker, "volume", "inspect", name]))[0]
|
|
24
|
+
labels = data.get("Labels") or {}
|
|
25
|
+
refs = run([docker, "ps", "-a", "--filter", "volume=" + name, "--format", "{{.ID}}"]).splitlines()
|
|
26
|
+
volumes.append({"name": name, "project_label": labels.get("com.docker.compose.project"),
|
|
27
|
+
"declared_disposable": labels.get("ai-kit.disposable") == "true",
|
|
28
|
+
"container_references": refs, "eligible_without_more_evidence": False})
|
|
29
|
+
report = {"context": context, "project": project, "volumes": volumes,
|
|
30
|
+
"disk_usage_raw": run([docker, "system", "df", "-v"]),
|
|
31
|
+
"measured_project_bytes": None,
|
|
32
|
+
"note": "Human-readable usage is evidence, not exact project ownership or a deletion plan."}
|
|
33
|
+
if builder:
|
|
34
|
+
report["builder"] = builder
|
|
35
|
+
report["build_cache_usage_raw"] = run([docker, "buildx", "du", "--builder", builder, "--format", "json"])
|
|
36
|
+
report["builder_project_ownership"] = "unverified"
|
|
37
|
+
return report
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def main(argv: list[str] | None = None) -> int:
|
|
41
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
42
|
+
parser.add_argument("--project", required=True)
|
|
43
|
+
parser.add_argument("--builder")
|
|
44
|
+
args = parser.parse_args(argv)
|
|
45
|
+
if not shutil.which("docker"):
|
|
46
|
+
parser.exit(1, "Docker client unavailable; install/configure the actual project runtime first.\n")
|
|
47
|
+
try:
|
|
48
|
+
print(json.dumps(collect(args.project, builder=args.builder), indent=2))
|
|
49
|
+
return 0
|
|
50
|
+
except (OSError, ValueError, subprocess.SubprocessError) as exc:
|
|
51
|
+
parser.exit(1, f"Docker evidence unavailable: {exc}\n")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
if __name__ == "__main__":
|
|
55
|
+
raise SystemExit(main())
|
aikit/scripts/doctor.py
ADDED
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
"""Offline health check for an AI-KIT-installed application project."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
from datetime import date
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
import re
|
|
10
|
+
import sys
|
|
11
|
+
from urllib.parse import unquote
|
|
12
|
+
|
|
13
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
14
|
+
import check_kit
|
|
15
|
+
import install
|
|
16
|
+
import router
|
|
17
|
+
|
|
18
|
+
BUDGETS = {"AGENTS.md": 2500, "ai-kit/CORE.md": 3000}
|
|
19
|
+
PROVIDER_FRESHNESS_DAYS = 90
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def examine(target: Path, *, root: Path = install.ROOT) -> dict:
|
|
23
|
+
errors: list[str] = []
|
|
24
|
+
warnings: list[str] = []
|
|
25
|
+
info: list[str] = []
|
|
26
|
+
try:
|
|
27
|
+
target = target.absolute()
|
|
28
|
+
if install.is_link(target):
|
|
29
|
+
raise ValueError("Refusing a linked target root")
|
|
30
|
+
target = target.resolve()
|
|
31
|
+
except ValueError as exc:
|
|
32
|
+
return {"target": str(target), "installed_version": None, "bundle_version": None,
|
|
33
|
+
"errors": [str(exc)], "warnings": warnings, "info": info}
|
|
34
|
+
report = {"target": str(target),
|
|
35
|
+
"installed_version": None,
|
|
36
|
+
"bundle_version": (root / "VERSION").read_text(encoding="utf-8").strip(),
|
|
37
|
+
"errors": errors, "warnings": warnings, "info": info}
|
|
38
|
+
if not target.is_dir():
|
|
39
|
+
errors.append("Target is not a directory")
|
|
40
|
+
return report
|
|
41
|
+
settings = {}
|
|
42
|
+
try:
|
|
43
|
+
settings = install.read_json(target / "ai-kit" / "settings.json", {})
|
|
44
|
+
install.validate_settings(settings)
|
|
45
|
+
except (OSError, ValueError, json.JSONDecodeError) as exc:
|
|
46
|
+
errors.append(f"Invalid installation settings: {exc}")
|
|
47
|
+
state_path = target / "ai-kit" / ".install-state.json"
|
|
48
|
+
if not state_path.is_file():
|
|
49
|
+
errors.append("No installer state found; is AI-KIT installed in this project?")
|
|
50
|
+
return report
|
|
51
|
+
try:
|
|
52
|
+
state = install.read_json(state_path, {})
|
|
53
|
+
install.validate_state(state, target)
|
|
54
|
+
except (OSError, ValueError, json.JSONDecodeError) as exc:
|
|
55
|
+
errors.append(f"Invalid installer state: {exc}")
|
|
56
|
+
return report
|
|
57
|
+
report["installed_version"] = state.get("accepted_version")
|
|
58
|
+
if report["installed_version"] != report["bundle_version"]:
|
|
59
|
+
info.append("Installed baseline differs from this bundle's VERSION.")
|
|
60
|
+
records = state.get("files", {})
|
|
61
|
+
modified, adapted = [], []
|
|
62
|
+
for relative in sorted(records):
|
|
63
|
+
try:
|
|
64
|
+
path = install.safe_path(target, relative)
|
|
65
|
+
except ValueError as exc:
|
|
66
|
+
errors.append(f"Unsafe recorded path {relative}: {exc}")
|
|
67
|
+
continue
|
|
68
|
+
if not path.is_file():
|
|
69
|
+
errors.append("Missing managed file: " + relative)
|
|
70
|
+
elif install.digest(path.read_bytes()) != records[relative]["installed_hash"]:
|
|
71
|
+
(adapted if records[relative].get("local_adaptation") else modified).append(relative)
|
|
72
|
+
warnings.extend(f"Modified after installation; rerun the installer to review: {r}" for r in modified)
|
|
73
|
+
info.extend(f"Locally adapted (accepted): {r}" for r in adapted)
|
|
74
|
+
for relative, entries in sorted(state.get("managed_json", {}).items()):
|
|
75
|
+
path = target / relative
|
|
76
|
+
if not path.is_file():
|
|
77
|
+
warnings.append(f"Missing settings file with AI-KIT entries: {relative}")
|
|
78
|
+
continue
|
|
79
|
+
try:
|
|
80
|
+
current = json.loads(path.read_text(encoding="utf-8-sig"))
|
|
81
|
+
if not isinstance(current, dict):
|
|
82
|
+
raise ValueError("expected a JSON object")
|
|
83
|
+
for key, values in sorted(entries.items()):
|
|
84
|
+
for value in values:
|
|
85
|
+
if install.merge_managed_json(current, {}, {key: [value]}) != current:
|
|
86
|
+
warnings.append(f"AI-KIT entry missing from {relative}: {key} {value}")
|
|
87
|
+
except (OSError, ValueError, UnicodeError) as exc:
|
|
88
|
+
warnings.append(f"Cannot read AI-KIT entries in {relative}: {exc}")
|
|
89
|
+
for relative in sorted(install.project_documents(settings)):
|
|
90
|
+
try:
|
|
91
|
+
document = install.safe_path(target, relative)
|
|
92
|
+
except (OSError, ValueError) as exc:
|
|
93
|
+
errors.append(f"Unsafe project document {relative}: {exc}")
|
|
94
|
+
continue
|
|
95
|
+
if not document.is_file():
|
|
96
|
+
warnings.append("Owned project document missing: " + relative)
|
|
97
|
+
for relative, limit in BUDGETS.items():
|
|
98
|
+
path = target / relative
|
|
99
|
+
if path.is_file() and path.stat().st_size > limit:
|
|
100
|
+
warnings.append(f"Always-loaded entry exceeds budget: {relative}")
|
|
101
|
+
candidates_dir = target / "ai-kit" / ".upstream-cache" / "candidates"
|
|
102
|
+
if candidates_dir.is_dir():
|
|
103
|
+
pending = list(candidates_dir.glob("*/*.metadata.json"))
|
|
104
|
+
if pending:
|
|
105
|
+
warnings.append(f"Pending conflict candidates need review: {len(pending)}")
|
|
106
|
+
providers_dir = target / "ai-kit" / "router" / "providers"
|
|
107
|
+
provider_configs = sorted(providers_dir.glob("*.json")) if providers_dir.is_dir() else []
|
|
108
|
+
for provider in provider_configs:
|
|
109
|
+
try:
|
|
110
|
+
config = json.loads(provider.read_text(encoding="utf-8"))
|
|
111
|
+
except ValueError as exc:
|
|
112
|
+
warnings.append(f"Invalid provider configuration {provider.name}: {exc}")
|
|
113
|
+
continue
|
|
114
|
+
user_supplied = config.get("user_supplied_models") is True
|
|
115
|
+
pending = config.get("verification_status") == "pending"
|
|
116
|
+
for role, spec in config.get("roles", {}).items():
|
|
117
|
+
if "effort" in spec and spec["effort"] not in config.get("supported_efforts", {}).get(role, []):
|
|
118
|
+
warnings.append(f"Unsupported configured effort: {provider.name} role {role}")
|
|
119
|
+
verified = config.get("verified_documentation_date")
|
|
120
|
+
try:
|
|
121
|
+
verified_date = date.fromisoformat(verified) if isinstance(verified, str) else None
|
|
122
|
+
except ValueError:
|
|
123
|
+
verified_date = None
|
|
124
|
+
if user_supplied:
|
|
125
|
+
info.append(f"Provider {provider.name} uses project-supplied local models")
|
|
126
|
+
elif pending:
|
|
127
|
+
info.append(f"Provider {provider.name} verification is pending")
|
|
128
|
+
elif verified_date is None:
|
|
129
|
+
warnings.append(f"Provider {provider.name} lacks a valid verified_documentation_date")
|
|
130
|
+
elif (date.today() - verified_date).days > PROVIDER_FRESHNESS_DAYS:
|
|
131
|
+
warnings.append(f"Provider {provider.name} verification is older than "
|
|
132
|
+
f"{PROVIDER_FRESHNESS_DAYS} days")
|
|
133
|
+
selection_path = target / "ai-kit" / "router" / "selection.json"
|
|
134
|
+
if selection_path.is_file():
|
|
135
|
+
try:
|
|
136
|
+
router.validate_selection(json.loads(selection_path.read_text(encoding="utf-8")))
|
|
137
|
+
except (OSError, ValueError, json.JSONDecodeError) as exc:
|
|
138
|
+
warnings.append(f"Invalid router selection: {exc}")
|
|
139
|
+
resolved_path = target / "ai-kit" / "router" / "resolved.json"
|
|
140
|
+
if resolved_path.is_file():
|
|
141
|
+
try:
|
|
142
|
+
resolved = json.loads(resolved_path.read_text(encoding="utf-8"))
|
|
143
|
+
if type(resolved.get("schema")) is not int or resolved["schema"] != 1:
|
|
144
|
+
raise ValueError("Unsupported resolved schema; expected schema 1")
|
|
145
|
+
if not isinstance(resolved.get("roles"), dict):
|
|
146
|
+
raise ValueError("Resolved roles must be an object")
|
|
147
|
+
except (ValueError, json.JSONDecodeError) as exc:
|
|
148
|
+
warnings.append(f"Invalid generated router config: {exc}")
|
|
149
|
+
project_json_path = target / "ai-kit" / "project.json"
|
|
150
|
+
project_value = None
|
|
151
|
+
if project_json_path.is_file():
|
|
152
|
+
try:
|
|
153
|
+
project_value = json.loads(project_json_path.read_text(encoding="utf-8"))
|
|
154
|
+
if type(project_value.get("schema")) is not int or project_value["schema"] != 1:
|
|
155
|
+
raise ValueError("Unsupported project schema; expected schema 1")
|
|
156
|
+
modules = project_value.get("modules")
|
|
157
|
+
if not isinstance(modules, list) or any(not isinstance(m, dict) for m in modules):
|
|
158
|
+
raise ValueError("Project modules must be a list of objects")
|
|
159
|
+
except (ValueError, json.JSONDecodeError) as exc:
|
|
160
|
+
warnings.append(f"Invalid project.json: {exc}")
|
|
161
|
+
context_path = target / "PROJECT_CONTEXT.md"
|
|
162
|
+
if project_value is not None and context_path.is_file():
|
|
163
|
+
try:
|
|
164
|
+
json_paths = project_module_paths(project_value)
|
|
165
|
+
context_paths = context_module_paths(context_path.read_text(encoding="utf-8"))
|
|
166
|
+
if json_paths != context_paths:
|
|
167
|
+
detail = []
|
|
168
|
+
if context_paths - json_paths:
|
|
169
|
+
detail.append("only in context: " + ", ".join(sorted(context_paths - json_paths)))
|
|
170
|
+
if json_paths - context_paths:
|
|
171
|
+
detail.append("only in project.json: " + ", ".join(sorted(json_paths - context_paths)))
|
|
172
|
+
warnings.append("project.json and PROJECT_CONTEXT.md module maps differ (" + "; ".join(detail) + ")")
|
|
173
|
+
except (OSError, UnicodeError) as exc:
|
|
174
|
+
warnings.append(f"Cannot compare module maps: {exc}")
|
|
175
|
+
if project_value is not None:
|
|
176
|
+
freshness_warnings(target, project_value, warnings, info)
|
|
177
|
+
warnings.extend(link_warnings(target))
|
|
178
|
+
return report
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def project_module_paths(value: dict) -> set[str]:
|
|
182
|
+
"""Module paths recorded in ai-kit/project.json."""
|
|
183
|
+
paths = set()
|
|
184
|
+
for module in value.get("modules", []):
|
|
185
|
+
if isinstance(module, dict) and isinstance(module.get("path"), str):
|
|
186
|
+
paths.add(module["path"])
|
|
187
|
+
return paths
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def context_module_paths(text: str) -> set[str]:
|
|
191
|
+
"""Module paths in the PROJECT_CONTEXT.md module-map table (canonical /-prefixed rows)."""
|
|
192
|
+
paths = set()
|
|
193
|
+
for line in text.splitlines():
|
|
194
|
+
stripped = line.strip()
|
|
195
|
+
if not stripped.startswith("| /"):
|
|
196
|
+
continue
|
|
197
|
+
cells = [cell.strip() for cell in stripped.strip("|").split("|")]
|
|
198
|
+
if cells and cells[0].startswith("/"):
|
|
199
|
+
paths.add(cells[0])
|
|
200
|
+
return paths
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def freshness_warnings(target: Path, project: dict, warnings: list[str], info: list[str]) -> None:
|
|
204
|
+
"""Compare recorded module facts and manifest evidence with current filesystem facts."""
|
|
205
|
+
try:
|
|
206
|
+
facts = install.detect_facts(target)
|
|
207
|
+
except (OSError, ValueError) as exc:
|
|
208
|
+
warnings.append(f"Cannot detect current project facts: {exc}")
|
|
209
|
+
return
|
|
210
|
+
detected = {module["path"]: module for module in facts["modules"]}
|
|
211
|
+
recorded = {module["path"]: module for module in project.get("modules", [])
|
|
212
|
+
if isinstance(module.get("path"), str)}
|
|
213
|
+
for path in sorted(set(detected) - set(recorded)):
|
|
214
|
+
warnings.append("Detected module missing from project facts: " + path)
|
|
215
|
+
for path in sorted(set(recorded) - set(detected)):
|
|
216
|
+
if isinstance(recorded[path].get("manifests"), list) and recorded[path]["manifests"]:
|
|
217
|
+
warnings.append("Recorded module no longer has detected manifests: " + path)
|
|
218
|
+
for path in sorted(set(detected) & set(recorded)):
|
|
219
|
+
expected_profiles = recorded[path].get("profiles")
|
|
220
|
+
if isinstance(expected_profiles, list) and sorted(expected_profiles) != detected[path]["profiles"]:
|
|
221
|
+
warnings.append("Detected profiles differ from project facts at " + path)
|
|
222
|
+
evidence = project.get("evidence")
|
|
223
|
+
if evidence is None:
|
|
224
|
+
info.append("No project fact evidence snapshot; run aikit context snapshot TARGET --apply after bootstrap.")
|
|
225
|
+
return
|
|
226
|
+
if not isinstance(evidence, dict) or evidence.get("schema") != 1 or not isinstance(evidence.get("modules"), list):
|
|
227
|
+
warnings.append("Invalid project fact evidence; rerun aikit context snapshot TARGET --apply.")
|
|
228
|
+
return
|
|
229
|
+
try:
|
|
230
|
+
current = install.project_fact_evidence(target, facts)
|
|
231
|
+
except (OSError, ValueError) as exc:
|
|
232
|
+
warnings.append(f"Cannot refresh project fact evidence: {exc}")
|
|
233
|
+
return
|
|
234
|
+
stored = {item.get("path"): item for item in evidence["modules"] if isinstance(item, dict) and
|
|
235
|
+
isinstance(item.get("path"), str) and isinstance(item.get("manifests"), dict)}
|
|
236
|
+
observed = {item["path"]: item for item in current["modules"]}
|
|
237
|
+
for path in sorted(set(stored) | set(observed)):
|
|
238
|
+
if path not in stored:
|
|
239
|
+
warnings.append("New detected manifest evidence at " + path + "; update context after review.")
|
|
240
|
+
elif path not in observed:
|
|
241
|
+
warnings.append("Recorded manifest evidence no longer detected at " + path + "; update context after review.")
|
|
242
|
+
else:
|
|
243
|
+
if stored[path]["manifests"] != observed[path]["manifests"]:
|
|
244
|
+
warnings.append("Manifest evidence changed at " + path + "; review context facts and commands.")
|
|
245
|
+
if stored[path].get("profiles") != observed[path].get("profiles"):
|
|
246
|
+
warnings.append("Detected profiles changed at " + path + "; review context facts.")
|
|
247
|
+
if stored[path].get("commands") != observed[path].get("commands"):
|
|
248
|
+
warnings.append("Detected commands changed at " + path + "; review context facts and commands.")
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def link_warnings(target: Path) -> list[str]:
|
|
252
|
+
"""Report broken relative links/anchors in the installed project's Markdown."""
|
|
253
|
+
problems = []
|
|
254
|
+
excluded = {".git", ".upstream-cache", "node_modules", "vendor", ".venv", "__pycache__"}
|
|
255
|
+
for directory, dirs, files in os.walk(target, followlinks=False):
|
|
256
|
+
dirs[:] = [d for d in dirs if d not in excluded and not install.is_link(Path(directory) / d)]
|
|
257
|
+
if Path(directory) == target / "ai-kit":
|
|
258
|
+
dirs[:] = [d for d in dirs if d != ".metrics"]
|
|
259
|
+
for name in files:
|
|
260
|
+
path = Path(directory) / name
|
|
261
|
+
if path.suffix != ".md":
|
|
262
|
+
continue
|
|
263
|
+
relative = path.relative_to(target).as_posix()
|
|
264
|
+
text = path.read_text(encoding="utf-8")
|
|
265
|
+
for link in check_kit.links(text):
|
|
266
|
+
if re.match(r"^[a-z][a-z0-9+.-]*:", link, re.I):
|
|
267
|
+
continue
|
|
268
|
+
filename, _, anchor = unquote(link).partition("#")
|
|
269
|
+
destination = (path.parent / filename).resolve() if filename else path.resolve()
|
|
270
|
+
try:
|
|
271
|
+
destination.relative_to(target.resolve())
|
|
272
|
+
except ValueError:
|
|
273
|
+
problems.append(f"{relative}: link escapes project: {link}")
|
|
274
|
+
continue
|
|
275
|
+
if not destination.exists():
|
|
276
|
+
problems.append(f"{relative}: missing link: {link}")
|
|
277
|
+
elif anchor and destination.is_file() and destination.suffix == ".md":
|
|
278
|
+
if anchor not in check_kit.headings(destination.read_text(encoding="utf-8")):
|
|
279
|
+
problems.append(f"{relative}: missing heading: {link}")
|
|
280
|
+
return problems
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def main(argv: list[str] | None = None) -> int:
|
|
284
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
285
|
+
parser.add_argument("target", type=Path)
|
|
286
|
+
parser.add_argument("--root", type=Path, default=install.ROOT,
|
|
287
|
+
help="Reference distribution used for the bundle version comparison")
|
|
288
|
+
parser.add_argument("--fix", action="store_true",
|
|
289
|
+
help="Re-run the installer to restore missing/baseline files; local edits are preserved")
|
|
290
|
+
args = parser.parse_args(argv)
|
|
291
|
+
if args.fix:
|
|
292
|
+
try:
|
|
293
|
+
plan = install.build_plan(args.target, root=args.root)
|
|
294
|
+
applied = install.apply_plan(plan)
|
|
295
|
+
report = examine(args.target, root=args.root)
|
|
296
|
+
except (OSError, ValueError, json.JSONDecodeError) as exc:
|
|
297
|
+
print(f"Fix refused: {exc}", file=sys.stderr)
|
|
298
|
+
return 1
|
|
299
|
+
print(json.dumps({"fix": {"writes": sorted(plan["actions"]),
|
|
300
|
+
"conflicts": sorted(plan["conflicts"]), "applied": applied},
|
|
301
|
+
"report": report}, indent=2))
|
|
302
|
+
return 2 if (plan["conflicts"] or report["errors"]) else 0
|
|
303
|
+
try:
|
|
304
|
+
report = examine(args.target, root=args.root)
|
|
305
|
+
except OSError as exc:
|
|
306
|
+
print(f"Examination failed: {exc}", file=sys.stderr)
|
|
307
|
+
return 1
|
|
308
|
+
print(json.dumps(report, indent=2))
|
|
309
|
+
return 1 if report["errors"] else 0
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
if __name__ == "__main__":
|
|
313
|
+
raise SystemExit(main())
|