@iamnishankhan/opencode-kit-grok 1.0.0
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.
- package/bin/cli.js +46 -0
- package/kit/.opencode/AGENTS.md +392 -0
- package/kit/.opencode/README.md +71 -0
- package/kit/.opencode/USAGE.md +405 -0
- package/kit/.opencode/agents/backend-specialist.md +278 -0
- package/kit/.opencode/agents/code-archaeologist.md +114 -0
- package/kit/.opencode/agents/database-architect.md +233 -0
- package/kit/.opencode/agents/debugger.md +234 -0
- package/kit/.opencode/agents/devops-engineer.md +252 -0
- package/kit/.opencode/agents/documentation-writer.md +111 -0
- package/kit/.opencode/agents/explorer-agent.md +83 -0
- package/kit/.opencode/agents/frontend-specialist.md +606 -0
- package/kit/.opencode/agents/game-developer.md +169 -0
- package/kit/.opencode/agents/mobile-developer.md +385 -0
- package/kit/.opencode/agents/orchestrator.md +213 -0
- package/kit/.opencode/agents/penetration-tester.md +197 -0
- package/kit/.opencode/agents/performance-optimizer.md +194 -0
- package/kit/.opencode/agents/product-manager.md +120 -0
- package/kit/.opencode/agents/product-owner.md +103 -0
- package/kit/.opencode/agents/project-planner.md +418 -0
- package/kit/.opencode/agents/qa-automation-engineer.md +113 -0
- package/kit/.opencode/agents/security-auditor.md +179 -0
- package/kit/.opencode/agents/seo-specialist.md +119 -0
- package/kit/.opencode/agents/test-engineer.md +169 -0
- package/kit/.opencode/commands/brainstorm.md +113 -0
- package/kit/.opencode/commands/coordinate.md +72 -0
- package/kit/.opencode/commands/create.md +63 -0
- package/kit/.opencode/commands/debug.md +102 -0
- package/kit/.opencode/commands/deploy.md +175 -0
- package/kit/.opencode/commands/enhance.md +62 -0
- package/kit/.opencode/commands/orchestrate.md +241 -0
- package/kit/.opencode/commands/plan.md +89 -0
- package/kit/.opencode/commands/preview.md +80 -0
- package/kit/.opencode/commands/remember.md +67 -0
- package/kit/.opencode/commands/status.md +86 -0
- package/kit/.opencode/commands/test.md +143 -0
- package/kit/.opencode/commands/verify.md +71 -0
- package/kit/.opencode/memory/MEMORY.md +6 -0
- package/kit/.opencode/memory/feedback-history.md +9 -0
- package/kit/.opencode/memory/project-conventions.md +16 -0
- package/kit/.opencode/memory/tech-decisions.md +10 -0
- package/kit/.opencode/memory/user-preferences.md +9 -0
- package/kit/.opencode/opencode.json +15 -0
- package/kit/.opencode/scripts/README.md +95 -0
- package/kit/.opencode/scripts/auto_preview.py +149 -0
- package/kit/.opencode/scripts/checklist.py +75 -0
- package/kit/.opencode/scripts/component_registry.py +238 -0
- package/kit/.opencode/scripts/dependency_graph.py +73 -0
- package/kit/.opencode/scripts/generate_manifest.py +44 -0
- package/kit/.opencode/scripts/session_manager.py +120 -0
- package/kit/.opencode/scripts/tests/test_toolkit.py +238 -0
- package/kit/.opencode/scripts/validate_kit.py +397 -0
- package/kit/.opencode/scripts/validation_runner.py +216 -0
- package/kit/.opencode/scripts/verify_all.py +85 -0
- package/kit/.opencode/skills/api-patterns/SKILL.md +83 -0
- package/kit/.opencode/skills/api-patterns/api-style.md +42 -0
- package/kit/.opencode/skills/api-patterns/auth.md +24 -0
- package/kit/.opencode/skills/api-patterns/documentation.md +26 -0
- package/kit/.opencode/skills/api-patterns/graphql.md +41 -0
- package/kit/.opencode/skills/api-patterns/rate-limiting.md +31 -0
- package/kit/.opencode/skills/api-patterns/response.md +37 -0
- package/kit/.opencode/skills/api-patterns/rest.md +40 -0
- package/kit/.opencode/skills/api-patterns/scripts/api_validator.py +211 -0
- package/kit/.opencode/skills/api-patterns/security-testing.md +122 -0
- package/kit/.opencode/skills/api-patterns/trpc.md +41 -0
- package/kit/.opencode/skills/api-patterns/versioning.md +22 -0
- package/kit/.opencode/skills/app-builder/SKILL.md +78 -0
- package/kit/.opencode/skills/app-builder/agent-coordination.md +71 -0
- package/kit/.opencode/skills/app-builder/feature-building.md +53 -0
- package/kit/.opencode/skills/app-builder/project-detection.md +45 -0
- package/kit/.opencode/skills/app-builder/scaffolding.md +110 -0
- package/kit/.opencode/skills/app-builder/tech-stack.md +41 -0
- package/kit/.opencode/skills/app-builder/templates/SKILL.md +39 -0
- package/kit/.opencode/skills/app-builder/templates/astro-static/TEMPLATE.md +78 -0
- package/kit/.opencode/skills/app-builder/templates/chrome-extension/TEMPLATE.md +96 -0
- package/kit/.opencode/skills/app-builder/templates/cli-tool/TEMPLATE.md +88 -0
- package/kit/.opencode/skills/app-builder/templates/electron-desktop/TEMPLATE.md +97 -0
- package/kit/.opencode/skills/app-builder/templates/express-api/TEMPLATE.md +89 -0
- package/kit/.opencode/skills/app-builder/templates/flutter-app/TEMPLATE.md +93 -0
- package/kit/.opencode/skills/app-builder/templates/monorepo-turborepo/TEMPLATE.md +97 -0
- package/kit/.opencode/skills/app-builder/templates/nextjs-fullstack/TEMPLATE.md +126 -0
- package/kit/.opencode/skills/app-builder/templates/nextjs-saas/TEMPLATE.md +125 -0
- package/kit/.opencode/skills/app-builder/templates/nextjs-static/TEMPLATE.md +174 -0
- package/kit/.opencode/skills/app-builder/templates/nuxt-app/TEMPLATE.md +127 -0
- package/kit/.opencode/skills/app-builder/templates/python-fastapi/TEMPLATE.md +94 -0
- package/kit/.opencode/skills/app-builder/templates/react-native-app/TEMPLATE.md +121 -0
- package/kit/.opencode/skills/architecture/SKILL.md +57 -0
- package/kit/.opencode/skills/architecture/context-discovery.md +43 -0
- package/kit/.opencode/skills/architecture/examples.md +94 -0
- package/kit/.opencode/skills/architecture/pattern-selection.md +68 -0
- package/kit/.opencode/skills/architecture/patterns-reference.md +50 -0
- package/kit/.opencode/skills/architecture/trade-off-analysis.md +77 -0
- package/kit/.opencode/skills/bash-linux/SKILL.md +201 -0
- package/kit/.opencode/skills/batch-operations/SKILL.md +105 -0
- package/kit/.opencode/skills/behavioral-modes/SKILL.md +253 -0
- package/kit/.opencode/skills/brainstorming/SKILL.md +178 -0
- package/kit/.opencode/skills/brainstorming/dynamic-questioning.md +350 -0
- package/kit/.opencode/skills/clean-code/SKILL.md +201 -0
- package/kit/.opencode/skills/code-review-checklist/SKILL.md +111 -0
- package/kit/.opencode/skills/code-review-graph/SKILL.md +305 -0
- package/kit/.opencode/skills/context-compression/SKILL.md +145 -0
- package/kit/.opencode/skills/coordinator-mode/SKILL.md +208 -0
- package/kit/.opencode/skills/database-design/SKILL.md +54 -0
- package/kit/.opencode/skills/database-design/database-selection.md +43 -0
- package/kit/.opencode/skills/database-design/indexing.md +39 -0
- package/kit/.opencode/skills/database-design/migrations.md +48 -0
- package/kit/.opencode/skills/database-design/optimization.md +36 -0
- package/kit/.opencode/skills/database-design/orm-selection.md +30 -0
- package/kit/.opencode/skills/database-design/schema-design.md +56 -0
- package/kit/.opencode/skills/database-design/scripts/schema_validator.py +172 -0
- package/kit/.opencode/skills/deployment-procedures/SKILL.md +243 -0
- package/kit/.opencode/skills/design-spec/SKILL.md +185 -0
- package/kit/.opencode/skills/design-spec/collection.md +106 -0
- package/kit/.opencode/skills/documentation-templates/SKILL.md +196 -0
- package/kit/.opencode/skills/frontend-architecture/SKILL.md +245 -0
- package/kit/.opencode/skills/frontend-design/SKILL.md +1222 -0
- package/kit/.opencode/skills/frontend-design/redesign.md +178 -0
- package/kit/.opencode/skills/frontend-design/scripts/accessibility_checker.py +145 -0
- package/kit/.opencode/skills/frontend-design/scripts/ux_audit.py +722 -0
- package/kit/.opencode/skills/frontend-design/style-brutalist.md +92 -0
- package/kit/.opencode/skills/frontend-design/style-minimalist.md +85 -0
- package/kit/.opencode/skills/game-development/2d-games/SKILL.md +118 -0
- package/kit/.opencode/skills/game-development/3d-games/SKILL.md +134 -0
- package/kit/.opencode/skills/game-development/SKILL.md +169 -0
- package/kit/.opencode/skills/game-development/game-art/SKILL.md +184 -0
- package/kit/.opencode/skills/game-development/game-audio/SKILL.md +189 -0
- package/kit/.opencode/skills/game-development/game-design/SKILL.md +128 -0
- package/kit/.opencode/skills/game-development/mobile-games/SKILL.md +107 -0
- package/kit/.opencode/skills/game-development/multiplayer/SKILL.md +131 -0
- package/kit/.opencode/skills/game-development/pc-games/SKILL.md +143 -0
- package/kit/.opencode/skills/game-development/vr-ar/SKILL.md +122 -0
- package/kit/.opencode/skills/game-development/web-games/SKILL.md +149 -0
- package/kit/.opencode/skills/geo-fundamentals/SKILL.md +158 -0
- package/kit/.opencode/skills/geo-fundamentals/scripts/geo_checker.py +273 -0
- package/kit/.opencode/skills/i18n-localization/SKILL.md +156 -0
- package/kit/.opencode/skills/i18n-localization/scripts/i18n_checker.py +223 -0
- package/kit/.opencode/skills/intelligent-routing/SKILL.md +337 -0
- package/kit/.opencode/skills/lint-and-validate/SKILL.md +47 -0
- package/kit/.opencode/skills/lint-and-validate/scripts/lint_runner.py +184 -0
- package/kit/.opencode/skills/lint-and-validate/scripts/type_coverage.py +245 -0
- package/kit/.opencode/skills/mcp-builder/SKILL.md +205 -0
- package/kit/.opencode/skills/memory-system/SKILL.md +181 -0
- package/kit/.opencode/skills/mobile-design/SKILL.md +396 -0
- package/kit/.opencode/skills/mobile-design/decision-trees.md +516 -0
- package/kit/.opencode/skills/mobile-design/mobile-backend.md +491 -0
- package/kit/.opencode/skills/mobile-design/mobile-color-system.md +420 -0
- package/kit/.opencode/skills/mobile-design/mobile-debugging.md +122 -0
- package/kit/.opencode/skills/mobile-design/mobile-design-thinking.md +357 -0
- package/kit/.opencode/skills/mobile-design/mobile-navigation.md +458 -0
- package/kit/.opencode/skills/mobile-design/mobile-performance.md +767 -0
- package/kit/.opencode/skills/mobile-design/mobile-testing.md +356 -0
- package/kit/.opencode/skills/mobile-design/mobile-typography.md +433 -0
- package/kit/.opencode/skills/mobile-design/platform-android.md +666 -0
- package/kit/.opencode/skills/mobile-design/platform-ios.md +561 -0
- package/kit/.opencode/skills/mobile-design/scripts/mobile_audit.py +670 -0
- package/kit/.opencode/skills/mobile-design/touch-psychology.md +537 -0
- package/kit/.opencode/skills/nextjs-react-expert/1-async-eliminating-waterfalls.md +351 -0
- package/kit/.opencode/skills/nextjs-react-expert/2-bundle-bundle-size-optimization.md +240 -0
- package/kit/.opencode/skills/nextjs-react-expert/3-server-server-side-performance.md +490 -0
- package/kit/.opencode/skills/nextjs-react-expert/4-client-client-side-data-fetching.md +264 -0
- package/kit/.opencode/skills/nextjs-react-expert/5-rerender-re-render-optimization.md +581 -0
- package/kit/.opencode/skills/nextjs-react-expert/6-rendering-rendering-performance.md +432 -0
- package/kit/.opencode/skills/nextjs-react-expert/7-js-javascript-performance.md +684 -0
- package/kit/.opencode/skills/nextjs-react-expert/8-advanced-advanced-patterns.md +150 -0
- package/kit/.opencode/skills/nextjs-react-expert/9-cache-components.md +103 -0
- package/kit/.opencode/skills/nextjs-react-expert/SKILL.md +293 -0
- package/kit/.opencode/skills/nextjs-react-expert/scripts/convert_rules.py +222 -0
- package/kit/.opencode/skills/nextjs-react-expert/scripts/react_performance_checker.py +281 -0
- package/kit/.opencode/skills/nodejs-best-practices/SKILL.md +336 -0
- package/kit/.opencode/skills/parallel-agents/SKILL.md +192 -0
- package/kit/.opencode/skills/performance-profiling/SKILL.md +145 -0
- package/kit/.opencode/skills/performance-profiling/scripts/bundle_analyzer.py +120 -0
- package/kit/.opencode/skills/performance-profiling/scripts/lighthouse_audit.py +108 -0
- package/kit/.opencode/skills/plan-writing/SKILL.md +153 -0
- package/kit/.opencode/skills/powershell-windows/SKILL.md +169 -0
- package/kit/.opencode/skills/python-patterns/SKILL.md +443 -0
- package/kit/.opencode/skills/red-team-tactics/SKILL.md +201 -0
- package/kit/.opencode/skills/rust-pro/SKILL.md +177 -0
- package/kit/.opencode/skills/seo-fundamentals/SKILL.md +131 -0
- package/kit/.opencode/skills/seo-fundamentals/scripts/seo_checker.py +219 -0
- package/kit/.opencode/skills/server-management/SKILL.md +163 -0
- package/kit/.opencode/skills/simplify-code/SKILL.md +131 -0
- package/kit/.opencode/skills/skillify/SKILL.md +115 -0
- package/kit/.opencode/skills/systematic-debugging/SKILL.md +111 -0
- package/kit/.opencode/skills/tailwind-patterns/SKILL.md +271 -0
- package/kit/.opencode/skills/tdd-workflow/SKILL.md +151 -0
- package/kit/.opencode/skills/testing-patterns/SKILL.md +180 -0
- package/kit/.opencode/skills/testing-patterns/scripts/test_runner.py +219 -0
- package/kit/.opencode/skills/verify-changes/SKILL.md +127 -0
- package/kit/.opencode/skills/vulnerability-scanner/SKILL.md +278 -0
- package/kit/.opencode/skills/vulnerability-scanner/checklists.md +124 -0
- package/kit/.opencode/skills/vulnerability-scanner/scripts/dependency_analyzer.py +144 -0
- package/kit/.opencode/skills/vulnerability-scanner/scripts/security_scan.py +410 -0
- package/kit/.opencode/skills/web-design-guidelines/SKILL.md +56 -0
- package/kit/.opencode/skills/webapp-testing/SKILL.md +189 -0
- package/kit/.opencode/skills/webapp-testing/scripts/playwright_runner.py +123 -0
- package/lib/init.js +61 -0
- package/package.json +21 -0
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Self-validate an OpenCode kit installation.
|
|
3
|
+
|
|
4
|
+
Checks machine-readable configuration, versioned frontmatter contracts,
|
|
5
|
+
cross references, generated registries, local Markdown links, memory schema,
|
|
6
|
+
Python syntax, and architecture inventory counts.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import argparse
|
|
11
|
+
import ast
|
|
12
|
+
import json
|
|
13
|
+
import re
|
|
14
|
+
import sys
|
|
15
|
+
from dataclasses import dataclass
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from urllib.parse import unquote
|
|
18
|
+
|
|
19
|
+
try:
|
|
20
|
+
import yaml # type: ignore
|
|
21
|
+
except ImportError: # pragma: no cover - optional enhancement
|
|
22
|
+
yaml = None
|
|
23
|
+
|
|
24
|
+
from component_registry import (
|
|
25
|
+
build_lock,
|
|
26
|
+
build_manifest,
|
|
27
|
+
canonical_json,
|
|
28
|
+
is_semver,
|
|
29
|
+
normalize_list,
|
|
30
|
+
version_satisfies,
|
|
31
|
+
)
|
|
32
|
+
from dependency_graph import render as render_dependency_graph
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class Finding:
|
|
37
|
+
severity: str
|
|
38
|
+
code: str
|
|
39
|
+
file: str
|
|
40
|
+
line: int
|
|
41
|
+
message: str
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
REQUIRED_FIELDS = {
|
|
45
|
+
"agent": {"name", "description", "tools", "model", "skills", "version"},
|
|
46
|
+
"skill": {"name", "description", "when_to_use", "allowed-tools", "version"},
|
|
47
|
+
"workflow": {
|
|
48
|
+
"name",
|
|
49
|
+
"description",
|
|
50
|
+
"version",
|
|
51
|
+
"requires_agents",
|
|
52
|
+
"requires_skills",
|
|
53
|
+
"artifact_outputs",
|
|
54
|
+
},
|
|
55
|
+
"rule": {"name", "trigger", "version", "priority"},
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def add(findings: list[Finding], severity: str, code: str, path: Path, message: str, line: int = 1) -> None:
|
|
60
|
+
findings.append(Finding(severity, code, path.as_posix(), line, message))
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def extract_frontmatter(path: Path) -> tuple[str | None, int]:
|
|
64
|
+
text = path.read_text("utf-8", errors="replace")
|
|
65
|
+
if not text.startswith("---\n"):
|
|
66
|
+
return None, 1
|
|
67
|
+
end = text.find("\n---\n", 4)
|
|
68
|
+
if end < 0:
|
|
69
|
+
return None, 1
|
|
70
|
+
return text[4:end], 1
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def fallback_frontmatter(raw: str) -> dict[str, object]:
|
|
74
|
+
data: dict[str, object] = {}
|
|
75
|
+
for line in raw.splitlines():
|
|
76
|
+
if not line or line[0].isspace() or line.lstrip().startswith("#"):
|
|
77
|
+
continue
|
|
78
|
+
match = re.match(r"^([A-Za-z0-9_-]+):\s*(.*)$", line)
|
|
79
|
+
if not match:
|
|
80
|
+
continue
|
|
81
|
+
key, value = match.groups()
|
|
82
|
+
data[key] = value.strip().strip('"\'')
|
|
83
|
+
return data
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def parse_frontmatter(path: Path, findings: list[Finding]) -> dict[str, object] | None:
|
|
87
|
+
raw, _ = extract_frontmatter(path)
|
|
88
|
+
if raw is None:
|
|
89
|
+
add(findings, "error", "frontmatter.missing", path, "Missing or unterminated YAML frontmatter")
|
|
90
|
+
return None
|
|
91
|
+
if yaml is None:
|
|
92
|
+
return fallback_frontmatter(raw)
|
|
93
|
+
try:
|
|
94
|
+
data = yaml.safe_load(raw)
|
|
95
|
+
except Exception as exc:
|
|
96
|
+
line = int(getattr(getattr(exc, "problem_mark", None), "line", 0)) + 2
|
|
97
|
+
add(findings, "error", "frontmatter.invalid_yaml", path, str(exc), line)
|
|
98
|
+
return None
|
|
99
|
+
if not isinstance(data, dict):
|
|
100
|
+
add(findings, "error", "frontmatter.not_mapping", path, "Frontmatter must be a YAML mapping")
|
|
101
|
+
return None
|
|
102
|
+
return data
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def validate_json(root: Path, findings: list[Finding]) -> None:
|
|
106
|
+
for path in root.rglob("*.json"):
|
|
107
|
+
if "__pycache__" in path.parts:
|
|
108
|
+
continue
|
|
109
|
+
try:
|
|
110
|
+
json.loads(path.read_text("utf-8"))
|
|
111
|
+
except (OSError, json.JSONDecodeError) as exc:
|
|
112
|
+
line = int(getattr(exc, "lineno", 1))
|
|
113
|
+
add(findings, "error", "json.invalid", path.relative_to(root), str(exc), line)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def validate_kit_version(root: Path, findings: list[Finding]) -> None:
|
|
117
|
+
version_path = root / "VERSION"
|
|
118
|
+
if not version_path.is_file():
|
|
119
|
+
add(findings, "error", "version.missing", Path("VERSION"), "VERSION is missing")
|
|
120
|
+
return
|
|
121
|
+
value = version_path.read_text("utf-8").strip()
|
|
122
|
+
if not re.fullmatch(r"\d{4}\.\d{1,2}\.\d{1,2}", value):
|
|
123
|
+
add(findings, "error", "version.invalid_calver", Path("VERSION"), f"Expected YYYY.M.D CalVer, got {value!r}")
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def validate_frontmatter(
|
|
127
|
+
root: Path, findings: list[Finding]
|
|
128
|
+
) -> tuple[
|
|
129
|
+
dict[str, dict[str, object]],
|
|
130
|
+
dict[str, dict[str, object]],
|
|
131
|
+
dict[str, dict[str, object]],
|
|
132
|
+
dict[str, dict[str, object]],
|
|
133
|
+
]:
|
|
134
|
+
agents: dict[str, dict[str, object]] = {}
|
|
135
|
+
skills: dict[str, dict[str, object]] = {}
|
|
136
|
+
workflows: dict[str, dict[str, object]] = {}
|
|
137
|
+
rules: dict[str, dict[str, object]] = {}
|
|
138
|
+
groups = (
|
|
139
|
+
("agent", sorted((root / "agent").glob("*.md"))),
|
|
140
|
+
("skill", sorted((root / "skills").glob("*/SKILL.md"))),
|
|
141
|
+
("workflow", sorted((root / "workflows").glob("*.md"))),
|
|
142
|
+
("rule", sorted((root / "rules").glob("*.md"))),
|
|
143
|
+
)
|
|
144
|
+
registries = {"agent": agents, "skill": skills, "workflow": workflows, "rule": rules}
|
|
145
|
+
for kind, paths in groups:
|
|
146
|
+
names_seen: set[str] = set()
|
|
147
|
+
for path in paths:
|
|
148
|
+
rel = path.relative_to(root)
|
|
149
|
+
data = parse_frontmatter(path, findings)
|
|
150
|
+
if data is None:
|
|
151
|
+
continue
|
|
152
|
+
missing = REQUIRED_FIELDS[kind] - set(data)
|
|
153
|
+
for field in sorted(missing):
|
|
154
|
+
add(findings, "error", "frontmatter.required_field", rel, f"Missing required field: {field}")
|
|
155
|
+
name = str(data.get("name", ""))
|
|
156
|
+
expected = path.parent.name if kind == "skill" else path.stem
|
|
157
|
+
if name != expected:
|
|
158
|
+
add(findings, "error", "frontmatter.name_mismatch", rel, f"name={name!r}, expected {expected!r}")
|
|
159
|
+
if name:
|
|
160
|
+
if name in names_seen:
|
|
161
|
+
add(findings, "error", "frontmatter.duplicate_name", rel, f"Duplicate {kind} name: {name}")
|
|
162
|
+
names_seen.add(name)
|
|
163
|
+
version = data.get("version")
|
|
164
|
+
if version is not None and not is_semver(str(version)):
|
|
165
|
+
add(findings, "error", "frontmatter.invalid_semver", rel, f"Invalid SemVer: {version!r}")
|
|
166
|
+
registries[kind][expected] = data
|
|
167
|
+
return agents, skills, workflows, rules
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def validate_references(
|
|
171
|
+
root: Path,
|
|
172
|
+
agents: dict[str, dict[str, object]],
|
|
173
|
+
skills: dict[str, dict[str, object]],
|
|
174
|
+
workflows: dict[str, dict[str, object]],
|
|
175
|
+
findings: list[Finding],
|
|
176
|
+
) -> None:
|
|
177
|
+
for agent_name, data in agents.items():
|
|
178
|
+
path = Path("agent") / f"{agent_name}.md"
|
|
179
|
+
for skill in normalize_list(data.get("skills")):
|
|
180
|
+
if skill not in skills:
|
|
181
|
+
add(findings, "error", "reference.unknown_skill", path, f"Agent references missing skill: {skill}")
|
|
182
|
+
|
|
183
|
+
for workflow_name, data in workflows.items():
|
|
184
|
+
path = Path("workflows") / f"{workflow_name}.md"
|
|
185
|
+
for agent in normalize_list(data.get("requires_agents")):
|
|
186
|
+
if agent not in agents:
|
|
187
|
+
add(findings, "error", "reference.unknown_agent", path, f"Workflow references missing agent: {agent}")
|
|
188
|
+
for skill in normalize_list(data.get("requires_skills")):
|
|
189
|
+
if skill not in skills:
|
|
190
|
+
add(findings, "error", "reference.unknown_skill", path, f"Workflow references missing skill: {skill}")
|
|
191
|
+
if not normalize_list(data.get("artifact_outputs")):
|
|
192
|
+
add(findings, "warning", "workflow.no_outputs", path, "Workflow declares no artifact outputs")
|
|
193
|
+
|
|
194
|
+
script_pattern = re.compile(r'["\']((?:skills|scripts)/[^"\']+?\.py)["\']')
|
|
195
|
+
for path in (root / "scripts").glob("*.py"):
|
|
196
|
+
text = path.read_text("utf-8", errors="replace")
|
|
197
|
+
for match in script_pattern.finditer(text):
|
|
198
|
+
if any(char in match.group(1) for char in "*?["):
|
|
199
|
+
continue
|
|
200
|
+
target = root / match.group(1)
|
|
201
|
+
if not target.is_file():
|
|
202
|
+
line = text.count("\n", 0, match.start()) + 1
|
|
203
|
+
add(findings, "error", "reference.missing_script", path.relative_to(root), f"Referenced script does not exist: {match.group(1)}", line)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def validate_manifest(root: Path, findings: list[Finding]) -> None:
|
|
207
|
+
manifest_path = root / "manifest.json"
|
|
208
|
+
lock_path = root / "manifest.lock.json"
|
|
209
|
+
if not manifest_path.is_file():
|
|
210
|
+
add(findings, "error", "manifest.missing", Path("manifest.json"), "Run .opencode/scripts/generate_manifest.py")
|
|
211
|
+
return
|
|
212
|
+
if not lock_path.is_file():
|
|
213
|
+
add(findings, "error", "manifest.lock_missing", Path("manifest.lock.json"), "Run .opencode/scripts/generate_manifest.py")
|
|
214
|
+
return
|
|
215
|
+
try:
|
|
216
|
+
actual_manifest = json.loads(manifest_path.read_text("utf-8"))
|
|
217
|
+
actual_lock = json.loads(lock_path.read_text("utf-8"))
|
|
218
|
+
except json.JSONDecodeError:
|
|
219
|
+
return
|
|
220
|
+
try:
|
|
221
|
+
expected_manifest = build_manifest(root)
|
|
222
|
+
expected_lock = build_lock(root, expected_manifest)
|
|
223
|
+
except Exception as exc:
|
|
224
|
+
add(findings, "error", "manifest.generation_failed", Path("manifest.json"), str(exc))
|
|
225
|
+
return
|
|
226
|
+
if actual_manifest != expected_manifest:
|
|
227
|
+
add(findings, "error", "manifest.stale", Path("manifest.json"), "Registry differs from component frontmatter; regenerate it")
|
|
228
|
+
if actual_lock != expected_lock:
|
|
229
|
+
add(findings, "error", "manifest.lock_stale", Path("manifest.lock.json"), "Lock differs from current component files; regenerate it")
|
|
230
|
+
|
|
231
|
+
skill_versions = {name: data["version"] for name, data in expected_manifest["skills"].items()}
|
|
232
|
+
for agent_name, agent in expected_manifest["agents"].items():
|
|
233
|
+
for skill_name, constraint in agent["requires"]["skills"].items():
|
|
234
|
+
version = skill_versions.get(skill_name)
|
|
235
|
+
if version and not version_satisfies(version, constraint):
|
|
236
|
+
add(
|
|
237
|
+
findings,
|
|
238
|
+
"error",
|
|
239
|
+
"manifest.incompatible_dependency",
|
|
240
|
+
Path(agent["path"]),
|
|
241
|
+
f"{skill_name} {version} does not satisfy {constraint}",
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def validate_generated_docs(root: Path, findings: list[Finding]) -> None:
|
|
246
|
+
path = root / "DEPENDENCY_GRAPH.md"
|
|
247
|
+
if not path.is_file():
|
|
248
|
+
add(findings, "error", "graph.missing", Path("DEPENDENCY_GRAPH.md"), "Run .opencode/scripts/dependency_graph.py")
|
|
249
|
+
return
|
|
250
|
+
expected = render_dependency_graph(root)
|
|
251
|
+
if path.read_text("utf-8") != expected:
|
|
252
|
+
add(findings, "error", "graph.stale", Path("DEPENDENCY_GRAPH.md"), "Dependency graph is stale")
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def validate_markdown_links(root: Path, findings: list[Finding]) -> None:
|
|
256
|
+
pattern = re.compile(r"!?\[[^\]]*\]\(([^)]+)\)")
|
|
257
|
+
for path in root.rglob("*.md"):
|
|
258
|
+
text = path.read_text("utf-8", errors="replace")
|
|
259
|
+
for match in pattern.finditer(text):
|
|
260
|
+
raw = match.group(1).strip()
|
|
261
|
+
if not raw:
|
|
262
|
+
continue
|
|
263
|
+
target_text = raw.split()[0].strip("<>")
|
|
264
|
+
if target_text.startswith(("#", "http://", "https://", "mailto:", "tel:", "data:")):
|
|
265
|
+
continue
|
|
266
|
+
target_text = unquote(target_text.split("#", 1)[0])
|
|
267
|
+
if not target_text:
|
|
268
|
+
continue
|
|
269
|
+
target = (path.parent / target_text).resolve()
|
|
270
|
+
try:
|
|
271
|
+
target.relative_to(root.resolve())
|
|
272
|
+
except ValueError:
|
|
273
|
+
continue
|
|
274
|
+
if path.relative_to(root).as_posix() == "skills/documentation-templates/SKILL.md" and target_text.startswith("./docs/"):
|
|
275
|
+
continue
|
|
276
|
+
if not target.exists():
|
|
277
|
+
line = text.count("\n", 0, match.start()) + 1
|
|
278
|
+
add(findings, "error", "markdown.missing_link", path.relative_to(root), f"Missing local target: {target_text}", line)
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def validate_memory(root: Path, findings: list[Finding]) -> None:
|
|
282
|
+
memory_root = root / "memory"
|
|
283
|
+
index = memory_root / "MEMORY.md"
|
|
284
|
+
required_topics = {"project-conventions.md", "user-preferences.md", "tech-decisions.md", "feedback-history.md"}
|
|
285
|
+
if not index.is_file():
|
|
286
|
+
add(findings, "error", "memory.index_missing", Path(".opencode/memory/MEMORY.md"), "Memory index is missing")
|
|
287
|
+
return
|
|
288
|
+
lines = index.read_text("utf-8").splitlines()
|
|
289
|
+
if len(lines) > 200:
|
|
290
|
+
add(findings, "error", "memory.index_too_large", Path(".opencode/memory/MEMORY.md"), f"Memory index has {len(lines)} lines; maximum is 200")
|
|
291
|
+
entry_pattern = re.compile(r"^- \[(user|feedback|project|reference)\] .+ → ([A-Za-z0-9._-]+\.md)$")
|
|
292
|
+
for line_no, line in enumerate(lines, 1):
|
|
293
|
+
if not line.startswith("- ["):
|
|
294
|
+
continue
|
|
295
|
+
match = entry_pattern.fullmatch(line)
|
|
296
|
+
if not match:
|
|
297
|
+
add(findings, "error", "memory.invalid_entry", Path(".opencode/memory/MEMORY.md"), "Invalid memory index entry", line_no)
|
|
298
|
+
continue
|
|
299
|
+
target = memory_root / match.group(2)
|
|
300
|
+
if not target.is_file():
|
|
301
|
+
add(findings, "error", "memory.missing_topic", Path(".opencode/memory/MEMORY.md"), f"Missing topic file: {match.group(2)}", line_no)
|
|
302
|
+
for topic in sorted(required_topics):
|
|
303
|
+
path = memory_root / topic
|
|
304
|
+
if not path.is_file():
|
|
305
|
+
add(findings, "error", "memory.required_topic", Path("memory") / topic, "Required memory topic is missing")
|
|
306
|
+
continue
|
|
307
|
+
data = parse_frontmatter(path, findings)
|
|
308
|
+
if data is None:
|
|
309
|
+
continue
|
|
310
|
+
missing = {"type", "created", "updated"} - set(data)
|
|
311
|
+
for field in sorted(missing):
|
|
312
|
+
add(findings, "error", "memory.required_field", path.relative_to(root), f"Missing required field: {field}")
|
|
313
|
+
if str(data.get("type", "")) not in {"user", "feedback", "project", "reference"}:
|
|
314
|
+
add(findings, "error", "memory.invalid_type", path.relative_to(root), f"Invalid memory type: {data.get('type')!r}")
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def validate_python(root: Path, findings: list[Finding]) -> None:
|
|
318
|
+
for path in root.rglob("*.py"):
|
|
319
|
+
if "__pycache__" in path.parts:
|
|
320
|
+
continue
|
|
321
|
+
try:
|
|
322
|
+
ast.parse(path.read_text("utf-8"), filename=str(path))
|
|
323
|
+
except (OSError, SyntaxError) as exc:
|
|
324
|
+
add(findings, "error", "python.syntax", path.relative_to(root), str(exc), int(getattr(exc, "lineno", 1) or 1))
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def validate_architecture_counts(root: Path, findings: list[Finding]) -> None:
|
|
328
|
+
path = root / "ARCHITECTURE.md"
|
|
329
|
+
if not path.is_file():
|
|
330
|
+
add(findings, "error", "architecture.missing", Path("ARCHITECTURE.md"), "ARCHITECTURE.md is missing")
|
|
331
|
+
return
|
|
332
|
+
text = path.read_text("utf-8", errors="replace")
|
|
333
|
+
actual = {
|
|
334
|
+
"agents": len(list((root / "agent").glob("*.md"))),
|
|
335
|
+
"skills": len(list((root / "skills").glob("*/SKILL.md"))),
|
|
336
|
+
"workflows": len(list((root / "workflows").glob("*.md"))),
|
|
337
|
+
"skill_scripts": len(list((root / "skills").glob("*/scripts/*.py"))),
|
|
338
|
+
}
|
|
339
|
+
patterns = {
|
|
340
|
+
"agents": r"\*\*Total Agents\*\*\s*\|\s*(\d+)",
|
|
341
|
+
"skills": r"\*\*Total Skills\*\*\s*\|\s*(\d+)",
|
|
342
|
+
"workflows": r"\*\*Total Workflows\*\*\s*\|\s*(\d+)",
|
|
343
|
+
"skill_scripts": r"\*\*Total Skill Scripts\*\*\s*\|\s*(\d+)",
|
|
344
|
+
}
|
|
345
|
+
for key, pattern in patterns.items():
|
|
346
|
+
match = re.search(pattern, text)
|
|
347
|
+
if not match:
|
|
348
|
+
add(findings, "error", "architecture.count_missing", Path("ARCHITECTURE.md"), f"Missing inventory field for {key}")
|
|
349
|
+
elif int(match.group(1)) != actual[key]:
|
|
350
|
+
add(findings, "error", "architecture.count_mismatch", Path("ARCHITECTURE.md"), f"{key}: documented {match.group(1)}, actual {actual[key]}")
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def validate(root: Path) -> list[Finding]:
|
|
354
|
+
findings: list[Finding] = []
|
|
355
|
+
validate_json(root, findings)
|
|
356
|
+
validate_kit_version(root, findings)
|
|
357
|
+
agents, skills, workflows, _rules = validate_frontmatter(root, findings)
|
|
358
|
+
validate_references(root, agents, skills, workflows, findings)
|
|
359
|
+
validate_manifest(root, findings)
|
|
360
|
+
validate_generated_docs(root, findings)
|
|
361
|
+
validate_markdown_links(root, findings)
|
|
362
|
+
validate_memory(root, findings)
|
|
363
|
+
validate_python(root, findings)
|
|
364
|
+
validate_architecture_counts(root, findings)
|
|
365
|
+
return findings
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def main() -> int:
|
|
369
|
+
parser = argparse.ArgumentParser(description="Validate OpenCode kit structure and cross references")
|
|
370
|
+
parser.add_argument("path", nargs="?", default=None, help="Path to .agents (defaults to this toolkit)")
|
|
371
|
+
parser.add_argument("--json", action="store_true", dest="as_json")
|
|
372
|
+
args = parser.parse_args()
|
|
373
|
+
root = Path(args.path).resolve() if args.path else Path(__file__).resolve().parents[1]
|
|
374
|
+
if not root.is_dir():
|
|
375
|
+
parser.error(f"Toolkit directory does not exist: {root}")
|
|
376
|
+
findings = validate(root)
|
|
377
|
+
errors = [item for item in findings if item.severity == "error"]
|
|
378
|
+
warnings = [item for item in findings if item.severity == "warning"]
|
|
379
|
+
payload = {
|
|
380
|
+
"toolkit": str(root),
|
|
381
|
+
"passed": not errors,
|
|
382
|
+
"summary": {"errors": len(errors), "warnings": len(warnings)},
|
|
383
|
+
"findings": [item.__dict__ for item in findings],
|
|
384
|
+
}
|
|
385
|
+
if args.as_json:
|
|
386
|
+
print(json.dumps(payload, indent=2, ensure_ascii=False))
|
|
387
|
+
else:
|
|
388
|
+
print(f"OpenCode kit self-validation: {root}")
|
|
389
|
+
for item in findings:
|
|
390
|
+
print(f"[{item.severity.upper()}] {item.file}:{item.line} {item.code} - {item.message}")
|
|
391
|
+
print(f"Summary: {len(errors)} error(s), {len(warnings)} warning(s)")
|
|
392
|
+
print("[PASS] Toolkit is structurally valid." if not errors else "[FAIL] Toolkit validation failed.")
|
|
393
|
+
return 0 if not errors else 1
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
if __name__ == "__main__":
|
|
397
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Shared process runner for OpenCode kit validation entry points."""
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import subprocess
|
|
8
|
+
import sys
|
|
9
|
+
from dataclasses import asdict, dataclass, field
|
|
10
|
+
from datetime import datetime, timezone
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Iterable, Literal
|
|
13
|
+
|
|
14
|
+
Target = Literal["project", "url", "none"]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(frozen=True)
|
|
18
|
+
class CheckSpec:
|
|
19
|
+
name: str
|
|
20
|
+
script: str
|
|
21
|
+
category: str
|
|
22
|
+
target: Target = "project"
|
|
23
|
+
required: bool = False
|
|
24
|
+
args: tuple[str, ...] = ()
|
|
25
|
+
timeout: int = 300
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class CheckResult:
|
|
30
|
+
name: str
|
|
31
|
+
category: str
|
|
32
|
+
status: Literal["passed", "failed", "skipped", "error"]
|
|
33
|
+
required: bool
|
|
34
|
+
duration_seconds: float = 0.0
|
|
35
|
+
command: list[str] = field(default_factory=list)
|
|
36
|
+
stdout: str = ""
|
|
37
|
+
stderr: str = ""
|
|
38
|
+
reason: str = ""
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def passed(self) -> bool:
|
|
42
|
+
return self.status in {"passed", "skipped"}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class Console:
|
|
46
|
+
def __init__(self) -> None:
|
|
47
|
+
enabled = sys.stdout.isatty() and "NO_COLOR" not in os.environ
|
|
48
|
+
self.bold = "\033[1m" if enabled else ""
|
|
49
|
+
self.cyan = "\033[96m" if enabled else ""
|
|
50
|
+
self.green = "\033[92m" if enabled else ""
|
|
51
|
+
self.yellow = "\033[93m" if enabled else ""
|
|
52
|
+
self.red = "\033[91m" if enabled else ""
|
|
53
|
+
self.end = "\033[0m" if enabled else ""
|
|
54
|
+
|
|
55
|
+
def header(self, text: str) -> None:
|
|
56
|
+
line = "=" * 70
|
|
57
|
+
print(f"\n{self.bold}{self.cyan}{line}\n{text.center(70)}\n{line}{self.end}\n")
|
|
58
|
+
|
|
59
|
+
def passed(self, text: str) -> None:
|
|
60
|
+
print(f"{self.green}[PASS] {text}{self.end}")
|
|
61
|
+
|
|
62
|
+
def skipped(self, text: str) -> None:
|
|
63
|
+
print(f"{self.yellow}[SKIP] {text}{self.end}")
|
|
64
|
+
|
|
65
|
+
def failed(self, text: str) -> None:
|
|
66
|
+
print(f"{self.red}[FAIL] {text}{self.end}")
|
|
67
|
+
|
|
68
|
+
def info(self, text: str) -> None:
|
|
69
|
+
print(text)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
CONSOLE = Console()
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def locate_toolkit_root(project: Path, caller_file: str) -> Path:
|
|
76
|
+
"""Find the toolkit independently from the project being audited."""
|
|
77
|
+
for candidate in (project / ".opencode", project / ".agents", project / ".agent"):
|
|
78
|
+
if (candidate / "skills").is_dir() and (candidate / "scripts").is_dir():
|
|
79
|
+
return candidate
|
|
80
|
+
embedded = Path(caller_file).resolve().parents[1]
|
|
81
|
+
if (embedded / "skills").is_dir() and (embedded / "scripts").is_dir():
|
|
82
|
+
return embedded
|
|
83
|
+
raise FileNotFoundError(
|
|
84
|
+
"OpenCode kit root was not found. Expected <project>/.opencode, <project>/.agents, <project>/.agent, "
|
|
85
|
+
"or a runner located inside the toolkit."
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _command_for(spec: CheckSpec, script: Path, project: Path, url: str | None) -> list[str] | None:
|
|
90
|
+
command = [sys.executable, str(script)]
|
|
91
|
+
if spec.target == "project":
|
|
92
|
+
command.append(str(project))
|
|
93
|
+
elif spec.target == "url":
|
|
94
|
+
if not url:
|
|
95
|
+
return None
|
|
96
|
+
command.append(url)
|
|
97
|
+
command.extend(spec.args)
|
|
98
|
+
return command
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def run_check(spec: CheckSpec, toolkit_root: Path, project: Path, url: str | None) -> CheckResult:
|
|
102
|
+
script = toolkit_root / spec.script
|
|
103
|
+
if not script.is_file():
|
|
104
|
+
status = "failed" if spec.required else "skipped"
|
|
105
|
+
reason = f"Script not found: {script}"
|
|
106
|
+
result = CheckResult(spec.name, spec.category, status, spec.required, reason=reason)
|
|
107
|
+
(CONSOLE.failed if status == "failed" else CONSOLE.skipped)(f"{spec.name}: {reason}")
|
|
108
|
+
return result
|
|
109
|
+
|
|
110
|
+
command = _command_for(spec, script, project, url)
|
|
111
|
+
if command is None:
|
|
112
|
+
reason = "URL not provided"
|
|
113
|
+
CONSOLE.skipped(f"{spec.name}: {reason}")
|
|
114
|
+
return CheckResult(spec.name, spec.category, "skipped", spec.required, reason=reason)
|
|
115
|
+
|
|
116
|
+
started = datetime.now(timezone.utc)
|
|
117
|
+
try:
|
|
118
|
+
proc = subprocess.run(
|
|
119
|
+
command,
|
|
120
|
+
cwd=project,
|
|
121
|
+
capture_output=True,
|
|
122
|
+
text=True,
|
|
123
|
+
timeout=spec.timeout,
|
|
124
|
+
check=False,
|
|
125
|
+
)
|
|
126
|
+
duration = (datetime.now(timezone.utc) - started).total_seconds()
|
|
127
|
+
status = "passed" if proc.returncode == 0 else "failed"
|
|
128
|
+
result = CheckResult(
|
|
129
|
+
spec.name, spec.category, status, spec.required, duration,
|
|
130
|
+
command, proc.stdout, proc.stderr,
|
|
131
|
+
reason="" if status == "passed" else f"Exit code {proc.returncode}",
|
|
132
|
+
)
|
|
133
|
+
if status == "passed":
|
|
134
|
+
CONSOLE.passed(f"{spec.name} ({duration:.1f}s)")
|
|
135
|
+
else:
|
|
136
|
+
CONSOLE.failed(f"{spec.name} ({duration:.1f}s, exit {proc.returncode})")
|
|
137
|
+
_print_failure_output(result)
|
|
138
|
+
return result
|
|
139
|
+
except subprocess.TimeoutExpired as exc:
|
|
140
|
+
duration = (datetime.now(timezone.utc) - started).total_seconds()
|
|
141
|
+
result = CheckResult(
|
|
142
|
+
spec.name, spec.category, "error", spec.required, duration, command,
|
|
143
|
+
_decode_timeout(exc.stdout), _decode_timeout(exc.stderr),
|
|
144
|
+
f"Timed out after {spec.timeout}s",
|
|
145
|
+
)
|
|
146
|
+
CONSOLE.failed(f"{spec.name}: {result.reason}")
|
|
147
|
+
return result
|
|
148
|
+
except OSError as exc:
|
|
149
|
+
duration = (datetime.now(timezone.utc) - started).total_seconds()
|
|
150
|
+
result = CheckResult(spec.name, spec.category, "error", spec.required, duration, command, reason=str(exc))
|
|
151
|
+
CONSOLE.failed(f"{spec.name}: {exc}")
|
|
152
|
+
return result
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _decode_timeout(value: str | bytes | None) -> str:
|
|
156
|
+
if value is None:
|
|
157
|
+
return ""
|
|
158
|
+
return value.decode(errors="replace") if isinstance(value, bytes) else value
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _print_failure_output(result: CheckResult, limit: int = 1600) -> None:
|
|
162
|
+
combined = "\n".join(part.strip() for part in (result.stdout, result.stderr) if part.strip())
|
|
163
|
+
if combined:
|
|
164
|
+
print(combined[-limit:])
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def execute_suite(
|
|
168
|
+
specs: Iterable[CheckSpec],
|
|
169
|
+
toolkit_root: Path,
|
|
170
|
+
project: Path,
|
|
171
|
+
url: str | None,
|
|
172
|
+
stop_on_fail: bool = False,
|
|
173
|
+
) -> list[CheckResult]:
|
|
174
|
+
results: list[CheckResult] = []
|
|
175
|
+
current_category = ""
|
|
176
|
+
for spec in specs:
|
|
177
|
+
if spec.category != current_category:
|
|
178
|
+
current_category = spec.category
|
|
179
|
+
CONSOLE.header(current_category)
|
|
180
|
+
result = run_check(spec, toolkit_root, project, url)
|
|
181
|
+
results.append(result)
|
|
182
|
+
if stop_on_fail and result.status in {"failed", "error"}:
|
|
183
|
+
break
|
|
184
|
+
return results
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def suite_success(results: Iterable[CheckResult]) -> bool:
|
|
188
|
+
return all(result.status not in {"failed", "error"} for result in results)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def print_summary(title: str, results: list[CheckResult], started: datetime) -> bool:
|
|
192
|
+
CONSOLE.header(title)
|
|
193
|
+
counts = {status: sum(r.status == status for r in results) for status in ("passed", "failed", "error", "skipped")}
|
|
194
|
+
duration = (datetime.now(timezone.utc) - started).total_seconds()
|
|
195
|
+
print(f"Duration: {duration:.1f}s")
|
|
196
|
+
print(f"Checks: {len(results)} | Passed: {counts['passed']} | Failed: {counts['failed']} | Errors: {counts['error']} | Skipped: {counts['skipped']}")
|
|
197
|
+
for result in results:
|
|
198
|
+
marker = {"passed": "PASS", "failed": "FAIL", "error": "ERROR", "skipped": "SKIP"}[result.status]
|
|
199
|
+
detail = f" - {result.reason}" if result.reason else ""
|
|
200
|
+
print(f"[{marker}] {result.category} / {result.name}{detail}")
|
|
201
|
+
ok = suite_success(results)
|
|
202
|
+
(CONSOLE.passed if ok else CONSOLE.failed)("Validation completed successfully" if ok else "Validation found blocking failures")
|
|
203
|
+
return ok
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def write_report(path: Path, project: Path, toolkit_root: Path, results: list[CheckResult], started: datetime) -> None:
|
|
207
|
+
payload = {
|
|
208
|
+
"project": str(project),
|
|
209
|
+
"toolkit_root": str(toolkit_root),
|
|
210
|
+
"started_at": started.isoformat(),
|
|
211
|
+
"finished_at": datetime.now(timezone.utc).isoformat(),
|
|
212
|
+
"success": suite_success(results),
|
|
213
|
+
"results": [asdict(result) for result in results],
|
|
214
|
+
}
|
|
215
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
216
|
+
path.write_text(json.dumps(payload, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
|