diff --git a/.ai/environment/tools.ai.yaml b/.ai/environment/tools.ai.yaml new file mode 100644 index 0000000..66be9aa --- /dev/null +++ b/.ai/environment/tools.ai.yaml @@ -0,0 +1,62 @@ +schema_version: 1 +generated_at_utc: "2026-03-21T04:01:27Z" +generated_from: ".ai/environment/tools.raw.yaml" +generator: "scripts/generate-ai-environment.py" +platform: + family: "wsl-linux" + os: "Linux" + distro: "Ubuntu 24.04.4 LTS" + shell: "bash" +capabilities: + dotnet: true + python: true + node: true + bun: true + docker: true + fast_search: true + json_cli: true +tool_selection: + search: + preferred: "rg" + fallback: "grep" + use_for: "Repository text search." + json: + preferred: "jq" + fallback: "python3" + use_for: "Inspecting or transforming JSON command output." + shell: + preferred: "bash" + fallback: "sh" + use_for: "Repository shell scripts and command execution." + scripting: + preferred: "python3" + fallback: "bash" + use_for: "Non-trivial local automation and helper scripts." + docs_package_manager: + preferred: "bun" + fallback: "npm" + use_for: "Installing and previewing the docs site." + build_and_test: + preferred: "dotnet" + fallback: "unavailable" + use_for: "Build, test, restore, and solution validation." +python: + available: true + helper_packages: + requests: true + rich: true + openai: false + tiktoken: false + pydantic: false + pytest: false +preferences: + prefer_project_listed_tools: true + prefer_python_for_non_trivial_automation: true + avoid_unlisted_system_tools: true +rules: + - "Use rg instead of grep for repository search when rg is available." + - "Use jq for JSON inspection; fall back to python3 if jq is unavailable." + - "Prefer python3 over complex bash for non-trivial scripting when python3 is available." + - "Use bun for docs preview workflows when bun is available; otherwise fall back to npm." + - "Use dotnet for repository build and test workflows." + - "Do not assume unrelated system tools are part of the supported project environment." diff --git a/.ai/environment/tools.raw.yaml b/.ai/environment/tools.raw.yaml new file mode 100644 index 0000000..6f1c959 --- /dev/null +++ b/.ai/environment/tools.raw.yaml @@ -0,0 +1,89 @@ +schema_version: 1 +generated_at_utc: "2026-03-21T04:00:19Z" +generator: "scripts/collect-dev-environment.sh" + +platform: + os: "Linux" + distro: "Ubuntu 24.04.4 LTS" + version: "24.04" + kernel: "5.15.167.4-microsoft-standard-WSL2" + wsl: true + wsl_version: "2.4.13" + shell: "bash" + +required_runtimes: + dotnet: + installed: true + version: "10.0.104" + path: "/usr/bin/dotnet" + purpose: "Builds and tests the GFramework solution." + python3: + installed: true + version: "Python 3.12.3" + path: "/usr/bin/python3" + purpose: "Runs local automation and environment collection scripts." + node: + installed: true + version: "v20.20.1" + path: "/usr/bin/node" + purpose: "Provides the JavaScript runtime used by docs tooling." + bun: + installed: true + version: "1.3.10" + path: "/root/.bun/bin/bun" + purpose: "Installs and previews the VitePress documentation site." + +required_tools: + git: + installed: true + version: "git version 2.43.0" + path: "/usr/bin/git" + purpose: "Source control and patch review." + bash: + installed: true + version: "GNU bash, version 5.2.21(1)-release (x86_64-pc-linux-gnu)" + path: "/usr/bin/bash" + purpose: "Executes repository scripts and shell automation." + rg: + installed: true + version: "ripgrep 15.1.0 (rev af60c2de9d)" + path: "/root/.bun/install/global/node_modules/@openai/codex-linux-x64/vendor/x86_64-unknown-linux-musl/path/rg" + purpose: "Fast text search across the repository." + jq: + installed: true + version: "jq-1.7" + path: "/usr/bin/jq" + purpose: "Inspecting and transforming JSON outputs." + +project_tools: + docker: + installed: true + version: "Docker version 29.2.1, build a5c7197" + path: "/usr/bin/docker" + purpose: "Runs MegaLinter and other containerized validation tools." + +python_packages: + requests: + installed: true + version: "2.31.0" + purpose: "Simple HTTP calls in local helper scripts." + rich: + installed: true + version: "13.7.1" + purpose: "Readable CLI output for local Python helpers." + openai: + installed: false + version: "not-installed" + purpose: "Optional scripted access to OpenAI APIs." + tiktoken: + installed: false + version: "not-installed" + purpose: "Optional token counting for prompt and context inspection." + pydantic: + installed: false + version: "not-installed" + purpose: "Optional typed config and schema validation for helper scripts." + pytest: + installed: false + version: "not-installed" + purpose: "Optional lightweight testing for Python helper scripts." diff --git a/.gitignore b/.gitignore index 8aea2c4..315acfb 100644 --- a/.gitignore +++ b/.gitignore @@ -12,4 +12,6 @@ opencode.json .omc/ docs/.omc/ docs/.vitepress/cache/ -local-plan/ \ No newline at end of file +local-plan/ +# tool +.venv/ \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md index 89402df..8f4234f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,6 +4,13 @@ This document is the single source of truth for coding behavior in this reposito All AI agents and contributors must follow these rules when writing, reviewing, or modifying code in `GFramework`. +## Environment Capability Inventory + +- Before choosing runtimes or CLI tools, read `@.ai/environment/tools.ai.yaml`. +- Use `@.ai/environment/tools.raw.yaml` only when you need the full collected facts behind the AI-facing hints. +- Prefer the project-relevant tools listed there instead of assuming every installed system tool is fair game. +- If the real environment differs from the inventory, use the project-relevant installed tool and report the mismatch. + ## Commenting Rules (MUST) All generated or modified code MUST include clear and meaningful comments where required by the rules below. diff --git a/docs/zh-CN/contributing.md b/docs/zh-CN/contributing.md index eeb1a2f..d9b92e1 100644 --- a/docs/zh-CN/contributing.md +++ b/docs/zh-CN/contributing.md @@ -118,6 +118,10 @@ GFramework 是一个开源的游戏开发框架,我们欢迎所有形式的贡 ## 开发环境设置 +当前推荐的项目相关环境、CLI 与 AI 可用工具清单请查看: + +- [开发环境能力清单](./contributor/development-environment.md) + ### 前置要求 - **.NET SDK**:8.0、9.0 或 10.0 diff --git a/docs/zh-CN/contributor/development-environment.md b/docs/zh-CN/contributor/development-environment.md new file mode 100644 index 0000000..99962af --- /dev/null +++ b/docs/zh-CN/contributor/development-environment.md @@ -0,0 +1,96 @@ +# 开发环境能力清单 + +这份文档只记录对 `GFramework` 当前开发和 AI 协作真正有用的环境能力,不收录与本项目无关的系统工具。 + +如果某个工具没有出现在这里,默认表示它对当前仓库不是必需项,AI 也不应因为“系统里刚好装了”就优先使用它。 + +## 当前环境基线 + +当前仓库验证基线是: + +- **运行环境**:WSL2 +- **发行版**:Ubuntu 24.04 LTS +- **Shell**:`bash` + +机器可读的环境数据分成两层: + +- `GFramework/.ai/environment/tools.raw.yaml`:完整事实采集 +- `GFramework/.ai/environment/tools.ai.yaml`:给 AI 看的精简决策提示 + +AI 应优先读取 `tools.ai.yaml`,只有在需要追溯完整事实时才查看 `tools.raw.yaml`。 + +## 当前项目需要的运行时 + +| 工具 | 是否需要 | 在 GFramework 中的用途 | +|-----------|------|---------------------------------| +| `dotnet` | 必需 | 构建、测试、打包整个解决方案 | +| `python3` | 推荐 | 运行本地辅助脚本、环境采集和轻量自动化 | +| `node` | 推荐 | 作为文档工具链的 JavaScript 运行时 | +| `bun` | 推荐 | 安装并预览 `docs/` 下的 VitePress 文档站点 | + +## 当前项目需要的命令行工具 + +| 工具 | 是否需要 | 在 GFramework 中的用途 | +|----------|------|-----------------------------------------------| +| `git` | 必需 | 提交代码、查看 diff、审查变更 | +| `bash` | 必需 | 执行仓库脚本,例如 `scripts/validate-csharp-naming.sh` | +| `rg` | 必需 | 在仓库中快速搜索代码和文档 | +| `jq` | 推荐 | 处理 JSON 输出,便于本地脚本和 AI 做结构化检查 | +| `docker` | 可选 | 运行 MegaLinter 等容器化检查工具 | + +这里只保留和当前仓库直接相关的 CLI。像 `kubectl`、`terraform`、`helm`、`java`、数据库客户端等工具,即使系统已安装,也不进入正式清单。 + +## Python 包 + +Python 包只记录两类内容: + +- 当前环境里已经存在、对开发辅助有价值的包 +- 明确对 AI/脚本化开发有帮助、后续可能会安装的包 + +| 包 | 当前状态 | 用途 | +|------------|---------|---------------------| +| `requests` | 当前环境已安装 | 用于简单 HTTP 调用和脚本集成 | +| `rich` | 当前环境已安装 | 用于更易读的终端输出 | +| `openai` | 当前环境可选 | 用于脚本化调用 OpenAI API | +| `tiktoken` | 当前环境可选 | 用于 token 估算和上下文检查 | +| `pydantic` | 当前环境可选 | 用于结构化配置和模式校验 | +| `pytest` | 当前环境可选 | 用于 Python 辅助脚本的小型测试 | + +如果某个 Python 包与当前仓库没有直接关系,就不要加入清单。 + +## AI 使用约定 + +AI 在这个仓库里应优先使用: + +- `rg` 做文本搜索 +- `jq` 做 JSON 检查 +- `bash` 执行仓库脚本 +- `dotnet` 做构建和测试 +- `bun` 做文档预览 +- `python3 + requests` 做轻量本地辅助脚本 + +AI 不应直接把原始探测数据当成决策规则;应以 `tools.ai.yaml` 中的推荐和 fallback 为准。如果确实需要引入新工具,应先更新环境清单,再在任务中使用。 + +## 如何刷新环境清单 + +使用仓库脚本先采集原始环境,再生成 AI 版本: + +```bash +# 输出原始环境清单到终端 +bash scripts/collect-dev-environment.sh --check + +# 写回原始清单 +bash scripts/collect-dev-environment.sh --write + +# 由原始清单生成 AI 决策清单 +python3 scripts/generate-ai-environment.py +``` + +## 维护规则 + +- 目标不是记录“这台机器装了什么”,而是记录“GFramework 开发和 AI 协作实际该用什么”。 +- 新工具只有在满足以下条件之一时才应加入清单: + - 当前仓库构建、测试、文档或验证直接依赖它 + - AI 在当前仓库中会高频使用,且能明显提升效率 + - 新贡献者配置当前仓库开发环境时确实需要知道它 +- 不满足上述条件的工具,不写入文档,也不写入 `.ai/environment/tools.raw.yaml` / `.ai/environment/tools.ai.yaml`。 diff --git a/scripts/collect-dev-environment.sh b/scripts/collect-dev-environment.sh new file mode 100644 index 0000000..af79bb0 --- /dev/null +++ b/scripts/collect-dev-environment.sh @@ -0,0 +1,278 @@ +#!/usr/bin/env bash + +set -euo pipefail + +ROOT_DIR="$(CDPATH='' cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)" +OUTPUT_PATH="${ROOT_DIR}/.ai/environment/tools.raw.yaml" +MODE="${1:---check}" + +usage() { + cat <<'EOF' +Usage: + bash scripts/collect-dev-environment.sh --check + bash scripts/collect-dev-environment.sh --write + +Modes: + --check Print the raw project-relevant environment inventory. + --write Write the raw inventory to .ai/environment/tools.raw.yaml. +EOF +} + +ensure_supported_mode() { + case "${MODE}" in + --check|--write) + ;; + *) + usage + exit 1 + ;; + esac +} + +command_path() { + local tool="$1" + + if command -v "${tool}" >/dev/null 2>&1; then + command -v "${tool}" + else + printf '%s' "" + fi +} + +command_installed() { + local tool="$1" + + if command -v "${tool}" >/dev/null 2>&1; then + printf 'true' + else + printf 'false' + fi +} + +command_version() { + local tool="$1" + + if ! command -v "${tool}" >/dev/null 2>&1; then + printf '%s' "not-installed" + return + fi + + case "${tool}" in + dotnet) + dotnet --version 2>/dev/null || printf '%s' "unknown" + ;; + python3) + python3 --version 2>/dev/null || printf '%s' "unknown" + ;; + node) + node --version 2>/dev/null || printf '%s' "unknown" + ;; + npm) + npm --version 2>/dev/null || printf '%s' "unknown" + ;; + bun) + bun --version 2>/dev/null || printf '%s' "unknown" + ;; + git) + git --version 2>/dev/null || printf '%s' "unknown" + ;; + rg) + rg --version 2>/dev/null | head -n 1 || printf '%s' "unknown" + ;; + jq) + jq --version 2>/dev/null || printf '%s' "unknown" + ;; + docker) + docker --version 2>/dev/null || printf '%s' "unknown" + ;; + bash) + bash --version 2>/dev/null | head -n 1 || printf '%s' "unknown" + ;; + *) + "${tool}" --version 2>/dev/null | head -n 1 || printf '%s' "unknown" + ;; + esac +} + +python_package_version() { + local package_name="$1" + + python3 - "${package_name}" <<'PY' +from importlib import metadata +import sys + +package_name = sys.argv[1] + +try: + print(metadata.version(package_name)) +except metadata.PackageNotFoundError: + print("not-installed") +PY +} + +python_package_installed() { + local package_name="$1" + local version + + version="$(python_package_version "${package_name}")" + + if [[ "${version}" == "not-installed" ]]; then + printf 'false' + else + printf 'true' + fi +} + +read_os_release() { + local key="$1" + + python3 - "$key" <<'PY' +import pathlib +import sys + +target_key = sys.argv[1] +values = {} +for line in pathlib.Path("/etc/os-release").read_text(encoding="utf-8").splitlines(): + if "=" not in line: + continue + key, value = line.split("=", 1) + values[key] = value.strip().strip('"') + +print(values.get(target_key, "unknown")) +PY +} + +collect_inventory() { + local os_name distro version_id kernel shell_name wsl_enabled wsl_version timestamp + + os_name="$(uname -s)" + distro="$(read_os_release PRETTY_NAME)" + version_id="$(read_os_release VERSION_ID)" + kernel="$(uname -r)" + shell_name="$(basename "${SHELL:-bash}")" + timestamp="$(date -u +"%Y-%m-%dT%H:%M:%SZ")" + + if grep -qi microsoft /proc/version 2>/dev/null; then + wsl_enabled="true" + else + wsl_enabled="false" + fi + + if command -v wslinfo >/dev/null 2>&1; then + wsl_version="$(wslinfo --wsl-version 2>/dev/null || printf '%s' "unknown")" + else + wsl_version="unknown" + fi + + cat < "${OUTPUT_PATH}" + printf 'Wrote %s\n' "${OUTPUT_PATH}" +else + collect_inventory +fi + +ensure_supported_mode + +if [[ "${MODE}" == "--write" ]]; then + mkdir -p "$(dirname "${OUTPUT_PATH}")" + collect_inventory > "${OUTPUT_PATH}" + printf 'Wrote %s\n' "${OUTPUT_PATH}" +else + collect_inventory +fi diff --git a/scripts/generate-ai-environment.py b/scripts/generate-ai-environment.py new file mode 100644 index 0000000..4bccbc5 --- /dev/null +++ b/scripts/generate-ai-environment.py @@ -0,0 +1,224 @@ +#!/usr/bin/env python3 + +from __future__ import annotations + +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + + +ROOT_DIR = Path(__file__).resolve().parent.parent +RAW_PATH = ROOT_DIR / ".ai" / "environment" / "tools.raw.yaml" +AI_PATH = ROOT_DIR / ".ai" / "environment" / "tools.ai.yaml" + + +def parse_scalar(value: str) -> Any: + if value == "true": + return True + if value == "false": + return False + if value.startswith('"') and value.endswith('"'): + return value[1:-1] + return value + + +def parse_simple_yaml(path: Path) -> dict[str, Any]: + root: dict[str, Any] = {} + stack: list[tuple[int, dict[str, Any]]] = [(-1, root)] + + for raw_line in path.read_text(encoding="utf-8").splitlines(): + if not raw_line.strip(): + continue + if raw_line.lstrip().startswith("#"): + continue + + indent = len(raw_line) - len(raw_line.lstrip(" ")) + key, _, tail = raw_line.strip().partition(":") + + while len(stack) > 1 and indent <= stack[-1][0]: + stack.pop() + + current = stack[-1][1] + value = tail.strip() + + if value == "": + child: dict[str, Any] = {} + current[key] = child + stack.append((indent, child)) + continue + + current[key] = parse_scalar(value) + + return root + + +def bool_value(data: dict[str, Any], *keys: str) -> bool: + current: Any = data + for key in keys: + current = current[key] + return bool(current) + + +def string_value(data: dict[str, Any], *keys: str) -> str: + current: Any = data + for key in keys: + current = current[key] + return str(current) + + +def choose(preferred: str | None, fallback: str | None) -> str: + if preferred: + return preferred + return fallback or "unavailable" + + +def available_tool(raw: dict[str, Any], section: str, name: str) -> bool: + return bool_value(raw, section, name, "installed") + + +def build_ai_inventory(raw: dict[str, Any]) -> dict[str, Any]: + has_python = available_tool(raw, "required_runtimes", "python3") + has_node = available_tool(raw, "required_runtimes", "node") + has_bun = available_tool(raw, "required_runtimes", "bun") + has_dotnet = available_tool(raw, "required_runtimes", "dotnet") + has_rg = available_tool(raw, "required_tools", "rg") + has_jq = available_tool(raw, "required_tools", "jq") + has_bash = available_tool(raw, "required_tools", "bash") + has_docker = available_tool(raw, "project_tools", "docker") + + search = { + "preferred": choose("rg" if has_rg else None, "grep"), + "fallback": "grep" if has_rg else "unavailable", + "use_for": "Repository text search.", + } + json = { + "preferred": choose("jq" if has_jq else None, "python3" if has_python else None), + "fallback": "python3" if has_jq and has_python else "unavailable", + "use_for": "Inspecting or transforming JSON command output.", + } + scripting = { + "preferred": choose("python3" if has_python else None, "bash" if has_bash else None), + "fallback": "bash" if has_python and has_bash else "unavailable", + "use_for": "Non-trivial local automation and helper scripts.", + } + shell = { + "preferred": choose("bash" if has_bash else None, "sh"), + "fallback": "sh" if has_bash else "unavailable", + "use_for": "Repository shell scripts and command execution.", + } + docs = { + "preferred": choose("bun" if has_bun else None, "npm" if has_node else None), + "fallback": "npm" if has_bun and has_node else "unavailable", + "use_for": "Installing and previewing the docs site.", + } + build = { + "preferred": choose("dotnet" if has_dotnet else None, None), + "fallback": "unavailable", + "use_for": "Build, test, restore, and solution validation.", + } + + if bool_value(raw, "platform", "wsl"): + platform_family = "wsl-linux" + else: + platform_family = string_value(raw, "platform", "os").lower() + + return { + "schema_version": 1, + "generated_at_utc": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"), + "generated_from": ".ai/environment/tools.raw.yaml", + "generator": "scripts/generate-ai-environment.py", + "platform": { + "family": platform_family, + "os": string_value(raw, "platform", "os"), + "distro": string_value(raw, "platform", "distro"), + "shell": string_value(raw, "platform", "shell"), + }, + "capabilities": { + "dotnet": has_dotnet, + "python": has_python, + "node": has_node, + "bun": has_bun, + "docker": has_docker, + "fast_search": has_rg, + "json_cli": has_jq, + }, + "tool_selection": { + "search": search, + "json": json, + "shell": shell, + "scripting": scripting, + "docs_package_manager": docs, + "build_and_test": build, + }, + "python": { + "available": has_python, + "helper_packages": { + "requests": bool_value(raw, "python_packages", "requests", "installed"), + "rich": bool_value(raw, "python_packages", "rich", "installed"), + "openai": bool_value(raw, "python_packages", "openai", "installed"), + "tiktoken": bool_value(raw, "python_packages", "tiktoken", "installed"), + "pydantic": bool_value(raw, "python_packages", "pydantic", "installed"), + "pytest": bool_value(raw, "python_packages", "pytest", "installed"), + }, + }, + "preferences": { + "prefer_project_listed_tools": True, + "prefer_python_for_non_trivial_automation": has_python, + "avoid_unlisted_system_tools": True, + }, + "rules": [ + "Use rg instead of grep for repository search when rg is available.", + "Use jq for JSON inspection; fall back to python3 if jq is unavailable.", + "Prefer python3 over complex bash for non-trivial scripting when python3 is available.", + "Use bun for docs preview workflows when bun is available; otherwise fall back to npm.", + "Use dotnet for repository build and test workflows.", + "Do not assume unrelated system tools are part of the supported project environment.", + ], + } + + +def emit_yaml(value: Any, indent: int = 0) -> list[str]: + prefix = " " * indent + + if isinstance(value, dict): + lines: list[str] = [] + for key, nested in value.items(): + if isinstance(nested, (dict, list)): + lines.append(f"{prefix}{key}:") + lines.extend(emit_yaml(nested, indent + 2)) + else: + lines.append(f"{prefix}{key}: {format_scalar(nested)}") + return lines + + if isinstance(value, list): + lines = [] + for item in value: + if isinstance(item, (dict, list)): + lines.append(f"{prefix}-") + lines.extend(emit_yaml(item, indent + 2)) + else: + lines.append(f"{prefix}- {format_scalar(item)}") + return lines + + return [f"{prefix}{format_scalar(value)}"] + + +def format_scalar(value: Any) -> str: + if isinstance(value, bool): + return "true" if value else "false" + if isinstance(value, int): + return str(value) + text = str(value).replace('"', '\\"') + return f'"{text}"' + + +def main() -> None: + raw = parse_simple_yaml(RAW_PATH) + ai_inventory = build_ai_inventory(raw) + AI_PATH.parent.mkdir(parents=True, exist_ok=True) + AI_PATH.write_text("\n".join(emit_yaml(ai_inventory)) + "\n", encoding="utf-8") + print(f"Wrote {AI_PATH}") + + +if __name__ == "__main__": + main()