From 7288114e33d48f0f0d8084f435f897f8b11ec2d0 Mon Sep 17 00:00:00 2001 From: gewuyou <95328647+GeWuYou@users.noreply.github.com> Date: Tue, 5 May 2026 12:39:10 +0800 Subject: [PATCH] =?UTF-8?q?fix(game):=20=E5=89=A5=E7=A6=BB=E8=BF=90?= =?UTF-8?q?=E8=A1=8C=E6=97=B6=E6=A8=A1=E5=9D=97=E5=AF=B9=E7=94=9F=E6=88=90?= =?UTF-8?q?=E5=99=A8=E4=BE=9D=E8=B5=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修复 GFramework.Game 对 SourceGenerators.Abstractions 的项目引用并移除未使用的枚举生成 attribute - 新增 runtime-generator 边界校验脚本并接入 CI 与发布打包校验 - 更新 AGENTS、贡献文档与 ai-plan 跟踪,明确运行时模块禁止依赖生成器能力 --- .github/workflows/ci.yml | 3 + .github/workflows/publish.yml | 3 + AGENTS.md | 3 + .../Config/YamlConfigSchemaPropertyType.cs | 3 - .../Config/YamlConfigStringFormatKind.cs | 3 - GFramework.Game/GFramework.Game.csproj | 1 - ai-plan/public/README.md | 7 + .../runtime-generator-boundary-tracking.md | 40 +++ .../runtime-generator-boundary-trace.md | 21 ++ docs/zh-CN/contributing.md | 8 + .../validate-runtime-generator-boundaries.py | 286 ++++++++++++++++++ 11 files changed, 371 insertions(+), 7 deletions(-) create mode 100644 ai-plan/public/runtime-generator-boundary/todos/runtime-generator-boundary-tracking.md create mode 100644 ai-plan/public/runtime-generator-boundary/traces/runtime-generator-boundary-trace.md create mode 100644 scripts/validate-runtime-generator-boundaries.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7484aa96..644e5cd4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,6 +35,9 @@ jobs: - name: Validate license headers run: python3 scripts/license-header.py --check + - name: Validate runtime-generator boundaries + run: python3 scripts/validate-runtime-generator-boundaries.py + # 缓存MegaLinter - name: Cache MegaLinter uses: actions/cache@v5 diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index c9ba8930..500ac980 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -118,6 +118,9 @@ jobs: diff -u expected-packages.txt actual-packages.txt + - name: Validate runtime-generator package boundaries + run: python3 scripts/validate-runtime-generator-boundaries.py --package-dir ./packages + - name: Show packages run: ls -la ./packages || true diff --git a/AGENTS.md b/AGENTS.md index 55f4cc59..80e007a3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -212,6 +212,9 @@ All generated or modified code MUST include clear and meaningful comments where - Private fields: `_camelCase` - Keep abstractions projects free of implementation details and engine-specific dependencies. - Preserve existing module boundaries. Do not introduce new cross-module dependencies without clear architectural need. +- Framework runtime, abstractions, and meta-package projects MUST NOT reference `*.SourceGenerators*` projects or packages, + and MUST NOT use source-generator attributes such as `GenerateEnumExtensions` or `ContextAware`. Those capabilities are + reserved for consumer projects, generator projects, examples explicitly meant to demonstrate generator usage, and related tests. ### Formatting diff --git a/GFramework.Game/Config/YamlConfigSchemaPropertyType.cs b/GFramework.Game/Config/YamlConfigSchemaPropertyType.cs index af27219e..04bb0a43 100644 --- a/GFramework.Game/Config/YamlConfigSchemaPropertyType.cs +++ b/GFramework.Game/Config/YamlConfigSchemaPropertyType.cs @@ -1,14 +1,11 @@ // Copyright (c) 2025-2026 GeWuYou // SPDX-License-Identifier: Apache-2.0 -using GFramework.Core.SourceGenerators.Abstractions.Enums; - namespace GFramework.Game.Config; /// /// 表示当前运行时 schema 校验器支持的属性类型。 /// -[GenerateEnumExtensions] internal enum YamlConfigSchemaPropertyType { /// diff --git a/GFramework.Game/Config/YamlConfigStringFormatKind.cs b/GFramework.Game/Config/YamlConfigStringFormatKind.cs index 7233e88a..3b211933 100644 --- a/GFramework.Game/Config/YamlConfigStringFormatKind.cs +++ b/GFramework.Game/Config/YamlConfigStringFormatKind.cs @@ -1,14 +1,11 @@ // Copyright (c) 2025-2026 GeWuYou // SPDX-License-Identifier: Apache-2.0 -using GFramework.Core.SourceGenerators.Abstractions.Enums; - namespace GFramework.Game.Config; /// /// 表示当前 Runtime / Generator / Tooling 共享支持的字符串 format 子集。 /// -[GenerateEnumExtensions] internal enum YamlConfigStringFormatKind { /// diff --git a/GFramework.Game/GFramework.Game.csproj b/GFramework.Game/GFramework.Game.csproj index 6d2e3a10..add4ad19 100644 --- a/GFramework.Game/GFramework.Game.csproj +++ b/GFramework.Game/GFramework.Game.csproj @@ -14,7 +14,6 @@ true - diff --git a/ai-plan/public/README.md b/ai-plan/public/README.md index 139061a5..b4fb1655 100644 --- a/ai-plan/public/README.md +++ b/ai-plan/public/README.md @@ -42,6 +42,10 @@ help the current worktree land on the right recovery documents without scanning - Purpose: migrate release version calculation from fixed patch bumps to semantic-release while keeping the existing tag-driven NuGet publish flow. - Tracking: `ai-plan/public/semantic-release-versioning/todos/semantic-release-versioning-tracking.md` - Trace: `ai-plan/public/semantic-release-versioning/traces/semantic-release-versioning-trace.md` +- `runtime-generator-boundary` + - Purpose: keep runtime and abstractions packages isolated from source-generator dependencies, packaging leaks, and attribute usage. + - Tracking: `ai-plan/public/runtime-generator-boundary/todos/runtime-generator-boundary-tracking.md` + - Trace: `ai-plan/public/runtime-generator-boundary/traces/runtime-generator-boundary-trace.md` ## Worktree To Active Topic Map @@ -68,6 +72,9 @@ help the current worktree land on the right recovery documents without scanning - Branch: `fix/release-notes-pr-links` - Worktree hint: `GFramework` - Priority 1: `semantic-release-versioning` +- Branch: `fix/runtime-generator-boundary` + - Worktree hint: `GFramework` + - Priority 1: `runtime-generator-boundary` - Branch: `docs/sdk-update-documentation` - Worktree hint: `GFramework-update-documentation` - Priority 1: `documentation-full-coverage-governance` diff --git a/ai-plan/public/runtime-generator-boundary/todos/runtime-generator-boundary-tracking.md b/ai-plan/public/runtime-generator-boundary/todos/runtime-generator-boundary-tracking.md new file mode 100644 index 00000000..cf81840e --- /dev/null +++ b/ai-plan/public/runtime-generator-boundary/todos/runtime-generator-boundary-tracking.md @@ -0,0 +1,40 @@ +# Runtime / Generator Boundary Tracking + +## Goal + +Keep runtime, abstractions, and meta-package modules free from source-generator project references, source-generator +attributes, and leaked NuGet dependencies. + +## Current Recovery Point + +- Recovery point: RGB-RP-001 +- Phase: remove `GFramework.Game` generator coupling and add repository guardrails +- Focus: + - delete `GFramework.Game`'s dependency on `GFramework.Core.SourceGenerators.Abstractions` + - remove unused `[GenerateEnumExtensions]` usage from `GFramework.Game` + - add static and packed-package validation so runtime packages cannot regress + +## Active Risks + +- A runtime package can still compile locally if it references a non-packable generator helper project, so regressions are + easy to miss without an explicit guard. +- A leaked package dependency may only surface when a consumer restores from NuGet, not during normal repository builds. + +## Completed In This Stage + +- Confirmed `GFramework.Game` was the direct runtime offender and `GeWuYou.GFramework.Game` leaked + `GFramework.Core.SourceGenerators.Abstractions` into its nuspec dependency graph. +- Confirmed the two `[GenerateEnumExtensions]` usages inside `GFramework.Game` do not need generated output and can be + removed outright. + +## Validation Target + +- `python3 scripts/validate-runtime-generator-boundaries.py` +- `dotnet build GFramework.Game/GFramework.Game.csproj -c Release` +- `dotnet build GFramework.Godot/GFramework.Godot.csproj -c Release` +- `dotnet pack GFramework.sln -c Release -p:PackageVersion=` + +## Next Recommended Resume Step + +Run the new boundary validator plus minimal Release build and pack validation, then inspect `GeWuYou.GFramework.Game` +and transitive runtime packages to confirm no `SourceGenerators` dependency remains. diff --git a/ai-plan/public/runtime-generator-boundary/traces/runtime-generator-boundary-trace.md b/ai-plan/public/runtime-generator-boundary/traces/runtime-generator-boundary-trace.md new file mode 100644 index 00000000..60ceb84c --- /dev/null +++ b/ai-plan/public/runtime-generator-boundary/traces/runtime-generator-boundary-trace.md @@ -0,0 +1,21 @@ +# Runtime / Generator Boundary Trace + +## 2026-05-05 + +### RGB-RP-001 Runtime package boundary repair + +- Trigger: + - external consumers restoring `GeWuYou.GFramework.Game` failed because NuGet looked for + `GFramework.Core.SourceGenerators.Abstractions` + - repository inspection showed `GFramework.Game` had a direct project reference to a non-packable generator + abstractions project and used `[GenerateEnumExtensions]` +- Decisions: + - treat the issue as a runtime/generator boundary violation, not as a missing publish target + - remove the runtime-side attribute usage instead of turning generator abstractions into public runtime packages + - add repository guardrails at both source-validation time and packed-package validation time +- Expected implementation: + - `GFramework.Game` removes the generator abstractions project reference + - `GFramework.Game` removes the two unused enum generator attributes + - CI and publish workflows run a dedicated boundary validator script +- Immediate next step: + - complete implementation and run Release build plus pack verification diff --git a/docs/zh-CN/contributing.md b/docs/zh-CN/contributing.md index ee760bcc..1628bed1 100644 --- a/docs/zh-CN/contributing.md +++ b/docs/zh-CN/contributing.md @@ -218,6 +218,14 @@ git push origin your-branch ### 命名规范 +### 代码生成器边界 + +- 框架内部的运行时模块、抽象层模块和顶层元包不得依赖 `*.SourceGenerators*` 项目或包。 +- `GenerateEnumExtensions`、`ContextAware`、`GetModel`、`GetService` 等代码生成器 attribute 只允许出现在消费端项目、 + 生成器项目本身、专门验证生成器行为的测试项目,或明确用于演示生成器接入的示例中。 +- 如果某个运行时模块为了编译而需要引入生成器 attribute,说明模块边界已经漂移;应优先移除该 attribute 使用,而不是把 + generator abstractions 暴露成新的运行时依赖。 + 遵循 C# 标准命名约定: - **类、接口、方法**:PascalCase diff --git a/scripts/validate-runtime-generator-boundaries.py b/scripts/validate-runtime-generator-boundaries.py new file mode 100644 index 00000000..75924d66 --- /dev/null +++ b/scripts/validate-runtime-generator-boundaries.py @@ -0,0 +1,286 @@ +#!/usr/bin/env python3 +# Copyright (c) 2025-2026 GeWuYou +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +import argparse +import re +import sys +import zipfile +import xml.etree.ElementTree as ET +from dataclasses import dataclass +from pathlib import Path + + +REPO_ROOT = Path(__file__).resolve().parent.parent + +RUNTIME_PROJECTS = ( + "GFramework", + "GFramework.Core", + "GFramework.Core.Abstractions", + "GFramework.Cqrs", + "GFramework.Cqrs.Abstractions", + "GFramework.Game", + "GFramework.Game.Abstractions", + "GFramework.Godot", + "GFramework.Ecs.Arch", + "GFramework.Ecs.Arch.Abstractions", +) + +FORBIDDEN_ATTRIBUTE_NAMES = ( + "GenerateEnumExtensions", + "ContextAware", + "GetModel", + "GetModels", + "GetSystem", + "GetSystems", + "GetUtility", + "GetUtilities", + "GetService", + "GetServices", + "GetAll", + "Log", + "Priority", +) + +FORBIDDEN_PROJECT_REFERENCE_PREFIX = "GFramework." +FORBIDDEN_PACKAGE_REFERENCE_PREFIX = "GeWuYou.GFramework." +PACKAGE_NAMESPACE = "http://schemas.microsoft.com/packaging/2013/05/nuspec.xsd" +PACKAGE_NAMESPACE_2012 = "http://schemas.microsoft.com/packaging/2012/06/nuspec.xsd" + + +@dataclass(frozen=True) +class Violation: + location: str + message: str + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser( + description="Validate that runtime and abstractions modules do not depend on source-generator packages or attributes." + ) + parser.add_argument( + "--package-dir", + type=Path, + help="Optional package directory. When supplied, validate packed runtime nuspec dependencies as well.", + ) + return parser.parse_args() + + +def get_project_file(project_name: str) -> Path: + if project_name == "GFramework": + return REPO_ROOT / "GFramework.csproj" + + return REPO_ROOT / project_name / f"{project_name}.csproj" + + +def get_source_root(project_name: str) -> Path | None: + if project_name == "GFramework": + return None + + return REPO_ROOT / project_name + + +def get_local_name(tag: str) -> str: + return tag.split("}", 1)[-1] + + +def get_xml_text(element: ET.Element) -> str: + return (element.text or "").strip() + + +def get_runtime_package_ids() -> set[str]: + package_ids: set[str] = set() + for project_name in RUNTIME_PROJECTS: + project_file = get_project_file(project_name) + tree = ET.parse(project_file) + root = tree.getroot() + package_id = None + assembly_name = None + + for element in root.iter(): + local_name = get_local_name(element.tag) + if local_name == "PackageId" and not package_id: + package_id = get_xml_text(element) + elif local_name == "AssemblyName" and not assembly_name: + assembly_name = get_xml_text(element) + + resolved_package_id = package_id or f"GeWuYou.{assembly_name or project_name}" + package_ids.add(resolved_package_id) + + return package_ids + + +def validate_project_references() -> list[Violation]: + violations: list[Violation] = [] + + for project_name in RUNTIME_PROJECTS: + project_file = get_project_file(project_name) + tree = ET.parse(project_file) + + for element in tree.getroot().iter(): + local_name = get_local_name(element.tag) + if local_name not in {"ProjectReference", "PackageReference"}: + continue + + include = element.attrib.get("Include", "").strip() + if local_name == "ProjectReference": + if FORBIDDEN_PROJECT_REFERENCE_PREFIX not in include or "SourceGenerators" not in include: + continue + else: + if not include.startswith(FORBIDDEN_PACKAGE_REFERENCE_PREFIX) or "SourceGenerators" not in include: + continue + + violations.append( + Violation( + location=str(project_file.relative_to(REPO_ROOT)), + message=f"forbidden {local_name} -> {include}", + ) + ) + + return violations + + +def compile_attribute_patterns() -> dict[str, re.Pattern[str]]: + patterns: dict[str, re.Pattern[str]] = {} + for attribute_name in FORBIDDEN_ATTRIBUTE_NAMES: + patterns[attribute_name] = re.compile( + rf"\[(?P[^\]]*?(?:^|[\s,(])(?:global::)?(?:[A-Za-z_][A-Za-z0-9_]*\.)*{attribute_name}(?:Attribute)?(?=[\s,)\]])[^\]]*)\]", + re.MULTILINE, + ) + + return patterns + + +def line_number_for_offset(text: str, offset: int) -> int: + return text.count("\n", 0, offset) + 1 + + +def validate_source_attributes() -> list[Violation]: + violations: list[Violation] = [] + patterns = compile_attribute_patterns() + + for project_name in RUNTIME_PROJECTS: + source_root = get_source_root(project_name) + if source_root is None or not source_root.is_dir(): + continue + + for file_path in source_root.rglob("*.cs"): + if any(part in {"bin", "obj"} for part in file_path.parts): + continue + + text = file_path.read_text(encoding="utf-8-sig") + for attribute_name, pattern in patterns.items(): + for match in pattern.finditer(text): + line_number = line_number_for_offset(text, match.start()) + relative_path = file_path.relative_to(REPO_ROOT) + violations.append( + Violation( + location=f"{relative_path}:{line_number}", + message=f"forbidden source-generator attribute [{attribute_name}] in runtime module", + ) + ) + + return violations + + +def iter_dependency_ids(nuspec_root: ET.Element) -> list[str]: + dependency_ids: list[str] = [] + for namespace in (PACKAGE_NAMESPACE, PACKAGE_NAMESPACE_2012): + dependency_ids.extend( + element.attrib["id"] + for element in nuspec_root.findall(f".//{{{namespace}}}dependency") + if "id" in element.attrib + ) + + if dependency_ids: + return dependency_ids + + dependency_ids.extend( + element.attrib["id"] + for element in nuspec_root.findall(".//dependency") + if "id" in element.attrib + ) + return dependency_ids + + +def validate_packed_dependencies(package_dir: Path) -> list[Violation]: + violations: list[Violation] = [] + runtime_package_ids = get_runtime_package_ids() + + for package_path in sorted(package_dir.glob("*.nupkg")): + with zipfile.ZipFile(package_path) as archive: + nuspec_entries = [name for name in archive.namelist() if name.endswith(".nuspec")] + if not nuspec_entries: + violations.append( + Violation( + location=str(package_path.relative_to(REPO_ROOT if package_path.is_relative_to(REPO_ROOT) else package_dir.parent)), + message="missing nuspec entry", + ) + ) + continue + + nuspec_root = ET.fromstring(archive.read(nuspec_entries[0])) + + package_id_element = nuspec_root.find(f".//{{{PACKAGE_NAMESPACE}}}id") + if package_id_element is None: + package_id_element = nuspec_root.find(f".//{{{PACKAGE_NAMESPACE_2012}}}id") + if package_id_element is None: + package_id_element = nuspec_root.find(".//id") + + package_id = get_xml_text(package_id_element) if package_id_element is not None else package_path.stem + if package_id not in runtime_package_ids: + continue + + dependency_ids = iter_dependency_ids(nuspec_root) + for dependency_id in dependency_ids: + if not dependency_id.startswith(FORBIDDEN_PACKAGE_REFERENCE_PREFIX) and not dependency_id.startswith( + FORBIDDEN_PROJECT_REFERENCE_PREFIX + ): + continue + + if "SourceGenerators" not in dependency_id: + continue + + violations.append( + Violation( + location=str(package_path), + message=f"runtime package {package_id} depends on forbidden package {dependency_id}", + ) + ) + + return violations + + +def print_violations(violations: list[Violation]) -> None: + for violation in violations: + print(f"- {violation.location}: {violation.message}") + + +def main() -> int: + args = parse_args() + + violations: list[Violation] = [] + violations.extend(validate_project_references()) + violations.extend(validate_source_attributes()) + + if args.package_dir is not None: + package_dir = args.package_dir if args.package_dir.is_absolute() else REPO_ROOT / args.package_dir + if not package_dir.is_dir(): + print(f"Package directory does not exist: {package_dir}", file=sys.stderr) + return 2 + + violations.extend(validate_packed_dependencies(package_dir)) + + if violations: + print("Runtime/source-generator boundary validation failed.") + print_violations(violations) + return 1 + + print("Runtime/source-generator boundary validation passed.") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())